LiveDate
has the ability to display the current date either by asynchronously fetching the time from a server or by just using the client system date. It's only as accurate / reliable as the source being used.
It's pretty much finished, but before I definitely considered it that I'd like to get a full review of it.
Specifically I'd also appreciate a review on my error handling and the way I'm importing the dependencies into the IIFE by just passing in window
. And sendAjaxRequest
since this function dependency is optional depending on which method is used to update the time.
Any suggestions for improvement are welcome!
LiveDate.js
var LiveDate = (function (global) {
'use strict';
function LiveDate(settings) {
this.element = settings.element;
this.timeUrl = settings.timeUrl || null;
this.format = settings.format || LiveDate.formats.ISO8601;
this.offset = settings.offset || 0;
this.errorHtml = settings.errorHtml || 'LiveDate Error';
this.weekdayNames = {
'long': settings.weekdayNames && settings.weekdayNames['long'] ? settings.weekdayNames['long'] : [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday'
],
'short': settings.weekdayNames && settings.weekdayNames['short'] ? settings.weekdayNames['short'] : [
'Sun',
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat'
]
};
this.monthNames = {
'long': settings.monthNames && settings.monthNames['long'] ? settings.monthNames['long'] : [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
],
'short': settings.monthNames && settings.monthNames['short'] ? settings.monthNames['short'] : [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
]
};
this.start();
}
LiveDate.prototype.start = function () {
var self = this;
if (!this.timeUrl) {
this._tickIntervalId = global.setInterval((function tick() {
self._date = new global.Date(new global.Date().getTime() + self.offset);
displayDate();
return tick;
}()), 200);
return;
}
global.sendAjaxRequest({
url: this.timeUrl,
headers: { 'X-Requested-With': 'XMLHttpRequest' },
cacheDocument: false,
successHandler: successHandler,
networkErrorHandler: networkErrorHandler
});
function successHandler(xhr) {
if (!(xhr.status >= 200 && xhr.status <= 299)) {
displayError();
throw {
name: 'AjaxRequestFetchError',
context: 'Fetching the document from: ' + self.timeUrl + '.',
problem: 'HTTP ' + xhr.status + ' was returned but expected HTTP 200.',
solution: 'Double check the URL and make sure it\'s reachable.',
toString: function () { return this.name; }
};
}
var requestTime = new global.Date().getTime(),
previousTime = new global.Date().getTime(),
serverTime = parseInt(xhr.responseText, 10);
self._tickIntervalId = global.setInterval((function () {
tick();
return tick;
}()), 200);
function tick() {
var NET_REINIT_WAIT_PERIOD = 10000;
var currentTime = new global.Date().getTime();
// Detecting client's system date changes to keep ours unaffected.
// Interval delay increment taken into account as well (+500 ms).
if ((currentTime - previousTime) < 0 || (currentTime - previousTime) >= 1500) {
self.stop();
// Introducing a delay particularly for PC's that've just been awoken to
// try and prevent an AjaxRequestFetchError from being thrown due to their
// Internet connection not being reinitialized yet.
global.setTimeout(function () {
self.start();
}, NET_REINIT_WAIT_PERIOD);
return;
}
self._date = new global.Date(serverTime + currentTime - requestTime + self.offset);
displayDate();
previousTime = currentTime;
}
}
function networkErrorHandler(xhr) {
displayError();
throw {
name: 'AjaxRequestNetworkError',
context: 'Sending an AJAX request to: ' + self.timeUrl + '.',
problem: 'Some type of network error or fetch termination occurred. (Error ' + xhr.status + ')',
solution: 'For the solution, analyse the network level for problems.',
toString: function () { return this.name; }
};
}
function displayError() {
self.element.innerHTML = self.errorHtml;
}
function displayDate() {
self.element.innerHTML = self.formatAs(self.format);
}
};
LiveDate.prototype.formatAs = function (format) {
var self = this;
function padNumber(number, width, character) {
number = '' + number;
character = character || 0;
if (number.length < width) {
return new global.Array(width - number.length + 1).join(character) + number;
}
return number;
}
var dateFormatter = {
/* DAY */
// Day of the month with leading zeros (01 to 31).
d: function () {
return padNumber(self._date.getUTCDate(), 2);
},
// Short textual representation of the day of the week (Mon).
D: function () {
return self.weekdayNames['short'][self._date.getUTCDay()];
},
// Day of the month without leading zeros (1 to 31).
j: function () {
return self._date.getUTCDate();
},
// Long textual representation of the day of the week (Monday).
l: function () {
return self.weekdayNames['long'][self._date.getUTCDay()];
},
/* MONTH */
// Long textual representation of the month (January).
F: function () {
return self.monthNames['long'][self._date.getUTCMonth()];
},
// Numeric representation of the month with leading zeros (01 to 12).
m: function () {
return padNumber(self._date.getUTCMonth() + 1, 2);
},
// Short textual representation of the month (Jan).
M: function () {
return self.monthNames['short'][self._date.getUTCMonth()];
},
// Numeric representation of the month without leading zeros (1 to 12).
n: function () {
return self._date.getUTCMonth() + 1;
},
/* YEAR */
// Short numeric representation of the year (15).
y: function () {
return ('' + self._date.getUTCFullYear()).slice(-2);
},
// Long numeric representation of the year (2015).
Y: function () {
return self._date.getUTCFullYear();
},
/* TIME */
// Lowercase ante meridiem (a.m.) or post meridiem (p.m.).
a: function () {
return self._date.getUTCHours() < 12 ? 'a.m.' : 'p.m.';
},
// Uppercase ante meridiem (A.M.) or post meridiem (P.M.).
A: function () {
return this.a().toUpperCase();
},
// 12-hour format of the hour without leading zeros (1 to 12).
g: function () {
return self._date.getUTCHours() % 12 || 12;
},
// 24-hour format of the hour without leading zeros (0 to 23).
G: function () {
return self._date.getUTCHours();
},
// 12-hour format of the hour with leading zeros (01 to 12).
h: function () {
return padNumber(self._date.getUTCHours() % 12 || 12, 2);
},
// 24-hour format of the hour with leading zeros (00 to 23).
H: function () {
return padNumber(self._date.getUTCHours(), 2);
},
// Minutes with leading zeros (00 to 59).
i: function () {
return padNumber(self._date.getUTCMinutes(), 2);
},
// Seconds with leading zeros (00 to 59).
s: function () {
return padNumber(self._date.getUTCSeconds(), 2);
},
/* TIMEZONE */
// Hour and minute difference to UTC without colon in-between and with leading zeros (+0000).
O: function () {
var sign = self.offset < 0 ? '-' : '+',
hours = global.Math.floor(global.Math.abs(self.offset) / 3600000),
minutes = self.offset / 3600000 % 1 * 60;
return sign + padNumber(hours, 2) + padNumber(minutes, 2);
},
// Hour and minute difference to UTC with colon in-between and leading zeros (+00:00).
P: function () {
var O = this.O();
return O.substr(0, 3) + ':' + O.substr(3);
}
};
return format.replace(/(\\?)([a-z])/gi, function (match, isEscaped, character) {
return !isEscaped && dateFormatter[character] ? dateFormatter[character]() : character;
});
};
LiveDate.prototype.getTime = function () {
return this._date.getTime();
};
LiveDate.prototype.stop = function () {
global.clearInterval(this._tickIntervalId);
delete this._tickIntervalId;
};
LiveDate.formats = {
ATOM: 'Y-m-d\\TH:i:sP',
ISO8601: 'Y-m-d\\TH:i:sP',
RSS: 'D, d M Y H:i:s O',
RFC822: 'D, d M y H:i:s O',
RFC1036: 'D, d M y H:i:s O',
RFC1123: 'D, d M Y H:i:s O',
RFC2822: 'D, d M Y H:i:s O',
RFC3339: 'Y-m-d\\TH:i:sP',
W3C: 'Y-m-d\\TH:i:sP'
};
return LiveDate;
}(window));
Example
// Server Synced Date
var serverLiveDate = new LiveDate({
timeUrl: 'time.php',
element: document.getElementById('server-date'),
format: 'l j F Y - <\\b>H:i:s</\\b>',
offset: 720000 // UTC+2
});
// Client System Synced Date
var clientLiveDate = new LiveDate({
element: document.getElementById('client-date')
});
Date
object is only as reliable as the client system date, which you have no control of. So if their system date is inaccurate, so will the time returned byDate
be. If you need a solution that displays the "real" time that's not affected by how the client system date is set, you can get the date from a (reliable) server. For instance, you might have a service that displays different live dates from all over the world. But if you rely on the client system date and it's not set correctly, every date shown by your service will be incorrect as well. \$\endgroup\$