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 global navigation, fantasy navigation + readable content font loader, fluid cards and live recent changes - v4.8 */
ERINDAL WIKI - COMMON.JS v5.0
(function () {
Target: MediaWiki 1.43.x, legacy Vector
'use strict';


The topbar itself is now rendered by MediaWiki:Sidebar. JavaScript only
function wikiUrl(title) { return mw.util.getUrl(title); }
adds dropdowns, the remembered sidebar toggle and dynamic homepage data.
It deliberately does not load fonts or construct the navigation bar.
======================================================================== */


function loadErindalFonts() {
( function () {
'use strict';
if (document.getElementById('ew-google-fonts')) return;


var preconnectGoogle = document.createElement('link');
var sidebarKey = 'erindal-sidebar';
preconnectGoogle.rel = 'preconnect';
preconnectGoogle.href = 'https://fonts.googleapis.com';
document.head.appendChild(preconnectGoogle);


/* Apply the saved state again as a fallback. LocalSettings.php applies the
var preconnectStatic = document.createElement('link');
same class in the document head, before first paint. */
preconnectStatic.rel = 'preconnect';
try {
preconnectStatic.href = 'https://fonts.gstatic.com';
if ( window.localStorage.getItem( sidebarKey ) === 'collapsed' ) {
preconnectStatic.crossOrigin = 'anonymous';
document.head.appendChild(preconnectStatic);
document.documentElement.classList.add( 'ew-sidebar-collapsed' );
}
} catch ( error ) {
/* localStorage can be unavailable in strict/private browser modes. */
}


function onReady( callback ) {
var fonts = document.createElement('link');
if ( document.readyState === 'loading' ) {
fonts.id = 'ew-google-fonts';
document.addEventListener( 'DOMContentLoaded', callback, { once: true } );
fonts.rel = 'stylesheet';
} else {
fonts.href = 'https://fonts.googleapis.com/css2?family=Alegreya:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Alegreya+SC:wght@400;500;700&family=Almendra+SC&family=Cinzel+Decorative:wght@400;700&display=swap';
callback();
document.head.appendChild(fonts);
}
}
}


function buildTopbar() {
function installSidebarToggle() {
if (document.getElementById('ew-global-topbar')) return;
if ( !document.getElementById( 'mw-panel' ) || document.getElementById( 'ew-sidebar-toggle' ) ) {
return;
var bar = document.createElement('div');
}
bar.id = 'ew-global-topbar';


var button = document.createElement( 'button' );
var links = [
['Explore', 'Special:AllPages'],
button.id = 'ew-sidebar-toggle';
button.type = 'button';
['World', 'Category:Nations and Realms'],
['People', 'Category:Characters'],
['Organizations', 'Category:Organizations'],
['Magic & Lore', 'Category:Magic and Cosmology'],
['History', 'Category:History and Timeline']
];


function updateButton() {
var nav = document.createElement('nav');
var collapsed = document.documentElement.classList.contains( 'ew-sidebar-collapsed' );
nav.className = 'ew-global-menu';
button.textContent = collapsed ? '\u203a' : '\u2039';
links.forEach(function (entry) {
button.setAttribute( 'aria-expanded', collapsed ? 'false' : 'true' );
var a = document.createElement('a');
button.setAttribute( 'aria-label', collapsed ? 'Open sidebar' : 'Close sidebar' );
a.href = wikiUrl(entry[1]);
button.title = collapsed ? 'Open sidebar' : 'Close sidebar';
a.appendChild(document.createTextNode(entry[0]));
}
var arrow = document.createElement('span');
arrow.className = 'ew-menu-arrow';
arrow.textContent = '⌄';
a.appendChild(arrow);
nav.appendChild(a);
});


button.addEventListener( 'click', function () {
var search = document.createElement('form');
var collapsed = document.documentElement.classList.toggle( 'ew-sidebar-collapsed' );
search.className = 'ew-global-search';
search.method = 'get';
search.action = mw.config.get('wgScriptPath') + '/index.php';
var titleField = document.createElement('input');
titleField.type = 'hidden';
titleField.name = 'title';
titleField.value = 'Special:Search';
var input = document.createElement('input');
input.type = 'search';
input.name = 'search';
input.placeholder = 'Search the Erindal wiki...';
input.setAttribute('aria-label', 'Search the Erindal wiki');
var button = document.createElement('button');
button.type = 'submit';
button.textContent = '⌕';
button.setAttribute('aria-label', 'Search');
search.appendChild(titleField);
search.appendChild(input);
search.appendChild(button);


try {
var tools = document.createElement('div');
window.localStorage.setItem( sidebarKey, collapsed ? 'collapsed' : 'open' );
tools.className = 'ew-global-tools';
} catch ( error ) {
var prefs = document.createElement('a');
/* The button still works for this page when storage is blocked. */
prefs.className = 'ew-tool-link';
}
prefs.href = wikiUrl('Special:Preferences');
prefs.textContent = '⚙';
prefs.title = 'Preferences';
var user = document.createElement('a');
user.className = 'ew-tool-link';
var username = mw.config.get('wgUserName');
user.href = username ? wikiUrl('User:' + username) : wikiUrl('Special:UserLogin');
user.textContent = '♙';
user.title = username || 'Log in';
tools.appendChild(prefs);


updateButton();
/* Vector's native Edit/Edit source tabs are retained; no duplicate button is added here. */
} );


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


function installTopbarDropdowns() {
bar.appendChild(nav);
var topbar = document.getElementById( 'p-erindal-topbar' );
bar.appendChild(search);
if ( !topbar || topbar.dataset.ewEnhanced === '1' ) {
bar.appendChild(tools);
return;
document.body.insertBefore(bar, document.body.firstChild);
}
document.documentElement.classList.add('ew-js-ready');
}


topbar.dataset.ewEnhanced = '1';


var menus = {
function activateCategoryCards() {
'Explore': [
document.querySelectorAll('.ew-card[data-ew-page]').forEach(function (card) {
[ 'Main Page', 'Main Page' ],
if (card.dataset.ewReady === '1') return;
[ 'Special:RecentChanges', 'Recent Changes' ],
card.dataset.ewReady = '1';
card.setAttribute('role', 'link');
[ 'Special:AllPages', 'All Pages' ],
[ 'Special:Categories', 'Categories' ]
card.setAttribute('tabindex', '0');
],
var destination = card.getAttribute('data-ew-page');
'World': [
function go() { window.location.href = wikiUrl(destination); }
[ 'Category:Nations & Realms', 'Nations & Realms' ],
card.addEventListener('click', go);
[ 'Category:Races & Peoples', 'Races & Peoples' ],
card.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.key === ' ') {
[ 'Category:Locations', 'Locations' ]
event.preventDefault();
],
go();
'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 & Deities', 'Religion & Deities' ],
[ 'Category:Magic & Cosmology', 'Magic & Cosmology' ]
],
'History': [
[ 'Category:History & Timeline', 'History & Timeline' ],
[ 'Timeline', 'Timeline' ]
]
};

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() {
Array.prototype.forEach.call( document.querySelectorAll( '.ew-card' ), function ( card ) {
var link = card.querySelector( 'a[href]' );
if ( !link ) {
return;
}

card.setAttribute( 'tabindex', '0' );
card.setAttribute( 'role', 'link' );

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

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

function loadRecentChanges() {
var loading = document.querySelector( '.ew-recent-loading' );
if ( !loading ) {
return;
}
}
});
});
}


var api = new mw.Api();
function loadRecentChanges() {
api.get( {
var target = document.getElementById('ew-recent-list');
action: 'query',
if (!target) return;
list: 'recentchanges',
var api = new mw.Api();
rcnamespace: 0,
api.get({
action: 'query',
rcshow: '!bot',
list: 'recentchanges',
rcprop: 'title|timestamp|user',
rcprop: 'title|timestamp|user',
rctype: 'edit|new',
rclimit: 5,
rclimit: 5,
rcnamespace: 0,
formatversion: 2
} ).then( function ( data ) {
formatversion: 2
var changes = data && data.query && data.query.recentchanges ? data.query.recentchanges : [];
}).done(function (data) {
loading.textContent = '';
var rows = data && data.query && data.query.recentchanges ? data.query.recentchanges : [];
target.innerHTML = '';
if (!rows.length) {
target.innerHTML = '<div class="ew-recent-loading">No article edits yet.</div>';
return;
}
rows.forEach(function (item) {
var row = document.createElement('div');
row.className = 'ew-recent-row';
var icon = document.createElement('span');
icon.className = 'ew-recent-icon';
icon.textContent = '◆';
var copy = document.createElement('div');
var link = document.createElement('a');
link.href = wikiUrl(item.title);
link.textContent = item.title;
var small = document.createElement('small');
var date = new Date(item.timestamp);
small.textContent = 'Edited ' + date.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
copy.appendChild(link);
copy.appendChild(small);
row.appendChild(icon);
row.appendChild(copy);
target.appendChild(row);
});
}).fail(function () {
target.innerHTML = '<div class="ew-recent-loading">Recent changes unavailable.</div>';
});
}


if ( !changes.length ) {
/* Callback form is compatible with the ResourceLoader API used by MW 1.43. */
loading.textContent = 'No recent article changes.';
mw.loader.using(['mediawiki.util', 'mediawiki.api'], function () {
return;
if (document.readyState === 'loading') {
}
document.addEventListener('DOMContentLoaded', function () {

loadErindalFonts();
changes.forEach( function ( change ) {
buildTopbar();
var row = document.createElement( 'div' );
activateCategoryCards();
row.className = 'ew-recent-row';
loadRecentChanges();

}, { once: true });
var icon = document.createElement( 'span' );
} else {
icon.className = 'ew-recent-icon';
loadErindalFonts();
icon.textContent = change.title.slice( 0, 2 ).toUpperCase();
buildTopbar();

activateCategoryCards();
var copy = document.createElement( 'span' );
loadRecentChanges();
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();
loadRecentChanges();
} );
} );
}() );

Revision as of 22:05, 16 August 2026

/* ========================================================================
   ERINDAL WIKI - COMMON.JS v5.0
   Target: MediaWiki 1.43.x, legacy Vector

   The topbar itself is now rendered by MediaWiki:Sidebar. JavaScript only
   adds dropdowns, the remembered sidebar toggle and dynamic homepage data.
   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 & Realms', 'Nations & Realms' ],
                [ 'Category:Races & Peoples', 'Races & Peoples' ],
                [ '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 & Deities', 'Religion & Deities' ],
                [ 'Category:Magic & Cosmology', 'Magic & Cosmology' ]
            ],
            'History': [
                [ 'Category:History & Timeline', 'History & Timeline' ],
                [ 'Timeline', 'Timeline' ]
            ]
        };

        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() {
        Array.prototype.forEach.call( document.querySelectorAll( '.ew-card' ), function ( card ) {
            var link = card.querySelector( 'a[href]' );
            if ( !link ) {
                return;
            }

            card.setAttribute( 'tabindex', '0' );
            card.setAttribute( 'role', 'link' );

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

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

    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();
            loadRecentChanges();
        } );
    } );
}() );