2
0

DayGrid.ts 26 KB

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