add menu filters + unspaghetti menu code

This commit is contained in:
dragonwocky 2021-05-11 23:29:12 +10:00
parent 4e3f921ee3
commit a55482d62d
13 changed files with 521 additions and 494 deletions

View File

@ -0,0 +1,2 @@
<!-- https://iconmonstr.com/party-5-svg/ -->
<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd"><path fill="currentColor" d="M4.823 21.933l2.734-1.171-3.241-8.847-1.561 4.372 2.068 5.646zm-2.594-4.174l-2.229 6.241 3.903-1.672-1.674-4.569zm6.248 2.609l2.934-1.258-3.482-9.141-2.215-1.969-.872 2.443 3.635 9.925zm7.523-3.224l-6.453-5.736 2.785 7.308 3.668-1.572zm-.826-5.003l2.201-1.445c.23-.152.295-.462.143-.693-.152-.232-.463-.295-.692-.143l-2.201 1.445c-.231.151-.295.461-.144.692.096.147.256.226.418.226.095 0 .19-.026.275-.082m-2.993-4.312l1.112-2.388c.117-.25.008-.548-.242-.664-.251-.116-.548-.009-.665.242l-1.111 2.388c-.117.25-.008.547.242.664l.211.047c.189 0 .368-.107.453-.289m-2.627.709c1.539-2.963 1.644-5.73.314-8.222-.09-.169-.263-.265-.442-.265-.37 0-.621.398-.44.736 1.166 2.184 1.058 4.637-.32 7.29-.127.245-.031.547.214.674.073.038.152.057.23.057.18 0 .355-.099.444-.27m6.505 6.095c2.017-1.434 4.463-1.64 7.272-.613.327.119.672-.123.672-.47 0-.203-.125-.395-.328-.47-3.136-1.147-5.894-.9-8.196.738-.224.16-.277.472-.117.698.098.136.251.209.407.209.101 0 .202-.03.29-.092m3.757-6.757l-1.697.014.938 1.415-.511 1.618 1.635-.455 1.381.986.073-1.696 1.365-1.009-1.591-.592-.538-1.61-1.055 1.329zm-7.307 3.624c.276-.009.492-.24.483-.517-.056-1.627.36-1.937 1.377-2.051 1.689-.191 1.785-1.312 1.842-1.982.053-.637.071-.851.773-.903.63-.046 1.331-.16 1.76-.659.461-.538.466-1.358.402-2.164-.021-.276-.266-.478-.537-.459-.275.021-.481.262-.459.537.062.787.011 1.23-.165 1.434-.149.174-.48.271-1.074.314-1.553.114-1.644 1.179-1.697 1.816-.057.668-.082.973-.956 1.071-2.075.234-2.315 1.619-2.266 3.08.01.27.231.483.5.483h.017m7.842-8.675c0 1.006.818 1.824 1.825 1.824 1.006 0 1.824-.818 1.824-1.824 0-1.007-.818-1.825-1.824-1.825-1.007 0-1.825.818-1.825 1.825m-6.623-2.841c1.104 0 2 .897 2 2 0 1.104-.896 2-2 2-1.103 0-2-.896-2-2 0-1.103.897-2 2-2"/></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -5,12 +5,13 @@
- improved: split the core mod into the theming & menu mods.
- improved: new larger menu layout, with individual mod pages.
- improved: merged bracketed-links into tweaks.
- improved: replaced confusing all-tag filters with themes/extensions/enabled/disabled filters.
- removed: integrated scrollbar tweak (notion now includes by default).
- removed: js insert. css insert moved to tweaks mod.
- ported: tweaks, bypass-preview.
#### todo
- tag sort
- documentation e.g. \_file
- complete/bugfix theming variables
- color pickers

View File

@ -4,6 +4,8 @@
* (https://notion-enhancer.github.io/) under the MIT license
*/
'use strict';
import { web } from '../../api.js';
web.whenReady().then(async () => {
@ -23,7 +25,7 @@ function getCurrentPage() {
return { type: 'page', id: location.pathname.split(/(-|\/)/g).reverse()[0] };
}
let lastPage = getCurrentPage();
web.observeDocument((event) => {
web.addDocumentObserver((event) => {
const currentPage = getCurrentPage();
if (currentPage.id !== lastPage.id || currentPage.type !== lastPage.type) {
const openAsPage = document.querySelector(

View File

@ -1,7 +1,7 @@
{
"name": "bypass-preview",
"id": "cb6fd684-f113-4a7a-9423-8f0f0cff069f",
"description": "go straight to the normal full view when opening a page..",
"description": "go straight to the normal full view when opening a page.",
"version": "0.2.0",
"tags": ["extension", "automation"],
"authors": [

View File

@ -1 +1,3 @@
# menu
[theming mod link test](?view=mod&id=0f0bf8b6-eae6-4273-b307-8fc43f2ee082)

View File

@ -25,6 +25,7 @@ web.whenReady([sidebarSelector]).then(async () => {
list: await fs.getJSON('https://notion-enhancer.github.io/notifications.json'),
dismissed: await storage.get(_id, 'notifications', []),
};
console.log($enhancerSidebarElement);
notifications.waiting = notifications.list.filter(
({ id }) => !notifications.dismissed.includes(id)
);
@ -48,4 +49,4 @@ web.whenReady([sidebarSelector]).then(async () => {
setTheme();
document.querySelector(sidebarSelector).appendChild($enhancerSidebarElement);
});
web.hotkeyListener(await storage.get(_id, 'hotkey.focustoggle'), env.openEnhancerMenu);
web.addHotkeyListener(await storage.get(_id, 'hotkey.focustoggle'), env.openEnhancerMenu);

View File

@ -11,6 +11,7 @@
-webkit-text-size-adjust: 100%;
font-size: inherit;
font-family: inherit;
fill: currentColor;
}
html {
@ -43,7 +44,7 @@ header > * {
margin: 0 1.25rem 0.1em 0;
font-size: var(--theme--font_heading1-size);
}
header h1 a:not([data-view-active]) {
header h1 a {
text-decoration: none;
}
header h1 img {
@ -80,7 +81,7 @@ main {
main {
grid-template-columns: 1fr 1fr;
}
[data-view='mod'] main > .documentation--buttons {
main > .action--buttons {
grid-column: span 2;
}
[data-view='mod'] main .library--card,
@ -97,7 +98,7 @@ main {
main {
grid-template-columns: 1fr 1fr 1fr;
}
[data-view='mod'] main > .documentation--buttons {
main > .action--buttons {
grid-column: span 3;
}
[data-view='mod'] main > .documentation--body {
@ -108,7 +109,7 @@ main {
main {
grid-template-columns: 1fr 1fr 1fr 1fr;
}
[data-view='mod'] main > .documentation--buttons {
main > .action--buttons {
grid-column: span 4;
}
[data-view='mod'] main > .documentation--body {
@ -119,7 +120,7 @@ main {
main {
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
}
[data-view='mod'] main > .documentation--buttons {
main > .action--buttons {
grid-column: span 5;
}
[data-view='mod'] main > .documentation--body {
@ -136,7 +137,7 @@ main article img {
max-width: 100%;
}
.documentation--buttons,
.action--buttons,
.library--expand {
margin: 0;
display: flex;
@ -146,14 +147,14 @@ main article img {
.library--expand a {
margin-left: auto;
}
.documentation--buttons a,
.action--buttons a,
.library--expand a {
border-radius: 3px;
padding: 0.35rem 0.45rem;
text-decoration: none;
display: flex;
}
.documentation--buttons .documentation--reload {
.action--buttons .action--alert {
cursor: pointer;
border-radius: 3px;
padding: 0.35rem 0.45rem;
@ -164,31 +165,32 @@ main article img {
opacity: 0;
transition: opacity 200ms ease-in-out;
}
.documentation--buttons .documentation--reload[data-triggered] {
.action--buttons .action--alert[data-triggered] {
pointer-events: all;
opacity: 1;
}
.documentation--buttons .documentation--reload[data-triggered]:hover {
.action--buttons .action--alert[data-triggered]:hover {
background: none;
color: var(--theme--block_grey-text);
box-shadow: var(--theme--block_grey) 0px 0px 0px 1px inset;
}
.documentation--buttons span,
.action--buttons span,
.library--expand span {
color: var(--theme--text_property);
}
.documentation--buttons a:hover,
.action--buttons a:hover,
.action--buttons a.action--active,
.library--expand a:hover {
background: var(--theme--button-hover);
}
.documentation--buttons svg,
.action--buttons svg,
.library--expand svg {
width: 1em;
height: 1em;
padding-top: 2px;
margin-right: 0.3rem;
}
.documentation--buttons svg *,
.action--buttons svg *,
.library--expand svg * {
fill: var(--theme--text_property);
}
@ -269,7 +271,7 @@ label p > span:not([class]),
label > span:not([class]) {
font-size: 1rem;
}
label [data-icon='fa/question-circle'] {
label [data-icon='fa/solid/question-circle'] {
height: var(--theme--font_ui_small-size);
width: var(--theme--font_ui_small-size);
margin-left: 0.25em;
@ -306,7 +308,7 @@ label [data-icon='fa/question-circle'] {
height: 0.8rem;
width: 0.8rem;
left: 0.325rem;
top: 0.225rem;
top: 0.2rem;
position: absolute;
border-radius: 100%;
background: var(--theme--toggle_dot);
@ -419,22 +421,7 @@ label [data-icon='fa/question-circle'] {
overflow-x: auto;
}
.tooltip {
position: absolute;
background: var(--theme--tooltip);
color: var(--theme--tooltip-text);
font-size: var(--theme--font_ui_small-size);
padding: 0.15rem 0.4rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
border-radius: 3px;
max-width: 20rem;
display: none;
}
.tooltip p {
margin: 0.25rem 0;
}
.notification--list {
.notifications {
position: absolute;
bottom: 1.5rem;
right: 1.5rem;
@ -459,7 +446,7 @@ label [data-icon='fa/question-circle'] {
transform-origin: 100% 100%;
opacity: 0;
}
.notification svg {
.notification :not(.notification--dismiss) > svg {
height: 1.5rem;
width: 1.5rem;
margin-top: 0.25rem;
@ -478,27 +465,20 @@ label [data-icon='fa/question-circle'] {
right: 0.75rem;
background: none;
border: none;
padding: 0.25rem 0.35rem;
font-size: var(--theme--font_body-size);
padding: 0.15rem 0 0.15rem 0.5rem;
width: var(--theme--font_body-size);
color: currentColor;
cursor: pointer;
transition: opacity 200ms ease-in-out;
opacity: 0;
}
.notification .notification--dismiss svg {
width: 100%;
}
.notification:hover .notification--dismiss,
.notification:focus-within .notification--dismiss {
opacity: 1;
}
.notification.celebration,
.notification.information {
background: var(--theme--block_blue);
color: var(--theme--block_blue-text);
}
.notification.warning,
.notification.danger {
background: var(--theme--block_red);
color: var(--theme--block_red-text);
}
::-webkit-scrollbar {
background: transparent;

View File

@ -8,21 +8,23 @@
<body data-view>
<header>
<h1>
<img data-view-target="notion" alt="" width="24" src="../../icons/colour.svg" />
<a href="?view=library" data-view-target="library">library</a>
<img data-notion alt="" width="24" src="../../icons/colour.svg" />
<a href="?view=library">library</a>
</h1>
<h1>
<i data-icon="fa/info"></i><a href="https://notion-enhancer.github.io/">website</a>
<i data-icon="fa/solid/info"></i>
<a href="https://notion-enhancer.github.io/">website</a>
</h1>
<h1>
<span data-icon="fa/code"></span
><a href="https://github.com/notion-enhancer/extension">source code</a>
<i data-icon="fa/solid/code"></i>
<a href="https://github.com/notion-enhancer/extension">source code</a>
</h1>
<h1>
<i data-icon="fa/brands/discord"></i>
<a href="https://discord.gg/sFWPXtA">support</a>
</h1>
<h1><i data-icon="fa/discord"></i><a href="https://discord.gg/sFWPXtA">support</a></h1>
</header>
<main></main>
<section class="tooltip"></section>
<footer class="notification--list"></footer>
<script src="./menu.js" type="module"></script>
</body>
</html>

View File

@ -9,112 +9,124 @@
const _id = 'a6621988-551d-495a-97d8-3c568bca2e9e';
import { env, storage, web, fmt, fs, registry } from '../../api.js';
for (const mod of await registry.get((mod) => registry.enabled(mod.id))) {
for (const mod of await registry.get((mod) => registry.isEnabled(mod.id))) {
for (const sheet of mod.css?.menu || []) {
web.loadStyleset(`repo/${mod._dir}/${sheet}`);
}
}
async function loadTheme() {
document.documentElement.className = `notion-${
(await storage.get(_id, 'theme')) || 'dark'
}-theme`;
}
window.addEventListener('focus', loadTheme);
loadTheme();
document
.querySelector('img[data-view-target="notion"]')
.addEventListener('click', env.focusNotion);
web.hotkeyListener(await storage.get(_id, 'hotkey.focustoggle'), env.focusNotion);
document.querySelector('img[data-notion]').addEventListener('click', env.focusNotion);
web.addHotkeyListener(await storage.get(_id, 'hotkey.focustoggle'), env.focusNotion);
web.addDocumentObserver(web.loadIcons);
const tooltips = {
$el: document.querySelector('.tooltip'),
add($parent, selector, text) {
text = fmt.md.render(text);
$parent.addEventListener('mouseover', (event) => {
if (event.target.matches(selector) || event.target.matches(`${selector} *`)) {
this.$el.innerHTML = text;
this.$el.style.display = 'block';
}
});
$parent.addEventListener('mousemove', (event) => {
this.$el.style.top = event.clientY - this.$el.clientHeight + 'px';
this.$el.style.left =
event.clientX < window.innerWidth / 2 ? event.clientX + 20 + 'px' : '';
});
$parent.addEventListener('mouseout', (event) => {
if (event.target.matches(selector) || event.target.matches(`${selector} *`)) {
this.$el.style.display = '';
}
const notifications = {
$el: web.createElement(web.html`<footer class="notifications"></footer>`),
push({ heading, message, icon, color }, onDismiss = () => {}) {
const $notif = web.createElement(web.html`
<div role="alert" class="notification" style="
background: var(--theme--block_${web.escapeHtml(color)});
color: var(--theme--block_${web.escapeHtml(color)}-text);
">
<div><i data-icon="${web.escapeHtml(icon)}"></i></div>
<div>
<h3>${web.escapeHtml(heading)}</h3>
<p>${fmt.md.renderInline(message)}</p>
</div>
<button class="notification--dismiss"><i data-icon="fa/solid/times"></i></button>
</div>`);
this.$el.append($notif);
setTimeout(() => {
$notif.style.opacity = 1;
}, 100);
$notif.querySelector('.notification--dismiss').addEventListener('click', (event) => {
$notif.style.opacity = 0;
$notif.style.transform = 'scaleY(0)';
$notif.style.marginTop = `-${
$notif.offsetHeight / parseFloat(getComputedStyle(document.documentElement).fontSize)
}rem`;
setTimeout(() => $notif.remove(), 400);
onDismiss();
});
return $notif;
},
};
document.body.append(notifications.$el);
for (const error of await registry.errors()) {
notifications.push({
heading: `error: ${error.source}`,
message: error.message,
color: 'red',
icon: 'fa/solid/exclamation-triangle',
});
}
for (const notification of await (async () => {
const dismissed = await storage.get('_notifications', 'dismissed', []);
return (await fs.getJSON('https://notion-enhancer.github.io/notifications.json'))
.sort((a, b) => b.id - a.id)
.filter(({ id }) => !dismissed.includes(id));
})()) {
if (
(!notification.versions || notification.versions.includes(env.version)) &&
(!notification.environments || notification.environments.includes(env.name))
) {
notifications.push(notification, async () => {
const dismissed = await storage.get('_notifications', 'dismissed', []);
storage.set('_notifications', 'dismissed', [
...new Set([...dismissed, notification.id]),
]);
});
}
}
import * as router from './router.js';
const components = {};
components.card = {
preview: ({ preview = '' }) =>
preview
? web.createElement(web.html`<img
alt=""
class="library--preview"
src="${web.escapeHtml(preview)}"
/>`)
: '',
async name({ name, id, version, tags }) {
if (registry.CORE.includes(id))
return web.createElement(web.html`<div class="library--title"><h2>
<span>
${web.escapeHtml(name)}
<span class="library--version">v${web.escapeHtml(version)}</span>
</span>
</h2></div>`);
const $el = web.createElement(web.html`<label
for="enable--${web.escapeHtml(id)}"
components.card = async (mod) => {
const $card = web.createElement(web.html`
<article class="library--card" data-mod='${mod.id}'>
${
mod.preview
? web.html`<img
alt=""
class="library--preview"
src="${web.escapeHtml(mod.preview)}"
/>`
: ''
}
<div>
<label
for="enable--${web.escapeHtml(mod.id)}"
class="library--title library--toggle_label"
>
<input type="checkbox" id="enable--${web.escapeHtml(id)}"
${(await storage.get('_enabled', id, false)) ? 'checked' : ''}/>
>
<input type="checkbox" id="enable--${web.escapeHtml(mod.id)}"
${(await registry.isEnabled(mod.id)) ? 'checked' : ''}/>
<h2>
<span>
${web.escapeHtml(name)}
<span class="library--version">v${web.escapeHtml(version)}</span>
${web.escapeHtml(mod.name)}
<span class="library--version">v${web.escapeHtml(mod.version)}</span>
</span>
<span class="library--toggle"></span>
</h2>
</label>`);
$el.addEventListener('change', async (event) => {
storage.set('_enabled', id, event.target.checked);
if (
event.target.checked &&
tags.includes('theme') &&
(await storage.get(_id, 'themes.autoresolve', true))
) {
const themes = await registry.get(
(mod) =>
mod.tags.includes('theme') &&
mod.id !== id &&
((mod.tags.includes('dark') && tags.includes('dark')) ||
(mod.tags.includes('light') && tags.includes('light')))
);
for (const theme of themes) {
if (document.body.dataset.view === 'library') {
const $toggle = document.getElementById(`enable--${theme.id}`);
if ($toggle.checked) $toggle.click();
} else storage.set('_enabled', theme.id, false);
${
registry.CORE.includes(mod.id) ? '' : web.html`<span class="library--toggle"></span>`
}
}
});
return $el;
},
tags: ({ tags = [] }) =>
web.createElement(web.html`<ul class="library--tags">
${tags.map((tag) => web.html`<li>#${web.escapeHtml(tag)}</li>`).join('')}
</ul>`),
description: ({ description }) =>
web.createElement(
web.html`<p class="library--description markdown">${fmt.md.renderInline(
description
)}</p>`
),
authors: ({ authors }) =>
web.createElement(web.html`<ul class="library--authors">
${authors
</h2>
</label>
<ul class="library--tags">
${mod.tags.map((tag) => web.html`<li>#${web.escapeHtml(tag)}</li>`).join('')}
</ul>
<p class="library--description markdown">${fmt.md.renderInline(mod.description)}</p>
<ul class="library--authors">
${mod.authors
.map(
(author) =>
web.html`<li>
web.html`
<li>
<a href="${web.escapeHtml(author.url)}">
<img alt="" src="${web.escapeHtml(author.icon)}" />
<span>${web.escapeHtml(author.name)}</span>
@ -122,83 +134,77 @@ components.card = {
</li>`
)
.join('')}
</ul>`),
expand: async ({ id }) =>
web.createElement(
web.html`<p class="library--expand">
<a href="?view=mod&id=${web.escapeHtml(id)}">
<span><i data-icon="fa/long-arrow-alt-right"></i></span>
<span>settings & documentation</span>
</a>
</p>`
),
async _generate(mod) {
const card = web.createElement(web.html`<article class="library--card"></article>`),
body = web.createElement(web.html`<div></div>`);
card.append(this.preview(mod));
body.append(await this.name(mod));
body.append(this.tags(mod));
body.append(this.description(mod));
body.append(this.authors(mod));
body.append(await this.expand(mod));
card.append(body);
return card;
},
</ul>
<p class="library--expand">
<a href="?view=mod&id=${web.escapeHtml(mod.id)}">
<span><i data-icon="fa/solid/long-arrow-alt-right"></i></span>
<span>settings & documentation</span>
</a>
</p>
</div>
</article>`);
$card.querySelector('.library--title input').addEventListener('change', async (event) => {
storage.set('_mods', mod.id, event.target.checked);
});
return $card;
};
components.options = {
async toggle(id, { key, label, tooltip }) {
const state = await storage.get(id, key),
opt = web.createElement(web.html`<label
for="toggle--${web.escapeHtml(`${id}.${key}`)}"
class="library--toggle_label"
opt = web.createElement(web.html`
<label
for="toggle--${web.escapeHtml(`${id}.${key}`)}"
class="library--toggle_label"
>
<input type="checkbox" id="toggle--${web.escapeHtml(`${id}.${key}`)}"
${state ? 'checked' : ''}/>
<p>
<span data-tooltip>${web.escapeHtml(label)}
${tooltip ? web.html`<i data-icon="fa/question-circle"></i>` : ''}</span>
<span class="library--toggle"></span>
</p>
<input type="checkbox" id="toggle--${web.escapeHtml(`${id}.${key}`)}"
${state ? 'checked' : ''}/>
<p>
<span data-tooltip>${web.escapeHtml(label)}
${tooltip ? web.html`<i data-icon="fa/solid/question-circle"></i>` : ''}</span>
<span class="library--toggle"></span>
</p>
</label>`);
opt.addEventListener('change', (event) => storage.set(id, key, event.target.checked));
if (tooltip) tooltips.add(opt, '[data-tooltip]', tooltip);
if (tooltip) web.addTooltip(opt.querySelector('[data-tooltip]'), tooltip);
return opt;
},
async select(id, { key, label, tooltip, values }) {
const state = await storage.get(id, key),
opt = web.createElement(web.html`<label
opt = web.createElement(web.html`
<label
for="select--${web.escapeHtml(`${id}.${key}`)}"
class="library--select_label"
>
<p><span data-tooltip>${web.escapeHtml(label)}
${tooltip ? web.html`<i data-icon="fa/question-circle"></i>` : ''}</span></p>
<p class="library--select">
<span><i data-icon="fa/caret-down"></i></span>
<select id="select--${web.escapeHtml(`${id}.${key}`)}">
${values.map(
(value) =>
web.html`<option value="${web.escapeHtml(value)}"
${value === state ? 'selected' : ''}>
${web.escapeHtml(value)}</option>`
)}
</select>
</p>
</label>`);
<p><span data-tooltip>${web.escapeHtml(label)}
${tooltip ? web.html`<i data-icon="fa/solid/question-circle"></i>` : ''}</span></p>
<p class="library--select">
<span><i data-icon="fa/solid/caret-down"></i></span>
<select id="select--${web.escapeHtml(`${id}.${key}`)}">
${values.map(
(value) =>
web.html`<option value="${web.escapeHtml(value)}"
${value === state ? 'selected' : ''}>
${web.escapeHtml(value)}</option>`
)}
</select>
</p>
</label>`);
opt.addEventListener('change', (event) => storage.set(id, key, event.target.value));
if (tooltip) tooltips.add(opt, '[data-tooltip]', tooltip);
if (tooltip) web.addTooltip(opt.querySelector('[data-tooltip]'), tooltip);
return opt;
},
async text(id, { key, label, tooltip }) {
const state = await storage.get(id, key),
opt = web.createElement(web.html`<label
for="text--${web.escapeHtml(`${id}.${key}`)}"
class="library--text_label"
opt = web.createElement(web.html`
<label
for="text--${web.escapeHtml(`${id}.${key}`)}"
class="library--text_label"
>
<p><span data-tooltip>${web.escapeHtml(label)}
${tooltip ? web.html`<i data-icon="fa/question-circle"></i>` : ''}</span></p>
<textarea id="text--${web.escapeHtml(`${id}.${key}`)}"
rows="1">${web.escapeHtml(state)}</textarea>
</label>`);
<p><span data-tooltip>${web.escapeHtml(label)}
${tooltip ? web.html`<i data-icon="fa/solid/question-circle"></i>` : ''}</span></p>
<textarea id="text--${web.escapeHtml(`${id}.${key}`)}"
rows="1">${web.escapeHtml(state)}</textarea>
</label>`);
opt.querySelector('textarea').addEventListener('input', (event) => {
event.target.style.removeProperty('--txt--scroll-height');
event.target.style.setProperty(
@ -207,52 +213,54 @@ components.options = {
);
});
opt.addEventListener('change', (event) => storage.set(id, key, event.target.value));
if (tooltip) tooltips.add(opt, '[data-tooltip]', tooltip);
if (tooltip) web.addTooltip(opt.querySelector('[data-tooltip]'), tooltip);
return opt;
},
async number(id, { key, label, tooltip }) {
const state = await storage.get(id, key),
opt = web.createElement(web.html`<label
for="number--${web.escapeHtml(`${id}.${key}`)}"
class="library--number_label"
opt = web.createElement(web.html`
<label
for="number--${web.escapeHtml(`${id}.${key}`)}"
class="library--number_label"
>
<p><span data-tooltip>${web.escapeHtml(label)}
${tooltip ? web.html`<i data-icon="fa/question-circle"></i>` : ''}</span></p>
<input id="number--${web.escapeHtml(`${id}.${key}`)}"
type="number" value="${web.escapeHtml(state.toString())}"/>
</label>`);
<p><span data-tooltip>${web.escapeHtml(label)}
${tooltip ? web.html`<i data-icon="fa/solid/question-circle"></i>` : ''}</span></p>
<input id="number--${web.escapeHtml(`${id}.${key}`)}"
type="number" value="${web.escapeHtml(state.toString())}"/>
</label>`);
opt.addEventListener('change', (event) => storage.set(id, key, event.target.value));
if (tooltip) tooltips.add(opt, '[data-tooltip]', tooltip);
if (tooltip) web.addTooltip(opt.querySelector('[data-tooltip]'), tooltip);
return opt;
},
async file(id, { key, label, tooltip, extensions }) {
const state = await storage.get(id, key),
opt = web.createElement(web.html`<label
for="file--${web.escapeHtml(`${id}.${key}`)}"
class="library--file_label"
opt = web.createElement(web.html`
<label
for="file--${web.escapeHtml(`${id}.${key}`)}"
class="library--file_label"
>
<input
type="file"
id="file--${web.escapeHtml(`${id}.${key}`)}"
${web.escapeHtml(
extensions && extensions.length
? ` accept=${web.escapeHtml(extensions.join(','))}`
: ''
)}
/>
<p class="library--file_title"><span data-tooltip>${web.escapeHtml(label)}
<i data-icon="fa/question-circle"></i></span>
<span class="library--file_remove"><i data-icon="fa/minus"></i></span></p>
<p class="library--file">
<span><i data-icon="fa/file"></i></span>
<span class="library--file_path">${web.escapeHtml(state || 'choose file...')}</span>
</p>
</label>`);
<input
type="file"
id="file--${web.escapeHtml(`${id}.${key}`)}"
${web.escapeHtml(
extensions && extensions.length
? ` accept=${web.escapeHtml(extensions.join(','))}`
: ''
)}
/>
<p class="library--file_title"><span data-tooltip>${web.escapeHtml(label)}
<i data-icon="fa/solid/question-circle"></i></span>
<span class="library--file_remove"><i data-icon="fa/solid/minus"></i></span></p>
<p class="library--file">
<span><i data-icon="fa/solid/file"></i></span>
<span class="library--file_path">${web.escapeHtml(state || 'choose file...')}</span>
</p>
</label>`);
opt.addEventListener('change', (event) => {
const file = event.target.files[0],
reader = new FileReader();
opt.querySelector('.library--file_path').innerText = file.name;
reader.onload = (progress) => {
opt.querySelector('.library--file_path').innerText = file.name;
reader.onload = (progress) => {
storage.set(id, key, file.name);
storage.set(id, `_file.${key}`, progress.currentTarget.result);
};
@ -272,281 +280,190 @@ components.options = {
opt.addEventListener('click', (event) => {
document.documentElement.scrollTop = 0;
});
tooltips.add(
opt,
'[data-tooltip]',
`${
tooltip ? `${tooltip}\n\n` : ''
}**warning:** browser extensions do not have true filesystem access,
so file content is only saved on selection. re-select files to apply edits.`
web.addTooltip(
opt.querySelector('[data-tooltip]'),
`${tooltip ? `${tooltip}\n\n` : ''}**warning:** ${
'browser extensions do not have true filesystem access, ' +
'so file content is only saved on selection. re-select files to apply edits.'
}`
);
return opt;
},
async _generate(mod) {
const card = await components.card._generate(mod);
card.querySelector('.library--expand').remove();
if (mod.options && mod.options.length) {
const options = web.createElement(web.html`<div class="library--options"></div>`),
inputs = await Promise.all(
mod.options
.filter((opt) => !opt.environments || opt.environments.includes(env.name))
.map((opt) => this[opt.type](mod.id, opt))
);
inputs.forEach((opt) => options.append(opt));
card.append(options);
}
return card;
},
};
components.documentation = {
const actionButtons = {
_reloadTriggered: false,
buttons({ _dir }) {
const $el = web.createElement(web.html`<p class="documentation--buttons">
<a href="?view=library">
<span><i data-icon="fa/long-arrow-alt-left"></i></span>
<span>back to library</span>
</a>
<a
href="https://github.com/notion-enhancer/extension/tree/main/repo/${encodeURIComponent(
_dir
)}"
>
<span><i data-icon="fa/code"></i></span>
<span>view source code</span>
</a>
<button class="documentation--reload"${this._reloadTriggered ? ' data-triggered' : ''}>
<span><i data-icon="fa/redo"></i></span>
async reload($fragment = document) {
let $reload = $fragment.querySelector('[data-reload]');
if (!$reload) {
$reload = web.createElement(web.html`
<button class="action--alert" data-reload>
<span><i data-icon="fa/solid/redo"></i></span>
<span>reload tabs to apply changes</span>
</button>
</p>`);
storage.onChange(() => {
const $reload = $el.querySelector('.documentation--reload');
if (document.body.contains($el) && !$reload.dataset.triggered) {
$reload.dataset.triggered = true;
this._reloadTriggered = true;
}
});
$el.querySelector('.documentation--reload').addEventListener('click', env.reloadTabs);
return $el;
</button>`);
$reload.addEventListener('click', env.reloadTabs);
}
if (this._reloadTriggered) {
$fragment.querySelector('.action--buttons').append($reload);
await new Promise((res, rej) => requestAnimationFrame(res));
$reload.dataset.triggered = true;
}
},
readme: async (mod) => {
const readme = web.createElement(web.html`<article class="documentation--body markdown">
${
(await fs.isFile(`repo/${mod._dir}/README.md`))
? fmt.md.render(await fs.getText(`repo/${mod._dir}/README.md`))
: ''
}
</article>`);
fmt.Prism.highlightAllUnder(readme);
return readme;
async clearFilters($fragment = document) {
let $clearFilters = $fragment.querySelector('[data-clear-filters]');
if (!$clearFilters) {
$clearFilters = web.createElement(web.html`
<a class="action--alert" href="?view=library" data-clear-filters>
<span><i data-icon="fa/solid/times"></i></span>
<span>clear filters</span>
</a>`);
}
const search = router.getSearch();
if (search.get('tag') || search.has('enabled') || search.has('disabled')) {
$fragment.querySelector('.action--buttons').append($clearFilters);
await new Promise((res, rej) => requestAnimationFrame(res));
$clearFilters.dataset.triggered = true;
} else $clearFilters.remove();
},
};
const views = {
$container: document.querySelector('main'),
_router(event) {
event.preventDefault();
let anchor,
i = 0;
do {
anchor = event.path[i];
i++;
} while (anchor.nodeName !== 'A');
if (location.search !== anchor.getAttribute('href')) {
window.history.pushState(
{ search: anchor.getAttribute('href'), hash: '' },
'',
anchor.href
);
this._load();
}
},
_navigator(event) {
event.preventDefault();
const hash = event.target.getAttribute('href').slice(1);
document.getElementById(hash).scrollIntoView(true);
document.documentElement.scrollTop = 0;
history.replaceState({ search: location.search, hash }, null, `#${hash}`);
},
_reset() {
document
.querySelectorAll('a[href^="?"]')
.forEach((a) => a.removeEventListener('click', this._router));
document
.querySelectorAll('a[href^="#"]')
.forEach((a) => a.removeEventListener('click', this._navigator));
this.$container.style.opacity = 0;
return new Promise((res, rej) => {
setTimeout(() => {
this.$container.innerHTML = '';
this.$container.style.opacity = '';
document.body.dataset.view = '';
document
.querySelector('[data-view-target][data-view-active]')
?.removeAttribute('data-view-active');
res();
}, 200);
});
},
async _load() {
await this._reset();
storage.addChangeListener(async (event) => {
actionButtons._reloadTriggered = true;
actionButtons.reload();
router.load();
const search = new Map(
location.search
.slice(1)
.split('&')
.map((query) => query.split('='))
);
switch (search.get('view')) {
case 'mod':
const mod = (await registry.get()).find((mod) => mod.id === search.get('id'));
if (mod) {
await this.mod(mod);
break;
}
case 'library':
await this.library();
break;
default:
window.history.replaceState(
{ search: '?view=library', hash: '' },
null,
'?view=library'
);
return this._load();
if (event.namespace === '_mods' && event.new === true) {
const enabledTheme = (await registry.get()).find((mod) => mod.id === event.key);
if (
enabledTheme.tags.includes('theme') &&
(await storage.get(_id, 'themes.autoresolve', true))
) {
for (const theme of await registry.get(
(mod) =>
mod.tags.includes('theme') &&
mod.id !== enabledTheme.id &&
((mod.tags.includes('dark') && enabledTheme.tags.includes('dark')) ||
(mod.tags.includes('light') && enabledTheme.tags.includes('light')))
)) {
if (document.body.dataset.view === 'library') {
const $toggle = document.getElementById(`enable--${theme.id}`);
if ($toggle.checked) $toggle.click();
} else storage.set('_mods', theme.id, false);
}
}
}
});
setTimeout(() => {
document.getElementById(location.hash.slice(1))?.scrollIntoView(true);
document.documentElement.scrollTop = 0;
}, 50);
document
.querySelectorAll('img')
.forEach((img) => (img.onerror = (event) => event.target.remove()));
document
.querySelectorAll('a[href^="?"]')
.forEach((a) => a.addEventListener('click', this._router));
document
.querySelectorAll('a[href^="#"]')
.forEach((a) => a.addEventListener('click', this._navigator));
document.querySelectorAll('[data-icon]').forEach((icon) =>
fs.getText(`icons/${icon.dataset.icon}.svg`).then((svg) => {
svg = web.createElement(svg);
for (const attr of icon.attributes) {
svg.setAttribute(attr.name, attr.value);
}
icon.replaceWith(svg);
})
);
},
async mod(mod) {
document.body.dataset.view = 'mod';
document.querySelector('header [data-view-target="library"]').dataset.active = true;
this.$container.append(components.documentation.buttons(mod));
this.$container.append(await components.options._generate(mod));
this.$container.append(await components.documentation.readme(mod));
},
async library() {
document.body.dataset.view = 'library';
document.querySelector('header [data-view-target="library"]').dataset.active = true;
router.addView(
'library',
async () => {
const $fragment = web.createFragment(web.html`
<p class="action--buttons">
<a href="?view=library&tag=theme">
<span><i data-icon="fa/solid/palette"></i></span>
<span>themes</span>
</a>
<a href="?view=library&tag=extension">
<span><i data-icon="fa/solid/plus"></i></span>
<span>extensions</span>
</a>
<a href="?view=library&enabled">
<span><i data-icon="fa/solid/toggle-on"></i></span>
<span>enabled</span>
</a>
<a href="?view=library&disabled">
<span><i data-icon="fa/solid/toggle-off"></i></span>
<span>disabled</span>
</a>
</p>`);
for (const mod of await registry.get(
(mod) => !mod.environments || mod.environments.includes(env.name)
)) {
this.$container.append(await components.card._generate(mod));
$fragment.append(await components.card(mod));
}
actionButtons.reload($fragment);
actionButtons.clearFilters($fragment);
return $fragment;
},
};
views._router = views._router.bind(views);
views._navigator = views._navigator.bind(views);
views._load();
window.addEventListener('popstate', (event) => {
if (event.state) views._load();
});
const notifications = {
$list: document.querySelector('.notification--list'),
push({ heading, message = '', type = 'information' }, onDismiss = () => {}) {
let svg = '',
className = 'notification';
switch (type) {
case 'celebration':
svg = web.html`<i data-icon="monster/party"></i>`;
className += ' celebration';
break;
case 'information':
svg = web.html`<i data-icon="fa/info"></i>`;
className += ' information';
break;
case 'warning':
svg = web.html`<i data-icon="fa/exclamation-triangle"></i>`;
className += ' warning';
break;
async (search = router.getSearch()) => {
for (const [filter, active] of [
['tag=theme', search.get('tag') === 'theme'],
['tag=extension', search.get('tag') === 'extension'],
['enabled', search.has('enabled')],
['disabled', search.has('disabled')],
]) {
document
.querySelector(`.action--buttons > [href="?view=library&${filter}"]`)
.classList[active ? 'add' : 'remove']('action--active');
}
const $notif = web.createElement(web.html`<div role="alert" class="${className}">
<div>${svg}</div>
<div>
<h3>${web.escapeHtml(heading)}</h3>
<p>${fmt.md.renderInline(message)}</p>
</div>
<button class="notification--dismiss">&times;</button>
</div>`);
$notif.querySelector('.notification--dismiss').addEventListener('click', (event) => {
$notif.style.opacity = 0;
$notif.style.transform = 'scaleY(0)';
$notif.style.marginTop = `-${
$notif.offsetHeight / parseFloat(getComputedStyle(document.documentElement).fontSize)
}rem`;
setTimeout(() => $notif.remove(), 400);
onDismiss();
});
setTimeout(() => {
$notif.style.opacity = 1;
}, 100);
return this.$list.append($notif);
},
async fetch() {
const notifications = {
list: await fs.getJSON('https://notion-enhancer.github.io/notifications.json'),
dismissed: await storage.get(_id, 'notifications', []),
};
notifications.list = notifications.list.sort((a, b) => b.id - a.id);
notifications.waiting = notifications.list.filter(
({ id }) => !notifications.dismissed.includes(id)
);
for (const notification of notifications.waiting) {
for (const card of document.querySelectorAll('main > .library--card')) {
const { tags } = (await registry.get()).find((mod) => mod.id === card.dataset.mod),
isEnabled = await registry.isEnabled(card.dataset.mod);
if (
notification.heading &&
notification.appears_on &&
(notification.appears_on.versions.includes('*') ||
notification.appears_on.versions.includes(env.version)) &&
notification.appears_on.extension
(search.has('tag') ? tags.includes(search.get('tag')) : true) &&
(search.has('enabled') && search.has('disabled')
? true
: search.has('enabled')
? isEnabled
: search.has('disabled')
? !isEnabled
: true)
) {
this.push(notification, async () => {
const dismissed = await storage.get(_id, 'notifications', []);
storage.set('_notifications', 'external', [
...new Set([...dismissed, notification.id]),
]);
});
}
card.style.display = '';
} else card.style.display = 'none';
}
actionButtons.clearFilters();
}
);
router.addView(
'mod',
async () => {
const mod = (await registry.get()).find((mod) => mod.id === router.getSearch().get('id'));
if (!mod) return false;
const $fragment = web.createFragment(web.html`
<p class="action--buttons">
<a href="?view=library">
<span><i data-icon="fa/solid/long-arrow-alt-left"></i></span>
<span>back to library</span>
</a>
<a href="https://github.com/notion-enhancer/extension/tree/main/repo/${encodeURIComponent(
mod._dir
)}">
<span><i data-icon="fa/solid/code"></i></span>
<span>view source code</span>
</a>
</p>`);
const $card = await components.card(mod);
$card.querySelector('.library--expand').remove();
if (mod.options && mod.options.length) {
const options = web.createElement(web.html`<div class="library--options"></div>`);
mod.options
.filter((opt) => !opt.environments || opt.environments.includes(env.name))
.forEach(async (opt) =>
options.append(await components.options[opt.type](mod.id, opt))
);
$card.append(options);
}
$fragment.append(
$card,
web.createElement(web.html`
<article class="documentation--body markdown">
${
(await fs.isFile(`repo/${mod._dir}/README.md`))
? fmt.md.render(await fs.getText(`repo/${mod._dir}/README.md`))
: ''
}
</article>`)
);
fmt.Prism.highlightAllUnder($fragment);
actionButtons.reload($fragment);
return $fragment;
},
};
for (const error of await registry.errors()) {
notifications.push({
heading: `error: ${error.source}`,
message: error.message,
type: 'warning',
});
}
notifications.fetch();
() => {
if (document.querySelector('[data-mod]').dataset.mod !== router.getSearch().get('id'))
router.load(true);
}
);
async function theme() {
document.documentElement.className = `notion-${
(await storage.get(_id, 'theme')) || 'dark'
}-theme`;
}
window.addEventListener('focus', theme);
theme();
// registry.errors().then((err) => {
// document.querySelector('[data-section="alerts"]').innerHTML = JSON.stringify(err);
// });
router.setDefaultView('library');
router.load();

View File

@ -13,8 +13,9 @@
}
],
"css": {
"client": ["client.css"],
"menu": ["menu.css", "markdown.css"]
"frame": ["tooltips.css"],
"client": ["client.css", "tooltips.css"],
"menu": ["menu.css", "markdown.css", "tooltips.css"]
},
"js": {
"client": ["client.js"]

View File

@ -0,0 +1,100 @@
/*
* notion-enhancer core: menu
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
'use strict';
import { web } from '../../api.js';
export const getSearch = () =>
new Map(
location.search
.slice(1)
.split('&')
.map((query) => query.split('='))
);
let defaultView = '';
const views = new Map(),
filters = new Map();
export function setDefaultView(name) {
defaultView = name;
}
export function addView(name, loader, filter = () => {}) {
views.set(name, loader);
filters.set(name, filter);
}
export function removeView(name) {
views.delete(name);
filters.delete(name);
}
function router(event) {
event.preventDefault();
const anchor = event.path.find((anchor) => anchor.nodeName === 'A');
if (location.search !== anchor.getAttribute('href')) {
window.history.pushState(
{ search: anchor.getAttribute('href'), hash: '' },
'',
anchor.href
);
load();
}
}
function navigator(event) {
event.preventDefault();
const anchor = event.path.find((anchor) => anchor.nodeName === 'A'),
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 load(force = false) {
const $container = document.querySelector('main'),
search = getSearch(),
fallbackView = () =>
window.history.replaceState(
{ search: `?view=${defaultView}`, hash: '' },
null,
`?view=${defaultView}`
);
if (force || !search.get('view') || document.body.dataset.view !== search.get('view')) {
if (views.get(search.get('view'))) {
const $body = await (views.get(search.get('view')) || (() => void 0))();
if ($body) {
$container.style.opacity = 0;
await new Promise((res, rej) =>
setTimeout(() => {
document.body.dataset.view = search.get('view');
$container.innerHTML = '';
$container.append($body);
requestAnimationFrame(() => {
$container.style.opacity = '';
setTimeout(res, 200);
});
}, 200)
);
} else return fallbackView();
} else return fallbackView();
}
if (filters.get(search.get('view'))) filters.get(search.get('view'))(search);
}
window.addEventListener('popstate', (event) => {
if (event.state) load();
document.getElementById(location.hash.slice(1))?.scrollIntoView(true);
document.documentElement.scrollTop = 0;
});
web.addDocumentObserver((mutation) => {
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);
});
});

View File

@ -0,0 +1,20 @@
/*
* notion-enhancer core: tooltips
* (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
.enhancer--tooltip {
position: absolute;
background: var(--theme--tooltip);
color: var(--theme--tooltip-text);
font-size: var(--theme--font_ui_small-size);
padding: 0.15rem 0.4rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
border-radius: 3px;
max-width: 20rem;
display: none;
}
.enhancer--tooltip p {
margin: 0.25rem 0;
}

View File

@ -14,9 +14,8 @@ web.whenReady().then(async () => {
if (cssInsert) {
document.body.append(
web.createElement(
web.html`<style id="${await storage.get(_id, 'insert.css')}@${_id}">
${cssInsert}
</style>`
web.html`
<style id="${await storage.get(_id, 'insert.css')}@${_id}">${cssInsert}</style>`
)
);
}