zoukankan      html  css  js  c++  java
  • 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元素设置为可编辑状态后,光标会移动到最前面,里面有对应的解决方案。

  • 相关阅读:
    实验 7 综合练习一
    实验或作业模版: 实验 6-1 最大公约数 最小公倍数
    实验 6 数组1
    Pro
    作业 4 函数应用
    老大
    双端队列
    zxa and leaf
    Baby Ming and Matrix games
    The more, The Better
  • 原文地址:https://www.cnblogs.com/fqh123/p/14001884.html
Copyright © 2011-2022 走看看