npm package: @myrmidon/gve-snapshot-view
Description: GVE snapshot view
Author: Daniele Fusi
Latest version: 1.2.1

# GVE Snapshot View Web Component

This project contains the [GVE](https://vedph.github.io/gve-doc/) snapshot view web component, which is a pure Typescript, JS-framework independent component to visualize a GVE snapshot with SVG.

## Workspace

🚀 Quick start to setup this workspace:

1. `npm i` to install packages.
2. `npm run build` to build.
3. `npm run start` to start the test page. Here you should paste the JSON code representing a snapshot, and click the button to view it. You can copy an example JSON code from [arzdc.json](arzdc.json), which is not used by the code but is a resource available to the user for this purpose. You can also use [abcxy.json](abcxy.json) to test for automatic positioning with multiple "layers" as derived from nodes added by operations.

>To publish the library package use `npm run publish`.

## API

⚙️ This component acts as a black box, receiving a snapshot object of type `Snapshot`, and rendering it into its `svg` element, according to the specified options and snapshot data.

- 📦 install: `npm i @myrmidon/gve-snapshot-view`
- 🔑 selector: `gve-snapshot-view`
- input:
  - ▶️ `data` (`SnapshotViewData | undefined | null`): the snapshot view data to render:
    - `snapshot` (`Snapshot`):
      - `size` (`Size`): the SVG size.
        - `width` (`number`)
        - `height` (`number`)
      - `style` (`string | undefined`): the SVG style.
      - `defs` (`string | undefined`): the optional SVG `defs` code to be used in the view.
      - `image` (`SnapshotImage | undefined`): the optional background image:
        - `url` (`string`)
        - `canvas` (`Rectangle`): the optional canvas to fit the image into. If not specified, the image will have the same size of the SVG container, and placed at its top-left corner (0,0):
          - `x` (`number`)
          - `y` (`number`)
          - `width` (`number`)
          - `height` (`number`)
        - `opacity` (`number`): the default background image opacity. If not specified this is equal to 0. So, to show the image you should specify an opacity value here, or set it later via the renderer's `setImageOpacity` method.
      - `text` (`string | CharNode[]`): the base text. This can be a string, which will be converted into an array of nodes, or directly an array of nodes.
        - `id` (`number`)
        - `index` (`number`)
        - `label` (`string`)
        - `data` (`string`)
        - `sourceTag`: (`string | undefined`)
        - `features` (`Feature[] | undefined`):
          - `name` (`string`)
          - `value` (`string`)
          - `setPolicy` (number)
      - `textStyle` (`string | undefined`): the style for the text `g` element.
      - `textOptions` (`SvgBaseTextOptions`):
        - `lineHeightOffset` (`number`): offset to add to each calculated line height.
        - `charSpacing` (`number`): offset to add to each calculated character width.
        - `spcWidthOffset` (`number`): offset to add to each calculated space width.
        - `offset` (`Point`): offset for the text origin.
          - `x` (`number`)
          - `y` (`number`)
        - `minLineHeights` (`Record<number, number>`): minimum line heights for each line number (1-N). You can specify
a minimum line height for each line number to ensure that its height is at least the specified value. This is useful for making room between lines for annotations that may exceed the base text.
        - `charDecorator` (`CharDecoration`): an optional function used to override the decoration of each rendered character. The function receives parameters `char` (`CharNode`), `ln`, `x`, `y` (all `number`), and returns either null or an object with any combination of these properties: `fill`, `stroke`, `strokeWidth`, `opacity`, `x`, `y` (where all the properties are numbers except for `fill` and `stroke`). This is typically used to leverage trace features in characters and change the character's fill and stroke accordingly. See the [test page](index.html) for an example.
        - `operations` (`CharChainOperation[]`):
          - `id` (`string`)
          - `rank` (`number | undefined`)
          - `groupId` (`string | undefined`)
          - `features` (`OperationFeature[] | undefined`):
            - `name` (`string`)
            - `value` (`string`)
            - `setPolicy` (number)
            - `isNegated` (`boolean | undefined`)
            - `isGlobal` (`boolean | undefined`)
            - `isShortLived` (`boolean | undefined`)
          - `sources` (`OperationSource[] | undefined`)
          - `diplomatics` (`OperationDiplomatics | undefined`):
            - `g` (`string`)
            - `isNewTextHidden` (`boolean | undefined`)
            - `features` (`Feature[] | undefined`):
              - `name` (`string`)
              - `value` (`string`)
              - `setPolicy` (`FeatureSetPolicy`)
            - `elementFeatures` (`Record<string, Feature[]>`)
          - `type` (`OperationType`)
          - `at` (`number`)
          - `atAsIndex` (`boolean | undefined`)
          - `to` (`number | undefined`): _to_ coordinate, for move/swap only.
          - `toAsIndex` (`boolean | undefined`)
          - `inputTag` (`string | undefined`)
          - `outputTag` (`string | undefined`)
          - `run` (`number`)
          - `toRun` (`number | undefined`)
          - `value` (`string | undefined`)
        - `opStyle` (`string`): the style for the operations `g` element.
        - `timelines` (`Record<string, GveAnimationTimeline>`):
          - `tag` (`string`)
          - `tweens` (`GveAnimationTween[]`):
            - `label` (`string`)
            - `note` (`string | undefined`)
            - `type` (`string`);
            - `selector` (`string`)
            - `vars` (`string | undefined`)
            - `vars2` (`string | undefined`)
            - `position` (`string | undefined`)
          - `vars` (`GveAnimationVars | undefined`)
    - `options` (`SnapshotViewOptions | undefined`):
      - `hideBase` (`boolean | undefined`): true to hide the base text.
      - `hideOperations` (`boolean | undefined`): true to hide the operations.
      - `operationIds` (`string[] | undefined`): the IDs of the operations to show; all the other operations will be hidden by setting their opacity to 0.
      - `showRulers` (`boolean | undefined`): true to show rulers.
      - `showGrid` (`boolean | undefined`): true to show gridlines on the rulers.
      - `rulerColor` (`boolean | undefined`): the rulers and gridlines color (default `#aaa`).
      - `debug` (`boolean | undefined`): true to enable debug mode.
      - `delayedRender` (`boolean | undefined`): true to delay the positioning of rendered text elements so that position is calculated after they are rendered in the DOM. This might be useful in some environments like Angular.
      - `panZoom` (`boolean | undefined`): true to enable pan and zoom functionality.
- output:
  - 🔥 `snapshotRender`: fired when the snapshot is rendered; `detail` (`SnapshotViewRenderEvent`) has the root SVG element and the renderer service:
    - `svg` (`SVGSVGElement`)
    - `renderer` (`SnapshotViewService`):
      - `characters` (`CharNode[]`)
      - `visuals` (`GveVisual[]`):
        - `id` (`string`)
        - `type` (`string`)
        - `style` (`string | undefined`)
        - `class` (`string | undefined`)
        - `data` (`any | undefined`)
        - `element` (`SVGElement | undefined`)
        - `attributes` (`Record<string, string> | undefined`): not used
        - `handlers` (`object | undefined`)
  - 🔥 `visualEvent`: fired when the user interacts with any rendered visual with the mouse; `detail` (`SnapshotViewVisualEvent`) has the mouse event and the source visual object (`GveVisual`):
    - `event` (`MouseEvent`)
    - `source` (`GveVisual`)

>GVE-related models are currently defined in this library for convenience ([src/models](src/models.ts)). Once they reach stability, they will be moved into an independent library (`gve-snapshot-core`) which will thus be shared among a wider range of frontend components dealing with snapshots visualization.

## Usage

💡 Example usage:

```js
// getting custom component
let component = document.querySelector('gve-snapshot-view');

// setting input data
component.data = data;

// listening to events
component.addEventListener("snapshotRender", (event) => {
  console.log("Snapshot rendered:", event.detail);
});
component.addEventListener("visualEvent", (event) => {
  console.log("Visual event:", event.detail);
});
```

## SVG Output

⚙️ The component renders a snapshot in this starting template:

```xml
<svg id="snapshot">
  <!-- background image -->
  <image id="image" x="0" y="0" width="0" height="0"></image>
  <!-- rulers -->
  <g id="rulers"></g>
  <!-- base text -->
  <g id="base"></g>
  <!-- operations -->
  <g id="operations"></g>
</svg>
```

According to data and options, the component will add new SVG elements as descendants of these `g` elements.

Element `g#base` contains multiline text, where each character (except for newline) is represented by an SVG `text` element. In most cases, the characters are displayed one after another on a line (until any newline is found), except for those character having a manually set position (as defined by the `x` and `y` features of the source model for each character).

To this end, the text renderer must get the bounding box of each character added and use it to determine the position of the next one. In some environments, like Angular, this does not work because there is some delay (probably due to `zone.js`) between the addition of the SVG element to the DOM and its effective rendition.

To work around [this issue](https://stackoverflow.com/questions/78548560/consuming-a-custom-web-element-in-angular-issues-in-dom-updates) the component has a special rendition mode, enabled by setting its `data`'s `delayedRender` setting to true. When this happens, the component behaves as follows:

1. each `text` element gets added and measured, but usually this measurement ends up with a 0-sized bounding box. Anyway, this will not be an issue here because `text` opacity is set to 0, which ensures that nothing is displayed at this stage (which would result in overwriting all the characters at the same position). The original `opacity` value, when set (and when different from `1`), is preserved for a later restore.
2. once all the elements have been added, inside the handler of `requestAnimationFrame` all the `text` elements corresponding to character and their visuals are updated after querying each `text` element's bounding box. Also, the element's `opacity` is restored.

If instead `delayedRender` is disabled, the position is set immediately after each SVG `text` is added, without resetting its `opacity` to 0, and this already is the correct position.

## Services

### Snapshot View Service

⚙️ The snapshot view service is the engine behind the snapshot view component, used to create all its SVG elements except for those belonging to its main template. This is just a POJO class, which must be simply instantiated and used, and maintains the state for the snapshot viewer.

The service can be accessed on rendition, and provides this API:

- ▶️ `characters` (`CharNode[]`): the base text characters of the snapshot.
- ▶️ `visuals` (`GveVisual[]`): the viewmodels of visuals rendered by the service.
- 🔵 `translateSpecialChar`: translate an ASCII special character to a printable character.
- 🔵 `stringToBaseChars`: convert a string to the character nodes of a base text.
- 🔵 `addEventHandlers`: add event listeners to a visual.
- 🔵 `removeEventHandlers`: remove event listeners from a visual.
- 🔵 `attachEventHandlers`: attach or detach event handlers to the rendered visuals.
- 🔵 `toggleRulers`: toggle the rulers on (pass `true`) or off (pass `false`), or just toggle the existing state (do not pass anything). Note that if rulers were not rendered by setting the corresponding options, this method will not have any effect.
- 🔵 `setImageOpacity`: set the opacity of the background image element, if any.
- 🔵 `render`: render the specified snapshot.
- 🔵 `playTimeline`: play the specified timeline.
- 🔵 `pauseTimeline`: pause the specified timeline.
- 🔵 `resumeTimeline`: resume the specified timeline.
- 🔵 `reverseTimeline`: play the specified timeline in reverse.

The main method in the service is `render`, which gets:

- the snapshot (`Snapshot`) to render;
- the rendering options (`SnapshotViewOptions`);
- the optional event handlers (`GveVisualEventHandlers`) for the rendered visuals.

The service maintains its rendition in these SVG elements:

- the root SVG `svg` element. This is injected in the service constructor, and it's assumed to have children elements for image, rulers, base, and operations, with their corresponding identifiers.
- a background SVG `image` element with ID `image`.
- the rulers SVG `g` element with ID `rulers`.
- the base text SVG `g` element with ID `base`.
- the operations root element with ID `operations`.

It also maintains its logical state in:

- an array of `CharNode` items (`characters` property): the viewmodels of the rendered characters.
- an array of `GveVisual` items (`visuals` property): the viewmodels of the rendered visuals.

The rendition is as follows:

1. set the attributes related to the SVG container as a whole:
    - width and height.
    - style.
    - background image with its X, Y, width, height.
    - base text style for the text root element.
    - operations style for the operations  root element.

2. if there are any `defs` defined, add them inside a newly created `defs` element, then appended to the `svg` element.

3. if required, create the rulers by adding SVG elements to the rulers `g` group. You can later toggle rulers off and on using `toggleRulers`, which just changes the rulers `g` element `display` style accordingly. So if you want rulers, specify this in the options or they will not be created and toggle will have no effect.

4. render the base text (delayed if requested): this gets the characters from the snapshot (or builds them if the snapshot text is just a string rather than an array of `CharNode`'s ), and renders them on 1 or more lines (every `\n` character ends a line). If the character has `x` and `y` among its features, these will be used as its coordinates, instead of the automatically calculated ones. Each character produces:
    - an SVG text appended to the text `g` element, with attributes `id`=`c_` + character ID (=its ordinal number), `x`, `y`, `dominant-baseline`=`hanging`; if using delayed rendering, the text is hidden with `opacity`=0 to be shown later in the rendition process. If the character node has features for style and classes, the corresponding attributes are added too.
    - a visual of type `GveTextVisual` added to the `visuals` property. This has ID=`c_<ID>`, and contains `x`, `y`, `type`=`char`, `width`, `height`, `lineNr`=current line number, `data`=the source `CharNode` object, and `element`=the SVG text element created for the character. Optionally it has `style` and `class` if set from features. If requested, the visual also gets event listeners added.

5. render operation visuals: each operation having a visual rendition produces:
    - a new `g` element as defined by the operation SVG `g` model. This element is added to the operations group element. The operation element also gets an `opacity`=0 attribute if requested (you can specify a list of transparent operations via the snapshot view `transparentIds` option).
    - a visual of type `GveVisual` added to the `visuals` property. This has ID = operation's ID, and contains `x`, `y`, `width`, `height`, `type`=`g`, data=the source `CharChainOperation` object, and `element`=the SVG `g` element created for the operation. Optionally it has `style` and `class` if set from features. If requested, the visual also gets event listeners added.

6. if delayed rendering is enabled, reposition text elements after measuring them and make them visible.

7. add pan and zoom support if requested.

### Popup Service

⚙️ You can use the `PopupService` to open/close a popup. In this case, you will need to include the following CSS:

```css
.popup {
  position: fixed;
  top: 0;
  left: 0;
  background-color: white;
  padding: 10px;
  box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
  z-index: 10;
}
@keyframes openPopup {
  0% {
    opacity: 0;
    transform: scale(0.5);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}
@keyframes closePopup {
  0% {
    opacity: 1;
    transform: scale(1);
  }
  100% {
    opacity: 0;
    transform: scale(0.5);
  }
}
.popup-open {
  animation: openPopup 0.3s forwards;
}
.popup-close {
  animation: closePopup 0.3s forwards;
}
```

### ParsedXmlTag

⚙️ This utility class represents an XML tag as parsed from a string. It can be used to manage its attributes (read, add, or update them), which can be useful when manipulating SVG code.

- 🔄 `name` (string): the tag name.
- 🔄 `type` (string): the tag type (open, close, empty).
- 🔄 `index`: the index of the first character of the tag (`<`) in the source XML string.
- 🔄 `length`: the length of the tag in the source XML string, starting from `index`.
- 🔄 `attributes`: the attributes in the tag:
  - 🔄 `name` (string): the name.
  - 🔄 `value` (string): the value.
  - 🔄 `index`: the index of the first character of the attribute's name in the source XML string.
  - 🔄 `length`: the length of the attribute in the source XML string, starting from its name and including its value if any.
  - 🔄 `singleQuote`: true if the attribute value was wrapped in single quotes rather than in double quotes in the source XML string.
- 🔵 `parse` (static): parse an XML tag from a string at the specified index.
- 🔵 `toString`: build an XML tag from the specified tag information, overriding the specified attributes values.

## Workspace Template

💡 These are the steps for building the template for a pure JS library in VSCode using Typescript, JEST testing, code maps, and webpack bundling like that used here:

1. create a new folder.
2. enter it and run `npm init`.
3. install these packages:

    ```bash
    npm i --save-dev source-map-loader ts-loader lite-server concurrency
    npm i --save-dev jest ts-jest @types/jest
    npm i --save-dev rollup rollup-plugin-terser @rollup/plugin-typescript @rollup/plugin-commonjs @rollup/plugin-node-resolve --force
    ```

4. change `package.json` to add the `scripts`, `exports`, `main`, `dependencies`, and `types`, like in the portion of its code sampled here:

    ```json
    {
      "main": "./dist/index.js",
      "type": "module",
      "types": "./dist/index.d.ts",
      "scripts": {
        "build": "rollup -c",
        "watch": "rollup -c -w",
        "start": "concurrently \"rollup -c -w\" \"lite-server\"",
        "test": "jest"
      },
      "exports": {
        ".": "./dist/index.js"
      },
      "dependencies": {
        "typescript": "^5.4.5"
      }
    }
    ```

5. add `tsconfig.json` for TS configuration:

    ```json
    {
      "compilerOptions": {
        "target": "es6",
        "module": "es6",
        "moduleResolution": "node",
        "lib": ["dom", "es2015"],
        "declaration": true,
        "outDir": "./dist",
        "strict": true,
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true
      },
      "include": ["src"],
      "exclude": ["node_modules", "dist"]
    }
    ```

6. add `rollup.config.mjs` for bundling:

    ```js
    import commonjs from "@rollup/plugin-commonjs";
    import typescript from "@rollup/plugin-typescript";
    import { terser } from "rollup-plugin-terser"; // Optional: Minification (for production)
    import { nodeResolve } from "@rollup/plugin-node-resolve";

    export default {
      input: "src/index.ts", // Entry point for your library
      output: [
        {
          file: "dist/index.js", // Output file for ES6 modules
          format: "es",
        },
        {
          file: "dist/index.cjs.min.js", // Output file for CommonJS (minified)
          format: "cjs",
          sourcemap: true,
          plugins: [terser()], // Minify for production
        },
      ],
      plugins: [
        typescript({ tsconfig: "./tsconfig.json" }), // Use your existing tsconfig.json
        commonjs(), // Convert CommonJS modules to ES6 (for GSAP),
        nodeResolve(), // Resolve node_modules
      ],
    };
    ```

7. add `jest.config.js` for testing:

    ```js
    module.exports = {
      preset: "ts-jest",
      testEnvironment: "node",
    };
    ```

8. create in `.vscode` [launch.json](./.vscode/launch.json) for debugging:

    ```json
    {
      "version": "0.2.0",
      "configurations": [
        {
          "type": "chrome",
          "request": "launch",
          "name": "Launch Chrome against localhost",
          "url": "http://localhost:3000",
          "webRoot": "${workspaceFolder}"
        }
      ]
    }
    ```

9. add [index.html](index.html) and its [consumer.js](consumer.js) script to the root to host your controls for testing. This should:

    - load your bundled library: `<script type="module" src="./dist/bundle.js"></script>`;
    - load the test consumer code: `<script type="module" src="consumer.js"></script>`;
    - use the web component by just placing its element in the page.
    - the test JS code must be inside `document.addEventListener("DOMContentLoaded", (event) => { });` to allow for the web component to load before interacting with it.

10. add your code (services and components) under `src`.

11. export all the required objects from the `src/index.ts` API entrypoint.

12. add `.npmignore` with a content like this:

    ```txt
    node_modules
    src
    *.md
    *.test.ts
    *.spec.ts
    *.spec.d.ts
    tsconfig.json
    package-lock.json
    *.log
    *.env
    .vscode
    consumer.js
    index.html
    jest.config.cjs
    rollup.config.mjs
    ```

>⚠️ Adding this is essential for NPM, otherwise it will use `.gitignore` for creating a package, which will result in an unusable package including just the sources, while we want the inverse. You can use `npm pack` to create a `.tz` file with the package to check its content before publishing.

You can now use:

- `npm run build` to build.
- `npm run start` to launch the server with the test page.
- `npm run watch` to start and watch for changes.
- `npm publish` to publish the package. Be sure to update the version in [package.json](package.json) as required before publishing.

## Note on Lite Server

The `lite-server` does not automatically rebuild your TypeScript files when they change. It only refreshes the browser when HTML or JavaScript files change.

To have your TypeScript files automatically recompile when you make changes, you can use `tsc --watch` command. This command starts the TypeScript compiler in watch mode; the compiler watches for file changes and recompiles when it sees them.

Then, you can run `npm run watch` in a separate terminal to start the TypeScript compiler in watch mode.

However, this will not refresh your browser when your TypeScript files are recompiled. To do this, you can use a tool like `concurrently` to run both `lite-server` and `tsc --watch` at the same time, and have `lite-server` refresh the browser whenever your compiled JavaScript files change.

Once installed this Update your start script to run both commands:

```json
"scripts": {
  "start": "concurrently \"tsc --watch\" \"lite-server\""
}
```

Now, when you run `npm start`, it will start both the TypeScript compiler in watch mode and lite-server. When you make changes to your TypeScript files, they will be automatically recompiled, and `lite-server` will refresh your browser.