react.js 原生文字下划线标注功能开发 (原生js封装)
效果展示:
可以输出标注的内容👆
index.jsx
import React, { useState, useEffect, useRef, } from 'react';
import { Menu, Button, Select, Tag } from 'antd';
import { CheckOutlined, RollbackOutlined ,UndoOutlined } from '@ant-design/icons';
import { LabelText } from './labeltext';
import './index.css';
const str =
'WebGL(全写Web Graphics Library)是一种3D绘图协议,这种绘图技术标准允许把JavaScript和OpenGL ES 2.0结合在一起,通过增加OpenGL ES 2.0的一个JavaScript绑定,WebGL可以为HTML5 Canvas提供硬件3D加速渲染,这样Web开发人员就可以借助系统显卡来在浏览器里更流畅地展示3D场景和模型了,还能创建复杂的导航和数据视觉化。显然,WebGL技术标准免去了开发网页专用渲染插件的麻烦,可被用于创建具有复杂3D结构的网站页面,甚至可以用来设计3D网页游戏等等。还有一些厂商对MSAA技术进行了扩展,这里只简单提下: - 2006年,NVIDIA提出的CSAA(coverage sampling antialiasing),AMD提出的EQAA(enhanced quality antialiasing),尝试优化MSAA的Coverage来改进AA效果,但是提升的效果非常有限,参见[7, 8]的介绍; - 2007年,ATI提出的CFAA(custom filter antialiasing),尝试优化MSAA的Resolve阶段的过滤算法(默认是Box Filter)来改进AA效果;移动平台的TBR架构能高效的支持MSAA,但是由于MSAA的原理限制,它不适用于延迟管线(deferred pipeline)。此时,快速近似抗锯齿(FXAA,fast approximate antialising)可以作为MSAA算法的补充。FXAA是通过单一次、全屏的后处理来实现的,也是对边缘像素多次超采样,达到边缘锯齿的效果,是一个偏经验性的算法(简单说,就是别问我它为什么有效果),虚幻4引擎集成了该技术[9]。光珊化后,每一个像素点都包含了 颜色 、深度 、纹理数据, 这个我们叫做片元小tips : 每个像素的颜色由片元着色器的gl_FragColor提供接收光栅化阶段生成的片元,在光栅化阶段中,已经计算出每个片元的颜色信息,这一阶段会将片元做逐片元挑选的操作,处理过的片元会继续向后面的阶段传递。 片元着色器运行的次数由图形有多少个片元决定的。逐片元挑选通过模板测试和深度测试来确定片元是否要显示,测试过程中会丢弃掉部分无用的片元内容,然后生成可绘制的二维图像绘制并显示。● 深度测试: 就是对 z 轴的值做测试,值比较小的片元内容会覆盖值比较大的。(类似于近处的物体会遮挡远处物体)。● 模板测试: 模拟观察者的观察行为,可以接为镜像观察。标记所有镜像中出现的片元,最后只绘制有标记的内容。';
let labelText;
export default function TextMark() {
const contextRef = useRef(null);
const [modalSelectContent, setModalSelectContent] = useState(null);
window.setModalSelectContent = setModalSelectContent;
const items = [
{
label: <Tag color="#c41d7f">#c41d7f</Tag>,
key: '#c41d7f',
color: '#c41d7f',
background: '#fff0f6',
border: '#ffadd2',
},
{
label: <Tag color="#cf1322">#cf1322</Tag>,
key: '#cf1322',
color: '#cf1322',
background: '#fff1f0',
border: '#ffa39e',
},
{
label: <Tag color="#d4380d">#d4380d</Tag>,
key: '#d4380d',
color: '#d4380d',
background: '#fff2e8',
border: '#ffbb96',
},
{
label: <Tag color="#d46b08">#d46b08</Tag>,
key: '#d46b08',
color: '#d46b08',
background: '#fff7e6',
border: '#ffd591',
},
{
label: <Tag color="#d48806">#d48806</Tag>,
key: '#d48806',
color: '#d48806',
background: '#fffbe6',
border: '#ffe58f',
},
{
label: <Tag color="#7cb305">#7cb305</Tag>,
key: '#7cb305',
color: '#7cb305',
background: '#fcffe6',
border: '#eaff8f',
},
{
label: <Tag color="#389e0d">#389e0d</Tag>,
key: '#389e0d',
color: '#389e0d',
background: '#f6ffed',
border: '#b7eb8f',
},
{
label: <Tag color="#08979c">#08979c</Tag>,
key: '#08979c',
color: '#08979c',
background: '#e6fffb',
border: '#87e8de',
},
{
label: <Tag color="#0958d9">#0958d9</Tag>,
key: '#0958d9',
color: '#0958d9',
background: '#e6f4ff',
border: '#91caff',
},
{
label: <Tag color="#1d39c4">#1d39c4</Tag>,
key: '#1d39c4',
color: '#1d39c4',
background: '#f0f5ff',
border: '#adc6ff',
},
{
label: <Tag color="#531dab">#531dab</Tag>,
key: '#531dab',
color: '#531dab',
background: '#f9f0ff',
border: '#d3adf7',
},
];
const [menuKey, setMenuKey] = useState(Math.random());
// 自定义tag
const tagRender = (props) => {
const { label, value } = props;
return (
<>
{label ? (
<Tag color={value} style={{ marginLeft: 5 }}>
{label}
</Tag>
) : null}
</>
);
};
useEffect(() => {
// Step1 :
// eslint-disable-next-line react-hooks/exhaustive-deps
labelText = new LabelText({
el: contextRef.current,
});
// Step2 : 模拟接口调用
setTimeout(() => {
labelText.addText(str);
}, 0);
}, []);
return (
<>
<header>
<div className="operation">
<Button
type="primary"
shape="round"
icon={<RollbackOutlined />}
onClick={() => {
window.repeal();
}}
>
上一步
</Button>
<Button
type="primary"
shape="round"
danger
icon={<UndoOutlined />}
onClick={() => {
window.clean();
}}
>
重置标注
</Button>
<Button
type="primary"
shape="round"
icon={<CheckOutlined />}
onClick={() => {
console.log(labelText.output());
labelText.output();
}}
>
标注结果
</Button>
<div className="labelSelect"></div>
</div>
</header>
<div className="container">
<div
className="content-container interval"
ref={contextRef}
></div>
{/* 下拉选择 */}
<div id="select-modal">
<div className="input-select">
<Select
size="large"
mode="multiple"
allowClear={modalSelectContent}
defaultOpen={false}
showArrow={false}
dropdownStyle={{ height: 0 }}
showSearch={false}
placeholder="请选择标签"
maxTagCount={1}
value={modalSelectContent || null}
tagRender={tagRender}
onClear={() => {
labelText.deleteLabel(modalSelectContent);
}}
style={{ width: '100%' }}
/>
</div>
<div className="label-show">
<Menu
key={ menuKey }
items={items}
onSelect={(item) => {
window.selectLabel(item?.key);
setMenuKey(Math.random());
}}
/>
</div>
</div>
<div className="operation-area">
<div className="card-title">操作</div>
</div>
</div>
</>
);
}
index.css
.operation {
padding: 0px 30px;
box-sizing: border-box;
line-height: 50px;
background-color: #ffffff;
box-shadow: 2px 0 6px rgb(0 21 41 / 35%);
}
.ant-btn {
margin-right: 5px;
}
.labelSelect {
margin-bottom: 10px;
line-height: 30px;
}
.container {
width: 100%;
height: calc(100% - 90px);
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 5px 10px;
}
.operation-area::-webkit-scrollbar,
#canvas::-webkit-scrollbar {
/*滚动条整体样式*/
width: 3px;
/*高宽分别对应横竖滚动条的尺寸*/
height: 3px;
}
.operation-area::-webkit-scrollbar-thumb,
#canvas::-webkit-scrollbar-thumb {
/*滚动条里面小方块*/
border-radius: 5px;
-webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
background: rgba(0, 0, 0, 0.2);
}
.operation-area::-webkit-scrollbar-track,
#canvas::-webkit-scrollbar-track {
/*滚动条里面轨道*/
-webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
border-radius: 0;
background: rgba(0, 0, 0, 0.1);
}
.operation-area {
position: relative;
padding: 0 5px;
min-width: 200px;
height: auto;
margin-left: 5px;
overflow-y: auto;
box-shadow: 2px 0 6px rgb(0 21 41 / 35%);
}
.card-title {
align-items: center;
display: flex;
flex-wrap: wrap;
padding: 5px 8px;
font-size: 1.25rem;
font-weight: 800;
letter-spacing: 0.0125em;
line-height: 2rem;
word-break: break-all;
}
.card-title::after {
content: '';
display: block;
width: 100%;
margin: 5px 0;
height: 1px;
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
}
.radio-label {
list-style: none;
margin: 0;
padding: 0;
box-sizing: border-box;
}
.li-radio-content {
padding: 10px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
background-color: #ffffff;
cursor: pointer;
}
.opacity {
opacity: 0.3;
}
.li-radio-content:hover {
background-color: rgb(245, 245, 245);
}
.li-radio {
padding: 4px 12px;
font-size: 12px;
background-color: rgb(0, 156, 224);
color: #ffffff;
letter-spacing: 3px;
border-radius: 15px;
}
.tjtyanjing,
.tjtbiyan {
margin-right: 15px;
}
.tjtlajitong1 {
color: rgb(100, 100, 100);
}
.radio-select {
background-color: #e0e0e0 !important;
}
/* 上面不太重要 */
.content-container {
flex: 1;
}
.text-selected {
position: relative;
}
.text-selected::after {
content: attr(data-attr);
display: inline-block;
position: absolute;
top: 0px;
left: 0px;
font-size: 16px;
width: auto;
white-space: nowrap;
height: 50px;
color: attr(data-attr) !important;
}
.interval {
padding: 0 20px;
line-height: 70px;
font-size: 16px;
}
.text-selected {
position: relative;
}
#select-modal {
display: none;
width: 250px;
padding: 10px;
box-sizing: border-box;
position: absolute;
overflow-y: auto;
overflow-x: hidden;
contain: content;
z-index: 9999;
background-color: #ffffff;
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
border-radius: 4px;
}
.label-show {
margin-top: 5px;
}
.next-select .next-select-inner {
border: none;
border-bottom: 1px solid #cfd2db;
margin-bottom: 10px;
}
.ant-menu-light {
border: 1px solid #cfd2db !important;
border-radius: 4px;
overflow: hidden;
border-radius: 4px;
}
.ant-menu-vertical .ant-menu-item {
margin-top: 0 !important;
padding-left: 5px !important;
margin-bottom: 0 !important;
}
.ant-menu-item {
padding: 0 !important;
}
.ant-menu-light .ant-menu-item:hover{
background-color: #e6f7ff;
}
labelText.js
/* eslint-disable no-unused-expressions */
/* eslint-disable no-loop-func */
// todo: 切换颜色
export const LabelText = (function () {
let _self;
let _el;
let initText;
let mouseDataIndex = 0;
let fixedTextStackLen = 0;
let deleteRecord = [];
function LabelText(opt) {
_el = opt.el;
_self = this;
this.config = Object.assign({}, this.default, opt);
this.color = 'tan';
this.textStack = [];
this.letters = []; // 存放被选中的文字内容
selectText();
}
LabelText.prototype = {
addText(htmlStr) {
// 清空已有的标记内容
clean();
_el.innerHTML = htmlStr;
initText = htmlStr;
},
output() {
if (window._self.letters.length) {
_self = window._self;
}
return _self.letters;
},
// 删除选中
deleteLabel(value) {
if (window._self.letters.length && window._self.textStack.length) {
_self = window._self;
const deleteIndex = _self.letters.findIndex(
(item) => item.dataIndex === mouseDataIndex
);
let deleteSpanValue = 0;
_el.style.cssText = `pointer-events: inherit;`;
const deleteValue = _self.letters[deleteIndex];
_el.innerHTML = _self.textStack.at(-1);
for (
let i = 0;
i < _el.children.length === 1 ? 2 : _el.children.length;
i++
) {
if (
_el.children[i].getAttribute('data-index') ===
mouseDataIndex
) {
deleteSpanValue = i;
deleteRecord.push({
idx: deleteSpanValue,
deleteStr: deleteValue.str,
prevMouseDataIndex: mouseDataIndex,
});
break;
}
}
// 彻底清除记录
for (let o = 0; o < deleteRecord.length; o++) {
const elChildrenIndex = Array.from(_el.children).findIndex(
(item) =>
item.getAttribute('data-index') ===
deleteRecord[o].prevMouseDataIndex
);
if (elChildrenIndex > -1) {
const outerText =
_el.children[elChildrenIndex].outerText;
_el.children[elChildrenIndex].replaceWith(outerText);
}
}
_self.letters.splice(deleteIndex, 1);
_self.textStack.splice(deleteIndex, 1);
document.getElementById('select-modal').style.display = 'none';
mouseDataIndex = 0;
window.setModalSelectContent(null);
}
},
};
// 改变颜色
function changeColor() {
const colors = document.getElementsByClassName('item-color');
const tc = document.getElementsByClassName('lt-tool-colors')[0];
for (let i = 0; i < colors.length; i++) {
colors[i].onclick = () => {
_self.color = this.dataset.color;
console.log(tc);
tc.style.backgroundColor = _self.color;
};
}
}
function selectText() {
const selObj = window.getSelection();
const range = document.createRange();
_el.onmouseup = function (e) {
if (e.target.classList.contains('text-selected')) {
console.log('在相同位置划线');
mouseDataIndex = e.target.getAttribute('data-index');
openModal(e, e.target.getAttribute('data-attr'));
return;
}
const acrossText = selObj.toString();
if (
selObj.anchorNode === selObj.focusNode &&
selObj.anchorOffset !== selObj.focusOffset
) {
if (window._self) {
_self = window._self;
}
_el.style.cssText = `pointer-events: none;`;
fixedTextStackLen++;
const span = createSpan(fixedTextStackLen);
// 从前往后
if (selObj.anchorOffset < selObj.focusOffset) {
range.setStart(selObj.anchorNode, selObj.anchorOffset);
range.setEnd(selObj.anchorNode, selObj.focusOffset);
} else {
// 从后往前
range.setStart(selObj.anchorNode, selObj.focusOffset);
range.setEnd(selObj.anchorNode, selObj.anchorOffset);
}
range.surroundContents(span);
// 确定 startIndex / endIndex (★ 核心)
const maxDataIndex = [];
let startIndex = 0;
let endIndex = 0;
for (
let i = 0;
i <
Array.from(
document.getElementsByClassName('content-container')[0]
.childNodes
).length;
i++
) {
const element =
document.getElementsByClassName('content-container')[0]
.childNodes[i];
if (element?.innerText) {
maxDataIndex.push(element.getAttribute('data-index'));
if (
span.getAttribute('data-index') ===
maxDataIndex.reduce((a, b) => (a > b ? a : b)[0])
) {
const index = i || 1;
for (let k = 0; k < index; k++) {
startIndex +=
document.getElementsByClassName(
'content-container'
)[0].childNodes[k].length ||
document.getElementsByClassName(
'content-container'
)[0].childNodes[k].innerText?.length ||
0;
endIndex +=
document.getElementsByClassName(
'content-container'
)[0].childNodes[k].length ||
document.getElementsByClassName(
'content-container'
)[0].childNodes[k].innerText?.length ||
0;
}
endIndex +=
document.getElementsByClassName(
'content-container'
)[0].childNodes[index].length ||
document.getElementsByClassName(
'content-container'
)[0].childNodes[index].innerText.length - 1;
break;
}
}
}
_self.letters.push({
str: acrossText,
tag: null,
dataIndex: span.getAttribute('data-index'),
startIndex,
endIndex,
});
_self.textStack.push(_el.innerHTML);
openModal(e);
}
};
}
// 打开弹窗
function openModal(e, selectValue) {
if (selectValue) {
window.setModalSelectContent([selectValue]);
}
document.getElementById('select-modal').style.cssText = `display:block`;
document.getElementById(
'select-modal'
).style.cssText = `display:block;top: ${e.offsetY + 120}px;left: ${
e.offsetX
}px`;
}
// 回退
function repeal() {
if (_self.textStack.length !== window._self.textStack.length) {
_self = window._self;
}
if (!_self.textStack.length) {
return;
}
if (_self.textStack.length === 1) {
_el.innerHTML = initText;
_self.letters = [];
_self.textStack = [];
_el.style.cssText = `pointer-events: inherit;`;
document.getElementById('select-modal').style.display = 'none';
return;
}
_self.letters.pop();
_self.textStack.pop();
_el.style.cssText = `pointer-events: inherit;`;
_el.innerHTML = _self.textStack[_self.textStack.length - 1];
mouseDataIndex = 0;
document.getElementById('select-modal').style.display = 'none';
window._self = _self;
}
window.repeal = repeal;
// 全清
function clean() {
_el.innerHTML = initText;
_self.textStack = [];
_self.letters = [];
deleteRecord = [];
initText;
mouseDataIndex = 0;
fixedTextStackLen = 0;
_el.style.cssText = `pointer-events: inherit;`;
document.getElementById('select-modal').style.display = 'none';
}
window.clean = clean;
// 创建span
function createSpan(index) {
const spanEle = document.createElement('span');
spanEle.className = 'text-selected';
spanEle.setAttribute('data-index', index);
spanEle.style.backgroundColor = _self.color;
return spanEle;
}
function selectLabel(v) {
const ts = document.getElementsByClassName('text-selected');
// 修改路线
if (mouseDataIndex) {
console.log('修改路线');
_self = window._self;
const changeIndex = Array.from(ts).findIndex(
(item) =>
item.getAttribute('data-index') === String(mouseDataIndex)
);
ts[changeIndex].setAttribute('data-attr', v);
const lettersAndTextStacksIndex = _self.letters.findIndex(
(item) => item.dataIndex === mouseDataIndex
);
_self.letters[lettersAndTextStacksIndex].tag = v;
_self.textStack[lettersAndTextStacksIndex] = _el.innerHTML;
_el.style.cssText = `pointer-events: inherit;`;
mouseDataIndex = document.getElementById(
'select-modal'
).style.display = 'none';
window.setModalSelectContent(null);
mouseDataIndex = 0;
window._self = _self;
return;
}
// 修改select内容
if (
window?._self &&
window?._self?.letters?.length !== _self?.letters?.length
) {
_self = {
..._self,
letters: [...window._self.letters, ..._self.letters],
textStack: [...window._self.textStack, ..._self.textStack],
};
mouseDataIndex = 0;
}
// 正常添加
const addIndex = Array.from(ts).findIndex(
(item) =>
item.getAttribute('data-index') === String(fixedTextStackLen)
);
ts[addIndex].setAttribute('data-attr', v);
_self.letters[ts.length - 1 > 0 ? ts.length - 1 : 0].tag = v;
_self.textStack[ts.length - 1 > 0 ? ts.length - 1 : 0] = _el.innerHTML;
_el.style.cssText = `pointer-events: inherit;`;
document.getElementById('select-modal').style.display = 'none';
window.setModalSelectContent(null);
mouseDataIndex = 0;
window._self = _self;
}
window.selectLabel = selectLabel;
return LabelText;
})();
http://localhost:3000/text-mark
**具体可以参考引用里的地址链接 **