(bug 17808) (bug 21167) use real links for search suggestions
authorMatmaRex <matma.rex@gmail.com>
Thu, 1 Nov 2012 13:16:41 +0000 (14:16 +0100)
committerGerrit Code Review <gerrit@wikimedia.org>
Fri, 18 Jan 2013 20:46:48 +0000 (20:46 +0000)
Use real links for search suggestions. This allows the viewer to, for
example, open the search results in a new tab or right-click them
to copy link's address.

We hook up the result.render callback and wrap the entire container
element (.suggestions-results <div>) in an <a>. This is permitted in HTML5
and appears to work even on IE 6. (It has some quirks related to e.g.
backgrounds handling, but thankfully none of them apply here, as we just
need it to be properly clickable - see http://jsbin.com/ejurub/2 for
a minimal test.)

We do the same for the special.render callback, although it has to be
handled slightly differently due to inconsistencies in $.suggestions that
would require a rewrite to fix.

We have to do some pretty ugly mangling to determine the links' hrefs,
but well, that's life.

This required some changes to $.suggestions:
* pass the necessary data to the render callback
* do not interfere with clicks with a mouse button other than left
  or with modifier keys active, to allow for the standard link behavior
  described above
* adjusting the width of suggestions container and applying autoellipsis
  should be done regardless of the render callback being used or not
* add some flexibility to $.suggestions.highlight, so it doesn't break
  down when DOM is modified

Also did some good-to-have changes to $.suggestions:
* detect suggestions elements in various places by their classes, not by
  them being <div>s

Change-Id: I87940ca86a2b3776969cbcee8cdf93e3c66b0cd9

resources/Resources.php
resources/jquery/jquery.suggestions.js
resources/mediawiki/mediawiki.searchSuggest.css [new file with mode: 0644]
resources/mediawiki/mediawiki.searchSuggest.js

index f279c84..442215c 100644 (file)
@@ -658,6 +658,7 @@ return array(
        ),
        'mediawiki.searchSuggest' => array(
                'scripts' => 'resources/mediawiki/mediawiki.searchSuggest.js',
+               'styles' => 'resources/mediawiki/mediawiki.searchSuggest.css',
                'messages' => array(
                        'searchsuggest-search',
                        'searchsuggest-containing',
index ac579db..303b18f 100644 (file)
@@ -113,7 +113,7 @@ $.suggestions = {
                        setTimeout( function () {
                                // Render special
                                var $special = context.data.$container.find( '.suggestions-special' );
-                               context.config.special.render.call( $special, context.data.$textbox.val() );
+                               context.config.special.render.call( $special, context.data.$textbox.val(), context );
                        }, 1 );
                }
        },
@@ -125,7 +125,7 @@ $.suggestions = {
         */
        configure: function ( context, property, value ) {
                var newCSS,
-                       $autoEllipseMe, $result, $results, $span,
+                       $autoEllipseMe, $result, $results, childrenWidth,
                        i, expWidth, matchedText, maxWidth, text;
 
                // Validate creation using fallback values
@@ -237,33 +237,34 @@ $.suggestions = {
                                                                        context.data.selectedWithMouse = true;
                                                                        $.suggestions.highlight(
                                                                                context,
-                                                                               $(this).closest( '.suggestions-results div' ),
+                                                                               $(this).closest( '.suggestions-results .suggestions-result' ),
                                                                                false
                                                                        );
                                                                } )
                                                                .appendTo( $results );
                                                        // Allow custom rendering
                                                        if ( typeof context.config.result.render === 'function' ) {
-                                                               context.config.result.render.call( $result, context.config.suggestions[i] );
+                                                               context.config.result.render.call( $result, context.config.suggestions[i], context );
                                                        } else {
                                                                // Add <span> with text
-                                                               if( context.config.highlightInput ) {
-                                                                       matchedText = context.data.prevText;
-                                                               }
                                                                $result.append( $( '<span>' )
                                                                                .css( 'whiteSpace', 'nowrap' )
                                                                                .text( text )
                                                                        );
+                                                       }
 
-                                                               // Widen results box if needed
-                                                               // New width is only calculated here, applied later
-                                                               $span = $result.children( 'span' );
-                                                               if ( $span.outerWidth() > $result.width() && $span.outerWidth() > expWidth ) {
-                                                                       // factor in any padding, margin, or border space on the parent
-                                                                       expWidth = $span.outerWidth() + ( context.data.$container.width() - $span.parent().width());
-                                                               }
-                                                               $autoEllipseMe = $autoEllipseMe.add( $result );
+                                                       if ( context.config.highlightInput ) {
+                                                               matchedText = context.data.prevText;
                                                        }
+
+                                                       // Widen results box if needed
+                                                       // New width is only calculated here, applied later
+                                                       childrenWidth = $result.children().outerWidth();
+                                                       if ( childrenWidth > $result.width() && childrenWidth > expWidth ) {
+                                                               // factor in any padding, margin, or border space on the parent
+                                                               expWidth = childrenWidth + ( context.data.$container.width() - $result.width() );
+                                                       }
+                                                       $autoEllipseMe = $autoEllipseMe.add( $result );
                                                }
                                                // Apply new width for results box, if any
                                                if ( expWidth > context.data.$container.width() ) {
@@ -309,25 +310,35 @@ $.suggestions = {
                                        result = context.data.$container.find( '.suggestions-result:last' );
                                } else {
                                        result = selected.prev();
+                                       if ( !( result.length && result.hasClass( '.suggestions-result' ) ) ) {
+                                               // there is something in the DOM between selected element and the wrapper, bypass it
+                                               result = selected.parents( '.suggestions-results > *' ).prev().find( '.suggestions-result' ).eq(0);
+                                       }
+
                                        if ( selected.length === 0 ) {
                                                // we are at the beginning, so lets jump to the last item
                                                if ( context.data.$container.find( '.suggestions-special' ).html() !== '' ) {
                                                        result = context.data.$container.find( '.suggestions-special' );
                                                } else {
-                                                       result = context.data.$container.find( '.suggestions-results div:last' );
+                                                       result = context.data.$container.find( '.suggestions-results .suggestions-result:last' );
                                                }
                                        }
                                }
                        } else if ( result === 'next' ) {
                                if ( selected.length === 0 ) {
                                        // No item selected, go to the first one
-                                       result = context.data.$container.find( '.suggestions-results div:first' );
+                                       result = context.data.$container.find( '.suggestions-results .suggestions-result:first' );
                                        if ( result.length === 0 && context.data.$container.find( '.suggestions-special' ).html() !== '' ) {
                                                // No suggestion exists, go to the special one directly
                                                result = context.data.$container.find( '.suggestions-special' );
                                        }
                                } else {
                                        result = selected.next();
+                                       if ( !( result.length && result.hasClass( '.suggestions-result' ) ) ) {
+                                               // there is something in the DOM between selected element and the wrapper, bypass it
+                                               result = selected.parents( '.suggestions-results > *' ).next().find( '.suggestions-result' ).eq(0);
+                                       }
+
                                        if ( selected.is( '.suggestions-special' ) ) {
                                                result = $( [] );
                                        } else if (
@@ -503,21 +514,25 @@ $.fn.suggestions = function () {
                                                // textbox loses focus. Instead, listen for a mousedown followed
                                                // by a mouseup on the same div.
                                                .mousedown( function ( e ) {
-                                                       context.data.mouseDownOn = $( e.target ).closest( '.suggestions-results div' );
+                                                       context.data.mouseDownOn = $( e.target ).closest( '.suggestions-results .suggestions-result' );
                                                } )
                                                .mouseup( function ( e ) {
-                                                       var $result = $( e.target ).closest( '.suggestions-results div' ),
+                                                       var $result = $( e.target ).closest( '.suggestions-results .suggestions-result' ),
                                                                $other = context.data.mouseDownOn;
 
                                                        context.data.mouseDownOn = $( [] );
                                                        if ( $result.get( 0 ) !== $other.get( 0 ) ) {
                                                                return;
                                                        }
-                                                       $.suggestions.highlight( context, $result, true );
-                                                       context.data.$container.hide();
-                                                       if ( typeof context.config.result.select === 'function' ) {
-                                                               context.config.result.select.call( $result, context.data.$textbox );
+                                                       // do not interfere with non-left clicks or if modifier keys are pressed (e.g. ctrl-click)
+                                                       if ( !( e.which !== 1 || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey ) ) {
+                                                               $.suggestions.highlight( context, $result, true );
+                                                               context.data.$container.hide();
+                                                               if ( typeof context.config.result.select === 'function' ) {
+                                                                       context.config.result.select.call( $result, context.data.$textbox );
+                                                               }
                                                        }
+                                                       // but still restore focus to the textbox, so that the suggestions will be hidden properly
                                                        context.data.$textbox.focus();
                                                } )
                                )
@@ -537,10 +552,14 @@ $.fn.suggestions = function () {
                                                        if ( $special.get( 0 ) !== $other.get( 0 ) ) {
                                                                return;
                                                        }
-                                                       context.data.$container.hide();
-                                                       if ( typeof context.config.special.select === 'function' ) {
-                                                               context.config.special.select.call( $special, context.data.$textbox );
+                                                       // do not interfere with non-left clicks or if modifier keys are pressed (e.g. ctrl-click)
+                                                       if ( !( e.which !== 1 || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey ) ) {
+                                                               context.data.$container.hide();
+                                                               if ( typeof context.config.special.select === 'function' ) {
+                                                                       context.config.special.select.call( $special, context.data.$textbox );
+                                                               }
                                                        }
+                                                       // but still restore focus to the textbox, so that the suggestions will be hidden properly
                                                        context.data.$textbox.focus();
                                                } )
                                                .mousemove( function ( e ) {
diff --git a/resources/mediawiki/mediawiki.searchSuggest.css b/resources/mediawiki/mediawiki.searchSuggest.css
new file mode 100644 (file)
index 0000000..0fb862b
--- /dev/null
@@ -0,0 +1,16 @@
+/* Make sure the links are not underlined or colored, ever. */
+/* There is already a :focus / :hover indication on the <div>. */
+.suggestions a.mw-searchSuggest-link,
+.suggestions a.mw-searchSuggest-link:hover,
+.suggestions a.mw-searchSuggest-link:active,
+.suggestions a.mw-searchSuggest-link:focus {
+       text-decoration: none;
+       color: black;
+}
+
+.suggestions-result-current a.mw-searchSuggest-link,
+.suggestions-result-current a.mw-searchSuggest-link:hover,
+.suggestions-result-current a.mw-searchSuggest-link:active,
+.suggestions-result-current a.mw-searchSuggest-link:focus {
+       color: white;
+}
index 99a5557..2bc7cea 100644 (file)
@@ -3,7 +3,7 @@
  */
 ( function ( mw, $ ) {
        $( document ).ready( function ( $ ) {
-               var map, searchboxesSelectors,
+               var map, resultRenderCache, searchboxesSelectors,
                        // Region where the suggestions box will appear directly below
                        // (using the same width). Can be a container element or the input
                        // itself, depending on what suits best in the environment.
                        return;
                }
 
+               // Compute form data for search suggestions functionality.
+               function computeResultRenderCache( context ) {
+                       var $form, formAction, baseHref, linkParams;
+
+                       // Compute common parameters for links' hrefs
+                       $form = context.config.$region.closest( 'form' );
+
+                       formAction = $form.attr( 'action' );
+                       baseHref = formAction + ( formAction.match(/\?/) ? '&' : '?' );
+
+                       linkParams = {};
+                       $.each( $form.serializeArray(), function ( idx, obj ) {
+                               linkParams[ obj.name ] = obj.value;
+                       } );
+
+                       return {
+                               textParam: context.data.$textbox.attr( 'name' ),
+                               linkParams: linkParams,
+                               baseHref: baseHref
+                       };
+               }
+
+               // The function used to render the suggestions.
+               function renderFunction( text, context ) {
+                       if ( !resultRenderCache ) {
+                               resultRenderCache = computeResultRenderCache( context );
+                       }
+
+                       // linkParams object is modified and reused
+                       resultRenderCache.linkParams[ resultRenderCache.textParam ] = text;
+
+                       // this is the container <div>, jQueryfied
+                       this
+                               .append(
+                                       // the <span> is needed for $.autoEllipsis to work
+                                       $( '<span>' )
+                                               .css( 'whiteSpace', 'nowrap' )
+                                               .text( text )
+                               )
+                               .wrap(
+                                       $( '<a>' )
+                                               .attr( 'href', resultRenderCache.baseHref + $.param( resultRenderCache.linkParams ) )
+                                               .addClass( 'mw-searchSuggest-link' )
+                               );
+               }
+
+               function specialRenderFunction( query, context ) {
+                       var $el = this;
+
+                       if ( !resultRenderCache ) {
+                               resultRenderCache = computeResultRenderCache( context );
+                       }
+
+                       // linkParams object is modified and reused
+                       resultRenderCache.linkParams[ resultRenderCache.textParam ] = query;
+
+                       if ( $el.children().length === 0 ) {
+                               $el
+                                       .append(
+                                               $( '<div>' )
+                                                       .addClass( 'special-label' )
+                                                       .text( mw.msg( 'searchsuggest-containing' ) ),
+                                               $( '<div>' )
+                                                       .addClass( 'special-query' )
+                                                       .text( query )
+                                                       .autoEllipsis()
+                                       )
+                                       .show();
+                       } else {
+                               $el.find( '.special-query' )
+                                       .text( query )
+                                       .autoEllipsis();
+                       }
+
+                       if ( $el.parent().hasClass( 'mw-searchSuggest-link' ) ) {
+                               $el.parent().attr( 'href', resultRenderCache.baseHref + $.param( resultRenderCache.linkParams ) + '&fulltext=1' );
+                       } else {
+                               $el.wrap(
+                                       $( '<a>' )
+                                               .attr( 'href', resultRenderCache.baseHref + $.param( resultRenderCache.linkParams ) + '&fulltext=1' )
+                                               .addClass( 'mw-searchSuggest-link' )
+                               );
+                       }
+               }
+
                // General suggestions functionality for all search boxes
                searchboxesSelectors = [
                        // Primary searchbox on every page in standard skins
                                        }
                                },
                                result: {
+                                       render: renderFunction,
                                        select: function ( $input ) {
                                                $input.closest( 'form' ).submit();
                                        }
                // Special suggestions functionality for skin-provided search box
                $searchInput.suggestions( {
                        result: {
+                               render: renderFunction,
                                select: function ( $input ) {
                                        $input.closest( 'form' ).submit();
                                }
                        },
                        special: {
-                               render: function ( query ) {
-                                       var $el = this;
-                                       if ( $el.children().length === 0 ) {
-                                               $el
-                                                       .append(
-                                                               $( '<div>' )
-                                                                       .addClass( 'special-label' )
-                                                                       .text( mw.msg( 'searchsuggest-containing' ) ),
-                                                               $( '<div>' )
-                                                                       .addClass( 'special-query' )
-                                                                       .text( query )
-                                                                       .autoEllipsis()
-                                                       )
-                                                       .show();
-                                       } else {
-                                               $el.find( '.special-query' )
-                                                       .text( query )
-                                                       .autoEllipsis();
-                                       }
-                               },
+                               render: specialRenderFunction,
                                select: function ( $input ) {
                                        $input.closest( 'form' ).append(
                                                $( '<input type="hidden" name="fulltext" value="1"/>' )