chore: migrate many of our interactive scripts to defer

This commit is contained in:
Corbin Crutchley
2022-09-25 05:50:52 -07:00
parent 51a4ad0fda
commit b5058b458a
9 changed files with 227 additions and 199 deletions

26
public/scripts/backbtn.js Normal file
View File

@@ -0,0 +1,26 @@
/* AFTER CHANGING THIS FILE, PLEASE MANUALLY MINIFY IT AND PUT INTO backbtn.min.js */
/* TODO: Add minifier to build script */
window.onload = () => {
const backBtn = document.querySelector('#backbtn');
let hasHistory = false;
window.addEventListener('beforeunload', () => {
hasHistory = true;
})
backBtn.addEventListener('click', () => {
if (!document.referrer) {
// This is the first page the user has visited on the site in this session
window.location.href = '/';
return;
}
history.back();
// User cannot go back, meaning that we're at the first page of the site session
setTimeout(() => {
if (!hasHistory){
window.location.href = "/";
}
}, 200);
})
}

1
public/scripts/backbtn.min.js vendored Normal file
View File

@@ -0,0 +1 @@
window.onload=()=>{let e=document.querySelector("#backbtn"),r=!1;window.addEventListener("beforeunload",()=>{r=!0}),e.addEventListener("click",()=>{if(!document.referrer){window.location.href="/";return}history.back(),setTimeout(()=>{r||(window.location.href="/")},200)})};

167
public/scripts/tabs.js Normal file
View File

@@ -0,0 +1,167 @@
/* AFTER CHANGING THIS FILE, PLEASE MANUALLY MINIFY IT AND PUT INTO tabs.min.js */
const LOCAL_STORAGE_KEY = "tabs-selection";
window.addEventListener('DOMContentLoaded', () => {
const tabLists = document.querySelectorAll('[role="tablist"]');
tabLists.forEach(tabList => {
/**
* @type {NodeListOf<HTMLElement>}
*/
const tabs = tabList.querySelectorAll('[role="tab"]');
// Add a click event handler to each tab
tabs.forEach((tab) => {
tab.addEventListener('click', e => {
/**
* @type {HTMLElement}
*/
const target = e.target;
// Scroll onto screen in order to avoid jumping page locations
setTimeout(() => {
target.scrollIntoView({
behavior: "auto",
block: "center",
inline: "center",
});
}, 0);
changeTabs({ target })
});
});
// Enable arrow navigation between tabs in the tab list
let tabFocus = 0;
tabList.addEventListener('keydown', (_e) => {
/**
* @type {KeyboardEvent}
*/
const e = _e;
// Move right
if (e.keyCode === 39 || e.keyCode === 37) {
tabs[tabFocus].setAttribute('tabindex', `-1`);
if (e.keyCode === 39) {
tabFocus++;
// If we're at the end, go to the start
if (tabFocus >= tabs.length) {
tabFocus = 0;
}
// Move left
} else if (e.keyCode === 37) {
tabFocus--;
// If we're at the start, move to the end
if (tabFocus < 0) {
tabFocus = tabs.length - 1;
}
}
tabs[tabFocus].setAttribute('tabindex', `0`);
tabs[tabFocus].focus();
tabs[tabFocus].click();
}
});
});
const currentTab = localStorage.getItem(LOCAL_STORAGE_KEY);
if (currentTab) {
/**
* @type {HTMLElement}
*/
const el = document.querySelector(`[data-tabname="${currentTab}"]`);
if (el) changeTabs({ target: el });
}
function changeTabs(_e) {
/**
* @type {{ target: HTMLElement }}
*/
const e = _e;
const target = e.target;
const parent = target.parentNode;
const grandparent = parent.parentNode;
// Remove all current selected tabs
parent
.querySelectorAll('[aria-selected="true"]')
.forEach((t) => t.setAttribute('aria-selected', `false`));
// Set this tab as selected
target.setAttribute('aria-selected', `true`);
const tabName = target.dataset.tabname;
/**
* @type {NodeListOf<HTMLElement>}
*/
const relatedTabs = document.querySelectorAll(`[role="tab"][data-tabname="${target.dataset.tabname}"]`);
localStorage.setItem(LOCAL_STORAGE_KEY, tabName);
for (let relatedTab of relatedTabs) {
if (relatedTab === target) continue;
changeTabs({ target: relatedTab });
}
// Hide all tab panels
grandparent
.querySelectorAll('[role="tabpanel"]')
.forEach((p) => p.setAttribute('hidden', `true`));
// Show the selected panel
grandparent.parentNode
.querySelector(`#${target.getAttribute('aria-controls')}`)
.removeAttribute('hidden');
}
/* -------------------- */
/**
*
* @param {HTMLElement} el
* @param {(el: HTMLElement) => boolean} check
* @returns {boolean}
*/
function checkElementsParents(el, check) {
if (el.parentElement) {
if (!check(el.parentElement)) {
return checkElementsParents(el.parentElement, check);
} else {
return true;
}
} else {
return false;
}
}
(() => {
// If user has linked to a heading that's inside of a tab
const hash = window.location.hash;
if (!hash) return;
const heading = document.querySelector < HTMLElement > (hash);
if (!heading) return;
const isHidden = checkElementsParents(heading, el =>
el.hasAttribute('hidden') && el.getAttribute('hidden') !== "false"
)
// If it's not hidden, then we can assume that the browser will auto-scroll to it
if (!isHidden) return;
const partialHash = hash.slice(1);
try {
const matchingTab = document.querySelector < HTMLElement > (
`[data-headers*="${partialHash}"`
);
if (!matchingTab) return;
// If header is not in a tab
const tabName = matchingTab.getAttribute("data-tabname");
if (!tabName) return;
matchingTab.click();
setTimeout(() => {
const el = document.querySelector(hash);
if (!el) return;
el.scrollIntoView(true);
}, 0);
} catch (e) {
console.error("Error finding matching tab", e);
}
})()
});

1
public/scripts/tabs.min.js vendored Normal file
View File

@@ -0,0 +1 @@
const LOCAL_STORAGE_KEY="tabs-selection";window.addEventListener("DOMContentLoaded",()=>{let e=document.querySelectorAll('[role="tablist"]');e.forEach(e=>{let t=e.querySelectorAll('[role="tab"]');t.forEach(e=>{e.addEventListener("click",e=>{let t=e.target;setTimeout(()=>{t.scrollIntoView({behavior:"auto",block:"center",inline:"center"})},0),a({target:t})})});let r=0;e.addEventListener("keydown",e=>{let a=e;(39===a.keyCode||37===a.keyCode)&&(t[r].setAttribute("tabindex","-1"),39===a.keyCode?++r>=t.length&&(r=0):37===a.keyCode&&--r<0&&(r=t.length-1),t[r].setAttribute("tabindex","0"),t[r].focus(),t[r].click())})});let t=localStorage.getItem(LOCAL_STORAGE_KEY);if(t){let r=document.querySelector(`[data-tabname="${t}"]`);r&&a({target:r})}function a(e){let t=e.target,r=t.parentNode,l=r.parentNode;r.querySelectorAll('[aria-selected="true"]').forEach(e=>e.setAttribute("aria-selected","false")),t.setAttribute("aria-selected","true");let n=t.dataset.tabname,o=document.querySelectorAll(`[role="tab"][data-tabname="${t.dataset.tabname}"]`);for(let i of(localStorage.setItem(LOCAL_STORAGE_KEY,n),o))i!==t&&a({target:i});l.querySelectorAll('[role="tabpanel"]').forEach(e=>e.setAttribute("hidden","true")),l.parentNode.querySelector(`#${t.getAttribute("aria-controls")}`).removeAttribute("hidden")}function l(e,t){return!!e.parentElement&&(!!t(e.parentElement)||l(e.parentElement,t))}(()=>{let e=window.location.hash;if(!e)return;let t=document.querySelector<HTMLElement>e;if(!t)return;let r=l(t,e=>e.hasAttribute("hidden")&&"false"!==e.getAttribute("hidden"));if(!r)return;let a=e.slice(1);try{let n=document.querySelector<HTMLElement>`[data-headers*="${a}"`;if(!n)return;let o=n.getAttribute("data-tabname");if(!o)return;n.click(),setTimeout(()=>{let t=document.querySelector(e);t&&t.scrollIntoView(!0)},0)}catch(i){console.error("Error finding matching tab",i)}})()});

View File

@@ -0,0 +1,28 @@
/* AFTER CHANGING THIS FILE, PLEASE MANUALLY MINIFY IT AND PUT INTO tabs.min.js */
const COLOR_MODE_STORAGE_KEY = "currentTheme";
const themeToggleBtn = document.querySelector('#theme-toggle-button');
const darkIconEl = document.querySelector('#dark-icon');
const lightIconEl = document.querySelector('#light-icon');
function toggleButton(theme) {
themeToggleBtn.ariaPressed = `${theme === 'dark'}`;
if (theme === 'light') {
lightIconEl.style.display = null;
darkIconEl.style.display = 'none';
} else {
lightIconEl.style.display = 'none';
darkIconEl.style.display = null;
}
}
// TODO: Migrate to `classList`
const initialTheme = document.documentElement.className;
toggleButton(initialTheme);
themeToggleBtn.addEventListener('click', () => {
const currentTheme = document.documentElement.className;
document.documentElement.className = currentTheme === 'light' ? 'dark' : 'light';
// TODO: Persist new setting
const newTheme = document.documentElement.className;
toggleButton(newTheme);
localStorage.setItem(COLOR_MODE_STORAGE_KEY, newTheme)
})

1
public/scripts/themetoggle.min.js vendored Normal file
View File

@@ -0,0 +1 @@
const COLOR_MODE_STORAGE_KEY="currentTheme",themeToggleBtn=document.querySelector("#theme-toggle-button"),darkIconEl=document.querySelector("#dark-icon"),lightIconEl=document.querySelector("#light-icon");function toggleButton(e){themeToggleBtn.ariaPressed=`${"dark"===e}`,"light"===e?(lightIconEl.style.display=null,darkIconEl.style.display="none"):(lightIconEl.style.display="none",darkIconEl.style.display=null)}const initialTheme=document.documentElement.className;toggleButton(initialTheme),themeToggleBtn.addEventListener("click",()=>{let e=document.documentElement.className;document.documentElement.className="light"===e?"dark":"light";let t=document.documentElement.className;toggleButton(t),localStorage.setItem("currentTheme",t)});

View File

@@ -1,11 +1,6 @@
---
import { Icon } from 'astro-icon';
import btnStyles from "./dark-light-button.module.scss";
import {
COLOR_MODE_STORAGE_KEY,
} from "constants/index";
---
<button
@@ -18,32 +13,4 @@ import {
<Icon name="light" height="36" width="36" id="light-icon" style="display: none;"/>
</button>
<!-- DO NOT SWITCH THE ORDER OF THESE TWO TAGS, IT WILL BREAK FUNCTIONALITY -->
<script is:inline define:vars={{COLOR_MODE_STORAGE_KEY}}>
const themeToggleBtn = document.querySelector('#theme-toggle-button');
const darkIconEl = document.querySelector('#dark-icon');
const lightIconEl = document.querySelector('#light-icon');
function toggleButton(theme) {
themeToggleBtn.ariaPressed = `${theme === 'dark'}`;
if (theme === 'light') {
lightIconEl.style.display = null;
darkIconEl.style.display = 'none';
} else {
lightIconEl.style.display = 'none';
darkIconEl.style.display = null;
}
}
// TODO: Migrate to `classList`
const initialTheme = document.documentElement.className;
toggleButton(initialTheme);
themeToggleBtn.addEventListener('click', () => {
const currentTheme = document.documentElement.className;
document.documentElement.className = currentTheme === 'light' ? 'dark' : 'light';
// TODO: Persist new setting
const newTheme = document.documentElement.className;
toggleButton(newTheme);
localStorage.setItem(COLOR_MODE_STORAGE_KEY, newTheme)
})
</script>
<script defer is:inline src="/scripts/themetoggle.min.js"/>

View File

@@ -19,30 +19,7 @@ const isCollection = Astro.url.pathname.startsWith(`${rootPath}collections`);
<button id="backbtn" class={`${layoutStyles.backBtn} baseBtn`} aria-label="Go back">
<Icon height="36" width="36" name="back" />
</button>
<script is:inline>
const backBtn = document.querySelector('#backbtn');
let hasHistory = false;
window.addEventListener('beforeunload', () => {
hasHistory = true;
})
backBtn.addEventListener('click', () => {
if (!document.referrer) {
// This is the first page the user has visited on the site in this session
window.location.href = '/';
return;
}
history.back();
// User cannot go back, meaning that we're at the first page of the site session
setTimeout(() => {
if (!hasHistory){
window.location.href = "/";
}
}, 200);
})
</script>
<script is:inline defer src="/scripts/backbtn.min.js"/>
</>
) : (
<div />

View File

@@ -1,144 +1,4 @@
---
---
<script>
const LOCAL_STORAGE_KEY = "tabs-selection";
window.addEventListener('DOMContentLoaded', () => {
const tabLists = document.querySelectorAll('[role="tablist"]');
tabLists.forEach(tabList => {
const tabs = tabList.querySelectorAll('[role="tab"]') as NodeListOf<HTMLElement>;
// Add a click event handler to each tab
tabs.forEach((tab) => {
tab.addEventListener('click', e => {
const target = e.target as HTMLElement;
// Scroll onto screen in order to avoid jumping page locations
setTimeout(() => {
target.scrollIntoView({
behavior: "auto",
block: "center",
inline: "center",
});
}, 0);
changeTabs({ target })
});
});
// Enable arrow navigation between tabs in the tab list
let tabFocus = 0;
tabList.addEventListener('keydown', (e: KeyboardEvent) => {
// Move right
if (e.keyCode === 39 || e.keyCode === 37) {
tabs[tabFocus].setAttribute('tabindex', `-1`);
if (e.keyCode === 39) {
tabFocus++;
// If we're at the end, go to the start
if (tabFocus >= tabs.length) {
tabFocus = 0;
}
// Move left
} else if (e.keyCode === 37) {
tabFocus--;
// If we're at the start, move to the end
if (tabFocus < 0) {
tabFocus = tabs.length - 1;
}
}
tabs[tabFocus].setAttribute('tabindex', `0`);
tabs[tabFocus].focus();
tabs[tabFocus].click();
}
});
});
const currentTab = localStorage.getItem(LOCAL_STORAGE_KEY);
if (currentTab) {
const el = document.querySelector(`[data-tabname="${currentTab}"]`) as HTMLElement;
if (el) changeTabs({target: el});
}
function changeTabs(e: { target: HTMLElement }) {
const target = e.target;
const parent = target.parentNode;
const grandparent = parent.parentNode;
// Remove all current selected tabs
parent
.querySelectorAll('[aria-selected="true"]')
.forEach((t) => t.setAttribute('aria-selected', `false`));
// Set this tab as selected
target.setAttribute('aria-selected', `true`);
const tabName = target.dataset.tabname;
const relatedTabs: NodeListOf<HTMLElement> = document.querySelectorAll(`[role="tab"][data-tabname="${target.dataset.tabname}"]`);
localStorage.setItem(LOCAL_STORAGE_KEY, tabName);
for (let relatedTab of relatedTabs) {
if (relatedTab === target) continue;
changeTabs({target: relatedTab});
}
// Hide all tab panels
grandparent
.querySelectorAll('[role="tabpanel"]')
.forEach((p) => p.setAttribute('hidden', `true`));
// Show the selected panel
grandparent.parentNode
.querySelector(`#${target.getAttribute('aria-controls')}`)
.removeAttribute('hidden');
}
/* -------------------- */
function checkElementsParents(el: HTMLElement, check: (el: HTMLElement) => boolean): boolean {
if (el.parentElement) {
if (!check(el.parentElement)) {
return checkElementsParents(el.parentElement, check);
} else {
return true;
}
} else {
return false;
}
}
(() => {
// If user has linked to a heading that's inside of a tab
const hash = window.location.hash;
if (!hash) return;
const heading = document.querySelector<HTMLElement>(hash);
if (!heading) return;
const isHidden = checkElementsParents(heading, el =>
el.hasAttribute('hidden') && el.getAttribute('hidden') !== "false"
)
// If it's not hidden, then we can assume that the browser will auto-scroll to it
if (!isHidden) return;
const partialHash = hash.slice(1);
try {
const matchingTab = document.querySelector<HTMLElement>(
`[data-headers*="${partialHash}"`
);
if (!matchingTab) return;
// If header is not in a tab
const tabName = matchingTab.getAttribute("data-tabname");
if (!tabName) return;
matchingTab.click();
setTimeout(() => {
const el = document.querySelector(hash);
if (!el) return;
el.scrollIntoView(true);
}, 0);
} catch (e) {
console.error("Error finding matching tab", e);
}
})()
});
</script>
<script is:inline defer src="/scripts/tabs.min.js"/>