From: Fomafix Date: Wed, 29 May 2019 22:25:03 +0000 (+0200) Subject: jquery.suggestions: Remove public object $.suggestions X-Git-Tag: 1.34.0-rc.0~1464^2~1 X-Git-Url: http://git.cyclocoop.org/%22.htmlspecialchars%28%24url_syndic%29.%22?a=commitdiff_plain;h=82f5752cb0a582a3c79ef373d42706c604a15805;p=lhc%2Fweb%2Fwiklou.git jquery.suggestions: Remove public object $.suggestions Private functions are sufficient here. Also reorder function declarations to avoid forward references. Change-Id: Idf99e56b6cb1dd0730ea349818fbb2133f941b90 --- diff --git a/resources/src/jquery/jquery.suggestions.js b/resources/src/jquery/jquery.suggestions.js index f58d73369c..4a1d637de9 100644 --- a/resources/src/jquery/jquery.suggestions.js +++ b/resources/src/jquery/jquery.suggestions.js @@ -10,8 +10,6 @@ * * $( '#textbox' ).suggestions(); * - * Uses jQuery.suggestions singleton internally. - * * @class jQuery.plugin.suggestions */ @@ -95,497 +93,488 @@ var hasOwn = Object.hasOwnProperty; /** - * Used by jQuery.plugin.suggestions. + * Cancel any delayed maybeFetch() call and callback the context so + * they can cancel any async fetching if they use AJAX or something. * - * @class jQuery.suggestions - * @singleton - * @private + * @param {Object} context */ - $.suggestions = { - /** - * Cancel any delayed maybeFetch() call and callback the context so - * they can cancel any async fetching if they use AJAX or something. - * - * @param {Object} context - */ - cancel: function ( context ) { - if ( context.data.timerID !== null ) { - clearTimeout( context.data.timerID ); - } - if ( typeof context.config.cancel === 'function' ) { - context.config.cancel.call( context.data.$textbox ); + function cancel( context ) { + if ( context.data.timerID !== null ) { + clearTimeout( context.data.timerID ); + } + if ( typeof context.config.cancel === 'function' ) { + context.config.cancel.call( context.data.$textbox ); + } + } + + /** + * Hide the element with suggestions and clean up some state. + * + * @param {Object} context + */ + function hide( context ) { + // Remove any highlights, including on "special" items + context.data.$container.find( '.suggestions-result-current' ).removeClass( 'suggestions-result-current' ); + // Hide the container + context.data.$container.hide(); + } + + /** + * Restore the text the user originally typed in the textbox, before it + * was overwritten by highlight(). This restores the value the currently + * displayed suggestions are based on, rather than the value just before + * highlight() overwrote it; the former is arguably slightly more sensible. + * + * @param {Object} context + */ + function restore( context ) { + context.data.$textbox.val( context.data.prevText ); + } + + /** + * @param {Object} context + */ + function special( context ) { + // Allow custom rendering - but otherwise don't do any rendering + if ( typeof context.config.special.render === 'function' ) { + // Wait for the browser to update the value + setTimeout( function () { + // Render special + var $special = context.data.$container.find( '.suggestions-special' ); + context.config.special.render.call( $special, context.data.$textbox.val(), context ); + }, 1 ); + } + } + + /** + * Ask the user-specified callback for new suggestions. Any previous delayed + * call to this function still pending will be canceled. If the value in the + * textbox is empty or hasn't changed since the last time suggestions were fetched, + * this function does nothing. + * + * @param {Object} context + * @param {boolean} delayed Whether or not to delay this by the currently configured amount of time + */ + function update( context, delayed ) { + function maybeFetch() { + var val = context.data.$textbox.val(), + cache = context.data.cache, + cacheHit; + + if ( typeof context.config.update.before === 'function' ) { + context.config.update.before.call( context.data.$textbox ); } - }, - - /** - * Hide the element with suggestions and clean up some state. - * - * @param {Object} context - */ - hide: function ( context ) { - // Remove any highlights, including on "special" items - context.data.$container.find( '.suggestions-result-current' ).removeClass( 'suggestions-result-current' ); - // Hide the container - context.data.$container.hide(); - }, - - /** - * Restore the text the user originally typed in the textbox, before it - * was overwritten by highlight(). This restores the value the currently - * displayed suggestions are based on, rather than the value just before - * highlight() overwrote it; the former is arguably slightly more sensible. - * - * @param {Object} context - */ - restore: function ( context ) { - context.data.$textbox.val( context.data.prevText ); - }, - - /** - * Ask the user-specified callback for new suggestions. Any previous delayed - * call to this function still pending will be canceled. If the value in the - * textbox is empty or hasn't changed since the last time suggestions were fetched, - * this function does nothing. - * - * @param {Object} context - * @param {boolean} delayed Whether or not to delay this by the currently configured amount of time - */ - update: function ( context, delayed ) { - function maybeFetch() { - var val = context.data.$textbox.val(), - cache = context.data.cache, - cacheHit; - - if ( typeof context.config.update.before === 'function' ) { - context.config.update.before.call( context.data.$textbox ); - } - // Only fetch if the value in the textbox changed and is not empty, or if the results were hidden - // if the textbox is empty then clear the result div, but leave other settings intouched - if ( val.length === 0 ) { - $.suggestions.hide( context ); - context.data.prevText = ''; - } else if ( - val !== context.data.prevText || - !context.data.$container.is( ':visible' ) - ) { - context.data.prevText = val; - // Try cache first - if ( context.config.cache && hasOwn.call( cache, val ) ) { - if ( mw.now() - cache[ val ].timestamp < context.config.cacheMaxAge ) { - context.data.$textbox.suggestions( 'suggestions', cache[ val ].suggestions ); + // Only fetch if the value in the textbox changed and is not empty, or if the results were hidden + // if the textbox is empty then clear the result div, but leave other settings intouched + if ( val.length === 0 ) { + hide( context ); + context.data.prevText = ''; + } else if ( + val !== context.data.prevText || + !context.data.$container.is( ':visible' ) + ) { + context.data.prevText = val; + // Try cache first + if ( context.config.cache && hasOwn.call( cache, val ) ) { + if ( mw.now() - cache[ val ].timestamp < context.config.cacheMaxAge ) { + context.data.$textbox.suggestions( 'suggestions', cache[ val ].suggestions ); + if ( typeof context.config.update.after === 'function' ) { + context.config.update.after.call( context.data.$textbox, cache[ val ].metadata ); + } + cacheHit = true; + } else { + // Cache expired + delete cache[ val ]; + } + } + if ( !cacheHit && typeof context.config.fetch === 'function' ) { + context.config.fetch.call( + context.data.$textbox, + val, + function ( suggestions, metadata ) { + suggestions = suggestions.slice( 0, context.config.maxRows ); + context.data.$textbox.suggestions( 'suggestions', suggestions ); if ( typeof context.config.update.after === 'function' ) { - context.config.update.after.call( context.data.$textbox, cache[ val ].metadata ); + context.config.update.after.call( context.data.$textbox, metadata ); + } + if ( context.config.cache ) { + cache[ val ] = { + suggestions: suggestions, + metadata: metadata, + timestamp: mw.now() + }; } - cacheHit = true; + }, + context.config.maxRows + ); + } + } + + // Always update special rendering + special( context ); + } + + // Cancels any delayed maybeFetch call, and invokes context.config.cancel. + cancel( context ); + + if ( delayed ) { + // To avoid many started/aborted requests while typing, we're gonna take a short + // break before trying to fetch data. + context.data.timerID = setTimeout( maybeFetch, context.config.delay ); + } else { + maybeFetch(); + } + } + + /** + * Highlight a result in the results table + * + * @param {Object} context + * @param {jQuery|string} result `` to highlight, or 'prev' or 'next' + * @param {boolean} updateTextbox If true, put the suggestion in the textbox + */ + function highlight( context, result, updateTextbox ) { + var selected = context.data.$container.find( '.suggestions-result-current' ); + if ( !result.get || selected.get( 0 ) !== result.get( 0 ) ) { + if ( result === 'prev' ) { + if ( selected.hasClass( 'suggestions-special' ) ) { + 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 { - // Cache expired - delete cache[ val ]; + result = context.data.$container.find( '.suggestions-results .suggestions-result:last' ); } } - if ( !cacheHit && typeof context.config.fetch === 'function' ) { - context.config.fetch.call( - context.data.$textbox, - val, - function ( suggestions, metadata ) { - suggestions = suggestions.slice( 0, context.config.maxRows ); - context.data.$textbox.suggestions( 'suggestions', suggestions ); - if ( typeof context.config.update.after === 'function' ) { - context.config.update.after.call( context.data.$textbox, metadata ); - } - if ( context.config.cache ) { - cache[ val ] = { - suggestions: suggestions, - metadata: metadata, - timestamp: mw.now() - }; - } - }, - context.config.maxRows - ); - } } + } else if ( result === 'next' ) { + if ( selected.length === 0 ) { + // No item selected, go to the first one + 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 ); + } - // Always update special rendering - $.suggestions.special( context ); + if ( selected.hasClass( 'suggestions-special' ) ) { + result = $( [] ); + } else if ( + result.length === 0 && + context.data.$container.find( '.suggestions-special' ).html() !== '' + ) { + // We were at the last item, jump to the specials! + result = context.data.$container.find( '.suggestions-special' ); + } + } } - - // Cancels any delayed maybeFetch call, and invokes context.config.cancel. - $.suggestions.cancel( context ); - - if ( delayed ) { - // To avoid many started/aborted requests while typing, we're gonna take a short - // break before trying to fetch data. - context.data.timerID = setTimeout( maybeFetch, context.config.delay ); + selected.removeClass( 'suggestions-result-current' ); + result.addClass( 'suggestions-result-current' ); + } + if ( updateTextbox ) { + if ( result.length === 0 || result.is( '.suggestions-special' ) ) { + restore( context ); } else { - maybeFetch(); - } - }, - - /** - * @param {Object} context - */ - special: function ( context ) { - // Allow custom rendering - but otherwise don't do any rendering - if ( typeof context.config.special.render === 'function' ) { - // Wait for the browser to update the value - setTimeout( function () { - // Render special - var $special = context.data.$container.find( '.suggestions-special' ); - context.config.special.render.call( $special, context.data.$textbox.val(), context ); - }, 1 ); + context.data.$textbox.val( result.data( 'text' ) ); + // .val() doesn't call any event handlers, so + // let the world know what happened + context.data.$textbox.trigger( 'change' ); } - }, - - /** - * Sets the value of a property, and updates the widget accordingly - * - * @param {Object} context - * @param {string} property Name of property - * @param {Mixed} value Value to set property with - */ - configure: function ( context, property, value ) { - var newCSS, - $result, $results, $spanForWidth, childrenWidth, - regionIsFixed, regionPosition, - i, expWidth, maxWidth, text; - - // Validate creation using fallback values - switch ( property ) { - case 'fetch': - case 'cancel': - case 'special': - case 'result': - case 'update': - case '$region': - case 'expandFrom': - context.config[ property ] = value; - break; - case 'suggestions': - context.config[ property ] = value; - // Update suggestions - if ( context.data !== undefined ) { - if ( context.data.$textbox.val().length === 0 ) { - // Hide the div when no suggestion exist - $.suggestions.hide( context ); - } else { - // Rebuild the suggestions list - context.data.$container.show(); - // Update the size and position of the list - regionIsFixed = ( function () { - var $el = context.config.$region; - do { - if ( $el.css( 'position' ) === 'fixed' ) { - return true; - } - $el = $( $el[ 0 ].offsetParent ); - } while ( $el.length ); - return false; - }() ); - regionPosition = regionIsFixed ? - context.config.$region[ 0 ].getBoundingClientRect() : - context.config.$region.offset(); - newCSS = { - position: regionIsFixed ? 'fixed' : 'absolute', - top: regionPosition.top + context.config.$region.outerHeight(), - bottom: 'auto', - width: context.config.$region.outerWidth(), - height: 'auto' - }; - - // Process expandFrom, after this it is set to left or right. - context.config.expandFrom = ( function ( expandFrom ) { - var regionWidth, docWidth, regionCenter, docCenter, - docDir = $( document.documentElement ).css( 'direction' ), - $region = context.config.$region; - - // Backwards compatible - if ( context.config.positionFromLeft ) { - expandFrom = 'left'; - - // Catch invalid values, default to 'auto' - } else if ( [ 'left', 'right', 'start', 'end', 'auto' ].indexOf( expandFrom ) === -1 ) { - expandFrom = 'auto'; + context.data.$textbox.trigger( 'change' ); + } + } + + /** + * Sets the value of a property, and updates the widget accordingly + * + * @param {Object} context + * @param {string} property Name of property + * @param {Mixed} value Value to set property with + */ + function configure( context, property, value ) { + var newCSS, + $result, $results, $spanForWidth, childrenWidth, + regionIsFixed, regionPosition, + i, expWidth, maxWidth, text; + + // Validate creation using fallback values + switch ( property ) { + case 'fetch': + case 'cancel': + case 'special': + case 'result': + case 'update': + case '$region': + case 'expandFrom': + context.config[ property ] = value; + break; + case 'suggestions': + context.config[ property ] = value; + // Update suggestions + if ( context.data !== undefined ) { + if ( context.data.$textbox.val().length === 0 ) { + // Hide the div when no suggestion exist + hide( context ); + } else { + // Rebuild the suggestions list + context.data.$container.show(); + // Update the size and position of the list + regionIsFixed = ( function () { + var $el = context.config.$region; + do { + if ( $el.css( 'position' ) === 'fixed' ) { + return true; } + $el = $( $el[ 0 ].offsetParent ); + } while ( $el.length ); + return false; + }() ); + regionPosition = regionIsFixed ? + context.config.$region[ 0 ].getBoundingClientRect() : + context.config.$region.offset(); + newCSS = { + position: regionIsFixed ? 'fixed' : 'absolute', + top: regionPosition.top + context.config.$region.outerHeight(), + bottom: 'auto', + width: context.config.$region.outerWidth(), + height: 'auto' + }; + + // Process expandFrom, after this it is set to left or right. + context.config.expandFrom = ( function ( expandFrom ) { + var regionWidth, docWidth, regionCenter, docCenter, + docDir = $( document.documentElement ).css( 'direction' ), + $region = context.config.$region; + + // Backwards compatible + if ( context.config.positionFromLeft ) { + expandFrom = 'left'; + + // Catch invalid values, default to 'auto' + } else if ( [ 'left', 'right', 'start', 'end', 'auto' ].indexOf( expandFrom ) === -1 ) { + expandFrom = 'auto'; + } - if ( expandFrom === 'auto' ) { - if ( $region.data( 'searchsuggest-expand-dir' ) ) { - // If the markup explicitly contains a direction, use it. - expandFrom = $region.data( 'searchsuggest-expand-dir' ); + if ( expandFrom === 'auto' ) { + if ( $region.data( 'searchsuggest-expand-dir' ) ) { + // If the markup explicitly contains a direction, use it. + expandFrom = $region.data( 'searchsuggest-expand-dir' ); + } else { + regionWidth = $region.outerWidth(); + docWidth = $( document ).width(); + if ( regionWidth > ( 0.85 * docWidth ) ) { + // If the input size takes up more than 85% of the document horizontally + // expand the suggestions to the writing direction's native end. + expandFrom = 'start'; } else { - regionWidth = $region.outerWidth(); - docWidth = $( document ).width(); - if ( regionWidth > ( 0.85 * docWidth ) ) { - // If the input size takes up more than 85% of the document horizontally - // expand the suggestions to the writing direction's native end. + // Calculate the center points of the input and document + regionCenter = regionPosition.left + regionWidth / 2; + docCenter = docWidth / 2; + if ( Math.abs( regionCenter - docCenter ) < ( 0.10 * docCenter ) ) { + // If the input's center is within 10% of the document center + // use the writing direction's native end. expandFrom = 'start'; } else { - // Calculate the center points of the input and document - regionCenter = regionPosition.left + regionWidth / 2; - docCenter = docWidth / 2; - if ( Math.abs( regionCenter - docCenter ) < ( 0.10 * docCenter ) ) { - // If the input's center is within 10% of the document center - // use the writing direction's native end. - expandFrom = 'start'; - } else { - // Otherwise expand the input from the closest side of the page, - // towards the side of the page with the most free open space - expandFrom = regionCenter > docCenter ? 'right' : 'left'; - } + // Otherwise expand the input from the closest side of the page, + // towards the side of the page with the most free open space + expandFrom = regionCenter > docCenter ? 'right' : 'left'; } } } + } - if ( expandFrom === 'start' ) { - expandFrom = docDir === 'rtl' ? 'right' : 'left'; - - } else if ( expandFrom === 'end' ) { - expandFrom = docDir === 'rtl' ? 'left' : 'right'; - } - - return expandFrom; - - }( context.config.expandFrom ) ); + if ( expandFrom === 'start' ) { + expandFrom = docDir === 'rtl' ? 'right' : 'left'; - if ( context.config.expandFrom === 'left' ) { - // Expand from left - newCSS.left = regionPosition.left; - newCSS.right = 'auto'; - } else { - // Expand from right - newCSS.left = 'auto'; - newCSS.right = $( 'body' ).width() - ( regionPosition.left + context.config.$region.outerWidth() ); + } else if ( expandFrom === 'end' ) { + expandFrom = docDir === 'rtl' ? 'left' : 'right'; } - context.data.$container.css( newCSS ); - $results = context.data.$container.children( '.suggestions-results' ); - $results.empty(); - expWidth = -1; - for ( i = 0; i < context.config.suggestions.length; i++ ) { - text = context.config.suggestions[ i ]; - $result = $( '
' ) - .addClass( 'suggestions-result' ) - .attr( 'rel', i ) - .data( 'text', context.config.suggestions[ i ] ) - .on( 'mousemove', function () { - context.data.selectedWithMouse = true; - $.suggestions.highlight( - context, - $( 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 ); - } else { - $result.text( text ); - } + return expandFrom; - if ( context.config.highlightInput ) { - $result.highlightText( context.data.prevText, { method: 'prefixHighlight' } ); - } + }( context.config.expandFrom ) ); - // Widen results box if needed (new width is only calculated here, applied later). - - // The monstrosity below accomplishes two things: - // * Wraps the text contents in a DOM element, so that we can know its width. There is - // no way to directly access the width of a text node, and we can't use the parent - // node width as it has text-overflow: ellipsis; and overflow: hidden; applied to - // it, which trims it to a smaller width. - // * Temporarily applies position: absolute; to the wrapper to pull it out of normal - // document flow. Otherwise the CSS text-overflow: ellipsis; and overflow: hidden; - // rules would cause some browsers (at least all versions of IE from 6 to 11) to - // still report the "trimmed" width. This should not be done in regular CSS - // stylesheets as we don't want this rule to apply to other elements, like - // the ones generated by jquery.highlightText. - $spanForWidth = $result.wrapInner( '' ).children(); - childrenWidth = $spanForWidth.css( 'position', 'absolute' ).outerWidth(); - $spanForWidth.contents().unwrap(); - - 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() ); - } + if ( context.config.expandFrom === 'left' ) { + // Expand from left + newCSS.left = regionPosition.left; + newCSS.right = 'auto'; + } else { + // Expand from right + newCSS.left = 'auto'; + newCSS.right = $( 'body' ).width() - ( regionPosition.left + context.config.$region.outerWidth() ); + } + + context.data.$container.css( newCSS ); + $results = context.data.$container.children( '.suggestions-results' ); + $results.empty(); + expWidth = -1; + for ( i = 0; i < context.config.suggestions.length; i++ ) { + text = context.config.suggestions[ i ]; + $result = $( '
' ) + .addClass( 'suggestions-result' ) + .attr( 'rel', i ) + .data( 'text', context.config.suggestions[ i ] ) + .on( 'mousemove', function () { + context.data.selectedWithMouse = true; + highlight( + context, + $( 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 ); + } else { + $result.text( text ); } - // Apply new width for results box, if any - if ( expWidth > context.data.$container.width() ) { - maxWidth = context.config.maxExpandFactor * context.data.$textbox.width(); - context.data.$container.width( Math.min( expWidth, maxWidth ) ); + if ( context.config.highlightInput ) { + $result.highlightText( context.data.prevText, { method: 'prefixHighlight' } ); } - } - } - break; - case 'maxRows': - context.config[ property ] = Math.max( 1, Math.min( 100, value ) ); - break; - case 'delay': - context.config[ property ] = Math.max( 0, Math.min( 1200, value ) ); - break; - case 'cacheMaxAge': - context.config[ property ] = Math.max( 1, value ); - break; - case 'maxExpandFactor': - context.config[ property ] = Math.max( 1, value ); - break; - case 'cache': - case 'submitOnClick': - case 'positionFromLeft': - case 'highlightInput': - context.config[ property ] = !!value; - break; - } - }, - - /** - * Highlight a result in the results table - * - * @param {Object} context - * @param {jQuery|string} result `` to highlight, or 'prev' or 'next' - * @param {boolean} updateTextbox If true, put the suggestion in the textbox - */ - highlight: function ( context, result, updateTextbox ) { - var selected = context.data.$container.find( '.suggestions-result-current' ); - if ( !result.get || selected.get( 0 ) !== result.get( 0 ) ) { - if ( result === 'prev' ) { - if ( selected.hasClass( 'suggestions-special' ) ) { - 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 .suggestions-result:last' ); + // Widen results box if needed (new width is only calculated here, applied later). + + // The monstrosity below accomplishes two things: + // * Wraps the text contents in a DOM element, so that we can know its width. There is + // no way to directly access the width of a text node, and we can't use the parent + // node width as it has text-overflow: ellipsis; and overflow: hidden; applied to + // it, which trims it to a smaller width. + // * Temporarily applies position: absolute; to the wrapper to pull it out of normal + // document flow. Otherwise the CSS text-overflow: ellipsis; and overflow: hidden; + // rules would cause some browsers (at least all versions of IE from 6 to 11) to + // still report the "trimmed" width. This should not be done in regular CSS + // stylesheets as we don't want this rule to apply to other elements, like + // the ones generated by jquery.highlightText. + $spanForWidth = $result.wrapInner( '' ).children(); + childrenWidth = $spanForWidth.css( 'position', 'absolute' ).outerWidth(); + $spanForWidth.contents().unwrap(); + + 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() ); } } - } - } else if ( result === 'next' ) { - if ( selected.length === 0 ) { - // No item selected, go to the first one - 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.hasClass( 'suggestions-special' ) ) { - result = $( [] ); - } else if ( - result.length === 0 && - context.data.$container.find( '.suggestions-special' ).html() !== '' - ) { - // We were at the last item, jump to the specials! - result = context.data.$container.find( '.suggestions-special' ); + // Apply new width for results box, if any + if ( expWidth > context.data.$container.width() ) { + maxWidth = context.config.maxExpandFactor * context.data.$textbox.width(); + context.data.$container.width( Math.min( expWidth, maxWidth ) ); } } } - selected.removeClass( 'suggestions-result-current' ); - result.addClass( 'suggestions-result-current' ); - } - if ( updateTextbox ) { - if ( result.length === 0 || result.is( '.suggestions-special' ) ) { - $.suggestions.restore( context ); + break; + case 'maxRows': + context.config[ property ] = Math.max( 1, Math.min( 100, value ) ); + break; + case 'delay': + context.config[ property ] = Math.max( 0, Math.min( 1200, value ) ); + break; + case 'cacheMaxAge': + context.config[ property ] = Math.max( 1, value ); + break; + case 'maxExpandFactor': + context.config[ property ] = Math.max( 1, value ); + break; + case 'cache': + case 'submitOnClick': + case 'positionFromLeft': + case 'highlightInput': + context.config[ property ] = !!value; + break; + } + } + + /** + * Respond to keypress event + * + * @param {jQuery.Event} e + * @param {Object} context + * @param {number} key Code of key pressed + */ + function keypress( e, context, key ) { + var selected, + wasVisible = context.data.$container.is( ':visible' ), + preventDefault = false; + + switch ( key ) { + // Arrow down + case 40: + if ( wasVisible ) { + highlight( context, 'next', true ); + context.data.selectedWithMouse = false; } else { - context.data.$textbox.val( result.data( 'text' ) ); - // .val() doesn't call any event handlers, so - // let the world know what happened - context.data.$textbox.trigger( 'change' ); + update( context, false ); + } + preventDefault = true; + break; + // Arrow up + case 38: + if ( wasVisible ) { + highlight( context, 'prev', true ); + context.data.selectedWithMouse = false; } + preventDefault = wasVisible; + break; + // Escape + case 27: + hide( context ); + restore( context ); + cancel( context ); context.data.$textbox.trigger( 'change' ); - } - }, - - /** - * Respond to keypress event - * - * @param {jQuery.Event} e - * @param {Object} context - * @param {number} key Code of key pressed - */ - keypress: function ( e, context, key ) { - var selected, - wasVisible = context.data.$container.is( ':visible' ), - preventDefault = false; - - switch ( key ) { - // Arrow down - case 40: - if ( wasVisible ) { - $.suggestions.highlight( context, 'next', true ); - context.data.selectedWithMouse = false; - } else { - $.suggestions.update( context, false ); - } - preventDefault = true; - break; - // Arrow up - case 38: - if ( wasVisible ) { - $.suggestions.highlight( context, 'prev', true ); - context.data.selectedWithMouse = false; - } - preventDefault = wasVisible; - break; - // Escape - case 27: - $.suggestions.hide( context ); - $.suggestions.restore( context ); - $.suggestions.cancel( context ); - context.data.$textbox.trigger( 'change' ); - preventDefault = wasVisible; - break; - // Enter - case 13: - preventDefault = wasVisible; - selected = context.data.$container.find( '.suggestions-result-current' ); - $.suggestions.hide( context ); - if ( selected.length === 0 || context.data.selectedWithMouse ) { - // If nothing is selected or if something was selected with the mouse - // cancel any current requests and allow the form to be submitted - // (simply don't prevent default behavior). - $.suggestions.cancel( context ); - preventDefault = false; - } else if ( selected.is( '.suggestions-special' ) ) { - if ( typeof context.config.special.select === 'function' ) { - // Allow the callback to decide whether to prevent default or not - if ( context.config.special.select.call( selected, context.data.$textbox, 'keyboard' ) === true ) { - preventDefault = false; - } + preventDefault = wasVisible; + break; + // Enter + case 13: + preventDefault = wasVisible; + selected = context.data.$container.find( '.suggestions-result-current' ); + hide( context ); + if ( selected.length === 0 || context.data.selectedWithMouse ) { + // If nothing is selected or if something was selected with the mouse + // cancel any current requests and allow the form to be submitted + // (simply don't prevent default behavior). + cancel( context ); + preventDefault = false; + } else if ( selected.is( '.suggestions-special' ) ) { + if ( typeof context.config.special.select === 'function' ) { + // Allow the callback to decide whether to prevent default or not + if ( context.config.special.select.call( selected, context.data.$textbox, 'keyboard' ) === true ) { + preventDefault = false; } - } else { - if ( typeof context.config.result.select === 'function' ) { - // Allow the callback to decide whether to prevent default or not - if ( context.config.result.select.call( selected, context.data.$textbox, 'keyboard' ) === true ) { - preventDefault = false; - } + } + } else { + if ( typeof context.config.result.select === 'function' ) { + // Allow the callback to decide whether to prevent default or not + if ( context.config.result.select.call( selected, context.data.$textbox, 'keyboard' ) === true ) { + preventDefault = false; } } - break; - default: - $.suggestions.update( context, true ); - break; - } - if ( preventDefault ) { - e.preventDefault(); - e.stopPropagation(); - } + } + break; + default: + update( context, true ); + break; } - }; + if ( preventDefault ) { + e.preventDefault(); + e.stopPropagation(); + } + } // See file header for method documentation $.fn.suggestions = function () { @@ -627,12 +616,12 @@ if ( typeof args[ 0 ] === 'object' ) { // Apply set of properties for ( key in args[ 0 ] ) { - $.suggestions.configure( context, key, args[ 0 ][ key ] ); + configure( context, key, args[ 0 ][ key ] ); } } else if ( typeof args[ 0 ] === 'string' ) { if ( args.length > 1 ) { // Set property values - $.suggestions.configure( context, args[ 0 ], args[ 1 ] ); + configure( context, args[ 0 ], args[ 1 ] ); } } } @@ -678,7 +667,7 @@ if ( $result.get( 0 ) !== $other.get( 0 ) ) { return; } - $.suggestions.highlight( context, $result, true ); + highlight( context, $result, true ); if ( typeof context.config.result.select === 'function' ) { context.config.result.select.call( $result, context.data.$textbox, 'mouse' ); } @@ -687,7 +676,7 @@ // This will hide the link we're just clicking on, which causes problems // when done synchronously in at least Firefox 3.6 (T64858). setTimeout( function () { - $.suggestions.hide( context ); + hide( context ); } ); } // Always bring focus to the textbox, as that's probably where the user expects it @@ -719,7 +708,7 @@ // This will hide the link we're just clicking on, which causes problems // when done synchronously in at least Firefox 3.6 (T64858). setTimeout( function () { - $.suggestions.hide( context ); + hide( context ); } ); } // Always bring focus to the textbox, as that's probably where the user expects it @@ -728,7 +717,7 @@ } ) .on( 'mousemove', function ( e ) { context.data.selectedWithMouse = true; - $.suggestions.highlight( + highlight( context, $( e.target ).closest( '.suggestions-special' ), false ); } ) @@ -745,7 +734,7 @@ } ) .on( 'keypress', function ( e ) { context.data.keypressedCount++; - $.suggestions.keypress( e, context, context.data.keypressed ); + keypress( e, context, context.data.keypressed ); } ) .on( 'keyup', function ( e ) { // The keypress event is fired when a key is pressed down and that key normally @@ -765,7 +754,7 @@ e.which === context.data.keypressed && allowed.indexOf( e.which ) !== -1 ) { - $.suggestions.keypress( e, context, context.data.keypressed ); + keypress( e, context, context.data.keypressed ); } } ) .on( 'blur', function () { @@ -774,13 +763,13 @@ if ( context.data.mouseDownOn.length > 0 ) { return; } - $.suggestions.hide( context ); - $.suggestions.cancel( context ); + hide( context ); + cancel( context ); } ); // Load suggestions if the value is changed because there are already // typed characters before the JavaScript is loaded. if ( this.value !== this.defaultValue ) { - $.suggestions.update( context, false ); + update( context, false ); } }