diff --git a/public/assets/js/script-extra.js b/public/assets/js/script-extra.js index 692171f..334f4d1 100644 --- a/public/assets/js/script-extra.js +++ b/public/assets/js/script-extra.js @@ -1,3 +1,144 @@ +/* View Transition */ +(() => { + if (!document.startViewTransition) return; + + function preloadAssets(newDoc) { + const curStyleHrefs = new Set( + [...document.head.querySelectorAll('link[rel="stylesheet"]')].map(l => l.href) + ); + const curScriptSrcs = new Set( + [...document.head.querySelectorAll('script[src]')].map(s => s.src) + ); + + const tasks = []; + + [...newDoc.head.querySelectorAll('link[rel="stylesheet"]')] + .filter(l => !curStyleHrefs.has(new URL(l.href, location.href).href)) + .forEach(link => tasks.push(new Promise(resolve => { + const l = link.cloneNode(true); + l.addEventListener('load', resolve, { once: true }); + l.addEventListener('error', resolve, { once: true }); + document.head.appendChild(l); + }))); + + [...newDoc.head.querySelectorAll('script[src]')] + .filter(s => !curScriptSrcs.has(new URL(s.src, location.href).href)) + .forEach(script => tasks.push(new Promise(resolve => { + const s = document.createElement('script'); + [...script.attributes].forEach(a => s.setAttribute(a.name, a.value)); + s.addEventListener('load', resolve, { once: true }); + s.addEventListener('error', resolve, { once: true }); + document.head.appendChild(s); + }))); + + return Promise.all(tasks); + } + + function updateHead(newDoc) { + const head = document.head; + const newHead = newDoc.head; + + const t = newHead.querySelector('title'); + if (t) document.title = t.textContent; + + const META_KEEP = new Set(['charset', 'viewport', 'color-scheme', 'theme-color']); + head.querySelectorAll('meta').forEach(m => { + const key = m.getAttribute('name') || m.getAttribute('property'); + if (!key || META_KEEP.has(key)) return; + m.remove(); + }); + const insertRef = head.querySelector( + 'link[rel="preconnect"], link[rel="stylesheet"], link[rel="manifest"], link[rel="icon"], script' + ); + newHead.querySelectorAll('meta[name], meta[property]').forEach(m => { + const key = m.getAttribute('name') || m.getAttribute('property'); + if (!META_KEEP.has(key)) head.insertBefore(m.cloneNode(true), insertRef); + }); + + const nc = newHead.querySelector('link[rel="canonical"]'); + const cc = head.querySelector('link[rel="canonical"]'); + if (nc && cc) cc.href = nc.href; + + const newStyleHrefs = new Set( + [...newHead.querySelectorAll('link[rel="stylesheet"]')] + .map(l => new URL(l.href, location.href).href) + ); + head.querySelectorAll('link[rel="stylesheet"]').forEach(l => { + if (!newStyleHrefs.has(l.href)) l.remove(); + }); + + const newScriptSrcs = new Set( + [...newHead.querySelectorAll('script[src]')] + .map(s => new URL(s.src, location.href).href) + ); + head.querySelectorAll('script[src]').forEach(s => { + if (!newScriptSrcs.has(s.src)) s.remove(); + }); + + head.querySelectorAll('style').forEach(s => s.remove()); + newHead.querySelectorAll('style').forEach(s => head.appendChild(s.cloneNode(true))); + } + + document.addEventListener('click', (event) => { + const link = event.target.closest('a'); + if (!link || link.hasAttribute('download')) return; + + const url = new URL(link.href, location.href); + if (url.origin !== location.origin) return; + if (link.target || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; + + if (url.hash && url.pathname === location.pathname) { + event.preventDefault(); + const target = document.querySelector(url.hash); + if (target) { + target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + history.pushState(null, '', url.hash); + } + return; + } + + event.preventDefault(); + + document.startViewTransition(async () => { + const response = await fetch(url.href, { + headers: { 'X-Requested-With': 'view-transition' } + }); + const html = await response.text(); + const doc = new DOMParser().parseFromString(html, 'text/html'); + + if (typeof window.__cursorCleanup === 'function') { + window.__cursorCleanup(); + window.__cursorCleanup = null; + } + + await preloadAssets(doc); + updateHead(doc); + + for (const tag of ['header', 'main', 'footer']) { + const newEl = doc.querySelector(tag); + const curEl = document.querySelector(tag); + if (!newEl || !curEl) { location.href = url.href; return; } + curEl.innerHTML = newEl.innerHTML; + curEl.querySelectorAll('script').forEach(old => { + const s = document.createElement('script'); + [...old.attributes].forEach(a => s.setAttribute(a.name, a.value)); + s.textContent = old.textContent; + old.replaceWith(s); + }); + } + + history.pushState(null, '', url.href); + + if (typeof window.__cursorReinit === 'function') { + window.__cursorReinit(); + } + }); + }); + + window.addEventListener('popstate', () => location.reload()); +})(); + +/* Cursor */ (() => { const textSelectors = 'p, h1, h2, h3, h4, h5, h6, span, li, label, td, th, pre, .code'; const linkSelectors = 'a, button, [role="button"], input[type="submit"], input[type="button"]'; @@ -15,7 +156,7 @@ let isMouseDown = false; const TOUCH_MOUSE_GUARD_MS = 800; - window.__scriptCleanup = () => { + window.__cursorCleanup = () => { if (ac) ac.abort(); if (rafId) { cancelAnimationFrame(rafId); rafId = null; } document.documentElement.style.cursor = ''; @@ -128,165 +269,37 @@ }, { passive: true, signal: sig }); } - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', init, { signal: sig }); - } else { + function reinit() { + cursor = document.getElementById('cursor'); + if (!cursor) return; + + currentLinkEl = null; + if (rafId) { cancelAnimationFrame(rafId); rafId = null; } + cursor.classList.remove('on-link', 'on-text'); + cursor.style.transform = 'translate(-50%, -50%)'; + cursor.style.left = mouseX + 'px'; + cursor.style.top = mouseY + 'px'; + cursor.style.width = ''; + cursor.style.height = ''; + + const el = document.elementFromPoint(mouseX, mouseY); + const newLinkEl = el ? el.closest(linkSelectors) : null; + if (newLinkEl) { + currentLinkEl = newLinkEl; + rafId = requestAnimationFrame(trackLink); + } else if (el && el.closest(textSelectors)) { + cursor.classList.add('on-text'); + } + init(); } - function preloadAssets(newDoc) { - const curStyleHrefs = new Set( - [...document.head.querySelectorAll('link[rel="stylesheet"]')].map(l => l.href) - ); - const curScriptSrcs = new Set( - [...document.head.querySelectorAll('script[src]')].map(s => s.src) - ); + window.__cursorReinit = reinit; + window.__cursorGetState = () => ({ mouseX, mouseY, currentLinkEl, rafId, trackLink, linkSelectors, textSelectors }); - const tasks = []; - - [...newDoc.head.querySelectorAll('link[rel="stylesheet"]')] - .filter(l => !curStyleHrefs.has(new URL(l.href, location.href).href)) - .forEach(link => tasks.push(new Promise(resolve => { - const l = link.cloneNode(true); - l.addEventListener('load', resolve, { once: true }); - l.addEventListener('error', resolve, { once: true }); - document.head.appendChild(l); - }))); - - [...newDoc.head.querySelectorAll('script[src]')] - .filter(s => !curScriptSrcs.has(new URL(s.src, location.href).href)) - .forEach(script => tasks.push(new Promise(resolve => { - const s = document.createElement('script'); - [...script.attributes].forEach(a => s.setAttribute(a.name, a.value)); - s.addEventListener('load', resolve, { once: true }); - s.addEventListener('error', resolve, { once: true }); - document.head.appendChild(s); - }))); - - return Promise.all(tasks); - } - - function updateHead(newDoc) { - const head = document.head; - const newHead = newDoc.head; - - const t = newHead.querySelector('title'); - if (t) document.title = t.textContent; - - const META_KEEP = new Set(['charset', 'viewport', 'color-scheme', 'theme-color']); - head.querySelectorAll('meta').forEach(m => { - const key = m.getAttribute('name') || m.getAttribute('property'); - if (!key || META_KEEP.has(key)) return; - m.remove(); - }); - const insertRef = head.querySelector( - 'link[rel="preconnect"], link[rel="stylesheet"], link[rel="manifest"], link[rel="icon"], script' - ); - newHead.querySelectorAll('meta[name], meta[property]').forEach(m => { - const key = m.getAttribute('name') || m.getAttribute('property'); - if (!META_KEEP.has(key)) head.insertBefore(m.cloneNode(true), insertRef); - }); - - const nc = newHead.querySelector('link[rel="canonical"]'); - const cc = head.querySelector('link[rel="canonical"]'); - if (nc && cc) cc.href = nc.href; - - const newStyleHrefs = new Set( - [...newHead.querySelectorAll('link[rel="stylesheet"]')] - .map(l => new URL(l.href, location.href).href) - ); - head.querySelectorAll('link[rel="stylesheet"]').forEach(l => { - if (!newStyleHrefs.has(l.href)) l.remove(); - }); - - const newScriptSrcs = new Set( - [...newHead.querySelectorAll('script[src]')] - .map(s => new URL(s.src, location.href).href) - ); - head.querySelectorAll('script[src]').forEach(s => { - if (!newScriptSrcs.has(s.src)) s.remove(); - }); - - head.querySelectorAll('style').forEach(s => s.remove()); - newHead.querySelectorAll('style').forEach(s => head.appendChild(s.cloneNode(true))); - } - - if (document.startViewTransition) { - document.addEventListener('click', (event) => { - const link = event.target.closest('a'); - if (!link || link.hasAttribute('download')) return; - - const url = new URL(link.href, location.href); - if (url.origin !== location.origin) return; - if (link.target || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; - - if (url.hash && url.pathname === location.pathname) { - event.preventDefault(); - const target = document.querySelector(url.hash); - if (target) { - target.scrollIntoView({ behavior: 'smooth', block: 'start' }); - history.pushState(null, '', url.hash); - } - return; - } - - event.preventDefault(); - - document.startViewTransition(async () => { - const response = await fetch(url.href, { - headers: { 'X-Requested-With': 'view-transition' } - }); - const html = await response.text(); - const doc = new DOMParser().parseFromString(html, 'text/html'); - - if (typeof window.__scriptCleanup === 'function') { - window.__scriptCleanup(); - window.__scriptCleanup = null; - } - - await preloadAssets(doc); - - updateHead(doc); - - for (const tag of ['header', 'main', 'footer']) { - const newEl = doc.querySelector(tag); - const curEl = document.querySelector(tag); - if (!newEl || !curEl) { location.href = url.href; return; } - curEl.innerHTML = newEl.innerHTML; - curEl.querySelectorAll('script').forEach(old => { - const s = document.createElement('script'); - [...old.attributes].forEach(a => s.setAttribute(a.name, a.value)); - s.textContent = old.textContent; - old.replaceWith(s); - }); - } - - history.pushState(null, '', url.href); - - if (cursor) { - currentLinkEl = null; - if (rafId) { cancelAnimationFrame(rafId); rafId = null; } - cursor.classList.remove('on-link', 'on-text'); - cursor.style.transform = 'translate(-50%, -50%)'; - cursor.style.left = mouseX + 'px'; - cursor.style.top = mouseY + 'px'; - cursor.style.width = ''; - cursor.style.height = ''; - - const el = document.elementFromPoint(mouseX, mouseY); - const newLinkEl = el ? el.closest(linkSelectors) : null; - if (newLinkEl) { - currentLinkEl = newLinkEl; - rafId = requestAnimationFrame(trackLink); - } else if (el && el.closest(textSelectors)) { - cursor.classList.add('on-text'); - } - } - - init(); - }); - }); - - window.addEventListener('popstate', () => location.reload()); + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init, { once: true }); + } else { + init(); } })();