1/*! 2 * SlickNav Responsive Mobile Menu v1.0.4 3 * (c) 2015 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 parentTag: 'a', 19 closeOnClick: false, 20 allowParentLinks: false, 21 nestedParentLinks: true, 22 showChildren: false, 23 removeIds: false, 24 removeClasses: false, 25 brand: '', 26 init: function () {}, 27 beforeOpen: function () {}, 28 beforeClose: function () {}, 29 afterOpen: function () {}, 30 afterClose: function () {} 31 }, 32 mobileMenu = 'slicknav', 33 prefix = 'slicknav'; 34 35 function Plugin(element, options) { 36 this.element = element; 37 38 // jQuery has an extend method which merges the contents of two or 39 // more objects, storing the result in the first object. The first object 40 // is generally empty as we don't want to alter the default options for 41 // future instances of the plugin 42 this.settings = $.extend({}, defaults, options); 43 44 this._defaults = defaults; 45 this._name = mobileMenu; 46 47 this.init(); 48 } 49 50 Plugin.prototype.init = function () { 51 var $this = this, 52 menu = $(this.element), 53 settings = this.settings, 54 iconClass, 55 menuBar; 56 57 // clone menu if needed 58 if (settings.duplicate) { 59 $this.mobileNav = menu.clone(); 60 //remove ids from clone to prevent css issues 61 $this.mobileNav.removeAttr('id'); 62 $this.mobileNav.find('*').each(function (i, e) { 63 $(e).removeAttr('id'); 64 }); 65 } else { 66 $this.mobileNav = menu; 67 68 // remove ids if set 69 $this.mobileNav.removeAttr('id'); 70 $this.mobileNav.find('*').each(function (i, e) { 71 $(e).removeAttr('id'); 72 }); 73 } 74 75 // remove classes if set 76 if (settings.removeClasses) { 77 $this.mobileNav.removeAttr('class'); 78 $this.mobileNav.find('*').each(function (i, e) { 79 $(e).removeAttr('class'); 80 }); 81 } 82 83 // styling class for the button 84 iconClass = prefix + '_icon'; 85 86 if (settings.label === '') { 87 iconClass += ' ' + prefix + '_no-text'; 88 } 89 90 if (settings.parentTag == 'a') { 91 settings.parentTag = 'a href="#"'; 92 } 93 94 // create menu bar 95 $this.mobileNav.attr('class', prefix + '_nav'); 96 menuBar = $('<div class="' + prefix + '_menu"></div>'); 97 if (settings.brand !== '') { 98 var brand = $('<div class="' + prefix + '_brand">'+settings.brand+'</div>'); 99 $(menuBar).append(brand); 100 } 101 $this.btn = $( 102 ['<' + settings.parentTag + ' aria-haspopup="true" tabindex="0" class="' + prefix + '_btn ' + prefix + '_collapsed">', 103 '<span class="' + prefix + '_menutxt">' + settings.label + '</span>', 104 '<span class="' + iconClass + '">', 105 '<span class="' + prefix + '_icon-bar"></span>', 106 '<span class="' + prefix + '_icon-bar"></span>', 107 '<span class="' + prefix + '_icon-bar"></span>', 108 '</span>', 109 '</' + settings.parentTag + '>' 110 ].join('') 111 ); 112 $(menuBar).append($this.btn); 113 $(settings.prependTo).prepend(menuBar); 114 menuBar.append($this.mobileNav); 115 116 // iterate over structure adding additional structure 117 var items = $this.mobileNav.find('li'); 118 $(items).each(function () { 119 var item = $(this), 120 data = {}; 121 data.children = item.children('ul').attr('role', 'menu'); 122 item.data('menu', data); 123 124 // if a list item has a nested menu 125 if (data.children.length > 0) { 126 127 // select all text before the child menu 128 // check for anchors 129 130 var a = item.contents(), 131 containsAnchor = false, 132 nodes = []; 133 134 $(a).each(function () { 135 if (!$(this).is('ul')) { 136 nodes.push(this); 137 } else { 138 return false;
139 } 140 141 if($(this).is("a")) { 142 containsAnchor = true; 143 } 144 }); 145 146 var wrapElement = $( 147 '<' + settings.parentTag + ' role="menuitem" aria-haspopup="true" tabindex="-1" class="' + prefix + '_item"/>' 148 ); 149 150 // wrap item text with tag and add classes unless we are separating parent links 151 if ((!settings.allowParentLinks || settings.nestedParentLinks) || !containsAnchor) { 152 var $wrap = $(nodes).wrapAll(wrapElement).parent(); 153 $wrap.addClass(prefix+'_row'); 154 } else 155 $(nodes).wrapAll('<span class="'+prefix+'_parent-link '+prefix+'_row"/>').parent(); 156 157 if (!settings.showChildren) { 158 item.addClass(prefix+'_collapsed'); 159 } else { 160 item.addClass(prefix+'_open'); 161 } 162 163 item.addClass(prefix+'_parent'); 164 165 // create parent arrow. wrap with link if parent links and separating 166 var arrowElement = $('<span class="'+prefix+'_arrow">'+(settings.showChildren?settings.openedSymbol:settings.closedSymbol)+'</span>'); 167 168 if (settings.allowParentLinks && !settings.nestedParentLinks && containsAnchor) 169 arrowElement = arrowElement.wrap(wrapElement).parent(); 170 171 //append arrow 172 $(nodes).last().after(arrowElement); 173 174 175 } else if ( item.children().length === 0) { 176 item.addClass(prefix+'_txtnode'); 177 } 178 179 // accessibility for links 180 item.children('a').attr('role', 'menuitem').click(function(event){ 181 //Ensure that it's not a parent 182 if (settings.closeOnClick && !$(event.target).parent().closest('li').hasClass(prefix+'_parent')) { 183 //Emulate menu close if set 184 $($this.btn).click(); 185 } 186 }); 187 188 //also close on click if parent links are set 189 if (settings.closeOnClick && settings.allowParentLinks) { 190 item.children('a').children('a').click(function (event) { 191 //Emulate menu close 192 $($this.btn).click(); 193 }); 194 195 item.find('.'+prefix+'_parent-link a:not(.'+prefix+'_item)').click(function(event){ 196 //Emulate menu close 197 $($this.btn).click(); 198 }); 199 } 200 }); 201 202 // structure is in place, now hide appropriate items 203 $(items).each(function () { 204 var data = $(this).data('menu'); 205 if (!settings.showChildren){ 206 $this._visibilityToggle(data.children, null, false, null, true); 207 } 208 }); 209 210 // finally toggle entire menu 211 $this._visibilityToggle($this.mobileNav, null, false, 'init', true); 212 213 // accessibility for menu button 214 $this.mobileNav.attr('role','menu'); 215 216 // outline prevention when using mouse 217 $(document).mousedown(function(){ 218 $this._outlines(false); 219 }); 220 221 $(document).keyup(function(){ 222 $this._outlines(true); 223 }); 224 225 // menu button click 226 $($this.btn).click(function (e) { 227 e.preventDefault(); 228 $this._menuToggle(); 229 }); 230 231 // click on menu parent 232 $this.mobileNav.on('click', '.' + prefix + '_item', function (e) { 233 e.preventDefault(); 234 $this._itemClick($(this)); 235 }); 236 237 // check for enter key on menu button and menu parents 238 $($this.btn).keydown(function (e) { 239 var ev = e || event; 240 if(ev.keyCode == 13) { 241 e.preventDefault(); 242 $this._menuToggle(); 243 } 244 }); 245 246 $this.mobileNav.on('keydown', '.'+prefix+'_item', function(e) { 247 var ev = e || event; 248 if(ev.keyCode == 13) { 249 e.preventDefault(); 250 $this._itemClick($(e.target)); 251 } 252 }); 253 254 // allow links clickable within parent tags if set 255 if (settings.allowParentLinks && settings.nestedParentLinks) { 256 $('.'+prefix+'_item a').click(function(e){ 257 e.stopImmediatePropagation(); 258 }); 259 } 260 }; 261 262 //toggle menu 263 Plugin.prototype._menuToggle = function (el) { 264 var $this = this; 265 var btn = $this.btn; 266 var mobileNav = $this.mobileNav; 267 268 if (btn.hasClass(prefix+'_collapsed')) { 269 btn.removeClass(prefix+'_collapsed'); 270 btn.addClass(prefix+'_open'); 271 } else { 272 btn.removeClass(prefix+'_open'); 273 btn.addClass(prefix+'_collapsed'); 274 } 275 btn.addClass(prefix+'_animating'); 276 $this._visibilityToggle(mobileNav, btn.parent(), true, btn); 277 }; 278 279 // toggle clicked items 280 Plugin.prototype._itemClick = function (el) { 281 var $this = this; 282 var settings = $this.settings; 283 var data = el.data('menu'); 284 if (!data) { 285 data = {}; 286 data.arrow = el.children('.'+prefix+'_arrow'); 287 data.ul = el.next('ul'); 288 data.parent = el.parent(); 289 //Separated parent link structure 290 if (data.parent.hasClass(prefix+'_parent-link')) { 291 data.parent = el.parent().parent(); 292 data.ul = el.parent().next('ul'); 293 } 294 el.data('menu', data); 295 } 296 if (data.parent.hasClass(prefix+'_collapsed')) {
297 data.arrow.html(settings.openedSymbol); 298 data.parent.removeClass(prefix+'_collapsed'); 299 data.parent.addClass(prefix+'_open'); 300 data.parent.addClass(prefix+'_animating'); 301 $this._visibilityToggle(data.ul, data.parent, true, el); 302 } else { 303 data.arrow.html(settings.closedSymbol); 304 data.parent.addClass(prefix+'_collapsed'); 305 data.parent.removeClass(prefix+'_open'); 306 data.parent.addClass(prefix+'_animating'); 307 $this._visibilityToggle(data.ul, data.parent, true, el); 308 } 309 }; 310 311 // toggle actual visibility and accessibility tags 312 Plugin.prototype._visibilityToggle = function(el, parent, animate, trigger, init) { 313 var $this = this; 314 var settings = $this.settings; 315 var items = $this._getActionItems(el); 316 var duration = 0; 317 if (animate) { 318 duration = settings.duration; 319 } 320 321 if (el.hasClass(prefix+'_hidden')) { 322 el.removeClass(prefix+'_hidden'); 323 //Fire beforeOpen callback 324 if (!init) { 325 settings.beforeOpen(trigger); 326 } 327 el.slideDown(duration, settings.easingOpen, function(){ 328 329 $(trigger).removeClass(prefix+'_animating'); 330 $(parent).removeClass(prefix+'_animating'); 331 332 //Fire afterOpen callback 333 if (!init) { 334 settings.afterOpen(trigger); 335 } 336 }); 337 el.attr('aria-hidden','false'); 338 items.attr('tabindex', '0'); 339 $this._setVisAttr(el, false); 340 } else { 341 el.addClass(prefix+'_hidden'); 342 343 //Fire init or beforeClose callback 344 if (!init){ 345 settings.beforeClose(trigger); 346 } 347 348 el.slideUp(duration, this.settings.easingClose, function() { 349 el.attr('aria-hidden','true'); 350 items.attr('tabindex', '-1'); 351 $this._setVisAttr(el, true); 352 el.hide(); //jQuery 1.7 bug fix 353 354 $(trigger).removeClass(prefix+'_animating'); 355 $(parent).removeClass(prefix+'_animating'); 356 357 //Fire init or afterClose callback 358 if (!init){ 359 settings.afterClose(trigger); 360 } else if (trigger == 'init'){ 361 settings.init(); 362 } 363 }); 364 } 365 }; 366 367 // set attributes of element and children based on visibility 368 Plugin.prototype._setVisAttr = function(el, hidden) { 369 var $this = this; 370 371 // select all parents that aren't hidden 372 var nonHidden = el.children('li').children('ul').not('.'+prefix+'_hidden'); 373 374 // iterate over all items setting appropriate tags 375 if (!hidden) { 376 nonHidden.each(function(){ 377 var ul = $(this); 378 ul.attr('aria-hidden','false'); 379 var items = $this._getActionItems(ul); 380 items.attr('tabindex', '0'); 381 $this._setVisAttr(ul, hidden); 382 }); 383 } else { 384 nonHidden.each(function(){ 385 var ul = $(this); 386 ul.attr('aria-hidden','true'); 387 var items = $this._getActionItems(ul); 388 items.attr('tabindex', '-1'); 389 $this._setVisAttr(ul, hidden); 390 }); 391 } 392 }; 393 394 // get all 1st level items that are clickable 395 Plugin.prototype._getActionItems = function(el) { 396 var data = el.data("menu"); 397 if (!data) { 398 data = {}; 399 var items = el.children('li'); 400 var anchors = items.find('a'); 401 data.links = anchors.add(items.find('.'+prefix+'_item')); 402 el.data('menu', data); 403 } 404 return data.links; 405 }; 406 407 Plugin.prototype._outlines = function(state) { 408 if (!state) { 409 $('.'+prefix+'_item, .'+prefix+'_btn').css('outline','none'); 410 } else { 411 $('.'+prefix+'_item, .'+prefix+'_btn').css('outline',''); 412 } 413 }; 414 415 Plugin.prototype.toggle = function(){ 416 var $this = this; 417 $this._menuToggle(); 418 }; 419 420 Plugin.prototype.open = function(){ 421 var $this = this; 422 if ($this.btn.hasClass(prefix+'_collapsed')) { 423 $this._menuToggle(); 424 } 425 }; 426 427 Plugin.prototype.close = function(){ 428 var $this = this; 429 if ($this.btn.hasClass(prefix+'_open')) { 430 $this._menuToggle(); 431 } 432 }; 433 434 $.fn[mobileMenu] = function ( options ) { 435 var args = arguments; 436 437 // Is the first parameter an object (options), or was omitted, instantiate a new instance 438 if (options === undefined || typeof options === 'object') { 439 return this.each(function () { 440 441 // Only allow the plugin to be instantiated once due to methods 442 if (!$.data(this, 'plugin_' + mobileMenu)) { 443 444 // if it has no instance, create a new one, pass options to our plugin constructor, 445 // and store the plugin instance in the elements jQuery data object. 446 $.data(this, 'plugin_' + mobileMenu, new Plugin( this, options )); 447 } 448 }); 449 450 // If is a string and doesn't start with an underscore or 'init' function, treat this as a call to a public method. 451 } else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') { 452 453 // Cache the method call to make it possible to return a value 454 var returns; 455 456 this.each(function () { 457 var instance = $.data(this, 'plugin_' + mobileMenu); 458 459 // Tests that there's already a plugin-instance and checks that the requested public method exists 460 if (instance instanceof Plugin && typeof instance[options] === 'function') { 461 462 // Call the method of our plugin instance, and pass it the supplied arguments. 463 returns = instance[options].apply( instance, Array.prototype.slice.call( args, 1 ) ); 464 } 465 }); 466 467 // If the earlier cached method gives a value back return the value, otherwise return this to preserve chainability. 468 return returns !== undefined ? returns : this; 469 } 470 }; 471}(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.