all repos — NoPaste @ edb00b0ab136a96023b51a3e1c59d21a807d66a2

Resurrected - The PussTheCat.org fork of NoPaste

scripts/micromodal.js (view raw)

  1(function (global, factory) {
  2  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  3  typeof define === 'function' && define.amd ? define(factory) :
  4  (global = global || self, global.MicroModal = factory());
  5}(this, (function () { 'use strict';
  6
  7  function _classCallCheck(instance, Constructor) {
  8    if (!(instance instanceof Constructor)) {
  9      throw new TypeError("Cannot call a class as a function");
 10    }
 11  }
 12
 13  function _defineProperties(target, props) {
 14    for (var i = 0; i < props.length; i++) {
 15      var descriptor = props[i];
 16      descriptor.enumerable = descriptor.enumerable || false;
 17      descriptor.configurable = true;
 18      if ("value" in descriptor) descriptor.writable = true;
 19      Object.defineProperty(target, descriptor.key, descriptor);
 20    }
 21  }
 22
 23  function _createClass(Constructor, protoProps, staticProps) {
 24    if (protoProps) _defineProperties(Constructor.prototype, protoProps);
 25    if (staticProps) _defineProperties(Constructor, staticProps);
 26    return Constructor;
 27  }
 28
 29  function _toConsumableArray(arr) {
 30    return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread();
 31  }
 32
 33  function _arrayWithoutHoles(arr) {
 34    if (Array.isArray(arr)) return _arrayLikeToArray(arr);
 35  }
 36
 37  function _iterableToArray(iter) {
 38    if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter);
 39  }
 40
 41  function _unsupportedIterableToArray(o, minLen) {
 42    if (!o) return;
 43    if (typeof o === "string") return _arrayLikeToArray(o, minLen);
 44    var n = Object.prototype.toString.call(o).slice(8, -1);
 45    if (n === "Object" && o.constructor) n = o.constructor.name;
 46    if (n === "Map" || n === "Set") return Array.from(n);
 47    if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
 48  }
 49
 50  function _arrayLikeToArray(arr, len) {
 51    if (len == null || len > arr.length) len = arr.length;
 52
 53    for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];
 54
 55    return arr2;
 56  }
 57
 58  function _nonIterableSpread() {
 59    throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
 60  }
 61
 62  var MicroModal = function () {
 63
 64    var FOCUSABLE_ELEMENTS = ['a[href]', 'area[href]', 'input:not([disabled]):not([type="hidden"]):not([aria-hidden])', 'select:not([disabled]):not([aria-hidden])', 'textarea:not([disabled]):not([aria-hidden])', 'button:not([disabled]):not([aria-hidden])', 'iframe', 'object', 'embed', '[contenteditable]', '[tabindex]:not([tabindex^="-"])'];
 65
 66    var Modal = /*#__PURE__*/function () {
 67      function Modal(_ref) {
 68        var targetModal = _ref.targetModal,
 69            _ref$triggers = _ref.triggers,
 70            triggers = _ref$triggers === void 0 ? [] : _ref$triggers,
 71            _ref$onShow = _ref.onShow,
 72            onShow = _ref$onShow === void 0 ? function () {} : _ref$onShow,
 73            _ref$onClose = _ref.onClose,
 74            onClose = _ref$onClose === void 0 ? function () {} : _ref$onClose,
 75            _ref$openTrigger = _ref.openTrigger,
 76            openTrigger = _ref$openTrigger === void 0 ? 'data-micromodal-trigger' : _ref$openTrigger,
 77            _ref$closeTrigger = _ref.closeTrigger,
 78            closeTrigger = _ref$closeTrigger === void 0 ? 'data-micromodal-close' : _ref$closeTrigger,
 79            _ref$openClass = _ref.openClass,
 80            openClass = _ref$openClass === void 0 ? 'is-open' : _ref$openClass,
 81            _ref$disableScroll = _ref.disableScroll,
 82            disableScroll = _ref$disableScroll === void 0 ? false : _ref$disableScroll,
 83            _ref$disableFocus = _ref.disableFocus,
 84            disableFocus = _ref$disableFocus === void 0 ? false : _ref$disableFocus,
 85            _ref$awaitCloseAnimat = _ref.awaitCloseAnimation,
 86            awaitCloseAnimation = _ref$awaitCloseAnimat === void 0 ? false : _ref$awaitCloseAnimat,
 87            _ref$awaitOpenAnimati = _ref.awaitOpenAnimation,
 88            awaitOpenAnimation = _ref$awaitOpenAnimati === void 0 ? false : _ref$awaitOpenAnimati,
 89            _ref$debugMode = _ref.debugMode,
 90            debugMode = _ref$debugMode === void 0 ? false : _ref$debugMode;
 91
 92        _classCallCheck(this, Modal);
 93
 94        // Save a reference of the modal
 95        this.modal = document.getElementById(targetModal); // Save a reference to the passed config
 96
 97        this.config = {
 98          debugMode: debugMode,
 99          disableScroll: disableScroll,
100          openTrigger: openTrigger,
101          closeTrigger: closeTrigger,
102          openClass: openClass,
103          onShow: onShow,
104          onClose: onClose,
105          awaitCloseAnimation: awaitCloseAnimation,
106          awaitOpenAnimation: awaitOpenAnimation,
107          disableFocus: disableFocus
108        }; // Register click events only if pre binding eventListeners
109
110        if (triggers.length > 0) this.registerTriggers.apply(this, _toConsumableArray(triggers)); // pre bind functions for event listeners
111
112        this.onClick = this.onClick.bind(this);
113        this.onKeydown = this.onKeydown.bind(this);
114      }
115      /**
116       * Loops through all openTriggers and binds click event
117       * @param  {array} triggers [Array of node elements]
118       * @return {void}
119       */
120
121
122      _createClass(Modal, [{
123        key: "registerTriggers",
124        value: function registerTriggers() {
125          var _this = this;
126
127          for (var _len = arguments.length, triggers = new Array(_len), _key = 0; _key < _len; _key++) {
128            triggers[_key] = arguments[_key];
129          }
130
131          triggers.filter(Boolean).forEach(function (trigger) {
132            trigger.addEventListener('click', function (event) {
133              return _this.showModal(event);
134            });
135          });
136        }
137      }, {
138        key: "showModal",
139        value: function showModal() {
140          var _this2 = this;
141
142          var event = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
143          this.activeElement = document.activeElement;
144          this.modal.setAttribute('aria-hidden', 'false');
145          this.modal.classList.add(this.config.openClass);
146          this.scrollBehaviour('disable');
147          this.addEventListeners();
148
149          if (this.config.awaitOpenAnimation) {
150            var handler = function handler() {
151              _this2.modal.removeEventListener('animationend', handler, false);
152
153              _this2.setFocusToFirstNode();
154            };
155
156            this.modal.addEventListener('animationend', handler, false);
157          } else {
158            this.setFocusToFirstNode();
159          }
160
161          this.config.onShow(this.modal, this.activeElement, event);
162        }
163      }, {
164        key: "closeModal",
165        value: function closeModal() {
166          var event = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
167          var modal = this.modal;
168          this.modal.setAttribute('aria-hidden', 'true');
169          this.removeEventListeners();
170          this.scrollBehaviour('enable');
171
172          if (this.activeElement && this.activeElement.focus) {
173            this.activeElement.focus();
174          }
175
176          this.config.onClose(this.modal, this.activeElement, event);
177
178          if (this.config.awaitCloseAnimation) {
179            var openClass = this.config.openClass; // <- old school ftw
180
181            this.modal.addEventListener('animationend', function handler() {
182              modal.classList.remove(openClass);
183              modal.removeEventListener('animationend', handler, false);
184            }, false);
185          } else {
186            modal.classList.remove(this.config.openClass);
187          }
188        }
189      }, {
190        key: "closeModalById",
191        value: function closeModalById(targetModal) {
192          this.modal = document.getElementById(targetModal);
193          if (this.modal) this.closeModal();
194        }
195      }, {
196        key: "scrollBehaviour",
197        value: function scrollBehaviour(toggle) {
198          if (!this.config.disableScroll) return;
199          var body = document.querySelector('body');
200
201          switch (toggle) {
202            case 'enable':
203              Object.assign(body.style, {
204                overflow: ''
205              });
206              break;
207
208            case 'disable':
209              Object.assign(body.style, {
210                overflow: 'hidden'
211              });
212              break;
213          }
214        }
215      }, {
216        key: "addEventListeners",
217        value: function addEventListeners() {
218          this.modal.addEventListener('touchstart', this.onClick);
219          this.modal.addEventListener('click', this.onClick);
220          document.addEventListener('keydown', this.onKeydown);
221        }
222      }, {
223        key: "removeEventListeners",
224        value: function removeEventListeners() {
225          this.modal.removeEventListener('touchstart', this.onClick);
226          this.modal.removeEventListener('click', this.onClick);
227          document.removeEventListener('keydown', this.onKeydown);
228        }
229      }, {
230        key: "onClick",
231        value: function onClick(event) {
232          if (event.target.hasAttribute(this.config.closeTrigger)) {
233            this.closeModal(event);
234          }
235        }
236      }, {
237        key: "onKeydown",
238        value: function onKeydown(event) {
239          if (event.keyCode === 27) this.closeModal(event); // esc
240
241          if (event.keyCode === 9) this.retainFocus(event); // tab
242        }
243      }, {
244        key: "getFocusableNodes",
245        value: function getFocusableNodes() {
246          var nodes = this.modal.querySelectorAll(FOCUSABLE_ELEMENTS);
247          return Array.apply(void 0, _toConsumableArray(nodes));
248        }
249        /**
250         * Tries to set focus on a node which is not a close trigger
251         * if no other nodes exist then focuses on first close trigger
252         */
253
254      }, {
255        key: "setFocusToFirstNode",
256        value: function setFocusToFirstNode() {
257          var _this3 = this;
258
259          if (this.config.disableFocus) return;
260          var focusableNodes = this.getFocusableNodes(); // no focusable nodes
261
262          if (focusableNodes.length === 0) return; // remove nodes on whose click, the modal closes
263          // could not think of a better name :(
264
265          var nodesWhichAreNotCloseTargets = focusableNodes.filter(function (node) {
266            return !node.hasAttribute(_this3.config.closeTrigger);
267          });
268          if (nodesWhichAreNotCloseTargets.length > 0) nodesWhichAreNotCloseTargets[0].focus();
269          if (nodesWhichAreNotCloseTargets.length === 0) focusableNodes[0].focus();
270        }
271      }, {
272        key: "retainFocus",
273        value: function retainFocus(event) {
274          var focusableNodes = this.getFocusableNodes(); // no focusable nodes
275
276          if (focusableNodes.length === 0) return;
277          /**
278           * Filters nodes which are hidden to prevent
279           * focus leak outside modal
280           */
281
282          focusableNodes = focusableNodes.filter(function (node) {
283            return node.offsetParent !== null;
284          }); // if disableFocus is true
285
286          if (!this.modal.contains(document.activeElement)) {
287            focusableNodes[0].focus();
288          } else {
289            var focusedItemIndex = focusableNodes.indexOf(document.activeElement);
290
291            if (event.shiftKey && focusedItemIndex === 0) {
292              focusableNodes[focusableNodes.length - 1].focus();
293              event.preventDefault();
294            }
295
296            if (!event.shiftKey && focusableNodes.length > 0 && focusedItemIndex === focusableNodes.length - 1) {
297              focusableNodes[0].focus();
298              event.preventDefault();
299            }
300          }
301        }
302      }]);
303
304      return Modal;
305    }();
306    /**
307     * Modal prototype ends.
308     * Here on code is responsible for detecting and
309     * auto binding event handlers on modal triggers
310     */
311    // Keep a reference to the opened modal
312
313
314    var activeModal = null;
315    /**
316     * Generates an associative array of modals and it's
317     * respective triggers
318     * @param  {array} triggers     An array of all triggers
319     * @param  {string} triggerAttr The data-attribute which triggers the module
320     * @return {array}
321     */
322
323    var generateTriggerMap = function generateTriggerMap(triggers, triggerAttr) {
324      var triggerMap = [];
325      triggers.forEach(function (trigger) {
326        var targetModal = trigger.attributes[triggerAttr].value;
327        if (triggerMap[targetModal] === undefined) triggerMap[targetModal] = [];
328        triggerMap[targetModal].push(trigger);
329      });
330      return triggerMap;
331    };
332    /**
333     * Validates whether a modal of the given id exists
334     * in the DOM
335     * @param  {number} id  The id of the modal
336     * @return {boolean}
337     */
338
339
340    var validateModalPresence = function validateModalPresence(id) {
341      if (!document.getElementById(id)) {
342        console.warn("MicroModal: \u2757Seems like you have missed %c'".concat(id, "'"), 'background-color: #f8f9fa;color: #50596c;font-weight: bold;', 'ID somewhere in your code. Refer example below to resolve it.');
343        console.warn("%cExample:", 'background-color: #f8f9fa;color: #50596c;font-weight: bold;', "<div class=\"modal\" id=\"".concat(id, "\"></div>"));
344        return false;
345      }
346    };
347    /**
348     * Validates if there are modal triggers present
349     * in the DOM
350     * @param  {array} triggers An array of data-triggers
351     * @return {boolean}
352     */
353
354
355    var validateTriggerPresence = function validateTriggerPresence(triggers) {
356      if (triggers.length <= 0) {
357        console.warn("MicroModal: \u2757Please specify at least one %c'micromodal-trigger'", 'background-color: #f8f9fa;color: #50596c;font-weight: bold;', 'data attribute.');
358        console.warn("%cExample:", 'background-color: #f8f9fa;color: #50596c;font-weight: bold;', "<a href=\"#\" data-micromodal-trigger=\"my-modal\"></a>");
359        return false;
360      }
361    };
362    /**
363     * Checks if triggers and their corresponding modals
364     * are present in the DOM
365     * @param  {array} triggers   Array of DOM nodes which have data-triggers
366     * @param  {array} triggerMap Associative array of modals and their triggers
367     * @return {boolean}
368     */
369
370
371    var validateArgs = function validateArgs(triggers, triggerMap) {
372      validateTriggerPresence(triggers);
373      if (!triggerMap) return true;
374
375      for (var id in triggerMap) {
376        validateModalPresence(id);
377      }
378
379      return true;
380    };
381    /**
382     * Binds click handlers to all modal triggers
383     * @param  {object} config [description]
384     * @return void
385     */
386
387
388    var init = function init(config) {
389      // Create an config object with default openTrigger
390      var options = Object.assign({}, {
391        openTrigger: 'data-micromodal-trigger'
392      }, config); // Collects all the nodes with the trigger
393
394      var triggers = _toConsumableArray(document.querySelectorAll("[".concat(options.openTrigger, "]"))); // Makes a mappings of modals with their trigger nodes
395
396
397      var triggerMap = generateTriggerMap(triggers, options.openTrigger); // Checks if modals and triggers exist in dom
398
399      if (options.debugMode === true && validateArgs(triggers, triggerMap) === false) return; // For every target modal creates a new instance
400
401      for (var key in triggerMap) {
402        var value = triggerMap[key];
403        options.targetModal = key;
404        options.triggers = _toConsumableArray(value);
405        activeModal = new Modal(options); // eslint-disable-line no-new
406      }
407    };
408    /**
409     * Shows a particular modal
410     * @param  {string} targetModal [The id of the modal to display]
411     * @param  {object} config [The configuration object to pass]
412     * @return {void}
413     */
414
415
416    var show = function show(targetModal, config) {
417      var options = config || {};
418      options.targetModal = targetModal; // Checks if modals and triggers exist in dom
419
420      if (options.debugMode === true && validateModalPresence(targetModal) === false) return; // clear events in case previous modal wasn't close
421
422      if (activeModal) activeModal.removeEventListeners(); // stores reference to active modal
423
424      activeModal = new Modal(options); // eslint-disable-line no-new
425
426      activeModal.showModal();
427    };
428    /**
429     * Closes the active modal
430     * @param  {string} targetModal [The id of the modal to close]
431     * @return {void}
432     */
433
434
435    var close = function close(targetModal) {
436      targetModal ? activeModal.closeModalById(targetModal) : activeModal.closeModal();
437    };
438
439    return {
440      init: init,
441      show: show,
442      close: close
443    };
444  }();
445  window.MicroModal = MicroModal;
446
447  return MicroModal;
448
449})));