From 30319d9ba8d68d49ee5c2fdf0c1198679159a47c Mon Sep 17 00:00:00 2001 From: Vladislav Zarakovsky Date: Wed, 10 Feb 2016 15:10:04 +0300 Subject: [PATCH 01/10] Separate label/value for each suggestion in 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. 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 | 33 ++++++++++++++++++++++++++++----- index.html | 13 +++---------- test/api/selectSpec.js | 4 ++-- test/static/replaceSpec.js | 2 +- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/awesomplete.js b/awesomplete.js index 1492f640..b29a5daf 100644 --- a/awesomplete.js +++ b/awesomplete.js @@ -24,6 +24,7 @@ var _ = function (input, o) { autoFirst: false, filter: _.FILTER_CONTAINS, sort: _.SORT_BYLENGTH, + data: _.DATA, item: _.ITEM, replace: _.REPLACE }, o); @@ -184,17 +185,21 @@ _.prototype = { }, select: function (selected, originalTarget) { - 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], originalTarget: originalTarget || 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,26 @@ _.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" ? data : { label: data, value: data }; + + this.label = o.label; + this.value = o.value; +} +Suggestion.prototype = new String; +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..bf6ce4db 100644 --- a/index.html +++ b/index.html @@ -335,17 +335,10 @@

E-mail autocomplete

<input type="email" />
diff --git a/test/api/selectSpec.js b/test/api/selectSpec.js index bdb3b963..4790e05f 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 }), originalTarget: this.selectArgument || this.subject.ul.children[0] }) ); 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 61666cfa75d8a902cd8fe17061062017cf004cd1 Mon Sep 17 00:00:00 2001 From: Vladislav Zarakovsky Date: Fri, 12 Feb 2016 11:42:58 +0300 Subject: [PATCH 02/10] Avoid needless constructor call and fix length --- awesomplete.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awesomplete.js b/awesomplete.js index b29a5daf..776a45eb 100644 --- a/awesomplete.js +++ b/awesomplete.js @@ -282,10 +282,10 @@ function Suggestion(data) { ? { label: data[0], value: data[1] } : typeof data === "object" ? data : { label: data, value: data }; - this.label = o.label; + this.length = (this.label = o.label).length; this.value = o.value; } -Suggestion.prototype = new String; +Suggestion.prototype = Object.create(String.prototype); Suggestion.prototype.toString = Suggestion.prototype.valueOf = function () { return this.label; }; From 2471773b9d9db94c20d7e2debb47b709de774c6e Mon Sep 17 00:00:00 2001 From: Vladislav Zarakovsky Date: Fri, 12 Feb 2016 12:49:01 +0300 Subject: [PATCH 03/10] Suggestion based on __proto__ solution --- awesomplete.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/awesomplete.js b/awesomplete.js index 776a45eb..c4b8077d 100644 --- a/awesomplete.js +++ b/awesomplete.js @@ -282,13 +282,13 @@ function Suggestion(data) { ? { label: data[0], value: data[1] } : typeof data === "object" ? data : { label: data, value: data }; - this.length = (this.label = o.label).length; - this.value = o.value; + var inst = new String(o.label); + inst.__proto__ = Suggestion.prototype; + inst.label = o.label; + inst.value = o.value; + return inst; } Suggestion.prototype = Object.create(String.prototype); -Suggestion.prototype.toString = Suggestion.prototype.valueOf = function () { - return this.label; -}; function configure(instance, properties, o) { for (var i in properties) { From 7ea89de70b67e971b9a1d8e1c942ff23142b72b7 Mon Sep 17 00:00:00 2001 From: Vladislav Zarakovsky Date: Fri, 12 Feb 2016 14:43:32 +0300 Subject: [PATCH 04/10] Expect label/value object, handle others similar to String --- awesomplete.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awesomplete.js b/awesomplete.js index c4b8077d..6023cbf6 100644 --- a/awesomplete.js +++ b/awesomplete.js @@ -280,7 +280,7 @@ _.DATA = function (item/*, input*/) { return item; }; function Suggestion(data) { var o = Array.isArray(data) ? { label: data[0], value: data[1] } - : typeof data === "object" ? data : { label: data, value: data }; + : typeof data === "object" && "label" in data && "value" in data ? data : { label: data, value: data }; var inst = new String(o.label); inst.__proto__ = Suggestion.prototype; From e7027413666eba5bf49a832a307bb1315bde43c0 Mon Sep 17 00:00:00 2001 From: Vladislav Zarakovsky Date: Fri, 12 Feb 2016 14:55:20 +0300 Subject: [PATCH 05/10] Add example for different label/value with Object/Array suggestion items --- index.html | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/index.html b/index.html index bf6ce4db..81edb0da 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:

+ +
<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" ]
+	]
+});
+
From 0165ce7abfd55c9d8258a60294d02d97d8239722 Mon Sep 17 00:00:00 2001 From: Vladislav Zarakovsky Date: Sat, 13 Feb 2016 18:15:08 +0300 Subject: [PATCH 06/10] Add docs for data function --- index.html | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index 81edb0da..0dc39172 100644 --- a/index.html +++ b/index.html @@ -125,7 +125,7 @@

Basic usage

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

Suggestions with different label and value are supported too:

+

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");
@@ -256,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: +
    +
  • "JavaScript"
  • +
  • { label: "JavaScript", value: "JS" }
  • +
  • [ "JavaScript", "JS" ]
  • +
+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. +
From 0424aa2ed04bf9594f46ee768bcfbb7c9530a2f6 Mon Sep 17 00:00:00 2001 From: Vladislav Zarakovsky Date: Sat, 13 Feb 2016 20:24:00 +0300 Subject: [PATCH 07/10] Specs for new data function --- test/init/optionsSpec.js | 4 ++++ test/static/dataSpec.js | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 test/static/dataSpec.js 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" ]); + }); +}); From 26b47a83c55f7c65776d3eb457e8414a830059e1 Mon Sep 17 00:00:00 2001 From: Vladislav Zarakovsky Date: Mon, 15 Feb 2016 14:06:25 +0300 Subject: [PATCH 08/10] Back to original Suggestion, but with fixed length property --- awesomplete.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/awesomplete.js b/awesomplete.js index 6023cbf6..9ef57729 100644 --- a/awesomplete.js +++ b/awesomplete.js @@ -282,13 +282,15 @@ function Suggestion(data) { ? { label: data[0], value: data[1] } : typeof data === "object" && "label" in data && "value" in data ? data : { label: data, value: data }; - var inst = new String(o.label); - inst.__proto__ = Suggestion.prototype; - inst.label = o.label; - inst.value = o.value; - return inst; + this.label = o.label; + this.value = o.value; } -Suggestion.prototype = Object.create(String.prototype); +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) { From 3149b72e862997fba4f9c66784a541ec775a36af Mon Sep 17 00:00:00 2001 From: Vladislav Zarakovsky Date: Mon, 15 Feb 2016 14:25:10 +0300 Subject: [PATCH 09/10] Put data/filter/sort/item in call order --- awesomplete.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awesomplete.js b/awesomplete.js index 9ef57729..3f035ae3 100644 --- a/awesomplete.js +++ b/awesomplete.js @@ -22,9 +22,9 @@ var _ = function (input, o) { minChars: 2, maxItems: 10, autoFirst: false, + data: _.DATA, filter: _.FILTER_CONTAINS, sort: _.SORT_BYLENGTH, - data: _.DATA, item: _.ITEM, replace: _.REPLACE }, o); From 9376b0bd659d28babc6dc698c40b77ad48785827 Mon Sep 17 00:00:00 2001 From: Vladislav Zarakovsky Date: Mon, 15 Feb 2016 15:43:18 +0300 Subject: [PATCH 10/10] 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 3f035ae3..82dac7a4 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" } + ]); }); }); });