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事件,另外发现了中文输入法输入时,会将中文和拼音同事输入,我是不允许这样的情况发生的,所以,需要将源码下载下来,加上去了两个事件以及把这个bug修复

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

阅读源码:

在components/highlightableInput.vue

源码解析:

<template>
  <!-- cusFocus事件和 cusBlur事件是自己加的源码不包含-->
  <div
    contenteditable="true"
    @focus="cusFocus"
    @blur="cusBlur"
    @compositionstart="divCompositionstart"
    @compositionend="divCompositionend"
  ></div>
</template>

<script>
/**
 * compositionstart:中文输入法时,刚开始输入会触发这个时间,代表翻译开始
 * compositionend:当输入法翻译成中文后,比如(space键)此刻,执行这个事件
 * 以上两个事件是解决,当输入中文时,会将拼音和英文同事放入输入框的问题
 */
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, //防抖方法
      inputFlag: true, //是否在非翻译/非转换状态
    };
  },
  mounted() {
    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;
      }
      if (!this.inputFlag) {
        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');
    },
    divCompositionstart() {
      // 翻译开始
      this.inputFlag = false;
    },
    divCompositionend() {
      // 翻译结束
      this.inputFlag = true;
      // 翻译结束后执行一次赋值和高亮程序
      this.internalValue = this.$el.textContent;
      this.processHighlights();
    },
  },
};
</script>

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

/*为空时显示 element attribute content*/
div:empty:before {
  font-family: 'iconfont';
  content: 'e6e4'attr(data-placeholder);
  /* element attribute*/
  /*content: 'this is content';*/
  color: #c0c4cc;
}

/*焦点时内容为空*/
div:focus:before {
  /* content: none; */
}
</style>
View Code

使用:

<template>
  <div class="home">
    <div class="hignlightWrap">
      <HighlightableInput
        ref="hignLightInput"
        class="cusInput"
        cusClass="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 class="selectWrap" v-show="popoverVisible">
        <div class="topBox">title标题</div>
        <div class="contentBox">
          <div
            class="options"
            v-for="item in 8"
            :key="item"
            @mousedown="clickOption(item)"
          >
            选项{{ item }}
          </div>
        </div>
      </div>
    </div>
  </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, //开启高亮模式
      popoverVisible: false,
    };
  },
  methods: {
    inputFocus() {
      // 获得焦点
      console.log('获得焦点');
      this.popoverVisible = true;
    },
    inputBlur() {
      // 失去焦点
      console.log('失去焦点');
      console.log(this.$refs.hignLightInput.$el);
      let pos = this.$refs.hignLightInput.saveSelection(
        this.$refs.hignLightInput.$el
      );
      console.log('光标位置', pos);
      sessionStorage.setItem('curPos', JSON.stringify(pos));
      this.popoverVisible = false;
    },
    inputHandler() {
      // input事件
      console.log('input事件', this.msg);
    },
    clickOption(item) {
      // 点击选项
      setTimeout(() => {
        console.log(this.msg);
        let str = `选项${item}`;
        console.log(str);
        let curPos = JSON.parse(sessionStorage.getItem('curPos'));
        let index = curPos.start > 0 ? curPos.start - 1 : 0;
        this.msg =
          this.msg.substr(0, curPos.start) + str + this.msg.substr(index + 1);
        console.log(this.msg);
        console.log(curPos);
        curPos.start += str.length;
        curPos.end += str.length;
        console.log(curPos);
        this.$nextTick(() => {
          this.$refs.hignLightInput.restoreSelection(
            this.$refs.hignLightInput.$el,
            curPos
          );
        });
      }, 100);

      console.log('选项点击');
    },
  },
};
</script>

<style lang="scss" scoped>
.hignlightWrap {
  position: relative;
  display: inline-block;
  .cusInput {
    border: 1px solid red;
    max-height: 300px;
    width: 400px;
    overflow-y: auto;
    padding: 8px;
  }
  .selectWrap {
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    width: 400px;
    border: 1px solid #3d80cc;
    border-radius: 4px;
    .topBox {
      height: 50px;
      border-bottom: 1px solid #3d80cc;
      padding: 8px;
      box-sizing: border-box;
      display: flex;
      align-items: center;
    }
    .contentBox {
      padding: 8px 0;
      max-height: 300px;
      overflow-y: auto;
      .options {
        display: flex;
        align-items: center;
        padding: 0 8px;
        height: 50px;
        box-sizing: border-box;
        border-bottom: 1px solid #eee;
        cursor: pointer;
        &:hover {
          background-color: #eee;
        }
      }
    }
  }
}
</style>

效果:

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

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

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

原文地址:https://www.cnblogs.com/fqh123/p/14001884.html