@materializecss/materialize 2.0.0-alpha → 2.0.2-alpha

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.
Files changed (46) hide show
  1. package/Gruntfile.js +7 -4
  2. package/README.md +24 -12
  3. package/dist/css/materialize.css +90 -86
  4. package/dist/css/materialize.min.css +2 -2
  5. package/dist/js/materialize.js +2869 -2764
  6. package/dist/js/materialize.min.js +2 -2
  7. package/dist/js/materialize.min.js.map +1 -1
  8. package/package.json +1 -1
  9. package/sass/components/_collapsible.scss +0 -41
  10. package/sass/components/_global.scss +3 -2
  11. package/sass/components/_icons-material-design.scss +2 -1
  12. package/sass/components/_navbar.scss +6 -3
  13. package/sass/components/_sidenav.scss +66 -37
  14. package/sass/components/_theme_variables.scss +98 -0
  15. package/sass/components/_typography.scss +2 -2
  16. package/sass/components/forms/_input-fields.scss +4 -10
  17. package/sass/materialize.scss +0 -4
  18. package/src/autocomplete.ts +188 -94
  19. package/src/buttons.ts +225 -260
  20. package/src/cards.ts +5 -6
  21. package/src/carousel.ts +611 -542
  22. package/src/characterCounter.ts +50 -21
  23. package/src/chips.ts +152 -63
  24. package/src/collapsible.ts +97 -32
  25. package/src/component.ts +99 -10
  26. package/src/datepicker.ts +905 -726
  27. package/src/dropdown.ts +576 -484
  28. package/src/edges.ts +4 -4
  29. package/src/forms.ts +17 -14
  30. package/src/global.ts +56 -325
  31. package/src/materialbox.ts +354 -298
  32. package/src/modal.ts +296 -211
  33. package/src/parallax.ts +129 -105
  34. package/src/pushpin.ts +148 -103
  35. package/src/range.ts +166 -150
  36. package/src/scrollspy.ts +214 -174
  37. package/src/select.ts +434 -398
  38. package/src/sidenav.ts +447 -381
  39. package/src/slider.ts +421 -362
  40. package/src/tabs.ts +284 -227
  41. package/src/tapTarget.ts +246 -213
  42. package/src/timepicker.ts +738 -614
  43. package/src/toasts.ts +254 -230
  44. package/src/tooltip.ts +315 -252
  45. package/src/utils.ts +271 -0
  46. package/src/waves.ts +10 -10
package/src/dropdown.ts CHANGED
@@ -1,555 +1,647 @@
1
- import { Component } from "./component";
2
- import { M } from "./global";
3
1
  import anim from "animejs";
4
2
 
5
- const _defaults = {
6
- alignment: 'left',
7
- autoFocus: true,
8
- constrainWidth: true,
9
- container: null,
10
- coverTrigger: true,
11
- closeOnClick: true,
12
- hover: false,
13
- inDuration: 150,
14
- outDuration: 250,
15
- onOpenStart: null,
16
- onOpenEnd: null,
17
- onCloseStart: null,
18
- onCloseEnd: null,
19
- onItemClick: null
20
- };
3
+ import { Utils } from "./utils";
4
+ import { Component, BaseOptions, InitElements, MElement, Openable } from "./component";
5
+
6
+ export interface DropdownOptions extends BaseOptions {
7
+ /**
8
+ * Defines the edge the menu is aligned to.
9
+ * @default 'left'
10
+ */
11
+ alignment: 'left' | 'right';
12
+ /**
13
+ * If true, automatically focus dropdown el for keyboard.
14
+ * @default true
15
+ */
16
+ autoFocus: boolean;
17
+ /**
18
+ * If true, constrainWidth to the size of the dropdown activator.
19
+ * @default true
20
+ */
21
+ constrainWidth: boolean;
22
+ /**
23
+ * Provide an element that will be the bounding container of the dropdown.
24
+ * @default null
25
+ */
26
+ container: Element;
27
+ /**
28
+ * If false, the dropdown will show below the trigger.
29
+ * @default true
30
+ */
31
+ coverTrigger: boolean;
32
+ /**
33
+ * If true, close dropdown on item click.
34
+ * @default true
35
+ */
36
+ closeOnClick: boolean;
37
+ /**
38
+ * If true, the dropdown will open on hover.
39
+ * @default false
40
+ */
41
+ hover: boolean;
42
+ /**
43
+ * The duration of the transition enter in milliseconds.
44
+ * @default 150
45
+ */
46
+ inDuration: number;
47
+ /**
48
+ * The duration of the transition out in milliseconds.
49
+ * @default 250
50
+ */
51
+ outDuration: number;
52
+ /**
53
+ * Function called when dropdown starts entering.
54
+ * @default null
55
+ */
56
+ onOpenStart: (el: HTMLElement) => void;
57
+ /**
58
+ * Function called when dropdown finishes entering.
59
+ * @default null
60
+ */
61
+ onOpenEnd: (el: HTMLElement) => void;
62
+ /**
63
+ * Function called when dropdown starts exiting.
64
+ * @default null
65
+ */
66
+ onCloseStart: (el: HTMLElement) => void;
67
+ /**
68
+ * Function called when dropdown finishes exiting.
69
+ * @default null
70
+ */
71
+ onCloseEnd: (el: HTMLElement) => void;
72
+ /**
73
+ * Function called when item is clicked.
74
+ * @default null
75
+ */
76
+ onItemClick: (el: HTMLLIElement) => void;
77
+ };
78
+
79
+ const _defaults: DropdownOptions = {
80
+ alignment: 'left',
81
+ autoFocus: true,
82
+ constrainWidth: true,
83
+ container: null,
84
+ coverTrigger: true,
85
+ closeOnClick: true,
86
+ hover: false,
87
+ inDuration: 150,
88
+ outDuration: 250,
89
+ onOpenStart: null,
90
+ onOpenEnd: null,
91
+ onCloseStart: null,
92
+ onCloseEnd: null,
93
+ onItemClick: null
94
+ };
95
+
96
+ export class Dropdown extends Component<DropdownOptions> implements Openable {
97
+ static _dropdowns: Dropdown[] = [];
98
+ /** ID of the dropdown element. */
99
+ id: string;
100
+ /** The DOM element of the dropdown. */
101
+ dropdownEl: HTMLElement;
102
+ /** If the dropdown is open. */
103
+ isOpen: boolean;
104
+ /** If the dropdown content is scrollable. */
105
+ isScrollable: boolean;
106
+ isTouchMoving: boolean;
107
+ /** The index of the item focused. */
108
+ focusedIndex: number;
109
+ filterQuery: any[];
110
+ filterTimeout: NodeJS.Timeout;
111
+
112
+ constructor(el: HTMLElement, options: Partial<DropdownOptions>) {
113
+ super(el, options, Dropdown);
114
+ (this.el as any).M_Dropdown = this;
115
+
116
+ Dropdown._dropdowns.push(this);
117
+ this.id = Utils.getIdFromTrigger(el);
118
+ this.dropdownEl = document.getElementById(this.id);
119
+
120
+ this.options = {
121
+ ...Dropdown.defaults,
122
+ ...options
123
+ };
21
124
 
22
- export class Dropdown extends Component {
23
- el: HTMLElement;
24
- static _dropdowns: Dropdown[] = [];
25
- id: string;
26
- dropdownEl: HTMLElement;
27
- isOpen: boolean;
28
- isScrollable: boolean;
29
- isTouchMoving: boolean;
30
- focusedIndex: number;
31
- filterQuery: any[];
32
- private _resetFilterQueryBound: any;
33
- private _handleDocumentClickBound: any;
34
- private _handleDocumentTouchmoveBound: any;
35
- private _handleDropdownClickBound: any;
36
- private _handleDropdownKeydownBound: any;
37
- private _handleTriggerKeydownBound: any;
38
- private _handleMouseEnterBound: any;
39
- private _handleMouseLeaveBound: any;
40
- _handleClickBound: any;
41
- filterTimeout: NodeJS.Timeout;
42
-
43
- constructor(el, options) {
44
- super(Dropdown, el, options);
45
- (this.el as any).M_Dropdown = this;
46
- Dropdown._dropdowns.push(this);
47
- this.id = M.getIdFromTrigger(el);
48
- this.dropdownEl = document.getElementById(this.id);
49
- //this.$dropdownEl = $(this.dropdownEl);
50
- this.options = {...Dropdown.defaults, ...options};
51
-
52
- this.isOpen = false;
53
- this.isScrollable = false;
54
- this.isTouchMoving = false;
55
- this.focusedIndex = -1;
56
- this.filterQuery = [];
57
-
58
- // Move dropdown-content after dropdown-trigger
59
- this._moveDropdown();
60
- this._makeDropdownFocusable();
61
- this._resetFilterQueryBound = this._resetFilterQuery.bind(this);
62
- this._handleDocumentClickBound = this._handleDocumentClick.bind(this);
63
- this._handleDocumentTouchmoveBound = this._handleDocumentTouchmove.bind(this);
64
- this._handleDropdownClickBound = this._handleDropdownClick.bind(this);
65
- this._handleDropdownKeydownBound = this._handleDropdownKeydown.bind(this);
66
- this._handleTriggerKeydownBound = this._handleTriggerKeydown.bind(this);
67
- this._setupEventHandlers();
68
- }
125
+ this.isOpen = false;
126
+ this.isScrollable = false;
127
+ this.isTouchMoving = false;
128
+ this.focusedIndex = -1;
129
+ this.filterQuery = [];
69
130
 
70
- static get defaults() {
71
- return _defaults;
72
- }
131
+ // Move dropdown-content after dropdown-trigger
132
+ this._moveDropdown();
133
+ this._makeDropdownFocusable();
134
+ this._setupEventHandlers();
135
+ }
73
136
 
74
- static init(els, options) {
75
- return super.init(this, els, options);
76
- }
137
+ static get defaults(): DropdownOptions {
138
+ return _defaults;
139
+ }
77
140
 
78
- static getInstance(el) {
79
- const domElem = !!el.jquery ? el[0] : el;
80
- return domElem.M_Dropdown;
81
- }
141
+ /**
142
+ * Initializes instance of Dropdown.
143
+ * @param el HTML element.
144
+ * @param options Component options.
145
+ */
146
+ static init(el: HTMLElement, options?: Partial<DropdownOptions>): Dropdown;
147
+ /**
148
+ * Initializes instances of Dropdown.
149
+ * @param els HTML elements.
150
+ * @param options Component options.
151
+ */
152
+ static init(els: InitElements<MElement>, options?: Partial<DropdownOptions>): Dropdown[];
153
+ /**
154
+ * Initializes instances of Dropdown.
155
+ * @param els HTML elements.
156
+ * @param options Component options.
157
+ */
158
+ static init(els: HTMLElement | InitElements<MElement>, options: Partial<DropdownOptions> = {}): Dropdown | Dropdown[] {
159
+ return super.init(els, options, Dropdown);
160
+ }
82
161
 
83
- destroy() {
84
- this._resetDropdownStyles();
85
- this._removeEventHandlers();
86
- Dropdown._dropdowns.splice(Dropdown._dropdowns.indexOf(this), 1);
87
- (this.el as any).M_Dropdown = undefined;
88
- }
162
+ static getInstance(el: HTMLElement): Dropdown {
163
+ return (el as any).M_Dropdown;
164
+ }
89
165
 
90
- _setupEventHandlers() {
91
- // Trigger keydown handler
92
- this.el.addEventListener('keydown', this._handleTriggerKeydownBound);
93
- // Item click handler
94
- this.dropdownEl?.addEventListener('click', this._handleDropdownClickBound);
95
- // Hover event handlers
96
- if (this.options.hover) {
97
- this._handleMouseEnterBound = this._handleMouseEnter.bind(this);
98
- this.el.addEventListener('mouseenter', this._handleMouseEnterBound);
99
- this._handleMouseLeaveBound = this._handleMouseLeave.bind(this);
100
- this.el.addEventListener('mouseleave', this._handleMouseLeaveBound);
101
- this.dropdownEl.addEventListener('mouseleave', this._handleMouseLeaveBound);
102
- // Click event handlers
103
- } else {
104
- this._handleClickBound = this._handleClick.bind(this);
105
- this.el.addEventListener('click', this._handleClickBound);
106
- }
107
- }
166
+ destroy() {
167
+ this._resetDropdownStyles();
168
+ this._removeEventHandlers();
169
+ Dropdown._dropdowns.splice(Dropdown._dropdowns.indexOf(this), 1);
170
+ (this.el as any).M_Dropdown = undefined;
171
+ }
108
172
 
109
- _removeEventHandlers() {
110
- this.el.removeEventListener('keydown', this._handleTriggerKeydownBound);
111
- this.dropdownEl.removeEventListener('click', this._handleDropdownClickBound);
112
- if (this.options.hover) {
113
- this.el.removeEventListener('mouseenter', this._handleMouseEnterBound);
114
- this.el.removeEventListener('mouseleave', this._handleMouseLeaveBound);
115
- this.dropdownEl.removeEventListener('mouseleave', this._handleMouseLeaveBound);
116
- } else {
117
- this.el.removeEventListener('click', this._handleClickBound);
118
- }
173
+ _setupEventHandlers() {
174
+ // Trigger keydown handler
175
+ this.el.addEventListener('keydown', this._handleTriggerKeydown);
176
+ // Item click handler
177
+ this.dropdownEl?.addEventListener('click', this._handleDropdownClick);
178
+ // Hover event handlers
179
+ if (this.options.hover) {
180
+ this.el.addEventListener('mouseenter', this._handleMouseEnter);
181
+ this.el.addEventListener('mouseleave', this._handleMouseLeave);
182
+ this.dropdownEl.addEventListener('mouseleave', this._handleMouseLeave);
183
+ // Click event handlers
184
+ } else {
185
+ this.el.addEventListener('click', this._handleClick);
119
186
  }
187
+ }
120
188
 
121
- _setupTemporaryEventHandlers() {
122
- // Use capture phase event handler to prevent click
123
- document.body.addEventListener('click', this._handleDocumentClickBound, true);
124
- document.body.addEventListener('touchmove', this._handleDocumentTouchmoveBound);
125
- this.dropdownEl.addEventListener('keydown', this._handleDropdownKeydownBound);
189
+ _removeEventHandlers() {
190
+ this.el.removeEventListener('keydown', this._handleTriggerKeydown);
191
+ this.dropdownEl.removeEventListener('click', this._handleDropdownClick);
192
+ if (this.options.hover) {
193
+ this.el.removeEventListener('mouseenter', this._handleMouseEnter);
194
+ this.el.removeEventListener('mouseleave', this._handleMouseLeave);
195
+ this.dropdownEl.removeEventListener('mouseleave', this._handleMouseLeave);
196
+ } else {
197
+ this.el.removeEventListener('click', this._handleClick);
126
198
  }
199
+ }
127
200
 
128
- _removeTemporaryEventHandlers() {
129
- // Use capture phase event handler to prevent click
130
- document.body.removeEventListener('click', this._handleDocumentClickBound, true);
131
- document.body.removeEventListener('touchmove', this._handleDocumentTouchmoveBound);
132
- this.dropdownEl.removeEventListener('keydown', this._handleDropdownKeydownBound);
133
- }
201
+ _setupTemporaryEventHandlers() {
202
+ // Use capture phase event handler to prevent click
203
+ document.body.addEventListener('click', this._handleDocumentClick, true);
204
+ document.body.addEventListener('touchmove', this._handleDocumentTouchmove);
205
+ this.dropdownEl.addEventListener('keydown', this._handleDropdownKeydown);
206
+ }
134
207
 
135
- _handleClick(e) {
136
- e.preventDefault();
137
- this.open();
138
- }
208
+ _removeTemporaryEventHandlers() {
209
+ // Use capture phase event handler to prevent click
210
+ document.body.removeEventListener('click', this._handleDocumentClick, true);
211
+ document.body.removeEventListener('touchmove', this._handleDocumentTouchmove);
212
+ this.dropdownEl.removeEventListener('keydown', this._handleDropdownKeydown);
213
+ }
139
214
 
140
- _handleMouseEnter() {
141
- this.open();
142
- }
215
+ _handleClick = (e: MouseEvent) => {
216
+ e.preventDefault();
217
+ this.open();
218
+ }
143
219
 
144
- _handleMouseLeave(e) {
145
- const toEl = e.toElement || e.relatedTarget;
146
- const leaveToDropdownContent = !!toEl.closest('.dropdown-content');
147
- let leaveToActiveDropdownTrigger = false;
148
- const closestTrigger = toEl.closest('.dropdown-trigger');
149
- if (
150
- closestTrigger &&
151
- !!(<any>closestTrigger).M_Dropdown &&
152
- (<any>closestTrigger).M_Dropdown.isOpen
153
- ) {
154
- leaveToActiveDropdownTrigger = true;
155
- }
156
- // Close hover dropdown if mouse did not leave to either active dropdown-trigger or dropdown-content
157
- if (!leaveToActiveDropdownTrigger && !leaveToDropdownContent) {
158
- this.close();
159
- }
220
+ _handleMouseEnter = () => {
221
+ this.open();
222
+ }
223
+
224
+ _handleMouseLeave = (e: MouseEvent) => {
225
+ const toEl = e.relatedTarget as HTMLElement;
226
+ const leaveToDropdownContent = !!toEl.closest('.dropdown-content');
227
+ let leaveToActiveDropdownTrigger = false;
228
+ const closestTrigger = toEl.closest('.dropdown-trigger');
229
+ if (
230
+ closestTrigger &&
231
+ !!(<any>closestTrigger).M_Dropdown &&
232
+ (<any>closestTrigger).M_Dropdown.isOpen
233
+ ) {
234
+ leaveToActiveDropdownTrigger = true;
160
235
  }
236
+ // Close hover dropdown if mouse did not leave to either active dropdown-trigger or dropdown-content
237
+ if (!leaveToActiveDropdownTrigger && !leaveToDropdownContent) {
238
+ this.close();
239
+ }
240
+ }
161
241
 
162
- _handleDocumentClick(e) {
163
- const target = <HTMLElement>e.target;
164
- if (
165
- this.options.closeOnClick &&
166
- target.closest('.dropdown-content') &&
167
- !this.isTouchMoving
168
- ) {
169
- // isTouchMoving to check if scrolling on mobile.
170
- //setTimeout(() => {
171
- this.close();
172
- //}, 0);
173
- }
174
- else if (
175
- target.closest('.dropdown-trigger') ||
176
- !target.closest('.dropdown-content')
177
- ) {
178
- //setTimeout(() => {
179
- this.close();
180
- //}, 0);
181
- }
182
- this.isTouchMoving = false;
242
+ _handleDocumentClick = (e: MouseEvent) => {
243
+ const target = <HTMLElement>e.target;
244
+ if (
245
+ this.options.closeOnClick &&
246
+ target.closest('.dropdown-content') &&
247
+ !this.isTouchMoving
248
+ ) {
249
+ // isTouchMoving to check if scrolling on mobile.
250
+ //setTimeout(() => {
251
+ this.close();
252
+ //}, 0);
183
253
  }
254
+ else if (
255
+ target.closest('.dropdown-trigger') ||
256
+ !target.closest('.dropdown-content')
257
+ ) {
258
+ //setTimeout(() => {
259
+ this.close();
260
+ //}, 0);
261
+ }
262
+ this.isTouchMoving = false;
263
+ }
184
264
 
185
- _handleTriggerKeydown(e) {
186
- // ARROW DOWN OR ENTER WHEN SELECT IS CLOSED - open Dropdown
187
- if ((e.which === M.keys.ARROW_DOWN || e.which === M.keys.ENTER) && !this.isOpen) {
188
- e.preventDefault();
189
- this.open();
190
- }
265
+ _handleTriggerKeydown = (e: KeyboardEvent) => {
266
+ // ARROW DOWN OR ENTER WHEN SELECT IS CLOSED - open Dropdown
267
+ const arrowDownOrEnter = Utils.keys.ARROW_DOWN.includes(e.key) || Utils.keys.ENTER.includes(e.key);
268
+ if (arrowDownOrEnter && !this.isOpen) {
269
+ e.preventDefault();
270
+ this.open();
191
271
  }
272
+ }
192
273
 
193
- _handleDocumentTouchmove(e) {
194
- const target = <HTMLElement>e.target;
195
- if (target.closest('.dropdown-content')) {
196
- this.isTouchMoving = true;
197
- }
274
+ _handleDocumentTouchmove = (e: TouchEvent) => {
275
+ const target = <HTMLElement>e.target;
276
+ if (target.closest('.dropdown-content')) {
277
+ this.isTouchMoving = true;
198
278
  }
279
+ }
199
280
 
200
- _handleDropdownClick(e) {
201
- // onItemClick callback
202
- if (typeof this.options.onItemClick === 'function') {
203
- const itemEl = <HTMLElement>e.target.closest('li');
204
- this.options.onItemClick.call(this, itemEl);
205
- }
281
+ _handleDropdownClick = (e: MouseEvent) => {
282
+ // onItemClick callback
283
+ if (typeof this.options.onItemClick === 'function') {
284
+ const itemEl = (<HTMLElement>e.target).closest('li');
285
+ this.options.onItemClick.call(this, itemEl);
206
286
  }
287
+ }
207
288
 
208
- _handleDropdownKeydown(e) {
209
- if (e.which === M.keys.TAB) {
210
- e.preventDefault();
211
- this.close();
212
- }
213
- // Navigate down dropdown list
214
- else if ((e.which === M.keys.ARROW_DOWN || e.which === M.keys.ARROW_UP) && this.isOpen) {
215
- e.preventDefault();
216
- const direction = e.which === M.keys.ARROW_DOWN ? 1 : -1;
217
- let newFocusedIndex = this.focusedIndex;
218
- let hasFoundNewIndex = false;
219
- do {
220
- newFocusedIndex = newFocusedIndex + direction;
221
- if (
222
- !!this.dropdownEl.children[newFocusedIndex] &&
223
- (<any>this.dropdownEl.children[newFocusedIndex]).tabIndex !== -1
224
- ) {
225
- hasFoundNewIndex = true;
226
- break;
227
- }
228
- } while (newFocusedIndex < this.dropdownEl.children.length && newFocusedIndex >= 0);
229
-
230
- if (hasFoundNewIndex) {
231
- // Remove active class from old element
232
- if (this.focusedIndex >= 0)
233
- this.dropdownEl.children[this.focusedIndex].classList.remove('active');
234
- this.focusedIndex = newFocusedIndex;
235
- this._focusFocusedItem();
236
- }
237
- }
238
- // ENTER selects choice on focused item
239
- else if (e.which === M.keys.ENTER && this.isOpen) {
240
- // Search for <a> and <button>
241
- const focusedElement = this.dropdownEl.children[this.focusedIndex];
242
- const activatableElement = <HTMLElement>focusedElement.querySelector('a, button');
243
- // Click a or button tag if exists, otherwise click li tag
244
- if (!!activatableElement) {
245
- activatableElement.click();
246
- }
247
- else if (!!focusedElement) {
248
- if (focusedElement instanceof HTMLElement) {
249
- focusedElement.click();
250
- }
289
+ _handleDropdownKeydown = (e: KeyboardEvent) => {
290
+ const arrowUpOrDown = Utils.keys.ARROW_DOWN.includes(e.key) || Utils.keys.ARROW_UP.includes(e.key);
291
+ if (Utils.keys.TAB.includes(e.key)) {
292
+ e.preventDefault();
293
+ this.close();
294
+ }
295
+ // Navigate down dropdown list
296
+ else if (arrowUpOrDown && this.isOpen) {
297
+ e.preventDefault();
298
+ const direction = Utils.keys.ARROW_DOWN.includes(e.key) ? 1 : -1;
299
+ let newFocusedIndex = this.focusedIndex;
300
+ let hasFoundNewIndex = false;
301
+ do {
302
+ newFocusedIndex = newFocusedIndex + direction;
303
+ if (
304
+ !!this.dropdownEl.children[newFocusedIndex] &&
305
+ (<any>this.dropdownEl.children[newFocusedIndex]).tabIndex !== -1
306
+ ) {
307
+ hasFoundNewIndex = true;
308
+ break;
251
309
  }
310
+ } while (newFocusedIndex < this.dropdownEl.children.length && newFocusedIndex >= 0);
311
+
312
+ if (hasFoundNewIndex) {
313
+ // Remove active class from old element
314
+ if (this.focusedIndex >= 0)
315
+ this.dropdownEl.children[this.focusedIndex].classList.remove('active');
316
+ this.focusedIndex = newFocusedIndex;
317
+ this._focusFocusedItem();
252
318
  }
253
- // Close dropdown on ESC
254
- else if (e.which === M.keys.ESC && this.isOpen) {
255
- e.preventDefault();
256
- this.close();
319
+ }
320
+ // ENTER selects choice on focused item
321
+ else if (Utils.keys.ENTER.includes(e.key) && this.isOpen) {
322
+ // Search for <a> and <button>
323
+ const focusedElement = this.dropdownEl.children[this.focusedIndex];
324
+ const activatableElement = <HTMLElement>focusedElement.querySelector('a, button');
325
+ // Click a or button tag if exists, otherwise click li tag
326
+ if (!!activatableElement) {
327
+ activatableElement.click();
257
328
  }
258
-
259
- // CASE WHEN USER TYPE LETTERS
260
- const letter = String.fromCharCode(e.which).toLowerCase();
261
- const nonLetters = [9, 13, 27, 38, 40];
262
- if (letter && nonLetters.indexOf(e.which) === -1) {
263
- this.filterQuery.push(letter);
264
- const string = this.filterQuery.join('');
265
- const newOptionEl = Array.from(this.dropdownEl.querySelectorAll('li'))
266
- .find((el) => el.innerText.toLowerCase().indexOf(string) === 0);
267
- if (newOptionEl) {
268
- this.focusedIndex = [...newOptionEl.parentNode.children].indexOf(newOptionEl);
269
- this._focusFocusedItem();
329
+ else if (!!focusedElement) {
330
+ if (focusedElement instanceof HTMLElement) {
331
+ focusedElement.click();
270
332
  }
271
333
  }
272
- this.filterTimeout = setTimeout(this._resetFilterQueryBound, 1000);
273
334
  }
274
-
275
- _resetFilterQuery() {
276
- this.filterQuery = [];
335
+ // Close dropdown on ESC
336
+ else if (Utils.keys.ESC.includes(e.key) && this.isOpen) {
337
+ e.preventDefault();
338
+ this.close();
277
339
  }
278
340
 
279
- _resetDropdownStyles() {
280
- this.dropdownEl.style.display = '';
281
- this.dropdownEl.style.width = '';
282
- this.dropdownEl.style.height = '';
283
- this.dropdownEl.style.left = '';
284
- this.dropdownEl.style.top = '';
285
- this.dropdownEl.style.transformOrigin = '';
286
- this.dropdownEl.style.transform = '';
287
- this.dropdownEl.style.opacity = '';
341
+ // CASE WHEN USER TYPE LTTERS
342
+ const keyText = e.key.toLowerCase();
343
+ const isLetter = /[a-zA-Z0-9-_]/.test(keyText);
344
+ const specialKeys = [...Utils.keys.ARROW_DOWN, ...Utils.keys.ARROW_UP, ...Utils.keys.ENTER, ...Utils.keys.ESC, ...Utils.keys.TAB];
345
+ if (isLetter && !specialKeys.includes(e.key)) {
346
+ this.filterQuery.push(keyText);
347
+ const string = this.filterQuery.join('');
348
+ const newOptionEl = Array.from(this.dropdownEl.querySelectorAll('li'))
349
+ .find((el) => el.innerText.toLowerCase().indexOf(string) === 0);
350
+ if (newOptionEl) {
351
+ this.focusedIndex = [...newOptionEl.parentNode.children].indexOf(newOptionEl);
352
+ this._focusFocusedItem();
353
+ }
288
354
  }
355
+ this.filterTimeout = setTimeout(this._resetFilterQuery, 1000);
356
+ }
289
357
 
290
- // Move dropdown after container or trigger
291
- _moveDropdown(containerEl = null) {
292
- if (!!this.options.container) {
293
- this.options.container.append(this.dropdownEl);
294
- }
295
- else if (containerEl) {
296
- if (!containerEl.contains(this.dropdownEl)) {
297
- containerEl.append(this.dropdownEl);
298
- }
299
- }
300
- else {
301
- this.el.after(this.dropdownEl);
358
+ _resetFilterQuery = () => {
359
+ this.filterQuery = [];
360
+ }
361
+
362
+ _resetDropdownStyles() {
363
+ this.dropdownEl.style.display = '';
364
+ this.dropdownEl.style.width = '';
365
+ this.dropdownEl.style.height = '';
366
+ this.dropdownEl.style.left = '';
367
+ this.dropdownEl.style.top = '';
368
+ this.dropdownEl.style.transformOrigin = '';
369
+ this.dropdownEl.style.transform = '';
370
+ this.dropdownEl.style.opacity = '';
371
+ }
372
+
373
+ // Move dropdown after container or trigger
374
+ _moveDropdown(containerEl: HTMLElement = null) {
375
+ if (!!this.options.container) {
376
+ this.options.container.append(this.dropdownEl);
377
+ }
378
+ else if (containerEl) {
379
+ if (!containerEl.contains(this.dropdownEl)) {
380
+ containerEl.append(this.dropdownEl);
302
381
  }
303
382
  }
383
+ else {
384
+ this.el.after(this.dropdownEl);
385
+ }
386
+ }
387
+
388
+ _makeDropdownFocusable() {
389
+ if (!this.dropdownEl) return;
390
+ // Needed for arrow key navigation
391
+ this.dropdownEl.tabIndex = 0;
392
+ // Only set tabindex if it hasn't been set by user
393
+ Array.from(this.dropdownEl.children).forEach((el)=> {
394
+ if (!el.getAttribute('tabindex'))
395
+ el.setAttribute('tabindex', '0');
396
+ });
397
+ }
304
398
 
305
- _makeDropdownFocusable() {
306
- if (!this.dropdownEl) return;
307
- // Needed for arrow key navigation
308
- this.dropdownEl.tabIndex = 0;
309
- // Only set tabindex if it hasn't been set by user
310
- Array.from(this.dropdownEl.children).forEach((el)=> {
311
- if (!el.getAttribute('tabindex'))
312
- el.setAttribute('tabindex', '0');
399
+ _focusFocusedItem() {
400
+ if (
401
+ this.focusedIndex >= 0 &&
402
+ this.focusedIndex < this.dropdownEl.children.length &&
403
+ this.options.autoFocus
404
+ ) {
405
+ (this.dropdownEl.children[this.focusedIndex] as HTMLElement).focus({
406
+ preventScroll: true
407
+ });
408
+ this.dropdownEl.children[this.focusedIndex].scrollIntoView({
409
+ behavior: 'smooth',
410
+ block: 'nearest',
411
+ inline: 'nearest'
313
412
  });
314
413
  }
414
+ }
315
415
 
316
- _focusFocusedItem() {
317
- if (
318
- this.focusedIndex >= 0 &&
319
- this.focusedIndex < this.dropdownEl.children.length &&
320
- this.options.autoFocus
321
- ) {
322
- (this.dropdownEl.children[this.focusedIndex] as HTMLElement).focus({
323
- preventScroll: true
324
- });
325
- this.dropdownEl.children[this.focusedIndex].scrollIntoView({
326
- behavior: 'smooth',
327
- block: 'nearest',
328
- inline: 'nearest'
329
- });
330
- }
331
- }
416
+ _getDropdownPosition(closestOverflowParent: HTMLElement) {
417
+ const offsetParentBRect = this.el.offsetParent.getBoundingClientRect();
418
+ const triggerBRect = this.el.getBoundingClientRect();
419
+ const dropdownBRect = this.dropdownEl.getBoundingClientRect();
420
+
421
+ let idealHeight = dropdownBRect.height;
422
+ let idealWidth = dropdownBRect.width;
423
+ let idealXPos = triggerBRect.left - dropdownBRect.left;
424
+ let idealYPos = triggerBRect.top - dropdownBRect.top;
425
+
426
+ const dropdownBounds = {
427
+ left: idealXPos,
428
+ top: idealYPos,
429
+ height: idealHeight,
430
+ width: idealWidth
431
+ };
332
432
 
333
- _getDropdownPosition(closestOverflowParent) {
334
- const offsetParentBRect = this.el.offsetParent.getBoundingClientRect();
335
- const triggerBRect = this.el.getBoundingClientRect();
336
- const dropdownBRect = this.dropdownEl.getBoundingClientRect();
337
-
338
- let idealHeight = dropdownBRect.height;
339
- let idealWidth = dropdownBRect.width;
340
- let idealXPos = triggerBRect.left - dropdownBRect.left;
341
- let idealYPos = triggerBRect.top - dropdownBRect.top;
342
-
343
- const dropdownBounds = {
344
- left: idealXPos,
345
- top: idealYPos,
346
- height: idealHeight,
347
- width: idealWidth
348
- };
349
-
350
- const alignments = M.checkPossibleAlignments(
351
- this.el,
352
- closestOverflowParent,
353
- dropdownBounds,
354
- this.options.coverTrigger ? 0 : triggerBRect.height
355
- );
356
-
357
- let verticalAlignment = 'top';
358
- let horizontalAlignment = this.options.alignment;
359
- idealYPos += this.options.coverTrigger ? 0 : triggerBRect.height;
360
-
361
- // Reset isScrollable
362
- this.isScrollable = false;
363
-
364
- if (!alignments.top) {
365
- if (alignments.bottom) {
366
- verticalAlignment = 'bottom';
433
+ const alignments = Utils.checkPossibleAlignments(
434
+ this.el,
435
+ closestOverflowParent,
436
+ dropdownBounds,
437
+ this.options.coverTrigger ? 0 : triggerBRect.height
438
+ );
367
439
 
368
- if (!this.options.coverTrigger) {
369
- idealYPos -= triggerBRect.height;
370
- }
371
- } else {
372
- this.isScrollable = true;
373
-
374
- // Determine which side has most space and cutoff at correct height
375
- idealHeight -= 20; // Add padding when cutoff
376
- if (alignments.spaceOnTop > alignments.spaceOnBottom) {
377
- verticalAlignment = 'bottom';
378
- idealHeight += alignments.spaceOnTop;
379
- idealYPos -= this.options.coverTrigger
380
- ? alignments.spaceOnTop - 20
381
- : alignments.spaceOnTop - 20 + triggerBRect.height;
382
- } else {
383
- idealHeight += alignments.spaceOnBottom;
384
- }
440
+ let verticalAlignment = 'top';
441
+ let horizontalAlignment = this.options.alignment;
442
+ idealYPos += this.options.coverTrigger ? 0 : triggerBRect.height;
443
+
444
+ // Reset isScrollable
445
+ this.isScrollable = false;
446
+
447
+ if (!alignments.top) {
448
+ if (alignments.bottom) {
449
+ verticalAlignment = 'bottom';
450
+
451
+ if (!this.options.coverTrigger) {
452
+ idealYPos -= triggerBRect.height;
385
453
  }
386
- }
454
+ } else {
455
+ this.isScrollable = true;
387
456
 
388
- // If preferred horizontal alignment is possible
389
- if (!alignments[horizontalAlignment]) {
390
- const oppositeAlignment = horizontalAlignment === 'left' ? 'right' : 'left';
391
- if (alignments[oppositeAlignment]) {
392
- horizontalAlignment = oppositeAlignment;
457
+ // Determine which side has most space and cutoff at correct height
458
+ idealHeight -= 20; // Add padding when cutoff
459
+ if (alignments.spaceOnTop > alignments.spaceOnBottom) {
460
+ verticalAlignment = 'bottom';
461
+ idealHeight += alignments.spaceOnTop;
462
+ idealYPos -= this.options.coverTrigger
463
+ ? alignments.spaceOnTop - 20
464
+ : alignments.spaceOnTop - 20 + triggerBRect.height;
393
465
  } else {
394
- // Determine which side has most space and cutoff at correct height
395
- if (alignments.spaceOnLeft > alignments.spaceOnRight) {
396
- horizontalAlignment = 'right';
397
- idealWidth += alignments.spaceOnLeft;
398
- idealXPos -= alignments.spaceOnLeft;
399
- } else {
400
- horizontalAlignment = 'left';
401
- idealWidth += alignments.spaceOnRight;
402
- }
466
+ idealHeight += alignments.spaceOnBottom;
403
467
  }
404
468
  }
405
-
406
- if (verticalAlignment === 'bottom') {
407
- idealYPos =
408
- idealYPos - dropdownBRect.height + (this.options.coverTrigger ? triggerBRect.height : 0);
409
- }
410
- if (horizontalAlignment === 'right') {
411
- idealXPos = idealXPos - dropdownBRect.width + triggerBRect.width;
412
- }
413
- return {
414
- x: idealXPos,
415
- y: idealYPos,
416
- verticalAlignment: verticalAlignment,
417
- horizontalAlignment: horizontalAlignment,
418
- height: idealHeight,
419
- width: idealWidth
420
- };
421
469
  }
422
470
 
423
- _animateIn() {
424
- anim.remove(this.dropdownEl);
425
- anim({
426
- targets: this.dropdownEl,
427
- opacity: {
428
- value: [0, 1],
429
- easing: 'easeOutQuad'
430
- },
431
- scaleX: [0.3, 1],
432
- scaleY: [0.3, 1],
433
- duration: this.options.inDuration,
434
- easing: 'easeOutQuint',
435
- complete: (anim) => {
436
- if (this.options.autoFocus) this.dropdownEl.focus();
437
- // onOpenEnd callback
438
- if (typeof this.options.onOpenEnd === 'function') {
439
- this.options.onOpenEnd.call(this, this.el);
440
- }
471
+ // If preferred horizontal alignment is possible
472
+ if (!alignments[horizontalAlignment]) {
473
+ const oppositeAlignment = horizontalAlignment === 'left' ? 'right' : 'left';
474
+ if (alignments[oppositeAlignment]) {
475
+ horizontalAlignment = oppositeAlignment;
476
+ } else {
477
+ // Determine which side has most space and cutoff at correct height
478
+ if (alignments.spaceOnLeft > alignments.spaceOnRight) {
479
+ horizontalAlignment = 'right';
480
+ idealWidth += alignments.spaceOnLeft;
481
+ idealXPos -= alignments.spaceOnLeft;
482
+ } else {
483
+ horizontalAlignment = 'left';
484
+ idealWidth += alignments.spaceOnRight;
441
485
  }
442
- });
486
+ }
443
487
  }
444
488
 
445
- _animateOut() {
446
- anim.remove(this.dropdownEl);
447
- anim({
448
- targets: this.dropdownEl,
449
- opacity: {
450
- value: 0,
451
- easing: 'easeOutQuint'
452
- },
453
- scaleX: 0.3,
454
- scaleY: 0.3,
455
- duration: this.options.outDuration,
456
- easing: 'easeOutQuint',
457
- complete: (anim) => {
458
- this._resetDropdownStyles();
459
- // onCloseEnd callback
460
- if (typeof this.options.onCloseEnd === 'function') {
461
- this.options.onCloseEnd.call(this, this.el);
462
- }
463
- }
464
- });
489
+ if (verticalAlignment === 'bottom') {
490
+ idealYPos =
491
+ idealYPos - dropdownBRect.height + (this.options.coverTrigger ? triggerBRect.height : 0);
492
+ }
493
+ if (horizontalAlignment === 'right') {
494
+ idealXPos = idealXPos - dropdownBRect.width + triggerBRect.width;
465
495
  }
496
+ return {
497
+ x: idealXPos,
498
+ y: idealYPos,
499
+ verticalAlignment: verticalAlignment,
500
+ horizontalAlignment: horizontalAlignment,
501
+ height: idealHeight,
502
+ width: idealWidth
503
+ };
504
+ }
466
505
 
467
- private _getClosestAncestor(el: Element, condition: Function): Element {
468
- let ancestor = el.parentNode;
469
- while (ancestor !== null && ancestor !== document) {
470
- if (condition(ancestor)) {
471
- return <Element>ancestor;
506
+ _animateIn() {
507
+ anim.remove(this.dropdownEl);
508
+ anim({
509
+ targets: this.dropdownEl,
510
+ opacity: {
511
+ value: [0, 1],
512
+ easing: 'easeOutQuad'
513
+ },
514
+ scaleX: [0.3, 1],
515
+ scaleY: [0.3, 1],
516
+ duration: this.options.inDuration,
517
+ easing: 'easeOutQuint',
518
+ complete: (anim) => {
519
+ if (this.options.autoFocus) this.dropdownEl.focus();
520
+ // onOpenEnd callback
521
+ if (typeof this.options.onOpenEnd === 'function') {
522
+ this.options.onOpenEnd.call(this, this.el);
472
523
  }
473
- ancestor = ancestor.parentNode;
474
524
  }
475
- return null;
476
- };
525
+ });
526
+ }
477
527
 
478
- _placeDropdown() {
479
- // Container here will be closest ancestor with overflow: hidden
480
- let closestOverflowParent: HTMLElement = <HTMLElement>this._getClosestAncestor(this.dropdownEl, (ancestor: HTMLElement) => {
481
- return !['HTML','BODY'].includes(ancestor.tagName) && getComputedStyle(ancestor).overflow !== 'visible';
482
- });
483
- // Fallback
484
- if (!closestOverflowParent) {
485
- closestOverflowParent = <HTMLElement>(!!this.dropdownEl.offsetParent
486
- ? this.dropdownEl.offsetParent
487
- : this.dropdownEl.parentNode);
528
+ _animateOut() {
529
+ anim.remove(this.dropdownEl);
530
+ anim({
531
+ targets: this.dropdownEl,
532
+ opacity: {
533
+ value: 0,
534
+ easing: 'easeOutQuint'
535
+ },
536
+ scaleX: 0.3,
537
+ scaleY: 0.3,
538
+ duration: this.options.outDuration,
539
+ easing: 'easeOutQuint',
540
+ complete: (anim) => {
541
+ this._resetDropdownStyles();
542
+ // onCloseEnd callback
543
+ if (typeof this.options.onCloseEnd === 'function') {
544
+ this.options.onCloseEnd.call(this, this.el);
545
+ }
488
546
  }
547
+ });
548
+ }
489
549
 
490
- if (getComputedStyle(closestOverflowParent).position === 'static')
491
- closestOverflowParent.style.position = 'relative';
492
-
493
- this._moveDropdown(closestOverflowParent);
494
-
495
- // Set width before calculating positionInfo
496
- const idealWidth = this.options.constrainWidth
497
- ? this.el.getBoundingClientRect().width
498
- : this.dropdownEl.getBoundingClientRect().width;
499
- this.dropdownEl.style.width = idealWidth + 'px';
500
-
501
- const positionInfo = this._getDropdownPosition(closestOverflowParent);
502
- this.dropdownEl.style.left = positionInfo.x + 'px';
503
- this.dropdownEl.style.top = positionInfo.y + 'px';
504
- this.dropdownEl.style.height = positionInfo.height + 'px';
505
- this.dropdownEl.style.width = positionInfo.width + 'px';
506
- this.dropdownEl.style.transformOrigin = `${
507
- positionInfo.horizontalAlignment === 'left' ? '0' : '100%'
508
- } ${positionInfo.verticalAlignment === 'top' ? '0' : '100%'}`;
550
+ private _getClosestAncestor(el: HTMLElement, condition: Function): HTMLElement {
551
+ let ancestor = el.parentNode;
552
+ while (ancestor !== null && ancestor !== document) {
553
+ if (condition(ancestor)) {
554
+ return <HTMLElement>ancestor;
555
+ }
556
+ ancestor = ancestor.parentElement;
509
557
  }
558
+ return null;
559
+ };
510
560
 
511
- open() {
512
- if (this.isOpen) return;
513
- this.isOpen = true;
514
- // onOpenStart callback
515
- if (typeof this.options.onOpenStart === 'function') {
516
- this.options.onOpenStart.call(this, this.el);
517
- }
518
- // Reset styles
519
- this._resetDropdownStyles();
520
- this.dropdownEl.style.display = 'block';
521
- this._placeDropdown();
522
- this._animateIn();
523
- this._setupTemporaryEventHandlers();
561
+ _placeDropdown() {
562
+ // Container here will be closest ancestor with overflow: hidden
563
+ let closestOverflowParent: HTMLElement = this._getClosestAncestor(this.dropdownEl, (ancestor: HTMLElement) => {
564
+ return !['HTML','BODY'].includes(ancestor.tagName) && getComputedStyle(ancestor).overflow !== 'visible';
565
+ });
566
+ // Fallback
567
+ if (!closestOverflowParent) {
568
+ closestOverflowParent = <HTMLElement>(!!this.dropdownEl.offsetParent
569
+ ? this.dropdownEl.offsetParent
570
+ : this.dropdownEl.parentNode);
524
571
  }
525
572
 
526
- close() {
527
- if (!this.isOpen) return;
528
- this.isOpen = false;
529
- this.focusedIndex = -1;
530
- // onCloseStart callback
531
- if (typeof this.options.onCloseStart === 'function') {
532
- this.options.onCloseStart.call(this, this.el);
533
- }
534
- this._animateOut();
535
- this._removeTemporaryEventHandlers();
536
- if (this.options.autoFocus) {
537
- this.el.focus();
538
- }
573
+ if (getComputedStyle(closestOverflowParent).position === 'static')
574
+ closestOverflowParent.style.position = 'relative';
575
+
576
+ this._moveDropdown(closestOverflowParent);
577
+
578
+ // Set width before calculating positionInfo
579
+ const idealWidth = this.options.constrainWidth
580
+ ? this.el.getBoundingClientRect().width
581
+ : this.dropdownEl.getBoundingClientRect().width;
582
+ this.dropdownEl.style.width = idealWidth + 'px';
583
+
584
+ const positionInfo = this._getDropdownPosition(closestOverflowParent);
585
+ this.dropdownEl.style.left = positionInfo.x + 'px';
586
+ this.dropdownEl.style.top = positionInfo.y + 'px';
587
+ this.dropdownEl.style.height = positionInfo.height + 'px';
588
+ this.dropdownEl.style.width = positionInfo.width + 'px';
589
+ this.dropdownEl.style.transformOrigin = `${
590
+ positionInfo.horizontalAlignment === 'left' ? '0' : '100%'
591
+ } ${positionInfo.verticalAlignment === 'top' ? '0' : '100%'}`;
592
+ }
593
+
594
+ /**
595
+ * Open dropdown.
596
+ */
597
+ open = () => {
598
+ if (this.isOpen) return;
599
+ this.isOpen = true;
600
+ // onOpenStart callback
601
+ if (typeof this.options.onOpenStart === 'function') {
602
+ this.options.onOpenStart.call(this, this.el);
539
603
  }
604
+ // Reset styles
605
+ this._resetDropdownStyles();
606
+ this.dropdownEl.style.display = 'block';
607
+ this._placeDropdown();
608
+ this._animateIn();
609
+ this._setupTemporaryEventHandlers();
610
+ }
540
611
 
541
- recalculateDimensions() {
542
- if (this.isOpen) {
543
- this.dropdownEl.style.width = '';
544
- this.dropdownEl.style.height = '';
545
- this.dropdownEl.style.left = '';
546
- this.dropdownEl.style.top = '';
547
- this.dropdownEl.style.transformOrigin = '';
548
- this._placeDropdown();
549
- }
612
+ /**
613
+ * Close dropdown.
614
+ */
615
+ close = () => {
616
+ if (!this.isOpen) return;
617
+ this.isOpen = false;
618
+ this.focusedIndex = -1;
619
+ // onCloseStart callback
620
+ if (typeof this.options.onCloseStart === 'function') {
621
+ this.options.onCloseStart.call(this, this.el);
622
+ }
623
+ this._animateOut();
624
+ this._removeTemporaryEventHandlers();
625
+ if (this.options.autoFocus) {
626
+ this.el.focus();
550
627
  }
628
+ }
551
629
 
552
- static {
553
- Dropdown._dropdowns = [];
630
+ /**
631
+ * While dropdown is open, you can recalculate its dimensions if its contents have changed.
632
+ */
633
+ recalculateDimensions = () => {
634
+ if (this.isOpen) {
635
+ this.dropdownEl.style.width = '';
636
+ this.dropdownEl.style.height = '';
637
+ this.dropdownEl.style.left = '';
638
+ this.dropdownEl.style.top = '';
639
+ this.dropdownEl.style.transformOrigin = '';
640
+ this._placeDropdown();
554
641
  }
555
642
  }
643
+
644
+ static {
645
+ Dropdown._dropdowns = [];
646
+ }
647
+ }