@tannerhodges/snap-slider
Version:
Simple JavaScript plugin to manage sliders using CSS Scroll Snap.
1,394 lines (1,139 loc) • 103 kB
JavaScript
(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 = false ? 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 (false) {}
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 easier callbacks.
var events = [eventName];
if (Object(_helpers_hasOwnProperty__WEBPACK_IMPORTED_MODULE_8__["default"])(this.callbacks, "".concat(eventName, ".").concat(event.type))) {
events.push("".concat(eventName, ".").concat(event.type));
} // Fire all the callbacks for each event.
events.forEach(function (name) {
_this11.callbacks[name].forEach(function (callback) {
if (typeof