From b5b95f417e22e16c4d5895692f3cb57e3c882fff Mon Sep 17 00:00:00 2001 From: Tomasz Nowak Date: Sat, 27 Jul 2019 00:35:25 +0200 Subject: [PATCH 1/5] Implement keyboard navigation between tags Keys supported: Home, Left, Right, End. --- src/components/InputTag.vue | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/components/InputTag.vue b/src/components/InputTag.vue index 551011f..97606fa 100644 --- a/src/components/InputTag.vue +++ b/src/components/InputTag.vue @@ -171,6 +171,38 @@ export default { this.tagChange(); }, + moveInputHome() { + const inputtag = this.$refs.inputtag; + if (inputtag.previousElementSibling) { + this.$el.prepend(inputtag); + this.focusNewTag(); + } + }, + + moveInputLeft() { + const inputtag = this.$refs.inputtag; + if (inputtag.previousElementSibling) { + this.$el.insertBefore(inputtag, inputtag.previousElementSibling); + this.focusNewTag(); + } + }, + + moveInputRight() { + const inputtag = this.$refs.inputtag; + if (inputtag.nextElementSibling) { + this.$el.insertBefore(inputtag.nextElementSibling, inputtag); + this.focusNewTag(); + } + }, + + moveInputEnd() { + const inputtag = this.$refs.inputtag; + if (inputtag.nextElementSibling) { + this.$el.appendChild(inputtag); + this.focusNewTag(); + } + }, + tagChange() { this.$emit("update:tags", this.innerTags); this.$emit("input", this.innerTags); @@ -201,6 +233,10 @@ export default { type = "text" v-model = "newTag" v-on:keydown.delete.stop = "removeLastTag" + v-on:keydown.home = "moveInputHome" + v-on:keydown.left = "moveInputLeft" + v-on:keydown.right = "moveInputRight" + v-on:keydown.end = "moveInputEnd" v-on:keydown = "addNew" v-on:blur = "handleInputBlur" v-on:focus = "handleInputFocus" From 1d40ffcf5c4bf998704f064c7c990ed18b3fcd34 Mon Sep 17 00:00:00 2001 From: Tomasz Nowak Date: Sat, 27 Jul 2019 00:52:15 +0200 Subject: [PATCH 2/5] Insert the tag in the proper place --- src/components/InputTag.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/InputTag.vue b/src/components/InputTag.vue index 97606fa..67ae19d 100644 --- a/src/components/InputTag.vue +++ b/src/components/InputTag.vue @@ -124,7 +124,11 @@ export default { isValid && (this.allowDuplicates || this.innerTags.indexOf(tag) === -1) ) { - this.innerTags.push(tag); + const position = [].indexOf.call( + this.$el.childNodes, + this.$refs.inputtag + ); + this.innerTags.splice(position, 0, tag); this.newTag = ""; this.tagChange(); From 8df280ee9a239c0b4358b09ccfa1872b8240f682 Mon Sep 17 00:00:00 2001 From: Tomasz Nowak Date: Wed, 31 Jul 2019 18:20:09 +0200 Subject: [PATCH 3/5] Move the input field left/right only when the cursor is on the edge --- src/components/InputTag.vue | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/InputTag.vue b/src/components/InputTag.vue index 67ae19d..a8d1e09 100644 --- a/src/components/InputTag.vue +++ b/src/components/InputTag.vue @@ -185,7 +185,7 @@ export default { moveInputLeft() { const inputtag = this.$refs.inputtag; - if (inputtag.previousElementSibling) { + if (inputtag.previousElementSibling && inputtag.selectionStart == 0) { this.$el.insertBefore(inputtag, inputtag.previousElementSibling); this.focusNewTag(); } @@ -193,7 +193,10 @@ export default { moveInputRight() { const inputtag = this.$refs.inputtag; - if (inputtag.nextElementSibling) { + if ( + inputtag.nextElementSibling && + inputtag.selectionEnd == inputtag.textLength + ) { this.$el.insertBefore(inputtag.nextElementSibling, inputtag); this.focusNewTag(); } From c4488ea563cd3a47f13e083725dd536c8c81d9bf Mon Sep 17 00:00:00 2001 From: Tomasz Nowak Date: Wed, 31 Jul 2019 23:12:25 +0200 Subject: [PATCH 4/5] Keep tags internally as objects Unique :key allows now keeping a proper order of tags. --- src/components/InputTag.vue | 29 +++++++++++++++++++++-------- tests/unit/InputTag.spec.js | 26 +++++++++++++------------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/components/InputTag.vue b/src/components/InputTag.vue index a8d1e09..b1d8a59 100644 --- a/src/components/InputTag.vue +++ b/src/components/InputTag.vue @@ -65,7 +65,9 @@ export default { data() { return { newTag: "", - innerTags: [...this.value], + innerTags: [...this.value].map(v => { + return { id: this.getUniqueId(), text: v }; + }), isInputActive: false }; }, @@ -78,11 +80,21 @@ export default { watch: { value() { - this.innerTags = [...this.value]; + this.innerTags = [...this.value].map(v => { + return { id: this.getUniqueId(), text: v }; + }); } }, methods: { + getUniqueId() { + return ( + Math.random() + .toString(36) + .substring(2) + Date.now().toString(36) + ); + }, + focusNewTag() { if (this.readOnly || !this.$el.querySelector(".new-tag")) { return; @@ -122,13 +134,14 @@ export default { if ( tag && isValid && - (this.allowDuplicates || this.innerTags.indexOf(tag) === -1) + (this.allowDuplicates || + this.innerTags.map(el => el.text).indexOf(tag) === -1) ) { const position = [].indexOf.call( this.$el.childNodes, this.$refs.inputtag ); - this.innerTags.splice(position, 0, tag); + this.innerTags.splice(position, 0, { id: this.getUniqueId(), text: tag }); this.newTag = ""; this.tagChange(); @@ -211,8 +224,8 @@ export default { }, tagChange() { - this.$emit("update:tags", this.innerTags); - this.$emit("input", this.innerTags); + this.$emit("update:tags", this.innerTags.map(el => el.text)); + this.$emit("input", this.innerTags.map(el => el.text)); } } }; @@ -227,8 +240,8 @@ export default { }" class="vue-input-tag-wrapper" > - - {{ tag }} + + {{ tag.text }} diff --git a/tests/unit/InputTag.spec.js b/tests/unit/InputTag.spec.js index 1cefc3b..8eaba16 100644 --- a/tests/unit/InputTag.spec.js +++ b/tests/unit/InputTag.spec.js @@ -32,11 +32,11 @@ describe("InputTag.vue", () => { }); it("should have a 'tag 1'", () => { - expect(wrapper.vm.innerTags[0]).toEqual("tag 1"); + expect(wrapper.vm.innerTags[0].text).toEqual("tag 1"); }); it("should have a 'tag 2'", () => { - expect(wrapper.vm.innerTags[1]).toEqual("tag 2"); + expect(wrapper.vm.innerTags[1].text).toEqual("tag 2"); }); it("should reset the new tag", () => { @@ -70,11 +70,11 @@ describe("InputTag.vue", () => { }); it("should have a 'tag 1'", () => { - expect(wrapper.vm.innerTags[0]).toEqual("tag 1"); + expect(wrapper.vm.innerTags[0].text).toEqual("tag 1"); }); it("should have a 'tag 3'", () => { - expect(wrapper.vm.innerTags[1]).toEqual("tag 3"); + expect(wrapper.vm.innerTags[1].text).toEqual("tag 3"); }); }); @@ -93,11 +93,11 @@ describe("InputTag.vue", () => { }); it("should have a 'tag 1'", () => { - expect(wrapper.vm.innerTags[0]).toEqual("tag 1"); + expect(wrapper.vm.innerTags[0].text).toEqual("tag 1"); }); it("should have a 'tag 2'", () => { - expect(wrapper.vm.innerTags[1]).toEqual("tag 2"); + expect(wrapper.vm.innerTags[1].text).toEqual("tag 2"); }); }); @@ -117,7 +117,7 @@ describe("InputTag.vue", () => { }); it("should have a 'tag 1'", () => { - expect(wrapper.vm.innerTags[2]).toEqual("tag 1"); + expect(wrapper.vm.innerTags[2].text).toEqual("tag 1"); }); }); @@ -186,7 +186,7 @@ describe("InputTag.vue", () => { }); it("should have a tag 'foo'", () => { - expect(wrapper.vm.innerTags[0]).toEqual("foo"); + expect(wrapper.vm.innerTags[0].text).toEqual("foo"); }); }); @@ -208,7 +208,7 @@ describe("InputTag.vue", () => { }); it("should have a tag '123'", () => { - expect(wrapper.vm.innerTags[0]).toEqual("123"); + expect(wrapper.vm.innerTags[0].text).toEqual("123"); }); }); @@ -230,7 +230,7 @@ describe("InputTag.vue", () => { }); it("should have a tag 'mati@tucci.me'", () => { - expect(wrapper.vm.innerTags[0]).toEqual("mati@tucci.me"); + expect(wrapper.vm.innerTags[0].text).toEqual("mati@tucci.me"); }); }); @@ -252,7 +252,7 @@ describe("InputTag.vue", () => { }); it("should have a tag 'https://tucci.me'", () => { - expect(wrapper.vm.innerTags[0]).toEqual("https://tucci.me"); + expect(wrapper.vm.innerTags[0].text).toEqual("https://tucci.me"); }); }); @@ -274,7 +274,7 @@ describe("InputTag.vue", () => { }); it("should have a tag '2002-04-03'", () => { - expect(wrapper.vm.innerTags[0]).toEqual("2002-04-03"); + expect(wrapper.vm.innerTags[0].text).toEqual("2002-04-03"); }); }); @@ -288,7 +288,7 @@ describe("InputTag.vue", () => { }); it("should have an uppercase tag", () => { - expect(wrapper.vm.innerTags[0]).toEqual("NEW TAG"); + expect(wrapper.vm.innerTags[0].text).toEqual("NEW TAG"); }); }); }); From cb7c06f4c7ef4f8970c15fc62b7499f117f3d39b Mon Sep 17 00:00:00 2001 From: Tomasz Nowak Date: Fri, 9 Aug 2019 01:21:41 +0200 Subject: [PATCH 5/5] WIP: unit tests for moving the input field --- tests/unit/InputTag.spec.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/unit/InputTag.spec.js b/tests/unit/InputTag.spec.js index 8eaba16..20aa3fb 100644 --- a/tests/unit/InputTag.spec.js +++ b/tests/unit/InputTag.spec.js @@ -335,4 +335,23 @@ describe("InputTag.vue", () => { ); }); }); + + describe("move input box", () => { + beforeEach(async () => { + await addTag(wrapper, "tag B"); + await addTag(wrapper, "tag D"); + await addTag(wrapper, "tag F"); + }); + + // FIXME events in "it" don't work + (false ? it : it.skip)("should add a tag before the last one", () => { + const input = wrapper.find("input.new-tag"); + input.trigger("focus"); + input.trigger("keydown.left"); + input.trigger("keydown", {key: 'E'}); + input.trigger("keydown.enter"); + expect(wrapper.findAll(".input-tag").length).toEqual(4); + expect(wrapper.vm.innerTags.length).toEqual(4); + }); + }); });