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 c1d01b395af7f65927130a6d9cc3aeeb2c12d62f Mon Sep 17 00:00:00 2001 From: elviscgn <96030189+elviscgn@users.noreply.github.com> Date: Thu, 23 Oct 2025 07:17:21 +0200 Subject: [PATCH 2/2] feat(widget): add button widget --- modules/widgets/button.js | 45 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 modules/widgets/button.js diff --git a/modules/widgets/button.js b/modules/widgets/button.js new file mode 100644 index 0000000..b05d7c7 --- /dev/null +++ b/modules/widgets/button.js @@ -0,0 +1,45 @@ +/** + * Creates a Button widget using DomWizard elements. + * + * @param {string} text - The text content displayed on the button. + * @param {Object} [options={}] - Optional button configuration. + * @param {function(Event): void} [options.onClick] - Callback triggered when the button is clicked. + * @param {Object} [options.styles={}] - Custom CSS style overrides for the button. + * @param {boolean} [options.disabled=false] - Whether the button is disabled. + * @param {string} [options.type="button"] - Button type attribute ("button", "submit", "reset"). + * @param {string} [options.title] - Tooltip text shown on hover. + * + * @returns {DomWizardElement} A DomWizard element representing the button. + * + * @example + * const saveButton = widget.Button("Save", { + * onClick: () => console.log("Saved!"), + * styles: { padding: "10px 20px", backgroundColor: "#4caf50", color: "#fff" }, + * }); + * + * domManager.create(saveButton, document.body); + */ +export const Button = (text, options = {}) => { + if (typeof text !== "string") + throw new TypeError("Button text must be a string."); + + const { + onClick, + styles = {}, + disabled = false, + type = "button", + title, + } = options; + + return { + tagName: "button", + options: { + type, + textContent: text, + onclick: onClick, + style: styles, + disabled, + title, + }, + }; +};