DateComponent.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835
  1. import * as moment from 'moment'
  2. import { attrsToStr, htmlEscape } from '../util/html'
  3. import { dayIDs } from '../util/date'
  4. import momentExt from '../moment-ext'
  5. import { formatRange } from '../date-formatting'
  6. import Component from './Component'
  7. import { eventRangeToEventFootprint } from '../models/event/util'
  8. import EventFootprint from '../models/event/EventFootprint'
  9. export default abstract class DateComponent extends Component {
  10. static guid: number = 0 // TODO: better system for this?
  11. eventRendererClass: any
  12. helperRendererClass: any
  13. businessHourRendererClass: any
  14. fillRendererClass: any
  15. uid: any
  16. childrenByUid: any
  17. isRTL: boolean = false // frequently accessed options
  18. nextDayThreshold: any // "
  19. dateProfile: any // hack
  20. eventRenderer: any
  21. helperRenderer: any
  22. businessHourRenderer: any
  23. fillRenderer: any
  24. hitsNeededDepth: number = 0 // necessary because multiple callers might need the same hits
  25. hasAllDayBusinessHours: boolean = false // TODO: unify with largeUnit and isTimeScale?
  26. isDatesRendered: boolean = false
  27. constructor(_view?, _options?) {
  28. super()
  29. // hack to set options prior to the this.opt calls
  30. if (_view) {
  31. this['view'] = _view
  32. }
  33. if (_options) {
  34. this['options'] = _options
  35. }
  36. this.uid = String(DateComponent.guid++)
  37. this.childrenByUid = {}
  38. this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold'))
  39. this.isRTL = this.opt('isRTL')
  40. if (this.fillRendererClass) {
  41. this.fillRenderer = new this.fillRendererClass(this)
  42. }
  43. if (this.eventRendererClass) { // fillRenderer is optional -----v
  44. this.eventRenderer = new this.eventRendererClass(this, this.fillRenderer)
  45. }
  46. if (this.helperRendererClass && this.eventRenderer) {
  47. this.helperRenderer = new this.helperRendererClass(this, this.eventRenderer)
  48. }
  49. if (this.businessHourRendererClass && this.fillRenderer) {
  50. this.businessHourRenderer = new this.businessHourRendererClass(this, this.fillRenderer)
  51. }
  52. }
  53. addChild(child) {
  54. if (!this.childrenByUid[child.uid]) {
  55. this.childrenByUid[child.uid] = child
  56. return true
  57. }
  58. return false
  59. }
  60. removeChild(child) {
  61. if (this.childrenByUid[child.uid]) {
  62. delete this.childrenByUid[child.uid]
  63. return true
  64. }
  65. return false
  66. }
  67. // TODO: only do if isInDom?
  68. // TODO: make part of Component, along with children/batch-render system?
  69. updateSize(totalHeight, isAuto, isResize) {
  70. this.callChildren('updateSize', arguments)
  71. }
  72. // Options
  73. // -----------------------------------------------------------------------------------------------------------------
  74. opt(name) {
  75. return this._getView().opt(name) // default implementation
  76. }
  77. publiclyTrigger(...args) {
  78. let calendar = this._getCalendar()
  79. return calendar.publiclyTrigger.apply(calendar, args)
  80. }
  81. hasPublicHandlers(...args) {
  82. let calendar = this._getCalendar()
  83. return calendar.hasPublicHandlers.apply(calendar, args)
  84. }
  85. // Date
  86. // -----------------------------------------------------------------------------------------------------------------
  87. executeDateRender(dateProfile) {
  88. this.dateProfile = dateProfile // for rendering
  89. this.renderDates(dateProfile)
  90. this.isDatesRendered = true
  91. this.callChildren('executeDateRender', arguments)
  92. }
  93. executeDateUnrender() { // wrapper
  94. this.callChildren('executeDateUnrender', arguments)
  95. this.dateProfile = null
  96. this.unrenderDates()
  97. this.isDatesRendered = false
  98. }
  99. // date-cell content only
  100. renderDates(dateProfile) {
  101. // subclasses should implement
  102. }
  103. // date-cell content only
  104. unrenderDates() {
  105. // subclasses should override
  106. }
  107. // Now-Indicator
  108. // -----------------------------------------------------------------------------------------------------------------
  109. // Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator
  110. // should be refreshed. If something falsy is returned, no time indicator is rendered at all.
  111. getNowIndicatorUnit() {
  112. // subclasses should implement
  113. }
  114. // Renders a current time indicator at the given datetime
  115. renderNowIndicator(date) {
  116. this.callChildren('renderNowIndicator', arguments)
  117. }
  118. // Undoes the rendering actions from renderNowIndicator
  119. unrenderNowIndicator() {
  120. this.callChildren('unrenderNowIndicator', arguments)
  121. }
  122. // Business Hours
  123. // ---------------------------------------------------------------------------------------------------------------
  124. renderBusinessHours(businessHourGenerator) {
  125. if (this.businessHourRenderer) {
  126. this.businessHourRenderer.render(businessHourGenerator)
  127. }
  128. this.callChildren('renderBusinessHours', arguments)
  129. }
  130. // Unrenders previously-rendered business-hours
  131. unrenderBusinessHours() {
  132. this.callChildren('unrenderBusinessHours', arguments)
  133. if (this.businessHourRenderer) {
  134. this.businessHourRenderer.unrender()
  135. }
  136. }
  137. // Event Displaying
  138. // -----------------------------------------------------------------------------------------------------------------
  139. executeEventRender(eventsPayload) {
  140. if (this.eventRenderer) {
  141. this.eventRenderer.rangeUpdated() // poorly named now
  142. this.eventRenderer.render(eventsPayload)
  143. } else if (this['renderEvents']) { // legacy
  144. this['renderEvents'](convertEventsPayloadToLegacyArray(eventsPayload))
  145. }
  146. this.callChildren('executeEventRender', arguments)
  147. }
  148. executeEventUnrender() {
  149. this.callChildren('executeEventUnrender', arguments)
  150. if (this.eventRenderer) {
  151. this.eventRenderer.unrender()
  152. } else if (this['destroyEvents']) { // legacy
  153. this['destroyEvents']()
  154. }
  155. }
  156. getBusinessHourSegs() { // recursive
  157. let segs = this.getOwnBusinessHourSegs()
  158. this.iterChildren(function(child) {
  159. segs.push.apply(segs, child.getBusinessHourSegs())
  160. })
  161. return segs
  162. }
  163. getOwnBusinessHourSegs() {
  164. if (this.businessHourRenderer) {
  165. return this.businessHourRenderer.getSegs()
  166. }
  167. return []
  168. }
  169. getEventSegs() { // recursive
  170. let segs = this.getOwnEventSegs()
  171. this.iterChildren(function(child) {
  172. segs.push.apply(segs, child.getEventSegs())
  173. })
  174. return segs
  175. }
  176. getOwnEventSegs() { // just for itself
  177. if (this.eventRenderer) {
  178. return this.eventRenderer.getSegs()
  179. }
  180. return []
  181. }
  182. // Event Rendering Triggering
  183. // -----------------------------------------------------------------------------------------------------------------
  184. triggerAfterEventsRendered() {
  185. this.triggerAfterEventSegsRendered(
  186. this.getEventSegs()
  187. )
  188. this.publiclyTrigger('eventAfterAllRender', {
  189. context: this,
  190. args: [ this ]
  191. })
  192. }
  193. triggerAfterEventSegsRendered(segs) {
  194. // an optimization, because getEventLegacy is expensive
  195. if (this.hasPublicHandlers('eventAfterRender')) {
  196. segs.forEach((seg) => {
  197. let legacy
  198. if (seg.el) { // necessary?
  199. legacy = seg.footprint.getEventLegacy()
  200. this.publiclyTrigger('eventAfterRender', {
  201. context: legacy,
  202. args: [ legacy, seg.el, this ]
  203. })
  204. }
  205. })
  206. }
  207. }
  208. triggerBeforeEventsDestroyed() {
  209. this.triggerBeforeEventSegsDestroyed(
  210. this.getEventSegs()
  211. )
  212. }
  213. triggerBeforeEventSegsDestroyed(segs) {
  214. if (this.hasPublicHandlers('eventDestroy')) {
  215. segs.forEach((seg) => {
  216. let legacy
  217. if (seg.el) { // necessary?
  218. legacy = seg.footprint.getEventLegacy()
  219. this.publiclyTrigger('eventDestroy', {
  220. context: legacy,
  221. args: [ legacy, seg.el, this ]
  222. })
  223. }
  224. })
  225. }
  226. }
  227. // Event Rendering Utils
  228. // -----------------------------------------------------------------------------------------------------------------
  229. // Hides all rendered event segments linked to the given event
  230. // RECURSIVE with subcomponents
  231. showEventsWithId(eventDefId) {
  232. this.getEventSegs().forEach(function(seg) {
  233. if (
  234. seg.footprint.eventDef.id === eventDefId &&
  235. seg.el // necessary?
  236. ) {
  237. seg.el.style.visibility = ''
  238. }
  239. })
  240. this.callChildren('showEventsWithId', arguments)
  241. }
  242. // Shows all rendered event segments linked to the given event
  243. // RECURSIVE with subcomponents
  244. hideEventsWithId(eventDefId) {
  245. this.getEventSegs().forEach(function(seg) {
  246. if (
  247. seg.footprint.eventDef.id === eventDefId &&
  248. seg.el // necessary?
  249. ) {
  250. seg.el.style.visibility = 'hidden'
  251. }
  252. })
  253. this.callChildren('hideEventsWithId', arguments)
  254. }
  255. // Drag-n-Drop Rendering (for both events and external elements)
  256. // ---------------------------------------------------------------------------------------------------------------
  257. // Renders a visual indication of a event or external-element drag over the given drop zone.
  258. // If an external-element, seg will be `null`.
  259. // Must return elements used for any mock events.
  260. renderDrag(eventFootprints, seg?, isTouch = false) {
  261. let renderedHelper = false
  262. this.iterChildren(function(child) {
  263. if (child.renderDrag(eventFootprints, seg, isTouch)) {
  264. renderedHelper = true
  265. }
  266. })
  267. return renderedHelper
  268. }
  269. // Unrenders a visual indication of an event or external-element being dragged.
  270. unrenderDrag() {
  271. this.callChildren('unrenderDrag', arguments)
  272. }
  273. // EXTERNAL Drag-n-Drop
  274. // ---------------------------------------------------------------------------------------------------------------
  275. // Doesn't need to implement a response, but must pass to children
  276. handlExternalDragStart(ev, el, skipBinding) {
  277. this.callChildren('handlExternalDragStart', arguments)
  278. }
  279. handleExternalDragMove(ev) {
  280. this.callChildren('handleExternalDragMove', arguments)
  281. }
  282. handleExternalDragStop(ev) {
  283. this.callChildren('handleExternalDragStop', arguments)
  284. }
  285. // Event Resizing
  286. // ---------------------------------------------------------------------------------------------------------------
  287. // Renders a visual indication of an event being resized.
  288. renderEventResize(eventFootprints, seg, isTouch) {
  289. this.callChildren('renderEventResize', arguments)
  290. }
  291. // Unrenders a visual indication of an event being resized.
  292. unrenderEventResize() {
  293. this.callChildren('unrenderEventResize', arguments)
  294. }
  295. // Selection
  296. // ---------------------------------------------------------------------------------------------------------------
  297. // Renders a visual indication of the selection
  298. // TODO: rename to `renderSelection` after legacy is gone
  299. renderSelectionFootprint(componentFootprint) {
  300. this.renderHighlight(componentFootprint)
  301. this.callChildren('renderSelectionFootprint', arguments)
  302. }
  303. // Unrenders a visual indication of selection
  304. unrenderSelection() {
  305. this.unrenderHighlight()
  306. this.callChildren('unrenderSelection', arguments)
  307. }
  308. // Highlight
  309. // ---------------------------------------------------------------------------------------------------------------
  310. // Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data)
  311. renderHighlight(componentFootprint) {
  312. if (this.fillRenderer) {
  313. this.fillRenderer.renderFootprint(
  314. 'highlight',
  315. componentFootprint,
  316. {
  317. getClasses() {
  318. return [ 'fc-highlight' ]
  319. }
  320. }
  321. )
  322. }
  323. this.callChildren('renderHighlight', arguments)
  324. }
  325. // Unrenders the emphasis on a date range
  326. unrenderHighlight() {
  327. if (this.fillRenderer) {
  328. this.fillRenderer.unrender('highlight')
  329. }
  330. this.callChildren('unrenderHighlight', arguments)
  331. }
  332. // Hit Areas
  333. // ---------------------------------------------------------------------------------------------------------------
  334. // just because all DateComponents support this interface
  335. // doesn't mean they need to have their own internal coord system. they can defer to sub-components.
  336. hitsNeeded() {
  337. if (!(this.hitsNeededDepth++)) {
  338. this.prepareHits()
  339. }
  340. this.callChildren('hitsNeeded', arguments)
  341. }
  342. hitsNotNeeded() {
  343. if (this.hitsNeededDepth && !(--this.hitsNeededDepth)) {
  344. this.releaseHits()
  345. }
  346. this.callChildren('hitsNotNeeded', arguments)
  347. }
  348. prepareHits() {
  349. // subclasses can implement
  350. }
  351. releaseHits() {
  352. // subclasses can implement
  353. }
  354. // Given coordinates from the topleft of the document, return data about the date-related area underneath.
  355. // Can return an object with arbitrary properties (although top/right/left/bottom are encouraged).
  356. // Must have a `grid` property, a reference to this current grid. TODO: avoid this
  357. // The returned object will be processed by getHitFootprint and getHitEl.
  358. queryHit(leftOffset, topOffset) {
  359. let childrenByUid = this.childrenByUid
  360. let uid
  361. let hit
  362. for (uid in childrenByUid) {
  363. hit = childrenByUid[uid].queryHit(leftOffset, topOffset)
  364. if (hit) {
  365. break
  366. }
  367. }
  368. return hit
  369. }
  370. getSafeHitFootprint(hit) {
  371. let footprint = this.getHitFootprint(hit)
  372. if (!this.dateProfile.activeUnzonedRange.containsRange(footprint.unzonedRange)) {
  373. return null
  374. }
  375. return footprint
  376. }
  377. getHitFootprint(hit): any {
  378. // what about being abstract!?
  379. }
  380. // Given position-level information about a date-related area within the grid,
  381. // should return an element that best represents it. passed to dayClick callback.
  382. getHitEl(hit): HTMLElement | null {
  383. return null
  384. }
  385. /* Converting eventRange -> eventFootprint
  386. ------------------------------------------------------------------------------------------------------------------*/
  387. eventRangesToEventFootprints(eventRanges) {
  388. let eventFootprints = []
  389. let i
  390. for (i = 0; i < eventRanges.length; i++) {
  391. eventFootprints.push.apply( // append
  392. eventFootprints,
  393. this.eventRangeToEventFootprints(eventRanges[i])
  394. )
  395. }
  396. return eventFootprints
  397. }
  398. eventRangeToEventFootprints(eventRange): EventFootprint[] {
  399. return [ eventRangeToEventFootprint(eventRange) ]
  400. }
  401. /* Converting componentFootprint/eventFootprint -> segs
  402. ------------------------------------------------------------------------------------------------------------------*/
  403. eventFootprintsToSegs(eventFootprints) {
  404. let segs = []
  405. let i
  406. for (i = 0; i < eventFootprints.length; i++) {
  407. segs.push.apply(segs,
  408. this.eventFootprintToSegs(eventFootprints[i])
  409. )
  410. }
  411. return segs
  412. }
  413. // Given an event's span (unzoned start/end and other misc data), and the event itself,
  414. // slices into segments and attaches event-derived properties to them.
  415. // eventSpan - { start, end, isStart, isEnd, otherthings... }
  416. eventFootprintToSegs(eventFootprint) {
  417. let unzonedRange = eventFootprint.componentFootprint.unzonedRange
  418. let segs
  419. let i
  420. let seg
  421. segs = this.componentFootprintToSegs(eventFootprint.componentFootprint)
  422. for (i = 0; i < segs.length; i++) {
  423. seg = segs[i]
  424. if (!unzonedRange.isStart) {
  425. seg.isStart = false
  426. }
  427. if (!unzonedRange.isEnd) {
  428. seg.isEnd = false
  429. }
  430. seg.footprint = eventFootprint
  431. // TODO: rename to seg.eventFootprint
  432. }
  433. return segs
  434. }
  435. componentFootprintToSegs(componentFootprint) {
  436. return []
  437. }
  438. // Utils
  439. // ---------------------------------------------------------------------------------------------------------------
  440. callChildren(methodName, args) {
  441. this.iterChildren(function(child) {
  442. child[methodName].apply(child, args)
  443. })
  444. }
  445. iterChildren(func) {
  446. let childrenByUid = this.childrenByUid
  447. let uid
  448. for (uid in childrenByUid) {
  449. func(childrenByUid[uid])
  450. }
  451. }
  452. _getCalendar() { // TODO: strip out. move to generic parent.
  453. let t = (this as any)
  454. return t.calendar || t.view.calendar
  455. }
  456. _getView() { // TODO: strip out. move to generic parent.
  457. return (this as any).view
  458. }
  459. _getDateProfile() {
  460. return this._getView().get('dateProfile')
  461. }
  462. // Generates HTML for an anchor to another view into the calendar.
  463. // Will either generate an <a> tag or a non-clickable <span> tag, depending on enabled settings.
  464. // `gotoOptions` can either be a moment input, or an object with the form:
  465. // { date, type, forceOff }
  466. // `type` is a view-type like "day" or "week". default value is "day".
  467. // `attrs` and `innerHtml` are use to generate the rest of the HTML tag.
  468. buildGotoAnchorHtml(gotoOptions, attrs, innerHtml) {
  469. let date
  470. let type
  471. let forceOff
  472. let finalOptions
  473. if (moment.isMoment(gotoOptions) || typeof gotoOptions !== 'object') {
  474. date = gotoOptions // a single moment input
  475. } else {
  476. date = gotoOptions.date
  477. type = gotoOptions.type
  478. forceOff = gotoOptions.forceOff
  479. }
  480. date = momentExt(date) // if a string, parse it
  481. finalOptions = { // for serialization into the link
  482. date: date.format('YYYY-MM-DD'),
  483. type: type || 'day'
  484. }
  485. if (typeof attrs === 'string') {
  486. innerHtml = attrs
  487. attrs = null
  488. }
  489. attrs = attrs ? ' ' + attrsToStr(attrs) : '' // will have a leading space
  490. innerHtml = innerHtml || ''
  491. if (!forceOff && this.opt('navLinks')) {
  492. return '<a' + attrs +
  493. ' data-goto="' + htmlEscape(JSON.stringify(finalOptions)) + '">' +
  494. innerHtml +
  495. '</a>'
  496. } else {
  497. return '<span' + attrs + '>' +
  498. innerHtml +
  499. '</span>'
  500. }
  501. }
  502. getAllDayHtml() {
  503. return this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'))
  504. }
  505. // Computes HTML classNames for a single-day element
  506. getDayClasses(date, noThemeHighlight?) {
  507. let view = this._getView()
  508. let classes = []
  509. let today
  510. if (!this.dateProfile.activeUnzonedRange.containsDate(date)) {
  511. classes.push('fc-disabled-day') // TODO: jQuery UI theme?
  512. } else {
  513. classes.push('fc-' + dayIDs[date.day()])
  514. if (view.isDateInOtherMonth(date, this.dateProfile)) { // TODO: use DateComponent subclass somehow
  515. classes.push('fc-other-month')
  516. }
  517. today = view.calendar.getNow()
  518. if (date.isSame(today, 'day')) {
  519. classes.push('fc-today')
  520. if (noThemeHighlight !== true) {
  521. classes.push(view.calendar.theme.getClass('today'))
  522. }
  523. } else if (date < today) {
  524. classes.push('fc-past')
  525. } else {
  526. classes.push('fc-future')
  527. }
  528. }
  529. return classes
  530. }
  531. // Utility for formatting a range. Accepts a range object, formatting string, and optional separator.
  532. // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account.
  533. // The timezones of the dates within `range` will be respected.
  534. formatRange(range, isAllDay, formatStr, separator) {
  535. let end = range.end
  536. if (isAllDay) {
  537. end = end.clone().subtract(1) // convert to inclusive. last ms of previous day
  538. }
  539. return formatRange(range.start, end, formatStr, separator, this.isRTL)
  540. }
  541. // Compute the number of the give units in the "current" range.
  542. // Will return a floating-point number. Won't round.
  543. currentRangeAs(unit) {
  544. return this._getDateProfile().currentUnzonedRange.as(unit)
  545. }
  546. // Returns the date range of the full days the given range visually appears to occupy.
  547. // Returns a plain object with start/end, NOT an UnzonedRange!
  548. computeDayRange(unzonedRange) {
  549. let calendar = this._getCalendar()
  550. let startDay = calendar.msToUtcMoment(unzonedRange.startMs, true) // the beginning of the day the range starts
  551. let end = calendar.msToUtcMoment(unzonedRange.endMs)
  552. let endTimeMS = +end.time() // # of milliseconds into `endDay`
  553. let endDay = end.clone().stripTime() // the beginning of the day the range exclusively ends
  554. // If the end time is actually inclusively part of the next day and is equal to or
  555. // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
  556. // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
  557. if (endTimeMS && endTimeMS >= this.nextDayThreshold) {
  558. endDay.add(1, 'days')
  559. }
  560. // If end is within `startDay` but not past nextDayThreshold, assign the default duration of one day.
  561. if (endDay <= startDay) {
  562. endDay = startDay.clone().add(1, 'days')
  563. }
  564. return { start: startDay, end: endDay }
  565. }
  566. // Does the given range visually appear to occupy more than one day?
  567. isMultiDayRange(unzonedRange) {
  568. let dayRange = this.computeDayRange(unzonedRange)
  569. return dayRange.end.diff(dayRange.start, 'days') > 1
  570. }
  571. }
  572. // legacy
  573. function convertEventsPayloadToLegacyArray(eventsPayload) {
  574. let eventDefId
  575. let eventInstances
  576. let legacyEvents = []
  577. let i
  578. for (eventDefId in eventsPayload) {
  579. eventInstances = eventsPayload[eventDefId].eventInstances
  580. for (i = 0; i < eventInstances.length; i++) {
  581. legacyEvents.push(
  582. eventInstances[i].toLegacy()
  583. )
  584. }
  585. }
  586. return legacyEvents
  587. }