Browse Source

Add auto scrollable canvas (#6408)

removed-event
mohamed yahia 12 months ago
committed by GitHub
parent
commit
fd51fa88ba
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 15
      packages/core/src/canvas/index.ts
  2. 92
      packages/core/src/canvas/view/FrameView.ts
  3. 103
      packages/core/src/utils/AutoScroller.ts

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

@ -44,10 +44,12 @@ import { CanvasEvents, CanvasRefreshOptions, ToWorldOption } from './types';
import CanvasView, { FitViewportOptions } from './view/CanvasView';
import FrameView from './view/FrameView';
import { DragSource } from '../utils/sorter/types';
import AutoScroller from '../utils/AutoScroller';
export type CanvasEvent = `${CanvasEvents}`;
export default class CanvasModule extends Module<CanvasConfig> {
autoScroller: AutoScroller;
/**
* Get configuration object
* @name getConfig
@ -83,6 +85,8 @@ export default class CanvasModule extends Module<CanvasConfig> {
this.model = this.canvas;
this.startAutoscroll = this.startAutoscroll.bind(this);
this.stopAutoscroll = this.stopAutoscroll.bind(this);
this.autoScroller = new AutoScroller();
return this;
}
@ -606,6 +610,13 @@ export default class CanvasModule extends Module<CanvasConfig> {
startAutoscroll(frame?: Frame) {
const fr = (frame && frame.view) || this.em.getCurrentFrame();
fr && fr.startAutoscroll();
if (this.config.scrollableCanvas) {
const el = this.getCanvasView().el;
this.autoScroller.start(el, el, {
zoom: this.em.getZoomDecimal(),
});
}
}
/**
@ -615,6 +626,10 @@ export default class CanvasModule extends Module<CanvasConfig> {
stopAutoscroll(frame?: Frame) {
const fr = (frame && frame.view) || this.em.getCurrentFrame();
fr && fr.stopAutoscroll();
if (this.config.scrollableCanvas) {
this.autoScroller.stop();
}
}
/**

92
packages/core/src/canvas/view/FrameView.ts

@ -6,21 +6,13 @@ import ComponentWrapperView from '../../dom_components/view/ComponentWrapperView
import ComponentView from '../../dom_components/view/ComponentView';
import { type as typeHead } from '../../dom_components/model/ComponentHead';
import Droppable from '../../utils/Droppable';
import {
append,
appendVNodes,
createCustomEvent,
createEl,
getPointerEvent,
motionsEv,
off,
on,
} from '../../utils/dom';
import { append, appendVNodes, createCustomEvent, createEl, motionsEv, off, on } from '../../utils/dom';
import { hasDnd, setViewEl } from '../../utils/mixins';
import Canvas from '../model/Canvas';
import Frame from '../model/Frame';
import FrameWrapView from './FrameWrapView';
import CanvasEvents from '../types';
import AutoScroller from '../../utils/AutoScroller';
export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
/** @ts-ignore */
@ -39,6 +31,7 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
lastClientY?: number;
lastMaxHeight = 0;
private autoScroller: AutoScroller;
private jsContainer?: HTMLElement;
private tools: { [key: string]: HTMLElement } = {};
private wrapper?: ComponentWrapperView;
@ -47,7 +40,7 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
constructor(model: Frame, view?: FrameWrapView) {
super({ model });
bindAll(this, 'updateClientY', 'stopAutoscroll', 'autoscroll', '_emitUpdate');
bindAll(this, 'startAutoscroll', 'stopAutoscroll', '_emitUpdate');
const { el } = this;
//@ts-ignore
this.module._config = {
@ -63,6 +56,17 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
this.listenTo(cvModel, 'change:styles', this.renderStyles);
model.view = this;
setViewEl(el, this);
this.autoScroller = new AutoScroller(this.config.autoscrollLimit, {
rectIsInScrollIframe: true,
onScroll: () => {
const toolsEl = this.getGlobalToolsEl();
toolsEl.style.opacity = '0';
this.showGlobalTools();
this.em.Canvas.spots.refreshDbn();
},
});
}
getBoxRect(): BoxRect {
@ -222,74 +226,20 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
}
startAutoscroll() {
this.lastMaxHeight = this.getWrapper().offsetHeight - this.el.offsetHeight;
// By detaching those from the stack avoid browsers lags
// Noticeable with "fast" drag of blocks
setTimeout(() => {
this._toggleAutoscrollFx(true);
requestAnimationFrame(this.autoscroll);
}, 0);
}
autoscroll() {
if (this.dragging) {
const { lastClientY } = this;
const canvas = this.em.Canvas;
const win = this.getWindow();
const actualTop = win.pageYOffset;
const clientY = lastClientY || 0;
const limitTop = canvas.getConfig().autoscrollLimit!;
const limitBottom = this.getRect().height - limitTop;
let nextTop = actualTop;
if (clientY < limitTop) {
nextTop -= limitTop - clientY;
}
if (clientY > limitBottom) {
nextTop += clientY - limitBottom;
}
if (
!isUndefined(lastClientY) && // Fixes #3134
nextTop !== actualTop &&
nextTop > 0 &&
nextTop < this.lastMaxHeight
) {
const toolsEl = this.getGlobalToolsEl();
toolsEl.style.opacity = '0';
this.showGlobalTools();
win.scrollTo(0, nextTop);
canvas.spots.refreshDbn();
}
requestAnimationFrame(this.autoscroll);
}
this.autoScroller.start(this.el, this.getWindow(), {
lastMaxHeight: this.getWrapper().offsetHeight - this.el.offsetHeight,
zoom: this.em.getZoomDecimal(),
});
}
updateClientY(ev: Event) {
ev.preventDefault();
this.lastClientY = getPointerEvent(ev).clientY * this.em.getZoomDecimal();
stopAutoscroll() {
this.autoScroller.stop();
}
showGlobalTools() {
this.getGlobalToolsEl().style.opacity = '';
}
stopAutoscroll() {
this.dragging && this._toggleAutoscrollFx(false);
}
_toggleAutoscrollFx(enable: boolean) {
this.dragging = enable;
const win = this.getWindow();
const method = enable ? 'on' : 'off';
const mt = { on, off };
mt[method](win, 'mousemove dragover', this.updateClientY);
mt[method](win, 'mouseup', this.stopAutoscroll);
}
render() {
const { $el, ppfx, em } = this;
$el.attr({ class: `${ppfx}frame` });

103
packages/core/src/utils/AutoScroller.ts

@ -0,0 +1,103 @@
import { bindAll } from 'underscore';
import { getPointerEvent, off, on } from './dom';
export default class AutoScroller {
private eventEl?: HTMLElement; // Element that handles mouse events
private scrollEl?: HTMLElement | Window; // Element that will be scrolled
private dragging: boolean = false;
private lastClientY?: number;
private lastMaxHeight: number = 0;
private onScroll?: () => void;
private autoscrollLimit: number;
private zoom: number = 1;
/**
* When an element is inside an iframe, its `getBoundingClientRect()` values
* are relative to the iframe's document, not the main window's.
*/
private rectIsInScrollIframe: boolean = false;
constructor(
autoscrollLimit: number = 50,
opts?: {
lastMaxHeight?: number;
onScroll?: () => void;
rectIsInScrollIframe?: boolean;
},
) {
this.autoscrollLimit = autoscrollLimit;
this.lastMaxHeight = opts?.lastMaxHeight ?? 0;
this.onScroll = opts?.onScroll;
this.rectIsInScrollIframe = !!opts?.rectIsInScrollIframe;
bindAll(this, 'start', 'autoscroll', 'updateClientY', 'stop');
}
start(eventEl: HTMLElement, scrollEl: HTMLElement | Window, opts?: { lastMaxHeight?: number; zoom?: number }) {
this.eventEl = eventEl;
this.scrollEl = scrollEl;
this.lastMaxHeight = opts?.lastMaxHeight || Number.POSITIVE_INFINITY;
this.zoom = opts?.zoom || 1;
// By detaching those from the stack avoid browsers lags
// Noticeable with "fast" drag of blocks
setTimeout(() => {
this.toggleAutoscrollFx(true);
requestAnimationFrame(this.autoscroll);
}, 0);
}
private autoscroll() {
const scrollEl = this.scrollEl;
if (this.dragging && scrollEl) {
const clientY = this.lastClientY ?? 0;
const limitTop = this.autoscrollLimit;
const eventElHeight = this.getEventElHeight();
const limitBottom = eventElHeight - limitTop;
let nextTop = 0;
if (clientY < limitTop) nextTop += clientY - limitTop;
if (clientY > limitBottom) nextTop += clientY - limitBottom;
const scrollTop = this.getElScrollTop(scrollEl);
if (this.lastClientY !== undefined && nextTop !== 0 && this.lastMaxHeight - nextTop > scrollTop) {
scrollEl.scrollBy({ top: nextTop, left: 0, behavior: 'auto' });
this.onScroll?.();
}
requestAnimationFrame(this.autoscroll);
}
}
private getEventElHeight() {
const eventEl = this.eventEl;
if (!eventEl) return 0;
const elRect = eventEl.getBoundingClientRect();
return elRect.height;
}
private updateClientY(ev: Event) {
const scrollEl = this.scrollEl;
ev.preventDefault();
const scrollTop = !this.rectIsInScrollIframe ? this.getElScrollTop(scrollEl) : 0;
this.lastClientY = getPointerEvent(ev).clientY * this.zoom - scrollTop;
}
private getElScrollTop(scrollEl: HTMLElement | Window | undefined) {
return (scrollEl instanceof HTMLElement ? scrollEl.scrollTop : scrollEl?.scrollY) || 0;
}
private toggleAutoscrollFx(enable: boolean) {
this.dragging = enable;
const eventEl = this.eventEl;
if (!eventEl) return;
const method = enable ? 'on' : 'off';
const mt = { on, off };
mt[method](eventEl, 'mousemove dragover', this.updateClientY);
mt[method](eventEl, 'mouseup', this.stop);
}
stop() {
this.toggleAutoscrollFx(false);
}
}
Loading…
Cancel
Save