ソースを参照

full working constraint/overlap system

Adam Shaw 8 年 前
コミット
e3066318e3
4 ファイル変更259 行追加255 行削除
  1. 0 31
      src/Calendar.business.js
  2. 255 30
      src/Calendar.constraints.js
  3. 0 194
      src/EventManager.js
  4. 4 0
      src/models/ComponentFootprint.js

+ 0 - 31
src/Calendar.business.js

@@ -25,37 +25,6 @@ Calendar.prototype.buildCurrentBusinessFootprints = function(wholeDay) {
 };
 
 
-// doesn't consider resources :(
-// this is not only used for business hours
-Calendar.prototype.eventInstanceGroupToFootprints = function(eventInstanceGroup) {
-	var activeRange = this.getView().activeRange;
-
-	var eventRanges = eventInstanceGroup.buildEventRanges(
-		new UnzonedRange(activeRange.start, activeRange.end),
-		this // calendar
-	);
-
-	var i, eventRange;
-	var footprints = [];
-
-	for (i = 0; i < eventRanges.length; i++) {
-		eventRange = eventRanges[i];
-
-		footprints.push(
-			new EventFootprint( // TODO: DRY. also in Grid.event.js
-				eventRange.eventInstance,
-				new ComponentFootprint(
-					eventRange.dateRange,
-					eventRange.eventInstance.eventDateProfile.isAllDay()
-				)
-			)
-		);
-	}
-
-	return footprints;
-};
-
-
 // Given a raw input value from options, return events objects for business hours within the current view.
 Calendar.prototype.buildBusinessGroup = function(wholeDay, input, rangeStart, rangeEnd) {
 	if (input === true) {

+ 255 - 30
src/Calendar.constraints.js

@@ -1,3 +1,7 @@
+/*
+TODO: caching. dont want to regenerate all these ranges
+twice if there are two events being dragged at once.
+*/
 
 Calendar.prototype.isEventFootprintAllowed = function(eventFootprint) {
 	var eventDef = eventFootprint.eventInstance.eventDefinition;
@@ -5,6 +9,7 @@ Calendar.prototype.isEventFootprintAllowed = function(eventFootprint) {
 	var constraintVal;
 	var overlapVal;
 
+	// TODO: use EventDef
 	constraintVal = eventDef.constraint;
 	if (constraintVal == null) {
 		constraintVal = source.constraint;
@@ -13,6 +18,7 @@ Calendar.prototype.isEventFootprintAllowed = function(eventFootprint) {
 		}
 	}
 
+	// TODO: use EventDef
 	overlapVal = eventDef.overlap;
 	if (overlapVal == null) {
 		overlapVal = source.overlap;
@@ -25,21 +31,23 @@ Calendar.prototype.isEventFootprintAllowed = function(eventFootprint) {
 		eventFootprint.componentFootprint,
 		constraintVal,
 		overlapVal,
-		eventDef
+		eventFootprint.eventInstance
 	);
 };
 
 
 Calendar.prototype.isSelectionFootprintAllowed = function(componentFootprint) {
-	var selectAllowFunc = this.opt('selectAllow');
+	var selectAllowFunc;
 
 	if (this.isFootprintAllowed(
 		componentFootprint,
 		this.opt('selectConstraint'),
 		this.opt('selectOverlap')
 	)) {
+		selectAllowFunc = this.opt('selectAllow');
+
 		if (selectAllowFunc) {
-			return selectAllowFunc(selectAllowFunc) !== false;
+			return selectAllowFunc(componentFootprint.toLegacy()) !== false;
 		}
 		else {
 			return true;
@@ -50,64 +58,281 @@ Calendar.prototype.isSelectionFootprintAllowed = function(componentFootprint) {
 };
 
 
-Calendar.prototype.isFootprintAllowed = function(componentFootprint, constraintVal, overlapVal, subjectEventDef) {
+Calendar.prototype.isFootprintAllowed = function(
+	componentFootprint,
+	constraintVal,
+	overlapVal,
+	subjectEventInstance
+) {
+	var constraintFootprints; // ComponentFootprint[]
+	var overlapEventFootprints; // EventFootprint[]
+
+	if (constraintVal != null) {
+		constraintFootprints = this.constraintValToFootprints(constraintVal);
 
-	console.log('constraint', this.constraintValToFootprints(constraintVal));
+		if (!this.isFootprintWithinConstraints(componentFootprint, constraintFootprints)) {
+			return false;
+		}
+	}
+
+	overlapEventFootprints = this.getOverlappingEventFootprints(componentFootprint, subjectEventInstance);
+
+	if (overlapVal === false) {
+		if (overlapEventFootprints.length) {
+			return false;
+		}
+	}
+	else if (typeof overlapVal === 'function') {
+		if (!isOverlapsAllowedByFunc(overlapEventFootprints, overlapVal, subjectEventInstance)) {
+			return false;
+		}
+	}
+	else if (subjectEventInstance) {
+		if (!isOverlapEventInstancesAllowed(overlapEventFootprints, subjectEventInstance)) {
+			return false;
+		}
+	}
 
 	return true;
 };
 
 
+// Constraint
+// ------------------------------------------------------------------------------------------------
+
+
+Calendar.prototype.isFootprintWithinConstraints = function(componentFootprint, constraintFootprints) {
+	var i;
+
+	for (i = 0; i < constraintFootprints.length; i++) {
+		if (this.footprintContainsFootprint(
+			constraintFootprints[i],
+			componentFootprint
+		)) {
+			return true;
+		}
+	}
+
+	return false;
+};
+
+
 Calendar.prototype.constraintValToFootprints = function(constraintVal) {
+	var eventDefs;
+	var eventDef;
+	var eventInstances;
+	var eventRanges;
+	var eventFootprints;
 
 	if (constraintVal === 'businessHours') {
-		return this.buildCurrentBusinessFootprints();
+
+		eventFootprints = this.buildCurrentBusinessFootprints();
+
+		return eventFootprintsToComponentFootprints(eventFootprints);
 	}
+	else if (typeof constraintVal === 'object') {
+
+		eventDef = parseEventInput(constraintVal, this);
+		eventInstances = this.eventDefToInstances(eventDef);
+		eventRanges = this.eventInstancesToEventRanges(eventInstances);
+		eventFootprints = this.eventRangesToEventFootprints(eventRanges);
 
-	if (typeof constraintVal === 'object') {
-		return this.eventInputToFootprints(constraintVal);
+		return eventFootprintsToComponentFootprints(eventFootprints);
 	}
+	else if (constraintVal != null) { // an ID
 
-	return this.eventIdToFootprints(constraintVal);
+		eventDefs = this.eventDefCollection.getById(constraintVal);
+		eventInstances = this.eventDefsToInstances(eventDefs);
+		eventRanges = this.eventInstancesToEventRanges(eventInstances);
+		eventFootprints = this.eventRangesToEventFootprints(eventRanges);
+
+		return eventFootprintsToComponentFootprints(eventFootprints);
+	}
+
+	return [];
 };
 
 
-Calendar.prototype.eventInputToFootprints = function(eventInput) {
-	var activeRange = this.getView().activeRange;
-	var eventDef;
-	var eventInstances;
-	var eventInstanceGroup;
+// Overlap
+// ------------------------------------------------------------------------------------------------
+
+
+Calendar.prototype.getOverlappingEventFootprints = function(componentFootprint, subjectEventInstance) {
+	var peerEventFootprints = this.getPeerEventFootprints(componentFootprint, subjectEventInstance);
+	var overlapEventFootprints = [];
+	var i;
 
-	if (eventInput.start == null) {
-		return []; // invalid
+	for (i = 0; i < peerEventFootprints.length; i++) {
+		if (this.footprintsIntersect(
+			componentFootprint,
+			peerEventFootprints[i].componentFootprint
+		)) {
+			overlapEventFootprints.push(peerEventFootprints[i]);
+		}
 	}
 
-	eventDef = parseEventInput(eventInput, this);
-	eventInstances = eventDef.buildInstances(activeRange.start, activeRange.end);
-	eventInstanceGroup = new EventInstanceGroup(eventInstances);
+	return overlapEventFootprints;
+};
+
+
+Calendar.prototype.getPeerEventFootprints = function(componentFootprint, subjectEventInstance) {
+	var peerEventDefs = subjectEventInstance ?
+		this.getUnrelatedEventDefs(subjectEventInstance.eventDefinition) :
+		this.eventDefCollection.eventDefs.slice(); // all. clone
 
-	return this.eventInstanceGroupToFootprints(eventInstanceGroup);
+	var peerEventInstances = this.eventDefsToInstances(peerEventDefs);
+	var peerEventRanges = this.eventInstancesToEventRanges(peerEventInstances);
+
+	return this.eventRangesToEventFootprints(peerEventRanges);
 };
 
 
-Calendar.prototype.eventIdToFootprints = function(eventId) {
-	var activeRange = this.getView().activeRange;
-	var eventDefs = this.eventDefCollection.getById(eventId);
-	var eventInstances = [];
-	var eventInstanceGroup;
+Calendar.prototype.getUnrelatedEventDefs = function(subjectEventDef) {
+	var eventDefs = this.eventDefCollection.eventDefs;
+	var i, eventDef;
+	var unrelated = [];
+
+	for (i = 0; i < eventDefs.length; i++) {
+		eventDef = eventDefs[i];
+
+		if (eventDef.id !== subjectEventDef.id) {
+			unrelated.push(eventDef);
+		}
+	}
+
+	return unrelated;
+};
+
+
+function isOverlapsAllowedByFunc(overlapEventFootprints, overlapFunc, subjectEventInstance) {
 	var i;
 
-	if (!eventDefs) {
-		return []; // invalid
+	for (i = 0; i < overlapEventFootprints.length; i++) {
+		if (!overlapFunc(
+			overlapEventFootprints[i].eventInstance.toLegacy(),
+			subjectEventInstance ? subjectEventInstance.toLegacy() : null
+		)) {
+			return false;
+		}
+	}
+
+	return true;
+}
+
+
+function isOverlapEventInstancesAllowed(overlapEventFootprints, subjectEventInstance) {
+	var i;
+	var overlapEventFootprint;
+	var overlapEventInstance;
+	var overlapEventDef;
+	var overlapVal;
+
+	for (i = 0; i < overlapEventFootprints.length; i++) {
+		overlapEventFootprint = overlapEventFootprints[i];
+		overlapEventInstance = overlapEventFootprint.eventInstance;
+		overlapEventDef = overlapEventInstance.eventDefinition;
+
+		// TODO: use EventDef
+		overlapVal = overlapEventDef.overlap;
+		if (overlapVal == null) {
+			if (overlapEventDef.source) {
+				overlapVal = overlapEventDef.source.overlap;
+			}
+		}
+
+		if (overlapVal === false) {
+			return false;
+		}
+		else if (typeof overlapVal === 'function') {
+			if (!overlapVal(
+				subjectEventInstance.toLegacy(),
+				overlapEventInstance.toLegacy()
+			)) {
+				return false;
+			}
+		}
 	}
 
+	return true;
+}
+
+
+// Conversion: eventDefs -> eventInstances -> eventRanges -> eventFootprints -> componentFootprints
+// ------------------------------------------------------------------------------------------------
+
+
+Calendar.prototype.eventDefsToInstances = function(eventDefs) {
+	var eventInstances = [];
+	var i;
+
 	for (i = 0; i < eventDefs.length; i++) {
 		eventInstances.push.apply(eventInstances, // append
-			eventDefs[i].buildInstances(activeRange.start, activeRange.end)
+			this.eventDefToInstances(eventDefs[i])
 		);
 	}
 
-	eventInstanceGroup = new EventInstanceGroup(eventInstances);
+	return eventInstances;
+};
+
+
+Calendar.prototype.eventDefToInstances = function(eventDef) {
+	var activeRange = this.getView().activeRange;
+
+	return eventDef.buildInstances(activeRange.start, activeRange.end);
+};
+
+
+Calendar.prototype.eventInstancesToEventRanges = function(eventInstances) {
+	var group = new EventInstanceGroup(eventInstances);
+	var activeRange = this.getView().activeRange;
+
+	return group.buildEventRanges(
+		new UnzonedRange(activeRange.start, activeRange.end),
+		this // calendar
+	);
+};
+
+
+Calendar.prototype.eventRangesToEventFootprints = function(eventRanges) {
+	var _this = this;
+
+	return eventRanges.map(function(eventRange) {
+		return _this.eventRangeToEventFootprint(eventRange);
+	});
+};
+
+
+Calendar.prototype.eventRangeToEventFootprint = function(eventRange) {
+	return new EventFootprint( // TODO: DRY. also in Grid.event.js
+		eventRange.eventInstance,
+		new ComponentFootprint(
+			eventRange.dateRange,
+			eventRange.eventInstance.eventDateProfile.isAllDay()
+		)
+	);
+};
+
+
+function eventFootprintsToComponentFootprints(eventFootprints) {
+	return eventFootprints.map(function(eventFootprint) {
+		return eventFootprint.componentFootprint;
+	});
+}
+
+
+// Footprint Utils
+// ----------------------------------------------------------------------------------------
+
+
+Calendar.prototype.footprintContainsFootprint = function(outerFootprint, innerFootprint) {
+	// TODO: use date range utils
+	return innerFootprint.dateRange.startMs >= outerFootprint.dateRange.startMs &&
+		innerFootprint.dateRange.endMs <= outerFootprint.dateRange.endMs;
+};
+
 
-	return this.eventInstanceGroupToFootprints(eventInstanceGroup);
+Calendar.prototype.footprintsIntersect = function(footprint0, footprint1) {
+	// TODO: use date range utils
+	return footprint0.dateRange.startMs < footprint1.dateRange.endMs &&
+		footprint0.dateRange.endMs > footprint1.dateRange.startMs;
 };

+ 0 - 194
src/EventManager.js

@@ -1192,37 +1192,6 @@ Calendar.prototype.normalizeEvent = function(event) {
 };
 
 
-// Does the given span (start, end, and other location information)
-// fully contain the other?
-Calendar.prototype.spanContainsSpan = function(outerSpan, innerSpan) {
-	var eventStart = outerSpan.start.clone().stripZone();
-	var eventEnd = this.getEventEnd(outerSpan).stripZone();
-
-	return innerSpan.start >= eventStart && innerSpan.end <= eventEnd;
-};
-
-
-// Returns a list of events that the given event should be compared against when being considered for a move to
-// 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;
-
-	for (i = 0; i < cache.length; i++) {
-		otherEvent = cache[i];
-		if (
-			!event ||
-			event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events
-		) {
-			peerEvents.push(otherEvent);
-		}
-	}
-
-	return peerEvents;
-};
-
-
 // updates the "backup" properties, which are preserved in order to compute diffs later on.
 function backupEventDates(event) {
 	event._allDay = event.allDay;
@@ -1253,166 +1222,3 @@ Calendar.prototype.buildMutatedEventInstanceGroup = function(eventId, eventMutat
 
 	return new EventInstanceGroup(allInstances);
 };
-
-
-/* Overlapping / Constraining
------------------------------------------------------------------------------------------*/
-
-
-// Determines if the given event can be relocated to the given span (unzoned start/end with other misc data)
-Calendar.prototype.isEventSpanAllowed = function(span, event) {
-	var source = event.source || {};
-	var eventAllowFunc = this.opt('eventAllow');
-
-	var constraint = firstDefined(
-		event.constraint,
-		source.constraint,
-		this.opt('eventConstraint')
-	);
-
-	var overlap = firstDefined(
-		event.overlap,
-		source.overlap,
-		this.opt('eventOverlap')
-	);
-
-	return this.isSpanAllowed(span, constraint, overlap, event) &&
-		(!eventAllowFunc || eventAllowFunc(span, event) !== false);
-};
-
-
-// Determines if an external event can be relocated to the given span (unzoned start/end with other misc data)
-Calendar.prototype.isExternalSpanAllowed = function(eventSpan, eventLocation, eventProps) {
-	var eventInput;
-	var event;
-
-	// note: very similar logic is in View's reportExternalDrop
-	if (eventProps) {
-		eventInput = $.extend({}, eventProps, eventLocation);
-		event = this.expandEvent(
-			this.buildEventFromInput(eventInput)
-		)[0];
-	}
-
-	if (event) {
-		return this.isEventSpanAllowed(eventSpan, event);
-	}
-	else { // treat it as a selection
-
-		return this.isSelectionSpanAllowed(eventSpan);
-	}
-};
-
-
-// Determines the given span (unzoned start/end with other misc data) can be selected.
-Calendar.prototype.isSelectionSpanAllowed = function(span) {
-	var selectAllowFunc = this.opt('selectAllow');
-
-	return this.isSpanAllowed(span, this.opt('selectConstraint'), this.opt('selectOverlap')) &&
-		(!selectAllowFunc || selectAllowFunc(span) !== false);
-};
-
-
-// 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.
-Calendar.prototype.isSpanAllowed = function(span, constraint, overlap, event) {
-	var constraintEvents;
-	var anyContainment;
-	var peerEvents;
-	var i, peerEvent;
-	var peerOverlap;
-
-	// the range must be fully contained by at least one of produced constraint events
-	if (constraint != null) {
-
-		// not treated as an event! intermediate data structure
-		// TODO: use ranges in the future
-		constraintEvents = this.constraintToEvents(constraint);
-		if (constraintEvents) { // not invalid
-
-			anyContainment = false;
-			for (i = 0; i < constraintEvents.length; i++) {
-				if (this.spanContainsSpan(constraintEvents[i], span)) {
-					anyContainment = true;
-					break;
-				}
-			}
-
-			if (!anyContainment) {
-				return false;
-			}
-		}
-	}
-
-	peerEvents = this.getPeerEvents(span, event);
-
-	for (i = 0; i < peerEvents.length; i++)  {
-		peerEvent = peerEvents[i];
-
-		// there needs to be an actual intersection before disallowing anything
-		if (this.eventIntersectsRange(peerEvent, span)) {
-
-			// evaluate overlap for the given range and short-circuit if necessary
-			if (overlap === false) {
-				return false;
-			}
-			// if the event's overlap is a test function, pass the peer event in question as the first param
-			else if (typeof overlap === 'function' && !overlap(peerEvent, event)) {
-				return false;
-			}
-
-			// if we are computing if the given range is allowable for an event, consider the other event's
-			// EventObject-specific or Source-specific `overlap` property
-			if (event) {
-				peerOverlap = firstDefined(
-					peerEvent.overlap,
-					(peerEvent.source || {}).overlap
-					// we already considered the global `eventOverlap`
-				);
-				if (peerOverlap === false) {
-					return false;
-				}
-				// if the peer event's overlap is a test function, pass the subject event as the first param
-				if (typeof peerOverlap === 'function' && !peerOverlap(event, peerEvent)) {
-					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)
-Calendar.prototype.constraintToEvents = function(constraintInput) {
-
-	if (constraintInput === 'businessHours') {
-		return this.getCurrentBusinessHourEvents();
-	}
-
-	if (typeof constraintInput === 'object') {
-		if (constraintInput.start != null) { // needs to be event-like input
-			return this.expandEvent(this.buildEventFromInput(constraintInput));
-		}
-		else {
-			return null; // invalid
-		}
-	}
-
-	return this.clientEvents(constraintInput); // probably an ID
-};
-
-
-// Does the event's date range intersect with the given range?
-// start/end already assumed to have stripped zones :(
-Calendar.prototype.eventIntersectsRange = function(event, range) {
-	var eventStart = event.start.clone().stripZone();
-	var eventEnd = this.getEventEnd(event).stripZone();
-
-	return range.start < eventEnd && range.end > eventStart;
-};

+ 4 - 0
src/models/ComponentFootprint.js

@@ -7,6 +7,10 @@ var ComponentFootprint = Class.extend({
 	constructor: function(dateRange, isAllDay) {
 		this.dateRange = dateRange;
 		this.isAllDay = isAllDay;
+	},
+
+	toLegacy: function() {
+		return this.dateRange.getRange();
 	}
 
 });