@hydranium/glsp-client 1.0.0-next.301 → 1.0.0-next.303
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -7
- package/lib/diagram-mount.d.ts +100 -0
- package/lib/diagram-mount.d.ts.map +1 -0
- package/lib/diagram-mount.js +157 -0
- package/lib/diagram-mount.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/touch-input.d.ts +31 -0
- package/lib/touch-input.d.ts.map +1 -0
- package/lib/touch-input.js +97 -0
- package/lib/touch-input.js.map +1 -0
- package/package.json +4 -4
- package/src/diagram-mount.ts +218 -0
- package/src/index.ts +2 -0
- package/src/touch-input.ts +103 -0
package/README.md
CHANGED
|
@@ -8,8 +8,9 @@ VS Code webview, or a plain browser page. It builds on `@eclipse-glsp/client`.
|
|
|
8
8
|
|
|
9
9
|
## What it gives you
|
|
10
10
|
|
|
11
|
-
- Load failures reported through your logger and
|
|
12
|
-
|
|
11
|
+
- Load failures reported through your logger and, unless the action
|
|
12
|
+
dispatcher itself failed, on the diagram's status overlay, instead of
|
|
13
|
+
vanishing as an uncaught rejection.
|
|
13
14
|
- Action traffic and bounds measurements in your log, so a slow diagram names
|
|
14
15
|
its cost.
|
|
15
16
|
- Edge routes repaired before they go to the server while sprotty has not
|
|
@@ -22,6 +23,10 @@ VS Code webview, or a plain browser page. It builds on `@eclipse-glsp/client`.
|
|
|
22
23
|
pending, so two quick clicks on different elements leave only the second
|
|
23
24
|
selected.
|
|
24
25
|
- One binder that mounts all of it in a diagram container.
|
|
26
|
+
- For a plain page, one call that builds the container, loads the model and
|
|
27
|
+
supplies what a shell would: framing, canvas bounds that follow the
|
|
28
|
+
element's size and position, touch input, and a context menu GLSP stops
|
|
29
|
+
warning about.
|
|
25
30
|
|
|
26
31
|
## Install
|
|
27
32
|
|
|
@@ -71,16 +76,56 @@ without bendpoints `NaN` bounds, and a fit to screen over such a label blanks
|
|
|
71
76
|
the canvas. The class is fixed per registered type, so a type registered with
|
|
72
77
|
`GEdge` keeps the defect.
|
|
73
78
|
|
|
79
|
+
In a plain page, call `mountGlspDiagram` instead of building the container
|
|
80
|
+
yourself. It takes the element, your `HydraniumGlspClient` over the head's
|
|
81
|
+
connection, and your diagram module's initializer in GLSP's own shape, and it
|
|
82
|
+
calls `createGlspClientModule` for you:
|
|
83
|
+
|
|
84
|
+
<!-- snippet-preamble
|
|
85
|
+
import type { GlspDiagramMountOptions } from '@hydranium/glsp-client';
|
|
86
|
+
import type { Logger } from '@hydranium/protocol';
|
|
87
|
+
import type { MessageConnection } from 'vscode-jsonrpc';
|
|
88
|
+
declare const connection: MessageConnection;
|
|
89
|
+
declare const logger: Logger;
|
|
90
|
+
declare const initializeMyDiagramContainer: GlspDiagramMountOptions['initializeDiagramContainer'];
|
|
91
|
+
-->
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { HydraniumGlspClient, mountGlspDiagram } from '@hydranium/glsp-client';
|
|
95
|
+
import { sendByMethodName } from '@hydranium/protocol';
|
|
96
|
+
|
|
97
|
+
const glspClient = new HydraniumGlspClient({ id: 'my-app', connectionProvider: sendByMethodName(connection) });
|
|
98
|
+
const diagram = await mountGlspDiagram({
|
|
99
|
+
mount: document.getElementById('my-diagram')!,
|
|
100
|
+
diagramType: 'my-diagram',
|
|
101
|
+
sourceUri: 'file:///workspace/my.model',
|
|
102
|
+
glspClient,
|
|
103
|
+
initializeDiagramContainer: initializeMyDiagramContainer,
|
|
104
|
+
logger
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Wrap the connection with `sendByMethodName`: GLSP's client sends message
|
|
109
|
+
types built by its own copy of `vscode-jsonrpc`, which another copy rejects.
|
|
110
|
+
One client serves every diagram on the page, each mounted as its own session.
|
|
111
|
+
The element's `id` is the diagram's `clientId`, so it needs one; give it a
|
|
112
|
+
height, `position: relative` so the palette stays on it, `touch-action: none`
|
|
113
|
+
for touch, and size its `<svg>` to it, or sprotty draws into 300×150. The
|
|
114
|
+
first render replaces the element with sprotty's own under the same id, so
|
|
115
|
+
keep no reference to it and find it by id when you need it. A failed load
|
|
116
|
+
resolves the mount; read `diagram.loadOutcome` to report it, since the status
|
|
117
|
+
overlay cannot show a failure of the action dispatcher itself.
|
|
118
|
+
|
|
74
119
|
For a browser page, see *Host in a browser* in
|
|
75
120
|
[Adopting Hydranium](https://github.com/eclipse-emfcloud/hydranium/blob/main/docs/ADOPTING.md).
|
|
76
121
|
|
|
77
122
|
## Entry points
|
|
78
123
|
|
|
79
|
-
| Subpath | Use it for
|
|
80
|
-
| ------------ |
|
|
81
|
-
| `.` | The classes, the binder and the
|
|
82
|
-
| `./messages` | The codes of the messages this package raises
|
|
83
|
-
| `./testing` | A binding recorder for testing your modules
|
|
124
|
+
| Subpath | Use it for | Runs in |
|
|
125
|
+
| ------------ | --------------------------------------------------------- | --------------- |
|
|
126
|
+
| `.` | The classes, the binder, the DI tokens and the page mount | browser |
|
|
127
|
+
| `./messages` | The codes of the messages this package raises | browser-neutral |
|
|
128
|
+
| `./testing` | A binding recorder for testing your modules | browser-neutral |
|
|
84
129
|
|
|
85
130
|
The root loads `@eclipse-glsp/client`, whose graph imports stylesheets, so
|
|
86
131
|
your bundler needs a CSS loader. The subpaths need a TypeScript
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
import { type ContainerConfiguration } from '@eclipse-glsp/client';
|
|
10
|
+
import { type Disposable, type Logger } from '@hydranium/protocol';
|
|
11
|
+
import { Container } from 'inversify';
|
|
12
|
+
import { type DiagramLoadOutcome } from './diagram-loader';
|
|
13
|
+
import type { HydraniumGlspClient } from './glsp-client';
|
|
14
|
+
/** Options for {@link mountGlspDiagram}. */
|
|
15
|
+
export interface GlspDiagramMountOptions {
|
|
16
|
+
/** The element the diagram renders into. Its `id` becomes the diagram's
|
|
17
|
+
* `clientId`, from which sprotty derives its base div, so an element
|
|
18
|
+
* without one is refused rather than left as an empty page. sprotty's
|
|
19
|
+
* first render replaces the element with one of its own under the same
|
|
20
|
+
* id, so keep no reference to it: the mount watches the live one. */
|
|
21
|
+
readonly mount: HTMLElement;
|
|
22
|
+
/** The diagram type the GLSP head serves. */
|
|
23
|
+
readonly diagramType: string;
|
|
24
|
+
/** The document the diagram shows. */
|
|
25
|
+
readonly sourceUri: string;
|
|
26
|
+
/** The client over the GLSP head's connection. One client serves every
|
|
27
|
+
* diagram on the page, each as its own client session. */
|
|
28
|
+
readonly glspClient: HydraniumGlspClient;
|
|
29
|
+
/** The diagram definition's initializer in GLSP's own shape: it receives the
|
|
30
|
+
* host's modules and appends the definition last, since GLSP's registries
|
|
31
|
+
* give an element type to whichever module registered it last. */
|
|
32
|
+
readonly initializeDiagramContainer: (container: Container, ...configuration: ContainerConfiguration) => Container;
|
|
33
|
+
/** The host's logger, as {@link createGlspClientModule} takes it. */
|
|
34
|
+
readonly logger?: Logger;
|
|
35
|
+
}
|
|
36
|
+
/** A diagram {@link mountGlspDiagram} put on a page. */
|
|
37
|
+
export interface GlspDiagramMount extends Disposable {
|
|
38
|
+
/** The diagram container, for the services a page reads after the mount. */
|
|
39
|
+
readonly container: Container;
|
|
40
|
+
/** How the load ended. A failed load resolves the mount, so this is where
|
|
41
|
+
* the caller learns of it; when the failure is not `surfaced`, no overlay
|
|
42
|
+
* shows it and the caller is the only report left. */
|
|
43
|
+
readonly loadOutcome: DiagramLoadOutcome;
|
|
44
|
+
/** Ends the client session on the server and stops watching the element;
|
|
45
|
+
* what is drawn in it is left to the host to remove. */
|
|
46
|
+
dispose(): void;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The canvas kept around the model when it is fitted, in model units before
|
|
50
|
+
* the zoom. Enough that the outermost shape's border is not the edge of the
|
|
51
|
+
* pane and no more, since the padding is taken from the space the fit has.
|
|
52
|
+
*/
|
|
53
|
+
export declare const DIAGRAM_FIT_PADDING = 24;
|
|
54
|
+
/**
|
|
55
|
+
* Mount a GLSP diagram in a page element: build the container over GLSP's
|
|
56
|
+
* standalone modules and the framework's binder, load the model, and supply
|
|
57
|
+
* what a shell would. Resolves once the model is loaded and framed, so a caller
|
|
58
|
+
* that then reads the model sees the viewport it is drawn in.
|
|
59
|
+
*
|
|
60
|
+
* The accommodations a page needs from whatever hosts the diagram, each silent
|
|
61
|
+
* when missing, so none is optional:
|
|
62
|
+
*
|
|
63
|
+
* - **Framing.** sprotty leaves the viewport at scale 1 and the origin top-left
|
|
64
|
+
* after a load, so a model wider than the pane is cut off at the right with
|
|
65
|
+
* no sign anything is missing. The model is fitted with
|
|
66
|
+
* {@link DIAGRAM_FIT_PADDING}, zooming out only: a fit that zooms in makes a
|
|
67
|
+
* two-node diagram render four times the size of a ten-node one. To frame
|
|
68
|
+
* differently, dispatch your own viewport action on the container after the
|
|
69
|
+
* mount resolves. Register edges with `HydraniumGEdge`: GLSP's own
|
|
70
|
+
* gives the labels of an edge without bendpoints no finite bounds, and the
|
|
71
|
+
* fit then blanks the canvas.
|
|
72
|
+
* - **Bounds tracking.** After the first render, sprotty reads its canvas
|
|
73
|
+
* bounds again on `window.resize` and on nothing else. It culls every shape
|
|
74
|
+
* outside the bounds it believes it has, so a pane widened by a divider
|
|
75
|
+
* stays empty in the new space, and it maps pointer positions through their
|
|
76
|
+
* corner, so after a region above the element folds, a palette drop and a
|
|
77
|
+
* wheel zoom land off by the distance the element moved. A `ResizeObserver`
|
|
78
|
+
* on the element and each ancestor, and a `scroll` listener for a container
|
|
79
|
+
* that scrolls, send the element's box in page coordinates whenever it
|
|
80
|
+
* differs from the model's. A zero-sized box, which a pane dragged shut
|
|
81
|
+
* reports, is skipped: a zero canvas culls the whole model.
|
|
82
|
+
* - **Touch input.** sprotty binds mouse events only; see
|
|
83
|
+
* {@link enableTouchDragging}. Give the element `touch-action: none`.
|
|
84
|
+
* - **The context menu.** GLSP warns on every container built without a
|
|
85
|
+
* `TYPES.IContextMenuService`, and the console is a page's only log. A no-op
|
|
86
|
+
* is bound when the host bound none; a menu the host binds, in its
|
|
87
|
+
* initializer or a module of its own, is left alone.
|
|
88
|
+
*
|
|
89
|
+
* A failed load does not reject: the loader reports it through the logger and,
|
|
90
|
+
* unless the action dispatcher is what failed, on the diagram's status
|
|
91
|
+
* overlay. The mount resolves without the accommodations, which a model that
|
|
92
|
+
* never arrived has no use for, and with the outcome in
|
|
93
|
+
* {@link GlspDiagramMount.loadOutcome}. Any other failure unbinds the
|
|
94
|
+
* container and rejects.
|
|
95
|
+
*
|
|
96
|
+
* The element also needs a height, `position: relative` so the palette stays
|
|
97
|
+
* on it, and its `<svg>` sized to it, or sprotty draws into 300×150.
|
|
98
|
+
*/
|
|
99
|
+
export declare function mountGlspDiagram(options: GlspDiagramMountOptions): Promise<GlspDiagramMount>;
|
|
100
|
+
//# sourceMappingURL=diagram-mount.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagram-mount.d.ts","sourceRoot":"","sources":["../src/diagram-mount.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAEJ,KAAK,sBAAsB,EAU7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,KAAK,UAAU,EAAwB,KAAK,MAAM,EAAE,MAAM,qBAAqB,CAAC;AACzF,OAAO,EAAE,SAAS,EAAmB,MAAM,WAAW,CAAC;AACvD,OAAO,EAAE,KAAK,kBAAkB,EAA0B,MAAM,kBAAkB,CAAC;AACnF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAKzD,4CAA4C;AAC5C,MAAM,WAAW,uBAAuB;IACrC;;;;0EAIsE;IACtE,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,6CAA6C;IAC7C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,sCAAsC;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;+DAC2D;IAC3D,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAC;IACzC;;uEAEmE;IACnE,QAAQ,CAAC,0BAA0B,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,GAAG,aAAa,EAAE,sBAAsB,KAAK,SAAS,CAAC;IACnH,qEAAqE;IACrE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wDAAwD;AACxD,MAAM,WAAW,gBAAiB,SAAQ,UAAU;IACjD,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B;;2DAEuD;IACvD,QAAQ,CAAC,WAAW,EAAE,kBAAkB,CAAC;IACzC;6DACyD;IACzD,OAAO,IAAI,IAAI,CAAC;CAClB;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAiElG"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/********************************************************************************
|
|
3
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
4
|
+
*
|
|
5
|
+
* This program and the accompanying materials are made available under the
|
|
6
|
+
* terms of the MIT License which is available in the project root.
|
|
7
|
+
*
|
|
8
|
+
* SPDX-License-Identifier: MIT
|
|
9
|
+
********************************************************************************/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.DIAGRAM_FIT_PADDING = void 0;
|
|
12
|
+
exports.mountGlspDiagram = mountGlspDiagram;
|
|
13
|
+
const client_1 = require("@eclipse-glsp/client");
|
|
14
|
+
const protocol_1 = require("@hydranium/protocol");
|
|
15
|
+
const inversify_1 = require("inversify");
|
|
16
|
+
const diagram_loader_1 = require("./diagram-loader");
|
|
17
|
+
const glsp_client_module_1 = require("./glsp-client-module");
|
|
18
|
+
const touch_input_1 = require("./touch-input");
|
|
19
|
+
const types_1 = require("./types");
|
|
20
|
+
/**
|
|
21
|
+
* The canvas kept around the model when it is fitted, in model units before
|
|
22
|
+
* the zoom. Enough that the outermost shape's border is not the edge of the
|
|
23
|
+
* pane and no more, since the padding is taken from the space the fit has.
|
|
24
|
+
*/
|
|
25
|
+
exports.DIAGRAM_FIT_PADDING = 24;
|
|
26
|
+
/**
|
|
27
|
+
* Mount a GLSP diagram in a page element: build the container over GLSP's
|
|
28
|
+
* standalone modules and the framework's binder, load the model, and supply
|
|
29
|
+
* what a shell would. Resolves once the model is loaded and framed, so a caller
|
|
30
|
+
* that then reads the model sees the viewport it is drawn in.
|
|
31
|
+
*
|
|
32
|
+
* The accommodations a page needs from whatever hosts the diagram, each silent
|
|
33
|
+
* when missing, so none is optional:
|
|
34
|
+
*
|
|
35
|
+
* - **Framing.** sprotty leaves the viewport at scale 1 and the origin top-left
|
|
36
|
+
* after a load, so a model wider than the pane is cut off at the right with
|
|
37
|
+
* no sign anything is missing. The model is fitted with
|
|
38
|
+
* {@link DIAGRAM_FIT_PADDING}, zooming out only: a fit that zooms in makes a
|
|
39
|
+
* two-node diagram render four times the size of a ten-node one. To frame
|
|
40
|
+
* differently, dispatch your own viewport action on the container after the
|
|
41
|
+
* mount resolves. Register edges with `HydraniumGEdge`: GLSP's own
|
|
42
|
+
* gives the labels of an edge without bendpoints no finite bounds, and the
|
|
43
|
+
* fit then blanks the canvas.
|
|
44
|
+
* - **Bounds tracking.** After the first render, sprotty reads its canvas
|
|
45
|
+
* bounds again on `window.resize` and on nothing else. It culls every shape
|
|
46
|
+
* outside the bounds it believes it has, so a pane widened by a divider
|
|
47
|
+
* stays empty in the new space, and it maps pointer positions through their
|
|
48
|
+
* corner, so after a region above the element folds, a palette drop and a
|
|
49
|
+
* wheel zoom land off by the distance the element moved. A `ResizeObserver`
|
|
50
|
+
* on the element and each ancestor, and a `scroll` listener for a container
|
|
51
|
+
* that scrolls, send the element's box in page coordinates whenever it
|
|
52
|
+
* differs from the model's. A zero-sized box, which a pane dragged shut
|
|
53
|
+
* reports, is skipped: a zero canvas culls the whole model.
|
|
54
|
+
* - **Touch input.** sprotty binds mouse events only; see
|
|
55
|
+
* {@link enableTouchDragging}. Give the element `touch-action: none`.
|
|
56
|
+
* - **The context menu.** GLSP warns on every container built without a
|
|
57
|
+
* `TYPES.IContextMenuService`, and the console is a page's only log. A no-op
|
|
58
|
+
* is bound when the host bound none; a menu the host binds, in its
|
|
59
|
+
* initializer or a module of its own, is left alone.
|
|
60
|
+
*
|
|
61
|
+
* A failed load does not reject: the loader reports it through the logger and,
|
|
62
|
+
* unless the action dispatcher is what failed, on the diagram's status
|
|
63
|
+
* overlay. The mount resolves without the accommodations, which a model that
|
|
64
|
+
* never arrived has no use for, and with the outcome in
|
|
65
|
+
* {@link GlspDiagramMount.loadOutcome}. Any other failure unbinds the
|
|
66
|
+
* container and rejects.
|
|
67
|
+
*
|
|
68
|
+
* The element also needs a height, `position: relative` so the palette stays
|
|
69
|
+
* on it, and its `<svg>` sized to it, or sprotty draws into 300×150.
|
|
70
|
+
*/
|
|
71
|
+
async function mountGlspDiagram(options) {
|
|
72
|
+
const { mount } = options;
|
|
73
|
+
if (mount.id === '') {
|
|
74
|
+
throw new Error("mountGlspDiagram: the mount element needs an id, which becomes the diagram's clientId");
|
|
75
|
+
}
|
|
76
|
+
const diagramOptions = {
|
|
77
|
+
clientId: mount.id,
|
|
78
|
+
diagramType: options.diagramType,
|
|
79
|
+
glspClientProvider: async () => options.glspClient,
|
|
80
|
+
sourceUri: options.sourceUri
|
|
81
|
+
};
|
|
82
|
+
// After the standalone modules, since every class the binder binds replaces
|
|
83
|
+
// a token those modules bind.
|
|
84
|
+
const hydraniumModule = new inversify_1.ContainerModule((bind, unbind, isBound, rebind) => {
|
|
85
|
+
(0, glsp_client_module_1.createGlspClientModule)({ bind, unbind, isBound, rebind }, { logger: options.logger });
|
|
86
|
+
});
|
|
87
|
+
const container = options.initializeDiagramContainer(new inversify_1.Container(), (0, client_1.createDiagramOptionsModule)(diagramOptions), client_1.STANDALONE_MODULE_CONFIG, hydraniumModule);
|
|
88
|
+
// After the container is built rather than in a module, so the host's own
|
|
89
|
+
// binding wins wherever its module sits in the order.
|
|
90
|
+
if (!container.isBound(client_1.TYPES.IContextMenuService)) {
|
|
91
|
+
container.bind(client_1.TYPES.IContextMenuService).toConstantValue({ show: () => undefined });
|
|
92
|
+
}
|
|
93
|
+
const toDispose = new protocol_1.DisposableCollection();
|
|
94
|
+
// Pushed first, so it runs last: `unbindAll` reaches `disposeClientSession`
|
|
95
|
+
// through the model source's pre-destroy hook, after the listeners are gone.
|
|
96
|
+
toDispose.push({ dispose: () => container.unbindAll() });
|
|
97
|
+
const dispose = () => toDispose.dispose();
|
|
98
|
+
try {
|
|
99
|
+
const loader = container.get(client_1.DiagramLoader);
|
|
100
|
+
await loader.load();
|
|
101
|
+
// A loader that is not the framework's rejects on a failure instead.
|
|
102
|
+
const loadOutcome = (loader instanceof diagram_loader_1.HydraniumDiagramLoader ? loader.loadOutcome : undefined) ?? { status: 'loaded' };
|
|
103
|
+
// A model that never arrived has no use for the accommodations, and when
|
|
104
|
+
// the dispatcher is what failed, every later dispatch rejects too.
|
|
105
|
+
if (loadOutcome.status === 'failed') {
|
|
106
|
+
return { container, loadOutcome, dispose };
|
|
107
|
+
}
|
|
108
|
+
const dispatcher = container.get(client_1.TYPES.IActionDispatcher);
|
|
109
|
+
// The bounds the fit measures come from the hidden rendering pass inside
|
|
110
|
+
// `load`; a fit dispatched before it has nothing to measure and leaves the
|
|
111
|
+
// viewport where it was.
|
|
112
|
+
await dispatcher.dispatch(client_1.FitToScreenAction.create([], { padding: exports.DIAGRAM_FIT_PADDING, maxZoom: 1, animate: false }));
|
|
113
|
+
// By id and not through `mount`: the load's first render replaced that
|
|
114
|
+
// element, and a listener on the replaced one never fires.
|
|
115
|
+
const live = document.getElementById(mount.id);
|
|
116
|
+
if (live === null) {
|
|
117
|
+
throw new Error(`mountGlspDiagram: the element '${mount.id}' left the document while the diagram loaded`);
|
|
118
|
+
}
|
|
119
|
+
toDispose.push(trackCanvasBounds(live, dispatcher, container.get(client_1.EditorContextService), container.get(types_1.HydraniumClientTypes.Logger)));
|
|
120
|
+
toDispose.push((0, touch_input_1.enableTouchDragging)(live));
|
|
121
|
+
return { container, loadOutcome, dispose };
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
// The caller gets no mount to dispose, so the session would outlive it.
|
|
125
|
+
toDispose.dispose();
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function trackCanvasBounds(mount, dispatcher, editorContext, logger) {
|
|
130
|
+
const update = () => {
|
|
131
|
+
const box = mount.getBoundingClientRect();
|
|
132
|
+
if (box.width === 0 || box.height === 0) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const bounds = { x: box.left + window.scrollX, y: box.top + window.scrollY, width: box.width, height: box.height };
|
|
136
|
+
// Against the model's bounds rather than the last ones sent, since sprotty
|
|
137
|
+
// sets them too on `window.resize`; and skipped when equal, since every
|
|
138
|
+
// page scroll lands here and each dispatch redraws the diagram.
|
|
139
|
+
if (!client_1.Bounds.equals(bounds, editorContext.canvasBounds)) {
|
|
140
|
+
dispatcher
|
|
141
|
+
.dispatch(client_1.InitializeCanvasBoundsAction.create(bounds))
|
|
142
|
+
.catch((err) => logger.error("Failed to update the diagram's canvas bounds", err));
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const observer = new ResizeObserver(update);
|
|
146
|
+
for (let element = mount; element !== null; element = element.parentElement) {
|
|
147
|
+
observer.observe(element);
|
|
148
|
+
}
|
|
149
|
+
document.addEventListener('scroll', update, { capture: true, passive: true });
|
|
150
|
+
return {
|
|
151
|
+
dispose: () => {
|
|
152
|
+
observer.disconnect();
|
|
153
|
+
document.removeEventListener('scroll', update, { capture: true });
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=diagram-mount.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagram-mount.js","sourceRoot":"","sources":["../src/diagram-mount.ts"],"names":[],"mappings":"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"}
|
package/lib/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
export * from './action-dispatcher';
|
|
10
10
|
export * from './diagram-load-failed';
|
|
11
11
|
export * from './diagram-loader';
|
|
12
|
+
export * from './diagram-mount';
|
|
12
13
|
export * from './edge';
|
|
13
14
|
export * from './glsp-client';
|
|
14
15
|
export * from './glsp-client-module';
|
|
@@ -16,5 +17,6 @@ export * from './hidden-bounds-updater';
|
|
|
16
17
|
export * from './select-mouse-listener';
|
|
17
18
|
export * from './sprotty-logger-adapter';
|
|
18
19
|
export * from './status-overlay';
|
|
20
|
+
export * from './touch-input';
|
|
19
21
|
export * from './types';
|
|
20
22
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAOlF,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAOlF,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,QAAQ,CAAC;AACvB,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC"}
|
package/lib/index.js
CHANGED
|
@@ -30,6 +30,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
30
30
|
__exportStar(require("./action-dispatcher"), exports);
|
|
31
31
|
__exportStar(require("./diagram-load-failed"), exports);
|
|
32
32
|
__exportStar(require("./diagram-loader"), exports);
|
|
33
|
+
__exportStar(require("./diagram-mount"), exports);
|
|
33
34
|
__exportStar(require("./edge"), exports);
|
|
34
35
|
__exportStar(require("./glsp-client"), exports);
|
|
35
36
|
__exportStar(require("./glsp-client-module"), exports);
|
|
@@ -37,5 +38,6 @@ __exportStar(require("./hidden-bounds-updater"), exports);
|
|
|
37
38
|
__exportStar(require("./select-mouse-listener"), exports);
|
|
38
39
|
__exportStar(require("./sprotty-logger-adapter"), exports);
|
|
39
40
|
__exportStar(require("./status-overlay"), exports);
|
|
41
|
+
__exportStar(require("./touch-input"), exports);
|
|
40
42
|
__exportStar(require("./types"), exports);
|
|
41
43
|
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;;;;;;;;;;;;;;AAElF,6EAA6E;AAC7E,6EAA6E;AAC7E,4EAA4E;AAC5E,4EAA4E;AAC5E,SAAS;AACT,sDAAoC;AACpC,wDAAsC;AACtC,mDAAiC;AACjC,yCAAuB;AACvB,gDAA8B;AAC9B,uDAAqC;AACrC,0DAAwC;AACxC,0DAAwC;AACxC,2DAAyC;AACzC,mDAAiC;AACjC,0CAAwB"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;kFAOkF;;;;;;;;;;;;;;;;AAElF,6EAA6E;AAC7E,6EAA6E;AAC7E,4EAA4E;AAC5E,4EAA4E;AAC5E,SAAS;AACT,sDAAoC;AACpC,wDAAsC;AACtC,mDAAiC;AACjC,kDAAgC;AAChC,yCAAuB;AACvB,gDAA8B;AAC9B,uDAAqC;AACrC,0DAAwC;AACxC,0DAAwC;AACxC,2DAAyC;AACzC,mDAAiC;AACjC,gDAA8B;AAC9B,0CAAwB"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
import { type Disposable } from '@hydranium/protocol';
|
|
10
|
+
/**
|
|
11
|
+
* Re-emit touch gestures over `mount` as the mouse events its canvas binds.
|
|
12
|
+
*
|
|
13
|
+
* sprotty's `MouseTool` registers `mousedown`, `mousemove` and `mouseup` and no
|
|
14
|
+
* pointer or touch listener, and a browser synthesizes mouse events for a tap
|
|
15
|
+
* and not for a drag, so a finger drag reaches the diagram as a complete
|
|
16
|
+
* pointer stream that moves nothing, with a clean console and a full model.
|
|
17
|
+
*
|
|
18
|
+
* A shim on the host rather than a change to the diagram client, because the
|
|
19
|
+
* gap is in which event family is bound: re-emitting the gesture as the bound
|
|
20
|
+
* family needs nothing from the model, the tools or the container. Pairs with
|
|
21
|
+
* `touch-action: none` on the same element, without which the browser takes the
|
|
22
|
+
* drag as a pan and cancels the pointer stream before the second move.
|
|
23
|
+
*
|
|
24
|
+
* Mouse and pen pointers are left alone: the canvas already hears those, and
|
|
25
|
+
* forwarding them would run every gesture twice. So is a second finger while
|
|
26
|
+
* one drags, since the canvas knows one mouse and its press would end the drag,
|
|
27
|
+
* and a press on a text field, which needs the browser's own press to take the
|
|
28
|
+
* focus and the caret.
|
|
29
|
+
*/
|
|
30
|
+
export declare function enableTouchDragging(mount: HTMLElement): Disposable;
|
|
31
|
+
//# sourceMappingURL=touch-input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"touch-input.d.ts","sourceRoot":"","sources":["../src/touch-input.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAAE,KAAK,UAAU,EAAwB,MAAM,qBAAqB,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,WAAW,GAAG,UAAU,CAmElE"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/********************************************************************************
|
|
3
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
4
|
+
*
|
|
5
|
+
* This program and the accompanying materials are made available under the
|
|
6
|
+
* terms of the MIT License which is available in the project root.
|
|
7
|
+
*
|
|
8
|
+
* SPDX-License-Identifier: MIT
|
|
9
|
+
********************************************************************************/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.enableTouchDragging = enableTouchDragging;
|
|
12
|
+
const protocol_1 = require("@hydranium/protocol");
|
|
13
|
+
/**
|
|
14
|
+
* Re-emit touch gestures over `mount` as the mouse events its canvas binds.
|
|
15
|
+
*
|
|
16
|
+
* sprotty's `MouseTool` registers `mousedown`, `mousemove` and `mouseup` and no
|
|
17
|
+
* pointer or touch listener, and a browser synthesizes mouse events for a tap
|
|
18
|
+
* and not for a drag, so a finger drag reaches the diagram as a complete
|
|
19
|
+
* pointer stream that moves nothing, with a clean console and a full model.
|
|
20
|
+
*
|
|
21
|
+
* A shim on the host rather than a change to the diagram client, because the
|
|
22
|
+
* gap is in which event family is bound: re-emitting the gesture as the bound
|
|
23
|
+
* family needs nothing from the model, the tools or the container. Pairs with
|
|
24
|
+
* `touch-action: none` on the same element, without which the browser takes the
|
|
25
|
+
* drag as a pan and cancels the pointer stream before the second move.
|
|
26
|
+
*
|
|
27
|
+
* Mouse and pen pointers are left alone: the canvas already hears those, and
|
|
28
|
+
* forwarding them would run every gesture twice. So is a second finger while
|
|
29
|
+
* one drags, since the canvas knows one mouse and its press would end the drag,
|
|
30
|
+
* and a press on a text field, which needs the browser's own press to take the
|
|
31
|
+
* focus and the caret.
|
|
32
|
+
*/
|
|
33
|
+
function enableTouchDragging(mount) {
|
|
34
|
+
/** The finger that started the drag and the element under it; every event of
|
|
35
|
+
* the drag goes to that element. */
|
|
36
|
+
let drag;
|
|
37
|
+
const dispatch = (type, source) => {
|
|
38
|
+
drag?.target?.dispatchEvent(new MouseEvent(type, {
|
|
39
|
+
bubbles: true,
|
|
40
|
+
cancelable: true,
|
|
41
|
+
clientX: source.clientX,
|
|
42
|
+
clientY: source.clientY,
|
|
43
|
+
button: 0,
|
|
44
|
+
buttons: type === 'mouseup' ? 0 : 1,
|
|
45
|
+
view: window
|
|
46
|
+
}));
|
|
47
|
+
};
|
|
48
|
+
const press = (event) => {
|
|
49
|
+
if (event.pointerType !== 'touch' || drag !== undefined || isTextField(event.target)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Cancelled, or the browser follows a tap with mouse events of its own and
|
|
53
|
+
// the canvas hears the gesture twice; a `click` still follows.
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
// Without the capture the stream stops when the finger leaves the element
|
|
56
|
+
// it started on, which during a node drag is immediately.
|
|
57
|
+
mount.setPointerCapture(event.pointerId);
|
|
58
|
+
drag = { pointerId: event.pointerId, target: document.elementFromPoint(event.clientX, event.clientY) };
|
|
59
|
+
// A priming move BEFORE the press, or the drag is a silent no-op: sprotty's
|
|
60
|
+
// `MousePositionTracker` writes the origin the change-bounds tool reads at
|
|
61
|
+
// `mousedown` on `mousemove` alone, so a pointer that never hovered leaves
|
|
62
|
+
// it unset and the node is dropped at 0,0.
|
|
63
|
+
dispatch('mousemove', event);
|
|
64
|
+
dispatch('mousedown', event);
|
|
65
|
+
};
|
|
66
|
+
const move = (event) => {
|
|
67
|
+
if (event.pointerId === drag?.pointerId) {
|
|
68
|
+
dispatch('mousemove', event);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const release = (event) => {
|
|
72
|
+
if (event.pointerId !== drag?.pointerId) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
dispatch('mouseup', event);
|
|
76
|
+
drag = undefined;
|
|
77
|
+
};
|
|
78
|
+
const listeners = [
|
|
79
|
+
['pointerdown', press],
|
|
80
|
+
['pointermove', move],
|
|
81
|
+
['pointerup', release],
|
|
82
|
+
// The browser fires `pointercancel` instead of `pointerup` when it takes
|
|
83
|
+
// the pointer over; without a release here the next touch resumes the
|
|
84
|
+
// abandoned drag.
|
|
85
|
+
['pointercancel', release]
|
|
86
|
+
];
|
|
87
|
+
const toDispose = new protocol_1.DisposableCollection();
|
|
88
|
+
for (const [type, listener] of listeners) {
|
|
89
|
+
mount.addEventListener(type, listener);
|
|
90
|
+
toDispose.push({ dispose: () => mount.removeEventListener(type, listener) });
|
|
91
|
+
}
|
|
92
|
+
return toDispose;
|
|
93
|
+
}
|
|
94
|
+
function isTextField(target) {
|
|
95
|
+
return target instanceof Element && target.closest('input, textarea, select, [contenteditable]:not([contenteditable="false"])') !== null;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=touch-input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"touch-input.js","sourceRoot":"","sources":["../src/touch-input.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hydranium/glsp-client",
|
|
3
|
-
"version": "1.0.0-next.
|
|
4
|
-
"description": "Host-neutral client classes for GLSP heads built on @hydranium/glsp-server, the binder that mounts them in a Theia frontend, a VS Code webview or a plain browser page, and the edge whose labels keep finite bounds without bendpoints.",
|
|
3
|
+
"version": "1.0.0-next.303",
|
|
4
|
+
"description": "Host-neutral client classes for GLSP heads built on @hydranium/glsp-server, the binder that mounts them in a Theia frontend, a VS Code webview or a plain browser page, the one call that mounts a diagram in a page, and the edge whose labels keep finite bounds without bendpoints.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"diagram",
|
|
7
7
|
"glsp",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@eclipse-glsp/client": "2.7.0",
|
|
62
|
-
"@hydranium/protocol": "1.0.0-next.
|
|
62
|
+
"@hydranium/protocol": "1.0.0-next.303",
|
|
63
63
|
"@types/node": "^22.0.0",
|
|
64
64
|
"inversify": "6.2.2",
|
|
65
65
|
"reflect-metadata": "0.2.2",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"@eclipse-glsp/client": "^2.6.0",
|
|
72
|
-
"@hydranium/protocol": "1.0.0-next.
|
|
72
|
+
"@hydranium/protocol": "1.0.0-next.303",
|
|
73
73
|
"inversify": "^6.0.0",
|
|
74
74
|
"snabbdom": "^3.5.1"
|
|
75
75
|
},
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
Bounds,
|
|
12
|
+
type ContainerConfiguration,
|
|
13
|
+
createDiagramOptionsModule,
|
|
14
|
+
DiagramLoader,
|
|
15
|
+
EditorContextService,
|
|
16
|
+
FitToScreenAction,
|
|
17
|
+
type GLSPActionDispatcher,
|
|
18
|
+
type IDiagramOptions,
|
|
19
|
+
InitializeCanvasBoundsAction,
|
|
20
|
+
STANDALONE_MODULE_CONFIG,
|
|
21
|
+
TYPES
|
|
22
|
+
} from '@eclipse-glsp/client';
|
|
23
|
+
import { type Disposable, DisposableCollection, type Logger } from '@hydranium/protocol';
|
|
24
|
+
import { Container, ContainerModule } from 'inversify';
|
|
25
|
+
import { type DiagramLoadOutcome, HydraniumDiagramLoader } from './diagram-loader';
|
|
26
|
+
import type { HydraniumGlspClient } from './glsp-client';
|
|
27
|
+
import { createGlspClientModule } from './glsp-client-module';
|
|
28
|
+
import { enableTouchDragging } from './touch-input';
|
|
29
|
+
import { HydraniumClientTypes } from './types';
|
|
30
|
+
|
|
31
|
+
/** Options for {@link mountGlspDiagram}. */
|
|
32
|
+
export interface GlspDiagramMountOptions {
|
|
33
|
+
/** The element the diagram renders into. Its `id` becomes the diagram's
|
|
34
|
+
* `clientId`, from which sprotty derives its base div, so an element
|
|
35
|
+
* without one is refused rather than left as an empty page. sprotty's
|
|
36
|
+
* first render replaces the element with one of its own under the same
|
|
37
|
+
* id, so keep no reference to it: the mount watches the live one. */
|
|
38
|
+
readonly mount: HTMLElement;
|
|
39
|
+
/** The diagram type the GLSP head serves. */
|
|
40
|
+
readonly diagramType: string;
|
|
41
|
+
/** The document the diagram shows. */
|
|
42
|
+
readonly sourceUri: string;
|
|
43
|
+
/** The client over the GLSP head's connection. One client serves every
|
|
44
|
+
* diagram on the page, each as its own client session. */
|
|
45
|
+
readonly glspClient: HydraniumGlspClient;
|
|
46
|
+
/** The diagram definition's initializer in GLSP's own shape: it receives the
|
|
47
|
+
* host's modules and appends the definition last, since GLSP's registries
|
|
48
|
+
* give an element type to whichever module registered it last. */
|
|
49
|
+
readonly initializeDiagramContainer: (container: Container, ...configuration: ContainerConfiguration) => Container;
|
|
50
|
+
/** The host's logger, as {@link createGlspClientModule} takes it. */
|
|
51
|
+
readonly logger?: Logger;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A diagram {@link mountGlspDiagram} put on a page. */
|
|
55
|
+
export interface GlspDiagramMount extends Disposable {
|
|
56
|
+
/** The diagram container, for the services a page reads after the mount. */
|
|
57
|
+
readonly container: Container;
|
|
58
|
+
/** How the load ended. A failed load resolves the mount, so this is where
|
|
59
|
+
* the caller learns of it; when the failure is not `surfaced`, no overlay
|
|
60
|
+
* shows it and the caller is the only report left. */
|
|
61
|
+
readonly loadOutcome: DiagramLoadOutcome;
|
|
62
|
+
/** Ends the client session on the server and stops watching the element;
|
|
63
|
+
* what is drawn in it is left to the host to remove. */
|
|
64
|
+
dispose(): void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The canvas kept around the model when it is fitted, in model units before
|
|
69
|
+
* the zoom. Enough that the outermost shape's border is not the edge of the
|
|
70
|
+
* pane and no more, since the padding is taken from the space the fit has.
|
|
71
|
+
*/
|
|
72
|
+
export const DIAGRAM_FIT_PADDING = 24;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Mount a GLSP diagram in a page element: build the container over GLSP's
|
|
76
|
+
* standalone modules and the framework's binder, load the model, and supply
|
|
77
|
+
* what a shell would. Resolves once the model is loaded and framed, so a caller
|
|
78
|
+
* that then reads the model sees the viewport it is drawn in.
|
|
79
|
+
*
|
|
80
|
+
* The accommodations a page needs from whatever hosts the diagram, each silent
|
|
81
|
+
* when missing, so none is optional:
|
|
82
|
+
*
|
|
83
|
+
* - **Framing.** sprotty leaves the viewport at scale 1 and the origin top-left
|
|
84
|
+
* after a load, so a model wider than the pane is cut off at the right with
|
|
85
|
+
* no sign anything is missing. The model is fitted with
|
|
86
|
+
* {@link DIAGRAM_FIT_PADDING}, zooming out only: a fit that zooms in makes a
|
|
87
|
+
* two-node diagram render four times the size of a ten-node one. To frame
|
|
88
|
+
* differently, dispatch your own viewport action on the container after the
|
|
89
|
+
* mount resolves. Register edges with `HydraniumGEdge`: GLSP's own
|
|
90
|
+
* gives the labels of an edge without bendpoints no finite bounds, and the
|
|
91
|
+
* fit then blanks the canvas.
|
|
92
|
+
* - **Bounds tracking.** After the first render, sprotty reads its canvas
|
|
93
|
+
* bounds again on `window.resize` and on nothing else. It culls every shape
|
|
94
|
+
* outside the bounds it believes it has, so a pane widened by a divider
|
|
95
|
+
* stays empty in the new space, and it maps pointer positions through their
|
|
96
|
+
* corner, so after a region above the element folds, a palette drop and a
|
|
97
|
+
* wheel zoom land off by the distance the element moved. A `ResizeObserver`
|
|
98
|
+
* on the element and each ancestor, and a `scroll` listener for a container
|
|
99
|
+
* that scrolls, send the element's box in page coordinates whenever it
|
|
100
|
+
* differs from the model's. A zero-sized box, which a pane dragged shut
|
|
101
|
+
* reports, is skipped: a zero canvas culls the whole model.
|
|
102
|
+
* - **Touch input.** sprotty binds mouse events only; see
|
|
103
|
+
* {@link enableTouchDragging}. Give the element `touch-action: none`.
|
|
104
|
+
* - **The context menu.** GLSP warns on every container built without a
|
|
105
|
+
* `TYPES.IContextMenuService`, and the console is a page's only log. A no-op
|
|
106
|
+
* is bound when the host bound none; a menu the host binds, in its
|
|
107
|
+
* initializer or a module of its own, is left alone.
|
|
108
|
+
*
|
|
109
|
+
* A failed load does not reject: the loader reports it through the logger and,
|
|
110
|
+
* unless the action dispatcher is what failed, on the diagram's status
|
|
111
|
+
* overlay. The mount resolves without the accommodations, which a model that
|
|
112
|
+
* never arrived has no use for, and with the outcome in
|
|
113
|
+
* {@link GlspDiagramMount.loadOutcome}. Any other failure unbinds the
|
|
114
|
+
* container and rejects.
|
|
115
|
+
*
|
|
116
|
+
* The element also needs a height, `position: relative` so the palette stays
|
|
117
|
+
* on it, and its `<svg>` sized to it, or sprotty draws into 300×150.
|
|
118
|
+
*/
|
|
119
|
+
export async function mountGlspDiagram(options: GlspDiagramMountOptions): Promise<GlspDiagramMount> {
|
|
120
|
+
const { mount } = options;
|
|
121
|
+
if (mount.id === '') {
|
|
122
|
+
throw new Error("mountGlspDiagram: the mount element needs an id, which becomes the diagram's clientId");
|
|
123
|
+
}
|
|
124
|
+
const diagramOptions: IDiagramOptions = {
|
|
125
|
+
clientId: mount.id,
|
|
126
|
+
diagramType: options.diagramType,
|
|
127
|
+
glspClientProvider: async () => options.glspClient,
|
|
128
|
+
sourceUri: options.sourceUri
|
|
129
|
+
};
|
|
130
|
+
// After the standalone modules, since every class the binder binds replaces
|
|
131
|
+
// a token those modules bind.
|
|
132
|
+
const hydraniumModule = new ContainerModule((bind, unbind, isBound, rebind) => {
|
|
133
|
+
createGlspClientModule({ bind, unbind, isBound, rebind }, { logger: options.logger });
|
|
134
|
+
});
|
|
135
|
+
const container = options.initializeDiagramContainer(
|
|
136
|
+
new Container(),
|
|
137
|
+
createDiagramOptionsModule(diagramOptions),
|
|
138
|
+
STANDALONE_MODULE_CONFIG,
|
|
139
|
+
hydraniumModule
|
|
140
|
+
);
|
|
141
|
+
// After the container is built rather than in a module, so the host's own
|
|
142
|
+
// binding wins wherever its module sits in the order.
|
|
143
|
+
if (!container.isBound(TYPES.IContextMenuService)) {
|
|
144
|
+
container.bind(TYPES.IContextMenuService).toConstantValue({ show: () => undefined });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const toDispose = new DisposableCollection();
|
|
148
|
+
// Pushed first, so it runs last: `unbindAll` reaches `disposeClientSession`
|
|
149
|
+
// through the model source's pre-destroy hook, after the listeners are gone.
|
|
150
|
+
toDispose.push({ dispose: () => container.unbindAll() });
|
|
151
|
+
const dispose = (): void => toDispose.dispose();
|
|
152
|
+
try {
|
|
153
|
+
const loader = container.get(DiagramLoader);
|
|
154
|
+
await loader.load();
|
|
155
|
+
// A loader that is not the framework's rejects on a failure instead.
|
|
156
|
+
const loadOutcome = (loader instanceof HydraniumDiagramLoader ? loader.loadOutcome : undefined) ?? { status: 'loaded' };
|
|
157
|
+
// A model that never arrived has no use for the accommodations, and when
|
|
158
|
+
// the dispatcher is what failed, every later dispatch rejects too.
|
|
159
|
+
if (loadOutcome.status === 'failed') {
|
|
160
|
+
return { container, loadOutcome, dispose };
|
|
161
|
+
}
|
|
162
|
+
const dispatcher = container.get<GLSPActionDispatcher>(TYPES.IActionDispatcher);
|
|
163
|
+
// The bounds the fit measures come from the hidden rendering pass inside
|
|
164
|
+
// `load`; a fit dispatched before it has nothing to measure and leaves the
|
|
165
|
+
// viewport where it was.
|
|
166
|
+
await dispatcher.dispatch(FitToScreenAction.create([], { padding: DIAGRAM_FIT_PADDING, maxZoom: 1, animate: false }));
|
|
167
|
+
|
|
168
|
+
// By id and not through `mount`: the load's first render replaced that
|
|
169
|
+
// element, and a listener on the replaced one never fires.
|
|
170
|
+
const live = document.getElementById(mount.id);
|
|
171
|
+
if (live === null) {
|
|
172
|
+
throw new Error(`mountGlspDiagram: the element '${mount.id}' left the document while the diagram loaded`);
|
|
173
|
+
}
|
|
174
|
+
toDispose.push(
|
|
175
|
+
trackCanvasBounds(live, dispatcher, container.get(EditorContextService), container.get<Logger>(HydraniumClientTypes.Logger))
|
|
176
|
+
);
|
|
177
|
+
toDispose.push(enableTouchDragging(live));
|
|
178
|
+
return { container, loadOutcome, dispose };
|
|
179
|
+
} catch (err: unknown) {
|
|
180
|
+
// The caller gets no mount to dispose, so the session would outlive it.
|
|
181
|
+
toDispose.dispose();
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function trackCanvasBounds(
|
|
187
|
+
mount: HTMLElement,
|
|
188
|
+
dispatcher: GLSPActionDispatcher,
|
|
189
|
+
editorContext: EditorContextService,
|
|
190
|
+
logger: Logger
|
|
191
|
+
): Disposable {
|
|
192
|
+
const update = (): void => {
|
|
193
|
+
const box = mount.getBoundingClientRect();
|
|
194
|
+
if (box.width === 0 || box.height === 0) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const bounds = { x: box.left + window.scrollX, y: box.top + window.scrollY, width: box.width, height: box.height };
|
|
198
|
+
// Against the model's bounds rather than the last ones sent, since sprotty
|
|
199
|
+
// sets them too on `window.resize`; and skipped when equal, since every
|
|
200
|
+
// page scroll lands here and each dispatch redraws the diagram.
|
|
201
|
+
if (!Bounds.equals(bounds, editorContext.canvasBounds)) {
|
|
202
|
+
dispatcher
|
|
203
|
+
.dispatch(InitializeCanvasBoundsAction.create(bounds))
|
|
204
|
+
.catch((err: unknown) => logger.error("Failed to update the diagram's canvas bounds", err));
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
const observer = new ResizeObserver(update);
|
|
208
|
+
for (let element: HTMLElement | null = mount; element !== null; element = element.parentElement) {
|
|
209
|
+
observer.observe(element);
|
|
210
|
+
}
|
|
211
|
+
document.addEventListener('scroll', update, { capture: true, passive: true });
|
|
212
|
+
return {
|
|
213
|
+
dispose: () => {
|
|
214
|
+
observer.disconnect();
|
|
215
|
+
document.removeEventListener('scroll', update, { capture: true });
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
export * from './action-dispatcher';
|
|
16
16
|
export * from './diagram-load-failed';
|
|
17
17
|
export * from './diagram-loader';
|
|
18
|
+
export * from './diagram-mount';
|
|
18
19
|
export * from './edge';
|
|
19
20
|
export * from './glsp-client';
|
|
20
21
|
export * from './glsp-client-module';
|
|
@@ -22,4 +23,5 @@ export * from './hidden-bounds-updater';
|
|
|
22
23
|
export * from './select-mouse-listener';
|
|
23
24
|
export * from './sprotty-logger-adapter';
|
|
24
25
|
export * from './status-overlay';
|
|
26
|
+
export * from './touch-input';
|
|
25
27
|
export * from './types';
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import { type Disposable, DisposableCollection } from '@hydranium/protocol';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Re-emit touch gestures over `mount` as the mouse events its canvas binds.
|
|
14
|
+
*
|
|
15
|
+
* sprotty's `MouseTool` registers `mousedown`, `mousemove` and `mouseup` and no
|
|
16
|
+
* pointer or touch listener, and a browser synthesizes mouse events for a tap
|
|
17
|
+
* and not for a drag, so a finger drag reaches the diagram as a complete
|
|
18
|
+
* pointer stream that moves nothing, with a clean console and a full model.
|
|
19
|
+
*
|
|
20
|
+
* A shim on the host rather than a change to the diagram client, because the
|
|
21
|
+
* gap is in which event family is bound: re-emitting the gesture as the bound
|
|
22
|
+
* family needs nothing from the model, the tools or the container. Pairs with
|
|
23
|
+
* `touch-action: none` on the same element, without which the browser takes the
|
|
24
|
+
* drag as a pan and cancels the pointer stream before the second move.
|
|
25
|
+
*
|
|
26
|
+
* Mouse and pen pointers are left alone: the canvas already hears those, and
|
|
27
|
+
* forwarding them would run every gesture twice. So is a second finger while
|
|
28
|
+
* one drags, since the canvas knows one mouse and its press would end the drag,
|
|
29
|
+
* and a press on a text field, which needs the browser's own press to take the
|
|
30
|
+
* focus and the caret.
|
|
31
|
+
*/
|
|
32
|
+
export function enableTouchDragging(mount: HTMLElement): Disposable {
|
|
33
|
+
/** The finger that started the drag and the element under it; every event of
|
|
34
|
+
* the drag goes to that element. */
|
|
35
|
+
let drag: { readonly pointerId: number; readonly target: Element | null } | undefined;
|
|
36
|
+
|
|
37
|
+
const dispatch = (type: 'mousedown' | 'mousemove' | 'mouseup', source: PointerEvent): void => {
|
|
38
|
+
drag?.target?.dispatchEvent(
|
|
39
|
+
new MouseEvent(type, {
|
|
40
|
+
bubbles: true,
|
|
41
|
+
cancelable: true,
|
|
42
|
+
clientX: source.clientX,
|
|
43
|
+
clientY: source.clientY,
|
|
44
|
+
button: 0,
|
|
45
|
+
buttons: type === 'mouseup' ? 0 : 1,
|
|
46
|
+
view: window
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const press = (event: PointerEvent): void => {
|
|
52
|
+
if (event.pointerType !== 'touch' || drag !== undefined || isTextField(event.target)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// Cancelled, or the browser follows a tap with mouse events of its own and
|
|
56
|
+
// the canvas hears the gesture twice; a `click` still follows.
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
// Without the capture the stream stops when the finger leaves the element
|
|
59
|
+
// it started on, which during a node drag is immediately.
|
|
60
|
+
mount.setPointerCapture(event.pointerId);
|
|
61
|
+
drag = { pointerId: event.pointerId, target: document.elementFromPoint(event.clientX, event.clientY) };
|
|
62
|
+
// A priming move BEFORE the press, or the drag is a silent no-op: sprotty's
|
|
63
|
+
// `MousePositionTracker` writes the origin the change-bounds tool reads at
|
|
64
|
+
// `mousedown` on `mousemove` alone, so a pointer that never hovered leaves
|
|
65
|
+
// it unset and the node is dropped at 0,0.
|
|
66
|
+
dispatch('mousemove', event);
|
|
67
|
+
dispatch('mousedown', event);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const move = (event: PointerEvent): void => {
|
|
71
|
+
if (event.pointerId === drag?.pointerId) {
|
|
72
|
+
dispatch('mousemove', event);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const release = (event: PointerEvent): void => {
|
|
77
|
+
if (event.pointerId !== drag?.pointerId) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
dispatch('mouseup', event);
|
|
81
|
+
drag = undefined;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const listeners: ['pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel', (event: PointerEvent) => void][] = [
|
|
85
|
+
['pointerdown', press],
|
|
86
|
+
['pointermove', move],
|
|
87
|
+
['pointerup', release],
|
|
88
|
+
// The browser fires `pointercancel` instead of `pointerup` when it takes
|
|
89
|
+
// the pointer over; without a release here the next touch resumes the
|
|
90
|
+
// abandoned drag.
|
|
91
|
+
['pointercancel', release]
|
|
92
|
+
];
|
|
93
|
+
const toDispose = new DisposableCollection();
|
|
94
|
+
for (const [type, listener] of listeners) {
|
|
95
|
+
mount.addEventListener(type, listener);
|
|
96
|
+
toDispose.push({ dispose: () => mount.removeEventListener(type, listener) });
|
|
97
|
+
}
|
|
98
|
+
return toDispose;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isTextField(target: EventTarget | null): boolean {
|
|
102
|
+
return target instanceof Element && target.closest('input, textarea, select, [contenteditable]:not([contenteditable="false"])') !== null;
|
|
103
|
+
}
|