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