Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* jshint undef: true, devel: true, typed: true, jquery: true, strict: true, eqeqeq: true, freeze: true, latedef: true, shadow: outer, varstmt: true, quotmark: single, esversion: 6, futurehostile: true */
/* global importArticle */
mw.loader.using(['mediawiki.api', 'mediawiki.util', 'mediawiki.Title'], () => {
'use strict';
// Double load protection and default settings loadout
(window.dev = window.dev || {}).betterTopNav = window.dev.betterTopNav || {
resize: true,
tools: true,
hovermenu: true
};
if (window.dev.betterTopNav._LOADED) {return;}
else {window.dev.betterTopNav._LOADED = true;}
// Load styles
importArticle({ type:'style', article: 'u:dev:MediaWiki:BetterTopNav.css' });
let
api = new mw.Api(),
config = mw.config.values,
// Main class
bTN = {
expandSearch: () => {
let sett = isNaN(window.dev.betterTopNav.results) ? 25 : window.dev.betterTopNav.results;
switch (typeof sett) {
case 'boolean':
sett = [50, {u:2, f:6, p:4, mw:8, t:10, c:14, m:828}];
break;
case 'number' || 'string':
sett = [(sett === 'max' ? 50 : sett), {u:2, f:6, p:4, mw:8, t:10, c:14, m:828}];
break;
case 'object':
if (!Array.isArray(sett)) {
sett = [50, sett];
}
break;
}
document.body.classList.add('dev-betterTopNav-expandSearch');
// Check what string is more relevant to `s` between `s1` and `s2`
// `false` means `s1` is more relevant
// `true` means `s2` is more relevant
/// 4th argument can be set to `true` to return the term instead of a boolean
function relevance(s, s1, s2, rT) {
function matchRate(a, b) {
let longer = (a.length > b.length ? a : b).toLowerCase(),
shorter = (a === longer ? b : a).toLowerCase(),
longerLength = longer.length,
matchCount = 0,
initMatch = null;
if (longerLength === 0) { return 1.0; }
for (let i = 0; i < longerLength; i++) {
if (longer[i] === shorter[i]) { matchCount++; }
else if (initMatch === null) {initMatch = i;}
}
return [(matchCount / longerLength) * 100, initMatch];
}
if (s.toLowerCase()===s1.toLowerCase()) {return rT ? s1 : false}
else if (s.toLowerCase()===s2.toLowerCase()) {return rT ? s2 : true}
let m1 = matchRate(s, s1),
m2 = matchRate(s, s2),
rel = m1[1] !== m2[1] ? m1[1] < m2[1]
: (m1[0] === m2[0] ? s1.localeCompare(s2) >= 0 : m1[0] < m2[0]);
if (rT) { return rel ? s2 : s1 ; }
else { return rel; }
}
// Detect DOM changes for when to load what
let search_observer = new MutationObserver(mw.util.throttle((ml)=>{
const nav = $(ml[0].target);
if (nav.find('input').length === 0 || nav.find('input').val().length === 0 ) { return; }
let searchquery = nav.find('input').val();
if (searchquery.length>0 && nav.is(':has(.search-app__suggestion, .search-app__no-suggestions)') && !nav.is(':has(.search-app__custom-suggestion, .search-app__heading)')) {
// Load search results
let opts = {
action: 'query',
list: 'search',
srlimit: sett[0],
};
// Check for namespaces
let namespace = searchquery.includes(':') ? searchquery.split(':')[0].trim().toLowerCase().replace(/ /g, '_') : null;
let namespaceID = namespace !== null ? (config.wgNamespaceIds[namespace] || sett[1][namespace]) : null;
if (namespaceID === undefined){namespaceID = null;}
if (namespace !== null && namespaceID !== null) {
opts.srsearch = searchquery.replace(/^.*?:\s*/, '');
searchquery = searchquery.replace(/^.*?:\s*/, config.wgFormattedNamespaces[namespaceID]+':');
opts.srnamespace = config.wgNamespaceIds[namespace] || sett[1][namespace];
if (opts.srsearch === '') {return;}
} else {
opts.srsearch = searchquery;
opts.srnamespace = 0;
}
let keyword = new RegExp(searchquery.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'), 'i');
// Do search then check for redirects
api.get(opts).then((_d)=>{
if (_d.query && _d.query.search && _d.query.search.length>0) {
let pages = _d.query.search.map((e)=>e); // make a copy of search results as we may need to add redirect data
let listResults = () => {
let list = nav.is(':has(.search-app__suggestions-list)') ? nav.find('.search-app__suggestions-list') :
nav.find('.search-app__suggestions-box').append($('<ul>', {'class':'search-app__suggestions-list'}));
pages.sort((a, b)=>relevance(searchquery,
a.redirect ? relevance(searchquery, a.title, a.redirect, true) : a.title,
b.redirect ? relevance(searchquery, b.title, b.redirect, true) : b.title
));
if ($('.search-app__heading').length !== 0){return;}
list.empty(); // Make sure no default results exist
nav.find('.search-app__suggestions-box').addClass('search-app__custom-suggestions');
pages.forEach((page)=>{
let label = page.title;
mw.Title.exist.set(page.title.toLowerCase());
let useRD = !page.redirect ? false : relevance(searchquery, page.title, page.redirect);
if ( namespaceID === null && page.ns !== 0 ) {
label = label.replace(/^[^:]+:/, '');
}
label = label.replace(keyword, '<b>$&</b>');
if ( namespaceID === null && page.ns !== 0 ) {
label = config.wgFormattedNamespaces[page.ns]+':'+label;
}
if (useRD) {
label += ' <span class="search-app__suggestion-redirect">'+mw.msg('redirectedfrom', page.redirect.replace(keyword, '<span class="search-app__suggestion-highlight">$&</span>'))+'</span>';
mw.Title.exist.set(page.redirect.toLowerCase());
}
list.append($('<li>', {
'class': 'search-app__suggestion',
dir: 'auto',
html: $('<a>', {
'class': 'search-app__suggestion-link',
href: location.origin+new mw.Title(useRD ? page.redirect : page.title).getUrl(),
html: label
})
}));
});
list.append($('<li>',{
'class': 'search-app__suggestion search-app__suggestion-all',
html: $('<a>', {
'class': 'search-app__suggestion-link',
href: location.origin+new mw.Title('Special:Search').getUrl()+'?scope=internal&query='+encodeURIComponent(searchquery),
text: mw.msg('search-modal-see-all-results', searchquery)
})
}));
};
api.get({
action: 'query',
prop: 'redirects',
rdprop: 'pageid|title',
rdlimit: 'max',
titles: _d.query.search.map((_i)=>_i.title).join('|'),
redirects: 'false',
}).then((data)=>{
pages.forEach((page, _i)=>{
if (
page.pageid &&
data.query.pages[page.pageid] &&
data.query.pages[page.pageid].redirects
) {
let rd = (data.query.pages[page.pageid].redirects || []).filter((elem, index)=>{
return elem && elem.title && keyword.test(elem.title);
})[0];
if (rd) {
pages[_i].redirect = rd.title;
}
}
});
listResults();
});
} else {
const noRes = $('<p>', {
'class': 'search-app__no-suggestions',
dir: 'auto',
text: mw.msg('fd-global-top-navigation-no-search-results')
});
nav.find('.search-app__suggestions-box').empty().append(noRes);
nav.find('.search-app__suggestions-box').addClass('search-app__custom-suggestions');
}
}).catch(console.log);
}
}, 150));
search_observer.observe(document.querySelector('#global-top-navigation .search-app__wrapper'), {childList: true, subtree: true});
search_observer.observe(document.querySelector('#community-navigation .search-app__wrapper'), {childList: true, subtree: true});
// Replace default arrow navigation as it's hardcoded to the default results
$('.search-app__input').on('keydown', (e) => {
if (e.key === 'Enter') {
e.stopPropagation(); // Turn off default
e.preventDefault(); // Dont move caret
if ($('.search-app__suggestion--active').length>0) {
document.querySelector('.search-app__suggestions-box .search-app__suggestion--active > a').click();
} else if ($('.search-app__input').val().length>0) {
let v = $('.search-app__input').val();
window.location = mw.Title.exists(v.toLowerCase()) ? new mw.Title(v).getUrl() :
location.origin+new mw.Title('Special:Search').getUrl()+'?scope=internal&query='+encodeURIComponent(v);
}
} else if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
e.stopPropagation(); // Turn off default
e.preventDefault(); // Dont move caret
let opts = $('.search-app__suggestions-box .search-app__suggestion'),
target = opts.index($('.search-app__suggestion--active'))+{ArrowUp: -1, ArrowDown: +1}[e.key];
if (target <= -1) {target = opts.length-1;}
else if (target >= opts.length) {target = 0;}
opts
.filter('.search-app__suggestion--active')
.add(opts.get(target))
.toggleClass('search-app__suggestion--active');
opts.filter('.search-app__suggestion--active').focus();
document.querySelector('.search-app__suggestion--active').scrollIntoView({behavior: 'smooth', block: 'start', inline: 'start'});
let selPage = opts.filter('.search-app__suggestion--active.search-app__suggestion-all').length>0 ? $('.search-app__input').val() :
(()=>{
let p = $('.search-app__suggestion--active .search-app__suggestion-link').clone();
p.children('.search-app__suggestion-redirect').remove();
return p.text().trim();
})();
$('.search-app__input').attr('value', selPage);
$('.search-app__input').val(selPage);
}
});
},
// Allow resizing the search bar to the left
resizeSearch: () => {
document.body.classList.add('dev-betterTopNav-resizeSearch');
$('.search-app__wrapper').append('<div class="search-resizer"><svg class="wds-icon wds-icon-tiny"><use xlink:href="#wds-icons-menu-control-tiny"></use></svg></div>');
let resizer = $('.search-container .search-resizer'),
resizable, startX, startWidth;
// Resize functions
function doDrag(e) { resizable.css('width', (startWidth - e.clientX + startX) + 'px'); }
function stopDrag(e) {
document.documentElement.removeEventListener('mousemove', doDrag, false);
document.documentElement.removeEventListener('mouseup', stopDrag, false);
api.post({
action: 'options',
token: mw.user.tokens.values.csrfToken,
optionname: 'userjs-betterTopNav',
optionvalue: JSON.stringify({
global: parseInt($('.global-top-navigation .search-container .search-app__wrapper').outerWidth(), 10),
sticky: parseInt($('.fandom-sticky-header .search-container .search-app__wrapper').outerWidth(), 10)
})
});
}
function initDrag(e) {
startX = e.clientX;
resizable = $(e.target.closest('.search-app__wrapper'));
startWidth = parseInt( resizable.outerWidth(), 10);
document.documentElement.addEventListener('mousemove', doDrag, false);
document.documentElement.addEventListener('mouseup', stopDrag, false);
}
// Local storage default widths
if (mw.user.options.values['userjs-betterTopNav']) {
const _default = JSON.parse(mw.user.options.values['userjs-betterTopNav']);
$('.global-top-navigation .search-container .search-app__wrapper').css('width', _default['global']+'px'); // jshint ignore:line
$('.fandom-sticky-header .search-container .search-app__wrapper').css('width', _default['sticky']+'px'); // jshint ignore:line
}
resizer.on( 'mousedown', initDrag );
},
// Customization of sticky nav tools
customizeTools: () => {
let sett = window.dev.betterTopNav.tools;
document.body.classList.add('dev-betterTopNav-customizeTools');
if (!Array.isArray(sett) || sett.length === 0) {
sett = [];
$('#community-navigation #wiki-tools-menu li > a').each((_, el) => {
let item = {
'track': el.getAttribute('data-tracking-label'),
'class': el.getAttribute('class'),
'link': el.getAttribute('href'),
'text': el.textContent.trim()
};
if (
el.parentNode.classList.contains('wiki-tool-in-dropdown') ||
el.classList.contains('wiki-tools__theme-switch')
) {
item.icon = $(
'.wiki-tools > a[data-tracking-label="'+el.getAttribute('data-tracking-label')+'"],'+
'.wiki-tools > a[data-tracking="'+el.getAttribute('data-tracking-label')+'"]'
).first().html().trim();
}
sett.push(item);
});
}
if (Array.isArray(sett) && sett.length>0) {
$('#community-navigation .wiki-tools > a').remove();
$('#community-navigation .wiki-tools #wiki-tools-menu li').remove();
sett.forEach((item) => {
if (item.link && item.text) {
$('#community-navigation .wiki-tools > .wds-dropdown ul').append(
$('<li>', {'class':(item.icon ? 'wiki-tool-in-dropdown' : '')}).append(
$('<a>', {
href: item.link,
'class': item.class,
title: item.title || item.text || 'Unknown',
'data-tracking-label': item.track
}).append(
(item.icon ? (item.icon+' ') : ''),
item.text
)
)
);
if (item.icon) {
$('#community-navigation .wiki-tools > .wds-dropdown').before(
$('<a>', {
href: item.link,
'class': 'wds-button wds-is-text is-hidden-on-smaller-breakpoints '+(item.class || ''),
title: item.title || item.text || 'Unknown',
'data-tracking-label': item.track
}).append(item.icon)
);
}
}
});
}
// Add the tools to global bar too
if (!document.querySelector('#global-top-navigation .wiki-tools')) {
$('#global-top-navigation .search-container').after(
$('#community-navigation .wiki-tools').clone()
);
}
},
hoverUserMenu: () => {
document.body.classList.add('dev-betterTopNav-hoverUserMenu');
// Inert attr makes the popup uninteractable, thx fandom, very cool
let observer = new MutationObserver(() => { $('#global-top-navigation .global-action__user .navigation-panel').removeAttr('inert'); });
observer.observe(document.querySelector('#global-top-navigation .global-action__user'), { attributes: true, subtree: true });
// Render custom list once the init process is done
bTN.waitFor('#global-top-navigation .global-action__user .navigation-panel ul>li>a', () => {
// Avoid initalization defaults
const hasMW = $('#global-top-navigation .global-action__user .navigation-panel ul > li > a[data-label="account.message-wall"]').length > 0;
$('#global-top-navigation .global-action__user > button').click();
$('#global-top-navigation .global-action__user .navigation-panel ul').empty();
document.body.classList.remove('btn-noflash');
// Render new list
let links = [
{href: new mw.Title('User:'+mw.user.getName()).getUrl(), text: mw.msg('fd-global-navigation-user-view-profile')},
hasMW
? {href: new mw.Title('Message_Wall:'+mw.user.getName()).getUrl(), text: mw.msg('fd-global-navigation-user-message-wall')}
: {href: new mw.Title('User_talk:'+mw.user.getName()).getUrl(), text: mw.msg('fd-global-navigation-user-my-talk')},
{href: new mw.Title('Special:Contributions/'+mw.user.getName()).getUrl(), text: mw.msg('mycontributions')},
{href: new mw.Title('Special:Preferences').getUrl(), text: mw.msg('fd-global-navigation-user-my-preferences')},
{href: 'https://www.fandom.com/my-fandom', text: mw.msg('fd-global-navigation-link-my-fandom-label')},
{href: 'https://auth.fandom.com/logout?source=mw&redirect='+encodeURIComponent(window.location.href), text: mw.msg('fd-global-navigation-user-sign-out')}
];
if (Array.isArray(window.dev.betterTopNav.hovermenu) && window.dev.betterTopNav.hovermenu.length>0) { links = links.concat(window.dev.betterTopNav.hovermenu); }
links.forEach((link) => {
$('#global-top-navigation .global-action__user .navigation-panel ul').append(
$('<li>', {'class':'user-panel__list-item'}).append(
$('<a>', link)
)
);
});
});
document.body.classList.add('btn-noflash');
$('#global-top-navigation .global-action__user > button').click();
},
// Delay until element exists to run function
waitFor: (query, callback, extraDelay) => {
if ('function' === typeof callback && 'string' === typeof query) {
extraDelay = extraDelay || 0;
if (document.querySelector(query)) {
setTimeout(callback, extraDelay);
} else {
// set up the mutation observer
let observer = new MutationObserver((mutations, me) => {
// mutations is an array of mutations that occurred
// me is the MutationObserver instance
let targetNode = document.querySelector(query);
if (targetNode) {
setTimeout(callback, extraDelay);
me.disconnect(); // stop observing
return;
}
});
// start observing
observer.observe(document, {
childList: true,
subtree: true
});
}
}
},
};
// Load conditions
const msgs = [
'redirectedfrom',
'search-modal-see-all-results',
'fd-global-top-navigation-no-search-results',
'fd-global-navigation-user-view-profile',
'fd-global-navigation-user-message-wall',
'fd-global-navigation-user-my-talk',
'mycontributions',
'fd-global-navigation-user-my-preferences',
'fd-global-navigation-link-my-fandom-label',
'fd-global-navigation-user-sign-out'
];
api.loadMessagesIfMissing(msgs).then(() => {
if (window.dev.betterTopNav.results) { bTN.waitFor('#global-top-navigation .search-app__wrapper input', bTN.expandSearch); }
if (window.dev.betterTopNav.resize) { bTN.waitFor('.search-app__wrapper', bTN.resizeSearch); }
if (window.dev.betterTopNav.tools) { bTN.waitFor('.search-app__wrapper', bTN.customizeTools); }
if (window.dev.betterTopNav.hovermenu) { bTN.waitFor('#global-top-navigation .global-action__user > button', bTN.hoverUserMenu); }
});
});