mirror of
https://github.com/notion-enhancer/notion-enhancer.git
synced 2025-04-11 15:59:03 +00:00
feat(panel): register/render custom views to side panel
+ fix: debounce useState callback triggers to prevent over-handling conflicts + minor refactoring
This commit is contained in:
parent
608b8d42d4
commit
463607c6d2
@ -123,13 +123,18 @@ const insertPanel = async (api, db) => {
|
|||||||
const notionFrame = ".notion-frame",
|
const notionFrame = ".notion-frame",
|
||||||
notionTopbarBtn = ".notion-topbar-more-button",
|
notionTopbarBtn = ".notion-topbar-more-button",
|
||||||
togglePanelHotkey = await db.get("togglePanelHotkey"),
|
togglePanelHotkey = await db.get("togglePanelHotkey"),
|
||||||
{ html } = api;
|
{ html, setState, addPanelView } = api;
|
||||||
|
|
||||||
const $panel = html`<${Panel}
|
const $panel = html`<${Panel}
|
||||||
_getWidth=${() => db.get("sidePanelWidth")}
|
...${Object.assign(
|
||||||
_setWidth=${(width) => db.set("sidePanelWidth", width)}
|
...["Width", "Open", "View"].map((key) => ({
|
||||||
_getOpen=${() => db.get("sidePanelOpen")}
|
[`_get${key}`]: () => db.get(`sidePanel${key}`),
|
||||||
_setOpen=${(open) => db.set("sidePanelOpen", open)}
|
[`_set${key}`]: async (value) => {
|
||||||
|
await db.set(`sidePanel${key}`, value);
|
||||||
|
setState({ rerender: true });
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
)}
|
||||||
/>`,
|
/>`,
|
||||||
togglePanel = () => {
|
togglePanel = () => {
|
||||||
if ($panel.hasAttribute("open")) $panel.close();
|
if ($panel.hasAttribute("open")) $panel.close();
|
||||||
|
@ -7,9 +7,73 @@
|
|||||||
import { Tooltip } from "./Tooltip.mjs";
|
import { Tooltip } from "./Tooltip.mjs";
|
||||||
import { Select } from "../menu/islands/Select.mjs";
|
import { Select } from "../menu/islands/Select.mjs";
|
||||||
|
|
||||||
function View(props) {
|
// note: these islands do not accept extensible
|
||||||
const { html } = globalThis.__enhancerApi;
|
// properties, i.e. they are not reusable.
|
||||||
return html``;
|
// please register your own interfaces via
|
||||||
|
// globalThis.__enhancerApi.addPanelView and
|
||||||
|
// not by re-instantiating additional panels
|
||||||
|
|
||||||
|
let panelViews = [],
|
||||||
|
// "$icon" may either be an actual dom element,
|
||||||
|
// or an icon name from the lucide icons set
|
||||||
|
addPanelView = ({ title, $icon, $view }) => {
|
||||||
|
panelViews.push([{ title, $icon }, $view]);
|
||||||
|
panelViews.sort(([{ title: a }], [{ title: b }]) => a.localeCompare(b));
|
||||||
|
const { setState } = globalThis.__enhancerApi;
|
||||||
|
setState?.({ panelViews });
|
||||||
|
},
|
||||||
|
removePanelView = ($view) => {
|
||||||
|
panelViews = panelViews.filter(([, v]) => v !== $view);
|
||||||
|
const { setState } = globalThis.__enhancerApi;
|
||||||
|
setState?.({ panelViews });
|
||||||
|
};
|
||||||
|
|
||||||
|
function View({ _get }) {
|
||||||
|
const { html, setState, useState } = globalThis.__enhancerApi,
|
||||||
|
$container = html`<div
|
||||||
|
class="overflow-(y-auto x-hidden)
|
||||||
|
h-full min-w-[var(--panel--width)]"
|
||||||
|
></div>`;
|
||||||
|
useState(["rerender"], async () => {
|
||||||
|
const openView = await _get?.(),
|
||||||
|
$view =
|
||||||
|
panelViews.find(([{ title }]) => {
|
||||||
|
return title === openView;
|
||||||
|
})?.[1] || panelViews[0]?.[1];
|
||||||
|
if (!$container.contains($view)) {
|
||||||
|
$container.innerHTML = "";
|
||||||
|
$container.append($view);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return $container;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Switcher({ _get, _set, minWidth, maxWidth }) {
|
||||||
|
const { html, extendProps, setState, useState } = globalThis.__enhancerApi,
|
||||||
|
$switcher = html`<div
|
||||||
|
class="relative flex items-center
|
||||||
|
font-medium p-[8.5px] ml-[4px] grow"
|
||||||
|
></div>`,
|
||||||
|
setView = (view) => {
|
||||||
|
_set?.(view);
|
||||||
|
setState({ activePanelView: view });
|
||||||
|
};
|
||||||
|
useState(["panelViews"], ([panelViews]) => {
|
||||||
|
const values = panelViews.map(([{ title, $icon }]) => {
|
||||||
|
// panel switcher internally uses the select island,
|
||||||
|
// which expects an option value rather than a title
|
||||||
|
return { value: title, $icon };
|
||||||
|
});
|
||||||
|
$switcher.innerHTML = "";
|
||||||
|
$switcher.append(html`<${Select}
|
||||||
|
popupMode="dropdown"
|
||||||
|
class="w-full text-left"
|
||||||
|
maxWidth=${maxWidth - 56}
|
||||||
|
minWidth=${minWidth - 56}
|
||||||
|
...${{ _get, _set: setView, values }}
|
||||||
|
/>`);
|
||||||
|
});
|
||||||
|
return $switcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Panel({
|
function Panel({
|
||||||
@ -17,91 +81,77 @@ function Panel({
|
|||||||
_setWidth,
|
_setWidth,
|
||||||
_getOpen,
|
_getOpen,
|
||||||
_setOpen,
|
_setOpen,
|
||||||
|
_getView,
|
||||||
|
_setView,
|
||||||
minWidth = 250,
|
minWidth = 250,
|
||||||
maxWidth = 640,
|
maxWidth = 640,
|
||||||
transitionDuration = 300,
|
transitionDuration = 300,
|
||||||
...props
|
|
||||||
}) {
|
}) {
|
||||||
const { html, extendProps, setState, useState } = globalThis.__enhancerApi,
|
const { html, setState, useState } = globalThis.__enhancerApi,
|
||||||
{ addMutationListener, removeMutationListener } = globalThis.__enhancerApi;
|
{ addMutationListener, removeMutationListener } = globalThis.__enhancerApi,
|
||||||
extendProps(props, {
|
$panel = html`<aside
|
||||||
class: `notion-enhancer--panel order-2 shrink-0
|
class="notion-enhancer--panel relative order-2
|
||||||
transition-[width] open:w-[var(--panel--width)]
|
shrink-0 bg-[color:var(--theme--bg-primary)]
|
||||||
border-l-1 border-[color:var(--theme--fg-border)]
|
border-(l-1 [color:var(--theme--fg-border)])
|
||||||
relative bg-[color:var(--theme--bg-primary)] w-0
|
transition-[width] w-[var(--panel--width,0)]
|
||||||
duration-[${transitionDuration}ms] group/panel`,
|
duration-[${transitionDuration}ms] group/panel"
|
||||||
});
|
>
|
||||||
|
<style>
|
||||||
|
.notion-frame {
|
||||||
|
flex-direction: row !important;
|
||||||
|
}
|
||||||
|
.notion-frame [role="progressbar"] {
|
||||||
|
padding-right: var(--panel--width);
|
||||||
|
}
|
||||||
|
.notion-frame [role="progressbar"] > div {
|
||||||
|
overflow-x: clip;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div
|
||||||
|
class="flex justify-between items-center
|
||||||
|
border-(b [color:var(--theme--fg-border)])"
|
||||||
|
>
|
||||||
|
<${Switcher}
|
||||||
|
...${{ _get: _getView, _set: _setView, minWidth, maxWidth }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
aria-label="Close side panel"
|
||||||
|
class="user-select-none h-[24px] w-[24px] duration-[20ms]
|
||||||
|
transition inline-flex items-center justify-center mr-[10px]
|
||||||
|
rounded-[3px] hover:bg-[color:var(--theme--bg-hover)]"
|
||||||
|
onclick=${() => $panel.close()}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="i-chevrons-right w-[20px] h-[20px]
|
||||||
|
text-[color:var(--theme--fg-secondary)]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<${View} ...${{ _get: _getView }} />
|
||||||
|
</aside>`;
|
||||||
|
|
||||||
const values = [
|
let preDragWidth,
|
||||||
{
|
dragStartX = 0;
|
||||||
icon: "type",
|
const $resizeHandle = html`<div
|
||||||
value: "word counter",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// prettier-ignore
|
|
||||||
$icon: html`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
|
|
||||||
<circle cx="5" cy="7" r="2.8"/>
|
|
||||||
<circle cx="5" cy="17" r="2.79"/>
|
|
||||||
<path d="M21,5.95H11c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h10c0.55,0,1,0.45,1,1v0C22,5.5,21.55,5.95,21,5.95z"/>
|
|
||||||
<path d="M17,10.05h-6c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h6c0.55,0,1,0.45,1,1v0C18,9.6,17.55,10.05,17,10.05z"/>
|
|
||||||
<path d="M21,15.95H11c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h10c0.55,0,1,0.45,1,1v0C22,15.5,21.55,15.95,21,15.95z" />
|
|
||||||
<path d="M17,20.05h-6c-0.55,0-1-0.45-1-1v0c0-0.55,0.45-1,1-1h6c0.55,0,1,0.45,1,1v0C18,19.6,17.55,20.05,17,20.05z"/>
|
|
||||||
</svg>`,
|
|
||||||
value: "outliner",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
_get = () => useState(["panelView"])[0],
|
|
||||||
_set = (value) => {
|
|
||||||
setState({ panelView: value, rerender: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
const $resize = html`<div
|
|
||||||
class="absolute h-full w-[3px] left-[-3px]
|
class="absolute h-full w-[3px] left-[-3px]
|
||||||
z-10 transition duration-300 hover:(cursor-col-resize
|
z-10 transition duration-300 hover:(cursor-col-resize
|
||||||
shadow-[var(--theme--fg-border)_-2px_0px_0px_0px_inset])
|
shadow-[var(--theme--fg-border)_-2px_0px_0px_0px_inset])
|
||||||
active:cursor-text group-not-[open]/panel:hidden"
|
active:cursor-text group-not-[open]/panel:hidden"
|
||||||
></div>`,
|
></div>`,
|
||||||
$close = html`<button
|
$resizeTooltip = html`<${Tooltip}>
|
||||||
aria-label="Close side panel"
|
<span>Drag</span> to resize<br />
|
||||||
class="user-select-none h-[24px] w-[24px] duration-[20ms]
|
<span>Click</span> to closed
|
||||||
transition inline-flex items-center justify-center mr-[10px]
|
<//>`,
|
||||||
rounded-[3px] hover:bg-[color:var(--theme--bg-hover)]"
|
showTooltip = (event) => {
|
||||||
>
|
setTimeout(() => {
|
||||||
<i
|
const panelOpen = $panel.hasAttribute("open"),
|
||||||
class="i-chevrons-right w-[20px] h-[20px]
|
handleHovered = $resizeHandle.matches(":hover");
|
||||||
text-[color:var(--theme--fg-secondary)]"
|
if (!panelOpen || !handleHovered) return;
|
||||||
/>
|
const { x } = $resizeHandle.getBoundingClientRect();
|
||||||
</div>`,
|
$resizeTooltip.show(x, event.clientY);
|
||||||
$switcher = html`<div
|
}, 200);
|
||||||
class="relative flex items-center
|
},
|
||||||
font-medium p-[8.5px] ml-[4px] grow"
|
startDrag = async (event) => {
|
||||||
>
|
|
||||||
<${Select}
|
|
||||||
popupMode="dropdown"
|
|
||||||
class="w-full text-left"
|
|
||||||
maxWidth=${maxWidth - 56}
|
|
||||||
minWidth=${minWidth - 56}
|
|
||||||
...${{ _get, _set, values }}
|
|
||||||
/>
|
|
||||||
</div>`,
|
|
||||||
$view = html`<div
|
|
||||||
class="overflow-(y-auto x-hidden)
|
|
||||||
h-full min-w-[var(--panel--width)]"
|
|
||||||
></div>`,
|
|
||||||
$panel = html`<aside ...${props}>
|
|
||||||
${$resize}
|
|
||||||
<div
|
|
||||||
class="flex justify-between items-center
|
|
||||||
border-(b [color:var(--theme--fg-border)])"
|
|
||||||
>
|
|
||||||
${$switcher}${$close}
|
|
||||||
</div>
|
|
||||||
${$view}
|
|
||||||
</aside>`;
|
|
||||||
|
|
||||||
let preDragWidth,
|
|
||||||
dragStartX = 0;
|
|
||||||
const startDrag = async (event) => {
|
|
||||||
dragStartX = event.clientX;
|
dragStartX = event.clientX;
|
||||||
preDragWidth = await _getWidth?.();
|
preDragWidth = await _getWidth?.();
|
||||||
if (isNaN(preDragWidth)) preDragWidth = minWidth;
|
if (isNaN(preDragWidth)) preDragWidth = minWidth;
|
||||||
@ -121,32 +171,18 @@ function Panel({
|
|||||||
// trigger panel close if not resized
|
// trigger panel close if not resized
|
||||||
if (dragStartX - event.clientX === 0) $panel.close();
|
if (dragStartX - event.clientX === 0) $panel.close();
|
||||||
};
|
};
|
||||||
$resize.addEventListener("mousedown", startDrag);
|
$resizeHandle.addEventListener("mouseout", $resizeTooltip.hide);
|
||||||
|
$resizeHandle.addEventListener("mousedown", startDrag);
|
||||||
const $tooltip = html`<${Tooltip}>
|
$resizeHandle.addEventListener("mouseover", showTooltip);
|
||||||
<span>Drag</span> to resize<br />
|
$panel.prepend($resizeHandle);
|
||||||
<span>Click</span> to closed
|
|
||||||
<//>`,
|
|
||||||
showTooltip = (event) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const panelOpen = $panel.hasAttribute("open"),
|
|
||||||
handleHovered = $resize.matches(":hover");
|
|
||||||
if (!panelOpen || !handleHovered) return;
|
|
||||||
const { x } = $resize.getBoundingClientRect();
|
|
||||||
$tooltip.show(x, event.clientY);
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
$resize.addEventListener("mouseover", showTooltip);
|
|
||||||
$resize.addEventListener("mouseout", () => $tooltip.hide());
|
|
||||||
$close.addEventListener("click", () => $panel.close());
|
|
||||||
|
|
||||||
// normally would place outside of an island, but in
|
// normally would place outside of an island, but in
|
||||||
// this case is necessary for syncing up animations
|
// this case is necessary for syncing up animations
|
||||||
const notionHelp = ".notion-help-button",
|
const notionHelp = ".notion-help-button",
|
||||||
repositionHelp = async () => {
|
repositionHelp = async (width) => {
|
||||||
const $notionHelp = document.querySelector(notionHelp);
|
const $notionHelp = document.querySelector(notionHelp);
|
||||||
if (!$notionHelp) return;
|
if (!$notionHelp) return;
|
||||||
let width = await _getWidth?.();
|
width ??= await _getWidth?.();
|
||||||
if (isNaN(width)) width = minWidth;
|
if (isNaN(width)) width = minWidth;
|
||||||
if (!$panel.hasAttribute("open")) width = 0;
|
if (!$panel.hasAttribute("open")) width = 0;
|
||||||
const position = $notionHelp.style.getPropertyValue("right"),
|
const position = $notionHelp.style.getPropertyValue("right"),
|
||||||
@ -163,15 +199,17 @@ function Panel({
|
|||||||
addMutationListener(notionHelp, repositionHelp);
|
addMutationListener(notionHelp, repositionHelp);
|
||||||
|
|
||||||
$panel.resize = async (width) => {
|
$panel.resize = async (width) => {
|
||||||
$tooltip.hide();
|
$resizeTooltip.hide();
|
||||||
if (width) {
|
if (width) {
|
||||||
width = Math.max(width, minWidth);
|
width = Math.max(width, minWidth);
|
||||||
width = Math.min(width, maxWidth);
|
width = Math.min(width, maxWidth);
|
||||||
_setWidth?.(width);
|
_setWidth?.(width);
|
||||||
} else width = await _getWidth?.();
|
} else width = await _getWidth?.();
|
||||||
if (isNaN(width)) width = minWidth;
|
if (isNaN(width)) width = minWidth;
|
||||||
$panel.style.setProperty("--panel--width", `${width}px`);
|
if (!$panel.hasAttribute("open")) width = 0;
|
||||||
repositionHelp();
|
const $cssVarTarget = $panel.parentElement || $panel;
|
||||||
|
$cssVarTarget.style.setProperty("--panel--width", `${width}px`);
|
||||||
|
repositionHelp(width);
|
||||||
};
|
};
|
||||||
$panel.open = () => {
|
$panel.open = () => {
|
||||||
$panel.setAttribute("open", true);
|
$panel.setAttribute("open", true);
|
||||||
@ -182,13 +220,13 @@ function Panel({
|
|||||||
$panel.resize();
|
$panel.resize();
|
||||||
};
|
};
|
||||||
$panel.close = () => {
|
$panel.close = () => {
|
||||||
$tooltip.hide();
|
$resizeTooltip.hide();
|
||||||
$panel.onbeforeclose?.();
|
$panel.onbeforeclose?.();
|
||||||
$panel.removeAttribute("open");
|
$panel.removeAttribute("open");
|
||||||
$panel.style.pointerEvents = "auto";
|
$panel.style.pointerEvents = "auto";
|
||||||
$panel.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = -1));
|
$panel.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = -1));
|
||||||
|
$panel.resize();
|
||||||
setState({ panelOpen: false });
|
setState({ panelOpen: false });
|
||||||
repositionHelp();
|
|
||||||
_setOpen(false);
|
_setOpen(false);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
$panel.style.pointerEvents = "";
|
$panel.style.pointerEvents = "";
|
||||||
@ -198,8 +236,13 @@ function Panel({
|
|||||||
_getOpen().then((open) => {
|
_getOpen().then((open) => {
|
||||||
if (open) $panel.open();
|
if (open) $panel.open();
|
||||||
});
|
});
|
||||||
|
|
||||||
return $panel;
|
return $panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
globalThis.__enhancerApi ??= {};
|
||||||
|
Object.assign(globalThis.__enhancerApi, {
|
||||||
|
addPanelView,
|
||||||
|
removePanelView,
|
||||||
|
});
|
||||||
|
|
||||||
export { Panel };
|
export { Panel };
|
||||||
|
@ -60,23 +60,25 @@ function Select({
|
|||||||
></div>`;
|
></div>`;
|
||||||
|
|
||||||
const options = values.map((opt) => {
|
const options = values.map((opt) => {
|
||||||
if (typeof opt === "string") opt = { value: opt };
|
if (["string", "number"].includes(typeof opt)) opt = { value: opt };
|
||||||
if (!(opt?.$icon instanceof Element)) {
|
if (!(opt?.$icon instanceof Element)) {
|
||||||
if (opt?.icon && typeof opt.icon === "string") {
|
if (typeof opt?.$icon === "string") {
|
||||||
opt.$icon = html`<i class="i-${opt.icon} h-[16px] w-[16px]" />`;
|
opt.$icon = html`<i class="i-${opt.$icon} h-[16px] w-[16px]" />`;
|
||||||
} else delete opt.$icon;
|
} else delete opt.$icon;
|
||||||
}
|
}
|
||||||
opt.$option = html`<${Option} ...${{ ...opt, _get, _set }} />`;
|
return {
|
||||||
opt.$selection = html`<div class="inline-flex items-center gap-[6px]">
|
...opt,
|
||||||
<!-- swap icon/value order for correct display when dir="rtl" -->
|
$option: html`<${Option} ...${{ ...opt, _get, _set }} />`,
|
||||||
<span>${opt.value}</span>${opt.$icon?.cloneNode(true) ?? ""}
|
$value: html`<div class="inline-flex items-center gap-[6px]">
|
||||||
</div>`;
|
<!-- swap icon/value order for correct display when dir="rtl" -->
|
||||||
return opt;
|
<span>${opt.value}</span>${opt.$icon?.cloneNode(true) ?? ""}
|
||||||
|
</div>`,
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
getSelected = async () => {
|
getSelected = async () => {
|
||||||
const value = (await _get?.()) ?? $select.innerText,
|
const value = (await _get?.()) ?? $select.innerText,
|
||||||
option = options.find((opt) => opt.value === value);
|
option = options.find((opt) => opt.value === value);
|
||||||
if (!option) _set(options[0].value);
|
if (!option) _set?.(options[0].value);
|
||||||
return option || options[0];
|
return option || options[0];
|
||||||
},
|
},
|
||||||
onKeydown = (event) => {
|
onKeydown = (event) => {
|
||||||
@ -102,9 +104,9 @@ function Select({
|
|||||||
let _initialValue;
|
let _initialValue;
|
||||||
useState(["rerender"], async () => {
|
useState(["rerender"], async () => {
|
||||||
if (!options.length) return;
|
if (!options.length) return;
|
||||||
const { value, $selection } = await getSelected();
|
const { value, $value } = await getSelected();
|
||||||
$select.innerHTML = "";
|
$select.innerHTML = "";
|
||||||
$select.append($selection);
|
$select.append($value);
|
||||||
if (_requireReload) {
|
if (_requireReload) {
|
||||||
_initialValue ??= value;
|
_initialValue ??= value;
|
||||||
if (value !== _initialValue) setState({ databaseUpdated: true });
|
if (value !== _initialValue) setState({ databaseUpdated: true });
|
||||||
|
@ -6,6 +6,51 @@
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
// convenient util: batch event handling
|
||||||
|
// every ___ ms to avoid over-handling &
|
||||||
|
// any conflicts / perf.issues that may
|
||||||
|
// otherwise result. a wait time of ~200ms
|
||||||
|
// is recommended (the avg. human visual
|
||||||
|
// reaction time is ~180-200ms)
|
||||||
|
const debounce = (callback, wait = 200) => {
|
||||||
|
let timeoutId;
|
||||||
|
return (...args) => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = setTimeout(() => callback(...args), wait);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// provides basic key/value reactivity:
|
||||||
|
// this is shared between all active mods,
|
||||||
|
// i.e. mods can read and update other mods'
|
||||||
|
// reactive states. this enables interop
|
||||||
|
// between a mod's component islands and
|
||||||
|
// supports inter-mod communication if so
|
||||||
|
// required. caution should be used in
|
||||||
|
// naming keys to avoid conflicts
|
||||||
|
const _state = {},
|
||||||
|
_subscribers = [],
|
||||||
|
setState = (state) => {
|
||||||
|
Object.assign(_state, state);
|
||||||
|
const updates = Object.keys(state);
|
||||||
|
_subscribers
|
||||||
|
.filter(([keys]) => updates.some((key) => keys.includes(key)))
|
||||||
|
.forEach(([keys, callback]) => callback(keys.map((key) => _state[key])));
|
||||||
|
},
|
||||||
|
// useState(["keyA", "keyB"]) => returns [valueA, valueB]
|
||||||
|
// useState(["keyA", "keyB"], callback) => registers callback
|
||||||
|
// to be triggered after each update to either keyA or keyB,
|
||||||
|
// with [valueA, valueB] passed to the callback's first arg
|
||||||
|
useState = (keys, callback) => {
|
||||||
|
const state = keys.map((key) => _state[key]);
|
||||||
|
if (callback) {
|
||||||
|
callback = debounce(callback);
|
||||||
|
_subscribers.push([keys, callback]);
|
||||||
|
callback(state);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
let documentObserver,
|
let documentObserver,
|
||||||
mutationListeners = [];
|
mutationListeners = [];
|
||||||
const mutationQueue = [],
|
const mutationQueue = [],
|
||||||
@ -99,20 +144,21 @@ const modifierAliases = [
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener("keyup", (event) => {
|
document.addEventListener("keyup", (event) => {
|
||||||
const keyupListeners = keyListeners.filter(
|
const keyupListeners = keyListeners //
|
||||||
([, , waitForKeyup]) => waitForKeyup
|
.filter(([, , waitForKeyup]) => waitForKeyup);
|
||||||
);
|
|
||||||
handleKeypress(event, keyupListeners);
|
handleKeypress(event, keyupListeners);
|
||||||
});
|
});
|
||||||
document.addEventListener("keydown", (event) => {
|
document.addEventListener("keydown", (event) => {
|
||||||
const keydownListeners = keyListeners.filter(
|
const keydownListeners = keyListeners //
|
||||||
([, , waitForKeyup]) => !waitForKeyup
|
.filter(([, , waitForKeyup]) => !waitForKeyup);
|
||||||
);
|
|
||||||
handleKeypress(event, keydownListeners);
|
handleKeypress(event, keydownListeners);
|
||||||
});
|
});
|
||||||
|
|
||||||
globalThis.__enhancerApi ??= {};
|
globalThis.__enhancerApi ??= {};
|
||||||
Object.assign(globalThis.__enhancerApi, {
|
Object.assign(globalThis.__enhancerApi, {
|
||||||
|
debounce,
|
||||||
|
setState,
|
||||||
|
useState,
|
||||||
addMutationListener,
|
addMutationListener,
|
||||||
removeMutationListener,
|
removeMutationListener,
|
||||||
addKeyListener,
|
addKeyListener,
|
||||||
|
@ -560,7 +560,9 @@ const h = (type, props, ...children) => {
|
|||||||
} else elem.setAttribute(prop, props[prop]);
|
} else elem.setAttribute(prop, props[prop]);
|
||||||
} else elem[prop] = props[prop];
|
} else elem[prop] = props[prop];
|
||||||
}
|
}
|
||||||
elem.append(...children);
|
if (type === "style") {
|
||||||
|
elem.append(children.join("").replace(/\s+/g, " "));
|
||||||
|
} else elem.append(...children);
|
||||||
return elem;
|
return elem;
|
||||||
},
|
},
|
||||||
// combines instance-provided element props
|
// combines instance-provided element props
|
||||||
@ -586,38 +588,8 @@ const h = (type, props, ...children) => {
|
|||||||
},
|
},
|
||||||
html = htm.bind(h);
|
html = htm.bind(h);
|
||||||
|
|
||||||
// provides basic key/value reactivity:
|
|
||||||
// this is shared between all active mods,
|
|
||||||
// i.e. mods can read and update other mods'
|
|
||||||
// reactive states. this enables interop
|
|
||||||
// between a mod's component islands and
|
|
||||||
// supports inter-mod communication if so
|
|
||||||
// required. caution should be used in
|
|
||||||
// naming keys to avoid conflicts
|
|
||||||
const _state = {},
|
|
||||||
_subscribers = [],
|
|
||||||
setState = (state) => {
|
|
||||||
Object.assign(_state, state);
|
|
||||||
const updates = Object.keys(state);
|
|
||||||
_subscribers
|
|
||||||
.filter(([keys]) => updates.some((key) => keys.includes(key)))
|
|
||||||
.forEach(([keys, callback]) => callback(keys.map((key) => _state[key])));
|
|
||||||
},
|
|
||||||
// useState(["keyA", "keyB"]) => returns [valueA, valueB]
|
|
||||||
// useState(["keyA", "keyB"], callback) => registers callback
|
|
||||||
// to be triggered after each update to either keyA or keyB,
|
|
||||||
// with [valueA, valueB] passed to the callback's first arg
|
|
||||||
useState = (keys, callback) => {
|
|
||||||
const state = keys.map((key) => _state[key]);
|
|
||||||
if (callback) _subscribers.push([keys, callback]);
|
|
||||||
callback?.(state);
|
|
||||||
return state;
|
|
||||||
};
|
|
||||||
|
|
||||||
globalThis.__enhancerApi ??= {};
|
globalThis.__enhancerApi ??= {};
|
||||||
Object.assign(globalThis.__enhancerApi, {
|
Object.assign(globalThis.__enhancerApi, {
|
||||||
html,
|
html,
|
||||||
extendProps,
|
extendProps,
|
||||||
setState,
|
|
||||||
useState,
|
|
||||||
});
|
});
|
||||||
|
@ -65,7 +65,12 @@ const modDatabase = async (id) => {
|
|||||||
const optionDefaults =
|
const optionDefaults =
|
||||||
(await getMods())
|
(await getMods())
|
||||||
.find((mod) => mod.id === id)
|
.find((mod) => mod.id === id)
|
||||||
?.options?.map?.((opt) => [opt.key, opt.value ?? opt.values?.[0]])
|
?.options?.map?.((opt) => {
|
||||||
|
let value = opt.value;
|
||||||
|
value ??= opt.values?.[0]?.value;
|
||||||
|
value ??= opt.values?.[0];
|
||||||
|
return [opt.key, value];
|
||||||
|
})
|
||||||
?.filter?.(([, value]) => typeof value !== "undefined") ?? {};
|
?.filter?.(([, value]) => typeof value !== "undefined") ?? {};
|
||||||
return globalThis.__enhancerApi.initDatabase(
|
return globalThis.__enhancerApi.initDatabase(
|
||||||
[await getProfile(), id],
|
[await getProfile(), id],
|
||||||
|
Loading…
Reference in New Issue
Block a user