Jump to content

MediaWiki:Common.js: Difference between revisions

From Erindal Wiki
Content deleted Content added
No edit summary
No edit summary
Line 1: Line 1:
/* ========================================================================
/* ========================================================================
ERINDAL WIKI - COMMON.JS v5.3
ERINDAL WIKI - VECTOR 2022.JS v1.0
Skin-specific enhancements. Common.js continues to own article behaviour.
Target: MediaWiki 1.43.x, legacy Vector

The topbar itself is now rendered by MediaWiki:Sidebar. JavaScript only
adds dropdowns, the remembered sidebar toggle, dynamic homepage data and
the progressive deity appearance/symbol switch.
It deliberately does not load fonts or construct the navigation bar.
======================================================================== */
======================================================================== */


Line 12: Line 7:
'use strict';
'use strict';


var themeKey = 'erindal-theme';
var sidebarKey = 'erindal-sidebar';
var sidebarKey = 'erindal-sidebar';
var themes = [ 'system', 'light', 'dark' ];

/* Apply the saved state again as a fallback. LocalSettings.php applies the
same class in the document head, before first paint. */
try {
if ( window.localStorage.getItem( sidebarKey ) === 'collapsed' ) {
document.documentElement.classList.add( 'ew-sidebar-collapsed' );
}
} catch ( error ) {
/* localStorage can be unavailable in strict/private browser modes. */
}


function onReady( callback ) {
function onReady( callback ) {
Line 32: Line 19:
}
}


function installSidebarToggle() {
function readTheme() {
var value = 'system';
if ( !document.getElementById( 'mw-panel' ) || document.getElementById( 'ew-sidebar-toggle' ) ) {

try {
value = window.localStorage.getItem( themeKey ) || 'system';
} catch ( error ) {
value = 'system';
}

return themes.indexOf( value ) === -1 ? 'system' : value;
}

function applyTheme( value ) {
document.documentElement.setAttribute( 'data-ew-theme', value );

try {
window.localStorage.setItem( themeKey, value );
} catch ( error ) {
/* The selected mode still applies to the current page. */
}
}

function installThemeToggle() {
var topbar = document.getElementById( 'ew-server-topbar' );
if ( !topbar || document.getElementById( 'ew-theme-toggle' ) ) {
return;
return;
}

var tools = topbar.querySelector( '.ew-topbar-tools' );
if ( !tools ) {
tools = document.createElement( 'div' );
tools.className = 'ew-topbar-tools';
topbar.appendChild( tools );
}
}


var button = document.createElement( 'button' );
var button = document.createElement( 'button' );
button.id = 'ew-sidebar-toggle';
button.id = 'ew-theme-toggle';
button.type = 'button';
button.type = 'button';

function updateButton() {
var value = readTheme();
var labels = {
system: '\u25d0 Auto',
light: '\u2600 Light',
dark: '\u263e Dark'
};

button.textContent = labels[ value ];
button.dataset.ewTheme = value;
button.setAttribute( 'aria-label', 'Colour mode: ' + value + '. Activate to change mode.' );
button.title = 'Colour mode: ' + value;
}

button.addEventListener( 'click', function () {
var current = readTheme();
var next = themes[ ( themes.indexOf( current ) + 1 ) % themes.length ];
applyTheme( next );
updateButton();
} );

tools.appendChild( button );
applyTheme( readTheme() );
updateButton();
}

function installSidebarToggleFallback() {
var button = document.getElementById( 'ew-sidebar-toggle' );
if ( !button || button.dataset.ewVectorBound === '1' || button.getAttribute( 'data-ew-bound' ) === '1' ) {
return;
}

button.dataset.ewVectorBound = '1';


function updateButton() {
function updateButton() {
Line 55: Line 106:
window.localStorage.setItem( sidebarKey, collapsed ? 'collapsed' : 'open' );
window.localStorage.setItem( sidebarKey, collapsed ? 'collapsed' : 'open' );
} catch ( error ) {
} catch ( error ) {
/* The button still works for this page when storage is blocked. */
/* The button still works for the current page. */
}
}


Line 62: Line 113:


updateButton();
updateButton();
document.body.appendChild( button );
}
}


function installTopbarDropdowns() {
function installTopbarDetails() {
var topbar = document.getElementById( 'p-erindal-topbar' );
var topbar = document.getElementById( 'ew-server-topbar' );
if ( !topbar || topbar.dataset.ewEnhanced === '1' ) {
if ( !topbar || topbar.dataset.ewDetailsBound === '1' ) {
return;
return;
}
}


topbar.dataset.ewEnhanced = '1';
topbar.dataset.ewDetailsBound = '1';


Array.prototype.forEach.call( topbar.querySelectorAll( 'details.ew-global-section' ), function ( section ) {
var menus = {
'Explore': [
section.addEventListener( 'toggle', function () {
[ 'Main Page', 'Main Page' ],
if ( !section.open ) {
[ 'Special:RecentChanges', 'Recent Changes' ],
return;
[ 'Special:AllPages', 'All Pages' ],
[ 'Special:Categories', 'Categories' ]
],
'World': [
[ 'Category:Nations and Realms', 'Nations & Realms' ],
[ 'Category:Ancestries and Cultures', 'Ancestries & Cultures' ],
[ 'Category:Locations', 'Locations' ]
],
'People': [
[ 'Category:Characters', 'Characters' ],
[ 'Category:Family Lines', 'Family Lines' ]
],
'Organizations': [
[ 'Category:Organizations', 'All Organizations' ],
[ 'The Conclave', 'The Conclave' ],
[ 'Order of the Holy Flame', 'Order of the Holy Flame' ],
[ 'The Dominion', 'The Dominion' ]
],
'Magic & Lore': [
[ 'Category:Religion and Deities', 'Religion & Deities' ],
[ 'Category:Magic and Cosmology', 'Magic & Cosmology' ]
],
'History': [
[ 'Category:History and Timeline', 'History & Timeline' ],
[ 'Chronology of Erindal', 'Timeline' ],
[ 'Calendar of Erindal', 'Calendar' ]
]
};

Array.prototype.forEach.call( topbar.querySelectorAll( '.mw-list-item' ), function ( item ) {
var link = item.querySelector( ':scope > a' );
if ( !link ) {
return;
}

var label = link.textContent.trim();
var entries = menus[ label ];
if ( !entries ) {
return;
}

item.classList.add( 'ew-has-dropdown' );

var caret = document.createElement( 'button' );
caret.type = 'button';
caret.className = 'ew-topbar-caret';
caret.textContent = '\u2304';
caret.setAttribute( 'aria-label', 'Open ' + label + ' menu' );
caret.setAttribute( 'aria-expanded', 'false' );

var dropdown = document.createElement( 'div' );
dropdown.className = 'ew-topbar-dropdown';

entries.forEach( function ( entry ) {
var menuLink = document.createElement( 'a' );
menuLink.href = mw.util.getUrl( entry[ 0 ] );
menuLink.textContent = entry[ 1 ];
dropdown.appendChild( menuLink );
} );

caret.addEventListener( 'click', function ( event ) {
event.preventDefault();
event.stopPropagation();

var willOpen = !item.classList.contains( 'ew-menu-open' );
Array.prototype.forEach.call( topbar.querySelectorAll( '.ew-menu-open' ), function ( openItem ) {
openItem.classList.remove( 'ew-menu-open' );
var openCaret = openItem.querySelector( '.ew-topbar-caret' );
if ( openCaret ) {
openCaret.setAttribute( 'aria-expanded', 'false' );
}
} );

item.classList.toggle( 'ew-menu-open', willOpen );
caret.setAttribute( 'aria-expanded', willOpen ? 'true' : 'false' );
} );

item.appendChild( caret );
item.appendChild( dropdown );
} );

document.addEventListener( 'click', function () {
Array.prototype.forEach.call( topbar.querySelectorAll( '.ew-menu-open' ), function ( item ) {
item.classList.remove( 'ew-menu-open' );
var caret = item.querySelector( '.ew-topbar-caret' );
if ( caret ) {
caret.setAttribute( 'aria-expanded', 'false' );
}
}
} );
} );


document.addEventListener( 'keydown', function ( event ) {
Array.prototype.forEach.call( topbar.querySelectorAll( 'details.ew-global-section[open]' ), function ( other ) {
if ( event.key === 'Escape' ) {
if ( other !== section ) {
other.open = false;
Array.prototype.forEach.call( topbar.querySelectorAll( '.ew-menu-open' ), function ( item ) {
item.classList.remove( 'ew-menu-open' );
var caret = item.querySelector( '.ew-topbar-caret' );
if ( caret ) {
caret.setAttribute( 'aria-expanded', 'false' );
}
}
} );
} );
}
} );
}

function makeHomepageCardsClickable() {
var destinations = {
'nations & realms': 'Category:Nations and Realms',
'ancestries & cultures': 'Category:Ancestries and Cultures',
'characters': 'Category:Characters',
'organizations': 'Category:Organizations',
'religion & deities': 'Category:Religion and Deities',
'magic & cosmology': 'Category:Magic and Cosmology',
'locations': 'Category:Locations',
'history & timeline': 'Category:History and Timeline',
'family lines': 'Category:Family Lines'
};

Array.prototype.forEach.call( document.querySelectorAll( '.ew-card' ), function ( card ) {
var link = card.querySelector( 'a[href]' );
var labelElement = card.querySelector( '.ew-card-label' ) || link;
var label = labelElement ? labelElement.textContent.trim().toLowerCase() : '';
var page = card.dataset.ewPage || destinations[ label ] || '';
var destination = link ? link.href : ( page ? mw.util.getUrl( page ) : '' );

if ( !destination || card.dataset.ewClickable === '1' ) {
return;
}

card.dataset.ewClickable = '1';
card.setAttribute( 'tabindex', '0' );
card.setAttribute( 'role', 'link' );
card.setAttribute( 'aria-label', 'Open ' + ( labelElement ? labelElement.textContent.trim() : 'category' ) );

card.addEventListener( 'click', function ( event ) {
if ( !event.target.closest( 'a' ) ) {
window.location.href = destination;
}
} );

card.addEventListener( 'keydown', function ( event ) {
if ( event.key === 'Enter' || event.key === ' ' ) {
event.preventDefault();
window.location.href = destination;
}
} );
} );
} );
} );
}


document.addEventListener( 'click', function ( event ) {
function installDeityVisualSwitches() {
if ( topbar.contains( event.target ) ) {
Array.prototype.forEach.call( document.querySelectorAll( '.deity-visual' ), function ( visual ) {
if ( visual.dataset.ewEnhanced === '1' ) {
return;
return;
}
}


Array.prototype.forEach.call( topbar.querySelectorAll( 'details.ew-global-section[open]' ), function ( section ) {
var appearanceTab = visual.querySelector( '.deity-visual__tab--appearance' );
var symbolTab = visual.querySelector( '.deity-visual__tab--symbol' );
section.open = false;
var appearancePanel = visual.querySelector( '.deity-visual__panel--appearance' );
var symbolPanel = visual.querySelector( '.deity-visual__panel--symbol' );

if ( !appearanceTab || !symbolTab || !appearancePanel || !symbolPanel ) {
return;
}

var tabs = [ appearanceTab, symbolTab ];
var panels = [ appearancePanel, symbolPanel ];

function selectView( selectedIndex ) {
tabs.forEach( function ( tab, index ) {
var active = index === selectedIndex;
tab.classList.toggle( 'is-active', active );
tab.setAttribute( 'aria-selected', active ? 'true' : 'false' );
tab.setAttribute( 'tabindex', active ? '0' : '-1' );
panels[ index ].classList.toggle( 'is-active', active );
panels[ index ].hidden = !active;
} );
}

tabs.forEach( function ( tab, index ) {
tab.setAttribute( 'role', 'tab' );

tab.addEventListener( 'click', function () {
selectView( index );
} );

tab.addEventListener( 'keydown', function ( event ) {
if ( event.key === 'Enter' || event.key === ' ' ) {
event.preventDefault();
selectView( index );
} else if ( event.key === 'ArrowLeft' || event.key === 'ArrowRight' ) {
event.preventDefault();
var nextIndex = event.key === 'ArrowRight' ? ( index + 1 ) % tabs.length : ( index + tabs.length - 1 ) % tabs.length;
selectView( nextIndex );
tabs[ nextIndex ].focus();
}
} );
} );
} );

var tabList = visual.querySelector( '.deity-visual__tabs' );
if ( tabList ) {
tabList.setAttribute( 'role', 'tablist' );
tabList.setAttribute( 'aria-label', 'Deity appearance and holy symbol' );
}

visual.dataset.ewEnhanced = '1';
visual.classList.add( 'is-enhanced' );
selectView( 0 );
} );
} );
}


function loadRecentChanges() {
document.addEventListener( 'keydown', function ( event ) {
var loading = document.querySelector( '.ew-recent-loading' );
if ( event.key !== 'Escape' ) {
if ( !loading ) {
return;
}

var api = new mw.Api();
api.get( {
action: 'query',
list: 'recentchanges',
rcnamespace: 0,
rcshow: '!bot',
rcprop: 'title|timestamp|user',
rctype: 'edit|new',
rclimit: 5,
formatversion: 2
} ).then( function ( data ) {
var changes = data && data.query && data.query.recentchanges ? data.query.recentchanges : [];
loading.textContent = '';

if ( !changes.length ) {
loading.textContent = 'No recent article changes.';
return;
return;
}
}


changes.forEach( function ( change ) {
Array.prototype.forEach.call( topbar.querySelectorAll( 'details.ew-global-section[open]' ), function ( section ) {
var row = document.createElement( 'div' );
section.open = false;
row.className = 'ew-recent-row';

var icon = document.createElement( 'span' );
icon.className = 'ew-recent-icon';
icon.textContent = change.title.slice( 0, 2 ).toUpperCase();

var copy = document.createElement( 'span' );
var link = document.createElement( 'a' );
link.href = mw.util.getUrl( change.title );
link.textContent = change.title;

var detail = document.createElement( 'small' );
var date = new Date( change.timestamp );
detail.textContent = date.toLocaleDateString() + ( change.user ? ' - ' + change.user : '' );

copy.appendChild( link );
copy.appendChild( detail );
row.appendChild( icon );
row.appendChild( copy );
loading.appendChild( row );
} );
} );
} ).catch( function () {
loading.textContent = 'Recent changes could not be loaded.';
} );
} );
}
}


mw.loader.using( [ 'mediawiki.api', 'mediawiki.util' ] ).then( function () {
onReady( function () {
onReady( function () {
installThemeToggle();
installSidebarToggle();
installSidebarToggleFallback();
installTopbarDropdowns();
installTopbarDetails();
makeHomepageCardsClickable();
installDeityVisualSwitches();
loadRecentChanges();
} );
} );
} );
}() );
}() );

Revision as of 22:52, 29 August 2026

/* ========================================================================
   ERINDAL WIKI - VECTOR 2022.JS v1.0
   Skin-specific enhancements. Common.js continues to own article behaviour.
   ======================================================================== */

( function () {
    'use strict';

    var themeKey = 'erindal-theme';
    var sidebarKey = 'erindal-sidebar';
    var themes = [ 'system', 'light', 'dark' ];

    function onReady( callback ) {
        if ( document.readyState === 'loading' ) {
            document.addEventListener( 'DOMContentLoaded', callback, { once: true } );
        } else {
            callback();
        }
    }

    function readTheme() {
        var value = 'system';

        try {
            value = window.localStorage.getItem( themeKey ) || 'system';
        } catch ( error ) {
            value = 'system';
        }

        return themes.indexOf( value ) === -1 ? 'system' : value;
    }

    function applyTheme( value ) {
        document.documentElement.setAttribute( 'data-ew-theme', value );

        try {
            window.localStorage.setItem( themeKey, value );
        } catch ( error ) {
            /* The selected mode still applies to the current page. */
        }
    }

    function installThemeToggle() {
        var topbar = document.getElementById( 'ew-server-topbar' );
        if ( !topbar || document.getElementById( 'ew-theme-toggle' ) ) {
            return;
        }

        var tools = topbar.querySelector( '.ew-topbar-tools' );
        if ( !tools ) {
            tools = document.createElement( 'div' );
            tools.className = 'ew-topbar-tools';
            topbar.appendChild( tools );
        }

        var button = document.createElement( 'button' );
        button.id = 'ew-theme-toggle';
        button.type = 'button';

        function updateButton() {
            var value = readTheme();
            var labels = {
                system: '\u25d0 Auto',
                light: '\u2600 Light',
                dark: '\u263e Dark'
            };

            button.textContent = labels[ value ];
            button.dataset.ewTheme = value;
            button.setAttribute( 'aria-label', 'Colour mode: ' + value + '. Activate to change mode.' );
            button.title = 'Colour mode: ' + value;
        }

        button.addEventListener( 'click', function () {
            var current = readTheme();
            var next = themes[ ( themes.indexOf( current ) + 1 ) % themes.length ];
            applyTheme( next );
            updateButton();
        } );

        tools.appendChild( button );
        applyTheme( readTheme() );
        updateButton();
    }

    function installSidebarToggleFallback() {
        var button = document.getElementById( 'ew-sidebar-toggle' );
        if ( !button || button.dataset.ewVectorBound === '1' || button.getAttribute( 'data-ew-bound' ) === '1' ) {
            return;
        }

        button.dataset.ewVectorBound = '1';

        function updateButton() {
            var collapsed = document.documentElement.classList.contains( 'ew-sidebar-collapsed' );
            button.textContent = collapsed ? '\u203a' : '\u2039';
            button.setAttribute( 'aria-expanded', collapsed ? 'false' : 'true' );
            button.setAttribute( 'aria-label', collapsed ? 'Open sidebar' : 'Close sidebar' );
            button.title = collapsed ? 'Open sidebar' : 'Close sidebar';
        }

        button.addEventListener( 'click', function () {
            var collapsed = document.documentElement.classList.toggle( 'ew-sidebar-collapsed' );

            try {
                window.localStorage.setItem( sidebarKey, collapsed ? 'collapsed' : 'open' );
            } catch ( error ) {
                /* The button still works for the current page. */
            }

            updateButton();
        } );

        updateButton();
    }

    function installTopbarDetails() {
        var topbar = document.getElementById( 'ew-server-topbar' );
        if ( !topbar || topbar.dataset.ewDetailsBound === '1' ) {
            return;
        }

        topbar.dataset.ewDetailsBound = '1';

        Array.prototype.forEach.call( topbar.querySelectorAll( 'details.ew-global-section' ), function ( section ) {
            section.addEventListener( 'toggle', function () {
                if ( !section.open ) {
                    return;
                }

                Array.prototype.forEach.call( topbar.querySelectorAll( 'details.ew-global-section[open]' ), function ( other ) {
                    if ( other !== section ) {
                        other.open = false;
                    }
                } );
            } );
        } );

        document.addEventListener( 'click', function ( event ) {
            if ( topbar.contains( event.target ) ) {
                return;
            }

            Array.prototype.forEach.call( topbar.querySelectorAll( 'details.ew-global-section[open]' ), function ( section ) {
                section.open = false;
            } );
        } );

        document.addEventListener( 'keydown', function ( event ) {
            if ( event.key !== 'Escape' ) {
                return;
            }

            Array.prototype.forEach.call( topbar.querySelectorAll( 'details.ew-global-section[open]' ), function ( section ) {
                section.open = false;
            } );
        } );
    }

    onReady( function () {
        installThemeToggle();
        installSidebarToggleFallback();
        installTopbarDetails();
    } );
}() );