Vue高亮输入 (Vue Highlightable Input)使用,node-interval-tree区间树,可编辑div光标前移解决方案

Posted fqh123

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue高亮输入 (Vue Highlightable Input)使用,node-interval-tree区间树,可编辑div光标前移解决方案相关的知识,希望对你有一定的参考价值。

安装:

npm install vue-highlightable-input --save

引入:

import HighlightableInput from "vue-highlightable-input"

页面中使用:

<template>
  <div class="home">
    <HighlightableInput 
      class="cusInput"
      highlight-style="background-color:yellow" 
      data-placeholder="Try typing any of the words below like hacker news or @Soup"
      :highlight-enabled="highlightEnabled" 
      :highlight="highlight" 
      v-model="msg"
      @input="inputHandler"
    />
  </div>
</template>

<script>
// @ is an alias to /src

import HighlightableInput from "vue-highlightable-input"
export default {
  name: "Home",
  data(){
    return{
      msg: ‘‘,
      highlight: [
        {text:chicken, style:"background-color:#f37373"},//需要高亮的文本样式
        {text:noodle, style:"background-color:#fca88f"},
        {text:soup, style:"background-color:#bbe4cb"},
        {text:so, style:"background-color:#fff05e;padding:0 10px;display:inline-block;border-radius:10px;"},
        "whatever",//走默认高亮样式
        // {start: 2, end: 5, style:"background-color:#f330ff"}
      ],
      highlightEnabled: true,//开启高亮模式
    }
  },
  methods:{
    inputHandler(){
    // input事件
      console.log("input事件",this.msg);
    }
  }
};
</script>

<style lang="scss" scoped>
.cusInput{
  border:1px solid red;
  max-height:200px;
  max-width: 200px;
  overflow-y: auto;
}
</style>

 

效果:

技术图片

 

 不过这个插件目前满足不了需求,我想让这个插件有focus和blur事件,所以,需要将源码下载下来,加上去

源码下载地址:https://github.com/SyedWasiHaider/vue-highlightable-input/archive/master.zip

阅读源码:

在components/highlightableInput.vue

源码解析:

技术图片
<template>
<!-- cusFocus事件和 cusBlur事件是自己加的源码不包含-->
  <div contenteditable="true"
    @focus="cusFocus"
    @blur="cusBlur">
  </div>
</template>

<script>

var tagsToReplace = {
    &: &amp;,
    <: &lt;,
    >: &gt;
};

import IntervalTree from node-interval-tree
import debounce from lodash/debounce
import isUndefined from lodash/isUndefined

export default {
props: {
    highlight: Array,//需要高亮的的数组(包含关键词和样式)
    value: String,
    highlightStyle: {
    // 默认的高亮样式
        type : [String, Object],
        default:  background-color:yellow
    },
    highlightEnabled: {
    // 高亮功能是否可用
        type: Boolean,
        default: true
    },
    highlightDelay: {
    // 防抖间隔毫秒数
        type: Number,
        default: 500 //This is milliseconds
    },
    caseSensitive: {
    // 区分大小写(默认不区分)
        type: Boolean,
        default: false
    },
    fireOn : {
    // 绑定的事件
    // 默认监听keydown事件
        type: String,
        default: keydown
    },
    fireOnEnabled : {
    // fireon事件是否可用
        type: Boolean,
        default: true
    }
},
data() { 
    return {
        internalValue: ‘‘,//克隆value值
        htmlOutput: ‘‘,//元素内innerHTML内容
        debouncedHandler: null,//防抖方法
    } 
},
mounted () {
    console.log(debounce);
    console.log(this.$el);
    if (this.fireOnEnabled){
    // 如果fireon事件可用,则绑定fireon事件
        this.$el.addEventListener(this.fireOn, this.handleChange)
    }
    this.internalValue = this.value;
    this.processHighlights();//执行高亮程序
},
watch: {
    highlightStyle(){
        this.processHighlights()
    },
    highlight() {
        this.processHighlights()
    },
    value() {
        if (this.internalValue != this.value){
            this.internalValue = this.value
            this.processHighlights()
        }
    },
    highlightEnabled () {
        this.processHighlights()
    },
    caseSensitive () {
        this.processHighlights();
    },
    htmlOutput() {
        var selection = this.saveSelection(this.$el);//返回光标的位置(起始与结束的索引)
        this.$el.innerHTML = this.htmlOutput;//往元素内填充内容
        this.restoreSelection(this.$el, selection);//恢复光标位置
    }
},
methods: {
    handleChange() {
    //键盘键入监听事件
        this.debouncedHandler = debounce(function(){
            console.log(this.$el.textContent);
            if (this.internalValue !== this.$el.textContent){
                this.internalValue = this.$el.textContent
                this.processHighlights();
            }
        }, this.highlightDelay)
        this.debouncedHandler();
    },
    processHighlights(){
    //高亮程序
        if (!this.highlightEnabled){
            // 如果不需要高亮
            this.htmlOutput = this.internalValue;//填充innerHTML
            this.$emit(input, this.internalValue);//触发input
            return;
        }
        var intervalTree = new IntervalTree();//区间重叠实例
        // Find the position ranges of the text to highlight
        var highlightPositions = [];//高亮位置数组
        var sortedHighlights = this.normalizedHighlights();// 生成正常的highlight格式
        if (!sortedHighlights){return};
        for (var i = 0; i < sortedHighlights.length; i++){
            var highlightObj = sortedHighlights[i];
            var indices = [];
            if (highlightObj.text){
            // 如果是对象
                if (typeof(highlightObj.text) == "string"){
                // 如果是字符串
                    // 拿到在字符串中需要插入节点的索引组成的数组
                    indices = this.getIndicesOf(highlightObj.text, this.internalValue, isUndefined(highlightObj.caseSensitive) ? this.caseSensitive : highlightObj.caseSensitive);
                    indices.forEach(start => {
                        var end = start+highlightObj.text.length - 1;
                        this.insertRange(start, end, highlightObj, intervalTree);
                    });
                }
                if (Object.prototype.toString.call(highlightObj.text) === [object RegExp]){
                // 如果是正则
                    indices = this.getRegexIndices(highlightObj.text, this.internalValue);
                    indices.forEach(pair => {
                        this.insertRange(pair.start, pair.end, highlightObj, intervalTree);
                    })
                }
            }
            if (highlightObj.start!=undefined && highlightObj.end!=undefined && highlightObj.start < highlightObj.end){
                var start = highlightObj.start;
                var end = highlightObj.end - 1;
                this.insertRange(start, end, highlightObj, intervalTree)
            }
        };
        highlightPositions = intervalTree.search(0, this.internalValue.length);
        highlightPositions = highlightPositions.sort((a,b) => a.start-b.start);
        // Construct the output with styled spans around the highlight text
        var result = ‘‘;
        var startingPosition = 0;
        for (var k = 0; k < highlightPositions.length; k++){
            var position = highlightPositions[k]
            result += this.safe_tags_replace(this.internalValue.substring(startingPosition, position.start))
            result += "<span style=‘" + highlightPositions[k].style + "‘>" + this.safe_tags_replace(this.internalValue.substring(position.start, position.end + 1)) + "</span>"
            startingPosition = position.end + 1
        }
        // In case we exited the loop early
        if (startingPosition < this.internalValue.length){
            result += this.safe_tags_replace(this.internalValue.substring(startingPosition, this.internalValue.length))
        }
        // Stupid firefox bug
        if (result[result.length-1] ==  ){
            result = result.substring(0, result.length-1)
            result += &nbsp;
        };
        this.htmlOutput = result;//设置innerhtml内容
        this.$emit(input, this.internalValue);
    },
    insertRange(start, end, highlightObj, intervalTree){
    // 插入区间树
    // 参数说明 起始索引、结束索引、高亮对象,区间数实例
        var overlap = intervalTree.search(start, end);
        
        var maxLengthOverlap = overlap.reduce((max, o) => { return Math.max(o.end-o.start, max) }, 0);
        if (overlap.length == 0){
            intervalTree.insert(start, end, { start: start, end: end, style: highlightObj.style} )
        }else if ((end - start) > maxLengthOverlap){
            overlap.forEach(o => {
                intervalTree.remove(o.start, o.end, o)
            })
            intervalTree.insert(start, end, { start: start, end: end, style: highlightObj.style} )
        }
    },
    normalizedHighlights () {
    // 生成正常的highlight格式
        if (this.highlight == null){
        // 如果不存在highlight,则返回null
            return null
        };
        if (Object.prototype.toString.call(this.highlight) === [object RegExp] || typeof(this.highlight) == "string"){
        // 如果highlight是一个正则或字符串,则返回数组格式
            return [{text: this.highlight}]
        }
        if (Object.prototype.toString.call(this.highlight) === [object Array] && this.highlight.length > 0){
        // 如果highlight是一个数组且长度不为0
            // 设置全局默认高亮样式
            var globalDefaultStyle = typeof(this.highlightStyle) == string ? this.highlightStyle : (Object.keys(this.highlightStyle).map(key => key + : + this.highlightStyle[key]).join(;) + ;)
            // 正则关键字数组
            var regExpHighlights = this.highlight.filter(x => x == Object.prototype.toString.call(x) === [object RegExp]);
            // 非正则关键字数组
            var nonRegExpHighlights = this.highlight.filter(x => x == Object.prototype.toString.call(x) !== [object RegExp])
            return nonRegExpHighlights.map(h => {
                if (h.text || typeof(h) == "string") {
                    return {
                        text:   h.text || h,
                        style:  h.style || globalDefaultStyle,
                        caseSensitive: h.caseSensitive
                    }
                }else if (h.start!=undefined && h.end!=undefined) {
                    return {
                        style:  h.style || globalDefaultStyle,
                        start: h.start,
                        end:   h.end,
                        caseSensitive: h.caseSensitive
                    }
                }else {
                    console.error("Please provide a valid highlight object or string")
                }
            }).sort((a,b) => (a.text && b.text) ? a.text > b.text : ((a.start == b.start ? (a.end < b.end) : (a.start < b.start)))).concat(regExpHighlights) 
            // We sort here in ascending order because we want to find highlights for the smaller strings first
            // and then override them later with any overlapping larger strings. So for example:
            // if we have highlights: g and gg and the string "sup gg" should have only "gg" highlighted.
            // RegExp highlights are not sorted and simply concated (this could be done better  in the future)
        }
        console.error("Expected a string or an array of strings")
        return null
    },

    // Copied from: https://stackoverflow.com/questions/5499078/fastest-method-to-escape-html-tags-as-html-entities
    safe_tags_replace(str) {
    // 安全标签替换
        return str.replace(/[&<>]/g, this.replaceTag);
    },

    replaceTag(tag) {
        return tagsToReplace[tag] || tag;
    },

    getRegexIndices(regex, str) {
    // 正则时 生成indices
        if (!regex.global){
            console.error("Expected " + regex + " to be global")
            return []
        }
        
        regex = RegExp(regex)
        var indices = [];
        var match = null;
        while ((match = regex.exec(str)) != null) {
            indices.push({start:match.index, end: match.index + match[0].length - 1});
        }
        return indices;
    },

    // Copied verbatim because I‘m lazy:
    // https://stackoverflow.com/questions/3410464/how-to-find-indices-of-all-occurrences-of-one-string-in-another-in-javascript
    getIndicesOf(searchStr, str, caseSensitive) {
    // 参数说明  关键字、当前元素内的文本、是否区分大小写
        var searchStrLen = searchStr.length;
        if (searchStrLen == 0) {
            return [];
        }
        var startIndex = 0, index, indices = [];
        if (!caseSensitive) {
            str = str.toLowerCase();
            searchStr = searchStr.toLowerCase();
        }
        while ((index = str.indexOf(searchStr, startIndex)) > -1) {
            indices.push(index);
            startIndex = index + searchStrLen;
        }
        return indices;
    },
    
    // Copied but modifed slightly from: https://stackoverflow.com/questions/14636218/jquery-convert-text-url-to-link-as-typing/14637351#14637351
    saveSelection(containerEl){
    // 保存光标位置
        var start;
        if (window.getSelection && document.createRange) {
            // 支持window.getSelection
            console.log("支持window.getSelection");
            var selection = window.getSelection()
            if (!selection || selection.rangeCount == 0){return}  
            var range = selection.getRangeAt(0);//获取指定索引的range
            var preSelectionRange = range.cloneRange();//克隆range对象
            preSelectionRange.selectNodeContents(containerEl);//此节点的内容被包含在range中
            preSelectionRange.setEnd(range.startContainer, range.startOffset);//设置range的结束位置(range的开始节点,在 startContainer 中的起始位置的数字。)
            start = preSelectionRange.toString().length;//range的长度
            // console.log(start,start + range.toString().length);
            return {
                start: start,
                end: start + range.toString().length
            }
        } else if (document.selection) {
            // 支持document.selection
            console.log("支持window.getSelection");
            var selectedTextRange = document.selection.createRange();
            var preSelectionTextRange = document.body.createTextRange();
            preSelectionTextRange.moveToElementText(containerEl);
            preSelectionTextRange.setEndPoint("EndToStart", selectedTextRange);
            start = preSelectionTextRange.text.length;
            return {
                start: start,
                end: start + selectedTextRange.text.length
            }
        }
    },

    // Copied but modifed slightly from: https://stackoverflow.com/questions/14636218/jquery-convert-text-url-to-link-as-typing/14637351#14637351
    restoreSelection(containerEl, savedSel){
    // 还原光标位置
        if (!savedSel){return}
        if (window.getSelection && document.createRange) {
            var charIndex = 0, range = document.createRange();
            range.setStart(containerEl, 0);
            range.collapse(true);
            var nodeStack = [containerEl], node, foundStart = false, stop = false;
            while (!stop && (node = nodeStack.pop())) {
                if (node.nodeType == 3) {
                    var nextCharIndex = charIndex + node.length;
                    if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {
                        range.setStart(node, savedSel.start - charIndex);
                        foundStart = true;
                    }
                    if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {
                        range.setEnd(node, savedSel.end - charIndex);
                        stop = true;
                    }
                    charIndex = nextCharIndex;
                } else {
                    var i = node.childNodes.length;
                    while (i--) {
                        nodeStack.push(node.childNodes[i]);
                    }
                }
            }

            var sel = window.getSelection();
            sel.removeAllRanges();
            sel.addRange(range);
        } else if (document.selection) {
            var textRange = document.body.createTextRange();
            textRange.moveToElementText(containerEl);
            textRange.collapse(true);
            textRange.moveEnd("character", savedSel.end);
            textRange.moveStart("character", savedSel.start);
            textRange.select();
        }
    },
    cusFocus(){
    // 自定义获取焦点方法 这是自己加的源吗里没有
        this.$emit("focus")
    },
    cusBlur(){
    // 自定义失去焦点方法 这是自己加的源吗里没有 
        this.$emit("blur")
    }
}
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
div {
  height: 50px;
}

</style>
View Code

使用:

<template>
  <div class="home">
    <HighlightableInput 
      class="cusInput"
      highlight-style="background-color:yellow" 
      data-placeholder="Try typing any of the words below like hacker news or @Soup"
      :highlight-enabled="highlightEnabled" 
      :highlight="highlight" 
      v-model="msg"
      @focus="inputFocus"
      @input="inputHandler"
      @blur="inputBlur"
    />
  </div>
</template>

<script>
// @ is an alias to /src

// import HighlightableInput from "vue-highlightable-input"
export default {
  name: "Home",
  components: {
    HighlightableInput:()=>import(@/components/highlightableInput)
  },
  data(){
    return{
      msg: ‘‘,
      highlight: [
        {text:chicken, style:"background-color:#f37373"},//需要高亮的文本样式
        {text:noodle, style:"background-color:#fca88f"},
        {text:soup, style:"background-color:#bbe4cb"},
        {text:so, style:"background-color:#fff05e;padding:0 10px;display:inline-block;border-radius:10px;"},
        "whatever",//走默认高亮样式
        // {start: 2, end: 5, style:"background-color:#f330ff"}
      ],
      highlightEnabled: true,//开启高亮模式
    }
  },
  methods:{
    inputFocus(){
    // 获得焦点
      console.log("获得焦点");
    },
    inputBlur(){
    // 失去焦点
      console.log("失去焦点");
    },
    inputHandler(){
    // input事件
      console.log("input事件",this.msg);
    }
  }
};
</script>

<style lang="scss" scoped>
.cusInput{
  border:1px solid red;
  max-height:200px;
  max-width: 200px;
  overflow-y: auto;
}
</style>

这个插件中用到了三个插件:

import IntervalTree from ‘node-interval-tree‘;//区间树
import debounce from ‘lodash/debounce‘;lodash防抖
import isUndefined from ‘lodash/isUndefined‘

以上就是HighlightableInput插件的简单使用,做一下记录;源码中写到了,区间树的使用以及div元素设置为可编辑状态后,光标会移动到最前面,里面有对应的解决方案。

 

以上是关于Vue高亮输入 (Vue Highlightable Input)使用,node-interval-tree区间树,可编辑div光标前移解决方案的主要内容,如果未能解决你的问题,请参考以下文章

Vue高亮输入 (Vue Highlightable Input)使用,node-interval-tree区间树,可编辑div光标前移解决方案

Sublime安装插件 vue代码高亮 + scss高亮

vue(element)中使用codemirror实现代码高亮,代码补全,版本差异对比

vue(element)中使用codemirror实现代码高亮,代码补全,版本差异对比

CodeMirror---实现关键词高亮

基于vue的实时搜索并高亮显示关键词