mw.widgets.CategorySelector: Link to category page and display existence status
authorBartosz Dziewoński <matma.rex@gmail.com>
Thu, 24 Sep 2015 18:45:36 +0000 (20:45 +0200)
committerBartosz Dziewoński <matma.rex@gmail.com>
Tue, 29 Sep 2015 18:58:13 +0000 (20:58 +0200)
This contains a silly amount of code to avoid firing separate requests
for each CategoryCapsuleItemWidget that is created, which really
should be a separate class shared with TitleInputWidget but isn't now.
The real widget code here is quite simple.

Change-Id: I66e603fd45b8d3fd61618f9115cd031f6fa01e9d

resources/Resources.php
resources/src/mediawiki.widgets/mw.widgets.CategoryCapsuleItemWidget.js [new file with mode: 0644]
resources/src/mediawiki.widgets/mw.widgets.CategorySelector.js

index b35ebad..a7212ef 100644 (file)
@@ -1897,6 +1897,7 @@ return array(
                        'resources/src/mediawiki.widgets/mw.widgets.ComplexTitleInputWidget.js',
                        'resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js',
                        'resources/src/mediawiki.widgets/mw.widgets.UserInputWidget.js',
+                       'resources/src/mediawiki.widgets/mw.widgets.CategoryCapsuleItemWidget.js',
                        'resources/src/mediawiki.widgets/mw.widgets.CategorySelector.js',
                ),
                'skinStyles' => array(
diff --git a/resources/src/mediawiki.widgets/mw.widgets.CategoryCapsuleItemWidget.js b/resources/src/mediawiki.widgets/mw.widgets.CategoryCapsuleItemWidget.js
new file mode 100644 (file)
index 0000000..f1c4f6f
--- /dev/null
@@ -0,0 +1,139 @@
+/*!
+ * MediaWiki Widgets - CategoryCapsuleItemWidget class.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+( function ( $, mw ) {
+
+       /**
+        * @class mw.widgets.CategoryCapsuleItemWidget
+        */
+
+       var processExistenceCheckQueueDebounced,
+               api = new mw.Api(),
+               currentRequest = null,
+               existenceCache = {},
+               existenceCheckQueue = {};
+
+       // The existence checking code really could be refactored into a separate class.
+
+       /**
+        * @private
+        */
+       function processExistenceCheckQueue() {
+               var queue, titles;
+               if ( currentRequest ) {
+                       // Don't fire off a million requests at the same time
+                       currentRequest.always( function () {
+                               currentRequest = null;
+                               processExistenceCheckQueueDebounced();
+                       } );
+                       return;
+               }
+               queue = existenceCheckQueue;
+               existenceCheckQueue = {};
+               titles = Object.keys( queue ).filter( function ( title ) {
+                       if ( existenceCache.hasOwnProperty( title ) ) {
+                               queue[ title ].resolve( existenceCache[ title ] );
+                       }
+                       return !existenceCache.hasOwnProperty( title );
+               } );
+               if ( !titles.length ) {
+                       return;
+               }
+               currentRequest = api.get( {
+                       action: 'query',
+                       prop: [ 'info' ],
+                       titles: titles
+               } ).done( function ( response ) {
+                       var index, curr, title;
+                       for ( index in response.query.pages ) {
+                               curr = response.query.pages[ index ];
+                               title = mw.Title.newFromText( curr.title ).getPrefixedText();
+                               existenceCache[ title ] = curr.missing === undefined;
+                               queue[ title ].resolve( existenceCache[ title ] );
+                       }
+               } );
+       }
+
+       processExistenceCheckQueueDebounced = OO.ui.debounce( processExistenceCheckQueue );
+
+       /**
+        * Register a request to check whether a page exists.
+        *
+        * @private
+        * @param {mw.Title} title
+        * @return {jQuery.Promise} Promise resolved with true if the page exists or false otherwise
+        */
+       function checkPageExistence( title ) {
+               var key = title.getPrefixedText();
+               if ( !existenceCheckQueue[ key ] ) {
+                       existenceCheckQueue[ key ] = $.Deferred();
+               }
+               processExistenceCheckQueueDebounced();
+               return existenceCheckQueue[ key ].promise();
+       }
+
+       /**
+        * Category selector capsule item widget. Extends OO.ui.CapsuleItemWidget with the ability to link
+        * to the given page, and to show its existence status (i.e., whether it is a redlink).
+        *
+        * @uses mw.Api
+        * @extends OO.ui.CapsuleItemWidget
+        *
+        * @constructor
+        * @param {Object} config Configuration options
+        * @cfg {mw.Title} title Page title to use (required)
+        */
+       mw.widgets.CategoryCapsuleItemWidget = function MWWCategoryCapsuleItemWidget( config ) {
+               // Parent constructor
+               mw.widgets.CategoryCapsuleItemWidget.parent.call( this, $.extend( {
+                       data: config.title.getMainText(),
+                       label: config.title.getMainText()
+               }, config ) );
+
+               // Properties
+               this.title = config.title;
+               this.$link = $( '<a>' )
+                       .text( this.label )
+                       .attr( 'target', '_blank' )
+                       .on( 'click', function ( e ) {
+                               // CapsuleMultiSelectWidget really wants to prevent you from clicking the link, don't let it
+                               e.stopPropagation();
+                       } );
+
+               // Initialize
+               this.setMissing( false );
+               this.$label.replaceWith( this.$link );
+               this.setLabelElement( this.$link );
+               checkPageExistence( this.title ).done( function ( exists ) {
+                       this.setMissing( !exists );
+               }.bind( this ) );
+       };
+
+       /* Setup */
+
+       OO.inheritClass( mw.widgets.CategoryCapsuleItemWidget, OO.ui.CapsuleItemWidget );
+
+       /* Methods */
+
+       /**
+        * Update label link href and CSS classes to reflect page existence status.
+        *
+        * @private
+        * @param {boolean} missing Whether the page is missing (does not exist)
+        */
+       mw.widgets.CategoryCapsuleItemWidget.prototype.setMissing = function ( missing ) {
+               if ( !missing ) {
+                       this.$link
+                               .attr( 'href', this.title.getUrl() )
+                               .removeClass( 'new' );
+               } else {
+                       this.$link
+                               .attr( 'href', this.title.getUrl( { action: 'edit', redlink: 1 } ) )
+                               .addClass( 'new' );
+               }
+       };
+
+}( jQuery, mediaWiki ) );
index 995561e..e40caaa 100644 (file)
@@ -5,7 +5,8 @@
  * @license The MIT License (MIT); see LICENSE.txt
  */
 ( function ( $, mw ) {
-       var CSP;
+       var CSP,
+               NS_CATEGORY = mw.config.get( 'wgNamespaceIds' ).category;
 
        /**
         * Category selector widget. Displays an OO.ui.CapsuleMultiSelectWidget
@@ -57,7 +58,6 @@
                this.$input.on( 'change input cut paste', OO.ui.debounce( this.updateMenuItems.bind( this ), 100 ) );
 
                // Initialize
-               this.catNsId = mw.config.get( 'wgNamespaceIds' ).category;
                this.api = new mw.Api();
 
        }
 
                        // Get titles
                        categoryNames = categories.map( function ( name ) {
-                               return mw.Title.newFromText( name, this.catNsId ).getMainText();
+                               return mw.Title.newFromText( name, NS_CATEGORY ).getMainText();
                        } );
 
                        deferred.resolve( categoryNames );
                return deferred.promise();
        };
 
+       /**
+        * @inheritdoc
+        */
+       CSP.createItemWidget = function ( data ) {
+               return new mw.widgets.CategoryCapsuleItemWidget( {
+                       title: mw.Title.newFromText( data, NS_CATEGORY )
+               } );
+       };
+
        /**
         * Validates the values in `this.searchType`.
         *
                        case CategorySelector.SearchType.OpenSearch:
                                this.api.get( {
                                        action: 'opensearch',
-                                       namespace: this.catNsId,
+                                       namespace: NS_CATEGORY,
                                        limit: this.limit,
                                        search: input
                                } ).done( function ( res ) {
                                this.api.get( {
                                        action: 'query',
                                        list: 'allpages',
-                                       apnamespace: this.catNsId,
+                                       apnamespace: NS_CATEGORY,
                                        aplimit: this.limit,
                                        apfrom: input,
                                        apprefix: input