Skip to content

Instantly share code, notes, and snippets.

@SirPepe
Created August 12, 2024 12:40
Show Gist options
  • Save SirPepe/2d9a718b749a2ca6e68a634e2393491f to your computer and use it in GitHub Desktop.
Save SirPepe/2d9a718b749a2ca6e68a634e2393491f to your computer and use it in GitHub Desktop.
"Framework" von Code.Movie
import {
subscribe,
reactive,
define as baseDefine,
connected,
debounce,
} from "@sirpepe/ornament";
import { html, htmlFor, svg, render } from "uhtml/keyed";
import { adoptStyles } from "./styles.js";
// The shadow root has no reason to be open. But it also can't be really private
// because event delegation decorators need a way to attach to it. For this
// reason it lives behind a symbol.
export const SHADOW_ROOT_KEY = Symbol();
class BaseElement extends HTMLElement {
[SHADOW_ROOT_KEY] = this.attachShadow({
mode: "closed",
delegatesFocus: false,
});
html(...args) {
return html(...args);
}
htmlFor(key) {
return (...args) => htmlFor(this, key)(...args);
}
#needsRenderOnConnect = true;
@connected()
#renderOnConnect() {
if (this.#needsRenderOnConnect && this.render) {
this.#render();
this.#needsRenderOnConnect = false;
}
}
@reactive()
@debounce()
#render() {
if (this.render && this.isConnected) {
render(this[SHADOW_ROOT_KEY], this.render());
} else {
this.#needsRenderOnConnect = true;
}
}
}
// @subscribe() for listening on events in the shadow root
function listen(events, selectorOrOptions, options = {}) {
if (typeof selectorOrOptions === "string") {
return subscribe((el) => el[SHADOW_ROOT_KEY], events, {
capture: true,
...options,
predicate: (_, evt) => Boolean(evt.target.closest(selectorOrOptions)),
});
}
return subscribe((el) => el[SHADOW_ROOT_KEY], events, {
capture: true,
...selectorOrOptions,
});
}
// Styles that are meant to apply to ALL components based on the base class, no
// matter what
const DEFAULT_CSS = "*, *::before, *::after { box-sizing: border-box }";
const defaultSheet = new CSSStyleSheet();
defaultSheet.replaceSync(DEFAULT_CSS);
// Ornament's regular @define() with added style support
function define(tagName, options = {}) {
const { sheets = [], css = "" } = options;
return function (target, context) {
return class StyleMixin extends baseDefine(tagName)(target, context) {
constructor() {
super();
// @define() can be applied to elements that don't extend BaseClass and
// that therefore manage their own Shadow DOM
if (this[SHADOW_ROOT_KEY]) {
this[SHADOW_ROOT_KEY].adoptedStyleSheets.push(defaultSheet);
adoptStyles(this[SHADOW_ROOT_KEY], sheets, DEFAULT_CSS + css);
} else {
if (sheets.length > 0 || css !== "") {
throw new Error(
`Can't apply styles to <${this.tagName.toLowerCase()}> because it does not extend the base class`,
);
}
}
}
};
};
}
export { State } from "./state";
export {
// Utilities
BaseElement,
html,
svg,
render,
// Enhanced decorators
define,
listen,
};
export {
// Standard decorators
attr,
init,
prop,
reactive,
connected,
disconnected,
adopted,
formAssociated,
formReset,
formDisabled,
formStateRestore,
subscribe,
debounce,
// Standard transformers
string,
href,
bool,
number,
int,
json,
list,
literal,
any,
event,
} from "@sirpepe/ornament";
function fingerprint(value) {
const bytes = new TextEncoder().encode(value);
let hash = 144_066_263_297_769_815_596_495_629_667_062_367_629n;
for (let i = 0; i < bytes.length; i++) {
hash ^= BigInt(bytes[i]);
hash = BigInt.asUintN(128, hash * 309_485_009_821_345_068_724_781_371n);
}
return hash;
}
// hash -> WeakRef<CSSStyleSheet>
const STYLE_SHEETS = new Map();
const cleanup = new FinalizationRegistry((id) => {
// A sheet with a matching fingerprint could have been revived by the time
// the finalization callback runs
if (!STYLE_SHEETS.get(id)?.deref()) {
STYLE_SHEETS.delete(id);
}
});
async function loadSheet(urlOrCss) {
const id = fingerprint(String(urlOrCss)); // stringify to handle URL objects
const existingSheet = STYLE_SHEETS.get(id)?.deref();
if (existingSheet) {
return existingSheet;
}
const newSheet = new CSSStyleSheet();
cleanup.register(newSheet, id);
STYLE_SHEETS.set(id, new WeakRef(newSheet));
let css;
if (typeof urlOrCss === "object") {
const response = await fetch(urlOrCss, { priority: "high" });
css = await response.text();
} else {
css = urlOrCss;
}
return await newSheet.replace(css);
}
export async function adoptStyles(target, urls, inlineCss) {
const styleSheets = await Promise.all([...urls, inlineCss].map(loadSheet));
// The target component could have been removed in the meantime
target?.adoptedStyleSheets.push(...styleSheets);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment