What I’ve Learned Building a Browser-Based Text Editor

After seeing Plume, I wanted to build a text editor as well. Unfortunately, my programming skills for native applications don't exist. A project like that would be a great exercise if I'd had either some experience or more time. I got neither. But I still wanted to build a text editor.

The only remaining option I had was to use HTML, CSS, and JavaScript and do it in the browser. So I did.

I still wanted to learn something, leave my comfort zone, and challenge myself. That's why I didn't use any dependencies, only pure vanilla technologies. This way, I should learn something, right?

Before

At first, I was ambitious. I wanted Markdown support, a VIM mode, and the ability to have more than one file and be able to download said files. There should also be a possibility to share a file and more. I planned to add everything without requiring a server or a database.

I realized I was delusional and would never finish a project with these requirements. So I slimmed it down. The only requirements left were supporting Markdown and storing changes locally.

So here's what I've learned writing Kurz.

JSDoc

JSDoc helps by having at least some form of a type system in JS without needing a dependency. With it, you can write comments above functions, declaration of variables, and such. These comments define a variable type or function parameters and the return type.

One could argue that JSDoc makes the code harder to read, but I object. When returning to the project after a break, these comments help a lot.

For example, it allowed me to only look at a function's comment without reading through the body. This way, I was back to understanding my code faster than without it.

As this project was small, it was a good choice. There was no need to waste time setting up and configuring TypeScript. A few comments and that's it. You'll get used to it quickly.

IndexedDB

As mentioned above, I wanted to save everything on the user's machine. For that, modern browsers offer some APIs. These include Web Storage API with session- and localStorage and IndexedDB.

sessionStorage doesn't work for this use case. localStorage would be fine, as the only limitation is the storage size of up to 10MB. Depending on the browser. But I went with IndexedDB.

IndexedDB is a database inside the browser. I've never worked with it, and, as mentioned above, I intended to support more than one file. With how the project pivoted, it might no longer be the right choice.

Working with it is unpleasant and, compared to localStorage, way more effort using it. You have to take care of many events and points of failure. That's a lot of work only to store one long string.

Selection and Range

I've also never interacted with selections and ranges. I wasn't even aware there was a way to do so. But it was the perfect solution for placing the cursor after parsing Markdown. It simplified escaping HTML elements, like i or strong tags. I'm not sure if it would've been possible without it.

Performance

I've never tried to write performant JavaScript before. All my projects until this point didn't need that. But when working on this project, I tried to keep performance in the back of my head. Because when using an editor, you want it to be snappy. If the editor can't keep up with your actions, it's not worth using.

For example, I avoided using the forEach() method. The reason is that it copies the whole array, thus costing performance. I also tried not to traverse the editor's HTML tree when parsing Markdown. There's likely a lot of room for optimization left, though.

Firefox

It doesn't work like that in Firefox.

That was always my second prompt when asking an LLM for help.

Librewolf, a Firefox fork, is my browser of choice. I prefer it to the alternatives. That's why I'm also using it when developing for the web. But man, what the hell. There were so many points when I wrote valid code, and it didn't work, making me doubt myself.

The following points happen inside a contenteditable parent element:

The sad part is that Firefox and its quirks forced me to find better solutions each time. This way, improving my code and 'helping me' support all browsers.

keyup and keydown

The last thing worth noting is the difference between keyup and keydown events.

preventDefault() on the keydown event stops the browser from adding an element on enter. The keyup event doesn't handle said element insertion. Before finding that out, I thought I had to use div tags instead of semantic HTML.

Only keydown will have properties, like ctrlKey, as true when the button causes the event. The keyup event won't have the property as true, even though the key will display it.

Firefox will not fire said events when pressing a button like control by itself.

After

As of writing this, the JavaScript weights around 17kB, including JSDoc comments. I'm fascinated by how much is possible with so little code. JavaScript gets a lot of hate. But when using it for some simple DOM manipulation, it's great.

Doing this was a great project to explore some unknowns. Is it bug-free? Unlikely. Do I care? A little bit.

I like projects like this one. I can use them afterward and dogfood them. I can find problems and bugs and resolve them. Projects like that are my favorites.