aboutsummaryrefslogtreecommitdiffstats
path: root/app/dom.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/dom.js')
-rw-r--r--app/dom.js558
1 files changed, 295 insertions, 263 deletions
diff --git a/app/dom.js b/app/dom.js
index 9473bf1..6f693c8 100644
--- a/app/dom.js
+++ b/app/dom.js
@@ -1,27 +1,3 @@
-export class DomOptions {
- attributes;
- children;
- classes;
- id;
- styles;
-
- /**
- * @param {Object} [options]
- * @param {Partial<CSSStyleDeclaration>} [options.styles]
- * @param {string|null} [options.id]
- * @param {string[]} [options.classes]
- * @param {HTMLElement[]} [options.children]
- * @param {Record<string, string>} [options.attributes]
- */
- constructor({ id = "", styles = {}, classes = [], children = [], attributes = {} } = {}) {
- this.attributes = attributes;
- this.children = children;
- this.classes = classes;
- this.id = id;
- this.styles = styles;
- }
-}
-
/**
* Toast notification types
* @enum {string}
@@ -35,351 +11,432 @@ export const ToastType = {
export class Dom {
/**
* Create a `<div>`
- * @param {DomOptions} options
+ * @param {Object} [o]
+ * @param {Partial<CSSStyleDeclaration>} [o.styles]
+ * @param {string} [o.id]
+ * @param {string[]} [o.classes]
+ * @param {HTMLElement[]} [o.children]
+ * @param {Record<string, string>} [o.attributes]
* @returns {HTMLDivElement}
*/
- static div(options = new DomOptions()) {
+ static div(o = {}) {
const div = document.createElement("div");
- Object.assign(div.style, options.styles);
- if (options.id) div.id = options.id;
- for (const cls of options.classes) div.classList.add(cls);
- for (const [k, v] of Object.entries(options.attributes)) div.setAttribute(k, v);
- if (options.children) div.append(...options.children);
+ if (o.styles) Object.assign(div.style, o.styles);
+ if (o.id) div.id = o.id;
+ if (o.classes) for (const cls of o.classes) div.classList.add(cls);
+ if (o.attributes) for (const [k, v] of Object.entries(o.attributes)) div.setAttribute(k, v);
+ if (o.children) div.append(...o.children);
return div;
}
/**
* Create a `<span>`
- * @param {string} text
- * @param {DomOptions} options
+ * @param {Object} o
+ * @param {string} o.text
+ * @param {Partial<CSSStyleDeclaration>} [o.styles]
+ * @param {string} [o.id]
+ * @param {string[]} [o.classes]
+ * @param {HTMLElement[]} [o.children]
+ * @param {Record<string, string>} [o.attributes]
* @returns {HTMLSpanElement}
*/
- static span(text, options = new DomOptions()) {
+ static span(o) {
const span = document.createElement("span");
- Object.assign(span.style, options.styles);
- if (options.id) span.id = options.id;
- for (const cls of options.classes) span.classList.add(cls);
- span.textContent = text;
- for (const [k, v] of Object.entries(options.attributes)) span.setAttribute(k, v);
- if (options.children) span.append(...options.children);
+ if (o.styles) Object.assign(span.style, o.styles);
+ if (o.id) span.id = o.id;
+ if (o.classes) for (const cls of o.classes) span.classList.add(cls);
+ span.textContent = o.text;
+ if (o.attributes) for (const [k, v] of Object.entries(o.attributes)) span.setAttribute(k, v);
+ if (o.children) span.append(...o.children);
return span;
}
/**
* Create a `<button>`
- * @param { string} text
- * @param { (e: Event) => void } onClick
- * @param {DomOptions} o
+ * @param {Object} o
+ * @param {string} [o.text]
+ * @param {(e: Event) => void} [o.onClick]
+ * @param {Partial<CSSStyleDeclaration>} [o.styles]
+ * @param {string} [o.id]
+ * @param {string[]} [o.classes]
+ * @param {HTMLElement[]} [o.children]
+ * @param {Record<string, string>} [o.attributes]
* @returns {HTMLButtonElement}
*/
- static button(text, onClick, o = new DomOptions()) {
+ static button(o = {}) {
const button = document.createElement("button");
- Object.assign(button.style, o.styles);
+ if (o.styles) Object.assign(button.style, o.styles);
if (o.id) button.id = o.id;
- for (const cls of o.classes) button.classList.add(cls);
- if (text) button.textContent = text;
- for (const [k, v] of Object.entries(o.attributes)) button.setAttribute(k, v);
+ if (o.classes) for (const cls of o.classes) button.classList.add(cls);
+ if (o.text) button.textContent = o.text;
+ if (o.attributes) for (const [k, v] of Object.entries(o.attributes)) button.setAttribute(k, v);
if (o.children) button.append(...o.children);
- if (onClick) button.addEventListener("pointerdown", onClick);
+ if (o.onClick) button.addEventListener("pointerdown", o.onClick);
return button;
}
/**
* Create an `<input>`
- * @param { string} type
- * @param { (e: Event) => void } onChange
- * @param { string|number} value
- * @param { string} placeholder
- * @param {DomOptions} [o]
+ * @param {Object} o
+ * @param {string} o.type
+ * @param {(e: Event) => void} [o.onChange]
+ * @param {(e: Event) => void} [o.onInput]
+ * @param {string|number} [o.value]
+ * @param {string} [o.placeholder]
+ * @param {string} [o.min]
+ * @param {string} [o.max]
+ * @param {string} [o.step]
+ * @param {Partial<CSSStyleDeclaration>} [o.styles]
+ * @param {string} [o.id]
+ * @param {string[]} [o.classes]
+ * @param {Record<string, string>} [o.attributes]
* @returns {HTMLInputElement}
*/
- static input(type, onChange, value = "", placeholder = "", o = new DomOptions()) {
+ static input(o) {
const input = document.createElement("input");
- Object.assign(input.style, o.styles);
+ if (o.styles) Object.assign(input.style, o.styles);
if (o.id) input.id = o.id;
- for (const cls of o.classes) input.classList.add(cls);
- for (const [k, v] of Object.entries(o.attributes)) input.setAttribute(k, v);
-
- input.type = type;
- input.placeholder = placeholder;
- input.value = value.toString();
- if (onChange) {
- input.addEventListener("change", onChange);
+ if (o.classes) for (const cls of o.classes) input.classList.add(cls);
+ if (o.attributes) for (const [k, v] of Object.entries(o.attributes)) input.setAttribute(k, v);
+
+ input.type = o.type;
+ if (o.placeholder) input.placeholder = o.placeholder;
+ if (o.value !== undefined) input.value = o.value.toString();
+ if (o.min !== undefined) input.min = o.min;
+ if (o.max !== undefined) input.max = o.max;
+ if (o.step !== undefined) input.step = o.step;
+
+ if (o.onChange) {
+ input.addEventListener("change", o.onChange);
+ }
+ if (o.onInput) {
+ input.addEventListener("input", o.onInput);
}
return input;
}
/**
* Create a `<strong>`
- * @param {string} text
+ * @param {Object} o
+ * @param {string} o.text
+ * @returns {HTMLElement}
*/
- static strong(text) {
+ static strong(o) {
const strong = document.createElement("strong");
- strong.textContent = text;
+ strong.textContent = o.text;
return strong;
}
/**
* Create a `<a>`
- * @param {string} url
- * @param {DomOptions} o
- * @param {string|undefined} text
+ * @param {Object} o
+ * @param {string} o.url
+ * @param {string} [o.text]
+ * @param {Partial<CSSStyleDeclaration>} [o.styles]
+ * @param {string} [o.id]
+ * @param {string[]} [o.classes]
+ * @param {HTMLElement[]} [o.children]
+ * @param {Record<string, string>} [o.attributes]
+ * @returns {HTMLAnchorElement}
*/
- static a(url, o, text = undefined) {
+ static a(o) {
const link = document.createElement("a");
- if (text) link.text = text;
- link.href = url;
- Object.assign(link.style, o.styles);
+ if (o.text) link.text = o.text;
+ link.href = o.url;
+ if (o.styles) Object.assign(link.style, o.styles);
if (o.id) link.id = o.id;
- for (const cls of o.classes) link.classList.add(cls);
- for (const [k, v] of Object.entries(o.attributes)) link.setAttribute(k, v);
+ if (o.classes) for (const cls of o.classes) link.classList.add(cls);
+ if (o.attributes) for (const [k, v] of Object.entries(o.attributes)) link.setAttribute(k, v);
if (o.children) link.append(...o.children);
return link;
}
/**
* Create a `<label>`
- * @param {string} to
- * @param {string} text
- * @param {DomOptions} o
+ * @param {Object} o
+ * @param {string} o.to
+ * @param {string} [o.text]
+ * @param {Partial<CSSStyleDeclaration>} [o.styles]
+ * @param {string} [o.id]
+ * @param {string[]} [o.classes]
+ * @param {HTMLElement[]} [o.children]
+ * @param {Record<string, string>} [o.attributes]
* @returns {HTMLLabelElement}
*/
- static label(to, text, o = new DomOptions()) {
+ static label(o) {
const label = document.createElement("label");
- Object.assign(label.style, o.styles);
+ if (o.styles) Object.assign(label.style, o.styles);
if (o.id) label.id = o.id;
- for (const cls of o.classes) label.classList.add(cls);
- for (const [k, v] of Object.entries(o.attributes)) label.setAttribute(k, v);
+ if (o.classes) for (const cls of o.classes) label.classList.add(cls);
+ if (o.attributes) for (const [k, v] of Object.entries(o.attributes)) label.setAttribute(k, v);
if (o.children) label.append(...o.children);
- label.textContent = text;
- label.htmlFor = to;
+ if (o.text) label.textContent = o.text;
+ label.htmlFor = o.to;
return label;
}
/**
* Create a heading `<h1>`–`<h6>`
- * @param {1|2|3|4|5|6} level
- * @param {string} text
- * @param {DomOptions} o
+ * @param {Object} o
+ * @param {1|2|3|4|5|6} o.level
+ * @param {string} [o.text]
+ * @param {Partial<CSSStyleDeclaration>} [o.styles]
+ * @param {string} [o.id]
+ * @param {string[]} [o.classes]
+ * @param {HTMLElement[]} [o.children]
+ * @param {Record<string, string>} [o.attributes]
* @returns {HTMLHeadingElement}
*/
- static heading(level, text, o = new DomOptions()) {
- const heading = document.createElement(`h${level}`);
- Object.assign(heading.style, o.styles);
+ static heading(o) {
+ const heading = document.createElement(`h${o.level}`);
+ if (o.styles) Object.assign(heading.style, o.styles);
if (o.id) heading.id = o.id;
- for (const cls of o.classes) heading.classList.add(cls);
- if (text) heading.textContent = text;
- for (const [k, v] of Object.entries(o.attributes)) heading.setAttribute(k, v);
+ if (o.classes) for (const cls of o.classes) heading.classList.add(cls);
+ if (o.text) heading.textContent = o.text;
+ if (o.attributes) for (const [k, v] of Object.entries(o.attributes)) heading.setAttribute(k, v);
if (o.children) heading.append(...o.children);
return heading;
}
/**
* Create an `<img>`
- * @param {string} src
- * @param {DomOptions} o
+ * @param {Object} o
+ * @param {string} o.src
+ * @param {Partial<CSSStyleDeclaration>} [o.styles]
+ * @param {string} [o.id]
+ * @param {string[]} [o.classes]
+ * @param {Record<string, string>} [o.attributes]
* @returns {HTMLImageElement}
*/
- static img(src, o = new DomOptions()) {
+ static img(o) {
const img = document.createElement("img");
- Object.assign(img.style, o.styles);
+ if (o.styles) Object.assign(img.style, o.styles);
if (o.id) img.id = o.id;
- for (const cls of o.classes) img.classList.add(cls);
- for (const [k, v] of Object.entries(o.attributes)) img.setAttribute(k, v);
- img.src = src;
+ if (o.classes) for (const cls of o.classes) img.classList.add(cls);
+ if (o.attributes) for (const [k, v] of Object.entries(o.attributes)) img.setAttribute(k, v);
+ img.src = o.src;
return img;
}
/**
* Create a `<select>`
- * @param {string|undefined} selected
- * @param {DomOptions} o
- * @param { (e: Event) => void } onChange
+ * @param {Object} o
+ * @param {string} [o.selected]
+ * @param {(e: Event) => void} [o.onChange]
+ * @param {Partial<CSSStyleDeclaration>} [o.styles]
+ * @param {string} [o.id]
+ * @param {string[]} [o.classes]
+ * @param {HTMLElement[]} [o.children]
+ * @param {Record<string, string>} [o.attributes]
* @returns {HTMLSelectElement}
*/
- static select(selected = undefined, onChange, o = new DomOptions()) {
+ static select(o = {}) {
const select = document.createElement("select");
- Object.assign(select.style, o.styles);
- if (selected !== undefined) select.value = selected;
+ if (o.styles) Object.assign(select.style, o.styles);
+ if (o.selected !== undefined) select.value = o.selected;
if (o.id) select.id = o.id;
- for (const cls of o.classes) select.classList.add(cls);
- for (const [k, v] of Object.entries(o.attributes)) select.setAttribute(k, v);
+ if (o.classes) for (const cls of o.classes) select.classList.add(cls);
+ if (o.attributes) for (const [k, v] of Object.entries(o.attributes)) select.setAttribute(k, v);
if (o.children) select.append(...o.children);
- if (onChange) select.addEventListener("change", onChange);
+ if (o.onChange) select.addEventListener("change", o.onChange);
return select;
}
/**
* Create an `<option>`
- * @param {string} value
- * @param {string} text
- * @param {boolean} [selected=false]
+ * @param {Object} o
+ * @param {string} o.value
+ * @param {string} o.text
+ * @param {boolean} [o.selected]
* @returns {HTMLOptionElement}
*/
- static option(value, text, selected = false) {
+ static option(o) {
const opt = document.createElement("option");
- opt.value = value;
- opt.textContent = text;
- opt.selected = selected;
+ opt.value = o.value;
+ opt.textContent = o.text;
+ opt.selected = o.selected || false;
return opt;
}
/**
* Create a `<p>`
- * @param {string} text
- * @param {DomOptions} o
+ * @param {Object} o
+ * @param {string} [o.text]
+ * @param {Partial<CSSStyleDeclaration>} [o.styles]
+ * @param {string} [o.id]
+ * @param {string[]} [o.classes]
+ * @param {HTMLElement[]} [o.children]
+ * @param {Record<string, string>} [o.attributes]
* @returns {HTMLParagraphElement}
*/
- static p(text, o = new DomOptions()) {
+ static p(o = {}) {
const p = document.createElement("p");
- Object.assign(p.style, o.styles);
+ if (o.styles) Object.assign(p.style, o.styles);
if (o.id) p.id = o.id;
- for (const cls of o.classes) p.classList.add(cls);
- if (text) p.textContent = text;
- for (const [k, v] of Object.entries(o.attributes)) p.setAttribute(k, v);
+ if (o.classes) for (const cls of o.classes) p.classList.add(cls);
+ if (o.text) p.textContent = o.text;
+ if (o.attributes) for (const [k, v] of Object.entries(o.attributes)) p.setAttribute(k, v);
if (o.children) p.append(...o.children);
return p;
}
/**
* Create a dual range input (min and max)
- * @param {number} min - Minimum possible value
- * @param {number} max - Maximum possible value
- * @param {number} currentMin - Current minimum value
- * @param {number} currentMax - Current maximum value
- * @param {number} step - Step size
- * @param {(min: number, max: number) => void} onChange - Callback when range changes
- * @param {DomOptions} options - DOM options
+ * @param {Object} o
+ * @param {number} o.min - Minimum possible value
+ * @param {number} o.max - Maximum possible value
+ * @param {number} o.currentMin - Current minimum value
+ * @param {number} o.currentMax - Current maximum value
+ * @param {number} o.step - Step size
+ * @param {(min: number, max: number) => void} o.onChange - Callback when range changes
+ * @param {Partial<CSSStyleDeclaration>} [o.styles]
+ * @param {string} [o.id]
+ * @param {string[]} [o.classes]
+ * @param {HTMLElement[]} [o.children]
+ * @param {Record<string, string>} [o.attributes]
* @returns {HTMLDivElement}
*/
- static range(min, max, currentMin, currentMax, step, onChange, options = new DomOptions()) {
+ static range(o) {
const container = document.createElement("div");
- Object.assign(container.style, options.styles);
- if (options.id) container.id = options.id;
- for (const cls of options.classes) container.classList.add(cls);
- for (const [k, v] of Object.entries(options.attributes)) container.setAttribute(k, v);
+ if (o.styles) Object.assign(container.style, o.styles);
+ if (o.id) container.id = o.id;
+ if (o.classes) for (const cls of o.classes) container.classList.add(cls);
+ if (o.attributes)
+ for (const [k, v] of Object.entries(o.attributes)) container.setAttribute(k, v);
// Ensure current values are within bounds
- const safeCurrentMin = Math.max(min, Math.min(currentMin, max));
- const safeCurrentMax = Math.max(min, Math.min(currentMax, max));
+ const safeCurrentMin = Math.max(o.min, Math.min(o.currentMin, o.max));
+ const safeCurrentMax = Math.max(o.min, Math.min(o.currentMax, o.max));
// Create track container
- const trackContainer = Dom.div(
- new DomOptions({
- styles: {
- alignItems: "center",
- display: "flex",
- height: "20px",
- margin: "10px 0",
- position: "relative",
- },
- }),
- );
+ const trackContainer = Dom.div({
+ styles: {
+ alignItems: "center",
+ display: "flex",
+ height: "20px",
+ margin: "10px 0",
+ position: "relative",
+ },
+ });
// Create track
- const track = Dom.div(
- new DomOptions({
- styles: {
- background: "#e0e0e0",
- borderRadius: "4px",
- height: "6px",
- position: "relative",
- width: "100%",
- },
- }),
- );
+ const track = Dom.div({
+ styles: {
+ background: "#e0e0e0",
+ borderRadius: "4px",
+ height: "6px",
+ position: "relative",
+ width: "100%",
+ },
+ });
// Create active range
- const activeRange = Dom.div(
- new DomOptions({
- styles: {
- background: "#4caf50",
- borderRadius: "4px",
- height: "100%",
- left: "0%",
- position: "absolute",
- width: "100%",
- },
- }),
- );
-
- // Create min slider
- const minSlider = document.createElement("input");
- minSlider.type = "range";
- minSlider.min = min.toString();
- minSlider.max = max.toString();
- minSlider.step = step.toString();
- minSlider.value = safeCurrentMin.toString();
- Object.assign(minSlider.style, {
- appearance: "none",
- background: "transparent",
- height: "100%",
- pointerEvents: "none",
- position: "absolute",
- width: "100%",
- zIndex: "2",
+ const activeRange = Dom.div({
+ styles: {
+ background: "#4caf50",
+ borderRadius: "4px",
+ height: "100%",
+ left: "0%",
+ position: "absolute",
+ width: "100%",
+ },
+ });
+
+ // Create min slider using Dom.input
+ const minSlider = Dom.input({
+ max: o.max.toString(),
+ min: o.min.toString(),
+ onInput: () => {
+ const minVal = parseInt(minSlider.value, 10);
+ const maxVal = parseInt(maxSlider.value, 10);
+
+ if (minVal > maxVal) {
+ minSlider.value = maxVal.toString();
+ }
+
+ updateActiveRange();
+ o.onChange(parseInt(minSlider.value, 10), parseInt(maxSlider.value, 10));
+ },
+ step: o.step.toString(),
+ styles: {
+ appearance: "none",
+ background: "transparent",
+ height: "100%",
+ pointerEvents: "none",
+ position: "absolute",
+ width: "100%",
+ zIndex: "2",
+ },
+ type: "range",
+ value: safeCurrentMin,
});
minSlider.style.pointerEvents = "auto";
- // Create max slider
- const maxSlider = document.createElement("input");
- maxSlider.type = "range";
- maxSlider.min = min.toString();
- maxSlider.max = max.toString();
- maxSlider.step = step.toString();
- maxSlider.value = safeCurrentMax.toString();
- Object.assign(maxSlider.style, {
- appearance: "none",
- background: "transparent",
- height: "100%",
- pointerEvents: "none",
- position: "absolute",
- width: "100%",
- zIndex: "2",
+ // Create max slider using Dom.input
+ const maxSlider = Dom.input({
+ max: o.max.toString(),
+ min: o.min.toString(),
+ onInput: () => {
+ const minVal = parseInt(minSlider.value, 10);
+ const maxVal = parseInt(maxSlider.value, 10);
+
+ if (maxVal < minVal) {
+ maxSlider.value = minVal.toString();
+ }
+
+ updateActiveRange();
+ o.onChange(parseInt(minSlider.value, 10), parseInt(maxSlider.value, 10));
+ },
+ step: o.step.toString(),
+ styles: {
+ appearance: "none",
+ background: "transparent",
+ height: "100%",
+ pointerEvents: "none",
+ position: "absolute",
+ width: "100%",
+ zIndex: "2",
+ },
+ type: "range",
+ value: safeCurrentMax,
});
maxSlider.style.pointerEvents = "auto";
// Value displays
- const minValueDisplay = Dom.span(
- safeCurrentMin.toString(),
- new DomOptions({
- styles: {
- color: "#0066cc",
- fontSize: "0.85rem",
- fontWeight: "bold",
- },
- }),
- );
-
- const maxValueDisplay = Dom.span(
- safeCurrentMax.toString(),
- new DomOptions({
- styles: {
- color: "#0066cc",
- fontSize: "0.85rem",
- fontWeight: "bold",
- },
- }),
- );
-
- const valueDisplay = Dom.div(
- new DomOptions({
- children: [minValueDisplay, maxValueDisplay],
- styles: {
- display: "flex",
- justifyContent: "space-between",
- marginBottom: "5px",
- },
- }),
- );
+ const minValueDisplay = Dom.span({
+ styles: {
+ color: "#0066cc",
+ fontSize: "0.85rem",
+ fontWeight: "bold",
+ },
+ text: safeCurrentMin.toString(),
+ });
+
+ const maxValueDisplay = Dom.span({
+ styles: {
+ color: "#0066cc",
+ fontSize: "0.85rem",
+ fontWeight: "bold",
+ },
+ text: safeCurrentMax.toString(),
+ });
+
+ const valueDisplay = Dom.div({
+ children: [minValueDisplay, maxValueDisplay],
+ styles: {
+ display: "flex",
+ justifyContent: "space-between",
+ marginBottom: "5px",
+ },
+ });
// Update active range position
const updateActiveRange = () => {
const minVal = parseInt(minSlider.value, 10);
const maxVal = parseInt(maxSlider.value, 10);
- const minPercent = ((minVal - min) / (max - min)) * 100;
- const maxPercent = ((maxVal - min) / (max - min)) * 100;
+ const minPercent = ((minVal - o.min) / (o.max - o.min)) * 100;
+ const maxPercent = ((maxVal - o.min) / (o.max - o.min)) * 100;
activeRange.style.left = `${minPercent}%`;
activeRange.style.width = `${maxPercent - minPercent}%`;
@@ -388,31 +445,6 @@ export class Dom {
maxValueDisplay.textContent = maxVal.toString();
};
- // Event listeners
- minSlider.addEventListener("input", () => {
- const minVal = parseInt(minSlider.value, 10);
- const maxVal = parseInt(maxSlider.value, 10);
-
- if (minVal > maxVal) {
- minSlider.value = maxVal.toString();
- }
-
- updateActiveRange();
- onChange(parseInt(minSlider.value, 10), parseInt(maxSlider.value, 10));
- });
-
- maxSlider.addEventListener("input", () => {
- const minVal = parseInt(minSlider.value, 10);
- const maxVal = parseInt(maxSlider.value, 10);
-
- if (maxVal < minVal) {
- maxSlider.value = minVal.toString();
- }
-
- updateActiveRange();
- onChange(parseInt(minSlider.value, 10), parseInt(maxSlider.value, 10));
- });
-
// Initial update
updateActiveRange();
@@ -423,7 +455,7 @@ export class Dom {
// Assemble container
container.append(valueDisplay, trackContainer);
- if (options.children) container.append(...options.children);
+ if (o.children) container.append(...o.children);
return container;
}