Make NumericUppercaseCollation use localized digit transforms
[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 var interwikiPrefixesPromise = new mw.Api().get( {
10 action: 'query',
11 meta: 'siteinfo',
12 siprop: 'interwikimap'
13 } ).then( function ( data ) {
14 return $.map( data.query.interwikimap, function ( interwiki ) {
15 return interwiki.prefix;
16 } );
17 } );
18
19 /**
20 * Mixin for title widgets
21 *
22 * @class
23 * @abstract
24 *
25 * @constructor
26 * @param {Object} [config] Configuration options
27 * @cfg {number} [limit=10] Number of results to show
28 * @cfg {number} [namespace] Namespace to prepend to queries
29 * @cfg {number} [maxLength=255] Maximum query length
30 * @cfg {boolean} [relative=true] If a namespace is set, display titles relative to it
31 * @cfg {boolean} [suggestions=true] Display search suggestions
32 * @cfg {boolean} [showRedirectTargets=true] Show the targets of redirects
33 * @cfg {boolean} [showRedlink] Show red link to exact match if it doesn't exist
34 * @cfg {boolean} [showImages] Show page images
35 * @cfg {boolean} [showDescriptions] Show page descriptions
36 * @cfg {boolean} [excludeCurrentPage] Exclude the current page from suggestions
37 * @cfg {boolean} [validateTitle=true] Whether the input must be a valid title (if set to true,
38 * the widget will marks itself red for invalid inputs, including an empty query).
39 * @cfg {Object} [cache] Result cache which implements a 'set' method, taking keyed values as an argument
40 */
41 mw.widgets.TitleWidget = function MwWidgetsTitleWidget( config ) {
42 // Config initialization
43 config = $.extend( {
44 maxLength: 255,
45 limit: 10
46 }, config );
47
48 // Properties
49 this.limit = config.limit;
50 this.maxLength = config.maxLength;
51 this.namespace = config.namespace !== undefined ? config.namespace : null;
52 this.relative = config.relative !== undefined ? config.relative : true;
53 this.suggestions = config.suggestions !== undefined ? config.suggestions : true;
54 this.showRedirectTargets = config.showRedirectTargets !== false;
55 this.showRedlink = !!config.showRedlink;
56 this.showImages = !!config.showImages;
57 this.showDescriptions = !!config.showDescriptions;
58 this.excludeCurrentPage = !!config.excludeCurrentPage;
59 this.validateTitle = config.validateTitle !== undefined ? config.validateTitle : true;
60 this.cache = config.cache;
61
62 // Initialization
63 this.$element.addClass( 'mw-widget-titleWidget' );
64 };
65
66 /* Setup */
67
68 OO.initClass( mw.widgets.TitleWidget );
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 /**
99 * Get a promise which resolves with an API repsonse for suggested
100 * links for the current query.
101 *
102 * @return {jQuery.Promise} Suggestions promise
103 */
104 mw.widgets.TitleWidget.prototype.getSuggestionsPromise = function () {
105 var req,
106 query = this.getQueryValue(),
107 widget = this,
108 promiseAbortObject = { abort: function () {
109 // Do nothing. This is just so OOUI doesn't break due to abort being undefined.
110 } };
111
112 if ( mw.Title.newFromText( query ) ) {
113 return interwikiPrefixesPromise.then( function ( interwikiPrefixes ) {
114 var params,
115 interwiki = query.substring( 0, query.indexOf( ':' ) );
116 if (
117 interwiki && interwiki !== '' &&
118 interwikiPrefixes.indexOf( interwiki ) !== -1
119 ) {
120 return $.Deferred().resolve( { query: {
121 pages: [ {
122 title: query
123 } ]
124 } } ).promise( promiseAbortObject );
125 } else {
126 params = {
127 action: 'query',
128 prop: [ 'info', 'pageprops' ],
129 generator: 'prefixsearch',
130 gpssearch: query,
131 gpsnamespace: widget.namespace !== null ? widget.namespace : undefined,
132 gpslimit: widget.limit,
133 ppprop: 'disambiguation'
134 };
135 if ( widget.showRedirectTargets ) {
136 params.redirects = true;
137 }
138 if ( widget.showImages ) {
139 params.prop.push( 'pageimages' );
140 params.pithumbsize = 80;
141 params.pilimit = widget.limit;
142 }
143 if ( widget.showDescriptions ) {
144 params.prop.push( 'pageterms' );
145 params.wbptterms = 'description';
146 }
147 req = new mw.Api().get( params );
148 promiseAbortObject.abort = req.abort.bind( req ); // TODO ew
149 return req.then( function ( ret ) {
150 if ( ret.query === undefined ) {
151 ret = new mw.Api().get( { action: 'query', titles: query } );
152 promiseAbortObject.abort = ret.abort.bind( ret );
153 }
154 return ret;
155 } );
156 }
157 } ).promise( promiseAbortObject );
158 } else {
159 // Don't send invalid titles to the API.
160 // Just pretend it returned nothing so we can show the 'invalid title' section
161 return $.Deferred().resolve( {} ).promise( promiseAbortObject );
162 }
163 };
164
165 /**
166 * Get option widgets from the server response
167 *
168 * @param {Object} data Query result
169 * @return {OO.ui.OptionWidget[]} Menu items
170 */
171 mw.widgets.TitleWidget.prototype.getOptionsFromData = function ( data ) {
172 var i, len, index, pageExists, pageExistsExact, suggestionPage, page, redirect, redirects,
173 currentPageName = new mw.Title( mw.config.get( 'wgRelevantPageName' ) ).getPrefixedText(),
174 items = [],
175 titles = [],
176 titleObj = mw.Title.newFromText( this.getQueryValue() ),
177 redirectsTo = {},
178 pageData = {};
179
180 if ( data.redirects ) {
181 for ( i = 0, len = data.redirects.length; i < len; i++ ) {
182 redirect = data.redirects[ i ];
183 redirectsTo[ redirect.to ] = redirectsTo[ redirect.to ] || [];
184 redirectsTo[ redirect.to ].push( redirect.from );
185 }
186 }
187
188 for ( index in data.pages ) {
189 suggestionPage = data.pages[ index ];
190 // When excludeCurrentPage is set, don't list the current page unless the user has type the full title
191 if ( this.excludeCurrentPage && suggestionPage.title === currentPageName && suggestionPage.title !== titleObj.getPrefixedText() ) {
192 continue;
193 }
194 pageData[ suggestionPage.title ] = {
195 known: suggestionPage.known !== undefined,
196 missing: suggestionPage.missing !== undefined,
197 redirect: suggestionPage.redirect !== undefined,
198 disambiguation: OO.getProp( suggestionPage, 'pageprops', 'disambiguation' ) !== undefined,
199 imageUrl: OO.getProp( suggestionPage, 'thumbnail', 'source' ),
200 description: OO.getProp( suggestionPage, 'terms', 'description' ),
201 // Sort index
202 index: suggestionPage.index
203 };
204
205 // Throw away pages from wrong namespaces. This can happen when 'showRedirectTargets' is true
206 // and we encounter a cross-namespace redirect.
207 if ( this.namespace === null || this.namespace === suggestionPage.ns ) {
208 titles.push( suggestionPage.title );
209 }
210
211 redirects = redirectsTo[ suggestionPage.title ] || [];
212 for ( i = 0, len = redirects.length; i < len; i++ ) {
213 pageData[ redirects[ i ] ] = {
214 missing: false,
215 known: true,
216 redirect: true,
217 disambiguation: false,
218 description: mw.msg( 'mw-widgets-titleinput-description-redirect', suggestionPage.title ),
219 // Sort index, just below its target
220 index: suggestionPage.index + 0.5
221 };
222 titles.push( redirects[ i ] );
223 }
224 }
225
226 titles.sort( function ( a, b ) {
227 return pageData[ a ].index - pageData[ b ].index;
228 } );
229
230 // If not found, run value through mw.Title to avoid treating a match as a
231 // mismatch where normalisation would make them matching (bug 48476)
232
233 pageExistsExact = (
234 Object.prototype.hasOwnProperty.call( pageData, this.getQueryValue() ) &&
235 (
236 !pageData[ this.getQueryValue() ].missing ||
237 pageData[ this.getQueryValue() ].known
238 )
239 );
240 pageExists = pageExistsExact || (
241 titleObj &&
242 Object.prototype.hasOwnProperty.call( pageData, titleObj.getPrefixedText() ) &&
243 (
244 !pageData[ titleObj.getPrefixedText() ].missing ||
245 pageData[ titleObj.getPrefixedText() ].known
246 )
247 );
248
249 if ( !pageExists ) {
250 pageData[ this.getQueryValue() ] = {
251 missing: true, known: false, redirect: false, disambiguation: false,
252 description: mw.msg( 'mw-widgets-titleinput-description-new-page' )
253 };
254 }
255
256 if ( this.cache ) {
257 this.cache.set( pageData );
258 }
259
260 // Offer the exact text as a suggestion if the page exists
261 if ( pageExists && !pageExistsExact ) {
262 titles.unshift( this.getQueryValue() );
263 }
264 // Offer the exact text as a new page if the title is valid
265 if ( this.showRedlink && !pageExists && titleObj ) {
266 titles.push( this.getQueryValue() );
267 }
268 for ( i = 0, len = titles.length; i < len; i++ ) {
269 page = pageData[ titles[ i ] ] || {};
270 items.push( new mw.widgets.TitleOptionWidget( this.getOptionWidgetData( titles[ i ], page ) ) );
271 }
272
273 return items;
274 };
275
276 /**
277 * Get menu option widget data from the title and page data
278 *
279 * @param {string} title Title object
280 * @param {Object} data Page data
281 * @return {Object} Data for option widget
282 */
283 mw.widgets.TitleWidget.prototype.getOptionWidgetData = function ( title, data ) {
284 var mwTitle = new mw.Title( title );
285 return {
286 data: this.namespace !== null && this.relative
287 ? mwTitle.getRelativeText( this.namespace )
288 : title,
289 url: mwTitle.getUrl(),
290 imageUrl: this.showImages ? data.imageUrl : null,
291 description: this.showDescriptions ? data.description : null,
292 missing: data.missing,
293 redirect: data.redirect,
294 disambiguation: data.disambiguation,
295 query: this.getQueryValue()
296 };
297 };
298
299 /**
300 * Get title object corresponding to given value, or #getQueryValue if not given.
301 *
302 * @param {string} [value] Value to get a title for
303 * @return {mw.Title|null} Title object, or null if value is invalid
304 */
305 mw.widgets.TitleWidget.prototype.getTitle = function ( value ) {
306 var title = value !== undefined ? value : this.getQueryValue(),
307 // mw.Title doesn't handle null well
308 titleObj = mw.Title.newFromText( title, this.namespace !== null ? this.namespace : undefined );
309
310 return titleObj;
311 };
312
313 /**
314 * Check if the query is valid
315 *
316 * @return {boolean} The query is valid
317 */
318 mw.widgets.TitleWidget.prototype.isQueryValid = function () {
319 return this.validateTitle ? !!this.getTitle() : true;
320 };
321
322 }( jQuery, mediaWiki ) );