/** * SPA Router v1.0.0 * ----------------- * Легкий роутер для односторінкових застосунків з підтримкою: * - History та hash режимів * - Параметрів маршруту та query-параметрів * - Делегування посилань та збереження прокрутки * * Використання: * Router.config({ mode: 'history', root: '/' }).listen().delegateLinks(); * * Автор: Rozenrod (https://github.com/rozenrod) * Ліцензія: MIT */ const Router = { routes: [], // Список маршрутів (шаблон + обробник) mode: 'history', // Режим: 'history' або 'hash' root: '/', // Корінь додатку // Налаштування роутера config({ mode = 'hash', root = '/' } = {}) { const cleanRoot = '/' + this.clearSlashes(root) + '/'; // Якщо браузер підтримує history API та mode === 'history' — використовуємо його this.mode = (mode === 'history' && history.pushState) ? 'history' : 'hash'; // Захист від абсолютних URL (наприклад, http://...) this.root = cleanRoot.startsWith('http') ? '/' : cleanRoot; return this; }, // Прибирає слеші на початку та в кінці шляху clearSlashes(path) { return path.toString().replace(/^\/+|\/+$/g, ''); }, // Отримує поточний фрагмент (частину URL після root або після #) getFragment() { let fragment = ''; if (this.mode === 'history') { fragment = decodeURI(location.pathname + location.search); fragment = fragment.replace(this.root, '').split('?')[0]; } else { fragment = location.hash.slice(1).split('?')[0]; } return this.clearSlashes(fragment); }, // Отримує query-параметри з URL getParams() { let query = this.mode === 'history' ? location.search : location.hash.split('?')[1] || ''; const params = {}; new URLSearchParams(query).forEach((v, k) => params[k] = v); return params; }, // Додає новий маршрут add(re, handler, options) { if (typeof re === 'function') { handler = re; re = ''; } this.routes.push({ re, handler, options }); return this; }, // Видаляє маршрут за функцією або шаблоном remove(param) { this.routes = this.routes.filter(r => r.handler !== param && r.re.toString() !== param.toString() ); return this; }, // Очищує усі маршрути та скидає режим flush() { this.routes = []; this.mode = 'hash'; this.root = '/'; return this; }, // Перевіряє фрагмент та викликає відповідний обробник маршруту check(fragment = this.getFragment()) { const query = this.getParams(); for (const { re, handler } of this.routes) { const match = (fragment || 'home').match(re); if (match) { handler.apply({ query }, match.slice(1)); // Прокрутка до елемента з id, якщо є #hash if (location.hash.length > 1) { const el = document.getElementById(location.hash.slice(1)); if (el) el.scrollIntoView({ behavior: 'smooth' }); } return this; } } return this; }, // Слухає зміни URL listen() { const onChange = (e) => { this.check(); // Витягуємо позицію прокрутки зі стану історії const pos = e?.state?.scroll; if (pos) { setTimeout(() => { window.scrollTo(pos.x, pos.y); }, 50); } }; if (this.mode === 'history') { window.addEventListener('popstate', onChange); } else { window.addEventListener('hashchange', onChange); } return this; }, // Делегування кліків по посиланнях з data-route delegateLinks() { window.addEventListener('click', (e) => { // const target = e.target.closest('[data-route]'); const pathNodes = e.composedPath(); const target = pathNodes.find(node => node.tagName === 'A' && node.hasAttribute('data-route') ); if (!target || !target.href) return; const path = target.href.replace(location.origin, ''); if (path === this.getFragment()) return; e.preventDefault(); this.navigate(path); // Викликає навігацію без перезавантаження сторінки }); return this; }, // Навігація до нового шляху (push або replace) navigate(path = '', push = true, update = true) { const scroll = { x: window.scrollX, y: window.scrollY }; history.replaceState({ scroll }, '', location.href); if (this.mode === 'history') { const url = new URL(path, location.origin); const relativePath = url.pathname + url.search + url.hash; history[push ? 'pushState' : 'replaceState']({}, '', relativePath); } else { location.hash = this.clearSlashes(path); } if (update == true) { window.scrollTo(0, 0); // Скидуємо прокрутку при переході this.check(); } return this; }, // Оновлює query-параметр у поточному URL update(key, value) { const params = new URLSearchParams(location.search); params.set(key, value); const newUrl = location.origin + location.pathname + '?' + params.toString(); history.replaceState({ position: window.pageYOffset }, '', newUrl); } };