UNPKG

cross-text-highlight

Version:

``` npm i cross-text-highlight ``` # 基于vue-search-highlight 的跨标签可标注序号的文本高亮插件

658 lines (612 loc) 22.8 kB
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define([], factory); else if(typeof exports === 'object') exports["cross-text-highlight"] = factory(); else root["cross-text-highlight"] = factory(); })((typeof self !== 'undefined' ? self : this), function() { return /******/ (function() { // webpackBootstrap /******/ "use strict"; /******/ // The require scope /******/ var __webpack_require__ = {}; /******/ /************************************************************************/ /******/ /* webpack/runtime/define property getters */ /******/ !function() { /******/ // define getter functions for harmony exports /******/ __webpack_require__.d = function(exports, definition) { /******/ for(var key in definition) { /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); /******/ } /******/ } /******/ }; /******/ }(); /******/ /******/ /* webpack/runtime/hasOwnProperty shorthand */ /******/ !function() { /******/ __webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } /******/ }(); /******/ /******/ /* webpack/runtime/make namespace object */ /******/ !function() { /******/ // define __esModule on exports /******/ __webpack_require__.r = function(exports) { /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); /******/ } /******/ Object.defineProperty(exports, '__esModule', { value: true }); /******/ }; /******/ }(); /******/ /******/ /* webpack/runtime/publicPath */ /******/ !function() { /******/ __webpack_require__.p = ""; /******/ }(); /******/ /************************************************************************/ var __webpack_exports__ = {}; // ESM COMPAT FLAG __webpack_require__.r(__webpack_exports__); // EXPORTS __webpack_require__.d(__webpack_exports__, { "default": function() { return /* binding */ entry_lib; } }); ;// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/setPublicPath.js /* eslint-disable no-var */ // This file is imported into lib/wc client bundles. if (typeof window !== 'undefined') { var currentScript = window.document.currentScript if (false) { var getCurrentScript; } var src = currentScript && currentScript.src.match(/(.+\/)[^/]+\.js(\?.*)?$/) if (src) { __webpack_require__.p = src[1] // eslint-disable-line } } // Indicate to webpack that this file can be concatenated /* harmony default export */ var setPublicPath = (null); ;// CONCATENATED MODULE: ./node_modules/@vue/cli-service/node_modules/@vue/vue-loader-v15/lib/loaders/templateLoader.js??ruleSet[1].rules[3]!./node_modules/@vue/cli-service/node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./src/components/crossTextHighLight/index.vue?vue&type=template&id=6579304a&scoped=true var render = function render(){var _vm=this,_c=_vm._self._c;return _c('div',{staticClass:"content-main",on:{"contextmenu":function($event){$event.preventDefault();$event.stopPropagation();return _vm.openContextMenu.apply(null, arguments)},"mouseup":_vm.handleMouseUp}},[(_vm.showContentMenu)?_c('div',{style:({ 'left': _vm.left + 'px', 'top': _vm.top + 'px' }),attrs:{"id":"contentMenu"}},[_c('ul',[_c('li',{on:{"click":_vm.copy}},[_c('i',{staticClass:"el-icon-copy-document"}),_c('span',[_vm._v("复制")])])])]):_vm._e(),_c('div',{staticClass:"highlight",attrs:{"id":"highlight"},domProps:{"innerHTML":_vm._s(_vm.contentShow)}})]) } var staticRenderFns = [] ;// CONCATENATED MODULE: ./node_modules/@vue/cli-service/node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./src/components/crossTextHighLight/index.vue?vue&type=script&lang=js const FONT_FLAG = 'cross-text-hightlight' /* harmony default export */ var crossTextHighLightvue_type_script_lang_js = ({ name: 'CrossTextHighLight', props: { content: { type: String, default: '' }, moveBehavior: { type: Boolean, default: false }, keyword: { type: String, default: '' }, highlightStyle: { type: String, default: 'background: #ffff00' }, currentStyle: { type: String, default: 'background: #ff9632' }, regExp: { type: Boolean, default: false }, // 标记样式‘ markWidth: { type: String, default: '32px' }, markHeight: { type: String, default: '32px' }, startColor: { type: String, default: '#ff9632' }, endColor: { type: String, default: '#ffff00' }, textColor: { type: String, default: '#fff' }, borderColor: { type: String, default: '#ff9632' }, markLeft: { type: String, default: '-30px' }, markTop: { type: String, default: '-16px' }, markWeight: { type: String, default: '700' }, markFontSize: { type: String, default: '22px' }, markZoom: { type: String, default: '.65' }, // 是否开启标记 isNeedMark: { type: Boolean, default: false }, pEle: { type: String, default: 'htmlContent' }, isContextMenu: { type: Boolean, default: false } }, data() { return { left: 0, top: 0, showContentMenu: false, lightIndex: 0, matchCount: 0, contentShow: '', copyText: '', random: `${Math.random()}`.slice(2) } }, computed: { watchString() { return [this.content, this.keyword] }, watchStyle() { return [this.lightIndex, this.highlightStyle, this.currentStyle] }, flag() { return `${FONT_FLAG}${this.random}` }, styleSelector() { return `style[${this.flag}]` }, }, watch: { watchString: { immediate: true, handler() { this.replaceKeywords() } }, watchStyle: { immediate: true, handler() { this.setStyle() } }, lightIndex: { immediate: true, handler() { this.$emit('current-change', this.lightIndex) } }, matchCount: { immediate: true, handler() { this.$emit('match-count-change', this.matchCount) } } }, mounted() { if (this.isNeedMark) { this.$nextTick(() => { this.markNumber(0) }) } // 选中监听 document.onclick = (event) => { console.log(event); if (this.showContentMenu && this.checkIn(document.getElementById('contentMenu'))) { return } this.showContentMenu = false } }, beforeDestroy() { this.clearStyle() }, methods: { handleMouseUp() { if (!this.isContextMenu) return let selectedText = ''; selectedText = window.getSelection().toString() if (selectedText) { this.copyText = selectedText } if (!this.showContentMenu && this.checkIn(document.getElementById('highlight'))) { this.left = event.offsetX this.top = event.offsetY } this.$emit('selection-change', selectedText, event.offsetX, event.offsetY); }, copy() { this.$copyText(this.copyText).then((e) => { this.$message({ message: '复制成功', type: 'success' }) console.log(e) }, (e) => { this.$message({ message: '复制失败', type: 'error' }) console.log(e) }) }, openContextMenu() { if (!this.isContextMenu) return if(!window.getSelection().toString()){ return } console.log(window.getSelection().toString()); this.showContentMenu = true }, checkIn(obj) { var x = Number(window.event.clientX) // 鼠标相对屏幕横坐标 var y = Number(window.event.clientY) // 鼠标相对屏幕纵坐标 var div_x = Number(obj.getBoundingClientRect().left) // obj相对屏幕的横坐标 var div_x_width = Number( obj.getBoundingClientRect().left + obj.clientWidth ) // obj相对屏幕的横坐标+width var div_y = Number(obj.getBoundingClientRect().top) // obj相对屏幕的纵坐标 var div_y_height = Number( obj.getBoundingClientRect().top + obj.clientHeight ) // obj相对屏幕的纵坐标+height if (x > div_x && x < div_x_width && y > div_y && y < div_y_height) { return true } else { return false } }, getTextNodeList(dom) { const nodeList = [...dom.childNodes] const textNodes = [] while (nodeList.length) { const node = nodeList.shift() if (node.nodeType === node.TEXT_NODE) { node.wholeText && textNodes.push(node) } else { nodeList.unshift(...node.childNodes) } } return textNodes }, getTextInfoList(textNodes) { let length = 0 return textNodes.map(node => { let startIdx = length, endIdx = length + node.wholeText.length length = endIdx return { text: node.wholeText, startIdx, endIdx } }) }, getMatchList(content, keyword) { if (!this.regExp) { const characters = [...'\\[](){}?.+*^$:|'].reduce((r, c) => (r[c] = true, r), {}) keyword = keyword.split('').map(s => characters[s] ? `\\${s}` : s).join('[\\s\\n]*') } const reg = new RegExp(keyword, 'gmi') const matchList = [] let match = reg.exec(content) while (match) { matchList.push(match) match = reg.exec(content) } return matchList }, replaceMatchResult(textNodes, textList, matchList) { // 对于每一个匹配结果,可能分散在多个标签中,找出这些标签,截取匹配片段并用font标签替换出 for (let i = matchList.length - 1; i >= 0; i--) { const match = matchList[i] const matchStart = match.index, matchEnd = matchStart + match[0].length // 匹配结果在拼接字符串中的起止索引 // 遍历文本信息列表,查找匹配的文本节点 for (let textIdx = 0; textIdx < textList.length; textIdx++) { const { text, startIdx, endIdx } = textList[textIdx] // 文本内容、文本在拼接串中开始、结束索引 if (endIdx < matchStart) continue // 匹配的文本节点还在后面 if (startIdx >= matchEnd) break // 匹配文本节点已经处理完了 let textNode = textNodes[textIdx] // 这个节点中的部分或全部内容匹配到了关键词,将匹配部分截取出来进行替换 const nodeMatchStartIdx = Math.max(0, matchStart - startIdx) // 匹配内容在文本节点内容中的开始索引 const nodeMatchLength = Math.min(endIdx, matchEnd) - startIdx - nodeMatchStartIdx // 文本节点内容匹配关键词的长度 if (nodeMatchStartIdx > 0) textNode = textNode.splitText(nodeMatchStartIdx) // textNode取后半部分 if (nodeMatchLength < textNode.wholeText.length) textNode.splitText(nodeMatchLength) const font = document.createElement('font') font.setAttribute(this.flag, i + 1) font.innerText = text.substr(nodeMatchStartIdx, nodeMatchLength) textNode.parentNode.replaceChild(font, textNode) } } }, replaceKeywords() { let errFlag = false if (this.regExp) { try { const reg = new RegExp(this.keyword) if (reg.test('')) errFlag = true } catch (err) { errFlag = true } } if (errFlag || !this.keyword) { this.matchCount = 0; this.lightIndex = 0; this.contentShow = this.content return } const div = document.createElement('div') div.innerHTML = this.content const textNodes = this.getTextNodeList(div) const textList = this.getTextInfoList(textNodes) const content = textList.map(({ text }) => text).join('') const matchList = this.getMatchList(content, this.keyword) this.matchCount = matchList.length this.lightIndex = this.matchCount ? 1 : 0 this.replaceMatchResult(textNodes, textList, matchList) this.contentShow = div.innerHTML }, /** * scrollToOrder 滚动方法 * @param index 当前序号 * @param cEle 子元素也是文本内容元素 */ scrollToOrder(index, cEle) { this.$nextTick(() => { let node = this.$el.querySelector(`font[${this.flag}='${index}']`) if (node) { this.lightIndex = index // 标记序号 if (this.isNeedMark) { this.markNumber(index - 1, this.pEle) } // 外层包裹的盒子 const contentElement2 = document.getElementById(this.pEle) setTimeout(() => { const contentDiv = cEle; // 获取 ref 为 "content" 的 DOM 元素 if (contentDiv) { const contentRect = contentDiv.$el.getBoundingClientRect() // 获取 contentDiv 的位置 const highlightRect = node.getBoundingClientRect() // 获取 highlightedElement 的位置 // 计算相对于 contentDiv 的位置 const relativeTop = highlightRect.top - contentRect.top; contentElement2.scrollTo({ top: relativeTop - 200, behavior: 'smooth' }); } }, 100) }else{ console.log("未找到该节点") return } }) }, scrollTo(index) { this.$nextTick(() => { let node = this.$el.querySelector(`font[${this.flag}='${index}']`) if (node) { this.lightIndex = index node.scrollIntoView() } }) }, markNumber(index) { let parent = document.getElementById(this.pEle); let arr = [] // 获取所有font标签 let fonts = parent.getElementsByTagName('font'); for (let i = 0; i < fonts.length; i++) { if (fonts[i].attributes.length > 0 && fonts[i].attributes[0].name.search('cross-text-hightlight') != -1) { arr.push(fonts[i]) } } if (!arr[index]) { console.log("未找到该节点") return } let canvas = document.createElement('canvas') canvas.width = 200 // 设置canvas宽度 canvas.height = 200 // 设置canvas高度 let ctx = canvas.getContext('2d') ctx.clearRect(0, 0, canvas.width, canvas.height) ctx.font = '150px Arial' // 设置文本字体大小和样式 ctx.fillStyle = 'white' // 设置文本颜色 ctx.textAlign = 'center' // 设置文本水平居中 ctx.textBaseline = 'middle' // 设置文本垂直居中 ctx.fillText(index + 1, 95, 110) // 在canvas上绘制文本 let img = new Image() img.src = canvas.toDataURL() let imageElement = document.createElement('img') imageElement.src = img.src // imageElement.style.zIndex = `${index}` imageElement.className = `common-marker` imageElement.style.setProperty('width', `${this.markWidth}`); imageElement.style.setProperty('height', `${this.markHeight}`); imageElement.style.setProperty('border-radius', '50%'); imageElement.style.setProperty('background', `linear-gradient(to right, ${this.startColor}, ${this.endColor})`); imageElement.style.setProperty('color', `${this.textColor}`); imageElement.style.setProperty('border', `1px solid ${this.borderColor}`); imageElement.style.setProperty('line-height', `${this.markHeight}`); imageElement.style.setProperty('font-weight', `${this.markWeight}`); imageElement.style.setProperty('position', 'absolute'); imageElement.style.setProperty('left', `${this.markLeft}`); imageElement.style.setProperty('top', `${this.markTop}`); imageElement.style.setProperty('font-size', `${this.markFontSize}`); imageElement.style.setProperty('zoom', `${this.markZoom}`); imageElement.style.setProperty('text-align', 'center'); imageElement.style.setProperty('user-select', 'none'); imageElement.style.setProperty('z-index', '999'); imageElement.style.setProperty('max-width', 'none', '!important'); // 清除之前节点 document.querySelectorAll(".common-marker").forEach(element => { element.remove(); }) // 把之前节点去掉 arr[index].insertBefore(imageElement, arr[index].firstChild); }, searchNext() { this.$nextTick(() => { let idx = this.lightIndex >= this.matchCount ? 1 : this.lightIndex + 1 this.scrollToOrder(idx) }) }, searchLast() { this.$nextTick(() => { let idx = this.lightIndex <= 1 ? this.matchCount : this.lightIndex - 1 this.scrollToOrder(idx) }) }, setStyle() { let style = document.head.querySelector(this.styleSelector) if (!style) { style = document.createElement('style') style.setAttribute(this.flag, 1) } style.innerText = `font[${this.flag}]{${this.highlightStyle};position:relative}font[${this.flag}='${this.lightIndex}']{${this.currentStyle};position:relative}` document.head.appendChild(style) }, clearStyle() { let style = document.head.querySelector(this.styleSelector) style && document.head.removeChild(style) } } }); ;// CONCATENATED MODULE: ./src/components/crossTextHighLight/index.vue?vue&type=script&lang=js /* harmony default export */ var components_crossTextHighLightvue_type_script_lang_js = (crossTextHighLightvue_type_script_lang_js); ;// CONCATENATED MODULE: ./node_modules/mini-css-extract-plugin/dist/loader.js??clonedRuleSet-64.use[0]!./node_modules/css-loader/dist/cjs.js??clonedRuleSet-64.use[1]!./node_modules/@vue/cli-service/node_modules/@vue/vue-loader-v15/lib/loaders/stylePostLoader.js!./node_modules/postcss-loader/dist/cjs.js??clonedRuleSet-64.use[2]!./node_modules/sass-loader/dist/cjs.js??clonedRuleSet-64.use[3]!./node_modules/@vue/cli-service/node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./src/components/crossTextHighLight/index.vue?vue&type=style&index=0&id=6579304a&prod&scoped=true&lang=scss // extracted by mini-css-extract-plugin ;// CONCATENATED MODULE: ./src/components/crossTextHighLight/index.vue?vue&type=style&index=0&id=6579304a&prod&scoped=true&lang=scss ;// CONCATENATED MODULE: ./node_modules/@vue/cli-service/node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js /* globals __VUE_SSR_CONTEXT__ */ // IMPORTANT: Do NOT use ES2015 features in this file (except for modules). // This module is a runtime utility for cleaner component module output and will // be included in the final webpack user bundle. function normalizeComponent( scriptExports, render, staticRenderFns, functionalTemplate, injectStyles, scopeId, moduleIdentifier /* server only */, shadowMode /* vue-cli only */ ) { // Vue.extend constructor export interop var options = typeof scriptExports === 'function' ? scriptExports.options : scriptExports // render functions if (render) { options.render = render options.staticRenderFns = staticRenderFns options._compiled = true } // functional template if (functionalTemplate) { options.functional = true } // scopedId if (scopeId) { options._scopeId = 'data-v-' + scopeId } var hook if (moduleIdentifier) { // server build hook = function (context) { // 2.3 injection context = context || // cached call (this.$vnode && this.$vnode.ssrContext) || // stateful (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional // 2.2 with runInNewContext: true if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { context = __VUE_SSR_CONTEXT__ } // inject component styles if (injectStyles) { injectStyles.call(this, context) } // register component module identifier for async chunk inferrence if (context && context._registeredComponents) { context._registeredComponents.add(moduleIdentifier) } } // used by ssr in case component is cached and beforeCreate // never gets called options._ssrRegister = hook } else if (injectStyles) { hook = shadowMode ? function () { injectStyles.call( this, (options.functional ? this.parent : this).$root.$options.shadowRoot ) } : injectStyles } if (hook) { if (options.functional) { // for template-only hot-reload because in that case the render fn doesn't // go through the normalizer options._injectStyles = hook // register for functional component in vue file var originalRender = options.render options.render = function renderWithStyleInjection(h, context) { hook.call(context) return originalRender(h, context) } } else { // inject component registration as beforeCreate hook var existing = options.beforeCreate options.beforeCreate = existing ? [].concat(existing, hook) : [hook] } } return { exports: scriptExports, options: options } } ;// CONCATENATED MODULE: ./src/components/crossTextHighLight/index.vue ; /* normalize component */ var component = normalizeComponent( components_crossTextHighLightvue_type_script_lang_js, render, staticRenderFns, false, null, "6579304a", null ) /* harmony default export */ var crossTextHighLight = (component.exports); ;// CONCATENATED MODULE: ./src/index.js /* harmony default export */ var src_0 = (crossTextHighLight); ;// CONCATENATED MODULE: ./node_modules/@vue/cli-service/lib/commands/build/entry-lib.js /* harmony default export */ var entry_lib = (src_0); /******/ return __webpack_exports__; /******/ })() ; }); //# sourceMappingURL=cross-text-highlight.umd.js.map