UNPKG

6.78 kBJavaScriptView Raw
1/**
2 * Copyright IBM Corp. 2016, 2018
3 *
4 * This source code is licensed under the Apache-2.0 license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import settings from '../../globals/js/settings';
9import mixin from '../../globals/js/misc/mixin';
10import createComponent from '../../globals/js/mixins/create-component';
11import initComponentBySearch from '../../globals/js/mixins/init-component-by-search';
12import eventedState from '../../globals/js/mixins/evented-state';
13import handles from '../../globals/js/mixins/handles';
14import on from '../../globals/js/misc/on';
15
16class Slider extends mixin(createComponent, initComponentBySearch, eventedState, handles) {
17 /**
18 * Slider.
19 * @extends CreateComponent
20 * @extends InitComponentBySearch
21 * @extends Handles
22 * @param {HTMLElement} element The element working as an slider.
23 */
24 constructor(element, options) {
25 super(element, options);
26
27 this.sliderActive = false;
28 this.dragging = false;
29
30 this.track = this.element.querySelector(this.options.selectorTrack);
31 this.filledTrack = this.element.querySelector(this.options.selectorFilledTrack);
32 this.thumb = this.element.querySelector(this.options.selectorThumb);
33 this.input = this.element.querySelector(this.options.selectorInput);
34
35 if (this.element.dataset.sliderInputBox) {
36 this.boundInput = this.element.ownerDocument.querySelector(this.element.dataset.sliderInputBox);
37 this._updateInput();
38 this.manage(
39 on(this.boundInput, 'change', evt => {
40 this.setValue(evt.target.value);
41 })
42 );
43 this.manage(
44 on(this.boundInput, 'focus', evt => {
45 evt.target.select();
46 })
47 );
48 // workaround for safari
49 this.manage(
50 on(this.boundInput, 'mouseup', evt => {
51 evt.preventDefault();
52 })
53 );
54 }
55
56 this._updatePosition();
57
58 this.manage(
59 on(this.thumb, 'mousedown', () => {
60 this.sliderActive = true;
61 })
62 );
63 this.manage(
64 on(this.element.ownerDocument, 'mouseup', () => {
65 this.sliderActive = false;
66 })
67 );
68 this.manage(
69 on(this.element.ownerDocument, 'mousemove', evt => {
70 const disabled = this.element.classList.contains(this.options.classDisabled);
71 if (this.sliderActive === true && !disabled) {
72 this._updatePosition(evt);
73 }
74 })
75 );
76 this.manage(
77 on(this.thumb, 'keydown', evt => {
78 const disabled = this.element.classList.contains(this.options.classDisabled);
79 if (!disabled) {
80 this._updatePosition(evt);
81 }
82 })
83 );
84 this.manage(
85 on(this.track, 'click', evt => {
86 const disabled = this.element.classList.contains(this.options.classDisabled);
87 if (!disabled) {
88 this._updatePosition(evt);
89 }
90 })
91 );
92 }
93
94 _changeState = (state, detail, callback) => {
95 callback();
96 };
97
98 _updatePosition(evt) {
99 const { left, newValue } = this._calcValue(evt);
100
101 if (this.dragging) {
102 return;
103 }
104
105 this.dragging = true;
106
107 requestAnimationFrame(() => {
108 this.dragging = false;
109 this.thumb.style.left = `${left}%`;
110 this.filledTrack.style.transform = `translate(0%, -50%) scaleX(${left / 100})`;
111 this.input.value = newValue;
112 this._updateInput();
113 this.changeState('slider-value-change', { value: newValue });
114 });
115 }
116
117 _calcValue(evt) {
118 const { value, min, max, step } = this.getInputProps();
119
120 const range = max - min;
121 const valuePercentage = ((value - min) / range) * 100;
122
123 let left;
124 let newValue;
125 left = valuePercentage;
126 newValue = value;
127
128 if (evt) {
129 const { type } = evt;
130
131 if (type === 'keydown') {
132 const direction = {
133 40: -1, // decreasing
134 37: -1, // decreasing
135 38: 1, // increasing
136 39: 1, // increasing
137 }[evt.which];
138
139 if (direction !== undefined) {
140 const multiplier = evt.shiftKey === true ? range / step / this.options.stepMultiplier : 1;
141 const stepMultiplied = step * multiplier;
142 const stepSize = (stepMultiplied / range) * 100;
143 left = valuePercentage + stepSize * direction;
144 newValue = Number(value) + stepMultiplied * direction;
145 }
146 }
147 if (type === 'mousemove' || type === 'click') {
148 if (type === 'click') {
149 this.element.querySelector(this.options.selectorThumb).classList.add(this.options.classThumbClicked);
150 } else {
151 this.element.querySelector(this.options.selectorThumb).classList.remove(this.options.classThumbClicked);
152 }
153
154 const track = this.track.getBoundingClientRect();
155 const unrounded = (evt.clientX - track.left) / track.width;
156 const rounded = Math.round((range * unrounded) / step) * step;
157 left = (rounded / range) * 100;
158 newValue = rounded + min;
159 }
160 }
161
162 if (newValue <= Number(min)) {
163 left = 0;
164 newValue = min;
165 }
166 if (newValue >= Number(max)) {
167 left = 100;
168 newValue = max;
169 }
170
171 return { left, newValue };
172 }
173
174 _updateInput() {
175 if (this.boundInput) {
176 this.boundInput.value = this.input.value;
177 }
178 }
179
180 getInputProps() {
181 const values = {
182 value: Number(this.input.value),
183 min: Number(this.input.min),
184 max: Number(this.input.max),
185 step: this.input.step ? Number(this.input.step) : 1,
186 };
187 return values;
188 }
189
190 setValue(value) {
191 this.input.value = value;
192 this._updatePosition();
193 }
194
195 stepUp() {
196 this.input.stepUp();
197 this._updatePosition();
198 }
199
200 stepDown() {
201 this.input.stepDown();
202 this._updatePosition();
203 }
204
205 /**
206 * The map associating DOM element and Slider UI instance.
207 * @type {WeakMap}
208 */
209 static components /* #__PURE_CLASS_PROPERTY__ */ = new WeakMap();
210
211 /**
212 * The component options.
213 * If `options` is specified in the constructor,
214 * properties in this object are overriden for the instance being created.
215 * @property {string} selectorInit The CSS selector to find slider instances.
216 */
217 static get options() {
218 const { prefix } = settings;
219 return {
220 selectorInit: '[data-slider]',
221 selectorTrack: `.${prefix}--slider__track`,
222 selectorFilledTrack: `.${prefix}--slider__filled-track`,
223 selectorThumb: `.${prefix}--slider__thumb`,
224 selectorInput: `.${prefix}--slider__input`,
225 classDisabled: `${prefix}--slider--disabled`,
226 classThumbClicked: `${prefix}--slider__thumb--clicked`,
227 eventBeforeSliderValueChange: 'slider-before-value-change',
228 eventAfterSliderValueChange: 'slider-after-value-change',
229 stepMultiplier: 4,
230 };
231 }
232}
233
234export default Slider;