diff --git a/CHANGELOG.md b/CHANGELOG.md index ba844720..c5ba703c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added +- Added 'readonly' property. #TINY-11907 + ### Fixed - Updated dependencies. #INT-3324 ### Changed - Moved tinymce dependency to be a optional peer dependency. #INT-3324 - Updated tinymce dev dependency to version ^7 from 5.10.7 so now all internal tinymce types point to version 7. #INT-3324 +- The 'disabled' property is now mapped to editor's 'disabled' option if Tiny >= 7.6.0 is used. #TINY-11907 ## 8.0.1 - 2024-07-12 diff --git a/package.json b/package.json index a7070adc..92d88979 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "tinymce-5": "npm:tinymce@^5", "tinymce-6": "npm:tinymce@^6", "tinymce-7": "npm:tinymce@^7", + "tinymce-7.5.0": "npm:tinymce@7.5.0", "to-string-loader": "^1.1.5", "tslib": "^2.6.2", "typescript": "~5.5.4", diff --git a/stories/Editor.stories.ts b/stories/Editor.stories.ts index 39146eb4..821d26f6 100644 --- a/stories/Editor.stories.ts +++ b/stories/Editor.stories.ts @@ -15,6 +15,7 @@ import { MatTabsModule } from '@angular/material/tabs'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ContainerComponent, ContentProjectionComponent } from './contentprojection/ContentProjection.component'; import { BindingComponent } from './data-binding/DataBinding.component'; +import { ReadonlyComponent } from './readonly/Readonly.component'; const meta: Meta = { component: EditorComponent, @@ -137,6 +138,20 @@ export const DisablingStory: StoryObj = { } }; +export const ReadonlyStory: StoryObj = { + name: 'Readonly', + render: () => ({ + moduleMetadata: { + imports: [ ReactiveFormsModule, FormsModule ], + declarations: [ ReadonlyComponent ], + }, + template: `` + }), + parameters: { + notes: 'Example of toggling readonly state in the editor component' + } +}; + export const ViewQueryStory: StoryObj = { name: 'View Query', render: () => ({ diff --git a/stories/readonly/Readonly.component.html b/stories/readonly/Readonly.component.html new file mode 100644 index 00000000..f4aea026 --- /dev/null +++ b/stories/readonly/Readonly.component.html @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/stories/readonly/Readonly.component.ts b/stories/readonly/Readonly.component.ts new file mode 100644 index 00000000..a53970eb --- /dev/null +++ b/stories/readonly/Readonly.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { apiKey, sampleContent } from '../Settings'; + +@Component({ + selector: 'readonly', + templateUrl: './Readonly.component.html', +}) +export class ReadonlyComponent { + public isReadonly = false; + public apiKey = apiKey; + public initialValue = sampleContent; + public toggleReadonly = () => (this.isReadonly = !this.isReadonly); +} diff --git a/tinymce-angular-component/src/main/ts/editor/editor.component.ts b/tinymce-angular-component/src/main/ts/editor/editor.component.ts index 4ed7572f..538e05da 100644 --- a/tinymce-angular-component/src/main/ts/editor/editor.component.ts +++ b/tinymce-angular-component/src/main/ts/editor/editor.component.ts @@ -18,7 +18,8 @@ import { import { FormsModule, ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Subject, takeUntil } from 'rxjs'; import { getTinymce } from '../TinyMCE'; -import { listenTinyMCEEvent, bindHandlers, isTextarea, mergePlugins, uuid, noop, isNullOrUndefined } from '../utils/Utils'; +import { listenTinyMCEEvent, bindHandlers, isTextarea, mergePlugins, uuid, noop, isNullOrUndefined, setMode } from '../utils/Utils'; +import * as DisabledUtils from '../utils/DisabledUtils'; import { EventObj, Events } from './Events'; import { ScriptLoader } from '../utils/ScriptLoader'; import type { Editor as TinyMCEEditor, TinyMCE } from 'tinymce'; @@ -64,14 +65,26 @@ export class EditorComponent extends Events implements AfterViewInit, ControlVal @Input() public modelEvents = 'change input undo redo'; @Input() public allowedEvents?: string | string[]; @Input() public ignoreEvents?: string | string[]; + @Input() + public set readonly(val) { + this._readonly = val; + if (this._editor && this._editor.initialized) { + setMode(this._editor, val ? 'readonly' : 'design'); + } + } + + public get readonly() { + return this._readonly; + } + @Input() public set disabled(val) { this._disabled = val; if (this._editor && this._editor.initialized) { - if (typeof this._editor.mode?.set === 'function') { - this._editor.mode.set(val ? 'readonly' : 'design'); - } else if ('setMode' in this._editor && typeof this._editor.setMode === 'function') { - this._editor.setMode(val ? 'readonly' : 'design'); + if (DisabledUtils.isDisabledOptionSupported()) { + this._editor.options.set('disabled', val ?? false); + } else { + setMode(this._editor, val ? 'readonly' : 'design'); } } } @@ -89,6 +102,7 @@ export class EditorComponent extends Events implements AfterViewInit, ControlVal private _elementRef: ElementRef; private _element?: HTMLElement; private _disabled?: boolean; + private _readonly?: boolean; private _editor?: TinyMCEEditor; private onTouchedCallback = noop; @@ -176,7 +190,10 @@ export class EditorComponent extends Events implements AfterViewInit, ControlVal selector: undefined, target: this._element, inline: this.inline, - readonly: this.disabled, + ...( DisabledUtils.isDisabledOptionSupported() + ? { disabled: this.disabled, readonly: this.readonly } + : { readonly: this.disabled || this.readonly } + ), license_key: this.licenseKey, plugins: mergePlugins((this.init && this.init.plugins) as string, this.plugins), toolbar: this.toolbar || (this.init && this.init.toolbar), diff --git a/tinymce-angular-component/src/main/ts/utils/DisabledUtils.ts b/tinymce-angular-component/src/main/ts/utils/DisabledUtils.ts new file mode 100644 index 00000000..43173f64 --- /dev/null +++ b/tinymce-angular-component/src/main/ts/utils/DisabledUtils.ts @@ -0,0 +1,12 @@ +import { getTinymce } from '../TinyMCE'; +import { TinyMCE } from 'tinymce'; + +const isDisabledOptionSupported = () => { + const tiny: TinyMCE = getTinymce(); + // Disabled option is supported since Tiny 7.6.0 + return Number(tiny.majorVersion) > 7 || (Number(tiny.majorVersion) === 7 && Number(tiny.minorVersion) >= 6); +}; + +export { + isDisabledOptionSupported +}; diff --git a/tinymce-angular-component/src/main/ts/utils/Utils.ts b/tinymce-angular-component/src/main/ts/utils/Utils.ts index 8ee194e8..f36f299e 100644 --- a/tinymce-angular-component/src/main/ts/utils/Utils.ts +++ b/tinymce-angular-component/src/main/ts/utils/Utils.ts @@ -12,6 +12,7 @@ import { HasEventTargetAddRemove } from 'rxjs/internal/observable/fromEvent'; import { EditorComponent } from '../editor/editor.component'; import { validEvents, Events } from '../editor/Events'; +import { Editor } from 'tinymce'; // Caretaker note: `fromEvent` supports passing JQuery-style event targets, the editor has `on` and `off` methods which // will be invoked upon subscription and teardown. @@ -47,10 +48,10 @@ const getValidEvents = (ctx: EditorComponent): (keyof Events)[] => { }; const parseStringProperty = (property: string | string[] | undefined, defaultValue: (keyof Events)[]): string[] => { - if ( typeof property === 'string') { + if (typeof property === 'string') { return property.split(',').map((value) => value.trim()); } - if ( Array.isArray(property)) { + if (Array.isArray(property)) { return property; } return defaultValue; @@ -91,6 +92,14 @@ const isObserved = (o: Subject): boolean => // checking if a subject has observers. o.observed || o.observers?.length > 0; +const setMode = (editor: Editor, mode: 'readonly' | 'design') => { + if (typeof editor.mode?.set === 'function') { + editor.mode.set(mode); + } else if ('setMode' in editor && typeof editor.setMode === 'function') { + editor.setMode(mode); + } +}; + export { listenTinyMCEEvent, bindHandlers, @@ -99,5 +108,6 @@ export { normalizePluginArray, mergePlugins, noop, - isNullOrUndefined + isNullOrUndefined, + setMode }; diff --git a/tinymce-angular-component/src/test/ts/browser/DisabledPropertyTest.ts b/tinymce-angular-component/src/test/ts/browser/DisabledPropertyTest.ts new file mode 100644 index 00000000..bf5f6e76 --- /dev/null +++ b/tinymce-angular-component/src/test/ts/browser/DisabledPropertyTest.ts @@ -0,0 +1,107 @@ +import { Assertions } from '@ephox/agar'; +import '../alien/InitTestEnvironment'; + +import { EditorComponent } from '../../../main/ts/public_api'; +import { describe, it } from '@ephox/bedrock-client'; +import { eachVersionContext, editorHook } from '../alien/TestHooks'; +import { Editor } from 'tinymce'; + +describe('DisabledPropertyTest', () => { + const getMode = (editor: Editor) => { + if (typeof editor.mode?.get === 'function') { + return editor.mode.get(); + } + return editor.readonly ? 'readonly' : 'design'; + }; + const assertDesignMode = (editor: Editor) => Assertions.assertEq('TinyMCE should be in design mode', 'design', getMode(editor)); + const assertReadonlyMode = (editor: Editor) => Assertions.assertEq('TinyMCE should be in readonly mode', 'readonly', getMode(editor)); + const assertDisabledOption = (editor: Editor, expected: boolean) => + Assertions.assertEq(`TinyMCE should have disabled option set to ${expected}`, expected, editor.options.get('disabled')); + + eachVersionContext([ '7.5.0' ], () => { + const createFixture = editorHook(EditorComponent); + + it(`Component 'disabled' property is mapped to editor 'readonly' property`, async () => { + const { editor } = await createFixture({ disabled: true }); + assertReadonlyMode(editor); + }); + + it(`Toggling component's 'disabled' property is mapped to editor 'readonly' property`, async () => { + const fixture = await createFixture(); + const { editor } = fixture; + + assertDesignMode(editor); + + fixture.componentRef.setInput('disabled', true); + fixture.detectChanges(); + assertReadonlyMode(editor); + + fixture.componentRef.setInput('disabled', false); + fixture.detectChanges(); + assertDesignMode(editor); + }); + + it(`[disabled]=true [readonly]=false triggers readonly mode`, async () => { + const { editor } = await createFixture({ disabled: true, readonly: false }); + assertReadonlyMode(editor); + }); + + it(`[disabled]=false [readonly]=true triggers readonly mode`, async () => { + const { editor } = await createFixture({ disabled: false, readonly: true }); + assertReadonlyMode(editor); + }); + }); + + eachVersionContext([ '7' ], () => { + const createFixture = editorHook(EditorComponent); + + it(`Component 'disabled' property is mapped to editor 'disabled' property`, async () => { + const { editor } = await createFixture({ disabled: true }); + + Assertions.assertEq('TinyMCE should have disabled option set to true', true, editor.options.get('disabled')); + assertDesignMode(editor); + }); + + it(`Toggling component's 'disabled' property is mapped to editor 'disabled' property`, async () => { + const fixture = await createFixture(); + const { editor } = fixture; + + assertDesignMode(editor); + assertDisabledOption(editor, false); + + fixture.componentRef.setInput('disabled', true); + fixture.detectChanges(); + assertDesignMode(editor); + assertDisabledOption(editor, true); + + fixture.componentRef.setInput('disabled', false); + fixture.detectChanges(); + assertDesignMode(editor); + assertDisabledOption(editor, false); + }); + }); + + eachVersionContext([ '4', '5', '6', '7' ], () => { + const createFixture = editorHook(EditorComponent); + + it(`Setting the 'readonly' property causing readonly mode`, async () => { + const { editor } = await createFixture({ readonly: true }); + assertReadonlyMode(editor); + }); + + it(`Toggling component's 'readonly' property is mapped to editor 'readonly' mode`, async () => { + const fixture = await createFixture(); + const { editor } = fixture; + + assertDesignMode(editor); + + fixture.componentRef.setInput('readonly', true); + fixture.detectChanges(); + assertReadonlyMode(editor); + + fixture.componentRef.setInput('readonly', false); + fixture.detectChanges(); + assertDesignMode(editor); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 348cecb0..91549f65 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13082,10 +13082,15 @@ tiny-invariant@^1.3.1, tiny-invariant@^1.3.3: resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-6.8.3.tgz#0025a4aaa4c24dc2a3e32e83dfda705d196fd802" integrity sha512-3fCHKAeqT+xNwBVESf6iDbDV0VNwZNmfrkx9c/6Gz5iB8piMfaO6s7FvoiTrj1hf1gVbfyLTnz1DooI6DhgINQ== +"tinymce-7.5.0@npm:tinymce@7.5.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-7.5.0.tgz#56388d314399c288a100df4aaf468153f29477f1" + integrity sha512-A7iuQPIfeze5rO6bvnnPwP7TiWnPA9AGr8U/9ssLwrEG+FMYPzvLPt3RT8ktVn/wPSJkVBBSLCAZX2dAHb8YEA== + "tinymce-7@npm:tinymce@^7": - version "7.1.2" - resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-7.1.2.tgz#cb40e527dc03d6a0547a23c91231a946e50dae03" - integrity sha512-I/M5WRyEJjwIhyIv6FhkvZS1mWNbb0sIEvDkP8akBnuV1X78mkNhi6Kz9FBBbHzy61U3pmXgzyCSaDZfdQbCSg== + version "7.8.0" + resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-7.8.0.tgz#d57a597aecdc2108f2dd68fe74c6099c0a0ef66f" + integrity sha512-MUER5MWV9mkOB4expgbWknh/C5ZJvOXQlMVSx4tJxTuYtcUCDB6bMZ34fWNOIc8LvrnXmGHGj0eGQuxjQyRgrA== tinymce@^7: version "7.2.1"