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