瀏覽代碼

more stuff

Adam Shaw 8 年之前
父節點
當前提交
03c5e2293e

+ 7 - 0
src.json

@@ -43,8 +43,15 @@
     "defaults.js",
     "locale.js",
     "Header.js",
+    "models/UnzonedRange.js",
+    "models/EventRange.js",
+    "models/EventDateProfile.js",
+    "models/EventInstance.js",
     "models/EventDefinition.js",
+    "models/SingleEventDefinition.js",
+    "models/RecurringEventDefinition.js",
     "models/EventDefinitionCollection.js",
+    "models/EventPeriod.js",
     "EventManager.js",
     "basic/BasicView.js",
     "basic/MonthView.js",

+ 32 - 0
src/models/EventDateProfile.js

@@ -0,0 +1,32 @@
+
+var EventDateProfile = Class.extend({
+
+	start: null,
+	end: null,
+
+	constructor: function(start, end) {
+		this.start = start;
+		this.end = end;
+	},
+
+	isAllDay: function() {
+		return !(this.start.hasTime() || (this.end && this.end.hasTime()));
+	},
+
+	// calendar object needed to compute missing end dates
+	buildRange: function(calendar) {
+		var startMs = this.start.clone().stripZone().valueOf();
+		var endMs = (
+				this.end ?
+					this.end.clone() :
+					// derive the end from the start and allDay. compute allDay if necessary
+					calendar.getDefaultEventEnd(
+						this.isAllDay()
+						this.start
+					)
+			).stripZone().valueOf();
+
+		return new UnzonedRange(startMs, endMs);
+	}
+
+});

+ 4 - 59
src/models/EventDefinition.js

@@ -7,6 +7,7 @@ var EventDefinition = Class.extend({
 	rendering: null,
 	miscProps: null,
 
+	// calendar needed for zoned moment instantiation
 	constructor: function(rawProps, source, calendar) {
 		this.source = source;
 		this.id = rawProps.id || ('_fc' + (++EventDefinition.uuid));
@@ -33,67 +34,11 @@ var EventDefinition = Class.extend({
 		return name === 'id' || name === 'title' || name === 'rendering';
 	},
 
-	buildPeriod: function(start, end) {
+	buildInstances: function(start, end) {
+		// subclasses must implement
 	}
-});
-
-EventDefinition.uuid = 0;
-
-
-
-function isEventInputRecurring(eventInput) {
-	var start = eventInput.start || eventInput.date;
-	var end = eventInput.end;
-
-	return eventInput.dow ||
-		(isTimeString(start) || moment.isDuration(start)) ||
-		(end && (isTimeString(start) || moment.isDuration(start)));
-}
-
-
-
-var RecurringEventDefinition = EventDefinition.extend({
-
-	startTime: null,
-	endTime: null,
-	dow: null,
-
-	constructor: function(rawProps, source, calendar) {
-		EventDefinition.apply(this, arguments);
-
-		this.startTime = moment.duration(rawProps.start);
-		this.endTime = rawProps.end ? moment.duration(rawProps.end) : null;
-		this.dow = rawProps.dow;
-	},
-
-	isStandardProp: function(name) {
-		return EventDefinition.prototype.isStandardProp(name) ||
-			name === 'start' || name === 'end' || name === 'dow';
-	},
 
-	buildPeriod: function(start, end) {
-	}
 });
 
+EventDefinition.uuid = 0;
 
-
-var SingleEventDefinition = EventDefinition.extend({
-
-	start: null,
-	end: null,
-
-	constructor: function(rawProps, source, calendar) {
-		EventDefinition.apply(this, arguments);
-
-		this.start = calendar.moment(rawProps.start || rawProps.date); // 'date' is an alias
-		this.end = rawProps.end ? calendar.moment(rawProps.end) : null;
-	},
-
-	isStandardProp: function(name) {
-		return EventDefinition.prototype.isStandardProp(name) ||
-			name === 'start' || name === 'end' || name === 'date'; // 'date' is an alias
-	},
-
-	buildPeriod: function() {
-	}
-});

+ 55 - 5
src/models/EventDefinitionCollection.js

@@ -2,14 +2,17 @@
 var EventDefinitionCollection = Class.extend({
 
 	calendar: null,
-	models: null,
+	eventDefs: null,
+	eventDefsById: null,
 
 	constructor: function(calendar) {
 		this.calendar = calendar;
-		this.models = [];
+		this.eventDefs = [];
+		this.eventDefsById = {};
 	},
 
 	addRaw: function(eventInput, source) {
+		var eventDefsById = this.eventDefsById;
 		var eventDef;
 
 		if (isEventInputRecurring(eventInput)) {
@@ -19,19 +22,66 @@ var EventDefinitionCollection = Class.extend({
 			eventDef = new SingleEventDefinition(eventInput, source, this.calendar);
 		}
 
-		this.models.push(eventDef);
+		this.eventDefs.push(eventDef);
+
+		(eventDefsById[eventDef.id] || (eventDefsById[eventDef.id] = []))
+			.push(eventDef);
 	},
 
 	clear: function() {
-		this.models = [];
+		this.eventDefs = [];
+		this.eventDefsById = {};
 	},
 
 	clearBySource: function() {
-
+		// TODO
 	},
 
 	clearByFilter: function() {
+		// TODO
+	},
+
+	buildPeriods: function(start, end) {
+		var eventPeriods = [];
+		var eventDefsById = this.eventDefsById;
+		var eventId;
+		var relatedEventDefs;
+		var relatedEventInstances;
+		var i, eventDef;
+
+		for (eventId in eventDefsById) {
+			relatedEventDefs = eventDefsById[eventId];
+			relatedEventInstances = [];
+
+			for (i = 0; i < relatedEventDefs.length; i++) {
+				eventDef = relatedEventDefs[i];
 
+				relatedEventInstances.push.apply( // append
+					relatedEventInstances,
+					eventDef.buildInstances(start, end)
+				);
+			}
+
+			eventPeriods.push(
+				new EventPeriod(
+					relatedEventInstances,
+					new UnzonedRange(start, end),
+					this.calendar // for computing event ends
+				)
+			);
+		}
+
+		return eventPeriods;
 	}
 
 });
+
+
+function isEventInputRecurring(eventInput) {
+	var start = eventInput.start || eventInput.date;
+	var end = eventInput.end;
+
+	return eventInput.dow ||
+		(isTimeString(start) || moment.isDuration(start)) ||
+		(end && (isTimeString(start) || moment.isDuration(start)));
+}

+ 12 - 0
src/models/EventInstance.js

@@ -0,0 +1,12 @@
+
+var EventInstance = Class.extend({
+
+	eventDefinition: null,
+	eventDateProfile: null,
+
+	constructor: function(eventDefinition, eventDateProfile) {
+		this.eventDefinition = eventDefinition;
+		this.eventDateProfile = eventDateProfile;
+	}
+
+});

+ 88 - 0
src/models/EventPeriod.js

@@ -0,0 +1,88 @@
+
+var EventPeriod = Class.extend({
+
+	eventInstances: null,
+	constraintRange: null,
+	calendar: null,
+
+	constructor: function(eventInstances, constraintRange, calendar) {
+		this.eventInstances = eventInstances;
+		this.constraintRange = constraintRange;
+		this.calendar = calendar;
+	},
+
+	buildRanges: function() {
+		var eventRanges = [];
+		var eventInstances = this.eventInstances;
+		var i, eventInstance;
+		var eventRange;
+
+		for (i = 0; i < eventInstances.length; i++) {
+			eventInstance = eventInstances[i];
+
+			eventRange = eventInstance.eventDateProfile.buildRange(this.calendar); // merely UnzonedRange here
+			eventRange = new EventRange(eventInstance, range.startMs, range.endMs);
+			eventRange = eventRange.constrainTo(this.constraintRange);
+
+			if (eventRange) {
+				eventRanges.push(eventRange);
+			}
+		}
+
+		return eventRanges;
+	},
+
+	buildRenderRanges: function() {
+		var eventInstances = this.eventInstances;
+		var ranges = this.buildRanges();
+
+		if (eventInstances.length && eventInstances[0].rendering === 'inverse-background') {
+			ranges = invertEventRanges(ranges, this.constraintRange, eventInstances[0]);
+		}
+
+		return ranges;
+	}
+
+});
+
+
+function invertEventRanges(eventRanges, constraintRange, ownerEventInstance) {
+	var inverseRanges = [];
+	var startMs = constraintRange.startMs; // the end of the previous range. the start of the new range
+	var i, eventRange;
+
+	// ranges need to be in order. required for our date-walking algorithm
+	eventRanges.sort(compareUnzonedRanges);
+
+	for (i = 0; i < eventRanges.length; i++) {
+		eventRange = eventRanges[i];
+
+		// add the span of time before the event (if there is any)
+		if (eventRange.startMs > startMs) { // compare millisecond time (skip any ambig logic)
+			inverseRanges.push(
+				new EventRange(
+					ownerEventInstance,
+					startMs,
+					eventRange.startMs
+				)
+			);
+		}
+
+		if (eventRange.endMs > startMs) {
+			startMs = eventRange.endMs;
+		}
+	}
+
+	// add the span of time after the last event (if there is any)
+	if (startMs < constraintRange.endMs) { // compare millisecond time (skip any ambig logic)
+		inverseRanges.push(
+			new EventRange(
+				ownerEventInstance,
+				startMs,
+				constraintRange.endMs
+			)
+		);
+	}
+
+	return inverseRanges;
+}

+ 32 - 0
src/models/EventRange.js

@@ -0,0 +1,32 @@
+
+var EventRange = UnzonedRange.extend({
+
+	eventInstance: null,
+	isStart: true,
+	isEnd: true,
+
+	constructor: function(eventInstance, startMs, endMs) {
+		UnzonedRange.call(this, startMs, endMs);
+
+		this.eventInstance = eventInstance;
+	},
+
+	constrainTo: function(constraintRange) {
+		var plainRange = UnzonedRange.prototype.constrainTo.apply(this, arguments);
+		var eventRange;
+
+		if (plainRange) {
+			eventRange = new EventRange(
+				this.eventInstance,
+				plainRange.startMs,
+				plainRange.endMs
+			);
+
+			eventRange.isStart = this.isStart && (this.startMs === plainRange.startMs);
+			eventRange.isEnd = this.isEnd && (this.endMs === plainRange.endMs);
+
+			return eventRange;
+		}
+	}
+
+});

+ 71 - 0
src/models/RecurringEventDefinition.js

@@ -0,0 +1,71 @@
+
+var RecurringEventDefinition = EventDefinition.extend({
+
+	startTime: null,
+	endTime: null,
+	dowHash: null,
+
+	constructor: function(rawProps, source, calendar) {
+		EventDefinition.apply(this, arguments);
+
+		this.startTime = moment.duration(rawProps.start);
+		this.endTime = rawProps.end ? moment.duration(rawProps.end) : null;
+
+		if (rawProps.dow) {
+			this.dowHash = this.buildDowHash(rawProps.dow);
+		}
+	},
+
+	isStandardProp: function(name) {
+		return EventDefinition.prototype.isStandardProp(name) ||
+			name === 'start' || name === 'end' || name === 'dow';
+	},
+
+	buildInstances: function(start, end) {
+		var date = start.clone();
+		var instanceStart, instanceEnd;
+		var eventInstances = [];
+
+		while (date.isBefore(end)) {
+
+			// if everyday, or this particular day-of-week
+			if (!this.dowHash || this.dowHash[date.day()]) {
+
+				instanceStart = date.clone();
+				instanceEnd = null;
+
+				if (this.startTime) {
+					instanceStart.time(this.startTime);
+				}
+
+				if (this.endTime) {
+					instanceEnd = date.clone().time(this.endTime);
+				}
+
+				eventInstances.push(
+					new EventInstance(
+						this, // definition
+						new EventDateProfile(instanceStart, instanceEnd)
+					)
+				);
+			}
+
+			date.add(1, 'days');
+		}
+
+		return eventInstances;
+	},
+
+	buildDowHash: function(dowArray) {
+		var dowHash = {};
+		var i;
+
+		// make a boolean hash as to whether the event occurs on each day-of-week
+		for (i = 0; i < dowArray.length; i++) {
+			dowHash[dowArray[i]] = true;
+		}
+
+		return dowHash;
+	}
+
+});

+ 28 - 0
src/models/SingleEventDefinition.js

@@ -0,0 +1,28 @@
+
+var SingleEventDefinition = EventDefinition.extend({
+
+	start: null,
+	end: null,
+
+	constructor: function(rawProps, source, calendar) {
+		EventDefinition.apply(this, arguments);
+
+		this.start = calendar.moment(rawProps.start || rawProps.date); // 'date' is an alias
+		this.end = rawProps.end ? calendar.moment(rawProps.end) : null;
+	},
+
+	isStandardProp: function(name) {
+		return EventDefinition.prototype.isStandardProp(name) ||
+			name === 'start' || name === 'end' || name === 'date'; // 'date' is an alias
+	},
+
+	buildInstances: function() { // disregards start/end
+		return [
+			new EventInstance(
+				this, // definition
+				new EventDateProfile(this.start, this.end)
+			)
+		];
+	}
+
+});

+ 43 - 0
src/models/UnzonedRange.js

@@ -0,0 +1,43 @@
+
+var UnzonedRange = Class.extend({
+
+	startMs: null,
+	endMs: null,
+
+	constructor: function(startInput, endInput) {
+
+		if (moment.isMoment(startInput)) {
+			startInput = startInput.clone().stripZone();
+		}
+
+		if (moment.isMoment(endInput)) {
+			endInput = endInput.clone().stripZone();
+		}
+
+		this.startMs = startInput.valueOf();
+		this.endMs = endInput.valueOf();
+	},
+
+	constrainTo: function(constraintRange) {
+		var startMs = Math.max(this.startMs, constraintRange.startMs);
+		var endMs = Math.min(this.endMs, constraintRange.endMs);
+
+		if (startMs < endMs) {
+			return new UnzonedRange(startMs, endMs);
+		}
+	},
+
+	getStart: function() {
+		return FC.moment.utc(this.startMs).stripZone();
+	},
+
+	getEnd: function() {
+		return FC.moment.utc(this.endMs).stripZone();
+	}
+
+});
+
+
+function compareUnzonedRanges(range1, range2) {
+	return range1.startMs - range2.startMs; // earlier ranges go first
+}