mirror of
				https://github.com/notion-enhancer/notion-enhancer.git
				synced 2025-11-04 08:08:08 +11: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