git/gitweb/static/js/adjust-timezone.js
John 'Warthog9' Hawley 2ae8da2552 gitweb.js: Add UI for selecting common timezone to display dates
This will modify HTML, add CSS rules and add DOM event handlers so
that clicking on any date (the common part, not the localtime part)
will display a drop down menu to choose the timezone to change to.

Currently menu displays only the following timezones:

  utc
  local
  -1200
  -1100
  ...
  +1100
  +1200
  +1300
  +1400

In timezone selection menu each timezone is +1hr to the previous.  The
code is capable of handling fractional timezones, but those have not
been added to the menu.

All changes are saved to a cookie, so page changes and closing /
reopening browser retains the last known timezone setting used.

[jn: Changed from innerHTML to DOM, moved to event delegation for
onclick to trigger menu, added close button and cookie refreshing]

Helped-by: Kevin Cernekee <cernekee@gmail.com>
Signed-off-by: John 'Warthog9' Hawley <warthog9@eaglescrag.net>
Signed-off-by: Jakub Narebski <jnareb@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
2011-05-24 11:22:45 -07:00

330 lines
11 KiB
JavaScript

// Copyright (C) 2011, John 'Warthog9' Hawley <warthog9@eaglescrag.net>
// 2011, Jakub Narebski <jnareb@gmail.com>
/**
* @fileOverview Manipulate dates in gitweb output, adjusting timezone
* @license GPLv2 or later
*/
/**
* Get common timezone, add UI for changing timezones, and adjust
* dates to use requested common timezone.
*
* This function is called during onload event (added to window.onload).
*
* @param {String} tzDefault: default timezone, if there is no cookie
* @param {Object} tzCookieInfo: object literal with info about cookie to store timezone
* @param {String} tzCookieInfo.name: name of cookie to store timezone
* @param {String} tzClassName: denotes elements with date to be adjusted
*/
function onloadTZSetup(tzDefault, tzCookieInfo, tzClassName) {
var tzCookieTZ = getCookie(tzCookieInfo.name, tzCookieInfo);
var tz = tzDefault;
if (tzCookieTZ) {
// set timezone to value saved in a cookie
tz = tzCookieTZ;
// refresh cookie, so its expiration counts from last use of gitweb
setCookie(tzCookieInfo.name, tzCookieTZ, tzCookieInfo);
}
// add UI for changing timezone
addChangeTZ(tz, tzCookieInfo, tzClassName);
// server-side of gitweb produces datetime in UTC,
// so if tz is 'utc' there is no need for changes
var nochange = tz === 'utc';
// adjust dates to use specified common timezone
fixDatetimeTZ(tz, tzClassName, nochange);
}
/* ...................................................................... */
/* Changing dates to use requested timezone */
/**
* Replace RFC-2822 dates contained in SPAN elements with tzClassName
* CSS class with equivalent dates in given timezone.
*
* @param {String} tz: numeric timezone in '(-|+)HHMM' format, or 'utc', or 'local'
* @param {String} tzClassName: specifies elements to be changed
* @param {Boolean} nochange: markup for timezone change, but don't change it
*/
function fixDatetimeTZ(tz, tzClassName, nochange) {
// sanity check, method should be ensured by common-lib.js
if (!document.getElementsByClassName) {
return;
}
// translate to timezone in '(-|+)HHMM' format
tz = normalizeTimezoneInfo(tz);
// NOTE: result of getElementsByClassName should probably be cached
var classesFound = document.getElementsByClassName(tzClassName, "span");
for (var i = 0, len = classesFound.length; i < len; i++) {
var curElement = classesFound[i];
curElement.title = 'Click to change timezone';
if (!nochange) {
// we use *.firstChild.data (W3C DOM) instead of *.innerHTML
// as the latter doesn't always work everywhere in every browser
var epoch = parseRFC2822Date(curElement.firstChild.data);
var adjusted = formatDateRFC2882(epoch, tz);
curElement.firstChild.data = adjusted;
}
}
}
/* ...................................................................... */
/* Adding triggers, generating timezone menu, displaying and hiding */
/**
* Adds triggers for UI to change common timezone used for dates in
* gitweb output: it marks up and/or creates item to click to invoke
* timezone change UI, creates timezone UI fragment to be attached,
* and installs appropriate onclick trigger (via event delegation).
*
* @param {String} tzSelected: pre-selected timezone,
* 'utc' or 'local' or '(-|+)HHMM'
* @param {Object} tzCookieInfo: object literal with info about cookie to store timezone
* @param {String} tzClassName: specifies elements to install trigger
*/
function addChangeTZ(tzSelected, tzCookieInfo, tzClassName) {
// make link to timezone UI discoverable
addCssRule('.'+tzClassName + ':hover',
'text-decoration: underline; cursor: help;');
// create form for selecting timezone (to be saved in a cookie)
var tzSelectFragment = document.createDocumentFragment();
tzSelectFragment = createChangeTZForm(tzSelectFragment,
tzSelected, tzCookieInfo, tzClassName);
// event delegation handler for timezone selection UI (clicking on entry)
// see http://www.nczonline.net/blog/2009/06/30/event-delegation-in-javascript/
// assumes that there is no existing document.onclick handler
document.onclick = function onclickHandler(event) {
//IE doesn't pass in the event object
event = event || window.event;
//IE uses srcElement as the target
var target = event.target || event.srcElement;
switch (target.className) {
case tzClassName:
// don't display timezone menu if it is already displayed
if (tzSelectFragment.childNodes.length > 0) {
displayChangeTZForm(target, tzSelectFragment);
}
break;
} // end switch
};
}
/**
* Create DocumentFragment with UI for changing common timezone in
* which dates are shown in.
*
* @param {DocumentFragment} documentFragment: where attach UI
* @param {String} tzSelected: default (pre-selected) timezone
* @param {Object} tzCookieInfo: object literal with info about cookie to store timezone
* @returns {DocumentFragment}
*/
function createChangeTZForm(documentFragment, tzSelected, tzCookieInfo, tzClassName) {
var div = document.createElement("div");
div.className = 'popup';
/* '<div class="close-button" title="(click on this box to close)">X</div>' */
var closeButton = document.createElement('div');
closeButton.className = 'close-button';
closeButton.title = '(click on this box to close)';
closeButton.appendChild(document.createTextNode('X'));
closeButton.onclick = closeTZFormHandler(documentFragment, tzClassName);
div.appendChild(closeButton);
/* 'Select timezone: <br clear="all">' */
div.appendChild(document.createTextNode('Select timezone: '));
var br = document.createElement('br');
br.clear = 'all';
div.appendChild(br);
/* '<select name="tzoffset">
* ...
* <option value="-0700">UTC-07:00</option>
* <option value="-0600">UTC-06:00</option>
* ...
* </select>' */
var select = document.createElement("select");
select.name = "tzoffset";
//select.style.clear = 'all';
select.appendChild(generateTZOptions(tzSelected));
select.onchange = selectTZHandler(documentFragment, tzCookieInfo, tzClassName);
div.appendChild(select);
documentFragment.appendChild(div);
return documentFragment;
}
/**
* Hide (remove from DOM) timezone change UI, ensuring that it is not
* garbage collected and that it can be re-enabled later.
*
* @param {DocumentFragment} documentFragment: contains detached UI
* @param {HTMLSelectElement} target: select element inside of UI
* @param {String} tzClassName: specifies element where UI was installed
* @returns {DocumentFragment} documentFragment
*/
function removeChangeTZForm(documentFragment, target, tzClassName) {
// find containing element, where we appended timezone selection UI
// `target' is somewhere inside timezone menu
var container = target.parentNode, popup = target;
while (container &&
container.className !== tzClassName) {
popup = container;
container = container.parentNode;
}
// safety check if we found correct container,
// and if it isn't deleted already
if (!container || !popup ||
container.className !== tzClassName ||
popup.className !== 'popup') {
return documentFragment;
}
// timezone selection UI was appended as last child
// see also displayChangeTZForm function
var removed = popup.parentNode.removeChild(popup);
if (documentFragment.firstChild !== removed) { // the only child
// re-append it so it would be available for next time
documentFragment.appendChild(removed);
}
// all of inline style was added by this script
// it is not really needed to remove it, but it is a good practice
container.removeAttribute('style');
return documentFragment;
}
/**
* Display UI for changing common timezone for dates in gitweb output.
* To be used from 'onclick' event handler.
*
* @param {HTMLElement} target: where to install/display UI
* @param {DocumentFragment} tzSelectFragment: timezone selection UI
*/
function displayChangeTZForm(target, tzSelectFragment) {
// for absolute positioning to be related to target element
target.style.position = 'relative';
target.style.display = 'inline-block';
// show/display UI for changing timezone
target.appendChild(tzSelectFragment);
}
/* ...................................................................... */
/* List of timezones for timezone selection menu */
/**
* Generate list of timezones for creating timezone select UI
*
* @returns {Object[]} list of e.g. { value: '+0100', descr: 'GMT+01:00' }
*/
function generateTZList() {
var timezones = [
{ value: "utc", descr: "UTC/GMT"},
{ value: "local", descr: "Local (per browser)"}
];
// generate all full hour timezones (no fractional timezones)
for (var x = -12, idx = timezones.length; x <= +14; x++, idx++) {
var hours = (x >= 0 ? '+' : '-') + padLeft(x >=0 ? x : -x, 2);
timezones[idx] = { value: hours + '00', descr: 'UTC' + hours + ':00'};
if (x === 0) {
timezones[idx].descr = 'UTC\u00B100:00'; // 'UTC&plusmn;00:00'
}
}
return timezones;
}
/**
* Generate <options> elements for timezone select UI
*
* @param {String} tzSelected: default timezone
* @returns {DocumentFragment} list of options elements to appendChild
*/
function generateTZOptions(tzSelected) {
var elems = document.createDocumentFragment();
var timezones = generateTZList();
for (var i = 0, len = timezones.length; i < len; i++) {
var tzone = timezones[i];
var option = document.createElement("option");
if (tzone.value === tzSelected) {
option.defaultSelected = true;
}
option.value = tzone.value;
option.appendChild(document.createTextNode(tzone.descr));
elems.appendChild(option);
}
return elems;
}
/* ...................................................................... */
/* Event handlers and/or their generators */
/**
* Create event handler that select timezone and closes timezone select UI.
* To be used as $('select[name="tzselect"]').onchange handler.
*
* @param {DocumentFragment} tzSelectFragment: timezone selection UI
* @param {Object} tzCookieInfo: object literal with info about cookie to store timezone
* @param {String} tzCookieInfo.name: name of cookie to save result of selection
* @param {String} tzClassName: specifies element where UI was installed
* @returns {Function} event handler
*/
function selectTZHandler(tzSelectFragment, tzCookieInfo, tzClassName) {
//return function selectTZ(event) {
return function (event) {
event = event || window.event;
var target = event.target || event.srcElement;
var selected = target.options.item(target.selectedIndex);
removeChangeTZForm(tzSelectFragment, target, tzClassName);
if (selected) {
selected.defaultSelected = true;
setCookie(tzCookieInfo.name, selected.value, tzCookieInfo);
fixDatetimeTZ(selected.value, tzClassName);
}
};
}
/**
* Create event handler that closes timezone select UI.
* To be used e.g. as $('.closebutton').onclick handler.
*
* @param {DocumentFragment} tzSelectFragment: timezone selection UI
* @param {String} tzClassName: specifies element where UI was installed
* @returns {Function} event handler
*/
function closeTZFormHandler(tzSelectFragment, tzClassName) {
//return function closeTZForm(event) {
return function (event) {
event = event || window.event;
var target = event.target || event.srcElement;
removeChangeTZForm(tzSelectFragment, target, tzClassName);
};
}
/* end of adjust-timezone.js */