ListView.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. /*
  2. Responsible for the scroller, and forwarding event-related actions into the "grid".
  3. TODO: remove CoordChronoComponentMixin
  4. BUT FIRST... need to decouple event click/mouseover/mouseout handlers
  5. */
  6. var ListView = View.extend(CoordChronoComponentMixin, SegChronoComponentMixin, {
  7. scroller: null,
  8. contentEl: null,
  9. dayDates: null, // localized ambig-time moment array
  10. dayRanges: null, // UnzonedRange[], of start-end of each day
  11. segSelector: '.fc-list-item', // which elements accept event actions
  12. hasDayInteractions: false, // no day selection or day clicking
  13. initialize: function() {
  14. // a requirement for CoordChronoComponentMixin
  15. this.initCoordChronoComponent();
  16. // a requirement for SegChronoComponentMixin
  17. this.initSegChronoComponent();
  18. this.scroller = new Scroller({
  19. overflowX: 'hidden',
  20. overflowY: 'auto'
  21. });
  22. },
  23. _getView: function() {
  24. return this;
  25. },
  26. renderSkeleton: function() {
  27. this.el.addClass(
  28. 'fc-list-view ' +
  29. this.calendar.theme.getClass('listView')
  30. );
  31. this.scroller.render();
  32. this.scroller.el.appendTo(this.el);
  33. this.contentEl = this.scroller.scrollEl; // shortcut
  34. },
  35. unrenderSkeleton: function() {
  36. this.scroller.destroy(); // will remove the Grid too
  37. },
  38. setHeight: function(totalHeight, isAuto) {
  39. this.scroller.setHeight(this.computeScrollerHeight(totalHeight));
  40. },
  41. computeScrollerHeight: function(totalHeight) {
  42. return totalHeight -
  43. subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller
  44. },
  45. renderDates: function() {
  46. var calendar = this.calendar;
  47. var dayStart = calendar.msToUtcMoment(this.renderUnzonedRange.startMs, true);
  48. var viewEnd = calendar.msToUtcMoment(this.renderUnzonedRange.endMs, true);
  49. var dayDates = [];
  50. var dayRanges = [];
  51. while (dayStart < viewEnd) {
  52. dayDates.push(dayStart.clone());
  53. dayRanges.push(new UnzonedRange(
  54. dayStart,
  55. dayStart.clone().add(1, 'day')
  56. ));
  57. dayStart.add(1, 'day');
  58. }
  59. this.dayDates = dayDates;
  60. this.dayRanges = dayRanges;
  61. // a requirement of SegChronoComponentMixin.
  62. // TODO: easy to forget. use listener.
  63. this.eventRenderUtils.rangeUpdated();
  64. },
  65. isEventDefResizable: function(eventDef) {
  66. return false;
  67. },
  68. isEventDefDraggable: function(eventDef) {
  69. return false;
  70. },
  71. // slices by day
  72. componentFootprintToSegs: function(footprint) {
  73. var dayRanges = this.dayRanges;
  74. var dayIndex;
  75. var segRange;
  76. var seg;
  77. var segs = [];
  78. for (dayIndex = 0; dayIndex < dayRanges.length; dayIndex++) {
  79. segRange = footprint.unzonedRange.intersect(dayRanges[dayIndex]);
  80. if (segRange) {
  81. seg = {
  82. startMs: segRange.startMs,
  83. endMs: segRange.endMs,
  84. isStart: segRange.isStart,
  85. isEnd: segRange.isEnd,
  86. dayIndex: dayIndex
  87. };
  88. segs.push(seg);
  89. // detect when footprint won't go fully into the next day,
  90. // and mutate the latest seg to the be the end.
  91. if (
  92. !seg.isEnd && !footprint.isAllDay &&
  93. footprint.unzonedRange.endMs < dayRanges[dayIndex + 1].startMs + this.nextDayThreshold
  94. ) {
  95. seg.endMs = footprint.unzonedRange.endMs;
  96. seg.isEnd = true;
  97. break;
  98. }
  99. }
  100. }
  101. return segs;
  102. },
  103. // like "4:00am"
  104. computeEventTimeFormat: function() {
  105. return this.opt('mediumTimeFormat');
  106. },
  107. // for events with a url, the whole <tr> should be clickable,
  108. // but it's impossible to wrap with an <a> tag. simulate this.
  109. handleSegClick: function(seg, ev) {
  110. var url;
  111. CoordChronoComponentMixin.handleSegClick.apply(this, arguments); // super. might prevent the default action
  112. // not clicking on or within an <a> with an href
  113. if (!$(ev.target).closest('a[href]').length) {
  114. url = seg.footprint.eventDef.url;
  115. if (url && !ev.isDefaultPrevented()) { // jsEvent not cancelled in handler
  116. window.location.href = url; // simulate link click
  117. }
  118. }
  119. },
  120. // returns list of foreground segs that were actually rendered
  121. renderFgSegs: function(segs) {
  122. segs = this.renderFgSegEls(segs); // might filter away hidden events
  123. if (!segs.length) {
  124. this.renderEmptyMessage();
  125. }
  126. else {
  127. this.renderSegList(segs);
  128. }
  129. return segs;
  130. },
  131. renderEmptyMessage: function() {
  132. this.contentEl.html(
  133. '<div class="fc-list-empty-wrap2">' + // TODO: try less wraps
  134. '<div class="fc-list-empty-wrap1">' +
  135. '<div class="fc-list-empty">' +
  136. htmlEscape(this.opt('noEventsMessage')) +
  137. '</div>' +
  138. '</div>' +
  139. '</div>'
  140. );
  141. },
  142. // render the event segments in the view
  143. renderSegList: function(allSegs) {
  144. var segsByDay = this.groupSegsByDay(allSegs); // sparse array
  145. var dayIndex;
  146. var daySegs;
  147. var i;
  148. var tableEl = $('<table class="fc-list-table ' + this.calendar.theme.getClass('tableList') + '"><tbody/></table>');
  149. var tbodyEl = tableEl.find('tbody');
  150. for (dayIndex = 0; dayIndex < segsByDay.length; dayIndex++) {
  151. daySegs = segsByDay[dayIndex];
  152. if (daySegs) { // sparse array, so might be undefined
  153. // append a day header
  154. tbodyEl.append(this.dayHeaderHtml(this.dayDates[dayIndex]));
  155. this.sortEventSegs(daySegs);
  156. for (i = 0; i < daySegs.length; i++) {
  157. tbodyEl.append(daySegs[i].el); // append event row
  158. }
  159. }
  160. }
  161. this.contentEl.empty().append(tableEl);
  162. },
  163. // Returns a sparse array of arrays, segs grouped by their dayIndex
  164. groupSegsByDay: function(segs) {
  165. var segsByDay = []; // sparse array
  166. var i, seg;
  167. for (i = 0; i < segs.length; i++) {
  168. seg = segs[i];
  169. (segsByDay[seg.dayIndex] || (segsByDay[seg.dayIndex] = []))
  170. .push(seg);
  171. }
  172. return segsByDay;
  173. },
  174. // generates the HTML for the day headers that live amongst the event rows
  175. dayHeaderHtml: function(dayDate) {
  176. var mainFormat = this.opt('listDayFormat');
  177. var altFormat = this.opt('listDayAltFormat');
  178. return '<tr class="fc-list-heading" data-date="' + dayDate.format('YYYY-MM-DD') + '">' +
  179. '<td class="' + this.calendar.theme.getClass('widgetHeader') + '" colspan="3">' +
  180. (mainFormat ?
  181. this.buildGotoAnchorHtml(
  182. dayDate,
  183. { 'class': 'fc-list-heading-main' },
  184. htmlEscape(dayDate.format(mainFormat)) // inner HTML
  185. ) :
  186. '') +
  187. (altFormat ?
  188. this.buildGotoAnchorHtml(
  189. dayDate,
  190. { 'class': 'fc-list-heading-alt' },
  191. htmlEscape(dayDate.format(altFormat)) // inner HTML
  192. ) :
  193. '') +
  194. '</td>' +
  195. '</tr>';
  196. },
  197. // generates the HTML for a single event row
  198. fgSegHtml: function(seg) {
  199. var calendar = this.calendar;
  200. var theme = calendar.theme;
  201. var eventFootprint = seg.footprint;
  202. var eventDef = eventFootprint.eventDef;
  203. var componentFootprint = eventFootprint.componentFootprint;
  204. var url = eventDef.url;
  205. var classes = [ 'fc-list-item' ].concat(this.eventRenderUtils.getClasses(seg.footprint));
  206. var bgColor = this.eventRenderUtils.getBgColor(seg.footprint);
  207. var timeHtml;
  208. if (componentFootprint.isAllDay) {
  209. timeHtml = this.getAllDayHtml();
  210. }
  211. // if the event appears to span more than one day
  212. else if (this.isMultiDayRange(componentFootprint.unzonedRange)) {
  213. if (seg.isStart || seg.isEnd) { // outer segment that probably lasts part of the day
  214. timeHtml = htmlEscape(this.eventRenderUtils._getTimeText(
  215. calendar.msToMoment(seg.startMs),
  216. calendar.msToMoment(seg.endMs),
  217. componentFootprint.isAllDay
  218. ));
  219. }
  220. else { // inner segment that lasts the whole day
  221. timeHtml = this.getAllDayHtml();
  222. }
  223. }
  224. else {
  225. // Display the normal time text for the *event's* times
  226. timeHtml = htmlEscape(this.eventRenderUtils.getTimeText(eventFootprint));
  227. }
  228. if (url) {
  229. classes.push('fc-has-url');
  230. }
  231. return '<tr class="' + classes.join(' ') + '">' +
  232. (this.eventRenderUtils.displayEventTime ?
  233. '<td class="fc-list-item-time ' + theme.getClass('widgetContent') + '">' +
  234. (timeHtml || '') +
  235. '</td>' :
  236. '') +
  237. '<td class="fc-list-item-marker ' + theme.getClass('widgetContent') + '">' +
  238. '<span class="fc-event-dot"' +
  239. (bgColor ?
  240. ' style="background-color:' + bgColor + '"' :
  241. '') +
  242. '></span>' +
  243. '</td>' +
  244. '<td class="fc-list-item-title ' + theme.getClass('widgetContent') + '">' +
  245. '<a' + (url ? ' href="' + htmlEscape(url) + '"' : '') + '>' +
  246. htmlEscape(eventDef.title || '') +
  247. '</a>' +
  248. '</td>' +
  249. '</tr>';
  250. }
  251. });