Procházet zdrojové kódy

overlap and constraint settings for event dragging/resizing and selecting

Adam Shaw před 11 roky
rodič
revize
48958ea3cb

+ 161 - 9
src/EventManager.js

@@ -592,9 +592,11 @@ function EventManager(options) { // assumed to be a calendar
 
 	// 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.
-	// `_rangeStart` and `_rangeEnd` and are HACKS for when the no events have been requested yet.
-	function expandEvent(abstractEvent, _rangeStart, _rangeEnd) {
+	function expandEvent(abstractEvent) {
 		var events = [];
+		var _rangeStart = rangeStart;
+		var _rangeEnd = rangeEnd;
+		var view;
 		var dowHash;
 		var dow;
 		var i;
@@ -603,9 +605,12 @@ function EventManager(options) { // assumed to be a calendar
 		var start, end;
 		var event;
 
-		// hack
-		_rangeStart = _rangeStart || rangeStart;
-		_rangeEnd = _rangeEnd || rangeEnd;
+		// hack for when fetchEvents hasn't been called yet (calculating businessHours for example)
+		if (!_rangeStart || !_rangeEnd) {
+			view = t.getView();
+			_rangeStart = view.start;
+			_rangeEnd = view.end;
+		}
 
 		if (abstractEvent._recurring) {
 
@@ -832,10 +837,14 @@ function EventManager(options) { // assumed to be a calendar
 	/* Business Hours
 	-----------------------------------------------------------------------------------------*/
 
-	t.getBusinessHoursEvents = function(view) {
+	t.getBusinessHoursEvents = getBusinessHoursEvents;
+
+
+	// Returns an array of events as to when the business hours occur in the current view.
+	// Abuse of our event system :(
+	function getBusinessHoursEvents() {
 		var optionVal = options.businessHours;
 		var defaultVal = {
-			id: '_businessHours',
 			className: 'fc-nonbusiness',
 			start: '09:00',
 			end: '17:00',
@@ -846,19 +855,162 @@ function EventManager(options) { // assumed to be a calendar
 
 		if (optionVal) {
 			if (typeof optionVal === 'object') {
+				// option value is an object that can override the default business hours
 				eventInput = $.extend({}, defaultVal, optionVal);
 			}
 			else {
+				// option value is `true`. use default business hours
 				eventInput = defaultVal;
 			}
 		}
 
 		if (eventInput) {
-			return expandEvent(buildEventFromInput(eventInput), view.start, view.end);
+			return expandEvent(buildEventFromInput(eventInput));
 		}
 
 		return [];
-	};
+	}
+
+
+	/* Overlapping / Constraining
+	-----------------------------------------------------------------------------------------*/
+
+	t.isEventAllowedInRange = isEventAllowedInRange;
+	t.isSelectionAllowedInRange = isSelectionAllowedInRange;
+	t.enableCursor = enableCursor;
+	t.disableCursor = disableCursor;
+
+
+	function isEventAllowedInRange(event, start, end) {
+		var source = event.source || {};
+		var constraint = firstDefined(
+			event.constraint,
+			source.constraint,
+			options.eventConstraint
+		);
+		var overlap = firstDefined(
+			event.overlap,
+			source.overlap,
+			options.eventOverlap
+		);
+
+		return isRangeAllowed(start, end, constraint, overlap, event);
+	}
+
+
+	function isSelectionAllowedInRange(start, end) {
+		return isRangeAllowed(
+			start,
+			end,
+			options.selectionConstraint,
+			options.selectionOverlap
+		);
+	}
+
+
+	// Returns true if the given range (caused by an event drop or a selection) is allowed to exist on the calendar
+	// according to the constraint/overlap settings.
+	// `event` is required only in the case of isEventAllowedInRange.
+	function isRangeAllowed(start, end, constraint, overlap, event) {
+		var constraintEvents;
+		var anyContainment;
+		var overlapFunc;
+		var i;
+
+		// normalize. fyi, we're normalizing in too many places :(
+		start = start.clone().stripZone();
+		end = end.clone().stripZone();
+
+		// the range must be fully contained by at least one of produced constraint events
+		if (constraint != null) {
+			constraintEvents = constraintToEvents(constraint);
+			anyContainment = false;
+
+			for (i = 0; i < constraintEvents.length; i++) {
+				if (eventContainsRange(constraintEvents[i], start, end)) {
+					anyContainment = true;
+					break;
+				}
+			}
+
+			if (!anyContainment) {
+				return false;
+			}
+		}
+
+		// overlap is a filter function
+		if (typeof overlap === 'function') {
+			overlapFunc = overlap;
+		}
+
+		if (overlap === false || overlapFunc) { // `false` means make sure there is no overlap with *any* event
+
+			// check for intersection with events
+			for (i = 0; i < cache.length; i++) {
+				if (
+					(!event || event._id !== cache[i]._id) && // don't compare the event against itself
+					eventIntersectsRange(cache[i], start, end) && // make sure it intersects
+					(!overlapFunc || overlapFunc(cache[i], event) === false) // use filter function (if there is one)
+				) {
+					return false;
+				}
+			}
+		}
+
+		return true;
+	}
+
+
+	// Given an event input from the API, produces an array of event objects. Possible event inputs:
+	// 'businessHours'
+	// An event ID (number or string)
+	// An object with specific start/end dates or a recurring event (like what businessHours accepts)
+	function constraintToEvents(constraintInput) {
+
+		if (constraintInput === 'businessHours') {
+			return getBusinessHoursEvents();
+		}
+
+		if (typeof constraintInput === 'object') {
+			return expandEvent(buildEventFromInput(constraintInput));
+		}
+
+		return clientEvents(constraintInput); // probably an ID
+	}
+
+
+	// Is the event's date ranged fully contained by the given range?
+	// start/end already assumed to have stripped zones :(
+	function eventContainsRange(event, start, end) {
+		var eventStart = event.start.clone().stripZone();
+		var eventEnd = t.getEventEnd(event).stripZone();
+
+		return start >= eventStart && end <= eventEnd;
+	}
+
+
+	// Does the event's date range intersect with the given range?
+	// start/end already assumed to have stripped zones :(
+	function eventIntersectsRange(event, start, end) {
+		var eventStart = event.start.clone().stripZone();
+		var eventEnd = t.getEventEnd(event).stripZone();
+
+		return start < eventEnd && end > eventStart;
+	}
+
+
+	// Make the cursor express that an event is not allowed in the current area.
+	// Shouldn't really be here :(
+	function disableCursor() {
+		$('body').addClass('fc-not-allowed');
+	}
+
+
+	// Returns the cursor to its original look.
+	// Shouldn't really be here :(
+	function enableCursor() {
+		$('body').removeClass('fc-not-allowed');
+	}
 
 }
 

+ 1 - 1
src/common/DragListener.js

@@ -1,7 +1,7 @@
 
 /* Tracks mouse movements over a CoordMap and raises events about which cell the mouse is over.
 ----------------------------------------------------------------------------------------------------------------------*/
-// TODO: implement scrolling
+// TODO: very useful to have a handler that gets called upon cellOut OR when dragging stops (for cleanup)
 
 function DragListener(coordMap, options) {
 	this.coordMap = coordMap;

+ 36 - 10
src/common/Grid.events.js

@@ -242,6 +242,7 @@ $.extend(Grid.prototype, {
 	segDragMousedown: function(seg, ev) {
 		var _this = this;
 		var view = this.view;
+		var calendar = view.calendar;
 		var el = seg.el;
 		var event = seg.event;
 		var newStart, newEnd;
@@ -275,17 +276,26 @@ $.extend(Grid.prototype, {
 				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
+				if (calendar.isEventAllowedInRange(event, newStart, res.visibleEnd)) { // allowed to drop here?
+					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();
+					}
 				}
 				else {
+					// have the helper follow the mouse (no snapping) with a warning-style cursor
+					newStart = null; // mark an invalid drop date
 					mouseFollower.show();
+					calendar.disableCursor();
 				}
 			},
 			cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells
-				newStart = null;
+				newStart = null; // mark an invalid drop date
 				view.destroyDrag(); // unrender whatever was done in view.renderDrag
 				mouseFollower.show(); // show in case we are moving out of all cells
+				calendar.enableCursor();
 			},
 			dragStop: function(ev) {
 				var hasChanged = newStart && !newStart.isSame(event.start);
@@ -301,6 +311,8 @@ $.extend(Grid.prototype, {
 						view.eventDrop(el[0], event, newStart, ev); // will rerender all events...
 					}
 				});
+
+				calendar.enableCursor();
 			},
 			listenStop: function() {
 				mouseFollower.stop(); // put in listenStop in case there was a mousedown but the drag never started
@@ -320,6 +332,7 @@ $.extend(Grid.prototype, {
 		var delta;
 		var newStart;
 		var newEnd;
+		var visibleEnd;
 
 		if (dropDate.hasTime() === dragStartDate.hasTime()) {
 			delta = dayishDiff(dropDate, dragStartDate);
@@ -337,7 +350,10 @@ $.extend(Grid.prototype, {
 			newEnd = null; // end should be cleared
 		}
 
-		return { start: newStart, end: newEnd };
+		// compute what the end date would appear to be if there isn't already one
+		visibleEnd = newEnd || view.calendar.getDefaultEventEnd(!dropDate.hasTime(), newStart);
+
+		return { start: newStart, end: newEnd, visibleEnd: visibleEnd };
 	},
 
 
@@ -350,6 +366,7 @@ $.extend(Grid.prototype, {
 	segResizeMousedown: function(seg, ev) {
 		var _this = this;
 		var view = this.view;
+		var calendar = view.calendar;
 		var el = seg.el;
 		var event = seg.event;
 		var start = event.start;
@@ -357,7 +374,7 @@ $.extend(Grid.prototype, {
 		var newEnd = null;
 		var dragListener;
 
-		function destroy() { // resets the rendering
+		function destroy() { // resets the rendering to show the original event
 			_this.destroyResize();
 			view.showEvent(event);
 		}
@@ -378,22 +395,31 @@ $.extend(Grid.prototype, {
 				}
 				newEnd = date.clone().add(_this.cellDuration); // make it an exclusive end
 
-				if (newEnd.isSame(end)) {
-					newEnd = null;
-					destroy();
+				if (calendar.isEventAllowedInRange(event, start, newEnd)) { // allowed to be resized here?
+					if (newEnd.isSame(end)) {
+						newEnd = null; // mark an invalid resize
+						destroy();
+					}
+					else {
+						_this.renderResize(start, newEnd, seg);
+						view.hideEvent(event);
+					}
 				}
 				else {
-					_this.renderResize(start, newEnd, seg);
-					view.hideEvent(event);
+					newEnd = null; // mark an invalid resize
+					destroy();
+					calendar.disableCursor();
 				}
 			},
 			cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells
 				newEnd = null;
 				destroy();
+				calendar.enableCursor();
 			},
 			dragStop: function(ev) {
 				_this.isResizingSeg = false;
 				destroy();
+				calendar.enableCursor();
 				view.trigger('eventResizeStop', el[0], event, ev, {}); // last argument is jqui dummy
 
 				if (newEnd) {

+ 10 - 1
src/common/Grid.js

@@ -84,6 +84,7 @@ $.extend(Grid.prototype, {
 	dayMousedown: function(ev) {
 		var _this = this;
 		var view = this.view;
+		var calendar = view.calendar;
 		var isSelectable = view.opt('selectable');
 		var dates = null; // the inclusive dates of the selection. will be null if no selection
 		var start; // the inclusive start of the selection
@@ -109,13 +110,20 @@ $.extend(Grid.prototype, {
 					end = dates[1].clone().add(_this.cellDuration);
 
 					if (isSelectable) {
-						_this.renderSelection(start, end);
+						if (calendar.isSelectionAllowedInRange(start, end)) { // allowed to select within this range?
+							_this.renderSelection(start, end);
+						}
+						else {
+							dates = null; // flag for an invalid selection
+							calendar.disableCursor();
+						}
 					}
 				}
 			},
 			cellOut: function(cell, date) {
 				dates = null;
 				_this.destroySelection();
+				calendar.enableCursor();
 			},
 			listenStop: function(ev) {
 				if (dates) { // started and ended on a cell?
@@ -127,6 +135,7 @@ $.extend(Grid.prototype, {
 						view.reportSelection(start, end, ev);
 					}
 				}
+				calendar.enableCursor();
 			}
 		});
 

+ 1 - 1
src/common/TimeGrid.js

@@ -46,7 +46,7 @@ $.extend(TimeGrid.prototype, {
 
 
 	renderBusinessHours: function() {
-		var events = this.view.calendar.getBusinessHoursEvents(this.view);
+		var events = this.view.calendar.getBusinessHoursEvents();
 		this.renderFill('businessHours', this.eventsToSegs(events), 'bgevent');
 	},
 

+ 5 - 0
src/common/common.css

@@ -469,6 +469,11 @@ temporary rendered events).
 	cursor: pointer; /* give events with links and draggable events a hand mouse pointer */
 }
 
+.fc-not-allowed, /* causes a "warning" cursor. applied on body */
+.fc-not-allowed .fc-event { /* to override an event's custom cursor */
+	cursor: not-allowed;
+}
+
 
 /* DayGrid events
 ----------------------------------------------------------------------------------------------------