From 0ee84eedfa4abbe1621a27ea046afef35161e97c Mon Sep 17 00:00:00 2001 From: MatmaRex Date: Thu, 1 Nov 2012 14:16:41 +0100 Subject: [PATCH] (bug 17808) (bug 21167) use real links for search suggestions 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
) in an . 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
s Change-Id: I87940ca86a2b3776969cbcee8cdf93e3c66b0cd9 --- resources/Resources.php | 1 + resources/jquery/jquery.suggestions.js | 71 ++++++----- .../mediawiki/mediawiki.searchSuggest.css | 16 +++ .../mediawiki/mediawiki.searchSuggest.js | 110 ++++++++++++++---- 4 files changed, 151 insertions(+), 47 deletions(-) create mode 100644 resources/mediawiki/mediawiki.searchSuggest.css diff --git a/resources/Resources.php b/resources/Resources.php index f279c848f5..442215c2c1 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -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', diff --git a/resources/jquery/jquery.suggestions.js b/resources/jquery/jquery.suggestions.js index ac579db1d6..303b18f1c5 100644 --- a/resources/jquery/jquery.suggestions.js +++ b/resources/jquery/jquery.suggestions.js @@ -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 with text - if( context.config.highlightInput ) { - matchedText = context.data.prevText; - } $result.append( $( '' ) .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 index 0000000000..0fb862b944 --- /dev/null +++ b/resources/mediawiki/mediawiki.searchSuggest.css @@ -0,0 +1,16 @@ +/* Make sure the links are not underlined or colored, ever. */ +/* There is already a :focus / :hover indication on the
. */ +.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; +} diff --git a/resources/mediawiki/mediawiki.searchSuggest.js b/resources/mediawiki/mediawiki.searchSuggest.js index 99a55576e7..2bc7cea9eb 100644 --- a/resources/mediawiki/mediawiki.searchSuggest.js +++ b/resources/mediawiki/mediawiki.searchSuggest.js @@ -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. @@ -41,6 +41,91 @@ 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
, jQueryfied + this + .append( + // the is needed for $.autoEllipsis to work + $( '' ) + .css( 'whiteSpace', 'nowrap' ) + .text( text ) + ) + .wrap( + $( '' ) + .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( + $( '
' ) + .addClass( 'special-label' ) + .text( mw.msg( 'searchsuggest-containing' ) ), + $( '
' ) + .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( + $( '' ) + .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 @@ -89,6 +174,7 @@ } }, result: { + render: renderFunction, select: function ( $input ) { $input.closest( 'form' ).submit(); } @@ -118,31 +204,13 @@ // 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( - $( '
' ) - .addClass( 'special-label' ) - .text( mw.msg( 'searchsuggest-containing' ) ), - $( '
' ) - .addClass( 'special-query' ) - .text( query ) - .autoEllipsis() - ) - .show(); - } else { - $el.find( '.special-query' ) - .text( query ) - .autoEllipsis(); - } - }, + render: specialRenderFunction, select: function ( $input ) { $input.closest( 'form' ).append( $( '' ) -- 2.20.1