View.ts 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035
  1. import * as $ from 'jquery'
  2. import * as moment from 'moment'
  3. import { parseFieldSpecs, proxy, isPrimaryMouseButton } from './util'
  4. import { assignTo } from './util/object'
  5. import RenderQueue from './common/RenderQueue'
  6. import Calendar from './Calendar'
  7. import DateProfileGenerator from './DateProfileGenerator'
  8. import InteractiveDateComponent from './component/InteractiveDateComponent'
  9. import GlobalEmitter from './common/GlobalEmitter'
  10. import UnzonedRange from './models/UnzonedRange'
  11. import EventInstance from './models/event/EventInstance'
  12. /* An abstract class from which other views inherit from
  13. ----------------------------------------------------------------------------------------------------------------------*/
  14. export default abstract class View extends InteractiveDateComponent {
  15. type: string // subclass' view name (string)
  16. name: string // deprecated. use `type` instead
  17. title: string // the text that will be displayed in the header's title
  18. calendar: Calendar // owner Calendar object
  19. viewSpec: any
  20. options: any // hash containing all options. already merged with view-specific-options
  21. renderQueue: RenderQueue
  22. batchRenderDepth: number = 0
  23. queuedScroll: object
  24. isSelected: boolean = false // boolean whether a range of time is user-selected or not
  25. selectedEventInstance: EventInstance
  26. eventOrderSpecs: any // criteria for ordering events when they have same date/time
  27. // for date utils, computed from options
  28. isHiddenDayHash: boolean[]
  29. // now indicator
  30. isNowIndicatorRendered: boolean
  31. initialNowDate: moment.Moment // result first getNow call
  32. initialNowQueriedMs: number // ms time the getNow was called
  33. nowIndicatorTimeoutID: any // for refresh timing of now indicator
  34. nowIndicatorIntervalID: any // "
  35. dateProfileGeneratorClass: any // initialized after class
  36. dateProfileGenerator: any
  37. // whether minTime/maxTime will affect the activeUnzonedRange. Views must opt-in.
  38. // initialized after class
  39. usesMinMaxTime: boolean
  40. // DEPRECATED
  41. start: moment.Moment // use activeUnzonedRange
  42. end: moment.Moment // use activeUnzonedRange
  43. intervalStart: moment.Moment // use currentUnzonedRange
  44. intervalEnd: moment.Moment // use currentUnzonedRange
  45. constructor(calendar, viewSpec) {
  46. super(null, viewSpec.options)
  47. this.calendar = calendar
  48. this.viewSpec = viewSpec
  49. // shortcuts
  50. this.type = viewSpec.type
  51. // .name is deprecated
  52. this.name = this.type
  53. this.initRenderQueue()
  54. this.initHiddenDays()
  55. this.dateProfileGenerator = new this.dateProfileGeneratorClass(this)
  56. this.bindBaseRenderHandlers()
  57. this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder'))
  58. // legacy
  59. if (this['initialize']) {
  60. this['initialize']()
  61. }
  62. }
  63. _getView() {
  64. return this
  65. }
  66. // Retrieves an option with the given name
  67. opt(name) {
  68. return this.options[name]
  69. }
  70. /* Render Queue
  71. ------------------------------------------------------------------------------------------------------------------*/
  72. initRenderQueue() {
  73. this.renderQueue = new RenderQueue({
  74. event: this.opt('eventRenderWait')
  75. })
  76. this.renderQueue.on('start', this.onRenderQueueStart.bind(this))
  77. this.renderQueue.on('stop', this.onRenderQueueStop.bind(this))
  78. this.on('before:change', this.startBatchRender)
  79. this.on('change', this.stopBatchRender)
  80. }
  81. onRenderQueueStart() {
  82. this.calendar.freezeContentHeight()
  83. this.addScroll(this.queryScroll())
  84. }
  85. onRenderQueueStop() {
  86. if (this.calendar.updateViewSize()) { // success?
  87. this.popScroll()
  88. }
  89. this.calendar.thawContentHeight()
  90. }
  91. startBatchRender() {
  92. if (!(this.batchRenderDepth++)) {
  93. this.renderQueue.pause()
  94. }
  95. }
  96. stopBatchRender() {
  97. if (!(--this.batchRenderDepth)) {
  98. this.renderQueue.resume()
  99. }
  100. }
  101. requestRender(func, namespace, actionType) {
  102. this.renderQueue.queue(func, namespace, actionType)
  103. }
  104. // given func will auto-bind to `this`
  105. whenSizeUpdated(func) {
  106. if (this.renderQueue.isRunning) {
  107. this.renderQueue.one('stop', func.bind(this))
  108. } else {
  109. func.call(this)
  110. }
  111. }
  112. /* Title and Date Formatting
  113. ------------------------------------------------------------------------------------------------------------------*/
  114. // Computes what the title at the top of the calendar should be for this view
  115. computeTitle(dateProfile) {
  116. let unzonedRange
  117. // for views that span a large unit of time, show the proper interval, ignoring stray days before and after
  118. if (/^(year|month)$/.test(dateProfile.currentRangeUnit)) {
  119. unzonedRange = dateProfile.currentUnzonedRange
  120. } else { // for day units or smaller, use the actual day range
  121. unzonedRange = dateProfile.activeUnzonedRange
  122. }
  123. return this.formatRange(
  124. {
  125. start: this.calendar.msToMoment(unzonedRange.startMs, dateProfile.isRangeAllDay),
  126. end: this.calendar.msToMoment(unzonedRange.endMs, dateProfile.isRangeAllDay)
  127. },
  128. dateProfile.isRangeAllDay,
  129. this.opt('titleFormat') || this.computeTitleFormat(dateProfile),
  130. this.opt('titleRangeSeparator')
  131. )
  132. }
  133. // Generates the format string that should be used to generate the title for the current date range.
  134. // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
  135. computeTitleFormat(dateProfile) {
  136. let currentRangeUnit = dateProfile.currentRangeUnit
  137. if (currentRangeUnit === 'year') {
  138. return 'YYYY'
  139. } else if (currentRangeUnit === 'month') {
  140. return this.opt('monthYearFormat') // like "September 2014"
  141. } else if (dateProfile.currentUnzonedRange.as('days') > 1) {
  142. return 'll' // multi-day range. shorter, like "Sep 9 - 10 2014"
  143. } else {
  144. return 'LL' // one day. longer, like "September 9 2014"
  145. }
  146. }
  147. // Date Setting/Unsetting
  148. // -----------------------------------------------------------------------------------------------------------------
  149. setDate(date) {
  150. let currentDateProfile = this.get('dateProfile')
  151. let newDateProfile = this.dateProfileGenerator.build(date, undefined, true) // forceToValid=true
  152. if (
  153. !currentDateProfile ||
  154. !currentDateProfile.activeUnzonedRange.equals(newDateProfile.activeUnzonedRange)
  155. ) {
  156. this.set('dateProfile', newDateProfile)
  157. }
  158. }
  159. unsetDate() {
  160. this.unset('dateProfile')
  161. }
  162. // Event Data
  163. // -----------------------------------------------------------------------------------------------------------------
  164. fetchInitialEvents(dateProfile, callback) {
  165. let calendar = this.calendar
  166. let forceAllDay = dateProfile.isRangeAllDay && !this.usesMinMaxTime
  167. calendar.requestEvents(
  168. calendar.msToMoment(dateProfile.activeUnzonedRange.startMs, forceAllDay),
  169. calendar.msToMoment(dateProfile.activeUnzonedRange.endMs, forceAllDay),
  170. callback
  171. )
  172. }
  173. bindEventChanges() {
  174. this.listenTo(this.calendar, 'eventsReset', this.resetEvents) // TODO: make this a real event
  175. }
  176. unbindEventChanges() {
  177. this.stopListeningTo(this.calendar, 'eventsReset')
  178. }
  179. setEvents(eventsPayload) {
  180. this.set('currentEvents', eventsPayload)
  181. this.set('hasEvents', true)
  182. }
  183. unsetEvents() {
  184. this.unset('currentEvents')
  185. this.unset('hasEvents')
  186. }
  187. resetEvents(eventsPayload) {
  188. this.startBatchRender()
  189. this.unsetEvents()
  190. this.setEvents(eventsPayload)
  191. this.stopBatchRender()
  192. }
  193. // Date High-level Rendering
  194. // -----------------------------------------------------------------------------------------------------------------
  195. requestDateRender(dateProfile) {
  196. this.requestRender(() => {
  197. this.executeDateRender(dateProfile)
  198. }, 'date', 'init')
  199. }
  200. requestDateUnrender() {
  201. this.requestRender(() => {
  202. this.executeDateUnrender()
  203. }, 'date', 'destroy')
  204. }
  205. // if dateProfile not specified, uses current
  206. executeDateRender(dateProfile) {
  207. super.executeDateRender(dateProfile)
  208. if (this['render']) {
  209. this['render']() // TODO: deprecate
  210. }
  211. this.trigger('datesRendered')
  212. this.addScroll({ isDateInit: true })
  213. this.startNowIndicator() // shouldn't render yet because updateSize will be called soon
  214. }
  215. executeDateUnrender() {
  216. this.unselect()
  217. this.stopNowIndicator()
  218. this.trigger('before:datesUnrendered')
  219. if (this['destroy']) {
  220. this['destroy']() // TODO: deprecate
  221. }
  222. super.executeDateUnrender()
  223. }
  224. // "Base" rendering
  225. // -----------------------------------------------------------------------------------------------------------------
  226. bindBaseRenderHandlers() {
  227. this.on('datesRendered', () => {
  228. this.whenSizeUpdated(
  229. this.triggerViewRender
  230. )
  231. })
  232. this.on('before:datesUnrendered', () => {
  233. this.triggerViewDestroy()
  234. })
  235. }
  236. triggerViewRender() {
  237. this.publiclyTrigger('viewRender', {
  238. context: this,
  239. args: [ this, this.el ]
  240. })
  241. }
  242. triggerViewDestroy() {
  243. this.publiclyTrigger('viewDestroy', {
  244. context: this,
  245. args: [ this, this.el ]
  246. })
  247. }
  248. // Event High-level Rendering
  249. // -----------------------------------------------------------------------------------------------------------------
  250. requestEventsRender(eventsPayload) {
  251. this.requestRender(() => {
  252. this.executeEventRender(eventsPayload)
  253. this.whenSizeUpdated(
  254. this.triggerAfterEventsRendered
  255. )
  256. }, 'event', 'init')
  257. }
  258. requestEventsUnrender() {
  259. this.requestRender(() => {
  260. this.triggerBeforeEventsDestroyed()
  261. this.executeEventUnrender()
  262. }, 'event', 'destroy')
  263. }
  264. // Business Hour High-level Rendering
  265. // -----------------------------------------------------------------------------------------------------------------
  266. requestBusinessHoursRender(businessHourGenerator) {
  267. this.requestRender(() => {
  268. this.renderBusinessHours(businessHourGenerator)
  269. }, 'businessHours', 'init')
  270. }
  271. requestBusinessHoursUnrender() {
  272. this.requestRender(() => {
  273. this.unrenderBusinessHours()
  274. }, 'businessHours', 'destroy')
  275. }
  276. // Misc view rendering utils
  277. // -----------------------------------------------------------------------------------------------------------------
  278. // Binds DOM handlers to elements that reside outside the view container, such as the document
  279. bindGlobalHandlers() {
  280. super.bindGlobalHandlers()
  281. this.listenTo(GlobalEmitter.get(), {
  282. touchstart: this.processUnselect,
  283. mousedown: this.handleDocumentMousedown
  284. })
  285. }
  286. // Unbinds DOM handlers from elements that reside outside the view container
  287. unbindGlobalHandlers() {
  288. super.unbindGlobalHandlers()
  289. this.stopListeningTo(GlobalEmitter.get())
  290. }
  291. /* Now Indicator
  292. ------------------------------------------------------------------------------------------------------------------*/
  293. // Immediately render the current time indicator and begins re-rendering it at an interval,
  294. // which is defined by this.getNowIndicatorUnit().
  295. // TODO: somehow do this for the current whole day's background too
  296. startNowIndicator() {
  297. let unit
  298. let update
  299. let delay // ms wait value
  300. if (this.opt('nowIndicator')) {
  301. unit = this.getNowIndicatorUnit()
  302. if (unit) {
  303. update = proxy(this, 'updateNowIndicator') // bind to `this`
  304. this.initialNowDate = this.calendar.getNow()
  305. this.initialNowQueriedMs = new Date().valueOf()
  306. // wait until the beginning of the next interval
  307. delay = this.initialNowDate.clone().startOf(unit).add(1, unit).valueOf() - this.initialNowDate.valueOf()
  308. this.nowIndicatorTimeoutID = setTimeout(() => {
  309. this.nowIndicatorTimeoutID = null
  310. update()
  311. delay = +moment.duration(1, unit)
  312. delay = Math.max(100, delay) // prevent too frequent
  313. this.nowIndicatorIntervalID = setInterval(update, delay) // update every interval
  314. }, delay)
  315. }
  316. // rendering will be initiated in updateSize
  317. }
  318. }
  319. // rerenders the now indicator, computing the new current time from the amount of time that has passed
  320. // since the initial getNow call.
  321. updateNowIndicator() {
  322. if (
  323. this.isDatesRendered &&
  324. this.initialNowDate // activated before?
  325. ) {
  326. this.unrenderNowIndicator() // won't unrender if unnecessary
  327. this.renderNowIndicator(
  328. this.initialNowDate.clone().add(new Date().valueOf() - this.initialNowQueriedMs) // add ms
  329. )
  330. this.isNowIndicatorRendered = true
  331. }
  332. }
  333. // Immediately unrenders the view's current time indicator and stops any re-rendering timers.
  334. // Won't cause side effects if indicator isn't rendered.
  335. stopNowIndicator() {
  336. if (this.isNowIndicatorRendered) {
  337. if (this.nowIndicatorTimeoutID) {
  338. clearTimeout(this.nowIndicatorTimeoutID)
  339. this.nowIndicatorTimeoutID = null
  340. }
  341. if (this.nowIndicatorIntervalID) {
  342. clearInterval(this.nowIndicatorIntervalID)
  343. this.nowIndicatorIntervalID = null
  344. }
  345. this.unrenderNowIndicator()
  346. this.isNowIndicatorRendered = false
  347. }
  348. }
  349. /* Dimensions
  350. ------------------------------------------------------------------------------------------------------------------*/
  351. updateSize(totalHeight, isAuto, isResize) {
  352. if (this['setHeight']) { // for legacy API
  353. this['setHeight'](totalHeight, isAuto)
  354. } else {
  355. super.updateSize(totalHeight, isAuto, isResize)
  356. }
  357. this.updateNowIndicator()
  358. }
  359. /* Scroller
  360. ------------------------------------------------------------------------------------------------------------------*/
  361. addScroll(scroll) {
  362. let queuedScroll = this.queuedScroll || (this.queuedScroll = {})
  363. assignTo(queuedScroll, scroll)
  364. }
  365. popScroll() {
  366. this.applyQueuedScroll()
  367. this.queuedScroll = null
  368. }
  369. applyQueuedScroll() {
  370. if (this.queuedScroll) {
  371. this.applyScroll(this.queuedScroll)
  372. }
  373. }
  374. queryScroll() {
  375. let scroll = {}
  376. if (this.isDatesRendered) {
  377. assignTo(scroll, this.queryDateScroll())
  378. }
  379. return scroll
  380. }
  381. applyScroll(scroll) {
  382. if (scroll.isDateInit && this.isDatesRendered) {
  383. assignTo(scroll, this.computeInitialDateScroll())
  384. }
  385. if (this.isDatesRendered) {
  386. this.applyDateScroll(scroll)
  387. }
  388. }
  389. computeInitialDateScroll() {
  390. return {} // subclasses must implement
  391. }
  392. queryDateScroll() {
  393. return {} // subclasses must implement
  394. }
  395. applyDateScroll(scroll) {
  396. // subclasses must implement
  397. }
  398. /* Event Drag-n-Drop
  399. ------------------------------------------------------------------------------------------------------------------*/
  400. reportEventDrop(eventInstance, eventMutation, el, ev) {
  401. let eventManager = this.calendar.eventManager
  402. let undoFunc = eventManager.mutateEventsWithId(
  403. eventInstance.def.id,
  404. eventMutation
  405. )
  406. let dateMutation = eventMutation.dateMutation
  407. // update the EventInstance, for handlers
  408. if (dateMutation) {
  409. eventInstance.dateProfile = dateMutation.buildNewDateProfile(
  410. eventInstance.dateProfile,
  411. this.calendar
  412. )
  413. }
  414. this.triggerEventDrop(
  415. eventInstance,
  416. // a drop doesn't necessarily mean a date mutation (ex: resource change)
  417. (dateMutation && dateMutation.dateDelta) || moment.duration(),
  418. undoFunc,
  419. el, ev
  420. )
  421. }
  422. // Triggers event-drop handlers that have subscribed via the API
  423. triggerEventDrop(eventInstance, dateDelta, undoFunc, el, ev) {
  424. this.publiclyTrigger('eventDrop', {
  425. context: el,
  426. args: [
  427. eventInstance.toLegacy(),
  428. dateDelta,
  429. undoFunc,
  430. ev,
  431. {}, // {} = jqui dummy
  432. this
  433. ]
  434. })
  435. }
  436. /* External Element Drag-n-Drop
  437. ------------------------------------------------------------------------------------------------------------------*/
  438. // Must be called when an external element, via jQuery UI, has been dropped onto the calendar.
  439. // `meta` is the parsed data that has been embedded into the dragging event.
  440. // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event.
  441. reportExternalDrop(singleEventDef, isEvent, isSticky, el, ev, ui) {
  442. if (isEvent) {
  443. this.calendar.eventManager.addEventDef(singleEventDef, isSticky)
  444. }
  445. this.triggerExternalDrop(singleEventDef, isEvent, el, ev, ui)
  446. }
  447. // Triggers external-drop handlers that have subscribed via the API
  448. triggerExternalDrop(singleEventDef, isEvent, el, ev, ui) {
  449. // trigger 'drop' regardless of whether element represents an event
  450. this.publiclyTrigger('drop', {
  451. context: el,
  452. args: [
  453. singleEventDef.dateProfile.start.clone(),
  454. ev,
  455. ui,
  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 || !$(ev.target).closest(ignore).length) {
  610. this.unselect(ev)
  611. }
  612. }
  613. }
  614. processEventUnselect(ev) {
  615. if (this.selectedEventInstance) {
  616. if (!$(ev.target).closest('.fc-selected').length) {
  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. })