如何实现一个微信PC端富文本输入框?

微信PC端输入框支持图片、文件、文字、Emoji四种消息类型,本篇文章就这四种类型的消息输入进行实现。我们选用HTML5新增标签属性contenteditable来实现。

contenteditable属性

contenteditable是一个全局属性,表示当前元素可编辑。

该属性拥有一下三个属性值:

  • true或空字符串: 表示元素可编辑
  • false: 表示元素不可编辑
  • plaintext-only: 表示只有原始文本可编辑,禁用富文本编辑

给div元素添加contenteditable, 将元素变为可编辑的状态

html 复制代码
<div contenteditable></div>

文字输入

当鼠标聚焦在输入框内时,会有outline展示,所以要去掉该样式

html 复制代码
<!-- index.html -->
<div contenteditable class="editor"></div>
css 复制代码
.editor:focus {
    outline:none;
}

此时文本就可以正常输入了

图片输入

图片输入分为两部分

  • 截图粘贴
  • 图片文件复制粘贴

有的浏览器是直接支持截图图片的粘贴的,不过图片文件的复制粘贴就不是我们想要的效果(会把图片粘贴成一个文件的图标俺不是展示图片)。

我们这里要自己做一个图片的粘贴展示,以兼容所有的浏览器。

这里我们使用浏览器提供的粘贴事件paste来实现

paste事件函数中存在e.clipboardData.files 属性,该属性是一个数组,当该数组大于0时,就证明用户粘贴的是文件;通过判断 filetype 属性是否为image/... 来确定粘贴的内容是否为图片。

js 复制代码
const editor = document.querySelector(".editor");

/**
 * 处理粘贴图片
 * @param {File} imageFile 图片文件
 */
function handleImage(imageFile) {
  // 创建文件读取器
  const reader = new FileReader();
  // 读取完成
  reader.onload = (e) => {
    // 创建img标签
    const img = new Image();
    img.src = e.target.result;
    editor.appendChild(img);
  };

  reader.readAsDataURL(imageFile);
}

// 添加paste事件
editor.addEventListener("paste", (e) => {
  const files = e.clipboardData.files;
  const filesLen = files.length;
  console.log("files", files);
  // 粘贴内用是否存在文件
  if (filesLen > 0) {
    //禁止默认事件
    e.preventDefault();
    for (let i = 0; i < filesLen; i++) {
      const file = files[i];
      if (file.type.indexOf("image") === 0) {
        // 粘贴的文件为图片
        handleImage(file);
        continue;
      }
      // 粘贴内容是普通文件
    }
  }
});

现在就可以粘贴图片了,无论是截图的图片还是复制的文件图片都可以粘贴到文本框内了。

现在存在的问题就是,图片尺寸太大,会按原尺寸展示图片。微信输入框展示的尺寸为最大宽高150x150

css 复制代码
.image {
    max-width: 150px;
    max-height: 150px;
}

现在图片的粘贴展示部分就是解释正常的了

细心的同学注意到了,粘贴图片后光标不会向后移动,这是因为我们禁用了粘贴的默认事件,这里需要我们手动处理光标;因为我们使用了appendChild 将图片插入到了输入框的最后面,就会出现无论光标在哪里,粘贴的图片都是出现在输入框的最后面,我们希望的是在光标所在的地方粘贴内容,接下来我们处理一下光标。

光标处理

处理光标主要以来两个WebAPI , SelectionRange

  • Selection:Selection 对象表示用户选择的文本范围或插入符号的当前位置。它代表页面中的文本选区,可能横跨多个元素。通过window.getSelection() 获取 Selection 对象。 具体可查阅MDN-Selection

  • Range:Range 接口表示一个包含节点与文本节点的一部分的文档片段。可以使用 Document.createRange 方法创建 Range。也可以用 Selection对象的 getRangeAt()方法或者 Document对象的caretRangeFromPoint() 方法获取 Range 对象。具体可查阅MDN-Range

我们使用window.getSelection获取当前位置,通过 getRangeAt()获取当前选中的范围,将选中的内容删除,再把我们自己的内容插入到当前的 Range 中,就实现了文件的输入。在这时插入的文件是选中状态的,所以要将选择范围折叠到结束位置,即在粘贴文本的后面显示光标。

js 复制代码
/**
 *  将指定节点插入到光标位置
 * @param {DOM} fileDom dom节点
 */
function insertNode(dom) {
  // 获取光标
  const selection = window.getSelection();
  // 获取选中的内容
  const range = selection.getRangeAt(0);
  // 删除选中的内容
  range.deleteContents();
  // 将节点插入范围最前面添加节点
  range.insertNode(dom);
  // 将光标移到选中范围的最后面
  selection.collapseToEnd();
}
diff 复制代码
/**
 * 处理粘贴图片
 * @param {File} imageFile 图片文件
 */
function handleImage(imageFile) {
  // 创建文件读取器
  const reader = new FileReader();
  // 读取完成
  reader.onload = (e) => {
    // 创建img标签
    const img = new Image();
    img.src = e.target.result;
-   editor.appendChild(img);
+   insertNode(img);
  };
  reader.readAsDataURL(imageFile);
}

到这里粘贴到光标位置就正常了

文件输入

文件输入分为两部分粘贴文件和选择文件

  • 粘贴文件
  • 选择文件

粘贴文件

微信是以卡片的方式展示的文件,所以我们先要准备一个类似的dom结构。

js 复制代码
/**
 * 返回文件卡片的DOM
 * @param {File} file 文件
 * @returns 返回dom结构
 */
function createFileDom(file) {
  const size = file.size / 1024;
  const templte = `
          <div class="file-container">
            <img src="./assets/PDF.png" class="image" />
            <div class="title">
              <span>${file.name || "未命名文件"}</span>
              <span>${size.toFixed(1)}K</span>
            </div>
          </div>`;
  const dom = document.createElement("span");
  dom.style = "display:flex;";
  dom.innerHTML = templte;
  return dom;
}
css 复制代码
.file-container {
    height: 75px;
    width: 405px;
    box-sizing: border-box;
    border: 1px solid rgb(208, 208, 208);
    padding: 13px;
    display: flex;
    justify-content: start;
    gap: 13px;
    background-color: #ffffff;
}
.file-container .image {
    height: 100%;
    object-fit: scale-down;
}
.file-container .title {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

按照微信样式卡片准备DOM的生成函数,如上。

将生成后的DOM加入到光标位置

js 复制代码
/**
 * 处理粘贴文件
 * @param {File} file 文件
 */
function handleFile(file) {
  insertNode(createFileDom(file));
}

在页面上就可以看到效果啦

可以看到以上的卡片还是存在问题的,

  • 光标位置不在卡片的最后面
  • Backspace 不会将整个卡片都删除掉

卡片的删除

因为我们的卡片是DOM,其父元素也就是输入框,开启了contenteditable ,因此它的该属性也继承了父元素的值,所以本身也是可以编辑的。分析到这里,就会涌现出一个方案------将卡片最外层 divcontenteditable 属性值设为 false

但是,设置了 contenteditable="false" 后就变成了不可编辑元素,光标在他周围就不显示了🥹🥹🥹

再接着考虑,思绪一下又回到了图片身上,我们能不能做一个一样的图片呢?答案是肯定的。

说到做图片我们又会有两种方案:

  • svg
  • canvas

这里就考虑使用SVG制作卡片。

SVG在这里就不多介绍了,直接开始

html 复制代码
<svg width="409" height="77" nsxml="http://www.w3.org/2000/svg">
  <!-- 矩形边框 -->
  <rect
    x="2"
    y="0"
    width="405"
    height="75"
    fill="none"
    stroke="rgb(208,208,208)"
  />
  <!-- 右侧文件图标 -->
  <polygon
    points="15 13,36 13,50 30,50 60,15 60"
    fill="rgb(156,193,233)"
    stroke="rgb(0,105,255)"
    stroke-width="2"
  />
  <!-- 图标折叠三角部分 -->
  <polygon points="36 13,36 30,50 30" fill="rgb(0,105,255)" />
  <!-- 文件类型 -->
  <text
    x="32"
    y="50"
    font-size="10"
    text-anchor="middle"
    fill="rgb(0,105,255)"
  >
    xmind
  </text>
  <!-- 文件名称 -->
  <text x="63" y="30" font-size="16">JavaScript.xmind</text>
  <!-- 文件大小 -->
  <text x="63" y="52" font-size="14">206.6K</text>
</svg>

然后就得到了一个卡片

现在我们只要将该卡片使用JS封装,在上传文件的时候调用生成图片就好了。

封装如下:

js 复制代码
/**
 * 返回文件卡片的DOM
 * @param {File} file 文件
 * @returns 返回dom结构
 */
function createFileDom(file) {
  const size = file.size / 1024;
  let extension = "未知文件";
  if (file.name.indexOf(".") >= 0) {
    const fileName = file.name.split(".");
    extension = fileName.pop();
  }
  const templte = `
  <rect
    x="2"
    y="0"
    width="405"
    height="75"
    fill="none"
    stroke="rgb(208,208,208)"
  />
  <polygon
    points="15 13,36 13,50 30,50 60,15 60"
    fill="rgb(156,193,233)"
    stroke="rgb(0,105,255)"
    stroke-width="2"
  />
  <polygon points="36 13,36 30,50 30" fill="rgb(0,105,255)" />
  <text
    x="32"
    y="50"
    font-size="10"
    text-anchor="middle"
    fill="rgb(0,105,255)"
  >
   ${extension}
  </text>
  <text x="63" y="30" font-size="16">${file.name || "未命名文件"}</text>
  <text x="63" y="52" font-size="14">${size.toFixed(1)}K</text>
`;
  const dom = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  dom.setAttribute("width", "409");
  dom.setAttribute("height", "77");
  dom.innerHTML = templte;
  const svg = new XMLSerializer().serializeToString(dom);
  const blob = new Blob([svg], {
    type: "image/svg+xml;charset=utf-8",
  });
  const imageUrl = URL.createObjectURL(blob);
  const img = new Image();
  img.src = imageUrl;
  return img;
}

到这里我们的文件粘贴就完成了,来展示

上传文件

上传文件又包含两部分:图片和普通文件

我们需要在这里做一个判断,如果是图片需要将其内容渲染出来,文件用卡片显示。

html 复制代码
<div class="controls">
    <img src="./assets/emoji.png" alt="表情" title="表情" />
    <img
      src="./assets/file.png"
      alt="发送文件"
      title="发送文件"
      class="file"
    />
</div>
js 复制代码
const fileDom = document.querySelector(".file");

fileDom.addEventListener("click", () => {
  const input = document.createElement("input");
  input.setAttribute("type", "file");
  input.setAttribute("multiple", "true");
  input.addEventListener("change", () => {
    const files = input.files;
    for (let i = 0; i < files.length; i++) {
      const file = files[i];
      fileTypeCheck(file);
    }
  });
  input.click();
});

以上代码,在输入框上方添加了一个文件的icon , 给这个icon 添加点击事件,选择文件并展示。

到这里文件上传就做好了,但是还是存在问题的。

输入框的光标,在鼠标点击页面其他地方的时候会从输入框移出,如果在这时选择文件不会添加到输入框中。

所以,我们在点击文件上传的时候需要叫光标聚焦到输入框。

diff 复制代码
const fileDom = document.querySelector(".file");

fileDom.addEventListener("click", () => {
+  // 光标聚焦到输入框
+  editor.focus();
  const input = document.createElement("input");
  input.setAttribute("type", "file");
  input.setAttribute("multiple", "true");
  input.addEventListener("change", () => {
    const files = input.files;
    for (let i = 0; i < files.length; i++) {
      const file = files[i];
      fileTypeCheck(file);
    }
  });
  input.click();
});

到这里文件上传就完美了。

总结

以上的源码已经上传到了Github, 想要源码的小伙伴自己去拉。代码读取文件那部分可以使用Promise 进行优化。最后,欢迎大佬批评指正。

相关推荐
_.Switch36 分钟前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光40 分钟前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   40 分钟前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   41 分钟前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web1 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常1 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇2 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr2 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho3 小时前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常4 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js