/* * notion-enhancer core: api * (c) 2021 dragonwocky (https://dragonwocky.me/) * (c) 2021 CloudHill (https://github.com/CloudHill) * (https://notion-enhancer.github.io/) under the MIT license */ 'use strict'; /** * shared notion-style elements * @module notion-enhancer/api/components/side-panel */ import { fmt, web, components, registry } from '../_.mjs'; const db = await registry.db('36a2ffc9-27ff-480e-84a7-c7700a7d232d'); web.loadStylesheet('api/components/panel.css'); const _views = [], svgExpand = web.raw` `; // open + close let $notionFrame, $notionRightSidebar, // resize dragStartX, dragStartWidth, dragEventsFired, panelWidth, // render content $notionApp; // open + close const $panel = web.html`
`, $pinnedToggle = web.html`
${await components.feather('chevrons-right')}
`, $hoverTrigger = web.html`
`, panelPinnedAttr = 'data-enhancer-panel-pinned', isPinned = () => $panel.hasAttribute(panelPinnedAttr), togglePanel = () => { const $elems = [$notionFrame, $notionRightSidebar, $hoverTrigger, $panel].filter( ($el) => $el ); if (isPinned()) { closeSwitcher(); for (const $elem of $elems) $elem.removeAttribute(panelPinnedAttr); } else { for (const $elem of $elems) $elem.setAttribute(panelPinnedAttr, 'true'); } db.set(['panel.pinned'], isPinned()); }, // resize $resizeHandle = web.html`
`, updateWidth = async () => { document.documentElement.style.setProperty('--component--panel-width', panelWidth + 'px'); db.set(['panel.width'], panelWidth); }, resizeDrag = (event) => { event.preventDefault(); dragEventsFired = true; panelWidth = dragStartWidth + (dragStartX - event.clientX); if (panelWidth < 190) panelWidth = 190; if (panelWidth > 480) panelWidth = 480; $panel.style.width = panelWidth + 'px'; $hoverTrigger.style.width = panelWidth + 'px'; $notionFrame.style.paddingRight = panelWidth + 'px'; $notionRightSidebar.style.right = panelWidth + 'px'; }, resizeEnd = (event) => { $panel.style.width = ''; $hoverTrigger.style.width = ''; $notionFrame.style.paddingRight = ''; $notionRightSidebar.style.right = ''; updateWidth(); $resizeHandle.style.cursor = ''; document.body.removeEventListener('mousemove', resizeDrag); document.body.removeEventListener('mouseup', resizeEnd); }, resizeStart = (event) => { dragStartX = event.clientX; dragStartWidth = panelWidth; $resizeHandle.style.cursor = 'auto'; document.body.addEventListener('mousemove', resizeDrag); document.body.addEventListener('mouseup', resizeEnd); }, // render content $panelTitle = web.html`
`, $header = web.render(web.html`
`, $panelTitle), $panelContent = web.html`
`, $switcher = web.html`
`, $switcherTrigger = web.html`
${svgExpand}
`, $switcherOverlayContainer = web.html`
`, isSwitcherOpen = () => document.body.contains($switcher), openSwitcher = () => { if (!isPinned()) return togglePanel(); web.render($notionApp, $switcherOverlayContainer); web.empty($switcher); for (const view of _views) { const open = $panelTitle.contains(view.$title), $item = web.render( web.html`
`, web.render( web.html``, view.$icon.cloneNode(true), view.$title.cloneNode(true) ) ); $item.addEventListener('click', () => { renderView(view); db.set(['panel.open'], view.id); }); web.render($switcher, $item); } const rect = $header.getBoundingClientRect(); web.render( web.empty($switcherOverlayContainer), web.render( web.html`
`, web.render( web.html`
`, $switcher ) ) ); $switcher.querySelector('[data-open]').focus(); $switcher.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 200 }); document.addEventListener('keydown', switcherKeyListeners); }, closeSwitcher = () => { document.removeEventListener('keydown', switcherKeyListeners); $switcher.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 200 }).onfinish = () => $switcherOverlayContainer.remove(); }, switcherKeyListeners = (event) => { if (isSwitcherOpen()) { switch (event.key) { case 'Escape': closeSwitcher(); event.stopPropagation(); break; case 'Enter': document.activeElement.click(); event.stopPropagation(); break; case 'ArrowUp': const $prev = event.target.previousElementSibling; ($prev || event.target.parentElement.lastElementChild).focus(); event.stopPropagation(); break; case 'ArrowDown': const $next = event.target.nextElementSibling; ($next || event.target.parentElement.firstElementChild).focus(); event.stopPropagation(); break; } } }, renderView = (view) => { const prevView = _views.find(({ $content }) => document.contains($content)); web.render( web.empty($panelTitle), web.render( web.html``, view.$icon, view.$title ) ); view.onFocus(); web.render(web.empty($panelContent), view.$content); if (prevView) prevView.onBlur(); }; async function createPanel() { await web.whenReady(['.notion-frame']); $notionFrame = document.querySelector('.notion-frame'); const notionRightSidebarSelector = '.notion-cursor-listener > div[style*="flex-end"]', detectRightSidebar = () => { if (!document.contains($notionRightSidebar)) { $notionRightSidebar = document.querySelector(notionRightSidebarSelector); if (isPinned() && $notionRightSidebar) { $notionRightSidebar.setAttribute(panelPinnedAttr, 'true'); } } }; $notionRightSidebar = document.querySelector(notionRightSidebarSelector); web.addDocumentObserver(detectRightSidebar, [notionRightSidebarSelector]); if (await db.get(['panel.pinned'])) togglePanel(); web.addHotkeyListener(await db.get(['panel.hotkey']), togglePanel); $pinnedToggle.addEventListener('click', (event) => { event.stopPropagation(); togglePanel(); }); web.render( $panel, web.render($header, $panelTitle, $switcherTrigger, $pinnedToggle), $panelContent, $resizeHandle ); await enablePanelResize(); await createViews(); const cursorListenerSelector = '.notion-cursor-listener > :last-child[style^="position: absolute"]'; await web.whenReady([cursorListenerSelector]); document.querySelector(cursorListenerSelector).before($hoverTrigger, $panel); } async function enablePanelResize() { panelWidth = await db.get(['panel.width'], 240); updateWidth(); $resizeHandle.addEventListener('mousedown', resizeStart); $resizeHandle.addEventListener('click', () => { if (dragEventsFired) { dragEventsFired = false; } else togglePanel(); }); } async function createViews() { $notionApp = document.querySelector('.notion-app-inner'); $header.addEventListener('click', openSwitcher); $switcherTrigger.addEventListener('click', openSwitcher); $switcherOverlayContainer.addEventListener('click', closeSwitcher); } /** * adds a view to the enhancer's side panel * @param {object} panel - information used to construct and render the panel * @param {string} panel.id - a uuid, used to restore the last open view on reload * @param {string} panel.icon - an svg string * @param {string} panel.title - the name of the view * @param {Element} panel.$content - an element containing the content of the view * @param {function} panel.onBlur - runs when the view is selected/focused * @param {function} panel.onFocus - runs when the view is unfocused/closed */ export const addPanelView = async ({ id, icon, title, $content, onFocus = () => {}, onBlur = () => {}, }) => { const view = { id, $icon: web.render( web.html``, icon instanceof Element ? icon : web.html`${icon}` ), $title: web.render( web.html``, title, web.html`` ), $content, onFocus, onBlur, }; _views.push(view); if (_views.length === 1) await createPanel(); if (_views.length === 1 || (await db.get(['panel.open'])) === id) renderView(view); };