UNPKG

webslides

Version:
475 lines (418 loc) 11.8 kB
import Plugins from '../plugins/plugins'; import Slide from './slide'; import DOM from '../utils/dom'; import scrollTo from '../utils/scroll-to'; const CLASSES = { VERTICAL: 'vertical', READY: 'ws-ready', DISABLED: 'disabled' }; // Default plugins const PLUGINS = { 'autoslide': Plugins.AutoSlide, 'clickNav': Plugins.ClickNav, 'grid': Plugins.Grid, 'hash': Plugins.Hash, 'keyboard': Plugins.Keyboard, 'nav': Plugins.Navigation, 'scroll': Plugins.Scroll, 'touch': Plugins.Touch, 'video': Plugins.Video, 'youtube': Plugins.YouTube, 'zoom': Plugins.Zoom }; /** * WebSlides module. */ export default class WebSlides { /** * Options for WebSlides * @param {number|boolean} autoslide If a number is provided, it will allow * autosliding by said amount of milliseconds. * @param {boolean} changeOnClick If true, it will allow * clicking on any place to change the slide. * @param {boolean} loop Whether to go to first slide from last one or not. * @param {number} minWheelDelta Controls the amount of needed scroll to * trigger navigation. * @param {boolean} navigateOnScroll Whether scroll can trigger navigation or * not. * @param {number} scrollWait Controls the amount of time to wait till * navigation can occur again with scroll. * @param {number} slideOffset Controls the amount of needed touch delta to * trigger navigation. * @param {boolean} showIndex Controls if the index can be shown. */ constructor({ autoslide = false, changeOnClick = false, loop = true, minWheelDelta = 40, navigateOnScroll = true, scrollWait = 450, slideOffset = 50, showIndex = true } = {}) { /** * WebSlide element. * @type {Element} */ this.el = document.getElementById('webslides'); if (!this.el) { throw new Error('Couldn\'t find the webslides container!'); } /** * Moving flag. * @type {boolean} */ this.isMoving = false; /** * Slide's array. * @type {?Array<Slide>} */ this.slides = null; /** * Current slide's index. * @type {number} * @private */ this.currentSlideI_ = -1; /** * Current slide reference. * @type {?Slide} * @private */ this.currentSlide_ = null; /** * Max slide index. * @type {number} * @private */ this.maxSlide_ = 0; /** * Whether the layout is going to be vertical or horizontal. * @type {boolean} */ this.isVertical = this.el.classList.contains(CLASSES.VERTICAL); /** * Plugin's dictionary. * @type {Object} */ this.plugins = {}; /** * Options dictionary. * @type {Object} */ this.options = { autoslide, changeOnClick, loop, minWheelDelta, navigateOnScroll, scrollWait, slideOffset, showIndex }; /** * Initialisation flag. * @type {boolean} */ this.initialised = false; // Bootstrapping this.removeChildren_(); this.grabSlides_(); this.createPlugins_(); this.initSlides_(); // Finished this.onInit_(); } /** * Removes all children elements inside of the main container that are not * eligible to be a Slide Element. * @private */ removeChildren_() { const nodes = this.el.childNodes; let i = nodes.length; while (i--) { const node = nodes[i]; if (!Slide.isCandidate(node)) { this.el.removeChild(node); } } } /** * Creates all the registered plugins and store the instances inside of the * the webslide instance. * @private */ createPlugins_() { Object.keys(PLUGINS).forEach(pluginName => { const PluginCto = PLUGINS[pluginName]; this.plugins[pluginName] = new PluginCto(this); }); } /** * Called once the WebSlide instance has finished initialising. * @private * @fires WebSlide#ws:init */ onInit_() { this.initialised = true; DOM.fireEvent(this.el, 'ws:init'); document.documentElement.classList.add(CLASSES.READY); } /** * Grabs the slides from the DOM and creates all the Slides modules. * @private */ grabSlides_() { this.slides = DOM.toArray(this.el.childNodes) .map((slide, i) => new Slide(slide, i)); this.maxSlide_ = this.slides.length; } /** * Goes to a given slide. * @param {!number} slideI The slide index. * @param {?boolean=} forward Whether we're forcing moving forward/backwards. * This parameter is used only from the goNext, goPrev functions to adjust the * scroll animations. */ goToSlide(slideI, forward = null) { if (this.isValidIndexSlide_(slideI) && !this.isMoving && this.currentSlideI_ !== slideI) { this.isMoving = true; let isMovingForward = false; if (forward !== null) { isMovingForward = forward; } else { if (this.currentSlideI_ >= 0) { isMovingForward = slideI > this.currentSlideI_; } } const nextSlide = this.slides[slideI]; if (this.currentSlide_ !== null && this.isVertical && (!this.plugins.touch || !this.plugins.touch.isEnabled)) { this.scrollTransitionToSlide_( isMovingForward, nextSlide, this.onSlideChange_); } else { this.transitionToSlide_( isMovingForward, nextSlide, this.onSlideChange_); } } } /** * Transitions to a slide, doing the scroll animation. * @param {boolean} isMovingForward Whether we're going forward or backwards. * @param {Slide} nextSlide Next slide. * @param {Function} callback Callback to be called upon finishing. This is an * async function so it'll happen once the scroll animation finishes. * @private * @see scrollTo */ scrollTransitionToSlide_(isMovingForward, nextSlide, callback) { this.el.style.overflow = 'hidden'; if (!isMovingForward) { nextSlide.moveBeforeFirst(); nextSlide.show(); scrollTo(this.currentSlide_.el.offsetTop, 0); } else { nextSlide.show(); } scrollTo(nextSlide.el.offsetTop, 500, () => { this.currentSlide_.hide(); if (isMovingForward) { this.currentSlide_.moveAfterLast(); } this.el.style.overflow = 'auto'; setTimeout(() => { callback.call(this, nextSlide); }, 150); }); } /** * Transitions to a slide, without doing the scroll animation. If the page is * already initialised and on mobile device, it will do a slide animation. * @param {boolean} isMovingForward Whether we're going forward or backwards. * @param {Slide} nextSlide Next slide. * @param {Function} callback Callback to be called upon finishing. This is a * sync function so it'll happen on run time. * @private */ transitionToSlide_(isMovingForward, nextSlide, callback) { scrollTo(0, 0); let className = 'slideInRight'; if (!isMovingForward) { nextSlide.moveBeforeFirst(); className = 'slideInLeft'; } if (this.currentSlide_) { if (isMovingForward) { this.currentSlide_.moveAfterLast(); } this.currentSlide_.hide(); } nextSlide.show(); if (this.initialised && this.plugins.touch && this.plugins.touch.isEnabled) { DOM.once(nextSlide.el, DOM.getAnimationEvent(), () => { nextSlide.el.classList.remove(className); callback.call(this, nextSlide); }); nextSlide.el.classList.add(className); } else { callback.call(this, nextSlide); } } /** * Whenever a slide is changed, this function gets called. It updates the * references to the current slide, disables the moving flag and fires * a custom event. * @param {Slide} slide The slide we're transitioning to. * @fires WebSlide#ws:slide-change * @private */ onSlideChange_(slide) { if (this.currentSlide_) { this.currentSlide_.disable(); } this.currentSlide_ = slide; this.currentSlideI_ = slide.i; this.currentSlide_.enable(); this.isMoving = false; DOM.fireEvent(this.el, 'ws:slide-change', { slides: this.maxSlide_, currentSlide0: this.currentSlideI_, currentSlide: this.currentSlideI_ + 1 }); } /** * Goes to the next slide. */ goNext() { let nextIndex = this.currentSlideI_ + 1; if (nextIndex >= this.maxSlide_) { if (!this.options.loop) { return; } nextIndex = 0; } this.goToSlide(nextIndex, true); } /** * Goes to the previous slide. */ goPrev() { let prevIndex = this.currentSlideI_ - 1; if (prevIndex < 0) { if (!this.options.loop) { return; } prevIndex = this.maxSlide_ - 1; } this.goToSlide(prevIndex, false); } /** * Check if the given number is a valid index to go to. * @param {number} i The index to check. * @return {boolean} Whether you can move to that slide or not. * @private */ isValidIndexSlide_(i) { return typeof i === 'number' && i >= 0 && i < this.maxSlide_; } /** * Init the shown slide on load. It'll fetch it from the Hash if present * and, otherwise, it'll default to the first one. * @private * @see Hash.getSlideNumber */ initSlides_() { let slideNumber = this.plugins.hash.constructor.getSlideNumber(); // Not valid if (slideNumber === null || slideNumber >= this.maxSlide_) { slideNumber = 0; } // Keeping the order if (slideNumber !== 0) { let i = 0; while(i < slideNumber) { this.slides[i].moveAfterLast(); i++; } } this.goToSlide(slideNumber); } /** * Toggles zoom */ toggleZoom() { if (this.options.showIndex) { this.plugins.zoom.toggleZoom(); } } /** * Disables the webslides element adding a class "disabled" */ disable() { this.el.classList.add(CLASSES.DISABLED); if (this.plugins.autoslide && this.plugins.autoslide.time !== false) { this.plugins.autoslide.stop(); } } /** * Enables the webslides element removing a class "disabled" */ enable() { this.el.classList.remove(CLASSES.DISABLED); if (this.plugins.autoslide && this.plugins.autoslide.time !== false) { this.plugins.autoslide.play(); } } /** * Checks if it is disabled * @return {boolean} */ isDisabled() { return this.el.classList.contains(CLASSES.DISABLED); } /** * Puts the browser into fullscreen */ fullscreen() { const el = document.documentElement; const isFullscreen = document.fullscreen || document.webkitIsFullScreen || document.mozFullScreen || document.msFullScreenElement; if (!isFullscreen) { /* istanbul ignore next hard to test prefixes */ const requestFullscreen = el.requestFullscreen || el.webkitRequestFullScreen || el.mozRequestFullScreen || el.msRequestFullscreen; requestFullscreen.call(el); } else { /* istanbul ignore next hard to test prefixes */ const cancelFullscreen = document.exitFullScreen || document.webkitCancelFullScreen || document.mozCancelFullScreen || document.msExitFullscreen; cancelFullscreen.call(document); } } /** * Registers a plugin to be loaded when the instance is created. It allows * (on purpose) to replace default plugins. * @param {!string} key They key under which it'll be stored inside of the * instance, inside the plugins dict. * @param {!Function} cto Plugin constructor. */ static registerPlugin(key, cto) { PLUGINS[key] = cto; } }