PageSourceSearch

https://notionpress.com/assets/store/multi-level-menu/js/main.js?642784464

js notionpress.com collected 2026-10-01 10:13:43 UTC 12,527 bytes, 399 lines download raw bytes

1;(function(window) {
2
3	'use strict';
4
5	var support = { animations : Modernizr.cssanimations },
6		animEndEventNames = { 'WebkitAnimation' : 'webkitAnimationEnd', 'OAnimation' : 'oAnimationEnd', 'msAnimation' : 'MSAnimationEnd', 'animation' : 'animationend' },
7		animEndEventName = animEndEventNames[ Modernizr.prefixed( 'animation' ) ],
8		onEndAnimation = function( el, callback ) {
9			var onEndCallbackFn = function( ev ) {
10				if( support.animations ) {
11					if( ev.target != this ) return;
12					this.removeEventListener( animEndEventName, onEndCallbackFn );
13				}
14				if( callback && typeof callback === 'function' ) { callback.call(); }
15			};
16			if( support.animations ) {
17				el.addEventListener( animEndEventName, onEndCallbackFn );
18			}
19			else {
20				onEndCallbackFn();
21			}
22		};
23
24	function extend( a, b ) {
25		for( var key in b ) { 
26			if( b.hasOwnProperty( key ) ) {
27				a[key] = b[key];
28			}
29		}
30		return a;
31	}
32
33	function MLMenu(el, options) {
34		this.el = el;
35		this.options = extend( {}, this.options );
36		extend( this.options, options );
37		
38		// the menus (<ul>´s)
39		this.menus = [].slice.call(this.el.querySelectorAll('.menu__level'));
40
41		// index of current menu
42		// Each level is actually a different menu so 0 is root, 1 is sub-1, 2 sub-2, etc.
43		this.current_menu = 0;
44
45		/* Determine what current menu actually is */
46		var current_menu;
47		this.menus.forEach(function(menuEl, pos) {
48			var items = menuEl.querySelectorAll('.menu__item');
49			items.forEach(function(itemEl, iPos) {
50				var currentLink = itemEl.querySelector('.menu__link--current');
51				if (currentLink) {
52					// This is the actual menu__level that should have current
53					current_menu = pos;
54				}
55			});
56		});
57
58		if (current_menu) {
59			this.current_menu = current_menu;	
60		}
61
62		this._init();
63	}
64
65	MLMenu.prototype.options = {
66		// show breadcrumbs
67		breadcrumbsCtrl : true,
68		// initial breadcrumb text
69		initialBreadcrumb : home_landing_txt,
70		// show back button
71		backCtrl : true,
72		// delay between each menu item sliding animation
73		itemsDelayInterval : 60,
74		// direction 
75		direction : 'r2l',
76		// callback: item that doesn´t have a submenu gets clicked
77		// onItemClick([event], [inner HTML of the clicked item])
78		onItemClick : function(ev, itemName) { return false; }
79	};
80
81	MLMenu.prototype._init = function() {
82		// iterate the existing menus and create an array of menus, 
83		// more specifically an array of objects where each one holds the info of each menu element and its menu items
84		this.menusArr = [];
85		this.breadCrumbs = false;
86		var self = this;
87		var submenus = [];
88
89		/* Loops over root level menu items */
90		this.menus.forEach(function(menuEl, pos) {
91			var menu = {menuEl : menuEl, menuItems : [].slice.call(menuEl.querySelectorAll('.menu__item'))};
92			
93			self.menusArr.push(menu);
94
95			// set current menu class
96			if( pos === self.current_menu ) {
97				classie.add(menuEl, 'menu__level--current');
98			}
99
100			var menu_x = menuEl.getAttribute('data-menu');
101			var links = menuEl.querySelectorAll('.menu__link');
102			links.forEach(function(linkEl, lPos) {
103				var submenu = linkEl.getAttribute('data-submenu');
104				if (submenu) {
105					var pushMe = {"menu":submenu, "name": linkEl.innerHTML };
106					if (submenus[pos]) {
107						submenus[pos].push(pushMe);
108					} else {
109						submenus[pos] = []
110						submenus[pos].push(pushMe);
111					}
112				}
113			});
114		});
115
116		/* For each MENU, find their parent MENU */		
117		this.menus.forEach(function(menuEl, pos) {
118			var menu_x = menuEl.getAttribute('data-menu');
119			submenus.forEach(function(subMenuEl, menu_root) {
120				subMenuEl.forEach(function(subMenuItem, subPos) {
121					if (subMenuItem.menu == menu_x) {
122						self.menusArr[pos].backIdx = menu_root;
123						self.menusArr[pos].name = subMenuItem.name;
124					}
125				});
126			});
127		});
128
129		// create breadcrumbs
130		if( self.options.breadcrumbsCtrl ) {
131			this.breadcrumbsCtrl = document.createElement('nav');
132			this.breadcrumbsCtrl.className = 'menu__breadcrumbs noBack';
133			this.breadcrumbsCtrl.setAttribute('aria-label', 'You are here');
134			this.el.insertBefore(this.breadcrumbsCtrl, this.el.firstChild);
135			// add initial breadcrumb
136			this._addBreadcrumb(0);
137
138			// Need to add breadcrumbs for all parents of current submenu
139			if (self.menusArr[self.current_menu].backIdx != 0 && self.current_menu != 0) {
140				this._crawlCrumbs(self.menusArr[self.current_menu].backIdx, self.menusArr);
141				this.breadCrumbs = true;
142			}
143
144			// Create current submenu breadcrumb
145			if (self.current_menu != 0) {
146				this._addBreadcrumb(self.current_menu);
147				this.breadCrumbs = true;
148			}
149		}
150
151		// create back button
152		this.options.backCtrl = 1;
153		if (this.options.backCtrl) {
154			
155			this.backCtrl = document.createElement('button');
156			if (this.breadCrumbs) {
157				this.backCtrl.className = 'menu__back';	
158			} else {
159				this.backCtrl.className = 'menu__back menu__back--hidden';
160			}
161			this.backCtrl.setAttribute('aria-label', 'Go back');
162			this.backCtrl.innerHTML = '<span class="icon icon--arrow-left"></span>';
163			this.el.insertBefore(this.backCtrl, this.el.firstChild);
164		}
165
166		// event binding
167		this._initEvents();
168	};
169
170	MLMenu.prototype._initEvents = function() {
171		var self = this;
172
173		for(var i = 0, len = this.menusArr.length; i < len; ++i) {
174			this.menusArr[i].menuItems.forEach(function(item, pos) {
175				item.querySelector('a').addEventListener('click', function(ev) {
176					
177					var submenu = ev.target.getAttribute('data-submenu'),
178						itemName = ev.target.innerHTML,
179						subMenuEl = self.el.querySelector('ul[data-menu="' + submenu + '"]');
180
181
182					// check if there's a sub menu for this item
183					if( submenu && subMenuEl ) {
184						ev.preventDefault();
185						// open it
186						self._openSubMenu(subMenuEl, pos, itemName);
187					}
188					else {
189						return false;
190						// add class current
191						var currentlink = self.el.querySelector('.menu__link--current');
192						if( currentlink ) {
193							classie.remove(self.el.querySelector('.menu__link--current'), 'menu__link--current');
194						}
195						classie.add(ev.target, 'menu__link--current');
196						
197						// callback
198						self.options.onItemClick(ev, itemName);
199					}
200				});
201			});
202		}
203		
204		// back navigation
205		if( this.options.backCtrl ) {
206			
207			this.backCtrl.addEventListener('click', function() {
208				self._back();
209			});
210		}
211
212		$('.menu__breadcrumbs').click(function(){ 
213			if( $('.menu__breadcrumbs').hasClass('noBack') )
214				window.location.href = $('.np_logo').attr('href');
215		});
216	};
217
218	MLMenu.prototype._openSubMenu = function(subMenuEl, clickPosition, subMenuName) {
219		
220		if( this.isAnimating ) {
221			return false;
222		}
223		this.isAnimating = true;
224		
225		// save "parent" menu index for back navigation
226		this.menusArr[this.menus.indexOf(subMenuEl)].backIdx = this.current_menu;
227		// save "parent" menu´s name
228		this.menusArr[this.menus.indexOf(subMenuEl)].name = subMenuName;
229		// current menu slides out
230		this._menuOut(clickPosition);
231		// next menu (submenu) slides in
232		this._menuIn(subMenuEl, clickPosition);
233	};
234
235	MLMenu.prototype._back = function() {
236		
237		if( this.isAnimating ) {
238			return false;
239		}
240		this.isAnimating = true;
241
242		// current menu slides out
243		this._menuOut();
244		// next menu (previous menu) slides in
245		var backMenu = this.menusArr[this.menusArr[this.current_menu].backIdx].menuEl;
246		this._menuIn(backMenu);
247
248		// remove last breadcrumb
249		if( this.options.breadcrumbsCtrl ) {
250			this.breadcrumbsCtrl.removeChild(this.breadcrumbsCtrl.lastElementChild);
251		}
252	};
253
254	MLMenu.prototype._menuOut = function(clickPosition) {
255		
256		// the current menu
257		var self = this,
258			currentMenu = this.menusArr[this.current_menu].menuEl,
259			isBackNavigation = typeof clickPosition == 'undefined' ? true : false;
260
261		// slide out current menu items - first, set the delays for the items
262		this.menusArr[this.current_menu].menuItems.forEach(function(item, pos) {
263			item.style.WebkitAnimationDelay = item.style.animationDelay = isBackNavigation ? parseInt(pos * self.options.itemsDelayInterval) + 'ms' : parseInt(Math.abs(clickPosition - pos) * self.options.itemsDelayInterval) + 'ms';
264		});
265		// animation class
266		if( this.options.direction === 'r2l' ) {
267			classie.add(currentMenu, !isBackNavigation ? 'animate-outToLeft' : 'animate-outToRight');
268		}
269		else {
270			classie.add(currentMenu, isBackNavigation ? 'animate-outToLeft' : 'animate-outToRight');	
271		}
272	};
273
274	MLMenu.prototype._menuIn = function(nextMenuEl, clickPosition) {
275		
276		var self = this,
277			// the current menu
278			currentMenu = this.menusArr[this.current_menu].menuEl,
279			isBackNavigation = typeof clickPosition == 'undefined' ? true : false,
280			// index of the nextMenuEl
281			nextMenuIdx = this.menus.indexOf(nextMenuEl),
282
283			nextMenu = this.menusArr[nextMenuIdx],
284			nextMenuEl = nextMenu.menuEl,
285			nextMenuItems = nextMenu.menuItems,
286			nextMenuItemsTotal = nextMenuItems.length;
287
288		// slide in next menu items - first, set the delays for the items
289		nextMenuItems.forEach(function(item, pos) {
290			item.style.WebkitAnimationDelay = item.style.animationDelay = isBackNavigation ? parseInt(pos * self.options.itemsDelayInterval) + 'ms' : parseInt(Math.abs(clickPosition - pos) * self.options.itemsDelayInterval) + 'ms';
291
292			// we need to reset the classes once the last item animates in
293			// the "last item" is the farthest from the clicked item
294			// let's calculate the index of the farthest item
295			var farthestIdx = clickPosition <= nextMenuItemsTotal/2 || isBackNavigation ? nextMenuItemsTotal - 1 : 0;
296
297			if( pos === farthestIdx ) {
298				onEndAnimation(item, function() {
299					// reset classes
300					if( self.options.direction === 'r2l' ) {
301						classie.remove(currentMenu, !isBackNavigation ? 'animate-outToLeft' : 'animate-outToRight');
302						classie.remove(nextMenuEl, !isBackNavigation ? 'animate-inFromRight' : 'animate-inFromLeft');
303					}
304					else {
305						classie.remove(currentMenu, isBackNavigation ? 'animate-outToLeft' : 'animate-outToRight');
306						classie.remove(nextMenuEl, isBackNavigation ? 'animate-inFromRight' : 'animate-inFromLeft');
307					}
308					classie.remove(currentMenu, 'menu__level--current');
309					classie.add(nextMenuEl, 'menu__level--current');
310
311					//reset current
312					self.current_menu = nextMenuIdx;
313					self.options.backCtrl = 1;
314					// control back button and breadcrumbs navigation elements
315					if( !isBackNavigation ) {
316						// show back button
317						//self.options.backCtrl = 1;
318						if( self.options.backCtrl ) {
319							classie.remove(self.backCtrl, 'menu__back--hidden');
320							$('nav.menu__breadcrumbs').removeClass('noBack').addClass('yesBack');
321						}
322						
323						// add breadcrumb
324						self._addBreadcrumb(nextMenuIdx);
325					}
326					else if( self.current_menu === 0 && self.options.backCtrl ) {
327						// hide back button
328						classie.add(self.backCtrl, 'menu__back--hidden');
329						$('nav.menu__breadcrumbs').addClass('noBack').removeClass('yesBack');
330
331					}
332
333					// we can navigate again..
334					self.isAnimating = false;
335
336					// focus retention
337					nextMenuEl.focus();
338				});
339			}
340		});
341
342		// animation class
343		if( this.options.direction === 'r2l' ) {
344			classie.add(nextMenuEl, !isBackNavigation ? 'animate-inFromRight' : 'animate-inFromLeft');
345		}
346		else {
347			classie.add(nextMenuEl, isBackNavigation ? 'animate-inFromRight' : 'animate-inFromLeft');
348		}
349	};
350
351	MLMenu.prototype._addBreadcrumb = function(idx) {
352		
353		if( !this.options.breadcrumbsCtrl ) {
354			return false;
355		}
356
357		var bc = document.createElement('a');
358		bc.href = '#'; // make it focusable
359		if(idx == 0)
360			bc.setAttribute('class','menu__link _home-icon'); // make it focusable
361		bc.innerHTML = idx ? this.menusArr[idx].name : this.options.initialBreadcrumb;
362		this.breadcrumbsCtrl.appendChild(bc);
363
364		var self = this;
365		bc.addEventListener('click', function(ev) {
366			ev.preventDefault();
367
368			// do nothing if this breadcrumb is the last one in the list of breadcrumbs
369			if( !bc.nextSibling || self.isAnimating ) {
370				return false;
371			}
372			self.isAnimating = true;
373			// current menu slides out
374			self._menuOut();
375			// next menu slides in
376			var nextMenu = self.menusArr[idx].menuEl;
377			self._menuIn(nextMenu);
378
379			// remove breadcrumbs that are ahead
380			var siblingNode;
381			while (siblingNode = bc.nextSibling) {
382				self.breadcrumbsCtrl.removeChild(siblingNode);
383			}
384
385		});
386	};
387
388	MLMenu.prototype._crawlCrumbs = function(currentMenu, menuArray) {
389		
390		if (menuArray[currentMenu].backIdx != 0) {
391			this._crawlCrumbs(menuArray[currentMenu].backIdx, menuArray);
392		}
393		// create breadcrumb
394		this._addBreadcrumb(currentMenu);
395	}
396
397	window.MLMenu = MLMenu;
398
399})(window);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.