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 = { ‘&‘: ‘&‘, ‘<‘: ‘<‘, ‘>‘: ‘>‘ }; 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 += ‘ ‘ }; 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>
使用:
<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光标前移解决方案
vue(element)中使用codemirror实现代码高亮,代码补全,版本差异对比