View.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. import { assignTo } from './util/object'
  2. import { parseFieldSpecs } from './util/misc'
  3. import DateProfileGenerator, { DateProfile } from './DateProfileGenerator'
  4. import { DateMarker, addMs } from './datelib/marker'
  5. import { createDuration, Duration } from './datelib/duration'
  6. import { default as EmitterMixin, EmitterInterface } from './common/EmitterMixin'
  7. import { ViewSpec } from './structs/view-spec'
  8. import { createElement } from './util/dom-manip'
  9. import { ComponentContext } from './component/Component'
  10. import DateComponent from './component/DateComponent'
  11. import { EventStore } from './structs/event-store'
  12. import { EventUiHash } from './component/event-ui'
  13. import { sliceEventStore, EventRenderRange } from './component/event-rendering'
  14. import { DateSpan } from './structs/date-span'
  15. import { EventInteractionState } from './interactions/event-interaction-state'
  16. import { memoizeRendering } from './component/memoized-rendering'
  17. export interface ViewProps {
  18. dateProfile: DateProfile
  19. businessHours: EventStore
  20. eventStore: EventStore
  21. eventUiBases: EventUiHash
  22. dateSelection: DateSpan | null
  23. eventSelection: string
  24. eventDrag: EventInteractionState | null
  25. eventResize: EventInteractionState | null
  26. }
  27. export default abstract class View extends DateComponent<ViewProps> {
  28. // config properties, initialized after class on prototype
  29. usesMinMaxTime: boolean // whether minTime/maxTime will affect the activeRange. Views must opt-in.
  30. dateProfileGeneratorClass: any // initialized after class. used by Calendar
  31. on: EmitterInterface['on']
  32. one: EmitterInterface['one']
  33. off: EmitterInterface['off']
  34. trigger: EmitterInterface['trigger']
  35. triggerWith: EmitterInterface['triggerWith']
  36. hasHandlers: EmitterInterface['hasHandlers']
  37. viewSpec: ViewSpec
  38. dateProfileGenerator: DateProfileGenerator
  39. type: string // subclass' view name (string). for the API
  40. title: string // the text that will be displayed in the header's title. SET BY CALLER for API
  41. queuedScroll: any
  42. eventOrderSpecs: any // criteria for ordering events when they have same date/time
  43. nextDayThreshold: Duration
  44. // now indicator
  45. isNowIndicatorRendered: boolean
  46. initialNowDate: DateMarker // result first getNow call
  47. initialNowQueriedMs: number // ms time the getNow was called
  48. nowIndicatorTimeoutID: any // for refresh timing of now indicator
  49. nowIndicatorIntervalID: any // "
  50. private renderDatesMem = memoizeRendering(this.renderDatesWrap, this.unrenderDatesWrap)
  51. private renderBusinessHoursMem = memoizeRendering(this.renderBusinessHours, this.unrenderBusinessHours, [ this.renderDatesMem ])
  52. private renderDateSelectionMem = memoizeRendering(this.renderDateSelectionWrap, this.unrenderDateSelectionWrap, [ this.renderDatesMem ])
  53. private renderEventsMem = memoizeRendering(this.renderEvents, this.unrenderEvents, [ this.renderDatesMem ])
  54. private renderEventSelectionMem = memoizeRendering(this.renderEventSelectionWrap, this.unrenderEventSelectionWrap, [ this.renderEventsMem ])
  55. private renderEventDragMem = memoizeRendering(this.renderEventDragWrap, this.unrenderEventDragWrap, [ this.renderDatesMem ])
  56. private renderEventResizeMem = memoizeRendering(this.renderEventResizeWrap, this.unrenderEventResizeWrap, [ this.renderDatesMem ])
  57. constructor(context: ComponentContext, viewSpec: ViewSpec, dateProfileGenerator: DateProfileGenerator, parentEl: HTMLElement) {
  58. super(
  59. {
  60. options: context.options,
  61. dateEnv: context.dateEnv,
  62. theme: context.theme,
  63. calendar: context.calendar
  64. },
  65. createElement('div', { className: 'fc-view fc-' + viewSpec.type + '-view' })
  66. )
  67. this.context.view = this // for when passing context to children
  68. this.viewSpec = viewSpec
  69. this.dateProfileGenerator = dateProfileGenerator
  70. this.type = viewSpec.type
  71. this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder'))
  72. this.nextDayThreshold = createDuration(this.opt('nextDayThreshold'))
  73. parentEl.appendChild(this.el)
  74. this.initialize()
  75. }
  76. initialize() { // convenient for sublcasses
  77. }
  78. // Date Setting/Unsetting
  79. // -----------------------------------------------------------------------------------------------------------------
  80. get activeStart(): Date {
  81. return this.dateEnv.toDate(this.props.dateProfile.activeRange.start)
  82. }
  83. get activeEnd(): Date {
  84. return this.dateEnv.toDate(this.props.dateProfile.activeRange.end)
  85. }
  86. get currentStart(): Date {
  87. return this.dateEnv.toDate(this.props.dateProfile.currentRange.start)
  88. }
  89. get currentEnd(): Date {
  90. return this.dateEnv.toDate(this.props.dateProfile.currentRange.end)
  91. }
  92. // General Rendering
  93. // -----------------------------------------------------------------------------------------------------------------
  94. render(props: ViewProps) {
  95. this.renderDatesMem(props.dateProfile)
  96. this.renderBusinessHoursMem(props.businessHours)
  97. this.renderDateSelectionMem(props.dateSelection)
  98. this.renderEventsMem(props.eventStore)
  99. this.renderEventSelectionMem(props.eventSelection)
  100. this.renderEventDragMem(props.eventDrag)
  101. this.renderEventResizeMem(props.eventResize)
  102. }
  103. destroy() {
  104. super.destroy()
  105. this.renderDatesMem.unrender() // should unrender everything else
  106. }
  107. // Sizing
  108. // -----------------------------------------------------------------------------------------------------------------
  109. updateSize(isResize: boolean, viewHeight: number, isAuto: boolean) {
  110. let { calendar } = this
  111. if (isResize || calendar.isViewUpdated || calendar.isDatesUpdated || calendar.isEventsUpdated) {
  112. // sort of the catch-all sizing
  113. // anything that might cause dimension changes
  114. this.updateBaseSize(isResize, viewHeight, isAuto)
  115. }
  116. }
  117. updateBaseSize(isResize: boolean, viewHeight: number, isAuto: boolean) {
  118. }
  119. // Date Rendering
  120. // -----------------------------------------------------------------------------------------------------------------
  121. renderDatesWrap(dateProfile: DateProfile) {
  122. this.renderDates(dateProfile)
  123. this.addScroll({ isDateInit: true })
  124. this.startNowIndicator() // shouldn't render yet because updateSize will be called soon
  125. }
  126. unrenderDatesWrap() {
  127. this.stopNowIndicator()
  128. this.unrenderDates()
  129. }
  130. renderDates(dateProfile: DateProfile) {}
  131. unrenderDates() {}
  132. // Business Hours
  133. // -----------------------------------------------------------------------------------------------------------------
  134. renderBusinessHours(businessHours: EventStore) {}
  135. unrenderBusinessHours() {}
  136. // Date Selection
  137. // -----------------------------------------------------------------------------------------------------------------
  138. renderDateSelectionWrap(selection: DateSpan) {
  139. if (selection) {
  140. this.renderDateSelection(selection)
  141. }
  142. }
  143. unrenderDateSelectionWrap(selection: DateSpan) {
  144. if (selection) {
  145. this.unrenderDateSelection(selection)
  146. }
  147. }
  148. renderDateSelection(selection: DateSpan) {}
  149. unrenderDateSelection(selection: DateSpan) {}
  150. // Event Rendering
  151. // -----------------------------------------------------------------------------------------------------------------
  152. renderEvents(eventStore: EventStore) {}
  153. unrenderEvents() {}
  154. // util for subclasses
  155. sliceEvents(eventStore: EventStore, allDay: boolean): EventRenderRange[] {
  156. let { props } = this
  157. return sliceEventStore(
  158. eventStore,
  159. props.eventUiBases,
  160. props.dateProfile.activeRange,
  161. allDay ? this.nextDayThreshold : null
  162. ).fg
  163. }
  164. // Event Selection
  165. // -----------------------------------------------------------------------------------------------------------------
  166. renderEventSelectionWrap(instanceId: string) {
  167. if (instanceId) {
  168. this.renderEventSelection(instanceId)
  169. }
  170. }
  171. unrenderEventSelectionWrap(instanceId: string) {
  172. if (instanceId) {
  173. this.unrenderEventSelection(instanceId)
  174. }
  175. }
  176. renderEventSelection(instanceId: string) {}
  177. unrenderEventSelection(instanceId: string) {}
  178. // Event Drag
  179. // -----------------------------------------------------------------------------------------------------------------
  180. renderEventDragWrap(state: EventInteractionState) {
  181. if (state) {
  182. this.renderEventDrag(state)
  183. }
  184. }
  185. unrenderEventDragWrap(state: EventInteractionState) {
  186. if (state) {
  187. this.unrenderEventDrag(state)
  188. }
  189. }
  190. renderEventDrag(state: EventInteractionState) {}
  191. unrenderEventDrag(state: EventInteractionState) {}
  192. // Event Resize
  193. // -----------------------------------------------------------------------------------------------------------------
  194. renderEventResizeWrap(state: EventInteractionState) {
  195. if (state) {
  196. this.renderEventResize(state)
  197. }
  198. }
  199. unrenderEventResizeWrap(state: EventInteractionState) {
  200. if (state) {
  201. this.unrenderEventResize(state)
  202. }
  203. }
  204. renderEventResize(state: EventInteractionState) {}
  205. unrenderEventResize(state: EventInteractionState) {}
  206. /* Now Indicator
  207. ------------------------------------------------------------------------------------------------------------------*/
  208. // Immediately render the current time indicator and begins re-rendering it at an interval,
  209. // which is defined by this.getNowIndicatorUnit().
  210. // TODO: somehow do this for the current whole day's background too
  211. startNowIndicator() {
  212. let { dateEnv } = this
  213. let unit
  214. let update
  215. let delay // ms wait value
  216. if (this.opt('nowIndicator')) {
  217. unit = this.getNowIndicatorUnit()
  218. if (unit) {
  219. update = this.updateNowIndicator.bind(this)
  220. this.initialNowDate = this.calendar.getNow()
  221. this.initialNowQueriedMs = new Date().valueOf()
  222. // wait until the beginning of the next interval
  223. delay = dateEnv.add(
  224. dateEnv.startOf(this.initialNowDate, unit),
  225. createDuration(1, unit)
  226. ).valueOf() - this.initialNowDate.valueOf()
  227. // TODO: maybe always use setTimeout, waiting until start of next unit
  228. this.nowIndicatorTimeoutID = setTimeout(() => {
  229. this.nowIndicatorTimeoutID = null
  230. update()
  231. if (unit === 'second') {
  232. delay = 1000 // every second
  233. } else {
  234. delay = 1000 * 60 // otherwise, every minute
  235. }
  236. this.nowIndicatorIntervalID = setInterval(update, delay) // update every interval
  237. }, delay)
  238. }
  239. // rendering will be initiated in updateSize
  240. }
  241. }
  242. // rerenders the now indicator, computing the new current time from the amount of time that has passed
  243. // since the initial getNow call.
  244. updateNowIndicator() {
  245. if (
  246. this.props.dateProfile && // a way to determine if dates were rendered yet
  247. this.initialNowDate // activated before?
  248. ) {
  249. this.unrenderNowIndicator() // won't unrender if unnecessary
  250. this.renderNowIndicator(
  251. addMs(this.initialNowDate, new Date().valueOf() - this.initialNowQueriedMs)
  252. )
  253. this.isNowIndicatorRendered = true
  254. }
  255. }
  256. // Immediately unrenders the view's current time indicator and stops any re-rendering timers.
  257. // Won't cause side effects if indicator isn't rendered.
  258. stopNowIndicator() {
  259. if (this.isNowIndicatorRendered) {
  260. if (this.nowIndicatorTimeoutID) {
  261. clearTimeout(this.nowIndicatorTimeoutID)
  262. this.nowIndicatorTimeoutID = null
  263. }
  264. if (this.nowIndicatorIntervalID) {
  265. clearInterval(this.nowIndicatorIntervalID)
  266. this.nowIndicatorIntervalID = null
  267. }
  268. this.unrenderNowIndicator()
  269. this.isNowIndicatorRendered = false
  270. }
  271. }
  272. getNowIndicatorUnit() {
  273. // subclasses should implement
  274. }
  275. // Renders a current time indicator at the given datetime
  276. renderNowIndicator(date) {
  277. // SUBCLASSES MUST PASS TO CHILDREN!
  278. }
  279. // Undoes the rendering actions from renderNowIndicator
  280. unrenderNowIndicator() {
  281. // SUBCLASSES MUST PASS TO CHILDREN!
  282. }
  283. /* Scroller
  284. ------------------------------------------------------------------------------------------------------------------*/
  285. addScroll(scroll) {
  286. let queuedScroll = this.queuedScroll || (this.queuedScroll = {})
  287. assignTo(queuedScroll, scroll)
  288. }
  289. popScroll() {
  290. this.applyQueuedScroll()
  291. this.queuedScroll = null
  292. }
  293. applyQueuedScroll() {
  294. this.applyScroll(this.queuedScroll || {})
  295. }
  296. queryScroll() {
  297. let scroll = {} as any
  298. if (this.props.dateProfile) { // dates rendered yet?
  299. assignTo(scroll, this.queryDateScroll())
  300. }
  301. return scroll
  302. }
  303. applyScroll(scroll) {
  304. if (scroll.isDateInit) {
  305. delete scroll.isDateInit
  306. if (this.props.dateProfile) { // dates rendered yet?
  307. assignTo(scroll, this.computeInitialDateScroll())
  308. }
  309. }
  310. if (this.props.dateProfile) { // dates rendered yet?
  311. this.applyDateScroll(scroll)
  312. }
  313. }
  314. computeInitialDateScroll() {
  315. return {} // subclasses must implement
  316. }
  317. queryDateScroll() {
  318. return {} // subclasses must implement
  319. }
  320. applyDateScroll(scroll) {
  321. // subclasses must implement
  322. }
  323. }
  324. EmitterMixin.mixInto(View)
  325. View.prototype.usesMinMaxTime = false
  326. View.prototype.dateProfileGeneratorClass = DateProfileGenerator