UNPKG

44.8 kBJavaScriptView Raw
1/**
2 * @license
3 * Copyright 2020 Google Inc.
4 *
5 * Permission is hereby granted, free of charge, to any person obtaining a copy
6 * of this software and associated documentation files (the "Software"), to deal
7 * in the Software without restriction, including without limitation the rights
8 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 * copies of the Software, and to permit persons to whom the Software is
10 * furnished to do so, subject to the following conditions:
11 *
12 * The above copyright notice and this permission notice shall be included in
13 * all copies or substantial portions of the Software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 * THE SOFTWARE.
22 */
23import { __assign, __extends } from "tslib";
24import { AnimationFrame } from '@material/animation/animationframe';
25import { getCorrectPropertyName } from '@material/animation/util';
26import { MDCFoundation } from '@material/base/foundation';
27import { attributes, cssClasses, numbers } from './constants';
28import { Thumb, TickMark } from './types';
29var AnimationKeys;
30(function (AnimationKeys) {
31 AnimationKeys["SLIDER_UPDATE"] = "slider_update";
32})(AnimationKeys || (AnimationKeys = {}));
33// Accessing `window` without a `typeof` check will throw on Node environments.
34var HAS_WINDOW = typeof window !== 'undefined';
35/**
36 * Foundation class for slider. Responsibilities include:
37 * - Updating slider values (internal state and DOM updates) based on client
38 * 'x' position.
39 * - Updating DOM after slider property updates (e.g. min, max).
40 */
41var MDCSliderFoundation = /** @class */ (function (_super) {
42 __extends(MDCSliderFoundation, _super);
43 function MDCSliderFoundation(adapter) {
44 var _this = _super.call(this, __assign(__assign({}, MDCSliderFoundation.defaultAdapter), adapter)) || this;
45 // Whether the initial styles (to position the thumb, before component
46 // initialization) have been removed.
47 _this.initialStylesRemoved = false;
48 _this.isDisabled = false;
49 _this.isDiscrete = false;
50 _this.step = numbers.STEP_SIZE;
51 _this.hasTickMarks = false;
52 // The following properties are only set for range sliders.
53 _this.isRange = false;
54 // Tracks the thumb being moved across a slider pointer interaction (down,
55 // move event).
56 _this.thumb = null;
57 // `clientX` from the most recent down event. Used in subsequent move
58 // events to determine which thumb to move (in the case of
59 // overlapping thumbs).
60 _this.downEventClientX = null;
61 // Width of the start thumb knob.
62 _this.startThumbKnobWidth = 0;
63 // Width of the end thumb knob.
64 _this.endThumbKnobWidth = 0;
65 _this.animFrame = new AnimationFrame();
66 return _this;
67 }
68 Object.defineProperty(MDCSliderFoundation, "defaultAdapter", {
69 get: function () {
70 // tslint:disable:object-literal-sort-keys Methods should be in the same
71 // order as the adapter interface.
72 return {
73 hasClass: function () { return false; },
74 addClass: function () { return undefined; },
75 removeClass: function () { return undefined; },
76 addThumbClass: function () { return undefined; },
77 removeThumbClass: function () { return undefined; },
78 getAttribute: function () { return null; },
79 getInputValue: function () { return ''; },
80 setInputValue: function () { return undefined; },
81 getInputAttribute: function () { return null; },
82 setInputAttribute: function () { return null; },
83 removeInputAttribute: function () { return null; },
84 focusInput: function () { return undefined; },
85 isInputFocused: function () { return false; },
86 getThumbKnobWidth: function () { return 0; },
87 getThumbBoundingClientRect: function () {
88 return ({ top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0 });
89 },
90 getBoundingClientRect: function () {
91 return ({ top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0 });
92 },
93 isRTL: function () { return false; },
94 setThumbStyleProperty: function () { return undefined; },
95 removeThumbStyleProperty: function () { return undefined; },
96 setTrackActiveStyleProperty: function () { return undefined; },
97 removeTrackActiveStyleProperty: function () { return undefined; },
98 setValueIndicatorText: function () { return undefined; },
99 getValueToAriaValueTextFn: function () { return null; },
100 updateTickMarks: function () { return undefined; },
101 setPointerCapture: function () { return undefined; },
102 emitChangeEvent: function () { return undefined; },
103 emitInputEvent: function () { return undefined; },
104 emitDragStartEvent: function () { return undefined; },
105 emitDragEndEvent: function () { return undefined; },
106 registerEventHandler: function () { return undefined; },
107 deregisterEventHandler: function () { return undefined; },
108 registerThumbEventHandler: function () { return undefined; },
109 deregisterThumbEventHandler: function () { return undefined; },
110 registerInputEventHandler: function () { return undefined; },
111 deregisterInputEventHandler: function () { return undefined; },
112 registerBodyEventHandler: function () { return undefined; },
113 deregisterBodyEventHandler: function () { return undefined; },
114 registerWindowEventHandler: function () { return undefined; },
115 deregisterWindowEventHandler: function () { return undefined; },
116 };
117 // tslint:enable:object-literal-sort-keys
118 },
119 enumerable: false,
120 configurable: true
121 });
122 MDCSliderFoundation.prototype.init = function () {
123 var _this = this;
124 this.isDisabled = this.adapter.hasClass(cssClasses.DISABLED);
125 this.isDiscrete = this.adapter.hasClass(cssClasses.DISCRETE);
126 this.hasTickMarks = this.adapter.hasClass(cssClasses.TICK_MARKS);
127 this.isRange = this.adapter.hasClass(cssClasses.RANGE);
128 var min = this.convertAttributeValueToNumber(this.adapter.getInputAttribute(attributes.INPUT_MIN, this.isRange ? Thumb.START : Thumb.END), attributes.INPUT_MIN);
129 var max = this.convertAttributeValueToNumber(this.adapter.getInputAttribute(attributes.INPUT_MAX, Thumb.END), attributes.INPUT_MAX);
130 var value = this.convertAttributeValueToNumber(this.adapter.getInputAttribute(attributes.INPUT_VALUE, Thumb.END), attributes.INPUT_VALUE);
131 var valueStart = this.isRange ?
132 this.convertAttributeValueToNumber(this.adapter.getInputAttribute(attributes.INPUT_VALUE, Thumb.START), attributes.INPUT_VALUE) :
133 min;
134 var stepAttr = this.adapter.getInputAttribute(attributes.INPUT_STEP, Thumb.END);
135 var step = stepAttr ?
136 this.convertAttributeValueToNumber(stepAttr, attributes.INPUT_STEP) :
137 this.step;
138 this.validateProperties({ min: min, max: max, value: value, valueStart: valueStart, step: step });
139 this.min = min;
140 this.max = max;
141 this.value = value;
142 this.valueStart = valueStart;
143 this.step = step;
144 this.numDecimalPlaces = getNumDecimalPlaces(this.step);
145 this.valueBeforeDownEvent = value;
146 this.valueStartBeforeDownEvent = valueStart;
147 this.mousedownOrTouchstartListener =
148 this.handleMousedownOrTouchstart.bind(this);
149 this.moveListener = this.handleMove.bind(this);
150 this.pointerdownListener = this.handlePointerdown.bind(this);
151 this.pointerupListener = this.handlePointerup.bind(this);
152 this.thumbMouseenterListener = this.handleThumbMouseenter.bind(this);
153 this.thumbMouseleaveListener = this.handleThumbMouseleave.bind(this);
154 this.inputStartChangeListener = function () {
155 _this.handleInputChange(Thumb.START);
156 };
157 this.inputEndChangeListener = function () {
158 _this.handleInputChange(Thumb.END);
159 };
160 this.inputStartFocusListener = function () {
161 _this.handleInputFocus(Thumb.START);
162 };
163 this.inputEndFocusListener = function () {
164 _this.handleInputFocus(Thumb.END);
165 };
166 this.inputStartBlurListener = function () {
167 _this.handleInputBlur(Thumb.START);
168 };
169 this.inputEndBlurListener = function () {
170 _this.handleInputBlur(Thumb.END);
171 };
172 this.resizeListener = this.handleResize.bind(this);
173 this.registerEventHandlers();
174 };
175 MDCSliderFoundation.prototype.destroy = function () {
176 this.deregisterEventHandlers();
177 };
178 MDCSliderFoundation.prototype.setMin = function (value) {
179 this.min = value;
180 if (!this.isRange) {
181 this.valueStart = value;
182 }
183 this.updateUI();
184 };
185 MDCSliderFoundation.prototype.setMax = function (value) {
186 this.max = value;
187 this.updateUI();
188 };
189 MDCSliderFoundation.prototype.getMin = function () {
190 return this.min;
191 };
192 MDCSliderFoundation.prototype.getMax = function () {
193 return this.max;
194 };
195 /**
196 * - For single point sliders, returns the thumb value.
197 * - For range (two-thumb) sliders, returns the end thumb's value.
198 */
199 MDCSliderFoundation.prototype.getValue = function () {
200 return this.value;
201 };
202 /**
203 * - For single point sliders, sets the thumb value.
204 * - For range (two-thumb) sliders, sets the end thumb's value.
205 */
206 MDCSliderFoundation.prototype.setValue = function (value) {
207 if (this.isRange && value < this.valueStart) {
208 throw new Error("end thumb value (" + value + ") must be >= start thumb " +
209 ("value (" + this.valueStart + ")"));
210 }
211 this.updateValue(value, Thumb.END);
212 };
213 /**
214 * Only applicable for range sliders.
215 * @return The start thumb's value.
216 */
217 MDCSliderFoundation.prototype.getValueStart = function () {
218 if (!this.isRange) {
219 throw new Error('`valueStart` is only applicable for range sliders.');
220 }
221 return this.valueStart;
222 };
223 /**
224 * Only applicable for range sliders. Sets the start thumb's value.
225 */
226 MDCSliderFoundation.prototype.setValueStart = function (valueStart) {
227 if (!this.isRange) {
228 throw new Error('`valueStart` is only applicable for range sliders.');
229 }
230 if (this.isRange && valueStart > this.value) {
231 throw new Error("start thumb value (" + valueStart + ") must be <= end thumb " +
232 ("value (" + this.value + ")"));
233 }
234 this.updateValue(valueStart, Thumb.START);
235 };
236 MDCSliderFoundation.prototype.setStep = function (value) {
237 this.step = value;
238 this.numDecimalPlaces = getNumDecimalPlaces(value);
239 this.updateUI();
240 };
241 MDCSliderFoundation.prototype.setIsDiscrete = function (value) {
242 this.isDiscrete = value;
243 this.updateValueIndicatorUI();
244 this.updateTickMarksUI();
245 };
246 MDCSliderFoundation.prototype.getStep = function () {
247 return this.step;
248 };
249 MDCSliderFoundation.prototype.setHasTickMarks = function (value) {
250 this.hasTickMarks = value;
251 this.updateTickMarksUI();
252 };
253 MDCSliderFoundation.prototype.getDisabled = function () {
254 return this.isDisabled;
255 };
256 /**
257 * Sets disabled state, including updating styles and thumb tabindex.
258 */
259 MDCSliderFoundation.prototype.setDisabled = function (disabled) {
260 this.isDisabled = disabled;
261 if (disabled) {
262 this.adapter.addClass(cssClasses.DISABLED);
263 if (this.isRange) {
264 this.adapter.setInputAttribute(attributes.INPUT_DISABLED, '', Thumb.START);
265 }
266 this.adapter.setInputAttribute(attributes.INPUT_DISABLED, '', Thumb.END);
267 }
268 else {
269 this.adapter.removeClass(cssClasses.DISABLED);
270 if (this.isRange) {
271 this.adapter.removeInputAttribute(attributes.INPUT_DISABLED, Thumb.START);
272 }
273 this.adapter.removeInputAttribute(attributes.INPUT_DISABLED, Thumb.END);
274 }
275 };
276 /** @return Whether the slider is a range slider. */
277 MDCSliderFoundation.prototype.getIsRange = function () {
278 return this.isRange;
279 };
280 /**
281 * - Syncs slider boundingClientRect with the current DOM.
282 * - Updates UI based on internal state.
283 */
284 MDCSliderFoundation.prototype.layout = function (_a) {
285 var _b = _a === void 0 ? {} : _a, skipUpdateUI = _b.skipUpdateUI;
286 this.rect = this.adapter.getBoundingClientRect();
287 if (this.isRange) {
288 this.startThumbKnobWidth = this.adapter.getThumbKnobWidth(Thumb.START);
289 this.endThumbKnobWidth = this.adapter.getThumbKnobWidth(Thumb.END);
290 }
291 if (!skipUpdateUI) {
292 this.updateUI();
293 }
294 };
295 /** Handles resize events on the window. */
296 MDCSliderFoundation.prototype.handleResize = function () {
297 this.layout();
298 };
299 /**
300 * Handles pointer down events on the slider root element.
301 */
302 MDCSliderFoundation.prototype.handleDown = function (event) {
303 if (this.isDisabled)
304 return;
305 this.valueStartBeforeDownEvent = this.valueStart;
306 this.valueBeforeDownEvent = this.value;
307 var clientX = event.clientX != null ?
308 event.clientX :
309 event.targetTouches[0].clientX;
310 this.downEventClientX = clientX;
311 var value = this.mapClientXOnSliderScale(clientX);
312 this.thumb = this.getThumbFromDownEvent(clientX, value);
313 if (this.thumb === null)
314 return;
315 this.handleDragStart(event, value, this.thumb);
316 this.updateValue(value, this.thumb, { emitInputEvent: true });
317 };
318 /**
319 * Handles pointer move events on the slider root element.
320 */
321 MDCSliderFoundation.prototype.handleMove = function (event) {
322 if (this.isDisabled)
323 return;
324 // Prevent scrolling.
325 event.preventDefault();
326 var clientX = event.clientX != null ?
327 event.clientX :
328 event.targetTouches[0].clientX;
329 var dragAlreadyStarted = this.thumb != null;
330 this.thumb = this.getThumbFromMoveEvent(clientX);
331 if (this.thumb === null)
332 return;
333 var value = this.mapClientXOnSliderScale(clientX);
334 if (!dragAlreadyStarted) {
335 this.handleDragStart(event, value, this.thumb);
336 this.adapter.emitDragStartEvent(value, this.thumb);
337 }
338 this.updateValue(value, this.thumb, { emitInputEvent: true });
339 };
340 /**
341 * Handles pointer up events on the slider root element.
342 */
343 MDCSliderFoundation.prototype.handleUp = function () {
344 if (this.isDisabled || this.thumb === null)
345 return;
346 var oldValue = this.thumb === Thumb.START ?
347 this.valueStartBeforeDownEvent :
348 this.valueBeforeDownEvent;
349 var newValue = this.thumb === Thumb.START ? this.valueStart : this.value;
350 if (oldValue !== newValue) {
351 this.adapter.emitChangeEvent(newValue, this.thumb);
352 }
353 this.adapter.emitDragEndEvent(newValue, this.thumb);
354 this.thumb = null;
355 };
356 /**
357 * For range, discrete slider, shows the value indicator on both thumbs.
358 */
359 MDCSliderFoundation.prototype.handleThumbMouseenter = function () {
360 if (!this.isDiscrete || !this.isRange)
361 return;
362 this.adapter.addThumbClass(cssClasses.THUMB_WITH_INDICATOR, Thumb.START);
363 this.adapter.addThumbClass(cssClasses.THUMB_WITH_INDICATOR, Thumb.END);
364 };
365 /**
366 * For range, discrete slider, hides the value indicator on both thumbs.
367 */
368 MDCSliderFoundation.prototype.handleThumbMouseleave = function () {
369 if (!this.isDiscrete || !this.isRange)
370 return;
371 if (this.adapter.isInputFocused(Thumb.START) ||
372 this.adapter.isInputFocused(Thumb.END)) {
373 // Leave value indicator shown if either input is focused.
374 return;
375 }
376 this.adapter.removeThumbClass(cssClasses.THUMB_WITH_INDICATOR, Thumb.START);
377 this.adapter.removeThumbClass(cssClasses.THUMB_WITH_INDICATOR, Thumb.END);
378 };
379 MDCSliderFoundation.prototype.handleMousedownOrTouchstart = function (event) {
380 var _this = this;
381 var moveEventType = event.type === 'mousedown' ? 'mousemove' : 'touchmove';
382 // After a down event on the slider root, listen for move events on
383 // body (so the slider value is updated for events outside of the
384 // slider root).
385 this.adapter.registerBodyEventHandler(moveEventType, this.moveListener);
386 var upHandler = function () {
387 _this.handleUp();
388 // Once the drag is finished (up event on body), remove the move
389 // handler.
390 _this.adapter.deregisterBodyEventHandler(moveEventType, _this.moveListener);
391 // Also stop listening for subsequent up events.
392 _this.adapter.deregisterEventHandler('mouseup', upHandler);
393 _this.adapter.deregisterEventHandler('touchend', upHandler);
394 };
395 this.adapter.registerBodyEventHandler('mouseup', upHandler);
396 this.adapter.registerBodyEventHandler('touchend', upHandler);
397 this.handleDown(event);
398 };
399 MDCSliderFoundation.prototype.handlePointerdown = function (event) {
400 this.adapter.setPointerCapture(event.pointerId);
401 this.adapter.registerEventHandler('pointermove', this.moveListener);
402 this.handleDown(event);
403 };
404 /**
405 * Handles input `change` event by setting internal slider value to match
406 * input's new value.
407 */
408 MDCSliderFoundation.prototype.handleInputChange = function (thumb) {
409 var value = Number(this.adapter.getInputValue(thumb));
410 if (thumb === Thumb.START) {
411 this.setValueStart(value);
412 }
413 else {
414 this.setValue(value);
415 }
416 this.adapter.emitChangeEvent(thumb === Thumb.START ? this.valueStart : this.value, thumb);
417 this.adapter.emitInputEvent(thumb === Thumb.START ? this.valueStart : this.value, thumb);
418 };
419 /** Shows activated state and value indicator on thumb(s). */
420 MDCSliderFoundation.prototype.handleInputFocus = function (thumb) {
421 this.adapter.addThumbClass(cssClasses.THUMB_FOCUSED, thumb);
422 if (!this.isDiscrete)
423 return;
424 this.adapter.addThumbClass(cssClasses.THUMB_WITH_INDICATOR, thumb);
425 if (this.isRange) {
426 var otherThumb = thumb === Thumb.START ? Thumb.END : Thumb.START;
427 this.adapter.addThumbClass(cssClasses.THUMB_WITH_INDICATOR, otherThumb);
428 }
429 };
430 /** Removes activated state and value indicator from thumb(s). */
431 MDCSliderFoundation.prototype.handleInputBlur = function (thumb) {
432 this.adapter.removeThumbClass(cssClasses.THUMB_FOCUSED, thumb);
433 if (!this.isDiscrete)
434 return;
435 this.adapter.removeThumbClass(cssClasses.THUMB_WITH_INDICATOR, thumb);
436 if (this.isRange) {
437 var otherThumb = thumb === Thumb.START ? Thumb.END : Thumb.START;
438 this.adapter.removeThumbClass(cssClasses.THUMB_WITH_INDICATOR, otherThumb);
439 }
440 };
441 /**
442 * Emits custom dragStart event, along with focusing the underlying input.
443 */
444 MDCSliderFoundation.prototype.handleDragStart = function (event, value, thumb) {
445 this.adapter.emitDragStartEvent(value, thumb);
446 this.adapter.focusInput(thumb);
447 // Prevent the input (that we just focused) from losing focus.
448 event.preventDefault();
449 };
450 /**
451 * @return The thumb to be moved based on initial down event.
452 */
453 MDCSliderFoundation.prototype.getThumbFromDownEvent = function (clientX, value) {
454 // For single point slider, thumb to be moved is always the END (only)
455 // thumb.
456 if (!this.isRange)
457 return Thumb.END;
458 // Check if event press point is in the bounds of any thumb.
459 var thumbStartRect = this.adapter.getThumbBoundingClientRect(Thumb.START);
460 var thumbEndRect = this.adapter.getThumbBoundingClientRect(Thumb.END);
461 var inThumbStartBounds = clientX >= thumbStartRect.left && clientX <= thumbStartRect.right;
462 var inThumbEndBounds = clientX >= thumbEndRect.left && clientX <= thumbEndRect.right;
463 if (inThumbStartBounds && inThumbEndBounds) {
464 // Thumbs overlapping. Thumb to be moved cannot be determined yet.
465 return null;
466 }
467 // If press is in bounds for either thumb on down event, that's the thumb
468 // to be moved.
469 if (inThumbStartBounds) {
470 return Thumb.START;
471 }
472 if (inThumbEndBounds) {
473 return Thumb.END;
474 }
475 // For presses outside the range, return whichever thumb is closer.
476 if (value < this.valueStart) {
477 return Thumb.START;
478 }
479 if (value > this.value) {
480 return Thumb.END;
481 }
482 // For presses inside the range, return whichever thumb is closer.
483 return (value - this.valueStart <= this.value - value) ? Thumb.START :
484 Thumb.END;
485 };
486 /**
487 * @return The thumb to be moved based on move event (based on drag
488 * direction from original down event). Only applicable if thumbs
489 * were overlapping in the down event.
490 */
491 MDCSliderFoundation.prototype.getThumbFromMoveEvent = function (clientX) {
492 // Thumb has already been chosen.
493 if (this.thumb !== null)
494 return this.thumb;
495 if (this.downEventClientX === null) {
496 throw new Error('`downEventClientX` is null after move event.');
497 }
498 var moveDistanceUnderThreshold = Math.abs(this.downEventClientX - clientX) < numbers.THUMB_UPDATE_MIN_PX;
499 if (moveDistanceUnderThreshold)
500 return this.thumb;
501 var draggedThumbToLeft = clientX < this.downEventClientX;
502 if (draggedThumbToLeft) {
503 return this.adapter.isRTL() ? Thumb.END : Thumb.START;
504 }
505 else {
506 return this.adapter.isRTL() ? Thumb.START : Thumb.END;
507 }
508 };
509 /**
510 * Updates UI based on internal state.
511 * @param thumb Thumb whose value is being updated. If undefined, UI is
512 * updated for both thumbs based on current internal state.
513 */
514 MDCSliderFoundation.prototype.updateUI = function (thumb) {
515 this.updateThumbAndInputAttributes(thumb);
516 this.updateThumbAndTrackUI(thumb);
517 this.updateValueIndicatorUI(thumb);
518 this.updateTickMarksUI();
519 };
520 /**
521 * Updates thumb and input attributes based on current value.
522 * @param thumb Thumb whose aria attributes to update.
523 */
524 MDCSliderFoundation.prototype.updateThumbAndInputAttributes = function (thumb) {
525 if (!thumb)
526 return;
527 var value = this.isRange && thumb === Thumb.START ? this.valueStart : this.value;
528 var valueStr = String(value);
529 this.adapter.setInputAttribute(attributes.INPUT_VALUE, valueStr, thumb);
530 if (this.isRange && thumb === Thumb.START) {
531 this.adapter.setInputAttribute(attributes.INPUT_MIN, valueStr, Thumb.END);
532 }
533 else if (this.isRange && thumb === Thumb.END) {
534 this.adapter.setInputAttribute(attributes.INPUT_MAX, valueStr, Thumb.START);
535 }
536 // Sync attribute with property.
537 if (this.adapter.getInputValue(thumb) !== valueStr) {
538 this.adapter.setInputValue(valueStr, thumb);
539 }
540 var valueToAriaValueTextFn = this.adapter.getValueToAriaValueTextFn();
541 if (valueToAriaValueTextFn) {
542 this.adapter.setInputAttribute(attributes.ARIA_VALUETEXT, valueToAriaValueTextFn(value), thumb);
543 }
544 };
545 /**
546 * Updates value indicator UI based on current value.
547 * @param thumb Thumb whose value indicator to update. If undefined, all
548 * thumbs' value indicators are updated.
549 */
550 MDCSliderFoundation.prototype.updateValueIndicatorUI = function (thumb) {
551 if (!this.isDiscrete)
552 return;
553 var value = this.isRange && thumb === Thumb.START ? this.valueStart : this.value;
554 this.adapter.setValueIndicatorText(value, thumb === Thumb.START ? Thumb.START : Thumb.END);
555 if (!thumb && this.isRange) {
556 this.adapter.setValueIndicatorText(this.valueStart, Thumb.START);
557 }
558 };
559 /**
560 * Updates tick marks UI within slider, based on current min, max, and step.
561 */
562 MDCSliderFoundation.prototype.updateTickMarksUI = function () {
563 if (!this.isDiscrete || !this.hasTickMarks)
564 return;
565 var numTickMarksInactiveStart = (this.valueStart - this.min) / this.step;
566 var numTickMarksActive = (this.value - this.valueStart) / this.step + 1;
567 var numTickMarksInactiveEnd = (this.max - this.value) / this.step;
568 var tickMarksInactiveStart = Array.from({ length: numTickMarksInactiveStart })
569 .fill(TickMark.INACTIVE);
570 var tickMarksActive = Array.from({ length: numTickMarksActive })
571 .fill(TickMark.ACTIVE);
572 var tickMarksInactiveEnd = Array.from({ length: numTickMarksInactiveEnd })
573 .fill(TickMark.INACTIVE);
574 this.adapter.updateTickMarks(tickMarksInactiveStart.concat(tickMarksActive)
575 .concat(tickMarksInactiveEnd));
576 };
577 /** Maps clientX to a value on the slider scale. */
578 MDCSliderFoundation.prototype.mapClientXOnSliderScale = function (clientX) {
579 var xPos = clientX - this.rect.left;
580 var pctComplete = xPos / this.rect.width;
581 if (this.adapter.isRTL()) {
582 pctComplete = 1 - pctComplete;
583 }
584 // Fit the percentage complete between the range [min,max]
585 // by remapping from [0, 1] to [min, min+(max-min)].
586 var value = this.min + pctComplete * (this.max - this.min);
587 if (value === this.max || value === this.min) {
588 return value;
589 }
590 return Number(this.quantize(value).toFixed(this.numDecimalPlaces));
591 };
592 /** Calculates the quantized value based on step value. */
593 MDCSliderFoundation.prototype.quantize = function (value) {
594 var numSteps = Math.round((value - this.min) / this.step);
595 return this.min + numSteps * this.step;
596 };
597 /**
598 * Updates slider value (internal state and UI) based on the given value.
599 */
600 MDCSliderFoundation.prototype.updateValue = function (value, thumb, _a) {
601 var _b = _a === void 0 ? {} : _a, emitInputEvent = _b.emitInputEvent;
602 value = this.clampValue(value, thumb);
603 if (this.isRange && thumb === Thumb.START) {
604 // Exit early if current value is the same as the new value.
605 if (this.valueStart === value)
606 return;
607 this.valueStart = value;
608 }
609 else {
610 // Exit early if current value is the same as the new value.
611 if (this.value === value)
612 return;
613 this.value = value;
614 }
615 this.updateUI(thumb);
616 if (emitInputEvent) {
617 this.adapter.emitInputEvent(thumb === Thumb.START ? this.valueStart : this.value, thumb);
618 }
619 };
620 /**
621 * Clamps the given value for the given thumb based on slider properties:
622 * - Restricts value within [min, max].
623 * - If range slider, clamp start value <= end value, and
624 * end value >= start value.
625 */
626 MDCSliderFoundation.prototype.clampValue = function (value, thumb) {
627 // Clamp value to [min, max] range.
628 value = Math.min(Math.max(value, this.min), this.max);
629 var thumbStartMovedPastThumbEnd = this.isRange && thumb === Thumb.START && value > this.value;
630 if (thumbStartMovedPastThumbEnd) {
631 return this.value;
632 }
633 var thumbEndMovedPastThumbStart = this.isRange && thumb === Thumb.END && value < this.valueStart;
634 if (thumbEndMovedPastThumbStart) {
635 return this.valueStart;
636 }
637 return value;
638 };
639 /**
640 * Updates the active track and thumb style properties to reflect current
641 * value.
642 */
643 MDCSliderFoundation.prototype.updateThumbAndTrackUI = function (thumb) {
644 var _this = this;
645 var _a = this, max = _a.max, min = _a.min;
646 var pctComplete = (this.value - this.valueStart) / (max - min);
647 var rangePx = pctComplete * this.rect.width;
648 var isRtl = this.adapter.isRTL();
649 var transformProp = HAS_WINDOW ? getCorrectPropertyName(window, 'transform') : 'transform';
650 if (this.isRange) {
651 var thumbLeftPos_1 = this.adapter.isRTL() ?
652 (max - this.value) / (max - min) * this.rect.width :
653 (this.valueStart - min) / (max - min) * this.rect.width;
654 var thumbRightPos_1 = thumbLeftPos_1 + rangePx;
655 this.animFrame.request(AnimationKeys.SLIDER_UPDATE, function () {
656 // Set active track styles, accounting for animation direction by
657 // setting `transform-origin`.
658 var trackAnimatesFromRight = (!isRtl && thumb === Thumb.START) ||
659 (isRtl && thumb !== Thumb.START);
660 if (trackAnimatesFromRight) {
661 _this.adapter.setTrackActiveStyleProperty('transform-origin', 'right');
662 _this.adapter.setTrackActiveStyleProperty('left', 'unset');
663 _this.adapter.setTrackActiveStyleProperty('right', _this.rect.width - thumbRightPos_1 + "px");
664 }
665 else {
666 _this.adapter.setTrackActiveStyleProperty('transform-origin', 'left');
667 _this.adapter.setTrackActiveStyleProperty('right', 'unset');
668 _this.adapter.setTrackActiveStyleProperty('left', thumbLeftPos_1 + "px");
669 }
670 _this.adapter.setTrackActiveStyleProperty(transformProp, "scaleX(" + pctComplete + ")");
671 // Set thumb styles.
672 var thumbStartPos = isRtl ? thumbRightPos_1 : thumbLeftPos_1;
673 var thumbEndPos = _this.adapter.isRTL() ? thumbLeftPos_1 : thumbRightPos_1;
674 if (thumb === Thumb.START || !thumb || !_this.initialStylesRemoved) {
675 _this.adapter.setThumbStyleProperty(transformProp, "translateX(" + thumbStartPos + "px)", Thumb.START);
676 }
677 if (thumb === Thumb.END || !thumb || !_this.initialStylesRemoved) {
678 _this.adapter.setThumbStyleProperty(transformProp, "translateX(" + thumbEndPos + "px)", Thumb.END);
679 }
680 _this.removeInitialStyles(isRtl);
681 _this.updateOverlappingThumbsUI(thumbStartPos, thumbEndPos, thumb);
682 });
683 }
684 else {
685 this.animFrame.request(AnimationKeys.SLIDER_UPDATE, function () {
686 var thumbStartPos = isRtl ? _this.rect.width - rangePx : rangePx;
687 _this.adapter.setThumbStyleProperty(transformProp, "translateX(" + thumbStartPos + "px)", Thumb.END);
688 _this.adapter.setTrackActiveStyleProperty(transformProp, "scaleX(" + pctComplete + ")");
689 _this.removeInitialStyles(isRtl);
690 });
691 }
692 };
693 /**
694 * Removes initial inline styles if not already removed. `left:<...>%`
695 * inline styles can be added to position the thumb correctly before JS
696 * initialization. However, they need to be removed before the JS starts
697 * positioning the thumb. This is because the JS uses
698 * `transform:translateX(<...>)px` (for performance reasons) to position
699 * the thumb (which is not possible for initial styles since we need the
700 * bounding rect measurements).
701 */
702 MDCSliderFoundation.prototype.removeInitialStyles = function (isRtl) {
703 if (this.initialStylesRemoved)
704 return;
705 // Remove thumb position properties that were added for initial render.
706 var position = isRtl ? 'right' : 'left';
707 this.adapter.removeThumbStyleProperty(position, Thumb.END);
708 if (this.isRange) {
709 this.adapter.removeThumbStyleProperty(position, Thumb.START);
710 }
711 this.initialStylesRemoved = true;
712 this.resetTrackAndThumbAnimation();
713 };
714 /**
715 * Resets track/thumb animation to prevent animation when adding
716 * `transform` styles to thumb initially.
717 */
718 MDCSliderFoundation.prototype.resetTrackAndThumbAnimation = function () {
719 var _this = this;
720 if (!this.isDiscrete)
721 return;
722 // Set transition properties to default (no animation), so that the
723 // newly added `transform` styles do not animate thumb/track from
724 // their default positions.
725 var transitionProp = HAS_WINDOW ?
726 getCorrectPropertyName(window, 'transition') :
727 'transition';
728 var transitionDefault = 'all 0s ease 0s';
729 this.adapter.setThumbStyleProperty(transitionProp, transitionDefault, Thumb.END);
730 if (this.isRange) {
731 this.adapter.setThumbStyleProperty(transitionProp, transitionDefault, Thumb.START);
732 }
733 this.adapter.setTrackActiveStyleProperty(transitionProp, transitionDefault);
734 // In the next frame, remove the transition inline styles we just
735 // added, such that any animations added in the CSS can now take effect.
736 requestAnimationFrame(function () {
737 _this.adapter.removeThumbStyleProperty(transitionProp, Thumb.END);
738 _this.adapter.removeTrackActiveStyleProperty(transitionProp);
739 if (_this.isRange) {
740 _this.adapter.removeThumbStyleProperty(transitionProp, Thumb.START);
741 }
742 });
743 };
744 /**
745 * Adds THUMB_TOP class to active thumb if thumb knobs overlap; otherwise
746 * removes THUMB_TOP class from both thumbs.
747 * @param thumb Thumb that is active (being moved).
748 */
749 MDCSliderFoundation.prototype.updateOverlappingThumbsUI = function (thumbStartPos, thumbEndPos, thumb) {
750 var thumbsOverlap = false;
751 if (this.adapter.isRTL()) {
752 var startThumbLeftEdge = thumbStartPos - this.startThumbKnobWidth / 2;
753 var endThumbRightEdge = thumbEndPos + this.endThumbKnobWidth / 2;
754 thumbsOverlap = endThumbRightEdge >= startThumbLeftEdge;
755 }
756 else {
757 var startThumbRightEdge = thumbStartPos + this.startThumbKnobWidth / 2;
758 var endThumbLeftEdge = thumbEndPos - this.endThumbKnobWidth / 2;
759 thumbsOverlap = startThumbRightEdge >= endThumbLeftEdge;
760 }
761 if (thumbsOverlap) {
762 this.adapter.addThumbClass(cssClasses.THUMB_TOP,
763 // If no thumb was dragged (in the case of initial layout), end
764 // thumb is on top by default.
765 thumb || Thumb.END);
766 this.adapter.removeThumbClass(cssClasses.THUMB_TOP, thumb === Thumb.START ? Thumb.END : Thumb.START);
767 }
768 else {
769 this.adapter.removeThumbClass(cssClasses.THUMB_TOP, Thumb.START);
770 this.adapter.removeThumbClass(cssClasses.THUMB_TOP, Thumb.END);
771 }
772 };
773 /**
774 * Converts attribute value to a number, e.g. '100' => 100. Throws errors
775 * for invalid values.
776 * @param attributeValue Attribute value, e.g. 100.
777 * @param attributeName Attribute name, e.g. `aria-valuemax`.
778 */
779 MDCSliderFoundation.prototype.convertAttributeValueToNumber = function (attributeValue, attributeName) {
780 if (attributeValue === null) {
781 throw new Error("MDCSliderFoundation: `" + attributeName + "` must be non-null.");
782 }
783 var value = Number(attributeValue);
784 if (isNaN(value)) {
785 throw new Error("MDCSliderFoundation: `" + attributeName + "` value is " +
786 ("`" + attributeValue + "`, but must be a number."));
787 }
788 return value;
789 };
790 /** Checks that the given properties are valid slider values. */
791 MDCSliderFoundation.prototype.validateProperties = function (_a) {
792 var min = _a.min, max = _a.max, value = _a.value, valueStart = _a.valueStart, step = _a.step;
793 if (min >= max) {
794 throw new Error("MDCSliderFoundation: min must be strictly less than max. " +
795 ("Current: [min: " + min + ", max: " + max + "]"));
796 }
797 if (step <= 0) {
798 throw new Error("MDCSliderFoundation: step must be a positive number. " +
799 ("Current step: " + this.step));
800 }
801 if (this.isRange) {
802 if (value < min || value > max || valueStart < min || valueStart > max) {
803 throw new Error("MDCSliderFoundation: values must be in [min, max] range. " +
804 ("Current values: [start value: " + valueStart + ", end value: " + value + "]"));
805 }
806 if (valueStart > value) {
807 throw new Error("MDCSliderFoundation: start value must be <= end value. " +
808 ("Current values: [start value: " + valueStart + ", end value: " + value + "]"));
809 }
810 var numStepsValueStartFromMin = (valueStart - min) / step;
811 var numStepsValueFromMin = (value - min) / step;
812 if ((numStepsValueStartFromMin % 1) !== 0 ||
813 (numStepsValueFromMin % 1) !== 0) {
814 throw new Error("MDCSliderFoundation: Slider values must be valid based on the " +
815 ("step value. Current values: [start value: " + valueStart + ", ") +
816 ("end value: " + value + "]"));
817 }
818 }
819 else { // Single point slider.
820 if (value < min || value > max) {
821 throw new Error("MDCSliderFoundation: value must be in [min, max] range. " +
822 ("Current value: " + value));
823 }
824 var numStepsValueFromMin = (value - min) / step;
825 if ((numStepsValueFromMin % 1) !== 0) {
826 throw new Error("MDCSliderFoundation: Slider value must be valid based on the " +
827 ("step value. Current value: " + value));
828 }
829 }
830 };
831 MDCSliderFoundation.prototype.registerEventHandlers = function () {
832 this.adapter.registerWindowEventHandler('resize', this.resizeListener);
833 if (MDCSliderFoundation.SUPPORTS_POINTER_EVENTS) {
834 // If supported, use pointer events API with #setPointerCapture.
835 this.adapter.registerEventHandler('pointerdown', this.pointerdownListener);
836 this.adapter.registerEventHandler('pointerup', this.pointerupListener);
837 }
838 else {
839 // Otherwise, fall back to mousedown/touchstart events.
840 this.adapter.registerEventHandler('mousedown', this.mousedownOrTouchstartListener);
841 this.adapter.registerEventHandler('touchstart', this.mousedownOrTouchstartListener);
842 }
843 if (this.isRange) {
844 this.adapter.registerThumbEventHandler(Thumb.START, 'mouseenter', this.thumbMouseenterListener);
845 this.adapter.registerThumbEventHandler(Thumb.START, 'mouseleave', this.thumbMouseleaveListener);
846 this.adapter.registerInputEventHandler(Thumb.START, 'change', this.inputStartChangeListener);
847 this.adapter.registerInputEventHandler(Thumb.START, 'focus', this.inputStartFocusListener);
848 this.adapter.registerInputEventHandler(Thumb.START, 'blur', this.inputStartBlurListener);
849 }
850 this.adapter.registerThumbEventHandler(Thumb.END, 'mouseenter', this.thumbMouseenterListener);
851 this.adapter.registerThumbEventHandler(Thumb.END, 'mouseleave', this.thumbMouseleaveListener);
852 this.adapter.registerInputEventHandler(Thumb.END, 'change', this.inputEndChangeListener);
853 this.adapter.registerInputEventHandler(Thumb.END, 'focus', this.inputEndFocusListener);
854 this.adapter.registerInputEventHandler(Thumb.END, 'blur', this.inputEndBlurListener);
855 };
856 MDCSliderFoundation.prototype.deregisterEventHandlers = function () {
857 this.adapter.deregisterWindowEventHandler('resize', this.resizeListener);
858 if (MDCSliderFoundation.SUPPORTS_POINTER_EVENTS) {
859 this.adapter.deregisterEventHandler('pointerdown', this.pointerdownListener);
860 this.adapter.deregisterEventHandler('pointerup', this.pointerupListener);
861 }
862 else {
863 this.adapter.deregisterEventHandler('mousedown', this.mousedownOrTouchstartListener);
864 this.adapter.deregisterEventHandler('touchstart', this.mousedownOrTouchstartListener);
865 }
866 if (this.isRange) {
867 this.adapter.deregisterThumbEventHandler(Thumb.START, 'mouseenter', this.thumbMouseenterListener);
868 this.adapter.deregisterThumbEventHandler(Thumb.START, 'mouseleave', this.thumbMouseleaveListener);
869 this.adapter.deregisterInputEventHandler(Thumb.START, 'change', this.inputStartChangeListener);
870 this.adapter.deregisterInputEventHandler(Thumb.START, 'focus', this.inputStartFocusListener);
871 this.adapter.deregisterInputEventHandler(Thumb.START, 'blur', this.inputStartBlurListener);
872 }
873 this.adapter.deregisterThumbEventHandler(Thumb.END, 'mouseenter', this.thumbMouseenterListener);
874 this.adapter.deregisterThumbEventHandler(Thumb.END, 'mouseleave', this.thumbMouseleaveListener);
875 this.adapter.deregisterInputEventHandler(Thumb.END, 'change', this.inputEndChangeListener);
876 this.adapter.deregisterInputEventHandler(Thumb.END, 'focus', this.inputEndFocusListener);
877 this.adapter.deregisterInputEventHandler(Thumb.END, 'blur', this.inputEndBlurListener);
878 };
879 MDCSliderFoundation.prototype.handlePointerup = function () {
880 this.handleUp();
881 this.adapter.deregisterEventHandler('pointermove', this.moveListener);
882 };
883 MDCSliderFoundation.SUPPORTS_POINTER_EVENTS = HAS_WINDOW && Boolean(window.PointerEvent) &&
884 // #setPointerCapture is buggy on iOS, so we can't use pointer events
885 // until the following bug is fixed:
886 // https://bugs.webkit.org/show_bug.cgi?id=220196
887 !isIOS();
888 return MDCSliderFoundation;
889}(MDCFoundation));
890export { MDCSliderFoundation };
891function isIOS() {
892 // Source:
893 // https://stackoverflow.com/questions/9038625/detect-if-device-is-ios
894 return [
895 'iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone',
896 'iPod'
897 ].includes(navigator.platform)
898 // iPad on iOS 13 detection
899 || (navigator.userAgent.includes('Mac') && 'ontouchend' in document);
900}
901/**
902 * Given a number, returns the number of digits that appear after the
903 * decimal point.
904 * See
905 * https://stackoverflow.com/questions/9539513/is-there-a-reliable-way-in-javascript-to-obtain-the-number-of-decimal-places-of
906 */
907function getNumDecimalPlaces(n) {
908 // Pull out the fraction and the exponent.
909 var match = /(?:\.(\d+))?(?:[eE]([+\-]?\d+))?$/.exec(String(n));
910 // NaN or Infinity or integer.
911 // We arbitrarily decide that Infinity is integral.
912 if (!match)
913 return 0;
914 var fraction = match[1] || ''; // E.g. 1.234e-2 => 234
915 var exponent = match[2] || 0; // E.g. 1.234e-2 => -2
916 // Count the number of digits in the fraction and subtract the
917 // exponent to simulate moving the decimal point left by exponent places.
918 // 1.234e+2 has 1 fraction digit and '234'.length - 2 == 1
919 // 1.234e-2 has 5 fraction digit and '234'.length - -2 == 5
920 return Math.max(0, // lower limit
921 (fraction === '0' ? 0 : fraction.length) - Number(exponent));
922}
923//# sourceMappingURL=foundation.js.map
\No newline at end of file