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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Changed
- The `disabled` property now toggles the `disabled` option. #TINY-11906

### Added
- Added `readonly` property that can be used to toggle the `readonly` mode. #TINY-11906

## 6.1.0 - 2025-03-31

### Added
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,15 @@
"react-dom": "^19.0.0",
"rimraf": "^6.0.1",
"storybook": "^8.6.4",
"tinymce": "^7.2.1",
"tinymce": "^7",
"tinymce-4": "npm:tinymce@^4",
"tinymce-5": "npm:tinymce@^5",
"tinymce-6": "npm:tinymce@^6",
"tinymce-7": "npm:tinymce@^7",
"tinymce-7.5": "npm:[email protected]",
"typescript": "~5.8.2",
"vite": "^6.2.1"
},
"version": "6.1.1-rc",
"version": "6.2.0-rc",
"name": "@tinymce/tinymce-react"
}
17 changes: 17 additions & 0 deletions src/main/ts/Utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { eventPropTypes, IEventPropTypes } from './components/EditorPropTypes';
import { IAllProps } from './components/Editor';
import type { Editor as TinyMCEEditor, EditorEvent } from 'tinymce';
import { getTinymce } from './TinyMCE';
import { TinyVer } from '@tinymce/miniature';

export const isFunction = (x: unknown): x is Function => typeof x === 'function';

Expand Down Expand Up @@ -109,4 +111,19 @@ export const setMode = (editor: TinyMCEEditor | undefined, mode: 'readonly' | 'd
(editor as any).setMode(mode);
}
}
};

export const getTinymceOrError = (view: Window) => {
const tinymce = getTinymce(view);
if (!tinymce) {
throw new Error('tinymce should have been loaded into global scope');
}

return tinymce;
};

export const isDisabledOptionSupported = (view: Window) => {
const tinymce = getTinymceOrError(view);

return !TinyVer.isLessThan(tinymce, '7.6.0');
};
45 changes: 31 additions & 14 deletions src/main/ts/components/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import * as React from 'react';
import type { Bookmark, EditorEvent, TinyMCE, Editor as TinyMCEEditor } from 'tinymce';
import { IEvents } from '../Events';
import { ScriptItem, ScriptLoader } from '../ScriptLoader2';
import { getTinymce } from '../TinyMCE';
import { configHandlers, isBeforeInputEventAvailable, isFunction, isInDoc, isTextareaOrInput, mergePlugins, setMode, uuid } from '../Utils';
import { configHandlers, isBeforeInputEventAvailable,
isFunction, isInDoc, isTextareaOrInput, mergePlugins,
setMode, uuid, isDisabledOptionSupported,
getTinymceOrError } from '../Utils';
import { EditorPropTypes, IEditorPropTypes } from './EditorPropTypes';
import { getTinymce } from '../TinyMCE';

const changeEvents = 'change keyup compositionend setcontent CommentChange';

Expand All @@ -14,14 +17,15 @@ interface DoNotUse<T extends string> {
__brand: T;
}

type OmittedInitProps = 'selector' | 'target' | 'readonly' | 'license_key';
type OmittedInitProps = 'selector' | 'target' | 'readonly' | 'disabled' | 'license_key';

type EditorOptions = Parameters<TinyMCE['init']>[0];

export type InitOptions = Omit<OmitStringIndexSignature<EditorOptions>, OmittedInitProps> & {
selector?: DoNotUse<'selector prop is handled internally by the component'>;
target?: DoNotUse<'target prop is handled internally by the component'>;
readonly?: DoNotUse<'readonly prop is overridden by the component, use the `disabled` prop instead'>;
readonly?: DoNotUse<'readonly prop is overridden by the component'>;
disabled?: DoNotUse<'disabled prop is overridden by the component'>;
license_key?: DoNotUse<'license_key prop is overridden by the integration, use the `licenseKey` prop instead'>;
} & { [key: string]: unknown };

Expand Down Expand Up @@ -95,9 +99,14 @@ export interface IProps {
toolbar: NonNullable<EditorOptions['toolbar']>;
/**
* @see {@link https://www.tiny.cloud/docs/tinymce/7/react-ref/#disabled React Tech Ref - disabled}
* @description Whether the editor should be "disabled" (read-only).
* @description Whether the editor should be disabled.
*/
disabled: boolean;
/**
* @see {@link https://www.tiny.cloud/docs/tinymce/7/react-ref/#readonly React Tech Ref - readonly}
* @description Whether the editor should be readonly.
*/
readonly: boolean;
/**
* @see {@link https://www.tiny.cloud/docs/tinymce/7/react-ref/#textareaname React Tech Ref - textareaName}
* @description Set the `name` attribute of the `textarea` element used for the editor in forms. Only valid in iframe mode.
Expand Down Expand Up @@ -209,9 +218,18 @@ export class Editor extends React.Component<IAllProps> {
}
});
}

if (this.props.readonly !== prevProps.readonly) {
const readonly = this.props.readonly ?? false;
setMode(this.editor, readonly ? 'readonly' : 'design');
}

if (this.props.disabled !== prevProps.disabled) {
const disabled = this.props.disabled ?? false;
setMode(this.editor, disabled ? 'readonly' : 'design');
if (isDisabledOptionSupported(this.view)) {
this.editor.options.set('disabled', this.props.disabled);
} else {
setMode(this.editor, this.props.disabled ? 'readonly' : 'design');
}
}
}
}
Expand Down Expand Up @@ -431,16 +449,16 @@ export class Editor extends React.Component<IAllProps> {
return;
}

const tinymce = getTinymce(this.view);
if (!tinymce) {
throw new Error('tinymce should have been loaded into global scope');
}
const tinymce = getTinymceOrError(this.view);

const finalInit: EditorOptions = {
...this.props.init as Omit<InitOptions, OmittedInitProps>,
selector: undefined,
target,
readonly: this.props.disabled,
...isDisabledOptionSupported(this.view)
? { disabled: this.props.disabled, readonly: this.props.readonly }
: { readonly: this.props.disabled || this.props.readonly }
,
inline: this.inline,
plugins: mergePlugins(this.props.init?.plugins, this.props.plugins),
toolbar: this.props.toolbar ?? this.props.init?.toolbar,
Expand Down Expand Up @@ -477,8 +495,7 @@ export class Editor extends React.Component<IAllProps> {
editor.undoManager.add();
editor.setDirty(false);
}
const disabled = this.props.disabled ?? false;
setMode(this.editor, disabled ? 'readonly' : 'design');

// ensure existing init_instance_callback is called
if (this.props.init && isFunction(this.props.init.init_instance_callback)) {
this.props.init.init_instance_callback(editor);
Expand Down
1 change: 1 addition & 0 deletions src/main/ts/components/EditorPropTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const EditorPropTypes: IEditorPropTypes = {
plugins: PropTypes.oneOfType([ PropTypes.string, PropTypes.array ]),
toolbar: PropTypes.oneOfType([ PropTypes.string, PropTypes.array ]),
disabled: PropTypes.bool,
readonly: PropTypes.bool,
textareaName: PropTypes.string,
tinymceScriptSrc: PropTypes.oneOfType([
PropTypes.string,
Expand Down
19 changes: 19 additions & 0 deletions src/stories/Editor.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,25 @@ export const ToggleDisabledProp: StoryObj<Editor> = {
}
};

export const ToggleReadonlyProp: StoryObj<Editor> = {
render: () => {
const [ readonly, setReadonly ] = React.useState(true);
const toggleReadonly = () => setReadonly((prev) => !prev);
return (
<div>
<Editor
apiKey={apiKey}
initialValue={initialValue}
readonly={readonly}
/>
<button onClick={toggleReadonly}>
{readonly ? 'Set editable' : 'Set Readonly'}
</button>
</div>
);
}
};

export const CloudChannelSetTo5Dev: StoryObj<Editor> = {
name: 'Cloud Channel Set To "6-dev"',
render: () => (
Expand Down
77 changes: 77 additions & 0 deletions src/test/ts/browser/EditorDisabledTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { context, describe, it } from '@ephox/bedrock-client';
import * as Loader from '../alien/Loader';
import { Assertions, Waiter } from '@ephox/agar';

describe('EditorDisabledTest', () => {

context('with TinyMCE < 7.6', () => {
Loader.withVersion('7.5', (render) => {
it('updating disabled prop should toggle the editor\'s mode', async () => {
using ctx = await render({
disabled: true
});

Assertions.assertEq('mode is readonly', 'readonly', ctx.editor.mode.get());

await ctx.reRender({
disabled: false
});
await Waiter.pTryUntil('mode is changed to design', () => {
Assertions.assertEq('mode is design', 'design', ctx.editor.mode.get());
});
});

it('updating readonly prop should toggle the editor\'s mode', async () => {
using ctx = await render({
readonly: true
});
Assertions.assertEq('mode is readonly', 'readonly', ctx.editor.mode.get());

await ctx.reRender({
readonly: false
});
await Waiter.pTryUntil('mode is changed to design', () => {
Assertions.assertEq('mode is design', 'design', ctx.editor.mode.get());
});
});
});
});

context('with TinyMCE >= 7.6', () => {
Loader.withVersion('7', (render) => {
it('updating disabled prop should only change the editor\'s state', async () => {
using ctx = await render({
disabled: true
});

Assertions.assertEq('mode is design', 'design', ctx.editor.mode.get());
Assertions.assertEq('editor is disabled', true, ctx.editor.options.get('disabled'));

await ctx.reRender({
disabled: false
});

await Waiter.pTryUntil('editor\'s state should be updated', () => {
Assertions.assertEq('mode is design', 'design', ctx.editor.mode.get());
Assertions.assertEq('editor is not disabled', false, ctx.editor.options.get('disabled'));
});
});

it('updating readonly prop should only change the editor\'s mode', async () => {
using ctx = await render({
readonly: true
});
Assertions.assertEq('mode is readonly', 'readonly', ctx.editor.mode.get());
Assertions.assertEq('editor is not disabled', false, ctx.editor.options.get('disabled'));

await ctx.reRender({
readonly: false
});
await Waiter.pTryUntil('editor\'s mode should be updated', () => {
Assertions.assertEq('mode is design', 'design', ctx.editor.mode.get());
Assertions.assertEq('editor is not disabled', false, ctx.editor.options.get('disabled'));
});
});
});
});
});
7 changes: 0 additions & 7 deletions src/test/ts/browser/EditorInitTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,6 @@ describe('EditorInitTest', () => {
TinyAssertions.assertContent(ctx.editor, '<p>New Value</p>');
});

it('Disabled prop should disable editor', async () => {
using ctx = await render();
Assertions.assertEq('Should be design mode', true, '4' === version ? !ctx.editor.readonly : ctx.editor.mode.get() === 'design');
await ctx.reRender({ ...defaultProps, disabled: true });
Assertions.assertEq('Should be readonly mode', true, '4' === version ? ctx.editor.readonly : ctx.editor.mode.get() === 'readonly');
});

it('Using an overriden props will cause a TS error', async () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
using _ = await render({
Expand Down
21 changes: 13 additions & 8 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8705,15 +8705,20 @@ tiny-invariant@^1.3.1, tiny-invariant@^1.3.3:
resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-6.8.4.tgz#53e1313ebfe5524b24c0fa45937d51fda058632d"
integrity sha512-okoJyxuPv1gzASxQDNgQbnUXOdAIyoOSXcXcZZu7tiW0PSKEdf3SdASxPBupRj+64/E3elHwVRnzSdo82Emqbg==

"tinymce-7@npm:tinymce@^7":
version "7.2.1"
resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-7.2.1.tgz#9b4f6b5a0fa647e2953c174ac69aa47483683332"
integrity sha512-ADd1cvdIuq6NWyii0ZOZRuu+9sHIdQfcRNWBcBps2K8vy7OjlRkX6iw7zz1WlL9kY4z4L1DvIP+xOrVX/46aHA==
"tinymce-7.5@npm:tinymce@7.5":
version "7.5.1"
resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-7.5.1.tgz#4c207ab930d3a073bf851ddd3a8aa44d8d94d7bd"
integrity sha512-GRXJUB0BEIOUHUEC+q9IjsgWGIAQ4Tn5t5hfpB/YR7No3oPgKHG03v1d3nbov9aqdyVW7Be+UD4I3ZerQG30VQ==

tinymce@^7.2.1:
version "7.7.1"
resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-7.7.1.tgz#e7c19f14153dc9bcde11bbc012db45167892c116"
integrity sha512-rMetqSgZtYbj4YPOX+gYgmlhy/sIjVlI/qlrSOul/Mpn9e0aIIG/fR0qvQSVYvxFv6OzRTge++NQyTbzLJK1NA==
"tinymce-7@npm:tinymce@^7":
version "7.8.0"
resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-7.8.0.tgz#d57a597aecdc2108f2dd68fe74c6099c0a0ef66f"
integrity sha512-MUER5MWV9mkOB4expgbWknh/C5ZJvOXQlMVSx4tJxTuYtcUCDB6bMZ34fWNOIc8LvrnXmGHGj0eGQuxjQyRgrA==

tinymce@^7:
version "7.8.0"
resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-7.8.0.tgz#d57a597aecdc2108f2dd68fe74c6099c0a0ef66f"
integrity sha512-MUER5MWV9mkOB4expgbWknh/C5ZJvOXQlMVSx4tJxTuYtcUCDB6bMZ34fWNOIc8LvrnXmGHGj0eGQuxjQyRgrA==

tinyrainbow@^1.2.0:
version "1.2.0"
Expand Down