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