Created
August 12, 2024 12:40
-
-
Save SirPepe/2d9a718b749a2ca6e68a634e2393491f to your computer and use it in GitHub Desktop.
"Framework" von Code.Movie
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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