cms/drupal/misc/autocomplete.js
changeset 541 e756a8c72c3d
equal deleted inserted replaced
540:07239de796bb 541:e756a8c72c3d
       
     1 (function ($) {
       
     2 
       
     3 /**
       
     4  * Attaches the autocomplete behavior to all required fields.
       
     5  */
       
     6 Drupal.behaviors.autocomplete = {
       
     7   attach: function (context, settings) {
       
     8     var acdb = [];
       
     9     $('input.autocomplete', context).once('autocomplete', function () {
       
    10       var uri = this.value;
       
    11       if (!acdb[uri]) {
       
    12         acdb[uri] = new Drupal.ACDB(uri);
       
    13       }
       
    14       var $input = $('#' + this.id.substr(0, this.id.length - 13))
       
    15         .attr('autocomplete', 'OFF')
       
    16         .attr('aria-autocomplete', 'list');
       
    17       $($input[0].form).submit(Drupal.autocompleteSubmit);
       
    18       $input.parent()
       
    19         .attr('role', 'application')
       
    20         .append($('<span class="element-invisible" aria-live="assertive"></span>')
       
    21           .attr('id', $input.attr('id') + '-autocomplete-aria-live')
       
    22         );
       
    23       new Drupal.jsAC($input, acdb[uri]);
       
    24     });
       
    25   }
       
    26 };
       
    27 
       
    28 /**
       
    29  * Prevents the form from submitting if the suggestions popup is open
       
    30  * and closes the suggestions popup when doing so.
       
    31  */
       
    32 Drupal.autocompleteSubmit = function () {
       
    33   return $('#autocomplete').each(function () {
       
    34     this.owner.hidePopup();
       
    35   }).length == 0;
       
    36 };
       
    37 
       
    38 /**
       
    39  * An AutoComplete object.
       
    40  */
       
    41 Drupal.jsAC = function ($input, db) {
       
    42   var ac = this;
       
    43   this.input = $input[0];
       
    44   this.ariaLive = $('#' + this.input.id + '-autocomplete-aria-live');
       
    45   this.db = db;
       
    46 
       
    47   $input
       
    48     .keydown(function (event) { return ac.onkeydown(this, event); })
       
    49     .keyup(function (event) { ac.onkeyup(this, event); })
       
    50     .blur(function () { ac.hidePopup(); ac.db.cancel(); });
       
    51 
       
    52 };
       
    53 
       
    54 /**
       
    55  * Handler for the "keydown" event.
       
    56  */
       
    57 Drupal.jsAC.prototype.onkeydown = function (input, e) {
       
    58   if (!e) {
       
    59     e = window.event;
       
    60   }
       
    61   switch (e.keyCode) {
       
    62     case 40: // down arrow.
       
    63       this.selectDown();
       
    64       return false;
       
    65     case 38: // up arrow.
       
    66       this.selectUp();
       
    67       return false;
       
    68     default: // All other keys.
       
    69       return true;
       
    70   }
       
    71 };
       
    72 
       
    73 /**
       
    74  * Handler for the "keyup" event.
       
    75  */
       
    76 Drupal.jsAC.prototype.onkeyup = function (input, e) {
       
    77   if (!e) {
       
    78     e = window.event;
       
    79   }
       
    80   switch (e.keyCode) {
       
    81     case 16: // Shift.
       
    82     case 17: // Ctrl.
       
    83     case 18: // Alt.
       
    84     case 20: // Caps lock.
       
    85     case 33: // Page up.
       
    86     case 34: // Page down.
       
    87     case 35: // End.
       
    88     case 36: // Home.
       
    89     case 37: // Left arrow.
       
    90     case 38: // Up arrow.
       
    91     case 39: // Right arrow.
       
    92     case 40: // Down arrow.
       
    93       return true;
       
    94 
       
    95     case 9:  // Tab.
       
    96     case 13: // Enter.
       
    97     case 27: // Esc.
       
    98       this.hidePopup(e.keyCode);
       
    99       return true;
       
   100 
       
   101     default: // All other keys.
       
   102       if (input.value.length > 0 && !input.readOnly) {
       
   103         this.populatePopup();
       
   104       }
       
   105       else {
       
   106         this.hidePopup(e.keyCode);
       
   107       }
       
   108       return true;
       
   109   }
       
   110 };
       
   111 
       
   112 /**
       
   113  * Puts the currently highlighted suggestion into the autocomplete field.
       
   114  */
       
   115 Drupal.jsAC.prototype.select = function (node) {
       
   116   this.input.value = $(node).data('autocompleteValue');
       
   117   $(this.input).trigger('autocompleteSelect', [node]);
       
   118 };
       
   119 
       
   120 /**
       
   121  * Highlights the next suggestion.
       
   122  */
       
   123 Drupal.jsAC.prototype.selectDown = function () {
       
   124   if (this.selected && this.selected.nextSibling) {
       
   125     this.highlight(this.selected.nextSibling);
       
   126   }
       
   127   else if (this.popup) {
       
   128     var lis = $('li', this.popup);
       
   129     if (lis.length > 0) {
       
   130       this.highlight(lis.get(0));
       
   131     }
       
   132   }
       
   133 };
       
   134 
       
   135 /**
       
   136  * Highlights the previous suggestion.
       
   137  */
       
   138 Drupal.jsAC.prototype.selectUp = function () {
       
   139   if (this.selected && this.selected.previousSibling) {
       
   140     this.highlight(this.selected.previousSibling);
       
   141   }
       
   142 };
       
   143 
       
   144 /**
       
   145  * Highlights a suggestion.
       
   146  */
       
   147 Drupal.jsAC.prototype.highlight = function (node) {
       
   148   if (this.selected) {
       
   149     $(this.selected).removeClass('selected');
       
   150   }
       
   151   $(node).addClass('selected');
       
   152   this.selected = node;
       
   153   $(this.ariaLive).html($(this.selected).html());
       
   154 };
       
   155 
       
   156 /**
       
   157  * Unhighlights a suggestion.
       
   158  */
       
   159 Drupal.jsAC.prototype.unhighlight = function (node) {
       
   160   $(node).removeClass('selected');
       
   161   this.selected = false;
       
   162   $(this.ariaLive).empty();
       
   163 };
       
   164 
       
   165 /**
       
   166  * Hides the autocomplete suggestions.
       
   167  */
       
   168 Drupal.jsAC.prototype.hidePopup = function (keycode) {
       
   169   // Select item if the right key or mousebutton was pressed.
       
   170   if (this.selected && ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode)) {
       
   171     this.select(this.selected);
       
   172   }
       
   173   // Hide popup.
       
   174   var popup = this.popup;
       
   175   if (popup) {
       
   176     this.popup = null;
       
   177     $(popup).fadeOut('fast', function () { $(popup).remove(); });
       
   178   }
       
   179   this.selected = false;
       
   180   $(this.ariaLive).empty();
       
   181 };
       
   182 
       
   183 /**
       
   184  * Positions the suggestions popup and starts a search.
       
   185  */
       
   186 Drupal.jsAC.prototype.populatePopup = function () {
       
   187   var $input = $(this.input);
       
   188   var position = $input.position();
       
   189   // Show popup.
       
   190   if (this.popup) {
       
   191     $(this.popup).remove();
       
   192   }
       
   193   this.selected = false;
       
   194   this.popup = $('<div id="autocomplete"></div>')[0];
       
   195   this.popup.owner = this;
       
   196   $(this.popup).css({
       
   197     top: parseInt(position.top + this.input.offsetHeight, 10) + 'px',
       
   198     left: parseInt(position.left, 10) + 'px',
       
   199     width: $input.innerWidth() + 'px',
       
   200     display: 'none'
       
   201   });
       
   202   $input.before(this.popup);
       
   203 
       
   204   // Do search.
       
   205   this.db.owner = this;
       
   206   this.db.search(this.input.value);
       
   207 };
       
   208 
       
   209 /**
       
   210  * Fills the suggestion popup with any matches received.
       
   211  */
       
   212 Drupal.jsAC.prototype.found = function (matches) {
       
   213   // If no value in the textfield, do not show the popup.
       
   214   if (!this.input.value.length) {
       
   215     return false;
       
   216   }
       
   217 
       
   218   // Prepare matches.
       
   219   var ul = $('<ul></ul>');
       
   220   var ac = this;
       
   221   for (key in matches) {
       
   222     $('<li></li>')
       
   223       .html($('<div></div>').html(matches[key]))
       
   224       .mousedown(function () { ac.hidePopup(this); })
       
   225       .mouseover(function () { ac.highlight(this); })
       
   226       .mouseout(function () { ac.unhighlight(this); })
       
   227       .data('autocompleteValue', key)
       
   228       .appendTo(ul);
       
   229   }
       
   230 
       
   231   // Show popup with matches, if any.
       
   232   if (this.popup) {
       
   233     if (ul.children().length) {
       
   234       $(this.popup).empty().append(ul).show();
       
   235       $(this.ariaLive).html(Drupal.t('Autocomplete popup'));
       
   236     }
       
   237     else {
       
   238       $(this.popup).css({ visibility: 'hidden' });
       
   239       this.hidePopup();
       
   240     }
       
   241   }
       
   242 };
       
   243 
       
   244 Drupal.jsAC.prototype.setStatus = function (status) {
       
   245   switch (status) {
       
   246     case 'begin':
       
   247       $(this.input).addClass('throbbing');
       
   248       $(this.ariaLive).html(Drupal.t('Searching for matches...'));
       
   249       break;
       
   250     case 'cancel':
       
   251     case 'error':
       
   252     case 'found':
       
   253       $(this.input).removeClass('throbbing');
       
   254       break;
       
   255   }
       
   256 };
       
   257 
       
   258 /**
       
   259  * An AutoComplete DataBase object.
       
   260  */
       
   261 Drupal.ACDB = function (uri) {
       
   262   this.uri = uri;
       
   263   this.delay = 300;
       
   264   this.cache = {};
       
   265 };
       
   266 
       
   267 /**
       
   268  * Performs a cached and delayed search.
       
   269  */
       
   270 Drupal.ACDB.prototype.search = function (searchString) {
       
   271   var db = this;
       
   272   this.searchString = searchString;
       
   273 
       
   274   // See if this string needs to be searched for anyway. The pattern ../ is
       
   275   // stripped since it may be misinterpreted by the browser.
       
   276   searchString = searchString.replace(/^\s+|\.{2,}\/|\s+$/g, '');
       
   277   // Skip empty search strings, or search strings ending with a comma, since
       
   278   // that is the separator between search terms.
       
   279   if (searchString.length <= 0 ||
       
   280     searchString.charAt(searchString.length - 1) == ',') {
       
   281     return;
       
   282   }
       
   283 
       
   284   // See if this key has been searched for before.
       
   285   if (this.cache[searchString]) {
       
   286     return this.owner.found(this.cache[searchString]);
       
   287   }
       
   288 
       
   289   // Initiate delayed search.
       
   290   if (this.timer) {
       
   291     clearTimeout(this.timer);
       
   292   }
       
   293   this.timer = setTimeout(function () {
       
   294     db.owner.setStatus('begin');
       
   295 
       
   296     // Ajax GET request for autocompletion. We use Drupal.encodePath instead of
       
   297     // encodeURIComponent to allow autocomplete search terms to contain slashes.
       
   298     $.ajax({
       
   299       type: 'GET',
       
   300       url: db.uri + '/' + Drupal.encodePath(searchString),
       
   301       dataType: 'json',
       
   302       success: function (matches) {
       
   303         if (typeof matches.status == 'undefined' || matches.status != 0) {
       
   304           db.cache[searchString] = matches;
       
   305           // Verify if these are still the matches the user wants to see.
       
   306           if (db.searchString == searchString) {
       
   307             db.owner.found(matches);
       
   308           }
       
   309           db.owner.setStatus('found');
       
   310         }
       
   311       },
       
   312       error: function (xmlhttp) {
       
   313         Drupal.displayAjaxError(Drupal.ajaxError(xmlhttp, db.uri));
       
   314       }
       
   315     });
       
   316   }, this.delay);
       
   317 };
       
   318 
       
   319 /**
       
   320  * Cancels the current autocomplete request.
       
   321  */
       
   322 Drupal.ACDB.prototype.cancel = function () {
       
   323   if (this.owner) this.owner.setStatus('cancel');
       
   324   if (this.timer) clearTimeout(this.timer);
       
   325   this.searchString = '';
       
   326 };
       
   327 
       
   328 })(jQuery);