UNPKG

devexpress-diagram

Version:

DevExpress Diagram Control

547 lines (505 loc) 25.4 kB
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; }