Merge "Import: Handle uploads with sha1 starting with 0 properly"
[mediawiki.git] / resources / src / mediawiki.widgets / mw.widgets.TitleWidget.js
blobabe1228cf43a185cac5f4328dc2a47608578f366
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 ) {
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} [validateTitle=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;
32                 // Config initialization
33                 config = $.extend( {
34                         maxLength: 255,
35                         limit: 10
36                 }, config );
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.validateTitle = config.validateTitle !== undefined ? config.validateTitle : true;
49                 this.cache = config.cache;
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         };
65         /* Setup */
67         OO.initClass( mw.widgets.TitleWidget );
69         /* Methods */
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;
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         };
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         };
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                         } };
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         };
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 = {};
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                 }
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                         };
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                         }
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                 }
208                 titles.sort( function ( a, b ) {
209                         return pageData[ a ].index - pageData[ b ].index;
210                 } );
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)
215                 pageExistsExact = titles.indexOf( this.getQueryValue() ) !== -1;
216                 pageExists = pageExistsExact || (
217                         titleObj && titles.indexOf( titleObj.getPrefixedText() ) !== -1
218                 );
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                 }
227                 if ( this.cache ) {
228                         this.cache.set( pageData );
229                 }
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                 }
244                 return items;
245         };
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         };
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 );
281                 return titleObj;
282         };
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.validateTitle ? !!this.getTitle() : true;
291         };
293 }( jQuery, mediaWiki ) );