DayGridEventRenderer.ts 8.4 KB

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