富文本编辑器
项目需求
不用任何框架,从0开始开发一个简单的富文本编辑器,实现“加粗”“设置标题”“设置颜色”三个功能,使用typescript和webpack,进行编译打包。
实现思路
webpack 配置
entry output常规配置,因为打算做个第三方包,所以打包出来的文件没用(hashcontent-防止缓存)进行文件名填充。resolve配置一些常用的路径匹配规则以及解析后缀为.ts的文件,不然webpack配合typescript找不到ts文件loader:
sass-loader支持scss文件解析postcss-loader添加特定于浏览器厂商的前缀、支持未来 CSS语法css-loader配置modules参数以使得css可以模块化导入,隔绝样式污染。asset/resource webpack5发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。babel-loader + ts-loader ts文件编译+babel支持es5
plugins:
- 使用
MiniCssExtractPlugin进行css打包拆分 PS:webpack打包默认会把css打包到output文件当中 HtmlWebpackPlugin打包自动以模板生成htmlCleanWebpackPlugin打包之前清除dist文件夹里的文件
const path = require('path')
const resolve = (dir) => path.join(path.resolve(__dirname, '../'), dir)
module.exports = {entry:resolve('src/index.ts'),
output: {
path: resolve('dist'),
filename:'index.js'
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
alias: {
'@': resolve('src/'),
}
},
module: {
rules: [
{
//组件化moudle scss配置
test: /\.scss$/i,
include:resolve("src/components"),
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders:2,
modules: {
localIdentName: "leo-[local]"
}
}
}
,'postcss-loader'
, 'sass-loader'],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
{
test: /\.ts$/,
use: ['babel-loader','ts-loader'],
exclude: /node-modules/
},
],
},
plugins: [
new HtmlWebpackPlugin({template:resolve('src/index.html')}),
new CleanWebpackPlugin(),
new MiniCssExtractPlugin()
]
}
ts.config.js
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es6",
"target": "es5",
"jsx": "react",
"allowJs": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
},
"include": [
"src/"
],
"exclude": [
"node_modules",
]
}
- .babelrc @babel/plugin-transform-runtime 支持polify和一些函数的helper 按需导入
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs":3
}
]
],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": 3
}
]
]
}
代码实现前信息获取
查阅了相关的资料文档获得以下信息
contenteditable指示元素是否应该由用户进行编辑 MDN文档
<div contenteditable="true"></div>
实现富文本编辑器,最重要的一个点就是光标定位,
let selection = window.getSelection()方法可以操作光标,selection.getRangeAt()返回所有光标的集合,因为富文本只有一个光标所以通常selection.getRangeAt(0)获取当前的光标Range对象。MDN文档
如图是选中的Range对象,里面包含了
startoffset,endoffset是鼠标选中拖拽的起始和终点位置.startContainer,endContainer返回Range开始和结束的节点.collapsed:startoffset和endoffset相等则返回true即当前光标的位置.
操作字体变粗改色等一些列操作也有对应实现的
apidocument.execCommand(aCommandName, aShowDefaultUI, aValueArgument)通过这些命令可以作用到当前Range对象区域。虽然官方已经Deprecated... MDN文档
document.execCommand("formatBlock", false, "H1") //设置为H1标题
document.execCommand("foreColor", false, "#D4F2E7") //改变颜色
document.execCommand("bold", false) //加粗
实现代码
项目实现图
封装光标操作函数
因为在实战中我们经常需要保存当前Range对象 然后在某个时间点重置到上次保存的节点
比如点击toolbar时,会失去焦点,此时Range对象与我们在编辑器时编辑的不是一个对象,所以会出现执行命令没无反应的效果
utils.ts
/**
* @function 保存当前的Range对象
*/
export const saveSelection = () => {
let selection = window.getSelection();
if (selection.rangeCount > 0) {
return selection.getRangeAt(0);
}
return null;
};
/**
* @function 重置Range对象
* @param selectedRange 之前光标的Range对象
*/
export const restoreSelection = (selectedRange: Range) => {
let selection = window.getSelection();
if (selectedRange) {
selection.removeAllRanges();
selection.addRange(selectedRange);
}
};
execCommand简单封装
export const execCommand = (id: string, val?: string) => {
return document.execCommand(id, false, val);
};
代码实现模板
然后抽离下常见的interface 比如所有的components都需要create初始化操作,template模板html挂载,handler添加监听事件
export interface LComponent {
readonly create: () => void;
readonly template: () => void;
readonly handler: () => void;
}
定义props 实现接口,此处就不贴所有的代码实现了,此处有伪代码!
interface EditContainerConfig {
height: string;
focus: boolean;
zIndex: number;
}
export default class EditContianer implements LComponent {
elem: HTMLElement;
config: EditContainerConfig;
selectedRange: Range;
currentSeleted: HTMLElement;
constructor(selector: string) {
this.elem = document.querySelector(selector);
this.config = {
height: "400px",
zIndex: 10001,
focus: true,
};
}
create() {
this.template()
this.handler()
}
template() {
this.elem.innerHTML = `
<div id="leoEditor" contenteditable="true">
<p id="keep-p"><br></p>
</div>
`;
}
handler() {
const toolBar: HTMLDivElement = document.querySelector(".leo-tool-bar");
this.elem.addEventListener(
"keyup",
(e) => {
//监听删除按键keep p标签
if (e.keyCode === 8 && keepP.innerHTML === "<br>") {
leoEditor.innerHTML = `<p id="keep-p"><br></p>`;
}
this.selectedRange = saveSelection();
}
);
}
//事件监听
delegate(toolBar, "click", "i", (e) => {
if(xxx) execCommand();
});
}
}
总结
虽然已经简单完成,但是bug特别多,算是初次窥探了下富文本编辑器的大坑,自己很多代码是实现上目前还没有很好的实现方案。如果真要逐一解决这些问题的话,需要耗费大量的精力,另外自定义光标选择器可能才是未来的的主流吧.
项目github链接leoeditor