thumbnail

介绍

富文本编辑器是现代网络应用程序中常见的一种功能,它使用户能够以类似于字处理软件的方式编辑文本内容。虽然市面上有很多成熟的富文本编辑器库和插件可供选择,但是利用HTML5的contenteditable属性,我们也可以轻松地创建一个简易的富文本编辑器,而无需依赖第三方库或插件。

这是mdn对于contenteditable属性的介绍 —— mdn web doc

当然,本文不是介绍如何开发一款富文本编辑器的,而是介绍 Salary主题 的表情图片插入是如何实现的。

实现表情图片插入

Salary主题 的表情图片插入,是我当初开发主题时的一个特殊的想法。市面上大部分的博客主题都是使用emoji,为了能有更丰富的表情,参考了 掘金 的评论组件,进而实现了 Salary主题 的表情插入。

起步

首先,我们需要一个具有contenteditable属性的div元素作为编辑器的容器,以及一个用于显示表情图片的面板。

<div contenteditable="true" spellcheck="false" />

编码

Salary 是基于 vueJS 开发的,所以我们在这里把表情面板提取成一个组件,新建一个Emoji.vue,这里直接贴出源码:

<script lang='ts' setup>
interface Emoji<T = string> {
  name?: T
  url: string
}
const emits = defineEmits(['emojiClick'])
// 定义表情释义
const emojiNames: string[] = [
  '微笑', '呲牙', '色', '发呆', '可怜', '流泪', '害羞', '闭嘴', '睡', '吃瓜群众', '尴尬', '发怒', '调皮', '撇嘴', '思考', '不失礼貌的微笑', '奸笑', '抓狂',
  '吐', '偷笑', '愉快', '白眼', '傲慢', '困', '灵光一现', '流汗', '憨笑', '捂脸', '奋斗', '咒骂', '疑问', '嘘', '晕', '衰', '敲打',
  '再见', '擦汗', '抠鼻', '泣不成声', '坏笑', '左哼哼', '右哼哼', '打哈欠', '鄙视', '委屈', '快哭了', '摸头', '阴险', '亲亲', '机智', '得意', '大金牙', '拥抱',
  '大笑', '送心', '震惊', '拽酷', '尬笑', '大哭', '笑哭', '做鬼脸', '红脸', '鼓掌', '恐惧', '斜眼', '嘿哈', '惊讶', '绝望的凝视', '囧', '皱眉', '耶', '石化',
  '我想静静', '吐血', '互粉', '互相关注', '加好友', '强', '钱', '飞吻', '打脸', '惊恐', '悠闲', '泪奔', '舔屏', '紫薇别走', '听歌', '难过', '生病', '绿帽子', '如花',
  '惊喜', '吐彩虹', '吐舌头', '无辜呆', '看', '白眼的狗', '黑脸', '猪头', '熊吉', '不看', '玫瑰', '凋谢', '嘴唇', '爱心', '心碎', '赞', '弱', '握手', 'ok', '谢谢',
  '比心', '碰拳', '击掌', '左', '右', '力量', '胜利', '抱拳', '勾引', '拳头', '庆祝', '礼物', '红包', '18禁', '去污粉', '666', '给力', 'v5', '菜刀', '炸弹', '便便',
  '月亮', '太阳', '发财', '黄瓜', '西瓜', '啤酒', '咖啡', '蛋糕']
// 导入所有表情图片Url(.png格式)并存储在emojiList对象中:
const emojiList = import.meta.globEager('@/assets/emojis/*.png')

// 对所有表情进行排序、命名,并将其返回作为计算长度的值:
const emojis = computed(() => {
  const emojiArray: Emoji[] = []

  // 遍历数组并推入 emojiArray 中
  Object.entries(emojiList).forEach(([_name, value]) => {
    emojiArray.push({
      url: (value as any).default,
    })
  })

  // 根据图片 Url 的长度排序,并为每个表情添加正确的名称
  emojiArray.sort((a, b) => a.url.length - b.url.length).map((item, index) => item.name = emojiNames[index])
  return emojiArray
})
const show_emoji_popover = shallowRef(false)
// 派发事件
function handleEmojiClick(emoji) {
  emits('emojiClick', emoji)
}
</script>

<template>
  <n-popover v-model:popup-visible="show_emoji_popover" class="somnia-tooltip" trigger="click" raw
    placement="bottom-start">
    <template #trigger>
      <button class="lite" max-w-10em truncate text-base h-2rem border-none rounded-lg font-500 transition-base px-0.65em
        cursor-pointer>
        <i class="fa-regular fa-face-grin-squint-tears" mr-1 />表情
      </button>
    </template>
    <div class="title">
      表情
    </div>
    <n-grid cols="6 s:3 m:6 l:9 xl:9 2xl:9" class="content emoji-wrapper" h-270px overflow-y-scroll p-0.5em
      overflow-scroll responsive="screen" :x-gap="5" :y-gap="5">
      <template v-for="emoji in emojis" :key="emoji.name">
        <n-gi @click="handleEmojiClick(emoji)">
          <img class="emoji-item" :src="emoji.url" w-36px h-36px cursor-pointer rounded flex p-1 transition items-center
            justify-center>
        </n-gi>
      </template>
    </n-grid>
  </n-popover>
</template>

<style scoped lang="scss">
.emoji-item {
  border: 1px solid transparent;

  &:hover {
    filter: drop-shadow(0 0 10px var(--primary-color));
    border-color: var(--primary-color);
    transition: 0s;
  }
}

button {
  font-size: 14px;
  color: var(--primary-color);
  border: 1px solid var(--primary-opacity-30);
  background: var(--primary-opacity-10);
  transition: .35s;

  &:hover {
    border-color: var(--primary-color);
  }
}
</style>

在使用时,我们需要给div增加一些事件,同时需要引入Emoji.vue组件

<div ref="replyInput" 
  contenteditable="true" 
  disabled="disabled" 
  spellcheck="false"
  :placeholder="placeholder" 
  class="reply-input replyInput" 
  @keydown="keyDown" 
  @paste="handlePaste" />

<Emoji @emojiClick="emojiClick" />

实现插入

重点来了,在点击表情的时候,如何实现插入html标签呢?

最初我是想通过 window.getSelection() 来实现插入图片到指定的光标位置的,但是使用过后发现总会有问题。

查阅资料之后,最终选择使用 document.execCommand 来实现。

MDN文档

当一个 HTML 文档切换到设计模式时,document暴露 execCommand 方法,该方法允许运行命令来操纵可编辑内容区域的元素。

function emojiClick(emoji) {
  const img = `<img src="${emoji.url}" alt="${emoji.name}" class="emoji" draggable="false">`
  document.execCommand('insertHTML', false, img)
}

监听粘贴事件,粘贴的时候去除html格式,只粘贴纯文本。

function handlePaste(e) {
  e.preventDefault()
  // 粘贴纯文本
  const text = e.clipboardData.getData('text/plain')
  document.execCommand('insertHTML', false, text)
}

使用 document.execCommand 可以完美的在指定的光标处插入html片段进行渲染。

总结

关于 contenteditable 属性的使用还远远不止这些,这里只是向大家公布了 Salary主题 中表情插入的实现方法。

你也可以利用这个属性来创建一个富文本编辑器,亦或是 nvpress 的文章编辑。

希望本文能够帮助到你,如果有任何疑问或建议,欢迎在评论区留言交流!