缘由

故事从我在coding的时分,忽然又被拉入了一个群开端….

很快产品司理就开端了需求评定,及其专业的术语描述着整个需求的背景以及完成后带来的收益,可是这我关怀吗,把会议挂在一旁继续修昨天写的bug,抽看看了眼技能文档:好像不难,无非便是增删查改,回头在看文档吧,很快到了提问环节。产品:前端还有问题吗?我:没有,没有。

很快需求上了排期,打开文档仔细阅读一番,其他需求看着都挺简单,忽然一个看似正常又不太常见的输入框出现在需求上,原来是要完成一个可刺进自定义标签的输入框,后续还需要进行展示,关于标签是动态数据。翻阅公司组件库,我当场就不乐意了赶紧找来产品可否进行需求降级,把可刺进标签的输入框变成全刺进标签的输入框,这不就简单多了,能够直接用InputTag:

实现一个可插入标签的输入框

明显这不符合产品的要求,咱们来看一下终究做出来的作用:

实现一个可插入标签的输入框

点击刺进因子能够刺进标签内容,这儿我用写死的文本代替了,同时支撑恣意方位文本的输入,关于标签支撑删去操作。

预先常识

contenteditable

要完成这个作用我想便是经过修改元素的可修改性,使之能够修改,html就给咱们提供了这么个特点,要知道在一个一般元素内刺进一个标签要比在textarea中要简单多了。

-webkit-user-modify

这是个css特点,经过对应特点的设置也能够达到同样的作用。

read-only: 默认值,元素只读,不可修改;

read-write: 能够修改,支撑富文本;

read-write-plaintext-only: 能够修改,不支撑富文本;

write-only: 使元素仅用于修改(几乎没有浏览器支撑)

Range目标

由于产品还需要对应的刺进修改能够根据光标的方位来定,所以咱们还需要这么一个目标。

能够用Document目标的Document.createRange办法创立 Range,也能够用Selection目标的getRangeAt办法获取 Range。另外,还能够经过Document目标的结构函数Range()来得到 Range。我在完成的时分经过的是selection目标(表示用户选择的文本范围或刺进符号的当前方位)的办法创立的,经过监听selectionchange事情来响应式的更新我的range,这样我就能够定位到光标的方位,那么关于标签插在哪的问题就处理了。

代码完成

这儿我用的是react完成的,我先把所有代码粘贴上来,下面在进行解说

import React, { useEffect, useRef, useState } from 'react'
import { Button } from "@ecom/auxo";
import "./index.css";
type Props = {
  callBack: Function;
}
export default function App({
  callBack
}: Props) {
  const handleInput = () => {
    callBack((inputTag as any).current.innerHTML);
  }
  // 鼠标焦点目标
  const [Range, saveRange] = useState<Range>();
  const getGuid=()=> {
    // 生成随机ID
    return `r${new Date().getTime()}d${Math.ceil(Math.random() * 1000)}`;
  }
  const [contentId,setContentId] = useState(`content${getGuid()}`)
  const selecthandler = () => {
    // 监听选定文本的移动
    let sel = window.getSelection();
    let range = sel ? sel.rangeCount > 0 ? sel?.getRangeAt(0) : null : null;
    if (range && range.commonAncestorContainer.ownerDocument?.activeElement?.id === contentId) {
      saveRange(range);
    }
  }
  useEffect(() => {
    document.addEventListener('selectionchange', selecthandler);
    return () => {
      document.removeEventListener('selectionchange', selecthandler);
    }
  }, [])
  const inputTag = useRef<HTMLDivElement>(null);
  const insertNode = (node: Element) => {
    // 删掉选中的内容(如有)
    Range && Range.deleteContents();
    // 刺进链接
    Range && Range.insertNode(node);
    // 更新内容  
    callBack((inputTag as any).current.innerHTML);
  }
  // 增加标签
  const addTag = (text: string) => {
    let node = document.createElement('wise');
    node.innerText = text;
    const cancelNode = document.createElement("span");
    cancelNode.innerText=("✕");
    cancelNode.style.color="black";
    cancelNode.onclick=(even)=>{
      inputTag.current?.removeChild((even.target as any).parentElement);
      callBack((inputTag as any).current.innerHTML);
    }
    node.append(cancelNode);
    insertNode(node);
    setDisabled(true);
  }
  // 增加因子
  const addfactor = () => {
    addTag("<div>test</div>");
  }
  // 是否能够增加因子
  const [disabled,setDisabled]=useState(true);
  return (
    <div
    className='tagTextArea'
    >
      <div
        contentEditable="true"
        className='myTextArea'
        onInput={handleInput}
        ref={inputTag}
        id={contentId}
        onFocus={()=>{
          setDisabled(false);
          console.log("focus")
        }}
      >
      </div>
      <Button disabled={disabled} onClick={addfactor} type='dashed'>刺进因子</Button>
    </div>
  )
} 

onInput事情监听

const handleInput = () => { callBack((inputTag as any).current.innerHTML); }

在这儿咱们经过父组件传递下来的回调函数把终究输入框的内容传递出去,所以每逢输入框内容改动的时分就会出发,保证数据的实时性。

getGuid

const getGuid=()=> {
// 生成随机ID 
return `r${new Date().getTime()}d${Math.ceil(Math.random() * 1000)}`;
}

这儿咱们凭借时刻搓来回来一个随机id用于确认每个输入框目标,由于咱们的页面或许不止一个输入框,假如存在多个输入框那么在定位光标的时分,区分按钮对应的输入框就要用到这个随机的ID值。

副作用

useEffect(() => { document.addEventListener('selectionchange', selecthandler);
 return () => { document.removeEventListener('selectionchange', 
 selecthandler); } }, [])

在程序一开端时,咱们就要监听光标的方位改动,为了定位标签刺进的方位,在销毁的时分取消监听。

selecthandler事情

  const selecthandler = () => {
    // 监听选定文本的移动
    let sel = window.getSelection();
    let range = sel ? sel.rangeCount > 0 ? sel?.getRangeAt(0) : null : null;
    if (range && range.commonAncestorContainer.ownerDocument?.activeElement?.id === contentId) {
      saveRange(range);
    }
  }

这儿咱们是处理光标改动后的方位存储,这儿不能直接存储,由于前面说了咱们或许会有多个输入框出现在一个页面中,所以咱们要用仅有的ID进行判断,只有是当前输入框的方位改动了咱们才进行存储。

addfactor->addTag->insertNode

  const insertNode = (node: Element) => {
    // 删掉选中的内容(如有)
    Range && Range.deleteContents();
    // 刺进链接
    Range && Range.insertNode(node);
    // 更新内容  
    callBack((inputTag as any).current.innerHTML);
  }
  // 增加标签
  const addTag = (text: string) => {
    let node = document.createElement('wise');
    node.innerText = text;
    const cancelNode = document.createElement("span");
    cancelNode.innerText=("✕");
    cancelNode.style.color="black";
    cancelNode.onclick=(even)=>{
      inputTag.current?.removeChild((even.target as any).parentElement);
      callBack((inputTag as any).current.innerHTML);
    }
    node.append(cancelNode);
    insertNode(node);
    setDisabled(true);
  }
  // 增加因子
  const addfactor = () => {
    addTag("<div>test</div>");
  }

在增加因子按钮这儿我是写死数据,正常应该经过后端接口获取来动态刺进,至于标签那还需要什么款式或者功用展示,咱们都能够经过append来进行追加,我这儿是加入了删去标签的功用,完成也是比较简单,监听点击事情,经过removeChild办法进行删去就能够了。

css部分

.myTextArea{
    -webkit-user-modify: read-write-plaintext-only !important;
    border:1px solid #ccc;
    overflow: hidden;
    box-sizing: border-box;
    word-break: break-word;
    height: 200px;
    width: 200px;
}
wise {
    background-color: #f0f6fe;
    color: #5387f7;
    padding: 0 1px;
    border-radius: 2px;
    /* white-space: nowrap; */
    cursor: default;
    -webkit-user-modify: read-only !important;
  }

这儿用到了咱们的主角-webkit-user-modify:,经过他的设置使得咱们的标签能够修改,这儿还有一个小技巧便是wise标签的设置,这个便是咱们的标签名字,这样咱们就能够经过标签款式一致设置咱们的标签,同时经过-webkit-user-modify: read-only !important;的设置,在删去的时分也是一整个的删去。

最后

至此咱们的功用已经完成了,这儿只是一个demo提供一种思路,里边还有许多anyscript,真要开发这样一个组件还需要考虑许多的兼容性