MediaWiki:Common.js: Difference between revisions
Jump to navigation
Jump to search
No edit summary Tag: Reverted |
No edit summary Tag: Reverted |
||
Line 1: | Line 1: | ||
/* Collapsible Sidebar for MediaWiki 1.44 (Vector + Vector-2022) | |||
* - Arrow at left of each portal header | |||
* - Remembers state via localStorage | |||
* - No deprecated libs; RL-safe | |||
* - Console logs for easy debugging | |||
*/ | |||
mw.loader.using( [ 'mediawiki.util' ] ).then( function () { | |||
$( function () { | |||
// --- Config ------------------------------------------------------------- | |||
var STORAGE_KEY = 'sidebar-collapse-state:v1'; | |||
// Portals to leave open on first load; you can edit this list: | |||
var defaultExpanded = [ 'navigation', 'book1' ]; | |||
// Force-collapse these even on first load: | |||
var defaultCollapsed = [ /* e.g. 'tools' */ ]; | |||
// --- Helpers ------------------------------------------------------------ | |||
function log() { /* flip to false to silence */ | |||
if ( true && window.console && console.log ) console.log.apply( console, [ '[SidebarCollapse]' ].concat( Array.from( arguments ) ) ); | |||
} | |||
function readState() { | |||
try { | |||
var raw = localStorage.getItem( STORAGE_KEY ); | |||
return raw ? JSON.parse( raw ) : {}; | |||
} catch (e) { | |||
}); | log('localStorage read failed', e); | ||
return {}; | |||
} | |||
} | |||
function writeState(state) { | |||
try { | |||
localStorage.setItem( STORAGE_KEY, JSON.stringify( state ) ); | |||
} catch (e) { | |||
log('localStorage write failed', e); | |||
} | |||
} | |||
// Normalize an identifier for a portal/menu node | |||
function portalIdFor( node ) { | |||
// Prefer element id like "p-navigation" -> "navigation" | |||
if ( node.id && node.id.indexOf('p-') === 0 ) { | |||
return node.id.slice(2).toLowerCase(); | |||
} | |||
// Vector-2022 often has data-name="navigation" | |||
var dn = node.getAttribute && node.getAttribute('data-name'); | |||
if ( dn ) return dn.toLowerCase(); | |||
// Fallback: use heading text | |||
var heading = node.querySelector('h3, .vector-menu-heading, .portal h3'); | |||
if ( heading ) { | |||
return heading.textContent.trim().toLowerCase().replace(/\s+/g,'-'); | |||
} | |||
// Last resort: index among siblings | |||
return 'portal-' + ( Array.prototype.indexOf.call( node.parentNode.children, node ) ); | |||
} | |||
function isExpandedClass( node ) { | |||
return node.classList.contains('vector-collapsible--expanded'); | |||
} | |||
function setExpanded( node, expanded ) { | |||
node.classList.toggle( 'vector-collapsible--expanded', !!expanded ); | |||
node.classList.toggle( 'vector-collapsible--collapsed', !expanded ); | |||
// ARIA for accessibility | |||
var heading = node.querySelector('h3, .vector-menu-heading'); | |||
if ( heading ) { | |||
heading.setAttribute( 'role', 'button' ); | |||
heading.setAttribute( 'tabindex', '0' ); | |||
heading.setAttribute( 'aria-expanded', String( !!expanded ) ); | |||
} | |||
} | |||
function toggleNode( node, persist ) { | |||
var nextState = !isExpandedClass( node ); | |||
setExpanded( node, nextState ); | |||
if ( persist ) { | |||
var id = portalIdFor( node ); | |||
var state = readState(); | |||
state[ id ] = nextState ? 1 : 0; | |||
writeState( state ); | |||
log('Toggled', id, '=>', nextState ? 'open' : 'closed' ); | |||
} | |||
} | |||
function attachToggle( node ) { | |||
var heading = node.querySelector('h3, .vector-menu-heading'); | |||
if ( !heading ) return; | |||
// Click | |||
heading.addEventListener( 'click', function ( e ) { | |||
e.preventDefault(); | |||
toggleNode( node, true ); | |||
} ); | |||
// Keyboard (Enter/Space) | |||
heading.addEventListener( 'keydown', function ( e ) { | |||
if ( e.key === 'Enter' || e.key === ' ' ) { | |||
e.preventDefault(); | |||
toggleNode( node, true ); | |||
} | |||
} ); | |||
} | |||
// --- Locate portals for both skins ------------------------------------- | |||
var isV22 = document.body.classList.contains('skin-vector-2022'); | |||
// Vector legacy portals: div.portal under #mw-panel | |||
var legacyPortals = Array.from( document.querySelectorAll('#mw-panel .portal') ); | |||
// Vector 2022 portals: nav.vector-menu-portal (exclude the main hamburger) | |||
var v22Portals = Array.from( document.querySelectorAll('nav.vector-menu-portal') ) | |||
.filter( function (el) { return !el.closest('#vector-main-menu'); } ); | |||
var portals = isV22 ? v22Portals : legacyPortals; | |||
if ( portals.length === 0 ) { | |||
log('No portals found — are you using Vector skin?'); | |||
return; | |||
} | |||
// --- Initialize state --------------------------------------------------- | |||
var saved = readState(); | |||
portals.forEach( function ( portal ) { | |||
var id = portalIdFor( portal ); | |||
var initial; | |||
if ( saved.hasOwnProperty( id ) ) { | |||
initial = !!saved[ id ]; | |||
} else if ( defaultCollapsed.indexOf( id ) !== -1 ) { | |||
initial = false; | |||
} else if ( defaultExpanded.indexOf( id ) !== -1 ) { | |||
initial = true; | |||
} else { | |||
// Default behavior: expanded | |||
initial = true; | |||
} | |||
portal.classList.add('vector-collapsible'); // cosmetic marker if you want to theme | |||
setExpanded( portal, initial ); | |||
attachToggle( portal ); | |||
log('Init portal:', id, 'expanded =', initial); | |||
}); | }); | ||
} ); | |||
} ).catch( function (e) { | |||
console.error('[SidebarCollapse] loader failed:', e); | |||
}); | }); |
Revision as of 15:58, 25 August 2025
/* Collapsible Sidebar for MediaWiki 1.44 (Vector + Vector-2022) * - Arrow at left of each portal header * - Remembers state via localStorage * - No deprecated libs; RL-safe * - Console logs for easy debugging */ mw.loader.using( [ 'mediawiki.util' ] ).then( function () { $( function () { // --- Config ------------------------------------------------------------- var STORAGE_KEY = 'sidebar-collapse-state:v1'; // Portals to leave open on first load; you can edit this list: var defaultExpanded = [ 'navigation', 'book1' ]; // Force-collapse these even on first load: var defaultCollapsed = [ /* e.g. 'tools' */ ]; // --- Helpers ------------------------------------------------------------ function log() { /* flip to false to silence */ if ( true && window.console && console.log ) console.log.apply( console, [ '[SidebarCollapse]' ].concat( Array.from( arguments ) ) ); } function readState() { try { var raw = localStorage.getItem( STORAGE_KEY ); return raw ? JSON.parse( raw ) : {}; } catch (e) { log('localStorage read failed', e); return {}; } } function writeState(state) { try { localStorage.setItem( STORAGE_KEY, JSON.stringify( state ) ); } catch (e) { log('localStorage write failed', e); } } // Normalize an identifier for a portal/menu node function portalIdFor( node ) { // Prefer element id like "p-navigation" -> "navigation" if ( node.id && node.id.indexOf('p-') === 0 ) { return node.id.slice(2).toLowerCase(); } // Vector-2022 often has data-name="navigation" var dn = node.getAttribute && node.getAttribute('data-name'); if ( dn ) return dn.toLowerCase(); // Fallback: use heading text var heading = node.querySelector('h3, .vector-menu-heading, .portal h3'); if ( heading ) { return heading.textContent.trim().toLowerCase().replace(/\s+/g,'-'); } // Last resort: index among siblings return 'portal-' + ( Array.prototype.indexOf.call( node.parentNode.children, node ) ); } function isExpandedClass( node ) { return node.classList.contains('vector-collapsible--expanded'); } function setExpanded( node, expanded ) { node.classList.toggle( 'vector-collapsible--expanded', !!expanded ); node.classList.toggle( 'vector-collapsible--collapsed', !expanded ); // ARIA for accessibility var heading = node.querySelector('h3, .vector-menu-heading'); if ( heading ) { heading.setAttribute( 'role', 'button' ); heading.setAttribute( 'tabindex', '0' ); heading.setAttribute( 'aria-expanded', String( !!expanded ) ); } } function toggleNode( node, persist ) { var nextState = !isExpandedClass( node ); setExpanded( node, nextState ); if ( persist ) { var id = portalIdFor( node ); var state = readState(); state[ id ] = nextState ? 1 : 0; writeState( state ); log('Toggled', id, '=>', nextState ? 'open' : 'closed' ); } } function attachToggle( node ) { var heading = node.querySelector('h3, .vector-menu-heading'); if ( !heading ) return; // Click heading.addEventListener( 'click', function ( e ) { e.preventDefault(); toggleNode( node, true ); } ); // Keyboard (Enter/Space) heading.addEventListener( 'keydown', function ( e ) { if ( e.key === 'Enter' || e.key === ' ' ) { e.preventDefault(); toggleNode( node, true ); } } ); } // --- Locate portals for both skins ------------------------------------- var isV22 = document.body.classList.contains('skin-vector-2022'); // Vector legacy portals: div.portal under #mw-panel var legacyPortals = Array.from( document.querySelectorAll('#mw-panel .portal') ); // Vector 2022 portals: nav.vector-menu-portal (exclude the main hamburger) var v22Portals = Array.from( document.querySelectorAll('nav.vector-menu-portal') ) .filter( function (el) { return !el.closest('#vector-main-menu'); } ); var portals = isV22 ? v22Portals : legacyPortals; if ( portals.length === 0 ) { log('No portals found — are you using Vector skin?'); return; } // --- Initialize state --------------------------------------------------- var saved = readState(); portals.forEach( function ( portal ) { var id = portalIdFor( portal ); var initial; if ( saved.hasOwnProperty( id ) ) { initial = !!saved[ id ]; } else if ( defaultCollapsed.indexOf( id ) !== -1 ) { initial = false; } else if ( defaultExpanded.indexOf( id ) !== -1 ) { initial = true; } else { // Default behavior: expanded initial = true; } portal.classList.add('vector-collapsible'); // cosmetic marker if you want to theme setExpanded( portal, initial ); attachToggle( portal ); log('Init portal:', id, 'expanded =', initial); }); } ); } ).catch( function (e) { console.error('[SidebarCollapse] loader failed:', e); });