Fossil SCM

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

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button