From af39a2320c24cf42c9a572248081fa79b51910d5 Mon Sep 17 00:00:00 2001 From: Yair Even Or Date: Mon, 12 Feb 2024 21:52:55 +0100 Subject: [PATCH] 4.21.0 --- dist/jQuery.tagify.min.js | 7 +- dist/react.tagify.js | 5 +- dist/tagify.esm.js | 1308 +++++++++++++++--------------- dist/tagify.esm.js.map | 1 + dist/tagify.js | 926 +++++++++++---------- dist/tagify.js.map | 1 + dist/tagify.min.js | 52 +- dist/tagify.min.js.map | 1 + dist/tagify.polyfills.min.js | 50 +- dist/tagify.polyfills.min.js.map | 1 + package.json | 2 +- 11 files changed, 1208 insertions(+), 1146 deletions(-) create mode 100644 dist/tagify.esm.js.map create mode 100644 dist/tagify.js.map create mode 100644 dist/tagify.min.js.map create mode 100644 dist/tagify.polyfills.min.js.map diff --git a/dist/jQuery.tagify.min.js b/dist/jQuery.tagify.min.js index fd8fd269..ada25776 100644 --- a/dist/jQuery.tagify.min.js +++ b/dist/jQuery.tagify.min.js @@ -1,7 +1,8 @@ /** - * Tagify (v 4.20.0) - tags input component - * By undefined + * Tagify v4.21.0 - tags input component + * By: Yair Even-Or * https://github.com/yairEO/tagify + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -23,4 +24,4 @@ * THE SOFTWARE IS NOT PERMISSIBLE TO BE SOLD. */ -!function(t){var e,i;t.fn.tagify=function(e={}){return this.each((function(){var i,s=t(this);if(s.data("tagify"))return this;e.isJQueryPlugin=!0,i=new Tagify(s[0],e),s.data("tagify",i)}))},e=this,i=function(){"use strict";function t(t,e){var i=Object.keys(t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(t);e&&(s=s.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),i.push.apply(i,s)}return i}function e(e){for(var s=1;s(t=""+t,e=""+e,s&&(t=t.trim(),e=e.trim()),i?t==e:t.toLowerCase()==e.toLowerCase()),a=(t,e)=>t&&Array.isArray(t)&&t.map((t=>n(t,e)));function n(t,e){var i,s={};for(i in t)e.indexOf(i)<0&&(s[i]=t[i]);return s}function o(t){var e=document.createElement("div");return t.replace(/\&#?[0-9a-z]+;/gi,(function(t){return e.innerHTML=t,e.innerText}))}function r(t){return(new DOMParser).parseFromString(t.trim(),"text/html").body.firstElementChild}function l(t,e){for(e=e||"previous";t=t[e+"Sibling"];)if(3==t.nodeType)return t}function d(t){return"string"==typeof t?t.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/`|'/g,"'"):t}function h(t){var e=Object.prototype.toString.call(t).split(" ")[1].slice(0,-1);return t===Object(t)&&"Array"!=e&&"Function"!=e&&"RegExp"!=e&&"HTMLUnknownElement"!=e}function g(t,e,i){function s(t,e){for(var i in e)if(e.hasOwnProperty(i)){if(h(e[i])){h(t[i])?s(t[i],e[i]):t[i]=Object.assign({},e[i]);continue}if(Array.isArray(e[i])){t[i]=Object.assign([],e[i]);continue}t[i]=e[i]}}return t instanceof Object||(t={}),s(t,e),i&&s(t,i),t}function p(){const t=[],e={};for(let i of arguments)for(let s of i)h(s)?e[s.value]||(t.push(s),e[s.value]=1):t.includes(s)||t.push(s);return t}function c(t){return String.prototype.normalize?"string"==typeof t?t.normalize("NFD").replace(/[\u0300-\u036f]/g,""):void 0:t}var u=()=>/(?=.*chrome)(?=.*android)/i.test(navigator.userAgent);function m(){return([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,(t=>(t^crypto.getRandomValues(new Uint8Array(1))[0]&15>>t/4).toString(16)))}function v(t){return t&&t.classList&&t.classList.contains(this.settings.classNames.tag)}function f(t,e){var i=window.getSelection();return e=e||i.getRangeAt(0),"string"==typeof t&&(t=document.createTextNode(t)),e&&(e.deleteContents(),e.insertNode(t)),t}function T(t,e,i){return t?(e&&(t.__tagifyTagData=i?e:g({},t.__tagifyTagData||{},e)),t.__tagifyTagData):(console.warn("tag element doesn't exist",t,e),e)}function w(t){if(t&&t.parentNode){var e=t,i=window.getSelection(),s=i.getRangeAt(0);i.rangeCount&&(s.setStartAfter(e),s.collapse(!0),i.removeAllRanges(),i.addRange(s))}}function b(t,e){t.forEach((t=>{if(T(t.previousSibling)||!t.previousSibling){var i=document.createTextNode("​");t.before(i),e&&w(i)}}))}var y={delimiters:",",pattern:null,tagTextProp:"value",maxTags:1/0,callbacks:{},addTagOnBlur:!0,addTagOn:["blur","tab","enter"],onChangeAfterBlur:!0,duplicates:!1,whitelist:[],blacklist:[],enforceWhitelist:!1,userInput:!0,keepInvalidTags:!1,createInvalidTags:!0,mixTagsAllowedAfter:/,|\.|\:|\s/,mixTagsInterpolator:["[[","]]"],backspace:!0,skipInvalid:!1,pasteAsTags:!0,editTags:{clicks:2,keepInvalid:!0},transformTag:()=>{},trim:!0,a11y:{focusableTags:!1},mixMode:{insertAfterTag:" "},autoComplete:{enabled:!0,rightKey:!1,tabKey:!1},classNames:{namespace:"tagify",mixMode:"tagify--mix",selectMode:"tagify--select",input:"tagify__input",focus:"tagify--focus",tagNoAnimation:"tagify--noAnim",tagInvalid:"tagify--invalid",tagNotAllowed:"tagify--notAllowed",scopeLoading:"tagify--loading",hasMaxTags:"tagify--hasMaxTags",hasNoTags:"tagify--noTags",empty:"tagify--empty",inputInvalid:"tagify__input--invalid",dropdown:"tagify__dropdown",dropdownWrapper:"tagify__dropdown__wrapper",dropdownHeader:"tagify__dropdown__header",dropdownFooter:"tagify__dropdown__footer",dropdownItem:"tagify__dropdown__item",dropdownItemActive:"tagify__dropdown__item--active",dropdownItemHidden:"tagify__dropdown__item--hidden",dropdownInital:"tagify__dropdown--initial",tag:"tagify__tag",tagText:"tagify__tag-text",tagX:"tagify__tag__removeBtn",tagLoading:"tagify__tag--loading",tagEditing:"tagify__tag--editable",tagFlash:"tagify__tag--flash",tagHide:"tagify__tag--hide"},dropdown:{classname:"",enabled:2,maxItems:10,searchKeys:["value","searchBy"],fuzzySearch:!0,caseSensitive:!1,accentedSearch:!0,includeSelectedTags:!1,escapeHTML:!0,highlightFirst:!1,closeOnSelect:!0,clearOnSelect:!0,position:"all",appendTarget:null},hooks:{beforeRemoveTag:()=>Promise.resolve(),beforePaste:()=>Promise.resolve(),suggestionClick:()=>Promise.resolve(),beforeKeyDown:()=>Promise.resolve()}};function x(){this.dropdown={};for(let t in this._dropdown)this.dropdown[t]="function"==typeof this._dropdown[t]?this._dropdown[t].bind(this):this._dropdown[t];this.dropdown.refs()}var O={refs(){this.DOM.dropdown=this.parseTemplate("dropdown",[this.settings]),this.DOM.dropdown.content=this.DOM.dropdown.querySelector("[data-selector='tagify-suggestions-wrapper']")},getHeaderRef(){return this.DOM.dropdown.querySelector("[data-selector='tagify-suggestions-header']")},getFooterRef(){return this.DOM.dropdown.querySelector("[data-selector='tagify-suggestions-footer']")},getAllSuggestionsRefs(){return[...this.DOM.dropdown.content.querySelectorAll(this.settings.classNames.dropdownItemSelector)]},show(t){var e,i,a,n=this.settings,o="mix"==n.mode&&!n.enforceWhitelist,r=!n.whitelist||!n.whitelist.length,l="manual"==n.dropdown.position;if(t=void 0===t?this.state.inputText:t,!(r&&!o&&!n.templates.dropdownItemNoMatch||!1===n.dropdown.enable||this.state.isLoading||this.settings.readonly)){if(clearTimeout(this.dropdownHide__bindEventsTimeout),this.suggestedListItems=this.dropdown.filterListItems(t),t&&!this.suggestedListItems.length&&(this.trigger("dropdown:noMatch",t),n.templates.dropdownItemNoMatch&&(a=n.templates.dropdownItemNoMatch.call(this,{value:t}))),!a){if(this.suggestedListItems.length)t&&o&&!this.state.editing.scope&&!s(this.suggestedListItems[0].value,t)&&this.suggestedListItems.unshift({value:t});else{if(!t||!o||this.state.editing.scope)return this.input.autocomplete.suggest.call(this),void this.dropdown.hide();this.suggestedListItems=[{value:t}]}i=""+(h(e=this.suggestedListItems[0])?e.value:e),n.autoComplete&&i&&0==i.indexOf(t)&&this.input.autocomplete.suggest.call(this,e)}this.dropdown.fill(a),n.dropdown.highlightFirst&&this.dropdown.highlightOption(this.DOM.dropdown.content.querySelector(n.classNames.dropdownItemSelector)),this.state.dropdown.visible||setTimeout(this.dropdown.events.binding.bind(this)),this.state.dropdown.visible=t||!0,this.state.dropdown.query=t,this.setStateSelection(),l||setTimeout((()=>{this.dropdown.position(),this.dropdown.render()})),setTimeout((()=>{this.trigger("dropdown:show",this.DOM.dropdown)}))}},hide(t){var e=this.DOM,i=e.scope,s=e.dropdown,a="manual"==this.settings.dropdown.position&&!t;if(s&&document.body.contains(s)&&!a)return window.removeEventListener("resize",this.dropdown.position),this.dropdown.events.binding.call(this,!1),i.setAttribute("aria-expanded",!1),s.parentNode.removeChild(s),setTimeout((()=>{this.state.dropdown.visible=!1}),100),this.state.dropdown.query=this.state.ddItemData=this.state.ddItemElm=this.state.selection=null,this.state.tag&&this.state.tag.value.length&&(this.state.flaggedTags[this.state.tag.baseOffset]=this.state.tag),this.trigger("dropdown:hide",s),this},toggle(t){this.dropdown[this.state.dropdown.visible&&!t?"hide":"show"]()},getAppendTarget(){var t=this.settings.dropdown;return"function"==typeof t.appendTarget?t.appendTarget():t.appendTarget},render(){var t,e,i=((e=this.DOM.dropdown.cloneNode(!0)).style.cssText="position:fixed; top:-9999px; opacity:0",document.body.appendChild(e),t=e.clientHeight,e.parentNode.removeChild(e),t),s=this.settings,a="number"==typeof s.dropdown.enabled&&s.dropdown.enabled>=0,n=this.dropdown.getAppendTarget();return a?(this.DOM.scope.setAttribute("aria-expanded",!0),document.body.contains(this.DOM.dropdown)||(this.DOM.dropdown.classList.add(s.classNames.dropdownInital),this.dropdown.position(i),n.appendChild(this.DOM.dropdown),setTimeout((()=>this.DOM.dropdown.classList.remove(s.classNames.dropdownInital)))),this):this},fill(t){t="string"==typeof t?t:this.dropdown.createListHTML(t||this.suggestedListItems);var e,i=this.settings.templates.dropdownContent.call(this,t);this.DOM.dropdown.content.innerHTML=(e=i)?e.replace(/\>[\r\n ]+\<").split(/>\s+<").trim():""},fillHeaderFooter(){var t=this.dropdown.filterListItems(this.state.dropdown.query),e=this.parseTemplate("dropdownHeader",[t]),i=this.parseTemplate("dropdownFooter",[t]),s=this.dropdown.getHeaderRef(),a=this.dropdown.getFooterRef();e&&s?.parentNode.replaceChild(e,s),i&&a?.parentNode.replaceChild(i,a)},refilter(t){t=t||this.state.dropdown.query||"",this.suggestedListItems=this.dropdown.filterListItems(t),this.dropdown.fill(),this.suggestedListItems.length||this.dropdown.hide(),this.trigger("dropdown:updated",this.DOM.dropdown)},position(t){var e=this.settings.dropdown,i=this.dropdown.getAppendTarget();if("manual"!=e.position&&i){var s,a,n,o,r,l,d,h,g,p=this.DOM.dropdown,c=e.RTL,u=i===document.body,m=i===this.DOM.scope,v=u?window.pageYOffset:i.scrollTop,f=document.fullscreenElement||document.webkitFullscreenElement||document.documentElement,T=f.clientHeight,w=Math.max(f.clientWidth||0,window.innerWidth||0)>480?e.position:"all",b=this.DOM["input"==w?"input":"scope"];if(t=t||p.clientHeight,this.state.dropdown.visible){if("text"==w?(n=(s=function(){const t=document.getSelection();if(t.rangeCount){const e=t.getRangeAt(0),i=e.startContainer,s=e.startOffset;let a,n;if(s>0)return n=document.createRange(),n.setStart(i,s-1),n.setEnd(i,s),a=n.getBoundingClientRect(),{left:a.right,top:a.top,bottom:a.bottom};if(i.getBoundingClientRect)return i.getBoundingClientRect()}return{left:-9999,top:-9999}}()).bottom,a=s.top,o=s.left,r="auto"):(l=function(t){var e=0,i=0;for(t=t.parentNode;t&&t!=f;)e+=t.offsetTop||0,i+=t.offsetLeft||0,t=t.parentNode;return{top:e,left:i}}(i),s=b.getBoundingClientRect(),a=m?-1:s.top-l.top,n=(m?s.height:s.bottom-l.top)-1,o=m?-1:s.left-l.left,r=s.width+"px"),!u){let t=function(){for(var t=0,i=e.appendTarget.parentNode;i;)t+=i.scrollTop||0,i=i.parentNode;return t}();a+=t,n+=t}a=Math.floor(a),n=Math.ceil(n),h=((d=e.placeAbove??T-s.bottom0&&void 0!==arguments[0])||arguments[0];var e=this.dropdown.events.callbacks,i=this.listeners.dropdown=this.listeners.dropdown||{position:this.dropdown.position.bind(this,null),onKeyDown:e.onKeyDown.bind(this),onMouseOver:e.onMouseOver.bind(this),onMouseLeave:e.onMouseLeave.bind(this),onClick:e.onClick.bind(this),onScroll:e.onScroll.bind(this)},s=t?"addEventListener":"removeEventListener";"manual"!=this.settings.dropdown.position&&(document[s]("scroll",i.position,!0),window[s]("resize",i.position),window[s]("keydown",i.onKeyDown)),this.DOM.dropdown[s]("mouseover",i.onMouseOver),this.DOM.dropdown[s]("mouseleave",i.onMouseLeave),this.DOM.dropdown[s]("mousedown",i.onClick),this.DOM.dropdown.content[s]("scroll",i.onScroll)},callbacks:{onKeyDown(t){if(this.state.hasFocus&&!this.state.composing){var e=this.settings,i=this.DOM.dropdown.querySelector(e.classNames.dropdownItemActiveSelector),s=this.dropdown.getSuggestionDataByNode(i),a="mix"==e.mode;e.hooks.beforeKeyDown(t,{tagify:this}).then((n=>{switch(t.key){case"ArrowDown":case"ArrowUp":case"Down":case"Up":t.preventDefault();var o=this.dropdown.getAllSuggestionsRefs(),r="ArrowUp"==t.key||"Up"==t.key;i&&(i=this.dropdown.getNextOrPrevOption(i,!r)),i&&i.matches(e.classNames.dropdownItemSelector)||(i=o[r?o.length-1:0]),this.dropdown.highlightOption(i,!0);break;case"Escape":case"Esc":this.dropdown.hide();break;case"ArrowRight":if(this.state.actions.ArrowLeft)return;case"Tab":{let n=!e.autoComplete.rightKey||!e.autoComplete.tabKey;if(!a&&i&&n&&!this.state.editing){t.preventDefault();var l=this.dropdown.getMappedValue(s);return this.input.autocomplete.set.call(this,l),!1}return!0}case"Enter":t.preventDefault(),e.hooks.suggestionClick(t,{tagify:this,tagData:s,suggestionElm:i}).then((()=>{if(i)return this.dropdown.selectOption(i),i=this.dropdown.getNextOrPrevOption(i,!r),void this.dropdown.highlightOption(i);this.dropdown.hide(),a||this.addTags(this.state.inputText.trim(),!0)})).catch((t=>t));break;case"Backspace":{if(a||this.state.editing.scope)return;const t=this.input.raw.call(this);""!=t&&8203!=t.charCodeAt(0)||(!0===e.backspace?this.removeTags():"edit"==e.backspace&&setTimeout(this.editTag.bind(this),0))}}}))}},onMouseOver(t){var e=t.target.closest(this.settings.classNames.dropdownItemSelector);this.dropdown.highlightOption(e)},onMouseLeave(t){this.dropdown.highlightOption()},onClick(t){if(0==t.button&&t.target!=this.DOM.dropdown&&t.target!=this.DOM.dropdown.content){var e=t.target.closest(this.settings.classNames.dropdownItemSelector),i=this.dropdown.getSuggestionDataByNode(e);this.state.actions.selectOption=!0,setTimeout((()=>this.state.actions.selectOption=!1),50),this.settings.hooks.suggestionClick(t,{tagify:this,tagData:i,suggestionElm:e}).then((()=>{e?this.dropdown.selectOption(e,t):this.dropdown.hide()})).catch((t=>console.warn(t)))}},onScroll(t){var e=t.target,i=e.scrollTop/(e.scrollHeight-e.parentNode.clientHeight)*100;this.trigger("dropdown:scroll",{percentage:Math.round(i)})}}},getSuggestionDataByNode(t){var e=t&&t.getAttribute("value");return this.suggestedListItems.find((t=>t.value==e))||null},getNextOrPrevOption(t){let e=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];var i=this.dropdown.getAllSuggestionsRefs(),s=i.findIndex((e=>e===t));return e?i[s+1]:i[s-1]},highlightOption(t,e){var i,s=this.settings.classNames.dropdownItemActive;if(this.state.ddItemElm&&(this.state.ddItemElm.classList.remove(s),this.state.ddItemElm.removeAttribute("aria-selected")),!t)return this.state.ddItemData=null,this.state.ddItemElm=null,void this.input.autocomplete.suggest.call(this);i=this.dropdown.getSuggestionDataByNode(t),this.state.ddItemData=i,this.state.ddItemElm=t,t.classList.add(s),t.setAttribute("aria-selected",!0),e&&(t.parentNode.scrollTop=t.clientHeight+t.offsetTop-t.parentNode.clientHeight),this.settings.autoComplete&&(this.input.autocomplete.suggest.call(this,i),this.dropdown.position())},selectOption(t,e){var i=this.settings,s=i.dropdown,a=s.clearOnSelect,n=s.closeOnSelect;if(!t)return this.addTags(this.state.inputText,!0),void(n&&this.dropdown.hide());e=e||{};var o=t.getAttribute("value"),r="noMatch"==o,l=this.suggestedListItems.find((t=>(t.value??t)==o));if(this.trigger("dropdown:select",{data:l,elm:t,event:e}),o&&(l||r)){if(this.state.editing){let t=this.normalizeTags([l])[0];l=i.transformTag.call(this,t)||t,this.onEditTagDone(null,g({__isValid:!0},l))}else this["mix"==i.mode?"addMixTags":"addTags"]([l||this.input.raw.call(this)],a);this.DOM.input.parentNode&&(setTimeout((()=>{this.DOM.input.focus(),this.toggleFocusClass(!0)})),n&&setTimeout(this.dropdown.hide.bind(this)),t.addEventListener("transitionend",(()=>{this.dropdown.fillHeaderFooter(),setTimeout((()=>t.remove()),100)}),{once:!0}),t.classList.add(this.settings.classNames.dropdownItemHidden))}else n&&setTimeout(this.dropdown.hide.bind(this))},selectAll(t){this.suggestedListItems.length=0,this.dropdown.hide(),this.dropdown.filterListItems("");var e=this.dropdown.filterListItems("");return t||(e=this.state.dropdown.suggestions),this.addTags(e,!0),this},filterListItems(t,e){var i,s,a,n,o,r=this.settings,l=r.dropdown,d=(e=e||{},[]),g=[],p=r.whitelist,u=l.maxItems>=0?l.maxItems:1/0,m=l.searchKeys,v=0;if(!(t="select"==r.mode&&this.value.length&&this.value[0][r.tagTextProp]==t?"":t)||!m.length)return d=l.includeSelectedTags?p:p.filter((t=>!this.isTagDuplicate(h(t)?t.value:t))),this.state.dropdown.suggestions=d,d.slice(0,u);function f(t,e){return e.toLowerCase().split(" ").every((e=>t.includes(e.toLowerCase())))}for(o=l.caseSensitive?""+t:(""+t).toLowerCase();vm.includes(t)))?m:["value"];l.fuzzySearch&&!e.exact?(a=u.reduce(((t,e)=>t+" "+(i[e]||"")),"").toLowerCase().trim(),l.accentedSearch&&(a=c(a),o=c(o)),t=0==a.indexOf(o),r=a===o,s=f(a,o)):(t=!0,s=u.some((t=>{var s=""+(i[t]||"");return l.accentedSearch&&(s=c(s),o=c(o)),l.caseSensitive||(s=s.toLowerCase()),r=s===o,e.exact?s===o:0==s.indexOf(o)}))),n=!l.includeSelectedTags&&this.isTagDuplicate(h(i)?i.value:i),s&&!n&&(r&&t?g.push(i):"startsWith"==l.sortby&&t?d.unshift(i):d.push(i))}return this.state.dropdown.suggestions=g.concat(d),"function"==typeof l.sortby?l.sortby(g.concat(d),o):g.concat(d).slice(0,u)},getMappedValue(t){var e=this.settings.dropdown.mapValueTo;return e?"function"==typeof e?e(t):t[e]||t.value:t.value},createListHTML(t){return g([],t).map(((t,i)=>{"string"!=typeof t&&"number"!=typeof t||(t={value:t});var s=this.dropdown.getMappedValue(t);return s="string"==typeof s&&this.settings.dropdown.escapeHTML?d(s):s,this.settings.templates.dropdownItem.apply(this,[e(e({},t),{},{mappedValue:s}),this])})).join("")}};const D="@yaireo/tagify/";var M,I={empty:"empty",exceed:"number of tags exceeded",pattern:"pattern mismatch",duplicate:"already exists",notAllowed:"not allowed"},N={wrapper:(t,e)=>`\n \n ​\n `,tag(t,e){let i=e.settings;return`\n \n
\n ${t[i.tagTextProp]||t.value}\n
\n
`},dropdown(t){var e=t.dropdown;return`
\n
\n
`},dropdownContent(t){var e=this.settings.templates,i=this.state.dropdown.suggestions;return`\n ${e.dropdownHeader.call(this,i)}\n ${t}\n ${e.dropdownFooter.call(this,i)}\n `},dropdownItem(t){return`
${t.mappedValue||t.value}
`},dropdownHeader(t){return`
`},dropdownFooter(t){var e=t.length-this.settings.dropdown.maxItems;return e>0?`
\n ${e} more items. Refine your search.\n
`:""},dropdownItemNoMatch:null},E={customBinding(){this.customEventsList.forEach((t=>{this.on(t,this.settings.callbacks[t])}))},binding(){let t=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];var e,i=this.settings,s=this.events.callbacks,a=t?"addEventListener":"removeEventListener";if(!this.state.mainEvents||!t){for(var n in this.state.mainEvents=t,t&&!this.listeners.main&&(this.events.bindGlobal.call(this),this.settings.isJQueryPlugin&&jQuery(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this))),e=this.listeners.main=this.listeners.main||{focus:["input",s.onFocusBlur.bind(this)],keydown:["input",s.onKeydown.bind(this)],click:["scope",s.onClickScope.bind(this)],dblclick:"select"!=i.mode&&["scope",s.onDoubleClickScope.bind(this)],paste:["input",s.onPaste.bind(this)],drop:["input",s.onDrop.bind(this)],compositionstart:["input",s.onCompositionStart.bind(this)],compositionend:["input",s.onCompositionEnd.bind(this)]})e[n]&&this.DOM[e[n][0]][a](n,e[n][1]);clearInterval(this.listeners.main.originalInputValueObserverInterval),this.listeners.main.originalInputValueObserverInterval=setInterval(s.observeOriginalInputValue.bind(this),500);var o=this.listeners.main.inputMutationObserver||new MutationObserver(s.onInputDOMChange.bind(this));o.disconnect(),"mix"==i.mode&&o.observe(this.DOM.input,{childList:!0})}},bindGlobal(t){var e,i=this.events.callbacks,s=t?"removeEventListener":"addEventListener";if(this.listeners&&(t||!this.listeners.global))for(e of(this.listeners.global=this.listeners.global||[{type:this.isIE?"keydown":"input",target:this.DOM.input,cb:i[this.isIE?"onInputIE":"onInput"].bind(this)},{type:"keydown",target:window,cb:i.onWindowKeyDown.bind(this)},{type:"blur",target:this.DOM.input,cb:i.onFocusBlur.bind(this)},{type:"click",target:document,cb:i.onClickAnywhere.bind(this),useCapture:!0}],this.listeners.global))e.target[s](e.type,e.cb,!!e.useCapture)},unbindGlobal(){this.events.bindGlobal.call(this,!0)},callbacks:{onFocusBlur(t){var e=this.settings,i=t.target?this.trim(t.target.textContent):"",s=this.value?.[0]?.[e.tagTextProp],a=t.type,n=e.dropdown.enabled>=0,o={relatedTarget:t.relatedTarget},r=this.state.actions.selectOption&&(n||!e.dropdown.closeOnSelect),l=this.state.actions.addNew&&n;if("blur"==a){if(t.relatedTarget===this.DOM.scope)return this.dropdown.hide(),void this.DOM.input.focus();this.postUpdate(),e.onChangeAfterBlur&&this.triggerChangeEvent()}if(!r&&!l)if(this.state.hasFocus="focus"==a&&+new Date,this.toggleFocusClass(this.state.hasFocus),"mix"!=e.mode){if("focus"==a)return this.trigger("focus",o),void(0!==e.dropdown.enabled&&e.userInput||this.state.dropdown.visible||this.dropdown.show(this.value.length?"":void 0));"blur"==a&&(this.trigger("blur",o),this.loading(!1),"select"==e.mode&&s===i&&(i=""),i&&!this.state.actions.selectOption&&e.addTagOnBlur&&e.addTagOn.includes("blur")&&this.addTags(i,!0)),this.DOM.input.removeAttribute("style"),this.dropdown.hide()}else"focus"==a?this.trigger("focus",o):"blur"==t.type&&(this.trigger("blur",o),this.loading(!1),this.dropdown.hide(),this.state.dropdown.visible=void 0,this.setStateSelection())},onCompositionStart(t){this.state.composing=!0},onCompositionEnd(t){this.state.composing=!1},onWindowKeyDown(t){var e,i=document.activeElement,s=v.call(this,i)&&this.DOM.scope.contains(document.activeElement),a=s&&i.hasAttribute("readonly");if(s&&!a)switch(e=i.nextElementSibling,t.key){case"Backspace":this.settings.readonly||(this.removeTags(i),(e||this.DOM.input).focus());break;case"Enter":setTimeout(this.editTag.bind(this),0,i)}},onKeydown(t){var e=this.settings;if(!this.state.composing&&e.userInput){"select"==e.mode&&e.enforceWhitelist&&this.value.length&&"Tab"!=t.key&&t.preventDefault();var i=this.trim(t.target.textContent);this.trigger("keydown",{event:t}),e.hooks.beforeKeyDown(t,{tagify:this}).then((s=>{if("mix"==e.mode){switch(t.key){case"Left":case"ArrowLeft":this.state.actions.ArrowLeft=!0;break;case"Delete":case"Backspace":if(this.state.editing)return;var a=document.getSelection(),n="Delete"==t.key&&a.anchorOffset==(a.anchorNode.length||0),r=a.anchorNode.previousSibling,d=1==a.anchorNode.nodeType||!a.anchorOffset&&r&&1==r.nodeType&&a.anchorNode.previousSibling;o(this.DOM.input.innerHTML);var h,g,p,c=this.getTagElms(),m=1===a.anchorNode.length&&a.anchorNode.nodeValue==String.fromCharCode(8203);if("edit"==e.backspace&&d)return h=1==a.anchorNode.nodeType?null:a.anchorNode.previousElementSibling,setTimeout(this.editTag.bind(this),0,h),void t.preventDefault();if(u()&&d instanceof Element)return p=l(d),d.hasAttribute("readonly")||d.remove(),this.DOM.input.focus(),void setTimeout((()=>{w(p),this.DOM.input.click()}));if("BR"==a.anchorNode.nodeName)return;if((n||d)&&1==a.anchorNode.nodeType?g=0==a.anchorOffset?n?c[0]:null:c[Math.min(c.length,a.anchorOffset)-1]:n?g=a.anchorNode.nextElementSibling:d instanceof Element&&(g=d),3==a.anchorNode.nodeType&&!a.anchorNode.nodeValue&&a.anchorNode.previousElementSibling&&t.preventDefault(),(d||n)&&!e.backspace)return void t.preventDefault();if("Range"!=a.type&&!a.anchorOffset&&a.anchorNode==this.DOM.input&&"Delete"!=t.key)return void t.preventDefault();if("Range"!=a.type&&g&&g.hasAttribute("readonly"))return void w(l(g));"Delete"==t.key&&m&&T(a.anchorNode.nextSibling)&&this.removeTags(a.anchorNode.nextSibling),clearTimeout(M),M=setTimeout((()=>{var t=document.getSelection();o(this.DOM.input.innerHTML),!n&&t.anchorNode.previousSibling,this.value=[].map.call(c,((t,e)=>{var i=T(t);if(t.parentNode||i.readonly)return i;this.trigger("remove",{tag:t,index:e,data:i})})).filter((t=>t))}),20)}return!0}var v="manual"==e.dropdown.position;switch(t.key){case"Backspace":"select"==e.mode&&e.enforceWhitelist&&this.value.length?this.removeTags():this.state.dropdown.visible&&"manual"!=e.dropdown.position||""!=t.target.textContent&&8203!=i.charCodeAt(0)||(!0===e.backspace?this.removeTags():"edit"==e.backspace&&setTimeout(this.editTag.bind(this),0));break;case"Esc":case"Escape":if(this.state.dropdown.visible)return;t.target.blur();break;case"Down":case"ArrowDown":this.state.dropdown.visible||this.dropdown.show();break;case"ArrowRight":{let t=this.state.inputSuggestion||this.state.ddItemData;if(t&&e.autoComplete.rightKey)return void this.addTags([t],!0);break}case"Tab":{let s="select"==e.mode;if(!i||s)return!0;t.preventDefault()}case"Enter":if(this.state.dropdown.visible&&!v)return;t.preventDefault(),setTimeout((()=>{this.state.dropdown.visible&&!v||this.state.actions.selectOption||!e.addTagOn.includes(t.key.toLowerCase())||this.addTags(i,!0)}))}})).catch((t=>t))}},onInput(t){this.postUpdate();var e=this.settings;if("mix"==e.mode)return this.events.callbacks.onMixTagsInput.call(this,t);var i=this.input.normalize.call(this,void 0,{trim:!1}),s=i.length>=e.dropdown.enabled,a={value:i,inputElm:this.DOM.input},n=this.validateTag({value:i});"select"==e.mode&&this.toggleScopeValidation(n),a.isValid=n,this.state.inputText!=i&&(this.input.set.call(this,i,!1),-1!=i.search(e.delimiters)?this.addTags(i)&&this.input.set.call(this):e.dropdown.enabled>=0&&this.dropdown[s?"show":"hide"](i),this.trigger("input",a))},onMixTagsInput(t){var e,i,s,a,n,o,r,l,d=this.settings,h=this.value.length,p=this.getTagElms(),c=document.createDocumentFragment(),m=window.getSelection().getRangeAt(0),v=[].map.call(p,(t=>T(t).value));if("deleteContentBackward"==t.inputType&&u()&&this.events.callbacks.onKeydown.call(this,{target:t.target,key:"Backspace"}),b(this.getTagElms()),this.value.slice().forEach((t=>{t.readonly&&!v.includes(t.value)&&c.appendChild(this.createTagElem(t))})),c.childNodes.length&&(m.insertNode(c),this.setRangeAtStartEnd(!1,c.lastChild)),p.length!=h)return this.value=[].map.call(this.getTagElms(),(t=>T(t))),void this.update({withoutChangeEvent:!0});if(this.hasMaxTags())return!0;if(window.getSelection&&(o=window.getSelection()).rangeCount>0&&3==o.anchorNode.nodeType){if((m=o.getRangeAt(0).cloneRange()).collapse(!0),m.setStart(o.focusNode,0),s=(e=m.toString().slice(0,m.endOffset)).split(d.pattern).length-1,(i=e.match(d.pattern))&&(a=e.slice(e.lastIndexOf(i[i.length-1]))),a){if(this.state.actions.ArrowLeft=!1,this.state.tag={prefix:a.match(d.pattern)[0],value:a.replace(d.pattern,"")},this.state.tag.baseOffset=o.baseOffset-this.state.tag.value.length,l=this.state.tag.value.match(d.delimiters))return this.state.tag.value=this.state.tag.value.replace(d.delimiters,""),this.state.tag.delimiters=l[0],this.addTags(this.state.tag.value,d.dropdown.clearOnSelect),void this.dropdown.hide();n=this.state.tag.value.length>=d.dropdown.enabled;try{r=(r=this.state.flaggedTags[this.state.tag.baseOffset]).prefix==this.state.tag.prefix&&r.value[0]==this.state.tag.value[0],this.state.flaggedTags[this.state.tag.baseOffset]&&!this.state.tag.value&&delete this.state.flaggedTags[this.state.tag.baseOffset]}catch(t){}(r||s{this.update({withoutChangeEvent:!0}),this.trigger("input",g({},this.state.tag,{textContent:this.DOM.input.textContent})),this.state.tag&&this.dropdown[n?"show":"hide"](this.state.tag.value)}),10)},onInputIE(t){var e=this;setTimeout((function(){e.events.callbacks.onInput.call(e,t)}))},observeOriginalInputValue(){this.DOM.originalInput.parentNode||this.destroy(),this.DOM.originalInput.value!=this.DOM.originalInput.tagifyValue&&this.loadOriginalValues()},onClickAnywhere(t){t.target==this.DOM.scope||this.DOM.scope.contains(t.target)||(this.toggleFocusClass(!1),this.state.hasFocus=!1,!this.settings.userInput&&this.dropdown.hide())},onClickScope(t){var i=this.settings,s=t.target.closest("."+i.classNames.tag),a=t.target===this.DOM.scope,n=+new Date-this.state.hasFocus;if(a&&"select"!=i.mode)this.DOM.input.focus();else{if(!t.target.classList.contains(i.classNames.tagX))return s&&!this.state.editing?(this.trigger("click",{tag:s,index:this.getNodeIndex(s),data:T(s),event:t}),void(1!==i.editTags&&1!==i.editTags.clicks&&"select"!=i.mode||this.events.callbacks.onDoubleClickScope.call(this,t))):void(t.target==this.DOM.input&&("mix"==i.mode&&this.fixFirefoxLastTagNoCaret(),n>500)?this.state.dropdown.visible?this.dropdown.hide():0===i.dropdown.enabled&&"mix"!=i.mode&&this.dropdown.show(this.value.length?"":void 0):"select"!=i.mode||0!==i.dropdown.enabled||this.state.dropdown.visible||(this.events.callbacks.onDoubleClickScope.call(this,e(e({},t),{},{target:this.getTagElms()[0]})),!i.userInput&&this.dropdown.show()));this.removeTags(t.target.parentNode)}},onPaste(t){t.preventDefault();var e,i,s,a=this.settings;if("select"==a.mode&&a.enforceWhitelist||!a.userInput)return!1;a.readonly||(i=t.clipboardData||window.clipboardData,s=i.getData("Text"),a.hooks.beforePaste(t,{tagify:this,pastedText:s,clipboardData:i}).then((a=>{void 0===a&&(a=s),a&&(this.injectAtCaret(a,window.getSelection().getRangeAt(0)),"mix"==this.settings.mode?this.events.callbacks.onMixTagsInput.call(this,t):this.settings.pasteAsTags?e=this.addTags(this.state.inputText+a,!0):(this.state.inputText=a,this.dropdown.show(a))),this.trigger("paste",{event:t,pastedText:s,clipboardData:i,tagsElems:e})})).catch((t=>t)))},onDrop(t){t.preventDefault()},onEditTagInput(t,e){var i=t.closest("."+this.settings.classNames.tag),s=this.getNodeIndex(i),a=T(i),n=this.input.normalize.call(this,t),o={[this.settings.tagTextProp]:n,__tagId:a.__tagId},r=this.validateTag(o);this.editTagChangeDetected(g(a,o))||!0!==t.originalIsValid||(r=!0),i.classList.toggle(this.settings.classNames.tagInvalid,!0!==r),a.__isValid=r,i.title=!0===r?a.title||a.value:r,n.length>=this.settings.dropdown.enabled&&(this.state.editing&&(this.state.editing.value=n),this.dropdown.show(n)),this.trigger("edit:input",{tag:i,index:s,data:g({},this.value[s],{newValue:n}),event:e})},onEditTagPaste(t,e){var i=(e.clipboardData||window.clipboardData).getData("Text");e.preventDefault();var s=f(i);this.setRangeAtStartEnd(!1,s)},onEditTagClick(t,e){this.events.callbacks.onClickScope.call(this,e)},onEditTagFocus(t){this.state.editing={scope:t,input:t.querySelector("[contenteditable]")}},onEditTagBlur(t,e){if(v.call(this,e.relatedTarget)&&e.relatedTarget.contains(e.target))this.dropdown.hide();else if(this.state.editing&&(this.state.hasFocus||this.toggleFocusClass(),this.DOM.scope.contains(t))){var i,s,a=this.settings,n=t.closest("."+a.classNames.tag),o=T(n),r=this.input.normalize.call(this,t),l={[a.tagTextProp]:r,__tagId:o.__tagId},d=o.__originalData,h=this.editTagChangeDetected(g(o,l)),p=this.validateTag(l);if(r)if(h){if(i=this.hasMaxTags(),s=g({},d,{[a.tagTextProp]:this.trim(r),__isValid:p}),a.transformTag.call(this,s,d),!0!==(p=(!i||!0===d.__isValid)&&this.validateTag(s))){if(this.trigger("invalid",{data:s,tag:n,message:p}),a.editTags.keepInvalid)return;a.keepInvalidTags?s.__isValid=p:s=d}else a.keepInvalidTags&&(delete s.title,delete s["aria-invalid"],delete s.class);this.onEditTagDone(n,s)}else this.onEditTagDone(n,d);else this.onEditTagDone(n)}},onEditTagkeydown(t,e){if(!this.state.composing)switch(this.trigger("edit:keydown",{event:t}),t.key){case"Esc":case"Escape":this.state.editing=!1,e.__tagifyTagData.__originalData.value?e.parentNode.replaceChild(e.__tagifyTagData.__originalHTML,e):e.remove();break;case"Enter":case"Tab":t.preventDefault(),setTimeout((()=>t.target.blur()),0)}},onDoubleClickScope(t){var e,i,s=t.target.closest("."+this.settings.classNames.tag),a=T(s),n=this.settings;s&&!1!==a.editable&&(e=s.classList.contains(this.settings.classNames.tagEditing),i=s.hasAttribute("readonly"),n.readonly||e||i||!this.settings.editTags||!n.userInput||this.editTag(s),this.toggleFocusClass(!0),"select"!=n.mode&&this.trigger("dblclick",{tag:s,index:this.getNodeIndex(s),data:T(s)}))},onInputDOMChange(t){t.forEach((t=>{t.addedNodes.forEach((t=>{if("

"==t.outerHTML)t.replaceWith(document.createElement("br"));else if(1==t.nodeType&&t.querySelector(this.settings.classNames.tagSelector)){let e=document.createTextNode("");3==t.childNodes[0].nodeType&&"BR"!=t.previousSibling.nodeName&&(e=document.createTextNode("\n")),t.replaceWith(e,...[...t.childNodes].slice(0,-1)),w(e)}else if(v.call(this,t))if(3!=t.previousSibling?.nodeType||t.previousSibling.textContent||t.previousSibling.remove(),t.previousSibling&&"BR"==t.previousSibling.nodeName){t.previousSibling.replaceWith("\n​");let e=t.nextSibling,i="";for(;e;)i+=e.textContent,e=e.nextSibling;i.trim()&&w(t.previousSibling)}else t.previousSibling&&!T(t.previousSibling)||t.before("​")})),t.removedNodes.forEach((t=>{t&&"BR"==t.nodeName&&v.call(this,e)&&(this.removeTags(e),this.fixFirefoxLastTagNoCaret())}))}));var e=this.DOM.input.lastChild;e&&""==e.nodeValue&&e.remove(),e&&"BR"==e.nodeName||this.DOM.input.appendChild(document.createElement("br"))}}};function S(t,e){if(!t){console.warn("Tagify:","input element not found",t);const e=new Proxy(this,{get:()=>()=>e});return e}if(t.__tagify)return console.warn("Tagify: ","input element is already Tagified - Same instance is returned.",t),t.__tagify;var i;g(this,function(t){var e=document.createTextNode("");function i(t,i,s){s&&i.split(/\s+/g).forEach((i=>e[t+"EventListener"].call(e,i,s)))}return{off(t,e){return i("remove",t,e),this},on(t,e){return e&&"function"==typeof e&&i("add",t,e),this},trigger(i,s,a){var n;if(a=a||{cloneData:!0},i)if(t.settings.isJQueryPlugin)"remove"==i&&(i="removeTag"),jQuery(t.DOM.originalInput).triggerHandler(i,[s]);else{try{var o="object"==typeof s?s:{value:s};if((o=a.cloneData?g({},o):o).tagify=this,s.event&&(o.event=this.cloneEvent(s.event)),s instanceof Object)for(var r in s)s[r]instanceof HTMLElement&&(o[r]=s[r]);n=new CustomEvent(i,{detail:o})}catch(t){console.warn(t)}e.dispatchEvent(n)}}}}(this)),this.isFirefox=/firefox|fxios/i.test(navigator.userAgent)&&!/seamonkey/i.test(navigator.userAgent),this.isIE=window.document.documentMode,e=e||{},this.getPersistedData=(i=e.id,t=>{let e,s="/"+t;if(1==localStorage.getItem(D+i+"/v",1))try{e=JSON.parse(localStorage[D+i+s])}catch(t){}return e}),this.setPersistedData=(t=>t?(localStorage.setItem(D+t+"/v",1),(e,i)=>{let s="/"+i,a=JSON.stringify(e);e&&i&&(localStorage.setItem(D+t+s,a),dispatchEvent(new Event("storage")))}):()=>{})(e.id),this.clearPersistedData=(t=>e=>{const i=D+"/"+t+"/";if(e)localStorage.removeItem(i+e);else for(let t in localStorage)t.includes(i)&&localStorage.removeItem(t)})(e.id),this.applySettings(t,e),this.state={inputText:"",editing:!1,composing:!1,actions:{},mixMode:{},dropdown:{},flaggedTags:{}},this.value=[],this.listeners={},this.DOM={},this.build(t),x.call(this),this.getCSSVars(),this.loadOriginalValues(),this.events.customBinding.call(this),this.events.binding.call(this),t.autofocus&&this.DOM.input.focus(),t.__tagify=this}return S.prototype={_dropdown:O,placeCaretAfterNode:w,getSetTagData:T,helpers:{sameStr:s,removeCollectionProp:a,omit:n,isObject:h,parseHTML:r,escapeHTML:d,extend:g,concatWithoutDups:p,getUID:m,isNodeTag:v},customEventsList:["change","add","remove","invalid","input","paste","click","keydown","focus","blur","edit:input","edit:beforeUpdate","edit:updated","edit:start","edit:keydown","dropdown:show","dropdown:hide","dropdown:select","dropdown:updated","dropdown:noMatch","dropdown:scroll"],dataProps:["__isValid","__removed","__originalData","__originalHTML","__tagId"],trim(t){return this.settings.trim&&t&&"string"==typeof t?t.trim():t},parseHTML:r,templates:N,parseTemplate(t,e){return r((t=this.settings.templates[t]||t).apply(this,e))},set whitelist(t){const e=t&&Array.isArray(t);this.settings.whitelist=e?t:[],this.setPersistedData(e?t:[],"whitelist")},get whitelist(){return this.settings.whitelist},generateClassSelectors(t){for(let e in t){let i=e;Object.defineProperty(t,i+"Selector",{get(){return"."+this[i].split(" ")[0]}})}},applySettings(t,i){y.templates=this.templates;var s=g({},y,"mix"==i.mode?{dropdown:{position:"text"}}:{}),a=this.settings=g({},s,i);if(a.disabled=t.hasAttribute("disabled"),a.readonly=a.readonly||t.hasAttribute("readonly"),a.placeholder=d(t.getAttribute("placeholder")||a.placeholder||""),a.required=t.hasAttribute("required"),this.generateClassSelectors(a.classNames),void 0===a.dropdown.includeSelectedTags&&(a.dropdown.includeSelectedTags=a.duplicates),this.isIE&&(a.autoComplete=!1),["whitelist","blacklist"].forEach((e=>{var i=t.getAttribute("data-"+e);i&&(i=i.split(a.delimiters))instanceof Array&&(a[e]=i)})),"autoComplete"in i&&!h(i.autoComplete)&&(a.autoComplete=y.autoComplete,a.autoComplete.enabled=i.autoComplete),"mix"==a.mode&&(a.pattern=a.pattern||/@/,a.autoComplete.rightKey=!0,a.delimiters=i.delimiters||null,a.tagTextProp&&!a.dropdown.searchKeys.includes(a.tagTextProp)&&a.dropdown.searchKeys.push(a.tagTextProp)),t.pattern)try{a.pattern=new RegExp(t.pattern)}catch(t){}if(a.delimiters){a._delimiters=a.delimiters;try{a.delimiters=new RegExp(this.settings.delimiters,"g")}catch(t){}}a.disabled&&(a.userInput=!1),this.TEXTS=e(e({},I),a.texts||{}),("select"!=a.mode||i.dropdown?.enabled)&&a.userInput||(a.dropdown.enabled=0),a.dropdown.appendTarget=i.dropdown?.appendTarget||document.body;let n=this.getPersistedData("whitelist");Array.isArray(n)&&(this.whitelist=Array.isArray(a.whitelist)?p(a.whitelist,n):n)},getAttributes(t){var e,i=this.getCustomAttributes(t),s="";for(e in i)s+=" "+e+(void 0!==t[e]?`="${i[e]}"`:"");return s},getCustomAttributes(t){if(!h(t))return"";var e,i={};for(e in t)"__"!=e.slice(0,2)&&"class"!=e&&t.hasOwnProperty(e)&&void 0!==t[e]&&(i[e]=d(t[e]));return i},setStateSelection(){var t=window.getSelection(),e={anchorOffset:t.anchorOffset,anchorNode:t.anchorNode,range:t.getRangeAt&&t.rangeCount&&t.getRangeAt(0)};return this.state.selection=e,e},getCSSVars(){var t=getComputedStyle(this.DOM.scope,null);this.CSSVars={tagHideTransition:(t=>{let e=t.value;return"s"==t.unit?1e3*e:e})(function(t){if(!t)return{};var e=(t=t.trim().split(" ")[0]).split(/\d+/g).filter((t=>t)).pop().trim();return{value:+t.split(e).filter((t=>t))[0].trim(),unit:e}}(t.getPropertyValue("--tag-hide-transition")))}},build(t){var e=this.DOM,i=t.closest("label");this.settings.mixMode.integrated?(e.originalInput=null,e.scope=t,e.input=t):(e.originalInput=t,e.originalInput_tabIndex=t.tabIndex,e.scope=this.parseTemplate("wrapper",[t,this.settings]),e.input=e.scope.querySelector(this.settings.classNames.inputSelector),t.parentNode.insertBefore(e.scope,t),t.tabIndex=-1),i&&i.setAttribute("for","")},destroy(){this.events.unbindGlobal.call(this),this.DOM.scope.parentNode.removeChild(this.DOM.scope),this.DOM.originalInput.tabIndex=this.DOM.originalInput_tabIndex,delete this.DOM.originalInput.__tagify,this.dropdown.hide(!0),clearTimeout(this.dropdownHide__bindEventsTimeout),clearInterval(this.listeners.main.originalInputValueObserverInterval)},loadOriginalValues(t){var e,i=this.settings;if(this.state.blockChangeEvent=!0,void 0===t){const e=this.getPersistedData("value");t=e&&!this.DOM.originalInput.value?e:i.mixMode.integrated?this.DOM.input.textContent:this.DOM.originalInput.value}if(this.removeAllTags(),t)if("mix"==i.mode)this.parseMixTags(t),(e=this.DOM.input.lastChild)&&"BR"==e.tagName||this.DOM.input.insertAdjacentHTML("beforeend","
");else{try{JSON.parse(t)instanceof Array&&(t=JSON.parse(t))}catch(t){}this.addTags(t,!0).forEach((t=>t&&t.classList.add(i.classNames.tagNoAnimation)))}else this.postUpdate();this.state.lastOriginalValueReported=i.mixMode.integrated?"":this.DOM.originalInput.value},cloneEvent(t){var e={};for(var i in t)"path"!=i&&(e[i]=t[i]);return e},loading(t){return this.state.isLoading=t,this.DOM.scope.classList[t?"add":"remove"](this.settings.classNames.scopeLoading),this},tagLoading(t,e){return t&&t.classList[e?"add":"remove"](this.settings.classNames.tagLoading),this},toggleClass(t,e){"string"==typeof t&&this.DOM.scope.classList.toggle(t,e)},toggleScopeValidation(t){var e=!0===t||void 0===t;!this.settings.required&&t&&t===this.TEXTS.empty&&(e=!0),this.toggleClass(this.settings.classNames.tagInvalid,!e),this.DOM.scope.title=e?"":t},toggleFocusClass(t){this.toggleClass(this.settings.classNames.focus,!!t)},triggerChangeEvent:function(){if(!this.settings.mixMode.integrated){var t=this.DOM.originalInput,e=this.state.lastOriginalValueReported!==t.value,i=new CustomEvent("change",{bubbles:!0});e&&(this.state.lastOriginalValueReported=t.value,i.simulated=!0,t._valueTracker&&t._valueTracker.setValue(Math.random()),t.dispatchEvent(i),this.trigger("change",this.state.lastOriginalValueReported),t.value=this.state.lastOriginalValueReported)}},events:E,fixFirefoxLastTagNoCaret(){},setRangeAtStartEnd(t,e){if(e){t="number"==typeof t?t:!!t,e=e.lastChild||e;var i=document.getSelection();if(i.focusNode instanceof Element&&!this.DOM.input.contains(i.focusNode))return!0;try{i.rangeCount>=1&&["Start","End"].forEach((s=>i.getRangeAt(0)["set"+s](e,t||e.length)))}catch(t){console.warn("Tagify: ",t)}}},insertAfterTag(t,e){if(e=e||this.settings.mixMode.insertAfterTag,t&&t.parentNode&&e)return e="string"==typeof e?document.createTextNode(e):e,t.parentNode.insertBefore(e,t.nextSibling),e},editTagChangeDetected(t){var e=t.__originalData;for(var i in e)if(!this.dataProps.includes(i)&&t[i]!=e[i])return!0;return!1},getTagTextNode(t){return t.querySelector(this.settings.classNames.tagTextSelector)},setTagTextNode(t,e){this.getTagTextNode(t).innerHTML=d(e)},editTag(t,e){t=t||this.getLastTag(),e=e||{},this.dropdown.hide();var i=this.settings,s=this.getTagTextNode(t),a=this.getNodeIndex(t),n=T(t),o=this.events.callbacks,r=!0;if(s){if(!(n instanceof Object&&"editable"in n)||n.editable)return n=T(t,{__originalData:g({},n),__originalHTML:t.cloneNode(!0)}),T(n.__originalHTML,n.__originalData),s.setAttribute("contenteditable",!0),t.classList.add(i.classNames.tagEditing),s.addEventListener("click",o.onEditTagClick.bind(this,t)),s.addEventListener("focus",o.onEditTagFocus.bind(this,t)),s.addEventListener("blur",o.onEditTagBlur.bind(this,this.getTagTextNode(t))),s.addEventListener("input",o.onEditTagInput.bind(this,s)),s.addEventListener("paste",o.onEditTagPaste.bind(this,s)),s.addEventListener("keydown",(e=>o.onEditTagkeydown.call(this,e,t))),s.addEventListener("compositionstart",o.onCompositionStart.bind(this)),s.addEventListener("compositionend",o.onCompositionEnd.bind(this)),e.skipValidation||(r=this.editTagToggleValidity(t)),s.originalIsValid=r,this.trigger("edit:start",{tag:t,index:a,data:n,isValid:r}),s.focus(),this.setRangeAtStartEnd(!1,s),0===i.dropdown.enabled&&this.dropdown.show(),this.state.hasFocus=!0,this}else console.warn("Cannot find element in Tag template: .",i.classNames.tagTextSelector)},editTagToggleValidity(t,e){var i;if(e=e||T(t))return(i=!("__isValid"in e)||!0===e.__isValid)||this.removeTagsFromValue(t),this.update(),t.classList.toggle(this.settings.classNames.tagNotAllowed,!i),e.__isValid=i,e.__isValid;console.warn("tag has no data: ",t,e)},onEditTagDone(t,e){e=e||{};var i,s={tag:t=t||this.state.editing.scope,index:this.getNodeIndex(t),previousData:T(t),data:e},a=this.settings;this.trigger("edit:beforeUpdate",s,{cloneData:!1}),this.state.editing=!1,delete e.__originalData,delete e.__originalHTML,t&&((i=e[a.tagTextProp])?i.trim?.():a.tagTextProp in e?void 0:e.value)?(t=this.replaceTag(t,e),this.editTagToggleValidity(t,e),a.a11y.focusableTags?t.focus():w(t)):t&&this.removeTags(t),this.trigger("edit:updated",s),this.dropdown.hide(),this.settings.keepInvalidTags&&this.reCheckInvalidTags()},replaceTag(t,e){e&&""!==e.value&&void 0!==e.value||(e=t.__tagifyTagData),e.__isValid&&1!=e.__isValid&&g(e,this.getInvalidTagAttrs(e,e.__isValid));var i=this.createTagElem(e);return t.parentNode.replaceChild(i,t),this.updateValueByDOMTags(),i},updateValueByDOMTags(){this.value.length=0,[].forEach.call(this.getTagElms(),(t=>{t.classList.contains(this.settings.classNames.tagNotAllowed.split(" ")[0])||this.value.push(T(t))})),this.update()},injectAtCaret(t,e){if(!(e=e||this.state.selection?.range)&&t)return this.appendMixTags(t),this;let i=f(t,e);return this.setRangeAtStartEnd(!1,i),this.updateValueByDOMTags(),this.update(),this},input:{set(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",e=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];var i=this.settings.dropdown.closeOnSelect;this.state.inputText=t,e&&(this.DOM.input.innerHTML=d(""+t)),!t&&i&&this.dropdown.hide.bind(this),this.input.autocomplete.suggest.call(this),this.input.validate.call(this)},raw(){return this.DOM.input.textContent},validate(){var t=!this.state.inputText||!0===this.validateTag({value:this.state.inputText});return this.DOM.input.classList.toggle(this.settings.classNames.inputInvalid,!t),t},normalize(t,e){var i=t||this.DOM.input,s=[];i.childNodes.forEach((t=>3==t.nodeType&&s.push(t.nodeValue))),s=s.join("\n");try{s=s.replace(/(?:\r\n|\r|\n)/g,this.settings.delimiters.source.charAt(0))}catch(t){}return s=s.replace(/\s/g," "),e?.trim?this.trim(s):s},autocomplete:{suggest(t){if(this.settings.autoComplete.enabled){"object"!=typeof(t=t||{value:""})&&(t={value:t});var e=this.dropdown.getMappedValue(t);if("number"!=typeof e){var i=this.state.inputText.toLowerCase(),s=e.substr(0,this.state.inputText.length).toLowerCase(),a=e.substring(this.state.inputText.length);e&&this.state.inputText&&s==i?(this.DOM.input.setAttribute("data-suggest",a),this.state.inputSuggestion=t):(this.DOM.input.removeAttribute("data-suggest"),delete this.state.inputSuggestion)}}},set(t){var e=this.DOM.input.getAttribute("data-suggest"),i=t||(e?this.state.inputText+e:null);return!!i&&("mix"==this.settings.mode?this.replaceTextWithNode(document.createTextNode(this.state.tag.prefix+i)):(this.input.set.call(this,i),this.setRangeAtStartEnd(!1,this.DOM.input)),this.input.autocomplete.suggest.call(this),this.dropdown.hide(),!0)}}},getTagIdx(t){return this.value.findIndex((e=>e.__tagId==(t||{}).__tagId))},getNodeIndex(t){var e=0;if(t)for(;t=t.previousElementSibling;)e++;return e},getTagElms(){for(var t=arguments.length,e=new Array(t),i=0;i{a.__tagifyTagData&&s(this.trim(a.__tagifyTagData.value),t,i)&&e.push(n)})),e},getTagElmByValue(t){var e=this.getTagIndexByValue(t)[0];return this.getTagElms()[e]},flashTag(t){t&&(t.classList.add(this.settings.classNames.tagFlash),setTimeout((()=>{t.classList.remove(this.settings.classNames.tagFlash)}),100))},isTagBlacklisted(t){return t=this.trim(t.toLowerCase()),this.settings.blacklist.filter((e=>(""+e).toLowerCase()==t)).length},isTagWhitelisted(t){return!!this.getWhitelistItem(t)},getWhitelistItem(t,e,i){e=e||"value";var a,n=this.settings;return(i=i||n.whitelist).some((i=>{var o="string"==typeof i?i:i[e]||i.value;if(s(o,t,n.dropdown.caseSensitive,n.trim))return a="string"==typeof i?{value:i}:i,!0})),a||"value"!=e||"value"==n.tagTextProp||(a=this.getWhitelistItem(t,n.tagTextProp,i)),a},validateTag(t){var e=this.settings,i="value"in t?"value":e.tagTextProp,s=this.trim(t[i]+"");return(t[i]+"").trim()?"mix"!=e.mode&&e.pattern&&e.pattern instanceof RegExp&&!e.pattern.test(s)?this.TEXTS.pattern:!e.duplicates&&this.isTagDuplicate(s,e.dropdown.caseSensitive,t.__tagId)?this.TEXTS.duplicate:this.isTagBlacklisted(s)||e.enforceWhitelist&&!this.isTagWhitelisted(s)?this.TEXTS.notAllowed:!e.validate||e.validate(t):this.TEXTS.empty},getInvalidTagAttrs(t,e){return{"aria-invalid":!0,class:`${t.class||""} ${this.settings.classNames.tagNotAllowed}`.trim(),title:e}},hasMaxTags(){return this.value.length>=this.settings.maxTags&&this.TEXTS.exceed},setReadonly(t,e){var i=this.settings;document.activeElement.blur(),i[e||"readonly"]=t,this.DOM.scope[(t?"set":"remove")+"Attribute"](e||"readonly",!0),this.settings.userInput=!0,this.setContentEditable(!t)},setContentEditable(t){this.settings.userInput&&(this.DOM.input.contentEditable=t,this.DOM.input.tabIndex=t?0:-1)},setDisabled(t){this.setReadonly(t,"disabled")},normalizeTags(t){var e=this.settings,i=e.whitelist,s=e.delimiters,a=e.mode,n=e.tagTextProp,o=[],r=!!i&&i[0]instanceof Object,l=Array.isArray(t),d=l&&t[0].value,h=t=>(t+"").split(s).filter((t=>t)).map((t=>({[n]:this.trim(t),value:this.trim(t)})));if("number"==typeof t&&(t=t.toString()),"string"==typeof t){if(!t.trim())return[];t=h(t)}else l&&(t=[].concat(...t.map((t=>null!=t.value?t:h(t)))));return r&&!d&&(t.forEach((t=>{var e=o.map((t=>t.value)),i=this.dropdown.filterListItems.call(this,t[n],{exact:!0});this.settings.duplicates||(i=i.filter((t=>!e.includes(t.value))));var s=i.length>1?this.getWhitelistItem(t[n],n,i):i[0];s&&s instanceof Object?o.push(s):"mix"!=a&&(null==t.value&&(t.value=t[n]),o.push(t))})),o.length&&(t=o)),t},parseMixTags(t){var e=this.settings,i=e.mixTagsInterpolator,s=e.duplicates,a=e.transformTag,n=e.enforceWhitelist,o=e.maxTags,r=e.tagTextProp,l=[];t=t.split(i[0]).map(((t,e)=>{var d,h,g,p=t.split(i[1]),c=p[0],u=l.length==o;try{if(c==+c)throw Error;h=JSON.parse(c)}catch(t){h=this.normalizeTags(c)[0]||{value:c}}if(a.call(this,h),u||!(p.length>1)||n&&!this.isTagWhitelisted(h.value)||!s&&this.isTagDuplicate(h.value)){if(t)return e?i[0]+t:t}else h[d=h[r]?r:"value"]=this.trim(h[d]),g=this.createTagElem(h),l.push(h),g.classList.add(this.settings.classNames.tagNoAnimation),p[0]=g.outerHTML,this.value.push(h);return p.join("")})).join(""),this.DOM.input.innerHTML=t,this.DOM.input.appendChild(document.createTextNode("")),this.DOM.input.normalize();var d=this.getTagElms();return d.forEach(((t,e)=>T(t,l[e]))),this.update({withoutChangeEvent:!0}),b(d,this.state.hasFocus),t},replaceTextWithNode(t,e){if(this.state.tag||e){e=e||this.state.tag.prefix+this.state.tag.value;var i,s,a=this.state.selection||window.getSelection(),n=a.anchorNode,o=this.state.tag.delimiters?this.state.tag.delimiters.length:0;return n.splitText(a.anchorOffset-o),-1==(i=n.nodeValue.lastIndexOf(e))||(s=n.splitText(i),t&&n.parentNode.replaceChild(t,s)),!0}},selectTag(t,e){if(!this.settings.enforceWhitelist||this.isTagWhitelisted(e.value)){this.state.actions.selectOption&&setTimeout((()=>this.setRangeAtStartEnd(!1,this.DOM.input)));var i=this.getLastTag();return i?this.replaceTag(i,e):this.appendTag(t),this.value[0]=e,this.update(),this.trigger("add",{tag:t,data:e}),[t]}},addEmptyTag(t){var e=g({value:""},t||{}),i=this.createTagElem(e);T(i,e),this.appendTag(i),this.editTag(i,{skipValidation:!0}),this.toggleFocusClass(!0)},addTags(t,e,i){var s=[],a=this.settings,n=[],o=document.createDocumentFragment();if(i=i||a.skipInvalid,!t||0==t.length)return s;switch(t=this.normalizeTags(t),a.mode){case"mix":return this.addMixTags(t);case"select":e=!1,this.removeAllTags()}return this.DOM.input.removeAttribute("style"),t.forEach((t=>{var e,r={},l=Object.assign({},t,{value:t.value+""});if(t=Object.assign({},l),a.transformTag.call(this,t),t.__isValid=this.hasMaxTags()||this.validateTag(t),!0!==t.__isValid){if(i)return;if(g(r,this.getInvalidTagAttrs(t,t.__isValid),{__preInvalidData:l}),t.__isValid==this.TEXTS.duplicate&&this.flashTag(this.getTagElmByValue(t.value)),!a.createInvalidTags)return void n.push(t.value)}if("readonly"in t&&(t.readonly?r["aria-readonly"]=!0:delete t.readonly),e=this.createTagElem(t,r),s.push(e),"select"==a.mode)return this.selectTag(e,t);o.appendChild(e),t.__isValid&&!0===t.__isValid?(this.value.push(t),this.trigger("add",{tag:e,index:this.value.length-1,data:t})):(this.trigger("invalid",{data:t,index:this.value.length,tag:e,message:t.__isValid}),a.keepInvalidTags||setTimeout((()=>this.removeTags(e,!0)),1e3)),this.dropdown.position()})),this.appendTag(o),this.update(),t.length&&e&&(this.input.set.call(this,a.createInvalidTags?"":n.join(a._delimiters)),this.setRangeAtStartEnd(!1,this.DOM.input)),a.dropdown.enabled&&this.dropdown.refilter(),s},addMixTags(t){if((t=this.normalizeTags(t))[0].prefix||this.state.tag)return this.prefixedTextToTag(t[0]);var e=document.createDocumentFragment();return t.forEach((t=>{var i=this.createTagElem(t);e.appendChild(i)})),this.appendMixTags(e),e},appendMixTags(t){var e=!!this.state.selection;e?this.injectAtCaret(t):(this.DOM.input.focus(),(e=this.setStateSelection()).range.setStart(this.DOM.input,e.range.endOffset),e.range.setEnd(this.DOM.input,e.range.endOffset),this.DOM.input.appendChild(t),this.updateValueByDOMTags(),this.update())},prefixedTextToTag(t){var e,i=this.settings,s=this.state.tag.delimiters;if(i.transformTag.call(this,t),t.prefix=t.prefix||this.state.tag?this.state.tag.prefix:(i.pattern.source||i.pattern)[0],e=this.createTagElem(t),this.replaceTextWithNode(e)||this.DOM.input.appendChild(e),setTimeout((()=>e.classList.add(this.settings.classNames.tagNoAnimation)),300),this.value.push(t),this.update(),!s){var a=this.insertAfterTag(e)||e;setTimeout(w,0,a)}return this.state.tag=null,this.trigger("add",g({},{tag:e},{data:t})),e},appendTag(t){var e=this.DOM,i=e.input;e.scope.insertBefore(t,i)},createTagElem(t,i){t.__tagId=m();var s,a=g({},t,e({value:d(t.value+"")},i));return function(t){for(var e,i=document.createNodeIterator(t,NodeFilter.SHOW_TEXT,null,!1);e=i.nextNode();)e.textContent.trim()||e.parentNode.removeChild(e)}(s=this.parseTemplate("tag",[a,this])),T(s,t),s},reCheckInvalidTags(){var t=this.settings;this.getTagElms(t.classNames.tagNotAllowed).forEach(((e,i)=>{var s=T(e),a=this.hasMaxTags(),n=this.validateTag(s),o=!0===n&&!a;if("select"==t.mode&&this.toggleScopeValidation(n),o)return s=s.__preInvalidData?s.__preInvalidData:{value:s.value},this.replaceTag(e,s);e.title=a||n}))},removeTags(t,e,i){var s,a=this.settings;if(t=t&&t instanceof HTMLElement?[t]:t instanceof Array?t:t?[t]:[this.getLastTag()],s=t.reduce(((t,e)=>{e&&"string"==typeof e&&(e=this.getTagElmByValue(e));var i=T(e);return e&&i&&!i.readonly&&t.push({node:e,idx:this.getTagIdx(i),data:T(e,{__removed:!0})}),t}),[]),i="number"==typeof i?i:this.CSSVars.tagHideTransition,"select"==a.mode&&(i=0,this.input.set.call(this)),1==s.length&&"select"!=a.mode&&s[0].node.classList.contains(a.classNames.tagNotAllowed)&&(e=!0),s.length)return a.hooks.beforeRemoveTag(s,{tagify:this}).then((()=>{function t(t){t.node.parentNode&&(t.node.parentNode.removeChild(t.node),e?a.keepInvalidTags&&this.trigger("remove",{tag:t.node,index:t.idx}):(this.trigger("remove",{tag:t.node,index:t.idx,data:t.data}),this.dropdown.refilter(),this.dropdown.position(),this.DOM.input.normalize(),a.keepInvalidTags&&this.reCheckInvalidTags()))}i&&i>10&&1==s.length?function(e){e.node.style.width=parseFloat(window.getComputedStyle(e.node).width)+"px",document.body.clientTop,e.node.classList.add(a.classNames.tagHide),setTimeout(t.bind(this),i,e)}.call(this,s[0]):s.forEach(t.bind(this)),e||(this.removeTagsFromValue(s.map((t=>t.node))),this.update(),"select"==a.mode&&this.setContentEditable(!0))})).catch((t=>{}))},removeTagsFromDOM(){[].slice.call(this.getTagElms()).forEach((t=>t.parentNode.removeChild(t)))},removeTagsFromValue(t){(t=Array.isArray(t)?t:[t]).forEach((t=>{var e=T(t),i=this.getTagIdx(e);i>-1&&this.value.splice(i,1)}))},removeAllTags(t){t=t||{},this.value=[],"mix"==this.settings.mode?this.DOM.input.innerHTML="":this.removeTagsFromDOM(),this.dropdown.refilter(),this.dropdown.position(),this.state.dropdown.visible&&setTimeout((()=>{this.DOM.input.focus()})),"select"==this.settings.mode&&(this.input.set.call(this),this.setContentEditable(!0)),this.update(t)},postUpdate(){this.state.blockChangeEvent=!1;var t=this.settings,e=t.classNames,i="mix"==t.mode?t.mixMode.integrated?this.DOM.input.textContent:this.DOM.originalInput.value.trim():this.value.length+this.input.raw.call(this).length;this.toggleClass(e.hasMaxTags,this.value.length>=t.maxTags),this.toggleClass(e.hasNoTags,!this.value.length),this.toggleClass(e.empty,!i),"select"==t.mode&&this.toggleScopeValidation(this.value?.[0]?.__isValid)},setOriginalInputValue(t){var e=this.DOM.originalInput;this.settings.mixMode.integrated||(e.value=t,e.tagifyValue=e.value,this.setPersistedData(t,"value"))},update(t){clearTimeout(this.debouncedUpdateTimeout),this.debouncedUpdateTimeout=setTimeout(function(){var e=this.getInputValue();this.setOriginalInputValue(e),this.settings.onChangeAfterBlur&&(t||{}).withoutChangeEvent||this.state.blockChangeEvent||this.triggerChangeEvent(),this.postUpdate()}.bind(this),100)},getInputValue(){var t=this.getCleanValue();return"mix"==this.settings.mode?this.getMixedTagsAsString(t):t.length?this.settings.originalInputValueFormat?this.settings.originalInputValueFormat(t):JSON.stringify(t):""},getCleanValue(t){return a(t||this.value,this.dataProps)},getMixedTagsAsString(){var t="",e=this,i=this.settings,s=i.originalInputValueFormat||JSON.stringify,a=i.mixTagsInterpolator;return function i(o){o.childNodes.forEach((o=>{if(1==o.nodeType){const r=T(o);if("BR"==o.tagName&&(t+="\r\n"),r&&v.call(e,o)){if(r.__removed)return;t+=a[0]+s(n(r,e.dataProps))+a[1]}else o.getAttribute("style")||["B","I","U"].includes(o.tagName)?t+=o.textContent:"DIV"!=o.tagName&&"P"!=o.tagName||(t+="\r\n",i(o))}else t+=o.textContent}))}(this.DOM.input),t}},S.prototype.removeTag=S.prototype.removeTags,S},"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(e="undefined"!=typeof globalThis?globalThis:e||self).Tagify=i()}(jQuery); \ No newline at end of file +!function(t){var e,i;t.fn.tagify=function(e={}){return this.each((function(){var i,s=t(this);if(s.data("tagify"))return this;e.isJQueryPlugin=!0,i=new Tagify(s[0],e),s.data("tagify",i)}))},e=this,i=function(){"use strict";function t(t,e){var i=Object.keys(t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(t);e&&(s=s.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),i.push.apply(i,s)}return i}function e(e){for(var s=1;s(t=""+t,e=""+e,s&&(t=t.trim(),e=e.trim()),i?t==e:t.toLowerCase()==e.toLowerCase()),a=(t,e)=>t&&Array.isArray(t)&&t.map((t=>n(t,e)));function n(t,e){var i,s={};for(i in t)e.indexOf(i)<0&&(s[i]=t[i]);return s}function o(t){var e=document.createElement("div");return t.replace(/\&#?[0-9a-z]+;/gi,(function(t){return e.innerHTML=t,e.innerText}))}function r(t){return(new DOMParser).parseFromString(t.trim(),"text/html").body.firstElementChild}function l(t,e){for(e=e||"previous";t=t[e+"Sibling"];)if(3==t.nodeType)return t}function d(t){return"string"==typeof t?t.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/`|'/g,"'"):t}function h(t){var e=Object.prototype.toString.call(t).split(" ")[1].slice(0,-1);return t===Object(t)&&"Array"!=e&&"Function"!=e&&"RegExp"!=e&&"HTMLUnknownElement"!=e}function g(t,e,i){function s(t,e){for(var i in e)if(e.hasOwnProperty(i)){if(h(e[i])){h(t[i])?s(t[i],e[i]):t[i]=Object.assign({},e[i]);continue}if(Array.isArray(e[i])){t[i]=Object.assign([],e[i]);continue}t[i]=e[i]}}return t instanceof Object||(t={}),s(t,e),i&&s(t,i),t}function p(){const t=[],e={};for(let i of arguments)for(let s of i)h(s)?e[s.value]||(t.push(s),e[s.value]=1):t.includes(s)||t.push(s);return t}function c(t){return String.prototype.normalize?"string"==typeof t?t.normalize("NFD").replace(/[\u0300-\u036f]/g,""):void 0:t}var u=()=>/(?=.*chrome)(?=.*android)/i.test(navigator.userAgent);function m(){return([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,(t=>(t^crypto.getRandomValues(new Uint8Array(1))[0]&15>>t/4).toString(16)))}function v(t){return t&&t.classList&&t.classList.contains(this.settings.classNames.tag)}function f(t,e){var i=window.getSelection();return e=e||i.getRangeAt(0),"string"==typeof t&&(t=document.createTextNode(t)),e&&(e.deleteContents(),e.insertNode(t)),t}function T(t,e,i){return t?(e&&(t.__tagifyTagData=i?e:g({},t.__tagifyTagData||{},e)),t.__tagifyTagData):(console.warn("tag element doesn't exist",t,e),e)}function w(t){if(t&&t.parentNode){var e=t,i=window.getSelection(),s=i.getRangeAt(0);i.rangeCount&&(s.setStartAfter(e),s.collapse(!0),i.removeAllRanges(),i.addRange(s))}}function b(t,e){t.forEach((t=>{if(T(t.previousSibling)||!t.previousSibling){var i=document.createTextNode("​");t.before(i),e&&w(i)}}))}var y={delimiters:",",pattern:null,tagTextProp:"value",maxTags:1/0,callbacks:{},addTagOnBlur:!0,addTagOn:["blur","tab","enter"],onChangeAfterBlur:!0,duplicates:!1,whitelist:[],blacklist:[],enforceWhitelist:!1,userInput:!0,keepInvalidTags:!1,createInvalidTags:!0,mixTagsAllowedAfter:/,|\.|\:|\s/,mixTagsInterpolator:["[[","]]"],backspace:!0,skipInvalid:!1,pasteAsTags:!0,editTags:{clicks:2,keepInvalid:!0},transformTag:()=>{},trim:!0,a11y:{focusableTags:!1},mixMode:{insertAfterTag:" "},autoComplete:{enabled:!0,rightKey:!1,tabKey:!1},classNames:{namespace:"tagify",mixMode:"tagify--mix",selectMode:"tagify--select",input:"tagify__input",focus:"tagify--focus",tagNoAnimation:"tagify--noAnim",tagInvalid:"tagify--invalid",tagNotAllowed:"tagify--notAllowed",scopeLoading:"tagify--loading",hasMaxTags:"tagify--hasMaxTags",hasNoTags:"tagify--noTags",empty:"tagify--empty",inputInvalid:"tagify__input--invalid",dropdown:"tagify__dropdown",dropdownWrapper:"tagify__dropdown__wrapper",dropdownHeader:"tagify__dropdown__header",dropdownFooter:"tagify__dropdown__footer",dropdownItem:"tagify__dropdown__item",dropdownItemActive:"tagify__dropdown__item--active",dropdownItemHidden:"tagify__dropdown__item--hidden",dropdownInital:"tagify__dropdown--initial",tag:"tagify__tag",tagText:"tagify__tag-text",tagX:"tagify__tag__removeBtn",tagLoading:"tagify__tag--loading",tagEditing:"tagify__tag--editable",tagFlash:"tagify__tag--flash",tagHide:"tagify__tag--hide"},dropdown:{classname:"",enabled:2,maxItems:10,searchKeys:["value","searchBy"],fuzzySearch:!0,caseSensitive:!1,accentedSearch:!0,includeSelectedTags:!1,escapeHTML:!0,highlightFirst:!0,closeOnSelect:!0,clearOnSelect:!0,position:"all",appendTarget:null},hooks:{beforeRemoveTag:()=>Promise.resolve(),beforePaste:()=>Promise.resolve(),suggestionClick:()=>Promise.resolve(),beforeKeyDown:()=>Promise.resolve()}},x={events:{binding(){let t=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];var e=this.dropdown.events.callbacks,i=this.listeners.dropdown=this.listeners.dropdown||{position:this.dropdown.position.bind(this,null),onKeyDown:e.onKeyDown.bind(this),onMouseOver:e.onMouseOver.bind(this),onMouseLeave:e.onMouseLeave.bind(this),onClick:e.onClick.bind(this),onScroll:e.onScroll.bind(this)},s=t?"addEventListener":"removeEventListener";"manual"!=this.settings.dropdown.position&&(document[s]("scroll",i.position,!0),window[s]("resize",i.position),window[s]("keydown",i.onKeyDown)),this.DOM.dropdown[s]("mouseover",i.onMouseOver),this.DOM.dropdown[s]("mouseleave",i.onMouseLeave),this.DOM.dropdown[s]("mousedown",i.onClick),this.DOM.dropdown.content[s]("scroll",i.onScroll)},callbacks:{onKeyDown(t){if(this.state.hasFocus&&!this.state.composing){var e=this.settings,i=this.DOM.dropdown.querySelector(e.classNames.dropdownItemActiveSelector),s=this.dropdown.getSuggestionDataByNode(i),a="mix"==e.mode;e.hooks.beforeKeyDown(t,{tagify:this}).then((n=>{switch(t.key){case"ArrowDown":case"ArrowUp":case"Down":case"Up":t.preventDefault();var o=this.dropdown.getAllSuggestionsRefs(),r="ArrowUp"==t.key||"Up"==t.key;i&&(i=this.dropdown.getNextOrPrevOption(i,!r)),i&&i.matches(e.classNames.dropdownItemSelector)||(i=o[r?o.length-1:0]),this.dropdown.highlightOption(i,!0);break;case"Escape":case"Esc":this.dropdown.hide();break;case"ArrowRight":if(this.state.actions.ArrowLeft)return;case"Tab":{let n=!e.autoComplete.rightKey||!e.autoComplete.tabKey;if(!a&&i&&n&&!this.state.editing){t.preventDefault();var l=this.dropdown.getMappedValue(s);return this.input.autocomplete.set.call(this,l),!1}return!0}case"Enter":t.preventDefault(),e.hooks.suggestionClick(t,{tagify:this,tagData:s,suggestionElm:i}).then((()=>{if(i)return this.dropdown.selectOption(i),i=this.dropdown.getNextOrPrevOption(i,!r),void this.dropdown.highlightOption(i);this.dropdown.hide(),a||this.addTags(this.state.inputText.trim(),!0)})).catch((t=>console.warn(t)));break;case"Backspace":{if(a||this.state.editing.scope)return;const t=this.input.raw.call(this);""!=t&&8203!=t.charCodeAt(0)||(!0===e.backspace?this.removeTags():"edit"==e.backspace&&setTimeout(this.editTag.bind(this),0))}}}))}},onMouseOver(t){var e=t.target.closest(this.settings.classNames.dropdownItemSelector);this.dropdown.highlightOption(e)},onMouseLeave(t){this.dropdown.highlightOption()},onClick(t){if(0==t.button&&t.target!=this.DOM.dropdown&&t.target!=this.DOM.dropdown.content){var e=t.target.closest(this.settings.classNames.dropdownItemSelector),i=this.dropdown.getSuggestionDataByNode(e);this.state.actions.selectOption=!0,setTimeout((()=>this.state.actions.selectOption=!1),50),this.settings.hooks.suggestionClick(t,{tagify:this,tagData:i,suggestionElm:e}).then((()=>{e?this.dropdown.selectOption(e,t):this.dropdown.hide()})).catch((t=>console.warn(t)))}},onScroll(t){var e=t.target,i=e.scrollTop/(e.scrollHeight-e.parentNode.clientHeight)*100;this.trigger("dropdown:scroll",{percentage:Math.round(i)})}}},refilter(t){t=t||this.state.dropdown.query||"",this.suggestedListItems=this.dropdown.filterListItems(t),this.dropdown.fill(),this.suggestedListItems.length||this.dropdown.hide(),this.trigger("dropdown:updated",this.DOM.dropdown)},getSuggestionDataByNode(t){var e=t&&t.getAttribute("value");return this.suggestedListItems.find((t=>t.value==e))||null},getNextOrPrevOption(t){let e=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];var i=this.dropdown.getAllSuggestionsRefs(),s=i.findIndex((e=>e===t));return e?i[s+1]:i[s-1]},highlightOption(t,e){var i,s=this.settings.classNames.dropdownItemActive;if(this.state.ddItemElm&&(this.state.ddItemElm.classList.remove(s),this.state.ddItemElm.removeAttribute("aria-selected")),!t)return this.state.ddItemData=null,this.state.ddItemElm=null,void this.input.autocomplete.suggest.call(this);i=this.dropdown.getSuggestionDataByNode(t),this.state.ddItemData=i,this.state.ddItemElm=t,t.classList.add(s),t.setAttribute("aria-selected",!0),e&&(t.parentNode.scrollTop=t.clientHeight+t.offsetTop-t.parentNode.clientHeight),this.settings.autoComplete&&(this.input.autocomplete.suggest.call(this,i),this.dropdown.position())},selectOption(t,e){var i=this.settings,s=i.dropdown,a=s.clearOnSelect,n=s.closeOnSelect;if(!t)return this.addTags(this.state.inputText,!0),void(n&&this.dropdown.hide());e=e||{};var o=t.getAttribute("value"),r="noMatch"==o,l="mix"==i.mode,d=this.suggestedListItems.find((t=>(t.value??t)==o));if(this.trigger("dropdown:select",{data:d,elm:t,event:e}),o&&(d||r)){if(this.state.editing){let t=this.normalizeTags([d])[0];d=i.transformTag.call(this,t)||t,this.onEditTagDone(null,g({__isValid:!0},d))}else this[l?"addMixTags":"addTags"]([d||this.input.raw.call(this)],a);(l||this.DOM.input.parentNode)&&(setTimeout((()=>{this.DOM.input.focus(),this.toggleFocusClass(!0)})),n&&setTimeout(this.dropdown.hide.bind(this)),t.addEventListener("transitionend",(()=>{this.dropdown.fillHeaderFooter(),setTimeout((()=>t.remove()),100)}),{once:!0}),t.classList.add(this.settings.classNames.dropdownItemHidden))}else n&&setTimeout(this.dropdown.hide.bind(this))},selectAll(t){this.suggestedListItems.length=0,this.dropdown.hide(),this.dropdown.filterListItems("");var e=this.dropdown.filterListItems("");return t||(e=this.state.dropdown.suggestions),this.addTags(e,!0),this},filterListItems(t,e){var i,s,a,n,o,r=this.settings,l=r.dropdown,d=(e=e||{},[]),g=[],p=r.whitelist,u=l.maxItems>=0?l.maxItems:1/0,m=l.searchKeys,v=0;if(!(t="select"==r.mode&&this.value.length&&this.value[0][r.tagTextProp]==t?"":t)||!m.length)return d=l.includeSelectedTags?p:p.filter((t=>!this.isTagDuplicate(h(t)?t.value:t))),this.state.dropdown.suggestions=d,d.slice(0,u);function f(t,e){return e.toLowerCase().split(" ").every((e=>t.includes(e.toLowerCase())))}for(o=l.caseSensitive?""+t:(""+t).toLowerCase();vm.includes(t)))?m:["value"];l.fuzzySearch&&!e.exact?(a=u.reduce(((t,e)=>t+" "+(i[e]||"")),"").toLowerCase().trim(),l.accentedSearch&&(a=c(a),o=c(o)),t=0==a.indexOf(o),r=a===o,s=f(a,o)):(t=!0,s=u.some((t=>{var s=""+(i[t]||"");return l.accentedSearch&&(s=c(s),o=c(o)),l.caseSensitive||(s=s.toLowerCase()),r=s===o,e.exact?s===o:0==s.indexOf(o)}))),n=!l.includeSelectedTags&&this.isTagDuplicate(h(i)?i.value:i),s&&!n&&(r&&t?g.push(i):"startsWith"==l.sortby&&t?d.unshift(i):d.push(i))}return this.state.dropdown.suggestions=g.concat(d),"function"==typeof l.sortby?l.sortby(g.concat(d),o):g.concat(d).slice(0,u)},getMappedValue(t){var e=this.settings.dropdown.mapValueTo;return e?"function"==typeof e?e(t):t[e]||t.value:t.value},createListHTML(t){return g([],t).map(((t,i)=>{"string"!=typeof t&&"number"!=typeof t||(t={value:t});var s=this.dropdown.getMappedValue(t);return s="string"==typeof s&&this.settings.dropdown.escapeHTML?d(s):s,this.settings.templates.dropdownItem.apply(this,[e(e({},t),{},{mappedValue:s}),this])})).join("")}};function O(){this.dropdown={};for(let t in this._dropdown)this.dropdown[t]="function"==typeof this._dropdown[t]?this._dropdown[t].bind(this):this._dropdown[t];this.dropdown.refs()}var D=e(e({},x),{},{refs(){this.DOM.dropdown=this.parseTemplate("dropdown",[this.settings]),this.DOM.dropdown.content=this.DOM.dropdown.querySelector("[data-selector='tagify-suggestions-wrapper']")},getHeaderRef(){return this.DOM.dropdown.querySelector("[data-selector='tagify-suggestions-header']")},getFooterRef(){return this.DOM.dropdown.querySelector("[data-selector='tagify-suggestions-footer']")},getAllSuggestionsRefs(){return[...this.DOM.dropdown.content.querySelectorAll(this.settings.classNames.dropdownItemSelector)]},show(t){var e,i,a,n=this.settings,o="mix"==n.mode&&!n.enforceWhitelist,r=!n.whitelist||!n.whitelist.length,l="manual"==n.dropdown.position;if(t=void 0===t?this.state.inputText:t,!(r&&!o&&!n.templates.dropdownItemNoMatch||!1===n.dropdown.enable||this.state.isLoading||this.settings.readonly)){if(clearTimeout(this.dropdownHide__bindEventsTimeout),this.suggestedListItems=this.dropdown.filterListItems(t),t&&!this.suggestedListItems.length&&(this.trigger("dropdown:noMatch",t),n.templates.dropdownItemNoMatch&&(a=n.templates.dropdownItemNoMatch.call(this,{value:t}))),!a){if(this.suggestedListItems.length)t&&o&&!this.state.editing.scope&&!s(this.suggestedListItems[0].value,t)&&this.suggestedListItems.unshift({value:t});else{if(!t||!o||this.state.editing.scope)return this.input.autocomplete.suggest.call(this),void this.dropdown.hide();this.suggestedListItems=[{value:t}]}i=""+(h(e=this.suggestedListItems[0])?e.value:e),n.autoComplete&&i&&0==i.indexOf(t)&&this.input.autocomplete.suggest.call(this,e)}this.dropdown.fill(a),n.dropdown.highlightFirst&&this.dropdown.highlightOption(this.DOM.dropdown.content.querySelector(n.classNames.dropdownItemSelector)),this.state.dropdown.visible||setTimeout(this.dropdown.events.binding.bind(this)),this.state.dropdown.visible=t||!0,this.state.dropdown.query=t,this.setStateSelection(),l||setTimeout((()=>{this.dropdown.position(),this.dropdown.render()})),setTimeout((()=>{this.trigger("dropdown:show",this.DOM.dropdown)}))}},hide(t){var e=this.DOM,i=e.scope,s=e.dropdown,a="manual"==this.settings.dropdown.position&&!t;if(s&&document.body.contains(s)&&!a)return window.removeEventListener("resize",this.dropdown.position),this.dropdown.events.binding.call(this,!1),i.setAttribute("aria-expanded",!1),s.parentNode.removeChild(s),setTimeout((()=>{this.state.dropdown.visible=!1}),100),this.state.dropdown.query=this.state.ddItemData=this.state.ddItemElm=this.state.selection=null,this.state.tag&&this.state.tag.value.length&&(this.state.flaggedTags[this.state.tag.baseOffset]=this.state.tag),this.trigger("dropdown:hide",s),this},toggle(t){this.dropdown[this.state.dropdown.visible&&!t?"hide":"show"]()},getAppendTarget(){var t=this.settings.dropdown;return"function"==typeof t.appendTarget?t.appendTarget():t.appendTarget},render(){var t,e,i=((e=this.DOM.dropdown.cloneNode(!0)).style.cssText="position:fixed; top:-9999px; opacity:0",document.body.appendChild(e),t=e.clientHeight,e.parentNode.removeChild(e),t),s=this.settings,a="number"==typeof s.dropdown.enabled&&s.dropdown.enabled>=0,n=this.dropdown.getAppendTarget();return a?(this.DOM.scope.setAttribute("aria-expanded",!0),document.body.contains(this.DOM.dropdown)||(this.DOM.dropdown.classList.add(s.classNames.dropdownInital),this.dropdown.position(i),n.appendChild(this.DOM.dropdown),setTimeout((()=>this.DOM.dropdown.classList.remove(s.classNames.dropdownInital)))),this):this},fill(t){t="string"==typeof t?t:this.dropdown.createListHTML(t||this.suggestedListItems);var e,i=this.settings.templates.dropdownContent.call(this,t);this.DOM.dropdown.content.innerHTML=(e=i)?e.replace(/\>[\r\n ]+\<").split(/>\s+<").trim():""},fillHeaderFooter(){var t=this.dropdown.filterListItems(this.state.dropdown.query),e=this.parseTemplate("dropdownHeader",[t]),i=this.parseTemplate("dropdownFooter",[t]),s=this.dropdown.getHeaderRef(),a=this.dropdown.getFooterRef();e&&s?.parentNode.replaceChild(e,s),i&&a?.parentNode.replaceChild(i,a)},position(t){var e=this.settings.dropdown,i=this.dropdown.getAppendTarget();if("manual"!=e.position&&i){var s,a,n,o,r,l,d,h,g,p=this.DOM.dropdown,c=e.RTL,u=i===document.body,m=i===this.DOM.scope,v=u?window.pageYOffset:i.scrollTop,f=document.fullscreenElement||document.webkitFullscreenElement||document.documentElement,T=f.clientHeight,w=Math.max(f.clientWidth||0,window.innerWidth||0)>480?e.position:"all",b=this.DOM["input"==w?"input":"scope"];if(t=t||p.clientHeight,this.state.dropdown.visible){if("text"==w?(n=(s=function(){const t=document.getSelection();if(t.rangeCount){const e=t.getRangeAt(0),i=e.startContainer,s=e.startOffset;let a,n;if(s>0)return n=document.createRange(),n.setStart(i,s-1),n.setEnd(i,s),a=n.getBoundingClientRect(),{left:a.right,top:a.top,bottom:a.bottom};if(i.getBoundingClientRect)return i.getBoundingClientRect()}return{left:-9999,top:-9999}}()).bottom,a=s.top,o=s.left,r="auto"):(l=function(t){var e=0,i=0;for(t=t.parentNode;t&&t!=f;)e+=t.offsetTop||0,i+=t.offsetLeft||0,t=t.parentNode;return{top:e,left:i}}(i),s=b.getBoundingClientRect(),a=m?-1:s.top-l.top,n=(m?s.height:s.bottom-l.top)-1,o=m?-1:s.left-l.left,r=s.width+"px"),!u){let t=function(){for(var t=0,i=e.appendTarget.parentNode;i;)t+=i.scrollTop||0,i=i.parentNode;return t}();a+=t,n+=t}a=Math.floor(a),n=Math.ceil(n),h=((d=e.placeAbove??T-s.bottom`\n \n ​\n `,tag(t,e){let i=e.settings;return`\n \n
\n ${t[i.tagTextProp]||t.value}\n
\n
`},dropdown(t){var e=t.dropdown;return`
\n
\n
`},dropdownContent(t){var e=this.settings.templates,i=this.state.dropdown.suggestions;return`\n ${e.dropdownHeader.call(this,i)}\n ${t}\n ${e.dropdownFooter.call(this,i)}\n `},dropdownItem(t){return`
${t.mappedValue||t.value}
`},dropdownHeader(t){return`
`},dropdownFooter(t){var e=t.length-this.settings.dropdown.maxItems;return e>0?`
\n ${e} more items. Refine your search.\n
`:""},dropdownItemNoMatch:null},S={customBinding(){this.customEventsList.forEach((t=>{this.on(t,this.settings.callbacks[t])}))},binding(){let t=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];var e,i=this.settings,s=this.events.callbacks,a=t?"addEventListener":"removeEventListener";if(!this.state.mainEvents||!t){for(var n in this.state.mainEvents=t,t&&!this.listeners.main&&(this.events.bindGlobal.call(this),this.settings.isJQueryPlugin&&jQuery(this.DOM.originalInput).on("tagify.removeAllTags",this.removeAllTags.bind(this))),e=this.listeners.main=this.listeners.main||{focus:["input",s.onFocusBlur.bind(this)],keydown:["input",s.onKeydown.bind(this)],click:["scope",s.onClickScope.bind(this)],dblclick:"select"!=i.mode&&["scope",s.onDoubleClickScope.bind(this)],paste:["input",s.onPaste.bind(this)],drop:["input",s.onDrop.bind(this)],compositionstart:["input",s.onCompositionStart.bind(this)],compositionend:["input",s.onCompositionEnd.bind(this)]})e[n]&&this.DOM[e[n][0]][a](n,e[n][1]);clearInterval(this.listeners.main.originalInputValueObserverInterval),this.listeners.main.originalInputValueObserverInterval=setInterval(s.observeOriginalInputValue.bind(this),500);var o=this.listeners.main.inputMutationObserver||new MutationObserver(s.onInputDOMChange.bind(this));o.disconnect(),"mix"==i.mode&&o.observe(this.DOM.input,{childList:!0})}},bindGlobal(t){var e,i=this.events.callbacks,s=t?"removeEventListener":"addEventListener";if(this.listeners&&(t||!this.listeners.global))for(e of(this.listeners.global=this.listeners.global||[{type:this.isIE?"keydown":"input",target:this.DOM.input,cb:i[this.isIE?"onInputIE":"onInput"].bind(this)},{type:"keydown",target:window,cb:i.onWindowKeyDown.bind(this)},{type:"blur",target:this.DOM.input,cb:i.onFocusBlur.bind(this)},{type:"click",target:document,cb:i.onClickAnywhere.bind(this),useCapture:!0}],this.listeners.global))e.target[s](e.type,e.cb,!!e.useCapture)},unbindGlobal(){this.events.bindGlobal.call(this,!0)},callbacks:{onFocusBlur(t){var e=this.settings,i=t.target?this.trim(t.target.textContent):"",s=this.value?.[0]?.[e.tagTextProp],a=t.type,n=e.dropdown.enabled>=0,o={relatedTarget:t.relatedTarget},r=this.state.actions.selectOption&&(n||!e.dropdown.closeOnSelect),l=this.state.actions.addNew&&n;if("blur"==a){if(t.relatedTarget===this.DOM.scope)return this.dropdown.hide(),void this.DOM.input.focus();this.postUpdate(),e.onChangeAfterBlur&&this.triggerChangeEvent()}if(!r&&!l)if(this.state.hasFocus="focus"==a&&+new Date,this.toggleFocusClass(this.state.hasFocus),"mix"!=e.mode){if("focus"==a)return this.trigger("focus",o),void(0!==e.dropdown.enabled&&e.userInput||this.state.dropdown.visible||this.dropdown.show(this.value.length?"":void 0));"blur"==a&&(this.trigger("blur",o),this.loading(!1),"select"==e.mode&&s===i&&(i=""),i&&!this.state.actions.selectOption&&e.addTagOnBlur&&e.addTagOn.includes("blur")&&this.addTags(i,!0)),this.DOM.input.removeAttribute("style"),this.dropdown.hide()}else"focus"==a?this.trigger("focus",o):"blur"==t.type&&(this.trigger("blur",o),this.loading(!1),this.dropdown.hide(),this.state.dropdown.visible=void 0,this.setStateSelection())},onCompositionStart(t){this.state.composing=!0},onCompositionEnd(t){this.state.composing=!1},onWindowKeyDown(t){var e,i=document.activeElement,s=v.call(this,i)&&this.DOM.scope.contains(document.activeElement),a=s&&i.hasAttribute("readonly");if(s&&!a)switch(e=i.nextElementSibling,t.key){case"Backspace":this.settings.readonly||(this.removeTags(i),(e||this.DOM.input).focus());break;case"Enter":setTimeout(this.editTag.bind(this),0,i)}},onKeydown(t){var e=this.settings;if(!this.state.composing&&e.userInput){"select"==e.mode&&e.enforceWhitelist&&this.value.length&&"Tab"!=t.key&&t.preventDefault();var i=this.trim(t.target.textContent);this.trigger("keydown",{event:t}),e.hooks.beforeKeyDown(t,{tagify:this}).then((s=>{if("mix"==e.mode){switch(t.key){case"Left":case"ArrowLeft":this.state.actions.ArrowLeft=!0;break;case"Delete":case"Backspace":if(this.state.editing)return;var a=document.getSelection(),n="Delete"==t.key&&a.anchorOffset==(a.anchorNode.length||0),r=a.anchorNode.previousSibling,d=1==a.anchorNode.nodeType||!a.anchorOffset&&r&&1==r.nodeType&&a.anchorNode.previousSibling;o(this.DOM.input.innerHTML);var h,g,p,c=this.getTagElms(),m=1===a.anchorNode.length&&a.anchorNode.nodeValue==String.fromCharCode(8203);if("edit"==e.backspace&&d)return h=1==a.anchorNode.nodeType?null:a.anchorNode.previousElementSibling,setTimeout(this.editTag.bind(this),0,h),void t.preventDefault();if(u()&&d instanceof Element)return p=l(d),d.hasAttribute("readonly")||d.remove(),this.DOM.input.focus(),void setTimeout((()=>{w(p),this.DOM.input.click()}));if("BR"==a.anchorNode.nodeName)return;if((n||d)&&1==a.anchorNode.nodeType?g=0==a.anchorOffset?n?c[0]:null:c[Math.min(c.length,a.anchorOffset)-1]:n?g=a.anchorNode.nextElementSibling:d instanceof Element&&(g=d),3==a.anchorNode.nodeType&&!a.anchorNode.nodeValue&&a.anchorNode.previousElementSibling&&t.preventDefault(),(d||n)&&!e.backspace)return void t.preventDefault();if("Range"!=a.type&&!a.anchorOffset&&a.anchorNode==this.DOM.input&&"Delete"!=t.key)return void t.preventDefault();if("Range"!=a.type&&g&&g.hasAttribute("readonly"))return void w(l(g));"Delete"==t.key&&m&&T(a.anchorNode.nextSibling)&&this.removeTags(a.anchorNode.nextSibling),clearTimeout(I),I=setTimeout((()=>{var t=document.getSelection();o(this.DOM.input.innerHTML),!n&&t.anchorNode.previousSibling,this.value=[].map.call(c,((t,e)=>{var i=T(t);if(t.parentNode||i.readonly)return i;this.trigger("remove",{tag:t,index:e,data:i})})).filter((t=>t))}),20)}return!0}var v="manual"==e.dropdown.position;switch(t.key){case"Backspace":"select"==e.mode&&e.enforceWhitelist&&this.value.length?this.removeTags():this.state.dropdown.visible&&"manual"!=e.dropdown.position||""!=t.target.textContent&&8203!=i.charCodeAt(0)||(!0===e.backspace?this.removeTags():"edit"==e.backspace&&setTimeout(this.editTag.bind(this),0));break;case"Esc":case"Escape":if(this.state.dropdown.visible)return;t.target.blur();break;case"Down":case"ArrowDown":this.state.dropdown.visible||this.dropdown.show();break;case"ArrowRight":{let t=this.state.inputSuggestion||this.state.ddItemData;if(t&&e.autoComplete.rightKey)return void this.addTags([t],!0);break}case"Tab":{let s="select"==e.mode;if(!i||s)return!0;t.preventDefault()}case"Enter":if(this.state.dropdown.visible&&!v)return;t.preventDefault(),setTimeout((()=>{this.state.dropdown.visible&&!v||this.state.actions.selectOption||!e.addTagOn.includes(t.key.toLowerCase())||this.addTags(i,!0)}))}})).catch((t=>t))}},onInput(t){this.postUpdate();var e=this.settings;if("mix"==e.mode)return this.events.callbacks.onMixTagsInput.call(this,t);var i=this.input.normalize.call(this,void 0,{trim:!1}),s=i.length>=e.dropdown.enabled,a={value:i,inputElm:this.DOM.input},n=this.validateTag({value:i});"select"==e.mode&&this.toggleScopeValidation(n),a.isValid=n,this.state.inputText!=i&&(this.input.set.call(this,i,!1),-1!=i.search(e.delimiters)?this.addTags(i)&&this.input.set.call(this):e.dropdown.enabled>=0&&this.dropdown[s?"show":"hide"](i),this.trigger("input",a))},onMixTagsInput(t){var e,i,s,a,n,o,r,l,d=this.settings,h=this.value.length,p=this.getTagElms(),c=document.createDocumentFragment(),m=window.getSelection().getRangeAt(0),v=[].map.call(p,(t=>T(t).value));if("deleteContentBackward"==t.inputType&&u()&&this.events.callbacks.onKeydown.call(this,{target:t.target,key:"Backspace"}),b(this.getTagElms()),this.value.slice().forEach((t=>{t.readonly&&!v.includes(t.value)&&c.appendChild(this.createTagElem(t))})),c.childNodes.length&&(m.insertNode(c),this.setRangeAtStartEnd(!1,c.lastChild)),p.length!=h)return this.value=[].map.call(this.getTagElms(),(t=>T(t))),void this.update({withoutChangeEvent:!0});if(this.hasMaxTags())return!0;if(window.getSelection&&(o=window.getSelection()).rangeCount>0&&3==o.anchorNode.nodeType){if((m=o.getRangeAt(0).cloneRange()).collapse(!0),m.setStart(o.focusNode,0),s=(e=m.toString().slice(0,m.endOffset)).split(d.pattern).length-1,(i=e.match(d.pattern))&&(a=e.slice(e.lastIndexOf(i[i.length-1]))),a){if(this.state.actions.ArrowLeft=!1,this.state.tag={prefix:a.match(d.pattern)[0],value:a.replace(d.pattern,"")},this.state.tag.baseOffset=o.baseOffset-this.state.tag.value.length,l=this.state.tag.value.match(d.delimiters))return this.state.tag.value=this.state.tag.value.replace(d.delimiters,""),this.state.tag.delimiters=l[0],this.addTags(this.state.tag.value,d.dropdown.clearOnSelect),void this.dropdown.hide();n=this.state.tag.value.length>=d.dropdown.enabled;try{r=(r=this.state.flaggedTags[this.state.tag.baseOffset]).prefix==this.state.tag.prefix&&r.value[0]==this.state.tag.value[0],this.state.flaggedTags[this.state.tag.baseOffset]&&!this.state.tag.value&&delete this.state.flaggedTags[this.state.tag.baseOffset]}catch(t){}(r||s{this.update({withoutChangeEvent:!0}),this.trigger("input",g({},this.state.tag,{textContent:this.DOM.input.textContent})),this.state.tag&&this.dropdown[n?"show":"hide"](this.state.tag.value)}),10)},onInputIE(t){var e=this;setTimeout((function(){e.events.callbacks.onInput.call(e,t)}))},observeOriginalInputValue(){this.DOM.originalInput.parentNode||this.destroy(),this.DOM.originalInput.value!=this.DOM.originalInput.tagifyValue&&this.loadOriginalValues()},onClickAnywhere(t){t.target==this.DOM.scope||this.DOM.scope.contains(t.target)||(this.toggleFocusClass(!1),this.state.hasFocus=!1,!this.settings.userInput&&this.dropdown.hide())},onClickScope(t){var i=this.settings,s=t.target.closest("."+i.classNames.tag),a=t.target===this.DOM.scope,n=+new Date-this.state.hasFocus;if(a&&"select"!=i.mode)this.DOM.input.focus();else{if(!t.target.classList.contains(i.classNames.tagX))return s&&!this.state.editing?(this.trigger("click",{tag:s,index:this.getNodeIndex(s),data:T(s),event:t}),void(1!==i.editTags&&1!==i.editTags.clicks&&"select"!=i.mode||this.events.callbacks.onDoubleClickScope.call(this,t))):void(t.target==this.DOM.input&&("mix"==i.mode&&this.fixFirefoxLastTagNoCaret(),n>500)?this.state.dropdown.visible?this.dropdown.hide():0===i.dropdown.enabled&&"mix"!=i.mode&&this.dropdown.show(this.value.length?"":void 0):"select"!=i.mode||0!==i.dropdown.enabled||this.state.dropdown.visible||(this.events.callbacks.onDoubleClickScope.call(this,e(e({},t),{},{target:this.getTagElms()[0]})),!i.userInput&&this.dropdown.show()));this.removeTags(t.target.parentNode)}},onPaste(t){t.preventDefault();var e,i,s,a=this.settings;if("select"==a.mode&&a.enforceWhitelist||!a.userInput)return!1;a.readonly||(i=t.clipboardData||window.clipboardData,s=i.getData("Text"),a.hooks.beforePaste(t,{tagify:this,pastedText:s,clipboardData:i}).then((a=>{void 0===a&&(a=s),a&&(this.injectAtCaret(a,window.getSelection().getRangeAt(0)),"mix"==this.settings.mode?this.events.callbacks.onMixTagsInput.call(this,t):this.settings.pasteAsTags?e=this.addTags(this.state.inputText+a,!0):(this.state.inputText=a,this.dropdown.show(a))),this.trigger("paste",{event:t,pastedText:s,clipboardData:i,tagsElems:e})})).catch((t=>t)))},onDrop(t){t.preventDefault()},onEditTagInput(t,e){var i=t.closest("."+this.settings.classNames.tag),s=this.getNodeIndex(i),a=T(i),n=this.input.normalize.call(this,t),o={[this.settings.tagTextProp]:n,__tagId:a.__tagId},r=this.validateTag(o);this.editTagChangeDetected(g(a,o))||!0!==t.originalIsValid||(r=!0),i.classList.toggle(this.settings.classNames.tagInvalid,!0!==r),a.__isValid=r,i.title=!0===r?a.title||a.value:r,n.length>=this.settings.dropdown.enabled&&(this.state.editing&&(this.state.editing.value=n),this.dropdown.show(n)),this.trigger("edit:input",{tag:i,index:s,data:g({},this.value[s],{newValue:n}),event:e})},onEditTagPaste(t,e){var i=(e.clipboardData||window.clipboardData).getData("Text");e.preventDefault();var s=f(i);this.setRangeAtStartEnd(!1,s)},onEditTagClick(t,e){this.events.callbacks.onClickScope.call(this,e)},onEditTagFocus(t){this.state.editing={scope:t,input:t.querySelector("[contenteditable]")}},onEditTagBlur(t,e){if(v.call(this,e.relatedTarget)&&e.relatedTarget.contains(e.target))this.dropdown.hide();else if(this.state.editing&&(this.state.hasFocus||this.toggleFocusClass(),this.DOM.scope.contains(t))){var i,s,a=this.settings,n=t.closest("."+a.classNames.tag),o=T(n),r=this.input.normalize.call(this,t),l={[a.tagTextProp]:r,__tagId:o.__tagId},d=o.__originalData,h=this.editTagChangeDetected(g(o,l)),p=this.validateTag(l);if(r)if(h){if(i=this.hasMaxTags(),s=g({},d,{[a.tagTextProp]:this.trim(r),__isValid:p}),a.transformTag.call(this,s,d),!0!==(p=(!i||!0===d.__isValid)&&this.validateTag(s))){if(this.trigger("invalid",{data:s,tag:n,message:p}),a.editTags.keepInvalid)return;a.keepInvalidTags?s.__isValid=p:s=d}else a.keepInvalidTags&&(delete s.title,delete s["aria-invalid"],delete s.class);this.onEditTagDone(n,s)}else this.onEditTagDone(n,d);else this.onEditTagDone(n)}},onEditTagkeydown(t,e){if(!this.state.composing)switch(this.trigger("edit:keydown",{event:t}),t.key){case"Esc":case"Escape":this.state.editing=!1,e.__tagifyTagData.__originalData.value?e.parentNode.replaceChild(e.__tagifyTagData.__originalHTML,e):e.remove();break;case"Enter":case"Tab":t.preventDefault(),setTimeout((()=>t.target.blur()),0)}},onDoubleClickScope(t){var e,i,s=t.target.closest("."+this.settings.classNames.tag),a=T(s),n=this.settings;s&&!1!==a.editable&&(e=s.classList.contains(this.settings.classNames.tagEditing),i=s.hasAttribute("readonly"),n.readonly||e||i||!this.settings.editTags||!n.userInput||this.editTag(s),this.toggleFocusClass(!0),"select"!=n.mode&&this.trigger("dblclick",{tag:s,index:this.getNodeIndex(s),data:T(s)}))},onInputDOMChange(t){t.forEach((t=>{t.addedNodes.forEach((t=>{if("

"==t.outerHTML)t.replaceWith(document.createElement("br"));else if(1==t.nodeType&&t.querySelector(this.settings.classNames.tagSelector)){let e=document.createTextNode("");3==t.childNodes[0].nodeType&&"BR"!=t.previousSibling.nodeName&&(e=document.createTextNode("\n")),t.replaceWith(e,...[...t.childNodes].slice(0,-1)),w(e)}else if(v.call(this,t))if(3!=t.previousSibling?.nodeType||t.previousSibling.textContent||t.previousSibling.remove(),t.previousSibling&&"BR"==t.previousSibling.nodeName){t.previousSibling.replaceWith("\n​");let e=t.nextSibling,i="";for(;e;)i+=e.textContent,e=e.nextSibling;i.trim()&&w(t.previousSibling)}else t.previousSibling&&!T(t.previousSibling)||t.before("​")})),t.removedNodes.forEach((t=>{t&&"BR"==t.nodeName&&v.call(this,e)&&(this.removeTags(e),this.fixFirefoxLastTagNoCaret())}))}));var e=this.DOM.input.lastChild;e&&""==e.nodeValue&&e.remove(),e&&"BR"==e.nodeName||this.DOM.input.appendChild(document.createElement("br"))}}};function _(t,e){if(!t){console.warn("Tagify:","input element not found",t);const e=new Proxy(this,{get:()=>()=>e});return e}if(t.__tagify)return console.warn("Tagify: ","input element is already Tagified - Same instance is returned.",t),t.__tagify;var i;g(this,function(t){var e=document.createTextNode("");function i(t,i,s){s&&i.split(/\s+/g).forEach((i=>e[t+"EventListener"].call(e,i,s)))}return{off(t,e){return i("remove",t,e),this},on(t,e){return e&&"function"==typeof e&&i("add",t,e),this},trigger(i,s,a){var n;if(a=a||{cloneData:!0},i)if(t.settings.isJQueryPlugin)"remove"==i&&(i="removeTag"),jQuery(t.DOM.originalInput).triggerHandler(i,[s]);else{try{var o="object"==typeof s?s:{value:s};if((o=a.cloneData?g({},o):o).tagify=this,s.event&&(o.event=this.cloneEvent(s.event)),s instanceof Object)for(var r in s)s[r]instanceof HTMLElement&&(o[r]=s[r]);n=new CustomEvent(i,{detail:o})}catch(t){console.warn(t)}e.dispatchEvent(n)}}}}(this)),this.isFirefox=/firefox|fxios/i.test(navigator.userAgent)&&!/seamonkey/i.test(navigator.userAgent),this.isIE=window.document.documentMode,e=e||{},this.getPersistedData=(i=e.id,t=>{let e,s="/"+t;if(1==localStorage.getItem(M+i+"/v",1))try{e=JSON.parse(localStorage[M+i+s])}catch(t){}return e}),this.setPersistedData=(t=>t?(localStorage.setItem(M+t+"/v",1),(e,i)=>{let s="/"+i,a=JSON.stringify(e);e&&i&&(localStorage.setItem(M+t+s,a),dispatchEvent(new Event("storage")))}):()=>{})(e.id),this.clearPersistedData=(t=>e=>{const i=M+"/"+t+"/";if(e)localStorage.removeItem(i+e);else for(let t in localStorage)t.includes(i)&&localStorage.removeItem(t)})(e.id),this.applySettings(t,e),this.state={inputText:"",editing:!1,composing:!1,actions:{},mixMode:{},dropdown:{},flaggedTags:{}},this.value=[],this.listeners={},this.DOM={},this.build(t),O.call(this),this.getCSSVars(),this.loadOriginalValues(),this.events.customBinding.call(this),this.events.binding.call(this),t.autofocus&&this.DOM.input.focus(),t.__tagify=this}return _.prototype={_dropdown:D,placeCaretAfterNode:w,getSetTagData:T,helpers:{sameStr:s,removeCollectionProp:a,omit:n,isObject:h,parseHTML:r,escapeHTML:d,extend:g,concatWithoutDups:p,getUID:m,isNodeTag:v},customEventsList:["change","add","remove","invalid","input","paste","click","keydown","focus","blur","edit:input","edit:beforeUpdate","edit:updated","edit:start","edit:keydown","dropdown:show","dropdown:hide","dropdown:select","dropdown:updated","dropdown:noMatch","dropdown:scroll"],dataProps:["__isValid","__removed","__originalData","__originalHTML","__tagId"],trim(t){return this.settings.trim&&t&&"string"==typeof t?t.trim():t},parseHTML:r,templates:E,parseTemplate(t,e){return r((t=this.settings.templates[t]||t).apply(this,e))},set whitelist(t){const e=t&&Array.isArray(t);this.settings.whitelist=e?t:[],this.setPersistedData(e?t:[],"whitelist")},get whitelist(){return this.settings.whitelist},generateClassSelectors(t){for(let e in t){let i=e;Object.defineProperty(t,i+"Selector",{get(){return"."+this[i].split(" ")[0]}})}},applySettings(t,i){y.templates=this.templates;var s=g({},y,"mix"==i.mode?{dropdown:{position:"text"}}:{}),a=this.settings=g({},s,i);if(a.disabled=t.hasAttribute("disabled"),a.readonly=a.readonly||t.hasAttribute("readonly"),a.placeholder=d(t.getAttribute("placeholder")||a.placeholder||""),a.required=t.hasAttribute("required"),this.generateClassSelectors(a.classNames),void 0===a.dropdown.includeSelectedTags&&(a.dropdown.includeSelectedTags=a.duplicates),this.isIE&&(a.autoComplete=!1),["whitelist","blacklist"].forEach((e=>{var i=t.getAttribute("data-"+e);i&&(i=i.split(a.delimiters))instanceof Array&&(a[e]=i)})),"autoComplete"in i&&!h(i.autoComplete)&&(a.autoComplete=y.autoComplete,a.autoComplete.enabled=i.autoComplete),"mix"==a.mode&&(a.pattern=a.pattern||/@/,a.autoComplete.rightKey=!0,a.delimiters=i.delimiters||null,a.tagTextProp&&!a.dropdown.searchKeys.includes(a.tagTextProp)&&a.dropdown.searchKeys.push(a.tagTextProp)),t.pattern)try{a.pattern=new RegExp(t.pattern)}catch(t){}if(a.delimiters){a._delimiters=a.delimiters;try{a.delimiters=new RegExp(this.settings.delimiters,"g")}catch(t){}}a.disabled&&(a.userInput=!1),this.TEXTS=e(e({},N),a.texts||{}),("select"!=a.mode||i.dropdown?.enabled)&&a.userInput||(a.dropdown.enabled=0),a.dropdown.appendTarget=i.dropdown?.appendTarget||document.body;let n=this.getPersistedData("whitelist");Array.isArray(n)&&(this.whitelist=Array.isArray(a.whitelist)?p(a.whitelist,n):n)},getAttributes(t){var e,i=this.getCustomAttributes(t),s="";for(e in i)s+=" "+e+(void 0!==t[e]?`="${i[e]}"`:"");return s},getCustomAttributes(t){if(!h(t))return"";var e,i={};for(e in t)"__"!=e.slice(0,2)&&"class"!=e&&t.hasOwnProperty(e)&&void 0!==t[e]&&(i[e]=d(t[e]));return i},setStateSelection(){var t=window.getSelection(),e={anchorOffset:t.anchorOffset,anchorNode:t.anchorNode,range:t.getRangeAt&&t.rangeCount&&t.getRangeAt(0)};return this.state.selection=e,e},getCSSVars(){var t=getComputedStyle(this.DOM.scope,null);this.CSSVars={tagHideTransition:(t=>{let e=t.value;return"s"==t.unit?1e3*e:e})(function(t){if(!t)return{};var e=(t=t.trim().split(" ")[0]).split(/\d+/g).filter((t=>t)).pop().trim();return{value:+t.split(e).filter((t=>t))[0].trim(),unit:e}}(t.getPropertyValue("--tag-hide-transition")))}},build(t){var e=this.DOM,i=t.closest("label");this.settings.mixMode.integrated?(e.originalInput=null,e.scope=t,e.input=t):(e.originalInput=t,e.originalInput_tabIndex=t.tabIndex,e.scope=this.parseTemplate("wrapper",[t,this.settings]),e.input=e.scope.querySelector(this.settings.classNames.inputSelector),t.parentNode.insertBefore(e.scope,t),t.tabIndex=-1),i&&i.setAttribute("for","")},destroy(){this.events.unbindGlobal.call(this),this.DOM.scope.parentNode.removeChild(this.DOM.scope),this.DOM.originalInput.tabIndex=this.DOM.originalInput_tabIndex,delete this.DOM.originalInput.__tagify,this.dropdown.hide(!0),clearTimeout(this.dropdownHide__bindEventsTimeout),clearInterval(this.listeners.main.originalInputValueObserverInterval)},loadOriginalValues(t){var e,i=this.settings;if(this.state.blockChangeEvent=!0,void 0===t){const e=this.getPersistedData("value");t=e&&!this.DOM.originalInput.value?e:i.mixMode.integrated?this.DOM.input.textContent:this.DOM.originalInput.value}if(this.removeAllTags(),t)if("mix"==i.mode)this.parseMixTags(t),(e=this.DOM.input.lastChild)&&"BR"==e.tagName||this.DOM.input.insertAdjacentHTML("beforeend","
");else{try{JSON.parse(t)instanceof Array&&(t=JSON.parse(t))}catch(t){}this.addTags(t,!0).forEach((t=>t&&t.classList.add(i.classNames.tagNoAnimation)))}else this.postUpdate();this.state.lastOriginalValueReported=i.mixMode.integrated?"":this.DOM.originalInput.value},cloneEvent(t){var e={};for(var i in t)"path"!=i&&(e[i]=t[i]);return e},loading(t){return this.state.isLoading=t,this.DOM.scope.classList[t?"add":"remove"](this.settings.classNames.scopeLoading),this},tagLoading(t,e){return t&&t.classList[e?"add":"remove"](this.settings.classNames.tagLoading),this},toggleClass(t,e){"string"==typeof t&&this.DOM.scope.classList.toggle(t,e)},toggleScopeValidation(t){var e=!0===t||void 0===t;!this.settings.required&&t&&t===this.TEXTS.empty&&(e=!0),this.toggleClass(this.settings.classNames.tagInvalid,!e),this.DOM.scope.title=e?"":t},toggleFocusClass(t){this.toggleClass(this.settings.classNames.focus,!!t)},triggerChangeEvent:function(){if(!this.settings.mixMode.integrated){var t=this.DOM.originalInput,e=this.state.lastOriginalValueReported!==t.value,i=new CustomEvent("change",{bubbles:!0});e&&(this.state.lastOriginalValueReported=t.value,i.simulated=!0,t._valueTracker&&t._valueTracker.setValue(Math.random()),t.dispatchEvent(i),this.trigger("change",this.state.lastOriginalValueReported),t.value=this.state.lastOriginalValueReported)}},events:S,fixFirefoxLastTagNoCaret(){},setRangeAtStartEnd(t,e){if(e){t="number"==typeof t?t:!!t,e=e.lastChild||e;var i=document.getSelection();if(i.focusNode instanceof Element&&!this.DOM.input.contains(i.focusNode))return!0;try{i.rangeCount>=1&&["Start","End"].forEach((s=>i.getRangeAt(0)["set"+s](e,t||e.length)))}catch(t){console.warn("Tagify: ",t)}}},insertAfterTag(t,e){if(e=e||this.settings.mixMode.insertAfterTag,t&&t.parentNode&&e)return e="string"==typeof e?document.createTextNode(e):e,t.parentNode.insertBefore(e,t.nextSibling),e},editTagChangeDetected(t){var e=t.__originalData;for(var i in e)if(!this.dataProps.includes(i)&&t[i]!=e[i])return!0;return!1},getTagTextNode(t){return t.querySelector(this.settings.classNames.tagTextSelector)},setTagTextNode(t,e){this.getTagTextNode(t).innerHTML=d(e)},editTag(t,e){t=t||this.getLastTag(),e=e||{},this.dropdown.hide();var i=this.settings,s=this.getTagTextNode(t),a=this.getNodeIndex(t),n=T(t),o=this.events.callbacks,r=!0;if(s){if(!(n instanceof Object&&"editable"in n)||n.editable)return n=T(t,{__originalData:g({},n),__originalHTML:t.cloneNode(!0)}),T(n.__originalHTML,n.__originalData),s.setAttribute("contenteditable",!0),t.classList.add(i.classNames.tagEditing),s.addEventListener("click",o.onEditTagClick.bind(this,t)),s.addEventListener("focus",o.onEditTagFocus.bind(this,t)),s.addEventListener("blur",o.onEditTagBlur.bind(this,this.getTagTextNode(t))),s.addEventListener("input",o.onEditTagInput.bind(this,s)),s.addEventListener("paste",o.onEditTagPaste.bind(this,s)),s.addEventListener("keydown",(e=>o.onEditTagkeydown.call(this,e,t))),s.addEventListener("compositionstart",o.onCompositionStart.bind(this)),s.addEventListener("compositionend",o.onCompositionEnd.bind(this)),e.skipValidation||(r=this.editTagToggleValidity(t)),s.originalIsValid=r,this.trigger("edit:start",{tag:t,index:a,data:n,isValid:r}),s.focus(),this.setRangeAtStartEnd(!1,s),0===i.dropdown.enabled&&this.dropdown.show(),this.state.hasFocus=!0,this}else console.warn("Cannot find element in Tag template: .",i.classNames.tagTextSelector)},editTagToggleValidity(t,e){var i;if(e=e||T(t))return(i=!("__isValid"in e)||!0===e.__isValid)||this.removeTagsFromValue(t),this.update(),t.classList.toggle(this.settings.classNames.tagNotAllowed,!i),e.__isValid=i,e.__isValid;console.warn("tag has no data: ",t,e)},onEditTagDone(t,e){e=e||{};var i,s={tag:t=t||this.state.editing.scope,index:this.getNodeIndex(t),previousData:T(t),data:e},a=this.settings;this.trigger("edit:beforeUpdate",s,{cloneData:!1}),this.state.editing=!1,delete e.__originalData,delete e.__originalHTML,t&&((i=e[a.tagTextProp])?i.trim?.():a.tagTextProp in e?void 0:e.value)?(t=this.replaceTag(t,e),this.editTagToggleValidity(t,e),a.a11y.focusableTags?t.focus():w(t)):t&&this.removeTags(t),this.trigger("edit:updated",s),this.dropdown.hide(),this.settings.keepInvalidTags&&this.reCheckInvalidTags()},replaceTag(t,e){e&&""!==e.value&&void 0!==e.value||(e=t.__tagifyTagData),e.__isValid&&1!=e.__isValid&&g(e,this.getInvalidTagAttrs(e,e.__isValid));var i=this.createTagElem(e);return t.parentNode.replaceChild(i,t),this.updateValueByDOMTags(),i},updateValueByDOMTags(){this.value.length=0,[].forEach.call(this.getTagElms(),(t=>{t.classList.contains(this.settings.classNames.tagNotAllowed.split(" ")[0])||this.value.push(T(t))})),this.update()},injectAtCaret(t,e){if(!(e=e||this.state.selection?.range)&&t)return this.appendMixTags(t),this;let i=f(t,e);return this.setRangeAtStartEnd(!1,i),this.updateValueByDOMTags(),this.update(),this},input:{set(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",e=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];var i=this.settings.dropdown.closeOnSelect;this.state.inputText=t,e&&(this.DOM.input.innerHTML=d(""+t)),!t&&i&&this.dropdown.hide.bind(this),this.input.autocomplete.suggest.call(this),this.input.validate.call(this)},raw(){return this.DOM.input.textContent},validate(){var t=!this.state.inputText||!0===this.validateTag({value:this.state.inputText});return this.DOM.input.classList.toggle(this.settings.classNames.inputInvalid,!t),t},normalize(t,e){var i=t||this.DOM.input,s=[];i.childNodes.forEach((t=>3==t.nodeType&&s.push(t.nodeValue))),s=s.join("\n");try{s=s.replace(/(?:\r\n|\r|\n)/g,this.settings.delimiters.source.charAt(0))}catch(t){}return s=s.replace(/\s/g," "),e?.trim?this.trim(s):s},autocomplete:{suggest(t){if(this.settings.autoComplete.enabled){"object"!=typeof(t=t||{value:""})&&(t={value:t});var e=this.dropdown.getMappedValue(t);if("number"!=typeof e){var i=this.state.inputText.toLowerCase(),s=e.substr(0,this.state.inputText.length).toLowerCase(),a=e.substring(this.state.inputText.length);e&&this.state.inputText&&s==i?(this.DOM.input.setAttribute("data-suggest",a),this.state.inputSuggestion=t):(this.DOM.input.removeAttribute("data-suggest"),delete this.state.inputSuggestion)}}},set(t){var e=this.DOM.input.getAttribute("data-suggest"),i=t||(e?this.state.inputText+e:null);return!!i&&("mix"==this.settings.mode?this.replaceTextWithNode(document.createTextNode(this.state.tag.prefix+i)):(this.input.set.call(this,i),this.setRangeAtStartEnd(!1,this.DOM.input)),this.input.autocomplete.suggest.call(this),this.dropdown.hide(),!0)}}},getTagIdx(t){return this.value.findIndex((e=>e.__tagId==(t||{}).__tagId))},getNodeIndex(t){var e=0;if(t)for(;t=t.previousElementSibling;)e++;return e},getTagElms(){for(var t=arguments.length,e=new Array(t),i=0;i{a.__tagifyTagData&&s(this.trim(a.__tagifyTagData.value),t,i)&&e.push(n)})),e},getTagElmByValue(t){var e=this.getTagIndexByValue(t)[0];return this.getTagElms()[e]},flashTag(t){t&&(t.classList.add(this.settings.classNames.tagFlash),setTimeout((()=>{t.classList.remove(this.settings.classNames.tagFlash)}),100))},isTagBlacklisted(t){return t=this.trim(t.toLowerCase()),this.settings.blacklist.filter((e=>(""+e).toLowerCase()==t)).length},isTagWhitelisted(t){return!!this.getWhitelistItem(t)},getWhitelistItem(t,e,i){e=e||"value";var a,n=this.settings;return(i=i||n.whitelist).some((i=>{var o="string"==typeof i?i:i[e]||i.value;if(s(o,t,n.dropdown.caseSensitive,n.trim))return a="string"==typeof i?{value:i}:i,!0})),a||"value"!=e||"value"==n.tagTextProp||(a=this.getWhitelistItem(t,n.tagTextProp,i)),a},validateTag(t){var e=this.settings,i="value"in t?"value":e.tagTextProp,s=this.trim(t[i]+"");return(t[i]+"").trim()?"mix"!=e.mode&&e.pattern&&e.pattern instanceof RegExp&&!e.pattern.test(s)?this.TEXTS.pattern:!e.duplicates&&this.isTagDuplicate(s,e.dropdown.caseSensitive,t.__tagId)?this.TEXTS.duplicate:this.isTagBlacklisted(s)||e.enforceWhitelist&&!this.isTagWhitelisted(s)?this.TEXTS.notAllowed:!e.validate||e.validate(t):this.TEXTS.empty},getInvalidTagAttrs(t,e){return{"aria-invalid":!0,class:`${t.class||""} ${this.settings.classNames.tagNotAllowed}`.trim(),title:e}},hasMaxTags(){return this.value.length>=this.settings.maxTags&&this.TEXTS.exceed},setReadonly(t,e){var i=this.settings;document.activeElement.blur(),i[e||"readonly"]=t,this.DOM.scope[(t?"set":"remove")+"Attribute"](e||"readonly",!0),this.settings.userInput=!0,this.setContentEditable(!t)},setContentEditable(t){this.settings.userInput&&(this.DOM.input.contentEditable=t,this.DOM.input.tabIndex=t?0:-1)},setDisabled(t){this.setReadonly(t,"disabled")},normalizeTags(t){var e=this.settings,i=e.whitelist,s=e.delimiters,a=e.mode,n=e.tagTextProp,o=[],r=!!i&&i[0]instanceof Object,l=Array.isArray(t),d=l&&t[0].value,h=t=>(t+"").split(s).filter((t=>t)).map((t=>({[n]:this.trim(t),value:this.trim(t)})));if("number"==typeof t&&(t=t.toString()),"string"==typeof t){if(!t.trim())return[];t=h(t)}else l&&(t=[].concat(...t.map((t=>null!=t.value?t:h(t)))));return r&&!d&&(t.forEach((t=>{var e=o.map((t=>t.value)),i=this.dropdown.filterListItems.call(this,t[n],{exact:!0});this.settings.duplicates||(i=i.filter((t=>!e.includes(t.value))));var s=i.length>1?this.getWhitelistItem(t[n],n,i):i[0];s&&s instanceof Object?o.push(s):"mix"!=a&&(null==t.value&&(t.value=t[n]),o.push(t))})),o.length&&(t=o)),t},parseMixTags(t){var e=this.settings,i=e.mixTagsInterpolator,s=e.duplicates,a=e.transformTag,n=e.enforceWhitelist,o=e.maxTags,r=e.tagTextProp,l=[];t=t.split(i[0]).map(((t,e)=>{var d,h,g,p=t.split(i[1]),c=p[0],u=l.length==o;try{if(c==+c)throw Error;h=JSON.parse(c)}catch(t){h=this.normalizeTags(c)[0]||{value:c}}if(a.call(this,h),u||!(p.length>1)||n&&!this.isTagWhitelisted(h.value)||!s&&this.isTagDuplicate(h.value)){if(t)return e?i[0]+t:t}else h[d=h[r]?r:"value"]=this.trim(h[d]),g=this.createTagElem(h),l.push(h),g.classList.add(this.settings.classNames.tagNoAnimation),p[0]=g.outerHTML,this.value.push(h);return p.join("")})).join(""),this.DOM.input.innerHTML=t,this.DOM.input.appendChild(document.createTextNode("")),this.DOM.input.normalize();var d=this.getTagElms();return d.forEach(((t,e)=>T(t,l[e]))),this.update({withoutChangeEvent:!0}),b(d,this.state.hasFocus),t},replaceTextWithNode(t,e){if(this.state.tag||e){e=e||this.state.tag.prefix+this.state.tag.value;var i,s,a=this.state.selection||window.getSelection(),n=a.anchorNode,o=this.state.tag.delimiters?this.state.tag.delimiters.length:0;return n.splitText(a.anchorOffset-o),-1==(i=n.nodeValue.lastIndexOf(e))||(s=n.splitText(i),t&&n.parentNode.replaceChild(t,s)),!0}},prepareNewTagNode(t,e){e=e||{};var i=this.settings,s=[],a={},n=Object.assign({},t,{value:t.value+""});if(t=Object.assign({},n),i.transformTag.call(this,t),t.__isValid=this.hasMaxTags()||this.validateTag(t),!0!==t.__isValid){if(e.skipInvalid)return;if(g(a,this.getInvalidTagAttrs(t,t.__isValid),{__preInvalidData:n}),t.__isValid==this.TEXTS.duplicate&&this.flashTag(this.getTagElmByValue(t.value)),!i.createInvalidTags)return void s.push(t.value)}return"readonly"in t&&(t.readonly?a["aria-readonly"]=!0:delete t.readonly),{tagElm:this.createTagElem(t,a),tagData:t,aggregatedInvalidInput:s}},selectTag(t,e){if(!this.settings.enforceWhitelist||this.isTagWhitelisted(e.value)){this.state.actions.selectOption&&setTimeout((()=>this.setRangeAtStartEnd(!1,this.DOM.input)));var i=this.getLastTag();return i?this.replaceTag(i,e):this.appendTag(t),this.value[0]=e,this.update(),this.trigger("add",{tag:t,data:e}),[t]}},addEmptyTag(t){var e=g({value:""},t||{}),i=this.createTagElem(e);T(i,e),this.appendTag(i),this.editTag(i,{skipValidation:!0}),this.toggleFocusClass(!0)},addTags(t,e,i){var s=[],a=this.settings,n=[],o=document.createDocumentFragment();if(!t||0==t.length)return s;switch(t=this.normalizeTags(t),a.mode){case"mix":return this.addMixTags(t);case"select":e=!1,this.removeAllTags()}return this.DOM.input.removeAttribute("style"),t.forEach((t=>{const e=this.prepareNewTagNode(t,{skipInvalid:i||a.skipInvalid}),r=e.tagElm;if(t=e.tagData,n=e.aggregatedInvalidInput,s.push(r),"select"==a.mode)return this.selectTag(r,t);o.appendChild(r),t.__isValid&&!0===t.__isValid?(this.value.push(t),this.trigger("add",{tag:r,index:this.value.length-1,data:t})):(this.trigger("invalid",{data:t,index:this.value.length,tag:r,message:t.__isValid}),a.keepInvalidTags||setTimeout((()=>this.removeTags(r,!0)),1e3)),this.dropdown.position()})),this.appendTag(o),this.update(),t.length&&e&&(this.input.set.call(this,a.createInvalidTags?"":n.join(a._delimiters)),this.setRangeAtStartEnd(!1,this.DOM.input)),a.dropdown.enabled&&this.dropdown.refilter(),s},addMixTags(t){if((t=this.normalizeTags(t))[0].prefix||this.state.tag)return this.prefixedTextToTag(t[0]);var e=document.createDocumentFragment();return t.forEach((t=>{const i=this.prepareNewTagNode(t);e.appendChild(i.tagElm)})),this.appendMixTags(e),e},appendMixTags(t){var e=!!this.state.selection;e?this.injectAtCaret(t):(this.DOM.input.focus(),(e=this.setStateSelection()).range.setStart(this.DOM.input,e.range.endOffset),e.range.setEnd(this.DOM.input,e.range.endOffset),this.DOM.input.appendChild(t),this.updateValueByDOMTags(),this.update())},prefixedTextToTag(t){var e,i=this.settings,s=this.state.tag?.delimiters;if(t.prefix=t.prefix||this.state.tag?this.state.tag.prefix:(i.pattern.source||i.pattern)[0],e=this.prepareNewTagNode(t).tagElm,this.replaceTextWithNode(e)||this.DOM.input.appendChild(e),setTimeout((()=>e.classList.add(this.settings.classNames.tagNoAnimation)),300),this.value.push(t),this.update(),!s){var a=this.insertAfterTag(e)||e;setTimeout(w,0,a)}return this.state.tag=null,this.trigger("add",g({},{tag:e},{data:t})),e},appendTag(t){var e=this.DOM,i=e.input;e.scope.insertBefore(t,i)},createTagElem(t,i){t.__tagId=m();var s,a=g({},t,e({value:d(t.value+"")},i));return function(t){for(var e,i=document.createNodeIterator(t,NodeFilter.SHOW_TEXT,null,!1);e=i.nextNode();)e.textContent.trim()||e.parentNode.removeChild(e)}(s=this.parseTemplate("tag",[a,this])),T(s,t),s},reCheckInvalidTags(){var t=this.settings;this.getTagElms(t.classNames.tagNotAllowed).forEach(((e,i)=>{var s=T(e),a=this.hasMaxTags(),n=this.validateTag(s),o=!0===n&&!a;if("select"==t.mode&&this.toggleScopeValidation(n),o)return s=s.__preInvalidData?s.__preInvalidData:{value:s.value},this.replaceTag(e,s);e.title=a||n}))},removeTags(t,e,i){var s,a=this.settings;if(t=t&&t instanceof HTMLElement?[t]:t instanceof Array?t:t?[t]:[this.getLastTag()],s=t.reduce(((t,e)=>{e&&"string"==typeof e&&(e=this.getTagElmByValue(e));var i=T(e);return e&&i&&!i.readonly&&t.push({node:e,idx:this.getTagIdx(i),data:T(e,{__removed:!0})}),t}),[]),i="number"==typeof i?i:this.CSSVars.tagHideTransition,"select"==a.mode&&(i=0,this.input.set.call(this)),1==s.length&&"select"!=a.mode&&s[0].node.classList.contains(a.classNames.tagNotAllowed)&&(e=!0),s.length)return a.hooks.beforeRemoveTag(s,{tagify:this}).then((()=>{function t(t){t.node.parentNode&&(t.node.parentNode.removeChild(t.node),e?a.keepInvalidTags&&this.trigger("remove",{tag:t.node,index:t.idx}):(this.trigger("remove",{tag:t.node,index:t.idx,data:t.data}),this.dropdown.refilter(),this.dropdown.position(),this.DOM.input.normalize(),a.keepInvalidTags&&this.reCheckInvalidTags()))}i&&i>10&&1==s.length?function(e){e.node.style.width=parseFloat(window.getComputedStyle(e.node).width)+"px",document.body.clientTop,e.node.classList.add(a.classNames.tagHide),setTimeout(t.bind(this),i,e)}.call(this,s[0]):s.forEach(t.bind(this)),e||(this.removeTagsFromValue(s.map((t=>t.node))),this.update(),"select"==a.mode&&this.setContentEditable(!0))})).catch((t=>{}))},removeTagsFromDOM(){[].slice.call(this.getTagElms()).forEach((t=>t.parentNode.removeChild(t)))},removeTagsFromValue(t){(t=Array.isArray(t)?t:[t]).forEach((t=>{var e=T(t),i=this.getTagIdx(e);i>-1&&this.value.splice(i,1)}))},removeAllTags(t){t=t||{},this.value=[],"mix"==this.settings.mode?this.DOM.input.innerHTML="":this.removeTagsFromDOM(),this.dropdown.refilter(),this.dropdown.position(),this.state.dropdown.visible&&setTimeout((()=>{this.DOM.input.focus()})),"select"==this.settings.mode&&(this.input.set.call(this),this.setContentEditable(!0)),this.update(t)},postUpdate(){this.state.blockChangeEvent=!1;var t=this.settings,e=t.classNames,i="mix"==t.mode?t.mixMode.integrated?this.DOM.input.textContent:this.DOM.originalInput.value.trim():this.value.length+this.input.raw.call(this).length;this.toggleClass(e.hasMaxTags,this.value.length>=t.maxTags),this.toggleClass(e.hasNoTags,!this.value.length),this.toggleClass(e.empty,!i),"select"==t.mode&&this.toggleScopeValidation(this.value?.[0]?.__isValid)},setOriginalInputValue(t){var e=this.DOM.originalInput;this.settings.mixMode.integrated||(e.value=t,e.tagifyValue=e.value,this.setPersistedData(t,"value"))},update(t){clearTimeout(this.debouncedUpdateTimeout),this.debouncedUpdateTimeout=setTimeout(function(){var e=this.getInputValue();this.setOriginalInputValue(e),this.settings.onChangeAfterBlur&&(t||{}).withoutChangeEvent||this.state.blockChangeEvent||this.triggerChangeEvent(),this.postUpdate()}.bind(this),100)},getInputValue(){var t=this.getCleanValue();return"mix"==this.settings.mode?this.getMixedTagsAsString(t):t.length?this.settings.originalInputValueFormat?this.settings.originalInputValueFormat(t):JSON.stringify(t):""},getCleanValue(t){return a(t||this.value,this.dataProps)},getMixedTagsAsString(){var t="",e=this,i=this.settings,s=i.originalInputValueFormat||JSON.stringify,a=i.mixTagsInterpolator;return function i(o){o.childNodes.forEach((o=>{if(1==o.nodeType){const r=T(o);if("BR"==o.tagName&&(t+="\r\n"),r&&v.call(e,o)){if(r.__removed)return;t+=a[0]+s(n(r,e.dataProps))+a[1]}else o.getAttribute("style")||["B","I","U"].includes(o.tagName)?t+=o.textContent:"DIV"!=o.tagName&&"P"!=o.tagName||(t+="\r\n",i(o))}else t+=o.textContent}))}(this.DOM.input),t}},_.prototype.removeTag=_.prototype.removeTags,_},"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(e="undefined"!=typeof globalThis?globalThis:e||self).Tagify=i()}(jQuery); \ No newline at end of file diff --git a/dist/react.tagify.js b/dist/react.tagify.js index 280287e8..7bf84c69 100644 --- a/dist/react.tagify.js +++ b/dist/react.tagify.js @@ -1,7 +1,8 @@ /** - * Tagify (v 4.20.0) - tags input component - * By undefined + * Tagify v4.21.0 - tags input component + * By: Yair Even-Or * https://github.com/yairEO/tagify + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights diff --git a/dist/tagify.esm.js b/dist/tagify.esm.js index 7e3d3991..97d87510 100644 --- a/dist/tagify.esm.js +++ b/dist/tagify.esm.js @@ -1,27 +1,28 @@ -/** - * Tagify (v 4.20.0) - tags input component - * By undefined - * https://github.com/yairEO/tagify - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * THE SOFTWARE IS NOT PERMISSIBLE TO BE SOLD. - */ +/* +Tagify v4.21.0 - tags input component +By: Yair Even-Or +https://github.com/yairEO/tagify + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +THE SOFTWARE IS NOT PERMISSIBLE TO BE SOLD. +*/ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); @@ -442,7 +443,7 @@ var DEFAULTS = { // Should the suggestions list Include already-selected tags (after filtering) escapeHTML: true, // escapes HTML entities in the suggestions' rendered text - highlightFirst: false, + highlightFirst: true, // highlights first-matched item in the list closeOnSelect: true, // closes the dropdown after selecting an item, if `enabled:0` (which means always show dropdown) @@ -461,520 +462,225 @@ var DEFAULTS = { } }; -function initDropdown() { - this.dropdown = {}; +/** + * Tagify's dropdown suggestions-related logic + */ - // auto-bind "this" to all the dropdown methods - for (let p in this._dropdown) this.dropdown[p] = typeof this._dropdown[p] === 'function' ? this._dropdown[p].bind(this) : this._dropdown[p]; - this.dropdown.refs(); -} -var _dropdown = { - refs() { - this.DOM.dropdown = this.parseTemplate('dropdown', [this.settings]); - this.DOM.dropdown.content = this.DOM.dropdown.querySelector("[data-selector='tagify-suggestions-wrapper']"); +var suggestionsMethods = { + events: { + /** + * Events should only be binded when the dropdown is rendered and removed when isn't + * because there might be multiple Tagify instances on a certain page + * @param {Boolean} bindUnbind [optional. true when wanting to unbind all the events] + */ + binding() { + let bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; + // references to the ".bind()" methods must be saved so they could be unbinded later + var _CB = this.dropdown.events.callbacks, + // callback-refs + _CBR = this.listeners.dropdown = this.listeners.dropdown || { + position: this.dropdown.position.bind(this, null), + onKeyDown: _CB.onKeyDown.bind(this), + onMouseOver: _CB.onMouseOver.bind(this), + onMouseLeave: _CB.onMouseLeave.bind(this), + onClick: _CB.onClick.bind(this), + onScroll: _CB.onScroll.bind(this) + }, + action = bindUnbind ? 'addEventListener' : 'removeEventListener'; + if (this.settings.dropdown.position != 'manual') { + document[action]('scroll', _CBR.position, true); + window[action]('resize', _CBR.position); + window[action]('keydown', _CBR.onKeyDown); + } + this.DOM.dropdown[action]('mouseover', _CBR.onMouseOver); + this.DOM.dropdown[action]('mouseleave', _CBR.onMouseLeave); + this.DOM.dropdown[action]('mousedown', _CBR.onClick); + this.DOM.dropdown.content[action]('scroll', _CBR.onScroll); + }, + callbacks: { + onKeyDown(e) { + // ignore keys during IME composition + if (!this.state.hasFocus || this.state.composing) return; + + // get the "active" element, and if there was none (yet) active, use first child + var _s = this.settings, + selectedElm = this.DOM.dropdown.querySelector(_s.classNames.dropdownItemActiveSelector), + selectedElmData = this.dropdown.getSuggestionDataByNode(selectedElm), + isMixMode = _s.mode == 'mix'; + _s.hooks.beforeKeyDown(e, { + tagify: this + }).then(result => { + switch (e.key) { + case 'ArrowDown': + case 'ArrowUp': + case 'Down': // >IE11 + case 'Up': + { + // >IE11 + e.preventDefault(); + var dropdownItems = this.dropdown.getAllSuggestionsRefs(), + actionUp = e.key == 'ArrowUp' || e.key == 'Up'; + if (selectedElm) { + selectedElm = this.dropdown.getNextOrPrevOption(selectedElm, !actionUp); + } + + // if no element was found OR current item is not a "real" item, loop + if (!selectedElm || !selectedElm.matches(_s.classNames.dropdownItemSelector)) { + selectedElm = dropdownItems[actionUp ? dropdownItems.length - 1 : 0]; + } + this.dropdown.highlightOption(selectedElm, true); + // selectedElm.scrollIntoView({inline: 'nearest', behavior: 'smooth'}) + break; + } + case 'Escape': + case 'Esc': + // IE11 + this.dropdown.hide(); + break; + case 'ArrowRight': + if (this.state.actions.ArrowLeft) return; + case 'Tab': + { + let shouldAutocompleteOnKey = !_s.autoComplete.rightKey || !_s.autoComplete.tabKey; + + // in mix-mode, treat arrowRight like Enter key, so a tag will be created + if (!isMixMode && selectedElm && shouldAutocompleteOnKey && !this.state.editing) { + e.preventDefault(); // prevents blur so the autocomplete suggestion will not become a tag + var value = this.dropdown.getMappedValue(selectedElmData); + this.input.autocomplete.set.call(this, value); + return false; + } + return true; + } + case 'Enter': + { + e.preventDefault(); + _s.hooks.suggestionClick(e, { + tagify: this, + tagData: selectedElmData, + suggestionElm: selectedElm + }).then(() => { + if (selectedElm) { + this.dropdown.selectOption(selectedElm); + // highlight next option + selectedElm = this.dropdown.getNextOrPrevOption(selectedElm, !actionUp); + this.dropdown.highlightOption(selectedElm); + return; + } else this.dropdown.hide(); + if (!isMixMode) this.addTags(this.state.inputText.trim(), true); + }).catch(err => console.warn(err)); + break; + } + case 'Backspace': + { + if (isMixMode || this.state.editing.scope) return; + const value = this.input.raw.call(this); + if (value == "" || value.charCodeAt(0) == 8203) { + if (_s.backspace === true) this.removeTags();else if (_s.backspace == 'edit') setTimeout(this.editTag.bind(this), 0); + } + } + } + }); + }, + onMouseOver(e) { + var ddItem = e.target.closest(this.settings.classNames.dropdownItemSelector); + // event delegation check + this.dropdown.highlightOption(ddItem); + }, + onMouseLeave(e) { + // de-highlight any previously highlighted option + this.dropdown.highlightOption(); + }, + onClick(e) { + if (e.button != 0 || e.target == this.DOM.dropdown || e.target == this.DOM.dropdown.content) return; // allow only mouse left-clicks + + var selectedElm = e.target.closest(this.settings.classNames.dropdownItemSelector), + selectedElmData = this.dropdown.getSuggestionDataByNode(selectedElm); + + // 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); + this.settings.hooks.suggestionClick(e, { + tagify: this, + tagData: selectedElmData, + suggestionElm: selectedElm + }).then(() => { + if (selectedElm) this.dropdown.selectOption(selectedElm, e);else this.dropdown.hide(); + }).catch(err => console.warn(err)); + }, + onScroll(e) { + var elm = e.target, + pos = elm.scrollTop / (elm.scrollHeight - elm.parentNode.clientHeight) * 100; + this.trigger("dropdown:scroll", { + percentage: Math.round(pos) + }); + } + } }, - getHeaderRef() { - return this.DOM.dropdown.querySelector("[data-selector='tagify-suggestions-header']"); + /** + * fill data into the suggestions list + * (mainly used to update the list when removing tags while the suggestions dropdown is visible, so they will be re-added to the list. not efficient) + */ + refilter(value) { + value = value || this.state.dropdown.query || ''; + this.suggestedListItems = this.dropdown.filterListItems(value); + this.dropdown.fill(); + if (!this.suggestedListItems.length) this.dropdown.hide(); + this.trigger("dropdown:updated", this.DOM.dropdown); }, - getFooterRef() { - return this.DOM.dropdown.querySelector("[data-selector='tagify-suggestions-footer']"); + /** + * Given a suggestion-item, return the data associated with it + * @param {HTMLElement} tagElm + * @returns Object + */ + getSuggestionDataByNode(tagElm) { + var value = tagElm && tagElm.getAttribute('value'); + return this.suggestedListItems.find(item => item.value == value) || null; }, - getAllSuggestionsRefs() { - return [...this.DOM.dropdown.content.querySelectorAll(this.settings.classNames.dropdownItemSelector)]; + getNextOrPrevOption(selected) { + let next = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; + var dropdownItems = this.dropdown.getAllSuggestionsRefs(), + selectedIdx = dropdownItems.findIndex(item => item === selected); + return next ? dropdownItems[selectedIdx + 1] : dropdownItems[selectedIdx - 1]; }, /** - * shows the suggestions select box - * @param {String} value [optional, filter the whitelist by this value] + * mark the currently active suggestion option + * @param {Object} elm option DOM node + * @param {Boolean} adjustScroll when navigation with keyboard arrows (up/down), aut-scroll to always show the highlighted element */ - show(value) { - var _s = this.settings, - firstListItem, - firstListItemValue, - allowNewTags = _s.mode == 'mix' && !_s.enforceWhitelist, - noWhitelist = !_s.whitelist || !_s.whitelist.length, - noMatchListItem, - isManual = _s.dropdown.position == 'manual'; + highlightOption(elm, adjustScroll) { + var className = this.settings.classNames.dropdownItemActive, + itemData; - // if text still exists in the input, and `show` method has no argument, then the input's text should be used - value = value === undefined ? this.state.inputText : value; + // focus casues a bug in Firefox with the placeholder been shown on the input element + // if( this.settings.dropdown.position != 'manual' ) + // elm.focus(); - // ⚠️ Do not render suggestions list if: - // 1. there's no whitelist (can happen while async loading) AND new tags arn't allowed - // 2. dropdown is disabled - // 3. loader is showing (controlled outside of this code) - if (noWhitelist && !allowNewTags && !_s.templates.dropdownItemNoMatch || _s.dropdown.enable === false || this.state.isLoading || this.settings.readonly) return; - clearTimeout(this.dropdownHide__bindEventsTimeout); + if (this.state.ddItemElm) { + this.state.ddItemElm.classList.remove(className); + this.state.ddItemElm.removeAttribute("aria-selected"); + } + if (!elm) { + this.state.ddItemData = null; + this.state.ddItemElm = null; + this.input.autocomplete.suggest.call(this); + return; + } + itemData = this.dropdown.getSuggestionDataByNode(elm); + this.state.ddItemData = itemData; + this.state.ddItemElm = elm; - // if no value was supplied, show all the "whitelist" items in the dropdown - // @type [Array] listItems - this.suggestedListItems = this.dropdown.filterListItems(value); + // this.DOM.dropdown.querySelectorAll("." + this.settings.classNames.dropdownItemActive).forEach(activeElm => activeElm.classList.remove(className)); + elm.classList.add(className); + elm.setAttribute("aria-selected", true); + if (adjustScroll) elm.parentNode.scrollTop = elm.clientHeight + elm.offsetTop - elm.parentNode.clientHeight; - // trigger at this exact point to let the developer the chance to manually set "this.suggestedListItems" - if (value && !this.suggestedListItems.length) { - this.trigger('dropdown:noMatch', value); - if (_s.templates.dropdownItemNoMatch) noMatchListItem = _s.templates.dropdownItemNoMatch.call(this, { - value - }); + // Try to autocomplete the typed value with the currently highlighted dropdown item + if (this.settings.autoComplete) { + this.input.autocomplete.suggest.call(this, itemData); + this.dropdown.position(); // suggestions might alter the height of the tagify wrapper because of unkown suggested term length that could drop to the next line } - - // if "dropdownItemNoMatch" was not defined, procceed regular flow. - // - if (!noMatchListItem) { - // in mix-mode, if the value isn't included in the whilelist & "enforceWhitelist" setting is "false", - // then add a custom suggestion item to the dropdown - if (this.suggestedListItems.length) { - if (value && allowNewTags && !this.state.editing.scope && !sameStr(this.suggestedListItems[0].value, value)) this.suggestedListItems.unshift({ - value - }); - } else { - if (value && allowNewTags && !this.state.editing.scope) { - this.suggestedListItems = [{ - value - }]; - } - // hide suggestions list if no suggestion matched - else { - this.input.autocomplete.suggest.call(this); - this.dropdown.hide(); - return; - } - } - firstListItem = this.suggestedListItems[0]; - firstListItemValue = "" + (isObject(firstListItem) ? firstListItem.value : firstListItem); - if (_s.autoComplete && firstListItemValue) { - // only fill the sugegstion if the value of the first list item STARTS with the input value (regardless of "fuzzysearch" setting) - if (firstListItemValue.indexOf(value) == 0) this.input.autocomplete.suggest.call(this, firstListItem); - } - } - this.dropdown.fill(noMatchListItem); - if (_s.dropdown.highlightFirst) { - this.dropdown.highlightOption(this.DOM.dropdown.content.querySelector(_s.classNames.dropdownItemSelector)); - } - - // bind events, exactly at this stage of the code. "dropdown.show" method is allowed to be - // called multiple times, regardless if the dropdown is currently visible, but the events-binding - // should only be called if the dropdown wasn't previously visible. - if (!this.state.dropdown.visible) - // 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 - setTimeout(this.dropdown.events.binding.bind(this)); - - // set the dropdown visible state to be the same as the searched value. - // MUST be set *before* position() is called - this.state.dropdown.visible = value || true; - this.state.dropdown.query = value; - this.setStateSelection(); - - // try to positioning the dropdown (it might not yet be on the page, doesn't matter, next code handles this) - if (!isManual) { - // a slight delay is needed if the dropdown "position" setting is "text", and nothing was typed in the input, - // so sadly the "getCaretGlobalPosition" method doesn't recognize the caret position without this delay - setTimeout(() => { - this.dropdown.position(); - this.dropdown.render(); - }); - } - - // a delay is needed because of the previous delay reason. - // this event must be fired after the dropdown was rendered & positioned - setTimeout(() => { - this.trigger("dropdown:show", this.DOM.dropdown); - }); - }, - /** - * Hides the dropdown (if it's not managed manually by the developer) - * @param {Boolean} overrideManual - */ - hide(overrideManual) { - var _this$DOM = this.DOM, - scope = _this$DOM.scope, - dropdown = _this$DOM.dropdown, - isManual = this.settings.dropdown.position == 'manual' && !overrideManual; - - // if there's no dropdown, this means the dropdown events aren't binded - if (!dropdown || !document.body.contains(dropdown) || isManual) return; - window.removeEventListener('resize', this.dropdown.position); - this.dropdown.events.binding.call(this, false); // unbind all events - - // if the dropdown is open, and the input (scope) is clicked, - // the dropdown should be now "close", and the next click (on the scope) - // should re-open it, and without a timeout, clicking to close will re-open immediately - // clearTimeout(this.dropdownHide__bindEventsTimeout) - // this.dropdownHide__bindEventsTimeout = setTimeout(this.events.binding.bind(this), 250) // re-bind main events - - scope.setAttribute("aria-expanded", false); - dropdown.parentNode.removeChild(dropdown); - - // scenario: clicking the scope to show the dropdown, clicking again to hide -> calls dropdown.hide() and then re-focuses the input - // which casues another onFocus event, which checked "this.state.dropdown.visible" and see it as "false" and re-open the dropdown - setTimeout(() => { - this.state.dropdown.visible = false; - }, 100); - this.state.dropdown.query = this.state.ddItemData = this.state.ddItemElm = this.state.selection = null; - - // if the user closed the dropdown (in mix-mode) while a potential tag was detected, flag the current tag - // so the dropdown won't be shown on following user input for that "tag" - if (this.state.tag && this.state.tag.value.length) { - this.state.flaggedTags[this.state.tag.baseOffset] = this.state.tag; - } - this.trigger("dropdown:hide", dropdown); - return this; - }, - /** - * Toggles dropdown show/hide - * @param {Boolean} show forces the dropdown to show - */ - toggle(show) { - this.dropdown[this.state.dropdown.visible && !show ? 'hide' : 'show'](); - }, - getAppendTarget() { - var _sd = this.settings.dropdown; - return typeof _sd.appendTarget === 'function' ? _sd.appendTarget() : _sd.appendTarget; - }, - render() { - // let the element render in the DOM first, to accurately measure it. - // this.DOM.dropdown.style.cssText = "left:-9999px; top:-9999px;"; - var ddHeight = getNodeHeight(this.DOM.dropdown), - _s = this.settings, - enabled = typeof _s.dropdown.enabled == 'number' && _s.dropdown.enabled >= 0, - appendTarget = this.dropdown.getAppendTarget(); - if (!enabled) return this; - this.DOM.scope.setAttribute("aria-expanded", true); - - // if the dropdown has yet to be appended to the DOM, - // append the dropdown to the body element & handle events - if (!document.body.contains(this.DOM.dropdown)) { - this.DOM.dropdown.classList.add(_s.classNames.dropdownInital); - this.dropdown.position(ddHeight); - appendTarget.appendChild(this.DOM.dropdown); - setTimeout(() => this.DOM.dropdown.classList.remove(_s.classNames.dropdownInital)); - } - return this; - }, - /** - * re-renders the dropdown content element (see "dropdownContent" in templates file) - * @param {String/Array} HTMLContent - optional - */ - fill(HTMLContent) { - HTMLContent = typeof HTMLContent == 'string' ? HTMLContent : this.dropdown.createListHTML(HTMLContent || this.suggestedListItems); - var dropdownContent = this.settings.templates.dropdownContent.call(this, HTMLContent); - this.DOM.dropdown.content.innerHTML = minify(dropdownContent); - }, - /** - * Re-renders only the header & footer. - * Used when selecting a suggestion and it is wanted that the suggestions dropdown stays open. - * Since the list of sugegstions is not being re-rendered completely every time a suggestion is selected (the item is transitioned-out) - * then the header & footer should be kept in sync with the suggestions data change - */ - fillHeaderFooter() { - var suggestions = this.dropdown.filterListItems(this.state.dropdown.query), - newHeaderElem = this.parseTemplate('dropdownHeader', [suggestions]), - newFooterElem = this.parseTemplate('dropdownFooter', [suggestions]), - headerRef = this.dropdown.getHeaderRef(), - footerRef = this.dropdown.getFooterRef(); - newHeaderElem && headerRef?.parentNode.replaceChild(newHeaderElem, headerRef); - newFooterElem && footerRef?.parentNode.replaceChild(newFooterElem, footerRef); - }, - /** - * fill data into the suggestions list - * (mainly used to update the list when removing tags while the suggestions dropdown is visible, so they will be re-added to the list. not efficient) - */ - refilter(value) { - value = value || this.state.dropdown.query || ''; - this.suggestedListItems = this.dropdown.filterListItems(value); - this.dropdown.fill(); - if (!this.suggestedListItems.length) this.dropdown.hide(); - this.trigger("dropdown:updated", this.DOM.dropdown); - }, - position(ddHeight) { - var _sd = this.settings.dropdown, - appendTarget = this.dropdown.getAppendTarget(); - if (_sd.position == 'manual' || !appendTarget) return; - var rect, - top, - bottom, - left, - width, - ancestorsOffsets, - isPlacedAbove, - cssTop, - cssLeft, - ddElm = this.DOM.dropdown, - isRTL = _sd.RTL, - isDefaultAppendTarget = appendTarget === document.body, - isSelfAppended = appendTarget === this.DOM.scope, - appendTargetScrollTop = isDefaultAppendTarget ? window.pageYOffset : appendTarget.scrollTop, - root = document.fullscreenElement || document.webkitFullscreenElement || document.documentElement, - viewportHeight = root.clientHeight, - viewportWidth = Math.max(root.clientWidth || 0, window.innerWidth || 0), - positionTo = viewportWidth > 480 ? _sd.position : 'all', - ddTarget = this.DOM[positionTo == 'input' ? 'input' : 'scope']; - ddHeight = ddHeight || ddElm.clientHeight; - function getAncestorsOffsets(p) { - var top = 0, - left = 0; - p = p.parentNode; - - // when in element-fullscreen mode, do not go above the fullscreened-element - while (p && p != root) { - top += p.offsetTop || 0; - left += p.offsetLeft || 0; - p = p.parentNode; - } - return { - top, - left - }; - } - function getAccumulatedAncestorsScrollTop() { - var scrollTop = 0, - p = _sd.appendTarget.parentNode; - while (p) { - scrollTop += p.scrollTop || 0; - p = p.parentNode; - } - return scrollTop; - } - if (!this.state.dropdown.visible) return; - if (positionTo == 'text') { - rect = getCaretGlobalPosition(); - bottom = rect.bottom; - top = rect.top; - left = rect.left; - width = 'auto'; - } else { - ancestorsOffsets = getAncestorsOffsets(appendTarget); - rect = ddTarget.getBoundingClientRect(); - top = isSelfAppended ? -1 : rect.top - ancestorsOffsets.top; - bottom = (isSelfAppended ? rect.height : rect.bottom - ancestorsOffsets.top) - 1; - left = isSelfAppended ? -1 : rect.left - ancestorsOffsets.left; - width = rect.width + 'px'; - } - - // if the "append target" isn't the default, correct the `top` variable by ignoring any scrollTop of the target's Ancestors - if (!isDefaultAppendTarget) { - let accumulatedAncestorsScrollTop = getAccumulatedAncestorsScrollTop(); - top += accumulatedAncestorsScrollTop; - bottom += accumulatedAncestorsScrollTop; - } - top = Math.floor(top); - bottom = Math.ceil(bottom); - isPlacedAbove = _sd.placeAbove ?? viewportHeight - rect.bottom < ddHeight; - - // flip vertically if there is no space for the dropdown below the input - cssTop = (isPlacedAbove ? top : bottom) + appendTargetScrollTop; - - // "pageXOffset" property is an alias for "scrollX" - cssLeft = `left: ${left + (isRTL ? rect.width || 0 : 0) + window.pageXOffset}px;`; - - // rtl = rtl ?? viewportWidth - - ddElm.style.cssText = `${cssLeft}; top: ${cssTop}px; min-width: ${width}; max-width: ${width}`; - ddElm.setAttribute('placement', isPlacedAbove ? 'top' : 'bottom'); - ddElm.setAttribute('position', positionTo); - }, - events: { - /** - * Events should only be binded when the dropdown is rendered and removed when isn't - * because there might be multiple Tagify instances on a certain page - * @param {Boolean} bindUnbind [optional. true when wanting to unbind all the events] - */ - binding() { - let bindUnbind = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; - // references to the ".bind()" methods must be saved so they could be unbinded later - var _CB = this.dropdown.events.callbacks, - // callback-refs - _CBR = this.listeners.dropdown = this.listeners.dropdown || { - position: this.dropdown.position.bind(this, null), - onKeyDown: _CB.onKeyDown.bind(this), - onMouseOver: _CB.onMouseOver.bind(this), - onMouseLeave: _CB.onMouseLeave.bind(this), - onClick: _CB.onClick.bind(this), - onScroll: _CB.onScroll.bind(this) - }, - action = bindUnbind ? 'addEventListener' : 'removeEventListener'; - if (this.settings.dropdown.position != 'manual') { - document[action]('scroll', _CBR.position, true); - window[action]('resize', _CBR.position); - window[action]('keydown', _CBR.onKeyDown); - } - this.DOM.dropdown[action]('mouseover', _CBR.onMouseOver); - this.DOM.dropdown[action]('mouseleave', _CBR.onMouseLeave); - this.DOM.dropdown[action]('mousedown', _CBR.onClick); - this.DOM.dropdown.content[action]('scroll', _CBR.onScroll); - }, - callbacks: { - onKeyDown(e) { - // ignore keys during IME composition - if (!this.state.hasFocus || this.state.composing) return; - - // get the "active" element, and if there was none (yet) active, use first child - var _s = this.settings, - selectedElm = this.DOM.dropdown.querySelector(_s.classNames.dropdownItemActiveSelector), - selectedElmData = this.dropdown.getSuggestionDataByNode(selectedElm), - isMixMode = _s.mode == 'mix'; - _s.hooks.beforeKeyDown(e, { - tagify: this - }).then(result => { - switch (e.key) { - case 'ArrowDown': - case 'ArrowUp': - case 'Down': // >IE11 - case 'Up': - { - // >IE11 - e.preventDefault(); - var dropdownItems = this.dropdown.getAllSuggestionsRefs(), - actionUp = e.key == 'ArrowUp' || e.key == 'Up'; - if (selectedElm) { - selectedElm = this.dropdown.getNextOrPrevOption(selectedElm, !actionUp); - } - - // if no element was found OR current item is not a "real" item, loop - if (!selectedElm || !selectedElm.matches(_s.classNames.dropdownItemSelector)) { - selectedElm = dropdownItems[actionUp ? dropdownItems.length - 1 : 0]; - } - this.dropdown.highlightOption(selectedElm, true); - // selectedElm.scrollIntoView({inline: 'nearest', behavior: 'smooth'}) - break; - } - case 'Escape': - case 'Esc': - // IE11 - this.dropdown.hide(); - break; - case 'ArrowRight': - if (this.state.actions.ArrowLeft) return; - case 'Tab': - { - let shouldAutocompleteOnKey = !_s.autoComplete.rightKey || !_s.autoComplete.tabKey; - - // in mix-mode, treat arrowRight like Enter key, so a tag will be created - if (!isMixMode && selectedElm && shouldAutocompleteOnKey && !this.state.editing) { - e.preventDefault(); // prevents blur so the autocomplete suggestion will not become a tag - var value = this.dropdown.getMappedValue(selectedElmData); - this.input.autocomplete.set.call(this, value); - return false; - } - return true; - } - case 'Enter': - { - e.preventDefault(); - _s.hooks.suggestionClick(e, { - tagify: this, - tagData: selectedElmData, - suggestionElm: selectedElm - }).then(() => { - if (selectedElm) { - this.dropdown.selectOption(selectedElm); - // highlight next option - selectedElm = this.dropdown.getNextOrPrevOption(selectedElm, !actionUp); - this.dropdown.highlightOption(selectedElm); - return; - } else this.dropdown.hide(); - if (!isMixMode) this.addTags(this.state.inputText.trim(), true); - }).catch(err => err); - break; - } - case 'Backspace': - { - if (isMixMode || this.state.editing.scope) return; - const value = this.input.raw.call(this); - if (value == "" || value.charCodeAt(0) == 8203) { - if (_s.backspace === true) this.removeTags();else if (_s.backspace == 'edit') setTimeout(this.editTag.bind(this), 0); - } - } - } - }); - }, - onMouseOver(e) { - var ddItem = e.target.closest(this.settings.classNames.dropdownItemSelector); - // event delegation check - this.dropdown.highlightOption(ddItem); - }, - onMouseLeave(e) { - // de-highlight any previously highlighted option - this.dropdown.highlightOption(); - }, - onClick(e) { - if (e.button != 0 || e.target == this.DOM.dropdown || e.target == this.DOM.dropdown.content) return; // allow only mouse left-clicks - - var selectedElm = e.target.closest(this.settings.classNames.dropdownItemSelector), - selectedElmData = this.dropdown.getSuggestionDataByNode(selectedElm); - - // 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); - this.settings.hooks.suggestionClick(e, { - tagify: this, - tagData: selectedElmData, - suggestionElm: selectedElm - }).then(() => { - if (selectedElm) this.dropdown.selectOption(selectedElm, e);else this.dropdown.hide(); - }).catch(err => console.warn(err)); - }, - onScroll(e) { - var elm = e.target, - pos = elm.scrollTop / (elm.scrollHeight - elm.parentNode.clientHeight) * 100; - this.trigger("dropdown:scroll", { - percentage: Math.round(pos) - }); - } - } - }, - /** - * Given a suggestion-item, return the data associated with it - * @param {HTMLElement} tagElm - * @returns Object - */ - getSuggestionDataByNode(tagElm) { - var value = tagElm && tagElm.getAttribute('value'); - return this.suggestedListItems.find(item => item.value == value) || null; - }, - getNextOrPrevOption(selected) { - let next = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; - var dropdownItems = this.dropdown.getAllSuggestionsRefs(), - selectedIdx = dropdownItems.findIndex(item => item === selected); - return next ? dropdownItems[selectedIdx + 1] : dropdownItems[selectedIdx - 1]; - }, - /** - * mark the currently active suggestion option - * @param {Object} elm option DOM node - * @param {Boolean} adjustScroll when navigation with keyboard arrows (up/down), aut-scroll to always show the highlighted element - */ - highlightOption(elm, adjustScroll) { - var className = this.settings.classNames.dropdownItemActive, - itemData; - - // focus casues a bug in Firefox with the placeholder been shown on the input element - // if( this.settings.dropdown.position != 'manual' ) - // elm.focus(); - - if (this.state.ddItemElm) { - this.state.ddItemElm.classList.remove(className); - this.state.ddItemElm.removeAttribute("aria-selected"); - } - if (!elm) { - this.state.ddItemData = null; - this.state.ddItemElm = null; - this.input.autocomplete.suggest.call(this); - return; - } - itemData = this.dropdown.getSuggestionDataByNode(elm); - this.state.ddItemData = itemData; - this.state.ddItemElm = elm; - - // this.DOM.dropdown.querySelectorAll("." + this.settings.classNames.dropdownItemActive).forEach(activeElm => activeElm.classList.remove(className)); - elm.classList.add(className); - elm.setAttribute("aria-selected", true); - if (adjustScroll) elm.parentNode.scrollTop = elm.clientHeight + elm.offsetTop - elm.parentNode.clientHeight; - - // Try to autocomplete the typed value with the currently highlighted dropdown item - if (this.settings.autoComplete) { - this.input.autocomplete.suggest.call(this, itemData); - this.dropdown.position(); // suggestions might alter the height of the tagify wrapper because of unkown suggested term length that could drop to the next line - } - }, + }, /** * Create a tag from the currently active suggestion option @@ -998,6 +704,7 @@ var _dropdown = { var value = elm.getAttribute('value'), isNoMatch = value == 'noMatch', + isMixMode = _s.mode == 'mix', tagData = this.suggestedListItems.find(item => (item.value ?? item) == value); // The below event must be triggered, regardless of anything else which might go wrong @@ -1021,11 +728,9 @@ var _dropdown = { } // Tagify instances should re-focus to the input element once an option was selected, to allow continuous typing else { - this[_s.mode == 'mix' ? "addMixTags" : "addTags"]([tagData || this.input.raw.call(this)], clearOnSelect); + this[isMixMode ? "addMixTags" : "addTags"]([tagData || this.input.raw.call(this)], clearOnSelect); } - - // todo: consider not doing this on mix-mode - if (!this.DOM.input.parentNode) return; + if (!isMixMode && !this.DOM.input.parentNode) return; setTimeout(() => { this.DOM.input.focus(); this.toggleFocusClass(true); @@ -1087,81 +792,387 @@ var _dropdown = { return list.slice(0, suggestionsCount); // respect "maxItems" dropdown setting } - niddle = _sd.caseSensitive ? "" + value : ("" + value).toLowerCase(); + niddle = _sd.caseSensitive ? "" + value : ("" + value).toLowerCase(); + + // checks if ALL of the words in the search query exists in the current whitelist item, regardless of their order + function stringHasAll(s, query) { + return query.toLowerCase().split(' ').every(q => s.includes(q.toLowerCase())); + } + for (; i < whitelist.length; i++) { + let startsWithMatch, exactMatch; + whitelistItem = whitelist[i] instanceof Object ? whitelist[i] : { + value: whitelist[i] + }; //normalize value as an Object + + let itemWithoutSearchKeys = !Object.keys(whitelistItem).some(k => searchKeys.includes(k)), + _searchKeys = itemWithoutSearchKeys ? ["value"] : searchKeys; + if (_sd.fuzzySearch && !options.exact) { + searchBy = _searchKeys.reduce((values, k) => values + " " + (whitelistItem[k] || ""), "").toLowerCase().trim(); + if (_sd.accentedSearch) { + searchBy = unaccent(searchBy); + niddle = unaccent(niddle); + } + startsWithMatch = searchBy.indexOf(niddle) == 0; + exactMatch = searchBy === niddle; + valueIsInWhitelist = stringHasAll(searchBy, niddle); + } else { + startsWithMatch = true; + valueIsInWhitelist = _searchKeys.some(k => { + var v = '' + (whitelistItem[k] || ''); // if key exists, cast to type String + + if (_sd.accentedSearch) { + v = unaccent(v); + niddle = unaccent(niddle); + } + if (!_sd.caseSensitive) v = v.toLowerCase(); + exactMatch = v === niddle; + return options.exact ? v === niddle : v.indexOf(niddle) == 0; + }); + } + isDuplicate = !_sd.includeSelectedTags && this.isTagDuplicate(isObject(whitelistItem) ? whitelistItem.value : whitelistItem); + + // match for the value within each "whitelist" item + if (valueIsInWhitelist && !isDuplicate) if (exactMatch && startsWithMatch) exactMatchesList.push(whitelistItem);else if (_sd.sortby == 'startsWith' && startsWithMatch) list.unshift(whitelistItem);else list.push(whitelistItem); + } + this.state.dropdown.suggestions = exactMatchesList.concat(list); + + // custom sorting function + return typeof _sd.sortby == 'function' ? _sd.sortby(exactMatchesList.concat(list), niddle) : exactMatchesList.concat(list).slice(0, suggestionsCount); + }, + /** + * Returns the final value of a tag data (object) with regards to the "mapValueTo" dropdown setting + * @param {Object} tagData + * @returns + */ + getMappedValue(tagData) { + var mapValueTo = this.settings.dropdown.mapValueTo, + value = mapValueTo ? typeof mapValueTo == 'function' ? mapValueTo(tagData) : tagData[mapValueTo] || tagData.value : tagData.value; + return value; + }, + /** + * Creates the dropdown items' HTML + * @param {Array} sugegstionsList [Array of Objects] + * @return {String} + */ + createListHTML(sugegstionsList) { + return extend([], sugegstionsList).map((suggestion, idx) => { + if (typeof suggestion == 'string' || typeof suggestion == 'number') suggestion = { + value: suggestion + }; + var mappedValue = this.dropdown.getMappedValue(suggestion); + mappedValue = typeof mappedValue == 'string' && this.settings.dropdown.escapeHTML ? escapeHTML(mappedValue) : mappedValue; + return this.settings.templates.dropdownItem.apply(this, [_objectSpread2(_objectSpread2({}, suggestion), {}, { + mappedValue + }), this]); + }).join(""); + } +}; + +function initDropdown() { + this.dropdown = {}; + + // auto-bind "this" to all the dropdown methods + for (let p in this._dropdown) this.dropdown[p] = typeof this._dropdown[p] === 'function' ? this._dropdown[p].bind(this) : this._dropdown[p]; + this.dropdown.refs(); +} +var _dropdown = _objectSpread2(_objectSpread2({}, suggestionsMethods), {}, { + refs() { + this.DOM.dropdown = this.parseTemplate('dropdown', [this.settings]); + this.DOM.dropdown.content = this.DOM.dropdown.querySelector("[data-selector='tagify-suggestions-wrapper']"); + }, + getHeaderRef() { + return this.DOM.dropdown.querySelector("[data-selector='tagify-suggestions-header']"); + }, + getFooterRef() { + return this.DOM.dropdown.querySelector("[data-selector='tagify-suggestions-footer']"); + }, + getAllSuggestionsRefs() { + return [...this.DOM.dropdown.content.querySelectorAll(this.settings.classNames.dropdownItemSelector)]; + }, + /** + * shows the suggestions select box + * @param {String} value [optional, filter the whitelist by this value] + */ + show(value) { + var _s = this.settings, + firstListItem, + firstListItemValue, + allowNewTags = _s.mode == 'mix' && !_s.enforceWhitelist, + noWhitelist = !_s.whitelist || !_s.whitelist.length, + noMatchListItem, + isManual = _s.dropdown.position == 'manual'; + + // if text still exists in the input, and `show` method has no argument, then the input's text should be used + value = value === undefined ? this.state.inputText : value; + + // ⚠️ Do not render suggestions list if: + // 1. there's no whitelist (can happen while async loading) AND new tags arn't allowed + // 2. dropdown is disabled + // 3. loader is showing (controlled outside of this code) + if (noWhitelist && !allowNewTags && !_s.templates.dropdownItemNoMatch || _s.dropdown.enable === false || this.state.isLoading || this.settings.readonly) return; + clearTimeout(this.dropdownHide__bindEventsTimeout); + + // if no value was supplied, show all the "whitelist" items in the dropdown + // @type [Array] listItems + this.suggestedListItems = this.dropdown.filterListItems(value); + + // trigger at this exact point to let the developer the chance to manually set "this.suggestedListItems" + if (value && !this.suggestedListItems.length) { + this.trigger('dropdown:noMatch', value); + if (_s.templates.dropdownItemNoMatch) noMatchListItem = _s.templates.dropdownItemNoMatch.call(this, { + value + }); + } + + // if "dropdownItemNoMatch" was not defined, procceed regular flow. + // + if (!noMatchListItem) { + // in mix-mode, if the value isn't included in the whilelist & "enforceWhitelist" setting is "false", + // then add a custom suggestion item to the dropdown + if (this.suggestedListItems.length) { + if (value && allowNewTags && !this.state.editing.scope && !sameStr(this.suggestedListItems[0].value, value)) this.suggestedListItems.unshift({ + value + }); + } else { + if (value && allowNewTags && !this.state.editing.scope) { + this.suggestedListItems = [{ + value + }]; + } + // hide suggestions list if no suggestion matched + else { + this.input.autocomplete.suggest.call(this); + this.dropdown.hide(); + return; + } + } + firstListItem = this.suggestedListItems[0]; + firstListItemValue = "" + (isObject(firstListItem) ? firstListItem.value : firstListItem); + if (_s.autoComplete && firstListItemValue) { + // only fill the sugegstion if the value of the first list item STARTS with the input value (regardless of "fuzzysearch" setting) + if (firstListItemValue.indexOf(value) == 0) this.input.autocomplete.suggest.call(this, firstListItem); + } + } + this.dropdown.fill(noMatchListItem); + if (_s.dropdown.highlightFirst) { + this.dropdown.highlightOption(this.DOM.dropdown.content.querySelector(_s.classNames.dropdownItemSelector)); + } + + // bind events, exactly at this stage of the code. "dropdown.show" method is allowed to be + // called multiple times, regardless if the dropdown is currently visible, but the events-binding + // should only be called if the dropdown wasn't previously visible. + if (!this.state.dropdown.visible) + // 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 + setTimeout(this.dropdown.events.binding.bind(this)); + + // set the dropdown visible state to be the same as the searched value. + // MUST be set *before* position() is called + this.state.dropdown.visible = value || true; + this.state.dropdown.query = value; + this.setStateSelection(); + + // try to positioning the dropdown (it might not yet be on the page, doesn't matter, next code handles this) + if (!isManual) { + // a slight delay is needed if the dropdown "position" setting is "text", and nothing was typed in the input, + // so sadly the "getCaretGlobalPosition" method doesn't recognize the caret position without this delay + setTimeout(() => { + this.dropdown.position(); + this.dropdown.render(); + }); + } + + // a delay is needed because of the previous delay reason. + // this event must be fired after the dropdown was rendered & positioned + setTimeout(() => { + this.trigger("dropdown:show", this.DOM.dropdown); + }); + }, + /** + * Hides the dropdown (if it's not managed manually by the developer) + * @param {Boolean} overrideManual + */ + hide(overrideManual) { + var _this$DOM = this.DOM, + scope = _this$DOM.scope, + dropdown = _this$DOM.dropdown, + isManual = this.settings.dropdown.position == 'manual' && !overrideManual; + + // if there's no dropdown, this means the dropdown events aren't binded + if (!dropdown || !document.body.contains(dropdown) || isManual) return; + window.removeEventListener('resize', this.dropdown.position); + this.dropdown.events.binding.call(this, false); // unbind all events - // checks if ALL of the words in the search query exists in the current whitelist item, regardless of their order - function stringHasAll(s, query) { - return query.toLowerCase().split(' ').every(q => s.includes(q.toLowerCase())); - } - for (; i < whitelist.length; i++) { - let startsWithMatch, exactMatch; - whitelistItem = whitelist[i] instanceof Object ? whitelist[i] : { - value: whitelist[i] - }; //normalize value as an Object + // if the dropdown is open, and the input (scope) is clicked, + // the dropdown should be now "close", and the next click (on the scope) + // should re-open it, and without a timeout, clicking to close will re-open immediately + // clearTimeout(this.dropdownHide__bindEventsTimeout) + // this.dropdownHide__bindEventsTimeout = setTimeout(this.events.binding.bind(this), 250) // re-bind main events - let itemWithoutSearchKeys = !Object.keys(whitelistItem).some(k => searchKeys.includes(k)), - _searchKeys = itemWithoutSearchKeys ? ["value"] : searchKeys; - if (_sd.fuzzySearch && !options.exact) { - searchBy = _searchKeys.reduce((values, k) => values + " " + (whitelistItem[k] || ""), "").toLowerCase().trim(); - if (_sd.accentedSearch) { - searchBy = unaccent(searchBy); - niddle = unaccent(niddle); - } - startsWithMatch = searchBy.indexOf(niddle) == 0; - exactMatch = searchBy === niddle; - valueIsInWhitelist = stringHasAll(searchBy, niddle); - } else { - startsWithMatch = true; - valueIsInWhitelist = _searchKeys.some(k => { - var v = '' + (whitelistItem[k] || ''); // if key exists, cast to type String + scope.setAttribute("aria-expanded", false); + dropdown.parentNode.removeChild(dropdown); - if (_sd.accentedSearch) { - v = unaccent(v); - niddle = unaccent(niddle); - } - if (!_sd.caseSensitive) v = v.toLowerCase(); - exactMatch = v === niddle; - return options.exact ? v === niddle : v.indexOf(niddle) == 0; - }); - } - isDuplicate = !_sd.includeSelectedTags && this.isTagDuplicate(isObject(whitelistItem) ? whitelistItem.value : whitelistItem); + // scenario: clicking the scope to show the dropdown, clicking again to hide -> calls dropdown.hide() and then re-focuses the input + // which casues another onFocus event, which checked "this.state.dropdown.visible" and see it as "false" and re-open the dropdown + setTimeout(() => { + this.state.dropdown.visible = false; + }, 100); + this.state.dropdown.query = this.state.ddItemData = this.state.ddItemElm = this.state.selection = null; - // match for the value within each "whitelist" item - if (valueIsInWhitelist && !isDuplicate) if (exactMatch && startsWithMatch) exactMatchesList.push(whitelistItem);else if (_sd.sortby == 'startsWith' && startsWithMatch) list.unshift(whitelistItem);else list.push(whitelistItem); + // if the user closed the dropdown (in mix-mode) while a potential tag was detected, flag the current tag + // so the dropdown won't be shown on following user input for that "tag" + if (this.state.tag && this.state.tag.value.length) { + this.state.flaggedTags[this.state.tag.baseOffset] = this.state.tag; } - this.state.dropdown.suggestions = exactMatchesList.concat(list); + this.trigger("dropdown:hide", dropdown); + return this; + }, + /** + * Toggles dropdown show/hide + * @param {Boolean} show forces the dropdown to show + */ + toggle(show) { + this.dropdown[this.state.dropdown.visible && !show ? 'hide' : 'show'](); + }, + getAppendTarget() { + var _sd = this.settings.dropdown; + return typeof _sd.appendTarget === 'function' ? _sd.appendTarget() : _sd.appendTarget; + }, + render() { + // let the element render in the DOM first, to accurately measure it. + // this.DOM.dropdown.style.cssText = "left:-9999px; top:-9999px;"; + var ddHeight = getNodeHeight(this.DOM.dropdown), + _s = this.settings, + enabled = typeof _s.dropdown.enabled == 'number' && _s.dropdown.enabled >= 0, + appendTarget = this.dropdown.getAppendTarget(); + if (!enabled) return this; + this.DOM.scope.setAttribute("aria-expanded", true); - // custom sorting function - return typeof _sd.sortby == 'function' ? _sd.sortby(exactMatchesList.concat(list), niddle) : exactMatchesList.concat(list).slice(0, suggestionsCount); + // if the dropdown has yet to be appended to the DOM, + // append the dropdown to the body element & handle events + if (!document.body.contains(this.DOM.dropdown)) { + this.DOM.dropdown.classList.add(_s.classNames.dropdownInital); + this.dropdown.position(ddHeight); + appendTarget.appendChild(this.DOM.dropdown); + setTimeout(() => this.DOM.dropdown.classList.remove(_s.classNames.dropdownInital)); + } + return this; }, /** - * Returns the final value of a tag data (object) with regards to the "mapValueTo" dropdown setting - * @param {Object} tagData - * @returns + * re-renders the dropdown content element (see "dropdownContent" in templates file) + * @param {String/Array} HTMLContent - optional */ - getMappedValue(tagData) { - var mapValueTo = this.settings.dropdown.mapValueTo, - value = mapValueTo ? typeof mapValueTo == 'function' ? mapValueTo(tagData) : tagData[mapValueTo] || tagData.value : tagData.value; - return value; + fill(HTMLContent) { + HTMLContent = typeof HTMLContent == 'string' ? HTMLContent : this.dropdown.createListHTML(HTMLContent || this.suggestedListItems); + var dropdownContent = this.settings.templates.dropdownContent.call(this, HTMLContent); + this.DOM.dropdown.content.innerHTML = minify(dropdownContent); }, /** - * Creates the dropdown items' HTML - * @param {Array} sugegstionsList [Array of Objects] - * @return {String} + * Re-renders only the header & footer. + * Used when selecting a suggestion and it is wanted that the suggestions dropdown stays open. + * Since the list of sugegstions is not being re-rendered completely every time a suggestion is selected (the item is transitioned-out) + * then the header & footer should be kept in sync with the suggestions data change */ - createListHTML(sugegstionsList) { - return extend([], sugegstionsList).map((suggestion, idx) => { - if (typeof suggestion == 'string' || typeof suggestion == 'number') suggestion = { - value: suggestion + fillHeaderFooter() { + var suggestions = this.dropdown.filterListItems(this.state.dropdown.query), + newHeaderElem = this.parseTemplate('dropdownHeader', [suggestions]), + newFooterElem = this.parseTemplate('dropdownFooter', [suggestions]), + headerRef = this.dropdown.getHeaderRef(), + footerRef = this.dropdown.getFooterRef(); + newHeaderElem && headerRef?.parentNode.replaceChild(newHeaderElem, headerRef); + newFooterElem && footerRef?.parentNode.replaceChild(newFooterElem, footerRef); + }, + /** + * dropdown positioning logic + * (shown above/below or next to typed text for mix-mode) + */ + position(ddHeight) { + var _sd = this.settings.dropdown, + appendTarget = this.dropdown.getAppendTarget(); + if (_sd.position == 'manual' || !appendTarget) return; + var rect, + top, + bottom, + left, + width, + ancestorsOffsets, + isPlacedAbove, + cssTop, + cssLeft, + ddElm = this.DOM.dropdown, + isRTL = _sd.RTL, + isDefaultAppendTarget = appendTarget === document.body, + isSelfAppended = appendTarget === this.DOM.scope, + appendTargetScrollTop = isDefaultAppendTarget ? window.pageYOffset : appendTarget.scrollTop, + root = document.fullscreenElement || document.webkitFullscreenElement || document.documentElement, + viewportHeight = root.clientHeight, + viewportWidth = Math.max(root.clientWidth || 0, window.innerWidth || 0), + positionTo = viewportWidth > 480 ? _sd.position : 'all', + ddTarget = this.DOM[positionTo == 'input' ? 'input' : 'scope']; + ddHeight = ddHeight || ddElm.clientHeight; + function getAncestorsOffsets(p) { + var top = 0, + left = 0; + p = p.parentNode; + + // when in element-fullscreen mode, do not go above the fullscreened-element + while (p && p != root) { + top += p.offsetTop || 0; + left += p.offsetLeft || 0; + p = p.parentNode; + } + return { + top, + left }; - var mappedValue = this.dropdown.getMappedValue(suggestion); - mappedValue = typeof mappedValue == 'string' && this.settings.dropdown.escapeHTML ? escapeHTML(mappedValue) : mappedValue; - return this.settings.templates.dropdownItem.apply(this, [_objectSpread2(_objectSpread2({}, suggestion), {}, { - mappedValue - }), this]); - }).join(""); + } + function getAccumulatedAncestorsScrollTop() { + var scrollTop = 0, + p = _sd.appendTarget.parentNode; + while (p) { + scrollTop += p.scrollTop || 0; + p = p.parentNode; + } + return scrollTop; + } + if (!this.state.dropdown.visible) return; + if (positionTo == 'text') { + rect = getCaretGlobalPosition(); + bottom = rect.bottom; + top = rect.top; + left = rect.left; + width = 'auto'; + } else { + ancestorsOffsets = getAncestorsOffsets(appendTarget); + rect = ddTarget.getBoundingClientRect(); + top = isSelfAppended ? -1 : rect.top - ancestorsOffsets.top; + bottom = (isSelfAppended ? rect.height : rect.bottom - ancestorsOffsets.top) - 1; + left = isSelfAppended ? -1 : rect.left - ancestorsOffsets.left; + width = rect.width + 'px'; + } + + // if the "append target" isn't the default, correct the `top` variable by ignoring any scrollTop of the target's Ancestors + if (!isDefaultAppendTarget) { + let accumulatedAncestorsScrollTop = getAccumulatedAncestorsScrollTop(); + top += accumulatedAncestorsScrollTop; + bottom += accumulatedAncestorsScrollTop; + } + top = Math.floor(top); + bottom = Math.ceil(bottom); + isPlacedAbove = _sd.placeAbove ?? viewportHeight - rect.bottom < ddHeight; + + // flip vertically if there is no space for the dropdown below the input + cssTop = (isPlacedAbove ? top : bottom) + appendTargetScrollTop; + + // "pageXOffset" property is an alias for "scrollX" + cssLeft = `left: ${left + (isRTL ? rect.width || 0 : 0) + window.pageXOffset}px;`; + + // rtl = rtl ?? viewportWidth - + ddElm.style.cssText = `${cssLeft}; top: ${cssTop}px; min-width: ${width}; max-width: ${width}`; + ddElm.setAttribute('placement', isPlacedAbove ? 'top' : 'bottom'); + ddElm.setAttribute('position', positionTo); } -}; +}); const VERSION = 1; // current version of persisted data. if code change breaks persisted data, verison number should be bumped. const STORE_KEY = '@yaireo/tagify/'; @@ -3282,6 +3293,50 @@ Tagify.prototype = { return true; }, + prepareNewTagNode(tagData, options) { + options = options || {}; + var tagElm, + _s = this.settings, + aggregatedInvalidInput = [], + tagElmParams = {}, + originalData = Object.assign({}, tagData, { + value: tagData.value + "" + }); + + // shallow-clone tagData so later modifications will not apply to the source + tagData = Object.assign({}, originalData); + _s.transformTag.call(this, tagData); + tagData.__isValid = this.hasMaxTags() || this.validateTag(tagData); + if (tagData.__isValid !== true) { + if (options.skipInvalid) return; + + // originalData is kept because it might be that this tag is invalid because it is a duplicate of another, + // and if that other tags is edited/deleted, this one should be re-validated and if is no more a duplicate - restored + extend(tagElmParams, this.getInvalidTagAttrs(tagData, tagData.__isValid), { + __preInvalidData: originalData + }); + if (tagData.__isValid == this.TEXTS.duplicate) + // mark, for a brief moment, the tag (this this one) which THIS CURRENT tag is a duplcate of + this.flashTag(this.getTagElmByValue(tagData.value)); + if (!_s.createInvalidTags) { + aggregatedInvalidInput.push(tagData.value); + return; + } + } + if ('readonly' in tagData) { + if (tagData.readonly) tagElmParams["aria-readonly"] = true; + // if "readonly" is "false", remove it from the tagData so it won't be added as an attribute in the template + else delete tagData.readonly; + } + + // Create tag HTML element + tagElm = this.createTagElem(tagData, tagElmParams); + return { + tagElm, + tagData, + aggregatedInvalidInput + }; + }, /** * For selecting a single option (not used for multiple tags, but for "mode:select" only) * @param {Object} tagElm Tag DOM node @@ -3336,9 +3391,8 @@ Tagify.prototype = { addTags(tagsItems, clearInput, skipInvalid) { var tagElems = [], _s = this.settings, - aggregatedinvalidInput = [], + aggregatedInvalidInput = [], frag = document.createDocumentFragment(); - skipInvalid = skipInvalid || _s.skipInvalid; if (!tagsItems || tagsItems.length == 0) { return tagElems; } @@ -3356,40 +3410,12 @@ Tagify.prototype = { } this.DOM.input.removeAttribute('style'); tagsItems.forEach(tagData => { - var tagElm, - tagElmParams = {}, - originalData = Object.assign({}, tagData, { - value: tagData.value + "" - }); - - // shallow-clone tagData so later modifications will not apply to the source - tagData = Object.assign({}, originalData); - _s.transformTag.call(this, tagData); - tagData.__isValid = this.hasMaxTags() || this.validateTag(tagData); - if (tagData.__isValid !== true) { - if (skipInvalid) return; - - // originalData is kept because it might be that this tag is invalid because it is a duplicate of another, - // and if that other tags is edited/deleted, this one should be re-validated and if is no more a duplicate - restored - extend(tagElmParams, this.getInvalidTagAttrs(tagData, tagData.__isValid), { - __preInvalidData: originalData - }); - if (tagData.__isValid == this.TEXTS.duplicate) - // mark, for a brief moment, the tag (this this one) which THIS CURRENT tag is a duplcate of - this.flashTag(this.getTagElmByValue(tagData.value)); - if (!_s.createInvalidTags) { - aggregatedinvalidInput.push(tagData.value); - return; - } - } - if ('readonly' in tagData) { - if (tagData.readonly) tagElmParams["aria-readonly"] = true; - // if "readonly" is "false", remove it from the tagData so it won't be added as an attribute in the template - else delete tagData.readonly; - } - - // Create tag HTML element - tagElm = this.createTagElem(tagData, tagElmParams); + const newTagNode = this.prepareNewTagNode(tagData, { + skipInvalid: skipInvalid || _s.skipInvalid + }), + tagElm = newTagNode.tagElm; + tagData = newTagNode.tagData; + aggregatedInvalidInput = newTagNode.aggregatedInvalidInput; tagElems.push(tagElm); // mode-select overrides @@ -3425,7 +3451,7 @@ Tagify.prototype = { this.appendTag(frag); this.update(); if (tagsItems.length && clearInput) { - this.input.set.call(this, _s.createInvalidTags ? '' : aggregatedinvalidInput.join(_s._delimiters)); + this.input.set.call(this, _s.createInvalidTags ? '' : aggregatedInvalidInput.join(_s._delimiters)); this.setRangeAtStartEnd(false, this.DOM.input); } _s.dropdown.enabled && this.dropdown.refilter(); @@ -3437,13 +3463,15 @@ Tagify.prototype = { */ addMixTags(tagsData) { tagsData = this.normalizeTags(tagsData); + + // flow for creating custom tags which aren't a part of the whitelist if (tagsData[0].prefix || this.state.tag) { return this.prefixedTextToTag(tagsData[0]); } var frag = document.createDocumentFragment(); tagsData.forEach(tagData => { - var tagElm = this.createTagElem(tagData); - frag.appendChild(tagElm); + const newTagNode = this.prepareNewTagNode(tagData); + frag.appendChild(newTagNode.tagElm); }); this.appendMixTags(frag); return frag; @@ -3470,17 +3498,14 @@ Tagify.prototype = { /** * Adds a tag which was activly typed by the user - * @param {String/Array} tagItem [A string (single or multiple values with a delimiter), or an Array of Objects or just Array of Strings] + * @param {String/Array} tagData [A string (single or multiple values with a delimiter), or an Array of Objects or just Array of Strings] */ - prefixedTextToTag(tagItem) { + prefixedTextToTag(tagData) { var _s = this.settings, tagElm, - createdFromDelimiters = this.state.tag.delimiters; - _s.transformTag.call(this, tagItem); - tagItem.prefix = tagItem.prefix || this.state.tag ? this.state.tag.prefix : (_s.pattern.source || _s.pattern)[0]; - - // TODO: should check if the tag is valid - tagElm = this.createTagElem(tagItem); + createdFromDelimiters = this.state.tag?.delimiters; + tagData.prefix = tagData.prefix || this.state.tag ? this.state.tag.prefix : (_s.pattern.source || _s.pattern)[0]; + tagElm = this.prepareNewTagNode(tagData).tagElm; // tries to replace a taged textNode with a tagElm, and if not able, // insert the new tag to the END if "addTags" was called from outside @@ -3488,7 +3513,7 @@ Tagify.prototype = { this.DOM.input.appendChild(tagElm); } setTimeout(() => tagElm.classList.add(this.settings.classNames.tagNoAnimation), 300); - this.value.push(tagItem); + this.value.push(tagData); this.update(); if (!createdFromDelimiters) { var elm = this.insertAfterTag(tagElm) || tagElm; @@ -3501,7 +3526,7 @@ Tagify.prototype = { this.trigger('add', extend({}, { tag: tagElm }, { - data: tagItem + data: tagData })); return tagElm; }, @@ -3778,3 +3803,4 @@ Tagify.prototype = { Tagify.prototype.removeTag = Tagify.prototype.removeTags; export { Tagify as default }; +//# sourceMappingURL=tagify.esm.js.map diff --git a/dist/tagify.esm.js.map b/dist/tagify.esm.js.map new file mode 100644 index 00000000..c36c1e14 --- /dev/null +++ b/dist/tagify.esm.js.map @@ -0,0 +1 @@ +{"version":3,"file":"tagify.esm.js","sources":["src/parts/constants.js","src/parts/helpers.js","src/parts/defaults.js","src/parts/suggestions.js","src/parts/dropdown.js","src/parts/persist.js","src/parts/texts.js","src/parts/templates.js","src/parts/EventDispatcher.js","src/parts/events.js","src/tagify.js"],"sourcesContent":["export var ZERO_WIDTH_CHAR = '\\u200B';","import {ZERO_WIDTH_CHAR} from './constants'\r\n\r\n// console.json = console.json || function(argument){\r\n// for(var arg=0; arg < arguments.length; ++arg)\r\n// console.log( JSON.stringify(arguments[arg], null, 4) )\r\n// }\r\n\r\n// const isEdge = /Edge/.test(navigator.userAgent)\r\nexport const sameStr = (s1, s2, caseSensitive, trim) => {\r\n // cast to String\r\n s1 = \"\"+s1;\r\n s2 = \"\"+s2;\r\n\r\n if( trim ){\r\n s1 = s1.trim()\r\n s2 = s2.trim()\r\n }\r\n\r\n return caseSensitive\r\n ? s1 == s2\r\n : s1.toLowerCase() == s2.toLowerCase()\r\n}\r\n\r\n\r\n// const getUID = () => (new Date().getTime() + Math.floor((Math.random()*10000)+1)).toString(16)\r\nexport const removeCollectionProp = (collection, unwantedProps) => collection && Array.isArray(collection) && collection.map(v => omit(v, unwantedProps))\r\n\r\nexport function omit(obj, props){\r\n var newObj = {}, p;\r\n for( p in obj )\r\n if( props.indexOf(p) < 0 )\r\n newObj[p] = obj[p]\r\n return newObj\r\n}\r\n\r\nexport function decode( s ) {\r\n var el = document.createElement('div');\r\n return s.replace(/\\&#?[0-9a-z]+;/gi, function(enc){\r\n el.innerHTML = enc;\r\n return el.innerText\r\n })\r\n}\r\n\r\n/**\r\n * utility method\r\n * https://stackoverflow.com/a/35385518/104380\r\n * @param {String} s [HTML string]\r\n * @return {Object} [DOM node]\r\n */\r\nexport function parseHTML( s ){\r\n var parser = new DOMParser(),\r\n node = parser.parseFromString(s.trim(), \"text/html\");\r\n\r\n return node.body.firstElementChild;\r\n}\r\n\r\n/**\r\n * Removed new lines and irrelevant spaces which might affect layout, and are better gone\r\n * @param {string} s [HTML string]\r\n */\r\nexport function minify( s ){\r\n return s ? s\r\n .replace(/\\>[\\r\\n ]+\\<\")\r\n .split(/>\\s+<').trim()\r\n : \"\"\r\n}\r\n\r\nexport function removeTextChildNodes( elm ){\r\n var iter = document.createNodeIterator(elm, NodeFilter.SHOW_TEXT, null, false),\r\n textnode;\r\n\r\n // print all text nodes\r\n while (textnode = iter.nextNode()){\r\n if( !textnode.textContent.trim() )\r\n textnode.parentNode.removeChild(textnode)\r\n }\r\n}\r\n\r\nexport function getfirstTextNode( elm, action ){\r\n action = action || 'previous';\r\n while ( elm = elm[action + 'Sibling'] )\r\n if( elm.nodeType == 3 )\r\n return elm\r\n}\r\n\r\n/**\r\n * utility method\r\n * https://stackoverflow.com/a/6234804/104380\r\n */\r\nexport function escapeHTML( s ){\r\n return typeof s == 'string' ? s\r\n .replace(/&/g, \"&\")\r\n .replace(//g, \">\")\r\n .replace(/\"/g, \""\")\r\n .replace(/`|'/g, \"'\")\r\n : s;\r\n}\r\n\r\n/**\r\n * Checks if an argument is a javascript Object\r\n */\r\nexport function isObject(obj) {\r\n var type = Object.prototype.toString.call(obj).split(' ')[1].slice(0, -1);\r\n return obj === Object(obj) && type != 'Array' && type != 'Function' && type != 'RegExp' && type != 'HTMLUnknownElement';\r\n}\r\n\r\n/**\r\n * merge objects into a single new one\r\n * TEST: extend({}, {a:{foo:1}, b:[]}, {a:{bar:2}, b:[1], c:()=>{}})\r\n */\r\nexport function extend( o, o1, o2) {\r\n if( !(o instanceof Object) ) o = {};\r\n\r\n copy(o, o1);\r\n if( o2 )\r\n copy(o, o2)\r\n\r\n function copy(a,b){\r\n // copy o2 to o\r\n for( var key in b )\r\n if( b.hasOwnProperty(key) ){\r\n if( isObject(b[key]) ){\r\n if( !isObject(a[key]) )\r\n a[key] = Object.assign({}, b[key])\r\n else\r\n copy(a[key], b[key])\r\n\r\n continue;\r\n }\r\n\r\n if( Array.isArray(b[key]) ){\r\n a[key] = Object.assign([], b[key])\r\n continue\r\n }\r\n\r\n a[key] = b[key]\r\n }\r\n }\r\n\r\n return o\r\n}\r\n\r\n/**\r\n * concatenates N arrays without dups.\r\n * If an array's item is an Object, compare by `value`\r\n */\r\nexport function concatWithoutDups(){\r\n const newArr = [],\r\n existingObj = {};\r\n\r\n for( let arr of arguments ) {\r\n for( let item of arr ) {\r\n // if current item is an object which has yet to be added to the new array\r\n if( isObject(item) ){\r\n if( !existingObj[item.value] ){\r\n newArr.push(item)\r\n existingObj[item.value] = 1\r\n }\r\n }\r\n\r\n // if current item is not an object and is not in the new array\r\n else if( !newArr.includes(item) )\r\n newArr.push(item)\r\n }\r\n }\r\n\r\n return newArr\r\n}\r\n\r\n/**\r\n * Extracted from: https://stackoverflow.com/a/37511463/104380\r\n * @param {String} s\r\n */\r\nexport function unaccent( s ){\r\n // if not supported, do not continue.\r\n // developers should use a polyfill:\r\n // https://github.com/walling/unorm\r\n if( !String.prototype.normalize )\r\n return s\r\n\r\n if (typeof(s) === 'string')\r\n return s.normalize(\"NFD\").replace(/[\\u0300-\\u036f]/g, \"\")\r\n}\r\n\r\n/**\r\n * Meassures an element's height, which might yet have been added DOM\r\n * https://stackoverflow.com/q/5944038/104380\r\n * @param {DOM} node\r\n */\r\nexport function getNodeHeight( node ){\r\n var height, clone = node.cloneNode(true)\r\n clone.style.cssText = \"position:fixed; top:-9999px; opacity:0\"\r\n document.body.appendChild(clone)\r\n height = clone.clientHeight\r\n clone.parentNode.removeChild(clone)\r\n return height\r\n}\r\n\r\nexport var isChromeAndroidBrowser = () => /(?=.*chrome)(?=.*android)/i.test(navigator.userAgent)\r\n\r\nexport function getUID() {\r\n return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>\r\n (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)\r\n )\r\n}\r\n\r\nexport function isNodeTag(node){\r\n return node && node.classList && node.classList.contains(this.settings.classNames.tag)\r\n}\r\n\r\n/**\r\n* Get the caret position relative to the viewport\r\n* https://stackoverflow.com/q/58985076/104380\r\n*\r\n* @returns {object} left, top distance in pixels\r\n*/\r\nexport function getCaretGlobalPosition(){\r\n const sel = document.getSelection()\r\n\r\n if( sel.rangeCount ){\r\n const r = sel.getRangeAt(0)\r\n const node = r.startContainer\r\n const offset = r.startOffset\r\n let rect, r2;\r\n\r\n if (offset > 0) {\r\n r2 = document.createRange()\r\n r2.setStart(node, offset - 1)\r\n r2.setEnd(node, offset)\r\n rect = r2.getBoundingClientRect()\r\n return {left:rect.right, top:rect.top, bottom:rect.bottom}\r\n }\r\n\r\n if( node.getBoundingClientRect )\r\n return node.getBoundingClientRect()\r\n }\r\n\r\n return {left:-9999, top:-9999}\r\n}\r\n\r\n/**\r\n * Injects content (either string or node) at the current the current (or specificed) caret position\r\n * @param {content} string/node\r\n * @param {range} Object (optional, a range other than the current window selection)\r\n */\r\nexport function injectAtCaret(content, range){\r\n var selection = window.getSelection();\r\n range = range || selection.getRangeAt(0)\r\n\r\n if( typeof content == 'string' )\r\n content = document.createTextNode(content)\r\n\r\n if( range ) {\r\n range.deleteContents()\r\n range.insertNode(content)\r\n }\r\n\r\n return content\r\n}\r\n\r\n/** Setter/Getter\r\n * Each tag DOM node contains a custom property called \"__tagifyTagData\" which hosts its data\r\n * @param {Node} tagElm\r\n * @param {Object} data\r\n */\r\nexport function getSetTagData(tagElm, data, override){\r\n if( !tagElm ){\r\n console.warn(\"tag element doesn't exist\",tagElm, data)\r\n return data\r\n }\r\n\r\n if( data )\r\n tagElm.__tagifyTagData = override\r\n ? data\r\n : extend({}, tagElm.__tagifyTagData || {}, data)\r\n\r\n return tagElm.__tagifyTagData\r\n}\r\n\r\nexport function placeCaretAfterNode( node ){\r\n if( !node || !node.parentNode ) return\r\n\r\n var nextSibling = node,\r\n sel = window.getSelection(),\r\n range = sel.getRangeAt(0);\r\n\r\n if (sel.rangeCount) {\r\n range.setStartAfter(nextSibling);\r\n range.collapse(true)\r\n // range.setEndBefore(nextSibling || node);\r\n sel.removeAllRanges();\r\n sel.addRange(range);\r\n }\r\n}\r\n\r\n/**\r\n * iterate all tags, checking if multiple ones are close-siblings and if so, add a zero-space width character between them,\r\n * which forces the caret to be rendered when the selection is between tags.\r\n * Also do that if the tag is the first node.\r\n * @param {Array} tags\r\n */\r\nexport function fixCaretBetweenTags(tags, TagifyHasFocuse) {\r\n tags.forEach(tag => {\r\n if( getSetTagData(tag.previousSibling) || !tag.previousSibling ) {\r\n var textNode = document.createTextNode(ZERO_WIDTH_CHAR)\r\n tag.before(textNode)\r\n TagifyHasFocuse && placeCaretAfterNode(textNode)\r\n }\r\n })\r\n}\r\n","export default {\r\n delimiters : \",\", // [RegEx] split tags by any of these delimiters (\"null\" to cancel) Example: \",| |.\"\r\n pattern : null, // RegEx pattern to validate input by. Ex: /[1-9]/\r\n tagTextProp : 'value', // tag data Object property which will be displayed as the tag's text\r\n maxTags : Infinity, // Maximum number of tags\r\n callbacks : {}, // Exposed callbacks object to be triggered on certain events\r\n addTagOnBlur : true, // automatically adds the text which was inputed as a tag when blur event happens\r\n addTagOn : ['blur', 'tab', 'enter'], // if the tagify field (in a normal mode) has any non-tag input in it, convert it to a tag on any of these events: blur away from the field, click \"tab\"/\"enter\" key\r\n onChangeAfterBlur : true, // By default, the native way of inputs' onChange events is kept, and it only fires when the field is blured.\r\n duplicates : false, // \"true\" - allow duplicate tags\r\n whitelist : [], // Array of tags to suggest as the user types (can be used along with \"enforceWhitelist\" setting)\r\n blacklist : [], // A list of non-allowed tags\r\n enforceWhitelist : false, // Only allow tags from the whitelist\r\n userInput : true, // disable manually typing/pasting/editing tags (tags may only be added from the whitelist)\r\n keepInvalidTags : false, // if true, do not remove tags which did not pass validation\r\n createInvalidTags : true, // if false, do not create invalid tags from invalid user input\r\n mixTagsAllowedAfter : /,|\\.|\\:|\\s/, // RegEx - Define conditions in which mix-tags content allows a tag to be added after\r\n mixTagsInterpolator : ['[[', ']]'], // Interpolation for mix mode. Everything between these will become a tag, if is a valid Object\r\n backspace : true, // false / true / \"edit\"\r\n skipInvalid : false, // If `true`, do not add invalid, temporary, tags before automatically removing them\r\n pasteAsTags : true, // automatically converts pasted text into tags. if \"false\", allows for further text editing\r\n\r\n editTags : {\r\n clicks : 2, // clicks to enter \"edit-mode\": 1 for single click. any other value is considered as double-click\r\n keepInvalid : true // keeps invalid edits as-is until `esc` is pressed while in focus\r\n }, // 1 or 2 clicks to edit a tag. false/null for not allowing editing\r\n transformTag : ()=>{}, // Takes a tag input string as argument and returns a transformed value\r\n trim : true, // whether or not the value provided should be trimmed, before being added as a tag\r\n a11y: {\r\n focusableTags: false\r\n },\r\n\r\n mixMode: {\r\n insertAfterTag : '\\u00A0', // String/Node to inject after a tag has been added (see #588)\r\n },\r\n\r\n autoComplete: {\r\n enabled: true, // Tries to suggest the input's value while typing (match from whitelist) by adding the rest of term as grayed-out text\r\n rightKey: false, // If `true`, when Right key is pressed, use the suggested value to create a tag, else just auto-completes the input. in mixed-mode this is set to \"true\"\r\n tabKey: false, // If 'true`, pressing `tab` key would only auto-complete but not also convert to a tag (like `rightKey` does).\r\n },\r\n\r\n classNames: {\r\n namespace : 'tagify',\r\n mixMode : 'tagify--mix',\r\n selectMode : 'tagify--select',\r\n input : 'tagify__input',\r\n focus : 'tagify--focus',\r\n tagNoAnimation : 'tagify--noAnim',\r\n tagInvalid : 'tagify--invalid',\r\n tagNotAllowed : 'tagify--notAllowed',\r\n scopeLoading : 'tagify--loading',\r\n hasMaxTags : 'tagify--hasMaxTags',\r\n hasNoTags : 'tagify--noTags',\r\n empty : 'tagify--empty',\r\n inputInvalid : 'tagify__input--invalid',\r\n dropdown : 'tagify__dropdown',\r\n dropdownWrapper : 'tagify__dropdown__wrapper',\r\n dropdownHeader : 'tagify__dropdown__header',\r\n dropdownFooter : 'tagify__dropdown__footer',\r\n dropdownItem : 'tagify__dropdown__item',\r\n dropdownItemActive : 'tagify__dropdown__item--active',\r\n dropdownItemHidden : 'tagify__dropdown__item--hidden',\r\n dropdownInital : 'tagify__dropdown--initial',\r\n tag : 'tagify__tag',\r\n tagText : 'tagify__tag-text',\r\n tagX : 'tagify__tag__removeBtn',\r\n tagLoading : 'tagify__tag--loading',\r\n tagEditing : 'tagify__tag--editable',\r\n tagFlash : 'tagify__tag--flash',\r\n tagHide : 'tagify__tag--hide',\r\n\r\n },\r\n\r\n dropdown: {\r\n classname : '',\r\n enabled : 2, // minimum input characters to be typed for the suggestions dropdown to show\r\n maxItems : 10,\r\n searchKeys : [\"value\", \"searchBy\"],\r\n fuzzySearch : true,\r\n caseSensitive : false,\r\n accentedSearch : true,\r\n includeSelectedTags: false, // Should the suggestions list Include already-selected tags (after filtering)\r\n escapeHTML : true, // escapes HTML entities in the suggestions' rendered text\r\n highlightFirst : true, // highlights first-matched item in the list\r\n closeOnSelect : true, // closes the dropdown after selecting an item, if `enabled:0` (which means always show dropdown)\r\n clearOnSelect : true, // after selecting a suggetion, should the typed text input remain or be cleared\r\n position : 'all', // 'manual' / 'text' / 'all'\r\n appendTarget : null // defaults to document.body once DOM has been loaded\r\n },\r\n\r\n hooks: {\r\n beforeRemoveTag: () => Promise.resolve(),\r\n beforePaste: () => Promise.resolve(),\r\n suggestionClick: () => Promise.resolve(),\r\n beforeKeyDown: () => Promise.resolve(),\r\n }\r\n}","import { isObject, escapeHTML, extend, unaccent } from './helpers'\n\n\n/**\n * Tagify's dropdown suggestions-related logic\n */\n\nexport default {\n events : {\n /**\n * Events should only be binded when the dropdown is rendered and removed when isn't\n * because there might be multiple Tagify instances on a certain page\n * @param {Boolean} bindUnbind [optional. true when wanting to unbind all the events]\n */\n binding( bindUnbind = true ){\n // references to the \".bind()\" methods must be saved so they could be unbinded later\n var _CB = this.dropdown.events.callbacks,\n // callback-refs\n _CBR = (this.listeners.dropdown = this.listeners.dropdown || {\n position : this.dropdown.position.bind(this, null),\n onKeyDown : _CB.onKeyDown.bind(this),\n onMouseOver : _CB.onMouseOver.bind(this),\n onMouseLeave : _CB.onMouseLeave.bind(this),\n onClick : _CB.onClick.bind(this),\n onScroll : _CB.onScroll.bind(this),\n }),\n action = bindUnbind ? 'addEventListener' : 'removeEventListener';\n\n if( this.settings.dropdown.position != 'manual' ){\n document[action]('scroll', _CBR.position, true)\n window[action]('resize', _CBR.position)\n window[action]('keydown', _CBR.onKeyDown)\n }\n\n this.DOM.dropdown[action]('mouseover', _CBR.onMouseOver)\n this.DOM.dropdown[action]('mouseleave', _CBR.onMouseLeave)\n this.DOM.dropdown[action]('mousedown', _CBR.onClick)\n this.DOM.dropdown.content[action]('scroll', _CBR.onScroll)\n },\n\n callbacks : {\n onKeyDown(e){\n // ignore keys during IME composition\n if( !this.state.hasFocus || this.state.composing )\n return\n\n // get the \"active\" element, and if there was none (yet) active, use first child\n var _s = this.settings,\n selectedElm = this.DOM.dropdown.querySelector(_s.classNames.dropdownItemActiveSelector),\n selectedElmData = this.dropdown.getSuggestionDataByNode(selectedElm),\n isMixMode = _s.mode == 'mix';\n\n _s.hooks.beforeKeyDown(e, {tagify:this})\n .then(result => {\n switch( e.key ){\n case 'ArrowDown' :\n case 'ArrowUp' :\n case 'Down' : // >IE11\n case 'Up' : { // >IE11\n e.preventDefault()\n var dropdownItems = this.dropdown.getAllSuggestionsRefs(),\n actionUp = e.key == 'ArrowUp' || e.key == 'Up';\n\n if( selectedElm ) {\n selectedElm = this.dropdown.getNextOrPrevOption(selectedElm, !actionUp)\n }\n\n // if no element was found OR current item is not a \"real\" item, loop\n if( !selectedElm || !selectedElm.matches(_s.classNames.dropdownItemSelector) ){\n selectedElm = dropdownItems[actionUp ? dropdownItems.length - 1 : 0];\n }\n\n this.dropdown.highlightOption(selectedElm, true)\n // selectedElm.scrollIntoView({inline: 'nearest', behavior: 'smooth'})\n break;\n }\n case 'Escape' :\n case 'Esc': // IE11\n this.dropdown.hide();\n break;\n\n case 'ArrowRight' :\n if( this.state.actions.ArrowLeft )\n return\n case 'Tab' : {\n let shouldAutocompleteOnKey = !_s.autoComplete.rightKey || !_s.autoComplete.tabKey\n\n // in mix-mode, treat arrowRight like Enter key, so a tag will be created\n if( !isMixMode && selectedElm && shouldAutocompleteOnKey && !this.state.editing ){\n e.preventDefault() // prevents blur so the autocomplete suggestion will not become a tag\n var value = this.dropdown.getMappedValue(selectedElmData)\n\n this.input.autocomplete.set.call(this, value)\n return false\n }\n return true\n }\n case 'Enter' : {\n e.preventDefault()\n\n _s.hooks.suggestionClick(e, {tagify:this, tagData:selectedElmData, suggestionElm:selectedElm})\n .then(() => {\n if( selectedElm ){\n this.dropdown.selectOption(selectedElm)\n // highlight next option\n selectedElm = this.dropdown.getNextOrPrevOption(selectedElm, !actionUp)\n this.dropdown.highlightOption(selectedElm)\n return\n }\n else\n this.dropdown.hide()\n\n if( !isMixMode )\n this.addTags(this.state.inputText.trim(), true)\n })\n .catch(err => console.warn(err))\n\n break;\n }\n case 'Backspace' : {\n if( isMixMode || this.state.editing.scope ) return;\n\n const value = this.input.raw.call(this)\n\n if( value == \"\" || value.charCodeAt(0) == 8203 ){\n if( _s.backspace === true )\n this.removeTags()\n else if( _s.backspace == 'edit' )\n setTimeout(this.editTag.bind(this), 0)\n }\n }\n }\n })\n },\n\n onMouseOver(e){\n var ddItem = e.target.closest(this.settings.classNames.dropdownItemSelector)\n // event delegation check\n this.dropdown.highlightOption(ddItem)\n },\n\n onMouseLeave(e){\n // de-highlight any previously highlighted option\n this.dropdown.highlightOption()\n },\n\n onClick(e){\n if( e.button != 0 || e.target == this.DOM.dropdown || e.target == this.DOM.dropdown.content ) return; // allow only mouse left-clicks\n\n var selectedElm = e.target.closest(this.settings.classNames.dropdownItemSelector),\n selectedElmData = this.dropdown.getSuggestionDataByNode(selectedElm)\n\n // temporary set the \"actions\" state to indicate to the main \"blur\" event it shouldn't run\n this.state.actions.selectOption = true;\n setTimeout(()=> this.state.actions.selectOption = false, 50)\n\n this.settings.hooks.suggestionClick(e, {tagify:this, tagData:selectedElmData, suggestionElm:selectedElm})\n .then(() => {\n if( selectedElm )\n this.dropdown.selectOption(selectedElm, e)\n else\n this.dropdown.hide()\n })\n .catch(err => console.warn(err))\n },\n\n onScroll(e){\n var elm = e.target,\n pos = elm.scrollTop / (elm.scrollHeight - elm.parentNode.clientHeight) * 100;\n\n this.trigger(\"dropdown:scroll\", {percentage:Math.round(pos)})\n },\n }\n },\n\n /**\n * fill data into the suggestions list\n * (mainly used to update the list when removing tags while the suggestions dropdown is visible, so they will be re-added to the list. not efficient)\n */\n refilter( value ){\n value = value || this.state.dropdown.query || ''\n this.suggestedListItems = this.dropdown.filterListItems(value)\n\n this.dropdown.fill()\n\n if( !this.suggestedListItems.length )\n this.dropdown.hide()\n\n this.trigger(\"dropdown:updated\", this.DOM.dropdown)\n },\n\n /**\n * Given a suggestion-item, return the data associated with it\n * @param {HTMLElement} tagElm\n * @returns Object\n */\n getSuggestionDataByNode( tagElm ){\n var value = tagElm && tagElm.getAttribute('value')\n return this.suggestedListItems.find(item => item.value == value) || null\n },\n\n getNextOrPrevOption(selected, next = true) {\n var dropdownItems = this.dropdown.getAllSuggestionsRefs(),\n selectedIdx = dropdownItems.findIndex(item => item === selected);\n\n return next ? dropdownItems[selectedIdx + 1] : dropdownItems[selectedIdx - 1]\n },\n\n /**\n * mark the currently active suggestion option\n * @param {Object} elm option DOM node\n * @param {Boolean} adjustScroll when navigation with keyboard arrows (up/down), aut-scroll to always show the highlighted element\n */\n highlightOption( elm, adjustScroll ){\n var className = this.settings.classNames.dropdownItemActive,\n itemData;\n\n // focus casues a bug in Firefox with the placeholder been shown on the input element\n // if( this.settings.dropdown.position != 'manual' )\n // elm.focus();\n\n if( this.state.ddItemElm ){\n this.state.ddItemElm.classList.remove(className)\n this.state.ddItemElm.removeAttribute(\"aria-selected\")\n }\n\n if( !elm ){\n this.state.ddItemData = null\n this.state.ddItemElm = null\n this.input.autocomplete.suggest.call(this)\n return;\n }\n\n itemData = this.dropdown.getSuggestionDataByNode(elm)\n this.state.ddItemData = itemData\n this.state.ddItemElm = elm\n\n // this.DOM.dropdown.querySelectorAll(\".\" + this.settings.classNames.dropdownItemActive).forEach(activeElm => activeElm.classList.remove(className));\n elm.classList.add(className);\n elm.setAttribute(\"aria-selected\", true)\n\n if( adjustScroll )\n elm.parentNode.scrollTop = elm.clientHeight + elm.offsetTop - elm.parentNode.clientHeight\n\n // Try to autocomplete the typed value with the currently highlighted dropdown item\n if( this.settings.autoComplete ){\n this.input.autocomplete.suggest.call(this, itemData)\n this.dropdown.position() // suggestions might alter the height of the tagify wrapper because of unkown suggested term length that could drop to the next line\n }\n },\n\n /**\n * Create a tag from the currently active suggestion option\n * @param {Object} elm DOM node to select\n * @param {Object} event The original Click event, if available (since keyboard ENTER key also triggers this method)\n */\n selectOption( elm, event ){\n var _s = this.settings,\n {clearOnSelect, closeOnSelect} = _s.dropdown;\n\n if( !elm ) {\n this.addTags(this.state.inputText, true)\n closeOnSelect && this.dropdown.hide()\n return;\n }\n\n event = event || {}\n\n // if in edit-mode, do not continue but instead replace the tag's text.\n // the scenario is that \"addTags\" was called from a dropdown suggested option selected while editing\n\n var value = elm.getAttribute('value'),\n isNoMatch = value == 'noMatch',\n isMixMode = _s.mode == 'mix',\n tagData = this.suggestedListItems.find(item => (item.value ?? item) == value)\n\n // The below event must be triggered, regardless of anything else which might go wrong\n this.trigger('dropdown:select', {data:tagData, elm, event})\n\n if( !value || !tagData && !isNoMatch ){\n closeOnSelect && setTimeout(this.dropdown.hide.bind(this))\n return\n }\n\n if( this.state.editing ) {\n let normalizedTagData = this.normalizeTags([tagData])[0]\n tagData = _s.transformTag.call(this, normalizedTagData) || normalizedTagData\n\n // normalizing value, because \"tagData\" might be a string, and therefore will not be able to extend the object\n this.onEditTagDone(null, extend({__isValid: true}, tagData))\n }\n // Tagify instances should re-focus to the input element once an option was selected, to allow continuous typing\n else {\n this[isMixMode ? \"addMixTags\" : \"addTags\"]([tagData || this.input.raw.call(this)], clearOnSelect)\n }\n\n if( !isMixMode && !this.DOM.input.parentNode )\n return\n\n setTimeout(() => {\n this.DOM.input.focus()\n this.toggleFocusClass(true)\n })\n\n closeOnSelect && setTimeout(this.dropdown.hide.bind(this))\n\n // hide selected suggestion\n elm.addEventListener('transitionend', () => {\n this.dropdown.fillHeaderFooter()\n setTimeout(() => elm.remove(), 100)\n }, {once: true})\n\n elm.classList.add(this.settings.classNames.dropdownItemHidden)\n },\n\n // adds all the suggested items, including the ones which are not currently rendered,\n // unless specified otherwise (by the \"onlyRendered\" argument)\n selectAll( onlyRendered ){\n // having suggestedListItems with items messes with \"normalizeTags\" when wanting\n // to add all tags\n this.suggestedListItems.length = 0;\n this.dropdown.hide()\n\n this.dropdown.filterListItems('');\n\n var tagsToAdd = this.dropdown.filterListItems('');\n\n if( !onlyRendered )\n tagsToAdd = this.state.dropdown.suggestions\n\n // some whitelist items might have already been added as tags so when addings all of them,\n // skip adding already-added ones, so best to use \"filterListItems\" method over \"settings.whitelist\"\n this.addTags(tagsToAdd, true)\n return this\n },\n\n /**\n * returns an HTML string of the suggestions' list items\n * @param {String} value string to filter the whitelist by\n * @param {Object} options \"exact\" - for exact complete match\n * @return {Array} list of filtered whitelist items according to the settings provided and current value\n */\n filterListItems( value, options ){\n var _s = this.settings,\n _sd = _s.dropdown,\n options = options || {},\n list = [],\n exactMatchesList = [],\n whitelist = _s.whitelist,\n suggestionsCount = _sd.maxItems >= 0 ? _sd.maxItems : Infinity,\n searchKeys = _sd.searchKeys,\n whitelistItem,\n valueIsInWhitelist,\n searchBy,\n isDuplicate,\n niddle,\n i = 0;\n\n value = (_s.mode == 'select' && this.value.length && this.value[0][_s.tagTextProp] == value\n ? '' // do not filter if the tag, which is already selecetd in \"select\" mode, is the same as the typed text\n : value);\n\n if( !value || !searchKeys.length ){\n list = _sd.includeSelectedTags\n ? whitelist\n : whitelist.filter(item => !this.isTagDuplicate( isObject(item) ? item.value : item )) // don't include tags which have already been added.\n\n this.state.dropdown.suggestions = list;\n return list.slice(0, suggestionsCount); // respect \"maxItems\" dropdown setting\n }\n\n niddle = _sd.caseSensitive\n ? \"\"+value\n : (\"\"+value).toLowerCase()\n\n // checks if ALL of the words in the search query exists in the current whitelist item, regardless of their order\n function stringHasAll(s, query){\n return query.toLowerCase().split(' ').every(q => s.includes(q.toLowerCase()))\n }\n\n for( ; i < whitelist.length; i++ ){\n let startsWithMatch, exactMatch;\n\n whitelistItem = whitelist[i] instanceof Object ? whitelist[i] : { value:whitelist[i] } //normalize value as an Object\n\n let itemWithoutSearchKeys = !Object.keys(whitelistItem).some(k => searchKeys.includes(k) ),\n _searchKeys = itemWithoutSearchKeys ? [\"value\"] : searchKeys\n\n if( _sd.fuzzySearch && !options.exact ){\n searchBy = _searchKeys.reduce((values, k) => values + \" \" + (whitelistItem[k]||\"\"), \"\").toLowerCase().trim()\n\n if( _sd.accentedSearch ){\n searchBy = unaccent(searchBy)\n niddle = unaccent(niddle)\n }\n\n startsWithMatch = searchBy.indexOf(niddle) == 0\n exactMatch = searchBy === niddle\n valueIsInWhitelist = stringHasAll(searchBy, niddle)\n }\n\n else {\n startsWithMatch = true;\n valueIsInWhitelist = _searchKeys.some(k => {\n var v = '' + (whitelistItem[k] || '') // if key exists, cast to type String\n\n if( _sd.accentedSearch ){\n v = unaccent(v)\n niddle = unaccent(niddle)\n }\n\n if( !_sd.caseSensitive )\n v = v.toLowerCase()\n\n exactMatch = v === niddle\n\n return options.exact\n ? v === niddle\n : v.indexOf(niddle) == 0\n })\n }\n\n isDuplicate = !_sd.includeSelectedTags && this.isTagDuplicate( isObject(whitelistItem) ? whitelistItem.value : whitelistItem )\n\n // match for the value within each \"whitelist\" item\n if( valueIsInWhitelist && !isDuplicate )\n if( exactMatch && startsWithMatch)\n exactMatchesList.push(whitelistItem)\n else if( _sd.sortby == 'startsWith' && startsWithMatch )\n list.unshift(whitelistItem)\n else\n list.push(whitelistItem)\n }\n\n this.state.dropdown.suggestions = exactMatchesList.concat(list);\n\n // custom sorting function\n return typeof _sd.sortby == 'function'\n ? _sd.sortby(exactMatchesList.concat(list), niddle)\n : exactMatchesList.concat(list).slice(0, suggestionsCount)\n },\n\n /**\n * Returns the final value of a tag data (object) with regards to the \"mapValueTo\" dropdown setting\n * @param {Object} tagData\n * @returns\n */\n getMappedValue(tagData){\n var mapValueTo = this.settings.dropdown.mapValueTo,\n value = (mapValueTo\n ? typeof mapValueTo == 'function' ? mapValueTo(tagData) : (tagData[mapValueTo] || tagData.value)\n : tagData.value);\n\n return value\n },\n\n /**\n * Creates the dropdown items' HTML\n * @param {Array} sugegstionsList [Array of Objects]\n * @return {String}\n */\n createListHTML( sugegstionsList ){\n return extend([], sugegstionsList).map((suggestion, idx) => {\n if( typeof suggestion == 'string' || typeof suggestion == 'number' )\n suggestion = {value:suggestion}\n\n var mappedValue = this.dropdown.getMappedValue(suggestion);\n\n mappedValue = (typeof mappedValue == 'string' && this.settings.dropdown.escapeHTML)\n ? escapeHTML(mappedValue)\n : mappedValue;\n\n return this.settings.templates.dropdownItem.apply(this, [{...suggestion, mappedValue}, this])\n }).join(\"\")\n }\n}","import { sameStr, isObject, minify, getNodeHeight, getCaretGlobalPosition } from './helpers'\r\nimport suggestionsMethods from './suggestions'\r\n\r\nexport function initDropdown(){\r\n this.dropdown = {}\r\n\r\n // auto-bind \"this\" to all the dropdown methods\r\n for( let p in this._dropdown )\r\n this.dropdown[p] = typeof this._dropdown[p] === 'function'\r\n ? this._dropdown[p].bind(this)\r\n : this._dropdown[p]\r\n\r\n this.dropdown.refs()\r\n}\r\n\r\nexport default {\r\n ...suggestionsMethods,\r\n\r\n refs(){\r\n this.DOM.dropdown = this.parseTemplate('dropdown', [this.settings])\r\n this.DOM.dropdown.content = this.DOM.dropdown.querySelector(\"[data-selector='tagify-suggestions-wrapper']\")\r\n },\r\n\r\n getHeaderRef(){\r\n return this.DOM.dropdown.querySelector(\"[data-selector='tagify-suggestions-header']\")\r\n },\r\n\r\n getFooterRef(){\r\n return this.DOM.dropdown.querySelector(\"[data-selector='tagify-suggestions-footer']\")\r\n },\r\n\r\n getAllSuggestionsRefs(){\r\n return [...this.DOM.dropdown.content.querySelectorAll(this.settings.classNames.dropdownItemSelector)]\r\n },\r\n\r\n /**\r\n * shows the suggestions select box\r\n * @param {String} value [optional, filter the whitelist by this value]\r\n */\r\n show( value ){\r\n var _s = this.settings,\r\n firstListItem,\r\n firstListItemValue,\r\n allowNewTags = _s.mode == 'mix' && !_s.enforceWhitelist,\r\n noWhitelist = !_s.whitelist || !_s.whitelist.length,\r\n noMatchListItem,\r\n isManual = _s.dropdown.position == 'manual';\r\n\r\n // if text still exists in the input, and `show` method has no argument, then the input's text should be used\r\n value = value === undefined ? this.state.inputText : value\r\n\r\n // ⚠️ Do not render suggestions list if:\r\n // 1. there's no whitelist (can happen while async loading) AND new tags arn't allowed\r\n // 2. dropdown is disabled\r\n // 3. loader is showing (controlled outside of this code)\r\n if( (noWhitelist && !allowNewTags && !_s.templates.dropdownItemNoMatch)\r\n || _s.dropdown.enable === false\r\n || this.state.isLoading\r\n || this.settings.readonly )\r\n return;\r\n\r\n clearTimeout(this.dropdownHide__bindEventsTimeout)\r\n\r\n // if no value was supplied, show all the \"whitelist\" items in the dropdown\r\n // @type [Array] listItems\r\n this.suggestedListItems = this.dropdown.filterListItems(value)\r\n\r\n // trigger at this exact point to let the developer the chance to manually set \"this.suggestedListItems\"\r\n if( value && !this.suggestedListItems.length ){\r\n this.trigger('dropdown:noMatch', value)\r\n\r\n if( _s.templates.dropdownItemNoMatch )\r\n noMatchListItem = _s.templates.dropdownItemNoMatch.call(this, {value})\r\n }\r\n\r\n // if \"dropdownItemNoMatch\" was not defined, procceed regular flow.\r\n //\r\n if( !noMatchListItem ){\r\n // in mix-mode, if the value isn't included in the whilelist & \"enforceWhitelist\" setting is \"false\",\r\n // then add a custom suggestion item to the dropdown\r\n if( this.suggestedListItems.length ){\r\n if( value && allowNewTags && !this.state.editing.scope && !sameStr(this.suggestedListItems[0].value, value) )\r\n this.suggestedListItems.unshift({value})\r\n }\r\n else{\r\n if( value && allowNewTags && !this.state.editing.scope ){\r\n this.suggestedListItems = [{value}]\r\n }\r\n // hide suggestions list if no suggestion matched\r\n else{\r\n this.input.autocomplete.suggest.call(this);\r\n this.dropdown.hide()\r\n return;\r\n }\r\n }\r\n\r\n firstListItem = this.suggestedListItems[0]\r\n firstListItemValue = \"\"+(isObject(firstListItem) ? firstListItem.value : firstListItem)\r\n\r\n if( _s.autoComplete && firstListItemValue ){\r\n // only fill the sugegstion if the value of the first list item STARTS with the input value (regardless of \"fuzzysearch\" setting)\r\n if( firstListItemValue.indexOf(value) == 0 )\r\n this.input.autocomplete.suggest.call(this, firstListItem)\r\n }\r\n }\r\n\r\n this.dropdown.fill(noMatchListItem)\r\n\r\n if( _s.dropdown.highlightFirst ) {\r\n this.dropdown.highlightOption(this.DOM.dropdown.content.querySelector(_s.classNames.dropdownItemSelector))\r\n }\r\n\r\n // bind events, exactly at this stage of the code. \"dropdown.show\" method is allowed to be\r\n // called multiple times, regardless if the dropdown is currently visible, but the events-binding\r\n // should only be called if the dropdown wasn't previously visible.\r\n if( !this.state.dropdown.visible )\r\n // timeout is needed for when pressing arrow down to show the dropdown,\r\n // so the key event won't get registered in the dropdown events listeners\r\n setTimeout(this.dropdown.events.binding.bind(this))\r\n\r\n // set the dropdown visible state to be the same as the searched value.\r\n // MUST be set *before* position() is called\r\n this.state.dropdown.visible = value || true\r\n this.state.dropdown.query = value\r\n\r\n this.setStateSelection()\r\n\r\n // try to positioning the dropdown (it might not yet be on the page, doesn't matter, next code handles this)\r\n if( !isManual ){\r\n // a slight delay is needed if the dropdown \"position\" setting is \"text\", and nothing was typed in the input,\r\n // so sadly the \"getCaretGlobalPosition\" method doesn't recognize the caret position without this delay\r\n setTimeout(() => {\r\n this.dropdown.position()\r\n this.dropdown.render()\r\n })\r\n }\r\n\r\n // a delay is needed because of the previous delay reason.\r\n // this event must be fired after the dropdown was rendered & positioned\r\n setTimeout(() => {\r\n this.trigger(\"dropdown:show\", this.DOM.dropdown)\r\n })\r\n },\r\n\r\n /**\r\n * Hides the dropdown (if it's not managed manually by the developer)\r\n * @param {Boolean} overrideManual\r\n */\r\n hide( overrideManual ){\r\n var {scope, dropdown} = this.DOM,\r\n isManual = this.settings.dropdown.position == 'manual' && !overrideManual;\r\n\r\n // if there's no dropdown, this means the dropdown events aren't binded\r\n if( !dropdown || !document.body.contains(dropdown) || isManual ) return;\r\n\r\n window.removeEventListener('resize', this.dropdown.position)\r\n this.dropdown.events.binding.call(this, false) // unbind all events\r\n\r\n // if the dropdown is open, and the input (scope) is clicked,\r\n // the dropdown should be now \"close\", and the next click (on the scope)\r\n // should re-open it, and without a timeout, clicking to close will re-open immediately\r\n // clearTimeout(this.dropdownHide__bindEventsTimeout)\r\n // this.dropdownHide__bindEventsTimeout = setTimeout(this.events.binding.bind(this), 250) // re-bind main events\r\n\r\n\r\n scope.setAttribute(\"aria-expanded\", false)\r\n dropdown.parentNode.removeChild(dropdown)\r\n\r\n // scenario: clicking the scope to show the dropdown, clicking again to hide -> calls dropdown.hide() and then re-focuses the input\r\n // which casues another onFocus event, which checked \"this.state.dropdown.visible\" and see it as \"false\" and re-open the dropdown\r\n setTimeout(() => {\r\n this.state.dropdown.visible = false\r\n }, 100)\r\n\r\n this.state.dropdown.query =\r\n this.state.ddItemData =\r\n this.state.ddItemElm =\r\n this.state.selection = null\r\n\r\n // if the user closed the dropdown (in mix-mode) while a potential tag was detected, flag the current tag\r\n // so the dropdown won't be shown on following user input for that \"tag\"\r\n if( this.state.tag && this.state.tag.value.length ){\r\n this.state.flaggedTags[this.state.tag.baseOffset] = this.state.tag\r\n }\r\n\r\n this.trigger(\"dropdown:hide\", dropdown)\r\n\r\n return this\r\n },\r\n\r\n /**\r\n * Toggles dropdown show/hide\r\n * @param {Boolean} show forces the dropdown to show\r\n */\r\n toggle(show){\r\n this.dropdown[this.state.dropdown.visible && !show ? 'hide' : 'show']()\r\n },\r\n\r\n getAppendTarget() {\r\n var _sd = this.settings.dropdown;\r\n return typeof _sd.appendTarget === 'function' ? _sd.appendTarget() : _sd.appendTarget;\r\n },\r\n\r\n render(){\r\n // let the element render in the DOM first, to accurately measure it.\r\n // this.DOM.dropdown.style.cssText = \"left:-9999px; top:-9999px;\";\r\n var ddHeight = getNodeHeight(this.DOM.dropdown),\r\n _s = this.settings,\r\n enabled = typeof _s.dropdown.enabled == 'number' && _s.dropdown.enabled >= 0,\r\n appendTarget = this.dropdown.getAppendTarget();\r\n\r\n if( !enabled ) return this;\r\n\r\n this.DOM.scope.setAttribute(\"aria-expanded\", true)\r\n\r\n // if the dropdown has yet to be appended to the DOM,\r\n // append the dropdown to the body element & handle events\r\n if( !document.body.contains(this.DOM.dropdown) ){\r\n this.DOM.dropdown.classList.add( _s.classNames.dropdownInital )\r\n this.dropdown.position(ddHeight)\r\n appendTarget.appendChild(this.DOM.dropdown)\r\n\r\n setTimeout(() =>\r\n this.DOM.dropdown.classList.remove( _s.classNames.dropdownInital )\r\n )\r\n }\r\n\r\n return this\r\n },\r\n\r\n /**\r\n * re-renders the dropdown content element (see \"dropdownContent\" in templates file)\r\n * @param {String/Array} HTMLContent - optional\r\n */\r\n fill( HTMLContent ){\r\n HTMLContent = typeof HTMLContent == 'string'\r\n ? HTMLContent\r\n : this.dropdown.createListHTML(HTMLContent || this.suggestedListItems)\r\n\r\n var dropdownContent = this.settings.templates.dropdownContent.call(this, HTMLContent)\r\n\r\n this.DOM.dropdown.content.innerHTML = minify(dropdownContent)\r\n },\r\n\r\n /**\r\n * Re-renders only the header & footer.\r\n * Used when selecting a suggestion and it is wanted that the suggestions dropdown stays open.\r\n * Since the list of sugegstions is not being re-rendered completely every time a suggestion is selected (the item is transitioned-out)\r\n * then the header & footer should be kept in sync with the suggestions data change\r\n */\r\n fillHeaderFooter(){\r\n var suggestions = this.dropdown.filterListItems(this.state.dropdown.query),\r\n newHeaderElem = this.parseTemplate('dropdownHeader', [suggestions]),\r\n newFooterElem = this.parseTemplate('dropdownFooter', [suggestions]),\r\n headerRef = this.dropdown.getHeaderRef(),\r\n footerRef = this.dropdown.getFooterRef();\r\n\r\n newHeaderElem && headerRef?.parentNode.replaceChild(newHeaderElem, headerRef)\r\n newFooterElem && footerRef?.parentNode.replaceChild(newFooterElem, footerRef)\r\n },\r\n\r\n /**\r\n * dropdown positioning logic\r\n * (shown above/below or next to typed text for mix-mode)\r\n */\r\n position( ddHeight ){\r\n var _sd = this.settings.dropdown,\r\n appendTarget = this.dropdown.getAppendTarget();\r\n\r\n if( _sd.position == 'manual' || !appendTarget) return\r\n\r\n var rect, top, bottom, left, width, ancestorsOffsets,\r\n isPlacedAbove,\r\n cssTop, cssLeft,\r\n ddElm = this.DOM.dropdown,\r\n isRTL = _sd.RTL,\r\n isDefaultAppendTarget = appendTarget === document.body,\r\n isSelfAppended = appendTarget === this.DOM.scope,\r\n appendTargetScrollTop = isDefaultAppendTarget ? window.pageYOffset : appendTarget.scrollTop,\r\n root = document.fullscreenElement || document.webkitFullscreenElement || document.documentElement,\r\n viewportHeight = root.clientHeight,\r\n viewportWidth = Math.max(root.clientWidth || 0, window.innerWidth || 0),\r\n positionTo = viewportWidth > 480 ? _sd.position : 'all',\r\n ddTarget = this.DOM[positionTo == 'input' ? 'input' : 'scope'];\r\n\r\n ddHeight = ddHeight || ddElm.clientHeight\r\n\r\n function getAncestorsOffsets(p){\r\n var top = 0, left = 0;\r\n\r\n p = p.parentNode;\r\n\r\n // when in element-fullscreen mode, do not go above the fullscreened-element\r\n while(p && p != root){\r\n top += p.offsetTop || 0\r\n left += p.offsetLeft || 0\r\n p = p.parentNode\r\n }\r\n\r\n return {top, left};\r\n }\r\n\r\n function getAccumulatedAncestorsScrollTop() {\r\n var scrollTop = 0,\r\n p = _sd.appendTarget.parentNode;\r\n\r\n while(p){\r\n scrollTop += p.scrollTop || 0;\r\n p = p.parentNode\r\n }\r\n\r\n return scrollTop;\r\n }\r\n\r\n if( !this.state.dropdown.visible ) return\r\n\r\n if( positionTo == 'text' ){\r\n rect = getCaretGlobalPosition()\r\n bottom = rect.bottom\r\n top = rect.top\r\n left = rect.left\r\n width = 'auto'\r\n }\r\n\r\n else{\r\n ancestorsOffsets = getAncestorsOffsets(appendTarget)\r\n rect = ddTarget.getBoundingClientRect()\r\n top = isSelfAppended ? -1 : rect.top - ancestorsOffsets.top\r\n bottom = (isSelfAppended ? rect.height : rect.bottom - ancestorsOffsets.top) - 1\r\n left = isSelfAppended ? -1 : rect.left - ancestorsOffsets.left\r\n width = rect.width + 'px'\r\n }\r\n\r\n // if the \"append target\" isn't the default, correct the `top` variable by ignoring any scrollTop of the target's Ancestors\r\n if( !isDefaultAppendTarget ) {\r\n let accumulatedAncestorsScrollTop = getAccumulatedAncestorsScrollTop()\r\n top += accumulatedAncestorsScrollTop\r\n bottom += accumulatedAncestorsScrollTop\r\n }\r\n\r\n top = Math.floor(top)\r\n bottom = Math.ceil(bottom)\r\n\r\n isPlacedAbove = _sd.placeAbove ?? viewportHeight - rect.bottom < ddHeight\r\n\r\n // flip vertically if there is no space for the dropdown below the input\r\n cssTop = (isPlacedAbove ? top : bottom) + appendTargetScrollTop;\r\n\r\n // \"pageXOffset\" property is an alias for \"scrollX\"\r\n cssLeft = `left: ${(left + (isRTL ? (rect.width || 0) : 0) + window.pageXOffset)}px;`\r\n\r\n // rtl = rtl ?? viewportWidth -\r\n ddElm.style.cssText = `${cssLeft}; top: ${cssTop}px; min-width: ${width}; max-width: ${width}`;\r\n\r\n ddElm.setAttribute('placement', isPlacedAbove ? 'top' : 'bottom')\r\n ddElm.setAttribute('position', positionTo)\r\n },\r\n}\r\n","const VERSION = 1; // current version of persisted data. if code change breaks persisted data, verison number should be bumped.\r\nconst STORE_KEY = '@yaireo/tagify/'\r\n\r\nexport const getPersistedData = id => key => {\r\n // if \"persist\" is \"false\", do not save to localstorage\r\n let customKey = '/'+key,\r\n persistedData,\r\n versionMatch = localStorage.getItem(STORE_KEY + id + '/v', VERSION) == VERSION\r\n\r\n if( versionMatch ){\r\n try{ persistedData = JSON.parse(localStorage[STORE_KEY + id + customKey]) }\r\n catch(err){}\r\n }\r\n\r\n return persistedData\r\n}\r\n\r\nexport const setPersistedData = id => {\r\n if( !id ) return () => {};\r\n\r\n // for storage invalidation\r\n localStorage.setItem(STORE_KEY + id + '/v', VERSION)\r\n\r\n return (data, key) => {\r\n let customKey = '/'+key,\r\n persistedData = JSON.stringify(data)\r\n\r\n if( data && key ){\r\n localStorage.setItem(STORE_KEY + id + customKey, persistedData)\r\n dispatchEvent( new Event('storage') )\r\n }\r\n }\r\n}\r\n\r\nexport const clearPersistedData = id => key => {\r\n const base = STORE_KEY + '/' + id + '/';\r\n\r\n // delete specific key in the storage\r\n if( key )\r\n localStorage.removeItem(base + key)\r\n\r\n // delete all keys in the storage with a specific tagify id\r\n else {\r\n for(let k in localStorage)\r\n if( k.includes(base) )\r\n localStorage.removeItem(k)\r\n }\r\n}\r\n","export default {\r\n empty : \"empty\",\r\n exceed : \"number of tags exceeded\",\r\n pattern : \"pattern mismatch\",\r\n duplicate : \"already exists\",\r\n notAllowed : \"not allowed\"\r\n}","export default {\r\n /**\r\n *\r\n * @param {DOM Object} input Original input DOm element\r\n * @param {Object} settings Tagify instance settings Object\r\n */\r\n wrapper(input, _s){\r\n return `\r\n \r\n ​\r\n `\r\n },\r\n\r\n tag(tagData, {settings: _s}){\r\n return `\r\n \r\n
\r\n ${tagData[_s.tagTextProp] || tagData.value}\r\n
\r\n
`\r\n },\r\n\r\n dropdown(settings){\r\n var _sd = settings.dropdown,\r\n isManual = _sd.position == 'manual';\r\n\r\n return `
\r\n
\r\n
`\r\n },\r\n\r\n dropdownContent(HTMLContent) {\r\n var _t = this.settings.templates,\r\n suggestions = this.state.dropdown.suggestions;\r\n\r\n return `\r\n ${_t.dropdownHeader.call(this, suggestions)}\r\n ${HTMLContent}\r\n ${_t.dropdownFooter.call(this, suggestions)}\r\n `\r\n },\r\n\r\n dropdownItem(item){\r\n return `
${item.mappedValue || item.value}
`\r\n },\r\n\r\n /**\r\n * @param {Array} suggestions An array of all the matched suggested items, including those which were sliced away due to the \"dropdown.maxItems\" setting\r\n */\r\n dropdownHeader(suggestions){\r\n return `
`\r\n },\r\n\r\n dropdownFooter(suggestions){\r\n var hasMore = suggestions.length - this.settings.dropdown.maxItems;\r\n\r\n return hasMore > 0\r\n ? `
\r\n ${hasMore} more items. Refine your search.\r\n
`\r\n : '';\r\n },\r\n\r\n dropdownItemNoMatch: null\r\n}\r\n","import { extend } from './helpers'\r\n\r\nexport default function EventDispatcher( instance ){\r\n // Create a DOM EventTarget object\r\n var target = document.createTextNode('')\r\n\r\n function addRemove(op, events, cb){\r\n if( cb )\r\n events.split(/\\s+/g).forEach(name => target[op + 'EventListener'].call(target, name, cb))\r\n }\r\n\r\n // Pass EventTarget interface calls to DOM EventTarget object\r\n return {\r\n off(events, cb){\r\n addRemove('remove', events, cb)\r\n return this\r\n },\r\n\r\n on(events, cb){\r\n if(cb && typeof cb == 'function')\r\n addRemove('add', events, cb)\r\n return this\r\n },\r\n\r\n trigger(eventName, data, opts){\r\n var e;\r\n\r\n opts = opts || {\r\n cloneData:true\r\n }\r\n\r\n if( !eventName ) return;\r\n\r\n if( instance.settings.isJQueryPlugin ){\r\n if( eventName == 'remove' ) eventName = 'removeTag' // issue #222\r\n jQuery(instance.DOM.originalInput).triggerHandler(eventName, [data])\r\n }\r\n else{\r\n try {\r\n var eventData = typeof data === 'object'\r\n ? data\r\n : {value:data};\r\n\r\n eventData = opts.cloneData ? extend({}, eventData) : eventData\r\n eventData.tagify = this\r\n\r\n if( data.event )\r\n eventData.event = this.cloneEvent(data.event)\r\n\r\n // TODO: move the below to the \"extend\" function\r\n if( data instanceof Object )\r\n for( var prop in data )\r\n if(data[prop] instanceof HTMLElement)\r\n eventData[prop] = data[prop]\r\n\r\n e = new CustomEvent(eventName, {\"detail\":eventData})\r\n }\r\n catch(err){ console.warn(err) }\r\n\r\n target.dispatchEvent(e);\r\n }\r\n }\r\n }\r\n}\r\n","import { decode, extend, getfirstTextNode, isChromeAndroidBrowser, isNodeTag, injectAtCaret, getSetTagData, fixCaretBetweenTags, placeCaretAfterNode } from './helpers'\r\nimport {ZERO_WIDTH_CHAR} from './constants'\r\n\r\nvar deleteBackspaceTimeout;\r\n\r\nexport function triggerChangeEvent(){\r\n if( this.settings.mixMode.integrated ) return;\r\n\r\n var inputElm = this.DOM.originalInput,\r\n changed = this.state.lastOriginalValueReported !== inputElm.value,\r\n event = new CustomEvent(\"change\", {bubbles: true}); // must use \"CustomEvent\" and not \"Event\" to support IE\r\n\r\n if( !changed ) return;\r\n\r\n // must apply this BEFORE triggering the simulated event\r\n this.state.lastOriginalValueReported = inputElm.value\r\n\r\n // React hack: https://github.com/facebook/react/issues/11488\r\n event.simulated = true\r\n if (inputElm._valueTracker)\r\n inputElm._valueTracker.setValue(Math.random())\r\n\r\n inputElm.dispatchEvent(event)\r\n\r\n // also trigger a Tagify event\r\n this.trigger(\"change\", this.state.lastOriginalValueReported)\r\n\r\n // React, for some reason, clears the input's value after \"dispatchEvent\" is fired\r\n inputElm.value = this.state.lastOriginalValueReported\r\n}\r\n\r\nexport default {\r\n // bind custom events which were passed in the settings\r\n customBinding(){\r\n this.customEventsList.forEach(name => {\r\n this.on(name, this.settings.callbacks[name])\r\n })\r\n },\r\n\r\n binding( bindUnbind = true ){\r\n var _s = this.settings,\r\n _CB = this.events.callbacks,\r\n _CBR,\r\n action = bindUnbind ? 'addEventListener' : 'removeEventListener';\r\n\r\n // do not allow the main events to be bound more than once\r\n if( this.state.mainEvents && bindUnbind )\r\n return;\r\n\r\n // set the binding state of the main events, so they will not be bound more than once\r\n this.state.mainEvents = bindUnbind;\r\n\r\n // everything inside gets executed only once-per instance\r\n if( bindUnbind && !this.listeners.main ){\r\n this.events.bindGlobal.call(this);\r\n\r\n if( this.settings.isJQueryPlugin )\r\n jQuery(this.DOM.originalInput).on('tagify.removeAllTags', this.removeAllTags.bind(this))\r\n }\r\n\r\n // setup callback references so events could be removed later\r\n _CBR = (this.listeners.main = this.listeners.main || {\r\n focus : ['input', _CB.onFocusBlur.bind(this)],\r\n keydown : ['input', _CB.onKeydown.bind(this)],\r\n click : ['scope', _CB.onClickScope.bind(this)],\r\n dblclick : _s.mode != 'select' && ['scope', _CB.onDoubleClickScope.bind(this)],\r\n paste : ['input', _CB.onPaste.bind(this)],\r\n drop : ['input', _CB.onDrop.bind(this)],\r\n compositionstart : ['input', _CB.onCompositionStart.bind(this)],\r\n compositionend : ['input', _CB.onCompositionEnd.bind(this)]\r\n })\r\n\r\n for( var eventName in _CBR ){\r\n _CBR[eventName] && this.DOM[_CBR[eventName][0]][action](eventName, _CBR[eventName][1]);\r\n }\r\n\r\n\r\n // listen to original input changes (unfortunetly this is the best way...)\r\n // https://stackoverflow.com/a/1949416/104380\r\n clearInterval(this.listeners.main.originalInputValueObserverInterval)\r\n this.listeners.main.originalInputValueObserverInterval = setInterval(_CB.observeOriginalInputValue.bind(this), 500)\r\n\r\n // observers\r\n var inputMutationObserver = this.listeners.main.inputMutationObserver || new MutationObserver(_CB.onInputDOMChange.bind(this));\r\n\r\n // cleaup just-in-case\r\n inputMutationObserver.disconnect()\r\n\r\n // observe stuff\r\n if( _s.mode == 'mix' ) {\r\n inputMutationObserver.observe(this.DOM.input, {childList:true})\r\n }\r\n },\r\n\r\n bindGlobal( unbind ) {\r\n var _CB = this.events.callbacks,\r\n action = unbind ? 'removeEventListener' : 'addEventListener',\r\n e;\r\n\r\n if( !this.listeners || (!unbind && this.listeners.global) ) return; // do not re-bind\r\n\r\n // these events are global and should never be unbinded, unless the instance is destroyed:\r\n this.listeners.global = this.listeners.global || [\r\n {\r\n type: this.isIE ? 'keydown' : 'input', // IE cannot register \"input\" events on contenteditable elements, so the \"keydown\" should be used instead..\r\n target: this.DOM.input,\r\n cb: _CB[this.isIE ? 'onInputIE' : 'onInput'].bind(this)\r\n },\r\n {\r\n type: 'keydown',\r\n target: window,\r\n cb: _CB.onWindowKeyDown.bind(this)\r\n },\r\n {\r\n type: 'blur',\r\n target: this.DOM.input,\r\n cb: _CB.onFocusBlur.bind(this)\r\n },\r\n {\r\n type: 'click',\r\n target: document,\r\n cb: _CB.onClickAnywhere.bind(this),\r\n useCapture: true\r\n },\r\n ]\r\n\r\n for( e of this.listeners.global )\r\n e.target[action](e.type, e.cb, !!e.useCapture);\r\n },\r\n\r\n unbindGlobal() {\r\n this.events.bindGlobal.call(this, true);\r\n },\r\n\r\n /**\r\n * DOM events callbacks\r\n */\r\n callbacks : {\r\n onFocusBlur(e){\r\n var _s = this.settings,\r\n text = e.target ? this.trim(e.target.textContent) : '', // a string\r\n currentDisplayValue = this.value?.[0]?.[_s.tagTextProp],\r\n type = e.type,\r\n ddEnabled = _s.dropdown.enabled >= 0,\r\n eventData = {relatedTarget:e.relatedTarget},\r\n isTargetSelectOption = this.state.actions.selectOption && (ddEnabled || !_s.dropdown.closeOnSelect),\r\n isTargetAddNewBtn = this.state.actions.addNew && ddEnabled,\r\n shouldAddTags;\r\n\r\n if( type == 'blur' ){\r\n if( e.relatedTarget === this.DOM.scope ){\r\n this.dropdown.hide()\r\n this.DOM.input.focus()\r\n return\r\n }\r\n\r\n this.postUpdate()\r\n _s.onChangeAfterBlur && this.triggerChangeEvent()\r\n }\r\n\r\n if( isTargetSelectOption || isTargetAddNewBtn )\r\n return;\r\n\r\n this.state.hasFocus = type == \"focus\" ? +new Date() : false\r\n this.toggleFocusClass(this.state.hasFocus)\r\n\r\n if( _s.mode == 'mix' ){\r\n if( type == \"focus\" ){\r\n this.trigger(\"focus\", eventData)\r\n }\r\n\r\n else if( e.type == \"blur\" ){\r\n this.trigger(\"blur\", eventData)\r\n this.loading(false)\r\n this.dropdown.hide()\r\n // reset state which needs reseting\r\n this.state.dropdown.visible = undefined\r\n this.setStateSelection()\r\n }\r\n\r\n return\r\n }\r\n\r\n if( type == \"focus\" ){\r\n this.trigger(\"focus\", eventData)\r\n // e.target.classList.remove('placeholder');\r\n if( (_s.dropdown.enabled === 0 || !_s.userInput) && !this.state.dropdown.visible ){ // && _s.mode != \"select\"\r\n this.dropdown.show(this.value.length ? '' : undefined)\r\n }\r\n return\r\n }\r\n\r\n else if( type == \"blur\" ){\r\n this.trigger(\"blur\", eventData)\r\n this.loading(false)\r\n\r\n // when clicking the X button of a selected tag, it is unwanted for it to be added back\r\n // again in a few more lines of code (shouldAddTags && addTags)\r\n if( _s.mode == 'select' ) {\r\n // if nothing has changed (same display value), do not add a tag\r\n if( currentDisplayValue === text )\r\n text = ''\r\n }\r\n\r\n shouldAddTags = text && !this.state.actions.selectOption && _s.addTagOnBlur && _s.addTagOn.includes('blur');\r\n\r\n // do not add a tag if \"selectOption\" action was just fired (this means a tag was just added from the dropdown)\r\n shouldAddTags && this.addTags(text, true)\r\n }\r\n\r\n this.DOM.input.removeAttribute('style')\r\n this.dropdown.hide()\r\n },\r\n\r\n onCompositionStart(e){\r\n this.state.composing = true\r\n },\r\n\r\n onCompositionEnd(e){\r\n this.state.composing = false\r\n },\r\n\r\n onWindowKeyDown(e){\r\n var focusedElm = document.activeElement,\r\n isTag = isNodeTag.call(this, focusedElm),\r\n isBelong = isTag && this.DOM.scope.contains(document.activeElement),\r\n isReadyOnlyTag = isBelong && focusedElm.hasAttribute('readonly'),\r\n nextTag;\r\n\r\n if( !isBelong || isReadyOnlyTag ) return;\r\n\r\n nextTag = focusedElm.nextElementSibling\r\n\r\n switch( e.key ){\r\n // remove tag if has focus\r\n case 'Backspace': {\r\n if( !this.settings.readonly ) {\r\n this.removeTags(focusedElm);\r\n (nextTag ? nextTag : this.DOM.input).focus()\r\n }\r\n\r\n break;\r\n }\r\n\r\n // edit tag if has focus\r\n case 'Enter': {\r\n setTimeout(this.editTag.bind(this), 0, focusedElm);\r\n break;\r\n }\r\n }\r\n },\r\n\r\n onKeydown(e){\r\n var _s = this.settings;\r\n\r\n // ignore keys during IME composition or when user input is not allowed\r\n if( this.state.composing || !_s.userInput )\r\n return\r\n\r\n if( _s.mode == 'select' && _s.enforceWhitelist && this.value.length && e.key != 'Tab' ){\r\n e.preventDefault()\r\n }\r\n\r\n var s = this.trim(e.target.textContent);\r\n\r\n this.trigger(\"keydown\", {event:e})\r\n\r\n _s.hooks.beforeKeyDown(e, {tagify:this})\r\n .then(result => {\r\n /**\r\n * ONLY FOR MIX-MODE:\r\n */\r\n if( _s.mode == 'mix' ){\r\n switch( e.key ){\r\n case 'Left' :\r\n case 'ArrowLeft' : {\r\n // when left arrow was pressed, set a flag so when the dropdown is shown, right-arrow will be ignored\r\n // because it seems likely the user wishes to use the arrows to move the caret\r\n this.state.actions.ArrowLeft = true\r\n break\r\n }\r\n\r\n case 'Delete':\r\n case 'Backspace' : {\r\n if( this.state.editing ) return\r\n\r\n var sel = document.getSelection(),\r\n deleteKeyTagDetected = e.key == 'Delete' && sel.anchorOffset == (sel.anchorNode.length || 0),\r\n prevAnchorSibling = sel.anchorNode.previousSibling,\r\n isCaretAfterTag = sel.anchorNode.nodeType == 1 || !sel.anchorOffset && prevAnchorSibling && prevAnchorSibling.nodeType == 1 && sel.anchorNode.previousSibling,\r\n lastInputValue = decode(this.DOM.input.innerHTML),\r\n lastTagElems = this.getTagElms(),\r\n isZWS = sel.anchorNode.length === 1 && sel.anchorNode.nodeValue == String.fromCharCode(8203),\r\n // isCaretInsideTag = sel.anchorNode.parentNode('.' + _s.classNames.tag),\r\n tagBeforeCaret,\r\n tagElmToBeDeleted,\r\n firstTextNodeBeforeTag;\r\n\r\n if( _s.backspace == 'edit' && isCaretAfterTag ){\r\n tagBeforeCaret = sel.anchorNode.nodeType == 1 ? null : sel.anchorNode.previousElementSibling;\r\n setTimeout(this.editTag.bind(this), 0, tagBeforeCaret); // timeout is needed to the last cahacrter in the edited tag won't get deleted\r\n e.preventDefault() // needed so the tag elm won't get deleted\r\n return;\r\n }\r\n\r\n if( isChromeAndroidBrowser() && isCaretAfterTag instanceof Element ){\r\n firstTextNodeBeforeTag = getfirstTextNode(isCaretAfterTag)\r\n\r\n if( !isCaretAfterTag.hasAttribute('readonly') )\r\n isCaretAfterTag.remove() // since this is Chrome, can safetly use this \"new\" DOM API\r\n\r\n // Android-Chrome wrongly hides the keyboard, and loses focus,\r\n // so this hack below is needed to regain focus at the correct place:\r\n this.DOM.input.focus()\r\n setTimeout(() => {\r\n placeCaretAfterNode(firstTextNodeBeforeTag)\r\n this.DOM.input.click()\r\n\r\n })\r\n\r\n return\r\n }\r\n\r\n if( sel.anchorNode.nodeName == 'BR')\r\n return\r\n\r\n if( (deleteKeyTagDetected || isCaretAfterTag) && sel.anchorNode.nodeType == 1 )\r\n if( sel.anchorOffset == 0 ) // caret is at the very begining, before a tag\r\n tagElmToBeDeleted = deleteKeyTagDetected // delete key pressed\r\n ? lastTagElems[0]\r\n : null;\r\n else\r\n tagElmToBeDeleted = lastTagElems[Math.min(lastTagElems.length, sel.anchorOffset) - 1]\r\n\r\n // find out if a tag *might* be a candidate for deletion, and if so, which\r\n else if( deleteKeyTagDetected )\r\n tagElmToBeDeleted = sel.anchorNode.nextElementSibling;\r\n\r\n else if( isCaretAfterTag instanceof Element )\r\n tagElmToBeDeleted = isCaretAfterTag;\r\n\r\n // tagElm.hasAttribute('readonly')\r\n if( sel.anchorNode.nodeType == 3 && // node at caret location is a Text node\r\n !sel.anchorNode.nodeValue && // has some text\r\n sel.anchorNode.previousElementSibling ) // text node has a Tag node before it\r\n e.preventDefault()\r\n\r\n // if backspace not allowed, do nothing\r\n // TODO: a better way to detect if nodes were deleted is to simply check the \"this.value\" before & after\r\n if( (isCaretAfterTag || deleteKeyTagDetected) && !_s.backspace ){\r\n e.preventDefault()\r\n return\r\n }\r\n\r\n if( sel.type != 'Range' && !sel.anchorOffset && sel.anchorNode == this.DOM.input && e.key != 'Delete' ){\r\n e.preventDefault()\r\n return\r\n }\r\n\r\n if( sel.type != 'Range' && tagElmToBeDeleted && tagElmToBeDeleted.hasAttribute('readonly') ){\r\n // allows the continuation of deletion by placing the caret on the first previous textNode.\r\n // since a few readonly-tags might be one after the other, iteration is needed:\r\n\r\n placeCaretAfterNode( getfirstTextNode(tagElmToBeDeleted) )\r\n return\r\n }\r\n\r\n if ( e.key == 'Delete' && isZWS && getSetTagData(sel.anchorNode.nextSibling) ) {\r\n this.removeTags(sel.anchorNode.nextSibling)\r\n }\r\n\r\n // update regarding https://github.com/yairEO/tagify/issues/762#issuecomment-786464317:\r\n // the bug described is more severe than the fix below, therefore I disable the fix until a solution\r\n // is found which work well for both cases.\r\n // -------\r\n // nodeType is \"1\" only when the caret is at the end after last tag (no text after), or before first first (no text before)\r\n /*\r\n if( this.isFirefox && sel.anchorNode.nodeType == 1 && sel.anchorOffset != 0 ){\r\n this.removeTags() // removes last tag by default if no parameter supplied\r\n // place caret inside last textNode, if exist. it's an annoying bug only in FF,\r\n // if the last tag is removed, and there is a textNode before it, the caret is not placed at its end\r\n placeCaretAfterNode( setRangeAtStartEnd(false, this.DOM.input) )\r\n }\r\n */\r\n\r\n clearTimeout(deleteBackspaceTimeout)\r\n // a minimum delay is needed before the node actually gets detached from the document (don't know why),\r\n // to know exactly which tag was deleted. This is the easiest way of knowing besides using MutationObserver\r\n deleteBackspaceTimeout = setTimeout(() => {\r\n var sel = document.getSelection(),\r\n currentValue = decode(this.DOM.input.innerHTML),\r\n prevElm = !deleteKeyTagDetected && sel.anchorNode.previousSibling;\r\n\r\n // fixes #384, where the first and only tag will not get removed with backspace\r\n /*\r\n * [UPDATE DEC 3, 22] SEEMS BELOEW CODE IS NOT NEEDED ANY MORE\r\n *\r\n if( currentValue.length > lastInputValue.length && prevElm ){\r\n if( isNodeTag.call(this, prevElm) && !prevElm.hasAttribute('readonly') ){\r\n this.removeTags(prevElm)\r\n this.fixFirefoxLastTagNoCaret()\r\n\r\n // the above \"removeTag\" methods removes the tag with a transition. Chrome adds a
element for some reason at this stage\r\n if( this.DOM.input.children.length == 2 && this.DOM.input.children[1].tagName == \"BR\" ){\r\n this.DOM.input.innerHTML = \"\"\r\n this.value.length = 0\r\n return true\r\n }\r\n }\r\n\r\n else\r\n prevElm.remove()\r\n }\r\n */\r\n\r\n // find out which tag(s) were deleted and trigger \"remove\" event\r\n // iterate over the list of tags still in the document and then filter only those from the \"this.value\" collection\r\n this.value = [].map.call(lastTagElems, (node, nodeIdx) => {\r\n var tagData = getSetTagData(node)\r\n\r\n // since readonly cannot be removed (it's technically resurrected if removed somehow)\r\n if( node.parentNode || tagData.readonly )\r\n return tagData\r\n else\r\n this.trigger('remove', { tag:node, index:nodeIdx, data:tagData })\r\n })\r\n .filter(n=>n) // remove empty items in the mapped array\r\n }, 20) // Firefox needs this higher duration for some reason or things get buggy when deleting text from the end\r\n break;\r\n }\r\n // currently commented to allow new lines in mixed-mode\r\n // case 'Enter' :\r\n // // e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380\r\n }\r\n\r\n return true\r\n }\r\n\r\n var isManualDropdown = _s.dropdown.position == 'manual';\r\n\r\n switch( e.key ){\r\n case 'Backspace' :\r\n if( _s.mode == 'select' && _s.enforceWhitelist && this.value.length)\r\n this.removeTags()\r\n\r\n else if( !this.state.dropdown.visible || _s.dropdown.position == 'manual' ){\r\n if( e.target.textContent == \"\" || s.charCodeAt(0) == 8203 ){ // 8203: ZERO WIDTH SPACE unicode\r\n if( _s.backspace === true )\r\n this.removeTags()\r\n else if( _s.backspace == 'edit' )\r\n setTimeout(this.editTag.bind(this), 0) // timeout reason: when edited tag gets focused and the caret is placed at the end, the last character gets deletec (because of backspace)\r\n }\r\n }\r\n break;\r\n\r\n case 'Esc' :\r\n case 'Escape' :\r\n if( this.state.dropdown.visible ) return\r\n e.target.blur()\r\n break;\r\n\r\n case 'Down' :\r\n case 'ArrowDown' :\r\n // if( _s.mode == 'select' ) // issue #333\r\n if( !this.state.dropdown.visible )\r\n this.dropdown.show()\r\n break;\r\n\r\n case 'ArrowRight' : {\r\n let tagData = this.state.inputSuggestion || this.state.ddItemData\r\n if( tagData && _s.autoComplete.rightKey ){\r\n this.addTags([tagData], true)\r\n return;\r\n }\r\n break\r\n }\r\n case 'Tab' : {\r\n let selectMode = _s.mode == 'select'\r\n if(s && !selectMode) e.preventDefault()\r\n else return true;\r\n }\r\n\r\n case 'Enter' :\r\n // manual suggestion boxes are assumed to always be visible\r\n if( this.state.dropdown.visible && !isManualDropdown ) return\r\n e.preventDefault(); // solves Chrome bug - http://stackoverflow.com/a/20398191/104380\r\n // because the main \"keydown\" event is bound before the dropdown events, this will fire first and will not *yet*\r\n // know if an option was just selected from the dropdown menu. If an option was selected,\r\n // the dropdown events should handle adding the tag\r\n\r\n setTimeout(()=>{\r\n if( (!this.state.dropdown.visible || isManualDropdown) && !this.state.actions.selectOption && _s.addTagOn.includes(e.key.toLowerCase()) )\r\n this.addTags(s, true)\r\n })\r\n }\r\n })\r\n .catch(err => err)\r\n },\r\n\r\n onInput(e){\r\n this.postUpdate() // toggles \"tagify--empty\" class\r\n\r\n var _s = this.settings;\r\n\r\n if( _s.mode == 'mix' )\r\n return this.events.callbacks.onMixTagsInput.call(this, e);\r\n\r\n var value = this.input.normalize.call(this, undefined, {trim: false}),\r\n showSuggestions = value.length >= _s.dropdown.enabled,\r\n eventData = {value, inputElm:this.DOM.input},\r\n validation = this.validateTag({value});\r\n\r\n if( _s.mode == 'select' ) {\r\n this.toggleScopeValidation(validation)\r\n }\r\n\r\n eventData.isValid = validation;\r\n\r\n // for IE; since IE doesn't have an \"input\" event so \"keyDown\" is used instead to trigger the \"onInput\" callback,\r\n // and so many keys do not change the input, and for those do not continue.\r\n if( this.state.inputText == value ) return;\r\n\r\n // save the value on the input's State object\r\n this.input.set.call(this, value, false); // update the input with the normalized value and run validations\r\n // this.setRangeAtStartEnd(false, this.DOM.input); // fix caret position\r\n\r\n // if delimiters detected, add tags\r\n if( value.search(_s.delimiters) != -1 ){\r\n if( this.addTags( value ) ){\r\n this.input.set.call(this); // clear the input field's value\r\n }\r\n }\r\n\r\n else if( _s.dropdown.enabled >= 0 ){\r\n this.dropdown[showSuggestions ? \"show\" : \"hide\"](value);\r\n }\r\n\r\n this.trigger('input', eventData) // \"input\" event must be triggered at this point, before the dropdown is shown\r\n },\r\n\r\n onMixTagsInput( e ){\r\n var rangeText, match, matchedPatternCount, tag, showSuggestions, selection,\r\n _s = this.settings,\r\n lastTagsCount = this.value.length,\r\n matchFlaggedTag,\r\n matchDelimiters,\r\n tagsElems = this.getTagElms(),\r\n fragment = document.createDocumentFragment(),\r\n range = window.getSelection().getRangeAt(0),\r\n remainingTagsValues = [].map.call(tagsElems, node => getSetTagData(node).value);\r\n\r\n // Android Chrome \"keydown\" event argument does not report the correct \"key\".\r\n // this workaround is needed to manually call \"onKeydown\" method with a synthesized event object\r\n if( e.inputType == \"deleteContentBackward\" && isChromeAndroidBrowser() ){\r\n this.events.callbacks.onKeydown.call(this, {\r\n target: e.target,\r\n key: \"Backspace\",\r\n })\r\n }\r\n\r\n // if there's a tag as the first child of the input, always make sure it has a zero-width character before it\r\n // or if two tags are next to each-other, add a zero-space width character (For the caret to appear)\r\n fixCaretBetweenTags(this.getTagElms())\r\n\r\n // re-add \"readonly\" tags which might have been removed\r\n this.value.slice().forEach(item => {\r\n if( item.readonly && !remainingTagsValues.includes(item.value) )\r\n fragment.appendChild( this.createTagElem(item) )\r\n })\r\n\r\n if( fragment.childNodes.length ){\r\n range.insertNode(fragment)\r\n this.setRangeAtStartEnd(false, fragment.lastChild)\r\n }\r\n\r\n // check if tags were \"magically\" added/removed (browser redo/undo or CTRL-A -> delete)\r\n if( tagsElems.length != lastTagsCount ){\r\n this.value = [].map.call(this.getTagElms(), node => getSetTagData(node))\r\n this.update({ withoutChangeEvent:true })\r\n return\r\n }\r\n\r\n if( this.hasMaxTags() )\r\n return true\r\n\r\n if( window.getSelection ){\r\n selection = window.getSelection()\r\n\r\n // only detect tags if selection is inside a textNode (not somehow on already-existing tag)\r\n if( selection.rangeCount > 0 && selection.anchorNode.nodeType == 3 ){\r\n range = selection.getRangeAt(0).cloneRange()\r\n range.collapse(true)\r\n range.setStart(selection.focusNode, 0)\r\n\r\n rangeText = range.toString().slice(0, range.endOffset) // slice the range so everything AFTER the caret will be trimmed\r\n // split = range.toString().split(_s.mixTagsAllowedAfter) // [\"foo\", \"bar\", \"@baz\"]\r\n matchedPatternCount = rangeText.split(_s.pattern).length - 1;\r\n\r\n match = rangeText.match( _s.pattern )\r\n\r\n if( match )\r\n // tag string, example: \"@aaa ccc\"\r\n tag = rangeText.slice( rangeText.lastIndexOf(match[match.length-1]) )\r\n\r\n if( tag ){\r\n this.state.actions.ArrowLeft = false // start fresh, assuming the user did not (yet) used any arrow to move the caret\r\n this.state.tag = {\r\n prefix : tag.match(_s.pattern)[0],\r\n value : tag.replace(_s.pattern, ''), // get rid of the prefix\r\n }\r\n this.state.tag.baseOffset = selection.baseOffset - this.state.tag.value.length\r\n\r\n matchDelimiters = this.state.tag.value.match(_s.delimiters)\r\n // if a delimeter exists, add the value as tag (exluding the delimiter)\r\n if( matchDelimiters ){\r\n this.state.tag.value = this.state.tag.value.replace(_s.delimiters, '')\r\n this.state.tag.delimiters = matchDelimiters[0]\r\n this.addTags(this.state.tag.value, _s.dropdown.clearOnSelect)\r\n this.dropdown.hide()\r\n return\r\n }\r\n\r\n showSuggestions = this.state.tag.value.length >= _s.dropdown.enabled\r\n\r\n // When writing something that might look like a tag (an email address) but isn't one - it is unwanted\r\n // the suggestions dropdown be shown, so the user can close it (in any way), and while continue typing,\r\n // dropdown should stay closed until another tag is typed.\r\n // if( this.state.tag.value.length && this.state.dropdown.visible === false )\r\n // showSuggestions = false\r\n\r\n // test for similar flagged tags to the current tag\r\n\r\n try{\r\n matchFlaggedTag = this.state.flaggedTags[this.state.tag.baseOffset]\r\n matchFlaggedTag = matchFlaggedTag.prefix == this.state.tag.prefix &&\r\n matchFlaggedTag.value[0] == this.state.tag.value[0]\r\n\r\n // reset\r\n if( this.state.flaggedTags[this.state.tag.baseOffset] && !this.state.tag.value )\r\n delete this.state.flaggedTags[this.state.tag.baseOffset];\r\n }\r\n catch(err){}\r\n\r\n // scenario: (do not show suggestions of another matched tag, if more than one detected)\r\n // (2 tags exist) \" a@a.com and @\"\r\n // (second tag is removed by backspace) \" a@a.com and \"\r\n if( matchFlaggedTag || matchedPatternCount < this.state.mixMode.matchedPatternCount )\r\n showSuggestions = false\r\n }\r\n // no (potential) tag found\r\n else{\r\n this.state.flaggedTags = {}\r\n }\r\n\r\n this.state.mixMode.matchedPatternCount = matchedPatternCount\r\n }\r\n }\r\n\r\n\r\n // wait until the \"this.value\" has been updated (see \"onKeydown\" method for \"mix-mode\")\r\n // the dropdown must be shown only after this event has been triggered, so an implementer could\r\n // dynamically change the whitelist.\r\n setTimeout(()=>{\r\n this.update({withoutChangeEvent:true})\r\n this.trigger('input', extend({}, this.state.tag, {textContent:this.DOM.input.textContent}))\r\n\r\n if( this.state.tag )\r\n this.dropdown[showSuggestions ? \"show\" : \"hide\"](this.state.tag.value);\r\n }, 10)\r\n },\r\n\r\n onInputIE(e){\r\n var _this = this;\r\n // for the \"e.target.textContent\" to be changed, the browser requires a small delay\r\n setTimeout(function(){\r\n _this.events.callbacks.onInput.call(_this, e)\r\n })\r\n },\r\n\r\n observeOriginalInputValue(){\r\n // if, for some reason, the Tagified element is no longer in the DOM,\r\n // call the \"destroy\" method to kill all references to timeouts/intervals\r\n if( !this.DOM.originalInput.parentNode ) this.destroy()\r\n\r\n // if original input value changed for some reason (for exmaple a form reset)\r\n if( this.DOM.originalInput.value != this.DOM.originalInput.tagifyValue )\r\n this.loadOriginalValues()\r\n },\r\n\r\n onClickAnywhere(e){\r\n if (e.target != this.DOM.scope && !this.DOM.scope.contains(e.target)) {\r\n this.toggleFocusClass(false)\r\n this.state.hasFocus = false\r\n !this.settings.userInput && this.dropdown.hide()\r\n }\r\n },\r\n\r\n onClickScope(e){\r\n var _s = this.settings,\r\n tagElm = e.target.closest('.' + _s.classNames.tag),\r\n isScope = e.target === this.DOM.scope,\r\n timeDiffFocus = +new Date() - this.state.hasFocus;\r\n\r\n if( isScope && _s.mode != 'select' ){\r\n // if( !this.state.hasFocus )\r\n this.DOM.input.focus()\r\n return\r\n }\r\n\r\n else if( e.target.classList.contains(_s.classNames.tagX) ){\r\n this.removeTags( e.target.parentNode )\r\n return\r\n }\r\n\r\n else if( tagElm && !this.state.editing ){\r\n this.trigger(\"click\", { tag:tagElm, index:this.getNodeIndex(tagElm), data:getSetTagData(tagElm), event:e })\r\n\r\n if( _s.editTags === 1 || _s.editTags.clicks === 1 || _s.mode == 'select' )\r\n this.events.callbacks.onDoubleClickScope.call(this, e)\r\n\r\n return\r\n }\r\n\r\n // when clicking on the input itself\r\n else if( e.target == this.DOM.input ){\r\n if( _s.mode == 'mix' ){\r\n // firefox won't show caret if last element is a tag (and not a textNode),\r\n // so an empty textnode should be added\r\n this.fixFirefoxLastTagNoCaret()\r\n }\r\n\r\n if( timeDiffFocus > 500 ){\r\n if( this.state.dropdown.visible )\r\n this.dropdown.hide()\r\n else if( _s.dropdown.enabled === 0 && _s.mode != 'mix' )\r\n this.dropdown.show(this.value.length ? '' : undefined)\r\n return\r\n }\r\n }\r\n\r\n if( _s.mode == 'select' && _s.dropdown.enabled === 0 && !this.state.dropdown.visible) {\r\n this.events.callbacks.onDoubleClickScope.call(this, {...e, target: this.getTagElms()[0]})\r\n\r\n !_s.userInput && this.dropdown.show()\r\n }\r\n },\r\n\r\n // special proccess is needed for pasted content in order to \"clean\" it\r\n onPaste(e){\r\n e.preventDefault()\r\n\r\n var tagsElems,\r\n _s = this.settings,\r\n selectModeWithoutInput =_s.mode == 'select' && _s.enforceWhitelist;\r\n\r\n if( selectModeWithoutInput || !_s.userInput ){\r\n return false;\r\n }\r\n\r\n var clipboardData, pastedText;\r\n\r\n if( _s.readonly ) return\r\n\r\n // Get pasted data via clipboard API\r\n clipboardData = e.clipboardData || window.clipboardData\r\n pastedText = clipboardData.getData('Text')\r\n\r\n _s.hooks.beforePaste(e, {tagify:this, pastedText, clipboardData})\r\n .then(result => {\r\n if( result === undefined )\r\n result = pastedText;\r\n\r\n if( result ){\r\n this.injectAtCaret(result, window.getSelection().getRangeAt(0))\r\n\r\n if( this.settings.mode == 'mix' ){\r\n this.events.callbacks.onMixTagsInput.call(this, e);\r\n }\r\n\r\n else if( this.settings.pasteAsTags ){\r\n tagsElems = this.addTags(this.state.inputText + result, true)\r\n }\r\n\r\n else {\r\n this.state.inputText = result\r\n this.dropdown.show(result)\r\n }\r\n }\r\n\r\n this.trigger('paste', {event: e, pastedText, clipboardData, tagsElems})\r\n })\r\n .catch(err => err)\r\n },\r\n\r\n onDrop(e){\r\n e.preventDefault()\r\n },\r\n\r\n onEditTagInput( editableElm, e ){\r\n var tagElm = editableElm.closest('.' + this.settings.classNames.tag),\r\n tagElmIdx = this.getNodeIndex(tagElm),\r\n tagData = getSetTagData(tagElm),\r\n textValue = this.input.normalize.call(this, editableElm),\r\n dataForChangedProp = {[this.settings.tagTextProp]: textValue, __tagId: tagData.__tagId}, // \"__tagId\" is needed so validation will skip current tag when checking for dups\r\n isValid = this.validateTag(dataForChangedProp), // the value could have been invalid in the first-place so make sure to re-validate it (via \"addEmptyTag\" method)\r\n hasChanged = this.editTagChangeDetected(extend(tagData, dataForChangedProp));\r\n\r\n // if the value is same as before-editing and the tag was valid before as well, ignore the current \"isValid\" result, which is false-positive\r\n if( !hasChanged && editableElm.originalIsValid === true )\r\n isValid = true\r\n\r\n tagElm.classList.toggle(this.settings.classNames.tagInvalid, isValid !== true)\r\n tagData.__isValid = isValid\r\n\r\n tagElm.title = isValid === true\r\n ? tagData.title || tagData.value\r\n : isValid // change the tag's title to indicate why is the tag invalid (if it's so)\r\n\r\n // show dropdown if typed text is equal or more than the \"enabled\" dropdown setting\r\n if( textValue.length >= this.settings.dropdown.enabled ){\r\n // this check is needed apparently because doing browser \"undo\" will fire\r\n // \"onEditTagInput\" but \"this.state.editing\" will be \"false\"\r\n if( this.state.editing )\r\n this.state.editing.value = textValue\r\n this.dropdown.show(textValue)\r\n }\r\n\r\n this.trigger(\"edit:input\", {\r\n tag : tagElm,\r\n index: tagElmIdx,\r\n data : extend({}, this.value[tagElmIdx], {newValue:textValue}),\r\n event: e\r\n })\r\n },\r\n\r\n onEditTagPaste( tagElm, e ){\r\n // Get pasted data via clipboard API\r\n var clipboardData = e.clipboardData || window.clipboardData,\r\n pastedText = clipboardData.getData('Text');\r\n\r\n e.preventDefault()\r\n\r\n var newNode = injectAtCaret(pastedText)\r\n this.setRangeAtStartEnd(false, newNode)\r\n },\r\n\r\n onEditTagClick( tagElm, e) {\r\n this.events.callbacks.onClickScope.call(this, e)\r\n },\r\n\r\n onEditTagFocus( tagElm ){\r\n this.state.editing = {\r\n scope: tagElm,\r\n input: tagElm.querySelector(\"[contenteditable]\")\r\n }\r\n },\r\n\r\n onEditTagBlur( editableElm, e ){\r\n // if \"relatedTarget\" is the tag then do not continue as this should not be considered a \"blur\" event\r\n var isRelatedTargetNodeTag = isNodeTag.call(this, e.relatedTarget)\r\n\r\n // in \"select-mode\" when editing the tag's template to include more nodes other than the editable \"span\",\r\n // clicking those elements should not be considered a blur event\r\n if( isRelatedTargetNodeTag && e.relatedTarget.contains(e.target) ) {\r\n this.dropdown.hide()\r\n return\r\n }\r\n\r\n // is \"ESC\" key was pressed then the \"editing\" state should be `false` and if so, logic should not continue\r\n // because \"ESC\" reverts the edited tag back to how it was (replace the node) before editing\r\n if( !this.state.editing )\r\n return;\r\n\r\n if( !this.state.hasFocus )\r\n this.toggleFocusClass()\r\n\r\n // one scenario is when selecting a suggestion from the dropdown, when editing, and by selecting it\r\n // the \"onEditTagDone\" is called directly, already replacing the tag, so the argument \"editableElm\"\r\n // node isn't in the DOM anynmore because it has been replaced.\r\n if( !this.DOM.scope.contains(editableElm) ) return;\r\n\r\n var _s = this.settings,\r\n tagElm = editableElm.closest('.' + _s.classNames.tag),\r\n tagData = getSetTagData(tagElm),\r\n textValue = this.input.normalize.call(this, editableElm),\r\n dataForChangedProp = {[_s.tagTextProp]: textValue, __tagId: tagData.__tagId}, // \"__tagId\" is needed so validation will skip current tag when checking for dups\r\n originalData = tagData.__originalData, // pre-edit data\r\n hasChanged = this.editTagChangeDetected(extend(tagData, dataForChangedProp)),\r\n isValid = this.validateTag(dataForChangedProp), // \"__tagId\" is needed so validation will skip current tag when checking for dups\r\n hasMaxTags,\r\n newTagData;\r\n\r\n if( !textValue ){\r\n this.onEditTagDone(tagElm)\r\n return\r\n }\r\n\r\n // if nothing changed revert back to how it was before editing\r\n if( !hasChanged ){\r\n this.onEditTagDone(tagElm, originalData)\r\n return\r\n }\r\n\r\n // need to know this because if \"keepInvalidTags\" setting is \"true\" and an invalid tag is edited as a valid one,\r\n // but the maximum number of tags have alreay been reached, so it should not allow saving the new valid value.\r\n // only if the tag was already valid before editing, ignore this check (see a few lines below)\r\n hasMaxTags = this.hasMaxTags()\r\n\r\n newTagData = extend(\r\n {},\r\n originalData,\r\n {\r\n [_s.tagTextProp]: this.trim(textValue),\r\n __isValid: isValid\r\n }\r\n )\r\n\r\n // pass through optional transformer defined in settings\r\n _s.transformTag.call(this, newTagData, originalData)\r\n\r\n // MUST re-validate after tag transformation\r\n // only validate the \"tagTextProp\" because is the only thing that metters for validating an edited tag.\r\n // -- Scenarios: --\r\n // 1. max 3 tags allowd. there are 4 tags, one has invalid input and is edited to a valid one, and now should be marked as \"not allowed\" because limit of tags has reached\r\n // 2. max 3 tags allowed. there are 3 tags, one is edited, and so max-tags vaildation should be OK\r\n isValid = (!hasMaxTags || originalData.__isValid === true) && this.validateTag(newTagData)\r\n\r\n if( isValid !== true ){\r\n this.trigger(\"invalid\", { data:newTagData, tag:tagElm, message:isValid })\r\n\r\n // do nothing if invalid, stay in edit-mode until corrected or reverted by presssing esc\r\n if( _s.editTags.keepInvalid ) return\r\n\r\n if( _s.keepInvalidTags )\r\n newTagData.__isValid = isValid\r\n else\r\n // revert back if not specified to keep\r\n newTagData = originalData\r\n }\r\n\r\n else if( _s.keepInvalidTags ){\r\n // cleaup any previous leftovers if the tag was invalid\r\n delete newTagData.title\r\n delete newTagData[\"aria-invalid\"]\r\n delete newTagData.class\r\n }\r\n\r\n // tagElm.classList.toggle(_s.classNames.tagInvalid, true)\r\n\r\n this.onEditTagDone(tagElm, newTagData)\r\n },\r\n\r\n onEditTagkeydown(e, tagElm){\r\n // ignore keys during IME composition\r\n if( this.state.composing )\r\n return\r\n\r\n this.trigger(\"edit:keydown\", {event:e})\r\n\r\n switch( e.key ){\r\n case 'Esc' :\r\n case 'Escape' : {\r\n this.state.editing = false\r\n var hasValueToRevertTo = !!tagElm.__tagifyTagData.__originalData.value\r\n\r\n if( hasValueToRevertTo )\r\n // revert the tag to how it was before editing\r\n // replace current tag with original one (pre-edited one)\r\n tagElm.parentNode.replaceChild(tagElm.__tagifyTagData.__originalHTML, tagElm)\r\n else\r\n tagElm.remove()\r\n\r\n break\r\n }\r\n case 'Enter' :\r\n case 'Tab' : {\r\n e.preventDefault()\r\n\r\n var EDITED_TAG_BLUR_DELAY = 0;\r\n\r\n // a setTimeout is used so when editing (in \"select\" mode) while the dropdown is shown and a suggestion is highlighted\r\n // and ENTER key is pressed down - the `dropdown.hide` method won't be invoked immediately and unbind the dropdown's\r\n // KEYDOWN \"ENTER\" before it has time to call the handler and select the suggestion.\r\n setTimeout(() => e.target.blur(), EDITED_TAG_BLUR_DELAY)\r\n }\r\n }\r\n },\r\n\r\n onDoubleClickScope(e){\r\n var tagElm = e.target.closest('.' + this.settings.classNames.tag),\r\n tagData = getSetTagData(tagElm),\r\n _s = this.settings,\r\n isEditingTag,\r\n isReadyOnlyTag;\r\n\r\n if( !tagElm || tagData.editable === false ) return\r\n\r\n isEditingTag = tagElm.classList.contains(this.settings.classNames.tagEditing)\r\n isReadyOnlyTag = tagElm.hasAttribute('readonly')\r\n\r\n if( !_s.readonly && !isEditingTag && !isReadyOnlyTag && this.settings.editTags && _s.userInput )\r\n this.editTag(tagElm)\r\n\r\n this.toggleFocusClass(true)\r\n\r\n if( _s.mode != 'select' )\r\n this.trigger('dblclick', { tag:tagElm, index:this.getNodeIndex(tagElm), data:getSetTagData(tagElm) })\r\n },\r\n\r\n /**\r\n *\r\n * @param {Object} m an object representing the observed DOM changes\r\n */\r\n onInputDOMChange(m){\r\n // iterate all DOM mutation\r\n m.forEach(record => {\r\n // only the ADDED nodes\r\n record.addedNodes.forEach(addedNode => {\r\n // fix chrome's placing '

' everytime ENTER key is pressed, and replace with just `
' ){\r\n addedNode.replaceWith(document.createElement('br'))\r\n }\r\n\r\n // if the added element is a div containing a tag within it (chrome does this when pressing ENTER before a tag)\r\n else if( addedNode.nodeType == 1 && addedNode.querySelector(this.settings.classNames.tagSelector) ){\r\n let newlineText = document.createTextNode('')\r\n\r\n if( addedNode.childNodes[0].nodeType == 3 && addedNode.previousSibling.nodeName != 'BR' )\r\n newlineText = document.createTextNode('\\n')\r\n\r\n // unwrap the useless div\r\n // chrome adds a BR at the end which should be removed\r\n addedNode.replaceWith(...[newlineText, ...[...addedNode.childNodes].slice(0,-1)])\r\n placeCaretAfterNode(newlineText)\r\n }\r\n\r\n // if this is a tag\r\n else if( isNodeTag.call(this, addedNode) ){\r\n if( addedNode.previousSibling?.nodeType == 3 && !addedNode.previousSibling.textContent )\r\n addedNode.previousSibling.remove()\r\n\r\n // and it is the first node in a new line\r\n if( addedNode.previousSibling && addedNode.previousSibling.nodeName == 'BR' ){\r\n // allows placing the caret just before the tag, when the tag is the first node in that line\r\n addedNode.previousSibling.replaceWith('\\n' + ZERO_WIDTH_CHAR)\r\n\r\n let nextNode = addedNode.nextSibling, anythingAfterNode = '';\r\n\r\n while (nextNode) {\r\n anythingAfterNode += nextNode.textContent\r\n nextNode = nextNode.nextSibling;\r\n }\r\n\r\n // when hitting ENTER for new line just before an existing tag, but skip below logic when a tag has been addded\r\n anythingAfterNode.trim() && placeCaretAfterNode(addedNode.previousSibling)\r\n }\r\n\r\n // if previous sibling does not exists (meanning the addedNode is the first node in this.DOM.input)\r\n // or, if the previous sibling is also a tag, add a zero-space character before (to allow showing the caret in Chrome)\r\n else if( !addedNode.previousSibling || getSetTagData(addedNode.previousSibling) ){\r\n addedNode.before(ZERO_WIDTH_CHAR)\r\n }\r\n }\r\n })\r\n\r\n record.removedNodes.forEach(removedNode => {\r\n // when trying to delete a tag which is in a new line and there's nothing else there (caret is after the tag)\r\n if( removedNode && removedNode.nodeName == 'BR' && isNodeTag.call(this, lastInputChild)){\r\n this.removeTags(lastInputChild)\r\n this.fixFirefoxLastTagNoCaret()\r\n }\r\n })\r\n })\r\n\r\n // get the last child only after the above DOM modifications\r\n // check these scenarios:\r\n // 1. after a single line, press ENTER once - should add only 1 BR\r\n // 2. presss ENTER right before a tag\r\n // 3. press enter within a text node before a tag\r\n var lastInputChild = this.DOM.input.lastChild;\r\n\r\n if( lastInputChild && lastInputChild.nodeValue == '' )\r\n lastInputChild.remove()\r\n\r\n // make sure the last element is always a BR\r\n if( !lastInputChild || lastInputChild.nodeName != 'BR' ){\r\n this.DOM.input.appendChild(document.createElement('br'))\r\n }\r\n },\r\n }\r\n}\r\n\r\n","import { sameStr, removeCollectionProp, omit, isObject, parseHTML, removeTextChildNodes, escapeHTML, extend, concatWithoutDups, getUID, isNodeTag, injectAtCaret, placeCaretAfterNode, getSetTagData, fixCaretBetweenTags } from './parts/helpers'\r\nimport DEFAULTS from './parts/defaults'\r\nimport _dropdown, { initDropdown } from './parts/dropdown'\r\nimport { getPersistedData, setPersistedData, clearPersistedData } from './parts/persist'\r\nimport TEXTS from './parts/texts'\r\nimport templates from './parts/templates'\r\nimport EventDispatcher from './parts/EventDispatcher'\r\nimport events, { triggerChangeEvent } from './parts/events'\r\n\r\n/**\r\n * @constructor\r\n * @param {Object} input DOM element\r\n * @param {Object} settings settings object\r\n */\r\nfunction Tagify( input, settings ){\r\n if( !input ){\r\n console.warn('Tagify:', 'input element not found', input)\r\n // return an empty mock of all methods, so the code using tagify will not break\r\n // because it might be calling methods even though the input element does not exist\r\n const mockInstance = new Proxy(this, { get(){ return () => mockInstance } })\r\n return mockInstance\r\n }\r\n\r\n if( input.__tagify ){\r\n console.warn('Tagify: ', 'input element is already Tagified - Same instance is returned.', input)\r\n return input.__tagify\r\n }\r\n\r\n extend(this, EventDispatcher(this))\r\n this.isFirefox = (/firefox|fxios/i).test(navigator.userAgent) && !(/seamonkey/i).test(navigator.userAgent)\r\n this.isIE = window.document.documentMode; // https://developer.mozilla.org/en-US/docs/Web/API/Document/compatMode#Browser_compatibility\r\n\r\n settings = settings || {};\r\n this.getPersistedData = getPersistedData(settings.id)\r\n this.setPersistedData = setPersistedData(settings.id)\r\n this.clearPersistedData = clearPersistedData(settings.id)\r\n this.applySettings(input, settings)\r\n\r\n this.state = {\r\n inputText: '',\r\n editing : false,\r\n composing: false,\r\n actions : {}, // UI actions for state-locking\r\n mixMode : {},\r\n dropdown: {},\r\n flaggedTags: {} // in mix-mode, when a string is detetced as potential tag, and the user has chocen to close the suggestions dropdown, keep the record of the tasg here\r\n }\r\n\r\n this.value = [] // tags' data\r\n\r\n // events' callbacks references will be stores here, so events could be unbinded\r\n this.listeners = {}\r\n\r\n this.DOM = {} // Store all relevant DOM elements in an Object\r\n\r\n this.build(input)\r\n initDropdown.call(this)\r\n\r\n this.getCSSVars()\r\n this.loadOriginalValues()\r\n\r\n this.events.customBinding.call(this)\r\n this.events.binding.call(this)\r\n input.autofocus && this.DOM.input.focus()\r\n input.__tagify = this\r\n}\r\n\r\nTagify.prototype = {\r\n _dropdown,\r\n placeCaretAfterNode,\r\n getSetTagData,\r\n helpers: {sameStr, removeCollectionProp, omit, isObject, parseHTML, escapeHTML, extend, concatWithoutDups, getUID, isNodeTag},\r\n\r\n customEventsList : ['change', 'add', 'remove', 'invalid', 'input', 'paste', 'click', 'keydown', 'focus', 'blur', 'edit:input', 'edit:beforeUpdate', 'edit:updated', 'edit:start', 'edit:keydown', 'dropdown:show', 'dropdown:hide', 'dropdown:select', 'dropdown:updated', 'dropdown:noMatch', 'dropdown:scroll'],\r\n dataProps: ['__isValid', '__removed', '__originalData', '__originalHTML', '__tagId'], // internal-uasge props\r\n\r\n trim(text){\r\n return this.settings.trim && text && typeof text == \"string\" ? text.trim() : text\r\n },\r\n\r\n // expose this handy utility function\r\n parseHTML,\r\n\r\n templates,\r\n\r\n parseTemplate(template, data){\r\n template = this.settings.templates[template] || template;\r\n return parseHTML( template.apply(this, data) )\r\n },\r\n\r\n set whitelist( arr ){\r\n const isArray = arr && Array.isArray(arr)\r\n this.settings.whitelist = isArray ? arr : []\r\n this.setPersistedData(isArray ? arr : [], 'whitelist')\r\n },\r\n\r\n get whitelist(){\r\n return this.settings.whitelist\r\n },\r\n\r\n generateClassSelectors(classNames){\r\n for( let name in classNames ) {\r\n let currentName = name;\r\n Object.defineProperty(classNames, currentName + \"Selector\" , {\r\n get(){ return \".\" + this[currentName].split(\" \")[0] }\r\n })\r\n }\r\n },\r\n\r\n applySettings( input, settings ){\r\n DEFAULTS.templates = this.templates\r\n\r\n var mixModeDefaults = {\r\n dropdown: {\r\n position: \"text\"\r\n }\r\n }\r\n\r\n var mergedDefaults = extend({}, DEFAULTS, (settings.mode == 'mix' ? mixModeDefaults : {}));\r\n var _s = this.settings = extend({}, mergedDefaults, settings)\r\n\r\n _s.disabled = input.hasAttribute('disabled')\r\n _s.readonly = _s.readonly || input.hasAttribute('readonly')\r\n _s.placeholder = escapeHTML(input.getAttribute('placeholder') || _s.placeholder || \"\")\r\n _s.required = input.hasAttribute('required')\r\n\r\n this.generateClassSelectors(_s.classNames)\r\n\r\n if ( _s.dropdown.includeSelectedTags === undefined )\r\n _s.dropdown.includeSelectedTags = _s.duplicates;\r\n\r\n if( this.isIE )\r\n _s.autoComplete = false; // IE goes crazy if this isn't false\r\n\r\n [\"whitelist\", \"blacklist\"].forEach(name => {\r\n var attrVal = input.getAttribute('data-' + name)\r\n if( attrVal ){\r\n attrVal = attrVal.split(_s.delimiters)\r\n if( attrVal instanceof Array )\r\n _s[name] = attrVal\r\n }\r\n })\r\n\r\n // backward-compatibility for old version of \"autoComplete\" setting:\r\n if( \"autoComplete\" in settings && !isObject(settings.autoComplete) ){\r\n _s.autoComplete = DEFAULTS.autoComplete\r\n _s.autoComplete.enabled = settings.autoComplete\r\n }\r\n\r\n if( _s.mode == 'mix' ){\r\n _s.pattern = _s.pattern || /@/;\r\n _s.autoComplete.rightKey = true\r\n _s.delimiters = settings.delimiters || null // default dlimiters in mix-mode must be NULL\r\n\r\n // needed for \"filterListItems\". This assumes the user might have forgotten to manually\r\n // define the same term in \"dropdown.searchKeys\" as defined in \"tagTextProp\" setting, so\r\n // by automatically adding it, tagify is \"helping\" out, guessing the intesntions of the developer.\r\n if( _s.tagTextProp && !_s.dropdown.searchKeys.includes(_s.tagTextProp) )\r\n _s.dropdown.searchKeys.push(_s.tagTextProp)\r\n }\r\n\r\n if( input.pattern )\r\n try { _s.pattern = new RegExp(input.pattern) }\r\n catch(e){}\r\n\r\n // Convert the \"delimiters\" setting into a REGEX object\r\n if( _s.delimiters ){\r\n _s._delimiters = _s.delimiters;\r\n try { _s.delimiters = new RegExp(this.settings.delimiters, \"g\") }\r\n catch(e){}\r\n }\r\n\r\n if( _s.disabled )\r\n _s.userInput = false;\r\n\r\n this.TEXTS = {...TEXTS, ...(_s.texts || {})}\r\n\r\n // make sure the dropdown will be shown on \"focus\" and not only after typing something (in \"select\" mode)\r\n if( (_s.mode == 'select' && !settings.dropdown?.enabled) || !_s.userInput ){\r\n _s.dropdown.enabled = 0\r\n }\r\n\r\n _s.dropdown.appendTarget = settings.dropdown?.appendTarget || document.body;\r\n\r\n\r\n // get & merge persisted data with current data\r\n let persistedWhitelist = this.getPersistedData('whitelist');\r\n\r\n if( Array.isArray(persistedWhitelist))\r\n this.whitelist = Array.isArray(_s.whitelist)\r\n ? concatWithoutDups(_s.whitelist, persistedWhitelist)\r\n : persistedWhitelist;\r\n },\r\n\r\n /**\r\n * Returns a string of HTML element attributes\r\n * @param {Object} data [Tag data]\r\n */\r\n getAttributes( data ){\r\n var attrs = this.getCustomAttributes(data), s = '', k;\r\n\r\n for( k in attrs )\r\n s += \" \" + k + (data[k] !== undefined ? `=\"${attrs[k]}\"` : \"\");\r\n\r\n return s;\r\n },\r\n\r\n /**\r\n * Returns an object of attributes to be used for the templates\r\n */\r\n getCustomAttributes( data ){\r\n // only items which are objects have properties which can be used as attributes\r\n if( !isObject(data) )\r\n return '';\r\n\r\n var output = {}, propName;\r\n\r\n for( propName in data ){\r\n if( propName.slice(0,2) != '__' && propName != 'class' && data.hasOwnProperty(propName) && data[propName] !== undefined )\r\n output[propName] = escapeHTML(data[propName])\r\n }\r\n return output\r\n },\r\n\r\n setStateSelection(){\r\n var selection = window.getSelection()\r\n\r\n // save last selection place to be able to inject anything from outside to that specific place\r\n var sel = {\r\n anchorOffset: selection.anchorOffset,\r\n anchorNode : selection.anchorNode,\r\n range : selection.getRangeAt && selection.rangeCount && selection.getRangeAt(0)\r\n }\r\n\r\n this.state.selection = sel\r\n return sel\r\n },\r\n\r\n /**\r\n * Get specific CSS variables which are relevant to this script and parse them as needed.\r\n * The result is saved on the instance in \"this.CSSVars\"\r\n */\r\n getCSSVars(){\r\n var compStyle = getComputedStyle(this.DOM.scope, null)\r\n\r\n const getProp = name => compStyle.getPropertyValue('--'+name)\r\n\r\n function seprateUnitFromValue(a){\r\n if( !a ) return {}\r\n a = a.trim().split(' ')[0]\r\n var unit = a.split(/\\d+/g).filter(n=>n).pop().trim(),\r\n value = +a.split(unit).filter(n=>n)[0].trim()\r\n return {value, unit}\r\n }\r\n\r\n this.CSSVars = {\r\n tagHideTransition: (({value, unit}) => unit=='s' ? value * 1000 : value)(seprateUnitFromValue(getProp('tag-hide-transition')))\r\n }\r\n },\r\n\r\n /**\r\n * builds the HTML of this component\r\n * @param {Object} input [DOM element which would be \"transformed\" into \"Tags\"]\r\n */\r\n build( input ){\r\n var DOM = this.DOM,\r\n labelWrapper = input.closest('label');\r\n\r\n if( this.settings.mixMode.integrated ){\r\n DOM.originalInput = null;\r\n DOM.scope = input;\r\n DOM.input = input;\r\n }\r\n\r\n else {\r\n DOM.originalInput = input\r\n DOM.originalInput_tabIndex = input.tabIndex\r\n DOM.scope = this.parseTemplate('wrapper', [input, this.settings])\r\n DOM.input = DOM.scope.querySelector(this.settings.classNames.inputSelector)\r\n input.parentNode.insertBefore(DOM.scope, input)\r\n input.tabIndex = -1; // do not allow focus or typing directly, once tagified\r\n }\r\n\r\n // fixes tagify nested inside a