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