From 0571468fc4d95a9355b9e1dae987117ee8805908 Mon Sep 17 00:00:00 2001 From: Vladislav Zarakovsky Date: Sat, 12 Mar 2016 14:03:53 +0300 Subject: [PATCH 1/2] Separate label/value for each suggestion on the list. It is now possible to show a suggestion label in completer, but insert a suggestion value into the input instead. In addition to String as before, each list item now can also be: - a `{ label, value }` Object - a `[ label, value ]` Array To show full country name in completer, but insert country code into the input you can use these items: - `{ label: "United States", value: "US" }` - `[ "United States", "US" ]` Despite this data format change, old code will continue to work as before. This is taken care by `Suggestion()`. It uses `label` property automatically when string is expected anywhere in the API. One known issue is that accessing suggestion's characters by index won't work with old API. It's easy to fix though. Instead of `item[idx]` use `item.value[idx]` or `item.label[idx]` directly. In addition to default support for String/Object/Array items, we also add `data` method, which can be used to support any additional custom item formats and to generate data dynamically, as in changed Email example. The only thing you need to do in this case is to return item in any of String/Array/Object formats supported by default. --- awesomplete.js | 35 +++++++++++++++++++---- index.html | 57 +++++++++++++++++++++++++++++++------- test/api/selectSpec.js | 4 +-- test/init/optionsSpec.js | 4 +++ test/static/dataSpec.js | 19 +++++++++++++ test/static/replaceSpec.js | 2 +- 6 files changed, 103 insertions(+), 18 deletions(-) create mode 100644 test/static/dataSpec.js diff --git a/awesomplete.js b/awesomplete.js index 950a5ace..d223d584 100644 --- a/awesomplete.js +++ b/awesomplete.js @@ -22,6 +22,7 @@ var _ = function (input, o) { minChars: 2, maxItems: 10, autoFirst: false, + data: _.DATA, filter: _.FILTER_CONTAINS, sort: _.SORT_BYLENGTH, item: _.ITEM, @@ -184,17 +185,21 @@ _.prototype = { }, select: function (selected, origin) { - selected = selected || this.ul.children[this.index]; + if (selected) { + this.index = $.siblingIndex(selected); + } else { + selected = this.ul.children[this.index]; + } if (selected) { var allowed = $.fire(this.input, "awesomplete-select", { - text: selected.textContent, - data: this.suggestions[$.siblingIndex(selected)], + text: this.suggestions[this.index], + data: this.suggestions[this.index], origin: origin || selected }); if (allowed) { - this.replace(selected.textContent); + this.replace(this.suggestions[this.index]); this.close(); $.fire(this.input, "awesomplete-selectcomplete"); } @@ -211,6 +216,9 @@ _.prototype = { this.ul.innerHTML = ""; this.suggestions = this._list + .map(function(item) { + return new Suggestion(me.data(item, value)); + }) .filter(function(item) { return me.filter(item, value); }) @@ -262,11 +270,28 @@ _.ITEM = function (text, input) { }; _.REPLACE = function (text) { - this.input.value = text; + this.input.value = text.value; }; +_.DATA = function (item/*, input*/) { return item; }; + // Private functions +function Suggestion(data) { + var o = Array.isArray(data) + ? { label: data[0], value: data[1] } + : typeof data === "object" && "label" in data && "value" in data ? data : { label: data, value: data }; + + this.label = o.label; + this.value = o.value; +} +Object.defineProperty(Suggestion.prototype = Object.create(String.prototype), "length", { + get: function() { return this.label.length; } +}); +Suggestion.prototype.toString = Suggestion.prototype.valueOf = function () { + return "" + this.label; +}; + function configure(instance, properties, o) { for (var i in properties) { var initial = properties[i], diff --git a/index.html b/index.html index b9ff3412..0dc39172 100644 --- a/index.html +++ b/index.html @@ -125,6 +125,29 @@

Basic usage

awesomplete.list = ["Ada", "Java", "JavaScript", "Brainfuck", "LOLCODE", "Node.js", "Ruby on Rails"]; +

Suggestions with different label and value are supported too. The label will be shown in autocompleter and the value will be inserted into the input.

+ +
<input id="myinput" />
+
var input = document.getElementById("myinput");
+
+// Show label but insert value into the input:
+new Awesomplete(input, {
+	list: [
+		{ label: "Belarus", value: "BY" },
+		{ label: "China", value: "CN" },
+		{ label: "United States", value: "US" }
+	]
+});
+
+// Same with arrays:
+new Awesomplete(input, {
+	list: [
+		[ "Belarus", "BY" ],
+		[ "China", "CN" ],
+		[ "United States", "US" ]
+	]
+});
+
@@ -233,6 +256,27 @@

Extend

this.input.value = text; } + + data + Controls suggestions' label and value. This is useful if you have list items in custom format, or want to change list items based on user's input. + Function that takes two parameters, the first one being the original list item and the second a string with the user’s input and returns a list item in one of supported by default formats: + +To use objects without label or value properties, e.g. name and id instead, you can do this: +
data: function (item, input) {
+	return { label: item.name, value: item.id };
+}
+You can use any object for label and value and it will be converted to String where necessary: +
list: [ new Date("2015-01-01"), ... ] 
+Original list items as Date objects will be accessible in filter, sort, item and replace functions, but by default we'll just see Date objects converted to strings in autocompleter and the same value will be inserted to the input. +
+We can also generate list items based on user's input. See E-mail autocomplete example in Advanced Examples section. + + Awesomplete.DATA: Identity function which just returns the original list item. +
@@ -335,17 +379,10 @@

E-mail autocomplete

<input type="email" />
diff --git a/test/api/selectSpec.js b/test/api/selectSpec.js index 84895b16..9a1214e0 100644 --- a/test/api/selectSpec.js +++ b/test/api/selectSpec.js @@ -46,8 +46,8 @@ describe("awesomplete.select", function () { expect(handler).toHaveBeenCalledWith( jasmine.objectContaining({ - text: expectedTxt, - data: expectedTxt, + text: jasmine.objectContaining({ label: expectedTxt, value: expectedTxt }), + data: jasmine.objectContaining({ label: expectedTxt, value: expectedTxt }), origin: this.selectArgument || this.subject.ul.children[0] }) ); diff --git a/test/init/optionsSpec.js b/test/init/optionsSpec.js index 40153c1d..fc60997c 100644 --- a/test/init/optionsSpec.js +++ b/test/init/optionsSpec.js @@ -19,6 +19,10 @@ describe("Constructor options", function () { expect(this.subject.autoFirst).toBe(false); }); + it("modifies list item with DATA", function () { + expect(this.subject.data).toBe(Awesomplete.DATA); + }); + it("filters with FILTER_CONTAINS", function () { expect(this.subject.filter).toBe(Awesomplete.FILTER_CONTAINS); }); diff --git a/test/static/dataSpec.js b/test/static/dataSpec.js new file mode 100644 index 00000000..311c6eb0 --- /dev/null +++ b/test/static/dataSpec.js @@ -0,0 +1,19 @@ +describe("Awesomplete.DATA", function () { + + subject(function () { return Awesomplete.DATA(this.item) }); + + it("returns original String", function () { + this.item = "JavaScript"; + expect(this.subject).toEqual("JavaScript"); + }); + + it("returns original Object", function () { + this.item = { label: "JavaScript", value: "JS" }; + expect(this.subject).toEqual({ label: "JavaScript", value: "JS" }); + }); + + it("returns original Array", function () { + this.item = [ "JavaScript", "JS" ]; + expect(this.subject).toEqual([ "JavaScript", "JS" ]); + }); +}); diff --git a/test/static/replaceSpec.js b/test/static/replaceSpec.js index 4339ff21..e2eddbae 100644 --- a/test/static/replaceSpec.js +++ b/test/static/replaceSpec.js @@ -5,7 +5,7 @@ describe("Awesomplete.REPLACE", function () { def("instance", function() { return { input: { value: "initial" } } }); it("replaces input value", function () { - this.subject.call(this.instance, "JavaScript"); + this.subject.call(this.instance, { value: "JavaScript" }); expect(this.instance.input.value).toBe("JavaScript"); }); }); From 539fbf5c0b7adc73c22b6b10f9742a5e46170707 Mon Sep 17 00:00:00 2001 From: Vladislav Zarakovsky Date: Sat, 12 Mar 2016 14:04:58 +0300 Subject: [PATCH 2/2] Support list with separate label/value via or
    --- awesomplete.js | 15 ++++++++++++--- test/init/listSpec.js | 35 +++++++++++++++++++++++++++++------ 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/awesomplete.js b/awesomplete.js index d223d584..545086ed 100644 --- a/awesomplete.js +++ b/awesomplete.js @@ -118,9 +118,18 @@ _.prototype = { list = $(list); if (list && list.children) { - this._list = slice.apply(list.children).map(function (el) { - return el.textContent.trim(); + var items = []; + slice.apply(list.children).forEach(function (el) { + if (!el.disabled) { + var text = el.textContent.trim(); + var value = el.value || text; + var label = el.label || text; + if (value !== "") { + items.push({ label: label, value: value }); + } + } }); + this._list = items; } } @@ -282,7 +291,7 @@ function Suggestion(data) { ? { label: data[0], value: data[1] } : typeof data === "object" && "label" in data && "value" in data ? data : { label: data, value: data }; - this.label = o.label; + this.label = o.label || o.value; this.value = o.value; } Object.defineProperty(Suggestion.prototype = Object.create(String.prototype), "length", { diff --git a/test/init/listSpec.js b/test/init/listSpec.js index 6f95ff05..eff78d61 100644 --- a/test/init/listSpec.js +++ b/test/init/listSpec.js @@ -23,12 +23,20 @@ describe("Awesomplete list", function () { it("assigns from element specified by selector", function () { this.subject.list = "#data-list"; - expect(this.subject._list).toEqual([ "With", "Data", "List" ]); + expect(this.subject._list).toEqual([ + { label: "With", value: "With" }, + { label: "Data", value: "Data" }, + { label: "List", value: "List" } + ]); }); it("assigns from element", function () { this.subject.list = $("#data-list"); - expect(this.subject._list).toEqual([ "With", "Data", "List" ]); + expect(this.subject._list).toEqual([ + { label: "With", value: "With" }, + { label: "Data", value: "Data" }, + { label: "List", value: "List" } + ]); }); it("does not assigns from not found list", function () { @@ -68,12 +76,20 @@ describe("Awesomplete list", function () { it("assigns from element specified by selector", function () { this.options = { list: "#data-list" }; - expect(this.subject._list).toEqual([ "With", "Data", "List" ]); + expect(this.subject._list).toEqual([ + { label: "With", value: "With" }, + { label: "Data", value: "Data" }, + { label: "List", value: "List" } + ]); }); it("assigns from list specified by element", function () { this.options = { list: $("#data-list") }; - expect(this.subject._list).toEqual([ "With", "Data", "List" ]); + expect(this.subject._list).toEqual([ + { label: "With", value: "With" }, + { label: "Data", value: "Data" }, + { label: "List", value: "List" } + ]); }); }); @@ -85,14 +101,21 @@ describe("Awesomplete list", function () { it("assigns from element referenced by selector", function () { this.element = "#with-data-list"; - expect(this.subject._list).toEqual(["With", "Data", "List"]); + expect(this.subject._list).toEqual([ + { label: "With", value: "With" }, + { label: "Data", value: "Data" }, + { label: "List", value: "List" } + ]); }); }); describe("list html attribute", function () { it("assigns from element referenced by id", function () { this.element = "#with-list"; - expect(this.subject._list).toEqual(["With", "List"]); + expect(this.subject._list).toEqual([ + { label: "With", value: "With" }, + { label: "List", value: "List" } + ]); }); }); });