@hydranium/glsp-client 1.0.0-next.300 → 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 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 shown on the diagram's
12
- status overlay, instead of vanishing as an uncaught rejection.
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 | Runs in |
80
- | ------------ | ------------------------------------------------- | --------------- |
81
- | `.` | The classes, the binder and the DI tokens | browser |
82
- | `./messages` | The codes of the messages this package raises | browser-neutral |
83
- | `./testing` | A binding recorder for testing your modules | browser-neutral |
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":";AAAA;;;;;;;kFAOkF;;;;AAElF,iDAY8B;AAC9B,kDAAyF;AACzF,yCAAuD;AACvD,qDAAmF;AAEnF,6DAA8D;AAC9D,+CAAoD;AACpD,mCAA+C;AAsC/C;;;;GAIG;AACU,QAAA,mBAAmB,GAAG,EAAE,CAAC;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACI,KAAK,2BAA2B,OAAgC;IACpE,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IAC1B,IAAI,KAAK,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,uFAAuF,CAAC,CAAC;IAC5G,CAAC;IACD,MAAM,cAAc,GAAoB;QACrC,QAAQ,EAAE,KAAK,CAAC,EAAE;QAClB,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,kBAAkB,EAAE,KAAK,IAAI,EAAE,CAAC,OAAO,CAAC,UAAU;QAClD,SAAS,EAAE,OAAO,CAAC,SAAS;KAC9B,CAAC;IACF,4EAA4E;IAC5E,8BAA8B;IAC9B,MAAM,eAAe,GAAG,IAAI,2BAAe,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;QAC3E,IAAA,2CAAsB,EAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACzF,CAAC,CAAC,CAAC;IACH,MAAM,SAAS,GAAG,OAAO,CAAC,0BAA0B,CACjD,IAAI,qBAAS,EAAE,EACf,IAAA,mCAA0B,EAAC,cAAc,CAAC,EAC1C,iCAAwB,EACxB,eAAe,CACjB,CAAC;IACF,0EAA0E;IAC1E,sDAAsD;IACtD,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,cAAK,CAAC,mBAAmB,CAAC,EAAE,CAAC;QACjD,SAAS,CAAC,IAAI,CAAC,cAAK,CAAC,mBAAmB,CAAC,CAAC,eAAe,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC,CAAC;IACxF,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,+BAAoB,EAAE,CAAC;IAC7C,4EAA4E;IAC5E,6EAA6E;IAC7E,SAAS,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IACzD,MAAM,OAAO,GAAG,GAAS,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;IAChD,IAAI,CAAC;QACF,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAC,sBAAa,CAAC,CAAC;QAC5C,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,qEAAqE;QACrE,MAAM,WAAW,GAAG,CAAC,MAAM,YAAY,uCAAsB,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;QACxH,yEAAyE;QACzE,mEAAmE;QACnE,IAAI,WAAW,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YACnC,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC;QAC9C,CAAC;QACD,MAAM,UAAU,GAAG,SAAS,CAAC,GAAG,CAAuB,cAAK,CAAC,iBAAiB,CAAC,CAAC;QAChF,yEAAyE;QACzE,2EAA2E;QAC3E,yBAAyB;QACzB,MAAM,UAAU,CAAC,QAAQ,CAAC,0BAAiB,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,QAAA,mBAAmB,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;QAEtH,uEAAuE;QACvE,2DAA2D;QAC3D,MAAM,IAAI,GAAG,QAAQ,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC/C,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,kCAAkC,KAAK,CAAC,EAAE,8CAA8C,CAAC,CAAC;QAC7G,CAAC;QACD,SAAS,CAAC,IAAI,CACX,iBAAiB,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,CAAC,GAAG,CAAC,6BAAoB,CAAC,EAAE,SAAS,CAAC,GAAG,CAAS,4BAAoB,CAAC,MAAM,CAAC,CAAC,CAC9H,CAAC;QACF,SAAS,CAAC,IAAI,CAAC,IAAA,iCAAmB,EAAC,IAAI,CAAC,CAAC,CAAC;QAC1C,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC;IAC9C,CAAC;IAAC,OAAO,GAAY,EAAE,CAAC;QACrB,wEAAwE;QACxE,SAAS,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,GAAG,CAAC;IACb,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CACvB,KAAkB,EAClB,UAAgC,EAChC,aAAmC,EACnC,MAAc;IAEd,MAAM,MAAM,GAAG,GAAS,EAAE;QACvB,MAAM,GAAG,GAAG,KAAK,CAAC,qBAAqB,EAAE,CAAC;QAC1C,IAAI,GAAG,CAAC,KAAK,KAAK,CAAC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvC,OAAO;QACV,CAAC;QACD,MAAM,MAAM,GAAG,EAAE,CAAC,EAAE,GAAG,CAAC,IAAI,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC;QACnH,2EAA2E;QAC3E,wEAAwE;QACxE,gEAAgE;QAChE,IAAI,CAAC,eAAM,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,YAAY,CAAC,EAAE,CAAC;YACtD,UAAU;iBACN,QAAQ,CAAC,qCAA4B,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;iBACrD,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,8CAA8C,EAAE,GAAG,CAAC,CAAC,CAAC;QAClG,CAAC;IACJ,CAAC,CAAC;IACF,MAAM,QAAQ,GAAG,IAAI,cAAc,CAAC,MAAM,CAAC,CAAC;IAC5C,KAAK,IAAI,OAAO,GAAuB,KAAK,EAAE,OAAO,KAAK,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;QAC/F,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7B,CAAC;IACD,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9E,OAAO;QACJ,OAAO,EAAE,GAAG,EAAE;YACX,QAAQ,CAAC,UAAU,EAAE,CAAC;YACtB,QAAQ,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QACrE,CAAC;KACH,CAAC;AACL,CAAC"}
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
@@ -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.300",
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.300",
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.300",
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
+ }