Browse Source

Add scrollable canvas feature (#6395)

pull/6413/head
mohamed yahia 1 year ago
committed by GitHub
parent
commit
8225e3e9d1
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 13
      packages/core/src/canvas/config/config.ts
  2. 37
      packages/core/src/canvas/index.ts
  3. 28
      packages/core/src/canvas/view/CanvasView.ts
  4. 18
      packages/core/src/commands/view/SelectComponent.ts

13
packages/core/src/canvas/config/config.ts

@ -89,6 +89,19 @@ export interface CanvasConfig {
* Experimental: enable infinite canvas.
*/
infiniteCanvas?: boolean;
/**
* Enables the scrollable canvas feature.
*
* When this feature flag is set to `true`, the canvas element
* will have its `overflow` style set to `auto`, allowing users to scroll
* the canvas content if it exceeds the visible area. This is useful for
* handling large diagrams or zoomed-in views where parts of the content
* are initially hidden. If `false`, the canvas will use default overflow (typically hidden).
*
* @default false
*/
scrollableCanvas?: boolean;
}
const config: () => CanvasConfig = () => ({

37
packages/core/src/canvas/index.ts

@ -418,25 +418,36 @@ export default class CanvasModule extends Module<CanvasConfig> {
const canvasOffset = opts.canvasOff || this.canvasRectOffset(el, elRect);
const targetHeight = targetEl.offsetHeight || 0;
const targetWidth = targetEl.offsetWidth || 0;
const elRight = elRect.left + elRect.width;
const elementRight = elRect.left + elRect.width;
const canvasView = this.getCanvasView();
const { scrollTop: canvasScrollTop, scrollLeft: canvasScrollLeft } = canvasView.getCanvasScroll();
const canvasRect = canvasView.getPosition();
const frameOffset = canvasView.getFrameOffset(el);
const { event } = opts;
let top = -targetHeight;
let left = !isUndefined(opts.left) ? opts.left : elRect.width - targetWidth;
left = elRect.left < -left ? -elRect.left : left;
left = elRight > canvasRect.width ? left - (elRight - canvasRect.width) : left;
const defaultLeftOffset = elRect.width - targetWidth;
const defaultTopOffset = -targetHeight;
// Check when the target top edge reaches the top of the viewable canvas
if (canvasOffset.top < targetHeight) {
let left = !isUndefined(opts.left) ? opts.left : defaultLeftOffset;
const canvasLiftLimit = Math.max(-elRect.left + canvasScrollLeft, 0);
left = Math.max(left, canvasLiftLimit);
const elementRightLimit = elementRight - targetWidth;
left = Math.min(left, elementRightLimit);
const canvasRightLimit = canvasRect.width + canvasScrollLeft - targetWidth - elRect.left;
left = Math.min(left, canvasRightLimit);
const targetReachesCanvasTop = canvasOffset.top < targetHeight + canvasScrollTop;
let top = defaultTopOffset;
if (targetReachesCanvasTop) {
const fullHeight = elRect.height + targetHeight;
const elIsShort = fullHeight < frameOffset.height;
const elementIsShorterThanFrame = fullHeight < frameOffset.height;
// Scroll with the window if the top edge is reached and the
// element is bigger than the canvas
if (elIsShort) {
if (elementIsShorterThanFrame) {
top = top + fullHeight;
} else {
top = -canvasOffset.top < elRect.height ? -canvasOffset.top : elRect.height;
@ -501,7 +512,13 @@ export default class CanvasModule extends Module<CanvasConfig> {
*/
getMouseRelativeCanvas(ev: MouseEvent | { clientX: number; clientY: number }, opts: any) {
const zoom = this.getZoomDecimal();
const { top = 0, left = 0 } = this.getCanvasView().getPosition(opts) ?? {};
const canvasView = this.getCanvasView();
const canvasPos = canvasView.getPosition(opts) ?? { top: 0, left: 0 };
const canvasScroll = canvasView.getCanvasScroll();
const { top, left } = {
top: canvasPos.top + canvasScroll.scrollTop,
left: canvasPos.left + canvasScroll.scrollLeft,
};
return {
y: ev.clientY * zoom + top,

28
packages/core/src/canvas/view/CanvasView.ts

@ -358,11 +358,12 @@ export default class CanvasView extends ModuleView<Canvas> {
}
getRectToScreen(boxRect: Partial<BoxRect>): BoxRect {
const canvasScroll = this.getCanvasScroll();
const zoom = this.module.getZoomDecimal();
const coords = this.module.getCoords();
const vwDelta = this.getViewportDelta();
const x = (boxRect.x ?? 0) * zoom + coords.x + vwDelta.x || 0;
const y = (boxRect.y ?? 0) * zoom + coords.y + vwDelta.y || 0;
const x = (boxRect.x ?? 0) * zoom - canvasScroll.scrollLeft + coords.x + vwDelta.x || 0;
const y = (boxRect.y ?? 0) * zoom - canvasScroll.scrollTop + coords.y + vwDelta.y || 0;
return {
x,
@ -461,6 +462,7 @@ export default class CanvasView extends ModuleView<Canvas> {
const frEl = winEl ? (winEl.frameElement as HTMLElement) : frame;
this.frmOff = this.offset(frEl || frame);
}
return this.frmOff;
}
@ -562,6 +564,23 @@ export default class CanvasView extends ModuleView<Canvas> {
};
}
/**
* Returns the scroll position of the canvas.
*
* If the canvas is scrollable, returns the current `scrollTop` and `scrollLeft` values.
* Otherwise, returns an object with `scrollTop` and `scrollLeft` both set to 0.
*
* @returns An object containing the vertical and horizontal scroll positions.
*/
getCanvasScroll(): { scrollTop: number; scrollLeft: number } {
return this.config.scrollableCanvas
? {
scrollTop: this.el.scrollTop,
scrollLeft: this.el.scrollLeft,
}
: { scrollTop: 0, scrollLeft: 0 };
}
/**
* Update javascript of a specific component passed by its View
* @param {ModuleView} view Component's View
@ -671,7 +690,10 @@ export default class CanvasView extends ModuleView<Canvas> {
this.toolsGlobEl = el.querySelector(`.${ppfx}tools-gl`)!;
this.spotsEl = el.querySelector('[data-spots]')!;
this.cvStyle = el.querySelector('[data-canvas-style]')!;
this.el.className = getUiClass(em, this.className);
el.className = getUiClass(em, this.className);
if (config.scrollableCanvas === true) {
el.style.overflow = 'auto';
}
this.ready = true;
this._renderFrames();

18
packages/core/src/commands/view/SelectComponent.ts

@ -40,6 +40,7 @@ export default {
'onHover',
'onOut',
'onClick',
'onCanvasScroll',
'onFrameScroll',
'onFrameResize',
'onFrameUpdated',
@ -75,14 +76,16 @@ export default {
* @private
* */
toggleSelectComponent(enable: boolean) {
const { em } = this;
const { em, canvas } = this;
const canvasEl = canvas.getCanvasView().el;
const listenToEl = em.getConfig().listenToEl!;
const { parentNode } = em.getContainer()!;
const method = enable ? 'on' : 'off';
const methods = { on, off };
const eventCmpUpdate = ComponentsEvents.update;
!listenToEl.length && parentNode && listenToEl.push(parentNode as HTMLElement);
const trigger = (win: Window, body: HTMLBodyElement) => {
const trigger = (win: Window, body: HTMLBodyElement, canvasEl: HTMLElement) => {
methods[method](canvasEl, 'scroll', this.onCanvasScroll, true);
methods[method](body, 'mouseover', this.onHover);
methods[method](body, 'mouseleave', this.onOut);
methods[method](body, 'click', this.onClick);
@ -101,7 +104,7 @@ export default {
em.Canvas.getFrames().forEach((frame) => {
const { view } = frame;
const win = view?.getWindow();
win && trigger(win, view?.getBody()!);
win && trigger(win, view?.getBody()!, canvasEl);
});
},
@ -589,6 +592,15 @@ export default {
return this.canvas.getBadgeEl(opts.view);
},
/**
* On canvas scroll callback
* @private
*/
onCanvasScroll(e: any) {
this.onFrameScroll(e);
this.onContainerChange();
},
/**
* On frame scroll callback
* @private

Loading…
Cancel
Save