| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476 |
- /***************************/
- /* INITIALIZATION REGISTRY */
- /***************************/
-
- /* Polyfill for requestIdleCallback in Apple and Microsoft browsers. */
- if (!window.requestIdleCallback) {
- window.requestIdleCallback = (fn) => { setTimeout(fn, 0); };
- }
-
- /* TBC. */
- GW.initializersDone = { };
- GW.initializers = { };
- function registerInitializer(name, tryEarly, precondition, fn) {
- GW.initializersDone[name] = false;
- GW.initializers[name] = fn;
- let wrapper = function () {
- if (GW.initializersDone[name]) return;
- if (!precondition()) {
- if (tryEarly) {
- setTimeout(() => requestIdleCallback(wrapper, { timeout: 1000 }), 50);
- } else {
- document.addEventListener("readystatechange", wrapper, { once: true });
- }
- return;
- }
- GW.initializersDone[name] = true;
- fn();
- };
- if (tryEarly) {
- requestIdleCallback(wrapper, { timeout: 1000 });
- } else {
- document.addEventListener("readystatechange", wrapper, { once: true });
- requestIdleCallback(wrapper);
- }
- }
- function forceInitializer(name) {
- if (GW.initializersDone[name]) return;
- GW.initializersDone[name] = true;
- GW.initializers[name]();
- }
-
- /***********/
- /* COOKIES */
- /***********/
-
- /* Sets a cookie. */
- function setCookie(name, value, days) {
- var expires = "";
- if (!days) days = 36500;
- if (days) {
- var date = new Date();
- date.setTime(date.getTime() + (days*24*60*60*1000));
- expires = "; expires=" + date.toUTCString();
- }
- document.cookie = name + "=" + (value || "") + expires + "; path=/";
- }
-
- /* Reads the value of named cookie.
- Returns the cookie as a string, or null if no such cookie exists. */
- function readCookie(name) {
- var nameEQ = name + "=";
- var ca = document.cookie.split(';');
- for(var i = 0; i < ca.length; i++) {
- var c = ca[i];
- while (c.charAt(0)==' ') c = c.substring(1, c.length);
- if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
- }
- return null;
- }
-
- /****************************************************/
- /* CSS CLASS MANIPULATION (polyfill for .classList) */
- /****************************************************/
-
- Element.prototype.addClass = function(className) {
- if (!this.hasClass(className))
- this.className = (this.className + " " + className).trim();
- }
- Element.prototype.addClasses = function(classNames) {
- let elementClassNames = this.className.trim().split(/\s/);
-
- classNames.forEach(className => {
- if (!this.hasClass(className))
- elementClassNames.push(className);
- });
-
- this.className = elementClassNames.join(" ");
- }
- Element.prototype.removeClass = function(className) {
- this.className = this.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), "$1").trim();
- if (this.className == "") this.removeAttribute("class");
- }
- Element.prototype.removeClasses = function(classNames) {
- classNames.forEach(className => {
- this.className = this.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), "$1").trim();
- });
- if (this.className == "") this.removeAttribute("class");
- }
- Element.prototype.hasClass = function(className) {
- return (new RegExp("(^|\\s+)" + className + "(\\s+|$)")).test(this.className);
- }
- Element.prototype.toggleClass = function(className, on) {
- if ((typeof on == "undefined" && !this.hasClass(className)) ||
- on == true) {
- this.addClass(className);
- } else {
- this.removeClass(className);
- }
- }
-
- /********************/
- /* QUERYING THE DOM */
- /********************/
-
- function queryAll(selector, context) {
- context = context || document;
- // Redirect simple selectors to the more performant function
- if (/^(#?[\w-]+|\.[\w-.]+)$/.test(selector)) {
- switch (selector.charAt(0)) {
- case '#':
- // Handle ID-based selectors
- let element = document.getElementById(selector.substr(1));
- return element ? [ element ] : [ ];
- case '.':
- // Handle class-based selectors
- // Query by multiple classes by converting the selector
- // string into single spaced class names
- var classes = selector.substr(1).replace(/\./g, ' ');
- return [].slice.call(context.getElementsByClassName(classes));
- default:
- // Handle tag-based selectors
- return [].slice.call(context.getElementsByTagName(selector));
- }
- }
- // Default to `querySelectorAll`
- return [].slice.call(context.querySelectorAll(selector));
- }
- function query(selector, context) {
- let all = queryAll(selector, context);
- return (all.length > 0) ? all[0] : null;
- }
- Object.prototype.queryAll = function (selector) {
- return queryAll(selector, this);
- }
- Object.prototype.query = function (selector) {
- return query(selector, this);
- }
-
- /*******************************/
- /* EVENT LISTENER MANIPULATION */
- /*******************************/
-
- /* Adds an event listener to a button (or other clickable element), attaching
- it to both ‘click’ and ‘keyup’ events (for use with keyboard navigation).
- Optionally also attaches the listener to the ‘mousedown’ event, making the
- element activate on mouse down instead of mouse up. */
- Element.prototype.addActivateEvent = function(func, includeMouseDown) {
- let ael = this.activateEventListener = (event) => { if (event.button === 0 || event.key === ' ') func(event) };
- if (includeMouseDown) this.addEventListener("mousedown", ael);
- this.addEventListener("click", ael);
- this.addEventListener("keyup", ael);
- }
-
- /* Removes event listener from a clickable element, automatically detaching it
- from all relevant event types. */
- Element.prototype.removeActivateEvent = function() {
- let ael = this.activateEventListener;
- this.removeEventListener("mousedown", ael);
- this.removeEventListener("click", ael);
- this.removeEventListener("keyup", ael);
- }
-
- /* Adds a scroll event listener to the page. */
- function addScrollListener(fn, name) {
- let wrapper = (event) => {
- requestAnimationFrame(() => {
- fn(event);
- document.addEventListener("scroll", wrapper, { once: true, passive: true });
- });
- }
- document.addEventListener("scroll", wrapper, { once: true, passive: true });
-
- // Retain a reference to the scroll listener, if a name is provided.
- if (typeof name != "undefined")
- GW[name] = wrapper;
- }
-
- /************************/
- /* ACTIVE MEDIA QUERIES */
- /************************/
-
- /* This function provides two slightly different versions of its functionality,
- depending on how many arguments it gets.
-
- If one function is given (in addition to the media query and its name), it
- is called whenever the media query changes (in either direction).
-
- If two functions are given (in addition to the media query and its name),
- then the first function is called whenever the media query starts matching,
- and the second function is called whenever the media query stops matching.
-
- If you want to call a function for a change in one direction only, pass an
- empty closure (NOT null!) as one of the function arguments.
-
- There is also an optional fifth argument. This should be a function to be
- called when the active media query is canceled.
- */
- function doWhenMatchMedia(mediaQuery, name, ifMatchesOrAlwaysDo, otherwiseDo = null, whenCanceledDo = null) {
- if (typeof GW.mediaQueryResponders == "undefined")
- GW.mediaQueryResponders = { };
-
- let mediaQueryResponder = (event, canceling = false) => {
- if (canceling) {
- GWLog(`Canceling media query “${name}”`);
-
- if (whenCanceledDo != null)
- whenCanceledDo(mediaQuery);
- } else {
- let matches = (typeof event == "undefined") ? mediaQuery.matches : event.matches;
-
- GWLog(`Media query “${name}” triggered (matches: ${matches ? "YES" : "NO"})`);
-
- if (otherwiseDo == null || matches) ifMatchesOrAlwaysDo(mediaQuery);
- else otherwiseDo(mediaQuery);
- }
- };
- mediaQueryResponder();
- mediaQuery.addListener(mediaQueryResponder);
-
- GW.mediaQueryResponders[name] = mediaQueryResponder;
- }
-
- /* Deactivates and discards an active media query, after calling the function
- that was passed as the whenCanceledDo parameter when the media query was
- added.
- */
- function cancelDoWhenMatchMedia(name) {
- GW.mediaQueryResponders[name](null, true);
-
- for ([ key, mediaQuery ] of Object.entries(GW.mediaQueries))
- mediaQuery.removeListener(GW.mediaQueryResponders[name]);
-
- GW.mediaQueryResponders[name] = null;
- }
-
- /****************/
- /* MISC HELPERS */
- /****************/
-
- /* Returns the passed object if it’s truthy, or a newly created HTMLElement.
- Æ(x) is the element analogue of (x||{}).
- */
- function Æ(x) {
- return x || document.createElement(null);
- }
-
- /* If top of element is not at or above the top of the screen, scroll it into
- view. */
- Element.prototype.scrollIntoViewIfNeeded = function() {
- GWLog("scrollIntoViewIfNeeded");
- let rect = this.getBoundingClientRect();
- if ((rect.bottom > window.innerHeight && rect.top > 0) ||
- rect.top < 0) {
- this.scrollIntoView(true);
- }
- }
-
- /* Return the currently selected text, as HTML (rather than unstyled text).
- */
- function getSelectionHTML() {
- var container = document.createElement("div");
- container.appendChild(window.getSelection().getRangeAt(0).cloneContents());
- return container.innerHTML;
- }
-
- /* Return the value of a GET (i.e., URL) parameter.
- */
- function getQueryVariable(variable) {
- var query = window.location.search.substring(1);
- var vars = query.split("&");
- for (var i = 0; i < vars.length; i++) {
- var pair = vars[i].split("=");
- if (pair[0] == variable)
- return pair[1];
- }
-
- return false;
- }
-
- /* Given an element or a selector, removes that element (or the element
- identified by the selector).
-
- If multiple elements match the selector, only the first is removed.
- */
- function removeElement(elementOrSelector, ancestor = document) {
- if (typeof elementOrSelector == "string") elementOrSelector = ancestor.query(elementOrSelector);
- if (elementOrSelector) elementOrSelector.parentElement.removeChild(elementOrSelector);
- }
-
- /* Returns true if the string begins with the given prefix.
- */
- String.prototype.hasPrefix = function (prefix) {
- return (this.lastIndexOf(prefix, 0) === 0);
- }
-
- /* Toggles whether the page is scrollable.
- */
- function togglePageScrolling(enable) {
- if (!enable) {
- window.addEventListener('keydown', GW.scrollingDisabledKeyDown = (event) => {
- let forbiddenKeys = [ " ", "Spacebar", "ArrowUp", "ArrowDown", "Up", "Down" ];
- if (forbiddenKeys.contains(event.key) &&
- event.target == document.body) {
- event.preventDefault();
- }
- });
- } else {
- window.removeEventListener('keydown', GW.scrollingDisabledKeyDown);
- }
- }
-
- /* Copies a string to the clipboard.
- */
- function copyTextToClipboard(string) {
- let scratchpad = query("#scratchpad");
- scratchpad.value = string;
- scratchpad.select();
- document.execCommand("copy");
- }
-
- /* Returns the next element sibling of the element, wrapping around to the
- first child of the parent if the element is the last child.
- Returns the element itself, if it has no siblings or no parent.
- */
- Element.prototype.nextElementSiblingCyclical = function() {
- if (this.parentElement == null) return this;
-
- return this.parentElement.children[(Array.prototype.indexOf.call(this.parentElement.children, this) + 1) % this.parentElement.children.length];
- }
-
- /* Returns the previous element sibling of the element, wrapping around to the
- last child of the parent if the element is the first child.
- Returns the element itself, if it has no siblings or no parent.
- */
- Element.prototype.previousElementSiblingCyclical = function() {
- if (this.parentElement == null) return this;
-
- return this.parentElement.children[(Array.prototype.indexOf.call(this.parentElement.children, this) - 1) % this.parentElement.children.length];
- }
-
- /********************/
- /* DEBUGGING OUTPUT */
- /********************/
-
- function GWLog (string) {
- if (GW.loggingEnabled == true || (GW.loggingEnabled == null && localStorage.getItem("logging-enabled") == "true"))
- console.log(string);
- }
- GW.enableLogging = (permanently = false) => {
- if (permanently)
- localStorage.setItem("logging-enabled", "true");
- else
- GW.loggingEnabled = true;
- };
- GW.disableLogging = (permanently = false) => {
- if (permanently)
- localStorage.removeItem("logging-enabled");
- else
- GW.loggingEnabled = false;
- };
-
- /**********/
- /* NAV UI */
- /**********/
-
- /* Hide the site nav UI on scroll down; show it on scroll up.
-
- Called by the ‘updateSiteNavUIStateScrollListener’ scroll listener.
- */
- function updateSiteNavUIState(event) {
- GWLog("updateSiteNavUIState");
-
- let newScrollTop = window.pageYOffset || document.documentElement.scrollTop;
- GW.scrollState.unbrokenDownScrollDistance = (newScrollTop > GW.scrollState.lastScrollTop) ?
- (GW.scrollState.unbrokenDownScrollDistance + newScrollTop - GW.scrollState.lastScrollTop) :
- 0;
- GW.scrollState.unbrokenUpScrollDistance = (newScrollTop < GW.scrollState.lastScrollTop) ?
- (GW.scrollState.unbrokenUpScrollDistance + GW.scrollState.lastScrollTop - newScrollTop) :
- 0;
- GW.scrollState.lastScrollTop = newScrollTop;
-
- // Hide site nav UI when scrolling a full page down.
- if (GW.scrollState.unbrokenDownScrollDistance > window.innerHeight) {
- if (!GW.scrollState.siteNavUI[0].hasClass("hidden")) toggleSiteNavUI();
- }
-
- // Make site nav UI translucent when scrolling down.
- GW.scrollState.siteNavUI.forEach(element => {
- if (GW.scrollState.unbrokenDownScrollDistance > 0) element.addClass("translucent-on-scroll");
- else element.removeClass("hidden");
- });
-
- // On desktop, show site nav UI when scrolling a full page up, or to the
- // the top of the page.
- // On mobile, show site nav UI translucent on ANY scroll up.
- if (GW.mediaQueries.mobileNarrow.matches) {
- if (GW.scrollState.unbrokenUpScrollDistance > 0)
- showSiteNavUI();
- } else if ( GW.scrollState.unbrokenUpScrollDistance > window.innerHeight
- || GW.scrollState.lastScrollTop == 0) {
- showSiteNavUI();
- }
- }
-
- function toggleSiteNavUI() {
- GWLog("toggleSiteNavUI");
-
- GW.scrollState.siteNavUI.forEach(element => {
- element.toggleClass("hidden");
- element.removeClass("translucent-on-scroll");
- });
- }
-
- function showSiteNavUI() {
- GWLog("showSiteNavUI");
-
- GW.scrollState.siteNavUI.forEach(element => {
- element.removeClass("hidden");
- element.removeClass("translucent-on-scroll");
- });
- }
-
- /******************/
- /* INITIALIZATION */
- /******************/
-
- registerInitializer('earlyInitialize', true, () => (query("#content") != null), () => {
- GWLog("INITIALIZER earlyInitialize");
- // Check to see whether we’re on a mobile device (which we define as a touchscreen^W narrow viewport).
- // GW.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
- // GW.isMobile = ('ontouchstart' in document.documentElement);
- GW.mediaQueries = {
- mobileNarrow: matchMedia("(max-width: 520px)"),
- mobileWide: matchMedia("(max-width: 900px)"),
- mobileMax: matchMedia("(max-width: 960px)"),
- hover: matchMedia("only screen and (hover: hover) and (pointer: fine)")
- };
- GW.isMobile = GW.mediaQueries.mobileMax.matches;
- GW.isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
-
- GW.siteNavUISelector = "main nav";
- });
-
- /*************************/
- /* POST-LOAD ADJUSTMENTS */
- /*************************/
-
- registerInitializer('pageLayoutFinished', false, () => (document.readyState == "complete"), () => {
- GWLog("INITIALIZER pageLayoutFinished");
-
- forceInitializer('earlyInitialize');
-
- // We pre-query the relevant elements, so we don’t have to run queryAll on
- // every firing of the scroll listener.
- GW.scrollState = {
- "lastScrollTop": window.pageYOffset || document.documentElement.scrollTop,
- "unbrokenDownScrollDistance": 0,
- "unbrokenUpScrollDistance": 0,
- "siteNavUI": queryAll(GW.siteNavUISelector),
- };
- addScrollListener(updateSiteNavUIState, "updateSiteNavUIStateScrollListener");
- GW.scrollState.siteNavUI.forEach(element => {
- element.addEventListener("mouseover", () => { showSiteNavUI(); });
- });
- });
|