devexpress-diagram
Version:
DevExpress Diagram Control
896 lines (855 loc) • 56.6 kB
text/typescript
import { PAGE_BG_TEXTFLOOR_FILTER_IDPREFIX } from "./CanvasManagerBase";
import { GroupPrimitive } from "./Primitives/GroupPrimitive";
import { RenderUtils } from "./Utils";
import { IVisualizersListener } from "../Events/EventManager";
import { Selection, ISelectionChangesListener } from "../Selection/Selection";
import { IMouseOperationsListener, MouseEventElementType, ITextInputOperationListener, ResizeEventSource } from "../Events/Event";
import { ItemKey, ConnectionPointSide, DiagramItem, ItemsMap } from "../Model/DiagramItem";
import { GeometryUtils } from "../Utils";
import { Rectangle } from "@devexpress/utils/lib/geometry/rectangle";
import { Size } from "@devexpress/utils/lib/geometry/size";
import { Point } from "@devexpress/utils/lib/geometry/point";
import { ConnectionPointInfo } from "../Events/Visualizers/ConnectionPointsVisualizer";
import { ExtensionLine, ExtensionLineType } from "../Events/Visualizers/ExtensionLinesVisualizer";
import { Shape } from "../Model/Shapes/Shape";
import { Connector } from "../Model/Connectors/Connector";
import { ConnectorLineOption } from "../Model/Connectors/ConnectorProperties";
import { RectanglePrimitive } from "./Primitives/RectaglePrimitive";
import { SvgPrimitive } from "./Primitives/Primitive";
import { PathPrimitive, PathPrimitiveMoveToCommand, PathPrimitiveLineToCommand, PathPrimitiveCommand } from "./Primitives/PathPrimitive";
import { TextPrimitive } from "./Primitives/TextPrimitive";
import { EllipsePrimitive } from "./Primitives/EllipsePrimitive";
import { IZoomChangesListener, IReadOnlyChangesListener } from "../Settings";
import { ShapeParameterPoint } from "../Model/Shapes/ShapeParameterPoint";
import { ConnectorRenderPoint } from "../Model/Connectors/ConnectorRenderPoint";
import { TextStyle, Style, StrokeStyle } from "../Model/Style";
import { ItemChange } from "../Model/ModelChange";
import { ICanvasViewListener } from "./CanvasViewManager";
import { UnitConverter } from "@devexpress/utils/lib/class/unit-converter";
import { TextOwner } from "./Measurer/ITextMeasurer";
import { DOMManipulator } from "./DOMManipulator";
import { Browser } from "@devexpress/utils/lib/browser";
import { DomUtils } from "@devexpress/utils/lib/utils/dom";
import { NOT_VALID_CSSCLASS } from "./CanvasItemsManager";
import { CanvasManager } from "./CanvasManager";
import { ConnectionTargetInfo } from "../Events/Visualizers/ConnectionTargetVisualizer";
import { ContainerTargetInfo } from "../Events/Visualizers/ContainerTargetVisualizer";
import { Metrics } from "@devexpress/utils/lib/geometry/metrics";
import { MathUtils } from "@devexpress/utils/lib/utils/math";
import { ModelUtils } from "../Model/ModelUtils";
const MULTIPLE_SELECTION_KEY = "-1";
export const SELECTION_ELEMENT_CLASSNAMES = {
SELECTION_RECTANGLE: "selection-rect",
CONNECTION_POINT: "connection-point",
ACTIVE: "active",
CONTAINER_TARGET: "container-target",
CONNECTION_TARGET: "connection-target",
EXTENSION_LINE: "extension-line",
CONNECTION_MARK: "connection-mark",
SELECTION_MARK: "selection-mark",
LOCKED_SELECTION_MARK: "locked-selection-mark",
ITEMS_SELECTION_RECT: "items-selection-rect",
CONNECTOR_MULTI_SELECTION: "connector-multi-selection",
CONNECTOR_SELECTION: "connector-selection",
CONNECTOR_POINT_MARK: "connector-point-mark",
CONNECTOR_SELECTION_MASK: "connector-selection-mask",
CONNECTOR_SIDE_MARK: "connector-side-mark",
ITEM_SELECTION_RECT: "item-selection-rect",
ITEM_MULTI_SELECTION: "item-multi-selection-rect"
};
export const ACTIVE_SELECTION_CSSCLASS = "dxdi-active-selection";
export class CanvasSelectionManager extends CanvasManager implements IVisualizersListener, IMouseOperationsListener,
ITextInputOperationListener, ISelectionChangesListener, ICanvasViewListener, IReadOnlyChangesListener {
private itemSelectionContainer: SVGElement;
private selectionMarksContainer: SVGElement;
private visualizersContainer: SVGElement;
private selectionRectElement: SVGElement;
private resizeInfoElement: SVGElement;
private connectionPointElements: SVGElement[] = [];
private connectionTargetElement: SVGElement;
private containerTargetElement: SVGElement;
private extensionLineElements: SVGElement[] = [];
private parentContainer: SVGElement;
private selectionMap: { [shapeKey: string]: CanvasElement } = {};
static selectionMarkSize: number = UnitConverter.pixelsToTwips(10);
static lockedSelectionMarkSize: number = UnitConverter.pixelsToTwips(8);
static selectionOffset: number = UnitConverter.pixelsToTwips(2);
static selectionRectLineWidth: number = UnitConverter.pixelsToTwips(1);
static multiSelectionRectLineWidth: number = UnitConverter.pixelsToTwips(1);
static connectionPointSmallSize: number = UnitConverter.pixelsToTwips(5);
static connectionPointLargeSize: number = UnitConverter.pixelsToTwips(12);
static connectionPointShift: number = UnitConverter.pixelsToTwips(16);
static connectionTargetBorderWidth: number = UnitConverter.pixelsToTwips(2);
static geomertyMarkSize: number = UnitConverter.pixelsToTwips(8);
static connectorPointMarkSize: number = UnitConverter.pixelsToTwips(6);
static connectorSideMarkSize: number = UnitConverter.pixelsToTwips(6);
static extensionLineWidth: number = UnitConverter.pixelsToTwips(1);
static extensionLineOffset: number = UnitConverter.pixelsToTwips(1);
static extensionLineEndingSize: number = UnitConverter.pixelsToTwips(6);
static resizeInfoOffset: number = UnitConverter.pixelsToTwips(16);
static resizeInfoTextOffset: number = UnitConverter.pixelsToTwips(2);
static resizeInfoLineWidth: number = UnitConverter.pixelsToTwips(1);
static evenOddSelectionCorrection = UnitConverter.pixelsToTwips(1);
constructor(parent: SVGElement, zoomLevel: number, private readOnly: boolean, dom: DOMManipulator, instanceId: string) {
super(zoomLevel, dom, instanceId);
this.parentContainer = parent;
this.initializeContainerElements(parent);
}
private initializeContainerElements(parent: SVGElement) {
this.itemSelectionContainer = this.createAndChangePrimitiveElement(
new GroupPrimitive([], null), parent
);
this.visualizersContainer = this.createAndChangePrimitiveElement(
new GroupPrimitive([], null), parent
);
this.selectionMarksContainer = this.createAndChangePrimitiveElement(
new GroupPrimitive([], null), parent
);
}
clear() {
RenderUtils.removeContent(this.itemSelectionContainer);
RenderUtils.removeContent(this.selectionMarksContainer);
RenderUtils.removeContent(this.visualizersContainer);
this.selectionRectElement = undefined;
this.resizeInfoElement = undefined;
this.connectionPointElements = [];
this.connectionTargetElement = undefined;
this.containerTargetElement = undefined;
this.extensionLineElements = [];
this.selectionMap = {};
}
private showSelectionRect(rect: Rectangle) {
DomUtils.addClassName(this.parentContainer, ACTIVE_SELECTION_CSSCLASS);
const primitive = new RectanglePrimitive(rect.x, rect.y, rect.width, rect.height,
StrokeStyle.default1pxInstance, SELECTION_ELEMENT_CLASSNAMES.SELECTION_RECTANGLE);
const rectEl = this.getSelectionRectElement(primitive);
this.changePrimitiveElement(primitive, rectEl);
}
private hideSelectionRect() {
DomUtils.removeClassName(this.parentContainer, ACTIVE_SELECTION_CSSCLASS);
if(this.selectionRectElement !== undefined)
this.dom.changeByFunc(this.selectionRectElement, e => e.style.display = "none");
}
private getSelectionRectElement(primitive: SvgPrimitive<SVGElement>) {
if(this.selectionRectElement !== undefined)
this.dom.changeByFunc(this.selectionRectElement, e => e.style.display = "");
else
this.selectionRectElement = this.createPrimitiveElement(primitive, this.visualizersContainer);
return this.selectionRectElement;
}
showResizeInfo(point: Point, text: string) {
const rectPrimitive = new RectanglePrimitive(point.x, point.y, 0, 0, StrokeStyle.default1pxInstance);
const primitive = new GroupPrimitive([
rectPrimitive,
new TextPrimitive(point.x, point.y, text, TextOwner.Resize)
], "resize-info");
const groupEl = this.getResizeInfoElement(primitive);
this.changePrimitiveElement(primitive, groupEl);
const textSize = this.dom.measurer.measureTextLine(text, null, TextOwner.Resize).applyConverter(UnitConverter.pixelsToTwips);
rectPrimitive.width = textSize.width + CanvasSelectionManager.resizeInfoTextOffset * 2;
rectPrimitive.height = textSize.height + CanvasSelectionManager.resizeInfoTextOffset * 2;
rectPrimitive.x = point.x - textSize.width / 2 - CanvasSelectionManager.resizeInfoTextOffset;
rectPrimitive.y = point.y - textSize.height / 2 - CanvasSelectionManager.resizeInfoTextOffset;
this.changePrimitiveElement(primitive, groupEl);
}
hideResizeInfo() {
if(this.resizeInfoElement !== undefined)
this.dom.changeByFunc(this.resizeInfoElement, e => e.style.display = "none");
}
private getResizeInfoElement(primitive: SvgPrimitive<SVGElement>) {
if(this.resizeInfoElement !== undefined)
this.dom.changeByFunc(this.resizeInfoElement, e => e.style.display = "");
else
this.resizeInfoElement = this.createPrimitiveElement(primitive, this.visualizersContainer);
return this.resizeInfoElement;
}
getConnectionPointClassName(active: boolean, allowed: boolean): string {
let className = SELECTION_ELEMENT_CLASSNAMES.CONNECTION_POINT;
if(active)
className += " " + SELECTION_ELEMENT_CLASSNAMES.ACTIVE;
if(!allowed)
className += " " + NOT_VALID_CSSCLASS;
return className;
}
showConnectionPoint(index: number, point: Point, key: ItemKey, value: any, active: boolean, allowed: boolean): void {
this.showConnectionPointCore(index * 2, point.x, point.y,
CanvasSelectionManager.connectionPointLargeSize, CanvasSelectionManager.connectionPointLargeSize,
MouseEventElementType.ShapeConnectionPoint, key, value, SELECTION_ELEMENT_CLASSNAMES.CONNECTION_POINT + " selector" + (!allowed ? " " + NOT_VALID_CSSCLASS : ""));
this.showConnectionPointCore(index * 2 + 1, point.x, point.y,
CanvasSelectionManager.connectionPointSmallSize, CanvasSelectionManager.connectionPointSmallSize,
MouseEventElementType.ShapeConnectionPoint, key, value, this.getConnectionPointClassName(active, allowed));
}
showConnectionPointCore(index: number, cx: number, cy: number, rx: number, ry: number,
type: MouseEventElementType, key: ItemKey, value: any, className: string) {
const primitive = new EllipsePrimitive(cx, cy, rx, ry, null, className, e => RenderUtils.setElementEventData(e, type, key, value));
const ellEl = this.getConnectionPointElement(primitive, index);
this.changePrimitiveElement(primitive, ellEl);
}
private hideConnectionPoints() {
for(let i = 0; i < this.connectionPointElements.length; i++)
this.dom.changeByFunc(this.connectionPointElements[i], e => e.style.display = "none");
}
private getConnectionPointElement(primitive: SvgPrimitive<SVGElement>, index: number) {
let ellEl = this.connectionPointElements[index];
if(ellEl !== undefined)
this.dom.changeByFunc(ellEl, e => e.style.display = "");
else {
ellEl = this.createPrimitiveElement(primitive, this.visualizersContainer);
this.connectionPointElements[index] = ellEl;
}
return ellEl;
}
private showContainerTarget(index: number, targetRect: Rectangle) {
const primitive = new RectanglePrimitive(targetRect.x, targetRect.y, targetRect.width, targetRect.height, null, SELECTION_ELEMENT_CLASSNAMES.CONTAINER_TARGET);
const rectEl = this.getContainerTargetElement(primitive);
this.changePrimitiveElement(primitive, rectEl);
}
private hideContainerTarget() {
if(this.containerTargetElement)
this.dom.changeByFunc(this.containerTargetElement, e => e.style.display = "none");
}
private getContainerTargetElement(primitive: SvgPrimitive<SVGElement>) {
if(this.containerTargetElement !== undefined)
this.dom.changeByFunc(this.containerTargetElement, e => e.style.display = "");
else
this.containerTargetElement = this.createPrimitiveElement(primitive, this.itemSelectionContainer);
return this.containerTargetElement;
}
private showConnectionTarget(index: number, targetRect: Rectangle) {
const primitive = new RectanglePrimitive(targetRect.x, targetRect.y, targetRect.width, targetRect.height, null, SELECTION_ELEMENT_CLASSNAMES.CONNECTION_TARGET);
const rectEl = this.getConnectionTargetElement(primitive);
this.changePrimitiveElement(primitive, rectEl);
}
private hideConnectionTarget() {
if(this.connectionTargetElement)
this.dom.changeByFunc(this.connectionTargetElement, e => e.style.display = "none");
}
private getConnectionTargetElement(primitive: SvgPrimitive<SVGElement>) {
if(this.connectionTargetElement !== undefined)
this.dom.changeByFunc(this.connectionTargetElement, e => e.style.display = "");
else
this.connectionTargetElement = this.createPrimitiveElement(primitive, this.itemSelectionContainer);
return this.connectionTargetElement;
}
private showExtensionLine(index: number, type: ExtensionLineType, startPoint: Point, endPoint: Point, text: string) {
let className = SELECTION_ELEMENT_CLASSNAMES.EXTENSION_LINE;
if(type === ExtensionLineType.VerticalCenterAfter || type === ExtensionLineType.VerticalCenterBefore ||
type === ExtensionLineType.HorizontalCenterAbove || type === ExtensionLineType.HorizontalCenterBelow)
className += " center";
if(type === ExtensionLineType.VerticalCenterToPageCenter || type === ExtensionLineType.HorizontalCenterToPageCenter ||
type === ExtensionLineType.LeftToPageCenter || type === ExtensionLineType.RightToPageCenter ||
type === ExtensionLineType.TopToPageCenter || type === ExtensionLineType.BottomToPageCenter)
className += " page";
let x1_1 = 0; let y1_1 = 0; let x1_2 = 0; let y1_2 = 0; let x2_1 = 0; let y2_1 = 0; let x2_2 = 0; let y2_2 = 0;
if(startPoint.y === endPoint.y) {
x1_1 = startPoint.x - CanvasSelectionManager.extensionLineWidth;
y1_1 = startPoint.y - CanvasSelectionManager.extensionLineEndingSize;
x1_2 = startPoint.x - CanvasSelectionManager.extensionLineWidth;
y1_2 = startPoint.y + CanvasSelectionManager.extensionLineEndingSize;
x2_1 = endPoint.x - CanvasSelectionManager.extensionLineWidth;
y2_1 = startPoint.y - CanvasSelectionManager.extensionLineEndingSize;
x2_2 = endPoint.x - CanvasSelectionManager.extensionLineWidth;
y2_2 = startPoint.y + CanvasSelectionManager.extensionLineEndingSize;
}
else if(startPoint.x === endPoint.x) {
x1_1 = startPoint.x - CanvasSelectionManager.extensionLineEndingSize;
y1_1 = startPoint.y - CanvasSelectionManager.extensionLineWidth;
x1_2 = startPoint.x + CanvasSelectionManager.extensionLineEndingSize;
y1_2 = startPoint.y - CanvasSelectionManager.extensionLineWidth;
x2_1 = startPoint.x - CanvasSelectionManager.extensionLineEndingSize;
y2_1 = endPoint.y - CanvasSelectionManager.extensionLineWidth;
x2_2 = startPoint.x + CanvasSelectionManager.extensionLineEndingSize;
y2_2 = endPoint.y - CanvasSelectionManager.extensionLineWidth;
}
let sizeLineXCorr = 0; let sizeLineYCorr = 0;
if(type === ExtensionLineType.RightToRightAbove || type === ExtensionLineType.RightToRightBelow)
sizeLineXCorr -= CanvasSelectionManager.extensionLineWidth;
if(type === ExtensionLineType.BottomToBottomAfter || type === ExtensionLineType.BottomToBottomBefore)
sizeLineYCorr -= CanvasSelectionManager.extensionLineWidth;
const children = [
new PathPrimitive([
PathPrimitiveMoveToCommand.fromPoint(startPoint.clone().offset(sizeLineXCorr, sizeLineYCorr)),
PathPrimitiveLineToCommand.fromPoint(endPoint.clone().offset(sizeLineXCorr, sizeLineYCorr))
], StrokeStyle.default1pxInstance, "size-line"),
new PathPrimitive([
new PathPrimitiveMoveToCommand(x1_1, y1_1),
new PathPrimitiveLineToCommand(x1_2, y1_2),
new PathPrimitiveMoveToCommand(x2_1, y2_1),
new PathPrimitiveLineToCommand(x2_2, y2_2)
], StrokeStyle.default1pxInstance),
new TextPrimitive(
(endPoint.x + startPoint.x) / 2,
(endPoint.y + startPoint.y) / 2,
text, TextOwner.ExtensionLine, undefined, undefined, undefined, null, undefined, null,
PAGE_BG_TEXTFLOOR_FILTER_IDPREFIX + this.instanceId
)
];
const primitive = new GroupPrimitive(children, className);
const ellEl = this.getExtensionLineElement(primitive, index);
this.changePrimitiveElement(primitive, ellEl);
}
private hideExtensionLines() {
for(let i = 0; i < this.extensionLineElements.length; i++)
if(this.extensionLineElements[i])
this.dom.changeByFunc(this.extensionLineElements[i], e => e.style.display = "none");
}
private getExtensionLineElement(primitive: SvgPrimitive<SVGElement>, index: number) {
let ellEl = this.extensionLineElements[index];
if(ellEl !== undefined)
this.dom.changeByFunc(ellEl, e => e.style.display = "");
else {
ellEl = this.createPrimitiveElement(primitive, this.visualizersContainer);
this.extensionLineElements[index] = (ellEl);
}
return ellEl;
}
protected getOrCreateShapeSelection(shape: Shape, usedItems?: ItemsMap): ShapeSelectionElement {
let element = <ShapeSelectionElement> this.selectionMap[shape.key];
if(!element) {
element = new ShapeSelectionElement(this.itemSelectionContainer, this.selectionMarksContainer,
this.actualZoom, this.readOnly, this.dom, shape.key, shape.isLocked, shape.rectangle, shape.style,
shape.allowResizeHorizontally, shape.allowResizeVertically, shape.description.getParameterPoints(shape));
this.selectionMap[shape.key] = element;
}
usedItems && (usedItems[shape.key] = true);
return element;
}
protected getOrCreateConnectorSelection(connector: Connector, usedItems?: ItemsMap): ConnectorSelectionElement {
let element = <ConnectorSelectionElement> this.selectionMap[connector.key];
const points = connector.getRenderPoints(true);
const pointsNonSkipped = connector.getRenderPoints(false);
if(!element) {
element = new ConnectorSelectionElement(this.itemSelectionContainer, this.selectionMarksContainer, this.actualZoom,
this.readOnly, this.dom, connector.key, connector.isLocked, connector.rectangle, points,
connector.style, connector.styleText, connector.enableText,
connector.texts.map(t => {
const textInfo = GeometryUtils.getPathPointByPosition(pointsNonSkipped, t.position);
return {
text: connector.getText(t.position),
point: textInfo[0],
pointIndex: textInfo[1],
pos: t.position
};
}).sort((a, b) => a.pos - b.pos), connector.points, connector.properties.lineOption);
this.selectionMap[connector.key] = element;
}
usedItems && (usedItems[connector.key] = true);
return element;
}
protected getOrCreateMultipleSelection(usedItems: ItemsMap) {
let element = <MultipleSelectionElement> this.selectionMap[MULTIPLE_SELECTION_KEY];
if(!element) {
element = new MultipleSelectionElement(this.itemSelectionContainer, this.selectionMarksContainer, this.actualZoom, this.readOnly, this.dom);
this.selectionMap[MULTIPLE_SELECTION_KEY] = element;
}
usedItems[MULTIPLE_SELECTION_KEY] = true;
return element;
}
protected getMultipleSelection(): MultipleSelectionElement {
return <MultipleSelectionElement> this.selectionMap[MULTIPLE_SELECTION_KEY];
}
protected updateShapeSelection(shape: Shape, multipleSelection: MultipleSelectionElement) {
if(shape.key in this.selectionMap) {
this.getOrCreateShapeSelection(shape).onModelChanged(shape.isLocked, shape.rectangle, shape.style,
shape.allowResizeHorizontally, shape.allowResizeVertically, shape.description.getParameterPoints(shape));
multipleSelection && multipleSelection.onModelItemChanged(shape.key, shape.rectangle);
}
}
protected updateConnectorSelection(connector: Connector, multipleSelection: MultipleSelectionElement) {
if(connector.key in this.selectionMap) {
const renderPoints = connector.getRenderPoints(true);
const renderPointsNonSkipped = connector.getRenderPoints(false);
this.getOrCreateConnectorSelection(connector)
.onModelChanged(connector.isLocked, connector.rectangle, renderPoints,
connector.style, connector.styleText, connector.enableText,
connector.texts.map(t => {
const textPos = GeometryUtils.getPathPointByPosition(renderPointsNonSkipped, t.position);
return {
text: connector.getText(t.position),
pointIndex: textPos[1],
pos: t.position,
point: textPos[0]
};
}).sort((a, b) => a.pos - b.pos), connector.points, connector.properties.lineOption);
multipleSelection && multipleSelection.onModelItemChanged(connector.key, connector.rectangle);
}
}
private hideOutdatedSelection(updated: ItemsMap) {
Object.keys(this.selectionMap)
.filter(k => !updated[k])
.forEach(k => {
this.selectionMap[k].destroy();
delete this.selectionMap[k];
});
}
private selectionCanBeDrawn(item: DiagramItem) {
return !item.container || (item.container.expanded && this.selectionCanBeDrawn(item.container));
}
notifySelectionChanged(selection: Selection) {
const items = selection.getSelectedItems(true).filter(item => this.selectionCanBeDrawn(item));
const changedItems: ItemsMap = {};
const isMultipleSelection = items.length > 1;
const shapes = selection.getSelectedShapes(true).filter(shape => this.selectionCanBeDrawn(shape));
const connectors = selection.getSelectedConnectors(true).filter(connector => this.selectionCanBeDrawn(connector));
shapes.forEach(shape => this.getOrCreateShapeSelection(shape, changedItems).onSelectionChanged(isMultipleSelection));
connectors.forEach(connector => this.getOrCreateConnectorSelection(connector, changedItems).onSelectionChanged(isMultipleSelection));
if(isMultipleSelection) {
const strokeWidth = items.length > 0 ? items[0].strokeWidth : 0;
const rectangles = {};
items.filter(i => !i.isLocked).forEach(item => rectangles[item.key] = item.rectangle);
this.getOrCreateMultipleSelection(changedItems).onSelectionChanged(!!shapes.filter(i => !i.isLocked).length, strokeWidth, rectangles);
}
this.hideOutdatedSelection(changedItems);
}
applyChangesCore(changes: ItemChange[]) {
super.applyChangesCore(changes);
const multipleSelection = this.getMultipleSelection();
multipleSelection && multipleSelection.onModelChanged();
}
applyChange(change: ItemChange) {
const multipleSelection = this.getMultipleSelection();
if(change.item instanceof Shape)
this.updateShapeSelection(change.item, multipleSelection);
else if(change.item instanceof Connector)
this.updateConnectorSelection(change.item, multipleSelection);
}
notifyPageColorChanged(color: number) { }
notifyPageSizeChanged(pageSize: Size, pageLandscape: boolean) { }
notifyActualZoomChanged(actualZoom: number) {
Object.keys(this.selectionMap).forEach(k => this.selectionMap[k].notifyZoomChanged(actualZoom));
this.actualZoom = actualZoom;
}
notifyViewAdjusted(canvasOffset: Point) { }
notifyReadOnlyChanged(readOnly: boolean) {
this.readOnly = readOnly;
Object.keys(this.selectionMap).forEach(k => this.selectionMap[k].notifyReadOnlyChanged(readOnly));
}
notifySelectionRectShow(rect: Rectangle) {
this.showSelectionRect(rect.clone().multiply(this.actualZoom, this.actualZoom));
}
notifySelectionRectHide() {
this.hideSelectionRect();
}
notifyResizeInfoShow(point: Point, text: string) {
this.showResizeInfo(point.clone().multiply(this.actualZoom, this.actualZoom), text);
}
notifyResizeInfoHide() {
this.hideResizeInfo();
}
notifyConnectionPointsShow(key: ItemKey, points: ConnectionPointInfo[], activePointIndex: number, outsideRectangle: Rectangle) {
this.hideConnectionPoints();
points.forEach((p, index) => {
const point = p.point.clone().multiply(this.actualZoom, this.actualZoom);
if(outsideRectangle)
switch(p.side) {
case ConnectionPointSide.North:
point.y = outsideRectangle.y * this.actualZoom - CanvasSelectionManager.connectionPointShift;
break;
case ConnectionPointSide.South:
point.y = outsideRectangle.bottom * this.actualZoom + CanvasSelectionManager.connectionPointShift;
break;
case ConnectionPointSide.West:
point.x = outsideRectangle.x * this.actualZoom - CanvasSelectionManager.connectionPointShift;
break;
case ConnectionPointSide.East:
point.x = outsideRectangle.right * this.actualZoom + CanvasSelectionManager.connectionPointShift;
break;
}
this.showConnectionPoint(index, point, key, index, index === activePointIndex, p.allowed);
});
}
notifyConnectionPointsHide() {
this.hideConnectionPoints();
}
notifyConnectionTargetShow(key: ItemKey, info: ConnectionTargetInfo) {
if(!info.allowed) return;
const rect = CanvasSelectionManager.correctSelectionRect(info.rect.clone().multiply(this.actualZoom, this.actualZoom), info.strokeWidth,
CanvasSelectionManager.connectionTargetBorderWidth, this.actualZoom, 0);
this.showConnectionTarget(0, rect);
}
notifyConnectionTargetHide() {
this.hideConnectionTarget();
}
notifyContainerTargetShow(key: ItemKey, info: ContainerTargetInfo) {
const rect = CanvasSelectionManager.correctSelectionRect(info.rect.clone().multiply(this.actualZoom, this.actualZoom), info.strokeWidth,
CanvasSelectionManager.connectionTargetBorderWidth, this.actualZoom, 0);
this.showContainerTarget(0, rect);
}
notifyContainerTargetHide() {
this.hideContainerTarget();
}
notifyExtensionLinesShow(lines: ExtensionLine[]) {
this.hideExtensionLines();
lines.forEach((line, index) => {
this.showExtensionLine(index, line.type,
line.segment.startPoint.clone().multiply(this.actualZoom, this.actualZoom),
line.segment.endPoint.clone().multiply(this.actualZoom, this.actualZoom),
line.text
);
});
}
notifyExtensionLinesHide() {
this.hideExtensionLines();
}
notifyDragStart(itemKeys: ItemKey[]) {
this.dom.changeByFunc(this.selectionMarksContainer, e => e.style.display = "none");
}
notifyDragEnd(itemKeys: ItemKey[]) {
this.dom.changeByFunc(this.selectionMarksContainer, e => e.style.display = "");
}
notifyDragScrollStart() { }
notifyDragScrollEnd() { }
notifyTextInputStart(item: DiagramItem, text: string, position: Point, size?: Size): void {
this.dom.changeByFunc(this.visualizersContainer, e => e.style.display = "none");
}
notifyTextInputEnd(item: DiagramItem, captureFocus?: boolean): void {
this.dom.changeByFunc(this.visualizersContainer, e => e.style.display = "");
}
notifyTextInputPermissionsCheck(item: DiagramItem, allowed: boolean): void {}
static correctSelectionRect(rect: Rectangle, targetLineWidth: number, selectionLineWidth: number,
zoomLevel: number, outsideOffset: number = CanvasSelectionManager.selectionOffset): Rectangle {
const evenOddWidth = UnitConverter.twipsToPixels(targetLineWidth) % 2 !== UnitConverter.twipsToPixels(selectionLineWidth) % 2;
const corr = Math.ceil(targetLineWidth / 2 * zoomLevel);
rect = rect.clone().inflate(corr, corr);
const lwCorr = Math.floor(selectionLineWidth / 2);
rect.x -= lwCorr;
rect.y -= lwCorr;
rect.width += selectionLineWidth;
rect.height += selectionLineWidth;
if(evenOddWidth) {
const correction = CanvasSelectionManager.evenOddSelectionCorrection * (UnitConverter.twipsToPixels(selectionLineWidth) % 2 === 1 ? -1 : 1);
rect = rect.clone().moveRectangle(correction, correction);
}
return rect.clone().inflate(outsideOffset, outsideOffset);
}
}
abstract class CanvasElement implements IZoomChangesListener, IReadOnlyChangesListener {
private elements: { [key: string]: SVGElement } = {};
private updatedElements: { [key: string]: boolean } = {};
constructor(
protected rectsContainer: SVGElement,
protected marksContainer: SVGElement,
protected key: ItemKey,
protected zoomLevel: number,
protected readOnly: boolean,
protected dom: DOMManipulator) { }
notifyZoomChanged(zoom: number) {
if(this.zoomLevel !== zoom) {
this.zoomLevel = zoom;
this.redraw();
}
}
notifyReadOnlyChanged(readOnly: boolean) {
this.readOnly = readOnly;
this.redraw();
}
destroy() {
Object.keys(this.elements)
.forEach(key => {
this.elements[key].parentNode.removeChild(this.elements[key]);
delete this.elements[key];
});
}
protected redraw() {
this.updatedElements = {};
this.redrawCore();
Object.keys(this.elements)
.filter(key => !this.updatedElements[key])
.forEach(key => {
this.elements[key].parentNode.removeChild(this.elements[key]);
delete this.elements[key];
});
this.updatedElements = {};
}
protected abstract redrawCore();
protected drawSelectionMarks(rect: Rectangle, allowResizeHorizontally: boolean, allowResizeVertically: boolean) {
if(this.readOnly) return;
const hasEWMarks = allowResizeHorizontally && rect.height > CanvasSelectionManager.selectionMarkSize * 3;
const hasNSMarks = allowResizeVertically && rect.width > CanvasSelectionManager.selectionMarkSize * 3;
const hasCornerMarks = allowResizeHorizontally || allowResizeVertically;
if(hasCornerMarks)
this.drawSelectionMark(0, new Point(rect.x, rect.y), CanvasSelectionManager.selectionMarkSize,
MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_NW, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK);
if(hasNSMarks && !Browser.TouchUI)
this.drawSelectionMark(1, new Point(rect.x + rect.width / 2, rect.y), CanvasSelectionManager.selectionMarkSize,
MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_N, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK);
if(hasCornerMarks)
this.drawSelectionMark(2, new Point(rect.right, rect.y), CanvasSelectionManager.selectionMarkSize,
MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_NE, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK);
if(hasEWMarks && !Browser.TouchUI)
this.drawSelectionMark(3, new Point(rect.right, rect.y + rect.height / 2), CanvasSelectionManager.selectionMarkSize,
MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_E, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK);
if(hasCornerMarks)
this.drawSelectionMark(4, new Point(rect.right, rect.bottom), CanvasSelectionManager.selectionMarkSize,
MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_SE, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK);
if(hasNSMarks && !Browser.TouchUI)
this.drawSelectionMark(5, new Point(rect.x + rect.width / 2, rect.bottom), CanvasSelectionManager.selectionMarkSize,
MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_S, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK);
if(hasCornerMarks)
this.drawSelectionMark(6, new Point(rect.x, rect.bottom), CanvasSelectionManager.selectionMarkSize,
MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_SW, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK);
if(hasEWMarks && !Browser.TouchUI)
this.drawSelectionMark(7, new Point(rect.x, rect.y + rect.height / 2), CanvasSelectionManager.selectionMarkSize,
MouseEventElementType.ShapeResizeBox, ResizeEventSource.ResizeBox_W, SELECTION_ELEMENT_CLASSNAMES.SELECTION_MARK);
}
protected drawSelectionMark(index: number, point: Point, size: number, type: MouseEventElementType, value: any, className: string) {
this.getOrCreateElement("SM" + index, new RectanglePrimitive(point.x - size / 2, point.y - size / 2, size, size, null, className, undefined, el => {
RenderUtils.setElementEventData(el, type, this.key, value);
}), this.marksContainer);
}
protected drawSelectionRect(rectangle: Rectangle, type: MouseEventElementType, className: string) {
const primitive = new RectanglePrimitive(rectangle.x, rectangle.y, rectangle.width, rectangle.height,
StrokeStyle.default1pxInstance, className, undefined, el => {
RenderUtils.setElementEventData(el, type, "-1", -1);
}
);
this.getOrCreateElement("shapeSelection", primitive, this.rectsContainer);
}
protected getOrCreateElement(cacheKey: string, primitive: SvgPrimitive<SVGElement>, container: SVGElement) {
let element = this.elements[cacheKey];
if(!element) {
element = primitive.createElement(el => container.appendChild(el));
this.elements[cacheKey] = element;
}
this.updatedElements[cacheKey] = true;
this.dom.changeByPrimitive(element, primitive);
return element;
}
}
abstract class ItemSelectionElement extends CanvasElement {
protected isMultipleSelection: boolean;
constructor(rectsContainer: SVGElement, marksContainer: SVGElement, key: ItemKey,
zoomLevel: number, readOnly: boolean, dom: DOMManipulator,
protected isLocked: boolean,
protected rectangle: Rectangle) {
super(rectsContainer, marksContainer, key, zoomLevel, readOnly, dom);
}
onSelectionChanged(isMultipleSelection: boolean) {
if(this.isMultipleSelection !== isMultipleSelection) {
this.isMultipleSelection = isMultipleSelection;
this.redraw();
}
}
protected isLockedRender() {
return this.isLocked && !this.readOnly;
}
protected drawLockedSelectionMark(index: number, point: Point, size: number, className: string) {
const primitive = new PathPrimitive([
new PathPrimitiveMoveToCommand(point.x - size / 2, point.y - size / 2),
new PathPrimitiveLineToCommand(point.x + size / 2, point.y + size / 2),
new PathPrimitiveMoveToCommand(point.x + size / 2, point.y - size / 2),
new PathPrimitiveLineToCommand(point.x - size / 2, point.y + size / 2)
], null, className);
this.getOrCreateElement("LSM" + index, primitive, this.marksContainer);
}
}
class MultipleSelectionElement extends CanvasElement {
private needDrawSelectionMarks: boolean;
private strokeWidth: number;
private rectangles: {[key: string]: Rectangle} = {};
constructor(rectsContainer: SVGElement, marksContainer: SVGElement, zoomLevel: number, readOnly: boolean, dom: DOMManipulator) {
super(rectsContainer, marksContainer, "-1", zoomLevel, readOnly, dom);
}
onModelItemChanged(key: ItemKey, rectangle: Rectangle) {
if(key in this.rectangles)
this.rectangles[key] = rectangle;
}
onModelChanged() {
this.redraw();
}
onSelectionChanged(needDrawSelectionMarks: boolean, strokeWidth: number, rectangles: {[key: string]: Rectangle}) {
this.needDrawSelectionMarks = needDrawSelectionMarks;
this.strokeWidth = strokeWidth;
this.rectangles = rectangles;
this.redraw();
}
protected redrawCore() {
const rectKeys = Object.keys(this.rectangles);
if(!rectKeys.length) return;
const rect = GeometryUtils.getCommonRectangle(rectKeys.map(key => this.rectangles[key])).clone().multiply(this.zoomLevel, this.zoomLevel);
const selRect = CanvasSelectionManager.correctSelectionRect(rect,
this.strokeWidth, CanvasSelectionManager.selectionRectLineWidth, this.zoomLevel);
this.drawSelectionRect(selRect, MouseEventElementType.SelectionRect, SELECTION_ELEMENT_CLASSNAMES.ITEMS_SELECTION_RECT);
if(this.needDrawSelectionMarks)
this.drawSelectionMarks(rect, true, true);
}
}
class ShapeSelectionElement extends ItemSelectionElement {
constructor(rectsContainer: SVGElement, marksContainer: SVGElement, zoomLevel: number, readOnly: boolean,
dom: DOMManipulator, key: ItemKey, isLocked: boolean, rectangle: Rectangle,
protected style: Style,
protected allowResizeHorizontally: boolean,
protected allowResizeVertically: boolean,
protected shapeParameterPoints: ShapeParameterPoint[]) {
super(rectsContainer, marksContainer, key, zoomLevel, readOnly, dom, isLocked, rectangle);
}
onModelChanged(isLocked: boolean, rectangle: Rectangle, style: Style,
allowResizeHorizontally: boolean, allowResizeVertically: boolean, shapeParameterPoints: ShapeParameterPoint[]) {
this.isLocked = isLocked;
this.rectangle = rectangle;
this.style = style;
this.allowResizeHorizontally = allowResizeHorizontally;
this.allowResizeVertically = allowResizeVertically;
this.shapeParameterPoints = shapeParameterPoints;
this.redraw();
}
protected redrawCore() {
const rect = this.rectangle.clone().multiply(this.zoomLevel, this.zoomLevel);
if(this.isLockedRender())
this.drawLockedSelection(rect);
else
this.drawUnlockedSelection(rect);
}
private drawLockedSelection(rect: Rectangle) {
this.drawLockedSelectionMark(0, new Point(rect.x, rect.y),
CanvasSelectionManager.lockedSelectionMarkSize, SELECTION_ELEMENT_CLASSNAMES.LOCKED_SELECTION_MARK);
this.drawLockedSelectionMark(1, new Point(rect.right, rect.y),
CanvasSelectionManager.lockedSelectionMarkSize, SELECTION_ELEMENT_CLASSNAMES.LOCKED_SELECTION_MARK);
this.drawLockedSelectionMark(2, new Point(rect.right, rect.bottom),
CanvasSelectionManager.lockedSelectionMarkSize, SELECTION_ELEMENT_CLASSNAMES.LOCKED_SELECTION_MARK);
this.drawLockedSelectionMark(3, new Point(rect.x, rect.bottom),
CanvasSelectionManager.lockedSelectionMarkSize, SELECTION_ELEMENT_CLASSNAMES.LOCKED_SELECTION_MARK);
}
private drawUnlockedSelection(rect: Rectangle) {
const selRect = CanvasSelectionManager.correctSelectionRect(rect, this.style.strokeWidth,
CanvasSelectionManager.selectionRectLineWidth, this.zoomLevel);
this.drawSelectionRect(selRect, MouseEventElementType.SelectionRect, this.isMultipleSelection ? SELECTION_ELEMENT_CLASSNAMES.ITEM_MULTI_SELECTION : SELECTION_ELEMENT_CLASSNAMES.ITEM_SELECTION_RECT);
if(!this.isMultipleSelection)
this.drawSelectionMarks(rect, this.allowResizeHorizontally, this.allowResizeVertically);
this.drawShapeParameterPoints();
}
private drawShapeParameterPoints() {
if(this.readOnly) return;
this.shapeParameterPoints.forEach((pp, index) => {
const point = pp.point.clone().multiply(this.zoomLevel, this.zoomLevel);
this.drawShapeParameterPoint(point, index, pp.key);
});
}
private drawShapeParameterPoint(point: Point, index: number, pointKey: string) {
const size = CanvasSelectionManager.geomertyMarkSize;
const primitive = new RectanglePrimitive(point.x - size / 2, point.y - size / 2,
size, size, null, "geometry-mark", undefined, el => {
RenderUtils.setElementEventData(el, MouseEventElementType.ShapeParameterBox, this.key, pointKey);
});
this.getOrCreateElement("pp" + index.toString(), primitive, this.marksContainer);
}
}
class ConnectorSelectionElement extends ItemSelectionElement {
constructor(rectsContainer: SVGElement, marksContainer: SVGElement, zoomLevel: number, readOnly: boolean,
dom: DOMManipulator, key: string, isLocked: boolean, rectangle: Rectangle,
protected renderPoints: ConnectorRenderPoint[],
protected style: Style,
protected styleText: TextStyle,
protected enableText: boolean,
protected texts: {text: string, point: Point, pointIndex: number}[],
protected points: Point[],
protected lineType: ConnectorLineOption) {
super(rectsContainer, marksContainer, key, zoomLevel, readOnly, dom, isLocked, rectangle);
}
onModelChanged(isLocked: boolean, rectangle: Rectangle,
renderPoints: ConnectorRenderPoint[],
style: Style, styleText: TextStyle, enableText: boolean,
texts: {text: string, point: Point, pointIndex: number}[], points: Point[], lineType: ConnectorLineOption) {
this.isLocked = isLocked;
this.rectangle = rectangle;
this.renderPoints = renderPoints;
this.style = style;
this.styleText = styleText;
this.enableText = enableText;
this.texts = texts;
this.points = points;
this.lineType = lineType;
this.redraw();
}
protected redrawCore() {
if(this.isLockedRender())
this.drawLockedSelection();
else
this.drawUnlockedSelection();
}
protected drawLockedSelection() {
this.renderPoints.forEach((pt, index) => {
this.drawLockedSelectionMark(index, pt,
CanvasSelectionManager.lockedSelectionMarkSize, SELECTION_ELEMENT_CLASSNAMES.LOCKED_SELECTION_MARK);
});
}
protected drawUnlockedSelection() {
this.drawConnectorSelection();
if(!this.isMultipleSelection && !this.readOnly)
this.drawConnectorSelectionMarks();
}
private drawConnectorSelection() {
const commands: PathPrimitiveCommand[] = [];
const commandsWB: PathPrimitiveCommand[] = [];
const className = this.isMultipleSelection ? SELECTION_ELEMENT_CLASSNAMES.CONNECTOR_MULTI_SELECTION : SELECTION_ELEMENT_CLASSNAMES.CONNECTOR_SELECTION;
this.populateSelectionPrimitiveCommands(commands, commandsWB);
const primitive = new PathPrimitive(commands.concat(commandsWB.reverse()), StrokeStyle.default1pxInstance, className);
this.getOrCreateElement("CS", primitive, this.rectsContainer);
}
populateSelectionPrimitiveCommands(commands: PathPrimitiveCommand[], commandsWB: PathPrimitiveCommand[]) {
const texts = this.texts;
const txtAlign = this.styleText.getAlignment();
const points = this.createNotSkippedRenderPoints();
const zoomLevel = this.zoomLevel;
const strokeWidthPx = this.style.strokeWidthPx;
const selectionOffset = this.getSelectionOffset(strokeWidthPx);
const strokeWidthPxIsEvenOdd = strokeWidthPx % 2 === 0;
let prevPt: Point = points[0];
let textIndex = 0;
let offset: Point;
let distance: number;
let nextOffset: Point;
let nextDistance: number;
for(let i = 1, pt: ConnectorRenderPoint; pt = points[i]; i++) {
const nextPt: Point = points[i + 1];
if(offset === undefined) {
distance = Metrics.euclideanDistance(prevPt, pt);
if(MathUtils.numberCloseTo(distance, 0))
continue;
offset = GeometryUtils.getSelectionOffsetPoint(prevPt, pt, distance).multiply(selectionOffset, selectionOffset);
}
if(nextPt) {
nextDistance = Metrics.euclideanDistance(pt, nextPt);
if(MathUtils.numberCloseTo(nextDistance, 0))
continue;
nextOffset = GeometryUtils.getSelectionOffsetPoint(pt, nextPt, nextDistance).multiply(selectionOffset, selectionOffset);
}
let offsetX = offset.x;
let offsetY = offset.y;
let offsetXNegative = -offsetX;
let offsetYNegative = -offsetY;
let nextOffsetX = nextOffset && nextOffset.x;
let nextOffsetY = nextOffset && nextOffset.y;
let nextOffsetXNegative = nextOffset && -nextOffset.x;
let nextOffsetYNegative = nextOffset && -nextOffset.y;
if(strokeWidthPxIsEvenOdd) {
if(offsetXNegative > 0)
offsetXNegative -= CanvasSelectionManager.evenOddSelectionCorrection;
else if(offsetX > 0)
offsetX -= CanvasSelectionManager.evenOddSelectionCorrection;
if(offsetYNegative > 0)
offsetYNegative -= CanvasSelectionManager.evenOddSelectionCorrection;
else if(offsetY > 0)
offsetY -= CanvasSelectionManager.evenOddSelectionCorrection;
if(nextOffsetXNegative > 0)
nextOffsetXNegative -= CanvasSelectionManager.evenOddSelectionCorrection;
else if(nextOffsetX > 0)
nextOffsetX -= CanvasSelectionManager.evenOddSelectionCorrection;
if(nextOffsetYNegative > 0)
nextOffsetYNegative -= CanvasSelectionManager.evenOddSelectionCorrection;
else if(nextOffsetY > 0)
nextOffsetY -= CanvasSelectionManager.evenOddSelectionCorrection;
}
while(texts[textIndex] && texts[textIndex].pointIndex <= i) {
const text = texts[textIndex];
const size = this.getConnectorSelectionTextSize(text.text, selectionOffset);
const textPts = GeometryUtils.getSelectionTextStartEndPoints(prevPt, pt, distance, text.point, size, txtAlign);
if(texts[textIndex].pointIndex < i) {
prevPt = textPts[1];
commands.push(PathPrimitiveMoveToCommand.fromPoint(prevPt.clone().offset(offsetX, offsetY).multiply(zoomLevel, zoomLevel)));
commandsWB.push(PathPrimitiveLineToCommand.fromPoint(prevPt.clone().offset(offsetXNegative, offsetYNegative).multiply(zoomLevel, zoomLevel)));
}
else {
if(!commands.length) {
commands.push(PathPrimitiveMoveToCommand.fromPoint(prevPt.clone().offset(offsetX, offsetY).multiply(zoomLevel, zoomLevel)));
commandsWB.push(PathPrimitiveLineToCommand.fromPoint(prevPt.clone().offset(offsetXNegative, offsetYNegative).