By Fred Every, Lead Developer on the Typester project.
This article is the second in a series offering a look into the inner workings of the open-source project Typester, a WYSIWYG editor designed and developed at Type/Code to return standardised, sanitised, and predictable HTML.
For more information on Typester, demos, or to get started with the editor, visit http://typester.io/.
The real magic in Typester comes from its formatters, the modules it uses to perform the user requested formatting on the selected content. For anyone who has looked into creating a WYSIWYG you may be familiar with the
document.execCommand API method exposed by the browser, which can perform various formatting tasks on selected content inside an editable element. There are some pitfalls with the execCommand api though, which include:
- Malformed HTML output: including illegally nested elements, inline styling, and if you really push it you can even break the DOM completely where open and close tags for elements no longer exist at the same level.
- Non-standardised output: each browser has its own implementation and often the results of performing a formatting task differ from browser to browser.
That said, the
document.execCommand does, for the most part, do a very good job. So what we’ve done is wrap the api with our formatter modules that perform pre- and post-processing on the content along with the
document.execCommand API. We’ve also sandboxed the formatting in an iframe (referred to as “the canvas”) which allows us to do all the processing we want without polluting the edit history of the editable content.
Before we dive into the various components associated with formatting content in Typester, here is the flow of how the plugin performs its formatting:
- The user selects a range of content inside an editable container.
- Typester then shows a contextual toolbar with various formatting controls.
- When the user clicks on a control the toolbar module will send an exec command to the relevant formatter.
- The contents of the editor are then cloned into the canvas.
- The selection range coordinates that we cached earlier are then applied inside the iframe canvas to mirror the user’s selection.
- The relevant formatter will then perform the tasks necessary to format the selected content. This can involve calling
document.execCommandas well as DOM manipulation where it may unwrap, remove, shuffle, or modify the DOM structure of the content in the canvas.
- Some formatters will perform post-processing on the content to ensure that it is well formed and standardised.
- The new selection range in the canvas is then resolved to coordinates and cached.
- The canvas, with the formatter result, is then cleaned. Invalid elements are removed, orphaned text nodes are converted to paragraph tags, br tags are removed, all block type elements are hoisted to the root element, all inline attributes are removed, and any empty nodes are stripped.
- The canvas content is then injected back into the editor using the paste command.
- The cached selection coordinates from the canvas is then applied to the updated content in the editor.
- The user can now continue writing and editing as per usual.
The formatters in Typester have been decomposed into separate modules based on the type of formatting they will need to perform.
The base formatter
The base formatter is a general, foundational formatter that assists with exporting/importing content to/from the canvas, resolving, caching and re-applying selection coordinates, and performing the cleanup flow.
The block level formatter
This formatter handles all the formatting for block level content including headers (H1~H6 etc.), paragraphs, and blockquotes.
The list formatter
This formatter handles ordered and unordered lists, including toggling content into/out of lists, indenting/outdenting nested lists, etc.
The link formatter
This formatter is responsible for wrapping and unwrapping links around content, as well as presenting the necessary tools for editing links and handling the user input.
The text / inline formatter
This formatter handles toggling content to/from bold and italic.
Selections & Ranges
Typester has a lot of logic around selections and ranges and the manipulation and mapping of these to ensure that the correct content is formatted and when presented back to the user the selection range is identical to the pre-formatted range. This does become quite involved when moving the range between the page and the canvas as well as remapping it after formatting. We found that some of the
document.execCommand calls would either lose the range completely or it would not map it back to the content accurately. So we had to put in logic to fix that.
On top of that we implemented a pseudo range/selection feature for when the user needs to create or edit a link. The issue we found was that when focusing the link input field in the toolbar, the selection range would be removed from the editable element as it loses focus. To combat this we paint a faux selection box around the content selected so that while the user is inputting, or editing, the link they can still clearly see the content that will be affected.
The canvas, as we’ve come to call it, is created by way of a iframe that is injected into the DOM of the page. Because iframes are naturally sandboxed from their parent pages, this allows us to be able to import content and then edit and manipulate it as much as is needed without messing with the edit history of the page. The reason for this is that in order to standardise the HTML output from the
document.execCommand the plugin needs to manipulate the DOM structure of the content. The problem here being that in doing so the edit history is polluted, resulting in undesired and wonky behaviour when the user undoes any of their edits using CTRL+Z or similar.
Often what happens is the resulting undone content is malformed and not what the user expected, resulting in the user having to re-write a lot of their content.
To circumvent this we created the canvas.
And, as an added bonus, it also allows us to handle the pasting of content in a way that sandboxes that as well. By intercepting the paste event and redirecting it to the canvas we are able to clean the content, sanitize it using DOMPurify to eliminate XSS threats, and then standardise the HTML.
The toolbar for Typester mimics the toolbar developed by Medium for their editing very closely. It is contextual and appears only when required, when a user has selected content for formatting.
We have also made it smarter than the average toolbar, in that it protects the content by stopping the user from performing illegal formatting. Illegal formatting in this sense includes things like injecting a
<b> tag into a header that already has the font-weight set to bold. Or injecting a
H1 tag into a list. The toolbar does this by inspecting the selected content and then adapting its controls to give the user only those formatters that make sense in the current situation.
And that’s it really. For more info checkout http://typester.io.