1 | import util from '../util.js';
|
2 | import platform from '../platform.js';
|
3 | import animit from '../animit.js';
|
4 | import GestureDetector from '../gesture-detector.js';
|
5 |
|
6 | const directionMap = {
|
7 | vertical: {
|
8 | axis: 'Y',
|
9 | size: 'Height',
|
10 | dir: ['up', 'down'],
|
11 | t3d: ['0px, ', 'px, 0px']
|
12 | },
|
13 | horizontal: {
|
14 | axis: 'X',
|
15 | size: 'Width',
|
16 | dir: ['left', 'right'],
|
17 | t3d: ['', 'px, 0px, 0px']
|
18 | }
|
19 | };
|
20 |
|
21 | export default class Swiper {
|
22 | constructor(params) {
|
23 |
|
24 | const FALSE = (() => false);
|
25 | `getInitialIndex getBubbleWidth isVertical isOverScrollable isCentered
|
26 | isAutoScrollable refreshHook preChangeHook postChangeHook overScrollHook`
|
27 | .split(/\s+/)
|
28 | .forEach(key => this[key] = params[key] || FALSE);
|
29 |
|
30 | this.getElement = params.getElement;
|
31 | this.scrollHook = params.scrollHook;
|
32 | this.itemSize = params.itemSize || '100%';
|
33 |
|
34 | this.getAutoScrollRatio = (...args) => {
|
35 | let ratio = params.getAutoScrollRatio && params.getAutoScrollRatio(...args);
|
36 | ratio = typeof ratio === 'number' && ratio === ratio ? ratio : .5;
|
37 | if (ratio < 0.0 || ratio > 1.0) {
|
38 | util.throw('Invalid auto-scroll-ratio ' + ratio + '. Must be between 0 and 1');
|
39 | }
|
40 | return ratio;
|
41 | };
|
42 |
|
43 |
|
44 | this.shouldBlock = util.globals.actualMobileOS === 'other';
|
45 |
|
46 |
|
47 | this.onDragStart = this.onDragStart.bind(this);
|
48 | this.onDrag = this.onDrag.bind(this);
|
49 | this.onDragEnd = this.onDragEnd.bind(this);
|
50 | this.onResize = this.onResize.bind(this);
|
51 |
|
52 | this._shouldFixScroll = util.globals.actualMobileOS === 'ios';
|
53 | }
|
54 |
|
55 | init({ swipeable, autoRefresh } = {}) {
|
56 | this.initialized = true;
|
57 | this.target = this.getElement().children[0];
|
58 | this.blocker = this.getElement().children[1];
|
59 | if (!this.target || !this.blocker) {
|
60 | util.throw('Expected "target" and "blocker" elements to exist before initializing Swiper');
|
61 | }
|
62 |
|
63 | if (!this.shouldBlock) {
|
64 | this.blocker.style.display = 'none';
|
65 | }
|
66 |
|
67 |
|
68 | this.getElement().classList.add('ons-swiper');
|
69 | this.target.classList.add('ons-swiper-target');
|
70 | this.blocker.classList.add('ons-swiper-blocker');
|
71 |
|
72 |
|
73 | this._gestureDetector = new GestureDetector(this.getElement(),
|
74 | { dragMinDistance: 1, dragLockToAxis: true, passive: !this._shouldFixScroll }
|
75 | );
|
76 | this._mutationObserver = new MutationObserver(() => this.refresh());
|
77 | this.updateSwipeable(swipeable);
|
78 | this.updateAutoRefresh(autoRefresh);
|
79 |
|
80 |
|
81 | this._scroll = this._offset = this._lastActiveIndex = 0;
|
82 | this._updateLayout();
|
83 | this._setupInitialIndex();
|
84 | setImmediate(() => this.initialized && this._setupInitialIndex());
|
85 |
|
86 |
|
87 |
|
88 | if (window !== window.parent || this.offsetHeight === 0) {
|
89 | window.requestAnimationFrame(() => this.initialized && this.onResize());
|
90 | }
|
91 | }
|
92 |
|
93 | dispose() {
|
94 | this.initialized = false;
|
95 | this.updateSwipeable(false);
|
96 | this.updateAutoRefresh(false);
|
97 |
|
98 | this._gestureDetector && this._gestureDetector.dispose();
|
99 | this.target = this.blocker = this._gestureDetector = this._mutationObserver = null;
|
100 |
|
101 | this.setupResize(false);
|
102 | }
|
103 |
|
104 | onResize() {
|
105 | const i = this._scroll / this.itemNumSize;
|
106 | this._reset();
|
107 | this.setActiveIndex(i);
|
108 | this.refresh();
|
109 | }
|
110 |
|
111 | get itemCount() {
|
112 | return this.target.children.length;
|
113 | }
|
114 |
|
115 | get itemNumSize() {
|
116 | if (typeof this._itemNumSize !== 'number' || this._itemNumSize !== this._itemNumSize) {
|
117 | this._itemNumSize = this._calculateItemSize();
|
118 | }
|
119 | return this._itemNumSize;
|
120 | }
|
121 |
|
122 | get maxScroll() {
|
123 | const max = this.itemCount * this.itemNumSize - this.targetSize;
|
124 | return Math.ceil(max < 0 ? 0 : max);
|
125 | }
|
126 |
|
127 | _calculateItemSize() {
|
128 | const matches = this.itemSize.match(/^(\d+)(px|%)/);
|
129 |
|
130 | if (!matches) {
|
131 | util.throw(`Invalid state: swiper's size unit must be '%' or 'px'`);
|
132 | }
|
133 |
|
134 | const value = parseInt(matches[1], 10);
|
135 | return matches[2] === '%' ? Math.round(value / 100 * this.targetSize) : value;
|
136 | }
|
137 |
|
138 | _setupInitialIndex() {
|
139 | this._reset();
|
140 | this._lastActiveIndex = Math.max(Math.min(Number(this.getInitialIndex()), this.itemCount), 0);
|
141 | this._scroll = this._offset + this.itemNumSize * this._lastActiveIndex;
|
142 | this._scrollTo(this._scroll);
|
143 | }
|
144 |
|
145 | _setSwiping(toggle) {
|
146 | this.target.classList.toggle('swiping', toggle);
|
147 | }
|
148 |
|
149 | setActiveIndex(index, options = {}) {
|
150 | this._setSwiping(true);
|
151 | index = Math.max(0, Math.min(index, this.itemCount - 1));
|
152 | const scroll = Math.max(0, Math.min(this.maxScroll, this._offset + this.itemNumSize * index));
|
153 |
|
154 | return this._changeTo(scroll, options);
|
155 | }
|
156 |
|
157 | getActiveIndex(scroll = this._scroll) {
|
158 | scroll -= this._offset;
|
159 | const count = this.itemCount,
|
160 | size = this.itemNumSize;
|
161 |
|
162 | if (this.itemNumSize === 0 || !util.isInteger(scroll)) {
|
163 | return this._lastActiveIndex;
|
164 | }
|
165 |
|
166 | if (scroll <= 0) {
|
167 | return 0;
|
168 | }
|
169 |
|
170 | for (let i = 0; i < count; i++) {
|
171 | if (size * i <= scroll && size * (i + 1) > scroll) {
|
172 | return i;
|
173 | }
|
174 | }
|
175 |
|
176 | return count - 1;
|
177 | }
|
178 |
|
179 | setupResize(add) {
|
180 | window[(add ? 'add' : 'remove') + 'EventListener']('resize', this.onResize, true);
|
181 | }
|
182 |
|
183 | show() {
|
184 | this.setupResize(true);
|
185 | this.onResize();
|
186 | setTimeout(() => this.target && this.target.classList.add('active'), 1000/60);
|
187 | }
|
188 |
|
189 | hide() {
|
190 | this.setupResize(false);
|
191 | this.target.classList.remove('active');
|
192 | }
|
193 |
|
194 | updateSwipeable(shouldUpdate) {
|
195 | if (this._gestureDetector) {
|
196 | const action = shouldUpdate ? 'on' : 'off';
|
197 | this._gestureDetector[action]('drag', this.onDrag);
|
198 | this._gestureDetector[action]('dragstart', this.onDragStart);
|
199 | this._gestureDetector[action]('dragend', this.onDragEnd);
|
200 | }
|
201 | }
|
202 |
|
203 | updateAutoRefresh(shouldWatch) {
|
204 | if (this._mutationObserver) {
|
205 | shouldWatch
|
206 | ? this._mutationObserver.observe(this.target, { childList: true })
|
207 | : this._mutationObserver.disconnect();
|
208 | }
|
209 | }
|
210 |
|
211 | updateItemSize(newSize) {
|
212 | this.itemSize = newSize || '100%';
|
213 | this.refresh();
|
214 | }
|
215 |
|
216 | toggleBlocker(block) {
|
217 | this.blocker.style.pointerEvents = block ? 'auto' : 'none';
|
218 | }
|
219 |
|
220 | _canConsumeGesture(gesture) {
|
221 | const d = gesture.direction;
|
222 | const isFirst = this._scroll === 0 && !this.isOverScrollable();
|
223 | const isLast = this._scroll === this.maxScroll && !this.isOverScrollable();
|
224 |
|
225 | return this.isVertical()
|
226 | ? ((d === 'down' && !isFirst) || (d === 'up' && !isLast))
|
227 | : ((d === 'right' && !isFirst) || (d === 'left' && !isLast));
|
228 | }
|
229 |
|
230 | onDragStart(event) {
|
231 | this._ignoreDrag = event.consumed || !util.isValidGesture(event);
|
232 |
|
233 | if (!this._ignoreDrag) {
|
234 | const consume = event.consume;
|
235 | event.consume = () => { consume && consume(); this._ignoreDrag = true; };
|
236 |
|
237 | if (this._canConsumeGesture(event.gesture)) {
|
238 | const startX = event.gesture.center && event.gesture.center.clientX || 0,
|
239 | distFromEdge = this.getBubbleWidth() || 0,
|
240 | start = () => {
|
241 | consume && consume();
|
242 | event.consumed = true;
|
243 | this._started = true;
|
244 | this.shouldBlock && this.toggleBlocker(true);
|
245 | this._setSwiping(true);
|
246 | util.iosPreventScroll(this._gestureDetector);
|
247 | };
|
248 |
|
249 |
|
250 | startX < distFromEdge || startX > (this.targetSize - distFromEdge)
|
251 | ? setImmediate(() => !this._ignoreDrag && start())
|
252 | : start();
|
253 | }
|
254 | }
|
255 | }
|
256 |
|
257 | onDrag(event) {
|
258 | if (!event.gesture || this._ignoreDrag || !this._started) {
|
259 | return;
|
260 | }
|
261 |
|
262 | this._continued = true;
|
263 | event.stopPropagation();
|
264 |
|
265 | this._scrollTo(this._scroll - this._getDelta(event), { throttle: true });
|
266 | }
|
267 |
|
268 | onDragEnd(event) {
|
269 | this._started = false;
|
270 | if (!event.gesture || this._ignoreDrag || !this._continued) {
|
271 | this._ignoreDrag = true;
|
272 | return;
|
273 | }
|
274 |
|
275 | this._continued = false;
|
276 | event.stopPropagation();
|
277 |
|
278 | const scroll = this._scroll - this._getDelta(event);
|
279 | const normalizedScroll = this._normalizeScroll(scroll);
|
280 | scroll === normalizedScroll ? this._startMomentumScroll(scroll, event) : this._killOverScroll(normalizedScroll);
|
281 | this.shouldBlock && this.toggleBlocker(false);
|
282 | }
|
283 |
|
284 | _startMomentumScroll(scroll, event) {
|
285 | const velocity = this._getVelocity(event),
|
286 | matchesDirection = event.gesture.interimDirection === this.dM.dir[this._getDelta(event) < 0 ? 0 : 1];
|
287 |
|
288 | const nextScroll = this._getAutoScroll(scroll, velocity, matchesDirection);
|
289 | let duration = Math.abs(nextScroll - scroll) / (velocity + 0.01) / 1000;
|
290 | duration = Math.min(.25, Math.max(.1, duration));
|
291 |
|
292 | this._changeTo(nextScroll, { swipe: true, animationOptions: { duration, timing: 'cubic-bezier(.4, .7, .5, 1)' } });
|
293 | }
|
294 |
|
295 | _killOverScroll(scroll) {
|
296 | this._scroll = scroll;
|
297 | const direction = this.dM.dir[Number(scroll > 0)];
|
298 | const killOverScroll = () => this._changeTo(scroll, { animationOptions: { duration: .4, timing: 'cubic-bezier(.1, .4, .1, 1)' } });
|
299 | this.overScrollHook({ direction, killOverScroll }) || killOverScroll();
|
300 | }
|
301 |
|
302 | _changeTo(scroll, options = {}) {
|
303 | const e = { activeIndex: this.getActiveIndex(scroll), lastActiveIndex: this._lastActiveIndex, swipe: options.swipe || false };
|
304 | const change = e.activeIndex !== e.lastActiveIndex;
|
305 | const canceled = change ? this.preChangeHook(e) : false;
|
306 |
|
307 | this._scroll = canceled ? this._offset + e.lastActiveIndex * this.itemNumSize : scroll;
|
308 | this._lastActiveIndex = canceled ? e.lastActiveIndex : e.activeIndex;
|
309 |
|
310 | return this._scrollTo(this._scroll, options).then(() => {
|
311 | if (scroll === this._scroll && !canceled) {
|
312 | this._setSwiping(false);
|
313 | change && this.postChangeHook(e);
|
314 | } else if (options.reject) {
|
315 | this._setSwiping(false);
|
316 | return Promise.reject('Canceled');
|
317 | }
|
318 | });
|
319 | }
|
320 |
|
321 | _scrollTo(scroll, options = {}) {
|
322 | if (options.throttle) {
|
323 | const ratio = 0.35;
|
324 | if (scroll < 0) {
|
325 | scroll = this.isOverScrollable() ? Math.round(scroll * ratio) : 0;
|
326 | } else {
|
327 | const maxScroll = this.maxScroll;
|
328 | if (maxScroll < scroll) {
|
329 | scroll = this.isOverScrollable() ? maxScroll + Math.round((scroll - maxScroll) * ratio) : maxScroll;
|
330 | }
|
331 | }
|
332 | }
|
333 |
|
334 | const opt = options.animation === 'none' ? {} : options.animationOptions;
|
335 | this.scrollHook && this.itemNumSize > 0 && this.scrollHook((scroll / this.itemNumSize).toFixed(2), options.animationOptions || {});
|
336 |
|
337 | return new Promise(resolve =>
|
338 | animit(this.target)
|
339 | .queue({ transform: this._getTransform(scroll) }, opt)
|
340 | .play(resolve)
|
341 | );
|
342 | }
|
343 |
|
344 | _getAutoScroll(scroll, velocity, matchesDirection) {
|
345 | const max = this.maxScroll,
|
346 | offset = this._offset,
|
347 | size = this.itemNumSize;
|
348 |
|
349 | if (!this.isAutoScrollable()) {
|
350 | return Math.max(0, Math.min(max, scroll));
|
351 | }
|
352 |
|
353 | let arr = [];
|
354 | for (let s = offset; s < max; s += size) {
|
355 | arr.push(s);
|
356 | }
|
357 | arr.push(max);
|
358 |
|
359 | arr = arr
|
360 | .sort((left, right) => Math.abs(left - scroll) - Math.abs(right - scroll))
|
361 | .filter((item, pos) => !pos || item !== arr[pos - 1]);
|
362 |
|
363 | let result = arr[0];
|
364 | const lastScroll = this._lastActiveIndex * size + offset;
|
365 | const scrollRatio = Math.abs(scroll - lastScroll) / size;
|
366 |
|
367 | if (scrollRatio <= this.getAutoScrollRatio(matchesDirection, velocity, size)) {
|
368 | result = lastScroll;
|
369 | } else {
|
370 | if (scrollRatio < 1.0 && arr[0] === lastScroll && arr.length > 1) {
|
371 | result = arr[1];
|
372 | }
|
373 | }
|
374 | return Math.max(0, Math.min(max, result));
|
375 | }
|
376 |
|
377 | _reset() {
|
378 | this._targetSize = this._itemNumSize = undefined;
|
379 | }
|
380 |
|
381 | _normalizeScroll(scroll) {
|
382 | return Math.max( Math.min(scroll, this.maxScroll), 0);
|
383 | }
|
384 |
|
385 | refresh() {
|
386 | this._reset();
|
387 | this._updateLayout();
|
388 |
|
389 | if (util.isInteger(this._scroll)) {
|
390 | const scroll = this._normalizeScroll(this._scroll);
|
391 | scroll !== this._scroll ? this._killOverScroll(scroll) : this._changeTo(scroll);
|
392 | } else {
|
393 | this._setupInitialIndex();
|
394 | }
|
395 |
|
396 | this.refreshHook();
|
397 | }
|
398 |
|
399 | get targetSize() {
|
400 | if (!this._targetSize) {
|
401 | this._targetSize = this.target[`offset${this.dM.size}`];
|
402 | }
|
403 | return this._targetSize;
|
404 | }
|
405 |
|
406 | _getDelta(event) {
|
407 | return event.gesture[`delta${this.dM.axis}`];
|
408 | }
|
409 |
|
410 | _getVelocity(event) {
|
411 | return event.gesture[`velocity${this.dM.axis}`];
|
412 | }
|
413 |
|
414 | _getTransform(scroll) {
|
415 | return `translate3d(${this.dM.t3d[0]}${-scroll}${this.dM.t3d[1]})`;
|
416 | }
|
417 |
|
418 | _updateLayout() {
|
419 | this.dM = directionMap[this.isVertical() ? 'vertical' : 'horizontal'];
|
420 | this.target.classList.toggle('ons-swiper-target--vertical', this.isVertical());
|
421 |
|
422 | for (let c = this.target.children[0]; c; c = c.nextElementSibling) {
|
423 | c.style[this.dM.size.toLowerCase()] = this.itemSize;
|
424 | }
|
425 |
|
426 | if (this.isCentered()) {
|
427 | this._offset = (this.targetSize - this.itemNumSize) / -2 || 0;
|
428 | }
|
429 | }
|
430 | }
|
431 |
|