2 * MediaWiki Widgets - TitleInputWidget class.
4 * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
5 * @license The MIT License (MIT); see LICENSE.txt
10 * Creates an mw.widgets.TitleInputWidget object.
13 * @extends OO.ui.TextInputWidget
14 * @mixins OO.ui.mixin.LookupElement
17 * @param {Object} [config] Configuration options
18 * @cfg {number} [limit=10] Number of results to show
19 * @cfg {number} [namespace] Namespace to prepend to queries
20 * @cfg {boolean} [relative=true] If a namespace is set, return a title relative to it
21 * @cfg {boolean} [suggestions=true] Display search suggestions
22 * @cfg {boolean} [showRedirectTargets=true] Show the targets of redirects
23 * @cfg {boolean} [showRedlink] Show red link to exact match if it doesn't exist
24 * @cfg {boolean} [showImages] Show page images
25 * @cfg {boolean} [showDescriptions] Show page descriptions
26 * @cfg {Object} [cache] Result cache which implements a 'set' method, taking keyed values as an argument
28 mw
.widgets
.TitleInputWidget
= function MwWidgetsTitleInputWidget( config
) {
31 // Config initialization
38 mw
.widgets
.TitleInputWidget
.parent
.call( this, $.extend( {}, config
, { autocomplete
: false } ) );
41 OO
.ui
.mixin
.LookupElement
.call( this, config
);
44 this.limit
= config
.limit
;
45 this.maxLength
= config
.maxLength
;
46 this.namespace = config
.namespace !== undefined ? config
.namespace : null;
47 this.relative
= config
.relative
!== undefined ? config
.relative
: true;
48 this.suggestions
= config
.suggestions
!== undefined ? config
.suggestions
: true;
49 this.showRedirectTargets
= config
.showRedirectTargets
!== false;
50 this.showRedlink
= !!config
.showRedlink
;
51 this.showImages
= !!config
.showImages
;
52 this.showDescriptions
= !!config
.showDescriptions
;
53 this.cache
= config
.cache
;
56 this.$element
.addClass( 'mw-widget-titleInputWidget' );
57 this.lookupMenu
.$element
.addClass( 'mw-widget-titleInputWidget-menu' );
58 if ( this.showImages
) {
59 this.lookupMenu
.$element
.addClass( 'mw-widget-titleInputWidget-menu-withImages' );
61 if ( this.showDescriptions
) {
62 this.lookupMenu
.$element
.addClass( 'mw-widget-titleInputWidget-menu-withDescriptions' );
64 this.setLookupsDisabled( !this.suggestions
);
66 this.interwikiPrefixes
= [];
67 this.interwikiPrefixesPromise
= new mw
.Api().get( {
70 siprop
: 'interwikimap'
71 } ).done( function ( data
) {
72 $.each( data
.query
.interwikimap
, function ( index
, interwiki
) {
73 widget
.interwikiPrefixes
.push( interwiki
.prefix
);
80 OO
.inheritClass( mw
.widgets
.TitleInputWidget
, OO
.ui
.TextInputWidget
);
81 OO
.mixinClass( mw
.widgets
.TitleInputWidget
, OO
.ui
.mixin
.LookupElement
);
86 * Get the namespace to prepend to titles in suggestions, if any.
88 * @return {number|null} Namespace number
90 mw
.widgets
.TitleInputWidget
.prototype.getNamespace = function () {
91 return this.namespace;
95 * Set the namespace to prepend to titles in suggestions, if any.
97 * @param {number|null} namespace Namespace number
99 mw
.widgets
.TitleInputWidget
.prototype.setNamespace = function ( namespace ) {
100 this.namespace = namespace;
101 this.lookupCache
= {};
102 this.closeLookupMenu();
108 mw
.widgets
.TitleInputWidget
.prototype.onLookupMenuItemChoose = function ( item
) {
109 this.closeLookupMenu();
110 this.setLookupsDisabled( true );
111 this.setValue( item
.getData() );
112 this.setLookupsDisabled( !this.suggestions
);
118 mw
.widgets
.TitleInputWidget
.prototype.focus = function () {
121 // Prevent programmatic focus from opening the menu
122 this.setLookupsDisabled( true );
125 retval
= mw
.widgets
.TitleInputWidget
.parent
.prototype.focus
.apply( this, arguments
);
127 this.setLookupsDisabled( !this.suggestions
);
135 mw
.widgets
.TitleInputWidget
.prototype.getLookupRequest = function () {
138 promiseAbortObject
= { abort: function () {
139 // Do nothing. This is just so OOUI doesn't break due to abort being undefined.
142 if ( mw
.Title
.newFromText( this.value
) ) {
143 return this.interwikiPrefixesPromise
.then( function () {
145 interwiki
= widget
.value
.substring( 0, widget
.value
.indexOf( ':' ) );
147 interwiki
&& interwiki
!== '' &&
148 widget
.interwikiPrefixes
.indexOf( interwiki
) !== -1
150 return $.Deferred().resolve( { query
: {
154 } } ).promise( promiseAbortObject
);
158 generator
: 'prefixsearch',
159 gpssearch
: widget
.value
,
160 gpsnamespace
: widget
.namespace !== null ? widget
.namespace : undefined,
161 gpslimit
: widget
.limit
,
162 ppprop
: 'disambiguation'
164 props
= [ 'info', 'pageprops' ];
165 if ( widget
.showRedirectTargets
) {
166 params
.redirects
= '1';
168 if ( widget
.showImages
) {
169 props
.push( 'pageimages' );
170 params
.pithumbsize
= 80;
171 params
.pilimit
= widget
.limit
;
173 if ( widget
.showDescriptions
) {
174 props
.push( 'pageterms' );
175 params
.wbptterms
= 'description';
177 params
.prop
= props
.join( '|' );
178 req
= new mw
.Api().get( params
);
179 promiseAbortObject
.abort
= req
.abort
.bind( req
); // todo: ew
182 } ).promise( promiseAbortObject
);
184 // Don't send invalid titles to the API.
185 // Just pretend it returned nothing so we can show the 'invalid title' section
186 return $.Deferred().resolve( {} ).promise( promiseAbortObject
);
191 * Get lookup cache item from server response data.
194 * @param {Mixed} response Response from server
196 mw
.widgets
.TitleInputWidget
.prototype.getLookupCacheDataFromResponse = function ( response
) {
197 return response
.query
|| {};
201 * Get list of menu items from a server response.
203 * @param {Object} data Query result
204 * @returns {OO.ui.MenuOptionWidget[]} Menu items
206 mw
.widgets
.TitleInputWidget
.prototype.getLookupMenuOptionsFromData = function ( data
) {
207 var i
, len
, index
, pageExists
, pageExistsExact
, suggestionPage
, page
, redirect
, redirects
,
210 titleObj
= mw
.Title
.newFromText( this.value
),
214 if ( data
.redirects
) {
215 for ( i
= 0, len
= data
.redirects
.length
; i
< len
; i
++ ) {
216 redirect
= data
.redirects
[ i
];
217 redirectsTo
[ redirect
.to
] = redirectsTo
[ redirect
.to
] || [];
218 redirectsTo
[ redirect
.to
].push( redirect
.from );
222 for ( index
in data
.pages
) {
223 suggestionPage
= data
.pages
[ index
];
224 pageData
[ suggestionPage
.title
] = {
225 missing
: suggestionPage
.missing
!== undefined,
226 redirect
: suggestionPage
.redirect
!== undefined,
227 disambiguation
: OO
.getProp( suggestionPage
, 'pageprops', 'disambiguation' ) !== undefined,
228 imageUrl
: OO
.getProp( suggestionPage
, 'thumbnail', 'source' ),
229 description
: OO
.getProp( suggestionPage
, 'terms', 'description' )
232 // Throw away pages from wrong namespaces. This can happen when 'showRedirectTargets' is true
233 // and we encounter a cross-namespace redirect.
234 if ( this.namespace === null || this.namespace === suggestionPage
.ns
) {
235 titles
.push( suggestionPage
.title
);
238 redirects
= redirectsTo
[ suggestionPage
.title
] || [];
239 for ( i
= 0, len
= redirects
.length
; i
< len
; i
++ ) {
240 pageData
[ redirects
[ i
] ] = {
243 disambiguation
: false,
244 description
: mw
.msg( 'mw-widgets-titleinput-description-redirect', suggestionPage
.title
)
246 titles
.push( redirects
[ i
] );
250 // If not found, run value through mw.Title to avoid treating a match as a
251 // mismatch where normalisation would make them matching (bug 48476)
253 pageExistsExact
= titles
.indexOf( this.value
) !== -1;
254 pageExists
= pageExistsExact
|| (
255 titleObj
&& titles
.indexOf( titleObj
.getPrefixedText() ) !== -1
259 pageData
[ this.value
] = {
260 missing
: true, redirect
: false, disambiguation
: false,
261 description
: mw
.msg( 'mw-widgets-titleinput-description-new-page' )
266 this.cache
.set( pageData
);
269 // Offer the exact text as a suggestion if the page exists
270 if ( pageExists
&& !pageExistsExact
) {
271 titles
.unshift( this.value
);
273 // Offer the exact text as a new page if the title is valid
274 if ( this.showRedlink
&& !pageExists
&& titleObj
) {
275 titles
.push( this.value
);
277 for ( i
= 0, len
= titles
.length
; i
< len
; i
++ ) {
278 page
= pageData
[ titles
[ i
] ] || {};
279 items
.push( new mw
.widgets
.TitleOptionWidget( this.getOptionWidgetData( titles
[ i
], page
) ) );
286 * Get menu option widget data from the title and page data
288 * @param {mw.Title} title Title object
289 * @param {Object} data Page data
290 * @return {Object} Data for option widget
292 mw
.widgets
.TitleInputWidget
.prototype.getOptionWidgetData = function ( title
, data
) {
293 var mwTitle
= new mw
.Title( title
);
295 data
: this.namespace !== null && this.relative
296 ? mwTitle
.getRelativeText( this.namespace )
299 imageUrl
: this.showImages
? data
.imageUrl
: null,
300 description
: this.showDescriptions
? data
.description
: null,
301 missing
: data
.missing
,
302 redirect
: data
.redirect
,
303 disambiguation
: data
.disambiguation
,
309 * Get title object corresponding to given value, or #getValue if not given.
311 * @param {string} [value] Value to get a title for
312 * @returns {mw.Title|null} Title object, or null if value is invalid
314 mw
.widgets
.TitleInputWidget
.prototype.getTitle = function ( value
) {
315 var title
= value
!== undefined ? value
: this.getValue(),
316 // mw.Title doesn't handle null well
317 titleObj
= mw
.Title
.newFromText( title
, this.namespace !== null ? this.namespace : undefined );
325 mw
.widgets
.TitleInputWidget
.prototype.cleanUpValue = function ( value
) {
327 value
= mw
.widgets
.TitleInputWidget
.parent
.prototype.cleanUpValue
.call( this, value
);
328 return $.trimByteLength( this.value
, value
, this.maxLength
, function ( value
) {
329 var title
= widget
.getTitle( value
);
330 return title
? title
.getMain() : value
;
337 mw
.widgets
.TitleInputWidget
.prototype.isValid = function () {
338 return $.Deferred().resolve( !!this.getTitle() ).promise();
341 }( jQuery
, mediaWiki
) );