diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/store/dot-ema.store.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/store/dot-ema.store.spec.ts
index bf2ba75afdf2..fd11c0c0a7e9 100644
--- a/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/store/dot-ema.store.spec.ts
+++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/store/dot-ema.store.spec.ts
@@ -24,6 +24,7 @@ import {
import { EditEmaStore } from './dot-ema.store';
+import { EmaDragItem } from '../../edit-ema-editor/components/ema-page-dropzone/types';
import { DotPageApiResponse, DotPageApiService } from '../../services/dot-page-api.service';
import { DEFAULT_PERSONA, MOCK_RESPONSE_HEADLESS } from '../../shared/consts';
import { EDITOR_MODE, EDITOR_STATE } from '../../shared/enums';
@@ -188,7 +189,11 @@ describe('EditEmaStore', () => {
lockedByUser: ''
},
variantId: undefined
- }
+ },
+ dragItem: undefined,
+ showContentletTools: false,
+ showDropzone: false,
+ showPalette: true
});
done();
});
@@ -241,7 +246,11 @@ describe('EditEmaStore', () => {
variantId: undefined
},
showWorkflowActions: true,
- showInfoDisplay: false
+ showInfoDisplay: false,
+ dragItem: undefined,
+ showContentletTools: false,
+ showDropzone: false,
+ showPalette: true
});
done();
});
@@ -292,7 +301,11 @@ describe('EditEmaStore', () => {
lockedByUser: ''
},
variantId: undefined
- }
+ },
+ dragItem: undefined,
+ showContentletTools: false,
+ showDropzone: false,
+ showPalette: false
});
done();
});
@@ -333,7 +346,11 @@ describe('EditEmaStore', () => {
isLocked: false,
lockedByUser: ''
}
- }
+ },
+ dragItem: undefined,
+ showContentletTools: false,
+ showDropzone: false,
+ showPalette: false
});
done();
});
@@ -374,7 +391,11 @@ describe('EditEmaStore', () => {
isLocked: false,
lockedByUser: ''
}
- }
+ },
+ dragItem: undefined,
+ showContentletTools: false,
+ showDropzone: false,
+ showPalette: false
});
done();
});
@@ -415,7 +436,11 @@ describe('EditEmaStore', () => {
isLocked: false,
lockedByUser: ''
}
- }
+ },
+ dragItem: undefined,
+ showContentletTools: false,
+ showDropzone: false,
+ showPalette: true
});
done();
});
@@ -425,7 +450,9 @@ describe('EditEmaStore', () => {
spectator.service.contentState$.subscribe((state) => {
expect(state).toEqual({
state: EDITOR_STATE.IDLE,
- code: undefined
+ code: undefined,
+ isVTL: false,
+ changedFromLoading: true
});
done();
});
@@ -498,7 +525,38 @@ describe('EditEmaStore', () => {
isLocked: false,
lockedByUser: ''
}
- }
+ },
+ dragItem: undefined,
+ showContentletTools: false,
+ showDropzone: false,
+ showPalette: true
+ });
+ done();
+ });
+ });
+
+ it('should update editor state to idle when dont have dragItem', (done) => {
+ spectator.service.updateEditorScrollState();
+
+ spectator.service.editorState$.subscribe((state) => {
+ expect(state).toEqual({
+ ...state,
+ bounds: [],
+ state: EDITOR_STATE.IDLE
+ });
+ done();
+ });
+ });
+
+ it('should update editor state to dragginf when have dragItem', (done) => {
+ spectator.service.setDragItem({} as EmaDragItem);
+ spectator.service.updateEditorScrollState();
+
+ spectator.service.editorState$.subscribe((state) => {
+ expect(state).toEqual({
+ ...state,
+ bounds: [],
+ state: EDITOR_STATE.DRAGGING
});
done();
});
@@ -974,7 +1032,11 @@ describe('EditEmaStore', () => {
lockedByUser: ''
},
variantId: undefined
- }
+ },
+ dragItem: undefined,
+ showContentletTools: false,
+ showDropzone: false,
+ showPalette: true
});
done();
});
@@ -984,7 +1046,9 @@ describe('EditEmaStore', () => {
spectator.service.contentState$.subscribe((state) => {
expect(state).toEqual({
state: EDITOR_STATE.IDLE,
- code: '
Hello, World!
'
+ code: 'Hello, World!
',
+ isVTL: true,
+ changedFromLoading: true
});
done();
});
@@ -1017,7 +1081,11 @@ describe('EditEmaStore', () => {
},
variantId: undefined
},
- currentExperiment: null
+ currentExperiment: null,
+ dragItem: undefined,
+ showContentletTools: false,
+ showDropzone: false,
+ showPalette: true
});
done();
});
diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/store/dot-ema.store.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/store/dot-ema.store.ts
index 0048524e8180..3016f3be474d 100644
--- a/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/store/dot-ema.store.ts
+++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/dot-ema-shell/store/dot-ema.store.ts
@@ -6,7 +6,17 @@ import { Injectable } from '@angular/core';
import { MessageService } from 'primeng/api';
-import { catchError, map, shareReplay, switchMap, take, tap } from 'rxjs/operators';
+import {
+ catchError,
+ map,
+ pairwise,
+ shareReplay,
+ startWith,
+ switchMap,
+ take,
+ tap,
+ filter
+} from 'rxjs/operators';
import {
DotContentletLockerService,
@@ -200,10 +210,28 @@ export class EditEmaStore extends ComponentStore {
readonly editorMode$ = this.select((state) => state.editorData.mode);
readonly editorData$ = this.select((state) => state.editorData);
readonly pageRendered$ = this.select((state) => state.editor.page.rendered);
- readonly contentState$ = this.select(this.code$, this.stateLoad$, (code, state) => ({
- state,
- code
- }));
+
+ readonly contentState$ = this.select(
+ this.code$,
+ this.stateLoad$,
+ this.clientHost$,
+ (code, state, clientHost) => ({
+ state,
+ code,
+ isVTL: !clientHost
+ })
+ ).pipe(
+ startWith({ state: EDITOR_STATE.LOADING, code: '', isVTL: false }),
+ pairwise(),
+ filter(([_prev, curr]) => curr?.state === EDITOR_STATE.IDLE),
+ map(([prev, curr]) => ({
+ changedFromLoading: prev.state === EDITOR_STATE.LOADING,
+ isVTL: curr.isVTL,
+ code: curr.code,
+ state: curr.state
+ }))
+ );
+
readonly vtlIframePage$ = this.select(
this.pageRendered$,
this.isEnterpriseLicense$,
@@ -259,7 +287,27 @@ export class EditEmaStore extends ComponentStore {
iframeURL,
isEnterpriseLicense,
state: currentState,
- dragItem
+ dragItem,
+ showContentletTools:
+ editorData.canEditVariant &&
+ !!contentletArea &&
+ !editorData.device &&
+ editor.page.canEdit &&
+ (currentState === EDITOR_STATE.IDLE ||
+ currentState === EDITOR_STATE.DRAGGING) &&
+ !editorData.page.isLocked,
+ showDropzone:
+ editorData.canEditVariant &&
+ !editorData.device &&
+ (currentState === EDITOR_STATE.DRAGGING ||
+ currentState === EDITOR_STATE.SCROLLING),
+ showPalette:
+ editorData.canEditVariant &&
+ isEnterpriseLicense &&
+ (editorData.mode === EDITOR_MODE.EDIT ||
+ editorData.mode === EDITOR_MODE.EDIT_VARIANT ||
+ editorData.mode === EDITOR_MODE.INLINE_EDITING) &&
+ editor.page.canEdit
};
}
);
@@ -319,6 +367,9 @@ export class EditEmaStore extends ComponentStore {
dragItem
}));
+ readonly isUserDragging$ = this.select((state) => state.editorState).pipe(
+ filter((state) => state === EDITOR_STATE.DRAGGING)
+ );
/**
* Concurrently loads page and license data to updat the state.
*
@@ -677,6 +728,19 @@ export class EditEmaStore extends ComponentStore {
editorState
}));
+ /**
+ * Update the editor state to scroll
+ *
+ * @memberof EditEmaStore
+ */
+ readonly setScrollingState = this.updater((state) => {
+ return {
+ ...state,
+ editorState: EDITOR_STATE.SCROLLING,
+ bounds: []
+ };
+ });
+
/**
* Update the preview state
*
@@ -693,6 +757,20 @@ export class EditEmaStore extends ComponentStore {
};
});
+ /**
+ * Updates the editor scroll state in the dot-ema store.
+ * If a drag item is present, we assume that scrolling was done during a drag and drop, and the state will automatically change to dragging.
+ * if there is no dragItem, we change the state to IDLE
+ *
+ * @returns The updated dot-ema store state.
+ */
+ readonly updateEditorScrollState = this.updater((state) => {
+ return {
+ ...state,
+ editorState: state.dragItem ? EDITOR_STATE.DRAGGING : EDITOR_STATE.IDLE
+ };
+ });
+
readonly setDevice = this.updater((state, device: DotDevice) => {
return {
...state,
diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/ema-page-dropzone.component.html b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/ema-page-dropzone.component.html
index a9b201cfa052..bd787c4c08f8 100644
--- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/ema-page-dropzone.component.html
+++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/components/ema-page-dropzone/ema-page-dropzone.component.html
@@ -22,7 +22,10 @@
context: { error: container | dotError : dragItem, container: container }
">
+
+@if (containers.length > 0) {
+}
@@ -83,14 +71,7 @@
diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.spec.ts
index 1e417a9cf6fb..71c0c4e5b3a7 100644
--- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.spec.ts
+++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.spec.ts
@@ -2625,6 +2625,86 @@ describe('EditEmaEditorComponent', () => {
});
});
+ describe('scroll inside iframe', () => {
+ it('should emit postMessage and change state to Scroll', () => {
+ const dragOver = new Event('dragover');
+
+ Object.defineProperty(dragOver, 'clientY', { value: 200, enumerable: true });
+ Object.defineProperty(dragOver, 'clientX', { value: 120, enumerable: true });
+
+ const postMessageSpy = jest.spyOn(
+ spectator.component.iframe.nativeElement.contentWindow,
+ 'postMessage'
+ );
+
+ const scrollingStateSpy = jest.spyOn(store, 'setScrollingState');
+
+ jest.spyOn(
+ spectator.component.iframe.nativeElement,
+ 'getBoundingClientRect'
+ ).mockReturnValue({
+ top: 150,
+ bottom: 700,
+ left: 100,
+ right: 500
+ } as DOMRect);
+
+ window.dispatchEvent(dragOver);
+ spectator.detectChanges();
+ expect(postMessageSpy).toHaveBeenCalled();
+ expect(scrollingStateSpy).toHaveBeenCalled();
+ });
+
+ it('should dont emit postMessage or changestate scroll when drag outside iframe', () => {
+ const dragOver = new Event('dragover');
+
+ Object.defineProperty(dragOver, 'clientY', { value: 200, enumerable: true });
+ Object.defineProperty(dragOver, 'clientX', { value: 90, enumerable: true });
+
+ const postMessageSpy = jest.spyOn(
+ spectator.component.iframe.nativeElement.contentWindow,
+ 'postMessage'
+ );
+
+ jest.spyOn(
+ spectator.component.iframe.nativeElement,
+ 'getBoundingClientRect'
+ ).mockReturnValue({
+ top: 150,
+ bottom: 700,
+ left: 100,
+ right: 500
+ } as DOMRect);
+
+ window.dispatchEvent(dragOver);
+ spectator.detectChanges();
+ expect(postMessageSpy).not.toHaveBeenCalled();
+ });
+
+ it('should change state to dragging when drag outsite scroll trigger area', () => {
+ const dragOver = new Event('dragover');
+
+ Object.defineProperty(dragOver, 'clientY', { value: 300, enumerable: true });
+ Object.defineProperty(dragOver, 'clientX', { value: 120, enumerable: true });
+
+ const updateEditorState = jest.spyOn(store, 'updateEditorState');
+
+ jest.spyOn(
+ spectator.component.iframe.nativeElement,
+ 'getBoundingClientRect'
+ ).mockReturnValue({
+ top: 150,
+ bottom: 700,
+ left: 100,
+ right: 500
+ } as DOMRect);
+
+ window.dispatchEvent(dragOver);
+ spectator.detectChanges();
+ expect(updateEditorState).toHaveBeenCalledWith(EDITOR_STATE.DRAGGING);
+ });
+ });
+
describe('DOM', () => {
it("should not show a loader when the editor state is not 'loading'", () => {
spectator.detectChanges();
@@ -2656,12 +2736,18 @@ describe('EditEmaEditorComponent', () => {
describe('VTL Page', () => {
beforeEach(() => {
jest.useFakeTimers(); // Mock the timers
+ spectator.detectChanges();
+
+ // We need to force the editor state to loading for this test
+ // because first we get the pageapi of "1" person
+ // and then we get the pageapi of "3" person
+ store.updateEditorState(EDITOR_STATE.LOADING);
+
store.load({
url: 'index',
language_id: '3',
'com.dotmarketing.persona.id': DEFAULT_PERSONA.identifier
});
- spectator.detectChanges();
});
afterEach(() => {
@@ -2669,13 +2755,12 @@ describe('EditEmaEditorComponent', () => {
});
it('iframe should have the correct content when is VTL', () => {
- spectator.detectChanges();
-
jest.runOnlyPendingTimers();
+
const iframe = spectator.debugElement.query(
By.css('[data-testId="iframe"]')
);
- expect(iframe.nativeElement.src).toBe('http://localhost/'); //When dont have src, the src is the same as the current page
+
expect(iframe.nativeElement.contentDocument.body.innerHTML).toContain(
'hello world
'
);
diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts
index f2b82ba1dbbc..828d1091ad4d 100644
--- a/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts
+++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/edit-ema-editor/edit-ema-editor.component.ts
@@ -6,6 +6,7 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
+ DestroyRef,
ElementRef,
HostListener,
OnDestroy,
@@ -18,7 +19,7 @@ import {
signal,
untracked
} from '@angular/core';
-import { toSignal } from '@angular/core/rxjs-interop';
+import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Params, Router } from '@angular/router';
@@ -72,7 +73,7 @@ import { DotEmaDialogComponent } from '../components/dot-ema-dialog/dot-ema-dial
import { EditEmaStore } from '../dot-ema-shell/store/dot-ema.store';
import { DotPageApiParams } from '../services/dot-page-api.service';
import { InlineEditService } from '../services/inline-edit/inline-edit.service';
-import { DEFAULT_PERSONA, WINDOW } from '../shared/consts';
+import { DEFAULT_PERSONA, IFRAME_SCROLL_ZONE, WINDOW } from '../shared/consts';
import { EDITOR_MODE, EDITOR_STATE, NG_CUSTOM_EVENTS, NOTIFY_CUSTOMER } from '../shared/enums';
import {
ActionPayload,
@@ -147,6 +148,7 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy {
private readonly tempFileUploadService = inject(DotTempFileUploadService);
private readonly dotWorkflowActionsFireService = inject(DotWorkflowActionsFireService);
private readonly inlineEditingService = inject(InlineEditService);
+ private readonly destroyRef = inject(DestroyRef);
readonly editorState$ = this.store.editorState$;
readonly dragState$ = this.store.dragState$;
@@ -305,6 +307,13 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy {
}
handleDragEvents() {
+ this.store.isUserDragging$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
+ this.iframe.nativeElement.contentWindow?.postMessage(
+ NOTIFY_CUSTOMER.EMA_REQUEST_BOUNDS,
+ this.host
+ );
+ });
+
fromEvent(this.window, 'dragstart')
.pipe(takeUntil(this.destroy$))
.subscribe((event: DragEvent) => {
@@ -341,11 +350,6 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy {
} as ContentletDragPayload
});
}
-
- this.iframe.nativeElement.contentWindow?.postMessage(
- NOTIFY_CUSTOMER.EMA_REQUEST_BOUNDS,
- this.host
- );
});
fromEvent(this.window, 'dragenter')
@@ -407,6 +411,43 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy {
.pipe(takeUntil(this.destroy$))
.subscribe((event: DragEvent) => {
event.preventDefault(); // Prevent file opening
+ const iframeRect = this.iframe.nativeElement.getBoundingClientRect();
+
+ const isInsideIframe =
+ event.clientX > iframeRect.left && event.clientX < iframeRect.right;
+
+ if (!isInsideIframe) {
+ return;
+ }
+
+ let direction;
+
+ if (
+ event.clientY > iframeRect.top &&
+ event.clientY < iframeRect.top + IFRAME_SCROLL_ZONE
+ ) {
+ direction = 'up';
+ }
+
+ if (
+ event.clientY > iframeRect.bottom - IFRAME_SCROLL_ZONE &&
+ event.clientY <= iframeRect.bottom
+ ) {
+ direction = 'down';
+ }
+
+ if (!direction) {
+ this.store.updateEditorState(EDITOR_STATE.DRAGGING);
+
+ return;
+ }
+
+ this.store.setScrollingState();
+
+ this.iframe.nativeElement.contentWindow?.postMessage(
+ { name: NOTIFY_CUSTOMER.EMA_SCROLL_INSIDE_IFRAME, direction },
+ this.host
+ );
});
fromEvent(this.window, 'drop')
@@ -499,20 +540,22 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy {
*/
handleReloadContent() {
this.store.contentState$
- .pipe(
- takeUntil(this.destroy$),
- filter(({ state }) => state === EDITOR_STATE.IDLE)
- )
- .subscribe(({ code }) => {
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe(({ changedFromLoading, code, isVTL }) => {
// If we are idle then we are not dragging
this.resetDragProperties();
- if (!this.isVTLPage()) {
- // Only reload if is Headless.
- // If is VTL, the content is updated by store.code$
- this.reloadIframe();
- } else {
+ if (!changedFromLoading) {
+ /** We have some EDITOR_STATE values that we don't want to reload the content
+ * Only when the state is changed from LOADING to IDLE we need to reload the content
+ */
+ return;
+ }
+
+ if (isVTL) {
this.setIframeContent(code);
+ } else {
+ this.reloadIframe();
}
});
}
@@ -949,8 +992,10 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy {
});
},
[CUSTOMER_ACTIONS.IFRAME_SCROLL]: () => {
- this.resetDragProperties();
- this.store.updateEditorState(EDITOR_STATE.IDLE);
+ this.store.updateEditorState(EDITOR_STATE.SCROLLING);
+ },
+ [CUSTOMER_ACTIONS.IFRAME_SCROLL_END]: () => {
+ this.store.updateEditorScrollState();
},
[CUSTOMER_ACTIONS.PING_EDITOR]: () => {
this.iframe?.nativeElement?.contentWindow.postMessage(
diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/consts.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/consts.ts
index eda4bc03e2b7..084a3f652eb8 100644
--- a/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/consts.ts
+++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/consts.ts
@@ -28,6 +28,8 @@ export const EDIT_CONTENT_CALLBACK_FUNCTION = 'saveAssignCallBackAngular';
export const VIEW_CONTENT_CALLBACK_FUNCTION = 'angularWorkflowEventCallback';
+export const IFRAME_SCROLL_ZONE = 100;
+
export const DEFAULT_PERSONA: DotPersona = {
hostFolder: 'SYSTEM_HOST',
inode: '',
diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/enums.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/enums.ts
index c68f1582d7ac..69fb483c4eec 100644
--- a/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/enums.ts
+++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/shared/enums.ts
@@ -1,7 +1,8 @@
export enum NOTIFY_CUSTOMER {
EMA_RELOAD_PAGE = 'ema-reload-page', // We need to reload the ema page
EMA_REQUEST_BOUNDS = 'ema-request-bounds',
- EMA_EDITOR_PONG = 'ema-editor-pong'
+ EMA_EDITOR_PONG = 'ema-editor-pong',
+ EMA_SCROLL_INSIDE_IFRAME = 'scroll-inside-iframe'
}
// All the custom events that come from the JSP Iframe
@@ -24,7 +25,8 @@ export enum EDITOR_STATE {
IDLE = 'idle',
DRAGGING = 'dragging',
ERROR = 'error',
- OUT_OF_BOUNDS = 'out-of-bounds'
+ OUT_OF_BOUNDS = 'out-of-bounds',
+ SCROLLING = 'scrolling'
}
export enum EDITOR_MODE {
diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/utils/index.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/utils/index.ts
index 7e605e618637..69fefa1ef474 100644
--- a/core-web/libs/portlets/edit-ema/portlet/src/lib/utils/index.ts
+++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/utils/index.ts
@@ -4,7 +4,7 @@ import { DotPageApiParams } from '../services/dot-page-api.service';
import { DEFAULT_PERSONA, EDIT_MODE } from '../shared/consts';
import { ActionPayload, ContainerPayload, PageContainer } from '../shared/models';
-export const SDK_EDITOR_SCRIPT_SOURCE = '/html/js/editor-js/sdk-editor.esm.js';
+export const SDK_EDITOR_SCRIPT_SOURCE = '/html/js/editor-js/sdk-editor.js';
/**
* Insert a contentlet in a container
diff --git a/core-web/libs/portlets/edit-ema/portlet/src/lib/utils/utils.spec.ts b/core-web/libs/portlets/edit-ema/portlet/src/lib/utils/utils.spec.ts
index 3719ef2bbd11..edd6d4dc6765 100644
--- a/core-web/libs/portlets/edit-ema/portlet/src/lib/utils/utils.spec.ts
+++ b/core-web/libs/portlets/edit-ema/portlet/src/lib/utils/utils.spec.ts
@@ -10,7 +10,7 @@ import {
describe('utils functions', () => {
describe('SDK Editor Script Source', () => {
it('should return the correct script source', () => {
- expect(SDK_EDITOR_SCRIPT_SOURCE).toEqual('/html/js/editor-js/sdk-editor.esm.js');
+ expect(SDK_EDITOR_SCRIPT_SOURCE).toEqual('/html/js/editor-js/sdk-editor.js');
});
});
diff --git a/core-web/libs/sdk/client/project.json b/core-web/libs/sdk/client/project.json
index 7ddfbdeeb82d..a7162d7bc94c 100644
--- a/core-web/libs/sdk/client/project.json
+++ b/core-web/libs/sdk/client/project.json
@@ -15,7 +15,7 @@
}
},
"build:js": {
- "executor": "@nrwl/rollup:rollup",
+ "executor": "@nx/esbuild:esbuild",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "../dotCMS/src/main/webapp/html/js/editor-js",
@@ -26,8 +26,9 @@
"entryFile": "libs/sdk/client/src/lib/editor/sdk-editor-vtl.ts",
"external": ["react/jsx-runtime"],
"rollupConfig": "@nrwl/react/plugins/bundle-rollup",
- "compiler": "swc",
- "extractCss": false
+ "compiler": "tsc",
+ "extractCss": false,
+ "minify": true
}
},
"publish": {
diff --git a/core-web/libs/sdk/client/src/lib/editor/listeners/listeners.ts b/core-web/libs/sdk/client/src/lib/editor/listeners/listeners.ts
index 61d6118e5ed8..dd4bc0a21ac5 100644
--- a/core-web/libs/sdk/client/src/lib/editor/listeners/listeners.ts
+++ b/core-web/libs/sdk/client/src/lib/editor/listeners/listeners.ts
@@ -80,6 +80,11 @@ export function listenEditorMessages() {
break;
}
}
+
+ if (event.data.name === NOTIFY_CUSTOMER.EMA_SCROLL_INSIDE_IFRAME) {
+ const scrollY = event.data.direction === 'up' ? -120 : 120;
+ window.scrollBy({ left: 0, top: scrollY, behavior: 'smooth' });
+ }
};
window.addEventListener('message', messageCallback);
@@ -175,7 +180,20 @@ export function scrollHandler() {
window.lastScrollYPosition = window.scrollY;
};
+ const scrollEndCallback = () => {
+ postMessageToEditor({
+ action: CUSTOMER_ACTIONS.IFRAME_SCROLL_END
+ });
+ };
+
window.addEventListener('scroll', scrollCallback);
+ window.addEventListener('scrollend', scrollEndCallback);
+
+ subscriptions.push({
+ type: 'listener',
+ event: 'scroll',
+ callback: scrollEndCallback
+ });
subscriptions.push({
type: 'listener',
diff --git a/core-web/libs/sdk/client/src/lib/editor/models/client.model.ts b/core-web/libs/sdk/client/src/lib/editor/models/client.model.ts
index 85118f2846de..d7b042cce114 100644
--- a/core-web/libs/sdk/client/src/lib/editor/models/client.model.ts
+++ b/core-web/libs/sdk/client/src/lib/editor/models/client.model.ts
@@ -21,6 +21,10 @@ export enum CUSTOMER_ACTIONS {
* Tell the editor that the page is being scrolled
*/
IFRAME_SCROLL = 'scroll',
+ /**
+ * Tell the editor that the page has stopped scrolling
+ */
+ IFRAME_SCROLL_END = 'scroll-end',
/**
* Ping the editor to see if the page is inside the editor
*/
diff --git a/core-web/libs/sdk/client/src/lib/editor/models/listeners.model.ts b/core-web/libs/sdk/client/src/lib/editor/models/listeners.model.ts
index a2291ad4fae0..bfcedc5a0799 100644
--- a/core-web/libs/sdk/client/src/lib/editor/models/listeners.model.ts
+++ b/core-web/libs/sdk/client/src/lib/editor/models/listeners.model.ts
@@ -16,7 +16,12 @@ export enum NOTIFY_CUSTOMER {
/**
* Received pong from the editor
*/
- EMA_EDITOR_PONG = 'ema-editor-pong'
+ EMA_EDITOR_PONG = 'ema-editor-pong',
+ /**
+ * Received scroll event trigger from the editor
+ */
+
+ EMA_SCROLL_INSIDE_IFRAME = 'scroll-inside-iframe'
}
type ListenerCallbackMessage = (event: MessageEvent) => void;
diff --git a/dotCMS/src/main/webapp/html/js/editor-js/sdk-editor.esm.js b/dotCMS/src/main/webapp/html/js/editor-js/sdk-editor.esm.js
deleted file mode 100644
index 1319b2157052..000000000000
--- a/dotCMS/src/main/webapp/html/js/editor-js/sdk-editor.esm.js
+++ /dev/null
@@ -1,352 +0,0 @@
-/**
- * Actions send to the dotcms editor
- *
- * @export
- * @enum {number}
- */ var CUSTOMER_ACTIONS;
-(function(CUSTOMER_ACTIONS) {
- /**
- * Tell the dotcms editor that page change
- */ CUSTOMER_ACTIONS["NAVIGATION_UPDATE"] = "set-url";
- /**
- * Send the element position of the rows, columnsm containers and contentlets
- */ CUSTOMER_ACTIONS["SET_BOUNDS"] = "set-bounds";
- /**
- * Send the information of the hovered contentlet
- */ CUSTOMER_ACTIONS["SET_CONTENTLET"] = "set-contentlet";
- /**
- * Tell the editor that the page is being scrolled
- */ CUSTOMER_ACTIONS["IFRAME_SCROLL"] = "scroll";
- /**
- * Ping the editor to see if the page is inside the editor
- */ CUSTOMER_ACTIONS["PING_EDITOR"] = "ping-editor";
- /**
- * Tell the editor to init the inline editing editor.
- */ CUSTOMER_ACTIONS["INIT_INLINE_EDITING"] = "init-inline-editing";
- /**
- * Tell the editor to open the Copy-contentlet dialog
- * To copy a content and then edit it inline.
- */ CUSTOMER_ACTIONS["COPY_CONTENTLET_INLINE_EDITING"] = "copy-contentlet-inline-editing";
- /**
- * Tell the editor to save inline edited contentlet
- */ CUSTOMER_ACTIONS["UPDATE_CONTENTLET_INLINE_EDITING"] = "update-contentlet-inline-editing";
- /**
- * Tell the editor to trigger a menu reorder
- */ CUSTOMER_ACTIONS["REORDER_MENU"] = "reorder-menu";
- CUSTOMER_ACTIONS["NOOP"] = "noop";
-})(CUSTOMER_ACTIONS || (CUSTOMER_ACTIONS = {}));
-/**
- * Post message to dotcms page editor
- *
- * @export
- * @template T
- * @param {PostMessageProps} message
- */ function postMessageToEditor(message) {
- window.parent.postMessage(message, "*");
-}
-
-/**
- * Actions received from the dotcms editor
- *
- * @export
- * @enum {number}
- */ var NOTIFY_CUSTOMER;
-(function(NOTIFY_CUSTOMER) {
- /**
- * Request to page to reload
- */ NOTIFY_CUSTOMER["EMA_RELOAD_PAGE"] = "ema-reload-page";
- /**
- * Request the bounds for the elements
- */ NOTIFY_CUSTOMER["EMA_REQUEST_BOUNDS"] = "ema-request-bounds";
- /**
- * Received pong from the editor
- */ NOTIFY_CUSTOMER["EMA_EDITOR_PONG"] = "ema-editor-pong";
-})(NOTIFY_CUSTOMER || (NOTIFY_CUSTOMER = {}));
-
-/**
- * Bound information for a contentlet.
- *
- * @interface ContentletBound
- */ /**
- * Calculates the bounding information for each page element within the given containers.
- *
- * @export
- * @param {HTMLDivElement[]} containers
- * @return {*} An array of objects containing the bounding information for each page element.
- */ function getPageElementBound(containers) {
- return containers.map(function(container) {
- var containerRect = container.getBoundingClientRect();
- var contentlets = Array.from(container.querySelectorAll('[data-dot-object="contentlet"]'));
- return {
- x: containerRect.x,
- y: containerRect.y,
- width: containerRect.width,
- height: containerRect.height,
- payload: JSON.stringify({
- container: getContainerData(container)
- }),
- contentlets: getContentletsBound(containerRect, contentlets)
- };
- });
-}
-/**
- * An array of objects containing the bounding information for each contentlet inside a container.
- *
- * @export
- * @param {DOMRect} containerRect
- * @param {HTMLDivElement[]} contentlets
- * @return {*}
- */ function getContentletsBound(containerRect, contentlets) {
- return contentlets.map(function(contentlet) {
- var _contentlet_dataset, _contentlet_dataset1, _contentlet_dataset2, _contentlet_dataset3, _contentlet_dataset4, _contentlet_dataset5;
- var contentletRect = contentlet.getBoundingClientRect();
- return {
- x: 0,
- y: contentletRect.y - containerRect.y,
- width: contentletRect.width,
- height: contentletRect.height,
- payload: JSON.stringify({
- container: ((_contentlet_dataset = contentlet.dataset) === null || _contentlet_dataset === void 0 ? void 0 : _contentlet_dataset["dotContainer"]) ? JSON.parse((_contentlet_dataset1 = contentlet.dataset) === null || _contentlet_dataset1 === void 0 ? void 0 : _contentlet_dataset1["dotContainer"]) : getClosestContainerData(contentlet),
- contentlet: {
- identifier: (_contentlet_dataset2 = contentlet.dataset) === null || _contentlet_dataset2 === void 0 ? void 0 : _contentlet_dataset2["dotIdentifier"],
- title: (_contentlet_dataset3 = contentlet.dataset) === null || _contentlet_dataset3 === void 0 ? void 0 : _contentlet_dataset3["dotTitle"],
- inode: (_contentlet_dataset4 = contentlet.dataset) === null || _contentlet_dataset4 === void 0 ? void 0 : _contentlet_dataset4["dotInode"],
- contentType: (_contentlet_dataset5 = contentlet.dataset) === null || _contentlet_dataset5 === void 0 ? void 0 : _contentlet_dataset5["dotType"]
- }
- })
- };
- });
-}
-/**
- * Get container data from VTLS.
- *
- * @export
- * @param {HTMLElement} container
- * @return {*}
- */ function getContainerData(container) {
- var _container_dataset, _container_dataset1, _container_dataset2, _container_dataset3;
- return {
- acceptTypes: ((_container_dataset = container.dataset) === null || _container_dataset === void 0 ? void 0 : _container_dataset["dotAcceptTypes"]) || "",
- identifier: ((_container_dataset1 = container.dataset) === null || _container_dataset1 === void 0 ? void 0 : _container_dataset1["dotIdentifier"]) || "",
- maxContentlets: ((_container_dataset2 = container.dataset) === null || _container_dataset2 === void 0 ? void 0 : _container_dataset2["maxContentlets"]) || "",
- uuid: ((_container_dataset3 = container.dataset) === null || _container_dataset3 === void 0 ? void 0 : _container_dataset3["dotUuid"]) || ""
- };
-}
-/**
- * Get the closest container data from the contentlet.
- *
- * @export
- * @param {Element} element
- * @return {*}
- */ function getClosestContainerData(element) {
- // Find the closest ancestor element with data-dot-object="container" attribute
- var container = element.closest('[data-dot-object="container"]');
- // If a container element is found
- if (container) {
- // Return the dataset of the container element
- return getContainerData(container);
- } else {
- // If no container element is found, return null
- console.warn("No container found for the contentlet");
- return null;
- }
-}
-/**
- * Find the closest contentlet element based on HTMLElement.
- *
- * @export
- * @param {(HTMLElement | null)} element
- * @return {*}
- */ function findDotElement(element) {
- var _element_dataset, _element_dataset1;
- if (!element) return null;
- if ((element === null || element === void 0 ? void 0 : (_element_dataset = element.dataset) === null || _element_dataset === void 0 ? void 0 : _element_dataset["dotObject"]) === "contentlet" || (element === null || element === void 0 ? void 0 : (_element_dataset1 = element.dataset) === null || _element_dataset1 === void 0 ? void 0 : _element_dataset1["dotObject"]) === "container" && element.children.length === 0) {
- return element;
- }
- return findDotElement(element === null || element === void 0 ? void 0 : element["parentElement"]);
-}
-function findVTLData(target) {
- var vltElements = target.querySelectorAll('[data-dot-object="vtl-file"]');
- if (!vltElements.length) {
- return null;
- }
- return Array.from(vltElements).map(function(vltElement) {
- var _vltElement_dataset, _vltElement_dataset1;
- return {
- inode: (_vltElement_dataset = vltElement.dataset) === null || _vltElement_dataset === void 0 ? void 0 : _vltElement_dataset["dotInode"],
- name: (_vltElement_dataset1 = vltElement.dataset) === null || _vltElement_dataset1 === void 0 ? void 0 : _vltElement_dataset1["dotUrl"]
- };
- });
-}
-
-/**
- * Default reload function that reloads the current window.
- */ var defaultReloadFn = function() {
- return window.location.reload();
-};
-/**
- * Configuration object for the DotCMSPageEditor.
- */ var pageEditorConfig = {
- onReload: defaultReloadFn
-};
-/**
- * Sets the bounds of the containers in the editor.
- * Retrieves the containers from the DOM and sends their position data to the editor.
- * @private
- * @memberof DotCMSPageEditor
- */ function setBounds() {
- var containers = Array.from(document.querySelectorAll('[data-dot-object="container"]'));
- var positionData = getPageElementBound(containers);
- postMessageToEditor({
- action: CUSTOMER_ACTIONS.SET_BOUNDS,
- payload: positionData
- });
-}
-/**
- * Reloads the page and triggers the onReload callback if it exists in the config object.
- */ function reloadPage() {
- pageEditorConfig === null || pageEditorConfig === void 0 ? void 0 : pageEditorConfig.onReload();
-}
-/**
- * Listens for editor messages and performs corresponding actions based on the received message.
- *
- * @private
- * @memberof DotCMSPageEditor
- */ function listenEditorMessages() {
- var messageCallback = function(event) {
- switch(event.data){
- case NOTIFY_CUSTOMER.EMA_REQUEST_BOUNDS:
- {
- setBounds();
- break;
- }
- case NOTIFY_CUSTOMER.EMA_RELOAD_PAGE:
- {
- reloadPage();
- break;
- }
- }
- };
- window.addEventListener("message", messageCallback);
-}
-/**
- * Listens for pointer move events and extracts information about the hovered contentlet.
- *
- * @private
- * @memberof DotCMSPageEditor
- */ function listenHoveredContentlet() {
- var pointerMoveCallback = function(event) {
- var _foundElement_dataset, _foundElement_dataset1, _foundElement_dataset2, _foundElement_dataset3, _foundElement_dataset4, _foundElement_dataset5, _foundElement_dataset6, _foundElement_dataset7, // Here extract dot-container from contentlet if is Headless
- // or search in parent container if is VTL
- _foundElement_dataset8, _foundElement_dataset9;
- var foundElement = findDotElement(event.target);
- if (!foundElement) return;
- var _foundElement_getBoundingClientRect = foundElement.getBoundingClientRect(), x = _foundElement_getBoundingClientRect.x, y = _foundElement_getBoundingClientRect.y, width = _foundElement_getBoundingClientRect.width, height = _foundElement_getBoundingClientRect.height;
- var isContainer = ((_foundElement_dataset = foundElement.dataset) === null || _foundElement_dataset === void 0 ? void 0 : _foundElement_dataset["dotObject"]) === "container";
- var contentletForEmptyContainer = {
- identifier: "TEMP_EMPTY_CONTENTLET",
- title: "TEMP_EMPTY_CONTENTLET",
- contentType: "TEMP_EMPTY_CONTENTLET_TYPE",
- inode: "TEMPY_EMPTY_CONTENTLET_INODE",
- widgetTitle: "TEMP_EMPTY_CONTENTLET",
- baseType: "TEMP_EMPTY_CONTENTLET",
- onNumberOfPages: 1
- };
- var contentlet = {
- identifier: (_foundElement_dataset1 = foundElement.dataset) === null || _foundElement_dataset1 === void 0 ? void 0 : _foundElement_dataset1["dotIdentifier"],
- title: (_foundElement_dataset2 = foundElement.dataset) === null || _foundElement_dataset2 === void 0 ? void 0 : _foundElement_dataset2["dotTitle"],
- inode: (_foundElement_dataset3 = foundElement.dataset) === null || _foundElement_dataset3 === void 0 ? void 0 : _foundElement_dataset3["dotInode"],
- contentType: (_foundElement_dataset4 = foundElement.dataset) === null || _foundElement_dataset4 === void 0 ? void 0 : _foundElement_dataset4["dotType"],
- baseType: (_foundElement_dataset5 = foundElement.dataset) === null || _foundElement_dataset5 === void 0 ? void 0 : _foundElement_dataset5["dotBasetype"],
- widgetTitle: (_foundElement_dataset6 = foundElement.dataset) === null || _foundElement_dataset6 === void 0 ? void 0 : _foundElement_dataset6["dotWidgetTitle"],
- onNumberOfPages: (_foundElement_dataset7 = foundElement.dataset) === null || _foundElement_dataset7 === void 0 ? void 0 : _foundElement_dataset7["dotOnNumberOfPages"]
- };
- var vtlFiles = findVTLData(foundElement);
- var contentletPayload = {
- container: ((_foundElement_dataset8 = foundElement.dataset) === null || _foundElement_dataset8 === void 0 ? void 0 : _foundElement_dataset8["dotContainer"]) ? JSON.parse((_foundElement_dataset9 = foundElement.dataset) === null || _foundElement_dataset9 === void 0 ? void 0 : _foundElement_dataset9["dotContainer"]) : getClosestContainerData(foundElement),
- contentlet: isContainer ? contentletForEmptyContainer : contentlet,
- vtlFiles: vtlFiles
- };
- postMessageToEditor({
- action: CUSTOMER_ACTIONS.SET_CONTENTLET,
- payload: {
- x: x,
- y: y,
- width: width,
- height: height,
- payload: contentletPayload
- }
- });
- };
- document.addEventListener("pointermove", pointerMoveCallback);
-}
-/**
- * Attaches a scroll event listener to the window
- * and sends a message to the editor when the window is scrolled.
- *
- * @private
- * @memberof DotCMSPageEditor
- */ function scrollHandler() {
- var scrollCallback = function() {
- postMessageToEditor({
- action: CUSTOMER_ACTIONS.IFRAME_SCROLL
- });
- window.lastScrollYPosition = window.scrollY;
- };
- window.addEventListener("scroll", scrollCallback);
-}
-/**
- * Restores the scroll position of the window when an iframe is loaded.
- * Only used in VTL Pages.
- * @export
- */ function preserveScrollOnIframe() {
- var preserveScrollCallback = function() {
- window.scrollTo(0, window.lastScrollYPosition);
- };
- window.addEventListener("load", preserveScrollCallback);
-}
-/**
- * Sends a ping message to the editor.
- *
- */ function pingEditor() {
- postMessageToEditor({
- action: CUSTOMER_ACTIONS.PING_EDITOR
- });
-}
-
-/**
- * Checks if the code is running inside an editor.
- * @returns {boolean} Returns true if the code is running inside an editor, otherwise false.
- */ function isInsideEditor() {
- if (typeof window === "undefined") {
- return false;
- }
- return window.parent !== window;
-}
-function addClasstForEmptyContentlets() {
- var contentlets = document.querySelectorAll('[data-dot-object="contentlet"]');
- contentlets.forEach(function(contentlet) {
- if (contentlet.clientHeight) {
- return;
- }
- contentlet.classList.add("empty-contentlet");
- });
-}
-
-/**
- * This is the main entry point for the SDK VTL.
- * This is added to VTL Script in the EditPage
- *
- * @remarks
- * This module sets up the necessary listeners and functionality for the SDK VTL.
- * It checks if the script is running inside the editor and then initializes the client by pinging the editor,
- * listening for editor messages, hovered contentlet changes, and content changes.
- *
- */ if (isInsideEditor()) {
- pingEditor();
- listenEditorMessages();
- scrollHandler();
- preserveScrollOnIframe();
- listenHoveredContentlet();
- addClasstForEmptyContentlets();
-}
diff --git a/dotCMS/src/main/webapp/html/js/editor-js/sdk-editor.js b/dotCMS/src/main/webapp/html/js/editor-js/sdk-editor.js
new file mode 100644
index 000000000000..13f6c4c858f7
--- /dev/null
+++ b/dotCMS/src/main/webapp/html/js/editor-js/sdk-editor.js
@@ -0,0 +1 @@
+function i(t){window.parent.postMessage(t,"*")}function p(t){return t.map(n=>{let e=n.getBoundingClientRect(),o=Array.from(n.querySelectorAll('[data-dot-object="contentlet"]'));return{x:e.x,y:e.y,width:e.width,height:e.height,payload:JSON.stringify({container:u(n)}),contentlets:N(e,o)}})}function N(t,n){return n.map(e=>{let o=e.getBoundingClientRect();return{x:0,y:o.y-t.y,width:o.width,height:o.height,payload:JSON.stringify({container:e.dataset?.dotContainer?JSON.parse(e.dataset?.dotContainer):a(e),contentlet:{identifier:e.dataset?.dotIdentifier,title:e.dataset?.dotTitle,inode:e.dataset?.dotInode,contentType:e.dataset?.dotType}})}})}function u(t){return{acceptTypes:t.dataset?.dotAcceptTypes||"",identifier:t.dataset?.dotIdentifier||"",maxContentlets:t.dataset?.maxContentlets||"",uuid:t.dataset?.dotUuid||""}}function a(t){let n=t.closest('[data-dot-object="container"]');return n?u(n):(console.warn("No container found for the contentlet"),null)}function s(t){return t?t?.dataset?.dotObject==="contentlet"||t?.dataset?.dotObject==="container"&&t.children.length===0?t:s(t?.parentElement):null}function f(t){let n=t.querySelectorAll('[data-dot-object="vtl-file"]');return n.length?Array.from(n).map(e=>({inode:e.dataset?.dotInode,name:e.dataset?.dotUrl})):null}var w=()=>window.location.reload(),_={onReload:w};var r=[];function h(){let t=Array.from(document.querySelectorAll('[data-dot-object="container"]')),n=p(t);i({action:"set-bounds",payload:n})}function I(){_?.onReload()}function l(){let t=n=>{switch(n.data){case"ema-request-bounds":{h();break}case"ema-reload-page":{I();break}}if(n.data.name==="scroll-inside-iframe"){let e=n.data.direction==="up"?-120:120;window.scrollBy({left:0,top:e,behavior:"smooth"})}};window.addEventListener("message",t),r.push({type:"listener",event:"message",callback:t})}function d(){let t=n=>{let e=s(n.target);if(!e)return;let{x:o,y:m,width:L,height:M}=e.getBoundingClientRect(),P=e.dataset?.dotObject==="container",b={identifier:"TEMP_EMPTY_CONTENTLET",title:"TEMP_EMPTY_CONTENTLET",contentType:"TEMP_EMPTY_CONTENTLET_TYPE",inode:"TEMPY_EMPTY_CONTENTLET_INODE",widgetTitle:"TEMP_EMPTY_CONTENTLET",baseType:"TEMP_EMPTY_CONTENTLET",onNumberOfPages:1},y={identifier:e.dataset?.dotIdentifier,title:e.dataset?.dotTitle,inode:e.dataset?.dotInode,contentType:e.dataset?.dotType,baseType:e.dataset?.dotBasetype,widgetTitle:e.dataset?.dotWidgetTitle,onNumberOfPages:e.dataset?.dotOnNumberOfPages},C=f(e),D={container:e.dataset?.dotContainer?JSON.parse(e.dataset?.dotContainer):a(e),contentlet:P?b:y,vtlFiles:C};i({action:"set-contentlet",payload:{x:o,y:m,width:L,height:M,payload:D}})};document.addEventListener("pointermove",t),r.push({type:"listener",event:"pointermove",callback:t})}function c(){let t=()=>{i({action:"scroll"}),window.lastScrollYPosition=window.scrollY},n=()=>{i({action:"scroll-end"})};window.addEventListener("scroll",t),window.addEventListener("scrollend",n),r.push({type:"listener",event:"scroll",callback:n}),r.push({type:"listener",event:"scroll",callback:t})}function g(){let t=()=>{window.scrollTo(0,window.lastScrollYPosition)};window.addEventListener("load",t),r.push({type:"listener",event:"scroll",callback:t})}function E(){i({action:"ping-editor"})}function T(){return typeof window>"u"?!1:window.parent!==window}T()&&(E(),l(),c(),g(),d());