项目推荐—文本标注 poplar-annotation用法及相关问题解决

一、项目介绍

poplar-annotation是一款基于js的文本标注类库,支持react、vue框架,且与其框架版本没有关系,相对于react-text-annotations库,需要依赖react 17以上版本,poplar-annotation更有优势。

1720429323665.png
1720430319670.png
图片.png

二、环境安装

npm i poplar-annotation

如果使用的是yarn:

yarn add poplar-annotation

三、基本语法

1.官网示例:

1.1 创建

import {Annotator} from 'poplar-annotation'
/**
  * Create an Annotator object
  * @param data          can be JSON or string
  * @param htmlElement   the html element to bind to
  * @param config        config object
  */
new Annotator(data: string, htmlElement: HTMLElement, config?: Object)

1.2 官网中文手册

建议!!!先看官方文档!!!

2.个人项目搭建源码及解析

首先我将所有的dom绑定、鼠标监听选词、连线方法等封装在useCreateAnnotato()里面,数据与操作分离。

2.1useCreateAnnotato.js文件

export const useCreateAnnotator = (data, config) => {
  const annRef = useRef()
  /**选择标签类型弹框 */
  const [show, setShow] = useState(false)
  /**选择连线类型弹框 */
  const [visible,setVisible]=useState(false)
  /**选词的起始坐标,结尾坐标。连线的首实体id,尾实体id*/
  const [optionInfo, setOptionInfo] = useState({ startIndex: -1, endIndex: -1,firstId:-1, secondId:-1})
  /**创建Annotator对象 */
  const refHtml = useCallback(node => {
  /**创建前判断,避免重新创建 */
    if (node && !annRef.current) {annRef.current = new Annotator(data, node, config)}
  }, [])
  /**Events 事件操作*/
  const textSelectedOp = () => {if(!annRef.current)return
    /**选词 */
    annRef.current.on("textSelected", (startIndex, endIndex) => {
        setOptionInfo(pre=>{
            const cpPre={...pre}
            cpPre.startIndex=startIndex
            cpPre.endIndex=endIndex
            return cpPre
        }
      setShow(true)
    });
    /**删除选词 */
    annRef.current.on('labelRightClicked', (id, x, y) => {
      annRef.current.applyAction(Action.Label.Delete(id))
    });
    /**连线 */
    annRef.current.on('twoLabelsClicked',(firstId,secondId)=>{
      setOptionInfo(pre=>{
        const cpPre={...pre}
        cpPre.firstId=firstId
        cpPre.secondId=secondId
        return cpPre
      })
      setVisible(true)
    })
    /**删除连线 */
    annRef.current.on('connectionRightClicked',(id,events)=>{
      annRef.current.applyAction(Action.Connection.Delete(id))
    })
  }
  /**选中标签类型 */
  const addLabel = (categoryId, startIndex, endIndex) => {
    if(!annRef.current)return
    annRef.current.applyAction(Action.Label.Create(categoryId, startIndex, endIndex))
  }
  /**选中连线类型 */
  const addConnect=(categoryId, fromId, toId)=>{
    if(!annRef.current)return
    annRef.current.applyAction(Action.Connection.Create(categoryId, fromId, toId))
  }
  /**移除Annotator实例 */
  const remove=()=>{
      console.log("remove")
      if(annRef.current?.remove){
        console.log("sucess");
        annRef.current.remove()
        annRef.current = null
      }
  return {
    optionInfo,
    textSelectedOp,
    refHtml,
    show,
    setShow,
    addLabel,
    addConnect,
    visible,
    setVisible,
    remove
    }
  }
}

2.2App.js文件

function App() {
/**标签类型弹框信息 */
  const [labelCategorieId, setLabelCategorieId] = useState(0)
  const onChangeLabel = (e) => {
    console.log('radio checked', e.target.value);
    setLabelCategorieId(e.target.value);
  };
const onLabelOk=()=>{
  addLabel(labelCategorieId,optionInfo.startIndex,optionInfo.endIndex)
  setShow(false)
}
/**连线类型弹框信息*/
  const [connectionCategorieId, setConnectionCategorieId] = useState(0)
  const onChangeConn = (e) => {
    console.log('radio checked', e.target.value);
    setConnectionCategorieId(e.target.value);
  };
  const onConnOk=()=>{
    addConnect(connectionCategorieId,optionInfo.firstId,optionInfo.secondId)
    setVisible(false)
  }
const {
  optionInfo,
  textSelectedOp,
  refHtml,
  show,
  setShow,
  addLabel,
  addConnect,
  visible,
  setVisible,remove
  }=useCreateAnnotator(data)
  
  useEffect(() => {
  //挂载
    textSelectedOp()//卸载
    return ()=>{
        remove && remove()
   }
  }, [])

  return (
    <AppContain>
     <Wrapper
      ref={refHtml}
    >
    </Wrapper>
    <Modal title={'标签类型'} open={show} onCancel={()=>{setShow(false)}} onOk={onLabelOk}>
      {data?.labelCategories?.map(e=>{
        return(
        <>
          <Radio.Group value={labelCategorieId} onChange={onChangeLabel}>
             <Radio value={e.id} key={e.id}>{e.text}</Radio>
          </Radio.Group></>)})}
      </Modal>
      <Modal title={'连线类型'} open={visible} onCancel={()=>{setVisible(false)}} onOk={onConnOk}>
      {data?.connectionCategories?.map(e=>{
        return(
        <>
          <Radio.Group value={connectionCategorieId} onChange={onChangeConn}>
             <Radio value={e.id} key={e.id}>{e.text}</Radio>
          </Radio.Group></>)})}
      </Modal>
   </AppContain>
  )
}

/** CSS */
const Wrapper = styled.div`
& > svg {
  width: 100%;
}
/* 内容 */
.poplar-annotation-content {
  font-family: 'PingFang SC', serif;
  font-size: 20px;
}
/* Label */
.poplar-annotation-label {
  font-family: 'PingFang SC', serif;
  font-size: 14px;
}
/* Connection */
.poplar-annotation-connection {
  font-family: 'PingFang SC', serif;
  font-size: 12px;
}
.poplar-annotation-connection-line {
  stroke: green; /* 有效 */
}
.poplar-annotation-connection-line.hover-from {
  stroke: red;
}
.poplar-annotation-connection-line.hover-to {
  stroke: blue;
}
.poplar-annotation-connection-line.hover {
  stroke: yellow;
}
`
export default App

2.3data.js JOSN数据

export const data = {
    content:'北冥有鱼,其名为鲲。鲲之大,不知其几千里也;化而为鸟,其名为鹏。鹏之背,不知其几千里也;怒而飞,其翼若垂天之云。是鸟也,海运则将徙于南冥。南冥者,天池也。\n《齐谐》者,志怪者也。《谐》之言曰:“鹏之徙于南冥也,水击三千里,抟扶摇而上者九万里,去以六月息者也。”野马也,尘埃也,生物之以息相吹也。天之苍苍,其正色邪?其远而无所至极邪?其视下也,亦若是则已矣。且夫水之积也不厚,则其负大舟也无力。覆杯水于坳堂之上,则芥为之舟,置杯焉则胶,水浅而舟大也',
    labelCategories: [
      {
        id: 0,
        text: '名词',
        color: '#eac0a2',
        borderColor: '#a38671'
      },
      {
        id: 1,
        text: '动词',
        color: '#619dff',
        borderColor: '#436db2'
      },
      {
        id: 2,
        text: '形容词',
        color: '#9d61ff',
        borderColor: '#6d43b2'
      },
      {
        id: 3,
        text: '副词',
        color: '#ff9d61',
        borderColor: '#b26d43'
      }
    ],
    labels: [
      {
        id: 0,
        categoryId: 0,
        startIndex: 0,
        endIndex: 2
      },
      { 
        id: 1,
        categoryId: 0,
        startIndex: 3,
        endIndex: 4
      },
      {
        id: 6,
        categoryId: 0,
        startIndex: 32,
        endIndex: 33
      },
      {
        id: 7,
        categoryId: 1,
        startIndex: 46,
        endIndex: 47
      },
      {
        id: 9,
        categoryId: 1,
        startIndex: 64,
        endIndex: 65
      },
      {
        id: 10,
        categoryId: 0,
        startIndex: 217,
        endIndex: 218
      },
      {
        id: 11,
        categoryId: 0,
        startIndex: 220,
        endIndex: 221
      },
      {
        id: 12,
        categoryId: 2,
        startIndex: 218,
        endIndex: 219
      },
      {
        id: 13,
        categoryId: 2,
        startIndex: 221,
        endIndex: 222
      },
      {
        id: 14,
        categoryId: 0,
        startIndex: 79,
        endIndex: 81
      },
      {
        id: 15,
        categoryId: 2,
        startIndex: 84,
        endIndex: 86
      }
    ],
    connectionCategories: [
      {
        id: 0,
        text: '修饰'
      },
      {
        id: 1,
        text: '限定'
      },
      {
        id: 2,
        text: '是...的动作'
      }
    ],
    connections: [
      {
        id: 0,
        categoryId: 2,
        fromId: 7,
        toId: 6
      },
      {
        id: 3,
        categoryId: 2,
        fromId: 9,
        toId: 6
      }
    ]
  }

四、项目存在问题及相关优化

1.Annotator对象重复创建问题

解决思路:创建之前先对 annRef.current进行判断是否已经存在

const refHtml = useCallback(node => {
    if (node && !annRef.current) {
        annRef.current = new Annotator(data, node, config)
    }
  }, [])

2.跨行标注,标签及文字显示不出问题

跨行标注选择,文字会自动在一行,svg的长度超出屏幕的长度,文字就会隐藏;

图片.png
删除也恢复不了文字显示

图片.png
根据官网描述,他们在内部进行了换行标注的限制的,也就是说不能跨行标注,但是有些情况还是能触发。

图片.png

解决思路:在选词前进行判断当前选词的长度是否小于整行的最大长度

/**选词 */
annRef.current.on("textSelected", (startIndex, endIndex) => {
    if((endIndex-startIndex)*20 < annRef.current.view.lineMaxWidth){
        setOptionInfo(pre=>{
            const cpPre={...pre}
            cpPre.startIndex=startIndex
            cpPre.endIndex=endIndex
            return cpPre
          })  
        setShow(true)
      }else{
          return message.info('选择的文本超出长度限制')
      }
    });
  • annRef.current.view.lineMaxWidth获取当前整行的最大长度
  • (endIndex-startIndex)*20选词的长度,这里暂定每个词长度为20,具体每个词的长度可以根据源码去研究。

图片.png

3.主题颜色背景改变

该文本标注的整个样式,它已经脱离了CSS,是一个SVG,所以直接用CSS样式去修改,是没有效果的。

  • 针对svg的颜色设置用fill属性填充

图片.png

& > svg {
  width: 100%;
  fill: #fff;
  background-color: #232323;
}
  • 对连线背景的矩形颜色设置

图片.png
图片.png
rect节点和text节点是兄弟关系,rect上无法直接添加类,text上有类

解决思路::has是一个 CSS 伪类选择器。提供了一种针对引用元素选择父元素或者先前的兄弟元素的方法。

& > svg {
  width: 100%;
  fill: #fff;
  background-color: #232323;
  rect:has(+ .poplar-annotation-connection){
    fill: #6c6c6c;
  }
}

图片.png

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值