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