| 'use strict'; |
| |
| var _isInteger = require('babel-runtime/core-js/number/is-integer'); |
| |
| var _isInteger2 = _interopRequireDefault(_isInteger); |
| |
| var _toConsumableArray2 = require('babel-runtime/helpers/toConsumableArray'); |
| |
| var _toConsumableArray3 = _interopRequireDefault(_toConsumableArray2); |
| |
| var _assign = require('babel-runtime/core-js/object/assign'); |
| |
| var _assign2 = _interopRequireDefault(_assign); |
| |
| var _intervalFunction = require('../utils/interval-function'); |
| |
| var _intervalFunction2 = _interopRequireDefault(_intervalFunction); |
| |
| var _easing = require('../utils/easing'); |
| |
| var _jsonUtils = require('../utils/json-utils'); |
| |
| var _constants = require('../utils/constants'); |
| |
| function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } |
| |
| /** |
| * @license |
| * Copyright 2016 Leif Olsen. All Rights Reserved. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| * |
| * This code is built with Google Material Design Lite, |
| * which is Licensed under the Apache License, Version 2.0 |
| */ |
| |
| /** |
| * Image carousel |
| */ |
| |
| var MDL_RIPPLE_CONTAINER = 'mdlext-carousel__slide__ripple-container'; |
| |
| (function () { |
| 'use strict'; |
| |
| //const CAROUSEL = 'mdlext-carousel'; |
| |
| var SLIDE = 'mdlext-carousel__slide'; |
| var ROLE = 'list'; |
| var SLIDE_ROLE = 'listitem'; |
| |
| /** |
| * @constructor |
| * @param {Element} element The element that will be upgraded. |
| */ |
| var MaterialExtCarousel = function MaterialExtCarousel(element) { |
| // Stores the element. |
| this.element_ = element; |
| |
| // Default config |
| this.config_ = { |
| interactive: true, |
| autostart: false, |
| type: 'slide', |
| interval: 1000, |
| animationLoop: (0, _intervalFunction2.default)(1000) |
| }; |
| |
| this.scrollAnimation_ = (0, _intervalFunction2.default)(33); |
| |
| // Initialize instance. |
| this.init(); |
| }; |
| |
| window['MaterialExtCarousel'] = MaterialExtCarousel; |
| |
| /** |
| * Start slideshow animation |
| * @private |
| */ |
| MaterialExtCarousel.prototype.startSlideShow_ = function () { |
| var _this = this; |
| |
| var nextSlide = function nextSlide() { |
| var slide = _this.element_.querySelector('.' + SLIDE + '[aria-selected]'); |
| if (slide) { |
| slide.removeAttribute('aria-selected'); |
| slide = slide.nextElementSibling; |
| } |
| if (!slide) { |
| slide = _this.element_.querySelector('.' + SLIDE + ':first-child'); |
| _this.animateScroll_(0); |
| } |
| if (slide) { |
| _this.moveSlideIntoViewport_(slide); |
| slide.setAttribute('aria-selected', ''); |
| _this.emitSelectEvent_('next', null, slide); |
| return true; |
| } |
| return false; |
| }; |
| |
| var nextScroll = function nextScroll(direction) { |
| var nextDirection = direction; |
| |
| if ('next' === direction && _this.element_.scrollLeft === _this.element_.scrollWidth - _this.element_.clientWidth) { |
| nextDirection = 'prev'; |
| } else if (_this.element_.scrollLeft === 0) { |
| nextDirection = 'next'; |
| } |
| 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); |
| |
| _this.animateScroll_(x, 1000); |
| return nextDirection; |
| }; |
| |
| if (!this.config_.animationLoop.started) { |
| (function () { |
| _this.config_.animationLoop.interval = _this.config_.interval; |
| var direction = 'next'; |
| |
| if ('scroll' === _this.config_.type) { |
| _this.config_.animationLoop.start(function () { |
| direction = nextScroll(direction); |
| return true; // It runs until cancelSlideShow_ is triggered |
| }); |
| } else { |
| nextSlide(); |
| _this.config_.animationLoop.start(function () { |
| return nextSlide(); // It runs until cancelSlideShow_ is triggered |
| }); |
| } |
| })(); |
| } |
| |
| // TODO: Pause animation when carousel is not in browser viewport or user changes tab |
| }; |
| |
| /** |
| * Cancel slideshow if running. Emmits a 'pause' event |
| * @private |
| */ |
| MaterialExtCarousel.prototype.cancelSlideShow_ = function () { |
| if (this.config_.animationLoop.started) { |
| this.config_.animationLoop.stop(); |
| this.emitSelectEvent_('pause', _constants.VK_ESC, this.element_.querySelector('.' + SLIDE + '[aria-selected]')); |
| } |
| }; |
| |
| /** |
| * Animate scroll |
| * @param newPosition |
| * @param newDuration |
| * @param completedCallback |
| * @private |
| */ |
| MaterialExtCarousel.prototype.animateScroll_ = function (newPosition, newDuration, completedCallback) { |
| var _this2 = this; |
| |
| var start = this.element_.scrollLeft; |
| var distance = newPosition - start; |
| |
| if (distance !== 0) { |
| (function () { |
| var duration = Math.max(Math.min(Math.abs(distance), newDuration || 400), 100); // duration is between 100 and newDuration||400ms||distance |
| var t = 0; |
| _this2.scrollAnimation_.stop(); |
| _this2.scrollAnimation_.start(function (timeElapsed) { |
| t += timeElapsed; |
| if (t < duration) { |
| _this2.element_.scrollLeft = (0, _easing.inOutQuintic)(t, start, distance, duration); |
| return true; |
| } else { |
| _this2.element_.scrollLeft = newPosition; |
| if (completedCallback) { |
| completedCallback(); |
| } |
| return false; |
| } |
| }); |
| })(); |
| } else { |
| if (completedCallback) { |
| completedCallback(); |
| } |
| } |
| }; |
| |
| /** |
| * Execute commend |
| * @param event |
| * @private |
| */ |
| MaterialExtCarousel.prototype.command_ = function (event) { |
| var _this3 = this; |
| |
| var x = 0; |
| var slide = null; |
| var a = event.detail.action.toLowerCase(); |
| |
| // Cancel slideshow if running |
| this.cancelSlideShow_(); |
| |
| switch (a) { |
| case 'first': |
| slide = this.element_.querySelector('.' + SLIDE + ':first-child'); |
| break; |
| |
| case 'last': |
| x = this.element_.scrollWidth - this.element_.clientWidth; |
| slide = this.element_.querySelector('.' + SLIDE + ':last-child'); |
| break; |
| |
| case 'scroll-prev': |
| x = Math.max(this.element_.scrollLeft - this.element_.clientWidth, 0); |
| break; |
| |
| case 'scroll-next': |
| x = Math.min(this.element_.scrollLeft + this.element_.clientWidth, this.element_.scrollWidth - this.element_.clientWidth); |
| break; |
| |
| case 'next': |
| case 'prev': |
| slide = this.element_.querySelector('.' + SLIDE + '[aria-selected]'); |
| if (slide) { |
| slide = a === 'next' ? slide.nextElementSibling : slide.previousElementSibling; |
| this.setAriaSelected_(slide); |
| this.emitSelectEvent_(a, null, slide); |
| } |
| return; |
| |
| case 'play': |
| (0, _assign2.default)(this.config_, event.detail); |
| this.startSlideShow_(); |
| return; |
| |
| case 'pause': |
| return; |
| |
| default: |
| return; |
| } |
| |
| this.animateScroll_(x, undefined, function () { |
| if ('scroll-next' === a || 'scroll-prev' === a) { |
| var slides = _this3.getSlidesInViewport_(); |
| if (slides.length > 0) { |
| slide = 'scroll-next' === a ? slides[0] : slides[slides.length - 1]; |
| } |
| } |
| _this3.setAriaSelected_(slide); |
| _this3.emitSelectEvent_(a, null, slide); |
| }); |
| }; |
| |
| /** |
| * Handles custom command event, 'scroll-prev', 'scroll-next', 'first', 'last', next, prev, play, pause |
| * @param event. A custom event |
| * @private |
| */ |
| MaterialExtCarousel.prototype.commandHandler_ = function (event) { |
| event.preventDefault(); |
| event.stopPropagation(); |
| if (event.detail && event.detail.action) { |
| this.command_(event); |
| } |
| }; |
| |
| /** |
| * Handle keypress |
| * @param event |
| * @private |
| */ |
| MaterialExtCarousel.prototype.keyDownHandler_ = function (event) { |
| |
| if (event && event.target && event.target !== this.element_) { |
| |
| var action = 'first'; |
| |
| if (event.keyCode === _constants.VK_HOME || event.keyCode === _constants.VK_END || event.keyCode === _constants.VK_PAGE_UP || event.keyCode === _constants.VK_PAGE_DOWN) { |
| |
| event.preventDefault(); |
| if (event.keyCode === _constants.VK_END) { |
| action = 'last'; |
| } else if (event.keyCode === _constants.VK_PAGE_UP) { |
| action = 'scroll-prev'; |
| } else if (event.keyCode === _constants.VK_PAGE_DOWN) { |
| action = 'scroll-next'; |
| } |
| |
| var cmd = new CustomEvent('select', { |
| detail: { |
| action: action |
| } |
| }); |
| this.command_(cmd); |
| } 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) { |
| |
| var slide = getSlide_(event.target); |
| |
| if (!slide) { |
| return; |
| } |
| |
| // Cancel slideshow if running |
| this.cancelSlideShow_(); |
| |
| switch (event.keyCode) { |
| case _constants.VK_ARROW_UP: |
| case _constants.VK_ARROW_LEFT: |
| action = 'prev'; |
| slide = slide.previousElementSibling; |
| break; |
| |
| case _constants.VK_ARROW_DOWN: |
| case _constants.VK_ARROW_RIGHT: |
| action = 'next'; |
| slide = slide.nextElementSibling; |
| break; |
| |
| case _constants.VK_TAB: |
| if (event.shiftKey) { |
| action = 'prev'; |
| slide = slide.previousElementSibling; |
| } else { |
| action = 'next'; |
| slide = slide.nextElementSibling; |
| } |
| break; |
| |
| case _constants.VK_SPACE: |
| case _constants.VK_ENTER: |
| action = 'select'; |
| break; |
| } |
| |
| if (slide) { |
| event.preventDefault(); |
| setFocus_(slide); |
| this.emitSelectEvent_(action, event.keyCode, slide); |
| } |
| } |
| } |
| }; |
| |
| /** |
| * Handle dragging |
| * @param event |
| * @private |
| */ |
| MaterialExtCarousel.prototype.dragHandler_ = function (event) { |
| var _this4 = this; |
| |
| event.preventDefault(); |
| |
| // Cancel slideshow if running |
| this.cancelSlideShow_(); |
| |
| var updating = false; |
| var rAFDragId = 0; |
| |
| var startX = event.clientX || (event.touches !== undefined ? event.touches[0].clientX : 0); |
| var prevX = startX; |
| var targetElement = event.target; |
| |
| var update = function update(e) { |
| var currentX = e.clientX || (e.touches !== undefined ? e.touches[0].clientX : 0); |
| var dx = prevX - currentX; |
| |
| if (dx < 0) { |
| _this4.element_.scrollLeft = Math.max(_this4.element_.scrollLeft + dx, 0); |
| } else if (dx > 0) { |
| _this4.element_.scrollLeft = Math.min(_this4.element_.scrollLeft + dx, _this4.element_.scrollWidth - _this4.element_.clientWidth); |
| } |
| |
| prevX = currentX; |
| updating = false; |
| }; |
| |
| // drag handler |
| var drag = function drag(e) { |
| e.preventDefault(); |
| |
| if (!updating) { |
| rAFDragId = window.requestAnimationFrame(function () { |
| return update(e); |
| }); |
| updating = true; |
| } |
| }; |
| |
| // end drag handler |
| var endDrag = function endDrag(e) { |
| e.preventDefault(); |
| |
| _this4.element_.removeEventListener('mousemove', drag); |
| _this4.element_.removeEventListener('touchmove', drag); |
| window.removeEventListener('mouseup', endDrag); |
| window.removeEventListener('touchend', endDrag); |
| |
| // cancel any existing drag rAF, see: http://www.html5rocks.com/en/tutorials/speed/animations/ |
| window.cancelAnimationFrame(rAFDragId); |
| |
| var slide = getSlide_(targetElement); |
| setFocus_(slide); |
| _this4.emitSelectEvent_('click', null, slide); |
| }; |
| |
| this.element_.addEventListener('mousemove', drag); |
| this.element_.addEventListener('touchmove', drag); |
| window.addEventListener('mouseup', endDrag); |
| window.addEventListener('touchend', endDrag); |
| }; |
| |
| /** |
| * Handle click |
| * @param event |
| * @private |
| */ |
| MaterialExtCarousel.prototype.clickHandler_ = function (event) { |
| // Click is handled by drag |
| event.preventDefault(); |
| }; |
| |
| /** |
| * Handle focus |
| * @param event |
| * @private |
| */ |
| MaterialExtCarousel.prototype.focusHandler_ = function (event) { |
| var slide = getSlide_(event.target); |
| if (slide) { |
| // The last focused/selected slide has 'aria-selected', even if focus is lost |
| this.setAriaSelected_(slide); |
| slide.classList.add(_constants.IS_FOCUSED); |
| } |
| }; |
| |
| /** |
| * Handle blur |
| * @param event |
| * @private |
| */ |
| MaterialExtCarousel.prototype.blurHandler_ = function (event) { |
| var slide = getSlide_(event.target); |
| if (slide) { |
| slide.classList.remove(_constants.IS_FOCUSED); |
| } |
| }; |
| |
| /** |
| * Emits a custeom 'select' event |
| * @param command |
| * @param keyCode |
| * @param slide |
| * @private |
| */ |
| MaterialExtCarousel.prototype.emitSelectEvent_ = function (command, keyCode, slide) { |
| |
| if (slide) { |
| this.moveSlideIntoViewport_(slide); |
| |
| var evt = new CustomEvent('select', { |
| bubbles: true, |
| cancelable: true, |
| detail: { |
| command: command, |
| keyCode: keyCode, |
| source: slide |
| } |
| }); |
| this.element_.dispatchEvent(evt); |
| } |
| }; |
| |
| /** |
| * Get the first visible slide in component viewport |
| * @private |
| */ |
| MaterialExtCarousel.prototype.getSlidesInViewport_ = function () { |
| var carouselRect = this.element_.getBoundingClientRect(); |
| |
| var slidesInViewport = [].concat((0, _toConsumableArray3.default)(this.element_.querySelectorAll('.' + SLIDE))).filter(function (slide) { |
| var slideRect = slide.getBoundingClientRect(); |
| return slideRect.left >= carouselRect.left && slideRect.right <= carouselRect.right; |
| }); |
| return slidesInViewport; |
| }; |
| |
| /** |
| * Move slide into component viewport - if needed |
| * @param slide |
| * @private |
| */ |
| MaterialExtCarousel.prototype.moveSlideIntoViewport_ = function (slide) { |
| var carouselRect = this.element_.getBoundingClientRect(); |
| var slideRect = slide.getBoundingClientRect(); |
| |
| if (slideRect.left < carouselRect.left) { |
| var x = this.element_.scrollLeft - (carouselRect.left - slideRect.left); |
| this.animateScroll_(x); |
| } else if (slideRect.right > carouselRect.right) { |
| var _x = this.element_.scrollLeft - (carouselRect.right - slideRect.right); |
| this.animateScroll_(_x); |
| } |
| }; |
| |
| /** |
| * Removes 'aria-selected' from all slides in carousel |
| * @private |
| */ |
| MaterialExtCarousel.prototype.setAriaSelected_ = function (slide) { |
| if (slide) { |
| [].concat((0, _toConsumableArray3.default)(this.element_.querySelectorAll('.' + SLIDE + '[aria-selected]'))).forEach(function (slide) { |
| return slide.removeAttribute('aria-selected'); |
| }); |
| slide.setAttribute('aria-selected', ''); |
| } |
| }; |
| |
| /** |
| * Removes event listeners |
| * @private |
| */ |
| MaterialExtCarousel.prototype.removeListeners_ = function () { |
| this.element_.removeEventListener('focus', this.focusHandler_); |
| this.element_.removeEventListener('blur', this.blurHandler_); |
| this.element_.removeEventListener('keydown', this.keyDownHandler_); |
| this.element_.removeEventListener('mousedown', this.dragHandler_); |
| this.element_.removeEventListener('touchstart', this.dragHandler_); |
| this.element_.removeEventListener('click', this.clickHandler_, false); |
| this.element_.removeEventListener('command', this.commandHandler_); |
| this.element_.removeEventListener('mdl-componentdowngraded', this.mdlDowngrade_); |
| }; |
| |
| // Helpers |
| var getSlide_ = function getSlide_(element) { |
| return element.closest('.' + SLIDE); |
| }; |
| |
| var setFocus_ = function setFocus_(slide) { |
| if (slide) { |
| slide.focus(); |
| } |
| }; |
| |
| var addRipple_ = function addRipple_(slide) { |
| if (!slide.querySelector('.' + MDL_RIPPLE_CONTAINER)) { |
| var rippleContainer = document.createElement('span'); |
| rippleContainer.classList.add(MDL_RIPPLE_CONTAINER); |
| rippleContainer.classList.add(_constants.MDL_RIPPLE_EFFECT); |
| var ripple = document.createElement('span'); |
| ripple.classList.add(_constants.MDL_RIPPLE); |
| rippleContainer.appendChild(ripple); |
| |
| var img = slide.querySelector('img'); |
| if (img) { |
| // rippleContainer blocks image title |
| rippleContainer.title = img.title; |
| } |
| slide.appendChild(rippleContainer); |
| componentHandler.upgradeElement(rippleContainer, _constants.MDL_RIPPLE_COMPONENT); |
| } |
| }; |
| // End helpers |
| |
| |
| // Public methods. |
| |
| /** |
| * Cancel animation - if running. |
| * |
| * @public |
| */ |
| MaterialExtCarousel.prototype.stopAnimation = function () { |
| this.config_.animationLoop.stop(); |
| }; |
| MaterialExtCarousel.prototype['stopAnimation'] = MaterialExtCarousel.prototype.stopAnimation; |
| |
| /** |
| * Upgrade slides |
| * Use if more list elements are added later (dynamically) |
| * |
| * @public |
| */ |
| MaterialExtCarousel.prototype.upgradeSlides = function () { |
| var _this5 = this; |
| |
| var hasRippleEffect = this.element_.classList.contains(_constants.MDL_RIPPLE_EFFECT); |
| |
| [].concat((0, _toConsumableArray3.default)(this.element_.querySelectorAll('.' + SLIDE))).forEach(function (slide) { |
| |
| slide.setAttribute('role', SLIDE_ROLE); |
| |
| if (_this5.config_.interactive) { |
| if (!slide.getAttribute('tabindex')) { |
| slide.setAttribute('tabindex', '0'); |
| } |
| if (hasRippleEffect) { |
| addRipple_(slide); |
| } |
| } else { |
| slide.setAttribute('tabindex', '-1'); |
| } |
| }); |
| }; |
| MaterialExtCarousel.prototype['upgradeSlides'] = MaterialExtCarousel.prototype.upgradeSlides; |
| |
| /** |
| * Get config object |
| * |
| * @public |
| */ |
| MaterialExtCarousel.prototype.getConfig = function () { |
| return this.config_; |
| }; |
| MaterialExtCarousel.prototype['getConfig'] = MaterialExtCarousel.prototype.getConfig; |
| |
| /** |
| * Initialize component |
| */ |
| MaterialExtCarousel.prototype.init = function () { |
| |
| if (this.element_) { |
| // Config |
| if (this.element_.hasAttribute('data-config')) { |
| this.config_ = (0, _jsonUtils.jsonStringToObject)(this.element_.getAttribute('data-config'), this.config_); |
| } |
| |
| // Wai-Aria |
| this.element_.setAttribute('role', ROLE); |
| |
| // Prefer tabindex -1 |
| if (!(0, _isInteger2.default)(this.element_.getAttribute('tabindex'))) { |
| this.element_.setAttribute('tabindex', -1); |
| } |
| |
| // Remove listeners, just in case ... |
| this.removeListeners_(); |
| |
| if (this.config_.interactive) { |
| |
| // Ripple |
| var hasRippleEffect = this.element_.classList.contains(_constants.MDL_RIPPLE_EFFECT); |
| if (hasRippleEffect) { |
| this.element_.classList.add(_constants.MDL_RIPPLE_EFFECT_IGNORE_EVENTS); |
| } |
| |
| // Listen to focus/blur events |
| this.element_.addEventListener('focus', this.focusHandler_.bind(this), true); |
| this.element_.addEventListener('blur', this.blurHandler_.bind(this), true); |
| |
| // Listen to keyboard events |
| this.element_.addEventListener('keydown', this.keyDownHandler_.bind(this), false); |
| |
| // Listen to drag events |
| this.element_.addEventListener('mousedown', this.dragHandler_.bind(this), false); |
| this.element_.addEventListener('touchstart', this.dragHandler_.bind(this), false); |
| |
| // Listen to click events |
| this.element_.addEventListener('click', this.clickHandler_.bind(this), false); |
| } |
| |
| // Listen to custom 'command' event |
| this.element_.addEventListener('command', this.commandHandler_.bind(this), false); |
| |
| // Listen to 'mdl-componentdowngraded' event |
| this.element_.addEventListener('mdl-componentdowngraded', this.mdlDowngrade_.bind(this)); |
| |
| // Slides collection |
| this.upgradeSlides(); |
| |
| // Set upgraded flag |
| this.element_.classList.add(_constants.IS_UPGRADED); |
| |
| if (this.config_.autostart) { |
| // Start slideshow |
| this.startSlideShow_(); |
| } |
| } |
| }; |
| |
| /* |
| * Downgrade component |
| * E.g remove listeners and clean up resources |
| */ |
| MaterialExtCarousel.prototype.mdlDowngrade_ = function () { |
| 'use strict'; |
| //console.log('***** MaterialExtCarousel.mdlDowngrade_'); |
| |
| // Stop animation - if any |
| |
| this.stopAnimation(); |
| |
| // Remove listeners |
| this.removeListeners_(); |
| }; |
| |
| // The component registers itself. It can assume componentHandler is available |
| // in the global scope. |
| /* eslint no-undef: 0 */ |
| componentHandler.register({ |
| constructor: MaterialExtCarousel, |
| classAsString: 'MaterialExtCarousel', |
| cssClass: 'mdlext-js-carousel', |
| widget: true |
| }); |
| })(); |