gitweb/static/js/adjust-timezone.js

Summary

Maintainability
A
1 hr
Test Coverage
// 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 */