/* A component that renders one or more columns of vertical time slots ----------------------------------------------------------------------------------------------------------------------*/ function TimeGrid(view) { Grid.call(this, view); // call the super-constructor } TimeGrid.prototype = createObject(Grid.prototype); // define the super-class $.extend(TimeGrid.prototype, { slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines snapDuration: null, // granularity of time for dragging and selecting minTime: null, // Duration object that denotes the first visible time of any given day maxTime: null, // Duration object that denotes the exclusive visible end time of any given day dayEls: null, // cells elements in the day-row background slatEls: null, // elements running horizontally across all columns slatTops: null, // an array of top positions, relative to the container. last item holds bottom of last slot highlightEl: null, // cell skeleton element for rendering the highlight helperEl: null, // cell skeleton element for rendering the mock event "helper" // Renders the time grid into `this.el`, which should already be assigned. // Relies on the view's colCnt. In the future, this component should probably be self-sufficient. render: function() { this.processOptions(); this.el.html(this.renderHtml()); this.dayEls = this.el.find('.fc-day'); this.slatEls = this.el.find('.fc-slats tr'); this.computeSlatTops(); Grid.prototype.render.call(this); // call the super-method }, // Renders the basic HTML skeleton for the grid renderHtml: function() { return '' + '
' + '' + this.rowHtml('slotBg') + // leverages RowRenderer, which will call slotBgCellHtml '
' + '
' + '
' + '' + this.slatRowHtml() + '
' + '
'; }, // Renders the HTML for a vertical background cell behind the slots. // This method is distinct from 'bg' because we wanted a new `rowType` so the View could customize the rendering. slotBgCellHtml: function(row, col, date) { return this.bgCellHtml(row, col, date); }, // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL. slatRowHtml: function() { var view = this.view; var calendar = view.calendar; var isRTL = view.opt('isRTL'); var html = ''; var slotNormal = this.slotDuration.asMinutes() % 15 === 0; var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations var slotDate; // will be on the view's first day, but we only care about its time var minutes; var axisHtml; // Calculate the time for each slot while (slotTime < this.maxTime) { slotDate = view.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues minutes = slotDate.minutes(); axisHtml = '' + ((!slotNormal || !minutes) ? // if irregular slot duration, or on the hour, then display the time '' + // for matchCellWidths htmlEscape(calendar.formatDate(slotDate, view.opt('axisFormat'))) + '' : '' ) + ''; html += '' + (!isRTL ? axisHtml : '') + '' + (isRTL ? axisHtml : '') + ""; slotTime.add(this.slotDuration); } return html; }, // Parses various options into properties of this object processOptions: function() { var view = this.view; var slotDuration = view.opt('slotDuration'); var snapDuration = view.opt('snapDuration'); slotDuration = moment.duration(slotDuration); snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration; this.slotDuration = slotDuration; this.snapDuration = snapDuration; this.cellDuration = snapDuration; // important to assign this for Grid.events.js this.minTime = moment.duration(view.opt('minTime')); this.maxTime = moment.duration(view.opt('maxTime')); }, // Slices up a date range into a segment for each column rangeToSegs: function(rangeStart, rangeEnd) { var view = this.view; var segs = []; var seg; var col; var cellDate; var colStart, colEnd; // normalize rangeStart = rangeStart.clone().stripZone(); rangeEnd = rangeEnd.clone().stripZone(); for (col = 0; col < view.colCnt; col++) { cellDate = view.cellToDate(0, col); // use the View's cell system for this colStart = cellDate.clone().time(this.minTime); colEnd = cellDate.clone().time(this.maxTime); seg = intersectionToSeg(rangeStart, rangeEnd, colStart, colEnd); if (seg) { seg.col = col; segs.push(seg); } } return segs; }, /* Coordinates ------------------------------------------------------------------------------------------------------------------*/ // Called when there is a window resize/zoom and we need to recalculate coordinates for the grid resize: function() { this.computeSlatTops(); this.updateSegVerticals(); }, // Populates the given empty `rows` and `cols` arrays with offset positions of the "snap" cells. // "Snap" cells are different the slots because they might have finer granularity. buildCoords: function(rows, cols) { var colCnt = this.view.colCnt; var originTop = this.el.offset().top; var snapTime = moment.duration(+this.minTime); var p = null; var e, n; this.dayEls.slice(0, colCnt).each(function(i, _e) { e = $(_e); n = e.offset().left; if (p) { p[1] = n; } p = [ n ]; cols[i] = p; }); p[1] = n + e.outerWidth(); p = null; while (snapTime < this.maxTime) { n = originTop + this.computeTimeTop(snapTime); if (p) { p[1] = n; } p = [ n ]; rows.push(p); snapTime.add(this.snapDuration); } p[1] = originTop + this.computeTimeTop(snapTime); // the position of the exclusive end }, // Gets the datetime for the given slot cell getCellDate: function(cell) { var view = this.view; var calendar = view.calendar; return calendar.rezoneDate( // since we are adding a time, it needs to be in the calendar's timezone view.cellToDate(0, cell.col) // View's coord system only accounts for start-of-day for column .time(this.minTime + this.snapDuration * cell.row) ); }, // Gets the element that represents the whole-day the cell resides on getCellDayEl: function(cell) { return this.dayEls.eq(cell.col); }, // Computes the top coordinate, relative to the bounds of the grid, of the given date. // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight. computeDateTop: function(date, startOfDayDate) { return this.computeTimeTop( moment.duration( date.clone().stripZone() - startOfDayDate.clone().stripTime() ) ); }, // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration). computeTimeTop: function(time) { var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered var slatIndex; var slatRemainder; var slatTop; var slatBottom; // constrain. because minTime/maxTime might be customized slatCoverage = Math.max(0, slatCoverage); slatCoverage = Math.min(this.slatEls.length, slatCoverage); slatIndex = Math.floor(slatCoverage); // an integer index of the furthest whole slot slatRemainder = slatCoverage - slatIndex; slatTop = this.slatTops[slatIndex]; // the top position of the furthest whole slot if (slatRemainder) { // time spans part-way into the slot slatBottom = this.slatTops[slatIndex + 1]; return slatTop + (slatBottom - slatTop) * slatRemainder; // part-way between slots } else { return slatTop; } }, // Queries each `slatEl` for its position relative to the grid's container and stores it in `slatTops`. // Includes the the bottom of the last slat as the last item in the array. computeSlatTops: function() { var tops = []; var top; this.slatEls.each(function(i, node) { top = $(node).position().top; tops.push(top); }); tops.push(top + this.slatEls.last().outerHeight()); // bottom of the last slat this.slatTops = tops; }, /* Event Drag Visualization ------------------------------------------------------------------------------------------------------------------*/ // Renders a visual indication of an event being dragged over the specified date(s). // `end` and `seg` can be null. See View's documentation on renderDrag for more info. renderDrag: function(start, end, seg) { var opacity; if (seg) { // if there is event information for this drag, render a helper event this.renderRangeHelper(start, end, seg); opacity = this.view.opt('dragOpacity'); if (opacity !== undefined) { this.helperEl.css('opacity', opacity); } return true; // signal that a helper has been rendered } else { // otherwise, just render a highlight this.renderHighlight( start, end || this.view.calendar.getDefaultEventEnd(false, start) ); } }, // Unrenders any visual indication of an event being dragged destroyDrag: function() { this.destroyHelper(); this.destroyHighlight(); }, /* Event Resize Visualization ------------------------------------------------------------------------------------------------------------------*/ // Renders a visual indication of an event being resized renderResize: function(start, end, seg) { this.renderRangeHelper(start, end, seg); }, // Unrenders any visual indication of an event being resized destroyResize: function() { this.destroyHelper(); }, /* Event Helper ------------------------------------------------------------------------------------------------------------------*/ // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag) renderHelper: function(event, sourceSeg) { var res = this.renderEventTable([ event ]); var tableEl = res.tableEl; var segs = res.segs; var i, seg; var sourceEl; // Try to make the segment that is in the same row as sourceSeg look the same for (i = 0; i < segs.length; i++) { seg = segs[i]; if (sourceSeg && sourceSeg.col === seg.col) { sourceEl = sourceSeg.el; seg.el.css({ left: sourceEl.css('left'), right: sourceEl.css('right'), 'margin-left': sourceEl.css('margin-left'), 'margin-right': sourceEl.css('margin-right') }); } } this.helperEl = $('
') .append(tableEl) .appendTo(this.el); }, // Unrenders any mock helper event destroyHelper: function() { if (this.helperEl) { this.helperEl.remove(); this.helperEl = null; } }, /* Selection ------------------------------------------------------------------------------------------------------------------*/ // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight. renderSelection: function(start, end) { if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered this.renderRangeHelper(start, end); } else { this.renderHighlight(start, end); } }, // Unrenders any visual indication of a selection destroySelection: function() { this.destroyHelper(); this.destroyHighlight(); }, /* Highlight ------------------------------------------------------------------------------------------------------------------*/ // Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive. renderHighlight: function(start, end) { this.highlightEl = $( this.highlightSkeletonHtml(start, end) ).appendTo(this.el); }, // Unrenders the emphasis on a date range destroyHighlight: function() { if (this.highlightEl) { this.highlightEl.remove(); this.highlightEl = null; } }, // Generates HTML for a table element with containers in each column, responsible for absolutely positioning the // highlight elements to cover the highlighted slots. highlightSkeletonHtml: function(start, end) { var view = this.view; var segs = this.rangeToSegs(start, end); var cellHtml = ''; var col = 0; var i, seg; var dayDate; var top, bottom; for (i = 0; i < segs.length; i++) { // loop through the segments. one per column seg = segs[i]; // need empty cells beforehand? if (col < seg.col) { cellHtml += ''; col = seg.col; } // compute vertical position dayDate = view.cellToDate(0, col); top = this.computeDateTop(seg.start, dayDate); bottom = this.computeDateTop(seg.end, dayDate); // the y position of the bottom edge // generate the cell HTML. bottom becomes negative because it needs to be a CSS value relative to the // bottom edge of the zero-height container. cellHtml += '' + '
' + '
' + '
' + ''; col++; } // need empty cells after the last segment? if (col < view.colCnt) { cellHtml += ''; } cellHtml = this.bookendCells(cellHtml, 'highlight'); return '' + '
' + '' + '' + cellHtml + '' + '
' + '
'; } });