Open philgyford opened 7 months ago
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!
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.
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!
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.
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 <p>hello</p>
.
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.
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:
And then this in my template:
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?