Auto Save in CKEditor5

June 19, 2022
blog-engine ckeditor

As part of the Blog Engine I think we need some auto-save superpowers. How do I know? Because I has about 800 words written about my “what I use now” and Windows 11 reinstall when I decided ot close the browser to install Node and when I came back I started to cry. I lost everything. And unhonestly, it was some of my wittiest, funniest content. So, the next thing I had to do was implement autosave in this Blog Engine of 1.

But I also knew that I am using CKEditor and they def have a plugin for this so… I go to WYSIWYG HTML Editor with Collaborative Rich Text Editing (ckeditor.com). Since I built one using their online builder, I had to do it again. Which isn't awful so I'd get the latest version. Added a bunch of things I am not sure I needed. But Autosave is one of them., Which gives you an easy place to provide a JS function when the editor changes. Perfect. Next, next, build. Download. Extract. Copy. Paste. Run. Errors. Fuck

I'll save you the drama and all the things I did. But basically, if I understand this correctly (because I am not sure I do - cause it's dumb) - there are some plugins that require you to install them via node and build them as part of your project. WTF. I hope I misunderstood and just could not get it to work after 30 minutes (or 60). 

Commercial Break

I realize that I am using a free product that's supported by a commercial product. My right to complain is limited considering I am the free user and not the paid user. However, why would I build out this custom editor environment and then require me to implement a build workflow that needs NPM - I hate NPM. I especially hate npm/node today when one of my projects refuses to build because of node-sass.

Back to the Show

Screw it. I'll build my own. C'mon this can only be a few lines of additional code anyway since I was going to have to implement my own strategy for auto-saving content. And it was. And here it is:

Let's tell ou editor to tell us when things change. I am using the watchdog thing, so this is in the setCreator:

watchdog.setCreator((element, config) => {
            return CKSource.Editor
                .create(element, config)
                .then(editor => {
                    document.getElementById("editor_one").style.display = "block";
                    editor.model.document.on('change:data', autosave);
                    return editor;
                })
        });

The new piece being:

editor.model.document.on('change:data', autosave);

This calls autosave() on every change. You may want to implement some logic to determine if you should or shouldn't do anything.

function autosave() {

  var data = watchdog.editor.getData();
  if (!posttitle) {
    if (document.getElementById("Post_Title").value) {    
      posttitle = document.getElementById("Post_Title").value;
    }  
  }
  
  if (posttitle && data) {
    localStorage.setItem(`POST_AUTOSAVE_${posttitle.toLowerCase()}`, JSON.stringify({ title: posttitle, content: data }));
  }
}

This is a bit rudimentary and probably could come up with a better strategy - but it took < 30 minutes :). Let's break it down:

  1. Get data from the editor
  2. posttitle is the post's title. We only save the content if we have a title. We use this as the key for local storage, so we need one
  3. if we have a posttitle and we have data (content)
  4. we save the content to localstorage using that title as a suffix to POST_AUTOSAVE_ as a key.

That saves our data. This is a bit aggressive. We should probably add a debounce or a timer (instead of “on change”). This basically overwrites the localStorage on every key up. This could be ideal for CRITICAL saving of data. Maybe.

So that's cool. We have the data in localStorage. Now we need to use it.

I have a Draft?

When the page loads, it checks localStorage for anything that starts with POST_AUTOSAVE_ and adds a button that says Drafts(x) where x is the number of drafts found (or autosaves based on title). Clicking the Drafts buttons displays all the titles. Clicking the button will reload the title and content.

Here's the complete code:

var posttitle
var drafts = [];
var keys;

function checkAutoSaves() {
    keys = Object.keys(localStorage);
    for (var i = 0; i < keys.length; i++) {
        if (keys[i].startsWith("POST_AUTOSAVE_")) {
            console.log("YES:", keys[i]);

            var elem = document.createElement("button")
            elem.innerHTML = keys[i].substring(14);
            elem.classList.add("btn");
            elem.classList.add("btn-outline-primary");
            elem.classList.add("me-1");
            elem.setAttribute("onclick", "loadPost(" + i + ")")
            document.getElementById("draft_list").appendChild(elem);
        }
    }

    if (document.getElementById("draft_list").children) {
        document.getElementById("draft_count").innerHTML = `Drafts (${document.getElementById("draft_list").children.length})`;
        document.getElementById("draft_count").style.display = "inline-block";

    }
}

function showDrafts() {
    document.getElementById("drafts").style.display = "block";
    document.getElementById("rich").style.display = "none";
}
function loadPost(index) {

    var data = JSON.parse(localStorage.getItem(keys[index]));
    document.getElementById("Post_Title").value = data.title;
    watchdog.editor.setData(data.content);

                document.getElementById("drafts").style.display = "none";
    document.getElementById("rich").style.display = "block";

}

checkAutoSaves();

And there we have it. There are a few more things to think through. Deleting old ones, change sin title. etc. I'll get to that.