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.2
ERINDAL WIKI - COMMON.JS v5.3
Target: MediaWiki 1.43.x, legacy Vector
Target: MediaWiki 1.43.x, legacy Vector


Line 82: Line 82:
'World': [
'World': [
[ 'Category:Nations and Realms', 'Nations & Realms' ],
[ 'Category:Nations and Realms', 'Nations & Realms' ],
[ 'Category:Races and Peoples', 'Races & Peoples' ],
[ 'Category:Ancestries and Cultures', 'Ancestries & Cultures' ],
[ 'Category:Locations', 'Locations' ]
[ 'Category:Locations', 'Locations' ]
],
],
Line 184: Line 184:
var destinations = {
var destinations = {
'nations & realms': 'Category:Nations and Realms',
'nations & realms': 'Category:Nations and Realms',
'races & peoples': 'Category:Races and Peoples',
'ancestries & cultures': 'Category:Ancestries and Cultures',
'characters': 'Category:Characters',
'characters': 'Category:Characters',
'organizations': 'Category:Organizations',
'organizations': 'Category:Organizations',
Line 198: Line 198:
var labelElement = card.querySelector( '.ew-card-label' ) || link;
var labelElement = card.querySelector( '.ew-card-label' ) || link;
var label = labelElement ? labelElement.textContent.trim().toLowerCase() : '';
var label = labelElement ? labelElement.textContent.trim().toLowerCase() : '';
var destination = link ? link.href : ( destinations[ label ] ? mw.util.getUrl( destinations[ label ] ) : '' );
var page = card.dataset.ewPage || destinations[ label ] || '';
var destination = link ? link.href : ( page ? mw.util.getUrl( page ) : '' );


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

Revision as of 14:39, 18 August 2026

/* ========================================================================
   ERINDAL WIKI - COMMON.JS v5.3
   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.
   ======================================================================== */

( function () {
    'use strict';

    var sidebarKey = 'erindal-sidebar';

    /* 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 ) {
        if ( document.readyState === 'loading' ) {
            document.addEventListener( 'DOMContentLoaded', callback, { once: true } );
        } else {
            callback();
        }
    }

    function installSidebarToggle() {
        if ( !document.getElementById( 'mw-panel' ) || document.getElementById( 'ew-sidebar-toggle' ) ) {
            return;
        }

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

        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 this page when storage is blocked. */
            }

            updateButton();
        } );

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

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

        topbar.dataset.ewEnhanced = '1';

        var menus = {
            'Explore': [
                [ 'Main Page', 'Main Page' ],
                [ 'Special:RecentChanges', 'Recent Changes' ],
                [ '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 ) {
            if ( event.key === 'Escape' ) {
                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;
                }
            } );
        } );
    }

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

            var appearanceTab = visual.querySelector( '.deity-visual__tab--appearance' );
            var symbolTab = visual.querySelector( '.deity-visual__tab--symbol' );
            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() {
        var loading = document.querySelector( '.ew-recent-loading' );
        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;
            }

            changes.forEach( function ( change ) {
                var row = document.createElement( 'div' );
                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 () {
            installSidebarToggle();
            installTopbarDropdowns();
            makeHomepageCardsClickable();
            installDeityVisualSwitches();
            loadRecentChanges();
        } );
    } );
}() );