Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f1ea6e7
Replace ts-node with tsx for running the demo due to an compatibility…
tiny-ben-tran Mar 22, 2026
fe823e3
INT-3358: Remove tinymce/miniature as dependency
tiny-ben-tran Mar 25, 2026
5020b0a
INT-3358: Remove @tinymce/miniature dependency
tiny-ben-tran Mar 26, 2026
ccda14f
Update test
tiny-ben-tran Mar 26, 2026
623bc37
Add more tests
tiny-ben-tran Apr 1, 2026
313577f
Update changelog
tiny-ben-tran Apr 1, 2026
47f9413
Correct ticket number in the changelog
tiny-ben-tran Apr 1, 2026
02ce3c5
Fix lining errors
tiny-ben-tran Apr 1, 2026
06ce570
Remove the accidentally added file
tiny-ben-tran Apr 6, 2026
f87ea90
Bump minor and corrected the heading in changelog
tiny-ben-tran Apr 6, 2026
cb188a9
Fixed tests
tiny-ben-tran Apr 7, 2026
b5e2bae
Remove eslint errors
tiny-ben-tran Apr 7, 2026
a94d177
Fix timeout and refactor
tiny-ben-tran Apr 7, 2026
631d802
Ignore pre-const lint error
tiny-ben-tran Apr 7, 2026
6bb424a
Move tinymce to dev
tiny-ben-tran Apr 7, 2026
001c1bb
Attempt to resolve a race condition
tiny-ben-tran Apr 7, 2026
c8b9043
Another go
tiny-ben-tran Apr 7, 2026
011d056
Run one test
tiny-ben-tran Apr 7, 2026
183f113
Repeat
tiny-ben-tran Apr 7, 2026
0e528d5
Run just one test
tiny-ben-tran Apr 7, 2026
3bbe818
Run disbledtest only
tiny-ben-tran Apr 7, 2026
842d0f6
Try again
tiny-ben-tran Apr 7, 2026
ff7b1d0
Fix lint errors
tiny-ben-tran Apr 7, 2026
f09c73e
Another try
tiny-ben-tran Apr 7, 2026
9448c31
Remove duplicated test
tiny-ben-tran Apr 7, 2026
f5a7108
Clear loader saved url
tiny-ben-tran Apr 7, 2026
789ac6f
Fix test failing
tiny-ben-tran Apr 7, 2026
977fd85
Give headless browser some time to fetch stuffs
tiny-ben-tran Apr 7, 2026
4a7e3a2
Resolve comments
tiny-ben-tran Apr 8, 2026
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: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Changed

- Moved `@tinymce/miniature` to devDependency. #INT-3358

## 2.3.2 - 2025-12-18

### Fixed
Expand All @@ -25,7 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## 2.2.0 - 2025-05-29

### Added
- New `readonly` attribute that can be used to toggle the editor's `readonly` mode. #TINY-11911
- New `disabled` attribute that can be used to toggle the editor's `disabled` option. #TINY-11911

## 2.1.0 - 2024-01-08

Expand Down
25 changes: 11 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"description": "Official TinyMCE Web Component",
"version": "2.3.3-rc",
"version": "2.4.0-rc",
"name": "@tinymce/tinymce-webcomponent",
"repository": {
"url": "https://github.com/tinymce/tinymce-webcomponent"
Expand All @@ -16,7 +16,7 @@
"test": "bedrock-auto -b chrome-headless -d src/test/ts",
"build": "tsc -p ./tsconfig.json && rollup -c rollup.config.js",
"lint": "yarn eslint src/**/*.ts",
"serve": "yarn ts-node -r esm src/demo/ts/Server.ts"
"serve": "yarn tsx src/demo/ts/Server.ts"
},
"keywords": [
"TinyMCE",
Expand All @@ -26,32 +26,29 @@
"license": "MIT",
"devDependencies": {
"@ephox/agar": "^8.0.1",
"@ephox/bedrock-client": "^15.0.0",
"@ephox/bedrock-server": "^15.0.0",
"@ephox/bedrock-client": "^16.0.0",
"@ephox/bedrock-server": "^16.2.0",
"@ephox/katamari": "^9.1.5",
"@ephox/sugar": "^9.2.1",
"@ephox/swag": "^4.6.0",
"@tinymce/beehive-flow": "^0.19.0",
"@tinymce/eslint-plugin": "^3.0.0",
"@types/esm": "^3.2.0",
"@types/express": "^5.0.0",
"@tinymce/miniature": "^6.0.0",
"@types/node": "^24.5.2",
"@typescript-eslint/eslint-plugin": "^8.44.0",
"@typescript-eslint/parser": "^8.44.0",
"eslint": "^9.36.0",
"eslint-config-eslint": "^13.0.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-prefer-arrow": "^1.2.3",
"esm": "^3.2.25",
"express": "^5.1.0",
"express": "^5.2.1",
"rollup": "^4.24.0",
"tinymce": "^8.0.0",
"ts-loader": "^9.4.2",
"ts-node": "^10.9.1",
"tinymce": "^8.4.0",
"tinymce-7.5.0": "npm:tinymce@7.5.0",
"tinymce-8": "npm:tinymce@8",
"tsx": "^4.21.0",
"typescript": "~5.9.2",
"webpack": "^5.75.0"
},
"dependencies": {
"@tinymce/miniature": "^6.0.0"
}
"dependencies": {}
}
5 changes: 2 additions & 3 deletions src/demo/html/disabled.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,11 @@ <h2>TinyMCE WebComponent Demo: Disabled and Readonly</h2>
<div id="ephox-ui">
<h2>Readonly mode</h2>
<button onclick="toggleReadonly()">Toggle readonly</button>
<tinymce-editor config="editorConfig" id="readonly_mode" readonly></tinymce-editor>
<tinymce-editor config="editorConfig" id="readonly_mode" readonly channel="6" api-key="prsghhxax677rv082a1zj9b7cgjuoaqysf7h8ayxi5ao43ha"></tinymce-editor>
<h2>Disabled state</h2>
<button onclick="toggleDisabled()" plugins="help">Toggle disabled</button>
<tinymce-editor config="editorConfig" id="disabled_state" disabled></tinymce-editor>
<tinymce-editor config="editorConfig" id="disabled_state" disabled channel="6" api-key="prsghhxax677rv082a1zj9b7cgjuoaqysf7h8ayxi5ao43ha"></tinymce-editor>
</div>
<script src="../../../node_modules/tinymce/tinymce.js"></script>
<script src="../../../dist/tinymce-webcomponent.js"></script>
</body>

Expand Down
10 changes: 4 additions & 6 deletions src/main/ts/component/Editor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Resolve, Obj, Fun, Global } from '@ephox/katamari';
import { TinyMCE, Editor } from 'tinymce';
import { ScriptLoader } from '../utils/ScriptLoader';
import { TinyVer } from '@tinymce/miniature';

type EditorOptions = Parameters<TinyMCE['init']>[0];
type EventHandler = Parameters<Editor['on']>[1];

Expand Down Expand Up @@ -80,7 +80,8 @@ const configAttributes: Record<string, (v: string) => unknown> = {
const configRenames: Record<string, string> = {};

// Function that checks if the disabled option is supported with the version used
const isDisabledOptionSupported = (tinymce: TinyMCE): boolean => !TinyVer.isLessThan(tinymce, '7.6.0');
const isDisabledOptionSupported = (editor: Editor | undefined): boolean =>
!!editor && typeof editor.options?.set === 'function' && editor.options.isRegistered('disabled');

class TinyMceEditor extends HTMLElement {
private _status: Status;
Expand Down Expand Up @@ -386,10 +387,7 @@ class TinyMceEditor extends HTMLElement {
}

set disabled(value: boolean) {
const tinymce = this._getTinymce?.();
const isVersionNewer = tinymce ? isDisabledOptionSupported(tinymce) : true;

if (this._editor && this._status === Status.Ready && isVersionNewer) {
if (this._editor && this._status === Status.Ready && isDisabledOptionSupported(this._editor)) {
this._editor.options.set('disabled', value);
}

Expand Down
39 changes: 39 additions & 0 deletions src/test/ts/alien/Utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Attribute, Remove, SelectorFilter, SugarElement } from '@ephox/sugar';
import { ScriptLoader } from 'src/main/ts/utils/ScriptLoader';
import { Arr, Strings, Optional, Fun } from '@ephox/katamari';
// eslint-disable-next-line @tinymce/no-direct-imports
import * as Globals from '@tinymce/miniature/lib/main/ts/loader/Globals';
import Editor from 'src/main/ts/component/Editor';

export const deleteTinymce = () => {
ScriptLoader.reinitialize();
Globals.deleteTinymceGlobals();

const hasTinyUri = (attrName: string) => (elm: SugarElement<Element>) =>
Attribute.getOpt(elm, attrName).exists((src) => Strings.contains(src, 'tinymce'));

const elements = Arr.flatten([
Arr.filter(SelectorFilter.all('script'), hasTinyUri('src')),
Arr.filter(SelectorFilter.all('link'), hasTinyUri('href')),
]);

Arr.each(elements, Remove.remove);
};

export const removeTinymceElements = () => {
Arr.each(SelectorFilter.all('tinymce-editor'), Remove.remove);
};

export const registerCustomElementIfNot = () => {
Optional.from(window.customElements?.get('tinymce-editor')).fold(Editor, Fun.noop);
};

export const createTinymceElement = (attrs: Record<string, string>, content?: string) => {
const ce = SugarElement.fromTag('tinymce-editor');
Attribute.setAll(ce, attrs);
if (content) {
ce.dom.innerHTML = content;
}
document.body.appendChild(ce.dom);
return ce;
};
123 changes: 123 additions & 0 deletions src/test/ts/browser/DisabledTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Assertions } from '@ephox/agar';
import { before, describe, it, context, after, afterEach } from '@ephox/bedrock-client';
import { Global } from '@ephox/katamari';
import type { Editor, TinyMCE, Editor as TinyMCEEditor } from 'tinymce';
import { VersionLoader, TinyVer } from '@tinymce/miniature';
import { createTinymceElement, deleteTinymce, registerCustomElementIfNot, removeTinymceElements } from '../alien/Utils';
import { Attribute, SugarElement } from '@ephox/sugar';

type EditorElement = HTMLElement & { disabled?: boolean };
declare const tinymce: TinyMCE;

describe('DisableTest', () => {
let uid = 0;
const nextId = () => `_disabled_test_fn_${uid++}`;

before(() => {
registerCustomElementIfNot();
Global.tinymceTestConfig = { license_key: 'gpl' };
});

after(() => {
delete Global.tinymceTestConfig;
});

const pCreateEditor =
(attrs: Record<string, string> = {}): Promise<{ element: SugarElement<EditorElement>; editor: TinyMCEEditor }> => new Promise((resolve) => {
const setupFnName = nextId();
// eslint-disable-next-line prefer-const
let tinymceEl: SugarElement<EditorElement>;

Global[setupFnName] = (editor: Editor) => {
editor.on('SkinLoaded', () => {
setTimeout(() => resolve({ element: tinymceEl, editor }), 500);
});
};

tinymceEl = createTinymceElement({
setup: setupFnName,
config: 'tinymceTestConfig',
...attrs
});
});

context('When using with Tinymce < 7.6', () => {
before(async () => {
await VersionLoader.pLoadVersion('7.5.0');
Assertions.assertEq('Tinymce 7.5.0 should be loaded',
'7.5.0',
TinyVer.getVersion(tinymce).major + '.' + TinyVer.getVersion(tinymce).minor + '.' + TinyVer.getVersion(tinymce).patch);
});

after(() => {
deleteTinymce();
});

it('Editor should be not be disabled when disabled attribute is present', async () => {
const { editor } = await pCreateEditor();
Assertions.assertEq('Editor should be in design mode', true, editor.mode.get() === 'design');
removeTinymceElements();
});
});

context('When using with Tinymce >= 7.6', () => {
before(async () => {
await VersionLoader.pLoadVersion('8');
Assertions.assertEq('Tinymce 8 should be loaded', '8', TinyVer.getVersion(tinymce).major + '');
});

afterEach(() => {
removeTinymceElements();
});

after(() => {
deleteTinymce();
});

const pWaitForDisabledStateChange = (editor: any): Promise<void> =>
new Promise((resolve) => editor.once('DisabledStateChange', resolve));

const assertDisabledState = (el: SugarElement<EditorElement>, editor: TinyMCEEditor, expected: boolean) => {
const hasDisabledAtt = Attribute.has(el, 'disabled');
Assertions.assertEq('Editor should be disabled', expected, editor.options.get('disabled'));
Assertions.assertEq(`disabled attribute should be ${expected ? 'present' : 'absent'}`, expected, hasDisabledAtt);
};

it('Editor should be disabled when disabled attribute is present', async () => {
const { element, editor } = await pCreateEditor({ disabled: '' });
assertDisabledState(element, editor, true);
});

it('Editor is not disabled when disabled attribute is absent', async () => {
const { element, editor } = await pCreateEditor();
assertDisabledState(element, editor, false);
});

it('Setting disabled attribute after init disables the editor', async () => {
const { element, editor } = await pCreateEditor();
assertDisabledState(element, editor, false);
Attribute.set(element, 'disabled', '');
await pWaitForDisabledStateChange(editor);
assertDisabledState(element, editor, true);
});

it('Removing disabled attribute after init enables the editor', async () => {
const { element, editor } = await pCreateEditor({ disabled: '' });
assertDisabledState(element, editor, true);
Attribute.remove(element, 'disabled');
await pWaitForDisabledStateChange(editor);
assertDisabledState(element, editor, false);
});

it('Updating disabled property directly syncs editor option and attribute', async () => {
const { element, editor } = await pCreateEditor();
Assertions.assertEq('disabled property should be false initially', false, element.dom.disabled);
element.dom.disabled = true;
await pWaitForDisabledStateChange(editor);
assertDisabledState(element, editor, true);
element.dom.disabled = false;
await pWaitForDisabledStateChange(editor);
assertDisabledState(element, editor, false);
});
});
});
80 changes: 39 additions & 41 deletions src/test/ts/browser/LoadTest.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,48 @@
import { Pipeline, Step, Waiter, Assertions } from '@ephox/agar';
import { SugarElement, Attribute, SugarBody, Insert, Remove, SelectorFilter, TextContent } from '@ephox/sugar';
import { UnitTest } from '@ephox/bedrock-client';
import Editor from '../../../main/ts/component/Editor';
import { Arr, Global } from '@ephox/katamari';
import { Assertions } from '@ephox/agar';
import { before, describe, after, it } from '@ephox/bedrock-client';
import { Global } from '@ephox/katamari';
import { createTinymceElement, deleteTinymce, registerCustomElementIfNot, removeTinymceElements } from '../alien/Utils';
import { Editor } from 'tinymce';
import { VersionLoader } from '@tinymce/miniature';

const makeTinymceElement = (attrs: Record<string, string>, content: string) => {
const ce = SugarElement.fromTag('tinymce-editor');
Attribute.set(ce, 'channel', '8');
Attribute.setAll(ce, attrs);
TextContent.set(ce, content);
Insert.append(SugarBody.body(), ce);
};
describe('LoadTest', () => {
before(async () => {
await VersionLoader.pLoadVersion('8');
registerCustomElementIfNot();
Global.tinymceTestConfig = { license_key: 'gpl' };
});

const removeTinymceElement = () => {
Arr.map(SelectorFilter.all('tinymce-editor'), Remove.remove);
};
after(() => {
delete Global.tinymceTestConfig;
deleteTinymce();
});

UnitTest.asynctest('LoadTest', (success, failure) => {
Editor();
let seenSetup = false;
let seenInit = false;
let editorInstance: any;
Pipeline.async('', [
Step.sync(() => {
Global.customElementTinymceSetup = (editor: any) => {
it('Should load the editor and execute setup and init callbacks', async () => {
let seenSetup = false;
let seenInit = false;

const { editor } = await new Promise<{ editor: Editor }>((resolve) => {
Global.customElementTinymceSetup = (ed: Editor) => {
seenSetup = true;
editorInstance = editor;
ed.on('SkinLoaded', () => {
setTimeout(() => resolve({ editor: ed }), 500);
});
};
Global.customElementTinymceInit = (_evt: unknown) => {
seenInit = true;
};
}),
Step.sync(() => makeTinymceElement({
'setup': 'customElementTinymceSetup',
'on-init': 'customElementTinymceInit',
'id': 'example_id'
}, '<p>Hello world</p>')),
Waiter.sTryUntilPredicate('Waiting for editor setup', () => seenSetup),
Waiter.sTryUntilPredicate('Waiting for editor init', () => seenInit),
Step.sync(() => {
Assertions.assertHtmlStructure('', '<p>Hello world</p>', editorInstance.getContent() as string);
Assertions.assertEq('An editor instance is registered', true, Global.tinymce.get('example_id') !== null);
}),
Step.sync(() => removeTinymceElement()),
Step.sync(() => {
Assertions.assertEq('The editor instance is removed', true, Global.tinymce.get('example_id') === null);
})
], success, failure);
createTinymceElement({
'setup': 'customElementTinymceSetup',
'on-init': 'customElementTinymceInit',
'config': 'tinymceTestConfig',
'id': 'example_id'
}, '<p>Hello world</p>');
});

Assertions.assertEq('Editor setup callback should be called', true, seenSetup);
Assertions.assertEq('Editor init callback should be called', true, seenInit);
Assertions.assertEq('An editor instance is registered', true, Global.tinymce.get('example_id') !== null);
Assertions.assertHtmlStructure('The editor has the correct content', '<p>Hello world</p>', editor.getContent() as string);
removeTinymceElements();
});
});
Loading
Loading