Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.
       🚧 True to our name, we’re still a work in progress. 🚧
   
       You’re welcome to explore, but account registration is currently invite-only as we finalize the setup. 
       Join our forum or follow Mastodon for updates. 
       Full Wiki launch coming soon!
   

MediaWiki:Gadget-subpagenav.js: Difference between revisions

MediaWiki interface page
Rewrite to align with new template update
No edit summary
 
Line 196: Line 196:
                 $a.addClass( 'is-current' );
                 $a.addClass( 'is-current' );


                 // Walk up the DOM and open every ancestor dropdown
                 // Walk up the DOM and open ancestor dropdowns.
                 $a.parents( '.subpage-nav-links li.has-dropdown' ).each( function () {
                // Start from $a.parent() so we do NOT auto-open the dropdown
                // that the current page itself belongs to — only true ancestors.
                 $a.parent().parents( '.subpage-nav-links li.has-dropdown' ).each( function () {
                     $( this ).addClass( 'is-open' );
                     $( this ).addClass( 'is-open' );
                 } );
                 } );

Latest revision as of 00:37, 5 April 2026

/* =============================================================================
   MediaWiki:Gadget-subpagenav.js
   UnfinishedProjects Wiki

   Auto-builds nested dropdown menus from Special:PrefixIndex output and
   wires up accessible toggle behaviour for Template:SubpageNav.

   TABLE OF CONTENTS
   Part 1 — Auto-Builder  (converts flat PrefixIndex list → nested tree)
   Part 2 — Manual Lists  (adds carets to hand-authored <ul> trees)
   Part 3 — Click Handlers
   Part 4 — Current-Page Highlight
   ============================================================================= */
( function () {
    $( function () {

        var $navLinks = $( '.subpage-nav-links' );
        if ( !$navLinks.length ) return;


        /* =====================================================================
           PART 1: AUTO-BUILDER
           Converts the flat <a> list from Special:PrefixIndex into a proper
           nested tree.

           WHY TWO PASSES?
           PrefixIndex lists pages alphabetically. If "About" appears before
           "About/More Details", a single-pass builder would create "About" as a
           leaf <li> (no child <ul>). When "About/More Details" is processed next
           it tries to descend into a non-existent <ul> and silently drops the
           item. Building a plain-JS tree first, then rendering, avoids this
           entirely — a node can be both a real page AND a parent folder.
           ===================================================================== */
        $navLinks.find( '.subpage-auto-list' ).each( function () {
            var $autoContainer = $( this );

            // --- Pass A: Build a plain-JS tree --------------------------------
            // Each node shape: { href: null|string, children: {} }
            var tree = {};

            $autoContainer.find( 'a' ).each( function () {
                var href = $( this ).attr( 'href' );
                var fullPath = $( this ).text().trim(); // e.g. "About/More Details"
                var parts = fullPath.split( '/' );

                var node = tree;
                for ( var i = 0; i < parts.length; i++ ) {
                    var part = parts[ i ].trim();
                    if ( !part ) { continue; }

                    if ( !node[ part ] ) {
                        node[ part ] = { href: null, children: {} };
                    }
                    // If this is the last segment it's a real, linkable page
                    if ( i === parts.length - 1 ) {
                        node[ part ].href = href;
                    }
                    // Descend regardless — the same segment can be both a page
                    // and a parent folder (dual-node case)
                    node = node[ part ].children;
                }
            } );

            // --- Pass B: Render tree to DOM (recursive) -----------------------
            function renderTree( treeNode ) {
                var $ul = $( '<ul>' );

                $.each( treeNode, function ( key, item ) {
                    var $li = $( '<li>' );
                    var hasChildren = Object.keys( item.children ).length > 0;

                    if ( item.href ) {
                        // Real page — render as a navigable link
                        $li.append( $( '<a>' ).attr( 'href', item.href ).text( key ) );
                    } else {
                        // Virtual folder — no page exists, render as a label
                        $li.append( $( '<span>' ).text( key ) );
                    }

                    if ( hasChildren ) {
                        // Add a SIBLING caret <button> — not inside the <a>.
                        // This keeps the link fully navigable; only the button toggles.
                        $li.addClass( 'has-dropdown' );
                        $li.append(
                            $( '<button>' )
                                .addClass( 'subpage-nav-caret' )
                                .attr( 'type', 'button' )
                                .attr( 'aria-label', 'Toggle submenu' )
                                .text( '▾' )
                        );
                        $li.append( renderTree( item.children ) );
                    }

                    $ul.append( $li );
                } );

                return $ul;
            }

            // Swap the raw PrefixIndex block with our clean nested list
            $autoContainer.replaceWith( renderTree( tree ) );
        } );


        /* =====================================================================
           PART 2: MANUAL LISTS
           When the template is called with a hand-authored wikitext list
           (parameter 1), MediaWiki already renders correct <ul>/<li> nesting.
           We just need to find <li> elements that contain a child <ul> and
           wire them up the same way the auto-builder does.

           KEY DIFFERENCE from old approach:
           Old code appended the caret INSIDE the <a> tag, then called
           e.preventDefault() on the <a> click — this prevented navigation.
           New code adds a separate sibling <button> for the caret, so the
           link remains fully clickable for navigation.
           ===================================================================== */
        $navLinks.find( 'li' ).has( 'ul' ).each( function () {
            var $li = $( this );

            // Skip anything already wired up by Part 1
            if ( $li.hasClass( 'has-dropdown' ) ) { return; }
            $li.addClass( 'has-dropdown' );

            var $trigger = $li.children( 'a, span' ).first();
            var $caret = $( '<button>' )
                .addClass( 'subpage-nav-caret' )
                .attr( 'type', 'button' )
                .attr( 'aria-label', 'Toggle submenu' )
                .text( '▾' );

            if ( $trigger.is( 'a' ) ) {
                // Link stays navigable — insert caret as a sibling AFTER the link
                $trigger.after( $caret );
            } else {
                // No real link — caret goes inside the <span> as a child
                $trigger.append( $caret );
            }
        } );


        /* =====================================================================
           PART 3: CLICK HANDLERS

           Caret button  → toggle is-open on parent <li>, close siblings
           Folder <span> → same (clicking the label of a folder-only item)
           Outside click → close all open dropdowns
           ===================================================================== */

        // Caret button click
        $navLinks.on( 'click', '.has-dropdown > .subpage-nav-caret', function ( e ) {
            e.preventDefault();
            e.stopPropagation();
            var $parentLi = $( this ).parent();
            // Close siblings at the same nesting level
            $parentLi.siblings( '.is-open' ).removeClass( 'is-open' );
            $parentLi.toggleClass( 'is-open' );
        } );

        // Folder label <span> click (virtual folder — no navigation possible)
        $navLinks.on( 'click', '.has-dropdown > span', function ( e ) {
            e.preventDefault();
            var $parentLi = $( this ).parent();
            $parentLi.siblings( '.is-open' ).removeClass( 'is-open' );
            $parentLi.toggleClass( 'is-open' );
        } );

        // Click anywhere outside the nav closes all open dropdowns
        $( document ).on( 'click', function ( e ) {
            if ( !$( e.target ).closest( '.subpage-nav-links' ).length ) {
                $navLinks.find( '.is-open' ).removeClass( 'is-open' );
            }
        } );


        /* =====================================================================
           PART 4: CURRENT-PAGE HIGHLIGHT
           Reads the current page name from MediaWiki, finds the matching link
           in the nav, marks it with .is-current, and auto-opens any ancestor
           dropdown <li> elements so the user can immediately see where they are.

           Normalises underscores ↔ spaces since MediaWiki uses both.
           ===================================================================== */
        var currentPage = ( mw.config.get( 'wgPageName' ) || '' ).replace( /_/g, ' ' );
        if ( !currentPage ) { return; }

        $navLinks.find( 'a' ).each( function () {
            var $a = $( this );
            var href = decodeURIComponent( $a.attr( 'href' ) || '' );

            // MediaWiki internal links look like /wiki/Page_Name
            // Strip the /wiki/ prefix and normalise underscores
            var linkPage = href.replace( /^.*\/wiki\//, '' ).replace( /_/g, ' ' );

            if ( linkPage === currentPage ) {
                $a.addClass( 'is-current' );

                // Walk up the DOM and open ancestor dropdowns.
                // Start from $a.parent() so we do NOT auto-open the dropdown
                // that the current page itself belongs to — only true ancestors.
                $a.parent().parents( '.subpage-nav-links li.has-dropdown' ).each( function () {
                    $( this ).addClass( 'is-open' );
                } );

                return false; // Stop iterating once found
            }
        } );

    } );
}() );