From 8aeef44d7d5e162c1516b6eb215beead90d91241 Mon Sep 17 00:00:00 2001 From: Ed Sanders Date: Fri, 25 Sep 2015 21:39:01 +0100 Subject: [PATCH] mediawiki.widgets: Create TitleSearchWidget TitleSearchWidget inherits from SearchWidget instead of TextInputWidget, showing search results even when blurred. To avoid duplication, much of the logic from TitleInputWidget has been moved into the TitleWidget mixin. Bug: T101169 Change-Id: I335bd912d4f5139646ba05a6a85a0d9ff3d772fa --- resources/Resources.php | 4 +- .../mw.widgets.TitleInputWidget.css | 57 ---- .../mw.widgets.TitleInputWidget.js | 288 +++--------------- .../mw.widgets.TitleOptionWidget.js | 5 + .../mw.widgets.TitleSearchWidget.js | 83 +++++ .../mw.widgets.TitleWidget.js | 285 +++++++++++++++++ .../mw.widgets.TitleWidget.less | 63 ++++ 7 files changed, 481 insertions(+), 304 deletions(-) delete mode 100644 resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.css create mode 100644 resources/src/mediawiki.widgets/mw.widgets.TitleSearchWidget.js create mode 100644 resources/src/mediawiki.widgets/mw.widgets.TitleWidget.js create mode 100644 resources/src/mediawiki.widgets/mw.widgets.TitleWidget.less diff --git a/resources/Resources.php b/resources/Resources.php index b35ebadc1c..6aeb206002 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -1893,7 +1893,9 @@ return array( 'resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.js', 'resources/src/mediawiki.widgets/mw.widgets.NamespaceInputWidget.js', 'resources/src/mediawiki.widgets/mw.widgets.ComplexNamespaceInputWidget.js', + 'resources/src/mediawiki.widgets/mw.widgets.TitleWidget.js', 'resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.js', + 'resources/src/mediawiki.widgets/mw.widgets.TitleSearchWidget.js', 'resources/src/mediawiki.widgets/mw.widgets.ComplexTitleInputWidget.js', 'resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js', 'resources/src/mediawiki.widgets/mw.widgets.UserInputWidget.js', @@ -1903,7 +1905,7 @@ return array( 'default' => array( 'resources/src/mediawiki.widgets/mw.widgets.CalendarWidget.less', 'resources/src/mediawiki.widgets/mw.widgets.DateInputWidget.less', - 'resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.css', + 'resources/src/mediawiki.widgets/mw.widgets.TitleWidget.less', ), ), 'dependencies' => array( diff --git a/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.css b/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.css deleted file mode 100644 index 2c24b2bbab..0000000000 --- a/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.css +++ /dev/null @@ -1,57 +0,0 @@ -/*! - * 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-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; -} diff --git a/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.js b/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.js index d5a7abc68f..fc1007e450 100644 --- a/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.js +++ b/resources/src/mediawiki.widgets/mw.widgets.TitleInputWidget.js @@ -11,97 +11,87 @@ * * @class * @extends OO.ui.TextInputWidget + * @mixins mw.widgets.TitleWidget * @mixins OO.ui.mixin.LookupElement * * @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} [relative=true] If a namespace is set, return a title relative to it * @cfg {boolean} [suggestions=true] Display search suggestions - * @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 MwWidgetsTitleInputWidget( config ) { - var widget = this; - - // Config initialization - config = $.extend( { - maxLength: 255, - limit: 10 - }, config ); + config = config || {}; // Parent constructor - mw.widgets.TitleInputWidget.parent.call( this, $.extend( {}, config, { autocomplete: false } ) ); + mw.widgets.TitleInputWidget.parent.call( this, $.extend( {}, config, { + validate: this.isQueryValid.bind( this ), + autocomplete: false + } ) ); // Mixin constructors + mw.widgets.TitleWidget.call( this, config ); OO.ui.mixin.LookupElement.call( this, config ); // Properties - this.limit = config.limit; - this.maxLength = config.maxLength; - this.namespace = config.namespace !== undefined ? config.namespace : null; - this.relative = config.relative !== undefined ? config.relative : true; this.suggestions = config.suggestions !== undefined ? config.suggestions : true; - 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.lookupMenu.$element.addClass( 'mw-widget-titleWidget-menu' ); if ( this.showImages ) { - this.lookupMenu.$element.addClass( 'mw-widget-titleInputWidget-menu-withImages' ); + this.lookupMenu.$element.addClass( 'mw-widget-titleWidget-menu-withImages' ); } if ( this.showDescriptions ) { - this.lookupMenu.$element.addClass( 'mw-widget-titleInputWidget-menu-withDescriptions' ); + this.lookupMenu.$element.addClass( 'mw-widget-titleWidget-menu-withDescriptions' ); } this.setLookupsDisabled( !this.suggestions ); - - 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 ); - } ); - } ); }; /* Setup */ OO.inheritClass( mw.widgets.TitleInputWidget, OO.ui.TextInputWidget ); + OO.mixinClass( mw.widgets.TitleInputWidget, mw.widgets.TitleWidget ); OO.mixinClass( mw.widgets.TitleInputWidget, OO.ui.mixin.LookupElement ); /* Methods */ /** - * Get the namespace to prepend to titles in suggestions, if any. - * - * @return {number|null} Namespace number + * @inheritdoc mw.widgets.TitleWidget */ - mw.widgets.TitleInputWidget.prototype.getNamespace = function () { - return this.namespace; + mw.widgets.TitleInputWidget.prototype.getQueryValue = function () { + return this.getValue(); }; /** - * Set the namespace to prepend to titles in suggestions, if any. - * - * @param {number|null} namespace Namespace number + * @inheritdoc mw.widgets.TitleWidget */ mw.widgets.TitleInputWidget.prototype.setNamespace = function ( namespace ) { - this.namespace = namespace; + // Mixin method + mw.widgets.TitleWidget.prototype.setNamespace.call( this, namespace ); + this.lookupCache = {}; this.closeLookupMenu(); }; + /** + * @inheritdoc + */ + mw.widgets.TitleInputWidget.prototype.getLookupRequest = function () { + return this.getSuggestionsPromise(); + }; + + /** + * @inheritdoc OO.ui.mixin.LookupElement + */ + mw.widgets.TitleInputWidget.prototype.getLookupCacheDataFromResponse = function ( response ) { + return response.query || {}; + }; + + /** + * @inheritdoc OO.ui.mixin.LookupElement + */ + mw.widgets.TitleInputWidget.prototype.getLookupMenuOptionsFromData = function ( response ) { + return this.getOptionsFromData( response ); + }; + /** * @inheritdoc */ @@ -129,213 +119,19 @@ 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} response Response from server - */ - mw.widgets.TitleInputWidget.prototype.getLookupCacheDataFromResponse = function ( response ) { - return response.query || {}; - }; - - /** - * 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, index, pageExists, pageExistsExact, suggestionPage, page, redirect, redirects, - items = [], - 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' ) - }; - - // Throw away pages from wrong namespaces. This can happen when 'showRedirectTargets' is true - // and we encounter a cross-namespace redirect. - if ( this.namespace === null || this.namespace === suggestionPage.ns ) { - 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 && this.relative - ? mwTitle.getRelativeText( this.namespace ) - : title, - title: mwTitle, - 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 given value, or #getValue if not given. - * - * @param {string} [value] Value to get a title for - * @returns {mw.Title|null} Title object, or null if value is invalid - */ - mw.widgets.TitleInputWidget.prototype.getTitle = function ( value ) { - var title = value !== undefined ? value : this.getValue(), - // mw.Title doesn't handle null well - titleObj = mw.Title.newFromText( title, this.namespace !== null ? this.namespace : undefined ); - - return titleObj; - }; - /** * @inheritdoc */ mw.widgets.TitleInputWidget.prototype.cleanUpValue = function ( value ) { var widget = this; + + // Parent method value = mw.widgets.TitleInputWidget.parent.prototype.cleanUpValue.call( this, value ); + return $.trimByteLength( this.value, value, this.maxLength, function ( value ) { var title = widget.getTitle( value ); return title ? title.getMain() : value; } ).newVal; }; - /** - * @inheritdoc - */ - mw.widgets.TitleInputWidget.prototype.isValid = function () { - return $.Deferred().resolve( !!this.getTitle() ).promise(); - }; - }( jQuery, mediaWiki ) ); diff --git a/resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js b/resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js index ec0c935747..1154c9f96f 100644 --- a/resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js +++ b/resources/src/mediawiki.widgets/mw.widgets.TitleOptionWidget.js @@ -73,6 +73,11 @@ .text( config.description ) ); } + + // Events + this.$link.on( 'click', function () { + return false; + } ); }; /* Setup */ diff --git a/resources/src/mediawiki.widgets/mw.widgets.TitleSearchWidget.js b/resources/src/mediawiki.widgets/mw.widgets.TitleSearchWidget.js new file mode 100644 index 0000000000..0e2546f8b7 --- /dev/null +++ b/resources/src/mediawiki.widgets/mw.widgets.TitleSearchWidget.js @@ -0,0 +1,83 @@ +/*! + * MediaWiki Widgets - TitleSearchWidget class. + * + * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ +( function ( $, mw ) { + + /** + * Creates an mw.widgets.TitleSearchWidget object. + * + * @class + * @extends OO.ui.SearchWidget + * @mixins mw.widgets.TitleWidget + * + * @constructor + */ + mw.widgets.TitleSearchWidget = function MwWidgetsTitleSearchWidget( config ) { + config = config || {}; + + // Parent constructor + mw.widgets.TitleSearchWidget.parent.call( this, config ); + + // Mixin constructors + mw.widgets.TitleWidget.call( this, config ); + + this.query.setValidation( this.isQueryValid.bind( this ) ); + + // Events + this.results.connect( this, { choose: 'onTitleSearchResultsChoose' } ); + + // Initialization + this.$element.addClass( 'mw-widget-titleSearchWidget' ); + this.results.$element.addClass( 'mw-widget-titleWidget-menu' ); + if ( this.showImages ) { + this.results.$element.addClass( 'mw-widget-titleWidget-menu-withImages' ); + } + if ( this.showDescriptions ) { + this.results.$element.addClass( 'mw-widget-titleWidget-menu-withDescriptions' ); + } + if ( this.maxLength !== undefined ) { + this.getQuery().$input.attr( 'maxlength', this.maxLength ); + } + }; + + /* Setup */ + + OO.inheritClass( mw.widgets.TitleSearchWidget, OO.ui.SearchWidget ); + OO.mixinClass( mw.widgets.TitleSearchWidget, mw.widgets.TitleWidget ); + + /* Methods */ + + /** + * @inheritdoc mw.widgets.TitleWidget + */ + mw.widgets.TitleSearchWidget.prototype.getQueryValue = function () { + return this.getQuery().getValue(); + }; + + /** + * Handle choose events from the result widget + * + * @param {OO.ui.OptionWidget} item Chosen item + */ + mw.widgets.TitleSearchWidget.prototype.onTitleSearchResultsChoose = function ( item ) { + this.getQuery().setValue( item.getData() ); + }; + + /** + * @inheritdoc + */ + mw.widgets.TitleSearchWidget.prototype.onQueryChange = function () { + var widget = this; + + this.getSuggestionsPromise().done( function ( response ) { + // Parent method + mw.widgets.TitleSearchWidget.parent.prototype.onQueryChange.call( widget ); + + widget.results.addItems( widget.getOptionsFromData( response.query || {} ) ); + } ); + }; + +}( jQuery, mediaWiki ) ); diff --git a/resources/src/mediawiki.widgets/mw.widgets.TitleWidget.js b/resources/src/mediawiki.widgets/mw.widgets.TitleWidget.js new file mode 100644 index 0000000000..f51c559d4c --- /dev/null +++ b/resources/src/mediawiki.widgets/mw.widgets.TitleWidget.js @@ -0,0 +1,285 @@ +/*! + * MediaWiki Widgets - TitleWidget class. + * + * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ +( function ( $, mw ) { + + /** + * Mixin for title widgets + * + * @class + * @abstract + * + * @constructor + * @param {Object} [config] Configuration options + * @cfg {number} [limit=10] Number of results to show + * @cfg {number} [namespace] Namespace to prepend to queries + * @cfg {number} [maxLength=255] Maximum query length + * @cfg {boolean} [relative=true] If a namespace is set, return a title relative to it + * @cfg {boolean} [suggestions=true] Display search suggestions + * @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.TitleWidget = function MwWidgetsTitleWidget( config ) { + var widget = this; + + // Config initialization + config = $.extend( { + maxLength: 255, + limit: 10 + }, config ); + + // Properties + this.limit = config.limit; + this.maxLength = config.maxLength; + this.namespace = config.namespace !== undefined ? config.namespace : null; + this.relative = config.relative !== undefined ? config.relative : true; + this.suggestions = config.suggestions !== undefined ? config.suggestions : true; + this.showRedirectTargets = config.showRedirectTargets !== false; + this.showRedlink = !!config.showRedlink; + this.showImages = !!config.showImages; + this.showDescriptions = !!config.showDescriptions; + this.cache = config.cache; + + // Initialization + 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 ); + } ); + } ); + }; + + /* Setup */ + + OO.initClass( mw.widgets.TitleWidget ); + + /* Methods */ + + /** + * Get the current value of the search query + * + * @abstract + * @return {string} Search query + */ + mw.widgets.TitleWidget.prototype.getQueryValue = null; + + /** + * Get the namespace to prepend to titles in suggestions, if any. + * + * @return {number|null} Namespace number + */ + mw.widgets.TitleWidget.prototype.getNamespace = function () { + return this.namespace; + }; + + /** + * Set the namespace to prepend to titles in suggestions, if any. + * + * @param {number|null} namespace Namespace number + */ + mw.widgets.TitleWidget.prototype.setNamespace = function ( namespace ) { + this.namespace = namespace; + }; + + /** + * Get a promise which resolves with an API repsonse for suggested + * links for the current query. + */ + mw.widgets.TitleWidget.prototype.getSuggestionsPromise = function () { + var req, + query = this.getQueryValue(), + widget = this, + promiseAbortObject = { abort: function () { + // Do nothing. This is just so OOUI doesn't break due to abort being undefined. + } }; + + if ( mw.Title.newFromText( query ) ) { + return this.interwikiPrefixesPromise.then( function () { + var params, props, + interwiki = query.substring( 0, query.indexOf( ':' ) ); + if ( + interwiki && interwiki !== '' && + widget.interwikiPrefixes.indexOf( interwiki ) !== -1 + ) { + return $.Deferred().resolve( { query: { + pages: [ { + title: query + } ] + } } ).promise( promiseAbortObject ); + } else { + params = { + action: 'query', + generator: 'prefixsearch', + gpssearch: query, + 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 option widgets from the server response + * + * @param {Object} data Query result + * @returns {OO.ui.OptionWidget[]} Menu items + */ + mw.widgets.TitleWidget.prototype.getOptionsFromData = function ( data ) { + var i, len, index, pageExists, pageExistsExact, suggestionPage, page, redirect, redirects, + items = [], + titles = [], + titleObj = mw.Title.newFromText( this.getQueryValue() ), + 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' ) + }; + + // Throw away pages from wrong namespaces. This can happen when 'showRedirectTargets' is true + // and we encounter a cross-namespace redirect. + if ( this.namespace === null || this.namespace === suggestionPage.ns ) { + 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.getQueryValue() ) !== -1; + pageExists = pageExistsExact || ( + titleObj && titles.indexOf( titleObj.getPrefixedText() ) !== -1 + ); + + if ( !pageExists ) { + pageData[ this.getQueryValue() ] = { + 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.getQueryValue() ); + } + // Offer the exact text as a new page if the title is valid + if ( this.showRedlink && !pageExists && titleObj ) { + titles.push( this.getQueryValue() ); + } + 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.TitleWidget.prototype.getOptionWidgetData = function ( title, data ) { + var mwTitle = new mw.Title( title ); + return { + data: this.namespace !== null && this.relative + ? mwTitle.getRelativeText( this.namespace ) + : title, + title: mwTitle, + imageUrl: this.showImages ? data.imageUrl : null, + description: this.showDescriptions ? data.description : null, + missing: data.missing, + redirect: data.redirect, + disambiguation: data.disambiguation, + query: this.getQueryValue() + }; + }; + + /** + * Get title object corresponding to given value, or #getQueryValue if not given. + * + * @param {string} [value] Value to get a title for + * @returns {mw.Title|null} Title object, or null if value is invalid + */ + mw.widgets.TitleWidget.prototype.getTitle = function ( value ) { + var title = value !== undefined ? value : this.getQueryValue(), + // mw.Title doesn't handle null well + titleObj = mw.Title.newFromText( title, this.namespace !== null ? this.namespace : undefined ); + + return titleObj; + }; + + /** + * Check if the query is valid + * + * @return {boolean} The query is valid + */ + mw.widgets.TitleWidget.prototype.isQueryValid = function () { + return !!this.getTitle(); + }; + +}( jQuery, mediaWiki ) ); diff --git a/resources/src/mediawiki.widgets/mw.widgets.TitleWidget.less b/resources/src/mediawiki.widgets/mw.widgets.TitleWidget.less new file mode 100644 index 0000000000..93c4b20ea0 --- /dev/null +++ b/resources/src/mediawiki.widgets/mw.widgets.TitleWidget.less @@ -0,0 +1,63 @@ +/*! + * MediaWiki Widgets - TitleWidget styles. + * + * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ + +.mw-widget-titleOptionWidget-description { + display: none; +} + +.mw-widget-titleWidget { + &-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-titleOptionWidget:not(:last-child) { + margin-bottom: 1px; + } + + .oo-ui-iconElement .oo-ui-iconElement-icon { + display: block; + width: 3.75em; + height: 3.75em; + left: -3.75em; + background-color: #ccc; + opacity: 0.4; + } + + .oo-ui-iconElement .mw-widget-titleOptionWidget-hasImage { + border: 0; + background-size: cover; + opacity: 1; + } + + .mw-widget-titleOptionWidget .oo-ui-labelElement-label { + line-height: 2.8em; + } + } + + &-menu-withDescriptions { + .mw-widget-titleOptionWidget .oo-ui-labelElement-label { + line-height: 1.5em; + } + + .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; +} -- 2.20.1