commit 53e214d32f39ec78670ccf58df1536375bdf4112 Author: Igor Vaynberg Date: Sun Mar 4 10:58:26 2012 -0800 initial checkin diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..60b675e3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,16 @@ +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. diff --git a/README.md b/README.md new file mode 100755 index 00000000..74bca4be --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +Select2 +================= + +Select2 is a jQuery based replacement for select boxes. It supports searching, remote data sets, and infinite scrolling of results. Look and feel of Select2 is based on the excellent [Chosen](http://harvesthq.github.com/chosen/) library. + +To get started -- checkout http://ivaynberg.github.com/select2! + + +Versioning +---------- + +For transparency and insight into our release cycle, and for striving to maintain backward compatibility, Bootstrap will be maintained under the Semantic Versioning guidelines as much as possible. + +Releases will be numbered with the follow format: + +`..` + +And constructed with the following guidelines: + +* Breaking backward compatibility bumps the major +* New additions without breaking backward compatibility bumps the minor +* Bug fixes and misc changes bump the patch + +For more information on SemVer, please visit http://semver.org/. + + +Bug tracker +----------- + +Have a bug? Please create an issue here on GitHub! + +https://github.com/twitter/bootstrap/issues + + +Twitter account +--------------- + +Keep up to date on announcements and more by following Bootstrap on Twitter, @TwBootstrap. + + +Mailing list +------------ + +Have a question? Ask on our mailing list! + +twitter-bootstrap@googlegroups.com + +http://groups.google.com/group/twitter-bootstrap + + +IRC +--- + +Server: irc.freenode.net + +Channel: ##twitter-bootstrap (the double ## is not a typo) + + +Developers +---------- + +We have included a makefile with convenience methods for working with the Bootstrap library. + ++ **build** - `make` +Runs the LESS compiler to rebuild the `/less` files and compiles the docs pages. Requires lessc and uglify-js. Read more in our docs » + ++ **watch** - `make watch` +This is a convenience method for watching just Less files and automatically building them whenever you save. Requires the Watchr gem. + + +Authors +------- + +**Mark Otto** + ++ http://twitter.com/mdo ++ http://github.com/markdotto + +**Jacob Thornton** + ++ http://twitter.com/fat ++ http://github.com/fat + + +Copyright and license +--------------------- + +Copyright 2012 Twitter, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this work except in compliance with the License. +You may obtain a copy of the License in the LICENSE file, or at: + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/select2.css b/select2.css new file mode 100755 index 00000000..5b110947 --- /dev/null +++ b/select2.css @@ -0,0 +1,231 @@ +.select2-container { + position: relative; + display: inline-block; + /* inline-block for ie7 */ + zoom: 1; + *display: inline; + +} + +.select2-container .select2-choice { + background-color: #fff; + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.5, white)); + background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, white 50%); + background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, white 50%); + background-image: -o-linear-gradient(top, #eeeeee 0%, #ffffff 50%); + background-image: -ms-linear-gradient(top, #eeeeee 0%, #ffffff 50%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#eeeeee', endColorstr = '#ffffff', GradientType = 0); + background-image: linear-gradient(top, #eeeeee 0%, #ffffff 50%); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -moz-background-clip: padding; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #aaa; + display: block; + overflow: hidden; + white-space: nowrap; + position: relative; + height: 26px; + line-height: 26px; + padding: 0 0 0 8px; + color: #444; + text-decoration: none; +} + +.select2-container .select2-choice span { + margin-right: 26px; + display: block; + overflow: hidden; + white-space: nowrap; + -o-text-overflow: ellipsis; + -ms-text-overflow: ellipsis; + text-overflow: ellipsis; +} + +.select2-container .select2-drop { + background: #fff; + border: 1px solid #aaa; + border-top: 0; + position: absolute; + top: 29px; + left: 0; + -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15); + -moz-box-shadow: 0 4px 5px rgba(0, 0, 0, .15); + -o-box-shadow: 0 4px 5px rgba(0, 0, 0, .15); + box-shadow: 0 4px 5px rgba(0, 0, 0, .15); + z-index: 999; + + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; + -moz-background-clip: padding; + -webkit-background-clip: padding-box; + background-clip: padding-box; +} + +.select2-container .select2-choice div { + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; + -moz-background-clip: padding; + -webkit-background-clip: padding-box; + background-clip: padding-box; + background: #ccc; + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccc), color-stop(0.6, #eee)); + background-image: -webkit-linear-gradient(center bottom, #ccc 0%, #eee 60%); + background-image: -moz-linear-gradient(center bottom, #ccc 0%, #eee 60%); + background-image: -o-linear-gradient(bottom, #ccc 0%, #eee 60%); + background-image: -ms-linear-gradient(top, #cccccc 0%, #eeeeee 60%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#cccccc', endColorstr = '#eeeeee', GradientType = 0); + background-image: linear-gradient(top, #cccccc 0%, #eeeeee 60%); + border-left: 1px solid #aaa; + position: absolute; + right: 0; + top: 0; + display: block; + height: 100%; + width: 18px; +} + +.select2-container .select2-choice div b { + background: url('select2.png') no-repeat 0 1px; + display: block; + width: 100%; + height: 100%; +} + +.select2-container .select2-search { + padding: 3px 4px; + position: relative; + margin: 0; + white-space: nowrap; + z-index: 1010; +} + +.select2-container .select2-search input { + background: #fff url('select2.png') no-repeat 100% -22px; + background: url('select2.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee)); + background: url('select2.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%); + background: url('select2.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%); + background: url('select2.png') no-repeat 100% -22px, -o-linear-gradient(bottom, white 85%, #eeeeee 99%); + background: url('select2.png') no-repeat 100% -22px, -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%); + background: url('select2.png') no-repeat 100% -22px, linear-gradient(top, #ffffff 85%, #eeeeee 99%); + margin: 1px 0; + padding: 4px 20px 4px 5px; + outline: 0; + border: 1px solid #aaa; + font-family: sans-serif; + font-size: 1em; +} + +.select2-container .select2-search input.select2-active { + background: #fff url('spinner.gif') no-repeat 100%; + background: url('spinner.gif') no-repeat 100%, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee)); + background: url('spinner.gif') no-repeat 100%, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%); + background: url('spinner.gif') no-repeat 100%, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%); + background: url('spinner.gif') no-repeat 100%, -o-linear-gradient(bottom, white 85%, #eeeeee 99%); + background: url('spinner.gif') no-repeat 100%, -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%); + background: url('spinner.gif') no-repeat 100%, linear-gradient(top, #ffffff 85%, #eeeeee 99%); + margin: 1px 0; + padding: 4px 20px 4px 5px; + outline: 0; + border: 1px solid #aaa; + font-family: sans-serif; + font-size: 1em; +} + + +/* active styles */ +.select2-container-focused .select2-choice { + -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3); + -moz-box-shadow : 0 0 5px rgba(0,0,0,.3); + -o-box-shadow : 0 0 5px rgba(0,0,0,.3); + box-shadow : 0 0 5px rgba(0,0,0,.3); + border: 1px solid #5897fb; +} + +.select2-dropdown-open .select2-choice { + border: 1px solid #aaa; + -webkit-box-shadow: 0 1px 0 #fff inset; + -moz-box-shadow : 0 1px 0 #fff inset; + -o-box-shadow : 0 1px 0 #fff inset; + box-shadow : 0 1px 0 #fff inset; + background-color: #eee; + background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, white), color-stop(0.5, #eeeeee)); + background-image: -webkit-linear-gradient(center bottom, white 0%, #eeeeee 50%); + background-image: -moz-linear-gradient(center bottom, white 0%, #eeeeee 50%); + background-image: -o-linear-gradient(bottom, white 0%, #eeeeee 50%); + background-image: -ms-linear-gradient(top, #ffffff 0%,#eeeeee 50%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee',GradientType=0 ); + background-image: linear-gradient(top, #ffffff 0%,#eeeeee 50%); + -webkit-border-bottom-left-radius : 0; + -webkit-border-bottom-right-radius: 0; + -moz-border-radius-bottomleft : 0; + -moz-border-radius-bottomright: 0; + border-bottom-left-radius : 0; + border-bottom-right-radius: 0; +} + + +.select2-dropdown-open .select2-choice div { + background: transparent; + border-left: none; +} +.select2-dropdown-open .select2-choice div b { + background-position: -18px 1px; +} + +/* results */ +.select2-container .select2-results { + margin: 0 4px 4px 0; + padding: 0 0 0 4px; + position: relative; + overflow-x: hidden; + overflow-y: auto; + max-height: 200px; +} +.select2-container .select2-results li { + line-height: 80%; + padding: 7px 7px 8px; + margin: 0; + list-style: none; + cursor: pointer; + display: list-item; +} + +.select2-container .select2-results .select2-highlighted { + background: #3875d7; + color: #fff; +} +.select2-container .select2-results li em { + background: #feffde; + font-style: normal; +} +.select2-container .select2-results .select2-highlighted em { + background: transparent; +} +.select2-container .select2-results .select2-no-results { + background: #f4f4f4; + display: list-item; +} + +.select2-more-results.select2-active { + background: #f4f4f4 url('spinner.gif') no-repeat 100%; +} + +.select2-more-results { + background: #f4f4f4; + display: list-item; +} + + + + + +/* +.select2-container .select2-drop { border: 1px solid red !important;} +.select2-container .select2-drop .select2-search { border: 1px solid green !important;} +.select2-container .select2-drop .select2-search input { border: 1px solid blue !important;} +*/ \ No newline at end of file diff --git a/select2.js b/select2.js new file mode 100755 index 00000000..8e031d6c --- /dev/null +++ b/select2.js @@ -0,0 +1,781 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ +(function ($) { + "use strict"; + /*global document, window, jQuery, console */ + + var KEY, Util, DropDown, ResultList, Selection, Select2, Queries; + + function createClass(def) { + var type = function (attrs) { + var self = this; + if (def.attrs !== undefined) { + $.each(def.attrs, function (name, body) { + if (attrs[name] !== undefined) { + self[name] = attrs[name]; + } else { + if (body.required === true) { + throw "Value for required attribute: " + name + " not defined"; + } + if (body.init !== undefined) { + self[name] = typeof (body.init) === "function" ? body.init.apply(self) : body.init; + } + } + }); + } + + if (def.methods !== undefined && def.methods.init !== undefined) { + self.init(attrs); + } + }; + + if (def.methods !== undefined) { + if (def.methods.bind !== undefined) { + throw "Class cannot declare a method called 'bind'"; + } + + $.each(def.methods, function (name, body) { + type.prototype[name] = body; + }); + + type.prototype.bind = function (func) { + var self = this; + return function () { + func.apply(self, arguments); + }; + }; + } + + return type; + } + + KEY = { + TAB: 9, + ENTER: 13, + ESC: 27, + SPACE: 32, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + SHIFT: 16, + CTRL: 17, + ALT: 18, + PAGE_UP: 33, + PAGE_DOWN: 34, + HOME: 36, + END: 35, + BACKSPACE: 8, + DELETE: 46 + }; + + Util = {}; + + Util.debounce = function (threshold, fn) { + var timeout; + return function () { + window.clearTimeout(timeout); + timeout = window.setTimeout(fn, threshold); + }; + }; + + Util.debounceEvent = function (element, threshold, event, debouncedEvent, direct) { + debouncedEvent = debouncedEvent || event + "-debounced"; + direct = direct || true; + + var notify = Util.debounce(threshold, function (e) { + element.trigger(debouncedEvent, e); + }); + + element.on(event, function (e) { + if (direct && element.get().indexOf(e.target) < 0) { + return; + } + notify(e); + }); + }; + + (function () { + + var lastpos; + + /** + * Filters mouse events so an event is fired only if the mouse moved. + * Filters out mouse events that occur when mouse is stationary but + * the elements under the pointer are scrolled + */ + Util.filterMouseEvent = function (element, event, filteredEvent, direct) { + filteredEvent = filteredEvent || event + "-filtered"; + direct = direct || false; + + element.on(event, "*", function (e) { + if (direct && element.get().indexOf(e.target) < 0) { + return; + } + if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) { + $(e.target).trigger(filteredEvent, e); + lastpos = {x: e.pageX, y: e.pageY}; + } + }); + }; + }()); + + DropDown = createClass({ + attrs: { + container: {required: true}, + element: {required: true}, + bus: {required: true} + }, + methods: { + open: function () { + if (this.isOpen()) { + return; + } + + this.container.addClass("select2-dropdown-open"); + + // register click-outside-closes-dropdown listener + $(document).on("mousedown.dropdown", this.bind(function (e) { + var inside = false, + container = this.container.get(0); + $(e.target).parents().each(function () { + return !(inside = (this === container)); + }); + if (!inside) { + this.close(); + } + })); + + this.element.show(); + this.bus.trigger("opened"); + }, + + close: function () { + if (!this.isOpen()) { + return; + } + + this.container.removeClass("select2-dropdown-open"); + + $(document).off("mousedown.dropdown"); + this.element.hide(); + this.bus.trigger("closed"); + }, + + isOpen: function () { + return this.container.hasClass("select2-dropdown-open"); + }, + + toggle: function () { + if (this.isOpen()) { + this.close(); + } else { + this.open(); + } + } + } + + }); + + ResultList = createClass({ + attrs: { + element: {required: true}, + bus: {required: true}, + formatInputTooShort: {required: true}, + formatNoMatches: {required: true}, + formatResult: {required: true}, + minimumInputLength: {required: true}, + query: {required: true}, + selection: {required: true} + }, + methods: { + init: function () { + var self = this; + + this.search = this.element.find("input"); + this.results = this.element.find("ul"); + this.scrollPosition = 0; + this.vars = {}; + + this.search.on("keyup", function (e) { + if (e.which >= 48 || e.which === KEY.SPACE || e.which === KEY.BACKSPACE || e.which === KEY.DELETE) { + self.update(); + } + }); + + this.search.on("keydown", function (e) { + switch (e.which) { + case KEY.TAB: + e.preventDefault(); + self.select(); + return; + case KEY.ENTER: + e.preventDefault(); + e.stopPropagation(); + self.select(); + return; + case KEY.UP: + self.moveSelection(-1); + e.preventDefault(); + e.stopPropagation(); + return; + case KEY.DOWN: + self.moveSelection(1); + e.preventDefault(); + e.stopPropagation(); + return; + case KEY.ESC: + e.preventDefault(); + e.stopPropagation(); + self.cancel(); + return; + } + }); + +// this.results.on("mouseleave", "li.select2-result", this.bind(this.unhighlight)); + Util.filterMouseEvent(this.results, "mousemove"); + this.results.on("mousemove-filtered", this.bind(function (e) { + var el = $(e.target).closest("li.select2-result"); + if (el.length < 1) { + return; + } + this.setSelection(el.index()); + })); + this.results.on("click", this.bind(function (e) { + var el = $(e.target).closest("li.select2-result"); + if (el.length < 1) { + return; + } + this.bus.trigger("selected", [el.data("select2-result")]); + })); + + Util.debounceEvent(this.results, 100, "scroll"); + + this.results.on("scroll-debounced", this.bind(function (e) { + this.scrollPosition = this.results.scrollTop(); + + var more = this.results.find("li.select2-more-results"), below; + + if (more.length === 0) { + return; + } + + // pixels the element is below the scroll fold, below==0 is when the element is starting to be visible + below = more.offset().top - this.results.offset().top - this.results.height(); + + if (below <= 0) { + more.addClass("select2-active"); + this.query({term: this.search.val(), vars: this.vars, callback: this.bind(this.append)}); + } + + })); + }, + open: function (e) { + this.search.focus(); + this.results.scrollTop(this.scrollPosition); + if (this.results.children().length === 0) { + // first time the dropdown is opened, update the results + this.update(); + } + }, + close: function () { + //this.search.val(""); + //this.clear(); + }, + clear: function () { + this.results.empty(); + }, + showInputTooShort: function () { + this.show("
  • " + this.formatInputTooShort(this.search.val(), this.minimumInputLength) + "
  • "); + }, + showNoMatches: function () { + this.show("
  • " + this.formatNoMatches(this.search.val()) + "
  • "); + }, + show: function (html) { + this.results.html(html); + this.results.scrollTop(0); + this.search.removeClass("select2-active"); + }, + update: function () { + var html = ""; + + if (this.search.val().length < this.minimumInputLength) { + this.showInputTooShort(); + return; + } + + this.search.addClass("select2-active"); + this.vars = {}; + this.query({term: this.search.val(), vars: this.vars, callback: this.bind(this.process)}); + }, + process: function (data) { + if (data.results.length === 0) { + this.showNoMatches(); + return; + } + + var html = this.stringizeResults(data.results), selectedId = this.selection.val(), selectedIndex = 0; + + if (data.more === true) { + html += "
  • Loading more results...
  • "; + } + this.vars = data.vars || {}; + + this.show(html); + + this.findChoices().each(function (i) { + if (selectedId === data.results[i].id) { + selectedIndex = i; + } + $(this).data("select2-result", data.results[i]); + }); + + this.setSelection(selectedIndex); + + }, + append: function (data) { + + var more = this.results.find("li.select2-more-results"), html, offset; + + this.vars = data.vars || {}; + + if (data.results.length === 0) { + more.remove(); + return; + } + + html = this.stringizeResults(data.results); + + offset = this.results.find("li.select2-result").length; + + more.before(html); + + this.results.find("li.select2-result").each(function (i) { + if (i >= offset) { + $(this).data("select2-result", data.results[i - offset]); + } + }); + + if (data.more !== true) { + more.remove(); + } else { + more.removeClass("select2-active"); + } + + }, + stringizeResults: function (results, html) { + var i, l, classes; + html = html || ""; + for (i = 0, l = results.length; i < l; i += 1) { + html += "
  • "; + html += this.formatResult(results[i]); + html += "
  • "; + } + return html; + }, + + findChoices: function () { + return this.results.children("li.select2-result"); + }, + + removeSelection: function () { + this.findChoices().each(function () { + $(this).removeClass("select2-highlighted"); + }); + }, + + setSelection: function (index) { + this.removeSelection(); + + var children = this.findChoices(), + child = $(children[index]), + hb, + rb, + y, + more; + + child.addClass("select2-highlighted"); + + this.search.focus(); + + hb = child.offset().top + child.outerHeight(); + + // if this is the last child lets also make sure select2-more-results is visible + if (index === children.length - 1) { + more = this.results.find("li.select2-more-results"); + if (more.length > 0) { + hb = more.offset().top + more.outerHeight(); + } + } + + rb = this.results.offset().top + this.results.outerHeight(); + if (hb > rb) { + this.results.scrollTop(this.results.scrollTop() + (hb - rb)); + } + y = child.offset().top - this.results.offset().top; + + // make sure the top of the element is visible + if (y < 0) { + this.results.scrollTop(this.results.scrollTop() + y); // y is negative + } + + }, + + getSelectionIndex: function () { + var children = this.findChoices(), i = 0, l = children.length; + for (; i < l; i += 1) { + if ($(children[i]).hasClass("select2-highlighted")) { + return i; + } + } + return -1; + }, + + moveSelection: function (delta) { + var current = this.getSelectionIndex(), + children = this.findChoices(), + next = current + delta; + + if (current >= 0 && next >= 0 && next < children.length) { + this.setSelection(next); + } + }, + + select: function () { + var selected = this.results.find("li.select2-highlighted"); + if (selected.length > 0) { + this.bus.trigger("selected", [selected.data("select2-result")]); + } + }, + + cancel: function () { + this.bus.trigger("cancelled"); + }, + + val: function (data) { + var choices = this.findChoices(), index; + + choices.each(function (i) { + if ($(this).data("select2-result").id === data) { + index = i; + return false; + } + }); + + if (index === undefined && data.id !== undefined) { + choices.each(function (i) { + if ($(this).data("select2-result").id === data.id) { + index = i; + return false; + } + }); + } + + if (index !== undefined) { + this.setSelection(index); + this.select(); + return; + } + + this.bus.trigger("selected", data); + } + + } + }); + + Selection = createClass({ + attrs: { + bus: {required: true}, + element: {required: true}, + display: {init: function () { + return this.element.find("span"); + }}, + hidden: {required: true}, + formatSelection: {required: true}, + placeholder: {}, + dropdown: {required: true} + }, + methods: { + init: function () { + if (this.placeholder) { + this.select(this.placeholder); + } + this.element.click(this.dropdown.bind(this.dropdown.toggle)); + + var self = this; + this.element.on("keydown", function (e) { + switch (e.which) { + case KEY.TAB: + case KEY.SHIFT: + case KEY.CTRL: + case KEY.ALT: + case KEY.LEFT: + case KEY.RIGHT: + return; + } + self.dropdown.open(); + }); + }, + select: function (data) { + this.display.html(this.formatSelection(data)); + this.hidden.val(data.id); + }, + focus: function () { + this.element.focus(); + }, + val: function () { + return this.hidden.val(); + } + } + + }); + + Queries = {}; + Queries.select = function (select2, element) { + var options = []; + element.find("option").each(function () { + var e = $(this); + options.push({id: e.attr("value"), text: e.text()}); + }); + return function (query) { + var data = {results: [], more: false}, + text = query.term.toUpperCase(); + $.each(options, function (i) { + if (this.text.toUpperCase().indexOf(text) >= 0) { + data.results.push(this); + } + }); + query.callback(data); + }; + }; + Queries.ajax = function (select2, el) { + var timeout, // current scheduled but not yet executed request + requestSequence = 0, // sequence used to drop out-of-order responses + quietMillis = select2.ajax.quietMillis || 100; + + return function (query) { + window.clearTimeout(timeout); + timeout = window.setTimeout(function () { + requestSequence += 1; // increment the sequence + var requestNumber = requestSequence, // this request's sequence number + options = select2.ajax, // ajax parameters + data = options.data; // ajax data function + + data = data.call(this, query.term, query.vars); + + $.ajax({ + url: options.url, + dataType: options.dataType, + data: data + }).success( + function (data) { + if (requestNumber < requestSequence) { + return; + } + query.callback(options.results(data, query.vars)); + } + ); + }, quietMillis); + }; + }; + + Select2 = createClass({ + attrs: { + el: {required: true}, + formatResult: {init: function () { + return function (data) { + return data.text; + }; + }}, + formatSelection: {init: function () { + return function (data) { + return data.text; + }; + }}, + formatNoMatches: {init: function () { + return function () { + return "No matches found"; + }; + }}, + formatInputTooShort: {init: function () { + return function (input, min) { + return "Please enter " + (min - input.length) + " more characters to start search"; + }; + }}, + minimumInputLength: {init: 0}, + placeholder: {init: undefined}, + ajax: {init: undefined}, + query: {init: undefined} + }, + methods: { + init: function () { + var self = this, width, dropdown, results, selected, select; + + this.el = $(this.el); + + width = this.el.outerWidth(); + + this.container = $("
    ", { + "class": "select2-container", + style: "width: " + width + "px" + }); + this.container.html( + " " + + " " + + "
    " + + "
    " + + "" + + "" + ); + + this.el.data("select2", this); + this.el.hide(); + this.el.after(self.container); + if (this.el.attr("class") !== undefined) { + this.container.addClass(this.el.attr("class")); + } + this.container.data("select2", this); + this.container.find("input[type=hidden]").attr("name", this.el.attr("name")); + + if (this.query === undefined && this.el.get(0).tagName.toUpperCase() === "SELECT") { + this.query = "select"; + select = true; + } + if (Queries[this.query] !== undefined) { + this.query = Queries[this.query](this, this.el); + } + + (function () { + + var dropdown, searchContainer, search, width; + + function getSideBorderPadding(e) { + return e.outerWidth() - e.width(); + } + + // position and size dropdown + dropdown = self.container.find("div.select2-drop"); + width = self.container.outerWidth() - getSideBorderPadding(dropdown); + dropdown.css({top: self.container.height(), width: width}); + + // size search field + searchContainer = self.container.find(".select2-search"); + search = searchContainer.find("input"); + width = dropdown.width(); + width -= getSideBorderPadding(searchContainer); + width -= getSideBorderPadding(search); + search.css({width: width}); + }()); + + dropdown = new DropDown({ + element: this.container.find("div.select2-drop"), + container: this.container, + bus: this.el + }); + + this.selection = new Selection({ + bus: this.el, + element: this.container.find(".select2-choice"), + hidden: this.container.find("input[type=hidden]"), + formatSelection: this.formatSelection, + placeholder: this.placeholder, + dropdown: dropdown + }); + + this.results = new ResultList({ + element: this.container.find("div.select2-drop"), + bus: this.el, + formatInputTooShort: this.formatInputTooShort, + formatNoMatches: this.formatNoMatches, + formatResult: this.formatResult, + minimumInputLength: this.minimumInputLength, + query: this.query, + selection: this.selection + }); + + this.el.on("selected", function (e, result) { + dropdown.close(); + self.selection.select(result); + }); + + this.el.on("cancelled", function () { + dropdown.close(); + }); + + this.el.on("opened", this.bind(function () { + this.results.open(); + })); + + this.el.on("closed", this.bind(function () { + this.container.removeClass("select2-dropdown-open"); + this.results.close(); + this.selection.focus(); + })); + + // if attached to a select do some default initialization + if (select) { + this.results.update(); // build the results + selected = this.el.find("option[selected]"); + if (selected.length < 1 && this.placeholder === undefined) { + selected = $(this.el.find("option")[0]); + } + if (selected.length > 0) { + this.val({id: selected.attr("value"), text: selected.text()}); + } + } + + }, + val: function () { + var data; + if (arguments.length === 0) { + return this.selection.val(); + } else { + data = arguments[0]; + this.results.val(data); + } + } + } + }); + + $.fn.select2 = function () { + var args = Array.prototype.slice.call(arguments, 0), value, tmp; + this.each(function () { + if (args.length === 0) { + tmp = new Select2({el: this}); + } else if (typeof (args[0]) === "object") { + args[0].el = this; + tmp = new Select2(args[0]); + } else if (typeof (args[0]) === "string") { + var select2 = $(this).data("select2"); + value = select2[args[0]].apply(select2, args.slice(1)); + return false; + } else { + throw "Invalid arguments to select2 plugin: " + args; + } + }); + return (value === undefined) ? this : value; + }; + +}(jQuery)); \ No newline at end of file diff --git a/select2.png b/select2.png new file mode 100755 index 00000000..d08e4b7e Binary files /dev/null and b/select2.png differ diff --git a/spinner.gif b/spinner.gif new file mode 100755 index 00000000..5b33f7e5 Binary files /dev/null and b/spinner.gif differ