Merge "Skin: Make skins aware of their registered skin name"
[lhc/web/wiklou.git] / resources / src / mediawiki / mediawiki.searchSuggest.js
1 /*!
2 * Add search suggestions to the search form.
3 */
4 ( function ( mw, $ ) {
5 var searchNS = $.map( mw.config.get( 'wgFormattedNamespaces' ), function ( nsName, nsID ) {
6 if ( nsID >= 0 && mw.user.options.get( 'searchNs' + nsID ) ) {
7 // Cast string key to number
8 return Number( nsID );
9 }
10 } );
11 mw.searchSuggest = {
12 // queries the wiki and calls response with the result
13 request: function ( api, query, response, maxRows, namespace ) {
14 return api.get( {
15 formatversion: 2,
16 action: 'opensearch',
17 search: query,
18 namespace: namespace || searchNS,
19 limit: maxRows,
20 suggest: true
21 } ).done( function ( data, jqXHR ) {
22 response( data[ 1 ], {
23 type: jqXHR.getResponseHeader( 'X-OpenSearch-Type' ),
24 query: query
25 } );
26 } );
27 }
28 };
29
30 $( function () {
31 var api, searchboxesSelectors,
32 // Region where the suggestions box will appear directly below
33 // (using the same width). Can be a container element or the input
34 // itself, depending on what suits best in the environment.
35 // For Vector the suggestion box should align with the simpleSearch
36 // container's borders, in other skins it should align with the input
37 // element (not the search form, as that would leave the buttons
38 // vertically between the input and the suggestions).
39 $searchRegion = $( '#simpleSearch, #searchInput' ).first(),
40 $searchInput = $( '#searchInput' ),
41 previousSearchText = $searchInput.val();
42
43 // Compute form data for search suggestions functionality.
44 function getFormData( context ) {
45 var $form, baseHref, linkParams;
46
47 if ( !context.formData ) {
48 // Compute common parameters for links' hrefs
49 $form = context.config.$region.closest( 'form' );
50
51 baseHref = $form.attr( 'action' );
52 baseHref += baseHref.indexOf( '?' ) > -1 ? '&' : '?';
53
54 linkParams = $form.serializeObject();
55
56 context.formData = {
57 textParam: context.data.$textbox.attr( 'name' ),
58 linkParams: linkParams,
59 baseHref: baseHref
60 };
61 }
62
63 return context.formData;
64 }
65
66 /**
67 * Callback that's run when the user changes the search input text
68 * 'this' is the search input box (jQuery object)
69 *
70 * @ignore
71 */
72 function onBeforeUpdate() {
73 var searchText = this.val();
74
75 if ( searchText && searchText !== previousSearchText ) {
76 mw.track( 'mediawiki.searchSuggest', {
77 action: 'session-start'
78 } );
79 }
80 previousSearchText = searchText;
81 }
82
83 /**
84 * Defines the location of autocomplete. Typically either
85 * header, which is in the top right of vector (for example)
86 * and content which identifies the main search bar on
87 * Special:Search. Defaults to header for skins that don't set
88 * explicitly.
89 *
90 * @ignore
91 * @param {Object} context
92 * @return {string}
93 */
94 function getInputLocation( context ) {
95 return context.config.$region
96 .closest( 'form' )
97 .find( '[data-search-loc]' )
98 .data( 'search-loc' ) || 'header';
99 }
100
101 /**
102 * Callback that's run when suggestions have been updated either from the cache or the API
103 * 'this' is the search input box (jQuery object)
104 *
105 * @ignore
106 * @param {Object} metadata
107 */
108 function onAfterUpdate( metadata ) {
109 var context = this.data( 'suggestionsContext' );
110
111 mw.track( 'mediawiki.searchSuggest', {
112 action: 'impression-results',
113 numberOfResults: context.config.suggestions.length,
114 resultSetType: metadata.type || 'unknown',
115 query: metadata.query,
116 inputLocation: getInputLocation( context )
117 } );
118 }
119
120 // The function used to render the suggestions.
121 function renderFunction( text, context ) {
122 var formData = getFormData( context ),
123 textboxConfig = context.data.$textbox.data( 'mw-searchsuggest' ) || {};
124
125 // linkParams object is modified and reused
126 formData.linkParams[ formData.textParam ] = text;
127
128 // Allow trackers to attach tracking information, such
129 // as wprov, to clicked links.
130 mw.track( 'mediawiki.searchSuggest', {
131 action: 'render-one',
132 formData: formData,
133 index: context.config.suggestions.indexOf( text )
134 } );
135
136 // this is the container <div>, jQueryfied
137 this.text( text );
138
139 // wrap only as link, if the config doesn't disallow it
140 if ( textboxConfig.wrapAsLink !== false ) {
141 this.wrap(
142 $( '<a>' )
143 .attr( 'href', formData.baseHref + $.param( formData.linkParams ) )
144 .attr( 'title', text )
145 .addClass( 'mw-searchSuggest-link' )
146 );
147 }
148 }
149
150 // The function used when the user makes a selection
151 function selectFunction( $input, source ) {
152 var context = $input.data( 'suggestionsContext' ),
153 text = $input.val();
154
155 // Selecting via keyboard triggers a form submission. That will fire
156 // the submit-form event in addition to this click-result event.
157 if ( source !== 'keyboard' ) {
158 mw.track( 'mediawiki.searchSuggest', {
159 action: 'click-result',
160 numberOfResults: context.config.suggestions.length,
161 index: context.config.suggestions.indexOf( text )
162 } );
163 }
164
165 // allow the form to be submitted
166 return true;
167 }
168
169 function specialRenderFunction( query, context ) {
170 var $el = this,
171 formData = getFormData( context );
172
173 // linkParams object is modified and reused
174 formData.linkParams[ formData.textParam ] = query;
175
176 mw.track( 'mediawiki.searchSuggest', {
177 action: 'render-one',
178 formData: formData,
179 index: context.config.suggestions.indexOf( query )
180 } );
181
182 if ( $el.children().length === 0 ) {
183 $el
184 .append(
185 $( '<div>' )
186 .addClass( 'special-label' )
187 .text( mw.msg( 'searchsuggest-containing' ) ),
188 $( '<div>' )
189 .addClass( 'special-query' )
190 .text( query )
191 )
192 .show();
193 } else {
194 $el.find( '.special-query' )
195 .text( query );
196 }
197
198 if ( $el.parent().hasClass( 'mw-searchSuggest-link' ) ) {
199 $el.parent().attr( 'href', formData.baseHref + $.param( formData.linkParams ) + '&fulltext=1' );
200 } else {
201 $el.wrap(
202 $( '<a>' )
203 .attr( 'href', formData.baseHref + $.param( formData.linkParams ) + '&fulltext=1' )
204 .addClass( 'mw-searchSuggest-link' )
205 );
206 }
207 }
208
209 // Generic suggestions functionality for all search boxes
210 searchboxesSelectors = [
211 // Primary searchbox on every page in standard skins
212 '#searchInput',
213 // Generic selector for skins with multiple searchboxes (used by CologneBlue)
214 // and for MediaWiki itself (special pages with page title inputs)
215 '.mw-searchInput'
216 ];
217 $( searchboxesSelectors.join( ', ' ) )
218 .suggestions( {
219 fetch: function ( query, response, maxRows ) {
220 var node = this[ 0 ];
221
222 api = api || new mw.Api();
223
224 $.data( node, 'request', mw.searchSuggest.request( api, query, response, maxRows ) );
225 },
226 cancel: function () {
227 var node = this[ 0 ],
228 request = $.data( node, 'request' );
229
230 if ( request ) {
231 request.abort();
232 $.removeData( node, 'request' );
233 }
234 },
235 result: {
236 render: renderFunction,
237 select: function () {
238 // allow the form to be submitted
239 return true;
240 }
241 },
242 update: {
243 before: onBeforeUpdate,
244 after: onAfterUpdate
245 },
246 cache: true,
247 highlightInput: true
248 } )
249 .on( 'paste cut drop', function () {
250 // make sure paste and cut events from the mouse and drag&drop events
251 // trigger the keypress handler and cause the suggestions to update
252 $( this ).trigger( 'keypress' );
253 } )
254 // In most skins (at least Monobook and Vector), the font-size is messed up in <body>.
255 // (they use 2 elements to get a sane font-height). So, instead of making exceptions for
256 // each skin or adding more stylesheets, just copy it from the active element so auto-fit.
257 .each( function () {
258 var $this = $( this );
259 $this
260 .data( 'suggestions-context' )
261 .data.$container.css( 'fontSize', $this.css( 'fontSize' ) );
262 } );
263
264 // Ensure that the thing is actually present!
265 if ( $searchRegion.length === 0 ) {
266 // Don't try to set anything up if simpleSearch is disabled sitewide.
267 // The loader code loads us if the option is present, even if we're
268 // not actually enabled (anymore).
269 return;
270 }
271
272 // Special suggestions functionality and tracking for skin-provided search box
273 $searchInput.suggestions( {
274 update: {
275 before: onBeforeUpdate,
276 after: onAfterUpdate
277 },
278 result: {
279 render: renderFunction,
280 select: selectFunction
281 },
282 special: {
283 render: specialRenderFunction,
284 select: function ( $input, source ) {
285 var context = $input.data( 'suggestionsContext' ),
286 text = $input.val();
287 if ( source === 'mouse' ) {
288 // mouse click won't trigger form submission, so we need to send a click event
289 mw.track( 'mediawiki.searchSuggest', {
290 action: 'click-result',
291 numberOfResults: context.config.suggestions.length,
292 index: context.config.suggestions.indexOf( text )
293 } );
294 } else {
295 $input.closest( 'form' )
296 .append( $( '<input type="hidden" name="fulltext" value="1"/>' ) );
297 }
298 return true; // allow the form to be submitted
299 }
300 },
301 $region: $searchRegion
302 } );
303
304 $searchInput.closest( 'form' )
305 // track the form submit event
306 .on( 'submit', function () {
307 var context = $searchInput.data( 'suggestionsContext' );
308 mw.track( 'mediawiki.searchSuggest', {
309 action: 'submit-form',
310 numberOfResults: context.config.suggestions.length,
311 $form: context.config.$region.closest( 'form' ),
312 inputLocation: getInputLocation( context ),
313 index: context.config.suggestions.indexOf(
314 context.data.$textbox.val()
315 )
316 } );
317 } )
318 // If the form includes any fallback fulltext search buttons, remove them
319 .find( '.mw-fallbackSearchButton' ).remove();
320 } );
321
322 }( mediaWiki, jQuery ) );