build: Replace jscs+jshint with eslint
[lhc/web/wiklou.git] / resources / src / mediawiki.widgets / mw.widgets.TitleWidget.js
1 /*!
2 * MediaWiki Widgets - TitleWidget 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 var interwikiPrefixesPromise = new mw.Api().get( {
10 action: 'query',
11 meta: 'siteinfo',
12 siprop: 'interwikimap'
13 } ).then( function ( data ) {
14 return $.map( data.query.interwikimap, function ( interwiki ) {
15 return interwiki.prefix;
16 } );
17 } );
18
19 /**
20 * Mixin for title widgets
21 *
22 * @class
23 * @abstract
24 *
25 * @constructor
26 * @param {Object} [config] Configuration options
27 * @cfg {number} [limit=10] Number of results to show
28 * @cfg {number} [namespace] Namespace to prepend to queries
29 * @cfg {number} [maxLength=255] Maximum query length
30 * @cfg {boolean} [relative=true] If a namespace is set, display titles relative to it
31 * @cfg {boolean} [suggestions=true] Display search suggestions
32 * @cfg {boolean} [showRedirectTargets=true] Show the targets of redirects
33 * @cfg {boolean} [showImages] Show page images
34 * @cfg {boolean} [showDescriptions] Show page descriptions
35 * @cfg {boolean} [excludeCurrentPage] Exclude the current page from suggestions
36 * @cfg {boolean} [validateTitle=true] Whether the input must be a valid title (if set to true,
37 * the widget will marks itself red for invalid inputs, including an empty query).
38 * @cfg {Object} [cache] Result cache which implements a 'set' method, taking keyed values as an argument
39 */
40 mw.widgets.TitleWidget = function MwWidgetsTitleWidget( config ) {
41 // Config initialization
42 config = $.extend( {
43 maxLength: 255,
44 limit: 10
45 }, config );
46
47 // Properties
48 this.limit = config.limit;
49 this.maxLength = config.maxLength;
50 this.namespace = config.namespace !== undefined ? config.namespace : null;
51 this.relative = config.relative !== undefined ? config.relative : true;
52 this.suggestions = config.suggestions !== undefined ? config.suggestions : true;
53 this.showRedirectTargets = config.showRedirectTargets !== false;
54 this.showImages = !!config.showImages;
55 this.showDescriptions = !!config.showDescriptions;
56 this.excludeCurrentPage = !!config.excludeCurrentPage;
57 this.validateTitle = config.validateTitle !== undefined ? config.validateTitle : true;
58 this.cache = config.cache;
59
60 // Initialization
61 this.$element.addClass( 'mw-widget-titleWidget' );
62 };
63
64 /* Setup */
65
66 OO.initClass( mw.widgets.TitleWidget );
67
68 /* Methods */
69
70 /**
71 * Get the current value of the search query
72 *
73 * @abstract
74 * @return {string} Search query
75 */
76 mw.widgets.TitleWidget.prototype.getQueryValue = null;
77
78 /**
79 * Get the namespace to prepend to titles in suggestions, if any.
80 *
81 * @return {number|null} Namespace number
82 */
83 mw.widgets.TitleWidget.prototype.getNamespace = function () {
84 return this.namespace;
85 };
86
87 /**
88 * Set the namespace to prepend to titles in suggestions, if any.
89 *
90 * @param {number|null} namespace Namespace number
91 */
92 mw.widgets.TitleWidget.prototype.setNamespace = function ( namespace ) {
93 this.namespace = namespace;
94 };
95
96 /**
97 * Get a promise which resolves with an API repsonse for suggested
98 * links for the current query.
99 *
100 * @return {jQuery.Promise} Suggestions promise
101 */
102 mw.widgets.TitleWidget.prototype.getSuggestionsPromise = function () {
103 var req,
104 query = this.getQueryValue(),
105 widget = this,
106 promiseAbortObject = { abort: function () {
107 // Do nothing. This is just so OOUI doesn't break due to abort being undefined.
108 } };
109
110 if ( mw.Title.newFromText( query ) ) {
111 return interwikiPrefixesPromise.then( function ( interwikiPrefixes ) {
112 var params,
113 interwiki = query.substring( 0, query.indexOf( ':' ) );
114 if (
115 interwiki && interwiki !== '' &&
116 interwikiPrefixes.indexOf( interwiki ) !== -1
117 ) {
118 return $.Deferred().resolve( { query: {
119 pages: [ {
120 title: query
121 } ]
122 } } ).promise( promiseAbortObject );
123 } else {
124 params = {
125 action: 'query',
126 prop: [ 'info', 'pageprops' ],
127 generator: 'prefixsearch',
128 gpssearch: query,
129 gpsnamespace: widget.namespace !== null ? widget.namespace : undefined,
130 gpslimit: widget.limit,
131 ppprop: 'disambiguation'
132 };
133 if ( widget.showRedirectTargets ) {
134 params.redirects = true;
135 }
136 if ( widget.showImages ) {
137 params.prop.push( 'pageimages' );
138 params.pithumbsize = 80;
139 params.pilimit = widget.limit;
140 }
141 if ( widget.showDescriptions ) {
142 params.prop.push( 'pageterms' );
143 params.wbptterms = 'description';
144 }
145 req = new mw.Api().get( params );
146 promiseAbortObject.abort = req.abort.bind( req ); // TODO ew
147 return req.then( function ( ret ) {
148 if ( ret.query === undefined ) {
149 ret = new mw.Api().get( { action: 'query', titles: query } );
150 promiseAbortObject.abort = ret.abort.bind( ret );
151 }
152 return ret;
153 } );
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 option widgets from the server response
165 *
166 * @param {Object} data Query result
167 * @return {OO.ui.OptionWidget[]} Menu items
168 */
169 mw.widgets.TitleWidget.prototype.getOptionsFromData = function ( data ) {
170 var i, len, index, pageExists, pageExistsExact, suggestionPage, page, redirect, redirects,
171 currentPageName = new mw.Title( mw.config.get( 'wgRelevantPageName' ) ).getPrefixedText(),
172 items = [],
173 titles = [],
174 titleObj = mw.Title.newFromText( this.getQueryValue() ),
175 redirectsTo = {},
176 pageData = {};
177
178 if ( data.redirects ) {
179 for ( i = 0, len = data.redirects.length; i < len; i++ ) {
180 redirect = data.redirects[ i ];
181 redirectsTo[ redirect.to ] = redirectsTo[ redirect.to ] || [];
182 redirectsTo[ redirect.to ].push( redirect.from );
183 }
184 }
185
186 for ( index in data.pages ) {
187 suggestionPage = data.pages[ index ];
188 // When excludeCurrentPage is set, don't list the current page unless the user has type the full title
189 if ( this.excludeCurrentPage && suggestionPage.title === currentPageName && suggestionPage.title !== titleObj.getPrefixedText() ) {
190 continue;
191 }
192 pageData[ suggestionPage.title ] = {
193 known: suggestionPage.known !== undefined,
194 missing: suggestionPage.missing !== undefined,
195 redirect: suggestionPage.redirect !== undefined,
196 disambiguation: OO.getProp( suggestionPage, 'pageprops', 'disambiguation' ) !== undefined,
197 imageUrl: OO.getProp( suggestionPage, 'thumbnail', 'source' ),
198 description: OO.getProp( suggestionPage, 'terms', 'description' ),
199 // Sort index
200 index: suggestionPage.index
201 };
202
203 // Throw away pages from wrong namespaces. This can happen when 'showRedirectTargets' is true
204 // and we encounter a cross-namespace redirect.
205 if ( this.namespace === null || this.namespace === suggestionPage.ns ) {
206 titles.push( suggestionPage.title );
207 }
208
209 redirects = redirectsTo[ suggestionPage.title ] || [];
210 for ( i = 0, len = redirects.length; i < len; i++ ) {
211 pageData[ redirects[ i ] ] = {
212 missing: false,
213 known: true,
214 redirect: true,
215 disambiguation: false,
216 description: mw.msg( 'mw-widgets-titleinput-description-redirect', suggestionPage.title ),
217 // Sort index, just below its target
218 index: suggestionPage.index + 0.5
219 };
220 titles.push( redirects[ i ] );
221 }
222 }
223
224 titles.sort( function ( a, b ) {
225 return pageData[ a ].index - pageData[ b ].index;
226 } );
227
228 // If not found, run value through mw.Title to avoid treating a match as a
229 // mismatch where normalisation would make them matching (bug 48476)
230
231 pageExistsExact = (
232 Object.prototype.hasOwnProperty.call( pageData, this.getQueryValue() ) &&
233 (
234 !pageData[ this.getQueryValue() ].missing ||
235 pageData[ this.getQueryValue() ].known
236 )
237 );
238 pageExists = pageExistsExact || (
239 titleObj &&
240 Object.prototype.hasOwnProperty.call( pageData, titleObj.getPrefixedText() ) &&
241 (
242 !pageData[ titleObj.getPrefixedText() ].missing ||
243 pageData[ titleObj.getPrefixedText() ].known
244 )
245 );
246
247 if ( this.cache ) {
248 this.cache.set( pageData );
249 }
250
251 // Offer the exact text as a suggestion if the page exists
252 if ( pageExists && !pageExistsExact ) {
253 titles.unshift( this.getQueryValue() );
254 }
255
256 for ( i = 0, len = titles.length; i < len; i++ ) {
257 page = pageData[ titles[ i ] ] || {};
258 items.push( new mw.widgets.TitleOptionWidget( this.getOptionWidgetData( titles[ i ], page ) ) );
259 }
260
261 return items;
262 };
263
264 /**
265 * Get menu option widget data from the title and page data
266 *
267 * @param {string} title Title object
268 * @param {Object} data Page data
269 * @return {Object} Data for option widget
270 */
271 mw.widgets.TitleWidget.prototype.getOptionWidgetData = function ( title, data ) {
272 var mwTitle = new mw.Title( title ),
273 description = data.description;
274 if ( data.missing && !description ) {
275 description = mw.msg( 'mw-widgets-titleinput-description-new-page' );
276 }
277 return {
278 data: this.namespace !== null && this.relative ?
279 mwTitle.getRelativeText( this.namespace ) :
280 title,
281 url: mwTitle.getUrl(),
282 imageUrl: this.showImages ? data.imageUrl : null,
283 description: this.showDescriptions ? description : null,
284 missing: data.missing,
285 redirect: data.redirect,
286 disambiguation: data.disambiguation,
287 query: this.getQueryValue()
288 };
289 };
290
291 /**
292 * Get title object corresponding to given value, or #getQueryValue if not given.
293 *
294 * @param {string} [value] Value to get a title for
295 * @return {mw.Title|null} Title object, or null if value is invalid
296 */
297 mw.widgets.TitleWidget.prototype.getTitle = function ( value ) {
298 var title = value !== undefined ? value : this.getQueryValue(),
299 // mw.Title doesn't handle null well
300 titleObj = mw.Title.newFromText( title, this.namespace !== null ? this.namespace : undefined );
301
302 return titleObj;
303 };
304
305 /**
306 * Check if the query is valid
307 *
308 * @return {boolean} The query is valid
309 */
310 mw.widgets.TitleWidget.prototype.isQueryValid = function () {
311 return this.validateTitle ? !!this.getTitle() : true;
312 };
313
314 }( jQuery, mediaWiki ) );