cross-text-highlight
Version:
``` npm i cross-text-highlight ``` # 基于vue-search-highlight 的跨标签可标注序号的文本高亮插件
658 lines (612 loc) • 22.8 kB
JavaScript
(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