Repository: maxGraph/maxGraph (ts)

Issue:
TS2748 Cannot access ambient const enums when the '--isolatedModules' flag is provided
I use maxGraph v0.2.1 in a TypeScript application that activates the `isolatedModules` option.

Accessing to enum declared in `constants` generates the TS2748 error. For instance, `constants.ALIGN.LEFT` or `constants.SHAPE.ELLIPSE`.


### My Proposal

`maxGraph` should probably not export "const enum" for interoperability reasons.

See
- https://ncjamieson.com/dont-export-const-enums/ (found via https://github.com/OfficeDev/microsoft-teams-library-js/issues/267#issuecomment-675864246)
- https://github.com/microsoft/TypeScript/issues/5243
- https://www.typescriptlang.org/tsconfig#isolatedModules

We could also ban const enums with [typescript-eslint.io/linting/troubleshooting#how-can-i-ban-specific-language-feature](https://typescript-eslint.io/linting/troubleshooting#how-can-i-ban-specific-language-feature) or if we absolutely must keep const enum, we should enable preserveConstEnums [typescriptlang.org/tsconfig#preserveConstEnums](https://www.typescriptlang.org/tsconfig#preserveConstEnums)

### Notes

Be also aware of #378.

Usage of const enum in maxGraph-core prevents to use them in the Storybook TypeScript stories (in the html package) as this project uses the `isolatedModules` option.
[build] Update the TypeScript configuration: enable `isolatedModules`
This will
- prevent the usage of const enum: #205
- eventually detect cyclic dependencies. This was previously done with a webpack plugin which was removed as part of #266 

**Note**: this is already enabled in the storybook build, see https://github.com/maxGraph/maxGraph/blob/v0.6.0/packages/html/tsconfig.json#L15

Pull request:
fix: declare regular enums instead of const enums
Const enums prevent usage of enums in TypeScript applications/projects that enable isolatedModules.

Add a ESLint rules to prevent future introduction of const enums. Also improve the JSDoc of existing enums.

Build @maxgraph/core with the isolatedModules option enabled.

Closes #205
Closes #268

Patch:
diff --git a/.eslintrc.json b/.eslintrc.json
index 85cbd5886..95f353809 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -24,6 +24,14 @@
         "@typescript-eslint/no-this-alias": "warn",
         "@typescript-eslint/no-explicit-any": "off",
         "@typescript-eslint/no-duplicate-enum-values": "off", // check the impact of changing enum values if we want to enable this
-        "@typescript-eslint/no-unused-vars": "off"
+        "@typescript-eslint/no-unused-vars": "off",
+        "no-restricted-syntax": [
+            "error",
+            // ban const enums
+            {
+                "selector": "TSEnumDeclaration[const=true]",
+                "message": "Const enums are forbidden to increase interoperability. Use regular enums instead."
+            }
+        ]
     }
 }
diff --git a/packages/core/src/util/Constants.ts b/packages/core/src/util/Constants.ts
index 5cbd19f9e..ac6ade5f8 100644
--- a/packages/core/src/util/Constants.ts
+++ b/packages/core/src/util/Constants.ts
@@ -34,32 +34,20 @@ export const MIN_HOTSPOT_SIZE = 8;
  */
 export const MAX_HOTSPOT_SIZE = 0;
 
-/**
- * Defines the exact rendering hint.
- *
- * Defines the faster rendering hint.
- *
- * Defines the fastest rendering hint.
- */
-export const enum RENDERING_HINT {
+export enum RENDERING_HINT {
   EXACT = 'exact',
   FASTER = 'faster',
   FASTEST = 'fastest',
 }
 
-/**
- * - DIALECT.SVG: Defines the SVG display dialect name.
- *
- * - DIALECT.MIXEDHTML: Defines the mixed HTML display dialect name.
- *
- * - DIALECT.PREFERHTML: Defines the preferred HTML display dialect name.
- *
- * - DIALECT.STRICTHTML: Defines the strict HTML display dialect.
- */
-export const enum DIALECT {
+export enum DIALECT {
+  /** the SVG display dialect name. */
   SVG = 'svg',
+  /** the mixed HTML display dialect name. */
   MIXEDHTML = 'mixedHtml',
+  /** the preferred HTML display dialect name. */
   PREFERHTML = 'preferHtml',
+  /** the strict HTML display dialect name. */
   STRICTHTML = 'strictHtml',
 }
 
@@ -100,7 +88,7 @@ export const SHADOW_OFFSET_Y = 3;
  */
 export const SHADOW_OPACITY = 1;
 
-export const enum NODETYPE {
+export enum NODETYPE {
   ELEMENT = 1,
   ATTRIBUTE = 2,
   TEXT = 3,
@@ -160,28 +148,20 @@ export const HIGHLIGHT_SIZE = 2;
  */
 export const HIGHLIGHT_OPACITY = 100;
 
-/**
- * - CURSOR_MOVABLE_VERTEX: Defines the cursor for a movable vertex. Default is 'move'.
- *
- * - CURSOR_MOVABLE_EDGE: Defines the cursor for a movable edge. Default is 'move'.
- *
- * - CURSOR_LABEL_HANDLE: Defines the cursor for a movable label. Default is 'default'.
- *
- * - CURSOR_TERMINAL_HANDLE: Defines the cursor for a terminal handle. Default is 'pointer'.
- *
- * - CURSOR_BEND_HANDLE: Defines the cursor for a movable bend. Default is 'crosshair'.
- *
- * - CURSOR_VIRTUAL_BEND_HANDLE: Defines the cursor for a movable bend. Default is 'crosshair'.
- *
- * - CURSOR_CONNECT: Defines the cursor for a connectable state. Default is 'pointer'.
- */
-export const enum CURSOR {
+export enum CURSOR {
+  /** Defines the cursor for a movable vertex. */
   MOVABLE_VERTEX = 'move',
+  /** Defines the cursor for a movable edge. */
   MOVABLE_EDGE = 'move',
+  /** Defines the cursor for a movable label. */
   LABEL_HANDLE = 'default',
+  /** Defines the cursor for a terminal handle. */
   TERMINAL_HANDLE = 'pointer',
+  /** Defines the cursor for a movable bend. */
   BEND_HANDLE = 'crosshair',
+  /** Defines the cursor for a movable bend. */
   VIRTUAL_BEND_HANDLE = 'crosshair',
+  /** Defines the cursor for a connectable state. */
   CONNECT = 'pointer',
 }
 
@@ -461,99 +441,69 @@ export const PAGE_FORMAT_LETTER_LANDSCAPE = [0, 0, 1100, 850];
  */
 export const NONE = 'none';
 
-/**
- * - FONT_BOLD: Constant for bold fonts. Default is 1.
- *
- * - FONT_ITALIC: Constant for italic fonts. Default is 2.
- *
- * - FONT_UNDERLINE: Constant for underlined fonts. Default is 4.
- *
- * - FONT_STRIKETHROUGH: Constant for strikethrough fonts. Default is 8.
- */
-export const enum FONT {
+export enum FONT {
+  /** for bold fonts. */
   BOLD = 1,
+  /** for italic fonts. */
   ITALIC = 2,
+  /** for underlined fonts. */
   UNDERLINE = 4,
+  /** for strikethrough fonts. */
   STRIKETHROUGH = 8,
 }
 
-/**
- * - ARROW_CLASSIC: Constant for classic arrow markers.
- *
- * - ARROW_CLASSIC_THIN: Constant for thin classic arrow markers.
- *
- * - ARROW_BLOCK: Constant for block arrow markers.
- *
- * - ARROW_BLOCK_THIN: Constant for thin block arrow markers.
- *
- * - ARROW_OPEN: Constant for open arrow markers.
- *
- * - ARROW_OPEN_THIN: Constant for thin open arrow markers.
- *
- * - ARROW_OVAL: Constant for oval arrow markers.
- *
- * - ARROW_DIAMOND: Constant for diamond arrow markers.
- *
- * - ARROW_DIAMOND_THIN: Constant for thin diamond arrow markers.
- */
-export const enum ARROW {
+export enum ARROW {
+  /** for classic arrow markers. */
   CLASSIC = 'classic',
+  /** for thin classic arrow markers. */
   CLASSIC_THIN = 'classicThin',
+  /** for block arrow markers. */
   BLOCK = 'block',
+  /** for thin block arrow markers. */
   BLOCK_THIN = 'blockThin',
+  /** for open arrow markers. */
   OPEN = 'open',
+  /** for thin open arrow markers. */
   OPEN_THIN = 'openThin',
+  /** for oval arrow markers. */
   OVAL = 'oval',
+  /** for diamond arrow markers. */
   DIAMOND = 'diamond',
+  /** for thin diamond arrow markers. */
   DIAMOND_THIN = 'diamondThin',
 }
 
-/**
- * - ALIGN_LEFT: Constant for left horizontal alignment. Default is left.
- *
- * - ALIGN_CENTER: Constant for center horizontal alignment. Default is center.
- *
- * - ALIGN_RIGHT: Constant for right horizontal alignment. Default is right.
- *
- * - ALIGN_TOP: Constant for top vertical alignment. Default is top.
- *
- * - ALIGN_MIDDLE: Constant for middle vertical alignment. Default is middle.
- *
- * - ALIGN_BOTTOM: Constant for bottom vertical alignment. Default is bottom.
- */
-export const enum ALIGN {
+export enum ALIGN {
+  /** left horizontal alignment. */
   LEFT = 'left',
+  /** center horizontal alignment. */
   CENTER = 'center',
+  /** right horizontal alignment. */
   RIGHT = 'right',
+  /** top vertical alignment. */
   TOP = 'top',
+  /** middle vertical alignment. */
   MIDDLE = 'middle',
+  /** bottom vertical alignment. */
   BOTTOM = 'bottom',
 }
 
-export const enum DIRECTION {
+export enum DIRECTION {
   NORTH = 'north',
   SOUTH = 'south',
   EAST = 'east',
   WEST = 'west',
 }
 
-/**
- * Constant for text direction default. Default is an empty string. Use
- * this value to use the default text direction of the operating system.
- *
- * Constant for text direction automatic. Default is auto. Use this value
- * to find the direction for a given text with {@link Text#getAutoDirection}.
- *
- * Constant for text direction left to right. Default is ltr. Use this
- * value for left to right text direction.
- *
- * Constant for text direction right to left. Default is rtl. Use this
- * value for right to left text direction.
- */
-export const enum TEXT_DIRECTION {
+export enum TEXT_DIRECTION {
+  /**
+   * Use this value to use the default text direction of the operating system. */
   DEFAULT = '',
+  /** Use this value to find the direction for a given text with {@link Text#getAutoDirection}. */
   AUTO = 'auto',
+  /** Use this value for left to right text direction. */
   LTR = 'ltr',
+  /** Use this value for right to left text direction. */
   RTL = 'rtl',
 }
 
@@ -582,7 +532,7 @@ export const DIRECTION_MASK = {
 /**
  * Default is horizontal.
  */
-export const enum ELBOW {
+export enum ELBOW {
   VERTICAL = 'vertical',
   HORIZONTAL = 'horizontal',
 }
@@ -590,7 +540,7 @@ export const enum ELBOW {
 /**
  * Can be used as a string value for the STYLE_EDGE style.
  */
-export const enum EDGESTYLE {
+export enum EDGESTYLE {
   ELBOW = 'elbowEdgeStyle',
   ENTITY_RELATION = 'entityRelationEdgeStyle',
   LOOP = 'loopEdgeStyle',
@@ -604,7 +554,7 @@ export const enum EDGESTYLE {
 /**
  * Can be used as a string value for the STYLE_PERIMETER style.
  */
-export const enum PERIMETER {
+export enum PERIMETER {
   ELLIPSE = 'ellipsePerimeter',
   RECTANGLE = 'rectanglePerimeter',
   RHOMBUS = 'rhombusPerimeter',
@@ -612,7 +562,7 @@ export const enum PERIMETER {
   TRIANGLE = 'trianglePerimeter',
 }
 
-export const enum SHAPE {
+export enum SHAPE {
   /**
    * Name under which {@link RectangleShape} is registered in {@link CellRenderer}.
    * Default is rectangle.
diff --git a/packages/core/src/util/styleUtils.ts b/packages/core/src/util/styleUtils.ts
index f58b60e54..a55904069 100644
--- a/packages/core/src/util/styleUtils.ts
+++ b/packages/core/src/util/styleUtils.ts
@@ -348,22 +348,20 @@ export const setCellStyles = (
 
 /**
  * Sets or toggles the flag bit for the given key in the cell's styles.
- * If value is null then the flag is toggled.
+ * If the `value` parameter is not set, then the flag is toggled.
  *
- * Example:
+ * Example that toggles the bold font style:
  *
  * ```javascript
- * let cells = graph.getSelectionCells();
- * mxUtils.setCellStyleFlags(graph.model,
+ * const cells = graph.getSelectionCells();
+ * setCellStyleFlags(graph.model,
  *       cells,
- *       mxConstants.STYLE_FONTSTYLE,
- *       mxConstants.FONT_BOLD);
+ *       'fontStyle',
+ *       constants.FONT.BOLD);
  * ```
  *
- * Toggles the bold font style.
- *
  * @param model <Transactions> that contains the cells.
- * @param cells Array of {@link Cells} to change the style for.
+ * @param cells Array of {@link Cell}s to change the style for.
  * @param key Key of the style to be changed.
  * @param flag Integer for the bit to be changed.
  * @param value Optional boolean value for the flag.
@@ -373,7 +371,7 @@ export const setCellStyleFlags = (
   cells: Cell[],
   key: NumericCellStateStyleKeys,
   flag: number,
-  value: boolean
+  value?: boolean
 ) => {
   if (cells.length > 0) {
     model.batchUpdate(() => {
@@ -390,8 +388,8 @@ export const setCellStyleFlags = (
 };
 
 /**
- * Sets or removes the given key from the specified style and returns the
- * new style. If value is null then the flag is toggled.
+ * Sets or toggles the flag bit for the given key in the cell's style.
+ * If the `value` parameter is not set, then the flag is toggled.
  *
  * @param style The style of the Cell.
  * @param key Key of the style to be changed.
@@ -407,7 +405,7 @@ export const setStyleFlag = (
   const v = style[key];
 
   if (v === undefined) {
-    style[key] = value === undefined ? flag : 0;
+    style[key] = value === undefined || value ? flag : 0;
   } else {
     if (value === undefined) {
       style[key] = v ^ flag;
diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json
index a0b057119..912e4c81d 100644
--- a/packages/core/tsconfig.json
+++ b/packages/core/tsconfig.json
@@ -5,6 +5,7 @@
     "declaration": true,
     "declarationMap": false,
     "emitDeclarationOnly": false,
+    "isolatedModules": true,
     "outDir": "./dist",
     "strict": true,
     "skipLibCheck": false,


Test patch:
diff --git a/packages/core/__tests__/util/styleUtils.test.ts b/packages/core/__tests__/util/styleUtils.test.ts
index 334528cf5..a7c50dff4 100644
--- a/packages/core/__tests__/util/styleUtils.test.ts
+++ b/packages/core/__tests__/util/styleUtils.test.ts
@@ -15,8 +15,9 @@ limitations under the License.
 */
 
 import { describe, expect, test } from '@jest/globals';
-import { matchBinaryMask } from '../../src/util/styleUtils';
+import { matchBinaryMask, setStyleFlag } from '../../src/util/styleUtils';
 import { FONT } from '../../src/util/Constants';
+import { type CellStyle } from '../../src/types';
 
 describe('matchBinaryMask', () => {
   test('match self', () => {
@@ -32,3 +33,64 @@ describe('matchBinaryMask', () => {
     expect(matchBinaryMask(46413, FONT.ITALIC)).toBeFalsy();
   });
 });
+
+describe('setStyleFlag', () => {
+  test('fontStyle undefined, set bold, no value', () => {
+    const style: CellStyle = {};
+    setStyleFlag(style, 'fontStyle', FONT.BOLD);
+    expect(style.fontStyle).toBe(1);
+  });
+  test('fontStyle undefined, set bold, value is false', () => {
+    const style: CellStyle = {};
+    setStyleFlag(style, 'fontStyle', FONT.BOLD, false);
+    expect(style.fontStyle).toBe(0);
+  });
+  test('fontStyle undefined, set italic, value is false', () => {
+    const style: CellStyle = {};
+    setStyleFlag(style, 'fontStyle', FONT.ITALIC, false);
+    expect(style.fontStyle).toBe(0);
+  });
+  test('fontStyle undefined, set underline, value is true', () => {
+    const style: CellStyle = {};
+    setStyleFlag(style, 'fontStyle', FONT.UNDERLINE, true);
+    expect(style.fontStyle).toBe(4);
+  });
+  test('fontStyle undefined, set strike-through, value is true', () => {
+    const style: CellStyle = {};
+    setStyleFlag(style, 'fontStyle', FONT.STRIKETHROUGH, true);
+    expect(style.fontStyle).toBe(8);
+  });
+
+  test('fontStyle set without bold, toggle bold', () => {
+    const style: CellStyle = { fontStyle: 2 };
+    setStyleFlag(style, 'fontStyle', FONT.BOLD);
+    expect(style.fontStyle).toBe(3);
+  });
+  test('fontStyle set with bold, toggle bold', () => {
+    const style: CellStyle = { fontStyle: 9 };
+    setStyleFlag(style, 'fontStyle', FONT.BOLD);
+    expect(style.fontStyle).toBe(8);
+  });
+
+  test('fontStyle set without strike-through, set strike-through', () => {
+    const style: CellStyle = { fontStyle: 7 };
+    setStyleFlag(style, 'fontStyle', FONT.STRIKETHROUGH, true);
+    expect(style.fontStyle).toBe(15);
+  });
+  test('fontStyle set without strike-through, unset strike-through', () => {
+    const style: CellStyle = { fontStyle: 7 };
+    setStyleFlag(style, 'fontStyle', FONT.STRIKETHROUGH, false);
+    expect(style.fontStyle).toBe(7);
+  });
+
+  test('fontStyle set with underline, set underline', () => {
+    const style: CellStyle = { fontStyle: 6 };
+    setStyleFlag(style, 'fontStyle', FONT.UNDERLINE, true);
+    expect(style.fontStyle).toBe(6);
+  });
+  test('fontStyle set with underline, unset underline', () => {
+    const style: CellStyle = { fontStyle: 6 };
+    setStyleFlag(style, 'fontStyle', FONT.UNDERLINE, false);
+    expect(style.fontStyle).toBe(2);
+  });
+});