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.
1216 lines
44 KiB
1216 lines
44 KiB
/*! |
|
* SmartMenus jQuery Plugin - v1.1.0 - September 17, 2017 |
|
* http://www.smartmenus.org/ |
|
* |
|
* Copyright Vasil Dinkov, Vadikom Web Ltd. |
|
* http://vadikom.com |
|
* |
|
* Licensed MIT |
|
*/ |
|
|
|
(function(factory) { |
|
if (typeof define === 'function' && define.amd) { |
|
// AMD |
|
define(['jquery'], factory); |
|
} else if (typeof module === 'object' && typeof module.exports === 'object') { |
|
// CommonJS |
|
module.exports = factory(require('jquery')); |
|
} else { |
|
// Global jQuery |
|
factory(jQuery); |
|
} |
|
} (function($) { |
|
|
|
var menuTrees = [], |
|
mouse = false, // optimize for touch by default - we will detect for mouse input |
|
touchEvents = 'ontouchstart' in window, // we use this just to choose between toucn and pointer events, not for touch screen detection |
|
mouseDetectionEnabled = false, |
|
requestAnimationFrame = window.requestAnimationFrame || function(callback) { return setTimeout(callback, 1000 / 60); }, |
|
cancelAnimationFrame = window.cancelAnimationFrame || function(id) { clearTimeout(id); }, |
|
canAnimate = !!$.fn.animate; |
|
|
|
// Handle detection for mouse input (i.e. desktop browsers, tablets with a mouse, etc.) |
|
function initMouseDetection(disable) { |
|
var eNS = '.smartmenus_mouse'; |
|
if (!mouseDetectionEnabled && !disable) { |
|
// if we get two consecutive mousemoves within 2 pixels from each other and within 300ms, we assume a real mouse/cursor is present |
|
// in practice, this seems like impossible to trick unintentianally with a real mouse and a pretty safe detection on touch devices (even with older browsers that do not support touch events) |
|
var firstTime = true, |
|
lastMove = null, |
|
events = { |
|
'mousemove': function(e) { |
|
var thisMove = { x: e.pageX, y: e.pageY, timeStamp: new Date().getTime() }; |
|
if (lastMove) { |
|
var deltaX = Math.abs(lastMove.x - thisMove.x), |
|
deltaY = Math.abs(lastMove.y - thisMove.y); |
|
if ((deltaX > 0 || deltaY > 0) && deltaX <= 2 && deltaY <= 2 && thisMove.timeStamp - lastMove.timeStamp <= 300) { |
|
mouse = true; |
|
// if this is the first check after page load, check if we are not over some item by chance and call the mouseenter handler if yes |
|
if (firstTime) { |
|
var $a = $(e.target).closest('a'); |
|
if ($a.is('a')) { |
|
$.each(menuTrees, function() { |
|
if ($.contains(this.$root[0], $a[0])) { |
|
this.itemEnter({ currentTarget: $a[0] }); |
|
return false; |
|
} |
|
}); |
|
} |
|
firstTime = false; |
|
} |
|
} |
|
} |
|
lastMove = thisMove; |
|
} |
|
}; |
|
events[touchEvents ? 'touchstart' : 'pointerover pointermove pointerout MSPointerOver MSPointerMove MSPointerOut'] = function(e) { |
|
if (isTouchEvent(e.originalEvent)) { |
|
mouse = false; |
|
} |
|
}; |
|
$(document).on(getEventsNS(events, eNS)); |
|
mouseDetectionEnabled = true; |
|
} else if (mouseDetectionEnabled && disable) { |
|
$(document).off(eNS); |
|
mouseDetectionEnabled = false; |
|
} |
|
} |
|
|
|
function isTouchEvent(e) { |
|
return !/^(4|mouse)$/.test(e.pointerType); |
|
} |
|
|
|
// returns a jQuery on() ready object |
|
function getEventsNS(events, eNS) { |
|
if (!eNS) { |
|
eNS = ''; |
|
} |
|
var eventsNS = {}; |
|
for (var i in events) { |
|
eventsNS[i.split(' ').join(eNS + ' ') + eNS] = events[i]; |
|
} |
|
return eventsNS; |
|
} |
|
|
|
$.SmartMenus = function(elm, options) { |
|
this.$root = $(elm); |
|
this.opts = options; |
|
this.rootId = ''; // internal |
|
this.accessIdPrefix = ''; |
|
this.$subArrow = null; |
|
this.activatedItems = []; // stores last activated A's for each level |
|
this.visibleSubMenus = []; // stores visible sub menus UL's (might be in no particular order) |
|
this.showTimeout = 0; |
|
this.hideTimeout = 0; |
|
this.scrollTimeout = 0; |
|
this.clickActivated = false; |
|
this.focusActivated = false; |
|
this.zIndexInc = 0; |
|
this.idInc = 0; |
|
this.$firstLink = null; // we'll use these for some tests |
|
this.$firstSub = null; // at runtime so we'll cache them |
|
this.disabled = false; |
|
this.$disableOverlay = null; |
|
this.$touchScrollingSub = null; |
|
this.cssTransforms3d = 'perspective' in elm.style || 'webkitPerspective' in elm.style; |
|
this.wasCollapsible = false; |
|
this.init(); |
|
}; |
|
|
|
$.extend($.SmartMenus, { |
|
hideAll: function() { |
|
$.each(menuTrees, function() { |
|
this.menuHideAll(); |
|
}); |
|
}, |
|
destroy: function() { |
|
while (menuTrees.length) { |
|
menuTrees[0].destroy(); |
|
} |
|
initMouseDetection(true); |
|
}, |
|
prototype: { |
|
init: function(refresh) { |
|
var self = this; |
|
|
|
if (!refresh) { |
|
menuTrees.push(this); |
|
|
|
this.rootId = (new Date().getTime() + Math.random() + '').replace(/\D/g, ''); |
|
this.accessIdPrefix = 'sm-' + this.rootId + '-'; |
|
|
|
if (this.$root.hasClass('sm-rtl')) { |
|
this.opts.rightToLeftSubMenus = true; |
|
} |
|
|
|
// init root (main menu) |
|
var eNS = '.smartmenus'; |
|
this.$root |
|
.data('smartmenus', this) |
|
.attr('data-smartmenus-id', this.rootId) |
|
.dataSM('level', 1) |
|
.on(getEventsNS({ |
|
'mouseover focusin': $.proxy(this.rootOver, this), |
|
'mouseout focusout': $.proxy(this.rootOut, this), |
|
'keydown': $.proxy(this.rootKeyDown, this) |
|
}, eNS)) |
|
.on(getEventsNS({ |
|
'mouseenter': $.proxy(this.itemEnter, this), |
|
'mouseleave': $.proxy(this.itemLeave, this), |
|
'mousedown': $.proxy(this.itemDown, this), |
|
'focus': $.proxy(this.itemFocus, this), |
|
'blur': $.proxy(this.itemBlur, this), |
|
'click': $.proxy(this.itemClick, this) |
|
}, eNS), 'a'); |
|
|
|
// hide menus on tap or click outside the root UL |
|
eNS += this.rootId; |
|
if (this.opts.hideOnClick) { |
|
$(document).on(getEventsNS({ |
|
'touchstart': $.proxy(this.docTouchStart, this), |
|
'touchmove': $.proxy(this.docTouchMove, this), |
|
'touchend': $.proxy(this.docTouchEnd, this), |
|
// for Opera Mobile < 11.5, webOS browser, etc. we'll check click too |
|
'click': $.proxy(this.docClick, this) |
|
}, eNS)); |
|
} |
|
// hide sub menus on resize |
|
$(window).on(getEventsNS({ 'resize orientationchange': $.proxy(this.winResize, this) }, eNS)); |
|
|
|
if (this.opts.subIndicators) { |
|
this.$subArrow = $('<span/>').addClass('sub-arrow'); |
|
if (this.opts.subIndicatorsText) { |
|
this.$subArrow.html(this.opts.subIndicatorsText); |
|
} |
|
} |
|
|
|
// make sure mouse detection is enabled |
|
initMouseDetection(); |
|
} |
|
|
|
// init sub menus |
|
this.$firstSub = this.$root.find('ul').each(function() { self.menuInit($(this)); }).eq(0); |
|
|
|
this.$firstLink = this.$root.find('a').eq(0); |
|
|
|
// find current item |
|
if (this.opts.markCurrentItem) { |
|
var reDefaultDoc = /(index|default)\.[^#\?\/]*/i, |
|
reHash = /#.*/, |
|
locHref = window.location.href.replace(reDefaultDoc, ''), |
|
locHrefNoHash = locHref.replace(reHash, ''); |
|
this.$root.find('a').each(function() { |
|
var href = this.href.replace(reDefaultDoc, ''), |
|
$this = $(this); |
|
if (href == locHref || href == locHrefNoHash) { |
|
$this.addClass('current'); |
|
if (self.opts.markCurrentTree) { |
|
$this.parentsUntil('[data-smartmenus-id]', 'ul').each(function() { |
|
$(this).dataSM('parent-a').addClass('current'); |
|
}); |
|
} |
|
} |
|
}); |
|
} |
|
|
|
// save initial state |
|
this.wasCollapsible = this.isCollapsible(); |
|
}, |
|
destroy: function(refresh) { |
|
if (!refresh) { |
|
var eNS = '.smartmenus'; |
|
this.$root |
|
.removeData('smartmenus') |
|
.removeAttr('data-smartmenus-id') |
|
.removeDataSM('level') |
|
.off(eNS); |
|
eNS += this.rootId; |
|
$(document).off(eNS); |
|
$(window).off(eNS); |
|
if (this.opts.subIndicators) { |
|
this.$subArrow = null; |
|
} |
|
} |
|
this.menuHideAll(); |
|
var self = this; |
|
this.$root.find('ul').each(function() { |
|
var $this = $(this); |
|
if ($this.dataSM('scroll-arrows')) { |
|
$this.dataSM('scroll-arrows').remove(); |
|
} |
|
if ($this.dataSM('shown-before')) { |
|
if (self.opts.subMenusMinWidth || self.opts.subMenusMaxWidth) { |
|
$this.css({ width: '', minWidth: '', maxWidth: '' }).removeClass('sm-nowrap'); |
|
} |
|
if ($this.dataSM('scroll-arrows')) { |
|
$this.dataSM('scroll-arrows').remove(); |
|
} |
|
$this.css({ zIndex: '', top: '', left: '', marginLeft: '', marginTop: '', display: '' }); |
|
} |
|
if (($this.attr('id') || '').indexOf(self.accessIdPrefix) == 0) { |
|
$this.removeAttr('id'); |
|
} |
|
}) |
|
.removeDataSM('in-mega') |
|
.removeDataSM('shown-before') |
|
.removeDataSM('scroll-arrows') |
|
.removeDataSM('parent-a') |
|
.removeDataSM('level') |
|
.removeDataSM('beforefirstshowfired') |
|
.removeAttr('role') |
|
.removeAttr('aria-hidden') |
|
.removeAttr('aria-labelledby') |
|
.removeAttr('aria-expanded'); |
|
this.$root.find('a.has-submenu').each(function() { |
|
var $this = $(this); |
|
if ($this.attr('id').indexOf(self.accessIdPrefix) == 0) { |
|
$this.removeAttr('id'); |
|
} |
|
}) |
|
.removeClass('has-submenu') |
|
.removeDataSM('sub') |
|
.removeAttr('aria-haspopup') |
|
.removeAttr('aria-controls') |
|
.removeAttr('aria-expanded') |
|
.closest('li').removeDataSM('sub'); |
|
if (this.opts.subIndicators) { |
|
this.$root.find('span.sub-arrow').remove(); |
|
} |
|
if (this.opts.markCurrentItem) { |
|
this.$root.find('a.current').removeClass('current'); |
|
} |
|
if (!refresh) { |
|
this.$root = null; |
|
this.$firstLink = null; |
|
this.$firstSub = null; |
|
if (this.$disableOverlay) { |
|
this.$disableOverlay.remove(); |
|
this.$disableOverlay = null; |
|
} |
|
menuTrees.splice($.inArray(this, menuTrees), 1); |
|
} |
|
}, |
|
disable: function(noOverlay) { |
|
if (!this.disabled) { |
|
this.menuHideAll(); |
|
// display overlay over the menu to prevent interaction |
|
if (!noOverlay && !this.opts.isPopup && this.$root.is(':visible')) { |
|
var pos = this.$root.offset(); |
|
this.$disableOverlay = $('<div class="sm-jquery-disable-overlay"/>').css({ |
|
position: 'absolute', |
|
top: pos.top, |
|
left: pos.left, |
|
width: this.$root.outerWidth(), |
|
height: this.$root.outerHeight(), |
|
zIndex: this.getStartZIndex(true), |
|
opacity: 0 |
|
}).appendTo(document.body); |
|
} |
|
this.disabled = true; |
|
} |
|
}, |
|
docClick: function(e) { |
|
if (this.$touchScrollingSub) { |
|
this.$touchScrollingSub = null; |
|
return; |
|
} |
|
// hide on any click outside the menu or on a menu link |
|
if (this.visibleSubMenus.length && !$.contains(this.$root[0], e.target) || $(e.target).closest('a').length) { |
|
this.menuHideAll(); |
|
} |
|
}, |
|
docTouchEnd: function(e) { |
|
if (!this.lastTouch) { |
|
return; |
|
} |
|
if (this.visibleSubMenus.length && (this.lastTouch.x2 === undefined || this.lastTouch.x1 == this.lastTouch.x2) && (this.lastTouch.y2 === undefined || this.lastTouch.y1 == this.lastTouch.y2) && (!this.lastTouch.target || !$.contains(this.$root[0], this.lastTouch.target))) { |
|
if (this.hideTimeout) { |
|
clearTimeout(this.hideTimeout); |
|
this.hideTimeout = 0; |
|
} |
|
// hide with a delay to prevent triggering accidental unwanted click on some page element |
|
var self = this; |
|
this.hideTimeout = setTimeout(function() { self.menuHideAll(); }, 350); |
|
} |
|
this.lastTouch = null; |
|
}, |
|
docTouchMove: function(e) { |
|
if (!this.lastTouch) { |
|
return; |
|
} |
|
var touchPoint = e.originalEvent.touches[0]; |
|
this.lastTouch.x2 = touchPoint.pageX; |
|
this.lastTouch.y2 = touchPoint.pageY; |
|
}, |
|
docTouchStart: function(e) { |
|
var touchPoint = e.originalEvent.touches[0]; |
|
this.lastTouch = { x1: touchPoint.pageX, y1: touchPoint.pageY, target: touchPoint.target }; |
|
}, |
|
enable: function() { |
|
if (this.disabled) { |
|
if (this.$disableOverlay) { |
|
this.$disableOverlay.remove(); |
|
this.$disableOverlay = null; |
|
} |
|
this.disabled = false; |
|
} |
|
}, |
|
getClosestMenu: function(elm) { |
|
var $closestMenu = $(elm).closest('ul'); |
|
while ($closestMenu.dataSM('in-mega')) { |
|
$closestMenu = $closestMenu.parent().closest('ul'); |
|
} |
|
return $closestMenu[0] || null; |
|
}, |
|
getHeight: function($elm) { |
|
return this.getOffset($elm, true); |
|
}, |
|
// returns precise width/height float values |
|
getOffset: function($elm, height) { |
|
var old; |
|
if ($elm.css('display') == 'none') { |
|
old = { position: $elm[0].style.position, visibility: $elm[0].style.visibility }; |
|
$elm.css({ position: 'absolute', visibility: 'hidden' }).show(); |
|
} |
|
var box = $elm[0].getBoundingClientRect && $elm[0].getBoundingClientRect(), |
|
val = box && (height ? box.height || box.bottom - box.top : box.width || box.right - box.left); |
|
if (!val && val !== 0) { |
|
val = height ? $elm[0].offsetHeight : $elm[0].offsetWidth; |
|
} |
|
if (old) { |
|
$elm.hide().css(old); |
|
} |
|
return val; |
|
}, |
|
getStartZIndex: function(root) { |
|
var zIndex = parseInt(this[root ? '$root' : '$firstSub'].css('z-index')); |
|
if (!root && isNaN(zIndex)) { |
|
zIndex = parseInt(this.$root.css('z-index')); |
|
} |
|
return !isNaN(zIndex) ? zIndex : 1; |
|
}, |
|
getTouchPoint: function(e) { |
|
return e.touches && e.touches[0] || e.changedTouches && e.changedTouches[0] || e; |
|
}, |
|
getViewport: function(height) { |
|
var name = height ? 'Height' : 'Width', |
|
val = document.documentElement['client' + name], |
|
val2 = window['inner' + name]; |
|
if (val2) { |
|
val = Math.min(val, val2); |
|
} |
|
return val; |
|
}, |
|
getViewportHeight: function() { |
|
return this.getViewport(true); |
|
}, |
|
getViewportWidth: function() { |
|
return this.getViewport(); |
|
}, |
|
getWidth: function($elm) { |
|
return this.getOffset($elm); |
|
}, |
|
handleEvents: function() { |
|
return !this.disabled && this.isCSSOn(); |
|
}, |
|
handleItemEvents: function($a) { |
|
return this.handleEvents() && !this.isLinkInMegaMenu($a); |
|
}, |
|
isCollapsible: function() { |
|
return this.$firstSub.css('position') == 'static'; |
|
}, |
|
isCSSOn: function() { |
|
return this.$firstLink.css('display') != 'inline'; |
|
}, |
|
isFixed: function() { |
|
var isFixed = this.$root.css('position') == 'fixed'; |
|
if (!isFixed) { |
|
this.$root.parentsUntil('body').each(function() { |
|
if ($(this).css('position') == 'fixed') { |
|
isFixed = true; |
|
return false; |
|
} |
|
}); |
|
} |
|
return isFixed; |
|
}, |
|
isLinkInMegaMenu: function($a) { |
|
return $(this.getClosestMenu($a[0])).hasClass('mega-menu'); |
|
}, |
|
isTouchMode: function() { |
|
return !mouse || this.opts.noMouseOver || this.isCollapsible(); |
|
}, |
|
itemActivate: function($a, hideDeeperSubs) { |
|
var $ul = $a.closest('ul'), |
|
level = $ul.dataSM('level'); |
|
// if for some reason the parent item is not activated (e.g. this is an API call to activate the item), activate all parent items first |
|
if (level > 1 && (!this.activatedItems[level - 2] || this.activatedItems[level - 2][0] != $ul.dataSM('parent-a')[0])) { |
|
var self = this; |
|
$($ul.parentsUntil('[data-smartmenus-id]', 'ul').get().reverse()).add($ul).each(function() { |
|
self.itemActivate($(this).dataSM('parent-a')); |
|
}); |
|
} |
|
// hide any visible deeper level sub menus |
|
if (!this.isCollapsible() || hideDeeperSubs) { |
|
this.menuHideSubMenus(!this.activatedItems[level - 1] || this.activatedItems[level - 1][0] != $a[0] ? level - 1 : level); |
|
} |
|
// save new active item for this level |
|
this.activatedItems[level - 1] = $a; |
|
if (this.$root.triggerHandler('activate.smapi', $a[0]) === false) { |
|
return; |
|
} |
|
// show the sub menu if this item has one |
|
var $sub = $a.dataSM('sub'); |
|
if ($sub && (this.isTouchMode() || (!this.opts.showOnClick || this.clickActivated))) { |
|
this.menuShow($sub); |
|
} |
|
}, |
|
itemBlur: function(e) { |
|
var $a = $(e.currentTarget); |
|
if (!this.handleItemEvents($a)) { |
|
return; |
|
} |
|
this.$root.triggerHandler('blur.smapi', $a[0]); |
|
}, |
|
itemClick: function(e) { |
|
var $a = $(e.currentTarget); |
|
if (!this.handleItemEvents($a)) { |
|
return; |
|
} |
|
if (this.$touchScrollingSub && this.$touchScrollingSub[0] == $a.closest('ul')[0]) { |
|
this.$touchScrollingSub = null; |
|
e.stopPropagation(); |
|
return false; |
|
} |
|
if (this.$root.triggerHandler('click.smapi', $a[0]) === false) { |
|
return false; |
|
} |
|
var subArrowClicked = $(e.target).is('.sub-arrow'), |
|
$sub = $a.dataSM('sub'), |
|
firstLevelSub = $sub ? $sub.dataSM('level') == 2 : false, |
|
collapsible = this.isCollapsible(), |
|
behaviorToggle = /toggle$/.test(this.opts.collapsibleBehavior), |
|
behaviorLink = /link$/.test(this.opts.collapsibleBehavior), |
|
behaviorAccordion = /^accordion/.test(this.opts.collapsibleBehavior); |
|
// if the sub is hidden, try to show it |
|
if ($sub && !$sub.is(':visible')) { |
|
if (!behaviorLink || !collapsible || subArrowClicked) { |
|
if (this.opts.showOnClick && firstLevelSub) { |
|
this.clickActivated = true; |
|
} |
|
// try to activate the item and show the sub |
|
this.itemActivate($a, behaviorAccordion); |
|
// if "itemActivate" showed the sub, prevent the click so that the link is not loaded |
|
// if it couldn't show it, then the sub menus are disabled with an !important declaration (e.g. via mobile styles) so let the link get loaded |
|
if ($sub.is(':visible')) { |
|
this.focusActivated = true; |
|
return false; |
|
} |
|
} |
|
// if the sub is visible and we are in collapsible mode |
|
} else if (collapsible && (behaviorToggle || subArrowClicked)) { |
|
this.itemActivate($a, behaviorAccordion); |
|
this.menuHide($sub); |
|
if (behaviorToggle) { |
|
this.focusActivated = false; |
|
} |
|
return false; |
|
} |
|
if (this.opts.showOnClick && firstLevelSub || $a.hasClass('disabled') || this.$root.triggerHandler('select.smapi', $a[0]) === false) { |
|
return false; |
|
} |
|
}, |
|
itemDown: function(e) { |
|
var $a = $(e.currentTarget); |
|
if (!this.handleItemEvents($a)) { |
|
return; |
|
} |
|
$a.dataSM('mousedown', true); |
|
}, |
|
itemEnter: function(e) { |
|
var $a = $(e.currentTarget); |
|
if (!this.handleItemEvents($a)) { |
|
return; |
|
} |
|
if (!this.isTouchMode()) { |
|
if (this.showTimeout) { |
|
clearTimeout(this.showTimeout); |
|
this.showTimeout = 0; |
|
} |
|
var self = this; |
|
this.showTimeout = setTimeout(function() { self.itemActivate($a); }, this.opts.showOnClick && $a.closest('ul').dataSM('level') == 1 ? 1 : this.opts.showTimeout); |
|
} |
|
this.$root.triggerHandler('mouseenter.smapi', $a[0]); |
|
}, |
|
itemFocus: function(e) { |
|
var $a = $(e.currentTarget); |
|
if (!this.handleItemEvents($a)) { |
|
return; |
|
} |
|
// fix (the mousedown check): in some browsers a tap/click produces consecutive focus + click events so we don't need to activate the item on focus |
|
if (this.focusActivated && (!this.isTouchMode() || !$a.dataSM('mousedown')) && (!this.activatedItems.length || this.activatedItems[this.activatedItems.length - 1][0] != $a[0])) { |
|
this.itemActivate($a, true); |
|
} |
|
this.$root.triggerHandler('focus.smapi', $a[0]); |
|
}, |
|
itemLeave: function(e) { |
|
var $a = $(e.currentTarget); |
|
if (!this.handleItemEvents($a)) { |
|
return; |
|
} |
|
if (!this.isTouchMode()) { |
|
$a[0].blur(); |
|
if (this.showTimeout) { |
|
clearTimeout(this.showTimeout); |
|
this.showTimeout = 0; |
|
} |
|
} |
|
$a.removeDataSM('mousedown'); |
|
this.$root.triggerHandler('mouseleave.smapi', $a[0]); |
|
}, |
|
menuHide: function($sub) { |
|
if (this.$root.triggerHandler('beforehide.smapi', $sub[0]) === false) { |
|
return; |
|
} |
|
if (canAnimate) { |
|
$sub.stop(true, true); |
|
} |
|
if ($sub.css('display') != 'none') { |
|
var complete = function() { |
|
// unset z-index |
|
$sub.css('z-index', ''); |
|
}; |
|
// if sub is collapsible (mobile view) |
|
if (this.isCollapsible()) { |
|
if (canAnimate && this.opts.collapsibleHideFunction) { |
|
this.opts.collapsibleHideFunction.call(this, $sub, complete); |
|
} else { |
|
$sub.hide(this.opts.collapsibleHideDuration, complete); |
|
} |
|
} else { |
|
if (canAnimate && this.opts.hideFunction) { |
|
this.opts.hideFunction.call(this, $sub, complete); |
|
} else { |
|
$sub.hide(this.opts.hideDuration, complete); |
|
} |
|
} |
|
// deactivate scrolling if it is activated for this sub |
|
if ($sub.dataSM('scroll')) { |
|
this.menuScrollStop($sub); |
|
$sub.css({ 'touch-action': '', '-ms-touch-action': '', '-webkit-transform': '', transform: '' }) |
|
.off('.smartmenus_scroll').removeDataSM('scroll').dataSM('scroll-arrows').hide(); |
|
} |
|
// unhighlight parent item + accessibility |
|
$sub.dataSM('parent-a').removeClass('highlighted').attr('aria-expanded', 'false'); |
|
$sub.attr({ |
|
'aria-expanded': 'false', |
|
'aria-hidden': 'true' |
|
}); |
|
var level = $sub.dataSM('level'); |
|
this.activatedItems.splice(level - 1, 1); |
|
this.visibleSubMenus.splice($.inArray($sub, this.visibleSubMenus), 1); |
|
this.$root.triggerHandler('hide.smapi', $sub[0]); |
|
} |
|
}, |
|
menuHideAll: function() { |
|
if (this.showTimeout) { |
|
clearTimeout(this.showTimeout); |
|
this.showTimeout = 0; |
|
} |
|
// hide all subs |
|
// if it's a popup, this.visibleSubMenus[0] is the root UL |
|
var level = this.opts.isPopup ? 1 : 0; |
|
for (var i = this.visibleSubMenus.length - 1; i >= level; i--) { |
|
this.menuHide(this.visibleSubMenus[i]); |
|
} |
|
// hide root if it's popup |
|
if (this.opts.isPopup) { |
|
if (canAnimate) { |
|
this.$root.stop(true, true); |
|
} |
|
if (this.$root.is(':visible')) { |
|
if (canAnimate && this.opts.hideFunction) { |
|
this.opts.hideFunction.call(this, this.$root); |
|
} else { |
|
this.$root.hide(this.opts.hideDuration); |
|
} |
|
} |
|
} |
|
this.activatedItems = []; |
|
this.visibleSubMenus = []; |
|
this.clickActivated = false; |
|
this.focusActivated = false; |
|
// reset z-index increment |
|
this.zIndexInc = 0; |
|
this.$root.triggerHandler('hideAll.smapi'); |
|
}, |
|
menuHideSubMenus: function(level) { |
|
for (var i = this.activatedItems.length - 1; i >= level; i--) { |
|
var $sub = this.activatedItems[i].dataSM('sub'); |
|
if ($sub) { |
|
this.menuHide($sub); |
|
} |
|
} |
|
}, |
|
menuInit: function($ul) { |
|
if (!$ul.dataSM('in-mega')) { |
|
// mark UL's in mega drop downs (if any) so we can neglect them |
|
if ($ul.hasClass('mega-menu')) { |
|
$ul.find('ul').dataSM('in-mega', true); |
|
} |
|
// get level (much faster than, for example, using parentsUntil) |
|
var level = 2, |
|
par = $ul[0]; |
|
while ((par = par.parentNode.parentNode) != this.$root[0]) { |
|
level++; |
|
} |
|
// cache stuff for quick access |
|
var $a = $ul.prevAll('a').eq(-1); |
|
// if the link is nested (e.g. in a heading) |
|
if (!$a.length) { |
|
$a = $ul.prevAll().find('a').eq(-1); |
|
} |
|
$a.addClass('has-submenu').dataSM('sub', $ul); |
|
$ul.dataSM('parent-a', $a) |
|
.dataSM('level', level) |
|
.parent().dataSM('sub', $ul); |
|
// accessibility |
|
var aId = $a.attr('id') || this.accessIdPrefix + (++this.idInc), |
|
ulId = $ul.attr('id') || this.accessIdPrefix + (++this.idInc); |
|
$a.attr({ |
|
id: aId, |
|
'aria-haspopup': 'true', |
|
'aria-controls': ulId, |
|
'aria-expanded': 'false' |
|
}); |
|
$ul.attr({ |
|
id: ulId, |
|
'role': 'group', |
|
'aria-hidden': 'true', |
|
'aria-labelledby': aId, |
|
'aria-expanded': 'false' |
|
}); |
|
// add sub indicator to parent item |
|
if (this.opts.subIndicators) { |
|
$a[this.opts.subIndicatorsPos](this.$subArrow.clone()); |
|
} |
|
} |
|
}, |
|
menuPosition: function($sub) { |
|
var $a = $sub.dataSM('parent-a'), |
|
$li = $a.closest('li'), |
|
$ul = $li.parent(), |
|
level = $sub.dataSM('level'), |
|
subW = this.getWidth($sub), |
|
subH = this.getHeight($sub), |
|
itemOffset = $a.offset(), |
|
itemX = itemOffset.left, |
|
itemY = itemOffset.top, |
|
itemW = this.getWidth($a), |
|
itemH = this.getHeight($a), |
|
$win = $(window), |
|
winX = $win.scrollLeft(), |
|
winY = $win.scrollTop(), |
|
winW = this.getViewportWidth(), |
|
winH = this.getViewportHeight(), |
|
horizontalParent = $ul.parent().is('[data-sm-horizontal-sub]') || level == 2 && !$ul.hasClass('sm-vertical'), |
|
rightToLeft = this.opts.rightToLeftSubMenus && !$li.is('[data-sm-reverse]') || !this.opts.rightToLeftSubMenus && $li.is('[data-sm-reverse]'), |
|
subOffsetX = level == 2 ? this.opts.mainMenuSubOffsetX : this.opts.subMenusSubOffsetX, |
|
subOffsetY = level == 2 ? this.opts.mainMenuSubOffsetY : this.opts.subMenusSubOffsetY, |
|
x, y; |
|
if (horizontalParent) { |
|
x = rightToLeft ? itemW - subW - subOffsetX : subOffsetX; |
|
y = this.opts.bottomToTopSubMenus ? -subH - subOffsetY : itemH + subOffsetY; |
|
} else { |
|
x = rightToLeft ? subOffsetX - subW : itemW - subOffsetX; |
|
y = this.opts.bottomToTopSubMenus ? itemH - subOffsetY - subH : subOffsetY; |
|
} |
|
if (this.opts.keepInViewport) { |
|
var absX = itemX + x, |
|
absY = itemY + y; |
|
if (rightToLeft && absX < winX) { |
|
x = horizontalParent ? winX - absX + x : itemW - subOffsetX; |
|
} else if (!rightToLeft && absX + subW > winX + winW) { |
|
x = horizontalParent ? winX + winW - subW - absX + x : subOffsetX - subW; |
|
} |
|
if (!horizontalParent) { |
|
if (subH < winH && absY + subH > winY + winH) { |
|
y += winY + winH - subH - absY; |
|
} else if (subH >= winH || absY < winY) { |
|
y += winY - absY; |
|
} |
|
} |
|
// do we need scrolling? |
|
// 0.49 used for better precision when dealing with float values |
|
if (horizontalParent && (absY + subH > winY + winH + 0.49 || absY < winY) || !horizontalParent && subH > winH + 0.49) { |
|
var self = this; |
|
if (!$sub.dataSM('scroll-arrows')) { |
|
$sub.dataSM('scroll-arrows', $([$('<span class="scroll-up"><span class="scroll-up-arrow"></span></span>')[0], $('<span class="scroll-down"><span class="scroll-down-arrow"></span></span>')[0]]) |
|
.on({ |
|
mouseenter: function() { |
|
$sub.dataSM('scroll').up = $(this).hasClass('scroll-up'); |
|
self.menuScroll($sub); |
|
}, |
|
mouseleave: function(e) { |
|
self.menuScrollStop($sub); |
|
self.menuScrollOut($sub, e); |
|
}, |
|
'mousewheel DOMMouseScroll': function(e) { e.preventDefault(); } |
|
}) |
|
.insertAfter($sub) |
|
); |
|
} |
|
// bind scroll events and save scroll data for this sub |
|
var eNS = '.smartmenus_scroll'; |
|
$sub.dataSM('scroll', { |
|
y: this.cssTransforms3d ? 0 : y - itemH, |
|
step: 1, |
|
// cache stuff for faster recalcs later |
|
itemH: itemH, |
|
subH: subH, |
|
arrowDownH: this.getHeight($sub.dataSM('scroll-arrows').eq(1)) |
|
}) |
|
.on(getEventsNS({ |
|
'mouseover': function(e) { self.menuScrollOver($sub, e); }, |
|
'mouseout': function(e) { self.menuScrollOut($sub, e); }, |
|
'mousewheel DOMMouseScroll': function(e) { self.menuScrollMousewheel($sub, e); } |
|
}, eNS)) |
|
.dataSM('scroll-arrows').css({ top: 'auto', left: '0', marginLeft: x + (parseInt($sub.css('border-left-width')) || 0), width: subW - (parseInt($sub.css('border-left-width')) || 0) - (parseInt($sub.css('border-right-width')) || 0), zIndex: $sub.css('z-index') }) |
|
.eq(horizontalParent && this.opts.bottomToTopSubMenus ? 0 : 1).show(); |
|
// when a menu tree is fixed positioned we allow scrolling via touch too |
|
// since there is no other way to access such long sub menus if no mouse is present |
|
if (this.isFixed()) { |
|
var events = {}; |
|
events[touchEvents ? 'touchstart touchmove touchend' : 'pointerdown pointermove pointerup MSPointerDown MSPointerMove MSPointerUp'] = function(e) { |
|
self.menuScrollTouch($sub, e); |
|
}; |
|
$sub.css({ 'touch-action': 'none', '-ms-touch-action': 'none' }).on(getEventsNS(events, eNS)); |
|
} |
|
} |
|
} |
|
$sub.css({ top: 'auto', left: '0', marginLeft: x, marginTop: y - itemH }); |
|
}, |
|
menuScroll: function($sub, once, step) { |
|
var data = $sub.dataSM('scroll'), |
|
$arrows = $sub.dataSM('scroll-arrows'), |
|
end = data.up ? data.upEnd : data.downEnd, |
|
diff; |
|
if (!once && data.momentum) { |
|
data.momentum *= 0.92; |
|
diff = data.momentum; |
|
if (diff < 0.5) { |
|
this.menuScrollStop($sub); |
|
return; |
|
} |
|
} else { |
|
diff = step || (once || !this.opts.scrollAccelerate ? this.opts.scrollStep : Math.floor(data.step)); |
|
} |
|
// hide any visible deeper level sub menus |
|
var level = $sub.dataSM('level'); |
|
if (this.activatedItems[level - 1] && this.activatedItems[level - 1].dataSM('sub') && this.activatedItems[level - 1].dataSM('sub').is(':visible')) { |
|
this.menuHideSubMenus(level - 1); |
|
} |
|
data.y = data.up && end <= data.y || !data.up && end >= data.y ? data.y : (Math.abs(end - data.y) > diff ? data.y + (data.up ? diff : -diff) : end); |
|
$sub.css(this.cssTransforms3d ? { '-webkit-transform': 'translate3d(0, ' + data.y + 'px, 0)', transform: 'translate3d(0, ' + data.y + 'px, 0)' } : { marginTop: data.y }); |
|
// show opposite arrow if appropriate |
|
if (mouse && (data.up && data.y > data.downEnd || !data.up && data.y < data.upEnd)) { |
|
$arrows.eq(data.up ? 1 : 0).show(); |
|
} |
|
// if we've reached the end |
|
if (data.y == end) { |
|
if (mouse) { |
|
$arrows.eq(data.up ? 0 : 1).hide(); |
|
} |
|
this.menuScrollStop($sub); |
|
} else if (!once) { |
|
if (this.opts.scrollAccelerate && data.step < this.opts.scrollStep) { |
|
data.step += 0.2; |
|
} |
|
var self = this; |
|
this.scrollTimeout = requestAnimationFrame(function() { self.menuScroll($sub); }); |
|
} |
|
}, |
|
menuScrollMousewheel: function($sub, e) { |
|
if (this.getClosestMenu(e.target) == $sub[0]) { |
|
e = e.originalEvent; |
|
var up = (e.wheelDelta || -e.detail) > 0; |
|
if ($sub.dataSM('scroll-arrows').eq(up ? 0 : 1).is(':visible')) { |
|
$sub.dataSM('scroll').up = up; |
|
this.menuScroll($sub, true); |
|
} |
|
} |
|
e.preventDefault(); |
|
}, |
|
menuScrollOut: function($sub, e) { |
|
if (mouse) { |
|
if (!/^scroll-(up|down)/.test((e.relatedTarget || '').className) && ($sub[0] != e.relatedTarget && !$.contains($sub[0], e.relatedTarget) || this.getClosestMenu(e.relatedTarget) != $sub[0])) { |
|
$sub.dataSM('scroll-arrows').css('visibility', 'hidden'); |
|
} |
|
} |
|
}, |
|
menuScrollOver: function($sub, e) { |
|
if (mouse) { |
|
if (!/^scroll-(up|down)/.test(e.target.className) && this.getClosestMenu(e.target) == $sub[0]) { |
|
this.menuScrollRefreshData($sub); |
|
var data = $sub.dataSM('scroll'), |
|
upEnd = $(window).scrollTop() - $sub.dataSM('parent-a').offset().top - data.itemH; |
|
$sub.dataSM('scroll-arrows').eq(0).css('margin-top', upEnd).end() |
|
.eq(1).css('margin-top', upEnd + this.getViewportHeight() - data.arrowDownH).end() |
|
.css('visibility', 'visible'); |
|
} |
|
} |
|
}, |
|
menuScrollRefreshData: function($sub) { |
|
var data = $sub.dataSM('scroll'), |
|
upEnd = $(window).scrollTop() - $sub.dataSM('parent-a').offset().top - data.itemH; |
|
if (this.cssTransforms3d) { |
|
upEnd = -(parseFloat($sub.css('margin-top')) - upEnd); |
|
} |
|
$.extend(data, { |
|
upEnd: upEnd, |
|
downEnd: upEnd + this.getViewportHeight() - data.subH |
|
}); |
|
}, |
|
menuScrollStop: function($sub) { |
|
if (this.scrollTimeout) { |
|
cancelAnimationFrame(this.scrollTimeout); |
|
this.scrollTimeout = 0; |
|
$sub.dataSM('scroll').step = 1; |
|
return true; |
|
} |
|
}, |
|
menuScrollTouch: function($sub, e) { |
|
e = e.originalEvent; |
|
if (isTouchEvent(e)) { |
|
var touchPoint = this.getTouchPoint(e); |
|
// neglect event if we touched a visible deeper level sub menu |
|
if (this.getClosestMenu(touchPoint.target) == $sub[0]) { |
|
var data = $sub.dataSM('scroll'); |
|
if (/(start|down)$/i.test(e.type)) { |
|
if (this.menuScrollStop($sub)) { |
|
// if we were scrolling, just stop and don't activate any link on the first touch |
|
e.preventDefault(); |
|
this.$touchScrollingSub = $sub; |
|
} else { |
|
this.$touchScrollingSub = null; |
|
} |
|
// update scroll data since the user might have zoomed, etc. |
|
this.menuScrollRefreshData($sub); |
|
// extend it with the touch properties |
|
$.extend(data, { |
|
touchStartY: touchPoint.pageY, |
|
touchStartTime: e.timeStamp |
|
}); |
|
} else if (/move$/i.test(e.type)) { |
|
var prevY = data.touchY !== undefined ? data.touchY : data.touchStartY; |
|
if (prevY !== undefined && prevY != touchPoint.pageY) { |
|
this.$touchScrollingSub = $sub; |
|
var up = prevY < touchPoint.pageY; |
|
// changed direction? reset... |
|
if (data.up !== undefined && data.up != up) { |
|
$.extend(data, { |
|
touchStartY: touchPoint.pageY, |
|
touchStartTime: e.timeStamp |
|
}); |
|
} |
|
$.extend(data, { |
|
up: up, |
|
touchY: touchPoint.pageY |
|
}); |
|
this.menuScroll($sub, true, Math.abs(touchPoint.pageY - prevY)); |
|
} |
|
e.preventDefault(); |
|
} else { // touchend/pointerup |
|
if (data.touchY !== undefined) { |
|
if (data.momentum = Math.pow(Math.abs(touchPoint.pageY - data.touchStartY) / (e.timeStamp - data.touchStartTime), 2) * 15) { |
|
this.menuScrollStop($sub); |
|
this.menuScroll($sub); |
|
e.preventDefault(); |
|
} |
|
delete data.touchY; |
|
} |
|
} |
|
} |
|
} |
|
}, |
|
menuShow: function($sub) { |
|
if (!$sub.dataSM('beforefirstshowfired')) { |
|
$sub.dataSM('beforefirstshowfired', true); |
|
if (this.$root.triggerHandler('beforefirstshow.smapi', $sub[0]) === false) { |
|
return; |
|
} |
|
} |
|
if (this.$root.triggerHandler('beforeshow.smapi', $sub[0]) === false) { |
|
return; |
|
} |
|
$sub.dataSM('shown-before', true); |
|
if (canAnimate) { |
|
$sub.stop(true, true); |
|
} |
|
if (!$sub.is(':visible')) { |
|
// highlight parent item |
|
var $a = $sub.dataSM('parent-a'), |
|
collapsible = this.isCollapsible(); |
|
if (this.opts.keepHighlighted || collapsible) { |
|
$a.addClass('highlighted'); |
|
} |
|
if (collapsible) { |
|
$sub.removeClass('sm-nowrap').css({ zIndex: '', width: 'auto', minWidth: '', maxWidth: '', top: '', left: '', marginLeft: '', marginTop: '' }); |
|
} else { |
|
// set z-index |
|
$sub.css('z-index', this.zIndexInc = (this.zIndexInc || this.getStartZIndex()) + 1); |
|
// min/max-width fix - no way to rely purely on CSS as all UL's are nested |
|
if (this.opts.subMenusMinWidth || this.opts.subMenusMaxWidth) { |
|
$sub.css({ width: 'auto', minWidth: '', maxWidth: '' }).addClass('sm-nowrap'); |
|
if (this.opts.subMenusMinWidth) { |
|
$sub.css('min-width', this.opts.subMenusMinWidth); |
|
} |
|
if (this.opts.subMenusMaxWidth) { |
|
var noMaxWidth = this.getWidth($sub); |
|
$sub.css('max-width', this.opts.subMenusMaxWidth); |
|
if (noMaxWidth > this.getWidth($sub)) { |
|
$sub.removeClass('sm-nowrap').css('width', this.opts.subMenusMaxWidth); |
|
} |
|
} |
|
} |
|
this.menuPosition($sub); |
|
} |
|
var complete = function() { |
|
// fix: "overflow: hidden;" is not reset on animation complete in jQuery < 1.9.0 in Chrome when global "box-sizing: border-box;" is used |
|
$sub.css('overflow', ''); |
|
}; |
|
// if sub is collapsible (mobile view) |
|
if (collapsible) { |
|
if (canAnimate && this.opts.collapsibleShowFunction) { |
|
this.opts.collapsibleShowFunction.call(this, $sub, complete); |
|
} else { |
|
$sub.show(this.opts.collapsibleShowDuration, complete); |
|
} |
|
} else { |
|
if (canAnimate && this.opts.showFunction) { |
|
this.opts.showFunction.call(this, $sub, complete); |
|
} else { |
|
$sub.show(this.opts.showDuration, complete); |
|
} |
|
} |
|
// accessibility |
|
$a.attr('aria-expanded', 'true'); |
|
$sub.attr({ |
|
'aria-expanded': 'true', |
|
'aria-hidden': 'false' |
|
}); |
|
// store sub menu in visible array |
|
this.visibleSubMenus.push($sub); |
|
this.$root.triggerHandler('show.smapi', $sub[0]); |
|
} |
|
}, |
|
popupHide: function(noHideTimeout) { |
|
if (this.hideTimeout) { |
|
clearTimeout(this.hideTimeout); |
|
this.hideTimeout = 0; |
|
} |
|
var self = this; |
|
this.hideTimeout = setTimeout(function() { |
|
self.menuHideAll(); |
|
}, noHideTimeout ? 1 : this.opts.hideTimeout); |
|
}, |
|
popupShow: function(left, top) { |
|
if (!this.opts.isPopup) { |
|
alert('SmartMenus jQuery Error:\n\nIf you want to show this menu via the "popupShow" method, set the isPopup:true option.'); |
|
return; |
|
} |
|
if (this.hideTimeout) { |
|
clearTimeout(this.hideTimeout); |
|
this.hideTimeout = 0; |
|
} |
|
this.$root.dataSM('shown-before', true); |
|
if (canAnimate) { |
|
this.$root.stop(true, true); |
|
} |
|
if (!this.$root.is(':visible')) { |
|
this.$root.css({ left: left, top: top }); |
|
// show menu |
|
var self = this, |
|
complete = function() { |
|
self.$root.css('overflow', ''); |
|
}; |
|
if (canAnimate && this.opts.showFunction) { |
|
this.opts.showFunction.call(this, this.$root, complete); |
|
} else { |
|
this.$root.show(this.opts.showDuration, complete); |
|
} |
|
this.visibleSubMenus[0] = this.$root; |
|
} |
|
}, |
|
refresh: function() { |
|
this.destroy(true); |
|
this.init(true); |
|
}, |
|
rootKeyDown: function(e) { |
|
if (!this.handleEvents()) { |
|
return; |
|
} |
|
switch (e.keyCode) { |
|
case 27: // reset on Esc |
|
var $activeTopItem = this.activatedItems[0]; |
|
if ($activeTopItem) { |
|
this.menuHideAll(); |
|
$activeTopItem[0].focus(); |
|
var $sub = $activeTopItem.dataSM('sub'); |
|
if ($sub) { |
|
this.menuHide($sub); |
|
} |
|
} |
|
break; |
|
case 32: // activate item's sub on Space |
|
var $target = $(e.target); |
|
if ($target.is('a') && this.handleItemEvents($target)) { |
|
var $sub = $target.dataSM('sub'); |
|
if ($sub && !$sub.is(':visible')) { |
|
this.itemClick({ currentTarget: e.target }); |
|
e.preventDefault(); |
|
} |
|
} |
|
break; |
|
} |
|
}, |
|
rootOut: function(e) { |
|
if (!this.handleEvents() || this.isTouchMode() || e.target == this.$root[0]) { |
|
return; |
|
} |
|
if (this.hideTimeout) { |
|
clearTimeout(this.hideTimeout); |
|
this.hideTimeout = 0; |
|
} |
|
if (!this.opts.showOnClick || !this.opts.hideOnClick) { |
|
var self = this; |
|
this.hideTimeout = setTimeout(function() { self.menuHideAll(); }, this.opts.hideTimeout); |
|
} |
|
}, |
|
rootOver: function(e) { |
|
if (!this.handleEvents() || this.isTouchMode() || e.target == this.$root[0]) { |
|
return; |
|
} |
|
if (this.hideTimeout) { |
|
clearTimeout(this.hideTimeout); |
|
this.hideTimeout = 0; |
|
} |
|
}, |
|
winResize: function(e) { |
|
if (!this.handleEvents()) { |
|
// we still need to resize the disable overlay if it's visible |
|
if (this.$disableOverlay) { |
|
var pos = this.$root.offset(); |
|
this.$disableOverlay.css({ |
|
top: pos.top, |
|
left: pos.left, |
|
width: this.$root.outerWidth(), |
|
height: this.$root.outerHeight() |
|
}); |
|
} |
|
return; |
|
} |
|
// hide sub menus on resize - on mobile do it only on orientation change |
|
if (!('onorientationchange' in window) || e.type == 'orientationchange') { |
|
var collapsible = this.isCollapsible(); |
|
// if it was collapsible before resize and still is, don't do it |
|
if (!(this.wasCollapsible && collapsible)) { |
|
if (this.activatedItems.length) { |
|
this.activatedItems[this.activatedItems.length - 1][0].blur(); |
|
} |
|
this.menuHideAll(); |
|
} |
|
this.wasCollapsible = collapsible; |
|
} |
|
} |
|
} |
|
}); |
|
|
|
$.fn.dataSM = function(key, val) { |
|
if (val) { |
|
return this.data(key + '_smartmenus', val); |
|
} |
|
return this.data(key + '_smartmenus'); |
|
}; |
|
|
|
$.fn.removeDataSM = function(key) { |
|
return this.removeData(key + '_smartmenus'); |
|
}; |
|
|
|
$.fn.smartmenus = function(options) { |
|
if (typeof options == 'string') { |
|
var args = arguments, |
|
method = options; |
|
Array.prototype.shift.call(args); |
|
return this.each(function() { |
|
var smartmenus = $(this).data('smartmenus'); |
|
if (smartmenus && smartmenus[method]) { |
|
smartmenus[method].apply(smartmenus, args); |
|
} |
|
}); |
|
} |
|
return this.each(function() { |
|
// [data-sm-options] attribute on the root UL |
|
var dataOpts = $(this).data('sm-options') || null; |
|
if (dataOpts) { |
|
try { |
|
dataOpts = eval('(' + dataOpts + ')'); |
|
} catch(e) { |
|
dataOpts = null; |
|
alert('ERROR\n\nSmartMenus jQuery init:\nInvalid "data-sm-options" attribute value syntax.'); |
|
}; |
|
} |
|
new $.SmartMenus(this, $.extend({}, $.fn.smartmenus.defaults, options, dataOpts)); |
|
}); |
|
}; |
|
|
|
// default settings |
|
$.fn.smartmenus.defaults = { |
|
isPopup: false, // is this a popup menu (can be shown via the popupShow/popupHide methods) or a permanent menu bar |
|
mainMenuSubOffsetX: 0, // pixels offset from default position |
|
mainMenuSubOffsetY: 0, // pixels offset from default position |
|
subMenusSubOffsetX: 0, // pixels offset from default position |
|
subMenusSubOffsetY: 0, // pixels offset from default position |
|
subMenusMinWidth: '10em', // min-width for the sub menus (any CSS unit) - if set, the fixed width set in CSS will be ignored |
|
subMenusMaxWidth: '20em', // max-width for the sub menus (any CSS unit) - if set, the fixed width set in CSS will be ignored |
|
subIndicators: true, // create sub menu indicators - creates a SPAN and inserts it in the A |
|
subIndicatorsPos: 'append', // position of the SPAN relative to the menu item content ('append', 'prepend') |
|
subIndicatorsText: '', // [optionally] add text in the SPAN (e.g. '+') (you may want to check the CSS for the sub indicators too) |
|
scrollStep: 30, // pixels step when scrolling long sub menus that do not fit in the viewport height |
|
scrollAccelerate: true, // accelerate scrolling or use a fixed step |
|
showTimeout: 250, // timeout before showing the sub menus |
|
hideTimeout: 500, // timeout before hiding the sub menus |
|
showDuration: 0, // duration for show animation - set to 0 for no animation - matters only if showFunction:null |
|
showFunction: null, // custom function to use when showing a sub menu (the default is the jQuery 'show') |
|
// don't forget to call complete() at the end of whatever you do |
|
// e.g.: function($ul, complete) { $ul.fadeIn(250, complete); } |
|
hideDuration: 0, // duration for hide animation - set to 0 for no animation - matters only if hideFunction:null |
|
hideFunction: function($ul, complete) { $ul.fadeOut(200, complete); }, // custom function to use when hiding a sub menu (the default is the jQuery 'hide') |
|
// don't forget to call complete() at the end of whatever you do |
|
// e.g.: function($ul, complete) { $ul.fadeOut(250, complete); } |
|
collapsibleShowDuration:0, // duration for show animation for collapsible sub menus - matters only if collapsibleShowFunction:null |
|
collapsibleShowFunction:function($ul, complete) { $ul.slideDown(200, complete); }, // custom function to use when showing a collapsible sub menu |
|
// (i.e. when mobile styles are used to make the sub menus collapsible) |
|
collapsibleHideDuration:0, // duration for hide animation for collapsible sub menus - matters only if collapsibleHideFunction:null |
|
collapsibleHideFunction:function($ul, complete) { $ul.slideUp(200, complete); }, // custom function to use when hiding a collapsible sub menu |
|
// (i.e. when mobile styles are used to make the sub menus collapsible) |
|
showOnClick: false, // show the first-level sub menus onclick instead of onmouseover (i.e. mimic desktop app menus) (matters only for mouse input) |
|
hideOnClick: true, // hide the sub menus on click/tap anywhere on the page |
|
noMouseOver: false, // disable sub menus activation onmouseover (i.e. behave like in touch mode - use just mouse clicks) (matters only for mouse input) |
|
keepInViewport: true, // reposition the sub menus if needed to make sure they always appear inside the viewport |
|
keepHighlighted: true, // keep all ancestor items of the current sub menu highlighted (adds the 'highlighted' class to the A's) |
|
markCurrentItem: false, // automatically add the 'current' class to the A element of the item linking to the current URL |
|
markCurrentTree: true, // add the 'current' class also to the A elements of all ancestor items of the current item |
|
rightToLeftSubMenus: false, // right to left display of the sub menus (check the CSS for the sub indicators' position) |
|
bottomToTopSubMenus: false, // bottom to top display of the sub menus |
|
collapsibleBehavior: 'default' // parent items behavior in collapsible (mobile) view ('default', 'toggle', 'link', 'accordion', 'accordion-toggle', 'accordion-link') |
|
// 'default' - first tap on parent item expands sub, second tap loads its link |
|
// 'toggle' - the whole parent item acts just as a toggle button for its sub menu (expands/collapses on each tap) |
|
// 'link' - the parent item acts as a regular item (first tap loads its link), the sub menu can be expanded only via the +/- button |
|
// 'accordion' - like 'default' but on expand also resets any visible sub menus from deeper levels or other branches |
|
// 'accordion-toggle' - like 'toggle' but on expand also resets any visible sub menus from deeper levels or other branches |
|
// 'accordion-link' - like 'link' but on expand also resets any visible sub menus from deeper levels or other branches |
|
}; |
|
|
|
return $; |
|
})); |