Building my Own wysiwyg Editor
So ultimately I decided to just build one from scratch. While looking through open source editors I came across MediumEditor, this editor is inspired by the editor used to write articles on Medium. I liked how it was super simple and kinda gave limited formatting options. I think most people writing articles (or project write-ups on Grepper) don't care too much about having a ton of formatting. And in the case of Grepper write-ups, I felt like some basic formatting options (h1, h2, bold, italic, links, lists) would be good for most cases, additionally it helps keep things looking simple and clean for readers.
The other things I knew I would need is the ability to embed example code snippets (that is what is Grepper is really all about). Additionally I thought some write-ups would need video explanations of things and of course some pictures to show/explain things when needed.
Getting basic text formatting working
So first I decided to get the basic text formatting working. I just wanted (h1,h2,bold,italic, and links). After making some slow progress in some weird directions I finally came across the "contenteditable" html attribute.
This "little" attribute basically changed my whole approach. It more or less makes all the content inside of the element you add it to editable in a kinda wysiwg type way. So my job was done... ;-) Well it did help a lot, but there was still quite a few issue with the default way the contenteditable worked.
When you add contenteditable anyone could highlight text within it and press b to make it bold or i to make it italic but there was still no way (I could find) to add h1,h2 or links. So I would need to build a little UI for that. I liked the way medium editor handled that... which is whenever a text is highlighted a little editor would show above that text with formatting options. Here is an example of the one I made:
Getting the CSS/look of the menu was not too hard. The kinda tricky part in building it, was getting it to display in the right spot, display at the right time and then handing the actions when they were clicked. I walk through the hard things so if you want to build one like it you can.
First the easy part the HTML/CSS. Below is the HTML.
And then here is the CSS to style the above HTML
First things to notice is the function takes in two parameters, el and currentSelection. The el is element containing the highlighted text when the menu is shown and the current selection is from the selection object (basically the highlighted text+some info). This is more or less how/when I call the showEditorBox function:
That !currentSelection.collapsed above basically means that something is highlighted, and that is when I want to call the showEditorBox function.
A few important/cool things to notice when looking at the showEditorBox function. You notice a bind a listener to all the menu items that calls document.execCommand() like this:
That document.execCommand has a bunch of methods that can be found here: https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand . These are really the core of the editor functionality, these calls allowed me to easily add formatting to the highlighted text. In the example above I can easily make the text bold, this method also handles unbolding the text too which is really nice.
Note: the document.execCommand may be depreciated in the future. But as of writing this is seems to work well. My hope is I can easily update this code to meet the standards in the future.
So adding bold and italic were pretty simple, but adding in headers became a little more difficult. Headers work a little different. They are applied the whole element rather than just the highlighted text. There is also no simple method like document.execCommand('bold') that will handle the toggling of headers so we have to do this by ourselves. In order to add a header the best method I could find was document.execCommand('formatBlock', true, '<h3>'); This basically wraps all the text inside of an element inside a h3 (or whatever tag you pass h1,h2,h3). In order to toggle the h3 on or off I add a class "action_on" to know if the element is already a h3 or not. So that code looks like this:
So now we have handled bold,italic,h3,h4, h5 tags so now we just need to handle inserting links.
I could have used h1,h2,h3 but I just chose to use h3,h4,h5 because h1 is reserved for the title and h2 I wanted to save for something else on the write-ups page. Also h5 is actually a note that looks like this... I should use pry use a different tag for notes like this, but I'm lazy so it is what is it is....
To handle adding a link we needed a little more UI that would show an input box where a user can type a link and when they press enter the link would be added to the highlighted text. Here is the function that does that:
Note: you will need to add some more css to get things to look right if you use the above code. For example .hideicons class should hide the icons in the menu so that the input field for the links shows up and looks pretty.
The last thing to look at is how to get the menu to show up in the right spot. Basically you can get the position of the current selection and have the menu show up right above it. Here is the code I used to do that:
Getting Media Object Working (Videos/Images/Code Snippets)
First we need a little UI menu that provided ability to upload image. That menu will look like this:
Alright at this point we have an editor than can add some basic text formatting. But we really need the ability to add images, videos and code snippets. I figured images would be the easiest place to start, so that is where we will begin. First we need a little UI menu that provided ability to upload image. That menu will look like this:
And will be displayed whenever a user clicks inside an empty "p" tag like so:
Note to get focusin call to work correctly you will need to add tabindex="0" to all the p tags.
Now whenever the little camera icon is clicked the: document.getElementById("new_image_upload").click(); will be called. For this to work you'll need to have this bit of html on your page:
After the images is uploaded it calls the addLineImage Function which simply adds the image to our content. That function looks like this:
You'll notice I wrap the image in a figure which make it a bit easier to do things like delete or drag the image elsewhere. So I actually need to setup a listener on the figure/image in order to delete the image if a user selects it and presses backspace or delete.
One very important concept to understand at this point is how to bind listeners to your content such as the image I just added. Initially I was tempted to just add a listener when I added the image in the above addLineImage. For example it's tempting do something like:
note: it is possible to add all the listeners to specific element when editing/reloading a page and you could take that approach. Initially I did take that approach, I basically loaded the html first then looped through all the elements. If the element was an image (or video) or something that needed to have an event listener I would add the event listener. I bailed on this approach after confusing myself and decided to just bind all the listeners to the document... Maybe I ended up taking the worse approach I don't know...
So rather than add listeners to the element I add listeners to the document. Here is an example of how I would listen for when a user wanted to delete an Image:
There were also some more listeners I setup on the image, for example I created the ability to drag/drop the image to a different spot. Also if an image was selected and a user presses enter, I wanted to add a new p tag and have the p tag be focused. If you want to see how that works, take a look at the full code here: https://www.codegrepper.com/app/js/writeup.js
Adding a video
When adding a video I decided to make my live easy for now and allow ability to add a video from youtube or vimeo. If you really want you could allow the users to upload a video to your server, but I decided to KISS (keep it simple stupid) for now and add that feature in later if needed. So for adding a video I just wanted to display a input box and allow the user to input a video url. Then when the user presses enter the video will be added to the content. The function looks like this:
Then to actually add the video:
Looks kinda like when I add the image. Again I wrap it in the figure element, this is allows me to add similar event listeners on the FIGURE tag. I basically want to treat a video like an image (until the end user needs to play the video)... You'll notice I call getIframeValueForVideo which does a little formatting on the users inputted video link. It should format youtube and vimeo (coming soon) videos src so it gets the correct embed link. That function looks like this:
Once the video is added I setup the same event listeners on the figure that holds the iframe that I did on the image. This gives the user the ability to delete the video if they want. Once caveat here was that I had to add a little css (pointer-events: none;) on the iframe so it would not start playing when clicked. This allows the user to click the video and delete it instead.
Adding Code Snippets
Adding ability to add code snippets was the same general process (with some additional complexities) as adding a video or image. I basically just added a textarea element instead of an image and then use CodeMirror https://codemirror.net/ to make the text area into the nice look code snippet. There are some complexities with loading the correct CodeMirror syntax formatting and stuff, but I'm not going to go into detail about that. Feel free to checkout code full code if your really interested: https://www.codegrepper.com/app/js/writeup.js
Saving and Loading the HTML
This was actually pretty easy. Because all my event listeners were at the document level, all I have to do is save the exact html on page and then load the html exactly how was stored in my database. My save function was basically as simple as this:
Then to reload the page for editing its as simple as :
Note: makeRequest is my hacky version of jquery's ajax function. Also getUrlParameter is my function to do just that. I keep these function in a little utility library that I use all over the place you can see it here: https://www.codegrepper.com/app/js/utils.js
A few tips on things you will run into.
This writeup is really just a kickoff to help others who may be trying to do a similar things or are running into similar problems. Some of these code samples I gave are stripped down to their bare bones to help others understand, additionally you would pry need to rework the code samples so they make sense in your environment.
That being said there a few pain point/spots I bet you will run into that I want to highlight.
- Focusing on contenteditable elements.
Focusing on "contenteditable" elements does not work how I would expect. It's not like focusing on input where the cursor actually starts blinking on the element. In order to get it to work correctly I had to use this function whenever I wanted to focus on an element:
2. The currently focused element is hard to access when adding listeners at the document level. I thought event.target would have the element that is focused (and on some elements it seems to work ok), but with contenteditable elements, the event.target was just always the document. In order to access the focused element I had to do this:
3. Add contenteditable to just one container element. The children don't all need that property.
4. Set tabindex="0" to pretty much all elements. This at least allows focusin/focusout events to fire with the correct event.target.
Final tip: I don't really know what I'm talking about... I just hacked away at this until things worked how I want. So take everything here with a grain of salt.
I hope this writeup can help others in some way. If you see issues, have feed back or just want to connect, hit me up on twitter @taylorhawkes or email me firstname.lastname@example.org.