menu-highlight-fix.js
11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
(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);