Forráskód Böngészése

fixed a bunch of moment/timezone issues

Adam Shaw 11 éve
szülő
commit
300670da43
5 módosított fájl, 262 hozzáadás és 92 törlés
  1. 1 0
      bower.json
  2. 7 2
      src/Calendar.js
  3. 126 89
      src/moment-ext.js
  4. 2 1
      src/util.js
  5. 126 0
      tests/issue_2154.html

+ 1 - 0
bower.json

@@ -3,6 +3,7 @@
   "version": "2.0.0-beta2",
   "devDependencies": {
     "moment": "2.6.0",
+    "moment-timezone": "~0.0.6",
     "jquery": "~1.10.2",
     "jquery-ui": "~1.10.3",
     "jquery-simulate-ext": "~1.3.0",

+ 7 - 2
src/Calendar.js

@@ -103,12 +103,17 @@ function Calendar(element, instanceOptions) {
 
 		if (options.timezone === 'local') {
 			mom = fc.moment.apply(null, arguments);
+
+			// Force the moment to be local, because fc.moment doesn't guarantee it.
+			if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone
+				mom.local();
+			}
 		}
 		else if (options.timezone === 'UTC') {
-			mom = fc.moment.utc.apply(null, arguments);
+			mom = fc.moment.utc.apply(null, arguments); // process as UTC
 		}
 		else {
-			mom = fc.moment.parseZone.apply(null, arguments);
+			mom = fc.moment.parseZone.apply(null, arguments); // let the input decide the zone
 		}
 
 		mom._lang = langData;

+ 126 - 89
src/moment-ext.js

@@ -6,99 +6,119 @@ var ambigTimeOrZoneRegex = /^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d
 // Creating
 // -------------------------------------------------------------------------------------------------
 
-// Creates a moment in the local timezone, similar to the vanilla moment(...) constructor,
-// but with extra features:
-// - ambiguous times
-// - enhanced formatting
+// Creates a new moment, similar to the vanilla moment(...) constructor, but with
+// extra features (ambiguous time, enhanced formatting). When gived an existing moment,
+// it will function as a clone (and retain the zone of the moment). Anything else will
+// result in a moment in the local zone.
 fc.moment = function() {
 	return makeMoment(arguments);
 };
 
-// Sames as fc.moment, but creates a moment in the UTC timezone.
+// Sames as fc.moment, but forces the resulting moment to be in the UTC timezone.
 fc.moment.utc = function() {
-	return makeMoment(arguments, true);
+	var mom = makeMoment(arguments, true);
+
+	// Force it into UTC because makeMoment doesn't guarantee it.
+	if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone
+		mom.utc();
+	}
+
+	return mom;
 };
 
-// Creates a moment and preserves the timezone offset of the ISO8601 string,
-// allowing for ambigous timezones. If the string is not an ISO8601 string,
-// the moment is processed in UTC-mode (a departure from moment's method).
+// Same as fc.moment, but when given an ISO8601 string, the timezone offset is preserved.
+// ISO8601 strings with no timezone offset will become ambiguously zoned.
 fc.moment.parseZone = function() {
 	return makeMoment(arguments, true, true);
 };
 
-// when parseZone==true, if can't figure it out, fall back to parseUTC
-function makeMoment(args, parseUTC, parseZone) {
+// Builds an FCMoment from args. When given an existing moment, it clones. When given a native
+// Date, or called with no arguments (the current time), the resulting moment will be local.
+// Anything else needs to be "parsed" (a string or an array), and will be affected by:
+//    parseAsUTC - if there is no zone information, should we parse the input in UTC?
+//    parseZone - if there is zone information, should we force the zone of the moment?
+function makeMoment(args, parseAsUTC, parseZone) {
 	var input = args[0];
 	var isSingleString = args.length == 1 && typeof input === 'string';
-	var isAmbigTime = false;
-	var isAmbigZone = false;
+	var isAmbigTime;
+	var isAmbigZone;
 	var ambigMatch;
-	var mom;
-
-	if (isSingleString) {
-		if (ambigDateOfMonthRegex.test(input)) {
-			// accept strings like '2014-05', but convert to the first of the month
-			input += '-01';
-			args = [ input ]; // for when pass it on to moment's constructor
-			isAmbigTime = true;
-			isAmbigZone = true;
-		}
-		else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) {
-			isAmbigTime = !ambigMatch[5]; // no time part?
-			isAmbigZone = true;
-		}
-	}
-	else if ($.isArray(input)) {
-		// arrays have no timezone information, so assume ambiguous zone
-		isAmbigZone = true;
-	}
-
-	// instantiate a vanilla moment
-	if (parseUTC || parseZone || isAmbigTime) {
-		mom = moment.utc.apply(moment, args);
-	}
-	else {
-		mom = moment.apply(null, args);
-	}
+	var output; // an object with fields for the new FCMoment object
 
 	if (moment.isMoment(input)) {
-		transferAmbigs(input, mom);
-	}
+		output = moment.apply(null, args); // clone it
 
-	if (isAmbigTime) {
-		mom._ambigTime = true;
-		mom._ambigZone = true; // if ambiguous time, also ambiguous timezone offset
+		// the ambig properties have not been preserved in the clone, so reassign them
+		if (input._ambigTime) {
+			output._ambigTime = true;
+		}
+		if (input._ambigZone) {
+			output._ambigZone = true;
+		}
 	}
+	else if (isNativeDate(input) || input === undefined) {
+		output = moment.apply(null, args); // will be local
+	}
+	else { // "parsing" is required
+		isAmbigTime = false;
+		isAmbigZone = false;
+
+		if (isSingleString) {
+			if (ambigDateOfMonthRegex.test(input)) {
+				// accept strings like '2014-05', but convert to the first of the month
+				input += '-01';
+				args = [ input ]; // for when we pass it on to moment's constructor
+				isAmbigTime = true;
+				isAmbigZone = true;
+			}
+			else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) {
+				isAmbigTime = !ambigMatch[5]; // no time part?
+				isAmbigZone = true;
+			}
+		}
+		else if ($.isArray(input)) {
+			// arrays have no timezone information, so assume ambiguous zone
+			isAmbigZone = true;
+		}
+		// otherwise, probably a string with a format
 
-	if (parseZone) {
-		if (isAmbigZone) {
-			mom._ambigZone = true;
+		if (parseAsUTC) {
+			output = moment.utc.apply(moment, args);
+		}
+		else {
+			output = moment.apply(null, args);
 		}
-		else if (isSingleString) {
-			mom.zone(input); // if fails, will set it to 0, which it already was
+
+		if (isAmbigTime) {
+			output._ambigTime = true;
+			output._ambigZone = true; // ambiguous time always means ambiguous zone
 		}
-		else if (isNativeDate(input) || input === undefined) {
-			// native Date object?
-			// specified with no arguments?
-			// then consider the moment to be local
-			mom.local();
+		else if (parseZone) { // let's record the inputted zone somehow
+			if (isAmbigZone) {
+				output._ambigZone = true;
+			}
+			else if (isSingleString) {
+				output.zone(input); // if not a valid zone, will assign UTC
+			}
 		}
 	}
 
-	return new FCMoment(mom);
+	return new FCMoment(output);
 }
 
-// our subclass of Moment.
-// accepts an object with the internal Moment properties that should be copied over to
-// this object (most likely another Moment object).
-function FCMoment(config) {
-	extend(this, config);
+// Our subclass of Moment.
+// Accepts an object with the internal Moment properties that should be copied over to
+// `this` object (most likely another Moment object). The values in this data must not
+// be referenced by anything else (two moments sharing a Date object for example).
+function FCMoment(internalData) {
+	extend(this, internalData);
 }
 
-// chain the prototype to Moment's
+// Chain the prototype to Moment's
 FCMoment.prototype = createObject(moment.fn);
 
-// we need this because Moment's implementation will not copy over the ambig flags
+// We need this because Moment's implementation won't create an FCMoment,
+// nor will it copy over the ambig flags.
 FCMoment.prototype.clone = function() {
 	return makeMoment([ this ]);
 };
@@ -175,11 +195,9 @@ FCMoment.prototype.hasTime = function() {
 // timezone offset when .format() is called.
 FCMoment.prototype.stripZone = function() {
 	var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array
+	var wasAmbigTime = this._ambigTime;
 
-	// set the internal UTC flag
-	moment.fn.utc.call(this); // call the original method, because we don't want to affect _ambigZone
-
-	this._ambigZone = true;
+	moment.fn.utc.call(this); // set the internal UTC flag
 
 	this.year(a[0]) // TODO: find a way to do this in one shot
 		.month(a[1])
@@ -189,6 +207,15 @@ FCMoment.prototype.stripZone = function() {
 		.seconds(a[5])
 		.milliseconds(a[6]);
 
+	if (wasAmbigTime) {
+		// the above call to .utc()/.zone() unfortunately clears the ambig flags, so reassign
+		this._ambigTime = true;
+	}
+
+	// Mark the zone as ambiguous. This needs to happen after the .utc() call, which calls .zone(), which
+	// clears all ambig flags. Same concept with the .year/month/date calls in the case of moment-timezone.
+	this._ambigZone = true;
+
 	return this; // for chaining
 };
 
@@ -199,23 +226,50 @@ FCMoment.prototype.hasZone = function() {
 
 // this method implicitly marks a zone
 FCMoment.prototype.zone = function(tzo) {
+
 	if (tzo != null) {
+		// FYI, the delete statements need to be before the .zone() call or else chaos ensues
+		// for reasons I don't understand. 
+		delete this._ambigTime;
 		delete this._ambigZone;
 	}
+
 	return moment.fn.zone.apply(this, arguments);
 };
 
-// this method implicitly marks a zone.
-// we don't need this, because .local internally calls .zone, but we don't want to depend on that.
+// this method implicitly marks a zone
 FCMoment.prototype.local = function() {
+	var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array
+	var wasAmbigZone = this._ambigZone;
+
+	// will happen anyway via .local()/.zone(), but don't want to rely on internal implementation
+	delete this._ambigTime;
 	delete this._ambigZone;
-	return moment.fn.local.apply(this, arguments);
+
+	moment.fn.local.apply(this, arguments);
+
+	if (wasAmbigZone) {
+		// If the moment was ambiguously zoned, the date fields were stored as UTC.
+		// We want to preserve these, but in local time.
+		this.year(a[0]) // TODO: find a way to do this in one shot
+			.month(a[1])
+			.date(a[2])
+			.hours(a[3])
+			.minutes(a[4])
+			.seconds(a[5])
+			.milliseconds(a[6]);
+	}
+
+	return this; // for chaining
 };
 
-// this method implicitly marks a zone.
-// we don't need this, because .utc internally calls .zone, but we don't want to depend on that.
+// this method implicitly marks a zone
 FCMoment.prototype.utc = function() {
+
+	// will happen anyway via .local()/.zone(), but don't want to rely on internal implementation
+	delete this._ambigTime;
 	delete this._ambigZone;
+
 	return moment.fn.utc.apply(this, arguments);
 };
 
@@ -272,23 +326,6 @@ $.each([
 // Misc Internals
 // -------------------------------------------------------------------------------------------------
 
-// transfers our internal _ambig properties from one moment to another
-function transferAmbigs(src, dest) {
-	if (src._ambigTime) {
-		dest._ambigTime = true;
-	}
-	else if (dest._ambigTime) {
-		delete dest._ambigTime;
-	}
-
-	if (src._ambigZone) {
-		dest._ambigZone = true;
-	}
-	else if (dest._ambigZone) {
-		delete dest._ambigZone;
-	}
-}
-
 // given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated.
 // for example, of one moment has ambig time, but not others, all moments will have their time stripped.
 function commonlyAmbiguate(inputs) {

+ 2 - 1
src/util.js

@@ -11,7 +11,8 @@ function createObject(proto) {
 	return new f();
 }
 
-// copy specifically-owned (non-protoype) properties of `b` onto `a`
+// Copies specifically-owned (non-protoype) properties of `b` onto `a`.
+// FYI, $.extend would copy *all* properties of `b` onto `a`.
 function extend(a, b) {
 	for (var i in b) {
 		if (b.hasOwnProperty(i)) {

+ 126 - 0
tests/issue_2154.html

@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset='utf-8' />
+<link href='../build/out/fullcalendar.css' rel='stylesheet' />
+<link href='../build/out/fullcalendar.print.css' rel='stylesheet' media='print' />
+<script src='../lib/moment/moment.js'></script>
+<script src='../lib/moment-timezone/moment-timezone.js'></script>
+<script src='../lib/jquery/jquery.js'></script>
+<script src='../lib/jquery-ui/ui/jquery-ui.js'></script>
+<script src='../build/out/fullcalendar.js'></script>
+<script>
+
+	//moment-timezone-data
+	moment.tz.add({
+	    "zones": {
+	        "CET": [
+	            "1 C-Eur CE%sT"
+	        ],
+	        "Etc/GMT": [
+	            "0 - GMT"
+	        ],
+	        "Etc/UTC": [
+	            "0 - UTC"
+	        ],
+	        "Europe/Berlin": [
+	            "0:53:28 - LMT 1893_3 0:53:28",
+	            "1 C-Eur CE%sT 1945_4_24_2 2",
+	            "1 SovietZone CE%sT 1946 1",
+	            "1 Germany CE%sT 1980 1",
+	            "1 EU CE%sT"
+	        ]
+	    },
+	    "rules": {
+	        "C-Eur": [
+	            "1916 1916 3 30 7 23 0 1 S",
+	            "1916 1916 9 1 7 1 0 0",
+	            "1917 1918 3 15 1 2 2 1 S",
+	            "1917 1918 8 15 1 2 2 0",
+	            "1940 1940 3 1 7 2 2 1 S",
+	            "1942 1942 10 2 7 2 2 0",
+	            "1943 1943 2 29 7 2 2 1 S",
+	            "1943 1943 9 4 7 2 2 0",
+	            "1944 1945 3 1 1 2 2 1 S",
+	            "1944 1944 9 2 7 2 2 0",
+	            "1945 1945 8 16 7 2 2 0",
+	            "1977 1980 3 1 0 2 2 1 S",
+	            "1977 1977 8 0 8 2 2 0",
+	            "1978 1978 9 1 7 2 2 0",
+	            "1979 1995 8 0 8 2 2 0",
+	            "1981 9999 2 0 8 2 2 1 S",
+	            "1996 9999 9 0 8 2 2 0"
+	        ],
+	        "SovietZone": [
+	            "1945 1945 4 24 7 2 0 2 M",
+	            "1945 1945 8 24 7 3 0 1 S",
+	            "1945 1945 10 18 7 2 2 0"
+	        ],
+	        "Germany": [
+	            "1946 1946 3 14 7 2 2 1 S",
+	            "1946 1946 9 7 7 2 2 0",
+	            "1947 1949 9 1 0 2 2 0",
+	            "1947 1947 3 6 7 3 2 1 S",
+	            "1947 1947 4 11 7 2 2 2 M",
+	            "1947 1947 5 29 7 3 0 1 S",
+	            "1948 1948 3 18 7 2 2 1 S",
+	            "1949 1949 3 10 7 2 2 1 S"
+	        ],
+	        "EU": [
+	            "1977 1980 3 1 0 1 1 1 S",
+	            "1977 1977 8 0 8 1 1 0",
+	            "1978 1978 9 1 7 1 1 0",
+	            "1979 1995 8 0 8 1 1 0",
+	            "1981 9999 2 0 8 1 1 1 S",
+	            "1996 9999 9 0 8 1 1 0"
+	        ]
+	    },
+	    "links": {}
+	});
+
+	$(document).ready(function() {
+
+		$('#calendar').fullCalendar({
+			editable: true,
+			timezone: 'Europe/Berlin',
+			events: [
+                {
+                    title: "event1",
+                    start: moment.parseZone('2014-05-30T10:00:00+02:00').tz('Europe/Berlin')
+                }
+			],
+			eventRender: function(event, el) {
+				// render the timezone offset below the event title
+				if (event.start.hasZone()) {
+					el.find('.fc-event-title').after(
+						$('<div class="tzo"/>').text(event.start.format('Z'))
+					);
+				}
+			}
+		});
+		
+	});
+
+</script>
+<style>
+
+	body {
+		margin: 0;
+		padding: 0;
+		font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif;
+		font-size: 14px;
+	}
+
+	#calendar {
+		width: 900px;
+		margin: 40px auto;
+	}
+
+</style>
+</head>
+<body>
+
+	<div id='calendar'></div>
+
+</body>
+</html>