From 2313a4e8c9d0096803a2bebd9a62179010ab4b2d Mon Sep 17 00:00:00 2001 From: matthewcsimpson Date: Thu, 19 Mar 2026 11:23:59 -0700 Subject: [PATCH 01/15] feat: rangeslidersimple --- Dist/Functional/RangeSlider.js | 903 ++++++++++++--------------- Dist/Functional/RangeSliderSimple.js | 485 ++++++++++++++ README.md | 15 + __tests__/RangeSlider.test.js | 50 ++ __tests__/RangeSliderSimple.test.js | 58 ++ docs/Functional/CookieConsent.md | 14 +- docs/Functional/CopyToClipboard.md | 15 +- docs/Functional/CountUp.md | 14 +- docs/Functional/DateCountDown.md | 15 +- docs/Functional/FormCheck.md | 14 +- docs/Functional/FormatNumbers.md | 15 +- docs/Functional/Marquee.md | 15 +- docs/Functional/RangeSlider.md | 20 +- docs/Functional/RangeSliderSimple.md | 77 +++ docs/Functional/ReadTime.md | 15 +- docs/Functional/ShareLink.md | 12 +- docs/WebflowOnly/CMSFilter.md | 24 +- docs/WebflowOnly/CMSSelect.md | 19 +- docs/WebflowOnly/HideContainer.md | 13 +- docs/WebflowOnly/MirrorClick.md | 14 +- docs/WebflowOnly/RenderStatic.md | 12 +- docs/WebflowOnly/TabsSlider.md | 12 +- package-lock.json | 4 +- 23 files changed, 1310 insertions(+), 525 deletions(-) create mode 100644 Dist/Functional/RangeSliderSimple.js create mode 100644 __tests__/RangeSlider.test.js create mode 100644 __tests__/RangeSliderSimple.test.js create mode 100644 docs/Functional/RangeSliderSimple.md diff --git a/Dist/Functional/RangeSlider.js b/Dist/Functional/RangeSlider.js index 6d8dd77..664e8e6 100644 --- a/Dist/Functional/RangeSlider.js +++ b/Dist/Functional/RangeSlider.js @@ -5,150 +5,183 @@ * (c) 2023 Jorge Cortez * MIT License * https://github.com/JorchCortez/Weblfow-Trickery + * + * Self-contained: no separate shared script required (backward compatible with single-tag embeds). */ +'use strict'; + +/** @private Core helpers (IIFE keeps globals clean if RangeSliderSimple.js is also on the page) */ +var __WT_RANGE_SLIDER_CORE = (function () { + function validateNumber(value) { + const num = parseFloat(value); + if (isNaN(num)) { + throw new Error(`Invalid number value: ${value}`); + } + return num; + } + + function formatNumber(number) { + return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + } + + function readSliderConfig(sliderEl, prefix) { + const minAttr = sliderEl.getAttribute(`${prefix}-min`); + const sliderMin = minAttr !== null ? validateNumber(minAttr) : 0; + + const maxAttr = sliderEl.getAttribute(`${prefix}-max`); + const sliderMax = maxAttr !== null ? validateNumber(maxAttr) : 100; + + const stepsAttr = sliderEl.getAttribute(`${prefix}-steps`); + const sliderSteps = stepsAttr !== null ? validateNumber(stepsAttr) : 1; + + const minDiffAttr = sliderEl.getAttribute(`${prefix}-mindifference`); + const minDifference = minDiffAttr !== null ? validateNumber(minDiffAttr) : sliderSteps; + + return { + sliderMin, + sliderMax, + sliderSteps, + minDifference, + rightSuffix: sliderEl.getAttribute(`${prefix}-rightsuffix`) || null, + defaultSuffix: sliderEl.getAttribute(`${prefix}-defaultsuffix`) || null, + shouldFormatNumber: sliderEl.getAttribute(`${prefix}-formatnumber`) || null, + }; + } + + function constrainLeftValue(rawValue, rightValueStr, minDifference) { + return Math.min( + parseInt(rawValue, 10), + parseInt(rightValueStr, 10) - minDifference, + ); + } + + function constrainRightValue(rawValue, leftValueStr, minDifference) { + return Math.max( + parseInt(rawValue, 10), + parseInt(leftValueStr, 10) + minDifference, + ); + } + + function setNativeTextInputValue(input, constrainedValue, suspendBegin, suspendEnd) { + if (!input) return; + const valueProp = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); + suspendBegin(); + valueProp.set.call(input, constrainedValue); + suspendEnd(); + } + + function formatStartDisplayContent(constrainedValue, shouldFormatNumberAttr) { + const displayLeft = + shouldFormatNumberAttr === 'true' + ? formatNumber(constrainedValue) + : constrainedValue; + return String(displayLeft); + } + + function formatEndDisplayContent( + constrainedValue, + rawInputValue, + sliderMax, + shouldFormatNumberAttr, + rightSuffix, + defaultSuffix, + ) { + let finalDisplay = + shouldFormatNumberAttr === 'true' + ? formatNumber(constrainedValue) + : constrainedValue; + const rawNum = parseFloat(rawInputValue); + if (rightSuffix && !isNaN(rawNum) && rawNum >= sliderMax) { + return `${finalDisplay}${rightSuffix}`; + } + if (defaultSuffix) { + return `${finalDisplay}${defaultSuffix}`; + } + return String(finalDisplay); + } + + function hookInputValueSync(input, handler, isSuspended) { + if (!input) return; + const valueProp = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); + try { + Object.defineProperty(input, 'value', { + get() { + return valueProp.get.call(this); + }, + set(v) { + valueProp.set.call(this, v); + if (!isSuspended()) { + handler(v); + } + }, + configurable: true, + enumerable: true, + }); + } catch (err) { + // Fallback: rely on 'input'/'change' listeners if defineProperty fails + } + } + + function dispatchInputEvent(element) { + if (element) { + element.dispatchEvent(new Event('input', { bubbles: true })); + } + } + + return { + readSliderConfig, + constrainLeftValue, + constrainRightValue, + setNativeTextInputValue, + formatStartDisplayContent, + formatEndDisplayContent, + hookInputValueSync, + dispatchInputEvent, + }; +}()); + /** * @file RangeSlider.js * @description A customizable dual-handle range slider for selecting value ranges. - * - * Key Features: - * - Dual handles for selecting a range of values - * - Real-time visual updates - * - Form integration with input fields - * - Display elements for current values - * - Customizable thumbs with SVG/image support - * - Mutation observer support for programmatic value changes - * - Configurable min/max values and step size - * - * Required Attributes: - * - wt-rangeslider-element="slider-wrapper": Container element - * - wt-rangeslider-element="slider": Main slider element - * - wt-rangeslider-element="input-left": Left range input - * - wt-rangeslider-element="input-right": Right range input - * - wt-rangeslider-element="thumb-left": Left thumb element - * - wt-rangeslider-element="thumb-right": Right thumb element - * - wt-rangeslider-element="range": Range indicator element - * - * Optional Attributes: - * - wt-rangeslider-min: Minimum value (default: 0) - * - wt-rangeslider-max: Maximum value (default: 100) - * - wt-rangeslider-steps: Step size (default: 1) - * - * Optional Elements: - * - [wt-rangeslider-range="from"]: Form input for start value - * - [wt-rangeslider-range="to"]: Form input for end value - * - [wt-rangeslider-display="from"]: Display element for start value - * - [wt-rangeslider-display="to"]: Display element for end value - * - * @example - * - *
- * - *
0
- *
100
- * - * - * - * - * - * - *
- *
- *
- *
- * - * - *
- *
- * - * @example - * - *
- *
- *
- *
- * - * - * - *
- *
- * - * - * - *
- * - * - *
- *
- * - * Behavior: - * 1. Slider updates in real-time during interaction - * 2. Form inputs update simultaneously with slider movement - * 3. Display elements update in real-time - * 4. Programmatic changes to form inputs trigger slider updates - * 5. Values are constrained within min/max range - * 6. Left value cannot exceed right value minus step size - * 7. Right value cannot be less than left value plus step size + * + * For native-only handles (no custom thumbs), see RangeSliderSimple.js. */ /** * @class RangeSlider - * @classdesc Creates a range slider component with two handles for selecting a range of values. - * The component supports both visual slider interaction and direct input of values. - * - * @param {HTMLElement} wrapper - The container element for the range slider - * @throws {Error} If required elements or attributes are missing + * @param {HTMLElement} wrapper */ class RangeSlider { -/** - * @constructor - * @param {HTMLElement} wrapper - The wrapper element containing all slider components - */ -constructor(wrapper) { - try { - this.wrapper = wrapper; - this.slider = wrapper.querySelector('[wt-rangeslider-element="slider"]'); - // Guard flag to avoid recursive updates when syncing external inputs - this.__suspendExternalSync = false; + constructor(wrapper) { + try { + this.rs = __WT_RANGE_SLIDER_CORE; - if (!this.slider) { - throw new Error('Slider element not found within wrapper'); - } - - // Add required styles - this.addStyles(); - - // Initialize configuration - this.initConfig(); + this.wrapper = wrapper; + this.slider = wrapper.querySelector('[wt-rangeslider-element="slider"]'); + this.__suspendExternalSync = false; - // Initialize elements - this.initElements(); - - // Setup initial state - this.initState(); + if (!this.slider) { + throw new Error('Slider element not found within wrapper'); + } - // Setup event listeners - this.setupEventListeners(); - } catch (err) { - console.error(`RangeSlider initialization failed: ${err.message}`); + this.addStyles(); + this.initConfig(); + this.initElements(); + this.initState(); + this.setupEventListeners(); + } catch (err) { + console.error(`RangeSlider initialization failed: ${err.message}`); + } } -} -/** - * Adds required styles for proper thumb alignment - * @private - */ -addStyles() { - // Inject styles once per document - const existing = document.getElementById('wt-rangeslider-styles'); - if (existing) return; - - const style = document.createElement('style'); - style.id = 'wt-rangeslider-styles'; - style.textContent = ` + addStyles() { + const existing = document.getElementById('wt-rangeslider-styles'); + if (existing) return; + + const style = document.createElement('style'); + style.id = 'wt-rangeslider-styles'; + style.textContent = ` [wt-rangeslider-element="slider"] { position: relative; } @@ -195,430 +228,292 @@ addStyles() { will-change: transform; } `; - document.head.appendChild(style); -} + document.head.appendChild(style); + } -/** - * Initialize slider configuration from attributes - * @private - */ -initConfig() { - const minAttr = this.slider.getAttribute('wt-rangeslider-min'); - this.sliderMin = minAttr !== null ? this.validateNumber(minAttr) : 0; - - const maxAttr = this.slider.getAttribute('wt-rangeslider-max'); - this.sliderMax = maxAttr !== null ? this.validateNumber(maxAttr) : 100; - - const stepsAttr = this.slider.getAttribute('wt-rangeslider-steps'); - this.sliderSteps = stepsAttr !== null ? this.validateNumber(stepsAttr) : 1; - - // Minimum difference between left and right values - const minDiffAttr = this.slider.getAttribute('wt-rangeslider-mindifference'); - this.minDifference = minDiffAttr !== null ? this.validateNumber(minDiffAttr) : this.sliderSteps; - - // Show preffix - this.rightSuffix = - this.slider.getAttribute('wt-rangeslider-rightsuffix') || null; - // Show preffix - this.defaultSuffix = - this.slider.getAttribute('wt-rangeslider-defaultsuffix') || null; - // Show preffix - this.shouldFormatNumber = - this.slider.getAttribute('wt-rangeslider-formatnumber') || null; -} + initConfig() { + const cfg = this.rs.readSliderConfig(this.slider, 'wt-rangeslider'); + this.sliderMin = cfg.sliderMin; + this.sliderMax = cfg.sliderMax; + this.sliderSteps = cfg.sliderSteps; + this.minDifference = cfg.minDifference; + this.rightSuffix = cfg.rightSuffix; + this.defaultSuffix = cfg.defaultSuffix; + this.shouldFormatNumber = cfg.shouldFormatNumber; + } -/** - * Initialize DOM elements - * @private - */ -initElements() { - // Range inputs (form elements) - this.rangeStart = this.wrapper.querySelector( - '[wt-rangeslider-range="from"]', - ); - this.rangeEnd = this.wrapper.querySelector('[wt-rangeslider-range="to"]'); - - // Display elements - this.displayStart = this.wrapper.querySelector( - '[wt-rangeslider-display="from"]', - ); - this.displayEnd = this.wrapper.querySelector( - '[wt-rangeslider-display="to"]', - ); - - // Slider elements - this.inputLeft = this.slider.querySelector( - '[wt-rangeslider-element="input-left"]', - ); - this.inputRight = this.slider.querySelector( - '[wt-rangeslider-element="input-right"]', - ); - this.thumbLeft = this.slider.querySelector( - '[wt-rangeslider-element="thumb-left"]', - ); - this.thumbRight = this.slider.querySelector( - '[wt-rangeslider-element="thumb-right"]', - ); - this.range = this.slider.querySelector('[wt-rangeslider-element="range"]'); - - this.validateRequiredElements(); - this.setupThumbStyles(); -} + initElements() { + this.rangeStart = this.wrapper.querySelector( + '[wt-rangeslider-range="from"]', + ); + this.rangeEnd = this.wrapper.querySelector('[wt-rangeslider-range="to"]'); + + this.displayStart = this.wrapper.querySelector( + '[wt-rangeslider-display="from"]', + ); + this.displayEnd = this.wrapper.querySelector( + '[wt-rangeslider-display="to"]', + ); + + this.inputLeft = this.slider.querySelector( + '[wt-rangeslider-element="input-left"]', + ); + this.inputRight = this.slider.querySelector( + '[wt-rangeslider-element="input-right"]', + ); + this.thumbLeft = this.slider.querySelector( + '[wt-rangeslider-element="thumb-left"]', + ); + this.thumbRight = this.slider.querySelector( + '[wt-rangeslider-element="thumb-right"]', + ); + this.range = this.slider.querySelector('[wt-rangeslider-element="range"]'); + + this.validateRequiredElements(); + this.setupThumbStyles(); + } -/** - * Sets up proper thumb styling to ensure clickable areas align with visuals - * @private - */ -setupThumbStyles() { - const setupThumb = (thumb, input) => { - // Ensure the input's thumb aligns with our custom thumb - const thumbWidth = thumb.offsetWidth || parseInt(getComputedStyle(thumb).width) || 20; - input.style.setProperty('--thumb-width', `${thumbWidth}px`); - - // Apply styles to ensure proper positioning and hit areas - thumb.style.position = 'absolute'; - thumb.style.pointerEvents = 'none'; - - // Create a custom property for the thumb offset - this.slider.style.setProperty('--thumb-offset', `${thumbWidth / 2}px`); - }; + setupThumbStyles() { + const setupThumb = (thumb, input) => { + const thumbWidth = + thumb.offsetWidth || + parseInt(getComputedStyle(thumb).width, 10) || + 20; + input.style.setProperty('--thumb-width', `${thumbWidth}px`); - setupThumb(this.thumbLeft, this.inputLeft); - setupThumb(this.thumbRight, this.inputRight); -} + thumb.style.position = 'absolute'; + thumb.style.pointerEvents = 'none'; -/** - * Initialize slider state - * @private - */ -initState() { - // Configure range inputs - [this.inputLeft, this.inputRight].forEach((input) => { - input.setAttribute('min', this.sliderMin); - input.setAttribute('max', this.sliderMax); - input.setAttribute('step', this.sliderSteps); - input.setAttribute('formnovalidate', ''); - input.setAttribute('data-form-ignore', ''); - }); - - // Set initial values from range inputs if they exist - if (this.rangeStart && this.rangeStart.value) { - this.updateLeftValues(this.rangeStart.value); - } else { - this.updateLeftValues(this.sliderMin); - } + this.slider.style.setProperty('--thumb-offset', `${thumbWidth / 2}px`); + }; - if (this.rangeEnd && this.rangeEnd.value) { - this.updateRightValues(this.rangeEnd.value); - } else { - this.updateRightValues(this.sliderMax); + setupThumb(this.thumbLeft, this.inputLeft); + setupThumb(this.thumbRight, this.inputRight); } -} -/** - * Formats a number with commas as thousand separators - * @param {number} number - The number to format - * @returns {string} The formatted number string - * @private - */ -formatNumber(number) { - return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); -} + initState() { + [this.inputLeft, this.inputRight].forEach((input) => { + input.setAttribute('min', this.sliderMin); + input.setAttribute('max', this.sliderMax); + input.setAttribute('step', this.sliderSteps); + input.setAttribute('formnovalidate', ''); + input.setAttribute('data-form-ignore', ''); + }); -/** - * Updates all visual elements and values for the left handle - * @private - */ -updateLeftValues(value) { - const constrainedValue = Math.min( - parseInt(value), - parseInt(this.inputRight.value) - this.minDifference, - ); - - // Update slider input - this.inputLeft.value = constrainedValue; - - // Update form input - if (this.rangeStart) { - // Avoid recursive reaction to our own programmatic updates - const valueProp = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); - this.__suspendExternalSync = true; - valueProp.set.call(this.rangeStart, constrainedValue); - this.__suspendExternalSync = false; + if (this.rangeStart && this.rangeStart.value) { + this.updateLeftValues(this.rangeStart.value); + } else { + this.updateLeftValues(this.sliderMin); + } + + if (this.rangeEnd && this.rangeEnd.value) { + this.updateRightValues(this.rangeEnd.value); + } else { + this.updateRightValues(this.sliderMax); + } } - // Update display - if (this.displayStart) { - const displayLeft = this.shouldFormatNumber === 'true' ? this.formatNumber(constrainedValue) : constrainedValue; - this.displayStart.textContent = String(displayLeft); + updateLeftValues(value) { + const constrainedValue = this.rs.constrainLeftValue( + value, + this.inputRight.value, + this.minDifference, + ); + + this.inputLeft.value = constrainedValue; + + if (this.rangeStart) { + this.rs.setNativeTextInputValue( + this.rangeStart, + constrainedValue, + () => { + this.__suspendExternalSync = true; + }, + () => { + this.__suspendExternalSync = false; + }, + ); + } + + if (this.displayStart) { + this.displayStart.textContent = this.rs.formatStartDisplayContent( + constrainedValue, + this.shouldFormatNumber, + ); + } + + this.updateThumbPosition( + this.inputLeft, + this.thumbLeft, + this.range, + 'left', + ); } - // Update visual position - this.updateThumbPosition( - this.inputLeft, - this.thumbLeft, - this.range, - 'left', - ); -} - -/** - * Updates all visual elements and values for the right handle - * @private - */ -updateRightValues(value) { - const constrainedValue = Math.max( - parseInt(value), - parseInt(this.inputLeft.value) + this.minDifference, - ); - - // Update slider input - this.inputRight.value = constrainedValue; - - // Update form input - if (this.rangeEnd) { - // Avoid recursive reaction to our own programmatic updates - const valueProp = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); - this.__suspendExternalSync = true; - valueProp.set.call(this.rangeEnd, constrainedValue); - this.__suspendExternalSync = false; + updateRightValues(value) { + const constrainedValue = this.rs.constrainRightValue( + value, + this.inputLeft.value, + this.minDifference, + ); + + this.inputRight.value = constrainedValue; + + if (this.rangeEnd) { + this.rs.setNativeTextInputValue( + this.rangeEnd, + constrainedValue, + () => { + this.__suspendExternalSync = true; + }, + () => { + this.__suspendExternalSync = false; + }, + ); + } + + if (this.displayEnd) { + this.displayEnd.textContent = this.rs.formatEndDisplayContent( + constrainedValue, + value, + this.sliderMax, + this.shouldFormatNumber, + this.rightSuffix, + this.defaultSuffix, + ); + } + + this.updateThumbPosition( + this.inputRight, + this.thumbRight, + this.range, + 'right', + ); } - // Update display - if (this.displayEnd) { - let finalDisplay = this.shouldFormatNumber === 'true' ? this.formatNumber(constrainedValue) : constrainedValue; - - if (this.rightSuffix && value >= this.sliderMax) { - this.displayEnd.textContent = `${finalDisplay}${this.rightSuffix}`; - } else if (this.defaultSuffix) { - this.displayEnd.textContent = `${finalDisplay}${this.defaultSuffix}`; - } else { - this.displayEnd.textContent = String(finalDisplay); - } - } + setupEventListeners() { + this.inputLeft.addEventListener('input', () => { + this.updateLeftValues(this.inputLeft.value); + if (this.rangeStart) { + this.rs.dispatchInputEvent(this.rangeStart); + } + }); - // Update visual position - this.updateThumbPosition( - this.inputRight, - this.thumbRight, - this.range, - 'right', - ); -} + this.inputRight.addEventListener('input', () => { + this.updateRightValues(this.inputRight.value); + if (this.rangeEnd) { + this.rs.dispatchInputEvent(this.rangeEnd); + } + }); -/** - * Sets up event listeners for the range slider - * @private - */ -setupEventListeners() { - // Input events for the slider inputs - this.inputLeft.addEventListener('input', () => { - this.updateLeftValues(this.inputLeft.value); - if (this.rangeStart) { - this.triggerEvent(this.rangeStart); + if (this.rangeStart) { + this.rangeStart.addEventListener('input', (e) => { + this.updateLeftValues(e.target.value); + }); + this.rangeStart.addEventListener('change', (e) => { + this.updateLeftValues(e.target.value); + }); + this.rs.hookInputValueSync( + this.rangeStart, + (val) => { + this.updateLeftValues(val); + }, + () => this.__suspendExternalSync, + ); + } + + if (this.rangeEnd) { + this.rangeEnd.addEventListener('input', (e) => { + this.updateRightValues(e.target.value); + }); + this.rangeEnd.addEventListener('change', (e) => { + this.updateRightValues(e.target.value); + }); + this.rs.hookInputValueSync( + this.rangeEnd, + (val) => { + this.updateRightValues(val); + }, + () => this.__suspendExternalSync, + ); + } } - }); - this.inputRight.addEventListener('input', () => { - this.updateRightValues(this.inputRight.value); - if (this.rangeEnd) { - this.triggerEvent(this.rangeEnd); - } - }); - - // Watch for changes to the form inputs - if (this.rangeStart) { - // React to user typing immediately - this.rangeStart.addEventListener('input', (e) => { - this.updateLeftValues(e.target.value); - }); - this.rangeStart.addEventListener('change', (e) => { - this.updateLeftValues(e.target.value); - }); - // Hook into programmatic value assignments - this.hookInputValueSync(this.rangeStart, (val) => { - this.updateLeftValues(val); - }); + updateThumbPosition(input, thumb, range, side) { + const min = parseInt(input.min, 10); + const max = parseInt(input.max, 10); + const current = parseInt(input.value, 10); + const percent = ((current - min) / (max - min)) * 100; + + if (side === 'left') { + thumb.style.left = `${percent}%`; + thumb.style.transform = 'translateX(-50%)'; + range.style.left = `${percent}%`; + } else { + thumb.style.right = `${100 - percent}%`; + thumb.style.transform = 'translateX(50%)'; + range.style.right = `${100 - percent}%`; + } } - if (this.rangeEnd) { - // React to user typing immediately - this.rangeEnd.addEventListener('input', (e) => { - this.updateRightValues(e.target.value); - }); - this.rangeEnd.addEventListener('change', (e) => { - this.updateRightValues(e.target.value); - }); - // Hook into programmatic value assignments - this.hookInputValueSync(this.rangeEnd, (val) => { - this.updateRightValues(val); - }); + setFrom(value) { + this.updateLeftValues(value); } -} -/** - * Updates the position of a thumb element - * @param {HTMLInputElement} input - The input element - * @param {HTMLElement} thumb - The thumb element - * @param {HTMLElement} range - The range element - * @param {string} side - The side of the slider ('left' or 'right') - * @private - */ -updateThumbPosition(input, thumb, range, side) { - const min = parseInt(input.min); - const max = parseInt(input.max); - const current = parseInt(input.value); - const percent = ((current - min) / (max - min)) * 100; - - // Get the thumb's width to account for its dimensions - const thumbWidth = thumb.offsetWidth || parseInt(getComputedStyle(thumb).width) || 20; - const sliderWidth = this.slider.offsetWidth || parseInt(getComputedStyle(this.slider).width) || 1; - - // Calculate the percentage that represents half the thumb width - const thumbHalfPercent = (thumbWidth / sliderWidth) * 100; - - if (side === 'left') { - thumb.style.left = `${percent}%`; - thumb.style.transform = 'translateX(-50%)'; - range.style.left = `${percent}%`; - } else { - thumb.style.right = `${100 - percent}%`; - thumb.style.transform = 'translateX(50%)'; - range.style.right = `${100 - percent}%`; + setTo(value) { + this.updateRightValues(value); } -} -/** - * Validates that a value is a valid number - * @param {string} value - The value to validate - * @returns {number} The parsed number - * @throws {Error} If the value is not a valid number - */ -validateNumber(value) { - const num = parseFloat(value); - if (isNaN(num)) { - throw new Error(`Invalid number value: ${value}`); + setRange(from, to) { + this.updateLeftValues(from); + this.updateRightValues(to); } - return num; -} -/** - * Triggers an input event on an element - * @param {HTMLElement} element - The element to trigger the event on - * @private - */ -triggerEvent(element) { - if (element) { - element.dispatchEvent(new Event('input', { bubbles: true })); + reset() { + this.setRange(this.sliderMin, this.sliderMax); } -} -/** - * Hook into a text input's value property to react to programmatic assignments - * @param {HTMLInputElement} input - The input to hook - * @param {(val: string|number) => void} handler - Handler to run on assignment - * @private - */ -hookInputValueSync(input, handler) { - if (!input) return; - const valueProp = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); - const self = this; - try { - Object.defineProperty(input, 'value', { - get() { - return valueProp.get.call(this); - }, - set(v) { - valueProp.set.call(this, v); - if (!self.__suspendExternalSync) { - handler(v); + validateRequiredElements() { + const requiredElements = { + inputLeft: this.inputLeft, + inputRight: this.inputRight, + thumbLeft: this.thumbLeft, + thumbRight: this.thumbRight, + range: this.range, + }; + + Object.entries(requiredElements).forEach(([name, element]) => { + if (!element) { + throw new Error(`Required element ${name} is missing`); } - }, - configurable: true, - enumerable: true, }); - } catch (err) { - // Fallback: rely on 'input'/'change' listeners if defineProperty fails } } -/** - * Public API: set left range value - * @param {number|string} value - */ -setFrom(value) { - this.updateLeftValues(value); -} - -/** - * Public API: set right range value - * @param {number|string} value - */ -setTo(value) { - this.updateRightValues(value); -} - -/** - * Public API: set both range values atomically and in correct order - * @param {number|string} from - * @param {number|string} to - */ -setRange(from, to) { - this.updateLeftValues(from); - this.updateRightValues(to); -} - -/** - * Public API: reset slider to configured bounds - */ -reset() { - this.setRange(this.sliderMin, this.sliderMax); -} - -/** - * Validates that all required elements are present - * @private - * @throws {Error} If any required element is missing - */ -validateRequiredElements() { - const requiredElements = { - inputLeft: this.inputLeft, - inputRight: this.inputRight, - thumbLeft: this.thumbLeft, - thumbRight: this.thumbRight, - range: this.range, - }; - - Object.entries(requiredElements).forEach(([name, element]) => { - if (!element) { - throw new Error(`Required element ${name} is missing`); - } - }); -} -} - -/** - * Initialize all range sliders on the page - */ const initializeRangeSlider = () => { try { window.webtricks = window.webtricks || []; - const wrappers = document.querySelectorAll('[wt-rangeslider-element="slider-wrapper"]'); + const wrappers = document.querySelectorAll( + '[wt-rangeslider-element="slider-wrapper"]', + ); if (!wrappers || wrappers.length === 0) return; - wrappers.forEach(wrapper => { + wrappers.forEach((wrapper) => { const instance = new RangeSlider(wrapper); - window.webtricks.push({ 'RangeSlider': instance }); + window.webtricks.push({ RangeSlider: instance }); }); } catch (err) { console.error(`RangeSlider initialization error: ${err.message}`); } }; -// Initialize on DOM ready if (/complete|interactive|loaded/.test(document.readyState)) { initializeRangeSlider(); } else { window.addEventListener('DOMContentLoaded', initializeRangeSlider); -} \ No newline at end of file +} + +if (typeof module !== 'undefined' && module.exports) { + module.exports = { RangeSlider, InitializeRangeSlider: initializeRangeSlider }; +} diff --git a/Dist/Functional/RangeSliderSimple.js b/Dist/Functional/RangeSliderSimple.js new file mode 100644 index 0000000..5a1d554 --- /dev/null +++ b/Dist/Functional/RangeSliderSimple.js @@ -0,0 +1,485 @@ +/*! + * WebTricks — RangeSliderSimple + * Dual native range inputs (no custom thumb DOM). Self-contained (single script tag). + * MIT License + */ + +'use strict'; + +/** @private Duplicated core logic (same behavior as RangeSlider) so this file has no shared dependency. */ +var __WT_RANGE_SLIDER_SIMPLE_CORE = (function () { + function validateNumber(value) { + const num = parseFloat(value); + if (isNaN(num)) { + throw new Error(`Invalid number value: ${value}`); + } + return num; + } + + function formatNumber(number) { + return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + } + + function readSliderConfig(sliderEl, prefix) { + const minAttr = sliderEl.getAttribute(`${prefix}-min`); + const sliderMin = minAttr !== null ? validateNumber(minAttr) : 0; + + const maxAttr = sliderEl.getAttribute(`${prefix}-max`); + const sliderMax = maxAttr !== null ? validateNumber(maxAttr) : 100; + + const stepsAttr = sliderEl.getAttribute(`${prefix}-steps`); + const sliderSteps = stepsAttr !== null ? validateNumber(stepsAttr) : 1; + + const minDiffAttr = sliderEl.getAttribute(`${prefix}-mindifference`); + const minDifference = minDiffAttr !== null ? validateNumber(minDiffAttr) : sliderSteps; + + return { + sliderMin, + sliderMax, + sliderSteps, + minDifference, + rightSuffix: sliderEl.getAttribute(`${prefix}-rightsuffix`) || null, + defaultSuffix: sliderEl.getAttribute(`${prefix}-defaultsuffix`) || null, + shouldFormatNumber: sliderEl.getAttribute(`${prefix}-formatnumber`) || null, + }; + } + + function constrainLeftValue(rawValue, rightValueStr, minDifference) { + return Math.min( + parseInt(rawValue, 10), + parseInt(rightValueStr, 10) - minDifference, + ); + } + + function constrainRightValue(rawValue, leftValueStr, minDifference) { + return Math.max( + parseInt(rawValue, 10), + parseInt(leftValueStr, 10) + minDifference, + ); + } + + function setNativeTextInputValue(input, constrainedValue, suspendBegin, suspendEnd) { + if (!input) return; + const valueProp = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); + suspendBegin(); + valueProp.set.call(input, constrainedValue); + suspendEnd(); + } + + function formatStartDisplayContent(constrainedValue, shouldFormatNumberAttr) { + const displayLeft = + shouldFormatNumberAttr === 'true' + ? formatNumber(constrainedValue) + : constrainedValue; + return String(displayLeft); + } + + function formatEndDisplayContent( + constrainedValue, + rawInputValue, + sliderMax, + shouldFormatNumberAttr, + rightSuffix, + defaultSuffix, + ) { + let finalDisplay = + shouldFormatNumberAttr === 'true' + ? formatNumber(constrainedValue) + : constrainedValue; + const rawNum = parseFloat(rawInputValue); + if (rightSuffix && !isNaN(rawNum) && rawNum >= sliderMax) { + return `${finalDisplay}${rightSuffix}`; + } + if (defaultSuffix) { + return `${finalDisplay}${defaultSuffix}`; + } + return String(finalDisplay); + } + + function hookInputValueSync(input, handler, isSuspended) { + if (!input) return; + const valueProp = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); + try { + Object.defineProperty(input, 'value', { + get() { + return valueProp.get.call(this); + }, + set(v) { + valueProp.set.call(this, v); + if (!isSuspended()) { + handler(v); + } + }, + configurable: true, + enumerable: true, + }); + } catch (err) { + // Fallback: rely on 'input'/'change' listeners if defineProperty fails + } + } + + function dispatchInputEvent(element) { + if (element) { + element.dispatchEvent(new Event('input', { bubbles: true })); + } + } + + return { + readSliderConfig, + constrainLeftValue, + constrainRightValue, + setNativeTextInputValue, + formatStartDisplayContent, + formatEndDisplayContent, + hookInputValueSync, + dispatchInputEvent, + }; +}()); + +const ATTR_PREFIX = 'wt-rangeslidersimple'; + +/** + * Native dual-handle range slider; visible thumbs match browser hit targets. + * @param {HTMLElement} wrapper + */ +class RangeSliderSimple { + constructor(wrapper) { + try { + this.rs = __WT_RANGE_SLIDER_SIMPLE_CORE; + + this.wrapper = wrapper; + this.slider = wrapper.querySelector( + `[${ATTR_PREFIX}-element="slider"]`, + ); + this.__suspendExternalSync = false; + + if (!this.slider) { + throw new Error('Slider element not found within wrapper'); + } + + this.addStyles(); + this.initConfig(); + this.initElements(); + this.initState(); + this.setupEventListeners(); + } catch (err) { + console.error(`RangeSliderSimple initialization failed: ${err.message}`); + } + } + + addStyles() { + const existing = document.getElementById('wt-rangeslidersimple-styles'); + if (existing) return; + + const style = document.createElement('style'); + style.id = 'wt-rangeslidersimple-styles'; + style.textContent = ` + [${ATTR_PREFIX}-element="slider"] { + position: relative; + min-height: 32px; + } + + [${ATTR_PREFIX}-element="input-left"], + [${ATTR_PREFIX}-element="input-right"] { + position: absolute; + left: 0; + width: 100%; + top: 0; + bottom: 0; + margin: auto; + height: 24px; + -webkit-appearance: none; + appearance: none; + background: transparent; + pointer-events: auto; + z-index: 2; + outline: none; + } + + [${ATTR_PREFIX}-element="input-right"] { + z-index: 1; + } + + [${ATTR_PREFIX}-element="input-left"]::-webkit-slider-thumb, + [${ATTR_PREFIX}-element="input-right"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 18px; + height: 18px; + border-radius: 50%; + background: #222; + cursor: pointer; + pointer-events: auto; + } + + [${ATTR_PREFIX}-element="input-left"]::-moz-range-thumb, + [${ATTR_PREFIX}-element="input-right"]::-moz-range-thumb { + width: 18px; + height: 18px; + border-radius: 50%; + background: #222; + cursor: pointer; + border: none; + } + + [${ATTR_PREFIX}-element="input-left"]::-webkit-slider-runnable-track, + [${ATTR_PREFIX}-element="input-right"]::-webkit-slider-runnable-track { + height: 6px; + border-radius: 3px; + background: rgba(0, 0, 0, 0.12); + } + + [${ATTR_PREFIX}-element="input-left"]::-moz-range-track, + [${ATTR_PREFIX}-element="input-right"]::-moz-range-track { + height: 6px; + border-radius: 3px; + background: rgba(0, 0, 0, 0.12); + } + `; + document.head.appendChild(style); + } + + initConfig() { + const cfg = this.rs.readSliderConfig(this.slider, ATTR_PREFIX); + this.sliderMin = cfg.sliderMin; + this.sliderMax = cfg.sliderMax; + this.sliderSteps = cfg.sliderSteps; + this.minDifference = cfg.minDifference; + this.rightSuffix = cfg.rightSuffix; + this.defaultSuffix = cfg.defaultSuffix; + this.shouldFormatNumber = cfg.shouldFormatNumber; + } + + initElements() { + this.rangeStart = this.wrapper.querySelector( + `[${ATTR_PREFIX}-range="from"]`, + ); + this.rangeEnd = this.wrapper.querySelector(`[${ATTR_PREFIX}-range="to"]`); + + this.displayStart = this.wrapper.querySelector( + `[${ATTR_PREFIX}-display="from"]`, + ); + this.displayEnd = this.wrapper.querySelector( + `[${ATTR_PREFIX}-display="to"]`, + ); + + this.inputLeft = this.slider.querySelector( + `[${ATTR_PREFIX}-element="input-left"]`, + ); + this.inputRight = this.slider.querySelector( + `[${ATTR_PREFIX}-element="input-right"]`, + ); + + this.validateRequiredElements(); + } + + initState() { + [this.inputLeft, this.inputRight].forEach((input) => { + input.setAttribute('min', this.sliderMin); + input.setAttribute('max', this.sliderMax); + input.setAttribute('step', this.sliderSteps); + input.setAttribute('formnovalidate', ''); + input.setAttribute('data-form-ignore', ''); + }); + + if (this.rangeStart && this.rangeStart.value) { + this.updateLeftValues(this.rangeStart.value); + } else { + this.updateLeftValues(this.sliderMin); + } + + if (this.rangeEnd && this.rangeEnd.value) { + this.updateRightValues(this.rangeEnd.value); + } else { + this.updateRightValues(this.sliderMax); + } + } + + bringInputToFront(which) { + if (which === 'left') { + this.inputLeft.style.zIndex = '3'; + this.inputRight.style.zIndex = '1'; + } else { + this.inputRight.style.zIndex = '3'; + this.inputLeft.style.zIndex = '2'; + } + } + + updateLeftValues(value) { + const constrainedValue = this.rs.constrainLeftValue( + value, + this.inputRight.value, + this.minDifference, + ); + + this.inputLeft.value = constrainedValue; + + if (this.rangeStart) { + this.rs.setNativeTextInputValue( + this.rangeStart, + constrainedValue, + () => { + this.__suspendExternalSync = true; + }, + () => { + this.__suspendExternalSync = false; + }, + ); + } + + if (this.displayStart) { + this.displayStart.textContent = this.rs.formatStartDisplayContent( + constrainedValue, + this.shouldFormatNumber, + ); + } + } + + updateRightValues(value) { + const constrainedValue = this.rs.constrainRightValue( + value, + this.inputLeft.value, + this.minDifference, + ); + + this.inputRight.value = constrainedValue; + + if (this.rangeEnd) { + this.rs.setNativeTextInputValue( + this.rangeEnd, + constrainedValue, + () => { + this.__suspendExternalSync = true; + }, + () => { + this.__suspendExternalSync = false; + }, + ); + } + + if (this.displayEnd) { + this.displayEnd.textContent = this.rs.formatEndDisplayContent( + constrainedValue, + value, + this.sliderMax, + this.shouldFormatNumber, + this.rightSuffix, + this.defaultSuffix, + ); + } + } + + setupEventListeners() { + const onLeftPointer = () => this.bringInputToFront('left'); + const onRightPointer = () => this.bringInputToFront('right'); + + this.inputLeft.addEventListener('pointerdown', onLeftPointer); + this.inputRight.addEventListener('pointerdown', onRightPointer); + + this.inputLeft.addEventListener('input', () => { + this.updateLeftValues(this.inputLeft.value); + if (this.rangeStart) { + this.rs.dispatchInputEvent(this.rangeStart); + } + }); + + this.inputRight.addEventListener('input', () => { + this.updateRightValues(this.inputRight.value); + if (this.rangeEnd) { + this.rs.dispatchInputEvent(this.rangeEnd); + } + }); + + if (this.rangeStart) { + this.rangeStart.addEventListener('input', (e) => { + this.updateLeftValues(e.target.value); + }); + this.rangeStart.addEventListener('change', (e) => { + this.updateLeftValues(e.target.value); + }); + this.rs.hookInputValueSync( + this.rangeStart, + (val) => { + this.updateLeftValues(val); + }, + () => this.__suspendExternalSync, + ); + } + + if (this.rangeEnd) { + this.rangeEnd.addEventListener('input', (e) => { + this.updateRightValues(e.target.value); + }); + this.rangeEnd.addEventListener('change', (e) => { + this.updateRightValues(e.target.value); + }); + this.rs.hookInputValueSync( + this.rangeEnd, + (val) => { + this.updateRightValues(val); + }, + () => this.__suspendExternalSync, + ); + } + } + + setFrom(value) { + this.updateLeftValues(value); + } + + setTo(value) { + this.updateRightValues(value); + } + + setRange(from, to) { + this.updateLeftValues(from); + this.updateRightValues(to); + } + + reset() { + this.setRange(this.sliderMin, this.sliderMax); + } + + validateRequiredElements() { + const requiredElements = { + inputLeft: this.inputLeft, + inputRight: this.inputRight, + }; + + Object.entries(requiredElements).forEach(([name, element]) => { + if (!element) { + throw new Error(`Required element ${name} is missing`); + } + }); + } +} + +const initializeRangeSliderSimple = () => { + try { + window.webtricks = window.webtricks || []; + const wrappers = document.querySelectorAll( + `[${ATTR_PREFIX}-element="slider-wrapper"]`, + ); + + if (!wrappers || wrappers.length === 0) return; + + wrappers.forEach((wrapper) => { + const instance = new RangeSliderSimple(wrapper); + window.webtricks.push({ RangeSliderSimple: instance }); + }); + } catch (err) { + console.error(`RangeSliderSimple initialization error: ${err.message}`); + } +}; + +if (/complete|interactive|loaded/.test(document.readyState)) { + initializeRangeSliderSimple(); +} else { + window.addEventListener('DOMContentLoaded', initializeRangeSliderSimple); +} + +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + RangeSliderSimple, + InitializeRangeSliderSimple: initializeRangeSliderSimple, + }; +} diff --git a/README.md b/README.md index 582f885..956ee63 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,21 @@ Multiple Scripts: Add as many scripts as you need to your project by referencing ``` + +**Range sliders:** each script is **self-contained** (one tag). Use **`RangeSlider.js`** for custom thumbs or **`RangeSliderSimple.js`** for native thumbs only. Use jsDelivr (not raw `githubusercontent.com`, which often serves `text/plain` and blocks execution). + +``` + +``` + +Native-thumb variant: + +``` + +``` + +If a page uses **both** slider types, you may include **both** scripts; they use separate attribute namespaces (`wt-rangeslider-*` vs `wt-rangeslidersimple-*`). + Ready to Use: Once imported, the scripts initialize automatically, provided the correct HTML attributes are in place. diff --git a/__tests__/RangeSlider.test.js b/__tests__/RangeSlider.test.js new file mode 100644 index 0000000..9f0f9f4 --- /dev/null +++ b/__tests__/RangeSlider.test.js @@ -0,0 +1,50 @@ +/** @jest-environment jsdom */ + +Object.defineProperty(document, 'readyState', { value: 'loading', configurable: true }); + +describe('RangeSlider', () => { + let RangeSlider; + let InitializeRangeSlider; + + beforeEach(() => { + document.body.innerHTML = ''; + window.webtricks = []; + jest.resetModules(); + ({ RangeSlider, InitializeRangeSlider } = require('../Dist/Functional/RangeSlider.js')); + }); + + function mountRangeSlider() { + document.body.innerHTML = ` +
+
+
+
+
+ + +
+
+ `; + } + + test('constructor sets initial values from min/max', () => { + mountRangeSlider(); + const wrapper = document.querySelector( + '[wt-rangeslider-element="slider-wrapper"]', + ); + new RangeSlider(wrapper); + const left = wrapper.querySelector('[wt-rangeslider-element="input-left"]'); + const right = wrapper.querySelector('[wt-rangeslider-element="input-right"]'); + expect(left.value).toBe('0'); + expect(right.value).toBe('100'); + }); + + test('InitializeRangeSlider pushes instance to webtricks', () => { + mountRangeSlider(); + InitializeRangeSlider(); + expect(window.webtricks.some((e) => e.RangeSlider)).toBe(true); + }); +}); diff --git a/__tests__/RangeSliderSimple.test.js b/__tests__/RangeSliderSimple.test.js new file mode 100644 index 0000000..94d4e28 --- /dev/null +++ b/__tests__/RangeSliderSimple.test.js @@ -0,0 +1,58 @@ +/** @jest-environment jsdom */ + +Object.defineProperty(document, 'readyState', { value: 'loading', configurable: true }); + +describe('RangeSliderSimple', () => { + let RangeSliderSimple; + let InitializeRangeSliderSimple; + + beforeEach(() => { + document.body.innerHTML = ''; + window.webtricks = []; + jest.resetModules(); + ({ + RangeSliderSimple, + InitializeRangeSliderSimple, + } = require('../Dist/Functional/RangeSliderSimple.js')); + }); + + function mountSlider() { + document.body.innerHTML = ` +
+
0
+
100
+
+ + +
+
+ `; + } + + test('constructor wires inputs and displays', () => { + mountSlider(); + const wrapper = document.querySelector( + '[wt-rangeslidersimple-element="slider-wrapper"]', + ); + const instance = new RangeSliderSimple(wrapper); + const left = wrapper.querySelector( + '[wt-rangeslidersimple-element="input-left"]', + ); + const right = wrapper.querySelector( + '[wt-rangeslidersimple-element="input-right"]', + ); + expect(left.value).toBe('0'); + expect(right.value).toBe('100'); + expect(instance.sliderMin).toBe(0); + expect(instance.sliderMax).toBe(100); + }); + + test('InitializeRangeSliderSimple pushes instance to webtricks', () => { + mountSlider(); + InitializeRangeSliderSimple(); + expect(window.webtricks.some((e) => e.RangeSliderSimple)).toBe(true); + }); +}); diff --git a/docs/Functional/CookieConsent.md b/docs/Functional/CookieConsent.md index 81aa7d1..b7c57df 100644 --- a/docs/Functional/CookieConsent.md +++ b/docs/Functional/CookieConsent.md @@ -1,12 +1,15 @@ # CookieConsent ## Version + Current Version: 1.0.0 ## Description + CookieConsent is a GDPR-compliant cookie consent management system that provides granular control over cookie preferences and script loading. It supports multiple consent categories, manages consent persistence, and controls script loading based on user preferences. ## Functionality + - Cookie consent banner management - Multiple consent categories - Granular script loading control @@ -17,18 +20,22 @@ CookieConsent is a GDPR-compliant cookie consent management system that provides - Category-based script loading ## Usage + Add the script to your project and include the required attributes on your banner and script elements. ### Installation + ```html ``` ### Required Attributes + - `wt-cookieconsent-element="banner"` - Applied to the main cookie banner container - `wt-cookieconsent-script="category"` - Applied to scripts that require consent ### Optional Elements and Attributes + - `wt-cookieconsent-element="accept-all"` - Accept all cookies button - `wt-cookieconsent-element="accept-necessary"` - Accept necessary cookies only button - `wt-cookieconsent-element="manage-cookies"` - Manage cookies settings button @@ -36,6 +43,7 @@ Add the script to your project and include the required attributes on your banne - `wt-cookieconsent-category="category-name"` - Category checkbox inputs ## Considerations + 1. **GDPR Compliance**: Supports granular consent management 2. **Script Loading**: Automatically handles script loading based on consent 3. **Persistence**: Stores consent in cookies with configurable expiry @@ -45,6 +53,7 @@ Add the script to your project and include the required attributes on your banne ## Examples ### Basic Implementation + ```html
@@ -59,6 +68,7 @@ Add the script to your project and include the required attributes on your banne ``` ### Advanced Implementation with Categories + ```html

Choose your cookie preferences

@@ -102,6 +112,7 @@ Add the script to your project and include the required attributes on your banne ``` ### Inline Script Implementation + ```html ``` ### Required Attributes + - `wt-copycb-element="container"` - Applied to the container element - `wt-copycb-element="trigger"` - Applied to the click trigger element - `wt-copycb-element="target"` - Applied to the element containing text to copy ### Optional Attributes + - `wt-copycb-message="Copied!"` - Custom success message - `wt-copycb-active="is-copy"` - CSS class for active state - `wt-copycb-timeout="2000"` - Duration to show success state (ms) - `wt-copycb-element="texttarget"` - Element within trigger to update with success message ## Considerations + 1. **Clipboard API**: Uses modern navigator.clipboard API 2. **State Management**: Automatically resets to original state 3. **Visual Feedback**: Supports both text and class-based feedback @@ -44,6 +52,7 @@ Add the script to your project and include the required attributes on your copy ## Examples ### Basic Implementation + ```html
@@ -52,6 +61,7 @@ Add the script to your project and include the required attributes on your copy ``` ### Advanced Implementation + ```html
@@ -50,6 +60,7 @@ For multiple pairs: ``` ### Multiple Pairs Implementation + ```html @@ -60,8 +71,9 @@ For multiple pairs: ``` ### Common Use Cases + 1. Synchronized tab switching 2. Multiple button controls 3. Hidden element triggering 4. Form submission from multiple locations -5. Modal/popup controls \ No newline at end of file +5. Modal/popup controls diff --git a/docs/WebflowOnly/RenderStatic.md b/docs/WebflowOnly/RenderStatic.md index 8c71284..d2e9e2d 100644 --- a/docs/WebflowOnly/RenderStatic.md +++ b/docs/WebflowOnly/RenderStatic.md @@ -1,12 +1,15 @@ # RenderStatic ## Version + Current Version: 1.0.0 ## Description + RenderStatic is a Webflow-specific script that allows you to automatically insert cloned elements at specified intervals within a container. It's particularly useful for creating dynamic layouts where you need to repeat certain elements at regular intervals among existing content. ## Functionality + - Automatically clones and inserts elements at specified intervals - Maintains proper spacing between original and cloned elements - Supports multiple different cloneable elements @@ -15,21 +18,26 @@ RenderStatic is a Webflow-specific script that allows you to automatically inser - Smart reinitialization on content changes ## Usage + Add the script to your Webflow project and include the required attributes on your container and cloneable elements. ### Installation + ```html ``` ### Required Attributes + - `wt-renderstatic-element="container"` - Applied to the main container where elements will be inserted - `wt-renderstatic-element="cloneable"` - Applied to elements that will be cloned and inserted ### Optional Attributes + - `wt-renderstatic-gap="1"` - Number of elements to skip before inserting a clone (default: 1) ## Considerations + 1. **DOM Updates**: The script automatically observes container changes and reinitializes when needed 2. **Element Order**: Cloned elements are inserted at calculated positions based on the gap value 3. **Multiple Cloneables**: If multiple cloneable elements are defined, they will be used in sequence @@ -39,6 +47,7 @@ Add the script to your Webflow project and include the required attributes on yo ## Examples ### Basic Implementation + ```html
Original Item 1
@@ -49,6 +58,7 @@ Add the script to your Webflow project and include the required attributes on yo ``` ### With Custom Gap and Multiple Cloneables + ```html
Original Item 1
@@ -58,4 +68,4 @@ Add the script to your Webflow project and include the required attributes on yo
Cloneable 1
Cloneable 2
-``` \ No newline at end of file +``` diff --git a/docs/WebflowOnly/TabsSlider.md b/docs/WebflowOnly/TabsSlider.md index 28426fe..06f4d8a 100644 --- a/docs/WebflowOnly/TabsSlider.md +++ b/docs/WebflowOnly/TabsSlider.md @@ -1,12 +1,15 @@ # TabsSlider ## Version + Current Version: 1.0.0 ## Description + TabsSlider is a Webflow-specific script that adds automatic sliding functionality to Webflow's native tab components. It enables automatic rotation between tabs with configurable timing and pause-on-hover functionality. ## Functionality + - Automatic rotation between tabs at specified intervals - Configurable slide timing - Pause-on-hover capability @@ -15,22 +18,27 @@ TabsSlider is a Webflow-specific script that adds automatic sliding functionalit - Error handling and graceful degradation ## Usage + Add the script to your Webflow project and include the required attributes on your tabs component. ### Installation + ```html ``` ### Required Attributes + - `wt-tabslider-element="tabs"` - Applied to the main tabs container - `wt-tabslider-element="menu"` - Applied to the tabs menu/navigation container ### Optional Attributes + - `wt-tabslider-speed="5000"` - Set the sliding speed in milliseconds (default: 5000ms) - `wt-tabslider-pauseonhover="true"` - Enable/disable pause on hover functionality (default: false) ## Considerations + 1. **Webflow Dependency**: This script requires Webflow's native tabs component to function 2. **Browser Compatibility**: Includes special handling for Safari browsers 3. **Performance**: Uses debouncing for hover events to optimize performance @@ -39,6 +47,7 @@ Add the script to your Webflow project and include the required attributes on yo ## Examples ### Basic Implementation + ```html
@@ -51,6 +60,7 @@ Add the script to your Webflow project and include the required attributes on yo ``` ### With Custom Speed and Hover Pause + ```html
-``` \ No newline at end of file +``` diff --git a/package-lock.json b/package-lock.json index 71519f4..f75581b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "webtricks", - "version": "0.1.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "webtricks", - "version": "0.1.0", + "version": "1.0.0", "devDependencies": { "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0" From 4926cf11c9960393e934d4f2a9d620650363eed3 Mon Sep 17 00:00:00 2001 From: matthewcsimpson Date: Thu, 19 Mar 2026 11:31:51 -0700 Subject: [PATCH 02/15] css revision --- Dist/Functional/RangeSliderSimple.js | 53 +++++----------------------- 1 file changed, 9 insertions(+), 44 deletions(-) diff --git a/Dist/Functional/RangeSliderSimple.js b/Dist/Functional/RangeSliderSimple.js index 5a1d554..3eeecc0 100644 --- a/Dist/Functional/RangeSliderSimple.js +++ b/Dist/Functional/RangeSliderSimple.js @@ -173,10 +173,12 @@ class RangeSliderSimple { const style = document.createElement('style'); style.id = 'wt-rangeslidersimple-styles'; + /* Layout only: no appearance:none or ::-webkit-slider-* / ::-moz-range-* so thumbs/tracks stay browser-default. */ style.textContent = ` [${ATTR_PREFIX}-element="slider"] { position: relative; - min-height: 32px; + min-height: 2.75rem; + box-sizing: border-box; } [${ATTR_PREFIX}-element="input-left"], @@ -184,56 +186,19 @@ class RangeSliderSimple { position: absolute; left: 0; width: 100%; - top: 0; - bottom: 0; - margin: auto; - height: 24px; - -webkit-appearance: none; - appearance: none; - background: transparent; + max-width: 100%; + top: 50%; + transform: translateY(-50%); + margin: 0; + padding: 0; + box-sizing: border-box; pointer-events: auto; z-index: 2; - outline: none; } [${ATTR_PREFIX}-element="input-right"] { z-index: 1; } - - [${ATTR_PREFIX}-element="input-left"]::-webkit-slider-thumb, - [${ATTR_PREFIX}-element="input-right"]::-webkit-slider-thumb { - -webkit-appearance: none; - width: 18px; - height: 18px; - border-radius: 50%; - background: #222; - cursor: pointer; - pointer-events: auto; - } - - [${ATTR_PREFIX}-element="input-left"]::-moz-range-thumb, - [${ATTR_PREFIX}-element="input-right"]::-moz-range-thumb { - width: 18px; - height: 18px; - border-radius: 50%; - background: #222; - cursor: pointer; - border: none; - } - - [${ATTR_PREFIX}-element="input-left"]::-webkit-slider-runnable-track, - [${ATTR_PREFIX}-element="input-right"]::-webkit-slider-runnable-track { - height: 6px; - border-radius: 3px; - background: rgba(0, 0, 0, 0.12); - } - - [${ATTR_PREFIX}-element="input-left"]::-moz-range-track, - [${ATTR_PREFIX}-element="input-right"]::-moz-range-track { - height: 6px; - border-radius: 3px; - background: rgba(0, 0, 0, 0.12); - } `; document.head.appendChild(style); } From 302d497e4927123dd8efa01d330ba32408baef20 Mon Sep 17 00:00:00 2001 From: matthewcsimpson Date: Thu, 19 Mar 2026 11:35:08 -0700 Subject: [PATCH 03/15] vnumber --- Dist/Functional/RangeSliderSimple.js | 1 + docs/Functional/RangeSliderSimple.md | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/Dist/Functional/RangeSliderSimple.js b/Dist/Functional/RangeSliderSimple.js index 3eeecc0..9d123d7 100644 --- a/Dist/Functional/RangeSliderSimple.js +++ b/Dist/Functional/RangeSliderSimple.js @@ -1,5 +1,6 @@ /*! * WebTricks — RangeSliderSimple + * @version 0.0.2 — pre-release; bump patch (and docs/Functional/RangeSliderSimple.md) on every change to this file. * Dual native range inputs (no custom thumb DOM). Self-contained (single script tag). * MIT License */ diff --git a/docs/Functional/RangeSliderSimple.md b/docs/Functional/RangeSliderSimple.md index 6d9ebd4..136a493 100644 --- a/docs/Functional/RangeSliderSimple.md +++ b/docs/Functional/RangeSliderSimple.md @@ -1,5 +1,9 @@ # RangeSliderSimple +## Version + +Current version: **0.0.2** (pre-release — see banner in `Dist/Functional/RangeSliderSimple.js`; bump patch and this line when the script changes). + ## Description `RangeSliderSimple` is a dual-handle range control built from two native `` elements. The draggable thumbs you see are the browser’s own controls, so hit targets stay aligned with the visuals. The file is **self-contained** (same core behavior as `RangeSlider`, inlined—keep edits in sync manually if you change constraint/display logic). From aa98ae5ad0d10b5feb87c6908775997df8c63862 Mon Sep 17 00:00:00 2001 From: matthewcsimpson Date: Thu, 19 Mar 2026 11:46:22 -0700 Subject: [PATCH 04/15] simplify --- Dist/Functional/RangeSliderSimple.js | 129 +++++++++++++++++++++++++-- docs/Functional/RangeSlider.md | 2 + docs/Functional/RangeSliderSimple.md | 19 +++- 3 files changed, 139 insertions(+), 11 deletions(-) diff --git a/Dist/Functional/RangeSliderSimple.js b/Dist/Functional/RangeSliderSimple.js index 9d123d7..1367091 100644 --- a/Dist/Functional/RangeSliderSimple.js +++ b/Dist/Functional/RangeSliderSimple.js @@ -1,6 +1,6 @@ /*! * WebTricks — RangeSliderSimple - * @version 0.0.2 — pre-release; bump patch (and docs/Functional/RangeSliderSimple.md) on every change to this file. + * @version 0.0.5 — pre-release; bump patch (and docs/Functional/RangeSliderSimple.md) on every change to this file. * Dual native range inputs (no custom thumb DOM). Self-contained (single script tag). * MIT License */ @@ -174,36 +174,125 @@ class RangeSliderSimple { const style = document.createElement('style'); style.id = 'wt-rangeslidersimple-styles'; - /* Layout only: no appearance:none or ::-webkit-slider-* / ::-moz-range-* so thumbs/tracks stay browser-default. */ + /* MDN-like range UI (Chrome docs): blue filled track, grey remainder, white pill thumb. + --wt-rs-track-fill defaults to #3b82f6 (typical RangeSlider [range] bar in docs). */ style.textContent = ` [${ATTR_PREFIX}-element="slider"] { + --wt-rs-track-fill: #3b82f6; + --wt-rs-track-bg: #e5e7eb; + --wt-rs-thumb-bg: #ffffff; + --wt-rs-thumb-border: #cbd5e1; + --wt-rs-thumb-shadow: 0 1px 2px rgba(0, 0, 0, 0.08); position: relative; + display: grid; + grid-template-columns: 1fr; + grid-template-rows: 1fr; + align-items: center; + justify-items: stretch; min-height: 2.75rem; box-sizing: border-box; } - [${ATTR_PREFIX}-element="input-left"], - [${ATTR_PREFIX}-element="input-right"] { - position: absolute; - left: 0; + input[type="range"][${ATTR_PREFIX}-element="input-left"], + input[type="range"][${ATTR_PREFIX}-element="input-right"] { + grid-column: 1; + grid-row: 1; width: 100%; max-width: 100%; - top: 50%; - transform: translateY(-50%); margin: 0; padding: 0; box-sizing: border-box; pointer-events: auto; z-index: 2; + height: 1.5rem; + min-height: 1.5rem; + background: transparent; + -webkit-appearance: none !important; + appearance: none !important; + -moz-appearance: none !important; } - [${ATTR_PREFIX}-element="input-right"] { + input[type="range"][${ATTR_PREFIX}-element="input-right"] { z-index: 1; } + + input[type="range"][${ATTR_PREFIX}-element="input-left"]::-webkit-slider-runnable-track, + input[type="range"][${ATTR_PREFIX}-element="input-right"]::-webkit-slider-runnable-track { + height: 6px; + border-radius: 3px; + background: linear-gradient( + to right, + var(--wt-rs-track-fill) 0%, + var(--wt-rs-track-fill) var(--wt-rs-pct, 0%), + var(--wt-rs-track-bg) var(--wt-rs-pct, 0%), + var(--wt-rs-track-bg) 100% + ); + } + + input[type="range"][${ATTR_PREFIX}-element="input-left"]::-webkit-slider-thumb, + input[type="range"][${ATTR_PREFIX}-element="input-right"]::-webkit-slider-thumb { + -webkit-appearance: none !important; + width: 12px; + height: 16px; + margin-top: -5px; + border-radius: 8px; + background: var(--wt-rs-thumb-bg); + border: 1px solid var(--wt-rs-thumb-border); + box-shadow: var(--wt-rs-thumb-shadow); + cursor: pointer; + } + + input[type="range"][${ATTR_PREFIX}-element="input-left"]::-moz-range-track, + input[type="range"][${ATTR_PREFIX}-element="input-right"]::-moz-range-track { + height: 6px; + border-radius: 3px; + background: var(--wt-rs-track-bg); + border: none; + } + + input[type="range"][${ATTR_PREFIX}-element="input-left"]::-moz-range-progress, + input[type="range"][${ATTR_PREFIX}-element="input-right"]::-moz-range-progress { + height: 6px; + border-radius: 3px; + background: var(--wt-rs-track-fill); + border: none; + } + + input[type="range"][${ATTR_PREFIX}-element="input-left"]::-moz-range-thumb, + input[type="range"][${ATTR_PREFIX}-element="input-right"]::-moz-range-thumb { + width: 12px; + height: 16px; + border-radius: 8px; + background: var(--wt-rs-thumb-bg); + border: 1px solid var(--wt-rs-thumb-border); + box-shadow: var(--wt-rs-thumb-shadow); + cursor: pointer; + } + + input[type="range"][${ATTR_PREFIX}-element="input-left"]:focus-visible, + input[type="range"][${ATTR_PREFIX}-element="input-right"]:focus-visible { + outline: 2px solid var(--wt-rs-track-fill); + outline-offset: 2px; + } `; document.head.appendChild(style); } + syncTrackFillPercents() { + if (!this.inputLeft || !this.inputRight) return; + [this.inputLeft, this.inputRight].forEach((input) => { + const min = parseInt(input.min, 10); + const max = parseInt(input.max, 10); + const val = parseInt(input.value, 10); + const safeMin = Number.isFinite(min) ? min : 0; + const safeMax = Number.isFinite(max) ? max : 100; + const safeVal = Number.isFinite(val) ? val : safeMin; + const pct = + safeMax <= safeMin ? 0 : ((safeVal - safeMin) / (safeMax - safeMin)) * 100; + input.style.setProperty('--wt-rs-pct', `${pct}%`); + }); + } + initConfig() { const cfg = this.rs.readSliderConfig(this.slider, ATTR_PREFIX); this.sliderMin = cfg.sliderMin; @@ -213,6 +302,15 @@ class RangeSliderSimple { this.rightSuffix = cfg.rightSuffix; this.defaultSuffix = cfg.defaultSuffix; this.shouldFormatNumber = cfg.shouldFormatNumber; + + const trackFill = this.slider.getAttribute(`${ATTR_PREFIX}-trackfill`); + if (trackFill) { + this.slider.style.setProperty('--wt-rs-track-fill', trackFill); + } + const trackBg = this.slider.getAttribute(`${ATTR_PREFIX}-trackbg`); + if (trackBg) { + this.slider.style.setProperty('--wt-rs-track-bg', trackBg); + } } initElements() { @@ -298,6 +396,8 @@ class RangeSliderSimple { this.shouldFormatNumber, ); } + + this.syncTrackFillPercents(); } updateRightValues(value) { @@ -332,6 +432,8 @@ class RangeSliderSimple { this.defaultSuffix, ); } + + this.syncTrackFillPercents(); } setupEventListeners() { @@ -432,6 +534,15 @@ const initializeRangeSliderSimple = () => { const instance = new RangeSliderSimple(wrapper); window.webtricks.push({ RangeSliderSimple: instance }); }); + + const bumpStyleOrder = () => { + const injectedStyle = document.getElementById('wt-rangeslidersimple-styles'); + if (injectedStyle && document.head) { + document.head.appendChild(injectedStyle); + } + }; + bumpStyleOrder(); + setTimeout(bumpStyleOrder, 0); } catch (err) { console.error(`RangeSliderSimple initialization error: ${err.message}`); } diff --git a/docs/Functional/RangeSlider.md b/docs/Functional/RangeSlider.md index 4ecfab1..f1ebb7a 100644 --- a/docs/Functional/RangeSlider.md +++ b/docs/Functional/RangeSlider.md @@ -50,6 +50,8 @@ Self-contained: **one** script tag. Use [jsDelivr](https://www.jsdelivr.com/) (n - `wt-rangeslider-max="100"` - Maximum value (default: 100) - `wt-rangeslider-steps="1"` - Step size (default: 1) +Style the `[wt-rangeslider-element="range"]` bar in your CSS; examples often use **#3b82f6** for the fill. [RangeSliderSimple](./RangeSliderSimple.md) defaults its track fill to the same for visual parity. + ### Optional Elements - `wt-rangeslider-range="from"` - Form input for start value diff --git a/docs/Functional/RangeSliderSimple.md b/docs/Functional/RangeSliderSimple.md index 136a493..7cdac52 100644 --- a/docs/Functional/RangeSliderSimple.md +++ b/docs/Functional/RangeSliderSimple.md @@ -2,11 +2,11 @@ ## Version -Current version: **0.0.2** (pre-release — see banner in `Dist/Functional/RangeSliderSimple.js`; bump patch and this line when the script changes). +Current version: **0.0.5** (pre-release — see banner in `Dist/Functional/RangeSliderSimple.js`; bump patch and this line when the script changes). ## Description -`RangeSliderSimple` is a dual-handle range control built from two native `` elements. The draggable thumbs you see are the browser’s own controls, so hit targets stay aligned with the visuals. The file is **self-contained** (same core behavior as `RangeSlider`, inlined—keep edits in sync manually if you change constraint/display logic). +`RangeSliderSimple` is a dual-handle range control built from two `` elements. Styling follows the **default MDN / Chrome** pattern (blue filled track, grey remainder, white pill thumb); default fill **#3b82f6** matches typical `RangeSlider` `[range]` bar examples. Hit targets match the painted thumbs. The file is **self-contained** (same core behavior as `RangeSlider`, inlined—keep edits in sync manually if you change constraint/display logic). Use **`RangeSlider`** when you need custom thumb graphics or a separate range bar element. Use **`RangeSliderSimple`** when native appearance (plus your own CSS overrides) is enough. @@ -40,6 +40,17 @@ Same semantics as `RangeSlider`, with the `wt-rangeslidersimple-` prefix: - `wt-rangeslidersimple-min`, `wt-rangeslidersimple-max`, `wt-rangeslidersimple-steps` - `wt-rangeslidersimple-mindifference` - `wt-rangeslidersimple-formatnumber`, `wt-rangeslidersimple-rightsuffix`, `wt-rangeslidersimple-defaultsuffix` +- `wt-rangeslidersimple-trackfill` — optional CSS color for the filled portion of the track (default **#3b82f6**, aligned with common `RangeSlider` `[range]` bar examples) +- `wt-rangeslidersimple-trackbg` — optional unfilled track color (default **#e5e7eb**) + +## Theming (CSS variables) + +On `[wt-rangeslidersimple-element="slider"]` you can override: + +- `--wt-rs-track-fill`, `--wt-rs-track-bg` +- `--wt-rs-thumb-bg`, `--wt-rs-thumb-border`, `--wt-rs-thumb-shadow` + +Injected styling approximates the default **MDN / Chrome** range look (white vertical pill thumb, blue progress, grey track). ## Optional elements (inside wrapper) @@ -79,3 +90,7 @@ Filter-driven min/max for collection ranges applies to **`wt-rangeslider-*`** on After init, instances are available on `window.webtricks` as `{ RangeSliderSimple: instance }`. - `setFrom(value)`, `setTo(value)`, `setRange(from, to)`, `reset()` + +## CodePen / global CSS + +The script injects complete **WebKit / Firefox** range pseudo-element styling so host resets (e.g. CodePen `input { appearance: none }`) do not leave unstyled thumbs. It also moves the injected `