Added JavaScript to support header behavior
This commit is contained in:
parent
f65982a350
commit
48da77317f
@ -25,7 +25,7 @@
|
||||
|
||||
/* Element colors & appearance. */
|
||||
--deus-body-background-color: var(--deus-color-darker-blue);
|
||||
--deus-body-background-image: url('/deus/assets/images/deus-text-only.png');
|
||||
--deus-body-background-image: url('/deus/assets/images/deus-logo-only.png');
|
||||
--deus-body-background-image-margin: 1.5rem;
|
||||
|
||||
--deus-box-background-color: var(--deus-color-dark-grey);
|
||||
@ -95,17 +95,17 @@ body {
|
||||
}
|
||||
body::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-image: var(--deus-body-background-image);
|
||||
background-size: calc(100% - 2 * var(--deus-body-background-image-margin));
|
||||
background-size: auto 150vh;
|
||||
background-repeat: no-repeat;
|
||||
background-position: var(--deus-body-background-image-margin) var(--deus-body-background-image-margin);
|
||||
background-position: right 50vw top 0;
|
||||
background-attachment: fixed;
|
||||
opacity: 0.1;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
main {
|
||||
@ -143,6 +143,18 @@ main nav {
|
||||
padding: 0.75em 1.25em;
|
||||
border-bottom: var(--deus-box-border-width) solid var(--deus-box-border-color);
|
||||
z-index: 1001;
|
||||
transition:
|
||||
top 0.3s ease,
|
||||
opacity 0.15s ease,
|
||||
filter 0.15s ease;
|
||||
}
|
||||
main nav.hidden {
|
||||
top: -5rem;
|
||||
}
|
||||
main nav.translucent-on-scroll {
|
||||
opacity: 0.6;
|
||||
filter: blur(2px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
main nav a {
|
||||
|
||||
@ -0,0 +1,467 @@
|
||||
/***************************/
|
||||
/* 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();
|
||||
}
|
||||
|
||||
// On mobile, make site nav UI translucent on ANY scroll down.
|
||||
// if (GW.mediaQueries.mobileNarrow.matches)
|
||||
GW.scrollState.siteNavUI.forEach(element => {
|
||||
if (GW.scrollState.unbrokenDownScrollDistance > 0) element.addClass("translucent-on-scroll");
|
||||
else element.removeClass("hidden");
|
||||
});
|
||||
|
||||
// Show site nav UI when scrolling a full page up, or to the top.
|
||||
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");
|
||||
});
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<script>window.GW = { };</script>
|
||||
<link rel='stylesheet' type='text/css' href='https://fonts.obormot.net?fonts=*'>
|
||||
<link rel='stylesheet' type='text/css' href='/deus/assets/css/main.css'>
|
||||
<link rel='stylesheet' type='text/css' href='/deus/assets/css/image-focus.css'>
|
||||
<script src="/deus/assets/js/script.js" defer></script>
|
||||
<script src="/deus/assets/js/script.js" async defer></script>
|
||||
@ -2,7 +2,7 @@
|
||||
<html lang='en-US'>
|
||||
<head>
|
||||
<title>DEUS gaming</title>
|
||||
<!--#include virtual="/deus/includes/head-css-links.html" -->
|
||||
<!--#include virtual="/deus/includes/head-content.html" -->
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
@ -39,6 +39,6 @@
|
||||
</article>
|
||||
<!--#include virtual="/deus/includes/colophon.html" -->
|
||||
</main>
|
||||
<!--#include virtual="/deus/includes/foot-scripts.html" -->
|
||||
<!--#include virtual="/deus/includes/foot-content.html" -->
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user