Index: skins/default/footer.txt ================================================================== --- skins/default/footer.txt +++ skins/default/footer.txt @@ -1,8 +1,5 @@ - Index: skins/default/header.txt ================================================================== --- skins/default/header.txt +++ skins/default/header.txt @@ -18,10 +18,11 @@ } else { html "$name\n" } } html "" +builtin_request_js hbmenu.js menulink $index_page Home {} if {[anycap jor]} { menulink /timeline Timeline {} } if {[hascap oh]} { DELETED skins/default/js.txt Index: skins/default/js.txt ================================================================== --- skins/default/js.txt +++ /dev/null @@ -1,234 +0,0 @@ -/* -** 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 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 - } - } - } -})(); Index: skins/plain_gray/header.txt ================================================================== --- skins/plain_gray/header.txt +++ skins/plain_gray/header.txt @@ -2,10 +2,11 @@
$: $</div> </div> <div class="mainmenu"> <th1> html "<a id='hbbtn' href='$home/sitemap' aria-label='Site Map'>☰</a>" +builtin_request_js hbmenu.js html "<a href='$home$index_page'>Home</a>\n" if {[anycap jor]} { html "<a href='$home/timeline'>Timeline</a>\n" } if {[anoncap oh]} { @@ -32,8 +33,5 @@ } else { html "<a href='$home/login'>Login</a>\n" } </th1></div> <div id='hbdrop' class='hbdrop'></div> -<script nonce="$nonce"> -<th1>styleScript skins/default/js.txt</th1> -</script> ADDED src/hbmenu.js Index: src/hbmenu.js ================================================================== --- /dev/null +++ src/hbmenu.js @@ -0,0 +1,254 @@ +/* +** Originally: 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/ +** Modified by others. +** +******************************************************************************* +** +** This file contains the JS code used to implement the expanding hamburger +** menu on various skins. +** +** This was original the "js.txt" file for the default skin. It was subsequently +** moved into src/hbmenu.js so that it could be more easily reused by other skins +** using the "builtin_request_js" TH1 command. +** +** Operation: +** +** This script request that the HTML contain two elements: +** +** <a id="hbbtn"> <--- The hamburger menu button +** <div id="hbdrop"> <--- Container for the hamburger menu +** +** Bindings are made on hbbtn so that when it is clicked, the following +** happens: +** +** 1. An XHR is made to /sitemap?popup to fetch the HTML for the +** popup menu. +** +** 2. The HTML for the popup is inserted into hddrop. +** +** 3. The hddrop container is made visible. +** +** CSS rules are also needed to cause the hddrop to be initially invisible, +** and to correctly style and position the hddrop container. +*/ +(function() { + var hbButton = document.getElementById("hbbtn"); + if (!hbButton) return; // no hamburger button + if (!document.addEventListener) return; // Incompatible browser + 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", hbButton.href + "?popup"); + xhr.responseType = "document"; + xhr.send(); + } + else { + showPanel(); // just show what we built above + } + } + } +})(); Index: src/main.mk ================================================================== --- src/main.mk +++ src/main.mk @@ -190,11 +190,10 @@ $(SRCDIR)/../skins/bootstrap/header.txt \ $(SRCDIR)/../skins/default/css.txt \ $(SRCDIR)/../skins/default/details.txt \ $(SRCDIR)/../skins/default/footer.txt \ $(SRCDIR)/../skins/default/header.txt \ - $(SRCDIR)/../skins/default/js.txt \ $(SRCDIR)/../skins/eagle/css.txt \ $(SRCDIR)/../skins/eagle/details.txt \ $(SRCDIR)/../skins/eagle/footer.txt \ $(SRCDIR)/../skins/eagle/header.txt \ $(SRCDIR)/../skins/enhanced1/css.txt \ @@ -246,10 +245,11 @@ $(SRCDIR)/fossil.popupwidget.js \ $(SRCDIR)/fossil.storage.js \ $(SRCDIR)/fossil.tabs.js \ $(SRCDIR)/fossil.wikiedit-wysiwyg.js \ $(SRCDIR)/graph.js \ + $(SRCDIR)/hbmenu.js \ $(SRCDIR)/href.js \ $(SRCDIR)/login.js \ $(SRCDIR)/markdown.md \ $(SRCDIR)/menu.js \ $(SRCDIR)/sbsdiff.js \ Index: src/th_main.c ================================================================== --- src/th_main.c +++ src/th_main.c @@ -1492,10 +1492,15 @@ /* ** TH1 command: styleScript ?BUILTIN-FILENAME? ** ** Render the js.txt file from the current skin. Or, if an argument ** is supplied, render the built-in filename given. +** +** By "rendering" we mean that the script is loaded and run through +** TH1 to expand variables and process <th1>...</th1> script. Contrast +** with the "builtin_request_js BUILTIN-FILENAME" command which just +** loads the file as-is without interpretation. */ static int styleScriptCmd( Th_Interp *interp, void *p, int argc, @@ -1520,10 +1525,31 @@ Th_SetResult(interp, "repository unavailable", -1); return TH_ERROR; } } +/* +** TH1 command: builtin_request_js NAME +** +** Request that the built-in javascript file called NAME be added to the +** end of the generated page. +** +** See also: styleScript +*/ +static int builtinRequestJsCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=2 ){ + return Th_WrongNumArgs(interp, "builtin_request_js NAME"); + } + builtin_request_js(argv[1]); + return TH_OK; +} /* ** TH1 command: artifact ID ?FILENAME? ** ** Attempts to locate the specified artifact and return its contents. An @@ -2237,10 +2263,11 @@ void *pContext; } aCommand[] = { {"anoncap", hascapCmd, (void*)&anonFlag}, {"anycap", anycapCmd, 0}, {"artifact", artifactCmd, 0}, + {"builtin_request_js", builtinRequestJsCmd, 0}, {"captureTh1", captureTh1Cmd, 0}, {"cgiHeaderLine", cgiHeaderLineCmd, 0}, {"checkout", checkoutCmd, 0}, {"combobox", comboboxCmd, 0}, {"copybtn", copybtnCmd, 0}, Index: win/Makefile.mingw ================================================================== --- win/Makefile.mingw +++ win/Makefile.mingw @@ -602,11 +602,10 @@ $(SRCDIR)/../skins/bootstrap/header.txt \ $(SRCDIR)/../skins/default/css.txt \ $(SRCDIR)/../skins/default/details.txt \ $(SRCDIR)/../skins/default/footer.txt \ $(SRCDIR)/../skins/default/header.txt \ - $(SRCDIR)/../skins/default/js.txt \ $(SRCDIR)/../skins/eagle/css.txt \ $(SRCDIR)/../skins/eagle/details.txt \ $(SRCDIR)/../skins/eagle/footer.txt \ $(SRCDIR)/../skins/eagle/header.txt \ $(SRCDIR)/../skins/enhanced1/css.txt \ @@ -658,10 +657,11 @@ $(SRCDIR)/fossil.popupwidget.js \ $(SRCDIR)/fossil.storage.js \ $(SRCDIR)/fossil.tabs.js \ $(SRCDIR)/fossil.wikiedit-wysiwyg.js \ $(SRCDIR)/graph.js \ + $(SRCDIR)/hbmenu.js \ $(SRCDIR)/href.js \ $(SRCDIR)/login.js \ $(SRCDIR)/markdown.md \ $(SRCDIR)/menu.js \ $(SRCDIR)/sbsdiff.js \ Index: win/Makefile.msc ================================================================== --- win/Makefile.msc +++ win/Makefile.msc @@ -523,11 +523,10 @@ "$(SRCDIR)\..\skins\bootstrap\header.txt" \ "$(SRCDIR)\..\skins\default\css.txt" \ "$(SRCDIR)\..\skins\default\details.txt" \ "$(SRCDIR)\..\skins\default\footer.txt" \ "$(SRCDIR)\..\skins\default\header.txt" \ - "$(SRCDIR)\..\skins\default\js.txt" \ "$(SRCDIR)\..\skins\eagle\css.txt" \ "$(SRCDIR)\..\skins\eagle\details.txt" \ "$(SRCDIR)\..\skins\eagle\footer.txt" \ "$(SRCDIR)\..\skins\eagle\header.txt" \ "$(SRCDIR)\..\skins\enhanced1\css.txt" \ @@ -579,10 +578,11 @@ "$(SRCDIR)\fossil.popupwidget.js" \ "$(SRCDIR)\fossil.storage.js" \ "$(SRCDIR)\fossil.tabs.js" \ "$(SRCDIR)\fossil.wikiedit-wysiwyg.js" \ "$(SRCDIR)\graph.js" \ + "$(SRCDIR)\hbmenu.js" \ "$(SRCDIR)\href.js" \ "$(SRCDIR)\login.js" \ "$(SRCDIR)\markdown.md" \ "$(SRCDIR)\menu.js" \ "$(SRCDIR)\sbsdiff.js" \ @@ -1136,11 +1136,10 @@ echo "$(SRCDIR)\../skins/bootstrap/header.txt" >> $@ echo "$(SRCDIR)\../skins/default/css.txt" >> $@ echo "$(SRCDIR)\../skins/default/details.txt" >> $@ echo "$(SRCDIR)\../skins/default/footer.txt" >> $@ echo "$(SRCDIR)\../skins/default/header.txt" >> $@ - echo "$(SRCDIR)\../skins/default/js.txt" >> $@ echo "$(SRCDIR)\../skins/eagle/css.txt" >> $@ echo "$(SRCDIR)\../skins/eagle/details.txt" >> $@ echo "$(SRCDIR)\../skins/eagle/footer.txt" >> $@ echo "$(SRCDIR)\../skins/eagle/header.txt" >> $@ echo "$(SRCDIR)\../skins/enhanced1/css.txt" >> $@ @@ -1192,10 +1191,11 @@ echo "$(SRCDIR)\fossil.popupwidget.js" >> $@ echo "$(SRCDIR)\fossil.storage.js" >> $@ echo "$(SRCDIR)\fossil.tabs.js" >> $@ echo "$(SRCDIR)\fossil.wikiedit-wysiwyg.js" >> $@ echo "$(SRCDIR)\graph.js" >> $@ + echo "$(SRCDIR)\hbmenu.js" >> $@ echo "$(SRCDIR)\href.js" >> $@ echo "$(SRCDIR)\login.js" >> $@ echo "$(SRCDIR)\markdown.md" >> $@ echo "$(SRCDIR)\menu.js" >> $@ echo "$(SRCDIR)\sbsdiff.js" >> $@