UNPKG

@smui/slider

Version:

Svelte Material UI - Slider

298 lines 14 kB
/** * @license * Copyright 2020 Google Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ import { MDCComponent } from '@smui/base/component'; import { applyPassive } from '@smui/dom/events'; import { matches } from '@smui/dom/ponyfill'; import { MDCRipple } from '@smui/ripple/component'; import { MDCRippleFoundation } from '@smui/ripple/foundation'; import { cssClasses, events } from './constants'; import { MDCSliderFoundation } from './foundation'; import { Thumb, TickMark } from './types'; /** Vanilla implementation of slider component. */ export class MDCSlider extends MDCComponent { constructor() { super(...arguments); this.skipInitialUIUpdate = false; // Function that maps a slider value to the value of the `aria-valuetext` // attribute on the thumb element. this.valueToAriaValueTextFn = null; } static attachTo(root, options = {}) { return new MDCSlider(root, undefined, options); } getDefaultFoundation() { // tslint:disable:object-literal-sort-keys Methods should be in the same // order as the adapter interface. const adapter = { hasClass: (className) => this.root.classList.contains(className), addClass: (className) => { this.root.classList.add(className); }, removeClass: (className) => { this.root.classList.remove(className); }, addThumbClass: (className, thumb) => { this.getThumbEl(thumb).classList.add(className); }, removeThumbClass: (className, thumb) => { this.getThumbEl(thumb).classList.remove(className); }, getAttribute: (attribute) => this.root.getAttribute(attribute), getInputValue: (thumb) => this.getInput(thumb).value, setInputValue: (value, thumb) => { this.getInput(thumb).value = value; }, getInputAttribute: (attribute, thumb) => this.getInput(thumb).getAttribute(attribute), setInputAttribute: (attribute, value, thumb) => { this.getInput(thumb).setAttribute(attribute, value); }, removeInputAttribute: (attribute, thumb) => { this.getInput(thumb).removeAttribute(attribute); }, focusInput: (thumb) => { this.getInput(thumb).focus(); }, isInputFocused: (thumb) => this.getInput(thumb) === document.activeElement, shouldHideFocusStylesForPointerEvents: () => false, getThumbKnobWidth: (thumb) => { return this.getThumbEl(thumb) .querySelector(`.${cssClasses.THUMB_KNOB}`) .getBoundingClientRect().width; }, getThumbBoundingClientRect: (thumb) => this.getThumbEl(thumb).getBoundingClientRect(), getBoundingClientRect: () => this.root.getBoundingClientRect(), getValueIndicatorContainerWidth: (thumb) => { return this.getThumbEl(thumb) .querySelector(`.${cssClasses.VALUE_INDICATOR_CONTAINER}`) .getBoundingClientRect().width; }, isRTL: () => getComputedStyle(this.root).direction === 'rtl', setThumbStyleProperty: (propertyName, value, thumb) => { this.getThumbEl(thumb).style.setProperty(propertyName, value); }, removeThumbStyleProperty: (propertyName, thumb) => { this.getThumbEl(thumb).style.removeProperty(propertyName); }, setTrackActiveStyleProperty: (propertyName, value) => { this.trackActive.style.setProperty(propertyName, value); }, removeTrackActiveStyleProperty: (propertyName) => { this.trackActive.style.removeProperty(propertyName); }, setValueIndicatorText: (value, thumb) => { const valueIndicatorEl = this.getThumbEl(thumb).querySelector(`.${cssClasses.VALUE_INDICATOR_TEXT}`); valueIndicatorEl.textContent = String(value); }, getValueToAriaValueTextFn: () => this.valueToAriaValueTextFn, updateTickMarks: (tickMarks) => { let tickMarksContainer = this.root.querySelector(`.${cssClasses.TICK_MARKS_CONTAINER}`); if (!tickMarksContainer) { tickMarksContainer = document.createElement('div'); tickMarksContainer.classList.add(cssClasses.TICK_MARKS_CONTAINER); const track = this.root.querySelector(`.${cssClasses.TRACK}`); track.appendChild(tickMarksContainer); } if (tickMarks.length !== tickMarksContainer.children.length) { while (tickMarksContainer.firstChild) { tickMarksContainer.removeChild(tickMarksContainer.firstChild); } this.addTickMarks(tickMarksContainer, tickMarks); } else { this.updateTickMarks(tickMarksContainer, tickMarks); } }, setPointerCapture: (pointerId) => { this.root.setPointerCapture(pointerId); }, emitChangeEvent: (value, thumb) => { this.emit(events.CHANGE, { value, thumb }); }, emitInputEvent: (value, thumb) => { this.emit(events.INPUT, { value, thumb }); }, // tslint:disable-next-line:enforce-name-casing emitDragStartEvent: (_, thumb) => { // Emitting event is not yet implemented. See issue: // https://github.com/material-components/material-components-web/issues/6448 this.getRipple(thumb).activate(); }, // tslint:disable-next-line:enforce-name-casing emitDragEndEvent: (_, thumb) => { // Emitting event is not yet implemented. See issue: // https://github.com/material-components/material-components-web/issues/6448 this.getRipple(thumb).deactivate(); }, registerEventHandler: (eventType, handler) => { this.listen(eventType, handler); }, deregisterEventHandler: (eventType, handler) => { this.unlisten(eventType, handler); }, registerThumbEventHandler: (thumb, eventType, handler) => { this.getThumbEl(thumb).addEventListener(eventType, handler); }, deregisterThumbEventHandler: (thumb, eventType, handler) => { this.getThumbEl(thumb).removeEventListener(eventType, handler); }, registerInputEventHandler: (thumb, eventType, handler) => { this.getInput(thumb).addEventListener(eventType, handler); }, deregisterInputEventHandler: (thumb, eventType, handler) => { this.getInput(thumb).removeEventListener(eventType, handler); }, registerBodyEventHandler: (eventType, handler) => { document.body.addEventListener(eventType, handler); }, deregisterBodyEventHandler: (eventType, handler) => { document.body.removeEventListener(eventType, handler); }, registerWindowEventHandler: (eventType, handler) => { window.addEventListener(eventType, handler); }, deregisterWindowEventHandler: (eventType, handler) => { window.removeEventListener(eventType, handler); }, // tslint:enable:object-literal-sort-keys }; return new MDCSliderFoundation(adapter); } /** * Initializes component, with the following options: * - `skipInitialUIUpdate`: Whether to skip updating the UI when initially * syncing with the DOM. This should be enabled when the slider position * is set before component initialization. */ initialize({ skipInitialUIUpdate, } = {}) { this.inputs = Array.from(this.root.querySelectorAll(`.${cssClasses.INPUT}`)); this.thumbs = Array.from(this.root.querySelectorAll(`.${cssClasses.THUMB}`)); this.trackActive = this.root.querySelector(`.${cssClasses.TRACK_ACTIVE}`); this.ripples = this.createRipples(); if (skipInitialUIUpdate) { this.skipInitialUIUpdate = true; } } initialSyncWithDOM() { this.foundation.layout({ skipUpdateUI: this.skipInitialUIUpdate }); } /** Redraws UI based on DOM (e.g. element dimensions, RTL). */ layout() { this.foundation.layout(); } getValueStart() { return this.foundation.getValueStart(); } setValueStart(valueStart) { this.foundation.setValueStart(valueStart); } getValue() { return this.foundation.getValue(); } setValue(value) { this.foundation.setValue(value); } /** @return Slider disabled state. */ getDisabled() { return this.foundation.getDisabled(); } /** Sets slider disabled state. */ setDisabled(disabled) { this.foundation.setDisabled(disabled); } /** * Sets a function that maps the slider value to the value of the * `aria-valuetext` attribute on the thumb element. */ setValueToAriaValueTextFn(mapFn) { this.valueToAriaValueTextFn = mapFn; } getThumbEl(thumb) { return thumb === Thumb.END ? this.thumbs[this.thumbs.length - 1] : this.thumbs[0]; } getInput(thumb) { return thumb === Thumb.END ? this.inputs[this.inputs.length - 1] : this.inputs[0]; } getRipple(thumb) { return thumb === Thumb.END ? this.ripples[this.ripples.length - 1] : this.ripples[0]; } /** Adds tick mark elements to the given container. */ addTickMarks(tickMarkContainer, tickMarks) { const fragment = document.createDocumentFragment(); for (let i = 0; i < tickMarks.length; i++) { const div = document.createElement('div'); const tickMarkClass = tickMarks[i] === TickMark.ACTIVE ? cssClasses.TICK_MARK_ACTIVE : cssClasses.TICK_MARK_INACTIVE; div.classList.add(tickMarkClass); fragment.appendChild(div); } tickMarkContainer.appendChild(fragment); } /** Updates tick mark elements' classes in the given container. */ updateTickMarks(tickMarkContainer, tickMarks) { const tickMarkEls = Array.from(tickMarkContainer.children); for (let i = 0; i < tickMarkEls.length; i++) { if (tickMarks[i] === TickMark.ACTIVE) { tickMarkEls[i].classList.add(cssClasses.TICK_MARK_ACTIVE); tickMarkEls[i].classList.remove(cssClasses.TICK_MARK_INACTIVE); } else { tickMarkEls[i].classList.add(cssClasses.TICK_MARK_INACTIVE); tickMarkEls[i].classList.remove(cssClasses.TICK_MARK_ACTIVE); } } } /** Initializes thumb ripples. */ createRipples() { const ripples = []; const rippleSurfaces = Array.from(this.root.querySelectorAll(`.${cssClasses.THUMB}`)); for (let i = 0; i < rippleSurfaces.length; i++) { const rippleSurface = rippleSurfaces[i]; // Use the corresponding input as the focus source for the ripple (i.e. // when the input is focused, the ripple is in the focused state). const input = this.inputs[i]; const adapter = Object.assign(Object.assign({}, MDCRipple.createAdapter(this)), { addClass: (className) => { rippleSurface.classList.add(className); }, computeBoundingRect: () => rippleSurface.getBoundingClientRect(), deregisterInteractionHandler: (eventType, handler) => { input.removeEventListener(eventType, handler); }, isSurfaceActive: () => matches(input, ':active'), isUnbounded: () => true, registerInteractionHandler: (eventType, handler) => { input.addEventListener(eventType, handler, applyPassive()); }, removeClass: (className) => { rippleSurface.classList.remove(className); }, updateCssVariable: (varName, value) => { rippleSurface.style.setProperty(varName, value); } }); const ripple = new MDCRipple(rippleSurface, new MDCRippleFoundation(adapter)); ripple.unbounded = true; ripples.push(ripple); } return ripples; } } //# sourceMappingURL=component.js.map