User:Enterprisey/superjump.js

Source: Wikipedia, the free encyclopedia.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// vim: ts=4 sw=4 et ai
// <nowiki>
( function () {
    // sample value: {"a": {"b": "WP:RFA", "c": {"d": "WP:ANI"}}}
    var gMenuJson = null;

    // sample value: ["a", "b"]
    var gPressedKeys = [];

    var gGeneralKeyListener = null;

    // GUI components
    var gBackBtn;
    //var gBreadcrumb;

    var SUPERJUMP_PANEL_ID = "superjump-container-panel";
    var SUPERJUMP_TABLE_ID = "superjump-keys-table";
    var MENU_JSON_LOCALSTORAGE_KEY = "enwp_superjump_menu_json";
    var MENU_JSON_LOCALSTORAGE_SEPARATOR = "|";

    function initAccessKeyListeners() {
        for( var key in gMenuJson ) {
            initAccessKeyListener( key, function () { handleKey( key, gMenuJson, /* accessKey */ true ); } );
        }
    }

    function navigateToPath( keys ) {
        var menu = gMenuJson;
        for( var i = 0; i < keys.length - 1; i++ ) {
            menu = menu[ keys[i] ];
        }
        gPressedKeys = keys.slice( 0, keys.length - 1 );
        var lastKey = keys[ keys.length - 1 ];
        handleKey( lastKey, menu );
    }

    function handleKey( key, menu, accessKey ) {
        if( key == "Escape" ) {
            closeInterface();
            gPressedKeys = [];
            return;
        } else if( key == "Backspace" ) {
            if( gPressedKeys.length > 1 ) {
                navigateToPath( gPressedKeys.slice( 0, gPressedKeys.length - 1 ) );
            }
            return;
        }

        var submenu = menu[ key ];

        if( typeof submenu === typeof "" ) {
            window.location.href = mw.util.getUrl( submenu );
        } else if( submenu ) {
            gPressedKeys.push( key );
            displayOrUpdateInterface( makePanel( submenu ) );
            if( gGeneralKeyListener ) {
                document.removeEventListener( "keydown", gGeneralKeyListener );
                gGeneralKeyListener = null;
            }
            gGeneralKeyListener = function ( event ) {
                handleKey( event.key, submenu );
            };
            document.addEventListener( "keydown", gGeneralKeyListener );
        }
    }

    function displayOrUpdateInterface( panel ) {
        panel.id = SUPERJUMP_TABLE_ID;

        var containerPanel = document.getElementById( SUPERJUMP_PANEL_ID );
        if( !containerPanel ) {
            containerPanel = createInterface();
            document.body.appendChild( containerPanel );
        }
        containerPanel.replaceChild( panel, document.getElementById( SUPERJUMP_TABLE_ID ) );
        gBackBtn.disabled = gPressedKeys.length < 2;
        //gBreadcrumb.textContent = "Pressed: " + gPressedKeys.map( function ( x ) { return "[" + x + "]"; } ).join( " > " );
    }

    function createInterface() {
        containerPanel = document.createElement( "div" );
        containerPanel.id = SUPERJUMP_PANEL_ID;

        var topBar = document.createElement( "div" );
        gBackBtn = document.createElement( "button" );
        gBackBtn.textContent = "Back [Backspace]";
        gBackBtn.addEventListener( "click", function () {
            navigateToPath( gPressedKeys.slice( 0, gPressedKeys.length - 1 ) );
        } );
        topBar.appendChild( gBackBtn );
        var closeBtn = document.createElement( "button" );
        closeBtn.textContent = "Close [Esc]";
        closeBtn.addEventListener( "click", closeInterface );
        topBar.appendChild( closeBtn );
        containerPanel.appendChild( topBar );

        var startingPanel = document.createElement( "div" );
        startingPanel.id = SUPERJUMP_TABLE_ID;
        containerPanel.appendChild( startingPanel );

        //var bottomBar = document.createElement( "div" );
        //gBreadcrumb = document.createElement( "span" );
        //bottomBar.appendChild( gBreadcrumb );
        //var editConfigBtn = document.createElement( "button" );
        //editConfigBtn.textContent = "Edit configuration";
        //editConfigBtn.addEventListener( "click", function () {
        //    window.location.href = mw.util.getUrl( getMenuJsonPageTitle(), { action: "edit" } );
        //} );
        //bottomBar.appendChild( editConfigBtn );
        //containerPanel.appendChild( bottomBar );

        return containerPanel;
    }

    function closeInterface() {
        var panel = document.getElementById( SUPERJUMP_PANEL_ID );
        panel.parentNode.removeChild( panel );
        if( gGeneralKeyListener ) {
            document.removeEventListener( "keydown", gGeneralKeyListener );
        }
    }

    function makePanel( menu ) {
        var table = document.createElement( "table" );

        var keys = Object.keys( menu );
        var numItems = keys.length;
        var tds = new Array( numItems );
        for( var i = 0; i < numItems; i++ ) {
            var td = document.createElement( "td" );
            var keySpan = document.createElement( "span" );
            keySpan.className = "key-span";
            keySpan.textContent = "[" + keys[i] + "]";
            td.appendChild( keySpan );
            var actionSpan = document.createElement( "span" );
            var menuItem = menu[ keys[i] ];
            if( typeof menuItem === typeof "" ) {
                actionSpan.textContent = menuItem;
            } else {
                var numChoices = Object.keys( menuItem ).length;
                actionSpan.textContent = numChoices + " choice" + ( numChoices !== 1 ? "s" : "" );
            }

            td.appendChild( actionSpan );
            tds[i] = td;
        }

        var numRows = Math.max( 2, Math.ceil( Math.sqrt( numItems ) ) );
        var numCols = numRows;
        for( var i = 0; i < numRows; i++ ) {
            var tr = document.createElement( "tr" );
            for( var j = 0; j < numCols; j++ ) {
                var cell = tds[ i * numCols + j ];
                if( cell ) {
                    tr.appendChild( cell );
                }
            }
            table.appendChild( tr );
        }

        return table;
    }

    function initAccessKeyListener( key, listener ) {
        var hasOurAccessKey = document.querySelectorAll( "a[accesskey=" + key + "]" );
        if( hasOurAccessKey.length ) {
            for( var i = 0; i < hasOurAccessKey.length; i++ ) {
                hasOurAccessKey[i].setAttribute( "accesskey", "" );
                $( hasOurAccessKey[i] ).updateTooltipAccessKeys();
            }
        }

        $( "body" ).append( $( "<a>" )
            .attr( { href: "#", accesskey: key } )
            .css( { position: "fixed", opacity: "0" } )
            .text( "superjump-link-" + key )
            .addClass( "superjump-accesskey-sink" )
            .click( listener ) );
    }

    function setMenuJson() {
        var reqParams = {
            format: "json",
            action: "query",
            prop: "revisions",
            titles: getMenuJsonPageTitle(),
            rvprop: "content|ids",
            rvslots: "main"
        };

        var storedRevidAndMenuJson = getStoredRevidAndMenuJson();
        var storedRevid, storedMenuJson;
        if( storedRevidAndMenuJson ) {
            storedRevid = storedRevidAndMenuJson[0];
            storedMenuJson = storedRevidAndMenuJson[1];

            reqParams.rvstartid = parseInt( storedRevid ) + 1;
            reqParams.rvdir = "newer";
        }

        return $.getJSON(
            mw.util.wikiScript( "api" ),
            reqParams
        ).then( function ( data ) {
            if( !data || !data.query || !data.query.pages ) {
                mw.notify( "Error loading menu JSON page!" );
                return;
            }

            var pageId = Object.keys( data.query.pages )[0];
            var pageObj = data.query.pages[pageId];
            if( pageObj.missing ) {
                mw.notify( "Error! You'll need to create " + getMenuJsonPageTitle() + " to use superjump." );
                return;
            }

            var revs = pageObj.revisions;
            if( !revs ) {
                gMenuJson = JSON.parse(storedMenuJson);
                return;
            }

            var mostRecentRev = revs[ revs.length - 1 ];
            var apiRevid = mostRecentRev.revid;
            var apiJson = mostRecentRev.slots.main["*"];

            if( isLocalStorageAvailable() ) {
                var storageValue = apiRevid + MENU_JSON_LOCALSTORAGE_SEPARATOR + apiJson;
                window.localStorage.setItem( MENU_JSON_LOCALSTORAGE_KEY, storageValue );
            }

            gMenuJson = JSON.parse(apiJson);
        } );
    }

    function getMenuJsonPageTitle() {
        return "User:" + mw.config.get( "wgUserName" ) + "/superjump-config.json";
    }

    function getStoredRevidAndMenuJson() {
        var localStorageAvailable = isLocalStorageAvailable();
        if( localStorageAvailable ) {
            var storedRevidAndMenuJson = window.localStorage.getItem( MENU_JSON_LOCALSTORAGE_KEY );
            if( storedRevidAndMenuJson && storedRevidAndMenuJson.indexOf( MENU_JSON_LOCALSTORAGE_SEPARATOR ) >= 0 ) {
                return storedRevidAndMenuJson.split( MENU_JSON_LOCALSTORAGE_SEPARATOR );
            }
        }

        return null;
    }

    function isLocalStorageAvailable() {
        try {
            var storage = window.localStorage,
                x = '__storage_test__';
            storage.setItem( x, x );
            storage.removeItem( x );
            return true;
        } catch( e ) {
            return false;
        }
    }

    $.when( $.ready, mw.loader.using( "mediawiki.util" ) )
        .then( setMenuJson )
        .then( function () {
            mw.util.addCSS( "#" + SUPERJUMP_PANEL_ID + "{ position: fixed; "+
                    "top: 50%; left: 50%; margin-right: -50%; transform: translate(-50%, -50%); " +
                "background-color:white; padding: 1em; border:thin solid gray;" +
                "box-shadow:0px 7px 7px 0px rgba(0,0,0,0.3)}"+
            "#" + SUPERJUMP_PANEL_ID + " > div * { margin-left: 0.75em; }"+
            "#" + SUPERJUMP_TABLE_ID + " td{padding: 0.5em;}"+
            "#" + SUPERJUMP_TABLE_ID + " .key-span{font-family:monospace; cursor:default; padding-right: 0.1em; font-size: 200%}"+
            "#" + SUPERJUMP_TABLE_ID + " span{vertical-align:middle;}");
            initAccessKeyListeners();
        } );
} )();
// </nowiki>