diff --git a/core-web/libs/sdk/angular/package.json b/core-web/libs/sdk/angular/package.json
index 477fcedcdf0d..e8d0e48b69ab 100644
--- a/core-web/libs/sdk/angular/package.json
+++ b/core-web/libs/sdk/angular/package.json
@@ -1,14 +1,17 @@
{
"name": "@dotcms/angular",
- "version": "0.0.1-alpha.32",
+ "version": "0.0.1-alpha.33",
+ "dependencies": {
+ "@tinymce/tinymce-angular": "^8.0.0"
+ },
"peerDependencies": {
"@angular/common": "^17.1.0",
"@angular/core": "^17.1.0",
"@angular/router": "^17.1.0",
- "@dotcms/client": "0.0.1-alpha.32",
- "@tinymce/tinymce-angular": "^8.0.0",
+ "@dotcms/client": "0.0.1-alpha.33",
"rxjs": "^7.8.0"
},
+ "allowedNonPeerDependencies": ["@tinymce/tinymce-angular"],
"description": "Official Angular Components library to render a dotCMS page.",
"repository": {
"type": "git",
diff --git a/core-web/libs/sdk/angular/src/lib/components/dot-editable-text/dot-editable-text.component.ts b/core-web/libs/sdk/angular/src/lib/components/dot-editable-text/dot-editable-text.component.ts
index 67bdc3d2db94..0e50404fb0f2 100644
--- a/core-web/libs/sdk/angular/src/lib/components/dot-editable-text/dot-editable-text.component.ts
+++ b/core-web/libs/sdk/angular/src/lib/components/dot-editable-text/dot-editable-text.component.ts
@@ -22,7 +22,7 @@ import {
postMessageToEditor
} from '@dotcms/client';
-import { TINYMCE_CONFIG, TINYMCE_FORMAT, TINYMCE_MODE } from './utils';
+import { TINYMCE_CONFIG, DOT_EDITABLE_TEXT_FORMAT, DOT_EDITABLE_TEXT_MODE } from './utils';
import { DotCMSContentlet } from '../../models';
@@ -56,17 +56,17 @@ export class DotEditableTextComponent implements OnInit, OnChanges {
/**
* Represents the mode of the editor which can be `plain`, `minimal`, or `full`
*
- * @type {TINYMCE_MODE}
+ * @type {DOT_EDITABLE_TEXT_MODE}
* @memberof DotEditableTextComponent
*/
- @Input() mode: TINYMCE_MODE = 'plain';
+ @Input() mode: DOT_EDITABLE_TEXT_MODE = 'plain';
/**
* Represents the format of the editor which can be `text` or `html`
*
- * @type {TINYMCE_FORMAT}
+ * @type {DOT_EDITABLE_TEXT_FORMAT}
* @memberof DotEditableTextComponent
*/
- @Input() format: TINYMCE_FORMAT = 'text';
+ @Input() format: DOT_EDITABLE_TEXT_FORMAT = 'text';
/**
* Represents the `contentlet` that can be inline edited
*
@@ -126,7 +126,7 @@ export class DotEditableTextComponent implements OnInit, OnChanges {
* @memberof DotEditableTextComponent
*/
get onNumberOfPages() {
- return this.contentlet['onNumberOfPages'];
+ return this.contentlet['onNumberOfPages'] || 1;
}
/**
diff --git a/core-web/libs/sdk/angular/src/lib/components/dot-editable-text/utils.ts b/core-web/libs/sdk/angular/src/lib/components/dot-editable-text/utils.ts
index c090da24553e..bff68d2c9ad4 100644
--- a/core-web/libs/sdk/angular/src/lib/components/dot-editable-text/utils.ts
+++ b/core-web/libs/sdk/angular/src/lib/components/dot-editable-text/utils.ts
@@ -1,8 +1,8 @@
import { EditorComponent } from '@tinymce/tinymce-angular';
-export type TINYMCE_MODE = 'minimal' | 'full' | 'plain';
+export type DOT_EDITABLE_TEXT_MODE = 'minimal' | 'full' | 'plain';
-export type TINYMCE_FORMAT = 'html' | 'text';
+export type DOT_EDITABLE_TEXT_FORMAT = 'html' | 'text';
const DEFAULT_TINYMCE_CONFIG: EditorComponent['init'] = {
menubar: false,
@@ -17,7 +17,7 @@ const DEFAULT_TINYMCE_CONFIG: EditorComponent['init'] = {
};
export const TINYMCE_CONFIG: {
- [key in TINYMCE_MODE]: EditorComponent['init'];
+ [key in DOT_EDITABLE_TEXT_MODE]: EditorComponent['init'];
} = {
minimal: {
...DEFAULT_TINYMCE_CONFIG,
diff --git a/core-web/libs/sdk/client/package.json b/core-web/libs/sdk/client/package.json
index 3c049aa9e858..b3c43998a5f5 100644
--- a/core-web/libs/sdk/client/package.json
+++ b/core-web/libs/sdk/client/package.json
@@ -1,6 +1,6 @@
{
"name": "@dotcms/client",
- "version": "0.0.1-alpha.32",
+ "version": "0.0.1-alpha.33",
"description": "Official JavaScript library for interacting with DotCMS REST APIs.",
"repository": {
"type": "git",
diff --git a/core-web/libs/sdk/experiments/package.json b/core-web/libs/sdk/experiments/package.json
index 2eed7e32c417..aaf274d22c49 100644
--- a/core-web/libs/sdk/experiments/package.json
+++ b/core-web/libs/sdk/experiments/package.json
@@ -1,6 +1,6 @@
{
"name": "@dotcms/experiments",
- "version": "0.0.1-alpha.32",
+ "version": "0.0.1-alpha.33",
"description": "Official JavaScript library to use Experiments with DotCMS.",
"repository": {
"type": "git",
@@ -25,6 +25,6 @@
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18",
- "@dotcms/client": "0.0.1-alpha.32"
+ "@dotcms/client": "0.0.1-alpha.33"
}
}
diff --git a/core-web/libs/sdk/react/package.json b/core-web/libs/sdk/react/package.json
index 598add55b570..1382b0b55f24 100644
--- a/core-web/libs/sdk/react/package.json
+++ b/core-web/libs/sdk/react/package.json
@@ -1,10 +1,13 @@
{
"name": "@dotcms/react",
- "version": "0.0.1-alpha.32",
+ "version": "0.0.1-alpha.33",
+ "dependencies": {
+ "@tinymce/tinymce-react": "^5.1.1"
+ },
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18",
- "@dotcms/client": "0.0.1-alpha.32"
+ "@dotcms/client": "0.0.1-alpha.33"
},
"description": "Official React Components library to render a dotCMS page.",
"repository": {
diff --git a/core-web/libs/sdk/react/src/index.ts b/core-web/libs/sdk/react/src/index.ts
index 0e77739be956..53c09f8a1ede 100644
--- a/core-web/libs/sdk/react/src/index.ts
+++ b/core-web/libs/sdk/react/src/index.ts
@@ -1,4 +1,5 @@
export * from './lib/components/DotcmsLayout/DotcmsLayout';
+export * from './lib/components/DotEditableText/DotEditableText';
export * from './lib/components/PageProvider/PageProvider';
export * from './lib/components/Row/Row';
export * from './lib/hooks/useDotcmsPageContext';
diff --git a/core-web/libs/sdk/react/src/lib/components/DotEditableText/DotEditableText.spec.tsx b/core-web/libs/sdk/react/src/lib/components/DotEditableText/DotEditableText.spec.tsx
new file mode 100644
index 000000000000..0aaa79b19335
--- /dev/null
+++ b/core-web/libs/sdk/react/src/lib/components/DotEditableText/DotEditableText.spec.tsx
@@ -0,0 +1,232 @@
+import { expect } from '@jest/globals';
+import { fireEvent, render, screen } from '@testing-library/react';
+import * as tinymceReact from '@tinymce/tinymce-react';
+
+import * as dotcmsClient from '@dotcms/client';
+
+import { DotEditableText } from './DotEditableText';
+
+import { dotcmsContentletMock } from '../../mocks/mockPageContext';
+
+const { CUSTOMER_ACTIONS, postMessageToEditor } = dotcmsClient;
+
+// Define mockEditor before using it in jest.mock
+const TINYMCE_EDITOR_MOCK = {
+ focus: () => {
+ /* */
+ },
+ getContent: (_data: string) => '',
+ isDirty: () => false,
+ hasFocus: () => false,
+ setContent: () => {
+ /* */
+ }
+};
+
+jest.mock('@tinymce/tinymce-react', () => ({
+ Editor: jest.fn(({ onInit, onMouseDown, onFocusOut }) => {
+ onInit({}, TINYMCE_EDITOR_MOCK);
+
+ return
;
+ })
+}));
+
+// Mock @dotcms/client module
+jest.mock('@dotcms/client', () => ({
+ ...jest.requireActual('@dotcms/client'),
+ isInsideEditor: jest.fn().mockImplementation(() => true),
+ postMessageToEditor: jest.fn(),
+ DotCmsClient: {
+ dotcmsUrl: 'http://localhost:8080'
+ }
+}));
+
+const mockedDotcmsClient = dotcmsClient as jest.Mocked;
+const { Editor } = tinymceReact as jest.Mocked;
+
+describe('DotEditableText', () => {
+ describe('Outside editor', () => {
+ beforeEach(() => {
+ mockedDotcmsClient.isInsideEditor.mockReturnValue(false);
+ render();
+ });
+
+ it('should render the content', () => {
+ const editor = screen.queryByTestId('tinymce-editor');
+ expect(editor).toBeNull();
+ expect(screen.getByText(dotcmsContentletMock['title'])).not.toBeNull();
+ });
+ });
+
+ describe('Inside editor', () => {
+ let rerenderFn: (ui: React.ReactNode) => void;
+
+ beforeEach(() => {
+ mockedDotcmsClient.isInsideEditor.mockReturnValue(true);
+ const { rerender } = render(
+
+ );
+ rerenderFn = rerender;
+ });
+
+ it('should pass the correct props to the Editor component', () => {
+ const editor = screen.getByTestId('tinymce-editor');
+ expect(editor).not.toBeNull();
+
+ expect(Editor).toHaveBeenCalledWith(
+ {
+ tinymceScriptSrc: 'http://localhost:8080/ext/tinymcev7/tinymce.min.js',
+ inline: true,
+ init: {
+ inline: true,
+ menubar: false,
+ plugins: '',
+ powerpaste_html_import: 'clean',
+ powerpaste_word_import: 'clean',
+ suffix: '.min',
+ toolbar: '',
+ valid_styles: {
+ '*': 'font-size,font-family,color,text-decoration,text-align'
+ }
+ },
+ initialValue: dotcmsContentletMock.title,
+ onMouseDown: expect.any(Function),
+ onFocusOut: expect.any(Function),
+ onInit: expect.any(Function)
+ },
+ {}
+ );
+ });
+
+ describe('DotEditableText events', () => {
+ let focusSpy: jest.SpyInstance;
+
+ describe('Window Message', () => {
+ beforeEach(() => {
+ focusSpy = jest.spyOn(TINYMCE_EDITOR_MOCK, 'focus');
+ });
+
+ it("should focus on the editor when the message is 'COPY_CONTENTLET_INLINE_EDITING_SUCCESS'", () => {
+ window.dispatchEvent(
+ new MessageEvent('message', {
+ data: {
+ name: 'COPY_CONTENTLET_INLINE_EDITING_SUCCESS',
+ payload: {
+ oldInode: dotcmsContentletMock['inode'],
+ inode: '456'
+ }
+ }
+ })
+ );
+ expect(focusSpy).toHaveBeenCalled();
+ });
+
+ it("should not focus on the editor when the message is not 'COPY_CONTENTLET_INLINE_EDITING_SUCCESS'", () => {
+ window.dispatchEvent(
+ new MessageEvent('message', {
+ data: { name: 'ANOTHER_EVENT' }
+ })
+ );
+ expect(focusSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('mousedown', () => {
+ const event = new MouseEvent('mousedown', {
+ bubbles: true
+ });
+ const mutiplePagesContentlet = {
+ ...dotcmsContentletMock,
+ onNumberOfPages: 2
+ };
+
+ it('should postMessage the UVE if the content is in multiple pages', () => {
+ rerenderFn(
+
+ );
+ const editorElem = screen.getByTestId('tinymce-editor');
+ fireEvent(editorElem, event);
+
+ const payload = {
+ dataset: {
+ fieldName: 'title',
+ inode: mutiplePagesContentlet.inode,
+ language: mutiplePagesContentlet.languageId
+ }
+ };
+ expect(postMessageToEditor).toHaveBeenCalledWith({
+ action: CUSTOMER_ACTIONS.COPY_CONTENTLET_INLINE_EDITING,
+ payload
+ });
+ });
+
+ it('should not postMessage the UVE if the content is in a single page', () => {
+ const editorElem = screen.getByTestId('tinymce-editor');
+ fireEvent(editorElem, event);
+ expect(postMessageToEditor).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('onFocusOut', () => {
+ let isDirtySpy: jest.SpyInstance;
+ let getContentSpy: jest.SpyInstance;
+
+ const event = new FocusEvent('focusout', {
+ bubbles: true
+ });
+
+ beforeEach(() => {
+ isDirtySpy = jest.spyOn(TINYMCE_EDITOR_MOCK, 'isDirty');
+ getContentSpy = jest.spyOn(TINYMCE_EDITOR_MOCK, 'getContent');
+ });
+
+ it('should not postMessage the UVE if the editor is not dirty', () => {
+ mockedDotcmsClient.isInsideEditor.mockReturnValue(false);
+ const editorElem = screen.getByTestId('tinymce-editor');
+ fireEvent(editorElem, event);
+ expect(isDirtySpy).toHaveBeenCalled();
+ expect(getContentSpy).toHaveBeenCalledWith({ format: 'text' });
+ expect(postMessageToEditor).not.toHaveBeenCalled();
+ });
+
+ it('should not postMessage the UVE if the content did not change', () => {
+ isDirtySpy.mockReturnValue(true);
+ getContentSpy.mockReturnValue(dotcmsContentletMock.title);
+
+ const editorElem = screen.getByTestId('tinymce-editor');
+ fireEvent(editorElem, event);
+
+ expect(isDirtySpy).toHaveBeenCalled();
+ expect(getContentSpy).toHaveBeenCalledWith({ format: 'text' });
+ expect(postMessageToEditor).not.toHaveBeenCalled();
+ });
+
+ it('should postMessage the UVE if the content changed', () => {
+ isDirtySpy.mockReturnValue(true);
+ getContentSpy.mockReturnValue('New content');
+
+ const editorElem = screen.getByTestId('tinymce-editor');
+ fireEvent(editorElem, event);
+
+ const postMessageData = {
+ action: CUSTOMER_ACTIONS.UPDATE_CONTENTLET_INLINE_EDITING,
+ payload: {
+ content: 'New content',
+ dataset: {
+ inode: dotcmsContentletMock.inode,
+ langId: dotcmsContentletMock.languageId,
+ fieldName: 'title'
+ }
+ }
+ };
+
+ expect(isDirtySpy).toHaveBeenCalled();
+ expect(getContentSpy).toHaveBeenCalledWith({ format: 'text' });
+ expect(postMessageToEditor).toHaveBeenCalledWith(postMessageData);
+ });
+ });
+ });
+ });
+
+ afterEach(() => jest.clearAllMocks()); // Clear all mocks to avoid side effects from other tests
+});
diff --git a/core-web/libs/sdk/react/src/lib/components/DotEditableText/DotEditableText.tsx b/core-web/libs/sdk/react/src/lib/components/DotEditableText/DotEditableText.tsx
new file mode 100644
index 000000000000..5799e525fe15
--- /dev/null
+++ b/core-web/libs/sdk/react/src/lib/components/DotEditableText/DotEditableText.tsx
@@ -0,0 +1,141 @@
+import { Editor } from '@tinymce/tinymce-react';
+import { useEffect, useRef, useState } from 'react';
+
+import {
+ isInsideEditor as isInsideEditorFn,
+ postMessageToEditor,
+ CUSTOMER_ACTIONS,
+ DotCmsClient
+} from '@dotcms/client';
+
+import { DotEditableTextProps, TINYMCE_CONFIG } from './utils';
+
+const MCE_URL = '/ext/tinymcev7/tinymce.min.js';
+
+export function DotEditableText({
+ mode = 'plain',
+ format = 'text',
+ contentlet,
+ fieldName = ''
+}: Readonly) {
+ const editorRef = useRef(null);
+ const [scriptSrc, setScriptSrc] = useState('');
+ const [isInsideEditor, setIsInsideEditor] = useState(false);
+ const [content, setContent] = useState(contentlet?.[fieldName] || '');
+
+ useEffect(() => {
+ setIsInsideEditor(isInsideEditorFn());
+
+ if (!contentlet || !fieldName) {
+ console.error('DotEditableText: contentlet or fieldName is missing');
+ console.error('Ensure that all needed props are passed to view and edit the content');
+
+ return;
+ }
+
+ if (!isInsideEditorFn()) {
+ return;
+ }
+
+ const createURL = new URL(MCE_URL, DotCmsClient.dotcmsUrl);
+ setScriptSrc(createURL.toString());
+
+ const content = contentlet?.[fieldName] || '';
+ editorRef.current?.setContent(content, { format });
+ setContent(content);
+ }, [format, fieldName, contentlet?.[fieldName]]);
+
+ useEffect(() => {
+ if (!isInsideEditorFn()) {
+ return;
+ }
+
+ const onMessage = ({ data }: MessageEvent) => {
+ const { name, payload } = data;
+ if (name !== 'COPY_CONTENTLET_INLINE_EDITING_SUCCESS') {
+ return;
+ }
+
+ const { oldInode, inode } = payload;
+ const currentInode = contentlet.inode;
+ const shouldFocus = currentInode === oldInode || currentInode === inode;
+
+ if (shouldFocus) {
+ editorRef.current?.focus();
+ }
+ };
+
+ window.addEventListener('message', onMessage);
+
+ return () => {
+ window.removeEventListener('message', onMessage);
+ };
+ }, [contentlet.inode]);
+
+ const onMouseDown = (event: MouseEvent) => {
+ const { onNumberOfPages = 1 } = contentlet;
+ const { inode, languageId: language } = contentlet;
+
+ if (onNumberOfPages <= 1 || editorRef.current?.hasFocus()) {
+ return;
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+
+ postMessageToEditor({
+ action: CUSTOMER_ACTIONS.COPY_CONTENTLET_INLINE_EDITING,
+ payload: {
+ dataset: {
+ inode,
+ language,
+ fieldName
+ }
+ }
+ });
+ };
+
+ const onFocusOut = () => {
+ const editedContent = editorRef.current?.getContent({ format: format }) || '';
+ const { inode, languageId: langId } = contentlet;
+
+ if (!editorRef.current?.isDirty() || !didContentChange(editedContent)) {
+ return;
+ }
+
+ postMessageToEditor({
+ action: CUSTOMER_ACTIONS.UPDATE_CONTENTLET_INLINE_EDITING,
+ payload: {
+ content: editedContent,
+ dataset: {
+ inode,
+ langId,
+ fieldName
+ }
+ }
+ });
+ };
+
+ const didContentChange = (editedContent: string) => {
+ return content !== editedContent;
+ };
+
+ if (!isInsideEditor) {
+ // We can let the user pass the Child Component and create a root to get the HTML for the editor
+ return ;
+ }
+
+ return (
+ (editorRef.current = editor)}
+ init={TINYMCE_CONFIG[mode]}
+ initialValue={content}
+ onMouseDown={onMouseDown}
+ onFocusOut={onFocusOut}
+ />
+ );
+}
+
+export default DotEditableText;
diff --git a/core-web/libs/sdk/react/src/lib/components/DotEditableText/utils.ts b/core-web/libs/sdk/react/src/lib/components/DotEditableText/utils.ts
new file mode 100644
index 000000000000..dc097d6276f8
--- /dev/null
+++ b/core-web/libs/sdk/react/src/lib/components/DotEditableText/utils.ts
@@ -0,0 +1,82 @@
+import { IAllProps } from '@tinymce/tinymce-react';
+
+import { DotCMSContentlet } from '../../models';
+
+export type DOT_EDITABLE_TEXT_FORMAT = 'html' | 'text';
+
+export type DOT_EDITABLE_TEXT_MODE = 'minimal' | 'full' | 'plain';
+
+export interface DotEditableTextProps {
+ /**
+ * Represents the field name of the `contentlet` that can be edited
+ *
+ * @memberof DotEditableTextProps
+ */
+ fieldName: string;
+ /**
+ * Represents the format of the editor which can be `text` or `html`
+ *
+ * @type {DOT_EDITABLE_TEXT_FORMAT}
+ * @memberof DotEditableTextProps
+ */
+ format?: DOT_EDITABLE_TEXT_FORMAT;
+ /**
+ * Represents the mode of the editor which can be `plain`, `minimal`, or `full`
+ *
+ * @type {DOT_EDITABLE_TEXT_MODE}
+ * @memberof DotEditableTextProps
+ */
+ mode?: DOT_EDITABLE_TEXT_MODE;
+ /**
+ * Represents the `contentlet` that can be inline edited
+ *
+ * @type {DotCMSContentlet}
+ * @memberof DotEditableTextProps
+ */
+ contentlet: DotCMSContentlet;
+}
+
+const DEFAULT_TINYMCE_CONFIG: IAllProps['init'] = {
+ inline: true,
+ menubar: false,
+ powerpaste_html_import: 'clean',
+ powerpaste_word_import: 'clean',
+ suffix: '.min',
+ valid_styles: {
+ '*': 'font-size,font-family,color,text-decoration,text-align'
+ }
+};
+
+export const TINYMCE_CONFIG: {
+ [key in DOT_EDITABLE_TEXT_MODE]: IAllProps['init'];
+} = {
+ full: {
+ ...DEFAULT_TINYMCE_CONFIG,
+ plugins: 'link lists autolink charmap',
+ toolbar: [
+ 'styleselect undo redo | bold italic underline | forecolor backcolor | alignleft aligncenter alignright alignfull | numlist bullist outdent indent | hr charmap removeformat | link'
+ ],
+ style_formats: [
+ { title: 'Paragraph', format: 'p' },
+ { title: 'Header 1', format: 'h1' },
+ { title: 'Header 2', format: 'h2' },
+ { title: 'Header 3', format: 'h3' },
+ { title: 'Header 4', format: 'h4' },
+ { title: 'Header 5', format: 'h5' },
+ { title: 'Header 6', format: 'h6' },
+ { title: 'Pre', format: 'pre' },
+ { title: 'Code', format: 'code' }
+ ]
+ },
+ plain: {
+ ...DEFAULT_TINYMCE_CONFIG,
+ plugins: '',
+ toolbar: ''
+ },
+ minimal: {
+ ...DEFAULT_TINYMCE_CONFIG,
+ plugins: 'link autolink',
+ toolbar: 'bold italic underline | link',
+ valid_elements: 'strong,em,span[style],a[href]'
+ }
+};
diff --git a/core-web/libs/sdk/react/src/lib/mocks/mockPageContext.tsx b/core-web/libs/sdk/react/src/lib/mocks/mockPageContext.tsx
index f00c83e03b1b..e6b1526de3af 100644
--- a/core-web/libs/sdk/react/src/lib/mocks/mockPageContext.tsx
+++ b/core-web/libs/sdk/react/src/lib/mocks/mockPageContext.tsx
@@ -1,5 +1,30 @@
import { PageProvider } from '../components/PageProvider/PageProvider';
-import { DotCMSPageContext } from '../models';
+import { DotCMSPageContext, DotCMSContentlet } from '../models';
+
+export const dotcmsContentletMock: DotCMSContentlet = {
+ archived: false,
+ baseType: '',
+ contentType: '',
+ folder: '',
+ hasTitleImage: false,
+ host: '',
+ hostName: '',
+ identifier: '',
+ inode: '',
+ languageId: 1,
+ live: false,
+ locked: false,
+ modDate: '',
+ modUser: '',
+ modUserName: '',
+ owner: '',
+ sortOrder: 1,
+ stInode: '',
+ title: 'This is my editable title',
+ titleImage: '',
+ url: '',
+ working: false
+};
export const mockPageContext: DotCMSPageContext = {
pageAsset: {
diff --git a/core-web/libs/sdk/react/src/lib/models/index.ts b/core-web/libs/sdk/react/src/lib/models/index.ts
index 42fc4275b389..4378ffa8156a 100644
--- a/core-web/libs/sdk/react/src/lib/models/index.ts
+++ b/core-web/libs/sdk/react/src/lib/models/index.ts
@@ -87,3 +87,44 @@ export interface DotCMSPageContext {
};
isInsideEditor: boolean;
}
+
+export interface DotCMSContentlet {
+ archived: boolean;
+ binaryContentAsset?: string;
+ deleted?: boolean;
+ baseType: string;
+ binary?: string;
+ binaryVersion?: string;
+ file?: string;
+ contentType: string;
+ hasLiveVersion?: boolean;
+ folder: string;
+ hasTitleImage: boolean;
+ hostName: string;
+ host: string;
+ inode: string;
+ identifier: string;
+ languageId: number;
+ image?: string;
+ locked: boolean;
+ language?: string;
+ mimeType?: string;
+ modUser: string;
+ modDate: string;
+ live: boolean;
+ sortOrder: number;
+ owner: string;
+ title: string;
+ stInode: string;
+ titleImage: string;
+ modUserName: string;
+ text?: string;
+ working: boolean;
+ url: string;
+ contentTypeIcon?: string;
+ body?: string;
+ variant?: string;
+ __icon__?: string;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ [key: string]: any; // This is a catch-all for any other custom properties that might be on the contentlet.
+}
diff --git a/core-web/package.json b/core-web/package.json
index c69a8486aef6..a8cb54f8f502 100644
--- a/core-web/package.json
+++ b/core-web/package.json
@@ -74,6 +74,7 @@
"@nx/angular": "19.5.6",
"@tarekraafat/autocomplete.js": "^10.2.6",
"@tinymce/tinymce-angular": "^7.0.0",
+ "@tinymce/tinymce-react": "^5.1.1",
"@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",
diff --git a/core-web/yarn.lock b/core-web/yarn.lock
index 3b4d951f1b17..216f98717367 100644
--- a/core-web/yarn.lock
+++ b/core-web/yarn.lock
@@ -5177,6 +5177,14 @@
tinymce "^6.0.0 || ^5.5.0"
tslib "^2.3.0"
+"@tinymce/tinymce-react@^5.1.1":
+ version "5.1.1"
+ resolved "https://registry.npmjs.org/@tinymce/tinymce-react/-/tinymce-react-5.1.1.tgz#3b8555ceaccfa6bb8bb03c3b0c8baaccde138bde"
+ integrity sha512-DQ0wpvnf/9z8RsOEAmrWZ1DN1PKqcQHfU+DpM3llLze7FHmxVtzuN8O+FYh0oAAF4stzAXwiCIVacfqjMwRieQ==
+ dependencies:
+ prop-types "^15.6.2"
+ tinymce "^7.0.0 || ^6.0.0 || ^5.5.1"
+
"@tiptap/core@^2.0.0-beta.218", "@tiptap/core@^2.5.8":
version "2.5.8"
resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.5.8.tgz#58de366b0d2acb0a6e67a4780de64d619ebd90fa"
@@ -18590,7 +18598,7 @@ promzard@^0.3.0:
dependencies:
read "1"
-prop-types@^15.8.1:
+prop-types@^15.6.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -21206,6 +21214,11 @@ tiny-relative-date@^1.3.0:
resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-6.8.4.tgz#53e1313ebfe5524b24c0fa45937d51fda058632d"
integrity sha512-okoJyxuPv1gzASxQDNgQbnUXOdAIyoOSXcXcZZu7tiW0PSKEdf3SdASxPBupRj+64/E3elHwVRnzSdo82Emqbg==
+"tinymce@^7.0.0 || ^6.0.0 || ^5.5.1":
+ version "7.3.0"
+ resolved "https://registry.npmjs.org/tinymce/-/tinymce-7.3.0.tgz#929b5d6d151b7d5e6f2efdaa802484834c42872c"
+ integrity sha512-Ls4PgYlpk73XAxBSBqbVmSl8Mb3DuNfgF01GZ0lY6/MOEVRl3IL+VxC1Oe6165e8WqbqVsxO3Qj/PmoYNvQKGQ==
+
tippy.js@^6.3.7:
version "6.3.7"
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c"
diff --git a/examples/nextjs/jsconfig.json b/examples/nextjs/jsconfig.json
index 611be824c455..0d856efda589 100644
--- a/examples/nextjs/jsconfig.json
+++ b/examples/nextjs/jsconfig.json
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"paths": {
- "@/*": ["./src/*"],
+ "@/*": ["./src/*"]
}
}
}
diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json
index d24ace61533d..6e1663951154 100644
--- a/examples/nextjs/package.json
+++ b/examples/nextjs/package.json
@@ -9,6 +9,7 @@
"lint": "next lint"
},
"dependencies": {
+ "@tinymce/tinymce-react": "^5.1.1",
"@dotcms/client": "0.0.1-alpha.32",
"@dotcms/react": "0.0.1-alpha.32",
"@dotcms/experiments": "0.0.1-alpha.32",
diff --git a/examples/nextjs/src/components/content-types/banner.js b/examples/nextjs/src/components/content-types/banner.js
index fa4ac476e5e3..b323ba96ff1c 100644
--- a/examples/nextjs/src/components/content-types/banner.js
+++ b/examples/nextjs/src/components/content-types/banner.js
@@ -1,7 +1,10 @@
import Image from "next/image";
import Link from "next/link";
+import { DotEditableText } from "@dotcms/react";
+
+function Banner(contentlet) {
+ const { title, caption, image, link, buttonText } = contentlet;
-function Banner({ title, image, caption, buttonText, link }) {
return (
{image && (
@@ -13,7 +16,12 @@ function Banner({ title, image, caption, buttonText, link }) {
/>
)}
-
{title}
+
+
+
{caption}