devexpress-diagram
Version:
DevExpress Diagram Control
547 lines (505 loc) • 25.4 kB
text/typescript
import { IEventManager } from "../Events/EventManager";
import { DiagramMouseEvent, MouseEventSource, MouseButton, MouseEventElementType, DiagramWheelEvent, IMouseOperationsListener, DiagramContextMenuEvent, DiagramMouseEventTouch } from "../Events/Event";
import { EvtUtils } from "@devexpress/utils/lib/utils/evt";
import { Rectangle } from "@devexpress/utils/lib/geometry/rectangle";
import { Offsets } from "@devexpress/utils/lib/geometry/offsets";
import { DomUtils } from "@devexpress/utils/lib/utils/dom";
import { Size } from "@devexpress/utils/lib/geometry/size";
import { Point } from "@devexpress/utils/lib/geometry/point";
import { KeyUtils } from "@devexpress/utils/lib/utils/key";
import { Browser } from "@devexpress/utils/lib/browser";
import { RenderUtils, raiseEvent } from "./Utils";
import { CanvasItemsManager } from "./CanvasItemsManager";
import { IScrollView, NativeScrollView } from "./ScrollView";
import { IReadOnlyChangesListener, AutoZoomMode } from "../Settings";
import { InputManager } from "./InputManager";
import { CanvasPageManager, ICanvasPageManagerSettings } from "./CanvasPageManager";
import { CanvasViewManager } from "./CanvasViewManager";
import { CanvasSelectionManager } from "./CanvasSelectionManager";
import { AutoScrollController } from "./AutoScrollController";
import { TextMeasurer } from "./Measurer/TextMeasurer";
import { DiagramItem } from "../Model/DiagramItem";
import { RenderHelper } from "./RenderHelper";
import { DOMManipulator } from "./DOMManipulator";
import { ITextMeasurer } from "./Measurer/ITextMeasurer";
import { EventUtils } from "../Utils";
const READONLY_CSSCLASS = "dxdi-read-only";
const TOUCH_ACTION_CSSCLASS = "dxdi-touch-action";
export const LONG_TOUCH_TIMEOUT = 500;
export const DBL_CLICK_TIMEOUT = 500;
export class RenderManager implements IReadOnlyChangesListener, IMouseOperationsListener {
view: CanvasViewManager;
input: InputManager;
items: CanvasItemsManager;
page: CanvasPageManager;
contextMenuEnabled: boolean;
selection: CanvasSelectionManager;
measurer: ITextMeasurer;
dom: DOMManipulator;
private moveLocked: boolean = false;
private lockMouseMoveTimer: number = -1;
private scroll: IScrollView;
private autoScroll: AutoScrollController;
private mainElement: HTMLElement;
private svgElement: SVGSVGElement;
private events: IEventManager;
private lastDownMouseEvent: DiagramMouseEvent;
private lastClickElement = undefined;
private longTouchTimer: number = undefined;
private dblTouchTimer: number = undefined;
private touchDownPoint: Point;
static touchPositionLimit = 4;
private pointers:Record<string, any> = { };
private onPointerDownHandler: any;
private onPointerUpHandler: any;
private onPointerMoveHandler: any;
private onPointerCancelHandler: any;
private onPointerLeaveHandler: any;
private onSvgMouseMoveHandler: any;
private onSvgMouseDownHandler: any;
private onMouseDblClickHandler: any;
private onMouseWheelHandler: any;
private onContextMenuHandler: any;
private onWindowResizelHandler: any;
private onOrientationChangeHandler: any;
private onMouseClickHandler: any;
private instanceId: string;
constructor(parent: HTMLElement, events: IEventManager, measurer: ITextMeasurer, settings: ICanvasPageManagerSettings, instanceId: string, scrollView?: IScrollView,
focusElementsParent?: HTMLElement) {
const mainElement = RenderHelper.createMainElement(parent);
const svgElement = RenderHelper.createSvgElement(mainElement);
this.instanceId = instanceId;
this.scroll = scrollView || new NativeScrollView(parent);
this.measurer = measurer;
this.dom = new DOMManipulator(this.measurer);
this.view = new CanvasViewManager(this.scroll, svgElement, settings.modelSize, settings.zoomLevel, settings.autoZoom, settings.simpleView, settings.rectangle, this.dom, this.instanceId);
this.input = new InputManager(mainElement, this.view, events, this.measurer, settings.zoomLevel, focusElementsParent);
this.items = new CanvasItemsManager(this.view.canvasElement, settings.zoomLevel, this.dom, this.instanceId);
this.page = new CanvasPageManager(this.view.pageElement, settings, this.dom, this.instanceId);
this.selection = new CanvasSelectionManager(this.view.canvasElement, settings.zoomLevel, settings.readOnly, this.dom, this.instanceId);
this.contextMenuEnabled = settings.contextMenuEnabled;
this.view.onViewChanged.add(this.page);
this.view.onViewChanged.add(this.items);
this.view.onViewChanged.add(this.selection);
this.view.onViewChanged.add(this.input);
this.autoScroll = new AutoScrollController(this.scroll, svgElement, this.view, this.dom);
this.attachEvents(svgElement);
this.mainElement = mainElement;
this.svgElement = svgElement;
this.events = events;
this.notifyReadOnlyChanged(settings.readOnly);
}
clean(removeElement?: (element: HTMLElement) => void): void {
this.killLockMouseMoveTimer();
this.clearLastMouseDownEvent();
this.detachEvents(this.svgElement);
this.scroll.detachEvents();
this.input.detachEvents();
this.dom.cancelAnimation();
if(removeElement)
removeElement(this.mainElement);
}
replaceParent(parent: HTMLElement, scroll?: IScrollView): void {
if(this.mainElement && this.mainElement.parentNode !== parent)
parent.appendChild(this.mainElement);
if(scroll && scroll !== this.scroll) {
this.scroll && this.scroll.detachEvents();
this.scroll = scroll;
}
if(this.measurer instanceof TextMeasurer)
this.measurer.replaceParent(parent);
}
update(saveScrollPosition: boolean): void {
this.view.adjust({ horizontal: !saveScrollPosition, vertical: !saveScrollPosition });
this.page.redraw();
}
onNewModel(items: DiagramItem[]): void {
this.measurer.onNewModel(items, this.dom);
}
clear(): void {
this.items.clear();
this.selection.clear();
this.input.clear();
}
protected attachPointerEvents(svgElement: SVGSVGElement): void {
DomUtils.addClassName(svgElement, TOUCH_ACTION_CSSCLASS);
RenderHelper.addEventListener(svgElement, "pointerdown", this.onPointerDownHandler);
RenderHelper.addEventListener(svgElement, "mousedown", this.onSvgMouseDownHandler);
RenderHelper.addEventListener(svgElement, "mousemove", this.onSvgMouseMoveHandler);
RenderHelper.addEventListener(Browser.TouchUI ? svgElement : document, "pointerup", this.onPointerUpHandler);
RenderHelper.addEventListener(Browser.TouchUI ? svgElement : document, "pointermove", this.onPointerMoveHandler);
RenderHelper.addEventListener(svgElement, "pointercancel", this.onPointerCancelHandler);
RenderHelper.addEventListener(svgElement, "pointerleave", this.onPointerLeaveHandler);
}
protected detachPointerEvents(svgElement: SVGSVGElement): void {
RenderHelper.removeEventListener(svgElement, "pointerdown", this.onPointerDownHandler);
RenderHelper.removeEventListener(svgElement, "mousedown", this.onSvgMouseDownHandler);
RenderHelper.removeEventListener(svgElement, "mousemove", this.onSvgMouseMoveHandler);
RenderHelper.removeEventListener(Browser.TouchUI ? svgElement : document, "pointerup", this.onPointerUpHandler);
RenderHelper.removeEventListener(Browser.TouchUI ? svgElement : document, "pointermove", this.onPointerMoveHandler);
RenderHelper.removeEventListener(svgElement, "pointercancel", this.onPointerCancelHandler);
RenderHelper.removeEventListener(svgElement, "pointerleave", this.onPointerLeaveHandler);
DomUtils.removeClassName(svgElement, TOUCH_ACTION_CSSCLASS);
}
private attachEvents(svgElement: SVGSVGElement) {
this.onPointerDownHandler = this.onPointerDown.bind(this);
this.onPointerUpHandler = this.onPointerUp.bind(this);
this.onPointerMoveHandler = this.onPointerMove.bind(this);
this.onPointerCancelHandler = this.onPointerCancel.bind(this);
this.onPointerLeaveHandler = this.onPointerLeave.bind(this);
this.onSvgMouseDownHandler = this.onSvgMouseDown.bind(this);
this.onSvgMouseMoveHandler = this.onSvgMouseMove.bind(this);
this.onMouseWheelHandler = this.onMouseWheel.bind(this);
this.onMouseDblClickHandler = this.onMouseDblClick.bind(this);
this.onContextMenuHandler = this.onContextMenu.bind(this);
this.onWindowResizelHandler = this.onWindowResize.bind(this);
this.onOrientationChangeHandler = this.onOrientationChange.bind(this);
this.onMouseClickHandler = this.onMouseClick.bind(this);
this.attachPointerEvents(svgElement);
RenderHelper.addEventListener(svgElement, "wheel", this.onMouseWheelHandler);
RenderHelper.addEventListener(svgElement, "dblclick", this.onMouseDblClickHandler);
RenderHelper.addEventListener(svgElement, "click", this.onMouseClickHandler);
RenderHelper.addEventListener(svgElement, "contextmenu", this.onContextMenuHandler);
RenderHelper.addEventListener(window, "resize", this.onWindowResizelHandler);
RenderHelper.addEventListener(window, "orientationchange", this.onOrientationChangeHandler);
this.input.mouseWheelHandler = this.onMouseWheelHandler;
}
private detachEvents(svgElement: SVGSVGElement) {
this.detachPointerEvents(svgElement);
RenderHelper.removeEventListener(svgElement, "wheel", this.onMouseWheelHandler);
RenderHelper.removeEventListener(svgElement, "dblclick", this.onMouseDblClickHandler);
RenderHelper.removeEventListener(svgElement, "contextmenu", this.onContextMenuHandler);
RenderHelper.removeEventListener(svgElement, "click", this.onMouseClickHandler);
RenderHelper.removeEventListener(window, "resize", this.onWindowResizelHandler);
RenderHelper.removeEventListener(window, "orientationchange", this.onOrientationChangeHandler);
}
setPointerPosition(evt: PointerEvent): void {
this.pointers[evt.pointerId] = {
clientX: evt.clientX,
clientY: evt.clientY
};
}
clearPointerPosition(evt: PointerEvent): void {
delete this.pointers[evt.pointerId];
}
onPointerUp(evt: PointerEvent): void {
this.clearPointerPosition(evt);
this.onMouseUp(evt);
}
onPointerMove(evt: PointerEvent): void {
if((Browser.TouchUI && !EventUtils.isMousePointer(evt)) || EventUtils.isLeftButtonPressed(evt))
this.setPointerPosition(evt);
this.onDocumentMouseMove(evt);
}
onPointerCancel(evt: PointerEvent): void {
this.clearPointerPosition(evt);
}
onPointerLeave(evt: PointerEvent): void {
if(EventUtils.isMousePointer(evt))
this.onMouseLeave(evt);
this.clearPointerPosition(evt);
}
onPointerDown(evt: PointerEvent): void {
this.setPointerPosition(evt);
if(this.getPointerCount() > 2)
this.pointers = {};
this.lockMouseMove();
this.input.lockFocus();
this.autoScroll.onMouseDown(evt);
this.lastDownMouseEvent = this.createDiagramMouseEvent(evt);
raiseEvent(evt, this.lastDownMouseEvent, e => this.events.onMouseDown(e));
if(this.events.canFinishTextEditing())
this.input.captureFocus();
if(EventUtils.isTouchEvent(evt))
this.processTouchDown(evt);
}
onSvgMouseDown(evt: MouseEvent): void {
EvtUtils.preventEvent(evt);
}
onDocumentMouseMove(evt: MouseEvent): void {
if(this.moveLocked) return;
this.autoScroll.onMouseMove(evt, () => this.onMouseMoveCore(evt));
this.onMouseMoveCore(evt);
Browser.IE && this.lockMouseMove();
if(EventUtils.isTouchEvent(evt))
this.processTouchMove(evt);
}
onSvgMouseMove(evt: MouseEvent): void {
EvtUtils.preventEventAndBubble(evt);
}
private onMouseMoveCore(evt: MouseEvent) {
raiseEvent(evt, this.createDiagramMouseEvent(evt), e => this.events.onMouseMove(e));
}
onMouseUp(evt: MouseEvent): void {
this.lockMouseMove();
const mouseEvent = this.createDiagramMouseEvent(evt);
raiseEvent(evt, mouseEvent, e => this.events.onMouseUp(e));
this.autoScroll.onMouseUp(evt);
if(mouseEvent.source.type !== MouseEventElementType.Undefined)
this.input.captureFocus(true);
if(EventUtils.isTouchEvent(evt))
this.processTouchUp(evt);
}
private onMouseEnter(evt: MouseEvent): void {
this.autoScroll.onMouseEnter(evt);
raiseEvent(evt, this.createDiagramMouseEvent(evt), e => this.events.onMouseEnter(e));
}
private onMouseLeave(evt: MouseEvent): void {
raiseEvent(evt, this.createDiagramMouseEvent(evt), e => this.events.onMouseLeave(e));
}
private onMouseDblClick(evt: MouseEvent): void {
raiseEvent(evt, this.createDiagramMouseEvent(evt), e => this.events.onDblClick(e));
}
private onMouseClick(evt: MouseEvent): void {
if(!EventUtils.isTouchEvent(evt))
raiseEvent(evt, this.createActualMouseClickEvent(evt), e => this.events.onClick(e));
else if(!EventUtils.isMousePointer(evt))
this.input.captureFocus();
}
private createActualMouseClickEvent(evt: MouseEvent) : DiagramMouseEvent {
if(!this.lastDownMouseEvent)
return this.createDiagramMouseEvent(evt);
return new DiagramMouseEvent(
this.lastDownMouseEvent.modifiers,
this.lastDownMouseEvent.button,
this.lastDownMouseEvent.offsetPoint.clone(),
this.lastDownMouseEvent.modelPoint.clone(),
this.lastDownMouseEvent.source,
this.createDiagramMouseEventTouches(evt));
}
private onContextMenu(evt: MouseEvent) {
if(!this.contextMenuEnabled) return;
if(evt.buttons !== 1)
raiseEvent(evt, this.createDiagramContextMenuEvent(evt), e => this.events.onContextMenu(e));
this.input.captureFocus();
return EvtUtils.preventEventAndBubble(evt);
}
processTouchDown(evt: MouseEvent): void {
this.touchDownPoint = this.getTouchPointFromEvent(evt);
this.resetLongTouch();
this.longTouchTimer = setTimeout(() => {
raiseEvent(evt, this.createDiagramMouseEvent(evt), e => this.events.onLongTouch(e));
this.resetLongTouch();
this.resetDblClick();
}, LONG_TOUCH_TIMEOUT);
}
processTouchMove(evt: MouseEvent): void {
const currentTouchPoint = this.getTouchPointFromEvent(evt);
if(this.touchDownPoint && currentTouchPoint && (Math.abs(this.touchDownPoint.x - currentTouchPoint.x) > RenderManager.touchPositionLimit ||
Math.abs(this.touchDownPoint.y - currentTouchPoint.y) > RenderManager.touchPositionLimit)) {
this.resetLongTouch();
this.resetDblClick();
}
}
getPointers(): Array<Record<string, any>> {
return Object.keys(this.pointers).map(k => this.pointers[k]);
}
getPointerCount(): number {
return Object.keys(this.pointers).length;
}
getTouchPointFromEvent(evt: MouseEvent): Point {
let touchPosition;
const touches = evt["touches"];
if(touches && touches.length > 0)
touchPosition = new Point(touches[0].clientX, touches[0].clientY);
else {
const pointers = this.getPointers();
if(pointers.length)
touchPosition = new Point(pointers[0].clientX, pointers[0].clientY);
}
return touchPosition;
}
processTouchUp(evt: MouseEvent): void {
if(this.longTouchTimer !== undefined) {
raiseEvent(evt, this.createDiagramMouseEvent(evt), e => this.events.onClick(e));
const element = EvtUtils.getEventSource(evt);
if(this.dblTouchTimer !== undefined && this.lastClickElement === element) {
raiseEvent(evt, this.createDiagramMouseEvent(evt), e => this.events.onDblClick(e));
this.resetDblClick();
}
else {
this.resetDblClick();
this.dblTouchTimer = setTimeout(() => this.dblTouchTimer = undefined, DBL_CLICK_TIMEOUT);
}
this.lastClickElement = element;
}
this.resetLongTouch();
this.touchDownPoint = undefined;
}
private resetLongTouch() {
if(this.longTouchTimer !== undefined)
clearTimeout(this.longTouchTimer);
this.longTouchTimer = undefined;
}
private resetDblClick() {
if(this.dblTouchTimer !== undefined)
clearTimeout(this.dblTouchTimer);
this.dblTouchTimer = undefined;
}
onOrientationChange(): void {
setTimeout(() => this.onWindowResize(), 100);
}
onWindowResize(): void {
let resetTo = { horizontal: false, vertical: false };
if(this.view.autoZoom !== AutoZoomMode.Disabled) {
resetTo.horizontal = true;
resetTo.vertical = true;
}
else {
const oldFitInfo = this.view.checkFitToCanvas();
const newFitInfo = this.view.checkFitToCanvas(this.scroll.getSize());
resetTo = { horizontal: oldFitInfo.horizontal !== newFitInfo.horizontal || newFitInfo.horizontal, vertical: oldFitInfo.vertical !== newFitInfo.vertical || newFitInfo.vertical };
}
this.view.adjust(resetTo);
}
onMouseWheel(evt: WheelEvent): void {
raiseEvent(evt, this.createDiagramWheelEvent(evt), e => this.events.onMouseWheel(e));
}
notifyModelSizeChanged(size: Size, offset?: Offsets): void {
this.view.notifyModelSizeChanged(size, offset);
}
notifyModelRectangleChanged(rectangle: Rectangle): void {
this.view.notifyModelRectangleChanged(rectangle);
}
notifyReadOnlyChanged(readOnly: boolean): void {
DomUtils.toggleClassName(this.mainElement, READONLY_CSSCLASS, readOnly);
}
notifyDragStart(_itemKeys: string[]): void { }
notifyDragEnd(_itemKeys: string[]): void { }
notifyDragScrollStart(): void {
this.autoScroll.onDragScrollStart();
}
notifyDragScrollEnd(): void {
this.autoScroll.onDragScrollEnd();
}
notifyToolboxDragStart(evt: MouseEvent): void {
this.onMouseEnter(evt);
}
notifyToolboxDragEnd(evt: MouseEvent): void {
if(evt && EventUtils.isPointerEvents())
this.onMouseUp(evt);
}
notifyToolboxDraggingMouseMove(evt: MouseEvent): void {
this.onDocumentMouseMove(evt);
}
private createDiagramMouseEvent(evt: MouseEvent): DiagramMouseEvent {
const modifiers = KeyUtils.getKeyModifiers(evt);
const button = isLeftButtonPressed(evt) ? MouseButton.Left : MouseButton.Right;
const offsetPoint = this.getOffsetPointByEvent(evt);
const modelPoint = this.getModelPoint(offsetPoint);
const isTouchMode = EventUtils.isTouchEvent(evt);
const eventSource = this.getEventSource(evt, isTouchMode);
const touches = this.createDiagramMouseEventTouches(evt);
return new DiagramMouseEvent(modifiers, button, offsetPoint, modelPoint, eventSource, touches, isTouchMode);
}
private createDiagramMouseEventTouches(evt: MouseEvent) {
const touches = [];
if(evt["touches"])
for(let i = 0; i < evt["touches"].length; i++) {
const x = evt["touches"][i].clientX;
const y = evt["touches"][i].clientY;
const offsetPoint = this.getOffsetPointByEventPoint(x, y);
const modelPoint = this.getModelPoint(offsetPoint);
touches.push(new DiagramMouseEventTouch(offsetPoint, modelPoint));
}
else {
const pointers = this.getPointers();
for(let i = 0; i < pointers.length; i++) {
const x = pointers[i].clientX;
const y = pointers[i].clientY;
const offsetPoint = this.getOffsetPointByEventPoint(x, y);
const modelPoint = this.getModelPoint(offsetPoint);
touches.push(new DiagramMouseEventTouch(offsetPoint, modelPoint));
}
}
return touches;
}
private createDiagramContextMenuEvent(evt: MouseEvent) {
const modifiers = KeyUtils.getKeyModifiers(evt);
const eventPoint = new Point(evt.pageX, evt.pageY);
const offsetPoint = this.getOffsetPointByEvent(evt);
const modelPoint = this.getModelPoint(offsetPoint);
return new DiagramContextMenuEvent(modifiers, eventPoint, modelPoint);
}
private createDiagramWheelEvent(evt: WheelEvent) {
const modifiers = KeyUtils.getKeyModifiers(evt);
const offsetPoint = this.getOffsetPointByEvent(evt);
const modelPoint = this.view.getModelPoint(offsetPoint);
const eventSource = this.getEventSource(evt);
const deltaX = evt.deltaX || (evt["originalEvent"] && evt["originalEvent"].deltaX);
const deltaY = evt.deltaY || (evt["originalEvent"] && evt["originalEvent"].deltaY);
return new DiagramWheelEvent(modifiers, deltaX, deltaY, offsetPoint, modelPoint, eventSource);
}
private getEventSource(evt: MouseEvent, findByPosition?: boolean): MouseEventSource {
let element = findByPosition ? EvtUtils.getEventSourceByPosition(evt) : EvtUtils.getEventSource(evt);
if(this.isDiagramControl(element))
while(element && !this.isDocumentContainer(element)) {
const src = RenderUtils.getElementEventData(element);
if(src !== undefined)
return src;
if(this.input.isTextInputElement(element))
return new MouseEventSource(MouseEventElementType.Document);
element = <HTMLElement>element.parentNode;
}
const src = new MouseEventSource(MouseEventElementType.Undefined);
if(element && this.isDocumentContainer(element))
src.type = MouseEventElementType.Background;
return src;
}
private isDiagramControl(element: HTMLElement): boolean {
while(element) {
if(this.isDocumentContainer(element))
return true;
element = <HTMLElement>element.parentNode;
}
return false;
}
private isDocumentContainer(element: HTMLElement): boolean {
return element === this.mainElement;
}
private lockMouseMove() {
this.moveLocked = true;
this.lockMouseMoveTimer = setTimeout(() => {
this.moveLocked = false;
this.lockMouseMoveTimer = -1;
}, 10);
}
private killLockMouseMoveTimer() {
if(this.lockMouseMoveTimer !== -1) {
clearTimeout(this.lockMouseMoveTimer);
this.lockMouseMoveTimer = -1;
}
}
private clearLastMouseDownEvent() {
this.lastDownMouseEvent = undefined;
}
protected getModelPoint(offsetPoint: Point): Point {
return this.view.getModelPoint(offsetPoint);
}
protected getOffsetPointByEvent(evt): Point {
const clientX: number = EvtUtils.getEventX(evt);
const clientY: number = EvtUtils.getEventY(evt);
return this.getOffsetPointByEventPoint(clientX, clientY);
}
protected getOffsetPointByEventPoint(clientX: number, clientY: number): Point {
const scrollContainer = this.scroll.getScrollContainer();
const containerX = DomUtils.getAbsolutePositionX(scrollContainer);
const containerY = DomUtils.getAbsolutePositionY(scrollContainer);
return new Point(clientX - containerX, clientY - containerY);
}
getModelPointByEventPoint(clientX: number, clientY: number): Point {
const offsetPoint = this.getOffsetPointByEventPoint(clientX, clientY);
return this.view.getModelPoint(offsetPoint);
}
getEventPointByModelPoint(point: Point): Point {
const pos = this.view.getAbsolutePoint(point);
const scrollContainer = this.scroll.getScrollContainer();
return new Point(
DomUtils.getAbsolutePositionX(scrollContainer) + pos.x,
DomUtils.getAbsolutePositionY(scrollContainer) + pos.y
);
}
}
function isLeftButtonPressed(evt: MouseEvent): boolean {
return !Browser.MSTouchUI ? EventUtils.isLeftButtonPressed(evt) : evt.button !== 2;
}