mirror of
https://github.com/notion-enhancer/notion-enhancer.git
synced 2025-04-06 21:49:03 +00:00
menu: sidebar responds to changes in history, restores on reload
This commit is contained in:
parent
4d8eec7399
commit
6a84fbec91
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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^="?"]']
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user