UNPKG

@scania/tegel

Version:
863 lines (862 loc) 35.2 kB
import { h, } from "@stencil/core"; import generateUniqueId from "../../utils/generateUniqueId"; export class TdsSlider { constructor() { this.wrapperElement = null; this.thumbElement = null; this.thumbInnerElement = null; this.trackElement = null; this.trackFillElement = null; this.thumbGrabbed = false; this.thumbLeft = 0; this.tickValues = []; this.useControls = false; this.useInput = false; this.useSmall = false; this.useSnapping = false; this.supposedValueSlot = -1; this.resizeObserverAdded = false; this.resetEventListenerAdded = false; this.ariaLiveElement = null; this.announcementDebounceTimeout = null; this.resetToInitialValue = () => { this.forceValueUpdate(this.initialValue); this.reset(); }; this.label = ''; this.value = '0'; this.min = '0'; this.max = '100'; this.ticks = '0'; this.showTickNumbers = false; this.tooltip = false; this.disabled = false; this.readOnly = false; this.controls = false; this.input = false; this.step = '1'; this.name = ''; this.thumbSize = 'lg'; this.snap = false; this.tdsAriaLabel = ''; this.sliderId = generateUniqueId(); this.tdsReadOnlyAriaLabel = ''; } roundToStep(val) { const stepNum = parseFloat(this.step); if (!stepNum) { return parseFloat(val.toFixed()); } const rounded = Math.round(val / stepNum) * stepNum; const precision = (stepNum.toString().split('.')[1] || '').length; return parseFloat(rounded.toFixed(precision)); } /** Public method to re-initialise the slider if some configuration props are changed */ async reset() { this.componentWillLoad(); this.componentDidLoad(); } handleKeydown(event) { switch (event.key) { case 'ArrowLeft': case 'ArrowDown': case '-': this.stepLeft(event); this.announceValueChange(); break; case 'ArrowRight': case 'ArrowUp': case '+': this.stepRight(event); this.announceValueChange(); break; case 'Home': this.setToMinValue(); this.announceValueChange(); break; case 'End': this.setToMaxValue(); this.announceValueChange(); break; default: break; } } handleRelease(event) { if (!this.thumbGrabbed) { const clickedOnTrack = event.target === this.trackElement || event.target === this.trackFillElement; if (clickedOnTrack) { this.thumbCore(event); this.trackElement.focus(); } return; } this.thumbGrabbed = false; this.thumbInnerElement.classList.remove('pressed'); if (this.thumbElement) { this.thumbElement.setAttribute('aria-grabbed', 'false'); } this.updateValue(event); this.trackElement.focus(); } handleMove(event) { if (!this.thumbGrabbed) { return; } this.thumbCore(event); } handleValueUpdate(newValue) { this.calculateThumbLeftFromValue(newValue); this.value = newValue; this.updateTrack(); } setToMinValue() { if (this.readOnly || this.disabled) { return; } this.forceValueUpdate(this.min); } setToMaxValue() { if (this.readOnly || this.disabled) { return; } this.forceValueUpdate(this.max); } updateSupposedValueSlot(localLeft) { const numTicks = parseInt(this.ticks); const trackWidth = this.getTrackWidth(); const distanceBetweenTicks = Math.round(trackWidth / (numTicks + 1)); const snappedLocalLeft = Math.round(localLeft / distanceBetweenTicks) * distanceBetweenTicks; let thumbPositionPX = 0; if (snappedLocalLeft >= 0 && snappedLocalLeft <= trackWidth) { thumbPositionPX = snappedLocalLeft; } else if (snappedLocalLeft > trackWidth) { thumbPositionPX = trackWidth; } else if (snappedLocalLeft < 0) { thumbPositionPX = 0; } this.supposedValueSlot = Math.round(thumbPositionPX / distanceBetweenTicks); return snappedLocalLeft; } thumbCore(event) { const numTicks = parseInt(this.ticks); const trackRect = this.trackElement.getBoundingClientRect(); let localLeft = 0; if (event.type === 'mousemove' || event.type === 'mouseup') { localLeft = event.clientX - trackRect.left; } else if (event.type === 'touchmove') { localLeft = event.touches[0].clientX - trackRect.left; } else console.warn('Slider component: Unsupported event!'); this.supposedValueSlot = -1; if (this.useSnapping && numTicks > 0) { localLeft = this.updateSupposedValueSlot(localLeft); } this.thumbLeft = this.constrainThumb(localLeft); this.thumbElement.style.left = `${this.thumbLeft}px`; this.updateValue(event); } updateTrack() { const trackWidth = this.getTrackWidth(); const percentageFilled = (this.thumbLeft / trackWidth) * 100; if (this.trackFillElement) { this.trackFillElement.style.width = `${percentageFilled}%`; } } announceValueChange() { if (!this.ariaLiveElement) return; // Debounce announcements to prevent too many rapid announcements clearTimeout(this.announcementDebounceTimeout); this.announcementDebounceTimeout = setTimeout(() => { this.ariaLiveElement.textContent = `${this.label ? this.label + ' ' : ''}${this.value} of ${this.max}`; }, 50); } updateValue(event) { const trackWidth = this.getTrackWidth(); const min = parseFloat(this.min); const max = parseFloat(this.max); // If snapping is enabled and a valid supposedValueSlot is available, // snap the value to the closest tick. Use the snapped value to update // the slider's thumb position and internal value. if (this.useSnapping && this.supposedValueSlot >= 0) { const snappedValue = this.tickValues[this.supposedValueSlot]; this.value = snappedValue.toString(); this.calculateThumbLeftFromValue(snappedValue); } else { const percentage = this.thumbLeft / trackWidth; const calculatedValue = min + percentage * (max - min); this.value = this.roundToStep(calculatedValue).toString(); } this.updateTrack(); // Update ARIA attributes if (this.thumbElement) { this.thumbElement.setAttribute('aria-valuenow', this.value); this.thumbElement.setAttribute('aria-valuetext', `${this.value} of ${this.max}`); } this.tdsInput.emit({ value: this.value }); /* Emit event after user has finished dragging the thumb */ if (event.type === 'touchend' || event.type === 'mouseup') { this.tdsChange.emit({ value: this.value }); this.announceValueChange(); } } forceValueUpdate(newValue) { this.calculateThumbLeftFromValue(newValue); this.value = newValue; // Update ARIA attributes if (this.thumbElement) { this.thumbElement.setAttribute('aria-valuenow', this.value); this.thumbElement.setAttribute('aria-valuetext', `${this.value} of ${this.max}`); } this.tdsChange.emit({ value: this.value }); this.updateTrack(); this.announceValueChange(); } constrainThumb(x) { const width = this.getTrackWidth(); if (x < 0) { return 0; } if (x > width) { return width; } return x; } getTrackWidth() { if (!this.trackElement) { return 0; } const trackRect = this.trackElement.getBoundingClientRect(); return trackRect.right - trackRect.left; } calculateThumbLeftFromValue(value) { const initValue = value; const trackWidth = this.getTrackWidth(); const normalizedValue = initValue - parseFloat(this.min); const normalizedMax = parseFloat(this.max) - parseFloat(this.min); const calculatedLeft = (normalizedValue / normalizedMax) * trackWidth; this.thumbLeft = calculatedLeft; this.updateSupposedValueSlot(this.thumbLeft); if (this.thumbElement) { this.thumbElement.style.left = `${this.thumbLeft}px`; this.thumbElement.setAttribute('aria-valuenow', this.value); } } /** Updates the slider value based on the current input value */ updateSliderValueOnInputChange(event) { const inputElement = event.target; let newValue = parseFloat(inputElement.value); // Check if the new value is different from the current value if (newValue === parseFloat(this.value)) { return; // Exit the function if the new value is the same as the current value } const minNum = parseFloat(this.min); const maxNum = parseFloat(this.max); if (newValue < minNum) { newValue = minNum; } else if (newValue > maxNum) { newValue = maxNum; } const rounded = this.roundToStep(newValue); this.forceValueUpdate(rounded.toString()); } /** Updates the slider value based on the current input value when enter is pressed */ handleInputFieldEnterPress(event) { event.stopPropagation(); if (event.key === 'Enter') { this.updateSliderValueOnInputChange(event); const inputElement = event.target; inputElement.blur(); } } grabThumb() { if (this.readOnly) { return; } this.thumbGrabbed = true; this.thumbInnerElement.classList.add('pressed'); if (this.thumbElement) { this.thumbElement.setAttribute('aria-grabbed', 'true'); } } calculateInputSizeFromMax() { return this.max.length; } controlsStep(delta, event) { if (this.readOnly || this.disabled) { return; } const numTicks = parseInt(this.ticks); /* if snapping is enabled, instead just increment or decrement the current "fixed" value from our ticknumber array */ if (this.useSnapping && numTicks > 0) { const stepDir = delta > 0 ? 1 : -1; this.supposedValueSlot += stepDir; if (this.supposedValueSlot < 0) { this.supposedValueSlot = 0; } else if (this.supposedValueSlot > numTicks + 1) { this.supposedValueSlot = numTicks + 1; } this.updateValue(event); } else { const trackWidth = this.getTrackWidth(); const percentage = this.thumbLeft / trackWidth; let currentValue = parseFloat(this.min) + percentage * (parseFloat(this.max) - parseFloat(this.min)); currentValue += delta; currentValue = this.roundToStep(currentValue); if (currentValue < parseFloat(this.min)) { currentValue = parseFloat(this.min); } else if (currentValue > parseFloat(this.max)) { currentValue = parseFloat(this.max); } this.value = `${currentValue}`; this.forceValueUpdate(this.value); } } stepLeft(event) { this.controlsStep(-parseFloat(this.step), event); } stepRight(event) { this.controlsStep(parseFloat(this.step), event); } componentWillLoad() { const numTicks = parseInt(this.ticks); if (numTicks > 0) { this.tickValues = [parseFloat(this.min)]; const interval = (parseFloat(this.max) - parseFloat(this.min)) / (numTicks + 1); const precision = (parseFloat(this.step).toString().split('.')[1] || '').length; for (let i = 1; i <= numTicks; i++) { const raw = parseFloat(this.min) + interval * i; this.tickValues.push(parseFloat(raw.toFixed(precision))); } this.tickValues.push(parseFloat(this.max)); } this.useInput = false; this.useControls = false; if (this.controls) { this.useControls = true; } else if (this.input) { this.useInput = true; } this.useSmall = this.thumbSize === 'sm'; this.useSnapping = this.snap; const min = parseFloat(this.min); const max = parseFloat(this.max); if (min > max) { console.warn('min-prop must have a higher value than max-prop for the component to work correctly.'); this.disabled = true; } } componentDidLoad() { if (!this.resizeObserverAdded) { this.resizeObserverAdded = true; const resizeObserver = new ResizeObserver(() => { this.calculateThumbLeftFromValue(this.value); this.updateTrack(); }); resizeObserver.observe(this.wrapperElement); } this.calculateThumbLeftFromValue(this.value); this.updateTrack(); // Only set the initial value once: if (!this.initialValue) { this.initialValue = this.value; } // Set initial aria attributes if (this.thumbElement) { this.thumbElement.setAttribute('aria-valuenow', this.value); this.thumbElement.setAttribute('aria-valuetext', `${this.value} of ${this.max}`); // Ensure the thumb can receive focus via keyboard this.thumbElement.tabIndex = this.disabled ? -1 : 0; } } componentDidRender() { // Only add the event listener once: if (!this.resetEventListenerAdded) { this.formElement = this.host.closest('form'); if (this.formElement) { this.formElement.addEventListener('reset', this.resetToInitialValue); this.resetEventListenerAdded = true; } } } connectedCallback() { if (this.readOnly && !this.tdsReadOnlyAriaLabel) { console.warn('tds-slider: tdsAriaLabel is reccomended when readonly is true'); } if (this.resetEventListenerAdded && this.formElement) { this.formElement.removeEventListener('reset', this.resetToInitialValue); this.resetEventListenerAdded = false; } } render() { const ariaLabel = this.readOnly ? this.tdsReadOnlyAriaLabel : this.label || this.tdsAriaLabel; return (h("div", { key: 'b4dd7706df0be052f0028a5f8ba6b6158f403b13', class: { 'tds-slider-wrapper': true, 'read-only': this.readOnly, } }, h("input", { key: '672c2aaa6dfee6489cd5e764ef151728c2af10fc', class: "tds-slider-native-element", type: "range", name: this.name, min: this.min, max: this.max, step: this.step, value: this.value, disabled: this.disabled }), h("div", { key: '8c7200436a9ef6524a3b2d554a2556bda8cc6afd', class: "sr-only", "aria-live": "assertive", ref: (el) => { this.ariaLiveElement = el; } }), h("div", { key: '1ce8e7ddccd19ca15733683768900c8fcde514d3', class: { 'tds-slider': true, 'disabled': this.disabled, 'tds-slider-small': this.useSmall, }, ref: (el) => { this.wrapperElement = el; }, "aria-disabled": this.disabled ? 'true' : 'false' }, h("label", { key: '54616f1cb5e4c13541f6f5fa700ddf4b1c4a9493', id: `${this.sliderId}-label`, class: this.showTickNumbers && 'offset' }, this.label), this.useInput && (h("div", { key: 'f6b8283409e9fb74ebf5fe89af5d88c29fde904d', class: "tds-slider__input-values" }, h("div", { key: '30532947c70dc8e0612fea3c01c25a5e86dacfca', class: "tds-slider__input-value min-value" }, this.min))), this.useControls && (h("div", { key: '6c001b8caba4a396bb9f4f4f3ba7b9ee6ca36f84', class: "tds-slider__controls" }, h("div", { key: '7a5804b789f84a4cd749c21d0ecc4b73bf7bae09', class: "tds-slider__control tds-slider__control-minus", onClick: (event) => this.stepLeft(event), role: "button", "aria-label": "Decrease value", tabindex: this.disabled || this.readOnly ? '-1' : '0' }, h("tds-icon", { key: '41db4dc1a8fda4972303fd48400078035648ca5c', name: "minus", size: "16px" })))), h("div", { key: 'f3cb2039ec4245b1d5c58bb819675381c4490197', class: "tds-slider-inner", tabIndex: -1 }, this.tickValues.length > 0 && (h("div", { key: '38bf8ca015894f3a42a0e9e33800cc7f0ff9c8d5', class: "tds-slider__value-dividers-wrapper" }, h("div", { key: '6a5aaa5d652d55f23db24b1b531664d125fc2a23', class: "tds-slider__value-dividers" }, this.tickValues.map((value) => (h("div", { class: "tds-slider__value-divider" }, this.showTickNumbers && h("span", null, value))))))), h("div", { key: '0c99475a8bdb1fdec04358421ec34630111a63b8', class: "tds-slider__track", ref: (el) => { this.trackElement = el; }, tabindex: this.disabled ? '-1' : '0', role: "presentation", onFocus: () => { if (this.thumbElement) { this.thumbElement.focus(); } } }, h("div", { key: 'd3ece50db5fbc17e4ebbf4272faa17fcf201dccf', class: "tds-slider__track-fill", ref: (el) => { this.trackFillElement = el; } }), h("div", { key: 'c304176e6a240820418d80aa8bfad35fbbdaf0f4', class: "tds-slider__thumb", ref: (el) => { this.thumbElement = el; }, onMouseDown: () => this.grabThumb(), onTouchStart: () => this.grabThumb(), role: "slider", "aria-valuemin": this.min, "aria-valuemax": this.max, "aria-valuenow": this.value, "aria-valuetext": `${this.value} of ${this.max}`, "aria-labelledby": `${this.sliderId}-label`, "aria-grabbed": this.thumbGrabbed ? 'true' : 'false', "aria-label": ariaLabel, tabindex: this.disabled ? '-1' : '0' }, this.tooltip && (h("div", { key: '7c46c4f8b3cc3bf3aeae7c841a2f38e7b3d1dfbb', class: "tds-slider__value" }, this.value, h("svg", { key: '871a05a8fc1560c8e94071172c57563d63e10f22', width: "18", height: "14", viewBox: "0 0 18 14", fill: "none", xmlns: "http://www.w3.org/2000/svg" }, h("path", { key: '0c99295a2049b3aac5d0472880e63acc4318fa07', d: "M8.15882 12.6915L0.990487 1.54076C0.562658 0.875246 1.0405 0 1.83167 0H16.1683C16.9595 0 17.4373 0.875246 17.0095 1.54076L9.84118 12.6915C9.44754 13.3038 8.55246 13.3038 8.15882 12.6915Z", fill: "currentColor" })))), h("div", { key: 'bf42f5b512d3041a88bd8cc420a5a4aaeb875e49', class: "tds-slider__thumb-inner", ref: (el) => { this.thumbInnerElement = el; } })))), this.useInput && (h("div", { key: '9a1ed941f13b84e738500921b12aeb21ace2464c', class: "tds-slider__input-values" }, h("div", { key: '2bdf5b0fb2687075b2fc12ce931859d2f26a0685', class: "tds-slider__input-value", onClick: (event) => this.stepLeft(event) }, this.max), h("div", { key: '480825ddccd550527ac18f046c6a8ce26baaba87', class: "tds-slider__input-field-wrapper" }, h("input", { key: '94a1446990ea0ae3955e8db9f0119e4a0ec71970', size: this.calculateInputSizeFromMax(), class: "tds-slider__input-field", value: this.value, "aria-label": this.readOnly ? this.tdsReadOnlyAriaLabel : undefined, onBlur: (event) => this.updateSliderValueOnInputChange(event), onKeyDown: (event) => this.handleInputFieldEnterPress(event), type: "number", min: this.min, max: this.max, step: this.step })))), this.useControls && (h("div", { key: 'b7415e621df023ffe0eca844d9970454d01c72ce', class: "tds-slider__controls" }, h("div", { key: '044a984a7212d56f8c1bcb5709608d90677d7647', class: "tds-slider__control tds-slider__control-plus", onClick: (event) => this.stepRight(event), role: "button", "aria-label": "Increase value", tabindex: this.disabled || this.readOnly ? '-1' : '0' }, h("tds-icon", { key: '0b6410f7fb4af32681c3f49f29c9696e871bfd5a', name: "plus", size: "16px" }))))))); } static get is() { return "tds-slider"; } static get originalStyleUrls() { return { "$": ["slider.scss"] }; } static get styleUrls() { return { "$": ["slider.css"] }; } static get properties() { return { "label": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Text for label" }, "attribute": "label", "reflect": false, "defaultValue": "''" }, "value": { "type": "string", "mutable": true, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Initial value" }, "attribute": "value", "reflect": false, "defaultValue": "'0'" }, "min": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Minimum value" }, "attribute": "min", "reflect": false, "defaultValue": "'0'" }, "max": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Maximum value" }, "attribute": "max", "reflect": false, "defaultValue": "'100'" }, "ticks": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Number of tick markers (tick for min- and max-value will be added automatically)" }, "attribute": "ticks", "reflect": false, "defaultValue": "'0'" }, "showTickNumbers": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Decide to show numbers above the tick markers or not" }, "attribute": "show-tick-numbers", "reflect": false, "defaultValue": "false" }, "tooltip": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Decide to show the tooltip or not" }, "attribute": "tooltip", "reflect": false, "defaultValue": "false" }, "disabled": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Sets the disabled state for the whole component" }, "attribute": "disabled", "reflect": false, "defaultValue": "false" }, "readOnly": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Sets the read only state for the whole component" }, "attribute": "read-only", "reflect": false, "defaultValue": "false" }, "controls": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Decide to show the controls or not" }, "attribute": "controls", "reflect": false, "defaultValue": "false" }, "input": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Decide to show the input field or not" }, "attribute": "input", "reflect": false, "defaultValue": "false" }, "step": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Defines how much to increment/decrement the value when using controls" }, "attribute": "step", "reflect": false, "defaultValue": "'1'" }, "name": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Name property (will be inherited by the native slider component)" }, "attribute": "name", "reflect": false, "defaultValue": "''" }, "thumbSize": { "type": "string", "mutable": false, "complexType": { "original": "'sm' | 'lg'", "resolved": "\"lg\" | \"sm\"", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Sets the size of the thumb" }, "attribute": "thumb-size", "reflect": false, "defaultValue": "'lg'" }, "snap": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Snap to the tick's grid" }, "attribute": "snap", "reflect": false, "defaultValue": "false" }, "tdsAriaLabel": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Sets the aria-label for the slider control." }, "attribute": "tds-aria-label", "reflect": false, "defaultValue": "''" }, "sliderId": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "ID for the Slider's input element, randomly generated if not specified." }, "attribute": "slider-id", "reflect": false, "defaultValue": "generateUniqueId()" }, "tdsReadOnlyAriaLabel": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Sets the read only aria label for the input field" }, "attribute": "tds-read-only-aria-label", "reflect": false, "defaultValue": "''" } }; } static get events() { return [{ "method": "tdsChange", "name": "tdsChange", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [], "text": "Sends the value of the slider when changed. Fires after mouse up and touch end events." }, "complexType": { "original": "{\n value: string;\n }", "resolved": "{ value: string; }", "references": {} } }, { "method": "tdsInput", "name": "tdsInput", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [], "text": "Sends the value of the slider while moving the thumb. Fires on mouse move and touch move events." }, "complexType": { "original": "{\n value: string;\n }", "resolved": "{ value: string; }", "references": {} } }]; } static get methods() { return { "reset": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global", "id": "global::Promise" } }, "return": "Promise<void>" }, "docs": { "text": "Public method to re-initialise the slider if some configuration props are changed", "tags": [] } } }; } static get elementRef() { return "host"; } static get watchers() { return [{ "propName": "value", "methodName": "handleValueUpdate" }]; } static get listeners() { return [{ "name": "keydown", "method": "handleKeydown", "target": undefined, "capture": false, "passive": false }, { "name": "mouseup", "method": "handleRelease", "target": "window", "capture": false, "passive": true }, { "name": "touchend", "method": "handleRelease", "target": "window", "capture": false, "passive": true }, { "name": "mousemove", "method": "handleMove", "target": "window", "capture": false, "passive": true }, { "name": "touchmove", "method": "handleMove", "target": "window", "capture": false, "passive": true }]; } }