DayGridEventRenderer.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. import * as $ from 'jquery'
  2. import { htmlEscape, cssToStr } from '../util'
  3. import EventRenderer from '../component/renderers/EventRenderer'
  4. import DayGrid from './DayGrid'
  5. /* Event-rendering methods for the DayGrid class
  6. ----------------------------------------------------------------------------------------------------------------------*/
  7. export default class DayGridEventRenderer extends EventRenderer {
  8. dayGrid: DayGrid
  9. rowStructs: any // an array of objects, each holding information about a row's foreground event-rendering
  10. constructor(dayGrid, fillRenderer) {
  11. super(dayGrid, fillRenderer)
  12. this.dayGrid = dayGrid
  13. }
  14. renderBgRanges(eventRanges) {
  15. // don't render timed background events
  16. eventRanges = eventRanges.filter(function(eventRange: any) {
  17. return eventRange.eventDef.isAllDay()
  18. })
  19. super.renderBgRanges(eventRanges)
  20. }
  21. // Renders the given foreground event segments onto the grid
  22. renderFgSegs(segs) {
  23. let rowStructs = this.rowStructs = this.renderSegRows(segs)
  24. // append to each row's content skeleton
  25. this.dayGrid.rowEls.forEach(function(rowNode, i) {
  26. $(rowNode).find('.fc-content-skeleton > table').append(
  27. rowStructs[i].tbodyEl
  28. )
  29. })
  30. }
  31. // Unrenders all currently rendered foreground event segments
  32. unrenderFgSegs() {
  33. let rowStructs = this.rowStructs || []
  34. let rowStruct
  35. while ((rowStruct = rowStructs.pop())) {
  36. rowStruct.tbodyEl.remove()
  37. }
  38. this.rowStructs = null
  39. }
  40. // Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
  41. // Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
  42. // PRECONDITION: each segment shoud already have a rendered and assigned `.el`
  43. renderSegRows(segs) {
  44. let rowStructs = []
  45. let segRows
  46. let row
  47. segRows = this.groupSegRows(segs) // group into nested arrays
  48. // iterate each row of segment groupings
  49. for (row = 0; row < segRows.length; row++) {
  50. rowStructs.push(
  51. this.renderSegRow(row, segRows[row])
  52. )
  53. }
  54. return rowStructs
  55. }
  56. // Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains
  57. // the segments. Returns object with a bunch of internal data about how the render was calculated.
  58. // NOTE: modifies rowSegs
  59. renderSegRow(row, rowSegs) {
  60. let colCnt = this.dayGrid.colCnt
  61. let segLevels = this.buildSegLevels(rowSegs) // group into sub-arrays of levels
  62. let levelCnt = Math.max(1, segLevels.length) // ensure at least one level
  63. let tbody = $('<tbody/>')
  64. let segMatrix = [] // lookup for which segments are rendered into which level+col cells
  65. let cellMatrix = [] // lookup for all <td> elements of the level+col matrix
  66. let loneCellMatrix = [] // lookup for <td> elements that only take up a single column
  67. let i
  68. let levelSegs
  69. let col
  70. let tr
  71. let j
  72. let seg
  73. let td
  74. // populates empty cells from the current column (`col`) to `endCol`
  75. function emptyCellsUntil(endCol) {
  76. while (col < endCol) {
  77. // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
  78. td = (loneCellMatrix[i - 1] || [])[col]
  79. if (td) {
  80. td.attr(
  81. 'rowspan',
  82. parseInt(td.attr('rowspan') || 1, 10) + 1
  83. )
  84. } else {
  85. td = $('<td/>')
  86. tr.append(td)
  87. }
  88. cellMatrix[i][col] = td
  89. loneCellMatrix[i][col] = td
  90. col++
  91. }
  92. }
  93. for (i = 0; i < levelCnt; i++) { // iterate through all levels
  94. levelSegs = segLevels[i]
  95. col = 0
  96. tr = $('<tr/>')
  97. segMatrix.push([])
  98. cellMatrix.push([])
  99. loneCellMatrix.push([])
  100. // levelCnt might be 1 even though there are no actual levels. protect against this.
  101. // this single empty row is useful for styling.
  102. if (levelSegs) {
  103. for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level
  104. seg = levelSegs[j]
  105. emptyCellsUntil(seg.leftCol)
  106. // create a container that occupies or more columns. append the event element.
  107. td = $('<td class="fc-event-container"/>').append(seg.el)
  108. if (seg.leftCol !== seg.rightCol) {
  109. td.attr('colspan', seg.rightCol - seg.leftCol + 1)
  110. } else { // a single-column segment
  111. loneCellMatrix[i][col] = td
  112. }
  113. while (col <= seg.rightCol) {
  114. cellMatrix[i][col] = td
  115. segMatrix[i][col] = seg
  116. col++
  117. }
  118. tr.append(td)
  119. }
  120. }
  121. emptyCellsUntil(colCnt) // finish off the row
  122. this.dayGrid.bookendCells(tr)
  123. tbody.append(tr)
  124. }
  125. return { // a "rowStruct"
  126. row: row, // the row number
  127. tbodyEl: tbody,
  128. cellMatrix: cellMatrix,
  129. segMatrix: segMatrix,
  130. segLevels: segLevels,
  131. segs: rowSegs
  132. }
  133. }
  134. // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
  135. // NOTE: modifies segs
  136. buildSegLevels(segs) {
  137. let levels = []
  138. let i
  139. let seg
  140. let j
  141. // Give preference to elements with certain criteria, so they have
  142. // a chance to be closer to the top.
  143. this.sortEventSegs(segs)
  144. for (i = 0; i < segs.length; i++) {
  145. seg = segs[i]
  146. // loop through levels, starting with the topmost, until the segment doesn't collide with other segments
  147. for (j = 0; j < levels.length; j++) {
  148. if (!isDaySegCollision(seg, levels[j])) {
  149. break
  150. }
  151. }
  152. // `j` now holds the desired subrow index
  153. seg.level = j;
  154. // create new level array if needed and append segment
  155. (levels[j] || (levels[j] = [])).push(seg)
  156. }
  157. // order segments left-to-right. very important if calendar is RTL
  158. for (j = 0; j < levels.length; j++) {
  159. levels[j].sort(compareDaySegCols)
  160. }
  161. return levels
  162. }
  163. // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
  164. groupSegRows(segs) {
  165. let segRows = []
  166. let i
  167. for (i = 0; i < this.dayGrid.rowCnt; i++) {
  168. segRows.push([])
  169. }
  170. for (i = 0; i < segs.length; i++) {
  171. segRows[segs[i].row].push(segs[i])
  172. }
  173. return segRows
  174. }
  175. // Computes a default event time formatting string if `timeFormat` is not explicitly defined
  176. computeEventTimeFormat() {
  177. return this.opt('extraSmallTimeFormat') // like "6p" or "6:30p"
  178. }
  179. // Computes a default `displayEventEnd` value if one is not expliclty defined
  180. computeDisplayEventEnd() {
  181. return this.dayGrid.colCnt === 1 // we'll likely have space if there's only one day
  182. }
  183. // Builds the HTML to be used for the default element for an individual segment
  184. fgSegHtml(seg, disableResizing) {
  185. let view = this.view
  186. let eventDef = seg.footprint.eventDef
  187. let isAllDay = seg.footprint.componentFootprint.isAllDay
  188. let isDraggable = view.isEventDefDraggable(eventDef)
  189. let isResizableFromStart = !disableResizing && isAllDay &&
  190. seg.isStart && view.isEventDefResizableFromStart(eventDef)
  191. let isResizableFromEnd = !disableResizing && isAllDay &&
  192. seg.isEnd && view.isEventDefResizableFromEnd(eventDef)
  193. let classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd)
  194. let skinCss = cssToStr(this.getSkinCss(eventDef))
  195. let timeHtml = ''
  196. let timeText
  197. let titleHtml
  198. classes.unshift('fc-day-grid-event', 'fc-h-event')
  199. // Only display a timed events time if it is the starting segment
  200. if (seg.isStart) {
  201. timeText = this.getTimeText(seg.footprint)
  202. if (timeText) {
  203. timeHtml = '<span class="fc-time">' + htmlEscape(timeText) + '</span>'
  204. }
  205. }
  206. titleHtml =
  207. '<span class="fc-title">' +
  208. (htmlEscape(eventDef.title || '') || '&nbsp;') + // we always want one line of height
  209. '</span>'
  210. return '<a class="' + classes.join(' ') + '"' +
  211. (eventDef.url ?
  212. ' href="' + htmlEscape(eventDef.url) + '"' :
  213. ''
  214. ) +
  215. (skinCss ?
  216. ' style="' + skinCss + '"' :
  217. ''
  218. ) +
  219. '>' +
  220. '<div class="fc-content">' +
  221. (this.dayGrid.isRTL ?
  222. titleHtml + ' ' + timeHtml : // put a natural space in between
  223. timeHtml + ' ' + titleHtml //
  224. ) +
  225. '</div>' +
  226. (isResizableFromStart ?
  227. '<div class="fc-resizer fc-start-resizer" />' :
  228. ''
  229. ) +
  230. (isResizableFromEnd ?
  231. '<div class="fc-resizer fc-end-resizer" />' :
  232. ''
  233. ) +
  234. '</a>'
  235. }
  236. }
  237. // Computes whether two segments' columns collide. They are assumed to be in the same row.
  238. function isDaySegCollision(seg, otherSegs) {
  239. let i
  240. let otherSeg
  241. for (i = 0; i < otherSegs.length; i++) {
  242. otherSeg = otherSegs[i]
  243. if (
  244. otherSeg.leftCol <= seg.rightCol &&
  245. otherSeg.rightCol >= seg.leftCol
  246. ) {
  247. return true
  248. }
  249. }
  250. return false
  251. }
  252. // A cmp function for determining the leftmost event
  253. function compareDaySegCols(a, b) {
  254. return a.leftCol - b.leftCol
  255. }