Files
byre/web/dist/dom.js
T

111 lines
4.4 KiB
JavaScript

export function h(tag, props = {}, ...children) {
const el = document.createElement(tag);
const { class: className, style, data, ...rest } = props;
if (className) {
el.className = className;
}
if (style) {
el.setAttribute("style", style);
}
for (const [key, value] of Object.entries(data ?? {})) {
el.dataset[key] = value;
}
Object.assign(el, rest);
append(el, ...children);
return el;
}
export function append(el, ...children) {
for (const child of children) {
if (child === null || child === undefined || child === false) {
continue;
}
el.appendChild(typeof child === "object" ? child : document.createTextNode(String(child)));
}
}
export function replace(el, ...children) {
el.replaceChildren();
append(el, ...children);
}
const copyIcon = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5"/><path d="M10.5 5.5v-2a1 1 0 0 0-1-1h-6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2"/></svg>';
const copiedIcon = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8.5l3.5 3.5L13 4.5"/></svg>';
export function copyable(pre) {
const button = h("button", { type: "button", class: "copy", title: "Copy to the clipboard", ariaLabel: "Copy to the clipboard", innerHTML: copyIcon });
button.onclick = async () => {
try {
await navigator.clipboard.writeText(pre.textContent ?? "");
}
catch (err) {
void showAlert(errorText(err));
return;
}
button.innerHTML = copiedIcon;
setTimeout(() => {
button.innerHTML = copyIcon;
}, 1500);
};
return h("div", { class: "copyable" }, pre, button);
}
export function ago(iso) {
const seconds = Math.round((Date.now() - Date.parse(iso)) / 1000);
if (seconds < 60) {
return "just now";
}
if (seconds < 3600) {
return `${Math.floor(seconds / 60)}m ago`;
}
if (seconds < 86400) {
return `${Math.floor(seconds / 3600)}h ago`;
}
return `${Math.floor(seconds / 86400)}d ago`;
}
export function errorText(err) {
return err instanceof Error ? err.message : String(err);
}
function modal(content, buttons, anchor = null) {
const actions = buttons.map((b) => h("button", { type: "submit", value: b.value, class: b.class ?? "", title: b.title ?? "", formNoValidate: b.value === "cancel" }, b.label));
const body = typeof content === "string" ? [h("p", { class: "modal-text" }, content)] : content;
const dialog = h("dialog", { class: "modal" }, h("form", { method: "dialog" }, ...body, h("div", { class: "modal-actions" }, ...actions)));
for (const input of dialog.querySelectorAll("input")) {
input.onkeydown = (e) => {
if (e.key === "Enter") {
e.preventDefault();
actions.at(-1)?.click();
}
};
}
if (anchor) {
const rect = anchor.getBoundingClientRect();
dialog.classList.add("anchored");
dialog.style.top = `${rect.bottom + 4}px`;
dialog.style.right = `${document.documentElement.clientWidth - rect.right}px`;
}
document.body.append(dialog);
return new Promise((resolve) => {
dialog.onclose = () => {
dialog.remove();
resolve(dialog.returnValue);
};
dialog.showModal();
const input = dialog.querySelector("input");
if (input) {
input.focus();
input.setSelectionRange(0, input.value.length, "backward");
return;
}
actions.at(-1)?.focus();
});
}
export async function showAlert(content) {
await modal(content, [{ label: "OK", value: "ok", class: "primary" }]);
}
export async function showConfirm(content, action, danger = false, anchor = null) {
const value = await modal(content, [
{ label: "Cancel", value: "cancel" },
{ label: action, value: "ok", class: danger ? "danger-primary" : "primary" },
], anchor);
return value === "ok";
}
export async function showChoice(content, choices, anchor = null) {
return modal(content, [{ label: "Cancel", value: "cancel" }, ...choices], anchor);
}
//# sourceMappingURL=dom.js.map