priority-nav 1.0.12 → 1.0.14

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.
@@ -1,724 +1,719 @@
1
- /*
2
- * priority-nav - v1.0.12 | (c) 2016 @gijsroge | MIT license
3
- * Repository: https://github.com/gijsroge/priority-navigation.git
4
- * Description: Priority+ pattern navigation that hides menu items if they don't fit on screen.
5
- * Demo: http://gijsroge.github.io/priority-nav.js/
6
- */
7
- (function (root, factory) {
8
- if (typeof define === "function" && define.amd) {
9
- define("priorityNav", factory(root));
10
- } else if (typeof exports === "object") {
11
- module.exports = factory(root);
12
- } else {
13
- root.priorityNav = factory(root);
14
- }
15
- })(window || this, function (root) {
16
-
17
- "use strict";
18
-
19
- /**
20
- * Variables
21
- */
22
- var priorityNav = {}; // Object for public APIs
23
- var breaks = []; // Object to store instances with breakpoints where the instances menu item"s didin"t fit.
24
- var supports = !!document.querySelector && !!root.addEventListener; // Feature test
25
- var settings = {};
26
- var instance = 0;
27
- var count = 0;
28
- var mainNavWrapper, totalWidth, restWidth, mainNav, navDropdown, navDropdownToggle, dropDownWidth, toggleWrapper;
29
- var viewportWidth = 0;
30
-
31
- /**
32
- * Default settings
33
- * @type {{initClass: string, navDropdown: string, navDropdownToggle: string, mainNavWrapper: string, moved: Function, movedBack: Function}}
34
- */
35
- var defaults = {
36
- initClass: "js-priorityNav", // Class that will be printed on html element to allow conditional css styling.
37
- mainNavWrapper: "nav", // mainnav wrapper selector (must be direct parent from mainNav)
38
- mainNav: "ul", // mainnav selector. (must be inline-block)
39
- navDropdownClassName: "nav__dropdown", // class used for the dropdown.
40
- navDropdownToggleClassName: "nav__dropdown-toggle", // class used for the dropdown toggle.
41
- navDropdownLabel: "more", // Text that is used for the dropdown toggle.
42
- navDropdownBreakpointLabel: "menu", //button label for navDropdownToggle when the breakPoint is reached.
43
- breakPoint: 500, //amount of pixels when all menu items should be moved to dropdown to simulate a mobile menu
44
- throttleDelay: 50, // this will throttle the calculating logic on resize because i'm a responsible dev.
45
- offsetPixels: 0, // increase to decrease the time it takes to move an item.
46
- count: true, // prints the amount of items are moved to the attribute data-count to style with css counter.
47
-
48
- //Callbacks
49
- moved: function () {
50
- },
51
- movedBack: function () {
52
- }
53
- };
54
-
55
-
56
- /**
57
- * A simple forEach() implementation for Arrays, Objects and NodeLists
58
- * @private
59
- * @param {Array|Object|NodeList} collection Collection of items to iterate
60
- * @param {Function} callback Callback function for each iteration
61
- * @param {Array|Object|NodeList} scope Object/NodeList/Array that forEach is iterating over (aka `this`)
62
- */
63
- var forEach = function (collection, callback, scope) {
64
- if (Object.prototype.toString.call(collection) === "[object Object]") {
65
- for (var prop in collection) {
66
- if (Object.prototype.hasOwnProperty.call(collection, prop)) {
67
- callback.call(scope, collection[prop], prop, collection);
68
- }
69
- }
70
- } else {
71
- for (var i = 0, len = collection.length; i < len; i++) {
72
- callback.call(scope, collection[i], i, collection);
73
- }
74
- }
75
- };
76
-
77
-
78
- /**
79
- * Get the closest matching element up the DOM tree
80
- * @param {Element} elem Starting element
81
- * @param {String} selector Selector to match against (class, ID, or data attribute)
82
- * @return {Boolean|Element} Returns false if not match found
83
- */
84
- var getClosest = function (elem, selector) {
85
- var firstChar = selector.charAt(0);
86
- for (; elem && elem !== document; elem = elem.parentNode) {
87
- if (firstChar === ".") {
88
- if (elem.classList.contains(selector.substr(1))) {
89
- return elem;
90
- }
91
- } else if (firstChar === "#") {
92
- if (elem.id === selector.substr(1)) {
93
- return elem;
94
- }
95
- } else if (firstChar === "[") {
96
- if (elem.hasAttribute(selector.substr(1, selector.length - 2))) {
97
- return elem;
98
- }
99
- }
100
- }
101
- return false;
102
- };
103
-
104
-
105
- /**
106
- * Merge defaults with user options
107
- * @private
108
- * @param {Object} defaults Default settings
109
- * @param {Object} options User options
110
- * @returns {Object} Merged values of defaults and options
111
- */
112
- var extend = function (defaults, options) {
113
- var extended = {};
114
- forEach(defaults, function (value, prop) {
115
- extended[prop] = defaults[prop];
116
- });
117
- forEach(options, function (value, prop) {
118
- extended[prop] = options[prop];
119
- });
120
- return extended;
121
- };
122
-
123
-
124
- /**
125
- * Debounced resize to throttle execution
126
- * @param func
127
- * @param wait
128
- * @param immediate
129
- * @returns {Function}
130
- */
131
- function debounce(func, wait, immediate) {
132
- var timeout;
133
- return function () {
134
- var context = this, args = arguments;
135
- var later = function () {
136
- timeout = null;
137
- if (!immediate) func.apply(context, args);
138
- };
139
- var callNow = immediate && !timeout;
140
- clearTimeout(timeout);
141
- timeout = setTimeout(later, wait);
142
- if (callNow) func.apply(context, args);
143
- };
144
- }
145
-
146
-
147
- /**
148
- * Toggle class on element
149
- * @param el
150
- * @param className
151
- */
152
- var toggleClass = function (el, className) {
153
- if (el.classList) {
154
- el.classList.toggle(className);
155
- } else {
156
- var classes = el.className.split(" ");
157
- var existingIndex = classes.indexOf(className);
158
-
159
- if (existingIndex >= 0)
160
- classes.splice(existingIndex, 1); else
161
- classes.push(className);
162
-
163
- el.className = classes.join(" ");
164
- }
165
- };
166
-
167
-
168
- /**
169
- * Check if dropdown menu is already on page before creating it
170
- * @param mainNavWrapper
171
- */
172
- var prepareHtml = function (_this, settings) {
173
-
174
- /**
175
- * Create dropdow menu
176
- * @type {HTMLElement}
177
- */
178
- toggleWrapper = document.createElement("span");
179
- navDropdown = document.createElement("ul");
180
- navDropdownToggle = document.createElement("button");
181
-
182
- /**
183
- * Set label for dropdown toggle
184
- * @type {string}
185
- */
186
- navDropdownToggle.innerHTML = settings.navDropdownLabel;
187
-
188
- /**
189
- * Set aria attributes for accessibility
190
- */
191
- navDropdownToggle.setAttribute("aria-controls", "menu");
192
- navDropdownToggle.setAttribute("type", "button");
193
- navDropdown.setAttribute("aria-hidden", "true");
194
-
195
-
196
- /**
197
- * Move elements to the right spot
198
- */
199
- if(_this.querySelector(mainNav).parentNode !== _this){
200
- console.warn("mainNav is not a direct child of mainNavWrapper, double check please");
201
- return;
202
- }
203
-
204
- _this.insertAfter(toggleWrapper, _this.querySelector(mainNav));
205
-
206
- toggleWrapper.appendChild(navDropdownToggle);
207
- toggleWrapper.appendChild(navDropdown);
208
-
209
- /**
210
- * Add classes so we can target elements
211
- */
212
- navDropdown.classList.add(settings.navDropdownClassName);
213
- navDropdown.classList.add("priority-nav__dropdown");
214
-
215
- navDropdownToggle.classList.add(settings.navDropdownToggleClassName);
216
- navDropdownToggle.classList.add("priority-nav__dropdown-toggle");
217
-
218
- toggleWrapper.classList.add(settings.navDropdownClassName+"-wrapper");
219
- toggleWrapper.classList.add("priority-nav__wrapper");
220
-
221
- _this.classList.add("priority-nav");
222
- };
223
-
224
-
225
- /**
226
- * Get innerwidth without padding
227
- * @param element
228
- * @returns {number}
229
- */
230
- var getElementContentWidth = function(element) {
231
- var styles = window.getComputedStyle(element);
232
- var padding = parseFloat(styles.paddingLeft) +
233
- parseFloat(styles.paddingRight);
234
-
235
- return element.clientWidth - padding;
236
- };
237
-
238
-
239
- /**
240
- * Get viewport size
241
- * @returns {{width: number, height: number}}
242
- */
243
- var viewportSize = function() {
244
- var doc = document, w = window;
245
- var docEl = (doc.compatMode && doc.compatMode === "CSS1Compat")?
246
- doc.documentElement: doc.body;
247
-
248
- var width = docEl.clientWidth;
249
- var height = docEl.clientHeight;
250
-
251
- // mobile zoomed in?
252
- if ( w.innerWidth && width > w.innerWidth ) {
253
- width = w.innerWidth;
254
- height = w.innerHeight;
255
- }
256
-
257
- return {width: width, height: height};
258
- };
259
-
260
-
261
- /**
262
- * Get width
263
- * @param elem
264
- * @returns {number}
265
- */
266
- var calculateWidths = function (_this) {
267
- totalWidth = getElementContentWidth(_this);
268
- //Check if parent is the navwrapper before calculating its width
269
- if (_this.querySelector(navDropdown).parentNode === _this) {
270
- dropDownWidth = _this.querySelector(navDropdown).offsetWidth;
271
- } else {
272
- dropDownWidth = 0;
273
- }
274
- restWidth = getChildrenWidth(_this) + settings.offsetPixels;
275
- viewportWidth = viewportSize().width;
276
- };
277
-
278
-
279
- /**
280
- * Move item to array
281
- * @param item
282
- */
283
- priorityNav.doesItFit = function (_this) {
284
-
285
- /**
286
- * Check if it is the first run
287
- */
288
- var delay = _this.getAttribute("instance") === 0 ? delay : settings.throttleDelay;
289
-
290
- /**
291
- * Increase instance
292
- */
293
- instance++;
294
-
295
- /**
296
- * Debounced execution of the main logic
297
- */
298
- (debounce(function () {
299
-
300
- /**
301
- * Get the current element"s instance
302
- * @type {string}
303
- */
304
- var identifier = _this.getAttribute("instance");
305
-
306
- /**
307
- * Update width
308
- */
309
- calculateWidths(_this);
310
-
311
- /**
312
- * Keep executing until all menu items that are overflowing are moved
313
- */
314
- while (totalWidth <= restWidth && _this.querySelector(mainNav).children.length > 0 || viewportWidth < settings.breakPoint && _this.querySelector(mainNav).children.length > 0) {
315
- //move item to dropdown
316
- priorityNav.toDropdown(_this, identifier);
317
- //recalculate widths
318
- calculateWidths(_this, identifier);
319
- //update dropdownToggle label
320
- if(viewportWidth < settings.breakPoint) updateLabel(_this, identifier, settings.navDropdownBreakpointLabel);
321
- }
322
-
323
- /**
324
- * Keep executing until all menu items that are able to move back are moved
325
- */
326
- while (totalWidth >= breaks[identifier][breaks[identifier].length - 1] && viewportWidth > settings.breakPoint) {
327
- //move item to menu
328
- priorityNav.toMenu(_this, identifier);
329
- //update dropdownToggle label
330
- if(viewportWidth > settings.breakPoint) updateLabel(_this, identifier, settings.navDropdownLabel);
331
- }
332
-
333
- /**
334
- * If there are no items in dropdown hide dropdown
335
- */
336
- if (breaks[identifier].length < 1) {
337
- _this.querySelector(navDropdown).classList.remove("show");
338
- //show navDropdownLabel
339
- updateLabel(_this, identifier, settings.navDropdownLabel);
340
- }
341
-
342
- /**
343
- * If there are no items in menu
344
- */
345
- if (_this.querySelector(mainNav).children.length < 1) {
346
- //show navDropdownBreakpointLabel
347
- _this.classList.add("is-empty");
348
- updateLabel(_this, identifier, settings.navDropdownBreakpointLabel);
349
- }else{
350
- _this.classList.remove("is-empty");
351
- }
352
-
353
- /**
354
- * Check if we need to show toggle menu button
355
- */
356
- showToggle(_this, identifier);
357
-
358
- }, delay ))();
359
- };
360
-
361
-
362
- /**
363
- * Show/hide toggle button
364
- */
365
- var showToggle = function (_this, identifier) {
366
- if (breaks[identifier].length < 1) {
367
- _this.querySelector(navDropdownToggle).classList.add("priority-nav-is-hidden");
368
- _this.querySelector(navDropdownToggle).classList.remove("priority-nav-is-visible");
369
- _this.classList.remove("priority-nav-has-dropdown");
370
-
371
- /**
372
- * Set aria attributes for accessibility
373
- */
374
- _this.querySelector(".priority-nav__wrapper").setAttribute("aria-haspopup", "false");
375
-
376
- } else {
377
- _this.querySelector(navDropdownToggle).classList.add("priority-nav-is-visible");
378
- _this.querySelector(navDropdownToggle).classList.remove("priority-nav-is-hidden");
379
- _this.classList.add("priority-nav-has-dropdown");
380
-
381
- /**
382
- * Set aria attributes for accessibility
383
- */
384
- _this.querySelector(".priority-nav__wrapper").setAttribute("aria-haspopup", "true");
385
- }
386
- };
387
-
388
-
389
- /**
390
- * Update count on dropdown toggle button
391
- */
392
- var updateCount = function (_this, identifier) {
393
- _this.querySelector(navDropdownToggle).setAttribute("priorityNav-count", breaks[identifier].length);
394
- };
395
-
396
- var updateLabel = function(_this, identifier, label){
397
- _this.querySelector(navDropdownToggle).innerHTML = label;
398
- };
399
-
400
-
401
- /**
402
- * Move item to dropdown
403
- */
404
- priorityNav.toDropdown = function (_this, identifier) {
405
-
406
-
407
- /**
408
- * move last child of navigation menu to dropdown
409
- */
410
- if (_this.querySelector(navDropdown).firstChild && _this.querySelector(mainNav).children.length > 0) {
411
- _this.querySelector(navDropdown).insertBefore(_this.querySelector(mainNav).lastElementChild, _this.querySelector(navDropdown).firstChild);
412
- } else if (_this.querySelector(mainNav).children.length > 0) {
413
- _this.querySelector(navDropdown).appendChild(_this.querySelector(mainNav).lastElementChild);
414
- }
415
-
416
- /**
417
- * store breakpoints
418
- */
419
- breaks[identifier].push(restWidth);
420
-
421
- /**
422
- * check if we need to show toggle menu button
423
- */
424
- showToggle(_this, identifier);
425
-
426
- /**
427
- * update count on dropdown toggle button
428
- */
429
- if (_this.querySelector(mainNav).children.length > 0 && settings.count) {
430
- updateCount(_this, identifier);
431
- }
432
-
433
- /**
434
- * If item has been moved to dropdown trigger the callback
435
- */
436
- settings.moved();
437
- };
438
-
439
-
440
- /**
441
- * Move item to menu
442
- */
443
- priorityNav.toMenu = function (_this, identifier) {
444
-
445
- /**
446
- * move last child of navigation menu to dropdown
447
- */
448
- if (_this.querySelector(navDropdown).children.length > 0) _this.querySelector(mainNav).appendChild(_this.querySelector(navDropdown).firstElementChild);
449
-
450
- /**
451
- * remove last breakpoint
452
- */
453
- breaks[identifier].pop();
454
-
455
- /**
456
- * Check if we need to show toggle menu button
457
- */
458
- showToggle(_this, identifier);
459
-
460
- /**
461
- * update count on dropdown toggle button
462
- */
463
- if (_this.querySelector(mainNav).children.length > 0 && settings.count) {
464
- updateCount(_this, identifier);
465
- }
466
-
467
- /**
468
- * If item has been moved back to the main menu trigger the callback
469
- */
470
- settings.movedBack();
471
- };
472
-
473
-
474
- /**
475
- * Count width of children and return the value
476
- * @param e
477
- */
478
- var getChildrenWidth = function (e) {
479
- var children = e.childNodes;
480
- var sum = 0;
481
- for (var i = 0; i < children.length; i++) {
482
- if (children[i].nodeType !== 3) {
483
- if(!isNaN(children[i].offsetWidth)){
484
- sum += children[i].offsetWidth;
485
- }
486
-
487
- }
488
- }
489
- return sum;
490
- };
491
-
492
-
493
-
494
- /**
495
- * Bind eventlisteners
496
- */
497
- var listeners = function (_this, settings) {
498
-
499
- // Check if an item needs to move
500
- if(window.attachEvent) {
501
- window.attachEvent("onresize", function() {
502
- if(priorityNav.doesItFit)priorityNav.doesItFit(_this);
503
- });
504
- }
505
- else if(window.addEventListener) {
506
- window.addEventListener("resize", function() {
507
- if(priorityNav.doesItFit)priorityNav.doesItFit(_this);
508
- }, true);
509
- }
510
-
511
- // Toggle dropdown
512
- _this.querySelector(navDropdownToggle).addEventListener("click", function () {
513
- toggleClass(_this.querySelector(navDropdown), "show");
514
- toggleClass(this, "is-open");
515
- toggleClass(_this, "is-open");
516
-
517
- /**
518
- * Toggle aria hidden for accessibility
519
- */
520
- if(-1 !== _this.className.indexOf( "is-open" )){
521
- _this.querySelector(navDropdown).setAttribute("aria-hidden", "false");
522
- }else{
523
- _this.querySelector(navDropdown).setAttribute("aria-hidden", "true");
524
- _this.querySelector(navDropdown).blur();
525
- }
526
- });
527
-
528
- /*
529
- * Remove when clicked outside dropdown
530
- */
531
- document.addEventListener("click", function (event) {
532
- if (!getClosest(event.target, "."+settings.navDropdownClassName) && event.target !== _this.querySelector(navDropdownToggle)) {
533
- _this.querySelector(navDropdown).classList.remove("show");
534
- _this.querySelector(navDropdownToggle).classList.remove("is-open");
535
- _this.classList.remove("is-open");
536
- }
537
- });
538
-
539
- /**
540
- * Remove when escape key is pressed
541
- */
542
- document.onkeydown = function (evt) {
543
- evt = evt || window.event;
544
- if (evt.keyCode === 27) {
545
- document.querySelector(navDropdown).classList.remove("show");
546
- document.querySelector(navDropdownToggle).classList.remove("is-open");
547
- mainNavWrapper.classList.remove("is-open");
548
- }
549
- };
550
- };
551
-
552
-
553
- /**
554
- * Remove function
555
- */
556
- Element.prototype.remove = function() {
557
- this.parentElement.removeChild(this);
558
- };
559
-
560
- /*global HTMLCollection */
561
- NodeList.prototype.remove = HTMLCollection.prototype.remove = function() {
562
- for(var i = 0, len = this.length; i < len; i++) {
563
- if(this[i] && this[i].parentElement) {
564
- this[i].parentElement.removeChild(this[i]);
565
- }
566
- }
567
- };
568
-
569
-
570
- /**
571
- * Destroy the current initialization.
572
- * @public
573
- */
574
- priorityNav.destroy = function () {
575
- // If plugin isn"t already initialized, stop
576
- if (!settings) return;
577
- // Remove feedback class
578
- document.documentElement.classList.remove(settings.initClass);
579
- // Remove toggle
580
- toggleWrapper.remove();
581
- // Remove settings
582
- settings = null;
583
- delete priorityNav.init;
584
- delete priorityNav.doesItFit;
585
- };
586
-
587
-
588
- /**
589
- * insertAfter function
590
- * @param n
591
- * @param r
592
- */
593
- if (supports && typeof Node !== "undefined"){
594
- Node.prototype.insertAfter = function(n,r) {this.insertBefore(n,r.nextSibling);};
595
- }
596
-
597
- var checkForSymbols = function(string){
598
- var firstChar = string.charAt(0);
599
- if (firstChar === "." || firstChar === "#") {
600
- return false;
601
- }else{
602
- return true;
603
- }
604
- };
605
-
606
-
607
- /**
608
- * Initialize Plugin
609
- * @public
610
- * @param {Object} options User settings
611
- */
612
- priorityNav.init = function (options) {
613
-
614
- /**
615
- * Merge user options with defaults
616
- * @type {Object}
617
- */
618
- settings = extend(defaults, options || {});
619
-
620
- // Feature test.
621
- if (!supports && typeof Node === "undefined"){
622
- console.warn("This browser doesn't support priorityNav");
623
- return;
624
- }
625
-
626
- // Options check
627
- if (!checkForSymbols(settings.navDropdownClassName) || !checkForSymbols(settings.navDropdownToggleClassName)){
628
- console.warn("No symbols allowed in navDropdownClassName & navDropdownToggleClassName. These are not selectors.");
629
- return;
630
- }
631
-
632
- /**
633
- * Store nodes
634
- * @type {NodeList}
635
- */
636
- var elements = document.querySelectorAll(settings.mainNavWrapper);
637
-
638
- /**
639
- * Loop over every instance and reference _this
640
- */
641
- forEach(elements, function(_this){
642
-
643
- /**
644
- * Create breaks array
645
- * @type {number}
646
- */
647
- breaks[count] = [];
648
-
649
- /**
650
- * Set the instance number as data attribute
651
- */
652
- _this.setAttribute("instance", count++);
653
-
654
- /**
655
- * Store the wrapper element
656
- */
657
- mainNavWrapper = _this;
658
- if (!mainNavWrapper) {
659
- console.warn("couldn't find the specified mainNavWrapper element");
660
- return;
661
- }
662
-
663
- /**
664
- * Store the menu elementStore the menu element
665
- */
666
- mainNav = settings.mainNav;
667
- if (!_this.querySelector(mainNav)) {
668
- console.warn("couldn't find the specified mainNav element");
669
- return;
670
- }
671
-
672
- /**
673
- * Check if we need to create the dropdown elements
674
- */
675
- prepareHtml(_this, settings);
676
-
677
- /**
678
- * Store the dropdown element
679
- */
680
- navDropdown = "."+settings.navDropdownClassName;
681
- if (!_this.querySelector(navDropdown)) {
682
- console.warn("couldn't find the specified navDropdown element");
683
- return;
684
- }
685
-
686
- /**
687
- * Store the dropdown toggle element
688
- */
689
- navDropdownToggle = "."+settings.navDropdownToggleClassName;
690
- if (!_this.querySelector(navDropdownToggle)) {
691
- console.warn("couldn't find the specified navDropdownToggle element");
692
- return;
693
- }
694
-
695
- /**
696
- * Event listeners
697
- */
698
- listeners(_this, settings);
699
-
700
- /**
701
- * Start first check
702
- */
703
- priorityNav.doesItFit(_this);
704
-
705
- });
706
-
707
- /**
708
- * Count amount of instances
709
- */
710
- instance++;
711
-
712
- /**
713
- * Add class to HTML element to activate conditional CSS
714
- */
715
- document.documentElement.classList.add(settings.initClass);
716
- };
717
-
718
-
719
- /**
720
- * Public APIs
721
- */
722
- return priorityNav;
723
-
724
- });
1
+ /*
2
+ * priority-nav - v1.0.13 | (c) 2025 @gijsroge | MIT license
3
+ * Repository: https://github.com/gijsroge/priority-navigation.git
4
+ * Description: Priority+ pattern navigation that hides menu items if they don't fit on screen.
5
+ * Demo: http://gijsroge.github.io/priority-nav.js/
6
+ */
7
+ (function (root, factory) {
8
+ if (typeof define === "function" && define.amd) {
9
+ define("priorityNav", factory(root));
10
+ } else if (typeof exports === "object") {
11
+ module.exports = factory(root);
12
+ } else {
13
+ root.priorityNav = factory(root);
14
+ }
15
+ })(window || this, function (root) {
16
+
17
+ "use strict";
18
+
19
+ /**
20
+ * Variables
21
+ */
22
+ var priorityNav = {}; // Object for public APIs
23
+ var breaks = []; // Object to store instances with breakpoints where the instances menu item"s didin"t fit.
24
+ var supports = !!document.querySelector && !!root.addEventListener; // Feature test
25
+ var settings = {};
26
+ var instance = 0;
27
+ var count = 0;
28
+ var mainNavWrapper, totalWidth, restWidth, mainNav, navDropdown, navDropdownToggle, dropDownWidth, toggleWrapper;
29
+ var viewportWidth = 0;
30
+
31
+ /**
32
+ * Default settings
33
+ * @type {{initClass: string, navDropdown: string, navDropdownToggle: string, mainNavWrapper: string, moved: Function, movedBack: Function}}
34
+ */
35
+ var defaults = {
36
+ initClass: "js-priorityNav", // Class that will be printed on html element to allow conditional css styling.
37
+ mainNavWrapper: "nav", // mainnav wrapper selector (must be direct parent from mainNav)
38
+ mainNav: "ul", // mainnav selector. (must be inline-block)
39
+ navDropdownClassName: "nav__dropdown", // class used for the dropdown.
40
+ navDropdownToggleClassName: "nav__dropdown-toggle", // class used for the dropdown toggle.
41
+ navDropdownLabel: "more", // Text that is used for the dropdown toggle.
42
+ navDropdownBreakpointLabel: "menu", //button label for navDropdownToggle when the breakPoint is reached.
43
+ breakPoint: 500, //amount of pixels when all menu items should be moved to dropdown to simulate a mobile menu
44
+ throttleDelay: 50, // this will throttle the calculating logic on resize because i'm a responsible dev.
45
+ offsetPixels: 0, // increase to decrease the time it takes to move an item.
46
+ count: true, // prints the amount of items are moved to the attribute data-count to style with css counter.
47
+
48
+ //Callbacks
49
+ moved: function () {
50
+ },
51
+ movedBack: function () {
52
+ }
53
+ };
54
+
55
+
56
+ /**
57
+ * A simple forEach() implementation for Arrays, Objects and NodeLists
58
+ * @private
59
+ * @param {Array|Object|NodeList} collection Collection of items to iterate
60
+ * @param {Function} callback Callback function for each iteration
61
+ * @param {Array|Object|NodeList} scope Object/NodeList/Array that forEach is iterating over (aka `this`)
62
+ */
63
+ var forEach = function (collection, callback, scope) {
64
+ if (Object.prototype.toString.call(collection) === "[object Object]") {
65
+ for (var prop in collection) {
66
+ if (Object.prototype.hasOwnProperty.call(collection, prop)) {
67
+ callback.call(scope, collection[prop], prop, collection);
68
+ }
69
+ }
70
+ } else {
71
+ for (var i = 0, len = collection.length; i < len; i++) {
72
+ callback.call(scope, collection[i], i, collection);
73
+ }
74
+ }
75
+ };
76
+
77
+
78
+ /**
79
+ * Get the closest matching element up the DOM tree
80
+ * @param {Element} elem Starting element
81
+ * @param {String} selector Selector to match against (class, ID, or data attribute)
82
+ * @return {Boolean|Element} Returns false if not match found
83
+ */
84
+ var getClosest = function (elem, selector) {
85
+ var firstChar = selector.charAt(0);
86
+ for (; elem && elem !== document; elem = elem.parentNode) {
87
+ if (firstChar === ".") {
88
+ if (elem.classList.contains(selector.substr(1))) {
89
+ return elem;
90
+ }
91
+ } else if (firstChar === "#") {
92
+ if (elem.id === selector.substr(1)) {
93
+ return elem;
94
+ }
95
+ } else if (firstChar === "[") {
96
+ if (elem.hasAttribute(selector.substr(1, selector.length - 2))) {
97
+ return elem;
98
+ }
99
+ }
100
+ }
101
+ return false;
102
+ };
103
+
104
+
105
+ /**
106
+ * Merge defaults with user options
107
+ * @private
108
+ * @param {Object} defaults Default settings
109
+ * @param {Object} options User options
110
+ * @returns {Object} Merged values of defaults and options
111
+ */
112
+ var extend = function (defaults, options) {
113
+ var extended = {};
114
+ forEach(defaults, function (value, prop) {
115
+ extended[prop] = defaults[prop];
116
+ });
117
+ forEach(options, function (value, prop) {
118
+ extended[prop] = options[prop];
119
+ });
120
+ return extended;
121
+ };
122
+
123
+
124
+ /**
125
+ * Debounced resize to throttle execution
126
+ * @param func
127
+ * @param wait
128
+ * @param immediate
129
+ * @returns {Function}
130
+ */
131
+ function debounce(func, wait, immediate) {
132
+ var timeout;
133
+ return function () {
134
+ var context = this, args = arguments;
135
+ var later = function () {
136
+ timeout = null;
137
+ if (!immediate) func.apply(context, args);
138
+ };
139
+ var callNow = immediate && !timeout;
140
+ clearTimeout(timeout);
141
+ timeout = setTimeout(later, wait);
142
+ if (callNow) func.apply(context, args);
143
+ };
144
+ }
145
+
146
+
147
+ /**
148
+ * Toggle class on element
149
+ * @param el
150
+ * @param className
151
+ */
152
+ var toggleClass = function (el, className) {
153
+ if (el.classList) {
154
+ el.classList.toggle(className);
155
+ } else {
156
+ var classes = el.className.split(" ");
157
+ var existingIndex = classes.indexOf(className);
158
+
159
+ if (existingIndex >= 0)
160
+ classes.splice(existingIndex, 1); else
161
+ classes.push(className);
162
+
163
+ el.className = classes.join(" ");
164
+ }
165
+ };
166
+
167
+
168
+ /**
169
+ * Check if dropdown menu is already on page before creating it
170
+ * @param mainNavWrapper
171
+ */
172
+ var prepareHtml = function (_this, settings) {
173
+
174
+ /**
175
+ * Create dropdow menu
176
+ * @type {HTMLElement}
177
+ */
178
+ toggleWrapper = document.createElement("span");
179
+ navDropdown = document.createElement("ul");
180
+ navDropdownToggle = document.createElement("button");
181
+
182
+ /**
183
+ * Set label for dropdown toggle
184
+ * @type {string}
185
+ */
186
+ navDropdownToggle.innerHTML = settings.navDropdownLabel;
187
+
188
+ /**
189
+ * Set aria attributes for accessibility
190
+ */
191
+ navDropdownToggle.setAttribute("aria-controls", "menu");
192
+ navDropdownToggle.setAttribute("type", "button");
193
+ navDropdown.setAttribute("aria-hidden", "true");
194
+
195
+
196
+ /**
197
+ * Move elements to the right spot
198
+ */
199
+ if(_this.querySelector(mainNav).parentNode !== _this){
200
+ console.warn("mainNav is not a direct child of mainNavWrapper, double check please");
201
+ return;
202
+ }
203
+
204
+ _this.insertAfter(toggleWrapper, _this.querySelector(mainNav));
205
+
206
+ toggleWrapper.appendChild(navDropdownToggle);
207
+ toggleWrapper.appendChild(navDropdown);
208
+
209
+ /**
210
+ * Add classes so we can target elements
211
+ */
212
+ navDropdown.classList.add(settings.navDropdownClassName);
213
+ navDropdown.classList.add("priority-nav__dropdown");
214
+
215
+ navDropdownToggle.classList.add(settings.navDropdownToggleClassName);
216
+ navDropdownToggle.classList.add("priority-nav__dropdown-toggle");
217
+
218
+ //fix so button is type="button" and do not submit forms
219
+ navDropdownToggle.setAttribute("type", "button");
220
+
221
+ toggleWrapper.classList.add(settings.navDropdownClassName+"-wrapper");
222
+ toggleWrapper.classList.add("priority-nav__wrapper");
223
+
224
+ _this.classList.add("priority-nav");
225
+ };
226
+
227
+
228
+ /**
229
+ * Get innerwidth without padding
230
+ * @param element
231
+ * @returns {number}
232
+ */
233
+ var getElementContentWidth = function(element) {
234
+ var styles = window.getComputedStyle(element);
235
+ var padding = parseFloat(styles.paddingLeft) +
236
+ parseFloat(styles.paddingRight);
237
+
238
+ return element.clientWidth - padding;
239
+ };
240
+
241
+
242
+ /**
243
+ * Get viewport size
244
+ * @returns {{width: number, height: number}}
245
+ */
246
+ var viewportSize = function() {
247
+ var doc = document, w = window;
248
+ var docEl = (doc.compatMode && doc.compatMode === "CSS1Compat")?
249
+ doc.documentElement: doc.body;
250
+
251
+ var width = docEl.clientWidth;
252
+ var height = docEl.clientHeight;
253
+
254
+ // mobile zoomed in?
255
+ if ( w.innerWidth && width > w.innerWidth ) {
256
+ width = w.innerWidth;
257
+ height = w.innerHeight;
258
+ }
259
+
260
+ return {width: width, height: height};
261
+ };
262
+
263
+
264
+ /**
265
+ * Get width
266
+ * @param elem
267
+ * @returns {number}
268
+ */
269
+ var calculateWidths = function (_this) {
270
+ totalWidth = getElementContentWidth(_this);
271
+ //Check if parent is the navwrapper before calculating its width
272
+ if (_this.querySelector(navDropdown).parentNode === _this) {
273
+ dropDownWidth = _this.querySelector(navDropdown).offsetWidth;
274
+ } else {
275
+ dropDownWidth = 0;
276
+ }
277
+ restWidth = getChildrenWidth(_this) + settings.offsetPixels;
278
+ viewportWidth = viewportSize().width;
279
+ };
280
+
281
+
282
+ /**
283
+ * Move item to array
284
+ * @param item
285
+ */
286
+ priorityNav.doesItFit = function (_this) {
287
+
288
+ /**
289
+ * Check if it is the first run
290
+ */
291
+ var delay = _this.getAttribute("instance") === 0 ? delay : settings.throttleDelay;
292
+
293
+ /**
294
+ * Increase instance
295
+ */
296
+ instance++;
297
+
298
+ /**
299
+ * Debounced execution of the main logic
300
+ */
301
+ (debounce(function () {
302
+
303
+ /**
304
+ * Get the current element"s instance
305
+ * @type {string}
306
+ */
307
+ var identifier = _this.getAttribute("instance");
308
+
309
+ /**
310
+ * Update width
311
+ */
312
+ calculateWidths(_this);
313
+
314
+ /**
315
+ * Keep executing until all menu items that are overflowing are moved
316
+ */
317
+ while (totalWidth <= restWidth && _this.querySelector(mainNav).children.length > 0 || viewportWidth < settings.breakPoint && _this.querySelector(mainNav).children.length > 0) {
318
+ //move item to dropdown
319
+ priorityNav.toDropdown(_this, identifier);
320
+ //recalculate widths
321
+ calculateWidths(_this, identifier);
322
+ //update dropdownToggle label
323
+ if(viewportWidth < settings.breakPoint) updateLabel(_this, identifier, settings.navDropdownBreakpointLabel);
324
+ }
325
+
326
+ /**
327
+ * Keep executing until all menu items that are able to move back are moved
328
+ */
329
+ while (totalWidth >= breaks[identifier][breaks[identifier].length - 1] && viewportWidth > settings.breakPoint) {
330
+ //move item to menu
331
+ priorityNav.toMenu(_this, identifier);
332
+ //update dropdownToggle label
333
+ if(viewportWidth > settings.breakPoint) updateLabel(_this, identifier, settings.navDropdownLabel);
334
+ }
335
+
336
+ /**
337
+ * If there are no items in dropdown hide dropdown
338
+ */
339
+ if (breaks[identifier].length < 1) {
340
+ _this.querySelector(navDropdown).classList.remove("show");
341
+ //show navDropdownLabel
342
+ updateLabel(_this, identifier, settings.navDropdownLabel);
343
+ }
344
+
345
+ /**
346
+ * If there are no items in menu
347
+ */
348
+ if (_this.querySelector(mainNav).children.length < 1) {
349
+ //show navDropdownBreakpointLabel
350
+ _this.classList.add("is-empty");
351
+ updateLabel(_this, identifier, settings.navDropdownBreakpointLabel);
352
+ }else{
353
+ _this.classList.remove("is-empty");
354
+ }
355
+
356
+ /**
357
+ * Check if we need to show toggle menu button
358
+ */
359
+ showToggle(_this, identifier);
360
+
361
+ }, delay ))();
362
+ };
363
+
364
+
365
+ /**
366
+ * Show/hide toggle button
367
+ */
368
+ var showToggle = function (_this, identifier) {
369
+ if (breaks[identifier].length < 1) {
370
+ _this.querySelector(navDropdownToggle).classList.add("priority-nav-is-hidden");
371
+ _this.querySelector(navDropdownToggle).classList.remove("priority-nav-is-visible");
372
+ _this.classList.remove("priority-nav-has-dropdown");
373
+
374
+ /**
375
+ * Set aria attributes for accessibility
376
+ */
377
+ _this.querySelector(".priority-nav__wrapper").setAttribute("aria-haspopup", "false");
378
+
379
+ } else {
380
+ _this.querySelector(navDropdownToggle).classList.add("priority-nav-is-visible");
381
+ _this.querySelector(navDropdownToggle).classList.remove("priority-nav-is-hidden");
382
+ _this.classList.add("priority-nav-has-dropdown");
383
+
384
+ /**
385
+ * Set aria attributes for accessibility
386
+ */
387
+ _this.querySelector(".priority-nav__wrapper").setAttribute("aria-haspopup", "true");
388
+ }
389
+ };
390
+
391
+
392
+ /**
393
+ * Update count on dropdown toggle button
394
+ */
395
+ var updateCount = function (_this, identifier) {
396
+ _this.querySelector(navDropdownToggle).setAttribute("priorityNav-count", breaks[identifier].length);
397
+ };
398
+
399
+ var updateLabel = function(_this, identifier, label){
400
+ _this.querySelector(navDropdownToggle).innerHTML = label;
401
+ };
402
+
403
+
404
+ /**
405
+ * Move item to dropdown
406
+ */
407
+ priorityNav.toDropdown = function (_this, identifier) {
408
+
409
+
410
+ /**
411
+ * move last child of navigation menu to dropdown
412
+ */
413
+ if (_this.querySelector(navDropdown).firstChild && _this.querySelector(mainNav).children.length > 0) {
414
+ _this.querySelector(navDropdown).insertBefore(_this.querySelector(mainNav).lastElementChild, _this.querySelector(navDropdown).firstChild);
415
+ } else if (_this.querySelector(mainNav).children.length > 0) {
416
+ _this.querySelector(navDropdown).appendChild(_this.querySelector(mainNav).lastElementChild);
417
+ }
418
+
419
+ /**
420
+ * store breakpoints
421
+ */
422
+ breaks[identifier].push(restWidth);
423
+
424
+ /**
425
+ * check if we need to show toggle menu button
426
+ */
427
+ showToggle(_this, identifier);
428
+
429
+ /**
430
+ * update count on dropdown toggle button
431
+ */
432
+ if (_this.querySelector(mainNav).children.length > 0 && settings.count) {
433
+ updateCount(_this, identifier);
434
+ }
435
+
436
+ /**
437
+ * If item has been moved to dropdown trigger the callback
438
+ */
439
+ settings.moved();
440
+ };
441
+
442
+
443
+ /**
444
+ * Move item to menu
445
+ */
446
+ priorityNav.toMenu = function (_this, identifier) {
447
+
448
+ /**
449
+ * move last child of navigation menu to dropdown
450
+ */
451
+ if (_this.querySelector(navDropdown).children.length > 0) _this.querySelector(mainNav).appendChild(_this.querySelector(navDropdown).firstElementChild);
452
+
453
+ /**
454
+ * remove last breakpoint
455
+ */
456
+ breaks[identifier].pop();
457
+
458
+ /**
459
+ * Check if we need to show toggle menu button
460
+ */
461
+ showToggle(_this, identifier);
462
+
463
+ /**
464
+ * update count on dropdown toggle button
465
+ */
466
+ if (_this.querySelector(mainNav).children.length > 0 && settings.count) {
467
+ updateCount(_this, identifier);
468
+ }
469
+
470
+ /**
471
+ * If item has been moved back to the main menu trigger the callback
472
+ */
473
+ settings.movedBack();
474
+ };
475
+
476
+
477
+ /**
478
+ * Count width of children and return the value
479
+ * @param e
480
+ */
481
+ var getChildrenWidth = function (e) {
482
+ var children = e.childNodes;
483
+ var sum = 0;
484
+ for (var i = 0; i < children.length; i++) {
485
+ if (children[i].nodeType !== 3) {
486
+ if(!isNaN(children[i].offsetWidth)){
487
+ sum += children[i].offsetWidth;
488
+ }
489
+
490
+ }
491
+ }
492
+ return sum;
493
+ };
494
+
495
+
496
+
497
+ /**
498
+ * Bind eventlisteners
499
+ */
500
+ var listeners = function (_this, settings) {
501
+
502
+ // Check if an item needs to move
503
+ if(window.attachEvent) {
504
+ window.attachEvent("onresize", function() {
505
+ if(priorityNav.doesItFit)priorityNav.doesItFit(_this);
506
+ });
507
+ }
508
+ else if(window.addEventListener) {
509
+ window.addEventListener("resize", function() {
510
+ if(priorityNav.doesItFit)priorityNav.doesItFit(_this);
511
+ }, true);
512
+ }
513
+
514
+ // Toggle dropdown
515
+ _this.querySelector(navDropdownToggle).addEventListener("click", function () {
516
+ toggleClass(_this.querySelector(navDropdown), "show");
517
+ toggleClass(this, "is-open");
518
+ toggleClass(_this, "is-open");
519
+
520
+ /**
521
+ * Toggle aria hidden for accessibility
522
+ */
523
+ if(-1 !== _this.className.indexOf( "is-open" )){
524
+ _this.querySelector(navDropdown).setAttribute("aria-hidden", "false");
525
+ }else{
526
+ _this.querySelector(navDropdown).setAttribute("aria-hidden", "true");
527
+ _this.querySelector(navDropdown).blur();
528
+ }
529
+ });
530
+
531
+ /*
532
+ * Remove when clicked outside dropdown
533
+ */
534
+ document.addEventListener("click", function (event) {
535
+ if (!getClosest(event.target, "."+settings.navDropdownClassName) && event.target !== _this.querySelector(navDropdownToggle)) {
536
+ _this.querySelector(navDropdown).classList.remove("show");
537
+ _this.querySelector(navDropdownToggle).classList.remove("is-open");
538
+ _this.classList.remove("is-open");
539
+ }
540
+ });
541
+
542
+ /**
543
+ * Remove when escape key is pressed
544
+ */
545
+ document.onkeydown = function (evt) {
546
+ evt = evt || window.event;
547
+ if (evt.keyCode === 27) {
548
+ document.querySelector(navDropdown).classList.remove("show");
549
+ document.querySelector(navDropdownToggle).classList.remove("is-open");
550
+ mainNavWrapper.classList.remove("is-open");
551
+ }
552
+ };
553
+ };
554
+
555
+ /*global HTMLCollection */
556
+ NodeList.prototype.remove = HTMLCollection.prototype.remove = function() {
557
+ for(var i = 0, len = this.length; i < len; i++) {
558
+ if(this[i] && this[i].parentElement) {
559
+ this[i].parentElement.removeChild(this[i]);
560
+ }
561
+ }
562
+ };
563
+
564
+
565
+ /**
566
+ * Destroy the current initialization.
567
+ * @public
568
+ */
569
+ priorityNav.destroy = function () {
570
+ // If plugin isn"t already initialized, stop
571
+ if (!settings) return;
572
+ // Remove feedback class
573
+ document.documentElement.classList.remove(settings.initClass);
574
+ // Remove toggle
575
+ toggleWrapper.remove();
576
+ // Remove settings
577
+ settings = null;
578
+ delete priorityNav.init;
579
+ delete priorityNav.doesItFit;
580
+ };
581
+
582
+
583
+ /**
584
+ * insertAfter function
585
+ * @param n
586
+ * @param r
587
+ */
588
+ if (supports && typeof Node !== "undefined"){
589
+ Node.prototype.insertAfter = function(n,r) {this.insertBefore(n,r.nextSibling);};
590
+ }
591
+
592
+ var checkForSymbols = function(string){
593
+ var firstChar = string.charAt(0);
594
+ if (firstChar === "." || firstChar === "#") {
595
+ return false;
596
+ }else{
597
+ return true;
598
+ }
599
+ };
600
+
601
+
602
+ /**
603
+ * Initialize Plugin
604
+ * @public
605
+ * @param {Object} options User settings
606
+ */
607
+ priorityNav.init = function (options) {
608
+
609
+ /**
610
+ * Merge user options with defaults
611
+ * @type {Object}
612
+ */
613
+ settings = extend(defaults, options || {});
614
+
615
+ // Feature test.
616
+ if (!supports && typeof Node === "undefined"){
617
+ console.warn("This browser doesn't support priorityNav");
618
+ return;
619
+ }
620
+
621
+ // Options check
622
+ if (!checkForSymbols(settings.navDropdownClassName) || !checkForSymbols(settings.navDropdownToggleClassName)){
623
+ console.warn("No symbols allowed in navDropdownClassName & navDropdownToggleClassName. These are not selectors.");
624
+ return;
625
+ }
626
+
627
+ /**
628
+ * Store nodes
629
+ * @type {NodeList}
630
+ */
631
+ var elements = document.querySelectorAll(settings.mainNavWrapper);
632
+
633
+ /**
634
+ * Loop over every instance and reference _this
635
+ */
636
+ forEach(elements, function(_this){
637
+
638
+ /**
639
+ * Create breaks array
640
+ * @type {number}
641
+ */
642
+ breaks[count] = [];
643
+
644
+ /**
645
+ * Set the instance number as data attribute
646
+ */
647
+ _this.setAttribute("instance", count++);
648
+
649
+ /**
650
+ * Store the wrapper element
651
+ */
652
+ mainNavWrapper = _this;
653
+ if (!mainNavWrapper) {
654
+ console.warn("couldn't find the specified mainNavWrapper element");
655
+ return;
656
+ }
657
+
658
+ /**
659
+ * Store the menu elementStore the menu element
660
+ */
661
+ mainNav = settings.mainNav;
662
+ if (!_this.querySelector(mainNav)) {
663
+ console.warn("couldn't find the specified mainNav element");
664
+ return;
665
+ }
666
+
667
+ /**
668
+ * Check if we need to create the dropdown elements
669
+ */
670
+ prepareHtml(_this, settings);
671
+
672
+ /**
673
+ * Store the dropdown element
674
+ */
675
+ navDropdown = "."+settings.navDropdownClassName;
676
+ if (!_this.querySelector(navDropdown)) {
677
+ console.warn("couldn't find the specified navDropdown element");
678
+ return;
679
+ }
680
+
681
+ /**
682
+ * Store the dropdown toggle element
683
+ */
684
+ navDropdownToggle = "."+settings.navDropdownToggleClassName;
685
+ if (!_this.querySelector(navDropdownToggle)) {
686
+ console.warn("couldn't find the specified navDropdownToggle element");
687
+ return;
688
+ }
689
+
690
+ /**
691
+ * Event listeners
692
+ */
693
+ listeners(_this, settings);
694
+
695
+ /**
696
+ * Start first check
697
+ */
698
+ priorityNav.doesItFit(_this);
699
+
700
+ });
701
+
702
+ /**
703
+ * Count amount of instances
704
+ */
705
+ instance++;
706
+
707
+ /**
708
+ * Add class to HTML element to activate conditional CSS
709
+ */
710
+ document.documentElement.classList.add(settings.initClass);
711
+ };
712
+
713
+
714
+ /**
715
+ * Public APIs
716
+ */
717
+ return priorityNav;
718
+
719
+ });