gitweb: handle local DST properly
[git/gitweb.git] / gitweb / static / js / adjust-timezone.js
blobcc32df3734bddcbf10fc57847e3c049473b980d8
1 // Copyright (C) 2011, John 'Warthog9' Hawley <warthog9@eaglescrag.net>
2 //               2011, Jakub Narebski <jnareb@gmail.com>
4 /**
5  * @fileOverview Manipulate dates in gitweb output, adjusting timezone
6  * @license GPLv2 or later
7  */
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;
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         }
31         // add UI for changing timezone
32         addChangeTZ(tz, tzCookieInfo, tzClassName);
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';
38         // adjust dates to use specified common timezone
39         fixDatetimeTZ(tz, tzClassName, nochange);
43 /* ...................................................................... */
44 /* Changing dates to use requested timezone */
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         }
60         // NOTE: result of getElementsByClassName should probably be cached
61         var classesFound = document.getElementsByClassName(tzClassName, "span");
62         for (var i = 0, len = classesFound.length; i < len; i++) {
63                 var curElement = classesFound[i];
65                 curElement.title = 'Click to change timezone';
66                 if (!nochange) {
67                         // we use *.firstChild.data (W3C DOM) instead of *.innerHTML
68                         // as the latter doesn't always work everywhere in every browser
69                         var epoch = parseRFC2822Date(curElement.firstChild.data);
70                         var adjusted = formatDateRFC2882(epoch, tz);
72                         curElement.firstChild.data = adjusted;
73                 }
74         }
78 /* ...................................................................... */
79 /* Adding triggers, generating timezone menu, displaying and hiding */
81 /**
82  * Adds triggers for UI to change common timezone used for dates in
83  * gitweb output: it marks up and/or creates item to click to invoke
84  * timezone change UI, creates timezone UI fragment to be attached,
85  * and installs appropriate onclick trigger (via event delegation).
86  *
87  * @param {String} tzSelected: pre-selected timezone,
88  *                             'utc' or 'local' or '(-|+)HHMM'
89  * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone
90  * @param {String} tzClassName: specifies elements to install trigger
91  */
92 function addChangeTZ(tzSelected, tzCookieInfo, tzClassName) {
93         // make link to timezone UI discoverable
94         addCssRule('.'+tzClassName + ':hover',
95                    'text-decoration: underline; cursor: help;');
97         // create form for selecting timezone (to be saved in a cookie)
98         var tzSelectFragment = document.createDocumentFragment();
99         tzSelectFragment = createChangeTZForm(tzSelectFragment,
100                                               tzSelected, tzCookieInfo, tzClassName);
102         // event delegation handler for timezone selection UI (clicking on entry)
103         // see http://www.nczonline.net/blog/2009/06/30/event-delegation-in-javascript/
104         // assumes that there is no existing document.onclick handler
105         document.onclick = function onclickHandler(event) {
106                 //IE doesn't pass in the event object
107                 event = event || window.event;
109                 //IE uses srcElement as the target
110                 var target = event.target || event.srcElement;
112                 switch (target.className) {
113                 case tzClassName:
114                         // don't display timezone menu if it is already displayed
115                         if (tzSelectFragment.childNodes.length > 0) {
116                                 displayChangeTZForm(target, tzSelectFragment);
117                         }
118                         break;
119                 } // end switch
120         };
124  * Create DocumentFragment with UI for changing common timezone in
125  * which dates are shown in.
127  * @param {DocumentFragment} documentFragment: where attach UI
128  * @param {String} tzSelected: default (pre-selected) timezone
129  * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone
130  * @returns {DocumentFragment}
131  */
132 function createChangeTZForm(documentFragment, tzSelected, tzCookieInfo, tzClassName) {
133         var div = document.createElement("div");
134         div.className = 'popup';
136         /* '<div class="close-button" title="(click on this box to close)">X</div>' */
137         var closeButton = document.createElement('div');
138         closeButton.className = 'close-button';
139         closeButton.title = '(click on this box to close)';
140         closeButton.appendChild(document.createTextNode('X'));
141         closeButton.onclick = closeTZFormHandler(documentFragment, tzClassName);
142         div.appendChild(closeButton);
144         /* 'Select timezone: <br clear="all">' */
145         div.appendChild(document.createTextNode('Select timezone: '));
146         var br = document.createElement('br');
147         br.clear = 'all';
148         div.appendChild(br);
150         /* '<select name="tzoffset">
151          *    ...
152          *    <option value="-0700">UTC-07:00</option>
153          *    <option value="-0600">UTC-06:00</option>
154          *    ...
155          *  </select>' */
156         var select = document.createElement("select");
157         select.name = "tzoffset";
158         //select.style.clear = 'all';
159         select.appendChild(generateTZOptions(tzSelected));
160         select.onchange = selectTZHandler(documentFragment, tzCookieInfo, tzClassName);
161         div.appendChild(select);
163         documentFragment.appendChild(div);
165         return documentFragment;
170  * Hide (remove from DOM) timezone change UI, ensuring that it is not
171  * garbage collected and that it can be re-enabled later.
173  * @param {DocumentFragment} documentFragment: contains detached UI
174  * @param {HTMLSelectElement} target: select element inside of UI
175  * @param {String} tzClassName: specifies element where UI was installed
176  * @returns {DocumentFragment} documentFragment
177  */
178 function removeChangeTZForm(documentFragment, target, tzClassName) {
179         // find containing element, where we appended timezone selection UI
180         // `target' is somewhere inside timezone menu
181         var container = target.parentNode, popup = target;
182         while (container &&
183                container.className !== tzClassName) {
184                 popup = container;
185                 container = container.parentNode;
186         }
187         // safety check if we found correct container,
188         // and if it isn't deleted already
189         if (!container || !popup ||
190             container.className !== tzClassName ||
191             popup.className     !== 'popup') {
192                 return documentFragment;
193         }
195         // timezone selection UI was appended as last child
196         // see also displayChangeTZForm function
197         var removed = popup.parentNode.removeChild(popup);
198         if (documentFragment.firstChild !== removed) { // the only child
199                 // re-append it so it would be available for next time
200                 documentFragment.appendChild(removed);
201         }
202         // all of inline style was added by this script
203         // it is not really needed to remove it, but it is a good practice
204         container.removeAttribute('style');
206         return documentFragment;
211  * Display UI for changing common timezone for dates in gitweb output.
212  * To be used from 'onclick' event handler.
214  * @param {HTMLElement} target: where to install/display UI
215  * @param {DocumentFragment} tzSelectFragment: timezone selection UI
216  */
217 function displayChangeTZForm(target, tzSelectFragment) {
218         // for absolute positioning to be related to target element
219         target.style.position = 'relative';
220         target.style.display = 'inline-block';
222         // show/display UI for changing timezone
223         target.appendChild(tzSelectFragment);
227 /* ...................................................................... */
228 /* List of timezones for timezone selection menu */
231  * Generate list of timezones for creating timezone select UI
233  * @returns {Object[]} list of e.g. { value: '+0100', descr: 'GMT+01:00' }
234  */
235 function generateTZList() {
236         var timezones = [
237                 { value: "utc",   descr: "UTC/GMT"},
238                 { value: "local", descr: "Local (per browser)"}
239         ];
241         // generate all full hour timezones (no fractional timezones)
242         for (var x = -12, idx = timezones.length; x <= +14; x++, idx++) {
243                 var hours = (x >= 0 ? '+' : '-') + padLeft(x >=0 ? x : -x, 2);
244                 timezones[idx] = { value: hours + '00', descr: 'UTC' + hours + ':00'};
245                 if (x === 0) {
246                         timezones[idx].descr = 'UTC\u00B100:00'; // 'UTC&plusmn;00:00'
247                 }
248         }
250         return timezones;
254  * Generate <options> elements for timezone select UI
256  * @param {String} tzSelected: default timezone
257  * @returns {DocumentFragment} list of options elements to appendChild
258  */
259 function generateTZOptions(tzSelected) {
260         var elems = document.createDocumentFragment();
261         var timezones = generateTZList();
263         for (var i = 0, len = timezones.length; i < len; i++) {
264                 var tzone = timezones[i];
265                 var option = document.createElement("option");
266                 if (tzone.value === tzSelected) {
267                         option.defaultSelected = true;
268                 }
269                 option.value = tzone.value;
270                 option.appendChild(document.createTextNode(tzone.descr));
272                 elems.appendChild(option);
273         }
275         return elems;
279 /* ...................................................................... */
280 /* Event handlers and/or their generators */
283  * Create event handler that select timezone and closes timezone select UI.
284  * To be used as $('select[name="tzselect"]').onchange handler.
286  * @param {DocumentFragment} tzSelectFragment: timezone selection UI
287  * @param {Object} tzCookieInfo: object literal with info about cookie to store timezone
288  * @param {String} tzCookieInfo.name: name of cookie to save result of selection
289  * @param {String} tzClassName: specifies element where UI was installed
290  * @returns {Function} event handler
291  */
292 function selectTZHandler(tzSelectFragment, tzCookieInfo, tzClassName) {
293         //return function selectTZ(event) {
294         return function (event) {
295                 event = event || window.event;
296                 var target = event.target || event.srcElement;
298                 var selected = target.options.item(target.selectedIndex);
299                 removeChangeTZForm(tzSelectFragment, target, tzClassName);
301                 if (selected) {
302                         selected.defaultSelected = true;
303                         setCookie(tzCookieInfo.name, selected.value, tzCookieInfo);
304                         fixDatetimeTZ(selected.value, tzClassName);
305                 }
306         };
310  * Create event handler that closes timezone select UI.
311  * To be used e.g. as $('.closebutton').onclick handler.
313  * @param {DocumentFragment} tzSelectFragment: timezone selection UI
314  * @param {String} tzClassName: specifies element where UI was installed
315  * @returns {Function} event handler
316  */
317 function closeTZFormHandler(tzSelectFragment, tzClassName) {
318         //return function closeTZForm(event) {
319         return function (event) {
320                 event = event || window.event;
321                 var target = event.target || event.srcElement;
323                 removeChangeTZForm(tzSelectFragment, target, tzClassName);
324         };
327 /* end of adjust-timezone.js */