@zywave/zui-slider 4.3.2 → 4.4.0-pre.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,12 +4,14 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
+ var _a;
7
8
  import { ZuiFormAssociatedElement } from '@zywave/zui-base';
8
- import { html } from 'lit';
9
+ import { html, nothing } from 'lit';
9
10
  import { property } from 'lit/decorators.js';
11
+ import { classMap } from 'lit/directives/class-map.js';
12
+ import { live } from 'lit/directives/live.js';
10
13
  import { styleMap } from 'lit/directives/style-map.js';
11
14
  import { style } from './zui-slider-css.js';
12
- const DEFAULT_MAX = 100;
13
15
  /**
14
16
  * A range form control for choosing values along a slider.
15
17
  * @element zui-slider
@@ -18,27 +20,89 @@ const DEFAULT_MAX = 100;
18
20
  * @attr {boolean} [disabled=false] - Represents whether a user can make changes to this element; if true, the value of this element will be excluded from the form submission
19
21
  * @attr {boolean} [readonly=false] - Represents whether a user can make changes to this element; the value of this element will still be included in the form submission
20
22
  * @attr {boolean} [autofocus=false] - If true, this element will be focused when connected to the document
21
- * @attr {number} [value=50] - Represents the value of the input. Can be set to a default value, and will reflect the value provided by the user when interactive with the control
23
+ * @attr {number} [value=50] - Represents the value of the input (single mode). Can be set to a default value, and will reflect the value provided by the user when interactive with the control
24
+ * @attr {boolean} [range=false] - Enables range mode with two thumbs for selecting a range
25
+ * @attr {number} [value-start=0] - Represents the start (lower) value in range mode
26
+ * @attr {number} [value-end=100] - Represents the end (upper) value in range mode
27
+ * @attr {number} [min=0] - Represents the minimum permitted value
28
+ * @attr {number} [max=100] - Represents the maximum permitted value
29
+ * @attr {number} [step=0] - Represents the stepping interval; 0 means any value is allowed
30
+ * @attr {boolean} [show-min-max=false] - Shows the min and max values beneath the slider
22
31
  *
23
32
  * @prop {string | null} [name=null] - The name of this element that is associated with form submission
24
33
  * @prop {boolean} [disabled=false] - Represents whether a user can make changes to this element; if true, the value of this element will be excluded from the form submission
25
34
  * @prop {boolean} [readOnly=false] - Represents whether a user can make changes to this element; the value of this element will still be included in the form submission
26
35
  * @prop {boolean} [autofocus=false] - If true, this element will be focused when connected to the document
27
- * @prop {number} [valueAsNumber=50] - Returns the value of the element, interpreted as one of the following, in order: A number, NaN if conversion is impossible
28
- * @prop {string} [value='50'] - Represents the value of the input. Can be set to a default value, and will reflect the value provided by the user when interactive with the control
29
- * @prop {number} [progress=50] - Determines visual placement of the slider thumb along the line
36
+ * @prop {number} [valueAsNumber=50] - Returns the value as a number. Invalid or non-numeric values are clamped to min; empty string (representing an in-progress edit) returns NaN
37
+ * @prop {string} [value='50'] - Represents the value of the input (single mode). Can be set to a default value, and will reflect the value provided by the user when interactive with the control
38
+ * @prop {number} [progress=50] - Determines visual placement of the slider thumb along the line (single mode)
39
+ * @prop {boolean} [range=false] - Enables range mode with two thumbs for selecting a range
40
+ * @prop {string} [valueStart='0'] - Represents the start (lower) value in range mode
41
+ * @prop {string} [valueEnd='100'] - Represents the end (upper) value in range mode
42
+ * @prop {number} [progressStart=0] - Determines visual placement of the start thumb in range mode
43
+ * @prop {number} [progressEnd=100] - Determines visual placement of the end thumb in range mode
44
+ * @prop {number} [min=0] - Represents the minimum permitted value
45
+ * @prop {number} [max=100] - Represents the maximum permitted value
46
+ * @prop {number} [step=0] - Represents the stepping interval; 0 means any value is allowed
47
+ * @prop {boolean} [showMinMax=false] - Shows the min and max values beneath the slider
30
48
  *
31
- * @cssprop [--zui-slider-thumb-size=1.875rem (30px)] - Point of contact to grab and slide to change value
49
+ * @cssprop [--zui-slider-thumb-size=0.875rem (14px)] - Controls the visual dot size and thumb hit-area (hit area is 3× this value)
50
+ * @cssprop [--zui-slider-input-width=7ch] - Width of the floating value input above each thumb
32
51
  *
33
- * @event {CustomEvent} change - Fires when value changes, details contain `value`
52
+ * @event {CustomEvent} change - Fires when value changes; in single mode detail is the value string; in range mode detail is { valueStart, valueEnd }
34
53
  */
35
54
  export class ZuiSlider extends ZuiFormAssociatedElement {
36
55
  constructor() {
37
56
  super(...arguments);
38
- this.#defaultValue = `${DEFAULT_MAX / 2}`;
39
- this.#value = `${DEFAULT_MAX / 2}`;
57
+ this.#defaultValue = '50';
58
+ this.#defaultValueStart = '0';
59
+ this.#defaultValueEnd = '100';
60
+ this.#value = '50';
61
+ this.#valueStart = '0';
62
+ this.#valueEnd = '100';
63
+ this.#thumbInputState = new Map([
64
+ ['thumb', { visible: false, focused: false }],
65
+ ['startThumb', { visible: false, focused: false }],
66
+ ['endThumb', { visible: false, focused: false }],
67
+ ]);
68
+ // Pre-bound floating input handlers cached to avoid new function references on every render
69
+ this.#onThumbFloatingInput = this.#onFloatingInput('thumb', (v) => (this.value = v));
70
+ this.#onStartThumbFloatingInput = this.#onFloatingInput('startThumb', (v) => (this.valueStart = v));
71
+ this.#onEndThumbFloatingInput = this.#onFloatingInput('endThumb', (v) => (this.valueEnd = v));
72
+ // Cached floating input change handlers — flush debounce and dispatch immediately on commit (Enter/blur)
73
+ this.#onThumbFloatingChange = this.#makeFloatingChange('thumb', (v) => (this.value = v), () => this.#onChange());
74
+ this.#onStartThumbFloatingChange = this.#makeFloatingChange('startThumb', (v) => (this.valueStart = v), () => this.#onRangeChange());
75
+ this.#onEndThumbFloatingChange = this.#makeFloatingChange('endThumb', (v) => (this.valueEnd = v), () => this.#onRangeChange());
76
+ // Cached range drag input handlers
77
+ this.#onRangeStartInput = this.#onRangeInput('start');
78
+ this.#onRangeEndInput = this.#onRangeInput('end');
79
+ // Cached pointer/focus handlers per thumb — prevents new closures on every render
80
+ this.#h = {
81
+ thumb: {
82
+ show: () => this.#showThumbInput('thumb'),
83
+ hide: () => this.#scheduleHideThumbInput('thumb'),
84
+ focus: () => this.#focusFloatingInput('thumb'),
85
+ blur: () => this.#blurFloatingInput('thumb'),
86
+ },
87
+ startThumb: {
88
+ show: () => this.#showThumbInput('startThumb'),
89
+ hide: () => this.#scheduleHideThumbInput('startThumb'),
90
+ focus: () => this.#focusFloatingInput('startThumb'),
91
+ blur: () => this.#blurFloatingInput('startThumb'),
92
+ },
93
+ endThumb: {
94
+ show: () => this.#showThumbInput('endThumb'),
95
+ hide: () => this.#scheduleHideThumbInput('endThumb'),
96
+ focus: () => this.#focusFloatingInput('endThumb'),
97
+ blur: () => this.#blurFloatingInput('endThumb'),
98
+ },
99
+ };
40
100
  /**
41
- * Represents the maximum permitted value
101
+ * Enables range mode with two thumbs for selecting a value range
102
+ */
103
+ this.range = false;
104
+ /**
105
+ * Represents the minimum permitted value
42
106
  */
43
107
  this.min = 0;
44
108
  /**
@@ -50,121 +114,487 @@ export class ZuiSlider extends ZuiFormAssociatedElement {
50
114
  */
51
115
  this.step = 0;
52
116
  /**
53
- * Represents that this control must be filled in for form submission
117
+ * Shows the min and max values beneath the slider
54
118
  */
55
- this.noText = false;
119
+ this.showMinMax = false;
120
+ }
121
+ #defaultValue;
122
+ #defaultValueStart;
123
+ #defaultValueEnd;
124
+ #value;
125
+ #valueStart;
126
+ #valueEnd;
127
+ #thumbInputState;
128
+ // Pre-bound floating input handlers cached to avoid new function references on every render
129
+ #onThumbFloatingInput;
130
+ #onStartThumbFloatingInput;
131
+ #onEndThumbFloatingInput;
132
+ // Cached floating input change handlers — flush debounce and dispatch immediately on commit (Enter/blur)
133
+ #onThumbFloatingChange;
134
+ #onStartThumbFloatingChange;
135
+ #onEndThumbFloatingChange;
136
+ // Cached range drag input handlers
137
+ #onRangeStartInput;
138
+ #onRangeEndInput;
139
+ // Cached pointer/focus handlers per thumb — prevents new closures on every render
140
+ #h;
141
+ static get styles() {
142
+ return [super.styles, style];
143
+ }
144
+ connectedCallback() {
145
+ super.connectedCallback();
146
+ this.updateComplete.then(() => {
147
+ if (this.range) {
148
+ const [startInput, endInput] = this.#rangeInputs();
149
+ this.#valueStart = startInput.value;
150
+ this.#valueEnd = endInput.value;
151
+ this.#defaultValueStart = startInput.value;
152
+ this.#defaultValueEnd = endInput.value;
153
+ this._setFormValue(`${startInput.value},${endInput.value}`);
154
+ }
155
+ else {
156
+ const input = this.#singleInput();
157
+ this.#value = input.value;
158
+ this.#defaultValue = input.value;
159
+ this._setFormValue(input.value);
160
+ }
161
+ });
162
+ }
163
+ disconnectedCallback() {
164
+ super.disconnectedCallback();
165
+ this.#clearAllThumbInputState();
166
+ // Schedule a re-render so the cleared visibility state is reflected in the DOM.
167
+ // Lit defers this render until the element reconnects if it is currently disconnected.
168
+ this.requestUpdate();
169
+ }
170
+ updated(changed) {
171
+ if (changed.has('disabled') && this.disabled) {
172
+ this.#clearAllThumbInputState();
173
+ }
174
+ if (changed.has('min') || changed.has('max')) {
175
+ // The native inputs have already clamped (and step-snapped) their values to the new bounds.
176
+ // Read them back and sync internal state + form value. If the value changed, request a
177
+ // re-render so the floating input (which binds to #value/#valueStart/#valueEnd) stays in sync.
178
+ if (this.range) {
179
+ const [startInput, endInput] = this.#rangeInputs();
180
+ const newStart = startInput.value;
181
+ const newEnd = endInput.value;
182
+ if (newStart !== this.#valueStart || newEnd !== this.#valueEnd) {
183
+ this.#valueStart = newStart;
184
+ this.#valueEnd = newEnd;
185
+ this.requestUpdate();
186
+ }
187
+ this._setFormValue(`${this.#valueStart},${this.#valueEnd}`);
188
+ }
189
+ else {
190
+ const newValue = this.#singleInput().value;
191
+ if (newValue !== this.#value) {
192
+ this.#value = newValue;
193
+ this.requestUpdate();
194
+ }
195
+ this._setFormValue(this.#value);
196
+ }
197
+ }
198
+ }
199
+ #singleInput() {
200
+ return this.shadowRoot.querySelector('input[type="range"]');
201
+ }
202
+ #rangeInputs() {
203
+ const inputs = this.shadowRoot.querySelectorAll('input[type="range"]');
204
+ return [inputs[0], inputs[1]];
56
205
  }
57
206
  get _focusControlSelector() {
58
207
  return 'input';
59
208
  }
60
209
  get _formValue() {
61
- return this.value;
210
+ return this.range ? `${this.#valueStart},${this.#valueEnd}` : this.#value;
62
211
  }
63
212
  formResetCallback() {
64
- this.value = this.#defaultValue;
213
+ this.#clearAllThumbInputState();
214
+ // Always request a re-render so the cleared thumb visibility is reflected even when the
215
+ // reset value equals the current value (the named setter requestUpdate would be a no-op).
216
+ this.requestUpdate();
217
+ if (this.range) {
218
+ this.valueStart = this.#defaultValueStart;
219
+ this.valueEnd = this.#defaultValueEnd;
220
+ }
221
+ else {
222
+ this.value = this.#defaultValue;
223
+ }
224
+ }
225
+ #clearAllThumbInputState() {
226
+ for (const entry of this.#thumbInputState.values()) {
227
+ clearTimeout(entry.timer);
228
+ clearTimeout(entry.debounceTimer);
229
+ entry.timer = undefined;
230
+ entry.debounceTimer = undefined;
231
+ entry.visible = false;
232
+ entry.focused = false;
233
+ }
234
+ }
235
+ /**
236
+ * Clamps a raw string value to the current [min, max] range.
237
+ * Empty string is passed through unchanged (represents an in-progress floating input edit).
238
+ * Non-numeric strings are passed through unchanged.
239
+ */
240
+ #clampToRange(rawVal) {
241
+ if (rawVal === '') {
242
+ return rawVal;
243
+ }
244
+ const num = parseFloat(rawVal);
245
+ if (isNaN(num)) {
246
+ return rawVal;
247
+ }
248
+ return String(Math.min(this.max, Math.max(this.min, num)));
65
249
  }
66
- #defaultValue;
67
- #value;
68
250
  get value() {
69
251
  return this.#value;
70
252
  }
71
- set value(val) {
253
+ set value(rawVal) {
254
+ rawVal = this.#clampToRange(rawVal);
72
255
  const oldVal = this.#value;
73
- val = this._ensureValidValue(val);
74
- this.#value = val;
75
- this._setFormValue(this.#value);
256
+ this.#value = rawVal;
257
+ this._setFormValue(rawVal);
76
258
  this.requestUpdate('value', oldVal);
77
259
  }
78
260
  get valueAsNumber() {
79
261
  return parseFloat(this.#value);
80
262
  }
263
+ get valueStart() {
264
+ return this.#valueStart;
265
+ }
266
+ set valueStart(rawVal) {
267
+ rawVal = this.#clampToRange(rawVal);
268
+ const oldVal = this.#valueStart;
269
+ this.#valueStart = rawVal;
270
+ this._setFormValue(`${rawVal},${this.#valueEnd}`);
271
+ this.requestUpdate('valueStart', oldVal);
272
+ }
273
+ get valueEnd() {
274
+ return this.#valueEnd;
275
+ }
276
+ set valueEnd(rawVal) {
277
+ rawVal = this.#clampToRange(rawVal);
278
+ const oldVal = this.#valueEnd;
279
+ this.#valueEnd = rawVal;
280
+ this._setFormValue(`${this.#valueStart},${rawVal}`);
281
+ this.requestUpdate('valueEnd', oldVal);
282
+ }
283
+ get #range() {
284
+ return this.max - this.min;
285
+ }
286
+ get #progressColor() {
287
+ return this.disabled ? 'var(--zui-gray)' : 'var(--zui-blue)';
288
+ }
289
+ #isVisible(flag) {
290
+ return this.#thumbInputState.get(flag).visible && !this.disabled;
291
+ }
81
292
  get progress() {
82
- return ((this.valueAsNumber - this.min) / (this.max - this.min)) * 100;
293
+ return this.#computeProgress(this.#value);
83
294
  }
84
- static get styles() {
85
- return [super.styles, style];
295
+ get progressStart() {
296
+ return this.#computeProgress(this.#valueStart);
86
297
  }
87
- connectedCallback() {
88
- super.connectedCallback();
89
- // we want to go a little faster than LitElement and behave more like native HTML Form Associated Elements
90
- let value = this.getAttribute('value') ?? this.value;
91
- const max = this.getAttribute('max') ?? this.max;
92
- const min = this.getAttribute('min') ?? this.min;
93
- value = this._ensureValidValue(value, min, max);
94
- this.#defaultValue = value;
95
- this._setFormValue(value.toString());
298
+ get progressEnd() {
299
+ return this.#computeProgress(this.#valueEnd);
300
+ }
301
+ #computeProgress(rawValue) {
302
+ if (this.#range === 0) {
303
+ return 0;
304
+ }
305
+ const num = parseFloat(rawValue);
306
+ return isNaN(num) ? 0 : parseFloat((((num - this.min) / this.#range) * 100).toFixed(4));
307
+ }
308
+ #singleTrackBackground(progress) {
309
+ const c = this.#progressColor;
310
+ const stop = _a.#thumbPositionCSS(progress);
311
+ return `linear-gradient(to right, transparent var(--zui-slider-thumb-size), ${c} var(--zui-slider-thumb-size), ${c} ${stop}, var(--zui-gray-200) ${stop}, var(--zui-gray-200) calc(100% - var(--zui-slider-thumb-size)), transparent calc(100% - var(--zui-slider-thumb-size)))`;
312
+ }
313
+ #rangeTrackBackground(progressStart, progressEnd) {
314
+ const c = this.#progressColor;
315
+ const startStop = _a.#thumbPositionCSS(progressStart);
316
+ const endStop = _a.#thumbPositionCSS(progressEnd);
317
+ return `linear-gradient(to right, transparent var(--zui-slider-thumb-size), var(--zui-gray-200) var(--zui-slider-thumb-size), var(--zui-gray-200) ${startStop}, ${c} ${startStop}, ${c} ${endStop}, var(--zui-gray-200) ${endStop}, var(--zui-gray-200) calc(100% - var(--zui-slider-thumb-size)), transparent calc(100% - var(--zui-slider-thumb-size)))`;
96
318
  }
97
319
  render() {
98
- const progressColor = this.disabled ? 'var(--zui-gray)' : 'var(--zui-blue)';
99
- const styles = {
100
- background: `linear-gradient(to right, ${progressColor} 0%, ${progressColor} ${this.progress}%, var(--zui-gray-200) ${this.progress}%, var(--zui-gray-200) 100%)`,
101
- };
102
- return html `<input
103
- style=${styleMap(styles)}
104
- type="range"
105
- .value="${this.value}"
106
- .min="${this.min}"
107
- .max="${this.max}"
108
- .step="${this.step}"
109
- ?disabled="${this.disabled}"
110
- @input="${this._onInput}"
111
- @change="${this._onChange}"
112
- />${this._renderText()}`;
320
+ return html `${this.range ? this.#renderRange() : this.#renderSingle()}${this.#renderMinMaxLabels()}`;
113
321
  }
114
- _renderText() {
115
- if (this.noText) {
116
- return html ``;
117
- }
118
- const thumbOffset = `var(--zui-slider-thumb-size) * ${this.progress / 100}`;
119
- const styles = {
120
- left: `calc(${this.progress}% - ${thumbOffset} + 0.125rem)`,
121
- };
122
- return html `<span style=${styleMap(styles)}>${this.value}</span>`;
322
+ #renderSingle() {
323
+ const progress = this.progress;
324
+ return html `
325
+ <div class="single-wrapper">
326
+ <input
327
+ aria-label="Slider value"
328
+ style=${styleMap({ background: this.#singleTrackBackground(progress) })}
329
+ type="range"
330
+ .min="${String(this.min)}"
331
+ .max="${String(this.max)}"
332
+ .step="${this.step > 0 ? String(this.step) : '1'}"
333
+ .value="${this.#value}"
334
+ ?disabled="${this.disabled}"
335
+ @input="${this.#onInput}"
336
+ @change="${this.#onChange}"
337
+ @pointerenter="${this.#h.thumb.show}"
338
+ @pointerleave="${this.#h.thumb.hide}"
339
+ @focus="${this.#h.thumb.show}"
340
+ @blur="${this.#h.thumb.hide}"
341
+ />
342
+ ${this.#renderStepDots()}
343
+ ${this.#renderFloatingInput(this.#value, this.#onThumbFloatingInput, this.#onThumbFloatingChange, 'thumb', this.#isVisible('thumb'), progress)}
344
+ </div>
345
+ `;
123
346
  }
124
- _onInput(e) {
125
- this.value = e.target.value;
347
+ #renderRange() {
348
+ const progressStart = this.progressStart;
349
+ const progressEnd = this.progressEnd;
350
+ return html `
351
+ <div
352
+ class="range-wrapper"
353
+ style=${styleMap({ background: this.#rangeTrackBackground(progressStart, progressEnd) })}
354
+ >
355
+ ${this.#renderStepDots()}${this.#renderRangeInput('start')}
356
+ ${this.#renderFloatingInput(this.#valueStart, this.#onStartThumbFloatingInput, this.#onStartThumbFloatingChange, 'startThumb', this.#isVisible('startThumb'), progressStart)}
357
+ ${this.#renderRangeInput('end')}
358
+ ${this.#renderFloatingInput(this.#valueEnd, this.#onEndThumbFloatingInput, this.#onEndThumbFloatingChange, 'endThumb', this.#isVisible('endThumb'), progressEnd)}
359
+ </div>
360
+ `;
126
361
  }
127
- _onChange() {
128
- this.dispatchEvent(new CustomEvent('change', { detail: this.value, bubbles: true }));
362
+ #renderRangeInput(which) {
363
+ const flag = which === 'start' ? 'startThumb' : 'endThumb';
364
+ const val = which === 'start' ? this.#valueStart : this.#valueEnd;
365
+ const onInput = which === 'start' ? this.#onRangeStartInput : this.#onRangeEndInput;
366
+ const h = this.#h[flag];
367
+ // live() is required: when #onRangeInput snaps the DOM value back via direct input.value
368
+ // assignment (drag past the other thumb), there is no state change → no re-render. live()
369
+ // forces Lit to re-read the current DOM value and re-sync it on every render.
370
+ return html `
371
+ <input
372
+ aria-label="${which === 'start' ? 'Range start' : 'Range end'}"
373
+ class="range-${which}"
374
+ type="range"
375
+ .min="${String(this.min)}"
376
+ .max="${String(this.max)}"
377
+ .step="${this.step > 0 ? String(this.step) : '1'}"
378
+ .value="${live(val)}"
379
+ ?disabled="${this.disabled}"
380
+ @input="${onInput}"
381
+ @change="${this.#onRangeChange}"
382
+ @pointerenter="${h.show}"
383
+ @pointerleave="${h.hide}"
384
+ @focus="${h.show}"
385
+ @blur="${h.hide}"
386
+ />
387
+ `;
129
388
  }
130
- _ensureValidValue(value, min, max) {
131
- if (value === '') {
132
- return value;
389
+ #renderFloatingInput(val, onInput, onFloatingChange, flag, visible, progress) {
390
+ const h = this.#h[flag];
391
+ // live() is required on the number input: when the debounced setter clamps a value to the
392
+ // same string it already holds (e.g. typing 150 when max=100 clamps back to '100'), no
393
+ // reactive property change occurs and Lit skips the DOM update. live() forces a re-read.
394
+ return html `
395
+ <div
396
+ class=${classMap({ 'thumb-input': true, 'thumb-input--visible': visible })}
397
+ style=${styleMap({ left: _a.#thumbPositionCSS(progress) })}
398
+ @pointerenter="${h.show}"
399
+ @pointerleave="${h.hide}"
400
+ >
401
+ <input
402
+ aria-label="${flag === 'startThumb'
403
+ ? 'Range start value'
404
+ : flag === 'endThumb'
405
+ ? 'Range end value'
406
+ : 'Slider value'}"
407
+ type="number"
408
+ .value="${live(val)}"
409
+ .min="${String(this.min)}"
410
+ .max="${String(this.max)}"
411
+ .step="${this.step > 0 ? String(this.step) : '1'}"
412
+ ?disabled="${this.disabled}"
413
+ ?readonly="${this.readOnly}"
414
+ @input="${onInput}"
415
+ @change="${onFloatingChange}"
416
+ @focus="${h.focus}"
417
+ @blur="${h.blur}"
418
+ />
419
+ </div>
420
+ `;
421
+ }
422
+ #renderStepDots() {
423
+ if (this.step <= 0 || this.#range === 0) {
424
+ return nothing;
133
425
  }
134
- min = min ?? this.min;
135
- max = max ?? this.max;
136
- if (typeof value === 'string') {
137
- value = parseFloat(value);
426
+ const count = Math.round(this.#range / this.step);
427
+ if (count > 100) {
428
+ return nothing;
138
429
  }
139
- if (typeof min === 'string') {
140
- min = parseFloat(min);
430
+ const dots = [];
431
+ for (let i = 0; i <= count; i++) {
432
+ let left;
433
+ if (i === 0) {
434
+ left = 'var(--zui-slider-thumb-size)';
435
+ }
436
+ else if (i === count) {
437
+ left = 'calc(100% - var(--zui-slider-thumb-size))';
438
+ }
439
+ else {
440
+ const pos = parseFloat((((i * this.step) / this.#range) * 100).toFixed(4));
441
+ left = _a.#thumbPositionCSS(pos);
442
+ }
443
+ dots.push(html `<span class="step-dot" style="left: ${left}"></span>`);
141
444
  }
142
- if (typeof max === 'string') {
143
- max = parseFloat(max);
445
+ return html `<div class="step-dots">${dots}</div>`;
446
+ }
447
+ #renderMinMaxLabels() {
448
+ if (!this.showMinMax) {
449
+ return nothing;
144
450
  }
145
- if (value < min) {
146
- value = min;
451
+ return html `
452
+ <div class="min-max-labels">
453
+ <span class="min-max-label">${this.min}</span>
454
+ <span class="min-max-label">${this.max}</span>
455
+ </div>
456
+ `;
457
+ }
458
+ #onInput(e) {
459
+ if (this.readOnly) {
460
+ return;
147
461
  }
148
- else if (value > max) {
149
- value = max;
462
+ this.value = e.target.value;
463
+ }
464
+ #onChange() {
465
+ this.dispatchEvent(new CustomEvent('change', { detail: this.#value, bubbles: true }));
466
+ }
467
+ #processFloatingValue(raw) {
468
+ const stepDecimals = this.step > 0 ? (String(this.step).split('.')[1] ?? '').length : 0;
469
+ const stepped = this.step > 0 ? Math.round((raw - this.min) / this.step) * this.step + this.min : raw;
470
+ const clamped = Math.min(this.max, Math.max(this.min, stepped));
471
+ return stepDecimals > 0 ? clamped.toFixed(stepDecimals) : String(Math.round(clamped));
472
+ }
473
+ #makeFloatingChange(flag, setter, dispatch) {
474
+ return (e) => {
475
+ if (this.readOnly) {
476
+ return;
477
+ }
478
+ const input = e.target;
479
+ if (input.value === '') {
480
+ return;
481
+ }
482
+ const entry = this.#thumbInputState.get(flag);
483
+ clearTimeout(entry.debounceTimer);
484
+ entry.debounceTimer = undefined;
485
+ setter(this.#processFloatingValue(parseFloat(input.value)));
486
+ dispatch();
487
+ };
488
+ }
489
+ #onFloatingInput(flag, setter) {
490
+ return (e) => {
491
+ if (this.readOnly) {
492
+ return;
493
+ }
494
+ const input = e.target;
495
+ if (input.value === '') {
496
+ return;
497
+ }
498
+ const raw = parseFloat(input.value);
499
+ const entry = this.#thumbInputState.get(flag);
500
+ clearTimeout(entry.debounceTimer);
501
+ entry.debounceTimer = setTimeout(() => setter(this.#processFloatingValue(raw)), 300);
502
+ };
503
+ }
504
+ #onRangeInput(which) {
505
+ return (e) => {
506
+ if (this.readOnly) {
507
+ return;
508
+ }
509
+ const input = e.target;
510
+ if (which === 'start') {
511
+ if (parseFloat(input.value) >= parseFloat(this.#valueEnd)) {
512
+ input.value = this.#valueStart;
513
+ return;
514
+ }
515
+ this.valueStart = input.value;
516
+ }
517
+ else {
518
+ if (parseFloat(input.value) <= parseFloat(this.#valueStart)) {
519
+ input.value = this.#valueEnd;
520
+ return;
521
+ }
522
+ this.valueEnd = input.value;
523
+ }
524
+ };
525
+ }
526
+ #onRangeChange() {
527
+ this.dispatchEvent(new CustomEvent('change', {
528
+ detail: { valueStart: this.#valueStart, valueEnd: this.#valueEnd },
529
+ bubbles: true,
530
+ }));
531
+ }
532
+ #showThumbInput(flag) {
533
+ if (this.disabled) {
534
+ return;
535
+ }
536
+ const entry = this.#thumbInputState.get(flag);
537
+ clearTimeout(entry.timer);
538
+ entry.timer = undefined;
539
+ entry.visible = true;
540
+ this.requestUpdate();
541
+ }
542
+ #scheduleHideThumbInput(flag) {
543
+ const entry = this.#thumbInputState.get(flag);
544
+ clearTimeout(entry.timer);
545
+ entry.timer = setTimeout(() => {
546
+ if (!entry.focused) {
547
+ entry.visible = false;
548
+ this.requestUpdate();
549
+ }
550
+ }, 100);
551
+ }
552
+ #focusFloatingInput(flag) {
553
+ if (this.disabled) {
554
+ return;
150
555
  }
151
- return value.toString();
556
+ this.#showThumbInput(flag);
557
+ this.#thumbInputState.get(flag).focused = true;
558
+ }
559
+ #blurFloatingInput(flag) {
560
+ const entry = this.#thumbInputState.get(flag);
561
+ entry.focused = false;
562
+ this.#scheduleHideThumbInput(flag);
563
+ }
564
+ // Maps a 0–100 progress value to a CSS multiplier for --zui-slider-thumb-size so that
565
+ // `progress% + thumbSize * offset` lands on the thumb center within the inset wrapper.
566
+ // At 0%: offset=1.5 → thumbSize*1.5 (thumb center at min). At 100%: offset=-1.5 → -thumbSize*1.5 (thumb center at max).
567
+ static #thumbCenterOffset(progress) {
568
+ return 1.5 - (3 * progress) / 100;
569
+ }
570
+ static #thumbPositionCSS(progress) {
571
+ return `calc(${progress}% + var(--zui-slider-thumb-size) * ${_a.#thumbCenterOffset(progress)})`;
152
572
  }
153
573
  }
574
+ _a = ZuiSlider;
154
575
  __decorate([
155
576
  property()
156
577
  ], ZuiSlider.prototype, "value", null);
578
+ __decorate([
579
+ property({ type: Boolean })
580
+ ], ZuiSlider.prototype, "range", void 0);
581
+ __decorate([
582
+ property({ attribute: 'value-start' })
583
+ ], ZuiSlider.prototype, "valueStart", null);
584
+ __decorate([
585
+ property({ attribute: 'value-end' })
586
+ ], ZuiSlider.prototype, "valueEnd", null);
157
587
  __decorate([
158
588
  property({ type: Number })
159
589
  ], ZuiSlider.prototype, "min", void 0);
160
590
  __decorate([
161
- property({ type: Number, attribute: 'max' })
591
+ property({ type: Number })
162
592
  ], ZuiSlider.prototype, "max", void 0);
163
593
  __decorate([
164
594
  property({ type: Number })
165
595
  ], ZuiSlider.prototype, "step", void 0);
166
596
  __decorate([
167
- property({ type: Boolean, attribute: 'no-text' })
168
- ], ZuiSlider.prototype, "noText", void 0);
597
+ property({ type: Boolean, attribute: 'show-min-max' })
598
+ ], ZuiSlider.prototype, "showMinMax", void 0);
169
599
  window.customElements.define('zui-slider', ZuiSlider);
170
600
  //# sourceMappingURL=zui-slider.js.map