textarea要粘贴图片,这可咋整?

1,305 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情

先说明一下项目需求,PC做了一个客服系统,聊天框用textare进行文本输入,但是后续需要对接微信公众号,需要发送微信表情,以及复制粘贴图片等功能。

所以,尽管我很不愿意,还是只能用contenteditable来实现一个简单的富文本编辑器。(基于vue3实现)

  <div
    class="rich-textarea-content"
    v-bind="$attrs"
    ref="textDom"
    id="rsvp-textarea"
    contenteditable
    :disabled="disabled"
    :placeholder="placeholder"
    @keyup.stop="keyup"
    @keydown.stop="keydown"
    @blur="$emit('blur')"
  ></div>

先看看样子

image.png

但是实现过程中,还是有很多的坑。

1.placeholder提示以及disabled

disabled的实现比较简单,只需要根据disabled状态更改contenteditable的值就行。

至于样式,可以通过属性选择器来实现。

[disabled="true"] {
    background-color: #f5f7fa;
    cursor: not-allowed;
 }
 
 [disabled="false"] {
    background-color: #fff;
    cursor: default;
 }

placeholder根据传入参数,动态改变,这个一直没思路。

后来发通过attr,给伪元素的content赋值,来实现。(但是..., 这个在ie中,光标会有点不正常,话说ie官方都不维护了,还兼容它干啥)

  &:empty {
    &::before {
      content: attr(placeholder);
      color: gray;
    }
  }

2.设置初始值并将光标移到最后

设置初始值可以使用 document.execCommand("insertHTML", true, ''),也可以直接设置innerText。

注意:(execCommand方法,需要编辑框处于focus状态下才能使用,如果需要点击操作栏,而又不想编辑框失去焦点,可以拦截mousedown事件)

移动光标到最后

    moveEnd() {
      var selection = window.getSelection();
      var range: any = document.createRange();
      range.selectNodeContents(this.$refs.textDom as HTMLDivElement);
      range.collapse(false);
      selection && selection.removeAllRanges();
      selection && selection.addRange(range);
    },

3.监听粘贴事件

      (this.$refs.textDom as HTMLDivElement).addEventListener(
        "paste",
        async (e) => {
          e.preventDefault();
          e.stopPropagation();

          let items = e.clipboardData?.items || [];
          for (let i = 0; i < items?.length; i++) {
            const item = items[i];
            if (item.kind === "string" && item.type === "text/plain") {
              item.getAsString(function (str) {
                document.execCommand("insertHTML", true, str);
              });
            } else if (
              item.kind === "file" &&
              item.type.indexOf("image") !== -1 &&
              !this.isAccount
            ) {
              const pasteFile = item.getAsFile();
              if (pasteFile && pasteFile.size < 4 * 1024 * 1024) {
                const form = new FormData();
                form.append("file", pasteFile);
                // 可以上传接口,也可以直接转base64
                 document.execCommand(
                    "insertHTML",
                    true,
                    `<img src=${url} style="max-width: 240px; max-height: 240px;vertical-align: bottom"></img>`
                  );
              }
            }
          }
        }
      );

4.文本获取

发送的时候,如果有图片,需要以图片位置为分割,分为多条消息发送。

image.png

比如这样的一张图,需要分成4条消息发送 1.表情你好 2.图片1 3.呵呵 4.图片2

所以获取内容不能单纯的获取innerText。

实现方式为: 通过遍历childNodes判断分割位置,以及内容。

      const dom = this.$refs.textDom as HTMLDivElement;
      let m = "";
      let arr: any = [];
      for (let i = 0; i < dom.childNodes.length; i++) {
        // console.log("dom", dom[i]);
        if (dom.childNodes[i].nodeName === "IMG") {
          const c = dom.childNodes[i] as HTMLImageElement;
          // 说明一下,这里加了个表情的判断,因为输入框的表情其实也是图片,所以需要转成微信能识别的编码
          if (c.className.indexOf("qqface") !== -1) {
            // @ts-ignore
            const index = c.attributes.data.value;
            m += Object.keys(wexinEmo)[index];
          } else {
            if (m) {
              arr.push({
                type: "text",
                content: m,
              });
            }
            m = "";
            arr.push({
              type: "image",
              content: (dom.childNodes[i] as any).currentSrc,
            });
          }
        } else {
          m +=
            dom.childNodes[i].nodeName === "#text"
              ? (dom.childNodes[i] as any).data
              : (dom.childNodes[i] as any).innerText;
        }
      }
      if (m) {
        arr.push({
          type: "text",
          content: m,
        });
      }