Merge "registration: Only allow one extension to set a specific config setting"
[lhc/web/wiklou.git] / resources / src / mediawiki.widgets / mw.widgets.TitleWidget.js
1 /*!
2 * MediaWiki Widgets - TitleWidget class.
3 *
4 * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt
5 * @license The MIT License (MIT); see LICENSE.txt
6 */
7 ( function ( $, mw ) {
8
9 /**
10 * Mixin for title widgets
11 *
12 * @class
13 * @abstract
14 *
15 * @constructor
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} [showMissing=true] Show missing pages
26 * @cfg {boolean} [excludeCurrentPage] Exclude the current page from suggestions
27 * @cfg {boolean} [validateTitle=true] Whether the input must be a valid title (if set to true,
28 * the widget will marks itself red for invalid inputs, including an empty query).
29 * @cfg {Object} [cache] Result cache which implements a 'set' method, taking keyed values as an argument
30 * @cfg {mw.Api} [api] API object to use, creates a default mw.Api instance if not specified
31 */
32 mw.widgets.TitleWidget = function MwWidgetsTitleWidget( config ) {
33 // Config initialization
34 config = $.extend( {
35 maxLength: 255,
36 limit: 10
37 }, config );
38
39 // Properties
40 this.limit = config.limit;
41 this.maxLength = config.maxLength;
42 this.namespace = config.namespace !== undefined ? config.namespace : null;
43 this.relative = config.relative !== undefined ? config.relative : true;
44 this.suggestions = config.suggestions !== undefined ? config.suggestions : true;
45 this.showRedirectTargets = config.showRedirectTargets !== false;
46 this.showImages = !!config.showImages;
47 this.showDescriptions = !!config.showDescriptions;
48 this.showMissing = config.showMissing !== false;
49 this.excludeCurrentPage = !!config.excludeCurrentPage;
50 this.validateTitle = config.validateTitle !== undefined ? config.validateTitle : true;
51 this.cache = config.cache;
52 this.api = config.api || new mw.Api();
53 // Supports: IE10, FF28, Chrome23
54 this.compare = window.Intl && Intl.Collator ?
55 new Intl.Collator( mw.config.get( 'wgContentLanguage' ), { sensitivity: 'base' } ).compare :
56 null;
57
58 // Initialization
59 this.$element.addClass( 'mw-widget-titleWidget' );
60 };
61
62 /* Setup */
63
64 OO.initClass( mw.widgets.TitleWidget );
65
66 /* Static properties */
67
68 mw.widgets.TitleWidget.static.interwikiPrefixesPromiseCache = {};
69
70 /* Methods */
71
72 /**
73 * Get the current value of the search query
74 *
75 * @abstract
76 * @return {string} Search query
77 */
78 mw.widgets.TitleWidget.prototype.getQueryValue = null;
79
80 /**
81 * Get the namespace to prepend to titles in suggestions, if any.
82 *
83 * @return {number|null} Namespace number
84 */
85 mw.widgets.TitleWidget.prototype.getNamespace = function () {
86 return this.namespace;
87 };
88
89 /**
90 * Set the namespace to prepend to titles in suggestions, if any.
91 *
92 * @param {number|null} namespace Namespace number
93 */
94 mw.widgets.TitleWidget.prototype.setNamespace = function ( namespace ) {
95 this.namespace = namespace;
96 };
97
98 mw.widgets.TitleWidget.prototype.getInterwikiPrefixesPromise = function () {
99 var api = this.getApi(),
100 cache = this.constructor.static.interwikiPrefixesPromiseCache,
101 key = api.defaults.ajax.url;
102 if ( !cache.hasOwnProperty( key ) ) {
103 cache[ key ] = api.get( {
104 action: 'query',
105 meta: 'siteinfo',
106 siprop: 'interwikimap',
107 // Cache client-side for a day since this info is mostly static
108 maxage: 60 * 60 * 24,
109 smaxage: 60 * 60 * 24,
110 // Workaround T97096 by setting uselang=content
111 uselang: 'content'
112 } ).then( function ( data ) {
113 return $.map( data.query.interwikimap, function ( interwiki ) {
114 return interwiki.prefix;
115 } );
116 } );
117 }
118 return cache[ key ];
119 };
120
121 /**
122 * Get a promise which resolves with an API repsonse for suggested
123 * links for the current query.
124 *
125 * @return {jQuery.Promise} Suggestions promise
126 */
127 mw.widgets.TitleWidget.prototype.getSuggestionsPromise = function () {
128 var req,
129 api = this.getApi(),
130 query = this.getQueryValue(),
131 widget = this,
132 promiseAbortObject = { abort: function () {
133 // Do nothing. This is just so OOUI doesn't break due to abort being undefined.
134 } };
135
136 if ( !mw.Title.newFromText( query ) ) {
137 // Don't send invalid titles to the API.
138 // Just pretend it returned nothing so we can show the 'invalid title' section
139 return $.Deferred().resolve( {} ).promise( promiseAbortObject );
140 }
141
142 return this.getInterwikiPrefixesPromise().then( function ( interwikiPrefixes ) {
143 var interwiki = query.substring( 0, query.indexOf( ':' ) );
144 if (
145 interwiki && interwiki !== '' &&
146 interwikiPrefixes.indexOf( interwiki ) !== -1
147 ) {
148 return $.Deferred().resolve( { query: {
149 pages: [ {
150 title: query
151 } ]
152 } } ).promise( promiseAbortObject );
153 } else {
154 req = api.get( widget.getApiParams( query ) );
155 promiseAbortObject.abort = req.abort.bind( req ); // TODO ew
156 return req.then( function ( ret ) {
157 if ( widget.showMissing && ret.query === undefined ) {
158 ret = api.get( { action: 'query', titles: query } );
159 promiseAbortObject.abort = ret.abort.bind( ret );
160 }
161 return ret;
162 } );
163 }
164 } ).promise( promiseAbortObject );
165 };
166
167 /**
168 * Get API params for a given query
169 *
170 * @param {string} query User query
171 * @return {Object} API params
172 */
173 mw.widgets.TitleWidget.prototype.getApiParams = function ( query ) {
174 var params = {
175 action: 'query',
176 prop: [ 'info', 'pageprops' ],
177 generator: 'prefixsearch',
178 gpssearch: query,
179 gpsnamespace: this.namespace !== null ? this.namespace : undefined,
180 gpslimit: this.limit,
181 ppprop: 'disambiguation'
182 };
183 if ( this.showRedirectTargets ) {
184 params.redirects = true;
185 }
186 if ( this.showImages ) {
187 params.prop.push( 'pageimages' );
188 params.pithumbsize = 80;
189 params.pilimit = this.limit;
190 }
191 if ( this.showDescriptions ) {
192 params.prop.push( 'pageterms' );
193 params.wbptterms = 'description';
194 }
195 return params;
196 };
197
198 /**
199 * Get the API object for title requests
200 *
201 * @return {mw.Api} MediaWiki API
202 */
203 mw.widgets.TitleWidget.prototype.getApi = function () {
204 return this.api;
205 };
206
207 /**
208 * Get option widgets from the server response
209 *
210 * @param {Object} data Query result
211 * @return {OO.ui.OptionWidget[]} Menu items
212 */
213 mw.widgets.TitleWidget.prototype.getOptionsFromData = function ( data ) {
214 var i, len, index, pageExists, pageExistsExact, suggestionPage, page, redirect, redirects,
215 currentPageName = new mw.Title( mw.config.get( 'wgRelevantPageName' ) ).getPrefixedText(),
216 items = [],
217 titles = [],
218 titleObj = mw.Title.newFromText( this.getQueryValue() ),
219 redirectsTo = {},
220 pageData = {};
221
222 if ( data.redirects ) {
223 for ( i = 0, len = data.redirects.length; i < len; i++ ) {
224 redirect = data.redirects[ i ];
225 redirectsTo[ redirect.to ] = redirectsTo[ redirect.to ] || [];
226 redirectsTo[ redirect.to ].push( redirect.from );
227 }
228 }
229
230 for ( index in data.pages ) {
231 suggestionPage = data.pages[ index ];
232
233 // When excludeCurrentPage is set, don't list the current page unless the user has type the full title
234 if ( this.excludeCurrentPage && suggestionPage.title === currentPageName && suggestionPage.title !== titleObj.getPrefixedText() ) {
235 continue;
236 }
237 pageData[ suggestionPage.title ] = {
238 known: suggestionPage.known !== undefined,
239 missing: suggestionPage.missing !== undefined,
240 redirect: suggestionPage.redirect !== undefined,
241 disambiguation: OO.getProp( suggestionPage, 'pageprops', 'disambiguation' ) !== undefined,
242 imageUrl: OO.getProp( suggestionPage, 'thumbnail', 'source' ),
243 description: OO.getProp( suggestionPage, 'terms', 'description' ),
244 // Sort index
245 index: suggestionPage.index,
246 originalData: suggestionPage
247 };
248
249 // Throw away pages from wrong namespaces. This can happen when 'showRedirectTargets' is true
250 // and we encounter a cross-namespace redirect.
251 if ( this.namespace === null || this.namespace === suggestionPage.ns ) {
252 titles.push( suggestionPage.title );
253 }
254
255 redirects = redirectsTo[ suggestionPage.title ] || [];
256 for ( i = 0, len = redirects.length; i < len; i++ ) {
257 pageData[ redirects[ i ] ] = {
258 missing: false,
259 known: true,
260 redirect: true,
261 disambiguation: false,
262 description: mw.msg( 'mw-widgets-titleinput-description-redirect', suggestionPage.title ),
263 // Sort index, just below its target
264 index: suggestionPage.index + 0.5,
265 originalData: suggestionPage
266 };
267 titles.push( redirects[ i ] );
268 }
269 }
270
271 titles.sort( function ( a, b ) {
272 return pageData[ a ].index - pageData[ b ].index;
273 } );
274
275 // If not found, run value through mw.Title to avoid treating a match as a
276 // mismatch where normalisation would make them matching (T50476)
277
278 pageExistsExact = (
279 Object.prototype.hasOwnProperty.call( pageData, this.getQueryValue() ) &&
280 (
281 !pageData[ this.getQueryValue() ].missing ||
282 pageData[ this.getQueryValue() ].known
283 )
284 );
285 pageExists = pageExistsExact || (
286 titleObj &&
287 Object.prototype.hasOwnProperty.call( pageData, titleObj.getPrefixedText() ) &&
288 (
289 !pageData[ titleObj.getPrefixedText() ].missing ||
290 pageData[ titleObj.getPrefixedText() ].known
291 )
292 );
293
294 if ( this.cache ) {
295 this.cache.set( pageData );
296 }
297
298 // Offer the exact text as a suggestion if the page exists
299 if ( pageExists && !pageExistsExact ) {
300 titles.unshift( this.getQueryValue() );
301 }
302
303 for ( i = 0, len = titles.length; i < len; i++ ) {
304 page = pageData[ titles[ i ] ] || {};
305 items.push( this.createOptionWidget( this.getOptionWidgetData( titles[ i ], page ) ) );
306 }
307
308 return items;
309 };
310
311 /**
312 * Create a menu option widget with specified data
313 *
314 * @param {Object} data Data for option widget
315 * @return {OO.ui.MenuOptionWidget} Data for option widget
316 */
317 mw.widgets.TitleWidget.prototype.createOptionWidget = function ( data ) {
318 return new mw.widgets.TitleOptionWidget( data );
319 };
320
321 /**
322 * Get menu option widget data from the title and page data
323 *
324 * @param {string} title Title object
325 * @param {Object} data Page data
326 * @return {Object} Data for option widget
327 */
328 mw.widgets.TitleWidget.prototype.getOptionWidgetData = function ( title, data ) {
329 var mwTitle = new mw.Title( title ),
330 description = data.description;
331 if ( data.missing && !description ) {
332 description = mw.msg( 'mw-widgets-titleinput-description-new-page' );
333 }
334 return {
335 data: this.namespace !== null && this.relative ?
336 mwTitle.getRelativeText( this.namespace ) :
337 title,
338 url: mwTitle.getUrl(),
339 showImages: this.showImages,
340 imageUrl: this.showImages ? data.imageUrl : null,
341 description: this.showDescriptions ? description : null,
342 missing: data.missing,
343 redirect: data.redirect,
344 disambiguation: data.disambiguation,
345 query: this.getQueryValue(),
346 compare: this.compare
347 };
348 };
349
350 /**
351 * Get title object corresponding to given value, or #getQueryValue if not given.
352 *
353 * @param {string} [value] Value to get a title for
354 * @return {mw.Title|null} Title object, or null if value is invalid
355 */
356 mw.widgets.TitleWidget.prototype.getTitle = function ( value ) {
357 var title = value !== undefined ? value : this.getQueryValue(),
358 // mw.Title doesn't handle null well
359 titleObj = mw.Title.newFromText( title, this.namespace !== null ? this.namespace : undefined );
360
361 return titleObj;
362 };
363
364 /**
365 * Check if the query is valid
366 *
367 * @return {boolean} The query is valid
368 */
369 mw.widgets.TitleWidget.prototype.isQueryValid = function () {
370 return this.validateTitle ? !!this.getTitle() : true;
371 };
372
373 }( jQuery, mediaWiki ) );