From db87c5812c218a8c719ce964c764829604c614bd Mon Sep 17 00:00:00 2001 From: Peter Mathis Date: Mon, 20 Jul 2026 09:50:34 +0200 Subject: [PATCH] fix(pat-tinymce): construct the modal implementation for the link modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pat-plone-modal lazification left links.js creating its modal via registry.patterns["plone-modal"].init(...). That static initializer constructs the *registered* pattern — now the thin registration module, whose methods (show/hide/...) only appear after an async graft — while LinkModal calls this.modal.show() synchronously right after construction. First click on Insert Link/Image threw and the modal stayed broken, which also failed every robot test funneling through it (tinymce, contentbrowser, linkintegrity). Construct modal--implementation directly instead; mockupParser reproduces the option parsing of the registry path. links.js is only reached via the lazily-loaded tinymce implementation, so the modal body stays out of the eager bundle. Add a regression test asserting the implementation exposes its methods synchronously. --- src/pat/modal/modal--implementation.test.js | 23 +++++++++++ src/pat/tinymce/js/links.js | 44 +++++++++++++-------- 2 files changed, 51 insertions(+), 16 deletions(-) create mode 100644 src/pat/modal/modal--implementation.test.js diff --git a/src/pat/modal/modal--implementation.test.js b/src/pat/modal/modal--implementation.test.js new file mode 100644 index 000000000..792c6d84b --- /dev/null +++ b/src/pat/modal/modal--implementation.test.js @@ -0,0 +1,23 @@ +import $ from "jquery"; +import Modal from "./modal--implementation"; + +// The tinymce link modal (pat-tinymce js/links.js) constructs the modal +// imperatively (`new Modal(...)`) and calls show() synchronously right after. +// The implementation must therefore be a constructable pattern that exposes +// its methods synchronously — not a config object grafted asynchronously by +// the thin modal.js registration module. +describe("modal implementation", function () { + let $el; + beforeEach(function () { + $el = $("
").appendTo("body"); + }); + afterEach(function () { + $el.remove(); + }); + + it("is constructable and exposes its methods synchronously", function () { + const modal = new Modal($el, { html: "
content
" }); + expect(typeof modal.show).toBe("function"); + expect(typeof modal.hide).toBe("function"); + }); +}); diff --git a/src/pat/tinymce/js/links.js b/src/pat/tinymce/js/links.js index 70f7b1b10..cc4ad3a9b 100644 --- a/src/pat/tinymce/js/links.js +++ b/src/pat/tinymce/js/links.js @@ -1,12 +1,16 @@ import Base from "@patternslib/patternslib/src/core/base"; import events from "@patternslib/patternslib/src/core/events"; -import registry from "@patternslib/patternslib/src/core/registry"; +import mockupParser from "@patternslib/patternslib/src/core/mockup-parser"; import $ from "jquery"; import _ from "underscore"; import tinymce from "tinymce/tinymce"; import "../../autotoc/autotoc"; -import "../../modal/modal"; +// The full modal implementation, not the thin registration module: the modal +// instance is used synchronously (show() right after construction), so its +// methods must live on the prototype. This module is only reached via the +// lazily-loaded tinymce implementation, so it stays out of the eager bundle. +import Modal from "../../modal/modal--implementation"; import ImageTemplate from "../templates/image.xml"; import LinkTemplate from "../templates/link.xml"; @@ -408,21 +412,29 @@ export default Base.extend({ this.dom = this.tiny.dom; this.linkType = this.options.initialLinkType; this.linkTypes = {}; - this.modal = registry.patterns["plone-modal"].init(this.$el, { - html: this.generateModalHtml(), - content: null, - buttons: ".plone-btn", - reloadWindowOnClose: false, - templateOptions: { - classDialog: "modal-dialog modal-lg", + // Construct the implementation class directly — its static init() + // (like registry.patterns["plone-modal"].init) would instantiate the + // registered thin pattern, whose methods only appear after an async + // graft, while show() is called synchronously right after this. + // mockupParser reproduces the option parsing of the registry path. + this.modal = new Modal( + this.$el, + mockupParser.getOptions(this.$el, "plone-modal", { + html: this.generateModalHtml(), + content: null, + buttons: ".plone-btn", reloadWindowOnClose: false, - }, - actionOptions: { reloadWindowOnClose: false }, - backdropOptions: { - zIndex: "1340", - closeOnClick: false, - }, - }); + templateOptions: { + classDialog: "modal-dialog modal-lg", + reloadWindowOnClose: false, + }, + actionOptions: { reloadWindowOnClose: false }, + backdropOptions: { + zIndex: "1340", + closeOnClick: false, + }, + }) + ); this.modal.on("shown", (e) => { this.modalShown.apply(this, [e]); });