Merge "WatchAction: Require POST for index.php action=watch"
[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.interwikiPrefixes = [];
51 this.interwikiPrefixesPromise = new mw.Api().get( {
52 action: 'query',
53 meta: 'siteinfo',
54 siprop: 'interwikimap'
55 } ).done( function ( data ) {
56 $.each( data.query.interwikimap, function ( index, interwiki ) {
57 widget.interwikiPrefixes.push( interwiki.prefix );
58 } );
59 } );
60 };
61
62 /* Setup */
63
64 OO.initClass( mw.widgets.TitleWidget );
65
66 /* Methods */
67
68 /**
69 * Get the current value of the search query
70 *
71 * @abstract
72 * @return {string} Search query
73 */
74 mw.widgets.TitleWidget.prototype.getQueryValue = null;
75
76 /**
77 * Get the namespace to prepend to titles in suggestions, if any.
78 *
79 * @return {number|null} Namespace number
80 */
81 mw.widgets.TitleWidget.prototype.getNamespace = function () {
82 return this.namespace;
83 };
84
85 /**
86 * Set the namespace to prepend to titles in suggestions, if any.
87 *
88 * @param {number|null} namespace Namespace number
89 */
90 mw.widgets.TitleWidget.prototype.setNamespace = function ( namespace ) {
91 this.namespace = namespace;
92 };
93
94 /**
95 * Get a promise which resolves with an API repsonse for suggested
96 * links for the current query.
97 */
98 mw.widgets.TitleWidget.prototype.getSuggestionsPromise = function () {
99 var req,
100 query = this.getQueryValue(),
101 widget = this,
102 promiseAbortObject = { abort: function () {
103 // Do nothing. This is just so OOUI doesn't break due to abort being undefined.
104 } };
105
106 if ( mw.Title.newFromText( query ) ) {
107 return this.interwikiPrefixesPromise.then( function () {
108 var params, props,
109 interwiki = query.substring( 0, query.indexOf( ':' ) );
110 if (
111 interwiki && interwiki !== '' &&
112 widget.interwikiPrefixes.indexOf( interwiki ) !== -1
113 ) {
114 return $.Deferred().resolve( { query: {
115 pages: [ {
116 title: query
117 } ]
118 } } ).promise( promiseAbortObject );
119 } else {
120 params = {
121 action: 'query',
122 generator: 'prefixsearch',
123 gpssearch: query,
124 gpsnamespace: widget.namespace !== null ? widget.namespace : undefined,
125 gpslimit: widget.limit,
126 ppprop: 'disambiguation'
127 };
128 props = [ 'info', 'pageprops' ];
129 if ( widget.showRedirectTargets ) {
130 params.redirects = '1';
131 }
132 if ( widget.showImages ) {
133 props.push( 'pageimages' );
134 params.pithumbsize = 80;
135 params.pilimit = widget.limit;
136 }
137 if ( widget.showDescriptions ) {
138 props.push( 'pageterms' );
139 params.wbptterms = 'description';
140 }
141 params.prop = props.join( '|' );
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 };
185
186 // Throw away pages from wrong namespaces. This can happen when 'showRedirectTargets' is true
187 // and we encounter a cross-namespace redirect.
188 if ( this.namespace === null || this.namespace === suggestionPage.ns ) {
189 titles.push( suggestionPage.title );
190 }
191
192 redirects = redirectsTo[ suggestionPage.title ] || [];
193 for ( i = 0, len = redirects.length; i < len; i++ ) {
194 pageData[ redirects[ i ] ] = {
195 missing: false,
196 redirect: true,
197 disambiguation: false,
198 description: mw.msg( 'mw-widgets-titleinput-description-redirect', suggestionPage.title )
199 };
200 titles.push( redirects[ i ] );
201 }
202 }
203
204 // If not found, run value through mw.Title to avoid treating a match as a
205 // mismatch where normalisation would make them matching (bug 48476)
206
207 pageExistsExact = titles.indexOf( this.getQueryValue() ) !== -1;
208 pageExists = pageExistsExact || (
209 titleObj && titles.indexOf( titleObj.getPrefixedText() ) !== -1
210 );
211
212 if ( !pageExists ) {
213 pageData[ this.getQueryValue() ] = {
214 missing: true, redirect: false, disambiguation: false,
215 description: mw.msg( 'mw-widgets-titleinput-description-new-page' )
216 };
217 }
218
219 if ( this.cache ) {
220 this.cache.set( pageData );
221 }
222
223 // Offer the exact text as a suggestion if the page exists
224 if ( pageExists && !pageExistsExact ) {
225 titles.unshift( this.getQueryValue() );
226 }
227 // Offer the exact text as a new page if the title is valid
228 if ( this.showRedlink && !pageExists && titleObj ) {
229 titles.push( this.getQueryValue() );
230 }
231 for ( i = 0, len = titles.length; i < len; i++ ) {
232 page = pageData[ titles[ i ] ] || {};
233 items.push( new mw.widgets.TitleOptionWidget( this.getOptionWidgetData( titles[ i ], page ) ) );
234 }
235
236 return items;
237 };
238
239 /**
240 * Get menu option widget data from the title and page data
241 *
242 * @param {mw.Title} title Title object
243 * @param {Object} data Page data
244 * @return {Object} Data for option widget
245 */
246 mw.widgets.TitleWidget.prototype.getOptionWidgetData = function ( title, data ) {
247 var mwTitle = new mw.Title( title );
248 return {
249 data: this.namespace !== null && this.relative
250 ? mwTitle.getRelativeText( this.namespace )
251 : title,
252 title: mwTitle,
253 imageUrl: this.showImages ? data.imageUrl : null,
254 description: this.showDescriptions ? data.description : null,
255 missing: data.missing,
256 redirect: data.redirect,
257 disambiguation: data.disambiguation,
258 query: this.getQueryValue()
259 };
260 };
261
262 /**
263 * Get title object corresponding to given value, or #getQueryValue if not given.
264 *
265 * @param {string} [value] Value to get a title for
266 * @returns {mw.Title|null} Title object, or null if value is invalid
267 */
268 mw.widgets.TitleWidget.prototype.getTitle = function ( value ) {
269 var title = value !== undefined ? value : this.getQueryValue(),
270 // mw.Title doesn't handle null well
271 titleObj = mw.Title.newFromText( title, this.namespace !== null ? this.namespace : undefined );
272
273 return titleObj;
274 };
275
276 /**
277 * Check if the query is valid
278 *
279 * @return {boolean} The query is valid
280 */
281 mw.widgets.TitleWidget.prototype.isQueryValid = function () {
282 return !!this.getTitle();
283 };
284
285 }( jQuery, mediaWiki ) );