2
0

View.ts 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035
  1. import * as moment from 'moment'
  2. import { assignTo } from './util/object'
  3. import { elementClosest } from './util/dom-manip'
  4. import { isPrimaryMouseButton } from './util/dom-event'
  5. import { parseFieldSpecs } from './util/misc'
  6. import RenderQueue from './common/RenderQueue'
  7. import Calendar from './Calendar'
  8. import DateProfileGenerator from './DateProfileGenerator'
  9. import InteractiveDateComponent from './component/InteractiveDateComponent'
  10. import GlobalEmitter from './common/GlobalEmitter'
  11. import UnzonedRange from './models/UnzonedRange'
  12. import EventInstance from './models/event/EventInstance'
  13. /* An abstract class from which other views inherit from
  14. ----------------------------------------------------------------------------------------------------------------------*/
  15. export default abstract class View extends InteractiveDateComponent {
  16. type: string // subclass' view name (string)
  17. name: string // deprecated. use `type` instead
  18. title: string // the text that will be displayed in the header's title
  19. calendar: Calendar // owner Calendar object
  20. viewSpec: any
  21. options: any // hash containing all options. already merged with view-specific-options
  22. renderQueue: RenderQueue
  23. batchRenderDepth: number = 0
  24. queuedScroll: object
  25. isSelected: boolean = false // boolean whether a range of time is user-selected or not
  26. selectedEventInstance: EventInstance
  27. eventOrderSpecs: any // criteria for ordering events when they have same date/time
  28. // for date utils, computed from options
  29. isHiddenDayHash: boolean[]
  30. // now indicator
  31. isNowIndicatorRendered: boolean
  32. initialNowDate: moment.Moment // result first getNow call
  33. initialNowQueriedMs: number // ms time the getNow was called
  34. nowIndicatorTimeoutID: any // for refresh timing of now indicator
  35. nowIndicatorIntervalID: any // "
  36. dateProfileGeneratorClass: any // initialized after class
  37. dateProfileGenerator: any
  38. // whether minTime/maxTime will affect the activeUnzonedRange. Views must opt-in.
  39. // initialized after class
  40. usesMinMaxTime: boolean
  41. // DEPRECATED
  42. start: moment.Moment // use activeUnzonedRange
  43. end: moment.Moment // use activeUnzonedRange
  44. intervalStart: moment.Moment // use currentUnzonedRange
  45. intervalEnd: moment.Moment // use currentUnzonedRange
  46. constructor(calendar, viewSpec) {
  47. super(null, viewSpec.options)
  48. this.calendar = calendar
  49. this.viewSpec = viewSpec
  50. // shortcuts
  51. this.type = viewSpec.type
  52. // .name is deprecated
  53. this.name = this.type
  54. this.initRenderQueue()
  55. this.initHiddenDays()
  56. this.dateProfileGenerator = new this.dateProfileGeneratorClass(this)
  57. this.bindBaseRenderHandlers()
  58. this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder'))
  59. // legacy
  60. if (this['initialize']) {
  61. this['initialize']()
  62. }
  63. }
  64. _getView() {
  65. return this
  66. }
  67. // Retrieves an option with the given name
  68. opt(name) {
  69. return this.options[name]
  70. }
  71. /* Render Queue
  72. ------------------------------------------------------------------------------------------------------------------*/
  73. initRenderQueue() {
  74. this.renderQueue = new RenderQueue({
  75. event: this.opt('eventRenderWait')
  76. })
  77. this.renderQueue.on('start', this.onRenderQueueStart.bind(this))
  78. this.renderQueue.on('stop', this.onRenderQueueStop.bind(this))
  79. this.on('before:change', this.startBatchRender)
  80. this.on('change', this.stopBatchRender)
  81. }
  82. onRenderQueueStart() {
  83. this.calendar.freezeContentHeight()
  84. this.addScroll(this.queryScroll())
  85. }
  86. onRenderQueueStop() {
  87. if (this.calendar.updateViewSize()) { // success?
  88. this.popScroll()
  89. }
  90. this.calendar.thawContentHeight()
  91. }
  92. startBatchRender() {
  93. if (!(this.batchRenderDepth++)) {
  94. this.renderQueue.pause()
  95. }
  96. }
  97. stopBatchRender() {
  98. if (!(--this.batchRenderDepth)) {
  99. this.renderQueue.resume()
  100. }
  101. }
  102. requestRender(func, namespace, actionType) {
  103. this.renderQueue.queue(func, namespace, actionType)
  104. }
  105. // given func will auto-bind to `this`
  106. whenSizeUpdated(func) {
  107. if (this.renderQueue.isRunning) {
  108. this.renderQueue.one('stop', func.bind(this))
  109. } else {
  110. func.call(this)
  111. }
  112. }
  113. /* Title and Date Formatting
  114. ------------------------------------------------------------------------------------------------------------------*/
  115. // Computes what the title at the top of the calendar should be for this view
  116. computeTitle(dateProfile) {
  117. let unzonedRange
  118. // for views that span a large unit of time, show the proper interval, ignoring stray days before and after
  119. if (/^(year|month)$/.test(dateProfile.currentRangeUnit)) {
  120. unzonedRange = dateProfile.currentUnzonedRange
  121. } else { // for day units or smaller, use the actual day range
  122. unzonedRange = dateProfile.activeUnzonedRange
  123. }
  124. return this.formatRange(
  125. {
  126. start: this.calendar.msToMoment(unzonedRange.startMs, dateProfile.isRangeAllDay),
  127. end: this.calendar.msToMoment(unzonedRange.endMs, dateProfile.isRangeAllDay)
  128. },
  129. dateProfile.isRangeAllDay,
  130. this.opt('titleFormat') || this.computeTitleFormat(dateProfile),
  131. this.opt('titleRangeSeparator')
  132. )
  133. }
  134. // Generates the format string that should be used to generate the title for the current date range.
  135. // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
  136. computeTitleFormat(dateProfile) {
  137. let currentRangeUnit = dateProfile.currentRangeUnit
  138. if (currentRangeUnit === 'year') {
  139. return 'YYYY'
  140. } else if (currentRangeUnit === 'month') {
  141. return this.opt('monthYearFormat') // like "September 2014"
  142. } else if (dateProfile.currentUnzonedRange.as('days') > 1) {
  143. return 'll' // multi-day range. shorter, like "Sep 9 - 10 2014"
  144. } else {
  145. return 'LL' // one day. longer, like "September 9 2014"
  146. }
  147. }
  148. // Date Setting/Unsetting
  149. // -----------------------------------------------------------------------------------------------------------------
  150. setDate(date) {
  151. let currentDateProfile = this.get('dateProfile')
  152. let newDateProfile = this.dateProfileGenerator.build(date, undefined, true) // forceToValid=true
  153. if (
  154. !currentDateProfile ||
  155. !currentDateProfile.activeUnzonedRange.equals(newDateProfile.activeUnzonedRange)
  156. ) {
  157. this.set('dateProfile', newDateProfile)
  158. }
  159. }
  160. unsetDate() {
  161. this.unset('dateProfile')
  162. }
  163. // Event Data
  164. // -----------------------------------------------------------------------------------------------------------------
  165. fetchInitialEvents(dateProfile, callback) {
  166. let calendar = this.calendar
  167. let forceAllDay = dateProfile.isRangeAllDay && !this.usesMinMaxTime
  168. calendar.requestEvents(
  169. calendar.msToMoment(dateProfile.activeUnzonedRange.startMs, forceAllDay),
  170. calendar.msToMoment(dateProfile.activeUnzonedRange.endMs, forceAllDay),
  171. callback
  172. )
  173. }
  174. bindEventChanges() {
  175. this.listenTo(this.calendar, 'eventsReset', this.resetEvents) // TODO: make this a real event
  176. }
  177. unbindEventChanges() {
  178. this.stopListeningTo(this.calendar, 'eventsReset')
  179. }
  180. setEvents(eventsPayload) {
  181. this.set('currentEvents', eventsPayload)
  182. this.set('hasEvents', true)
  183. }
  184. unsetEvents() {
  185. this.unset('currentEvents')
  186. this.unset('hasEvents')
  187. }
  188. resetEvents(eventsPayload) {
  189. this.startBatchRender()
  190. this.unsetEvents()
  191. this.setEvents(eventsPayload)
  192. this.stopBatchRender()
  193. }
  194. // Date High-level Rendering
  195. // -----------------------------------------------------------------------------------------------------------------
  196. requestDateRender(dateProfile) {
  197. this.requestRender(() => {
  198. this.executeDateRender(dateProfile)
  199. }, 'date', 'init')
  200. }
  201. requestDateUnrender() {
  202. this.requestRender(() => {
  203. this.executeDateUnrender()
  204. }, 'date', 'destroy')
  205. }
  206. // if dateProfile not specified, uses current
  207. executeDateRender(dateProfile) {
  208. super.executeDateRender(dateProfile)
  209. if (this['render']) {
  210. this['render']() // TODO: deprecate
  211. }
  212. this.trigger('datesRendered')
  213. this.addScroll({ isDateInit: true })
  214. this.startNowIndicator() // shouldn't render yet because updateSize will be called soon
  215. }
  216. executeDateUnrender() {
  217. this.unselect()
  218. this.stopNowIndicator()
  219. this.trigger('before:datesUnrendered')
  220. if (this['destroy']) {
  221. this['destroy']() // TODO: deprecate
  222. }
  223. super.executeDateUnrender()
  224. }
  225. // "Base" rendering
  226. // -----------------------------------------------------------------------------------------------------------------
  227. bindBaseRenderHandlers() {
  228. this.on('datesRendered', () => {
  229. this.whenSizeUpdated(
  230. this.triggerViewRender
  231. )
  232. })
  233. this.on('before:datesUnrendered', () => {
  234. this.triggerViewDestroy()
  235. })
  236. }
  237. triggerViewRender() {
  238. this.publiclyTrigger('viewRender', {
  239. context: this,
  240. args: [ this, this.el ]
  241. })
  242. }
  243. triggerViewDestroy() {
  244. this.publiclyTrigger('viewDestroy', {
  245. context: this,
  246. args: [ this, this.el ]
  247. })
  248. }
  249. // Event High-level Rendering
  250. // -----------------------------------------------------------------------------------------------------------------
  251. requestEventsRender(eventsPayload) {
  252. this.requestRender(() => {
  253. this.executeEventRender(eventsPayload)
  254. this.whenSizeUpdated(
  255. this.triggerAfterEventsRendered
  256. )
  257. }, 'event', 'init')
  258. }
  259. requestEventsUnrender() {
  260. this.requestRender(() => {
  261. this.triggerBeforeEventsDestroyed()
  262. this.executeEventUnrender()
  263. }, 'event', 'destroy')
  264. }
  265. // Business Hour High-level Rendering
  266. // -----------------------------------------------------------------------------------------------------------------
  267. requestBusinessHoursRender(businessHourGenerator) {
  268. this.requestRender(() => {
  269. this.renderBusinessHours(businessHourGenerator)
  270. }, 'businessHours', 'init')
  271. }
  272. requestBusinessHoursUnrender() {
  273. this.requestRender(() => {
  274. this.unrenderBusinessHours()
  275. }, 'businessHours', 'destroy')
  276. }
  277. // Misc view rendering utils
  278. // -----------------------------------------------------------------------------------------------------------------
  279. // Binds DOM handlers to elements that reside outside the view container, such as the document
  280. bindGlobalHandlers() {
  281. super.bindGlobalHandlers()
  282. this.listenTo(GlobalEmitter.get(), {
  283. touchstart: this.processUnselect,
  284. mousedown: this.handleDocumentMousedown
  285. })
  286. }
  287. // Unbinds DOM handlers from elements that reside outside the view container
  288. unbindGlobalHandlers() {
  289. super.unbindGlobalHandlers()
  290. this.stopListeningTo(GlobalEmitter.get())
  291. }
  292. /* Now Indicator
  293. ------------------------------------------------------------------------------------------------------------------*/
  294. // Immediately render the current time indicator and begins re-rendering it at an interval,
  295. // which is defined by this.getNowIndicatorUnit().
  296. // TODO: somehow do this for the current whole day's background too
  297. startNowIndicator() {
  298. let unit
  299. let update
  300. let delay // ms wait value
  301. if (this.opt('nowIndicator')) {
  302. unit = this.getNowIndicatorUnit()
  303. if (unit) {
  304. update = this.updateNowIndicator.bind(this)
  305. this.initialNowDate = this.calendar.getNow()
  306. this.initialNowQueriedMs = new Date().valueOf()
  307. // wait until the beginning of the next interval
  308. delay = this.initialNowDate.clone().startOf(unit).add(1, unit).valueOf() - this.initialNowDate.valueOf()
  309. this.nowIndicatorTimeoutID = setTimeout(() => {
  310. this.nowIndicatorTimeoutID = null
  311. update()
  312. delay = +moment.duration(1, unit)
  313. delay = Math.max(100, delay) // prevent too frequent
  314. this.nowIndicatorIntervalID = setInterval(update, delay) // update every interval
  315. }, delay)
  316. }
  317. // rendering will be initiated in updateSize
  318. }
  319. }
  320. // rerenders the now indicator, computing the new current time from the amount of time that has passed
  321. // since the initial getNow call.
  322. updateNowIndicator() {
  323. if (
  324. this.isDatesRendered &&
  325. this.initialNowDate // activated before?
  326. ) {
  327. this.unrenderNowIndicator() // won't unrender if unnecessary
  328. this.renderNowIndicator(
  329. this.initialNowDate.clone().add(new Date().valueOf() - this.initialNowQueriedMs) // add ms
  330. )
  331. this.isNowIndicatorRendered = true
  332. }
  333. }
  334. // Immediately unrenders the view's current time indicator and stops any re-rendering timers.
  335. // Won't cause side effects if indicator isn't rendered.
  336. stopNowIndicator() {
  337. if (this.isNowIndicatorRendered) {
  338. if (this.nowIndicatorTimeoutID) {
  339. clearTimeout(this.nowIndicatorTimeoutID)
  340. this.nowIndicatorTimeoutID = null
  341. }
  342. if (this.nowIndicatorIntervalID) {
  343. clearInterval(this.nowIndicatorIntervalID)
  344. this.nowIndicatorIntervalID = null
  345. }
  346. this.unrenderNowIndicator()
  347. this.isNowIndicatorRendered = false
  348. }
  349. }
  350. /* Dimensions
  351. ------------------------------------------------------------------------------------------------------------------*/
  352. updateSize(totalHeight, isAuto, isResize) {
  353. if (this['setHeight']) { // for legacy API
  354. this['setHeight'](totalHeight, isAuto)
  355. } else {
  356. super.updateSize(totalHeight, isAuto, isResize)
  357. }
  358. this.updateNowIndicator()
  359. }
  360. /* Scroller
  361. ------------------------------------------------------------------------------------------------------------------*/
  362. addScroll(scroll) {
  363. let queuedScroll = this.queuedScroll || (this.queuedScroll = {})
  364. assignTo(queuedScroll, scroll)
  365. }
  366. popScroll() {
  367. this.applyQueuedScroll()
  368. this.queuedScroll = null
  369. }
  370. applyQueuedScroll() {
  371. if (this.queuedScroll) {
  372. this.applyScroll(this.queuedScroll)
  373. }
  374. }
  375. queryScroll() {
  376. let scroll = {}
  377. if (this.isDatesRendered) {
  378. assignTo(scroll, this.queryDateScroll())
  379. }
  380. return scroll
  381. }
  382. applyScroll(scroll) {
  383. if (scroll.isDateInit && this.isDatesRendered) {
  384. assignTo(scroll, this.computeInitialDateScroll())
  385. }
  386. if (this.isDatesRendered) {
  387. this.applyDateScroll(scroll)
  388. }
  389. }
  390. computeInitialDateScroll() {
  391. return {} // subclasses must implement
  392. }
  393. queryDateScroll() {
  394. return {} // subclasses must implement
  395. }
  396. applyDateScroll(scroll) {
  397. // subclasses must implement
  398. }
  399. /* Event Drag-n-Drop
  400. ------------------------------------------------------------------------------------------------------------------*/
  401. reportEventDrop(eventInstance, eventMutation, el, ev) {
  402. let eventManager = this.calendar.eventManager
  403. let undoFunc = eventManager.mutateEventsWithId(
  404. eventInstance.def.id,
  405. eventMutation
  406. )
  407. let dateMutation = eventMutation.dateMutation
  408. // update the EventInstance, for handlers
  409. if (dateMutation) {
  410. eventInstance.dateProfile = dateMutation.buildNewDateProfile(
  411. eventInstance.dateProfile,
  412. this.calendar
  413. )
  414. }
  415. this.triggerEventDrop(
  416. eventInstance,
  417. // a drop doesn't necessarily mean a date mutation (ex: resource change)
  418. (dateMutation && dateMutation.dateDelta) || moment.duration(),
  419. undoFunc,
  420. el, ev
  421. )
  422. }
  423. // Triggers event-drop handlers that have subscribed via the API
  424. triggerEventDrop(eventInstance, dateDelta, undoFunc, el, ev) {
  425. this.publiclyTrigger('eventDrop', {
  426. context: el,
  427. args: [
  428. eventInstance.toLegacy(),
  429. dateDelta,
  430. undoFunc,
  431. ev,
  432. {}, // {} = jqui dummy
  433. this
  434. ]
  435. })
  436. }
  437. /* External Element Drag-n-Drop
  438. ------------------------------------------------------------------------------------------------------------------*/
  439. // Must be called when an external element, via jQuery UI, has been dropped onto the calendar.
  440. // `meta` is the parsed data that has been embedded into the dragging event.
  441. // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event.
  442. reportExternalDrop(singleEventDef, isEvent, isSticky, el, ev) {
  443. if (isEvent) {
  444. this.calendar.eventManager.addEventDef(singleEventDef, isSticky)
  445. }
  446. this.triggerExternalDrop(singleEventDef, isEvent, el, ev)
  447. }
  448. // Triggers external-drop handlers that have subscribed via the API
  449. triggerExternalDrop(singleEventDef, isEvent, el, ev) {
  450. // trigger 'drop' regardless of whether element represents an event
  451. this.publiclyTrigger('drop', {
  452. context: el,
  453. args: [
  454. singleEventDef.dateProfile.start.clone(),
  455. ev,
  456. this
  457. ]
  458. })
  459. if (isEvent) {
  460. // signal an external event landed
  461. this.publiclyTrigger('eventReceive', {
  462. context: this,
  463. args: [
  464. singleEventDef.buildInstance().toLegacy(),
  465. this
  466. ]
  467. })
  468. }
  469. }
  470. /* Event Resizing
  471. ------------------------------------------------------------------------------------------------------------------*/
  472. // Must be called when an event in the view has been resized to a new length
  473. reportEventResize(eventInstance, eventMutation, el, ev) {
  474. let eventManager = this.calendar.eventManager
  475. let undoFunc = eventManager.mutateEventsWithId(
  476. eventInstance.def.id,
  477. eventMutation
  478. )
  479. // update the EventInstance, for handlers
  480. eventInstance.dateProfile = eventMutation.dateMutation.buildNewDateProfile(
  481. eventInstance.dateProfile,
  482. this.calendar
  483. )
  484. this.triggerEventResize(
  485. eventInstance,
  486. eventMutation.dateMutation.endDelta,
  487. undoFunc,
  488. el, ev
  489. )
  490. }
  491. // Triggers event-resize handlers that have subscribed via the API
  492. triggerEventResize(eventInstance, durationDelta, undoFunc, el, ev) {
  493. this.publiclyTrigger('eventResize', {
  494. context: el,
  495. args: [
  496. eventInstance.toLegacy(),
  497. durationDelta,
  498. undoFunc,
  499. ev,
  500. {}, // {} = jqui dummy
  501. this
  502. ]
  503. })
  504. }
  505. /* Selection (time range)
  506. ------------------------------------------------------------------------------------------------------------------*/
  507. // Selects a date span on the view. `start` and `end` are both Moments.
  508. // `ev` is the native mouse event that begin the interaction.
  509. select(footprint, ev?) {
  510. this.unselect(ev)
  511. this.renderSelectionFootprint(footprint)
  512. this.reportSelection(footprint, ev)
  513. }
  514. renderSelectionFootprint(footprint) {
  515. if (this['renderSelection']) { // legacy method in custom view classes
  516. this['renderSelection'](
  517. footprint.toLegacy(this.calendar)
  518. )
  519. } else {
  520. super.renderSelectionFootprint(footprint)
  521. }
  522. }
  523. // Called when a new selection is made. Updates internal state and triggers handlers.
  524. reportSelection(footprint, ev?) {
  525. this.isSelected = true
  526. this.triggerSelect(footprint, ev)
  527. }
  528. // Triggers handlers to 'select'
  529. triggerSelect(footprint, ev?) {
  530. let dateProfile = this.calendar.footprintToDateProfile(footprint) // abuse of "Event"DateProfile?
  531. this.publiclyTrigger('select', {
  532. context: this,
  533. args: [
  534. dateProfile.start,
  535. dateProfile.end,
  536. ev,
  537. this
  538. ]
  539. })
  540. }
  541. // Undoes a selection. updates in the internal state and triggers handlers.
  542. // `ev` is the native mouse event that began the interaction.
  543. unselect(ev?) {
  544. if (this.isSelected) {
  545. this.isSelected = false
  546. if (this['destroySelection']) {
  547. this['destroySelection']() // TODO: deprecate
  548. }
  549. this.unrenderSelection()
  550. this.publiclyTrigger('unselect', {
  551. context: this,
  552. args: [ ev, this ]
  553. })
  554. }
  555. }
  556. /* Event Selection
  557. ------------------------------------------------------------------------------------------------------------------*/
  558. selectEventInstance(eventInstance) {
  559. if (
  560. !this.selectedEventInstance ||
  561. this.selectedEventInstance !== eventInstance
  562. ) {
  563. this.unselectEventInstance()
  564. this.getEventSegs().forEach(function(seg) {
  565. if (
  566. seg.footprint.eventInstance === eventInstance &&
  567. seg.el // necessary?
  568. ) {
  569. seg.el.classList.add('fc-selected')
  570. }
  571. })
  572. this.selectedEventInstance = eventInstance
  573. }
  574. }
  575. unselectEventInstance() {
  576. if (this.selectedEventInstance) {
  577. this.getEventSegs().forEach(function(seg) {
  578. if (seg.el) { // necessary?
  579. seg.el.classList.remove('fc-selected')
  580. }
  581. })
  582. this.selectedEventInstance = null
  583. }
  584. }
  585. isEventDefSelected(eventDef) {
  586. // event references might change on refetchEvents(), while selectedEventInstance doesn't,
  587. // so compare IDs
  588. return this.selectedEventInstance && this.selectedEventInstance.def.id === eventDef.id
  589. }
  590. /* Mouse / Touch Unselecting (time range & event unselection)
  591. ------------------------------------------------------------------------------------------------------------------*/
  592. // TODO: move consistently to down/start or up/end?
  593. // TODO: don't kill previous selection if touch scrolling
  594. handleDocumentMousedown(ev) {
  595. if (isPrimaryMouseButton(ev)) {
  596. this.processUnselect(ev)
  597. }
  598. }
  599. processUnselect(ev) {
  600. this.processRangeUnselect(ev)
  601. this.processEventUnselect(ev)
  602. }
  603. processRangeUnselect(ev) {
  604. let ignore
  605. // is there a time-range selection?
  606. if (this.isSelected && this.opt('unselectAuto')) {
  607. // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
  608. ignore = this.opt('unselectCancel')
  609. if (!ignore || !elementClosest(ev.target, ignore)) {
  610. this.unselect(ev)
  611. }
  612. }
  613. }
  614. processEventUnselect(ev) {
  615. if (this.selectedEventInstance) {
  616. if (!elementClosest(ev.target, '.fc-selected')) {
  617. this.unselectEventInstance()
  618. }
  619. }
  620. }
  621. /* Triggers
  622. ------------------------------------------------------------------------------------------------------------------*/
  623. triggerBaseRendered() {
  624. this.publiclyTrigger('viewRender', {
  625. context: this,
  626. args: [ this, this.el ]
  627. })
  628. }
  629. triggerBaseUnrendered() {
  630. this.publiclyTrigger('viewDestroy', {
  631. context: this,
  632. args: [ this, this.el ]
  633. })
  634. }
  635. // Triggers handlers to 'dayClick'
  636. // Span has start/end of the clicked area. Only the start is useful.
  637. triggerDayClick(footprint, dayEl, ev) {
  638. let dateProfile = this.calendar.footprintToDateProfile(footprint) // abuse of "Event"DateProfile?
  639. this.publiclyTrigger('dayClick', {
  640. context: dayEl,
  641. args: [ dateProfile.start, ev, this ]
  642. })
  643. }
  644. /* Date Utils
  645. ------------------------------------------------------------------------------------------------------------------*/
  646. // For DateComponent::getDayClasses
  647. isDateInOtherMonth(date, dateProfile) {
  648. return false
  649. }
  650. // Arguments after name will be forwarded to a hypothetical function value
  651. // WARNING: passed-in arguments will be given to generator functions as-is and can cause side-effects.
  652. // Always clone your objects if you fear mutation.
  653. getUnzonedRangeOption(name) {
  654. let val = this.opt(name)
  655. if (typeof val === 'function') {
  656. val = val.apply(
  657. null,
  658. Array.prototype.slice.call(arguments, 1)
  659. )
  660. }
  661. if (val) {
  662. return this.calendar.parseUnzonedRange(val)
  663. }
  664. }
  665. /* Hidden Days
  666. ------------------------------------------------------------------------------------------------------------------*/
  667. // Initializes internal variables related to calculating hidden days-of-week
  668. initHiddenDays() {
  669. let hiddenDays = this.opt('hiddenDays') || [] // array of day-of-week indices that are hidden
  670. let isHiddenDayHash = [] // is the day-of-week hidden? (hash with day-of-week-index -> bool)
  671. let dayCnt = 0
  672. let i
  673. if (this.opt('weekends') === false) {
  674. hiddenDays.push(0, 6) // 0=sunday, 6=saturday
  675. }
  676. for (i = 0; i < 7; i++) {
  677. if (
  678. !(isHiddenDayHash[i] = hiddenDays.indexOf(i) !== -1)
  679. ) {
  680. dayCnt++
  681. }
  682. }
  683. if (!dayCnt) {
  684. throw new Error('invalid hiddenDays') // all days were hidden? bad.
  685. }
  686. this.isHiddenDayHash = isHiddenDayHash
  687. }
  688. // Remove days from the beginning and end of the range that are computed as hidden.
  689. // If the whole range is trimmed off, returns null
  690. trimHiddenDays(inputUnzonedRange) {
  691. let start = inputUnzonedRange.getStart()
  692. let end = inputUnzonedRange.getEnd()
  693. if (start) {
  694. start = this.skipHiddenDays(start)
  695. }
  696. if (end) {
  697. end = this.skipHiddenDays(end, -1, true)
  698. }
  699. if (start === null || end === null || start < end) {
  700. return new UnzonedRange(start, end)
  701. }
  702. return null
  703. }
  704. // Is the current day hidden?
  705. // `day` is a day-of-week index (0-6), or a Moment
  706. isHiddenDay(day) {
  707. if (moment.isMoment(day)) {
  708. day = day.day()
  709. }
  710. return this.isHiddenDayHash[day]
  711. }
  712. // Incrementing the current day until it is no longer a hidden day, returning a copy.
  713. // DOES NOT CONSIDER validUnzonedRange!
  714. // If the initial value of `date` is not a hidden day, don't do anything.
  715. // Pass `isExclusive` as `true` if you are dealing with an end date.
  716. // `inc` defaults to `1` (increment one day forward each time)
  717. skipHiddenDays(date, inc= 1, isExclusive= false) {
  718. let out = date.clone()
  719. while (
  720. this.isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7]
  721. ) {
  722. out.add(inc, 'days')
  723. }
  724. return out
  725. }
  726. }
  727. View.prototype.usesMinMaxTime = false
  728. View.prototype.dateProfileGeneratorClass = DateProfileGenerator
  729. View.watch('displayingDates', [ 'isInDom', 'dateProfile' ], function(deps) {
  730. this.requestDateRender(deps.dateProfile)
  731. }, function() {
  732. this.requestDateUnrender()
  733. })
  734. View.watch('displayingBusinessHours', [ 'displayingDates', 'businessHourGenerator' ], function(deps) {
  735. this.requestBusinessHoursRender(deps.businessHourGenerator)
  736. }, function() {
  737. this.requestBusinessHoursUnrender()
  738. })
  739. View.watch('initialEvents', [ 'dateProfile' ], function(deps, callback) {
  740. this.fetchInitialEvents(deps.dateProfile, callback)
  741. }, null, true) // async=true
  742. View.watch('bindingEvents', [ 'initialEvents' ], function(deps) {
  743. this.setEvents(deps.initialEvents)
  744. this.bindEventChanges()
  745. }, function() {
  746. this.unbindEventChanges()
  747. this.unsetEvents()
  748. })
  749. View.watch('displayingEvents', [ 'displayingDates', 'hasEvents' ], function() {
  750. this.requestEventsRender(this.get('currentEvents'))
  751. }, function() {
  752. this.requestEventsUnrender()
  753. })
  754. View.watch('title', [ 'dateProfile' ], function(deps) {
  755. return (this.title = this.computeTitle(deps.dateProfile)) // assign to View for legacy reasons
  756. })
  757. View.watch('legacyDateProps', [ 'dateProfile' ], function(deps) {
  758. let calendar = this.calendar
  759. let dateProfile = deps.dateProfile
  760. // DEPRECATED, but we need to keep it updated...
  761. this.start = calendar.msToMoment(dateProfile.activeUnzonedRange.startMs, dateProfile.isRangeAllDay)
  762. this.end = calendar.msToMoment(dateProfile.activeUnzonedRange.endMs, dateProfile.isRangeAllDay)
  763. this.intervalStart = calendar.msToMoment(dateProfile.currentUnzonedRange.startMs, dateProfile.isRangeAllDay)
  764. this.intervalEnd = calendar.msToMoment(dateProfile.currentUnzonedRange.endMs, dateProfile.isRangeAllDay)
  765. })