DayGrid.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. /* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.
  2. ----------------------------------------------------------------------------------------------------------------------*/
  3. var DayGrid = FC.DayGrid = InteractiveDateComponent.extend(StandardInteractionsMixin, DayTableMixin, {
  4. eventRendererClass: DayGridEventRenderer,
  5. helperRendererClass: DayGridHelperRenderer,
  6. fillRendererClass: DayGridFillRenderer,
  7. view: null, // TODO: make more general and/or remove
  8. helperRenderer: null,
  9. numbersVisible: false, // should render a row for day/week numbers? set by outside view. TODO: make internal
  10. bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid
  11. rowEls: null, // set of fake row elements
  12. cellEls: null, // set of whole-day elements comprising the row's background
  13. rowCoordCache: null,
  14. colCoordCache: null,
  15. constructor: function(view) {
  16. this.view = view; // do first, for opt calls during initialization
  17. InteractiveDateComponent.call(this);
  18. },
  19. // Slices up the given span (unzoned start/end with other misc data) into an array of segments
  20. componentFootprintToSegs: function(componentFootprint) {
  21. var segs = this.sliceRangeByRow(componentFootprint.unzonedRange);
  22. var i, seg;
  23. for (i = 0; i < segs.length; i++) {
  24. seg = segs[i];
  25. if (this.isRTL) {
  26. seg.leftCol = this.daysPerRow - 1 - seg.lastRowDayIndex;
  27. seg.rightCol = this.daysPerRow - 1 - seg.firstRowDayIndex;
  28. }
  29. else {
  30. seg.leftCol = seg.firstRowDayIndex;
  31. seg.rightCol = seg.lastRowDayIndex;
  32. }
  33. }
  34. return segs;
  35. },
  36. rangeUpdated: function() {
  37. this.updateDayTable();
  38. // needs to go after updateDayTable because computeEventTimeFormat/computeDisplayEventEnd depends on colCnt.
  39. // TODO: easy to forget. use listener.
  40. this.eventRenderer.rangeUpdated();
  41. },
  42. opt: function(name) {
  43. return this.view.opt(name);
  44. },
  45. /* Date Rendering
  46. ------------------------------------------------------------------------------------------------------------------*/
  47. // Renders the rows and columns into the component's `this.el`, which should already be assigned.
  48. // isRigid determins 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. renderDates: function(isRigid) {
  51. var view = this.view;
  52. var rowCnt = this.rowCnt;
  53. var colCnt = this.colCnt;
  54. var html = '';
  55. var row;
  56. var col;
  57. for (row = 0; row < rowCnt; row++) {
  58. html += this.renderDayRowHtml(row, isRigid);
  59. }
  60. this.el.html(html);
  61. this.rowEls = this.el.find('.fc-row');
  62. this.cellEls = this.el.find('.fc-day, .fc-disabled-day');
  63. this.rowCoordCache = new CoordCache({
  64. els: this.rowEls,
  65. isVertical: true
  66. });
  67. this.colCoordCache = new CoordCache({
  68. els: this.cellEls.slice(0, this.colCnt), // only the first row
  69. isHorizontal: true
  70. });
  71. // trigger dayRender with each cell's element
  72. for (row = 0; row < rowCnt; row++) {
  73. for (col = 0; col < colCnt; col++) {
  74. this.publiclyTrigger('dayRender', {
  75. context: view,
  76. args: [
  77. this.getCellDate(row, col),
  78. this.getCellEl(row, col),
  79. view
  80. ]
  81. });
  82. }
  83. }
  84. },
  85. unrenderDates: function() {
  86. this.removeSegPopover();
  87. },
  88. // Generates the HTML for a single row, which is a div that wraps a table.
  89. // `row` is the row number.
  90. renderDayRowHtml: function(row, isRigid) {
  91. var theme = this.view.calendar.theme;
  92. var classes = [ 'fc-row', 'fc-week', theme.getClass('dayRow') ];
  93. if (isRigid) {
  94. classes.push('fc-rigid');
  95. }
  96. return '' +
  97. '<div class="' + classes.join(' ') + '">' +
  98. '<div class="fc-bg">' +
  99. '<table class="' + theme.getClass('tableGrid') + '">' +
  100. this.renderBgTrHtml(row) +
  101. '</table>' +
  102. '</div>' +
  103. '<div class="fc-content-skeleton">' +
  104. '<table>' +
  105. (this.numbersVisible ?
  106. '<thead>' +
  107. this.renderNumberTrHtml(row) +
  108. '</thead>' :
  109. ''
  110. ) +
  111. '</table>' +
  112. '</div>' +
  113. '</div>';
  114. },
  115. /* Grid Number Rendering
  116. ------------------------------------------------------------------------------------------------------------------*/
  117. renderNumberTrHtml: function(row) {
  118. return '' +
  119. '<tr>' +
  120. (this.isRTL ? '' : this.renderNumberIntroHtml(row)) +
  121. this.renderNumberCellsHtml(row) +
  122. (this.isRTL ? this.renderNumberIntroHtml(row) : '') +
  123. '</tr>';
  124. },
  125. renderNumberIntroHtml: function(row) {
  126. return this.renderIntroHtml();
  127. },
  128. renderNumberCellsHtml: function(row) {
  129. var htmls = [];
  130. var col, date;
  131. for (col = 0; col < this.colCnt; col++) {
  132. date = this.getCellDate(row, col);
  133. htmls.push(this.renderNumberCellHtml(date));
  134. }
  135. return htmls.join('');
  136. },
  137. // Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton.
  138. // The number row will only exist if either day numbers or week numbers are turned on.
  139. renderNumberCellHtml: function(date) {
  140. var view = this.view;
  141. var html = '';
  142. var isDateValid = view.activeUnzonedRange.containsDate(date); // TODO: called too frequently. cache somehow.
  143. var isDayNumberVisible = view.dayNumbersVisible && isDateValid;
  144. var classes;
  145. var weekCalcFirstDoW;
  146. if (!isDayNumberVisible && !view.cellWeekNumbersVisible) {
  147. // no numbers in day cell (week number must be along the side)
  148. return '<td/>'; // will create an empty space above events :(
  149. }
  150. classes = this.getDayClasses(date);
  151. classes.unshift('fc-day-top');
  152. if (view.cellWeekNumbersVisible) {
  153. // To determine the day of week number change under ISO, we cannot
  154. // rely on moment.js methods such as firstDayOfWeek() or weekday(),
  155. // because they rely on the locale's dow (possibly overridden by
  156. // our firstDay option), which may not be Monday. We cannot change
  157. // dow, because that would affect the calendar start day as well.
  158. if (date._locale._fullCalendar_weekCalc === 'ISO') {
  159. weekCalcFirstDoW = 1; // Monday by ISO 8601 definition
  160. }
  161. else {
  162. weekCalcFirstDoW = date._locale.firstDayOfWeek();
  163. }
  164. }
  165. html += '<td class="' + classes.join(' ') + '"' +
  166. (isDateValid ?
  167. ' data-date="' + date.format() + '"' :
  168. ''
  169. ) +
  170. '>';
  171. if (view.cellWeekNumbersVisible && (date.day() == weekCalcFirstDoW)) {
  172. html += view.buildGotoAnchorHtml(
  173. { date: date, type: 'week' },
  174. { 'class': 'fc-week-number' },
  175. date.format('w') // inner HTML
  176. );
  177. }
  178. if (isDayNumberVisible) {
  179. html += view.buildGotoAnchorHtml(
  180. date,
  181. { 'class': 'fc-day-number' },
  182. date.date() // inner HTML
  183. );
  184. }
  185. html += '</td>';
  186. return html;
  187. },
  188. /* Hit System
  189. ------------------------------------------------------------------------------------------------------------------*/
  190. prepareHits: function() {
  191. this.colCoordCache.build();
  192. this.rowCoordCache.build();
  193. this.rowCoordCache.bottoms[this.rowCnt - 1] += this.bottomCoordPadding; // hack
  194. },
  195. releaseHits: function() {
  196. this.colCoordCache.clear();
  197. this.rowCoordCache.clear();
  198. },
  199. queryHit: function(leftOffset, topOffset) {
  200. if (this.colCoordCache.isLeftInBounds(leftOffset) && this.rowCoordCache.isTopInBounds(topOffset)) {
  201. var col = this.colCoordCache.getHorizontalIndex(leftOffset);
  202. var row = this.rowCoordCache.getVerticalIndex(topOffset);
  203. if (row != null && col != null) {
  204. return this.getCellHit(row, col);
  205. }
  206. }
  207. },
  208. getHitFootprint: function(hit) {
  209. var range = this.getCellRange(hit.row, hit.col);
  210. return new ComponentFootprint(
  211. new UnzonedRange(range.start, range.end),
  212. true // all-day?
  213. );
  214. },
  215. getHitEl: function(hit) {
  216. return this.getCellEl(hit.row, hit.col);
  217. },
  218. /* Cell System
  219. ------------------------------------------------------------------------------------------------------------------*/
  220. // FYI: the first column is the leftmost column, regardless of date
  221. getCellHit: function(row, col) {
  222. return {
  223. row: row,
  224. col: col,
  225. component: this, // needed unfortunately :(
  226. left: this.colCoordCache.getLeftOffset(col),
  227. right: this.colCoordCache.getRightOffset(col),
  228. top: this.rowCoordCache.getTopOffset(row),
  229. bottom: this.rowCoordCache.getBottomOffset(row)
  230. };
  231. },
  232. getCellEl: function(row, col) {
  233. return this.cellEls.eq(row * this.colCnt + col);
  234. },
  235. /* Event Rendering
  236. ------------------------------------------------------------------------------------------------------------------*/
  237. renderBgEventFootprints: function(eventFootprints) {
  238. // don't render timed background events
  239. var allDayEventFootprints = $.grep(eventFootprints, function(eventFootprint) {
  240. return eventFootprint.componentFootprint.isAllDay;
  241. });
  242. return InteractiveDateComponent.prototype.renderBgEventFootprints.call(this, allDayEventFootprints);
  243. },
  244. // Unrenders all events currently rendered on the grid
  245. unrenderEvents: function() {
  246. this.removeSegPopover(); // removes the "more.." events popover
  247. InteractiveDateComponent.prototype.unrenderEvents.apply(this, arguments);
  248. },
  249. // Retrieves all rendered segment objects currently rendered on the grid
  250. getEventSegs: function() {
  251. return InteractiveDateComponent.prototype.getEventSegs.call(this) // get the segments from the super-method
  252. .concat(this.popoverSegs || []); // append the segments from the "more..." popover
  253. },
  254. /* Event Drag Visualization
  255. ------------------------------------------------------------------------------------------------------------------*/
  256. // Renders a visual indication of an event or external element being dragged.
  257. // `eventLocation` has zoned start and end (optional)
  258. renderDrag: function(eventFootprints, seg, isTouch) {
  259. var i;
  260. for (i = 0; i < eventFootprints.length; i++) {
  261. this.renderHighlight(eventFootprints[i].componentFootprint);
  262. }
  263. // if a segment from the same calendar but another component is being dragged, render a helper event
  264. if (seg && seg.component !== this) {
  265. this.helperRenderer.renderEventDraggingFootprints(eventFootprints, seg, isTouch);
  266. return true; // signal that a helper was rendered
  267. }
  268. },
  269. // Unrenders any visual indication of a hovering event
  270. unrenderDrag: function() {
  271. this.unrenderHighlight();
  272. this.helperRenderer.unrender();
  273. },
  274. /* Event Resize Visualization
  275. ------------------------------------------------------------------------------------------------------------------*/
  276. // Renders a visual indication of an event being resized
  277. renderEventResize: function(eventFootprints, seg, isTouch) {
  278. var i;
  279. for (i = 0; i < eventFootprints.length; i++) {
  280. this.renderHighlight(eventFootprints[i].componentFootprint);
  281. }
  282. this.helperRenderer.renderEventResizingFootprints(eventFootprints, seg, isTouch);
  283. },
  284. // Unrenders a visual indication of an event being resized
  285. unrenderEventResize: function() {
  286. this.unrenderHighlight();
  287. this.helperRenderer.unrender();
  288. },
  289. /* Business Hours
  290. ------------------------------------------------------------------------------------------------------------------*/
  291. businessHourRendererClass: BusinessHourRenderer.extend({
  292. isWholeDay: true // TODO: config param on component?
  293. })
  294. });