Merge "maintenance: Script to rename titles for Unicode uppercasing changes"
[lhc/web/wiklou.git] / resources / src / mediawiki.widgets / mw.widgets.SearchInputWidget.js
1 /*!
2 * MediaWiki Widgets - SearchInputWidget 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 () {
8
9 /**
10 * Creates a mw.widgets.SearchInputWidget object.
11 *
12 * @class
13 * @extends mw.widgets.TitleInputWidget
14 *
15 * @constructor
16 * @param {Object} [config] Configuration options
17 * @cfg {boolean} [performSearchOnClick=true] If true, the script will start a search when-
18 * ever a user hits a suggestion. If false, the text of the suggestion is inserted into the
19 * text field only.
20 * @cfg {string} [dataLocation='header'] Where the search input field will be
21 * used (header or content).
22 */
23 mw.widgets.SearchInputWidget = function MwWidgetsSearchInputWidget( config ) {
24 // The parent constructors will detach this from the DOM, and won't
25 // be reattached until after this function is completed. As such
26 // grab a handle here. If no config.$input is passed tracking of
27 // form submissions won't work.
28 var $form = config.$input ? config.$input.closest( 'form' ) : $();
29
30 config = $.extend( {
31 icon: 'search',
32 maxLength: undefined,
33 showPendingRequest: false,
34 performSearchOnClick: true,
35 dataLocation: 'header'
36 }, config );
37
38 // Parent constructor
39 mw.widgets.SearchInputWidget.parent.call( this, config );
40
41 // Initialization
42 this.$element.addClass( 'mw-widget-searchInputWidget' );
43 this.lookupMenu.$element.addClass( 'mw-widget-searchWidget-menu' );
44 this.lastLookupItems = [];
45 if ( config.dataLocation ) {
46 this.dataLocation = config.dataLocation;
47 }
48 if ( config.performSearchOnClick ) {
49 this.performSearchOnClick = config.performSearchOnClick;
50 }
51 this.setLookupsDisabled( !this.suggestions );
52
53 $form.on( 'submit', function () {
54 mw.track( 'mw.widgets.SearchInputWidget', {
55 action: 'submit-form',
56 numberOfResults: this.lastLookupItems.length,
57 $form: $form,
58 inputLocation: this.dataLocation || 'header',
59 index: this.lastLookupItems.indexOf(
60 this.$input.val()
61 )
62 } );
63 }.bind( this ) );
64
65 this.connect( this, {
66 change: 'onChange'
67 } );
68
69 this.$element.addClass( 'oo-ui-textInputWidget-type-search' );
70 this.updateSearchIndicator();
71 this.connect( this, {
72 disable: 'onDisable'
73 } );
74 };
75
76 /* Setup */
77
78 OO.inheritClass( mw.widgets.SearchInputWidget, mw.widgets.TitleInputWidget );
79
80 /* Methods */
81
82 /**
83 * @inheritdoc
84 * @protected
85 */
86 mw.widgets.SearchInputWidget.prototype.getInputElement = function () {
87 return $( '<input>' ).attr( 'type', 'search' );
88 };
89
90 /**
91 * @inheritdoc
92 */
93 mw.widgets.SearchInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
94 if ( e.which === OO.ui.MouseButtons.LEFT ) {
95 // Clear the text field
96 this.setValue( '' );
97 this.$input[ 0 ].focus();
98 return false;
99 }
100 };
101
102 /**
103 * Update the 'clear' indicator displayed on type: 'search' text
104 * fields, hiding it when the field is already empty or when it's not
105 * editable.
106 */
107 mw.widgets.SearchInputWidget.prototype.updateSearchIndicator = function () {
108 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
109 this.setIndicator( null );
110 } else {
111 this.setIndicator( 'clear' );
112 }
113 };
114
115 /**
116 * @see OO.ui.SearchInputWidget#onChange
117 */
118 mw.widgets.SearchInputWidget.prototype.onChange = function () {
119 this.updateSearchIndicator();
120 };
121
122 /**
123 * Handle disable events.
124 *
125 * @param {boolean} disabled Element is disabled
126 * @private
127 */
128 mw.widgets.SearchInputWidget.prototype.onDisable = function () {
129 this.updateSearchIndicator();
130 };
131
132 /**
133 * @inheritdoc
134 */
135 mw.widgets.SearchInputWidget.prototype.setReadOnly = function ( state ) {
136 mw.widgets.SearchInputWidget.parent.prototype.setReadOnly.call( this, state );
137 this.updateSearchIndicator();
138 return this;
139 };
140
141 /**
142 * @inheritdoc mw.widgets.TitleWidget
143 */
144 mw.widgets.SearchInputWidget.prototype.getSuggestionsPromise = function () {
145 var api = this.getApi(),
146 promise,
147 self = this;
148
149 // reuse the searchSuggest function from mw.searchSuggest
150 promise = mw.searchSuggest.request( api, this.getQueryValue(), function () {}, this.limit, this.getNamespace() );
151
152 // tracking purposes
153 promise.done( function ( data, jqXHR ) {
154 self.requestType = jqXHR.getResponseHeader( 'X-OpenSearch-Type' );
155 self.searchId = jqXHR.getResponseHeader( 'X-Search-ID' );
156 } );
157
158 return promise;
159 };
160
161 /**
162 * @inheritdoc mw.widgets.TitleInputWidget
163 */
164 mw.widgets.SearchInputWidget.prototype.getLookupCacheDataFromResponse = function ( response ) {
165 var resp;
166
167 // mw.widgets.TitleInputWidget uses response.query, which doesn't exist for opensearch,
168 // so return the whole response (titles only, and links)
169 resp = {
170 data: response || {},
171 metadata: {
172 type: this.requestType || 'unknown',
173 searchId: this.searchId || null,
174 query: this.getQueryValue()
175 }
176 };
177 this.requestType = undefined;
178 this.searchId = undefined;
179
180 return resp;
181 };
182
183 /**
184 * @inheritdoc mw.widgets.TitleWidget
185 */
186 mw.widgets.SearchInputWidget.prototype.getOptionsFromData = function ( data ) {
187 var items = [],
188 titles = data.data[ 1 ],
189 descriptions = data.data[ 2 ],
190 urls = data.data[ 3 ],
191 self = this;
192
193 // eslint-disable-next-line no-jquery/no-each-util
194 $.each( titles, function ( i, result ) {
195 items.push( new mw.widgets.TitleOptionWidget(
196 self.getOptionWidgetData(
197 result,
198 // Create a result object that looks like the one from
199 // the parent's API query.
200 {
201 data: result,
202 url: urls[ i ],
203 imageUrl: null, // The JSON 'opensearch' API doesn't have images
204 description: descriptions[ i ],
205 missing: false,
206 redirect: false,
207 disambiguation: false
208 }
209 )
210 ) );
211 } );
212
213 mw.track( 'mw.widgets.SearchInputWidget', {
214 action: 'impression-results',
215 numberOfResults: items.length,
216 resultSetType: data.metadata.type,
217 searchId: data.metadata.searchId,
218 query: data.metadata.query,
219 inputLocation: this.dataLocation || 'header'
220 } );
221
222 return items;
223 };
224
225 /**
226 * @inheritdoc
227 */
228 mw.widgets.SearchInputWidget.prototype.onLookupMenuItemChoose = function () {
229 mw.widgets.SearchInputWidget.parent.prototype.onLookupMenuItemChoose.apply( this, arguments );
230
231 if ( this.performSearchOnClick ) {
232 this.$element.closest( 'form' ).trigger( 'submit' );
233 }
234 };
235
236 /**
237 * @inheritdoc
238 */
239 mw.widgets.SearchInputWidget.prototype.getLookupMenuOptionsFromData = function () {
240 var items = mw.widgets.SearchInputWidget.parent.prototype.getLookupMenuOptionsFromData.apply(
241 this, arguments
242 );
243
244 this.lastLookupItems = items.map( function ( item ) {
245 return item.data;
246 } );
247
248 return items;
249 };
250
251 }() );