*
* $( '#textbox' ).suggestions();
*
- * Uses jQuery.suggestions singleton internally.
- *
* @class jQuery.plugin.suggestions
*/
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 `<tr>` 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 = $( '<div>' )
- .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 <span> elements, like
- // the ones generated by jquery.highlightText.
- $spanForWidth = $result.wrapInner( '<span>' ).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 = $( '<div>' )
+ .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 `<tr>` 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 <span> elements, like
+ // the ones generated by jquery.highlightText.
+ $spanForWidth = $result.wrapInner( '<span>' ).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 () {
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 ] );
}
}
}
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' );
}
// 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
// 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
} )
.on( 'mousemove', function ( e ) {
context.data.selectedWithMouse = true;
- $.suggestions.highlight(
+ highlight(
context, $( e.target ).closest( '.suggestions-special' ), false
);
} )
} )
.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
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 () {
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 );
}
}