Merge "SearchInputWidget: Pass through description data"
[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.$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 mw.widgets.SearchInputWidget.parent.prototype.onChange.call( this );
120 this.updateSearchIndicator();
121 };
122
123 /**
124 * Handle disable events.
125 *
126 * @param {boolean} disabled Element is disabled
127 * @private
128 */
129 mw.widgets.SearchInputWidget.prototype.onDisable = function () {
130 this.updateSearchIndicator();
131 };
132
133 /**
134 * @inheritdoc
135 */
136 mw.widgets.SearchInputWidget.prototype.setReadOnly = function ( state ) {
137 mw.widgets.SearchInputWidget.parent.prototype.setReadOnly.call( this, state );
138 this.updateSearchIndicator();
139 return this;
140 };
141
142 /**
143 * @inheritdoc mw.widgets.TitleWidget
144 */
145 mw.widgets.SearchInputWidget.prototype.getSuggestionsPromise = function () {
146 var api = this.getApi(),
147 promise,
148 self = this;
149
150 // reuse the searchSuggest function from mw.searchSuggest
151 promise = mw.searchSuggest.request( api, this.getQueryValue(), $.noop, this.limit, this.getNamespace() );
152
153 // tracking purposes
154 promise.done( function ( data, jqXHR ) {
155 self.requestType = jqXHR.getResponseHeader( 'X-OpenSearch-Type' );
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 query: this.getQueryValue()
174 }
175 };
176 this.requestType = undefined;
177
178 return resp;
179 };
180
181 /**
182 * @inheritdoc mw.widgets.TitleWidget
183 */
184 mw.widgets.SearchInputWidget.prototype.getOptionsFromData = function ( data ) {
185 var items = [],
186 titles = data.data[ 1 ],
187 descriptions = data.data[ 2 ],
188 urls = data.data[ 3 ],
189 self = this;
190
191 $.each( titles, function ( i, result ) {
192 items.push( new mw.widgets.TitleOptionWidget(
193 self.getOptionWidgetData(
194 result,
195 // Create a result object that looks like the one from
196 // the parent's API query.
197 {
198 data: result,
199 url: urls[ i ],
200 imageUrl: null, // The JSON 'opensearch' API doesn't have images
201 description: descriptions[ i ],
202 missing: false,
203 redirect: false,
204 disambiguation: false
205 }
206 )
207 ) );
208 } );
209
210 mw.track( 'mw.widgets.SearchInputWidget', {
211 action: 'impression-results',
212 numberOfResults: items.length,
213 resultSetType: data.metadata.type,
214 query: data.metadata.query,
215 inputLocation: this.dataLocation || 'header'
216 } );
217
218 return items;
219 };
220
221 /**
222 * @inheritdoc
223 */
224 mw.widgets.SearchInputWidget.prototype.onLookupMenuItemChoose = function () {
225 mw.widgets.SearchInputWidget.parent.prototype.onLookupMenuItemChoose.apply( this, arguments );
226
227 if ( this.performSearchOnClick ) {
228 this.$element.closest( 'form' ).submit();
229 }
230 };
231
232 /**
233 * @inheritdoc
234 */
235 mw.widgets.SearchInputWidget.prototype.getLookupMenuOptionsFromData = function () {
236 var items = mw.widgets.SearchInputWidget.parent.prototype.getLookupMenuOptionsFromData.apply(
237 this, arguments
238 );
239
240 this.lastLookupItems = items.map( function ( item ) {
241 return item.data;
242 } );
243
244 return items;
245 };
246
247 }( jQuery, mediaWiki ) );