diff --git a/core-web/apps/dotcms-ui/project.json b/core-web/apps/dotcms-ui/project.json index 419b3e4c7570..a9b08db9a6ad 100644 --- a/core-web/apps/dotcms-ui/project.json +++ b/core-web/apps/dotcms-ui/project.json @@ -18,6 +18,11 @@ "assets": [ "apps/dotcms-ui/src/favicon.ico", "apps/dotcms-ui/src/assets", + { + "glob": "**/*", + "input": "node_modules/tinymce", + "output": "/tinymce/" + }, { "glob": "**/*", "input": "node_modules/monaco-editor", diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.html b/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.html index aaf23a98b415..f0295e60e405 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.html +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.html @@ -77,6 +77,11 @@ *ngSwitchCase="fieldTypes.KEY_VALUE" [formControlName]="field.variable" [attr.data-testId]="'field-' + field.variable" /> + + {{ field.hint }} diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.spec.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.spec.ts index ded8eb8f6c7f..333f08b83358 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.spec.ts +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.spec.ts @@ -1,5 +1,6 @@ import { describe } from '@jest/globals'; import { byTestId, createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest'; +import { EditorComponent } from '@tinymce/tinymce-angular'; import { MockComponent } from 'ng-mocks'; import { of } from 'rxjs'; @@ -30,6 +31,7 @@ import { DotEditContentSelectFieldComponent } from '../../fields/dot-edit-conten import { DotEditContentTagFieldComponent } from '../../fields/dot-edit-content-tag-field/dot-edit-content-tag-field.component'; import { DotEditContentTextAreaComponent } from '../../fields/dot-edit-content-text-area/dot-edit-content-text-area.component'; import { DotEditContentTextFieldComponent } from '../../fields/dot-edit-content-text-field/dot-edit-content-text-field.component'; +import { DotWYSIWYGFieldComponent } from '../../fields/dot-wysiwyg-field/dot-wysiwyg-field.component'; import { FIELD_TYPES } from '../../models/dot-edit-content-field.enum'; import { DotEditContentService } from '../../services/dot-edit-content.service'; import { @@ -122,6 +124,10 @@ const FIELD_TYPES_COMPONENTS: Record | DotEditFieldTe component: DotEditContentKeyValueComponent, declarations: [MockComponent(DotKeyValueComponent)], providers: [mockProvider(DotMessageDisplayService)] + }, + [FIELD_TYPES.WYSIWYG]: { + component: DotWYSIWYGFieldComponent, + declarations: [MockComponent(EditorComponent)] } }; diff --git a/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.ts b/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.ts index eb97cc58e43b..34ebe3afe029 100644 --- a/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.ts +++ b/core-web/libs/edit-content/src/lib/components/dot-edit-content-field/dot-edit-content-field.component.ts @@ -9,6 +9,7 @@ import { DotFieldRequiredDirective } from '@dotcms/ui'; import { DotEditContentBinaryFieldComponent } from '../../fields/dot-edit-content-binary-field/dot-edit-content-binary-field.component'; import { DotEditContentFieldsModule } from '../../fields/dot-edit-content-fields.module'; import { DotEditContentKeyValueComponent } from '../../fields/dot-edit-content-key-value/dot-edit-content-key-value.component'; +import { DotWYSIWYGFieldComponent } from '../../fields/dot-wysiwyg-field/dot-wysiwyg-field.component'; import { CALENDAR_FIELD_TYPES } from '../../models/dot-edit-content-field.constant'; import { FIELD_TYPES } from '../../models/dot-edit-content-field.enum'; @@ -33,7 +34,8 @@ import { FIELD_TYPES } from '../../models/dot-edit-content-field.enum'; DotFieldRequiredDirective, BlockEditorModule, DotEditContentBinaryFieldComponent, - DotEditContentKeyValueComponent + DotEditContentKeyValueComponent, + DotWYSIWYGFieldComponent ] }) export class DotEditContentFieldComponent { diff --git a/core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-wysiwyg-field.component.html b/core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-wysiwyg-field.component.html new file mode 100644 index 000000000000..a12bdf282cbf --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-wysiwyg-field.component.html @@ -0,0 +1 @@ + diff --git a/core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-wysiwyg-field.component.scss b/core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-wysiwyg-field.component.scss new file mode 100644 index 000000000000..b10802be3462 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-wysiwyg-field.component.scss @@ -0,0 +1,7 @@ +:host::ng-deep { + // Hide the promotion button + // This button redirect to the tinyMCE premium page + .tox-promotion { + display: none; + } +} diff --git a/core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-wysiwyg-field.component.spec.ts b/core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-wysiwyg-field.component.spec.ts new file mode 100644 index 000000000000..4e30beac1231 --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-wysiwyg-field.component.spec.ts @@ -0,0 +1,50 @@ +import { Spectator, createComponentFactory } from '@ngneat/spectator'; +import { EditorComponent, EditorModule } from '@tinymce/tinymce-angular'; +import { MockComponent } from 'ng-mocks'; + +import { + ControlContainer, + FormGroupDirective, + FormsModule, + ReactiveFormsModule +} from '@angular/forms'; + +import { DotWYSIWYGFieldComponent } from './dot-wysiwyg-field.component'; + +import { WYSIWYG_MOCK, createFormGroupDirectiveMock } from '../../utils/mocks'; + +const ALL_PLUGINS = + 'advlist autolink lists link image charmap preview anchor pagebreak searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking save table directionality emoticons template'; +const ALL_TOOLBAR_ITEMS = + 'paste print textpattern | insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image hr | preview | validation media | forecolor dotimageclipboard backcolor emoticons'; + +describe('DotWYSIWYGFieldComponent', () => { + let spectator: Spectator; + const createComponent = createComponentFactory({ + component: DotWYSIWYGFieldComponent, + imports: [EditorModule, FormsModule, ReactiveFormsModule], + declarations: [MockComponent(EditorComponent)], + componentViewProviders: [ + { + provide: ControlContainer, + useValue: createFormGroupDirectiveMock() + } + ], + providers: [FormGroupDirective] + }); + + beforeEach(() => { + spectator = createComponent({ + props: { + field: WYSIWYG_MOCK + } + }); + }); + + it('should instance WYSIWYG editor and set the correct plugins and toolbar items', () => { + const editor = spectator.query(EditorComponent); + expect(editor).toBeTruthy(); + expect(editor.plugins).toEqual(ALL_PLUGINS); + expect(editor.toolbar).toEqual(ALL_TOOLBAR_ITEMS); + }); +}); diff --git a/core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-wysiwyg-field.component.ts b/core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-wysiwyg-field.component.ts new file mode 100644 index 000000000000..7d01bc07171d --- /dev/null +++ b/core-web/libs/edit-content/src/lib/fields/dot-wysiwyg-field/dot-wysiwyg-field.component.ts @@ -0,0 +1,32 @@ +import { EditorModule, TINYMCE_SCRIPT_SRC } from '@tinymce/tinymce-angular'; + +import { ChangeDetectionStrategy, Component, Input, inject, signal } from '@angular/core'; +import { ControlContainer, FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { DotCMSContentTypeField } from '@dotcms/dotcms-models'; + +@Component({ + selector: 'dot-wysiwyg-field', + standalone: true, + imports: [EditorModule, FormsModule, ReactiveFormsModule], + templateUrl: './dot-wysiwyg-field.component.html', + styleUrl: './dot-wysiwyg-field.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [{ provide: TINYMCE_SCRIPT_SRC, useValue: 'tinymce/tinymce.min.js' }], + viewProviders: [ + { + provide: ControlContainer, + useFactory: () => inject(ControlContainer, { skipSelf: true }) + } + ] +}) +export class DotWYSIWYGFieldComponent { + @Input() field!: DotCMSContentTypeField; + + protected readonly plugins = signal( + 'advlist autolink lists link image charmap preview anchor pagebreak searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking save table directionality emoticons template' + ); + protected readonly toolbar = signal( + 'paste print textpattern | insertfile undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image hr | preview | validation media | forecolor dotimageclipboard backcolor emoticons' + ); +} diff --git a/core-web/libs/edit-content/src/lib/models/dot-edit-content-field.enum.ts b/core-web/libs/edit-content/src/lib/models/dot-edit-content-field.enum.ts index e07f833a9dee..7fc7ecfb8683 100644 --- a/core-web/libs/edit-content/src/lib/models/dot-edit-content-field.enum.ts +++ b/core-web/libs/edit-content/src/lib/models/dot-edit-content-field.enum.ts @@ -23,5 +23,6 @@ export enum FIELD_TYPES { BINARY = 'Binary', CUSTOM_FIELD = 'Custom-Field', JSON = 'JSON-Field', - KEY_VALUE = 'Key-Value' + KEY_VALUE = 'Key-Value', + WYSIWYG = 'WYSIWYG' } diff --git a/core-web/libs/edit-content/src/lib/utils/mocks.ts b/core-web/libs/edit-content/src/lib/utils/mocks.ts index 6b21333419ae..8fa94bc86b29 100644 --- a/core-web/libs/edit-content/src/lib/utils/mocks.ts +++ b/core-web/libs/edit-content/src/lib/utils/mocks.ts @@ -539,6 +539,30 @@ export const KEY_VALUE_MOCK: DotCMSContentTypeField = { variable: 'KeyValue' }; +export const WYSIWYG_MOCK: DotCMSContentTypeField = { + clazz: 'com.dotcms.contenttype.model.field.ImmutableWYSIWYGField', + contentTypeId: '93ebaff75f3e3887bea73ecd04588dc9', + dataType: 'TEXT', + fieldType: 'WYSIWYG', + fieldTypeLabel: 'WYSIWYG', + fieldVariables: [], + fixed: false, + hint: 'A hint text', + iDate: 1698291913000, + id: '96909fa20a00497cd3b766b52edac0ec', + indexed: false, + listed: false, + modDate: 1698291913000, + name: 'WYSIWYG', + readOnly: false, + required: false, + searchable: false, + sortOrder: 1, + unique: false, + values: '

HELLO

', + variable: 'WYSIWYG' +}; + export const FIELDS_MOCK: DotCMSContentTypeField[] = [ TEXT_FIELD_MOCK, TEXT_AREA_FIELD_MOCK, @@ -560,7 +584,8 @@ export const FIELDS_MOCK: DotCMSContentTypeField[] = [ BINARY_FIELD_MOCK, CUSTOM_FIELD_MOCK, JSON_FIELD_MOCK, - KEY_VALUE_MOCK + KEY_VALUE_MOCK, + WYSIWYG_MOCK ]; export const FIELD_MOCK: DotCMSContentTypeField = TEXT_FIELD_MOCK; diff --git a/core-web/package.json b/core-web/package.json index 5ab9bc8ee8d9..a4d6128a2032 100644 --- a/core-web/package.json +++ b/core-web/package.json @@ -73,6 +73,7 @@ "@nx/angular": "18.0.4", "@swc/helpers": "~0.5.2", "@tarekraafat/autocomplete.js": "^10.2.6", + "@tinymce/tinymce-angular": "^7.0.0", "@tiptap/core": "^2.0.0-beta.218", "@tiptap/extension-bubble-menu": "^2.0.0-beta.218", "@tiptap/extension-character-count": "^2.0.0-beta.218", @@ -126,6 +127,7 @@ "superstruct": "^1.0.3", "terser": "^5.28.1", "test": "^0.6.0", + "tinymce": "^6.8.3", "tslib": "^2.3.0", "uuid": "^9.0.0", "zone.js": "0.14.2" diff --git a/core-web/yarn.lock b/core-web/yarn.lock index a22f0d8615fd..10d511ad4a20 100644 --- a/core-web/yarn.lock +++ b/core-web/yarn.lock @@ -6122,6 +6122,14 @@ resolved "https://registry.npmjs.org/@thednp/shorty/-/shorty-2.0.0.tgz#e05e6754032c091f599ac0c4ed7efde6558bdedb" integrity sha512-kwtLivCxYIoFfGIVU4NlZtfdA/zxZ6X8UcWaJrb7XqU3WQ4Q1p5IaZlLBfOVAO06WH5oWE87QUdK/dS56Wnfjg== +"@tinymce/tinymce-angular@^7.0.0": + version "7.0.0" + resolved "https://registry.npmjs.org/@tinymce/tinymce-angular/-/tinymce-angular-7.0.0.tgz#010de497d5774a8bdc5d5936bf4fb976adf05f56" + integrity sha512-IKNaG/ihlxE1XCfq6lzULbnsqZO9KNJtlpu5jo6JDJDL9zcFzj/N2A16Kk7rTj1yfmDoB1IXAk/BpMOvgDY8cg== + dependencies: + tinymce "^6.0.0 || ^5.5.0" + tslib "^2.3.0" + "@tiptap/core@^2.0.0-beta.218", "@tiptap/core@^2.2.2": version "2.2.2" resolved "https://registry.npmjs.org/@tiptap/core/-/core-2.2.2.tgz#7664197dafee890a5f42ba03b50c202bdf1da761" @@ -21994,6 +22002,11 @@ tiny-relative-date@^1.3.0: resolved "https://registry.npmjs.org/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07" integrity sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A== +"tinymce@^6.0.0 || ^5.5.0", tinymce@^6.8.3: + version "6.8.3" + resolved "https://registry.npmjs.org/tinymce/-/tinymce-6.8.3.tgz#0025a4aaa4c24dc2a3e32e83dfda705d196fd802" + integrity sha512-3fCHKAeqT+xNwBVESf6iDbDV0VNwZNmfrkx9c/6Gz5iB8piMfaO6s7FvoiTrj1hf1gVbfyLTnz1DooI6DhgINQ== + tippy.js@^6.3.7: version "6.3.7" resolved "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c"