256 Kilobytes

[Solved] CKEditor Autosave Draft Functionality - 1.4KB, No Plugin

Articles in Client-Side | By August R. Garcia

Published | Last Update

Like using a plugin but easier.

1,143 view, 1 RAM, and 1 comment

Automatically saving user drafts of comments and other data started by users on a website is useful for obvious reasons.

Code to Autosave Drafts with CKEditor

When using CKEditor, it seems that this functionality is not built-in by default. While there are plugins that can do this, such as this AutoSave Plugin for CKEditor, the download is 96.4 KB compressed as a .zip and the main uncompressed plugin.js file is 16.8 KB by itself. For basic draft saving functionality, here is 1.4 KB of minified code that does basically what you want.

function init_ckeditors(){ckes=[],CKEDITOR.on("instanceReady",function(e){init_ckeditor(e.editor),ckes.push(e.editor)}),window.onbeforeunload=function(){for(iii=0;iii<ckes.length;++iii)1==ckes[iii].has_been_modified&&ckes[iii].save_draft()}}function init_ckeditor(e){e.form=get_cke_parent_form(e.id),e.key=get_ckeditor_draft_key(e.form),e.has_been_modified=!1,e.save_draft=save_ckeditor_draft;let t=localStorage.getItem(e.key);null!==t&&e.setData(t),e.on("blur",function(){this.save_draft()}),e.on("focus",function(){this.save_draft()}),e.form&&e.form.addEventListener("submit",function(t){localStorage.removeItem(e.key)})}function save_ckeditor_draft(){this.has_been_modified=!0,localStorage[this.key]=this.getData()}function get_ckeditor_draft_key(e){return"CKEditor Draft - URL w/o Params: "+window.location.href.split("?")[0]+" - Form Identifiers: "+get_form_identifiers(e)}function get_cke_parent_form(e){let t=document.querySelector("."+e+".cke.cke_editor_body[role='application']");for(;null!=t&&"FORM"!==t.nodeName;)t=t.parentNode;return t}function get_form_identifiers(e){if(!e)return"CKEditor Has No Form";let t=e.querySelectorAll("*[name"),i=[];for(let e=0;e<t.length;++e)i.push({tagName:t[e].tagName,name:t[e].getAttribute("name")});return JSON.stringify({name:e.getAttribute("name"),action:e.getAttribute("action"),method:e.getAttribute("method"),fields:i})}

And then to run the initialization for the draft-saving functionality, use:

// Call the init function to run the code. 

Full Commented Code for Autosaving CKEditor Drafts

Here's the same code as above commented and in non-minified form, which is 6.8 KB total. 

// =============================================== // 
// ===== ===== CKEditor Init Functions ===== ===== // 
// =============================================== // 
// Intialize CKEditor Instances 
function init_ckeditors() {
        ckes = [];
        CKEDITOR.on('instanceReady', function( evt ){ 

        // When the page unloads, save all drafts that were modified  
        window.onbeforeunload = function(){
                for (iii = 0; iii < ckes.length; ++iii) {
                        if (ckes[iii].has_been_modified == true)

// Initializes a single CKEditor's draft functionality. Called from init_ckeditors()
function init_ckeditor(cke) {

        // Add various attributes and methods to the CKEditor instance 
        cke.form              = get_cke_parent_form(    cke.id   );
        cke.key               = get_ckeditor_draft_key( cke.form );
        cke.has_been_modified = false                             ;
        cke.save_draft        = save_ckeditor_draft               ;

        // If there is an existing draft, put it into the editor 
        let draft = localStorage.getItem(cke.key); 
        if (draft !== null) {
                cke.setData( draft );

        // If the editor loses or gains focus, then save the draft 
        // Drafts are also saved when the user closes the page via the onbeforeunload 
        // function in init_ckeditor()  
        // Documentation on binding functions to CKEditor: 
        // https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_editor.html
        cke.on(  'blur'  , function() { this.save_draft(); }  );
        cke.on(  'focus' , function() { this.save_draft(); }  ); 

        // Saving the draft on every change is potentially excessive/performance intensive
        // but can be done by uncommenting this line, if desired. 
        //cke.on(  'change', function() { this.save_draft(); }  );

        // ===================== // 
        // ===== ON SUBMIT ===== // 
        // ===================== // 
        // Set on-submit function to clear saved drafts on submit of the parent form, 
        // if a parent form exists. 
        // Presumably, most if not all CKEditor instances will be within <form> elements 
        // Assuming that a form was found, bind an onsubmit function that clears saved
        // drafts on submit. 
        // If the editor is not--for whatever reason--within a <form> tag, 
        // no onsubmit function is bound.  
        // Note: If the form is submitted, but the form submit fails on the server-side, then 
        //       the draft will still be removed from localStorage. 
        //       When the back button is hit, the draft should still be restored by the 
        //       browser (at least in Chrome). 
        if (cke.form) { 
                cke.form.addEventListener("submit", function(event) {

// Additional method bound to CKEditor instances.
// Uses a unique key to store drafts for different URLs as well as multiple 
// instances on the same page. 
function save_ckeditor_draft() {  
        this.has_been_modified  = true         ;
        localStorage[this.key] = this.getData(); 

// Get the key used in localStorage to reference a particular editor's saved draft. 
// To create a unique draft ID, this function uses: 
// * The URL of the current page, without params. 
//   --> This allows the cache to ignore ?page=2 
//       and so on, for example if a forum thread spans multiple threads. 
// * If applicable, data from the <form> tag and it's child elements. Specifically: 
//   * The <form> tag's name="", action="", and method="" attributes. 
//   * The tag name and name="" attributes of all <input>, <textarea>, and other 
//     elements within the 
//     <form> tag that have a name="" attribute (e.g., <select> tags and so on). 
//   --> These allow the cache to make a reasonably accurate guess about whether; ex: a form  
//       on ?page=2 should be treated as the same form shown on ?page=1/3/69/420 
// Note: The CKEditor instance's ID cannot be used reliably, because it is autogenerated and 
//       is based on the number of CKEditors on a page. If, for example, there are separate  
//       forms to edit every forum comment that exists on a page, these will be 
//       inconsistent between pages that have different numbers of comments.
// Format is: "CKEditor Draft - [URL without Params] - [<form> and child element info]" 
// Ignores any query parameters, 
function get_ckeditor_draft_key(form) {
        let url_no_params = window.location.href.split('?')[0];
        let form_info     = get_form_identifiers(form);
        return "CKEditor Draft - URL w/o Params: " + url_no_params + " - Form Identifiers: " + form_info;

// Helper function. 
// Select the CKEditor by its ID and then loop up through its parent node until a <form>
// is found or until all parent nodes have been checked. 
function get_cke_parent_form( cke_id ) {
        // CKEditor instances' ID attribute aren't actually used as id="" HTML attributes; 
        // they are used as an additional class instead. 
        let node = document.querySelector("."+cke_id+".cke.cke_editor_body[role='application']"); 

        while ( node != null  &&  node.nodeName !== "FORM" ) {
                node = node.parentNode;
        } return node; 

// As discussed in regards to get_ckeditor_draft_key(), it is common for the same form to be  
// shown on multiple pages of a website. For example, a forum thread may have 
// Consider the following 
function get_form_identifiers( form ) {
        if (!form) return "CKEditor Has No Form";

        let fields = form.querySelectorAll("*[name");
        let field_objs = [];  
        for (let iii = 0; iii < fields.length; ++iii) { 
                        "tagName" : fields[iii].tagName              ,
                        "name"    : fields[iii].getAttribute("name")

        return JSON.stringify({
                "name"   : form.getAttribute("name"  ) ,
                "action" : form.getAttribute("action") ,
                "method" : form.getAttribute("method") ,
                "fields" : field_objs

In conclusion, very nice. Feel free to use and/or modify this code for your project or other purposes. 

Users Who Have Downloaded More RAM:
Hash Brown (2 years ago)
🐏 ⨉ 1
Posted by August R. Garcia 2 years ago

Edit History

• [2019-11-23 10:25 PST] August R. Garcia (2 years ago)
• [2019-11-23 10:25 PST] August R. Garcia (2 years ago)
• [2019-11-23 10:25 PST] August R. Garcia (2 years ago)
🕓 Posted at 23 November, 2019 10:25 AM PST

Profile Photo - August R. GarciaAugust R. GarciaLARPing as a Sysadmi...Portland, ORSite Owner

August Garcia is some guy who used to sell Viagra on the Internet. He made this website to LARP as a sysadmin while posting about garbage like user-agent spoofing, spintax, the only good keyboard, virtual assitants from Pakistan, links with the rel="nofollow" attributeproxiessin, the developer console, literally every link building method, and other junk.

Available at arg@256kilobytes.com, via Twitter, or arg.256kilobytes.com. Open to business inquiries based on availability.

Profile Photo - August R. GarciaAugust R. GarciaLARPing as a Sysadmi...Portland, ORSite Owner

PS: If you're wondering, this code is not yet live on 256 Kilobytes; will be in the next patch.

Edit: There's a bug in the original code related to an onblur (?) function overwriting the deleted cache key if it is handled after the onsubmit event, which can happen. Will post updated code at some point, unless I forget.

Users Who Have Downloaded More RAM:
Hash Brown (2 years ago)
🐏 ⨉ 1
Posted by August R. Garcia 2 years ago

Edit History

• [2019-11-23 10:27 PST] August R. Garcia (2 years ago)
🕓 Posted at 23 November, 2019 10:27 AM PST

Sir, I can do you a nice SEO.

Post a New Comment

Do you like having a good time?

Register an Account

You can also login to an existing account or reset your password. All use of this site is subject to the terms of service and privacy policy.

Read Quality Articles

Read some quality articles. If you can manage to not get banned for like five minutes, you can even post your own articles.

View Articles →

Argue with People on the Internet

Use your account to explain why people are wrong on the Internet forum.

View Forum →

Vandalize the Wiki

Or don't. I'm not your dad.

View Wiki →

Ask and/or Answer Questions

If someone asks a terrible question, post a LMGTFY link.

View Answers →

Make Some Money

Hire freelancers and/or advertise your goods and/or services. Hire people directly. We're not a middleman or your dad. Manage your own business transactions.

Register an Account