怎样算一个好的富文本呢? 为什么要从零写一个富文本呢?
公司采购了一款 FroalaEditor 富文本工具 但该工具同时存在一些不可修改的问题 加之不好维护 不易扩展 官方文档也不全
有误项目上线已经很久了 临时换框架是不现实的
有时间摸鱼的我不如从零学习写一款富文本组件 用来替换该库
那么怎么实现一个简易的富文本呢?
首先想到的是 document.execCommand
但该命令已经被官方废弃 虽然仍可以使用 说不定什么时候就噶了
document.execCommand
已弃用: 不再推荐使用该特性。虽然一些浏览器仍然支持它,但也许已从相关的 web 标准中移除,也许正准备移除或出于兼容性而保留。请尽量不要使用该特性,并更新现有的代码;参见本页面底部的兼容性表格以指导你作出决定。请注意,该特性随时可能无法正常工作。
当一个 HTML 文档切换到设计模式时,
document暴露execCommand方法,该方法允许运行命令来操纵可编辑内容区域的元素。大多数命令影响
document的 selection(粗体,斜体等),当其他命令插入新元素(添加链接)或影响整行(缩进)。当使用contentEditable时,调用execCommand()将影响当前活动的可编辑元素。
那么不用execCommand 怎么写一个简易富文本呢?
下面是一个简单的思路
一个加粗功能的流程如下:
选中文本 -> 点击加粗按钮 -> 文本加粗 / 文本取消加粗
在html中 我们获取文本时 需要使用到
window.getSelection().getRangeAt(0)
上面这句话的含意 及 获取最后一个选区
其中包含一些内容
startContainer 即 文本内容 这四个字所在的位置
text 是无法增加样式的 只有 HtmlElement才能使用 style 属性 所以 需要将该文本内容追加到一个 HtmlElement中
export function blocks<K extends keyof HTMLElementTagNameMap>(tag: K): HTMLElement {
const block = document.createElement(tag);
const range = window.getSelection().getRangeAt(0);
const old = range.extractContents();
range.deleteContents();
block.appendChild(old);
range.insertNode(block);
return block;
}
这样就在 文本内容 的位置加入了一个 span 标签
接着在对 创建的 block 块 进行额外的样式操作即可!
我们获取还需要个设置样式的函数
export function setStyle<K extends keyof HTMLElementTagNameMap>(
style: { [id: string]: string | number },
target?: HTMLElement,
tag?: K,
) {
if (target) {
Object.keys(style).forEach((key) => {
target.style[key] = style[key];
});
return;
}
const block = blocks(tag ?? 'span');
Object.keys(style).forEach((key) => {
block.style[key] = style[key];
});
}
整体代码如下:
/**
* 生成一个包装块
*
* 将选中内容放置进入包装块 并移除界面上旧的内容
*
* @param tag
*/
export function blocks<K extends keyof HTMLElementTagNameMap>(tag: K): HTMLElement {
const block = document.createElement(tag);
const range = window.getSelection().getRangeAt(0);
const old = range.extractContents();
range.deleteContents();
block.appendChild(old);
range.insertNode(block);
return block;
}
export function createTable(row: number, col: number): HTMLTableElement {
const table = document.createElement('table');
const trFrag = document.createDocumentFragment();
new Array(row).fill(1).forEach(() => {
const tr = document.createElement('tr');
const tdFrag = document.createDocumentFragment();
new Array(col).fill(1).forEach(() => {
const td = document.createElement('td');
const br = document.createElement('br');
td.appendChild(br);
tdFrag.appendChild(td);
});
tr.appendChild(tdFrag);
trFrag.appendChild(tr);
});
table.appendChild(trFrag);
return table;
}
export function createImage(src: string): HTMLImageElement {
const img = document.createElement('img');
img.src = src;
img.width = 100;
return img;
}
export function createLink(src: string): HTMLAnchorElement {
const content = document.createTextNode('link');
const anchor = document.createElement('a');
anchor.href = src;
anchor.target = '_black';
anchor.appendChild(content);
return anchor;
}
export function getStyle(tag: HTMLElement): CSSStyleDeclaration {
return window.getComputedStyle(tag);
}
export function getTag<K extends keyof HTMLElementTagNameMap>(tag: K): HTMLElement {
let node = window.getSelection().getRangeAt(0).startContainer;
while (
!(node instanceof HTMLElement) ||
(node instanceof HTMLElement && node.tagName !== tag.toUpperCase() && node.parentNode)
) {
node = node.parentNode;
}
return node;
}
// 设置样式
export function setStyle<K extends keyof HTMLElementTagNameMap>(
style: { [id: string]: string | number },
target?: HTMLElement,
tag?: K,
) {
if (target) {
Object.keys(style).forEach((key) => {
target.style[key] = style[key];
});
return;
}
const block = blocks(tag ?? 'span');
Object.keys(style).forEach((key) => {
block.style[key] = style[key];
});
}
调用组件:
import { FC, useMemo, useRef } from 'react';
import { blocks, createImage, createLink, createTable, getStyle, getTag, setStyle } from '../../helper';
import './index.scss';
interface ITool {
name: string;
callback: () => void;
}
export const App: FC = () => {
const ref = useRef<HTMLDivElement>(null);
const toolbars = useMemo<ITool[]>(() => {
return [
{
name: '加粗',
callback: () => {
setStyle({ 'font-weight': 'bold' });
},
},
{
name: '斜体',
callback: () => {
setStyle({ 'font-style': 'italic' });
},
},
{
name: '下划线',
callback: () => {
setStyle({ 'text-decoration': 'underline' });
},
},
{
name: '删除线',
callback: () => {
setStyle({ 'text-decoration': 'line-through' });
},
},
{
name: '插入表格',
callback: () => {
const table = createTable(2, 2);
window.getSelection().getRangeAt(0).insertNode(table);
},
},
{
name: '插入图片',
callback: () => {
const img = createImage(
'https://i0.wp.com/wx4.sinaimg.cn/large/0072Vf1pgy1foxkioq4i5j31hc0u0e1o.jpg',
);
window.getSelection().getRangeAt(0).insertNode(img);
},
},
{
name: '插入链接',
callback: () => {
const img = createLink(
'https://img0.baidu.com/it/u=3512817427,2177083968&fm=253&fmt=auto&app=138&f=JPEG?w=650&h=431',
);
window.getSelection().getRangeAt(0).insertNode(img);
},
},
{
name: '换行',
callback: () => {
const br = document.createElement('br');
const p = document.createElement('p');
p.appendChild(br);
const selection = window.getSelection();
const range = selection.getRangeAt(0);
const parentNode = range.startContainer.parentNode;
range.deleteContents();
if (parentNode === ref.current) {
range.setEndAfter(parentNode);
range.insertNode(p);
} else {
range.setStartAfter(parentNode);
range.insertNode(p);
}
range.collapse(true);
},
},
{
name: '字号',
callback: () => {
setStyle({ 'font-size': '30px' });
},
},
{
name: '字前景色',
callback: () => {
setStyle({ color: '#eeeeee' });
},
},
{
name: '字背景色',
callback: () => {
setStyle({ 'background-color': 'orange' });
},
},
{
name: '字体',
callback: () => {
setStyle({ 'font-family': 'STSong' });
},
},
{
name: '右对齐',
callback: () => {
setStyle({ 'text-align': 'right' }, getTag('p'));
},
},
{
name: '左对齐',
callback: () => {
setStyle({ 'text-align': 'left' }, getTag('p'));
},
},
{
name: '居中对齐',
callback: () => {
setStyle({ 'text-align': 'center' }, getTag('p'));
},
},
{
name: '增加缩进',
callback: () => {
const data = getStyle(getTag('p')).marginLeft.match(/\d+/gi);
setStyle({ 'margin-left': `${Number(data[0]) + 20}px` }, getTag('p'));
},
},
{
name: '减少缩进',
callback: () => {
const tag = getTag('p');
const data = getStyle(tag).marginLeft.match(/\d+/gi);
setStyle({ 'margin-left': `${Math.max(Number(data[0]) - 20, 0)}px` }, tag);
},
},
{
name: '行高',
callback: () => {
setStyle({ 'line-height': '3' }, getTag('p'));
},
},
{
name: '块',
callback: () => {
blocks('span');
},
},
];
}, []);
return (
<div className="editor">
<div className="toolbar">
{toolbars.map((tool, index) => (
<button key={index} onClick={tool.callback}>
{tool.name}
</button>
))}
</div>
<div
className="editor-content"
contentEditable
ref={ref}
dangerouslySetInnerHTML={{ __html: '<p>placeholder</p>' }}
/>
</div>
);
};
体验 demo 点击 : demo-rich.netlify.app/