Ver código fonte

popover when user clicks on "more" link

Adam Shaw 11 anos atrás
pai
commit
bba2d6fcad

+ 1 - 0
lumbar.json

@@ -22,6 +22,7 @@
         "src/util.js",
         "src/moment-ext.js",
         "src/date-formatting.js",
+        "src/common/Popover.js",
         "src/common/CoordMap.js",
         "src/common/DragListener.js",
         "src/common/MouseFollower.js",

+ 19 - 12
src/Calendar.js

@@ -52,7 +52,7 @@ function Calendar(element, instanceOptions) {
 	t.today = today;
 	t.gotoDate = gotoDate;
 	t.incrementDate = incrementDate;
-	t.zoomToDay = zoomToDay;
+	t.zoomTo = zoomTo;
 	t.getDate = getDate;
 	t.getCalendar = getCalendar;
 	t.getView = getView;
@@ -424,6 +424,7 @@ function Calendar(element, instanceOptions) {
 			}
 
 			ignoreWindowResize++;
+			currentView.recordScroll(); // for keeping the scroll value (if any) before refreshing dimensions
 			currentView.updateHeight(); // will poll getSuggestedViewHeight() and isHeightAuto()
 			currentView.updateWidth();
 			ignoreWindowResize--;
@@ -625,19 +626,25 @@ function Calendar(element, instanceOptions) {
 	}
 
 
-	// Forces navigation to a day-view on the given date. `viewName` is the name of an explicit view to go to.
-	// If not specified, or 'auto', it will guess the best view based on which buttons are in the header toolbar.
-	function zoomToDay(newDate, viewName) {
-		var viewsWithButtons;
+	// Forces navigation to a view for the given date.
+	// `viewName` can be a specific view name or a generic one like "week" or "day".
+	function zoomTo(newDate, viewName) {
+		var viewStr;
+		var match;
 
-		if (fcViews[viewName] === undefined) { // not an available view, or 'auto'
-			viewsWithButtons = header.getViewsWithButtons();
-			if ($.inArray('basicDay', viewsWithButtons) !== -1) {
-				viewName = 'basicDay';
-			}
-			else {
-				viewName = 'agendaDay'; // the fallback
+		if (!viewName || fcViews[viewName] === undefined) { // a general view name, or "auto"
+			viewName = viewName || 'day';
+			viewStr = header.getViewsWithButtons().join(' '); // space-separated string of all the views in the header
+
+			// try to match a general view name, like "week", against a specific one, like "agendaWeek"
+			match = viewStr.match(new RegExp('\\w+' + capitaliseFirstLetter(viewName)));
+
+			// fall back to the day view being used in the header
+			if (!match) {
+				match = viewStr.match(/\w+Day/);
 			}
+
+			viewName = match ? match[0] : 'agendaDay'; // fall back to agendaDay
 		}
 
 		date = newDate;

+ 19 - 3
src/agenda/AgendaView.js

@@ -115,6 +115,16 @@ $.extend(AgendaView.prototype, {
 	},
 
 
+	// Make subcomponents ready for cleanup
+	destroy: function() {
+		this.timeGrid.destroy();
+		if (this.dayGrid) {
+			this.dayGrid.destroy();
+		}
+		View.prototype.destroy.call(this); // call the super-method
+	},
+
+
 	// Builds the HTML skeleton for the view.
 	// The day-grid and time-grid components will render inside containers defined by this HTML.
 	renderHtml: function() {
@@ -317,12 +327,18 @@ $.extend(AgendaView.prototype, {
 		// the all-day area is flexible and might have a lot of events, so shift the height
 		this.updateHeight();
 
-		this.segs = daySegs.concat(timedSegs); // needed by the View super-class
-
 		View.prototype.renderEvents.call(this, events); // call the super-method
 	},
 
 
+	// Retrieves all segment objects that are rendered in the view
+	getSegs: function() {
+		return this.timeGrid.getSegs().concat(
+			this.dayGrid ? this.dayGrid.getSegs() : []
+		);
+	},
+
+
 	// Unrenders all event elements and clears internal segment data
 	destroyEvents: function() {
 
@@ -340,7 +356,7 @@ $.extend(AgendaView.prototype, {
 		// A) a renderEvents() call always happens after this, which will eventually call updateHeight()
 		// B) in IE8, this causes a flash whenever events are rerendered
 
-		View.prototype.destroyEvents.call(this); // call the super-method. will kill `this.segs`
+		View.prototype.destroyEvents.call(this); // call the super-method
 	},
 
 

+ 6 - 9
src/agenda/agenda.css

@@ -126,9 +126,12 @@
 
 
 /* TimeGrid Event Styling
---------------------------------------------------------------------------------------------------*/
+----------------------------------------------------------------------------------------------------
+We use the full "fc-time-grid-event" class instead of using descendants because the event won't
+be a descendant of the grid when it is being dragged.
+*/
 
-.fc-time-grid .fc-event.fc-not-start { /* events that are continuing from another day */
+.fc-time-grid-event.fc-not-start { /* events that are continuing from another day */
 	/* replace space made by the top border with padding */
 	border-top-width: 0;
 	padding-top: 1px;
@@ -138,7 +141,7 @@
 	border-top-right-radius: 0;
 }
 
-.fc-time-grid .fc-event.fc-not-end {
+.fc-time-grid-event.fc-not-end {
 	/* replace space made by the top border with padding */
 	border-bottom-width: 0;
 	padding-bottom: 1px;
@@ -148,12 +151,6 @@
 	border-bottom-right-radius: 0;
 }
 
-/*
-The above event styles will not apply to events that are being dragged. Dragged events are attached
-to an outer parent not part of the .fc-view, thus we need the className "fc-time-grid-event".
-The below styles WILL be applied to dragged events.
-*/
-
 .fc-time-grid-event {
 	overflow: hidden; /* don't let the bg flow over rounded corners */
 }

+ 14 - 1
src/basic/BasicView.js

@@ -50,6 +50,13 @@ $.extend(BasicView.prototype, {
 	},
 
 
+	// Make subcomponents ready for cleanup
+	destroy: function() {
+		this.dayGrid.destroy();
+		View.prototype.destroy.call(this); // call the super-method
+	},
+
+
 	// Builds the HTML skeleton for the view.
 	// The day-grid component will render inside of a container defined by this HTML.
 	renderHtml: function() {
@@ -214,7 +221,7 @@ $.extend(BasicView.prototype, {
 
 	// Renders the given events onto the view and populates the segments array
 	renderEvents: function(events) {
-		this.segs = this.dayGrid.renderEvents(events);
+		this.dayGrid.renderEvents(events);
 
 		this.updateHeight(); // must compensate for events that overflow the row
 
@@ -222,6 +229,12 @@ $.extend(BasicView.prototype, {
 	},
 
 
+	// Retrieves all segment objects that are rendered in the view
+	getSegs: function() {
+		return this.dayGrid.getSegs();
+	},
+
+
 	// Unrenders all event elements and clears internal segment data
 	destroyEvents: function() {
 		this.recordScroll(); // removing events will reduce height and mess with the scroll, so record beforehand

+ 3 - 1
src/basic/MonthView.js

@@ -6,7 +6,7 @@ setDefaults({
 	fixedWeekCount: true,
 	eventLimit: false,
 	eventLimitText: 'more',
-	eventLimitClick: 'auto'
+	eventLimitClick: 'popover'
 });
 
 fcViews.month = MonthView; // register the view
@@ -68,6 +68,8 @@ $.extend(MonthView.prototype, {
 			height *= this.rowCnt / 6;
 		}
 
+		this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed
+
 		// is the event limit a constant level number?
 		if (eventLimit && typeof eventLimit === 'number') {
 			this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after

+ 15 - 0
src/basic/basic.css

@@ -75,3 +75,18 @@ a.fc-more:hover {
 .fc-limited { /* rows and cells that are hidden because of a "more" link */
 	display: none;
 }
+
+/* popover that appears when "more" link is clicked */
+
+.fc-day-grid .fc-row {
+	z-index: 1; /* make the "more" popover one higher than this */
+}
+
+.fc-more-popover {
+	z-index: 2;
+	width: 220px;
+}
+
+.fc-more-popover .fc-event-container {
+	padding: 10px;
+}

+ 22 - 25
src/common/DayGrid.events.js

@@ -4,24 +4,32 @@
 
 $.extend(DayGrid.prototype, {
 
-
+	segs: null,
 	rowStructs: null, // an array of objects, each holding information about a row's event-rendering
 
 
 	// Render the given events onto the Grid and return the rendered segments
 	renderEvents: function(events) {
 		var rowStructs = this.rowStructs = this.renderEventRows(events);
-		var allSegs = [];
+		var segs = [];
 
 		// append to each row's content skeleton
 		this.rowEls.each(function(i, rowNode) {
 			$(rowNode).find('.fc-content-skeleton > table').append(
 				rowStructs[i].tbodyEl
 			);
-			allSegs.push.apply(allSegs, rowStructs[i].segs);
+			segs.push.apply(segs, rowStructs[i].segs);
 		});
 
-		return allSegs; // return segment objects. for the view
+		this.segs = segs;
+	},
+
+
+	// Retrieves all segment objects that have been rendered
+	getSegs: function() {
+		return (this.segs || []).concat(
+			this.popoverSegs || [] // segs rendered in the "more" events popover
+		);
 	},
 
 
@@ -33,38 +41,27 @@ $.extend(DayGrid.prototype, {
 		while ((rowStruct = rowStructs.pop())) {
 			rowStruct.tbodyEl.remove();
 		}
+
+		this.segs = null;
+		this.destroySegPopover(); // removes the "more.." events popover
 	},
 
 
 	// Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
 	// Returns an array of rowStruct objects (see the bottom of `renderEventRow`).
 	renderEventRows: function(events) {
-		var view = this.view;
-		var allSegs = this.eventsToSegs(events);
-		var segRows = this.groupSegRows(allSegs); // group into nested arrays
-		var html = '';
+		var segs = this.eventsToSegs(events);
 		var rowStructs = [];
-		var i;
+		var segRows;
 		var row;
-		var rowSegs;
 
-		// build a large concatenation of event segment HTML
-		for (i = 0; i < allSegs.length; i++) {
-			html += this.renderSegHtml(allSegs[i]);
-		}
-
-		// Grab individual elements from the combined HTML string. Use each as the default rendering.
-		// Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
-		$(html).each(function(i, node) {
-			allSegs[i].el = view.resolveEventEl(allSegs[i].event, $(node));
-		});
+		segs = this.renderSegs(segs); // returns a new array with only visible segments
+		segRows = this.groupSegRows(segs); // group into nested arrays
 
 		// iterate each row of segment groupings
 		for (row = 0; row < segRows.length; row++) {
-			rowSegs = segRows[row];
-			rowSegs = $.grep(rowSegs, renderedSegFilter); // filter out non-rendered segments
 			rowStructs.push(
-				this.renderEventRow(row, rowSegs)
+				this.renderEventRow(row, segRows[row])
 			);
 		}
 
@@ -73,12 +70,12 @@ $.extend(DayGrid.prototype, {
 
 
 	// Builds the HTML to be used for the default element for an individual segment
-	renderSegHtml: function(seg) {
+	renderSegHtml: function(seg, disableResizing) {
 		var view = this.view;
 		var isRTL = view.opt('isRTL');
 		var event = seg.event;
 		var isDraggable = view.isEventDraggable(event);
-		var isResizable = event.allDay && seg.isEnd && view.isEventResizable(event); // only on endings of timed events
+		var isResizable = !disableResizing && event.allDay && seg.isEnd && view.isEventResizable(event);
 		var classes = this.getSegClasses(seg, isDraggable, isResizable);
 		var skinCss = this.getEventSkinCss(event);
 		var timeHtml = '';

+ 5 - 0
src/common/DayGrid.js

@@ -46,6 +46,11 @@ $.extend(DayGrid.prototype, {
 	},
 
 
+	destroy: function() {
+		this.destroySegPopover();
+	},
+
+
 	// Generates the HTML for a single row. `row` is the row number.
 	dayRowHtml: function(row, isRigid) {
 		var view = this.view;

+ 119 - 17
src/common/DayGrid.limit.js

@@ -5,6 +5,17 @@
 $.extend(DayGrid.prototype, {
 
 
+	segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible
+	popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible
+
+
+	destroySegPopover: function() {
+		if (this.segPopover) {
+			this.segPopover.hide(); // will trigger destruction of `segPopover` and `popoverSegs`
+		}
+	},
+
+
 	// Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.
 	// `levelLimit` can be false (don't limit), a number, or true (should be computed).
 	limitRows: function(levelLimit) {
@@ -73,7 +84,7 @@ $.extend(DayGrid.prototype, {
 		var td, rowspan;
 		var segMoreNodes; // array of "more" <td> cells that will stand-in for the current seg's cell
 		var j;
-		var moreTd, moreLink;
+		var moreTd, moreWrap, moreLink;
 
 		// Iterates through empty level cells and places "more" links inside if need be
 		function emptyCellsUntil(endCol) { // goes from current `col` to `endCol`
@@ -83,8 +94,9 @@ $.extend(DayGrid.prototype, {
 				if (segsBelow.length) {
 					td = cellMatrix[levelLimit - 1][col];
 					moreLink = _this.renderMoreLink(cell, segsBelow);
-					td.append(moreLink);
-					moreNodes.push(moreLink[0]);
+					moreWrap = $('<div/>').append(moreLink);
+					td.append(moreWrap);
+					moreNodes.push(moreWrap[0]);
 				}
 				col++;
 			}
@@ -124,7 +136,8 @@ $.extend(DayGrid.prototype, {
 						segsBelow = colSegsBelow[j];
 						cell = { row: row, col: seg.leftCol + j };
 						moreLink = this.renderMoreLink(cell, [ seg ].concat(segsBelow)); // count seg as hidden too
-						moreTd.append(moreLink);
+						moreWrap = $('<div/>').append(moreLink);
+						moreTd.append(moreWrap);
 						segMoreNodes.push(moreTd[0]);
 						moreNodes.push(moreTd[0]);
 					}
@@ -168,27 +181,116 @@ $.extend(DayGrid.prototype, {
 			.text(
 				this.getMoreLinkText(hiddenSegs.length)
 			)
-			.on('click', function() {
-				var date = view.cellToDate(cell);
+			.on('click', function(ev) {
 				var clickOption = view.opt('eventLimitClick');
+				var date = view.cellToDate(cell);
+				var moreEl = $(this);
+				var dayEl = _this.getCellDayEl(cell);
+				var allSegs = _this.getCellSegs(cell);
 
-				if (typeof clickOption === 'string') { // a view name or 'auto'
-					view.calendar.zoomToDay(date, clickOption);
+				// rescope the segments to be within the cell's date
+				var reslicedAllSegs = _this.resliceDaySegs(allSegs, date);
+				var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date);
+
+				if (clickOption === 'popover') {
+					_this.showSegPopover(date, cell, moreEl, reslicedAllSegs);
+				}
+				else if (typeof clickOption === 'string') { // a view name
+					view.calendar.zoomTo(date, clickOption);
 				}
-				else {
-					view.trigger(
-						'eventLimitClick',
-						null,
-						date,
-						_this.getCellDayEl(cell), // element for the cell's day
-						hiddenSegs,
-						_this.getCellSegs(cell) // all segments on that day
-					);
+				else if (typeof clickOption === 'function') {
+					view.trigger('eventLimitClick', null, {
+						date: date,
+						dayEl: dayEl,
+						moreEl: moreEl,
+						segs: reslicedAllSegs,
+						hiddenSegs: reslicedHiddenSegs
+					}, ev);
 				}
 			});
 	},
 
 
+	// Reveals the popover that displays all events within a cell
+	showSegPopover: function(date, cell, moreLink, segs) {
+		var _this = this;
+		var view = this.view;
+		var moreWrap = moreLink.parent(); // the <div> wrapper around the <a>
+		var options = {
+			className: 'fc-more-popover',
+			content: this.renderSegPopoverContent(date, segs),
+			parentEl: this.el,
+			top: this.rowEls.eq(cell.row).offset().top, // better than the <td>. no border confusion
+			autoHide: true, // when the user clicks elsewhere, hide the popover
+			hide: function() {
+				// destroy everything when the popover is hidden
+				_this.segPopover.destroy();
+				_this.segPopover = null;
+				_this.popoverSegs = null;
+			}
+		};
+
+		// Determine horizontal coordinate.
+		// We use the moreWrap instead of the <td> to avoid border confusion.
+		if (view.opt('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(date, segs) {
+		var view = this.view;
+		var isTheme = view.opt('theme');
+		var title = date.format('LL'); // TODO: make this an option somehow
+		var content = $(
+			'<div class="fc-header ' + view.widgetHeaderClass + '">' +
+				'<span class="fc-close ' +
+					(isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') +
+				'"></span>' +
+				'<span class="fc-title">' +
+					htmlEscape(title) +
+				'</span>' +
+				'<div class="fc-clear"/>' +
+			'</div>' +
+			'<div class="fc-body ' + view.widgetContentClass + '">' +
+				'<div class="fc-event-container"></div>' +
+			'</div>'
+		);
+		var segContainer = content.find('.fc-event-container');
+		var i;
+
+		// render each seg's `el` and only return the visible segs
+		segs = this.renderSegs(segs, true); // disableResizing=true
+		this.popoverSegs = segs;
+
+		for (i = 0; i < segs.length; i++) {
+			segs[i].isDetached = true; // signals the segment doesn't live in a cell. needed for event DnD
+			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) {
+		var events = $.map(segs, function(seg) {
+			return seg.event;
+		});
+		var dayStart = dayDate.clone().stripTime();
+		var dayEnd = dayStart.clone().add('days', 1);
+
+		return this.eventsToSegs(events, dayStart, dayEnd);
+	},
+
+
 	// Generates the text that should be inside a "more" link, given the number of events it represents
 	getMoreLinkText: function(num) {
 		var view = this.view;

+ 157 - 50
src/common/Grid.events.js

@@ -4,6 +4,7 @@
 
 $.extend(Grid.prototype, {
 
+	isMouseOverSeg: false, // is the user's mouse over a segment?
 	isDraggingSeg: false, // is a segment being dragged?
 	isResizingSeg: false, // is a segment being resized?
 
@@ -14,29 +15,79 @@ $.extend(Grid.prototype, {
 	},
 
 
+	// Retrieves all rendered segment objects in this grid
+	getSegs: function() {
+		// subclasses must implement
+	},
+
+
 	// Unrenders all events
 	destroyEvents: function() {
 		// subclasses must implement
 	},
 
 
+	// Renders a `el` property for each seg, and only returns segments that successfully rendered
+	renderSegs: function(segs, disableResizing) {
+		var view = this.view;
+		var html = '';
+		var renderedSegs = [];
+		var i;
+
+		// build a large concatenation of event segment HTML
+		for (i = 0; i < segs.length; i++) {
+			html += this.renderSegHtml(segs[i], disableResizing);
+		}
+
+		// Grab individual elements from the combined HTML string. Use each as the default rendering.
+		// Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
+		$(html).each(function(i, node) {
+			var seg = segs[i];
+			var el = view.resolveEventEl(seg.event, $(node));
+			if (el) {
+				el.data('fc-seg', seg); // used by handlers
+				seg.el = el;
+				renderedSegs.push(seg);
+			}
+		});
+
+		return renderedSegs;
+	},
+
+
+	// Generates the HTML for the default rendering of a segment
+	renderSegHtml: function(seg, disableResizing) {
+		// subclasses must implement
+	},
+
+
 	// Converts an array of event objects into an array of segment objects
-	eventsToSegs: function(events) {
+	eventsToSegs: function(events, intervalStart, intervalEnd) {
 		var _this = this;
 
 		return $.map(events, function(event) {
-			return _this.eventToSegs(event); // $.map flattens all returned arrays together
+			return _this.eventToSegs(event, intervalStart, intervalEnd); // $.map flattens all returned arrays together
 		});
 	},
 
 
-	// Slices a single event into an array of event segments
-	eventToSegs: function(event) {
+	// Slices a single event into an array of event segments.
+	// When `intervalStart` and `intervalEnd` are specified, intersect the events with that interval.
+	// Otherwise, let the subclass decide how it wants to slice the segments over the grid.
+	eventToSegs: function(event, intervalStart, intervalEnd) {
 		var eventStart = event.start.clone().stripZone(); // normalize
 		var eventEnd = this.view.calendar.getEventEnd(event).stripZone(); // compute (if necessary) and normalize
-		var segs = this.rangeToSegs(eventStart, eventEnd); // defined by the subclass
+		var segs;
 		var i, seg;
 
+		if (intervalStart && intervalEnd) {
+			seg = intersectionToSeg(eventStart, eventEnd, intervalStart, intervalEnd);
+			segs = seg ? [ seg ] : [];
+		}
+		else {
+			segs = this.rangeToSegs(eventStart, eventEnd); // defined by the subclass
+		}
+
 		// assign extra event-related properties to the segment objects
 		for (i = 0; i < segs.length; i++) {
 			seg = segs[i];
@@ -49,6 +100,10 @@ $.extend(Grid.prototype, {
 	},
 
 
+	/* Handlers
+	------------------------------------------------------------------------------------------------------------------*/
+
+
 	// Attaches event-element-related handlers to the container element and leverage bubbling
 	bindSegHandlers: function() {
 		var _this = this;
@@ -57,10 +112,10 @@ $.extend(Grid.prototype, {
 		$.each(
 			{
 				mouseenter: function(seg, ev) {
-					view.trigger('eventMouseover', this, seg.event, ev);
+					_this.triggerSegMouseover(seg, ev);
 				},
 				mouseleave: function(seg, ev) {
-					view.trigger('eventMouseout', this, seg.event, ev);
+					_this.triggerSegMouseout(seg, ev);
 				},
 				click: function(seg, ev) {
 					return view.trigger('eventClick', this, seg.event, ev); // can return `false` to cancel
@@ -76,11 +131,11 @@ $.extend(Grid.prototype, {
 			},
 			function(name, func) {
 				// attach the handler to the container element and only listen for real event elements via bubbling
-				_this.el.on(name, '.fc-content-skeleton .fc-event-container > *', function(ev) {
+				_this.el.on(name, '.fc-event-container > *', function(ev) {
 					var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents
 
-					if (seg /*&& !_this.isDraggingSeg && !_this.isResizingSeg*/) {
-						   // needs more work if we want eventMouseout to fire correctly
+					// only call the handlers if there is not a drag/resize in progress
+					if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {
 						func.call(this, seg, ev); // `this` will be the event element
 					}
 				});
@@ -89,6 +144,28 @@ $.extend(Grid.prototype, {
 	},
 
 
+	// Updates internal state and triggers handlers for when an event element is moused over
+	triggerSegMouseover: function(seg, ev) {
+		if (!this.isMouseOverSeg) {
+			this.isMouseOverSeg = true;
+			this.view.trigger('eventMouseover', seg.el[0], seg.event, ev);
+		}
+	},
+
+
+	// Updates internal state and triggers handlers for when an event element is moused out
+	triggerSegMouseout: function(seg, ev) {
+		if (this.isMouseOverSeg) {
+			this.isMouseOverSeg = false;
+			this.view.trigger('eventMouseout', seg.el[0], seg.event, ev);
+		}
+	},
+
+
+	/* Dragging
+	------------------------------------------------------------------------------------------------------------------*/
+
+
 	// Called when the user does a mousedown on an event, which might lead to dragging.
 	// Generic enough to work with any type of Grid.
 	segDragMousedown: function(seg, ev) {
@@ -96,9 +173,7 @@ $.extend(Grid.prototype, {
 		var view = this.view;
 		var el = seg.el;
 		var event = seg.event;
-		var start = event.start;
-		var end = view.calendar.getEventEnd(event);
-		var newStart = null;
+		var newStart, newEnd;
 
 		// A clone of the original element that will move with the mouse
 		var mouseFollower = new MouseFollower(seg.el, {
@@ -117,39 +192,21 @@ $.extend(Grid.prototype, {
 				mouseFollower.start(ev);
 			},
 			dragStart: function(ev) {
+				_this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
 				_this.isDraggingSeg = true;
 				view.hideEvent(event); // hide all event segments. our mouseFollower will take over
-
 				view.trigger('eventDragStart', el[0], event, ev, {}); // last argument is jqui dummy
 			},
 			cellOver: function(cell, date) {
-				var origDate = dragListener.origDate;
-				var delta;
-				var newEnd;
-
-				if (origDate) { // must start out on a cell (weird accident if it didn't)
-
-					if (date.hasTime() === origDate.hasTime()) { // staying all-day or staying timed
-						delta = dayishDiff(date, origDate);
-						newStart = start.clone().add(delta);
-						if (event.end === null) { // do we need to compute an end?
-							newEnd = null;
-						}
-						else {
-							newEnd = end.clone().add(delta);
-						}
-					}
-					else { // switching from all-day to timed, or vice versa
-						newStart = date;
-						newEnd = null; // end should be cleared
-					}
+				var res = _this.computeDraggedEventDates(seg, dragListener.origDate, date);
+				newStart = res.start;
+				newEnd = res.end;
 
-					if (view.renderDrag(newStart, newEnd, seg)) { // have the view render a visual indication
-						mouseFollower.hide(); // if the view is already using a mock event "helper", hide our own
-					}
-					else {
-						mouseFollower.show();
-					}
+				if (view.renderDrag(newStart, newEnd, seg)) { // have the view render a visual indication
+					mouseFollower.hide(); // if the view is already using a mock event "helper", hide our own
+				}
+				else {
+					mouseFollower.show();
 				}
 			},
 			cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells
@@ -158,14 +215,13 @@ $.extend(Grid.prototype, {
 				mouseFollower.show(); // show in case we are moving out of all cells
 			},
 			dragStop: function(ev) {
-				var hasChanged = newStart && !newStart.isSame(start);
+				var hasChanged = newStart && !newStart.isSame(event.start);
 
 				// do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
 				mouseFollower.stop(!hasChanged, function() {
 					_this.isDraggingSeg = false;
 					view.destroyDrag();
 					view.showEvent(event);
-
 					view.trigger('eventDragStop', el[0], event, ev, {}); // last argument is jqui dummy
 
 					if (hasChanged) {
@@ -182,6 +238,60 @@ $.extend(Grid.prototype, {
 	},
 
 
+	// Given a segment, where it originally resided on the grid, and the new date it has been dragged to,
+	// calculates the Event Object's new start and end dates.
+	computeDraggedEventDates: function(seg, origDate, newDate) {
+		var view = this.view;
+		var event = seg.event;
+		var start = event.start;
+		var end = view.calendar.getEventEnd(event);
+		var delta;
+		var newStart;
+		var newEnd;
+
+		// the segment might be explicitly marked as not-in-the-grid
+		if (seg.isDetached) {
+			origDate = null;
+		}
+
+		// calculate the delta (a Duration) that the event's dates must be moved.
+		// if `delta` remains undefined, that means the event's start will literally become newDate.
+		if (!origDate) {
+			if (newDate.hasTime()) { // over a time slot
+				delta = dayishDiff(newDate, start); // will move the start to the exact new datetime
+			}
+			else { // over a whole-day cell
+				delta = dayDiff(newDate, start); // will be a whole-day diff, so that start's time will be kept
+			}
+		}
+		else if (newDate.hasTime() === origDate.hasTime()) { // staying all-day or staying timed
+			delta = dayishDiff(newDate, origDate);
+		}
+		// if switching from day <-> timed, start should be reset to the dropped date, and the end cleared
+
+		// recalculate start/end
+		if (delta) {
+			newStart = start.clone().add(delta);
+			if (event.end === null) { // do we need to compute an end?
+				newEnd = null;
+			}
+			else {
+				newEnd = end.clone().add(delta);
+			}
+		}
+		else {
+			newStart = newDate;
+			newEnd = null; // end should be cleared
+		}
+
+		return { start: newStart, end: newEnd };
+	},
+
+
+	/* Resizing
+	------------------------------------------------------------------------------------------------------------------*/
+
+
 	// Called when the user does a mousedown on an event's resizer, which might lead to resizing.
 	// Generic enough to work with any type of Grid.
 	segResizeMousedown: function(seg, ev) {
@@ -203,8 +313,8 @@ $.extend(Grid.prototype, {
 		dragListener = new DragListener(this.coordMap, {
 			distance: 5,
 			dragStart: function(ev) {
+				_this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
 				_this.isResizingSeg = true;
-
 				view.trigger('eventResizeStart', el[0], event, ev, {}); // last argument is jqui dummy
 			},
 			cellOver: function(cell, date) {
@@ -230,7 +340,6 @@ $.extend(Grid.prototype, {
 			dragStop: function(ev) {
 				_this.isResizingSeg = false;
 				destroy();
-
 				view.trigger('eventResizeStop', el[0], event, ev, {}); // last argument is jqui dummy
 
 				if (newEnd) {
@@ -243,6 +352,10 @@ $.extend(Grid.prototype, {
 	},
 
 
+	/* Rendering Utils
+	------------------------------------------------------------------------------------------------------------------*/
+
+
 	// Generic utility for generating the HTML classNames for an event segment's element
 	getSegClasses: function(seg, isDraggable, isResizable) {
 		var event = seg.event;
@@ -319,9 +432,3 @@ function compareSegs(seg1, seg2) {
 		(seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title
 }
 
-
-// Returns `true` if the segment has a rendered element and `false` otherwise
-function renderedSegFilter(seg) {
-	return !!seg.el;
-}
-

+ 10 - 1
src/common/Grid.js

@@ -23,6 +23,12 @@ $.extend(Grid.prototype, {
 	},
 
 
+	// Called when the grid's resources need to be cleaned up
+	destroy: function() {
+		// subclasses can implement
+	},
+
+
 	/* Coordinates & Cells
 	------------------------------------------------------------------------------------------------------------------*/
 
@@ -62,7 +68,10 @@ $.extend(Grid.prototype, {
 		var _this = this;
 
 		this.el.on('mousedown', function(ev) {
-			if (!$(ev.target).is('.fc-event-container *, .fc-more')) { // not an event element or "more" link
+			if (
+				!$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link
+				!$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one)
+			) {
 				_this.dayMousedown(ev);
 			}
 		});

+ 165 - 0
src/common/Popover.js

@@ -0,0 +1,165 @@
+
+/* A rectangular panel that is absolutely positioned over other content
+------------------------------------------------------------------------------------------------------------------------
+Options:
+	- className (string)
+	- content (HTML string or jQuery element set)
+	- parentEl
+	- top
+	- left
+	- right (the x coord of where the right edge should be. not a "CSS" right)
+	- autoHide (boolean)
+	- show (callback)
+	- hide (callback)
+*/
+
+function Popover(options) {
+	this.options = options || {};
+}
+
+
+Popover.prototype = {
+
+	isHidden: true,
+	options: null,
+	el: null, // the container element for the popover. generated by this object
+	documentMousedownProxy: null, // document mousedown handler bound to `this`
+	margin: 10, // the space required between the popover and the edges of the scroll container
+
+
+	// Shows the popover on the specified position. Renders it if not already
+	show: function() {
+		if (this.isHidden) {
+			if (!this.el) {
+				this.render();
+			}
+			this.el.show();
+			this.position();
+			this.isHidden = false;
+			this.trigger('show');
+		}
+	},
+
+
+	// Hides the popover, through CSS, but does not remove it from the DOM
+	hide: function() {
+		if (!this.isHidden) {
+			this.el.hide();
+			this.isHidden = true;
+			this.trigger('hide');
+		}
+	},
+
+
+	// Creates `this.el` and renders content inside of it
+	render: function() {
+		var _this = this;
+		var options = this.options;
+
+		this.el = $('<div class="fc-popover"/>')
+			.addClass(options.className || '')
+			.css({
+				// position initially to the top left to avoid creating scrollbars
+				top: 0,
+				left: 0
+			})
+			.append(options.content)
+			.appendTo(options.parentEl);
+
+		// when a click happens on anything inside with a 'fc-close' className, hide the popover
+		this.el.on('click', '.fc-close', function() {
+			_this.hide();
+		});
+
+		if (options.autoHide) {
+			$(document).on('mousedown', this.documentMousedownProxy = $.proxy(this, 'documentMousedown'));
+		}
+	},
+
+
+	// Triggered when the user clicks *anywhere* in the document, for the autoHide feature
+	documentMousedown: function(ev) {
+		// only hide the popover if the click happened outside the popover
+		if (this.el && !$(ev.target).closest(this.el).length) {
+			this.hide();
+		}
+	},
+
+
+	// Hides and unregisters any handlers
+	destroy: function() {
+		this.hide();
+
+		if (this.el) {
+			this.el.remove();
+			this.el = null;
+		}
+
+		$(document).off('mousedown', this.documentMousedownProxy);
+	},
+
+
+	// Positions the popover optimally, using the top/left/right options
+	position: function() {
+		var options = this.options;
+		var origin = this.el.offsetParent().offset();
+		var width = this.el.outerWidth();
+		var height = this.el.outerHeight();
+		var windowEl = $(window);
+		var viewportEl = getScrollParent(this.el);
+		var viewportTop;
+		var viewportLeft;
+		var viewportOffset;
+		var top; // the "position" (not "offset") values for the popover
+		var left; //
+
+		// compute top and left
+		top = options.top || 0;
+		if (options.left !== undefined) {
+			left = options.left;
+		}
+		else if (options.right !== undefined) {
+			left = options.right - width; // derive the left value from the right value
+		}
+		else {
+			left = 0;
+		}
+
+		if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result
+			viewportEl = windowEl;
+			viewportTop = 0; // the window is always at the top left
+			viewportLeft = 0; // (and .offset() won't work if called here)
+		}
+		else {
+			viewportOffset = viewportEl.offset();
+			viewportTop = viewportOffset.top;
+			viewportLeft = viewportOffset.left;
+		}
+
+		// if the window is scrolled, it causes the visible area to be further down
+		viewportTop += windowEl.scrollTop();
+		viewportLeft += windowEl.scrollLeft();
+
+		// constrain to the view port. if constrained by two edges, give precedence to top/left
+		top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin);
+		top = Math.max(top, viewportTop + this.margin);
+		left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin);
+		left = Math.max(left, viewportLeft + this.margin);
+
+		this.el.css({
+			top: top - origin.top,
+			left: left - origin.left
+		});
+	},
+
+
+	// Triggers a callback. Calls a function in the option hash of the same name.
+	// Arguments beyond the first `name` are forwarded on.
+	// TODO: better code reuse for this. Repeat code
+	trigger: function(name) {
+		if (this.options[name]) {
+			this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
+		}
+	}
+
+};

+ 25 - 26
src/common/TimeGrid.events.js

@@ -4,6 +4,7 @@
 
 $.extend(TimeGrid.prototype, {
 
+	segs: null, // segment objects rendered in the component. null of events haven't been rendered yet
 	eventSkeletonEl: null, // has cells with event-containers, which contain absolutely positioned event elements
 
 
@@ -14,7 +15,13 @@ $.extend(TimeGrid.prototype, {
 		this.eventSkeletonEl = $('<div class="fc-content-skeleton"/>').append(res.tableEl);
 		this.el.append(this.eventSkeletonEl);
 
-		return res.segs; // return segment objects. for the view
+		this.segs = res.segs;
+	},
+
+
+	// Retrieves rendered segment objects
+	getSegs: function() {
+		return this.segs || [];
 	},
 
 
@@ -24,49 +31,41 @@ $.extend(TimeGrid.prototype, {
 			this.eventSkeletonEl.remove();
 			this.eventSkeletonEl = null;
 		}
+
+		this.segs = null;
 	},
 
 
 	// Renders and returns the <table> portion of the event-skeleton.
 	// Returns an object with properties 'tbodyEl' and 'segs'.
 	renderEventTable: function(events) {
-		var view = this.view;
 		var tableEl = $('<table><tr/></table>');
 		var trEl = tableEl.find('tr');
-		var allSegs = this.eventsToSegs(events);
-		var segCols = this.groupSegCols(allSegs); // groups into sub-arrays, and assigns 'col' to each seg
-		var html = ''; // html string with default HTML for all events, concatenated together
+		var segs = this.eventsToSegs(events);
+		var segCols;
 		var i, seg;
-		var col, segs;
+		var col, colSegs;
 		var containerEl;
 
-		// build the combined HTML string. and compute top/bottom
-		for (i = 0; i < allSegs.length; i++) {
-			seg = allSegs[i];
-			html += this.renderSegHtml(seg);
+		segs = this.renderSegs(segs); // returns only the visible segs
+		segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg
 
+		// compute vertical coordinates
+		for (i = 0; i < segs.length; i++) {
+			seg = segs[i];
 			seg.top = this.computeDateTop(seg.start, seg.start);
 			seg.bottom = this.computeDateTop(seg.end, seg.start);
 		}
 
-		// Grab individual elements from the combined HTML string. Use each as the default rendering.
-		// Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
-		$(html).each(function(i, node) {
-			allSegs[i].el = view.resolveEventEl(allSegs[i].event, $(node));
-		});
-
 		for (col = 0; col < segCols.length; col++) { // iterate each column grouping
-			segs = segCols[col];
-
-			segs = $.grep(segs, renderedSegFilter); // filter out unrendered segments
-			placeSlotSegs(segs); // compute horizontal coordinates, z-index's, and reorder the array
-			segCols[col] = segs; // assign back
+			colSegs = segCols[col];
+			placeSlotSegs(colSegs); // compute horizontal coordinates, z-index's, and reorder the array
 
 			containerEl = $('<div class="fc-event-container"/>');
 
 			// assign positioning CSS and insert into container
-			for (i = 0; i < segs.length; i++) {
-				seg = segs[i];
+			for (i = 0; i < colSegs.length; i++) {
+				seg = colSegs[i];
 				seg.el.css(this.generateSegPositionCss(seg));
 				containerEl.append(seg.el);
 			}
@@ -78,17 +77,17 @@ $.extend(TimeGrid.prototype, {
 
 		return  {
 			tableEl: tableEl,
-			segs: flattenArray(segCols) // will contain only segments with rendered els
+			segs: segs
 		};
 	},
 
 
 	// Renders the HTML for a single event segment's default rendering
-	renderSegHtml: function(seg) {
+	renderSegHtml: function(seg, disableResizing) {
 		var view = this.view;
 		var event = seg.event;
 		var isDraggable = view.isEventDraggable(event);
-		var isResizable = seg.isEnd && view.isEventResizable(event);
+		var isResizable = !disableResizing && seg.isEnd && view.isEventResizable(event);
 		var classes = this.getSegClasses(seg, isDraggable, isResizable);
 		var skinCss = this.getEventSkinCss(event);
 		var timeText;

+ 10 - 35
src/common/TimeGrid.js

@@ -124,51 +124,26 @@ $.extend(TimeGrid.prototype, {
 
 
 	// Slices up a date range into a segment for each column
-	rangeToSegs: function(start, end) {
+	rangeToSegs: function(rangeStart, rangeEnd) {
 		var view = this.view;
 		var segs = [];
+		var seg;
 		var col;
 		var cellDate;
 		var colStart, colEnd;
-		var segStart, segEnd;
-		var isStart, isEnd;
 
 		// normalize
-		start = start.clone().stripZone();
-		end = end.clone().stripZone();
+		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().stripZone().time(this.minTime); // normalize and calculate
-			colEnd = cellDate.clone().stripZone().time(this.maxTime); // normalize and calculate
-
-			if (end > colStart && start < colEnd) { // in bounds at all?
-
-				if (start >= colStart) {
-					segStart = start.clone();
-					isStart = true;
-				}
-				else {
-					segStart = colStart; // don't need to clone
-					isStart =  false;
-				}
-
-				if (end <= colEnd) {
-					segEnd = end.clone();
-					isEnd = true;
-				}
-				else {
-					segEnd = colEnd; // don't need to clone
-					isEnd = false;
-				}
-
-				segs.push({
-					col: col,
-					start: segStart,
-					end: segEnd,
-					isStart: isStart,
-					isEnd: isEnd
-				});
+			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);
 			}
 		}
 

+ 11 - 7
src/common/View.js

@@ -19,8 +19,6 @@ View.prototype = {
 	rowCnt: null, // # of weeks
 	colCnt: null, // # of days displayed in a week
 
-	segs: null, // array of rendered event segment objects
-
 	isSelected: false, // boolean whether cells are user-selected or not
 
 	// subclasses can optionally use a scroll container
@@ -124,7 +122,9 @@ View.prototype = {
 	// Should be called before there is a destructive operation (like removing DOM elements) that might inadvertently
 	// change the scroll of the container.
 	recordScroll: function() {
-		this.scrollTop = this.scrollerEl.scrollTop();
+		if (this.scrollerEl) {
+			this.scrollTop = this.scrollerEl.scrollTop();
+		}
 	},
 
 
@@ -143,10 +143,9 @@ View.prototype = {
 
 
 	// Renders the events onto the view.
-	// Should be overriden by subclasses. Subclasses should assign `this.segs` and call the super-method afterwards.
+	// Should be overriden by subclasses. Subclasses should call the super-method afterwards.
 	renderEvents: function(events) {
 		this.segEach(function(seg) {
-			seg.el.data('fc-seg', seg); // store info about the segment object. used by handlers
 			this.trigger('eventAfterRender', seg.event, seg.event, seg.el);
 		});
 		this.trigger('eventAfterAllRender');
@@ -159,7 +158,6 @@ View.prototype = {
 		this.segEach(function(seg) {
 			this.trigger('eventDestroy', seg.event, seg.event, seg.el);
 		});
-		this.segs = [];
 	},
 
 
@@ -199,7 +197,7 @@ View.prototype = {
 	// If the optional `event` argument is specified, only iterates through segments linked to that event.
 	// The `this` value of the callback function will be the view.
 	segEach: function(func, event) {
-		var segs = this.segs || [];
+		var segs = this.getSegs();
 		var i;
 
 		for (i = 0; i < segs.length; i++) {
@@ -210,6 +208,12 @@ View.prototype = {
 	},
 
 
+	// Retrieves all the rendered segment objects for the view
+	getSegs: function() {
+		// subclasses must implement
+	},
+
+
 	/* Event Drag Visualization
 	------------------------------------------------------------------------------------------------------------------*/
 

+ 74 - 14
src/common/common.css

@@ -27,14 +27,24 @@ body .fc { /* extra precedence to overcome jqui */
 .fc-unthemed hr,
 .fc-unthemed thead,
 .fc-unthemed tbody,
-.fc-unthemed .fc-row {
+.fc-unthemed .fc-row,
+.fc-unthemed .fc-popover {
 	border-color: #ddd;
 }
 
-.fc-unthemed hr {
+.fc-unthemed .fc-popover {
+	background-color: #fff;
+}
+
+.fc-unthemed hr,
+.fc-unthemed .fc-popover .fc-header {
 	background: #eee;
 }
 
+.fc-unthemed .fc-popover .fc-header .fc-close {
+	color: #666;
+}
+
 .fc-unthemed .fc-today {
 	background: #fcf8e3;
 }
@@ -75,6 +85,10 @@ body .fc { /* extra precedence to overcome jqui */
 	content: "\000BB";
 }
 
+.fc-icon-x:after {
+	content: "\000D7";
+}
+
 
 /* Buttons (styled <button> tags, normalized to work cross-browser)
 --------------------------------------------------------------------------------------------------*/
@@ -197,6 +211,55 @@ previous button's border...
 }
 
 
+/* Popover
+--------------------------------------------------------------------------------------------------*/
+
+.fc-popover {
+	position: absolute;
+	box-shadow: 0 2px 6px rgba(0,0,0,.15);
+}
+
+.fc-popover .fc-header {
+	padding: 2px 4px;
+}
+
+.fc-popover .fc-header .fc-title {
+	margin: 0 2px;
+}
+
+.fc-popover .fc-header .fc-close {
+	cursor: pointer;
+}
+
+.fc-ltr .fc-popover .fc-header .fc-title,
+.fc-rtl .fc-popover .fc-header .fc-close {
+	float: left;
+}
+
+.fc-rtl .fc-popover .fc-header .fc-title,
+.fc-ltr .fc-popover .fc-header .fc-close {
+	float: right;
+}
+
+/* unthemed */
+
+.fc-unthemed .fc-popover {
+	border-width: 1px;
+	border-style: solid;
+}
+
+.fc-unthemed .fc-popover .fc-header .fc-close {
+	font-size: 25px;
+	margin-top: 4px;
+}
+
+/* jqui themed */
+
+.fc-popover > .ui-widget-header + .ui-widget-content {
+	border-top: 0; /* where they meet, let the header have the border */
+}
+
+
 /* Misc Reusable Components
 --------------------------------------------------------------------------------------------------*/
 
@@ -383,16 +446,19 @@ temporary rendered events).
 
 
 /* DayGrid events
---------------------------------------------------------------------------------------------------*/
+----------------------------------------------------------------------------------------------------
+We use the full "fc-day-grid-event" class instead of using descendants because the event won't
+be a descendant of the grid when it is being dragged.
+*/
 
-.fc-day-grid .fc-event {
+.fc-day-grid-event {
 	margin: 1px 1px 0; /* spacing between events and edges */
 }
 
 /* events that are continuing to/from another week. kill rounded corners and butt up against edge */
 
-.fc-ltr .fc-day-grid .fc-event.fc-not-start,
-.fc-rtl .fc-day-grid .fc-event.fc-not-end {
+.fc-ltr .fc-day-grid-event.fc-not-start,
+.fc-rtl .fc-day-grid-event.fc-not-end {
 	margin-left: 0;
 	border-left-width: 0;
 	padding-left: 1px; /* replace the border with padding */
@@ -400,8 +466,8 @@ temporary rendered events).
 	border-bottom-left-radius: 0;
 }
 
-.fc-ltr .fc-day-grid .fc-event.fc-not-end,
-.fc-rtl .fc-day-grid .fc-event.fc-not-start {
+.fc-ltr .fc-day-grid-event.fc-not-end,
+.fc-rtl .fc-day-grid-event.fc-not-start {
 	margin-right: 0;
 	border-right-width: 0;
 	padding-right: 1px; /* replace the border with padding */
@@ -409,12 +475,6 @@ temporary rendered events).
 	border-bottom-right-radius: 0;
 }
 
-/*
-The above event styles will not apply to events that are being dragged. Dragged events are attached
-to an outer parent not part of the .fc-view, thus we need the className "fc-day-grid-event".
-The below styles WILL be applied to dragged events.
-*/
-
 .fc-day-grid-event > .fc-content { /* force events to be one-line tall */
 	white-space: nowrap;
 	overflow: hidden;

+ 61 - 7
src/util.js

@@ -116,6 +116,20 @@ function matchCellWidths(els) {
 ----------------------------------------------------------------------------------------------------------------------*/
 
 
+// borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51
+function getScrollParent(el) {
+	var position = el.css('position'),
+		scrollParent = el.parents().filter(function() {
+			var parent = $(this);
+			return (/(auto|scroll)/).test(
+				parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x')
+			);
+		}).eq(0);
+
+	return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent;
+}
+
+
 // Given a container element, return an object with the pixel values of the left/right scrollbars.
 // Left scrollbars might occur on RTL browsers (IE maybe?) but I have not tested.
 // PREREQUISITE: container element must have a single child with display:block
@@ -143,6 +157,42 @@ function isPrimaryMouseButton(ev) {
 ----------------------------------------------------------------------------------------------------------------------*/
 
 
+// Creates a basic segment with the intersection of the two ranges. Returns undefined if no intersection.
+// Expects all dates to be normalized to the same timezone beforehand.
+function intersectionToSeg(subjectStart, subjectEnd, intervalStart, intervalEnd) {
+	var segStart, segEnd;
+	var isStart, isEnd;
+
+	if (subjectEnd > intervalStart && subjectStart < intervalEnd) { // in bounds at all?
+
+		if (subjectStart >= intervalStart) {
+			segStart = subjectStart.clone();
+			isStart = true;
+		}
+		else {
+			segStart = intervalStart.clone();
+			isStart =  false;
+		}
+
+		if (subjectEnd <= intervalEnd) {
+			segEnd = subjectEnd.clone();
+			isEnd = true;
+		}
+		else {
+			segEnd = intervalEnd.clone();
+			isEnd = false;
+		}
+
+		return {
+			start: segStart,
+			end: segEnd,
+			isStart: isStart,
+			isEnd: isEnd
+		};
+	}
+}
+
+
 function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object
 	obj = obj || {};
 	if (obj[name] !== undefined) {
@@ -163,11 +213,20 @@ function smartProperty(obj, name) { // get a camel-cased/namespaced property of
 /* Date Utilities
 ----------------------------------------------------------------------------------------------------------------------*/
 
-
 var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
 
 
-// diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
+// Diffs the two moments into a Duration where only full-days are considered.
+// Moments will have their timezones normalized.
+function dayDiff(a, b) {
+	return moment.duration({
+		days: a.clone().stripTime().diff(b.clone().stripTime(), 'days')
+	});
+}
+
+
+// Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
+// Moments will have their timezones normalized.
 function dayishDiff(a, b) {
 	return moment.duration({
 		days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'),
@@ -211,11 +270,6 @@ function extend(a, b) {
 }
 
 
-function flattenArray(a) { // flatten an array of arrays, one level deep
-	return Array.prototype.concat.apply([], a);
-}
-
-
 function applyAll(functions, thisObj, args) {
 	if ($.isFunction(functions)) {
 		functions = [ functions ];