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());