import { sameStr, removeCollectionProp, omit, isObject, parseHTML, removeTextChildNodes, escapeHTML, extend, concatWithoutDups, getUID, isNodeTag } from './parts/helpers'import DEFAULTS from './parts/defaults'import _dropdown, { initDropdown } from './parts/dropdown'import { getPersistedData, setPersistedData, clearPersistedData } from './parts/persist'import TEXTS from './parts/texts'import templates from './parts/templates'import EventDispatcher from './parts/EventDispatcher'import events, { triggerChangeEvent } from './parts/events'/** * @constructor * @param {Object} input    DOM element * @param {Object} settings settings object */function Tagify( input, settings ){    if( !input ){        console.warn('Tagify:', 'input element not found', input)        // return an empty mock of all methods, so the code using tagify will not break        // because it might be calling methods even though the input element does not exists        const mockInstance = new Proxy(this, { get(){ return () => mockInstance } })        return mockInstance    }    if( input.previousElementSibling && input.previousElementSibling.classList.contains('tagify') ){        console.warn('Tagify: ', 'input element is already Tagified', input)        return this    }    extend(this, EventDispatcher(this))    this.isFirefox = typeof InstallTrigger !== 'undefined'    this.isIE = window.document.documentMode; // https://developer.mozilla.org/en-US/docs/Web/API/Document/compatMode#Browser_compatibility    settings = settings || {};    this.getPersistedData = getPersistedData(settings.id)    this.setPersistedData = setPersistedData(settings.id)    this.clearPersistedData = clearPersistedData(settings.id)    this.applySettings(input, settings)    this.state = {        inputText: '',        editing : false,        actions : {},   // UI actions for state-locking        mixMode : {},        dropdown: {},        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    }    this.value = [] // tags' data    // events' callbacks references will be stores here, so events could be unbinded    this.listeners = {}    this.DOM = {} // Store all relevant DOM elements in an Object    this.build(input)    initDropdown.call(this)    this.getCSSVars()    this.loadOriginalValues()    this.events.customBinding.call(this);    this.events.binding.call(this)    input.autofocus && this.DOM.input.focus()}Tagify.prototype = {    _dropdown,    customEventsList : ['change', 'add', 'remove', 'invalid', 'input', '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'], // internal-uasge props    trim(text){        return this.settings.trim && text && typeof text == "string" ? text.trim() : text    },    // expose this handy utility function    parseHTML,    templates,    parseTemplate(template, data){        template = this.settings.templates[template] || template;        return this.parseHTML( template.apply(this, data) )    },    set whitelist( arr ){        const isArray = arr && Array.isArray(arr)        this.settings.whitelist = isArray ? arr : []        this.setPersistedData(isArray ? arr : [], 'whitelist')    },    get whitelist(){        return this.settings.whitelist    },    applySettings( input, settings ){        DEFAULTS.templates = this.templates        var _s = this.settings = extend({}, DEFAULTS, settings)        _s.disabled = input.hasAttribute('disabled')        _s.readonly = _s.readonly || input.hasAttribute('readonly')        // BUG: CWE-79 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')        // _s.placeholder = input.getAttribute('placeholder') || _s.placeholder || ""        // FIXED:        _s.placeholder = escapeHTML(input.getAttribute('placeholder') || _s.placeholder || "")        _s.required = input.hasAttribute('required')        for( let name in _s.classNames )            Object.defineProperty(_s.classNames, name + "Selector" , {                get(){ return "."+this[name].split(" ")[0] }            })        if( this.isIE )            _s.autoComplete = false; // IE goes crazy if this isn't false        ["whitelist", "blacklist"].forEach(name => {            var attrVal = input.getAttribute('data-' + name)            if( attrVal ){                attrVal = attrVal.split(_s.delimiters)                if( attrVal instanceof Array )                    _s[name] = attrVal            }        })        // backward-compatibility for old version of "autoComplete" setting:        if( "autoComplete" in settings && !isObject(settings.autoComplete) ){            _s.autoComplete = DEFAULTS.autoComplete            _s.autoComplete.enabled = settings.autoComplete        }        if( _s.mode == 'mix' ){            _s.autoComplete.rightKey = true            _s.delimiters = settings.delimiters || null // default dlimiters in mix-mode must be NULL            // needed for "filterListItems". This assumes the user might have forgotten to manually            // define the same term in "dropdown.searchKeys" as defined in "tagTextProp" setting, so            // by automatically adding it, tagify is "helping" out, guessing the intesntions of the developer.            if( _s.tagTextProp && !_s.dropdown.searchKeys.includes(_s.tagTextProp) )                _s.dropdown.searchKeys.push(_s.tagTextProp)        }        if( input.pattern )            try { _s.pattern = new RegExp(input.pattern)  }            catch(e){}        // Convert the "delimiters" setting into a REGEX object        if( this.settings.delimiters ){            try { _s.delimiters = new RegExp(this.settings.delimiters, "g") }            catch(e){}        }        if( _s.disabled )            _s.userInput = false;        this.TEXTS = {...TEXTS, ...(_s.texts || {})}        // make sure the dropdown will be shown on "focus" and not only after typing something (in "select" mode)        if( _s.mode == 'select' || !_s.userInput )            _s.dropdown.enabled = 0        _s.dropdown.appendTarget = settings.dropdown && settings.dropdown.appendTarget            ? settings.dropdown.appendTarget            : document.body        // get & merge persisted data with current data        let persistedWhitelist = this.getPersistedData('whitelist');        if( Array.isArray(persistedWhitelist))            this.whitelist = Array.isArray(_s.whitelist)                ? concatWithoutDups(_s.whitelist, persistedWhitelist)                : persistedWhitelist;    },    /**     * Returns a string of HTML element attributes     * @param {Object} data [Tag data]     */    getAttributes( data ){        var attrs = this.getCustomAttributes(data), s = '', k;        for( k in attrs )            s += " " + k + (data[k] !== undefined ? `="${attrs[k]}"` : "");        return s;    },    /**     * Returns an object of attributes to be used for the templates     */    getCustomAttributes( data ){        // only items which are objects have properties which can be used as attributes        if( !isObject(data) )            return '';        var output = {}, propName;        for( propName in data ){            if( propName.slice(0,2) != '__' && propName != 'class' && data.hasOwnProperty(propName) && data[propName] !== undefined )                output[propName] = escapeHTML(data[propName])        }        return output    },    setStateSelection(){        var selection = window.getSelection()        // save last selection place to be able to inject anything from outside to that specific place        var sel = {            anchorOffset: selection.anchorOffset,            anchorNode  : selection.anchorNode,            range       : selection.getRangeAt && selection.rangeCount && selection.getRangeAt(0)        }        this.state.selection = sel        return sel    },    /**     * Get the caret position relative to the viewport     * https://stackoverflow.com/q/58985076/104380     *     * @returns {object} left, top distance in pixels     */    getCaretGlobalPosition(){        const sel = document.getSelection()        if( sel.rangeCount ){            const r = sel.getRangeAt(0)            const node = r.startContainer            const offset = r.startOffset            let rect,  r2;            if (offset > 0) {                r2 = document.createRange()                r2.setStart(node, offset - 1)                r2.setEnd(node, offset)                rect = r2.getBoundingClientRect()                return {left:rect.right, top:rect.top, bottom:rect.bottom}            }            if( node.getBoundingClientRect )                return node.getBoundingClientRect()        }        return {left:-9999, top:-9999}    },    /**     * Get specific CSS variables which are relevant to this script and parse them as needed.     * The result is saved on the instance in "this.CSSVars"     */    getCSSVars(){        var compStyle = getComputedStyle(this.DOM.scope, null)        const getProp = name => compStyle.getPropertyValue('--'+name)        function seprateUnitFromValue(a){            if( !a ) return {}            a = a.trim().split(' ')[0]            var unit  = a.split(/\d+/g).filter(n=>n).pop().trim(),                value = +a.split(unit).filter(n=>n)[0].trim()            return {value, unit}        }        this.CSSVars = {            tagHideTransition: (({value, unit}) => unit=='s' ? value * 1000 : value)(seprateUnitFromValue(getProp('tag-hide-transition')))        }    },    /**     * builds the HTML of this component     * @param  {Object} input [DOM element which would be "transformed" into "Tags"]     */    build( input ){        var DOM  = this.DOM;        if( this.settings.mixMode.integrated ){            DOM.originalInput = null;            DOM.scope = input;            DOM.input = input;        }        else {            DOM.originalInput = input            DOM.scope = this.parseTemplate('wrapper', [input, this.settings])            DOM.input = DOM.scope.querySelector(this.settings.classNames.inputSelector)            input.parentNode.insertBefore(DOM.scope, input)        }    },    /**     * revert any changes made by this component     */    destroy(){        this.events.unbindGlobal.call(this)        this.DOM.scope.parentNode.removeChild(this.DOM.scope)        this.dropdown.hide(true)        clearTimeout(this.dropdownHide__bindEventsTimeout)    },    /**     * if the original input has any values, add them as tags     */    loadOriginalValues( value ){        var lastChild,            _s = this.settings        // temporarily block firign the "change" event on the original input unil        // this method finish removing current value and adding the new one        this.state.blockChangeEvent = true        if( value === undefined ){            const persistedOriginalValue = this.getPersistedData('value')            // if the field already has a field, trust its the desired            // one to be rendered and do not use the persisted one            if( persistedOriginalValue && !this.DOM.originalInput.value )                value = persistedOriginalValue            else                value = _s.mixMode.integrated ? this.DOM.input.textContent : this.DOM.originalInput.value        }        this.removeAllTags()        if( value ){            if( _s.mode == 'mix' ){                this.parseMixTags(this.trim(value))                lastChild = this.DOM.input.lastChild;                if( !lastChild || lastChild.tagName != 'BR' )                    this.DOM.input.insertAdjacentHTML('beforeend', '<br>')            }            else{                try{                    if( JSON.parse(value) instanceof Array )                        value = JSON.parse(value)                }                catch(err){}                this.addTags(value).forEach(tag => tag && tag.classList.add(_s.classNames.tagNoAnimation))            }        }        else            this.postUpdate()        this.state.lastOriginalValueReported = _s.mixMode.integrated ? '' : this.DOM.originalInput.value        this.state.blockChangeEvent = false    },    cloneEvent(e){        var clonedEvent = {}        for( var v in e )            clonedEvent[v] = e[v]        return clonedEvent    },    /**     * Toogle global loading state on/off     * Useful when fetching async whitelist while user is typing     * @param {Boolean} isLoading     */    loading( isLoading ){        this.state.isLoading = isLoading        // IE11 doesn't support toggle with second parameter        this.DOM.scope.classList[isLoading?"add":"remove"](this.settings.classNames.scopeLoading)        return this    },    /**     * Toogle specieif tag loading state on/off     * @param {Boolean} isLoading     */    tagLoading( tagElm, isLoading ){        if( tagElm )            // IE11 doesn't support toggle with second parameter            tagElm.classList[isLoading?"add":"remove"](this.settings.classNames.tagLoading)        return this    },    /**     * Toggles class on the main tagify container ("scope")     * @param {String} className     * @param {Boolean} force     */    toggleClass( className, force ){        if( typeof className == 'string' )            this.DOM.scope.classList.toggle(className, force)    },    toggleFocusClass( force ){        this.toggleClass(this.settings.classNames.focus, !!force)    },    triggerChangeEvent,    events,    fixFirefoxLastTagNoCaret(){        return // seems to be fixed in newer version of FF, so retiring below code (for now)        var inputElm = this.DOM.input        if( this.isFirefox && inputElm.childNodes.length && inputElm.lastChild.nodeType == 1 ){            inputElm.appendChild(document.createTextNode("\u200b"))            this.setRangeAtStartEnd(true)            return true        }    },    placeCaretAfterNode( node ){        if( !node || !node.parentNode ) return        var nextSibling = node.nextSibling,            sel = window.getSelection(),            range = sel.getRangeAt(0);        if (sel.rangeCount) {            range.setStartAfter(nextSibling || node);            range.collapse(true)            // range.setEndBefore(nextSibling || node);            sel.removeAllRanges();            sel.addRange(range);        }    },    insertAfterTag( tagElm, newNode ){        newNode = newNode || this.settings.mixMode.insertAfterTag;        if( !tagElm || !tagElm.parentNode || !newNode ) return        newNode = typeof newNode == 'string'            ? document.createTextNode(newNode)            : newNode        tagElm.parentNode.insertBefore(newNode, tagElm.nextSibling)        return newNode    },    /**     * Enters a tag into "edit" mode     * @param {Node} tagElm the tag element to edit. if nothing specified, use last last     */    editTag( tagElm, opts ){        tagElm = tagElm || this.getLastTag()        opts = opts || {}        this.dropdown.hide()        var _s = this.settings;        function getEditableElm(){            return tagElm.querySelector(_s.classNames.tagTextSelector)        }        var editableElm = getEditableElm(),            tagIdx = this.getNodeIndex(tagElm),            tagData = this.tagData(tagElm),            _CB = this.events.callbacks,            that = this,            isValid = true,            delayed_onEditTagBlur = function(){                setTimeout(() => _CB.onEditTagBlur.call(that, getEditableElm()))            }        if( !editableElm ){            console.warn('Cannot find element in Tag template: .', _s.classNames.tagTextSelector);            return;        }        if( tagData instanceof Object && "editable" in tagData && !tagData.editable )            return        editableElm.setAttribute('contenteditable', true)        tagElm.classList.add( _s.classNames.tagEditing )        // cache the original data, on the DOM node, before any modification ocurs, for possible revert        this.tagData(tagElm, {            __originalData: extend({}, tagData),            __originalHTML: tagElm.innerHTML        })        editableElm.addEventListener('focus', _CB.onEditTagFocus.bind(this, tagElm))        editableElm.addEventListener('blur', delayed_onEditTagBlur)        editableElm.addEventListener('input', _CB.onEditTagInput.bind(this, editableElm))        editableElm.addEventListener('keydown', e => _CB.onEditTagkeydown.call(this, e, tagElm))        editableElm.focus()        this.setRangeAtStartEnd(false, editableElm)        if( !opts.skipValidation )            isValid = this.editTagToggleValidity(tagElm)        editableElm.originalIsValid = isValid        this.trigger("edit:start", { tag:tagElm, index:tagIdx, data:tagData, isValid })        return this    },    /**     * If a tag is invalid, for any reason, set its class to as "not allowed" (see defaults file)     * @param {Node} tagElm required     * @param {Object} tagData optional     * @returns true if valid, a string (reason) if not     */    editTagToggleValidity( tagElm, tagData ){        var tagData = tagData || this.tagData(tagElm),            isValid;        if( !tagData ){            console.warn("tag has no data: ", tagElm, tagData)            return;        }        isValid = !("__isValid" in tagData) || tagData.__isValid === true        if( !isValid ){            this.removeTagsFromValue(tagElm)        }        this.update()        //this.validateTag(tagData);        tagElm.classList.toggle(this.settings.classNames.tagNotAllowed, !isValid)        return tagData.__isValid    },    onEditTagDone(tagElm, tagData){        tagElm = tagElm || this.state.editing.scope        tagData = tagData || {}        var eventData = {            tag         : tagElm,            index       : this.getNodeIndex(tagElm),            previousData: this.tagData(tagElm),            data        : tagData        }        this.trigger("edit:beforeUpdate", eventData, {cloneData:false})        this.state.editing = false;        delete tagData.__originalData        delete tagData.__originalHTML        if( tagElm && tagData[this.settings.tagTextProp] ){            tagElm = this.replaceTag(tagElm, tagData)            this.editTagToggleValidity(tagElm, tagData)            if( this.settings.a11y.focusableTags )                tagElm.focus()            else                // place caret after edited tag                this.placeCaretAfterNode(tagElm.previousSibling)        }        else if(tagElm)            this.removeTags(tagElm)        this.trigger("edit:updated", eventData)        this.dropdown.hide()        // check if any of the current tags which might have been marked as "duplicate" should be now un-marked        if( this.settings.keepInvalidTags )            this.reCheckInvalidTags()    },    /**     * Replaces an exisitng tag with a new one. Used for updating a tag's data     * @param {Object} tagElm  [DOM node to replace]     * @param {Object} tagData [data to create new tag from]     */    replaceTag(tagElm, tagData){        if( !tagData || !tagData.value )            tagData = tagElm.__tagifyTagData        // if tag is invalid, make the according changes in the newly created element        if( tagData.__isValid && tagData.__isValid != true )            extend( tagData, this.getInvalidTagAttrs(tagData, tagData.__isValid) )        var newTagElm = this.createTagElem(tagData)        // update DOM        tagElm.parentNode.replaceChild(newTagElm, tagElm)        this.updateValueByDOMTags()        return newTagElm    },    /**     * update "value" (Array of Objects) by traversing all valid tags     */    updateValueByDOMTags(){        this.value.length = 0;        [].forEach.call(this.getTagElms(), node => {            if( node.classList.contains(this.settings.classNames.tagNotAllowed.split(' ')[0]) ) return            this.value.push( this.tagData(node) )        })        this.update()    },    /** https://stackoverflow.com/a/59156872/104380     * @param {Boolean} start indicating where to place it (start or end of the node)     * @param {Object}  node  DOM node to place the caret at     */    setRangeAtStartEnd( start, node ){        start = typeof start == 'number' ? start : !!start        node = node || this.DOM.input;        node = node.lastChild || node;        var sel = document.getSelection()        try{            if( sel.rangeCount >= 1 ){                ['Start', 'End'].forEach(pos =>                    sel.getRangeAt(0)["set" + pos](node, start ? start : node.length)                )            }        } catch(err){            // console.warn("Tagify: ", err)        }    },    /**     * injects nodes/text at caret position, which is saved on the "state" when "blur" event gets triggered     * @param {Node} injectedNode [the node to inject at the caret position]     * @param {Object} selection [optional range Object. must have "anchorNode" & "anchorOffset"]     */    injectAtCaret( injectedNode, range ){        range = range || this.state.selection.range        if( !range ) return;        if( typeof injectedNode == 'string' )            injectedNode = document.createTextNode(injectedNode);        range.deleteContents()        range.insertNode(injectedNode)        this.setRangeAtStartEnd(false, injectedNode)        this.updateValueByDOMTags() // updates internal "this.value"        this.update() // updates original input/textarea        return this    },    /**     * input bridge for accessing & setting     * @type {Object}     */    input : {        set( s = '', updateDOM = true ){            var hideDropdown = this.settings.dropdown.closeOnSelect            this.state.inputText = s            if( updateDOM )                this.DOM.input.innerHTML = escapeHTML(""+s);            if( !s && hideDropdown )                this.dropdown.hide.bind(this)            this.input.autocomplete.suggest.call(this);            this.input.validate.call(this);        },        raw(){            return this.DOM.input.textContent        },        /**         * Marks the tagify's input as "invalid" if the value did not pass "validateTag()"         */        validate(){            var isValid = !this.state.inputText || this.validateTag({value:this.state.inputText}) === true;            this.DOM.input.classList.toggle(this.settings.classNames.inputInvalid, !isValid)            return isValid        },        // remove any child DOM elements that aren't of type TEXT (like <br>)        normalize( node ){            var clone = node || this.DOM.input, //.cloneNode(true),                v = [];            // when a text was pasted in FF, the "this.DOM.input" element will have <br> but no newline symbols (\n), and this will            // result in tags not being properly created if one wishes to create a separate tag per newline.            clone.childNodes.forEach(n => n.nodeType==3 && v.push(n.nodeValue))            v = v.join("\n")            try{                // "delimiters" might be of a non-regex value, where this will fail ("Tags With Properties" example in demo page):                v = v.replace(/(?:\r\n|\r|\n)/g, this.settings.delimiters.source.charAt(0))            }            catch(err){}            v = v.replace(/\s/g, ' ')  // replace NBSPs with spaces characters            if( this.settings.trim )                v = v.replace(/^\s+/, '') // trimLeft            return v        },        /**         * suggest the rest of the input's value (via CSS "::after" using "content:attr(...)")         * @param  {String} s [description]         */        autocomplete : {            suggest( data ){                if( !this.settings.autoComplete.enabled ) return;                data = data || {}                if( typeof data == 'string' )                    data = {value:data}                var suggestedText = data.value ? ''+data.value : '',                    suggestionStart = suggestedText.substr(0, this.state.inputText.length).toLowerCase(),                    suggestionTrimmed = suggestedText.substring(this.state.inputText.length);                if( !suggestedText || !this.state.inputText || suggestionStart != this.state.inputText.toLowerCase() ){                    this.DOM.input.removeAttribute("data-suggest");                    delete this.state.inputSuggestion                }                else{                    this.DOM.input.setAttribute("data-suggest", suggestionTrimmed);                    this.state.inputSuggestion = data                }            },            /**             * sets the suggested text as the input's value & cleanup the suggestion autocomplete.             * @param {String} s [text]             */            set( s ){                var dataSuggest = this.DOM.input.getAttribute('data-suggest'),                    suggestion = s || (dataSuggest ? this.state.inputText + dataSuggest : null);                if( suggestion ){                    if( this.settings.mode == 'mix' ){                        this.replaceTextWithNode( document.createTextNode(this.state.tag.prefix + suggestion) )                    }                    else{                        this.input.set.call(this, suggestion);                        this.setRangeAtStartEnd()                    }                    this.input.autocomplete.suggest.call(this);                    this.dropdown.hide();                    return true;                }                return false;            }        }    },    /**     * returns the index of the the tagData within the "this.value" array collection.     * since values should be unique, it is suffice to only search by "value" property     * @param {Object} tagData     */    getTagIdx( tagData ){        return this.value.findIndex(item => item.__tagId == (tagData||{}).__tagId )    },    getNodeIndex( node ){        var index = 0;        if( node )            while( (node = node.previousElementSibling) )                index++;        return index;    },    getTagElms( ...classess ){        var classname = '.' + [...this.settings.classNames.tag.split(' '), ...classess].join('.')        return [].slice.call(this.DOM.scope.querySelectorAll(classname)) // convert nodeList to Array - https://stackoverflow.com/a/3199627/104380    },    /**     * gets the last non-readonly, not-in-the-proccess-of-removal tag     */    getLastTag(){        var lastTag = this.DOM.scope.querySelectorAll(`${this.settings.classNames.tagSelector}:not(.${this.settings.classNames.tagHide}):not([readonly])`);        return lastTag[lastTag.length - 1];    },    /** Setter/Getter     * Each tag DOM node contains a custom property called "__tagifyTagData" which hosts its data     * @param {Node}   tagElm     * @param {Object} data     */    tagData(tagElm, data, override){        if( !tagElm ){            console.warn("tag element doesn't exist",tagElm, data)            return data        }        if( data )            tagElm.__tagifyTagData = override                ? data                : extend({}, tagElm.__tagifyTagData || {}, data)        return tagElm.__tagifyTagData    },    /**     * Searches if any tag with a certain value already exis     * @param  {String/Object} value [text value / tag data object]     * @param  {Boolean} caseSensitive     * @return {Number}     */    isTagDuplicate( value, caseSensitive ){        var dupsCount,            _s = this.settings;        // duplications are irrelevant for this scenario        if( _s.mode == 'select' )            return false        dupsCount = this.value.reduce((acc, item) => (            sameStr( this.trim(""+value), item.value, caseSensitive || _s.dropdown.caseSensitive )                ? acc+1                : acc        ), 0)        return dupsCount    },    getTagIndexByValue( value ){        var indices = [];        this.getTagElms().forEach((tagElm, i) => {            if(  sameStr( this.trim(tagElm.textContent), value, this.settings.dropdown.caseSensitive )  )                indices.push(i)        })        return indices;    },    getTagElmByValue( value ){        var tagIdx = this.getTagIndexByValue(value)[0]        return this.getTagElms()[tagIdx]    },    /**     * Temporarily marks a tag element (by value or Node argument)     * @param  {Object} tagElm [a specific "tag" element to compare to the other tag elements siblings]     */    flashTag( tagElm ){        if( tagElm ){            tagElm.classList.add(this.settings.classNames.tagFlash)            setTimeout(() => { tagElm.classList.remove(this.settings.classNames.tagFlash) }, 100)        }    },    /**     * checks if text is in the blacklist     */    isTagBlacklisted( v ){        v = this.trim(v.toLowerCase());        return this.settings.blacklist.filter(x => (""+x).toLowerCase() == v).length;    },    /**     * checks if text is in the whitelist     */    isTagWhitelisted( v ){        return !!this.getWhitelistItem(v)        /*        return this.settings.whitelist.some(item =>            typeof v == 'string'                ? sameStr(this.trim(v), (item.value || item))                : sameStr(JSON.stringify(item), JSON.stringify(v))        )        */    },    /**     * Returns the first whitelist item matched, by value (if match found)     * @param {String} value [text to match by]     */    getWhitelistItem( value, prop, whitelist ){        var result,            prop = prop || 'value',            _s = this.settings,            whitelist = whitelist || _s.whitelist;        whitelist.some(_wi => {            var _wiv = typeof _wi == 'string' ? _wi : (_wi[prop] || _wi.value),                isSameStr = sameStr(_wiv, value, _s.dropdown.caseSensitive, _s.trim)            if( isSameStr ){                result = typeof _wi == 'string' ? {value:_wi} : _wi                return true            }        })        // first iterate the whitelist, try find maches by "value" and if that fails        // and a "tagTextProp" is set to be other than "value", try that also        if( !result && prop == 'value' && _s.tagTextProp != 'value' ){            // if found, adds the first which matches            result = this.getWhitelistItem(value, _s.tagTextProp, whitelist)        }        return result    },    /**     * validate a tag object BEFORE the actual tag will be created & appeneded     * @param  {String} s     * @param  {String} uid      [unique ID, to not inclue own tag when cheking for duplicates]     * @return {Boolean/String}  ["true" if validation has passed, String for a fail]     */    validateTag( tagData ){        var _s = this.settings,            // when validating a tag in edit-mode, need to take "tagTextProp" into consideration            prop = "value" in tagData ? "value" : _s.tagTextProp,            v = this.trim(tagData[prop] + "");        // check for definitive empty value        if( !(tagData[prop]+"").trim() )            return this.TEXTS.empty;        // check if pattern should be used and if so, use it to test the value        if( _s.pattern && _s.pattern instanceof RegExp && !(_s.pattern.test(v)) )            return this.TEXTS.pattern;        // check for duplicates        if( !_s.duplicates && this.isTagDuplicate(v, this.state.editing) )            return this.TEXTS.duplicate;        if( this.isTagBlacklisted(v) || (_s.enforceWhitelist && !this.isTagWhitelisted(v)) )            return this.TEXTS.notAllowed;        if( _s.validate )            return _s.validate(tagData)        return true    },    getInvalidTagAttrs(tagData, validation){        return {            "aria-invalid" : true,            "class": `${tagData.class || ''} ${this.settings.classNames.tagNotAllowed}`.trim(),            "title": validation        }    },    hasMaxTags(){        return this.value.length >= this.settings.maxTags            ? this.TEXTS.exceed            : false    },    setReadonly( toggle, attrribute ){        var _s = this.settings        document.activeElement.blur() // exit possible edit-mode        _s[attrribute || 'readonly'] = toggle        this.DOM.scope[(toggle ? 'set' : 'remove') + 'Attribute'](attrribute || 'readonly', true)        if( _s.mode == 'mix' ){            this.setContentEditable(!toggle)        }    },    setContentEditable(state){        if( !this.settings.readonly && this.settings.userInput )            this.DOM.input.contentEditable = state    },    setDisabled( isDisabled ){        this.setReadonly(isDisabled, 'disabled')    },    /**     * pre-proccess the tagsItems, which can be a complex tagsItems like an Array of Objects or a string comprised of multiple words     * so each item should be iterated on and a tag created for.     * @return {Array} [Array of Objects]     */    normalizeTags( tagsItems ){        var {whitelist, delimiters, mode, tagTextProp, enforceWhitelist} = this.settings,            whitelistMatches = [],            whitelistWithProps = whitelist ? whitelist[0] instanceof Object : false,            // checks if this is a "collection", meanning an Array of Objects            isArray = tagsItems instanceof Array,            mapStringToCollection = s => (s+"").split(delimiters).filter(n => n).map(v => ({ [tagTextProp]:this.trim(v), value:this.trim(v) }))        if( typeof tagsItems == 'number' )            tagsItems = tagsItems.toString()        // if the argument is a "simple" String, ex: "aaa, bbb, ccc"        if( typeof tagsItems == 'string' ){            if( !tagsItems.trim() ) return [];            // go over each tag and add it (if there were multiple ones)            tagsItems = mapStringToCollection(tagsItems)        }        // is is an Array of Strings, convert to an Array of Objects        else if( isArray ){            // flatten the 2D array            tagsItems = [].concat(...tagsItems.map(item => item.value                ? item // mapStringToCollection(item.value).map(newItem => ({...item,...newItem}))                : mapStringToCollection(item)            ))        }        // search if the tag exists in the whitelist as an Object (has props),        // to be able to use its properties        if( whitelistWithProps ){            tagsItems.forEach(item => {                var whitelistMatchesValues = whitelistMatches.map(a=>a.value)                // if suggestions are shown, they are already filtered, so it's easier to use them,                // because the whitelist might also include items which have already been added                var filteredList = this.dropdown.filterListItems.call(this, item[tagTextProp], { exact:true })                if( !this.settings.duplicates )                    // also filter out items which have already been matched in previous iterations                    filteredList = filteredList.filter(filteredItem => !whitelistMatchesValues.includes(filteredItem.value))                // get the best match out of list of possible matches.                // if there was a single item in the filtered list, use that one                var matchObj = filteredList.length > 1                    ? this.getWhitelistItem(item[tagTextProp], tagTextProp, filteredList)                    : filteredList[0]                if( matchObj && matchObj instanceof Object ){                    whitelistMatches.push( matchObj ) // set the Array (with the found Object) as the new value                }                else if( mode != 'mix' ){                    if( item.value == undefined )                        item.value = item[tagTextProp]                    whitelistMatches.push(item)                }            })            if( whitelistMatches.length )                tagsItems = whitelistMatches        }        return tagsItems;    },    /**     * Parse the initial value of a textarea (or input) element and generate mixed text w/ tags     * https://stackoverflow.com/a/57598892/104380     * @param {String} s     */    parseMixTags( s ){        var {mixTagsInterpolator, duplicates, transformTag, enforceWhitelist, maxTags, tagTextProp} = this.settings,            tagsDataSet = [];        s = s.split(mixTagsInterpolator[0]).map((s1, i) => {            var s2 = s1.split(mixTagsInterpolator[1]),                preInterpolated = s2[0],                maxTagsReached = tagsDataSet.length == maxTags,                textProp,                tagData,                tagElm;            try{                // skip numbers and go straight to the "catch" statement                if( preInterpolated == +preInterpolated )                    throw Error                tagData = JSON.parse(preInterpolated)            } catch(err){                tagData = this.normalizeTags(preInterpolated)[0] || {value:preInterpolated}            }            transformTag.call(this, tagData)            if( !maxTagsReached   &&                s2.length > 1   &&                (!enforceWhitelist || this.isTagWhitelisted(tagData.value))   &&                !(!duplicates && this.isTagDuplicate(tagData.value)) ){                // in case "tagTextProp" setting is set to other than "value" and this tag does not have this prop                textProp = tagData[tagTextProp] ? tagTextProp : 'value'                tagData[textProp] = this.trim(tagData[textProp])                tagElm = this.createTagElem(tagData)                tagsDataSet.push( tagData )                tagElm.classList.add(this.settings.classNames.tagNoAnimation)                s2[0] = tagElm.outerHTML //+ "&#8288;"  // put a zero-space at the end so the caret won't jump back to the start (when the last input's child element is a tag)                this.value.push(tagData)            }            else if(s1)                return i ? mixTagsInterpolator[0] + s1 : s1            return s2.join('')        }).join('')        this.DOM.input.innerHTML = s        this.DOM.input.appendChild(document.createTextNode(''))        this.DOM.input.normalize()        this.getTagElms().forEach((elm, idx) => this.tagData(elm,  tagsDataSet[idx]))        this.update({withoutChangeEvent:true})        return s    },    /**     * For mixed-mode: replaces a text starting with a prefix with a wrapper element (tag or something)     * First there *has* to be a "this.state.tag" which is a string that was just typed and is staring with a prefix     */    replaceTextWithNode( newWrapperNode, strToReplace ){        if( !this.state.tag && !strToReplace ) return;        strToReplace = strToReplace || this.state.tag.prefix + this.state.tag.value;        var idx, nodeToReplace,            selection = window.getSelection(),            nodeAtCaret = selection.anchorNode,            firstSplitOffset = this.state.tag.delimiters ? this.state.tag.delimiters.length : 0;        // STEP 1: ex. replace #ba with the tag "bart" where "|" is where the caret is:        // CURRENT STATE: "foo #ba #ba| #ba"        // split the text node at the index of the caret        nodeAtCaret.splitText(selection.anchorOffset - firstSplitOffset)        // node 0: "foo #ba #ba|"        // node 1: " #ba"        // get index of LAST occurence of "#ba"        idx = nodeAtCaret.nodeValue.lastIndexOf(strToReplace)        if( idx == -1 ) return true;        nodeToReplace = nodeAtCaret.splitText(idx)        // node 0: "foo #ba "        // node 1: "#ba"    <- nodeToReplace        newWrapperNode && nodeAtCaret.parentNode.replaceChild(newWrapperNode, nodeToReplace)        // must NOT normalize contenteditable or it will cause unwanted issues:        // https://monosnap.com/file/ZDVmRvq5upYkidiFedvrwzSswegWk7        // nodeAtCaret.parentNode.normalize()        return true;    },    /**     * For selecting a single option (not used for multiple tags, but for "mode:select" only)     * @param {Object} tagElm   Tag DOM node     * @param {Object} tagData  Tag data     */    selectTag( tagElm, tagData ){        var _s = this.settings        if( _s.enforceWhitelist && !this.isTagWhitelisted(tagData.value) )            return        this.input.set.call(this, tagData[_s.tagTextProp] || tagData.value, true)        // place the caret at the end of the input, only if a dropdown option was selected (and not by manually typing another value and clicking "TAB")        if( this.state.actions.selectOption )            setTimeout(this.setRangeAtStartEnd.bind(this))        var lastTagElm = this.getLastTag()        if( lastTagElm )            this.replaceTag(lastTagElm, tagData)        else            this.appendTag(tagElm)        if( _s.enforceWhitelist )            this.setContentEditable(false);        this.value[0] = tagData        this.update()        this.trigger('add', { tag:tagElm, data:tagData })        return [tagElm]    },    /**     * add an empty "tag" element in an editable state     */    addEmptyTag( initialData ){        var tagData = extend({ value:"" }, initialData || {}),            tagElm = this.createTagElem(tagData)        this.tagData(tagElm, tagData)        // add the tag to the component's DOM        this.appendTag(tagElm)        this.editTag(tagElm, {skipValidation:true})    },    /**     * add a "tag" element to the "tags" component     * @param {String/Array} tagsItems   [A string (single or multiple values with a delimiter), or an Array of Objects or just Array of Strings]     * @param {Boolean}      clearInput  [flag if the input's value should be cleared after adding tags]     * @param {Boolean}      skipInvalid [do not add, mark & remove invalid tags]     * @return {Array} Array of DOM elements (tags)     */    addTags( tagsItems, clearInput, skipInvalid ){        var tagElems = [],            _s = this.settings,            frag = document.createDocumentFragment()        skipInvalid = skipInvalid || _s.skipInvalid;        if( !tagsItems || tagsItems.length == 0 ){            if( _s.mode == 'select' )                this.removeAllTags()            return tagElems        }        // converts Array/String/Object to an Array of Objects        tagsItems = this.normalizeTags(tagsItems)        if( _s.mode == 'mix' ){            return this.addMixTags(tagsItems)        }        if( _s.mode == 'select' )            clearInput = false        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( '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)            tagElems.push(tagElm)            // mode-select overrides            if( _s.mode == 'select' ){                return this.selectTag(tagElm, tagData)            }            // add the tag to the component's DOM            // this.appendTag(tagElm)            frag.appendChild(tagElm)            if( tagData.__isValid && tagData.__isValid === true ){                // update state                this.value.push(tagData)                this.trigger('add', {tag:tagElm, index:this.value.length - 1, data:tagData})            }            else{                this.trigger("invalid", {data:tagData, index:this.value.length, tag:tagElm, message:tagData.__isValid})                if( !_s.keepInvalidTags )                    // remove invalid tags (if "keepInvalidTags" is set to "false")                    setTimeout(() => this.removeTags(tagElm, true), 1000)            }            this.dropdown.position() // reposition the dropdown because the just-added tag might cause a new-line        })        this.appendTag(frag)        this.update()        if( tagsItems.length && clearInput ){            this.input.set.call(this)        }        this.dropdown.refilter()        return tagElems    },    /**     * Adds a mix-content tag     * @param {String/Array} tagData    A string (single or multiple values with a delimiter), or an Array of Objects or just Array of Strings     */    addMixTags( tagsData ){        tagsData = this.normalizeTags(tagsData);        if( tagsData[0].prefix || this.state.tag ){            return this.prefixedTextToTag(tagsData[0])        }        if( typeof tagsData == 'string' )            tagsData = [{ value:tagsData }]        var selection = !!this.state.selection, // must be cast, not to use the reference which is changing            frag = document.createDocumentFragment()        tagsData.forEach(tagData => {            var tagElm = this.createTagElem(tagData)            frag.appendChild(tagElm)            this.insertAfterTag(tagElm)        })        // if "selection" exists, assumes intention of inecting the new tag at the last        // saved location of the caret inside "this.DOM.input"        if( selection ){            this.injectAtCaret(frag)        }        // else, create a range and inject the new tag as the last child of "this.DOM.input"        else{            this.DOM.input.focus()            selection = this.setStateSelection()            selection.range.setStart(this.DOM.input, selection.range.endOffset)            selection.range.setEnd(this.DOM.input, selection.range.endOffset)            this.DOM.input.appendChild(frag)            this.updateValueByDOMTags() // updates internal "this.value"            this.update() // updates original input/textarea        }        return frag    },    /**     * 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]     */    prefixedTextToTag( tagItem ){        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)        // 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        if( !this.replaceTextWithNode(tagElm) ){            this.DOM.input.appendChild(tagElm)        }        setTimeout(()=> tagElm.classList.add(this.settings.classNames.tagNoAnimation), 300)        this.value.push(tagItem)        this.update()        if( !createdFromDelimiters ) {            var elm = this.insertAfterTag(tagElm) || tagElm;            this.placeCaretAfterNode(elm)        }        this.state.tag = null        this.trigger('add', extend({}, {tag:tagElm}, {data:tagItem}))        return tagElm    },    /**     * appened (validated) tag to the component's DOM scope     */    appendTag(tagElm){        var DOM = this.DOM,            insertBeforeNode = DOM.scope.lastElementChild;        if( insertBeforeNode === DOM.input )            DOM.scope.insertBefore(tagElm, insertBeforeNode)        else            DOM.scope.appendChild(tagElm)    },    /**     * creates a DOM tag element and injects it into the component (this.DOM.scope)     * @param  {Object}  tagData [text value & properties for the created tag]     * @param  {Object}  extraData [properties which are for the HTML template only]     * @return {Object} [DOM element]     */    createTagElem( tagData, extraData ){        tagData.__tagId = getUID()        var tagElm,            templateData = extend({}, tagData, { value:escapeHTML(tagData.value+""), ...extraData });        // if( this.settings.readonly )        //     tagData.readonly = true        tagElm = this.parseTemplate('tag', [templateData])        // crucial for proper caret placement when deleting content. if textNodes are allowed as children of        // a tag element, a browser bug casues the caret to misplaced inside the tag element (especcially affects "readonly" tags)        removeTextChildNodes(tagElm)        // while( tagElm.lastChild.nodeType == 3 )        //     tagElm.lastChild.parentNode.removeChild(tagElm.lastChild)        this.tagData(tagElm, tagData)        return tagElm    },    /**     * re-check all invalid tags.     * called after a tag was edited or removed     */    reCheckInvalidTags(){        var _s = this.settings        this.getTagElms(_s.classNames.tagNotAllowed).forEach((tagElm, i) => {            var tagData = this.tagData(tagElm),                hasMaxTags = this.hasMaxTags(),                tagValidation = this.validateTag(tagData);            // if the tag has become valid            if( tagValidation === true && !hasMaxTags ){                tagData = tagData.__preInvalidData                    ? tagData.__preInvalidData                    : { value:tagData.value }                return this.replaceTag(tagElm, tagData)            }            // if the tag is still invaild, set its title as such (reson of invalid might have changed)            tagElm.title = hasMaxTags || tagValidation        })    },    /**     * Removes a tag     * @param  {Array|Node|String}  tagElms         [DOM element(s) or a String value. if undefined or null, remove last added tag]     * @param  {Boolean}            silent          [A flag, which when turned on, does not remove any value and does not update the original input value but simply removes the tag from tagify]     * @param  {Number}             tranDuration    [Transition duration in MS]     * TODO: Allow multiple tags to be removed at-once     */    removeTags( tagElms, silent, tranDuration ){        var tagsToRemove;        tagElms = tagElms && tagElms instanceof HTMLElement            ? [tagElms]            : tagElms instanceof Array                ? tagElms                : tagElms                    ? [tagElms]                    : [this.getLastTag()]        // normalize tagElms array values:        // 1. removing invalid items        // 2, if an item is String try to get the matching Tag HTML node        // 3. get the tag data        // 4. return a collection of Objects        tagsToRemove = tagElms.reduce((elms, tagElm) => {            if( tagElm && typeof tagElm == 'string')                tagElm = this.getTagElmByValue(tagElm)            var tagData = this.tagData(tagElm);            if( tagElm && tagData && !tagData.readonly ) // make sure it's a tag and not some other node                // because the DOM node might be removed by async animation, the state will be updated while                // the node might still be in the DOM, so the "update" method should know which nodes to ignore                elms.push({                    node: tagElm,                    idx: this.getTagIdx(tagData), // this.getNodeIndex(tagElm); // this.getTagIndexByValue(tagElm.textContent)                    data: this.tagData(tagElm, {'__removed':true})                })            return elms        }, [])        tranDuration = typeof tranDuration == "number" ? tranDuration : this.CSSVars.tagHideTransition        if( this.settings.mode == 'select' ){            tranDuration = 0;            this.input.set.call(this)        }        // if only a single tag is to be removed        if( tagsToRemove.length == 1 ){            if( tagsToRemove[0].node.classList.contains(this.settings.classNames.tagNotAllowed) )                silent = true        }        if( !tagsToRemove.length )            return;        return this.settings.hooks.beforeRemoveTag(tagsToRemove, {tagify:this})            .then(() => {                function removeNode( tag ){                    if( !tag.node.parentNode ) return                    tag.node.parentNode.removeChild(tag.node)                    if( !silent ){                        // this.removeValueById(tagData.__uid)                        this.trigger('remove', { tag:tag.node, index:tag.idx, data:tag.data })                        this.dropdown.refilter()                        this.dropdown.position()                        this.DOM.input.normalize() // best-practice when in mix-mode (safe to do always anyways)                        // check if any of the current tags which might have been marked as "duplicate" should be un-marked                        if( this.settings.keepInvalidTags )                            this.reCheckInvalidTags()                    }                    else if( this.settings.keepInvalidTags )                        this.trigger('remove', { tag:tag.node, index:tag.idx })                }                function animation( tag ){                    tag.node.style.width = parseFloat(window.getComputedStyle(tag.node).width) + 'px'                    document.body.clientTop // force repaint for the width to take affect before the "hide" class below                    tag.node.classList.add(this.settings.classNames.tagHide)                    // manual timeout (hack, since transitionend cannot be used because of hover)                    setTimeout(removeNode.bind(this), tranDuration, tag)                }                if( tranDuration && tranDuration > 10 && tagsToRemove.length == 1 )                    animation.call(this, tagsToRemove[0])                else                    tagsToRemove.forEach(removeNode.bind(this))                // update state regardless of animation                if( !silent ){                    this.removeTagsFromValue(tagsToRemove.map(tag => tag.node))                    this.update() // update the original input with the current value                    if( this.settings.mode == 'select' )                        this.setContentEditable(true);                }            })            .catch(reason => {})    },    removeTagsFromDOM(){        [].slice.call(this.getTagElms()).forEach(elm => elm.parentNode.removeChild(elm))    },    /**     * @param {Array/Node} tags to be removed from the this.value array     */    removeTagsFromValue( tags ){        tags = Array.isArray(tags) ? tags : [tags];        tags.forEach(tag => {            var tagData = this.tagData(tag),                tagIdx = this.getTagIdx(tagData)            //  delete tagData.__removed            if( tagIdx > -1 )                this.value.splice(tagIdx, 1)        })    },    removeAllTags( opts ){        opts = opts || {}        this.value = []        if( this.settings.mode == 'mix' )            this.DOM.input.innerHTML = ''        else            this.removeTagsFromDOM()        this.dropdown.position()        if( this.settings.mode == 'select' ){            this.input.set.call(this)            this.setContentEditable(true)        }        // technically for now only "withoutChangeEvent" exists in the opts.        // if more properties will be added later, only pass what's needed to "update"        this.update(opts)    },    postUpdate(){        var classNames = this.settings.classNames,            hasValue = this.settings.mode == 'mix'                ? this.settings.mixMode.integrated                    ? this.DOM.input.textContent                    : this.DOM.originalInput.value.trim()                : this.value.length + this.input.raw.call(this).length;        this.toggleClass(classNames.hasMaxTags, this.value.length >= this.settings.maxTags)        this.toggleClass(classNames.hasNoTags, !this.value.length)        this.toggleClass(classNames.empty, !hasValue)    },    setOriginalInputValue( v ){        var inputElm = this.DOM.originalInput;        if( !this.settings.mixMode.integrated ){            inputElm.value = v            inputElm.tagifyValue = inputElm.value // must set to "inputElm.value" and not again to "inputValue" because for some reason the browser changes the string afterwards a bit.            this.setPersistedData(v, 'value')        }    },    /**     * update the origianl (hidden) input field's value     * see - https://stackoverflow.com/q/50957841/104380     */    update( args ){        var inputValue = this.getInputValue();        this.setOriginalInputValue(inputValue)        this.postUpdate()        if( !(args||{}).withoutChangeEvent && !this.state.blockChangeEvent )            this.triggerChangeEvent()    },    getInputValue(){        var value = this.getCleanValue();        return this.settings.mode == 'mix'            ? this.getMixedTagsAsString(value)            : value.length                ? this.settings.originalInputValueFormat                    ? this.settings.originalInputValueFormat(value)                    : JSON.stringify(value)                : ""    },    /**     * removes properties from `this.value` which are only used internally     */    getCleanValue(v){        return removeCollectionProp(v || this.value, this.dataProps);    },    getMixedTagsAsString(){        var result = "",            that = this,            _interpolator = this.settings.mixTagsInterpolator;        function iterateChildren(rootNode){            rootNode.childNodes.forEach((node) => {                if( node.nodeType == 1 ){                    const tagData = that.tagData(node);                    if( node.tagName == 'BR'  ){                        result += "\r\n";                    }                    if( node.tagName == 'DIV' || node.tagName == 'P' ){                        result += "\r\n";                        //  if( !node.children.length && node.textContent )                        //  result += node.textContent;                        iterateChildren(node)                    }                    else if( isNodeTag.call(that, node) && tagData ){                        if( tagData.__removed )                            return;                        else                            result += _interpolator[0] + JSON.stringify( omit(tagData, that.dataProps) ) + _interpolator[1]                    }                }                else                    result += node.textContent;            })        }        iterateChildren(this.DOM.input)        return result;    }}// legacy support for changed methods namesTagify.prototype.removeTag = Tagify.prototype.removeTagsexport default Tagify