diff options
| author | Petri Hienonen <petri.hienonen@gmail.com> | 2025-11-08 19:40:00 +0200 |
|---|---|---|
| committer | Petri Hienonen <petri.hienonen@gmail.com> | 2025-11-08 19:40:00 +0200 |
| commit | a9a0070662c2494b37528d27d7420f3da33e749d (patch) | |
| tree | caf97caf333086fee1fc9684e8743f08004a6842 /app/dom.js | |
| parent | 08528a9e05a12e564f2c3776be4c8ae672f5054c (diff) | |
| download | housing-a9a0070662c2494b37528d27d7420f3da33e749d.tar.zst | |
Try to refactor dom
Diffstat (limited to 'app/dom.js')
| -rw-r--r-- | app/dom.js | 706 |
1 files changed, 391 insertions, 315 deletions
@@ -1,12 +1,28 @@ -/** - * @typedef {Object} BaseElementOptions - * @property {Partial<CSSStyleDeclaration>} [styles] - * @property {string} [id] - * @property {string[]} [classes] - * @property {string} [textContent] - * @property {HTMLElement[]} [children] - * @property {Record<string, string>} [attributes] - */ +import { House } from "models"; + +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 @@ -25,129 +41,74 @@ export const ToastType = { export class Dom { /** * Create a `<div>` - * @param {BaseElementOptions} [options={}] + * @param {DomOptions} options * @returns {HTMLDivElement} */ - static div(options = {}) { - const { - styles = {}, - id = "", - classes = [], - textContent = "", - children = [], - attributes = {}, - } = options; - + static div(options) { const div = document.createElement("div"); - Object.assign(div.style, styles); - if (id) div.id = id; - for (const cls of classes) div.classList.add(cls); - if (textContent) div.textContent = textContent; - for (const [k, v] of Object.entries(attributes)) div.setAttribute(k, v); - if (children) div.append(...children); + 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); return div; } /** * Create a `<span>` - * @param {BaseElementOptions} [options={}] + * @param {string} text + * @param {DomOptions} options * @returns {HTMLSpanElement} */ - static span(options = {}) { - const { - styles = {}, - id = "", - classes = [], - textContent = "", - children = [], - attributes = {}, - } = options; - + static span(text, options = new DomOptions()) { const span = document.createElement("span"); - Object.assign(span.style, styles); - if (id) span.id = id; - for (const cls of classes) span.classList.add(cls); - if (textContent) span.textContent = textContent; - for (const [k, v] of Object.entries(attributes)) span.setAttribute(k, v); - if (children) span.append(...children); + 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); return span; } /** * Create a `<button>` - * @param {BaseElementOptions & { onClick?: (e: MouseEvent) => void }} [options={}] + * @param { string} text + * @param { (e: Event) => void } onClick + * @param {DomOptions} o * @returns {HTMLButtonElement} */ - static button(options = {}) { - const { - styles = {}, - id = "", - classes = [], - textContent = "", - children = [], - attributes = {}, - onClick, - } = options; - + static button(text, onClick, o = new DomOptions()) { const button = document.createElement("button"); - Object.assign(button.style, styles); - if (id) button.id = id; - for (const cls of classes) button.classList.add(cls); - if (textContent) button.textContent = textContent; - for (const [k, v] of Object.entries(attributes)) button.setAttribute(k, v); - if (children) button.append(...children); + 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.children) button.append(...o.children); if (onClick) button.addEventListener("click", onClick); return button; } /** - * @typedef {BaseElementOptions & { - * type?: string, - * placeholder?: string, - * value?: string, - * onInput?: (e: InputEvent) => void, - * onChange?: (e: Event) => void - * }} InputOptions - */ - - /** * Create an `<input>` - * @param {InputOptions} [options={}] + * @param { string} type + * @param { (e: Event) => void } onChange + * @param { string} value + * @param { string} placeholder + * @param {DomOptions} [o] * @returns {HTMLInputElement} */ - static input(options = {}) { - const { - styles = {}, - id = "", - classes = [], - textContent = "", - attributes = {}, - type = "text", - placeholder = "", - value = "", - onInput, - onChange, - } = options; - + static input(type, onChange, value = "", placeholder = "", o = new DomOptions()) { const input = document.createElement("input"); - Object.assign(input.style, styles); - if (id) input.id = id; - for (const cls of classes) input.classList.add(cls); - if (textContent) input.textContent = textContent; - for (const [k, v] of Object.entries(attributes)) input.setAttribute(k, v); + 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; - - if (onInput) { - input.addEventListener( - "input", - /** @param {Event} e */ (e) => { - onInput(/** @type {InputEvent} */ (e)); - }, - ); - } if (onChange) { input.addEventListener("change", onChange); } @@ -155,112 +116,71 @@ export class Dom { } /** - * @typedef {BaseElementOptions & { for?: string }} LabelOptions - */ - - /** * Create a `<label>` - * @param {LabelOptions} [options={}] + * @param {string} to + * @param {string} text + * @param {DomOptions} o * @returns {HTMLLabelElement} */ - static label(options = {}) { - const { - styles = {}, - id = "", - classes = [], - textContent = "", - children = [], - attributes = {}, - for: htmlFor = "", - } = options; - + static label(to, text, o = new DomOptions()) { const label = document.createElement("label"); - Object.assign(label.style, styles); - if (id) label.id = id; - for (const cls of classes) label.classList.add(cls); - if (textContent) label.textContent = textContent; - for (const [k, v] of Object.entries(attributes)) label.setAttribute(k, v); - if (children) label.append(...children); - label.htmlFor = htmlFor; + 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.children) label.append(...o.children); + label.textContent = text; + label.htmlFor = to; return label; } /** - * @typedef {BaseElementOptions & { level: 1|2|3|4|5|6 }} HeadingOptions - */ - - /** * Create a heading `<h1>`–`<h6>` - * @param {HeadingOptions["level"]} level - * @param {Omit<HeadingOptions, "level">} [options={}] + * @param {1|2|3|4|5|6} level + * @param {string} text + * @param {DomOptions} o * @returns {HTMLHeadingElement} */ - static heading(level, options = {}) { - const { - styles = {}, - id = "", - classes = [], - textContent = "", - children = [], - attributes = {}, - } = options; - + static heading(level, text, o = new DomOptions()) { const heading = document.createElement(`h${level}`); - Object.assign(heading.style, styles); - if (id) heading.id = id; - for (const cls of classes) heading.classList.add(cls); - if (textContent) heading.textContent = textContent; - for (const [k, v] of Object.entries(attributes)) heading.setAttribute(k, v); - if (children) heading.append(...children); + 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.children) heading.append(...o.children); return heading; } /** - * @typedef {BaseElementOptions & { src?: string }} ImgOptions - */ - - /** * Create an `<img>` - * @param {ImgOptions} [options={}] + * @param {string} src + * @param {DomOptions} o * @returns {HTMLImageElement} */ - static img(options = {}) { - const { styles = {}, id = "", classes = [], attributes = {}, src = "" } = options; - + static img(src, o = new DomOptions()) { const img = document.createElement("img"); - Object.assign(img.style, styles); - if (id) img.id = id; - for (const cls of classes) img.classList.add(cls); - for (const [k, v] of Object.entries(attributes)) img.setAttribute(k, v); + 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; return img; } /** - * @typedef {BaseElementOptions & { onChange?: (e: Event) => void }} SelectOptions - */ - - /** * Create a `<select>` - * @param {SelectOptions} [options={}] + * @param {DomOptions} o + * @param { (e: Event) => void } onChange * @returns {HTMLSelectElement} */ - static select(options = {}) { - const { - styles = {}, - id = "", - classes = [], - children = [], - attributes = {}, - onChange, - } = options; - + static select(onChange, o = new DomOptions()) { const select = document.createElement("select"); - Object.assign(select.style, styles); - if (id) select.id = id; - for (const cls of classes) select.classList.add(cls); - for (const [k, v] of Object.entries(attributes)) select.setAttribute(k, v); - if (children) select.append(...children); + Object.assign(select.style, o.styles); + 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.children) select.append(...o.children); if (onChange) select.addEventListener("change", onChange); return select; } @@ -282,26 +202,18 @@ export class Dom { /** * Create a `<p>` - * @param {BaseElementOptions} [options={}] + * @param {string} text + * @param {DomOptions} o * @returns {HTMLParagraphElement} */ - static p(options = {}) { - const { - styles = {}, - id = "", - classes = [], - textContent = "", - children = [], - attributes = {}, - } = options; - + static p(text, o = new DomOptions()) { const p = document.createElement("p"); - Object.assign(p.style, styles); - if (id) p.id = id; - for (const cls of classes) p.classList.add(cls); - if (textContent) p.textContent = textContent; - for (const [k, v] of Object.entries(attributes)) p.setAttribute(k, v); - if (children) p.append(...children); + 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.children) p.append(...o.children); return p; } @@ -328,20 +240,22 @@ export class Dom { zIndex: "1000", }); - const closeBtn = Dom.button({ - onClick: onClose, - styles: { - background: "none", - border: "none", - color: "#666", - cursor: "pointer", - fontSize: "24px", - position: "absolute", - right: "10px", - top: "10px", - }, - textContent: "×", - }); + const closeBtn = Dom.button( + "x", + onClose, + new DomOptions({ + styles: { + background: "none", + border: "none", + color: "#666", + cursor: "pointer", + fontSize: "24px", + position: "absolute", + right: "10px", + top: "10px", + }, + }), + ); modal.appendChild(closeBtn); modal.addEventListener("close", onClose); @@ -357,41 +271,50 @@ export class Dom { const frag = document.createDocumentFragment(); /* Header */ - const header = Dom.div({ - styles: { - alignItems: "center", - display: "flex", - justifyContent: "space-between", - marginBottom: "20px", - }, - }); - const title = Dom.heading(2, { - styles: { color: "#333", fontSize: "20px", margin: "0" }, - textContent: house.address, - }); - const score = Dom.span({ - styles: { - background: "#e8f5e9", - borderRadius: "4px", - color: "#2e7d32", - fontSize: "16px", - fontWeight: "bold", - padding: "4px 8px", - }, - textContent: `Score: ${house.scores.current.toFixed(1)}`, - }); + const header = Dom.div( + new DomOptions({ + styles: { + alignItems: "center", + display: "flex", + justifyContent: "space-between", + marginBottom: "20px", + }, + }), + ); + + const title = Dom.heading( + 2, + house.address, + new DomOptions({ + styles: { color: "#333", fontSize: "20px", margin: "0" }, + }), + ); + const score = Dom.span( + `Score: ${house.scores.current.toFixed(1)}`, + new DomOptions({ + styles: { + background: "#e8f5e9", + borderRadius: "4px", + color: "#2e7d32", + fontSize: "16px", + fontWeight: "bold", + padding: "4px 8px", + }, + }), + ); header.append(title, score); frag.appendChild(header); - /* Details grid */ - const grid = Dom.div({ - styles: { - display: "grid", - gap: "15px", - gridTemplateColumns: "repeat(2,1fr)", - marginBottom: "20px", - }, - }); + const grid = Dom.div( + new DomOptions({ + styles: { + display: "grid", + gap: "15px", + gridTemplateColumns: "repeat(2,1fr)", + marginBottom: "20px", + }, + }), + ); const details = [ { label: "Price", value: `${house.price} €` }, { label: "Building Type", value: house.buildingType }, @@ -407,49 +330,62 @@ export class Dom { { label: "Rooms", value: house.rooms?.toString() ?? "N/A" }, ]; for (const { label, value } of details) { - const item = Dom.div({ - children: [ - Dom.div({ - styles: { fontSize: "14px", fontWeight: "bold", marginBottom: "4px" }, - textContent: label, - }), - Dom.div({ styles: { color: "#333", fontSize: "14px" }, textContent: value }), - ], - }); + const item = Dom.div( + new DomOptions({ + children: [ + Dom.span( + label, + new DomOptions({ + styles: { fontSize: "14px", fontWeight: "bold", marginBottom: "4px" }, + }), + ), + Dom.span(value, new DomOptions({ styles: { color: "#333", fontSize: "14px" } })), + ], + }), + ); grid.appendChild(item); } frag.appendChild(grid); /* Description */ - const descSect = Dom.div({ styles: { marginBottom: "20px" } }); - const descTitle = Dom.div({ - styles: { fontSize: "14px", fontWeight: "bold", marginBottom: "5px" }, - textContent: "Description", - }); - const descText = Dom.p({ - styles: { color: "#333", fontSize: "14px", lineHeight: "1.4", marginTop: "5px" }, - textContent: house.description ?? "No description available.", - }); + const descSect = Dom.div(new DomOptions({ styles: { marginBottom: "20px" } })); + const descTitle = Dom.span( + "Description", + new DomOptions({ + styles: { fontSize: "14px", fontWeight: "bold", marginBottom: "5px" }, + }), + ); + const descText = Dom.p( + house.description ?? "No description available.", + new DomOptions({ + styles: { color: "#333", fontSize: "14px", lineHeight: "1.4", marginTop: "5px" }, + }), + ); descSect.append(descTitle, descText); frag.appendChild(descSect); /* Images */ if (house.images?.length) { - const imgSect = Dom.div({ styles: { marginBottom: "20px" } }); - const imgTitle = Dom.div({ - styles: { fontSize: "14px", fontWeight: "bold", marginBottom: "10px" }, - textContent: "Images", - }); - const imgCont = Dom.div({ - styles: { display: "flex", gap: "10px", overflowX: "auto", paddingBottom: "5px" }, - }); + const imgSect = Dom.div(new DomOptions({ styles: { marginBottom: "20px" } })); + const imgTitle = Dom.div( + new DomOptions({ + styles: { fontSize: "14px", fontWeight: "bold", marginBottom: "10px" }, + }), + ); + const imgCont = Dom.div( + new DomOptions({ + styles: { display: "flex", gap: "10px", overflowX: "auto", paddingBottom: "5px" }, + }), + ); for (const src of house.images.slice(0, 3)) { imgCont.appendChild( - Dom.img({ - attributes: { loading: "lazy" }, + Dom.img( src, - styles: { borderRadius: "4px", flexShrink: "0", height: "100px" }, - }), + new DomOptions({ + attributes: { loading: "lazy" }, + styles: { borderRadius: "4px", flexShrink: "0", height: "100px" }, + }), + ), ); } imgSect.append(imgTitle, imgCont); @@ -471,26 +407,28 @@ export class Dom { const bg = type === ToastType.error ? "#f44336" : type === ToastType.warning ? "#ff9800" : "#4caf50"; - const toast = Dom.div({ - classes: ["toast", `toast-${type}`], - id: "app-toast", - styles: { - background: bg, - borderRadius: "4px", - boxShadow: "0 4px 12px rgba(0,0,0,0.15)", - color: "white", - fontSize: "14px", - fontWeight: "500", - maxWidth: "300px", - padding: "12px 20px", - position: "fixed", - right: "20px", - top: "20px", - transition: "all .3s ease", - zIndex: "10000", - }, - textContent: message, - }); + const toast = Dom.div( + new DomOptions({ + children: [Dom.p(message)], + classes: ["toast", `toast-${type}`], + id: "app-toast", + styles: { + background: bg, + borderRadius: "4px", + boxShadow: "0 4px 12px rgba(0,0,0,0.15)", + color: "white", + fontSize: "14px", + fontWeight: "500", + maxWidth: "300px", + padding: "12px 20px", + position: "fixed", + right: "20px", + top: "20px", + transition: "all .3s ease", + zIndex: "10000", + }, + }), + ); document.body.appendChild(toast); @@ -520,41 +458,179 @@ export class Dom { * @returns {HTMLElement} */ static slider(id, labelText, weightKey, initialValue, onChange) { - const group = Dom.div({ - styles: { display: "flex", flexDirection: "column", marginBottom: "1rem" }, - }); - - const label = Dom.label({ for: id }); - const output = Dom.span({ - id: `${id}-value`, - styles: { color: "#0066cc", fontSize: "0.85rem", fontWeight: "bold", textAlign: "center" }, - textContent: initialValue.toFixed(1), - }); - - const labelTextSpan = Dom.span({ - styles: { fontSize: "0.85rem" }, - textContent: labelText, - }); + const group = Dom.div( + new DomOptions({ + styles: { display: "flex", flexDirection: "column", marginBottom: "1rem" }, + }), + ); + + const label = Dom.label(id, labelText); + const output = Dom.span( + initialValue.toFixed(1), + new DomOptions({ + id: `${id}-value`, + styles: { color: "#0066cc", fontSize: "0.85rem", fontWeight: "bold", textAlign: "center" }, + }), + ); + + const labelTextSpan = Dom.span( + labelText, + new DomOptions({ + styles: { fontSize: "0.85rem" }, + }), + ); label.append(labelTextSpan, " ", output); - const slider = Dom.input({ - attributes: { max: "1", min: "0", step: "0.1", value: initialValue.toString() }, - id, - onInput: /** @param {Event} e */ (e) => { + const slider = Dom.input( + "range", + (e) => { const target = /** @type {HTMLInputElement} */ (e.target); const val = Number(target.value); output.textContent = val.toFixed(1); onChange(weightKey, val); }, - styles: { - margin: "0.5rem 0", - width: "100%", - }, - type: "range", - }); + "", + "", + new DomOptions({ + attributes: { max: "1", min: "0", step: "0.1", value: initialValue.toString() }, + id, + styles: { + margin: "0.5rem 0", + width: "100%", + }, + }), + ); group.append(label, slider); return group; } + + /** + * Build modal content for a house + * @param {House} house + * @returns {DocumentFragment} + */ + static buildHouseModalContent(house) { + const frag = document.createDocumentFragment(); + + /* Header */ + const header = Dom.div( + new DomOptions({ + styles: { + alignItems: "center", + display: "flex", + justifyContent: "space-between", + marginBottom: "20px", + }, + }), + ); + const title = Dom.heading( + 2, + house.address, + new DomOptions({ + styles: { color: "#333", fontSize: "20px", margin: "0" }, + }), + ); + const score = Dom.span( + `Score: ${house.scores.current}`, + new DomOptions({ + styles: { + background: "#e8f5e9", + borderRadius: "4px", + color: "#2e7d32", + fontSize: "16px", + fontWeight: "bold", + padding: "4px 8px", + }, + }), + ); + header.append(title, score); + frag.appendChild(header); + + /* Details grid */ + const grid = Dom.div( + new DomOptions({ + styles: { + display: "grid", + gap: "15px", + gridTemplateColumns: "repeat(2,1fr)", + marginBottom: "20px", + }, + }), + ); + const details = [ + { label: "Price", value: `€${house.price.toLocaleString()}` }, + { label: "Building Type", value: house.buildingType }, + { label: "Construction Year", value: house.constructionYear?.toString() ?? "N/A" }, + { label: "Living Area", value: `${house.livingArea} m²` }, + { label: "District", value: house.district }, + { label: "Rooms", value: house.rooms?.toString() ?? "N/A" }, + ]; + for (const { label, value } of details) { + const item = Dom.div( + new DomOptions({ + children: [ + Dom.span( + label, + new DomOptions({ + styles: { fontSize: "14px", fontWeight: "bold", marginBottom: "4px" }, + }), + ), + Dom.span(value, new DomOptions({ styles: { color: "#333", fontSize: "14px" } })), + ], + }), + ); + grid.appendChild(item); + } + frag.appendChild(grid); + + /* Description */ + const descSect = Dom.div(new DomOptions({ styles: { marginBottom: "20px" } })); + const descTitle = Dom.span( + "Description", + new DomOptions({ + styles: { fontSize: "14px", fontWeight: "bold", marginBottom: "5px" }, + }), + ); + const descText = Dom.p( + house.description || "No description available.", + new DomOptions({ + styles: { color: "#333", fontSize: "14px", lineHeight: "1.4", marginTop: "5px" }, + }), + ); + descSect.append(descTitle, descText); + frag.appendChild(descSect); + + /* Images */ + if (house.images?.length) { + const imgSect = Dom.div(new DomOptions({ styles: { marginBottom: "20px" } })); + const imgTitle = Dom.span( + "Images", + new DomOptions({ + styles: { fontSize: "14px", fontWeight: "bold", marginBottom: "10px" }, + }), + ); + const imgCont = Dom.div( + new DomOptions({ + styles: { display: "flex", gap: "10px", overflowX: "auto", paddingBottom: "5px" }, + }), + ); + for (const src of house.images.slice(0, 3)) { + imgCont.appendChild( + Dom.img( + src, + new DomOptions({ + attributes: { loading: "lazy" }, + styles: { borderRadius: "4px", flexShrink: "0", height: "100px" }, + }), + ), + ); + } + imgSect.append(imgTitle, imgCont); + frag.appendChild(imgSect); + } + + return frag; + } } |
