vrembem 1.41.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -56,10 +56,60 @@ const camelCase = str => {
56
56
  });
57
57
  };
58
58
 
59
+ class Collection {
60
+ constructor() {
61
+ this.collection = [];
62
+ }
63
+
64
+ async register(item) {
65
+ await this.deregister(item);
66
+ this.collection.push(item);
67
+ return this.collection;
68
+ }
69
+
70
+ async deregister(ref) {
71
+ const index = this.collection.findIndex(entry => {
72
+ return entry === ref;
73
+ });
74
+
75
+ if (index >= 0) {
76
+ const entry = this.collection[index];
77
+ Object.getOwnPropertyNames(entry).forEach(prop => {
78
+ delete entry[prop];
79
+ });
80
+ this.collection.splice(index, 1);
81
+ }
82
+
83
+ return this.collection;
84
+ }
85
+
86
+ async registerCollection(items) {
87
+ await Promise.all(Array.from(items, item => {
88
+ this.register(item);
89
+ }));
90
+ return this.collection;
91
+ }
92
+
93
+ async deregisterCollection() {
94
+ while (this.collection.length > 0) {
95
+ await this.deregister(this.collection[0]);
96
+ }
97
+
98
+ return this.collection;
99
+ }
100
+
101
+ get(value, key = 'id') {
102
+ return this.collection.find(item => {
103
+ return item[key] === value;
104
+ });
105
+ }
106
+
107
+ }
108
+
59
109
  var focusableSelectors = ['a[href]:not([tabindex^="-"])', 'area[href]:not([tabindex^="-"])', 'input:not([type="hidden"]):not([type="radio"]):not([disabled]):not([tabindex^="-"])', 'input[type="radio"]:not([disabled]):not([tabindex^="-"])', 'select:not([disabled]):not([tabindex^="-"])', 'textarea:not([disabled]):not([tabindex^="-"])', 'button:not([disabled]):not([tabindex^="-"])', 'iframe:not([tabindex^="-"])', 'audio[controls]:not([tabindex^="-"])', 'video[controls]:not([tabindex^="-"])', '[contenteditable]:not([tabindex^="-"])', '[tabindex]:not([tabindex^="-"])'];
60
110
 
61
111
  const focusTarget = (target, settings) => {
62
- const innerFocus = target.querySelector(`[data-${settings.dataFocus}]`);
112
+ const innerFocus = target.querySelector(settings.selectorFocus);
63
113
 
64
114
  if (innerFocus) {
65
115
  innerFocus.focus();
@@ -68,7 +118,7 @@ const focusTarget = (target, settings) => {
68
118
  if (innerElement) innerElement.focus();
69
119
  }
70
120
  };
71
- const focusTrigger = (obj = null) => {
121
+ const focusTrigger = obj => {
72
122
  if (!obj || !obj.memory || !obj.memory.trigger) return;
73
123
  obj.memory.trigger.focus();
74
124
  obj.memory.trigger = null;
@@ -80,6 +130,7 @@ class FocusTrap {
80
130
  }
81
131
 
82
132
  init(target) {
133
+ this.destroy();
83
134
  this.target = target;
84
135
  this.inner = this.target.querySelector('[tabindex="-1"]');
85
136
  this.focusable = this.getFocusable();
@@ -166,16 +217,6 @@ class FocusTrap {
166
217
 
167
218
  }
168
219
 
169
- /**
170
- * Get an element(s) from a selector or return value if not a string.
171
- * @param {String} selector - Selector to query.
172
- * @param {Boolean} single - Whether to return a single or all matches.
173
- */
174
- const getElement = function getElement(selector, single = 0) {
175
- if (typeof selector !== 'string') return selector;
176
- return single ? document.querySelector(selector) : document.querySelectorAll(selector);
177
- };
178
-
179
220
  /**
180
221
  * Checks an element or NodeList whether they contain a class or classes.
181
222
  * Ref: https://davidwalsh.name/nodelist-array
@@ -204,60 +245,6 @@ const hyphenCase = str => {
204
245
  });
205
246
  };
206
247
 
207
- /**
208
- * Moves element(s) in the DOM based on a reference and move type.
209
- * @param {String} target - The element(s) to move.
210
- * @param {String} type - Move type can be 'after', 'before', 'append' or 'prepend'.
211
- * @param {String} reference - The reference element the move is relative to.
212
- */
213
-
214
- function moveElement(target, type, reference = false) {
215
- if (reference) {
216
- const els = getElement(target);
217
- if (!els.length) throw new Error(`Move target element "${target}" not found!`);
218
- const ref = getElement(reference, 1);
219
- if (!ref) throw new Error(`Move reference element "${reference}" not found!`);
220
- els.forEach(el => {
221
- switch (type) {
222
- case 'after':
223
- ref.after(el);
224
- return {
225
- ref,
226
- el,
227
- type
228
- };
229
-
230
- case 'before':
231
- ref.before(el);
232
- return {
233
- ref,
234
- el,
235
- type
236
- };
237
-
238
- case 'append':
239
- ref.append(el);
240
- return {
241
- ref,
242
- el,
243
- type
244
- };
245
-
246
- case 'prepend':
247
- ref.prepend(el);
248
- return {
249
- ref,
250
- el,
251
- type
252
- };
253
-
254
- default:
255
- throw new Error(`Move type "${type}" does not exist!`);
256
- }
257
- });
258
- }
259
- }
260
-
261
248
  /**
262
249
  * Remove a class or classes from an element or NodeList.
263
250
  * @param {Node || NodeList} el - Element(s) to remove class(es) from.
@@ -270,6 +257,48 @@ const removeClass = (el, ...cl) => {
270
257
  });
271
258
  };
272
259
 
260
+ /**
261
+ * Teleports an element in the DOM based on a reference and teleport method.
262
+ * Provide the comment node as the reference to teleport the element back to its
263
+ * previous location.
264
+ * @param {Node} what - What element to teleport.
265
+ * @param {String || Node} where - Where to teleport the element.
266
+ * @param {String} how - How (method) to teleport the element, e.g: 'after',
267
+ * 'before', 'append' or 'prepend'.
268
+ * @return {Node} Return the return reference if it was teleported else return
269
+ * null if it was returned to a comment reference.
270
+ */
271
+ function teleport(what, where, how) {
272
+ // Check if ref is either a comment or element node.
273
+ const isComment = where.nodeType === Node.COMMENT_NODE;
274
+ const isElement = where.nodeType === Node.ELEMENT_NODE; // Get the reference element.
275
+
276
+ where = isComment || isElement ? where : document.querySelector(where); // If ref is a comment, set teleport type to 'after'.
277
+
278
+ if (isComment) how = 'after'; // Must be a valid reference element and method.
279
+
280
+ if (!where) throw new Error(`Not a valid teleport reference: '${where}'`);
281
+ if (typeof where[how] != 'function') throw new Error(`Not a valid teleport method: '${how}'`); // Initial return ref is null.
282
+
283
+ let returnRef = null; // If ref is not a comment, set a return reference comment.
284
+
285
+ if (!isComment) {
286
+ returnRef = document.createComment('teleported #' + what.id);
287
+ what.before(returnRef);
288
+ } // Teleport the target node.
289
+
290
+
291
+ where[how](what); // Delete the comment node if element was returned to a comment reference.
292
+
293
+ if (isComment) {
294
+ where.remove();
295
+ } // Return the return reference if it was teleported else return null if it was
296
+ // returned to a comment reference.
297
+
298
+
299
+ return returnRef;
300
+ }
301
+
273
302
  /**
274
303
  * Toggle a class or classes on an element or NodeList.
275
304
  * @param {Node || NodeList} el - Element(s) to toggle class(es) on.
@@ -328,14 +357,14 @@ var index = {
328
357
  setTabindex: setTabindex,
329
358
  addClass: addClass,
330
359
  camelCase: camelCase,
360
+ Collection: Collection,
331
361
  focusTarget: focusTarget,
332
362
  focusTrigger: focusTrigger,
333
363
  FocusTrap: FocusTrap,
334
- getElement: getElement,
335
364
  hasClass: hasClass,
336
365
  hyphenCase: hyphenCase,
337
- moveElement: moveElement,
338
366
  removeClass: removeClass,
367
+ teleport: teleport,
339
368
  toggleClass: toggleClass,
340
369
  openTransition: openTransition,
341
370
  closeTransition: closeTransition
@@ -429,7 +458,10 @@ var defaults$2 = {
429
458
  dataOpen: 'drawer-open',
430
459
  dataClose: 'drawer-close',
431
460
  dataBreakpoint: 'drawer-breakpoint',
432
- dataFocus: 'drawer-focus',
461
+ // Selectors
462
+ selectorFocus: '[data-focus]',
463
+ selectorInert: null,
464
+ selectorOverflow: null,
433
465
  // State classes
434
466
  stateOpened: 'is-opened',
435
467
  stateOpening: 'is-opening',
@@ -437,9 +469,6 @@ var defaults$2 = {
437
469
  stateClosed: 'is-closed',
438
470
  // Classes
439
471
  classModal: 'drawer_modal',
440
- // Selectors
441
- selectorInert: null,
442
- selectorOverflow: null,
443
472
  // Feature toggles
444
473
  breakpoints: null,
445
474
  customEventPrefix: 'drawer:',
@@ -573,7 +602,7 @@ async function close$2(drawerKey) {
573
602
  }
574
603
  }
575
604
 
576
- function handlerClick$2(event) {
605
+ function handlerClick$1(event) {
577
606
  // Working catch
578
607
  if (this.working) return; // Toggle data trigger
579
608
 
@@ -622,7 +651,7 @@ function handlerClick$2(event) {
622
651
  return;
623
652
  }
624
653
  }
625
- function handlerKeydown$2(event) {
654
+ function handlerKeydown$1(event) {
626
655
  // Working catch
627
656
  if (this.working) return;
628
657
 
@@ -774,8 +803,8 @@ class Drawer {
774
803
  this.state = {};
775
804
  this.focusTrap = new FocusTrap();
776
805
  this.breakpoint = new Breakpoint(this);
777
- this.__handlerClick = handlerClick$2.bind(this);
778
- this.__handlerKeydown = handlerKeydown$2.bind(this);
806
+ this.__handlerClick = handlerClick$1.bind(this);
807
+ this.__handlerKeydown = handlerKeydown$1.bind(this);
779
808
  if (this.settings.autoInit) this.init();
780
809
  }
781
810
 
@@ -885,400 +914,547 @@ class Drawer {
885
914
  var defaults$1 = {
886
915
  autoInit: false,
887
916
  // Data attributes
888
- dataModal: 'modal',
889
- dataDialog: 'modal-dialog',
890
917
  dataOpen: 'modal-open',
891
918
  dataClose: 'modal-close',
892
- dataFocus: 'modal-focus',
893
- dataRequired: 'modal-required',
919
+ dataReplace: 'modal-replace',
920
+ dataConfig: 'modal-config',
921
+ // Selectors
922
+ selectorModal: '.modal',
923
+ selectorDialog: '.modal__dialog',
924
+ selectorRequired: '[role="alertdialog"]',
925
+ selectorFocus: '[data-focus]',
926
+ selectorInert: null,
927
+ selectorOverflow: 'body',
894
928
  // State classes
895
929
  stateOpened: 'is-opened',
896
930
  stateOpening: 'is-opening',
897
931
  stateClosing: 'is-closing',
898
932
  stateClosed: 'is-closed',
899
- // Selector
900
- selectorInert: null,
901
- selectorOverflow: 'body',
902
- // Feature toggles
933
+ // Feature settings
903
934
  customEventPrefix: 'modal:',
904
935
  eventListeners: true,
905
- moveModals: {
906
- ref: null,
907
- type: null
908
- },
936
+ teleport: null,
937
+ teleportMethod: 'append',
909
938
  setTabindex: true,
910
939
  transition: true
911
940
  };
912
941
 
913
- async function close$1(returnFocus = true) {
914
- const modal = document.querySelector(`[data-${this.settings.dataModal}].${this.settings.stateOpened}`);
942
+ function updateGlobalState() {
943
+ // Set inert state based on if a modal is active.
944
+ setInert(!!this.active, this.settings.selectorInert); // Set overflow state based on if a modal is active.
915
945
 
916
- if (modal) {
917
- this.working = true;
918
- setInert(false, this.settings.selectorInert);
919
- setOverflowHidden(false, this.settings.selectorOverflow);
920
- await closeTransition(modal, this.settings);
921
- if (returnFocus) focusTrigger(this);
946
+ setOverflowHidden(!!this.active, this.settings.selectorOverflow); // Update the z-index of the stack.
947
+
948
+ updateStackIndex(this.stack);
949
+ }
950
+ function updateFocusState() {
951
+ // Check if there's an active modal
952
+ if (this.active) {
953
+ // Set focus and init focus trap on active modal.
954
+ focusTarget(this.active.target, this.settings);
955
+ this.focusTrap.init(this.active.target);
956
+ } else {
957
+ // Set focus to root trigger and destroy focus trap.
958
+ focusTrigger(this);
922
959
  this.focusTrap.destroy();
923
- modal.dispatchEvent(new CustomEvent(this.settings.customEventPrefix + 'closed', {
924
- detail: this,
925
- bubbles: true
926
- }));
927
- this.working = false;
928
- return modal;
960
+ }
961
+ }
962
+ function updateStackIndex(stack) {
963
+ stack.forEach((entry, index) => {
964
+ entry.target.style.zIndex = null;
965
+ const value = getComputedStyle(entry.target)['z-index'];
966
+ entry.target.style.zIndex = parseInt(value) + index + 1;
967
+ });
968
+ }
969
+ function getConfig$1(el) {
970
+ const string = el.getAttribute(`data-${this.settings.dataConfig}`) || '';
971
+ const json = string.replace(/'/g, '"');
972
+ return json ? JSON.parse(json) : {};
973
+ }
974
+ function getModal(query) {
975
+ // Get the entry from collection.
976
+ const entry = typeof query === 'string' ? this.get(query) : this.get(query.id); // Return entry if it was resolved, otherwise throw error.
977
+
978
+ if (entry) {
979
+ return entry;
929
980
  } else {
930
- return modal;
981
+ throw new Error(`Modal not found in collection with id of "${query}".`);
931
982
  }
932
983
  }
984
+ function getModalID(obj) {
985
+ // If it's a string, return the string.
986
+ if (typeof obj === 'string') {
987
+ return obj;
988
+ } // If it's an HTML element.
989
+ else if (typeof obj.hasAttribute === 'function') {
990
+ // If it's a modal open trigger, return data value.
991
+ if (obj.hasAttribute(`data-${this.settings.dataOpen}`)) {
992
+ return obj.getAttribute(`data-${this.settings.dataOpen}`);
993
+ } // If it's a modal close trigger, return data value or false.
994
+ else if (obj.hasAttribute(`data-${this.settings.dataClose}`)) {
995
+ return obj.getAttribute(`data-${this.settings.dataClose}`) || false;
996
+ } // If it's a modal replace trigger, return data value.
997
+ else if (obj.hasAttribute(`data-${this.settings.dataReplace}`)) {
998
+ return obj.getAttribute(`data-${this.settings.dataReplace}`);
999
+ } // If it's a modal target, return the id.
1000
+ else if (obj.closest(this.settings.selectorModal)) {
1001
+ obj = obj.closest(this.settings.selectorModal);
1002
+ return obj.id || false;
1003
+ } // Return false if no id was found.
1004
+ else return false;
1005
+ } // If it has an id property, return its value.
1006
+ else if (obj.id) {
1007
+ return obj.id;
1008
+ } // Return false if no id was found.
1009
+ else return false;
1010
+ }
1011
+ function getModalElements(query) {
1012
+ const id = getModalID.call(this, query);
933
1013
 
934
- async function handlerClick$1(event) {
935
- // Working catch
936
- if (this.working) return; // Trigger click
1014
+ if (id) {
1015
+ const target = document.querySelector(`#${id}`);
1016
+ const dialog = target ? target.querySelector(this.settings.selectorDialog) : null;
937
1017
 
938
- const trigger = event.target.closest(`[data-${this.settings.dataOpen}]`);
1018
+ if (!target && !dialog) {
1019
+ return {
1020
+ error: new Error(`No modal elements found using the ID: "${id}".`)
1021
+ };
1022
+ } else if (!dialog) {
1023
+ return {
1024
+ error: new Error('Modal is missing dialog element.')
1025
+ };
1026
+ } else {
1027
+ return {
1028
+ target,
1029
+ dialog
1030
+ };
1031
+ }
1032
+ } else {
1033
+ return {
1034
+ error: new Error('Could not resolve the modal ID.')
1035
+ };
1036
+ }
1037
+ }
1038
+
1039
+ async function handleClick(event) {
1040
+ // If an open or replace button was clicked, open or replace the modal.
1041
+ let trigger = event.target.closest(`[data-${this.settings.dataOpen}], [data-${this.settings.dataReplace}]`);
939
1042
 
940
1043
  if (trigger) {
941
- event.preventDefault();
942
- const modalKey = trigger.getAttribute(`data-${this.settings.dataOpen}`);
943
- const fromModal = event.target.closest(`[data-${this.settings.dataModal}]`);
944
- if (!fromModal) this.memory.trigger = trigger;
945
- await this.close(!fromModal);
946
- this.open(modalKey);
947
- return;
948
- } // Close click
1044
+ event.preventDefault(); // Save the trigger if it's not coming from inside a modal.
949
1045
 
1046
+ const fromModal = event.target.closest(this.settings.selectorModal);
1047
+ if (!fromModal) this.memory.trigger = trigger; // Get the modal.
950
1048
 
951
- if (event.target.closest(`[data-${this.settings.dataClose}]`)) {
952
- event.preventDefault();
953
- this.close();
954
- return;
955
- } // Root click
1049
+ const modal = this.get(getModalID.call(this, trigger)); // Depending on the button type, either open or replace the modal.
956
1050
 
1051
+ return trigger.matches(`[data-${this.settings.dataOpen}]`) ? modal.open() : modal.replace();
1052
+ } // If a close button was clicked, close the modal.
957
1053
 
958
- if (event.target.hasAttribute(`data-${this.settings.dataModal}`) && !event.target.hasAttribute(`data-${this.settings.dataRequired}`)) {
959
- this.close();
960
- return;
1054
+
1055
+ trigger = event.target.closest(`[data-${this.settings.dataClose}]`);
1056
+
1057
+ if (trigger) {
1058
+ event.preventDefault(); // Get the value of the data attribute.
1059
+
1060
+ const value = trigger.getAttribute(`data-${this.settings.dataClose}`); // Close all if * wildcard is passed, otherwise close a single modal.
1061
+
1062
+ return value === '*' ? this.closeAll() : this.close(value);
1063
+ } // If the modal screen was clicked, close the modal.
1064
+
1065
+
1066
+ if (event.target.matches(this.settings.selectorModal) && !event.target.querySelector(this.settings.selectorRequired)) {
1067
+ return this.close(getModalID.call(this, event.target));
961
1068
  }
962
1069
  }
963
- function handlerKeydown$1(event) {
964
- // Working catch
965
- if (this.working) return;
966
-
1070
+ function handleKeydown(event) {
1071
+ // If escape key was pressed.
967
1072
  if (event.key === 'Escape') {
968
- const target = document.querySelector(`[data-${this.settings.dataModal}].${this.settings.stateOpened}`);
969
-
970
- if (target && !target.hasAttribute(`data-${this.settings.dataRequired}`)) {
971
- this.close();
1073
+ // If a modal is opened and not required, close the modal.
1074
+ if (this.active && !this.active.dialog.matches(this.settings.selectorRequired)) {
1075
+ return this.close();
972
1076
  }
973
1077
  }
974
1078
  }
975
1079
 
976
- function getModal(modalKey) {
977
- if (typeof modalKey !== 'string') return modalKey;
978
- return document.querySelector(`[data-${this.settings.dataModal}="${modalKey}"]`);
979
- }
980
- function modalNotFound(key) {
981
- return Promise.reject(new Error(`Did not find modal with key: "${key}"`));
982
- }
983
- function moveModals(type = this.settings.moveModals.type, ref = this.settings.moveModals.ref) {
984
- const modals = document.querySelectorAll(`[data-${this.settings.dataModal}]`);
985
- if (modals.length) moveElement(modals, type, ref);
1080
+ async function deregister$1(obj, close = true) {
1081
+ // Return collection if nothing was passed.
1082
+ if (!obj) return this.collection; // Check if entry has been registered in the collection.
1083
+
1084
+ const index = this.collection.findIndex(entry => {
1085
+ return entry.id === obj.id;
1086
+ });
1087
+
1088
+ if (index >= 0) {
1089
+ // Get the collection entry.
1090
+ const entry = this.collection[index]; // If entry is in the opened state, close it.
1091
+
1092
+ if (close && entry.state === 'opened') {
1093
+ await entry.close(false);
1094
+ } else {
1095
+ // Get index of modal in stack array.
1096
+ const stackIndex = this.stack.findIndex(item => {
1097
+ return item.id === entry.id;
1098
+ }); // Remove modal from stack array.
1099
+
1100
+ if (stackIndex >= 0) {
1101
+ this.stack.splice(stackIndex, 1);
1102
+ }
1103
+ } // Return teleported modal if a reference has been set.
1104
+
1105
+
1106
+ if (entry.getSetting('teleport')) {
1107
+ entry.teleportReturn();
1108
+ } // Delete properties from collection entry.
1109
+
1110
+
1111
+ Object.getOwnPropertyNames(entry).forEach(prop => {
1112
+ delete entry[prop];
1113
+ }); // Remove entry from collection.
1114
+
1115
+ this.collection.splice(index, 1);
1116
+ } // Return the modified collection.
1117
+
1118
+
1119
+ return this.collection;
986
1120
  }
987
1121
 
988
- function setInitialState() {
989
- const modals = document.querySelectorAll(`[data-${this.settings.dataModal}]`);
990
- modals.forEach(el => {
991
- // Remove opened state setup
992
- if (el.classList.contains(this.settings.stateOpened)) {
993
- setInert(false, this.settings.selectorInert);
994
- setOverflowHidden(false, this.settings.selectorOverflow);
995
- focusTrigger(this);
996
- this.focusTrap.destroy();
997
- } // Remove all state classes and add the default state (closed)
1122
+ async function open$1(query, transition, bulk = false) {
1123
+ // Get the modal from collection.
1124
+ const modal = getModal.call(this, query); // Get the modal configuration.
998
1125
 
1126
+ const config = _extends({}, this.settings, modal.settings); // Add transition parameter to configuration.
999
1127
 
1000
- removeClass(el, this.settings.stateOpened, this.settings.stateOpening, this.settings.stateClosing);
1001
- addClass(el, this.settings.stateClosed);
1002
- });
1128
+
1129
+ if (transition !== undefined) config.transition = transition; // Check if modal is already in the stack.
1130
+
1131
+ const index = this.stack.findIndex(entry => {
1132
+ return entry.id === modal.id;
1133
+ }); // If modal is already open.
1134
+
1135
+ if (index >= 0) {
1136
+ // Remove modal from stack array.
1137
+ this.stack.splice(index, 1); // Move back to end of stack.
1138
+
1139
+ this.stack.push(modal);
1140
+ } // If modal is closed.
1141
+
1142
+
1143
+ if (modal.state === 'closed') {
1144
+ // Update modal state.
1145
+ modal.state = 'opening'; // Apply z-index styles based on stack length.
1146
+
1147
+ modal.target.style.zIndex = null;
1148
+ const value = getComputedStyle(modal.target)['z-index'];
1149
+ modal.target.style.zIndex = parseInt(value) + this.stack.length + 1; // Store modal in stack array.
1150
+
1151
+ this.stack.push(modal); // Run the open transition.
1152
+
1153
+ await openTransition(modal.target, config); // Update modal state.
1154
+
1155
+ modal.state = 'opened';
1156
+ } // Update the focus state if this is not a bulk action.
1157
+
1158
+
1159
+ if (!bulk) {
1160
+ updateFocusState.call(this);
1161
+ } // Dispatch custom opened event.
1162
+
1163
+
1164
+ modal.target.dispatchEvent(new CustomEvent(config.customEventPrefix + 'opened', {
1165
+ detail: this,
1166
+ bubbles: true
1167
+ })); // Return the modal.
1168
+
1169
+ return modal;
1003
1170
  }
1004
1171
 
1005
- async function open$1(modalKey) {
1006
- const modal = getModal.call(this, modalKey);
1007
- if (!modal) return modalNotFound(modalKey);
1172
+ async function close$1(query, transition, bulk = false) {
1173
+ // Get the modal from collection, or top modal in stack if no query is provided.
1174
+ const modal = query ? getModal.call(this, query) : this.active; // If a modal exists and its state is opened.
1008
1175
 
1009
- if (hasClass(modal, this.settings.stateClosed)) {
1010
- this.working = true;
1011
- setOverflowHidden(true, this.settings.selectorOverflow);
1012
- await openTransition(modal, this.settings);
1013
- this.focusTrap.init(modal);
1014
- focusTarget(modal, this.settings);
1015
- setInert(true, this.settings.selectorInert);
1016
- modal.dispatchEvent(new CustomEvent(this.settings.customEventPrefix + 'opened', {
1176
+ if (modal && modal.state === 'opened') {
1177
+ // Update modal state.
1178
+ modal.state = 'closing'; // Get the modal configuration.
1179
+
1180
+ const config = _extends({}, this.settings, modal.settings); // Add transition parameter to configuration.
1181
+
1182
+
1183
+ if (transition !== undefined) config.transition = transition; // Remove focus from active element.
1184
+
1185
+ document.activeElement.blur(); // Run the close transition.
1186
+
1187
+ await closeTransition(modal.target, config); // Remove z-index styles.
1188
+
1189
+ modal.target.style.zIndex = null; // Get index of modal in stack array.
1190
+
1191
+ const index = this.stack.findIndex(entry => {
1192
+ return entry.id === modal.id;
1193
+ }); // Remove modal from stack array.
1194
+
1195
+ this.stack.splice(index, 1); // Update the focus state if this is not a bulk action.
1196
+
1197
+ if (!bulk) {
1198
+ updateFocusState.call(this);
1199
+ } // Update modal state.
1200
+
1201
+
1202
+ modal.state = 'closed'; // Dispatch custom closed event.
1203
+
1204
+ modal.target.dispatchEvent(new CustomEvent(config.customEventPrefix + 'closed', {
1017
1205
  detail: this,
1018
1206
  bubbles: true
1019
1207
  }));
1020
- this.working = false;
1021
- return modal;
1022
- } else {
1023
- return modal;
1024
- }
1025
- }
1208
+ } // Return the modal.
1026
1209
 
1027
- class Modal {
1028
- constructor(options) {
1029
- this.defaults = defaults$1;
1030
- this.settings = _extends({}, this.defaults, options);
1031
- this.working = false;
1032
- this.memory = {};
1033
- this.focusTrap = new FocusTrap();
1034
- this.__handlerClick = handlerClick$1.bind(this);
1035
- this.__handlerKeydown = handlerKeydown$1.bind(this);
1036
- if (this.settings.autoInit) this.init();
1037
- }
1038
1210
 
1039
- init(options = null) {
1040
- if (options) this.settings = _extends({}, this.settings, options);
1041
- this.moveModals();
1042
- this.setInitialState();
1211
+ return modal;
1212
+ }
1043
1213
 
1044
- if (this.settings.setTabindex) {
1045
- this.setTabindex();
1214
+ async function closeAll$1(exclude, transition) {
1215
+ const result = [];
1216
+ await Promise.all(this.stack.map(async modal => {
1217
+ if (exclude && exclude === modal.id) {
1218
+ Promise.resolve();
1219
+ } else {
1220
+ result.push(await close$1.call(this, modal, transition, true));
1046
1221
  }
1047
1222
 
1048
- if (this.settings.eventListeners) {
1049
- this.initEventListeners();
1050
- }
1051
- }
1223
+ modal.trigger = null;
1224
+ }));
1225
+ return result;
1226
+ }
1052
1227
 
1053
- destroy() {
1054
- this.memory = {};
1228
+ async function replace(query, transition) {
1229
+ // Get the modal from collection.
1230
+ const modal = getModal.call(this, query); // Setup results for return.
1055
1231
 
1056
- if (this.settings.eventListeners) {
1057
- this.destroyEventListeners();
1058
- }
1059
- }
1060
- /**
1061
- * Event listeners
1062
- */
1232
+ let resultOpened, resultClosed;
1063
1233
 
1234
+ if (modal.state === 'opened') {
1235
+ // If modal is open, close all modals except for replacement.
1236
+ resultOpened = modal;
1237
+ resultClosed = await closeAll$1.call(this, modal.id, transition);
1238
+ } else {
1239
+ // If modal is closed, close all and open replacement at the same time.
1240
+ resultOpened = open$1.call(this, modal, transition, true);
1241
+ resultClosed = closeAll$1.call(this, false, transition);
1242
+ await Promise.all([resultOpened, resultClosed]);
1243
+ } // Update the focus state.
1064
1244
 
1065
- initEventListeners() {
1066
- document.addEventListener('click', this.__handlerClick, false);
1067
- document.addEventListener('touchend', this.__handlerClick, false);
1068
- document.addEventListener('keydown', this.__handlerKeydown, false);
1069
- }
1070
1245
 
1071
- destroyEventListeners() {
1072
- document.removeEventListener('click', this.__handlerClick, false);
1073
- document.removeEventListener('touchend', this.__handlerClick, false);
1074
- document.removeEventListener('keydown', this.__handlerKeydown, false);
1075
- }
1076
- /**
1077
- * Helpers
1078
- */
1246
+ updateFocusState.call(this); // Return the modals there were opened and closed.
1079
1247
 
1248
+ return {
1249
+ opened: resultOpened,
1250
+ closed: resultClosed
1251
+ };
1252
+ }
1080
1253
 
1081
- getModal(modalKey) {
1082
- return getModal.call(this, modalKey);
1083
- }
1254
+ async function register$1(target, dialog) {
1255
+ // Deregister entry incase it has already been registered.
1256
+ await deregister$1.call(this, target, false); // Save root this for use inside methods API.
1084
1257
 
1085
- setTabindex() {
1086
- return setTabindex(`
1087
- [data-${this.settings.dataModal}]
1088
- [data-${this.settings.dataDialog}]
1089
- `);
1090
- }
1258
+ const root = this; // Setup methods API.
1091
1259
 
1092
- setInitialState() {
1093
- return setInitialState.call(this);
1094
- }
1260
+ const methods = {
1261
+ open(transition) {
1262
+ return open$1.call(root, this, transition);
1263
+ },
1095
1264
 
1096
- moveModals(type, ref) {
1097
- return moveModals.call(this, type, ref);
1098
- }
1099
- /**
1100
- * Change state functionality
1101
- */
1265
+ close(transition) {
1266
+ return close$1.call(root, this, transition);
1267
+ },
1102
1268
 
1269
+ replace(transition) {
1270
+ return replace.call(root, this, transition);
1271
+ },
1103
1272
 
1104
- open(modalKey) {
1105
- return open$1.call(this, modalKey);
1106
- }
1273
+ deregister() {
1274
+ return deregister$1.call(root, this);
1275
+ },
1107
1276
 
1108
- close(returnFocus) {
1109
- return close$1.call(this, returnFocus);
1110
- }
1277
+ teleport(ref = this.getSetting('teleport'), method = this.getSetting('teleportMethod')) {
1278
+ if (!this.returnRef) {
1279
+ this.returnRef = teleport(this.target, ref, method);
1280
+ return this.target;
1281
+ } else {
1282
+ console.error('Element has already been teleported:', this.target);
1283
+ return false;
1284
+ }
1285
+ },
1111
1286
 
1112
- }
1287
+ teleportReturn() {
1288
+ if (this.returnRef) {
1289
+ this.returnRef = teleport(this.target, this.returnRef);
1290
+ return this.target;
1291
+ } else {
1292
+ console.error('No return reference found:', this.target);
1293
+ return false;
1294
+ }
1295
+ },
1113
1296
 
1114
- class Collection {
1115
- constructor() {
1116
- this.collection = [];
1117
- }
1297
+ getSetting(key) {
1298
+ return key in this.settings ? this.settings[key] : root.settings[key];
1299
+ }
1118
1300
 
1119
- register(item) {
1120
- this.deregister(item);
1121
- this.collection.push(item);
1122
- return this.collection;
1123
- }
1301
+ }; // Setup the modal object.
1124
1302
 
1125
- deregister(ref) {
1126
- const index = this.collection.findIndex(entry => {
1127
- return entry === ref;
1128
- });
1303
+ const entry = _extends({
1304
+ id: target.id,
1305
+ state: 'closed',
1306
+ settings: getConfig$1.call(this, target),
1307
+ target: target,
1308
+ dialog: dialog,
1309
+ returnRef: null
1310
+ }, methods); // Set aria-modal attribute to true.
1129
1311
 
1130
- if (index >= 0) {
1131
- const entry = this.collection[index];
1132
- Object.getOwnPropertyNames(entry).forEach(prop => {
1133
- delete entry[prop];
1134
- });
1135
- this.collection.splice(index, 1);
1136
- }
1137
1312
 
1138
- return this.collection;
1139
- }
1313
+ entry.dialog.setAttribute('aria-modal', 'true'); // If a role attribute is not set, set it to "dialog" as the default.
1140
1314
 
1141
- registerCollection(items) {
1142
- items.forEach(item => {
1143
- this.register(item);
1144
- });
1145
- return this.collection;
1146
- }
1315
+ if (!entry.dialog.hasAttribute('role')) {
1316
+ entry.dialog.setAttribute('role', 'dialog');
1317
+ } // Set tabindex="-1" so dialog is focusable via JS or click.
1147
1318
 
1148
- deregisterCollection() {
1149
- while (this.collection.length > 0) {
1150
- this.deregister(this.collection[0]);
1151
- }
1152
1319
 
1153
- return this.collection;
1154
- }
1320
+ if (entry.getSetting('setTabindex')) {
1321
+ entry.dialog.setAttribute('tabindex', '-1');
1322
+ } // Teleport modal if a reference has been set.
1323
+
1324
+
1325
+ if (entry.getSetting('teleport')) {
1326
+ entry.teleport();
1327
+ } // Add entry to collection.
1328
+
1329
+
1330
+ this.collection.push(entry); // Setup initial state.
1331
+
1332
+ if (entry.target.classList.contains(this.settings.stateOpened)) {
1333
+ // Open modal with transitions disabled.
1334
+ entry.open(false);
1335
+ } else {
1336
+ // Remove transition state classes.
1337
+ entry.target.classList.remove(this.settings.stateOpening);
1338
+ entry.target.classList.remove(this.settings.stateClosing); // Add closed state class.
1339
+
1340
+ entry.target.classList.add(this.settings.stateClosed);
1341
+ } // Return the registered entry.
1155
1342
 
1156
- get(query, key = 'id') {
1157
- const result = this.collection.find(item => {
1158
- return item[key] === query;
1159
- });
1160
- return result || null;
1161
- }
1162
1343
 
1344
+ return entry;
1163
1345
  }
1164
1346
 
1165
- var defaults = {
1166
- autoInit: false,
1167
- // Selectors
1168
- selectorPopover: '.popover',
1169
- selectorArrow: '.popover__arrow',
1170
- // State classes
1171
- stateActive: 'is-active',
1172
- // Feature toggles
1173
- eventListeners: true,
1174
- eventType: 'click',
1175
- placement: 'bottom'
1176
- };
1347
+ class Modal extends Collection {
1348
+ constructor(options) {
1349
+ super();
1350
+ this.defaults = defaults$1;
1351
+ this.settings = _extends({}, this.defaults, options);
1352
+ this.memory = {};
1353
+ this.focusTrap = new FocusTrap(); // Setup a proxy for stack array.
1177
1354
 
1178
- function close(popover) {
1179
- // Update state class
1180
- popover.target.classList.remove(this.settings.stateActive); // Update a11y attributes
1355
+ this.stack = new Proxy([], {
1356
+ set: (target, property, value) => {
1357
+ target[property] = value; // Update global state whenever the length property of stack changes.
1181
1358
 
1182
- popover.trigger.setAttribute('aria-expanded', 'false'); // Disable popper event listeners
1359
+ if (property === 'length') {
1360
+ updateGlobalState.call(this);
1361
+ }
1183
1362
 
1184
- popover.popper.setOptions({
1185
- modifiers: [{
1186
- name: 'eventListeners',
1187
- enabled: false
1188
- }]
1189
- }); // Update popover state
1363
+ return true;
1364
+ }
1365
+ });
1366
+ this.__handleClick = handleClick.bind(this);
1367
+ this.__handleKeydown = handleKeydown.bind(this);
1368
+ if (this.settings.autoInit) this.init();
1369
+ }
1370
+
1371
+ get active() {
1372
+ return this.stack[this.stack.length - 1];
1373
+ }
1190
1374
 
1191
- popover.state = 'closed'; // Clear memory if popover trigger matches the one saved in memory
1375
+ async init(options) {
1376
+ // Update settings with passed options.
1377
+ if (options) this.settings = _extends({}, this.settings, options); // Get all the modals.
1192
1378
 
1193
- if (popover.trigger === this.memory.trigger) {
1194
- this.memory.trigger = null;
1195
- } // Return the popover
1379
+ const modals = document.querySelectorAll(this.settings.selectorModal); // Register the collections array with modal instances.
1196
1380
 
1381
+ await this.registerCollection(modals); // If eventListeners are enabled, init event listeners.
1197
1382
 
1198
- return popover;
1199
- }
1200
- function closeAll() {
1201
- this.collection.forEach(popover => {
1202
- if (popover.state === 'opened') {
1203
- popover.close();
1383
+ if (this.settings.eventListeners) {
1384
+ this.initEventListeners();
1204
1385
  }
1205
- }); // Return the collection
1386
+ }
1206
1387
 
1207
- return this.collection;
1208
- }
1209
- function closeCheck(popover) {
1210
- // Only run closeCheck if provided popover is currently open
1211
- if (popover.state != 'opened') return; // Needed to correctly check which element is currently being focused
1388
+ async destroy() {
1389
+ // Clear any stored memory.
1390
+ this.memory = {}; // Remove all entries from the collection.
1212
1391
 
1213
- setTimeout(() => {
1214
- // Check if trigger or target are being hovered
1215
- const isHovered = popover.target.closest(':hover') === popover.target || popover.trigger.closest(':hover') === popover.trigger; // Check if trigger or target are being focused
1392
+ await this.deregisterCollection(); // If eventListeners are enabled, destroy event listeners.
1216
1393
 
1217
- const isFocused = document.activeElement.closest(`#${popover.id}, [aria-controls="${popover.id}"]`); // Close if the trigger and target are not currently hovered or focused
1394
+ if (this.settings.eventListeners) {
1395
+ this.destroyEventListeners();
1396
+ }
1397
+ }
1218
1398
 
1219
- if (!isHovered && !isFocused) {
1220
- popover.close();
1221
- } // Return the popover
1399
+ initEventListeners() {
1400
+ document.addEventListener('click', this.__handleClick, false);
1401
+ document.addEventListener('touchend', this.__handleClick, false);
1402
+ document.addEventListener('keydown', this.__handleKeydown, false);
1403
+ }
1222
1404
 
1405
+ destroyEventListeners() {
1406
+ document.removeEventListener('click', this.__handleClick, false);
1407
+ document.removeEventListener('touchend', this.__handleClick, false);
1408
+ document.removeEventListener('keydown', this.__handleKeydown, false);
1409
+ }
1223
1410
 
1224
- return popover;
1225
- }, 1);
1226
- }
1411
+ register(query) {
1412
+ const els = getModalElements.call(this, query);
1413
+ if (els.error) return Promise.reject(els.error);
1414
+ return register$1.call(this, els.target, els.dialog);
1415
+ }
1227
1416
 
1228
- function handlerClick(popover) {
1229
- if (popover.target.classList.contains(this.settings.stateActive)) {
1230
- popover.close();
1231
- } else {
1232
- this.memory.trigger = popover.trigger;
1233
- popover.open();
1234
- documentClick.call(this, popover);
1417
+ deregister(query) {
1418
+ const modal = this.get(getModalID.call(this, query));
1419
+ return deregister$1.call(this, modal);
1235
1420
  }
1236
- }
1237
- function handlerKeydown(event) {
1238
- switch (event.key) {
1239
- case 'Escape':
1240
- if (this.memory.trigger) {
1241
- this.memory.trigger.focus();
1242
- }
1243
1421
 
1244
- closeAll.call(this);
1245
- return;
1422
+ open(id, transition) {
1423
+ return open$1.call(this, id, transition);
1424
+ }
1246
1425
 
1247
- case 'Tab':
1248
- this.collection.forEach(popover => {
1249
- closeCheck.call(this, popover);
1250
- });
1251
- return;
1426
+ close(id, transition) {
1427
+ return close$1.call(this, id, transition);
1428
+ }
1252
1429
 
1253
- default:
1254
- return;
1430
+ replace(id, transition) {
1431
+ return replace.call(this, id, transition);
1255
1432
  }
1256
- }
1257
- function documentClick(popover) {
1258
- const root = this;
1259
- document.addEventListener('click', function _f(event) {
1260
- // Check if a popover was clicked
1261
- const result = event.target.closest(`#${popover.id}, [aria-controls="${popover.id}"]`);
1262
1433
 
1263
- if (!result) {
1264
- // If it doesn't match and popover is open, close it and remove event listener
1265
- if (popover.target && popover.target.classList.contains(root.settings.stateActive)) {
1266
- popover.close();
1267
- }
1434
+ async closeAll(exclude = false, transition) {
1435
+ const result = await closeAll$1.call(this, exclude, transition);
1436
+ updateFocusState.call(this);
1437
+ return result;
1438
+ }
1268
1439
 
1269
- this.removeEventListener('click', _f);
1270
- } else {
1271
- // If it does match and popover isn't currently active, remove event listener
1272
- if (popover.target && !popover.target.classList.contains(root.settings.stateActive)) {
1273
- this.removeEventListener('click', _f);
1274
- }
1275
- }
1276
- });
1277
1440
  }
1278
1441
 
1442
+ var defaults = {
1443
+ autoInit: false,
1444
+ // Selectors
1445
+ selectorPopover: '.popover',
1446
+ selectorArrow: '.popover__arrow',
1447
+ // State classes
1448
+ stateActive: 'is-active',
1449
+ // Feature settings
1450
+ eventListeners: true,
1451
+ eventType: 'click',
1452
+ placement: 'bottom'
1453
+ };
1454
+
1279
1455
  function getConfig(el, settings) {
1280
- // Get the computed styles of the popover
1281
- const styles = getComputedStyle(el); // Setup the config obj with default values
1456
+ // Get the computed styles of the element.
1457
+ const styles = getComputedStyle(el); // Setup the config obj with default values.
1282
1458
 
1283
1459
  const config = {
1284
1460
  'placement': settings.placement,
@@ -1288,29 +1464,29 @@ function getConfig(el, settings) {
1288
1464
  'flip-padding': 0,
1289
1465
  'arrow-element': settings.selectorArrow,
1290
1466
  'arrow-padding': 0
1291
- }; // Loop through config obj
1467
+ }; // Loop through config obj.
1292
1468
 
1293
1469
  for (const prop in config) {
1294
- // Get the CSS variable property values
1470
+ // Get the CSS variable property values.
1295
1471
  const prefix = getComputedStyle(document.body).getPropertyValue('--vrembem-variable-prefix');
1296
- const value = styles.getPropertyValue(`--${prefix}popover-${prop}`).trim(); // If a value was found, replace the default in config obj
1472
+ const value = styles.getPropertyValue(`--${prefix}popover-${prop}`).trim(); // If a value was found, replace the default in config obj.
1297
1473
 
1298
1474
  if (value) {
1299
1475
  config[prop] = value;
1300
1476
  }
1301
- } // Return the config obj
1477
+ } // Return the config obj.
1302
1478
 
1303
1479
 
1304
1480
  return config;
1305
1481
  }
1306
1482
  function getPadding(value) {
1307
- let padding; // Split the value by spaces if it's a string
1483
+ let padding; // Split the value by spaces if it's a string.
1308
1484
 
1309
- const array = typeof value === 'string' ? value.trim().split(' ') : [value]; // Convert individual values to integers
1485
+ const array = typeof value === 'string' ? value.trim().split(' ') : [value]; // Convert individual values to integers.
1310
1486
 
1311
1487
  array.forEach(function (item, index) {
1312
1488
  array[index] = parseInt(item, 10);
1313
- }); // Build the padding object based on the number of values passed
1489
+ }); // Build the padding object based on the number of values passed.
1314
1490
 
1315
1491
  switch (array.length) {
1316
1492
  case 1:
@@ -1347,7 +1523,7 @@ function getPadding(value) {
1347
1523
  default:
1348
1524
  padding = false;
1349
1525
  break;
1350
- } // Return the padding object
1526
+ } // Return the padding object.
1351
1527
 
1352
1528
 
1353
1529
  return padding;
@@ -1376,43 +1552,59 @@ function getModifiers(options) {
1376
1552
  }
1377
1553
  }];
1378
1554
  }
1555
+ function getPopover(query) {
1556
+ // Get the entry from collection.
1557
+ const entry = typeof query === 'string' ? this.get(query) : this.get(query.id); // Return entry if it was resolved, otherwise throw error.
1558
+
1559
+ if (entry) {
1560
+ return entry;
1561
+ } else {
1562
+ throw new Error(`Popover not found in collection with id of "${query}".`);
1563
+ }
1564
+ }
1379
1565
  function getPopoverID(obj) {
1380
- // If it's a string
1566
+ // If it's a string, return the string.
1381
1567
  if (typeof obj === 'string') {
1382
1568
  return obj;
1383
- } // If it's an HTML element
1569
+ } // If it's an HTML element.
1384
1570
  else if (typeof obj.hasAttribute === 'function') {
1385
- // If it's a popover trigger
1386
- if (obj.hasAttribute('aria-controls')) {
1387
- return obj.getAttribute('aria-controls');
1388
- } // If it's a popover target
1389
- else if (obj.closest(this.settings.selectorPopover)) {
1571
+ // If it's a popover target, return the id.
1572
+ if (obj.closest(this.settings.selectorPopover)) {
1573
+ obj = obj.closest(this.settings.selectorPopover);
1390
1574
  return obj.id;
1391
- } // Return false if no id was found
1575
+ } // If it's a popover trigger, return value of aria-controls.
1576
+ else if (obj.hasAttribute('aria-controls')) {
1577
+ return obj.getAttribute('aria-controls');
1578
+ } // If it's a popover tooltip trigger, return the value of aria-describedby.
1579
+ else if (obj.hasAttribute('aria-describedby')) {
1580
+ return obj.getAttribute('aria-describedby');
1581
+ } // Return false if no id was found.
1392
1582
  else return false;
1393
- } // If it has an ID property
1583
+ } // If it has an id property, return its value.
1394
1584
  else if (obj.id) {
1395
1585
  return obj.id;
1396
- } // Return false if no id was found
1586
+ } // Return false if no id was found.
1397
1587
  else return false;
1398
1588
  }
1399
1589
  function getPopoverElements(query) {
1400
1590
  const id = getPopoverID.call(this, query);
1401
1591
 
1402
1592
  if (id) {
1403
- const trigger = document.querySelector(`[aria-controls="${id}"]`);
1593
+ const trigger = document.querySelector(`[aria-controls="${id}"]`) || document.querySelector(`[aria-describedby="${id}"]`);
1404
1594
  const target = document.querySelector(`#${id}`);
1405
1595
 
1406
1596
  if (!trigger && !target) {
1407
- console.error('No popover elements found using the provided ID:', id);
1597
+ return {
1598
+ error: new Error(`No popover elements found using the ID: "${id}".`)
1599
+ };
1408
1600
  } else if (!trigger) {
1409
- console.error('No popover trigger associated with the provided popover:', target);
1601
+ return {
1602
+ error: new Error('No popover trigger associated with the provided popover.')
1603
+ };
1410
1604
  } else if (!target) {
1411
- console.error('No popover associated with the provided popover trigger:', trigger);
1412
- }
1413
-
1414
- if (!trigger || !target) {
1415
- return false;
1605
+ return {
1606
+ error: new Error('No popover associated with the provided popover trigger.')
1607
+ };
1416
1608
  } else {
1417
1609
  return {
1418
1610
  trigger,
@@ -1420,10 +1612,120 @@ function getPopoverElements(query) {
1420
1612
  };
1421
1613
  }
1422
1614
  } else {
1423
- console.error('Could not resolve the popover ID:', query);
1424
- return false;
1615
+ return {
1616
+ error: new Error('Could not resolve the popover ID.')
1617
+ };
1618
+ }
1619
+ }
1620
+
1621
+ async function close(query) {
1622
+ // Get the popover from collection.
1623
+ const popover = query ? getPopover.call(this, query) : await closeAll.call(this); // If a modal exists and its state is opened.
1624
+
1625
+ if (popover && popover.state === 'opened') {
1626
+ // Update state class.
1627
+ popover.target.classList.remove(this.settings.stateActive); // Update accessibility attribute(s).
1628
+
1629
+ if (popover.trigger.hasAttribute('aria-controls')) {
1630
+ popover.trigger.setAttribute('aria-expanded', 'false');
1631
+ } // Disable popper event listeners.
1632
+
1633
+
1634
+ popover.popper.setOptions({
1635
+ modifiers: [{
1636
+ name: 'eventListeners',
1637
+ enabled: false
1638
+ }]
1639
+ }); // Update popover state.
1640
+
1641
+ popover.state = 'closed'; // Clear memory if popover trigger matches the one saved in memory.
1642
+
1643
+ if (popover.trigger === this.memory.trigger) {
1644
+ this.memory.trigger = null;
1645
+ }
1646
+ } // Return the popover.
1647
+
1648
+
1649
+ return popover;
1650
+ }
1651
+ async function closeAll() {
1652
+ const result = [];
1653
+ await Promise.all(this.collection.map(async popover => {
1654
+ if (popover.state === 'opened') {
1655
+ result.push(await close.call(this, popover));
1656
+ }
1657
+ }));
1658
+ return result;
1659
+ }
1660
+ function closeCheck(popover) {
1661
+ // Only run closeCheck if provided popover is currently open.
1662
+ if (popover.state != 'opened') return; // Needed to correctly check which element is currently being focused.
1663
+
1664
+ setTimeout(() => {
1665
+ // Check if trigger or target are being hovered.
1666
+ const isHovered = popover.target.closest(':hover') === popover.target || popover.trigger.closest(':hover') === popover.trigger; // Check if trigger or target are being focused.
1667
+
1668
+ const isFocused = document.activeElement.closest(`#${popover.id}, [aria-controls="${popover.id}"]`); // Close if the trigger and target are not currently hovered or focused.
1669
+
1670
+ if (!isHovered && !isFocused) {
1671
+ popover.close();
1672
+ } // Return the popover.
1673
+
1674
+
1675
+ return popover;
1676
+ }, 1);
1677
+ }
1678
+
1679
+ function handlerClick(popover) {
1680
+ if (popover.state === 'opened') {
1681
+ popover.close();
1682
+ } else {
1683
+ this.memory.trigger = popover.trigger;
1684
+ popover.open();
1685
+ documentClick.call(this, popover);
1686
+ }
1687
+ }
1688
+ function handlerKeydown(event) {
1689
+ switch (event.key) {
1690
+ case 'Escape':
1691
+ if (this.memory.trigger) {
1692
+ this.memory.trigger.focus();
1693
+ }
1694
+
1695
+ closeAll.call(this);
1696
+ return;
1697
+
1698
+ case 'Tab':
1699
+ this.collection.forEach(popover => {
1700
+ closeCheck.call(this, popover);
1701
+ });
1702
+ return;
1703
+
1704
+ default:
1705
+ return;
1425
1706
  }
1426
1707
  }
1708
+ function documentClick(popover) {
1709
+ const root = this;
1710
+ document.addEventListener('click', function _f(event) {
1711
+ // Check if a popover was clicked.
1712
+ const result = event.target.closest(`#${popover.id}, [aria-controls="${popover.id}"]`);
1713
+
1714
+ if (!result) {
1715
+ // If it doesn't match and popover is open, close it and remove event listener.
1716
+ if (popover.target && popover.target.classList.contains(root.settings.stateActive)) {
1717
+ popover.close();
1718
+ }
1719
+
1720
+ this.removeEventListener('click', _f);
1721
+ } else {
1722
+ // If it does match and popover isn't currently active, remove event listener.
1723
+ if (popover.target && !popover.target.classList.contains(root.settings.stateActive)) {
1724
+ this.removeEventListener('click', _f);
1725
+ }
1726
+ }
1727
+ });
1728
+ }
1427
1729
 
1428
1730
  var top = 'top';
1429
1731
  var bottom = 'bottom';
@@ -1725,6 +2027,10 @@ function getContainingBlock(element) {
1725
2027
 
1726
2028
  var currentNode = getParentNode(element);
1727
2029
 
2030
+ if (isShadowRoot(currentNode)) {
2031
+ currentNode = currentNode.host;
2032
+ }
2033
+
1728
2034
  while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) {
1729
2035
  var css = getComputedStyle$1(currentNode); // This is non-exhaustive but covers the most common CSS properties that
1730
2036
  // create a containing block.
@@ -1948,7 +2254,7 @@ function mapToStyles(_ref2) {
1948
2254
 
1949
2255
  if (placement === top || (placement === left || placement === right) && variation === end) {
1950
2256
  sideY = bottom;
1951
- var offsetY = isFixed && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing]
2257
+ var offsetY = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing]
1952
2258
  offsetParent[heightProp];
1953
2259
  y -= offsetY - popperRect.height;
1954
2260
  y *= gpuAcceleration ? 1 : -1;
@@ -1956,7 +2262,7 @@ function mapToStyles(_ref2) {
1956
2262
 
1957
2263
  if (placement === left || (placement === top || placement === bottom) && variation === end) {
1958
2264
  sideX = right;
1959
- var offsetX = isFixed && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing]
2265
+ var offsetX = isFixed && offsetParent === win && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing]
1960
2266
  offsetParent[widthProp];
1961
2267
  x -= offsetX - popperRect.width;
1962
2268
  x *= gpuAcceleration ? 1 : -1;
@@ -3208,13 +3514,69 @@ var createPopper = /*#__PURE__*/popperGenerator({
3208
3514
  defaultModifiers: defaultModifiers
3209
3515
  }); // eslint-disable-next-line import/no-unused-modules
3210
3516
 
3211
- function open(popover) {
3212
- // Update state class
3213
- popover.target.classList.add(this.settings.stateActive); // Update a11y attribute
3517
+ async function deregister(obj) {
3518
+ // Return collection if nothing was passed.
3519
+ if (!obj) return this.collection; // Check if entry has been registered in the collection.
3520
+
3521
+ const index = this.collection.findIndex(entry => {
3522
+ return entry.id === obj.id;
3523
+ });
3524
+
3525
+ if (index >= 0) {
3526
+ // Get the collection entry.
3527
+ const entry = this.collection[index]; // If entry is in the opened state, close it.
3528
+
3529
+ if (entry.state === 'opened') {
3530
+ entry.close();
3531
+ } // Clean up the popper instance.
3532
+
3533
+
3534
+ entry.popper.destroy(); // Remove event listeners.
3535
+
3536
+ deregisterEventListeners(entry); // Delete properties from collection entry.
3537
+
3538
+ Object.getOwnPropertyNames(entry).forEach(prop => {
3539
+ delete entry[prop];
3540
+ }); // Remove entry from collection.
3541
+
3542
+ this.collection.splice(index, 1);
3543
+ } // Return the modified collection.
3544
+
3545
+
3546
+ return this.collection;
3547
+ }
3548
+ function deregisterEventListeners(entry) {
3549
+ // If event listeners have been setup.
3550
+ if (entry.__eventListeners) {
3551
+ // Loop through listeners and remove from the appropriate elements.
3552
+ entry.__eventListeners.forEach(evObj => {
3553
+ evObj.el.forEach(el => {
3554
+ evObj.type.forEach(type => {
3555
+ entry[el].removeEventListener(type, evObj.listener, false);
3556
+ });
3557
+ });
3558
+ }); // Remove eventListeners object from collection.
3559
+
3560
+
3561
+ delete entry.__eventListeners;
3562
+ } // Return the entry object.
3563
+
3564
+
3565
+ return entry;
3566
+ }
3567
+
3568
+ async function open(query) {
3569
+ // Get the popover from collection.
3570
+ const popover = getPopover.call(this, query); // Update state class.
3214
3571
 
3215
- popover.trigger.setAttribute('aria-expanded', 'true'); // Update popover config
3572
+ popover.target.classList.add(this.settings.stateActive); // Update accessibility attribute(s).
3216
3573
 
3217
- popover.config = getConfig(popover.target, this.settings); // Enable popper event listeners and set placement/modifiers
3574
+ if (popover.trigger.hasAttribute('aria-controls')) {
3575
+ popover.trigger.setAttribute('aria-expanded', 'true');
3576
+ } // Update popover config.
3577
+
3578
+
3579
+ popover.config = getConfig(popover.target, this.settings); // Enable popper event listeners and set placement/modifiers.
3218
3580
 
3219
3581
  popover.popper.setOptions({
3220
3582
  placement: popover.config['placement'],
@@ -3222,206 +3584,156 @@ function open(popover) {
3222
3584
  name: 'eventListeners',
3223
3585
  enabled: true
3224
3586
  }, ...getModifiers(popover.config)]
3225
- }); // Update popover position
3587
+ }); // Update popover position.
3226
3588
 
3227
- popover.popper.update(); // Update popover state
3589
+ popover.popper.update(); // Update popover state.
3228
3590
 
3229
- popover.state = 'opened'; // Return the popover
3591
+ popover.state = 'opened'; // Return the popover.
3230
3592
 
3231
3593
  return popover;
3232
3594
  }
3233
3595
 
3234
- function register(trigger, target) {
3235
- // Deregister popover if it already exists in the collection
3236
- this.deregister(target.id); // Create popper instance
3596
+ async function register(trigger, target) {
3597
+ // Deregister entry incase it has already been registered.
3598
+ deregister.call(this, target); // Save root this for use inside methods API.
3237
3599
 
3238
- const popperInstance = createPopper(trigger, target); // Save root this for use inside object & create methods API
3600
+ const root = this; // Setup methods API.
3239
3601
 
3240
- const root = this;
3241
3602
  const methods = {
3242
3603
  open() {
3243
- open.call(root, this);
3604
+ return open.call(root, this);
3244
3605
  },
3245
3606
 
3246
3607
  close() {
3247
- close.call(root, this);
3608
+ return close.call(root, this);
3248
3609
  },
3249
3610
 
3250
3611
  deregister() {
3251
- deregister.call(root, this);
3612
+ return deregister.call(root, this);
3252
3613
  }
3253
3614
 
3254
- }; // Build popover object and push to collection array
3615
+ }; // Setup the popover object.
3255
3616
 
3256
- const popover = _extends({
3617
+ const entry = _extends({
3257
3618
  id: target.id,
3258
3619
  state: 'closed',
3259
3620
  trigger: trigger,
3260
3621
  target: target,
3261
- popper: popperInstance,
3622
+ popper: createPopper(trigger, target),
3262
3623
  config: getConfig(target, this.settings)
3263
- }, methods); // Setup event listeners
3264
-
3265
-
3266
- registerEventListeners.call(this, popover); // Set initial state of popover
3267
-
3268
- if (popover.target.classList.contains(this.settings.stateActive)) {
3269
- popover.open();
3270
- documentClick.call(this, popover);
3271
- } else {
3272
- popover.close();
3273
- } // Add item to collection
3274
-
3275
-
3276
- this.collection.push(popover); // Return the popover object
3277
-
3278
- return popover;
3279
- }
3280
- function deregister(popover) {
3281
- // Check if this item has been registered in the collection
3282
- const index = this.collection.findIndex(entry => {
3283
- return entry.id === popover.id;
3284
- }); // If the entry exists in the collection
3624
+ }, methods); // Set aria-expanded to false if trigger has aria-controls attribute.
3285
3625
 
3286
- if (index >= 0) {
3287
- // Get the collection entry
3288
- const entry = this.collection[index]; // Close the collection entry if it's open
3289
-
3290
- if (entry.state === 'opened') {
3291
- entry.close();
3292
- } // Clean up the popper instance
3293
3626
 
3627
+ if (entry.trigger.hasAttribute('aria-controls')) {
3628
+ entry.trigger.setAttribute('aria-expanded', 'false');
3629
+ } // Setup event listeners.
3294
3630
 
3295
- entry.popper.destroy(); // Remove event listeners
3296
3631
 
3297
- deregisterEventListeners(entry); // Delete properties from collection entry
3632
+ registerEventListeners.call(this, entry); // Add entry to collection.
3298
3633
 
3299
- Object.getOwnPropertyNames(entry).forEach(prop => {
3300
- delete entry[prop];
3301
- }); // Remove entry from collection
3634
+ this.collection.push(entry); // Set initial state.
3302
3635
 
3303
- this.collection.splice(index, 1);
3304
- } // Return the new collection
3636
+ if (entry.target.classList.contains(this.settings.stateActive)) {
3637
+ await entry.open();
3638
+ documentClick.call(this, entry);
3639
+ } // Return the registered entry.
3305
3640
 
3306
3641
 
3307
- return this.collection;
3642
+ return entry;
3308
3643
  }
3309
- function registerEventListeners(popover) {
3310
- // If event listeners aren't already setup
3311
- if (!popover.__eventListeners) {
3312
- // Add event listeners based on event type
3313
- const eventType = popover.config['event'];
3644
+ function registerEventListeners(entry) {
3645
+ // If event listeners aren't already setup.
3646
+ if (!entry.__eventListeners) {
3647
+ // Add event listeners based on event type.
3648
+ const eventType = entry.config['event']; // If the event type is hover.
3314
3649
 
3315
3650
  if (eventType === 'hover') {
3316
- // Setup event listeners object for hover
3317
- popover.__eventListeners = [{
3651
+ // Setup event listeners object for hover.
3652
+ entry.__eventListeners = [{
3318
3653
  el: ['trigger'],
3319
3654
  type: ['mouseenter', 'focus'],
3320
- listener: open.bind(this, popover)
3655
+ listener: open.bind(this, entry)
3321
3656
  }, {
3322
3657
  el: ['trigger', 'target'],
3323
3658
  type: ['mouseleave', 'focusout'],
3324
- listener: closeCheck.bind(this, popover)
3325
- }]; // Loop through listeners and apply to appropriate elements
3659
+ listener: closeCheck.bind(this, entry)
3660
+ }]; // Loop through listeners and apply to the appropriate elements.
3326
3661
 
3327
- popover.__eventListeners.forEach(evObj => {
3662
+ entry.__eventListeners.forEach(evObj => {
3328
3663
  evObj.el.forEach(el => {
3329
3664
  evObj.type.forEach(type => {
3330
- popover[el].addEventListener(type, evObj.listener, false);
3665
+ entry[el].addEventListener(type, evObj.listener, false);
3331
3666
  });
3332
3667
  });
3333
3668
  });
3334
- } else {
3335
- // Setup event listeners object for click
3336
- popover.__eventListeners = [{
3669
+ } // Else the event type is click.
3670
+ else {
3671
+ // Setup event listeners object for click.
3672
+ entry.__eventListeners = [{
3337
3673
  el: ['trigger'],
3338
3674
  type: ['click'],
3339
- listener: handlerClick.bind(this, popover)
3340
- }]; // Loop through listeners and apply to appropriate elements
3675
+ listener: handlerClick.bind(this, entry)
3676
+ }]; // Loop through listeners and apply to the appropriate elements.
3341
3677
 
3342
- popover.__eventListeners.forEach(evObj => {
3678
+ entry.__eventListeners.forEach(evObj => {
3343
3679
  evObj.el.forEach(el => {
3344
3680
  evObj.type.forEach(type => {
3345
- popover[el].addEventListener(type, evObj.listener, false);
3681
+ entry[el].addEventListener(type, evObj.listener, false);
3346
3682
  });
3347
3683
  });
3348
3684
  });
3349
3685
  }
3350
- } // Return the popover object
3351
-
3352
-
3353
- return popover;
3354
- }
3355
- function deregisterEventListeners(popover) {
3356
- // If event listeners have been setup
3357
- if (popover.__eventListeners) {
3358
- // Loop through listeners and remove from appropriate elements
3359
- popover.__eventListeners.forEach(evObj => {
3360
- evObj.el.forEach(el => {
3361
- evObj.type.forEach(type => {
3362
- popover[el].removeEventListener(type, evObj.listener, false);
3363
- });
3364
- });
3365
- }); // Remove eventListeners object from collection
3366
-
3686
+ } // Return the entry object.
3367
3687
 
3368
- delete popover.__eventListeners;
3369
- } // Return the popover object
3370
3688
 
3371
-
3372
- return popover;
3689
+ return entry;
3373
3690
  }
3374
3691
 
3375
3692
  class Popover extends Collection {
3376
3693
  constructor(options) {
3377
3694
  super();
3378
3695
  this.defaults = defaults;
3379
- this.settings = _extends({}, this.defaults, options); // this.collection = [];
3380
-
3381
- this.memory = {
3382
- trigger: null
3383
- };
3696
+ this.settings = _extends({}, this.defaults, options);
3697
+ this.memory = {};
3384
3698
  this.__handlerKeydown = handlerKeydown.bind(this);
3385
3699
  if (this.settings.autoInit) this.init();
3386
3700
  }
3387
3701
 
3388
- init(options = null) {
3389
- // Update settings with passed options
3390
- if (options) this.settings = _extends({}, this.settings, options); // Get all the popovers
3702
+ async init(options) {
3703
+ // Update settings with passed options.
3704
+ if (options) this.settings = _extends({}, this.settings, options); // Get all the popovers.
3391
3705
 
3392
- const popovers = document.querySelectorAll(this.settings.selectorPopover); // Build the collections array with popover instances
3706
+ const popovers = document.querySelectorAll(this.settings.selectorPopover); // Register the collections array with popover instances.
3393
3707
 
3394
- this.registerCollection(popovers); // If eventListeners is enabled
3708
+ await this.registerCollection(popovers); // If eventListeners are enabled, init event listeners.
3395
3709
 
3396
3710
  if (this.settings.eventListeners) {
3397
3711
  // Pass false to initEventListeners() since registerCollection()
3398
- // already adds event listeners to popovers
3712
+ // already adds event listeners to popovers.
3399
3713
  this.initEventListeners(false);
3400
3714
  }
3401
3715
  }
3402
3716
 
3403
- destroy() {
3404
- // Deregister all popovers from collection
3405
- this.deregisterCollection(); // If eventListeners is enabled
3717
+ async destroy() {
3718
+ // Clear any stored memory.
3719
+ this.memory = {}; // Remove all entries from the collection.
3720
+
3721
+ await this.deregisterCollection(); // If eventListeners are enabled, destroy event listeners.
3406
3722
 
3407
3723
  if (this.settings.eventListeners) {
3408
3724
  // Pass false to destroyEventListeners() since deregisterCollection()
3409
- // already removes event listeners from popovers
3725
+ // already removes event listeners from popovers.
3410
3726
  this.destroyEventListeners(false);
3411
3727
  }
3412
3728
  }
3413
- /**
3414
- * Event listeners
3415
- */
3416
-
3417
3729
 
3418
3730
  initEventListeners(processCollection = true) {
3419
3731
  if (processCollection) {
3420
- // Loop through collection and setup event listeners
3732
+ // Loop through collection and setup event listeners.
3421
3733
  this.collection.forEach(popover => {
3422
3734
  registerEventListeners.call(this, popover);
3423
3735
  });
3424
- } // Add keydown global event listener
3736
+ } // Add keydown global event listener.
3425
3737
 
3426
3738
 
3427
3739
  document.addEventListener('keydown', this.__handlerKeydown, false);
@@ -3429,50 +3741,33 @@ class Popover extends Collection {
3429
3741
 
3430
3742
  destroyEventListeners(processCollection = true) {
3431
3743
  if (processCollection) {
3432
- // Loop through collection and remove event listeners
3744
+ // Loop through collection and remove event listeners.
3433
3745
  this.collection.forEach(popover => {
3434
3746
  deregisterEventListeners(popover);
3435
3747
  });
3436
- } // Remove keydown global event listener
3748
+ } // Remove keydown global event listener.
3437
3749
 
3438
3750
 
3439
3751
  document.removeEventListener('keydown', this.__handlerKeydown, false);
3440
3752
  }
3441
- /**
3442
- * Register popover functionality
3443
- */
3444
-
3445
3753
 
3446
3754
  register(query) {
3447
3755
  const els = getPopoverElements.call(this, query);
3448
- if (!els) return false;
3756
+ if (els.error) return Promise.reject(els.error);
3449
3757
  return register.call(this, els.trigger, els.target);
3450
3758
  }
3451
3759
 
3452
3760
  deregister(query) {
3453
- const popover = this.get(getPopoverID(query));
3454
- if (!popover) return false;
3761
+ const popover = this.get(getPopoverID.call(this, query));
3455
3762
  return deregister.call(this, popover);
3456
3763
  }
3457
- /**
3458
- * Change state functionality
3459
- */
3460
-
3461
3764
 
3462
3765
  open(id) {
3463
- const popover = this.get(id);
3464
- if (!popover) return false;
3465
- return popover.open();
3766
+ return open.call(this, id);
3466
3767
  }
3467
3768
 
3468
3769
  close(id) {
3469
- if (id) {
3470
- const popover = this.get(id);
3471
- if (!popover) return false;
3472
- return popover.close();
3473
- } else {
3474
- return closeAll.call(this);
3475
- }
3770
+ return close.call(this, id);
3476
3771
  }
3477
3772
 
3478
3773
  }