summaryrefslogtreecommitdiffstatshomepage
path: root/core/misc/autocomplete.js
diff options
context:
space:
mode:
Diffstat (limited to 'core/misc/autocomplete.js')
-rw-r--r--core/misc/autocomplete.js321
1 files changed, 321 insertions, 0 deletions
diff --git a/core/misc/autocomplete.js b/core/misc/autocomplete.js
new file mode 100644
index 00000000000..5e85be44fc5
--- /dev/null
+++ b/core/misc/autocomplete.js
@@ -0,0 +1,321 @@
+(function ($) {
+
+/**
+ * Attaches the autocomplete behavior to all required fields.
+ */
+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($('<span class="element-invisible" aria-live="assertive"></span>')
+ .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 = $('<div id="autocomplete"></div>')[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 = $('<ul></ul>');
+ var ac = this;
+ for (key in matches) {
+ $('<li></li>')
+ .html($('<div></div>').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);
+ }
+ db.owner.setStatus('found');
+ }
+ },
+ error: function (xmlhttp) {
+ alert(Drupal.ajaxError(xmlhttp, db.uri));
+ }
+ });
+ }, 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 = '';
+};
+
+})(jQuery);