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