DateComponent.ts 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167
  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, diffWholeDays } from '../datelib/marker'
  8. import { Duration, createDuration } from '../datelib/duration'
  9. import { DateSpan } from '../structs/date-span'
  10. import { EventRenderRange, sliceEventStore, computeEventDefUi, EventUiHash, computeEventDefUis } from '../component/event-rendering'
  11. import { EventStore, expandRecurring } from '../structs/event-store'
  12. import { DateEnv } from '../datelib/env'
  13. import Theme from '../theme/Theme'
  14. import { EventInteractionUiState } from '../interactions/event-interaction-state'
  15. import { assignTo } from '../util/object'
  16. import browserContext from '../common/browser-context'
  17. import { Hit } from '../interactions/HitDragging'
  18. import { DateRange, rangeContainsMarker, rangeContainsRange } from '../datelib/date-range'
  19. import EventApi from '../api/EventApi'
  20. import { createEventInstance, parseEventDef } from '../structs/event'
  21. import EmitterMixin from '../common/EmitterMixin'
  22. import { isEventsValid, isSelectionValid } from '../validation'
  23. export interface DateComponentRenderState {
  24. dateProfile: DateProfile | null
  25. businessHours: EventStore
  26. eventStore: EventStore
  27. eventUis: EventUiHash
  28. dateSelection: DateSpan | null
  29. eventSelection: string
  30. eventDrag: EventInteractionUiState | null
  31. eventResize: EventInteractionUiState | null
  32. }
  33. // NOTE: for fg-events, eventRange.range is NOT sliced,
  34. // thus, we need isStart/isEnd
  35. export interface Seg {
  36. component: DateComponent
  37. isStart: boolean
  38. isEnd: boolean
  39. eventRange?: EventRenderRange
  40. el?: HTMLElement
  41. [otherProp: string]: any
  42. }
  43. export type DateComponentHash = { [id: string]: DateComponent }
  44. let uid = 0
  45. export default abstract class DateComponent extends Component {
  46. // self-config, overridable by subclasses
  47. isInteractable: boolean = false
  48. useEventCenter: boolean = true // for dragging geometry
  49. doesDragMirror: boolean = false // for events that ORIGINATE from this component
  50. doesDragHighlight: boolean = false // for events that ORIGINATE from this component
  51. fgSegSelector: string = '.fc-event-container > *' // lets eventRender produce elements without fc-event class
  52. bgSegSelector: string = '.fc-bgevent'
  53. // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity
  54. // of the date areas. if not defined, assumes to be day and time granularity.
  55. // TODO: port isTimeScale into same system?
  56. largeUnit: any
  57. slicingType: 'timed' | 'all-day' | null = null
  58. eventRendererClass: any
  59. mirrorRendererClass: any
  60. fillRendererClass: any
  61. uid: any
  62. childrenByUid: any
  63. isRtl: boolean = false // frequently accessed options
  64. nextDayThreshold: Duration // "
  65. view: View
  66. emitter: EmitterMixin = new EmitterMixin()
  67. eventRenderer: any
  68. mirrorRenderer: any
  69. fillRenderer: any
  70. renderedFlags: any = {}
  71. dirtySizeFlags: any = {}
  72. needHitsDepth: number = 0
  73. dateProfile: DateProfile = null
  74. businessHours: EventStore = null
  75. eventStore: EventStore = null
  76. eventUis: EventUiHash = null
  77. dateSelection: DateSpan = null
  78. eventSelection: string = ''
  79. eventDrag: EventInteractionUiState = null
  80. eventResize: EventInteractionUiState = null
  81. constructor(_view, _options?) {
  82. super()
  83. // hack to set options prior to the this.opt calls
  84. this.view = _view || this
  85. if (_options) {
  86. this['options'] = _options
  87. }
  88. this.uid = String(uid++)
  89. this.childrenByUid = {}
  90. this.nextDayThreshold = createDuration(this.opt('nextDayThreshold'))
  91. this.isRtl = this.opt('isRtl')
  92. if (this.fillRendererClass) {
  93. this.fillRenderer = new this.fillRendererClass(this)
  94. }
  95. if (this.eventRendererClass) { // fillRenderer is optional -----v
  96. this.eventRenderer = new this.eventRendererClass(this, this.fillRenderer)
  97. }
  98. if (this.mirrorRendererClass && this.eventRenderer) {
  99. this.mirrorRenderer = new this.mirrorRendererClass(this, this.eventRenderer)
  100. }
  101. }
  102. addChild(child) {
  103. if (!this.childrenByUid[child.uid]) {
  104. this.childrenByUid[child.uid] = child
  105. return true
  106. }
  107. return false
  108. }
  109. removeChild(child) {
  110. if (this.childrenByUid[child.uid]) {
  111. delete this.childrenByUid[child.uid]
  112. return true
  113. }
  114. return false
  115. }
  116. updateSize(totalHeight, isAuto, force) {
  117. let flags = this.dirtySizeFlags
  118. if (force || flags.skeleton || flags.dates || flags.events) {
  119. // sort of the catch-all sizing
  120. // anything that might cause dimension changes
  121. this.updateBaseSize(totalHeight, isAuto)
  122. this.buildPositionCaches()
  123. }
  124. if (force || flags.businessHours) {
  125. this.computeBusinessHoursSize()
  126. }
  127. if (force || flags.dateSelection || flags.eventDrag || flags.eventResize) {
  128. this.computeHighlightSize()
  129. this.computeMirrorSize()
  130. }
  131. if (force || flags.events) {
  132. this.computeEventsSize()
  133. }
  134. if (force || flags.businessHours) {
  135. this.assignBusinessHoursSize()
  136. }
  137. if (force || flags.dateSelection || flags.eventDrag || flags.eventResize) {
  138. this.assignHighlightSize()
  139. this.assignMirrorSize()
  140. }
  141. if (force || flags.events) {
  142. this.assignEventsSize()
  143. }
  144. this.dirtySizeFlags = {}
  145. this.callChildren('updateSize', arguments) // always do this at end?
  146. }
  147. updateBaseSize(totalHeight, isAuto) {
  148. }
  149. buildPositionCaches() {
  150. }
  151. requestPrepareHits() {
  152. if (!(this.needHitsDepth++)) {
  153. this.prepareHits()
  154. }
  155. }
  156. requestReleaseHits() {
  157. if (!(--this.needHitsDepth)) {
  158. this.releaseHits()
  159. }
  160. }
  161. protected prepareHits() {
  162. }
  163. protected releaseHits() {
  164. }
  165. queryHit(leftOffset, topOffset): Hit | null {
  166. return null // this should be abstract
  167. }
  168. bindGlobalHandlers() {
  169. if (this.isInteractable) {
  170. browserContext.registerComponent(this)
  171. }
  172. }
  173. unbindGlobalHandlers() {
  174. if (this.isInteractable) {
  175. browserContext.unregisterComponent(this)
  176. }
  177. }
  178. // Options
  179. // -----------------------------------------------------------------------------------------------------------------
  180. opt(name) {
  181. return this.view.options[name]
  182. }
  183. // Triggering
  184. // -----------------------------------------------------------------------------------------------------------------
  185. publiclyTrigger(name, args) {
  186. let calendar = this.getCalendar()
  187. return calendar.publiclyTrigger(name, args)
  188. }
  189. publiclyTriggerAfterSizing(name, args) {
  190. let calendar = this.getCalendar()
  191. return calendar.publiclyTriggerAfterSizing(name, args)
  192. }
  193. hasPublicHandlers(name) {
  194. let calendar = this.getCalendar()
  195. return calendar.hasPublicHandlers(name)
  196. }
  197. triggerRenderedSegs(segs: Seg[], isMirrors: boolean = false) {
  198. if (this.hasPublicHandlers('eventPositioned')) {
  199. let calendar = this.getCalendar()
  200. for (let seg of segs) {
  201. this.publiclyTriggerAfterSizing('eventPositioned', [
  202. {
  203. event: new EventApi(
  204. calendar,
  205. seg.eventRange.def,
  206. seg.eventRange.instance
  207. ),
  208. isMirror: isMirrors,
  209. isStart: seg.isStart,
  210. isEnd: seg.isEnd,
  211. el: seg.el,
  212. view: this
  213. }
  214. ])
  215. }
  216. }
  217. }
  218. triggerWillRemoveSegs(segs: Seg[]) {
  219. for (let seg of segs) {
  220. this.emitter.trigger('eventElRemove', seg.el)
  221. }
  222. if (this.hasPublicHandlers('eventDestroy')) {
  223. let calendar = this.getCalendar()
  224. for (let seg of segs) {
  225. this.publiclyTrigger('eventDestroy', [
  226. {
  227. event: new EventApi(
  228. calendar,
  229. seg.eventRange.def,
  230. seg.eventRange.instance
  231. ),
  232. el: seg.el,
  233. view: this
  234. }
  235. ])
  236. }
  237. }
  238. }
  239. // Root Rendering
  240. // -----------------------------------------------------------------------------------------------------------------
  241. render(renderState: DateComponentRenderState, forceFlags: RenderForceFlags) {
  242. let { renderedFlags } = this
  243. let dirtyFlags = {
  244. skeleton: false,
  245. dates: renderState.dateProfile !== this.dateProfile,
  246. events: renderState.eventStore !== this.eventStore || renderState.eventUis !== this.eventUis,
  247. businessHours: renderState.businessHours !== this.businessHours,
  248. dateSelection: renderState.dateSelection !== this.dateSelection,
  249. eventSelection: renderState.eventSelection !== this.eventSelection,
  250. eventDrag: renderState.eventDrag !== this.eventDrag,
  251. eventResize: renderState.eventResize !== this.eventResize
  252. }
  253. assignTo(dirtyFlags, forceFlags)
  254. if (forceFlags === true) {
  255. // everthing must be marked as dirty when doing a forced resize
  256. for (let name in dirtyFlags) {
  257. dirtyFlags[name] = true
  258. }
  259. } else {
  260. // mark things that are still not rendered as dirty
  261. for (let name in dirtyFlags) {
  262. if (!renderedFlags[name]) {
  263. dirtyFlags[name] = true
  264. }
  265. }
  266. // when the dates are dirty, mark nearly everything else as dirty too
  267. if (dirtyFlags.dates) {
  268. for (let name in dirtyFlags) {
  269. if (name !== 'skeleton') {
  270. dirtyFlags[name] = true
  271. }
  272. }
  273. }
  274. }
  275. this.unrender(dirtyFlags) // only unrender dirty things
  276. assignTo(this, renderState) // assign incoming state to local state
  277. this.renderByFlag(renderState, dirtyFlags) // only render dirty things
  278. this.renderChildren(renderState, forceFlags)
  279. }
  280. renderByFlag(renderState: DateComponentRenderState, flags) {
  281. let { renderedFlags, dirtySizeFlags } = this
  282. if (flags.skeleton) {
  283. this.renderSkeleton()
  284. this.afterSkeletonRender()
  285. renderedFlags.skeleton = true
  286. dirtySizeFlags.skeleton = true
  287. }
  288. if (flags.dates && renderState.dateProfile) {
  289. this.renderDates(renderState.dateProfile)
  290. this.afterDatesRender()
  291. renderedFlags.dates = true
  292. dirtySizeFlags.dates = true
  293. }
  294. if (flags.businessHours && renderState.businessHours) {
  295. this.renderBusinessHours(renderState.businessHours)
  296. renderedFlags.businessHours = true
  297. dirtySizeFlags.businessHours = true
  298. }
  299. if (flags.dateSelection && renderState.dateSelection) {
  300. this.renderDateSelection(renderState.dateSelection)
  301. renderedFlags.dateSelection = true
  302. dirtySizeFlags.dateSelection = true
  303. }
  304. if (flags.events && renderState.eventStore) {
  305. this.renderEvents(renderState.eventStore, renderState.eventUis)
  306. renderedFlags.events = true
  307. dirtySizeFlags.events = true
  308. }
  309. if (flags.eventSelection) {
  310. this.selectEventsByInstanceId(renderState.eventSelection)
  311. renderedFlags.eventSelection = true
  312. dirtySizeFlags.eventSelection = true
  313. }
  314. if (flags.eventDrag && renderState.eventDrag) {
  315. this.renderEventDragState(renderState.eventDrag)
  316. renderedFlags.eventDrag = true
  317. dirtySizeFlags.eventDrag = true
  318. }
  319. if (flags.eventResize && renderState.eventResize) {
  320. this.renderEventResizeState(renderState.eventResize)
  321. renderedFlags.eventResize = true
  322. dirtySizeFlags.eventResize = true
  323. }
  324. }
  325. unrender(flags?: any) {
  326. let { renderedFlags } = this
  327. if ((!flags || flags.eventResize) && renderedFlags.eventResize) {
  328. this.unrenderEventResizeState()
  329. renderedFlags.eventResize = false
  330. }
  331. if ((!flags || flags.eventDrag) && renderedFlags.eventDrag) {
  332. this.unrenderEventDragState()
  333. renderedFlags.eventDrag = false
  334. }
  335. if ((!flags || flags.eventSelection) && renderedFlags.eventSelection) {
  336. this.unselectAllEvents()
  337. renderedFlags.eventSelection = false
  338. }
  339. if ((!flags || flags.events) && renderedFlags.events) {
  340. this.unrenderEvents()
  341. renderedFlags.events = false
  342. }
  343. if ((!flags || flags.dateSelection) && renderedFlags.dateSelection) {
  344. this.unrenderDateSelection()
  345. renderedFlags.dateSelection = false
  346. }
  347. if ((!flags || flags.businessHours) && renderedFlags.businessHours) {
  348. this.unrenderBusinessHours()
  349. renderedFlags.businessHours = false
  350. }
  351. if ((!flags || flags.dates) && renderedFlags.dates) {
  352. this.beforeDatesUnrender()
  353. this.unrenderDates()
  354. renderedFlags.dates = false
  355. }
  356. if ((!flags || flags.skeleton) && renderedFlags.skeleton) {
  357. this.beforeSkeletonUnrender()
  358. this.unrenderSkeleton()
  359. renderedFlags.skeleton = false
  360. }
  361. }
  362. renderChildren(renderState: DateComponentRenderState, forceFlags: RenderForceFlags) {
  363. this.callChildren('render', arguments)
  364. }
  365. removeElement() {
  366. this.unrender()
  367. this.dirtySizeFlags = {}
  368. super.removeElement()
  369. }
  370. // Skeleton
  371. // -----------------------------------------------------------------------------------------------------------------
  372. renderSkeleton() {
  373. // subclasses should implement
  374. }
  375. afterSkeletonRender() { }
  376. beforeSkeletonUnrender() { }
  377. unrenderSkeleton() {
  378. // subclasses should implement
  379. }
  380. // Date
  381. // -----------------------------------------------------------------------------------------------------------------
  382. // date-cell content only
  383. renderDates(dateProfile: DateProfile) {
  384. // subclasses should implement
  385. }
  386. afterDatesRender() { }
  387. beforeDatesUnrender() { }
  388. // date-cell content only
  389. unrenderDates() {
  390. // subclasses should override
  391. }
  392. // Now-Indicator
  393. // -----------------------------------------------------------------------------------------------------------------
  394. // Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator
  395. // should be refreshed. If something falsy is returned, no time indicator is rendered at all.
  396. getNowIndicatorUnit() {
  397. // subclasses should implement
  398. }
  399. // Renders a current time indicator at the given datetime
  400. renderNowIndicator(date) {
  401. this.callChildren('renderNowIndicator', arguments)
  402. }
  403. // Undoes the rendering actions from renderNowIndicator
  404. unrenderNowIndicator() {
  405. this.callChildren('unrenderNowIndicator', arguments)
  406. }
  407. // Business Hours
  408. // ---------------------------------------------------------------------------------------------------------------
  409. renderBusinessHours(businessHours: EventStore) {
  410. if (this.slicingType) { // can use eventStoreToRanges?
  411. let expandedStore = expandRecurring(businessHours, this.dateProfile.activeRange, this.getCalendar())
  412. this.renderBusinessHourRanges(
  413. this.eventStoreToRanges(
  414. expandedStore,
  415. computeEventDefUis(expandedStore.defs, {}, {})
  416. )
  417. )
  418. }
  419. }
  420. renderBusinessHourRanges(eventRanges: EventRenderRange[]) {
  421. if (this.fillRenderer) {
  422. this.fillRenderer.renderSegs(
  423. 'businessHours',
  424. this.eventRangesToSegs(eventRanges),
  425. {
  426. getClasses(seg) {
  427. return [ 'fc-bgevent' ].concat(seg.eventRange.def.classNames)
  428. }
  429. }
  430. )
  431. }
  432. }
  433. // Unrenders previously-rendered business-hours
  434. unrenderBusinessHours() {
  435. if (this.fillRenderer) {
  436. this.fillRenderer.unrender('businessHours')
  437. }
  438. }
  439. computeBusinessHoursSize() {
  440. if (this.fillRenderer) {
  441. this.fillRenderer.computeSize('businessHours')
  442. }
  443. }
  444. assignBusinessHoursSize() {
  445. if (this.fillRenderer) {
  446. this.fillRenderer.assignSize('businessHours')
  447. }
  448. }
  449. // Event Displaying
  450. // -----------------------------------------------------------------------------------------------------------------
  451. renderEvents(eventStore: EventStore, eventUis: EventUiHash) {
  452. if (this.slicingType) { // can use eventStoreToRanges?
  453. this.renderEventRanges(
  454. this.eventStoreToRanges(eventStore, eventUis)
  455. )
  456. }
  457. }
  458. renderEventRanges(eventRanges: EventRenderRange[]) {
  459. if (this.eventRenderer) {
  460. this.eventRenderer.rangeUpdated() // poorly named now
  461. this.eventRenderer.renderSegs(
  462. this.eventRangesToSegs(eventRanges)
  463. )
  464. let calendar = this.getCalendar()
  465. if (!calendar.state.loadingLevel) { // avoid initial empty state while pending
  466. calendar.afterSizingTriggers._eventsPositioned = [ null ] // fire once
  467. }
  468. }
  469. }
  470. unrenderEvents() {
  471. if (this.eventRenderer) {
  472. this.triggerWillRemoveSegs(this.eventRenderer.getSegs())
  473. this.eventRenderer.unrender()
  474. }
  475. }
  476. computeEventsSize() {
  477. if (this.fillRenderer) {
  478. this.fillRenderer.computeSize('bgEvent')
  479. }
  480. if (this.eventRenderer) {
  481. this.eventRenderer.computeFgSize()
  482. }
  483. }
  484. assignEventsSize() {
  485. if (this.fillRenderer) {
  486. this.fillRenderer.assignSize('bgEvent')
  487. }
  488. if (this.eventRenderer) {
  489. this.eventRenderer.assignFgSize()
  490. }
  491. }
  492. // Drag-n-Drop Rendering (for both events and external elements)
  493. // ---------------------------------------------------------------------------------------------------------------
  494. renderEventDragState(state: EventInteractionUiState) {
  495. this.hideSegsByHash(state.affectedEvents.instances)
  496. this.renderEventDrag(
  497. state.mutatedEvents,
  498. state.eventUis,
  499. state.isEvent,
  500. state.origSeg
  501. )
  502. }
  503. unrenderEventDragState() {
  504. this.showSegsByHash(this.eventDrag.affectedEvents.instances)
  505. this.unrenderEventDrag()
  506. }
  507. // Renders a visual indication of a event or external-element drag over the given drop zone.
  508. // If an external-element, seg will be `null`.
  509. renderEventDrag(eventStore: EventStore, eventUis: EventUiHash, isEvent: boolean, origSeg: Seg | null) {
  510. let segs = this.eventRangesToSegs(
  511. this.eventStoreToRanges(eventStore, eventUis)
  512. )
  513. // if the user is dragging something that is considered an event with real event data,
  514. // and this component likes to do drag mirrors OR the component where the seg came from
  515. // likes to do drag mirrors, then render a drag mirror.
  516. if (isEvent && (this.doesDragMirror || origSeg && origSeg.component.doesDragMirror)) {
  517. if (this.mirrorRenderer) {
  518. this.mirrorRenderer.renderEventDraggingSegs(segs, origSeg)
  519. }
  520. }
  521. // if it would be impossible to render a drag mirror OR this component likes to render
  522. // highlights, then render a highlight.
  523. if (!isEvent || this.doesDragHighlight) {
  524. this.renderHighlightSegs(segs)
  525. }
  526. }
  527. // Unrenders a visual indication of an event or external-element being dragged.
  528. unrenderEventDrag() {
  529. this.unrenderHighlight()
  530. if (this.mirrorRenderer) {
  531. this.mirrorRenderer.unrender()
  532. }
  533. }
  534. // Event Resizing
  535. // ---------------------------------------------------------------------------------------------------------------
  536. renderEventResizeState(state: EventInteractionUiState) {
  537. this.hideSegsByHash(state.affectedEvents.instances)
  538. this.renderEventResize(
  539. state.mutatedEvents,
  540. state.eventUis,
  541. state.origSeg
  542. )
  543. }
  544. unrenderEventResizeState() {
  545. this.showSegsByHash(this.eventResize.affectedEvents.instances)
  546. this.unrenderEventResize()
  547. }
  548. // Renders a visual indication of an event being resized.
  549. renderEventResize(eventStore: EventStore, eventUis: EventUiHash, origSeg: any) {
  550. // subclasses can implement
  551. }
  552. // Unrenders a visual indication of an event being resized.
  553. unrenderEventResize() {
  554. // subclasses can implement
  555. }
  556. // Seg Utils
  557. // -----------------------------------------------------------------------------------------------------------------
  558. hideSegsByHash(hash) {
  559. this.getAllEventSegs().forEach(function(seg) {
  560. if (hash[seg.eventRange.instance.instanceId]) {
  561. seg.el.style.visibility = 'hidden'
  562. }
  563. })
  564. }
  565. showSegsByHash(hash) {
  566. this.getAllEventSegs().forEach(function(seg) {
  567. if (hash[seg.eventRange.instance.instanceId]) {
  568. seg.el.style.visibility = ''
  569. }
  570. })
  571. }
  572. getAllEventSegs(): Seg[] {
  573. if (this.eventRenderer) {
  574. return this.eventRenderer.getSegs()
  575. } else {
  576. return []
  577. }
  578. }
  579. // Event Instance Selection (aka long-touch focus)
  580. // -----------------------------------------------------------------------------------------------------------------
  581. // TODO: show/hide according to groupId?
  582. selectEventsByInstanceId(instanceId) {
  583. this.getAllEventSegs().forEach(function(seg) {
  584. let eventInstance = seg.eventRange.instance
  585. if (
  586. eventInstance && eventInstance.instanceId === instanceId &&
  587. seg.el // necessary?
  588. ) {
  589. seg.el.classList.add('fc-selected')
  590. }
  591. })
  592. }
  593. unselectAllEvents() {
  594. this.getAllEventSegs().forEach(function(seg) {
  595. if (seg.el) { // necessary?
  596. seg.el.classList.remove('fc-selected')
  597. }
  598. })
  599. }
  600. // EXTERNAL Drag-n-Drop
  601. // ---------------------------------------------------------------------------------------------------------------
  602. // Doesn't need to implement a response, but must pass to children
  603. handlExternalDragStart(ev, el, skipBinding) {
  604. this.callChildren('handlExternalDragStart', arguments)
  605. }
  606. handleExternalDragMove(ev) {
  607. this.callChildren('handleExternalDragMove', arguments)
  608. }
  609. handleExternalDragStop(ev) {
  610. this.callChildren('handleExternalDragStop', arguments)
  611. }
  612. // DateSpan
  613. // ---------------------------------------------------------------------------------------------------------------
  614. // Renders a visual indication of the selection
  615. renderDateSelection(selection: DateSpan) {
  616. this.renderHighlightSegs(this.selectionToSegs(selection, false))
  617. }
  618. // Unrenders a visual indication of selection
  619. unrenderDateSelection() {
  620. this.unrenderHighlight()
  621. }
  622. // Highlight
  623. // ---------------------------------------------------------------------------------------------------------------
  624. // Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data)
  625. renderHighlightSegs(segs) {
  626. if (this.fillRenderer) {
  627. this.fillRenderer.renderSegs('highlight', segs, {
  628. getClasses() {
  629. return [ 'fc-highlight' ]
  630. }
  631. })
  632. }
  633. }
  634. // Unrenders the emphasis on a date range
  635. unrenderHighlight() {
  636. if (this.fillRenderer) {
  637. this.fillRenderer.unrender('highlight')
  638. }
  639. }
  640. computeHighlightSize() {
  641. if (this.fillRenderer) {
  642. this.fillRenderer.computeSize('highlight')
  643. }
  644. }
  645. assignHighlightSize() {
  646. if (this.fillRenderer) {
  647. this.fillRenderer.assignSize('highlight')
  648. }
  649. }
  650. /*
  651. ------------------------------------------------------------------------------------------------------------------*/
  652. computeMirrorSize() {
  653. if (this.mirrorRenderer) {
  654. this.mirrorRenderer.computeSize()
  655. }
  656. }
  657. assignMirrorSize() {
  658. if (this.mirrorRenderer) {
  659. this.mirrorRenderer.assignSize()
  660. }
  661. }
  662. /* Converting selection/eventRanges -> segs
  663. ------------------------------------------------------------------------------------------------------------------*/
  664. eventStoreToRanges(eventStore: EventStore, eventUis: EventUiHash): EventRenderRange[] {
  665. return sliceEventStore(
  666. eventStore,
  667. eventUis,
  668. this.dateProfile.activeRange,
  669. this.slicingType === 'all-day' ? this.nextDayThreshold : null
  670. )
  671. }
  672. eventRangesToSegs(eventRenderRanges: EventRenderRange[]): Seg[] {
  673. let allSegs: Seg[] = []
  674. for (let eventRenderRange of eventRenderRanges) {
  675. let segs = this.rangeToSegs(eventRenderRange.range, eventRenderRange.def.isAllDay)
  676. for (let seg of segs) {
  677. seg.eventRange = eventRenderRange
  678. seg.isStart = seg.isStart && eventRenderRange.isStart
  679. seg.isEnd = seg.isEnd && eventRenderRange.isEnd
  680. allSegs.push(seg)
  681. }
  682. }
  683. return allSegs
  684. }
  685. selectionToSegs(selection: DateSpan, fabricateEvents: boolean): Seg[] {
  686. let segs = this.rangeToSegs(selection.range, selection.isAllDay)
  687. if (fabricateEvents) {
  688. // fabricate an eventRange. important for mirror
  689. // TODO: make a separate utility for this?
  690. let def = parseEventDef(
  691. { editable: false },
  692. '', // sourceId
  693. selection.isAllDay,
  694. true, // hasEnd
  695. this.getCalendar()
  696. )
  697. let eventRange = {
  698. def,
  699. ui: computeEventDefUi(def, {}, {}),
  700. instance: createEventInstance(def.defId, selection.range),
  701. range: selection.range,
  702. isStart: true,
  703. isEnd: true
  704. }
  705. for (let seg of segs) {
  706. seg.eventRange = eventRange
  707. }
  708. }
  709. return segs
  710. }
  711. // must implement if want to use many of the rendering utils
  712. rangeToSegs(range: DateRange, isAllDay: boolean): Seg[] {
  713. return []
  714. }
  715. // Utils
  716. // ---------------------------------------------------------------------------------------------------------------
  717. callChildren(methodName, args) {
  718. this.iterChildren(function(child) {
  719. child[methodName].apply(child, args)
  720. })
  721. }
  722. iterChildren(func) {
  723. let childrenByUid = this.childrenByUid
  724. let uid
  725. for (uid in childrenByUid) {
  726. func(childrenByUid[uid])
  727. }
  728. }
  729. getCalendar(): Calendar {
  730. return this.view.calendar
  731. }
  732. getDateEnv(): DateEnv {
  733. return this.getCalendar().dateEnv
  734. }
  735. getTheme(): Theme {
  736. return this.getCalendar().theme
  737. }
  738. // Generates HTML for an anchor to another view into the calendar.
  739. // Will either generate an <a> tag or a non-clickable <span> tag, depending on enabled settings.
  740. // `gotoOptions` can either be a date input, or an object with the form:
  741. // { date, type, forceOff }
  742. // `type` is a view-type like "day" or "week". default value is "day".
  743. // `attrs` and `innerHtml` are use to generate the rest of the HTML tag.
  744. buildGotoAnchorHtml(gotoOptions, attrs, innerHtml) {
  745. let dateEnv = this.getDateEnv()
  746. let date
  747. let type
  748. let forceOff
  749. let finalOptions
  750. if (gotoOptions instanceof Date || typeof gotoOptions !== 'object') {
  751. date = gotoOptions // a single date-like input
  752. } else {
  753. date = gotoOptions.date
  754. type = gotoOptions.type
  755. forceOff = gotoOptions.forceOff
  756. }
  757. date = dateEnv.createMarker(date) // if a string, parse it
  758. finalOptions = { // for serialization into the link
  759. date: dateEnv.formatIso(date, { omitTime: true }),
  760. type: type || 'day'
  761. }
  762. if (typeof attrs === 'string') {
  763. innerHtml = attrs
  764. attrs = null
  765. }
  766. attrs = attrs ? ' ' + attrsToStr(attrs) : '' // will have a leading space
  767. innerHtml = innerHtml || ''
  768. if (!forceOff && this.opt('navLinks')) {
  769. return '<a' + attrs +
  770. ' data-goto="' + htmlEscape(JSON.stringify(finalOptions)) + '">' +
  771. innerHtml +
  772. '</a>'
  773. } else {
  774. return '<span' + attrs + '>' +
  775. innerHtml +
  776. '</span>'
  777. }
  778. }
  779. getAllDayHtml() {
  780. return this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'))
  781. }
  782. // Computes HTML classNames for a single-day element
  783. getDayClasses(date: DateMarker, noThemeHighlight?) {
  784. let view = this.view
  785. let classes = []
  786. let todayStart: DateMarker
  787. let todayEnd: DateMarker
  788. if (!rangeContainsMarker(this.dateProfile.activeRange, date)) {
  789. classes.push('fc-disabled-day') // TODO: jQuery UI theme?
  790. } else {
  791. classes.push('fc-' + DAY_IDS[date.getUTCDay()])
  792. if (view.isDateInOtherMonth(date, this.dateProfile)) { // TODO: use DateComponent subclass somehow
  793. classes.push('fc-other-month')
  794. }
  795. todayStart = startOfDay(view.calendar.getNow())
  796. todayEnd = addDays(todayStart, 1)
  797. if (date < todayStart) {
  798. classes.push('fc-past')
  799. } else if (date >= todayEnd) {
  800. classes.push('fc-future')
  801. } else {
  802. classes.push('fc-today')
  803. if (noThemeHighlight !== true) {
  804. classes.push(view.calendar.theme.getClass('today'))
  805. }
  806. }
  807. }
  808. return classes
  809. }
  810. // Compute the number of the give units in the "current" range.
  811. // Won't go more precise than days.
  812. // Will return `0` if there's not a clean whole interval.
  813. currentRangeAs(unit) { // PLURAL :(
  814. let dateEnv = this.getDateEnv()
  815. let range = this.dateProfile.currentRange
  816. let res = null
  817. if (unit === 'years') {
  818. res = dateEnv.diffWholeYears(range.start, range.end)
  819. } else if (unit === 'months') {
  820. res = dateEnv.diffWholeMonths(range.start, range.end)
  821. } else if (unit === 'weeks') {
  822. res = dateEnv.diffWholeMonths(range.start, range.end)
  823. } else if (unit === 'days') {
  824. res = diffWholeDays(range.start, range.end)
  825. }
  826. return res || 0
  827. }
  828. isValidSegDownEl(el: HTMLElement) {
  829. return !this.eventDrag && !this.eventResize &&
  830. !elementClosest(el, '.fc-mirror') &&
  831. !this.isInPopover(el)
  832. }
  833. isValidDateDownEl(el: HTMLElement) {
  834. let segEl = elementClosest(el, this.fgSegSelector)
  835. return (!segEl || segEl.classList.contains('fc-mirror')) &&
  836. !elementClosest(el, '.fc-more') && // a "more.." link
  837. !elementClosest(el, 'a[data-goto]') && // a clickable nav link
  838. !this.isInPopover(el)
  839. }
  840. // is the element inside of an inner popover?
  841. isInPopover(el: HTMLElement) {
  842. let popoverEl = elementClosest(el, '.fc-popover')
  843. return popoverEl && popoverEl !== this.el // if the current component IS a popover, okay
  844. }
  845. isEventsValid(eventStore: EventStore) {
  846. let { dateProfile } = this
  847. let instances = eventStore.instances
  848. if (dateProfile) { // HACK for DayTile
  849. for (let instanceId in instances) {
  850. if (!rangeContainsRange(dateProfile.validRange, instances[instanceId].range)) {
  851. return false
  852. }
  853. }
  854. }
  855. return isEventsValid(eventStore, this.getCalendar())
  856. }
  857. isSelectionValid(selection: DateSpan): boolean {
  858. let { dateProfile } = this
  859. if (
  860. dateProfile && // HACK for DayTile
  861. !rangeContainsRange(dateProfile.validRange, selection.range)
  862. ) {
  863. return false
  864. }
  865. return isSelectionValid(selection, this.getCalendar())
  866. }
  867. }