@@ -111,6 +111,8 @@ constructor(wrapper) {
111111 try {
112112 this . wrapper = wrapper ;
113113 this . slider = wrapper . querySelector ( '[wt-rangeslider-element="slider"]' ) ;
114+ // Guard flag to avoid recursive updates when syncing external inputs
115+ this . __suspendExternalSync = false ;
114116
115117 if ( ! this . slider ) {
116118 throw new Error ( 'Slider element not found within wrapper' ) ;
@@ -140,7 +142,12 @@ constructor(wrapper) {
140142 * @private
141143 */
142144addStyles ( ) {
145+ // Inject styles once per document
146+ const existing = document . getElementById ( 'wt-rangeslider-styles' ) ;
147+ if ( existing ) return ;
148+
143149 const style = document . createElement ( 'style' ) ;
150+ style . id = 'wt-rangeslider-styles' ;
144151 style . textContent = `
145152 [wt-rangeslider-element="slider"] {
146153 position: relative;
@@ -266,12 +273,12 @@ initElements() {
266273setupThumbStyles ( ) {
267274 const setupThumb = ( thumb , input ) => {
268275 // Ensure the input's thumb aligns with our custom thumb
269- const thumbWidth = thumb . offsetWidth ;
276+ const thumbWidth = thumb . offsetWidth || parseInt ( getComputedStyle ( thumb ) . width ) || 20 ;
270277 input . style . setProperty ( '--thumb-width' , `${ thumbWidth } px` ) ;
271278
272279 // Apply styles to ensure proper positioning and hit areas
273280 thumb . style . position = 'absolute' ;
274- thumb . style . pointerEvents = 'none' ; // Let clicks pass through to input
281+ thumb . style . pointerEvents = 'none' ;
275282
276283 // Create a custom property for the thumb offset
277284 this . slider . style . setProperty ( '--thumb-offset' , `${ thumbWidth / 2 } px` ) ;
@@ -334,12 +341,17 @@ updateLeftValues(value) {
334341
335342 // Update form input
336343 if ( this . rangeStart ) {
337- this . rangeStart . value = constrainedValue ;
344+ // Avoid recursive reaction to our own programmatic updates
345+ const valueProp = Object . getOwnPropertyDescriptor ( HTMLInputElement . prototype , 'value' ) ;
346+ this . __suspendExternalSync = true ;
347+ valueProp . set . call ( this . rangeStart , constrainedValue ) ;
348+ this . __suspendExternalSync = false ;
338349 }
339350
340351 // Update display
341352 if ( this . displayStart ) {
342- this . displayStart . innerHTML = this . shouldFormatNumber === 'true' ? this . formatNumber ( constrainedValue ) : constrainedValue ;
353+ const displayLeft = this . shouldFormatNumber === 'true' ? this . formatNumber ( constrainedValue ) : constrainedValue ;
354+ this . displayStart . textContent = String ( displayLeft ) ;
343355 }
344356
345357 // Update visual position
@@ -366,19 +378,23 @@ updateRightValues(value) {
366378
367379 // Update form input
368380 if ( this . rangeEnd ) {
369- this . rangeEnd . value = constrainedValue ;
381+ // Avoid recursive reaction to our own programmatic updates
382+ const valueProp = Object . getOwnPropertyDescriptor ( HTMLInputElement . prototype , 'value' ) ;
383+ this . __suspendExternalSync = true ;
384+ valueProp . set . call ( this . rangeEnd , constrainedValue ) ;
385+ this . __suspendExternalSync = false ;
370386 }
371387
372388 // Update display
373389 if ( this . displayEnd ) {
374390 let finalDisplay = this . shouldFormatNumber === 'true' ? this . formatNumber ( constrainedValue ) : constrainedValue ;
375391
376392 if ( this . rightSuffix && value >= this . sliderMax ) {
377- this . displayEnd . innerHTML = `${ finalDisplay } ${ this . rightSuffix } ` ;
393+ this . displayEnd . textContent = `${ finalDisplay } ${ this . rightSuffix } ` ;
378394 } else if ( this . defaultSuffix ) {
379- this . displayEnd . innerHTML = `${ finalDisplay } ${ this . defaultSuffix } ` ;
395+ this . displayEnd . textContent = `${ finalDisplay } ${ this . defaultSuffix } ` ;
380396 } else {
381- this . displayEnd . innerHTML = finalDisplay ;
397+ this . displayEnd . textContent = String ( finalDisplay ) ;
382398 }
383399 }
384400
@@ -413,55 +429,31 @@ setupEventListeners() {
413429
414430 // Watch for changes to the form inputs
415431 if ( this . rangeStart ) {
432+ // React to user typing immediately
416433 this . rangeStart . addEventListener ( 'input' , ( e ) => {
417434 this . updateLeftValues ( e . target . value ) ;
418435 } ) ;
419-
420- // Watch for programmatic changes
421- const startObserver = new MutationObserver ( ( mutations ) => {
422- mutations . forEach ( ( mutation ) => {
423- if (
424- mutation . type === 'attributes' &&
425- mutation . attributeName === 'value'
426- ) {
427- const value = parseInt ( this . rangeStart . value ) ;
428- if (
429- ! isNaN ( value ) &&
430- value >= this . sliderMin &&
431- value <= this . sliderMax
432- ) {
433- this . updateLeftValues ( value ) ;
434- }
435- }
436- } ) ;
436+ this . rangeStart . addEventListener ( 'change' , ( e ) => {
437+ this . updateLeftValues ( e . target . value ) ;
438+ } ) ;
439+ // Hook into programmatic value assignments
440+ this . hookInputValueSync ( this . rangeStart , ( val ) => {
441+ this . updateLeftValues ( val ) ;
437442 } ) ;
438- startObserver . observe ( this . rangeStart , { attributes : true } ) ;
439443 }
440444
441445 if ( this . rangeEnd ) {
446+ // React to user typing immediately
442447 this . rangeEnd . addEventListener ( 'input' , ( e ) => {
443448 this . updateRightValues ( e . target . value ) ;
444449 } ) ;
445-
446- // Watch for programmatic changes
447- const endObserver = new MutationObserver ( ( mutations ) => {
448- mutations . forEach ( ( mutation ) => {
449- if (
450- mutation . type === 'attributes' &&
451- mutation . attributeName === 'value'
452- ) {
453- const value = parseInt ( this . rangeEnd . value ) ;
454- if (
455- ! isNaN ( value ) &&
456- value >= this . sliderMin &&
457- value <= this . sliderMax
458- ) {
459- this . updateRightValues ( value ) ;
460- }
461- }
462- } ) ;
450+ this . rangeEnd . addEventListener ( 'change' , ( e ) => {
451+ this . updateRightValues ( e . target . value ) ;
452+ } ) ;
453+ // Hook into programmatic value assignments
454+ this . hookInputValueSync ( this . rangeEnd , ( val ) => {
455+ this . updateRightValues ( val ) ;
463456 } ) ;
464- endObserver . observe ( this . rangeEnd , { attributes : true } ) ;
465457 }
466458}
467459
@@ -476,11 +468,12 @@ setupEventListeners() {
476468updateThumbPosition ( input , thumb , range , side ) {
477469 const min = parseInt ( input . min ) ;
478470 const max = parseInt ( input . max ) ;
479- const percent = ( ( input . value - min ) / ( max - min ) ) * 100 ;
471+ const current = parseInt ( input . value ) ;
472+ const percent = ( ( current - min ) / ( max - min ) ) * 100 ;
480473
481474 // Get the thumb's width to account for its dimensions
482- const thumbWidth = thumb . offsetWidth ;
483- const sliderWidth = this . slider . offsetWidth ;
475+ const thumbWidth = thumb . offsetWidth || parseInt ( getComputedStyle ( thumb ) . width ) || 20 ;
476+ const sliderWidth = this . slider . offsetWidth || parseInt ( getComputedStyle ( this . slider ) . width ) || 1 ;
484477
485478 // Calculate the percentage that represents half the thumb width
486479 const thumbHalfPercent = ( thumbWidth / sliderWidth ) * 100 ;
@@ -521,6 +514,68 @@ triggerEvent(element) {
521514 }
522515}
523516
517+ /**
518+ * Hook into a text input's value property to react to programmatic assignments
519+ * @param {HTMLInputElement } input - The input to hook
520+ * @param {(val: string|number) => void } handler - Handler to run on assignment
521+ * @private
522+ */
523+ hookInputValueSync ( input , handler ) {
524+ if ( ! input ) return ;
525+ const valueProp = Object . getOwnPropertyDescriptor ( HTMLInputElement . prototype , 'value' ) ;
526+ const self = this ;
527+ try {
528+ Object . defineProperty ( input , 'value' , {
529+ get ( ) {
530+ return valueProp . get . call ( this ) ;
531+ } ,
532+ set ( v ) {
533+ valueProp . set . call ( this , v ) ;
534+ if ( ! self . __suspendExternalSync ) {
535+ handler ( v ) ;
536+ }
537+ } ,
538+ configurable : true ,
539+ enumerable : true ,
540+ } ) ;
541+ } catch ( err ) {
542+ // Fallback: rely on 'input'/'change' listeners if defineProperty fails
543+ }
544+ }
545+
546+ /**
547+ * Public API: set left range value
548+ * @param {number|string } value
549+ */
550+ setFrom ( value ) {
551+ this . updateLeftValues ( value ) ;
552+ }
553+
554+ /**
555+ * Public API: set right range value
556+ * @param {number|string } value
557+ */
558+ setTo ( value ) {
559+ this . updateRightValues ( value ) ;
560+ }
561+
562+ /**
563+ * Public API: set both range values atomically and in correct order
564+ * @param {number|string } from
565+ * @param {number|string } to
566+ */
567+ setRange ( from , to ) {
568+ this . updateLeftValues ( from ) ;
569+ this . updateRightValues ( to ) ;
570+ }
571+
572+ /**
573+ * Public API: reset slider to configured bounds
574+ */
575+ reset ( ) {
576+ this . setRange ( this . sliderMin , this . sliderMax ) ;
577+ }
578+
524579/**
525580 * Validates that all required elements are present
526581 * @private
0 commit comments