Skip to content

Commit 2ae8da2

Browse files
warthog9gitster
authored andcommitted
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 <[email protected]> Signed-off-by: John 'Warthog9' Hawley <[email protected]> Signed-off-by: Jakub Narebski <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 291e52b commit 2ae8da2

File tree

4 files changed

+345
-16
lines changed

4 files changed

+345
-16
lines changed

gitweb/gitweb.perl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3738,7 +3738,8 @@ sub git_footer_html {
37383738
(gitweb_check_feature('javascript-actions') ?
37393739
qq! fixLinks();\n! : '').
37403740
# 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!.
37423743
qq!};\n!.
37433744
qq!</script>\n!;
37443745
}

gitweb/static/gitweb.css

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,39 @@ div.remote {
579579
display: inline-block;
580580
}
581581

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+
582615
/* Style definition generated by highlight 2.4.5, http://www.andre-simon.de/ */
583616

584617
/* Highlighting theme definition: */

gitweb/static/js/adjust-timezone.js

Lines changed: 284 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,51 @@
77
*/
88

99
/**
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.
1112
*
1213
* This function is called during onload event (added to window.onload).
1314
*
1415
* @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
1618
* @param {String} tzClassName: denotes elements with date to be adjusted
1719
*/
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);
2133

2234
// server-side of gitweb produces datetime in UTC,
2335
// 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);
2740
}
2841

2942

43+
/* ...................................................................... */
44+
/* Changing dates to use requested timezone */
45+
3046
/**
3147
* Replace RFC-2822 dates contained in SPAN elements with tzClassName
3248
* CSS class with equivalent dates in given timezone.
3349
*
3450
* @param {String} tz: numeric timezone in '(-|+)HHMM' format, or 'utc', or 'local'
3551
* @param {String} tzClassName: specifies elements to be changed
52+
* @param {Boolean} nochange: markup for timezone change, but don't change it
3653
*/
37-
function fixDatetimeTZ(tz, tzClassName) {
54+
function fixDatetimeTZ(tz, tzClassName, nochange) {
3855
// sanity check, method should be ensured by common-lib.js
3956
if (!document.getElementsByClassName) {
4057
return;
@@ -48,13 +65,266 @@ function fixDatetimeTZ(tz, tzClassName) {
4865
for (var i = 0, len = classesFound.length; i < len; i++) {
4966
var curElement = classesFound[i];
5067

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);
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+
}
55197

56-
curElement.firstChild.data = adjusted;
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);
57204
}
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+
};
58328
}
59329

60330
/* end of adjust-timezone.js */

0 commit comments

Comments
 (0)