/* * notion-enhancer core: menu * (c) 2021 dragonwocky (https://dragonwocky.me/) * (https://notion-enhancer.github.io/) under the MIT license */ 'use strict'; 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 sheet of mod.css?.menu || []) { web.loadStyleset(`repo/${mod._dir}/${sheet}`); } } document .querySelector('img[data-view-target="notion"]') .addEventListener('click', env.focusNotion); web.hotkeyListener(await storage.get(_id, 'hotkey.focustoggle'), env.focusNotion); 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 components = {}; components.card = { preview: ({ preview = '' }) => preview ? web.createElement(web.html``) : '', async name({ name, id, version, tags }) { if (registry.CORE.includes(id)) return web.createElement(web.html`

${web.escapeHtml(name)} v${web.escapeHtml(version)}

`); const $el = web.createElement(web.html``); $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); } } }); return $el; }, tags: ({ tags = [] }) => web.createElement(web.html``), description: ({ description }) => web.createElement( web.html`

${fmt.md.renderInline( description )}

` ), authors: ({ authors }) => web.createElement(web.html``), expand: async ({ id }) => web.createElement( web.html`

settings & documentation

` ), async _generate(mod) { const card = web.createElement(web.html`
`), body = web.createElement(web.html`
`); 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; }, }; components.options = { async toggle(id, { key, label, tooltip }) { const state = await storage.get(id, key), opt = web.createElement(web.html``); opt.addEventListener('change', (event) => storage.set(id, key, event.target.checked)); if (tooltip) tooltips.add(opt, '[data-tooltip]', tooltip); return opt; }, async select(id, { key, label, tooltip, values }) { const state = await storage.get(id, key), opt = web.createElement(web.html``); opt.addEventListener('change', (event) => storage.set(id, key, event.target.value)); if (tooltip) tooltips.add(opt, '[data-tooltip]', tooltip); return opt; }, async text(id, { key, label, tooltip }) { const state = await storage.get(id, key), opt = web.createElement(web.html``); opt.querySelector('textarea').addEventListener('input', (event) => { event.target.style.removeProperty('--txt--scroll-height'); event.target.style.setProperty( '--txt--scroll-height', event.target.scrollHeight + 1 + 'px' ); }); opt.addEventListener('change', (event) => storage.set(id, key, event.target.value)); if (tooltip) tooltips.add(opt, '[data-tooltip]', tooltip); return opt; }, async number(id, { key, label, tooltip }) { const state = await storage.get(id, key), opt = web.createElement(web.html``); opt.addEventListener('change', (event) => storage.set(id, key, event.target.value)); if (tooltip) tooltips.add(opt, '[data-tooltip]', tooltip); return opt; }, async file(id, { key, label, tooltip, extensions }) { const state = await storage.get(id, key), opt = web.createElement(web.html``); opt.addEventListener('change', (event) => { const file = event.target.files[0], reader = new FileReader(); 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); }; reader.readAsText(file); }); opt.querySelector('.library--file_remove').addEventListener( 'click', (event) => { event.preventDefault(); opt.querySelector('input').value = ''; opt.querySelector('.library--file_path').innerText = 'choose file...'; storage.set(id, key, undefined); storage.set(id, `_file.${key}`, undefined); }, false ); 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.` ); 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`
`), 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 = { _reloadTriggered: false, buttons({ _dir }) { const $el = web.createElement(web.html`

back to library view source code

`); 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; }, readme: async (mod) => { const readme = web.createElement(web.html`
${ (await fs.isFile(`repo/${mod._dir}/README.md`)) ? fmt.md.render(await fs.getText(`repo/${mod._dir}/README.md`)) : '' }
`); fmt.Prism.highlightAllUnder(readme); return readme; }, }; 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(); 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(); } 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; for (const mod of await registry.get( (mod) => !mod.environments || mod.environments.includes(env.name) )) { this.$container.append(await components.card._generate(mod)); } }, }; 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``; className += ' celebration'; break; case 'information': svg = web.html``; className += ' information'; break; case 'warning': svg = web.html``; className += ' warning'; break; } const $notif = web.createElement(web.html``); $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) { if ( notification.heading && notification.appears_on && (notification.appears_on.versions.includes('*') || notification.appears_on.versions.includes(env.version)) && notification.appears_on.extension ) { this.push(notification, async () => { const dismissed = await storage.get(_id, 'notifications', []); storage.set('_notifications', 'external', [ ...new Set([...dismissed, notification.id]), ]); }); } } }, }; for (const error of await registry.errors()) { notifications.push({ heading: `error: ${error.source}`, message: error.message, type: 'warning', }); } notifications.fetch(); 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); // });