Skip to content

Commit 8ceeac1

Browse files
TINY-11906: Enable support for readonly mode (#588)
* TINY-11906: Enable support for readonly mode * TINY-11906: Add tests * TINY-11906: Add changelog and update tinymce * TINY-11906: Add the missing readonly prop * TINY-11906: Change to support previous versions of TinyMCE * TINY-11906: Update tests
1 parent 4ce80bb commit 8ceeac1

File tree

9 files changed

+167
-31
lines changed

9 files changed

+167
-31
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## Unreleased
88

9+
### Changed
10+
- The `disabled` property now toggles the `disabled` option. #TINY-11906
11+
12+
### Added
13+
- Added `readonly` property that can be used to toggle the `readonly` mode. #TINY-11906
14+
915
## 6.1.0 - 2025-03-31
1016

1117
### Added

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,15 @@
6868
"react-dom": "^19.0.0",
6969
"rimraf": "^6.0.1",
7070
"storybook": "^8.6.4",
71-
"tinymce": "^7.2.1",
71+
"tinymce": "^7",
7272
"tinymce-4": "npm:tinymce@^4",
7373
"tinymce-5": "npm:tinymce@^5",
7474
"tinymce-6": "npm:tinymce@^6",
7575
"tinymce-7": "npm:tinymce@^7",
76+
"tinymce-7.5": "npm:[email protected]",
7677
"typescript": "~5.8.2",
7778
"vite": "^6.2.1"
7879
},
79-
"version": "6.1.1-rc",
80+
"version": "6.2.0-rc",
8081
"name": "@tinymce/tinymce-react"
8182
}

src/main/ts/Utils.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { eventPropTypes, IEventPropTypes } from './components/EditorPropTypes';
22
import { IAllProps } from './components/Editor';
33
import type { Editor as TinyMCEEditor, EditorEvent } from 'tinymce';
4+
import { getTinymce } from './TinyMCE';
5+
import { TinyVer } from '@tinymce/miniature';
46

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

@@ -109,4 +111,19 @@ export const setMode = (editor: TinyMCEEditor | undefined, mode: 'readonly' | 'd
109111
(editor as any).setMode(mode);
110112
}
111113
}
114+
};
115+
116+
export const getTinymceOrError = (view: Window) => {
117+
const tinymce = getTinymce(view);
118+
if (!tinymce) {
119+
throw new Error('tinymce should have been loaded into global scope');
120+
}
121+
122+
return tinymce;
123+
};
124+
125+
export const isDisabledOptionSupported = (view: Window) => {
126+
const tinymce = getTinymceOrError(view);
127+
128+
return !TinyVer.isLessThan(tinymce, '7.6.0');
112129
};

src/main/ts/components/Editor.tsx

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ import * as React from 'react';
22
import type { Bookmark, EditorEvent, TinyMCE, Editor as TinyMCEEditor } from 'tinymce';
33
import { IEvents } from '../Events';
44
import { ScriptItem, ScriptLoader } from '../ScriptLoader2';
5-
import { getTinymce } from '../TinyMCE';
6-
import { configHandlers, isBeforeInputEventAvailable, isFunction, isInDoc, isTextareaOrInput, mergePlugins, setMode, uuid } from '../Utils';
5+
import { configHandlers, isBeforeInputEventAvailable,
6+
isFunction, isInDoc, isTextareaOrInput, mergePlugins,
7+
setMode, uuid, isDisabledOptionSupported,
8+
getTinymceOrError } from '../Utils';
79
import { EditorPropTypes, IEditorPropTypes } from './EditorPropTypes';
10+
import { getTinymce } from '../TinyMCE';
811

912
const changeEvents = 'change keyup compositionend setcontent CommentChange';
1013

@@ -14,14 +17,15 @@ interface DoNotUse<T extends string> {
1417
__brand: T;
1518
}
1619

17-
type OmittedInitProps = 'selector' | 'target' | 'readonly' | 'license_key';
20+
type OmittedInitProps = 'selector' | 'target' | 'readonly' | 'disabled' | 'license_key';
1821

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

2124
export type InitOptions = Omit<OmitStringIndexSignature<EditorOptions>, OmittedInitProps> & {
2225
selector?: DoNotUse<'selector prop is handled internally by the component'>;
2326
target?: DoNotUse<'target prop is handled internally by the component'>;
24-
readonly?: DoNotUse<'readonly prop is overridden by the component, use the `disabled` prop instead'>;
27+
readonly?: DoNotUse<'readonly prop is overridden by the component'>;
28+
disabled?: DoNotUse<'disabled prop is overridden by the component'>;
2529
license_key?: DoNotUse<'license_key prop is overridden by the integration, use the `licenseKey` prop instead'>;
2630
} & { [key: string]: unknown };
2731

@@ -95,9 +99,14 @@ export interface IProps {
9599
toolbar: NonNullable<EditorOptions['toolbar']>;
96100
/**
97101
* @see {@link https://www.tiny.cloud/docs/tinymce/7/react-ref/#disabled React Tech Ref - disabled}
98-
* @description Whether the editor should be "disabled" (read-only).
102+
* @description Whether the editor should be disabled.
99103
*/
100104
disabled: boolean;
105+
/**
106+
* @see {@link https://www.tiny.cloud/docs/tinymce/7/react-ref/#readonly React Tech Ref - readonly}
107+
* @description Whether the editor should be readonly.
108+
*/
109+
readonly: boolean;
101110
/**
102111
* @see {@link https://www.tiny.cloud/docs/tinymce/7/react-ref/#textareaname React Tech Ref - textareaName}
103112
* @description Set the `name` attribute of the `textarea` element used for the editor in forms. Only valid in iframe mode.
@@ -209,9 +218,18 @@ export class Editor extends React.Component<IAllProps> {
209218
}
210219
});
211220
}
221+
222+
if (this.props.readonly !== prevProps.readonly) {
223+
const readonly = this.props.readonly ?? false;
224+
setMode(this.editor, readonly ? 'readonly' : 'design');
225+
}
226+
212227
if (this.props.disabled !== prevProps.disabled) {
213-
const disabled = this.props.disabled ?? false;
214-
setMode(this.editor, disabled ? 'readonly' : 'design');
228+
if (isDisabledOptionSupported(this.view)) {
229+
this.editor.options.set('disabled', this.props.disabled);
230+
} else {
231+
setMode(this.editor, this.props.disabled ? 'readonly' : 'design');
232+
}
215233
}
216234
}
217235
}
@@ -431,16 +449,16 @@ export class Editor extends React.Component<IAllProps> {
431449
return;
432450
}
433451

434-
const tinymce = getTinymce(this.view);
435-
if (!tinymce) {
436-
throw new Error('tinymce should have been loaded into global scope');
437-
}
452+
const tinymce = getTinymceOrError(this.view);
438453

439454
const finalInit: EditorOptions = {
440455
...this.props.init as Omit<InitOptions, OmittedInitProps>,
441456
selector: undefined,
442457
target,
443-
readonly: this.props.disabled,
458+
...isDisabledOptionSupported(this.view)
459+
? { disabled: this.props.disabled, readonly: this.props.readonly }
460+
: { readonly: this.props.disabled || this.props.readonly }
461+
,
444462
inline: this.inline,
445463
plugins: mergePlugins(this.props.init?.plugins, this.props.plugins),
446464
toolbar: this.props.toolbar ?? this.props.init?.toolbar,
@@ -477,8 +495,7 @@ export class Editor extends React.Component<IAllProps> {
477495
editor.undoManager.add();
478496
editor.setDirty(false);
479497
}
480-
const disabled = this.props.disabled ?? false;
481-
setMode(this.editor, disabled ? 'readonly' : 'design');
498+
482499
// ensure existing init_instance_callback is called
483500
if (this.props.init && isFunction(this.props.init.init_instance_callback)) {
484501
this.props.init.init_instance_callback(editor);

src/main/ts/components/EditorPropTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export const EditorPropTypes: IEditorPropTypes = {
101101
plugins: PropTypes.oneOfType([ PropTypes.string, PropTypes.array ]),
102102
toolbar: PropTypes.oneOfType([ PropTypes.string, PropTypes.array ]),
103103
disabled: PropTypes.bool,
104+
readonly: PropTypes.bool,
104105
textareaName: PropTypes.string,
105106
tinymceScriptSrc: PropTypes.oneOfType([
106107
PropTypes.string,

src/stories/Editor.stories.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,25 @@ export const ToggleDisabledProp: StoryObj<Editor> = {
145145
}
146146
};
147147

148+
export const ToggleReadonlyProp: StoryObj<Editor> = {
149+
render: () => {
150+
const [ readonly, setReadonly ] = React.useState(true);
151+
const toggleReadonly = () => setReadonly((prev) => !prev);
152+
return (
153+
<div>
154+
<Editor
155+
apiKey={apiKey}
156+
initialValue={initialValue}
157+
readonly={readonly}
158+
/>
159+
<button onClick={toggleReadonly}>
160+
{readonly ? 'Set editable' : 'Set Readonly'}
161+
</button>
162+
</div>
163+
);
164+
}
165+
};
166+
148167
export const CloudChannelSetTo5Dev: StoryObj<Editor> = {
149168
name: 'Cloud Channel Set To "6-dev"',
150169
render: () => (
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { context, describe, it } from '@ephox/bedrock-client';
2+
import * as Loader from '../alien/Loader';
3+
import { Assertions, Waiter } from '@ephox/agar';
4+
5+
describe('EditorDisabledTest', () => {
6+
7+
context('with TinyMCE < 7.6', () => {
8+
Loader.withVersion('7.5', (render) => {
9+
it('updating disabled prop should toggle the editor\'s mode', async () => {
10+
using ctx = await render({
11+
disabled: true
12+
});
13+
14+
Assertions.assertEq('mode is readonly', 'readonly', ctx.editor.mode.get());
15+
16+
await ctx.reRender({
17+
disabled: false
18+
});
19+
await Waiter.pTryUntil('mode is changed to design', () => {
20+
Assertions.assertEq('mode is design', 'design', ctx.editor.mode.get());
21+
});
22+
});
23+
24+
it('updating readonly prop should toggle the editor\'s mode', async () => {
25+
using ctx = await render({
26+
readonly: true
27+
});
28+
Assertions.assertEq('mode is readonly', 'readonly', ctx.editor.mode.get());
29+
30+
await ctx.reRender({
31+
readonly: false
32+
});
33+
await Waiter.pTryUntil('mode is changed to design', () => {
34+
Assertions.assertEq('mode is design', 'design', ctx.editor.mode.get());
35+
});
36+
});
37+
});
38+
});
39+
40+
context('with TinyMCE >= 7.6', () => {
41+
Loader.withVersion('7', (render) => {
42+
it('updating disabled prop should only change the editor\'s state', async () => {
43+
using ctx = await render({
44+
disabled: true
45+
});
46+
47+
Assertions.assertEq('mode is design', 'design', ctx.editor.mode.get());
48+
Assertions.assertEq('editor is disabled', true, ctx.editor.options.get('disabled'));
49+
50+
await ctx.reRender({
51+
disabled: false
52+
});
53+
54+
await Waiter.pTryUntil('editor\'s state should be updated', () => {
55+
Assertions.assertEq('mode is design', 'design', ctx.editor.mode.get());
56+
Assertions.assertEq('editor is not disabled', false, ctx.editor.options.get('disabled'));
57+
});
58+
});
59+
60+
it('updating readonly prop should only change the editor\'s mode', async () => {
61+
using ctx = await render({
62+
readonly: true
63+
});
64+
Assertions.assertEq('mode is readonly', 'readonly', ctx.editor.mode.get());
65+
Assertions.assertEq('editor is not disabled', false, ctx.editor.options.get('disabled'));
66+
67+
await ctx.reRender({
68+
readonly: false
69+
});
70+
await Waiter.pTryUntil('editor\'s mode should be updated', () => {
71+
Assertions.assertEq('mode is design', 'design', ctx.editor.mode.get());
72+
Assertions.assertEq('editor is not disabled', false, ctx.editor.options.get('disabled'));
73+
});
74+
});
75+
});
76+
});
77+
});

src/test/ts/browser/EditorInitTest.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,6 @@ describe('EditorInitTest', () => {
6969
TinyAssertions.assertContent(ctx.editor, '<p>New Value</p>');
7070
});
7171

72-
it('Disabled prop should disable editor', async () => {
73-
using ctx = await render();
74-
Assertions.assertEq('Should be design mode', true, '4' === version ? !ctx.editor.readonly : ctx.editor.mode.get() === 'design');
75-
await ctx.reRender({ ...defaultProps, disabled: true });
76-
Assertions.assertEq('Should be readonly mode', true, '4' === version ? ctx.editor.readonly : ctx.editor.mode.get() === 'readonly');
77-
});
78-
7972
it('Using an overriden props will cause a TS error', async () => {
8073
// eslint-disable-next-line @typescript-eslint/no-unused-vars
8174
using _ = await render({

yarn.lock

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8705,15 +8705,20 @@ tiny-invariant@^1.3.1, tiny-invariant@^1.3.3:
87058705
resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-6.8.4.tgz#53e1313ebfe5524b24c0fa45937d51fda058632d"
87068706
integrity sha512-okoJyxuPv1gzASxQDNgQbnUXOdAIyoOSXcXcZZu7tiW0PSKEdf3SdASxPBupRj+64/E3elHwVRnzSdo82Emqbg==
87078707

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

8713-
tinymce@^7.2.1:
8714-
version "7.7.1"
8715-
resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-7.7.1.tgz#e7c19f14153dc9bcde11bbc012db45167892c116"
8716-
integrity sha512-rMetqSgZtYbj4YPOX+gYgmlhy/sIjVlI/qlrSOul/Mpn9e0aIIG/fR0qvQSVYvxFv6OzRTge++NQyTbzLJK1NA==
8713+
"tinymce-7@npm:tinymce@^7":
8714+
version "7.8.0"
8715+
resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-7.8.0.tgz#d57a597aecdc2108f2dd68fe74c6099c0a0ef66f"
8716+
integrity sha512-MUER5MWV9mkOB4expgbWknh/C5ZJvOXQlMVSx4tJxTuYtcUCDB6bMZ34fWNOIc8LvrnXmGHGj0eGQuxjQyRgrA==
8717+
8718+
tinymce@^7:
8719+
version "7.8.0"
8720+
resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-7.8.0.tgz#d57a597aecdc2108f2dd68fe74c6099c0a0ef66f"
8721+
integrity sha512-MUER5MWV9mkOB4expgbWknh/C5ZJvOXQlMVSx4tJxTuYtcUCDB6bMZ34fWNOIc8LvrnXmGHGj0eGQuxjQyRgrA==
87178722

87188723
tinyrainbow@^1.2.0:
87198724
version "1.2.0"

0 commit comments

Comments
 (0)