menu: sidebar responds to changes in history, restores on reload

This commit is contained in:
dragonwocky 2021-10-29 12:22:24 +11:00
parent 4d8eec7399
commit 6a84fbec91
4 changed files with 146 additions and 120 deletions

View File

@ -1,5 +1,5 @@
/*
* notion-enhancer: collapsible headerrs
* notion-enhancer: collapsible headers
* (c) 2020 CloudHill <rl.cloudhill@gmail.com> (https://github.com/CloudHill)
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license

View File

@ -1,5 +1,5 @@
/*
* notion-enhancer: collapsible headerrs
* notion-enhancer: collapsible headers
* (c) 2020 CloudHill <rl.cloudhill@gmail.com> (https://github.com/CloudHill)
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license

View File

@ -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`<main class="main"></main>`,
Profile: ${web.escape(profileName)}
</button>`;
// 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
</button>`,
$error = web.html`<p class="profile-error"></p>`;
$export.addEventListener('click', async (event) => {
const now = new Date(),
$a = web.html`<a
@ -98,6 +99,7 @@ $profile.addEventListener('click', async (event) => {
$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`<article class="mod-container"></article>`,
web.render(
@ -262,7 +256,7 @@ const _$modListCache = {},
);
},
modList: async (category, message = '') => {
if (!_$modListCache[category]) {
if (!$modLists[category]) {
const $search = web.html`<input type="search" class="search"
placeholder="Search ('/' to focus)">`,
$list = web.html`<div class="mods-list"></div>`,
@ -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`<div></div>`,
web.render(
web.html`<label class="search-container"></label>`,
@ -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`<h1 class="nav-notion">
${(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);
}

View File

@ -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^="?"]']
);