fix(robustness): prevent errors when a plugin is not loaded (#272)

Add guards to prevent accessing properties and methods on undefined
instances when a plugin is not loaded.
Also fix and improve the JSDoc of the `EdgeHandler` and `Guide` classes.
development
Thomas Bouffard 2023-12-07 16:10:33 +01:00 committed by GitHub
parent 8d6727a411
commit ffd5036054
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 226 additions and 143 deletions

View File

@ -0,0 +1,25 @@
/*
Copyright 2023-present The maxGraph project Contributors
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.
*/
import { expect, test } from '@jest/globals';
import { createGraphWithoutPlugins } from './utils';
test('The "ConnectionHandler" plugin is not available', () => {
const graph = createGraphWithoutPlugins();
graph.setConnectable(true);
graph.isConnectable();
expect(graph.isConnectable()).toBe(false);
});

View File

@ -15,16 +15,12 @@ limitations under the License.
*/ */
import { describe, expect, test } from '@jest/globals'; import { describe, expect, test } from '@jest/globals';
import { Cell, type CellStyle, Geometry, Graph } from '../../../src'; import { createGraphWithoutContainer } from './utils';
import { Cell, type CellStyle, Geometry } from '../../../src';
function createGraph(): Graph {
// @ts-ignore - no need for a container, we don't check the view here
return new Graph(null);
}
describe('insertEdge', () => { describe('insertEdge', () => {
test('with several parameters', () => { test('with several parameters', () => {
const graph = createGraph(); const graph = createGraphWithoutContainer();
const source = new Cell(); const source = new Cell();
const target = new Cell(); const target = new Cell();
@ -55,7 +51,7 @@ describe('insertEdge', () => {
}); });
test('with single parameter', () => { test('with single parameter', () => {
const graph = createGraph(); const graph = createGraphWithoutContainer();
const source = new Cell(); const source = new Cell();
const target = new Cell(); const target = new Cell();
@ -91,7 +87,7 @@ describe('insertEdge', () => {
}); });
test('with single parameter and non default parent', () => { test('with single parameter and non default parent', () => {
const graph = createGraph(); const graph = createGraphWithoutContainer();
const parentCell = graph.insertVertex({ const parentCell = graph.insertVertex({
value: 'non default', value: 'non default',

View File

@ -0,0 +1,23 @@
/*
Copyright 2023-present The maxGraph project Contributors
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.
*/
import { test } from '@jest/globals';
import { createGraphWithoutPlugins } from './utils';
test('The "PanningHandler" plugin is not available', () => {
const graph = createGraphWithoutPlugins();
graph.setPanning(true);
});

View File

@ -0,0 +1,29 @@
/*
Copyright 2023-present The maxGraph project Contributors
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.
*/
import { test } from '@jest/globals';
import { createGraphWithoutPlugins } from './utils';
import { Cell, CellState } from '../../../src';
test('The "TooltipHandler" plugin is not available', () => {
const graph = createGraphWithoutPlugins();
graph.setTooltips(true);
});
test('The "SelectionCellsHandler" plugin is not available', () => {
const graph = createGraphWithoutPlugins();
graph.getTooltip(new CellState(null, new Cell()), null!, 0, 0);
});

View File

@ -15,16 +15,12 @@ limitations under the License.
*/ */
import { describe, expect, test } from '@jest/globals'; import { describe, expect, test } from '@jest/globals';
import { type CellStyle, Geometry, Graph } from '../../../src'; import { createGraphWithoutContainer } from './utils';
import { type CellStyle, Geometry } from '../../../src';
function createGraph(): Graph {
// @ts-ignore - no need for a container, we don't check the view here
return new Graph(null);
}
describe('insertVertex', () => { describe('insertVertex', () => {
test('with several parameters', () => { test('with several parameters', () => {
const graph = createGraph(); const graph = createGraphWithoutContainer();
const style: CellStyle = { rounded: true, shape: 'cloud' }; const style: CellStyle = { rounded: true, shape: 'cloud' };
const cell = graph.insertVertex(null, 'vertex_1', 'a value', 10, 20, 110, 120, style); const cell = graph.insertVertex(null, 'vertex_1', 'a value', 10, 20, 110, 120, style);
expect(cell.getId()).toBe('vertex_1'); expect(cell.getId()).toBe('vertex_1');
@ -50,7 +46,7 @@ describe('insertVertex', () => {
}); });
test('with single parameter', () => { test('with single parameter', () => {
const graph = createGraph(); const graph = createGraphWithoutContainer();
const style: CellStyle = { align: 'right', fillColor: 'red' }; const style: CellStyle = { align: 'right', fillColor: 'red' };
const cell = graph.insertVertex({ const cell = graph.insertVertex({
value: 'a value', value: 'a value',
@ -82,7 +78,7 @@ describe('insertVertex', () => {
}); });
test('with single parameter and non default parent', () => { test('with single parameter and non default parent', () => {
const graph = createGraph(); const graph = createGraphWithoutContainer();
const parentCell = graph.insertVertex({ const parentCell = graph.insertVertex({
value: 'non default', value: 'non default',

View File

@ -0,0 +1,22 @@
/*
Copyright 2023-present The maxGraph project Contributors
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.
*/
import { Graph } from '../../../src';
// no need for a container, we don't check the view here
export const createGraphWithoutContainer = (): Graph => new Graph(null!);
export const createGraphWithoutPlugins = (): Graph => new Graph(null!, null!, []);

View File

@ -3,4 +3,5 @@ module.exports = {
// preset: 'ts-jest', // preset: 'ts-jest',
preset: 'ts-jest/presets/default-esm', preset: 'ts-jest/presets/default-esm',
testEnvironment: 'jsdom', // need to access to the browser objects testEnvironment: 'jsdom', // need to access to the browser objects
testMatch: ['**/__tests__/**/?(*.)+(spec|test).ts'],
}; };

View File

@ -1437,24 +1437,24 @@ export class Editor extends EventSource {
// event if an insert function is defined // event if an insert function is defined
this.installInsertHandler(graph); this.installInsertHandler(graph);
// Redirects the function for creating the // Redirects the function for creating the popupmenu items
// popupmenu items
const popupMenuHandler = <PopupMenuHandler>graph.getPlugin('PopupMenuHandler'); const popupMenuHandler = <PopupMenuHandler>graph.getPlugin('PopupMenuHandler');
if (popupMenuHandler) {
popupMenuHandler.factoryMethod = (menu: any, cell: Cell | null, evt: any): void => {
return this.createPopupMenu(menu, cell, evt);
};
}
popupMenuHandler.factoryMethod = (menu: any, cell: Cell | null, evt: any): void => { // Redirects the function for creating new connections in the diagram
return this.createPopupMenu(menu, cell, evt);
};
// Redirects the function for creating
// new connections in the diagram
const connectionHandler = <ConnectionHandler>graph.getPlugin('ConnectionHandler'); const connectionHandler = <ConnectionHandler>graph.getPlugin('ConnectionHandler');
if (connectionHandler) {
connectionHandler.factoryMethod = ( connectionHandler.factoryMethod = (
source: Cell | null, source: Cell | null,
target: Cell | null target: Cell | null
): Cell => { ): Cell => {
return this.createEdge(source, target); return this.createEdge(source, target);
}; };
}
// Maintains swimlanes and installs autolayout // Maintains swimlanes and installs autolayout
this.createSwimlaneManager(graph); this.createSwimlaneManager(graph);
@ -2458,13 +2458,13 @@ export class Editor extends EventSource {
); );
if (modename === 'select') { if (modename === 'select') {
panningHandler.useLeftButtonForPanning = false; panningHandler && (panningHandler.useLeftButtonForPanning = false);
this.graph.setConnectable(false); this.graph.setConnectable(false);
} else if (modename === 'connect') { } else if (modename === 'connect') {
panningHandler.useLeftButtonForPanning = false; panningHandler && (panningHandler.useLeftButtonForPanning = false);
this.graph.setConnectable(true); this.graph.setConnectable(true);
} else if (modename === 'pan') { } else if (modename === 'pan') {
panningHandler.useLeftButtonForPanning = true; panningHandler && (panningHandler.useLeftButtonForPanning = true);
this.graph.setConnectable(false); this.graph.setConnectable(false);
} }
} }

View File

@ -262,14 +262,14 @@ export const VERTEX_SELECTION_DASHED = true;
export const EDGE_SELECTION_DASHED = true; export const EDGE_SELECTION_DASHED = true;
/** /**
* Defines the color to be used for the guidelines in mxGraphHandler. * Defines the color to be used for the guidelines in `Guide`.
* Default is #FF0000. * @default #FF0000.
*/ */
export const GUIDE_COLOR = '#FF0000'; export const GUIDE_COLOR = '#FF0000';
/** /**
* Defines the strokewidth to be used for the guidelines in mxGraphHandler. * Defines the strokewidth to be used for the guidelines in `Guide`.
* Default is 1. * @default 1.
*/ */
export const GUIDE_STROKEWIDTH = 1; export const GUIDE_STROKEWIDTH = 1;

View File

@ -68,21 +68,19 @@ export const defaultPlugins: GraphPluginConstructor[] = [
]; ];
/** /**
* Extends {@link EventSource} to implement a graph component for * Extends {@link EventSource} to implement a graph component for the browser. This is the main class of the package.
* the browser. This is the main class of the package. To activate *
* panning and connections use {@link setPanning} and {@link setConnectable}. * To activate panning and connections use {@link setPanning} and {@link setConnectable}.
* For rubberband selection you must create a new instance of * For rubberband selection you must create a new instance of {@link rubberband}.
* {@link rubberband}. The following listeners are added to *
* {@link mouseListeners} by default: * The following listeners are added to {@link mouseListeners} by default:
* *
* - tooltipHandler: {@link TooltipHandler} that displays tooltips * - tooltipHandler: {@link TooltipHandler} that displays tooltips
* - panningHandler: {@link PanningHandler} for panning and popup menus * - panningHandler: {@link PanningHandler} for panning and popup menus
* - connectionHandler: {@link ConnectionHandler} for creating connections * - connectionHandler: {@link ConnectionHandler} for creating connections
* - graphHandler: {@link SelectionHandler} for moving and cloning cells * - selectionHandler: {@link SelectionHandler} for moving and cloning cells
* *
* These listeners will be called in the above order if they are enabled. * These listeners will be called in the above order if they are enabled.
* @class graph
* @extends {EventSource}
*/ */
class Graph extends EventSource { class Graph extends EventSource {
container: HTMLElement; container: HTMLElement;
@ -723,7 +721,11 @@ class Graph extends EventSource {
} }
} }
} }
} else if (this.isAllowAutoPanning() && !panningHandler.isActive()) { } else if (
this.isAllowAutoPanning() &&
panningHandler &&
!panningHandler.isActive()
) {
panningHandler.getPanningManager().panTo(x + this.getPanDx(), y + this.getPanDy()); panningHandler.getPanningManager().panTo(x + this.getPanDx(), y + this.getPanDy());
} }
} }

View File

@ -2165,8 +2165,7 @@ export class GraphView extends EventSource {
graph.addMouseListener({ graph.addMouseListener({
mouseDown: (sender: any, me: InternalMouseEvent) => { mouseDown: (sender: any, me: InternalMouseEvent) => {
const popupMenuHandler = graph.getPlugin('PopupMenuHandler') as PopupMenuHandler; const popupMenuHandler = graph.getPlugin('PopupMenuHandler') as PopupMenuHandler;
popupMenuHandler?.hideMenu();
if (popupMenuHandler) popupMenuHandler.hideMenu();
}, },
mouseMove: () => { mouseMove: () => {
return; return;

View File

@ -1398,7 +1398,7 @@ class CellRenderer {
const selectionCellsHandler = graph.getPlugin( const selectionCellsHandler = graph.getPlugin(
'SelectionCellsHandler' 'SelectionCellsHandler'
) as SelectionCellsHandler; ) as SelectionCellsHandler;
selectionCellsHandler.updateHandler(state); selectionCellsHandler?.updateHandler(state);
} }
} else if ( } else if (
!force && !force &&
@ -1412,7 +1412,7 @@ class CellRenderer {
const selectionCellsHandler = graph.getPlugin( const selectionCellsHandler = graph.getPlugin(
'SelectionCellsHandler' 'SelectionCellsHandler'
) as SelectionCellsHandler; ) as SelectionCellsHandler;
selectionCellsHandler.updateHandler(state); selectionCellsHandler?.updateHandler(state);
force = true; force = true;
} }

View File

@ -712,10 +712,7 @@ class CellEditorHandler implements GraphPlugin {
} }
const tooltipHandler = this.graph.getPlugin('TooltipHandler') as TooltipHandler; const tooltipHandler = this.graph.getPlugin('TooltipHandler') as TooltipHandler;
tooltipHandler?.hideTooltip();
if (tooltipHandler) {
tooltipHandler.hideTooltip();
}
const state = this.graph.getView().getState(cell); const state = this.graph.getView().getState(cell);

View File

@ -184,7 +184,7 @@ type FactoryMethod = (
* the port IDs, use <Transactions.getCell>. * the port IDs, use <Transactions.getCell>.
* *
* ```javascript * ```javascript
* graph.getPlugin('ConnectionHandler').addListener(mxEvent.CONNECT, (sender, evt)=> * graph.getPlugin('ConnectionHandler')?.addListener(mxEvent.CONNECT, (sender, evt) =>
* { * {
* let edge = evt.getProperty('cell'); * let edge = evt.getProperty('cell');
* let source = graph.getDataModel().getTerminal(edge, true); * let source = graph.getDataModel().getTerminal(edge, true);

View File

@ -75,7 +75,7 @@ import { equalPoints } from '../../util/arrayUtils';
/** /**
* Graph event handler that reconnects edges and modifies control points and the edge * Graph event handler that reconnects edges and modifies control points and the edge
* label location. * label location.
* Uses {@link TerminalMarker} for finding and highlighting new source and target vertices. * Uses {@link CellMarker} for finding and highlighting new source and target vertices.
* This handler is automatically created in mxGraph.createHandler for each selected edge. * This handler is automatically created in mxGraph.createHandler for each selected edge.
* **To enable adding/removing control points, the following code can be used** * **To enable adding/removing control points, the following code can be used**
* @example * @example
@ -98,7 +98,7 @@ class EdgeHandler {
state: CellState; state: CellState;
/** /**
* Holds the {@link TerminalMarker} which is used for highlighting terminals. * Holds the {@link CellMarker} which is used for highlighting terminals.
*/ */
marker: CellMarker; marker: CellMarker;
@ -311,13 +311,14 @@ class EdgeHandler {
} }
} }
const graphHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler; const selectionHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler;
// Creates bends for the non-routed absolute points // Creates bends for the non-routed absolute points
// or bends that don't correspond to points // or bends that don't correspond to points
if ( if (
this.graph.getSelectionCount() < graphHandler.maxCells || selectionHandler &&
graphHandler.maxCells <= 0 (this.graph.getSelectionCount() < selectionHandler.maxCells ||
selectionHandler.maxCells <= 0)
) { ) {
this.bends = this.createBends(); this.bends = this.createBends();
@ -496,21 +497,21 @@ class EdgeHandler {
} }
/** /**
* Returns {@link Constants#EDGE_SELECTION_COLOR}. * Returns {@link EDGE_SELECTION_COLOR}.
*/ */
getSelectionColor() { getSelectionColor() {
return EDGE_SELECTION_COLOR; return EDGE_SELECTION_COLOR;
} }
/** /**
* Returns {@link Constants#EDGE_SELECTION_STROKEWIDTH}. * Returns {@link EDGE_SELECTION_STROKEWIDTH}.
*/ */
getSelectionStrokeWidth() { getSelectionStrokeWidth() {
return EDGE_SELECTION_STROKEWIDTH; return EDGE_SELECTION_STROKEWIDTH;
} }
/** /**
* Returns {@link Constants#EDGE_SELECTION_DASHED}. * Returns {@link EDGE_SELECTION_DASHED}.
*/ */
isSelectionDashed() { isSelectionDashed() {
return EDGE_SELECTION_DASHED; return EDGE_SELECTION_DASHED;
@ -525,14 +526,14 @@ class EdgeHandler {
} }
/** /**
* Creates and returns the {@link CellMarker} used in {@link arker}. * Creates and returns the {@link CellMarker} used in {@link marker}.
*/ */
getCellAt(x: number, y: number) { getCellAt(x: number, y: number) {
return !this.outlineConnect ? this.graph.getCellAt(x, y) : null; return !this.outlineConnect ? this.graph.getCellAt(x, y) : null;
} }
/** /**
* Creates and returns the {@link CellMarker} used in {@link arker}. * Creates and returns the {@link CellMarker} used in {@link marker}.
*/ */
createMarker() { createMarker() {
return new EdgeHandlerCellMarker(this.graph, this); return new EdgeHandlerCellMarker(this.graph, this);
@ -552,7 +553,7 @@ class EdgeHandler {
/** /**
* Creates and returns the bends used for modifying the edge. This is * Creates and returns the bends used for modifying the edge. This is
* typically an array of {@link RectangleShapes}. * typically an array of {@link RectangleShape}.
*/ */
createBends() { createBends() {
const { cell } = this.state; const { cell } = this.state;
@ -593,7 +594,7 @@ class EdgeHandler {
/** /**
* Creates and returns the bends used for modifying the edge. This is * Creates and returns the bends used for modifying the edge. This is
* typically an array of {@link RectangleShapes}. * typically an array of {@link RectangleShape}.
*/ */
// createVirtualBends(): mxRectangleShape[]; // createVirtualBends(): mxRectangleShape[];
createVirtualBends() { createVirtualBends() {
@ -1439,7 +1440,7 @@ class EdgeHandler {
/** /**
* Handles the event to applying the previewed changes on the edge by * Handles the event to applying the previewed changes on the edge by
* using {@link oveLabel}, <connect> or <changePoints>. * using {@link moveLabel}, <connect> or <changePoints>.
*/ */
mouseUp(sender: EventSource, me: InternalMouseEvent) { mouseUp(sender: EventSource, me: InternalMouseEvent) {
// Workaround for wrong event source in Webkit // Workaround for wrong event source in Webkit

View File

@ -242,15 +242,14 @@ class KeyHandler {
isGraphEvent(evt: KeyboardEvent) { isGraphEvent(evt: KeyboardEvent) {
const source = <Element>getSource(evt); const source = <Element>getSource(evt);
// Accepts events from the target object or // Accepts events from the target object or in-place editing inside graph
// in-place editing inside graph const cellEditorHandler = this.graph?.getPlugin(
const cellEditor = this.graph?.getPlugin(
'CellEditorHandler' 'CellEditorHandler'
) as CellEditorHandler | null; ) as CellEditorHandler;
if ( if (
source === this.target || source === this.target ||
source.parentNode === this.target || source.parentNode === this.target ||
(cellEditor != null && cellEditor.isEventSource(evt)) (cellEditorHandler && cellEditorHandler.isEventSource(evt))
) { ) {
return true; return true;
} }

View File

@ -99,11 +99,10 @@ class PopupMenuHandler extends MaxPopupMenu implements GraphPlugin {
* Initializes the shapes required for this vertex handler. * Initializes the shapes required for this vertex handler.
*/ */
init() { init() {
// Hides the tooltip if the mouse is over // Hides the tooltip if the mouse is over the context menu
// the context menu
InternalEvent.addGestureListeners(this.div, (evt) => { InternalEvent.addGestureListeners(this.div, (evt) => {
const tooltipHandler = this.graph.getPlugin('TooltipHandler') as TooltipHandler; const tooltipHandler = this.graph.getPlugin('TooltipHandler') as TooltipHandler;
tooltipHandler.hide(); tooltipHandler?.hide();
}); });
} }
@ -176,7 +175,7 @@ class PopupMenuHandler extends MaxPopupMenu implements GraphPlugin {
// Hides the tooltip if there is one // Hides the tooltip if there is one
const tooltipHandler = this.graph.getPlugin('TooltipHandler') as TooltipHandler; const tooltipHandler = this.graph.getPlugin('TooltipHandler') as TooltipHandler;
tooltipHandler.hide(); tooltipHandler?.hide();
// Menu is shifted by 1 pixel so that the mouse up event // Menu is shifted by 1 pixel so that the mouse up event
// is routed via the underlying shape instead of the DIV // is routed via the underlying shape instead of the DIV

View File

@ -61,13 +61,9 @@ import type { ColorValue, GraphPlugin } from '../../types';
* handlers are created using {@link Graph#createHandler} in * handlers are created using {@link Graph#createHandler} in
* {@link GraphSelectionModel#cellAdded}. * {@link GraphSelectionModel#cellAdded}.
* *
* To avoid the container to scroll a moved cell into view, set * To avoid the container to scroll a moved cell into view, set {@link scrollOnMove} to `false`.
* <scrollAfterMove> to false.
* *
* Constructor: mxGraphHandler * Constructs an event handler that creates handles for the selection cells.
*
* Constructs an event handler that creates handles for the
* selection cells.
* *
* @param graph Reference to the enclosing {@link Graph}. * @param graph Reference to the enclosing {@link Graph}.
*/ */
@ -132,7 +128,7 @@ class SelectionHandler implements GraphPlugin {
// Forces update to ignore last visible state // Forces update to ignore last visible state
this.setHandlesVisibleForCells( this.setHandlesVisibleForCells(
selectionCellsHandler.getHandledSelectionCells(), selectionCellsHandler?.getHandledSelectionCells() ?? [],
false, false,
true true
); );
@ -269,8 +265,8 @@ class SelectionHandler implements GraphPlugin {
connectOnDrop = false; connectOnDrop = false;
/** /**
* Specifies if the view should be scrolled so that a moved cell is * Specifies if the view should be scrolled so that a moved cell is visible.
* visible. Default is true. * @default true
*/ */
scrollOnMove = true; scrollOnMove = true;
@ -479,11 +475,11 @@ class SelectionHandler implements GraphPlugin {
if (!this.graph.isToggleEvent(me.getEvent()) || !isAltDown(me.getEvent())) { if (!this.graph.isToggleEvent(me.getEvent()) || !isAltDown(me.getEvent())) {
while (c) { while (c) {
if (selectionCellsHandler.isHandled(c)) { if (selectionCellsHandler?.isHandled(c)) {
const cellEditor = this.graph.getPlugin( const cellEditorHandler = this.graph.getPlugin(
'CellEditorHandler' 'CellEditorHandler'
) as CellEditorHandler; ) as CellEditorHandler;
return cellEditor.getEditingCell() !== c; return cellEditorHandler?.getEditingCell() !== c;
} }
c = c.getParent(); c = c.getParent();
} }
@ -497,7 +493,7 @@ class SelectionHandler implements GraphPlugin {
selectDelayed(me: InternalMouseEvent) { selectDelayed(me: InternalMouseEvent) {
const popupMenuHandler = this.graph.getPlugin('PopupMenuHandler') as PopupMenuHandler; const popupMenuHandler = this.graph.getPlugin('PopupMenuHandler') as PopupMenuHandler;
if (!popupMenuHandler.isPopupTrigger(me)) { if (!popupMenuHandler || !popupMenuHandler.isPopupTrigger(me)) {
let cell = me.getCell(); let cell = me.getCell();
if (cell === null) { if (cell === null) {
cell = this.cell; cell = this.cell;
@ -1064,7 +1060,7 @@ class SelectionHandler implements GraphPlugin {
) as SelectionCellsHandler; ) as SelectionCellsHandler;
this.setHandlesVisibleForCells( this.setHandlesVisibleForCells(
selectionCellsHandler.getHandledSelectionCells(), selectionCellsHandler?.getHandledSelectionCells() ?? [],
false false
); );
this.updateLivePreview(this.currentDx, this.currentDy); this.updateLivePreview(this.currentDx, this.currentDy);
@ -1265,11 +1261,8 @@ class SelectionHandler implements GraphPlugin {
) as SelectionCellsHandler; ) as SelectionCellsHandler;
for (let i = 0; i < states.length; i += 1) { for (let i = 0; i < states.length; i += 1) {
const handler = selectionCellsHandler.getHandler(states[i][0].cell); const handler = selectionCellsHandler?.getHandler(states[i][0].cell);
handler?.redraw(true);
if (handler != null) {
handler.redraw(true);
}
} }
} }
@ -1383,11 +1376,9 @@ class SelectionHandler implements GraphPlugin {
) as SelectionCellsHandler; ) as SelectionCellsHandler;
for (let i = 0; i < cells.length; i += 1) { for (let i = 0; i < cells.length; i += 1) {
const handler = selectionCellsHandler.getHandler(cells[i]); const handler = selectionCellsHandler?.getHandler(cells[i]);
if (handler) {
if (handler != null) {
handler.setHandlesVisible(visible); handler.setHandlesVisible(visible);
if (visible) { if (visible) {
handler.redraw(); handler.redraw();
} }
@ -1438,7 +1429,7 @@ class SelectionHandler implements GraphPlugin {
'ConnectionHandler' 'ConnectionHandler'
) as ConnectionHandler; ) as ConnectionHandler;
connectionHandler.connect(this.cell, cell, me.getEvent()); connectionHandler?.connect(this.cell, cell, me.getEvent());
} else { } else {
const clone = const clone =
graph.isCloneEvent(me.getEvent()) && graph.isCloneEvent(me.getEvent()) &&
@ -1493,7 +1484,7 @@ class SelectionHandler implements GraphPlugin {
) as SelectionCellsHandler; ) as SelectionCellsHandler;
this.setHandlesVisibleForCells( this.setHandlesVisibleForCells(
selectionCellsHandler.getHandledSelectionCells(), selectionCellsHandler?.getHandledSelectionCells() ?? [],
true true
); );
} }

View File

@ -233,12 +233,13 @@ class VertexHandler {
this.selectionBorder.setCursor(CURSOR.MOVABLE_VERTEX); this.selectionBorder.setCursor(CURSOR.MOVABLE_VERTEX);
} }
const graphHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler; const selectionHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler;
// Adds the sizer handles // Adds the sizer handles
if ( if (
graphHandler.maxCells <= 0 || selectionHandler &&
this.graph.getSelectionCount() < graphHandler.maxCells (selectionHandler.maxCells <= 0 ||
this.graph.getSelectionCount() < selectionHandler.maxCells)
) { ) {
const resizable = this.graph.isCellResizable(this.state.cell); const resizable = this.graph.isCellResizable(this.state.cell);
this.sizers = []; this.sizers = [];
@ -338,14 +339,17 @@ class VertexHandler {
* Returns true if the rotation handle should be showing. * Returns true if the rotation handle should be showing.
*/ */
isRotationHandleVisible() { isRotationHandleVisible() {
const graphHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler; const selectionHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler;
const selectionHandlerCheck = selectionHandler
? selectionHandler.maxCells <= 0 ||
this.graph.getSelectionCount() < selectionHandler.maxCells
: true;
return ( return (
this.graph.isEnabled() && this.graph.isEnabled() &&
this.rotationEnabled && this.rotationEnabled &&
this.graph.isCellRotatable(this.state.cell) && this.graph.isCellRotatable(this.state.cell) &&
(graphHandler.maxCells <= 0 || selectionHandlerCheck
this.graph.getSelectionCount() < graphHandler.maxCells)
); );
} }
@ -724,8 +728,7 @@ class VertexHandler {
) as SelectionCellsHandler; ) as SelectionCellsHandler;
for (let i = 0; i < edges.length; i += 1) { for (let i = 0; i < edges.length; i += 1) {
const handler = selectionCellsHandler.getHandler(edges[i]); const handler = selectionCellsHandler?.getHandler(edges[i]);
if (handler) { if (handler) {
this.edgeHandlers.push(handler as EdgeHandler); this.edgeHandlers.push(handler as EdgeHandler);
} }

View File

@ -746,7 +746,7 @@ const ConnectionsMixin: PartialType = {
*/ */
setConnectable(connectable) { setConnectable(connectable) {
const connectionHandler = this.getPlugin('ConnectionHandler') as ConnectionHandler; const connectionHandler = this.getPlugin('ConnectionHandler') as ConnectionHandler;
connectionHandler.setEnabled(connectable); connectionHandler?.setEnabled(connectable);
}, },
/** /**
@ -754,7 +754,7 @@ const ConnectionsMixin: PartialType = {
*/ */
isConnectable() { isConnectable() {
const connectionHandler = this.getPlugin('ConnectionHandler') as ConnectionHandler; const connectionHandler = this.getPlugin('ConnectionHandler') as ConnectionHandler;
return connectionHandler.isEnabled(); return connectionHandler?.isEnabled() ?? false;
}, },
}; };

View File

@ -92,8 +92,8 @@ const EditingMixin: PartialType = {
}, },
/** /**
* Fires a {@link startEditing} event and invokes {@link CellEditorHandler.startEditing} * Fires a {@link InternalEvent.START_EDITING} event and invokes {@link CellEditorHandler.startEditing}.
* on {@link editor}. After editing was started, a {@link editingStarted} event is * After editing was started, a {@link InternalEvent.EDITING_STARTED} event is
* fired. * fired.
* *
* @param cell {@link mxCell} to start the in-place editor for. * @param cell {@link mxCell} to start the in-place editor for.
@ -112,8 +112,10 @@ const EditingMixin: PartialType = {
new EventObject(InternalEvent.START_EDITING, { cell, event: evt }) new EventObject(InternalEvent.START_EDITING, { cell, event: evt })
); );
const cellEditor = this.getPlugin('CellEditorHandler') as CellEditorHandler; const cellEditorHandler = this.getPlugin(
cellEditor.startEditing(cell, evt); 'CellEditorHandler'
) as CellEditorHandler;
cellEditorHandler?.startEditing(cell, evt);
this.fireEvent( this.fireEvent(
new EventObject(InternalEvent.EDITING_STARTED, { cell, event: evt }) new EventObject(InternalEvent.EDITING_STARTED, { cell, event: evt })
@ -136,14 +138,14 @@ const EditingMixin: PartialType = {
}, },
/** /**
* Stops the current editing and fires a {@link editingStopped} event. * Stops the current editing and fires a {@link InternalEvent.EDITING_STOPPED} event.
* *
* @param cancel Boolean that specifies if the current editing value * @param cancel Boolean that specifies if the current editing value
* should be stored. * should be stored.
*/ */
stopEditing(cancel = false) { stopEditing(cancel = false) {
const cellEditor = this.getPlugin('CellEditorHandler') as CellEditorHandler; const cellEditorHandler = this.getPlugin('CellEditorHandler') as CellEditorHandler;
cellEditor.stopEditing(cancel); cellEditorHandler?.stopEditing(cancel);
this.fireEvent(new EventObject(InternalEvent.EDITING_STOPPED, { cancel })); this.fireEvent(new EventObject(InternalEvent.EDITING_STOPPED, { cancel }));
}, },
@ -218,11 +220,11 @@ const EditingMixin: PartialType = {
* If no cell is specified then this returns true if any * If no cell is specified then this returns true if any
* cell is currently being edited. * cell is currently being edited.
* *
* @param cell {@link mxCell} that should be checked. * @param cell {@link Cell} that should be checked.
*/ */
isEditing(cell = null) { isEditing(cell = null) {
const cellEditor = this.getPlugin('CellEditorHandler') as CellEditorHandler; const cellEditorHandler = this.getPlugin('CellEditorHandler') as CellEditorHandler;
const editingCell = cellEditor.getEditingCell(); const editingCell = cellEditorHandler?.getEditingCell();
return !cell ? !!editingCell : cell === editingCell; return !cell ? !!editingCell : cell === editingCell;
}, },

View File

@ -589,7 +589,7 @@ const EventsMixin: PartialType = {
if (mxe.isConsumed()) { if (mxe.isConsumed()) {
// Resets the state of the panning handler // Resets the state of the panning handler
panningHandler.panningTrigger = false; panningHandler && (panningHandler.panningTrigger = false);
} }
// Handles the event if it has not been consumed // Handles the event if it has not been consumed
@ -597,6 +597,7 @@ const EventsMixin: PartialType = {
this.isEnabled() && this.isEnabled() &&
!isConsumed(evt) && !isConsumed(evt) &&
!mxe.isConsumed() && !mxe.isConsumed() &&
connectionHandler &&
connectionHandler.isEnabled() connectionHandler.isEnabled()
) { ) {
const cell = connectionHandler.marker.getCell(me); const cell = connectionHandler.marker.getCell(me);
@ -1057,13 +1058,13 @@ const EventsMixin: PartialType = {
Math.abs(this.initialTouchY - me.getGraphY()) < this.tolerance; Math.abs(this.initialTouchY - me.getGraphY()) < this.tolerance;
} }
const cellEditor = this.getPlugin('CellEditorHandler') as CellEditorHandler; const cellEditorHandler = this.getPlugin('CellEditorHandler') as CellEditorHandler;
// Stops editing for all events other than from cellEditor // Stops editing for all events other than from cellEditorHandler
if ( if (
evtName === InternalEvent.MOUSE_DOWN && evtName === InternalEvent.MOUSE_DOWN &&
this.isEditing() && this.isEditing() &&
!cellEditor.isEventSource(me.getEvent()) !cellEditorHandler?.isEventSource(me.getEvent())
) { ) {
this.stopEditing(!this.isInvokesStopCellEditing()); this.stopEditing(!this.isInvokesStopCellEditing());
} }

View File

@ -395,15 +395,13 @@ const PanningMixin: PartialType = {
*****************************************************************************/ *****************************************************************************/
/** /**
* Specifies if panning should be enabled. This implementation updates * Specifies if panning should be enabled. This implementation updates {@link PanningHandler.panningEnabled}.
* {@link PanningHandler.panningEnabled} in {@link panningHandler}.
* *
* @param enabled Boolean indicating if panning should be enabled. * @param enabled Boolean indicating if panning should be enabled.
*/ */
setPanning(enabled) { setPanning(enabled) {
const panningHandler = this.getPlugin('PanningHandler') as PanningHandler; const panningHandler = this.getPlugin('PanningHandler') as PanningHandler;
panningHandler && (panningHandler.panningEnabled = enabled);
if (panningHandler) panningHandler.panningEnabled = enabled;
}, },
}; };

View File

@ -87,7 +87,7 @@ const TooltipMixin: PartialType = {
'SelectionCellsHandler' 'SelectionCellsHandler'
) as SelectionCellsHandler; ) as SelectionCellsHandler;
const handler = selectionCellsHandler.getHandler(state.cell); const handler = selectionCellsHandler?.getHandler(state.cell);
// @ts-ignore Guarded against undefined error already. // @ts-ignore Guarded against undefined error already.
if (handler && typeof handler.getTooltipForNode === 'function') { if (handler && typeof handler.getTooltipForNode === 'function') {
@ -147,8 +147,7 @@ const TooltipMixin: PartialType = {
*/ */
setTooltips(enabled: boolean) { setTooltips(enabled: boolean) {
const tooltipHandler = this.getPlugin('TooltipHandler') as TooltipHandler; const tooltipHandler = this.getPlugin('TooltipHandler') as TooltipHandler;
tooltipHandler?.setEnabled(enabled);
tooltipHandler.setEnabled(enabled);
}, },
}; };

View File

@ -534,8 +534,8 @@ class DragSource {
// Guide is only needed if preview element is used // Guide is only needed if preview element is used
if (this.isGuidesEnabled() && this.previewElement) { if (this.isGuidesEnabled() && this.previewElement) {
const graphHandler = graph.getPlugin('SelectionHandler') as SelectionHandler; const selectionHandler = graph.getPlugin('SelectionHandler') as SelectionHandler;
this.currentGuide = new Guide(graph, graphHandler.getGuideStates()); this.currentGuide = new Guide(graph, selectionHandler?.getGuideStates());
} }
if (this.highlightDropTargets) { if (this.highlightDropTargets) {

View File

@ -78,7 +78,7 @@ class Guide {
tolerance = 2; tolerance = 2;
/** /**
* Sets the {@link CellStates} that should be used for alignment. * Sets the {@link CellState}s that should be used for alignment.
*/ */
setStates(states: CellState[]) { setStates(states: CellState[]) {
this.states = states; this.states = states;
@ -103,8 +103,8 @@ class Guide {
/** /**
* Returns the mxShape to be used for painting the respective guide. This * Returns the mxShape to be used for painting the respective guide. This
* implementation returns a new, dashed and crisp {@link Polyline} using * implementation returns a new, dashed and crisp {@link PolylineShape} using
* {@link Constants#GUIDE_COLOR} and {@link Constants#GUIDE_STROKEWIDTH} as the format. * {@link GUIDE_COLOR} and {@link GUIDE_STROKEWIDTH} as the format.
* *
* @param horizontal Boolean that specifies which guide should be created. * @param horizontal Boolean that specifies which guide should be created.
*/ */