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