diff --git a/src/core/client.mjs b/src/core/client.mjs
index 621e69f..9e92adf 100644
--- a/src/core/client.mjs
+++ b/src/core/client.mjs
@@ -123,7 +123,8 @@ const insertPanel = async (api, db) => {
const notionFrame = ".notion-frame",
notionTopbarBtn = ".notion-topbar-more-button",
togglePanelHotkey = await db.get("togglePanelHotkey"),
- { html, setState, addPanelView } = api;
+ { addPanelView, addMutationListener, addKeyListener } = api,
+ { html, setState, useState } = api;
const $panel = html`<${Panel}
...${Object.assign(
@@ -178,17 +179,18 @@ const insertPanel = async (api, db) => {
$notionTopbarBtn?.before($panelTopbarBtn);
}
};
- api.addMutationListener(`${notionFrame}, ${notionTopbarBtn}`, appendToDom);
- api.useState(["panelOpen"], ([panelOpen]) => {
+ addMutationListener(`${notionFrame}, ${notionTopbarBtn}`, appendToDom);
+ appendToDom();
+
+ useState(["panelOpen"], ([panelOpen]) => {
if (panelOpen) $panelTopbarBtn.setAttribute("data-active", true);
else $panelTopbarBtn.removeAttribute("data-active");
});
- api.useState(["panelViews"], ([panelViews = []]) => {
+ useState(["panelViews"], ([panelViews = []]) => {
$panelTopbarBtn.style.display = panelViews.length ? "" : "none";
});
- appendToDom();
- api.addKeyListener(togglePanelHotkey, (event) => {
+ addKeyListener(togglePanelHotkey, (event) => {
event.preventDefault();
event.stopPropagation();
$panel.toggle();
diff --git a/src/core/islands/Panel.mjs b/src/core/islands/Panel.mjs
index 17b17e0..b371bcd 100644
--- a/src/core/islands/Panel.mjs
+++ b/src/core/islands/Panel.mjs
@@ -8,11 +8,9 @@
import { Tooltip } from "./Tooltip.mjs";
import { Select } from "../menu/islands/Select.mjs";
-// note: these islands do not accept extensible
-// properties, i.e. they are not reusable.
-// please register your own interfaces via
-// globalThis.__enhancerApi.addPanelView and
-// not by re-instantiating additional panels
+// note: these islands are not reusable.
+// panel views can be added via addPanelView,
+// do not instantiate additional panels
let panelViews = [],
// "$icon" may either be an actual dom element,
@@ -90,9 +88,9 @@ function Panel({
}) {
const { html, setState, useState } = globalThis.__enhancerApi,
{ addMutationListener, removeMutationListener } = globalThis.__enhancerApi,
- $panel = html``;
+
+ `;
- let preDragWidth,
- dragStartX = 0;
+ let preDragWidth, dragStartX;
+ const getWidth = async (width) => {
+ if (width && !isNaN(width)) {
+ width = Math.max(width, minWidth);
+ width = Math.min(width, maxWidth);
+ } else width = await _getWidth?.();
+ if (isNaN(width)) width = minWidth;
+ return width;
+ },
+ setInteractive = (interactive) => {
+ $panel
+ .querySelectorAll("[tabindex]")
+ .forEach(($el) => ($el.tabIndex = interactive ? 1 : -1));
+ },
+ isDragging = () => !isNaN(preDragWidth) && !isNaN(dragStartX),
+ isPinned = () => $panel.hasAttribute("data-pinned"),
+ isPeeked = () => $panel.hasAttribute("data-peeked"),
+ isClosed = () => !isPinned() && !isPeeked();
+
+ const closedWidth = { width: "0px" },
+ openWidth = { width: "var(--panel--width, 0px)" },
+ peekAnimation = {
+ height: "calc(100vh - 120px)",
+ bottom: "60px",
+ borderTopWidth: "1px",
+ borderBottomWidth: "1px",
+ borderTopLeftRadius: "8px",
+ borderBottomLeftRadius: "8px",
+ boxShadow:
+ "rgba(15, 15, 15, 0.1) 0px 0px 0px 1px, rgba(15, 15, 15, 0.2) 0px 3px 6px, rgba(15, 15, 15, 0.4) 0px 9px 24px",
+ },
+ pinAnimation = {
+ height: "calc(100vh - 45px)",
+ bottom: "0px",
+ borderTopWidth: "0px",
+ borderBottomWidth: "0px",
+ borderTopLeftRadius: "0px",
+ borderBottomLeftRadius: "0px",
+ boxShadow: "none",
+ };
+
+ const animationState = {},
+ easing = "cubic-bezier(0.4, 0, 0.2, 1)",
+ animate = ($target, keyframes) => {
+ const opts = { fill: "forwards", duration: transitionDuration, easing };
+ $target.animate(keyframes, opts);
+ console.log($target, keyframes);
+ },
+ animatePanel = (to) => {
+ animate($panel.lastElementChild, [animationState, to]);
+ Object.assign(animationState, to);
+ };
+
+ // dragging the resize handle horizontally will
+ // adjust the width of the panel correspondingly
const $resizeHandle = html`
`,
$onResizeClick = html`close`,
@@ -160,120 +209,132 @@ function Panel({
/>`,
showTooltip = (event) => {
setTimeout(() => {
- const handleHovered = $resizeHandle.matches(":hover");
- if (!handleHovered) return;
- const panelOpen = $panel.hasAttribute("open"),
+ if (!$resizeHandle.matches(":hover")) return;
+ const open = $panel.hasAttribute("open"),
{ x } = $resizeHandle.getBoundingClientRect();
- $onResizeClick.innerText = panelOpen ? "close" : "lock open";
+ $onResizeClick.innerText = open ? "close" : "lock open";
$resizeTooltip.show(x, event.clientY);
}, 200);
},
startDrag = async (event) => {
dragStartX = event.clientX;
- preDragWidth = await _getWidth?.();
- if (isNaN(preDragWidth)) preDragWidth = minWidth;
+ preDragWidth = await getWidth();
document.addEventListener("mousemove", onDrag);
document.addEventListener("mouseup", endDrag);
- $panel.style.transitionDuration = "0ms";
},
onDrag = (event) => {
event.preventDefault();
+ if (!isDragging()) return;
$panel.resize(preDragWidth + (dragStartX - event.clientX));
},
endDrag = (event) => {
document.removeEventListener("mousemove", onDrag);
document.removeEventListener("mouseup", endDrag);
- $panel.style.transitionDuration = "";
+ if (!isDragging()) return;
$panel.resize(preDragWidth + (dragStartX - event.clientX));
// toggle panel if not resized
if (dragStartX - event.clientX === 0) $panel.toggle();
+ preDragWidth = dragStartX = undefined;
};
$resizeHandle.addEventListener("mouseout", $resizeTooltip.hide);
$resizeHandle.addEventListener("mousedown", startDrag);
$resizeHandle.addEventListener("mouseover", showTooltip);
- $panel.prepend($resizeHandle);
+ $panel.lastElementChild.prepend($resizeHandle);
- // pop out panel preview when hovering near the right edge
- // of the screen, otherwise collapse panel when closed
- const $hoverTrigger = html``;
- $hoverTrigger.addEventListener("mouseover", () => $panel.resize(0, true));
- $panel.addEventListener("mouseenter", () => $panel.resize());
- $panel.addEventListener("mouseout", () => $panel.resize());
- $panel.append($hoverTrigger);
+ $panel.prepend($peekTrigger);
+ $panel.addEventListener("mouseout", () => {
+ if (isDragging() || isPinned()) return;
+ if (!$panel.matches(":hover")) $panel.close();
+ });
+ $panel.addEventListener("mouseover", () => {
+ if (isClosed() && $panel.matches(":hover")) $panel.peek();
+ });
+ // moves help button out of the way of open panel.
// normally would place outside of an island, but in
// this case is necessary for syncing up animations
const notionHelp = ".notion-help-button",
repositionHelp = async (width) => {
const $notionHelp = document.querySelector(notionHelp);
if (!$notionHelp) return;
- width ??= await _getWidth?.();
+ 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: transitionDuration,
- easing: "cubic-bezier(0.4, 0, 0.2, 1)",
- };
- $notionHelp.style.setProperty("right", destination);
- $notionHelp.animate(keyframes, options);
+ const to = `${26 + width}px`,
+ from = $notionHelp.style.getPropertyValue("right"),
+ opts = { duration: transitionDuration, easing };
+ if (from === to) return;
+ $notionHelp.style.setProperty("right", to);
+ animate($notionHelp, [({ right: from }, { right: to })]);
removeMutationListener(repositionHelp);
};
addMutationListener(notionHelp, repositionHelp);
- $panel.resize = async (width, peek = false) => {
- $resizeTooltip.hide();
- if (width && !isNaN(width)) {
- width = Math.max(width, minWidth);
- width = Math.min(width, maxWidth);
- _setWidth?.(width);
- } else width = await _getWidth?.();
- if (isNaN(width)) width = minWidth;
- const panelOpen = $panel.hasAttribute("open"),
- panelHovered = $panel.matches(":hover");
- if (panelOpen) {
- } else {
- if (!panelHovered && !peek) width = 0;
- }
- const $cssVarTarget = $panel.parentElement || $panel;
- $cssVarTarget.style.setProperty("--panel--width", `${width}px`);
- if ($cssVarTarget !== $panel) $panel.style.removeProperty("--panel--width");
- repositionHelp(width);
- };
- $panel.open = () => {
- if (!panelViews.length) return;
- $panel.setAttribute("open", true);
- $panel.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = 1));
- setState({ panelOpen: true });
- $panel.onopen?.();
+ $panel.pin = () => {
+ if (isPinned() || !panelViews.length) return;
+ if (isClosed()) Object.assign(animationState, pinAnimation);
+ animatePanel({ ...openWidth, ...pinAnimation });
+ animate($panel, [closedWidth, openWidth]);
+ $panel.removeAttribute("data-peeked");
+ $panel.dataset.pinned = true;
+ setInteractive(true);
_setOpen(true);
+ setState({ panelOpen: true });
$panel.resize();
};
- $panel.close = () => {
- $resizeTooltip.hide();
- $panel.onbeforeclose?.();
- $panel.removeAttribute("open");
- $panel.style.pointerEvents = "auto";
- $panel.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = -1));
+ $panel.peek = () => {
+ if (isPeeked() || !panelViews.length) return;
+ if (isClosed()) Object.assign(animationState, peekAnimation);
+ animatePanel({ ...openWidth, ...peekAnimation });
+ $panel.removeAttribute("data-pinned");
+ $panel.dataset.peeked = true;
+ setInteractive(true);
$panel.resize();
+ };
+ $panel.close = async () => {
+ if (isClosed()) return;
setState({ panelOpen: false });
if (panelViews.length) _setOpen(false);
- setTimeout(() => {
- $panel.style.pointerEvents = "";
- $panel.onclose?.();
- }, transitionDuration);
+ const width = (animationState.width = `${await getWidth()}px`);
+ // only animate container close if it is actually taking up space,
+ // otherwise will unnaturally grow + retrigger peek on peek mouseout
+ if (isPinned()) animate($panel, [{ width }, closedWidth]);
+ if (!$panel.matches(":hover")) {
+ $panel.removeAttribute("data-pinned");
+ $panel.removeAttribute("data-peeked");
+ animatePanel(closedWidth);
+ setInteractive(false);
+ $panel.resize();
+ } else $panel.peek();
};
$panel.toggle = () => {
- if ($panel.hasAttribute("open")) $panel.close();
- else $panel.open();
+ if (isPinned()) $panel.close();
+ else $panel.pin();
};
+ // resizing handles visual resizes (inc. setting width to 0
+ // if closed) and actual resizes on drag (inc. saving to db)
+ $panel.resize = async (width) => {
+ $resizeTooltip.hide();
+ width = await getWidth(width);
+ _setWidth?.(width);
+ // works in conjunction with animations, acts as fallback
+ // plus updates dependent styles e.g. page skeleton padding
+ if (isClosed()) width = 0;
+ const $parent = $panel.parentElement || $panel;
+ $parent.style.setProperty("--panel--width", `${width}px`);
+ if ($parent !== $panel) $panel.style.removeProperty("--panel--width");
+ repositionHelp(width);
+ };
+
useState(["panelViews"], async ([panelViews = []]) => {
- if (panelViews.length && (await _getOpen())) $panel.open();
+ if (panelViews.length && (await _getOpen())) $panel.pin();
else $panel.close();
});
return $panel;
diff --git a/src/shared/markup.js b/src/shared/markup.js
index 3ce6a96..d52ac35 100644
--- a/src/shared/markup.js
+++ b/src/shared/markup.js
@@ -93,7 +93,6 @@ twind.install({
["children", "&>*"],
["siblings", "&~*"],
["sibling", "&+*"],
- ["\\[.+]", (match) => "&" + match.input],
[/^&/, (match) => match.input],
],
});