那些被忽略但很好用的 Web API / 简易文字编辑器

学习之後,刻意进行练习能够加深印象。

前面三天我们已经习得 DesignMode、Selection API、Clipboard API,今天我们就将这三个 Web API 一同使用,动手写一个简易的文字编辑器吧!


简易文字编辑器

我们今天要做的文字编辑器没有特别的功能,只会模拟「全选」、「复制」、「剪下」、「贴上」四大功能,纯粹就是为了让大家更熟悉前面认识的 Web API 而已,未来各位想要新增其他的 feature,可以再自行发挥。

# HTML

首先我们先准备一下今天的网页版面:

<div>
  <button id="all">全选</button>
  <button id="copy">复制</button>
  <button id="cut">剪下</button>
  <button id="paste">贴上</button>
</div>
<div id="editable" contenteditable="true"></div>

https://ithelp.ithome.com.tw/upload/images/20210926/20125431Kv4ocrlRtc.png

那上面就是我们这次的编辑器,上面是工具列的部分,下面则是文字编辑的区域,样式的话各位可以自由发挥,CSS 的部分就不额外放上来了。而为了方便,这次的可编辑范围我们使用 contenteditable 属性,各位也可以改用 iframe 搭配 DesignMode。

 

# Utilities

再来,我们先准备一些共用的变数和函式:

const editableDiv = document.querySelector("#editable");

function addClickListener(selector, callback) {
  const el = document.querySelector(selector);
  el.addEventListener("mousedown", function (e) {
    e.preventDefault(); // 关闭 mousedown 原生事件
  });
  el.addEventListener("click", function (e) {
    callback(e);
  });
}

由於我们这次有四个按钮需要绑定事件,所以为了避免重复撰写,我们把事件监听的程序码拉出来作为共用函式,要注意的是,因为我们在点击按钮时会使得「可编辑区域」(#editable)的 focus 造成失焦,所以要特别把 mousedown 的原生事件关闭。

 

# 全选按钮

首先第一个按钮是「全选」,我们希望使用者在点击按钮後可以把编辑区域中的文字全部反白:

addClickListener("#all", function (e) {
  const selection = window.getSelection();
  selection.collapse(editableDiv, 0);
  if (!editableDiv.childNodes.length) return; // 避免在没有任何文字节点时进行 extend
  selection.extend(editableDiv.childNodes[0], editableDiv.textContent.length);
});

利用前面的共用函式 addClickListener,来设定按钮,而在 Callback 中我们的步骤是先将 Selection 坍缩(collapse)在编辑区的最前头,然後透过 extend 来将 focus 移动到最後面,这样编辑区自然就会被全部反白了。

不过要注意,当编辑区没有任何文字节点时进行 extend 是会报错的,所以我们在中间有加一行防御性的判断式。

 

# 复制按钮

再来复制按钮要让使用者可以将目前反白的文字放进系统剪贴簿中,这样之後才能将其「贴上」。

addClickListener("#copy", function (e) {
  const selection = window.getSelection();
  navigator.clipboard.writeText(selection.toString());
});

复制功能就相对简单很多了,只要使用之前介绍 Selection 和 Clipboard 的基本 method 就可以实现。

 

# 剪下按钮

剪下按钮要做的事情其实和「复制」没差多少,不过除了要将文字加进剪贴簿外,原本的文本内容需要将其移除,所以最後我们使用 Selection 的 deleteFromDocument 来处理。

addClickListener("#cut", function (e) {
  const selection = window.getSelection();
  navigator.clipboard.writeText(selection.toString());
  selection.deleteFromDocument();
});

 

# 贴上按钮

最後就剩贴上按钮了,也是本次练习中最复杂的功能,因为「贴上」这个动作在将剪贴簿的内容放入编辑区时,可能会有两种情境需要一并考虑:

  1. 使用者将输入游标插入编辑区的某个位置,想要并将剪贴簿内容贴上
  2. 使用者先反白了文字,想要将剪贴簿内容贴上,并取代反白的文字

根据以上两个情境,我们可以大致盘点出需要做的事情:

  1. 如果有反白文字,必须要将反白文字删除
  2. 如果有反白文字,反白文字以前和以後的文字要保留
  3. 如果没有反白文字,游标插入位置之前和之後的文字要保留
  4. 最後编辑区要显示的内容应该是,前半部保留的文字 + 剪贴簿的文字 + 後半部保留的文字
addClickListener("#paste", async function (e) {
  const selection = window.getSelection();
  selection.deleteFromDocument();

  const offset = selection.anchorOffset;
  const prefix = editableDiv.textContent.substr(0, offset);
  const suffix = editableDiv.textContent.substr(offset);

  const clipboardText = await navigator.clipboard.readText();
  const textNode = document.createTextNode(prefix + clipboardText + suffix);
  editableDiv.innerHTML = "";
  editableDiv.appendChild(textNode);
  selection.collapse(textNode, (prefix + clipboardText).length);
});

在整理出事项後,上面就是我们最後撰写出来的程序码了,首先执行 deleteFromDocument,只要有反白的文字就会被删除,而且与此同时,Selection 会被自动坍缩在一个点上,这样只要取得 anchorOffset 就可以知道游标目前插入在第几个字。

然後就可以使用 substr 把需要保留的文字分割出来,最後只要加上剪贴簿的内容再放回去就行罗,最後一行则是将游标放回原本的位置。

如果想要玩玩看的话,这边是这次练习的 CodePen

 

不晓得大家对於这次的练习还满意吗?虽然今天做出来的功能并不是很实用,但主要还是希望和大家一起在复习前面几天学习的内容,而且其实各位时间心力的话,可以再为这个范例不断添加新功能,完成自己的作品。

另外这种文字编辑器,其实有很多 js 套件都有实现了,例如 Slate.js 就是一套很完整的工具,它使用的技术和我们今天使用的差不了多少,有兴趣的人可以再去看看。

通常这种所见即所得的编辑器会称作「富文字编辑器」(Rich Text)


<<:  [番外] 来个 Weather App (续)

>>:  meta标签及link标签-常用语法

[Day21]Palindromes

上一篇介绍了The Huge One,这题因为数字太大,所以选择使用了BigInteger,当然各位...

Day10-119. Pascal's Triangle II

今日题目:119. Pascal's Triangle II Given an integer ro...

资安学习路上-网站常见漏洞与 Injection的爱恨情仇2

SQL injection 先来简单练习 https://www.hacksplaining.com...

易用性、无障碍、通用、包容性设计 — part1

最近看设计相关文章时,常常会看到这四个名词,usability、accessibility、univ...

Dungeon Mizarka 026

调整移动UI 今天试着调整UI到新的架构里,问题比我想像的还要多,一方面是整个架构和之前完全不一样,...