| 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 | */ |
| 20 | function 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 | */ |
| 54 | function 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 | */ |
| 95 | function 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 | */ |
| 135 | function 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 | */ |
| 181 | function 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 | */ |
| 220 | function 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 | */ |
| 238 | function 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±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 | */ |
| 262 | function 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 | */ |
| 295 | function 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 | */ |
| 320 | function 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 */ |