blob: 51b9451a418f06dd370c83c3c782683f4a207cee [file] [log] [blame]
'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
});
})();