From 738e14348d4cb902b9fd490bd5d846f2f27bbe5f Mon Sep 17 00:00:00 2001 From: Mario Vavti Date: Sun, 3 Apr 2016 14:56:19 +0200 Subject: update upstream libs fullcalendar and moment to a recent version --- library/fullcalendar/fullcalendar.js | 1736 +++++++++++++++++++++------------- 1 file changed, 1085 insertions(+), 651 deletions(-) (limited to 'library/fullcalendar/fullcalendar.js') diff --git a/library/fullcalendar/fullcalendar.js b/library/fullcalendar/fullcalendar.js index 4d17f353e..d6042cc17 100644 --- a/library/fullcalendar/fullcalendar.js +++ b/library/fullcalendar/fullcalendar.js @@ -1,5 +1,5 @@ /*! - * FullCalendar v2.5.0-beta + * FullCalendar v2.6.1 * Docs & License: http://fullcalendar.io/ * (c) 2015 Adam Shaw */ @@ -18,7 +18,10 @@ ;; -var FC = $.fullCalendar = { version: "2.5.0-beta" }; +var FC = $.fullCalendar = { + version: "2.6.1", + internalApiVersion: 3 +}; var fcViews = FC.views = {}; @@ -121,7 +124,7 @@ function massageOverrides(input) { ;; // exports -FC.intersectionToSeg = intersectionToSeg; +FC.intersectRanges = intersectRanges; FC.applyAll = applyAll; FC.debounce = debounce; FC.isInt = isInt; @@ -244,7 +247,7 @@ function undistributeHeight(els) { function matchCellWidths(els) { var maxInnerWidth = 0; - els.find('> *').each(function(i, innerEl) { + els.find('> span').each(function(i, innerEl) { var innerWidth = $(innerEl).outerWidth(); if (innerWidth > maxInnerWidth) { maxInnerWidth = innerWidth; @@ -553,10 +556,10 @@ function flexibleCompare(a, b) { ----------------------------------------------------------------------------------------------------------------------*/ -// Creates a basic segment with the intersection of the two ranges. Returns undefined if no intersection. +// Computes the intersection of the two ranges. Returns undefined if no intersection. // Expects all dates to be normalized to the same timezone beforehand. // TODO: move to date section? -function intersectionToSeg(subjectRange, constraintRange) { +function intersectRanges(subjectRange, constraintRange) { var subjectStart = subjectRange.start; var subjectEnd = subjectRange.end; var constraintStart = constraintRange.start; @@ -1580,6 +1583,8 @@ FC.formatRange = formatRange; // expose function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) { + var unzonedDate1 = date1.clone().stripZone(); // for formatSimilarChunk + var unzonedDate2 = date2.clone().stripZone(); // " var chunkStr; // the rendering of the chunk var leftI; var leftStr = ''; @@ -1593,7 +1598,7 @@ function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) { // Start at the leftmost side of the formatting string and continue until you hit a token // that is not the same between dates. for (leftI=0; leftIleftI; rightI--) { - chunkStr = formatSimilarChunk(date1, date2, chunks[rightI]); + chunkStr = formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunks[rightI]); if (chunkStr === false) { break; } @@ -1649,7 +1654,7 @@ var similarUnitMap = { // Given a formatting chunk, and given that both dates are similar in the regard the // formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`. -function formatSimilarChunk(date1, date2, chunk) { +function formatSimilarChunk(date1, date2, unzonedDate1, unzonedDate2, chunk) { var token; var unit; @@ -1658,8 +1663,10 @@ function formatSimilarChunk(date1, date2, chunk) { } else if ((token = chunk.token)) { unit = similarUnitMap[token.charAt(0)]; + // are the dates the same for this unit of measurement? - if (unit && date1.isSame(date2, unit)) { + // use the unzoned dates for this calculation because unreliable when near DST (bug #2396) + if (unit && unzonedDate1.isSame(unzonedDate2, unit)) { return oldMomentFormat(date1, token); // would be the same if we used `date2` // BTW, don't support custom tokens } @@ -2008,6 +2015,7 @@ options: var CoordCache = FC.CoordCache = Class.extend({ els: null, // jQuery set (assumed to be siblings) + forcedOffsetParentEl: null, // options can override the natural offsetParent origin: null, // {left,top} position of offsetParent of els boundingRect: null, // constrain cordinates to this rectangle. {left,right,top,bottom} or null isHorizontal: false, // whether to query for left/right/width @@ -2024,13 +2032,16 @@ var CoordCache = FC.CoordCache = Class.extend({ this.els = $(options.els); this.isHorizontal = options.isHorizontal; this.isVertical = options.isVertical; + this.forcedOffsetParentEl = options.offsetParent ? $(options.offsetParent) : null; }, // Queries the els for coordinates and stores them. // Call this method before using and of the get* methods below. build: function() { - this.origin = this.els.eq(0).offsetParent().offset(); + var offsetParentEl = this.forcedOffsetParentEl || this.els.eq(0).offsetParent(); + + this.origin = offsetParentEl.offset(); this.boundingRect = this.queryBoundingRect(); if (this.isHorizontal) { @@ -2053,6 +2064,14 @@ var CoordCache = FC.CoordCache = Class.extend({ }, + // When called, if coord caches aren't built, builds them + ensureBuilt: function() { + if (!this.origin) { + this.build(); + } + }, + + // Compute and return what the elements' bounding rectangle is, from the user's perspective. // Right now, only returns a rectangle if constrained by an overflow:scroll element. queryBoundingRect: function() { @@ -2105,6 +2124,8 @@ var CoordCache = FC.CoordCache = Class.extend({ // Given a left offset (from document left), returns the index of the el that it horizontally intersects. // If no intersection is made, or outside of the boundingRect, returns undefined. getHorizontalIndex: function(leftOffset) { + this.ensureBuilt(); + var boundingRect = this.boundingRect; var lefts = this.lefts; var rights = this.rights; @@ -2124,6 +2145,8 @@ var CoordCache = FC.CoordCache = Class.extend({ // Given a top offset (from document top), returns the index of the el that it vertically intersects. // If no intersection is made, or outside of the boundingRect, returns undefined. getVerticalIndex: function(topOffset) { + this.ensureBuilt(); + var boundingRect = this.boundingRect; var tops = this.tops; var bottoms = this.bottoms; @@ -2142,12 +2165,14 @@ var CoordCache = FC.CoordCache = Class.extend({ // Gets the left offset (from document left) of the element at the given index getLeftOffset: function(leftIndex) { + this.ensureBuilt(); return this.lefts[leftIndex]; }, // Gets the left position (from offsetParent left) of the element at the given index getLeftPosition: function(leftIndex) { + this.ensureBuilt(); return this.lefts[leftIndex] - this.origin.left; }, @@ -2155,6 +2180,7 @@ var CoordCache = FC.CoordCache = Class.extend({ // Gets the right offset (from document left) of the element at the given index. // This value is NOT relative to the document's right edge, like the CSS concept of "right" would be. getRightOffset: function(leftIndex) { + this.ensureBuilt(); return this.rights[leftIndex]; }, @@ -2162,30 +2188,35 @@ var CoordCache = FC.CoordCache = Class.extend({ // Gets the right position (from offsetParent left) of the element at the given index. // This value is NOT relative to the offsetParent's right edge, like the CSS concept of "right" would be. getRightPosition: function(leftIndex) { + this.ensureBuilt(); return this.rights[leftIndex] - this.origin.left; }, // Gets the width of the element at the given index getWidth: function(leftIndex) { + this.ensureBuilt(); return this.rights[leftIndex] - this.lefts[leftIndex]; }, // Gets the top offset (from document top) of the element at the given index getTopOffset: function(topIndex) { + this.ensureBuilt(); return this.tops[topIndex]; }, // Gets the top position (from offsetParent top) of the element at the given position getTopPosition: function(topIndex) { + this.ensureBuilt(); return this.tops[topIndex] - this.origin.top; }, // Gets the bottom offset (from the document top) of the element at the given index. // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be. getBottomOffset: function(topIndex) { + this.ensureBuilt(); return this.bottoms[topIndex]; }, @@ -2193,12 +2224,14 @@ var CoordCache = FC.CoordCache = Class.extend({ // Gets the bottom position (from the offsetParent top) of the element at the given index. // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be. getBottomPosition: function(topIndex) { + this.ensureBuilt(); return this.bottoms[topIndex] - this.origin.top; }, // Gets the height of the element at the given index getHeight: function(topIndex) { + this.ensureBuilt(); return this.bottoms[topIndex] - this.tops[topIndex]; } @@ -3102,8 +3135,9 @@ var Grid = FC.Grid = Class.extend({ }, - // Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects - rangeToSegs: function(range) { + // Converts a span (has unzoned start/end and any other grid-specific location information) + // into an array of segments (pieces of events whose format is decided by the grid). + spanToSegs: function(span) { // subclasses must implement }, @@ -3273,7 +3307,7 @@ var Grid = FC.Grid = Class.extend({ listenStop: function(ev) { if (dayClickHit) { view.triggerDayClick( - _this.getHitSpan(dayClickHit).start, + _this.getHitSpan(dayClickHit), _this.getHitEl(dayClickHit), ev ); @@ -3295,24 +3329,24 @@ var Grid = FC.Grid = Class.extend({ // TODO: should probably move this to Grid.events, like we did event dragging / resizing - // Renders a mock event over the given range - renderRangeHelper: function(range, sourceSeg) { - var fakeEvent = this.fabricateHelperEvent(range, sourceSeg); + // Renders a mock event at the given event location, which contains zoned start/end properties. + renderEventLocationHelper: function(eventLocation, sourceSeg) { + var fakeEvent = this.fabricateHelperEvent(eventLocation, sourceSeg); this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering }, - // Builds a fake event given a date range it should cover, and a segment is should be inspired from. + // Builds a fake event given zoned event date properties and a segment is should be inspired from. // The range's end can be null, in which case the mock event that is rendered will have a null end time. // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging. - fabricateHelperEvent: function(range, sourceSeg) { + fabricateHelperEvent: function(eventLocation, sourceSeg) { var fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible - fakeEvent.start = range.start.clone(); - fakeEvent.end = range.end ? range.end.clone() : null; - fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventRange - this.view.calendar.normalizeEventRange(fakeEvent); + fakeEvent.start = eventLocation.start.clone(); + fakeEvent.end = eventLocation.end ? eventLocation.end.clone() : null; + fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventDates + this.view.calendar.normalizeEventDates(fakeEvent); // this extra className will be useful for differentiating real events from mock events in CSS fakeEvent.className = (fakeEvent.className || []).concat('fc-helper'); @@ -3326,8 +3360,8 @@ var Grid = FC.Grid = Class.extend({ }, - // Renders a mock event - renderHelper: function(event, sourceSeg) { + // Renders a mock event. Given zoned event date properties. + renderHelper: function(eventLocation, sourceSeg) { // subclasses must implement }, @@ -3343,8 +3377,9 @@ var Grid = FC.Grid = Class.extend({ // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses. - renderSelection: function(range) { - this.renderHighlight(this.selectionRangeToSegs(range)); + // Given a span (unzoned start/end and other misc data) + renderSelection: function(span) { + this.renderHighlight(span); }, @@ -3359,22 +3394,24 @@ var Grid = FC.Grid = Class.extend({ // Will return false if the selection is invalid and this should be indicated to the user. // Will return null/undefined if a selection invalid but no error should be reported. computeSelection: function(span0, span1) { - var dates = [ span0.start, span0.end, span1.start, span1.end ]; - var combinedSpan; - - dates.sort(compareNumbers); // sorts chronologically. works with Moments - combinedSpan = { start: dates[0].clone(), end: dates[3].clone() }; + var span = this.computeSelectionSpan(span0, span1); - if (!this.view.calendar.isSelectionRangeAllowed(combinedSpan)) { + if (span && !this.view.calendar.isSelectionSpanAllowed(span)) { return false; } - return combinedSpan; + return span; }, - selectionRangeToSegs: function(range) { - return this.rangeToSegs(range); + // Given two spans, must return the combination of the two. + // TODO: do this separation of concerns (combining VS validation) for event dnd/resize too. + computeSelectionSpan: function(span0, span1) { + var dates = [ span0.start, span0.end, span1.start, span1.end ]; + + dates.sort(compareNumbers); // sorts chronologically. works with Moments + + return { start: dates[0].clone(), end: dates[3].clone() }; }, @@ -3382,9 +3419,9 @@ var Grid = FC.Grid = Class.extend({ ------------------------------------------------------------------------------------------------------------------*/ - // Renders an emphasis on the given date range. Given an array of segments. - renderHighlight: function(segs) { - this.renderFill('highlight', segs); + // Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data) + renderHighlight: function(span) { + this.renderFill('highlight', this.spanToSegs(span)); }, @@ -3400,10 +3437,40 @@ var Grid = FC.Grid = Class.extend({ }, - /* Fill System (highlight, background events, business hours) + /* Business Hours + ------------------------------------------------------------------------------------------------------------------*/ + + + renderBusinessHours: function() { + }, + + + unrenderBusinessHours: function() { + }, + + + /* Now Indicator ------------------------------------------------------------------------------------------------------------------*/ + getNowIndicatorUnit: function() { + }, + + + renderNowIndicator: function(date) { + }, + + + unrenderNowIndicator: function() { + }, + + + /* Fill System (highlight, background events, business hours) + -------------------------------------------------------------------------------------------------------------------- + TODO: remove this system. like we did in TimeGrid + */ + + // Renders a set of rectangles over the given segments of time. // MUST RETURN a subset of segs, the segs that were actually rendered. // Responsible for populating this.elsByFill. TODO: better API for expressing this requirement @@ -3496,7 +3563,7 @@ var Grid = FC.Grid = Class.extend({ // Computes HTML classNames for a single-day element getDayClasses: function(date) { var view = this.view; - var today = view.calendar.getNow().stripTime(); + var today = view.calendar.getNow(); var classes = [ 'fc-' + dayIDs[date.day()] ]; if ( @@ -3535,33 +3602,39 @@ Grid.mixin({ isDraggingSeg: false, // is a segment being dragged? boolean isResizingSeg: false, // is a segment being resized? boolean isDraggingExternal: false, // jqui-dragging an external element? boolean - segs: null, // the event segments currently rendered in the grid + segs: null, // the *event* segments currently rendered in the grid. TODO: rename to `eventSegs` // Renders the given events onto the grid renderEvents: function(events) { + var bgEvents = []; + var fgEvents = []; + var i; + + for (i = 0; i < events.length; i++) { + (isBgEvent(events[i]) ? bgEvents : fgEvents).push(events[i]); + } + + this.segs = [].concat( // record all segs + this.renderBgEvents(bgEvents), + this.renderFgEvents(fgEvents) + ); + }, + + + renderBgEvents: function(events) { var segs = this.eventsToSegs(events); - var bgSegs = []; - var fgSegs = []; - var i, seg; - for (i = 0; i < segs.length; i++) { - seg = segs[i]; + // renderBgSegs might return a subset of segs, segs that were actually rendered + return this.renderBgSegs(segs) || segs; + }, - if (isBgEvent(seg.event)) { - bgSegs.push(seg); - } - else { - fgSegs.push(seg); - } - } - // Render each different type of segment. - // Each function may return a subset of the segs, segs that were actually rendered. - bgSegs = this.renderBgSegs(bgSegs) || bgSegs; - fgSegs = this.renderFgSegs(fgSegs) || fgSegs; + renderFgEvents: function(events) { + var segs = this.eventsToSegs(events); - this.segs = bgSegs.concat(fgSegs); + // renderFgSegs might return a subset of segs, segs that were actually rendered + return this.renderFgSegs(segs) || segs; }, @@ -3676,20 +3749,9 @@ Grid.mixin({ // Generates a semicolon-separated CSS string to be used for the default rendering of a background event. // Called by the fill system. - // TODO: consolidate with getEventSkinCss? bgEventSegCss: function(seg) { - var view = this.view; - var event = seg.event; - var source = event.source || {}; - return { - 'background-color': - event.backgroundColor || - event.color || - source.backgroundColor || - source.color || - view.opt('eventBackgroundColor') || - view.opt('eventColor') + 'background-color': this.getSegSkinCss(seg)['background-color'] }; }, @@ -3778,7 +3840,7 @@ Grid.mixin({ var calendar = view.calendar; var el = seg.el; var event = seg.event; - var dropLocation; // a "span" (start/end possibly with additional properties) + var dropLocation; // zoned event date properties // A clone of the original element that will move with the mouse var mouseFollower = new MouseFollower(seg.el, { @@ -3818,7 +3880,7 @@ Grid.mixin({ event ); - if (dropLocation && !calendar.isEventRangeAllowed(dropLocation, event)) { + if (dropLocation && !calendar.isEventSpanAllowed(_this.eventToSpan(dropLocation), event)) { disableCursor(); dropLocation = null; } @@ -3878,15 +3940,16 @@ Grid.mixin({ }, - // Given the spans an event drag began, and the span event was dropped, calculates the new start/end/allDay + // Given the spans an event drag began, and the span event was dropped, calculates the new zoned start/end/allDay // values for the event. Subclasses may override and set additional properties to be used by renderDrag. // A falsy returned value indicates an invalid drop. + // DOES NOT consider overlap/constraint. computeEventDrop: function(startSpan, endSpan, event) { var calendar = this.view.calendar; var dragStart = startSpan.start; var dragEnd = endSpan.start; var delta; - var dropLocation; + var dropLocation; // zoned event date properties if (dragStart.hasTime() === dragEnd.hasTime()) { delta = this.diffDates(dragEnd, dragStart); @@ -3897,9 +3960,9 @@ Grid.mixin({ dropLocation = { start: event.start.clone(), end: calendar.getEventEnd(event), // will be an ambig day - allDay: false // for normalizeEventRangeTimes + allDay: false // for normalizeEventTimes }; - calendar.normalizeEventRangeTimes(dropLocation); + calendar.normalizeEventTimes(dropLocation); } // othewise, work off existing values else { @@ -3970,6 +4033,7 @@ Grid.mixin({ // Called when a jQuery UI drag starts and it needs to be monitored for dropping listenToExternalDrag: function(el, ev, ui) { var _this = this; + var calendar = this.view.calendar; var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create var dropLocation; // a null value signals an unsuccessful drag @@ -3983,22 +4047,27 @@ Grid.mixin({ hit.component.getHitSpan(hit), // since we are querying the parent view, might not belong to this grid meta ); + + if ( // invalid hit? + dropLocation && + !calendar.isExternalSpanAllowed(_this.eventToSpan(dropLocation), dropLocation, meta.eventProps) + ) { + disableCursor(); + dropLocation = null; + } + if (dropLocation) { _this.renderDrag(dropLocation); // called without a seg parameter } - else { // invalid hit - disableCursor(); - } }, hitOut: function() { dropLocation = null; // signal unsuccessful - _this.unrenderDrag(); + }, + hitDone: function() { // Called after a hitOut OR before a dragStop enableCursor(); + _this.unrenderDrag(); }, dragStop: function() { - _this.unrenderDrag(); - enableCursor(); - if (dropLocation) { // element was dropped on a valid hit _this.view.reportExternalDrop(meta, dropLocation, el, ev, ui); } @@ -4013,11 +4082,13 @@ Grid.mixin({ // Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object), - // returns start/end dates for the event that would result from the hypothetical drop. end might be null. + // returns the zoned start/end dates for the event that would result from the hypothetical drop. end might be null. // Returning a null value signals an invalid drop hit. + // DOES NOT consider overlap/constraint. computeExternalDrop: function(span, meta) { + var calendar = this.view.calendar; var dropLocation = { - start: span.start, + start: calendar.applyTimezone(span.start), // simulate a zoned event start date end: null }; @@ -4030,10 +4101,6 @@ Grid.mixin({ dropLocation.end = dropLocation.start.clone().add(meta.duration); } - if (!this.view.calendar.isExternalDropRangeAllowed(dropLocation, meta.eventProps)) { - return null; - } - return dropLocation; }, @@ -4071,7 +4138,7 @@ Grid.mixin({ var el = seg.el; var event = seg.event; var eventEnd = calendar.getEventEnd(event); - var resizeLocation; // falsy if invalid resize + var resizeLocation; // zoned event date properties. falsy if invalid resize // Tracks mouse movement over the *grid's* coordinate map var dragListener = new HitDragListener(this, { @@ -4091,7 +4158,7 @@ Grid.mixin({ _this.computeEventEndResize(origHitSpan, hitSpan, event); if (resizeLocation) { - if (!calendar.isEventRangeAllowed(resizeLocation, event)) { + if (!calendar.isEventSpanAllowed(_this.eventToSpan(resizeLocation), event)) { disableCursor(); resizeLocation = null; } @@ -4153,31 +4220,32 @@ Grid.mixin({ }, - // Returns new date-information for an event segment being resized from its start OR end - // `type` is either 'start' or 'end' + // Returns new zoned date information for an event segment being resized from its start OR end + // `type` is either 'start' or 'end'. + // DOES NOT consider overlap/constraint. computeEventResize: function(type, startSpan, endSpan, event) { var calendar = this.view.calendar; var delta = this.diffDates(endSpan[type], startSpan[type]); - var range; + var resizeLocation; // zoned event date properties var defaultDuration; // build original values to work from, guaranteeing a start and end - range = { + resizeLocation = { start: event.start.clone(), end: calendar.getEventEnd(event), allDay: event.allDay }; // if an all-day event was in a timed area and was resized to a time, adjust start/end to have times - if (range.allDay && durationHasTime(delta)) { - range.allDay = false; - calendar.normalizeEventRangeTimes(range); + if (resizeLocation.allDay && durationHasTime(delta)) { + resizeLocation.allDay = false; + calendar.normalizeEventTimes(resizeLocation); } - range[type].add(delta); // apply delta to start or end + resizeLocation[type].add(delta); // apply delta to start or end // if the event was compressed too small, find a new reasonable duration for it - if (!range.start.isBefore(range.end)) { + if (!resizeLocation.start.isBefore(resizeLocation.end)) { defaultDuration = this.minResizeDuration || // TODO: hack @@ -4186,14 +4254,14 @@ Grid.mixin({ calendar.defaultTimedEventDuration); if (type == 'start') { // resizing the start? - range.start = range.end.clone().subtract(defaultDuration); + resizeLocation.start = resizeLocation.end.clone().subtract(defaultDuration); } else { // resizing the end? - range.end = range.start.clone().add(defaultDuration); + resizeLocation.end = resizeLocation.start.clone().add(defaultDuration); } } - return range; + return resizeLocation; }, @@ -4266,7 +4334,8 @@ Grid.mixin({ // Utility for generating event skin-related CSS properties - getEventSkinCss: function(event) { + getSegSkinCss: function(seg) { + var event = seg.event; var view = this.view; var source = event.source || {}; var eventColor = event.color; @@ -4296,116 +4365,160 @@ Grid.mixin({ }, - /* Converting events -> ranges -> segs + /* Converting events -> eventRange -> eventSpan -> eventSegs ------------------------------------------------------------------------------------------------------------------*/ + // Generates an array of segments for the given single event + // Can accept an event "location" as well (which only has start/end and no allDay) + eventToSegs: function(event) { + return this.eventsToSegs([ event ]); + }, + + + eventToSpan: function(event) { + return this.eventToSpans(event)[0]; + }, + + + // Generates spans (always unzoned) for the given event. + // Does not do any inverting for inverse-background events. + // Can accept an event "location" as well (which only has start/end and no allDay) + eventToSpans: function(event) { + var range = this.eventToRange(event); + return this.eventRangeToSpans(range, event); + }, + + + // Converts an array of event objects into an array of event segment objects. - // A custom `rangeToSegsFunc` may be given for arbitrarily slicing up events. + // A custom `segSliceFunc` may be given for arbitrarily slicing up events. // Doesn't guarantee an order for the resulting array. - eventsToSegs: function(events, rangeToSegsFunc) { - var eventRanges = this.eventsToRanges(events); + eventsToSegs: function(allEvents, segSliceFunc) { + var _this = this; + var eventsById = groupEventsById(allEvents); + var segs = []; + + $.each(eventsById, function(id, events) { + var ranges = []; + var i; + + for (i = 0; i < events.length; i++) { + ranges.push(_this.eventToRange(events[i])); + } + + // inverse-background events (utilize only the first event in calculations) + if (isInverseBgEvent(events[0])) { + ranges = _this.invertRanges(ranges); + + for (i = 0; i < ranges.length; i++) { + segs.push.apply(segs, // append to + _this.eventRangeToSegs(ranges[i], events[0], segSliceFunc)); + } + } + // normal event ranges + else { + for (i = 0; i < ranges.length; i++) { + segs.push.apply(segs, // append to + _this.eventRangeToSegs(ranges[i], events[i], segSliceFunc)); + } + } + }); + + return segs; + }, + + + // Generates the unzoned start/end dates an event appears to occupy + // Can accept an event "location" as well (which only has start/end and no allDay) + eventToRange: function(event) { + return { + start: event.start.clone().stripZone(), + end: ( + event.end ? + event.end.clone() : + // derive the end from the start and allDay. compute allDay if necessary + this.view.calendar.getDefaultEventEnd( + event.allDay != null ? + event.allDay : + !event.start.hasTime(), + event.start + ) + ).stripZone() + }; + }, + + + // Given an event's range (unzoned start/end), and the event itself, + // slice into segments (using the segSliceFunc function if specified) + eventRangeToSegs: function(range, event, segSliceFunc) { + var spans = this.eventRangeToSpans(range, event); var segs = []; var i; - for (i = 0; i < eventRanges.length; i++) { - segs.push.apply( - segs, - this.eventRangeToSegs(eventRanges[i], rangeToSegsFunc) - ); + for (i = 0; i < spans.length; i++) { + segs.push.apply(segs, // append to + this.eventSpanToSegs(spans[i], event, segSliceFunc)); } return segs; }, - // Converts an array of events into an array of "range" objects. - // A "range" object is a plain object with start/end properties denoting the time it covers. Also an event property. - // For "normal" events, this will be identical to the event's start/end, but for "inverse-background" events, - // will create an array of ranges that span the time *not* covered by the given event. - // Doesn't guarantee an order for the resulting array. - eventsToRanges: function(events) { - var _this = this; - var eventsById = groupEventsById(events); - var ranges = []; - - // group by ID so that related inverse-background events can be rendered together - $.each(eventsById, function(id, eventGroup) { - if (eventGroup.length) { - ranges.push.apply( - ranges, - isInverseBgEvent(eventGroup[0]) ? - _this.eventsToInverseRanges(eventGroup) : - _this.eventsToNormalRanges(eventGroup) - ); - } - }); - - return ranges; + // Given an event's unzoned date range, return an array of "span" objects. + // Subclasses can override. + eventRangeToSpans: function(range, event) { + return [ $.extend({}, range) ]; // copy into a single-item array }, - // Converts an array of "normal" events (not inverted rendering) into a parallel array of ranges - eventsToNormalRanges: function(events) { - var calendar = this.view.calendar; - var ranges = []; - var i, event; - var eventStart, eventEnd; + // Given an event's span (unzoned start/end and other misc data), and the event itself, + // slices into segments and attaches event-derived properties to them. + eventSpanToSegs: function(span, event, segSliceFunc) { + var segs = segSliceFunc ? segSliceFunc(span) : this.spanToSegs(span); + var i, seg; - for (i = 0; i < events.length; i++) { - event = events[i]; - - // make copies and normalize by stripping timezone - eventStart = event.start.clone().stripZone(); - eventEnd = calendar.getEventEnd(event).stripZone(); - - ranges.push({ - event: event, - start: eventStart, - end: eventEnd, - eventStartMS: +eventStart, - eventDurationMS: eventEnd - eventStart - }); + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + seg.event = event; + seg.eventStartMS = +span.start; // TODO: not the best name after making spans unzoned + seg.eventDurationMS = span.end - span.start; } - return ranges; + return segs; }, - // Converts an array of events, with inverse-background rendering, into an array of range objects. - // The range objects will cover all the time NOT covered by the events. - eventsToInverseRanges: function(events) { + // Produces a new array of range objects that will cover all the time NOT covered by the given ranges. + // SIDE EFFECT: will mutate the given array and will use its date references. + invertRanges: function(ranges) { var view = this.view; - var viewStart = view.start.clone().stripZone(); // normalize timezone - var viewEnd = view.end.clone().stripZone(); // normalize timezone - var normalRanges = this.eventsToNormalRanges(events); // will give us normalized dates we can use w/o copies + var viewStart = view.start.clone(); // need a copy + var viewEnd = view.end.clone(); // need a copy var inverseRanges = []; - var event0 = events[0]; // assign this to each range's `.event` var start = viewStart; // the end of the previous range. the start of the new range - var i, normalRange; + var i, range; // ranges need to be in order. required for our date-walking algorithm - normalRanges.sort(compareNormalRanges); + ranges.sort(compareRanges); - for (i = 0; i < normalRanges.length; i++) { - normalRange = normalRanges[i]; + for (i = 0; i < ranges.length; i++) { + range = ranges[i]; // add the span of time before the event (if there is any) - if (normalRange.start > start) { // compare millisecond time (skip any ambig logic) + if (range.start > start) { // compare millisecond time (skip any ambig logic) inverseRanges.push({ - event: event0, start: start, - end: normalRange.start + end: range.start }); } - start = normalRange.end; + start = range.end; } // add the span of time after the last event (if there is any) if (start < viewEnd) { // compare millisecond time (skip any ambig logic) inverseRanges.push({ - event: event0, start: start, end: viewEnd }); @@ -4415,40 +4528,13 @@ Grid.mixin({ }, - // Slices the given event range into one or more segment objects. - // A `rangeToSegsFunc` custom slicing function can be given. - eventRangeToSegs: function(eventRange, rangeToSegsFunc) { - var segs; - var i, seg; - - eventRange = this.view.calendar.ensureVisibleEventRange(eventRange); - - if (rangeToSegsFunc) { - segs = rangeToSegsFunc(eventRange); - } - else { - segs = this.rangeToSegs(eventRange); // defined by the subclass - } - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - seg.event = eventRange.event; - seg.eventStartMS = eventRange.eventStartMS; - seg.eventDurationMS = eventRange.eventDurationMS; - } - - return segs; - }, - - - sortSegs: function(segs) { - segs.sort(proxy(this, 'compareSegs')); + sortEventSegs: function(segs) { + segs.sort(proxy(this, 'compareEventSegs')); }, // A cmp function for determining which segments should take visual priority - // DOES NOT WORK ON INVERTED BACKGROUND EVENTS because they have no eventStartMS/eventDurationMS - compareSegs: function(seg1, seg2) { + compareEventSegs: function(seg1, seg2) { return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1) @@ -4466,6 +4552,7 @@ function isBgEvent(event) { // returns true if background OR inverse-background var rendering = getEventRendering(event); return rendering === 'background' || rendering === 'inverse-background'; } +FC.isBgEvent = isBgEvent; // export function isInverseBgEvent(event) { @@ -4492,8 +4579,8 @@ function groupEventsById(events) { // A cmp function for determining which non-inverted "ranges" (see above) happen earlier -function compareNormalRanges(range1, range2) { - return range1.eventStartMS - range2.eventStartMS; // earlier ranges go first +function compareRanges(range1, range2) { + return range1.start - range2.start; // earlier ranges go first } @@ -4844,13 +4931,23 @@ var DayTableMixin = FC.DayTableMixin = { }, - renderHeadDateCellHtml: function(date, colspan) { + // TODO: when internalApiVersion, accept an object for HTML attributes + // (colspan should be no different) + renderHeadDateCellHtml: function(date, colspan, otherAttrs) { var view = this.view; return '' + ' 1 ? ' colspan="' + colspan + '"' : '') + - '>' + + (this.rowCnt == 1 ? + ' data-date="' + date.format('YYYY-MM-DD') + '"' : + '') + + (colspan > 1 ? + ' colspan="' + colspan + '"' : + '') + + (otherAttrs ? + ' ' + otherAttrs : + '') + + '>' + htmlEscape(date.format(this.colHeadFormat)) + ''; }, @@ -4888,7 +4985,7 @@ var DayTableMixin = FC.DayTableMixin = { }, - renderBgCellHtml: function(date) { + renderBgCellHtml: function(date, otherAttrs) { var view = this.view; var classes = this.getDayClasses(date); @@ -4896,6 +4993,9 @@ var DayTableMixin = FC.DayTableMixin = { return ''; }, @@ -4909,6 +5009,11 @@ var DayTableMixin = FC.DayTableMixin = { }, + // TODO: a generic method for dealing with , RTL, intro + // when increment internalApiVersion + // wrapTr (scheduler) + + /* Utils ------------------------------------------------------------------------------------------------------------------*/ @@ -5110,9 +5215,9 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, { }, - // Slices up a date range by row into an array of segments - rangeToSegs: function(range) { - var segs = this.sliceRangeByRow(range); + // Slices up the given span (unzoned start/end with other misc data) into an array of segments + spanToSegs: function(span) { + var segs = this.sliceRangeByRow(span); var i, seg; for (i = 0; i < segs.length; i++) { @@ -5197,16 +5302,16 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, { // Renders a visual indication of an event or external element being dragged. - // The dropLocation's end can be null. seg can be null. See Grid::renderDrag for more info. - renderDrag: function(dropLocation, seg) { + // `eventLocation` has zoned start and end (optional) + renderDrag: function(eventLocation, seg) { // always render a highlight underneath - this.renderHighlight(this.eventRangeToSegs(dropLocation)); + this.renderHighlight(this.eventToSpan(eventLocation)); // if a segment from the same calendar but another component is being dragged, render a helper event if (seg && !seg.el.closest(this.el).length) { - this.renderRangeHelper(dropLocation, seg); + this.renderEventLocationHelper(eventLocation, seg); this.applyDragOpacity(this.helperEls); return true; // a helper has been rendered @@ -5226,9 +5331,9 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, { // Renders a visual indication of an event being resized - renderEventResize: function(range, seg) { - this.renderHighlight(this.eventRangeToSegs(range)); - this.renderRangeHelper(range, seg); + renderEventResize: function(eventLocation, seg) { + this.renderHighlight(this.eventToSpan(eventLocation)); + this.renderEventLocationHelper(eventLocation, seg); }, @@ -5246,7 +5351,7 @@ var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, { // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null. renderHelper: function(event, sourceSeg) { var helperNodes = []; - var segs = this.eventsToSegs([ event ]); + var segs = this.eventToSegs(event); var rowStructs; segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered @@ -5453,7 +5558,7 @@ DayGrid.mixin({ var isResizableFromEnd = !disableResizing && event.allDay && seg.isEnd && view.isEventResizableFromEnd(event); var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd); - var skinCss = cssToStr(this.getEventSkinCss(event)); + var skinCss = cssToStr(this.getSegSkinCss(seg)); var timeHtml = ''; var timeText; var titleHtml; @@ -5600,7 +5705,7 @@ DayGrid.mixin({ // Give preference to elements with certain criteria, so they have // a chance to be closer to the top. - this.sortSegs(segs); + this.sortEventSegs(segs); for (i = 0; i < segs.length; i++) { seg = segs[i]; @@ -5989,7 +6094,7 @@ DayGrid.mixin({ return seg.event; }); - var dayStart = dayDate.clone().stripTime(); + var dayStart = dayDate.clone(); var dayEnd = dayStart.clone().add(1, 'days'); var dayRange = { start: dayStart, end: dayEnd }; @@ -5997,13 +6102,13 @@ DayGrid.mixin({ segs = this.eventsToSegs( events, function(range) { - var seg = intersectionToSeg(range, dayRange); // undefind if no intersection + var seg = intersectRanges(range, dayRange); // undefind if no intersection return seg ? [ seg ] : []; // must return an array of segments } ); // force an order because eventsToSegs doesn't guarantee one - this.sortSegs(segs); + this.sortEventSegs(segs); return segs; }, @@ -6061,13 +6166,11 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, { colEls: null, // cells elements in the day-row background slatEls: null, // elements running horizontally across all columns - helperEl: null, // cell skeleton element for rendering the mock event "helper" + nowIndicatorEls: null, colCoordCache: null, slatCoordCache: null, - businessHourSegs: null, - constructor: function() { Grid.apply(this, arguments); // call the super-constructor @@ -6091,12 +6194,8 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, { els: this.slatEls, isVertical: true }); - }, - - renderBusinessHours: function() { - var events = this.view.calendar.getBusinessHoursEvents(); - this.businessHourSegs = this.renderFill('businessHours', this.eventsToSegs(events), 'bgevent'); + this.renderContentSkeleton(); }, @@ -6128,7 +6227,7 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, { // Calculate the time for each slot while (slotTime < this.maxTime) { - slotDate = this.start.clone().time(slotTime); // after .time() will be in UTC. but that's good, avoids DST issues + slotDate = this.start.clone().time(slotTime); isLabeled = isInt(divideDurationByDuration(slotTime, this.labelInterval)); axisHtml = @@ -6142,7 +6241,9 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, { ''; html += - '' + + '' + (!isRTL ? axisHtml : '') + '' + (isRTL ? axisHtml : '') + @@ -6274,9 +6375,8 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, { getHitSpan: function(hit) { - var date = this.getCellDate(0, hit.col); // row=0 + var start = this.getCellDate(0, hit.col); // row=0 var time = this.computeSnapTime(hit.snap); // pass in the snap-index - var start = this.view.calendar.rezoneDate(date); // gives it a 00:00 time var end; start.time(time); @@ -6306,9 +6406,9 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, { }, - // Slices up a date range by column into an array of segments - rangeToSegs: function(range) { - var segs = this.sliceRangeByTimes(range); + // Slices up the given span (unzoned start/end with other misc data) into an array of segments + spanToSegs: function(span) { + var segs = this.sliceRangeByTimes(span); var i; for (i = 0; i < segs.length; i++) { @@ -6331,19 +6431,13 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, { var dayDate; var dayRange; - // normalize :( - range = { - start: range.start.clone().stripZone(), - end: range.end.clone().stripZone() - }; - for (dayIndex = 0; dayIndex < this.daysPerRow; dayIndex++) { dayDate = this.dayDates[dayIndex].clone(); // TODO: better API for this? dayRange = { start: dayDate.clone().time(this.minTime), end: dayDate.clone().time(this.maxTime) }; - seg = intersectionToSeg(range, dayRange); // both will be ambig timezone + seg = intersectRanges(range, dayRange); // both will be ambig timezone if (seg) { seg.dayIndex = dayIndex; segs.push(seg); @@ -6362,7 +6456,9 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, { this.slatCoordCache.build(); if (isResize) { - this.updateSegVerticals(); + this.updateSegVerticals( + [].concat(this.fgSegs || [], this.bgSegs || [], this.businessSegs || []) + ); } }, @@ -6372,7 +6468,7 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, { computeDateTop: function(date, startOfDayDate) { return this.computeTimeTop( moment.duration( - date.clone().stripZone() - startOfDayDate.clone().stripTime() + date - startOfDayDate.clone().stripTime() ) ); }, @@ -6411,19 +6507,21 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, { // 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) { + renderDrag: function(eventLocation, seg) { if (seg) { // if there is event information for this drag, render a helper event - this.renderRangeHelper(dropLocation, seg); - this.applyDragOpacity(this.helperEl); + this.renderEventLocationHelper(eventLocation, seg); + + for (var i = 0; i < this.helperSegs.length; i++) { + this.applyDragOpacity(this.helperSegs[i].el); + } return true; // signal that a helper has been rendered } else { // otherwise, just render a highlight - this.renderHighlight(this.eventRangeToSegs(dropLocation)); + this.renderHighlight(this.eventToSpan(eventLocation)); } }, @@ -6440,8 +6538,8 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, { // Renders a visual indication of an event being resized - renderEventResize: function(range, seg) { - this.renderRangeHelper(range, seg); + renderEventResize: function(eventLocation, seg) { + this.renderEventLocationHelper(eventLocation, seg); }, @@ -6457,39 +6555,72 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, { // 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; + this.renderHelperSegs(this.eventToSegs(event), sourceSeg); + }, - 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 + // Unrenders any mock helper event + unrenderHelper: function() { + this.unrenderHelperSegs(); + }, + + + /* Business Hours + ------------------------------------------------------------------------------------------------------------------*/ + + + renderBusinessHours: function() { + var events = this.view.calendar.getBusinessHoursEvents(); + var segs = this.eventsToSegs(events); + + this.renderBusinessSegs(segs); + }, + + + unrenderBusinessHours: function() { + this.unrenderBusinessSegs(); + }, + + + /* Now Indicator + ------------------------------------------------------------------------------------------------------------------*/ + + + getNowIndicatorUnit: function() { + return 'minute'; // will refresh on the minute + }, + + + renderNowIndicator: function(date) { + // seg system might be overkill, but it handles scenario where line needs to be rendered + // more than once because of columns with the same date (resources columns for example) + var segs = this.spanToSegs({ start: date, end: date }); + var top = this.computeDateTop(date, date); + var nodes = []; + var i; + + // render lines within the columns 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') - }); - } + nodes.push($('
') + .css('top', top) + .appendTo(this.colContainerEls.eq(segs[i].col))[0]); + } + + // render an arrow over the axis + if (segs.length > 0) { // is the current time in view? + nodes.push($('
') + .css('top', top) + .appendTo(this.el.find('.fc-content-skeleton'))[0]); } - this.helperEl = $('
') - .append(tableEl) - .appendTo(this.el); + this.nowIndicatorEls = $(nodes); }, - // Unrenders any mock helper event - unrenderHelper: function() { - if (this.helperEl) { - this.helperEl.remove(); - this.helperEl = null; + unrenderNowIndicator: function() { + if (this.nowIndicatorEls) { + this.nowIndicatorEls.remove(); + this.nowIndicatorEls = null; } }, @@ -6499,12 +6630,14 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, { // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight. - renderSelection: function(range) { + renderSelection: function(span) { if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered - this.renderRangeHelper(range); + + // normally acceps an eventLocation, span has a start/end, which is good enough + this.renderEventLocationHelper(span); } else { - this.renderHighlight(this.selectionRangeToSegs(range)); + this.renderHighlight(span); } }, @@ -6516,233 +6649,260 @@ var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, { }, - /* Fill System (highlight, background events, business hours) + /* Highlight ------------------------------------------------------------------------------------------------------------------*/ - // Renders a set of rectangles over the given time segments. - // Only returns segments that successfully rendered. - renderFill: function(type, segs, className) { - var segCols; - var skeletonEl; - var trEl; - var col, colSegs; - var tdEl; - var containerEl; - var dayDate; - var i, seg; - - if (segs.length) { - - segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs - segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg - - className = className || type.toLowerCase(); - skeletonEl = $( - '
' + - '
' + - '
' - ); - trEl = skeletonEl.find('tr'); - - for (col = 0; col < segCols.length; col++) { - colSegs = segCols[col]; - tdEl = $('').appendTo(trEl); - - if (colSegs.length) { - containerEl = $('
').appendTo(tdEl); - dayDate = this.getCellDate(0, col); // row=0 - - for (i = 0; i < colSegs.length; i++) { - seg = colSegs[i]; - containerEl.append( - seg.el.css({ - top: this.computeDateTop(seg.start, dayDate), - bottom: -this.computeDateTop(seg.end, dayDate) // the y position of the bottom edge - }) - ); - } - } - } - - this.bookendCells(trEl); + renderHighlight: function(span) { + this.renderHighlightSegs(this.spanToSegs(span)); + }, - this.el.append(skeletonEl); - this.elsByFill[type] = skeletonEl; - } - return segs; + unrenderHighlight: function() { + this.unrenderHighlightSegs(); } }); ;; -/* Event-rendering methods for the TimeGrid class +/* Methods for rendering SEGMENTS, pieces of content that live on the view + ( this file is no longer just for events ) ----------------------------------------------------------------------------------------------------------------------*/ TimeGrid.mixin({ - eventSkeletonEl: null, // has cells with event-containers, which contain absolutely positioned event elements - + colContainerEls: null, // containers for each column - // Renders the given foreground event segments onto the grid - renderFgSegs: function(segs) { - segs = this.renderFgSegEls(segs); // returns a subset of the segs. segs that were actually rendered + // inner-containers for each column where different types of segs live + fgContainerEls: null, + bgContainerEls: null, + helperContainerEls: null, + highlightContainerEls: null, + businessContainerEls: null, - this.el.append( - this.eventSkeletonEl = $('
') - .append(this.renderSegTable(segs)) - ); + // arrays of different types of displayed segments + fgSegs: null, + bgSegs: null, + helperSegs: null, + highlightSegs: null, + businessSegs: null, - return segs; // return only the segs that were actually rendered - }, + // Renders the DOM that the view's content will live in + renderContentSkeleton: function() { + var cellHtml = ''; + var i; + var skeletonEl; - // Unrenders all currently rendered foreground event segments - unrenderFgSegs: function(segs) { - if (this.eventSkeletonEl) { - this.eventSkeletonEl.remove(); - this.eventSkeletonEl = null; + for (i = 0; i < this.colCnt; i++) { + cellHtml += + '' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + ''; } - }, - - - // Renders and returns the portion of the event-skeleton. - // Returns an object with properties 'tbodyEl' and 'segs'. - renderSegTable: function(segs) { - var tableEl = $('
'); - var trEl = tableEl.find('tr'); - var segCols; - var i, seg; - var col, colSegs; - var containerEl; - segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg - - this.computeSegVerticals(segs); // compute and assign top/bottom + skeletonEl = $( + '
' + + '' + + '' + cellHtml + '' + + '
' + + '
' + ); - for (col = 0; col < segCols.length; col++) { // iterate each column grouping - colSegs = segCols[col]; - this.placeSlotSegs(colSegs); // compute horizontal coordinates, z-index's, and reorder the array + this.colContainerEls = skeletonEl.find('.fc-content-col'); + this.helperContainerEls = skeletonEl.find('.fc-helper-container'); + this.fgContainerEls = skeletonEl.find('.fc-event-container:not(.fc-helper-container)'); + this.bgContainerEls = skeletonEl.find('.fc-bgevent-container'); + this.highlightContainerEls = skeletonEl.find('.fc-highlight-container'); + this.businessContainerEls = skeletonEl.find('.fc-business-container'); - containerEl = $('
'); + this.bookendCells(skeletonEl.find('tr')); // TODO: do this on string level + this.el.append(skeletonEl); + }, - // assign positioning CSS and insert into container - for (i = 0; i < colSegs.length; i++) { - seg = colSegs[i]; - seg.el.css(this.generateSegPositionCss(seg)); - // if the height is short, add a className for alternate styling - if (seg.bottom - seg.top < 30) { - seg.el.addClass('fc-short'); - } + /* Foreground Events + ------------------------------------------------------------------------------------------------------------------*/ - containerEl.append(seg.el); - } - trEl.append($('').append(containerEl)); - } + renderFgSegs: function(segs) { + segs = this.renderFgSegsIntoContainers(segs, this.fgContainerEls); + this.fgSegs = segs; + return segs; // needed for Grid::renderEvents + }, - this.bookendCells(trEl); - return tableEl; + unrenderFgSegs: function() { + this.unrenderNamedSegs('fgSegs'); }, - // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each. - // NOTE: Also reorders the given array by date! - placeSlotSegs: function(segs) { - var levels; - var level0; - var i; + /* Foreground Helper Events + ------------------------------------------------------------------------------------------------------------------*/ - this.sortSegs(segs); // order by date - levels = buildSlotSegLevels(segs); - computeForwardSlotSegs(levels); - if ((level0 = levels[0])) { + renderHelperSegs: function(segs, sourceSeg) { + var i, seg; + var sourceEl; - for (i = 0; i < level0.length; i++) { - computeSlotSegPressures(level0[i]); - } + segs = this.renderFgSegsIntoContainers(segs, this.helperContainerEls); - for (i = 0; i < level0.length; i++) { - this.computeSlotSegCoords(level0[i], 0, 0); + // 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.helperSegs = segs; }, - // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range - // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and - // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left. - // - // The segment might be part of a "series", which means consecutive segments with the same pressure - // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of - // segments behind this one in the current series, and `seriesBackwardCoord` is the starting - // coordinate of the first segment in the series. - computeSlotSegCoords: function(seg, seriesBackwardPressure, seriesBackwardCoord) { - var forwardSegs = seg.forwardSegs; + unrenderHelperSegs: function() { + this.unrenderNamedSegs('helperSegs'); + }, + + + /* Background Events + ------------------------------------------------------------------------------------------------------------------*/ + + + renderBgSegs: function(segs) { + segs = this.renderFillSegEls('bgEvent', segs); // TODO: old fill system + this.updateSegVerticals(segs); + this.attachSegsByCol(this.groupSegsByCol(segs), this.bgContainerEls); + this.bgSegs = segs; + return segs; // needed for Grid::renderEvents + }, + + + unrenderBgSegs: function() { + this.unrenderNamedSegs('bgSegs'); + }, + + + /* Highlight + ------------------------------------------------------------------------------------------------------------------*/ + + + renderHighlightSegs: function(segs) { + segs = this.renderFillSegEls('highlight', segs); // TODO: old fill system + this.updateSegVerticals(segs); + this.attachSegsByCol(this.groupSegsByCol(segs), this.highlightContainerEls); + this.highlightSegs = segs; + }, + + + unrenderHighlightSegs: function() { + this.unrenderNamedSegs('highlightSegs'); + }, + + + /* Business Hours + ------------------------------------------------------------------------------------------------------------------*/ + + + renderBusinessSegs: function(segs) { + segs = this.renderFillSegEls('businessHours', segs); // TODO: old fill system + this.updateSegVerticals(segs); + this.attachSegsByCol(this.groupSegsByCol(segs), this.businessContainerEls); + this.businessSegs = segs; + }, + + + unrenderBusinessSegs: function() { + this.unrenderNamedSegs('businessSegs'); + }, + + + /* Seg Rendering Utils + ------------------------------------------------------------------------------------------------------------------*/ + + + // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col + groupSegsByCol: function(segs) { + var segsByCol = []; var i; - if (seg.forwardCoord === undefined) { // not already computed + for (i = 0; i < this.colCnt; i++) { + segsByCol.push([]); + } - if (!forwardSegs.length) { + for (i = 0; i < segs.length; i++) { + segsByCol[segs[i].col].push(segs[i]); + } - // if there are no forward segments, this segment should butt up against the edge - seg.forwardCoord = 1; - } - else { + return segsByCol; + }, - // sort highest pressure first - this.sortForwardSlotSegs(forwardSegs); - // this segment's forwardCoord will be calculated from the backwardCoord of the - // highest-pressure forward segment. - this.computeSlotSegCoords(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord); - seg.forwardCoord = forwardSegs[0].backwardCoord; - } + // Given segments grouped by column, insert the segments' elements into a parallel array of container + // elements, each living within a column. + attachSegsByCol: function(segsByCol, containerEls) { + var col; + var segs; + var i; - // calculate the backwardCoord from the forwardCoord. consider the series - seg.backwardCoord = seg.forwardCoord - - (seg.forwardCoord - seriesBackwardCoord) / // available width for series - (seriesBackwardPressure + 1); // # of segments in the series + for (col = 0; col < this.colCnt; col++) { // iterate each column grouping + segs = segsByCol[col]; - // use this segment's coordinates to computed the coordinates of the less-pressurized - // forward segments - for (i=0; i rangeEnd.clone().stripZone(); + start < rangeStart || end > rangeEnd; // is part of the new range outside of the old range? } @@ -9932,7 +10362,7 @@ function EventManager(options) { // assumed to be a calendar source ? source.allDayDefault : undefined, options.allDayDefault ); - // still undefined? normalizeEventRange will calculate it + // still undefined? normalizeEventDates will calculate it } assignDatesToEvent(start, end, allDay, out); @@ -9948,76 +10378,56 @@ function EventManager(options) { // assumed to be a calendar event.start = start; event.end = end; event.allDay = allDay; - normalizeEventRange(event); + normalizeEventDates(event); backupEventDates(event); } // Ensures proper values for allDay/start/end. Accepts an Event object, or a plain object with event-ish properties. // NOTE: Will modify the given object. - function normalizeEventRange(props) { + function normalizeEventDates(eventProps) { - normalizeEventRangeTimes(props); + normalizeEventTimes(eventProps); - if (props.end && !props.end.isAfter(props.start)) { - props.end = null; + if (eventProps.end && !eventProps.end.isAfter(eventProps.start)) { + eventProps.end = null; } - if (!props.end) { + if (!eventProps.end) { if (options.forceEventDuration) { - props.end = t.getDefaultEventEnd(props.allDay, props.start); + eventProps.end = t.getDefaultEventEnd(eventProps.allDay, eventProps.start); } else { - props.end = null; + eventProps.end = null; } } } // Ensures the allDay property exists and the timeliness of the start/end dates are consistent - function normalizeEventRangeTimes(range) { - if (range.allDay == null) { - range.allDay = !(range.start.hasTime() || (range.end && range.end.hasTime())); + function normalizeEventTimes(eventProps) { + if (eventProps.allDay == null) { + eventProps.allDay = !(eventProps.start.hasTime() || (eventProps.end && eventProps.end.hasTime())); } - if (range.allDay) { - range.start.stripTime(); - if (range.end) { + if (eventProps.allDay) { + eventProps.start.stripTime(); + if (eventProps.end) { // TODO: consider nextDayThreshold here? If so, will require a lot of testing and adjustment - range.end.stripTime(); + eventProps.end.stripTime(); } } else { - if (!range.start.hasTime()) { - range.start = t.rezoneDate(range.start); // will assign a 00:00 time + if (!eventProps.start.hasTime()) { + eventProps.start = t.applyTimezone(eventProps.start.time(0)); // will assign a 00:00 time } - if (range.end && !range.end.hasTime()) { - range.end = t.rezoneDate(range.end); // will assign a 00:00 time + if (eventProps.end && !eventProps.end.hasTime()) { + eventProps.end = t.applyTimezone(eventProps.end.time(0)); // will assign a 00:00 time } } } - // If `range` is a proper range with a start and end, returns the original object. - // If missing an end, computes a new range with an end, computing it as if it were an event. - // TODO: make this a part of the event -> eventRange system - function ensureVisibleEventRange(range) { - var allDay; - - if (!range.end) { - - allDay = range.allDay; // range might be more event-ish than we think - if (allDay == null) { - allDay = !range.start.hasTime(); - } - - range = $.extend({}, range); // make a copy, copying over other misc properties - range.end = t.getDefaultEventEnd(allDay, range.start); - } - return range; - } - - // If the given event is a recurring event, break it down into an array of individual instances. // If not a recurring event, return an array with the single original event. // If given a falsy input (probably because of a failed buildEventFromInput call), returns an empty array. @@ -10131,7 +10541,7 @@ function EventManager(options) { // assumed to be a calendar if (newProps.allDay == null) { // is null or undefined? newProps.allDay = event.allDay; } - normalizeEventRange(newProps); + normalizeEventDates(newProps); // create normalized versions of the original props to compare against // need a real end value, for diffing @@ -10140,7 +10550,7 @@ function EventManager(options) { // assumed to be a calendar end: event._end ? event._end.clone() : t.getDefaultEventEnd(event._allDay, event._start), allDay: newProps.allDay // normalize the dates in the same regard as the new properties }; - normalizeEventRange(oldProps); + normalizeEventDates(oldProps); // need to clear the end date if explicitly changed to null clearEnd = event._end !== null && newProps.end === null; @@ -10225,7 +10635,7 @@ function EventManager(options) { // assumed to be a calendar end: event._end, allDay: allDay // normalize the dates in the same regard as the new properties }; - normalizeEventRange(newProps); // massages start/end/allDay + normalizeEventDates(newProps); // massages start/end/allDay // strip or ensure the end date if (clearEnd) { @@ -10326,12 +10736,13 @@ function EventManager(options) { // assumed to be a calendar /* Overlapping / Constraining -----------------------------------------------------------------------------------------*/ - t.isEventRangeAllowed = isEventRangeAllowed; - t.isSelectionRangeAllowed = isSelectionRangeAllowed; - t.isExternalDropRangeAllowed = isExternalDropRangeAllowed; + t.isEventSpanAllowed = isEventSpanAllowed; + t.isExternalSpanAllowed = isExternalSpanAllowed; + t.isSelectionSpanAllowed = isSelectionSpanAllowed; - function isEventRangeAllowed(range, event) { + // Determines if the given event can be relocated to the given span (unzoned start/end with other misc data) + function isEventSpanAllowed(span, event) { var source = event.source || {}; var constraint = firstDefined( event.constraint, @@ -10343,57 +10754,47 @@ function EventManager(options) { // assumed to be a calendar source.overlap, options.eventOverlap ); - - range = ensureVisibleEventRange(range); // ensure a proper range with an end for isRangeAllowed - - return isRangeAllowed(range, constraint, overlap, event); - } - - - function isSelectionRangeAllowed(range) { - return isRangeAllowed(range, options.selectConstraint, options.selectOverlap); + return isSpanAllowed(span, constraint, overlap, event); } - // when `eventProps` is defined, consider this an event. - // `eventProps` can contain misc non-date-related info about the event. - function isExternalDropRangeAllowed(range, eventProps) { + // Determines if an external event can be relocated to the given span (unzoned start/end with other misc data) + function isExternalSpanAllowed(eventSpan, eventLocation, eventProps) { var eventInput; var event; // note: very similar logic is in View's reportExternalDrop if (eventProps) { - eventInput = $.extend({}, eventProps, range); + eventInput = $.extend({}, eventProps, eventLocation); event = expandEvent(buildEventFromInput(eventInput))[0]; } if (event) { - return isEventRangeAllowed(range, event); + return isEventSpanAllowed(eventSpan, event); } else { // treat it as a selection - range = ensureVisibleEventRange(range); // ensure a proper range with an end for isSelectionRangeAllowed - - return isSelectionRangeAllowed(range); + return isSelectionSpanAllowed(eventSpan); } } - // Returns true if the given range (caused by an event drop/resize or a selection) is allowed to exist + // Determines the given span (unzoned start/end with other misc data) can be selected. + function isSelectionSpanAllowed(span) { + return isSpanAllowed(span, options.selectConstraint, options.selectOverlap); + } + + + // Returns true if the given span (caused by an event drop/resize or a selection) is allowed to exist // according to the constraint/overlap settings. // `event` is not required if checking a selection. - function isRangeAllowed(range, constraint, overlap, event) { + function isSpanAllowed(span, constraint, overlap, event) { var constraintEvents; var anyContainment; var peerEvents; var i, peerEvent; var peerOverlap; - // normalize. fyi, we're normalizing in too many places :( - range = $.extend({}, range); // copy all properties in case there are misc non-date properties - range.start = range.start.clone().stripZone(); - range.end = range.end.clone().stripZone(); - // the range must be fully contained by at least one of produced constraint events if (constraint != null) { @@ -10403,7 +10804,7 @@ function EventManager(options) { // assumed to be a calendar anyContainment = false; for (i = 0; i < constraintEvents.length; i++) { - if (eventContainsRange(constraintEvents[i], range)) { + if (eventContainsRange(constraintEvents[i], span)) { anyContainment = true; break; } @@ -10414,13 +10815,13 @@ function EventManager(options) { // assumed to be a calendar } } - peerEvents = t.getPeerEvents(event, range); + peerEvents = t.getPeerEvents(span, event); for (i = 0; i < peerEvents.length; i++) { peerEvent = peerEvents[i]; // there needs to be an actual intersection before disallowing anything - if (eventIntersectsRange(peerEvent, range)) { + if (eventIntersectsRange(peerEvent, span)) { // evaluate overlap for the given range and short-circuit if necessary if (overlap === false) { @@ -10500,8 +10901,8 @@ function EventManager(options) { // assumed to be a calendar // Returns a list of events that the given event should be compared against when being considered for a move to -// the specified range. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar. -Calendar.prototype.getPeerEvents = function(event, range) { +// the specified span. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar. +Calendar.prototype.getPeerEvents = function(span, event) { var cache = this.getEventCache(); var peerEvents = []; var i, otherEvent; @@ -10810,8 +11211,8 @@ var BasicView = FC.BasicView = View.extend({ // Renders a visual indication of a selection - renderSelection: function(range) { - this.dayGrid.renderSelection(range); + renderSelection: function(span) { + this.dayGrid.renderSelection(span); }, @@ -11072,15 +11473,6 @@ var AgendaView = FC.AgendaView = View.extend({ }, - renderBusinessHours: function() { - this.timeGrid.renderBusinessHours(); - - if (this.dayGrid) { - this.dayGrid.renderBusinessHours(); - } - }, - - // Builds the HTML skeleton for the view. // The day-grid and time-grid components will render inside containers defined by this HTML. renderSkeletonHtml: function() { @@ -11118,6 +11510,47 @@ var AgendaView = FC.AgendaView = View.extend({ }, + /* Business Hours + ------------------------------------------------------------------------------------------------------------------*/ + + + renderBusinessHours: function() { + this.timeGrid.renderBusinessHours(); + + if (this.dayGrid) { + this.dayGrid.renderBusinessHours(); + } + }, + + + unrenderBusinessHours: function() { + this.timeGrid.unrenderBusinessHours(); + + if (this.dayGrid) { + this.dayGrid.unrenderBusinessHours(); + } + }, + + + /* Now Indicator + ------------------------------------------------------------------------------------------------------------------*/ + + + getNowIndicatorUnit: function() { + return this.timeGrid.getNowIndicatorUnit(); + }, + + + renderNowIndicator: function(date) { + this.timeGrid.renderNowIndicator(date); + }, + + + unrenderNowIndicator: function() { + this.timeGrid.unrenderNowIndicator(); + }, + + /* Dimensions ------------------------------------------------------------------------------------------------------------------*/ @@ -11331,12 +11764,12 @@ var AgendaView = FC.AgendaView = View.extend({ // Renders a visual indication of a selection - renderSelection: function(range) { - if (range.start.hasTime() || range.end.hasTime()) { - this.timeGrid.renderSelection(range); + renderSelection: function(span) { + if (span.start.hasTime() || span.end.hasTime()) { + this.timeGrid.renderSelection(span); } else if (this.dayGrid) { - this.dayGrid.renderSelection(range); + this.dayGrid.renderSelection(span); } }, @@ -11353,6 +11786,7 @@ var AgendaView = FC.AgendaView = View.extend({ // Methods that will customize the rendering behavior of the AgendaView's timeGrid +// TODO: move into TimeGrid var agendaTimeGridMethods = { -- cgit v1.2.3