Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using django-tinymce in inline editing mode #463

Open
philgyford opened this issue Apr 10, 2024 · 5 comments
Open

Using django-tinymce in inline editing mode #463

philgyford opened this issue Apr 10, 2024 · 5 comments

Comments

@philgyford
Copy link

The django-tinymce documentation doesn't mention using it for inline editing mode. The only issue I've found about it (#135) was closed as "resolved" but with no info. Is it possible to use it for this purpose?

I feel like I've got close, but it requires some custom JavaScript, and it doesn't pick up any of the django-tinymce settings from settings.py.

I have a Django form:

class TestModelInlineUpdateForm(forms.ModelForm):
    class Meta:
        model = TestModel
        fields = ("description",)
        widgets = {"description": TinyMCE()}

And then this in my template:

  <form method="post" id="testForm">
    {% csrf_token %}
    {{ form.media }}

    <div class="tinymce" id="inline-editor">
      {{ object.description|safe }}
    </div>

    {% for hidden in form.hidden_fields %}
      {{ hidden }}
    {% endfor %}

    <input type="hidden" name="description" value="">

    <button type="submit">Update</button>
  </form>

  <script type="text/javascript">
    tinymce.init({
      selector: '#inline-editor',
      inline: true
    });

    // Set the description field to the new HTML on form submission.
    document.querySelector("#testForm").addEventListener("submit", function(ev) {
      ev.preventDefault();
      let descriptionHTML = document.querySelector("#inline-editor").innerHTML;
      let descriptionField = document.querySelector("input[name='description']");
      descriptionField.value = descriptionHTML;
      this.submit();
    });
  </script>

I have to manually copy the HTML from the editor to my manually-added hidden form field when the form is submitted. It works, but is clunky. And, as I say, django-tinymce's settings are ignored.

Have I missed an easier, better way?

@philgyford
Copy link
Author

I should add, if there is a good way to do it, I'll happily do a PR to add the info to the docs!

@philgyford
Copy link
Author

Now I've come back to this I realise that the only django-tinymce thing my form is using is the static files, in {{ form.media }}. Everything else is standard TinyMCE.

My aim is to be able to use the same config for both standard and inline TinyMCE editors, and to use django-filebrowser for uploading/browsing images in both kinds.

@philgyford
Copy link
Author

I've made good progress. I've scrapped all the above and created my own widget, based off TinyMCE, to create and initialise a <div> as an inline TinyMCE element. Clicking it, it becomes an editable field, with my django-tinymce config, and the django-filebrowser upload button working.

My widgets.py:

import json
import tinymce
from django.core.serializers.json import DjangoJSONEncoder
from django.forms.utils import flatatt
from django.utils.safestring import mark_safe
from tinymce.widgets import TinyMCE

class TinyMCEInline(TinyMCE):
    def render(self, name, value, attrs=None, renderer=None):
        """
        A duplicate of TinyMCE.render() with one line added and one
        line changed.
        """
        if value is None:
            value = ""
        final_attrs = self.build_attrs(self.attrs, attrs)
        final_attrs["name"] = name
        if final_attrs.get("class", None) is None:
            final_attrs["class"] = "tinymce"
        else:
            final_attrs["class"] = " ".join(
                final_attrs["class"].split(" ") + ["tinymce"]
            )
        assert "id" in final_attrs, "TinyMCE widget attributes must contain 'id'"
        mce_config = self.get_mce_config(final_attrs)

        # NEW LINE #####################################################
        mce_config["inline"] = True

        mce_json = json.dumps(mce_config, cls=DjangoJSONEncoder)
        if tinymce.settings.USE_COMPRESSOR:
            compressor_config = {
                "plugins": mce_config.get("plugins", ""),
                "themes": mce_config.get("theme", "advanced"),
                "languages": mce_config.get("language", ""),
                "diskcache": True,
                "debug": False,
            }
            final_attrs["data-mce-gz-conf"] = json.dumps(compressor_config)
        final_attrs["data-mce-conf"] = mce_json

        # CHANGED LINE #################################################
        # CHANGED TEXTAREA TO DIV AND REMOVED escape()
        html = [f"<div{flatatt(final_attrs)}>{value}</div>"]

        return mark_safe("\n".join(html))

    def value_from_datadict(self, data, files, name):
        """
        TinyMCE submits the hidden field it generates using a name of
        "id_{name}". So we need to get the data using that, instead of
        our actual field name.
        """
        return data.get(f"id_{name}")

My form:

from django import forms
from .models import TestModel
from .widgets import TinyMCEInline

class TestModelInlineUpdateForm(forms.ModelForm):
    class Meta:
        model = TestModel
        fields = ("description",)
        widgets = {"description": TinyMCEInline()}

And the relevant part of my template:

  <form method="post">
    {% csrf_token %}
    {{ form.media }}
    {{ form }}

    <button type="submit">Update</button>
  </form>

I've only tried this with my one model, form and field, so there may well be cases in which it doesn't work, or could be better.

Having to duplicate the entire TinyMCE.render() method, only to add/change a couple of lines, isn't great. Some tweaks to the original widget could avoid all that duplication – if this approach seems good I'd be happy to try a PR, but any thoughts appreciated!

@marius-mather
Copy link
Contributor

Based on your changes, it looks like you could just set "inline": True in your TinyMCE configuration (e.g. in settings.py), and then in TinyMCE.render() the HTML could be changed to:

if mce_config["inline"]:
    html = [f"<div{flatatt(final_attrs)}>{value}</div>"]
else:
    html = [f"<textarea{flatatt(final_attrs)}>{escape(value)}</textarea>"

Not escaping the HTML would have slightly different security implications though, so it might be good to document that if you're setting inline to True you should only use it for trusted sources of HTML.

@philgyford
Copy link
Author

Based on your changes, it looks like you could just set "inline": True in your TinyMCE configuration (e.g. in settings.py)…

Yes, true. In my case this was a secondary use, as well as my default, but I could pass mce_attrs{"inline": True} in as an arg to TinyMCE().

Not escaping the HTML would have slightly different security implications though…

Yes, it's a shame about that but it seems necessary or else the HTML within the <div> is like &lt;p&gt;hello&lt;/p&gt;.

I can't see a way around needing the custom value_from_datadict(), which feels annoyingly fiddly. I guess it could be:

    def value_from_datadict(self, data, files, name):
        if name in data:
            return data.get(name)
        else:
            return data.get(f"id_{name}")

? Ideally there would be a way for that method to tell if mce_config["inline"] is True, and use that for the logic, but having had a quick look I'm not sure there is a way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants