react.js 原生文字下划线标注功能开发

react.js 原生文字下划线标注功能开发 (原生js封装)

github地址

效果展示:

文本下划线描述

可以输出标注的内容👆

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

**具体可以参考引用里的地址链接 **

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值