RichText behavior with plone.app.drafts

As far as I can tell, rich text fields are not included as part of the drafts saved by plone.app.drafts. I think what is happening is that drafts are created on the @@z3cform_validate_field request and this form data contains only the initial value of IRichTextBehavior.text. Looking at the DOM, the hidden textarea tag isn't actually updated as you edit in the wysiwyg editor - I guess TinyMCE does this only on save.

My use case is a non-Mosaic site where I'd like to save page drafts as a convenience. Especially because of SSO sites where the IdP may expire the user's session - existing form data will be lost in this case. The RichText field is the most important part to save as a draft!

What I think might be useful is to have a periodic bit of JS that parses the TinyMCE iframe body and injects this into the current draft if it exists. I don't want to rely on z3cform_validate_field because that only fires on focus change and it is quite likely that someone would stay within the wysiwyg area for a long time, making changes without that trigger firing. If anyone has experience with trying to do this in p.a.drafts I'd love to hear about it.

This actually looks fairly simple to do, assuming you are ok with a periodic check every X seconds (is there an event that would be relevant to listen to?). I have a proof of concept that is just a small javascript file and a small browser view.

$(document).ready(function() {
    setInterval(function() {
        try {
            $.ajax({
                url: '@@tinymce_draft',
                data: {"body": tinymce.activeEditor.getContent()},
                dataType: 'json'
            })
        }
        catch(err) {
            // no tinymce editor
        }
    }, 30000)
})
from Products.Five import BrowserView
from plone.app.drafts.utils import getCurrentDraft
from plone.app.textfield.value import RichTextValue
from plone.protect.interfaces import IDisableCSRFProtection
from zope.interface import alsoProvides


class TinymceDraft(BrowserView):
    def __call__(self, *args, **kwargs):
        body = self.request.get('body')
        current = getCurrentDraft(self.request, create=True)
        if current and body:
            current.text = RichTextValue(body)
        alsoProvides(self.request, IDisableCSRFProtection)

All the real work is done by plone.app.drafts, which handily gives us the ability to current draft and make additions to it. Note that I do not make any attempt to remove nasty tags or apply the Plone outputfilters. I do not think this is necessary because it will only be saved as a string in the db (never rendered as HTML in draft form) and this work will be done by Plone when the user actually saves the form data.

The major caveat again here is that you could lose up to X seconds of work (whatever you set interval to). I don't want to have a ridiculously large number of DB writes.

Nice insights, thanks! Never looked in detail into plone.app.drafts.

Might be worth a note here that @@z3cform_validate_field is completely gone in Plone 6 in favor of pat-validation and HTML5 form validation. So the autosave functionality of the drafts package needs some refactoring if somebody wants to have it in Plone 6.

plone.app.drafts is a dependency for Mosaic, which is how I found out about it. Really it's a dependency of plone.app.tiles which is a dependency of Mosaic. I haven't looked at how Mosaic integrates with Plone 6 at all but I assume it still needs drafts for tiles at least.

Note on the JS above: a better solution would be to use debounce so that it's not sending an XHR while nothing is changing.