import { signal, effect, batch, owned, onCleanup } from './signal.js';
function read(value) {
return typeof value === 'function' ? value() : value;
}
export function el(tag, props = {}, children = []) {
const node = document.createElement(tag);
for (const [name, value] of Object.entries(props)) {
if (name.startsWith('on')) {
on(node, name.slice(2).toLowerCase(), value);
} else if (name === 'style' && typeof value === 'object') {
for (const [prop, v] of Object.entries(value)) {
effect(() => {
node.style.setProperty(prop, String(read(v)));
});
}
} else if (typeof value === 'function') {
effect(() => setAttribute(node, name, value()));
} else {
setAttribute(node, name, value);
}
}
appendChildren(node, children);
return node;
}
function setAttribute(node, name, value) {
if (name === 'value' && 'value' in node) {
if (node.value !== String(value)) node.value = String(value);
} else if (name === 'checked' && 'checked' in node) {
node.checked = Boolean(value);
} else if (value === false || value === null || value === undefined) {
node.removeAttribute(name);
} else if (value === true) {
node.setAttribute(name, '');
} else {
node.setAttribute(name, String(value));
}
}
function appendChildren(parent, children) {
for (const child of [].concat(children)) {
if (child === null || child === undefined) continue;
if (child instanceof Node) {
parent.appendChild(child);
} else if (typeof child === 'function') {
parent.appendChild(dynamic(child));
} else {
parent.appendChild(document.createTextNode(String(child)));
}
}
}
export function text(getter) {
const node = document.createTextNode('');
effect(() => {
const value = read(getter);
node.nodeValue = value === null || value === undefined ? '' : String(value);
});
return node;
}
export function template(html) {
let content;
return () => {
if (content === undefined) {
const element = document.createElement('template');
element.innerHTML = html;
content = element.content;
}
return content.cloneNode(true);
};
}
export function anchors() {
const fragment = document.createDocumentFragment();
fragment.append(document.createComment(''), document.createComment(''));
return fragment;
}
export function bindText(node, getter) {
effect(() => {
const value = read(getter);
const next = value === null || value === undefined ? '' : String(value);
if (node.nodeValue !== next) node.nodeValue = next;
});
}
const URL_SCHEMES = ['http', 'https', 'mailto', 'tel'];
export function safeUrl(value) {
const url = value === null || value === undefined ? '' : String(value);
const trimmed = url.trimStart();
const colon = trimmed.indexOf(':');
if (colon === -1) return url;
const scheme = trimmed.slice(0, colon);
if (/[/?#]/.test(scheme)) return url;
return URL_SCHEMES.includes(scheme.toLowerCase()) ? url : '';
}
export function bindAttr(node, name, getter) {
effect(() => setAttribute(node, name, read(getter)));
}
export function bindStyle(node, property, getter) {
effect(() => {
node.style.setProperty(property, String(read(getter)));
});
}
export function on(node, event, handler) {
node.addEventListener(event, (e) => {
try {
batch(() => handler(e));
} catch (failure) {
reportError(failure);
}
});
}
export function dynamicInto(start, end, getter) {
effect(() => {
const value = read(getter);
clearBetween(start, end);
const rendered = value instanceof Node ? value : document.createTextNode(String(value ?? ''));
end.parentNode.insertBefore(rendered, end);
});
}
export function dynamic(getter) {
const fragment = anchors();
dynamicInto(fragment.firstChild, fragment.lastChild, getter);
return fragment;
}
export function when(getter, arms) {
const fragment = anchors();
whenInto(fragment.firstChild, fragment.lastChild, getter, arms);
return fragment;
}
export function whenInto(start, end, getter, arms) {
const [fields, setFields] = signal([]);
let currentTag = null;
let disposeArm = null;
onCleanup(() => disposeArm && disposeArm());
effect(() => {
const value = read(getter);
setFields(value.fields ?? []);
if (value.tag === currentTag) return;
const arm = arms[value.tag];
if (arm === undefined) {
throw new Error(
`No arm for variant ${JSON.stringify(value.tag)}. The compiler should have rejected this.`
);
}
currentTag = value.tag;
if (disposeArm !== null) disposeArm();
clearBetween(start, end);
const binders = (value.fields ?? []).map((_, index) => () => fields()[index]);
const [rendered, dispose] = owned(() => arm(...binders));
disposeArm = dispose;
end.parentNode.insertBefore(rendered, end);
});
}
export function ifInto(start, end, condition, render, otherwise) {
let current = null;
let disposeBranch = null;
onCleanup(() => disposeBranch && disposeBranch());
effect(() => {
const taken = Boolean(read(condition));
if (taken === current) return;
current = taken;
if (disposeBranch !== null) disposeBranch();
disposeBranch = null;
clearBetween(start, end);
const branch = taken ? render : otherwise;
if (branch === null || branch === undefined) return;
const [rendered, dispose] = owned(() => branch());
disposeBranch = dispose;
end.parentNode.insertBefore(rendered, end);
});
}
export function variant(tag, ...fields) {
return { tag, fields };
}
export function mount(node, container) {
container.replaceChildren(node);
return node;
}
function clearBetween(start, end) {
let node = start.nextSibling;
while (node && node !== end) {
const next = node.nextSibling;
node.remove();
node = next;
}
}