devexpress-diagram
Version:
DevExpress Diagram Control
398 lines (372 loc) • 21.3 kB
text/typescript
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) {
super(zoomLevel, dom);
this.initializeContainerElements(viewElement);
}
private initializeContainerElements(view: SVGElement) {
this.itemSelectorsContainer = this.createAndChangePrimitiveElement(
new GroupPrimitive([], null), view
);
this.itemsContainer = this.createAndChangePrimitiveElement(
new GroupPrimitive([], null), view
);
}
clear() {
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[]) {
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[]) {
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() { }
notifyDragScrollEnd() { }
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) {
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) { }
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): SvgPrimitive<SVGElement>[] {
if(!this.primitives[item.key])
this.primitives[item.key] = item.createPrimitives();
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),
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.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.invalidatePrimitives(shape);
}
this.changeItemElements(this.getSelectorPrimitives(shape), this.itemSelectorElements[key],
type === ItemChangeType.UpdateStructure);
this.changeItemElements(this.getPrimitives(shape), 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),
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.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.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.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) {
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) {
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) {
if(className && element)
this.changePrimitiveElement(new GroupPrimitive([], className), element);
}
removeItemElements(element: SVGElement) {
if(element && element.parentNode)
element.parentNode.removeChild(element);
}
removeItemCustomContent(element: SVGElement, primitives: SvgPrimitive<SVGElement>[]) {
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) {
if(element && parent)
if(sibling)
parent.insertBefore(element, sibling);
else
parent.appendChild(element);
}
changeItemChildrenVisibility(element: SVGElement, expanded: boolean) {
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().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().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.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;
}