Skip to content

Commit

Permalink
Merge pull request #179 from Ageu-Meireles/master
Browse files Browse the repository at this point in the history
Add support for dynamically loaded editors
  • Loading branch information
hvlads authored Mar 2, 2024
2 parents 091c209 + e9f6733 commit 1fecf8c
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 24 deletions.
98 changes: 79 additions & 19 deletions django_ckeditor_5/static/django_ckeditor_5/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,52 @@ function getCookie(name) {
return cookieValue;
}

function createEditors() {
const allEditors = document.querySelectorAll('.django_ckeditor_5');
for (let i = 0; i < allEditors.length; ++i) {
/**
* Checks whether the element or its children match the query and returns
* an array with the matches.
*
* @param {!HTMLElement} element
* @param {!string} query
*
* @returns {array.<HTMLElement>}
*/
function resolveElementArray(element, query) {
return element.matches(query) ? [element] : [...element.querySelectorAll(query)];
}

/**
* This function initializes the CKEditor inputs within an optional element and
* assigns properties necessary for the correct operation
*
* @param {HTMLElement} [element=document.body] - The element to search for elements
*
* @returns {void}
*/
function createEditors(element = document.body) {
const allEditors = resolveElementArray(element, '.django_ckeditor_5');

allEditors.forEach(editorEl => {
if (
allEditors[i].id.indexOf('__prefix__') !== -1 ||
allEditors[i].getAttribute('data-processed') === '1'
editorEl.id.indexOf('__prefix__') !== -1 ||
editorEl.getAttribute('data-processed') === '1'
) {
continue;
return
}
const script_id = `${allEditors[i].id}_script`;
allEditors[i].nextSibling.remove();
const upload_url = document.getElementById(
`${script_id}-ck-editor-5-upload-url`
const script_id = `${editorEl.id}_script`;
editorEl.nextSibling.remove();
const upload_url = element.querySelector(
`#${script_id}-ck-editor-5-upload-url`
).getAttribute('data-upload-url');
const csrf_cookie_name = document.getElementById(
`${script_id}-ck-editor-5-upload-url`
const csrf_cookie_name = element.querySelector(
`#${script_id}-ck-editor-5-upload-url`
).getAttribute('data-csrf_cookie_name');
const labelElement = document.querySelector(`[for$="${allEditors[i].id}"]`);
const labelElement = element.querySelector(`[for$="${editorEl.id}"]`);
if (labelElement) {
labelElement.style.float = 'none';
}

const config = JSON.parse(
document.getElementById(`${script_id}-span`).textContent,
element.querySelector(`#${script_id}-span`).textContent,
(key, value) => {
var match = value.toString().match(new RegExp('^/(.*?)/([gimy]*)$'));
if (match) {
Expand All @@ -58,29 +80,67 @@ function createEditors() {
}
};
ClassicEditor.create(
allEditors[i],
editorEl,
config
).then(editor => {
if (editor.plugins.has('WordCount')) {
const wordCountPlugin = editor.plugins.get('WordCount');
const wordCountWrapper = document.getElementById(`${script_id}-word-count`);
const wordCountWrapper = element.querySelector(`#${script_id}-word-count`);
wordCountWrapper.innerHTML = '';
wordCountWrapper.appendChild(wordCountPlugin.wordCountContainer);
}
editors.push(editor);
}).catch(error => {
console.error((error));
});
allEditors[i].setAttribute('data-processed', '1');
}
editorEl.setAttribute('data-processed', '1');
});

window.editors = editors;
window.ClassicEditor = ClassicEditor;
}

/**
* This function filters the list of mutations only by added elements, thus
* eliminates the occurrence of text nodes and tags where it does not make sense
* to try to use with `QuerySelectorAll()` and `matches()` functions.
*
* @param {MutationRecord} recordList - It is the object inside the array
* passed to the callback of a MutationObserver.
*
* @returns {Array} Array containing filtered nodes.
*/
function getAddedNodes(recordList) {
return recordList
.flatMap(({ addedNodes }) => Array.from(addedNodes))
.filter(node => node.nodeType === 1);
}

document.addEventListener("DOMContentLoaded", () => {
createEditors();

if (typeof django === "object" && django.jQuery) {
django.jQuery(document).on("formset:added", createEditors);
}
});

const observer = new MutationObserver((mutations) => {
let addedNodes = getAddedNodes(mutations);

addedNodes.forEach(node => {
// Initializes editors
createEditors(node);
});
});

// Configure MutationObserver options
const observerOptions = {
childList: true,
subtree: true,
};

// Selects the parent element where the events occur
const mainContent = document.body;

// Starts to observe the selected father element with the configured options
observer.observe(mainContent, observerOptions);
});
22 changes: 21 additions & 1 deletion example/blog/articles/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from django_ckeditor_5.widgets import CKEditor5Widget

from .models import Comment
from .models import Comment, Article


class CommentForm(forms.ModelForm):
Expand All @@ -21,3 +21,23 @@ class Meta:
config_name="comment",
),
}


class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ['title', 'text']
widgets = {
"text": CKEditor5Widget(
attrs={"class": "django_ckeditor_5"},
config_name="comment",
),
"text2": CKEditor5Widget(
attrs={"class": "django_ckeditor_5"},
config_name="comment",
),
}

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["text"].required = False
11 changes: 11 additions & 0 deletions example/blog/articles/templates/articles/article_create.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% block header %}
{{ form.media }}
{% endblock %}
{% block content %}
<form method="POST">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit article">
</form>
{% endblock %}
29 changes: 28 additions & 1 deletion example/blog/articles/templates/articles/article_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,31 @@ <h1>Articles</h1>
<li>No articles yet.</li>
{% endfor %}
</div>
{% endblock %}
<div>
<button onclick="create_article()">Create article</button>
</div>
<div id="dynamicEditor"></div>

{# scripts to handle editor behavior #}
{{media}}
<script>
const wrapper = document.getElementById('dynamicEditor');

function create_article(){
fetch("{% url 'get-editor' %}")
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.text();
})
.then(html => {
wrapper.innerHTML = html;
})
.catch(error => console.error('Request error:', error));
}
function dispose(){
wrapper.innerHTML = "";
}
</script>
{% endblock %}
13 changes: 13 additions & 0 deletions example/blog/articles/templates/articles/dynamic_editor.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<div style="position: absolute;backdrop-filter: blur(10px);top: 0;bottom: 0;left: 0;right: 0;padding-inline: 100px;padding-block: 50px;">
<h1>Create Article</h1>
<form action="{% url 'article-create' %}" method="post" id="createArticleForm"
enctype="multipart/form-data"
>
{% csrf_token %}
{{ form.as_p }}
<button onclick="dispose()" id="cancelBtn" style="padding: 10px;">
Cancel
</button>
<input type="submit" value="submit" style="float: right;padding: 10px;">
</form>
</div>
6 changes: 5 additions & 1 deletion example/blog/articles/urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from django.urls import path

from .views import ArticleDetailView, ArticleListView
from .views import (
ArticleDetailView, ArticleListView, ArticleCreateView, GetEditorView
)

urlpatterns = [
path("", ArticleListView.as_view(), name="article-list"),
path("<int:pk>/", ArticleDetailView.as_view(), name="article-detail"),
path("get_editor", GetEditorView.as_view(), name="get-editor"),
path("create_article/", ArticleCreateView.as_view(), name="article-create"),
]
25 changes: 23 additions & 2 deletions example/blog/articles/views.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.views.generic import FormView
from django.views.generic import CreateView, FormView, TemplateView
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView

from .forms import CommentForm
from .forms import CommentForm, ArticleForm
from .models import Article


Expand All @@ -13,6 +13,9 @@ class ArticleListView(ListView):

model = Article
paginate_by = 100
extra_context = {
'media': CommentForm().media
}


class ArticleDetailView(DetailView, FormView):
Expand All @@ -35,3 +38,21 @@ def post(self, request, *args, **kwargs):
comment.save()
success_url = reverse("article-detail", kwargs={"pk": self.get_object().id})
return HttpResponseRedirect(success_url)


class ArticleCreateView(CreateView):
""" Article create view """

model = Article
form_class = ArticleForm
template_name = "articles/article_create.html"

def get_success_url(self):
return reverse('article-list')


class GetEditorView(TemplateView):
template_name = 'articles/dynamic_editor.html'
extra_context = {
'form': ArticleForm()
}

0 comments on commit 1fecf8c

Please sign in to comment.