soma3/src/util.ts
2025-02-06 15:29:06 -05:00

235 lines
6.2 KiB
TypeScript

import { Component } from ".";
export function stringToElement(template: string): Element {
const parser = new DOMParser();
const doc = parser.parseFromString(template, "text/html");
return doc.body.firstChild as Element;
}
export const isText = (node: Node): node is Text => {
return node.nodeType === Node.TEXT_NODE;
};
export const isTemplate = (node: Node): node is HTMLTemplateElement => {
return node.nodeName === "TEMPLATE";
};
export const isElement = (node: Node): node is Element => {
return node.nodeType === Node.ELEMENT_NODE;
};
export function isObject(value: any): value is object {
return value !== null && typeof value === "object" && !isArray(value);
}
export function isArray(value: any): value is any[] {
return Array.isArray(value);
}
export function checkAndRemoveAttribute(el: Element, attrName: string): string | null {
// Attempt to get the attribute value
const attributeValue = el.getAttribute(attrName);
// If attribute exists, remove it from the element
if (attributeValue !== null) {
el.removeAttribute(attrName);
}
// Return the value of the attribute or null if not present
return attributeValue;
}
export interface Slot {
node: Element;
name: string;
}
export interface Template {
targetSlotName: string;
node: HTMLTemplateElement;
}
export function findSlotNodes(element: Element): Slot[] {
const slots: Slot[] = [];
const findSlots = (node: Element) => {
Array.from(node.childNodes).forEach((node) => {
if (isElement(node)) {
if (node.nodeName === "SLOT") {
slots.push({ node, name: node.getAttribute("name") || "default" });
}
if (node.hasChildNodes()) {
findSlots(node);
}
}
});
};
findSlots(element);
return slots;
}
export function findTemplateNodes(element: Element) {
const templates: Template[] = [];
const findTemplates = (element: Element) => {
let defaultContentNodes: Node[] = [];
Array.from(element.childNodes).forEach((node) => {
if (isElement(node) || isText(node)) {
if (isElement(node) && node.nodeName === "TEMPLATE" && isTemplate(node)) {
templates.push({ targetSlotName: node.getAttribute("slot") || "", node });
} else {
// Capture non-template top-level nodes and text nodes for default slot
defaultContentNodes.push(node);
}
}
});
if (defaultContentNodes.length > 0) {
// Create a template element with a default slot
const defaultTemplate = document.createElement("template");
defaultTemplate.setAttribute("slot", "default");
defaultContentNodes.forEach((node) => {
defaultTemplate.content.appendChild(node);
});
templates.push({ targetSlotName: "default", node: defaultTemplate });
}
};
findTemplates(element);
return templates;
}
export const nextTick = async (f?: Function) => {
await new Promise<void>((r) =>
setTimeout((_) =>
requestAnimationFrame((_) => {
f && f();
r();
}),
),
);
};
export function html(strings: TemplateStringsArray, ...values: any[]): string {
// List of valid self-closing tags in HTML
const selfClosingTags = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"];
// Join the strings and values into a single template
let result = strings.reduce((acc, str, i) => acc + str + (values[i] || ""), "");
// Match non-HTML valid self-closing tags
result = result.replace(/<([a-zA-Z][^\s/>]*)\s*([^>]*?)\/>/g, (match, tagName, attributes) => {
// If the tag is a valid self-closing tag, return it as is
if (selfClosingTags.includes(tagName.toLowerCase())) {
return match;
}
// Return the tag as an open/close tag preserving attributes
return `<${tagName} ${attributes}></${tagName}>`;
});
return result;
}
export function toDisplayString(value: unknown) {
return value == null ? "" : isObject(value) ? JSON.stringify(value, null, 2) : String(value);
}
export function insertAfter(newNode: Node, existingNode: Node) {
if (existingNode.nextSibling) {
existingNode.parentNode.insertBefore(newNode, existingNode.nextSibling);
} else {
existingNode?.parentNode?.appendChild(newNode);
}
}
export function insertBefore(newNode: Node, existingNode: Node) {
existingNode.parentNode?.insertBefore(newNode, existingNode);
}
export function isPropAttribute(attrName: string) {
if (attrName.startsWith(".")) {
return true;
}
if (attrName.startsWith("{") && attrName.endsWith("}")) {
return true;
}
return false;
}
export function isSpreadProp(attr: string) {
return attr.startsWith("...");
}
export function isMirrorProp(attr: string) {
return attr.startsWith("{") && attr.endsWith("}");
}
export function isRegularProp(attr: string) {
return attr.startsWith(".");
}
export function isEventAttribute(attrName: string) {
return attrName.startsWith("@");
}
export function componentHasPropByName(name: string, component: Component) {
return Object.keys(component?.props ?? {}).some((prop) => prop === name);
}
export function extractAttributeName(attrName: string) {
return attrName
.replace(/^\.\.\./, "")
.replace(/^\./, "")
.replace(/^{/, "")
.replace(/}$/, "")
.replace(/:bind$/, "");
}
function dashToCamel(str: string) {
return str.toLowerCase().replace(/-([a-z])/g, (g) => g[1].toUpperCase());
}
export function extractPropName(attrName: string) {
return dashToCamel(extractAttributeName(attrName));
}
export function classNames(_: any) {
const classes = [];
for (let i = 0; i < arguments.length; i++) {
const arg = arguments[i];
if (!arg) continue;
const argType = typeof arg;
if (argType === "string" || argType === "number") {
classes.push(arg);
} else if (Array.isArray(arg)) {
if (arg.length) {
const inner = classNames.apply(null, arg);
if (inner) {
classes.push(inner);
}
}
} else if (argType === "object") {
if (arg.toString === Object.prototype.toString) {
for (let key in arg) {
if (Object.hasOwnProperty.call(arg, key) && arg[key]) {
classes.push(key);
}
}
} else {
classes.push(arg.toString());
}
}
}
return classes.join(" ");
}