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
   Tokenized tag input with autocomplete on Submit Project / Tutorial forms.
   Pulls existing tags from the Projects and Tutorials Cargo tables.
   ============================================================================= */
( function () {

    // Only run on Special:FormEdit/* pages
    if ( mw.config.get( 'wgCanonicalSpecialPageName' ) !== 'FormEdit' ) { return; }

    $( function () {

        // ── Find the tags input by name attribute ────────────────────────────
        // Page Forms names inputs as TemplateName[fieldname] β€” target [tags]
        // specifically. We use .filter() because jQuery CSS attribute selectors
        // require the attribute to be explicitly present in the HTML; Page Forms
        // does not write type="text" on its inputs, so [type="text"] never matches.
        var $raw = $( 'input' ).filter( function () {
            var name = $( this ).attr( 'name' ) || '';
            return name === 'ProjectMeta[tags]' || name === 'TutorialMeta[tags]';
        } );

        if ( !$raw.length ) { return; }

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

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

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

        // ── Tokenizer ────────────────────────────────────────────────────────
        function initTokenizer( $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 existing hidden input value (edit mode)
            var existing = ( $input.val() || '' ).split( ',' )
                .map( function ( t ) { return t.trim(); } )
                .filter( Boolean );
            existing.forEach( addToken );
            syncHidden();

            function addToken( tag ) {
                tag = tag.trim().toLowerCase().replace( /\s+/g, '-' );
                if ( !tag ) { return; }
                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' )
                    .on( 'click', function () {
                        $token.remove();
                        syncHidden();
                    } );
                $token.append( $x ).insertBefore( $entry );
                syncHidden();
            }

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

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

                var matches = allTags.filter( function ( t ) {
                    return t.indexOf( query ) === 0 && active.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();
                        addToken( tag );
                        $entry.val( '' );
                        $dropdown.hide();
                    } ).appendTo( $dropdown );
                } );
                $dropdown.show();
            }

            $entry.on( 'input', function () {
                var val = $entry.val();
                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 ) {
                if ( e.key === 'Enter' ) {
                    e.preventDefault();
                    var first = $dropdown.find( 'li:first' ).text();
                    addToken( first || $entry.val() );
                    $entry.val( '' );
                    $dropdown.hide();
                }
                if ( e.key === 'Backspace' && $entry.val() === '' ) {
                    $box.find( '.tag-token' ).last().remove();
                    syncHidden();
                }
            } );

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

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

    } );

}() );