fixed a bunch of moment/timezone issues

This commit is contained in:
Adam Shaw
2014-05-29 10:15:55 -07:00
parent 1d0c3838ad
commit 300670da43
5 changed files with 262 additions and 92 deletions
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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 (parseZone) {
if (isAmbigZone) {
mom._ambigZone = true;
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 (isSingleString) {
mom.zone(input); // if fails, will set it to 0, which it already was
else if ($.isArray(input)) {
// arrays have no timezone information, so assume ambiguous zone
isAmbigZone = true;
}
else if (isNativeDate(input) || input === undefined) {
// native Date object?
// specified with no arguments?
// then consider the moment to be local
mom.local();
// otherwise, probably a string with a format
if (parseAsUTC) {
output = moment.utc.apply(moment, args);
}
else {
output = moment.apply(null, args);
}
if (isAmbigTime) {
output._ambigTime = true;
output._ambigZone = true; // ambiguous time always means ambiguous zone
}
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
View File
@@ -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
View File
@@ -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>