rvx
Version:
A signal based rendering library
850 lines • 22.9 kB
JavaScript
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