Merge "Drop zh-tw message "saveprefs""
[lhc/web/wiklou.git] / resources / src / mediawiki.widgets / mw.widgets.TitleInputWidget.js
1 /*!
2 * MediaWiki Widgets - TitleInputWidget 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 * Creates an mw.widgets.TitleInputWidget object.
11 *
12 * @class
13 * @extends OO.ui.TextInputWidget
14 * @mixins OO.ui.mixin.LookupElement
15 *
16 * @constructor
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 {boolean} [relative=true] If a namespace is set, return a title relative to it
21 * @cfg {boolean} [suggestions=true] Display search suggestions
22 * @cfg {boolean} [showRedirectTargets=true] Show the targets of redirects
23 * @cfg {boolean} [showRedlink] Show red link to exact match if it doesn't exist
24 * @cfg {boolean} [showImages] Show page images
25 * @cfg {boolean} [showDescriptions] Show page descriptions
26 * @cfg {Object} [cache] Result cache which implements a 'set' method, taking keyed values as an argument
27 */
28 mw.widgets.TitleInputWidget = function MwWidgetsTitleInputWidget( config ) {
29 var widget = this;
30
31 // Config initialization
32 config = $.extend( {
33 maxLength: 255,
34 limit: 10
35 }, config );
36
37 // Parent constructor
38 mw.widgets.TitleInputWidget.parent.call( this, $.extend( {}, config, { autocomplete: false } ) );
39
40 // Mixin constructors
41 OO.ui.mixin.LookupElement.call( this, config );
42
43 // Properties
44 this.limit = config.limit;
45 this.maxLength = config.maxLength;
46 this.namespace = config.namespace !== undefined ? config.namespace : null;
47 this.relative = config.relative !== undefined ? config.relative : true;
48 this.suggestions = config.suggestions !== undefined ? config.suggestions : true;
49 this.showRedirectTargets = config.showRedirectTargets !== false;
50 this.showRedlink = !!config.showRedlink;
51 this.showImages = !!config.showImages;
52 this.showDescriptions = !!config.showDescriptions;
53 this.cache = config.cache;
54
55 // Initialization
56 this.$element.addClass( 'mw-widget-titleInputWidget' );
57 this.lookupMenu.$element.addClass( 'mw-widget-titleInputWidget-menu' );
58 if ( this.showImages ) {
59 this.lookupMenu.$element.addClass( 'mw-widget-titleInputWidget-menu-withImages' );
60 }
61 if ( this.showDescriptions ) {
62 this.lookupMenu.$element.addClass( 'mw-widget-titleInputWidget-menu-withDescriptions' );
63 }
64 this.setLookupsDisabled( !this.suggestions );
65
66 this.interwikiPrefixes = [];
67 this.interwikiPrefixesPromise = new mw.Api().get( {
68 action: 'query',
69 meta: 'siteinfo',
70 siprop: 'interwikimap'
71 } ).done( function ( data ) {
72 $.each( data.query.interwikimap, function ( index, interwiki ) {
73 widget.interwikiPrefixes.push( interwiki.prefix );
74 } );
75 } );
76 };
77
78 /* Setup */
79
80 OO.inheritClass( mw.widgets.TitleInputWidget, OO.ui.TextInputWidget );
81 OO.mixinClass( mw.widgets.TitleInputWidget, OO.ui.mixin.LookupElement );
82
83 /* Methods */
84
85 /**
86 * Get the namespace to prepend to titles in suggestions, if any.
87 *
88 * @return {number|null} Namespace number
89 */
90 mw.widgets.TitleInputWidget.prototype.getNamespace = function () {
91 return this.namespace;
92 };
93
94 /**
95 * Set the namespace to prepend to titles in suggestions, if any.
96 *
97 * @param {number|null} namespace Namespace number
98 */
99 mw.widgets.TitleInputWidget.prototype.setNamespace = function ( namespace ) {
100 this.namespace = namespace;
101 this.lookupCache = {};
102 this.closeLookupMenu();
103 };
104
105 /**
106 * @inheritdoc
107 */
108 mw.widgets.TitleInputWidget.prototype.onLookupMenuItemChoose = function ( item ) {
109 this.closeLookupMenu();
110 this.setLookupsDisabled( true );
111 this.setValue( item.getData() );
112 this.setLookupsDisabled( !this.suggestions );
113 };
114
115 /**
116 * @inheritdoc
117 */
118 mw.widgets.TitleInputWidget.prototype.focus = function () {
119 var retval;
120
121 // Prevent programmatic focus from opening the menu
122 this.setLookupsDisabled( true );
123
124 // Parent method
125 retval = mw.widgets.TitleInputWidget.parent.prototype.focus.apply( this, arguments );
126
127 this.setLookupsDisabled( !this.suggestions );
128
129 return retval;
130 };
131
132 /**
133 * @inheritdoc
134 */
135 mw.widgets.TitleInputWidget.prototype.getLookupRequest = function () {
136 var req,
137 widget = this,
138 promiseAbortObject = { abort: function () {
139 // Do nothing. This is just so OOUI doesn't break due to abort being undefined.
140 } };
141
142 if ( mw.Title.newFromText( this.value ) ) {
143 return this.interwikiPrefixesPromise.then( function () {
144 var params, props,
145 interwiki = widget.value.substring( 0, widget.value.indexOf( ':' ) );
146 if (
147 interwiki && interwiki !== '' &&
148 widget.interwikiPrefixes.indexOf( interwiki ) !== -1
149 ) {
150 return $.Deferred().resolve( { query: {
151 pages: [ {
152 title: widget.value
153 } ]
154 } } ).promise( promiseAbortObject );
155 } else {
156 params = {
157 action: 'query',
158 generator: 'prefixsearch',
159 gpssearch: widget.value,
160 gpsnamespace: widget.namespace !== null ? widget.namespace : undefined,
161 gpslimit: widget.limit,
162 ppprop: 'disambiguation'
163 };
164 props = [ 'info', 'pageprops' ];
165 if ( widget.showRedirectTargets ) {
166 params.redirects = '1';
167 }
168 if ( widget.showImages ) {
169 props.push( 'pageimages' );
170 params.pithumbsize = 80;
171 params.pilimit = widget.limit;
172 }
173 if ( widget.showDescriptions ) {
174 props.push( 'pageterms' );
175 params.wbptterms = 'description';
176 }
177 params.prop = props.join( '|' );
178 req = new mw.Api().get( params );
179 promiseAbortObject.abort = req.abort.bind( req ); // todo: ew
180 return req;
181 }
182 } ).promise( promiseAbortObject );
183 } else {
184 // Don't send invalid titles to the API.
185 // Just pretend it returned nothing so we can show the 'invalid title' section
186 return $.Deferred().resolve( {} ).promise( promiseAbortObject );
187 }
188 };
189
190 /**
191 * Get lookup cache item from server response data.
192 *
193 * @method
194 * @param {Mixed} response Response from server
195 */
196 mw.widgets.TitleInputWidget.prototype.getLookupCacheDataFromResponse = function ( response ) {
197 return response.query || {};
198 };
199
200 /**
201 * Get list of menu items from a server response.
202 *
203 * @param {Object} data Query result
204 * @returns {OO.ui.MenuOptionWidget[]} Menu items
205 */
206 mw.widgets.TitleInputWidget.prototype.getLookupMenuOptionsFromData = function ( data ) {
207 var i, len, index, pageExists, pageExistsExact, suggestionPage, page, redirect, redirects,
208 items = [],
209 titles = [],
210 titleObj = mw.Title.newFromText( this.value ),
211 redirectsTo = {},
212 pageData = {};
213
214 if ( data.redirects ) {
215 for ( i = 0, len = data.redirects.length; i < len; i++ ) {
216 redirect = data.redirects[ i ];
217 redirectsTo[ redirect.to ] = redirectsTo[ redirect.to ] || [];
218 redirectsTo[ redirect.to ].push( redirect.from );
219 }
220 }
221
222 for ( index in data.pages ) {
223 suggestionPage = data.pages[ index ];
224 pageData[ suggestionPage.title ] = {
225 missing: suggestionPage.missing !== undefined,
226 redirect: suggestionPage.redirect !== undefined,
227 disambiguation: OO.getProp( suggestionPage, 'pageprops', 'disambiguation' ) !== undefined,
228 imageUrl: OO.getProp( suggestionPage, 'thumbnail', 'source' ),
229 description: OO.getProp( suggestionPage, 'terms', 'description' )
230 };
231
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 );
236 }
237
238 redirects = redirectsTo[ suggestionPage.title ] || [];
239 for ( i = 0, len = redirects.length; i < len; i++ ) {
240 pageData[ redirects[ i ] ] = {
241 missing: false,
242 redirect: true,
243 disambiguation: false,
244 description: mw.msg( 'mw-widgets-titleinput-description-redirect', suggestionPage.title )
245 };
246 titles.push( redirects[ i ] );
247 }
248 }
249
250 // If not found, run value through mw.Title to avoid treating a match as a
251 // mismatch where normalisation would make them matching (bug 48476)
252
253 pageExistsExact = titles.indexOf( this.value ) !== -1;
254 pageExists = pageExistsExact || (
255 titleObj && titles.indexOf( titleObj.getPrefixedText() ) !== -1
256 );
257
258 if ( !pageExists ) {
259 pageData[ this.value ] = {
260 missing: true, redirect: false, disambiguation: false,
261 description: mw.msg( 'mw-widgets-titleinput-description-new-page' )
262 };
263 }
264
265 if ( this.cache ) {
266 this.cache.set( pageData );
267 }
268
269 // Offer the exact text as a suggestion if the page exists
270 if ( pageExists && !pageExistsExact ) {
271 titles.unshift( this.value );
272 }
273 // Offer the exact text as a new page if the title is valid
274 if ( this.showRedlink && !pageExists && titleObj ) {
275 titles.push( this.value );
276 }
277 for ( i = 0, len = titles.length; i < len; i++ ) {
278 page = pageData[ titles[ i ] ] || {};
279 items.push( new mw.widgets.TitleOptionWidget( this.getOptionWidgetData( titles[ i ], page ) ) );
280 }
281
282 return items;
283 };
284
285 /**
286 * Get menu option widget data from the title and page data
287 *
288 * @param {mw.Title} title Title object
289 * @param {Object} data Page data
290 * @return {Object} Data for option widget
291 */
292 mw.widgets.TitleInputWidget.prototype.getOptionWidgetData = function ( title, data ) {
293 var mwTitle = new mw.Title( title );
294 return {
295 data: this.namespace !== null && this.relative
296 ? mwTitle.getRelativeText( this.namespace )
297 : title,
298 title: mwTitle,
299 imageUrl: this.showImages ? data.imageUrl : null,
300 description: this.showDescriptions ? data.description : null,
301 missing: data.missing,
302 redirect: data.redirect,
303 disambiguation: data.disambiguation,
304 query: this.value
305 };
306 };
307
308 /**
309 * Get title object corresponding to given value, or #getValue if not given.
310 *
311 * @param {string} [value] Value to get a title for
312 * @returns {mw.Title|null} Title object, or null if value is invalid
313 */
314 mw.widgets.TitleInputWidget.prototype.getTitle = function ( value ) {
315 var title = value !== undefined ? value : this.getValue(),
316 // mw.Title doesn't handle null well
317 titleObj = mw.Title.newFromText( title, this.namespace !== null ? this.namespace : undefined );
318
319 return titleObj;
320 };
321
322 /**
323 * @inheritdoc
324 */
325 mw.widgets.TitleInputWidget.prototype.cleanUpValue = function ( value ) {
326 var widget = this;
327 value = mw.widgets.TitleInputWidget.parent.prototype.cleanUpValue.call( this, value );
328 return $.trimByteLength( this.value, value, this.maxLength, function ( value ) {
329 var title = widget.getTitle( value );
330 return title ? title.getMain() : value;
331 } ).newVal;
332 };
333
334 /**
335 * @inheritdoc
336 */
337 mw.widgets.TitleInputWidget.prototype.isValid = function () {
338 return $.Deferred().resolve( !!this.getTitle() ).promise();
339 };
340
341 }( jQuery, mediaWiki ) );