diff --git a/public/scripts/backbtn.js b/public/scripts/backbtn.js new file mode 100644 index 00000000..fe7263d4 --- /dev/null +++ b/public/scripts/backbtn.js @@ -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); + }) +} \ No newline at end of file diff --git a/public/scripts/backbtn.min.js b/public/scripts/backbtn.min.js new file mode 100644 index 00000000..16ca72e3 --- /dev/null +++ b/public/scripts/backbtn.min.js @@ -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)})}; \ No newline at end of file diff --git a/public/scripts/tabs.js b/public/scripts/tabs.js new file mode 100644 index 00000000..e9f7cbfd --- /dev/null +++ b/public/scripts/tabs.js @@ -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} + */ + 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} + */ + 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); + } + })() + +}); \ No newline at end of file diff --git a/public/scripts/tabs.min.js b/public/scripts/tabs.min.js new file mode 100644 index 00000000..361a6cef --- /dev/null +++ b/public/scripts/tabs.min.js @@ -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.querySelectore;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`[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)}})()}); \ No newline at end of file diff --git a/public/scripts/themetoggle.js b/public/scripts/themetoggle.js new file mode 100644 index 00000000..28192c57 --- /dev/null +++ b/public/scripts/themetoggle.js @@ -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) +}) \ No newline at end of file diff --git a/public/scripts/themetoggle.min.js b/public/scripts/themetoggle.min.js new file mode 100644 index 00000000..138f0ab2 --- /dev/null +++ b/public/scripts/themetoggle.min.js @@ -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)}); \ No newline at end of file diff --git a/src/components/dark-light-button/dark-light-button.astro b/src/components/dark-light-button/dark-light-button.astro index d8643428..5ba37eb6 100644 --- a/src/components/dark-light-button/dark-light-button.astro +++ b/src/components/dark-light-button/dark-light-button.astro @@ -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"; - --- - - - + + \ No newline at end of file +