diff --git a/dist/tagify.js b/dist/tagify.js index d17ec11b..8b410389 100644 --- a/dist/tagify.js +++ b/dist/tagify.js @@ -29,6 +29,10 @@ function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object. function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } } +// console.json = console.json || function(argument){ +// for(var arg=0; arg < arguments.length; ++arg) +// console.log( JSON.stringify(arguments[arg], null, 4) ) +// } var isFirefox = typeof InstallTrigger !== 'undefined'; // const isEdge = /Edge/.test(navigator.userAgent) var sameStr = function sameStr(s1, s2) { @@ -971,7 +975,10 @@ Tagify.prototype = { }; }, onEditTagBlur: function onEditTagBlur(editableElm) { - if (!this.state.hasFocus) this.toggleFocusClass(); + this.state.editing = false; + if (!this.state.hasFocus) this.toggleFocusClass(); // one scenario is when selecting a suggestion from the dropdown, when editing, and by selecting it + // the "onEditTagDone" is called directly, already replacing the tag, so the argument "editableElm" node isn't in the DOM + if (!this.DOM.scope.contains(editableElm)) return; var tagElm = editableElm.closest('.tagify__tag'), currentValue = this.input.normalize.call(this, editableElm), @@ -1073,7 +1080,6 @@ Tagify.prototype = { that = this, isValid = true, delayed_onEditTagBlur = function delayed_onEditTagBlur() { - that.state.editing = false; setTimeout(_CB.onEditTagBlur.bind(that), 0, editableElm); }; @@ -1138,13 +1144,14 @@ Tagify.prototype = { } this.trigger("edit:updated", eventData); - this.dropdown.hide.call(this); + this.dropdown.hide.call(this); // need to re-validate all invalid tags, which might not be invalid anymore + // (if one of them was a duplicate of a tag which was edited and not they aren't a duplicate of the edited one) + if (tagData.__isValid === true) this.getTagElms('tagify--notAllowed').forEach(function (notAllowedTag) { var isValid = _this7.validateTag(notAllowedTag.__tagifyTagData); if (isValid === true) { notAllowedTag.__tagifyTagData.__isValid = isValid; - delete _this7.state.editing.locked; // unfortunatly this must be cleaned so "replaceTag" could re-run _this7.replaceTag(notAllowedTag); } @@ -1157,21 +1164,10 @@ Tagify.prototype = { * @param {Object} tagData [data to create new tag from] */ replaceTag: function replaceTag(tagElm, tagData) { - var _this8 = this; - if (!tagData || !tagData.value) tagData = tagElm.__tagifyTagData; // if tag is invalid, make the according changes in the newly created element if (tagData.__isValid && tagData.__isValid != true) extend(tagData, this.getInvaildTagParams(tagData, tagData.__isValid)); - var newTag = this.createTagElem(tagData); // when editing a tag and selecting a dropdown suggested item, the state should be "locked" - // so "onEditTagBlur" won't run and change the tag also *after* it was just changed. - - if (this.state.editing.locked) return; - this.state.editing = { - locked: true - }; - setTimeout(function () { - return delete _this8.state.editing.locked; - }, 500); // update DOM + var newTag = this.createTagElem(tagData); // update DOM tagElm.parentNode.replaceChild(newTag, tagElm); this.updateValueByDOMTags(); @@ -1181,13 +1177,13 @@ Tagify.prototype = { * update value by traversing all valid tags */ updateValueByDOMTags: function updateValueByDOMTags() { - var _this9 = this; + var _this8 = this; this.value = []; [].forEach.call(this.getTagElms(), function (node) { if (node.classList.contains('tagify--notAllowed')) return; - _this9.value.push(node.__tagifyTagData); + _this8.value.push(node.__tagifyTagData); }); this.update(); }, @@ -1512,7 +1508,7 @@ Tagify.prototype = { * @param {String} s */ parseMixTags: function parseMixTags(s) { - var _this10 = this; + var _this9 = this; var _this$settings2 = this.settings, mixTagsInterpolator = _this$settings2.mixTagsInterpolator, @@ -1529,17 +1525,17 @@ Tagify.prototype = { try { tagData = JSON.parse(preInterpolated); } catch (err) { - tagData = _this10.normalizeTags(preInterpolated)[0]; //{value:preInterpolated} + tagData = _this9.normalizeTags(preInterpolated)[0]; //{value:preInterpolated} } - if (s2.length > 1 && (!enforceWhitelist || _this10.isTagWhitelisted(tagData.value)) && !(!duplicates && _this10.isTagDuplicate(tagData.value))) { - transformTag.call(_this10, tagData); - tagElm = _this10.createTagElem(tagData); + if (s2.length > 1 && (!enforceWhitelist || _this9.isTagWhitelisted(tagData.value)) && !(!duplicates && _this9.isTagDuplicate(tagData.value))) { + transformTag.call(_this9, tagData); + tagElm = _this9.createTagElem(tagData); tagsDataSet.push(tagData); tagElm.classList.add('tagify--noAnim'); s2[0] = tagElm.outerHTML; //+ "⁠" // put a zero-space at the end so the caret won't jump back to the start (when the last input's child element is a tag) - _this10.value.push(tagData); + _this9.value.push(tagData); } else if (s1) return i ? mixTagsInterpolator[0] + s1 : s1; return s2.join(''); @@ -1581,7 +1577,7 @@ Tagify.prototype = { }, /** - * For selecting a single option (not used for multiple tags) + * For selecting a single option (not used for multiple tags, but for "mode:select" only) * @param {Object} tagElm Tag DOM node * @param {Object} tagData Tag data */ @@ -1625,7 +1621,7 @@ Tagify.prototype = { * @return {Array} Array of DOM elements (tags) */ addTags: function addTags(tagsItems, clearInput) { - var _this11 = this; + var _this10 = this; var skipInvalid = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.settings.skipInvalid; var tagElems = [], @@ -1639,14 +1635,7 @@ Tagify.prototype = { } // converts Array/String/Object to an Array of Objects - tagsItems = this.normalizeTags(tagsItems); // if in edit-mode, do not continue but instead replace the tag's text. - // the scenario is that "addTags" was called from a dropdown suggested option selected while editing - - if (this.state.editing.scope) { - tagsItems[0].__isValid = true; // must be "true" at this point because it must have been coming from the dropdown sugegstions list - - return this.onEditTagDone(this.state.editing.scope, tagsItems[0]); - } + tagsItems = this.normalizeTags(tagsItems); if (_s.mode == 'mix') { _s.transformTag.call(this, tagsItems[0]); // TODO: should check if the tag is valid @@ -1684,16 +1673,16 @@ Tagify.prototype = { tagData = Object.assign({}, tagData); - _s.transformTag.call(_this11, tagData); ///////////////// ( validation )////////////////////// + _s.transformTag.call(_this10, tagData); ///////////////// ( validation )////////////////////// - tagData.__isValid = _this11.hasMaxTags() || _this11.validateTag(tagData); + tagData.__isValid = _this10.hasMaxTags() || _this10.validateTag(tagData); if (tagData.__isValid !== true) { if (skipInvalid) return; - extend(tagElmParams, _this11.getInvaildTagParams(tagData, tagData.__isValid)); // mark, for a brief moment, the tag THIS CURRENT tag is a duplcate of + extend(tagElmParams, _this10.getInvaildTagParams(tagData, tagData.__isValid)); // mark, for a brief moment, the tag THIS CURRENT tag is a duplcate of - if (tagData.__isValid == _this11.TEXTS.duplicate) _this11.markTagByValue(tagData.value); + if (tagData.__isValid == _this10.TEXTS.duplicate) _this10.markTagByValue(tagData.value); } ///////////////////////////////////////////////////// // add accessibility attributes @@ -1701,43 +1690,43 @@ Tagify.prototype = { tagElmParams.role = "tag"; if (tagData.readonly) tagElmParams["aria-readonly"] = true; // Create tag HTML element - tagElm = _this11.createTagElem(extend({}, tagData, tagElmParams)); + tagElm = _this10.createTagElem(extend({}, tagData, tagElmParams)); tagElm.__tagifyTagData = tagData; tagElems.push(tagElm); // mode-select overrides if (_s.mode == 'select') { - return _this11.selectTag(tagElm, tagData); + return _this10.selectTag(tagElm, tagData); } // add the tag to the component's DOM - _this11.appendTag(tagElm); + _this10.appendTag(tagElm); if (tagData.__isValid && tagData.__isValid === true) { // update state - _this11.value.push(tagData); + _this10.value.push(tagData); - _this11.update(); + _this10.update(); - _this11.trigger('add', { + _this10.trigger('add', { tag: tagElm, - index: _this11.value.length - 1, + index: _this10.value.length - 1, data: tagData }); } else { - _this11.trigger("invalid", { + _this10.trigger("invalid", { data: tagData, - index: _this11.value.length, + index: _this10.value.length, tag: tagElm, message: tagData.__isValid }); if (!_s.keepInvalidTags) // remove invalid tags (if "keepInvalidTags" is set to "false") setTimeout(function () { - return _this11.removeTags(tagElm, true); + return _this10.removeTags(tagElm, true); }, 1000); } - _this11.dropdown.position.call(_this11); // reposition the dropdown because the just-added tag might cause a new-line + _this10.dropdown.position.call(_this10); // reposition the dropdown because the just-added tag might cause a new-line }); @@ -1774,20 +1763,20 @@ Tagify.prototype = { return tagElm; }, reCheckInvalidTags: function reCheckInvalidTags() { - var _this12 = this; + var _this11 = this; // find all invalid tags and re-check them var tagElms = this.DOM.scope.querySelectorAll('.tagify__tag.tagify--notAllowed'); [].forEach.call(tagElms, function (node) { var tagData = node.__tagifyTagData, - wasNodeDuplicate = node.getAttribute('title') == _this12.TEXTS.duplicate, - isNodeValid = _this12.validateTag(tagData) === true; // if this tag node was marked as a dulpicate, unmark it (it might have been marked as "notAllowed" for other reasons) + wasNodeDuplicate = node.getAttribute('title') == _this11.TEXTS.duplicate, + isNodeValid = _this11.validateTag(tagData) === true; // if this tag node was marked as a dulpicate, unmark it (it might have been marked as "notAllowed" for other reasons) if (wasNodeDuplicate && isNodeValid) { tagData.__isValid = true; - _this12.replaceTag(node, tagData); + _this11.replaceTag(node, tagData); } }); }, @@ -1800,7 +1789,7 @@ Tagify.prototype = { * TODO: Allow multiple tags to be removed at-once */ removeTags: function removeTags(tagElms, silent, tranDuration) { - var _this13 = this; + var _this12 = this; tagElms = tagElms && tagElms instanceof HTMLElement ? [tagElms] : tagElms instanceof Array ? tagElms : tagElms ? [tagElms] : [this.getLastTag()]; // normalize tagElms array values: // 1. removing invalid items @@ -1808,7 +1797,7 @@ Tagify.prototype = { tagElms = tagElms.reduce(function (elms, tagElm) { if (tagElm) { - if (typeof tagElm == 'string') tagElm = _this13.getTagElmByValue(tagElm); + if (typeof tagElm == 'string') tagElm = _this12.getTagElmByValue(tagElm); tagElm && elms.push(tagElm); } @@ -1870,9 +1859,9 @@ Tagify.prototype = { if (!silent) { tagElms.forEach(function (tagElm) { - var tagIdx = _this13.getTagIdx(tagElm.__tagifyTagData); + var tagIdx = _this12.getTagIdx(tagElm.__tagifyTagData); - if (tagIdx > -1) _this13.value.splice(tagIdx, 1); + if (tagIdx > -1) _this12.value.splice(tagIdx, 1); }); // that.removeValueById(tagData.__uid) this.update(); // update the original input with the current value @@ -1971,7 +1960,7 @@ Tagify.prototype = { this.DOM.dropdown.content = this.DOM.dropdown.querySelector('.tagify__dropdown__wrapper'); }, show: function show(value) { - var _this14 = this; + var _this13 = this; var HTMLContent, _s = this.settings, @@ -2049,7 +2038,7 @@ Tagify.prototype = { this.dropdown.position.call(this, ddHeight); document.body.appendChild(this.DOM.dropdown); setTimeout(function () { - return _this14.DOM.dropdown.classList.remove('tagify__dropdown--initial'); + return _this13.DOM.dropdown.classList.remove('tagify__dropdown--initial'); }); } // timeout is needed for when pressing arrow down to show the dropdown, // so the key event won't get registered in the dropdown events listeners @@ -2312,23 +2301,31 @@ Tagify.prototype = { * @param {Object} elm DOM node to select */ selectOption: function selectOption(elm) { - var _this15 = this; + var _this14 = this; - if (!elm) return; // temporary set the "actions" state to indicate to the main "blur" event it shouldn't run + if (!elm) return; // if in edit-mode, do not continue but instead replace the tag's text. + // the scenario is that "addTags" was called from a dropdown suggested option selected while editing + // temporary set the "actions" state to indicate to the main "blur" event it shouldn't run this.state.actions.selectOption = true; setTimeout(function () { - return _this15.state.actions.selectOption = false; + return _this14.state.actions.selectOption = false; }, 50); var hideDropdown = this.settings.dropdown.closeOnSelect, - value = this.suggestedListItems[this.getNodeIndex(elm)] || this.input.value; + value = this.suggestedListItems[this.getNodeIndex(elm)].value || this.input.value; this.trigger("dropdown:select", value); - this.addTags([value], true); // Tagify instances should re-focus to the input element once an option was selected, to allow continuous typing + if (this.state.editing) this.onEditTagDone(this.state.editing.scope, _objectSpread({}, this.state.editing.scope.__tagifyTagData, { + value: value, + __isValid: true + })); // Tagify instances should re-focus to the input element once an option was selected, to allow continuous typing + else { + this.addTags([value], true); + } // todo: consider not doing this on mix-mode - if (!this.state.editing) setTimeout(function () { - _this15.DOM.input.focus(); + setTimeout(function () { + _this14.DOM.input.focus(); - _this15.toggleFocusClass(true); + _this14.toggleFocusClass(true); }); if (hideDropdown) { @@ -2342,7 +2339,7 @@ Tagify.prototype = { * @return {Array} list of filtered whitelist items according to the settings provided and current value */ filterListItems: function filterListItems(value) { - var _this16 = this; + var _this15 = this; var _s = this.settings, list = [], @@ -2358,7 +2355,7 @@ Tagify.prototype = { if (!value) { return (_s.duplicates ? whitelist : whitelist.filter(function (item) { - return !_this16.isTagDuplicate(isObject(item) ? item.value : item); + return !_this15.isTagDuplicate(isObject(item) ? item.value : item); }) // don't include tags which have already been added. ).slice(0, suggestionsCount); // respect "maxItems" dropdown setting } @@ -2388,18 +2385,18 @@ Tagify.prototype = { * @return {String} */ createListHTML: function createListHTML(optionsArr) { - var _this17 = this; + var _this16 = this; return optionsArr.map(function (item) { if (typeof item == 'string') item = { value: item }; - var mapValueTo = _this17.settings.dropdown.mapValueTo, + var mapValueTo = _this16.settings.dropdown.mapValueTo, value = mapValueTo ? typeof mapValueTo == 'function' ? mapValueTo(item) : item[mapValueTo] : item.value, data = extend({}, item, { value: escapeHTML(value || "") }); - return _this17.settings.templates.dropdownItem.call(_this17, data); + return _this16.settings.templates.dropdownItem.call(_this16, data); }).join(""); } } diff --git a/dist/tagify.min.js b/dist/tagify.min.js index 46b8a0f1..ae2cbf01 100644 --- a/dist/tagify.min.js +++ b/dist/tagify.min.js @@ -35,6 +35,10 @@ function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object. function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } } +// console.json = console.json || function(argument){ +// for(var arg=0; arg < arguments.length; ++arg) +// console.log( JSON.stringify(arguments[arg], null, 4) ) +// } var isFirefox = typeof InstallTrigger !== 'undefined'; // const isEdge = /Edge/.test(navigator.userAgent) var sameStr = function sameStr(s1, s2) { @@ -977,7 +981,10 @@ Tagify.prototype = { }; }, onEditTagBlur: function onEditTagBlur(editableElm) { - if (!this.state.hasFocus) this.toggleFocusClass(); + this.state.editing = false; + if (!this.state.hasFocus) this.toggleFocusClass(); // one scenario is when selecting a suggestion from the dropdown, when editing, and by selecting it + // the "onEditTagDone" is called directly, already replacing the tag, so the argument "editableElm" node isn't in the DOM + if (!this.DOM.scope.contains(editableElm)) return; var tagElm = editableElm.closest('.tagify__tag'), currentValue = this.input.normalize.call(this, editableElm), @@ -1079,7 +1086,6 @@ Tagify.prototype = { that = this, isValid = true, delayed_onEditTagBlur = function delayed_onEditTagBlur() { - that.state.editing = false; setTimeout(_CB.onEditTagBlur.bind(that), 0, editableElm); }; @@ -1144,13 +1150,14 @@ Tagify.prototype = { } this.trigger("edit:updated", eventData); - this.dropdown.hide.call(this); + this.dropdown.hide.call(this); // need to re-validate all invalid tags, which might not be invalid anymore + // (if one of them was a duplicate of a tag which was edited and not they aren't a duplicate of the edited one) + if (tagData.__isValid === true) this.getTagElms('tagify--notAllowed').forEach(function (notAllowedTag) { var isValid = _this7.validateTag(notAllowedTag.__tagifyTagData); if (isValid === true) { notAllowedTag.__tagifyTagData.__isValid = isValid; - delete _this7.state.editing.locked; // unfortunatly this must be cleaned so "replaceTag" could re-run _this7.replaceTag(notAllowedTag); } @@ -1163,21 +1170,10 @@ Tagify.prototype = { * @param {Object} tagData [data to create new tag from] */ replaceTag: function replaceTag(tagElm, tagData) { - var _this8 = this; - if (!tagData || !tagData.value) tagData = tagElm.__tagifyTagData; // if tag is invalid, make the according changes in the newly created element if (tagData.__isValid && tagData.__isValid != true) extend(tagData, this.getInvaildTagParams(tagData, tagData.__isValid)); - var newTag = this.createTagElem(tagData); // when editing a tag and selecting a dropdown suggested item, the state should be "locked" - // so "onEditTagBlur" won't run and change the tag also *after* it was just changed. - - if (this.state.editing.locked) return; - this.state.editing = { - locked: true - }; - setTimeout(function () { - return delete _this8.state.editing.locked; - }, 500); // update DOM + var newTag = this.createTagElem(tagData); // update DOM tagElm.parentNode.replaceChild(newTag, tagElm); this.updateValueByDOMTags(); @@ -1187,13 +1183,13 @@ Tagify.prototype = { * update value by traversing all valid tags */ updateValueByDOMTags: function updateValueByDOMTags() { - var _this9 = this; + var _this8 = this; this.value = []; [].forEach.call(this.getTagElms(), function (node) { if (node.classList.contains('tagify--notAllowed')) return; - _this9.value.push(node.__tagifyTagData); + _this8.value.push(node.__tagifyTagData); }); this.update(); }, @@ -1518,7 +1514,7 @@ Tagify.prototype = { * @param {String} s */ parseMixTags: function parseMixTags(s) { - var _this10 = this; + var _this9 = this; var _this$settings2 = this.settings, mixTagsInterpolator = _this$settings2.mixTagsInterpolator, @@ -1535,17 +1531,17 @@ Tagify.prototype = { try { tagData = JSON.parse(preInterpolated); } catch (err) { - tagData = _this10.normalizeTags(preInterpolated)[0]; //{value:preInterpolated} + tagData = _this9.normalizeTags(preInterpolated)[0]; //{value:preInterpolated} } - if (s2.length > 1 && (!enforceWhitelist || _this10.isTagWhitelisted(tagData.value)) && !(!duplicates && _this10.isTagDuplicate(tagData.value))) { - transformTag.call(_this10, tagData); - tagElm = _this10.createTagElem(tagData); + if (s2.length > 1 && (!enforceWhitelist || _this9.isTagWhitelisted(tagData.value)) && !(!duplicates && _this9.isTagDuplicate(tagData.value))) { + transformTag.call(_this9, tagData); + tagElm = _this9.createTagElem(tagData); tagsDataSet.push(tagData); tagElm.classList.add('tagify--noAnim'); s2[0] = tagElm.outerHTML; //+ "⁠" // put a zero-space at the end so the caret won't jump back to the start (when the last input's child element is a tag) - _this10.value.push(tagData); + _this9.value.push(tagData); } else if (s1) return i ? mixTagsInterpolator[0] + s1 : s1; return s2.join(''); @@ -1587,7 +1583,7 @@ Tagify.prototype = { }, /** - * For selecting a single option (not used for multiple tags) + * For selecting a single option (not used for multiple tags, but for "mode:select" only) * @param {Object} tagElm Tag DOM node * @param {Object} tagData Tag data */ @@ -1631,7 +1627,7 @@ Tagify.prototype = { * @return {Array} Array of DOM elements (tags) */ addTags: function addTags(tagsItems, clearInput) { - var _this11 = this; + var _this10 = this; var skipInvalid = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.settings.skipInvalid; var tagElems = [], @@ -1645,14 +1641,7 @@ Tagify.prototype = { } // converts Array/String/Object to an Array of Objects - tagsItems = this.normalizeTags(tagsItems); // if in edit-mode, do not continue but instead replace the tag's text. - // the scenario is that "addTags" was called from a dropdown suggested option selected while editing - - if (this.state.editing.scope) { - tagsItems[0].__isValid = true; // must be "true" at this point because it must have been coming from the dropdown sugegstions list - - return this.onEditTagDone(this.state.editing.scope, tagsItems[0]); - } + tagsItems = this.normalizeTags(tagsItems); if (_s.mode == 'mix') { _s.transformTag.call(this, tagsItems[0]); // TODO: should check if the tag is valid @@ -1690,16 +1679,16 @@ Tagify.prototype = { tagData = Object.assign({}, tagData); - _s.transformTag.call(_this11, tagData); ///////////////// ( validation )////////////////////// + _s.transformTag.call(_this10, tagData); ///////////////// ( validation )////////////////////// - tagData.__isValid = _this11.hasMaxTags() || _this11.validateTag(tagData); + tagData.__isValid = _this10.hasMaxTags() || _this10.validateTag(tagData); if (tagData.__isValid !== true) { if (skipInvalid) return; - extend(tagElmParams, _this11.getInvaildTagParams(tagData, tagData.__isValid)); // mark, for a brief moment, the tag THIS CURRENT tag is a duplcate of + extend(tagElmParams, _this10.getInvaildTagParams(tagData, tagData.__isValid)); // mark, for a brief moment, the tag THIS CURRENT tag is a duplcate of - if (tagData.__isValid == _this11.TEXTS.duplicate) _this11.markTagByValue(tagData.value); + if (tagData.__isValid == _this10.TEXTS.duplicate) _this10.markTagByValue(tagData.value); } ///////////////////////////////////////////////////// // add accessibility attributes @@ -1707,43 +1696,43 @@ Tagify.prototype = { tagElmParams.role = "tag"; if (tagData.readonly) tagElmParams["aria-readonly"] = true; // Create tag HTML element - tagElm = _this11.createTagElem(extend({}, tagData, tagElmParams)); + tagElm = _this10.createTagElem(extend({}, tagData, tagElmParams)); tagElm.__tagifyTagData = tagData; tagElems.push(tagElm); // mode-select overrides if (_s.mode == 'select') { - return _this11.selectTag(tagElm, tagData); + return _this10.selectTag(tagElm, tagData); } // add the tag to the component's DOM - _this11.appendTag(tagElm); + _this10.appendTag(tagElm); if (tagData.__isValid && tagData.__isValid === true) { // update state - _this11.value.push(tagData); + _this10.value.push(tagData); - _this11.update(); + _this10.update(); - _this11.trigger('add', { + _this10.trigger('add', { tag: tagElm, - index: _this11.value.length - 1, + index: _this10.value.length - 1, data: tagData }); } else { - _this11.trigger("invalid", { + _this10.trigger("invalid", { data: tagData, - index: _this11.value.length, + index: _this10.value.length, tag: tagElm, message: tagData.__isValid }); if (!_s.keepInvalidTags) // remove invalid tags (if "keepInvalidTags" is set to "false") setTimeout(function () { - return _this11.removeTags(tagElm, true); + return _this10.removeTags(tagElm, true); }, 1000); } - _this11.dropdown.position.call(_this11); // reposition the dropdown because the just-added tag might cause a new-line + _this10.dropdown.position.call(_this10); // reposition the dropdown because the just-added tag might cause a new-line }); @@ -1780,20 +1769,20 @@ Tagify.prototype = { return tagElm; }, reCheckInvalidTags: function reCheckInvalidTags() { - var _this12 = this; + var _this11 = this; // find all invalid tags and re-check them var tagElms = this.DOM.scope.querySelectorAll('.tagify__tag.tagify--notAllowed'); [].forEach.call(tagElms, function (node) { var tagData = node.__tagifyTagData, - wasNodeDuplicate = node.getAttribute('title') == _this12.TEXTS.duplicate, - isNodeValid = _this12.validateTag(tagData) === true; // if this tag node was marked as a dulpicate, unmark it (it might have been marked as "notAllowed" for other reasons) + wasNodeDuplicate = node.getAttribute('title') == _this11.TEXTS.duplicate, + isNodeValid = _this11.validateTag(tagData) === true; // if this tag node was marked as a dulpicate, unmark it (it might have been marked as "notAllowed" for other reasons) if (wasNodeDuplicate && isNodeValid) { tagData.__isValid = true; - _this12.replaceTag(node, tagData); + _this11.replaceTag(node, tagData); } }); }, @@ -1806,7 +1795,7 @@ Tagify.prototype = { * TODO: Allow multiple tags to be removed at-once */ removeTags: function removeTags(tagElms, silent, tranDuration) { - var _this13 = this; + var _this12 = this; tagElms = tagElms && tagElms instanceof HTMLElement ? [tagElms] : tagElms instanceof Array ? tagElms : tagElms ? [tagElms] : [this.getLastTag()]; // normalize tagElms array values: // 1. removing invalid items @@ -1814,7 +1803,7 @@ Tagify.prototype = { tagElms = tagElms.reduce(function (elms, tagElm) { if (tagElm) { - if (typeof tagElm == 'string') tagElm = _this13.getTagElmByValue(tagElm); + if (typeof tagElm == 'string') tagElm = _this12.getTagElmByValue(tagElm); tagElm && elms.push(tagElm); } @@ -1876,9 +1865,9 @@ Tagify.prototype = { if (!silent) { tagElms.forEach(function (tagElm) { - var tagIdx = _this13.getTagIdx(tagElm.__tagifyTagData); + var tagIdx = _this12.getTagIdx(tagElm.__tagifyTagData); - if (tagIdx > -1) _this13.value.splice(tagIdx, 1); + if (tagIdx > -1) _this12.value.splice(tagIdx, 1); }); // that.removeValueById(tagData.__uid) this.update(); // update the original input with the current value @@ -1977,7 +1966,7 @@ Tagify.prototype = { this.DOM.dropdown.content = this.DOM.dropdown.querySelector('.tagify__dropdown__wrapper'); }, show: function show(value) { - var _this14 = this; + var _this13 = this; var HTMLContent, _s = this.settings, @@ -2055,7 +2044,7 @@ Tagify.prototype = { this.dropdown.position.call(this, ddHeight); document.body.appendChild(this.DOM.dropdown); setTimeout(function () { - return _this14.DOM.dropdown.classList.remove('tagify__dropdown--initial'); + return _this13.DOM.dropdown.classList.remove('tagify__dropdown--initial'); }); } // timeout is needed for when pressing arrow down to show the dropdown, // so the key event won't get registered in the dropdown events listeners @@ -2318,23 +2307,31 @@ Tagify.prototype = { * @param {Object} elm DOM node to select */ selectOption: function selectOption(elm) { - var _this15 = this; + var _this14 = this; - if (!elm) return; // temporary set the "actions" state to indicate to the main "blur" event it shouldn't run + if (!elm) return; // if in edit-mode, do not continue but instead replace the tag's text. + // the scenario is that "addTags" was called from a dropdown suggested option selected while editing + // temporary set the "actions" state to indicate to the main "blur" event it shouldn't run this.state.actions.selectOption = true; setTimeout(function () { - return _this15.state.actions.selectOption = false; + return _this14.state.actions.selectOption = false; }, 50); var hideDropdown = this.settings.dropdown.closeOnSelect, - value = this.suggestedListItems[this.getNodeIndex(elm)] || this.input.value; + value = this.suggestedListItems[this.getNodeIndex(elm)].value || this.input.value; this.trigger("dropdown:select", value); - this.addTags([value], true); // Tagify instances should re-focus to the input element once an option was selected, to allow continuous typing + if (this.state.editing) this.onEditTagDone(this.state.editing.scope, _objectSpread({}, this.state.editing.scope.__tagifyTagData, { + value: value, + __isValid: true + })); // Tagify instances should re-focus to the input element once an option was selected, to allow continuous typing + else { + this.addTags([value], true); + } // todo: consider not doing this on mix-mode - if (!this.state.editing) setTimeout(function () { - _this15.DOM.input.focus(); + setTimeout(function () { + _this14.DOM.input.focus(); - _this15.toggleFocusClass(true); + _this14.toggleFocusClass(true); }); if (hideDropdown) { @@ -2348,7 +2345,7 @@ Tagify.prototype = { * @return {Array} list of filtered whitelist items according to the settings provided and current value */ filterListItems: function filterListItems(value) { - var _this16 = this; + var _this15 = this; var _s = this.settings, list = [], @@ -2364,7 +2361,7 @@ Tagify.prototype = { if (!value) { return (_s.duplicates ? whitelist : whitelist.filter(function (item) { - return !_this16.isTagDuplicate(isObject(item) ? item.value : item); + return !_this15.isTagDuplicate(isObject(item) ? item.value : item); }) // don't include tags which have already been added. ).slice(0, suggestionsCount); // respect "maxItems" dropdown setting } @@ -2394,18 +2391,18 @@ Tagify.prototype = { * @return {String} */ createListHTML: function createListHTML(optionsArr) { - var _this17 = this; + var _this16 = this; return optionsArr.map(function (item) { if (typeof item == 'string') item = { value: item }; - var mapValueTo = _this17.settings.dropdown.mapValueTo, + var mapValueTo = _this16.settings.dropdown.mapValueTo, value = mapValueTo ? typeof mapValueTo == 'function' ? mapValueTo(item) : item[mapValueTo] : item.value, data = extend({}, item, { value: escapeHTML(value || "") }); - return _this17.settings.templates.dropdownItem.call(_this17, data); + return _this16.settings.templates.dropdownItem.call(_this16, data); }).join(""); } } diff --git a/src/tagify.js b/src/tagify.js index 0f748579..67843841 100644 --- a/src/tagify.js +++ b/src/tagify.js @@ -1,3 +1,8 @@ +// console.json = console.json || function(argument){ +// for(var arg=0; arg < arguments.length; ++arg) +// console.log( JSON.stringify(arguments[arg], null, 4) ) +// } + const isFirefox = typeof InstallTrigger !== 'undefined' // const isEdge = /Edge/.test(navigator.userAgent) const sameStr = (s1, s2) => s1.toLowerCase() == s2.toLowerCase() @@ -967,9 +972,13 @@ Tagify.prototype = { }, onEditTagBlur( editableElm ){ + this.state.editing = false; + if( !this.state.hasFocus ) this.toggleFocusClass() + // one scenario is when selecting a suggestion from the dropdown, when editing, and by selecting it + // the "onEditTagDone" is called directly, already replacing the tag, so the argument "editableElm" node isn't in the DOM if( !this.DOM.scope.contains(editableElm) ) return; var tagElm = editableElm.closest('.tagify__tag'), @@ -1065,7 +1074,6 @@ Tagify.prototype = { that = this, isValid = true, delayed_onEditTagBlur = function(){ - that.state.editing = false; setTimeout(_CB.onEditTagBlur.bind(that), 0, editableElm) } @@ -1136,12 +1144,13 @@ Tagify.prototype = { this.trigger("edit:updated", eventData) this.dropdown.hide.call(this) + // need to re-validate all invalid tags, which might not be invalid anymore + // (if one of them was a duplicate of a tag which was edited and not they aren't a duplicate of the edited one) if( tagData.__isValid === true ) this.getTagElms('tagify--notAllowed').forEach(notAllowedTag => { var isValid = this.validateTag(notAllowedTag.__tagifyTagData) if( isValid === true){ notAllowedTag.__tagifyTagData.__isValid = isValid - delete this.state.editing.locked // unfortunatly this must be cleaned so "replaceTag" could re-run this.replaceTag(notAllowedTag) } }) @@ -1162,12 +1171,6 @@ Tagify.prototype = { var newTag = this.createTagElem(tagData); - // when editing a tag and selecting a dropdown suggested item, the state should be "locked" - // so "onEditTagBlur" won't run and change the tag also *after* it was just changed. - if( this.state.editing.locked ) return; - this.state.editing = { locked:true } - setTimeout(() => delete this.state.editing.locked, 500) - // update DOM tagElm.parentNode.replaceChild(newTag, tagElm) this.updateValueByDOMTags() @@ -1594,7 +1597,7 @@ Tagify.prototype = { }, /** - * For selecting a single option (not used for multiple tags) + * For selecting a single option (not used for multiple tags, but for "mode:select" only) * @param {Object} tagElm Tag DOM node * @param {Object} tagData Tag data */ @@ -1655,12 +1658,7 @@ Tagify.prototype = { // converts Array/String/Object to an Array of Objects tagsItems = this.normalizeTags(tagsItems) - // if in edit-mode, do not continue but instead replace the tag's text. - // the scenario is that "addTags" was called from a dropdown suggested option selected while editing - if( this.state.editing.scope ){ - tagsItems[0].__isValid = true; // must be "true" at this point because it must have been coming from the dropdown sugegstions list - return this.onEditTagDone(this.state.editing.scope, tagsItems[0]) - } + if( _s.mode == 'mix' ){ _s.transformTag.call(this, tagsItems[0]) @@ -2399,22 +2397,32 @@ Tagify.prototype = { */ selectOption( elm ){ if( !elm ) return; + + // if in edit-mode, do not continue but instead replace the tag's text. + // the scenario is that "addTags" was called from a dropdown suggested option selected while editing + // temporary set the "actions" state to indicate to the main "blur" event it shouldn't run this.state.actions.selectOption = true; setTimeout(()=> this.state.actions.selectOption = false, 50) var hideDropdown = this.settings.dropdown.closeOnSelect, - value = this.suggestedListItems[this.getNodeIndex(elm)] || this.input.value; + value = this.suggestedListItems[this.getNodeIndex(elm)].value || this.input.value; this.trigger("dropdown:select", value) - this.addTags([value], true) + + if( this.state.editing ) + this.onEditTagDone(this.state.editing.scope, {...this.state.editing.scope.__tagifyTagData, value, __isValid:true}) // Tagify instances should re-focus to the input element once an option was selected, to allow continuous typing - if( !this.state.editing ) - setTimeout(() => { - this.DOM.input.focus() - this.toggleFocusClass(true) - }) + else{ + this.addTags([value], true) + } + + // todo: consider not doing this on mix-mode + setTimeout(() => { + this.DOM.input.focus() + this.toggleFocusClass(true) + }) if( hideDropdown ){ this.dropdown.hide.call(this)