diff options
Diffstat (limited to 'app/dom.js')
| -rw-r--r-- | app/dom.js | 192 |
1 files changed, 191 insertions, 1 deletions
@@ -125,7 +125,7 @@ export class Dom { * @param {DomOptions} o * @param {string|undefined} text */ - static a(url, o, text) { + static a(url, o, text = undefined) { const link = document.createElement("a"); if (text) link.text = text; link.href = url; @@ -238,4 +238,194 @@ export class Dom { 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 + * @returns {HTMLDivElement} + */ + static range(min, max, currentMin, currentMax, step, onChange, options = new DomOptions()) { + 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); + + // Ensure current values are within bounds + const safeCurrentMin = Math.max(min, Math.min(currentMin, max)); + const safeCurrentMax = Math.max(min, Math.min(currentMax, max)); + + // Create track container + const trackContainer = Dom.div( + new DomOptions({ + 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%", + }, + }), + ); + + // 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", + }); + 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", + }); + 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", + }, + }), + ); + + // Update active range position + const updateActiveRange = () => { + const minVal = parseInt(minSlider.value); + const maxVal = parseInt(maxSlider.value); + + const minPercent = ((minVal - min) / (max - min)) * 100; + const maxPercent = ((maxVal - min) / (max - min)) * 100; + + activeRange.style.left = `${minPercent}%`; + activeRange.style.width = `${maxPercent - minPercent}%`; + + minValueDisplay.textContent = minVal.toString(); + maxValueDisplay.textContent = maxVal.toString(); + }; + + // Event listeners + minSlider.addEventListener("input", () => { + const minVal = parseInt(minSlider.value); + const maxVal = parseInt(maxSlider.value); + + if (minVal > maxVal) { + minSlider.value = maxVal.toString(); + } + + updateActiveRange(); + onChange(parseInt(minSlider.value), parseInt(maxSlider.value)); + }); + + maxSlider.addEventListener("input", () => { + const minVal = parseInt(minSlider.value); + const maxVal = parseInt(maxSlider.value); + + if (maxVal < minVal) { + maxSlider.value = minVal.toString(); + } + + updateActiveRange(); + onChange(parseInt(minSlider.value), parseInt(maxSlider.value)); + }); + + // Initial update + updateActiveRange(); + + // Assemble track + track.appendChild(activeRange); + track.appendChild(minSlider); + track.appendChild(maxSlider); + trackContainer.appendChild(track); + + // Assemble container + container.appendChild(valueDisplay); + container.appendChild(trackContainer); + + if (options.children) container.append(...options.children); + + return container; + } } |
