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