2 * MediaWiki Widgets - TitleWidget class.
4 * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
5 * @license The MIT License (MIT); see LICENSE.txt
8 var hasOwn
= Object
.prototype.hasOwnProperty
;
11 * Mixin for title widgets
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 {number} [maxLength=255] Maximum query length
21 * @cfg {boolean} [relative=true] If a namespace is set, display titles relative to it
22 * @cfg {boolean} [suggestions=true] Display search suggestions
23 * @cfg {boolean} [showRedirectTargets=true] Show the targets of redirects
24 * @cfg {boolean} [showImages] Show page images
25 * @cfg {boolean} [showDescriptions] Show page descriptions
26 * @cfg {boolean} [showMissing=true] Show missing pages
27 * @cfg {boolean} [showInterwikis=false] Show pages with a valid interwiki prefix
28 * @cfg {boolean} [addQueryInput=true] Add exact user's input query to results
29 * @cfg {boolean} [excludeCurrentPage] Exclude the current page from suggestions
30 * @cfg {boolean} [excludeDynamicNamespaces] Exclude pages whose namespace is negative
31 * @cfg {boolean} [validateTitle=true] Whether the input must be a valid title
32 * @cfg {boolean} [required=false] Whether the input must not be empty
33 * @cfg {boolean} [highlightSearchQuery=true] Highlight the partial query the user used for this title
34 * @cfg {Object} [cache] Result cache which implements a 'set' method, taking keyed values as an argument
35 * @cfg {mw.Api} [api] API object to use, creates a default mw.Api instance if not specified
37 mw
.widgets
.TitleWidget
= function MwWidgetsTitleWidget( config
) {
38 // Config initialization
45 this.limit
= config
.limit
;
46 this.maxLength
= config
.maxLength
;
47 this.namespace = config
.namespace !== undefined ? config
.namespace : null;
48 this.relative
= config
.relative
!== undefined ? config
.relative
: true;
49 this.suggestions
= config
.suggestions
!== undefined ? config
.suggestions
: true;
50 this.showRedirectTargets
= config
.showRedirectTargets
!== false;
51 this.showImages
= !!config
.showImages
;
52 this.showDescriptions
= !!config
.showDescriptions
;
53 this.showMissing
= config
.showMissing
!== false;
54 this.showInterwikis
= !!config
.showInterwikis
;
55 this.addQueryInput
= config
.addQueryInput
!== false;
56 this.excludeCurrentPage
= !!config
.excludeCurrentPage
;
57 this.excludeDynamicNamespaces
= !!config
.excludeDynamicNamespaces
;
58 this.validateTitle
= config
.validateTitle
!== undefined ? config
.validateTitle
: true;
59 this.highlightSearchQuery
= config
.highlightSearchQuery
=== undefined ? true : !!config
.highlightSearchQuery
;
60 this.cache
= config
.cache
;
61 this.api
= config
.api
|| new mw
.Api();
62 // Supports: IE10, FF28, Chrome23
63 this.compare
= window
.Intl
&& Intl
.Collator
?
64 new Intl
.Collator( mw
.config
.get( 'wgContentLanguage' ), { sensitivity
: 'base' } ).compare
:
68 this.$element
.addClass( 'mw-widget-titleWidget' );
73 OO
.initClass( mw
.widgets
.TitleWidget
);
75 /* Static properties */
77 mw
.widgets
.TitleWidget
.static.interwikiPrefixesPromiseCache
= {};
82 * Get the current value of the search query
85 * @return {string} Search query
87 mw
.widgets
.TitleWidget
.prototype.getQueryValue
= null;
90 * Get the namespace to prepend to titles in suggestions, if any.
92 * @return {number|null} Namespace number
94 mw
.widgets
.TitleWidget
.prototype.getNamespace = function () {
95 return this.namespace;
99 * Set the namespace to prepend to titles in suggestions, if any.
101 * @param {number|null} namespace Namespace number
103 mw
.widgets
.TitleWidget
.prototype.setNamespace = function ( namespace ) {
104 this.namespace = namespace;
107 mw
.widgets
.TitleWidget
.prototype.getInterwikiPrefixesPromise = function () {
110 if ( !this.showInterwikis
) {
111 return $.Deferred().resolve( [] ).promise();
115 cache
= this.constructor.static.interwikiPrefixesPromiseCache
;
116 key
= api
.defaults
.ajax
.url
;
118 if ( !Object
.prototype.hasOwnProperty
.call( cache
, key
) ) {
119 cache
[ key
] = api
.get( {
122 siprop
: 'interwikimap',
123 // Cache client-side for a day since this info is mostly static
124 maxage
: 60 * 60 * 24,
125 smaxage
: 60 * 60 * 24,
126 // Workaround T97096 by setting uselang=content
128 } ).then( function ( data
) {
129 return data
.query
.interwikimap
.map( function ( interwiki
) {
130 return interwiki
.prefix
;
138 * Get a promise which resolves with an API repsonse for suggested
139 * links for the current query.
141 * @return {jQuery.Promise} Suggestions promise
143 mw
.widgets
.TitleWidget
.prototype.getSuggestionsPromise = function () {
146 query
= this.getQueryValue(),
148 promiseAbortObject
= { abort: function () {
149 // Do nothing. This is just so OOUI doesn't break due to abort being undefined.
152 if ( !mw
.Title
.newFromText( query
) ) {
153 // Don't send invalid titles to the API.
154 // Just pretend it returned nothing so we can show the 'invalid title' section
155 return $.Deferred().resolve( {} ).promise( promiseAbortObject
);
158 return this.getInterwikiPrefixesPromise().then( function ( interwikiPrefixes
) {
160 // Optimization: check we have any prefixes.
161 if ( interwikiPrefixes
.length
) {
162 interwiki
= query
.substring( 0, query
.indexOf( ':' ) );
164 interwiki
&& interwiki
!== '' &&
165 interwikiPrefixes
.indexOf( interwiki
) !== -1
167 // Interwiki prefix is valid: return the original query as a valid title
168 // NB: This doesn't check if the title actually exists on the other wiki
169 return $.Deferred().resolve( { query
: {
173 } } ).promise( promiseAbortObject
);
176 // Not a interwiki: do an API lookup of the query
177 req
= api
.get( widget
.getApiParams( query
) );
178 promiseAbortObject
.abort
= req
.abort
.bind( req
); // TODO ew
179 return req
.then( function ( ret
) {
180 if ( widget
.showMissing
&& ret
.query
=== undefined ) {
181 ret
= api
.get( { action
: 'query', titles
: query
} );
182 promiseAbortObject
.abort
= ret
.abort
.bind( ret
);
186 } ).promise( promiseAbortObject
);
190 * Get API params for a given query
192 * @param {string} query User query
193 * @return {Object} API params
195 mw
.widgets
.TitleWidget
.prototype.getApiParams = function ( query
) {
198 prop
: [ 'info', 'pageprops' ],
199 generator
: 'prefixsearch',
201 gpsnamespace
: this.namespace !== null ? this.namespace : undefined,
202 gpslimit
: this.limit
,
203 ppprop
: 'disambiguation'
205 if ( this.showRedirectTargets
) {
206 params
.redirects
= true;
208 if ( this.showImages
) {
209 params
.prop
.push( 'pageimages' );
210 params
.pithumbsize
= 80;
211 params
.pilimit
= this.limit
;
213 if ( this.showDescriptions
) {
214 params
.prop
.push( 'description' );
220 * Get the API object for title requests
222 * @return {mw.Api} MediaWiki API
224 mw
.widgets
.TitleWidget
.prototype.getApi = function () {
229 * Get option widgets from the server response
231 * @param {Object} data Query result
232 * @return {OO.ui.OptionWidget[]} Menu items
234 mw
.widgets
.TitleWidget
.prototype.getOptionsFromData = function ( data
) {
235 var i
, len
, index
, pageExists
, pageExistsExact
, suggestionPage
, page
, redirect
, redirects
,
236 currentPageName
= new mw
.Title( mw
.config
.get( 'wgRelevantPageName' ) ).getPrefixedText(),
239 titleObj
= mw
.Title
.newFromText( this.getQueryValue() ),
243 if ( data
.redirects
) {
244 for ( i
= 0, len
= data
.redirects
.length
; i
< len
; i
++ ) {
245 redirect
= data
.redirects
[ i
];
246 redirectsTo
[ redirect
.to
] = redirectsTo
[ redirect
.to
] || [];
247 redirectsTo
[ redirect
.to
].push( redirect
.from );
251 for ( index
in data
.pages
) {
252 suggestionPage
= data
.pages
[ index
];
254 // When excludeCurrentPage is set, don't list the current page unless the user has type the full title
255 if ( this.excludeCurrentPage
&& suggestionPage
.title
=== currentPageName
&& suggestionPage
.title
!== titleObj
.getPrefixedText() ) {
259 // When excludeDynamicNamespaces is set, ignore all pages with negative namespace
260 if ( this.excludeDynamicNamespaces
&& suggestionPage
.ns
< 0 ) {
263 pageData
[ suggestionPage
.title
] = {
264 known
: suggestionPage
.known
!== undefined,
265 missing
: suggestionPage
.missing
!== undefined,
266 redirect
: suggestionPage
.redirect
!== undefined,
267 disambiguation
: OO
.getProp( suggestionPage
, 'pageprops', 'disambiguation' ) !== undefined,
268 imageUrl
: OO
.getProp( suggestionPage
, 'thumbnail', 'source' ),
269 description
: suggestionPage
.description
,
271 index
: suggestionPage
.index
,
272 originalData
: suggestionPage
275 // Throw away pages from wrong namespaces. This can happen when 'showRedirectTargets' is true
276 // and we encounter a cross-namespace redirect.
277 if ( this.namespace === null || this.namespace === suggestionPage
.ns
) {
278 titles
.push( suggestionPage
.title
);
281 redirects
= hasOwn
.call( redirectsTo
, suggestionPage
.title
) ? redirectsTo
[ suggestionPage
.title
] : [];
282 for ( i
= 0, len
= redirects
.length
; i
< len
; i
++ ) {
283 pageData
[ redirects
[ i
] ] = {
287 disambiguation
: false,
288 description
: mw
.msg( 'mw-widgets-titleinput-description-redirect', suggestionPage
.title
),
289 // Sort index, just below its target
290 index
: suggestionPage
.index
+ 0.5,
291 originalData
: suggestionPage
293 titles
.push( redirects
[ i
] );
297 titles
.sort( function ( a
, b
) {
298 return pageData
[ a
].index
- pageData
[ b
].index
;
301 // If not found, run value through mw.Title to avoid treating a match as a
302 // mismatch where normalisation would make them matching (T50476)
305 hasOwn
.call( pageData
, this.getQueryValue() ) &&
307 !pageData
[ this.getQueryValue() ].missing
||
308 pageData
[ this.getQueryValue() ].known
311 pageExists
= pageExistsExact
|| (
313 hasOwn
.call( pageData
, titleObj
.getPrefixedText() ) &&
315 !pageData
[ titleObj
.getPrefixedText() ].missing
||
316 pageData
[ titleObj
.getPrefixedText() ].known
320 // Offer the exact text as a suggestion if the page exists
321 if ( this.addQueryInput
&& pageExists
&& !pageExistsExact
) {
322 titles
.unshift( this.getQueryValue() );
323 // Ensure correct page metadata gets used
324 pageData
[ this.getQueryValue() ] = pageData
[ titleObj
.getPrefixedText() ];
328 this.cache
.set( pageData
);
331 for ( i
= 0, len
= titles
.length
; i
< len
; i
++ ) {
332 page
= hasOwn
.call( pageData
, titles
[ i
] ) ? pageData
[ titles
[ i
] ] : {};
333 items
.push( this.createOptionWidget( this.getOptionWidgetData( titles
[ i
], page
) ) );
340 * Create a menu option widget with specified data
342 * @param {Object} data Data for option widget
343 * @return {OO.ui.MenuOptionWidget} Data for option widget
345 mw
.widgets
.TitleWidget
.prototype.createOptionWidget = function ( data
) {
346 return new mw
.widgets
.TitleOptionWidget( data
);
350 * Get menu option widget data from the title and page data
352 * @param {string} title Title object
353 * @param {Object} data Page data
354 * @return {Object} Data for option widget
356 mw
.widgets
.TitleWidget
.prototype.getOptionWidgetData = function ( title
, data
) {
357 var mwTitle
= new mw
.Title( title
),
358 description
= data
.description
;
359 if ( data
.missing
&& !description
) {
360 description
= mw
.msg( 'mw-widgets-titleinput-description-new-page' );
363 data
: this.namespace !== null && this.relative
?
364 mwTitle
.getRelativeText( this.namespace ) :
366 url
: mwTitle
.getUrl(),
367 showImages
: this.showImages
,
368 imageUrl
: this.showImages
? data
.imageUrl
: null,
369 description
: this.showDescriptions
? description
: null,
370 missing
: data
.missing
,
371 redirect
: data
.redirect
,
372 disambiguation
: data
.disambiguation
,
373 query
: this.highlightSearchQuery
? this.getQueryValue() : null,
374 compare
: this.compare
379 * Get title object corresponding to given value, or #getQueryValue if not given.
381 * @param {string} [value] Value to get a title for
382 * @return {mw.Title|null} Title object, or null if value is invalid
384 mw
.widgets
.TitleWidget
.prototype.getMWTitle = function ( value
) {
385 var title
= value
!== undefined ? value
: this.getQueryValue(),
386 // mw.Title doesn't handle null well
387 titleObj
= mw
.Title
.newFromText( title
, this.namespace !== null ? this.namespace : undefined );
393 * Check if the query is valid
395 * @return {boolean} The query is valid
397 mw
.widgets
.TitleWidget
.prototype.isQueryValid = function () {
398 if ( !this.validateTitle
) {
401 if ( !this.required
&& this.getQueryValue() === '' ) {
404 return !!this.getMWTitle();