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})));