From f76a172caef96405bc4bfdbc7c55f349d8e608cb Mon Sep 17 00:00:00 2001 From: mcyph <20507948+mcyph@users.noreply.github.com> Date: Sun, 6 Jun 2021 23:04:44 +1000 Subject: [PATCH] started refactoring+reorganising core to not have mx prefix, and breaking up mxGraph into smaller classes for easier maintenance --- packages/core/src/view/Graph.ts | 2304 ++--------------- packages/core/src/view/GraphHandler.js | 2 +- packages/core/src/view/cell/CellMarker.ts | 20 +- packages/core/src/view/cell/CellRenderer.ts | 81 +- .../src/view/cell/{Cells.ts => GraphCells.ts} | 267 +- packages/core/src/view/cell/datatypes/Cell.ts | 2 +- .../core/src/view/cell/datatypes/CellState.ts | 101 + packages/core/src/view/cell/edge/Edge.ts | 60 + packages/core/src/view/cell/vertex/Vertex.ts | 6 + .../{Connections.ts => GraphConnections.ts} | 124 +- .../core/src/view/drag_drop/GraphDragDrop.ts | 39 +- .../core/src/view/editing/InPlaceEditing.ts | 88 + packages/core/src/view/event/GraphEvents.ts | 36 +- .../core/src/view/folding/GraphFolding.ts | 9 + .../view/grouping_ordering/GraphGrouping.ts | 58 + packages/core/src/view/label/GraphLabel.ts | 163 ++ .../src/view/page_breaks/GraphPageBreaks.ts | 119 + .../core/src/view/panning/GraphPanning.ts | 15 + packages/core/src/view/ports/GraphPorts.ts | 83 + packages/core/src/view/selection/Selection.ts | 142 +- packages/core/src/view/snap/GraphSnap.ts | 146 ++ packages/core/src/view/split/GraphSplit.ts | 36 + packages/core/src/view/swimlane/Swimlane.ts | 283 ++ .../core/src/view/tooltip/GraphTooltip.ts | 127 + packages/core/src/view/zoom/GraphZoom.ts | 217 ++ packages/html/stories/Animation.stories.js | 11 + packages/html/stories/Clipboard.stories.js | 2 +- packages/html/stories/Collapse.stories.js | 2 +- packages/html/stories/Drop.stories.js | 4 +- packages/html/stories/Events.stories.js | 6 +- packages/html/stories/FileIO.stories.js | 16 +- packages/html/stories/FixedPoints.stories.js | 2 +- packages/html/stories/HelloPort.stories.js | 2 +- packages/html/stories/Monitor.stories.js | 6 +- packages/html/stories/OrgChart.stories.js | 8 +- packages/html/stories/PageBreaks.stories.js | 6 +- 36 files changed, 2356 insertions(+), 2237 deletions(-) rename packages/core/src/view/cell/{Cells.ts => GraphCells.ts} (92%) rename packages/core/src/view/connection/{Connections.ts => GraphConnections.ts} (83%) create mode 100644 packages/core/src/view/label/GraphLabel.ts create mode 100644 packages/core/src/view/page_breaks/GraphPageBreaks.ts create mode 100644 packages/core/src/view/ports/GraphPorts.ts create mode 100644 packages/core/src/view/snap/GraphSnap.ts create mode 100644 packages/core/src/view/split/GraphSplit.ts create mode 100644 packages/core/src/view/tooltip/GraphTooltip.ts create mode 100644 packages/core/src/view/zoom/GraphZoom.ts diff --git a/packages/core/src/view/Graph.ts b/packages/core/src/view/Graph.ts index 0d36a9b48..d3a7f6911 100644 --- a/packages/core/src/view/Graph.ts +++ b/packages/core/src/view/Graph.ts @@ -22,10 +22,12 @@ import GraphView from './view/GraphView'; import CellRenderer from './cell/CellRenderer'; import CellEditor from './editing/CellEditor'; import Point from './geometry/Point'; -import utils, {contains, convertPoint, - getBoundingBox, +import utils, { + contains, convertPoint, + getBoundingBox, getCurrentStyle, getRotatedPoint, - getValue, hasScrollbars, intersects, ptSegDistSq, toRadians} from '../util/Utils'; + getValue, hasScrollbars, intersects, parseCssNumber, ptSegDistSq, toRadians +} from '../util/Utils'; import mxDictionary from '../util/mxDictionary'; import InternalMouseEvent from './event/InternalMouseEvent'; import Resources from '../util/Resources'; @@ -62,6 +64,11 @@ import { } from '../util/EventUtils'; import { isNode } from '../util/DomUtils'; import CellArray from "./cell/datatypes/CellArray"; +import EdgeStyle from "./style/EdgeStyle"; +import mxEdgeHandler from "./cell/edge/mxEdgeHandler"; +import mxVertexHandler from "./cell/vertex/mxVertexHandler"; +import mxEdgeSegmentHandler from "./cell/edge/mxEdgeSegmentHandler"; +import mxElbowEdgeHandler from "./cell/edge/mxElbowEdgeHandler"; /** * Extends {@link EventSource} to implement a graph component for @@ -117,7 +124,7 @@ class Graph extends EventSource { this.init(container); } - this.getView().revalidate(); + this.view.revalidate(); } /** @@ -132,7 +139,7 @@ class Graph extends EventSource { this.cellEditor = this.createCellEditor(); // Initializes the container using the view - this.getView().init(); + this.view.init(); // Updates the size of the container for the current graph this.sizeDidChange(); @@ -160,8 +167,6 @@ class Graph extends EventSource { connectionHandler: mxConnectionHandler | null = null; graphHandler: GraphHandler | null = null; graphModelChangeListener: Function | null = null; - horizontalPageBreaks: any[] | null = null; - verticalPageBreaks: any[] | null = null; paintBackground: Function | null = null; /***************************************************************************** @@ -221,18 +226,6 @@ class Graph extends EventSource { */ dialect: 'svg' | 'mixedHtml' | 'preferHtml' | 'strictHtml' = 'svg'; - /** - * Specifies the grid size. - * @default 10 - */ - gridSize: number = 10; - - /** - * Specifies if the grid is enabled. This is used in {@link snap}. - * @default true - */ - gridEnabled: boolean = true; - /** * Specifies if ports are enabled. This is used in {@link cellConnected} to update * the respective style. @@ -255,13 +248,6 @@ class Graph extends EventSource { */ defaultParent: Cell | null = null; - /** - * Specifies the alternate edge style to be used if the main control point - * on an edge is being double clicked. - * @default null - */ - alternateEdgeStyle: string | null = null; - /** * Specifies the {@link Image} to be returned by {@link getBackgroundImage}. * @default null @@ -348,93 +334,6 @@ class Graph extends EventSource { */ importEnabled: boolean = true; - /** - * Specifies the return value for {@link isCellLocked}. - * @default false - */ - cellsLocked: boolean = false; - - /** - * Specifies the return value for {@link isCellCloneable}. - * @default true - */ - cellsCloneable: boolean = true; - - /** - * Specifies the return value for {@link isCellEditable}. - * @default true - */ - cellsEditable: boolean = true; - - /** - * Specifies the return value for {@link isCellDeletable}. - * @default true - */ - cellsDeletable: boolean = true; - - /** - * Specifies the return value for {@link isCellMovable}. - * @default true - */ - cellsMovable: boolean = true; - - /** - * Specifies the return value for edges in {@link isLabelMovable}. - * @default true - */ - edgeLabelsMovable: boolean = true; - - /** - * Specifies the return value for {@link isDropEnabled}. - * @default false - */ - dropEnabled: boolean = false; - - /** - * Specifies if dropping onto edges should be enabled. This is ignored if - * {@link dropEnabled} is `false`. If enabled, it will call {@link splitEdge} to carry - * out the drop operation. - * @default true - */ - splitEnabled: boolean = true; - - /** - * Specifies the return value for {@link isCellsResizable}. - * @default true - */ - cellsResizable: boolean = true; - - /** - * Specifies the return value for {@link isCellsBendable}. - * @default true - */ - cellsBendable: boolean = true; - - /** - * Specifies the return value for {@link isCellsSelectable}. - * @default true - */ - cellsSelectable: boolean = true; - - /** - * Specifies the return value for {@link isCellsDisconnectable}. - * @default true - */ - cellsDisconnectable: boolean = true; - - /** - * Specifies if the graph should automatically update the cell size after an - * edit. This is used in {@link isAutoSizeCell}. - * @default false - */ - autoSizeCells: boolean = false; - - /** - * Specifies if autoSize style should be applied when cells are added. - * @default false - */ - autoSizeCellsOnAdd: boolean = false; - /** * Specifies if the graph should automatically scroll regardless of the * scrollbars. This will scroll the container using positive values for @@ -507,12 +406,6 @@ class Graph extends EventSource { */ keepEdgesInBackground: boolean = false; - /** - * Specifies if negative coordinates for vertices are allowed. - * @default true - */ - allowNegativeCoordinates: boolean = true; - /** * Specifies if a child should be constrained inside the parent bounds after a * move or resize of the child. @@ -555,25 +448,6 @@ class Graph extends EventSource { */ recursiveResize: boolean = false; - /** - * Specifies the factor used for {@link zoomIn} and {@link zoomOut}. - * @default 1.2 (120%) - */ - zoomFactor: number = 1.2; - - /** - * Specifies if the viewport should automatically contain the selection cells after a zoom operation. - * @default false - */ - keepSelectionVisibleOnZoom: boolean = false; - - /** - * Specifies if the zoom operations should go into the center of the actual - * diagram rather than going from top, left. - * @default true - */ - centerZoom: boolean = true; - /** * Specifies if the scale and translate should be reset if the root changes in * the model. @@ -581,27 +455,6 @@ class Graph extends EventSource { */ resetViewOnRootChange: boolean = true; - /** - * Specifies if edge control points should be reset after the resize of a - * connected cell. - * @default false - */ - resetEdgesOnResize: boolean = false; - - /** - * Specifies if edge control points should be reset after the move of a - * connected cell. - * @default false - */ - resetEdgesOnMove: boolean = false; - - /** - * Specifies if edge control points should be reset after the the edge has been - * reconnected. - * @default true - */ - resetEdgesOnConnect: boolean = true; - /** * Specifies if loops (aka self-references) are allowed. * @default false @@ -613,7 +466,7 @@ class Graph extends EventSource { * {@link mxConstants.STYLE_LOOP} is undefined. * @default {@link EdgeStyle.Loop} */ - defaultLoopStyle = mxEdgeStyle.Loop; + defaultLoopStyle = EdgeStyle.Loop; /** * Specifies if multiple edges in the same direction between the same pair of @@ -622,32 +475,6 @@ class Graph extends EventSource { */ multigraph: boolean = true; - /** - * Specifies if edges are connectable. This overrides the connectable field in edges. - * @default false - */ - connectableEdges: boolean = false; - - /** - * Specifies if edges with disconnected terminals are allowed in the graph. - * @default true - */ - allowDanglingEdges: boolean = true; - - /** - * Specifies if edges that are cloned should be validated and only inserted - * if they are valid. - * @default true - */ - cloneInvalidEdges: boolean = false; - - /** - * Specifies if edges should be disconnected from their terminals when they - * are moved. - * @default true - */ - disconnectOnMove: boolean = true; - /** * Specifies if labels should be visible. This is used in {@link getLabel}. Default * is true. @@ -660,26 +487,6 @@ class Graph extends EventSource { */ htmlLabels: boolean = false; - /** - * Specifies if swimlanes should be selectable via the content if the - * mouse is released. - * @default true - */ - swimlaneSelectionEnabled: boolean = true; - - /** - * Specifies if nesting of swimlanes is allowed. - * @default true - */ - swimlaneNesting: boolean = true; - - /** - * The attribute used to find the color for the indicator if the indicator - * color is set to 'swimlane'. - * @default {@link 'fillColor'} - */ - swimlaneIndicatorColorAttribute: string = 'fillColor'; - /** * Specifies the minimum scale to be applied in {@link fit}. Set this to `null` to allow any value. * @default 0.1 @@ -722,15 +529,6 @@ class Graph extends EventSource { containsValidationErrorsResource: string = mxClient.language != 'none' ? 'containsValidationErrors' : ''; - /** - * Specifies the resource key for the tooltip on the collapse/expand icon. - * If the resource for this key does not exist then the value is used as - * the tooltip. - * @default 'collapse-expand' - */ - collapseExpandResource: string = - mxClient.language != 'none' ? 'collapse-expand' : ''; - // TODO: Document me!! batchUpdate(fn: Function): void { (this.getModel()).beginUpdate(); @@ -853,62 +651,6 @@ class Graph extends EventSource { this.stylesheet = stylesheet; } - /** - * Function: getSelectionCellsForChanges - * - * Returns the cells to be selected for the given array of changes. - * - * Parameters: - * - * ignoreFn - Optional function that takes a change and returns true if the - * change should be ignored. - * - */ - getSelectionCellsForChanges( - changes: any[], - ignoreFn: Function | null = null - ): CellArray { - const dict = new mxDictionary(); - const cells: CellArray = new CellArray(); - - const addCell = (cell: Cell) => { - if (!dict.get(cell) && this.getModel().contains(cell)) { - if (cell.isEdge() || cell.isVertex()) { - dict.put(cell, true); - cells.push(cell); - } else { - const childCount = cell.getChildCount(); - - for (let i = 0; i < childCount; i += 1) { - addCell(cell.getChildAt(i)); - } - } - } - }; - - for (let i = 0; i < changes.length; i += 1) { - const change = changes[i]; - - if ( - change.constructor !== RootChange && - (ignoreFn == null || !ignoreFn(change)) - ) { - let cell = null; - - if (change instanceof ChildChange) { - cell = change.child; - } else if (change.cell != null && change.cell instanceof Cell) { - cell = change.cell; - } - - if (cell != null) { - addCell(cell); - } - } - } - return cells; - } - /** * Called when the graph model changes. Invokes {@link processChange} on each * item of the given array to update the view accordingly. @@ -921,36 +663,10 @@ class Graph extends EventSource { } this.updateSelection(); - this.getView().validate(); + this.view.validate(); this.sizeDidChange(); } - /** - * Removes selection cells that are not in the model from the selection. - */ - updateSelection(): void { - const cells = this.getSelectionCells(); - const removed = new CellArray(); - - for (const cell of cells) { - if (!this.getModel().contains(cell) || !cell.isVisible()) { - removed.push(cell); - } else { - let par = cell.getParent(); - - while (par != null && par !== this.getView().currentRoot) { - if (par.isCollapsed() || !par.isVisible()) { - removed.push(cell); - break; - } - - par = par.getParent(); - } - } - } - this.removeSelectionCells(removed); - } - /** * Processes the given change and invalidates the respective cached data * in {@link GraphView}. This fires a {@link root} event if the root has changed in the @@ -962,14 +678,14 @@ class Graph extends EventSource { // Resets the view settings, removes all cells and clears // the selection if the root changes. if (change instanceof RootChange) { - this.clearSelection(); + this.selection.clearSelection(); this.setDefaultParent(null); - this.removeStateForCell(change.previous); + this.cells.removeStateForCell(change.previous); if (this.resetViewOnRootChange) { - this.getView().scale = 1; - this.getView().translate.x = 0; - this.getView().translate.y = 0; + this.view.scale = 1; + this.view.translate.x = 0; + this.view.translate.y = 0; } this.fireEvent(new EventObject(InternalEvent.ROOT)); @@ -980,14 +696,14 @@ class Graph extends EventSource { // old and new parent and the child. else if (change instanceof ChildChange) { const newParent = change.child.getParent(); - this.getView().invalidate(change.child, true, true); + this.view.invalidate(change.child, true, true); if (!this.getModel().contains(newParent) || newParent.isCollapsed()) { - this.getView().invalidate(change.child, true, true); - this.removeStateForCell(change.child); + this.view.invalidate(change.child, true, true); + this.cells.removeStateForCell(change.child); // Handles special case of current root of view being removed - if (this.getView().currentRoot == change.child) { + if (this.view.currentRoot == change.child) { this.home(); } } @@ -995,11 +711,11 @@ class Graph extends EventSource { if (newParent != change.previous) { // Refreshes the collapse/expand icons on the parents if (newParent != null) { - this.getView().invalidate(newParent, false, false); + this.view.invalidate(newParent, false, false); } if (change.previous != null) { - this.getView().invalidate(change.previous, false, false); + this.view.invalidate(change.previous, false, false); } } } @@ -1016,20 +732,20 @@ class Graph extends EventSource { (change.previous == null && change.geometry != null) || (change.previous != null && !change.previous.equals(change.geometry)) ) { - this.getView().invalidate(change.cell); + this.view.invalidate(change.cell); } } // Handles two special cases where only the shape, but no // descendants need to be recreated else if (change instanceof ValueChange) { - this.getView().invalidate(change.cell, false, false); + this.view.invalidate(change.cell, false, false); } // Requires a new mxShape in JavaScript else if (change instanceof StyleChange) { - this.getView().invalidate(change.cell, true, true); - const state = this.getView().getState(change.cell); + this.view.invalidate(change.cell, true, true); + const state = this.view.getState(change.cell); if (state != null) { state.invalidStyle = true; @@ -1038,12 +754,10 @@ class Graph extends EventSource { // Removes the state from the cache by default else if (change.cell != null && change.cell instanceof Cell) { - this.removeStateForCell(change.cell); + this.cells.removeStateForCell(change.cell); } } - // ??? - /** * Scrolls the graph to the given point, extending the graph container if * specified. @@ -1077,7 +791,7 @@ class Graph extends EventSource { // canvas has been reached. Notes: Needs fix for IE. if (extend && old === c.scrollLeft) { // @ts-ignore - const root = this.getView().getDrawPane().ownerSVGElement; + const root = this.view.getDrawPane().ownerSVGElement; const width = c.scrollWidth + border - dx; // Updates the clipping region. This is an expensive @@ -1103,7 +817,7 @@ class Graph extends EventSource { if (old == c.scrollTop && extend) { // @ts-ignore - const root = this.getView().getDrawPane().ownerSVGElement; + const root = this.view.getDrawPane().ownerSVGElement; const height = c.scrollHeight + border - dy; // Updates the clipping region. This is an expensive @@ -1138,24 +852,24 @@ class Graph extends EventSource { * width and height of the returned {@link Rectangle}, respectively. */ getBorderSizes(): Rectangle { - const css = utils.getCurrentStyle(this.container); + const css = getCurrentStyle(this.container); return new Rectangle( - utils.parseCssNumber(css.paddingLeft) + + parseCssNumber(css.paddingLeft) + (css.borderLeftStyle != 'none' - ? utils.parseCssNumber(css.borderLeftWidth) + ? parseCssNumber(css.borderLeftWidth) : 0), - utils.parseCssNumber(css.paddingTop) + + parseCssNumber(css.paddingTop) + (css.borderTopStyle != 'none' - ? utils.parseCssNumber(css.borderTopWidth) + ? parseCssNumber(css.borderTopWidth) : 0), - utils.parseCssNumber(css.paddingRight) + + parseCssNumber(css.paddingRight) + (css.borderRightStyle != 'none' - ? utils.parseCssNumber(css.borderRightWidth) + ? parseCssNumber(css.borderRightWidth) : 0), - utils.parseCssNumber(css.paddingBottom) + + parseCssNumber(css.paddingBottom) + (css.borderBottomStyle != 'none' - ? utils.parseCssNumber(css.borderBottomWidth) + ? parseCssNumber(css.borderBottomWidth) : 0) ); } @@ -1163,13 +877,9 @@ class Graph extends EventSource { /** * Returns the preferred size of the background page if {@link preferPageSize} is true. */ - getPreferredPageSize( - bounds: Rectangle, - width: number, - height: number - ): Rectangle { + getPreferredPageSize(bounds: Rectangle, width: number, height: number): Rectangle { const { scale } = this.view; - const tr = this.getView().translate; + const tr = this.view.translate; const fmt = this.pageFormat; const ps = this.pageScale; const page = new Rectangle( @@ -1254,7 +964,7 @@ class Graph extends EventSource { maxHeight != null ? maxHeight : this.container.offsetHeight - cssBorder.y - cssBorder.height - 1; - let bounds = this.getView().getGraphBounds(); + let bounds = this.view.getGraphBounds(); if (bounds.width > 0 && bounds.height > 0) { if (keepOrigin && bounds.x != null && bounds.y != null) { @@ -1266,7 +976,7 @@ class Graph extends EventSource { } // LATER: Use unscaled bounding boxes to fix rounding errors - const s = this.getView().scale; + const s = this.view.scale; let w2 = bounds.width / s; let h2 = bounds.height / s; @@ -1301,7 +1011,7 @@ class Graph extends EventSource { const x0 = bounds.x != null ? Math.floor( - this.getView().translate.x - + this.view.translate.x - bounds.x / s + border / s2 + margin / 2 @@ -1310,16 +1020,16 @@ class Graph extends EventSource { const y0 = bounds.y != null ? Math.floor( - this.getView().translate.y - + this.view.translate.y - bounds.y / s + border / s2 + margin / 2 ) : border; - this.getView().scaleAndTranslate(s2, x0, y0); + this.view.scaleAndTranslate(s2, x0, y0); } else { - this.getView().setScale(s2); + this.view.setScale(s2); const b2 = this.getGraphBounds(); if (b2.x != null) { @@ -1330,18 +1040,17 @@ class Graph extends EventSource { this.container.scrollTop = b2.y; } } - } else if (this.getView().scale != s2) { - this.getView().setScale(s2); + } else if (this.view.scale != s2) { + this.view.setScale(s2); } } else { return s2; } } } - return this.getView().scale; + return this.view.scale; } - /** * Resizes the container for the given graph width and height. */ @@ -1355,112 +1064,90 @@ class Graph extends EventSource { container.style.height = `${Math.ceil(height)}px`; } + /***************************************************************************** + * Group: UNCLASSIFIED + *****************************************************************************/ + /** - * Invokes from {@link sizeDidChange} to redraw the page breaks. + * Creates a new handler for the given cell state. This implementation + * returns a new {@link mxEdgeHandler} of the corresponding cell is an edge, + * otherwise it returns an {@link mxVertexHandler}. * - * @param visible Boolean that specifies if page breaks should be shown. - * @param width Specifies the width of the container in pixels. - * @param height Specifies the height of the container in pixels. + * @param state {@link mxCellState} whose handler should be created. */ - updatePageBreaks(visible: boolean, width: number, height: number): void { - const { scale } = this.view; - const tr = this.getView().translate; - const fmt = this.pageFormat; - const ps = scale * this.pageScale; - const bounds = new Rectangle(0, 0, fmt.width * ps, fmt.height * ps); + createHandler( + state: CellState + ): mxEdgeHandler | mxVertexHandler | null { + let result: mxEdgeHandler | mxVertexHandler | null = null; - const gb = Rectangle.fromRectangle(this.getGraphBounds()); - gb.width = Math.max(1, gb.width); - gb.height = Math.max(1, gb.height); + if (state.cell.isEdge()) { + const source = state.getVisibleTerminalState(true); + const target = state.getVisibleTerminalState(false); + const geo = (state.cell).getGeometry(); - bounds.x = - Math.floor((gb.x - tr.x * scale) / bounds.width) * bounds.width + - tr.x * scale; - bounds.y = - Math.floor((gb.y - tr.y * scale) / bounds.height) * bounds.height + - tr.y * scale; - - gb.width = - Math.ceil((gb.width + (gb.x - bounds.x)) / bounds.width) * bounds.width; - gb.height = - Math.ceil((gb.height + (gb.y - bounds.y)) / bounds.height) * - bounds.height; - - // Does not show page breaks if the scale is too small - visible = - visible && Math.min(bounds.width, bounds.height) > this.minPageBreakDist; - - const horizontalCount = visible - ? Math.ceil(gb.height / bounds.height) + 1 - : 0; - const verticalCount = visible ? Math.ceil(gb.width / bounds.width) + 1 : 0; - const right = (verticalCount - 1) * bounds.width; - const bottom = (horizontalCount - 1) * bounds.height; - - if (this.horizontalPageBreaks == null && horizontalCount > 0) { - this.horizontalPageBreaks = []; + const edgeStyle = this.getView().getEdgeStyle( + state, + geo != null ? geo.points : null, + source, + target + ); + result = this.createEdgeHandler(state, edgeStyle); + } else { + result = this.createVertexHandler(state); } + return result; + } - if (this.verticalPageBreaks == null && verticalCount > 0) { - this.verticalPageBreaks = []; + /** + * Hooks to create a new {@link mxVertexHandler} for the given {@link CellState}. + * + * @param state {@link mxCellState} to create the handler for. + */ + createVertexHandler(state: CellState): mxVertexHandler { + return new mxVertexHandler(state); + } + + /** + * Hooks to create a new {@link mxEdgeHandler} for the given {@link CellState}. + * + * @param state {@link mxCellState} to create the handler for. + */ + createEdgeHandler(state: CellState, edgeStyle: any): mxEdgeHandler { + let result = null; + if ( + edgeStyle == EdgeStyle.Loop || + edgeStyle == EdgeStyle.ElbowConnector || + edgeStyle == EdgeStyle.SideToSide || + edgeStyle == EdgeStyle.TopToBottom + ) { + result = this.createElbowEdgeHandler(state); + } else if ( + edgeStyle == EdgeStyle.SegmentConnector || + edgeStyle == EdgeStyle.OrthConnector + ) { + result = this.createEdgeSegmentHandler(state); + } else { + result = new mxEdgeHandler(state); } + return result; + } - const drawPageBreaks = (breaks: any) => { - if (breaks != null) { - const count = - breaks === this.horizontalPageBreaks - ? horizontalCount - : verticalCount; + /** + * Hooks to create a new {@link mxEdgeSegmentHandler} for the given {@link CellState}. + * + * @param state {@link mxCellState} to create the handler for. + */ + createEdgeSegmentHandler(state: CellState): mxEdgeSegmentHandler { + return new mxEdgeSegmentHandler(state); + } - for (let i = 0; i <= count; i += 1) { - const pts = - breaks === this.horizontalPageBreaks - ? [ - new Point( - Math.round(bounds.x), - Math.round(bounds.y + i * bounds.height) - ), - new Point( - Math.round(bounds.x + right), - Math.round(bounds.y + i * bounds.height) - ), - ] - : [ - new Point( - Math.round(bounds.x + i * bounds.width), - Math.round(bounds.y) - ), - new Point( - Math.round(bounds.x + i * bounds.width), - Math.round(bounds.y + bottom) - ), - ]; - - if (breaks[i] != null) { - breaks[i].points = pts; - breaks[i].redraw(); - } else { - const pageBreak = new mxPolyline(pts, this.pageBreakColor); - pageBreak.dialect = this.dialect; - pageBreak.pointerEvents = false; - pageBreak.isDashed = this.pageBreakDashed; - pageBreak.init(this.getView().backgroundPane); - pageBreak.redraw(); - - breaks[i] = pageBreak; - } - } - - for (let i = count; i < breaks.length; i += 1) { - breaks[i].destroy(); - } - - breaks.splice(count, breaks.length - count); - } - }; - - drawPageBreaks(this.horizontalPageBreaks); - drawPageBreaks(this.verticalPageBreaks); + /** + * Hooks to create a new {@link mxElbowEdgeHandler} for the given {@link CellState}. + * + * @param state {@link mxCellState} to create the handler for. + */ + createElbowEdgeHandler(state: CellState): mxElbowEdgeHandler { + return new mxElbowEdgeHandler(state); } /***************************************************************************** @@ -1472,7 +1159,7 @@ class Graph extends EventSource { * shortcut to {@link GraphView.currentRoot} in {@link GraphView}. */ getCurrentRoot(): Cell | null { - return this.getView().currentRoot; + return this.view.currentRoot; } /** @@ -1508,42 +1195,6 @@ class Graph extends EventSource { return null; } - /** - * Returns true if the given cell is a "port", that is, when connecting to - * it, the cell returned by getTerminalForPort should be used as the - * terminal and the port should be referenced by the ID in either the - * mxConstants.STYLE_SOURCE_PORT or the or the - * mxConstants.STYLE_TARGET_PORT. Note that a port should not be movable. - * This implementation always returns false. - * - * A typical implementation is the following: - * - * ```javascript - * graph.isPort = function(cell) - * { - * var geo = cell.getGeometry(); - * - * return (geo != null) ? geo.relative : false; - * }; - * ``` - * - * @param cell {@link mxCell} that represents the port. - */ - isPort(cell: Cell): boolean { - return false; - } - - /** - * Returns the terminal to be used for a given port. This implementation - * always returns the parent cell. - * - * @param cell {@link mxCell} that represents the port. - * @param source If the cell is the source or target port. - */ - getTerminalForPort(cell: Cell, source: boolean = false): Cell | null { - return cell.getParent(); - } - /** * Returns the offset to be used for the cells inside the given cell. The * root and layer cells may be identified using {@link Model.isRoot} and @@ -1559,60 +1210,6 @@ class Graph extends EventSource { return null; } - /** - * Uses the given cell as the root of the displayed cell hierarchy. If no - * cell is specified then the selection cell is used. The cell is only used - * if {@link isValidRoot} returns true. - * - * @param cell Optional {@link Cell} to be used as the new root. Default is the - * selection cell. - */ - enterGroup(cell: Cell): void { - cell = cell || this.getSelectionCell(); - - if (cell != null && this.isValidRoot(cell)) { - this.getView().setCurrentRoot(cell); - this.clearSelection(); - } - } - - /** - * Changes the current root to the next valid root in the displayed cell - * hierarchy. - */ - exitGroup(): void { - const root = this.getModel().getRoot(); - const current = this.getCurrentRoot(); - - if (current != null) { - let next = current.getParent(); - - // Finds the next valid root in the hierarchy - while ( - next !== root && - !this.isValidRoot(next) && - next.getParent() !== root - ) { - next = next.getParent(); - } - - // Clears the current root if the new root is - // the model's root or one of the layers. - if (next === root || next.getParent() === root) { - this.getView().setCurrentRoot(null); - } else { - this.getView().setCurrentRoot(next); - } - - const state = this.getView().getState(current); - - // Selects the previous root in the graph - if (state != null) { - this.setSelectionCell(current); - } - } - } - /** * Uses the root of the model as the root of the displayed cell hierarchy * and selects the previous root. @@ -1621,11 +1218,11 @@ class Graph extends EventSource { const current = this.getCurrentRoot(); if (current != null) { - this.getView().setCurrentRoot(null); - const state = this.getView().getState(current); + this.view.setCurrentRoot(null); + const state = this.view.getState(current); if (state != null) { - this.setSelectionCell(current); + this.selection.setSelectionCell(current); } } } @@ -1649,46 +1246,15 @@ class Graph extends EventSource { * {@link GraphView.getGraphBounds}. See also: {@link getBoundingBoxFromGeometry}. */ getGraphBounds(): Rectangle { - return this.getView().getGraphBounds(); + return this.view.getGraphBounds(); } /** - * Returns the scaled, translated bounds for the given cell. See - * {@link GraphView.getBounds} for arrays. - * - * @param cell {@link mxCell} whose bounds should be returned. - * @param includeEdges Optional boolean that specifies if the bounds of - * the connected edges should be included. Default is `false`. - * @param includeDescendants Optional boolean that specifies if the bounds - * of all descendants should be included. Default is `false`. + * Returns the bounds inside which the diagram should be kept as an + * {@link Rectangle}. */ - getCellBounds( - cell: Cell, - includeEdges: boolean = false, - includeDescendants: boolean = false - ): Rectangle | null { - let cells = new CellArray(cell); - - // Includes all connected edges - if (includeEdges) { - cells = cells.concat(cell.getEdges()); - } - - let result = this.getView().getBounds(cells); - - // Recursively includes the bounds of the children - if (includeDescendants) { - for (const child of cell.getChildren()) { - const tmp = this.getCellBounds(child, includeEdges, true); - - if (result != null) { - result.add(tmp); - } else { - result = tmp; - } - } - } - return result; + getMaximumGraphBounds(): Rectangle | null { + return this.maximumGraphBounds; } /** @@ -1731,106 +1297,104 @@ class Graph extends EventSource { let result = null; let tmp: Rectangle | null = null; - if (cells != null) { - for (const cell of cells) { - if (includeEdges || cell.isVertex()) { - // Computes the bounding box for the points in the geometry - const geo = cell.getGeometry(); + for (const cell of cells) { + if (includeEdges || cell.isVertex()) { + // Computes the bounding box for the points in the geometry + const geo = cell.getGeometry(); - if (geo != null) { - let bbox = null; + if (geo != null) { + let bbox = null; - if (cell.isEdge()) { - const addPoint = (pt: Point | null) => { - if (pt != null) { - if (tmp == null) { - tmp = new Rectangle(pt.x, pt.y, 0, 0); - } else { - tmp.add(new Rectangle(pt.x, pt.y, 0, 0)); - } - } - }; - - if (cell.getTerminal(true) == null) { - addPoint(geo.getTerminalPoint(true)); - } - - if (cell.getTerminal(false) == null) { - addPoint(geo.getTerminalPoint(false)); - } - - const pts = geo.points; - - if (pts != null && pts.length > 0) { - tmp = new Rectangle(pts[0].x, pts[0].y, 0, 0); - - for (let j = 1; j < pts.length; j++) { - addPoint(pts[j]); + if (cell.isEdge()) { + const addPoint = (pt: Point | null) => { + if (pt != null) { + if (tmp == null) { + tmp = new Rectangle(pt.x, pt.y, 0, 0); + } else { + tmp.add(new Rectangle(pt.x, pt.y, 0, 0)); } } + }; - bbox = tmp; - } else { - const parent = cell.getParent(); + if (cell.getTerminal(true) == null) { + addPoint(geo.getTerminalPoint(true)); + } - if (geo.relative) { - if ( - parent.isVertex() && - parent !== this.getView().currentRoot - ) { - tmp = this.getBoundingBoxFromGeometry(new CellArray(parent), false); + if (cell.getTerminal(false) == null) { + addPoint(geo.getTerminalPoint(false)); + } - if (tmp != null) { - bbox = new Rectangle( - geo.x * tmp.width, - geo.y * tmp.height, - geo.width, - geo.height - ); + const pts = geo.points; - if (cells.indexOf(parent) >= 0) { - bbox.x += tmp.x; - bbox.y += tmp.y; - } - } - } - } else { - bbox = Rectangle.fromRectangle(geo); + if (pts != null && pts.length > 0) { + tmp = new Rectangle(pts[0].x, pts[0].y, 0, 0); - if (parent.isVertex() && cells.indexOf(parent) >= 0) { - tmp = this.getBoundingBoxFromGeometry(new CellArray(parent), false); + for (let j = 1; j < pts.length; j++) { + addPoint(pts[j]); + } + } - if (tmp != null) { + bbox = tmp; + } else { + const parent = cell.getParent(); + + if (geo.relative) { + if ( + parent.isVertex() && + parent !== this.view.currentRoot + ) { + tmp = this.getBoundingBoxFromGeometry(new CellArray(parent), false); + + if (tmp != null) { + bbox = new Rectangle( + geo.x * tmp.width, + geo.y * tmp.height, + geo.width, + geo.height + ); + + if (cells.indexOf(parent) >= 0) { bbox.x += tmp.x; bbox.y += tmp.y; } } } + } else { + bbox = Rectangle.fromRectangle(geo); - if (bbox != null && geo.offset != null) { - bbox.x += geo.offset.x; - bbox.y += geo.offset.y; - } + if (parent.isVertex() && cells.indexOf(parent) >= 0) { + tmp = this.getBoundingBoxFromGeometry(new CellArray(parent), false); - const style = this.getCurrentCellStyle(cell); - - if (bbox != null) { - const angle = getValue(style, 'rotation', 0); - - if (angle !== 0) { - bbox = getBoundingBox(bbox, angle); + if (tmp != null) { + bbox.x += tmp.x; + bbox.y += tmp.y; } } } + if (bbox != null && geo.offset != null) { + bbox.x += geo.offset.x; + bbox.y += geo.offset.y; + } + + const style = this.getCurrentCellStyle(cell); + if (bbox != null) { - if (result == null) { - result = Rectangle.fromRectangle(bbox); - } else { - result.add(bbox); + const angle = getValue(style, 'rotation', 0); + + if (angle !== 0) { + bbox = getBoundingBox(bbox, angle); } } } + + if (bbox != null) { + if (result == null) { + result = Rectangle.fromRectangle(bbox); + } else { + result.add(bbox); + } + } } } } @@ -1846,126 +1410,15 @@ class Graph extends EventSource { */ refresh(cell: Cell | null = null): void { if (cell) { - this.getView().clear(cell, false); + this.view.clear(cell, false); } else { - this.getView().clear(undefined, true); + this.view.clear(undefined, true); } - this.getView().validate(); + this.view.validate(); this.sizeDidChange(); this.fireEvent(new EventObject(InternalEvent.REFRESH)); } - /** - * Snaps the given numeric value to the grid if {@link gridEnabled} is true. - * - * @param value Numeric value to be snapped to the grid. - */ - snap(value: number): number { - if (this.gridEnabled) { - value = Math.round(value / this.gridSize) * this.gridSize; - } - return value; - } - - /** - * Function: snapDelta - * - * Snaps the given delta with the given scaled bounds. - */ - snapDelta( - delta: Point, - bounds: Rectangle, - ignoreGrid: boolean = false, - ignoreHorizontal: boolean = false, - ignoreVertical: boolean = false - ): Point { - const t = this.getView().translate; - const s = this.getView().scale; - - if (!ignoreGrid && this.gridEnabled) { - const tol = this.gridSize * s * 0.5; - - if (!ignoreHorizontal) { - const tx = bounds.x - (this.snap(bounds.x / s - t.x) + t.x) * s; - - if (Math.abs(delta.x - tx) < tol) { - delta.x = 0; - } else { - delta.x = this.snap(delta.x / s) * s - tx; - } - } - - if (!ignoreVertical) { - const ty = bounds.y - (this.snap(bounds.y / s - t.y) + t.y) * s; - - if (Math.abs(delta.y - ty) < tol) { - delta.y = 0; - } else { - delta.y = this.snap(delta.y / s) * s - ty; - } - } - } else { - const tol = 0.5 * s; - - if (!ignoreHorizontal) { - const tx = bounds.x - (Math.round(bounds.x / s - t.x) + t.x) * s; - - if (Math.abs(delta.x - tx) < tol) { - delta.x = 0; - } else { - delta.x = Math.round(delta.x / s) * s - tx; - } - } - - if (!ignoreVertical) { - const ty = bounds.y - (Math.round(bounds.y / s - t.y) + t.y) * s; - - if (Math.abs(delta.y - ty) < tol) { - delta.y = 0; - } else { - delta.y = Math.round(delta.y / s) * s - ty; - } - } - } - return delta; - } - - /** - * Zooms into the graph by {@link zoomFactor}. - */ - zoomIn(): void { - this.zoom(this.zoomFactor); - } - - /** - * Zooms out of the graph by {@link zoomFactor}. - */ - zoomOut(): void { - this.zoom(1 / this.zoomFactor); - } - - /** - * Resets the zoom and panning in the view. - */ - zoomActual(): void { - if (this.getView().scale === 1) { - this.getView().setTranslate(0, 0); - } else { - this.getView().translate.x = 0; - this.getView().translate.y = 0; - - this.getView().setScale(1); - } - } - - /** - * Zooms the graph to the given scale with an optional boolean center - * argument, which is passd to {@link zoom}. - */ - zoomTo(scale: number, center: boolean = false): void { - this.zoom(scale / this.getView().scale, center); - } - /** * Centers the graph in the container. * @@ -1990,14 +1443,14 @@ class Graph extends EventSource { const ch = container.clientHeight - padding; const bounds = this.getGraphBounds(); - const t = this.getView().translate; - const s = this.getView().scale; + const t = this.view.translate; + const s = this.view.scale; let dx = horizontal ? cw - bounds.width : 0; let dy = vertical ? ch - bounds.height : 0; if (!_hasScrollbars) { - this.getView().setTranslate( + this.view.setTranslate( horizontal ? Math.floor(t.x - bounds.x / s + (dx * cx) / s) : t.x, vertical ? Math.floor(t.y - bounds.y / s + (dy * cy) / s) : t.y ); @@ -2016,7 +1469,7 @@ class Graph extends EventSource { dy = 0; } - this.getView().setTranslate( + this.view.setTranslate( Math.floor(dx / 2 - bounds.x), Math.floor(dy / 2 - bounds.y) ); @@ -2025,158 +1478,6 @@ class Graph extends EventSource { } } - /** - * Zooms the graph using the given factor. Center is an optional boolean - * argument that keeps the graph scrolled to the center. If the center argument - * is omitted, then {@link centerZoom} will be used as its value. - */ - zoom(factor: number, center: boolean = this.centerZoom): void { - const scale = Math.round(this.getView().scale * factor * 100) / 100; - const state = this.getView().getState(this.getSelectionCell()); - const container = this.container; - factor = scale / this.getView().scale; - - if (this.keepSelectionVisibleOnZoom && state != null) { - const rect = new Rectangle( - state.x * factor, - state.y * factor, - state.width * factor, - state.height * factor - ); - - // Refreshes the display only once if a scroll is carried out - this.getView().scale = scale; - - if (!this.scrollRectToVisible(rect)) { - this.getView().revalidate(); - - // Forces an event to be fired but does not revalidate again - this.getView().setScale(scale); - } - } else { - const _hasScrollbars = hasScrollbars(this.container); - - if (center && !_hasScrollbars) { - let dx = container.offsetWidth; - let dy = container.offsetHeight; - - if (factor > 1) { - const f = (factor - 1) / (scale * 2); - dx *= -f; - dy *= -f; - } else { - const f = (1 / factor - 1) / (this.getView().scale * 2); - dx *= f; - dy *= f; - } - - this.getView().scaleAndTranslate( - scale, - this.getView().translate.x + dx, - this.getView().translate.y + dy - ); - } else { - // Allows for changes of translate and scrollbars during setscale - const tx = this.getView().translate.x; - const ty = this.getView().translate.y; - const sl = container.scrollLeft; - const st = container.scrollTop; - - this.getView().setScale(scale); - - if (_hasScrollbars) { - let dx = 0; - let dy = 0; - - if (center) { - dx = (container.offsetWidth * (factor - 1)) / 2; - dy = (container.offsetHeight * (factor - 1)) / 2; - } - - container.scrollLeft = - (this.getView().translate.x - tx) * this.getView().scale + - Math.round(sl * factor + dx); - container.scrollTop = - (this.getView().translate.y - ty) * this.getView().scale + - Math.round(st * factor + dy); - } - } - } - } - - /** - * Zooms the graph to the specified rectangle. If the rectangle does not have same aspect - * ratio as the display container, it is increased in the smaller relative dimension only - * until the aspect match. The original rectangle is centralised within this expanded one. - * - * Note that the input rectangular must be un-scaled and un-translated. - * - * @param rect The un-scaled and un-translated rectangluar region that should be just visible - * after the operation - */ - zoomToRect(rect: Rectangle): void { - const container = this.container; - const scaleX = container.clientWidth / rect.width; - const scaleY = container.clientHeight / rect.height; - const aspectFactor = scaleX / scaleY; - - // Remove any overlap of the rect outside the client area - rect.x = Math.max(0, rect.x); - rect.y = Math.max(0, rect.y); - let rectRight = Math.min(container.scrollWidth, rect.x + rect.width); - let rectBottom = Math.min(container.scrollHeight, rect.y + rect.height); - rect.width = rectRight - rect.x; - rect.height = rectBottom - rect.y; - - // The selection area has to be increased to the same aspect - // ratio as the container, centred around the centre point of the - // original rect passed in. - if (aspectFactor < 1.0) { - // Height needs increasing - const newHeight = rect.height / aspectFactor; - const deltaHeightBuffer = (newHeight - rect.height) / 2.0; - rect.height = newHeight; - - // Assign up to half the buffer to the upper part of the rect, not crossing 0 - // put the rest on the bottom - const upperBuffer = Math.min(rect.y, deltaHeightBuffer); - rect.y -= upperBuffer; - - // Check if the bottom has extended too far - rectBottom = Math.min(container.scrollHeight, rect.y + rect.height); - rect.height = rectBottom - rect.y; - } else { - // Width needs increasing - const newWidth = rect.width * aspectFactor; - const deltaWidthBuffer = (newWidth - rect.width) / 2.0; - rect.width = newWidth; - - // Assign up to half the buffer to the upper part of the rect, not crossing 0 - // put the rest on the bottom - const leftBuffer = Math.min(rect.x, deltaWidthBuffer); - rect.x -= leftBuffer; - - // Check if the right hand side has extended too far - rectRight = Math.min(container.scrollWidth, rect.x + rect.width); - rect.width = rectRight - rect.x; - } - - const scale = container.clientWidth / rect.width; - const newScale = this.getView().scale * scale; - - if (!hasScrollbars(this.container)) { - this.getView().scaleAndTranslate( - newScale, - this.getView().translate.x - rect.x / this.getView().scale, - this.getView().translate.y - rect.y / this.getView().scale - ); - } else { - this.getView().setScale(newScale); - container.scrollLeft = Math.round(rect.x * scale); - container.scrollTop = Math.round(rect.y * scale); - } - } - /** * Returns true if perimeter points should be computed such that the * resulting edge has only horizontal or vertical segments. @@ -2197,15 +1498,15 @@ class Graph extends EventSource { return orthogonal; } - const tmp = this.getView().getEdgeStyle(edge); + const tmp = this.view.getEdgeStyle(edge); return ( - tmp === mxEdgeStyle.SegmentConnector || - tmp === mxEdgeStyle.ElbowConnector || - tmp === mxEdgeStyle.SideToSide || - tmp === mxEdgeStyle.TopToBottom || - tmp === mxEdgeStyle.EntityRelation || - tmp === mxEdgeStyle.OrthConnector + tmp === EdgeStyle.SegmentConnector || + tmp === EdgeStyle.ElbowConnector || + tmp === EdgeStyle.SideToSide || + tmp === EdgeStyle.TopToBottom || + tmp === EdgeStyle.EntityRelation || + tmp === EdgeStyle.OrthConnector ); } @@ -2250,7 +1551,7 @@ class Graph extends EventSource { * * @param cell {@link mxCell} whose textual representation should be returned. */ - convertValueToString(cell: Cell): string | null { + convertValueToString(cell: Cell): string { const value = cell.getValue(); if (value != null) { @@ -2264,249 +1565,6 @@ class Graph extends EventSource { return ''; } - /** - * Returns a string or DOM node that represents the label for the given - * cell. This implementation uses {@link convertValueToString} if {@link labelsVisible} - * is true. Otherwise it returns an empty string. - * - * To truncate a label to match the size of the cell, the following code - * can be used. - * - * ```javascript - * graph.getLabel = function(cell) - * { - * var label = getLabel.apply(this, arguments); - * - * if (label != null && this.model.isVertex(cell)) - * { - * var geo = cell.getCellGeometry(); - * - * if (geo != null) - * { - * var max = parseInt(geo.width / 8); - * - * if (label.length > max) - * { - * label = label.substring(0, max)+'...'; - * } - * } - * } - * return mxUtils.htmlEntities(label); - * } - * ``` - * - * A resize listener is needed in the graph to force a repaint of the label - * after a resize. - * - * ```javascript - * graph.addListener(mxEvent.RESIZE_CELLS, function(sender, evt) - * { - * var cells = evt.getProperty('cells'); - * - * for (var i = 0; i < cells.length; i++) - * { - * this.view.removeState(cells[i]); - * } - * }); - * ``` - * - * @param cell {@link mxCell} whose label should be returned. - */ - getLabel(cell: Cell): string | Node | null { - let result: string | null = ''; - - if (this.labelsVisible && cell != null) { - const style = this.getCurrentCellStyle(cell); - - if (!getValue(style, 'noLabel', false)) { - result = this.convertValueToString(cell); - } - } - return result; - } - - /** - * Returns true if the label must be rendered as HTML markup. The default - * implementation returns {@link htmlLabels}. - * - * @param cell {@link mxCell} whose label should be displayed as HTML markup. - */ - isHtmlLabel(cell: Cell): boolean { - return this.isHtmlLabels(); - } - - /** - * Returns {@link htmlLabels}. - */ - isHtmlLabels(): boolean { - return this.htmlLabels; - } - - /** - * Sets {@link htmlLabels}. - */ - setHtmlLabels(value: boolean): void { - this.htmlLabels = value; - } - - /** - * This enables wrapping for HTML labels. - * - * Returns true if no white-space CSS style directive should be used for - * displaying the given cells label. This implementation returns true if - * {@link 'whiteSpace'} in the style of the given cell is 'wrap'. - * - * This is used as a workaround for IE ignoring the white-space directive - * of child elements if the directive appears in a parent element. It - * should be overridden to return true if a white-space directive is used - * in the HTML markup that represents the given cells label. In order for - * HTML markup to work in labels, {@link isHtmlLabel} must also return true - * for the given cell. - * - * @example - * - * ```javascript - * graph.getLabel = function(cell) - * { - * var tmp = getLabel.apply(this, arguments); // "supercall" - * - * if (this.model.isEdge(cell)) - * { - * tmp = '
'+tmp+'
'; - * } - * - * return tmp; - * } - * - * graph.isWrapping = function(state) - * { - * return this.model.isEdge(state.cell); - * } - * ``` - * - * Makes sure no edge label is wider than 150 pixels, otherwise the content - * is wrapped. Note: No width must be specified for wrapped vertex labels as - * the vertex defines the width in its geometry. - * - * @param state {@link mxCell} whose label should be wrapped. - */ - isWrapping(cell: Cell): boolean { - return this.getCurrentCellStyle(cell).whiteSpace === 'wrap'; - } - - /** - * Returns true if the overflow portion of labels should be hidden. If this - * returns true then vertex labels will be clipped to the size of the vertices. - * This implementation returns true if `overflow` in the - * style of the given cell is 'hidden'. - * - * @param state {@link mxCell} whose label should be clipped. - */ - isLabelClipped(cell: Cell): boolean { - return this.getCurrentCellStyle(cell).overflow === 'hidden'; - } - - /** - * Returns the string or DOM node that represents the tooltip for the given - * state, node and coordinate pair. This implementation checks if the given - * node is a folding icon or overlay and returns the respective tooltip. If - * this does not result in a tooltip, the handler for the cell is retrieved - * from {@link selectionCellsHandler} and the optional getTooltipForNode method is - * called. If no special tooltip exists here then {@link getTooltipForCell} is used - * with the cell in the given state as the argument to return a tooltip for the - * given state. - * - * @param state {@link mxCellState} whose tooltip should be returned. - * @param node DOM node that is currently under the mouse. - * @param x X-coordinate of the mouse. - * @param y Y-coordinate of the mouse. - */ - // getTooltip(state: mxCellState, node: Node, x: number, y: number): string; - getTooltip( - state: CellState, - node: HTMLElement, - x: number, - y: number - ): string | null { - let tip: string | null = null; - - if (state != null) { - // Checks if the mouse is over the folding icon - if ( - state.control != null && - // @ts-ignore - (node === state.control.node || node.parentNode === state.control.node) - ) { - tip = this.collapseExpandResource; - tip = htmlEntities(Resources.get(tip) || tip, true).replace( - /\\n/g, - '
' - ); - } - - if (tip == null && state.overlays != null) { - state.overlays.visit((id: string, shape: Shape) => { - // LATER: Exit loop if tip is not null - if ( - tip == null && - // @ts-ignore - (node === shape.node || node.parentNode === shape.node) - ) { - // @ts-ignore - tip = shape.overlay.toString(); - } - }); - } - - if (tip == null) { - const handler = (( - this.selectionCellsHandler - )).getHandler(state.cell); - if ( - handler != null && - typeof handler.getTooltipForNode === 'function' - ) { - tip = handler.getTooltipForNode(node); - } - } - - if (tip == null) { - tip = this.getTooltipForCell(state.cell); - } - } - return tip; - } - - /** - * Returns the string or DOM node to be used as the tooltip for the given - * cell. This implementation uses the cells getTooltip function if it - * exists, or else it returns {@link convertValueToString} for the cell. - * - * @example - * - * ```javascript - * graph.getTooltipForCell = function(cell) - * { - * return 'Hello, World!'; - * } - * ``` - * - * Replaces all tooltips with the string Hello, World! - * - * @param cell {@link mxCell} whose tooltip should be returned. - */ - getTooltipForCell(cell: Cell): string | null { - let tip = null; - - if (cell != null && 'getTooltip' in cell) { - // @ts-ignore - tip = cell.getTooltip(); - } else { - tip = this.convertValueToString(cell); - } - return tip; - } - /** * Returns the string to be used as the link for the given cell. This * implementation returns null. @@ -2517,212 +1575,6 @@ class Graph extends EventSource { return null; } - /** - * Returns the cursor value to be used for the CSS of the shape for the - * given event. This implementation calls {@link getCursorForCell}. - * - * @param me {@link mxMouseEvent} whose cursor should be returned. - */ - getCursorForMouseEvent(me: InternalMouseEvent): string | null { - return this.getCursorForCell(me.getCell()); - } - - /** - * Returns the cursor value to be used for the CSS of the shape for the - * given cell. This implementation returns null. - * - * @param cell {@link mxCell} whose cursor should be returned. - */ - getCursorForCell(cell: Cell): string | null { - return null; - } - - /** - * Returns the start size of the given swimlane, that is, the width or - * height of the part that contains the title, depending on the - * horizontal style. The return value is an {@link Rectangle} with either - * width or height set as appropriate. - * - * @param swimlane {@link mxCell} whose start size should be returned. - * @param ignoreState Optional boolean that specifies if cell state should be ignored. - */ - getStartSize(swimlane: Cell, ignoreState: boolean = false): Rectangle { - const result = new Rectangle(); - const style = this.getCurrentCellStyle(swimlane, ignoreState); - const size = parseInt( - getValue(style, 'startSize', DEFAULT_STARTSIZE) - ); - - if (getValue(style, 'horizontal', true)) { - result.height = size; - } else { - result.width = size; - } - return result; - } - - /** - * Returns the direction for the given swimlane style. - */ - getSwimlaneDirection(style: any): string { - const dir = getValue(style, 'direction', DIRECTION_EAST); - const flipH = getValue(style, 'flipH', 0) == 1; - const flipV = getValue(style, 'flipV', 0) == 1; - const h = getValue(style, 'horizontal', true); - let n = h ? 0 : 3; - - if (dir === DIRECTION_NORTH) { - n--; - } else if (dir === DIRECTION_WEST) { - n += 2; - } else if (dir === DIRECTION_SOUTH) { - n += 1; - } - - const mod = utils.mod(n, 2); - - if (flipH && mod === 1) { - n += 2; - } - - if (flipV && mod === 0) { - n += 2; - } - - return [DIRECTION_NORTH, DIRECTION_EAST, DIRECTION_SOUTH, DIRECTION_WEST][ - utils.mod(n, 4) - ]; - } - - /** - * Returns the actual start size of the given swimlane taking into account - * direction and horizontal and vertial flip styles. The start size is - * returned as an {@link Rectangle} where top, left, bottom, right start sizes - * are returned as x, y, height and width, respectively. - * - * @param swimlane {@link mxCell} whose start size should be returned. - * @param ignoreState Optional boolean that specifies if cell state should be ignored. - */ - getActualStartSize( - swimlane: Cell, - ignoreState: boolean = false - ): Rectangle { - const result = new Rectangle(); - - if (this.isSwimlane(swimlane, ignoreState)) { - const style = this.getCurrentCellStyle(swimlane, ignoreState); - const size = parseInt(getValue(style, 'startSize', DEFAULT_STARTSIZE)); - const dir = this.getSwimlaneDirection(style); - - if (dir === DIRECTION_NORTH) { - result.y = size; - } else if (dir === DIRECTION_WEST) { - result.x = size; - } else if (dir === DIRECTION_SOUTH) { - result.height = size; - } else { - result.width = size; - } - } - return result; - } - - /** - * Returns the image URL for the given cell state. This implementation - * returns the value stored under {@link 'image'} in the cell - * style. - * - * @param state {@link mxCellState} whose image URL should be returned. - */ - getImage(state: CellState): Image | null { - return state != null && state.style != null - ? state.style.image - : null; - } - - /** - * Returns true if the given state has no stroke- or fillcolor and no image. - * - * @param state {@link mxCellState} to check. - */ - isTransparentState(state: CellState): boolean { - let result = false; - if (state != null) { - const stroke = getValue(state.style, 'strokeColor', NONE); - const fill = getValue(state.style, 'fillColor', NONE); - result = stroke === NONE && fill === NONE && this.getImage(state) == null; - } - return result; - } - - /** - * Returns the vertical alignment for the given cell state. This - * implementation returns the value stored under - * {@link 'verticalAlign'} in the cell style. - * - * @param state {@link mxCellState} whose vertical alignment should be - * returned. - */ - getVerticalAlign(state: CellState): string | null { - return state != null && state.style != null - ? state.style.verticalAlign || ALIGN_MIDDLE - : null; - } - - /** - * Returns the indicator color for the given cell state. This - * implementation returns the value stored under - * {@link mxConstants.STYLE_INDICATOR_COLOR} in the cell style. - * - * @param state {@link mxCellState} whose indicator color should be - * returned. - */ - getIndicatorColor(state: CellState): string | null { - return state != null && state.style != null - ? state.style.indicatorColor - : null; - } - - /** - * Returns the indicator gradient color for the given cell state. This - * implementation returns the value stored under - * {@link mxConstants.STYLE_INDICATOR_GRADIENTCOLOR} in the cell style. - * - * @param state {@link mxCellState} whose indicator gradient color should be - * returned. - */ - getIndicatorGradientColor(state: CellState): string | null { - return state != null && state.style != null - ? state.style.gradientColor - : null; - } - - /** - * Returns the indicator shape for the given cell state. This - * implementation returns the value stored under - * {@link mxConstants.STYLE_INDICATOR_SHAPE} in the cell style. - * - * @param state {@link mxCellState} whose indicator shape should be returned. - */ - getIndicatorShape(state: CellState): string | null { - return state != null && state.style != null - ? state.style.indicatorShape - : null; - } - - /** - * Returns the indicator image for the given cell state. This - * implementation returns the value stored under - * {@link mxConstants.STYLE_INDICATOR_IMAGE} in the cell style. - * - * @param state {@link mxCellState} whose indicator image should be returned. - */ - getIndicatorImage(state: CellState): Image | null { - return state != null && state.style != null - ? state.style.indicatorImage - : null; - } - /** * Returns the value of {@link border}. */ @@ -2739,28 +1591,6 @@ class Graph extends EventSource { this.border = value; } - /** - * Returns true if the given cell is a swimlane in the graph. A swimlane is - * a container cell with some specific behaviour. This implementation - * checks if the shape associated with the given cell is a {@link mxSwimlane}. - * - * @param cell {@link mxCell} to be checked. - * @param ignoreState Optional boolean that specifies if the cell state should be ignored. - */ - isSwimlane(cell: Cell, ignoreState: boolean = false): boolean { - if ( - cell != null && - cell.getParent() !== this.getModel().getRoot() && - !cell.isEdge() - ) { - return ( - this.getCurrentCellStyle(cell, ignoreState).shape === - SHAPE_SWIMLANE - ); - } - return false; - } - /***************************************************************************** * Group: Graph behaviour *****************************************************************************/ @@ -2798,160 +1628,6 @@ class Graph extends EventSource { this.enabled = value; } - /** - * Returns {@link escapeEnabled}. - */ - isEscapeEnabled(): boolean { - return this.escapeEnabled; - } - - /** - * Sets {@link escapeEnabled}. - * - * @param enabled Boolean indicating if escape should be enabled. - */ - setEscapeEnabled(value: boolean): void { - this.escapeEnabled = value; - } - - /** - * Returns {@link invokesStopCellEditing}. - */ - isInvokesStopCellEditing(): boolean { - return this.invokesStopCellEditing; - } - - /** - * Sets {@link invokesStopCellEditing}. - */ - setInvokesStopCellEditing(value: boolean): void { - this.invokesStopCellEditing = value; - } - - /** - * Returns {@link enterStopsCellEditing}. - */ - isEnterStopsCellEditing(): boolean { - return this.enterStopsCellEditing; - } - - /** - * Sets {@link enterStopsCellEditing}. - */ - setEnterStopsCellEditing(value: boolean): void { - this.enterStopsCellEditing = value; - } - - /** - * Returns true if the given edges's label is moveable. This returns - * {@link movable} for all given cells if {@link isLocked} does not return true - * for the given cell. - * - * @param cell {@link mxCell} whose label should be moved. - */ - isLabelMovable(cell: Cell): boolean { - return ( - !this.isCellLocked(cell) && - ((cell.isEdge() && this.edgeLabelsMovable) || - (cell.isVertex() && this.vertexLabelsMovable)) - ); - } - - /** - * Returns {@link gridEnabled} as a boolean. - */ - isGridEnabled(): boolean { - return this.gridEnabled; - } - - /** - * Specifies if the grid should be enabled. - * - * @param value Boolean indicating if the grid should be enabled. - */ - setGridEnabled(value: boolean): void { - this.gridEnabled = value; - } - - /** - * Returns {@link portsEnabled} as a boolean. - */ - isPortsEnabled(): boolean { - return this.portsEnabled; - } - - /** - * Specifies if the ports should be enabled. - * - * @param value Boolean indicating if the ports should be enabled. - */ - setPortsEnabled(value: boolean): void { - this.portsEnabled = value; - } - - /** - * Returns {@link gridSize}. - */ - getGridSize(): number { - return this.gridSize; - } - - /** - * Sets {@link gridSize}. - */ - setGridSize(value: number): void { - this.gridSize = value; - } - - /** - * Returns {@link tolerance}. - */ - getTolerance(): number { - return this.tolerance; - } - - /** - * Sets {@link tolerance}. - */ - setTolerance(value: number): void { - this.tolerance = value; - } - - /** - * Returns {@link swimlaneNesting} as a boolean. - */ - isSwimlaneNesting(): boolean { - return this.swimlaneNesting; - } - - /** - * Specifies if swimlanes can be nested by drag and drop. This is only - * taken into account if dropEnabled is true. - * - * @param value Boolean indicating if swimlanes can be nested. - */ - setSwimlaneNesting(value: boolean): void { - this.swimlaneNesting = value; - } - - /** - * Returns {@link swimlaneSelectionEnabled} as a boolean. - */ - isSwimlaneSelectionEnabled(): boolean { - return this.swimlaneSelectionEnabled; - } - - /** - * Specifies if swimlanes should be selected if the mouse is released - * over their content area. - * - * @param value Boolean indicating if swimlanes content areas - * should be selected when the mouse is released over them. - */ - setSwimlaneSelectionEnabled(value: boolean): void { - this.swimlaneSelectionEnabled = value; - } - /** * Returns {@link multigraph} as a boolean. */ @@ -2986,60 +1662,6 @@ class Graph extends EventSource { this.allowLoops = value; } - /** - * Returns {@link disconnectOnMove} as a boolean. - */ - isDisconnectOnMove(): boolean { - return this.disconnectOnMove; - } - - /** - * Specifies if edges should be disconnected when moved. (Note: Cloned - * edges are always disconnected.) - * - * @param value Boolean indicating if edges should be disconnected - * when moved. - */ - setDisconnectOnMove(value: boolean): void { - this.disconnectOnMove = value; - } - - /** - * Returns {@link dropEnabled} as a boolean. - */ - isDropEnabled(): boolean { - return this.dropEnabled; - } - - /** - * Specifies if the graph should allow dropping of cells onto or into other - * cells. - * - * @param dropEnabled Boolean indicating if the graph should allow dropping - * of cells into other cells. - */ - setDropEnabled(value: boolean): void { - this.dropEnabled = value; - } - - /** - * Returns {@link splitEnabled} as a boolean. - */ - isSplitEnabled(): boolean { - return this.splitEnabled; - } - - /** - * Specifies if the graph should allow dropping of cells onto or into other - * cells. - * - * @param dropEnabled Boolean indicating if the graph should allow dropping - * of cells into other cells. - */ - setSplitEnabled(value: boolean): void { - this.splitEnabled = value; - } - /** * Returns true if the given terminal point is movable. This is independent * from {@link isCellConnectable} and {@link isCellDisconnectable} and controls if terminal @@ -3054,293 +1676,6 @@ class Graph extends EventSource { return true; } - /** - * Returns true if the given cell is bendable. This returns {@link cellsBendable} - * for all given cells if {@link isLocked} does not return true for the given - * cell and its style does not specify {@link mxConstants.STYLE_BENDABLE} to be 0. - * - * @param cell {@link mxCell} whose bendable state should be returned. - */ - isCellBendable(cell: Cell): boolean { - const style = this.getCurrentCellStyle(cell); - - return ( - this.isCellsBendable() && - !this.isCellLocked(cell) && - style.bendable !== 0 - ); - } - - /** - * Returns {@link cellsBenadable}. - */ - isCellsBendable(): boolean { - return this.cellsBendable; - } - - /** - * Specifies if the graph should allow bending of edges. This - * implementation updates {@link bendable}. - * - * @param value Boolean indicating if the graph should allow bending of - * edges. - */ - setCellsBendable(value: boolean): void { - this.cellsBendable = value; - } - - /** - * Returns true if the given cell is editable. This returns {@link cellsEditable} for - * all given cells if {@link isCellLocked} does not return true for the given cell - * and its style does not specify {@link 'editable'} to be 0. - * - * @param cell {@link mxCell} whose editable state should be returned. - */ - isCellEditable(cell: Cell): boolean { - const style = this.getCurrentCellStyle(cell); - - return ( - this.isCellsEditable() && - !this.isCellLocked(cell) && - style.editable != 0 - ); - } - - /** - * Returns {@link cellsEditable}. - */ - isCellsEditable(): boolean { - return this.cellsEditable; - } - - /** - * Specifies if the graph should allow in-place editing for cell labels. - * This implementation updates {@link cellsEditable}. - * - * @param value Boolean indicating if the graph should allow in-place - * editing. - */ - setCellsEditable(value: boolean): void { - this.cellsEditable = value; - } - - /** - * Returns true if the given cell is disconnectable from the source or - * target terminal. This returns {@link isCellsDisconnectable} for all given - * cells if {@link isCellLocked} does not return true for the given cell. - * - * @param cell {@link mxCell} whose disconnectable state should be returned. - * @param terminal {@link mxCell} that represents the source or target terminal. - * @param source Boolean indicating if the source or target terminal is to be - * disconnected. - */ - isCellDisconnectable( - cell: Cell, - terminal: Cell | null = null, - source: boolean = false - ): boolean { - return this.isCellsDisconnectable() && !this.isCellLocked(cell); - } - - /** - * Returns {@link cellsDisconnectable}. - */ - isCellsDisconnectable(): boolean { - return this.cellsDisconnectable; - } - - /** - * Sets {@link cellsDisconnectable}. - */ - setCellsDisconnectable(value: boolean): void { - this.cellsDisconnectable = value; - } - - /** - * Returns true if the given cell is a valid source for new connections. - * This implementation returns true for all non-null values and is - * called by is called by {@link isValidConnection}. - * - * @param cell {@link mxCell} that represents a possible source or null. - */ - isValidSource(cell: Cell): boolean { - return ( - (cell == null && this.allowDanglingEdges) || - (cell != null && - (!cell.isEdge() || this.connectableEdges) && - cell.isConnectable()) - ); - } - - /** - * Returns {@link isValidSource} for the given cell. This is called by - * {@link isValidConnection}. - * - * @param cell {@link mxCell} that represents a possible target or null. - */ - isValidTarget(cell: Cell): boolean { - return this.isValidSource(cell); - } - - /** - * Returns true if the given target cell is a valid target for source. - * This is a boolean implementation for not allowing connections between - * certain pairs of vertices and is called by {@link getEdgeValidationError}. - * This implementation returns true if {@link isValidSource} returns true for - * the source and {@link isValidTarget} returns true for the target. - * - * @param source {@link mxCell} that represents the source cell. - * @param target {@link mxCell} that represents the target cell. - */ - isValidConnection(source: Cell, target: Cell): boolean { - return this.isValidSource(source) && this.isValidTarget(target); - } - - /** - * Specifies if the graph should allow new connections. This implementation - * updates {@link mxConnectionHandler.enabled} in {@link connectionHandler}. - * - * @param connectable Boolean indicating if new connections should be allowed. - */ - setConnectable(connectable: boolean): void { - (this.connectionHandler).setEnabled(connectable); - } - - /** - * Returns true if the {@link connectionHandler} is enabled. - */ - isConnectable(): boolean { - return (this.connectionHandler).isEnabled(); - } - - /** - * Specifies if tooltips should be enabled. This implementation updates - * {@link TooltipHandler.enabled} in {@link tooltipHandler}. - * - * @param enabled Boolean indicating if tooltips should be enabled. - */ - setTooltips(enabled: boolean): void { - (this.tooltipHandler).setEnabled(enabled); - } - - /** - * Specifies if panning should be enabled. This implementation updates - * {@link PanningHandler.panningEnabled} in {@link panningHandler}. - * - * @param enabled Boolean indicating if panning should be enabled. - */ - setPanning(enabled: boolean): void { - (this.panningHandler).panningEnabled = enabled; - } - - /** - * Returns true if the given cell is currently being edited. - * If no cell is specified then this returns true if any - * cell is currently being edited. - * - * @param cell {@link mxCell} that should be checked. - */ - isEditing(cell: Cell | null = null): boolean { - if (this.cellEditor != null) { - const editingCell = this.cellEditor.getEditingCell(); - return cell == null ? editingCell != null : cell === editingCell; - } - return false; - } - - /** - * Returns true if the size of the given cell should automatically be - * updated after a change of the label. This implementation returns - * {@link autoSizeCells} or checks if the cell style does specify - * {@link 'autoSize'} to be 1. - * - * @param cell {@link mxCell} that should be resized. - */ - isAutoSizeCell(cell: Cell): boolean { - const style = this.getCurrentCellStyle(cell); - - return this.isAutoSizeCells() || style.autosize == 1; - } - - /** - * Returns {@link autoSizeCells}. - */ - isAutoSizeCells(): boolean { - return this.autoSizeCells; - } - - /** - * Specifies if cell sizes should be automatically updated after a label - * change. This implementation sets {@link autoSizeCells} to the given parameter. - * To update the size of cells when the cells are added, set - * {@link autoSizeCellsOnAdd} to true. - * - * @param value Boolean indicating if cells should be resized - * automatically. - */ - setAutoSizeCells(value: boolean): void { - this.autoSizeCells = value; - } - - /** - * Returns true if the parent of the given cell should be extended if the - * child has been resized so that it overlaps the parent. This - * implementation returns {@link isExtendParents} if the cell is not an edge. - * - * @param cell {@link mxCell} that has been resized. - */ - isExtendParent(cell: Cell): boolean { - return !cell.isEdge() && this.isExtendParents(); - } - - /** - * Returns {@link extendParents}. - */ - isExtendParents(): boolean { - return this.extendParents; - } - - /** - * Sets {@link extendParents}. - * - * @param value New boolean value for {@link extendParents}. - */ - setExtendParents(value: boolean): void { - this.extendParents = value; - } - - /** - * Returns {@link extendParentsOnAdd}. - */ - isExtendParentsOnAdd(cell: Cell): boolean { - return this.extendParentsOnAdd; - } - - /** - * Sets {@link extendParentsOnAdd}. - * - * @param value New boolean value for {@link extendParentsOnAdd}. - */ - setExtendParentsOnAdd(value: boolean): void { - this.extendParentsOnAdd = value; - } - - /** - * Returns {@link extendParentsOnMove}. - */ - isExtendParentsOnMove(): boolean { - return this.extendParentsOnMove; - } - - /** - * Sets {@link extendParentsOnMove}. - * - * @param value New boolean value for {@link extendParentsOnAdd}. - */ - setExtendParentsOnMove(value: boolean): void { - this.extendParentsOnMove = value; - } - /** * Returns {@link recursiveResize}. * @@ -3397,131 +1732,6 @@ class Graph extends EventSource { return false; } - /** - * Returns true if the given cell is a valid drop target for the specified - * cells. If {@link splitEnabled} is true then this returns {@link isSplitTarget} for - * the given arguments else it returns true if the cell is not collapsed - * and its child count is greater than 0. - * - * @param cell {@link mxCell} that represents the possible drop target. - * @param cells {@link mxCell} that should be dropped into the target. - * @param evt Mouseevent that triggered the invocation. - */ - // isValidDropTarget(cell: mxCell, cells: mxCellArray, evt: Event): boolean; - isValidDropTarget(cell: Cell, cells: CellArray, evt: InternalMouseEvent): boolean { - return ( - cell != null && - ((this.isSplitEnabled() && this.isSplitTarget(cell, cells, evt)) || - (!cell.isEdge() && - (this.isSwimlane(cell) || - (cell.getChildCount() > 0 && !cell.isCollapsed())))) - ); - } - - /** - * Returns true if the given edge may be splitted into two edges with the - * given cell as a new terminal between the two. - * - * @param target {@link mxCell} that represents the edge to be splitted. - * @param cells {@link mxCell} that should split the edge. - * @param evt Mouseevent that triggered the invocation. - */ - // isSplitTarget(target: mxCell, cells: mxCellArray, evt: Event): boolean; - isSplitTarget(target: Cell, cells: CellArray, evt: InternalMouseEvent): boolean { - if ( - target.isEdge() && - cells != null && - cells.length == 1 && - cells[0].isConnectable() && - this.getEdgeValidationError(target, target.getTerminal(true), cells[0]) == - null - ) { - const src = target.getTerminal(true); - const trg = target.getTerminal(false); - - return ( - !cells[0].isAncestor(src) && - !cells[0].isAncestor(trg) - ); - } - return false; - } - - /** - * Returns the given cell if it is a drop target for the given cells or the - * nearest ancestor that may be used as a drop target for the given cells. - * If the given array contains a swimlane and {@link swimlaneNesting} is false - * then this always returns null. If no cell is given, then the bottommost - * swimlane at the location of the given event is returned. - * - * This function should only be used if {@link isDropEnabled} returns true. - * - * @param cells Array of {@link Cell} which are to be dropped onto the target. - * @param evt Mouseevent for the drag and drop. - * @param cell {@link mxCell} that is under the mousepointer. - * @param clone Optional boolean to indicate of cells will be cloned. - */ - // getDropTarget(cells: mxCellArray, evt: Event, cell: mxCell, clone?: boolean): mxCell; - getDropTarget( - cells: CellArray, - evt: InternalMouseEvent, - cell: Cell | null = null, - clone: boolean = false - ): Cell | null { - if (!this.isSwimlaneNesting()) { - for (let i = 0; i < cells.length; i += 1) { - if (this.isSwimlane(cells[i])) { - return null; - } - } - } - - const pt = convertPoint( - this.container, - getClientX(evt), - getClientY(evt) - ); - pt.x -= this.panDx; - pt.y -= this.panDy; - const swimlane = this.getSwimlaneAt(pt.x, pt.y); - - if (cell == null) { - cell = swimlane; - } else if (swimlane != null) { - // Checks if the cell is an ancestor of the swimlane - // under the mouse and uses the swimlane in that case - let tmp = swimlane.getParent(); - - while (tmp != null && this.isSwimlane(tmp) && tmp != cell) { - tmp = tmp.getParent(); - } - - if (tmp == cell) { - cell = swimlane; - } - } - - while ( - cell != null && - !this.isValidDropTarget(cell, cells, evt) && - !this.getModel().isLayer(cell) - ) { - cell = cell.getParent(); - } - - // Checks if parent is dropped into child if not cloning - if (!clone) { - let parent = cell; - while (parent != null && cells.indexOf(parent) < 0) { - parent = parent.getParent(); - } - } - - return !this.getModel().isLayer(cell) && parent == null - ? cell - : null; - } - /***************************************************************************** * Group: Cell retrieval *****************************************************************************/ @@ -3578,45 +1788,43 @@ class Graph extends EventSource { // Fast lookup to avoid duplicates in terminals array const dict = new mxDictionary(); - if (edges != null) { - for (let i = 0; i < edges.length; i += 1) { - const state = this.getView().getState(edges[i]); + for (let i = 0; i < edges.length; i += 1) { + const state = this.view.getState(edges[i]); - const source = - state != null - ? state.getVisibleTerminal(true) - : this.getView().getVisibleTerminal(edges[i], true); - const target = - state != null - ? state.getVisibleTerminal(false) - : this.getView().getVisibleTerminal(edges[i], false); + const source = + state != null + ? state.getVisibleTerminal(true) + : this.view.getVisibleTerminal(edges[i], true); + const target = + state != null + ? state.getVisibleTerminal(false) + : this.view.getVisibleTerminal(edges[i], false); - // Checks if the terminal is the source of the edge and if the - // target should be stored in the result - if ( - source == terminal && - target != null && - target != terminal && - targets - ) { - if (!dict.get(target)) { - dict.put(target, true); - terminals.push(target); - } + // Checks if the terminal is the source of the edge and if the + // target should be stored in the result + if ( + source == terminal && + target != null && + target != terminal && + targets + ) { + if (!dict.get(target)) { + dict.put(target, true); + terminals.push(target); } + } - // Checks if the terminal is the taget of the edge and if the - // source should be stored in the result - else if ( - target == terminal && - source != null && - source != terminal && - sources - ) { - if (!dict.get(source)) { - dict.put(source, true); - terminals.push(source); - } + // Checks if the terminal is the taget of the edge and if the + // source should be stored in the result + else if ( + target == terminal && + source != null && + source != terminal && + sources + ) { + if (!dict.get(source)) { + dict.put(source, true); + terminals.push(source); } } } diff --git a/packages/core/src/view/GraphHandler.js b/packages/core/src/view/GraphHandler.js index d831f70d7..96ae541cd 100644 --- a/packages/core/src/view/GraphHandler.js +++ b/packages/core/src/view/GraphHandler.js @@ -1817,7 +1817,7 @@ class GraphHandler { (state.cell.isEdge() || state.cell.isVertex()) && this.graph.isCellDeletable(state.cell) && state.cell.getChildCount() === 0 && - this.graph.isTransparentState(state) + state.isTransparentState() ); } diff --git a/packages/core/src/view/cell/CellMarker.ts b/packages/core/src/view/cell/CellMarker.ts index d816cc57c..a5c768b36 100644 --- a/packages/core/src/view/cell/CellMarker.ts +++ b/packages/core/src/view/cell/CellMarker.ts @@ -15,7 +15,7 @@ import { import mxCellHighlight from '../selection/mxCellHighlight'; import EventObject from '../event/EventObject'; import InternalEvent from '../event/InternalEvent'; -import utils from '../../util/Utils'; +import utils, { intersectsHotspot } from '../../util/Utils'; import graph from '../Graph'; import { ColorValue } from '../../types'; import CellState from './datatypes/CellState'; @@ -343,8 +343,7 @@ class CellMarker extends EventSource { * returns true, then the state is stored in . The return value * of this method is used as the argument for . */ - // isValidState(state: mxCellState): boolean; - isValidState(state) { + isValidState(state: CellState): boolean { return true; } @@ -354,8 +353,7 @@ class CellMarker extends EventSource { * Returns the valid- or invalidColor depending on the value of isValid. * The given is ignored by this implementation. */ - // getMarkerColor(evt: Event, state: mxCellState, isValid: boolean): string; - getMarkerColor(evt, state, isValid) { + getMarkerColor(evt: Event, state: CellState, isValid: boolean): string { return isValid ? this.validColor : this.invalidColor; } @@ -365,8 +363,7 @@ class CellMarker extends EventSource { * Uses , and to return the * for the given . */ - // getState(me: mxMouseEvent): mxCellState; - getState(me) { + getState(me: InternalMouseEvent): CellState { const view = this.graph.getView(); const cell = this.getCell(me); const state = this.getStateToMark(view.getState(cell)); @@ -380,8 +377,7 @@ class CellMarker extends EventSource { * Returns the for the given event and cell. This returns the * given cell. */ - // getCell(me: mxMouseEvent): mxCell; - getCell(me) { + getCell(me: InternalMouseEvent): Cell { return me.getCell(); } @@ -403,10 +399,9 @@ class CellMarker extends EventSource { * This returns true if the is 0 or the coordinates are inside * the hotspot for the given cell state. */ - // intersects(state: mxCellState, me: mxMouseEvent): boolean; - intersects(state, me) { + intersects(state: CellState, me: InternalMouseEvent): boolean { if (this.hotspotEnabled) { - return utils.intersectsHotspot( + return intersectsHotspot( state, me.getGraphX(), me.getGraphY(), @@ -415,7 +410,6 @@ class CellMarker extends EventSource { MAX_HOTSPOT_SIZE ); } - return true; } diff --git a/packages/core/src/view/cell/CellRenderer.ts b/packages/core/src/view/cell/CellRenderer.ts index 4b4b37e10..dd0e10dd0 100644 --- a/packages/core/src/view/cell/CellRenderer.ts +++ b/packages/core/src/view/cell/CellRenderer.ts @@ -47,7 +47,7 @@ import { SHAPE_SWIMLANE, SHAPE_TRIANGLE, } from '../../util/Constants'; -import utils, { convertPoint, getValue } from '../../util/Utils'; +import utils, {convertPoint, equalPoints, getRotatedPoint, getValue, mod, toRadians} from '../../util/Utils'; import Rectangle from '../geometry/Rectangle'; import StencilRegistry from '../geometry/shape/node/StencilRegistry'; import InternalEvent from '../event/InternalEvent'; @@ -234,9 +234,7 @@ class CellRenderer { */ createIndicatorShape(state: CellState) { if (state.shape) { - state.shape.indicatorShape = this.getShape( - state.view.graph.getIndicatorShape(state) - ); + state.shape.indicatorShape = this.getShape(state.getIndicatorShape()); } } @@ -280,14 +278,12 @@ class CellRenderer { if (shape) { shape.apply(state); - shape.image = state.view.graph.getImage(state); - shape.indicatorColor = state.view.graph.getIndicatorColor(state); + shape.image = state.getImage(); + shape.indicatorColor = state.getIndicatorColor(); shape.indicatorStrokeColor = state.style.indicatorStrokeColor; - shape.indicatorGradientColor = state.view.graph.getIndicatorGradientColor( - state - ); + shape.indicatorGradientColor = state.getIndicatorGradientColor(); shape.indicatorDirection = state.style.indicatorDirection; - shape.indicatorImage = state.view.graph.getIndicatorImage(state); + shape.indicatorImage = state.getIndicatorImage(); this.postConfigureShape(state); } } @@ -300,8 +296,7 @@ class CellRenderer { * This implementation resolves these keywords on the fill, stroke * and gradient color keys. */ - // postConfigureShape(state: mxCellState): void; - postConfigureShape(state: CellState) { + postConfigureShape(state: CellState): void { if (state.shape != null) { this.resolveColor(state, 'indicatorGradientColor', 'gradientColor'); this.resolveColor(state, 'indicatorColor', 'fillColor'); @@ -317,8 +312,7 @@ class CellRenderer { * Resolves special keywords 'inherit', 'indicated' and 'swimlane' and sets * the respective color on the shape. */ - // checkPlaceholderStyles(state: mxCellState): boolean; - checkPlaceholderStyles(state: CellState) { + checkPlaceholderStyles(state: CellState): boolean { // LATER: Check if the color has actually changed if (state.style != null) { const values = ['inherit', 'swimlane', 'indicated']; @@ -339,8 +333,7 @@ class CellRenderer { * Resolves special keywords 'inherit', 'indicated' and 'swimlane' and sets * the respective color on the shape. */ - // resolveColor(state: mxCellState, field: string, key: string): void; - resolveColor(state: CellState, field: string, key: string) { + resolveColor(state: CellState, field: string, key: string): void { const shape = key === 'fontColor' ? state.text : state.shape; if (shape != null) { @@ -365,7 +358,7 @@ class CellRenderer { referenced = state.cell; } - referenced = graph.getSwimlane(referenced); + referenced = graph.swimlane.getSwimlane(referenced); key = graph.swimlaneIndicatorColorAttribute; } else if (value === 'indicated' && state.shape != null) { // @ts-ignore @@ -443,7 +436,7 @@ class CellRenderer { value, new Rectangle(), state.style.align || ALIGN_CENTER, - graph.getVerticalAlign(state), + state.getVerticalAlign(), state.style.fontColor, state.style.fontFamily, state.style.fontSize, @@ -500,7 +493,7 @@ class CellRenderer { state.text.node, (evt: InternalMouseEvent) => { if (this.isLabelEvent(state, evt)) { - graph.fireMouseEvent( + graph.event.fireMouseEvent( InternalEvent.MOUSE_DOWN, new InternalMouseEvent(evt, state) ); @@ -511,7 +504,7 @@ class CellRenderer { }, (evt: InternalMouseEvent) => { if (this.isLabelEvent(state, evt)) { - graph.fireMouseEvent( + graph.event.fireMouseEvent( InternalEvent.MOUSE_MOVE, new InternalMouseEvent(evt, getState(evt)) ); @@ -519,7 +512,7 @@ class CellRenderer { }, (evt: InternalMouseEvent) => { if (this.isLabelEvent(state, evt)) { - graph.fireMouseEvent( + graph.event.fireMouseEvent( InternalEvent.MOUSE_UP, new InternalMouseEvent(evt, getState(evt)) ); @@ -529,10 +522,10 @@ class CellRenderer { ); // Uses double click timeout in mxGraph for quirks mode - if (graph.nativeDblClickEnabled) { + if (graph.event.nativeDblClickEnabled) { InternalEvent.addListener(state.text.node, 'dblclick', (evt: MouseEvent) => { if (this.isLabelEvent(state, evt)) { - graph.dblClick(evt, state.cell); + graph.event.dblClick(evt, state.cell); InternalEvent.consume(evt); } }); @@ -549,8 +542,7 @@ class CellRenderer { * * state - whose label should be initialized. */ - // initializeLabel(state: mxCellState, shape: mxShape): void; - initializeLabel(state: CellState, shape: Shape) { + initializeLabel(state: CellState, shape: Shape): void { if (mxClient.IS_SVG && mxClient.NO_FO && shape.dialect !== DIALECT_SVG) { shape.init(state.view.graph.container); } else { @@ -567,8 +559,7 @@ class CellRenderer { * * state - for which the overlay should be created. */ - // createCellOverlays(state: mxCellState): void; - createCellOverlays(state: CellState) { + createCellOverlays(state: CellState): void { const { graph } = state.view; const overlays = graph.getCellOverlays(state.cell); let dict = null; @@ -623,7 +614,6 @@ class CellRenderer { * state - for which the overlay should be created. * overlay - that represents the overlay. */ - // initializeOverlay(state: mxCellState, overlay: mxImageShape): void; initializeOverlay(state: CellState, overlay: ImageShape): void { overlay.init(state.view.getOverlayPane()); } @@ -634,12 +624,11 @@ class CellRenderer { * Installs the listeners for the given , and * that represents the overlay. */ - // installCellOverlayListeners(state: mxCellState, overlay: mxCellOverlay, shape: mxShape): void; installCellOverlayListeners( state: CellState, overlay: CellOverlay, shape: Shape - ) { + ): void { const { graph } = state.view; InternalEvent.addListener(shape.node, 'click', (evt: Event) => { @@ -680,8 +669,7 @@ class CellRenderer { * * state - for which the control should be created. */ - // createControl(state: mxCellState): void; - createControl(state: CellState) { + createControl(state: CellState): void { const { graph } = state.view; const image = graph.getFoldingImage(state); @@ -714,7 +702,6 @@ class CellRenderer { * * state - whose control click handler should be returned. */ - // createControlClickHandler(state: mxCellState): void; createControlClickHandler(state: CellState): Function { const { graph } = state.view; @@ -784,20 +771,20 @@ class CellRenderer { node, (evt: Event) => { first = new Point(getClientX(evt), getClientY(evt)); - graph.fireMouseEvent( + graph.event.fireMouseEvent( InternalEvent.MOUSE_DOWN, new InternalMouseEvent(evt, state) ); InternalEvent.consume(evt); }, (evt: Event) => { - graph.fireMouseEvent( + graph.event.fireMouseEvent( InternalEvent.MOUSE_MOVE, new InternalMouseEvent(evt, state) ); }, (evt: Event) => { - graph.fireMouseEvent(InternalEvent.MOUSE_UP, new InternalMouseEvent(evt, state)); + graph.event.fireMouseEvent(InternalEvent.MOUSE_UP, new InternalMouseEvent(evt, state)); InternalEvent.consume(evt); } ); @@ -839,8 +826,8 @@ class CellRenderer { * state - whose shape fired the event. * evt - Mouse event which was fired. */ - // isShapeEvent(state: mxCellState, evt: MouseEvent): boolean; - isShapeEvent(state: CellState, evt: InternalMouseEvent | MouseEvent) { + // isShapeEvent(state: mxCellState, evt: MouseEvent); + isShapeEvent(state: CellState, evt: InternalMouseEvent | MouseEvent): boolean { return true; } @@ -856,7 +843,7 @@ class CellRenderer { * evt - Mouse event which was fired. */ // isLabelEvent(state: mxCellState, evt: MouseEvent): boolean; - isLabelEvent(state: CellState, evt: InternalMouseEvent | MouseEvent) { + isLabelEvent(state: CellState, evt: InternalMouseEvent | MouseEvent): boolean { return true; } @@ -996,7 +983,7 @@ class CellRenderer { state.text.apply(state); // Special case where value is obtained via hook in graph - state.text.valign = graph.getVerticalAlign(state); + state.text.valign = state.getVerticalAlign(); } const bounds = this.getLabelBounds(state); @@ -1284,8 +1271,7 @@ class CellRenderer { * * state - whose overlays should be redrawn. */ - // redrawCellOverlays(state: mxCellState, forced?: boolean): void; - redrawCellOverlays(state: CellState, forced: boolean = false) { + redrawCellOverlays(state: CellState, forced: boolean = false): void { this.createCellOverlays(state); if (state.overlays != null) { @@ -1540,7 +1526,6 @@ class CellRenderer { * * state - whose shapes should be returned. */ - // getShapesForState(state: mxCellState): mxShape[]; getShapesForState( state: CellState ): [Shape | null, mxText | null, Shape | null] { @@ -1563,7 +1548,6 @@ class CellRenderer { * be drawn into the DOM. If this is false then redraw and/or reconfigure * will not be called on the shape. */ - // redraw(state: mxCellState, force?: boolean, rendering?: boolean): void; redraw( state: CellState, force: boolean = false, @@ -1587,7 +1571,6 @@ class CellRenderer { * * state - whose label should be redrawn. */ - // redrawShape(state: mxCellState, force?: boolean, rendering?: boolean): void; redrawShape( state: CellState, force: boolean = false, @@ -1645,7 +1628,7 @@ class CellRenderer { if ( state.shape != null && state.shape.indicatorShape != - this.getShape(state.view.graph.getIndicatorShape(state)) + this.getShape(state.getIndicatorShape()) ) { if (state.shape.indicator != null) { state.shape.indicator.destroy(); @@ -1702,8 +1685,7 @@ class CellRenderer { * * Invokes redraw on the shape of the given state. */ - // doRedrawShape(state: mxCellState): void; - doRedrawShape(state: CellState) { + doRedrawShape(state: CellState): void { state.shape?.redraw(); } @@ -1732,8 +1714,7 @@ class CellRenderer { * * state - for which the shapes should be destroyed. */ - // destroy(state: mxCellState): void; - destroy(state: CellState) { + destroy(state: CellState): void { if (state.shape != null) { if (state.text != null) { state.text.destroy(); diff --git a/packages/core/src/view/cell/Cells.ts b/packages/core/src/view/cell/GraphCells.ts similarity index 92% rename from packages/core/src/view/cell/Cells.ts rename to packages/core/src/view/cell/GraphCells.ts index 0fea1a40a..62e688112 100644 --- a/packages/core/src/view/cell/Cells.ts +++ b/packages/core/src/view/cell/GraphCells.ts @@ -33,7 +33,7 @@ import InternalMouseEvent from "../event/InternalMouseEvent"; import Graph from "../Graph"; import CellState from "./datatypes/CellState"; -class Cells { +class GraphCells { constructor(graph: Graph) { this.graph = graph; } @@ -41,6 +41,68 @@ class Cells { graph: Graph; + /** + * Specifies the return value for {@link isCellsResizable}. + * @default true + */ + cellsResizable: boolean = true; + + /** + * Specifies the return value for {@link isCellsBendable}. + * @default true + */ + cellsBendable: boolean = true; + + /** + * Specifies the return value for {@link isCellsSelectable}. + * @default true + */ + cellsSelectable: boolean = true; + + /** + * Specifies the return value for {@link isCellsDisconnectable}. + * @default true + */ + cellsDisconnectable: boolean = true; + + /** + * Specifies if the graph should automatically update the cell size after an + * edit. This is used in {@link isAutoSizeCell}. + * @default false + */ + autoSizeCells: boolean = false; + + /** + * Specifies if autoSize style should be applied when cells are added. + * @default false + */ + autoSizeCellsOnAdd: boolean = false; + + + /** + * Specifies the return value for {@link isCellLocked}. + * @default false + */ + cellsLocked: boolean = false; + + /** + * Specifies the return value for {@link isCellCloneable}. + * @default true + */ + cellsCloneable: boolean = true; + + /** + * Specifies the return value for {@link isCellDeletable}. + * @default true + */ + cellsDeletable: boolean = true; + + /** + * Specifies the return value for {@link isCellMovable}. + * @default true + */ + cellsMovable: boolean = true; + /** * Returns the bounding box for the given array of {@link Cell}. The bounding box for * each cell and its descendants is computed using {@link view.getBoundingBox}. @@ -1235,7 +1297,7 @@ class Cells { geo.x += Math.round((geo.width - size.width) / 2); } - const valign = this.getVerticalAlign(state); + const valign = state.getVerticalAlign(); if (valign === ALIGN_BOTTOM) { geo.y += geo.height - size.height; @@ -1311,7 +1373,7 @@ class Cells { let dy = 0; // Adds dimension of image if shape is a label - if (this.getImage(state) != null || style.image != null) { + if (state.getImage() != null || style.image != null) { if (style.shape === SHAPE_LABEL) { if (style.verticalAlign === ALIGN_MIDDLE) { dx += @@ -2061,14 +2123,6 @@ class Cells { return null; } - /** - * Returns the bounds inside which the diagram should be kept as an - * {@link Rectangle}. - */ - getMaximumGraphBounds(): Rectangle | null { - return this.maximumGraphBounds; - } - /** * Keeps the given cell inside the bounds returned by * {@link getCellContainmentArea} for its parent, according to the rules defined by @@ -2199,9 +2253,6 @@ class Cells { } } - - - /***************************************************************************** * Group: Cell retrieval *****************************************************************************/ @@ -2493,7 +2544,6 @@ class Cells { return false; } - /** * Returns whether or not the specified parent is a valid * ancestor of the specified cell, either direct or indirectly @@ -2800,6 +2850,191 @@ class Cells { setCellsResizable(value: boolean): void { this.cellsResizable = value; } + + /** + * Returns true if the given cell is bendable. This returns {@link cellsBendable} + * for all given cells if {@link isLocked} does not return true for the given + * cell and its style does not specify {@link mxConstants.STYLE_BENDABLE} to be 0. + * + * @param cell {@link mxCell} whose bendable state should be returned. + */ + isCellBendable(cell: Cell): boolean { + const style = this.getCurrentCellStyle(cell); + + return ( + this.isCellsBendable() && + !this.isCellLocked(cell) && + style.bendable !== 0 + ); + } + + /** + * Returns {@link cellsBenadable}. + */ + isCellsBendable(): boolean { + return this.cellsBendable; + } + + /** + * Specifies if the graph should allow bending of edges. This + * implementation updates {@link bendable}. + * + * @param value Boolean indicating if the graph should allow bending of + * edges. + */ + setCellsBendable(value: boolean): void { + this.cellsBendable = value; + } + + /** + * Returns true if the size of the given cell should automatically be + * updated after a change of the label. This implementation returns + * {@link autoSizeCells} or checks if the cell style does specify + * {@link 'autoSize'} to be 1. + * + * @param cell {@link mxCell} that should be resized. + */ + isAutoSizeCell(cell: Cell): boolean { + const style = this.getCurrentCellStyle(cell); + + return this.isAutoSizeCells() || style.autosize == 1; + } + + /** + * Returns {@link autoSizeCells}. + */ + isAutoSizeCells(): boolean { + return this.autoSizeCells; + } + + /** + * Specifies if cell sizes should be automatically updated after a label + * change. This implementation sets {@link autoSizeCells} to the given parameter. + * To update the size of cells when the cells are added, set + * {@link autoSizeCellsOnAdd} to true. + * + * @param value Boolean indicating if cells should be resized + * automatically. + */ + setAutoSizeCells(value: boolean): void { + this.autoSizeCells = value; + } + + /** + * Returns true if the parent of the given cell should be extended if the + * child has been resized so that it overlaps the parent. This + * implementation returns {@link isExtendParents} if the cell is not an edge. + * + * @param cell {@link mxCell} that has been resized. + */ + isExtendParent(cell: Cell): boolean { + return !cell.isEdge() && this.isExtendParents(); + } + + /** + * Returns {@link extendParents}. + */ + isExtendParents(): boolean { + return this.extendParents; + } + + /** + * Sets {@link extendParents}. + * + * @param value New boolean value for {@link extendParents}. + */ + setExtendParents(value: boolean): void { + this.extendParents = value; + } + + /** + * Returns {@link extendParentsOnAdd}. + */ + isExtendParentsOnAdd(cell: Cell): boolean { + return this.extendParentsOnAdd; + } + + /** + * Sets {@link extendParentsOnAdd}. + * + * @param value New boolean value for {@link extendParentsOnAdd}. + */ + setExtendParentsOnAdd(value: boolean): void { + this.extendParentsOnAdd = value; + } + + /** + * Returns {@link extendParentsOnMove}. + */ + isExtendParentsOnMove(): boolean { + return this.extendParentsOnMove; + } + + /** + * Sets {@link extendParentsOnMove}. + * + * @param value New boolean value for {@link extendParentsOnAdd}. + */ + setExtendParentsOnMove(value: boolean): void { + this.extendParentsOnMove = value; + } + + /***************************************************************************** + * Group: Graph appearance + *****************************************************************************/ + + /** + * Returns the cursor value to be used for the CSS of the shape for the + * given cell. This implementation returns null. + * + * @param cell {@link mxCell} whose cursor should be returned. + */ + getCursorForCell(cell: Cell): string | null { + return null; + } + + /***************************************************************************** + * Group: Graph display + *****************************************************************************/ + + /** + * Returns the scaled, translated bounds for the given cell. See + * {@link GraphView.getBounds} for arrays. + * + * @param cell {@link mxCell} whose bounds should be returned. + * @param includeEdges Optional boolean that specifies if the bounds of + * the connected edges should be included. Default is `false`. + * @param includeDescendants Optional boolean that specifies if the bounds + * of all descendants should be included. Default is `false`. + */ + getCellBounds( + cell: Cell, + includeEdges: boolean = false, + includeDescendants: boolean = false + ): Rectangle | null { + let cells = new CellArray(cell); + + // Includes all connected edges + if (includeEdges) { + cells = cells.concat(cell.getEdges()); + } + + let result = this.view.getBounds(cells); + + // Recursively includes the bounds of the children + if (includeDescendants) { + for (const child of cell.getChildren()) { + const tmp = this.getCellBounds(child, includeEdges, true); + + if (result != null) { + result.add(tmp); + } else { + result = tmp; + } + } + } + return result; + } } -export default Cells; +export default GraphCells; diff --git a/packages/core/src/view/cell/datatypes/Cell.ts b/packages/core/src/view/cell/datatypes/Cell.ts index 65c01284d..317474c3c 100644 --- a/packages/core/src/view/cell/datatypes/Cell.ts +++ b/packages/core/src/view/cell/datatypes/Cell.ts @@ -600,7 +600,7 @@ class Cell { * @param defaultValueOptional default value to use if the attribute has no * value. */ - getAttribute(name: string, defaultValue: any): any { + getAttribute(name: string, defaultValue?: any): any { const userObject = this.getValue(); const val = isNotNullish(userObject) && userObject.nodeType === NODETYPE_ELEMENT diff --git a/packages/core/src/view/cell/datatypes/CellState.ts b/packages/core/src/view/cell/datatypes/CellState.ts index 4e90532bf..cbf923270 100644 --- a/packages/core/src/view/cell/datatypes/CellState.ts +++ b/packages/core/src/view/cell/datatypes/CellState.ts @@ -14,6 +14,9 @@ import mxText from '../../geometry/shape/mxText'; import mxDictionary from '../../../util/mxDictionary'; import type { CellStateStyles } from '../../../types'; +import Image from "../../image/Image"; +import {ALIGN_MIDDLE, NONE} from "../../../util/Constants"; +import {getValue} from "../../../util/Utils"; /** * Class: mxCellState @@ -454,6 +457,104 @@ class CellState extends Rectangle { const trg = this.getVisibleTerminalState(false); return src && src === trg; } + + /***************************************************************************** + * Group: Graph appearance + *****************************************************************************/ + + /** + * Returns the vertical alignment for the given cell state. This + * implementation returns the value stored under + * {@link 'verticalAlign'} in the cell style. + * + * @param state {@link mxCellState} whose vertical alignment should be + * returned. + */ + getVerticalAlign(): string | null { + return this.style != null + ? this.style.verticalAlign || ALIGN_MIDDLE + : null; + } + + /** + * Returns true if the given state has no stroke- or fillcolor and no image. + * + * @param state {@link mxCellState} to check. + */ + isTransparentState(): boolean { + let result = false; + const stroke = getValue(this.style, 'strokeColor', NONE); + const fill = getValue(this.style, 'fillColor', NONE); + result = stroke === NONE && fill === NONE && this.getImage(state) == null; + return result; + } + + /** + * Returns the image URL for the given cell state. This implementation + * returns the value stored under {@link 'image'} in the cell + * style. + * + * @param state {@link mxCellState} whose image URL should be returned. + */ + getImage(): Image | null { + return this.style != null + ? this.style.image + : null; + } + + /** + * Returns the indicator color for the given cell state. This + * implementation returns the value stored under + * {@link mxConstants.STYLE_INDICATOR_COLOR} in the cell style. + * + * @param state {@link mxCellState} whose indicator color should be + * returned. + */ + getIndicatorColor(): string | null { + return this.style != null + ? this.style.indicatorColor + : null; + } + + /** + * Returns the indicator gradient color for the given cell state. This + * implementation returns the value stored under + * {@link mxConstants.STYLE_INDICATOR_GRADIENTCOLOR} in the cell style. + * + * @param state {@link mxCellState} whose indicator gradient color should be + * returned. + */ + getIndicatorGradientColor(): string | null { + return this.style != null + ? this.style.gradientColor + : null; + } + + /** + * Returns the indicator shape for the given cell state. This + * implementation returns the value stored under + * {@link mxConstants.STYLE_INDICATOR_SHAPE} in the cell style. + * + * @param state {@link mxCellState} whose indicator shape should be returned. + */ + getIndicatorShape(): string | null { + return this.style != null + ? this.style.indicatorShape + : null; + } + + /** + * Returns the indicator image for the given cell state. This + * implementation returns the value stored under + * {@link mxConstants.STYLE_INDICATOR_IMAGE} in the cell style. + * + * @param state {@link mxCellState} whose indicator image should be returned. + */ + getIndicatorImage(): Image | null { + return this.style != null + ? this.style.indicatorImage + : null; + } } export default CellState; diff --git a/packages/core/src/view/cell/edge/Edge.ts b/packages/core/src/view/cell/edge/Edge.ts index 2a7abb225..29f4c048e 100644 --- a/packages/core/src/view/cell/edge/Edge.ts +++ b/packages/core/src/view/cell/edge/Edge.ts @@ -17,6 +17,66 @@ class Edge { graph: Graph; + /** + * Specifies if edge control points should be reset after the resize of a + * connected cell. + * @default false + */ + resetEdgesOnResize: boolean = false; + + /** + * Specifies if edge control points should be reset after the move of a + * connected cell. + * @default false + */ + resetEdgesOnMove: boolean = false; + + /** + * Specifies if edge control points should be reset after the the edge has been + * reconnected. + * @default true + */ + resetEdgesOnConnect: boolean = true; + + /** + * Specifies if edges are connectable. This overrides the connectable field in edges. + * @default false + */ + connectableEdges: boolean = false; + + /** + * Specifies if edges with disconnected terminals are allowed in the graph. + * @default true + */ + allowDanglingEdges: boolean = true; + + /** + * Specifies if edges that are cloned should be validated and only inserted + * if they are valid. + * @default true + */ + cloneInvalidEdges: boolean = false; + + /** + * Specifies if edges should be disconnected from their terminals when they + * are moved. + * @default true + */ + disconnectOnMove: boolean = true; + + /** + * Specifies the alternate edge style to be used if the main control point + * on an edge is being double clicked. + * @default null + */ + alternateEdgeStyle: string | null = null; + + /** + * Specifies the return value for edges in {@link isLabelMovable}. + * @default true + */ + edgeLabelsMovable: boolean = true; + /***************************************************************************** * Group: Cell alignment and orientation *****************************************************************************/ diff --git a/packages/core/src/view/cell/vertex/Vertex.ts b/packages/core/src/view/cell/vertex/Vertex.ts index 039bdcd4e..8812998da 100644 --- a/packages/core/src/view/cell/vertex/Vertex.ts +++ b/packages/core/src/view/cell/vertex/Vertex.ts @@ -10,6 +10,12 @@ class Vertex { */ vertexLabelsMovable: boolean = false; + /** + * Specifies if negative coordinates for vertices are allowed. + * @default true + */ + allowNegativeCoordinates: boolean = true; + /** * Function: insertVertex * diff --git a/packages/core/src/view/connection/Connections.ts b/packages/core/src/view/connection/GraphConnections.ts similarity index 83% rename from packages/core/src/view/connection/Connections.ts rename to packages/core/src/view/connection/GraphConnections.ts index ec5826f7c..5e902006e 100644 --- a/packages/core/src/view/connection/Connections.ts +++ b/packages/core/src/view/connection/GraphConnections.ts @@ -12,8 +12,9 @@ import InternalEvent from "../event/InternalEvent"; import mxDictionary from "../../util/mxDictionary"; import Geometry from "../geometry/Geometry"; import Graph from "../Graph"; +import mxConnectionHandler from "./mxConnectionHandler"; -class Connections { +class GraphConnections { constructor(graph: Graph) { this.graph = graph; } @@ -580,9 +581,6 @@ class Connections { } } - - - /** * Returns all visible edges connected to the given cell without loops. * @@ -595,11 +593,6 @@ class Connections { return this.getEdges(cell, parent, true, true, false); } - - - - - /** * Returns true if the given cell should be kept inside the bounds of its * parent according to the rules defined by {@link getOverlap} and @@ -643,6 +636,117 @@ class Connections { setConstrainRelativeChildren(value: boolean): void { this.constrainRelativeChildren = value; } + + /***************************************************************************** + * Group: Graph behaviour + *****************************************************************************/ + + /** + * Returns {@link disconnectOnMove} as a boolean. + */ + isDisconnectOnMove(): boolean { + return this.disconnectOnMove; + } + + /** + * Specifies if edges should be disconnected when moved. (Note: Cloned + * edges are always disconnected.) + * + * @param value Boolean indicating if edges should be disconnected + * when moved. + */ + setDisconnectOnMove(value: boolean): void { + this.disconnectOnMove = value; + } + + /** + * Returns true if the given cell is disconnectable from the source or + * target terminal. This returns {@link isCellsDisconnectable} for all given + * cells if {@link isCellLocked} does not return true for the given cell. + * + * @param cell {@link mxCell} whose disconnectable state should be returned. + * @param terminal {@link mxCell} that represents the source or target terminal. + * @param source Boolean indicating if the source or target terminal is to be + * disconnected. + */ + isCellDisconnectable( + cell: Cell, + terminal: Cell | null = null, + source: boolean = false + ): boolean { + return this.isCellsDisconnectable() && !this.isCellLocked(cell); + } + + /** + * Returns {@link cellsDisconnectable}. + */ + isCellsDisconnectable(): boolean { + return this.cellsDisconnectable; + } + + /** + * Sets {@link cellsDisconnectable}. + */ + setCellsDisconnectable(value: boolean): void { + this.cellsDisconnectable = value; + } + + /** + * Returns true if the given cell is a valid source for new connections. + * This implementation returns true for all non-null values and is + * called by is called by {@link isValidConnection}. + * + * @param cell {@link mxCell} that represents a possible source or null. + */ + isValidSource(cell: Cell): boolean { + return ( + (cell == null && this.allowDanglingEdges) || + (cell != null && + (!cell.isEdge() || this.connectableEdges) && + cell.isConnectable()) + ); + } + + /** + * Returns {@link isValidSource} for the given cell. This is called by + * {@link isValidConnection}. + * + * @param cell {@link mxCell} that represents a possible target or null. + */ + isValidTarget(cell: Cell): boolean { + return this.isValidSource(cell); + } + + /** + * Returns true if the given target cell is a valid target for source. + * This is a boolean implementation for not allowing connections between + * certain pairs of vertices and is called by {@link getEdgeValidationError}. + * This implementation returns true if {@link isValidSource} returns true for + * the source and {@link isValidTarget} returns true for the target. + * + * @param source {@link mxCell} that represents the source cell. + * @param target {@link mxCell} that represents the target cell. + */ + isValidConnection(source: Cell, target: Cell): boolean { + return this.isValidSource(source) && this.isValidTarget(target); + } + + /** + * Specifies if the graph should allow new connections. This implementation + * updates {@link mxConnectionHandler.enabled} in {@link connectionHandler}. + * + * @param connectable Boolean indicating if new connections should be allowed. + */ + setConnectable(connectable: boolean): void { + (this.connectionHandler).setEnabled(connectable); + } + + /** + * Returns true if the {@link connectionHandler} is enabled. + */ + isConnectable(): boolean { + return (this.connectionHandler).isEnabled(); + } } -export default Connections; +export default GraphConnections; diff --git a/packages/core/src/view/drag_drop/GraphDragDrop.ts b/packages/core/src/view/drag_drop/GraphDragDrop.ts index 1e4fbfd5b..4542f956b 100644 --- a/packages/core/src/view/drag_drop/GraphDragDrop.ts +++ b/packages/core/src/view/drag_drop/GraphDragDrop.ts @@ -1,4 +1,18 @@ class GraphDragDrop { + /** + * Specifies the return value for {@link isDropEnabled}. + * @default false + */ + dropEnabled: boolean = false; + + /** + * Specifies if dropping onto edges should be enabled. This is ignored if + * {@link dropEnabled} is `false`. If enabled, it will call {@link splitEdge} to carry + * out the drop operation. + * @default true + */ + splitEnabled: boolean = true; + /** * Specifies if the graph should automatically scroll if the mouse goes near * the container edge while dragging. This is only taken into account if the @@ -18,6 +32,29 @@ class GraphDragDrop { * @default true */ autoExtend: boolean = true; + + + /***************************************************************************** + * Group: Graph behaviour + *****************************************************************************/ + + /** + * Returns {@link dropEnabled} as a boolean. + */ + isDropEnabled(): boolean { + return this.dropEnabled; + } + + /** + * Specifies if the graph should allow dropping of cells onto or into other + * cells. + * + * @param dropEnabled Boolean indicating if the graph should allow dropping + * of cells into other cells. + */ + setDropEnabled(value: boolean): void { + this.dropEnabled = value; + } } -export default GraphDragDrop; \ No newline at end of file +export default GraphDragDrop; diff --git a/packages/core/src/view/editing/InPlaceEditing.ts b/packages/core/src/view/editing/InPlaceEditing.ts index 9b0bd011e..a564f0df7 100644 --- a/packages/core/src/view/editing/InPlaceEditing.ts +++ b/packages/core/src/view/editing/InPlaceEditing.ts @@ -13,6 +13,12 @@ class InPlaceEditing { graph: Graph; + /** + * Specifies the return value for {@link isCellEditable}. + * @default true + */ + cellsEditable: boolean = true; + /***************************************************************************** * Group: Cell in-place editing *****************************************************************************/ @@ -158,6 +164,88 @@ class InPlaceEditing { } }); } + + /***************************************************************************** + * Group: Graph behaviour + *****************************************************************************/ + + /** + * Returns true if the given cell is currently being edited. + * If no cell is specified then this returns true if any + * cell is currently being edited. + * + * @param cell {@link mxCell} that should be checked. + */ + isEditing(cell: Cell | null = null): boolean { + if (this.cellEditor != null) { + const editingCell = this.cellEditor.getEditingCell(); + return cell == null ? editingCell != null : cell === editingCell; + } + return false; + } + + /** + * Returns {@link invokesStopCellEditing}. + */ + isInvokesStopCellEditing(): boolean { + return this.invokesStopCellEditing; + } + + /** + * Sets {@link invokesStopCellEditing}. + */ + setInvokesStopCellEditing(value: boolean): void { + this.invokesStopCellEditing = value; + } + + /** + * Returns {@link enterStopsCellEditing}. + */ + isEnterStopsCellEditing(): boolean { + return this.enterStopsCellEditing; + } + + /** + * Sets {@link enterStopsCellEditing}. + */ + setEnterStopsCellEditing(value: boolean): void { + this.enterStopsCellEditing = value; + } + + /** + * Returns true if the given cell is editable. This returns {@link cellsEditable} for + * all given cells if {@link isCellLocked} does not return true for the given cell + * and its style does not specify {@link 'editable'} to be 0. + * + * @param cell {@link mxCell} whose editable state should be returned. + */ + isCellEditable(cell: Cell): boolean { + const style = this.getCurrentCellStyle(cell); + + return ( + this.isCellsEditable() && + !this.isCellLocked(cell) && + style.editable != 0 + ); + } + + /** + * Returns {@link cellsEditable}. + */ + isCellsEditable(): boolean { + return this.cellsEditable; + } + + /** + * Specifies if the graph should allow in-place editing for cell labels. + * This implementation updates {@link cellsEditable}. + * + * @param value Boolean indicating if the graph should allow in-place + * editing. + */ + setCellsEditable(value: boolean): void { + this.cellsEditable = value; + } } export default InPlaceEditing; diff --git a/packages/core/src/view/event/GraphEvents.ts b/packages/core/src/view/event/GraphEvents.ts index 8ccef4aca..8bdd0b191 100644 --- a/packages/core/src/view/event/GraphEvents.ts +++ b/packages/core/src/view/event/GraphEvents.ts @@ -16,7 +16,7 @@ import PanningHandler from "../panning/PanningHandler"; import mxConnectionHandler from "../connection/mxConnectionHandler"; import Point from "../geometry/Point"; import {convertPoint, getValue} from "../../util/Utils"; -import {NONE} from "../../util/Constants"; +import {NONE, SHAPE_SWIMLANE} from "../../util/Constants"; import mxClient from "../../mxClient"; import EventSource from "./EventSource"; import CellEditor from "../editing/CellEditor"; @@ -1091,6 +1091,40 @@ class GraphEvents { p.y = this.snap(p.y / s - tr.y - off); return p; } + + /***************************************************************************** + * Group: Graph behaviour + *****************************************************************************/ + + /** + * Returns {@link escapeEnabled}. + */ + isEscapeEnabled(): boolean { + return this.escapeEnabled; + } + + /** + * Sets {@link escapeEnabled}. + * + * @param enabled Boolean indicating if escape should be enabled. + */ + setEscapeEnabled(value: boolean): void { + this.escapeEnabled = value; + } + + /***************************************************************************** + * Group: Graph appearance + *****************************************************************************/ + + /** + * Returns the cursor value to be used for the CSS of the shape for the + * given event. This implementation calls {@link getCursorForCell}. + * + * @param me {@link mxMouseEvent} whose cursor should be returned. + */ + getCursorForMouseEvent(me: InternalMouseEvent): string | null { + return this.getCursorForCell(me.getCell()); + } } export default GraphEvents; diff --git a/packages/core/src/view/folding/GraphFolding.ts b/packages/core/src/view/folding/GraphFolding.ts index b91a34721..af540b7cc 100644 --- a/packages/core/src/view/folding/GraphFolding.ts +++ b/packages/core/src/view/folding/GraphFolding.ts @@ -47,6 +47,15 @@ class GraphFolding { graph: Graph; options: GraphFoldingOptions; + /** + * Specifies the resource key for the tooltip on the collapse/expand icon. + * If the resource for this key does not exist then the value is used as + * the tooltip. + * @default 'collapse-expand' + */ + collapseExpandResource: string = + mxClient.language != 'none' ? 'collapse-expand' : ''; + /** * * @default true diff --git a/packages/core/src/view/grouping_ordering/GraphGrouping.ts b/packages/core/src/view/grouping_ordering/GraphGrouping.ts index 5f693d95f..17b17d6a0 100644 --- a/packages/core/src/view/grouping_ordering/GraphGrouping.ts +++ b/packages/core/src/view/grouping_ordering/GraphGrouping.ts @@ -385,6 +385,64 @@ class GraphGrouping { }); return cells; } + + /***************************************************************************** + * Group: Drilldown + *****************************************************************************/ + + /** + * Uses the given cell as the root of the displayed cell hierarchy. If no + * cell is specified then the selection cell is used. The cell is only used + * if {@link isValidRoot} returns true. + * + * @param cell Optional {@link Cell} to be used as the new root. Default is the + * selection cell. + */ + enterGroup(cell: Cell): void { + cell = cell || this.getSelectionCell(); + + if (cell != null && this.isValidRoot(cell)) { + this.view.setCurrentRoot(cell); + this.clearSelection(); + } + } + + /** + * Changes the current root to the next valid root in the displayed cell + * hierarchy. + */ + exitGroup(): void { + const root = this.getModel().getRoot(); + const current = this.getCurrentRoot(); + + if (current != null) { + let next = current.getParent(); + + // Finds the next valid root in the hierarchy + while ( + next !== root && + !this.isValidRoot(next) && + next.getParent() !== root + ) { + next = next.getParent(); + } + + // Clears the current root if the new root is + // the model's root or one of the layers. + if (next === root || next.getParent() === root) { + this.view.setCurrentRoot(null); + } else { + this.view.setCurrentRoot(next); + } + + const state = this.view.getState(current); + + // Selects the previous root in the graph + if (state != null) { + this.selection.setSelectionCell(current); + } + } + } } export default GraphGrouping; diff --git a/packages/core/src/view/label/GraphLabel.ts b/packages/core/src/view/label/GraphLabel.ts new file mode 100644 index 000000000..2d0bff57a --- /dev/null +++ b/packages/core/src/view/label/GraphLabel.ts @@ -0,0 +1,163 @@ +import Cell from "../cell/datatypes/Cell"; +import {getValue} from "../../util/Utils"; + +class GraphLabel { + /** + * Returns a string or DOM node that represents the label for the given + * cell. This implementation uses {@link convertValueToString} if {@link labelsVisible} + * is true. Otherwise it returns an empty string. + * + * To truncate a label to match the size of the cell, the following code + * can be used. + * + * ```javascript + * graph.getLabel = function(cell) + * { + * var label = getLabel.apply(this, arguments); + * + * if (label != null && this.model.isVertex(cell)) + * { + * var geo = cell.getCellGeometry(); + * + * if (geo != null) + * { + * var max = parseInt(geo.width / 8); + * + * if (label.length > max) + * { + * label = label.substring(0, max)+'...'; + * } + * } + * } + * return mxUtils.htmlEntities(label); + * } + * ``` + * + * A resize listener is needed in the graph to force a repaint of the label + * after a resize. + * + * ```javascript + * graph.addListener(mxEvent.RESIZE_CELLS, function(sender, evt) + * { + * var cells = evt.getProperty('cells'); + * + * for (var i = 0; i < cells.length; i++) + * { + * this.view.removeState(cells[i]); + * } + * }); + * ``` + * + * @param cell {@link mxCell} whose label should be returned. + */ + getLabel(cell: Cell): string | Node | null { + let result: string | null = ''; + + if (this.labelsVisible && cell != null) { + const style = this.getCurrentCellStyle(cell); + + if (!getValue(style, 'noLabel', false)) { + result = this.convertValueToString(cell); + } + } + return result; + } + + /** + * Returns true if the label must be rendered as HTML markup. The default + * implementation returns {@link htmlLabels}. + * + * @param cell {@link mxCell} whose label should be displayed as HTML markup. + */ + isHtmlLabel(cell: Cell): boolean { + return this.isHtmlLabels(); + } + + /** + * Returns {@link htmlLabels}. + */ + isHtmlLabels(): boolean { + return this.htmlLabels; + } + + /** + * Sets {@link htmlLabels}. + */ + setHtmlLabels(value: boolean): void { + this.htmlLabels = value; + } + + /** + * This enables wrapping for HTML labels. + * + * Returns true if no white-space CSS style directive should be used for + * displaying the given cells label. This implementation returns true if + * {@link 'whiteSpace'} in the style of the given cell is 'wrap'. + * + * This is used as a workaround for IE ignoring the white-space directive + * of child elements if the directive appears in a parent element. It + * should be overridden to return true if a white-space directive is used + * in the HTML markup that represents the given cells label. In order for + * HTML markup to work in labels, {@link isHtmlLabel} must also return true + * for the given cell. + * + * @example + * + * ```javascript + * graph.getLabel = function(cell) + * { + * var tmp = getLabel.apply(this, arguments); // "supercall" + * + * if (this.model.isEdge(cell)) + * { + * tmp = '
'+tmp+'
'; + * } + * + * return tmp; + * } + * + * graph.isWrapping = function(state) + * { + * return this.model.isEdge(state.cell); + * } + * ``` + * + * Makes sure no edge label is wider than 150 pixels, otherwise the content + * is wrapped. Note: No width must be specified for wrapped vertex labels as + * the vertex defines the width in its geometry. + * + * @param state {@link mxCell} whose label should be wrapped. + */ + isWrapping(cell: Cell): boolean { + return this.getCurrentCellStyle(cell).whiteSpace === 'wrap'; + } + + /** + * Returns true if the overflow portion of labels should be hidden. If this + * returns true then vertex labels will be clipped to the size of the vertices. + * This implementation returns true if `overflow` in the + * style of the given cell is 'hidden'. + * + * @param state {@link mxCell} whose label should be clipped. + */ + isLabelClipped(cell: Cell): boolean { + return this.getCurrentCellStyle(cell).overflow === 'hidden'; + } + + /** + * Returns true if the given edges's label is moveable. This returns + * {@link movable} for all given cells if {@link isLocked} does not return true + * for the given cell. + * + * @param cell {@link mxCell} whose label should be moved. + */ + isLabelMovable(cell: Cell): boolean { + return ( + !this.isCellLocked(cell) && + ((cell.isEdge() && this.edgeLabelsMovable) || + (cell.isVertex() && this.vertexLabelsMovable)) + ); + } +} + +export default GraphLabel; diff --git a/packages/core/src/view/page_breaks/GraphPageBreaks.ts b/packages/core/src/view/page_breaks/GraphPageBreaks.ts new file mode 100644 index 000000000..a4e0b563c --- /dev/null +++ b/packages/core/src/view/page_breaks/GraphPageBreaks.ts @@ -0,0 +1,119 @@ +import Rectangle from "../geometry/Rectangle"; +import Point from "../geometry/Point"; +import mxPolyline from "../geometry/shape/edge/mxPolyline"; + +class GraphPageBreaks { + horizontalPageBreaks: any[] | null = null; + verticalPageBreaks: any[] | null = null; + + /** + * Invokes from {@link sizeDidChange} to redraw the page breaks. + * + * @param visible Boolean that specifies if page breaks should be shown. + * @param width Specifies the width of the container in pixels. + * @param height Specifies the height of the container in pixels. + */ + updatePageBreaks(visible: boolean, width: number, height: number): void { + const { scale } = this.view; + const tr = this.getView().translate; + const fmt = this.pageFormat; + const ps = scale * this.pageScale; + const bounds = new Rectangle(0, 0, fmt.width * ps, fmt.height * ps); + + const gb = Rectangle.fromRectangle(this.getGraphBounds()); + gb.width = Math.max(1, gb.width); + gb.height = Math.max(1, gb.height); + + bounds.x = + Math.floor((gb.x - tr.x * scale) / bounds.width) * bounds.width + + tr.x * scale; + bounds.y = + Math.floor((gb.y - tr.y * scale) / bounds.height) * bounds.height + + tr.y * scale; + + gb.width = + Math.ceil((gb.width + (gb.x - bounds.x)) / bounds.width) * bounds.width; + gb.height = + Math.ceil((gb.height + (gb.y - bounds.y)) / bounds.height) * + bounds.height; + + // Does not show page breaks if the scale is too small + visible = + visible && Math.min(bounds.width, bounds.height) > this.minPageBreakDist; + + const horizontalCount = visible + ? Math.ceil(gb.height / bounds.height) + 1 + : 0; + const verticalCount = visible ? Math.ceil(gb.width / bounds.width) + 1 : 0; + const right = (verticalCount - 1) * bounds.width; + const bottom = (horizontalCount - 1) * bounds.height; + + if (this.horizontalPageBreaks == null && horizontalCount > 0) { + this.horizontalPageBreaks = []; + } + + if (this.verticalPageBreaks == null && verticalCount > 0) { + this.verticalPageBreaks = []; + } + + const drawPageBreaks = (breaks: any) => { + if (breaks != null) { + const count = + breaks === this.horizontalPageBreaks + ? horizontalCount + : verticalCount; + + for (let i = 0; i <= count; i += 1) { + const pts = + breaks === this.horizontalPageBreaks + ? [ + new Point( + Math.round(bounds.x), + Math.round(bounds.y + i * bounds.height) + ), + new Point( + Math.round(bounds.x + right), + Math.round(bounds.y + i * bounds.height) + ), + ] + : [ + new Point( + Math.round(bounds.x + i * bounds.width), + Math.round(bounds.y) + ), + new Point( + Math.round(bounds.x + i * bounds.width), + Math.round(bounds.y + bottom) + ), + ]; + + if (breaks[i] != null) { + breaks[i].points = pts; + breaks[i].redraw(); + } else { + const pageBreak = new mxPolyline(pts, this.pageBreakColor); + pageBreak.dialect = this.dialect; + pageBreak.pointerEvents = false; + pageBreak.isDashed = this.pageBreakDashed; + pageBreak.init(this.getView().backgroundPane); + pageBreak.redraw(); + + breaks[i] = pageBreak; + } + } + + for (let i = count; i < breaks.length; i += 1) { + breaks[i].destroy(); + } + + breaks.splice(count, breaks.length - count); + } + }; + + drawPageBreaks(this.horizontalPageBreaks); + drawPageBreaks(this.verticalPageBreaks); + } +} + +export default GraphPageBreaks; + diff --git a/packages/core/src/view/panning/GraphPanning.ts b/packages/core/src/view/panning/GraphPanning.ts index 494c3c7b2..62dde0248 100644 --- a/packages/core/src/view/panning/GraphPanning.ts +++ b/packages/core/src/view/panning/GraphPanning.ts @@ -334,6 +334,21 @@ class GraphPanning { return isChanged; } + + /***************************************************************************** + * Group: Graph behaviour + *****************************************************************************/ + + /** + * Specifies if panning should be enabled. This implementation updates + * {@link PanningHandler.panningEnabled} in {@link panningHandler}. + * + * @param enabled Boolean indicating if panning should be enabled. + */ + setPanning(enabled: boolean): void { + (this.panningHandler).panningEnabled = enabled; + } + } export default GraphPanning; diff --git a/packages/core/src/view/ports/GraphPorts.ts b/packages/core/src/view/ports/GraphPorts.ts new file mode 100644 index 000000000..55ef1d477 --- /dev/null +++ b/packages/core/src/view/ports/GraphPorts.ts @@ -0,0 +1,83 @@ +import Cell from "../cell/datatypes/Cell"; + +class GraphPorts { + /***************************************************************************** + * Group: Drilldown + *****************************************************************************/ + + /** + * Returns true if the given cell is a "port", that is, when connecting to + * it, the cell returned by getTerminalForPort should be used as the + * terminal and the port should be referenced by the ID in either the + * mxConstants.STYLE_SOURCE_PORT or the or the + * mxConstants.STYLE_TARGET_PORT. Note that a port should not be movable. + * This implementation always returns false. + * + * A typical implementation is the following: + * + * ```javascript + * graph.isPort = function(cell) + * { + * var geo = cell.getGeometry(); + * + * return (geo != null) ? geo.relative : false; + * }; + * ``` + * + * @param cell {@link mxCell} that represents the port. + */ + isPort(cell: Cell): boolean { + return false; + } + + /** + * Returns the terminal to be used for a given port. This implementation + * always returns the parent cell. + * + * @param cell {@link mxCell} that represents the port. + * @param source If the cell is the source or target port. + */ + getTerminalForPort(cell: Cell, source: boolean = false): Cell | null { + return cell.getParent(); + } + + /***************************************************************************** + * Group: Graph behaviour + *****************************************************************************/ + + /** + * Returns {@link portsEnabled} as a boolean. + */ + isPortsEnabled(): boolean { + return this.portsEnabled; + } + + /** + * Specifies if the ports should be enabled. + * + * @param value Boolean indicating if the ports should be enabled. + */ + setPortsEnabled(value: boolean): void { + this.portsEnabled = value; + } + + /** + * Returns {@link splitEnabled} as a boolean. + */ + isSplitEnabled(): boolean { + return this.splitEnabled; + } + + /** + * Specifies if the graph should allow dropping of cells onto or into other + * cells. + * + * @param dropEnabled Boolean indicating if the graph should allow dropping + * of cells into other cells. + */ + setSplitEnabled(value: boolean): void { + this.splitEnabled = value; + } +} + +export default GraphPorts; diff --git a/packages/core/src/view/selection/Selection.ts b/packages/core/src/view/selection/Selection.ts index 8a70b7507..5fe83eb5a 100644 --- a/packages/core/src/view/selection/Selection.ts +++ b/packages/core/src/view/selection/Selection.ts @@ -15,6 +15,9 @@ import mxUndoableEdit from "../model/mxUndoableEdit"; import EventObject from "../event/EventObject"; import InternalEvent from "../event/InternalEvent"; import EventSource from "../event/EventSource"; +import mxDictionary from "../../util/mxDictionary"; +import RootChange from "../model/RootChange"; +import ChildChange from "../model/ChildChange"; class Selection extends EventSource { constructor(graph: graph) { @@ -614,88 +617,89 @@ class Selection extends EventSource { *****************************************************************************/ /** - * Creates a new handler for the given cell state. This implementation - * returns a new {@link mxEdgeHandler} of the corresponding cell is an edge, - * otherwise it returns an {@link mxVertexHandler}. + * Function: getSelectionCellsForChanges + * + * Returns the cells to be selected for the given array of changes. + * + * Parameters: + * + * ignoreFn - Optional function that takes a change and returns true if the + * change should be ignored. * - * @param state {@link mxCellState} whose handler should be created. */ - createHandler( - state: CellState | null = null - ): mxEdgeHandler | mxVertexHandler | null { - let result: mxEdgeHandler | mxVertexHandler | null = null; + getSelectionCellsForChanges( + changes: any[], + ignoreFn: Function | null = null + ): CellArray { + const dict = new mxDictionary(); + const cells: CellArray = new CellArray(); - if (state != null) { - if (state.cell.isEdge()) { - const source = state.getVisibleTerminalState(true); - const target = state.getVisibleTerminalState(false); - const geo = (state.cell).getGeometry(); + const addCell = (cell: Cell) => { + if (!dict.get(cell) && this.getModel().contains(cell)) { + if (cell.isEdge() || cell.isVertex()) { + dict.put(cell, true); + cells.push(cell); + } else { + const childCount = cell.getChildCount(); - const edgeStyle = this.graph.getView().getEdgeStyle( - state, - geo != null ? geo.points : null, - source, - target - ); - result = this.createEdgeHandler(state, edgeStyle); - } else { - result = this.createVertexHandler(state); + for (let i = 0; i < childCount; i += 1) { + addCell(cell.getChildAt(i)); + } + } + } + }; + + for (let i = 0; i < changes.length; i += 1) { + const change = changes[i]; + + if ( + change.constructor !== RootChange && + (ignoreFn == null || !ignoreFn(change)) + ) { + let cell = null; + + if (change instanceof ChildChange) { + cell = change.child; + } else if (change.cell != null && change.cell instanceof Cell) { + cell = change.cell; + } + + if (cell != null) { + addCell(cell); + } } } - return result; + return cells; } - /** - * Hooks to create a new {@link mxVertexHandler} for the given {@link CellState}. - * - * @param state {@link mxCellState} to create the handler for. - */ - createVertexHandler(state: CellState): mxVertexHandler { - return new mxVertexHandler(state); - } /** - * Hooks to create a new {@link mxEdgeHandler} for the given {@link CellState}. - * - * @param state {@link mxCellState} to create the handler for. + * Removes selection cells that are not in the model from the selection. */ - createEdgeHandler(state: CellState, edgeStyle: any): mxEdgeHandler { - let result = null; - if ( - edgeStyle == mxEdgeStyle.Loop || - edgeStyle == mxEdgeStyle.ElbowConnector || - edgeStyle == mxEdgeStyle.SideToSide || - edgeStyle == mxEdgeStyle.TopToBottom - ) { - result = this.createElbowEdgeHandler(state); - } else if ( - edgeStyle == mxEdgeStyle.SegmentConnector || - edgeStyle == mxEdgeStyle.OrthConnector - ) { - result = this.createEdgeSegmentHandler(state); - } else { - result = new mxEdgeHandler(state); + updateSelection(): void { + const cells = this.getSelectionCells(); + const removed = new CellArray(); + + for (const cell of cells) { + if (!this.getModel().contains(cell) || !cell.isVisible()) { + removed.push(cell); + } else { + let par = cell.getParent(); + + while (par != null && par !== this.view.currentRoot) { + if (par.isCollapsed() || !par.isVisible()) { + removed.push(cell); + break; + } + + par = par.getParent(); + } + } } - return result; - } - - /** - * Hooks to create a new {@link mxEdgeSegmentHandler} for the given {@link CellState}. - * - * @param state {@link mxCellState} to create the handler for. - */ - createEdgeSegmentHandler(state: CellState): mxEdgeSegmentHandler { - return new mxEdgeSegmentHandler(state); - } - - /** - * Hooks to create a new {@link mxElbowEdgeHandler} for the given {@link CellState}. - * - * @param state {@link mxCellState} to create the handler for. - */ - createElbowEdgeHandler(state: CellState): mxElbowEdgeHandler { - return new mxElbowEdgeHandler(state); + this.selection.removeSelectionCells(removed); } } export default Selection; + + diff --git a/packages/core/src/view/snap/GraphSnap.ts b/packages/core/src/view/snap/GraphSnap.ts new file mode 100644 index 000000000..a806ac588 --- /dev/null +++ b/packages/core/src/view/snap/GraphSnap.ts @@ -0,0 +1,146 @@ +import Point from "../geometry/Point"; +import Rectangle from "../geometry/Rectangle"; + +class GraphSnap { + /** + * Specifies the grid size. + * @default 10 + */ + gridSize: number = 10; + + /** + * Specifies if the grid is enabled. This is used in {@link snap}. + * @default true + */ + gridEnabled: boolean = true; + + /***************************************************************************** + * Group: Graph display + *****************************************************************************/ + + /** + * Snaps the given numeric value to the grid if {@link gridEnabled} is true. + * + * @param value Numeric value to be snapped to the grid. + */ + snap(value: number): number { + if (this.gridEnabled) { + value = Math.round(value / this.gridSize) * this.gridSize; + } + return value; + } + + /** + * Function: snapDelta + * + * Snaps the given delta with the given scaled bounds. + */ + snapDelta( + delta: Point, + bounds: Rectangle, + ignoreGrid: boolean = false, + ignoreHorizontal: boolean = false, + ignoreVertical: boolean = false + ): Point { + const t = this.view.translate; + const s = this.view.scale; + + if (!ignoreGrid && this.gridEnabled) { + const tol = this.gridSize * s * 0.5; + + if (!ignoreHorizontal) { + const tx = bounds.x - (this.snap(bounds.x / s - t.x) + t.x) * s; + + if (Math.abs(delta.x - tx) < tol) { + delta.x = 0; + } else { + delta.x = this.snap(delta.x / s) * s - tx; + } + } + + if (!ignoreVertical) { + const ty = bounds.y - (this.snap(bounds.y / s - t.y) + t.y) * s; + + if (Math.abs(delta.y - ty) < tol) { + delta.y = 0; + } else { + delta.y = this.snap(delta.y / s) * s - ty; + } + } + } else { + const tol = 0.5 * s; + + if (!ignoreHorizontal) { + const tx = bounds.x - (Math.round(bounds.x / s - t.x) + t.x) * s; + + if (Math.abs(delta.x - tx) < tol) { + delta.x = 0; + } else { + delta.x = Math.round(delta.x / s) * s - tx; + } + } + + if (!ignoreVertical) { + const ty = bounds.y - (Math.round(bounds.y / s - t.y) + t.y) * s; + + if (Math.abs(delta.y - ty) < tol) { + delta.y = 0; + } else { + delta.y = Math.round(delta.y / s) * s - ty; + } + } + } + return delta; + } + + /***************************************************************************** + * Group: Graph behaviour + *****************************************************************************/ + + /** + * Returns {@link gridEnabled} as a boolean. + */ + isGridEnabled(): boolean { + return this.gridEnabled; + } + + /** + * Specifies if the grid should be enabled. + * + * @param value Boolean indicating if the grid should be enabled. + */ + setGridEnabled(value: boolean): void { + this.gridEnabled = value; + } + + + /** + * Returns {@link gridSize}. + */ + getGridSize(): number { + return this.gridSize; + } + + /** + * Sets {@link gridSize}. + */ + setGridSize(value: number): void { + this.gridSize = value; + } + + /** + * Returns {@link tolerance}. + */ + getTolerance(): number { + return this.tolerance; + } + + /** + * Sets {@link tolerance}. + */ + setTolerance(value: number): void { + this.tolerance = value; + } +} + +export default GraphSnap; diff --git a/packages/core/src/view/split/GraphSplit.ts b/packages/core/src/view/split/GraphSplit.ts new file mode 100644 index 000000000..4e4f5b5b4 --- /dev/null +++ b/packages/core/src/view/split/GraphSplit.ts @@ -0,0 +1,36 @@ +import Cell from "../cell/datatypes/Cell"; +import CellArray from "../cell/datatypes/CellArray"; +import InternalMouseEvent from "../event/InternalMouseEvent"; + +class GraphSplit { + /** + * Returns true if the given edge may be splitted into two edges with the + * given cell as a new terminal between the two. + * + * @param target {@link mxCell} that represents the edge to be splitted. + * @param cells {@link mxCell} that should split the edge. + * @param evt Mouseevent that triggered the invocation. + */ + // isSplitTarget(target: mxCell, cells: mxCellArray, evt: Event): boolean; + isSplitTarget(target: Cell, cells: CellArray, evt: InternalMouseEvent): boolean { + if ( + target.isEdge() && + cells != null && + cells.length == 1 && + cells[0].isConnectable() && + this.getEdgeValidationError(target, target.getTerminal(true), cells[0]) == + null + ) { + const src = target.getTerminal(true); + const trg = target.getTerminal(false); + + return ( + !cells[0].isAncestor(src) && + !cells[0].isAncestor(trg) + ); + } + return false; + } +} + +export default GraphSplit; diff --git a/packages/core/src/view/swimlane/Swimlane.ts b/packages/core/src/view/swimlane/Swimlane.ts index cd24a14db..3000817fa 100644 --- a/packages/core/src/view/swimlane/Swimlane.ts +++ b/packages/core/src/view/swimlane/Swimlane.ts @@ -1,9 +1,41 @@ import Cell from "../cell/datatypes/Cell"; +import Rectangle from "../geometry/Rectangle"; +import utils, {convertPoint, getValue} from "../../util/Utils"; +import { + DEFAULT_STARTSIZE, + DIRECTION_EAST, + DIRECTION_NORTH, + DIRECTION_SOUTH, + DIRECTION_WEST, SHAPE_SWIMLANE +} from "../../util/Constants"; +import CellArray from "../cell/datatypes/CellArray"; +import InternalMouseEvent from "../event/InternalMouseEvent"; +import {getClientX, getClientY} from "../../util/EventUtils"; class Swimlane { constructor() { } + /** + * Specifies if swimlanes should be selectable via the content if the + * mouse is released. + * @default true + */ + swimlaneSelectionEnabled: boolean = true; + + /** + * Specifies if nesting of swimlanes is allowed. + * @default true + */ + swimlaneNesting: boolean = true; + + /** + * The attribute used to find the color for the indicator if the indicator + * color is set to 'swimlane'. + * @default {@link 'fillColor'} + */ + swimlaneIndicatorColorAttribute: string = 'fillColor'; + /** * Returns the nearest ancestor of the given cell which is a swimlane, or * the given cell, if it is itself a swimlane. @@ -93,6 +125,257 @@ class Swimlane { } return false; } + + /***************************************************************************** + * Group: Graph appearance + *****************************************************************************/ + + /** + * Returns the start size of the given swimlane, that is, the width or + * height of the part that contains the title, depending on the + * horizontal style. The return value is an {@link Rectangle} with either + * width or height set as appropriate. + * + * @param swimlane {@link mxCell} whose start size should be returned. + * @param ignoreState Optional boolean that specifies if cell state should be ignored. + */ + getStartSize(swimlane: Cell, ignoreState: boolean = false): Rectangle { + const result = new Rectangle(); + const style = this.getCurrentCellStyle(swimlane, ignoreState); + const size = parseInt( + getValue(style, 'startSize', DEFAULT_STARTSIZE) + ); + + if (getValue(style, 'horizontal', true)) { + result.height = size; + } else { + result.width = size; + } + return result; + } + + /** + * Returns the direction for the given swimlane style. + */ + getSwimlaneDirection(style: any): string { + const dir = getValue(style, 'direction', DIRECTION_EAST); + const flipH = getValue(style, 'flipH', 0) == 1; + const flipV = getValue(style, 'flipV', 0) == 1; + const h = getValue(style, 'horizontal', true); + let n = h ? 0 : 3; + + if (dir === DIRECTION_NORTH) { + n--; + } else if (dir === DIRECTION_WEST) { + n += 2; + } else if (dir === DIRECTION_SOUTH) { + n += 1; + } + + const mod = utils.mod(n, 2); + + if (flipH && mod === 1) { + n += 2; + } + + if (flipV && mod === 0) { + n += 2; + } + + return [DIRECTION_NORTH, DIRECTION_EAST, DIRECTION_SOUTH, DIRECTION_WEST][ + utils.mod(n, 4) + ]; + } + + /** + * Returns the actual start size of the given swimlane taking into account + * direction and horizontal and vertial flip styles. The start size is + * returned as an {@link Rectangle} where top, left, bottom, right start sizes + * are returned as x, y, height and width, respectively. + * + * @param swimlane {@link mxCell} whose start size should be returned. + * @param ignoreState Optional boolean that specifies if cell state should be ignored. + */ + getActualStartSize( + swimlane: Cell, + ignoreState: boolean = false + ): Rectangle { + const result = new Rectangle(); + + if (this.isSwimlane(swimlane, ignoreState)) { + const style = this.getCurrentCellStyle(swimlane, ignoreState); + const size = parseInt(getValue(style, 'startSize', DEFAULT_STARTSIZE)); + const dir = this.getSwimlaneDirection(style); + + if (dir === DIRECTION_NORTH) { + result.y = size; + } else if (dir === DIRECTION_WEST) { + result.x = size; + } else if (dir === DIRECTION_SOUTH) { + result.height = size; + } else { + result.width = size; + } + } + return result; + } + + /** + * Returns true if the given cell is a swimlane in the graph. A swimlane is + * a container cell with some specific behaviour. This implementation + * checks if the shape associated with the given cell is a {@link mxSwimlane}. + * + * @param cell {@link mxCell} to be checked. + * @param ignoreState Optional boolean that specifies if the cell state should be ignored. + */ + isSwimlane(cell: Cell, ignoreState: boolean = false): boolean { + if ( + cell != null && + cell.getParent() !== this.getModel().getRoot() && + !cell.isEdge() + ) { + return ( + this.getCurrentCellStyle(cell, ignoreState).shape === + SHAPE_SWIMLANE + ); + } + return false; + } + + /***************************************************************************** + * Group: Graph behaviour + *****************************************************************************/ + + /** + * Returns true if the given cell is a valid drop target for the specified + * cells. If {@link splitEnabled} is true then this returns {@link isSplitTarget} for + * the given arguments else it returns true if the cell is not collapsed + * and its child count is greater than 0. + * + * @param cell {@link mxCell} that represents the possible drop target. + * @param cells {@link mxCell} that should be dropped into the target. + * @param evt Mouseevent that triggered the invocation. + */ + // isValidDropTarget(cell: mxCell, cells: mxCellArray, evt: Event): boolean; + isValidDropTarget(cell: Cell, cells: CellArray, evt: InternalMouseEvent): boolean { + return ( + cell != null && + ((this.isSplitEnabled() && this.isSplitTarget(cell, cells, evt)) || + (!cell.isEdge() && + (this.isSwimlane(cell) || + (cell.getChildCount() > 0 && !cell.isCollapsed())))) + ); + } + + /** + * Returns the given cell if it is a drop target for the given cells or the + * nearest ancestor that may be used as a drop target for the given cells. + * If the given array contains a swimlane and {@link swimlaneNesting} is false + * then this always returns null. If no cell is given, then the bottommost + * swimlane at the location of the given event is returned. + * + * This function should only be used if {@link isDropEnabled} returns true. + * + * @param cells Array of {@link Cell} which are to be dropped onto the target. + * @param evt Mouseevent for the drag and drop. + * @param cell {@link mxCell} that is under the mousepointer. + * @param clone Optional boolean to indicate of cells will be cloned. + */ + // getDropTarget(cells: mxCellArray, evt: Event, cell: mxCell, clone?: boolean): mxCell; + getDropTarget( + cells: CellArray, + evt: InternalMouseEvent, + cell: Cell | null = null, + clone: boolean = false + ): Cell | null { + if (!this.isSwimlaneNesting()) { + for (let i = 0; i < cells.length; i += 1) { + if (this.isSwimlane(cells[i])) { + return null; + } + } + } + + const pt = convertPoint( + this.container, + getClientX(evt), + getClientY(evt) + ); + pt.x -= this.panDx; + pt.y -= this.panDy; + const swimlane = this.getSwimlaneAt(pt.x, pt.y); + + if (cell == null) { + cell = swimlane; + } else if (swimlane != null) { + // Checks if the cell is an ancestor of the swimlane + // under the mouse and uses the swimlane in that case + let tmp = swimlane.getParent(); + + while (tmp != null && this.isSwimlane(tmp) && tmp != cell) { + tmp = tmp.getParent(); + } + + if (tmp == cell) { + cell = swimlane; + } + } + + while ( + cell != null && + !this.isValidDropTarget(cell, cells, evt) && + !this.getModel().isLayer(cell) + ) { + cell = cell.getParent(); + } + + // Checks if parent is dropped into child if not cloning + if (!clone) { + let parent = cell; + while (parent != null && cells.indexOf(parent) < 0) { + parent = parent.getParent(); + } + } + + return !this.getModel().isLayer(cell) && parent == null + ? cell + : null; + } + + /** + * Returns {@link swimlaneNesting} as a boolean. + */ + isSwimlaneNesting(): boolean { + return this.swimlaneNesting; + } + + /** + * Specifies if swimlanes can be nested by drag and drop. This is only + * taken into account if dropEnabled is true. + * + * @param value Boolean indicating if swimlanes can be nested. + */ + setSwimlaneNesting(value: boolean): void { + this.swimlaneNesting = value; + } + + /** + * Returns {@link swimlaneSelectionEnabled} as a boolean. + */ + isSwimlaneSelectionEnabled(): boolean { + return this.swimlaneSelectionEnabled; + } + + /** + * Specifies if swimlanes should be selected if the mouse is released + * over their content area. + * + * @param value Boolean indicating if swimlanes content areas + * should be selected when the mouse is released over them. + */ + setSwimlaneSelectionEnabled(value: boolean): void { + this.swimlaneSelectionEnabled = value; + } } export default Swimlane; diff --git a/packages/core/src/view/tooltip/GraphTooltip.ts b/packages/core/src/view/tooltip/GraphTooltip.ts new file mode 100644 index 000000000..0275447a1 --- /dev/null +++ b/packages/core/src/view/tooltip/GraphTooltip.ts @@ -0,0 +1,127 @@ +import CellState from "../cell/datatypes/CellState"; +import {htmlEntities} from "../../util/StringUtils"; +import Resources from "../../util/Resources"; +import Shape from "../geometry/shape/Shape"; +import mxSelectionCellsHandler from "../selection/mxSelectionCellsHandler"; +import Cell from "../cell/datatypes/Cell"; +import TooltipHandler from "../popups_menus/TooltipHandler"; + +class GraphTooltip { + /** + * Returns the string or DOM node that represents the tooltip for the given + * state, node and coordinate pair. This implementation checks if the given + * node is a folding icon or overlay and returns the respective tooltip. If + * this does not result in a tooltip, the handler for the cell is retrieved + * from {@link selectionCellsHandler} and the optional getTooltipForNode method is + * called. If no special tooltip exists here then {@link getTooltipForCell} is used + * with the cell in the given state as the argument to return a tooltip for the + * given state. + * + * @param state {@link mxCellState} whose tooltip should be returned. + * @param node DOM node that is currently under the mouse. + * @param x X-coordinate of the mouse. + * @param y Y-coordinate of the mouse. + */ + // getTooltip(state: mxCellState, node: Node, x: number, y: number): string; + getTooltip( + state: CellState, + node: HTMLElement, + x: number, + y: number + ): string | null { + let tip: string | null = null; + + if (state != null) { + // Checks if the mouse is over the folding icon + if ( + state.control != null && + // @ts-ignore + (node === state.control.node || node.parentNode === state.control.node) + ) { + tip = this.collapseExpandResource; + tip = htmlEntities(Resources.get(tip) || tip, true).replace( + /\\n/g, + '
' + ); + } + + if (tip == null && state.overlays != null) { + state.overlays.visit((id: string, shape: Shape) => { + // LATER: Exit loop if tip is not null + if ( + tip == null && + // @ts-ignore + (node === shape.node || node.parentNode === shape.node) + ) { + // @ts-ignore + tip = shape.overlay.toString(); + } + }); + } + + if (tip == null) { + const handler = (( + this.selectionCellsHandler + )).getHandler(state.cell); + if ( + handler != null && + typeof handler.getTooltipForNode === 'function' + ) { + tip = handler.getTooltipForNode(node); + } + } + + if (tip == null) { + tip = this.getTooltipForCell(state.cell); + } + } + return tip; + } + + /** + * Returns the string or DOM node to be used as the tooltip for the given + * cell. This implementation uses the cells getTooltip function if it + * exists, or else it returns {@link convertValueToString} for the cell. + * + * @example + * + * ```javascript + * graph.getTooltipForCell = function(cell) + * { + * return 'Hello, World!'; + * } + * ``` + * + * Replaces all tooltips with the string Hello, World! + * + * @param cell {@link mxCell} whose tooltip should be returned. + */ + getTooltipForCell(cell: Cell): string | null { + let tip = null; + + if (cell != null && 'getTooltip' in cell) { + // @ts-ignore + tip = cell.getTooltip(); + } else { + tip = this.convertValueToString(cell); + } + return tip; + } + + /***************************************************************************** + * Group: Graph behaviour + *****************************************************************************/ + + /** + * Specifies if tooltips should be enabled. This implementation updates + * {@link TooltipHandler.enabled} in {@link tooltipHandler}. + * + * @param enabled Boolean indicating if tooltips should be enabled. + */ + setTooltips(enabled: boolean): void { + (this.tooltipHandler).setEnabled(enabled); + } + +} + +export default GraphTooltip; diff --git a/packages/core/src/view/zoom/GraphZoom.ts b/packages/core/src/view/zoom/GraphZoom.ts new file mode 100644 index 000000000..ed30121c4 --- /dev/null +++ b/packages/core/src/view/zoom/GraphZoom.ts @@ -0,0 +1,217 @@ +import Rectangle from "../geometry/Rectangle"; +import {hasScrollbars} from "../../util/Utils"; + +class GraphZoom { + /** + * Specifies the factor used for {@link zoomIn} and {@link zoomOut}. + * @default 1.2 (120%) + */ + zoomFactor: number = 1.2; + + /** + * Specifies if the viewport should automatically contain the selection cells after a zoom operation. + * @default false + */ + keepSelectionVisibleOnZoom: boolean = false; + + /** + * Specifies if the zoom operations should go into the center of the actual + * diagram rather than going from top, left. + * @default true + */ + centerZoom: boolean = true; + + /***************************************************************************** + * Group: Graph display + *****************************************************************************/ + + /** + * Zooms into the graph by {@link zoomFactor}. + */ + zoomIn(): void { + this.zoom(this.zoomFactor); + } + + /** + * Zooms out of the graph by {@link zoomFactor}. + */ + zoomOut(): void { + this.zoom(1 / this.zoomFactor); + } + + /** + * Resets the zoom and panning in the view. + */ + zoomActual(): void { + if (this.view.scale === 1) { + this.view.setTranslate(0, 0); + } else { + this.view.translate.x = 0; + this.view.translate.y = 0; + + this.view.setScale(1); + } + } + + /** + * Zooms the graph to the given scale with an optional boolean center + * argument, which is passd to {@link zoom}. + */ + zoomTo(scale: number, center: boolean = false): void { + this.zoom(scale / this.view.scale, center); + } + + /** + * Zooms the graph using the given factor. Center is an optional boolean + * argument that keeps the graph scrolled to the center. If the center argument + * is omitted, then {@link centerZoom} will be used as its value. + */ + zoom(factor: number, center: boolean = this.centerZoom): void { + const scale = Math.round(this.view.scale * factor * 100) / 100; + const state = this.view.getState(this.getSelectionCell()); + const container = this.container; + factor = scale / this.view.scale; + + if (this.keepSelectionVisibleOnZoom && state != null) { + const rect = new Rectangle( + state.x * factor, + state.y * factor, + state.width * factor, + state.height * factor + ); + + // Refreshes the display only once if a scroll is carried out + this.view.scale = scale; + + if (!this.scrollRectToVisible(rect)) { + this.view.revalidate(); + + // Forces an event to be fired but does not revalidate again + this.view.setScale(scale); + } + } else { + const _hasScrollbars = hasScrollbars(this.container); + + if (center && !_hasScrollbars) { + let dx = container.offsetWidth; + let dy = container.offsetHeight; + + if (factor > 1) { + const f = (factor - 1) / (scale * 2); + dx *= -f; + dy *= -f; + } else { + const f = (1 / factor - 1) / (this.view.scale * 2); + dx *= f; + dy *= f; + } + + this.view.scaleAndTranslate( + scale, + this.view.translate.x + dx, + this.view.translate.y + dy + ); + } else { + // Allows for changes of translate and scrollbars during setscale + const tx = this.view.translate.x; + const ty = this.view.translate.y; + const sl = container.scrollLeft; + const st = container.scrollTop; + + this.view.setScale(scale); + + if (_hasScrollbars) { + let dx = 0; + let dy = 0; + + if (center) { + dx = (container.offsetWidth * (factor - 1)) / 2; + dy = (container.offsetHeight * (factor - 1)) / 2; + } + + container.scrollLeft = + (this.view.translate.x - tx) * this.view.scale + + Math.round(sl * factor + dx); + container.scrollTop = + (this.view.translate.y - ty) * this.view.scale + + Math.round(st * factor + dy); + } + } + } + } + + /** + * Zooms the graph to the specified rectangle. If the rectangle does not have same aspect + * ratio as the display container, it is increased in the smaller relative dimension only + * until the aspect match. The original rectangle is centralised within this expanded one. + * + * Note that the input rectangular must be un-scaled and un-translated. + * + * @param rect The un-scaled and un-translated rectangluar region that should be just visible + * after the operation + */ + zoomToRect(rect: Rectangle): void { + const container = this.container; + const scaleX = container.clientWidth / rect.width; + const scaleY = container.clientHeight / rect.height; + const aspectFactor = scaleX / scaleY; + + // Remove any overlap of the rect outside the client area + rect.x = Math.max(0, rect.x); + rect.y = Math.max(0, rect.y); + let rectRight = Math.min(container.scrollWidth, rect.x + rect.width); + let rectBottom = Math.min(container.scrollHeight, rect.y + rect.height); + rect.width = rectRight - rect.x; + rect.height = rectBottom - rect.y; + + // The selection area has to be increased to the same aspect + // ratio as the container, centred around the centre point of the + // original rect passed in. + if (aspectFactor < 1.0) { + // Height needs increasing + const newHeight = rect.height / aspectFactor; + const deltaHeightBuffer = (newHeight - rect.height) / 2.0; + rect.height = newHeight; + + // Assign up to half the buffer to the upper part of the rect, not crossing 0 + // put the rest on the bottom + const upperBuffer = Math.min(rect.y, deltaHeightBuffer); + rect.y -= upperBuffer; + + // Check if the bottom has extended too far + rectBottom = Math.min(container.scrollHeight, rect.y + rect.height); + rect.height = rectBottom - rect.y; + } else { + // Width needs increasing + const newWidth = rect.width * aspectFactor; + const deltaWidthBuffer = (newWidth - rect.width) / 2.0; + rect.width = newWidth; + + // Assign up to half the buffer to the upper part of the rect, not crossing 0 + // put the rest on the bottom + const leftBuffer = Math.min(rect.x, deltaWidthBuffer); + rect.x -= leftBuffer; + + // Check if the right hand side has extended too far + rectRight = Math.min(container.scrollWidth, rect.x + rect.width); + rect.width = rectRight - rect.x; + } + + const scale = container.clientWidth / rect.width; + const newScale = this.view.scale * scale; + + if (!hasScrollbars(this.container)) { + this.view.scaleAndTranslate( + newScale, + this.view.translate.x - rect.x / this.view.scale, + this.view.translate.y - rect.y / this.view.scale + ); + } else { + this.view.setScale(newScale); + container.scrollLeft = Math.round(rect.x * scale); + container.scrollTop = Math.round(rect.y * scale); + } + } +} + +export default GraphZoom; diff --git a/packages/html/stories/Animation.stories.js b/packages/html/stories/Animation.stories.js index ce045542e..1bbd44769 100644 --- a/packages/html/stories/Animation.stories.js +++ b/packages/html/stories/Animation.stories.js @@ -33,6 +33,17 @@ const Template = ({ label, ...args }) => { 'shape=cylinder;strokeWidth=2;fillColor=#ffffff;strokeColor=black;' + 'gradientColor=#a0a0a0;fontColor=black;fontStyle=1;spacingTop=14;'; + /*const vertexStyle = { + shape: 'cylinder', + strokeWidth: 2, + fillColor: '#ffffff', + strokeColor: 'black', + gradientColor: '#a0a0a0', + fontColor: 'black', + fontStyle: 1, + spacingTop: 14, + };*/ + let e1; graph.batchUpdate(() => { const v1 = graph.insertVertex({ diff --git a/packages/html/stories/Clipboard.stories.js b/packages/html/stories/Clipboard.stories.js index 546922cd0..a1a4792b0 100644 --- a/packages/html/stories/Clipboard.stories.js +++ b/packages/html/stories/Clipboard.stories.js @@ -265,7 +265,7 @@ const Template = ({ label, ...args }) => { } // Standard paste via control-v - if (xml.substring(0, 14) === '') { + if (xml.substring(0, 14) === '') { graph.setSelectionCells(importXml(xml, dx, dy)); graph.scrollCellToVisible(graph.getSelectionCell()); } diff --git a/packages/html/stories/Collapse.stories.js b/packages/html/stories/Collapse.stories.js index b112acb5e..c26885f37 100644 --- a/packages/html/stories/Collapse.stories.js +++ b/packages/html/stories/Collapse.stories.js @@ -27,7 +27,7 @@ const Template = ({ label, ...args }) => { const parent = graph.getDefaultParent(); const getStyle = function() { - // Extends mxGraphModel.getStyle to show an image when collapsed + // Extends Transactions.getStyle to show an image when collapsed // TODO cannot use super without a parent class // let style = super.getStyle(); let style = ''; diff --git a/packages/html/stories/Drop.stories.js b/packages/html/stories/Drop.stories.js index 8c5f6798c..5f35f444f 100644 --- a/packages/html/stories/Drop.stories.js +++ b/packages/html/stories/Drop.stories.js @@ -112,7 +112,7 @@ function handleDrop(graph, file, x, y) { if (file.type.substring(0, 9) === 'image/svg') { const comma = data.indexOf(','); const svgText = atob(data.substring(comma + 1)); - const root = mxUtils.parseXml(svgText); + const root = utils.parseXml(svgText); // Parses SVG to find width and height if (root != null) { @@ -144,7 +144,7 @@ function handleDrop(graph, file, x, y) { h = Math.max(1, Math.round(h)); data = `data:image/svg+xml,${btoa( - mxUtils.getXml(svgs[0], '\n') + utils.getXml(svgs[0], '\n') )}`; graph.insertVertex({ position: [x, y], diff --git a/packages/html/stories/Events.stories.js b/packages/html/stories/Events.stories.js index 19721edab..d3269a2e2 100644 --- a/packages/html/stories/Events.stories.js +++ b/packages/html/stories/Events.stories.js @@ -158,16 +158,16 @@ function createPopupMenu(graph, menu, cell, evt) { // Function to create the entries in the popupmenu if (cell != null) { menu.addItem('Cell Item', '/images/image.gif', () => { - mxUtils.alert('MenuItem1'); + utils.alert('MenuItem1'); }); } else { menu.addItem('No-Cell Item', '/images/image.gif', () => { - mxUtils.alert('MenuItem2'); + utils.alert('MenuItem2'); }); } menu.addSeparator(); menu.addItem('MenuItem3', '/images/warning.gif', () => { - mxUtils.alert(`MenuItem3: ${graph.getSelectionCount()} selected`); + utils.alert(`MenuItem3: ${graph.getSelectionCount()} selected`); }); } diff --git a/packages/html/stories/FileIO.stories.js b/packages/html/stories/FileIO.stories.js index 53ceab6c8..91558a46e 100644 --- a/packages/html/stories/FileIO.stories.js +++ b/packages/html/stories/FileIO.stories.js @@ -34,7 +34,7 @@ const Template = ({ label, ...args }) => { if (!mxClient.isBrowserSupported()) { // Displays an error message if the browser is // not supported. - mxUtils.error('Browser is not supported!', 200, false); + utils.error('Browser is not supported!', 200, false); } else { // Creates the graph inside the given container const graph = new mxGraph(container); @@ -50,7 +50,7 @@ const Template = ({ label, ...args }) => { // Changes the default vertex style in-place let style = graph.getStylesheet().getDefaultVertexStyle(); style.shape = mxConstants.SHAPE_ROUNDED; - style.perimiter = mxPerimeter.RectanglePerimeter; + style.perimiter = Perimeter.RectanglePerimeter; style.gradientColor = 'white'; style.perimeterSpacing = 4; style.shadow = true; @@ -58,7 +58,7 @@ const Template = ({ label, ...args }) => { style = graph.getStylesheet().getDefaultEdgeStyle(); style.labelBackgroundColor = 'white'; - style = mxUtils.clone(style); + style = utils.clone(style); style.startArrow = mxConstants.ARROW_CLASSIC; graph.getStylesheet().putCellStyle('2way', style); @@ -78,7 +78,7 @@ const Template = ({ label, ...args }) => { // Adds a button to execute the layout this.el2.appendChild( - mxUtils.button('Arrange', function(evt) { + utils.button('Arrange', function(evt) { const parent = graph.getDefaultParent(); layout.execute(parent); }) @@ -105,7 +105,7 @@ const Template = ({ label, ...args }) => { } graph.dblClick = function(evt, cell) { - const mxe = new mxEventObject( + const mxe = new EventObject( mxEvent.DOUBLE_CLICK, 'event', evt, @@ -120,7 +120,7 @@ const Template = ({ label, ...args }) => { !mxe.isConsumed() && cell != null ) { - mxUtils.alert( + utils.alert( `Show properties for cell ${cell.customId || cell.getId()}` ); } @@ -136,7 +136,7 @@ const Template = ({ label, ...args }) => { // is normally the first child of the root (ie. layer 0). const parent = graph.getDefaultParent(); - const req = mxUtils.load(filename); + const req = utils.load(filename); const text = req.getText(); const lines = text.split('\n'); @@ -192,7 +192,7 @@ const Template = ({ label, ...args }) => { // Parses the mxGraph XML file format function read(graph, filename) { - const req = mxUtils.load(filename); + const req = utils.load(filename); const root = req.getDocumentElement(); const dec = new mxCodec(root.ownerDocument); diff --git a/packages/html/stories/FixedPoints.stories.js b/packages/html/stories/FixedPoints.stories.js index cd3fbac44..c652e4294 100644 --- a/packages/html/stories/FixedPoints.stories.js +++ b/packages/html/stories/FixedPoints.stories.js @@ -37,7 +37,7 @@ const Template = ({ label, ...args }) => { // Snaps to fixed points intersects(icon, point, source, existingEdge) { return ( - !source || existingEdge || mxUtils.intersects(icon.bounds, point) + !source || existingEdge || utils.intersects(icon.bounds, point) ); } } diff --git a/packages/html/stories/HelloPort.stories.js b/packages/html/stories/HelloPort.stories.js index 64a519fdf..6e63aefeb 100644 --- a/packages/html/stories/HelloPort.stories.js +++ b/packages/html/stories/HelloPort.stories.js @@ -107,7 +107,7 @@ const Template = ({ label, ...args }) => { const button = mxDomHelpers.button('View XML', function() { const encoder = new mxCodec(); const node = encoder.encode(graph.getModel()); - mxUtils.popup(mxUtils.getPrettyXml(node), true); + utils.popup(utils.getPrettyXml(node), true); }); controller.appendChild(button); diff --git a/packages/html/stories/Monitor.stories.js b/packages/html/stories/Monitor.stories.js index 816e1c0c9..6832d8974 100644 --- a/packages/html/stories/Monitor.stories.js +++ b/packages/html/stories/Monitor.stories.js @@ -40,7 +40,7 @@ const Template = ({ label, ...args }) => { // Creates a process display using the activity names as IDs to refer to the elements const xml = - '' + + '' + '' + '' + '' + @@ -77,7 +77,7 @@ const Template = ({ label, ...args }) => { '' + '' + '' + - ''; + ''; const doc = mxXmlUtils.parseXml(xml); const codec = new mxCodec(doc); codec.decode(doc.documentElement, graph.getModel()); @@ -173,7 +173,7 @@ const Template = ({ label, ...args }) => { * which is being displayed on click. */ function createOverlay(image, tooltip) { - const overlay = new mxCellOverlay(image, tooltip); + const overlay = new CellOverlay(image, tooltip); // Installs a handler for clicks on the overlay overlay.addListener(mxEvent.CLICK, function(sender, evt) { diff --git a/packages/html/stories/OrgChart.stories.js b/packages/html/stories/OrgChart.stories.js index 2083626b4..fe531188c 100644 --- a/packages/html/stories/OrgChart.stories.js +++ b/packages/html/stories/OrgChart.stories.js @@ -232,7 +232,7 @@ const Template = ({ label, ...args }) => { }); tb.addItem('Print', 'images/print32.png', function(evt) { - const preview = new mxPrintPreview(graph, 1); + const preview = new PrintPreview(graph, 1); preview.open(); }); @@ -241,7 +241,7 @@ const Template = ({ label, ...args }) => { if (pageCount != null) { const scale = mxUtils.getScaleForPageCount(pageCount, graph); - const preview = new mxPrintPreview(graph, scale); + const preview = new PrintPreview(graph, scale); preview.open(); } }); @@ -285,7 +285,7 @@ const Template = ({ label, ...args }) => { menu.addSeparator(); menu.addItem('Print', '/images/print.gif', function() { - const preview = new mxPrintPreview(graph, 1); + const preview = new PrintPreview(graph, 1); preview.open(); }); @@ -294,7 +294,7 @@ const Template = ({ label, ...args }) => { if (pageCount != null) { const scale = mxUtils.getScaleForPageCount(pageCount, graph); - const preview = new mxPrintPreview(graph, scale); + const preview = new PrintPreview(graph, scale); preview.open(); } }); diff --git a/packages/html/stories/PageBreaks.stories.js b/packages/html/stories/PageBreaks.stories.js index 613a3e741..8d62c1388 100644 --- a/packages/html/stories/PageBreaks.stories.js +++ b/packages/html/stories/PageBreaks.stories.js @@ -128,7 +128,7 @@ const Template = ({ label, ...args }) => { const x0 = Math.floor(bounds.x / pf.width) * pf.width; const y0 = Math.floor(bounds.y / pf.height) * pf.height; - const preview = new mxPrintPreview(graph, scale, pf, 0, -x0, -y0); + const preview = new PrintPreview(graph, scale, pf, 0, -x0, -y0); preview.marginTop = headerSize * scale * graph.pageScale; preview.marginBottom = footerSize * scale * graph.pageScale; preview.autoOrigin = false; @@ -153,11 +153,11 @@ const Template = ({ label, ...args }) => { const footer = header.cloneNode(true); - mxUtils.write(header, `Page ${pageNumber} - Header`); + utils.write(header, `Page ${pageNumber} - Header`); header.style.borderBottom = '1px solid gray'; header.style.top = '0px'; - mxUtils.write(footer, `Page ${pageNumber} - Footer`); + utils.write(footer, `Page ${pageNumber} - Footer`); footer.style.borderTop = '1px solid gray'; footer.style.bottom = '0px';