Merge "registration: Only allow one extension to set a specific config setting"
[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 ( $, mw ) {
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 } );
160
161 return promise;
162 };
163
164 /**
165 * @inheritdoc mw.widgets.TitleInputWidget
166 */
167 mw.widgets.SearchInputWidget.prototype.getLookupCacheDataFromResponse = function ( response ) {
168 var resp;
169
170 // mw.widgets.TitleInputWidget uses response.query, which doesn't exist for opensearch,
171 // so return the whole response (titles only, and links)
172 resp = {
173 data: response || {},
174 metadata: {
175 type: this.requestType || 'unknown',
176 query: this.getQueryValue()
177 }
178 };
179 this.requestType = undefined;
180
181 return resp;
182 };
183
184 /**
185 * @inheritdoc mw.widgets.TitleWidget
186 */
187 mw.widgets.SearchInputWidget.prototype.getOptionsFromData = function ( data ) {
188 var items = [],
189 titles = data.data[ 1 ],
190 descriptions = data.data[ 2 ],
191 urls = data.data[ 3 ],
192 self = this;
193
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 query: data.metadata.query,
218 inputLocation: this.dataLocation || 'header'
219 } );
220
221 return items;
222 };
223
224 /**
225 * @inheritdoc
226 */
227 mw.widgets.SearchInputWidget.prototype.onLookupMenuItemChoose = function () {
228 mw.widgets.SearchInputWidget.parent.prototype.onLookupMenuItemChoose.apply( this, arguments );
229
230 if ( this.performSearchOnClick ) {
231 this.$element.closest( 'form' ).submit();
232 }
233 };
234
235 /**
236 * @inheritdoc
237 */
238 mw.widgets.SearchInputWidget.prototype.getLookupMenuOptionsFromData = function () {
239 var items = mw.widgets.SearchInputWidget.parent.prototype.getLookupMenuOptionsFromData.apply(
240 this, arguments
241 );
242
243 this.lastLookupItems = items.map( function ( item ) {
244 return item.data;
245 } );
246
247 return items;
248 };
249
250 }( jQuery, mediaWiki ) );