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