Merge "Change "userright" to "user right""
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 25 Jun 2015 10:26:02 +0000 (10:26 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 25 Jun 2015 10:26:02 +0000 (10:26 +0000)
13 files changed:
includes/api/i18n/en.json
includes/htmlform/HTMLTextAreaField.php
includes/htmlform/HTMLTextField.php
includes/page/Article.php
includes/utils/AutoloadGenerator.php
languages/i18n/en.json
languages/i18n/qqq.json
resources/Resources.php
resources/src/mediawiki.skinning/elements.css
resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.css
resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.js
resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js [new file with mode: 0644]
tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js

index 02a52b8..6d1b1ec 100644 (file)
        "apihelp-imagerotate-example-simple": "Rotate <kbd>File:Example.png</kbd> by <kbd>90</kbd> degrees.",
        "apihelp-imagerotate-example-generator": "Rotate all images in <kbd>Category:Flip</kbd> by <kbd>180</kbd> degrees.",
 
-       "apihelp-import-description": "Import a page from another wiki, or an XML file.\n\nNote that the HTTP POST must be done as a file upload (i.e. using multipart/form-data) when sending a file for the <var>xml</var> parameter.",
+       "apihelp-import-description": "Import a page from another wiki, or from an XML file.\n\nNote that the HTTP POST must be done as a file upload (i.e. using multipart/form-data) when sending a file for the <var>xml</var> parameter.",
        "apihelp-import-param-summary": "Import summary.",
        "apihelp-import-param-xml": "Uploaded XML file.",
        "apihelp-import-param-interwikisource": "For interwiki imports: wiki to import from.",
index 22e96f6..a4ed95f 100644 (file)
@@ -12,11 +12,21 @@ class HTMLTextAreaField extends HTMLFormField {
                return isset( $this->mParams['rows'] ) ? $this->mParams['rows'] : static::DEFAULT_ROWS;
        }
 
+       function getSpellCheck() {
+               $val = isset( $this->mParams['spellcheck'] ) ? $this->mParams['spellcheck'] : null;
+               if( is_bool( $val ) ) {
+                       // "spellcheck" attribute literally requires "true" or "false" to work.
+                       return $val === true ? 'true' : 'false';
+               }
+               return null;
+       }
+
        function getInputHTML( $value ) {
                $attribs = array(
                                'id' => $this->mID,
                                'cols' => $this->getCols(),
                                'rows' => $this->getRows(),
+                               'spellcheck' => $this->getSpellCheck(),
                        ) + $this->getTooltipAndAccessKey();
 
                if ( $this->mClass !== '' ) {
index 2958274..06b397f 100644 (file)
@@ -5,6 +5,15 @@ class HTMLTextField extends HTMLFormField {
                return isset( $this->mParams['size'] ) ? $this->mParams['size'] : 45;
        }
 
+       function getSpellCheck() {
+               $val = isset( $this->mParams['spellcheck'] ) ? $this->mParams['spellcheck'] : null;
+               if( is_bool( $val ) ) {
+                       // "spellcheck" attribute literally requires "true" or "false" to work.
+                       return $val === true ? 'true' : 'false';
+               }
+               return null;
+       }
+
        function getInputHTML( $value ) {
                $attribs = array(
                                'id' => $this->mID,
@@ -12,6 +21,7 @@ class HTMLTextField extends HTMLFormField {
                                'size' => $this->getSize(),
                                'value' => $value,
                                'dir' => $this->mDir,
+                               'spellcheck' => $this->getSpellCheck(),
                        ) + $this->getTooltipAndAccessKey();
 
                if ( $this->mClass !== '' ) {
index 053753e..a6b6b51 100644 (file)
@@ -1706,10 +1706,8 @@ class Article implements Page {
 
                if ( $user->isAllowed( 'suppressrevision' ) ) {
                        $suppress = Html::openElement( 'div', array( 'id' => 'wpDeleteSuppressRow' ) ) .
-                               "<strong>" .
-                                               Xml::checkLabel( wfMessage( 'revdelete-suppress' )->text(),
-                                                       'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '4' ) ) .
-                                       "</strong>" .
+                               Xml::checkLabel( wfMessage( 'revdelete-suppress' )->text(),
+                                       'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '4' ) ) .
                                Html::closeElement( 'div' );
                } else {
                        $suppress = '';
index dd1a38a..7d63156 100644 (file)
@@ -119,85 +119,88 @@ class AutoloadGenerator {
        }
 
        /**
-        * Write out all known classes to autoload.php in
-        * the provided basedir
+        * Updates the AutoloadClasses field at the given
+        * filename.
         *
-        * @param string $commandName Value used in file comment to direct
-        *  developers towards the appropriate way to update the autoload.
+        * @param {string} $filename Filename of JSON
+        *  extension/skin registration file
         */
-       public function generateAutoload( $commandName = 'AutoloadGenerator' ) {
-
-               // We need to check whether an extenson.json exists or not, and
-               // incase it doesn't, update the autoload.php file.
-
-               if ( file_exists( $this->basepath . '/extension.json' ) ) {
-                       require_once __DIR__ . '/../../includes/json/FormatJson.php';
-                       $key = 'AutoloadClasses';
-                       $json = FormatJson::decode( file_get_contents( $this->basepath
-                               . '/extension.json' ), true );
-                       unset( $json[$key] );
-                       // Inverting the key-value pairs so that they become of the
-                       // format class-name : path when they get converted into json.
-                       foreach ( $this->classes as $path => $contained ) {
-                               foreach ( $contained as $fqcn ) {
-
-                                       // Using substr to remove the leading '/'
-                                       $json[$key][$fqcn] = substr( $path, 1 );
-                               }
-                       }
-                       foreach ( $this->overrides as $path => $fqcn ) {
+       protected function generateJsonAutoload( $filename ) {
+               require_once __DIR__ . '/../../includes/json/FormatJson.php';
+               $key = 'AutoloadClasses';
+               $json = FormatJson::decode( file_get_contents( $filename ), true );
+               unset( $json[$key] );
+               // Inverting the key-value pairs so that they become of the
+               // format class-name : path when they get converted into json.
+               foreach ( $this->classes as $path => $contained ) {
+                       foreach ( $contained as $fqcn ) {
 
                                // Using substr to remove the leading '/'
                                $json[$key][$fqcn] = substr( $path, 1 );
                        }
+               }
+               foreach ( $this->overrides as $path => $fqcn ) {
 
-                       // Sorting the list of autoload classes.
-                       ksort( $json[$key] );
+                       // Using substr to remove the leading '/'
+                       $json[$key][$fqcn] = substr( $path, 1 );
+               }
 
-                       // Update extension.json, using constants for the required
-                       // formatting.
-                       file_put_contents( $this->basepath . '/extension.json',
-                               FormatJson::encode( $json, true ) . "\n" );
-               } else {
-                       $content = array();
-
-                       // We need to generate a line each rather than exporting the
-                       // full array so __DIR__ can be prepended to all the paths
-                       $format = "%s => __DIR__ . %s,";
-                       foreach ( $this->classes as $path => $contained ) {
-                               $exportedPath = var_export( $path, true );
-                               foreach ( $contained as $fqcn ) {
-                                       $content[$fqcn] = sprintf(
-                                               $format,
-                                               var_export( $fqcn, true ),
-                                               $exportedPath
-                                       );
-                               }
-                       }
+               // Sorting the list of autoload classes.
+               ksort( $json[$key] );
 
-                       foreach ( $this->overrides as $fqcn => $path ) {
+               // Update file, using constants for the required
+               // formatting.
+               file_put_contents( $filename,
+                       FormatJson::encode( $json, true ) . "\n" );
+       }
+
+       /**
+        * Generates a PHP file setting up autoload information.
+        *
+        * @param {string} $commandName Command name to include in comment
+        * @param {string} $filename of PHP file to put autoload information in.
+        */
+       protected function generatePHPAutoload( $commandName, $filename ) {
+               // No existing JSON file found; update/generate PHP file
+               $content = array();
+
+               // We need to generate a line each rather than exporting the
+               // full array so __DIR__ can be prepended to all the paths
+               $format = "%s => __DIR__ . %s,";
+               foreach ( $this->classes as $path => $contained ) {
+                       $exportedPath = var_export( $path, true );
+                       foreach ( $contained as $fqcn ) {
                                $content[$fqcn] = sprintf(
                                        $format,
                                        var_export( $fqcn, true ),
-                                       var_export( $path, true )
+                                       $exportedPath
                                );
                        }
+               }
 
-                       // sort for stable output
-                       ksort( $content );
+               foreach ( $this->overrides as $fqcn => $path ) {
+                       $content[$fqcn] = sprintf(
+                               $format,
+                               var_export( $fqcn, true ),
+                               var_export( $path, true )
+                       );
+               }
 
-                       // extensions using this generator are appending to the existing
-                       // autoload.
-                       if ( $this->variableName === 'wgAutoloadClasses' ) {
-                               $op = '+=';
-                       } else {
-                               $op = '=';
-                       }
+               // sort for stable output
+               ksort( $content );
+
+               // extensions using this generator are appending to the existing
+               // autoload.
+               if ( $this->variableName === 'wgAutoloadClasses' ) {
+                       $op = '+=';
+               } else {
+                       $op = '=';
+               }
 
-                       $output = implode( "\n\t", $content );
-                       file_put_contents(
-                               $this->basepath . '/autoload.php',
-                               <<<EOD
+               $output = implode( "\n\t", $content );
+               file_put_contents(
+                       $filename,
+                       <<<EOD
 <?php
 // This file is generated by $commandName, do not adjust manually
 // @codingStandardsIgnoreFile
@@ -208,7 +211,33 @@ global \${$this->variableName};
 );
 
 EOD
-                       );
+               );
+
+       }
+
+       /**
+        * Write out all known classes to autoload.php, extension.json, or skin.json in
+        * the provided basedir
+        *
+        * @param string $commandName Value used in file comment to direct
+        *  developers towards the appropriate way to update the autoload.
+        */
+       public function generateAutoload( $commandName = 'AutoloadGenerator' ) {
+
+               // We need to check whether an extenson.json or skin.json exists or not, and
+               // incase it doesn't, update the autoload.php file.
+
+               $jsonFilename = null;
+               if ( file_exists( $this->basepath . "/extension.json" ) ) {
+                       $jsonFilename = $this->basepath . "/extension.json";
+               } elseif ( file_exists( $this->basepath . "/skin.json" ) ) {
+                       $jsonFilename = $this->basepath . "/skin.json";
+               }
+
+               if ( $jsonFilename !== null ) {
+                       $this->generateJsonAutoload( $jsonFilename );
+               } else {
+                       $this->generatePHPAutoload( $commandName, $this->basepath . '/autoload.php' );
                }
        }
        /**
index de4119e..25ae0a2 100644 (file)
        "special-characters-group-khmer": "Khmer",
        "special-characters-title-endash": "en dash",
        "special-characters-title-emdash": "em dash",
-       "special-characters-title-minus": "minus sign"
+       "special-characters-title-minus": "minus sign",
+       "mw-widgets-titleinput-description-new-page": "page does not exist yet",
+       "mw-widgets-titleinput-description-redirect": "redirect to $1"
 }
index 9638138..7288066 100644 (file)
        "special-characters-group-khmer": "{{Identical|Khmer}}",
        "special-characters-title-endash": "Title tooltip for the en dash character (–); See https://en.wikipedia.org/wiki/Dash",
        "special-characters-title-emdash": "Title tooltip for the em dash character (—); See https://en.wikipedia.org/wiki/Dash",
-       "special-characters-title-minus": "Title tooltip for the minus sign character (−), not to be confused with a hyphen"
+       "special-characters-title-minus": "Title tooltip for the minus sign character (−), not to be confused with a hyphen",
+       "mw-widgets-titleinput-description-new-page": "Description label for a new page in the title input widget.",
+       "mw-widgets-titleinput-description-redirect": "Description label for a redirect in the title input widget."
 }
index d75c8a1..f7a0531 100644 (file)
@@ -1735,6 +1735,7 @@ return array(
                'scripts' => array(
                        'resources/src/mediawiki.widgets/mw.widgets.js',
                        'resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.js',
+                       'resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js',
                ),
                'skinStyles' => array(
                        'default' => 'resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.css',
@@ -1745,7 +1746,8 @@ return array(
                        'oojs-ui',
                ),
                'messages' => array(
-                       // â€¦
+                       'mw-widgets-titleinput-description-new-page',
+                       'mw-widgets-titleinput-description-redirect',
                ),
                'targets' => array( 'desktop', 'mobile' ),
        ),
index 9c59fc1..6986034 100644 (file)
@@ -199,6 +199,11 @@ pre, .mw-code {
        background-color: #f9f9f9;
        border: 1px solid #ddd;
        padding: 1em;
+       /* @noflip */
+       direction: ltr;
+       unicode-bidi: embed;
+       /* Wrap lines in overflow. T103780 */
+       white-space: pre-wrap;
 }
 
 /* Tables */
index 0065f70..2c24b2b 100644 (file)
@@ -1,10 +1,57 @@
 /*!
- * MediaWiki Widgets â€“ TitleInputWidget styles.
+ * MediaWiki Widgets - TitleInputWidget styles.
  *
  * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
  * @license The MIT License (MIT); see LICENSE.txt
  */
 
-.mw-widget-TitleInputWidget {
-       width: 30em;
+.mw-widget-titleInputWidget-menu-withImages .mw-widget-titleOptionWidget {
+       -webkit-box-sizing: border-box;
+       -moz-box-sizing: border-box;
+       box-sizing: border-box;
+       min-height: 3.75em;
+       margin-left: 3.75em;
+}
+
+.mw-widget-titleInputWidget-menu-withImages .mw-widget-titleOptionWidget:not(:last-child) {
+       margin-bottom: 1px;
+}
+
+.mw-widget-titleInputWidget-menu-withImages .oo-ui-iconElement .oo-ui-iconElement-icon {
+       display: block;
+       width: 3.75em;
+       height: 3.75em;
+       left: -3.75em;
+       background-color: #ccc;
+       opacity: 0.4;
+}
+
+.mw-widget-titleInputWidget-menu-withImages .oo-ui-iconElement .mw-widget-titleOptionWidget-hasImage {
+       border: 0;
+       background-size: cover;
+       opacity: 1;
+}
+
+.mw-widget-titleInputWidget-menu-withImages .mw-widget-titleOptionWidget .oo-ui-labelElement-label {
+       line-height: 2.8em;
+}
+
+.mw-widget-titleOptionWidget-description {
+       display: none;
+}
+
+.mw-widget-titleInputWidget-menu-withDescriptions .mw-widget-titleOptionWidget .oo-ui-labelElement-label {
+       line-height: 1.5em;
+}
+
+.mw-widget-titleInputWidget-menu-withDescriptions .mw-widget-titleOptionWidget-description {
+       display: block;
+       white-space: nowrap;
+       text-overflow: ellipsis;
+       overflow: hidden;
+}
+
+.oo-ui-menuOptionWidget:not(.oo-ui-optionWidget-selected) .mw-widget-titleOptionWidget-description,
+.oo-ui-menuOptionWidget.oo-ui-optionWidget-highlighted .mw-widget-titleOptionWidget-description {
+       color: #888;
 }
index 26b2f5d..221de0f 100644 (file)
@@ -1,5 +1,5 @@
 /*!
- * MediaWiki Widgets â€“ TitleInputWidget class.
+ * MediaWiki Widgets - TitleInputWidget class.
  *
  * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
  * @license The MIT License (MIT); see LICENSE.txt
         *
         * @constructor
         * @param {Object} [config] Configuration options
+        * @cfg {number} [limit=10] Number of results to show
         * @cfg {number} [namespace] Namespace to prepend to queries
+        * @cfg {boolean} [showRedirectTargets=true] Show the targets of redirects
+        * @cfg {boolean} [showRedlink] Show red link to exact match if it doesn't exist
+        * @cfg {boolean} [showImages] Show page images
+        * @cfg {boolean} [showDescriptions] Show page descriptions
+        * @cfg {Object} [cache] Result cache which implements a 'set' method, taking keyed values as an argument
         */
-       mw.widgets.TitleInputWidget = function MWWTitleInputWidget( config ) {
+       mw.widgets.TitleInputWidget = function MwWidgetsTitleInputWidget( config ) {
+               var widget = this;
+
                // Config initialization
                config = config || {};
 
                OO.ui.mixin.LookupElement.call( this, config );
 
                // Properties
+               this.limit = config.limit || 10;
                this.namespace = config.namespace || null;
+               this.showRedirectTargets = config.showRedirectTargets !== false;
+               this.showRedlink = !!config.showRedlink;
+               this.showImages = !!config.showImages;
+               this.showDescriptions = !!config.showDescriptions;
+               this.cache = config.cache;
 
                // Initialization
-               this.$element.addClass( 'mw-widget-TitleInputWidget' );
-               this.lookupMenu.$element.addClass( 'mw-widget-TitleInputWidget-menu' );
+               this.$element.addClass( 'mw-widget-titleInputWidget' );
+               this.lookupMenu.$element.addClass( 'mw-widget-titleInputWidget-menu' );
+               if ( this.showImages ) {
+                       this.lookupMenu.$element.addClass( 'mw-widget-titleInputWidget-menu-withImages' );
+               }
+               if ( this.showDescriptions ) {
+                       this.lookupMenu.$element.addClass( 'mw-widget-titleInputWidget-menu-withDescriptions' );
+               }
+
+               this.interwikiPrefixes = [];
+               this.interwikiPrefixesPromise = new mw.Api().get( {
+                       action: 'query',
+                       meta: 'siteinfo',
+                       siprop: 'interwikimap'
+               } ).done( function ( data ) {
+                       $.each( data.query.interwikimap, function ( index, interwiki ) {
+                               widget.interwikiPrefixes.push( interwiki.prefix );
+                       } );
+               } );
        };
 
        /* Inheritance */
        /**
         * @inheritdoc
         */
-       mw.widgets.TitleInputWidget.prototype.getLookupRequest = function () {
-               var value = this.value;
+       mw.widgets.TitleInputWidget.prototype.focus = function () {
+               var retval;
 
-               // Prefix with default namespace name
-               if ( this.namespace !== null && mw.Title.newFromText( value, this.namespace ) ) {
-                       value = mw.Title.newFromText( value, this.namespace ).getPrefixedText();
-               }
+               // Prevent programmatic focus from opening the menu
+               this.setLookupsDisabled( true );
 
-               // Dont send leading ':' to open search
-               if ( value[0] === ':' ) {
-                       value = value.slice( 1 );
-               }
+               // Parent method
+               retval = OO.ui.TextInputWidget.prototype.focus.apply( this, arguments );
 
-               return new mw.Api().get( {
-                       action: 'opensearch',
-                       search: value,
-                       suggest: ''
-               } );
+               this.setLookupsDisabled( false );
+
+               return retval;
        };
 
        /**
         * @inheritdoc
         */
+       mw.widgets.TitleInputWidget.prototype.getLookupRequest = function () {
+               var req,
+                       widget = this,
+                       promiseAbortObject = { abort: function () {
+                               // Do nothing. This is just so OOUI doesn't break due to abort being undefined.
+                       } };
+
+               if ( mw.Title.newFromText( this.value ) ) {
+                       return this.interwikiPrefixesPromise.then( function () {
+                               var params, props,
+                                       interwiki = widget.value.substring( 0, widget.value.indexOf( ':' ) );
+                               if (
+                                       interwiki && interwiki !== '' &&
+                                       widget.interwikiPrefixes.indexOf( interwiki ) !== -1
+                               ) {
+                                       return $.Deferred().resolve( { query: {
+                                               pages: [{
+                                                       title: widget.value
+                                               }]
+                                       } } ).promise( promiseAbortObject );
+                               } else {
+                                       params = {
+                                               action: 'query',
+                                               generator: 'prefixsearch',
+                                               gpssearch: widget.value,
+                                               gpsnamespace: widget.namespace !== null ? widget.namespace : undefined,
+                                               gpslimit: widget.limit,
+                                               ppprop: 'disambiguation'
+                                       };
+                                       props = [ 'info', 'pageprops' ];
+                                       if ( widget.showRedirectTargets ) {
+                                               params.redirects = '1';
+                                       }
+                                       if ( widget.showImages ) {
+                                               props.push( 'pageimages' );
+                                               params.pithumbsize = 80;
+                                               params.pilimit = widget.limit;
+                                       }
+                                       if ( widget.showDescriptions ) {
+                                               props.push( 'pageterms' );
+                                               params.wbptterms = 'description';
+                                       }
+                                       params.prop = props.join( '|' );
+                                       req = new mw.Api().get( params );
+                                       promiseAbortObject.abort = req.abort.bind( req ); // todo: ew
+                                       return req;
+                               }
+                       } ).promise( promiseAbortObject );
+               } else {
+                       // Don't send invalid titles to the API.
+                       // Just pretend it returned nothing so we can show the 'invalid title' section
+                       return $.Deferred().resolve( {} ).promise( promiseAbortObject );
+               }
+       };
+
+       /**
+        * Get lookup cache item from server response data.
+        *
+        * @method
+        * @param {Mixed} data Response from server
+        */
        mw.widgets.TitleInputWidget.prototype.getLookupCacheDataFromResponse = function ( data ) {
-               return data[1] || [];
+               return data.query || {};
        };
 
        /**
-        * @inheritdoc
+        * Get list of menu items from a server response.
+        *
+        * @param {Object} data Query result
+        * @returns {OO.ui.MenuOptionWidget[]} Menu items
         */
        mw.widgets.TitleInputWidget.prototype.getLookupMenuOptionsFromData = function ( data ) {
-               var i, len, title, value,
+               var i, len, index, pageExists, pageExistsExact, suggestionPage, page, redirect, redirects,
                        items = [],
-                       matchingPages = data;
-
-               // Matching pages
-               if ( matchingPages && matchingPages.length ) {
-                       for ( i = 0, len = matchingPages.length; i < len; i++ ) {
-                               title = new mw.Title( matchingPages[i] );
-                               if ( this.namespace !== null ) {
-                                       value = title.getRelativeText( this.namespace );
-                               } else {
-                                       value = title.getPrefixedText();
-                               }
-                               items.push( new OO.ui.MenuOptionWidget( {
-                                       data: value,
-                                       label: value
-                               } ) );
+                       titles = [],
+                       titleObj = mw.Title.newFromText( this.value ),
+                       redirectsTo = {},
+                       pageData = {};
+
+               if ( data.redirects ) {
+                       for ( i = 0, len = data.redirects.length; i < len; i++ ) {
+                               redirect = data.redirects[i];
+                               redirectsTo[redirect.to] = redirectsTo[redirect.to] || [];
+                               redirectsTo[redirect.to].push( redirect.from );
+                       }
+               }
+
+               for ( index in data.pages ) {
+                       suggestionPage = data.pages[index];
+                       pageData[suggestionPage.title] = {
+                               missing: suggestionPage.missing !== undefined,
+                               redirect: suggestionPage.redirect !== undefined,
+                               disambiguation: OO.getProp( suggestionPage, 'pageprops', 'disambiguation' ) !== undefined,
+                               imageUrl: OO.getProp( suggestionPage, 'thumbnail', 'source' ),
+                               description: OO.getProp( suggestionPage, 'terms', 'description' )
+                       };
+                       titles.push( suggestionPage.title );
+
+                       redirects = redirectsTo[suggestionPage.title] || [];
+                       for ( i = 0, len = redirects.length; i < len; i++ ) {
+                               pageData[redirects[i]] = {
+                                       missing: false,
+                                       redirect: true,
+                                       disambiguation: false,
+                                       description: mw.msg( 'mw-widgets-titleinput-description-redirect', suggestionPage.title )
+                               };
+                               titles.push( redirects[i] );
                        }
                }
 
+               // If not found, run value through mw.Title to avoid treating a match as a
+               // mismatch where normalisation would make them matching (bug 48476)
+
+               pageExistsExact = titles.indexOf( this.value ) !== -1;
+               pageExists = pageExistsExact || (
+                       titleObj && titles.indexOf( titleObj.getPrefixedText() ) !== -1
+               );
+
+               if ( !pageExists ) {
+                       pageData[this.value] = {
+                               missing: true, redirect: false, disambiguation: false,
+                               description: mw.msg( 'mw-widgets-titleinput-description-new-page' )
+                       };
+               }
+
+               if ( this.cache ) {
+                       this.cache.set( pageData );
+               }
+
+               // Offer the exact text as a suggestion if the page exists
+               if ( pageExists && !pageExistsExact ) {
+                       titles.unshift( this.value );
+               }
+               // Offer the exact text as a new page if the title is valid
+               if ( this.showRedlink && !pageExists && titleObj ) {
+                       titles.push( this.value );
+               }
+               for ( i = 0, len = titles.length; i < len; i++ ) {
+                       page = pageData[titles[i]] || {};
+                       items.push( new mw.widgets.TitleOptionWidget( this.getOptionWidgetData( titles[i], page ) ) );
+               }
+
                return items;
        };
 
+       /**
+        * Get menu option widget data from the title and page data
+        *
+        * @param {mw.Title} title Title object
+        * @param {Object} data Page data
+        * @return {Object} Data for option widget
+        */
+       mw.widgets.TitleInputWidget.prototype.getOptionWidgetData = function ( title, data ) {
+               var mwTitle = new mw.Title( title );
+               return {
+                       data: this.namespace !== null ? mwTitle.getRelativeText( this.namespace ) : title,
+                       imageUrl: this.showImages ? data.imageUrl : null,
+                       description: this.showDescriptions ? data.description : null,
+                       missing: data.missing,
+                       redirect: data.redirect,
+                       disambiguation: data.disambiguation,
+                       query: this.value
+               };
+       };
+
        /**
         * Get title object corresponding to #getValue
         *
diff --git a/resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js b/resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js
new file mode 100644 (file)
index 0000000..07b81e4
--- /dev/null
@@ -0,0 +1,81 @@
+/*!
+ * MediaWiki Widgets - TitleOptionWidget class.
+ *
+ * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+( function ( $, mw ) {
+
+       /**
+        * Creates a mw.widgets.TitleOptionWidget object.
+        *
+        * @class
+        * @extends OO.ui.MenuOptionWidget
+        *
+        * @constructor
+        * @param {Object} [config] Configuration options
+        * @cfg {string} [data] Page title
+        * @cfg {string} [imageUrl] Thumbnail image URL with URL encoding
+        * @cfg {string} [description] Page description
+        * @cfg {boolean} [missing] Page doesn't exist
+        * @cfg {boolean} [redirect] Page is a redirect
+        * @cfg {boolean} [disambiguation] Page is a disambiguation page
+        * @cfg {string} [query] Matching query string
+        */
+       mw.widgets.TitleOptionWidget = function MwWidgetsTitleOptionWidget( config ) {
+               var icon, title = config.data;
+
+               if ( config.missing ) {
+                       icon = 'page-not-found';
+               } else if ( config.redirect ) {
+                       icon = 'page-redirect';
+               } else if ( config.disambiguation ) {
+                       icon = 'page-disambiguation';
+               } else {
+                       icon = 'page-existing';
+               }
+
+               // Config initialization
+               config = $.extend( {
+                       icon: icon,
+                       label: title,
+                       href: mw.util.getUrl( title ),
+                       autoFitLabel: false
+               }, config );
+
+               // Parent constructor
+               OO.ui.MenuOptionWidget.call( this, config );
+
+               // Intialization
+               this.$label.wrap( '<a>' );
+               this.$link = this.$label.parent();
+               this.$link.attr( 'href', config.href );
+               this.$element.addClass( 'mw-widget-titleOptionWidget' );
+
+               // Highlight matching parts of link suggestion
+               this.$label.autoEllipsis( { hasSpan: false, tooltip: true, matchText: config.query } );
+
+               if ( config.missing ) {
+                       this.$link.addClass( 'new' );
+               }
+
+               if ( config.imageUrl ) {
+                       this.$icon
+                               .addClass( 'mw-widget-titleOptionWidget-hasImage' )
+                               .css( 'background-image', 'url(' + config.imageUrl + ')' );
+               }
+
+               if ( config.description ) {
+                       this.$element.append(
+                               $( '<span>' )
+                                       .addClass( 'mw-widget-titleOptionWidget-description' )
+                                       .text( config.description )
+                       );
+               }
+       };
+
+       /* Inheritance */
+
+       OO.inheritClass( mw.widgets.TitleOptionWidget, OO.ui.MenuOptionWidget );
+
+}( jQuery, mediaWiki ) );
index 0b42af4..8a3e100 100644 (file)
                        'ResourceLoader', 't-rl', 'More info about ResourceLoader on MediaWiki.org ', 'l'
                );
 
-               assert.ok( $.isDomElement( tbRL ), 'addPortletLink returns a valid DOM Element according to $.isDomElement' );
+               assert.ok( tbRL && tbRL.nodeType, 'addPortletLink returns a DOM Node' );
 
                tbMW = mw.util.addPortletLink( 'p-test-tb', '//mediawiki.org/',
                        'MediaWiki.org', 't-mworg', 'Go to MediaWiki.org', 'm', tbRL );