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
10 * Mixin for title widgets
16 * @param {Object} [config] Configuration options
17 * @cfg {number} [limit=10] Number of results to show
18 * @cfg {number} [namespace] Namespace to prepend to queries
19 * @cfg {number} [maxLength=255] Maximum query length
20 * @cfg {boolean} [relative=true] If a namespace is set, display titles relative to it
21 * @cfg {boolean} [suggestions=true] Display search suggestions
22 * @cfg {boolean} [showRedirectTargets=true] Show the targets of redirects
23 * @cfg {boolean} [showImages] Show page images
24 * @cfg {boolean} [showDescriptions] Show page descriptions
25 * @cfg {boolean} [excludeCurrentPage] Exclude the current page from suggestions
26 * @cfg {boolean} [validateTitle=true] Whether the input must be a valid title (if set to true,
27 * the widget will marks itself red for invalid inputs, including an empty query).
28 * @cfg {Object} [cache] Result cache which implements a 'set' method, taking keyed values as an argument
29 * @cfg {mw.Api} [api] API object to use, creates a default mw.Api instance if not specified
31 mw
.widgets
.TitleWidget
= function MwWidgetsTitleWidget( config
) {
32 // Config initialization
39 this.limit
= config
.limit
;
40 this.maxLength
= config
.maxLength
;
41 this.namespace = config
.namespace !== undefined ? config
.namespace : null;
42 this.relative
= config
.relative
!== undefined ? config
.relative
: true;
43 this.suggestions
= config
.suggestions
!== undefined ? config
.suggestions
: true;
44 this.showRedirectTargets
= config
.showRedirectTargets
!== false;
45 this.showImages
= !!config
.showImages
;
46 this.showDescriptions
= !!config
.showDescriptions
;
47 this.excludeCurrentPage
= !!config
.excludeCurrentPage
;
48 this.validateTitle
= config
.validateTitle
!== undefined ? config
.validateTitle
: true;
49 this.cache
= config
.cache
;
50 this.api
= config
.api
|| new mw
.Api();
53 this.$element
.addClass( 'mw-widget-titleWidget' );
58 OO
.initClass( mw
.widgets
.TitleWidget
);
60 /* Static properties */
62 mw
.widgets
.TitleWidget
.static.interwikiPrefixesPromiseCache
= {};
67 * Get the current value of the search query
70 * @return {string} Search query
72 mw
.widgets
.TitleWidget
.prototype.getQueryValue
= null;
75 * Get the namespace to prepend to titles in suggestions, if any.
77 * @return {number|null} Namespace number
79 mw
.widgets
.TitleWidget
.prototype.getNamespace = function () {
80 return this.namespace;
84 * Set the namespace to prepend to titles in suggestions, if any.
86 * @param {number|null} namespace Namespace number
88 mw
.widgets
.TitleWidget
.prototype.setNamespace = function ( namespace ) {
89 this.namespace = namespace;
92 mw
.widgets
.TitleWidget
.prototype.getInterwikiPrefixesPromise = function () {
93 var api
= this.getApi(),
94 cache
= this.constructor.static.interwikiPrefixesPromiseCache
,
95 key
= api
.defaults
.ajax
.url
;
96 if ( !cache
.hasOwnProperty( key
) ) {
97 cache
[ key
] = api
.get( {
100 siprop
: 'interwikimap',
101 // Cache client-side for a day since this info is mostly static
102 maxage
: 60 * 60 * 24,
103 smaxage
: 60 * 60 * 24,
104 // Workaround T97096 by setting uselang=content
106 } ).then( function ( data
) {
107 return $.map( data
.query
.interwikimap
, function ( interwiki
) {
108 return interwiki
.prefix
;
116 * Get a promise which resolves with an API repsonse for suggested
117 * links for the current query.
119 * @return {jQuery.Promise} Suggestions promise
121 mw
.widgets
.TitleWidget
.prototype.getSuggestionsPromise = function () {
124 query
= this.getQueryValue(),
126 promiseAbortObject
= { abort: function () {
127 // Do nothing. This is just so OOUI doesn't break due to abort being undefined.
130 if ( mw
.Title
.newFromText( query
) ) {
131 return this.getInterwikiPrefixesPromise().then( function ( interwikiPrefixes
) {
133 interwiki
= query
.substring( 0, query
.indexOf( ':' ) );
135 interwiki
&& interwiki
!== '' &&
136 interwikiPrefixes
.indexOf( interwiki
) !== -1
138 return $.Deferred().resolve( { query
: {
142 } } ).promise( promiseAbortObject
);
146 prop
: [ 'info', 'pageprops' ],
147 generator
: 'prefixsearch',
149 gpsnamespace
: widget
.namespace !== null ? widget
.namespace : undefined,
150 gpslimit
: widget
.limit
,
151 ppprop
: 'disambiguation'
153 if ( widget
.showRedirectTargets
) {
154 params
.redirects
= true;
156 if ( widget
.showImages
) {
157 params
.prop
.push( 'pageimages' );
158 params
.pithumbsize
= 80;
159 params
.pilimit
= widget
.limit
;
161 if ( widget
.showDescriptions
) {
162 params
.prop
.push( 'pageterms' );
163 params
.wbptterms
= 'description';
165 req
= api
.get( params
);
166 promiseAbortObject
.abort
= req
.abort
.bind( req
); // TODO ew
167 return req
.then( function ( ret
) {
168 if ( ret
.query
=== undefined ) {
169 ret
= api
.get( { action
: 'query', titles
: query
} );
170 promiseAbortObject
.abort
= ret
.abort
.bind( ret
);
175 } ).promise( promiseAbortObject
);
177 // Don't send invalid titles to the API.
178 // Just pretend it returned nothing so we can show the 'invalid title' section
179 return $.Deferred().resolve( {} ).promise( promiseAbortObject
);
184 * Get the API object for title requests
186 * @return {mw.Api} MediaWiki API
188 mw
.widgets
.TitleWidget
.prototype.getApi = function () {
193 * Get option widgets from the server response
195 * @param {Object} data Query result
196 * @return {OO.ui.OptionWidget[]} Menu items
198 mw
.widgets
.TitleWidget
.prototype.getOptionsFromData = function ( data
) {
199 var i
, len
, index
, pageExists
, pageExistsExact
, suggestionPage
, page
, redirect
, redirects
,
200 currentPageName
= new mw
.Title( mw
.config
.get( 'wgRelevantPageName' ) ).getPrefixedText(),
203 titleObj
= mw
.Title
.newFromText( this.getQueryValue() ),
207 if ( data
.redirects
) {
208 for ( i
= 0, len
= data
.redirects
.length
; i
< len
; i
++ ) {
209 redirect
= data
.redirects
[ i
];
210 redirectsTo
[ redirect
.to
] = redirectsTo
[ redirect
.to
] || [];
211 redirectsTo
[ redirect
.to
].push( redirect
.from );
215 for ( index
in data
.pages
) {
216 suggestionPage
= data
.pages
[ index
];
217 // When excludeCurrentPage is set, don't list the current page unless the user has type the full title
218 if ( this.excludeCurrentPage
&& suggestionPage
.title
=== currentPageName
&& suggestionPage
.title
!== titleObj
.getPrefixedText() ) {
221 pageData
[ suggestionPage
.title
] = {
222 known
: suggestionPage
.known
!== undefined,
223 missing
: suggestionPage
.missing
!== undefined,
224 redirect
: suggestionPage
.redirect
!== undefined,
225 disambiguation
: OO
.getProp( suggestionPage
, 'pageprops', 'disambiguation' ) !== undefined,
226 imageUrl
: OO
.getProp( suggestionPage
, 'thumbnail', 'source' ),
227 description
: OO
.getProp( suggestionPage
, 'terms', 'description' ),
229 index
: suggestionPage
.index
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
] ] = {
244 disambiguation
: false,
245 description
: mw
.msg( 'mw-widgets-titleinput-description-redirect', suggestionPage
.title
),
246 // Sort index, just below its target
247 index
: suggestionPage
.index
+ 0.5
249 titles
.push( redirects
[ i
] );
253 titles
.sort( function ( a
, b
) {
254 return pageData
[ a
].index
- pageData
[ b
].index
;
257 // If not found, run value through mw.Title to avoid treating a match as a
258 // mismatch where normalisation would make them matching (bug 48476)
261 Object
.prototype.hasOwnProperty
.call( pageData
, this.getQueryValue() ) &&
263 !pageData
[ this.getQueryValue() ].missing
||
264 pageData
[ this.getQueryValue() ].known
267 pageExists
= pageExistsExact
|| (
269 Object
.prototype.hasOwnProperty
.call( pageData
, titleObj
.getPrefixedText() ) &&
271 !pageData
[ titleObj
.getPrefixedText() ].missing
||
272 pageData
[ titleObj
.getPrefixedText() ].known
277 this.cache
.set( pageData
);
280 // Offer the exact text as a suggestion if the page exists
281 if ( pageExists
&& !pageExistsExact
) {
282 titles
.unshift( this.getQueryValue() );
285 for ( i
= 0, len
= titles
.length
; i
< len
; i
++ ) {
286 page
= pageData
[ titles
[ i
] ] || {};
287 items
.push( new mw
.widgets
.TitleOptionWidget( this.getOptionWidgetData( titles
[ i
], page
) ) );
294 * Get menu option widget data from the title and page data
296 * @param {string} title Title object
297 * @param {Object} data Page data
298 * @return {Object} Data for option widget
300 mw
.widgets
.TitleWidget
.prototype.getOptionWidgetData = function ( title
, data
) {
301 var mwTitle
= new mw
.Title( title
),
302 description
= data
.description
;
303 if ( data
.missing
&& !description
) {
304 description
= mw
.msg( 'mw-widgets-titleinput-description-new-page' );
307 data
: this.namespace !== null && this.relative
?
308 mwTitle
.getRelativeText( this.namespace ) :
310 url
: mwTitle
.getUrl(),
311 imageUrl
: this.showImages
? data
.imageUrl
: null,
312 description
: this.showDescriptions
? description
: null,
313 missing
: data
.missing
,
314 redirect
: data
.redirect
,
315 disambiguation
: data
.disambiguation
,
316 query
: this.getQueryValue()
321 * Get title object corresponding to given value, or #getQueryValue if not given.
323 * @param {string} [value] Value to get a title for
324 * @return {mw.Title|null} Title object, or null if value is invalid
326 mw
.widgets
.TitleWidget
.prototype.getTitle = function ( value
) {
327 var title
= value
!== undefined ? value
: this.getQueryValue(),
328 // mw.Title doesn't handle null well
329 titleObj
= mw
.Title
.newFromText( title
, this.namespace !== null ? this.namespace : undefined );
335 * Check if the query is valid
337 * @return {boolean} The query is valid
339 mw
.widgets
.TitleWidget
.prototype.isQueryValid = function () {
340 return this.validateTitle
? !!this.getTitle() : true;
343 }( jQuery
, mediaWiki
) );