diff --git a/wcomponents-core/src/main/java/com/github/bordertech/wcomponents/render/webxml/WTextAreaRenderer.java b/wcomponents-core/src/main/java/com/github/bordertech/wcomponents/render/webxml/WTextAreaRenderer.java index dde6e0447..43cabc8b3 100755 --- a/wcomponents-core/src/main/java/com/github/bordertech/wcomponents/render/webxml/WTextAreaRenderer.java +++ b/wcomponents-core/src/main/java/com/github/bordertech/wcomponents/render/webxml/WTextAreaRenderer.java @@ -16,6 +16,8 @@ */ class WTextAreaRenderer extends AbstractWebXmlRenderer { + private static final String TAG_RTF = "wc-rtf"; + /** * Paints the given WTextArea. * @@ -66,7 +68,7 @@ public void doRender(final WComponent component, final WebXmlRenderContext rende * such as turning rich text features on or off, or specifying JSON config either as * a URL attribute or a nested CDATA section. */ - xml.append(""); + xml.append(String.format("<%s>", TAG_RTF)); } String textString = textArea.getText(); diff --git a/wcomponents-core/src/test/java/com/github/bordertech/wcomponents/render/webxml/WTextAreaRenderer_Test.java b/wcomponents-core/src/test/java/com/github/bordertech/wcomponents/render/webxml/WTextAreaRenderer_Test.java index 495ce5693..06f88f695 100755 --- a/wcomponents-core/src/test/java/com/github/bordertech/wcomponents/render/webxml/WTextAreaRenderer_Test.java +++ b/wcomponents-core/src/test/java/com/github/bordertech/wcomponents/render/webxml/WTextAreaRenderer_Test.java @@ -88,42 +88,33 @@ public void testDoPaint() throws IOException, SAXException, XpathException { assertXpathEvaluatesTo("40", "//ui:textarea/@cols", field); field.setRichTextArea(false); - assertSchemaMatch(field); - assertXpathNotExists("//ui:textarea/ui:rtf", field); + assertXpathNotExists("//ui:textarea/html:wc-rtf", field); field.setRichTextArea(true); - assertSchemaMatch(field); - assertXpathExists("//ui:textarea/ui:rtf", field); + assertXpathExists("//ui:textarea/html:wc-rtf", field); field.setDefaultSubmitButton(button); - assertSchemaMatch(field); assertXpathEvaluatesTo(button.getId(), "//ui:textarea/@buttonId", field); field.setPattern(""); - assertSchemaMatch(field); assertXpathNotExists("//ui:textarea/@pattern", field); // Pattern is not supported on the client for TextArea, and will not be rendered field.setPattern("test[123]"); - assertSchemaMatch(field); assertXpathNotExists("//ui:textarea/@pattern", field); field.setText("Hello"); - assertSchemaMatch(field); assertXpathEvaluatesTo(field.getText(), "normalize-space(//ui:textarea)", field); field.setPlaceholder("enter stuff here"); - assertSchemaMatch(field); assertXpathEvaluatesTo("enter stuff here", "//ui:textarea/@placeholder", field); field.setAutocomplete(AddressType.BILLING); - assertSchemaMatch(field); assertXpathEvaluatesTo(field.getAutocomplete(), "//ui:textarea/@autocomplete", field); field.setAutocompleteOff(); - assertSchemaMatch(field); assertXpathEvaluatesTo(AutocompleteUtil.getOff(), "//ui:textarea/@autocomplete", field); } diff --git a/wcomponents-theme/src/main/js/wc/ui/rtf.mjs b/wcomponents-theme/src/main/js/wc/ui/rtf.mjs index 51a1058a7..85602e631 100755 --- a/wcomponents-theme/src/main/js/wc/ui/rtf.mjs +++ b/wcomponents-theme/src/main/js/wc/ui/rtf.mjs @@ -2,7 +2,7 @@ * Provides a Rich Text Field implementation using tinyMCE. * * Optional module configuration. - * The config member "initObj" can be set to an abject containing any tinyMCE cofiguration members **except** + * The config member "initObj" can be set to an abject containing any tinyMCE configuration members **except** * selector. This allows customised RTF per implementation. This should be added in the template */ import initialise from "wc/dom/initialise.mjs"; @@ -45,7 +45,7 @@ function processNow(idArr) { } } -export default { +const instance = { /** * Register Rich Text Fields that need to be initialised. * @@ -58,10 +58,16 @@ export default { const callback = () => processNow(idArr); initialise.addCallback((element) => { if (!tinyMCE) { - const baseUrl = resourceLoader.getUrlFromImportMap("tinymce/"); - return import("tinymce/tinymce.js").then(() => { + let baseUrl = resourceLoader.getUrlFromImportMap("tinymce/"); + + return import(`${baseUrl}tinymce.js`).then(() => { tinyMCE = element.ownerDocument.defaultView.tinymce; if (baseUrl) { + // remove trailing forward slash since TinyMCE adds its own + while (baseUrl.charAt(baseUrl.length - 1) === '/') { + baseUrl = baseUrl.substring(0, baseUrl.length - 1); + } + tinyMCE.baseURL = baseUrl; } callback(); @@ -72,3 +78,5 @@ export default { } } }; + +export default instance; diff --git a/wcomponents-theme/src/main/js/wc/ui/textArea.mjs b/wcomponents-theme/src/main/js/wc/ui/textArea.mjs index c29ae326e..d6cb9982c 100644 --- a/wcomponents-theme/src/main/js/wc/ui/textArea.mjs +++ b/wcomponents-theme/src/main/js/wc/ui/textArea.mjs @@ -215,3 +215,17 @@ initialise.register({ }); export default instance; + +// Handle registration of rich text areas +const rtfTag = "wc-rtf"; +class WRichTextField extends HTMLElement { + connectedCallback() { + import("wc/ui/rtf.mjs").then(({ default: c }) => { + c.register([this.parentElement.getAttribute("id")]); + }); + } +} + +if (!customElements.get(rtfTag)) { + customElements.define(rtfTag, WRichTextField); +} diff --git a/wcomponents-theme/src/test/resource/mock-modules/tinymce.js b/wcomponents-theme/src/test/resource/mock-modules/tinymce.js new file mode 100644 index 000000000..4e6c7f038 --- /dev/null +++ b/wcomponents-theme/src/test/resource/mock-modules/tinymce.js @@ -0,0 +1,9 @@ +let tinymce = { + baseUrl : "", + init : function() {}, + triggerSave : function() {} +}; + +document.defaultView.tinymce = tinymce; + +export default tinymce; diff --git a/wcomponents-theme/src/test/spec/wc.dom.initialise.test.mjs b/wcomponents-theme/src/test/spec/wc.dom.initialise.test.mjs index 502a76e5f..82a02e6ed 100644 --- a/wcomponents-theme/src/test/spec/wc.dom.initialise.test.mjs +++ b/wcomponents-theme/src/test/spec/wc.dom.initialise.test.mjs @@ -2,6 +2,15 @@ import initialise, {setView} from "wc/dom/initialise.mjs"; import {setUpExternalHTML} from "../helpers/specUtils.mjs"; describe("wc/dom/initialise", () => { + let origWindow; + + beforeAll(() => { + origWindow = window; + }); + + afterAll(() => { + setView(origWindow); + }); beforeEach(() => { return setUpExternalHTML("domUsefulDom.html").then(dom => { diff --git a/wcomponents-theme/src/test/spec/wc.ui.rtf.test.mjs b/wcomponents-theme/src/test/spec/wc.ui.rtf.test.mjs new file mode 100644 index 000000000..6a81df678 --- /dev/null +++ b/wcomponents-theme/src/test/spec/wc.ui.rtf.test.mjs @@ -0,0 +1,88 @@ +import rtf from "wc/ui/rtf.mjs"; +import initialise from "wc/dom/initialise.mjs"; +import tinymce from "../resource/mock-modules/tinymce.js"; + +describe("wc/ui/rtf", () => { + let origHead; + + beforeAll(() => { + origHead = window.document.head.innerHTML; + + let importMap = window.document.createElement("script"); + importMap.setAttribute("type", "importmap"); + importMap.innerHTML = + `{ + "imports": { + "tinymce/": "../../../../test/resource/mock-modules/" + } + }`; + window.document.head.append(importMap); + }); + + afterAll(() => { + window.document.head.innerHTML = origHead; + delete document.defaultView.tinymce; + }); + + it("sets a callback for every nonempty invocation of register", (done) => { + spyOn(initialise, "addCallback"); + rtf.register(["exID", "anotherEx"]); + rtf.register(["exID"]); + rtf.register([]); + + expect(initialise.addCallback).toHaveBeenCalledTimes(2); + done(); + }); + + it("passes the correct selectors to TinyMCE on editor initialisation", (done) => { + const selectors = ["textarea#exID_input", "textarea#anotherEx_input"]; + const expectedCalls = 2; + let callCount = 0; + + const initSpy = spyOn(tinymce, "init").and.callFake(() => { + let givenSelector = initSpy.calls.mostRecent().args[0].selector; + expect(selectors).toContain(givenSelector); + + if (selectors.includes(givenSelector)) { + if (++callCount === expectedCalls) { + done(); + } + + const idx = selectors.indexOf(givenSelector); + if (idx > -1) { + selectors.splice(idx, 1); + } + } + }); + + rtf.register(["exID"]); + rtf.register(["anotherEx"]); + }); + + it("passes the correct default plugins to TinyMCE on editor initialisation", (done) => { + const initSpy = spyOn(tinymce, "init").and.callFake(() => { + const defaultPlugins = ["autolink", "link", "lists", "advlist", "preview", "help"]; + + for (const plugin of defaultPlugins) { + expect(initSpy.calls.mostRecent().args[0].plugins).toContain(plugin); + } + done(); + }); + + rtf.register(["exID"]); + }); + + it("passes the correct default tools to TinyMCE on editor initialisation", (done) => { + const initSpy = spyOn(tinymce, "init").and.callFake(() => { + const defaultTools = ["undo", "redo", "formatselect", "bold", "italic", "alignleft", "aligncenter", + "alignright", "alignjustify", "bullist", "numlist", "outdent", "indent", "removeformat", "help"]; + + for (const tool of defaultTools) { + expect(initSpy.calls.mostRecent().args[0].toolbar).toContain(tool); + } + done(); + }); + + rtf.register(["exID"]); + }); +}); diff --git a/wcomponents-xslt/src/main/xslt/all.xsl b/wcomponents-xslt/src/main/xslt/all.xsl index 328c51631..a18220990 100644 --- a/wcomponents-xslt/src/main/xslt/all.xsl +++ b/wcomponents-xslt/src/main/xslt/all.xsl @@ -334,7 +334,6 @@ select=".//ui:dropdown[@data and not(@type) and not(@readOnly)] | .//ui:listbox[@data and not(@readOnly)] | .//ui:shuffler[@data and not(@readOnly)]" /> - @@ -387,11 +386,6 @@ ]);}); - - import("wc/ui/rtf.mjs").then(({ default: c }) => {c.register([ - - ]);}); - import("wc/ui/containerload.mjs").then(({ default: c }) => {c.register([ diff --git a/wcomponents-xslt/src/main/xslt/wc.ui.textarea.xsl b/wcomponents-xslt/src/main/xslt/wc.ui.textarea.xsl index 3be0b89f8..8b865f9b3 100644 --- a/wcomponents-xslt/src/main/xslt/wc.ui.textarea.xsl +++ b/wcomponents-xslt/src/main/xslt/wc.ui.textarea.xsl @@ -21,7 +21,7 @@ - div + div pre @@ -41,7 +41,7 @@ - + @@ -58,7 +58,7 @@ - + div @@ -166,10 +166,9 @@