HTML中,任何元素都可以被编辑。现代浏览器为我们提供了许多 API 使我们可以在web浏览器上进行富文本编辑功能。
想要使元素切换到编辑模式,我们只需要在 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 属性,操作的仅仅只是内容,而非元素结构,因此我们必须得避开这种存在于两个可编辑内容中不可编辑的元素。
那么,为什么多选时能够删除编辑容器的内部结构呢?我们继续往下看。
作为富文本编辑器,开发者必须能够控制光标的各种状态信息,位置信息等。浏览器为开发者提供了Selection
和Range
对象来获取光标范围内的信息。
Selection 对象表示用户选择的文本范围或插入符号的当前位置。它代表页面中的文本选区,可能横跨多个元素。这里我们涉及到两个概念anchor
和 focus
,前者是锚点,指的是整个选区的起点;后者是焦点,指的是整个选区的终点。
提示:所谓选区的起点和终点,并非视觉上自上而下的起点和终点,而是鼠标按下或者开始拖拽的那个点,以及鼠标释放时选区所处位置的那个点。
Selection 对象存在deleteFromDocument
方法,它能够让我们实现前面那样,删除选区中的所有内容,哪怕你是不可编辑的状态。
editor.onclick = function () {
const selected = document.getSelection();
if (selected.type === "Range") {
selected.deleteFromDocument();
}
};
演示效果如下图所示:
通过 Selection 对象获得的 Range 对象才是我们操作光标的重点。Range 表示的是页面上一段连续的区域,通过该对象我们可以获取或修改页面上任何区域的内容,以及复制和移动页面任何区域的元素。
Range 对象的创建方式有两种,第一种是通过Document
对象的createRange
方法。第二种便是通过Selection
对象的getRangeAt
方法获取。
每一个 Selection 对象至少一个 Range 对象,每个 Range 对象代表用户鼠标所选取范围内的一段连续区域(有些浏览器不支持多选,只能存在一个选区,因此只有一个Range 对象)。我们可以通过Selection 对象的 rangeCount 参数的值判断用户是否选取了内容,以及选区的个数:
我们来看一个简单的案例,代码如下所示:
const selected = document.getSelection();
for (let i = 0; i < selected.rangeCount; i++) {
console.log(selected.getRangeAt(i));
}
在“火狐浏览器”下,我们可以长按 ctrl 键,选择 n 段选区,我们可以看到控制台打印出来每一段选区的 range 对象。
确定了范围,以及范围内的数据之后,接下来我们便可以使用浏览器提供的各种 API 编写一个简单的原生编辑器了。至此,原生编辑器的介绍便告一段落。
draggable 是一个枚举类型的属性,用于标识元素是否允许被拖拽。它的取值如下:
如果该属性没有设值,则默认值 为 auto,使用浏览器定义的默认行为。
拖拽过程涉及两元素,分别是:“可拖动元素”,又称源对象,指的是我们所拖动的元素;“可放置元素”,又称目标对象,指的是鼠标落点处放置源对象的元素。用户通过将屏幕指针(鼠标箭头)放置上源对象上,长按点击键即可通过指针拖动到新的位置。
拖拽过程中,开发者可以按照特定的方式自由解释拖放交互。DragEvent
便是一个表示拖、放交互的DOM event
接口,它的属性dataTransfer
存储了在拖放交互期间传输的数据。通过该对象的事件类型,我们能够对具体的拖拽行为执行作出解释。
事件名称 | 描述 |
当用户开始拖动元素或选择文本时触发此事件。 | |
拖动元素或选择文本时,每几百毫秒触发此事件。 | |
当拖动的元素或选择文本进入有效的放置目标时,会触发此事件。 | |
当将元素或文本选择悬浮在有效放置目标上时,每几百毫秒会触发此事件。 | |
当拖动的元素或文本选择离开有效的放置目标时,会触发此事件。 | |
当拖动的元素或文本选择被释放在有效放置目标上时触发此事件。 | |
当拖动操作结束时(释放鼠标按钮或按下退出键),会触发此事件。 | |
当元素不再是拖动操作的选择目标时触发此事件(没有浏览器实现) |
接下来我们来看一个简单的案例:
一个完整的拖拽流程是这样的:用户长按可拖动(属性为draggable)的元素,触发dragstart
事件,此后会频繁的触发drag
事件。当拖拽元素进入到可放置的元素时,便会触发dragenter
事件,此后会频繁的触发dragover
事件,直到拖拽元素离开当前可放置元素并触发dragleave
事件后结束。当拖拽元素到达目标元素后,触发 drop
函数对目标对象执行处理。最后执行 dragend
函数表明整个拖拽过程已经结束。
需要注意的是,不管是浏览器内部元素的拖拽行为,还是外部文件的拖拽行为,都会触发元素的拖拽事件。如果外界的拖拽行为会影响到内容时,我们可能需要做出一些特殊处理。
以上便是拖拽过程中,通过DragEvent
的 API 以对特定的步骤做出特殊的处理。除此之外,我们还可以对拖拽过程中的样式,以及数据的转移进行存储和访问,这里就不过多阐释了。