UNPKG

@maxgraph/core

Version:

maxGraph is a fully client side JavaScript diagramming library that uses SVG and HTML for rendering.

1,058 lines (1,054 loc) 44 kB
"use strict"; /* Copyright 2021-present The maxGraph project Contributors Copyright (c) 2006-2015, JGraph Ltd Copyright (c) 2006-2015, Gaudenz Alder Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AbstractGraph = void 0; const ImageBox_js_1 = __importDefault(require("./image/ImageBox.js")); const EventObject_js_1 = __importDefault(require("./event/EventObject.js")); const EventSource_js_1 = __importDefault(require("./event/EventSource.js")); const InternalEvent_js_1 = __importDefault(require("./event/InternalEvent.js")); const Rectangle_js_1 = __importDefault(require("./geometry/Rectangle.js")); const Client_js_1 = __importDefault(require("../Client.js")); const styleUtils_js_1 = require("../util/styleUtils.js"); const Cell_js_1 = __importDefault(require("./cell/Cell.js")); const Constants_js_1 = require("../util/Constants.js"); const ChildChange_js_1 = __importDefault(require("./undoable_changes/ChildChange.js")); const GeometryChange_js_1 = __importDefault(require("./undoable_changes/GeometryChange.js")); const RootChange_js_1 = __importDefault(require("./undoable_changes/RootChange.js")); const StyleChange_js_1 = __importDefault(require("./undoable_changes/StyleChange.js")); const TerminalChange_js_1 = __importDefault(require("./undoable_changes/TerminalChange.js")); const ValueChange_js_1 = __importDefault(require("./undoable_changes/ValueChange.js")); const domUtils_js_1 = require("../util/domUtils.js"); const builtin_style_elements_js_1 = require("./style/builtin-style-elements.js"); const EdgeStyleRegistry_js_1 = require("./style/edge/EdgeStyleRegistry.js"); const EdgeHandler_js_1 = __importDefault(require("./handler/EdgeHandler.js")); const VertexHandler_js_1 = __importDefault(require("./handler/VertexHandler.js")); const EdgeSegmentHandler_js_1 = __importDefault(require("./handler/EdgeSegmentHandler.js")); const ElbowEdgeHandler_js_1 = __importDefault(require("./handler/ElbowEdgeHandler.js")); const _graph_mixins_apply_js_1 = require("./mixins/_graph-mixins-apply.js"); const utils_js_1 = require("../internal/utils.js"); const i18n_utils_js_1 = require("../internal/i18n-utils.js"); /** * Extends {@link EventSource} to implement a graph component for the browser. This is the entry point class of the package. * * To activate panning and connections use {@link setPanning} and {@link setConnectable}. * For rubberband selection you must create a new instance of {@link rubberband}. * * The following listeners are added to {@link mouseListeners} by default: * * - tooltipHandler: {@link TooltipHandler} that displays tooltips * - panningHandler: {@link PanningHandler} for panning and popup menus * - connectionHandler: {@link ConnectionHandler} for creating connections * - selectionHandler: {@link SelectionHandler} for moving and cloning cells * * These listeners will be called in the above order if they are enabled. * * @category Graph */ class AbstractGraph extends EventSource_js_1.default { // =================================================================================================================== // Group: "Create Class Instance" factory functions. // These can be overridden in subclasses to allow the Graph to instantiate user-defined implementations with custom behavior. // Notice that the methods will be moved as part of https://github.com/maxGraph/maxGraph/issues/762 // =================================================================================================================== /** * Hooks to create a new {@link EdgeHandler} for the given {@link CellState}. * * @param state {@link CellState} to create the handler for. */ createEdgeHandlerInstance(state) { // Note this method not being called createEdgeHandler to keep compatibility // with older code which overrides/calls createEdgeHandler return new EdgeHandler_js_1.default(state); } /** * Hooks to create a new {@link EdgeSegmentHandler} for the given {@link CellState}. * * @param state {@link CellState} to create the handler for. */ createEdgeSegmentHandler(state) { return new EdgeSegmentHandler_js_1.default(state); } /** * Hooks to create a new {@link ElbowEdgeHandler} for the given {@link CellState}. * * @param state {@link CellState} to create the handler for. */ createElbowEdgeHandler(state) { return new ElbowEdgeHandler_js_1.default(state); } /** * Hooks to create a new {@link VertexHandler} for the given {@link CellState}. * * @param state {@link CellState} to create the handler for. */ createVertexHandler(state) { return new VertexHandler_js_1.default(state); } // =================================================================================================================== // Group: Main graph constructor and functions // =================================================================================================================== /** * Convenient hook method that can be used to register global styles and shapes using the related global registries. * * While registration can also be done outside of this class (as it applies globally), * implementing it here makes the registration process transparent to the caller of this class. * * Subclasses can override this method to register custom defaults. */ registerDefaults() { // do nothing, it's the purpose of this class not to load defaults. } constructor(options) { super(); this.destroyed = false; this.graphModelChangeListener = null; this.paintBackground = null; this.isConstrainedMoving = false; // =================================================================================================================== // Group: Variables (that maybe should be in the mixins, but need to be created for each new class instance) // =================================================================================================================== this.cells = []; this.imageBundles = []; /** * Holds the mouse event listeners. See {@link fireMouseEvent}. */ this.mouseListeners = []; /** * An array of {@link Multiplicity} describing the allowed connections in a graph. */ this.multiplicities = []; this.plugins = new Map(); /** * RenderHint as it was passed to the constructor. */ this.renderHint = null; /** * Dialect to be used for drawing the graph. */ this.dialect = 'svg'; /** * Value returned by {@link getOverlap} if {@link isAllowOverlapParent} returns * `true` for the given cell. {@link getOverlap} is used in {@link constrainChild} if * {@link isConstrainChild} returns `true`. The value specifies the * portion of the child which is allowed to overlap the parent. */ this.defaultOverlap = 0.5; /** * Specifies the default parent to be used to insert new cells. * This is used in {@link getDefaultParent}. * @default null */ this.defaultParent = null; /** * Specifies the {@link Image} to be returned by {@link getBackgroundImage}. * @default null * * @example * ```javascript * var img = new mxImage('http://www.example.com/maps/examplemap.jpg', 1024, 768); * graph.setBackgroundImage(img); * graph.view.validate(); * ``` */ this.backgroundImage = null; /** * Specifies if the background page should be visible. * Not yet implemented. * @default false */ this.pageVisible = false; /** * Specifies if a dashed line should be drawn between multiple pages. * If you change this value while a graph is being displayed then you * should call {@link sizeDidChange} to force an update of the display. * @default false */ this.pageBreaksVisible = false; /** * Specifies the color for page breaks. * @default gray */ this.pageBreakColor = 'gray'; /** * Specifies the page breaks should be dashed. * @default true */ this.pageBreakDashed = true; /** * Specifies the minimum distance in pixels for page breaks to be visible. * @default 20 */ this.minPageBreakDist = 20; /** * Specifies if the graph size should be rounded to the next page number in * {@link sizeDidChange}. This is only used if the graph container has scrollbars. * @default false */ this.preferPageSize = false; /** * Specifies the page format for the background page. * This is used as the default in {@link PrintPreview} and for painting the background page * if {@link pageVisible} is `true` and the page breaks if {@link pageBreaksVisible} is `true`. * @default {@link PAGE_FORMAT_A4_PORTRAIT} */ this.pageFormat = new Rectangle_js_1.default(...Constants_js_1.PAGE_FORMAT_A4_PORTRAIT); /** * Specifies the scale of the background page. * Not yet implemented. * @default 1.5 */ this.pageScale = 1.5; /** * Specifies the return value for {@link isEnabled}. * @default true */ this.enabled = true; /** * Specifies the return value for {@link canExportCell}. * @default true */ this.exportEnabled = true; /** * Specifies the return value for {@link canImportCell}. * @default true */ this.importEnabled = true; /** * Specifies if the graph should automatically scroll regardless of the * scrollbars. This will scroll the container using positive values for * scroll positions (ie usually only rightwards and downwards). To avoid * possible conflicts with panning, set {@link translateToScrollPosition} to `true`. */ this.ignoreScrollbars = false; /** * Specifies if the graph should automatically convert the current scroll * position to a translate in the graph view when a mouseUp event is received. * This can be used to avoid conflicts when using {@link autoScroll} and * {@link ignoreScrollbars} with no scrollbars in the container. */ this.translateToScrollPosition = false; /** * {@link Rectangle} that specifies the area in which all cells in the diagram * should be placed. Uses in {@link getMaximumGraphBounds}. Use a width or height of * `0` if you only want to give a upper, left corner. */ this.maximumGraphBounds = null; /** * {@link Rectangle} that specifies the minimum size of the graph. This is ignored * if the graph container has no scrollbars. * @default null */ this.minimumGraphSize = null; /** * {@link Rectangle} that specifies the minimum size of the {@link container} if * {@link resizeContainer} is `true`. */ this.minimumContainerSize = null; /** * {@link Rectangle} that specifies the maximum size of the container if * {@link resizeContainer} is `true`. */ this.maximumContainerSize = null; /** * Specifies if the container should be resized to the graph size when * the graph size has changed. * @default false */ this.resizeContainer = false; /** * Border to be added to the bottom and right side when the container is * being resized after the graph has been changed. * @default 0 */ this.border = 0; /** * Specifies if edges should appear in the foreground regardless of their order * in the model. If {@link keepEdgesInForeground} and {@link keepEdgesInBackground} are * both `true` then the normal order is applied. * @default false */ this.keepEdgesInForeground = false; /** * Specifies if edges should appear in the background regardless of their order * in the model. If {@link keepEdgesInForeground} and {@link keepEdgesInBackground} are * both `true` then the normal order is applied. * @default false */ this.keepEdgesInBackground = false; /** * Specifies the return value for {@link isRecursiveResize}. * @default false (for backwards compatibility) */ this.recursiveResize = false; /** * Specifies if the scale and translate should be reset if the root changes in * the model. * @default true */ this.resetViewOnRootChange = true; /** * Specifies if loops (aka self-references) are allowed. * @default false */ this.allowLoops = false; /** * {@link EdgeStyle} to be used for loops. * * This is a fallback for loops if the {@link CellStateStyle.loopStyle} is `undefined`. * @default {@link EdgeStyle.Loop} */ this.defaultLoopStyle = builtin_style_elements_js_1.EdgeStyle.Loop; /** * Specifies if multiple edges in the same direction between the same pair of * vertices are allowed. * @default true */ this.multigraph = true; /** * Specifies the {@link Image} for the image to be used to display a warning * overlay. See {@link setCellWarning}. Default value is Client.imageBasePath + * '/warning'. The extension for the image depends on the platform. It is * '.png' on the Mac and '.gif' on all other platforms. */ this.warningImage = new ImageBox_js_1.default(`${Client_js_1.default.imageBasePath}/warning${Client_js_1.default.IS_MAC ? '.png' : '.gif'}`, 16, 16); /** * Specifies the resource key for the error message to be displayed in * non-multigraphs when two vertices are already connected. If the resource * for this key does not exist then the value is used as the error message. * @default 'alreadyConnected' */ this.alreadyConnectedResource = (0, i18n_utils_js_1.isI18nEnabled)() ? 'alreadyConnected' : ''; /** * Specifies the resource key for the warning message to be displayed when * a collapsed cell contains validation errors. If the resource for this * key does not exist then the value is used as the warning message. * @default 'containsValidationErrors' */ this.containsValidationErrorsResource = (0, i18n_utils_js_1.isI18nEnabled)() ? 'containsValidationErrors' : ''; /** Folding options. */ this.options = { foldingEnabled: true, collapsedImage: new ImageBox_js_1.default(`${Client_js_1.default.imageBasePath}/collapsed.gif`, 9, 9), expandedImage: new ImageBox_js_1.default(`${Client_js_1.default.imageBasePath}/expanded.gif`, 9, 9), collapseToPreferredSize: true, }; this.getContainer = () => this.container; this.getPlugin = (id) => this.plugins.get(id); this.getCellRenderer = () => this.cellRenderer; this.getDialect = () => this.dialect; this.isPageVisible = () => this.pageVisible; this.isPageBreaksVisible = () => this.pageBreaksVisible; this.getPageBreakColor = () => this.pageBreakColor; this.isPageBreakDashed = () => this.pageBreakDashed; this.getMinPageBreakDist = () => this.minPageBreakDist; this.isPreferPageSize = () => this.preferPageSize; this.getPageFormat = () => this.pageFormat; this.getPageScale = () => this.pageScale; this.isExportEnabled = () => this.exportEnabled; this.isImportEnabled = () => this.importEnabled; this.isIgnoreScrollbars = () => this.ignoreScrollbars; this.isTranslateToScrollPosition = () => this.translateToScrollPosition; this.getMinimumGraphSize = () => this.minimumGraphSize; this.setMinimumGraphSize = (size) => (this.minimumGraphSize = size); this.getMinimumContainerSize = () => this.minimumContainerSize; this.setMinimumContainerSize = (size) => (this.minimumContainerSize = size); this.getAlreadyConnectedResource = () => this.alreadyConnectedResource; this.getContainsValidationErrorsResource = () => this.containsValidationErrorsResource; this.registerDefaults(); this.container = options?.container ?? document.createElement('div'); // collaborators this.initializeCollaborators(options); // Adds a graph model listener to update the view this.graphModelChangeListener = (_sender, evt) => { this.graphModelChanged(evt.getProperty('edit').changes); }; this.getDataModel().addListener(InternalEvent_js_1.default.CHANGE, this.graphModelChangeListener); // Initializes the container using the view this.view.init(); // Updates the size of the container for the current graph this.sizeDidChange(); // Initializes plugins options?.plugins?.forEach((p) => this.plugins.set(p.pluginId, new p(this))); this.view.revalidate(); } getWarningImage() { return this.warningImage; } /** * Updates the model in a transaction. * * @param fn the update to be performed in the transaction. * * @see {@link GraphDataModel.batchUpdate} */ batchUpdate(fn) { this.getDataModel().batchUpdate(fn); } /** * Returns the {@link GraphDataModel} that contains the cells. */ getDataModel() { return this.model; } /** * Returns the {@link GraphView} that contains the {@link CellState}s. */ getView() { return this.view; } /** * Returns the {@link Stylesheet} that defines the style. */ getStylesheet() { return this.stylesheet; } /** * Sets the {@link Stylesheet} that defines the style. */ setStylesheet(stylesheet) { this.stylesheet = stylesheet; } /** * Called when the graph model changes. Invokes {@link processChange} on each * item of the given array to update the view accordingly. * * @param changes Array that contains the individual changes. */ graphModelChanged(changes) { for (const change of changes) { this.processChange(change); } this.updateSelection(); this.view.validate(); this.sizeDidChange(); } /** * 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 * model. * * @param {(RootChange|ChildChange|TerminalChange|GeometryChange|ValueChange|StyleChange)} change - Object that represents the change on the model. */ processChange(change) { // Resets the view settings, removes all cells and clears // the selection if the root changes. if (change instanceof RootChange_js_1.default) { this.clearSelection(); this.setDefaultParent(null); if (change.previous) this.removeStateForCell(change.previous); if (this.resetViewOnRootChange) { this.view.scale = 1; this.view.translate.x = 0; this.view.translate.y = 0; } this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.ROOT)); } // Adds or removes a child to the view by online invaliding // the minimal required portions of the cache, namely, the // old and new parent and the child. else if (change instanceof ChildChange_js_1.default) { const newParent = change.child.getParent(); this.view.invalidate(change.child, true, true); if (!newParent || !this.getDataModel().contains(newParent) || newParent.isCollapsed()) { this.view.invalidate(change.child, true, true); this.removeStateForCell(change.child); // Handles special case of current root of view being removed if (this.view.currentRoot == change.child) { this.home(); } } if (newParent != change.previous) { // Refreshes the collapse/expand icons on the parents if (newParent != null) { this.view.invalidate(newParent, false, false); } if (change.previous != null) { this.view.invalidate(change.previous, false, false); } } } // Handles two special cases where the shape does not need to be // recreated from scratch, it only needs to be invalidated. else if (change instanceof TerminalChange_js_1.default || change instanceof GeometryChange_js_1.default) { // Checks if the geometry has changed to avoid unnessecary revalidation if (change instanceof TerminalChange_js_1.default || (change.previous == null && change.geometry != null) || (change.previous != null && !change.previous.equals(change.geometry))) { 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_js_1.default) { this.view.invalidate(change.cell, false, false); } // Requires a new mxShape in JavaScript else if (change instanceof StyleChange_js_1.default) { this.view.invalidate(change.cell, true, true); const state = this.view.getState(change.cell); if (state != null) { state.invalidStyle = true; } } // Removes the state from the cache by default else if (change.cell != null && change.cell instanceof Cell_js_1.default) { this.removeStateForCell(change.cell); } } /** * Scrolls the graph to the given point, extending the graph container if * specified. */ scrollPointToVisible(x, y, extend = false, border = 20) { const panningHandler = this.getPlugin('PanningHandler'); if (!this.isTimerAutoScroll() && (this.ignoreScrollbars || (0, styleUtils_js_1.hasScrollbars)(this.container))) { const c = this.container; if (x >= c.scrollLeft && y >= c.scrollTop && x <= c.scrollLeft + c.clientWidth && y <= c.scrollTop + c.clientHeight) { let dx = c.scrollLeft + c.clientWidth - x; if (dx < border) { const old = c.scrollLeft; c.scrollLeft += border - dx; // Automatically extends the canvas size to the bottom, right // if the event is outside of the canvas and the edge of the // canvas has been reached. Notes: Needs fix for IE. if (extend && old === c.scrollLeft) { // @ts-ignore const root = this.view.getDrawPane().ownerSVGElement; const width = c.scrollWidth + border - dx; // Updates the clipping region. This is an expensive // operation that should not be executed too often. // @ts-ignore root.style.width = `${width}px`; c.scrollLeft += border - dx; } } else { dx = x - c.scrollLeft; if (dx < border) { c.scrollLeft -= border - dx; } } let dy = c.scrollTop + c.clientHeight - y; if (dy < border) { const old = c.scrollTop; c.scrollTop += border - dy; if (old == c.scrollTop && extend) { // @ts-ignore const root = this.view.getDrawPane().ownerSVGElement; const height = c.scrollHeight + border - dy; // Updates the clipping region. This is an expensive // operation that should not be executed too often. // @ts-ignore root.style.height = `${height}px`; c.scrollTop += border - dy; } } else { dy = y - c.scrollTop; if (dy < border) { c.scrollTop -= border - dy; } } } } else if (this.isAllowAutoPanning() && panningHandler && !panningHandler.isActive()) { panningHandler.getPanningManager().panTo(x + this.getPanDx(), y + this.getPanDy()); } } /** * Returns the size of the border and padding on all four sides of the * container. The left, top, right and bottom borders are stored in the x, y, * width and height of the returned {@link Rectangle}, respectively. */ getBorderSizes() { const css = (0, styleUtils_js_1.getCurrentStyle)(this.container); return new Rectangle_js_1.default((0, styleUtils_js_1.parseCssNumber)(css.paddingLeft) + (css.borderLeftStyle != 'none' ? (0, styleUtils_js_1.parseCssNumber)(css.borderLeftWidth) : 0), (0, styleUtils_js_1.parseCssNumber)(css.paddingTop) + (css.borderTopStyle != 'none' ? (0, styleUtils_js_1.parseCssNumber)(css.borderTopWidth) : 0), (0, styleUtils_js_1.parseCssNumber)(css.paddingRight) + (css.borderRightStyle != 'none' ? (0, styleUtils_js_1.parseCssNumber)(css.borderRightWidth) : 0), (0, styleUtils_js_1.parseCssNumber)(css.paddingBottom) + (css.borderBottomStyle != 'none' ? (0, styleUtils_js_1.parseCssNumber)(css.borderBottomWidth) : 0)); } /** * Returns the preferred size of the background page if {@link preferPageSize} is true. */ getPreferredPageSize(bounds, width, height) { const tr = this.view.translate; const fmt = this.pageFormat; const ps = this.pageScale; const page = new Rectangle_js_1.default(0, 0, Math.ceil(fmt.width * ps), Math.ceil(fmt.height * ps)); const hCount = this.pageBreaksVisible ? Math.ceil(width / page.width) : 1; const vCount = this.pageBreaksVisible ? Math.ceil(height / page.height) : 1; return new Rectangle_js_1.default(0, 0, hCount * page.width + 2 + tr.x, vCount * page.height + 2 + tr.y); } /** * Resizes the container for the given graph width and height. */ doResizeContainer(width, height) { if (this.maximumContainerSize != null) { width = Math.min(this.maximumContainerSize.width, width); height = Math.min(this.maximumContainerSize.height, height); } const container = this.container; container.style.width = `${Math.ceil(width)}px`; container.style.height = `${Math.ceil(height)}px`; } /***************************************************************************** * Group: UNCLASSIFIED *****************************************************************************/ /** * Creates a new handler for the given cell state. This implementation * returns a new {@link EdgeHandler} of the corresponding cell is an edge, * otherwise it returns an {@link VertexHandler}. * * @param state {@link CellState} whose handler should be created. */ createHandler(state) { let result = null; if (state.cell.isEdge()) { const source = state.getVisibleTerminalState(true); const target = state.getVisibleTerminalState(false); const geo = state.cell.getGeometry(); const edgeStyle = this.getView().getEdgeStyle(state, geo ? geo.points || undefined : undefined, source, target); result = this.createEdgeHandler(state, edgeStyle); } else { result = this.createVertexHandler(state); } return result; } /** * Hooks to create a new {@link EdgeHandler} for the given {@link CellState}. * * This method relies on the registered elements in {@link EdgeStyleRegistry} to know which {@link EdgeHandler} to create. * If the {@link EdgeStyle} is not registered, it will return a default {@link EdgeHandler}. * * @param state {@link CellState} to create the handler for. * @param edgeStyle the {@link EdgeStyleFunction} that let choose the actual edge handler. */ createEdgeHandler(state, edgeStyle) { const handlerKind = EdgeStyleRegistry_js_1.EdgeStyleRegistry.getHandlerKind(edgeStyle); switch (handlerKind) { case 'elbow': return this.createElbowEdgeHandler(state); case 'segment': return this.createEdgeSegmentHandler(state); } return this.createEdgeHandlerInstance(state); } /***************************************************************************** * Group: Drill down *****************************************************************************/ /** * Returns the current root of the displayed cell hierarchy. This is a * shortcut to {@link GraphView.currentRoot} in {@link GraphView}. */ getCurrentRoot() { return this.view.currentRoot; } /** * Returns the translation to be used if the given cell is the root cell as * an {@link Point}. This implementation returns null. * * To keep the children at their absolute position while stepping into groups, * this function can be overridden as follows. * * @example * ```javascript * var offset = new mxPoint(0, 0); * * while (cell != null) * { * var geo = this.model.getGeometry(cell); * * if (geo != null) * { * offset.x -= geo.x; * offset.y -= geo.y; * } * * cell = this.model.getParent(cell); * } * * return offset; * ``` * * @param cell {@link Cell} that represents the root. */ getTranslateForRoot(cell) { return null; } /** * Returns the offset to be used for the cells inside the given cell. The * root and layer cells may be identified using {@link GraphDataModel.isRoot} and * {@link GraphDataModel.isLayer}. For all other current roots, the * {@link GraphView.currentRoot} field points to the respective cell, so that * the following holds: cell == this.view.currentRoot. This implementation * returns null. * * @param cell {@link Cell} whose offset should be returned. */ getChildOffsetForCell(cell) { return null; } /** * Uses the root of the model as the root of the displayed cell hierarchy * and selects the previous root. */ home() { const current = this.getCurrentRoot(); if (current != null) { this.view.setCurrentRoot(null); const state = this.view.getState(current); if (state != null) { this.setSelectionCell(current); } } } /** * Returns true if the given cell is a valid root for the cell display * hierarchy. This implementation returns true for all non-null values. * * @param cell {@link Cell} which should be checked as a possible root. */ isValidRoot(cell) { return !!cell; } /***************************************************************************** * Group: Graph display *****************************************************************************/ /** * Returns the bounds of the visible graph. Shortcut to * {@link GraphView.getGraphBounds}. See also: {@link getBoundingBoxFromGeometry}. */ getGraphBounds() { return this.view.getGraphBounds(); } /** * Returns the bounds inside which the diagram should be kept as an * {@link Rectangle}. */ getMaximumGraphBounds() { return this.maximumGraphBounds; } /** * Clears all cell states or the states for the hierarchy starting at the * given cell and validates the graph. This fires a refresh event as the * last step. * * @param cell Optional {@link Cell} for which the cell states should be cleared. */ refresh(cell = null) { if (cell) { this.view.clear(cell, false); } else { this.view.clear(undefined, true); } this.view.validate(); this.sizeDidChange(); this.fireEvent(new EventObject_js_1.default(InternalEvent_js_1.default.REFRESH)); } /** * Centers the graph in the container. * * @param horizontal Optional boolean that specifies if the graph should be centered * horizontally. Default is `true`. * @param vertical Optional boolean that specifies if the graph should be centered * vertically. Default is `true`. * @param cx Optional float that specifies the horizontal center. Default is `0.5`. * @param cy Optional float that specifies the vertical center. Default is `0.5`. */ center(horizontal = true, vertical = true, cx = 0.5, cy = 0.5) { const container = this.container; const _hasScrollbars = (0, styleUtils_js_1.hasScrollbars)(this.container); const padding = 2 * this.getBorder(); const cw = container.clientWidth - padding; const ch = container.clientHeight - padding; const bounds = this.getGraphBounds(); 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.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); } else { bounds.x -= t.x; bounds.y -= t.y; const sw = container.scrollWidth; const sh = container.scrollHeight; if (sw > cw) { dx = 0; } if (sh > ch) { dy = 0; } this.view.setTranslate(Math.floor(dx / 2 - bounds.x), Math.floor(dy / 2 - bounds.y)); container.scrollLeft = (sw - cw) / 2; container.scrollTop = (sh - ch) / 2; } } /** * Returns `true` if perimeter points should be computed such that the resulting edge has only horizontal or vertical segments. * * This method relies on the registered elements in {@link EdgeStyleRegistry} to know if the {@link CellStateStyle.edgeStyle} of the {@link CellState} is orthogonal. * If the {@link EdgeStyle} is not registered, it is considered as NOT orthogonal. * * @param edge {@link CellState} that represents the edge. */ isOrthogonal(edge) { const orthogonal = edge.style.orthogonal; if (!(0, utils_js_1.isNullish)(orthogonal)) { return orthogonal; } // fallback when the orthogonal style is not defined const edgeStyle = this.view.getEdgeStyle(edge); return EdgeStyleRegistry_js_1.EdgeStyleRegistry.isOrthogonal(edgeStyle); } /***************************************************************************** * Group: Graph appearance *****************************************************************************/ /** * Returns the {@link backgroundImage} as an {@link Image}. */ getBackgroundImage() { return this.backgroundImage; } /** * Sets the new {@link backgroundImage}. * * @param image New {@link Image} to be used for the background. */ setBackgroundImage(image) { this.backgroundImage = image; } /** * Returns the textual representation for the given cell. * * This implementation returns the node name or string-representation of the user object. * * * The following returns the label attribute from the cells user object if it is an XML node. * * @example * ```javascript * graph.convertValueToString = function(cell) * { * return cell.getAttribute('label'); * } * ``` * * See also: {@link cellLabelChanged}. * * @param cell {@link Cell} whose textual representation should be returned. */ convertValueToString(cell) { const value = cell.getValue(); if (value != null) { if ((0, domUtils_js_1.isNode)(value)) { return value.nodeName; } if (typeof value.toString === 'function') { return value.toString(); } } return ''; } /** * Returns the string to be used as the link for the given cell. * * This implementation returns null. * * @param cell {@link Cell} whose link should be returned. */ getLinkForCell(cell) { return null; } /** * Returns the value of {@link border}. */ getBorder() { return this.border; } /** * Sets the value of {@link border}. * * @param value Positive integer that represents the border to be used. */ setBorder(value) { this.border = value; } /***************************************************************************** * Group: Graph behaviour *****************************************************************************/ /** * Returns {@link resizeContainer}. */ isResizeContainer() { return this.resizeContainer; } /** * Sets {@link resizeContainer}. * * @param value Boolean indicating if the container should be resized. */ setResizeContainer(value) { this.resizeContainer = value; } /** * Returns true if the graph is {@link enabled}. */ isEnabled() { return this.enabled; } /** * Specifies if the graph should allow any interactions. This * implementation updates {@link enabled}. * * @param value Boolean indicating if the graph should be enabled. */ setEnabled(value) { this.enabled = value; } /** * Returns {@link multigraph} as a boolean. */ isMultigraph() { return this.multigraph; } /** * Specifies if the graph should allow multiple connections between the * same pair of vertices. * * @param value Boolean indicating if the graph allows multiple connections * between the same pair of vertices. */ setMultigraph(value) { this.multigraph = value; } /** * Returns {@link allowLoops} as a boolean. */ isAllowLoops() { return this.allowLoops; } /** * Specifies if loops are allowed. * * @param value Boolean indicating if loops are allowed. */ setAllowLoops(value) { this.allowLoops = value; } /** * Returns {@link recursiveResize}. * * @param state {@link CellState} that is being resized. */ isRecursiveResize(state = null) { return this.recursiveResize; } /** * Sets {@link recursiveResize}. * * @param value New boolean value for {@link recursiveResize}. */ setRecursiveResize(value) { this.recursiveResize = value; } /** * Returns a decimal number representing the amount of the width and height * of the given cell that is allowed to overlap its parent. A value of 0 * means all children must stay inside the parent, 1 means the child is * allowed to be placed outside of the parent such that it touches one of * the parents sides. If {@link isAllowOverlapParent} returns false for the given * cell, then this method returns 0. * * @param cell {@link Cell} for which the overlap ratio should be returned. */ getOverlap(cell) { return this.isAllowOverlapParent(cell) ? this.defaultOverlap : 0; } /** * Returns true if the given cell is allowed to be placed outside the * parents area. * * @param cell {@link Cell} that represents the child to be checked. */ isAllowOverlapParent(cell) { return false; } /***************************************************************************** * Group: Cell retrieval *****************************************************************************/ /** * Returns {@link defaultParent} or {@link GraphView.currentRoot} or the first child * of {@link GraphDataModel.root} if both are null. The value returned by * this function should be used as the parent for new cells (aka default * layer). */ getDefaultParent() { let parent = this.getCurrentRoot(); if (!parent) { parent = this.defaultParent; if (!parent) { const root = this.getDataModel().getRoot(); parent = root.getChildAt(0); } } return parent; } /** * Sets the {@link defaultParent} to the given cell. Set this to null to return * the first child of the root in getDefaultParent. */ setDefaultParent(cell) { this.defaultParent = cell; } /** * Destroys the graph and all its resources. */ destroy() { if (!this.destroyed) { this.destroyed = true; Object.values(this.plugins).forEach((p) => p.onDestroy()); this.view.destroy(); if (this.model && this.graphModelChangeListener) { this.getDataModel().removeListener(this.graphModelChangeListener); this.graphModelChangeListener = null; } } } } exports.AbstractGraph = AbstractGraph; // This introduces a side effect, but it is necessary to ensure the Graph is enriched with all properties and methods defined in mixins. // It is only called when Graph is imported, so the Graph definition is always consistent. // And this doesn't impact the tree-shaking. (0, _graph_mixins_apply_js_1.applyGraphMixins)(AbstractGraph);