Rolling our own Medium-style WYSIWYG

Mark Lancaster
Level Up Coding
Published in
6 min readJan 24, 2019

--

A year ago, a client approached us and asked for a rich text editor (RTE) that could look and feel like Medium.com with some added custom functionality. The mockup we started with was simple and only the body of the post was to be Medium-esque.

Jan 2018 — Rich Text Editor Mockup

One of our junior developers started with an existing RTE, CKEditor, and began to customize it to work with the API data structure we had architected in the months prior, but red flags started to pop up a couple weeks into development:

  • How do we get the horizontal and vertical position of a user’s text caret reliably? (the blinking vertical dash where you are actively writing)
  • Should we convert the formatting of text to markdown or store the raw HTML in the data?
  • How do we handle splitting a text block in two if a user inserts an image or video mid-paragraph?
  • Older versions of Safari/Firefox didn’t some of the new ECMA standards in CKEditor.
  • Our build size ballooned in size to over 1.5mb gzipped from ~450kb.

And with each release of CKEditor, it seemed the customization we added broke and caused delay after delay. — What initially seemed “do-able” soon became a game of “well… let’s just make it usable”.

That first version of our editor was riddled with bugs and our client agreed to work around them while we manually edited the post data when formatting issues arose. Of all the bugs, the most common were related to markdown.

Markdown isn’t perfect. It’s very useful, but far from perfect. There’s a myriad of cases in which it could fail proper conversion back into HTML especially when you need to support formatting like strikethrough, underline, alternative headings, intra-word formatting, and combinatory formatting (bold, italic, & strikethrough). — Not to mention markdown HATES trailing whitespaces.

Fun fact: not even Medium supports strikethrough or underline.

The reason for that is likely due to a popular web standards push for only having links underlined in HTML, but we work for our clients and our clients ask for both formatting options. — So we have to support that.

For months our client frustratingly continued to use the bastardized solution we made for them while we spun our wheels trying to figure out a better way. After a few weeks of debate and research, we decided the best solution would be to start from scratch and roll our own. Fun. “Fun”. Fun?

Enter contenteditable

The “contenteditable” attribute in HTML has been around for years and has some great functionality that is included right out of the box, but devs have railed against it for just as long as its been around (including Medium in 2014, but which now uses “contenteditable”).

Quick peak at Medium code while writing this article.

Coupled with a solid markdown converter to store the data appropriately, a contenteditable HTML element is a very powerful thing.

  • Formatting commands are simple: document.execCommand(‘bold’).
  • Keyboard shortcuts work by default: CTRL + I, CTRL + Z, etc.
  • Finding the cursor position and text selection is easier than with vanilla JS.
  • Child HTML elements (embed and iframe specifically) work natively.

And while there remain valid arguments for never using contenteditable, we didn’t have the development resources needed to develop our own set of custom listeners that contenteditable already supports.

The basis of our WYSIWYG: contenteditable content blocks + markdown.

From there, things got more complicated as we started to explore adding the following custom functionality:

  • Custom markdown: strikethrough, underline
  • Sanitizing and formatting pasted content from external apps
  • Drag and drop of content blocks
  • Exiting certain elements like <ul> & <ol> when the user intends
  • Splitting a text block when the user inserts an embedded element
  • Making sure the user cannot reach a state of incorrect HTML structure
  • Autoscrolling the page if a user is approaching the end of the HTML element while typing
  • Allowing a user to create dynamic tables within content
  • Detecting pasted images as opposed to upload images

All while keeping the build size as small as possible. So, let’s dive into some of my solutions for the hardest of problems.

1. Custom markdown

One thing I think markdown is weak on is opening and closing syntax.
**text here** is less usable than something like #%text here%#. There may be reasons for this I’m unaware of, but it’s far easier to convert custom markdown when I know which character strings are opening vs closing.

For our strikethrough custom markdown, I decided to use %%#strikethrough#%% and I convert my custom markdown via regular expressions after the traditional markdown conversion so there weren’t any conflicts.

2. Sanitizing text on paste from software

This one proves to be an on-going battle. Every piece of software tries to format their text differently. And when you copy and paste from it, hidden formatting comes with it. Below is an example of pasting from Google Docs. Ugly, but makes sense.

Some of this needs to be retained, the rest can be discarded.

I could have just as well stripped ALL formatting and pasted the raw text, but that’s not very user-friendly. So instead, we strip all non-essential elements and then clean up usable elements like bold, italics, line breaks, and links and then check that every major text editor is supported (Google, Word, WordPad, TextEdit, etc). This involves a lot of regular expression magic. This is a small sample of our custom sanitizing.

const removeStyle = function(e) {
e.preventDefault();
const pastedText = e.clipboardData;// change line breaks into p tags
let sanitizedText = pastedText.replace(/^\s*\n/gm, '<p><br /></p>');
// remove all spans
sanitizedText = sanitizedText.replace(/<\/?span[^>]*>/g, '');
// remove classes
sanitizedText = sanitizedText.replace(/class='[a-zA-Z0-9:;\\.\\s\\(\\)\\-\\,]*'/g, '');

// remove styles
sanitizedText = sanitizedText.replace(/style='[a-zA-Z0-9:;\\.\\s\\(\\)\\-\\,]*'/g, '');

document.execCommand('insertHTML', false, sanitizedText);
}editableDiv.addEventListener('paste', removeStyle, false);

3. Caret and selection detection

It’s surprisingly easy to get the caret (blinking cursor) position within a string of text.

window.getSelection().getRangeAt(0).startOffset// or if user has selected textwindow.getSelection().getRangeAt(0).getBoundingClientRect().left

It’s not, however, easy to get the vertical position of the caret. Because not only do we need to know where the “+” sign on the left should go, but we also need to know where to insert the new content block you are creating (e.g. inserting an image or Twitter embed). So we first get an array of all “tags above” the cursor and find which one we’re on.

// index is passed by keyup or click eventconst contentContainer = document.getElementById('textBlock-' + index);const child = window.getSelection().anchorNode;const currentParagraph = (Array.from(contentContainer['children']).indexOf(child)) + 1;const tagsAbove = Array.from(contentContainer['children']).slice(0, currentParagraph - 1);

Then once we know where we’re at we determine the height of the elements and we know the “heightAbove” our cursor and we can set that value to the top CSS position of the “+” button.

let heightAbove = 0;tagsAbove.forEach((tag) => {
heightAbove += tag['clientHeight'] + 20;
});
// adding 20px because each element has a margin bottom of 20px
Screenshot of our new WYSIWYG. Plus sign in the correct position.

4 months later… a final product

To date, it has been the most difficult project I’ve worked on in my career. The final RTE is just under 2500 lines of HTML, CSS, and JS and supports embedding of YouTube, Vimeo, SoundCloud, Twitter, Instagram, CrowdSignal, Images, and Videos.

We do use a markdown library to convert rich text. We aren’t that crazy to write our own, yet. — Below you can see a screenshot of our rich text editor.

Big thanks to our QA Master Ian Shirley at FanReact who found literally hundreds of bugs. It was an arduous 4 weeks of testing, bug squashing and releasing, but we have a highly stable and user-friendly editor I’m proud of.

For more articles about Angular, Javascript, CSS and more visit my site.

--

--

Director of Engineering @ HelloAlice. Designer, Entrepreneur on the side. @markmakes