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-tagautocomplete.js

MediaWiki interface page

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* =============================================================================
   Gadget-tagautocomplete.js
   Adds tokenized tag input with autocomplete on Submit Project / Submit Tutorial
   forms. Pulls existing tags from the Projects and Tutorials Cargo tables.
   ============================================================================= */
( function () {

    var FORM_PAGES = [ 'Form:Submit_Project', 'Form:Submit_Tutorial' ];
    if ( FORM_PAGES.indexOf( mw.config.get( 'wgPageName' ) ) === -1 ) { return; }

    $( function () {

        // ── Find the tags input ──────────────────────────────────────────────
        // Page Forms renders inputs near their label; match by placeholder text.
        var $raw = $( 'input[type="text"]' ).filter( function () {
            var ph = ( $( this ).attr( 'placeholder' ) || '' ).toLowerCase();
            return ph.indexOf( 'tag' ) !== -1;
        } );
        if ( !$raw.length ) { return; }

        // ── Load all existing tags from Cargo ────────────────────────────────
        var allTags = [];

        function fetchTags( table, field ) {
            return $.getJSON( mw.util.wikiScript( 'api' ), {
                action   : 'cargoquery',
                tables   : table,
                fields   : field,
                limit    : 500,
                format   : 'json'
            } ).then( function ( data ) {
                ( data.cargoquery || [] ).forEach( function ( r ) {
                    ( r.title[ field ] || '' ).split( ',' ).forEach( function ( t ) {
                        t = t.trim().toLowerCase();
                        if ( t && allTags.indexOf( t ) === -1 ) {
                            allTags.push( t );
                        }
                    } );
                } );
            } );
        }

        $.when(
            fetchTags( 'Projects',  'tags' ),
            fetchTags( 'Tutorials', 'tags' )
        ).then( function () {
            allTags.sort();
            $raw.each( function () {
                initTokenizer( $( this ) );
            } );
        } );

        // ── Tokenizer ────────────────────────────────────────────────────────
        function initTokenizer( $input ) {
            // Build wrapper to sit alongside the original (now hidden) input
            $input.hide();

            var $wrap = $( '<div>' ).addClass( 'tag-tokenizer' ).insertAfter( $input );
            var $box  = $( '<div>' ).addClass( 'tag-tokenizer-box' ).appendTo( $wrap );
            var $entry = $( '<input>' ).attr( {
                type        : 'text',
                placeholder : 'Type a tag and press Enter or comma\u2026',
                autocomplete: 'off'
            } ).addClass( 'tag-tokenizer-entry' ).appendTo( $box );
            var $dropdown = $( '<ul>' ).addClass( 'tag-tokenizer-dropdown' ).hide().appendTo( $wrap );

            // Seed from any value already in the hidden input (edit mode)
            var existing = ( $input.val() || '' ).split( ',' ).map( function ( t ) { return t.trim(); } ).filter( Boolean );
            existing.forEach( function ( tag ) { addToken( tag ); } );
            syncHidden();

            // ── Token rendering ──────────────────────────────────────────────
            function addToken( tag ) {
                tag = tag.trim().toLowerCase().replace( /\s+/g, '-' );
                if ( !tag ) { return; }
                // Prevent duplicates
                if ( $box.find( '.tag-token' ).filter( function () {
                    return $( this ).data( 'tag' ) === tag;
                } ).length ) { return; }

                var $token = $( '<span>' ).addClass( 'tag-token' ).text( tag ).attr( 'data-tag', tag );
                var $x = $( '<button>' ).attr( 'type', 'button' ).addClass( 'tag-token-remove' ).text( '\u00d7' );
                $token.append( $x );
                $x.on( 'click', function () {
                    $token.remove();
                    syncHidden();
                } );
                $token.insertBefore( $entry );
                syncHidden();
            }

            function syncHidden() {
                var tags = [];
                $box.find( '.tag-token' ).each( function () {
                    tags.push( $( this ).data( 'tag' ) );
                } );
                $input.val( tags.join( ', ' ) );
            }

            // ── Autocomplete dropdown ────────────────────────────────────────
            function showDropdown( query ) {
                var existing = [];
                $box.find( '.tag-token' ).each( function () {
                    existing.push( $( this ).data( 'tag' ) );
                } );

                var matches = allTags.filter( function ( t ) {
                    return t.indexOf( query ) === 0 && existing.indexOf( t ) === -1;
                } ).slice( 0, 8 );

                $dropdown.empty();
                if ( !matches.length ) { $dropdown.hide(); return; }

                matches.forEach( function ( tag ) {
                    $( '<li>' ).text( tag ).on( 'mousedown', function ( e ) {
                        e.preventDefault(); // keep focus in $entry
                        addToken( tag );
                        $entry.val( '' );
                        $dropdown.hide();
                    } ).appendTo( $dropdown );
                } );
                $dropdown.show();
            }

            // ── Events ───────────────────────────────────────────────────────
            $entry.on( 'input', function () {
                var val = $entry.val();
                // Comma or space after a word = commit tag
                if ( /[,]$/.test( val ) ) {
                    addToken( val.replace( /,$/, '' ) );
                    $entry.val( '' );
                    $dropdown.hide();
                    return;
                }
                var q = val.trim().toLowerCase();
                if ( q.length >= 1 ) { showDropdown( q ); } else { $dropdown.hide(); }
            } );

            $entry.on( 'keydown', function ( e ) {
                // Enter = commit current text or first suggestion
                if ( e.key === 'Enter' ) {
                    e.preventDefault();
                    var first = $dropdown.find( 'li' ).first().text();
                    addToken( first || $entry.val() );
                    $entry.val( '' );
                    $dropdown.hide();
                }
                // Backspace on empty entry = remove last token
                if ( e.key === 'Backspace' && $entry.val() === '' ) {
                    $box.find( '.tag-token' ).last().remove();
                    syncHidden();
                }
            } );

            $entry.on( 'blur', function () {
                setTimeout( function () { $dropdown.hide(); }, 150 );
                // Commit anything partially typed on blur
                var val = $entry.val().trim();
                if ( val ) { addToken( val ); $entry.val( '' ); }
            } );

            $box.on( 'click', function () { $entry.trigger( 'focus' ); } );
        }

    } );

}() );