Click here to Skip to main content
15,881,803 members
Articles / Web Development / ASP.NET

Custom ASP.NET Editable DropDownList

Rate me:
Please Sign up or sign in to vote.
4.91/5 (54 votes)
15 Jan 2013CPOL6 min read 546.2K   33.4K   131  
A custom made DropDownList control for ASP.NET
/*
 * jQuery UI Autocomplete 1.8.16
 *
 * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about)
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * http://jquery.org/license
 *
 * http://docs.jquery.com/UI/Autocomplete
 *
 * Depends:
 *	jquery.ui.core.js
 *	jquery.ui.widget.js
 *	jquery.ui.position.js
 */
(function ($, undefined) {

    // used to prevent race conditions with remote data sources
    var requestIndex = 0;

    $.widget("ui.autocomplete", {
        options: {
            appendTo: "body",
            autoFocus: false,
            delay: 300,
            minLength: 1,
            position: {
                my: "left top",
                at: "left bottom",
                collision: "none"
            },
            source: null
        },

        pending: 0,

        _create: function () {
            var self = this,
			doc = this.element[0].ownerDocument,
			suppressKeyPress;

            this.element
			.addClass("ui-autocomplete-input")
			.attr("autocomplete", "off")
            // TODO verify these actually work as intended
			.attr({
			    role: "textbox",
			    "aria-autocomplete": "list",
			    "aria-haspopup": "true"
			})
			.bind("keydown.autocomplete", function (event) {
			    if (self.options.disabled || self.element.propAttr("readOnly")) {
			        return;
			    }

			    suppressKeyPress = false;
			    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:
			        case keyCode.NUMPAD_ENTER:
			            // when menu is open and has focus
			            if (self.menu.active) {
			                // #6055 - Opera still allows the keypress to occur
			                // which causes forms to submit
			                suppressKeyPress = true;
			                event.preventDefault();
			            }
			            //passthrough - ENTER and TAB both select the current element
			        case keyCode.TAB:
			            if (!self.menu.active) {
			                return;
			            }
			            self.menu.select(event);
			            break;
			        case keyCode.ESCAPE:
			            self.element.val(self.term);
			            self.close(event);
			            break;
			        default:
			            // keypress is triggered before the input value is changed
			            clearTimeout(self.searching);
			            self.searching = setTimeout(function () {
			                // only search if the value has changed
			                if (self.term != self.element.val()) {
			                    self.selectedItem = null;
			                    self.search(null, event);
			                }
			            }, self.options.delay);
			            break;
			    }
			})
			.bind("keypress.autocomplete", function (event) {
			    if (suppressKeyPress) {
			        suppressKeyPress = false;
			        event.preventDefault();
			    }
			})
			.bind("focus.autocomplete", function () {
			    if (self.options.disabled) {
			        return;
			    }

			    self.selectedItem = null;
			    self.previous = self.element.val();
			})
			.bind("blur.autocomplete", function (event) {
			    if (self.options.disabled) {
			        return;
			    }

			    self._change(event);

			    clearTimeout(self.searching);
			    // clicks on the menu (or a button to trigger a search) will cause a blur event
			    self.closing = setTimeout(function () {
			        self.close(event);
			    }, 150);
			});
            this._initSource();
            this.response = function () {
                return self._response.apply(self, arguments);
            };
            this.menu = $("<ul></ul>")
			.addClass("ui-autocomplete")
			.appendTo($(this.options.appendTo || "body", doc)[0])
            // prevent the close-on-blur in case of a "slow" click on the menu (long mousedown)
			.mousedown(function (event) {
			    // clicking on the scrollbar causes focus to shift to the body
			    // but we can't detect a mouseup or a click immediately afterward
			    // so we have to track the next mousedown and close the menu if
			    // the user clicks somewhere outside of the autocomplete
			    var menuElement = self.menu.element[0];
			    if (!$(event.target).closest(".ui-menu-item").length) {
			        setTimeout(function () {
			            $(document).one('mousedown', function (event) {
			                if (event.target !== self.element[0] &&
								event.target !== menuElement &&
								!$.ui.contains(menuElement, event.target)) {
			                    self.close();
			                }
			            });
			        }, 1);
			    }

			    // use another timeout to make sure the blur-event-handler on the input was already triggered
			    setTimeout(function () {
			        clearTimeout(self.closing);
			    }, 13);
			})
			.menu({
			    focus: function (event, ui) {
			        var item = ui.item.data("item.autocomplete");
			        if (false !== self._trigger("focus", event, { item: item })) {
			            // use value to match what will end up in the input, if it was a key event
			            if (/^key/.test(event.originalEvent.type)) {
			                self.element.val(item.value);
			            }
			        }
			    },
			    selected: function (event, ui) {
			        var item = ui.item.data("item.autocomplete"),
						previous = self.previous;

			        // only trigger when focus was lost (click on menu)
			        if (self.element[0] !== doc.activeElement) {
			            self.element.focus();
			            self.previous = previous;
			            // #6109 - IE triggers two focus events and the second
			            // is asynchronous, so we need to reset the previous
			            // term synchronously and asynchronously :-(
			            setTimeout(function () {
			                self.previous = previous;
			                self.selectedItem = item;
			            }, 1);
			        }

			        if (false !== self._trigger("select", event, { item: item })) {
			            self.element.val(item.value);
			        }
			        // reset the term after the select event
			        // this allows custom select handling to work properly
			        self.term = self.element.val();

			        self.close(event);
			        self.selectedItem = item;
			    },
			    blur: function (event, ui) {
			        // don't set the value of the text field if it's already correct
			        // this prevents moving the cursor unnecessarily
			        if (self.menu.element.is(":visible") &&
						(self.element.val() !== self.term)) {
			            self.element.val(self.term);
			        }
			    }
			})
			.zIndex(this.element.zIndex() + 1)
            // workaround for jQuery bug #5781 http://dev.jquery.com/ticket/5781
			.css({ top: 0, left: 0 })
			.hide()
			.data("menu");
            if ($.fn.bgiframe) {
                this.menu.element.bgiframe();
            }
        },

        destroy: function () {
            this.element
			.removeClass("ui-autocomplete-input")
			.removeAttr("autocomplete")
			.removeAttr("role")
			.removeAttr("aria-autocomplete")
			.removeAttr("aria-haspopup");
            this.menu.element.remove();
            $.Widget.prototype.destroy.call(this);
        },

        _setOption: function (key, value) {
            $.Widget.prototype._setOption.apply(this, arguments);
            if (key === "source") {
                this._initSource();
            }
            if (key === "appendTo") {
                this.menu.element.appendTo($(value || "body", this.element[0].ownerDocument)[0])
            }
            if (key === "disabled" && value && this.xhr) {
                this.xhr.abort();
            }
        },

        _initSource: function () {
            var self = this,
			array,
			url;
            if ($.isArray(this.options.source)) {
                array = this.options.source;
                this.source = function (request, response) {
                    response($.ui.autocomplete.filter(array, request.term));
                };
            } else if (typeof this.options.source === "string") {
                url = this.options.source;
                this.source = function (request, response) {
                    if (self.xhr) {
                        self.xhr.abort();
                    }
                    self.xhr = $.ajax({
                        url: url,
                        data: request,
                        dataType: "json",
                        autocompleteRequest: ++requestIndex,
                        success: function (data, status) {
                            if (this.autocompleteRequest === requestIndex) {
                                response(data);
                            }
                        },
                        error: function () {
                            if (this.autocompleteRequest === requestIndex) {
                                response([]);
                            }
                        }
                    });
                };
            } else {
                this.source = this.options.source;
            }
        },

        search: function (value, event) {
            value = value != null ? value : this.element.val();

            // always save the actual value, not the one passed as an argument
            this.term = this.element.val();

            if (value.length < this.options.minLength) {
                return this.close(event);
            }

            clearTimeout(this.closing);
            if (this._trigger("search", event) === false) {
                return;
            }

            return this._search(value);
        },

        _search: function (value) {
            this.pending++;
            this.element.addClass("ui-autocomplete-loading");

            this.source({ term: value }, this.response);
        },

        _response: function (content) {
            if (!this.options.disabled && content && content.length) {
                content = this._normalize(content);
                this._suggest(content);
                this._trigger("open");
            } else {
                this.close();
            }
            this.pending--;
            if (!this.pending) {
                this.element.removeClass("ui-autocomplete-loading");
            }
        },

        close: function (event) {
            clearTimeout(this.closing);
            if (this.menu.element.is(":visible")) {
                this.menu.element.hide();
                this.menu.deactivate();
                this._trigger("close", event);
            }
        },

        _change: function (event) {
            if (this.previous !== this.element.val()) {
                this._trigger("change", event, { item: this.selectedItem });
            }
        },

        _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) {
            var ul = this.menu.element
			.empty()
			.zIndex(this.element.zIndex() + 1);
            this._renderMenu(ul, items);
            // TODO refresh should check if the active item is still in the dom, removing the need for a manual deactivate
            this.menu.deactivate();
            this.menu.refresh();

            // size and position menu
            ul.show();
            this._resizeMenu();
            ul.position($.extend({
                of: this.element
            }, this.options.position));

            if (this.options.autoFocus) {
                this.menu.next(new $.Event("mouseover"));
            }
        },

        _resizeMenu: function () {
            var ul = this.menu.element;
            ul.outerWidth(Math.max(
			ul.width("").outerWidth(),
			this.element.outerWidth()
		));
        },

        _renderMenu: function (ul, items) {
            var self = this;
            $.each(items, function (index, item) {
                self._renderItem(ul, item);
            });
        },

        _renderItem: function (ul, item) {
            return $("<li></li>")
			.data("item.autocomplete", item)
			.append($("<a></a>").text(item.label))
			.appendTo(ul);
        },

        _move: function (direction, event) {
            if (!this.menu.element.is(":visible")) {
                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](event);
        },

        widget: function () {
            return this.menu.element;
        }
    });

    $.extend($.ui.autocomplete, {
        escapeRegex: function (value) {
            return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
        },
        filter: function (array, term) {
            var matcher = new RegExp($.ui.autocomplete.escapeRegex(term), "i");
            return $.grep(array, function (value) {
                return matcher.test(value.label || value.value || value);
            });
        }
    });

} (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 2010, AUTHORS.txt (http://jqueryui.com/about)
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * http://jquery.org/license
 *
 * http://docs.jquery.com/UI/Menu
 *
 * Depends:
 *	jquery.ui.core.js
 *  jquery.ui.widget.js
 */
(function($) {

$.widget("ui.menu", {
	_create: function() {
		var self = this;
		this.element
			.addClass("ui-menu ui-widget ui-widget-content ui-corner-all")
			.attr({
				role: "listbox",
				"aria-activedescendant": "ui-active-menuitem"
			})
			.click(function( event ) {
				if ( !$( event.target ).closest( ".ui-menu-item a" ).length ) {
					return;
				}
				// temporary
				event.preventDefault();
				self.select( event );
			});
		this.refresh();
	},
	
	refresh: function() {
		var self = this;

		// don't refresh list items that are already adapted
		var items = this.element.children("li:not(.ui-menu-item):has(a)")
			.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( event ) {
				self.activate( event, $(this).parent() );
			})
			.mouseleave(function() {
				self.deactivate();
			});
	},

	activate: function( event, item ) {
		this.deactivate();
		if (this.hasScroll()) {
			var offset = item.offset().top - this.element.offset().top,
				scroll = this.element.scrollTop(),
				elementHeight = this.element.height();
			if (offset < 0) {
				this.element.scrollTop( scroll + offset);
			} else if (offset >= elementHeight) {
				this.element.scrollTop( scroll + offset - elementHeight + item.height());
			}
		}
		this.active = item.eq(0)
			.children("a")
				.addClass("ui-state-hover")
				.attr("id", "ui-active-menuitem")
			.end();
		this._trigger("focus", event, { item: item });
	},

	deactivate: function() {
		if (!this.active) { return; }

		this.active.children("a")
			.removeClass("ui-state-hover")
			.removeAttr("id");
		this._trigger("blur");
		this.active = null;
	},

	next: function(event) {
		this.move("next", ".ui-menu-item:first", event);
	},

	previous: function(event) {
		this.move("prev", ".ui-menu-item:last", event);
	},

	first: function() {
		return this.active && !this.active.prevAll(".ui-menu-item").length;
	},

	last: function() {
		return this.active && !this.active.nextAll(".ui-menu-item").length;
	},

	move: function(direction, edge, event) {
		if (!this.active) {
			this.activate(event, this.element.children(edge));
			return;
		}
		var next = this.active[direction + "All"](".ui-menu-item").eq(0);
		if (next.length) {
			this.activate(event, next);
		} else {
			this.activate(event, this.element.children(edge));
		}
	},

	// TODO merge with previousPage
	nextPage: function(event) {
		if (this.hasScroll()) {
			// TODO merge with no-scroll-else
			if (!this.active || this.last()) {
				this.activate(event, this.element.children(".ui-menu-item:first"));
				return;
			}
			var base = this.active.offset().top,
				height = this.element.height(),
				result = this.element.children(".ui-menu-item").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(".ui-menu-item:last");
			}
			this.activate(event, result);
		} else {
			this.activate(event, this.element.children(".ui-menu-item")
				.filter(!this.active || this.last() ? ":first" : ":last"));
		}
	},

	// TODO merge with nextPage
	previousPage: function(event) {
		if (this.hasScroll()) {
			// TODO merge with no-scroll-else
			if (!this.active || this.first()) {
				this.activate(event, this.element.children(".ui-menu-item:last"));
				return;
			}

			var base = this.active.offset().top,
				height = this.element.height();
				result = this.element.children(".ui-menu-item").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(".ui-menu-item:first");
			}
			this.activate(event, result);
		} else {
			this.activate(event, this.element.children(".ui-menu-item")
				.filter(!this.active || this.first() ? ":last" : ":first"));
		}
	},

	hasScroll: function() {
		return this.element.height() < this.element[ $.fn.prop ? "prop" : "attr" ]("scrollHeight");
	},

	select: function( event ) {
		this._trigger("selected", event, { item: this.active });
	}
});

}(jQuery));

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead ABILITY Network Inc
United States United States
Began writing software at the age of 10.
Graduated from the University of Houston in 1995 with a BS in Computer Science and a minor in Math.
I love developing great software and websites.
I recently moved to Seattle and I'm currently working for a Health Care Analytics company.

Hobbies include Basketball and Soccer and very happily married to a wonderful wife.

Comments and Discussions