当前位置: 首页 > 工具软件 > ProseMirror > 使用案例 >

ProseMirror分享会 —— 富文本基础知识

汪阿苏
2023-12-01

一、原生富文本

        HTML中,任何元素都可以被编辑。现代浏览器为我们提供了许多 API 使我们可以在web浏览器上进行富文本编辑功能。 

1.1 编辑状态

                想要使元素切换到编辑模式,我们只需要在 html 标签上设置 "contentEditable" 属性值为 true 即可。该枚举属性的值存在以下三种:

  • "true" 表明该元素可编辑。
  • "false" 表明该元素不可编”辑。 
  • "inherit" 表明该元素继承了其父元素的可编辑状态。

        下面来看一个简单的例子,我们只需要为最外层的元素添加该属性,用户就可以编辑其内容了。

<div class="container" contenteditable="true">
    我是顶部内容
    <div class="item">我是第一段</div>
    <div class="item">我是第二段</div>
    <div class="item">我是第三段</div>
    我是末尾内容
</div>

        打开浏览器,我们即可看到该容器可以出现聚焦效果,并且其内容的子元素可被编辑。元素组件已经为我们实现了基础的文本操作,例如加粗、斜体等。

【题外话】

当一个HTML文档切换到设计模式时,document暴露execCommand方法,该方法允许运行命令来操纵可编辑内容区域的元素。这个 API 并非标准 API,而是 IE 的私有 API,若干年里陆续被现代浏览器做了兼容支持。

但是,目前execCommand 方法已经被废弃。主要有两个原因,第一个便是安全问题,能够在用户未经授权的情况下就可以执行一些敏感操作;第二个问题是因为这是一个同步方法,而且操作了 DOM 对象,会阻塞页面渲染和脚本执行,这是因为当初还没 Promise,所以就没设计成异步,更没想到随后的日子里,业务复杂度变化的如此之快。

目前,W3C 也正在拟新的草案,大概率以后会引入 Clipboard API 结合 Permissions API,待用户授予了相应的权限后异步处理跟剪贴版相关的操作。

        接下来如果我们将编辑容器的子元素设为不可编辑的状态。理想状态下,我们的任何操作都不应该会影响到他们。

<div class="container" contenteditable="true">
    我是顶部内容
    <div class="item" contenteditable="false">我是第一段</div>
    <div class="item" contenteditable="false">我是第二段</div>
    <div class="item" contenteditable="false">我是第三段</div>
    我是末尾内容
</div>

        但是运行时候,我们发现在对内容的操作过程中,涉及到多选操作时,依旧能够影响到无法编辑的内容。

        这是因为 contenteditable 属性,操作的仅仅只是内容,而非元素结构,因此我们必须得避开这种存在于两个可编辑内容中不可编辑的元素。

        那么,为什么多选时能够删除编辑容器的内部结构呢?我们继续往下看。

1.2 光标操作

        作为富文本编辑器,开发者必须能够控制光标的各种状态信息,位置信息等。浏览器为开发者提供了SelectionRange对象来获取光标范围内的信息。

     a.Selection API(尚在开发中)

        Selection 对象表示用户选择的文本范围或插入符号的当前位置。它代表页面中的文本选区,可能横跨多个元素。这里我们涉及到两个概念anchorfocus,前者是锚点,指的是整个选区的起点;后者是焦点,指的是整个选区的终点

        提示:所谓选区的起点和终点,并非视觉上自上而下的起点和终点,而是鼠标按下或者开始拖拽的那个点,以及鼠标释放时选区所处位置的那个点。

        Selection 对象存在deleteFromDocument方法,它能够让我们实现前面那样,删除选区中的所有内容,哪怕你是不可编辑的状态。

editor.onclick = function () {
    const selected = document.getSelection();
    if (selected.type === "Range") {
      selected.deleteFromDocument();
    }
};

        演示效果如下图所示:

      b.Range API

        通过 Selection 对象获得的 Range 对象才是我们操作光标的重点。Range 表示的是页面上一段连续的区域,通过该对象我们可以获取或修改页面上任何区域的内容,以及复制和移动页面任何区域的元素。

        Range 对象的创建方式有两种,第一种是通过Document对象的createRange方法。第二种便是通过Selection对象的getRangeAt方法获取。

        每一个 Selection 对象至少一个 Range 对象,每个 Range 对象代表用户鼠标所选取范围内的一段连续区域(有些浏览器不支持多选,只能存在一个选区,因此只有一个Range 对象)。我们可以通过Selection 对象的 rangeCount 参数的值判断用户是否选取了内容,以及选区的个数:

  • 当用户没有按下鼠标时,该参数的值为 0。
  • 当用户按下鼠标的时,该参数值为 1。
  • 当用户使用鼠标同时按住ctrl键选取了一个或者多个区域时,该参数值代表用户选取区域的数量。
  • 当用户取消区域的选取时,该属性值为1,代表页面上存在一个空的Range对象。

        我们来看一个简单的案例,代码如下所示:

const selected = document.getSelection();
for (let i = 0; i < selected.rangeCount; i++) {
  console.log(selected.getRangeAt(i));
}

        在“火狐浏览器”下,我们可以长按 ctrl 键,选择 n 段选区,我们可以看到控制台打印出来每一段选区的 range 对象。

        确定了范围,以及范围内的数据之后,接下来我们便可以使用浏览器提供的各种 API 编写一个简单的原生编辑器了。至此,原生编辑器的介绍便告一段落。

二、元素拖拽

        draggable 是一个枚举类型的属性,用于标识元素是否允许被拖拽。它的取值如下:

  • true,表示元素可以被拖动
  • false,表示元素不可以被拖动

        如果该属性没有设值,则默认值 为 auto,使用浏览器定义的默认行为。

        拖拽过程涉及两元素,分别是:“可拖动元素”,又称源对象,指的是我们所拖动的元素;“可放置元素”,又称目标对象,指的是鼠标落点处放置源对象的元素。用户通过将屏幕指针(鼠标箭头)放置上源对象上,长按点击键即可通过指针拖动到新的位置。

        拖拽过程中,开发者可以按照特定的方式自由解释拖放交互DragEvent便是一个表示拖、放交互的DOM event接口,它的属性dataTransfer存储了在拖放交互期间传输的数据。通过该对象的事件类型,我们能够对具体的拖拽行为执行作出解释。

事件名称

描述

dragstart

当用户开始拖动元素或选择文本时触发此事件。

drag

拖动元素或选择文本时,每几百毫秒触发此事件。

dragenter

当拖动的元素或选择文本进入有效的放置目标时,会触发此事件。

dragover

当将元素或文本选择悬浮在有效放置目标上时,每几百毫秒会触发此事件。

dragleave

当拖动的元素或文本选择离开有效的放置目标时,会触发此事件。

drop

当拖动的元素或文本选择被释放在有效放置目标上时触发此事件。

dragend

当拖动操作结束时(释放鼠标按钮或按下退出键),会触发此事件。

dragexit

当元素不再是拖动操作的选择目标时触发此事件(没有浏览器实现)

        接下来我们来看一个简单的案例:

        一个完整的拖拽流程是这样的:用户长按可拖动(属性为draggable)的元素,触发dragstart事件,此后会频繁的触发drag事件。当拖拽元素进入到可放置的元素时,便会触发dragenter事件,此后会频繁的触发dragover事件,直到拖拽元素离开当前可放置元素并触发dragleave事件后结束。当拖拽元素到达目标元素后,触发 drop函数对目标对象执行处理。最后执行 dragend函数表明整个拖拽过程已经结束。

        需要注意的是,不管是浏览器内部元素的拖拽行为,还是外部文件的拖拽行为,都会触发元素的拖拽事件。如果外界的拖拽行为会影响到内容时,我们可能需要做出一些特殊处理。

        以上便是拖拽过程中,通过DragEvent的 API 以对特定的步骤做出特殊的处理。除此之外,我们还可以对拖拽过程中的样式,以及数据的转移进行存储和访问,这里就不过多阐释了。

 

 类似资料: