diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 13737068d3..6fd6077b3a 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -58,6 +58,7 @@ Changelog
* Fix: Show not allowed extension in error message (Sahil Jangra)
* Fix: Fix focal point chooser when localization enabled (Sébastien Corbin)
* Fix: Ensure that system checks for `WAGTAIL_DATE_FORMAT`, `WAGTAIL_DATETIME_FORMAT` and `WAGTAIL_TIME_FORMAT` take `FORMAT_MODULE_PATH` into account (Sébastien Corbin)
+ * Fix: Prevent rich text fields inside choosers from being duplicated when opened repeatedly (Sage Abdullah)
* Docs: Remove duplicate section on frontend caching proxies from performance page (Jake Howard)
* Docs: Document `restriction_type` field on PageViewRestriction (Shlomo Markowitz)
* Docs: Document Wagtail's bug bounty policy (Jake Howard)
diff --git a/client/src/entrypoints/admin/draftail.js b/client/src/entrypoints/admin/draftail.js
index 6534c43c7a..950ff8c9f5 100644
--- a/client/src/entrypoints/admin/draftail.js
+++ b/client/src/entrypoints/admin/draftail.js
@@ -11,52 +11,59 @@ import draftail, {
* Entry point loaded when the Draftail editor is in use.
*/
-// Expose Draftail package as a global.
-window.Draftail = Draftail;
-// Expose module as a global.
-window.draftail = draftail;
+// This file is included and run when there's a DraftailRichTextArea widget in the response.
+// Normally this is only included once in the initial page load, but it may be included
+// more than once when there's an AJAX response that includes the widget, e.g. in choosers.
+// Ensure we only run the initialization code once.
+// https://github.com/wagtail/wagtail/issues/12002
+if (!window.Draftail || !window.draftail) {
+ // Expose Draftail package as a global.
+ window.Draftail = Draftail;
+ // Expose module as a global.
+ window.draftail = draftail;
-/** @deprecated RemovedInWagtail70 - Ensure that any third party packages that use global.chooserUrls can still append to this global object until support is removed. */
-window.chooserUrls = global.chooserUrls || {};
+ /** @deprecated RemovedInWagtail70 - Ensure that any third party packages that use global.chooserUrls can still append to this global object until support is removed. */
+ window.chooserUrls = global.chooserUrls || {};
-// Plugins for the built-in entities.
-const entityTypes = [
- {
- type: 'DOCUMENT',
- source: draftail.DocumentModalWorkflowSource,
- decorator: Document,
- },
- {
- type: 'LINK',
- source: draftail.LinkModalWorkflowSource,
- decorator: Link,
- onPaste: onPasteLink,
- },
- {
- type: 'IMAGE',
- source: draftail.ImageModalWorkflowSource,
- block: ImageBlock,
- },
- {
- type: 'EMBED',
- source: draftail.EmbedModalWorkflowSource,
- block: EmbedBlock,
- },
-];
+ // Plugins for the built-in entities.
+ const entityTypes = [
+ {
+ type: 'DOCUMENT',
+ source: draftail.DocumentModalWorkflowSource,
+ decorator: Document,
+ },
+ {
+ type: 'LINK',
+ source: draftail.LinkModalWorkflowSource,
+ decorator: Link,
+ onPaste: onPasteLink,
+ },
+ {
+ type: 'IMAGE',
+ source: draftail.ImageModalWorkflowSource,
+ block: ImageBlock,
+ },
+ {
+ type: 'EMBED',
+ source: draftail.EmbedModalWorkflowSource,
+ block: EmbedBlock,
+ },
+ ];
-entityTypes.forEach((type) => draftail.registerPlugin(type, 'entityTypes'));
+ entityTypes.forEach((type) => draftail.registerPlugin(type, 'entityTypes'));
-/**
- * Initialize a Draftail editor on a given element when the w-draftail:init event is fired.
- */
-document.addEventListener('w-draftail:init', ({ detail = {}, target }) => {
- const id = target.id;
+ /**
+ * Initialize a Draftail editor on a given element when the w-draftail:init event is fired.
+ */
+ document.addEventListener('w-draftail:init', ({ detail = {}, target }) => {
+ const id = target.id;
- if (!id) {
- // eslint-disable-next-line no-console
- console.error('`w-draftail:init` event must have a target with an id.');
- return;
- }
+ if (!id) {
+ // eslint-disable-next-line no-console
+ console.error('`w-draftail:init` event must have a target with an id.');
+ return;
+ }
- window.draftail.initEditor(`#${id}`, detail, document.currentScript);
-});
+ window.draftail.initEditor(`#${id}`, detail, document.currentScript);
+ });
+}
diff --git a/client/src/entrypoints/admin/draftail.test.js b/client/src/entrypoints/admin/draftail.test.js
index 2484e85740..5a22615f9d 100644
--- a/client/src/entrypoints/admin/draftail.test.js
+++ b/client/src/entrypoints/admin/draftail.test.js
@@ -62,7 +62,7 @@ describe('Calling initEditor via event dispatching', () => {
jest.resetAllMocks();
});
- it.only('should support creating a new editor with event dispatching', async () => {
+ it('should support creating a new editor with event dispatching', async () => {
expect(window.draftail.initEditor).not.toHaveBeenCalled();
document.body.innerHTML = '';
@@ -106,6 +106,59 @@ describe('Calling initEditor via event dispatching', () => {
afterAll(() => {
console.error.mockRestore();
window.draftail.initEditor.mockRestore();
- /* eslint-enable no-console */
+ });
+});
+
+describe('importing the module multiple times', () => {
+ it('should run the init function once if the script is included multiple times', async () => {
+ // Imported at the top level (similar to the initial page load)
+ const firstDraftail = window.draftail;
+
+ // Subsequent imports (e.g. in AJAX responses)
+ jest.isolateModules(() => {
+ require('./draftail');
+ });
+
+ // Should be the same instance
+ const secondDraftail = window.draftail;
+ expect(secondDraftail).toBe(firstDraftail);
+
+ jest.isolateModules(() => {
+ require('./draftail');
+ });
+
+ const thirdDraftail = window.draftail;
+ expect(thirdDraftail).toBe(firstDraftail);
+
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+ jest.spyOn(window.draftail, 'initEditor').mockImplementation(() => {});
+
+ expect(window.draftail.initEditor).not.toHaveBeenCalled();
+
+ document.body.innerHTML = '';
+
+ document.getElementById('editor').dispatchEvent(
+ new CustomEvent('w-draftail:init', {
+ bubbles: true,
+ cancelable: false,
+ detail: { some: 'detail' },
+ }),
+ );
+
+ expect(console.error).toHaveBeenCalledTimes(0);
+
+ // Should only be called once. If the script isn't written correctly, then
+ // the window.draftail object would be a new instance every time, or
+ // the initEditor function would be called multiple times.
+ expect(window.draftail.initEditor).toHaveBeenCalledTimes(1);
+ expect(window.draftail.initEditor).toHaveBeenLastCalledWith(
+ '#editor',
+ { some: 'detail' },
+ null,
+ );
+
+ /* eslint-enable no-console */
+ jest.clearAllMocks();
+ jest.restoreAllMocks();
});
});
diff --git a/docs/releases/6.2.md b/docs/releases/6.2.md
index bc2d93d6d3..791137f115 100644
--- a/docs/releases/6.2.md
+++ b/docs/releases/6.2.md
@@ -85,6 +85,7 @@ StreamField definitions within migrations are now represented in a more compact
* Show not allowed extension in error message (Sahil Jangra)
* Fix focal point chooser when localization enabled (Sébastien Corbin)
* Ensure that system checks for `WAGTAIL_DATE_FORMAT`, `WAGTAIL_DATETIME_FORMAT` and `WAGTAIL_TIME_FORMAT` take `FORMAT_MODULE_PATH` into account (Sébastien Corbin)
+ * Prevent rich text fields inside choosers from being duplicated when opened repeatedly (Sage Abdullah)
### Documentation