diff --git a/includes/form.inc b/includes/form.inc index c0163ca..c756b50 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -3639,14 +3639,7 @@ function theme_textfield($variables) { if ($element['#autocomplete_path'] && drupal_valid_path($element['#autocomplete_path'])) { drupal_add_library('system', 'drupal.autocomplete'); $element['#attributes']['class'][] = 'form-autocomplete'; - - $attributes = array(); - $attributes['type'] = 'hidden'; - $attributes['id'] = $element['#attributes']['id'] . '-autocomplete'; - $attributes['value'] = url($element['#autocomplete_path'], array('absolute' => TRUE)); - $attributes['disabled'] = 'disabled'; - $attributes['class'][] = 'autocomplete'; - $extra = ''; + $element['#attributes']['data-autocomplete-path'] = url($element['#autocomplete_path'], array('absolute' => TRUE)); } $output = ''; diff --git a/misc/autocomplete.js b/misc/autocomplete.js index 5e85be4..a7a51b7 100644 --- a/misc/autocomplete.js +++ b/misc/autocomplete.js @@ -5,317 +5,65 @@ */ Drupal.behaviors.autocomplete = { attach: function (context, settings) { - var acdb = []; - $('input.autocomplete', context).once('autocomplete', function () { - var uri = this.value; - if (!acdb[uri]) { - acdb[uri] = new Drupal.ACDB(uri); - } - var $input = $('#' + this.id.substr(0, this.id.length - 13)) - .attr('autocomplete', 'OFF') - .attr('aria-autocomplete', 'list'); - $($input[0].form).submit(Drupal.autocompleteSubmit); - $input.parent() - .attr('role', 'application') - .append($('') - .attr('id', $input.attr('id') + '-autocomplete-aria-live') - ); - new Drupal.jsAC($input, acdb[uri]); - }); - } -}; - -/** - * Prevents the form from submitting if the suggestions popup is open - * and closes the suggestions popup when doing so. - */ -Drupal.autocompleteSubmit = function () { - return $('#autocomplete').each(function () { - this.owner.hidePopup(); - }).size() == 0; -}; - -/** - * An AutoComplete object. - */ -Drupal.jsAC = function ($input, db) { - var ac = this; - this.input = $input[0]; - this.ariaLive = $('#' + $input.attr('id') + '-autocomplete-aria-live'); - this.db = db; - - $input - .keydown(function (event) { return ac.onkeydown(this, event); }) - .keyup(function (event) { ac.onkeyup(this, event); }) - .blur(function () { ac.hidePopup(); ac.db.cancel(); }); - -}; - -/** - * Handler for the "keydown" event. - */ -Drupal.jsAC.prototype.onkeydown = function (input, e) { - if (!e) { - e = window.event; - } - switch (e.keyCode) { - case 40: // down arrow. - this.selectDown(); - return false; - case 38: // up arrow. - this.selectUp(); - return false; - default: // All other keys. - return true; - } -}; - -/** - * Handler for the "keyup" event. - */ -Drupal.jsAC.prototype.onkeyup = function (input, e) { - if (!e) { - e = window.event; - } - switch (e.keyCode) { - case 16: // Shift. - case 17: // Ctrl. - case 18: // Alt. - case 20: // Caps lock. - case 33: // Page up. - case 34: // Page down. - case 35: // End. - case 36: // Home. - case 37: // Left arrow. - case 38: // Up arrow. - case 39: // Right arrow. - case 40: // Down arrow. - return true; - - case 9: // Tab. - case 13: // Enter. - case 27: // Esc. - this.hidePopup(e.keyCode); - return true; - - default: // All other keys. - if (input.value.length > 0) - this.populatePopup(); - else - this.hidePopup(e.keyCode); - return true; - } -}; - -/** - * Puts the currently highlighted suggestion into the autocomplete field. - */ -Drupal.jsAC.prototype.select = function (node) { - this.input.value = $(node).data('autocompleteValue'); -}; - -/** - * Highlights the next suggestion. - */ -Drupal.jsAC.prototype.selectDown = function () { - if (this.selected && this.selected.nextSibling) { - this.highlight(this.selected.nextSibling); - } - else if (this.popup) { - var lis = $('li', this.popup); - if (lis.size() > 0) { - this.highlight(lis.get(0)); - } - } -}; - -/** - * Highlights the previous suggestion. - */ -Drupal.jsAC.prototype.selectUp = function () { - if (this.selected && this.selected.previousSibling) { - this.highlight(this.selected.previousSibling); - } -}; - -/** - * Highlights a suggestion. - */ -Drupal.jsAC.prototype.highlight = function (node) { - if (this.selected) { - $(this.selected).removeClass('selected'); - } - $(node).addClass('selected'); - this.selected = node; - $(this.ariaLive).html($(this.selected).html()); -}; - -/** - * Unhighlights a suggestion. - */ -Drupal.jsAC.prototype.unhighlight = function (node) { - $(node).removeClass('selected'); - this.selected = false; - $(this.ariaLive).empty(); -}; - -/** - * Hides the autocomplete suggestions. - */ -Drupal.jsAC.prototype.hidePopup = function (keycode) { - // Select item if the right key or mousebutton was pressed. - if (this.selected && ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode)) { - this.input.value = $(this.selected).data('autocompleteValue'); - } - // Hide popup. - var popup = this.popup; - if (popup) { - this.popup = null; - $(popup).fadeOut('fast', function () { $(popup).remove(); }); - } - this.selected = false; - $(this.ariaLive).empty(); -}; - -/** - * Positions the suggestions popup and starts a search. - */ -Drupal.jsAC.prototype.populatePopup = function () { - var $input = $(this.input); - var position = $input.position(); - // Show popup. - if (this.popup) { - $(this.popup).remove(); - } - this.selected = false; - this.popup = $('
')[0]; - this.popup.owner = this; - $(this.popup).css({ - top: parseInt(position.top + this.input.offsetHeight, 10) + 'px', - left: parseInt(position.left, 10) + 'px', - width: $input.innerWidth() + 'px', - display: 'none' - }); - $input.before(this.popup); - - // Do search. - this.db.owner = this; - this.db.search(this.input.value); -}; - -/** - * Fills the suggestion popup with any matches received. - */ -Drupal.jsAC.prototype.found = function (matches) { - // If no value in the textfield, do not show the popup. - if (!this.input.value.length) { - return false; - } - - // Prepare matches. - var ul = $(''); - var ac = this; - for (key in matches) { - $('
  • ') - .html($('
    ').html(matches[key])) - .mousedown(function () { ac.select(this); }) - .mouseover(function () { ac.highlight(this); }) - .mouseout(function () { ac.unhighlight(this); }) - .data('autocompleteValue', key) - .appendTo(ul); - } - - // Show popup with matches, if any. - if (this.popup) { - if (ul.children().size()) { - $(this.popup).empty().append(ul).show(); - $(this.ariaLive).html(Drupal.t('Autocomplete popup')); - } - else { - $(this.popup).css({ visibility: 'hidden' }); - this.hidePopup(); - } - } -}; - -Drupal.jsAC.prototype.setStatus = function (status) { - switch (status) { - case 'begin': - $(this.input).addClass('throbbing'); - $(this.ariaLive).html(Drupal.t('Searching for matches...')); - break; - case 'cancel': - case 'error': - case 'found': - $(this.input).removeClass('throbbing'); - break; - } -}; - -/** - * An AutoComplete DataBase object. - */ -Drupal.ACDB = function (uri) { - this.uri = uri; - this.delay = 300; - this.cache = {}; -}; - -/** - * Performs a cached and delayed search. - */ -Drupal.ACDB.prototype.search = function (searchString) { - var db = this; - this.searchString = searchString; - - // See if this string needs to be searched for anyway. - searchString = searchString.replace(/^\s+|\s+$/, ''); - if (searchString.length <= 0 || - searchString.charAt(searchString.length - 1) == ',') { - return; - } - - // See if this key has been searched for before. - if (this.cache[searchString]) { - return this.owner.found(this.cache[searchString]); - } - - // Initiate delayed search. - if (this.timer) { - clearTimeout(this.timer); - } - this.timer = setTimeout(function () { - db.owner.setStatus('begin'); - - // Ajax GET request for autocompletion. - $.ajax({ - type: 'GET', - url: db.uri + '/' + encodeURIComponent(searchString), - dataType: 'json', - success: function (matches) { - if (typeof matches.status == 'undefined' || matches.status != 0) { - db.cache[searchString] = matches; - // Verify if these are still the matches the user wants to see. - if (db.searchString == searchString) { - db.owner.found(matches); + // Act on textfields with the "form-autocomplete" class. + $('input.form-autocomplete', context).once('autocomplete', function() { + // Use jQuery UI Autocomplete on the textfield. + $(this).autocomplete({ + // The autcomplete source is provided by the data-autocomplete-path + // attribute within the element itself. + source: function(request, response) { + // Get the desired term and construct the autocomplete URL for it. + var term = Drupal.autocompleteextractLast(request.term); + var url = $(this.element).attr('data-autocomplete-path') + '/' + encodeURIComponent(term); + // Make the AJAX request. + $.getJSON(url, function(data) { + // Drupal returns an object array, but we need a string array. + var terms = new Array(); + $.each(data, function(key, value) { + terms.push(value); + }); + // Send the new string array of terms to the jQuery UI list. + response(terms); + }); + }, + search: function() { + // Only search if there has been more then two characters in there. + var term = Drupal.autocompleteextractLast(this.value); + if (term.length < 2) { + return false; } - db.owner.setStatus('found'); + }, + focus: function() { + // Prevent value inserted on focus. + return false; + }, + // It will select the search term based on the last item in the list. + select: function(event, ui) { + var terms = Drupal.autocompletesplit(this.value); + // Remove the current input. + terms.pop(); + // Add the selected item. + terms.push(ui.item.value); + this.value = terms.join(", "); + return false; } - }, - error: function (xmlhttp) { - alert(Drupal.ajaxError(xmlhttp, db.uri)); + }); + }); + + // Pop up the autocomplete if tab was entered. + $(this).bind('keydown', function(event) { + if (event.keyCode === $.ui.keyCode.TAB && $(this).data("autocomplete").menu.active) { + event.preventDefault(); } }); - }, this.delay); + } }; -/** - * Cancels the current autocomplete request. - */ -Drupal.ACDB.prototype.cancel = function () { - if (this.owner) this.owner.setStatus('cancel'); - if (this.timer) clearTimeout(this.timer); - this.searchString = ''; +Drupal.autocompletesplit = function(val) { + return val.split(/,\s*/); +}; +Drupal.autocompleteextractLast = function(term) { + return Drupal.autocompletesplit(term).pop(); }; })(jQuery); diff --git a/modules/system/system.base-rtl.css b/modules/system/system.base-rtl.css index 9099c9d..5ea42ed 100644 --- a/modules/system/system.base-rtl.css +++ b/modules/system/system.base-rtl.css @@ -4,14 +4,11 @@ * Generic theme-independent base styles. */ -/** - * Autocomplete. - */ /* Animated throbber */ html.js input.form-autocomplete { background-position: 0% 2px; } -html.js input.throbbing { +html.js input.form-autocomplete.ui-autocomplete-loading { background-position: 0% -18px; } diff --git a/modules/system/system.base.css b/modules/system/system.base.css index a6748de..d6127e5 100644 --- a/modules/system/system.base.css +++ b/modules/system/system.base.css @@ -4,37 +4,13 @@ * Generic theme-independent base styles. */ -/** - * Autocomplete. - * - * @see autocomplete.js - */ -/* Suggestion list */ -#autocomplete { - border: 1px solid; - overflow: hidden; - position: absolute; - z-index: 100; -} -#autocomplete ul { - list-style: none; - list-style-image: none; - margin: 0; - padding: 0; -} -#autocomplete li { - background: #fff; - color: #000; - cursor: default; - white-space: pre; -} /* Animated throbber */ html.js input.form-autocomplete { background-image: url(../../misc/throbber.gif); background-position: 100% 2px; /* LTR */ background-repeat: no-repeat; } -html.js input.throbbing { +html.js input.form-autocomplete.ui-autocomplete-loading { background-position: 100% -18px; /* LTR */ } diff --git a/modules/system/system.module b/modules/system/system.module index 21b23f4..309f3ed 100644 --- a/modules/system/system.module +++ b/modules/system/system.module @@ -1159,6 +1159,9 @@ function system_library() { 'js' => array( 'misc/autocomplete.js' => array('group' => JS_DEFAULT), ), + 'dependencies' => array( + array('system', 'ui.autocomplete'), + ), ); // jQuery. diff --git a/themes/bartik/css/style.css b/themes/bartik/css/style.css index 75e43b0..72a3bfa 100644 --- a/themes/bartik/css/style.css +++ b/themes/bartik/css/style.css @@ -1330,9 +1330,6 @@ input.form-button-disabled:active, } /* Animated throbber */ -html.js input.form-autocomplete { - background-position: 100% 4px; /* LTR */ -} html.js input.throbbing { background-position: 100% -16px; /* LTR */ } diff --git a/themes/garland/style.css b/themes/garland/style.css index f36fa96..55b9181 100644 --- a/themes/garland/style.css +++ b/themes/garland/style.css @@ -927,15 +927,6 @@ tr.even td.menu-disabled { } /** - * Autocomplete. - */ -#autocomplete li { - cursor: default; - padding: 2px; - margin: 0; -} - -/** * Collapsible fieldsets */ fieldset { @@ -1137,7 +1128,6 @@ table.system-status-report th { border-color: #d3e7f4; } -#autocomplete li.selected, tr.selected td, tr.selected td.active { background: #027ac6; diff --git a/themes/seven/style.css b/themes/seven/style.css index 66303c5..00a3ed9 100644 --- a/themes/seven/style.css +++ b/themes/seven/style.css @@ -594,7 +594,6 @@ div.teaser-checkbox .form-item, .form-item label.option input { vertical-align: middle; } -.form-disabled input.form-autocomplete, .form-disabled input.form-text, .form-disabled input.form-file, .form-disabled textarea.form-textarea, @@ -681,7 +680,6 @@ input.form-button-disabled:active { text-shadow: none; color: #999; } -input.form-autocomplete, input.form-text, input.form-file, textarea.form-textarea, @@ -699,9 +697,6 @@ select.form-select:focus { color: #000; border-color: #ace; } -html.js input.form-autocomplete { - background-position: 100% 4px; -} html.js input.throbbing { background-position: 100% -16px; }