UNPKG

devexpress-diagram

Version:

DevExpress Diagram Control

398 lines (372 loc) 21.7 kB
import { ItemChange, ItemChangeType } from "../Model/ModelChange"; import { IMouseOperationsListener, MouseEventElementType, ITextInputOperationListener } from "../Events/Event"; import { Size } from "@devexpress/utils/lib/geometry/size"; import { Point } from "@devexpress/utils/lib/geometry/point"; import { DiagramItem, ItemKey } from "../Model/DiagramItem"; import { SvgPrimitive } from "./Primitives/Primitive"; import { GroupPrimitive } from "./Primitives/GroupPrimitive"; import { RenderUtils } from "./Utils"; import { Shape } from "../Model/Shapes/Shape"; import { Connector } from "../Model/Connectors/Connector"; import { ICanvasViewListener } from "./CanvasViewManager"; import { DOMManipulator } from "./DOMManipulator"; import { DomUtils } from "@devexpress/utils/lib/utils/dom"; import { CanvasManager } from "./CanvasManager"; import { ISelectionChangesListener, Selection } from "../Selection/Selection"; import { ModelUtils } from "../Model/ModelUtils"; export const NOT_VALID_CSSCLASS = "not-valid"; export const CONNECTOR_CAN_MOVE = "can-move"; export class CanvasItemsManager extends CanvasManager implements IMouseOperationsListener, ITextInputOperationListener, ICanvasViewListener, ISelectionChangesListener { itemSelectorsContainer: SVGElement; itemsContainer: SVGElement; private itemSelectorGroupContainers: { [key: number]: SVGElement } = {}; private itemSelectorElements: { [key: number]: SVGElement } = {}; private itemGroupContainers: { [key: number]: SVGElement } = {}; private itemElements: { [key: number]: SVGElement } = {}; private itemChildElements: { [key: number]: SVGElement } = {}; private primitives: { [key: number]: SvgPrimitive<SVGGraphicsElement>[] } = {}; private selectorPrimitives: { [key: number]: SvgPrimitive<SVGGraphicsElement>[] } = {}; private selectedItems : { [key: string] : DiagramItem } = {}; constructor(viewElement: SVGElement, zoomLevel: number, dom: DOMManipulator, instanceId: string) { super(zoomLevel, dom, instanceId); this.initializeContainerElements(viewElement); } private initializeContainerElements(view: SVGElement) { this.itemSelectorsContainer = this.createAndChangePrimitiveElement( new GroupPrimitive([], null), view ); this.itemsContainer = this.createAndChangePrimitiveElement( new GroupPrimitive([], null), view ); } clear(): void { this.primitives = {}; this.selectorPrimitives = {}; RenderUtils.removeContent(this.itemSelectorsContainer); RenderUtils.removeContent(this.itemsContainer); this.itemSelectorGroupContainers = {}; this.itemSelectorElements = {}; this.itemGroupContainers = {}; this.itemElements = {}; this.itemChildElements = {}; this.selectedItems = {}; } applyChange(change: ItemChange): boolean { const item = change.item; const itemParent = this.getItemParent(item.zIndex, item.container && item.container.key); if(!itemParent) return false; if(item instanceof Shape) this.applyShapeChange(item, change.type, change.isValid); else if(item instanceof Connector) this.applyConnectorChange(item, change.type, change.isValid); return true; } private setPointerEventsNone(element: HTMLElement, value: boolean) { const style = element.style; if(style !== undefined && style.pointerEvents !== undefined) style.pointerEvents = value ? "none" : ""; const childNodes = element.childNodes; for(let i = 0; i < childNodes.length; i++) this.setPointerEventsNone(childNodes[i] as HTMLElement, value); } notifyDragStart(itemKeys: ItemKey[]): void { itemKeys.forEach(itemKey => { if(this.itemElements[itemKey]) this.setPointerEventsNone(this.itemElements[itemKey], true); if(this.itemChildElements[itemKey]) this.setPointerEventsNone(this.itemChildElements[itemKey], true); if(this.itemSelectorElements[itemKey]) this.setPointerEventsNone(this.itemSelectorElements[itemKey], true); }); } notifyDragEnd(itemKeys: ItemKey[]): void { itemKeys.forEach(itemKey => { if(this.itemElements[itemKey]) this.setPointerEventsNone(this.itemElements[itemKey], false); if(this.itemChildElements[itemKey]) this.setPointerEventsNone(this.itemChildElements[itemKey], false); if(this.itemSelectorElements[itemKey]) this.setPointerEventsNone(this.itemSelectorElements[itemKey], false); }); } notifyDragScrollStart(): void { } notifyDragScrollEnd(): void { } notifyTextInputStart(item: DiagramItem, _text: string, _position: Point, _size?: Size): void { const element = this.itemElements[item.key]; DomUtils.addClassName(element, "text-input"); } notifyTextInputEnd(item: DiagramItem, _captureFocus?: boolean): void { const element = this.itemElements[item.key]; DomUtils.removeClassName(element, "text-input"); } notifyTextInputPermissionsCheck(item: DiagramItem, allowed: boolean): void { const element = this.itemElements[item.key]; if(allowed) DomUtils.removeClassName(element, NOT_VALID_CSSCLASS); else DomUtils.addClassName(element, NOT_VALID_CSSCLASS); } notifyActualZoomChanged(actualZoom: number): void { const scale = "scale(" + actualZoom + ")"; this.dom.changeByFunc(this.itemsContainer, e => e.setAttribute("transform", scale)); this.dom.changeByFunc(this.itemSelectorsContainer, e => e.setAttribute("transform", scale)); this.actualZoom = actualZoom; } notifyViewAdjusted(_canvasOffset: Point): void { } invalidatePrimitives(item: DiagramItem): void { if(this.primitives[item.key]) { this.primitives[item.key].forEach(primitive => { primitive.dispose(); }); delete this.primitives[item.key]; } if(this.selectorPrimitives[item.key]) { this.selectorPrimitives[item.key].forEach(primitive => { primitive.dispose(); }); delete this.selectorPrimitives[item.key]; } } getPrimitives(item: DiagramItem, instanceId: string): SvgPrimitive<SVGElement>[] { if(!this.primitives[item.key]) this.primitives[item.key] = item.createPrimitives(instanceId); return this.primitives[item.key]; } getSelectorPrimitives(item: DiagramItem): SvgPrimitive<SVGElement>[] { if(!this.selectorPrimitives[item.key]) this.selectorPrimitives[item.key] = item.createSelectorPrimitives(); return this.selectorPrimitives[item.key]; } private getShapeSelectorClassName(shape: Shape): string { let className = "shape"; if(shape.enableChildren) className += " container"; if(shape.isLocked) className += " locked"; return className; } private getShapeClassName(shape: Shape, isValid: boolean): string { const selectorClassName = this.getShapeSelectorClassName(shape); return isValid ? selectorClassName : selectorClassName + " " + NOT_VALID_CSSCLASS; } applyShapeChange(shape: Shape, type: ItemChangeType, isValid: boolean): void { const key = shape.key; const containerKey = shape.container && shape.container.key; const itemSelectorParent = this.getItemSelectorGroupContainer(shape.zIndex, containerKey); const itemParent = this.getItemGroupContainer(shape.zIndex, containerKey); const itemClassName = this.getShapeClassName(shape, isValid); switch(type) { case ItemChangeType.Create: this.itemSelectorElements[key] = this.createItemElements(key, this.getSelectorPrimitives(shape), itemSelectorParent, this.getShapeSelectorClassName(shape), MouseEventElementType.Shape); this.itemElements[key] = this.createItemElements(key, this.getPrimitives(shape, this.instanceId), itemParent, itemClassName, MouseEventElementType.Shape); if(shape.enableChildren) { this.itemChildElements[key] = this.createItemElements(key, [], itemParent, "container-children", MouseEventElementType.Undefined); this.changeItemChildrenVisibility(this.itemChildElements[key], shape.expanded); } break; case ItemChangeType.Remove: this.removeItemCustomContent(this.itemSelectorElements[key], this.getSelectorPrimitives(shape)); this.removeItemCustomContent(this.itemElements[key], this.getPrimitives(shape, this.instanceId)); this.invalidatePrimitives(shape); this.removeItemElements(this.itemSelectorElements[key]); delete this.itemSelectorElements[key]; this.removeItemElements(this.itemElements[key]); delete this.itemElements[key]; if(this.itemChildElements[key]) { this.removeItemElements(this.itemChildElements[key]); delete this.itemChildElements[key]; delete this.itemGroupContainers[key]; delete this.itemSelectorGroupContainers[key]; } break; case ItemChangeType.UpdateStructure: case ItemChangeType.UpdateProperties: case ItemChangeType.Update: if(type !== ItemChangeType.Update) { if(type === ItemChangeType.UpdateStructure) { this.removeItemCustomContent(this.itemSelectorElements[key], this.getSelectorPrimitives(shape)); this.removeItemCustomContent(this.itemElements[key], this.getPrimitives(shape, this.instanceId)); } this.invalidatePrimitives(shape); } this.changeItemElements(this.getSelectorPrimitives(shape), this.itemSelectorElements[key], type === ItemChangeType.UpdateStructure); this.changeItemElements(this.getPrimitives(shape, this.instanceId), this.itemElements[key], type === ItemChangeType.UpdateStructure); this.changeItemClassName(this.itemElements[key], itemClassName); if(this.itemChildElements[key]) this.changeItemChildrenVisibility(this.itemChildElements[key], shape.expanded); if(itemSelectorParent !== (this.itemSelectorElements[key] && this.itemSelectorElements[key].parentNode)) this.moveItemElements(itemSelectorParent, this.itemSelectorElements[key]); if(itemParent !== (this.itemElements[key] && this.itemElements[key].parentNode)) this.moveItemElements(itemParent, this.itemElements[key]); if(this.itemChildElements[key] && (itemParent !== this.itemChildElements[key].parentNode)) this.moveItemElements(itemParent, this.itemChildElements[key]); break; case ItemChangeType.UpdateClassName: this.changeItemClassName(this.itemElements[key], itemClassName); } } private getConnectorSelectorClassName(connector: Connector): string { const selectorClassName = "connector"; return ModelUtils.canMoveConnector(this.selectedItems, connector) ? selectorClassName + " " + CONNECTOR_CAN_MOVE : selectorClassName; } private getConnectorClassName(connector: Connector, isValid: boolean): string { const selectorMoveClassName = this.getConnectorSelectorClassName(connector); return isValid ? selectorMoveClassName : selectorMoveClassName + " " + NOT_VALID_CSSCLASS; } applyConnectorChange(connector: Connector, type: ItemChangeType, isValid: boolean): void { const key = connector.key; const containerKey = connector.container && connector.container.key; const itemSelectorParent = this.getItemSelectorGroupContainer(connector.zIndex, containerKey); const itemParent = this.getItemGroupContainer(connector.zIndex, containerKey); const className = this.getConnectorClassName(connector, isValid); switch(type) { case ItemChangeType.Create: this.itemSelectorElements[key] = this.createItemElements(key, this.getSelectorPrimitives(connector), itemSelectorParent, this.getConnectorSelectorClassName(connector), MouseEventElementType.Connector); this.itemElements[key] = this.createItemElements(key, this.getPrimitives(connector, this.instanceId), itemParent, className, MouseEventElementType.Connector); break; case ItemChangeType.Remove: this.removeItemCustomContent(this.itemSelectorElements[key], this.getSelectorPrimitives(connector)); this.removeItemCustomContent(this.itemElements[key], this.getPrimitives(connector, this.instanceId)); this.invalidatePrimitives(connector); this.removeItemElements(this.itemSelectorElements[key]); delete this.itemSelectorElements[key]; this.removeItemElements(this.itemElements[key]); delete this.itemElements[key]; break; case ItemChangeType.UpdateStructure: case ItemChangeType.UpdateProperties: case ItemChangeType.Update: if(type !== ItemChangeType.Update) { if(type === ItemChangeType.UpdateStructure) { this.removeItemCustomContent(this.itemSelectorElements[key], this.getSelectorPrimitives(connector)); this.removeItemCustomContent(this.itemElements[key], this.getPrimitives(connector, this.instanceId)); } this.invalidatePrimitives(connector); } this.changeItemElements(this.getSelectorPrimitives(connector), this.itemSelectorElements[key], type === ItemChangeType.UpdateStructure); this.changeItemClassName(this.itemSelectorElements[key], this.getConnectorSelectorClassName(connector)); this.changeItemElements(this.getPrimitives(connector, this.instanceId), this.itemElements[key], type === ItemChangeType.UpdateStructure); this.changeItemClassName(this.itemElements[key], className); if(itemSelectorParent !== (this.itemSelectorElements[key] && this.itemSelectorElements[key].parentNode)) this.moveItemElements(itemSelectorParent, this.itemSelectorElements[key]); if(itemParent !== (this.itemElements[key] && this.itemElements[key].parentNode)) this.moveItemElements(itemParent, this.itemElements[key]); break; case ItemChangeType.UpdateClassName: this.changeItemClassName(this.itemSelectorElements[key], this.getConnectorSelectorClassName(connector)); this.changeItemClassName(this.itemElements[key], className); } } createItemElements(key: ItemKey, primitives: SvgPrimitive<SVGElement>[], parent: SVGElement, className: string, type: MouseEventElementType): SVGElement { const gEl = this.createAndChangePrimitiveElement(new GroupPrimitive([], className, undefined, undefined, e => RenderUtils.setElementEventData(e, type, key)), parent); this.createAndChangePrimitivesElements(primitives, gEl); return gEl; } changeItemElements(primitives: SvgPrimitive<SVGElement>[], element: SVGElement, updateStructure: boolean): void { if(updateStructure || primitives.length !== element.childNodes.length) { RenderUtils.removeContent(element); this.createAndChangePrimitivesElements(primitives, element); } else this.dom.changeChildrenByPrimitives(primitives, element); } changeItemClassName(element: SVGElement, className?: string): void { if(className && element) this.changePrimitiveElement(new GroupPrimitive([], className), element); } removeItemElements(element: SVGElement): void { if(element && element.parentNode) element.parentNode.removeChild(element); } removeItemCustomContent(element: SVGElement, primitives: SvgPrimitive<SVGElement>[]): void { if(element && primitives && primitives.length === element.childNodes.length) primitives.forEach((primitive, index) => { primitive.destroyCustomContent(<SVGElement>element.childNodes[index]); }); } moveItemElements(parent: SVGElement, element: SVGElement, sibling?: SVGElement): void { if(element && parent) if(sibling) parent.insertBefore(element, sibling); else parent.appendChild(element); } changeItemChildrenVisibility(element: SVGElement, expanded: boolean): void { element.style.display = expanded ? "" : "none"; } getItemGroupContainerKey(zIndex: number, parentContainerKey?: ItemKey): string { return parentContainerKey !== undefined ? zIndex + "_" + parentContainerKey : zIndex.toString(); } getItemGroupContainer(zIndex: number, parentContainerKey?: ItemKey): SVGElement { const parent = parentContainerKey !== undefined ? this.getItemParent(zIndex, parentContainerKey) : this.itemsContainer; const key = parentContainerKey || "Main"; if(this.itemGroupContainers[key] === undefined || this.itemGroupContainers[key][zIndex] === undefined) { if(this.itemGroupContainers[key] === undefined) this.itemGroupContainers[key] = []; const nextSiblingZIndex = Object.keys(this.itemGroupContainers[key]).map(z => +z).sort((a, b) => a - b).filter(z => z > zIndex)[0]; this.itemGroupContainers[key][zIndex] = this.createAndChangePrimitiveElement( new GroupPrimitive([], null, zIndex), parent, this.itemGroupContainers[key][nextSiblingZIndex] ); } return this.itemGroupContainers[key][zIndex]; } getItemSelectorGroupContainer(zIndex: number, parentContainerKey?: ItemKey): SVGElement { const parent = parentContainerKey !== undefined ? this.getItemSelectorParent(zIndex, parentContainerKey) : this.itemSelectorsContainer; const key = parentContainerKey || "Main"; if(this.itemSelectorGroupContainers[key] === undefined || this.itemSelectorGroupContainers[key][zIndex] === undefined) { if(this.itemSelectorGroupContainers[key] === undefined) this.itemSelectorGroupContainers[key] = []; const nextSiblingZIndex = Object.keys(this.itemSelectorGroupContainers[key]).map(z => +z).sort((a, b) => a - b).filter(z => z > zIndex)[0]; this.itemSelectorGroupContainers[key][zIndex] = this.createAndChangePrimitiveElement( new GroupPrimitive([], null, zIndex), parent, this.itemSelectorGroupContainers[key][nextSiblingZIndex] ); } return this.itemSelectorGroupContainers[key][zIndex]; } getItemParent(zIndex: number, parentContainerKey?: ItemKey): SVGElement { return parentContainerKey !== undefined ? this.itemChildElements[parentContainerKey] : this.getItemGroupContainer(zIndex); } getItemSelectorParent(zIndex: number, parentContainerKey?: ItemKey): SVGElement { return parentContainerKey !== undefined ? this.itemChildElements[parentContainerKey] : this.getItemSelectorGroupContainer(zIndex); } notifySelectionChanged(selection: Selection): void { const newSelectedItems = ModelUtils.createSelectedItems(selection); const itemsToUpdate : DiagramItem[] = []; this.populateItems(itemsToUpdate, newSelectedItems, this.selectedItems); this.populateItems(itemsToUpdate, this.selectedItems, newSelectedItems); this.selectedItems = newSelectedItems; itemsToUpdate.forEach( item => { if(item instanceof Connector && !this.pendingRemoveChanges[item.key]) this.applyOrPostponeChanges([new ItemChange(item, ItemChangeType.UpdateClassName, true)]); } ); } private populateItems(target : DiagramItem[], sourceSet: { [key: string] : DiagramItem }, checkSet: { [key: string] : DiagramItem }) : void { Object.keys(sourceSet).forEach(key => { if(!checkSet[key]) target.push(sourceSet[key]); }); } } export interface ILayoutPointResolver { getModelPoint(absolutePoint: Point, checkScrollArea?: boolean): Point; getAbsolutePoint(modelPoint: Point, excludeScroll?: boolean, checkScrollArea?: boolean): Point; }