Merge "Add MediaWiki-Timestamp header to ResourceLoader requests"
[mediawiki.git] / resources / src / mediawiki.widgets / mw.widgets.TitleInputWidget.js
blobca8c400fcb5bfd3a270b7fb57f123f57e4944a32
1 /*!
2  * MediaWiki Widgets - TitleInputWidget 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          * Creates an mw.widgets.TitleInputWidget object.
10          *
11          * @class
12          * @extends OO.ui.TextInputWidget
13          * @mixins OO.ui.mixin.LookupElement
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 {boolean} [relative=true] If a namespace is set, return a title relative to it
20          * @cfg {boolean} [showRedirectTargets=true] Show the targets of redirects
21          * @cfg {boolean} [showRedlink] Show red link to exact match if it doesn't exist
22          * @cfg {boolean} [showImages] Show page images
23          * @cfg {boolean} [showDescriptions] Show page descriptions
24          * @cfg {Object} [cache] Result cache which implements a 'set' method, taking keyed values as an argument
25          */
26         mw.widgets.TitleInputWidget = function MwWidgetsTitleInputWidget( config ) {
27                 var widget = this;
29                 // Config initialization
30                 config = config || {};
32                 // Parent constructor
33                 OO.ui.TextInputWidget.call( this, $.extend( {}, config, { autocomplete: false } ) );
35                 // Mixin constructors
36                 OO.ui.mixin.LookupElement.call( this, config );
38                 // Properties
39                 this.limit = config.limit || 10;
40                 this.namespace = config.namespace || null;
41                 this.relative = config.relative !== undefined ? config.relative : true;
42                 this.showRedirectTargets = config.showRedirectTargets !== false;
43                 this.showRedlink = !!config.showRedlink;
44                 this.showImages = !!config.showImages;
45                 this.showDescriptions = !!config.showDescriptions;
46                 this.cache = config.cache;
48                 // Initialization
49                 this.$element.addClass( 'mw-widget-titleInputWidget' );
50                 this.lookupMenu.$element.addClass( 'mw-widget-titleInputWidget-menu' );
51                 if ( this.showImages ) {
52                         this.lookupMenu.$element.addClass( 'mw-widget-titleInputWidget-menu-withImages' );
53                 }
54                 if ( this.showDescriptions ) {
55                         this.lookupMenu.$element.addClass( 'mw-widget-titleInputWidget-menu-withDescriptions' );
56                 }
58                 this.interwikiPrefixes = [];
59                 this.interwikiPrefixesPromise = new mw.Api().get( {
60                         action: 'query',
61                         meta: 'siteinfo',
62                         siprop: 'interwikimap'
63                 } ).done( function ( data ) {
64                         $.each( data.query.interwikimap, function ( index, interwiki ) {
65                                 widget.interwikiPrefixes.push( interwiki.prefix );
66                         } );
67                 } );
68         };
70         /* Inheritance */
72         OO.inheritClass( mw.widgets.TitleInputWidget, OO.ui.TextInputWidget );
74         OO.mixinClass( mw.widgets.TitleInputWidget, OO.ui.mixin.LookupElement );
76         /* Methods */
78         /**
79          * @inheritdoc
80          */
81         mw.widgets.TitleInputWidget.prototype.onLookupMenuItemChoose = function ( item ) {
82                 this.closeLookupMenu();
83                 this.setLookupsDisabled( true );
84                 this.setValue( item.getData() );
85                 this.setLookupsDisabled( false );
86         };
88         /**
89          * @inheritdoc
90          */
91         mw.widgets.TitleInputWidget.prototype.focus = function () {
92                 var retval;
94                 // Prevent programmatic focus from opening the menu
95                 this.setLookupsDisabled( true );
97                 // Parent method
98                 retval = OO.ui.TextInputWidget.prototype.focus.apply( this, arguments );
100                 this.setLookupsDisabled( false );
102                 return retval;
103         };
105         /**
106          * @inheritdoc
107          */
108         mw.widgets.TitleInputWidget.prototype.getLookupRequest = function () {
109                 var req,
110                         widget = this,
111                         promiseAbortObject = { abort: function () {
112                                 // Do nothing. This is just so OOUI doesn't break due to abort being undefined.
113                         } };
115                 if ( mw.Title.newFromText( this.value ) ) {
116                         return this.interwikiPrefixesPromise.then( function () {
117                                 var params, props,
118                                         interwiki = widget.value.substring( 0, widget.value.indexOf( ':' ) );
119                                 if (
120                                         interwiki && interwiki !== '' &&
121                                         widget.interwikiPrefixes.indexOf( interwiki ) !== -1
122                                 ) {
123                                         return $.Deferred().resolve( { query: {
124                                                 pages: [{
125                                                         title: widget.value
126                                                 }]
127                                         } } ).promise( promiseAbortObject );
128                                 } else {
129                                         params = {
130                                                 action: 'query',
131                                                 generator: 'prefixsearch',
132                                                 gpssearch: widget.value,
133                                                 gpsnamespace: widget.namespace !== null ? widget.namespace : undefined,
134                                                 gpslimit: widget.limit,
135                                                 ppprop: 'disambiguation'
136                                         };
137                                         props = [ 'info', 'pageprops' ];
138                                         if ( widget.showRedirectTargets ) {
139                                                 params.redirects = '1';
140                                         }
141                                         if ( widget.showImages ) {
142                                                 props.push( 'pageimages' );
143                                                 params.pithumbsize = 80;
144                                                 params.pilimit = widget.limit;
145                                         }
146                                         if ( widget.showDescriptions ) {
147                                                 props.push( 'pageterms' );
148                                                 params.wbptterms = 'description';
149                                         }
150                                         params.prop = props.join( '|' );
151                                         req = new mw.Api().get( params );
152                                         promiseAbortObject.abort = req.abort.bind( req ); // todo: ew
153                                         return req;
154                                 }
155                         } ).promise( promiseAbortObject );
156                 } else {
157                         // Don't send invalid titles to the API.
158                         // Just pretend it returned nothing so we can show the 'invalid title' section
159                         return $.Deferred().resolve( {} ).promise( promiseAbortObject );
160                 }
161         };
163         /**
164          * Get lookup cache item from server response data.
165          *
166          * @method
167          * @param {Mixed} data Response from server
168          */
169         mw.widgets.TitleInputWidget.prototype.getLookupCacheDataFromResponse = function ( data ) {
170                 return data.query || {};
171         };
173         /**
174          * Get list of menu items from a server response.
175          *
176          * @param {Object} data Query result
177          * @returns {OO.ui.MenuOptionWidget[]} Menu items
178          */
179         mw.widgets.TitleInputWidget.prototype.getLookupMenuOptionsFromData = function ( data ) {
180                 var i, len, index, pageExists, pageExistsExact, suggestionPage, page, redirect, redirects,
181                         items = [],
182                         titles = [],
183                         titleObj = mw.Title.newFromText( this.value ),
184                         redirectsTo = {},
185                         pageData = {};
187                 if ( data.redirects ) {
188                         for ( i = 0, len = data.redirects.length; i < len; i++ ) {
189                                 redirect = data.redirects[i];
190                                 redirectsTo[redirect.to] = redirectsTo[redirect.to] || [];
191                                 redirectsTo[redirect.to].push( redirect.from );
192                         }
193                 }
195                 for ( index in data.pages ) {
196                         suggestionPage = data.pages[index];
197                         pageData[suggestionPage.title] = {
198                                 missing: suggestionPage.missing !== undefined,
199                                 redirect: suggestionPage.redirect !== undefined,
200                                 disambiguation: OO.getProp( suggestionPage, 'pageprops', 'disambiguation' ) !== undefined,
201                                 imageUrl: OO.getProp( suggestionPage, 'thumbnail', 'source' ),
202                                 description: OO.getProp( suggestionPage, 'terms', 'description' )
203                         };
204                         titles.push( suggestionPage.title );
206                         redirects = redirectsTo[suggestionPage.title] || [];
207                         for ( i = 0, len = redirects.length; i < len; i++ ) {
208                                 pageData[redirects[i]] = {
209                                         missing: false,
210                                         redirect: true,
211                                         disambiguation: false,
212                                         description: mw.msg( 'mw-widgets-titleinput-description-redirect', suggestionPage.title )
213                                 };
214                                 titles.push( redirects[i] );
215                         }
216                 }
218                 // If not found, run value through mw.Title to avoid treating a match as a
219                 // mismatch where normalisation would make them matching (bug 48476)
221                 pageExistsExact = titles.indexOf( this.value ) !== -1;
222                 pageExists = pageExistsExact || (
223                         titleObj && titles.indexOf( titleObj.getPrefixedText() ) !== -1
224                 );
226                 if ( !pageExists ) {
227                         pageData[this.value] = {
228                                 missing: true, redirect: false, disambiguation: false,
229                                 description: mw.msg( 'mw-widgets-titleinput-description-new-page' )
230                         };
231                 }
233                 if ( this.cache ) {
234                         this.cache.set( pageData );
235                 }
237                 // Offer the exact text as a suggestion if the page exists
238                 if ( pageExists && !pageExistsExact ) {
239                         titles.unshift( this.value );
240                 }
241                 // Offer the exact text as a new page if the title is valid
242                 if ( this.showRedlink && !pageExists && titleObj ) {
243                         titles.push( this.value );
244                 }
245                 for ( i = 0, len = titles.length; i < len; i++ ) {
246                         page = pageData[titles[i]] || {};
247                         items.push( new mw.widgets.TitleOptionWidget( this.getOptionWidgetData( titles[i], page ) ) );
248                 }
250                 return items;
251         };
253         /**
254          * Get menu option widget data from the title and page data
255          *
256          * @param {mw.Title} title Title object
257          * @param {Object} data Page data
258          * @return {Object} Data for option widget
259          */
260         mw.widgets.TitleInputWidget.prototype.getOptionWidgetData = function ( title, data ) {
261                 var mwTitle = new mw.Title( title );
262                 return {
263                         data: this.namespace !== null && this.relative
264                                 ? mwTitle.getRelativeText( this.namespace )
265                                 : title,
266                         imageUrl: this.showImages ? data.imageUrl : null,
267                         description: this.showDescriptions ? data.description : null,
268                         missing: data.missing,
269                         redirect: data.redirect,
270                         disambiguation: data.disambiguation,
271                         query: this.value
272                 };
273         };
275         /**
276          * Get title object corresponding to #getValue
277          *
278          * @returns {mw.Title|null} Title object, or null if value is invalid
279          */
280         mw.widgets.TitleInputWidget.prototype.getTitle = function () {
281                 var title = this.getValue(),
282                         // mw.Title doesn't handle null well
283                         titleObj = mw.Title.newFromText( title, this.namespace !== null ? this.namespace : undefined );
285                 return titleObj;
286         };
288         /**
289          * @inheritdoc
290          */
291         mw.widgets.TitleInputWidget.prototype.isValid = function () {
292                 return $.Deferred().resolve( !!this.getTitle() ).promise();
293         };
295 }( jQuery, mediaWiki ) );