gitweb.js: Add UI for selecting common timezone to display dates
authorJohn 'Warthog9' Hawley <warthog9@eaglescrag.net>
Thu, 28 Apr 2011 19:04:10 +0000 (21:04 +0200)
committerJunio C Hamano <gitster@pobox.com>
Tue, 24 May 2011 18:22:45 +0000 (11:22 -0700)
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>
gitweb/gitweb.perl
gitweb/static/gitweb.css
gitweb/static/js/adjust-timezone.js
gitweb/static/js/lib/common-lib.js

index 6651946f54f8fbfd8b96701753caa028f9814232..b1e80ef87bb18912479180e8d4f596e052f5bf1a 100755 (executable)
@@ -3738,7 +3738,8 @@ sub git_footer_html {
                      (gitweb_check_feature('javascript-actions') ?
                      qq!       fixLinks();\n! : '').
                      # last parameter to onloadTZSetup must be CSS class used by format_timestamp_html
-                     qq!       onloadTZSetup('local', 'gitweb_tz', 'datetime');\n!.
+                     qq!       var tz_cookie = { name: 'gitweb_tz', expires: 14, path: '/' };\n!. # in days
+                     qq!       onloadTZSetup('local', tz_cookie, 'datetime');\n!.
                      qq!};\n!.
                      qq!</script>\n!;
        }
index 79d7eebba797f3bd80a639f6ca0835e15016762d..8dd093563eb879872821c62008b35d32a819e9d1 100644 (file)
@@ -579,6 +579,39 @@ div.remote {
        display: inline-block;
 }
 
+/* JavaScript-based timezone manipulation */
+
+.popup { /* timezone selection UI */
+       position: absolute;
+       /* "top: 0; right: 0;" would be better, if not for bugs in browsers */
+       top: 0; left: 0;
+       border: 1px solid;
+       padding: 2px;
+       background-color: #f0f0f0;
+       font-style: normal;
+       color: #000000;
+       cursor: auto;
+}
+
+.close-button { /* close timezone selection UI without selecting */
+       /* float doesn't work within absolutely positioned container,
+        * if width of container is not set explicitly */
+       /* float: right; */
+       position: absolute;
+       top: 0px; right: 0px;
+       border:  1px solid green;
+       margin:  1px 1px 1px 1px;
+       padding-bottom: 2px;
+       width:     12px;
+       height:    10px;
+       font-size:  9px;
+       font-weight: bold;
+       text-align: center;
+       background-color: #fff0f0;
+       cursor: pointer;
+}
+
+
 /* Style definition generated by highlight 2.4.5, http://www.andre-simon.de/ */
 
 /* Highlighting theme definition: */
index 1577d780f0f34a2e910d67bdfd726f32af56d244..0c677795006a2e2f2dc138abf7566b7670ba6fee 100644 (file)
@@ -7,34 +7,51 @@
  */
 
 /**
- * Get common timezone and adjust dates to use this common timezone.
+ * 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 {String} tzCookieName: name of cookie to store timezone
+ * @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, tzCookieName, tzClassName) {
-       var tzCookie = getCookie(tzCookieName);
-       var tz = tzCookie ? tzCookie : tzDefault;
+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
-       if (tz !== 'utc') {
-               fixDatetimeTZ(tz, tzClassName);
-       }
+       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) {
+function fixDatetimeTZ(tz, tzClassName, nochange) {
        // sanity check, method should be ensured by common-lib.js
        if (!document.getElementsByClassName) {
                return;
@@ -48,13 +65,266 @@ function fixDatetimeTZ(tz, tzClassName) {
        for (var i = 0, len = classesFound.length; i < len; i++) {
                var curElement = classesFound[i];
 
-               // 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.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;
+       }
 
-               curElement.firstChild.data = adjusted;
+       // 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 */
index b37139152db790a7c6dfb4073fefff2ffa3eab98..018bbb7d4cb72eeb786c3309ba318e7789a02b37 100644 (file)
@@ -64,7 +64,7 @@ function padLeft(input, width, ch) {
 
 
 /* ............................................................ */
-/* Ajax */
+/* Handling browser incompatibilities */
 
 /**
  * Create XMLHttpRequest object in cross-browser way
@@ -88,6 +88,31 @@ function createRequestObject() {
 }
 
 
+/**
+ * Insert rule giving specified STYLE to given SELECTOR at the end of
+ * first CSS stylesheet.
+ *
+ * @param {String} selector: CSS selector, e.g. '.class'
+ * @param {String} style: rule contents, e.g. 'background-color: red;'
+ */
+function addCssRule(selector, style) {
+       var stylesheet = document.styleSheets[0];
+
+       var theRules = [];
+       if (stylesheet.cssRules) {     // W3C way
+               theRules = stylesheet.cssRules;
+       } else if (stylesheet.rules) { // IE way
+               theRules = stylesheet.rules;
+       }
+
+       if (stylesheet.insertRule) {    // W3C way
+               stylesheet.insertRule(selector + ' { ' + style + ' }', theRules.length);
+       } else if (stylesheet.addRule) { // IE way
+               stylesheet.addRule(selector, style);
+       }
+}
+
+
 /* ............................................................ */
 /* Support for legacy browsers */