Merge branch 'jk/maint-do-not-feed-stdin-to-tests'
[git] / gitweb / static / js / adjust-timezone.js
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&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         };
328 }
329
330 /* end of adjust-timezone.js */