From e56334176c119b2eaf6528ae5c31b1a93b86f0fb Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Thu, 12 Dec 2024 08:05:59 +0200 Subject: [PATCH 01/24] refactor: two-way binding using proxy to improve input synchronization - Refactored two-way binding logic to allow number inputs to remain empty. - Added `getRangePercent` function to compute slider percentage values. - Introduced `updateFieldValue` to handle empty or numeric inputs gracefully. - Implemented `sliderStateProxy` to synchronize range and number inputs. - Centralized input handling via `handleInputChange` to simplify event management. - Removed wired `addEventListener` logic to reduce bugs and improve maintainability. --- dxb-slider.js | 81 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 20 deletions(-) diff --git a/dxb-slider.js b/dxb-slider.js index 93313b8..96ac6d9 100644 --- a/dxb-slider.js +++ b/dxb-slider.js @@ -24,6 +24,39 @@ return wrapper; } + function getRangePercent(value = 0, min = 0, max = 0) { + return ((value - min) / (max - min)) * 100; + } + + function updateFieldValue(field, value) { + // Handle empty value for number inputs + if (field.type === "number") { + + // Keep empty for cleared input + field.value = value === "" ? "" : Number(value); + } else if (field.type === "range") { + + // Default to 0 for range inputs if value is empty or NaN + const valueAsNumber = Number(value) || 0; + field.value = valueAsNumber; + + // Calculate percentage for range input + const percent = getRangePercent(field.value, field.min, field.max); + field.style.setProperty('--value-percent', `${percent}%`); + field.setAttribute('aria-valuenow', field.value); + } + } + + // Proxy object to synchronize field values and update all matching fields when a value changes + const sliderStateProxy = new Proxy({}, { + set(target, key, value) { + target[key] = value; + const fields = document.querySelectorAll(`[name="${key}"]`); + fields.forEach(field => updateFieldValue(field, value)); + return true; + } + }); + function initDXBSliders() { document.querySelectorAll('[data-dxb-slider]:not([data-dxb-initialized])').forEach(rangeInput => { const wrapper = createSliderStructure(rangeInput); @@ -45,34 +78,42 @@ wrapper.appendChild(numberInput); - function updateValue() { - const val = rangeInput.value; - const min = rangeInput.min; - const max = rangeInput.max; - const percent = (val - min) / (max - min) * 100; - rangeInput.style.setProperty('--value-percent', `${percent}%`); - numberInput.value = val; - numberInput.min = min; - numberInput.max = max; - rangeInput.setAttribute('aria-valuenow', val); + + function handleInputChange(e) { + + // Only update fields within the DXB slider container's scope + if (!e.target.closest('.dxb-slider-container')) { + return; + } + + sliderStateProxy[e.target.name] = e.target.value; } - rangeInput.addEventListener('input', updateValue); - numberInput.addEventListener('input', () => { - rangeInput.value = numberInput.value; - updateValue(); - rangeInput.dispatchEvent(new Event('input', { bubbles: true })); - }); + [rangeInput, numberInput].forEach(input => + input.addEventListener('input', handleInputChange) + ); - numberInput.addEventListener('change', () => { - rangeInput.dispatchEvent(new Event('change', { bubbles: true })); - }); + // Initialize the proxy with the initial value of the range input + sliderStateProxy[rangeInput.name] = rangeInput.value; // Set initial ARIA attributes rangeInput.setAttribute('aria-valuemin', rangeInput.min); rangeInput.setAttribute('aria-valuemax', rangeInput.max); - updateValue(); + // Populate inputs in first initiation + const value = rangeInput.value; + const min = rangeInput.min; + const max = rangeInput.max; + + const percent = getRangePercent(value, min, max); + rangeInput.style.setProperty('--value-percent', `${percent}%`); + + numberInput.value = value; + numberInput.min = min; + numberInput.max = max; + numberInput.name = rangeInput.name; + + rangeInput.setAttribute('aria-valuenow', value); // Mark as initialized rangeInput.setAttribute('data-dxb-initialized', 'true'); From f16a7c1832c2af3d19af4ed835b17402675c440e Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Thu, 12 Dec 2024 08:20:29 +0200 Subject: [PATCH 02/24] chore: generate the new assets --- dxb-slider.min.css | 2 +- dxb-slider.min.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dxb-slider.min.css b/dxb-slider.min.css index f5d52d8..d3aae3c 100644 --- a/dxb-slider.min.css +++ b/dxb-slider.min.css @@ -1 +1 @@ -.dxb-slider-container{display:flex;flex-direction:column;align-items:flex-start;width:100%;margin-bottom:20px}.dxb-slider-container+label{margin-bottom:10px;font-weight:700;font-size:1.2em}.dxb-slider-wrapper{display:flex;align-items:center;width:100%}.dxb-slider-track{flex:1}.dxb-slider{-webkit-appearance:none;width:calc(100% - 20px);height:5px;background:transparent;outline:none}.dxb-slider::-webkit-slider-runnable-track{height:5px;background-image:linear-gradient(#0550e6,#0550e6),linear-gradient(#dadfe7,#dadfe7);background-size:var(--value-percent) 5px,100% 5px;background-repeat:no-repeat;background-position:0}.dxb-slider::-moz-range-track{height:5px;background-image:linear-gradient(#0550e6,#0550e6),linear-gradient(#dadfe7,#dadfe7);background-size:var(--value-percent) 5px,100% 5px;background-repeat:no-repeat;background-position:0}.dxb-slider::-webkit-slider-thumb{appearance:none;width:20px;height:20px;background:#fff;cursor:pointer;border:none;outline:none;box-shadow:0 .0625rem .125rem rgba(16,24,40,.06),0 .0625rem .1875rem rgba(16,24,40,.1);margin-top:-8px;border-radius:0;transition:background .3s,transform .3s}.dxb-slider::-moz-range-thumb{width:20px;height:20px;background:#fff;cursor:pointer;border:none;outline:none;box-shadow:0 .0625rem .125rem rgba(16,24,40,.06),0 .0625rem .1875rem rgba(16,24,40,.1);margin-top:-8px;border-radius:0;transition:background .3s,transform .3s}.dxb-slider:hover::-moz-range-thumb,.dxb-slider:hover::-webkit-slider-thumb{background:#e0e0e0;transform:scale(1.1)}.dxb-slider:focus::-moz-range-thumb,.dxb-slider:focus::-webkit-slider-thumb{background:silver;transform:scale(1.1);box-shadow:0 0 0 3px rgba(0,123,255,.25)}.dxb-slider-value{width:60px;text-align:center;margin-inline-start:10px;border:1px solid #ddd;border-radius:0;height:24px;font-size:1em;box-shadow:0 .0625rem .125rem rgba(16,24,40,.06),0 .0625rem .1875rem rgba(16,24,40,.1)}[dir=rtl] .dxb-slider-wrapper{flex-direction:row-reverse}[dir=rtl] .dxb-slider{direction:rtl}[dir=rtl] .dxb-slider::-webkit-slider-runnable-track{background-position:100%}[dir=rtl] .dxb-slider::-moz-range-track{background-position:100%}[dir=rtl] .dxb-slider-value{margin-inline-start:10px;margin-inline-end:0;order:-1}[lang=ar] .dxb-slider-value{font-variant-numeric:arabic-indic}[lang=fa] .dxb-slider-value{font-variant-numeric:persian}[lang=bn] .dxb-slider-value{font-variant-numeric:bengali}[lang=hi],[lang=mr],[lang=ne] .dxb-slider-value{font-variant-numeric:devanagari} \ No newline at end of file +.dxb-slider-container{display:flex;flex-direction:column;align-items:flex-start;width:100%;margin-bottom:20px}.dxb-slider-wrapper{display:flex;align-items:center;width:100%}.dxb-slider-track{flex:1}.dxb-slider{-webkit-appearance:none;width:calc(100% - 20px);height:5px;background:transparent;outline:none}.dxb-slider::-webkit-slider-runnable-track{height:5px;background-image:linear-gradient(#0550e6,#0550e6),linear-gradient(#dadfe7,#dadfe7);background-size:var(--value-percent) 5px,100% 5px;background-repeat:no-repeat;background-position:0}.dxb-slider::-moz-range-track{height:5px;background-image:linear-gradient(#0550e6,#0550e6),linear-gradient(#dadfe7,#dadfe7);background-size:var(--value-percent) 5px,100% 5px;background-repeat:no-repeat;background-position:0}.dxb-slider::-webkit-slider-thumb{appearance:none;width:20px;height:20px;background:#fff;cursor:pointer;border:none;outline:none;box-shadow:0 .0625rem .125rem rgba(16,24,40,.06),0 .0625rem .1875rem rgba(16,24,40,.1);margin-top:-8px;border-radius:0;transition:background .3s,transform .3s}.dxb-slider::-moz-range-thumb{width:20px;height:20px;background:#fff;cursor:pointer;border:none;outline:none;box-shadow:0 .0625rem .125rem rgba(16,24,40,.06),0 .0625rem .1875rem rgba(16,24,40,.1);margin-top:-8px;border-radius:0;transition:background .3s,transform .3s}.dxb-slider:hover::-moz-range-thumb,.dxb-slider:hover::-webkit-slider-thumb{background:#e0e0e0;transform:scale(1.1)}.dxb-slider:focus::-moz-range-thumb,.dxb-slider:focus::-webkit-slider-thumb{background:silver;transform:scale(1.1);box-shadow:0 0 0 3px rgba(0,123,255,.25)}.dxb-slider-value{width:60px;text-align:center;margin-inline-start:10px;border:1px solid #ddd;border-radius:0;height:24px;font-size:1em;box-shadow:0 .0625rem .125rem rgba(16,24,40,.06),0 .0625rem .1875rem rgba(16,24,40,.1)}[dir=rtl] .dxb-slider-wrapper{flex-direction:row-reverse}[dir=rtl] .dxb-slider{direction:rtl}[dir=rtl] .dxb-slider::-webkit-slider-runnable-track{background-position:100%}[dir=rtl] .dxb-slider::-moz-range-track{background-position:100%}[dir=rtl] .dxb-slider-value{margin-inline-start:10px;margin-inline-end:0;order:-1}[lang=ar] .dxb-slider-value{font-variant-numeric:arabic-indic}[lang=fa] .dxb-slider-value{font-variant-numeric:persian}[lang=bn] .dxb-slider-value{font-variant-numeric:bengali}[lang=hi],[lang=mr],[lang=ne] .dxb-slider-value{font-variant-numeric:devanagari} \ No newline at end of file diff --git a/dxb-slider.min.js b/dxb-slider.min.js index 19bf47e..1d4d675 100644 --- a/dxb-slider.min.js +++ b/dxb-slider.min.js @@ -1 +1 @@ -(()=>{function i(){document.querySelectorAll("[data-dxb-slider]:not([data-dxb-initialized])").forEach(d=>{e=d,(a=document.createElement("div")).className="dxb-slider-container",(t=document.createElement("div")).className="dxb-slider-wrapper",(r=document.createElement("div")).className="dxb-slider-track",e.classList.add("dxb-slider"),e.parentNode.insertBefore(a,e),a.appendChild(t),t.appendChild(r),r.appendChild(e);var e,t,a=t;let i=document.createElement("input");i.type="number",i.className="dxb-slider-value",i.setAttribute("tabindex","-1"),i.setAttribute("pattern","[0-9]*"),i.setAttribute("step",d.step);var r=parseFloat(d.step);function n(){var e=d.value,t=d.min,a=d.max;d.style.setProperty("--value-percent",(e-t)/(a-t)*100+"%"),i.value=e,i.min=t,i.max=a,d.setAttribute("aria-valuenow",e)}r&&r%1!=0?i.setAttribute("inputmode","decimal"):i.setAttribute("inputmode","numeric"),a.appendChild(i),d.addEventListener("input",n),i.addEventListener("input",()=>{d.value=i.value,n(),d.dispatchEvent(new Event("input",{bubbles:!0}))}),i.addEventListener("change",()=>{d.dispatchEvent(new Event("change",{bubbles:!0}))}),d.setAttribute("aria-valuemin",d.min),d.setAttribute("aria-valuemax",d.max),n(),d.setAttribute("data-dxb-initialized","true")})}i(),new MutationObserver(e=>{let t=!1;for(var a of e)if("childList"===a.type){for(var d of a.addedNodes)if(d.nodeType===Node.ELEMENT_NODE&&(d.matches("[data-dxb-slider]")||d.querySelector("[data-dxb-slider]"))){t=!0;break}if(t)break}t&&i()}).observe(document.body,{childList:!0,subtree:!0})})(); \ No newline at end of file +(()=>{function l(e=0,t=0,a=0){return(e-t)/(a-t)*100}let u=new Proxy({},{set(e,t,a){return e[t]=a,document.querySelectorAll(`[name="${t}"]`).forEach(e=>{var t;t=a,"number"===(e=e).type?e.value=""===t?"":Number(t):"range"===e.type&&(t=Number(t)||0,e.value=t,t=l(e.value,e.min,e.max),e.style.setProperty("--value-percent",t+"%"),e.setAttribute("aria-valuenow",e.value))}),!0}});function i(){document.querySelectorAll("[data-dxb-slider]:not([data-dxb-initialized])").forEach(e=>{r=e,(t=document.createElement("div")).className="dxb-slider-container",(d=document.createElement("div")).className="dxb-slider-wrapper",(a=document.createElement("div")).className="dxb-slider-track",r.classList.add("dxb-slider"),r.parentNode.insertBefore(t,r),t.appendChild(d),d.appendChild(a),a.appendChild(r);var t=d,a=document.createElement("input"),r=(a.type="number",a.className="dxb-slider-value",a.setAttribute("tabindex","-1"),a.setAttribute("pattern","[0-9]*"),a.setAttribute("step",e.step),parseFloat(e.step));function i(e){e.target.closest(".dxb-slider-container")&&(u[e.target.name]=e.target.value)}r&&r%1!=0?a.setAttribute("inputmode","decimal"):a.setAttribute("inputmode","numeric"),t.appendChild(a),[e,a].forEach(e=>e.addEventListener("input",i)),u[e.name]=e.value,e.setAttribute("aria-valuemin",e.min),e.setAttribute("aria-valuemax",e.max);var d=e.value,r=e.min,t=e.max,n=l(d,r,t);e.style.setProperty("--value-percent",n+"%"),a.value=d,a.min=r,a.max=t,a.name=e.name,e.setAttribute("aria-valuenow",d),e.setAttribute("data-dxb-initialized","true")})}i(),new MutationObserver(e=>{let t=!1;for(var a of e)if("childList"===a.type){for(var r of a.addedNodes)if(r.nodeType===Node.ELEMENT_NODE&&(r.matches("[data-dxb-slider]")||r.querySelector("[data-dxb-slider]"))){t=!0;break}if(t)break}t&&i()}).observe(document.body,{childList:!0,subtree:!0})})(); \ No newline at end of file From 7c4183f4bc639a64d01da95ba318ce22bbf3569f Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Thu, 12 Dec 2024 08:24:06 +0200 Subject: [PATCH 03/24] refactor: add 'name' attributes for proxy-based synchronization - Added `name` attributes to slider inputs in `index.html` for proxy-based synchronization. --- index.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/index.html b/index.html index b48422c..4f5e7a9 100644 --- a/index.html +++ b/index.html @@ -27,6 +27,7 @@

LTR (Left-to-Right) Mode

@@ -35,6 +36,7 @@

LTR (Left-to-Right) Mode

@@ -43,6 +45,7 @@

LTR (Left-to-Right) Mode

@@ -55,6 +58,7 @@

RTL (Right-to-Left) Mode

@@ -63,6 +67,7 @@

RTL (Right-to-Left) Mode

@@ -71,6 +76,7 @@

RTL (Right-to-Left) Mode

From 6616d756e541dcce4dc0b0c052a8ab0b808d8067 Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Thu, 12 Dec 2024 08:29:25 +0200 Subject: [PATCH 04/24] fix: unit tests --- __tests__/dxb-slider.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__tests__/dxb-slider.test.js b/__tests__/dxb-slider.test.js index 9515e6c..7d4d3c7 100644 --- a/__tests__/dxb-slider.test.js +++ b/__tests__/dxb-slider.test.js @@ -16,7 +16,7 @@ describe('DXB Slider Core Tests', () => { - @@ -106,7 +106,7 @@ describe('DXB Slider Step Tests', () => { - From 64173270ed52f83e3a7ca6d4cdf8c9005036b7d4 Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Thu, 12 Dec 2024 08:32:40 +0200 Subject: [PATCH 05/24] chore: deprecate multi-stage event test Removed test as the proxy now handles input and range updates automatically. --- __tests__/dxb-slider.test.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/__tests__/dxb-slider.test.js b/__tests__/dxb-slider.test.js index 7d4d3c7..d9c1ae3 100644 --- a/__tests__/dxb-slider.test.js +++ b/__tests__/dxb-slider.test.js @@ -63,16 +63,6 @@ describe('DXB Slider Core Tests', () => { expect(newSlider.hasAttribute('data-dxb-initialized')).toBe(true); }); - it('should dispatch change event on number input change', () => { - const changeHandler = vi.fn(); - - slider.addEventListener('change', changeHandler); - numberInput.value = 80; - numberInput.dispatchEvent(new window.Event('change')); - - expect(changeHandler).toHaveBeenCalled(); - }); - it('should synchronize values on number input change', () => { numberInput.value = 80; numberInput.dispatchEvent(new window.Event('input')); From 73174a8899f172888f351577ca9e5a3b4b785ae4 Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Thu, 12 Dec 2024 08:53:50 +0200 Subject: [PATCH 06/24] test: add new test coverage --- __tests__/dxb-slider.test.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/__tests__/dxb-slider.test.js b/__tests__/dxb-slider.test.js index d9c1ae3..73e5745 100644 --- a/__tests__/dxb-slider.test.js +++ b/__tests__/dxb-slider.test.js @@ -49,6 +49,12 @@ describe('DXB Slider Core Tests', () => { expect(numberInput.value).toBe('75'); }); + it('should synchronize range and number input values (0 based)', () => { + slider.value = 0; + slider.dispatchEvent(new window.Event('input')); + expect(numberInput.value).toBe('0'); + }); + it('should initialize dynamically added sliders', async () => { const newSlider = document.createElement('input'); newSlider.type = 'range'; @@ -70,6 +76,20 @@ describe('DXB Slider Core Tests', () => { expect(slider.value).toBe('80'); }); + it('should synchronize values on number input change (0 based)', () => { + numberInput.value = ""; + numberInput.dispatchEvent(new window.Event('input')); + + expect(slider.value).toBe('0'); + }); + + it('should synchronize values on number input change (empty)', () => { + numberInput.value = ""; + numberInput.dispatchEvent(new window.Event('input')); + + expect(numberInput.value).toBe(''); + }); + it('should set initial ARIA attributes', () => { expect(slider.getAttribute('aria-valuemin')).toBe('0'); expect(slider.getAttribute('aria-valuemax')).toBe('100'); From c38fb111c90bd98a03d950fc2de88a86fd51d2bd Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Thu, 12 Dec 2024 08:58:12 +0200 Subject: [PATCH 07/24] style: reformating the file The current style of file not consistent with vscode reformating style --- dxb-slider.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/dxb-slider.js b/dxb-slider.js index 96ac6d9..8a2f764 100644 --- a/dxb-slider.js +++ b/dxb-slider.js @@ -1,26 +1,27 @@ // dxb-slider.js -(function() { +(function () { function createSliderStructure(rangeInput) { + // Create container and wrapper const container = document.createElement('div'); container.className = 'dxb-slider-container'; - + const wrapper = document.createElement('div'); wrapper.className = 'dxb-slider-wrapper'; - + const track = document.createElement('div'); track.className = 'dxb-slider-track'; - + // Add slider class rangeInput.classList.add('dxb-slider'); - + // Restructure DOM rangeInput.parentNode.insertBefore(container, rangeInput); container.appendChild(wrapper); wrapper.appendChild(track); track.appendChild(rangeInput); - + return wrapper; } @@ -29,6 +30,7 @@ } function updateFieldValue(field, value) { + // Handle empty value for number inputs if (field.type === "number") { @@ -60,7 +62,7 @@ function initDXBSliders() { document.querySelectorAll('[data-dxb-slider]:not([data-dxb-initialized])').forEach(rangeInput => { const wrapper = createSliderStructure(rangeInput); - + // Create number input programmatically const numberInput = document.createElement('input'); numberInput.type = 'number'; @@ -127,8 +129,8 @@ for (const mutation of mutations) { if (mutation.type === 'childList') { for (const node of mutation.addedNodes) { - if (node.nodeType === Node.ELEMENT_NODE && - (node.matches('[data-dxb-slider]') || node.querySelector('[data-dxb-slider]'))) { + if (node.nodeType === Node.ELEMENT_NODE && + (node.matches('[data-dxb-slider]') || node.querySelector('[data-dxb-slider]'))) { shouldInit = true; break; } @@ -142,4 +144,4 @@ }); observer.observe(document.body, { childList: true, subtree: true }); -})(); \ No newline at end of file +})(); From 71f288de5a8c2afb6abddfd1d06cac194e7d8256 Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Tue, 17 Dec 2024 07:45:59 +0200 Subject: [PATCH 08/24] fix: replace `name` with `data-dxb-proxy-key` --- __tests__/dxb-slider.test.js | 4 ++-- dxb-slider.js | 8 +++++--- dxb-slider.min.js | 2 +- index.html | 12 ++++++------ 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/__tests__/dxb-slider.test.js b/__tests__/dxb-slider.test.js index 73e5745..6ea1369 100644 --- a/__tests__/dxb-slider.test.js +++ b/__tests__/dxb-slider.test.js @@ -16,7 +16,7 @@ describe('DXB Slider Core Tests', () => { - @@ -116,7 +116,7 @@ describe('DXB Slider Step Tests', () => { - diff --git a/dxb-slider.js b/dxb-slider.js index 8a2f764..d4ac00e 100644 --- a/dxb-slider.js +++ b/dxb-slider.js @@ -53,7 +53,7 @@ const sliderStateProxy = new Proxy({}, { set(target, key, value) { target[key] = value; - const fields = document.querySelectorAll(`[name="${key}"]`); + const fields = document.querySelectorAll(`[data-dxb-proxy-key="${key}"]`); fields.forEach(field => updateFieldValue(field, value)); return true; } @@ -88,7 +88,7 @@ return; } - sliderStateProxy[e.target.name] = e.target.value; + sliderStateProxy[e.target.dataset["dxbProxyKey"]] = e.target.value; } [rangeInput, numberInput].forEach(input => @@ -96,7 +96,7 @@ ); // Initialize the proxy with the initial value of the range input - sliderStateProxy[rangeInput.name] = rangeInput.value; + sliderStateProxy[rangeInput.dataset["dxbProxyKey"]] = rangeInput.value; // Set initial ARIA attributes rangeInput.setAttribute('aria-valuemin', rangeInput.min); @@ -115,6 +115,8 @@ numberInput.max = max; numberInput.name = rangeInput.name; + numberInput.setAttribute("data-dxb-proxy-key", rangeInput.dataset["dxbProxyKey"]); + rangeInput.setAttribute('aria-valuenow', value); // Mark as initialized diff --git a/dxb-slider.min.js b/dxb-slider.min.js index 1d4d675..cb084a3 100644 --- a/dxb-slider.min.js +++ b/dxb-slider.min.js @@ -1 +1 @@ -(()=>{function l(e=0,t=0,a=0){return(e-t)/(a-t)*100}let u=new Proxy({},{set(e,t,a){return e[t]=a,document.querySelectorAll(`[name="${t}"]`).forEach(e=>{var t;t=a,"number"===(e=e).type?e.value=""===t?"":Number(t):"range"===e.type&&(t=Number(t)||0,e.value=t,t=l(e.value,e.min,e.max),e.style.setProperty("--value-percent",t+"%"),e.setAttribute("aria-valuenow",e.value))}),!0}});function i(){document.querySelectorAll("[data-dxb-slider]:not([data-dxb-initialized])").forEach(e=>{r=e,(t=document.createElement("div")).className="dxb-slider-container",(d=document.createElement("div")).className="dxb-slider-wrapper",(a=document.createElement("div")).className="dxb-slider-track",r.classList.add("dxb-slider"),r.parentNode.insertBefore(t,r),t.appendChild(d),d.appendChild(a),a.appendChild(r);var t=d,a=document.createElement("input"),r=(a.type="number",a.className="dxb-slider-value",a.setAttribute("tabindex","-1"),a.setAttribute("pattern","[0-9]*"),a.setAttribute("step",e.step),parseFloat(e.step));function i(e){e.target.closest(".dxb-slider-container")&&(u[e.target.name]=e.target.value)}r&&r%1!=0?a.setAttribute("inputmode","decimal"):a.setAttribute("inputmode","numeric"),t.appendChild(a),[e,a].forEach(e=>e.addEventListener("input",i)),u[e.name]=e.value,e.setAttribute("aria-valuemin",e.min),e.setAttribute("aria-valuemax",e.max);var d=e.value,r=e.min,t=e.max,n=l(d,r,t);e.style.setProperty("--value-percent",n+"%"),a.value=d,a.min=r,a.max=t,a.name=e.name,e.setAttribute("aria-valuenow",d),e.setAttribute("data-dxb-initialized","true")})}i(),new MutationObserver(e=>{let t=!1;for(var a of e)if("childList"===a.type){for(var r of a.addedNodes)if(r.nodeType===Node.ELEMENT_NODE&&(r.matches("[data-dxb-slider]")||r.querySelector("[data-dxb-slider]"))){t=!0;break}if(t)break}t&&i()}).observe(document.body,{childList:!0,subtree:!0})})(); \ No newline at end of file +(()=>{function l(e=0,t=0,a=0){return(e-t)/(a-t)*100}let s=new Proxy({},{set(e,t,a){return e[t]=a,document.querySelectorAll(`[data-dxb-proxy-key="${t}"]`).forEach(e=>{var t;t=a,"number"===(e=e).type?e.value=""===t?"":Number(t):"range"===e.type&&(t=Number(t)||0,e.value=t,t=l(e.value,e.min,e.max),e.style.setProperty("--value-percent",t+"%"),e.setAttribute("aria-valuenow",e.value))}),!0}});function d(){document.querySelectorAll("[data-dxb-slider]:not([data-dxb-initialized])").forEach(e=>{r=e,(t=document.createElement("div")).className="dxb-slider-container",(i=document.createElement("div")).className="dxb-slider-wrapper",(a=document.createElement("div")).className="dxb-slider-track",r.classList.add("dxb-slider"),r.parentNode.insertBefore(t,r),t.appendChild(i),i.appendChild(a),a.appendChild(r);var t=i,a=document.createElement("input"),r=(a.type="number",a.className="dxb-slider-value",a.setAttribute("tabindex","-1"),a.setAttribute("pattern","[0-9]*"),a.setAttribute("step",e.step),parseFloat(e.step));function d(e){e.target.closest(".dxb-slider-container")&&(s[e.target.dataset.dxbProxyKey]=e.target.value)}r&&r%1!=0?a.setAttribute("inputmode","decimal"):a.setAttribute("inputmode","numeric"),t.appendChild(a),[e,a].forEach(e=>e.addEventListener("input",d)),s[e.dataset.dxbProxyKey]=e.value,e.setAttribute("aria-valuemin",e.min),e.setAttribute("aria-valuemax",e.max);var i=e.value,r=e.min,t=e.max,n=l(i,r,t);e.style.setProperty("--value-percent",n+"%"),a.value=i,a.min=r,a.max=t,a.name=e.name,a.setAttribute("data-dxb-proxy-key",e.dataset.dxbProxyKey),e.setAttribute("aria-valuenow",i),e.setAttribute("data-dxb-initialized","true")})}d(),new MutationObserver(e=>{let t=!1;for(var a of e)if("childList"===a.type){for(var r of a.addedNodes)if(r.nodeType===Node.ELEMENT_NODE&&(r.matches("[data-dxb-slider]")||r.querySelector("[data-dxb-slider]"))){t=!0;break}if(t)break}t&&d()}).observe(document.body,{childList:!0,subtree:!0})})(); \ No newline at end of file diff --git a/index.html b/index.html index 4f5e7a9..2093a6e 100644 --- a/index.html +++ b/index.html @@ -27,7 +27,7 @@

LTR (Left-to-Right) Mode

@@ -36,7 +36,7 @@

LTR (Left-to-Right) Mode

@@ -45,7 +45,7 @@

LTR (Left-to-Right) Mode

@@ -58,7 +58,7 @@

RTL (Right-to-Left) Mode

@@ -67,7 +67,7 @@

RTL (Right-to-Left) Mode

@@ -76,7 +76,7 @@

RTL (Right-to-Left) Mode

From 8ecc562fa78a03aed9210b4499f58142d518cc24 Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Thu, 19 Dec 2024 08:59:49 +0200 Subject: [PATCH 09/24] fix: set slider in the middle if the input field is cleared --- __tests__/dxb-slider.test.js | 2 +- dxb-slider.js | 19 +++++++++++++++++-- dxb-slider.min.js | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/__tests__/dxb-slider.test.js b/__tests__/dxb-slider.test.js index 6ea1369..ec1385d 100644 --- a/__tests__/dxb-slider.test.js +++ b/__tests__/dxb-slider.test.js @@ -80,7 +80,7 @@ describe('DXB Slider Core Tests', () => { numberInput.value = ""; numberInput.dispatchEvent(new window.Event('input')); - expect(slider.value).toBe('0'); + expect(slider.value).toBe('50'); }); it('should synchronize values on number input change (empty)', () => { diff --git a/dxb-slider.js b/dxb-slider.js index d4ac00e..10ca5b0 100644 --- a/dxb-slider.js +++ b/dxb-slider.js @@ -25,12 +25,25 @@ return wrapper; } + function getDefaultValue(num) { + + // Convert the input to a number + const convertedNum = Number(num); + + // Check if the converted number is not finite (e.g., NaN, Infinity) + if (!isFinite(convertedNum)) { + return 0; + } + + return Math.floor(convertedNum / 2); + } + function getRangePercent(value = 0, min = 0, max = 0) { return ((value - min) / (max - min)) * 100; } function updateFieldValue(field, value) { - + // Handle empty value for number inputs if (field.type === "number") { @@ -40,7 +53,9 @@ // Default to 0 for range inputs if value is empty or NaN const valueAsNumber = Number(value) || 0; - field.value = valueAsNumber; + + // Reset to default if the input is cleared or empty + field.value = valueAsNumber === 0 ? getDefaultValue(field.max) : valueAsNumber; // Calculate percentage for range input const percent = getRangePercent(field.value, field.min, field.max); diff --git a/dxb-slider.min.js b/dxb-slider.min.js index cb084a3..8a81ba6 100644 --- a/dxb-slider.min.js +++ b/dxb-slider.min.js @@ -1 +1 @@ -(()=>{function l(e=0,t=0,a=0){return(e-t)/(a-t)*100}let s=new Proxy({},{set(e,t,a){return e[t]=a,document.querySelectorAll(`[data-dxb-proxy-key="${t}"]`).forEach(e=>{var t;t=a,"number"===(e=e).type?e.value=""===t?"":Number(t):"range"===e.type&&(t=Number(t)||0,e.value=t,t=l(e.value,e.min,e.max),e.style.setProperty("--value-percent",t+"%"),e.setAttribute("aria-valuenow",e.value))}),!0}});function d(){document.querySelectorAll("[data-dxb-slider]:not([data-dxb-initialized])").forEach(e=>{r=e,(t=document.createElement("div")).className="dxb-slider-container",(i=document.createElement("div")).className="dxb-slider-wrapper",(a=document.createElement("div")).className="dxb-slider-track",r.classList.add("dxb-slider"),r.parentNode.insertBefore(t,r),t.appendChild(i),i.appendChild(a),a.appendChild(r);var t=i,a=document.createElement("input"),r=(a.type="number",a.className="dxb-slider-value",a.setAttribute("tabindex","-1"),a.setAttribute("pattern","[0-9]*"),a.setAttribute("step",e.step),parseFloat(e.step));function d(e){e.target.closest(".dxb-slider-container")&&(s[e.target.dataset.dxbProxyKey]=e.target.value)}r&&r%1!=0?a.setAttribute("inputmode","decimal"):a.setAttribute("inputmode","numeric"),t.appendChild(a),[e,a].forEach(e=>e.addEventListener("input",d)),s[e.dataset.dxbProxyKey]=e.value,e.setAttribute("aria-valuemin",e.min),e.setAttribute("aria-valuemax",e.max);var i=e.value,r=e.min,t=e.max,n=l(i,r,t);e.style.setProperty("--value-percent",n+"%"),a.value=i,a.min=r,a.max=t,a.name=e.name,a.setAttribute("data-dxb-proxy-key",e.dataset.dxbProxyKey),e.setAttribute("aria-valuenow",i),e.setAttribute("data-dxb-initialized","true")})}d(),new MutationObserver(e=>{let t=!1;for(var a of e)if("childList"===a.type){for(var r of a.addedNodes)if(r.nodeType===Node.ELEMENT_NODE&&(r.matches("[data-dxb-slider]")||r.querySelector("[data-dxb-slider]"))){t=!0;break}if(t)break}t&&d()}).observe(document.body,{childList:!0,subtree:!0})})(); \ No newline at end of file +(()=>{function l(e=0,t=0,a=0){return(e-t)/(a-t)*100}function r(e,t){var a;"number"===e.type?e.value=""===t?"":Number(t):"range"===e.type&&(t=Number(t)||0,e.value=0===t?(a=e.max,a=Number(a),isFinite(a)?Math.floor(a/2):0):t,a=l(e.value,e.min,e.max),e.style.setProperty("--value-percent",a+"%"),e.setAttribute("aria-valuenow",e.value))}let s=new Proxy({},{set(e,t,a){return e[t]=a,document.querySelectorAll(`[data-dxb-proxy-key="${t}"]`).forEach(e=>r(e,a)),!0}});function d(){document.querySelectorAll("[data-dxb-slider]:not([data-dxb-initialized])").forEach(e=>{r=e,(t=document.createElement("div")).className="dxb-slider-container",(i=document.createElement("div")).className="dxb-slider-wrapper",(a=document.createElement("div")).className="dxb-slider-track",r.classList.add("dxb-slider"),r.parentNode.insertBefore(t,r),t.appendChild(i),i.appendChild(a),a.appendChild(r);var t=i,a=document.createElement("input"),r=(a.type="number",a.className="dxb-slider-value",a.setAttribute("tabindex","-1"),a.setAttribute("pattern","[0-9]*"),a.setAttribute("step",e.step),parseFloat(e.step));function d(e){e.target.closest(".dxb-slider-container")&&(s[e.target.dataset.dxbProxyKey]=e.target.value)}r&&r%1!=0?a.setAttribute("inputmode","decimal"):a.setAttribute("inputmode","numeric"),t.appendChild(a),[e,a].forEach(e=>e.addEventListener("input",d)),s[e.dataset.dxbProxyKey]=e.value,e.setAttribute("aria-valuemin",e.min),e.setAttribute("aria-valuemax",e.max);var i=e.value,r=e.min,t=e.max,n=l(i,r,t);e.style.setProperty("--value-percent",n+"%"),a.value=i,a.min=r,a.max=t,a.name=e.name,a.setAttribute("data-dxb-proxy-key",e.dataset.dxbProxyKey),e.setAttribute("aria-valuenow",i),e.setAttribute("data-dxb-initialized","true")})}d(),new MutationObserver(e=>{let t=!1;for(var a of e)if("childList"===a.type){for(var r of a.addedNodes)if(r.nodeType===Node.ELEMENT_NODE&&(r.matches("[data-dxb-slider]")||r.querySelector("[data-dxb-slider]"))){t=!0;break}if(t)break}t&&d()}).observe(document.body,{childList:!0,subtree:!0})})(); \ No newline at end of file From 402568a0b88a4c8cd66016f74b122ad9a3cffb97 Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Sun, 22 Dec 2024 08:48:08 +0200 Subject: [PATCH 10/24] fix: set slider in the middle in both inputs when input number is empty --- __tests__/dxb-slider.test.js | 2 +- dxb-slider.js | 26 ++++++++++---------------- dxb-slider.min.js | 2 +- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/__tests__/dxb-slider.test.js b/__tests__/dxb-slider.test.js index ec1385d..5e2e881 100644 --- a/__tests__/dxb-slider.test.js +++ b/__tests__/dxb-slider.test.js @@ -87,7 +87,7 @@ describe('DXB Slider Core Tests', () => { numberInput.value = ""; numberInput.dispatchEvent(new window.Event('input')); - expect(numberInput.value).toBe(''); + expect(numberInput.value).toBe('50'); }); it('should set initial ARIA attributes', () => { diff --git a/dxb-slider.js b/dxb-slider.js index 10ca5b0..5bce4d0 100644 --- a/dxb-slider.js +++ b/dxb-slider.js @@ -35,7 +35,7 @@ return 0; } - return Math.floor(convertedNum / 2); + return Math.ceil(convertedNum / 2); } function getRangePercent(value = 0, min = 0, max = 0) { @@ -44,23 +44,17 @@ function updateFieldValue(field, value) { - // Handle empty value for number inputs - if (field.type === "number") { + if (['number', 'range'].includes(field.type)) { - // Keep empty for cleared input - field.value = value === "" ? "" : Number(value); - } else if (field.type === "range") { + // Set value to default if empty, else convert to valid number + field.value = value || getDefaultValue(field.max); - // Default to 0 for range inputs if value is empty or NaN - const valueAsNumber = Number(value) || 0; - - // Reset to default if the input is cleared or empty - field.value = valueAsNumber === 0 ? getDefaultValue(field.max) : valueAsNumber; - - // Calculate percentage for range input - const percent = getRangePercent(field.value, field.min, field.max); - field.style.setProperty('--value-percent', `${percent}%`); - field.setAttribute('aria-valuenow', field.value); + // Apply additional settings for range inputs + if (field.type === "range") { + const percent = getRangePercent(field.value, field.min, field.max); + field.style.setProperty('--value-percent', `${percent}%`); + field.setAttribute('aria-valuenow', field.value); + } } } diff --git a/dxb-slider.min.js b/dxb-slider.min.js index 8a81ba6..5b93a55 100644 --- a/dxb-slider.min.js +++ b/dxb-slider.min.js @@ -1 +1 @@ -(()=>{function l(e=0,t=0,a=0){return(e-t)/(a-t)*100}function r(e,t){var a;"number"===e.type?e.value=""===t?"":Number(t):"range"===e.type&&(t=Number(t)||0,e.value=0===t?(a=e.max,a=Number(a),isFinite(a)?Math.floor(a/2):0):t,a=l(e.value,e.min,e.max),e.style.setProperty("--value-percent",a+"%"),e.setAttribute("aria-valuenow",e.value))}let s=new Proxy({},{set(e,t,a){return e[t]=a,document.querySelectorAll(`[data-dxb-proxy-key="${t}"]`).forEach(e=>r(e,a)),!0}});function d(){document.querySelectorAll("[data-dxb-slider]:not([data-dxb-initialized])").forEach(e=>{r=e,(t=document.createElement("div")).className="dxb-slider-container",(i=document.createElement("div")).className="dxb-slider-wrapper",(a=document.createElement("div")).className="dxb-slider-track",r.classList.add("dxb-slider"),r.parentNode.insertBefore(t,r),t.appendChild(i),i.appendChild(a),a.appendChild(r);var t=i,a=document.createElement("input"),r=(a.type="number",a.className="dxb-slider-value",a.setAttribute("tabindex","-1"),a.setAttribute("pattern","[0-9]*"),a.setAttribute("step",e.step),parseFloat(e.step));function d(e){e.target.closest(".dxb-slider-container")&&(s[e.target.dataset.dxbProxyKey]=e.target.value)}r&&r%1!=0?a.setAttribute("inputmode","decimal"):a.setAttribute("inputmode","numeric"),t.appendChild(a),[e,a].forEach(e=>e.addEventListener("input",d)),s[e.dataset.dxbProxyKey]=e.value,e.setAttribute("aria-valuemin",e.min),e.setAttribute("aria-valuemax",e.max);var i=e.value,r=e.min,t=e.max,n=l(i,r,t);e.style.setProperty("--value-percent",n+"%"),a.value=i,a.min=r,a.max=t,a.name=e.name,a.setAttribute("data-dxb-proxy-key",e.dataset.dxbProxyKey),e.setAttribute("aria-valuenow",i),e.setAttribute("data-dxb-initialized","true")})}d(),new MutationObserver(e=>{let t=!1;for(var a of e)if("childList"===a.type){for(var r of a.addedNodes)if(r.nodeType===Node.ELEMENT_NODE&&(r.matches("[data-dxb-slider]")||r.querySelector("[data-dxb-slider]"))){t=!0;break}if(t)break}t&&d()}).observe(document.body,{childList:!0,subtree:!0})})(); \ No newline at end of file +(()=>{function l(e=0,t=0,a=0){return(e-t)/(a-t)*100}function r(e,t){["number","range"].includes(e.type)&&(e.value=t||(t=e.max,t=Number(t),isFinite(t)?Math.ceil(t/2):0),"range"===e.type)&&(t=l(e.value,e.min,e.max),e.style.setProperty("--value-percent",t+"%"),e.setAttribute("aria-valuenow",e.value))}let s=new Proxy({},{set(e,t,a){return e[t]=a,document.querySelectorAll(`[data-dxb-proxy-key="${t}"]`).forEach(e=>r(e,a)),!0}});function d(){document.querySelectorAll("[data-dxb-slider]:not([data-dxb-initialized])").forEach(e=>{r=e,(t=document.createElement("div")).className="dxb-slider-container",(i=document.createElement("div")).className="dxb-slider-wrapper",(a=document.createElement("div")).className="dxb-slider-track",r.classList.add("dxb-slider"),r.parentNode.insertBefore(t,r),t.appendChild(i),i.appendChild(a),a.appendChild(r);var t=i,a=document.createElement("input"),r=(a.type="number",a.className="dxb-slider-value",a.setAttribute("tabindex","-1"),a.setAttribute("pattern","[0-9]*"),a.setAttribute("step",e.step),parseFloat(e.step));function d(e){e.target.closest(".dxb-slider-container")&&(s[e.target.dataset.dxbProxyKey]=e.target.value)}r&&r%1!=0?a.setAttribute("inputmode","decimal"):a.setAttribute("inputmode","numeric"),t.appendChild(a),[e,a].forEach(e=>e.addEventListener("input",d)),s[e.dataset.dxbProxyKey]=e.value,e.setAttribute("aria-valuemin",e.min),e.setAttribute("aria-valuemax",e.max);var i=e.value,r=e.min,t=e.max,n=l(i,r,t);e.style.setProperty("--value-percent",n+"%"),a.value=i,a.min=r,a.max=t,a.name=e.name,a.setAttribute("data-dxb-proxy-key",e.dataset.dxbProxyKey),e.setAttribute("aria-valuenow",i),e.setAttribute("data-dxb-initialized","true")})}d(),new MutationObserver(e=>{let t=!1;for(var a of e)if("childList"===a.type){for(var r of a.addedNodes)if(r.nodeType===Node.ELEMENT_NODE&&(r.matches("[data-dxb-slider]")||r.querySelector("[data-dxb-slider]"))){t=!0;break}if(t)break}t&&d()}).observe(document.body,{childList:!0,subtree:!0})})(); \ No newline at end of file From fd634955d4a4bbecf43906770332fcd233aa72c0 Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Tue, 21 Jan 2025 08:00:26 +0200 Subject: [PATCH 11/24] fix: prevent input value from exceeding maximum value --- dxb-slider.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dxb-slider.js b/dxb-slider.js index 5bce4d0..1184cf8 100644 --- a/dxb-slider.js +++ b/dxb-slider.js @@ -97,6 +97,14 @@ return; } + // Reset the input value to the previously stored value if it exceeds the maximum allowed value + const newValue = Number(e.target.value); + const max = e.target.max; + + if (max && newValue > e.target.max) { + e.target.value = sliderStateProxy[e.target.dataset["dxbProxyKey"]] + } + sliderStateProxy[e.target.dataset["dxbProxyKey"]] = e.target.value; } From fcc97f4b24a1859522f53416c7976b689d39e691 Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Tue, 21 Jan 2025 08:07:13 +0200 Subject: [PATCH 12/24] fix: support new slider `investment` --- index.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.html b/index.html index 4ddf54f..c1bafca 100644 --- a/index.html +++ b/index.html @@ -63,6 +63,7 @@

LTR (Left-to-Right) Mode

@@ -107,6 +108,7 @@

RTL (Right-to-Left) Mode

From 5aa8e3350bcf46d58604c3b767771be86ac91083 Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Tue, 21 Jan 2025 10:44:05 +0200 Subject: [PATCH 13/24] fix: deprecate `data-dxb-proxy-key` data attribute --- __tests__/dxb-slider.test.js | 4 ++-- dxb-slider.js | 20 ++++++++++++++------ index.html | 8 -------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/__tests__/dxb-slider.test.js b/__tests__/dxb-slider.test.js index 5e2e881..c74c13d 100644 --- a/__tests__/dxb-slider.test.js +++ b/__tests__/dxb-slider.test.js @@ -16,7 +16,7 @@ describe('DXB Slider Core Tests', () => { - @@ -116,7 +116,7 @@ describe('DXB Slider Step Tests', () => { - diff --git a/dxb-slider.js b/dxb-slider.js index 1184cf8..604e9c7 100644 --- a/dxb-slider.js +++ b/dxb-slider.js @@ -62,7 +62,10 @@ const sliderStateProxy = new Proxy({}, { set(target, key, value) { target[key] = value; - const fields = document.querySelectorAll(`[data-dxb-proxy-key="${key}"]`); + + const rangeInputWrapper = document.getElementById(key).closest(".dxb-slider-wrapper"); + const fields = rangeInputWrapper.querySelectorAll('input'); + fields.forEach(field => updateFieldValue(field, value)); return true; } @@ -97,15 +100,22 @@ return; } + let proxyKey = e.target.id; + + if (e.target.type === "number") { + const rangeInputWrapper = e.target.closest(".dxb-slider-wrapper"); + proxyKey = rangeInputWrapper.querySelector("input").id; + } + // Reset the input value to the previously stored value if it exceeds the maximum allowed value const newValue = Number(e.target.value); const max = e.target.max; if (max && newValue > e.target.max) { - e.target.value = sliderStateProxy[e.target.dataset["dxbProxyKey"]] + e.target.value = sliderStateProxy[proxyKey] } - sliderStateProxy[e.target.dataset["dxbProxyKey"]] = e.target.value; + sliderStateProxy[proxyKey] = e.target.value; } [rangeInput, numberInput].forEach(input => @@ -113,7 +123,7 @@ ); // Initialize the proxy with the initial value of the range input - sliderStateProxy[rangeInput.dataset["dxbProxyKey"]] = rangeInput.value; + sliderStateProxy[rangeInput.id] = rangeInput.value; // Set initial ARIA attributes rangeInput.setAttribute('aria-valuemin', rangeInput.min); @@ -132,8 +142,6 @@ numberInput.max = max; numberInput.name = rangeInput.name; - numberInput.setAttribute("data-dxb-proxy-key", rangeInput.dataset["dxbProxyKey"]); - rangeInput.setAttribute('aria-valuenow', value); // Mark as initialized diff --git a/index.html b/index.html index c1bafca..2698977 100644 --- a/index.html +++ b/index.html @@ -32,7 +32,6 @@

LTR (Left-to-Right) Mode

Range: 0-100, Default step (1)

@@ -42,7 +41,6 @@

LTR (Left-to-Right) Mode

Range: 1-5, Integer steps

@@ -52,7 +50,6 @@

LTR (Left-to-Right) Mode

@@ -63,7 +60,6 @@

LTR (Left-to-Right) Mode

@@ -77,7 +73,6 @@

RTL (Right-to-Left) Mode

النطاق: 0-100، الخطوة: 1

@@ -87,7 +82,6 @@

RTL (Right-to-Left) Mode

النطاق: 1-5، خطوات صحيحة

@@ -97,7 +91,6 @@

RTL (Right-to-Left) Mode

@@ -108,7 +101,6 @@

RTL (Right-to-Left) Mode

From 68ee689c5cbceaac0e48a8d262e0ff390790e1e8 Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Wed, 22 Jan 2025 09:36:07 +0200 Subject: [PATCH 14/24] fix: `MutationObserver` infinite loop --- __tests__/dxb-slider.test.js | 1 + dxb-slider.js | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/__tests__/dxb-slider.test.js b/__tests__/dxb-slider.test.js index c74c13d..622ebe8 100644 --- a/__tests__/dxb-slider.test.js +++ b/__tests__/dxb-slider.test.js @@ -59,6 +59,7 @@ describe('DXB Slider Core Tests', () => { const newSlider = document.createElement('input'); newSlider.type = 'range'; newSlider.setAttribute('data-dxb-slider', ''); + newSlider.id = "myNewSlider"; // Append the new slider to the DOM document.body.appendChild(newSlider); diff --git a/dxb-slider.js b/dxb-slider.js index 604e9c7..130eb2e 100644 --- a/dxb-slider.js +++ b/dxb-slider.js @@ -61,9 +61,16 @@ // Proxy object to synchronize field values and update all matching fields when a value changes const sliderStateProxy = new Proxy({}, { set(target, key, value) { + + // Guard: If no valid input is found + const validInput = document.getElementById(key); + if (!validInput) { + return; + } + target[key] = value; - const rangeInputWrapper = document.getElementById(key).closest(".dxb-slider-wrapper"); + const rangeInputWrapper = validInput.closest(".dxb-slider-wrapper"); const fields = rangeInputWrapper.querySelectorAll('input'); fields.forEach(field => updateFieldValue(field, value)); @@ -73,6 +80,18 @@ function initDXBSliders() { document.querySelectorAll('[data-dxb-slider]:not([data-dxb-initialized])').forEach(rangeInput => { + + // Guard: Ensure the range input has a valid "id" + // Missing an "id" breaks slider functionality and can cause errors or infinite loops in the MutationObserver. + if (!rangeInput.id) { + console.error( + `DXB Slider Error: A range input is missing a required "id" attribute. Initialization skipped. + Element details: ${rangeInput.outerHTML}` + ); + + return; + } + const wrapper = createSliderStructure(rangeInput); // Create number input programmatically From a4496c08e53b84eaa400a907ed3a5b437925b199 Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Wed, 22 Jan 2025 10:00:41 +0200 Subject: [PATCH 15/24] fix: move reset max logic to proxy --- __tests__/dxb-slider.test.js | 9 +++++++++ dxb-slider.js | 16 ++++++++-------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/__tests__/dxb-slider.test.js b/__tests__/dxb-slider.test.js index 622ebe8..e982671 100644 --- a/__tests__/dxb-slider.test.js +++ b/__tests__/dxb-slider.test.js @@ -77,6 +77,15 @@ describe('DXB Slider Core Tests', () => { expect(slider.value).toBe('80'); }); + it('should clamp value to max when number input exceeds max limit', () => { + numberInput.max = 100; + numberInput.value = 1000; + numberInput.dispatchEvent(new window.Event('input')); + + expect(numberInput.value).toBe('100'); + }); + + it('should synchronize values on number input change (0 based)', () => { numberInput.value = ""; numberInput.dispatchEvent(new window.Event('input')); diff --git a/dxb-slider.js b/dxb-slider.js index 130eb2e..4389ca2 100644 --- a/dxb-slider.js +++ b/dxb-slider.js @@ -68,6 +68,14 @@ return; } + // Reset the input value to the previously stored value if it exceeds the maximum allowed value + const newValue = Number(value); + const max = validInput.max; + + if (max && newValue > max) { + value = max; + } + target[key] = value; const rangeInputWrapper = validInput.closest(".dxb-slider-wrapper"); @@ -126,14 +134,6 @@ proxyKey = rangeInputWrapper.querySelector("input").id; } - // Reset the input value to the previously stored value if it exceeds the maximum allowed value - const newValue = Number(e.target.value); - const max = e.target.max; - - if (max && newValue > e.target.max) { - e.target.value = sliderStateProxy[proxyKey] - } - sliderStateProxy[proxyKey] = e.target.value; } From a4a1d4c6cfec7a26b084b7a4f27494475c0864b7 Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Wed, 22 Jan 2025 10:50:07 +0200 Subject: [PATCH 16/24] chore: decouple input listeners --- dxb-slider.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dxb-slider.js b/dxb-slider.js index 4389ca2..1d56784 100644 --- a/dxb-slider.js +++ b/dxb-slider.js @@ -137,9 +137,9 @@ sliderStateProxy[proxyKey] = e.target.value; } - [rangeInput, numberInput].forEach(input => - input.addEventListener('input', handleInputChange) - ); + rangeInput.addEventListener('input', handleInputChange); + + numberInput.addEventListener('input', handleInputChange); // Initialize the proxy with the initial value of the range input sliderStateProxy[rangeInput.id] = rangeInput.value; From c2927bf12464a6a469b28875b24d132b060086a3 Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Wed, 22 Jan 2025 10:51:00 +0200 Subject: [PATCH 17/24] chore: update dist file --- dxb-slider.min.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dxb-slider.min.js b/dxb-slider.min.js index 5b93a55..dcde6f0 100644 --- a/dxb-slider.min.js +++ b/dxb-slider.min.js @@ -1 +1,2 @@ -(()=>{function l(e=0,t=0,a=0){return(e-t)/(a-t)*100}function r(e,t){["number","range"].includes(e.type)&&(e.value=t||(t=e.max,t=Number(t),isFinite(t)?Math.ceil(t/2):0),"range"===e.type)&&(t=l(e.value,e.min,e.max),e.style.setProperty("--value-percent",t+"%"),e.setAttribute("aria-valuenow",e.value))}let s=new Proxy({},{set(e,t,a){return e[t]=a,document.querySelectorAll(`[data-dxb-proxy-key="${t}"]`).forEach(e=>r(e,a)),!0}});function d(){document.querySelectorAll("[data-dxb-slider]:not([data-dxb-initialized])").forEach(e=>{r=e,(t=document.createElement("div")).className="dxb-slider-container",(i=document.createElement("div")).className="dxb-slider-wrapper",(a=document.createElement("div")).className="dxb-slider-track",r.classList.add("dxb-slider"),r.parentNode.insertBefore(t,r),t.appendChild(i),i.appendChild(a),a.appendChild(r);var t=i,a=document.createElement("input"),r=(a.type="number",a.className="dxb-slider-value",a.setAttribute("tabindex","-1"),a.setAttribute("pattern","[0-9]*"),a.setAttribute("step",e.step),parseFloat(e.step));function d(e){e.target.closest(".dxb-slider-container")&&(s[e.target.dataset.dxbProxyKey]=e.target.value)}r&&r%1!=0?a.setAttribute("inputmode","decimal"):a.setAttribute("inputmode","numeric"),t.appendChild(a),[e,a].forEach(e=>e.addEventListener("input",d)),s[e.dataset.dxbProxyKey]=e.value,e.setAttribute("aria-valuemin",e.min),e.setAttribute("aria-valuemax",e.max);var i=e.value,r=e.min,t=e.max,n=l(i,r,t);e.style.setProperty("--value-percent",n+"%"),a.value=i,a.min=r,a.max=t,a.name=e.name,a.setAttribute("data-dxb-proxy-key",e.dataset.dxbProxyKey),e.setAttribute("aria-valuenow",i),e.setAttribute("data-dxb-initialized","true")})}d(),new MutationObserver(e=>{let t=!1;for(var a of e)if("childList"===a.type){for(var r of a.addedNodes)if(r.nodeType===Node.ELEMENT_NODE&&(r.matches("[data-dxb-slider]")||r.querySelector("[data-dxb-slider]"))){t=!0;break}if(t)break}t&&d()}).observe(document.body,{childList:!0,subtree:!0})})(); \ No newline at end of file +(()=>{function l(e=0,t=0,r=0){return(e-t)/(r-t)*100}function n(e,t){["number","range"].includes(e.type)&&(e.value=t||(t=e.max,t=Number(t),isFinite(t)?Math.ceil(t/2):0),"range"===e.type)&&(t=l(e.value,e.min,e.max),e.style.setProperty("--value-percent",t+"%"),e.setAttribute("aria-valuenow",e.value))}let s=new Proxy({},{set(e,t,r){var a,i,d=document.getElementById(t);if(d)return a=Number(r),(i=d.max)&&in(e,r)),!0}});function i(){document.querySelectorAll("[data-dxb-slider]:not([data-dxb-initialized])").forEach(e=>{var t,r,a,i,d;function n(t){if(t.target.closest(".dxb-slider-container")){let e=t.target.id;var r;"number"===t.target.type&&(r=t.target.closest(".dxb-slider-wrapper"),e=r.querySelector("input").id),s[e]=t.target.value}}e.id?(a=e,(i=document.createElement("div")).className="dxb-slider-container",(r=document.createElement("div")).className="dxb-slider-wrapper",(t=document.createElement("div")).className="dxb-slider-track",a.classList.add("dxb-slider"),a.parentNode.insertBefore(i,a),i.appendChild(r),r.appendChild(t),t.appendChild(a),i=r,(t=document.createElement("input")).type="number",t.className="dxb-slider-value",t.setAttribute("tabindex","-1"),t.setAttribute("pattern","[0-9]*"),t.setAttribute("step",e.step),(a=parseFloat(e.step))&&a%1!=0?t.setAttribute("inputmode","decimal"):t.setAttribute("inputmode","numeric"),i.appendChild(t),e.addEventListener("input",n),t.addEventListener("input",n),s[e.id]=e.value,e.setAttribute("aria-valuemin",e.min),e.setAttribute("aria-valuemax",e.max),d=l(r=e.value,a=e.min,i=e.max),e.style.setProperty("--value-percent",d+"%"),t.value=r,t.min=a,t.max=i,t.name=e.name,e.setAttribute("aria-valuenow",r),e.setAttribute("data-dxb-initialized","true")):console.error(`DXB Slider Error: A range input is missing a required "id" attribute. Initialization skipped. + Element details: `+e.outerHTML)})}i(),new MutationObserver(e=>{let t=!1;for(var r of e)if("childList"===r.type){for(var a of r.addedNodes)if(a.nodeType===Node.ELEMENT_NODE&&(a.matches("[data-dxb-slider]")||a.querySelector("[data-dxb-slider]"))){t=!0;break}if(t)break}t&&i()}).observe(document.body,{childList:!0,subtree:!0})})(); \ No newline at end of file From 7a415141f00e9737fcab5fe29cc26ffdc780939d Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Sun, 2 Feb 2025 10:24:53 +0200 Subject: [PATCH 18/24] feat: improve input validation and handling in `dxb-slider.js` - UPDATED: Enhanced min/max value validation to handle empty attributes correctly - UPDATED: Converted `validInput.min` and `validInput.max` to numbers for consistent comparisons - ADDED: Allow temporary entry of negative (`"-"`) and floating point (`"."`) characters before validation - FIXED: Prevent premature value correction while the user is still typing --- dxb-slider.js | 21 ++++++++++++++++++--- index.html | 22 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/dxb-slider.js b/dxb-slider.js index 1d56784..5b80c50 100644 --- a/dxb-slider.js +++ b/dxb-slider.js @@ -68,14 +68,21 @@ return; } - // Reset the input value to the previously stored value if it exceeds the maximum allowed value + // Convert input value to a number const newValue = Number(value); - const max = validInput.max; + const max = validInput.max !== "" ? Number(validInput.max) : null; + const min = validInput.min !== "" ? Number(validInput.min) : null; - if (max && newValue > max) { + // Reset the input value if it exceeds the maximum allowed value + if (max !== null && newValue > max) { value = max; } + // Reset the input value if it falls below the minimum allowed value + if (min !== null && newValue < min) { + value = min; + } + target[key] = value; const rangeInputWrapper = validInput.closest(".dxb-slider-wrapper"); @@ -134,6 +141,14 @@ proxyKey = rangeInputWrapper.querySelector("input").id; } + const eventValue = e.data; // Capture only the newly entered character from the event + + // Allow negative sign (-) or decimal point (.) to be temporarily entered + // This prevents premature validation while the user is still typing + if (eventValue === "-" || eventValue === ".") { + return; + } + sliderStateProxy[proxyKey] = e.target.value; } diff --git a/index.html b/index.html index 2698977..694846e 100644 --- a/index.html +++ b/index.html @@ -65,6 +65,17 @@

LTR (Left-to-Right) Mode

step="100">

Range: 100-1000, Step: 100

+ + + +

Range: -1000 to 1000, Step: 50

+

RTL (Right-to-Left) Mode

@@ -105,6 +116,17 @@

RTL (Right-to-Left) Mode

value="500" step="100">

النطاق: 100-1000، الخطوة: 100

+ + + + +

النطاق: 1000- الى 1000، الخطوة: 100

From 631254b3d6d82de15dd021f1a0241241e0ba9801 Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Mon, 3 Feb 2025 09:10:55 +0200 Subject: [PATCH 19/24] chore: update tests --- __tests__/dxb-slider.test.js | 148 +++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/__tests__/dxb-slider.test.js b/__tests__/dxb-slider.test.js index e982671..23ee8c4 100644 --- a/__tests__/dxb-slider.test.js +++ b/__tests__/dxb-slider.test.js @@ -113,6 +113,154 @@ describe('DXB Slider Core Tests', () => { }); }); +describe('DXB Slider Negative Value Tests', () => { + let document; + let window; + let slider; + let numberInput; + + beforeEach(() => { + const scriptContent = fs.readFileSync(path.resolve(__dirname, '../dxb-slider.js'), 'utf8'); + + const dom = new JSDOM(` + + + + + + + + `, { runScripts: "dangerously", resources: "usable" }); + + document = dom.window.document; + window = dom.window; + global.document = document; + global.window = window; + + slider = document.querySelector('#negativeSlider'); + numberInput = document.querySelector('.dxb-slider-value'); + }); + + it('should initialize the slider with negative min value', () => { + expect(slider.min).toBe('-50'); + expect(slider.max).toBe('50'); + expect(slider.value).toBe('-25'); + }); + + it('should synchronize values between slider and number input (negative values)', () => { + slider.value = -40; + slider.dispatchEvent(new window.Event('input')); + expect(numberInput.value).toBe('-40'); + }); + + it('should synchronize values when number input is updated with a negative value', () => { + numberInput.value = -10; + numberInput.dispatchEvent(new window.Event('input')); + expect(slider.value).toBe('-10'); + }); + + it('should clamp values to min when below min limit', () => { + numberInput.value = -100; + numberInput.dispatchEvent(new window.Event('input')); + expect(numberInput.value).toBe('-50'); + expect(slider.value).toBe('-50'); + }); + + it('should allow entering "-" without immediately parsing', () => { + + // Since JSDOM does not allow incomplete number input states (like "-"), we have to mock it manually + Object.defineProperty(numberInput, "value", { + get: () => "-", + set: () => { }, // Prevents JSDOM from resetting it + configurable: true + }); + + numberInput.dispatchEvent(new window.InputEvent("input", { data: "-" })); + + expect(numberInput.value).toBe("-"); + }); + +}); + +describe('DXB Slider Floating Point Tests', () => { + let document; + let window; + let slider; + let numberInput; + + beforeEach(() => { + const scriptContent = fs.readFileSync(path.resolve(__dirname, '../dxb-slider.js'), 'utf8'); + + const dom = new JSDOM(` + + + + + + + + `, { runScripts: "dangerously", resources: "usable" }); + + document = dom.window.document; + window = dom.window; + global.document = document; + global.window = window; + + slider = document.querySelector('#mySlider'); + numberInput = document.querySelector('.dxb-slider-value'); + }); + + it('should allow entering "." without immediate parsing', () => { + // Prevent JSDOM from resetting an incomplete decimal state + Object.defineProperty(numberInput, "value", { + get: () => "5.", + set: () => { }, + configurable: true + }); + + numberInput.dispatchEvent(new window.InputEvent("input", { data: "." })); + + expect(numberInput.value).toBe("5."); // Allow incomplete decimal state + }); + + it('should synchronize range and number input values with floating points', () => { + slider.value = "7.3"; + slider.dispatchEvent(new window.Event('input')); + expect(numberInput.value).toBe("7.3"); + }); + + it('should clamp values to max boundary for floating points', () => { + numberInput.value = "20.3"; // Exceeding max + numberInput.dispatchEvent(new window.Event('input')); + expect(numberInput.value).toBe("10.5"); // Clamped to max + }); + + it('should clamp values to min boundary for floating points', () => { + numberInput.value = "-5.0"; // Below min + numberInput.dispatchEvent(new window.Event('input')); + expect(numberInput.value).toBe("0.1"); // Clamped to min + }); + + it('should retain correct floating point precision when stepping up', () => { + numberInput.value = "2.2"; + numberInput.stepUp(); + numberInput.dispatchEvent(new window.Event('input')); + expect(numberInput.value).toBe("2.3"); // Step increment of 0.1 + }); + + it('should retain correct floating point precision when stepping down', () => { + numberInput.value = "3.5"; + numberInput.stepDown(); + numberInput.dispatchEvent(new window.Event('input')); + expect(numberInput.value).toBe("3.4"); // Step decrement of 0.1 + }); +}); + + describe('DXB Slider Step Tests', () => { let document; let window; From 4e88e93082453274ac2f6dca4378b0a7b9c4845d Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Mon, 3 Feb 2025 09:12:40 +0200 Subject: [PATCH 20/24] fix: allow empty/clear input --- __tests__/dxb-slider.test.js | 4 ++-- dxb-slider.js | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/__tests__/dxb-slider.test.js b/__tests__/dxb-slider.test.js index 23ee8c4..3bc4740 100644 --- a/__tests__/dxb-slider.test.js +++ b/__tests__/dxb-slider.test.js @@ -90,14 +90,14 @@ describe('DXB Slider Core Tests', () => { numberInput.value = ""; numberInput.dispatchEvent(new window.Event('input')); - expect(slider.value).toBe('50'); + expect(slider.value).toBe('0'); }); it('should synchronize values on number input change (empty)', () => { numberInput.value = ""; numberInput.dispatchEvent(new window.Event('input')); - expect(numberInput.value).toBe('50'); + expect(numberInput.value).toBe(''); }); it('should set initial ARIA attributes', () => { diff --git a/dxb-slider.js b/dxb-slider.js index 5b80c50..455e120 100644 --- a/dxb-slider.js +++ b/dxb-slider.js @@ -44,17 +44,20 @@ function updateFieldValue(field, value) { - if (['number', 'range'].includes(field.type)) { + if (field.type === "number") { + // Allow the input to be empty or actual value + const val = value === "" ? "" : Number(value); + field.value = val; + } - // Set value to default if empty, else convert to valid number - field.value = value || getDefaultValue(field.max); + if (field.type === "range") { + const val = Number(value) || 0; + field.value = val; // Apply additional settings for range inputs - if (field.type === "range") { - const percent = getRangePercent(field.value, field.min, field.max); - field.style.setProperty('--value-percent', `${percent}%`); - field.setAttribute('aria-valuenow', field.value); - } + const percent = getRangePercent(field.value, field.min, field.max); + field.style.setProperty('--value-percent', `${percent}%`); + field.setAttribute('aria-valuenow', field.value); } } From f789629904db1cc12b680bb22f067e7b203589ee Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Mon, 3 Feb 2025 09:14:21 +0200 Subject: [PATCH 21/24] chore: update dist file --- dxb-slider.min.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dxb-slider.min.js b/dxb-slider.min.js index dcde6f0..992620a 100644 --- a/dxb-slider.min.js +++ b/dxb-slider.min.js @@ -1,2 +1,2 @@ -(()=>{function l(e=0,t=0,r=0){return(e-t)/(r-t)*100}function n(e,t){["number","range"].includes(e.type)&&(e.value=t||(t=e.max,t=Number(t),isFinite(t)?Math.ceil(t/2):0),"range"===e.type)&&(t=l(e.value,e.min,e.max),e.style.setProperty("--value-percent",t+"%"),e.setAttribute("aria-valuenow",e.value))}let s=new Proxy({},{set(e,t,r){var a,i,d=document.getElementById(t);if(d)return a=Number(r),(i=d.max)&&in(e,r)),!0}});function i(){document.querySelectorAll("[data-dxb-slider]:not([data-dxb-initialized])").forEach(e=>{var t,r,a,i,d;function n(t){if(t.target.closest(".dxb-slider-container")){let e=t.target.id;var r;"number"===t.target.type&&(r=t.target.closest(".dxb-slider-wrapper"),e=r.querySelector("input").id),s[e]=t.target.value}}e.id?(a=e,(i=document.createElement("div")).className="dxb-slider-container",(r=document.createElement("div")).className="dxb-slider-wrapper",(t=document.createElement("div")).className="dxb-slider-track",a.classList.add("dxb-slider"),a.parentNode.insertBefore(i,a),i.appendChild(r),r.appendChild(t),t.appendChild(a),i=r,(t=document.createElement("input")).type="number",t.className="dxb-slider-value",t.setAttribute("tabindex","-1"),t.setAttribute("pattern","[0-9]*"),t.setAttribute("step",e.step),(a=parseFloat(e.step))&&a%1!=0?t.setAttribute("inputmode","decimal"):t.setAttribute("inputmode","numeric"),i.appendChild(t),e.addEventListener("input",n),t.addEventListener("input",n),s[e.id]=e.value,e.setAttribute("aria-valuemin",e.min),e.setAttribute("aria-valuemax",e.max),d=l(r=e.value,a=e.min,i=e.max),e.style.setProperty("--value-percent",d+"%"),t.value=r,t.min=a,t.max=i,t.name=e.name,e.setAttribute("aria-valuenow",r),e.setAttribute("data-dxb-initialized","true")):console.error(`DXB Slider Error: A range input is missing a required "id" attribute. Initialization skipped. +(()=>{function l(e=0,t=0,r=0){return(e-t)/(r-t)*100}let u=new Proxy({},{set(e,t,a){var r,i,d,n=document.getElementById(t);if(n)return r=Number(a),i=""!==n.max?Number(n.max):null,d=""!==n.min?Number(n.min):null,null!==i&&i{return t=a,"number"===(e=e).type&&(r=""===t?"":Number(t),e.value=r),void("range"===e.type&&(r=Number(t)||0,e.value=r,t=l(e.value,e.min,e.max),e.style.setProperty("--value-percent",t+"%"),e.setAttribute("aria-valuenow",e.value)));var t,r}),!0}});function i(){document.querySelectorAll("[data-dxb-slider]:not([data-dxb-initialized])").forEach(e=>{var t,r,a,i,d;function n(t){if(t.target.closest(".dxb-slider-container")){let e=t.target.id;"number"===t.target.type&&(r=t.target.closest(".dxb-slider-wrapper"),e=r.querySelector("input").id);var r=t.data;"-"!==r&&"."!==r&&(u[e]=t.target.value)}}e.id?(a=e,(i=document.createElement("div")).className="dxb-slider-container",(r=document.createElement("div")).className="dxb-slider-wrapper",(t=document.createElement("div")).className="dxb-slider-track",a.classList.add("dxb-slider"),a.parentNode.insertBefore(i,a),i.appendChild(r),r.appendChild(t),t.appendChild(a),i=r,(t=document.createElement("input")).type="number",t.className="dxb-slider-value",t.setAttribute("tabindex","-1"),t.setAttribute("pattern","[0-9]*"),t.setAttribute("step",e.step),(a=parseFloat(e.step))&&a%1!=0?t.setAttribute("inputmode","decimal"):t.setAttribute("inputmode","numeric"),i.appendChild(t),e.addEventListener("input",n),t.addEventListener("input",n),u[e.id]=e.value,e.setAttribute("aria-valuemin",e.min),e.setAttribute("aria-valuemax",e.max),d=l(r=e.value,a=e.min,i=e.max),e.style.setProperty("--value-percent",d+"%"),t.value=r,t.min=a,t.max=i,t.name=e.name,e.setAttribute("aria-valuenow",r),e.setAttribute("data-dxb-initialized","true")):console.error(`DXB Slider Error: A range input is missing a required "id" attribute. Initialization skipped. Element details: `+e.outerHTML)})}i(),new MutationObserver(e=>{let t=!1;for(var r of e)if("childList"===r.type){for(var a of r.addedNodes)if(a.nodeType===Node.ELEMENT_NODE&&(a.matches("[data-dxb-slider]")||a.querySelector("[data-dxb-slider]"))){t=!0;break}if(t)break}t&&i()}).observe(document.body,{childList:!0,subtree:!0})})(); \ No newline at end of file From bba3606f390afdef83b106bfe2c1634e25ada99d Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Mon, 3 Feb 2025 09:31:31 +0200 Subject: [PATCH 22/24] chore: remove `getDefaultValue` --- dxb-slider.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/dxb-slider.js b/dxb-slider.js index 455e120..8151544 100644 --- a/dxb-slider.js +++ b/dxb-slider.js @@ -25,19 +25,6 @@ return wrapper; } - function getDefaultValue(num) { - - // Convert the input to a number - const convertedNum = Number(num); - - // Check if the converted number is not finite (e.g., NaN, Infinity) - if (!isFinite(convertedNum)) { - return 0; - } - - return Math.ceil(convertedNum / 2); - } - function getRangePercent(value = 0, min = 0, max = 0) { return ((value - min) / (max - min)) * 100; } From 09d663a4b6734e6dd32d322bc7a13123c759df9b Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Mon, 3 Feb 2025 09:33:02 +0200 Subject: [PATCH 23/24] fix: allow emptiness when clearing in case of min isn't Zero --- dxb-slider.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dxb-slider.js b/dxb-slider.js index 8151544..fe9b3bd 100644 --- a/dxb-slider.js +++ b/dxb-slider.js @@ -70,7 +70,8 @@ // Reset the input value if it falls below the minimum allowed value if (min !== null && newValue < min) { - value = min; + // Allow emptiness when clearing + value = value === "" ? "" : min; } target[key] = value; From 1354a0cb7399f81c941724b4afdd70446843f878 Mon Sep 17 00:00:00 2001 From: mohamedhyouns Date: Thu, 6 Feb 2025 09:08:30 +0200 Subject: [PATCH 24/24] refactor: improve value handling and precision in `updateFieldValue` - ADDED: `roundToPrecision` function to maintain correct floating-point precision - UPDATED: Ensured number input allows empty values without applying calculations - UPDATED: Added explicit parsing for `min`, `max`, and `step` attributes to prevent unintended behavior - UPDATED: Clamped input values within the allowed range before applying rounding - UPDATED: Improved rounding logic to correctly align with step increments - FIXED: Ensured precision consistency when updating number input values --- dxb-slider.js | 29 ++++++++++++++++++++++++++--- dxb-slider.min.js | 4 ++-- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/dxb-slider.js b/dxb-slider.js index fe9b3bd..57ad3d4 100644 --- a/dxb-slider.js +++ b/dxb-slider.js @@ -29,12 +29,35 @@ return ((value - min) / (max - min)) * 100; } + function roundToPrecision(value, step) { + const precision = (step.toString().split(".")[1] || "").length; // Get decimal places in step + return parseFloat(value.toFixed(precision)); // Ensure correct precision + } + function updateFieldValue(field, value) { if (field.type === "number") { - // Allow the input to be empty or actual value - const val = value === "" ? "" : Number(value); - field.value = val; + + // Allow the input to be empty + if (value === "") { + field.value = value; + return; + } + + // Parse min, max, and step values from the field attributes + const min = parseFloat(field.min); + const max = parseFloat(field.max); + const step = parseFloat(field.step) || 1; // Default step to 1 if not specified + const currentValue = parseFloat(value); + + // Ensure the value stays within the min/max range + const val = Math.max(min, Math.min(currentValue, max)); + + // Round the value to the nearest step increment + const roundedValue = Math.round((val - min) / step) * step + min; + + // Ensure precision is maintained when updating the field value + field.value = roundToPrecision(roundedValue, field.step); } if (field.type === "range") { diff --git a/dxb-slider.min.js b/dxb-slider.min.js index 992620a..cd4b213 100644 --- a/dxb-slider.min.js +++ b/dxb-slider.min.js @@ -1,2 +1,2 @@ -(()=>{function l(e=0,t=0,r=0){return(e-t)/(r-t)*100}let u=new Proxy({},{set(e,t,a){var r,i,d,n=document.getElementById(t);if(n)return r=Number(a),i=""!==n.max?Number(n.max):null,d=""!==n.min?Number(n.min):null,null!==i&&i{return t=a,"number"===(e=e).type&&(r=""===t?"":Number(t),e.value=r),void("range"===e.type&&(r=Number(t)||0,e.value=r,t=l(e.value,e.min,e.max),e.style.setProperty("--value-percent",t+"%"),e.setAttribute("aria-valuenow",e.value)));var t,r}),!0}});function i(){document.querySelectorAll("[data-dxb-slider]:not([data-dxb-initialized])").forEach(e=>{var t,r,a,i,d;function n(t){if(t.target.closest(".dxb-slider-container")){let e=t.target.id;"number"===t.target.type&&(r=t.target.closest(".dxb-slider-wrapper"),e=r.querySelector("input").id);var r=t.data;"-"!==r&&"."!==r&&(u[e]=t.target.value)}}e.id?(a=e,(i=document.createElement("div")).className="dxb-slider-container",(r=document.createElement("div")).className="dxb-slider-wrapper",(t=document.createElement("div")).className="dxb-slider-track",a.classList.add("dxb-slider"),a.parentNode.insertBefore(i,a),i.appendChild(r),r.appendChild(t),t.appendChild(a),i=r,(t=document.createElement("input")).type="number",t.className="dxb-slider-value",t.setAttribute("tabindex","-1"),t.setAttribute("pattern","[0-9]*"),t.setAttribute("step",e.step),(a=parseFloat(e.step))&&a%1!=0?t.setAttribute("inputmode","decimal"):t.setAttribute("inputmode","numeric"),i.appendChild(t),e.addEventListener("input",n),t.addEventListener("input",n),u[e.id]=e.value,e.setAttribute("aria-valuemin",e.min),e.setAttribute("aria-valuemax",e.max),d=l(r=e.value,a=e.min,i=e.max),e.style.setProperty("--value-percent",d+"%"),t.value=r,t.min=a,t.max=i,t.name=e.name,e.setAttribute("aria-valuenow",r),e.setAttribute("data-dxb-initialized","true")):console.error(`DXB Slider Error: A range input is missing a required "id" attribute. Initialization skipped. - Element details: `+e.outerHTML)})}i(),new MutationObserver(e=>{let t=!1;for(var r of e)if("childList"===r.type){for(var a of r.addedNodes)if(a.nodeType===Node.ELEMENT_NODE&&(a.matches("[data-dxb-slider]")||a.querySelector("[data-dxb-slider]"))){t=!0;break}if(t)break}t&&i()}).observe(document.body,{childList:!0,subtree:!0})})(); \ No newline at end of file +(()=>{function l(e=0,t=0,a=0){return(e-t)/(a-t)*100}function s(e,t){if("number"===e.type){if(""===t)return void(e.value=t);var a=parseFloat(e.min),r=parseFloat(e.max),i=parseFloat(e.step)||1,n=parseFloat(t),n=Math.max(a,Math.min(n,r)),r=Math.round((n-a)/i)*i+a;e.value=(n=r,i=((i=e.step).toString().split(".")[1]||"").length,parseFloat(n.toFixed(i)))}"range"===e.type&&(a=Number(t)||0,e.value=a,r=l(e.value,e.min,e.max),e.style.setProperty("--value-percent",r+"%"),e.setAttribute("aria-valuenow",e.value))}let u=new Proxy({},{set(e,t,a){var r,i,n,d=document.getElementById(t);if(d)return r=Number(a),i=""!==d.max?Number(d.max):null,n=""!==d.min?Number(d.min):null,null!==i&&is(e,a)),!0}});function i(){document.querySelectorAll("[data-dxb-slider]:not([data-dxb-initialized])").forEach(e=>{var t,a,r,i,n;function d(t){if(t.target.closest(".dxb-slider-container")){let e=t.target.id;"number"===t.target.type&&(a=t.target.closest(".dxb-slider-wrapper"),e=a.querySelector("input").id);var a=t.data;"-"!==a&&"."!==a&&(u[e]=t.target.value)}}e.id?(r=e,(i=document.createElement("div")).className="dxb-slider-container",(a=document.createElement("div")).className="dxb-slider-wrapper",(t=document.createElement("div")).className="dxb-slider-track",r.classList.add("dxb-slider"),r.parentNode.insertBefore(i,r),i.appendChild(a),a.appendChild(t),t.appendChild(r),i=a,(t=document.createElement("input")).type="number",t.className="dxb-slider-value",t.setAttribute("tabindex","-1"),t.setAttribute("pattern","[0-9]*"),t.setAttribute("step",e.step),(r=parseFloat(e.step))&&r%1!=0?t.setAttribute("inputmode","decimal"):t.setAttribute("inputmode","numeric"),i.appendChild(t),e.addEventListener("input",d),t.addEventListener("input",d),u[e.id]=e.value,e.setAttribute("aria-valuemin",e.min),e.setAttribute("aria-valuemax",e.max),n=l(a=e.value,r=e.min,i=e.max),e.style.setProperty("--value-percent",n+"%"),t.value=a,t.min=r,t.max=i,t.name=e.name,e.setAttribute("aria-valuenow",a),e.setAttribute("data-dxb-initialized","true")):console.error(`DXB Slider Error: A range input is missing a required "id" attribute. Initialization skipped. + Element details: `+e.outerHTML)})}i(),new MutationObserver(e=>{let t=!1;for(var a of e)if("childList"===a.type){for(var r of a.addedNodes)if(r.nodeType===Node.ELEMENT_NODE&&(r.matches("[data-dxb-slider]")||r.querySelector("[data-dxb-slider]"))){t=!0;break}if(t)break}t&&i()}).observe(document.body,{childList:!0,subtree:!0})})(); \ No newline at end of file