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>%1$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 @@
+
-
-