From 8d679ae3c59c5a8f1d6587eec2889e33f91f6e17 Mon Sep 17 00:00:00 2001 From: dragonwocky Date: Fri, 4 Aug 2023 22:46:53 +1000 Subject: [PATCH] feat: add toggleable side panel w/ hotkey + tooltip --- src/core/client.mjs | 38 +++++++++- src/core/islands/Panel.mjs | 113 ++++++++++++++++++++++++++++- src/core/islands/Tooltip.mjs | 48 ++++++++++++ src/core/menu/islands/Banner.mjs | 2 +- src/core/menu/islands/Popup.mjs | 16 ++-- src/core/menu/islands/Profiles.mjs | 10 +-- src/core/mod.json | 6 ++ 7 files changed, 214 insertions(+), 19 deletions(-) create mode 100644 src/core/islands/Tooltip.mjs diff --git a/src/core/client.mjs b/src/core/client.mjs index 1a1df39..82a4585 100644 --- a/src/core/client.mjs +++ b/src/core/client.mjs @@ -8,6 +8,7 @@ import { checkForUpdate } from "./updateCheck.mjs"; import { sendTelemetryPing } from "./sendTelemetry.mjs"; import { Modal, Frame } from "./islands/Modal.mjs"; import { MenuButton } from "./islands/MenuButton.mjs"; +import { Panel } from "./islands/Panel.mjs"; const shouldLoadThemeOverrides = async (db) => { const { getMods, isEnabled } = globalThis.__enhancerApi, @@ -73,7 +74,7 @@ const insertMenu = async (db) => { src="${enhancerUrl("core/menu/index.html")}" onload=${function () { // pass notion-enhancer api to electron menu process - if (["darwin", "win32", "linux"].includes(platform)) { + if (["linux", "win32", "darwin"].includes(platform)) { const apiKey = "__enhancerApi"; this.contentWindow[apiKey] = globalThis[apiKey]; } @@ -92,9 +93,8 @@ const insertMenu = async (db) => { `; const appendToDom = () => { if (!document.contains($modal)) document.body.append($modal); - if (!document.querySelector(notionSidebar)?.contains($button)) { + if (!document.querySelector(notionSidebar)?.contains($button)) document.querySelector(notionSettingsAndMembers)?.after($button); - } }; addMutationListener(notionSidebar, appendToDom); addMutationListener("body", updateMenuTheme); @@ -102,6 +102,7 @@ const insertMenu = async (db) => { addKeyListener(openMenuHotkey, (event) => { event.preventDefault(); + event.stopPropagation(); $modal.open(); }); window.addEventListener("focus", triggerMenuRender); @@ -117,9 +118,40 @@ const insertMenu = async (db) => { }); }; +const insertPanel = async (db) => { + const notionFrame = ".notion-frame", + { html, addKeyListener, addMutationListener } = globalThis.__enhancerApi, + togglePanelHotkey = await db.get("togglePanelHotkey"); + + const $panel = html`<${Panel} + _getWidth=${() => db.get("sidePanelWidth")} + _setWidth=${(width) => db.set("sidePanelWidth", width)} + _getOpen=${() => db.get("sidePanelOpen")} + _setOpen=${(open) => db.set("sidePanelOpen", open)} + />`, + appendToDom = () => { + const $frame = document.querySelector(notionFrame); + if (!$frame) return; + if (!$frame.contains($panel)) $frame.append($panel); + if (!$frame.style.flexDirection !== "row") + $frame.style.flexDirection = "row"; + }; + addMutationListener(notionFrame, appendToDom); + appendToDom(); + + addKeyListener(togglePanelHotkey, (event) => { + event.preventDefault(); + event.stopPropagation(); + if ($panel.hasAttribute("open")) { + $panel.close(); + } else $panel.open(); + }); +}; + export default async (api, db) => Promise.all([ insertMenu(db), + insertPanel(db), insertCustomStyles(db), loadThemeOverrides(db), sendTelemetryPing(), diff --git a/src/core/islands/Panel.mjs b/src/core/islands/Panel.mjs index ddae96f..1a34cb1 100644 --- a/src/core/islands/Panel.mjs +++ b/src/core/islands/Panel.mjs @@ -4,9 +4,118 @@ * (https://notion-enhancer.github.io/) under the MIT license */ -function Panel(props) { +//
+// +// +// +//
; + +import { Tooltip } from "./Tooltip.mjs"; + +function Panel( + { + _getWidth, + _setWidth, + _getOpen, + _setOpen, + minWidth = 260, + maxWidth = 520, + ...props + }, + ...children +) { const { html, extendProps } = globalThis.__enhancerApi; - return html``; + extendProps(props, { + class: `notion-enhancer--side-panel order-2 shrink-0 + transition-[width] open:w-[var(--side\\_panel--width)] + w-0 border-l-1 border-[color:var(--theme--fg-border)] + relative bg-[color:var(--theme--bg-primary)]`, + }); + + const $tooltip = html`<${Tooltip}> + Drag to resize
+ Click to closed + `, + $panel = html``; + + const notionHelp = ".notion-help-button", + repositionHelp = async (attempts = 0) => { + const $notionHelp = document.querySelector(notionHelp); + if (!$notionHelp) { + if (attempts < 20) setTimeout(() => repositionHelp(attempts + 1), 150); + return; + } + let width = await _getWidth?.(); + if (isNaN(width)) width = minWidth; + if (!$panel.hasAttribute("open")) width = 0; + const position = $notionHelp.style.getPropertyValue("right"), + destination = `${26 + width}px`, + keyframes = [{ right: position }, { right: destination }], + options = { duration: 150, easing: "cubic-bezier(0.4, 0, 0.2, 1)" }; + $notionHelp.style.setProperty("right", destination); + $notionHelp.animate(keyframes, options); + }; + + $panel.resize = async (width) => { + $tooltip.hide(); + if (width) { + width = Math.max(width, minWidth); + width = Math.min(width, maxWidth); + _setWidth?.(width); + } else width = await _getWidth?.(); + if (isNaN(width)) width = minWidth; + $panel.style.setProperty("--side_panel--width", `${width}px`); + repositionHelp(); + }; + $panel.open = () => { + $panel.setAttribute("open", true); + $panel.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = 0)); + $panel.onopen?.(); + _setOpen(true); + $panel.resize(); + }; + $panel.close = () => { + $tooltip.hide(); + $panel.onbeforeclose?.(); + $panel.removeAttribute("open"); + $panel.style.pointerEvents = "auto"; + $panel.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = -1)); + repositionHelp(); + _setOpen(false); + setTimeout(() => { + $panel.style.pointerEvents = ""; + $panel.onclose?.(); + }, 150); + }; + _getOpen().then((open) => { + if (open) $panel.open(); + }); + + return $panel; } export { Panel }; diff --git a/src/core/islands/Tooltip.mjs b/src/core/islands/Tooltip.mjs new file mode 100644 index 0000000..286025f --- /dev/null +++ b/src/core/islands/Tooltip.mjs @@ -0,0 +1,48 @@ +/** + * notion-enhancer + * (c) 2023 dragonwocky (https://dragonwocky.me/) + * (https://notion-enhancer.github.io/) under the MIT license + */ + +function Tooltip(props, ...children) { + const { html, extendProps } = globalThis.__enhancerApi; + extendProps(props, { + role: "dialog", + class: `absolute group z-[999] pointer-events-none`, + }); + + const notionApp = ".notion-app-inner", + $tooltip = html`
+
+ ${children} +
+
`; + $tooltip.show = (x, y) => { + const $notionApp = document.querySelector(notionApp); + if (!document.contains($tooltip)) $notionApp?.append($tooltip); + requestAnimationFrame(() => { + $tooltip.setAttribute("open", true); + $tooltip.style.left = `${x - $tooltip.clientWidth - 6}px`; + $tooltip.style.top = `${y}px`; + $tooltip.onshow?.(); + }); + }; + $tooltip.hide = () => { + $tooltip.onbeforehide?.(); + $tooltip.removeAttribute("open"); + setTimeout(() => { + $tooltip.onhide?.(); + }, 200); + }; + + return $tooltip; +} + +export { Tooltip }; diff --git a/src/core/menu/islands/Banner.mjs b/src/core/menu/islands/Banner.mjs index 10d8199..0e5e791 100644 --- a/src/core/menu/islands/Banner.mjs +++ b/src/core/menu/islands/Banner.mjs @@ -107,7 +107,7 @@ function Banner({ updateAvailable, isDevelopmentBuild }) { useState(["focus", "view"], ([, view = "welcome"]) => { if (view !== "welcome") return; // delayed appearance = movement attracts eye - setTimeout(() => $version.lastElementChild.show(), 400); + setTimeout(() => $popup.open(), 400); }); } diff --git a/src/core/menu/islands/Popup.mjs b/src/core/menu/islands/Popup.mjs index 815365b..838eede 100644 --- a/src/core/menu/islands/Popup.mjs +++ b/src/core/menu/islands/Popup.mjs @@ -16,25 +16,25 @@ function Popup({ trigger, ...props }, ...children) { }); const $popup = html`
-
+
${children}
`; - $popup.show = () => { + $popup.open = () => { $popup.setAttribute("open", true); $popup.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = 0)); setState({ popupOpen: true }); $popup.onopen?.(); }; - $popup.hide = () => { + $popup.close = () => { $popup.onbeforeclose?.(); $popup.removeAttribute("open"); $popup.style.pointerEvents = "auto"; @@ -51,19 +51,19 @@ function Popup({ trigger, ...props }, ...children) { if (!$popup.hasAttribute("open")) return; if ($popup.contains(event.target) || $popup === event.target) return; if (trigger?.contains(event.target) || trigger === event.target) return; - $popup.hide(); + $popup.close(); }); useState(["rerender"], () => { - if ($popup.hasAttribute("open")) $popup.hide(); + if ($popup.hasAttribute("open")) $popup.close(); }); if (!trigger) return $popup; extendProps(trigger, { - onclick: $popup.show, + onclick: $popup.open, onkeydown(event) { if ([" ", "Enter"].includes(event.key)) { event.preventDefault(); - $popup.show(); + $popup.open(); } }, }); diff --git a/src/core/menu/islands/Profiles.mjs b/src/core/menu/islands/Profiles.mjs index c09a46c..cd2e72d 100644 --- a/src/core/menu/islands/Profiles.mjs +++ b/src/core/menu/islands/Profiles.mjs @@ -65,11 +65,11 @@ function Profile({ id }) { delete res["profileName"]; await profile.import(res); setState({ rerender: true }); - $uploadSuccess.show(); - setTimeout(() => $uploadSuccess.hide(), 2000); + $uploadSuccess.open(); + setTimeout(() => $uploadSuccess.close(), 2000); } catch (err) { - $uploadError.show(); - setTimeout(() => $uploadError.hide(), 2000); + $uploadError.open(); + setTimeout(() => $uploadError.close(), 2000); } // clear input value to allow repeat uploads event.target.value = ""; @@ -144,7 +144,7 @@ function Profile({ id }) { <${Button} tabindex="0" class="justify-center" - onclick=${() => $confirm.hide()} + onclick=${() => $confirm.close()} > Cancel diff --git a/src/core/mod.json b/src/core/mod.json index 9f464ce..50ab2b3 100644 --- a/src/core/mod.json +++ b/src/core/mod.json @@ -22,6 +22,12 @@ "description": "Opens the notion-enhancer menu from within Notion.", "value": "Ctrl+Shift+," }, + { + "type": "hotkey", + "key": "togglePanelHotkey", + "description": "Toggles the side panel used by some notion-enhancer extensions to display additional information and interfaces within the Notion app.", + "value": "Ctrl+Shift+|" + }, { "type": "heading", "label": "Appearance"