From 542e73e4ea21e963026b1571f4f0e6a3b268ec91 Mon Sep 17 00:00:00 2001 From: Massi-X Date: Thu, 14 Nov 2024 00:35:52 +0100 Subject: [PATCH 01/14] Fix delete key behavior in select mode When using delete key the behavior was inconsistent and also led to focus lost. Fix this by never calling removeTags() while we are editing but only after blur --- src/parts/events.js | 5 +++++ src/parts/suggestions.js | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/src/parts/events.js b/src/parts/events.js index 6b6d5951..e660d3e7 100644 --- a/src/parts/events.js +++ b/src/parts/events.js @@ -164,6 +164,9 @@ export default { // if clicked anywhere else inside a tag, which had triggered an `focusin` event, // the onFocusBlur should be aborted. This part was spcifically written for `select` mode. // tagTextNode && this.events.callbacks.onEditTagFocus.call(this, nodeTag) + + // this fixes select mode input focus lost, to trigger: Tab into it, delete (backspace) without moving caret + _s.mode == 'select' && this.events.callbacks.onEditTagFocus.call(this, nodeTag) } var text = e.target ? this.trim(this.DOM.input.textContent) : '', // a string @@ -246,6 +249,8 @@ export default { // if nothing has changed (same display value), do not add a tag if( currentDisplayValue === text ) text = '' + else //the input is empty, we should now remove the tag + this.removeTags() } shouldAddTags = text && !this.state.actions.selectOption && _s.addTagOnBlur && _s.addTagOn.includes('blur'); diff --git a/src/parts/suggestions.js b/src/parts/suggestions.js index a6354f3f..2dc17151 100644 --- a/src/parts/suggestions.js +++ b/src/parts/suggestions.js @@ -290,6 +290,14 @@ export default { isMixMode = _s.mode == 'mix', tagData = this.suggestedListItems.find(item => (item.value ?? item) == value) + // select mode: after the tag has been removed and focus is lost, trying to click a suggestion (after focusing again) will fail because it tries to replace an inexistent tag + // catch and use addTags() instead. This happens only once + if(_s.mode == 'select' && !this.state.composing) { + this.addTags(value, true) + closeOnSelect && this.dropdown.hide() + return; + } + // The below event must be triggered, regardless of anything else which might go wrong this.trigger('dropdown:select', {data:tagData, elm, event}) From bea5b664ea4c7a9e12740806497fb1a7c4de963b Mon Sep 17 00:00:00 2001 From: Massi-X Date: Thu, 14 Nov 2024 00:53:17 +0100 Subject: [PATCH 02/14] This should only be applied when userInput == true --- src/parts/suggestions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parts/suggestions.js b/src/parts/suggestions.js index 2dc17151..7d31b6b2 100644 --- a/src/parts/suggestions.js +++ b/src/parts/suggestions.js @@ -292,7 +292,7 @@ export default { // select mode: after the tag has been removed and focus is lost, trying to click a suggestion (after focusing again) will fail because it tries to replace an inexistent tag // catch and use addTags() instead. This happens only once - if(_s.mode == 'select' && !this.state.composing) { + if(_s.mode == 'select' && !this.state.composing && this.userInput) { this.addTags(value, true) closeOnSelect && this.dropdown.hide() return; From 2c8ccabf7a1f14f917212b7d917b9c17358de38c Mon Sep 17 00:00:00 2001 From: Massi-X Date: Fri, 15 Nov 2024 00:01:32 +0100 Subject: [PATCH 03/14] Fix handle of focus/blur and dropdown Previously the focus class remained even after leaving the input, moreover the dropdown stayed open if navigating with tab. At last the input focus was inconsistent on suggestion selection with a tag present or not, now every time a suggestion is clicked, the input regain focus. --- src/parts/suggestions.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/parts/suggestions.js b/src/parts/suggestions.js index 7d31b6b2..b1177df8 100644 --- a/src/parts/suggestions.js +++ b/src/parts/suggestions.js @@ -96,7 +96,11 @@ export default { this.state.autoCompleteData = selectedElmData; this.input.autocomplete.set.call(this, value) return false + } else if(isSelectMode && _s.userInput && !_s.autoComplete.tabKey) { // do not forget to hide the dropdown in select mode! + this.dropdown.hide() + return false } + return true } case 'Enter' : { @@ -295,6 +299,7 @@ export default { if(_s.mode == 'select' && !this.state.composing && this.userInput) { this.addTags(value, true) closeOnSelect && this.dropdown.hide() + setTimeout(()=> this.DOM.scope.querySelector('.' + _s.classNames.tagText).focus(), 0) //set the focus back to input on each select to ensure consistent behavior return; } From b35a43ef7cd0b0c2d70af0aaa7bca0ffc00ed5a0 Mon Sep 17 00:00:00 2001 From: Massi-X Date: Tue, 19 Nov 2024 23:53:35 +0100 Subject: [PATCH 04/14] Fill dropdown after selection Currently after the user inputs some text and then select a suggestion, the dropdown is stuck to that suggestion list. To see all the suggestions again the dropdown must be toggle. Fixed by showing all suggestions after selection only for select mode (should this be considered for non-select too?) --- src/parts/suggestions.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/parts/suggestions.js b/src/parts/suggestions.js index b1177df8..9b60797e 100644 --- a/src/parts/suggestions.js +++ b/src/parts/suggestions.js @@ -276,7 +276,8 @@ export default { */ selectOption( elm, event ){ var _s = this.settings, - {clearOnSelect, closeOnSelect} = _s.dropdown; + {clearOnSelect, closeOnSelect} = _s.dropdown, + includeSelectedTags = _s.dropdown.includeSelectedTags || _s.mode == 'select'; if( !elm ) { this.addTags(this.state.inputText, true) @@ -294,6 +295,9 @@ export default { isMixMode = _s.mode == 'mix', tagData = this.suggestedListItems.find(item => (item.value ?? item) == value) + // select mode: reset the dropdown to show all options again to the user + _s.mode == 'select' && setTimeout(() => this.dropdown.show(), 0) + // select mode: after the tag has been removed and focus is lost, trying to click a suggestion (after focusing again) will fail because it tries to replace an inexistent tag // catch and use addTags() instead. This happens only once if(_s.mode == 'select' && !this.state.composing && this.userInput) { @@ -334,7 +338,7 @@ export default { closeOnSelect && setTimeout(this.dropdown.hide.bind(this)) // execute these tasks once a suggestion has been selected - elm.addEventListener('transitionend', () => { + !includeSelectedTags && elm.addEventListener('transitionend', () => { this.dropdown.fillHeaderFooter() setTimeout(() => { elm.remove() @@ -343,7 +347,7 @@ export default { }, {once: true}) // hide selected suggestion - elm.classList.add(this.settings.classNames.dropdownItemHidden) + !includeSelectedTags && elm.classList.add(this.settings.classNames.dropdownItemHidden) }, // adds all the suggested items, including the ones which are not currently rendered, From 89655c191e7a1a1403c21536c8a400b85540117c Mon Sep 17 00:00:00 2001 From: Massi-X Date: Thu, 21 Nov 2024 00:22:46 +0100 Subject: [PATCH 05/14] Avoid conflict between show and hide --- src/parts/suggestions.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/parts/suggestions.js b/src/parts/suggestions.js index 9b60797e..dce6f74b 100644 --- a/src/parts/suggestions.js +++ b/src/parts/suggestions.js @@ -302,8 +302,10 @@ export default { // catch and use addTags() instead. This happens only once if(_s.mode == 'select' && !this.state.composing && this.userInput) { this.addTags(value, true) - closeOnSelect && this.dropdown.hide() - setTimeout(()=> this.DOM.scope.querySelector('.' + _s.classNames.tagText).focus(), 0) //set the focus back to input on each select to ensure consistent behavior + setTimeout(() => { + closeOnSelect && this.dropdown.hide() + this.DOM.scope.querySelector('.' + _s.classNames.tagText).focus() //set the focus back to input on each select to ensure consistent behavior + }, 0) return; } From 16e678838a67c509bac4a5e226716369293759ba Mon Sep 17 00:00:00 2001 From: Massi-X Date: Thu, 21 Nov 2024 00:54:04 +0100 Subject: [PATCH 06/14] Do not advance in select mode Do not higlight the next option while in select mode but keep the current one --- src/parts/suggestions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parts/suggestions.js b/src/parts/suggestions.js index dce6f74b..1b516d3e 100644 --- a/src/parts/suggestions.js +++ b/src/parts/suggestions.js @@ -116,7 +116,7 @@ export default { if( selectedElm ){ this.dropdown.selectOption(selectedElm) // highlight next option - selectedElm = this.dropdown.getNextOrPrevOption(selectedElm, !actionUp) + if(!isSelectMode) selectedElm = this.dropdown.getNextOrPrevOption(selectedElm, !actionUp) this.dropdown.highlightOption(selectedElm) return } From 29a8fe9f137e2afae23e485e13b5a0c6e57319c1 Mon Sep 17 00:00:00 2001 From: Massi-X Date: Sun, 24 Nov 2024 14:00:55 +0100 Subject: [PATCH 07/14] Revert "Fix delete key behavior in select mode" This reverts commit 542e73e4ea21e963026b1571f4f0e6a3b268ec91. --- src/parts/events.js | 5 ----- src/parts/suggestions.js | 7 +------ 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/parts/events.js b/src/parts/events.js index 8a158f5c..6e1432b1 100644 --- a/src/parts/events.js +++ b/src/parts/events.js @@ -168,9 +168,6 @@ export default { // if clicked anywhere else inside a tag, which had triggered an `focusin` event, // the onFocusBlur should be aborted. This part was spcifically written for `select` mode. // tagTextNode && this.events.callbacks.onEditTagFocus.call(this, nodeTag) - - // this fixes select mode input focus lost, to trigger: Tab into it, delete (backspace) without moving caret - _s.mode == 'select' && this.events.callbacks.onEditTagFocus.call(this, nodeTag) } var text = e.target ? this.trim(this.DOM.input.textContent) : '', // a string @@ -253,8 +250,6 @@ export default { // if nothing has changed (same display value), do not add a tag if( currentDisplayValue === text ) text = '' - else //the input is empty, we should now remove the tag - this.removeTags() } shouldAddTags = text && !this.state.actions.selectOption && _s.addTagOnBlur && _s.addTagOn.includes('blur'); diff --git a/src/parts/suggestions.js b/src/parts/suggestions.js index 1b516d3e..5be61569 100644 --- a/src/parts/suggestions.js +++ b/src/parts/suggestions.js @@ -296,14 +296,9 @@ export default { tagData = this.suggestedListItems.find(item => (item.value ?? item) == value) // select mode: reset the dropdown to show all options again to the user - _s.mode == 'select' && setTimeout(() => this.dropdown.show(), 0) - - // select mode: after the tag has been removed and focus is lost, trying to click a suggestion (after focusing again) will fail because it tries to replace an inexistent tag - // catch and use addTags() instead. This happens only once if(_s.mode == 'select' && !this.state.composing && this.userInput) { - this.addTags(value, true) setTimeout(() => { - closeOnSelect && this.dropdown.hide() + setTimeout(() => closeOnSelect ? this.dropdown.hide() : this.dropdown.show(), 0) this.DOM.scope.querySelector('.' + _s.classNames.tagText).focus() //set the focus back to input on each select to ensure consistent behavior }, 0) return; From 2b66604e976e9843818856b3971505b7c8bbd674 Mon Sep 17 00:00:00 2001 From: Massi-X Date: Sun, 24 Nov 2024 14:03:57 +0100 Subject: [PATCH 08/14] Fix delete key behavior in select mode When using delete key the focus was lost when using backspace --- src/parts/events.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parts/events.js b/src/parts/events.js index 6e1432b1..55731f01 100644 --- a/src/parts/events.js +++ b/src/parts/events.js @@ -292,7 +292,7 @@ export default { case 'Backspace': { if( !_s.readonly && !this.state.editing ) { this.removeTags(focusedElm); - (nextTag ? nextTag : this.DOM.input).focus() + setTimeout(() => (nextTag ? nextTag : this.DOM.input).focus(), 0) } break; From e6381e3785eeffd40a3516903cc577a8efbb7fe0 Mon Sep 17 00:00:00 2001 From: Massi-X Date: Mon, 25 Nov 2024 23:55:34 +0100 Subject: [PATCH 09/14] Fix broken commit --- src/parts/suggestions.js | 13 ++++--------- src/tagify.js | 1 - 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/parts/suggestions.js b/src/parts/suggestions.js index 5be61569..75ce0ee7 100644 --- a/src/parts/suggestions.js +++ b/src/parts/suggestions.js @@ -295,15 +295,6 @@ export default { isMixMode = _s.mode == 'mix', tagData = this.suggestedListItems.find(item => (item.value ?? item) == value) - // select mode: reset the dropdown to show all options again to the user - if(_s.mode == 'select' && !this.state.composing && this.userInput) { - setTimeout(() => { - setTimeout(() => closeOnSelect ? this.dropdown.hide() : this.dropdown.show(), 0) - this.DOM.scope.querySelector('.' + _s.classNames.tagText).focus() //set the focus back to input on each select to ensure consistent behavior - }, 0) - return; - } - // The below event must be triggered, regardless of anything else which might go wrong this.trigger('dropdown:select', {data:tagData, elm, event}) @@ -332,6 +323,10 @@ export default { this.toggleFocusClass(true) }) + // select mode: reset the dropdown to show all options again to the user + if(_s.mode == 'select' && !this.state.composing && this.userInput) + setTimeout(() => this.DOM.scope.querySelector('.' + _s.classNames.tagText).focus(), 0) //set the focus back to input on each select to ensure consistent behavior + closeOnSelect && setTimeout(this.dropdown.hide.bind(this)) // execute these tasks once a suggestion has been selected diff --git a/src/tagify.js b/src/tagify.js index cce4ea05..f3d3d2a7 100644 --- a/src/tagify.js +++ b/src/tagify.js @@ -648,7 +648,6 @@ Tagify.prototype = { this.removeTags(tagElm) this.trigger("edit:updated", eventData) - this.dropdown.hide() // check if any of the current tags which might have been marked as "duplicate" should be now un-marked if( this.settings.keepInvalidTags ) From 5ae3c627f67c7085a0b54086cdaf2bfd69e54e5b Mon Sep 17 00:00:00 2001 From: Massi-X Date: Tue, 26 Nov 2024 00:02:17 +0100 Subject: [PATCH 10/14] Make sure the dropdown closes if no instance is found --- src/parts/events.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parts/events.js b/src/parts/events.js index 55731f01..fcbde9bc 100644 --- a/src/parts/events.js +++ b/src/parts/events.js @@ -723,7 +723,7 @@ export default { this.state.hasFocus = false // do not hide the dropdown if a click was initiated within it and that dropdown belongs to this Tagify instance - if( e.target.closest('.tagify__dropdown') && e.target.closest('.tagify__dropdown').__tagify != this ) + if( e.target.closest('.tagify__dropdown') == null || e.target.closest('.tagify__dropdown').__tagify != this ) this.dropdown.hide() } }, From 8d619fa4aaf08126cf3b67bcaeb37cbd82b369c2 Mon Sep 17 00:00:00 2001 From: Massi-X Date: Tue, 26 Nov 2024 23:35:14 +0100 Subject: [PATCH 11/14] Fix commit 2c8ccab The dropdown was incorrectly closing with right arrow too --- src/parts/suggestions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parts/suggestions.js b/src/parts/suggestions.js index 75ce0ee7..bc8a6971 100644 --- a/src/parts/suggestions.js +++ b/src/parts/suggestions.js @@ -96,7 +96,7 @@ export default { this.state.autoCompleteData = selectedElmData; this.input.autocomplete.set.call(this, value) return false - } else if(isSelectMode && _s.userInput && !_s.autoComplete.tabKey) { // do not forget to hide the dropdown in select mode! + } else if( e.key == 'Tab' && !_s.autoComplete.tabKey && isSelectMode && _s.userInput ) { // do not forget to hide the dropdown in select mode if Tab is pressed this.dropdown.hide() return false } From 05a103998cd828396b1b210a149c53ee4c7dea73 Mon Sep 17 00:00:00 2001 From: Massi-X Date: Sat, 7 Dec 2024 14:40:04 +0100 Subject: [PATCH 12/14] Prevent undefined exceptions --- src/parts/suggestions.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/parts/suggestions.js b/src/parts/suggestions.js index bc8a6971..e1f00720 100644 --- a/src/parts/suggestions.js +++ b/src/parts/suggestions.js @@ -293,7 +293,8 @@ export default { var value = elm.getAttribute('value'), isNoMatch = value == 'noMatch', isMixMode = _s.mode == 'mix', - tagData = this.suggestedListItems.find(item => (item.value ?? item) == value) + tagData = this.suggestedListItems.find(item => (item.value ?? item) == value), + tagTextElem = this.DOM.scope.querySelector('.' + _s.classNames.tagText) // The below event must be triggered, regardless of anything else which might go wrong this.trigger('dropdown:select', {data:tagData, elm, event}) @@ -325,7 +326,7 @@ export default { // select mode: reset the dropdown to show all options again to the user if(_s.mode == 'select' && !this.state.composing && this.userInput) - setTimeout(() => this.DOM.scope.querySelector('.' + _s.classNames.tagText).focus(), 0) //set the focus back to input on each select to ensure consistent behavior + setTimeout(() => tagTextElem && tagTextElem.focus(), 0) //set the focus back to input on each select to ensure consistent behavior closeOnSelect && setTimeout(this.dropdown.hide.bind(this)) @@ -503,4 +504,4 @@ export default { return this.settings.templates.dropdownItem.apply(this, [{...suggestion, mappedValue}, this]) }).join("") } -} \ No newline at end of file +} From 95292e27508dabb233b1c71b3c1d36d09c789513 Mon Sep 17 00:00:00 2001 From: Massi-X Date: Mon, 9 Dec 2024 00:19:12 +0100 Subject: [PATCH 13/14] Revert "Make sure the dropdown closes if no instance is found" This reverts commit 5ae3c627f67c7085a0b54086cdaf2bfd69e54e5b. --- src/parts/events.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parts/events.js b/src/parts/events.js index fcbde9bc..55731f01 100644 --- a/src/parts/events.js +++ b/src/parts/events.js @@ -723,7 +723,7 @@ export default { this.state.hasFocus = false // do not hide the dropdown if a click was initiated within it and that dropdown belongs to this Tagify instance - if( e.target.closest('.tagify__dropdown') == null || e.target.closest('.tagify__dropdown').__tagify != this ) + if( e.target.closest('.tagify__dropdown') && e.target.closest('.tagify__dropdown').__tagify != this ) this.dropdown.hide() } }, From 41178f7dd5a80fe69878f2543a2e9591752268ad Mon Sep 17 00:00:00 2001 From: Massi-X Date: Mon, 9 Dec 2024 20:32:06 +0100 Subject: [PATCH 14/14] Fix checkmark if input is in edit mode When input is in edit mode the checkmark stayed on the previous item --- src/parts/suggestions.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/parts/suggestions.js b/src/parts/suggestions.js index e1f00720..263548f1 100644 --- a/src/parts/suggestions.js +++ b/src/parts/suggestions.js @@ -311,10 +311,9 @@ export default { // normalizing value, because "tagData" might be a string, and therefore will not be able to extend the object this.onEditTagDone(null, extend({__isValid: true}, tagData)) } + // Tagify instances should re-focus to the input element once an option was selected, to allow continuous typing - else { - this[isMixMode ? "addMixTags" : "addTags"]([tagData || this.input.raw.call(this)], clearOnSelect) - } + this[isMixMode ? "addMixTags" : "addTags"]([tagData || this.input.raw.call(this)], clearOnSelect) if( !isMixMode && !this.DOM.input.parentNode ) return