From 3ddda7e2ca5b6950296ec4fef6c6d40f9741acf4 Mon Sep 17 00:00:00 2001 From: nercone-dev Date: Thu, 16 Apr 2026 12:33:10 +0900 Subject: [PATCH] -- --- public/assets/js/script.js | 198 ++++++++++++++++++++++++++++--------- 1 file changed, 153 insertions(+), 45 deletions(-) diff --git a/public/assets/js/script.js b/public/assets/js/script.js index 2e37af7..3ad40fd 100644 --- a/public/assets/js/script.js +++ b/public/assets/js/script.js @@ -3,6 +3,9 @@ const linkSelectors = 'a, button, [role="button"], input[type="submit"], input[type="button"]'; const padding = 6; + const ac = new AbortController(); + const sig = ac.signal; + let mouseX = 0, mouseY = 0; let currentLinkEl = null; let rafId = null; @@ -12,6 +15,15 @@ let isMouseDown = false; const TOUCH_MOUSE_GUARD_MS = 800; + window.__scriptCleanup = () => { + ac.abort(); + if (rafId) { cancelAnimationFrame(rafId); rafId = null; } + document.documentElement.style.cursor = ''; + if (cursor) cursor.classList.remove('visible', 'on-link', 'on-text'); + cursorVisible = false; + currentLinkEl = null; + }; + function showCursor() { if (!cursorVisible && cursor) { cursorVisible = true; @@ -51,13 +63,15 @@ } } - document.addEventListener('DOMContentLoaded', () => { + document.documentElement.style.cursor = 'none'; + + function init() { cursor = document.getElementById('cursor'); if (!cursor) return; - document.addEventListener('touchstart', () => { lastTouchTime = Date.now(); hideCursor(); }, { passive: true }); - document.addEventListener('touchmove', () => { lastTouchTime = Date.now(); hideCursor(); }, { passive: true }); - document.addEventListener('touchend', () => { lastTouchTime = Date.now(); }, { passive: true }); + document.addEventListener('touchstart', () => { lastTouchTime = Date.now(); hideCursor(); }, { passive: true, signal: sig }); + document.addEventListener('touchmove', () => { lastTouchTime = Date.now(); hideCursor(); }, { passive: true, signal: sig }); + document.addEventListener('touchend', () => { lastTouchTime = Date.now(); }, { passive: true, signal: sig }); document.addEventListener('mousemove', (e) => { if (isSyntheticMouse()) return; @@ -95,51 +109,143 @@ cursor.classList.remove('on-text'); } } - }); + }, { signal: sig }); document.addEventListener('mousedown', () => { isMouseDown = true; cursor.style.transform = currentLinkEl ? 'none' : 'translate(-50%, -50%) scale(0.9)'; - }); + }, { signal: sig }); document.addEventListener('mouseup', () => { isMouseDown = false; cursor.style.transform = currentLinkEl ? 'none' : 'translate(-50%, -50%) scale(1)'; - }); + }, { signal: sig }); window.addEventListener('scroll', () => { if (currentLinkEl) updateCursorForLink(currentLinkEl); - }, { passive: true }); - }); + }, { passive: true, signal: sig }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init, { signal: sig }); + } else { + 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) + ); + + 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; + 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; + 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); + 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; } - return; - } - event.preventDefault(); + event.preventDefault(); document.startViewTransition(async () => { const response = await fetch(url.href, { - headers: { "X-Requested-With": "view-transition" } + headers: { 'X-Requested-With': 'view-transition' } }); const html = await response.text(); - const doc = new DOMParser().parseFromString(html, "text/html"); + const doc = new DOMParser().parseFromString(html, 'text/html'); - for (const tag of ['head', 'header', 'main', 'footer']) { + 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; } @@ -152,28 +258,30 @@ }); } - history.pushState(null, "", url.href); + history.pushState(null, '', url.href); - 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 = ''; + 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'); + 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'); + } } }); - }); + }, { signal: sig }); - window.addEventListener("popstate", () => location.reload()); + window.addEventListener('popstate', () => location.reload(), { signal: sig }); } })();