gitweb / static / js / adjust-timezone.json commit Merge branch 'maint' (ed16d0d)
   1// Copyright (C) 2011, John 'Warthog9' Hawley <warthog9@eaglescrag.net>
   2//               2011, Jakub Narebski <jnareb@gmail.com>
   3
   4/**
   5 * @fileOverview Manipulate dates in gitweb output, adjusting timezone
   6 * @license GPLv2 or later
   7 */
   8
   9/**
  10 * Get common timezone, add UI for changing timezones, and adjust
  11 * dates to use requested common timezone.
  12 *
  13 * This function is called during onload event (added to window.onload).
  14 *
  15 * @param {String} tzDefault: default timezone, if there is no cookie
  16 * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone
  17 * @param {String} tzCookieInfo.name: name of cookie to store timezone
  18 * @param {String} tzClassName: denotes elements with date to be adjusted
  19 */
  20function onloadTZSetup(tzDefault, tzCookieInfo, tzClassName) {
  21        var tzCookieTZ = getCookie(tzCookieInfo.name, tzCookieInfo);
  22        var tz = tzDefault;
  23
  24        if (tzCookieTZ) {
  25                // set timezone to value saved in a cookie
  26                tz = tzCookieTZ;
  27                // refresh cookie, so its expiration counts from last use of gitweb
  28                setCookie(tzCookieInfo.name, tzCookieTZ, tzCookieInfo);
  29        }
  30
  31        // add UI for changing timezone
  32        addChangeTZ(tz, tzCookieInfo, tzClassName);
  33
  34        // server-side of gitweb produces datetime in UTC,
  35        // so if tz is 'utc' there is no need for changes
  36        var nochange = tz === 'utc';
  37
  38        // adjust dates to use specified common timezone
  39        fixDatetimeTZ(tz, tzClassName, nochange);
  40}
  41
  42
  43/* ...................................................................... */
  44/* Changing dates to use requested timezone */
  45
  46/**
  47 * Replace RFC-2822 dates contained in SPAN elements with tzClassName
  48 * CSS class with equivalent dates in given timezone.
  49 *
  50 * @param {String} tz: numeric timezone in '(-|+)HHMM' format, or 'utc', or 'local'
  51 * @param {String} tzClassName: specifies elements to be changed
  52 * @param {Boolean} nochange: markup for timezone change, but don't change it
  53 */
  54function fixDatetimeTZ(tz, tzClassName, nochange) {
  55        // sanity check, method should be ensured by common-lib.js
  56        if (!document.getElementsByClassName) {
  57                return;
  58        }
  59
  60        // translate to timezone in '(-|+)HHMM' format
  61        tz = normalizeTimezoneInfo(tz);
  62
  63        // NOTE: result of getElementsByClassName should probably be cached
  64        var classesFound = document.getElementsByClassName(tzClassName, "span");
  65        for (var i = 0, len = classesFound.length; i < len; i++) {
  66                var curElement = classesFound[i];
  67
  68                curElement.title = 'Click to change timezone';
  69                if (!nochange) {
  70                        // we use *.firstChild.data (W3C DOM) instead of *.innerHTML
  71                        // as the latter doesn't always work everywhere in every browser
  72                        var epoch = parseRFC2822Date(curElement.firstChild.data);
  73                        var adjusted = formatDateRFC2882(epoch, tz);
  74
  75                        curElement.firstChild.data = adjusted;
  76                }
  77        }
  78}
  79
  80
  81/* ...................................................................... */
  82/* Adding triggers, generating timezone menu, displaying and hiding */
  83
  84/**
  85 * Adds triggers for UI to change common timezone used for dates in
  86 * gitweb output: it marks up and/or creates item to click to invoke
  87 * timezone change UI, creates timezone UI fragment to be attached,
  88 * and installs appropriate onclick trigger (via event delegation).
  89 *
  90 * @param {String} tzSelected: pre-selected timezone,
  91 *                             'utc' or 'local' or '(-|+)HHMM'
  92 * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone
  93 * @param {String} tzClassName: specifies elements to install trigger
  94 */
  95function addChangeTZ(tzSelected, tzCookieInfo, tzClassName) {
  96        // make link to timezone UI discoverable
  97        addCssRule('.'+tzClassName + ':hover',
  98                   'text-decoration: underline; cursor: help;');
  99
 100        // create form for selecting timezone (to be saved in a cookie)
 101        var tzSelectFragment = document.createDocumentFragment();
 102        tzSelectFragment = createChangeTZForm(tzSelectFragment,
 103                                              tzSelected, tzCookieInfo, tzClassName);
 104
 105        // event delegation handler for timezone selection UI (clicking on entry)
 106        // see http://www.nczonline.net/blog/2009/06/30/event-delegation-in-javascript/
 107        // assumes that there is no existing document.onclick handler
 108        document.onclick = function onclickHandler(event) {
 109                //IE doesn't pass in the event object
 110                event = event || window.event;
 111
 112                //IE uses srcElement as the target
 113                var target = event.target || event.srcElement;
 114
 115                switch (target.className) {
 116                case tzClassName:
 117                        // don't display timezone menu if it is already displayed
 118                        if (tzSelectFragment.childNodes.length > 0) {
 119                                displayChangeTZForm(target, tzSelectFragment);
 120                        }
 121                        break;
 122                } // end switch
 123        };
 124}
 125
 126/**
 127 * Create DocumentFragment with UI for changing common timezone in
 128 * which dates are shown in.
 129 *
 130 * @param {DocumentFragment} documentFragment: where attach UI
 131 * @param {String} tzSelected: default (pre-selected) timezone
 132 * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone
 133 * @returns {DocumentFragment}
 134 */
 135function createChangeTZForm(documentFragment, tzSelected, tzCookieInfo, tzClassName) {
 136        var div = document.createElement("div");
 137        div.className = 'popup';
 138
 139        /* '<div class="close-button" title="(click on this box to close)">X</div>' */
 140        var closeButton = document.createElement('div');
 141        closeButton.className = 'close-button';
 142        closeButton.title = '(click on this box to close)';
 143        closeButton.appendChild(document.createTextNode('X'));
 144        closeButton.onclick = closeTZFormHandler(documentFragment, tzClassName);
 145        div.appendChild(closeButton);
 146
 147        /* 'Select timezone: <br clear="all">' */
 148        div.appendChild(document.createTextNode('Select timezone: '));
 149        var br = document.createElement('br');
 150        br.clear = 'all';
 151        div.appendChild(br);
 152
 153        /* '<select name="tzoffset">
 154         *    ...
 155         *    <option value="-0700">UTC-07:00</option>
 156         *    <option value="-0600">UTC-06:00</option>
 157         *    ...
 158         *  </select>' */
 159        var select = document.createElement("select");
 160        select.name = "tzoffset";
 161        //select.style.clear = 'all';
 162        select.appendChild(generateTZOptions(tzSelected));
 163        select.onchange = selectTZHandler(documentFragment, tzCookieInfo, tzClassName);
 164        div.appendChild(select);
 165
 166        documentFragment.appendChild(div);
 167
 168        return documentFragment;
 169}
 170
 171
 172/**
 173 * Hide (remove from DOM) timezone change UI, ensuring that it is not
 174 * garbage collected and that it can be re-enabled later.
 175 *
 176 * @param {DocumentFragment} documentFragment: contains detached UI
 177 * @param {HTMLSelectElement} target: select element inside of UI
 178 * @param {String} tzClassName: specifies element where UI was installed
 179 * @returns {DocumentFragment} documentFragment
 180 */
 181function removeChangeTZForm(documentFragment, target, tzClassName) {
 182        // find containing element, where we appended timezone selection UI
 183        // `target' is somewhere inside timezone menu
 184        var container = target.parentNode, popup = target;
 185        while (container &&
 186               container.className !== tzClassName) {
 187                popup = container;
 188                container = container.parentNode;
 189        }
 190        // safety check if we found correct container,
 191        // and if it isn't deleted already
 192        if (!container || !popup ||
 193            container.className !== tzClassName ||
 194            popup.className     !== 'popup') {
 195                return documentFragment;
 196        }
 197
 198        // timezone selection UI was appended as last child
 199        // see also displayChangeTZForm function
 200        var removed = popup.parentNode.removeChild(popup);
 201        if (documentFragment.firstChild !== removed) { // the only child
 202                // re-append it so it would be available for next time
 203                documentFragment.appendChild(removed);
 204        }
 205        // all of inline style was added by this script
 206        // it is not really needed to remove it, but it is a good practice
 207        container.removeAttribute('style');
 208
 209        return documentFragment;
 210}
 211
 212
 213/**
 214 * Display UI for changing common timezone for dates in gitweb output.
 215 * To be used from 'onclick' event handler.
 216 *
 217 * @param {HTMLElement} target: where to install/display UI
 218 * @param {DocumentFragment} tzSelectFragment: timezone selection UI
 219 */
 220function displayChangeTZForm(target, tzSelectFragment) {
 221        // for absolute positioning to be related to target element
 222        target.style.position = 'relative';
 223        target.style.display = 'inline-block';
 224
 225        // show/display UI for changing timezone
 226        target.appendChild(tzSelectFragment);
 227}
 228
 229
 230/* ...................................................................... */
 231/* List of timezones for timezone selection menu */
 232
 233/**
 234 * Generate list of timezones for creating timezone select UI
 235 *
 236 * @returns {Object[]} list of e.g. { value: '+0100', descr: 'GMT+01:00' }
 237 */
 238function generateTZList() {
 239        var timezones = [
 240                { value: "utc",   descr: "UTC/GMT"},
 241                { value: "local", descr: "Local (per browser)"}
 242        ];
 243
 244        // generate all full hour timezones (no fractional timezones)
 245        for (var x = -12, idx = timezones.length; x <= +14; x++, idx++) {
 246                var hours = (x >= 0 ? '+' : '-') + padLeft(x >=0 ? x : -x, 2);
 247                timezones[idx] = { value: hours + '00', descr: 'UTC' + hours + ':00'};
 248                if (x === 0) {
 249                        timezones[idx].descr = 'UTC\u00B100:00'; // 'UTC&plusmn;00:00'
 250                }
 251        }
 252
 253        return timezones;
 254}
 255
 256/**
 257 * Generate <options> elements for timezone select UI
 258 *
 259 * @param {String} tzSelected: default timezone
 260 * @returns {DocumentFragment} list of options elements to appendChild
 261 */
 262function generateTZOptions(tzSelected) {
 263        var elems = document.createDocumentFragment();
 264        var timezones = generateTZList();
 265
 266        for (var i = 0, len = timezones.length; i < len; i++) {
 267                var tzone = timezones[i];
 268                var option = document.createElement("option");
 269                if (tzone.value === tzSelected) {
 270                        option.defaultSelected = true;
 271                }
 272                option.value = tzone.value;
 273                option.appendChild(document.createTextNode(tzone.descr));
 274
 275                elems.appendChild(option);
 276        }
 277
 278        return elems;
 279}
 280
 281
 282/* ...................................................................... */
 283/* Event handlers and/or their generators */
 284
 285/**
 286 * Create event handler that select timezone and closes timezone select UI.
 287 * To be used as $('select[name="tzselect"]').onchange handler.
 288 *
 289 * @param {DocumentFragment} tzSelectFragment: timezone selection UI
 290 * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone
 291 * @param {String} tzCookieInfo.name: name of cookie to save result of selection
 292 * @param {String} tzClassName: specifies element where UI was installed
 293 * @returns {Function} event handler
 294 */
 295function selectTZHandler(tzSelectFragment, tzCookieInfo, tzClassName) {
 296        //return function selectTZ(event) {
 297        return function (event) {
 298                event = event || window.event;
 299                var target = event.target || event.srcElement;
 300
 301                var selected = target.options.item(target.selectedIndex);
 302                removeChangeTZForm(tzSelectFragment, target, tzClassName);
 303
 304                if (selected) {
 305                        selected.defaultSelected = true;
 306                        setCookie(tzCookieInfo.name, selected.value, tzCookieInfo);
 307                        fixDatetimeTZ(selected.value, tzClassName);
 308                }
 309        };
 310}
 311
 312/**
 313 * Create event handler that closes timezone select UI.
 314 * To be used e.g. as $('.closebutton').onclick handler.
 315 *
 316 * @param {DocumentFragment} tzSelectFragment: timezone selection UI
 317 * @param {String} tzClassName: specifies element where UI was installed
 318 * @returns {Function} event handler
 319 */
 320function closeTZFormHandler(tzSelectFragment, tzClassName) {
 321        //return function closeTZForm(event) {
 322        return function (event) {
 323                event = event || window.event;
 324                var target = event.target || event.srcElement;
 325
 326                removeChangeTZForm(tzSelectFragment, target, tzClassName);
 327        };
 328}
 329
 330/* end of adjust-timezone.js */