aboutsummaryrefslogtreecommitdiffstats
path: root/ui/jquery.ui.autocomplete.js
diff options
context:
space:
mode:
Diffstat (limited to 'ui/jquery.ui.autocomplete.js')
-rw-r--r--ui/jquery.ui.autocomplete.js445
1 files changed, 445 insertions, 0 deletions
diff --git a/ui/jquery.ui.autocomplete.js b/ui/jquery.ui.autocomplete.js
new file mode 100644
index 000000000..3a6c5607c
--- /dev/null
+++ b/ui/jquery.ui.autocomplete.js
@@ -0,0 +1,445 @@
+/*
+ * jQuery UI Autocomplete @VERSION
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Autocomplete
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.widget.js
+ */
+(function($) {
+
+$.widget("ui.autocomplete", {
+ options: {
+ minLength: 1,
+ delay: 300
+ },
+ _init: function() {
+ var self = this;
+ this.element
+ .addClass("ui-autocomplete ui-widget ui-widget-content ui-corner-all")
+ .attr("autocomplete", "off")
+ // TODO verify these actually work as intended
+ .attr({
+ role: "textbox",
+ "aria-autocomplete": "list",
+ "aria-haspopup": "true"
+ })
+ .bind("keydown.autocomplete", function(event) {
+ var keyCode = $.ui.keyCode;
+ switch(event.keyCode) {
+ case keyCode.PAGE_UP:
+ self._move("previousPage", event);
+ break;
+ case keyCode.PAGE_DOWN:
+ self._move("nextPage", event);
+ break;
+ case keyCode.UP:
+ self._move("previous", event);
+ // prevent moving cursor to beginning of text field in some browsers
+ event.preventDefault();
+ break;
+ case keyCode.DOWN:
+ self._move("next", event);
+ // prevent moving cursor to end of text field in some browsers
+ event.preventDefault();
+ break;
+ case keyCode.ENTER:
+ // when menu is open or has focus
+ if (self.menu && self.menu.active) {
+ event.preventDefault();
+ }
+ case keyCode.TAB:
+ if (!self.menu || !self.menu.active) {
+ return;
+ }
+ self.menu.select();
+ break;
+ case keyCode.ESCAPE:
+ self.element.val(self.term);
+ self.close(event);
+ break;
+ case 16:
+ case 17:
+ case 18:
+ // ignore metakeys (shift, ctrl, alt)
+ break;
+ default:
+ // keypress is triggered before the input value is changed
+ clearTimeout(self.searching);
+ self.searching = setTimeout(function() {
+ self.search(null, event);
+ }, self.options.delay);
+ break;
+ }
+ })
+ .bind("focus.autocomplete", function() {
+ self.previous = self.element.val();
+ })
+ .bind("blur.autocomplete", function(event) {
+ clearTimeout(self.searching);
+ // clicks on the menu (or a button to trigger a search) will cause a blur event
+ // TODO try to implement this without a timeout, see clearTimeout in search()
+ self.closing = setTimeout(function() {
+ self.close(event);
+ }, 150);
+ });
+ this._initSource();
+ this.response = function() {
+ return self._response.apply(self, arguments);
+ };
+ },
+
+ destroy: function() {
+ this.element
+ .removeClass("ui-autocomplete ui-widget ui-widget-content ui-corner-all")
+ .removeAttr("autocomplete")
+ .removeAttr("role")
+ .removeAttr("aria-autocomplete")
+ .removeAttr("aria-haspopup");
+ if (this.menu) {
+ this.menu.element.remove();
+ }
+ $.Widget.prototype.destroy.call(this);
+ },
+
+ _setOption: function(key) {
+ $.Widget.prototype._setOption.apply(this, arguments);
+ if (key == "source") {
+ this._initSource();
+ }
+ },
+
+ _initSource: function() {
+ if ($.isArray(this.options.source)) {
+ var array = this.options.source;
+ this.source = function(request, response) {
+ // escape regex characters
+ var matcher = new RegExp($.ui.autocomplete.escapeRegex(request.term), "i");
+ response($.grep(array, function(value) {
+ return matcher.test(value.value || value.label || value);
+ }));
+ };
+ } else if (typeof this.options.source == "string") {
+ var url = this.options.source;
+ this.source = function(request, response) {
+ $.getJSON(url, request, response);
+ };
+ } else {
+ this.source = this.options.source;
+ }
+ },
+
+ search: function(value, event) {
+ value = value != null ? value : this.element.val();
+ if (value.length < this.options.minLength) {
+ return this.close(event);
+ }
+
+ clearTimeout(this.closing);
+ if (this._trigger("search") === false) {
+ return;
+ }
+
+ return this._search(value);
+ },
+
+ _search: function(value) {
+ this.term = this.element
+ .addClass("ui-autocomplete-loading")
+ // always save the actual value, not the one passed as an argument
+ .val();
+
+ this.source({ term: value }, this.response);
+ },
+
+ _response: function(content) {
+ if (content.length) {
+ content = this._normalize(content);
+ this._trigger("open");
+ this._suggest(content);
+ } else {
+ this.close();
+ }
+ this.element.removeClass("ui-autocomplete-loading");
+ },
+
+ close: function(event) {
+ clearTimeout(this.closing);
+ if (this.menu) {
+ this._trigger("close", event);
+ this.menu.element.remove();
+ this.menu = null;
+ }
+ if (this.previous != this.element.val()) {
+ this._trigger("change", event);
+ }
+ },
+
+ _normalize: function(items) {
+ // assume all items have the right format when the first item is complete
+ if (items.length && items[0].label && items[0].value) {
+ return items;
+ }
+ return $.map(items, function(item) {
+ if (typeof item == "string") {
+ return {
+ label: item,
+ value: item
+ };
+ }
+ return $.extend({
+ label: item.label || item.value,
+ value: item.value || item.label
+ }, item);
+ });
+ },
+
+ _suggest: function(items) {
+ (this.menu && this.menu.element.remove());
+ var self = this,
+ ul = $("<ul></ul>"),
+ parent = this.element.parent();
+
+ $.each(items, function(index, item) {
+ $("<li></li>")
+ .data("item.autocomplete", item)
+ .append("<a>" + item.label + "</a>")
+ .appendTo(ul);
+ });
+ this.menu = ul
+ .addClass("ui-autocomplete-menu")
+ .appendTo(parent)
+ .menu({
+ focus: function(event, ui) {
+ var item = ui.item.data("item.autocomplete");
+ if (false !== self._trigger("focus", null, { item: item })) {
+ // use value to match what will end up in the input
+ self.element.val(item.value);
+ }
+ },
+ selected: function(event, ui) {
+ var item = ui.item.data("item.autocomplete");
+ if (false !== self._trigger('select', event, { item: item })) {
+ self.element.val( item.value );
+ }
+ self.close(event);
+ self.previous = self.element.val();
+ // only trigger when focus was lost (click on menu)
+ if (self.element[0] != document.activeElement) {
+ self.element.focus();
+ }
+ }
+ })
+ // workaround for jQuery bug #5781 http://dev.jquery.com/ticket/5781
+ .css({ top: 0, left: 0 })
+ .position({
+ my: "left top",
+ at: "left bottom",
+ of: this.element
+ })
+ .data("menu");
+ if (ul.width() <= this.element.width()) {
+ ul.width(this.element.width());
+ }
+ },
+
+ _move: function(direction, event) {
+ if (!this.menu) {
+ this.search(null, event);
+ return;
+ }
+ if (this.menu.first() && /^previous/.test(direction)
+ || this.menu.last() && /^next/.test(direction)) {
+ this.element.val(this.term);
+ this.menu.deactivate();
+ return;
+ }
+ this.menu[direction]();
+ },
+
+ widget: function() {
+ // return empty jQuery object when menu isn't initialized yet
+ return this.menu && this.menu.element || $([]);
+ }
+});
+
+$.extend($.ui.autocomplete, {
+ escapeRegex: function(value) {
+ return value.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1");
+ }
+});
+
+})(jQuery);
+
+/*
+ * jQuery UI Menu (not officially released)
+ *
+ * This widget isn't yet finished and the API is subject to change. We plan to finish
+ * it for the next release. You're welcome to give it a try anyway and give us feedback,
+ * as long as you're okay with migrating your code later on. We can help with that, too.
+ *
+ * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * http://docs.jquery.com/UI/Menu
+ *
+ * Depends:
+ * jquery.ui.core.js
+ * jquery.ui.widget.js
+ */
+(function($) {
+
+$.widget("ui.menu", {
+ _init: function() {
+ var self = this;
+ this.element
+ .addClass("ui-menu ui-widget ui-widget-content ui-corner-all")
+ .attr({
+ role: "menu",
+ "aria-activedescendant": "ui-active-menuitem"
+ })
+ .click(function(e) {
+ // temporary
+ e.preventDefault();
+ self.select();
+ });
+ var items = this.element.children("li")
+ .addClass("ui-menu-item")
+ .attr("role", "menuitem");
+
+ items.children("a")
+ .addClass("ui-corner-all")
+ .attr("tabindex", -1)
+ // mouseenter doesn't work with event delegation
+ .mouseenter(function() {
+ self.activate($(this).parent());
+ });
+ },
+
+ activate: function(item) {
+ this.deactivate();
+ this.active = item.eq(0)
+ .children("a")
+ .addClass("ui-state-hover")
+ .attr("id", "ui-active-menuitem")
+ .end();
+ this._trigger("focus", null, { item: item });
+ if (this.hasScroll()) {
+ var offset = item.offset().top - this.element.offset().top,
+ scroll = this.element.attr("scrollTop"),
+ elementHeight = this.element.height();
+ if (offset < 0) {
+ this.element.attr("scrollTop", scroll + offset);
+ } else if (offset > elementHeight) {
+ this.element.attr("scrollTop", scroll + offset - elementHeight + item.height());
+ }
+ }
+ },
+
+ deactivate: function() {
+ if (!this.active) { return; }
+
+ this.active.children("a")
+ .removeClass("ui-state-hover")
+ .removeAttr("id");
+ this.active = null;
+ },
+
+ next: function() {
+ this.move("next", "li:first");
+ },
+
+ previous: function() {
+ this.move("prev", "li:last");
+ },
+
+ first: function() {
+ return this.active && !this.active.prev().length;
+ },
+
+ last: function() {
+ return this.active && !this.active.next().length;
+ },
+
+ move: function(direction, edge) {
+ if (!this.active) {
+ this.activate(this.element.children(edge));
+ return;
+ }
+ var next = this.active[direction]();
+ if (next.length) {
+ this.activate(next);
+ } else {
+ this.activate(this.element.children(edge));
+ }
+ },
+
+ // TODO merge with previousPage
+ nextPage: function() {
+ if (this.hasScroll()) {
+ // TODO merge with no-scroll-else
+ if (!this.active || this.last()) {
+ this.activate(this.element.children(":first"));
+ return;
+ }
+ var base = this.active.offset().top,
+ height = this.element.height(),
+ result = this.element.children("li").filter(function() {
+ var close = $(this).offset().top - base - height + $(this).height();
+ // TODO improve approximation
+ return close < 10 && close > -10;
+ });
+
+ // TODO try to catch this earlier when scrollTop indicates the last page anyway
+ if (!result.length) {
+ result = this.element.children(":last");
+ }
+ this.activate(result);
+ } else {
+ this.activate(this.element.children(!this.active || this.last() ? ":first" : ":last"));
+ }
+ },
+
+ // TODO merge with nextPage
+ previousPage: function() {
+ if (this.hasScroll()) {
+ // TODO merge with no-scroll-else
+ if (!this.active || this.first()) {
+ this.activate(this.element.children(":last"));
+ return;
+ }
+
+ var base = this.active.offset().top,
+ height = this.element.height();
+ result = this.element.children("li").filter(function() {
+ var close = $(this).offset().top - base + height - $(this).height();
+ // TODO improve approximation
+ return close < 10 && close > -10;
+ });
+
+ // TODO try to catch this earlier when scrollTop indicates the last page anyway
+ if (!result.length) {
+ result = this.element.children(":first");
+ }
+ this.activate(result);
+ } else {
+ this.activate(this.element.children(!this.active || this.first() ? ":last" : ":first"));
+ }
+ },
+
+ hasScroll: function() {
+ return this.element.height() < this.element.attr("scrollHeight");
+ },
+
+ select: function() {
+ this._trigger("selected", null, { item: this.active });
+ }
+});
+
+})(jQuery); \ No newline at end of file