1import * as $ from 'jquery'
2import { htmlEscape } from '../util'
3import CoordCache from '../common/CoordCache'
4import Popover from '../common/Popover'
5import UnzonedRange from '../models/UnzonedRange'
6import ComponentFootprint from '../models/ComponentFootprint'
7import EventFootprint from '../models/event/EventFootprint'
8import BusinessHourRenderer from '../component/renderers/BusinessHourRenderer'
9import StandardInteractionsMixin from '../component/interactions/StandardInteractionsMixin'
10import InteractiveDateComponent from '../component/InteractiveDateComponent'
11import { default as DayTableMixin, DayTableInterface } from '../component/DayTableMixin'
12import DayGridEventRenderer from './DayGridEventRenderer'
13import DayGridHelperRenderer from './DayGridHelperRenderer'
14import DayGridFillRenderer from './DayGridFillRenderer'
15
16
17/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.
18----------------------------------------------------------------------------------------------------------------------*/
19
20export default class DayGrid extends InteractiveDateComponent {
21
22  rowCnt: DayTableInterface['rowCnt']
23  colCnt: DayTableInterface['colCnt']
24  daysPerRow: DayTableInterface['daysPerRow']
25  sliceRangeByRow: DayTableInterface['sliceRangeByRow']
26  updateDayTable: DayTableInterface['updateDayTable']
27  renderHeadHtml: DayTableInterface['renderHeadHtml']
28  getCellDate: DayTableInterface['getCellDate']
29  renderBgTrHtml: DayTableInterface['renderBgTrHtml']
30  renderIntroHtml: DayTableInterface['renderIntroHtml']
31  getCellRange: DayTableInterface['getCellRange']
32  sliceRangeByDay: DayTableInterface['sliceRangeByDay']
33
34  view: any // TODO: make more general and/or remove
35  helperRenderer: any
36
37  cellWeekNumbersVisible: boolean = false // display week numbers in day cell?
38
39  bottomCoordPadding: number = 0 // hack for extending the hit area for the last row of the coordinate grid
40
41  headContainerEl: any // div that hold's the date header
42  rowEls: any // set of fake row elements
43  cellEls: any // set of whole-day elements comprising the row's background
44
45  rowCoordCache: any
46  colCoordCache: any
47
48  // isRigid determines whether the individual rows should ignore the contents and be a constant height.
49  // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient.
50  isRigid: boolean = false
51
52  hasAllDayBusinessHours: boolean = true
53
54  segPopover: any // the Popover that holds events that can't fit in a cell. null when not visible
55  popoverSegs: any // an array of segment objects that the segPopover holds. null when not visible
56
57
58  constructor(view) { // view is required, unlike superclass
59    super(view)
60  }
61
62
63  // Slices up the given span (unzoned start/end with other misc data) into an array of segments
64  componentFootprintToSegs(componentFootprint) {
65    let segs = this.sliceRangeByRow(componentFootprint.unzonedRange)
66    let i
67    let seg
68
69    for (i = 0; i < segs.length; i++) {
70      seg = segs[i]
71
72      if (this.isRTL) {
73        seg.leftCol = this.daysPerRow - 1 - seg.lastRowDayIndex
74        seg.rightCol = this.daysPerRow - 1 - seg.firstRowDayIndex
75      } else {
76        seg.leftCol = seg.firstRowDayIndex
77        seg.rightCol = seg.lastRowDayIndex
78      }
79    }
80
81    return segs
82  }
83
84
85  /* Date Rendering
86  ------------------------------------------------------------------------------------------------------------------*/
87
88
89  renderDates(dateProfile) {
90    this.dateProfile = dateProfile
91    this.updateDayTable()
92    this.renderGrid()
93  }
94
95
96  unrenderDates() {
97    this.removeSegPopover()
98  }
99
100
101  // Renders the rows and columns into the component's `this.el`, which should already be assigned.
102  renderGrid() {
103    let view = this.view
104    let rowCnt = this.rowCnt
105    let colCnt = this.colCnt
106    let html = ''
107    let row
108    let col
109
110    if (this.headContainerEl) {
111      this.headContainerEl.html(this.renderHeadHtml())
112    }
113
114    for (row = 0; row < rowCnt; row++) {
115      html += this.renderDayRowHtml(row, this.isRigid)
116    }
117    this.el.html(html)
118
119    this.rowEls = this.el.find('.fc-row')
120    this.cellEls = this.el.find('.fc-day, .fc-disabled-day')
121
122    this.rowCoordCache = new CoordCache({
123      els: this.rowEls,
124      isVertical: true
125    })
126    this.colCoordCache = new CoordCache({
127      els: this.cellEls.slice(0, this.colCnt), // only the first row
128      isHorizontal: true
129    })
130
131    // trigger dayRender with each cell's element
132    for (row = 0; row < rowCnt; row++) {
133      for (col = 0; col < colCnt; col++) {
134        this.publiclyTrigger('dayRender', {
135          context: view,
136          args: [
137            this.getCellDate(row, col),
138            this.getCellEl(row, col),
139            view
140          ]
141        })
142      }
143    }
144  }
145
146
147  // Generates the HTML for a single row, which is a div that wraps a table.
148  // `row` is the row number.
149  renderDayRowHtml(row, isRigid) {
150    let theme = this.view.calendar.theme
151    let classes = [ 'fc-row', 'fc-week', theme.getClass('dayRow') ]
152
153    if (isRigid) {
154      classes.push('fc-rigid')
155    }
156
157    return '' +
158      '<div class="' + classes.join(' ') + '">' +
159        '<div class="fc-bg">' +
160          '<table class="' + theme.getClass('tableGrid') + '">' +
161            this.renderBgTrHtml(row) +
162          '</table>' +
163        '</div>' +
164        '<div class="fc-content-skeleton">' +
165          '<table>' +
166            (this.getIsNumbersVisible() ?
167              '<thead>' +
168                this.renderNumberTrHtml(row) +
169              '</thead>' :
170              ''
171              ) +
172          '</table>' +
173        '</div>' +
174      '</div>'
175  }
176
177
178  getIsNumbersVisible() {
179    return this.getIsDayNumbersVisible() || this.cellWeekNumbersVisible
180  }
181
182
183  getIsDayNumbersVisible() {
184    return this.rowCnt > 1
185  }
186
187
188  /* Grid Number Rendering
189  ------------------------------------------------------------------------------------------------------------------*/
190
191
192  renderNumberTrHtml(row) {
193    return '' +
194      '<tr>' +
195        (this.isRTL ? '' : this.renderNumberIntroHtml(row)) +
196        this.renderNumberCellsHtml(row) +
197        (this.isRTL ? this.renderNumberIntroHtml(row) : '') +
198      '</tr>'
199  }
200
201
202  renderNumberIntroHtml(row) {
203    return this.renderIntroHtml()
204  }
205
206
207  renderNumberCellsHtml(row) {
208    let htmls = []
209    let col
210    let date
211
212    for (col = 0; col < this.colCnt; col++) {
213      date = this.getCellDate(row, col)
214      htmls.push(this.renderNumberCellHtml(date))
215    }
216
217    return htmls.join('')
218  }
219
220
221  // Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton.
222  // The number row will only exist if either day numbers or week numbers are turned on.
223  renderNumberCellHtml(date) {
224    let view = this.view
225    let html = ''
226    let isDateValid = this.dateProfile.activeUnzonedRange.containsDate(date) // TODO: called too frequently. cache somehow.
227    let isDayNumberVisible = this.getIsDayNumbersVisible() && isDateValid
228    let classes
229    let weekCalcFirstDoW
230
231    if (!isDayNumberVisible && !this.cellWeekNumbersVisible) {
232      // no numbers in day cell (week number must be along the side)
233      return '<td></td>' //  will create an empty space above events :(
234    }
235
236    classes = this.getDayClasses(date)
237    classes.unshift('fc-day-top')
238
239    if (this.cellWeekNumbersVisible) {
240      // To determine the day of week number change under ISO, we cannot
241      // rely on moment.js methods such as firstDayOfWeek() or weekday(),
242      // because they rely on the locale's dow (possibly overridden by
243      // our firstDay option), which may not be Monday. We cannot change
244      // dow, because that would affect the calendar start day as well.
245      if (date._locale._fullCalendar_weekCalc === 'ISO') {
246        weekCalcFirstDoW = 1  // Monday by ISO 8601 definition
247      } else {
248        weekCalcFirstDoW = date._locale.firstDayOfWeek()
249      }
250    }
251
252    html += '<td class="' + classes.join(' ') + '"' +
253      (isDateValid ?
254        ' data-date="' + date.format() + '"' :
255        ''
256        ) +
257      '>'
258
259    if (this.cellWeekNumbersVisible && (date.day() === weekCalcFirstDoW)) {
260      html += view.buildGotoAnchorHtml(
261        { date: date, type: 'week' },
262        { 'class': 'fc-week-number' },
263        date.format('w') // inner HTML
264      )
265    }
266
267    if (isDayNumberVisible) {
268      html += view.buildGotoAnchorHtml(
269        date,
270        { 'class': 'fc-day-number' },
271        date.format('D') // inner HTML
272      )
273    }
274
275    html += '</td>'
276
277    return html
278  }
279
280
281  /* Hit System
282  ------------------------------------------------------------------------------------------------------------------*/
283
284
285  prepareHits() {
286    this.colCoordCache.build()
287    this.rowCoordCache.build()
288    this.rowCoordCache.bottoms[this.rowCnt - 1] += this.bottomCoordPadding // hack
289  }
290
291
292  releaseHits() {
293    this.colCoordCache.clear()
294    this.rowCoordCache.clear()
295  }
296
297
298  queryHit(leftOffset, topOffset) {
299    if (this.colCoordCache.isLeftInBounds(leftOffset) && this.rowCoordCache.isTopInBounds(topOffset)) {
300      let col = this.colCoordCache.getHorizontalIndex(leftOffset)
301      let row = this.rowCoordCache.getVerticalIndex(topOffset)
302
303      if (row != null && col != null) {
304        return this.getCellHit(row, col)
305      }
306    }
307  }
308
309
310  getHitFootprint(hit) {
311    let range = this.getCellRange(hit.row, hit.col)
312
313    return new ComponentFootprint(
314      new UnzonedRange(range.start, range.end),
315      true // all-day?
316    )
317  }
318
319
320  getHitEl(hit) {
321    return this.getCellEl(hit.row, hit.col)
322  }
323
324
325  /* Cell System
326  ------------------------------------------------------------------------------------------------------------------*/
327  // FYI: the first column is the leftmost column, regardless of date
328
329
330  getCellHit(row, col): any {
331    return {
332      row: row,
333      col: col,
334      component: this, // needed unfortunately :(
335      left: this.colCoordCache.getLeftOffset(col),
336      right: this.colCoordCache.getRightOffset(col),
337      top: this.rowCoordCache.getTopOffset(row),
338      bottom: this.rowCoordCache.getBottomOffset(row)
339    }
340  }
341
342
343  getCellEl(row, col) {
344    return this.cellEls.eq(row * this.colCnt + col)
345  }
346
347
348  /* Event Rendering
349  ------------------------------------------------------------------------------------------------------------------*/
350
351
352  // Unrenders all events currently rendered on the grid
353  executeEventUnrender() {
354    this.removeSegPopover() // removes the "more.." events popover
355    super.executeEventUnrender()
356  }
357
358
359  // Retrieves all rendered segment objects currently rendered on the grid
360  getOwnEventSegs() {
361    // append the segments from the "more..." popover
362    return super.getOwnEventSegs().concat(this.popoverSegs || [])
363  }
364
365
366  /* Event Drag Visualization
367  ------------------------------------------------------------------------------------------------------------------*/
368
369
370  // Renders a visual indication of an event or external element being dragged.
371  // `eventLocation` has zoned start and end (optional)
372  renderDrag(eventFootprints, seg, isTouch) {
373    let i
374
375    for (i = 0; i < eventFootprints.length; i++) {
376      this.renderHighlight(eventFootprints[i].componentFootprint)
377    }
378
379    // render drags from OTHER components as helpers
380    if (eventFootprints.length && seg && seg.component !== this) {
381      this.helperRenderer.renderEventDraggingFootprints(eventFootprints, seg, isTouch)
382
383      return true // signal helpers rendered
384    }
385  }
386
387
388  // Unrenders any visual indication of a hovering event
389  unrenderDrag() {
390    this.unrenderHighlight()
391    this.helperRenderer.unrender()
392  }
393
394
395  /* Event Resize Visualization
396  ------------------------------------------------------------------------------------------------------------------*/
397
398
399  // Renders a visual indication of an event being resized
400  renderEventResize(eventFootprints, seg, isTouch) {
401    let i
402
403    for (i = 0; i < eventFootprints.length; i++) {
404      this.renderHighlight(eventFootprints[i].componentFootprint)
405    }
406
407    this.helperRenderer.renderEventResizingFootprints(eventFootprints, seg, isTouch)
408  }
409
410
411  // Unrenders a visual indication of an event being resized
412  unrenderEventResize() {
413    this.unrenderHighlight()
414    this.helperRenderer.unrender()
415  }
416
417
418  /* More+ Link Popover
419  ------------------------------------------------------------------------------------------------------------------*/
420
421
422  removeSegPopover() {
423    if (this.segPopover) {
424      this.segPopover.hide() // in handler, will call segPopover's removeElement
425    }
426  }
427
428
429  // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.
430  // `levelLimit` can be false (don't limit), a number, or true (should be computed).
431  limitRows(levelLimit) {
432    let rowStructs = this.eventRenderer.rowStructs || []
433    let row // row #
434    let rowLevelLimit
435
436    for (row = 0; row < rowStructs.length; row++) {
437      this.unlimitRow(row)
438
439      if (!levelLimit) {
440        rowLevelLimit = false
441      } else if (typeof levelLimit === 'number') {
442        rowLevelLimit = levelLimit
443      } else {
444        rowLevelLimit = this.computeRowLevelLimit(row)
445      }
446
447      if (rowLevelLimit !== false) {
448        this.limitRow(row, rowLevelLimit)
449      }
450    }
451  }
452
453
454  // Computes the number of levels a row will accomodate without going outside its bounds.
455  // Assumes the row is "rigid" (maintains a constant height regardless of what is inside).
456  // `row` is the row number.
457  computeRowLevelLimit(row): (number | false) {
458    let rowEl = this.rowEls.eq(row) // the containing "fake" row div
459    let rowHeight = rowEl.height() // TODO: cache somehow?
460    let trEls = this.eventRenderer.rowStructs[row].tbodyEl.children()
461    let i
462    let trEl
463    let trHeight
464
465    function iterInnerHeights(i, childNode) {
466      trHeight = Math.max(trHeight, $(childNode).outerHeight())
467    }
468
469    // Reveal one level <tr> at a time and stop when we find one out of bounds
470    for (i = 0; i < trEls.length; i++) {
471      trEl = trEls.eq(i).removeClass('fc-limited') // reset to original state (reveal)
472
473      // with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell,
474      // so instead, find the tallest inner content element.
475      trHeight = 0
476      trEl.find('> td > :first-child').each(iterInnerHeights)
477
478      if (trEl.position().top + trHeight > rowHeight) {
479        return i
480      }
481    }
482
483    return false // should not limit at all
484  }
485
486
487  // Limits the given grid row to the maximum number of levels and injects "more" links if necessary.
488  // `row` is the row number.
489  // `levelLimit` is a number for the maximum (inclusive) number of levels allowed.
490  limitRow(row, levelLimit) {
491    let rowStruct = this.eventRenderer.rowStructs[row]
492    let moreNodes = [] // array of "more" <a> links and <td> DOM nodes
493    let col = 0 // col #, left-to-right (not chronologically)
494    let levelSegs // array of segment objects in the last allowable level, ordered left-to-right
495    let cellMatrix // a matrix (by level, then column) of all <td> jQuery elements in the row
496    let limitedNodes // array of temporarily hidden level <tr> and segment <td> DOM nodes
497    let i
498    let seg
499    let segsBelow // array of segment objects below `seg` in the current `col`
500    let totalSegsBelow // total number of segments below `seg` in any of the columns `seg` occupies
501    let colSegsBelow // array of segment arrays, below seg, one for each column (offset from segs's first column)
502    let td
503    let rowspan
504    let segMoreNodes // array of "more" <td> cells that will stand-in for the current seg's cell
505    let j
506    let moreTd
507    let moreWrap
508    let moreLink
509
510    // Iterates through empty level cells and places "more" links inside if need be
511    let emptyCellsUntil = (endCol) => { // goes from current `col` to `endCol`
512      while (col < endCol) {
513        segsBelow = this.getCellSegs(row, col, levelLimit)
514        if (segsBelow.length) {
515          td = cellMatrix[levelLimit - 1][col]
516          moreLink = this.renderMoreLink(row, col, segsBelow)
517          moreWrap = $('<div>').append(moreLink)
518          td.append(moreWrap)
519          moreNodes.push(moreWrap[0])
520        }
521        col++
522      }
523    }
524
525    if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit?
526      levelSegs = rowStruct.segLevels[levelLimit - 1]
527      cellMatrix = rowStruct.cellMatrix
528
529      limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level <tr> elements past the limit
530        .addClass('fc-limited').get() // hide elements and get a simple DOM-nodes array
531
532      // iterate though segments in the last allowable level
533      for (i = 0; i < levelSegs.length; i++) {
534        seg = levelSegs[i]
535        emptyCellsUntil(seg.leftCol) // process empty cells before the segment
536
537        // determine *all* segments below `seg` that occupy the same columns
538        colSegsBelow = []
539        totalSegsBelow = 0
540        while (col <= seg.rightCol) {
541          segsBelow = this.getCellSegs(row, col, levelLimit)
542          colSegsBelow.push(segsBelow)
543          totalSegsBelow += segsBelow.length
544          col++
545        }
546
547        if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links?
548          td = cellMatrix[levelLimit - 1][seg.leftCol] // the segment's parent cell
549          rowspan = td.attr('rowspan') || 1
550          segMoreNodes = []
551
552          // make a replacement <td> for each column the segment occupies. will be one for each colspan
553          for (j = 0; j < colSegsBelow.length; j++) {
554            moreTd = $('<td class="fc-more-cell">').attr('rowspan', rowspan)
555            segsBelow = colSegsBelow[j]
556            moreLink = this.renderMoreLink(
557              row,
558              seg.leftCol + j,
559              [ seg ].concat(segsBelow) // count seg as hidden too
560            )
561            moreWrap = $('<div>').append(moreLink)
562            moreTd.append(moreWrap)
563            segMoreNodes.push(moreTd[0])
564            moreNodes.push(moreTd[0])
565          }
566
567          td.addClass('fc-limited').after($(segMoreNodes)) // hide original <td> and inject replacements
568          limitedNodes.push(td[0])
569        }
570      }
571
572      emptyCellsUntil(this.colCnt) // finish off the level
573      rowStruct.moreEls = $(moreNodes) // for easy undoing later
574      rowStruct.limitedEls = $(limitedNodes) // for easy undoing later
575    }
576  }
577
578
579  // Reveals all levels and removes all "more"-related elements for a grid's row.
580  // `row` is a row number.
581  unlimitRow(row) {
582    let rowStruct = this.eventRenderer.rowStructs[row]
583
584    if (rowStruct.moreEls) {
585      rowStruct.moreEls.remove()
586      rowStruct.moreEls = null
587    }
588
589    if (rowStruct.limitedEls) {
590      rowStruct.limitedEls.removeClass('fc-limited')
591      rowStruct.limitedEls = null
592    }
593  }
594
595
596  // Renders an <a> element that represents hidden event element for a cell.
597  // Responsible for attaching click handler as well.
598  renderMoreLink(row, col, hiddenSegs) {
599    let view = this.view
600
601    return $('<a class="fc-more">')
602      .text(
603        this.getMoreLinkText(hiddenSegs.length)
604      )
605      .on('click', (ev) => {
606        let clickOption = this.opt('eventLimitClick')
607        let date = this.getCellDate(row, col)
608        let moreEl = $(ev.currentTarget)
609        let dayEl = this.getCellEl(row, col)
610        let allSegs = this.getCellSegs(row, col)
611
612        // rescope the segments to be within the cell's date
613        let reslicedAllSegs = this.resliceDaySegs(allSegs, date)
614        let reslicedHiddenSegs = this.resliceDaySegs(hiddenSegs, date)
615
616        if (typeof clickOption === 'function') {
617          // the returned value can be an atomic option
618          clickOption = this.publiclyTrigger('eventLimitClick', {
619            context: view,
620            args: [
621              {
622                date: date.clone(),
623                dayEl: dayEl,
624                moreEl: moreEl,
625                segs: reslicedAllSegs,
626                hiddenSegs: reslicedHiddenSegs
627              },
628              ev,
629              view
630            ]
631          })
632        }
633
634        if (clickOption === 'popover') {
635          this.showSegPopover(row, col, moreEl, reslicedAllSegs)
636        } else if (typeof clickOption === 'string') { // a view name
637          view.calendar.zoomTo(date, clickOption)
638        }
639      })
640  }
641
642
643  // Reveals the popover that displays all events within a cell
644  showSegPopover(row, col, moreLink, segs) {
645    let view = this.view
646    let moreWrap = moreLink.parent() // the <div> wrapper around the <a>
647    let topEl // the element we want to match the top coordinate of
648    let options
649
650    if (this.rowCnt === 1) {
651      topEl = view.el // will cause the popover to cover any sort of header
652    } else {
653      topEl = this.rowEls.eq(row) // will align with top of row
654    }
655
656    options = {
657      className: 'fc-more-popover ' + view.calendar.theme.getClass('popover'),
658      content: this.renderSegPopoverContent(row, col, segs),
659      parentEl: view.el, // attach to root of view. guarantees outside of scrollbars.
660      top: topEl.offset().top,
661      autoHide: true, // when the user clicks elsewhere, hide the popover
662      viewportConstrain: this.opt('popoverViewportConstrain'),
663      hide: () => {
664        // kill everything when the popover is hidden
665        // notify events to be removed
666        if (this.popoverSegs) {
667          this.triggerBeforeEventSegsDestroyed(this.popoverSegs)
668        }
669        this.segPopover.removeElement()
670        this.segPopover = null
671        this.popoverSegs = null
672      }
673    }
674
675    // Determine horizontal coordinate.
676    // We use the moreWrap instead of the <td> to avoid border confusion.
677    if (this.isRTL) {
678      options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1 // +1 to be over cell border
679    } else {
680      options.left = moreWrap.offset().left - 1 // -1 to be over cell border
681    }
682
683    this.segPopover = new Popover(options)
684    this.segPopover.show()
685
686    // the popover doesn't live within the grid's container element, and thus won't get the event
687    // delegated-handlers for free. attach event-related handlers to the popover.
688    this.bindAllSegHandlersToEl(this.segPopover.el)
689
690    this.triggerAfterEventSegsRendered(segs)
691  }
692
693
694  // Builds the inner DOM contents of the segment popover
695  renderSegPopoverContent(row, col, segs) {
696    let view = this.view
697    let theme = view.calendar.theme
698    let title = this.getCellDate(row, col).format(this.opt('dayPopoverFormat'))
699    let content = $(
700      '<div class="fc-header ' + theme.getClass('popoverHeader') + '">' +
701        '<span class="fc-close ' + theme.getIconClass('close') + '"></span>' +
702        '<span class="fc-title">' +
703          htmlEscape(title) +
704        '</span>' +
705        '<div class="fc-clear"></div>' +
706      '</div>' +
707      '<div class="fc-body ' + theme.getClass('popoverContent') + '">' +
708        '<div class="fc-event-container"></div>' +
709      '</div>'
710    )
711    let segContainer = content.find('.fc-event-container')
712    let i
713
714    // render each seg's `el` and only return the visible segs
715    segs = this.eventRenderer.renderFgSegEls(segs, true) // disableResizing=true
716    this.popoverSegs = segs
717
718    for (i = 0; i < segs.length; i++) {
719
720      // because segments in the popover are not part of a grid coordinate system, provide a hint to any
721      // grids that want to do drag-n-drop about which cell it came from
722      this.hitsNeeded()
723      segs[i].hit = this.getCellHit(row, col)
724      this.hitsNotNeeded()
725
726      segContainer.append(segs[i].el)
727    }
728
729    return content
730  }
731
732
733  // Given the events within an array of segment objects, reslice them to be in a single day
734  resliceDaySegs(segs, dayDate) {
735    let dayStart = dayDate.clone()
736    let dayEnd = dayStart.clone().add(1, 'days')
737    let dayRange = new UnzonedRange(dayStart, dayEnd)
738    let newSegs = []
739    let i
740    let seg
741    let slicedRange
742
743    for (i = 0; i < segs.length; i++) {
744      seg = segs[i]
745      slicedRange = seg.footprint.componentFootprint.unzonedRange.intersect(dayRange)
746
747      if (slicedRange) {
748        newSegs.push(
749          $.extend({}, seg, {
750            footprint: new EventFootprint(
751              new ComponentFootprint(
752                slicedRange,
753                seg.footprint.componentFootprint.isAllDay
754              ),
755              seg.footprint.eventDef,
756              seg.footprint.eventInstance
757            ),
758            isStart: seg.isStart && slicedRange.isStart,
759            isEnd: seg.isEnd && slicedRange.isEnd
760          })
761        )
762      }
763    }
764
765    // force an order because eventsToSegs doesn't guarantee one
766    // TODO: research if still needed
767    this.eventRenderer.sortEventSegs(newSegs)
768
769    return newSegs
770  }
771
772
773  // Generates the text that should be inside a "more" link, given the number of events it represents
774  getMoreLinkText(num) {
775    let opt = this.opt('eventLimitText')
776
777    if (typeof opt === 'function') {
778      return opt(num)
779    } else {
780      return '+' + num + ' ' + opt
781    }
782  }
783
784
785  // Returns segments within a given cell.
786  // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs.
787  getCellSegs(row, col, startLevel?) {
788    let segMatrix = this.eventRenderer.rowStructs[row].segMatrix
789    let level = startLevel || 0
790    let segs = []
791    let seg
792
793    while (level < segMatrix.length) {
794      seg = segMatrix[level][col]
795      if (seg) {
796        segs.push(seg)
797      }
798      level++
799    }
800
801    return segs
802  }
803
804}
805
806DayGrid.prototype.eventRendererClass = DayGridEventRenderer
807DayGrid.prototype.businessHourRendererClass = BusinessHourRenderer
808DayGrid.prototype.helperRendererClass = DayGridHelperRenderer
809DayGrid.prototype.fillRendererClass = DayGridFillRenderer
810
811StandardInteractionsMixin.mixInto(DayGrid)
812DayTableMixin.mixInto(DayGrid)
813