This article is a Chinese translation of the original English text link medium.com/swlh/reimpl…
Or dev. To/daviddalbus… (This is accessible)
The translator said
I am the author of wangEditor and am currently working on a V4.0 refactoring with the development team. The code prior to V4.0 is available at WE-Next and will be available on the official website after release.
For those of you who know rich text editors, Document. execCommand is the core API for implementing rich text on web pages. Unfortunately, Document. execCommand has been deprecated by MDN. And, all the time, each browser for its implementation, details are not completely unified, browser compatibility has been a headache.
In order to solve the above problems, various rich text editors have long had their own exploration. The powerful Google Doc was the first to implement its own rich text editing API. Later, with the popularization and use of online Office, rich text editors at home and abroad are also developing rapidly, and some have developed document. WangEditor will inevitably undergo such self-development and upgrades (once V4.0 is released and features become stable).
How do you implement Document. execCommand yourself? Chinese materials are still relatively few, so I found an English article to translate. If the translation is wrong, you can reply to the comments.
(Original addressJuejin. Cn/post / 686491…, reprint must be approved by the author!
Now the translation begins!!
Now the translation begins!!
Now the translation begins!!
This feature (document.execcommand) is deprecated, and although it is still available in many browsers, it can be removed at any time. So avoid — MDN Web Docs
Document.execcommand () has been marked as obsolete in MDN since some time ago or for some reason. Interestingly, it is not marked as deprecated in all languages (e.g., Chinese, English, not programming languages), such as French and Spanish, which do not mention this ๐.
For DeckDeckGo, an open source slide editor, we developed and published a custom WYSIWYG editor that uses document.execcommand ().
I thought a custom implementation of Document.execcommand () might be the way forward, so it took me quite a while to re-implement it ๐.
Even though the way I did it didn’t look so bad (hopefully), I felt like I had to reinvent the wheel. This is also the reason why I write this article, I hope you can put forward some improvement suggestions, let it more mature and perfect ๐.
introduce
WYSIWYG editor, its cross-device compatibility is better. It works in PC browsers as well as mobile devices. It can be attached to the top of the attempt (iOS) or the bottom (Android), depending on the keyboard’s behavior.
It can modify text styles (bold, italic, underline, and strikeout), font colors and background colors, alignment, and lists. Some custom slots are even supported.
limited
My own implementation of Document.execcommand () works fine, but it doesn’t support undo yet, unfortunately ๐. You can put forward your own good suggestions.
The target
The next API to be implemented in this article is the following, which can be referred to the MDN documentation.
document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
Copy the code
The functions to be implemented are
bold
Bold/unbolditalic
Italic/unitalicunderline
Set/ununderlinestrikeThrough
Set/cancel the delete lineforeColor
Modify font colorbackColor
Modify the background color
implementation
I like to develop in TS, so the rest of the code will be strongly typed and use interfaces.
export interface ExecCommandStyle {
style: 'color' |
'background-color' |
'font-size' |
'font-weight' |
'font-style' |
'text-decoration';
value: string;
initial: (element: HTMLElement | null) = > Promise<boolean>;
}
Copy the code
By default, executing document.execCommand creates new elements (such as elements in bold), and I decided to modify the CSS to achieve this effect. For example, value: ‘bold’ can indicate style: ‘font-weight’, and if style: “color” value can be # CCC, like this. The interface also contains an initial function that I use to determine whether to set or remove the style.
Now that the interface is defined, let’s start implementing it and see how the style is applied. The selected text is retrieved through Selection, and its Container is identified. Note that the Container could be a text node or the parent element of a selection.
Also, the second argument containers in the function, which defines a list of elements that the function can apply, default to H1, H2, H3, H4, H5,h6,div. This restriction is introduced so that you don’t have to traverse the entire DOM tree when searching.
export async function execCommandStyle(action: ExecCommandStyle, containers: string) {
const selection: Selection | null = await getSelection();
if(! selection) {return;
}
const anchorNode: Node = selection.anchorNode;
if(! anchorNode) {return;
}
constcontainer: HTMLElement = anchorNode.nodeType ! == Node.TEXT_NODE && anchorNode.nodeType ! == Node.COMMENT_NODE ? (anchorNodeas HTMLElement) : anchorNode.parentElement;
// TODO: next chapter
}
async function getSelection() :Promise<Selection | null> {
if (window && window.getSelection) {
return window.getSelection();
} else if (document && document.getSelection) {
return document.getSelection();
} else if (document && (document as any).selection) {
return (document as any).selection.createRange().text;
}
return null;
}
Copy the code
To set CSS styles, convert the text selected by the user to a SPAN tag.
Also, I think setting CSS styles is better than always adding tags. For example, if the user selects the same text and sets the background color first to red and then to green, it is definitely better to modify the CSS style directly, rather than generate new elements for the same Settings. So, when I implement it, I’m going to decide whether it’s updateSelection or replaceSelection.
const sameSelection: boolean = container && container.innerText === selection.toString();
if(sameSelection && ! isContainer(containers, container) && container.style[action.style] ! = =undefined) {
await updateSelection(container, action, containers);
return;
}
await replaceSelection(container, action, selection, containers);
Copy the code
Update Selection Updates the Selection
Simply put, this means setting new CSS styles for existing elements. async function updateSelection(container: HTMLElement, action: ExecCommandStyle, containers: string) {
container.style[action.style] = await getStyleValue(container, action, containers);
await cleanChildren(action, container);
}
Copy the code
Changing styles is more difficult than adding them. For example, for bold or ITALic, the user might want to set, then cancel, then set, then cancel again… For Bold, the Initial function is a simple property check. For colors, it’s a bit trickier because color values can be in HEX and RBG formats, both of which need to be supported. With initial, I can decide whether to add or remove styles. But that’s not enough. Because the selection style may inherit its parent element, such as
. So I created a new function, findStyleNode, that will recursively search until it finds an element or container with the same style. Finally, the style is set up and cleanChildren is executed. CleanChildren is also a recursive method, but instead of recursing up the DOM tree, it recurses all the children of the container until it is finished. Replacing Selection styles is slightly simpler (compared to Update Selection), and using range.Extractcontents gives you a fragment that can be added as content to a new SPAN. To style the new SPAN, I can just reuse the getStyleValue function from above. Once you have created a new span and added a fragment, you need to go to cleanChildren so that the style works for all the child elements. CleanChildren is also the same as described above. Finally, I tried to avoid a span tag that had no pattern, so I created a flattening children function. It checks the span in the child element and, if there is no style left, turns the span into a plain text node. That is, clean up empty SPAN elements that are useless. Detailed code for this article can be obtained here If you want to download it locally, you need to clone mono-repo. The article is almost finished, I review again, to be honest, I am not sure if anyone can understand what I wrote ๐
. I hope this article has at least piqued your curiosity about WYSIWYG Try DeckDeckGo next time you do a slide show, and give us some suggestions and good ideas. To infinity and beyond! By Davidasync function getStyleValue(container: HTMLElement, action: ExecCommandStyle, containers: string) :Promise<string> {
if(! container) {return action.value;
}
if (await action.initial(container)) {
return 'initial';
}
const style: Node | null =
await findStyleNode(container, action.style, containers);
if (await action.initial(style as HTMLElement)) {
return 'initial';
}
return action.value;
}
Copy the code
{
style: 'font-weight'.value: 'bold'.initial: (element: HTMLElement | null) = >
Promise.resolve(element &&
element.style['font-weight'= = ='bold')}Copy the code
{
style: this.action,
value: $event.detail.hex, // Color selected by the user
initial: (element: HTMLElement | null) = > {
return new Promise<boolean> (async (resolve) => {
const rgb: string = await hexToRgb($event.detail.hex);
resolve(element && (element.style[this.action] ===
$event.detail.hex ||
element.style[this.action] === `rgb(${rgb}) `));
});
}
Copy the code
async function findStyleNode(
node: Node,
style: string,
containers: string
) :Promise<Node | null> {
// Just in case
if (node.nodeName.toUpperCase() === 'HTML' || node.nodeName.toUpperCase() === 'BODY') {
return null;
}
if(! node.parentNode) {return null;
}
if (DeckdeckgoInlineEditorUtils.isContainer(containers, node)) {
return null;
}
const hasStyle: boolean =
(node asHTMLElement).style[style] ! = =null &&
(node asHTMLElement).style[style] ! = =undefined &&
(node asHTMLElement).style[style] ! = =' ';
if (hasStyle) {
return node;
}
return await findStyleNode(node.parentNode, style, containers);
}
Copy the code
async function cleanChildren(action: ExecCommandStyle, span: HTMLSpanElement) {
if(! span.hasChildNodes()) {return;
}
// Clean direct (> *) children with same style
const children: HTMLElement[] =
Array.from(span.children)
.filter((element: HTMLElement) = > {
returnelement.style[action.style] ! = =undefined&& element.style[action.style] ! = =' ';
}) as HTMLElement[];
if (children && children.length > 0) {
children.forEach((element: HTMLElement) = > {
element.style[action.style] = ' ';
if (element.getAttribute('style') = = =' ' || element.style === null) {
element.removeAttribute('style'); }}); }// Direct children (> *) may have children (*) to be clean too
const cleanChildrenChildren: Promise<void= > []Array.from(span.children).map((element: HTMLElement) = > {
return cleanChildren(action, element);
});
if(! cleanChildrenChildren || cleanChildrenChildren.length <=0) {
return;
}
await Promise.all(cleanChildrenChildren);
}
Copy the code
Replace Selection Replace Selection
async function replaceSelection(container: HTMLElement,
action: ExecCommandStyle,
selection: Selection,
containers: string) {
const range: Range = selection.getRangeAt(0);
const fragment: DocumentFragment = range.extractContents();
const span: HTMLSpanElement = await createSpan(container, action, containers);
span.appendChild(fragment);
await cleanChildren(action, span);
await flattenChildren(action, span);
range.insertNode(span);
selection.selectAllChildren(span);
}
Copy the code
async function createSpan(
container: HTMLElement,
action: ExecCommandStyle,
containers: string
) :Promise<HTMLSpanElement> {
const span: HTMLSpanElement = document.createElement('span');
span.style[action.style] = await getStyleValue(container, action, containers);
return span;
}
Copy the code
async function flattenChildren(action: ExecCommandStyle, span: HTMLSpanElement) {
if(! span.hasChildNodes()) {return;
}
// Flatten direct (> *) children with no style
const children: HTMLElement[] =
Array.from(span.children).filter((element: HTMLElement) = > {
const style: string | null = element.getAttribute('style');
return! style || style ===' ';
}) as HTMLElement[];
if (children && children.length > 0) {
children.forEach((element: HTMLElement) = > {
const styledChildren: NodeListOf<HTMLElement> = element.querySelectorAll('[style]');
if(! styledChildren || styledChildren.length ===0) {
const text: Text = document.createTextNode(element.textContent); element.parentElement.replaceChild(text, element); }});return;
}
// Direct children (> *) may have children (*) to flatten too
const flattenChildrenChildren: Promise<void= > []Array.from(span.children).map((element: HTMLElement) = > {
return flattenChildren(action, element);
});
if(! flattenChildrenChildren || flattenChildrenChildren.length <=0) {
return;
}
await Promise.all(flattenChildrenChildren);
}
Copy the code
In a nutshell
ExecCommandStyle
interfaceconclusion