1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
|
|
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
|
/*
** Copyright © 2018 Warren Young
**
** This program is free software; you can redistribute it and/or
** modify it under the terms of the Simplified BSD License (also
** known as the "2-Clause License" or "FreeBSD License".)
**
** This program is distributed in the hope that it will be useful,
** but without any warranty; without even the implied warranty of
** merchantability or fitness for a particular purpose.
**
** Contact: wyoung on the Fossil forum, https://fossil-scm.org/forum/
**
*******************************************************************************
**
** This file contains the JS code specific to the Fossil default skin.
** Currently, the only thing this does is handle clicks on its hamburger
** menu button.
*/
(function() {
var hbButton = document.getElementById("hbbtn");
if (!hbButton) return; // no hamburger button
if (!document.addEventListener) {
// Turn the button into a link to the sitemap for incompatible browsers.
hbButton.href = "$home/sitemap";
return;
}
var panel = document.getElementById("hbdrop");
if (!panel) return; // site admin might've nuked it
if (!panel.style) return; // shouldn't happen, but be sure
var panelBorder = panel.style.border;
var panelInitialized = false; // reset if browser window is resized
var panelResetBorderTimerID = 0; // used to cancel post-animation tasks
// Disable animation if this browser doesn't support CSS transitions.
//
// We need this ugly calling form for old browsers that don't allow
// panel.style.hasOwnProperty('transition'); catering to old browsers
// is the whole point here.
var animate = panel.style.transition !== null && (typeof(panel.style.transition) == "string");
// The duration of the animation can be overridden from the default skin
// header.txt by setting the "data-anim-ms" attribute of the panel.
var animMS = panel.getAttribute("data-anim-ms");
if (animMS) { // not null or empty string, parse it
animMS = parseInt(animMS);
if (isNaN(animMS) || animMS == 0)
animate = false; // disable animation if non-numeric or zero
else if (animMS < 0)
animMS = 400; // set default animation duration if negative
}
else // attribute is null or empty string, use default
animMS = 400;
// Calculate panel height despite its being hidden at call time.
// Based on https://stackoverflow.com/a/29047447/142454
var panelHeight; // computed on first panel display
function calculatePanelHeight() {
// Clear the max-height CSS property in case the panel size is recalculated
// after the browser window was resized.
panel.style.maxHeight = '';
// Get initial panel styles so we can restore them below.
var es = window.getComputedStyle(panel),
edis = es.display,
epos = es.position,
evis = es.visibility;
// Restyle the panel so we can measure its height while invisible.
panel.style.visibility = 'hidden';
panel.style.position = 'absolute';
panel.style.display = 'block';
panelHeight = panel.offsetHeight + 'px';
// Revert styles now that job is done.
panel.style.display = edis;
panel.style.position = epos;
panel.style.visibility = evis;
}
// Show the panel by changing the panel height, which kicks off the
// slide-open/closed transition set up in the XHR onload handler.
//
// Schedule the change for a near-future time in case this is the
// first call, where the div was initially invisible. If we were
// to change the panel's visibility and height at the same time
// instead, that would prevent the browser from seeing the height
// change as a state transition, so it'd skip the CSS transition:
//
// https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions#JavaScript_examples
function showPanel() {
// Cancel the timer to remove the panel border after the closing animation,
// otherwise double-clicking the hamburger button with the panel opened will
// remove the borders from the (closed and immediately reopened) panel.
if (panelResetBorderTimerID) {
clearTimeout(panelResetBorderTimerID);
panelResetBorderTimerID = 0;
}
if (animate) {
if (!panelInitialized) {
panelInitialized = true;
// Set up a CSS transition to animate the panel open and
// closed. Only needs to be done once per page load.
// Based on https://stackoverflow.com/a/29047447/142454
calculatePanelHeight();
panel.style.transition = 'max-height ' + animMS +
'ms ease-in-out';
panel.style.overflowY = 'hidden';
panel.style.maxHeight = '0';
}
setTimeout(function() {
panel.style.maxHeight = panelHeight;
panel.style.border = panelBorder;
}, 40); // 25ms is insufficient with Firefox 62
}
panel.style.display = 'block';
document.addEventListener('keydown',panelKeydown,/* useCapture == */true);
document.addEventListener('click',panelClick,false);
}
var panelKeydown = function(event) {
var key = event.which || event.keyCode;
if (key == 27) {
event.stopPropagation(); // ignore other keydown handlers
panelToggle(true);
}
};
var panelClick = function(event) {
if (!panel.contains(event.target)) {
// Call event.preventDefault() to have clicks outside the opened panel
// just close the panel, and swallow clicks on links or form elements.
//event.preventDefault();
panelToggle(true);
}
};
// Return true if the panel is showing.
function panelShowing() {
if (animate) {
return panel.style.maxHeight == panelHeight;
}
else {
return panel.style.display == 'block';
}
}
// Check if the specified HTML element has any child elements. Note that plain
// text nodes, comments, and any spaces (presentational or not) are ignored.
function hasChildren(element) {
var childElement = element.firstChild;
while (childElement) {
if (childElement.nodeType == 1) // Node.ELEMENT_NODE == 1
return true;
childElement = childElement.nextSibling;
}
return false;
}
// Reset the state of the panel to uninitialized if the browser window is
// resized, so the dimensions are recalculated the next time it's opened.
window.addEventListener('resize',function(event) {
panelInitialized = false;
},false);
// Click handler for the hamburger button.
hbButton.addEventListener('click',function(event) {
// Break the event handler chain, or the handler for document → click
// (about to be installed) may already be triggered by the current event.
event.stopPropagation();
event.preventDefault(); // prevent browser from acting on <a> click
panelToggle(false);
},false);
function panelToggle(suppressAnimation) {
if (panelShowing()) {
document.removeEventListener('keydown',panelKeydown,/* useCapture == */true);
document.removeEventListener('click',panelClick,false);
// Transition back to hidden state.
if (animate) {
if (suppressAnimation) {
var transition = panel.style.transition;
panel.style.transition = '';
panel.style.maxHeight = '0';
panel.style.border = 'none';
setTimeout(function() {
// Make sure CSS transition won't take effect now, so restore it
// asynchronously. Outer variable 'transition' still valid here.
panel.style.transition = transition;
}, 40); // 25ms is insufficient with Firefox 62
}
else {
panel.style.maxHeight = '0';
panelResetBorderTimerID = setTimeout(function() {
// Browsers show a 1px high border line when maxHeight == 0,
// our "hidden" state, so hide the borders in that state, too.
panel.style.border = 'none';
panelResetBorderTimerID = 0; // clear ID of completed timer
}, animMS);
}
}
else {
panel.style.display = 'none';
}
}
else {
if (!hasChildren(panel)) {
// Only get the sitemap once per page load: it isn't likely to
// change on us.
var xhr = new XMLHttpRequest();
xhr.onload = function() {
var doc = xhr.responseXML;
if (doc) {
var sm = doc.querySelector("ul#sitemap");
if (sm && xhr.status == 200) {
// Got sitemap. Insert it into the drop-down panel.
panel.innerHTML = sm.outerHTML;
// Display the panel
showPanel();
}
}
// else, can't parse response as HTML or XML
}
xhr.open("GET", "$home/sitemap?popup"); // note the TH1 substitution!
xhr.responseType = "document";
xhr.send();
}
else {
showPanel(); // just show what we built above
}
}
}
})();
|