chore: simplify api imports

This commit is contained in:
dragonwocky 2024-02-17 00:27:35 +11:00
parent f0e2570448
commit 1f717b98ca
Signed by: dragonwocky
GPG Key ID: 7998D08F7D7BD7A8
9 changed files with 224 additions and 220 deletions

View File

@ -1,6 +1,6 @@
/** /**
* notion-enhancer * notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/) * (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
@ -13,6 +13,7 @@ import {
expandVariantGroup, expandVariantGroup,
} from "../vendor/@unocss-core.mjs"; } from "../vendor/@unocss-core.mjs";
import { presetUno } from "../vendor/@unocss-preset-uno.mjs"; import { presetUno } from "../vendor/@unocss-preset-uno.mjs";
import "../assets/icons.svg.js";
// prettier-ignore // prettier-ignore
// https://developer.mozilla.org/en-US/docs/Web/SVG/Element // https://developer.mozilla.org/en-US/docs/Web/SVG/Element
@ -21,71 +22,87 @@ import { presetUno } from "../vendor/@unocss-preset-uno.mjs";
const svgElements = ["animate","animateMotion","animateTransform","circle","clipPath","defs","desc","discard","ellipse","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence","filter","foreignObject","g","hatch","hatchpath","image","line","linearGradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialGradient","rect","script","set","stop","style","svg","switch","symbol","text","textPath","title","tspan","use","view"], const svgElements = ["animate","animateMotion","animateTransform","circle","clipPath","defs","desc","discard","ellipse","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence","filter","foreignObject","g","hatch","hatchpath","image","line","linearGradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialGradient","rect","script","set","stop","style","svg","switch","symbol","text","textPath","title","tspan","use","view"],
htmlAttributes = ["accept","accept-charset","accesskey","action","align","allow","alt","async","autocapitalize","autocomplete","autofocus","autoplay","background","bgcolor","border","buffered","capture","challenge","charset","checked","cite","class","code","codebase","color","cols","colspan","content","contenteditable","contextmenu","controls","coords","crossorigin","csp","data","data-*","datetime","decoding","default","defer","dir","dirname","disabled","download","draggable","enctype","enterkeyhint","for","form","formaction","formenctype","formmethod","formnovalidate","formtarget","headers","height","hidden","high","href","hreflang","http-equiv","icon","id","importance","integrity","inputmode","ismap","itemprop","keytype","kind","label","lang","loading","list","loop","low","max","maxlength","minlength","media","method","min","multiple","muted","name","novalidate","open","optimum","pattern","ping","placeholder","playsinline","poster","preload","radiogroup","readonly","referrerpolicy","rel","required","reversed","role","rows","rowspan","sandbox","scope","selected","shape","size","sizes","slot","span","spellcheck","src","srcdoc","srclang","srcset","start","step","style","tabindex","target","title","translate","type","usemap","value","width","wrap","accent-height","accumulate","additive","alignment-baseline","alphabetic","amplitude","arabic-form","ascent","attributeName","attributeType","azimuth","baseFrequency","baseline-shift","baseProfile","bbox","begin","bias","by","calcMode","cap-height","clip","clipPathUnits","clip-path","clip-rule","color-interpolation","color-interpolation-filters","color-profile","color-rendering","contentScriptType","contentStyleType","cursor","cx","cy","d","decelerate","descent","diffuseConstant","direction","display","divisor","dominant-baseline","dur","dx","dy","edgeMode","elevation","enable-background","end","exponent","fill","fill-opacity","fill-rule","filter","filterRes","filterUnits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","format","from","fr","fx","fy","g1","g2","glyph-name","glyph-orientation-horizontal","glyph-orientation-vertical","glyphRef","gradientTransform","gradientUnits","hanging","horiz-adv-x","horiz-origin-x","ideographic","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kernelMatrix","kernelUnitLength","kerning","keyPoints","keySplines","keyTimes","lengthAdjust","letter-spacing","lighting-color","limitingConeAngle","local","marker-end","marker-mid","marker-start","markerHeight","markerUnits","markerWidth","mask","maskContentUnits","maskUnits","mathematical","mode","numOctaves","offset","opacity","operator","order","orient","orientation","origin","overflow","overline-position","overline-thickness","panose-1","paint-order","path","pathLength","patternContentUnits","patternTransform","patternUnits","pointer-events","points","pointsAtX","pointsAtY","pointsAtZ","preserveAlpha","preserveAspectRatio","primitiveUnits","r","radius","referrerPolicy","refX","refY","rendering-intent","repeatCount","repeatDur","requiredExtensions","requiredFeatures","restart","result","rotate","rx","ry","scale","seed","shape-rendering","slope","spacing","specularConstant","specularExponent","speed","spreadMethod","startOffset","stdDeviation","stemh","stemv","stitchTiles","stop-color","stop-opacity","strikethrough-position","strikethrough-thickness","string","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","surfaceScale","systemLanguage","tableValues","targetX","targetY","text-anchor","text-decoration","text-rendering","textLength","to","transform","transform-origin","u1","u2","underline-position","underline-thickness","unicode","unicode-bidi","unicode-range","units-per-em","v-alphabetic","v-hanging","v-ideographic","v-mathematical","values","vector-effect","version","vert-adv-y","vert-origin-x","vert-origin-y","viewBox","viewTarget","visibility","widths","word-spacing","writing-mode","x","x-height","x1","x2","xChannelSelector","xlink:actuate","xlink:arcrole","xlink:href","xlink:role","xlink:show","xlink:title","xlink:type","xml:base","xml:lang","xml:space","y","y1","y2","yChannelSelector","z","zoomAndPan"]; htmlAttributes = ["accept","accept-charset","accesskey","action","align","allow","alt","async","autocapitalize","autocomplete","autofocus","autoplay","background","bgcolor","border","buffered","capture","challenge","charset","checked","cite","class","code","codebase","color","cols","colspan","content","contenteditable","contextmenu","controls","coords","crossorigin","csp","data","data-*","datetime","decoding","default","defer","dir","dirname","disabled","download","draggable","enctype","enterkeyhint","for","form","formaction","formenctype","formmethod","formnovalidate","formtarget","headers","height","hidden","high","href","hreflang","http-equiv","icon","id","importance","integrity","inputmode","ismap","itemprop","keytype","kind","label","lang","loading","list","loop","low","max","maxlength","minlength","media","method","min","multiple","muted","name","novalidate","open","optimum","pattern","ping","placeholder","playsinline","poster","preload","radiogroup","readonly","referrerpolicy","rel","required","reversed","role","rows","rowspan","sandbox","scope","selected","shape","size","sizes","slot","span","spellcheck","src","srcdoc","srclang","srcset","start","step","style","tabindex","target","title","translate","type","usemap","value","width","wrap","accent-height","accumulate","additive","alignment-baseline","alphabetic","amplitude","arabic-form","ascent","attributeName","attributeType","azimuth","baseFrequency","baseline-shift","baseProfile","bbox","begin","bias","by","calcMode","cap-height","clip","clipPathUnits","clip-path","clip-rule","color-interpolation","color-interpolation-filters","color-profile","color-rendering","contentScriptType","contentStyleType","cursor","cx","cy","d","decelerate","descent","diffuseConstant","direction","display","divisor","dominant-baseline","dur","dx","dy","edgeMode","elevation","enable-background","end","exponent","fill","fill-opacity","fill-rule","filter","filterRes","filterUnits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","format","from","fr","fx","fy","g1","g2","glyph-name","glyph-orientation-horizontal","glyph-orientation-vertical","glyphRef","gradientTransform","gradientUnits","hanging","horiz-adv-x","horiz-origin-x","ideographic","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kernelMatrix","kernelUnitLength","kerning","keyPoints","keySplines","keyTimes","lengthAdjust","letter-spacing","lighting-color","limitingConeAngle","local","marker-end","marker-mid","marker-start","markerHeight","markerUnits","markerWidth","mask","maskContentUnits","maskUnits","mathematical","mode","numOctaves","offset","opacity","operator","order","orient","orientation","origin","overflow","overline-position","overline-thickness","panose-1","paint-order","path","pathLength","patternContentUnits","patternTransform","patternUnits","pointer-events","points","pointsAtX","pointsAtY","pointsAtZ","preserveAlpha","preserveAspectRatio","primitiveUnits","r","radius","referrerPolicy","refX","refY","rendering-intent","repeatCount","repeatDur","requiredExtensions","requiredFeatures","restart","result","rotate","rx","ry","scale","seed","shape-rendering","slope","spacing","specularConstant","specularExponent","speed","spreadMethod","startOffset","stdDeviation","stemh","stemv","stitchTiles","stop-color","stop-opacity","strikethrough-position","strikethrough-thickness","string","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","surfaceScale","systemLanguage","tableValues","targetX","targetY","text-anchor","text-decoration","text-rendering","textLength","to","transform","transform-origin","u1","u2","underline-position","underline-thickness","unicode","unicode-bidi","unicode-range","units-per-em","v-alphabetic","v-hanging","v-ideographic","v-mathematical","values","vector-effect","version","vert-adv-y","vert-origin-x","vert-origin-y","viewBox","viewTarget","visibility","widths","word-spacing","writing-mode","x","x-height","x1","x2","xChannelSelector","xlink:actuate","xlink:arcrole","xlink:href","xlink:role","xlink:show","xlink:title","xlink:type","xml:base","xml:lang","xml:space","y","y1","y2","yChannelSelector","z","zoomAndPan"];
const kebabToPascalCase = (string) => // accelerators approximately match electron accelerators.
string[0].toUpperCase() + // logic used when recording hotkeys in menu matches logic used
string.replace(/-[a-z]/g, (match) => match.slice(1).toUpperCase()).slice(1), // when triggering hotkeys => detection should be reliable.
hToString = (type, props, ...children) => // default hotkeys using "alt" may trigger an altcode or
`<${type}${Object.entries(props) // accented character on some keyboard layouts (not recommended).
.map(([attr, value]) => ` ${attr}="${value}"`) let keyListeners = [];
.join("")}>${children const modifierAliases = [
.map((child) => (Array.isArray(child) ? hToString(...child) : child)) ["metaKey", ["meta", "os", "win", "cmd", "command"]],
.join("")}</${type}>`, ["ctrlKey", ["ctrl", "control"]],
svgToUri = (svg) => { ["shiftKey", ["shift"]],
// https://gist.github.com/jennyknuth/222825e315d45a738ed9d6e04c7a88d0 ["altKey", ["alt"]],
const xlmns = ~svg.indexOf("xmlns") ],
? "<svg" addKeyListener = (accelerator, callback, waitForKeyup = false) => {
: '<svg xmlns="http://www.w3.org/2000/svg"'; if (typeof accelerator === "string") accelerator = accelerator.split("+");
return `url("data:image/svg+xml;utf8,${svg accelerator = accelerator.map((key) => key.toLowerCase());
.replace("<svg", xlmns) keyListeners.push([accelerator, callback, waitForKeyup]);
.replace(/"/g, "'") },
.replace(/%/g, "%25") removeKeyListener = (callback) => {
.replace(/#/g, "%23") keyListeners = keyListeners.filter(([, c]) => c !== callback);
.replace(/{/g, "%7B") },
.replace(/}/g, "%7D") handleKeypress = (event, keyListeners) => {
.replace(/</g, "%3C") for (const [accelerator, callback] of keyListeners) {
.replace(/>/g, "%3E") const acceleratorModifiers = [],
.replace(/\s+/g, " ") combinationTriggered =
.trim()}")`; accelerator.every((key) => {
}; for (const [modifier, aliases] of modifierAliases) {
if (aliases.includes(key)) {
const iconPattern = /^i-((?:\w|-)+)(?:\?(mask|bg|auto))?$/, acceleratorModifiers.push(modifier);
presetIcons = ([, icon, mode]) => { return true;
let svg; }
if (!["bg", "mask"].includes(mode)) mode = undefined; }
if (icon === "notion-enhancer") { if (key === "space") key = " ";
const { iconColour, iconMonochrome } = globalThis.__enhancerApi; if (key === "plus") key = "equal";
svg = mode === "mask" ? iconMonochrome : iconColour; if (key === "minus") key = "-";
} else { if (key === "\\") key = "backslash";
icon = kebabToPascalCase(icon); if (key === ",") key = "comma";
if (!lucide[icon]) return; if (key === ".") key = "period";
const [type, props, children] = lucide[icon]; const keyPressed = [
svg = hToString(type, props, ...children); event.key.toLowerCase(),
event.code.toLowerCase(),
].includes(key);
return keyPressed;
}) &&
modifierAliases.every(([modifier]) => {
// required && used -> matches accelerator
// !required && !used -> matches accelerator
// (required && !used) || (!required && used) -> no match
// differentiates e.g.ctrl + x from ctrl + shift + x
return acceleratorModifiers.includes(modifier) === event[modifier];
});
if (combinationTriggered) callback(event);
} }
mode ??= svg.includes("currentColor") ? "mask" : "bg"; },
return { onKeyup = (event) => {
// https://antfu.me/posts/icons-in-pure-css const keyupListeners = keyListeners //
display: "inline-block", .filter(([, , waitForKeyup]) => waitForKeyup);
height: "1em", handleKeypress(event, keyupListeners);
width: "1em", },
...(mode === "mask" onKeydown = (event) => {
? { const keydownListeners = keyListeners //
mask: `${svgToUri(svg)} no-repeat`, .filter(([, , waitForKeyup]) => !waitForKeyup);
"mask-size": "100% 100%", handleKeypress(event, keydownListeners);
"background-color": "currentColor",
}
: {
background: `${svgToUri(svg)} no-repeat`,
"background-size": "100% 100%",
"background-color": "transparent",
}),
};
}; };
document.removeEventListener("keyup", onKeyup);
document.removeEventListener("keydown", onKeydown);
document.addEventListener("keyup", onKeyup);
document.addEventListener("keydown", onKeydown);
// mutation listeners observe updates to the dom.
// by default, the criteria for matching a selector
// is very broad. custom opts can be passed when
// adding a listener to reduce handler calls
let documentObserver, let documentObserver,
observerDefaults = { observerDefaults = {
// whether to observe attribute updates
attributes: true, attributes: true,
// whether to observe innerText updates
characterData: true, characterData: true,
// whether to observe added/removed nodes
childList: true, childList: true,
// whether to observe parent/descendant nodes
subtree: true, subtree: true,
}, },
mutationListeners = []; mutationListeners = [];
@ -109,7 +126,6 @@ const _mutations = [],
matchesParent = opts.subtree && target.matches(`${selector} *`), matchesParent = opts.subtree && target.matches(`${selector} *`),
matchesChild = opts.subtree && target.querySelector(selector), matchesChild = opts.subtree && target.querySelector(selector),
matchesAdded = matchesAdded =
// was matching element added?
opts.childList && opts.childList &&
[...(mutation.addedNodes || [])].some((node) => { [...(mutation.addedNodes || [])].some((node) => {
if (!(node instanceof HTMLElement)) node = node.parentElement; if (!(node instanceof HTMLElement)) node = node.parentElement;
@ -136,12 +152,21 @@ const _mutations = [],
document.addEventListener("readystatechange", attachObserver); document.addEventListener("readystatechange", attachObserver);
attachObserver(); attachObserver();
// combines instance-provided element props const kebabToPascalCase = (string) =>
// with a template of element props such that string[0].toUpperCase() +
// island/component/template props handlers string.replace(/-[a-z]/g, (match) => match.slice(1).toUpperCase()).slice(1),
// and styles can be preserved and extended hToString = (type, props, ...children) =>
// rather than overwritten `<${type}${Object.entries(props)
const extendProps = (props, extend) => { .map(([attr, value]) => ` ${attr}="${value}"`)
.join("")}>${children
.map((child) => (Array.isArray(child) ? hToString(...child) : child))
.join("")}</${type}>`,
// combines instance-provided element props
// with a template of element props such that
// island/component/template props handlers
// and styles can be preserved and extended
// rather than overwritten
extendProps = (props, extend) => {
for (const key in extend) { for (const key in extend) {
const { [key]: value } = props; const { [key]: value } = props;
if (typeof extend[key] === "function") { if (typeof extend[key] === "function") {
@ -194,6 +219,53 @@ const extendProps = (props, extend) => {
}, },
html = htm.bind(h); html = htm.bind(h);
const iconPattern = /^i-((?:\w|-)+)(?:\?(mask|bg|auto))?$/,
svgToUri = (svg) => {
// https://gist.github.com/jennyknuth/222825e315d45a738ed9d6e04c7a88d0
const xlmns = ~svg.indexOf("xmlns")
? "<svg"
: '<svg xmlns="http://www.w3.org/2000/svg"';
return `url("data:image/svg+xml;utf8,${svg
.replace("<svg", xlmns)
.replace(/"/g, "'")
.replace(/%/g, "%25")
.replace(/#/g, "%23")
.replace(/{/g, "%7B")
.replace(/}/g, "%7D")
.replace(/</g, "%3C")
.replace(/>/g, "%3E")
.replace(/\s+/g, " ")
.trim()}")`;
},
// prefer custom preset over @unocss/preset-icons:
// limits icons to single set, avoids loading over
// cdn (otherwise could cause issues when submitting
// to the chrome webstore). also makes custom icon
// handling straightforward
presetIcons = ([, icon, mode]) => {
let svg,
mask = mode === "mask";
if (icon === "notion-enhancer") {
const { iconColour, iconMonochrome } = globalThis.__enhancerApi;
svg = mask ? iconMonochrome : iconColour;
} else {
icon = kebabToPascalCase(icon);
if (!lucide[icon]) return;
const [type, props, children] = lucide[icon];
svg = hToString(type, props, ...children);
}
mask ||= mode !== "bg" && svg.includes("currentColor");
return {
// https://antfu.me/posts/icons-in-pure-css
display: "inline-block",
height: "1em",
width: "1em",
[mask ? "mask" : "background"]: `${svgToUri(svg)} no-repeat`,
[mask ? "mask-size" : "background-size"]: "100% 100%",
"background-color": mask ? "currentColor" : "transparent",
};
};
let _renderedTokens = -1; let _renderedTokens = -1;
const _tokens = new Set(), const _tokens = new Set(),
_stylesheet = html`<style id="__unocss"></style>`, _stylesheet = html`<style id="__unocss"></style>`,
@ -229,6 +301,8 @@ renderStylesheet();
Object.assign((globalThis.__enhancerApi ??= {}), { Object.assign((globalThis.__enhancerApi ??= {}), {
html, html,
extendProps, extendProps,
addKeyListener,
removeKeyListener,
addMutationListener, addMutationListener,
removeMutationListener, removeMutationListener,
}); });

View File

@ -1,6 +1,6 @@
/** /**
* notion-enhancer * notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/) * (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */

70
src/api/state.js Normal file
View File

@ -0,0 +1,70 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
"use strict";
// batch event callbacks to avoid over-handling
// and any conflicts / perf.issues that may
// otherwise result. initial call is immediate,
// following calls are delayed. a wait time of
// ~200ms is recommended (the avg. human visual
// reaction time is ~180-200ms)
const sleep = async (ms) => {
return new Promise((res, rej) => setTimeout(res, ms));
},
debounce = (callback, ms = 200) => {
let delay, update;
const next = () =>
sleep(ms).then(() => {
if (!update) return (delay = undefined);
update(), (update = undefined);
delay = next();
});
return (...args) => {
if (delay) update = callback.bind(this, ...args);
return delay || ((delay = next()), callback(...args));
};
};
// 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;
},
dumpState = () => _state;
Object.assign((globalThis.__enhancerApi ??= {}), {
sleep,
debounce,
setState,
useState,
dumpState,
});

View File

@ -1,6 +1,6 @@
/** /**
* notion-enhancer * notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/) * (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
@ -8,9 +8,10 @@
const IS_ELECTRON = typeof module !== "undefined", const IS_ELECTRON = typeof module !== "undefined",
IS_RENDERER = IS_ELECTRON && process.type === "renderer", IS_RENDERER = IS_ELECTRON && process.type === "renderer",
whenReady = new Promise((res, rej) => { API_LOADED = new Promise((res, rej) => {
(globalThis.__enhancerApi ??= {}).__isReady = res; (globalThis.__enhancerApi ??= {}).__isReady = res;
}); }),
whenReady = (callback) => API_LOADED.then(callback);
// expected values: 'linux', 'win32', 'darwin' (== macos), 'firefox' // expected values: 'linux', 'win32', 'darwin' (== macos), 'firefox'
// and 'chromium' (inc. chromium-based browsers like edge and brave) // and 'chromium' (inc. chromium-based browsers like edge and brave)
@ -159,5 +160,5 @@ Object.assign((globalThis.__enhancerApi ??= {}), {
readJson, readJson,
initDatabase, initDatabase,
reloadApp, reloadApp,
whenReady: (callback) => whenReady.then(callback), whenReady,
}); });

View File

@ -1,140 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
"use strict";
// batch event callbacks to avoid over-handling
// and any conflicts / perf.issues that may
// otherwise result. initial call is immediate,
// following calls are delayed. a wait time of
// ~200ms is recommended (the avg. human visual
// reaction time is ~180-200ms)
const sleep = async (ms) => {
return new Promise((res, rej) => setTimeout(res, ms));
},
debounce = (callback, ms = 200) => {
let delay, update;
const next = () =>
sleep(ms).then(() => {
if (!update) return (delay = undefined);
update(), (update = undefined);
delay = next();
});
return (...args) => {
if (delay) update = callback.bind(this, ...args);
return delay || ((delay = next()), callback(...args));
};
};
// 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;
},
dumpState = () => _state;
let keyListeners = [];
// accelerators approximately match electron accelerators.
// logic used when recording hotkeys in menu matches logic used
// when triggering hotkeys => detection should be reliable.
// default hotkeys using "alt" may trigger an altcode or
// accented character on some keyboard layouts (not recommended).
const modifierAliases = [
["metaKey", ["meta", "os", "win", "cmd", "command"]],
["ctrlKey", ["ctrl", "control"]],
["shiftKey", ["shift"]],
["altKey", ["alt"]],
],
addKeyListener = (accelerator, callback, waitForKeyup = false) => {
if (typeof accelerator === "string") accelerator = accelerator.split("+");
accelerator = accelerator.map((key) => key.toLowerCase());
keyListeners.push([accelerator, callback, waitForKeyup]);
},
removeKeyListener = (callback) => {
keyListeners = keyListeners.filter(([, c]) => c !== callback);
},
handleKeypress = (event, keyListeners) => {
for (const [accelerator, callback] of keyListeners) {
const acceleratorModifiers = [],
combinationTriggered =
accelerator.every((key) => {
for (const [modifier, aliases] of modifierAliases) {
if (aliases.includes(key)) {
acceleratorModifiers.push(modifier);
return true;
}
}
if (key === "space") key = " ";
if (key === "plus") key = "equal";
if (key === "minus") key = "-";
if (key === "\\") key = "backslash";
if (key === ",") key = "comma";
if (key === ".") key = "period";
const keyPressed = [
event.key.toLowerCase(),
event.code.toLowerCase(),
].includes(key);
return keyPressed;
}) &&
modifierAliases.every(([modifier]) => {
// required && used -> matches accelerator
// !required && !used -> matches accelerator
// (required && !used) || (!required && used) -> no match
// differentiates e.g.ctrl + x from ctrl + shift + x
return acceleratorModifiers.includes(modifier) === event[modifier];
});
if (combinationTriggered) callback(event);
}
},
onKeyup = (event) => {
const keyupListeners = keyListeners //
.filter(([, , waitForKeyup]) => waitForKeyup);
handleKeypress(event, keyupListeners);
},
onKeydown = (event) => {
const keydownListeners = keyListeners //
.filter(([, , waitForKeyup]) => !waitForKeyup);
handleKeypress(event, keydownListeners);
};
document.removeEventListener("keyup", onKeyup);
document.removeEventListener("keydown", onKeydown);
document.addEventListener("keyup", onKeyup);
document.addEventListener("keydown", onKeydown);
Object.assign((globalThis.__enhancerApi ??= {}), {
sleep,
debounce,
setState,
useState,
dumpState,
addKeyListener,
removeKeyListener,
});

View File

@ -11,8 +11,8 @@ function View({ id }, ...children) {
// set padding on last child to maintain pad on overflow // set padding on last child to maintain pad on overflow
$view = html`<article $view = html`<article
id=${id} id=${id}
class="notion-enhancer--menu-view h-full w-full min-w-[580px] class="notion-enhancer--menu-view min-h-full w-full
absolute px-[60px] pt-[36px] important:[&>*]:last:pb-[36px]" absolute px-[60px] py-[36px] min-w-[580px]"
> >
${children} ${children}
</article>`; </article>`;

View File

@ -14,8 +14,8 @@ const isElectron = () => {
}; };
if (isElectron()) { if (isElectron()) {
require("./common/system.js"); require("./api/system.js");
require("./common/registry.js"); require("./api/registry.js");
const { enhancerUrl } = globalThis.__enhancerApi, const { enhancerUrl } = globalThis.__enhancerApi,
{ getMods, isEnabled, modDatabase } = globalThis.__enhancerApi; { getMods, isEnabled, modDatabase } = globalThis.__enhancerApi;
@ -49,6 +49,6 @@ if (isElectron()) {
} }
}; };
} else { } else {
import(chrome.runtime.getURL("/common/system.js")) // import(chrome.runtime.getURL("/api/system.js")) //
.then(() => import(chrome.runtime.getURL("/load.mjs"))); .then(() => import(chrome.runtime.getURL("/load.mjs")));
} }

View File

@ -39,12 +39,11 @@ export default (async () => {
// in both situations, modules that attach to // in both situations, modules that attach to
// the dom must be re-imported // the dom must be re-imported
await Promise.all([ await Promise.all([
// i.e. if (not_menu) or (is_menu && not_electron), then import IS_ELECTRON || import(enhancerUrl("common/registry.js")),
!(!IS_MENU || !IS_ELECTRON) || import(enhancerUrl("assets/icons.svg.js")), (IS_ELECTRON && IS_MENU) || import(enhancerUrl("api/state.js")),
!(!IS_MENU || !IS_ELECTRON) || import(enhancerUrl("common/registry.js")), import(enhancerUrl("api/interface.mjs")),
import(enhancerUrl("common/scaffold.mjs")),
import(enhancerUrl("common/events.js")),
]); ]);
globalThis.__enhancerApi.__isReady(globalThis.__enhancerApi); globalThis.__enhancerApi.__isReady(globalThis.__enhancerApi);