Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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/),
Expand Down
4 changes: 2 additions & 2 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
67 changes: 59 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 `<Editor />` 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).*
```js
import React from 'react';
import { Editor } from '@tinymce/tinymce-react';

export default function App() {
return (
<>
<h1>TinyMCE React demo</h1>
<Editor
apiKey="your-api-key"
init={{
plugins: 'lists link image table code help wordcount'
}}
/>
</>
);
}
```
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.
7 changes: 2 additions & 5 deletions contributing.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 14 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
},
Expand All @@ -22,11 +22,21 @@
"build-storybook": "storybook build",
"deploy-storybook": "storybook build && gh-pages -d ./storybook-static -u 'tiny-bot <no-reply@tiny.cloud>' --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",
Expand Down Expand Up @@ -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"
},
Expand Down
36 changes: 28 additions & 8 deletions src/test/ts/alien/Loader.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -51,13 +51,25 @@ export const render = async (props: Partial<IAllProps> = {}, container: HTMLElem
.filter(SugarNode.isHTMLElement)
.map((val) => val.dom)
.fold(() => reject('Could not find DOMNode'), (DOMNode) => {
resolve({
ref: ref as React.RefObject<Editor>,
editor,
DOMNode,
});
}
);
// This is a workaround to avoid a race condition occurring in tinymce 8 where licenseKeyManager is still validating the license key
Comment thread
tiny-ben-tran marked this conversation as resolved.
// 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>,
editor,
DOMNode,
});
// eslint-disable-next-line no-console
}).catch((reason) => console.warn(reason));
} else {
resolve({
ref: ref as React.RefObject<Editor>,
editor,
DOMNode,
});
}
});
}, 0);
});
}
Expand Down Expand Up @@ -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();
}
});
});
};
24 changes: 14 additions & 10 deletions src/test/ts/browser/EditorBehaviorTest.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -67,7 +71,6 @@ describe('EditorBehaviourTest', () => {
Assertions.assertEq('First arg should be new content', '<p>Initial Content</p>', 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 () => {
Expand All @@ -81,15 +84,14 @@ 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 () => {
using ctx = await render({ initialValue: '<p>Initial Content</p>' });
await ctx.reRender({ onSetContent: eventStore.createHandler('onSetContent') });

TinyAssertions.assertContent(ctx.editor, '<p>Initial Content</p>');
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('<p>New Content</p>');

eventStore.each<SetContentEvent>('onSetContent', (events) => {
Expand All @@ -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 () => {
Expand All @@ -115,7 +116,7 @@ describe('EditorBehaviourTest', () => {
eventStore.clearState();
ctx.editor.setContent('<p>Initial Content</p>');
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('<p>New Content</p>');

eventStore.each<SetContentEvent>('InitialHandler', (events) => {
Expand All @@ -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('<p>abc</p>');
await Waiter.pTryUntilPredicate('Editor content is set to correct value', () => ctx.editor.getContent() === '<p>abc</p>');
await Waiter.pTryUntilPredicate('Editor content is set to correct value', () => {
return ctx.editor.getContent() === '<p>abc</p>';
});
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() === '<p>aec</p>');
await Waiter.pTryUntilPredicate('Editor content is set to correct value', () => {
return ctx.editor.getContent() === '<p>aec</p>';
});
eventStore.each<string>('onEditorChange', (events) => {
Assertions.assertEq(
'onEditorChange should have been triggered once',
Expand Down
28 changes: 21 additions & 7 deletions src/test/ts/browser/LoadTinyTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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
);
});
});
Expand Down
11 changes: 8 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading