diff --git a/CHANGELOG.md b/CHANGELOG.md
index 60031a4e..69e52e8d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-# Change log
+# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
diff --git a/Jenkinsfile b/Jenkinsfile
index 101b5e83..05f689ee 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -5,9 +5,9 @@ mixedBeehiveFlow(
container: [ resourceRequestMemory: '3Gi', resourceLimitMemory: '3Gi' ],
testPrefix: 'Tiny-React',
platforms: [
- [ browser: 'chrome', headless: true ],
+ [ browser: 'chrome', provider: 'lambdatest', os: 'macOS Sonoma', buckets: 1 ],
[ browser: 'firefox', provider: 'aws', buckets: 1 ],
- [ browser: 'safari', provider: 'lambdatest', buckets: 1 ]
+ [ browser: 'safari', provider: 'lambdatest', os: 'macOS Sonoma', buckets: 1 ]
],
customSteps: {
stage("update storybook") {
diff --git a/README.md b/README.md
index 30408b46..5f57546a 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,66 @@
-# Official TinyMCE React component
+# Official React Component for TinyMCE
## About
-This package is a thin wrapper around [TinyMCE](https://github.com/tinymce/tinymce) to make it easier to use in a React application.
+Official React component for TinyMCE, the rich text editor. It wraps TinyMCE as a React `` component, supporting controlled and uncontrolled usage. By default, it pulls TinyMCE from the Tiny Cloud CDN unless configured to use a different setup, such as self-hosting the [tinymce NPM package](https://www.npmjs.com/package/tinymce).
-* If you need detailed documentation on TinyMCE, see: [TinyMCE Documentation](https://www.tiny.cloud/docs/tinymce/8/).
-* For the TinyMCE React Quick Start, see: [TinyMCE Documentation - React Integration](https://www.tiny.cloud/docs/tinymce/8/react-cloud/).
-* For the TinyMCE React Technical Reference, see: [TinyMCE Documentation - TinyMCE React Technical Reference](https://www.tiny.cloud/docs/tinymce/8/react-ref/).
-* For our quick demos, check out the TinyMCE React [Storybook](https://tinymce.github.io/tinymce-react/).
+## Quickstart
+### Cloud CDN
-### Issues
+1. [Sign up for a Tiny Cloud account](https://www.tiny.cloud/pricing/) to receive a Tiny Cloud API key.
+1. Then in your React project:
+ 1. Run `npm install @tinymce/tinymce-react`
+ 1. Include the following code:
-Have you found an issue with tinymce-react or do you have a feature request? Open up an [issue](https://github.com/tinymce/tinymce-react/issues) and let us know or submit a [pull request](https://github.com/tinymce/tinymce-react/pulls). *Note: For issues concerning TinyMCE please visit the [TinyMCE repository](https://github.com/tinymce/tinymce).*
\ No newline at end of file
+ ```js
+ import React from 'react';
+ import { Editor } from '@tinymce/tinymce-react';
+
+ export default function App() {
+ return (
+ <>
+
TinyMCE React demo
+
+ >
+ );
+ }
+ ```
+ 1. Update the `apiKey` prop on the `Editor` component to include your Tiny Cloud API key.
+
+For more information: [Using TinyMCE with React - Cloud CDN](https://www.tiny.cloud/docs/tinymce/latest/react-cloud/)
+
+### Self hosted via NPM package
+
+Using TinyMCE from NPM in a React project requires a couple of extra steps. See the documentation for more information: [Using TinyMCE with React - Self hosted via NPM](https://www.tiny.cloud/docs/tinymce/latest/react-pm/)
+
+
+## Demos
+
+For our quick demos, check out the TinyMCE React [Storybook](https://tinymce.github.io/tinymce-react/).
+
+## Detailed documentation
+
+* [TinyMCE React Technical Reference](https://www.tiny.cloud/docs/tinymce/latest/react-ref/).
+* [TinyMCE Documentation](https://www.tiny.cloud/docs/tinymce/latest/).
+
+
+## Issues
+
+Have you found an issue with `tinymce-react` or do you have a feature request?
+Open up an [issue](https://github.com/tinymce/tinymce-react/issues) and let us know
+or submit a [pull request](https://github.com/tinymce/tinymce-react/pulls).
+
+_Note: for issues concerning TinyMCE please visit the [TinyMCE repository](https://github.com/tinymce/tinymce)._
+
+
+## License
+
+`tinymce-react` is licensed under the MIT License. See the LICENSE.txt file for details.
+
+Depending on use case, the TinyMCE core editor can be used under either GPL-2.0-or-later or a commercial license. See the [tinymce package](https://www.npmjs.com/package/tinymce) for details.
diff --git a/contributing.md b/contributing.md
index b561e52d..c970dad8 100644
--- a/contributing.md
+++ b/contributing.md
@@ -1,8 +1,5 @@
# Contributing
-This project follows the [beehive-flow](https://github.com/tinymce/beehive-flow/) branching process ("Basic process - release from main branch").
-Please read the [beehive-flow readme](https://github.com/tinymce/beehive-flow/blob/main/README.md) for more information.
-This mainly affects branching and merging for Tiny staff.
+External contributors are free to submit PRs against the `main` branch.
-External contributors are free to submit PRs against the `main` branch.
-Note that contributions will require signing of our Contributor License Agreement.
+In order for Tiny to accept your contribution, we will need legal permission from you to use your code. Please email `legal@tiny.cloud` with the subject `CLA request for [your Github username]`. Our legal team will send you a Contributors License Agreement (CLA) which you will need to sign and return.
diff --git a/package.json b/package.json
index b627bd6e..719a7f0f 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "description": "Official TinyMCE React Component",
+ "description": "Official React component for the TinyMCE rich text editor.",
"repository": {
"url": "https://github.com/tinymce/tinymce-react"
},
@@ -22,11 +22,21 @@
"build-storybook": "storybook build",
"deploy-storybook": "storybook build && gh-pages -d ./storybook-static -u 'tiny-bot ' --nojekyll"
},
- "keywords": [],
+ "keywords": [
+ "tinymce",
+ "react",
+ "component",
+ "editor",
+ "wysiwyg",
+ "rich-text-editor",
+ "html",
+ "contenteditable"
+ ],
"author": "Ephox Corporation DBA Tiny Technologies, Inc.",
"license": "MIT",
"dependencies": {
- "prop-types": "^15.8.1"
+ "prop-types": "^15.8.1",
+ "tinymce-8": "npm:tinymce@^8.0.0"
},
"peerDependencies": {
"react": "^19.0.0 || ^18.0.0 || ^17.0.1 || ^16.7.0",
@@ -74,7 +84,7 @@
"tinymce-6": "npm:tinymce@^6",
"tinymce-7": "npm:tinymce@^7",
"tinymce-7.5": "npm:tinymce@7.5",
- "tinymce-8": "npm:tinymce@^8.0.0",
+ "tinymce-premium": "^8.9.2",
"typescript": "~5.8.2",
"vite": "^7.1.5"
},
diff --git a/src/test/ts/alien/Loader.tsx b/src/test/ts/alien/Loader.tsx
index 67fd19b6..f39a70d1 100644
--- a/src/test/ts/alien/Loader.tsx
+++ b/src/test/ts/alien/Loader.tsx
@@ -1,4 +1,4 @@
-import { before, context } from '@ephox/bedrock-client';
+import { after, before, context } from '@ephox/bedrock-client';
import { Fun, Optional } from '@ephox/katamari';
import { Remove, SugarElement, SugarNode } from '@ephox/sugar';
import { VersionLoader } from '@tinymce/miniature';
@@ -51,13 +51,25 @@ export const render = async (props: Partial = {}, container: HTMLElem
.filter(SugarNode.isHTMLElement)
.map((val) => val.dom)
.fold(() => reject('Could not find DOMNode'), (DOMNode) => {
- resolve({
- ref: ref as React.RefObject,
- editor,
- DOMNode,
- });
- }
- );
+ // This is a workaround to avoid a race condition occurring in tinymce 8 where licenseKeyManager is still validating the license key
+ // after global tinymce is removed in a clean up. Specifically, it happens when unloading/loading different versions of TinyMCE
+ if (editor.licenseKeyManager) {
+ editor.licenseKeyManager.validate({}).then(() => {
+ resolve({
+ ref: ref as React.RefObject,
+ editor,
+ DOMNode,
+ });
+ // eslint-disable-next-line no-console
+ }).catch((reason) => console.warn(reason));
+ } else {
+ resolve({
+ ref: ref as React.RefObject,
+ editor,
+ DOMNode,
+ });
+ }
+ });
}, 0);
});
}
@@ -109,5 +121,13 @@ export const withVersion = (version: Version, fn: (render: RenderWithVersion) =>
});
fn(render as RenderWithVersion);
+
+ after(() => {
+ // remove script tag for version
+ const scriptTag = document.querySelector(`script[src*="tinymce-${version}"]`);
+ if (scriptTag) {
+ scriptTag.remove();
+ }
+ });
});
};
diff --git a/src/test/ts/browser/EditorBehaviorTest.ts b/src/test/ts/browser/EditorBehaviorTest.ts
index 9f4f25fc..cb1445cb 100644
--- a/src/test/ts/browser/EditorBehaviorTest.ts
+++ b/src/test/ts/browser/EditorBehaviorTest.ts
@@ -1,6 +1,6 @@
import { Assertions, Waiter } from '@ephox/agar';
-import { describe, it } from '@ephox/bedrock-client';
+import { afterEach, describe, it } from '@ephox/bedrock-client';
import { TinyAssertions, TinySelections } from '@ephox/mcagar';
import { PlatformDetection } from '@ephox/sand';
import { EditorEvent, Events, Editor as TinyMCEEditor } from 'tinymce';
@@ -29,6 +29,10 @@ describe('EditorBehaviourTest', () => {
const eventStore = EventStore();
+ afterEach(() => {
+ eventStore.clearState();
+ });
+
VERSIONS.forEach((version) =>
Loader.withVersion(version, (render) => {
it('Assert structure of tinymce and tinymce-react events', async () => {
@@ -67,7 +71,6 @@ describe('EditorBehaviourTest', () => {
Assertions.assertEq('First arg should be new content', 'Initial Content
', events[0].editorEvent);
Assertions.assertEq('Second arg should be editor', true, isEditor(events[0].editor));
});
- eventStore.clearState();
});
it('onEditorChange should only fire when the editors content changes', async () => {
@@ -81,7 +84,6 @@ describe('EditorBehaviourTest', () => {
eventStore.each('onEditorChange', (events) => {
Assertions.assertEq('onEditorChange should have been fired once', 1, events.length);
});
- eventStore.clearState();
});
it('Should be able to register an event handler after initial render', async () => {
@@ -89,7 +91,7 @@ describe('EditorBehaviourTest', () => {
await ctx.reRender({ onSetContent: eventStore.createHandler('onSetContent') });
TinyAssertions.assertContent(ctx.editor, 'Initial Content
');
- await Waiter.pWait(0); // Wait for React's state updates to complete before setting new content
+ await Waiter.pWait(100); // Wait for React's state updates to complete before setting new content
ctx.editor.setContent('New Content
');
eventStore.each('onSetContent', (events) => {
@@ -99,7 +101,6 @@ describe('EditorBehaviourTest', () => {
events[0].editorEvent.content
);
});
- eventStore.clearState();
});
it('Providing a new event handler and re-rendering should unbind old handler and bind new handler', async () => {
@@ -115,7 +116,7 @@ describe('EditorBehaviourTest', () => {
eventStore.clearState();
ctx.editor.setContent('Initial Content
');
await ctx.reRender({ onSetContent: eventStore.createHandler('NewHandler') });
- await Waiter.pWait(0); // Wait for React's state updates to complete before setting new content
+ await Waiter.pWait(100); // Wait for React's state updates to complete before setting new content
ctx.editor.setContent('New Content
');
eventStore.each('InitialHandler', (events) => {
@@ -132,18 +133,21 @@ describe('EditorBehaviourTest', () => {
events[0].editorEvent.content
);
});
-
- eventStore.clearState();
});
+
it('INT-3226: onEditorChange is triggered only once after calling insertContent', async () => {
using ctx = await render({ onEditorChange: eventStore.createHandler('onEditorChange') });
const { editor } = ctx;
editor.setContent('abc
');
- await Waiter.pTryUntilPredicate('Editor content is set to correct value', () => ctx.editor.getContent() === 'abc
');
+ await Waiter.pTryUntilPredicate('Editor content is set to correct value', () => {
+ return ctx.editor.getContent() === 'abc
';
+ });
eventStore.clearState();
TinySelections.setSelection(editor, [ 0, 0 ], 1, [ 0, 0 ], 2);
editor.insertContent('e');
- await Waiter.pTryUntilPredicate('Editor content is set to correct value', () => ctx.editor.getContent() === 'aec
');
+ await Waiter.pTryUntilPredicate('Editor content is set to correct value', () => {
+ return ctx.editor.getContent() === 'aec
';
+ });
eventStore.each('onEditorChange', (events) => {
Assertions.assertEq(
'onEditorChange should have been triggered once',
diff --git a/src/test/ts/browser/LoadTinyTest.ts b/src/test/ts/browser/LoadTinyTest.ts
index f6865b85..de531e9e 100644
--- a/src/test/ts/browser/LoadTinyTest.ts
+++ b/src/test/ts/browser/LoadTinyTest.ts
@@ -42,7 +42,7 @@ describe('LoadTinyTest', () => {
CLOUD_VERSIONS.forEach((version) => {
it(`Should be able to load TinyMCE from Cloud (${version})`, async () => {
- const apiKey = 'a-fake-api-key';
+ const apiKey = VALID_API_KEY;
using _ = await render({ apiKey, cloudChannel: version });
assertTinymceVersion(version);
Assertions.assertEq(
@@ -55,10 +55,24 @@ describe('LoadTinyTest', () => {
it(`Should be able to load TinyMCE (${version}) in hybrid`, async () => {
using _ = await render({
tinymceScriptSrc: [
- `/project/node_modules/tinymce-${version}/tinymce.min.js`,
- `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce/${version}/cloud-plugins.min.js?tinydrive=${version}`
+ `/project/node_modules/tinymce-${version}/tinymce.min.js`
],
- plugins: [ 'tinydrive' ]
+ apiKey: VALID_API_KEY,
+ licenseKey: 'gpl',
+ plugins: [ 'formatpainter' ],
+ // TinyMCE 8 requires the Commercial License Key Manager to validate an apiKey. It's not
+ // served from Tiny Cloud, so for a self-hosted core it needs to load from the local
+ // `tinymce-premium` package instead. See https://www.tiny.cloud/docs/tinymce/latest/license-key/
+ init: version === '8' ? {
+ external_plugins: {
+ licensekeymanager: '/project/node_modules/tinymce-premium/plugins/licensekeymanager/plugin.min.js',
+ formatpainter: `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce-plugins/formatpainter/${version}/plugin.min.js`
+ }
+ } : {
+ external_plugins: {
+ formatpainter: `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce-plugins/formatpainter/${version}/plugin.min.js`,
+ }
+ },
});
assertTinymceVersion(version);
Assertions.assertEq(
@@ -67,9 +81,9 @@ describe('LoadTinyTest', () => {
Global.tinymce.baseURI.path
);
Assertions.assertEq(
- 'The tinydrive plugin should have defaults for the cloud',
- `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce-plugins/tinydrive/${version}/plugin.min.js`,
- (Global.tinymce.defaultOptions || Global.tinymce.defaultSettings)?.custom_plugin_urls?.tinydrive
+ 'The formatpainter plugin should have been loaded from Cloud',
+ `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce-plugins/formatpainter/${version}`,
+ Global.tinymce.PluginManager.urls.formatpainter
);
});
});
diff --git a/yarn.lock b/yarn.lock
index bbfa5ff9..0657aec3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8756,9 +8756,14 @@ tinyglobby@^0.2.15:
integrity sha512-zaOHwmiP1EqTeLRXAvVriDb00JYnfEjWGPdKEuac7MiZJ5aiDMZ4Unc98Gmajn+PBljOmO1GKV6G0KwWn3+k8A==
"tinymce-8@npm:tinymce@^8.0.0":
- version "8.2.0"
- resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-8.2.0.tgz#3fde03d91a1a8fd2d33de6a378843724e1341e2e"
- integrity sha512-HZasRaAYQmePi+jjP4YFqHMtBJvbUaxDmjlXNf96uuyqsH6zZkGtJiFpsNi80Ec+L+snyF0OTDjrWVYDbkUrgQ==
+ version "8.9.2"
+ resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-8.9.2.tgz#0e6cb1d0230a27afe701651c1dbd99e2dac7ffdb"
+ integrity sha512-Qh6ADSC0x1F/KsrupfResNJFAaINJQBtfVjx5zxxWJ0ZrGtaKldtAXDMI6uEA57s2n+0rEtXmtbitiU4zV7+0Q==
+
+tinymce-premium@^8.9.2:
+ version "8.9.2"
+ resolved "https://registry.yarnpkg.com/tinymce-premium/-/tinymce-premium-8.9.2.tgz#e3beb35676d19709ba7ee33c5d54af46941084fa"
+ integrity sha512-9fx33G7Omixa7BlPqakx6xEs4jwQzA1EyPcS/5KenTWi+/yVEA2ILSkLeE4pfWW5qyY8ogdMYmGZDd476UJuEQ==
tinymce@^8.0.2:
version "8.2.0"