[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/js/jquery/ui/ -> autocomplete.js (source)

   1  /*!
   2   * jQuery UI Autocomplete 1.14.2
   3   * https://jqueryui.com
   4   *
   5   * Copyright OpenJS Foundation and other contributors
   6   * Released under the MIT license.
   7   * https://jquery.org/license
   8   */
   9  
  10  //>>label: Autocomplete
  11  //>>group: Widgets
  12  //>>description: Lists suggested words as the user is typing.
  13  //>>docs: https://api.jqueryui.com/autocomplete/
  14  //>>demos: https://jqueryui.com/autocomplete/
  15  //>>css.structure: ../../themes/base/core.css
  16  //>>css.structure: ../../themes/base/autocomplete.css
  17  //>>css.theme: ../../themes/base/theme.css
  18  
  19  ( function( factory ) {
  20      "use strict";
  21  
  22      if ( typeof define === "function" && define.amd ) {
  23  
  24          // AMD. Register as an anonymous module.
  25          define( [
  26              "jquery",
  27              "./menu",
  28              "../keycode",
  29              "../position",
  30              "../version",
  31              "../widget"
  32          ], factory );
  33      } else {
  34  
  35          // Browser globals
  36          factory( jQuery );
  37      }
  38  } )( function( $ ) {
  39  "use strict";
  40  
  41  $.widget( "ui.autocomplete", {
  42      version: "1.14.2",
  43      defaultElement: "<input>",
  44      options: {
  45          appendTo: null,
  46          autoFocus: false,
  47          delay: 300,
  48          minLength: 1,
  49          position: {
  50              my: "left top",
  51              at: "left bottom",
  52              collision: "none"
  53          },
  54          source: null,
  55  
  56          // Callbacks
  57          change: null,
  58          close: null,
  59          focus: null,
  60          open: null,
  61          response: null,
  62          search: null,
  63          select: null
  64      },
  65  
  66      requestIndex: 0,
  67      pending: 0,
  68      liveRegionTimer: null,
  69  
  70      _create: function() {
  71  
  72          // Some browsers only repeat keydown events, not keypress events,
  73          // so we use the suppressKeyPress flag to determine if we've already
  74          // handled the keydown event. #7269
  75          // Unfortunately the code for & in keypress is the same as the up arrow,
  76          // so we use the suppressKeyPressRepeat flag to avoid handling keypress
  77          // events when we know the keydown event was used to modify the
  78          // search term. #7799
  79          var suppressKeyPress, suppressKeyPressRepeat, suppressInput,
  80              nodeName = this.element[ 0 ].nodeName.toLowerCase(),
  81              isTextarea = nodeName === "textarea",
  82              isInput = nodeName === "input";
  83  
  84          // Textareas are always multi-line
  85          // Inputs are always single-line, even if inside a contentEditable element
  86          // All other element types are determined by whether they're contentEditable
  87          this.isMultiLine = isTextarea ||
  88              !isInput && this.element.prop( "contentEditable" ) === "true";
  89  
  90          this.valueMethod = this.element[ isTextarea || isInput ? "val" : "text" ];
  91          this.isNewMenu = true;
  92  
  93          this._addClass( "ui-autocomplete-input" );
  94          this.element.attr( "autocomplete", "off" );
  95  
  96          this._on( this.element, {
  97              keydown: function( event ) {
  98                  if ( this.element.prop( "readOnly" ) ) {
  99                      suppressKeyPress = true;
 100                      suppressInput = true;
 101                      suppressKeyPressRepeat = true;
 102                      return;
 103                  }
 104  
 105                  suppressKeyPress = false;
 106                  suppressInput = false;
 107                  suppressKeyPressRepeat = false;
 108                  var keyCode = $.ui.keyCode;
 109                  switch ( event.keyCode ) {
 110                  case keyCode.PAGE_UP:
 111                      suppressKeyPress = true;
 112                      this._move( "previousPage", event );
 113                      break;
 114                  case keyCode.PAGE_DOWN:
 115                      suppressKeyPress = true;
 116                      this._move( "nextPage", event );
 117                      break;
 118                  case keyCode.UP:
 119                      suppressKeyPress = true;
 120                      this._keyEvent( "previous", event );
 121                      break;
 122                  case keyCode.DOWN:
 123                      suppressKeyPress = true;
 124                      this._keyEvent( "next", event );
 125                      break;
 126                  case keyCode.ENTER:
 127  
 128                      // when menu is open and has focus
 129                      if ( this.menu.active ) {
 130  
 131                          // #6055 - Opera still allows the keypress to occur
 132                          // which causes forms to submit
 133                          suppressKeyPress = true;
 134                          event.preventDefault();
 135                          this.menu.select( event );
 136                      }
 137                      break;
 138                  case keyCode.TAB:
 139                      if ( this.menu.active ) {
 140                          this.menu.select( event );
 141                      }
 142                      break;
 143                  case keyCode.ESCAPE:
 144                      if ( this.menu.element.is( ":visible" ) ) {
 145                          if ( !this.isMultiLine ) {
 146                              this._value( this.term );
 147                          }
 148                          this.close( event );
 149  
 150                          // Different browsers have different default behavior for escape
 151                          // Single press can mean undo or clear
 152                          event.preventDefault();
 153                      }
 154                      break;
 155                  default:
 156                      suppressKeyPressRepeat = true;
 157  
 158                      // search timeout should be triggered before the input value is changed
 159                      this._searchTimeout( event );
 160                      break;
 161                  }
 162              },
 163              keypress: function( event ) {
 164                  if ( suppressKeyPress ) {
 165                      suppressKeyPress = false;
 166                      if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
 167                          event.preventDefault();
 168                      }
 169                      return;
 170                  }
 171                  if ( suppressKeyPressRepeat ) {
 172                      return;
 173                  }
 174  
 175                  // Replicate some key handlers to allow them to repeat in Firefox and Opera
 176                  var keyCode = $.ui.keyCode;
 177                  switch ( event.keyCode ) {
 178                  case keyCode.PAGE_UP:
 179                      this._move( "previousPage", event );
 180                      break;
 181                  case keyCode.PAGE_DOWN:
 182                      this._move( "nextPage", event );
 183                      break;
 184                  case keyCode.UP:
 185                      this._keyEvent( "previous", event );
 186                      break;
 187                  case keyCode.DOWN:
 188                      this._keyEvent( "next", event );
 189                      break;
 190                  }
 191              },
 192              input: function( event ) {
 193                  if ( suppressInput ) {
 194                      suppressInput = false;
 195                      event.preventDefault();
 196                      return;
 197                  }
 198                  this._searchTimeout( event );
 199              },
 200              focus: function() {
 201                  this.selectedItem = null;
 202                  this.previous = this._value();
 203              },
 204              blur: function( event ) {
 205                  clearTimeout( this.searching );
 206                  this.close( event );
 207                  this._change( event );
 208              }
 209          } );
 210  
 211          this._initSource();
 212          this.menu = $( "<ul>" )
 213              .appendTo( this._appendTo() )
 214              .menu( {
 215  
 216                  // disable ARIA support, the live region takes care of that
 217                  role: null
 218              } )
 219              .hide()
 220              .menu( "instance" );
 221  
 222          this._addClass( this.menu.element, "ui-autocomplete", "ui-front" );
 223          this._on( this.menu.element, {
 224              mousedown: function( event ) {
 225  
 226                  // Prevent moving focus out of the text field
 227                  event.preventDefault();
 228              },
 229              menufocus: function( event, ui ) {
 230                  var label, item;
 231  
 232                  // Support: Firefox
 233                  // Prevent accidental activation of menu items in Firefox (#7024 #9118)
 234                  if ( this.isNewMenu ) {
 235                      this.isNewMenu = false;
 236                      if ( event.originalEvent && /^mouse/.test( event.originalEvent.type ) ) {
 237                          this.menu.blur();
 238  
 239                          this.document.one( "mousemove", function() {
 240                              $( event.target ).trigger( event.originalEvent );
 241                          } );
 242  
 243                          return;
 244                      }
 245                  }
 246  
 247                  item = ui.item.data( "ui-autocomplete-item" );
 248                  if ( false !== this._trigger( "focus", event, { item: item } ) ) {
 249  
 250                      // use value to match what will end up in the input, if it was a key event
 251                      if ( event.originalEvent && /^key/.test( event.originalEvent.type ) ) {
 252                          this._value( item.value );
 253                      }
 254                  }
 255  
 256                  // Announce the value in the liveRegion
 257                  label = ui.item.attr( "aria-label" ) || item.value;
 258                  if ( label && String.prototype.trim.call( label ).length ) {
 259                      clearTimeout( this.liveRegionTimer );
 260                      this.liveRegionTimer = this._delay( function() {
 261                          this.liveRegion.html( $( "<div>" ).text( label ) );
 262                      }, 100 );
 263                  }
 264              },
 265              menuselect: function( event, ui ) {
 266                  var item = ui.item.data( "ui-autocomplete-item" ),
 267                      previous = this.previous;
 268  
 269                  // Only trigger when focus was lost (click on menu)
 270                  if ( this.element[ 0 ] !== this.document[ 0 ].activeElement ) {
 271                      this.element.trigger( "focus" );
 272                      this.previous = previous;
 273                  }
 274  
 275                  if ( false !== this._trigger( "select", event, { item: item } ) ) {
 276                      this._value( item.value );
 277                  }
 278  
 279                  // reset the term after the select event
 280                  // this allows custom select handling to work properly
 281                  this.term = this._value();
 282  
 283                  this.close( event );
 284                  this.selectedItem = item;
 285              }
 286          } );
 287  
 288          this.liveRegion = $( "<div>", {
 289              role: "status",
 290              "aria-live": "assertive",
 291              "aria-relevant": "additions"
 292          } )
 293              .appendTo( this.document[ 0 ].body );
 294  
 295          this._addClass( this.liveRegion, null, "ui-helper-hidden-accessible" );
 296  
 297          // Turning off autocomplete prevents the browser from remembering the
 298          // value when navigating through history, so we re-enable autocomplete
 299          // if the page is unloaded before the widget is destroyed. #7790
 300          this._on( this.window, {
 301              beforeunload: function() {
 302                  this.element.removeAttr( "autocomplete" );
 303              }
 304          } );
 305      },
 306  
 307      _destroy: function() {
 308          clearTimeout( this.searching );
 309          this.element.removeAttr( "autocomplete" );
 310          this.menu.element.remove();
 311          this.liveRegion.remove();
 312      },
 313  
 314      _setOption: function( key, value ) {
 315          this._super( key, value );
 316          if ( key === "source" ) {
 317              this._initSource();
 318          }
 319          if ( key === "appendTo" ) {
 320              this.menu.element.appendTo( this._appendTo() );
 321          }
 322          if ( key === "disabled" && value && this.xhr ) {
 323              this.xhr.abort();
 324          }
 325      },
 326  
 327      _isEventTargetInWidget: function( event ) {
 328          var menuElement = this.menu.element[ 0 ];
 329  
 330          return event.target === this.element[ 0 ] ||
 331              event.target === menuElement ||
 332              $.contains( menuElement, event.target );
 333      },
 334  
 335      _closeOnClickOutside: function( event ) {
 336          if ( !this._isEventTargetInWidget( event ) ) {
 337              this.close();
 338          }
 339      },
 340  
 341      _appendTo: function() {
 342          var element = this.options.appendTo;
 343  
 344          if ( element ) {
 345              element = element.jquery || element.nodeType ?
 346                  $( element ) :
 347                  this.document.find( element ).eq( 0 );
 348          }
 349  
 350          if ( !element || !element[ 0 ] ) {
 351              element = this.element.closest( ".ui-front, dialog" );
 352          }
 353  
 354          if ( !element.length ) {
 355              element = this.document[ 0 ].body;
 356          }
 357  
 358          return element;
 359      },
 360  
 361      _initSource: function() {
 362          var array, url,
 363              that = this;
 364          if ( Array.isArray( this.options.source ) ) {
 365              array = this.options.source;
 366              this.source = function( request, response ) {
 367                  response( $.ui.autocomplete.filter( array, request.term ) );
 368              };
 369          } else if ( typeof this.options.source === "string" ) {
 370              url = this.options.source;
 371              this.source = function( request, response ) {
 372                  if ( that.xhr ) {
 373                      that.xhr.abort();
 374                  }
 375                  that.xhr = $.ajax( {
 376                      url: url,
 377                      data: request,
 378                      dataType: "json",
 379                      success: function( data ) {
 380                          response( data );
 381                      },
 382                      error: function() {
 383                          response( [] );
 384                      }
 385                  } );
 386              };
 387          } else {
 388              this.source = this.options.source;
 389          }
 390      },
 391  
 392      _searchTimeout: function( event ) {
 393          clearTimeout( this.searching );
 394          this.searching = this._delay( function() {
 395  
 396              // Search if the value has changed, or if the user retypes the same value (see #7434)
 397              var equalValues = this.term === this._value(),
 398                  menuVisible = this.menu.element.is( ":visible" ),
 399                  modifierKey = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
 400  
 401              if ( !equalValues || ( equalValues && !menuVisible && !modifierKey ) ) {
 402                  this.selectedItem = null;
 403                  this.search( null, event );
 404              }
 405          }, this.options.delay );
 406      },
 407  
 408      search: function( value, event ) {
 409          value = value != null ? value : this._value();
 410  
 411          // Always save the actual value, not the one passed as an argument
 412          this.term = this._value();
 413  
 414          if ( value.length < this.options.minLength ) {
 415              return this.close( event );
 416          }
 417  
 418          if ( this._trigger( "search", event ) === false ) {
 419              return;
 420          }
 421  
 422          return this._search( value );
 423      },
 424  
 425      _search: function( value ) {
 426          this.pending++;
 427          this._addClass( "ui-autocomplete-loading" );
 428          this.cancelSearch = false;
 429  
 430          this.source( { term: value }, this._response() );
 431      },
 432  
 433      _response: function() {
 434          var index = ++this.requestIndex;
 435  
 436          return function( content ) {
 437              if ( index === this.requestIndex ) {
 438                  this.__response( content );
 439              }
 440  
 441              this.pending--;
 442              if ( !this.pending ) {
 443                  this._removeClass( "ui-autocomplete-loading" );
 444              }
 445          }.bind( this );
 446      },
 447  
 448      __response: function( content ) {
 449          if ( content ) {
 450              content = this._normalize( content );
 451          }
 452          this._trigger( "response", null, { content: content } );
 453          if ( !this.options.disabled && content && content.length && !this.cancelSearch ) {
 454              this._suggest( content );
 455              this._trigger( "open" );
 456          } else {
 457  
 458              // use ._close() instead of .close() so we don't cancel future searches
 459              this._close();
 460          }
 461      },
 462  
 463      close: function( event ) {
 464          this.cancelSearch = true;
 465          this._close( event );
 466      },
 467  
 468      _close: function( event ) {
 469  
 470          // Remove the handler that closes the menu on outside clicks
 471          this._off( this.document, "mousedown" );
 472  
 473          if ( this.menu.element.is( ":visible" ) ) {
 474              this.menu.element.hide();
 475              this.menu.blur();
 476              this.isNewMenu = true;
 477              this._trigger( "close", event );
 478          }
 479      },
 480  
 481      _change: function( event ) {
 482          if ( this.previous !== this._value() ) {
 483              this._trigger( "change", event, { item: this.selectedItem } );
 484          }
 485      },
 486  
 487      _normalize: function( items ) {
 488  
 489          // assume all items have the right format when the first item is complete
 490          if ( items.length && items[ 0 ].label && items[ 0 ].value ) {
 491              return items;
 492          }
 493          return $.map( items, function( item ) {
 494              if ( typeof item === "string" ) {
 495                  return {
 496                      label: item,
 497                      value: item
 498                  };
 499              }
 500              return $.extend( {}, item, {
 501                  label: item.label || item.value,
 502                  value: item.value || item.label
 503              } );
 504          } );
 505      },
 506  
 507      _suggest: function( items ) {
 508          var ul = this.menu.element.empty();
 509          this._renderMenu( ul, items );
 510          this.isNewMenu = true;
 511          this.menu.refresh();
 512  
 513          // Size and position menu
 514          ul.show();
 515          this._resizeMenu();
 516          ul.position( $.extend( {
 517              of: this.element
 518          }, this.options.position ) );
 519  
 520          if ( this.options.autoFocus ) {
 521              this.menu.next();
 522          }
 523  
 524          // Listen for interactions outside of the widget (#6642)
 525          this._on( this.document, {
 526              mousedown: "_closeOnClickOutside"
 527          } );
 528      },
 529  
 530      _resizeMenu: function() {
 531          var ul = this.menu.element;
 532          ul.outerWidth( Math.max(
 533  
 534              // Firefox wraps long text (possibly a rounding bug)
 535              // so we add 1px to avoid the wrapping (#7513)
 536              ul.width( "" ).outerWidth() + 1,
 537              this.element.outerWidth()
 538          ) );
 539      },
 540  
 541      _renderMenu: function( ul, items ) {
 542          var that = this;
 543          $.each( items, function( index, item ) {
 544              that._renderItemData( ul, item );
 545          } );
 546      },
 547  
 548      _renderItemData: function( ul, item ) {
 549          return this._renderItem( ul, item ).data( "ui-autocomplete-item", item );
 550      },
 551  
 552      _renderItem: function( ul, item ) {
 553          return $( "<li>" )
 554              .append( $( "<div>" ).text( item.label ) )
 555              .appendTo( ul );
 556      },
 557  
 558      _move: function( direction, event ) {
 559          if ( !this.menu.element.is( ":visible" ) ) {
 560              this.search( null, event );
 561              return;
 562          }
 563          if ( this.menu.isFirstItem() && /^previous/.test( direction ) ||
 564                  this.menu.isLastItem() && /^next/.test( direction ) ) {
 565  
 566              if ( !this.isMultiLine ) {
 567                  this._value( this.term );
 568              }
 569  
 570              this.menu.blur();
 571              return;
 572          }
 573          this.menu[ direction ]( event );
 574      },
 575  
 576      widget: function() {
 577          return this.menu.element;
 578      },
 579  
 580      _value: function() {
 581          return this.valueMethod.apply( this.element, arguments );
 582      },
 583  
 584      _keyEvent: function( keyEvent, event ) {
 585          if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
 586              this._move( keyEvent, event );
 587  
 588              // Prevents moving cursor to beginning/end of the text field in some browsers
 589              event.preventDefault();
 590          }
 591      }
 592  } );
 593  
 594  $.extend( $.ui.autocomplete, {
 595      escapeRegex: function( value ) {
 596          return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" );
 597      },
 598      filter: function( array, term ) {
 599          var matcher = new RegExp( $.ui.autocomplete.escapeRegex( term ), "i" );
 600          return $.grep( array, function( value ) {
 601              return matcher.test( value.label || value.value || value );
 602          } );
 603      }
 604  } );
 605  
 606  // Live region extension, adding a `messages` option
 607  // NOTE: This is an experimental API. We are still investigating
 608  // a full solution for string manipulation and internationalization.
 609  $.widget( "ui.autocomplete", $.ui.autocomplete, {
 610      options: {
 611          messages: {
 612              noResults: "No search results.",
 613              results: function( amount ) {
 614                  return amount + ( amount > 1 ? " results are" : " result is" ) +
 615                      " available, use up and down arrow keys to navigate.";
 616              }
 617          }
 618      },
 619  
 620      __response: function( content ) {
 621          var message;
 622          this._superApply( arguments );
 623          if ( this.options.disabled || this.cancelSearch ) {
 624              return;
 625          }
 626          if ( content && content.length ) {
 627              message = this.options.messages.results( content.length );
 628          } else {
 629              message = this.options.messages.noResults;
 630          }
 631          clearTimeout( this.liveRegionTimer );
 632          this.liveRegionTimer = this._delay( function() {
 633              this.liveRegion.html( $( "<div>" ).text( message ) );
 634          }, 100 );
 635      }
 636  } );
 637  
 638  return $.ui.autocomplete;
 639  
 640  } );


Generated : Wed Jul 15 08:20:16 2026 Cross-referenced by PHPXref