基于 Wangeditor 4 开发一个格式刷功能
关于Wangeditor Wangeditor提供了扩展菜单的项的功能,使用方法 参考这里
格式刷实现
引入 BtnMenu
const { BtnMenu } = E;
自定义格式刷的样式内容
const $elem = E.$(
`<div class="w-e-menu" data-title="${'格式刷'}"><span role="img" aria-label="format-painter" class="anticon anticon-format-painter"><svg viewBox="64 64 896 896" focusable="false" data-icon="format-painter" width="1em" height="1em" fill="currentColor" aria-hidden="true"><defs><style></style></defs><path d="M840 192h-56v-72c0-13.3-10.7-24-24-24H168c-13.3 0-24 10.7-24 24v272c0 13.3 10.7 24 24 24h592c13.3 0 24-10.7 24-24V256h32v200H465c-22.1 0-40 17.9-40 40v136h-44c-4.4 0-8 3.6-8 8v228c0 1.1.2 2.2.6 3.1-.4 1.6-.6 3.2-.6 4.9 0 46.4 37.6 84 84 84s84-37.6 84-84c0-1.7-.2-3.3-.6-4.9.4-1 .6-2 .6-3.1V640c0-4.4-3.6-8-8-8h-44V520h351c22.1 0 40-17.9 40-40V232c0-22.1-17.9-40-40-40z"></path></svg></span></div>`
);
构造函数中监听鼠标事件
editor.$textElem.on('mouseup', () => {
// 如果格式刷功能处于激活状态
if (me._active) {
// 延迟执行,避免获取不到正确的元素
setTimeout(() => {
// 复制格式刷样式
pasteStyle(editor);
// 取消格式刷激活样式
me.unActive();
}, 100);
}
});
定义菜单点击事件
// 菜单点击事件
clickHandler() {
const me = this;
const editor = me.editor;
if (me._active) {
// 已经在激活状态时取消激活
me.unActive();
// 清空格式刷样式数据
editor.copyStyleList = [];
} else {
// 没有选中则终端
if (editor.selection.isSelectionEmpty()) return;
// 激活按钮
me.active();
// 获取格式刷样式
const domToParse = editor.selection.getSelectionContainerElem()?.elems[0];
const copyStyleList = parseDom(domToParse);
// 保存格式刷样式
editor.copyStyleList = copyStyleList || [];
}
}
解析选中的DOM
获取目标dom,也就是当前选中模块的最小dom,需要一层层递归进行获取 在获取到对应的dom元素之后,执行getAllStyle方法,生成节点以及对应的属性,为之后进行数据填充做准备 getAllStyle方法负责向上递归获取最小dom的父级节点,在处理父级节点的时候需要额外处理 p、div、td、th标签
function parseDom(dom: HTMLElement | undefined) {
if (!dom) return;
let targetDom: Element | null = null;
const nodeArray: {
tagName: any;
attributes: { [index: number]: Attr }[];
}[] = [];
getTargetDom(dom);
targetDom && getAllStyle(targetDom);
function getTargetDom(dom: HTMLElement) {
if (dom.children.length <= 0) {
targetDom = dom;
return;
}
for (let index = 0; index < dom.children.length; index++) {
if (dom.children[index] && dom.children[index].children.length > 0) {
getTargetDom(dom.children[index] as HTMLElement);
} else {
if (dom.children[index].childNodes?.length > 0) {
for (const i of dom.children[index].childNodes) {
if (i.nodeType === 3 && i.nodeValue && i.nodeValue.trim() !== '') {
targetDom = dom.children[index];
return;
}
}
}
}
if (!!targetDom) {
return;
}
if (index + 1 >= dom.children.length) {
targetDom = dom;
return;
} else {
getTargetDom(dom.children[index + 1] as HTMLElement);
}
}
}
function getAllStyle(dom: HTMLElement) {
if (!dom) return;
const tagName = dom?.tagName?.toLowerCase();
if (tagName === 'p') {
nodeArray.push({
tagName: 'span',
attributes: Array.from(dom?.attributes || []).map(i => {
return {
name: i.name,
value: outputAttr(i, tagName),
};
}),
});
return;
} else if (tagName === 'div') {
nodeArray.push({
tagName: 'div',
attributes: Array.from(dom?.attributes || []).map(i => {
return {
name: i.name,
value: outputAttr(i, tagName),
};
}),
});
return;
} else if (tagName === 'td' || tagName === 'th') {
/** 这两个元素拦截了表格元素的向上递归 */
const arrayLikeArr = Array.from(dom?.attributes || []);
const hasStyleAttr = arrayLikeArr.some(item => item.name === 'style');
const newAttrList = hasStyleAttr ? arrayLikeArr : arrayLikeArr.concat([{ name: 'style', value: '' }]);
nodeArray.push({
tagName: 'span',
attributes: newAttrList.map(i => {
return {
name: i.name,
value: outputAttr(i, tagName),
};
}),
});
return;
} else {
nodeArray.push({
tagName: tagName,
attributes: Array.from(dom?.attributes || []).map(i => {
return {
name: i.name,
value: outputAttr(i, tagName),
};
}),
});
getAllStyle(dom?.parentNode as HTMLElement);
}
}
- 样式内容会进行处理
- 如果标签是td\tr\table\tbody\th,则不需要边框,背景色,margin,padding,
- 其它的标签不需要 边框、背景色
function outputAttr(i: Attr, tagName: string) {
const isStyle = i.name === 'style';
const hasOther =
tagName === 'td' || tagName === 'tr' || tagName === 'table' || tagName === 'tbody' || tagName === 'th';
const paddingAndMargin = ';padding:0;margin:0;';
const normalStyle = ';border:0;background-color:transparent;';
if (isStyle && hasOther) {
return i.value + normalStyle + paddingAndMargin;
}
if (isStyle && !hasOther) {
return i.value + normalStyle;
}
return i.value;
}
return nodeArray;
}
添加样式方法
传入对应的选中的文本内容,将获取到的父级节点,动态生成,之后将选中的文本内容作为文本元素放进去,以此来展示
function addStyle(text: any, nodeArray: TypeCopyStyleList) {
let currentNode: any = null;
// 遍历元素节点,动态生成节点,将获取的文本节点,作为内容,进行样式的层级覆盖
nodeArray.forEach((ele, index) => {
const node = document.createElement(ele.tagName);
for (const attr of ele.attributes) {
node.setAttribute(attr.name, attr.value);
}
if (index === 0) {
node.innerText = text;
currentNode = node;
} else {
node.appendChild(currentNode);
currentNode = node;
}
});
return currentNode;
}
粘贴
- 获取格式刷保存的样式
- 有样式说明格式刷被激活
- 获取当前选中内容
- 如果没选中也会执行,再次使用需要重新激活格式刷功能
- 清空格式刷样式
function pasteStyle(editor: E) {
const copyStyleList = editor.copyStyleList;
if (copyStyleList) {
const text = editor.selection.getSelectionText();
const targetDom = addStyle(text, copyStyleList);
if (targetDom) {
editor.cmd.do('insertHTML', targetDom.outerHTML);
}
editor.copyStyleList = null;
}
}
格式刷功能注册到富文本组件中
- 注册菜单
const menuKey = 'formatBrushMenu';
// 注册进入menus
editorRef.current.menus.extend(menuKey, FormatBrushMenu);
// 添加到菜单栏
editorRef.current.config.menus.push(menuKey);
多语言
按照文档说明如果需要多语言需要i18next
模块,通过config.languages
模块注入对应的文案内容
转载自:https://juejin.cn/post/7244487194174275639