UNPKG

rvx

Version:

A signal based rendering library

850 lines 22.9 kB
import { Context } from "../core/context.js"; import { HTML } from "../core/element-common.js"; import { isVoidTag, resolveNamespaceURI, XMLNS_HTML } from "./internals/element-info.js"; import { htmlEscapeAppendTo } from "./internals/html-escape.js"; import { WINDOW_MARKER } from "./internals/window-marker.js"; const NODE_LENGTH = Symbol("length"); const NODE_APPEND_HTML_TO = Symbol("appendHtmlTo"); class NodeListIterator { #current; constructor(node) { this.#current = node.firstChild; } next() { const current = this.#current; if (current === null) { return { value: null, done: true }; } this.#current = current.nextSibling; return { value: current, done: false }; } } export class NodeList { #node; constructor(node) { this.#node = node; } get length() { return this.#node[NODE_LENGTH](); } forEach(cb, thisArg) { let index = 0; let node = this.#node.firstChild; while (node !== null) { cb.call(thisArg, node, index, this); node = node.nextSibling; index++; } } [Symbol.iterator]() { return new NodeListIterator(this.#node); } values() { return new NodeListIterator(this.#node); } } export class Event { } export const NoopEvent = Event; export class EventTarget { addEventListener() { } removeEventListener() { } dispatchEvent() { throw new Error("dispatching events is not supported"); } } export const NoopEventTarget = EventTarget; export class Document extends EventTarget { get body() { return null; } get activeElement() { return null; } createTextNode(data) { return new Text(data); } createComment(data) { return new Comment(data); } createDocumentFragment() { return new DocumentFragment(); } createElementNS(namespaceURI, tagName) { return new Element(namespaceURI, tagName); } createElement(tagName) { return new Element(HTML, tagName); } } export class Node extends EventTarget { #parent = null; #first = null; #last = null; #prev = null; #next = null; #length = 0; #childNodes = null; get parentNode() { return this.#parent; } get firstChild() { return this.#first; } get lastChild() { return this.#last; } get previousSibling() { return this.#prev; } get nextSibling() { return this.#next; } get childNodes() { if (this.#childNodes === null) { this.#childNodes = new NodeList(this); } return this.#childNodes; } [NODE_LENGTH]() { return this.#length; } [NODE_APPEND_HTML_TO](html) { let child = this.firstChild; while (child !== null) { html = child[NODE_APPEND_HTML_TO](html); child = child.nextSibling; } return html; } contains(node) { if (node === null) { return false; } do { if (node === this) { return true; } node = node.#parent; } while (node !== null); return false; } hasChildNodes() { return this.#length > 0; } removeChild(node) { if (node.#parent !== this) { throw new Error("node is not a child of this node"); } const prev = node.#prev; const next = node.#next; if (prev === null) { this.#first = next; } else { prev.#next = next; } if (next === null) { this.#last = prev; } else { next.#prev = prev; } node.#prev = null; node.#next = null; node.#parent = null; this.#length--; return node; } appendChild(node) { if (node.nodeType === 11) { if (node.#length === 0) { return node; } const prev = this.#last; const first = node.#first; if (prev === null) { this.#first = first; } else { prev.#next = first; } first.#prev = prev; this.#last = node.#last; this.#length += node.#length; let child = first; while (child !== null) { child.#parent = this; child = child.#next; } node.#first = null; node.#last = null; node.#length = 0; return node; } node.#parent?.removeChild(node); const prev = this.#last; if (prev === null) { this.#first = node; } else { prev.#next = node; } node.#prev = prev; node.#parent = this; this.#last = node; this.#length++; return node; } insertBefore(node, ref) { if (ref.#parent !== this) { throw new Error("ref must be a child of this node"); } if (node.nodeType === 11) { if (node.#length === 0) { return node; } const prev = ref.#prev; const first = node.#first; const last = node.#last; if (prev === null) { this.#first = first; } else { prev.#next = first; } first.#prev = prev; last.#next = ref; ref.#prev = last; this.#length += node.#length; let child = first; while (child !== null) { child.#parent = this; child = child.#next; } node.#first = null; node.#last = null; node.#length = 0; return node; } node.#parent?.removeChild(node); const prev = ref.#prev; if (prev === null) { this.#first = node; } else { prev.#next = node; } ref.#prev = node; node.#parent = this; node.#prev = prev; node.#next = ref; this.#length++; return node; } replaceChild(node, ref) { if (ref.#parent !== this) { throw new Error("ref must be a child of this node"); } if (node.nodeType === 11) { if (node.#length === 0) { const prev = ref.#prev; const next = ref.#next; if (prev === null) { this.#first = next; } else { prev.#next = next; } if (next === null) { this.#last = prev; } else { next.#prev = prev; } ref.#parent = null; ref.#prev = null; ref.#next = null; this.#length--; } else { const first = node.#first; const last = node.#last; const prev = ref.#prev; const next = ref.#next; if (prev === null) { this.#first = first; } else { prev.#next = first; first.#prev = prev; } if (next === null) { this.#last = last; } else { next.#prev = last; last.#next = next; } ref.#parent = null; ref.#prev = null; ref.#next = null; this.#length = this.#length - 1 + node.#length; node.#first = null; node.#last = null; node.#length = 0; let child = first; while (child !== null) { child.#parent = this; child = child.#next; } } return ref; } const prev = ref.#prev; const next = ref.#next; if (prev === null) { this.#first = node; } else { prev.#next = node; } if (next === null) { this.#last = node; } else { next.#prev = node; } node.#parent = this; node.#prev = prev; node.#next = next; ref.#parent = null; ref.#prev = null; ref.#next = null; return ref; } remove() { this.#parent?.removeChild(this); } append(...nodes) { for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (typeof node === "string") { this.appendChild(new Text(node)); } else { this.appendChild(node); } } } replaceChildren(...nodes) { let child = this.#first; while (child !== null) { const next = child.#next; child.#parent = null; child.#prev = null; child.#next = null; child = next; } this.#length = 0; this.#first = null; this.#last = null; this.append(...nodes); } get textContent() { let text = ""; let node = this.#first; while (node !== null) { if (node.nodeType !== 8) { text += node.textContent; } node = node.#next; } return text; } get outerHTML() { return this[NODE_APPEND_HTML_TO](""); } } export class DocumentFragment extends Node { static { this.prototype.nodeType = 11; this.prototype.nodeName = "#document-fragment"; } } export const VISIBLE_COMMENTS = new Context(false); export class Comment extends Node { static { this.prototype.nodeType = 8; this.prototype.nodeName = "#comment"; } #data; #visible = VISIBLE_COMMENTS.current; constructor(data) { super(); this.#data = String(data); } get textContent() { return this.#data; } set textContent(data) { this.#data = String(data); } [NODE_APPEND_HTML_TO](html) { if (this.#visible) { return html + "<!--" + this.#data + "-->"; } else { return html; } } } export const NoopComment = Comment; export class Text extends Node { static { this.prototype.nodeType = 3; this.prototype.nodeName = "#text"; } #data; constructor(data) { super(); this.#data = String(data); } get textContent() { return this.#data; } set textContent(data) { this.#data = String(data); } [NODE_APPEND_HTML_TO](html) { return htmlEscapeAppendTo(html, this.#data); } } const ATTR_CHANGED = Symbol("attrChanged"); export class ElementClassList { #attrs; #attr = null; #tokens = null; constructor(attrs) { this.#attrs = attrs; } get length() { return this.#parse().length; } get value() { const attr = this.#attr; if (attr === null || attr.stale) { const tokens = this.#tokens; if (tokens === null) { return ""; } let value = ""; for (let i = 0; i < tokens.length; i++) { if (i > 0) { value += " "; } value += tokens[i]; } attr.value = value; attr.stale = false; return value; } return attr.value; } #parse() { let tokens = this.#tokens; if (tokens === null) { const attr = this.#attr; if (attr === null || attr.stale) { tokens = []; } else { tokens = attr.value.split(" "); } this.#tokens = tokens; } return tokens; } #setAttrStale() { const attr = this.#attr; if (attr === null) { this.#attrs.push(this.#attr = { name: "class", value: "", stale: true }); } else { attr.stale = true; } } [ATTR_CHANGED](attr) { this.#attr = attr; this.#tokens = null; } add(...tokens) { const set = this.#parse(); for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; if (!set.includes(token)) { set.push(token); } } this.#setAttrStale(); } contains(token) { return this.#parse().includes(String(token)); } remove(...tokens) { const set = this.#parse(); for (let i = 0; i < tokens.length; i++) { const token = String(tokens[i]); const index = set.indexOf(token); if (index >= 0) { set.splice(index, 1); } } this.#setAttrStale(); } replace(oldToken, newToken) { const set = this.#parse(); const index = set.indexOf(String(oldToken)); if (index >= 0) { set[index] = String(newToken); this.#setAttrStale(); return true; } return false; } toggle(token, force) { token = String(token); const set = this.#parse(); const index = set.indexOf(token); let exists = false; if (force === undefined) { if (index < 0) { set.push(token); exists = true; } else { set.splice(index, 1); } } else if (force) { if (index < 0) { set.push(token); } exists = true; } else if (index >= 0) { set.splice(index, 1); } this.#setAttrStale(); return exists; } values() { return this.#parse()[Symbol.iterator](); } [Symbol.iterator]() { return this.#parse()[Symbol.iterator](); } } export class ElementStyles { #attrs; #attr = null; #props = null; constructor(attrs) { this.#attrs = attrs; } get cssText() { const attr = this.#attr; if (attr === null || attr.stale) { const props = this.#props; if (props === null) { return ""; } let cssText = ""; for (let i = 0; i < props.length; i++) { const prop = props[i]; if (i > 0) { cssText += "; "; } cssText = cssText + prop.name + ": " + prop.value; if (prop.important) { cssText += " !important"; } } attr.stale = false; attr.value = cssText; return cssText; } return attr.value; } #parse() { let props = this.#props; if (props === null) { const attr = this.#attr; if (attr === null || attr.stale || attr.value === "") { this.#props = props = []; } else { throw new Error("style attribute parsing is not supported"); } } return props; } #setAttrStale() { const attr = this.#attr; if (attr === null) { this.#attrs.push(this.#attr = { name: "style", value: "", stale: true }); } else { attr.stale = true; } } [ATTR_CHANGED](attr) { this.#attr = attr; this.#props = null; } setProperty(name, value, priority) { const props = this.#parse(); for (let i = 0; i < props.length; i++) { const prop = props[i]; if (prop.name === name) { prop.value = String(value); prop.important = priority === "important"; this.#setAttrStale(); return; } } props.push({ name, value: String(value), important: priority === "important", }); this.#setAttrStale(); } removeProperty(name) { const props = this.#parse(); for (let i = 0; i < props.length; i++) { const prop = props[i]; if (prop.name === name) { props.splice(i, 1); this.#setAttrStale(); return prop.value; } } return ""; } getPropertyValue(name) { const props = this.#parse(); for (let i = 0; i < props.length; i++) { const prop = props[i]; if (prop.name === name) { return prop.value; } } return ""; } } export class Element extends Node { static { this.prototype.nodeType = 1; } #xmlns; #namespaceURI; #void; #tagName; #attrs = []; #classList = new ElementClassList(this.#attrs); #styles = new ElementStyles(this.#attrs); constructor(namespaceURI, tagName) { super(); this.#xmlns = resolveNamespaceURI(namespaceURI); this.#namespaceURI = namespaceURI; this.#tagName = tagName; } get tagName() { return this.#tagName; } get nodeName() { return this.#tagName; } get namespaceURI() { return this.#namespaceURI; } get innerHTML() { let html = ""; let child = this.firstChild; while (child !== null) { html = child[NODE_APPEND_HTML_TO](html); child = child.nextSibling; } return html; } set innerHTML(html) { if (html === "") { this.replaceChildren(); } else { this.replaceChildren(new RawHTML(html)); } } get classList() { if (this.#classList === null) { this.#classList = new ElementClassList(this.#attrs); } return this.#classList; } get style() { if (this.#styles === null) { this.#styles = new ElementStyles(this.#attrs); } return this.#styles; } focus() { } blur() { } #attrChanged(name, attr) { switch (name) { case "class": this.#classList[ATTR_CHANGED](attr); break; case "style": this.#styles[ATTR_CHANGED](attr); break; } } setAttribute(name, value) { const attrs = this.#attrs; for (let i = 0; i < attrs.length; i++) { const attr = attrs[i]; if (attr.name === name) { attr.value = String(value); attr.stale = false; this.#attrChanged(name, attr); return; } } const attr = { name, value: String(value), stale: false, }; attrs.push(attr); this.#attrChanged(name, attr); } removeAttribute(name) { const attrs = this.#attrs; for (let i = 0; i < attrs.length; i++) { const attr = attrs[i]; if (attr.name === name) { attrs.splice(i, 1); this.#attrChanged(name, null); return; } } } toggleAttribute(name, force) { const attrs = this.#attrs; for (let i = 0; i < attrs.length; i++) { const attr = attrs[i]; if (attr.name === name) { if (force === undefined || !force) { attrs.splice(i, 1); this.#attrChanged(name, null); } return; } } if (force === undefined || force) { const attr = { name, value: "", stale: false, }; attrs.push(attr); this.#attrChanged(name, attr); } } getAttribute(name) { const attrs = this.#attrs; for (let i = 0; i < attrs.length; i++) { const attr = attrs[i]; if (attr.name === name) { return this.#resolveAttr(attr); } } return null; } hasAttribute(name) { const attrs = this.#attrs; for (let i = 0; i < attrs.length; i++) { if (attrs[i].name === name) { return true; } } return false; } #resolveAttr(attr) { if (attr.stale) { switch (attr.name) { case "class": return this.#classList.value; case "style": return this.#styles.cssText; default: throw new Error("invalid internal state"); } } return attr.value; } #isVoidTag() { return this.#void ?? (this.#void = isVoidTag(this.#xmlns, this.#tagName)); } [NODE_APPEND_HTML_TO](html) { html = html + "<" + this.#tagName; const attrs = this.#attrs; for (let i = 0; i < attrs.length; i++) { const attr = attrs[i]; html = htmlEscapeAppendTo(html + " " + attr.name + "=\"", this.#resolveAttr(attr)) + "\""; } if (this.#isVoidTag()) { html += ">"; } else if (this.hasChildNodes() || this.#xmlns === XMLNS_HTML) { html = super[NODE_APPEND_HTML_TO](html + ">") + "</" + this.#tagName + ">"; } else { html += "/>"; } return html; } } export class RawHTML extends Node { static { this.prototype.nodeType = 0; this.prototype.nodeName = "#rvx-dom-raw-html"; } #html; constructor(html) { super(); this.#html = html; } [NODE_APPEND_HTML_TO](html) { return html + this.#html; } } export class Window extends EventTarget { static { this.prototype[WINDOW_MARKER] = true; this.prototype.Comment = Comment; this.prototype.CustomEvent = Event; this.prototype.Document = Document; this.prototype.DocumentFragment = DocumentFragment; this.prototype.Element = Element; this.prototype.Event = Event; this.prototype.Node = Node; this.prototype.Text = Text; } window = this; document = new Document(); } export const WINDOW = new Window(); //# sourceMappingURL=model.js.map