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