MediaWiki:Timeless.js: Difference between revisions
From Inkipedia, the Splatoon wiki
GuyPerfect (talk | contribs) m (More troubleshooting) Tags: Blanking Manual revert Reverted |
GuyPerfect (talk | contribs) m (Undo revision 299802 by GuyPerfect (talk)) Tag: Undo |
||
Line 1: | Line 1: | ||
// All JavaScript here will be loaded for users of the Timeless skin | |||
/* | |||
$(document).ready(function() { | |||
// On desktop, display text logo once user scrolls down | |||
function toggleTextLogo() { | |||
if ($(window).width() > 1099) { | |||
if ($(window).scrollTop() > 130) { | |||
if ($('#p-logo-text').css('opacity') === '0') { | |||
$('#p-logo-text').css('visibility', 'visible'); | |||
$('#p-logo-text').stop().animate({ opacity: 1 }, 250); | |||
} | |||
} else { | |||
if ($('#p-logo-text').css('opacity') === '1') { | |||
$('#p-logo-text').css('visibility', 'hidden'); | |||
$('#p-logo-text').stop().animate({ opacity: 0 }, 250); | |||
} | |||
} | |||
} else { | |||
if ($('#p-logo-text').css('opacity') === '0') { | |||
$('#p-logo-text').css('visibility', 'visible'); | |||
$('#p-logo-text').stop().animate({ opacity: 1 }, 250); | |||
} | |||
} | |||
} | |||
toggleTextLogo(); | |||
$(window).scroll(toggleTextLogo); | |||
$(window).resize(toggleTextLogo); | |||
// Open Social links in new tab/window | |||
$( '#n-Discord' ).find( 'a' ).attr( 'target' , '_blank' ); | |||
$( '#n-Twitter' ).find( 'a' ).attr( 'target' , '_blank' ); | |||
$( '#n-Instagram' ).find( 'a' ).attr( 'target' , '_blank' ); | |||
$( '#n-Facebook' ).find( 'a' ).attr( 'target' , '_blank' ); | |||
}); | |||
*/ | |||
// Ensure the document has finished loading | |||
new Promise(function(resolve) { | |||
if (document.readyState == "complete") | |||
return resolve(); | |||
window.addEventListener("load", resolve); | |||
}).then(function() { | |||
// Auto-increment key for dynamic element IDs | |||
var nextId = 0; | |||
// Initial focus order | |||
document.getElementById("p-banner" ).tabIndex = 1; | |||
document.getElementById("searchInput").tabIndex = 2; | |||
////////////////////////////// Utility functions ////////////////////////////// | |||
// Test whether an element could receive focus | |||
var isVisible = function(element) { | |||
for (; element instanceof Element; element = element.parentNode) { | |||
var style = getComputedStyle(element); | |||
if ( | |||
style.display == "none" || | |||
style.visibility == "hidden" || | |||
parseFloat(style.top) <= -1000 | |||
) return false; | |||
} | |||
return true; | |||
}; | |||
// Retrieve all focusable children of an element | |||
var getFocusableChildren = function(element) { | |||
return Array.from(element.querySelectorAll( | |||
"*:is(a[href],area,button,details,input," + | |||
"textarea,select,[tabindex='0']):not([disabled])" | |||
)).filter(function(e) { return isVisible(e); }); | |||
}; | |||
// Determine which responsive layout is in effect | |||
var getLayoutSize = function() { | |||
return ( | |||
window.innerWidth <= 850 ? "small" : | |||
window.innerWidth < 1100 ? "medium" : | |||
"large" | |||
); | |||
}; | |||
//////////////////////////////// Search button //////////////////////////////// | |||
var searchInput = document.getElementById("searchButton"); | |||
var searchButton = document.createElement("button"); | |||
searchButton.id = searchInput.id; | |||
searchButton.title = searchInput.title; | |||
searchButton.append(document.createElement("div")); | |||
searchInput.after(searchButton); | |||
searchInput.remove(); | |||
var badButton = document.getElementById("mw-searchButton"); | |||
if (badButton) | |||
badButton.remove(); | |||
/////////////////////////////// Drop-down menus /////////////////////////////// | |||
// Configure drop-down elements | |||
var dropDowns = [ | |||
{ root: "#personal" , menu: "#personal-inner", | |||
small: true , medium: true , large: true }, | |||
{ root: "#site-navigation", menu: "#site-navigation .sidebar-inner", | |||
small: true , medium: true , large: false }, | |||
{ root: "#site-tools" , menu: "#site-tools .sidebar-inner", | |||
small: true , medium: true , large: false }, | |||
{ root: "#page-tools" , menu: "#page-tools .sidebar-inner", | |||
small: false, medium: true , large: false }, | |||
{ root: "#ca-more" , menu: "#page-tools .sidebar-inner", | |||
small: true , medium: false, large: false }, | |||
{ root: "#ca-languages" , menu: "#other-languages div", | |||
small: true , medium: false, large: false }, | |||
]; | |||
for (var x = 0; x < dropDowns.length; x++) { | |||
(function(props) { | |||
// Top-level control | |||
var root = document.querySelector(props.root); | |||
if (!root) | |||
return; // Languages may not be available | |||
// Interactive element | |||
var ctrl = root.querySelector("*:is(h2,span)"); | |||
// Internal menu | |||
var menu = document.querySelector(props.menu); | |||
if (menu) | |||
menu.id = menu.id || ("id-timeless-" + nextId++); | |||
// Watch the viewport size | |||
window.addEventListener("resize", function() { | |||
var layout = getLayoutSize(); | |||
// Control is a drop-down | |||
if (props[layout]) { | |||
ctrl.tabIndex = 0; | |||
ctrl.setAttribute("role" , "button"); | |||
if (menu) { | |||
ctrl.setAttribute("aria-controls", menu.id); | |||
ctrl.setAttribute("aria-haspopup", "menu"); | |||
} | |||
ctrl.setAttribute("aria-expanded", Array.from(ctrl.classList) | |||
.indexOf("dropdown-active") != -1); | |||
} | |||
// Control is not a drop-down | |||
else { | |||
ctrl.removeAttribute("tabindex"); | |||
ctrl.removeAttribute("role"); | |||
ctrl.removeAttribute("aria-controls"); | |||
ctrl.removeAttribute("aria-expanded"); | |||
ctrl.removeAttribute("aria-haspopup"); | |||
} | |||
// Menu is not available | |||
if (!menu) | |||
return; | |||
// Menu is inside a drop-down | |||
if ( | |||
props.menu.startsWith("#page-tools") && layout != "large" || | |||
props[layout] | |||
) menu.setAttribute("role", "menu"); | |||
// Menu is not inside a drop-down | |||
else menu.removeAttribute("role"); | |||
}); | |||
// Watch the class attribute of the control | |||
new MutationObserver(function() { | |||
if (!props.large && getLayoutSize() == "large") | |||
return; | |||
ctrl.setAttribute("aria-expanded", | |||
Array.from(root.classList).indexOf("dropdown-active") != -1); | |||
}).observe(root, { attributeFilter: [ "class" ] }); | |||
// Respond to pointer presses on the control | |||
ctrl.addEventListener("pointerdown", function() { ctrl.focus(); }); | |||
// Respond to key presses on the control | |||
ctrl.addEventListener("keydown", function(e) { | |||
// Filter by key | |||
switch (e.key) { | |||
case " ": case "Enter": case "ArrowDown": case "ArrowUp": | |||
break; | |||
default: return; | |||
} | |||
// Handle the event | |||
e.preventDefault(); | |||
e.stopPropagation(); | |||
// Open the menu | |||
if (ctrl.getAttribute("aria-expanded") != "true") | |||
ctrl.click(); | |||
// The menu is not available | |||
if (!menu) | |||
return; | |||
// Focus the appropriate menu item | |||
var items = getFocusableChildren(menu); | |||
if (items.length == 0) | |||
return; | |||
items[e.key == "ArrowUp" ? items.length - 1 : 0].focus(); | |||
}); | |||
// Respond to key presses on menu items | |||
if (menu && props.root != "#ca-more") | |||
menu.addEventListener("keydown", function(e){ | |||
if (!props.large && getLayoutSize() == "large") | |||
return; | |||
var items = getFocusableChildren(menu); | |||
var index = items.indexOf(document.activeElement); | |||
var focus; | |||
switch (index == -1 ? null : e.key) { | |||
case "ArrowDown": | |||
focus = items[(index + 1) % items.length]; | |||
break; | |||
case "ArrowUp": | |||
focus = items[(index + items.length - 1) % items.length]; | |||
break; | |||
case "End": | |||
focus = items[items.length - 1]; | |||
break; | |||
case " ": | |||
case "Enter": | |||
focus = document.activeElement; | |||
focus.click(); | |||
break; | |||
case "Home": | |||
focus = items[0]; | |||
break; | |||
case "Escape": | |||
case "Tab": | |||
focus = | |||
props.root == "#page-tools" && | |||
getLayoutSize() == "small" ? | |||
document.querySelector("#ca-more span") : ctrl | |||
; | |||
focus.click(); | |||
break; | |||
case null: | |||
focus = items[0] || ctrl; | |||
break; | |||
default: return; | |||
} | |||
// Handle the event | |||
e.preventDefault(); | |||
e.stopPropagation(); | |||
// Focus the appropriate element | |||
focus.focus(); | |||
}); | |||
})(dropDowns[x]); | |||
// Simulate a window resize | |||
window.dispatchEvent(new Event("resize")); | |||
} | |||
///////////////////////////////// Focus order ///////////////////////////////// | |||
// Specify the desired element order by unique selector | |||
// If the specified element isn't focusable, takes all its focusable children | |||
var ORDERS = { | |||
small: [ | |||
"#site-navigation h2", | |||
"#p-banner", | |||
"#pt-notifications-alert a", | |||
"#pt-notifications-notice a", | |||
"#site-tools h2", | |||
"#personal h2", | |||
"#searchInput", | |||
"#searchButton", | |||
"#content > .mw-indicators", | |||
"#p-namespaces", | |||
"#p-views", | |||
"#p-more", | |||
"#bodyContent", | |||
"#content-bottom-stuff", | |||
"#footer-list", | |||
"#footer-copyrightico a", | |||
"#footer-poweredbyico a", | |||
"#footer-strategywikiico a" | |||
], | |||
medium: [ | |||
"#p-banner", | |||
"#searchInput", | |||
"#searchButton", | |||
"#pt-notifications-alert a", | |||
"#pt-notifications-notice a", | |||
"#personal h2", | |||
"#site-navigation h2", | |||
"#site-tools h2", | |||
"#page-tools h2", | |||
"#content > .mw-indicators", | |||
"#p-namespaces", | |||
"#p-views", | |||
"#bodyContent", | |||
"#content-bottom-stuff", | |||
"#footer-list", | |||
"#footer-strategywikiico a", | |||
"#footer-poweredbyico a", | |||
"#footer-copyrightico a" | |||
], | |||
large: [ | |||
"#p-banner", | |||
"#searchInput", | |||
"#searchButton", | |||
"#pt-notifications-alert a", | |||
"#pt-notifications-notice a", | |||
"#personal h2", | |||
"#p-logo a", | |||
"#site-navigation", | |||
"#site-tools", | |||
"#mw-related-navigation", | |||
"#content > .mw-indicators", | |||
"#p-namespaces", | |||
"#p-views", | |||
"#bodyContent", | |||
"#content-bottom-stuff", | |||
"#footer-list", | |||
"#footer-strategywikiico a", | |||
"#footer-poweredbyico a", | |||
"#footer-copyrightico a" | |||
] | |||
} | |||
// Watch key presses on any element that doesn't trap one | |||
document.body.addEventListener("keydown", function(e) { | |||
// Only monitoring Tab key | |||
if (e.key != "Tab") | |||
return; | |||
// Construct a list of all focusable elements | |||
var elements = ORDERS[getLayoutSize()].slice(); | |||
for (var y = 0; y < elements.length; y++) { | |||
var selector = elements[y]; | |||
var element = elements[y] = document.querySelector(selector); | |||
// The element is not visible | |||
if (!element || !isVisible(element)) { | |||
elements.splice(y--, 1); | |||
continue; | |||
} | |||
// The element is focusable | |||
if (element.parentNode.querySelector( | |||
selector + ":is(a[href],area,button,details,input," + | |||
"textarea,select,[tabindex='0']):not([disabled])" | |||
)) continue; | |||
// The element is not focusable | |||
var children = getFocusableChildren(element); | |||
elements.splice(y, 1); | |||
for (var x = 0; x < children.length; x++) | |||
elements.splice(y++, 0, children[x]); | |||
y--; | |||
} | |||
// Let the user agent decide what to do | |||
var index = elements.indexOf(document.activeElement); | |||
if (index == -1) | |||
return; | |||
// Handle the event | |||
e.preventDefault(); | |||
e.stopPropagation(); | |||
elements[ | |||
(index + (e.shiftKey ? elements.length - 1 : 1)) % elements.length | |||
].focus(); | |||
}); | |||
}); |
Latest revision as of 17:43, 23 September 2022
// All JavaScript here will be loaded for users of the Timeless skin /* $(document).ready(function() { // On desktop, display text logo once user scrolls down function toggleTextLogo() { if ($(window).width() > 1099) { if ($(window).scrollTop() > 130) { if ($('#p-logo-text').css('opacity') === '0') { $('#p-logo-text').css('visibility', 'visible'); $('#p-logo-text').stop().animate({ opacity: 1 }, 250); } } else { if ($('#p-logo-text').css('opacity') === '1') { $('#p-logo-text').css('visibility', 'hidden'); $('#p-logo-text').stop().animate({ opacity: 0 }, 250); } } } else { if ($('#p-logo-text').css('opacity') === '0') { $('#p-logo-text').css('visibility', 'visible'); $('#p-logo-text').stop().animate({ opacity: 1 }, 250); } } } toggleTextLogo(); $(window).scroll(toggleTextLogo); $(window).resize(toggleTextLogo); // Open Social links in new tab/window $( '#n-Discord' ).find( 'a' ).attr( 'target' , '_blank' ); $( '#n-Twitter' ).find( 'a' ).attr( 'target' , '_blank' ); $( '#n-Instagram' ).find( 'a' ).attr( 'target' , '_blank' ); $( '#n-Facebook' ).find( 'a' ).attr( 'target' , '_blank' ); }); */ // Ensure the document has finished loading new Promise(function(resolve) { if (document.readyState == "complete") return resolve(); window.addEventListener("load", resolve); }).then(function() { // Auto-increment key for dynamic element IDs var nextId = 0; // Initial focus order document.getElementById("p-banner" ).tabIndex = 1; document.getElementById("searchInput").tabIndex = 2; ////////////////////////////// Utility functions ////////////////////////////// // Test whether an element could receive focus var isVisible = function(element) { for (; element instanceof Element; element = element.parentNode) { var style = getComputedStyle(element); if ( style.display == "none" || style.visibility == "hidden" || parseFloat(style.top) <= -1000 ) return false; } return true; }; // Retrieve all focusable children of an element var getFocusableChildren = function(element) { return Array.from(element.querySelectorAll( "*:is(a[href],area,button,details,input," + "textarea,select,[tabindex='0']):not([disabled])" )).filter(function(e) { return isVisible(e); }); }; // Determine which responsive layout is in effect var getLayoutSize = function() { return ( window.innerWidth <= 850 ? "small" : window.innerWidth < 1100 ? "medium" : "large" ); }; //////////////////////////////// Search button //////////////////////////////// var searchInput = document.getElementById("searchButton"); var searchButton = document.createElement("button"); searchButton.id = searchInput.id; searchButton.title = searchInput.title; searchButton.append(document.createElement("div")); searchInput.after(searchButton); searchInput.remove(); var badButton = document.getElementById("mw-searchButton"); if (badButton) badButton.remove(); /////////////////////////////// Drop-down menus /////////////////////////////// // Configure drop-down elements var dropDowns = [ { root: "#personal" , menu: "#personal-inner", small: true , medium: true , large: true }, { root: "#site-navigation", menu: "#site-navigation .sidebar-inner", small: true , medium: true , large: false }, { root: "#site-tools" , menu: "#site-tools .sidebar-inner", small: true , medium: true , large: false }, { root: "#page-tools" , menu: "#page-tools .sidebar-inner", small: false, medium: true , large: false }, { root: "#ca-more" , menu: "#page-tools .sidebar-inner", small: true , medium: false, large: false }, { root: "#ca-languages" , menu: "#other-languages div", small: true , medium: false, large: false }, ]; for (var x = 0; x < dropDowns.length; x++) { (function(props) { // Top-level control var root = document.querySelector(props.root); if (!root) return; // Languages may not be available // Interactive element var ctrl = root.querySelector("*:is(h2,span)"); // Internal menu var menu = document.querySelector(props.menu); if (menu) menu.id = menu.id || ("id-timeless-" + nextId++); // Watch the viewport size window.addEventListener("resize", function() { var layout = getLayoutSize(); // Control is a drop-down if (props[layout]) { ctrl.tabIndex = 0; ctrl.setAttribute("role" , "button"); if (menu) { ctrl.setAttribute("aria-controls", menu.id); ctrl.setAttribute("aria-haspopup", "menu"); } ctrl.setAttribute("aria-expanded", Array.from(ctrl.classList) .indexOf("dropdown-active") != -1); } // Control is not a drop-down else { ctrl.removeAttribute("tabindex"); ctrl.removeAttribute("role"); ctrl.removeAttribute("aria-controls"); ctrl.removeAttribute("aria-expanded"); ctrl.removeAttribute("aria-haspopup"); } // Menu is not available if (!menu) return; // Menu is inside a drop-down if ( props.menu.startsWith("#page-tools") && layout != "large" || props[layout] ) menu.setAttribute("role", "menu"); // Menu is not inside a drop-down else menu.removeAttribute("role"); }); // Watch the class attribute of the control new MutationObserver(function() { if (!props.large && getLayoutSize() == "large") return; ctrl.setAttribute("aria-expanded", Array.from(root.classList).indexOf("dropdown-active") != -1); }).observe(root, { attributeFilter: [ "class" ] }); // Respond to pointer presses on the control ctrl.addEventListener("pointerdown", function() { ctrl.focus(); }); // Respond to key presses on the control ctrl.addEventListener("keydown", function(e) { // Filter by key switch (e.key) { case " ": case "Enter": case "ArrowDown": case "ArrowUp": break; default: return; } // Handle the event e.preventDefault(); e.stopPropagation(); // Open the menu if (ctrl.getAttribute("aria-expanded") != "true") ctrl.click(); // The menu is not available if (!menu) return; // Focus the appropriate menu item var items = getFocusableChildren(menu); if (items.length == 0) return; items[e.key == "ArrowUp" ? items.length - 1 : 0].focus(); }); // Respond to key presses on menu items if (menu && props.root != "#ca-more") menu.addEventListener("keydown", function(e){ if (!props.large && getLayoutSize() == "large") return; var items = getFocusableChildren(menu); var index = items.indexOf(document.activeElement); var focus; switch (index == -1 ? null : e.key) { case "ArrowDown": focus = items[(index + 1) % items.length]; break; case "ArrowUp": focus = items[(index + items.length - 1) % items.length]; break; case "End": focus = items[items.length - 1]; break; case " ": case "Enter": focus = document.activeElement; focus.click(); break; case "Home": focus = items[0]; break; case "Escape": case "Tab": focus = props.root == "#page-tools" && getLayoutSize() == "small" ? document.querySelector("#ca-more span") : ctrl ; focus.click(); break; case null: focus = items[0] || ctrl; break; default: return; } // Handle the event e.preventDefault(); e.stopPropagation(); // Focus the appropriate element focus.focus(); }); })(dropDowns[x]); // Simulate a window resize window.dispatchEvent(new Event("resize")); } ///////////////////////////////// Focus order ///////////////////////////////// // Specify the desired element order by unique selector // If the specified element isn't focusable, takes all its focusable children var ORDERS = { small: [ "#site-navigation h2", "#p-banner", "#pt-notifications-alert a", "#pt-notifications-notice a", "#site-tools h2", "#personal h2", "#searchInput", "#searchButton", "#content > .mw-indicators", "#p-namespaces", "#p-views", "#p-more", "#bodyContent", "#content-bottom-stuff", "#footer-list", "#footer-copyrightico a", "#footer-poweredbyico a", "#footer-strategywikiico a" ], medium: [ "#p-banner", "#searchInput", "#searchButton", "#pt-notifications-alert a", "#pt-notifications-notice a", "#personal h2", "#site-navigation h2", "#site-tools h2", "#page-tools h2", "#content > .mw-indicators", "#p-namespaces", "#p-views", "#bodyContent", "#content-bottom-stuff", "#footer-list", "#footer-strategywikiico a", "#footer-poweredbyico a", "#footer-copyrightico a" ], large: [ "#p-banner", "#searchInput", "#searchButton", "#pt-notifications-alert a", "#pt-notifications-notice a", "#personal h2", "#p-logo a", "#site-navigation", "#site-tools", "#mw-related-navigation", "#content > .mw-indicators", "#p-namespaces", "#p-views", "#bodyContent", "#content-bottom-stuff", "#footer-list", "#footer-strategywikiico a", "#footer-poweredbyico a", "#footer-copyrightico a" ] } // Watch key presses on any element that doesn't trap one document.body.addEventListener("keydown", function(e) { // Only monitoring Tab key if (e.key != "Tab") return; // Construct a list of all focusable elements var elements = ORDERS[getLayoutSize()].slice(); for (var y = 0; y < elements.length; y++) { var selector = elements[y]; var element = elements[y] = document.querySelector(selector); // The element is not visible if (!element || !isVisible(element)) { elements.splice(y--, 1); continue; } // The element is focusable if (element.parentNode.querySelector( selector + ":is(a[href],area,button,details,input," + "textarea,select,[tabindex='0']):not([disabled])" )) continue; // The element is not focusable var children = getFocusableChildren(element); elements.splice(y, 1); for (var x = 0; x < children.length; x++) elements.splice(y++, 0, children[x]); y--; } // Let the user agent decide what to do var index = elements.indexOf(document.activeElement); if (index == -1) return; // Handle the event e.preventDefault(); e.stopPropagation(); elements[ (index + (e.shiftKey ? elements.length - 1 : 1)) % elements.length ].focus(); }); });