wrapper around the
+ var topEl; // the element we want to match the top coordinate of
+ var options;
+
+ if (this.rowCnt == 1) {
+ topEl = view.el; // will cause the popover to cover any sort of header
+ }
+ else {
+ topEl = this.rowEls.eq(cell.row); // will align with top of row
+ }
+
+ options = {
+ className: 'fc-more-popover',
+ content: this.renderSegPopoverContent(cell, segs),
+ parentEl: this.el,
+ top: topEl.offset().top,
+ autoHide: true, // when the user clicks elsewhere, hide the popover
+ viewportConstrain: view.opt('popoverViewportConstrain'),
+ hide: function() {
+ // kill everything when the popover is hidden
+ _this.segPopover.removeElement();
+ _this.segPopover = null;
+ _this.popoverSegs = null;
+ }
+ };
+
+ // Determine horizontal coordinate.
+ // We use the moreWrap instead of the to avoid border confusion.
+ if (this.isRTL) {
+ options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border
+ }
+ else {
+ options.left = moreWrap.offset().left - 1; // -1 to be over cell border
+ }
+
+ this.segPopover = new Popover(options);
+ this.segPopover.show();
+ },
+
+
+ // Builds the inner DOM contents of the segment popover
+ renderSegPopoverContent: function(cell, segs) {
+ var view = this.view;
+ var isTheme = view.opt('theme');
+ var title = cell.start.format(view.opt('dayPopoverFormat'));
+ var content = $(
+ '' +
+ ''
+ );
+ var segContainer = content.find('.fc-event-container');
+ var i;
+
+ // render each seg's `el` and only return the visible segs
+ segs = this.renderFgSegEls(segs, true); // disableResizing=true
+ this.popoverSegs = segs;
+
+ for (i = 0; i < segs.length; i++) {
+
+ // because segments in the popover are not part of a grid coordinate system, provide a hint to any
+ // grids that want to do drag-n-drop about which cell it came from
+ segs[i].cell = cell;
+
+ segContainer.append(segs[i].el);
+ }
+
+ return content;
+ },
+
+
+ // Given the events within an array of segment objects, reslice them to be in a single day
+ resliceDaySegs: function(segs, dayDate) {
+
+ // build an array of the original events
+ var events = $.map(segs, function(seg) {
+ return seg.event;
+ });
+
+ var dayStart = dayDate.clone().stripTime();
+ var dayEnd = dayStart.clone().add(1, 'days');
+ var dayRange = { start: dayStart, end: dayEnd };
+
+ // slice the events with a custom slicing function
+ segs = this.eventsToSegs(
+ events,
+ function(range) {
+ var seg = intersectionToSeg(range, dayRange); // undefind if no intersection
+ return seg ? [ seg ] : []; // must return an array of segments
+ }
+ );
+
+ // force an order because eventsToSegs doesn't guarantee one
+ segs.sort(compareSegs);
+
+ return segs;
+ },
+
+
+ // Generates the text that should be inside a "more" link, given the number of events it represents
+ getMoreLinkText: function(num) {
+ var opt = this.view.opt('eventLimitText');
+
+ if (typeof opt === 'function') {
+ return opt(num);
+ }
+ else {
+ return '+' + num + ' ' + opt;
+ }
+ },
+
+
+ // Returns segments within a given cell.
+ // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs.
+ getCellSegs: function(cell, startLevel) {
+ var segMatrix = this.rowStructs[cell.row].segMatrix;
+ var level = startLevel || 0;
+ var segs = [];
+ var seg;
+
+ while (level < segMatrix.length) {
+ seg = segMatrix[level][cell.col];
+ if (seg) {
+ segs.push(seg);
+ }
+ level++;
+ }
+
+ return segs;
+ }
+
+});
+
+;;
+
+/* A component that renders one or more columns of vertical time slots
+----------------------------------------------------------------------------------------------------------------------*/
+
+var TimeGrid = Grid.extend({
+
+ 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
+ colDates: null, // whole-day dates for each column. left to right
+ axisFormat: null, // formatting string for times running along vertical axis
+
+ 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
+
+ helperEl: null, // cell skeleton element for rendering the mock event "helper"
+
+ businessHourSegs: null,
+
+
+ constructor: function() {
+ Grid.apply(this, arguments); // call the super-constructor
+ this.processOptions();
+ },
+
+
+ // 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.
+ renderDates: function() {
+ this.el.html(this.renderHtml());
+ this.dayEls = this.el.find('.fc-day');
+ this.slatEls = this.el.find('.fc-slats tr');
+ },
+
+
+ renderBusinessHours: function() {
+ var events = this.view.calendar.getBusinessHoursEvents();
+ this.businessHourSegs = this.renderFill('businessHours', this.eventsToSegs(events), 'bgevent');
+ },
+
+
+ // 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(cell) {
+ return this.bgCellHtml(cell);
+ },
+
+
+ // 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 isRTL = this.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 = this.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(slotDate.format(this.axisFormat)) +
+ '' :
+ ''
+ ) +
+ ' | ';
+
+ html +=
+ '
' +
+ (!isRTL ? axisHtml : '') +
+ ' | ' +
+ (isRTL ? axisHtml : '') +
+ "
";
+
+ slotTime.add(this.slotDuration);
+ }
+
+ return html;
+ },
+
+
+ /* Options
+ ------------------------------------------------------------------------------------------------------------------*/
+
+
+ // 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; // for Grid system
+
+ this.minTime = moment.duration(view.opt('minTime'));
+ this.maxTime = moment.duration(view.opt('maxTime'));
+
+ this.axisFormat = view.opt('axisFormat') || view.opt('smallTimeFormat');
+ },
+
+
+ // Computes a default column header formatting string if `colFormat` is not explicitly defined
+ computeColHeadFormat: function() {
+ if (this.colCnt > 1) { // multiple days, so full single date string WON'T be in title text
+ return this.view.opt('dayOfMonthFormat'); // "Sat 12/10"
+ }
+ else { // single day, so full single date string will probably be in title text
+ return 'dddd'; // "Saturday"
+ }
+ },
+
+
+ // Computes a default event time formatting string if `timeFormat` is not explicitly defined
+ computeEventTimeFormat: function() {
+ return this.view.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM)
+ },
+
+
+ // Computes a default `displayEventEnd` value if one is not expliclty defined
+ computeDisplayEventEnd: function() {
+ return true;
+ },
+
+
+ /* Cell System
+ ------------------------------------------------------------------------------------------------------------------*/
+
+
+ rangeUpdated: function() {
+ var view = this.view;
+ var colDates = [];
+ var date;
+
+ date = this.start.clone();
+ while (date.isBefore(this.end)) {
+ colDates.push(date.clone());
+ date.add(1, 'day');
+ date = view.skipHiddenDays(date);
+ }
+
+ if (this.isRTL) {
+ colDates.reverse();
+ }
+
+ this.colDates = colDates;
+ this.colCnt = colDates.length;
+ this.rowCnt = Math.ceil((this.maxTime - this.minTime) / this.snapDuration); // # of vertical snaps
+ },
+
+
+ // Given a cell object, generates its start date. Returns a reference-free copy.
+ computeCellDate: function(cell) {
+ var date = this.colDates[cell.col];
+ var time = this.computeSnapTime(cell.row);
+
+ date = this.view.calendar.rezoneDate(date); // give it a 00:00 time
+ date.time(time);
+
+ return date;
+ },
+
+
+ // Retrieves the element representing the given column
+ getColEl: function(col) {
+ return this.dayEls.eq(col);
+ },
+
+
+ /* Dates
+ ------------------------------------------------------------------------------------------------------------------*/
+
+
+ // Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day
+ computeSnapTime: function(row) {
+ return moment.duration(this.minTime + this.snapDuration * row);
+ },
+
+
+ // Slices up a date range by column into an array of segments
+ rangeToSegs: function(range) {
+ var colCnt = this.colCnt;
+ var segs = [];
+ var seg;
+ var col;
+ var colDate;
+ var colRange;
+
+ // normalize :(
+ range = {
+ start: range.start.clone().stripZone(),
+ end: range.end.clone().stripZone()
+ };
+
+ for (col = 0; col < colCnt; col++) {
+ colDate = this.colDates[col]; // will be ambig time/timezone
+ colRange = {
+ start: colDate.clone().time(this.minTime),
+ end: colDate.clone().time(this.maxTime)
+ };
+ seg = intersectionToSeg(range, colRange); // both will be ambig timezone
+ if (seg) {
+ seg.col = col;
+ segs.push(seg);
+ }
+ }
+
+ return segs;
+ },
+
+
+ /* Coordinates
+ ------------------------------------------------------------------------------------------------------------------*/
+
+
+ updateSize: function(isResize) { // NOT a standard Grid method
+ this.computeSlatTops();
+
+ if (isResize) {
+ this.updateSegVerticals();
+ }
+ },
+
+
+ // Computes the top/bottom coordinates of each "snap" rows
+ computeRowCoords: function() {
+ var originTop = this.el.offset().top;
+ var items = [];
+ var i;
+ var item;
+
+ for (i = 0; i < this.rowCnt; i++) {
+ item = {
+ top: originTop + this.computeTimeTop(this.computeSnapTime(i))
+ };
+ if (i > 0) {
+ items[i - 1].bottom = item.top;
+ }
+ items.push(item);
+ }
+ item.bottom = item.top + this.computeTimeTop(this.computeSnapTime(i));
+
+ return items;
+ },
+
+
+ // 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).
+ // dropLocation's end might be null, as well as `seg`. See Grid::renderDrag for more info.
+ // A returned value of `true` signals that a mock "helper" event has been rendered.
+ renderDrag: function(dropLocation, seg) {
+
+ if (seg) { // if there is event information for this drag, render a helper event
+ this.renderRangeHelper(dropLocation, seg);
+ this.applyDragOpacity(this.helperEl);
+
+ return true; // signal that a helper has been rendered
+ }
+ else {
+ // otherwise, just render a highlight
+ this.renderHighlight(this.eventRangeToSegs(dropLocation));
+ }
+ },
+
+
+ // Unrenders any visual indication of an event being dragged
+ unrenderDrag: function() {
+ this.unrenderHelper();
+ this.unrenderHighlight();
+ },
+
+
+ /* Event Resize Visualization
+ ------------------------------------------------------------------------------------------------------------------*/
+
+
+ // Renders a visual indication of an event being resized
+ renderEventResize: function(range, seg) {
+ this.renderRangeHelper(range, seg);
+ },
+
+
+ // Unrenders any visual indication of an event being resized
+ unrenderEventResize: function() {
+ this.unrenderHelper();
+ },
+
+
+ /* 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 segs = this.eventsToSegs([ event ]);
+ var tableEl;
+ var i, seg;
+ var sourceEl;
+
+ segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered
+ tableEl = this.renderSegTable(segs);
+
+ // 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')
});
}
}
- return segs.sort(compareSlotSegs);
- }
-
-
- // renders events in the 'time slots' at the bottom
- // TODO: when we refactor this, when user returns `false` eventRender, don't have empty space
- // TODO: refactor will include using pixels to detect collisions instead of dates (handy for seg cmp)
-
- function renderSlotSegs(segs, modifiedEventId) {
-
- var i, segCnt=segs.length, seg,
- event,
- top,
- bottom,
- columnLeft,
- columnRight,
- columnWidth,
- width,
- left,
- right,
- html = '',
- eventElements,
- eventElement,
- triggerRes,
- titleElement,
- height,
- slotSegmentContainer = getSlotSegmentContainer(),
- isRTL = opt('isRTL');
-
- // calculate position/dimensions, create html
- for (i=0; i
')
+ .append(tableEl)
+ .appendTo(this.el);
+ },
- // shave off space on right near scrollbars (2.5%)
- // TODO: move this to CSS somehow
- columnRight -= columnWidth * .025;
- columnWidth = columnRight - columnLeft;
- width = columnWidth * (seg.forwardCoord - seg.backwardCoord);
-
- if (opt('slotEventOverlap')) {
- // double the width while making sure resize handle is visible
- // (assumed to be 20px wide)
- width = Math.max(
- (width - (20/2)) * 2,
- width // narrow columns will want to make the segment smaller than
- // the natural width. don't allow it
- );
- }
-
- if (isRTL) {
- right = columnRight - seg.backwardCoord * columnWidth;
- left = right - width;
- }
- else {
- left = columnLeft + seg.backwardCoord * columnWidth;
- right = left + width;
- }
-
- // make sure horizontal coordinates are in bounds
- left = Math.max(left, columnLeft);
- right = Math.min(right, columnRight);
- width = right - left;
-
- seg.top = top;
- seg.left = left;
- seg.outerWidth = width;
- seg.outerHeight = bottom - top;
- html += slotSegHtml(event, seg);
+ // Unrenders any mock helper event
+ unrenderHelper: function() {
+ if (this.helperEl) {
+ this.helperEl.remove();
+ this.helperEl = null;
}
+ },
- slotSegmentContainer[0].innerHTML = html; // faster than html()
- eventElements = slotSegmentContainer.children();
-
- // retrieve elements, run through eventRender callback, bind event handlers
- for (i=0; i
' +
+ '' +
+ ' '
+ );
+ trEl = skeletonEl.find('tr');
+
+ for (col = 0; col < segCols.length; col++) {
+ colSegs = segCols[col];
+ tdEl = $('