1/*! 2 * SlickNav Responsive Mobile Menu v1.0.10 3 * (c) 2016 Josh Cope 4 * licensed under MIT 5 */ 6;(function ($, document, window) { 7 var 8 // default settings object. 9 defaults = { 10 label: 'MENU', 11 duplicate: true, 12 duration: 200, 13 easingOpen: 'swing', 14 easingClose: 'swing', 15 closedSymbol: '►', 16 openedSymbol: '▼', 17 prependTo: 'body', 18 appendTo: '', 19 parentTag: 'a', 20 closeOnClick: false, 21 allowParentLinks: false, 22 nestedParentLinks: true, 23 showChildren: false, 24 removeIds: true, 25 removeClasses: false, 26 removeStyles: false, 27 brand: '', 28 animations: 'jquery', 29 init: function () {}, 30 beforeOpen: function () {}, 31 beforeClose: function () {}, 32 afterOpen: function () {}, 33 afterClose: function () {} 34 }, 35 mobileMenu = 'slicknav', 36 prefix = 'slicknav'; 37 38 Keyboard = { 39 DOWN: 40, 40 ENTER: 13, 41 ESCAPE: 27, 42 LEFT: 37, 43 RIGHT: 39, 44 SPACE: 32, 45 TAB: 9, 46 UP: 38, 47 }; 48 49 function Plugin(element, options) { 50 this.element = element; 51 52 // jQuery has an extend method which merges the contents of two or 53 // more objects, storing the result in the first object. The first object 54 // is generally empty as we don't want to alter the default options for 55 // future instances of the plugin 56 this.settings = $.extend({}, defaults, options); 57 58 // Don't remove IDs by default if duplicate is false 59 if (!this.settings.duplicate && !options.hasOwnProperty("removeIds")) { 60 this.settings.removeIds = false; 61 } 62 63 this._defaults = defaults; 64 this._name = mobileMenu; 65 66 this.init(); 67 } 68 69 Plugin.prototype.init = function () { 70 var $this = this, 71 menu = $(this.element), 72 settings = this.settings, 73 iconClass, 74 menuBar; 75 76 // clone menu if needed 77 if (settings.duplicate) { 78 $this.mobileNav = menu.clone(); 79 } else { 80 $this.mobileNav = menu; 81 } 82 83 // remove IDs if set 84 if (settings.removeIds) { 85 $this.mobileNav.removeAttr('id'); 86 $this.mobileNav.find('*').each(function (i, e) { 87 $(e).removeAttr('id'); 88 }); 89 } 90 91 // remove classes if set 92 if (settings.removeClasses) { 93 $this.mobileNav.removeAttr('class'); 94 $this.mobileNav.find('*').each(function (i, e) { 95 $(e).removeAttr('class'); 96 }); 97 } 98 99 // remove styles if set 100 if (settings.removeStyles) { 101 $this.mobileNav.removeAttr('style'); 102 $this.mobileNav.find('*').each(function (i, e) { 103 $(e).removeAttr('style'); 104 }); 105 } 106 107 // styling class for the button 108 iconClass = prefix + '_icon'; 109 110 if (settings.label === '') { 111 iconClass += ' ' + prefix + '_no-text'; 112 } 113 114 if (settings.parentTag == 'a') { 115 settings.parentTag = 'a href="#"'; 116 } 117 118 // create menu bar 119 $this.mobileNav.attr('class', prefix + '_nav'); 120 menuBar = $('<div class="' + prefix + '_menu"></div>'); 121 if (settings.brand !== '') { 122 var brand = $('<div class="' + prefix + '_brand">'+settings.brand+'</div>'); 123 $(menuBar).append(brand); 124 } 125 $this.btn = $( 126 ['<' + settings.parentTag + ' aria-haspopup="true" role="button" tabindex="0" class="' + prefix + '_btn ' + prefix + '_collapsed">', 127 '<span class="' + prefix + '_menutxt">' + settings.label + '</span>', 128 '<span class="' + iconClass + '">', 129 '<span class="' + prefix + '_icon-bar"></span>', 130 '<span class="' + prefix + '_icon-bar"></span>', 131 '<span class="' + prefix + '_icon-bar"></span>', 132 '</span>', 133 '</' + settings.parentTag + '>' 134 ].join('') 135 ); 136 $('.navbar-toggle').append($this.btn); 137 if(settings.appendTo !== '') { 138 $(settings.appendTo).append(menuBar); 139 } else { 140 $(settings.prependTo).prepend(menuBar); 141 } 142 menuBar.append($this.mobileNav); 143 144 // iterate over structure adding additional structure 145 var items = $this.mobileNav.find('li'); 146 $(items).each(function () { 147 var item = $(this), 148 data = {}; 149 data.children = item.children('ul').attr('role', 'menu'); 150 item.data('menu', data); 151 152 // if a list item has a nested menu 153 if (data.children.length > 0) { 154 155 // select all text before the child menu 156 // check for anchors 157 158 var a = item.contents(), 159 containsAnchor = false, 160 nodes = []; 161 162 $(a).each(function () { 163 if (!$(this).is('ul')) { 164 nodes.push(this); 165 } else { 166 return false; 167 } 168 169 if($(this).is("a")) { 170 containsAnchor = true; 171 } 172 }); 173 174 var wrapElement = $( 175 '<' + settings.parentTag + ' role="menuitem" aria-haspopup="true" tabindex="-1" class="' + prefix + '_item"/>' 176 ); 177 178 // wrap item text with tag and add classes unless we are separating parent links 179 if ((!settings.allowParentLinks || settings.nestedParentLinks) || !containsAnchor) { 180 var $wrap = $(nodes).wrapAll(wrapElement).parent(); 181 $wrap.addClass(prefix+'_row'); 182 } else 183 $(nodes).wrapAll('<span class="'+prefix+'_parent-link '+prefix+'_row"/>').parent(); 184 185 if (!settings.showChildren) { 186 item.addClass(prefix+'_collapsed'); 187 } else { 188 item.addClass(prefix+'_open'); 189 } 190 191 item.addClass(prefix+'_parent'); 192 193 // create parent arrow. wrap with link if parent links and separating 194 var arrowElement = $('<span class="'+prefix+'_arrow">'+(settings.showChildren?settings.openedSymbol:settings.closedSymbol)+'</span>'); 195
196 if (settings.allowParentLinks && !settings.nestedParentLinks && containsAnchor) 197 arrowElement = arrowElement.wrap(wrapElement).parent(); 198 199 //append arrow 200 $(nodes).last().after(arrowElement); 201 202 203 } else if ( item.children().length === 0) { 204 item.addClass(prefix+'_txtnode'); 205 } 206 207 // accessibility for links 208 item.children('a').attr('role', 'menuitem').click(function(event){ 209 //Ensure that it's not a parent 210 if (settings.closeOnClick && !$(event.target).parent().closest('li').hasClass(prefix+'_parent')) { 211 //Emulate menu close if set 212 $($this.btn).click(); 213 } 214 }); 215 216 //also close on click if parent links are set 217 if (settings.closeOnClick && settings.allowParentLinks) { 218 item.children('a').children('a').click(function (event) { 219 //Emulate menu close 220 $($this.btn).click(); 221 }); 222 223 item.find('.'+prefix+'_parent-link a:not(.'+prefix+'_item)').click(function(event){ 224 //Emulate menu close 225 $($this.btn).click(); 226 }); 227 } 228 }); 229 230 // Handle click event for links 231 $this.mobileNav.on('click', 'a', function(event) { 232 var link = $(this); 233 var href = link.attr('href'); 234 if (href !== '#' && href !== '') { 235 // If the link's href is not '#', allow navigation 236 window.location.href = href; 237 } else { 238 // Prevent default behavior if href is '#' 239 event.preventDefault(); 240 } 241 }); 242 243 // structure is in place, now hide appropriate items 244 $(items).each(function () { 245 var data = $(this).data('menu'); 246 if (!settings.showChildren){ 247 $this._visibilityToggle(data.children, null, false, null, true); 248 } 249 }); 250 251 // finally toggle entire menu 252 $this._visibilityToggle($this.mobileNav, null, false, 'init', true); 253 254 // accessibility for menu button 255 $this.mobileNav.attr('role','menu'); 256 257 // outline prevention when using mouse 258 $(document).mousedown(function(){ 259 $this._outlines(false); 260 }); 261 262 $(document).keyup(function(){ 263 $this._outlines(true); 264 }); 265 266 // menu button click 267 $($this.btn).click(function (e) { 268 e.preventDefault(); 269 $this._menuToggle(); 270 }); 271 272 // click on menu parent 273 $this.mobileNav.on('click', '.' + prefix + '_item', function (e) { 274 e.preventDefault(); 275 $this._itemClick($(this)); 276 }); 277 278 // check for keyboard events on menu button and menu parents 279 $($this.btn).keydown(function (e) { 280 var ev = e || event; 281 282 switch(ev.keyCode) { 283 case Keyboard.ENTER: 284 case Keyboard.SPACE: 285 case Keyboard.DOWN: 286 e.preventDefault(); 287 if (ev.keyCode !== Keyboard.DOWN || !$($this.btn).hasClass(prefix+'_open')){ 288 $this._menuToggle(); 289 } 290 291 $($this.btn).next().find('[role="menuitem"]').first().focus(); 292 break; 293 } 294 295 296 }); 297 298 $this.mobileNav.on('keydown', '.'+prefix+'_item', function(e) { 299 var ev = e || event; 300 301 switch(ev.keyCode) { 302 case Keyboard.ENTER: 303 e.preventDefault(); 304 $this._itemClick($(e.target)); 305 break; 306 case Keyboard.RIGHT: 307 e.preventDefault(); 308 if ($(e.target).parent().hasClass(prefix+'_collapsed')) { 309 $this._itemClick($(e.target)); 310 } 311 $(e.target).next().find('[role="menuitem"]').first().focus(); 312 break; 313 } 314 }); 315 316 $this.mobileNav.on('keydown', '[role="menuitem"]', function(e) { 317 var ev = e || event; 318 319 switch(ev.keyCode){
320 case Keyboard.DOWN: 321 e.preventDefault(); 322 var allItems = $(e.target).parent().parent().children().children('[role="menuitem"]:visible'); 323 var idx = allItems.index( e.target ); 324 var nextIdx = idx + 1; 325 if (allItems.length <= nextIdx) { 326 nextIdx = 0; 327 } 328 var next = allItems.eq( nextIdx ); 329 next.focus(); 330 break; 331 case Keyboard.UP: 332 e.preventDefault(); 333 var allItems = $(e.target).parent().parent().children().children('[role="menuitem"]:visible'); 334 var idx = allItems.index( e.target ); 335 var next = allItems.eq( idx - 1 ); 336 next.focus(); 337 break; 338 case Keyboard.LEFT: 339 e.preventDefault(); 340 if ($(e.target).parent().parent().parent().hasClass(prefix+'_open')) { 341 var parent = $(e.target).parent().parent().prev(); 342 parent.focus(); 343 $this._itemClick(parent); 344 } else if ($(e.target).parent().parent().hasClass(prefix+'_nav')){ 345 $this._menuToggle(); 346 $($this.btn).focus(); 347 } 348 break; 349 case Keyboard.ESCAPE: 350 e.preventDefault(); 351 $this._menuToggle(); 352 $($this.btn).focus(); 353 break; 354 } 355 }); 356 357 // allow links clickable within parent tags if set 358 if (settings.allowParentLinks && settings.nestedParentLinks) { 359 $('.'+prefix+'_item a').click(function(e){ 360 e.stopImmediatePropagation(); 361 }); 362 } 363 }; 364 365 //toggle menu 366 Plugin.prototype._menuToggle = function (el) { 367 var $this = this; 368 var btn = $this.btn; 369 var mobileNav = $this.mobileNav; 370 371 if (btn.hasClass(prefix+'_collapsed')) { 372 btn.removeClass(prefix+'_collapsed'); 373 btn.addClass(prefix+'_open'); 374 } else { 375 btn.removeClass(prefix+'_open'); 376 btn.addClass(prefix+'_collapsed'); 377 } 378 btn.addClass(prefix+'_animating'); 379 $this._visibilityToggle(mobileNav, btn.parent(), true, btn); 380 }; 381 382 // toggle clicked items 383 Plugin.prototype._itemClick = function (el) { 384 var $this = this; 385 var settings = $this.settings; 386 var data = el.data('menu'); 387 if (!data) { 388 data = {}; 389 data.arrow = el.children('.'+prefix+'_arrow'); 390 data.ul = el.next('ul'); 391 data.parent = el.parent(); 392 //Separated parent link structure 393 if (data.parent.hasClass(prefix+'_parent-link')) { 394 data.parent = el.parent().parent(); 395 data.ul = el.parent().next('ul'); 396 } 397 el.data('menu', data); 398 } 399 if (data.parent.hasClass(prefix+'_collapsed')) { 400 data.arrow.html(settings.openedSymbol); 401 data.parent.removeClass(prefix+'_collapsed'); 402 data.parent.addClass(prefix+'_open'); 403 data.parent.addClass(prefix+'_animating'); 404 $this._visibilityToggle(data.ul, data.parent, true, el); 405 } else { 406 data.arrow.html(settings.closedSymbol); 407 data.parent.addClass(prefix+'_collapsed'); 408 data.parent.removeClass(prefix+'_open'); 409 data.parent.addClass(prefix+'_animating'); 410 $this._visibilityToggle(data.ul, data.parent, true, el); 411 } 412 }; 413 414 // toggle actual visibility and accessibility tags 415 Plugin.prototype._visibilityToggle = function(el, parent, animate, trigger, init) { 416 var $this = this; 417 var settings = $this.settings; 418 var items = $this._getActionItems(el); 419 var duration = 0; 420 if (animate) { 421 duration = settings.duration; 422 } 423 424 function afterOpen(trigger, parent) { 425 $(trigger).removeClass(prefix+'_animating'); 426 $(parent).removeClass(prefix+'_animating'); 427 428 //Fire afterOpen callback 429 if (!init) { 430 settings.afterOpen(trigger); 431 } 432 } 433
434 function afterClose(trigger, parent) { 435 el.attr('aria-hidden','true'); 436 items.attr('tabindex', '-1'); 437 $this._setVisAttr(el, true); 438 el.hide(); //jQuery 1.7 bug fix 439 440 $(trigger).removeClass(prefix+'_animating'); 441 $(parent).removeClass(prefix+'_animating'); 442 443 //Fire init or afterClose callback 444 if (!init){ 445 settings.afterClose(trigger); 446 } else if (trigger == 'init'){ 447 settings.init(); 448 } 449 } 450 451 if (el.hasClass(prefix+'_hidden')) { 452 el.removeClass(prefix+'_hidden'); 453 //Fire beforeOpen callback 454 if (!init) { 455 settings.beforeOpen(trigger); 456 } 457 if (settings.animations === 'jquery') { 458 el.stop(true,true).slideDown(duration, settings.easingOpen, function(){ 459 afterOpen(trigger, parent); 460 }); 461 } else if(settings.animations === 'velocity') { 462 el.velocity("finish").velocity("slideDown", { 463 duration: duration, 464 easing: settings.easingOpen, 465 complete: function() { 466 afterOpen(trigger, parent); 467 } 468 }); 469 } 470 el.attr('aria-hidden','false'); 471 items.attr('tabindex', '0'); 472 $this._setVisAttr(el, false); 473 } else { 474 el.addClass(prefix+'_hidden'); 475 476 //Fire init or beforeClose callback 477 if (!init){ 478 settings.beforeClose(trigger); 479 } 480 481 if (settings.animations === 'jquery') { 482 el.stop(true,true).slideUp(duration, this.settings.easingClose, function() { 483 afterClose(trigger, parent) 484 }); 485 } else if (settings.animations === 'velocity') { 486 487 el.velocity("finish").velocity("slideUp", { 488 duration: duration, 489 easing: settings.easingClose, 490 complete: function() { 491 afterClose(trigger, parent); 492 } 493 }); 494 } 495 } 496 }; 497 498 // set attributes of element and children based on visibility 499 Plugin.prototype._setVisAttr = function(el, hidden) { 500 var $this = this; 501 502 // select all parents that aren't hidden 503 var nonHidden = el.children('li').children('ul').not('.'+prefix+'_hidden'); 504 505 // iterate over all items setting appropriate tags 506 if (!hidden) { 507 nonHidden.each(function(){ 508 var ul = $(this); 509 ul.attr('aria-hidden','false'); 510 var items = $this._getActionItems(ul); 511 items.attr('tabindex', '0'); 512 $this._setVisAttr(ul, hidden); 513 }); 514 } else { 515 nonHidden.each(function(){ 516 var ul = $(this); 517 ul.attr('aria-hidden','true'); 518 var items = $this._getActionItems(ul); 519 items.attr('tabindex', '-1'); 520 $this._setVisAttr(ul, hidden); 521 }); 522 } 523 }; 524 525 // get all 1st level items that are clickable 526 Plugin.prototype._getActionItems = function(el) { 527 var data = el.data("menu"); 528 if (!data) { 529 data = {}; 530 var items = el.children('li'); 531 var anchors = items.find('a'); 532 data.links = anchors.add(items.find('.'+prefix+'_item')); 533 el.data('menu', data); 534 } 535 return data.links; 536 }; 537 538 Plugin.prototype._outlines = function(state) { 539 if (!state) { 540 $('.'+prefix+'_item, .'+prefix+'_btn').css('outline','none'); 541 } else { 542 $('.'+prefix+'_item, .'+prefix+'_btn').css('outline',''); 543 } 544 }; 545 546 Plugin.prototype.toggle = function(){ 547 var $this = this; 548 $this._menuToggle(); 549 }; 550 551 Plugin.prototype.open = function(){ 552 var $this = this; 553 if ($this.btn.hasClass(prefix+'_collapsed')) { 554 $this._menuToggle(); 555 } 556 }; 557 558 Plugin.prototype.close = function(){ 559 var $this = this; 560 if ($this.btn.hasClass(prefix+'_open')) { 561 $this._menuToggle(); 562 } 563 }; 564 565 $.fn[mobileMenu] = function ( options ) { 566 var args = arguments; 567 568 // Is the first parameter an object (options), or was omitted, instantiate a new instance 569 if (options === undefined || typeof options === 'object') { 570 return this.each(function () { 571 572 // Only allow the plugin to be instantiated once due to methods 573 if (!$.data(this, 'plugin_' + mobileMenu)) { 574 575 // if it has no instance, create a new one, pass options to our plugin constructor, 576 // and store the plugin instance in the elements jQuery data object. 577 $.data(this, 'plugin_' + mobileMenu, new Plugin( this, options )); 578 } 579 }); 580 581 // If is a string and doesn't start with an underscore or 'init' function, treat this as a call to a public method. 582 } else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') { 583 584 // Cache the method call to make it possible to return a value 585 var returns; 586 587 this.each(function () { 588 var instance = $.data(this, 'plugin_' + mobileMenu); 589 590 // Tests that there's already a plugin-instance and checks that the requested public method exists 591 if (instance instanceof Plugin && typeof instance[options] === 'function') { 592 593 // Call the method of our plugin instance, and pass it the supplied arguments. 594 returns = instance[options].apply( instance, Array.prototype.slice.call( args, 1 ) ); 595 } 596 }); 597 598 // If the earlier cached method gives a value back return the value, otherwise return this to preserve chainability. 599 return returns !== undefined ? returns : this; 600 } 601 }; 602}(jQuery, document, 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.