Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(edit-page-v2): Add scroll while dragging content inside editor #28419

Merged
merged 9 commits into from
May 8, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,7 @@ export class EditEmaStore extends ComponentStore<EditEmaState> {

private readonly editor$ = this.select((state) => state.editor);
private readonly isEnterpriseLicense$ = this.select((state) => state.isEnterpriseLicense);
private readonly currentState$ = this.select(
(state) => state.editorState ?? EDITOR_STATE.LOADING
);
readonly currentState$ = this.select((state) => state.editorState ?? EDITOR_STATE.LOADING);
private readonly currentExperiment$ = this.select((state) => state.currentExperiment);
private readonly templateThemeId$ = this.select((state) => state.editor.template.themeId);
private readonly templateIdentifier$ = this.select((state) => state.editor.template.identifier);
Expand Down Expand Up @@ -677,6 +675,19 @@ export class EditEmaStore extends ComponentStore<EditEmaState> {
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
*
Expand All @@ -693,6 +704,20 @@ export class EditEmaStore extends ComponentStore<EditEmaState> {
};
});

/**
* 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.SCROLLING : EDITOR_STATE.IDLE
};
});

readonly setDevice = this.updater((state, device: DotDevice) => {
return {
...state,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,11 @@
data-testId="contentlet-tools" />
<dot-ema-page-dropzone
*ngIf="
es.editorData.canEditVariant &&
!!es.bounds.length &&
!es.editorData.device &&
es.state === editorState.DRAGGING
(es.editorData.canEditVariant &&
!!es.bounds.length &&
!es.editorData.device &&
es.state === editorState.DRAGGING) ||
es.state === editorState.SCROLLING
"
[containers]="es.bounds"
[dragItem]="es.dragItem"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2635,26 +2635,31 @@ 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(() => {
jest.useRealTimers(); // Restore the real timers after each test
});

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(
'<div>hello world</div>'
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,17 @@ import { ConfirmationService, MessageService } from 'primeng/api';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { ProgressBarModule } from 'primeng/progressbar';

import { takeUntil, catchError, filter, map, switchMap, tap, take } from 'rxjs/operators';
import {
takeUntil,
catchError,
filter,
map,
switchMap,
tap,
take,
startWith,
pairwise
} from 'rxjs/operators';

import { CUSTOMER_ACTIONS } from '@dotcms/client';
import {
Expand Down Expand Up @@ -304,6 +314,15 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy {
}

handleDragEvents() {
this.store.currentState$.pipe(takeUntil(this.destroy$)).subscribe((state) => {
if (state === EDITOR_STATE.DRAGGING) {
this.iframe.nativeElement.contentWindow?.postMessage(
NOTIFY_CUSTOMER.EMA_REQUEST_BOUNDS,
this.host
);
}
});

fromEvent(this.window, 'dragstart')
.pipe(takeUntil(this.destroy$))
.subscribe((event: DragEvent) => {
Expand Down Expand Up @@ -340,11 +359,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')
Expand Down Expand Up @@ -406,6 +420,42 @@ 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;

let direction;

if (
isInsideIframe &&
event.clientY > iframeRect.top &&
event.clientY < iframeRect.top + 100
) {
direction = 'up';
}

if (
isInsideIframe &&
event.clientY > iframeRect.bottom - 100 &&
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')
Expand Down Expand Up @@ -499,19 +549,28 @@ export class EditEmaEditorComponent implements OnInit, OnDestroy {
handleReloadContent() {
this.store.contentState$
.pipe(
startWith({ state: EDITOR_STATE.LOADING, code: '' }),
takeUntil(this.destroy$),
filter(({ state }) => state === EDITOR_STATE.IDLE)
pairwise(),
filter(([_prev, curr]) => curr?.state === EDITOR_STATE.IDLE)
)
.subscribe(({ code }) => {
.subscribe(([prev, curr]) => {
// If we are idle then we are not dragging
this.resetDragProperties();

if (!this.isVTLPage()) {
// Only reload if is Headless.
if (prev?.state !== EDITOR_STATE.LOADING) {
/** 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 (this.isVTLPage()) {
// If is VTL, the content is updated by store.code$
this.reloadIframe();
this.setIframeContent(curr.code);
} else {
this.setIframeContent(code);
// Only reload if is Headless.
this.reloadIframe();
}
});
}
Expand Down Expand Up @@ -924,8 +983,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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 {
Expand Down
18 changes: 18 additions & 0 deletions core-web/libs/sdk/client/src/lib/editor/listeners/listeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
20 changes: 20 additions & 0 deletions dotCMS/src/main/webapp/html/js/editor-js/sdk-editor.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
/**
* Tell the editor that the page is being scrolled
*/ CUSTOMER_ACTIONS["IFRAME_SCROLL"] = "scroll";
/**
* Tell the editor that the page has stopped scrolling
*/ CUSTOMER_ACTIONS["IFRAME_SCROLL_END"] = "scroll-end";
/**
* Ping the editor to see if the page is inside the editor
*/ CUSTOMER_ACTIONS["PING_EDITOR"] = "ping-editor";
Expand Down Expand Up @@ -61,6 +64,9 @@
/**
* Received pong from the editor
*/ NOTIFY_CUSTOMER["EMA_EDITOR_PONG"] = "ema-editor-pong";
/**
* Received scroll event trigger from the editor
*/ NOTIFY_CUSTOMER["EMA_SCROLL_INSIDE_IFRAME"] = "scroll-inside-iframe";
})(NOTIFY_CUSTOMER || (NOTIFY_CUSTOMER = {}));

/**
Expand Down Expand Up @@ -226,6 +232,14 @@ function findVTLData(target) {
break;
}
}
if (event.data.name === NOTIFY_CUSTOMER.EMA_SCROLL_INSIDE_IFRAME) {
var scrollY = event.data.direction === "up" ? -120 : 120;
window.scrollBy({
left: 0,
top: scrollY,
behavior: "smooth"
});
}
};
window.addEventListener("message", messageCallback);
}
Expand Down Expand Up @@ -293,7 +307,13 @@ function findVTLData(target) {
});
window.lastScrollYPosition = window.scrollY;
};
var scrollEndCallback = function() {
postMessageToEditor({
action: CUSTOMER_ACTIONS.IFRAME_SCROLL_END
});
};
window.addEventListener("scroll", scrollCallback);
window.addEventListener("scrollend", scrollEndCallback);
}
/**
* Restores the scroll position of the window when an iframe is loaded.
Expand Down
Loading