Merge "Give instructions on removing email address from account"
[mediawiki.git] / resources / src / mediawiki.widgets / mw.widgets.TitleWidget.js
blob672b54a24ae8e4078a277e281d005b9c53f9e136
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, 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;
31                 // Config initialization
32                 config = $.extend( {
33                         maxLength: 255,
34                         limit: 10
35                 }, config );
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;
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         };
63         /* Setup */
65         OO.initClass( mw.widgets.TitleWidget );
67         /* Methods */
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;
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         };
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         };
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                         } };
107                 if ( mw.Title.newFromText( query ) ) {
108                         return this.interwikiPrefixesPromise.then( function () {
109                                 var params, props,
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                                                 generator: 'prefixsearch',
124                                                 gpssearch: query,
125                                                 gpsnamespace: widget.namespace !== null ? widget.namespace : undefined,
126                                                 gpslimit: widget.limit,
127                                                 ppprop: 'disambiguation'
128                                         };
129                                         props = [ 'info', 'pageprops' ];
130                                         if ( widget.showRedirectTargets ) {
131                                                 params.redirects = '1';
132                                         }
133                                         if ( widget.showImages ) {
134                                                 props.push( 'pageimages' );
135                                                 params.pithumbsize = 80;
136                                                 params.pilimit = widget.limit;
137                                         }
138                                         if ( widget.showDescriptions ) {
139                                                 props.push( 'pageterms' );
140                                                 params.wbptterms = 'description';
141                                         }
142                                         params.prop = props.join( '|' );
143                                         req = new mw.Api().get( params );
144                                         promiseAbortObject.abort = req.abort.bind( req ); // todo: ew
145                                         return req;
146                                 }
147                         } ).promise( promiseAbortObject );
148                 } else {
149                         // Don't send invalid titles to the API.
150                         // Just pretend it returned nothing so we can show the 'invalid title' section
151                         return $.Deferred().resolve( {} ).promise( promiseAbortObject );
152                 }
153         };
155         /**
156          * Get option widgets from the server response
157          *
158          * @param {Object} data Query result
159          * @returns {OO.ui.OptionWidget[]} Menu items
160          */
161         mw.widgets.TitleWidget.prototype.getOptionsFromData = function ( data ) {
162                 var i, len, index, pageExists, pageExistsExact, suggestionPage, page, redirect, redirects,
163                         items = [],
164                         titles = [],
165                         titleObj = mw.Title.newFromText( this.getQueryValue() ),
166                         redirectsTo = {},
167                         pageData = {};
169                 if ( data.redirects ) {
170                         for ( i = 0, len = data.redirects.length; i < len; i++ ) {
171                                 redirect = data.redirects[ i ];
172                                 redirectsTo[ redirect.to ] = redirectsTo[ redirect.to ] || [];
173                                 redirectsTo[ redirect.to ].push( redirect.from );
174                         }
175                 }
177                 for ( index in data.pages ) {
178                         suggestionPage = data.pages[ index ];
179                         pageData[ suggestionPage.title ] = {
180                                 missing: suggestionPage.missing !== undefined,
181                                 redirect: suggestionPage.redirect !== undefined,
182                                 disambiguation: OO.getProp( suggestionPage, 'pageprops', 'disambiguation' ) !== undefined,
183                                 imageUrl: OO.getProp( suggestionPage, 'thumbnail', 'source' ),
184                                 description: OO.getProp( suggestionPage, 'terms', 'description' )
185                         };
187                         // Throw away pages from wrong namespaces. This can happen when 'showRedirectTargets' is true
188                         // and we encounter a cross-namespace redirect.
189                         if ( this.namespace === null || this.namespace === suggestionPage.ns ) {
190                                 titles.push( suggestionPage.title );
191                         }
193                         redirects = redirectsTo[ suggestionPage.title ] || [];
194                         for ( i = 0, len = redirects.length; i < len; i++ ) {
195                                 pageData[ redirects[ i ] ] = {
196                                         missing: false,
197                                         redirect: true,
198                                         disambiguation: false,
199                                         description: mw.msg( 'mw-widgets-titleinput-description-redirect', suggestionPage.title )
200                                 };
201                                 titles.push( redirects[ i ] );
202                         }
203                 }
205                 // If not found, run value through mw.Title to avoid treating a match as a
206                 // mismatch where normalisation would make them matching (bug 48476)
208                 pageExistsExact = titles.indexOf( this.getQueryValue() ) !== -1;
209                 pageExists = pageExistsExact || (
210                         titleObj && titles.indexOf( titleObj.getPrefixedText() ) !== -1
211                 );
213                 if ( !pageExists ) {
214                         pageData[ this.getQueryValue() ] = {
215                                 missing: true, redirect: false, disambiguation: false,
216                                 description: mw.msg( 'mw-widgets-titleinput-description-new-page' )
217                         };
218                 }
220                 if ( this.cache ) {
221                         this.cache.set( pageData );
222                 }
224                 // Offer the exact text as a suggestion if the page exists
225                 if ( pageExists && !pageExistsExact ) {
226                         titles.unshift( this.getQueryValue() );
227                 }
228                 // Offer the exact text as a new page if the title is valid
229                 if ( this.showRedlink && !pageExists && titleObj ) {
230                         titles.push( this.getQueryValue() );
231                 }
232                 for ( i = 0, len = titles.length; i < len; i++ ) {
233                         page = pageData[ titles[ i ] ] || {};
234                         items.push( new mw.widgets.TitleOptionWidget( this.getOptionWidgetData( titles[ i ], page ) ) );
235                 }
237                 return items;
238         };
240         /**
241          * Get menu option widget data from the title and page data
242          *
243          * @param {string} title Title object
244          * @param {Object} data Page data
245          * @return {Object} Data for option widget
246          */
247         mw.widgets.TitleWidget.prototype.getOptionWidgetData = function ( title, data ) {
248                 var mwTitle = new mw.Title( title );
249                 return {
250                         data: this.namespace !== null && this.relative
251                                 ? mwTitle.getRelativeText( this.namespace )
252                                 : title,
253                         url: mwTitle.getUrl(),
254                         imageUrl: this.showImages ? data.imageUrl : null,
255                         description: this.showDescriptions ? data.description : null,
256                         missing: data.missing,
257                         redirect: data.redirect,
258                         disambiguation: data.disambiguation,
259                         query: this.getQueryValue()
260                 };
261         };
263         /**
264          * Get title object corresponding to given value, or #getQueryValue if not given.
265          *
266          * @param {string} [value] Value to get a title for
267          * @returns {mw.Title|null} Title object, or null if value is invalid
268          */
269         mw.widgets.TitleWidget.prototype.getTitle = function ( value ) {
270                 var title = value !== undefined ? value : this.getQueryValue(),
271                         // mw.Title doesn't handle null well
272                         titleObj = mw.Title.newFromText( title, this.namespace !== null ? this.namespace : undefined );
274                 return titleObj;
275         };
277         /**
278          * Check if the query is valid
279          *
280          * @return {boolean} The query is valid
281          */
282         mw.widgets.TitleWidget.prototype.isQueryValid = function () {
283                 return !!this.getTitle();
284         };
286 }( jQuery, mediaWiki ) );