Repository: maxGraph/maxGraph
Pull request #501 (2024-08-17 08:49:22)

feat: let retrieve the list of default plugins
This is useful for enriching the list of plugins passed to the `Graph` constructor.

Demonstrate the usage of the new functions in examples and in TypeScript stories.
Improve the JSDoc of `RubberbandHandler` to better explain its usage as a Plugin.
Introduce the Plugin category to better group the features relating to the feature in the HTML doc.



Patch:
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index d361ad350..313e62df5 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -18,6 +18,7 @@ limitations under the License.
 import './view/mixins/_graph-mixins-types';
 
 export { Graph } from './view/Graph';
+export * from './view/plugins';
 
 export { GraphDataModel } from './view/GraphDataModel';
 export { GraphView } from './view/GraphView';
diff --git a/packages/core/src/view/Graph.ts b/packages/core/src/view/Graph.ts
index ed8c9d283..e06860b08 100644
--- a/packages/core/src/view/Graph.ts
+++ b/packages/core/src/view/Graph.ts
@@ -21,16 +21,10 @@ import EventObject from './event/EventObject';
 import EventSource from './event/EventSource';
 import InternalEvent from './event/InternalEvent';
 import Rectangle from './geometry/Rectangle';
-import TooltipHandler from './handler/TooltipHandler';
 import Client from '../Client';
-import SelectionCellsHandler from './handler/SelectionCellsHandler';
-import ConnectionHandler from './handler/ConnectionHandler';
-import SelectionHandler from './handler/SelectionHandler';
-import PanningHandler from './handler/PanningHandler';
-import PopupMenuHandler from './handler/PopupMenuHandler';
+import type PanningHandler from './handler/PanningHandler';
 import GraphView from './GraphView';
 import CellRenderer from './cell/CellRenderer';
-import CellEditorHandler from './handler/CellEditorHandler';
 import Point from './geometry/Point';
 import { getCurrentStyle, hasScrollbars, parseCssNumber } from '../util/styleUtils';
 import Cell from './cell/Cell';
@@ -65,16 +59,7 @@ import { registerDefaultShapes } from './cell/register-shapes';
 import { registerDefaultEdgeMarkers } from './geometry/edge/MarkerShape';
 import { registerDefaultStyleElements } from './style/register';
 import { applyGraphMixins } from './mixins/_graph-mixins-apply';
-
-export const defaultPlugins: GraphPluginConstructor[] = [
-  CellEditorHandler,
-  TooltipHandler,
-  SelectionCellsHandler,
-  PopupMenuHandler,
-  ConnectionHandler,
-  SelectionHandler,
-  PanningHandler,
-];
+import { getDefaultPlugins } from './plugins';
 
 /**
  * Extends {@link EventSource} to implement a graph component for the browser. This is the main class of the package.
@@ -500,7 +485,7 @@ class Graph extends EventSource {
   constructor(
     container: HTMLElement,
     model?: GraphDataModel,
-    plugins: GraphPluginConstructor[] = defaultPlugins,
+    plugins: GraphPluginConstructor[] = getDefaultPlugins(),
     stylesheet: Stylesheet | null = null
   ) {
     super();
@@ -528,9 +513,7 @@ class Graph extends EventSource {
     this.setSelectionModel(this.createSelectionModel());
 
     // Initializes plugins
-    plugins.forEach((p: GraphPluginConstructor) => {
-      this.plugins[p.pluginId] = new p(this);
-    });
+    plugins.forEach((p) => (this.plugins[p.pluginId] = new p(this)));
 
     this.view.revalidate();
   }
diff --git a/packages/core/src/view/handler/CellEditorHandler.ts b/packages/core/src/view/handler/CellEditorHandler.ts
index 2e35908e0..86bdf2a34 100644
--- a/packages/core/src/view/handler/CellEditorHandler.ts
+++ b/packages/core/src/view/handler/CellEditorHandler.ts
@@ -153,11 +153,7 @@ import TooltipHandler from './TooltipHandler';
  * };
  * ```
  *
- * Constructor: mxCellEditor
- *
- * Constructs a new in-place editor for the specified graph.
- *
- * @param graph Reference to the enclosing {@link Graph}.
+ * @category Plugin
  */
 class CellEditorHandler implements GraphPlugin {
   static pluginId = 'CellEditorHandler';
diff --git a/packages/core/src/view/handler/ConnectionHandler.ts b/packages/core/src/view/handler/ConnectionHandler.ts
index d5f392817..05528b103 100644
--- a/packages/core/src/view/handler/ConnectionHandler.ts
+++ b/packages/core/src/view/handler/ConnectionHandler.ts
@@ -203,19 +203,7 @@ type FactoryMethod = (
  *
  * Fires when the <reset> method is invoked.
  *
- * Constructor: mxConnectionHandler
- *
- * Constructs an event handler that connects vertices using the specified
- * factory method to create the new edges. Modify
- * {@link Constants#ACTIVE_REGION} to setup the region on a cell which triggers
- * the creation of a new connection or use connect icons as explained
- * above.
- *
- * @param graph Reference to the enclosing {@link Graph}.
- * @param factoryMethod Optional function to create the edge. The function takes
- * the source and target <Cell> as the first and second argument and an
- * optional cell style from the preview as the third argument. It returns
- * the <Cell> that represents the new edge.
+ * @category Plugin
  */
 class ConnectionHandler extends EventSource implements GraphPlugin {
   static pluginId = 'ConnectionHandler';
@@ -394,6 +382,16 @@ class ConnectionHandler extends EventSource implements GraphPlugin {
 
   escapeHandler: () => void;
 
+  /**
+   * Constructs an event handler that connects vertices using the specified
+   * factory method to create the new edges.
+   *
+   * @param graph Reference to the enclosing {@link Graph}.
+   * @param factoryMethod Optional function to create the edge. The function takes
+   * the source and target {@link Cell} as the first and second argument and an
+   * optional cell style from the preview as the third argument. It returns
+   * the {@link Cell} that represents the new edge.
+   */
   constructor(graph: Graph, factoryMethod: FactoryMethod | null = null) {
     super();
 
diff --git a/packages/core/src/view/handler/PanningHandler.ts b/packages/core/src/view/handler/PanningHandler.ts
index dd90ff512..1f2eae3b7 100644
--- a/packages/core/src/view/handler/PanningHandler.ts
+++ b/packages/core/src/view/handler/PanningHandler.ts
@@ -36,15 +36,11 @@ import type { Graph } from '../Graph';
 
 /**
  * Event handler that pans and creates popupmenus. To use the left
- * mousebutton for panning without interfering with cell moving and
+ * mouse button for panning without interfering with cell moving and
  * resizing, use <isUseLeftButton> and <isIgnoreCell>. For grid size
- * steps while panning, use <useGrid>. This handler is built-into
- * {@link Graph#panningHandler} and enabled using {@link Graph#setPanning}.
+ * steps while panning, use <useGrid>.
  *
- * Constructor: mxPanningHandler
- *
- * Constructs an event handler that creates a {@link PopupMenu}
- * and pans the graph.
+ * When registered in the {@link Graph.constructor} plugins list, it can be enabled using {@link Graph.setPanning}.
  *
  * Event: mxEvent.PAN_START
  *
@@ -60,6 +56,8 @@ import type { Graph } from '../Graph';
  *
  * Fires when the panning handler changes its <active> state to false. The
  * <code>event</code> property contains the corresponding {@link MouseEvent}.
+ *
+ * @category Plugin
  */
 class PanningHandler extends EventSource implements GraphPlugin {
   static pluginId = 'PanningHandler';
diff --git a/packages/core/src/view/handler/PopupMenuHandler.ts b/packages/core/src/view/handler/PopupMenuHandler.ts
index ae7d5aff2..cd51f3df3 100644
--- a/packages/core/src/view/handler/PopupMenuHandler.ts
+++ b/packages/core/src/view/handler/PopupMenuHandler.ts
@@ -30,9 +30,9 @@ import EventObject from '../event/EventObject';
 /**
  * Event handler that creates popupmenus.
  *
- * Constructor: mxPopupMenuHandler
+ * Relates to {@link MaxPopupMenu}.
  *
- * Constructs an event handler that creates a {@link PopupMenu}.
+ * @category Plugin
  */
 class PopupMenuHandler extends MaxPopupMenu implements GraphPlugin {
   static pluginId = 'PopupMenuHandler';
diff --git a/packages/core/src/view/handler/RubberBandHandler.ts b/packages/core/src/view/handler/RubberBandHandler.ts
index bf7c5dcaf..5bd1948e1 100644
--- a/packages/core/src/view/handler/RubberBandHandler.ts
+++ b/packages/core/src/view/handler/RubberBandHandler.ts
@@ -37,8 +37,25 @@ import EventSource from '../event/EventSource';
 
 /**
  * Event handler that selects rectangular regions.
- * This is not built-into [mxGraph].
+ *
+ * **IMPORTANT**: This is not built-into `maxGraph` i.e. this plugin is not in the `maxGraph` default plugins, see {@link getDefaultPlugins}.
+ *
  * To enable rubberband selection in a graph, use the following code.
+ *
+ * ```javascript
+ * const plugins = [
+ *   ...getDefaultPlugins(), // or any other plugins you want
+ *   RubberBandHandler,
+ * ];
+ *
+ * // Creates the graph with the custom plugins
+ * const graph = new Graph(container, undefined, plugins);
+ * ```
+ *
+ * **IMPORTANT**: the RubberBandHandler requires CSS styles in order to work properly.
+ * See the CSS rules in the `css/common.css` file provided within the npm package. They relate to the `.mxRubberband` class.
+ *
+ * @category Plugin
  */
 class RubberBandHandler implements GraphPlugin {
   static pluginId = 'RubberBandHandler';
diff --git a/packages/core/src/view/handler/SelectionCellsHandler.ts b/packages/core/src/view/handler/SelectionCellsHandler.ts
index 7be7733e0..fffc2f146 100644
--- a/packages/core/src/view/handler/SelectionCellsHandler.ts
+++ b/packages/core/src/view/handler/SelectionCellsHandler.ts
@@ -45,7 +45,7 @@ import InternalMouseEvent from '../event/InternalMouseEvent';
  * Fires if a cell has been remove from the selection. The <code>state</code>
  * property contains the <CellState> that has been removed.
  *
- * @param graph Reference to the enclosing {@link Graph}.
+ * @category Plugin
  */
 class SelectionCellsHandler extends EventSource implements GraphPlugin {
   static pluginId = 'SelectionCellsHandler';
diff --git a/packages/core/src/view/handler/SelectionHandler.ts b/packages/core/src/view/handler/SelectionHandler.ts
index 3b27bd544..6271e7370 100644
--- a/packages/core/src/view/handler/SelectionHandler.ts
+++ b/packages/core/src/view/handler/SelectionHandler.ts
@@ -63,13 +63,16 @@ import type { ColorValue, GraphPlugin } from '../../types';
  *
  * To avoid the container to scroll a moved cell into view, set {@link scrollOnMove} to `false`.
  *
- * Constructs an event handler that creates handles for the selection cells.
- *
- * @param graph Reference to the enclosing {@link Graph}.
+ * @category Plugin
  */
 class SelectionHandler implements GraphPlugin {
   static pluginId = 'SelectionHandler';
 
+  /**
+   * Constructs an event handler that creates handles for the selection cells.
+   *
+   * @param graph Reference to the enclosing {@link Graph}.
+   */
   constructor(graph: Graph) {
     this.graph = graph;
     this.graph.addMouseListener(this);
diff --git a/packages/core/src/view/handler/TooltipHandler.ts b/packages/core/src/view/handler/TooltipHandler.ts
index ceffb398c..47ec21923 100644
--- a/packages/core/src/view/handler/TooltipHandler.ts
+++ b/packages/core/src/view/handler/TooltipHandler.ts
@@ -35,6 +35,8 @@ import EventSource from '../event/EventSource';
  * {@link Graph#getTooltip} is used to get the tooltip for a cell or handle.
  *
  * This handler is generally enabled using {@link Graph#setTooltips}.
+ *
+ * @category Plugin
  */
 class TooltipHandler implements GraphPlugin {
   static pluginId = 'TooltipHandler';
diff --git a/packages/core/src/view/other/Outline.ts b/packages/core/src/view/other/Outline.ts
index 739f8bd22..be72f393a 100644
--- a/packages/core/src/view/other/Outline.ts
+++ b/packages/core/src/view/other/Outline.ts
@@ -27,7 +27,7 @@ import {
 import Point from '../geometry/Point';
 import Rectangle from '../geometry/Rectangle';
 import RectangleShape from '../geometry/node/RectangleShape';
-import { Graph, defaultPlugins } from '../Graph';
+import { Graph } from '../Graph';
 import ImageShape from '../geometry/node/ImageShape';
 import InternalEvent from '../event/InternalEvent';
 import Image from '../image/ImageBox';
@@ -36,6 +36,7 @@ import { getSource, isMouseEvent } from '../../util/EventUtils';
 import EventSource from '../event/EventSource';
 import { hasScrollbars } from '../../util/styleUtils';
 import { Listenable } from '../../types';
+import { getDefaultPlugins } from '../plugins';
 
 /**
  * @class Outline
@@ -322,7 +323,10 @@ class Outline {
     const graph = new Graph(
       container,
       this.source.getDataModel(),
-      defaultPlugins,
+      // TODO review the list of plugins for the Graph of an Outline
+      // we could pass an empty array or a selection of plugins
+      // it may be necessary to make the plugins array configurable to allow custom plugins and improve tree-shaking
+      getDefaultPlugins(),
       this.source.getStylesheet()
     );
     graph.options.foldingEnabled = false;
diff --git a/packages/core/src/view/plugins/index.ts b/packages/core/src/view/plugins/index.ts
new file mode 100644
index 000000000..3ab8935fa
--- /dev/null
+++ b/packages/core/src/view/plugins/index.ts
@@ -0,0 +1,41 @@
+/*
+Copyright 2024-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 type { GraphPluginConstructor } from '../../types';
+import CellEditorHandler from '../handler/CellEditorHandler';
+import TooltipHandler from '../handler/TooltipHandler';
+import SelectionCellsHandler from '../handler/SelectionCellsHandler';
+import PopupMenuHandler from '../handler/PopupMenuHandler';
+import ConnectionHandler from '../handler/ConnectionHandler';
+import SelectionHandler from '../handler/SelectionHandler';
+import PanningHandler from '../handler/PanningHandler';
+
+/**
+ * Returns the list of plugins used by default in `maxGraph`.
+ *
+ * The function returns a new array each time it is called.
+ *
+ * @category Plugin
+ */
+export const getDefaultPlugins = (): GraphPluginConstructor[] => [
+  CellEditorHandler,
+  TooltipHandler,
+  SelectionCellsHandler,
+  PopupMenuHandler,
+  ConnectionHandler,
+  SelectionHandler,
+  PanningHandler,
+];
diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json
index 0d76c5d03..ee22cdcf5 100644
--- a/packages/core/tsconfig.json
+++ b/packages/core/tsconfig.json
@@ -24,6 +24,7 @@
       "Vertex Shapes",
       "Layout",
       "Perimeter",
+      "Plugin",
       "Serialization with Codecs",
       "*"
     ],
diff --git a/packages/html/stories/Anchors.stories.ts b/packages/html/stories/Anchors.stories.ts
index ff89352ab..31f8547ba 100644
--- a/packages/html/stories/Anchors.stories.ts
+++ b/packages/html/stories/Anchors.stories.ts
@@ -22,6 +22,7 @@ import {
   ConnectionConstraint,
   Geometry,
   Graph,
+  type GraphPluginConstructor,
   InternalMouseEvent,
   Point,
   RubberBandHandler,
@@ -63,19 +64,19 @@ const Template = ({ label, ...args }: Record<string, string>) => {
     }
   }
 
+  // Use a dedicated set of plugins to use MyCustomConnectionHandler and to not use extra plugins not needed here
+  const plugins: GraphPluginConstructor[] = [
+    CellEditorHandler,
+    SelectionCellsHandler,
+    MyCustomConnectionHandler,
+    SelectionHandler,
+  ];
+  // Enables rubberband selection
+  if (args.rubberBand) plugins.push(RubberBandHandler);
+
   class MyCustomGraph extends Graph {
     constructor(container: HTMLElement) {
-      super(
-        container,
-        undefined,
-        // Use a dedicated set of plugins to use MyCustomConnectionHandler and to not use extra plugins not needed here
-        [
-          CellEditorHandler,
-          SelectionCellsHandler,
-          MyCustomConnectionHandler,
-          SelectionHandler,
-        ]
-      );
+      super(container, undefined, plugins);
     }
     getAllConnectionConstraints = (terminal: CellState | null, _source: boolean) => {
       // Overridden to define per-geometry connection points
@@ -111,9 +112,6 @@ const Template = ({ label, ...args }: Record<string, string>) => {
   // Specifies the default edge style
   graph.getStylesheet().getDefaultEdgeStyle().edgeStyle = 'orthogonalEdgeStyle';
 
-  // Enables rubberband selection
-  if (args.rubberBand) new RubberBandHandler(graph);
-
   // Gets the default parent for inserting new cells. This
   // is normally the first child of the root (ie. layer 0).
   const parent = graph.getDefaultParent();
diff --git a/packages/html/stories/ContextIcons.stories.ts b/packages/html/stories/ContextIcons.stories.ts
index 48fbd9de6..73eacd412 100644
--- a/packages/html/stories/ContextIcons.stories.ts
+++ b/packages/html/stories/ContextIcons.stories.ts
@@ -27,6 +27,8 @@ import {
   type CellState,
   type ConnectionHandler,
   type SelectionHandler,
+  getDefaultPlugins,
+  type GraphPluginConstructor,
 } from '@maxgraph/core';
 
 import {
@@ -180,6 +182,10 @@ const Template = ({ label, ...args }: Record<string, any>) => {
   }
 
   class MyCustomGraph extends Graph {
+    constructor(container: HTMLElement, plugins: GraphPluginConstructor[]) {
+      super(container, undefined, plugins);
+    }
+
     createHandler(state: CellState) {
       if (state != null && state.cell.isVertex()) {
         return new CustomVertexToolHandler(state);
@@ -188,8 +194,12 @@ const Template = ({ label, ...args }: Record<string, any>) => {
     }
   }
 
+  // Enables rubberband selection
+  const plugins = getDefaultPlugins();
+  if (args.rubberBand) plugins.push(RubberBandHandler);
+
   // Creates the graph inside the given container
-  const graph = new MyCustomGraph(container);
+  const graph = new MyCustomGraph(container, plugins);
   graph.setConnectable(true);
 
   const connectionHandler = graph.getPlugin<ConnectionHandler>('ConnectionHandler');
@@ -199,9 +209,6 @@ const Template = ({ label, ...args }: Record<string, any>) => {
   // to fit the size of the graph
   // graph.setResizeContainer(true);
 
-  // Enables rubberband selection
-  if (args.rubberBand) new RubberBandHandler(graph);
-
   // Gets the default parent for inserting new cells. This
   // is normally the first child of the root (ie. layer 0).
   const parent = graph.getDefaultParent();
diff --git a/packages/html/stories/DynamicToolbar.stories.ts b/packages/html/stories/DynamicToolbar.stories.ts
index 73d7d3112..1d611d9c0 100644
--- a/packages/html/stories/DynamicToolbar.stories.ts
+++ b/packages/html/stories/DynamicToolbar.stories.ts
@@ -16,21 +16,22 @@ limitations under the License.
 */
 
 import {
+  Cell,
+  cellArrayUtils,
   type CellStyle,
+  Client,
+  type ConnectionHandler,
+  Geometry,
+  gestureUtils,
+  getDefaultPlugins,
   Graph,
+  GraphDataModel,
   type HTMLImageElementWithProps,
-  RubberBandHandler,
-  type ConnectionHandler,
   ImageBox,
-  MaxToolbar,
-  GraphDataModel,
-  Cell,
-  Geometry,
   InternalEvent,
+  MaxToolbar,
+  RubberBandHandler,
   styleUtils,
-  gestureUtils,
-  Client,
-  cellArrayUtils,
 } from '@maxgraph/core';
 import {
   globalTypes,
@@ -77,10 +78,12 @@ const Template = ({ label, ...args }: Record<string, string>) => {
   const container = createGraphContainer(args);
   div.appendChild(container);
 
-  // Creates the model and the graph inside the container
-  // using the fastest rendering available on the browser
-  const model = new GraphDataModel();
-  const graph = new Graph(container, model);
+  // Enables rubberband selection
+  const plugins = getDefaultPlugins();
+  if (args.rubberBand) plugins.push(RubberBandHandler);
+
+  // Creates the model and the graph inside the container using the fastest rendering available on the browser
+  const graph = new Graph(container, new GraphDataModel(), plugins);
 
   // Defines an icon for creating new connections in the connection handler.
   // This will automatically disable the highlighting of the source vertex.
@@ -98,8 +101,6 @@ const Template = ({ label, ...args }: Record<string, string>) => {
   // Stops editing on enter or escape keypress (TODO not working, do we want to keep this here?)
   // const keyHandler = new KeyHandler(graph);
 
-  if (args.rubberBand) new RubberBandHandler(graph);
-
   addVertex('images/rectangle.gif', 100, 40, {});
   addVertex('images/rounded.gif', 100, 40, { rounded: true });
   addVertex('images/ellipse.gif', 40, 40, {
diff --git a/packages/html/stories/Handles.stories.ts b/packages/html/stories/Handles.stories.ts
index 8b874563f..fa1012977 100644
--- a/packages/html/stories/Handles.stories.ts
+++ b/packages/html/stories/Handles.stories.ts
@@ -31,6 +31,8 @@ import {
   type CellState,
   type CellStateStyle,
   VertexHandlerConfig,
+  getDefaultPlugins,
+  type GraphPluginConstructor,
 } from '@maxgraph/core';
 
 import {
@@ -234,6 +236,10 @@ const Template = ({ label, ...args }: Record<string, string>) => {
   }
 
   class MyCustomGraph extends Graph {
+    constructor(container: HTMLElement, plugins: GraphPluginConstructor[]) {
+      super(container, undefined, plugins);
+    }
+
     createVertexHandler(state: CellState) {
       return new MyCustomVertexHandler(state);
     }
@@ -242,16 +248,17 @@ const Template = ({ label, ...args }: Record<string, string>) => {
   // Disables the built-in context menu
   if (!args.contextMenu) InternalEvent.disableContextMenu(container);
 
+  // Enables rubberband selection
+  const plugins = getDefaultPlugins();
+  if (args.rubberBand) plugins.push(RubberBandHandler);
+
   // Creates the graph inside the given container
-  const graph = new MyCustomGraph(container);
+  const graph = new MyCustomGraph(container, plugins);
   graph.setCellsCloneable(true);
   graph.setHtmlLabels(true);
   graph.setPanning(true);
   graph.centerZoom = false;
 
-  // Enables rubberband selection
-  if (args.rubberBand) new RubberBandHandler(graph);
-
   // Gets the default parent for inserting new cells. This
   // is normally the first child of the root (ie. layer 0).
   const parent = graph.getDefaultParent();
diff --git a/packages/html/stories/HelloPort.stories.ts b/packages/html/stories/HelloPort.stories.ts
index e687e3c2c..4696966db 100644
--- a/packages/html/stories/HelloPort.stories.ts
+++ b/packages/html/stories/HelloPort.stories.ts
@@ -18,6 +18,7 @@ limitations under the License.
 import {
   DomHelpers,
   EdgeStyle,
+  getDefaultPlugins,
   Graph,
   ModelXmlSerializer,
   Point,
@@ -53,8 +54,12 @@ const Template = ({ label, ...args }: Record<string, string>) => {
   const container = createGraphContainer(args);
   div.appendChild(container);
 
+  // Enables rubberband selection
+  const plugins = getDefaultPlugins();
+  if (args.rubberBand) plugins.push(RubberBandHandler);
+
   // Creates the graph inside the given container
-  const graph = new Graph(container);
+  const graph = new Graph(container, undefined, plugins);
   graph.setConnectable(true);
   graph.setTooltips(true);
 
@@ -88,9 +93,6 @@ const Template = ({ label, ...args }: Record<string, string>) => {
   // Removes the folding icon and disables any folding
   graph.isCellFoldable = (_cell) => false;
 
-  // Enables rubberband selection
-  if (args.rubberBand) new RubberBandHandler(graph);
-
   // Gets the default parent for inserting new cells. This
   // is normally the first child of the root (ie. layer 0).
   const parent = graph.getDefaultParent();
diff --git a/packages/html/stories/HierarchicalLayout.stories.ts b/packages/html/stories/HierarchicalLayout.stories.ts
index 27ae34432..ab79e0b0f 100644
--- a/packages/html/stories/HierarchicalLayout.stories.ts
+++ b/packages/html/stories/HierarchicalLayout.stories.ts
@@ -18,6 +18,7 @@ limitations under the License.
 import {
   DomHelpers,
   FastOrganicLayout,
+  getDefaultPlugins,
   Graph,
   HierarchicalLayout,
   InternalEvent,
@@ -52,11 +53,12 @@ const Template = ({ label, ...args }: Record<string, any>) => {
 
   InternalEvent.disableContextMenu(container);
 
-  // Creates the graph inside the given container
-  const graph = new Graph(container);
+  // Enables rubberband selection
+  const plugins = getDefaultPlugins();
+  if (args.rubberBand) plugins.push(RubberBandHandler);
 
-  // Adds rubberband selection
-  if (args.rubberBand) new RubberBandHandler(graph);
+  // Creates the graph inside the given container
+  const graph = new Graph(container, undefined, plugins);
 
   // Changes the default vertex style in-place
   let style = graph.getStylesheet().getDefaultVertexStyle();
diff --git a/packages/html/stories/JsonData.stories.ts b/packages/html/stories/JsonData.stories.ts
index 6aa2d0d93..ddd16a548 100644
--- a/packages/html/stories/JsonData.stories.ts
+++ b/packages/html/stories/JsonData.stories.ts
@@ -26,6 +26,7 @@ import {
   RubberBandHandler,
   ModelXmlSerializer,
   type Cell,
+  getDefaultPlugins,
 } from '@maxgraph/core';
 import {
   globalTypes,
@@ -79,11 +80,12 @@ const Template = ({ label, ...args }: Record<string, string>) => {
   // Disables the built-in context menu
   if (!args.contextMenu) InternalEvent.disableContextMenu(container);
 
-  // Creates the graph inside the given container
-  const graph = new Graph(container);
-
   // Enables rubberband selection
-  if (args.rubberBand) new RubberBandHandler(graph);
+  const plugins = getDefaultPlugins();
+  if (args.rubberBand) plugins.push(RubberBandHandler);
+
+  // Creates the graph inside the given container
+  const graph = new Graph(container, undefined, plugins);
 
   // Gets the default parent for inserting new cells. This
   // is normally the first child of the root (ie. layer 0).
diff --git a/packages/html/stories/Merge.stories.ts b/packages/html/stories/Merge.stories.ts
index 285ce4487..04efe8026 100644
--- a/packages/html/stories/Merge.stories.ts
+++ b/packages/html/stories/Merge.stories.ts
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import { Graph, Perimeter, RubberBandHandler } from '@maxgraph/core';
+import { getDefaultPlugins, Graph, Perimeter, RubberBandHandler } from '@maxgraph/core';
 import {
   globalTypes,
   globalValues,
@@ -44,8 +44,12 @@ const Template = ({ label, ...args }: Record<string, any>) => {
   // Should we allow overriding constants?
   // constants.SHADOWCOLOR = '#c0c0c0';
 
+  // Enables rubberband selection
+  const plugins = getDefaultPlugins();
+  if (args.rubberBand) plugins.push(RubberBandHandler);
+
   // Creates the graph inside the given container
-  const graph = new Graph(container);
+  const graph = new Graph(container, undefined, plugins);
 
   // No size handles, please...
   graph.setCellsResizable(false);
@@ -67,9 +71,6 @@ const Template = ({ label, ...args }: Record<string, any>) => {
   style.fontColor = 'black';
   style.strokeWidth = 2;
 
-  // Enables rubberband selection
-  if (args.rubberBand) new RubberBandHandler(graph);
-
   // Gets the default parent for inserting new cells. This
   // is normally the first child of the root (ie. layer 0).
   const parent = graph.getDefaultParent();
diff --git a/packages/html/stories/PerimeterOnLabelBounds.stories.ts b/packages/html/stories/PerimeterOnLabelBounds.stories.ts
index deda7253d..6b73f991f 100644
--- a/packages/html/stories/PerimeterOnLabelBounds.stories.ts
+++ b/packages/html/stories/PerimeterOnLabelBounds.stories.ts
@@ -17,6 +17,7 @@ limitations under the License.
 
 import {
   type CellState,
+  getDefaultPlugins,
   Graph,
   GraphView,
   InternalEvent,
@@ -82,9 +83,13 @@ const Template = ({ label, ...args }: Record<string, any>) => {
     }
   }
 
+  // Enables rubberband selection
+  const plugins = getDefaultPlugins();
+  if (args.rubberBand) plugins.push(RubberBandHandler);
+
   class MyCustomGraph extends Graph {
     constructor(container: HTMLElement) {
-      super(container);
+      super(container, undefined, plugins);
     }
 
     createGraphView(): GraphView {
@@ -100,9 +105,6 @@ const Template = ({ label, ...args }: Record<string, any>) => {
   // Uncomment the following if you want the container to fit the size of the graph
   // graph.setResizeContainer(true);
 
-  // Enables rubberband selection
-  if (args.rubberBand) new RubberBandHandler(graph);
-
   // Gets the default parent for inserting new cells. This is normally the first child of the root (i.e. layer 0).
   const parent = graph.getDefaultParent();
 
diff --git a/packages/html/stories/PerimeterVariousImplementations.stories.ts b/packages/html/stories/PerimeterVariousImplementations.stories.ts
index acd243507..0f733c625 100644
--- a/packages/html/stories/PerimeterVariousImplementations.stories.ts
+++ b/packages/html/stories/PerimeterVariousImplementations.stories.ts
@@ -16,6 +16,7 @@ limitations under the License.
 
 import {
   type CellStateStyle,
+  getDefaultPlugins,
   Graph,
   InternalEvent,
   RubberBandHandler,
@@ -54,11 +55,12 @@ const Template = ({ label, ...args }: Record<string, any>) => {
 
   InternalEvent.disableContextMenu(container);
 
-  // Creates the graph inside the given container
-  const graph: Graph = new Graph(container);
-
   // Enables rubberband selection
-  if (args.rubberBand) new RubberBandHandler(graph);
+  const plugins = getDefaultPlugins();
+  if (args.rubberBand) plugins.push(RubberBandHandler);
+
+  // Creates the graph inside the given container
+  const graph = new Graph(container, undefined, plugins);
 
   // Gets the default parent for inserting new cells. This is normally the first child of the root (i.e. layer 0).
   const parent = graph.getDefaultParent();
diff --git a/packages/html/stories/RadialTreeLayout.stories.ts b/packages/html/stories/RadialTreeLayout.stories.ts
index 215a11c72..b5b924b30 100644
--- a/packages/html/stories/RadialTreeLayout.stories.ts
+++ b/packages/html/stories/RadialTreeLayout.stories.ts
@@ -15,7 +15,12 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import { Graph, RubberBandHandler, RadialTreeLayout } from '@maxgraph/core';
+import {
+  getDefaultPlugins,
+  Graph,
+  RubberBandHandler,
+  RadialTreeLayout,
+} from '@maxgraph/core';
 import {
   globalTypes,
   globalValues,
@@ -41,11 +46,12 @@ export default {
 const Template = ({ label, ...args }: Record<string, any>) => {
   const container = createGraphContainer(args);
 
-  // Creates the graph inside the given container
-  const graph = new Graph(container);
+  // Enables rubberband selection
+  const plugins = getDefaultPlugins();
+  if (args.rubberBand) plugins.push(RubberBandHandler);
 
-  // Adds rubberband selection
-  if (args.rubberBand) new RubberBandHandler(graph);
+  // Creates the graph inside the given container
+  const graph = new Graph(container, undefined, plugins);
 
   // Changes the default vertex style in-place
   let style = graph.getStylesheet().getDefaultVertexStyle();
diff --git a/packages/html/stories/Stencils.stories.ts b/packages/html/stories/Stencils.stories.ts
index b8e5932bd..1ea9f8c39 100644
--- a/packages/html/stories/Stencils.stories.ts
+++ b/packages/html/stories/Stencils.stories.ts
@@ -24,6 +24,7 @@ import {
   constants,
   DomHelpers,
   EdgeHandler,
+  getDefaultPlugins,
   Graph,
   InternalEvent,
   load,
@@ -111,11 +112,15 @@ const Template = ({ label, ...args }: Record<string, string>) => {
       return shape;
     }
   }
+  // Enables rubberband selection
+  const plugins = getDefaultPlugins();
+  if (args.rubberBand) plugins.push(RubberBandHandler);
 
   class CustomGraph extends Graph {
     constructor(container: HTMLElement) {
-      super(container);
+      super(container, undefined, plugins);
     }
+
     createVertexHandler(state: CellState) {
       return new CustomVertexHandler(state);
     }
@@ -194,9 +199,6 @@ const Template = ({ label, ...args }: Record<string, string>) => {
   style.gradientColor = '#7d85df';
   style.shadow = true;
 
-  // Enables rubberband selection
-  if (args.rubberBand) new RubberBandHandler(graph);
-
   // Gets the default parent for inserting new cells. This
   // is normally the first child of the root (ie. layer 0).
   const parent = graph.getDefaultParent();
diff --git a/packages/html/stories/Toolbar.stories.ts b/packages/html/stories/Toolbar.stories.ts
index 1bfec5cd6..c122dfa93 100644
--- a/packages/html/stories/Toolbar.stories.ts
+++ b/packages/html/stories/Toolbar.stories.ts
@@ -31,6 +31,7 @@ import {
   Point,
   RubberBandHandler,
   cellArrayUtils,
+  getDefaultPlugins,
 } from '@maxgraph/core';
 import {
   globalTypes,
@@ -84,8 +85,13 @@ const Template = ({ label, ...args }: { [p: string]: any }) => {
   const container = createGraphContainer(args);
   toolbarAndGraphParentContainer.appendChild(container);
 
+  // Enables rubberband selection
+  const plugins = getDefaultPlugins();
+  if (args.rubberBand) plugins.push(RubberBandHandler);
+
+  // Creates the graph inside the given container
   const model = new GraphDataModel();
-  const graph = new Graph(container, model);
+  const graph = new Graph(container, model, plugins);
   configureExpandedAndCollapsedImages(graph);
   graph.dropEnabled = true;
 
@@ -119,8 +125,6 @@ const Template = ({ label, ...args }: { [p: string]: any }) => {
   // Stops editing on enter or escape keypress (TODO not working, do we want to keep this here?)
   // const keyHandler = new KeyHandler(graph);
 
-  if (args.rubberBand) new RubberBandHandler(graph);
-
   function addCell(
     isVertex: boolean,
     icon: string,
diff --git a/packages/html/stories/UserObject.stories.ts b/packages/html/stories/UserObject.stories.ts
index 6b0e9c750..e2d0dea58 100644
--- a/packages/html/stories/UserObject.stories.ts
+++ b/packages/html/stories/UserObject.stories.ts
@@ -31,6 +31,7 @@ import {
   ModelXmlSerializer,
   popup,
   type Cell,
+  getDefaultPlugins,
 } from '@maxgraph/core';
 import {
   globalTypes,
@@ -87,8 +88,12 @@ const Template = ({ label, ...args }: Record<string, string>) => {
   const relation = doc.createElement('Knows');
   relation.setAttribute('since', '1985');
 
+  // Enables rubberband selection
+  const plugins = getDefaultPlugins();
+  if (args.rubberBand) plugins.push(RubberBandHandler);
+
   // Creates the graph inside the given container
-  const graph = new Graph(container);
+  const graph = new Graph(container, undefined, plugins);
 
   // Optional disabling of sizing
   graph.setCellsResizable(false);
@@ -172,9 +177,6 @@ const Template = ({ label, ...args }: Record<string, string>) => {
     return getTooltipForCell.apply(this, [cell]);
   };
 
-  // Enables rubberband selection
-  if (args.rubberBand) new RubberBandHandler(graph);
-
   const buttons = document.createElement('div');
   mainDiv.appendChild(buttons);
 
diff --git a/packages/html/stories/Window.stories.ts b/packages/html/stories/Window.stories.ts
index 05dae776e..196f0c73c 100644
--- a/packages/html/stories/Window.stories.ts
+++ b/packages/html/stories/Window.stories.ts
@@ -25,6 +25,7 @@ import {
   domUtils,
   GlobalConfig,
   MaxLogAsLogger,
+  getDefaultPlugins,
 } from '@maxgraph/core';
 import {
   contextMenuTypes,
@@ -71,15 +72,17 @@ const Template = ({ label, ...args }: Record<string, string>) => {
     true
   );
 
+  // Enables rubberband selection
+  const plugins = getDefaultPlugins();
+  if (args.rubberBand) plugins.push(RubberBandHandler);
+
   // Creates the graph inside the given container
-  const graph = new Graph(container);
+  const graph = new Graph(container, undefined, plugins);
 
   // Adds rubberband selection and keystrokes
   graph.setTooltips(true);
   graph.setPanning(true);
 
-  if (args.rubberBand) new RubberBandHandler(graph);
-
   new KeyHandler(graph);
 
   if (!args.contextMenu) InternalEvent.disableContextMenu(container);
diff --git a/packages/js-example/src/index.js b/packages/js-example/src/index.js
index f7a6dabd3..9a85d621c 100644
--- a/packages/js-example/src/index.js
+++ b/packages/js-example/src/index.js
@@ -19,6 +19,7 @@ import './style.css';
 import {
   Client,
   DomHelpers,
+  getDefaultPlugins,
   Graph,
   InternalEvent,
   ModelXmlSerializer,
@@ -59,9 +60,11 @@ const initializeGraph = (container) => {
   // Disables the built-in context menu
   InternalEvent.disableContextMenu(container);
 
-  const graph = new Graph(container);
+  const graph = new Graph(container, undefined, [
+    ...getDefaultPlugins(),
+    RubberBandHandler, // Enables rubber band selection
+  ]);
   graph.setPanning(true); // Use mouse right button for panning
-  new RubberBandHandler(graph); // Enables rubber band selection
 
   const modelXmlSerializer = new ModelXmlSerializer(graph.model);
   modelXmlSerializer.import(xmlWithVerticesAndEdges);
diff --git a/packages/ts-example/src/main.ts b/packages/ts-example/src/main.ts
index dec7898a2..295cfcdac 100644
--- a/packages/ts-example/src/main.ts
+++ b/packages/ts-example/src/main.ts
@@ -19,6 +19,7 @@ import './style.css';
 import {
   Client,
   constants,
+  getDefaultPlugins,
   Graph,
   InternalEvent,
   Perimeter,
@@ -30,9 +31,11 @@ const initializeGraph = (container: HTMLElement) => {
   // Disables the built-in context menu
   InternalEvent.disableContextMenu(container);
 
-  const graph = new Graph(container);
+  const graph = new Graph(container, undefined, [
+    ...getDefaultPlugins(),
+    RubberBandHandler, // Enables rubber band selection
+  ]);
   graph.setPanning(true); // Use mouse right button for panning
-  new RubberBandHandler(graph); // Enables rubber band selection
 
   // shapes and styles
   registerCustomShapes();


Test patch:
diff --git a/packages/core/__tests__/view/plugins/index.test.ts b/packages/core/__tests__/view/plugins/index.test.ts
new file mode 100644
index 000000000..8fcd861fa
--- /dev/null
+++ b/packages/core/__tests__/view/plugins/index.test.ts
@@ -0,0 +1,40 @@
+/*
+Copyright 2022-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 { describe, expect, test } from '@jest/globals';
+import { getDefaultPlugins } from '../../../src';
+
+describe('getDefaultPlugins', () => {
+  test('returns a new array each time it is called', () => {
+    const plugins1 = getDefaultPlugins();
+    const plugins2 = getDefaultPlugins();
+    expect(plugins1).not.toBe(plugins2);
+    expect(plugins1).toEqual(plugins2);
+  });
+
+  test('returns an array with the correct length', () => {
+    const plugins = getDefaultPlugins();
+    // detect any changes in default plugins, order does not matter
+    expect(plugins).toHaveLength(7);
+  });
+
+  test('returns an array containing only functions', () => {
+    const plugins = getDefaultPlugins();
+    plugins.forEach((plugin) => {
+      expect(typeof plugin).toBe('function');
+    });
+  });
+});