From 5d9effb67f891e9470009d6c9a4f8eebe7c454ec Mon Sep 17 00:00:00 2001 From: Tobias Markmann Date: Sun, 10 Jan 2010 21:19:30 +0000 Subject: [PATCH] Adding some JS scripts needed for future site improvements. git-svn-id: file:///home/ksmith/gitmigration/svn/xmpp/trunk@3789 4b5297f7-1745-476d-ba37-a9c6900126ab --- js/dateiso8601.js | 47 +++ js/jquery.icalendar.js | 642 +++++++++++++++++++++++++++++++++++++++++ js/jquery.timeago.js | 140 +++++++++ sitegen.sh | 1 + 4 files changed, 830 insertions(+) create mode 100644 js/dateiso8601.js create mode 100644 js/jquery.icalendar.js create mode 100644 js/jquery.timeago.js diff --git a/js/dateiso8601.js b/js/dateiso8601.js new file mode 100644 index 00000000..27bc1dd5 --- /dev/null +++ b/js/dateiso8601.js @@ -0,0 +1,47 @@ +ate.prototype.toISO8601String = function (format, offset) { + /* accepted values for the format [1-6]: + 1 Year: + YYYY (eg 1997) + 2 Year and month: + YYYY-MM (eg 1997-07) + 3 Complete date: + YYYY-MM-DD (eg 1997-07-16) + 4 Complete date plus hours and minutes: + YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00) + 5 Complete date plus hours, minutes and seconds: + YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00) + 6 Complete date plus hours, minutes, seconds and a decimal + fraction of a second + YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00) + */ + if (!format) { var format = 6; } + if (!offset) { + var offset = 'Z'; + var date = this; + } else { + var d = offset.match(/([-+])([0-9]{2}):([0-9]{2})/); + var offsetnum = (Number(d[2]) * 60) + Number(d[3]); + offsetnum *= ((d[1] == '-') ? -1 : 1); + var date = new Date(Number(Number(this) + (offsetnum * 60000))); + } + + var zeropad = function (num) { return ((num < 10) ? '0' : '') + num; } + + var str = ""; + str += date.getUTCFullYear(); + if (format > 1) { str += "-" + zeropad(date.getUTCMonth() + 1); } + if (format > 2) { str += "-" + zeropad(date.getUTCDate()); } + if (format > 3) { + str += "T" + zeropad(date.getUTCHours()) + + ":" + zeropad(date.getUTCMinutes()); + } + if (format > 5) { + var secs = Number(date.getUTCSeconds() + "." + + ((date.getUTCMilliseconds() < 100) ? '0' : '') + + zeropad(date.getUTCMilliseconds())); + str += ":" + zeropad(secs); + } else if (format > 4) { str += ":" + zeropad(date.getUTCSeconds()); } + + if (format > 3) { str += offset; } + return str; +} diff --git a/js/jquery.icalendar.js b/js/jquery.icalendar.js new file mode 100644 index 00000000..f9c55261 --- /dev/null +++ b/js/jquery.icalendar.js @@ -0,0 +1,642 @@ +/* http://keith-wood.name/icalendar.html + iCalendar processing for jQuery v1.0.0. + Written by Keith Wood (kbwood@virginbroadband.com.au) October 2008. + Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and + MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses. + Please attribute the author if you use it. */ + +(function($) { // Hide scope, no $ conflict + +var PROP_NAME = 'icalendar'; +var FLASH_ID = 'icalendar-flash-copy'; + +/* iCalendar sharing manager. */ +function iCalendar() { + this._defaults = { + sites: [], // List of site IDs to use, empty for all + icons: 'icalendar.png', // Horizontal amalgamation of all site icons + iconSize: 16, // The size of the individual icons + target: '_blank', // The name of the target window for the iCalendar links + compact: false, // True if a compact presentation should be used, false for full + tipPrefix: '', // Additional text to show in the tool tip for each icon + echoUrl: '', // The URL to echo back iCalendar content, or blank for clipboard + echoField: '', // The ID of a field to copy the iCalendar definition into, or blank for clipboard + start: null, // The start date/time of the event + end: null, // The end date/time of the event + title: '', // The title of the event + summary: '', // The summary of the event + description: '', // The description of the event + location: '', // The location of the event + url: '', // A URL with more information about the event + contact: '', // An e-mail address for further contact about the event + copyConfirm: 'The event will be copied to your clipboard. Continue?', // Confirmation message for clipboard copy + copySucceeded: 'The event has been copied to your clipboard', // Success message during clipboard copy + copyFailed: 'Failed to copy the event to the clipboard\n', // Failure message during clipboard copy + copyFlash: 'clipboard.swf', // The URL for the Flash clipboard copy module + // Clipboard not supported message + copyUnavailable: 'The clipboard is unavailable, please copy the event details from below:\n' + }; + this._sites = { // The definitions of the available iCalendar sites + 'google': {display: 'Google', icon: 0, + url: 'http://www.google.com/calendar/event?action=TEMPLATE&text={t}&dates={s}/{e}&details={d}&location={l}'}, + 'icalendar': {display: 'iCalendar', icon: 1, url: 'echo'}, + 'outlook': {display: 'Outlook', icon: 2, url: 'echo'}, + 'yahoo': {display: 'Yahoo', icon: 3, + url: 'http://calendar.yahoo.com/?v=60&view=d&type=20&title={t}&st={s}&dur={p}&desc={d}&in_loc={l}'} + }; +} +var FREQ_SETTINGS = [{method: 'Seconds', factor: 1}, + {method: 'Minutes', factor: 60}, {method: 'Hours', factor: 3600}, + {method: 'Date', factor: 86400}, {method: 'Month', factor: 1}, + {method: 'FullYear', factor: 12}, {method: 'Date', factor: 604800}]; +var SE = 0; +var MI = 1; +var HR = 2; +var DY = 3; +var MO = 4; +var YR = 5; +var WK = 6; + +$.extend(iCalendar.prototype, { + /* Class name added to elements to indicate already configured with iCalendar. */ + markerClassName: 'hasICalendar', + + /* Override the default settings for all iCalendar instances. + @param settings (object) the new settings to use as defaults + @return void */ + setDefaults: function(settings) { + extendRemove(this._defaults, settings || {}); + return this; + }, + + /* Add a new iCalendar site to the list. + @param id (string) the ID of the new site + @param display (string) the display name for this site + @param icon (url) the location of an icon for this site (16x16), or + (number) the index of the icon within the combined image + @param url (url) the submission URL for this site + with {t} marking where the event title should be inserted, + {s} indicating the event start date/time insertion point, + {e} indicating the event end date/time insertion point, + {p} indicating the event period (duration) insertion point, + {d} indicating the event description insertion point, + {l} indicating the event location insertion point, + {u} indicating the event URL insertion point, + {c} indicating the event contact insertion point + @return void */ + addSite: function(id, display, icon, url) { + this._sites[id] = {display: display, icon: icon, url: url}; + return this; + }, + + /* Return the list of defined sites. + @return object[] - indexed by site id (string), each object contains + display (string) the display name, + icon (string) the location of the icon, or + (number) the icon's index in the combined image + url (string) the submission URL for the site */ + getSites: function() { + return this._sites; + }, + + /* Attach the iCalendar widget to a div. */ + _attachICalendar: function(target, settings) { + target = $(target); + if (target.hasClass(this.markerClassName)) { + return; + } + target.addClass(this.markerClassName); + this._updateICalendar(target, settings); + }, + + /* Reconfigure the settings for an iCalendar div. */ + _changeICalendar: function(target, settings) { + target = $(target); + if (!target.hasClass(this.markerClassName)) { + return; + } + this._updateICalendar(target, settings); + }, + + /* Construct the requested iCalendar links. */ + _updateICalendar: function(target, settings) { + settings = extendRemove($.extend({}, this._defaults, + $.data(target[0], PROP_NAME) || {}), settings); + $.data(target[0], PROP_NAME, settings); + if (!target[0].id) { + target[0].id = 'ic' + new Date().getTime(); + } + var sites = settings.sites || this._defaults.sites; + if (sites.length == 0) { // default to all sites + $.each(this._sites, function(id) { + sites[sites.length] = id; + }); + } + var addSite = function(site, calId) { + var url = (site.url == 'echo' ? '#' : + site.url.replace(/{t}/, escape(settings.title)). + replace(/{d}/, escape(settings.description)). + replace(/{s}/, $.icalendar.formatDate(settings.start)). + replace(/{e}/, $.icalendar.formatDate(settings.end)). + replace(/{p}/, $.icalendar.calculateDuration(settings.start, settings.end)). + replace(/{l}/, escape(settings.location)). + replace(/{u}/, escape(settings.url)). + replace(/{c}/, escape(settings.contact))); + var html = '
  • '; + if (site.icon != null) { + if (typeof site.icon == 'number') { + html += ''; + } + else { + html += ''; + } + html += (settings.compact ? '' : ' '); + } + html += (settings.compact ? '' : site.display) + '
  • '; + return html; + }; + var html = ''; + target.html(html); + }, + + /* Remove the iCalendar widget from a div. */ + _destroyICalendar: function(target) { + target = $(target); + if (!target.hasClass(this.markerClassName)) { + return; + } + target.removeClass(this.markerClassName).empty(); + $.removeData(target[0], PROP_NAME); + }, + + /* Echo the iCalendar text back to the user either as a + downloadable file or via the clipboard. + @param id (string) the ID of the owning division + @param calId (string) the ID of the site to send the calendar to */ + _echo: function(id, calId) { + var settings = $.data($(id)[0], PROP_NAME); + var event = makeICalendar(settings); + if (settings.echoUrl) { + window.location.href = settings.echoUrl + '?content=' + escape(event); + } + else if (settings.echoField) { + $(settings.echoField).val(event); + } + else if (!settings.copyFlash) { + alert(settings.copyUnavailable + event); + } + else if (confirm(settings.copyConfirm)) { + var error = ''; + if (error = copyViaFlash(event, settings.copyFlash)) { + alert(settings.copyFailed + error); + } + else { + alert(settings.copySucceeded); + } + } + return false; // Don't follow link + }, + + /* Format a date/time for iCalendar: yyyymmddThhmmss[Z]. + @param date (Date) the date to format + @param local (boolean) true if this should be a local date/time + @return (string) the formatted date */ + formatDate: function(date, local) { + var ensureTwo = function(value) { + return (value < 10 ? '0' : '') + value; + }; + date = (date ? new Date(date.getTime()) : null); + return (!date ? '' : (local ? '' + date.getFullYear() + ensureTwo(date.getMonth() + 1) + + ensureTwo(date.getDate()) + 'T' + ensureTwo(date.getHours()) + + ensureTwo(date.getMinutes()) + ensureTwo(date.getSeconds()) : + '' + date.getUTCFullYear() + ensureTwo(date.getUTCMonth() + 1) + + ensureTwo(date.getUTCDate()) + 'T' + ensureTwo(date.getUTCHours()) + + ensureTwo(date.getUTCMinutes()) + ensureTwo(date.getUTCSeconds()) + 'Z')); + }, + + /* Calculate the duration between two date/times. + @param start (Date) the starting date/time + @param end (Date) the ending date/time + @return (string) the formatted duration or blank if invalid parameters */ + calculateDuration: function(start, end) { + if (!start || !end) { + return ''; + } + var seconds = Math.abs(end.getTime() - start.getTime()) / 1000; + var days = Math.floor(seconds / 86400); + seconds -= days * 86400; + var hours = Math.floor(seconds / 3600); + seconds -= hours * 3600; + var minutes = Math.floor(seconds / 60); + seconds -= minutes * 60; + return (start.getTime() > end.getTime() ? '-' : '') + + 'P' + (days > 0 ? days + 'D' : '') + + (hours || minutes || seconds ? 'T' + hours + 'H' : '') + + (minutes || seconds ? minutes + 'M' : '') + (seconds ? seconds + 'S' : ''); + }, + + /* Calculate the end date/time given a start and a duration. + @param start (Date) the starting date/time + @param duration (string) the description of the duration + @return (Date) the ending date/time + @throws error if an invalid duration is found */ + addDuration: function(start, duration) { + if (!duration) { + return start; + } + var end = new Date(start.getTime()); + var matches = DURATION.exec(duration); + if (!matches) { + throw 'Invalid duration'; + } + if (matches[2] && (matches[3] || matches[5] || matches[6] || matches[7])) { + throw 'Invalid duration - week must be on its own'; // Week must be on its own + } + if (!matches[4] && (matches[5] || matches[6] || matches[7])) { + throw 'Invalid duration - missing time marker'; // Missing T with hours/minutes/seconds + } + var sign = (matches[1] == '-' ? -1 : +1); + var apply = function(value, factor, method) { + value = parseInt(value); + if (!isNaN(value)) { + end['setUTC' + method](end['getUTC' + method]() + sign * value * factor); + } + }; + if (matches[2]) { + apply(matches[2], 7, 'Date'); + } + else { + apply(matches[3], 1, 'Date'); + apply(matches[5], 1, 'Hours'); + apply(matches[6], 1, 'Minutes'); + apply(matches[7], 1, 'Seconds'); + } + return end; + }, + + /* Parse the iCalendar data into a JavaScript object model. + @param content (string) the original iCalendar data + @return (object) the iCalendar JavaScript model + @throws errors if the iCalendar structure is incorrect */ + parse: function(content) { + var cal = {}; + var timezones = {}; + var lines = unfoldLines(content); + parseGroup(lines, 0, cal, timezones); + if (!cal.vcalendar) { + throw 'Invalid iCalendar data'; + } + return cal.vcalendar; + }, + + /* Calculate the week of the year for a given date + according to the ISO 8601 definition. + @param date (Date) the date to calculate the week for + @param weekStart (number) the day on which a week starts: + 0 = Sun, 1 = Mon, ... (optional, defaults to 1) + @return (number) the week for these parameters (1-53) */ + getWeekOfYear: function(date, weekStart) { + return getWeekOfYear(date, weekStart); + } +}); + +/* jQuery extend now ignores nulls! */ +function extendRemove(target, props) { + $.extend(target, props); + for (var name in props) { + if (props[name] == null) { + target[name] = null; + } + } + return target; +} + +/* Attach the iCalendar functionality to a jQuery selection. + @param command (string) the command to run (optional, default 'attach') + @param options (object) the new settings to use for these iCalendar instances + @return (jQuery object) jQuery for chaining further calls */ +$.fn.icalendar = function(options) { + var otherArgs = Array.prototype.slice.call(arguments, 1); + return this.each(function() { + if (typeof options == 'string') { + $.icalendar['_' + options + 'ICalendar']. + apply($.icalendar, [this].concat(otherArgs)); + } + else { + $.icalendar._attachICalendar(this, options || {}); + } + }); +}; + +/* Initialise the iCalendar functionality. */ +$.icalendar = new iCalendar(); // singleton instance + +/* Construct an iCalendar with an event object. + @param event (object) the event details + @return (string) the iCalendar definition */ +function makeICalendar(event) { + var limit75 = function(text) { + var out = ''; + while (text.length > 75) { + out += text.substr(0, 75) + '\n'; + text = ' ' + text.substr(75); + } + out += text; + return out; + }; + return 'BEGIN:VCALENDAR\n' + + 'VERSION:2.0\n' + + 'PRODID:jquery.icalendar\n' + + 'BEGIN:VEVENT\n' + + (event.url ? limit75('URL:' + event.url) + '\n' : '') + + (event.contact ? limit75('MAILTO:' + event.contact) + '\n' : '') + + limit75('TITLE:' + event.title) + '\n' + + 'DTSTART:' + $.icalendar.formatDate(event.start) + '\n' + + 'DTEND:' + $.icalendar.formatDate(event.end) + '\n' + + (event.summary ? limit75('SUMMARY:' + event.summary) + '\n' : '') + + (event.description ? limit75('DESCRIPTION:' + event.description) + '\n' : '') + + (event.location ? limit75('LOCATION:' + event.location) + '\n' : '') + + 'END:VEVENT\n' + + 'END:VCALENDAR'; +} + +/* Copy the given text to the system clipboard via Flash. + @param text (string) the text to copy + @param url (string) the URL for the Flash clipboard copy module + @return (string) '' if successful, error message if not */ +function copyViaFlash(text, url) { + $('#' + FLASH_ID).remove(); + try { + $('body').append('
    '); + return ''; + } + catch(e) { + return e; + } +} + +/* Pattern for folded lines: start with a whitespace character */ +var FOLDED = /^\s(.*)$/; +/* Pattern for an individual entry: name:value */ +var ENTRY = /^([^:]+):(.*)$/; +/* Pattern for a date only field: yyyymmdd */ +var DATEONLY = /^(\d{4})(\d\d)(\d\d)$/; +/* Pattern for a date/time field: yyyymmddThhmmss[Z] */ +var DATETIME = /^(\d{4})(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)(Z?)$/; +/* Pattern for a date/time range field: yyyymmddThhmmss[Z]/yyyymmddThhmmss[Z] */ +var DATETIME_RANGE = /^(\d{4})(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)(Z?)\/(\d{4})(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)(Z?)$/; +/* Pattern for a timezone offset field: +hhmm */ +var TZ_OFFSET = /^([+-])(\d\d)(\d\d)$/; +/* Pattern for a duration: [+-]PnnW or [+-]PnnDTnnHnnMnnS */ +var DURATION = /^([+-])?P(\d+W)?(\d+D)?(T)?(\d+H)?(\d+M)?(\d+S)?$/; +/* Reserved names not suitable for attrbiute names. */ +var RESERVED_NAMES = ['class']; + +/* iCalendar lines are split so the max length is no more than 75. + Split lines start with a whitespace character. + @param content (string) the original iCalendar data + @return (string[]) the restored iCalendar data */ +function unfoldLines(content) { + content = content.replace(/\r\n/g, '\n'); + var lines = content.split('\n'); + for (var i = lines.length - 1; i > 0; i--) { + var matches = FOLDED.exec(lines[i]); + if (matches) { + lines[i - 1] += matches[1]; + lines[i] = ''; + } + } + return $.map(lines, function(line, i) { // Remove blank lines + return (line ? line : null); + }); +} + +/* Parse a group in the file, delimited by BEGIN:xxx and END:xxx. + Recurse if an embedded group encountered. + @param lines (string[]) the iCalendar data + @param index (number) the current position within the data + @param owner (object) the current owner for the new group + @param timezones (object) collection of defined timezones + @return (number) the updated position after processing this group + @throws errors if group structure is incorrect */ +function parseGroup(lines, index, owner, timezones) { + if (index >= lines.length || lines[index].indexOf('BEGIN:') != 0) { + throw 'Missing group start'; + } + var group = {}; + var name = lines[index].substr(6); + addEntry(owner, name.toLowerCase(), group); + index++; + while (index < lines.length && lines[index].indexOf('END:') != 0) { + if (lines[index].indexOf('BEGIN:') == 0) { // Recurse for embedded group + index = parseGroup(lines, index, group, timezones); + } + else { + var entry = parseEntry(lines[index]); + addEntry(group, entry._name, (entry._simple ? entry._value : entry)); + } + index++; + } + if (name == 'VTIMEZONE') { // Save timezone offset + var matches = TZ_OFFSET.exec(group.standard.tzoffsetto); + if (matches) { + timezones[group.tzid] = (matches[1] == '-' ? -1 : +1) * + (parseInt(matches[2]) * 60 + parseInt(matches[3])); + } + } + else { + for (var name2 in group) { + resolveTimezones(group[name2], timezones); + } + } + if (lines[index] != 'END:' + name) { + throw 'Missing group end ' + name; + } + return index; +} + +/* Resolve timezone references for dates. + @param value (any) the current value to check - updated if appropriate + @param timezones (object) collection of defined timezones */ +function resolveTimezones(value, timezones) { + if (!value) { + return; + } + if (value.tzid && value._value) { + var offset = timezones[value.tzid]; + var offsetDate = function(date, tzid) { + date.setMinutes(date.getMinutes() - offset); + date._type = tzid; + }; + if (isArray(value._value)) { + for (var i = 0; i < value._value.length; i++) { + offsetDate(value._value[i], value.tzid); + } + } + else if (value._value.start && value._value.end) { + offsetDate(value._value.start, value.tzid); + offsetDate(value._value.end, value.tzid); + } + else { + offsetDate(value._value, value.tzid); + } + } + else if (isArray(value)) { + for (var i = 0; i < value.length; i++) { + resolveTimezones(value[i], timezones); + } + } +} + +/* Add a new entry to an object, making multiple entries into an array. + @param owner (object) the owning object for the new entry + @param name (string) the name of the new entry + @param value (string or object) the new entry value */ +function addEntry(owner, name, value) { + if (typeof value == 'string') { + value = value.replace(/\\n/g, '\n'); + } + if ($.inArray(name, RESERVED_NAMES) > -1) { + name += '_'; + } + if (owner[name]) { // Turn multiple values into an array + if (!isArray(owner[name]) || owner['_' + name + 'IsArray']) { + owner[name] = [owner[name]]; + } + owner[name][owner[name].length] = value; + if (owner['_' + name + 'IsArray']) { + owner['_' + name + 'IsArray'] = undefined; + } + } + else { + owner[name] = value; + if (isArray(value)) { + owner['_' + name + 'IsArray'] = true; + } + } +} + +/* Parse an individual entry. + The format is: [;=]...: + @param line (string) the line to parse + @return (object) the parsed entry with _name and _value + attributes, _simple to indicate whether or not + other parameters, and other parameters as necessary */ +function parseEntry(line) { + var entry = {}; + var matches = ENTRY.exec(line); + if (!matches) { + throw 'Missing entry name: ' + line; + } + var params = matches[1].split(';'); + entry._name = params[0].toLowerCase(); + entry._value = checkDate(matches[2]); + entry._simple = true; + parseParams(entry, params.slice(1)); + return entry; +} + +/* Parse parameters for an individual entry. + The format is: =[;...] + @param owner (object) the owning object for the parameters, + updated with parameters as attributes, and + _simple to indicate whether or not other parameters + @param params (string or string[]) the parameters to parse */ +function parseParams(owner, params) { + params = (isArray(params) ? params : params.split(';')); + owner._simple = true; + for (var i = 0; i < params.length; i++) { + var nameValue = params[i].split('='); + owner[nameValue[0].toLowerCase()] = checkDate(nameValue[1] || ''); + owner._simple = false; + } +} + +/* Convert a value into a Date object or array of Date objects if appropriate. + @param value (string) the value to check + @return (string or Date) the converted value (if appropriate) */ +function checkDate(value) { + var matches = DATETIME.exec(value); + if (matches) { + return makeDate(matches); + } + matches = DATETIME_RANGE.exec(value); + if (matches) { + return {start: makeDate(matches), end: makeDate(matches.slice(7))}; + } + matches = DATEONLY.exec(value); + if (matches) { + return makeDate(matches.concat([0, 0, 0, ''])); + } + return value; +} + +/* Create a date value from matches on a string. + @param matches (string[]) the component parts of the date + @return (Date) the corresponding date */ +function makeDate(matches) { + var date = new Date(matches[1], matches[2] - 1, matches[3], + matches[4], matches[5], matches[6]); + date._type = (matches[7] ? 'UTC' : 'float'); + return utcDate(date); +} + +/* Standardise a date to UTC. + @param date (Date) the date to standardise + @return (Date) the equivalent UTC date */ +function utcDate(date) { + date.setMinutes(date.getMinutes() - date.getTimezoneOffset()); + return date; +} + +/* Calculate the week of the year for a given date + according to the ISO 8601 definition. + @param date (Date) the date to calculate the week for + @param weekStart (number) the day on which a week starts: + 0 = Sun, 1 = Mon, ... (optional, defaults to 1) + @return (number) the week for these parameters (1-53) */ +function getWeekOfYear(date, weekStart) { + weekStart = (weekStart || weekStart == 0 ? weekStart : 1); + var checkDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), + (date.getTimezoneOffset() / -60)); + var firstDay = new Date(checkDate.getFullYear(), 1 - 1, 4); // First week always contains 4 Jan + var firstDOW = firstDay.getDay(); // Day of week: Sun = 0, Mon = 1, ... + firstDay.setDate(4 + weekStart - firstDOW - (weekStart > firstDOW ? 7 : 0)); // Preceding week start + if (checkDate < firstDay) { // Adjust first three days in year if necessary + checkDate.setDate(checkDate.getDate() - 3); // Generate for previous year + return getWeekOfYear(checkDate, weekStart); + } else if (checkDate > new Date(checkDate.getFullYear(), 12 - 1, 28)) { // Check last three days in year + var firstDay2 = new Date(checkDate.getFullYear() + 1, 1 - 1, 4); // Find first week in next year + firstDOW = firstDay2.getDay(); + firstDay2.setDate(4 + weekStart - firstDOW - (weekStart > firstDOW ? 7 : 0)); + if (checkDate >= firstDay2) { // Adjust if necessary + return 1; + } + } + return Math.floor(((checkDate - firstDay) / + (FREQ_SETTINGS[DY].factor * 1000)) / 7) + 1; // Weeks to given date +} + +/* Determine whether an object is an array. + @param a (object) the object to test + @return (boolean) true if it is an array, or false if not */ +function isArray(a) { + return (a && a.constructor == Array); +} + +})(jQuery); diff --git a/js/jquery.timeago.js b/js/jquery.timeago.js new file mode 100644 index 00000000..a85a9211 --- /dev/null +++ b/js/jquery.timeago.js @@ -0,0 +1,140 @@ +/* + * timeago: a jQuery plugin, version: 0.8.1 (2010-01-04) + * @requires jQuery v1.2.3 or later + * + * Timeago is a jQuery plugin that makes it easy to support automatically + * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago"). + * + * For usage and examples, visit: + * http://timeago.yarp.com/ + * + * Licensed under the MIT: + * http://www.opensource.org/licenses/mit-license.php + * + * Copyright (c) 2008-2010, Ryan McGeary (ryanonjavascript -[at]- mcgeary [*dot*] org) + */ +(function($) { + $.timeago = function(timestamp) { + if (timestamp instanceof Date) return inWords(timestamp); + else if (typeof timestamp == "string") return inWords($.timeago.parse(timestamp)); + else return inWords($.timeago.datetime(timestamp)); + }; + var $t = $.timeago; + + $.extend($.timeago, { + settings: { + refreshMillis: 60000, + allowFuture: false, + strings: { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "ago", + suffixFromNow: "from now", + ago: null, // DEPRECATED, use suffixAgo + fromNow: null, // DEPRECATED, use suffixFromNow + seconds: "less than a minute", + minute: "about a minute", + minutes: "%d minutes", + hour: "about an hour", + hours: "about %d hours", + day: "a day", + days: "%d days", + month: "about a month", + months: "%d months", + year: "about a year", + years: "%d years" + } + }, + inWords: function(distanceMillis) { + var $l = this.settings.strings; + var prefix = $l.prefixAgo; + var suffix = $l.suffixAgo || $l.ago; + if (this.settings.allowFuture) { + if (distanceMillis < 0) { + prefix = $l.prefixFromNow; + suffix = $l.suffixFromNow || $l.fromNow; + } + distanceMillis = Math.abs(distanceMillis); + } + + var seconds = distanceMillis / 1000; + var minutes = seconds / 60; + var hours = minutes / 60; + var days = hours / 24; + var years = days / 365; + + var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) || + seconds < 90 && substitute($l.minute, 1) || + minutes < 45 && substitute($l.minutes, Math.round(minutes)) || + minutes < 90 && substitute($l.hour, 1) || + hours < 24 && substitute($l.hours, Math.round(hours)) || + hours < 48 && substitute($l.day, 1) || + days < 30 && substitute($l.days, Math.floor(days)) || + days < 60 && substitute($l.month, 1) || + days < 365 && substitute($l.months, Math.floor(days / 30)) || + years < 2 && substitute($l.year, 1) || + substitute($l.years, Math.floor(years)); + + return $.trim([prefix, words, suffix].join(" ")); + }, + parse: function(iso8601) { + var s = $.trim(iso8601); + s = s.replace(/-/,"/").replace(/-/,"/"); + s = s.replace(/T/," ").replace(/Z/," UTC"); + s = s.replace(/([\+-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400 + return new Date(s); + }, + datetime: function(elem) { + // jQuery's `is()` doesn't play well with HTML5 in IE + var isTime = $(elem).get(0).tagName.toLowerCase() == 'time'; // $(elem).is('time'); + var iso8601 = isTime ? $(elem).attr('datetime') : $(elem).attr('title'); + return $t.parse(iso8601); + } + }); + + $.fn.timeago = function() { + var self = this; + self.each(refresh); + + var $s = $t.settings; + if ($s.refreshMillis > 0) { + setInterval(function() { self.each(refresh); }, $s.refreshMillis); + } + return self; + }; + + function refresh() { + var data = prepareData(this); + if (!isNaN(data.datetime)) { + $(this).text(inWords(data.datetime)); + } + return this; + } + + function prepareData(element) { + element = $(element); + if (element.data("timeago") === undefined) { + element.data("timeago", { datetime: $t.datetime(element) }); + var text = $.trim(element.text()); + if (text.length > 0) element.attr("title", text); + } + return element.data("timeago"); + } + + function inWords(date) { + return $t.inWords(distance(date)); + } + + function distance(date) { + return (new Date().getTime() - date.getTime()); + } + + function substitute(stringOrFunction, value) { + var string = $.isFunction(stringOrFunction) ? stringOrFunction(value) : stringOrFunction; + return string.replace(/%d/i, value); + } + + // fix for IE6 suckage + document.createElement('abbr'); + document.createElement('time'); +})(jQuery); diff --git a/sitegen.sh b/sitegen.sh index ebd81a86..de894a6a 100755 --- a/sitegen.sh +++ b/sitegen.sh @@ -54,5 +54,6 @@ cp xsf/teams/*.* $basepath/xsf/teams/ cp xsf/teams/infrastructure/*.* $basepath/xsf/teams/infrastructure/ cp xsf/teams/communication/*.* $basepath/xsf/teams/communication/ cp xsf/teams/techreview/*.* $basepath/xsf/teams/techreview/ +cp js/*.* $basepath/js/ # END