From a35aebb720e9c38d50bb10ba4ab4a827e6350c3d Mon Sep 17 00:00:00 2001 From: elviscgn <96030189+elviscgn@users.noreply.github.com> Date: Sat, 18 Oct 2025 23:37:10 +0200 Subject: [PATCH 1/2] feat(widget): add ImageCarousel widget for DomWizard with autoplay and controls --- modules/widgets/carousel.js | 262 ++++++++++++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 modules/widgets/carousel.js diff --git a/modules/widgets/carousel.js b/modules/widgets/carousel.js new file mode 100644 index 0000000..f8dc535 --- /dev/null +++ b/modules/widgets/carousel.js @@ -0,0 +1,262 @@ +import { nanoid } from "nanoid"; +import { cssManager } from "dom-wizard"; + +/** + * Creates an ImageCarousel using domManager elements. + * + * @param {Array} images - Array of image objects: { src: 'url', alt: 'description' }. + * @param {Object} [options] - Optional settings. + * @param {number} [options.interval=3000] - Time in ms between automatic slides. + * @param {boolean} [options.showControls=true] - Whether to show arrows and dots. + * @param {Object} [options.styles] - CSS styles for the carousel container. + * @param {Object} [options.imageStyles] - CSS styles for images. + * @param {function} [options.onImageClick] - Called when an image is clicked: (src, index) => {}. + * @throws an error if images is not a non empty array of {src, alt} + * @returns {DomWizardElement} A domManager element representing the carousel. + */ + +cssManager.createCSSRules([ + { + ".slider-container": ` + position: relative; + width: 500px; + height: 300px; + overflow: hidden; + margin: 50px auto; + border: 2px solid #ccc; + border-radius: 10px; + `, + }, + { + ".slider-image": ` + width: 100%; + height: 100%; + object-fit: cover; + display: block; + opacity: 0; + transition: opacity 0.5s ease; + position: absolute; + top: 0; + left: 0; + `, + }, + { + ".slider-image.active": ` + opacity: 1; + position: relative; + `, + }, + { + ".slider-overlay": ` + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + `, + }, + { + ".slider-prev, .slider-next": ` + position: absolute; + top: 50%; + transform: translateY(-50%); + pointer-events: auto; + font-size: 2rem; + color: #fff; + background-color: rgba(0, 0, 0, 0.4); + padding: 0.5rem 1rem; + border-radius: 50%; + cursor: pointer; + user-select: none; + `, + }, + { + ".slider-prev": ` + left: 10px; + `, + }, + { + ".slider-next": ` + right: 10px; + `, + }, + { + ".slider-counter": ` + position: absolute; + bottom: 10px; + right: 10px; + background: rgba(0, 0, 0, 0.5); + color: #fff; + padding: 0.3rem 0.6rem; + border-radius: 5px; + font-size: 0.9rem; + `, + }, + { + ".slider-dots": ` + position: absolute; + bottom: 10px; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 5px; + `, + }, + { + ".slider-dot": ` + width: 12px; + height: 12px; + background-color: rgba(255, 255, 255, 0.5); + border-radius: 50%; + cursor: pointer; + `, + }, + { + ".slider-dot.active": ` + background-color: #fff; + `, + }, +]); + +export const ImageCarousel = (images, options = {}) => { + if (!Array.isArray(images) || images.length === 0) + throw new Error("images must be a non-empty array of { src, alt }"); + + images.forEach((img) => { + if (!img.src || !img.alt) + throw new Error("Each image must have src and alt"); + }); + + const id = "d" + nanoid(); + + const { + interval = 3000, + showControls = true, + showDots = true, + styles = {}, + imageStyles = {}, + onImageClick, + } = options; + + let imagesEl = []; + let currentIndex = 0; + + images.forEach((img, index) => { + console.log("index" + index); + + imagesEl.push({ + tagName: "img", + options: { + src: img.src, + alt: img.alt, + className: + "slider-image" + (currentIndex == index ? " active" : ""), + style: imageStyles, + onclick: () => onImageClick && onImageClick(img.src, index), + }, + }); + }); + + let sliderPrev = {}; + let sliderNext = {}; + let sliderCounter = {}; + + // Control switch + if (showControls) { + sliderPrev = { + text: "❮", + options: { + className: "slider-prev", + onclick: () => { + currentIndex = + (currentIndex - 1 + images.length) % images.length; + updateSlider(); + }, + }, + }; + + sliderNext = { + text: "❯", + options: { + className: "slider-next", + onclick: () => { + currentIndex = (currentIndex + 1) % images.length; + updateSlider(); + }, + }, + }; + + sliderCounter = { + text: `1 / ${images.length}`, + options: { + className: "slider-counter", + }, + }; + } + + let sliderDots = {}; + + // Dots switch + if (showDots) { + const allSliderDots = []; + + const imagesLen = images.length; + + for (let i = 0; i < imagesLen; i++) { + const sliderDot = { + options: { + className: + "slider-dot" + (currentIndex == i ? " active" : ""), + }, + }; + allSliderDots.push(sliderDot); + } + + sliderDots = { + children: allSliderDots, + options: { + className: "slider-dots", + }, + }; + } + + const sliderOverlay = { + children: [sliderPrev, sliderNext, sliderCounter, sliderDots], + options: { className: "slider-overlay" }, + }; + + function updateSlider() { + const slider = document.getElementById(id); + const imageEl = slider.querySelectorAll(".slider-image"); + const counter = slider.querySelector(".slider-counter"); + + imageEl.forEach((img, i) => + img.classList.toggle("active", i === currentIndex) + ); + counter.textContent = `${currentIndex + 1} / ${imageEl.length}`; + + if (showDots) { + const dots = slider.querySelectorAll(".slider-dot"); + dots.forEach((dot, i) => + dot.classList.toggle("active", i === currentIndex) + ); + } + } + + // Automatically scroll through the carousel + setInterval(() => { + currentIndex = (currentIndex + 1) % images.length; + updateSlider(); + }, interval); + + // Finally return Dom Wizard Element + return { + children: [...imagesEl, sliderOverlay], + options: { + id: id, + className: "slider-container", + style: styles, + }, + }; +}; From a4ce0cc2c8089b786045758aa982097fec509a29 Mon Sep 17 00:00:00 2001 From: elviscgn <96030189+elviscgn@users.noreply.github.com> Date: Thu, 23 Oct 2025 08:50:12 +0200 Subject: [PATCH 2/2] feat(widget): add table widget --- modules/widgets/table.js | 137 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 modules/widgets/table.js diff --git a/modules/widgets/table.js b/modules/widgets/table.js new file mode 100644 index 0000000..fcf05c9 --- /dev/null +++ b/modules/widgets/table.js @@ -0,0 +1,137 @@ +/** + * Creates a Table widget using DomWizard elements. + * + * Renders a fully styled and responsive table. Each row and cell can have + * click handlers. Supports custom column definitions and empty state messages. + * + * @param {Array} data - Array of objects representing table rows. + * @param {Array} columns - Defines which columns to display. + * If object: { key: 'propertyName', label: 'Column Label' }. + * @param {Object} [options={}] - Optional configuration. + * @param {Object} [options.styles={}] - CSS style overrides for the table. + * @param {function(Object, number): void} [options.onRowClick] - Called when a row is clicked; receives (rowData, rowIndex). + * @param {function(Object, string, number, number): void} [options.onCellClick] - Called when a cell is clicked; receives (rowData, key, rowIndex, colIndex). + * @param {string} [options.caption] - Adds a caption/title above the table. + * @param {string} [options.emptyMessage="No data available"] - Message to show when `data` is empty. + * + * @returns {DomWizardElement} A DomWizard element representing the table. + * + * @example + * const data = [ + * { name: "Elvis", age: 21 }, + * { name: "Paris", age: 20 }, + * ]; + * const columns = [ + * { key: "name", label: "Full Name" }, + * "age" + * ]; + * + * const table = widget.Table(data, columns, { + * caption: "User List", + * onRowClick: (row) => console.log("Row clicked:", row), + * styles: { border: "1px solid #ccc", width: "100%" }, + * }); + * + * domManager.create(table, document.body); + */ +export const Table = (data, columns, options = {}) => { + const { + styles = {}, + onRowClick, + onCellClick, + caption, + emptyMessage = "No data available", + } = options; + + // Map columns to objects { key, label } + const cols = columns.map((col) => + typeof col === "string" ? { key: col, label: col } : col + ); + + // Table rows + const rows = (data.length ? data : [null]).map((rowData, rowIndex) => { + if (!rowData) { + return { + tagName: "tr", + children: [ + { + tagName: "td", + options: { + textContent: emptyMessage, + colSpan: cols.length, + style: { textAlign: "center", padding: "8px" }, + }, + }, + ], + }; + } + + return { + tagName: "tr", + options: { + onclick: () => onRowClick && onRowClick(rowData, rowIndex), + style: { cursor: onRowClick ? "pointer" : "default" }, + }, + children: cols.map((col, colIndex) => ({ + tagName: "td", + options: { + textContent: rowData[col.key], + onclick: (e) => { + e.stopPropagation(); + onCellClick && + onCellClick(rowData, col.key, rowIndex, colIndex); + }, + style: { padding: "8px", border: "1px solid #ccc" }, + }, + })), + }; + }); + + // Table header + const thead = { + tagName: "thead", + children: [ + { + tagName: "tr", + children: cols.map((col) => ({ + tagName: "th", + options: { + textContent: col.label, + style: { + padding: "8px", + border: "1px solid #ccc", + backgroundColor: "#787878", + textAlign: "left", + }, + }, + })), + }, + ], + }; + + // Table body + const tbody = { tagName: "tbody", children: rows }; + + // Caption element if provided + const captionEl = caption + ? { + tagName: "caption", + options: { + textContent: caption, + style: { + captionSide: "top", + fontWeight: "bold", + marginBottom: "8px", + }, + }, + } + : null; + + return { + tagName: "table", + children: captionEl ? [captionEl, thead, tbody] : [thead, tbody], + options: { + style: { borderCollapse: "collapse", width: "100%", ...styles }, + }, + }; +};