You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
471 lines
16 KiB
471 lines
16 KiB
/*! |
|
* SlickNav Responsive Mobile Menu v1.0.4 |
|
* (c) 2015 Josh Cope |
|
* licensed under MIT |
|
*/ |
|
;(function ($, document, window) { |
|
var |
|
// default settings object. |
|
defaults = { |
|
label: 'MENU', |
|
duplicate: true, |
|
duration: 200, |
|
easingOpen: 'swing', |
|
easingClose: 'swing', |
|
closedSymbol: '►', |
|
openedSymbol: '▼', |
|
prependTo: 'body', |
|
parentTag: 'a', |
|
closeOnClick: false, |
|
allowParentLinks: false, |
|
nestedParentLinks: true, |
|
showChildren: false, |
|
removeIds: false, |
|
removeClasses: false, |
|
brand: '', |
|
init: function () {}, |
|
beforeOpen: function () {}, |
|
beforeClose: function () {}, |
|
afterOpen: function () {}, |
|
afterClose: function () {} |
|
}, |
|
mobileMenu = 'slicknav', |
|
prefix = 'slicknav'; |
|
|
|
function Plugin(element, options) { |
|
this.element = element; |
|
|
|
// jQuery has an extend method which merges the contents of two or |
|
// more objects, storing the result in the first object. The first object |
|
// is generally empty as we don't want to alter the default options for |
|
// future instances of the plugin |
|
this.settings = $.extend({}, defaults, options); |
|
|
|
this._defaults = defaults; |
|
this._name = mobileMenu; |
|
|
|
this.init(); |
|
} |
|
|
|
Plugin.prototype.init = function () { |
|
var $this = this, |
|
menu = $(this.element), |
|
settings = this.settings, |
|
iconClass, |
|
menuBar; |
|
|
|
// clone menu if needed |
|
if (settings.duplicate) { |
|
$this.mobileNav = menu.clone(); |
|
//remove ids from clone to prevent css issues |
|
$this.mobileNav.removeAttr('id'); |
|
$this.mobileNav.find('*').each(function (i, e) { |
|
$(e).removeAttr('id'); |
|
}); |
|
} else { |
|
$this.mobileNav = menu; |
|
|
|
// remove ids if set |
|
$this.mobileNav.removeAttr('id'); |
|
$this.mobileNav.find('*').each(function (i, e) { |
|
$(e).removeAttr('id'); |
|
}); |
|
} |
|
|
|
// remove classes if set |
|
if (settings.removeClasses) { |
|
$this.mobileNav.removeAttr('class'); |
|
$this.mobileNav.find('*').each(function (i, e) { |
|
$(e).removeAttr('class'); |
|
}); |
|
} |
|
|
|
// styling class for the button |
|
iconClass = prefix + '_icon'; |
|
|
|
if (settings.label === '') { |
|
iconClass += ' ' + prefix + '_no-text'; |
|
} |
|
|
|
if (settings.parentTag == 'a') { |
|
settings.parentTag = 'a href="#"'; |
|
} |
|
|
|
// create menu bar |
|
$this.mobileNav.attr('class', prefix + '_nav'); |
|
menuBar = $('<div class="' + prefix + '_menu"></div>'); |
|
if (settings.brand !== '') { |
|
var brand = $('<div class="' + prefix + '_brand">'+settings.brand+'</div>'); |
|
$(menuBar).append(brand); |
|
} |
|
$this.btn = $( |
|
['<' + settings.parentTag + ' aria-haspopup="true" tabindex="0" class="' + prefix + '_btn ' + prefix + '_collapsed">', |
|
'<span class="' + prefix + '_menutxt">' + settings.label + '</span>', |
|
'<span class="' + iconClass + '">', |
|
'<span class="' + prefix + '_icon-bar"></span>', |
|
'<span class="' + prefix + '_icon-bar"></span>', |
|
'<span class="' + prefix + '_icon-bar"></span>', |
|
'</span>', |
|
'</' + settings.parentTag + '>' |
|
].join('') |
|
); |
|
$(menuBar).append($this.btn); |
|
$(settings.prependTo).prepend(menuBar); |
|
menuBar.append($this.mobileNav); |
|
|
|
// iterate over structure adding additional structure |
|
var items = $this.mobileNav.find('li'); |
|
$(items).each(function () { |
|
var item = $(this), |
|
data = {}; |
|
data.children = item.children('ul').attr('role', 'menu'); |
|
item.data('menu', data); |
|
|
|
// if a list item has a nested menu |
|
if (data.children.length > 0) { |
|
|
|
// select all text before the child menu |
|
// check for anchors |
|
|
|
var a = item.contents(), |
|
containsAnchor = false, |
|
nodes = []; |
|
|
|
$(a).each(function () { |
|
if (!$(this).is('ul')) { |
|
nodes.push(this); |
|
} else { |
|
return false; |
|
} |
|
|
|
if($(this).is("a")) { |
|
containsAnchor = true; |
|
} |
|
}); |
|
|
|
var wrapElement = $( |
|
'<' + settings.parentTag + ' role="menuitem" aria-haspopup="true" tabindex="-1" class="' + prefix + '_item"/>' |
|
); |
|
|
|
// wrap item text with tag and add classes unless we are separating parent links |
|
if ((!settings.allowParentLinks || settings.nestedParentLinks) || !containsAnchor) { |
|
var $wrap = $(nodes).wrapAll(wrapElement).parent(); |
|
$wrap.addClass(prefix+'_row'); |
|
} else |
|
$(nodes).wrapAll('<span class="'+prefix+'_parent-link '+prefix+'_row"/>').parent(); |
|
|
|
if (!settings.showChildren) { |
|
item.addClass(prefix+'_collapsed'); |
|
} else { |
|
item.addClass(prefix+'_open'); |
|
} |
|
|
|
item.addClass(prefix+'_parent'); |
|
|
|
// create parent arrow. wrap with link if parent links and separating |
|
var arrowElement = $('<span class="'+prefix+'_arrow">'+(settings.showChildren?settings.openedSymbol:settings.closedSymbol)+'</span>'); |
|
|
|
if (settings.allowParentLinks && !settings.nestedParentLinks && containsAnchor) |
|
arrowElement = arrowElement.wrap(wrapElement).parent(); |
|
|
|
//append arrow |
|
$(nodes).last().after(arrowElement); |
|
|
|
|
|
} else if ( item.children().length === 0) { |
|
item.addClass(prefix+'_txtnode'); |
|
} |
|
|
|
// accessibility for links |
|
item.children('a').attr('role', 'menuitem').click(function(event){ |
|
//Ensure that it's not a parent |
|
if (settings.closeOnClick && !$(event.target).parent().closest('li').hasClass(prefix+'_parent')) { |
|
//Emulate menu close if set |
|
$($this.btn).click(); |
|
} |
|
}); |
|
|
|
//also close on click if parent links are set |
|
if (settings.closeOnClick && settings.allowParentLinks) { |
|
item.children('a').children('a').click(function (event) { |
|
//Emulate menu close |
|
$($this.btn).click(); |
|
}); |
|
|
|
item.find('.'+prefix+'_parent-link a:not(.'+prefix+'_item)').click(function(event){ |
|
//Emulate menu close |
|
$($this.btn).click(); |
|
}); |
|
} |
|
}); |
|
|
|
// structure is in place, now hide appropriate items |
|
$(items).each(function () { |
|
var data = $(this).data('menu'); |
|
if (!settings.showChildren){ |
|
$this._visibilityToggle(data.children, null, false, null, true); |
|
} |
|
}); |
|
|
|
// finally toggle entire menu |
|
$this._visibilityToggle($this.mobileNav, null, false, 'init', true); |
|
|
|
// accessibility for menu button |
|
$this.mobileNav.attr('role','menu'); |
|
|
|
// outline prevention when using mouse |
|
$(document).mousedown(function(){ |
|
$this._outlines(false); |
|
}); |
|
|
|
$(document).keyup(function(){ |
|
$this._outlines(true); |
|
}); |
|
|
|
// menu button click |
|
$($this.btn).click(function (e) { |
|
e.preventDefault(); |
|
$this._menuToggle(); |
|
}); |
|
|
|
// click on menu parent |
|
$this.mobileNav.on('click', '.' + prefix + '_item', function (e) { |
|
e.preventDefault(); |
|
$this._itemClick($(this)); |
|
}); |
|
|
|
// check for enter key on menu button and menu parents |
|
$($this.btn).keydown(function (e) { |
|
var ev = e || event; |
|
if(ev.keyCode == 13) { |
|
e.preventDefault(); |
|
$this._menuToggle(); |
|
} |
|
}); |
|
|
|
$this.mobileNav.on('keydown', '.'+prefix+'_item', function(e) { |
|
var ev = e || event; |
|
if(ev.keyCode == 13) { |
|
e.preventDefault(); |
|
$this._itemClick($(e.target)); |
|
} |
|
}); |
|
|
|
// allow links clickable within parent tags if set |
|
if (settings.allowParentLinks && settings.nestedParentLinks) { |
|
$('.'+prefix+'_item a').click(function(e){ |
|
e.stopImmediatePropagation(); |
|
}); |
|
} |
|
}; |
|
|
|
//toggle menu |
|
Plugin.prototype._menuToggle = function (el) { |
|
var $this = this; |
|
var btn = $this.btn; |
|
var mobileNav = $this.mobileNav; |
|
|
|
if (btn.hasClass(prefix+'_collapsed')) { |
|
btn.removeClass(prefix+'_collapsed'); |
|
btn.addClass(prefix+'_open'); |
|
} else { |
|
btn.removeClass(prefix+'_open'); |
|
btn.addClass(prefix+'_collapsed'); |
|
} |
|
btn.addClass(prefix+'_animating'); |
|
$this._visibilityToggle(mobileNav, btn.parent(), true, btn); |
|
}; |
|
|
|
// toggle clicked items |
|
Plugin.prototype._itemClick = function (el) { |
|
var $this = this; |
|
var settings = $this.settings; |
|
var data = el.data('menu'); |
|
if (!data) { |
|
data = {}; |
|
data.arrow = el.children('.'+prefix+'_arrow'); |
|
data.ul = el.next('ul'); |
|
data.parent = el.parent(); |
|
//Separated parent link structure |
|
if (data.parent.hasClass(prefix+'_parent-link')) { |
|
data.parent = el.parent().parent(); |
|
data.ul = el.parent().next('ul'); |
|
} |
|
el.data('menu', data); |
|
} |
|
if (data.parent.hasClass(prefix+'_collapsed')) { |
|
data.arrow.html(settings.openedSymbol); |
|
data.parent.removeClass(prefix+'_collapsed'); |
|
data.parent.addClass(prefix+'_open'); |
|
data.parent.addClass(prefix+'_animating'); |
|
$this._visibilityToggle(data.ul, data.parent, true, el); |
|
} else { |
|
data.arrow.html(settings.closedSymbol); |
|
data.parent.addClass(prefix+'_collapsed'); |
|
data.parent.removeClass(prefix+'_open'); |
|
data.parent.addClass(prefix+'_animating'); |
|
$this._visibilityToggle(data.ul, data.parent, true, el); |
|
} |
|
}; |
|
|
|
// toggle actual visibility and accessibility tags |
|
Plugin.prototype._visibilityToggle = function(el, parent, animate, trigger, init) { |
|
var $this = this; |
|
var settings = $this.settings; |
|
var items = $this._getActionItems(el); |
|
var duration = 0; |
|
if (animate) { |
|
duration = settings.duration; |
|
} |
|
|
|
if (el.hasClass(prefix+'_hidden')) { |
|
el.removeClass(prefix+'_hidden'); |
|
//Fire beforeOpen callback |
|
if (!init) { |
|
settings.beforeOpen(trigger); |
|
} |
|
el.slideDown(duration, settings.easingOpen, function(){ |
|
|
|
$(trigger).removeClass(prefix+'_animating'); |
|
$(parent).removeClass(prefix+'_animating'); |
|
|
|
//Fire afterOpen callback |
|
if (!init) { |
|
settings.afterOpen(trigger); |
|
} |
|
}); |
|
el.attr('aria-hidden','false'); |
|
items.attr('tabindex', '0'); |
|
$this._setVisAttr(el, false); |
|
} else { |
|
el.addClass(prefix+'_hidden'); |
|
|
|
//Fire init or beforeClose callback |
|
if (!init){ |
|
settings.beforeClose(trigger); |
|
} |
|
|
|
el.slideUp(duration, this.settings.easingClose, function() { |
|
el.attr('aria-hidden','true'); |
|
items.attr('tabindex', '-1'); |
|
$this._setVisAttr(el, true); |
|
el.hide(); //jQuery 1.7 bug fix |
|
|
|
$(trigger).removeClass(prefix+'_animating'); |
|
$(parent).removeClass(prefix+'_animating'); |
|
|
|
//Fire init or afterClose callback |
|
if (!init){ |
|
settings.afterClose(trigger); |
|
} else if (trigger == 'init'){ |
|
settings.init(); |
|
} |
|
}); |
|
} |
|
}; |
|
|
|
// set attributes of element and children based on visibility |
|
Plugin.prototype._setVisAttr = function(el, hidden) { |
|
var $this = this; |
|
|
|
// select all parents that aren't hidden |
|
var nonHidden = el.children('li').children('ul').not('.'+prefix+'_hidden'); |
|
|
|
// iterate over all items setting appropriate tags |
|
if (!hidden) { |
|
nonHidden.each(function(){ |
|
var ul = $(this); |
|
ul.attr('aria-hidden','false'); |
|
var items = $this._getActionItems(ul); |
|
items.attr('tabindex', '0'); |
|
$this._setVisAttr(ul, hidden); |
|
}); |
|
} else { |
|
nonHidden.each(function(){ |
|
var ul = $(this); |
|
ul.attr('aria-hidden','true'); |
|
var items = $this._getActionItems(ul); |
|
items.attr('tabindex', '-1'); |
|
$this._setVisAttr(ul, hidden); |
|
}); |
|
} |
|
}; |
|
|
|
// get all 1st level items that are clickable |
|
Plugin.prototype._getActionItems = function(el) { |
|
var data = el.data("menu"); |
|
if (!data) { |
|
data = {}; |
|
var items = el.children('li'); |
|
var anchors = items.find('a'); |
|
data.links = anchors.add(items.find('.'+prefix+'_item')); |
|
el.data('menu', data); |
|
} |
|
return data.links; |
|
}; |
|
|
|
Plugin.prototype._outlines = function(state) { |
|
if (!state) { |
|
$('.'+prefix+'_item, .'+prefix+'_btn').css('outline','none'); |
|
} else { |
|
$('.'+prefix+'_item, .'+prefix+'_btn').css('outline',''); |
|
} |
|
}; |
|
|
|
Plugin.prototype.toggle = function(){ |
|
var $this = this; |
|
$this._menuToggle(); |
|
}; |
|
|
|
Plugin.prototype.open = function(){ |
|
var $this = this; |
|
if ($this.btn.hasClass(prefix+'_collapsed')) { |
|
$this._menuToggle(); |
|
} |
|
}; |
|
|
|
Plugin.prototype.close = function(){ |
|
var $this = this; |
|
if ($this.btn.hasClass(prefix+'_open')) { |
|
$this._menuToggle(); |
|
} |
|
}; |
|
|
|
$.fn[mobileMenu] = function ( options ) { |
|
var args = arguments; |
|
|
|
// Is the first parameter an object (options), or was omitted, instantiate a new instance |
|
if (options === undefined || typeof options === 'object') { |
|
return this.each(function () { |
|
|
|
// Only allow the plugin to be instantiated once due to methods |
|
if (!$.data(this, 'plugin_' + mobileMenu)) { |
|
|
|
// if it has no instance, create a new one, pass options to our plugin constructor, |
|
// and store the plugin instance in the elements jQuery data object. |
|
$.data(this, 'plugin_' + mobileMenu, new Plugin( this, options )); |
|
} |
|
}); |
|
|
|
// If is a string and doesn't start with an underscore or 'init' function, treat this as a call to a public method. |
|
} else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') { |
|
|
|
// Cache the method call to make it possible to return a value |
|
var returns; |
|
|
|
this.each(function () { |
|
var instance = $.data(this, 'plugin_' + mobileMenu); |
|
|
|
// Tests that there's already a plugin-instance and checks that the requested public method exists |
|
if (instance instanceof Plugin && typeof instance[options] === 'function') { |
|
|
|
// Call the method of our plugin instance, and pass it the supplied arguments. |
|
returns = instance[options].apply( instance, Array.prototype.slice.call( args, 1 ) ); |
|
} |
|
}); |
|
|
|
// If the earlier cached method gives a value back return the value, otherwise return this to preserve chainability. |
|
return returns !== undefined ? returns : this; |
|
} |
|
}; |
|
}(jQuery, document, window));
|
|
|