MediaWiki:Gadget-tagautocomplete.js: Difference between revisions
MediaWiki interface page
More actions
Created page with "→============================================================================= 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.co..."  |
Previous Version did not work - hopefully this one does. Â |
||
| (One intermediate revision by the same user not shown) | |||
| Line 1: | Line 1: | ||
/* ============================================================================= | /* ============================================================================= | ||
  Gadget-tagautocomplete.js |   Gadget-tagautocomplete.js | ||
  |   Tokenized tag input with autocomplete on Submit Project / Tutorial forms. | ||
  |   Pulls existing tags from the Projects and Tutorials Cargo tables. | ||
  ============================================================================= */ |   ============================================================================= */ | ||
( function () { | ( function () { | ||
   |    // Only run on Special:FormEdit/* pages | ||
   if |    if ( mw.config.get( 'wgCanonicalSpecialPageName' ) !== 'FormEdit' ) { return; } | ||
   $( function () { |    $( function () { | ||
     // ── Find the tags input |      // ── Find the tags input by name attribute ──────────────────────────── | ||
     // Page Forms |      // Page Forms names inputs as TemplateName[fieldname] — target [tags] | ||
     var $raw = $( 'input |     // specifically. We use .filter() because jQuery CSS attribute selectors | ||
       var |     // require the attribute to be explicitly present in the HTML; Page Forms | ||
       return |     // 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; } |      if ( !$raw.length ) { return; } | ||
| Line 22: | Line 26: | ||
     var allTags = []; |      var allTags = []; | ||
     function fetchTags( table |      function fetchTags( table ) { | ||
       return $.getJSON( mw.util.wikiScript( 'api' ), { |        return $.getJSON( mw.util.wikiScript( 'api' ), { | ||
         action |          action : 'cargoquery', | ||
         tables |          tables : table, | ||
         fields |          fields : 'tags', | ||
         limit |          limit  : 500, | ||
         format |          format : 'json' | ||
       } ).then( function ( data ) { |        } ).then( function ( data ) { | ||
         ( data.cargoquery || [] ).forEach( function ( r ) { |          ( data.cargoquery || [] ).forEach( function ( r ) { | ||
           ( r.title |            ( r.title.tags || '' ).split( ',' ).forEach( function ( t ) { | ||
             t = t.trim().toLowerCase(); |              t = t.trim().toLowerCase(); | ||
             if ( t && allTags.indexOf( t ) === -1 ) { |              if ( t && allTags.indexOf( t ) === -1 ) { | ||
| Line 42: | Line 46: | ||
     $.when( |      $.when( | ||
       fetchTags( 'Projects |        fetchTags( 'Projects' ), | ||
       fetchTags( 'Tutorials |        fetchTags( 'Tutorials' ) | ||
     ).then( function () { |      ).then( function () { | ||
       allTags.sort(); |        allTags.sort(); | ||
| Line 53: | Line 57: | ||
     // ── Tokenizer ──────────────────────────────────────────────────────── |      // ── Tokenizer ──────────────────────────────────────────────────────── | ||
     function initTokenizer( $input ) { |      function initTokenizer( $input ) { | ||
       $input.hide(); |        $input.hide(); | ||
       var $wrap = $( '<div>' ).addClass( 'tag-tokenizer' ).insertAfter( $input ); |        var $wrap   = $( '<div>' ).addClass( 'tag-tokenizer' ).insertAfter( $input ); | ||
       var $box |        var $box   = $( '<div>' ).addClass( 'tag-tokenizer-box' ).appendTo( $wrap ); | ||
       var $entry = $( '<input>' ).attr( { |        var $entry  = $( '<input>' ).attr( { | ||
         type    : 'text', |          type    : 'text', | ||
         placeholder : 'Type a tag and press Enter or comma\u2026', |          placeholder : 'Type a tag and press Enter or comma\u2026', | ||
| Line 65: | Line 68: | ||
       var $dropdown = $( '<ul>' ).addClass( 'tag-tokenizer-dropdown' ).hide().appendTo( $wrap ); |        var $dropdown = $( '<ul>' ).addClass( 'tag-tokenizer-dropdown' ).hide().appendTo( $wrap ); | ||
       // Seed from |        // Seed from existing hidden input value (edit mode) | ||
       var existing = ( $input.val() || '' ).split( ',' ).map( function ( t ) { return t.trim(); } ).filter( Boolean ); |        var existing = ( $input.val() || '' ).split( ',' ) | ||
       existing.forEach( |         .map( function ( t ) { return t.trim(); } ) | ||
        .filter( Boolean ); | |||
       existing.forEach( addToken ); | |||
       syncHidden(); |        syncHidden(); | ||
       function addToken( tag ) { |        function addToken( tag ) { | ||
         tag = tag.trim().toLowerCase().replace( /\s+/g, '-' ); |          tag = tag.trim().toLowerCase().replace( /\s+/g, '-' ); | ||
         if ( !tag ) { return; } |          if ( !tag ) { return; } | ||
         if ( $box.find( '.tag-token' ).filter( function () { |          if ( $box.find( '.tag-token' ).filter( function () { | ||
           return $( this ).data( 'tag' ) === tag; |            return $( this ).data( 'tag' ) === tag; | ||
| Line 80: | Line 83: | ||
         var $token = $( '<span>' ).addClass( 'tag-token' ).text( tag ).attr( 'data-tag', tag ); |          var $token = $( '<span>' ).addClass( 'tag-token' ).text( tag ).attr( 'data-tag', tag ); | ||
         var $x = $( '<button>' ).attr( 'type', 'button' ).addClass( 'tag-token-remove' ).text( '\u00d7' ) |          var $x = $( '<button>' ) | ||
          .attr( 'type', 'button' ) | |||
          .addClass( 'tag-token-remove' ) | |||
          .text( '\u00d7' ) | |||
          .on( 'click', function () { | |||
            $token.remove(); | |||
         $token.insertBefore( $entry ); |             syncHidden(); | ||
          } ); | |||
         $token.append( $x ).insertBefore( $entry ); | |||
         syncHidden(); |          syncHidden(); | ||
       } |        } | ||
| Line 98: | Line 103: | ||
       } |        } | ||
       function showDropdown( query ) { |        function showDropdown( query ) { | ||
         var |          var active = []; | ||
         $box.find( '.tag-token' ).each( function () { |          $box.find( '.tag-token' ).each( function () { | ||
           |            active.push( $( this ).data( 'tag' ) ); | ||
         } ); |          } ); | ||
         var matches = allTags.filter( function ( t ) { |          var matches = allTags.filter( function ( t ) { | ||
           return t.indexOf( query ) === 0 && |            return t.indexOf( query ) === 0 && active.indexOf( t ) === -1; | ||
         } ).slice( 0, 8 ); |          } ).slice( 0, 8 ); | ||
| Line 114: | Line 118: | ||
         matches.forEach( function ( tag ) { |          matches.forEach( function ( tag ) { | ||
           $( '<li>' ).text( tag ).on( 'mousedown', function ( e ) { |            $( '<li>' ).text( tag ).on( 'mousedown', function ( e ) { | ||
             e.preventDefault(); |              e.preventDefault(); | ||
             addToken( tag ); |              addToken( tag ); | ||
             $entry.val( '' ); |              $entry.val( '' ); | ||
| Line 123: | Line 127: | ||
       } |        } | ||
       $entry.on( 'input', function () { |        $entry.on( 'input', function () { | ||
         var val = $entry.val(); |          var val = $entry.val(); | ||
         if ( /,$/.test( val ) ) { | |||
         if ( / | |||
           addToken( val.replace( /,$/, '' ) ); |            addToken( val.replace( /,$/, '' ) ); | ||
           $entry.val( '' ); |            $entry.val( '' ); | ||
| Line 138: | Line 140: | ||
       $entry.on( 'keydown', function ( e ) { |        $entry.on( 'keydown', function ( e ) { | ||
         if ( e.key === 'Enter' ) { |          if ( e.key === 'Enter' ) { | ||
           e.preventDefault(); |            e.preventDefault(); | ||
           var first = $dropdown.find( 'li' |            var first = $dropdown.find( 'li:first' ).text(); | ||
           addToken( first || $entry.val() ); |            addToken( first || $entry.val() ); | ||
           $entry.val( '' ); |            $entry.val( '' ); | ||
           $dropdown.hide(); |            $dropdown.hide(); | ||
         } |          } | ||
         if ( e.key === 'Backspace' && $entry.val() === '' ) { |          if ( e.key === 'Backspace' && $entry.val() === '' ) { | ||
           $box.find( '.tag-token' ).last().remove(); |            $box.find( '.tag-token' ).last().remove(); | ||
| Line 155: | Line 155: | ||
       $entry.on( 'blur', function () { |        $entry.on( 'blur', function () { | ||
         setTimeout( function () { $dropdown.hide(); }, 150 ); |          setTimeout( function () { $dropdown.hide(); }, 150 ); | ||
         var val = $entry.val().trim(); |          var val = $entry.val().trim(); | ||
         if ( val ) { addToken( val ); $entry.val( '' ); } |          if ( val ) { addToken( val ); $entry.val( '' ); } | ||
Latest revision as of 09:22, 5 April 2026
/* =============================================================================
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' ); } );
}
} );
}() );