stacked_menu 0.1.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.
@@ -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