DateComponent.ts 27 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030
  1. import { attrsToStr, htmlEscape } from '../util/html'
  2. import { elementClosest } from '../util/dom-manip'
  3. import { default as Component, RenderForceFlags } from './Component'
  4. import Calendar from '../Calendar'
  5. import View from '../View'
  6. import { DateProfile } from '../DateProfileGenerator'
  7. import { DateMarker, DAY_IDS, addDays, startOfDay, diffDays, diffWholeDays } from '../datelib/marker'
  8. import { Duration, createDuration, asRoughMs } from '../datelib/duration'
  9. import { DateSpan } from '../reducers/date-span'
  10. import UnzonedRange from '../models/UnzonedRange'
  11. import { EventRenderRange, sliceEventStore } from '../reducers/event-rendering'
  12. import { EventStore } from '../reducers/event-store'
  13. import { BusinessHourDef, buildBusinessHourEventStore } from '../reducers/business-hours'
  14. import { DateEnv } from '../datelib/env'
  15. import Theme from '../theme/Theme'
  16. import { EventInteractionState } from '../reducers/event-interaction'
  17. import { assignTo } from '../util/object'
  18. import browserContext from '../common/browser-context'
  19. import { Hit } from '../interactions/HitDragging'
  20. export interface DateComponentRenderState {
  21. dateProfile: DateProfile
  22. eventStore: EventStore
  23. selection: DateSpan | null
  24. dragState: EventInteractionState | null
  25. eventResizeState: EventInteractionState | null
  26. businessHoursDef: BusinessHourDef // BusinessHourDef's `false` is the empty state
  27. selectedEventInstanceId: string | null
  28. }
  29. // NOTE: for fg-events, eventRange.range is NOT sliced,
  30. // thus, we need isStart/isEnd
  31. export interface Seg {
  32. component: DateComponent
  33. isStart: boolean
  34. isEnd: boolean
  35. eventRange?: EventRenderRange
  36. el?: HTMLElement
  37. [otherProp: string]: any
  38. }
  39. export type DateComponentHash = { [id: string]: DateComponent }
  40. let uid = 0
  41. export default abstract class DateComponent extends Component {
  42. // self-config, overridable by subclasses
  43. isInteractable: boolean = false
  44. segSelector: string = '.fc-event-container > *' // what constitutes an event element?
  45. // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity
  46. // of the date areas. if not defined, assumes to be day and time granularity.
  47. // TODO: port isTimeScale into same system?
  48. largeUnit: any
  49. eventRendererClass: any
  50. helperRendererClass: any
  51. fillRendererClass: any
  52. uid: any
  53. childrenByUid: any
  54. isRTL: boolean = false // frequently accessed options
  55. nextDayThreshold: Duration // "
  56. view: View
  57. eventRenderer: any
  58. helperRenderer: any
  59. fillRenderer: any
  60. hasAllDayBusinessHours: boolean = false // TODO: unify with largeUnit and isTimeScale?
  61. renderedFlags: any = {}
  62. dirtySizeFlags: any = {}
  63. dateProfile: DateProfile = null
  64. businessHoursDef: BusinessHourDef = false
  65. selection: DateSpan = null
  66. eventStore: EventStore = null
  67. dragState: EventInteractionState = null
  68. eventResizeState: EventInteractionState = null
  69. interactingEventDefId: string = null
  70. selectedEventInstanceId: string = null
  71. constructor(_view, _options?) {
  72. super()
  73. // hack to set options prior to the this.opt calls
  74. this.view = _view || this
  75. if (_options) {
  76. this['options'] = _options
  77. }
  78. this.uid = String(uid++)
  79. this.childrenByUid = {}
  80. this.nextDayThreshold = createDuration(this.opt('nextDayThreshold'))
  81. this.isRTL = this.opt('isRTL')
  82. if (this.fillRendererClass) {
  83. this.fillRenderer = new this.fillRendererClass(this)
  84. }
  85. if (this.eventRendererClass) { // fillRenderer is optional -----v
  86. this.eventRenderer = new this.eventRendererClass(this, this.fillRenderer)
  87. }
  88. if (this.helperRendererClass && this.eventRenderer) {
  89. this.helperRenderer = new this.helperRendererClass(this, this.eventRenderer)
  90. }
  91. }
  92. addChild(child) {
  93. if (!this.childrenByUid[child.uid]) {
  94. this.childrenByUid[child.uid] = child
  95. return true
  96. }
  97. return false
  98. }
  99. removeChild(child) {
  100. if (this.childrenByUid[child.uid]) {
  101. delete this.childrenByUid[child.uid]
  102. return true
  103. }
  104. return false
  105. }
  106. updateSize(totalHeight, isAuto, force) {
  107. let flags = this.dirtySizeFlags
  108. if (force || flags.skeleton || flags.dates || flags.events) {
  109. // sort of the catch-all sizing
  110. // anything that might cause dimension changes
  111. this.updateBaseSize(totalHeight, isAuto)
  112. this.buildCoordCaches()
  113. }
  114. if (force || flags.businessHours) {
  115. this.computeBusinessHoursSize()
  116. }
  117. // don't worry about updating the resize of the helper
  118. if (force || flags.selection || flags.drag || flags.eventResize) {
  119. this.computeHighlightSize()
  120. }
  121. if (force || flags.drag || flags.eventResize) {
  122. this.computeHelperSize()
  123. }
  124. if (force || flags.events) {
  125. this.computeEventsSize()
  126. }
  127. if (force || flags.businessHours) {
  128. this.assignBusinessHoursSize()
  129. }
  130. if (force || flags.selection || flags.drag || flags.eventResize) {
  131. this.assignHighlightSize()
  132. }
  133. if (force || flags.drag || flags.eventResize) {
  134. this.assignHelperSize()
  135. }
  136. if (force || flags.events) {
  137. this.assignEventsSize()
  138. }
  139. this.dirtySizeFlags = {}
  140. this.callChildren('updateSize', arguments) // always do this at end?
  141. }
  142. updateBaseSize(totalHeight, isAuto) {
  143. }
  144. buildCoordCaches() {
  145. }
  146. queryHit(leftOffset, topOffset): Hit {
  147. return null // this should be abstract
  148. }
  149. bindGlobalHandlers() {
  150. if (this.isInteractable) {
  151. browserContext.registerComponent(this)
  152. }
  153. }
  154. unbindGlobalHandlers() {
  155. if (this.isInteractable) {
  156. browserContext.unregisterComponent(this)
  157. }
  158. }
  159. // Options
  160. // -----------------------------------------------------------------------------------------------------------------
  161. opt(name) {
  162. return this.view.options[name]
  163. }
  164. // Triggering
  165. // -----------------------------------------------------------------------------------------------------------------
  166. publiclyTrigger(name, args) {
  167. let calendar = this.getCalendar()
  168. return calendar.publiclyTrigger(name, args)
  169. }
  170. publiclyTriggerAfterSizing(name, args) {
  171. let calendar = this.getCalendar()
  172. return calendar.publiclyTriggerAfterSizing(name, args)
  173. }
  174. hasPublicHandlers(name) {
  175. let calendar = this.getCalendar()
  176. return calendar.hasPublicHandlers(name)
  177. }
  178. triggerRenderedSegs(segs: Seg[]) {
  179. if (this.hasPublicHandlers('eventAfterRender')) {
  180. for (let seg of segs) {
  181. this.publiclyTriggerAfterSizing('eventAfterRender', [
  182. {
  183. event: seg.eventRange, // what to do here?
  184. el: seg.el,
  185. view: this
  186. }
  187. ])
  188. }
  189. }
  190. }
  191. triggerWillRemoveSegs(segs: Seg[]) {
  192. if (this.hasPublicHandlers('eventDestroy')) {
  193. for (let seg of segs) {
  194. this.publiclyTrigger('eventDestroy', [
  195. {
  196. event: seg.eventRange, // what to do here?
  197. el: seg.el,
  198. view: this
  199. }
  200. ])
  201. }
  202. }
  203. }
  204. // Root Rendering
  205. // -----------------------------------------------------------------------------------------------------------------
  206. render(renderState: DateComponentRenderState, forceFlags: RenderForceFlags) {
  207. let { renderedFlags } = this
  208. let dirtyFlags = {
  209. skeleton: false,
  210. dates: renderState.dateProfile !== this.dateProfile,
  211. businessHours: renderState.businessHoursDef !== this.businessHoursDef,
  212. selection: renderState.selection !== this.selection,
  213. events: renderState.eventStore !== this.eventStore,
  214. selectedEvent: renderState.selectedEventInstanceId !== this.selectedEventInstanceId,
  215. drag: renderState.dragState !== this.dragState,
  216. eventResize: renderState.eventResizeState !== this.eventResizeState
  217. }
  218. assignTo(dirtyFlags, forceFlags)
  219. if (forceFlags === true) {
  220. // everthing must be marked as dirty when doing a forced resize
  221. for (let name in dirtyFlags) {
  222. dirtyFlags[name] = true
  223. }
  224. } else {
  225. // mark things that are still not rendered as dirty
  226. for (let name in dirtyFlags) {
  227. if (!renderedFlags[name]) {
  228. dirtyFlags[name] = true
  229. }
  230. }
  231. // when the dates are dirty, mark nearly everything else as dirty too
  232. if (dirtyFlags.dates) {
  233. for (let name in dirtyFlags) {
  234. if (name !== 'skeleton') {
  235. forceFlags = true
  236. }
  237. }
  238. }
  239. }
  240. this.unrender(dirtyFlags) // only unrender dirty things
  241. assignTo(this, renderState) // assign incoming state to local state
  242. this.renderByFlag(renderState, dirtyFlags) // only render dirty things
  243. this.renderChildren(renderState, forceFlags)
  244. }
  245. renderByFlag(renderState: DateComponentRenderState, flags) {
  246. let { renderedFlags, dirtySizeFlags } = this
  247. if (flags.skeleton) {
  248. this.renderSkeleton()
  249. renderedFlags.skeleton = true
  250. dirtySizeFlags.skeleton = true
  251. }
  252. if (flags.dates && renderState.dateProfile) {
  253. this.renderDates() // pass in dateProfile too?
  254. renderedFlags.dates = true
  255. dirtySizeFlags.dates = true
  256. }
  257. if (flags.businessHours && renderState.businessHoursDef) {
  258. this.renderBusinessHours(renderState.businessHoursDef)
  259. renderedFlags.businessHours = true
  260. dirtySizeFlags.businessHours = true
  261. }
  262. if (flags.selection && renderState.selection) {
  263. this.renderSelection(renderState.selection)
  264. renderedFlags.selection = true
  265. dirtySizeFlags.selection = true
  266. }
  267. if (flags.events && renderState.eventStore) {
  268. this.renderEvents(renderState.eventStore)
  269. renderedFlags.events = true
  270. dirtySizeFlags.events = true
  271. }
  272. if (flags.selectedEvent) {
  273. this.selectEventsByInstanceId(renderState.selectedEventInstanceId)
  274. renderedFlags.selectedEvent = true
  275. dirtySizeFlags.selectedEvent = true
  276. }
  277. if (flags.drag && renderState.dragState) {
  278. this.renderDragState(renderState.dragState)
  279. renderedFlags.drag = true
  280. dirtySizeFlags.drag = true
  281. }
  282. if (flags.eventResize && renderState.eventResizeState) {
  283. this.renderEventResizeState(renderState.eventResizeState)
  284. renderedFlags.eventResize = true
  285. dirtySizeFlags.eventResize = true
  286. }
  287. }
  288. unrender(flags?: any) {
  289. let { renderedFlags } = this
  290. if ((!flags || flags.eventResize) && renderedFlags.eventResize) {
  291. this.unrenderEventResizeState()
  292. renderedFlags.eventResize = false
  293. }
  294. if ((!flags || flags.drag) && renderedFlags.drag) {
  295. this.unrenderDragState()
  296. renderedFlags.drag = false
  297. }
  298. if ((!flags || flags.selectedEvent) && renderedFlags.selectedEvent) {
  299. this.unselectAllEvents()
  300. renderedFlags.selectedEvent = false
  301. }
  302. if ((!flags || flags.events) && renderedFlags.events) {
  303. this.unrenderEvents()
  304. renderedFlags.events = false
  305. }
  306. if ((!flags || flags.selection) && renderedFlags.selection) {
  307. this.unrenderSelection()
  308. renderedFlags.selection = false
  309. }
  310. if ((!flags || flags.businessHours) && renderedFlags.businessHours) {
  311. this.unrenderBusinessHours()
  312. renderedFlags.businessHours = false
  313. }
  314. if ((!flags || flags.dates) && renderedFlags.dates) {
  315. this.unrenderDates()
  316. renderedFlags.dates = false
  317. }
  318. if ((!flags || flags.skeleton) && renderedFlags.skeleton) {
  319. this.unrenderSkeleton()
  320. renderedFlags.skeleton = false
  321. }
  322. }
  323. renderChildren(renderState: DateComponentRenderState, forceFlags: RenderForceFlags) {
  324. this.callChildren('render', arguments)
  325. }
  326. removeElement() {
  327. this.unrender()
  328. this.dirtySizeFlags = {}
  329. super.removeElement()
  330. }
  331. // Skeleton
  332. // -----------------------------------------------------------------------------------------------------------------
  333. renderSkeleton() {
  334. // subclasses should implement
  335. }
  336. unrenderSkeleton() {
  337. // subclasses should implement
  338. }
  339. // Date
  340. // -----------------------------------------------------------------------------------------------------------------
  341. // date-cell content only
  342. renderDates() {
  343. // subclasses should implement
  344. }
  345. // date-cell content only
  346. unrenderDates() {
  347. // subclasses should override
  348. }
  349. // Now-Indicator
  350. // -----------------------------------------------------------------------------------------------------------------
  351. // Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator
  352. // should be refreshed. If something falsy is returned, no time indicator is rendered at all.
  353. getNowIndicatorUnit() {
  354. // subclasses should implement
  355. }
  356. // Renders a current time indicator at the given datetime
  357. renderNowIndicator(date) {
  358. this.callChildren('renderNowIndicator', arguments)
  359. }
  360. // Undoes the rendering actions from renderNowIndicator
  361. unrenderNowIndicator() {
  362. this.callChildren('unrenderNowIndicator', arguments)
  363. }
  364. // Business Hours
  365. // ---------------------------------------------------------------------------------------------------------------
  366. renderBusinessHours(businessHoursDef: BusinessHourDef) {
  367. if (this.fillRenderer) {
  368. this.fillRenderer.renderSegs(
  369. 'businessHours',
  370. this.eventStoreToSegs(
  371. buildBusinessHourEventStore(
  372. businessHoursDef,
  373. this.hasAllDayBusinessHours,
  374. this.dateProfile.activeUnzonedRange,
  375. this.getCalendar()
  376. )
  377. ),
  378. {
  379. getClasses(seg) {
  380. return [ 'fc-bgevent' ].concat(seg.eventRange.eventDef.className)
  381. }
  382. }
  383. )
  384. }
  385. }
  386. // Unrenders previously-rendered business-hours
  387. unrenderBusinessHours() {
  388. if (this.fillRenderer) {
  389. this.fillRenderer.unrender('businessHours')
  390. }
  391. }
  392. computeBusinessHoursSize() {
  393. if (this.fillRenderer) {
  394. this.fillRenderer.computeSize('businessHours')
  395. }
  396. }
  397. assignBusinessHoursSize() {
  398. if (this.fillRenderer) {
  399. this.fillRenderer.assignSize('businessHours')
  400. }
  401. }
  402. // Event Displaying
  403. // -----------------------------------------------------------------------------------------------------------------
  404. renderEvents(eventStore: EventStore) {
  405. if (this.eventRenderer) {
  406. this.eventRenderer.rangeUpdated() // poorly named now
  407. this.eventRenderer.renderSegs(
  408. this.eventStoreToSegs(eventStore)
  409. )
  410. this.triggerRenderedSegs(this.eventRenderer.getSegs())
  411. }
  412. }
  413. unrenderEvents() {
  414. if (this.eventRenderer) {
  415. this.triggerWillRemoveSegs(this.eventRenderer.getSegs())
  416. this.eventRenderer.unrender()
  417. }
  418. }
  419. computeEventsSize() {
  420. if (this.eventRenderer) {
  421. this.eventRenderer.computeFgSize()
  422. }
  423. }
  424. assignEventsSize() {
  425. if (this.eventRenderer) {
  426. this.eventRenderer.assignFgSize()
  427. }
  428. }
  429. // Drag-n-Drop Rendering (for both events and external elements)
  430. // ---------------------------------------------------------------------------------------------------------------
  431. renderDragState(dragState: EventInteractionState) {
  432. if (dragState.origSeg) {
  433. this.hideRelatedSegs(dragState.origSeg)
  434. }
  435. this.renderDrag(dragState.eventStore, dragState.origSeg, dragState.willCreateEvent)
  436. }
  437. unrenderDragState() {
  438. if (this.dragState.origSeg) {
  439. this.showRelatedSegs(this.dragState.origSeg)
  440. }
  441. this.unrenderDrag()
  442. }
  443. // Renders a visual indication of a event or external-element drag over the given drop zone.
  444. // If an external-element, seg will be `null`.
  445. // Must return elements used for any mock events.
  446. renderDrag(eventStore: EventStore, origSeg, willCreateEvent) {
  447. // subclasses can implement
  448. }
  449. // Unrenders a visual indication of an event or external-element being dragged.
  450. unrenderDrag() {
  451. // subclasses can implement
  452. }
  453. // Event Resizing
  454. // ---------------------------------------------------------------------------------------------------------------
  455. renderEventResizeState(eventResizeState: EventInteractionState) {
  456. if (eventResizeState.origSeg) {
  457. this.hideRelatedSegs(eventResizeState.origSeg)
  458. }
  459. this.renderEventResize(eventResizeState.eventStore, eventResizeState.origSeg)
  460. }
  461. unrenderEventResizeState() {
  462. if (this.eventResizeState.origSeg) {
  463. this.showRelatedSegs(this.eventResizeState.origSeg)
  464. }
  465. this.unrenderEventResize()
  466. }
  467. // Renders a visual indication of an event being resized.
  468. renderEventResize(eventStore: EventStore, origSeg: any) {
  469. // subclasses can implement
  470. }
  471. // Unrenders a visual indication of an event being resized.
  472. unrenderEventResize() {
  473. // subclasses can implement
  474. }
  475. // Seg Utils
  476. // -----------------------------------------------------------------------------------------------------------------
  477. hideRelatedSegs(targetSeg: Seg) {
  478. this.getRelatedSegs(targetSeg).forEach(function(seg) {
  479. seg.el.style.visibility = 'hidden'
  480. })
  481. }
  482. showRelatedSegs(targetSeg: Seg) {
  483. this.getRelatedSegs(targetSeg).forEach(function(seg) {
  484. seg.el.style.visibility = ''
  485. })
  486. }
  487. getRelatedSegs(targetSeg: Seg) {
  488. let targetEventDef = targetSeg.eventRange.eventDef
  489. return this.getAllEventSegs().filter(function(seg: Seg) {
  490. let segEventDef = seg.eventRange.eventDef
  491. return segEventDef.defId === targetEventDef.defId || // include defId as well???
  492. segEventDef.groupId && segEventDef.groupId === targetEventDef.groupId
  493. })
  494. }
  495. getAllEventSegs() {
  496. if (this.eventRenderer) {
  497. return this.eventRenderer.getSegs()
  498. } else {
  499. return []
  500. }
  501. }
  502. // Event Instance Selection (aka long-touch focus)
  503. // -----------------------------------------------------------------------------------------------------------------
  504. // TODO: show/hide according to groupId?
  505. selectEventsByInstanceId(instanceId) {
  506. this.getAllEventSegs().forEach(function(seg) {
  507. if (
  508. seg.eventRange.eventInstance.instanceId === instanceId &&
  509. seg.el // necessary?
  510. ) {
  511. seg.el.classList.add('fc-selected')
  512. }
  513. })
  514. }
  515. unselectAllEvents() {
  516. this.getAllEventSegs().forEach(function(seg) {
  517. if (seg.el) { // necessary?
  518. seg.el.classList.remove('fc-selected')
  519. }
  520. })
  521. }
  522. // EXTERNAL Drag-n-Drop
  523. // ---------------------------------------------------------------------------------------------------------------
  524. // Doesn't need to implement a response, but must pass to children
  525. handlExternalDragStart(ev, el, skipBinding) {
  526. this.callChildren('handlExternalDragStart', arguments)
  527. }
  528. handleExternalDragMove(ev) {
  529. this.callChildren('handleExternalDragMove', arguments)
  530. }
  531. handleExternalDragStop(ev) {
  532. this.callChildren('handleExternalDragStop', arguments)
  533. }
  534. // DateSpan
  535. // ---------------------------------------------------------------------------------------------------------------
  536. // Renders a visual indication of the selection
  537. renderSelection(selection: DateSpan) {
  538. this.renderHighlightSegs(this.selectionToSegs(selection))
  539. }
  540. // Unrenders a visual indication of selection
  541. unrenderSelection() {
  542. this.unrenderHighlight()
  543. }
  544. // Highlight
  545. // ---------------------------------------------------------------------------------------------------------------
  546. // Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data)
  547. renderHighlightSegs(segs) {
  548. if (this.fillRenderer) {
  549. this.fillRenderer.renderSegs('highlight', segs, {
  550. getClasses() {
  551. return [ 'fc-highlight' ]
  552. }
  553. })
  554. }
  555. }
  556. // Unrenders the emphasis on a date range
  557. unrenderHighlight() {
  558. if (this.fillRenderer) {
  559. this.fillRenderer.unrender('highlight')
  560. }
  561. }
  562. computeHighlightSize() {
  563. if (this.fillRenderer) {
  564. this.fillRenderer.computeSize('highlight')
  565. }
  566. }
  567. assignHighlightSize() {
  568. if (this.fillRenderer) {
  569. this.fillRenderer.assignSize('highlight')
  570. }
  571. }
  572. /*
  573. ------------------------------------------------------------------------------------------------------------------*/
  574. computeHelperSize() {
  575. if (this.helperRenderer) {
  576. this.helperRenderer.computeSize()
  577. }
  578. }
  579. assignHelperSize() {
  580. if (this.helperRenderer) {
  581. this.helperRenderer.assignSize()
  582. }
  583. }
  584. /* Converting selection/eventRanges -> segs
  585. ------------------------------------------------------------------------------------------------------------------*/
  586. eventStoreToSegs(eventStore: EventStore): Seg[] {
  587. let activeUnzonedRange = this.dateProfile.activeUnzonedRange
  588. let eventRenderRanges = sliceEventStore(eventStore, activeUnzonedRange)
  589. let allSegs: Seg[] = []
  590. for (let eventRenderRange of eventRenderRanges) {
  591. let segs = this.rangeToSegs(eventRenderRange.range, eventRenderRange.eventDef.isAllDay)
  592. for (let seg of segs) {
  593. seg.eventRange = eventRenderRange
  594. allSegs.push(seg)
  595. }
  596. }
  597. return allSegs
  598. }
  599. selectionToSegs(selection: DateSpan): Seg[] {
  600. return this.rangeToSegs(selection.range, selection.isAllDay)
  601. }
  602. // must implement if want to use many of the rendering utils
  603. rangeToSegs(range: UnzonedRange, isAllDay: boolean): Seg[] {
  604. return []
  605. }
  606. // Utils
  607. // ---------------------------------------------------------------------------------------------------------------
  608. callChildren(methodName, args) {
  609. this.iterChildren(function(child) {
  610. child[methodName].apply(child, args)
  611. })
  612. }
  613. iterChildren(func) {
  614. let childrenByUid = this.childrenByUid
  615. let uid
  616. for (uid in childrenByUid) {
  617. func(childrenByUid[uid])
  618. }
  619. }
  620. getCalendar(): Calendar {
  621. return this.view.calendar
  622. }
  623. getDateEnv(): DateEnv {
  624. return this.getCalendar().dateEnv
  625. }
  626. getTheme(): Theme {
  627. return this.getCalendar().theme
  628. }
  629. // Generates HTML for an anchor to another view into the calendar.
  630. // Will either generate an <a> tag or a non-clickable <span> tag, depending on enabled settings.
  631. // `gotoOptions` can either be a date input, or an object with the form:
  632. // { date, type, forceOff }
  633. // `type` is a view-type like "day" or "week". default value is "day".
  634. // `attrs` and `innerHtml` are use to generate the rest of the HTML tag.
  635. buildGotoAnchorHtml(gotoOptions, attrs, innerHtml) {
  636. let dateEnv = this.getDateEnv()
  637. let date
  638. let type
  639. let forceOff
  640. let finalOptions
  641. if (gotoOptions instanceof Date || typeof gotoOptions !== 'object') {
  642. date = gotoOptions // a single date-like input
  643. } else {
  644. date = gotoOptions.date
  645. type = gotoOptions.type
  646. forceOff = gotoOptions.forceOff
  647. }
  648. date = dateEnv.createMarker(date) // if a string, parse it
  649. finalOptions = { // for serialization into the link
  650. date: dateEnv.formatIso(date, { omitTime: true }),
  651. type: type || 'day'
  652. }
  653. if (typeof attrs === 'string') {
  654. innerHtml = attrs
  655. attrs = null
  656. }
  657. attrs = attrs ? ' ' + attrsToStr(attrs) : '' // will have a leading space
  658. innerHtml = innerHtml || ''
  659. if (!forceOff && this.opt('navLinks')) {
  660. return '<a' + attrs +
  661. ' data-goto="' + htmlEscape(JSON.stringify(finalOptions)) + '">' +
  662. innerHtml +
  663. '</a>'
  664. } else {
  665. return '<span' + attrs + '>' +
  666. innerHtml +
  667. '</span>'
  668. }
  669. }
  670. getAllDayHtml() {
  671. return this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'))
  672. }
  673. // Computes HTML classNames for a single-day element
  674. getDayClasses(date: DateMarker, noThemeHighlight?) {
  675. let view = this.view
  676. let classes = []
  677. let todayStart: DateMarker
  678. let todayEnd: DateMarker
  679. if (!this.dateProfile.activeUnzonedRange.containsDate(date)) {
  680. classes.push('fc-disabled-day') // TODO: jQuery UI theme?
  681. } else {
  682. classes.push('fc-' + DAY_IDS[date.getUTCDay()])
  683. if (view.isDateInOtherMonth(date, this.dateProfile)) { // TODO: use DateComponent subclass somehow
  684. classes.push('fc-other-month')
  685. }
  686. todayStart = startOfDay(view.calendar.getNow())
  687. todayEnd = addDays(todayStart, 1)
  688. if (date < todayStart) {
  689. classes.push('fc-past')
  690. } else if (date >= todayEnd) {
  691. classes.push('fc-future')
  692. } else {
  693. classes.push('fc-today')
  694. if (noThemeHighlight !== true) {
  695. classes.push(view.calendar.theme.getClass('today'))
  696. }
  697. }
  698. }
  699. return classes
  700. }
  701. // Compute the number of the give units in the "current" range.
  702. // Won't go more precise than days.
  703. // Will return `0` if there's not a clean whole interval.
  704. currentRangeAs(unit) { // PLURAL :(
  705. let dateEnv = this.getDateEnv()
  706. let range = this.dateProfile.currentUnzonedRange
  707. let res = null
  708. if (unit === 'years') {
  709. res = dateEnv.diffWholeYears(range.start, range.end)
  710. } else if (unit === 'months') {
  711. res = dateEnv.diffWholeMonths(range.start, range.end)
  712. } else if (unit === 'weeks') {
  713. res = dateEnv.diffWholeMonths(range.start, range.end)
  714. } else if (unit === 'days') {
  715. res = diffWholeDays(range.start, range.end)
  716. }
  717. return res || 0
  718. }
  719. // Returns the date range of the full days the given range visually appears to occupy.
  720. // Returns a plain object with start/end, NOT an UnzonedRange!
  721. computeDayRange(unzonedRange): UnzonedRange {
  722. return computeDayRange(unzonedRange, this.nextDayThreshold)
  723. }
  724. // Does the given range visually appear to occupy more than one day?
  725. isMultiDayRange(unzonedRange) {
  726. let dayRange = this.computeDayRange(unzonedRange)
  727. return diffDays(dayRange.start, dayRange.end) > 1
  728. }
  729. isValidSegInteraction(evTarget: HTMLElement) {
  730. return !this.dragState &&
  731. !this.eventResizeState &&
  732. !elementClosest(evTarget, '.fc-helper')
  733. }
  734. isValidDateInteraction(evTarget: HTMLElement) {
  735. return !elementClosest(evTarget, this.segSelector) &&
  736. !elementClosest(evTarget, '.fc-more') && // a "more.." link
  737. !elementClosest(evTarget, 'a[data-goto]') // a clickable nav link
  738. }
  739. }
  740. function computeDayRange(unzonedRange: UnzonedRange, nextDayThreshold: Duration): UnzonedRange {
  741. let startDay: DateMarker = startOfDay(unzonedRange.start) // the beginning of the day the range starts
  742. let end: DateMarker = unzonedRange.end
  743. let endDay: DateMarker = startOfDay(end)
  744. let endTimeMS: number = end.valueOf() - endDay.valueOf() // # of milliseconds into `endDay`
  745. // If the end time is actually inclusively part of the next day and is equal to or
  746. // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
  747. // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
  748. if (endTimeMS && endTimeMS >= asRoughMs(nextDayThreshold)) {
  749. endDay = addDays(endDay, 1)
  750. }
  751. // If end is within `startDay` but not past nextDayThreshold, assign the default duration of one day.
  752. if (endDay <= startDay) {
  753. endDay = addDays(startDay, 1)
  754. }
  755. return new UnzonedRange(startDay, endDay)
  756. }