r80320 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r80319‎ | r80320 | r80321 >
Date:20:38, 14 January 2011
Author:husky
Status:ok
Tags:
Comment:
jquery suggestions and stuff
Modified paths:
  • /trunk/tools/wp-photocommons/inc/class-photocommons.php (modified) (history)
  • /trunk/tools/wp-photocommons/js/jquery.suggestions.js (added) (history)
  • /trunk/tools/wp-photocommons/search.php (modified) (history)

Diff [purge]

Index: trunk/tools/wp-photocommons/inc/class-photocommons.php
@@ -18,15 +18,17 @@
1919 private function initAdmin() {
2020 wp_enqueue_script('jquery');
2121 wp_enqueue_script('jquery-ui-core');
22 - wp_enqueue_script('jquery-ui-dialog');
 22+ wp_enqueue_script('jquery-ui-dialog');
 23+
 24+ wp_register_script('admin', self::PLUGIN_PATH . 'js/admin.js');
2325
2426 wp_register_style('jquid_jquery_blog_stylesheet', 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.6/themes/sunny/jquery-ui.css');
2527 wp_enqueue_style('jquid_jquery_blog_stylesheet');
26 -
 28+
2729 wp_register_script('admin', self::PLUGIN_PATH . 'js/admin.js');
28 - wp_register_script('search', self::PLUGIN_PATH . 'search.js');
 30+ wp_register_script('search', self::PLUGIN_PATH . 'js/search.js');
2931 wp_enqueue_script('admin');
30 - wp_enqueue_script('search');
 32+ wp_enqueue_script('search');
3133 }
3234
3335 private function initFrontend() {
Index: trunk/tools/wp-photocommons/search.php
@@ -21,7 +21,7 @@
2222 <?php if ($standalone) : ?>
2323 <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
2424 <script src="js/jquery-ui-1.8.5.custom.min.js"></script>
25 - <script src="search.js"></script>
 25+ <script src="js/search.js"></script>
2626 </body>
2727 </html>
2828 <?php endif; ?>
\ No newline at end of file
Index: trunk/tools/wp-photocommons/js/jquery.suggestions.js
@@ -0,0 +1,518 @@
 2+/**
 3+ * This plugin provides a generic way to add suggestions to a text box.
 4+ *
 5+ * Usage:
 6+ *
 7+ * Set options:
 8+ * $('#textbox').suggestions( { option1: value1, option2: value2 } );
 9+ * $('#textbox').suggestions( option, value );
 10+ * Get option:
 11+ * value = $('#textbox').suggestions( option );
 12+ * Initialize:
 13+ * $('#textbox').suggestions();
 14+ *
 15+ * Options:
 16+ *
 17+ * fetch(query): Callback that should fetch suggestions and set the suggestions property. Executed in the context of the
 18+ * textbox
 19+ * Type: Function
 20+ * cancel: Callback function to call when any pending asynchronous suggestions fetches should be canceled.
 21+ * Executed in the context of the textbox
 22+ * Type: Function
 23+ * special: Set of callbacks for rendering and selecting
 24+ * Type: Object of Functions 'render' and 'select'
 25+ * result: Set of callbacks for rendering and selecting
 26+ * Type: Object of Functions 'render' and 'select'
 27+ * $region: jQuery selection of element to place the suggestions below and match width of
 28+ * Type: jQuery Object, Default: $(this)
 29+ * suggestions: Suggestions to display
 30+ * Type: Array of strings
 31+ * maxRows: Maximum number of suggestions to display at one time
 32+ * Type: Number, Range: 1 - 100, Default: 7
 33+ * delay: Number of ms to wait for the user to stop typing
 34+ * Type: Number, Range: 0 - 1200, Default: 120
 35+ * submitOnClick: Whether to submit the form containing the textbox when a suggestion is clicked
 36+ * Type: Boolean, Default: false
 37+ * maxExpandFactor: Maximum suggestions box width relative to the textbox width. If set to e.g. 2, the suggestions box
 38+ * will never be grown beyond 2 times the width of the textbox.
 39+ * Type: Number, Range: 1 - infinity, Default: 3
 40+ * positionFromLeft: Whether to position the suggestion box with the left attribute or the right
 41+ * Type: Boolean, Default: true
 42+ * highlightInput: Whether to hightlight matched portions of the input or not
 43+ * Type: Boolean, Default: false
 44+ */
 45+
 46+$.suggestions = {
 47+ /**
 48+ * Cancel any delayed updateSuggestions() call and inform the user so
 49+ * they can cancel their result fetching if they use AJAX or something
 50+ */
 51+ cancel: function( context ) {
 52+ if ( context.data.timerID != null ) {
 53+ clearTimeout( context.data.timerID );
 54+ }
 55+ if ( typeof context.config.cancel == 'function' ) {
 56+ context.config.cancel.call( context.data.$textbox );
 57+ }
 58+ },
 59+ /**
 60+ * Restore the text the user originally typed in the textbox, before it was overwritten by highlight(). This
 61+ * restores the value the currently displayed suggestions are based on, rather than the value just before
 62+ * highlight() overwrote it; the former is arguably slightly more sensible.
 63+ */
 64+ restore: function( context ) {
 65+ context.data.$textbox.val( context.data.prevText );
 66+ },
 67+ /**
 68+ * Ask the user-specified callback for new suggestions. Any previous delayed call to this function still pending
 69+ * will be canceled. If the value in the textbox hasn't changed since the last time suggestions were fetched, this
 70+ * function does nothing.
 71+ * @param {Boolean} delayed Whether or not to delay this by the currently configured amount of time
 72+ */
 73+ update: function( context, delayed ) {
 74+ // Only fetch if the value in the textbox changed
 75+ function maybeFetch() {
 76+ if ( context.data.$textbox.val() !== context.data.prevText ) {
 77+ context.data.prevText = context.data.$textbox.val();
 78+ if ( typeof context.config.fetch == 'function' ) {
 79+ context.config.fetch.call( context.data.$textbox, context.data.$textbox.val() );
 80+ }
 81+ }
 82+ }
 83+ // Cancel previous call
 84+ if ( context.data.timerID != null ) {
 85+ clearTimeout( context.data.timerID );
 86+ }
 87+ if ( delayed ) {
 88+ // Start a new asynchronous call
 89+ context.data.timerID = setTimeout( maybeFetch, context.config.delay );
 90+ } else {
 91+ maybeFetch();
 92+ }
 93+ $.suggestions.special( context );
 94+ },
 95+ special: function( context ) {
 96+ // Allow custom rendering - but otherwise don't do any rendering
 97+ if ( typeof context.config.special.render == 'function' ) {
 98+ // Wait for the browser to update the value
 99+ setTimeout( function() {
 100+ // Render special
 101+ $special = context.data.$container.find( '.suggestions-special' );
 102+ context.config.special.render.call( $special, context.data.$textbox.val() );
 103+ }, 1 );
 104+ }
 105+ },
 106+ /**
 107+ * Sets the value of a property, and updates the widget accordingly
 108+ * @param property String Name of property
 109+ * @param value Mixed Value to set property with
 110+ */
 111+ configure: function( context, property, value ) {
 112+ // Validate creation using fallback values
 113+ switch( property ) {
 114+ case 'fetch':
 115+ case 'cancel':
 116+ case 'special':
 117+ case 'result':
 118+ case '$region':
 119+ context.config[property] = value;
 120+ break;
 121+ case 'suggestions':
 122+ context.config[property] = value;
 123+ // Update suggestions
 124+ if ( typeof context.data !== 'undefined' ) {
 125+ if ( context.data.$textbox.val().length == 0 ) {
 126+ // Hide the div when no suggestion exist
 127+ context.data.$container.hide();
 128+ } else {
 129+ // Rebuild the suggestions list
 130+ context.data.$container.show();
 131+ // Update the size and position of the list
 132+ var newCSS = {
 133+ 'top': context.config.$region.offset().top + context.config.$region.outerHeight(),
 134+ 'bottom': 'auto',
 135+ 'width': context.config.$region.outerWidth(),
 136+ 'height': 'auto'
 137+ };
 138+ if ( context.config.positionFromLeft ) {
 139+ newCSS['left'] = context.config.$region.offset().left;
 140+ newCSS['right'] = 'auto';
 141+ } else {
 142+ newCSS['left'] = 'auto';
 143+ newCSS['right'] = $( 'body' ).width() - ( context.config.$region.offset().left + context.config.$region.outerWidth() );
 144+ }
 145+ context.data.$container.css( newCSS );
 146+ var $results = context.data.$container.children( '.suggestions-results' );
 147+ $results.empty();
 148+ var expWidth = -1;
 149+ var $autoEllipseMe = $( [] );
 150+ var matchedText = null;
 151+ for ( var i = 0; i < context.config.suggestions.length; i++ ) {
 152+ var text = context.config.suggestions[i];
 153+ var $result = $( '<div />' )
 154+ .addClass( 'suggestions-result' )
 155+ .attr( 'rel', i )
 156+ .data( 'text', context.config.suggestions[i] )
 157+ .mousemove( function( e ) {
 158+ context.data.selectedWithMouse = true;
 159+ $.suggestions.highlight(
 160+ context, $(this).closest( '.suggestions-results div' ), false
 161+ );
 162+ } )
 163+ .appendTo( $results );
 164+ // Allow custom rendering
 165+ if ( typeof context.config.result.render == 'function' ) {
 166+ context.config.result.render.call( $result, context.config.suggestions[i] );
 167+ } else {
 168+ // Add <span> with text
 169+ if( context.config.highlightInput ) {
 170+ matchedText = context.data.prevText;
 171+ }
 172+ $result.append( $( '<span />' )
 173+ .css( 'whiteSpace', 'nowrap' )
 174+ .text( text )
 175+ );
 176+
 177+ // Widen results box if needed
 178+ // New width is only calculated here, applied later
 179+ var $span = $result.children( 'span' );
 180+ if ( $span.outerWidth() > $result.width() && $span.outerWidth() > expWidth ) {
 181+ // factor in any padding, margin, or border space on the parent
 182+ expWidth = $span.outerWidth() + ( context.data.$container.width() - $span.parent().width());
 183+ }
 184+ $autoEllipseMe = $autoEllipseMe.add( $result );
 185+ }
 186+ }
 187+ // Apply new width for results box, if any
 188+ if ( expWidth > context.data.$container.width() ) {
 189+ var maxWidth = context.config.maxExpandFactor*context.data.$textbox.width();
 190+ context.data.$container.width( Math.min( expWidth, maxWidth ) );
 191+ }
 192+ // autoEllipse the results. Has to be done after changing the width
 193+ $autoEllipseMe.autoEllipsis( { hasSpan: true, tooltip: true, matchText: matchedText } );
 194+ }
 195+ }
 196+ break;
 197+ case 'maxRows':
 198+ context.config[property] = Math.max( 1, Math.min( 100, value ) );
 199+ break;
 200+ case 'delay':
 201+ context.config[property] = Math.max( 0, Math.min( 1200, value ) );
 202+ break;
 203+ case 'maxExpandFactor':
 204+ context.config[property] = Math.max( 1, value );
 205+ break;
 206+ case 'submitOnClick':
 207+ case 'positionFromLeft':
 208+ case 'highlightInput':
 209+ context.config[property] = value ? true : false;
 210+ break;
 211+ }
 212+ },
 213+ /**
 214+ * Highlight a result in the results table
 215+ * @param result <tr> to highlight: jQuery object, or 'prev' or 'next'
 216+ * @param updateTextbox If true, put the suggestion in the textbox
 217+ */
 218+ highlight: function( context, result, updateTextbox ) {
 219+ var selected = context.data.$container.find( '.suggestions-result-current' );
 220+ if ( !result.get || selected.get( 0 ) != result.get( 0 ) ) {
 221+ if ( result == 'prev' ) {
 222+ if( selected.is( '.suggestions-special' ) ) {
 223+ result = context.data.$container.find( '.suggestions-result:last' )
 224+ } else {
 225+ result = selected.prev();
 226+ if ( selected.length == 0 ) {
 227+ // we are at the begginning, so lets jump to the last item
 228+ if ( context.data.$container.find( '.suggestions-special' ).html() != "" ) {
 229+ result = context.data.$container.find( '.suggestions-special' );
 230+ } else {
 231+ result = context.data.$container.find( '.suggestions-results div:last' );
 232+ }
 233+ }
 234+ }
 235+ } else if ( result == 'next' ) {
 236+ if ( selected.length == 0 ) {
 237+ // No item selected, go to the first one
 238+ result = context.data.$container.find( '.suggestions-results div:first' );
 239+ if ( result.length == 0 && context.data.$container.find( '.suggestions-special' ).html() != "" ) {
 240+ // No suggestion exists, go to the special one directly
 241+ result = context.data.$container.find( '.suggestions-special' );
 242+ }
 243+ } else {
 244+ result = selected.next();
 245+ if ( selected.is( '.suggestions-special' ) ) {
 246+ result = $( [] );
 247+ } else if (
 248+ result.length == 0 &&
 249+ context.data.$container.find( '.suggestions-special' ).html() != ""
 250+ ) {
 251+ // We were at the last item, jump to the specials!
 252+ result = context.data.$container.find( '.suggestions-special' );
 253+ }
 254+ }
 255+ }
 256+ selected.removeClass( 'suggestions-result-current' );
 257+ result.addClass( 'suggestions-result-current' );
 258+ }
 259+ if ( updateTextbox ) {
 260+ if ( result.length == 0 || result.is( '.suggestions-special' ) ) {
 261+ $.suggestions.restore( context );
 262+ } else {
 263+ context.data.$textbox.val( result.data( 'text' ) );
 264+ // .val() doesn't call any event handlers, so
 265+ // let the world know what happened
 266+ context.data.$textbox.change();
 267+ }
 268+ context.data.$textbox.trigger( 'change' );
 269+ }
 270+ },
 271+ /**
 272+ * Respond to keypress event
 273+ * @param key Integer Code of key pressed
 274+ */
 275+ keypress: function( e, context, key ) {
 276+ var wasVisible = context.data.$container.is( ':visible' );
 277+ var preventDefault = false;
 278+ switch ( key ) {
 279+ // Arrow down
 280+ case 40:
 281+ if ( wasVisible ) {
 282+ $.suggestions.highlight( context, 'next', true );
 283+ context.data.selectedWithMouse = false;
 284+ } else {
 285+ $.suggestions.update( context, false );
 286+ }
 287+ preventDefault = true;
 288+ break;
 289+ // Arrow up
 290+ case 38:
 291+ if ( wasVisible ) {
 292+ $.suggestions.highlight( context, 'prev', true );
 293+ context.data.selectedWithMouse = false;
 294+ }
 295+ preventDefault = wasVisible;
 296+ break;
 297+ // Escape
 298+ case 27:
 299+ context.data.$container.hide();
 300+ $.suggestions.restore( context );
 301+ $.suggestions.cancel( context );
 302+ context.data.$textbox.trigger( 'change' );
 303+ preventDefault = wasVisible;
 304+ break;
 305+ // Enter
 306+ case 13:
 307+ context.data.$container.hide();
 308+ preventDefault = wasVisible;
 309+ selected = context.data.$container.find( '.suggestions-result-current' );
 310+ if ( selected.size() == 0 || context.data.selectedWithMouse ) {
 311+ // if nothing is selected OR if something was selected with the mouse,
 312+ // cancel any current requests and submit the form
 313+ $.suggestions.cancel( context );
 314+ context.config.$region.closest( 'form' ).submit();
 315+ } else if ( selected.is( '.suggestions-special' ) ) {
 316+ if ( typeof context.config.special.select == 'function' ) {
 317+ context.config.special.select.call( selected, context.data.$textbox );
 318+ }
 319+ } else {
 320+ if ( typeof context.config.result.select == 'function' ) {
 321+ $.suggestions.highlight( context, selected, true );
 322+ context.config.result.select.call( selected, context.data.$textbox );
 323+ } else {
 324+ $.suggestions.highlight( context, selected, true );
 325+ }
 326+ }
 327+ break;
 328+ default:
 329+ $.suggestions.update( context, true );
 330+ break;
 331+ }
 332+ if ( preventDefault ) {
 333+ e.preventDefault();
 334+ e.stopImmediatePropagation();
 335+ }
 336+ }
 337+};
 338+$.fn.suggestions = function() {
 339+
 340+ // Multi-context fields
 341+ var returnValue = null;
 342+ var args = arguments;
 343+
 344+ $(this).each( function() {
 345+
 346+ /* Construction / Loading */
 347+
 348+ var context = $(this).data( 'suggestions-context' );
 349+ if ( typeof context == 'undefined' || context == null ) {
 350+ context = {
 351+ config: {
 352+ 'fetch' : function() {},
 353+ 'cancel': function() {},
 354+ 'special': {},
 355+ 'result': {},
 356+ '$region': $(this),
 357+ 'suggestions': [],
 358+ 'maxRows': 7,
 359+ 'delay': 120,
 360+ 'submitOnClick': false,
 361+ 'maxExpandFactor': 3,
 362+ 'positionFromLeft': true,
 363+ 'highlightInput': false
 364+ }
 365+ };
 366+ }
 367+
 368+ /* API */
 369+
 370+ // Handle various calling styles
 371+ if ( args.length > 0 ) {
 372+ if ( typeof args[0] == 'object' ) {
 373+ // Apply set of properties
 374+ for ( var key in args[0] ) {
 375+ $.suggestions.configure( context, key, args[0][key] );
 376+ }
 377+ } else if ( typeof args[0] == 'string' ) {
 378+ if ( args.length > 1 ) {
 379+ // Set property values
 380+ $.suggestions.configure( context, args[0], args[1] );
 381+ } else if ( returnValue == null ) {
 382+ // Get property values, but don't give access to internal data - returns only the first
 383+ returnValue = ( args[0] in context.config ? undefined : context.config[args[0]] );
 384+ }
 385+ }
 386+ }
 387+
 388+ /* Initialization */
 389+
 390+ if ( typeof context.data == 'undefined' ) {
 391+ context.data = {
 392+ // ID of running timer
 393+ 'timerID': null,
 394+ // Text in textbox when suggestions were last fetched
 395+ 'prevText': null,
 396+ // Number of results visible without scrolling
 397+ 'visibleResults': 0,
 398+ // Suggestion the last mousedown event occured on
 399+ 'mouseDownOn': $( [] ),
 400+ '$textbox': $(this),
 401+ 'selectedWithMouse': false
 402+ };
 403+ // Setup the css for positioning the results box
 404+ var newCSS = {
 405+ 'top': Math.round( context.data.$textbox.offset().top + context.data.$textbox.outerHeight() ),
 406+ 'width': context.data.$textbox.outerWidth(),
 407+ 'display': 'none'
 408+ };
 409+ if ( context.config.positionFromLeft ) {
 410+ newCSS['left'] = context.config.$region.offset().left;
 411+ newCSS['right'] = 'auto';
 412+ } else {
 413+ newCSS['left'] = 'auto';
 414+ newCSS['right'] = $( 'body' ).width() - ( context.config.$region.offset().left + context.config.$region.outerWidth() );
 415+ }
 416+
 417+ context.data.$container = $( '<div />' )
 418+ .css( newCSS )
 419+ .addClass( 'suggestions' )
 420+ .append(
 421+ $( '<div />' ).addClass( 'suggestions-results' )
 422+ // Can't use click() because the container div is hidden when the textbox loses focus. Instead,
 423+ // listen for a mousedown followed by a mouseup on the same div
 424+ .mousedown( function( e ) {
 425+ context.data.mouseDownOn = $( e.target ).closest( '.suggestions-results div' );
 426+ } )
 427+ .mouseup( function( e ) {
 428+ var $result = $( e.target ).closest( '.suggestions-results div' );
 429+ var $other = context.data.mouseDownOn;
 430+ context.data.mouseDownOn = $( [] );
 431+ if ( $result.get( 0 ) != $other.get( 0 ) ) {
 432+ return;
 433+ }
 434+ $.suggestions.highlight( context, $result, true );
 435+ context.data.$container.hide();
 436+ if ( typeof context.config.result.select == 'function' ) {
 437+ context.config.result.select.call( $result, context.data.$textbox );
 438+ }
 439+ context.data.$textbox.focus();
 440+ } )
 441+ )
 442+ .append(
 443+ $( '<div />' ).addClass( 'suggestions-special' )
 444+ // Can't use click() because the container div is hidden when the textbox loses focus. Instead,
 445+ // listen for a mousedown followed by a mouseup on the same div
 446+ .mousedown( function( e ) {
 447+ context.data.mouseDownOn = $( e.target ).closest( '.suggestions-special' );
 448+ } )
 449+ .mouseup( function( e ) {
 450+ var $special = $( e.target ).closest( '.suggestions-special' );
 451+ var $other = context.data.mouseDownOn;
 452+ context.data.mouseDownOn = $( [] );
 453+ if ( $special.get( 0 ) != $other.get( 0 ) ) {
 454+ return;
 455+ }
 456+ context.data.$container.hide();
 457+ if ( typeof context.config.special.select == 'function' ) {
 458+ context.config.special.select.call( $special, context.data.$textbox );
 459+ }
 460+ context.data.$textbox.focus();
 461+ } )
 462+ .mousemove( function( e ) {
 463+ context.data.selectedWithMouse = true;
 464+ $.suggestions.highlight(
 465+ context, $( e.target ).closest( '.suggestions-special' ), false
 466+ );
 467+ } )
 468+ )
 469+ .appendTo( $( 'body' ) );
 470+ $(this)
 471+ // Stop browser autocomplete from interfering
 472+ .attr( 'autocomplete', 'off')
 473+ .keydown( function( e ) {
 474+ // Store key pressed to handle later
 475+ context.data.keypressed = ( e.keyCode == undefined ) ? e.which : e.keyCode;
 476+ context.data.keypressedCount = 0;
 477+
 478+ switch ( context.data.keypressed ) {
 479+ // This preventDefault logic is duplicated from
 480+ // $.suggestions.keypress(), which sucks
 481+ case 40:
 482+ e.preventDefault();
 483+ e.stopImmediatePropagation();
 484+ break;
 485+ case 38:
 486+ case 27:
 487+ case 13:
 488+ if ( context.data.$container.is( ':visible' ) ) {
 489+ e.preventDefault();
 490+ e.stopImmediatePropagation();
 491+ }
 492+ }
 493+ } )
 494+ .keypress( function( e ) {
 495+ context.data.keypressedCount++;
 496+ $.suggestions.keypress( e, context, context.data.keypressed );
 497+ } )
 498+ .keyup( function( e ) {
 499+ // Some browsers won't throw keypress() for arrow keys. If we got a keydown and a keyup without a
 500+ // keypress in between, solve it
 501+ if ( context.data.keypressedCount == 0 ) {
 502+ $.suggestions.keypress( e, context, context.data.keypressed );
 503+ }
 504+ } )
 505+ .blur( function() {
 506+ // When losing focus because of a mousedown
 507+ // on a suggestion, don't hide the suggestions
 508+ if ( context.data.mouseDownOn.length > 0 ) {
 509+ return;
 510+ }
 511+ context.data.$container.hide();
 512+ $.suggestions.cancel( context );
 513+ } );
 514+ }
 515+ // Store the context for next time
 516+ $(this).data( 'suggestions-context', context );
 517+ } );
 518+ return returnValue !== null ? returnValue : $(this);
 519+};
\ No newline at end of file
Property changes on: trunk/tools/wp-photocommons/js/jquery.suggestions.js
___________________________________________________________________
Added: svn:eol-style
1520 + native

Status & tagging log