UNPKG

@tannerhodges/snap-slider

Version:

Simple JavaScript plugin to manage sliders using CSS Scroll Snap.

1,385 lines (1,134 loc) 117 kB
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define([], factory); else if(typeof exports === 'object') exports["SnapSlider"] = factory(); else root["SnapSlider"] = factory(); })(window, function() { return /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); /******/ } /******/ }; /******/ /******/ // define __esModule on exports /******/ __webpack_require__.r = function(exports) { /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); /******/ } /******/ Object.defineProperty(exports, '__esModule', { value: true }); /******/ }; /******/ /******/ // create a fake namespace object /******/ // mode & 1: value is a module id, require it /******/ // mode & 2: merge all properties of value into the ns /******/ // mode & 4: return value when already ns object /******/ // mode & 8|1: behave like require /******/ __webpack_require__.t = function(value, mode) { /******/ if(mode & 1) value = __webpack_require__(value); /******/ if(mode & 8) return value; /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; /******/ var ns = Object.create(null); /******/ __webpack_require__.r(ns); /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); /******/ return ns; /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = function(module) { /******/ var getter = module && module.__esModule ? /******/ function getDefault() { return module['default']; } : /******/ function getModuleExports() { return module; }; /******/ __webpack_require__.d(getter, 'a', getter); /******/ return getter; /******/ }; /******/ /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 0); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony import */ var element_closest__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony import */ var smoothscroll_polyfill__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2); /* harmony import */ var smoothscroll_polyfill__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(smoothscroll_polyfill__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var tabbable__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(3); /* harmony import */ var tabbable__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(tabbable__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var lodash_debounce__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4); /* harmony import */ var lodash_debounce__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(lodash_debounce__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var lodash_throttle__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(19); /* harmony import */ var lodash_throttle__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(lodash_throttle__WEBPACK_IMPORTED_MODULE_4__); /* harmony import */ var _helpers_getClosestAttribute__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(20); /* harmony import */ var _helpers_getElements__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(21); /* harmony import */ var _helpers_getStyle__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(24); /* harmony import */ var _helpers_hasOwnProperty__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(25); /* harmony import */ var _helpers_isObject__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(26); /* harmony import */ var _helpers_minmax__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(27); /* harmony import */ var _helpers_on__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(28); /* harmony import */ var _helpers_onReady__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(29); /* harmony import */ var _helpers_passive__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(30); /* harmony import */ var _helpers_pick__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(31); /* harmony import */ var _helpers_qsa__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(22); /* harmony import */ var _helpers_toArray__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(23); /* harmony import */ var _helpers_values__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(32); function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } // Dependencies // Helpers // Modules var logger = true ? __webpack_require__(33) : undefined; // Internal Variables var counter = 1; /** * Snap Slider. * @class */ var SnapSlider = /*#__PURE__*/function () { /** * New Snap Slider. * * See `init()` for a full breakdown of `options`. * * @param {String|Element|Array|Object} containerOrOptions * @param {Object} options * @constructor */ function SnapSlider(containerOrOptions) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; _classCallCheck(this, SnapSlider); // Setup internal variables. this.terms = { prev: /(prev|back|before|left|up)/, next: /(next|forward|after|right|down)/ }; /* eslint-disable quote-props */ this.callbacks = { 'load': [], 'change': [], 'change.click': [], 'change.scroll': [], 'change.keydown': [], 'change.focusin': [], 'scroll': [], 'scroll.start': [], 'scroll.end': [] }; /* eslint-enable quote-props */ this.init(containerOrOptions, options); // Don't construct sliders with empty containers. if (!this.container) { return; } this.watchForChanges(); // Keep track of the slider so we can reference & debug it later. this.container.SnapSlider = this; window._SnapSliders[this.id] = this; } /** * Initialize this slider. * * @param {String|Element|Array|Object} containerOrOptions * @param {Object} options * @param {String|Element|Array} options.container * @param {String} options.id * @param {String|Element|Array} options.slides * @param {String|Number} options.start * @param {String|Element|Array} options.nav * @param {String|Element|Array} options.buttons * @param {String|Element|Array} options.prev * @param {String|Element|Array} options.next * @return {void} */ _createClass(SnapSlider, [{ key: "init", value: function init(containerOrOptions) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; // Allow people to quickly spin up sliders by just passing a container // element, or by passing in a single options object. if (Object(_helpers_isObject__WEBPACK_IMPORTED_MODULE_9__["default"])(containerOrOptions)) { options = containerOrOptions; } // Fill default options. this.options = _objectSpread({ container: containerOrOptions, id: '', slides: '', nav: '', buttons: '', prev: '', next: '', start: 0, loop: null, on: {} }, options); // Get single element from params. var container = Object(_helpers_getElements__WEBPACK_IMPORTED_MODULE_6__["default"])(this.options.container).shift(); // Don't construct sliders with empty containers. if (!container) { if (true) { console.log("\n\uD83D\uDEAB Whoops! Snap Slider can't find a container element matching \"".concat(this.options.container, "\".\n\n\uD83D\uDD0D Please check your selectors for typos. Make sure the element actually exists\n in the DOM when Snap Slider tries to initialize it.\n\n\uD83D\uDC47 Here's a copy of the options you tried to initialize with for debugging:\n\n"), this.options, '\n\n'); } return; } // Great! Now let's start initializing everything. this.container = container; // Get selectors from JavaScript or data attributes. this.options.buttons = options.buttons || this.container.getAttribute('data-snap-slider-buttons'); this.options.prev = options.prev || this.container.getAttribute('data-snap-slider-prev'); this.options.next = options.next || this.container.getAttribute('data-snap-slider-next'); // Get and set persistent options in data attributes. this.id = this.getMaybeSetID(container, this.options.id); this.slides = this.getMaybeSetSlides(container, this.options.slides); this.align = this.getMaybeSetAlign(container, this.options.align); this.current = this.getMaybeSetStart(container, this.options.start); this.loop = this.getMaybeSetLoop(container, this.options.loop); // Reset internal variables. this.transition = null; this.scrolling = false; // Add custom callbacks. // eslint-disable-next-line no-restricted-syntax for (var eventName in this.options.on) { if (Object(_helpers_hasOwnProperty__WEBPACK_IMPORTED_MODULE_8__["default"])(this.options.on, eventName)) { this.on(eventName, this.options.on[eventName]); } } // Setup navigation. // NOTE: If left blank, `addNav()` will handle the fallbacks for button selectors. var navOptions = Object(_helpers_pick__WEBPACK_IMPORTED_MODULE_14__["default"])(this.options, ['buttons', 'prev', 'next']); // Init custom goto buttons in the container. // NOTE: "Goto" buttons are automatically handled by delegated click // events on the `body`. For more details, see `handleGotoClick()`. this.addGotoButtons(_objectSpread(_objectSpread({}, navOptions), {}, { container: container })); // Init standard navs with data attributes. this.addNav("[data-snap-slider-nav=\"".concat(this.id, "\"]"), navOptions); // Then init custom navs too. if (this.options.nav) { this.addNav(this.options.nav, navOptions); } // Go to the slide we want to start on. this.update(); } /** * Get and maybe set a slider's ID on the closest container element. * * If no ID was specified, generates a fallback ID. * * @param {Element} container * @param {String} id * @return {String} */ }, { key: "getMaybeSetID", value: function getMaybeSetID(container, id) { // Either use the ID we were given or the ID already on the container. id = id || container.getAttribute('data-snap-slider') || container.id; // If we don't have an ID, make one up and increment our internal // counter for the next slider. if (!id) { id = "slider-".concat(counter); counter += 1; } // Store value in data attribute. container.setAttribute('data-snap-slider', id); // Return the final ID. return id; } /** * Get all slide elements for a given container. * * Defaults to container's children. * * @param {Element} container * @param {String} selector * @return {Array} */ }, { key: "getMaybeSetSlides", value: function getMaybeSetSlides(container, selector) { // Get selector from JavaScript or data attribute. selector = selector && typeof selector === 'string' ? selector : container.getAttribute('data-snap-slider-slides'); // Store value in data attribute. container.setAttribute('data-snap-slider-slides', selector || ''); // If selector exists, use those elements. Otherwise, // assume the container's immediate children are slides. var slides = selector ? Object(_helpers_getElements__WEBPACK_IMPORTED_MODULE_6__["default"])(selector, container) : Object(_helpers_toArray__WEBPACK_IMPORTED_MODULE_16__["default"])(container.children); // Ensure all slides are focusable but not tabbable. slides.forEach(function (slide) { return slide.setAttribute('tabindex', '-1'); }); // Return array of slides. return slides; } /** * Get alignment fallback for a given container. * * @param {Element} container * @param {String} align * @return {String} */ }, { key: "getMaybeSetAlign", value: function getMaybeSetAlign(container, align) { // Get align index from JavaScript, data attribute, or leave blank. align = align || container.getAttribute('data-snap-slider-align') || ''; // Store value in data attribute. container.setAttribute('data-snap-slider-align', align); return align; } /** * Get start index for a given container. * * Defaults to 1. * * @param {Element} container * @param {String} start * @return {String|Number} */ }, { key: "getMaybeSetStart", value: function getMaybeSetStart(container, start) { // Get start index from JavaScript, data attribute, or default to 1. if (!SnapSlider.isValidIndex(start)) { start = container.getAttribute('data-snap-slider-start') || 1; } // Store value in data attribute. container.setAttribute('data-snap-slider-start', start); return start; } /** * Get and maybe set a slider's `loop` option on the closest container element. * * @param {Element} container * @param {Boolean} loop * @return {String} */ }, { key: "getMaybeSetLoop", value: function getMaybeSetLoop(container, loop) { // If we were given a Boolean value to set, use that. // Else check for an existing data attribute. // Defaults to `false`. loop = typeof loop === 'boolean' ? loop : container.getAttribute('data-snap-slider-loop') === 'true'; // Store value in data attribute. container.setAttribute('data-snap-slider-loop', loop); // Return the final loop value. return loop; } /** * Get the `scroll-snap-align` for a snap slider element. * * Falls back to `data-snap-slider-align` when no CSS * is detected, otherwise defaults to `start`. * * @param {Element} el * @return {String} */ }, { key: "getSnapAlign", value: function getSnapAlign(el) { // Get element's CSS align value. var style = Object(_helpers_getStyle__WEBPACK_IMPORTED_MODULE_7__["default"])(el, 'scrollSnapAlign'); // If browser supports Scroll Snap and slide // has a non-empty value, return it. if (style && style.indexOf('none') < 0) { return style; } // Otherwise, fallback to the slider's align attribute. // Else assume "start" for everything. return Object(_helpers_getClosestAttribute__WEBPACK_IMPORTED_MODULE_5__["default"])(el, 'data-snap-slider-align') || 'start'; } /** * Get a specific slide element. Accepts any valid goto alias. * * @param {Number} index Starts at 1. * @return {Element} */ }, { key: "getSlide", value: function getSlide(index) { // Convert index aliases to numbers. index = this.getIndexNumber(index); // Return the slide for that numeric index. // NOTE: Subtract 1 because the array is 0-index, but our API is 1-index. return this.slides[index - 1]; } /** * Get the current slide element. * * @return {Element} */ }, { key: "getCurrentSlide", value: function getCurrentSlide() { // NOTE: Subtract 1 because the array is 0-index, but our API is 1-index. return this.slides[this.current - 1]; } /** * Is this a valid index? * * - first * - middle * - last * - prev * - next * * @param {String|Number} index * @return {Number} */ }, { key: "getIndexNumber", /** * Get the slide number for any index. * * Returns -1 if index is invalid. * * @param {String|Number} index * @return {Number} */ value: function getIndexNumber(index) { var num; if (index === 'first') { // Get the first slide. num = 1; } else if (index === 'middle') { // Get the middle slide. num = Math.ceil(this.slides.length / 2); } else if (index === 'last') { // Get the last slide. num = this.slides.length; } else if (index === 'prev') { // Get the previous slide. num = this.current - 1; } else if (index === 'next') { // Get the next slide. num = this.current + 1; } else { // Try to get a number. num = parseInt(index, 10) || -1; } if (this.loop) { // If we're looping, send out-of-bounds requests // to the other end of the slider. if (num < 1) { num = this.slides.length; } if (num > this.slides.length) { num = 1; } } else if (num < 1 || num > this.slides.length) { // Otherwise, ignore out-of-range indexes. num = -1; } // Return numeric index. Or, if something goes wrong, // fallback to the first slide. return num || 1; } /** * Get the offset we should scroll to for a specific slide. * * @param {Element} slide * @return {Object} { top, left } */ }, { key: "getScrollOffset", value: function getScrollOffset(slide) { var container = this.container; var align = this.getSnapAlign(slide); // Calculate the 'start' position by default. // NOTE: This forces slides with align `none` to still snap into place. var top = slide.offsetTop; var left = slide.offsetLeft; // NOTE: Because Safari uses the 2-value syntax, we simply check for matching // keywords. If this causes incorrect behavior, use the `data-snap-slider-align` // attribute to override our automatic CSS detection. if (align.indexOf('center') >= 0) { // To center a slide, start with its beginning offset (the 'start' position). // Then add half the slide's size minus half the container size. top = slide.offsetTop + slide.offsetHeight / 2 - container.offsetHeight / 2; left = slide.offsetLeft + slide.offsetWidth / 2 - container.offsetWidth / 2; } else if (align.indexOf('end') >= 0) { // To align the end of a slide, start with its beginning offset (the 'start' position). // Then subtract the size of the container, but add back the size of the slide. top = slide.offsetTop - container.offsetHeight + slide.offsetHeight; left = slide.offsetLeft - container.offsetWidth + slide.offsetWidth; } // Keep offsets within the scrollable area. top = Object(_helpers_minmax__WEBPACK_IMPORTED_MODULE_10__["default"])(top, 0, container.scrollHeight); left = Object(_helpers_minmax__WEBPACK_IMPORTED_MODULE_10__["default"])(left, 0, container.scrollWidth); return { top: top, left: left }; } /** * Go to a slide. * * @param {String|Number} index Starts at 1. * @param {Object} options * @param {Boolean} options.focus * @param {Boolean} options.force * @param {Boolean} options.ignoreCallbacks * @param {Boolean} options.immediate * @param {Event} event * @return {Boolean} */ }, { key: "goto", value: function goto(index) { var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var event = arguments.length > 2 ? arguments[2] : undefined; // Fill default options. options = _objectSpread({ // By default, focus the slide we're going to. focus: true, // Force-update the scroll position, even if we're already on the current slide. force: false, // Ignore custom callbacks for events. ignoreCallbacks: false, // Immediately update position without smooth scrolling. immediate: false }, options); // Get the next slide we should go to. var next = this.getIndexNumber(index); // If nothing changed, don't do anything (as long as // we're not trying to force it). if (!options.force && next === this.current) { return false; } // Get the next slide. var slide = this.getSlide(next); if (!slide) { return false; } // Scroll to it! var _this$getScrollOffset = this.getScrollOffset(slide), top = _this$getScrollOffset.top, left = _this$getScrollOffset.left; if (options.immediate) { // Scroll immediately. this.container.scroll({ top: top, left: left }); } else { // Let the event handlers know we're coming, then smooth scroll. this.startTransition(next); this.container.scroll({ top: top, left: left, behavior: 'smooth' }); } // Update state. this.current = next; // We changed slides! this.fireEvent('change', event, options); return true; } /** * Build the `goto` attribute for a nav button. * * @param {Element|Boolean} nav * @param {String|Number} index * @return {String} */ }, { key: "buildGoto", value: function buildGoto(nav) { var index = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; // Start with an empty string. var _goto = ''; // If this button isn't part of a nav, include the slider ID. if (!nav) { _goto += "".concat(this.id, ":"); } // Add the index and return. return _goto + index; } /** * Set the `goto` attribute for nav buttons. * * @param {String|Element|Array} buttons * @param {String} index * @return {void} */ }, { key: "setGoto", value: function setGoto(buttons, index) { var _this = this; buttons = Object(_helpers_getElements__WEBPACK_IMPORTED_MODULE_6__["default"])(buttons); // If we found custom prev/next buttons, set their `goto` attributes // before we loop through the rest of the buttons. buttons.forEach(function (button) { button.setAttribute('data-snap-slider-goto', _this.buildGoto( // Don't assume this button is grouped with the others. It may // be somewhere else on the page, so double check for a parent // slider or nav container. button.closest('[data-snap-slider], [data-snap-slider-nav]'), index)); }); } /** * Get the slider ID and slide index a goto button is targeting. * * NOTE: This method is static so we can call it in the delegated body * click events. For more details, see `handleGotoClick()`. * * @param {String|Element|Array} button * @return {Object} { sliderID, index } */ }, { key: "startTransition", /** * Start transitioning to another slide. * * This way when you click a nav button, the current slide updates * immediately but the scroll listener doesn't override it, or fire * extra change events. * * @param {Number} next * @return {void} */ value: function startTransition(next) { var _this2 = this; // Tell the scroll listener which slide we're transitioning to. this.transition = { from: this.current, to: next, diff: Math.abs(next - this.current) }; // In case someone's fast enough to start scrolling again before our // scroll listener resolves the `transition` flag, or if the slide's // already visible and nothing actually has to scroll, // set a timeout to resolve the transition. var stuck = this.transition.to; // If there's already a check waiting, clear it to avoid accidentally // reverting to the wrong slide. if (this.checkTransition) { clearTimeout(this.checkTransition); } // Now make sure we don't get stuck! this.checkTransition = setTimeout(function () { if (_this2.transition.to === stuck) { _this2.stopTransition(); } }, 1000); } /** * Stop the transitions! Set things back to normal. * * @return {void} */ }, { key: "stopTransition", value: function stopTransition() { // Clear transition checks. this.transition = null; clearTimeout(this.checkTransition); } /** * Is this a "previous" button? * * @param {String|Element|Array} button * @return {Boolean} */ }, { key: "isPrevButton", value: function isPrevButton(button) { button = Object(_helpers_getElements__WEBPACK_IMPORTED_MODULE_6__["default"])(button).shift(); // Ignore missing elements. if (!button) { return false; } // Check whether the `goto` attribute is "prev". // If not, check the text & class for common "prev" terms. return (button.getAttribute('data-snap-slider-goto') || '').match(/\bprev$/) || button.textContent.toLowerCase().match(this.terms.prev) || button.className.toLowerCase().match(this.terms.prev); } /** * Is this a "next" button? * * @param {String|Element|Array} button * @return {Boolean} */ }, { key: "isNextButton", value: function isNextButton(button) { button = Object(_helpers_getElements__WEBPACK_IMPORTED_MODULE_6__["default"])(button).shift(); // Ignore missing elements. if (!button) { return false; } // Check whether the `goto` attribute is "next". // If not, check the text & class for common "next" terms. return (button.getAttribute('data-snap-slider-goto') || '').match(/\bnext$/) || button.textContent.toLowerCase().match(this.terms.next) || button.className.toLowerCase().match(this.terms.next); } /** * Is this index a relative term? I.e., is it `prev` or `next`? * * @param {String|Number} index * @return {Boolean} */ }, { key: "isCurrent", /** * Does an index match the current slide? * * @param {String|Number} index * @return {Boolean} */ value: function isCurrent(index) { // Ignore relative indexes (i.e., `prev` and `next`) since they // always refer to one more or less than the current index. if (SnapSlider.isRelative(index)) { return false; } // Does this numeric index match the current slide? return this.getIndexNumber(index) === this.current; } /** * Add goto buttons for the current slider. * * @param {String|Element|Array|Object} buttonsOrOptions * @param {Object} options * @param {String|Element|Array} options.container * @param {String|Element|Array} options.buttons * @param {String|Element|Array} options.prev * @param {String|Element|Array} options.next * @return {Boolean} */ }, { key: "addGotoButtons", value: function addGotoButtons(buttonsOrOptions) { var _this3 = this; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; // Allow people to quickly add nav buttons by just passing the // selector, or by passing in a single options object. if (Object(_helpers_isObject__WEBPACK_IMPORTED_MODULE_9__["default"])(buttonsOrOptions)) { options = buttonsOrOptions; } // Fill default options. options = _objectSpread({ container: '', buttons: buttonsOrOptions, prev: '', next: '' }, options); // Get button elements. // NOTE: If someone passes an overly-generic selector (e.g., `button`) // this will query the entire document. In general, you should either // specify a container element, use specific selectors, or pass // the elements directly. var buttons = Object(_helpers_getElements__WEBPACK_IMPORTED_MODULE_6__["default"])(options.buttons, options.container); var prev = Object(_helpers_getElements__WEBPACK_IMPORTED_MODULE_6__["default"])(options.prev, options.container); var next = Object(_helpers_getElements__WEBPACK_IMPORTED_MODULE_6__["default"])(options.next, options.container); // If we found custom prev/next buttons, set their `goto` attributes // before we loop through the rest of the buttons. prev.forEach(function (b) { return b.hasAttribute('data-snap-slider-goto') || _this3.setGoto(prev, 'prev'); }); next.forEach(function (b) { return b.hasAttribute('data-snap-slider-goto') || _this3.setGoto(next, 'next'); }); // Keep track of the index outside of the loop so we can // skip prev/next buttons but still go in order. var nextIndex = 1; // Loop through the buttons and set each one's `goto` attribute. buttons.forEach(function (button) { // Ignore buttons that already have a `goto` attribute. if (button.hasAttribute('data-snap-slider-goto')) { return null; } // Previous if (_this3.isPrevButton(button)) { return _this3.setGoto(button, 'prev'); } // Next if (_this3.isNextButton(button)) { return _this3.setGoto(button, 'next'); } // Numeric: Check the text for a number, else fallback to the next index. var index = parseInt(button.textContent.replace(/.*\b(\d+)\b.*/, '$1'), 10) || nextIndex; // Increment the next index. nextIndex = index + 1; return _this3.setGoto(button, index); }); this.updateButtons(); return true; } /** * Get navs for the current slider. * * @return {Array} */ }, { key: "getNavs", value: function getNavs() { var _this4 = this; // eslint-disable-next-line arrow-body-style return Object(_helpers_qsa__WEBPACK_IMPORTED_MODULE_15__["default"])('[data-snap-slider-nav]').filter(function (nav) { // Only return navs targeting the current slider. return nav.getAttribute('data-snap-slider-nav') === _this4.id; }); } /** * Get nav buttons for the current slider. * * @return {Array} */ }, { key: "getButtons", value: function getButtons() { var _this5 = this; return Object(_helpers_qsa__WEBPACK_IMPORTED_MODULE_15__["default"])('[data-snap-slider-goto]').filter(function (button) { var _SnapSlider$getButton = SnapSlider.getButtonTarget(button), sliderID = _SnapSlider$getButton.sliderID; // Only return buttons targeting the current slider. return sliderID === _this5.id; }); } /** * Update nav buttons for the current slider. * * @return {void} */ }, { key: "updateButtons", value: function updateButtons() { var _this6 = this; // Wait until the slider has initialized. if (!this.current) { return; } // Loop through all the nav buttons. this.getButtons().forEach(function (button) { // Figure out which slide it's for... var _SnapSlider$getButton2 = SnapSlider.getButtonTarget(button), index = _SnapSlider$getButton2.index; // And update its class. if (_this6.isCurrent(index)) { button.classList.add('is-current'); } else { button.classList.remove('is-current'); } // Also, enable/disable relative buttons unless `loop` is on. if (!_this6.loop && SnapSlider.isRelative(index)) { // Disable prev button on first slide. // Disable next button on last slide. var disabled = index === 'prev' && _this6.current === 1 || index === 'next' && _this6.current === _this6.slides.length; if (disabled) { // button.setAttribute('disabled', ''); button.classList.add('is-disabled'); } else { // button.removeAttribute('disabled', ''); button.classList.remove('is-disabled'); } } }); } /** * Update slide active states when the slider changes. * * @return {void} */ }, { key: "updateSlides", value: function updateSlides() { var _this7 = this; this.slides.forEach(function (slide, index) { // NOTE: Subtract 1 because the array is 0-index, but our API is 1-index. if (index === _this7.current - 1) { slide.classList.add('is-current'); slide.removeAttribute('aria-hidden'); // Enable tabbing for current slide Object(_helpers_qsa__WEBPACK_IMPORTED_MODULE_15__["default"])('[data-snap-slider-tabindex]', slide).forEach(function (tab) { tab.removeAttribute('tabindex'); }); } else { slide.classList.remove('is-current'); slide.setAttribute('aria-hidden', 'true'); // Disable tabbing for non-current slides tabbable__WEBPACK_IMPORTED_MODULE_2___default()(slide).forEach(function (tab) { tab.setAttribute('tabindex', '-1'); tab.setAttribute('data-snap-slider-tabindex', ''); }); } }); } /** * Add a nav element for the current slider. Automatically hooks up any nav * buttons inside the nav. * * @param {String|Element|Array|Object} containerOrOptions * @param {Object} options * @param {String|Element|Array} options.container * @param {String|Element|Array} options.buttons * @param {String|Element|Array} options.prev * @param {String|Element|Array} options.next * @return {Boolean} */ }, { key: "addNav", value: function addNav(containerOrOptions) { var _this8 = this; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; // Allow people to quickly add a nav by just passing a container // element, or by passing in a single options object. if (Object(_helpers_isObject__WEBPACK_IMPORTED_MODULE_9__["default"])(containerOrOptions)) { options = containerOrOptions; } // Fill default options. options = _objectSpread({ container: containerOrOptions, buttons: '', prev: '', next: '' }, options); // Get matching nav containers. var navContainers = Object(_helpers_getElements__WEBPACK_IMPORTED_MODULE_6__["default"])(options.container); // Don't add navs without container elements. if (!navContainers.length) { return false; } navContainers.forEach(function (navContainer) { // Set a data attribute assigning the nav to this slider. navContainer.setAttribute('data-snap-slider-nav', _this8.id); // Get button selectors from JavaScript, data attribute, or default to 'button'. // NOTE: In this case, allow the nav's data attribute to override the parent // container's options. var buttons = navContainer.getAttribute('data-snap-slider-buttons') || options.buttons || 'button'; var prev = options.prev || navContainer.getAttribute('data-snap-slider-prev'); var next = options.next || navContainer.getAttribute('data-snap-slider-next'); // And add them. _this8.addGotoButtons({ container: navContainer, buttons: buttons, prev: prev, next: next }); }); return true; } /** * Which slide is closest to its active offset position? * * Returns an object include the slide's index, element, * and the diff between its active offset and our * current scroll position. * * @return {Object} { index, slide, diff } */ }, { key: "getClosest", value: function getClosest() { var _this9 = this; return this.slides.reduce(function (prev, slide, index) { // 1-index to stay consistent with our API. index += 1; // How far away are we from the next slide's active offset position? var offset = _this9.getScrollOffset(slide); var diff = { top: Math.abs(_this9.container.scrollTop - offset.top), left: Math.abs(_this9.container.scrollLeft - offset.left) }; // Save the next slide's info to compare with other slides. var next = { index: index, slide: slide, diff: diff }; // If this is the first slide, return it and compare the next one. if (!prev) { return next; } // Compare each slide to see which one is the closest to its active offset position. // As soon as the next slide is at least as close as the previous one, return it. if (next.diff.left <= prev.diff.left && next.diff.top <= prev.diff.top) { return next; } // Otherwise, keep the last closest slide. return prev; // Init with `false` so the first slide gets processed just like the rest of them. }, false); } /** * Watch the container scroll for when the current slide changes. * * @return {void} */ }, { key: "watchForChanges", value: function watchForChanges() { var _this10 = this; // Scroll listener. Save so we can remove it during `destroy()`. this.scrollListener = lodash_throttle__WEBPACK_IMPORTED_MODULE_4___default()(function (event) { // Which slide is closest to their active offset position? var closest = _this10.getClosest(); // If someone's passively scrolling (i.e., not in a transition), // then as soon as we've scrolled to another slide, mark that // slide as the new current one and fire a change event. if (!_this10.transition && closest.index !== _this10.current) { _this10.current = closest.index; _this10.fireEvent('change', event); } // If we just started scrolling, update state and // fire a `scroll.start` event. if (!_this10.scrolling) { _this10.scrolling = true; _this10.fireEvent('scroll.start', event); } // Fire a generic `scroll` event. _this10.fireEvent('scroll', event); }, 250); // Scroll end listener. Save so we can remove it during `destroy()`. this.scrollEndListener = lodash_debounce__WEBPACK_IMPORTED_MODULE_3___default()(function (event) { // We're done scrolling! _this10.scrolling = false; _this10.fireEvent('scroll.end', event); // Clear any previous transition checks. // NOTE: This has to happen *after* we fire the `scroll.end` event, // otherwise `handleFocus` won't be able to access `this.transition`. _this10.stopTransition(); }, 250); // Arrow key listener. Save so we can remove it during `destroy()`. this.arrowKeyListener = lodash_throttle__WEBPACK_IMPORTED_MODULE_4___default()(function (event) { // Ignore events that have already been prevented. if (event.defaultPrevented) { return; } // Listen for arrow keys. // @see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key var isPrev = ['Up', 'ArrowUp', 'Left', 'ArrowLeft'].indexOf(event.key) >= 0; var isNext = ['Down', 'ArrowDown', 'Right', 'ArrowRight'].indexOf(event.key) >= 0; // Ignore non-arrow keys. if (!isPrev && !isNext) { return; } // Go to the next or previous slide. _this10["goto"](isNext ? 'next' : 'prev', null, event); // Prevent default browser scroll. event.preventDefault(); }, 250); // Focus listener. Save so we can remove it during `destroy()`. this.focusListener = function (event) { // Only trigger `goto` on focus when we're not passively scrolling. // However, if someone manually triggered a transition then // allow them to click or tab away to a different slide. if (_this10.scrolling && !_this10.transition) { return; } // Get slide + index. var slide; var index; _this10.slides.forEach(function (s, i) { if (s.contains(event.target)) { slide = s; index = i + 1; } }, null); // If there's a matching slide, go to it. if (slide) { _this10["goto"](index, null, event); } }; // Resize Observer. Save so we can disconnect it during `destroy()`. // Only init if browser supports it, else fallback to noop. this.resizeObserver = { observe: function observe() {}, disconnect: function disconnect() {} }; if ('ResizeObserver' in window) { this.resizeObserver = new ResizeObserver(this.resizeCallback.bind(this)); } // Add all our listeners. // Set timeout to avoid initial `goto` event triggering a scroll listener. setTimeout(function () { _this10.container.addEventListener('scroll', _this10.scrollListener, _helpers_passive__WEBPACK_IMPORTED_MODULE_13__["default"]); _this10.container.addEventListener('scroll', _this10.scrollEndListener, _helpers_passive__WEBPACK_IMPORTED_MODULE_13__["default"]); _this10.container.addEventListener('keydown', _this10.arrowKeyListener); _this10.container.addEventListener('focusin', _this10.focusListener); _this10.resizeObserver.observe(_this10.container); // Done loading! _this10.fireEvent('load'); }, 100); } /** * Update the slider on load. * * @return {void} */ }, { key: "hasLoaded", value: function hasLoaded() { this.container.classList.add('has-loaded'); } /** * Update this slider (e.g., on resize). Basically just repositions the * current slide. * * @return {void} */ }, { key: "update", value: function update() { // Make sure we're still on the current slide. this["goto"](this.current, { focus: false, force: true, ignoreCallbacks: true, immediate: true }); } /** * Destroy this slider. Stop any active transitions, remove its event * listeners, and delete it from our internal array of slider instances. * * @return {void} */ }, { key: "destroy", value: function destroy() { // Stop running transitions, event listeners, etc. this.stopTransition(); this.container.removeEventListener('scroll', this.scrollListener); this.container.removeEventListener('scroll', this.scrollEndListener); this.container.removeEventListener('keydown', this.arrowKeyListener); this.resizeObserver.disconnect(); // Reset callbacks. // eslint-disable-next-line no-restricted-syntax for (var eventName in this.callbacks) { if (Object(_helpers_hasOwnProperty__WEBPACK_IMPORTED_MODULE_8__["default"])(this.callbacks, eventName)) { this.callbacks[eventName] = []; } } // Remove references to this slider. delete this.container.SnapSlider; delete window._SnapSliders[this.id]; } /** * Reset this slider (e.g., after adding or removing a slide). * * See `init()` for a full breakdown of `options`. * * @param {Object} options * @return {void} */ }, { key: "reset", value: function reset() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; // Copy initial options. var initialOptions = this.options; // Remove initial callbacks to avoid duplicating them. delete initialOptions.on; // Don't let people reset critical options during reset (e.g., slider ID). delete options.container; delete options.id; // Re-initialize this slider with initial options + overrides. this.init(this.container, _objectSpread(_objectSpread({}, initialOptions), options)); } /** * Handle resize observer events. * * @return {void} */ }, { key: "resizeCallback", value: function resizeCallback() { this.update(); } /** * When an event happens, fire all the callback functions for that event. * * @param {String} eventName * @param {Event} event * @param {Object} options * @param {Boolean} options.focus * @param {Boolean} options.ignoreCallbacks * @return {void} */ }, { key: "fireEvent", value: function fireEvent(eventName, event) { var _this11 = this; var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; // Ignore invalid events. if (!Object(_helpers_hasOwnProperty__WEBPACK_IMPORTED_MODULE_8__["default"])(this.callbacks, eventName)) { return; } // Fill default options. options = _objectSpread({ // By default, focus the slide we're going to. focus: true, // Ignore custom callbacks for events. ignoreCallbacks: false }, options); // Required: Update slider attributes on load. if (eventName === 'load') { this.hasLoaded(); } // Required: Update buttons and slides on every change. if (eventName === 'change') { this.updateButtons(); this.updateSlides(); } // Allow focus events to be ignored. if (options.focus) { this.handleFocus(eventName, event); } // Allow callbacks to be ignored. if (options.ignoreCallbacks) { return; } // Fallback object for `null` events. event = event || {}; // Include more granular event types for easie