Quill富文本实现内容自定义格式format

news2025/1/10 17:37:44

在使用quill富文本编辑器时,我们输入文本会被作为类似DOM节点的数据对象存储在内部,渲染时生成相应的DOM节点。这是quill的文档模型Parchment,它提供了多种内容节点类型,如Inline \ Block \ Embed等。

quill 扩展了 Parchment 提供的的基础类型节点,并实现一些操作方法、定义了相关属性。我们可以使用quill扩展的节点再次进行自定义格式内容节点的扩展实现;当然也可以从Parchment提供的基础类型实现自定义内容节点,这需要对Parchment有足够的了解。

在源码目录quill/blots下可以看到quill扩展的节点,如BlockInlineText等。

🌒 基本使用

安装quill,现在版本是v2.0.2

$> npm i quill

创建quill实例,加载基本的样式、设置主题snow

<template>
  <div class="rich-editor">
    <div ref="root"></div>
  </div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import Quill from "quill";
// 核心样式
// import "quill/dist/quill.core.css";
// 主题
import "quill/dist/quill.snow.css";

defineOptions({
  name: "RichEditor",
});

const root = ref<HTMLElement>();
let editor: Quill | null = null;

onMounted(() => {
  editor = new Quill(root.value!, {
    modules: { toolbar: true },
    theme: "snow",
  });
});
</script>

这是基础示例的完整代码,之后的更改不再贴重复代码。

🌓 需求背景

实现在输入内容/或者{时触发弹窗选择数据后插入到富文本中,节点插入的格式为{{id.x.y}}表示选择的结构数据。但是直接插入这种格式数据展示不够直观,我们需要格式内容展示自定义样式。

这是dify的样子,有做过流程编排的同学应该熟悉。我们需要映射数据结构字段为中文,并展示在富文本中。

请添加图片描述

首先实现插入原始格式的内容节点,这个还是比较好实现的。我们通过监听输入事件,获取到输入内容判断末尾是包含了/ 或者{,然后展示弹出层提供数据选择。需要注意的是,我们在弹出层选择数据时,富文本失焦,插入选择的内容就不知道放在哪了,所以我们需要存储当前光标位置,等选择数据后插入到富文本中。

onMounted(() => {
    // ...
    // 监听值变化
    editor.on(
        "text-change",
        (_delta: Delta, _oldContents: Delta, _source: string) => {
            reference.value?.close();
            // 用户操作
            if (source !== "user") return;

            // 鼠标位置
            const range = editor!.getSelection()!;
            
            for (const op of delta.ops) {
                if (typeof op.insert == "string" && ["/", "{"].includes(op.insert)) {
                    handleShowReference();
                    // 存储当前光标位置
                    cursorIndex.value = range.index;
                }
            }
        }
  );
});

数据选择弹出的代码不必在此处展示了,调用handleShowReference()展示弹出层。弹出选择后我们点击选择数据,获取到数据后,插入到富文本中。

按照需求,我们插入的数据格式为{{id.x.y}},其中id是数据结构字段的idx.y是字段名称。

const handlePickParam = ({
  nodeId,
  name,
}: {
  nodeId: string;
  name: string;
}) => {
  reference.value?.close();

  // 插入的数据模板
  const template = `{{${nodeId}.${name}}}`;

  // 删掉之前作为触发弹出层的一个字符
  cursorIndex.value -= 1;
  editor!.deleteText(cursorIndex.value, 1);

  // 插入数据,光标位置为存储的光标位置
  editor!.insertText(cursorIndex.value, template);
};

可以看看效果,基本实现了需求的第一步,插入了指定格式的内容数据;

请添加图片描述

这里光标的位置获取有个BUG 🐞 ,在输入第一个字符时/触发选择时,获取到的光标为0,导致我们删除和插入的字符位置不对,导致数据不正确。

这里可以先处理下,光标位置不能小于0

// 删掉之前作为触发的一个字符
cursorIndex.value -= 1;
if (cursorIndex.value < 0) cursorIndex.value=0;

我们在选择数据后,富文本是失焦的,我们重新调用focus()方法,让富文本重新获取焦点。

// 移动光标到后一个位置
editor!.setSelection(
    cursorIndex.value + template.length,
    Quill.sources.SILENT
);

🌔 实现自定义格式

实现了需求要求的格数数据后,继续实现进一步的需求,将插入的格式内容展示为自定义样式。我们内容格式不需要独占一行,所以需要继承Inline类型。

quill内部使用Parchment一个文档模型定义内容节点,最小单元称之为Blots。它提供了基础的几种节点类型,包括InlineBlotBlockBlotEmbedBlot等。而quill又继承扩展了这些基础类型,实现了富文本需要的功能。我们基于quill扩展的类型Inline实现自定义的节点;当然也可以从Parchment提供的类型实现自定义节点。

quill/blots目录下找到Inline类型的节点,继承实现一些方法。因为我们需要格式内容,要改变它的DOM结构,按照我们的样式去渲染,所以覆盖重写create()方法,它是一个静态方法,返回一个DOM节点。

还有一些静态属性,我们在自定义节点时设置来标识节点类型,其他不需要的则不需要再次声明:

  • blotName 扩展节点名称
  • className 节点类名,用于样式控制
  • tagName 标签名

直接贴定义的ReferenceBlock节点代码,重写了create()方法:

import Inline from "quill/blots/inline";

class ReferenceBlock extends Inline {
  static blotName: string = "reference-block";
  static className: string = "reference-block";
  static tagName: string = "div";

  static create(value: { id: string; name: string,label:string }) {
    const root = super.create();
    // 将占位符值作为节点的属性保存
    root.dataset.id = value.id;
    root.dataset.name = value.name;
    root.setAttribute("contenteditable", "false"); // 禁用编辑

    const span = document.createElement("span");
    span.classList.add("reference-node-name");
    span.textContent = value.label;

    root.appendChild(span);

    const label = document.createElement("span");
    label.classList.add("reference-node-key");
    label.textContent = "·" + value.name;

    root.appendChild(label);

    return root;
  }
}

自定义节点需要注册到quill中才能使用;定义了我们的节点不可编辑contenteditable=false,这样嵌入的格式内容不受影响。

// 注册自定义块
Quill.register(ReferenceBlock);

注册完,就可以使用自定义节点了,对于非纯文本内容,在插入时需要使用insertEmbed()方法。可以看一下insertEmbed()方法的签名

// 插入位置、格式类型、输入值、来源
// 返回值为 Delta对象
insertEmbed(index: number, type: string, value: any, source: string = 'api'): Delta

我们修改之前的插入方法handlePickParam,使用insertEmbed()方法插入自定义节点。

const handlePickParam = ({
  nodeId,
  name,
}: {
  nodeId: string;
  name: string;
}) => {
  reference.value?.close();

  // 删掉之前作为触发的一个字符
  cursorIndex.value -= 1;
  if (cursorIndex.value < 0) cursorIndex.value = 0;
  editor!.deleteText(cursorIndex.value, 1);

  // 查找节点数据
  const node = findNode(nodeId)
  // 插入数据,光标位置为存储的光标位置
  editor!.insertEmbed(cursorIndex.value, "reference-block", {
    id: nodeId,
    name,
    label: node.label
  });
};

因为div元素自身是一个块级元素,所以需要设置样式display:inline-block更改为内联元素。

可以看到改动的地方,插入数据调用了insertEmbed(),将选择的数据传给节点。在create方法中将数据字段作为属性设置到节点上,后续会使用到;现在测试下看看展示效果以及内容DOM节点:

请添加图片描述

可以看到富文本内容展示的基本符合了我们的需求,但是生成的内容DOM结构并不对,我们是在内部分两个span放置文本内容,为了便于我们给每个部分设置不同的样式,但是实际并没有生成这两个span,而是直接将文本内容作为父节点的内容。

这是因为quill对于Inline的限制,Inline适合处理纯文本内容的格式;我们插入子节点DOM,改为继承Embed,其他的不用调整;

import Embed from "quill/blots/embed";

class ReferenceBlock extends Embed {
  // ... 代码
}

再次测试,可以看到生成的节点符合了我们的需求:

请添加图片描述

查看quill/blots/embed可以看到这部分结构的实现,也为我们解决更复杂的问题提供了一个方式。Embed创建了一个span元素,并设置了contenteditable,所以我们之前自定义元素可以移除掉属性。

const GUARD_TEXT = '\uFEFF';

class Embed extends EmbedBlot {
  constructor(scroll: ScrollBlot, node: Node) {
    super(scroll, node);

    this.contentNode = document.createElement('span');
    this.contentNode.setAttribute('contenteditable', 'false');
    Array.from(this.domNode.childNodes).forEach((childNode) => {
      this.contentNode.appendChild(childNode);
    });
    this.leftGuard = document.createTextNode(GUARD_TEXT);
    this.rightGuard = document.createTextNode(GUARD_TEXT);
    this.domNode.appendChild(this.leftGuard);
    this.domNode.appendChild(this.contentNode);
    this.domNode.appendChild(this.rightGuard);
  }
}

实现了选择数据嵌入了自定义的格式内容,此时富文本失焦状态,需要使富文本聚焦。自定义嵌入的格式默认长度为1,所以我们聚焦时,只需要传入当前记录的光标位置的下一个位置即可。

暂时没搞懂为什么要加leftGuard这种字符节点。

// 移动光标到后一个位置
editor!.setSelection(cursorIndex.value + 1, Quill.sources.API);

光标在嵌入格式内容之后没有问题,但是在输入中文时,选择了输入的中文后,光标并没有更新;输入英文光标是正常的。就算不去自动聚焦,手动聚焦也有问题,感觉是个bug 🐞

完成了需要的功能,最后追加一下自定义元素的样式即可。

🌕 收尾工作

完成了交互功能,还需要考虑信息保存,由于quill并不会保留原始的数据信息,这就需要我们手动去处理。quill处理内容的数据结构为Delta,它存储了针对内容格式的一些信息。

我们在定义ReferenceBlock时,重写了 create方法,并把一些关键属性绑定到了DOM节点上。这样只要我们可以拿到节点DOM就可以拿到数据。

比如前面我们调用insertEmbed它会返回一个Delta对象,来看看返回的内容有什么,我们将对象输出:

{
    "ops": [
        {
            "insert": {
                "reference-block": true
            }
        }
    ]
}

标识了当前操作是一个插入insert操作,并且格式应用的是我们自定义的reference-block。其他信息没有了,Blot定义了value方法返回当前节点的值,我们覆盖方法定义我们需要的数据。

class ReferenceBlock extends Embed {
  static value(node: HTMLElement) {
    return {
      id: node.dataset.id,
      name: node.dataset.name,
      label: node.dataset.label,
    };
  }
}

再次执行,输出插入的Delta对象,可以看到结构已经变成我们定义的格式了,这样在后续的处理就变了容易了。

我们要怎么获取值呢,quill提供了API getContents方法用来获取编辑器内容,并且包含了格式内容信息。我们输入一些文本、增加一些删除操作、再添加我们自定义的内容格式;来看看返回的数据是什么样的

{
    "ops": [
        {
            "insert": "你好啊 "
        },
        {
            "insert": {
                "reference-block": {
                    "id": "001",
                    "name": "name",
                    "label": "开始"
                }
            }
        },
        {
            "insert": " nic\n"
        }
    ]
}

看到数据结构就很容易处理了,我们遍历数据,判断是否插入的是reference-block格式,然后取出对象里的字段值,然后字符串拼接传递到后端你好啊 {{001.name}} nic

既然有保存,那就有回显,我们初始化拿到之前保存的内容后,需要回显到富文本中,且符合模式{{*.**.**}}的我们需要特殊处理,应用我们自定义格式。

当然,如果能说服后端直接存储Delta对象,我们就不用自己处理了。通常后端需要的字符串,将Delta对象传给他们,会造成沟通理解的成本,还是自己处理吧。

onMounted(() => {
  // 手动格式化文本
  const delta = formatContent(value.value);
  // 插入文本内容
  editor.setContents(delta)
})

formatContent()是自定义方法处理字符串,构造Delta对象并通过setContents()方法将内容插入到富文本中。如果我们富文本还应用到其他内容格式功能,比如Bold \ Italic等工具栏中的功能,那最好再加个字段保存getContents()返回的格式内容数据。

quill 可扩展支持自定义格式的能力极大的增强了富文本内容的丰富性。而且内容操作、格式都有迹可循,我们不直接操作DOM,通过Parchment模型构内容节点进行操作。

关联文章:

  1. Quill 富文本编辑器实现自定义font-size

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2257120.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MQTT知识要点

介绍 MQTT (Message Queuing Telemetry Transport) 是一种轻量级的发布/订阅消息协议&#xff0c;专为低带宽环境M2M而设计。是物联网&#xff08;IoT&#xff09;最常用的消息传递协议。 轻量高效双向通信可以扩展以连接数百万台物联网设备。可靠的消息传递&#xff08;支持…

Linux -基础指令3

博客主页&#xff1a;【夜泉_ly】 本文专栏&#xff1a;【Linux】 欢迎点赞&#x1f44d;收藏⭐关注❤️ 文章目录 &#x1f4da; 前言⏰ 时间相关&#x1f511; 概念一&#xff1a;日志 date&#x1f511; 概念二&#xff1a;时间戳 Cal &#x1f50d; 查找findwhichwhereisgr…

如何在 Redis 上配置 SSL/TLS ?

在数据泄露非常普遍的时代&#xff0c;数据安全传输对于各种规模的应用程序来说都变得至关重要。 Redis 作为一种非常流行的内存数据结构存储&#xff0c;被广泛用于缓存、消息代理和数据库。鉴于其广泛使用&#xff0c;使用SSL/TLS 加密保护 Redis 连接&#xff0c;对于保护敏…

选择大于一切!Amazon Bedrock重塑大模型领域的竞合规则

文 | 智能相对论 作者 | 陈泊丞 早些年&#xff0c;“百模大战”打得火热&#xff0c;但是随着模型发展的深入&#xff0c;人们发现如果只是争抢市场份额&#xff0c;意义并不大&#xff0c;产业链上下游需要协作共进&#xff0c;才能为市场和社会提供更优质的生成式AI服务。…

MySQL 性能优化详解

MySQL 性能优化详解 硬件升级系统配置优化调整buffer_pool数据预热降低日志的磁盘落盘 表结构设计优化SQL语句及索引优化SQL优化实战案例 MySQL性能优化我们可以从以下四个维度考虑&#xff1a;硬件升级、系统配置、表结构设计、SQL语句和索引。 从成本上来说&#xff1a;硬件升…

RK3568平台开发系列讲解(pinctrl 子系统篇)pinctrl_debug

🚀返回专栏总目录 文章目录 1. Overview2. debug信息2.1 pinctrl-devices2.2. pinctrl-handles2.3. pinctrl-handles3. debug信息3.1. 查看(pinctrl_register_pins)注册了哪些pins3.2. 查看pin groups;3.3. 查看每种functions所占用的gpio groups信息:3.4. pinconf沉淀、…

目标跟踪算法:SORT、卡尔曼滤波、匈牙利算法

目录 1 目标检测 2 卡尔曼滤波 3《从放弃到精通&#xff01;卡尔曼滤波从理论到实践》视频简单学习笔记 3.1 入门 3.2 进阶 3.2.1 状态空间表达式 3.2.2 高斯分布 3.3 放弃 3.4 精通 4 匈牙利算法 5 《【运筹学】-指派问题&#xff08;匈牙利算法&#xff09;》视…

5G Multicast/Broadcast Services(MBS) (八) MBS多播DRX

这里简单看下多播DRX的内容。 1 MBS multicast 对于MBS多播,RRC可配置 MAC entity使其具备per G-RNTI 或per G-CS-RNTI DRX 功能,从而控制 UE 对 MAC entity的G-RNTI和G-CS-RNTI 的 PDCCH 监听活动。当处于 RRC_CONNECTED 状态时,如果为 G-RNTI 或 G-CS-RNTI 配置了多播…

【JavaEE】多线程(7)

一、JUC的常见类 JUC→java.util.concurrent&#xff0c;放了和多线程相关的组件 1.1 Callable 接口 看以下从计算从1加到1000的代码&#xff1a; public class Demo {public static int sum;public static void main(String[] args) throws InterruptedException {Thread …

宝塔面板-java项目 spring 无法正常启动 java spring 宝塔 没有显示日志 问题解决方案-spring项目宝塔面板无日志

宝塔面板-java项目 spring 无法正常启动 java spring 宝塔 没有显示日志 -优雅草央千澈问题解决方案-spring项目宝塔面板无日志 问题描述 昨天安排了一个新项目的开发&#xff0c;搭建兄弟搭建完但是通信有问题&#xff0c;spring服务无法正常启动&#xff0c;于是交代后端兄…

关于一些游戏需要转区的方法

当玩非国区游戏时有时会出现乱码导致无法启动&#xff0c;此时多半需要转区来进行解决 1.下载转区软件 【转区工具】Locale Emulator 下载链接&#xff1a;Locale.Emulator.2.5.0.1.zip - 蓝奏云 用此软件可以解决大部分问题。 2.进行系统转区 首先打开控制面板选择时间与…

浅谈网络 | 应用层之云网络隔离GRE/VXLAN

目录 前言GRE 隧道技术VXLANGRE/VXLAN接入云平台 前言 之前提到&#xff0c;为云平台中的租户实现隔离时&#xff0c;常用的策略是基于 VLAN。然而&#xff0c;VLAN 只有 12 位&#xff0c;共支持 4096 个 ID&#xff0c;这在最初设计时看似足够&#xff0c;但随着云计算的快速…

【Python】批量下载抖音视频

1、代码 import os import re from concurrent.futures import ThreadPoolExecutor import requestsdef get_urls(max_cursor):# 请求头 &#xff08;页面获取&#xff09;headers {Cookie: ,Referer: ,User-Agent: }# 请求地址&#xff08;页面获取&#xff09;url # max_c…

刚入行Java,如何深入学习JVM底层原理?

对于JVM&#xff0c;我想大部分小伙伴都是要面试了才会去学&#xff0c;其余时间基本不会去看&#xff08;掐指一算&#xff0c;你们书架上面的深入理解Java虚拟机第三版应该都一层灰了吧【手动狗头】&#xff09;。但值得一说的是&#xff0c;当你工作多年之后&#xff0c;你遇…

【Redis】深入解析Redis缓存机制:全面掌握缓存更新、穿透、雪崩与击穿的终极指南

文章目录 一、Redis缓存机制概述1.1 Redis缓存的基本原理1.2 常见的Redis缓存应用场景 二、缓存更新机制2.1 缓存更新的策略2.2 示例代码&#xff1a;主动更新缓存 三、缓存穿透3.1 缓存穿透的原因3.2 缓解缓存穿透的方法3.3 示例代码&#xff1a;使用布隆过滤器 四、缓存雪崩4…

java中的数组(2)

大家好&#xff0c;我们今天继续来看java中数组这方面的知识点&#xff0c;那么话不多说&#xff0c;我们直接开始。 一.数组的使用 1.数组中元素访问 数组在内存中是一段连续的空间,空间的编号都是从0开始的,依次递增,数组可以通过下标访问其任意位置的元素. 也可以进行修改…

Qt入门7——Qt事件

目录 1. Qt事件介绍&#xff1a; 2. 事件的处理 示例1&#xff1a;鼠标进入(enterEvent)与离开事件(leaveEvent) 示例2&#xff1a;鼠标点击事件(mousePressEvent) 示例3&#xff1a;鼠标移动事件(mouseMoveEvent) 3. 按键事件 4. 定时器 5. 窗口事件 1. Qt事件介绍&a…

PyQt事件机制练习

一、思维导图 二、代码 import sysfrom PyQt6.QtTextToSpeech import QTextToSpeech from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QLineEdit from PyQt6 import uic from PyQt6.QtCore import Qt, QTimerEvent, QTimeclass MyWidget(QWidget):d…

【河南】《关于省级政务信息化建设项目支出预算标准的规定(试行)》(豫财预〔2020〕81号)-省市费用标准解读系列25

《关于省级政务信息化建设项目支出预算标准的规定(试行)》&#xff08;豫财预 〔2020〕81号&#xff09;是河南省财政厅2020年8月27日发布的信息化项目预算标准&#xff08;了解更多可直接关注我们咨询&#xff09;。该标准旨在加强河南省省级部门预算管理&#xff0c;规范省级…

oscp备考,oscp系列——Kioptix Level 3靶场

Kioptix Level 3 oscp备考&#xff0c;oscp系列——Kioptix Level 3靶场 nmap扫描 主机发现 └─# nmap -sn 192.168.80.0/24 Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-12-09 00:33 CST Nmap scan report for 192.168.80.1 Host is up (0.00014s latency). MAC…