stacked_menu 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,929 @@
1
+ /**
2
+ * A flexible stacked navigation menu.
3
+ * @class
4
+ *
5
+ * @example <caption>The StackedMenu basic template looks like:</caption>
6
+ * <div id="stacked-menu" class="stacked-menu">
7
+ * <nav class="menu">
8
+ * <li class="menu-item">
9
+ * <a href="home.html" class="menu-link">
10
+ * <i class="menu-icon fa fa-home"></i>
11
+ * <span class="menu-text">Home</span>
12
+ * <span class="badge badge-danger">9+</span>
13
+ * </a>
14
+ * <li>
15
+ * </nav>
16
+ * </div>
17
+ *
18
+ * @example <caption>Instance the StackedMenu:</caption>
19
+ * var menus = new StackedMenu();
20
+ */
21
+ class StackedMenu {
22
+
23
+ /**
24
+ * Create a StackedMenu.
25
+ * @constructor
26
+ * @param {Object} options - An object containing key:value that representing the current StackedMenu.
27
+ */
28
+ constructor(options) {
29
+ /**
30
+ * The StackedMenu options.
31
+ * @type {Object}
32
+ * @property {Boolean} compact=false - Transform StackedMenu items (except item childs) to small size.
33
+ * @property {Boolean} hoverable=false - How StackedMenu triggered `open`/`close` state. Use `false` for hoverable and `true` for collapsible (clickable).
34
+ * @property {Boolean} closeOther=true - Control whether expanding an item will cause the other items to close. Only available when `hoverable=false`.
35
+ * @property {String} align='left' - Where StackedMenu items childs will open when `hoverable=true` (`left`/`right`).
36
+ * @property {String} selector='#stacked-menu' - The StackedMenu element selector.
37
+ * @property {String} selectorClass='stacked-menu' - The css class name that will be added to the StackedMenu and used for css prefix classes.
38
+ * @example
39
+ * var options = {
40
+ * closeOther: false,
41
+ * align: 'right',
42
+ * };
43
+ *
44
+ * var menus = new StackedMenu(options);
45
+ */
46
+ this.options = {
47
+ compact: false,
48
+ hoverable: false,
49
+ closeOther: true,
50
+ align: 'right',
51
+ selector: '#stacked-menu',
52
+ selectorClass: 'stacked-menu'
53
+ }
54
+
55
+ // mixed default and custom options
56
+ this.options = this._extend({}, this.options, options)
57
+
58
+ /**
59
+ * The StackedMenu element.
60
+ * @type {Element}
61
+ */
62
+ this.selector = document.querySelector(this.options.selector)
63
+
64
+ /**
65
+ * The StackedMenu items.
66
+ * @type {Element}
67
+ */
68
+ this.items = this.selector ? this.selector.querySelectorAll('.menu-item') : null
69
+
70
+ // forEach fallback
71
+ if (!Array.prototype.forEach) {
72
+ Array.prototype.forEach = function forEach(cb, arg) {
73
+ if(typeof cb !== 'function') throw new TypeError(`${cb} is not a function`)
74
+
75
+ let array = this
76
+ arg = arg || this
77
+ for(let i = 0; i < array.length; i++) {
78
+ cb.call(arg, array[i], i, array)
79
+ }
80
+ }
81
+ }
82
+ this.each = Array.prototype.forEach
83
+
84
+ /**
85
+ * Lists of feature classes that will be added to the StackedMenu depend to current options.
86
+ * Used selectorClass for prefix.
87
+ * @type {Object}
88
+ */
89
+ this.classes = {
90
+ alignLeft: this.options.selectorClass + '-has-left',
91
+ compact: this.options.selectorClass + '-has-compact',
92
+ collapsible: this.options.selectorClass + '-has-collapsible',
93
+ hoverable: this.options.selectorClass + '-has-hoverable',
94
+ hasChild: 'has-child',
95
+ hasActive: 'has-active',
96
+ hasOpen: 'has-open'
97
+ }
98
+
99
+ /** states element */
100
+ /**
101
+ * The active item.
102
+ * @type {Element}
103
+ */
104
+ this.active = null
105
+
106
+ /**
107
+ * The open item(s).
108
+ * @type {Element}
109
+ */
110
+ this.open = []
111
+
112
+ /**
113
+ * The StackedMenu element.
114
+ * @type {Boolean}
115
+ */
116
+ this.turbolinksAvailable = typeof window.Turbolinks === 'object' && window.Turbolinks.supported
117
+
118
+ /** event handlers */
119
+ this.handlerClickDoc = []
120
+ this.handlerOver = []
121
+ this.handlerOut = []
122
+ this.handlerClick = []
123
+
124
+ // Initialization
125
+ this.init()
126
+ }
127
+
128
+ /** Private methods */
129
+ /**
130
+ * Listen on document when the page is ready.
131
+ * @private
132
+ * @param {Function} handler - The callback function when page is ready.
133
+ * @return {void}
134
+ */
135
+ _onReady(handler) {
136
+ if(document.readyState != 'loading') {
137
+ handler()
138
+ } else {
139
+ document.addEventListener('DOMContentLoaded', handler, false)
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Handles clicking on menu leaves. Turbolinks friendly.
145
+ * @private
146
+ * @param {Object} self - The StackedMenu self instance.
147
+ * @return {void}
148
+ */
149
+ _handleNavigation(self) {
150
+ self.each.call(this.items, (el) => {
151
+ self._on(el, 'click', function(e) {
152
+ // Stop propagating the event to parent links
153
+ e.stopPropagation()
154
+ // if Turbolinks are available preventDefault immediatelly.
155
+ self.turbolinksAvailable ? e.preventDefault() : null
156
+ // if the element is "parent" and Turbolinks are not available,
157
+ // maintain the original behaviour. Otherwise navigate programmatically
158
+ if (self._hasChild(el)) {
159
+ self.turbolinksAvailable ? null : e.preventDefault()
160
+ } else {
161
+ self.turbolinksAvailable ? window.Turbolinks.visit(el.firstElementChild.href) : null
162
+ }
163
+ })
164
+ })
165
+ }
166
+
167
+
168
+ /**
169
+ * Merge the contents of two or more objects together into the first object.
170
+ * @private
171
+ * @param {Object} obj - An object containing additional properties to merge in.
172
+ * @return {Object} The merged object.
173
+ */
174
+ _extend(obj) {
175
+ obj = obj || {}
176
+ const args = arguments
177
+ for (let i = 1; i < args.length; i++) {
178
+ if (!args[i]) continue
179
+ for (let key in args[i]) {
180
+ if (args[i].hasOwnProperty(key))
181
+ obj[key] = args[i][key]
182
+ }
183
+ }
184
+ return obj
185
+ }
186
+
187
+ /**
188
+ * Attach an event to StackedMenu selector.
189
+ * @private
190
+ * @param {String} type - The name of the event (case-insensitive).
191
+ * @param {(Boolean|Number|String|Array|Object)} data - The custom data that will be added to event.
192
+ * @return {void}
193
+ */
194
+ _emit(type, data) {
195
+ let e
196
+ if (document.createEvent) {
197
+ e = document.createEvent('Event')
198
+ e.initEvent(type, true, true)
199
+ } else {
200
+ e = document.createEventObject()
201
+ e.eventType = type
202
+ }
203
+ e.eventName = type
204
+ e.data = data || this
205
+ // attach event to selector
206
+ document.createEvent
207
+ ? this.selector.dispatchEvent(e)
208
+ : this.selector.fireEvent('on' + type, e)
209
+ }
210
+
211
+ /**
212
+ * Bind one or two handlers to the element, to be executed when the mouse pointer enters and leaves the element.
213
+ * @private
214
+ * @param {Element} el - The target element.
215
+ * @param {Function} handlerOver - A function to execute when the mouse pointer enters the element.
216
+ * @param {Function} handlerOut - A function to execute when the mouse pointer leaves the element.
217
+ * @return {void}
218
+ */
219
+ _hover(el, handlerOver, handlerOut) {
220
+ if (el.tagName === 'A') {
221
+ this._on(el, 'focus', handlerOver)
222
+ this._on(el, 'blur', handlerOut)
223
+ } else {
224
+ this._on(el, 'mouseover', handlerOver)
225
+ this._on(el, 'mouseout', handlerOut)
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Registers the specified listener on the element.
231
+ * @private
232
+ * @param {Element} el - The target element.
233
+ * @param {String} type - The name of the event.
234
+ * @param {Function} handler - The callback function when event type is fired.
235
+ * @return {void}
236
+ */
237
+ _on(el, type, handler) {
238
+ let types = type.split(' ')
239
+ for (let i = 0; i < types.length; i++) {
240
+ el[window.addEventListener ? 'addEventListener' : 'attachEvent']( window.addEventListener ? types[i] : `on${types[i]}` , handler, false)
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Removes the event listener previously registered with [_on()]{@link StackedMenu#_on} method.
246
+ * @private
247
+ * @param {Element} el - The target element.
248
+ * @param {String} type - The name of the event.
249
+ * @param {Function} handler - The callback function when event type is fired.
250
+ * @return {void}
251
+ */
252
+ _off(el, type, handler) {
253
+ let types = type.split(' ')
254
+ for (let i = 0; i < types.length; i++) {
255
+ el[window.removeEventListener ? 'removeEventListener' : 'detachEvent']( window.removeEventListener ? types[i] : `on${types[i]}` , handler, false)
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Adds one or more class names to the target element.
261
+ * @private
262
+ * @param {Element} el - The target element.
263
+ * @param {String} className - Specifies one or more class names to be added.
264
+ * @return {void}
265
+ */
266
+ _addClass(el, className) {
267
+ let classes = className.split(' ')
268
+ for (let i = 0; i < classes.length; i++) {
269
+ if (el.classList) el.classList.add(classes[i])
270
+ else el.classes[i] += ' ' + classes[i]
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Removes one or more class names to the target element.
276
+ * @private
277
+ * @param {Element} el - The target element.
278
+ * @param {String} className - Specifies one or more class names to be added.
279
+ * @return {void}
280
+ */
281
+ _removeClass(el, className) {
282
+ let classes = className.split(' ')
283
+ for (let i = 0; i < classes.length; i++) {
284
+ if (el.classList) el.classList.remove(classes[i])
285
+ else el.classes[i] = el.classes[i].replace(new RegExp('(^|\\b)' + classes[i].split(' ').join('|') + '(\\b|$)', 'gi'), ' ')
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Determine whether the element is assigned the given class.
291
+ * @private
292
+ * @param {Element} el - The target element.
293
+ * @param {String} className - The class name to search for.
294
+ * @return {Boolean} is has className.
295
+ */
296
+ _hasClass(el, className) {
297
+ if (el.classList) return el.classList.contains(className)
298
+ return new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className)
299
+ }
300
+
301
+ /**
302
+ * Determine whether the element is a menu child.
303
+ * @private
304
+ * @param {Element} el - The target element.
305
+ * @return {Boolean} is has child.
306
+ */
307
+ _hasChild(el) {
308
+ return this._hasClass(el, this.classes.hasChild)
309
+ }
310
+
311
+ /**
312
+ * Determine whether the element is a active menu.
313
+ * @private
314
+ * @param {Element} el - The target element.
315
+ * @return {Boolean} is has active state.
316
+ */
317
+ _hasActive(el) {
318
+ return this._hasClass(el, this.classes.hasActive)
319
+ }
320
+
321
+ /**
322
+ * Determine whether the element is a open menu.
323
+ * @private
324
+ * @param {Element} el - The target element.
325
+ * @return {Boolean} is has open state.
326
+ */
327
+ _hasOpen(el) {
328
+ return this._hasClass(el, this.classes.hasOpen)
329
+ }
330
+
331
+ /**
332
+ * Determine whether the element is a level menu.
333
+ * @private
334
+ * @param {Element} el - The target element.
335
+ * @return {Boolean} is a level menu.
336
+ */
337
+ _isLevelMenu (el) {
338
+ return this._hasClass(el.parentNode.parentNode, this.options.selectorClass)
339
+ }
340
+
341
+ /**
342
+ * Attach an event to menu item depend on hoverable option.
343
+ * @private
344
+ * @param {Element} el - The target element.
345
+ * @param {Number} index - An array index from each menu item use to detach the current event.
346
+ * @return {void}
347
+ */
348
+ _menuTrigger(el, index) {
349
+ let elHover = el.querySelector('a')
350
+
351
+ // remove exist listener
352
+ this._off(el, 'mouseover', this.handlerOver[index])
353
+ this._off(el, 'mouseout', this.handlerOut[index])
354
+ this._off(elHover, 'focus', this.handlerOver[index])
355
+ this._off(elHover, 'blur', this.handlerOut[index])
356
+ this._off(el, 'click', this.handlerClick[index])
357
+
358
+ // handler listener
359
+ this.handlerOver[index] = this.openMenu.bind(this, el)
360
+ this.handlerOut[index] = this.closeMenu.bind(this, el)
361
+ this.handlerClick[index] = this.toggleMenu.bind(this, el)
362
+
363
+ // add listener
364
+ if (this.isHoverable()) {
365
+ if (this._hasChild(el)) {
366
+ this._hover(el, this.handlerOver[index], this.handlerOut[index])
367
+ this._hover(elHover, this.handlerOver[index], this.handlerOut[index])
368
+ }
369
+ } else {
370
+ this._on(el, 'click', this.handlerClick[index])
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Handle for menu items interactions.
376
+ * @private
377
+ * @param {Element} items - The element of menu items.
378
+ * @return {void}
379
+ */
380
+ _handleInteractions(items) {
381
+ const self = this
382
+
383
+ this.each.call(items, (el, i) => {
384
+ if (self._hasChild(el)) {
385
+ self._menuTrigger(el, i)
386
+ }
387
+
388
+ if(self._hasActive(el)) self.active = el
389
+ })
390
+ }
391
+
392
+ /**
393
+ * Get the parent menu item text of menu to be use on menu subhead.
394
+ * @private
395
+ * @param {Element} el - The target element.
396
+ * @return {void}
397
+ */
398
+ _getSubhead(el) {
399
+ return el.querySelector('.menu-text').textContent
400
+ }
401
+
402
+ /**
403
+ * Generate the subhead element for each child menu.
404
+ * @private
405
+ * @return {void}
406
+ */
407
+ _generateSubhead() {
408
+ const self = this
409
+ let menus = this.selector.children
410
+ let link, menu, subhead, label
411
+ this.each.call(menus, el => {
412
+ self.each.call(el.children, child => {
413
+ if (self._hasChild(child)) {
414
+ self.each.call(child.children, cc => {
415
+ if(self._hasClass(cc, 'menu-link')) link = cc
416
+ })
417
+
418
+ menu = link.nextElementSibling
419
+ subhead = document.createElement('li')
420
+ label = document.createTextNode(self._getSubhead(link))
421
+ subhead.appendChild(label)
422
+ self._addClass(subhead, 'menu-subhead')
423
+
424
+ menu.insertBefore(subhead, menu.firstChild)
425
+ }
426
+ })
427
+ })
428
+ }
429
+
430
+ /**
431
+ * Handle menu link tabindex depend on parent states.
432
+ * @return {void}
433
+ */
434
+ _handleTabIndex () {
435
+ const self = this
436
+ this.each.call(this.items, el => {
437
+ let container = el.parentNode.parentNode
438
+ if (!self._isLevelMenu(el)) {
439
+ el.querySelector('a').setAttribute('tabindex', '-1')
440
+ }
441
+ if (self._hasActive(container) || self._hasOpen(container)) {
442
+ el.querySelector('a').removeAttribute('tabindex')
443
+ }
444
+ })
445
+ }
446
+
447
+ /**
448
+ * Animate slide menu item.
449
+ * @private
450
+ * @param {Object} el - The target element.
451
+ * @param {String} direction - Up/Down slide direction.
452
+ * @param {Number} speed - Animation Speed in millisecond.
453
+ * @param {String} easing - CSS Animation effect.
454
+ * @return {Promise} resolve
455
+ */
456
+ _slide(el, direction, speed, easing) {
457
+ speed = speed || 300
458
+ easing = easing || 'ease'
459
+ let self = this
460
+ let menu = el.querySelector('.menu')
461
+ let es = window.getComputedStyle(el)['height']
462
+ // wait to resolve
463
+ let walkSpeed = speed + 50
464
+ // wait to clean style attribute
465
+ let clearSpeed = walkSpeed + 100
466
+
467
+ menu.style.transition = `height ${speed}ms ${easing}, opacity ${speed/2}ms ${easing}, visibility ${speed/2}ms ${easing}`
468
+
469
+ // slideDown
470
+ if (direction === 'down') {
471
+ // element
472
+ el.style.overflow = 'hidden'
473
+ el.style.height = es
474
+ // menu
475
+ menu.style.height = 'auto'
476
+ // get the current menu height
477
+ let height = window.getComputedStyle(menu)['height']
478
+ menu.style.height = 0
479
+ menu.style.visibility = 'hidden'
480
+ menu.style.opacity = 0
481
+ // remove element style
482
+ el.style.overflow = ''
483
+ el.style.height = ''
484
+
485
+ setTimeout(function() {
486
+ menu.style.height = height
487
+ menu.style.opacity = 1
488
+ menu.style.visibility = 'visible'
489
+ }, 0)
490
+ } else if (direction === 'up') {
491
+ // get the menu height
492
+ let height = window.getComputedStyle(menu)['height']
493
+ menu.style.height = height
494
+ menu.style.visibility = 'visible'
495
+ menu.style.opacity = 1
496
+
497
+ setTimeout(function() {
498
+ menu.style.height = 0
499
+ menu.style.visibility = 'hidden'
500
+ menu.style.opacity = 0
501
+ }, 0)
502
+ }
503
+
504
+ let done = new Promise(function(resolve) {
505
+ // remove the temporary styles
506
+ setTimeout(function() {
507
+ resolve(el)
508
+ // emit event
509
+ self._emit('menu:slide' + direction)
510
+ }, walkSpeed)
511
+ })
512
+
513
+ // remove styles after done has resolve
514
+ setTimeout(function() {
515
+ menu.removeAttribute('style')
516
+ }, clearSpeed)
517
+
518
+ return done
519
+ }
520
+
521
+ /** Public methods */
522
+ /**
523
+ * The first process that called after constructs the StackedMenu instance.
524
+ * @public
525
+ * @fires StackedMenu#menu:init
526
+ * @return {void}
527
+ */
528
+ init() {
529
+ const self = this
530
+ let opts = this.options
531
+
532
+ this._addClass(this.selector, opts.selectorClass)
533
+
534
+ // generate subhead
535
+ this._generateSubhead()
536
+
537
+ // implement compact feature
538
+ this.compact(opts.compact)
539
+ // implement hoverable feature
540
+ this.hoverable(opts.hoverable)
541
+
542
+ // handle menu link tabindex
543
+ this._handleTabIndex()
544
+
545
+ // handle menu click with or without Turbolinks
546
+ this._handleNavigation(self)
547
+
548
+ // close on outside click, only on collapsible with compact mode
549
+ this._on(document.body, 'click', function () {
550
+ if (!self.isHoverable() && self.isCompact()) {
551
+ // handle listener
552
+ self.closeAllMenu()
553
+ }
554
+ })
555
+
556
+ // on ready state
557
+ this._onReady(() => {
558
+
559
+ /**
560
+ * This event is fired when the Menu has completed init.
561
+ *
562
+ * @event StackedMenu#menu:init
563
+ * @type {Object}
564
+ * @property {Object} data - The StackedMenu data instance.
565
+ *
566
+ * @example
567
+ * document.querySelector('#stacked-menu').addEventListener('menu:init', function(e) {
568
+ * console.log(e.data);
569
+ * });
570
+ * @example <caption>Or using jQuery:</caption>
571
+ * $('#stacked-menu').on('menu:init', function() {
572
+ * console.log('fired on menu:init!!');
573
+ * });
574
+ */
575
+ self._emit('menu:init')
576
+ })
577
+ }
578
+
579
+ /**
580
+ * Open/show the target menu item. This method didn't take effect to an active item if not on compact mode.
581
+ * @public
582
+ * @fires StackedMenu#menu:open
583
+ * @param {Element} el - The target element.
584
+ * @param {Boolean} emiter - are the element will fire menu:open or not.
585
+ * @return {Object} The StackedMenu instance.
586
+ *
587
+ * @example
588
+ * var menuItem2 = menu.querySelectorAll('.menu-item.has-child')[1];
589
+ * menu.openMenu(menuItem2);
590
+ */
591
+ openMenu(el, emiter = true) {
592
+ // prevent open on active item if not on compact mode
593
+ if(this._hasActive(el) && !this.isCompact()) return
594
+ const self = this
595
+ let blockedSlide = this._isLevelMenu(el) && this.isCompact()
596
+
597
+ // open menu
598
+ if (this.isHoverable() || blockedSlide) {
599
+ this._addClass(el, this.classes.hasOpen)
600
+ // handle tabindex
601
+ this._handleTabIndex()
602
+ } else {
603
+ // slide down
604
+ this._slide(el, 'down', 150, 'linear').then(function() {
605
+ self._addClass(el, self.classes.hasOpen)
606
+ // handle tabindex
607
+ self._handleTabIndex()
608
+ })
609
+ }
610
+
611
+ this.open.push(el)
612
+
613
+ // child menu behavior
614
+ if (this.isHoverable() || (this.isCompact() && !this.hoverable())) {
615
+ const clientHeight = document.documentElement.clientHeight
616
+ const child = el.querySelector('.menu')
617
+ const pos = child.getBoundingClientRect()
618
+ const tolerance = pos.height - 20
619
+ const bottom = clientHeight - pos.top
620
+ const transformOriginX = this.options.align === 'left' ? '100%' : '0px'
621
+
622
+ if (pos.top >= 500 || tolerance >= bottom) {
623
+ child.style.top = 'auto'
624
+ child.style.bottom = 0
625
+ child.style.transformOrigin = `${transformOriginX} 100% 0`
626
+ }
627
+ }
628
+
629
+ /**
630
+ * This event is fired when the Menu has open.
631
+ *
632
+ * @event StackedMenu#menu:open
633
+ * @type {Object}
634
+ * @property {Object} data - The StackedMenu data instance.
635
+ *
636
+ * @example
637
+ * document.querySelector('#stacked-menu').addEventListener('menu:open', function(e) {
638
+ * console.log(e.data);
639
+ * });
640
+ * @example <caption>Or using jQuery:</caption>
641
+ * $('#stacked-menu').on('menu:open', function() {
642
+ * console.log('fired on menu:open!!');
643
+ * });
644
+ */
645
+ if (emiter) {
646
+ this._emit('menu:open')
647
+ }
648
+
649
+ return this
650
+ }
651
+
652
+ /**
653
+ * Close/hide the target menu item.
654
+ * @public
655
+ * @fires StackedMenu#menu:close
656
+ * @param {Element} el - The target element.
657
+ * @param {Boolean} emiter - are the element will fire menu:open or not.
658
+ * @return {Object} The StackedMenu instance.
659
+ *
660
+ * @example
661
+ * var menuItem2 = menu.querySelectorAll('.menu-item.has-child')[1];
662
+ * menu.closeMenu(menuItem2);
663
+ */
664
+ closeMenu(el, emiter = true) {
665
+ const self = this
666
+ let blockedSlide = this._isLevelMenu(el) && this.isCompact()
667
+ // open menu
668
+ if (this.isHoverable() || blockedSlide) {
669
+ this._removeClass(el, this.classes.hasOpen)
670
+ // handle tabindex
671
+ this._handleTabIndex()
672
+ } else {
673
+ if (!this._hasActive(el)) {
674
+ // slide up
675
+ this._slide(el, 'up', 150, 'linear').then(function() {
676
+ self._removeClass(el, self.classes.hasOpen)
677
+ // handle tabindex
678
+ self._handleTabIndex()
679
+ })
680
+ }
681
+ }
682
+
683
+ this.each.call(this.open, (v, i) => {
684
+ if (el == v) self.open.splice(i, 1)
685
+ })
686
+
687
+ // remove child menu behavior style
688
+ if (this.isHoverable() || (this.isCompact() && !this.hoverable())) {
689
+ const child = el.querySelector('.menu')
690
+
691
+ child.style.top = ''
692
+ child.style.bottom = ''
693
+ child.style.transformOrigin = ''
694
+ }
695
+
696
+ /**
697
+ * This event is fired when the Menu has close.
698
+ *
699
+ * @event StackedMenu#menu:close
700
+ * @type {Object}
701
+ * @property {Object} data - The StackedMenu data instance.
702
+ *
703
+ * @example
704
+ * document.querySelector('#stacked-menu').addEventListener('menu:close', function(e) {
705
+ * console.log(e.data);
706
+ * });
707
+ * @example <caption>Or using jQuery:</caption>
708
+ * $('#stacked-menu').on('menu:close', function() {
709
+ * console.log('fired on menu:close!!');
710
+ * });
711
+ */
712
+ if (emiter) {
713
+ this._emit('menu:close')
714
+ }
715
+
716
+ return this
717
+ }
718
+
719
+ /**
720
+ * Close all opened menu items.
721
+ * @public
722
+ * @fires StackedMenu#menu:close
723
+ * @return {Object} The StackedMenu instance.
724
+ *
725
+ * @example
726
+ * menu.closeAllMenu();
727
+ */
728
+ closeAllMenu() {
729
+ const self = this
730
+ this.each.call(this.items, el => {
731
+ if (self._hasOpen(el)) {
732
+ self.closeMenu(el, false)
733
+ }
734
+ })
735
+
736
+ return this
737
+ }
738
+
739
+ /**
740
+ * Toggle open/close the target menu item.
741
+ * @public
742
+ * @fires StackedMenu#menu:open
743
+ * @fires StackedMenu#menu:close
744
+ * @param {Element} el - The target element.
745
+ * @return {Object} The StackedMenu instance.
746
+ *
747
+ * @example
748
+ * var menuItem2 = menu.querySelectorAll('.menu-item.has-child')[1];
749
+ * menu.toggleMenu(menuItem2);
750
+ */
751
+ toggleMenu(el) {
752
+ const method = this._hasOpen(el) ? 'closeMenu': 'openMenu'
753
+ const self = this
754
+ let itemParent, elParent
755
+
756
+ // close other
757
+ this.each.call(this.items, item => {
758
+ itemParent = item.parentNode.parentNode
759
+ itemParent = self._hasClass(itemParent, 'menu-item') ? itemParent : itemParent.parentNode
760
+ elParent = el.parentNode.parentNode
761
+ elParent = self._hasClass(elParent, 'menu-item') ? elParent : elParent.parentNode
762
+
763
+ // close other except parents that has open state and an active item
764
+ if(!self._hasOpen(elParent) && self._hasChild(itemParent)) {
765
+ if (self.options.closeOther || (!self.options.closeOther && self.isCompact())) {
766
+ if (self._hasOpen(itemParent)) {
767
+ self.closeMenu(itemParent, false)
768
+ }
769
+ }
770
+ }
771
+ })
772
+ // open target el
773
+ if (this._hasChild(el)) this[method](el)
774
+
775
+ return this
776
+ }
777
+
778
+ /**
779
+ * Set the open menu position to `left` or `right`.
780
+ * @public
781
+ * @fires StackedMenu#menu:align
782
+ * @param {String} position - The position that will be set to the Menu.
783
+ * @return {Object} The StackedMenu instance.
784
+ *
785
+ * @example
786
+ * menu.align('left');
787
+ */
788
+ align(position) {
789
+ const method = (position === 'left') ? '_addClass': '_removeClass'
790
+ const classes = this.classes
791
+
792
+ this[method](this.selector, classes.alignLeft)
793
+
794
+ this.options.align = position
795
+
796
+ /**
797
+ * This event is fired when the Menu has changed align position.
798
+ *
799
+ * @event StackedMenu#menu:align
800
+ * @type {Object}
801
+ * @property {Object} data - The StackedMenu data instance.
802
+ *
803
+ * @example
804
+ * document.querySelector('#stacked-menu').addEventListener('menu:align', function(e) {
805
+ * console.log(e.data);
806
+ * });
807
+ * @example <caption>Or using jQuery:</caption>
808
+ * $('#stacked-menu').on('menu:align', function() {
809
+ * console.log('fired on menu:align!!');
810
+ * });
811
+ */
812
+ this._emit('menu:align')
813
+
814
+ return this
815
+ }
816
+
817
+ /**
818
+ * Determine whether the Menu is currently compact.
819
+ * @public
820
+ * @return {Boolean} is compact.
821
+ *
822
+ * @example
823
+ * var isCompact = menu.isCompact();
824
+ */
825
+ isCompact() {
826
+ return this.options.compact
827
+ }
828
+
829
+ /**
830
+ * Toggle the Menu compact mode.
831
+ * @public
832
+ * @fires StackedMenu#menu:compact
833
+ * @param {Boolean} isCompact - The compact mode.
834
+ * @return {Object} The StackedMenu instance.
835
+ *
836
+ * @example
837
+ * menu.compact(true);
838
+ */
839
+ compact(isCompact) {
840
+ const method = (isCompact) ? '_addClass': '_removeClass'
841
+ const classes = this.classes
842
+
843
+ this[method](this.selector, classes.compact)
844
+
845
+ this.options.compact = isCompact
846
+ // reset interactions
847
+ this._handleInteractions(this.items)
848
+
849
+ /**
850
+ * This event is fired when the Menu has completed toggle compact mode.
851
+ *
852
+ * @event StackedMenu#menu:compact
853
+ * @type {Object}
854
+ * @property {Object} data - The StackedMenu data instance.
855
+ *
856
+ * @example
857
+ * document.querySelector('#stacked-menu').addEventListener('menu:compact', function(e) {
858
+ * console.log(e.data);
859
+ * });
860
+ * @example <caption>Or using jQuery:</caption>
861
+ * $('#stacked-menu').on('menu:compact', function() {
862
+ * console.log('fired on menu:compact!!');
863
+ * });
864
+ */
865
+ this._emit('menu:compact')
866
+
867
+ return this
868
+ }
869
+
870
+ /**
871
+ * Determine whether the Menu is currently hoverable.
872
+ * @public
873
+ * @return {Boolean} is hoverable.
874
+ *
875
+ * @example
876
+ * var isHoverable = menu.isHoverable();
877
+ */
878
+ isHoverable() {
879
+ return this.options.hoverable
880
+ }
881
+
882
+ /**
883
+ * Toggle the Menu (interaction) hoverable.
884
+ * @public
885
+ * @fires StackedMenu#menu:hoverable
886
+ * @param {Boolean} isHoverable - `true` for hoverable and `false` for collapsible (clickable).
887
+ * @return {Object} The StackedMenu instance.
888
+ *
889
+ * @example
890
+ * menu.hoverable(true);
891
+ */
892
+ hoverable(isHoverable) {
893
+ const classes = this.classes
894
+
895
+ if (isHoverable) {
896
+ this._addClass(this.selector, classes.hoverable)
897
+ this._removeClass(this.selector, classes.collapsible)
898
+ } else {
899
+ this._addClass(this.selector, classes.collapsible)
900
+ this._removeClass(this.selector, classes.hoverable)
901
+ }
902
+
903
+ this.options.hoverable = isHoverable
904
+ // reset interactions
905
+ this._handleInteractions(this.items)
906
+
907
+ /**
908
+ * This event is fired when the Menu has completed toggle hoverable.
909
+ *
910
+ * @event StackedMenu#menu:hoverable
911
+ * @type {Object}
912
+ * @property {Object} data - The StackedMenu data instance.
913
+ *
914
+ * @example
915
+ * document.querySelector('#stacked-menu').addEventListener('menu:hoverable', function(e) {
916
+ * console.log(e.data);
917
+ * });
918
+ * @example <caption>Or using jQuery:</caption>
919
+ * $('#stacked-menu').on('menu:hoverable', function() {
920
+ * console.log('fired on menu:hoverable!!');
921
+ * });
922
+ */
923
+ this._emit('menu:hoverable')
924
+
925
+ return this
926
+ }
927
+ }
928
+
929
+ export default StackedMenu