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