(function ($) {
// parseUri 1.2.2
// (c) Steven Levithan <stevenlevithan.com>
// MIT License
// Modified: Added partial URL-decoding support.
function parseUri (str) {
var o = parseUri.options,
m = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
uri = {},
i = 14;
while (i--) uri[o.key[i]] = m[i] || "";
uri[o.q.name] = {};
uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
if ($1) {
//Decode percent-encoded query parameters.
if (o.q.name === 'queryKey') {
//A space can be encoded either as "%20" or "+". decodeUriComponent doesn't decode plus signs,
//so we need to do that first.
$2 = $2.replace('+', ' ');
$1 = decodeURIComponent($1);
$2 = decodeURIComponent($2);
}
uri[o.q.name][$1] = $2;
}
});
return uri;
}
parseUri.options = {
strictMode: false,
key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
q: {
name: "queryKey",
parser: /(?:^|&)([^&=]*)=?([^&]*)/g
},
parser: {
strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
}
};
// --- parseUri ends ---
var hasRunAtLeastOnce = false;
function highlightCurrentMenu() {
hasRunAtLeastOnce = true;
//Find the menu item whose URL best matches the currently open page.
var currentUri = parseUri(location.href);
var bestMatch = {
uri: null,
/**
* @type {JQuery|null}
*/
link: null,
matchingParams: -1,
differentParams: 10000,
isAnchorMatch: false,
isTopMenu: false,
level: 0,
isHighlighted: false
};
//Special case: ".../wp-admin/" should match ".../wp-admin/index.php".
if (currentUri.path.match(/\/wp-admin\/$/)) {
currentUri.path = currentUri.path + 'index.php';
}
//Special case: if post_type is not specified for edit.php and post-new.php,
//WordPress assumes it is "post". Here we make this explicit.
if ((currentUri.file === 'edit.php') || (currentUri.file === 'post-new.php')) {
if (!currentUri.queryKey.hasOwnProperty('post_type')) {
currentUri.queryKey['post_type'] = 'post';
}
}
var adminMenu = $('#adminmenu');
adminMenu.find('li > a').each(function (index, link) {
var $link = $(link);
//Skip links that have no href or contain nothing but an "#anchor". Both AME and some
//other plugins (e.g. S2Member 120703) use them as separators.
if (!$link.is('[href]') || ($link.attr('href').substring(0, 1) === '#')) {
return;
}
var uri = parseUri(link.href);
//Same as above - use "post" as the default post type.
if ((uri.file === 'edit.php') || (uri.file === 'post-new.php')) {
if (!uri.queryKey.hasOwnProperty('post_type')) {
uri.queryKey['post_type'] = 'post';
}
}
//TODO: Consider using get_current_screen and the current_screen filter to get post types and taxonomies.
//Check for a close match - everything but query and #anchor.
var components = ['protocol', 'host', 'port', 'user', 'password', 'path'];
var isCloseMatch = true;
for (var i = 0; (i < components.length) && isCloseMatch; i++) {
isCloseMatch = isCloseMatch && (uri[components[i]] === currentUri[components[i]]);
}
if (!isCloseMatch) {
return; //Skip to the next link.
}
//Calculate the number of matching and different query parameters.
var matchingParams = 0, differentParams = 0, param;
for (param in uri.queryKey) {
if (uri.queryKey.hasOwnProperty(param)) {
if (currentUri.queryKey.hasOwnProperty(param)) {
//All parameters that are present in *both* URLs must have the same exact values.
if (uri.queryKey[param] === currentUri.queryKey[param]) {
matchingParams++;
} else {
return; //Skip to the next link.
}
} else {
differentParams++;
}
}
}
for (param in currentUri.queryKey) {
if (currentUri.queryKey.hasOwnProperty(param) && !uri.queryKey.hasOwnProperty(param)) {
differentParams++;
}
}
var isAnchorMatch = uri.anchor === currentUri.anchor;
var level = $link.parentsUntil(adminMenu, 'li').length;
var isHighlighted = $link.is('.current, .wp-has-current-submenu');
//Figure out if the current link is better than the best found so far.
//To do that, we compare them by several criteria (in order of priority):
var comparisons = [
{
better: (matchingParams > bestMatch.matchingParams),
equal: (matchingParams === bestMatch.matchingParams)
},
{
better: (differentParams < bestMatch.differentParams),
equal: (differentParams === bestMatch.differentParams)
},
{
better: (isAnchorMatch && (!bestMatch.isAnchorMatch)),
equal: (isAnchorMatch === bestMatch.isAnchorMatch)
},
//When a menu has multiple submenus, the first submenu usually has the same URL
//as the parent menu. We want to highlight this item and not just the parent.
{
better: ((level > bestMatch.level)
//Is this link a child of the current best match?
&& (!bestMatch.link || ($link.closest(bestMatch.link.closest('li')).length > 0))
),
equal: (level === bestMatch.level)
},
//All else being equal, the item highlighted by WP is probably a better match.
{
better: (isHighlighted && !bestMatch.isHighlighted),
equal: (isHighlighted === bestMatch.isHighlighted)
}
];
var isBetterMatch = false,
isEquallyGood = true,
j = 0;
while (isEquallyGood && !isBetterMatch && (j < comparisons.length)) {
isBetterMatch = comparisons[j].better;
isEquallyGood = comparisons[j].equal;
j++;
}
if (isBetterMatch) {
bestMatch = {
uri: uri,
link: $link,
matchingParams: matchingParams,
differentParams: differentParams,
isAnchorMatch: isAnchorMatch,
level: level,
isHighlighted: isHighlighted
}
}
});
var shouldUpdateStickiness = false;
function isInViewport($thing) {
if (!$thing || ($thing.length < 1)) {
return false; //A non-existent element is never visible.
}
var element = $thing.get(0);
if (!element.getBoundingClientRect || !window.innerHeight || !window.innerWidth) {
return true; //The browser doesn't support the necessary APIs, default to true.
}
var rect = element.getBoundingClientRect();
return (
(rect.top < window.innerHeight)
&& (rect.bottom > 0)
&& (rect.left < window.innerWidth)
&& (rect.right > 0)
);
}
//Highlight and/or expand the best matching menu.
if (bestMatch.link !== null) {
var bestMatchLink = bestMatch.link;
var linkAndItem = bestMatchLink.add(bestMatchLink.closest('li'));
var allParentMenus = bestMatchLink.parentsUntil(adminMenu, 'li');
var topLevelParent = allParentMenus.filter('li.menu-top').last();
//console.log('Best match is: ', bestMatchLink);
shouldUpdateStickiness = !isInViewport(bestMatchLink);
var otherHighlightedMenus = $('li.wp-has-current-submenu, li.menu-top.current', '#adminmenu')
.not(allParentMenus)
.not('.ws-ame-has-always-open-submenu');
var otherHighlightedSubmenus = adminMenu.find('li.current,a.current').not(linkAndItem);
var isWrongItemHighlighted = !bestMatchLink.hasClass('current') || (otherHighlightedSubmenus.length > 0);
var isWrongMenuHighlighted = !topLevelParent.is('.wp-has-current-submenu, .current') ||
(otherHighlightedMenus.length > 0);
if (isWrongMenuHighlighted) {
//Account for users who use the Expanded Admin Menus plugin to keep all menus expanded.
var shouldCloseOtherMenus = !$('div.expand-arrow', '#adminmenu').get(0);
if (shouldCloseOtherMenus) {
otherHighlightedMenus
.add('> a', otherHighlightedMenus)
.removeClass('wp-menu-open wp-has-current-submenu current')
.addClass('wp-not-current-submenu');
}
var parentMenuAndLink = topLevelParent.add('> a.menu-top', topLevelParent.get(0));
parentMenuAndLink.removeClass('wp-not-current-submenu');
if (topLevelParent.hasClass('wp-has-submenu')) {
parentMenuAndLink.addClass('wp-has-current-submenu wp-menu-open');
}
shouldUpdateStickiness = true;
//Workaround: Prevent the current submenu from "jumping around" when you click an item. This glitch is
//caused by a `focusin` event handler in common.js. WP adds this handler to all top level menus that
//are not the current menu. Since we're changing the current menu, we need to also remove this handler.
if (typeof topLevelParent['off'] === 'function') {
topLevelParent.off('focusin.adminmenu');
}
}
if (isWrongItemHighlighted) {
adminMenu.find('.current').removeClass('current');
linkAndItem.addClass('current');
shouldUpdateStickiness = true;
}
//WordPress adds the "current" class only to the <li> that is the direct parent of the highlighted link.
//If the highlighted item is a submenu, its top-level parent shouldn't have this class.
bestMatchLink.closest('li').parentsUntil(adminMenu, 'li').children('a').addBack().removeClass('current');
bestMatchLink.closest('ul').parentsUntil(adminMenu, 'li').addClass('ame-has-highlighted-item');
}
//If a submenu is highlighted, so must be its parent.
//In some cases, if we decide to stick with the WP-selected highlighted menu,
//this might not be the case and we'll need to fix it.
var parentOfHighlightedMenu = $('.wp-submenu a.current', '#adminmenu').closest('.menu-top').first();
parentOfHighlightedMenu
.add('> a.menu-top', parentOfHighlightedMenu)
.removeClass('wp-not-current-submenu')
.addClass('wp-has-current-submenu wp-menu-open');
if (shouldUpdateStickiness) {
//Note: WordPress switches the admin menu between `position: fixed` and `position: relative` depending on
//how tall it is compared to the browser window. Opening a different submenu can change the menu's height,
//so we must trigger the position update to avoid bugs. If we don't, we can end up with a very tall menu
//that's not scrollable (due to being stuck with `position: fixed`).
if ((typeof window['stickyMenu'] === 'object') && (typeof window['stickyMenu']['update'] === 'function')) {
window.stickyMenu.update();
} else {
//As of WP core revision 29599 (2014-10-05) the `stickyMenu` object no longer exists
//and the replacement (`setPinMenu` in common.js) is not accessible from an outside scope.
//We'll resort to faking a resize event to make WP update the menu height and state.
$(document).trigger('wp-window-resized');
}
}
}
//Other scripts can trigger this feature by using a custom event.
$(document).on('adminMenuEditor:highlightCurrentMenu', highlightCurrentMenu);
$(function () {
if (!hasRunAtLeastOnce) {
highlightCurrentMenu();
}
});
})(jQuery);