diff --git a/repo/collapsible-headers/client.css b/repo/collapsible-headers/client.css index 69016ed..668e9ec 100644 --- a/repo/collapsible-headers/client.css +++ b/repo/collapsible-headers/client.css @@ -1,5 +1,5 @@ /* - * notion-enhancer: collapsible headerrs + * notion-enhancer: collapsible headers * (c) 2020 CloudHill (https://github.com/CloudHill) * (c) 2021 dragonwocky (https://dragonwocky.me/) * (https://notion-enhancer.github.io/) under the MIT license diff --git a/repo/collapsible-headers/client.mjs b/repo/collapsible-headers/client.mjs index 858178a..9dc41fc 100644 --- a/repo/collapsible-headers/client.mjs +++ b/repo/collapsible-headers/client.mjs @@ -1,5 +1,5 @@ /* - * notion-enhancer: collapsible headerrs + * notion-enhancer: collapsible headers * (c) 2020 CloudHill (https://github.com/CloudHill) * (c) 2021 dragonwocky (https://dragonwocky.me/) * (https://notion-enhancer.github.io/) under the MIT license diff --git a/repo/menu/menu.mjs b/repo/menu/menu.mjs index 1072545..b34d9ee 100644 --- a/repo/menu/menu.mjs +++ b/repo/menu/menu.mjs @@ -12,6 +12,7 @@ import './styles.mjs'; import { env, fs, storage, registry, web, components } from '../../api/_.mjs'; import { notifications } from './launcher.mjs'; import { modComponents, options } from './components.mjs'; +import * as router from './router.mjs'; const db = await registry.db('a6621988-551d-495a-97d8-3c568bca2e9e'), profileName = await registry.profileName(), @@ -40,11 +41,10 @@ const $main = web.html`
`, Profile: ${web.escape(profileName)} `; +// profile + let _$profileConfig; -$profile.addEventListener('click', async (event) => { - for (const $selected of document.querySelectorAll('.mod-selected')) { - $selected.className = 'mod'; - } +const openProfileMenu = async () => { if (!_$profileConfig) { const profileNames = [ ...new Set([ @@ -83,6 +83,7 @@ $profile.addEventListener('click', async (event) => { ${await components.feather('trash-2', { class: 'profile-icon-text' })} Delete `, $error = web.html`

`; + $export.addEventListener('click', async (event) => { const now = new Date(), $a = web.html` { $a.click(); $a.remove(); }); + $import.addEventListener('change', (event) => { const file = event.target.files[0], reader = new FileReader(); @@ -113,11 +115,13 @@ $profile.addEventListener('click', async (event) => { }; reader.readAsText(file); }); + $select.addEventListener('change', async (event) => { if ($select.value === '--') { $edit.value = ''; } else $edit.value = $select.value; }); + $save.addEventListener('click', async (event) => { if (profileNames.includes($edit.value) && $select.value !== $edit.value) { web.render( @@ -149,6 +153,7 @@ $profile.addEventListener('click', async (event) => { } env.reload(); }); + $delete.addEventListener('click', async (event) => { await storage.set(['profiles', $select.value], undefined); await storage.set( @@ -183,9 +188,12 @@ $profile.addEventListener('click', async (event) => { ); } web.render(web.empty($options), _$profileConfig); -}); +}; +$profile.addEventListener('click', () => openSidebarMenu('profile')); -const _$modListCache = {}, +// mods + +const $modLists = {}, generators = { options: async (mod) => { const $fragment = document.createDocumentFragment(); @@ -222,21 +230,7 @@ const _$modListCache = {}, profileDB.set(['_mods', mod.id], event.target.checked); notifications.onChange(); }); - $mod.addEventListener('click', async (event) => { - if ($mod.className === 'mod-selected') return; - for (const $list of Object.values(_$modListCache)) { - for (const $selected of $list.querySelectorAll('.mod-selected')) { - $selected.className = 'mod'; - } - } - $mod.className = 'mod-selected'; - const fragment = [ - web.render(modComponents.title(mod.name), modComponents.version(mod.version)), - modComponents.tags(mod.tags), - await generators.options(mod), - ]; - web.render(web.empty($options), ...fragment); - }); + $mod.addEventListener('click', () => openSidebarMenu(mod.id)); return web.render( web.html`
`, web.render( @@ -262,7 +256,7 @@ const _$modListCache = {}, ); }, modList: async (category, message = '') => { - if (!_$modListCache[category]) { + if (!$modLists[category]) { const $search = web.html``, $list = web.html`
`, @@ -282,7 +276,7 @@ const _$modListCache = {}, web.render($list, await generators.mod(mod)); mod.tags.unshift(category); } - _$modListCache[category] = web.render( + $modLists[category] = web.render( web.html`
`, web.render( web.html``, @@ -293,10 +287,30 @@ const _$modListCache = {}, $list ); } - return _$modListCache[category]; + return $modLists[category]; }, }; +async function openModMenu(id) { + let $mod; + for (const $list of Object.values($modLists)) { + $mod = $list.querySelector(`[data-id="${web.escape(id)}"]`); + if ($mod) break; + } + const mod = await registry.get(id); + if (!$mod || !mod || $mod.className === 'mod-selected') return; + + $mod.className = 'mod-selected'; + const fragment = [ + web.render(modComponents.title(mod.name), modComponents.version(mod.version)), + modComponents.tags(mod.tags), + await generators.options(mod), + ]; + web.render(web.empty($options), ...fragment); +} + +// views + const $notionNavItem = web.html`

${(await fs.getText('media/colour.svg')).replace( /width="\d+" height="\d+"/, @@ -338,51 +352,61 @@ function selectNavItem($item) { $item.className = 'nav-item-selected'; } -import * as router from './router.mjs'; - +await generators.modList( + 'core', + `Core mods provide the basics required for + all other extensions and themes to work. They + can't be disabled, but they can be configured + - just click on a mod to access its options.` +); router.addView('core', async () => { web.empty($main); selectNavItem($coreNavItem); - return web.render( - $main, - await generators.modList( - 'core', - `Core mods provide the basics required for - all other extensions and themes to work. They - can't be disabled, but they can be configured - - just click on a mod to access its options.` - ) - ); + return web.render($main, await generators.modList('core')); }); +await generators.modList( + 'extension', + `Extensions modify and extend the functionality + or layout of the Notion client. They don't interfere + with Notion's data structures, so they can be safely + enabled or disabled at any time.` +); router.addView('extensions', async () => { web.empty($main); selectNavItem($extensionsNavItem); - return web.render( - $main, - await generators.modList( - 'extension', - `Extensions modify and extend the functionality - or layout of the Notion client. They don't interfere - with Notion's data structures, so they can be safely - enabled or disabled at any time.` - ) - ); + return web.render($main, await generators.modList('extension')); }); +await generators.modList( + 'theme', + `Themes change Notion's colour scheme. + Dark themes will only work when Notion is in dark mode, + and light themes will only work when Notion is in light mode. + Only one theme of each mode can be enabled at a time.` +); router.addView('themes', async () => { web.empty($main); selectNavItem($themesNavItem); - return web.render( - $main, - await generators.modList( - 'theme', - `Themes change Notion's colour scheme. - Dark themes will only work when Notion is in dark mode, - and light themes will only work when Notion is in light mode. - Only one theme of each mode can be enabled at a time.` - ) - ); + return web.render($main, await generators.modList('theme')); }); -router.loadView('extensions', $main); +router.setDefaultView('extensions'); + +router.addQueryListener('id', openSidebarMenu); +async function openSidebarMenu(id) { + if (!id) return; + id = web.escape(id); + + const deselectedMods = `.mod-selected:not([data-id="${id}"])`; + for (const $list of Object.values($modLists)) { + for (const $selected of $list.querySelectorAll(deselectedMods)) { + $selected.className = 'mod'; + } + } + router.updateQuery(`?id=${id}`); + + if (id === 'profile') { + openProfileMenu(); + } else openModMenu(id); +} diff --git a/repo/menu/router.mjs b/repo/menu/router.mjs index 1eb189f..6ca13dd 100644 --- a/repo/menu/router.mjs +++ b/repo/menu/router.mjs @@ -8,80 +8,82 @@ import { web } from '../../api/_.mjs'; -let _defaultView = ''; -const _views = new Map(); +const _queryListeners = new Set(); export function addView(name, loadFunc) { - _views.set(name, loadFunc); + const handlerFunc = (newView) => { + if (newView === name) return loadFunc(); + return false; + }; + _queryListeners.add({ param: 'view', viewName: name, handlerFunc }); + handlerFunc(web.queryParams().get('view'), null); } export function removeView(name) { - _views.delete(name); + const view = [..._queryListeners].find((view) => view.viewName === name); + if (view) _queryListeners.delete(view); } +export async function setDefaultView(viewName) { + const viewList = [..._queryListeners].filter((q) => q.viewName).map((v) => v.viewName); + if (!viewList.includes(web.queryParams().get('view'))) { + updateQuery(`?view=${viewName}`, true); + } +} + +export function addQueryListener(param, handlerFunc) { + _queryListeners.add({ param: param, handlerFunc }); + handlerFunc(web.queryParams().get(param), null); +} +export function removeQueryListener(handlerFunc) { + const listener = [..._queryListeners].find((view) => view.handlerFunc === handlerFunc); + if (listener) _queryListeners.delete(listener); +} + +export const updateQuery = (search, replace = false) => { + let query = web.queryParams(); + for (const [key, val] of new URLSearchParams(search)) { + query.set(key, val); + } + query = `?${query.toString()}`; + if (location.search !== query) { + if (replace) { + window.history.replaceState(null, null, query); + } else { + window.history.pushState(null, null, query); + } + triggerQueryListeners(); + } +}; function router(event) { event.preventDefault(); const anchor = event.path ? event.path.find((anchor) => anchor.nodeName === 'A') : event.target; - if (location.search !== anchor.getAttribute('href')) { - window.history.pushState(null, null, anchor.href); - loadView(); + updateQuery(anchor.getAttribute('href')); +} + +let queryCache = ''; +async function triggerQueryListeners() { + if (location.search === queryCache) return; + const newQuery = web.queryParams(), + oldQuery = new URLSearchParams(queryCache); + console.log(location.search, queryCache); + queryCache = location.search; + for (const listener of _queryListeners) { + const newParam = newQuery.get(listener.param), + oldParam = oldQuery.get(listener.param); + if (newParam !== oldParam) listener.handlerFunc(newParam, oldParam); } } -function navigator(event) { - event.preventDefault(); - const anchor = event.path - ? event.path.find((anchor) => anchor.nodeName === 'A') - : event.target, - hash = anchor.getAttribute('href').slice(1); - document.getElementById(hash).scrollIntoView(true); - document.documentElement.scrollTop = 0; - history.replaceState({ search: location.search, hash }, null, `#${hash}`); -} -export async function loadView(defaultView = null) { - if (defaultView) _defaultView = defaultView; - if (!_defaultView) throw new Error('no view root set.'); +window.addEventListener('popstate', triggerQueryListeners); - const query = web.queryParams(), - fallbackView = () => { - window.history.replaceState(null, null, `?view=${_defaultView}`); - return loadView(); - }; - if (!query.get('view') || document.body.dataset.view !== query.get('view')) { - if (_views.get(query.get('view'))) { - await _views.get(query.get('view'))(); - } else return fallbackView(); - } else return fallbackView(); -} - -window.addEventListener('popstate', (event) => { - if (event.state) loadView(); - document.getElementById(location.hash.slice(1))?.scrollIntoView(true); - document.documentElement.scrollTop = 0; -}); - -const documentObserverEvents = [], - handleDocumentMutations = (queue) => { - while (queue.length) { - const mutation = queue.shift(); - mutation.target.querySelectorAll('a[href^="?"]').forEach((a) => { - a.removeEventListener('click', router); - a.addEventListener('click', router); - }); - mutation.target.querySelectorAll('a[href^="#"]').forEach((a) => { - a.removeEventListener('click', navigator); - a.addEventListener('click', navigator); - }); - } +web.addDocumentObserver( + (mutation) => { + mutation.target.querySelectorAll('a[href^="?"]').forEach((a) => { + a.removeEventListener('click', router); + a.addEventListener('click', router); + }); }, - documentObserver = new MutationObserver((list, observer) => { - if (!documentObserverEvents.length) - requestIdleCallback(() => handleDocumentMutations(documentObserverEvents)); - documentObserverEvents.push(...list); - }); -documentObserver.observe(document.body, { - childList: true, - subtree: true, - attributes: true, -}); + ['a[href^="?"]'] +);