Git fork

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>

authored by

John 'Warthog9' Hawley and committed by
Junio C Hamano
2ae8da25 291e52bd

+345 -16
+2 -1
gitweb/gitweb.perl
··· 3738 3738 (gitweb_check_feature('javascript-actions') ? 3739 3739 qq! fixLinks();\n! : ''). 3740 3740 # last parameter to onloadTZSetup must be CSS class used by format_timestamp_html 3741 - qq! onloadTZSetup('local', 'gitweb_tz', 'datetime');\n!. 3741 + qq! var tz_cookie = { name: 'gitweb_tz', expires: 14, path: '/' };\n!. # in days 3742 + qq! onloadTZSetup('local', tz_cookie, 'datetime');\n!. 3742 3743 qq!};\n!. 3743 3744 qq!</script>\n!; 3744 3745 }
+33
gitweb/static/gitweb.css
··· 579 579 display: inline-block; 580 580 } 581 581 582 + /* JavaScript-based timezone manipulation */ 583 + 584 + .popup { /* timezone selection UI */ 585 + position: absolute; 586 + /* "top: 0; right: 0;" would be better, if not for bugs in browsers */ 587 + top: 0; left: 0; 588 + border: 1px solid; 589 + padding: 2px; 590 + background-color: #f0f0f0; 591 + font-style: normal; 592 + color: #000000; 593 + cursor: auto; 594 + } 595 + 596 + .close-button { /* close timezone selection UI without selecting */ 597 + /* float doesn't work within absolutely positioned container, 598 + * if width of container is not set explicitly */ 599 + /* float: right; */ 600 + position: absolute; 601 + top: 0px; right: 0px; 602 + border: 1px solid green; 603 + margin: 1px 1px 1px 1px; 604 + padding-bottom: 2px; 605 + width: 12px; 606 + height: 10px; 607 + font-size: 9px; 608 + font-weight: bold; 609 + text-align: center; 610 + background-color: #fff0f0; 611 + cursor: pointer; 612 + } 613 + 614 + 582 615 /* Style definition generated by highlight 2.4.5, http://www.andre-simon.de/ */ 583 616 584 617 /* Highlighting theme definition: */
+284 -14
gitweb/static/js/adjust-timezone.js
··· 7 7 */ 8 8 9 9 /** 10 - * Get common timezone and adjust dates to use this common timezone. 10 + * Get common timezone, add UI for changing timezones, and adjust 11 + * dates to use requested common timezone. 11 12 * 12 13 * This function is called during onload event (added to window.onload). 13 14 * 14 15 * @param {String} tzDefault: default timezone, if there is no cookie 15 - * @param {String} tzCookieName: name of cookie to store timezone 16 + * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone 17 + * @param {String} tzCookieInfo.name: name of cookie to store timezone 16 18 * @param {String} tzClassName: denotes elements with date to be adjusted 17 19 */ 18 - function onloadTZSetup(tzDefault, tzCookieName, tzClassName) { 19 - var tzCookie = getCookie(tzCookieName); 20 - var tz = tzCookie ? tzCookie : tzDefault; 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); 21 33 22 34 // server-side of gitweb produces datetime in UTC, 23 35 // so if tz is 'utc' there is no need for changes 24 - if (tz !== 'utc') { 25 - fixDatetimeTZ(tz, tzClassName); 26 - } 36 + var nochange = tz === 'utc'; 37 + 38 + // adjust dates to use specified common timezone 39 + fixDatetimeTZ(tz, tzClassName, nochange); 27 40 } 28 41 29 42 43 + /* ...................................................................... */ 44 + /* Changing dates to use requested timezone */ 45 + 30 46 /** 31 47 * Replace RFC-2822 dates contained in SPAN elements with tzClassName 32 48 * CSS class with equivalent dates in given timezone. 33 49 * 34 50 * @param {String} tz: numeric timezone in '(-|+)HHMM' format, or 'utc', or 'local' 35 51 * @param {String} tzClassName: specifies elements to be changed 52 + * @param {Boolean} nochange: markup for timezone change, but don't change it 36 53 */ 37 - function fixDatetimeTZ(tz, tzClassName) { 54 + function fixDatetimeTZ(tz, tzClassName, nochange) { 38 55 // sanity check, method should be ensured by common-lib.js 39 56 if (!document.getElementsByClassName) { 40 57 return; ··· 48 65 for (var i = 0, len = classesFound.length; i < len; i++) { 49 66 var curElement = classesFound[i]; 50 67 51 - // we use *.firstChild.data (W3C DOM) instead of *.innerHTML 52 - // as the latter doesn't always work everywhere in every browser 53 - var epoch = parseRFC2822Date(curElement.firstChild.data); 54 - var adjusted = formatDateRFC2882(epoch, tz); 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); 55 74 56 - curElement.firstChild.data = adjusted; 75 + curElement.firstChild.data = adjusted; 76 + } 57 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&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 + */ 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 + }; 58 328 } 59 329 60 330 /* end of adjust-timezone.js */
+26 -1
gitweb/static/js/lib/common-lib.js
··· 64 64 65 65 66 66 /* ............................................................ */ 67 - /* Ajax */ 67 + /* Handling browser incompatibilities */ 68 68 69 69 /** 70 70 * Create XMLHttpRequest object in cross-browser way ··· 85 85 } catch (e) {} 86 86 87 87 return null; 88 + } 89 + 90 + 91 + /** 92 + * Insert rule giving specified STYLE to given SELECTOR at the end of 93 + * first CSS stylesheet. 94 + * 95 + * @param {String} selector: CSS selector, e.g. '.class' 96 + * @param {String} style: rule contents, e.g. 'background-color: red;' 97 + */ 98 + function addCssRule(selector, style) { 99 + var stylesheet = document.styleSheets[0]; 100 + 101 + var theRules = []; 102 + if (stylesheet.cssRules) { // W3C way 103 + theRules = stylesheet.cssRules; 104 + } else if (stylesheet.rules) { // IE way 105 + theRules = stylesheet.rules; 106 + } 107 + 108 + if (stylesheet.insertRule) { // W3C way 109 + stylesheet.insertRule(selector + ' { ' + style + ' }', theRules.length); 110 + } else if (stylesheet.addRule) { // IE way 111 + stylesheet.addRule(selector, style); 112 + } 88 113 } 89 114 90 115