blob: 51b9451a418f06dd370c83c3c782683f4a207cee [file] [log] [blame]
Copybara botbe50d492023-11-30 00:16:42 +01001'use strict';
2
3var _isInteger = require('babel-runtime/core-js/number/is-integer');
4
5var _isInteger2 = _interopRequireDefault(_isInteger);
6
7var _toConsumableArray2 = require('babel-runtime/helpers/toConsumableArray');
8
9var _toConsumableArray3 = _interopRequireDefault(_toConsumableArray2);
10
11var _assign = require('babel-runtime/core-js/object/assign');
12
13var _assign2 = _interopRequireDefault(_assign);
14
15var _intervalFunction = require('../utils/interval-function');
16
17var _intervalFunction2 = _interopRequireDefault(_intervalFunction);
18
19var _easing = require('../utils/easing');
20
21var _jsonUtils = require('../utils/json-utils');
22
23var _constants = require('../utils/constants');
24
25function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
26
27/**
28 * @license
29 * Copyright 2016 Leif Olsen. All Rights Reserved.
30 *
31 * Licensed under the Apache License, Version 2.0 (the "License");
32 * you may not use this file except in compliance with the License.
33 * You may obtain a copy of the License at
34 *
35 * http://www.apache.org/licenses/LICENSE-2.0
36 *
37 * Unless required by applicable law or agreed to in writing, software
38 * distributed under the License is distributed on an "AS IS" BASIS,
39 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
40 * See the License for the specific language governing permissions and
41 * limitations under the License.
42 *
43 * This code is built with Google Material Design Lite,
44 * which is Licensed under the Apache License, Version 2.0
45 */
46
47/**
48 * Image carousel
49 */
50
51var MDL_RIPPLE_CONTAINER = 'mdlext-carousel__slide__ripple-container';
52
53(function () {
54 'use strict';
55
56 //const CAROUSEL = 'mdlext-carousel';
57
58 var SLIDE = 'mdlext-carousel__slide';
59 var ROLE = 'list';
60 var SLIDE_ROLE = 'listitem';
61
62 /**
63 * @constructor
64 * @param {Element} element The element that will be upgraded.
65 */
66 var MaterialExtCarousel = function MaterialExtCarousel(element) {
67 // Stores the element.
68 this.element_ = element;
69
70 // Default config
71 this.config_ = {
72 interactive: true,
73 autostart: false,
74 type: 'slide',
75 interval: 1000,
76 animationLoop: (0, _intervalFunction2.default)(1000)
77 };
78
79 this.scrollAnimation_ = (0, _intervalFunction2.default)(33);
80
81 // Initialize instance.
82 this.init();
83 };
84
85 window['MaterialExtCarousel'] = MaterialExtCarousel;
86
87 /**
88 * Start slideshow animation
89 * @private
90 */
91 MaterialExtCarousel.prototype.startSlideShow_ = function () {
92 var _this = this;
93
94 var nextSlide = function nextSlide() {
95 var slide = _this.element_.querySelector('.' + SLIDE + '[aria-selected]');
96 if (slide) {
97 slide.removeAttribute('aria-selected');
98 slide = slide.nextElementSibling;
99 }
100 if (!slide) {
101 slide = _this.element_.querySelector('.' + SLIDE + ':first-child');
102 _this.animateScroll_(0);
103 }
104 if (slide) {
105 _this.moveSlideIntoViewport_(slide);
106 slide.setAttribute('aria-selected', '');
107 _this.emitSelectEvent_('next', null, slide);
108 return true;
109 }
110 return false;
111 };
112
113 var nextScroll = function nextScroll(direction) {
114 var nextDirection = direction;
115
116 if ('next' === direction && _this.element_.scrollLeft === _this.element_.scrollWidth - _this.element_.clientWidth) {
117 nextDirection = 'prev';
118 } else if (_this.element_.scrollLeft === 0) {
119 nextDirection = 'next';
120 }
121 var x = 'next' === nextDirection ? Math.min(_this.element_.scrollLeft + _this.element_.clientWidth, _this.element_.scrollWidth - _this.element_.clientWidth) : Math.max(_this.element_.scrollLeft - _this.element_.clientWidth, 0);
122
123 _this.animateScroll_(x, 1000);
124 return nextDirection;
125 };
126
127 if (!this.config_.animationLoop.started) {
128 (function () {
129 _this.config_.animationLoop.interval = _this.config_.interval;
130 var direction = 'next';
131
132 if ('scroll' === _this.config_.type) {
133 _this.config_.animationLoop.start(function () {
134 direction = nextScroll(direction);
135 return true; // It runs until cancelSlideShow_ is triggered
136 });
137 } else {
138 nextSlide();
139 _this.config_.animationLoop.start(function () {
140 return nextSlide(); // It runs until cancelSlideShow_ is triggered
141 });
142 }
143 })();
144 }
145
146 // TODO: Pause animation when carousel is not in browser viewport or user changes tab
147 };
148
149 /**
150 * Cancel slideshow if running. Emmits a 'pause' event
151 * @private
152 */
153 MaterialExtCarousel.prototype.cancelSlideShow_ = function () {
154 if (this.config_.animationLoop.started) {
155 this.config_.animationLoop.stop();
156 this.emitSelectEvent_('pause', _constants.VK_ESC, this.element_.querySelector('.' + SLIDE + '[aria-selected]'));
157 }
158 };
159
160 /**
161 * Animate scroll
162 * @param newPosition
163 * @param newDuration
164 * @param completedCallback
165 * @private
166 */
167 MaterialExtCarousel.prototype.animateScroll_ = function (newPosition, newDuration, completedCallback) {
168 var _this2 = this;
169
170 var start = this.element_.scrollLeft;
171 var distance = newPosition - start;
172
173 if (distance !== 0) {
174 (function () {
175 var duration = Math.max(Math.min(Math.abs(distance), newDuration || 400), 100); // duration is between 100 and newDuration||400ms||distance
176 var t = 0;
177 _this2.scrollAnimation_.stop();
178 _this2.scrollAnimation_.start(function (timeElapsed) {
179 t += timeElapsed;
180 if (t < duration) {
181 _this2.element_.scrollLeft = (0, _easing.inOutQuintic)(t, start, distance, duration);
182 return true;
183 } else {
184 _this2.element_.scrollLeft = newPosition;
185 if (completedCallback) {
186 completedCallback();
187 }
188 return false;
189 }
190 });
191 })();
192 } else {
193 if (completedCallback) {
194 completedCallback();
195 }
196 }
197 };
198
199 /**
200 * Execute commend
201 * @param event
202 * @private
203 */
204 MaterialExtCarousel.prototype.command_ = function (event) {
205 var _this3 = this;
206
207 var x = 0;
208 var slide = null;
209 var a = event.detail.action.toLowerCase();
210
211 // Cancel slideshow if running
212 this.cancelSlideShow_();
213
214 switch (a) {
215 case 'first':
216 slide = this.element_.querySelector('.' + SLIDE + ':first-child');
217 break;
218
219 case 'last':
220 x = this.element_.scrollWidth - this.element_.clientWidth;
221 slide = this.element_.querySelector('.' + SLIDE + ':last-child');
222 break;
223
224 case 'scroll-prev':
225 x = Math.max(this.element_.scrollLeft - this.element_.clientWidth, 0);
226 break;
227
228 case 'scroll-next':
229 x = Math.min(this.element_.scrollLeft + this.element_.clientWidth, this.element_.scrollWidth - this.element_.clientWidth);
230 break;
231
232 case 'next':
233 case 'prev':
234 slide = this.element_.querySelector('.' + SLIDE + '[aria-selected]');
235 if (slide) {
236 slide = a === 'next' ? slide.nextElementSibling : slide.previousElementSibling;
237 this.setAriaSelected_(slide);
238 this.emitSelectEvent_(a, null, slide);
239 }
240 return;
241
242 case 'play':
243 (0, _assign2.default)(this.config_, event.detail);
244 this.startSlideShow_();
245 return;
246
247 case 'pause':
248 return;
249
250 default:
251 return;
252 }
253
254 this.animateScroll_(x, undefined, function () {
255 if ('scroll-next' === a || 'scroll-prev' === a) {
256 var slides = _this3.getSlidesInViewport_();
257 if (slides.length > 0) {
258 slide = 'scroll-next' === a ? slides[0] : slides[slides.length - 1];
259 }
260 }
261 _this3.setAriaSelected_(slide);
262 _this3.emitSelectEvent_(a, null, slide);
263 });
264 };
265
266 /**
267 * Handles custom command event, 'scroll-prev', 'scroll-next', 'first', 'last', next, prev, play, pause
268 * @param event. A custom event
269 * @private
270 */
271 MaterialExtCarousel.prototype.commandHandler_ = function (event) {
272 event.preventDefault();
273 event.stopPropagation();
274 if (event.detail && event.detail.action) {
275 this.command_(event);
276 }
277 };
278
279 /**
280 * Handle keypress
281 * @param event
282 * @private
283 */
284 MaterialExtCarousel.prototype.keyDownHandler_ = function (event) {
285
286 if (event && event.target && event.target !== this.element_) {
287
288 var action = 'first';
289
290 if (event.keyCode === _constants.VK_HOME || event.keyCode === _constants.VK_END || event.keyCode === _constants.VK_PAGE_UP || event.keyCode === _constants.VK_PAGE_DOWN) {
291
292 event.preventDefault();
293 if (event.keyCode === _constants.VK_END) {
294 action = 'last';
295 } else if (event.keyCode === _constants.VK_PAGE_UP) {
296 action = 'scroll-prev';
297 } else if (event.keyCode === _constants.VK_PAGE_DOWN) {
298 action = 'scroll-next';
299 }
300
301 var cmd = new CustomEvent('select', {
302 detail: {
303 action: action
304 }
305 });
306 this.command_(cmd);
307 } else if (event.keyCode === _constants.VK_TAB || event.keyCode === _constants.VK_ENTER || event.keyCode === _constants.VK_SPACE || event.keyCode === _constants.VK_ARROW_UP || event.keyCode === _constants.VK_ARROW_LEFT || event.keyCode === _constants.VK_ARROW_DOWN || event.keyCode === _constants.VK_ARROW_RIGHT) {
308
309 var slide = getSlide_(event.target);
310
311 if (!slide) {
312 return;
313 }
314
315 // Cancel slideshow if running
316 this.cancelSlideShow_();
317
318 switch (event.keyCode) {
319 case _constants.VK_ARROW_UP:
320 case _constants.VK_ARROW_LEFT:
321 action = 'prev';
322 slide = slide.previousElementSibling;
323 break;
324
325 case _constants.VK_ARROW_DOWN:
326 case _constants.VK_ARROW_RIGHT:
327 action = 'next';
328 slide = slide.nextElementSibling;
329 break;
330
331 case _constants.VK_TAB:
332 if (event.shiftKey) {
333 action = 'prev';
334 slide = slide.previousElementSibling;
335 } else {
336 action = 'next';
337 slide = slide.nextElementSibling;
338 }
339 break;
340
341 case _constants.VK_SPACE:
342 case _constants.VK_ENTER:
343 action = 'select';
344 break;
345 }
346
347 if (slide) {
348 event.preventDefault();
349 setFocus_(slide);
350 this.emitSelectEvent_(action, event.keyCode, slide);
351 }
352 }
353 }
354 };
355
356 /**
357 * Handle dragging
358 * @param event
359 * @private
360 */
361 MaterialExtCarousel.prototype.dragHandler_ = function (event) {
362 var _this4 = this;
363
364 event.preventDefault();
365
366 // Cancel slideshow if running
367 this.cancelSlideShow_();
368
369 var updating = false;
370 var rAFDragId = 0;
371
372 var startX = event.clientX || (event.touches !== undefined ? event.touches[0].clientX : 0);
373 var prevX = startX;
374 var targetElement = event.target;
375
376 var update = function update(e) {
377 var currentX = e.clientX || (e.touches !== undefined ? e.touches[0].clientX : 0);
378 var dx = prevX - currentX;
379
380 if (dx < 0) {
381 _this4.element_.scrollLeft = Math.max(_this4.element_.scrollLeft + dx, 0);
382 } else if (dx > 0) {
383 _this4.element_.scrollLeft = Math.min(_this4.element_.scrollLeft + dx, _this4.element_.scrollWidth - _this4.element_.clientWidth);
384 }
385
386 prevX = currentX;
387 updating = false;
388 };
389
390 // drag handler
391 var drag = function drag(e) {
392 e.preventDefault();
393
394 if (!updating) {
395 rAFDragId = window.requestAnimationFrame(function () {
396 return update(e);
397 });
398 updating = true;
399 }
400 };
401
402 // end drag handler
403 var endDrag = function endDrag(e) {
404 e.preventDefault();
405
406 _this4.element_.removeEventListener('mousemove', drag);
407 _this4.element_.removeEventListener('touchmove', drag);
408 window.removeEventListener('mouseup', endDrag);
409 window.removeEventListener('touchend', endDrag);
410
411 // cancel any existing drag rAF, see: http://www.html5rocks.com/en/tutorials/speed/animations/
412 window.cancelAnimationFrame(rAFDragId);
413
414 var slide = getSlide_(targetElement);
415 setFocus_(slide);
416 _this4.emitSelectEvent_('click', null, slide);
417 };
418
419 this.element_.addEventListener('mousemove', drag);
420 this.element_.addEventListener('touchmove', drag);
421 window.addEventListener('mouseup', endDrag);
422 window.addEventListener('touchend', endDrag);
423 };
424
425 /**
426 * Handle click
427 * @param event
428 * @private
429 */
430 MaterialExtCarousel.prototype.clickHandler_ = function (event) {
431 // Click is handled by drag
432 event.preventDefault();
433 };
434
435 /**
436 * Handle focus
437 * @param event
438 * @private
439 */
440 MaterialExtCarousel.prototype.focusHandler_ = function (event) {
441 var slide = getSlide_(event.target);
442 if (slide) {
443 // The last focused/selected slide has 'aria-selected', even if focus is lost
444 this.setAriaSelected_(slide);
445 slide.classList.add(_constants.IS_FOCUSED);
446 }
447 };
448
449 /**
450 * Handle blur
451 * @param event
452 * @private
453 */
454 MaterialExtCarousel.prototype.blurHandler_ = function (event) {
455 var slide = getSlide_(event.target);
456 if (slide) {
457 slide.classList.remove(_constants.IS_FOCUSED);
458 }
459 };
460
461 /**
462 * Emits a custeom 'select' event
463 * @param command
464 * @param keyCode
465 * @param slide
466 * @private
467 */
468 MaterialExtCarousel.prototype.emitSelectEvent_ = function (command, keyCode, slide) {
469
470 if (slide) {
471 this.moveSlideIntoViewport_(slide);
472
473 var evt = new CustomEvent('select', {
474 bubbles: true,
475 cancelable: true,
476 detail: {
477 command: command,
478 keyCode: keyCode,
479 source: slide
480 }
481 });
482 this.element_.dispatchEvent(evt);
483 }
484 };
485
486 /**
487 * Get the first visible slide in component viewport
488 * @private
489 */
490 MaterialExtCarousel.prototype.getSlidesInViewport_ = function () {
491 var carouselRect = this.element_.getBoundingClientRect();
492
493 var slidesInViewport = [].concat((0, _toConsumableArray3.default)(this.element_.querySelectorAll('.' + SLIDE))).filter(function (slide) {
494 var slideRect = slide.getBoundingClientRect();
495 return slideRect.left >= carouselRect.left && slideRect.right <= carouselRect.right;
496 });
497 return slidesInViewport;
498 };
499
500 /**
501 * Move slide into component viewport - if needed
502 * @param slide
503 * @private
504 */
505 MaterialExtCarousel.prototype.moveSlideIntoViewport_ = function (slide) {
506 var carouselRect = this.element_.getBoundingClientRect();
507 var slideRect = slide.getBoundingClientRect();
508
509 if (slideRect.left < carouselRect.left) {
510 var x = this.element_.scrollLeft - (carouselRect.left - slideRect.left);
511 this.animateScroll_(x);
512 } else if (slideRect.right > carouselRect.right) {
513 var _x = this.element_.scrollLeft - (carouselRect.right - slideRect.right);
514 this.animateScroll_(_x);
515 }
516 };
517
518 /**
519 * Removes 'aria-selected' from all slides in carousel
520 * @private
521 */
522 MaterialExtCarousel.prototype.setAriaSelected_ = function (slide) {
523 if (slide) {
524 [].concat((0, _toConsumableArray3.default)(this.element_.querySelectorAll('.' + SLIDE + '[aria-selected]'))).forEach(function (slide) {
525 return slide.removeAttribute('aria-selected');
526 });
527 slide.setAttribute('aria-selected', '');
528 }
529 };
530
531 /**
532 * Removes event listeners
533 * @private
534 */
535 MaterialExtCarousel.prototype.removeListeners_ = function () {
536 this.element_.removeEventListener('focus', this.focusHandler_);
537 this.element_.removeEventListener('blur', this.blurHandler_);
538 this.element_.removeEventListener('keydown', this.keyDownHandler_);
539 this.element_.removeEventListener('mousedown', this.dragHandler_);
540 this.element_.removeEventListener('touchstart', this.dragHandler_);
541 this.element_.removeEventListener('click', this.clickHandler_, false);
542 this.element_.removeEventListener('command', this.commandHandler_);
543 this.element_.removeEventListener('mdl-componentdowngraded', this.mdlDowngrade_);
544 };
545
546 // Helpers
547 var getSlide_ = function getSlide_(element) {
548 return element.closest('.' + SLIDE);
549 };
550
551 var setFocus_ = function setFocus_(slide) {
552 if (slide) {
553 slide.focus();
554 }
555 };
556
557 var addRipple_ = function addRipple_(slide) {
558 if (!slide.querySelector('.' + MDL_RIPPLE_CONTAINER)) {
559 var rippleContainer = document.createElement('span');
560 rippleContainer.classList.add(MDL_RIPPLE_CONTAINER);
561 rippleContainer.classList.add(_constants.MDL_RIPPLE_EFFECT);
562 var ripple = document.createElement('span');
563 ripple.classList.add(_constants.MDL_RIPPLE);
564 rippleContainer.appendChild(ripple);
565
566 var img = slide.querySelector('img');
567 if (img) {
568 // rippleContainer blocks image title
569 rippleContainer.title = img.title;
570 }
571 slide.appendChild(rippleContainer);
572 componentHandler.upgradeElement(rippleContainer, _constants.MDL_RIPPLE_COMPONENT);
573 }
574 };
575 // End helpers
576
577
578 // Public methods.
579
580 /**
581 * Cancel animation - if running.
582 *
583 * @public
584 */
585 MaterialExtCarousel.prototype.stopAnimation = function () {
586 this.config_.animationLoop.stop();
587 };
588 MaterialExtCarousel.prototype['stopAnimation'] = MaterialExtCarousel.prototype.stopAnimation;
589
590 /**
591 * Upgrade slides
592 * Use if more list elements are added later (dynamically)
593 *
594 * @public
595 */
596 MaterialExtCarousel.prototype.upgradeSlides = function () {
597 var _this5 = this;
598
599 var hasRippleEffect = this.element_.classList.contains(_constants.MDL_RIPPLE_EFFECT);
600
601 [].concat((0, _toConsumableArray3.default)(this.element_.querySelectorAll('.' + SLIDE))).forEach(function (slide) {
602
603 slide.setAttribute('role', SLIDE_ROLE);
604
605 if (_this5.config_.interactive) {
606 if (!slide.getAttribute('tabindex')) {
607 slide.setAttribute('tabindex', '0');
608 }
609 if (hasRippleEffect) {
610 addRipple_(slide);
611 }
612 } else {
613 slide.setAttribute('tabindex', '-1');
614 }
615 });
616 };
617 MaterialExtCarousel.prototype['upgradeSlides'] = MaterialExtCarousel.prototype.upgradeSlides;
618
619 /**
620 * Get config object
621 *
622 * @public
623 */
624 MaterialExtCarousel.prototype.getConfig = function () {
625 return this.config_;
626 };
627 MaterialExtCarousel.prototype['getConfig'] = MaterialExtCarousel.prototype.getConfig;
628
629 /**
630 * Initialize component
631 */
632 MaterialExtCarousel.prototype.init = function () {
633
634 if (this.element_) {
635 // Config
636 if (this.element_.hasAttribute('data-config')) {
637 this.config_ = (0, _jsonUtils.jsonStringToObject)(this.element_.getAttribute('data-config'), this.config_);
638 }
639
640 // Wai-Aria
641 this.element_.setAttribute('role', ROLE);
642
643 // Prefer tabindex -1
644 if (!(0, _isInteger2.default)(this.element_.getAttribute('tabindex'))) {
645 this.element_.setAttribute('tabindex', -1);
646 }
647
648 // Remove listeners, just in case ...
649 this.removeListeners_();
650
651 if (this.config_.interactive) {
652
653 // Ripple
654 var hasRippleEffect = this.element_.classList.contains(_constants.MDL_RIPPLE_EFFECT);
655 if (hasRippleEffect) {
656 this.element_.classList.add(_constants.MDL_RIPPLE_EFFECT_IGNORE_EVENTS);
657 }
658
659 // Listen to focus/blur events
660 this.element_.addEventListener('focus', this.focusHandler_.bind(this), true);
661 this.element_.addEventListener('blur', this.blurHandler_.bind(this), true);
662
663 // Listen to keyboard events
664 this.element_.addEventListener('keydown', this.keyDownHandler_.bind(this), false);
665
666 // Listen to drag events
667 this.element_.addEventListener('mousedown', this.dragHandler_.bind(this), false);
668 this.element_.addEventListener('touchstart', this.dragHandler_.bind(this), false);
669
670 // Listen to click events
671 this.element_.addEventListener('click', this.clickHandler_.bind(this), false);
672 }
673
674 // Listen to custom 'command' event
675 this.element_.addEventListener('command', this.commandHandler_.bind(this), false);
676
677 // Listen to 'mdl-componentdowngraded' event
678 this.element_.addEventListener('mdl-componentdowngraded', this.mdlDowngrade_.bind(this));
679
680 // Slides collection
681 this.upgradeSlides();
682
683 // Set upgraded flag
684 this.element_.classList.add(_constants.IS_UPGRADED);
685
686 if (this.config_.autostart) {
687 // Start slideshow
688 this.startSlideShow_();
689 }
690 }
691 };
692
693 /*
694 * Downgrade component
695 * E.g remove listeners and clean up resources
696 */
697 MaterialExtCarousel.prototype.mdlDowngrade_ = function () {
698 'use strict';
699 //console.log('***** MaterialExtCarousel.mdlDowngrade_');
700
701 // Stop animation - if any
702
703 this.stopAnimation();
704
705 // Remove listeners
706 this.removeListeners_();
707 };
708
709 // The component registers itself. It can assume componentHandler is available
710 // in the global scope.
711 /* eslint no-undef: 0 */
712 componentHandler.register({
713 constructor: MaterialExtCarousel,
714 classAsString: 'MaterialExtCarousel',
715 cssClass: 'mdlext-js-carousel',
716 widget: true
717 });
718})();