mirror of
https://github.com/notion-enhancer/notion-enhancer.git
synced 2025-04-09 15:09:02 +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) 2020 CloudHill <rl.cloudhill@gmail.com> (https://github.com/CloudHill)
|
||||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
* (https://notion-enhancer.github.io/) under the MIT license
|
* (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) 2020 CloudHill <rl.cloudhill@gmail.com> (https://github.com/CloudHill)
|
||||||
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
|
||||||
* (https://notion-enhancer.github.io/) under the MIT license
|
* (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 { env, fs, storage, registry, web, components } from '../../api/_.mjs';
|
||||||
import { notifications } from './launcher.mjs';
|
import { notifications } from './launcher.mjs';
|
||||||
import { modComponents, options } from './components.mjs';
|
import { modComponents, options } from './components.mjs';
|
||||||
|
import * as router from './router.mjs';
|
||||||
|
|
||||||
const db = await registry.db('a6621988-551d-495a-97d8-3c568bca2e9e'),
|
const db = await registry.db('a6621988-551d-495a-97d8-3c568bca2e9e'),
|
||||||
profileName = await registry.profileName(),
|
profileName = await registry.profileName(),
|
||||||
@ -40,11 +41,10 @@ const $main = web.html`<main class="main"></main>`,
|
|||||||
Profile: ${web.escape(profileName)}
|
Profile: ${web.escape(profileName)}
|
||||||
</button>`;
|
</button>`;
|
||||||
|
|
||||||
|
// profile
|
||||||
|
|
||||||
let _$profileConfig;
|
let _$profileConfig;
|
||||||
$profile.addEventListener('click', async (event) => {
|
const openProfileMenu = async () => {
|
||||||
for (const $selected of document.querySelectorAll('.mod-selected')) {
|
|
||||||
$selected.className = 'mod';
|
|
||||||
}
|
|
||||||
if (!_$profileConfig) {
|
if (!_$profileConfig) {
|
||||||
const profileNames = [
|
const profileNames = [
|
||||||
...new Set([
|
...new Set([
|
||||||
@ -83,6 +83,7 @@ $profile.addEventListener('click', async (event) => {
|
|||||||
${await components.feather('trash-2', { class: 'profile-icon-text' })} Delete
|
${await components.feather('trash-2', { class: 'profile-icon-text' })} Delete
|
||||||
</button>`,
|
</button>`,
|
||||||
$error = web.html`<p class="profile-error"></p>`;
|
$error = web.html`<p class="profile-error"></p>`;
|
||||||
|
|
||||||
$export.addEventListener('click', async (event) => {
|
$export.addEventListener('click', async (event) => {
|
||||||
const now = new Date(),
|
const now = new Date(),
|
||||||
$a = web.html`<a
|
$a = web.html`<a
|
||||||
@ -98,6 +99,7 @@ $profile.addEventListener('click', async (event) => {
|
|||||||
$a.click();
|
$a.click();
|
||||||
$a.remove();
|
$a.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
$import.addEventListener('change', (event) => {
|
$import.addEventListener('change', (event) => {
|
||||||
const file = event.target.files[0],
|
const file = event.target.files[0],
|
||||||
reader = new FileReader();
|
reader = new FileReader();
|
||||||
@ -113,11 +115,13 @@ $profile.addEventListener('click', async (event) => {
|
|||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
});
|
});
|
||||||
|
|
||||||
$select.addEventListener('change', async (event) => {
|
$select.addEventListener('change', async (event) => {
|
||||||
if ($select.value === '--') {
|
if ($select.value === '--') {
|
||||||
$edit.value = '';
|
$edit.value = '';
|
||||||
} else $edit.value = $select.value;
|
} else $edit.value = $select.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
$save.addEventListener('click', async (event) => {
|
$save.addEventListener('click', async (event) => {
|
||||||
if (profileNames.includes($edit.value) && $select.value !== $edit.value) {
|
if (profileNames.includes($edit.value) && $select.value !== $edit.value) {
|
||||||
web.render(
|
web.render(
|
||||||
@ -149,6 +153,7 @@ $profile.addEventListener('click', async (event) => {
|
|||||||
}
|
}
|
||||||
env.reload();
|
env.reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
$delete.addEventListener('click', async (event) => {
|
$delete.addEventListener('click', async (event) => {
|
||||||
await storage.set(['profiles', $select.value], undefined);
|
await storage.set(['profiles', $select.value], undefined);
|
||||||
await storage.set(
|
await storage.set(
|
||||||
@ -183,9 +188,12 @@ $profile.addEventListener('click', async (event) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
web.render(web.empty($options), _$profileConfig);
|
web.render(web.empty($options), _$profileConfig);
|
||||||
});
|
};
|
||||||
|
$profile.addEventListener('click', () => openSidebarMenu('profile'));
|
||||||
|
|
||||||
const _$modListCache = {},
|
// mods
|
||||||
|
|
||||||
|
const $modLists = {},
|
||||||
generators = {
|
generators = {
|
||||||
options: async (mod) => {
|
options: async (mod) => {
|
||||||
const $fragment = document.createDocumentFragment();
|
const $fragment = document.createDocumentFragment();
|
||||||
@ -222,21 +230,7 @@ const _$modListCache = {},
|
|||||||
profileDB.set(['_mods', mod.id], event.target.checked);
|
profileDB.set(['_mods', mod.id], event.target.checked);
|
||||||
notifications.onChange();
|
notifications.onChange();
|
||||||
});
|
});
|
||||||
$mod.addEventListener('click', async (event) => {
|
$mod.addEventListener('click', () => openSidebarMenu(mod.id));
|
||||||
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);
|
|
||||||
});
|
|
||||||
return web.render(
|
return web.render(
|
||||||
web.html`<article class="mod-container"></article>`,
|
web.html`<article class="mod-container"></article>`,
|
||||||
web.render(
|
web.render(
|
||||||
@ -262,7 +256,7 @@ const _$modListCache = {},
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
modList: async (category, message = '') => {
|
modList: async (category, message = '') => {
|
||||||
if (!_$modListCache[category]) {
|
if (!$modLists[category]) {
|
||||||
const $search = web.html`<input type="search" class="search"
|
const $search = web.html`<input type="search" class="search"
|
||||||
placeholder="Search ('/' to focus)">`,
|
placeholder="Search ('/' to focus)">`,
|
||||||
$list = web.html`<div class="mods-list"></div>`,
|
$list = web.html`<div class="mods-list"></div>`,
|
||||||
@ -282,7 +276,7 @@ const _$modListCache = {},
|
|||||||
web.render($list, await generators.mod(mod));
|
web.render($list, await generators.mod(mod));
|
||||||
mod.tags.unshift(category);
|
mod.tags.unshift(category);
|
||||||
}
|
}
|
||||||
_$modListCache[category] = web.render(
|
$modLists[category] = web.render(
|
||||||
web.html`<div></div>`,
|
web.html`<div></div>`,
|
||||||
web.render(
|
web.render(
|
||||||
web.html`<label class="search-container"></label>`,
|
web.html`<label class="search-container"></label>`,
|
||||||
@ -293,10 +287,30 @@ const _$modListCache = {},
|
|||||||
$list
|
$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">
|
const $notionNavItem = web.html`<h1 class="nav-notion">
|
||||||
${(await fs.getText('media/colour.svg')).replace(
|
${(await fs.getText('media/colour.svg')).replace(
|
||||||
/width="\d+" height="\d+"/,
|
/width="\d+" height="\d+"/,
|
||||||
@ -338,51 +352,61 @@ function selectNavItem($item) {
|
|||||||
$item.className = 'nav-item-selected';
|
$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 () => {
|
router.addView('core', async () => {
|
||||||
web.empty($main);
|
web.empty($main);
|
||||||
selectNavItem($coreNavItem);
|
selectNavItem($coreNavItem);
|
||||||
return web.render(
|
return web.render($main, await generators.modList('core'));
|
||||||
$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.`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 () => {
|
router.addView('extensions', async () => {
|
||||||
web.empty($main);
|
web.empty($main);
|
||||||
selectNavItem($extensionsNavItem);
|
selectNavItem($extensionsNavItem);
|
||||||
return web.render(
|
return web.render($main, await generators.modList('extension'));
|
||||||
$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.`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 () => {
|
router.addView('themes', async () => {
|
||||||
web.empty($main);
|
web.empty($main);
|
||||||
selectNavItem($themesNavItem);
|
selectNavItem($themesNavItem);
|
||||||
return web.render(
|
return web.render($main, await generators.modList('theme'));
|
||||||
$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.`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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';
|
import { web } from '../../api/_.mjs';
|
||||||
|
|
||||||
let _defaultView = '';
|
const _queryListeners = new Set();
|
||||||
const _views = new Map();
|
|
||||||
|
|
||||||
export function addView(name, loadFunc) {
|
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) {
|
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) {
|
function router(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const anchor = event.path
|
const anchor = event.path
|
||||||
? event.path.find((anchor) => anchor.nodeName === 'A')
|
? event.path.find((anchor) => anchor.nodeName === 'A')
|
||||||
: event.target;
|
: event.target;
|
||||||
if (location.search !== anchor.getAttribute('href')) {
|
updateQuery(anchor.getAttribute('href'));
|
||||||
window.history.pushState(null, null, anchor.href);
|
}
|
||||||
loadView();
|
|
||||||
|
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) {
|
window.addEventListener('popstate', triggerQueryListeners);
|
||||||
if (defaultView) _defaultView = defaultView;
|
|
||||||
if (!_defaultView) throw new Error('no view root set.');
|
|
||||||
|
|
||||||
const query = web.queryParams(),
|
web.addDocumentObserver(
|
||||||
fallbackView = () => {
|
(mutation) => {
|
||||||
window.history.replaceState(null, null, `?view=${_defaultView}`);
|
mutation.target.querySelectorAll('a[href^="?"]').forEach((a) => {
|
||||||
return loadView();
|
a.removeEventListener('click', router);
|
||||||
};
|
a.addEventListener('click', router);
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
documentObserver = new MutationObserver((list, observer) => {
|
['a[href^="?"]']
|
||||||
if (!documentObserverEvents.length)
|
);
|
||||||
requestIdleCallback(() => handleDocumentMutations(documentObserverEvents));
|
|
||||||
documentObserverEvents.push(...list);
|
|
||||||
});
|
|
||||||
documentObserver.observe(document.body, {
|
|
||||||
childList: true,
|
|
||||||
subtree: true,
|
|
||||||
attributes: true,
|
|
||||||
});
|
|
||||||
|
Loading…
Reference in New Issue
Block a user