@stocksharp/diagram 0.1.0

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.
Files changed (87) hide show
  1. package/LICENSE +30 -0
  2. package/NOTICE +21 -0
  3. package/README.md +346 -0
  4. package/dist/esm/canvas-renderer.js +3226 -0
  5. package/dist/esm/canvas-renderer.js.map +1 -0
  6. package/dist/esm/core/action-registry.js +35 -0
  7. package/dist/esm/core/action-registry.js.map +1 -0
  8. package/dist/esm/core/document.js +350 -0
  9. package/dist/esm/core/document.js.map +1 -0
  10. package/dist/esm/core/history.js +136 -0
  11. package/dist/esm/core/history.js.map +1 -0
  12. package/dist/esm/core/model.js +2 -0
  13. package/dist/esm/core/model.js.map +1 -0
  14. package/dist/esm/core/state.js +73 -0
  15. package/dist/esm/core/state.js.map +1 -0
  16. package/dist/esm/core/view-state.js +70 -0
  17. package/dist/esm/core/view-state.js.map +1 -0
  18. package/dist/esm/diagram/api.js +2 -0
  19. package/dist/esm/diagram/api.js.map +1 -0
  20. package/dist/esm/diagram/catalog.js +45 -0
  21. package/dist/esm/diagram/catalog.js.map +1 -0
  22. package/dist/esm/diagram/event-emitter.js +42 -0
  23. package/dist/esm/diagram/event-emitter.js.map +1 -0
  24. package/dist/esm/diagram/palette.js +269 -0
  25. package/dist/esm/diagram/palette.js.map +1 -0
  26. package/dist/esm/diagram/stocksharp-diagram.js +784 -0
  27. package/dist/esm/diagram/stocksharp-diagram.js.map +1 -0
  28. package/dist/esm/diagram/types.js +105 -0
  29. package/dist/esm/diagram/types.js.map +1 -0
  30. package/dist/esm/embed.js +398 -0
  31. package/dist/esm/embed.js.map +1 -0
  32. package/dist/esm/i18n.js +12 -0
  33. package/dist/esm/i18n.js.map +1 -0
  34. package/dist/esm/index.js +12 -0
  35. package/dist/esm/index.js.map +1 -0
  36. package/dist/ssdiagram.js +5141 -0
  37. package/dist/ssdiagram.js.map +7 -0
  38. package/dist/types/canvas-renderer.d.ts +512 -0
  39. package/dist/types/canvas-renderer.d.ts.map +1 -0
  40. package/dist/types/core/action-registry.d.ts +18 -0
  41. package/dist/types/core/action-registry.d.ts.map +1 -0
  42. package/dist/types/core/document.d.ts +12 -0
  43. package/dist/types/core/document.d.ts.map +1 -0
  44. package/dist/types/core/history.d.ts +33 -0
  45. package/dist/types/core/history.d.ts.map +1 -0
  46. package/dist/types/core/model.d.ts +85 -0
  47. package/dist/types/core/model.d.ts.map +1 -0
  48. package/dist/types/core/state.d.ts +68 -0
  49. package/dist/types/core/state.d.ts.map +1 -0
  50. package/dist/types/core/view-state.d.ts +14 -0
  51. package/dist/types/core/view-state.d.ts.map +1 -0
  52. package/dist/types/diagram/api.d.ts +213 -0
  53. package/dist/types/diagram/api.d.ts.map +1 -0
  54. package/dist/types/diagram/catalog.d.ts +19 -0
  55. package/dist/types/diagram/catalog.d.ts.map +1 -0
  56. package/dist/types/diagram/event-emitter.d.ts +9 -0
  57. package/dist/types/diagram/event-emitter.d.ts.map +1 -0
  58. package/dist/types/diagram/palette.d.ts +63 -0
  59. package/dist/types/diagram/palette.d.ts.map +1 -0
  60. package/dist/types/diagram/stocksharp-diagram.d.ts +157 -0
  61. package/dist/types/diagram/stocksharp-diagram.d.ts.map +1 -0
  62. package/dist/types/diagram/types.d.ts +171 -0
  63. package/dist/types/diagram/types.d.ts.map +1 -0
  64. package/dist/types/embed.d.ts +35 -0
  65. package/dist/types/embed.d.ts.map +1 -0
  66. package/dist/types/i18n.d.ts +167 -0
  67. package/dist/types/i18n.d.ts.map +1 -0
  68. package/dist/types/index.d.ts +22 -0
  69. package/dist/types/index.d.ts.map +1 -0
  70. package/package.json +126 -0
  71. package/sample.png +0 -0
  72. package/src/canvas-renderer.ts +3249 -0
  73. package/src/core/action-registry.ts +46 -0
  74. package/src/core/document.ts +387 -0
  75. package/src/core/history.ts +154 -0
  76. package/src/core/model.ts +99 -0
  77. package/src/core/state.ts +148 -0
  78. package/src/core/view-state.ts +82 -0
  79. package/src/diagram/api.ts +257 -0
  80. package/src/diagram/catalog.ts +55 -0
  81. package/src/diagram/event-emitter.ts +44 -0
  82. package/src/diagram/palette.ts +312 -0
  83. package/src/diagram/stocksharp-diagram.ts +945 -0
  84. package/src/diagram/types.ts +332 -0
  85. package/src/embed.ts +508 -0
  86. package/src/i18n.ts +210 -0
  87. package/src/index.ts +171 -0
@@ -0,0 +1,312 @@
1
+ import { EventEmitter } from './event-emitter.js';
2
+ import { StockSharpCatalog } from './catalog.js';
3
+ import { Node } from './types.js';
4
+
5
+ export interface PaletteOptions {
6
+ div: HTMLElement;
7
+ catalog: StockSharpCatalog;
8
+ /** Node type ids hidden from this palette. Matching is case-insensitive. */
9
+ excludedTypeIds?: Iterable<string>;
10
+ }
11
+
12
+ export interface PaletteNodePayload {
13
+ node: Node;
14
+ }
15
+
16
+ export interface PaletteSelectionChangedPayload {
17
+ node: Node | null;
18
+ }
19
+
20
+ export interface PaletteContextMenuPayload extends PaletteNodePayload {
21
+ x: number;
22
+ y: number;
23
+ }
24
+
25
+ export interface PaletteEvents extends Record<string, unknown> {
26
+ selectionChanged: PaletteSelectionChangedPayload;
27
+ nodeActivated: PaletteNodePayload;
28
+ contextMenuRequested: PaletteContextMenuPayload;
29
+ }
30
+
31
+ /** Typed payload used by native drag-and-drop between the palette and canvas. */
32
+ export const PALETTE_DRAG_MIME = 'application/x-stocksharp-node';
33
+
34
+ /**
35
+ * Accessible HTML palette for catalog node types.
36
+ *
37
+ * The component owns the contents of `div`. The host decides what activation
38
+ * and context-menu actions mean by subscribing to the typed events.
39
+ */
40
+ export class StockSharpPalette extends EventEmitter<PaletteEvents> {
41
+ private readonly host: HTMLElement;
42
+ private readonly catalog: StockSharpCatalog;
43
+ private readonly unsubscribeCatalog: () => void;
44
+ private filter = '';
45
+ private collapsed = new Set<string>();
46
+ private excludedTypeIds: Set<string>;
47
+ private selectedTypeId: string | null = null;
48
+ private destroyed = false;
49
+
50
+ constructor({ div, catalog, excludedTypeIds = [] }: PaletteOptions) {
51
+ super();
52
+ this.host = div;
53
+ this.catalog = catalog;
54
+ this.excludedTypeIds = new Set(Array.from(excludedTypeIds, normalizeTypeId));
55
+ this.host.classList.add('d-palette');
56
+ this.host.setAttribute('role', 'tree');
57
+ this.unsubscribeCatalog = this.catalog.on('nodeTypesChanged', () => this.render());
58
+ this.render();
59
+ }
60
+
61
+ setFilter(text: string): void {
62
+ if (this.destroyed) return;
63
+ this.filter = (text ?? '').trim().toLowerCase();
64
+ this.applyFilter();
65
+ }
66
+
67
+ collapseAll(): void {
68
+ if (this.destroyed) return;
69
+ for (const groupName of this.groupNames()) this.collapsed.add(groupName);
70
+ this.applyFilter();
71
+ }
72
+
73
+ expandAll(): void {
74
+ if (this.destroyed) return;
75
+ this.collapsed.clear();
76
+ this.applyFilter();
77
+ }
78
+
79
+ /** Replaces the WPF palette's dynamic ExcludedTypeIds set. */
80
+ setExcludedTypeIds(typeIds: Iterable<string>): void {
81
+ if (this.destroyed) return;
82
+ const next = new Set(Array.from(typeIds, normalizeTypeId));
83
+ if (setsEqual(this.excludedTypeIds, next)) return;
84
+ this.excludedTypeIds = next;
85
+ this.render();
86
+ }
87
+
88
+ setNodeTypeExcluded(typeId: string, excluded = true): void {
89
+ if (this.destroyed) return;
90
+ const normalized = normalizeTypeId(typeId);
91
+ const changed = excluded
92
+ ? !this.excludedTypeIds.has(normalized)
93
+ : this.excludedTypeIds.has(normalized);
94
+ if (!changed) return;
95
+ if (excluded) this.excludedTypeIds.add(normalized);
96
+ else this.excludedTypeIds.delete(normalized);
97
+ this.render();
98
+ }
99
+
100
+ getExcludedTypeIds(): string[] {
101
+ return [...this.excludedTypeIds];
102
+ }
103
+
104
+ getSelectedNodeType(): Node | null {
105
+ return this.selectedTypeId === null
106
+ ? null
107
+ : this.visibleNodeType(this.selectedTypeId)?.clone() ?? null;
108
+ }
109
+
110
+ selectNodeType(typeId: string | null): boolean {
111
+ if (this.destroyed) return false;
112
+ const node = typeId === null ? null : this.visibleNodeType(typeId);
113
+ if (typeId !== null && node === null) return false;
114
+ const next = node === null ? null : normalizeTypeId(node.id);
115
+ if (next === this.selectedTypeId) return true;
116
+ this.selectedTypeId = next;
117
+ this.syncSelectionClass();
118
+ this.emit('selectionChanged', { node: node?.clone() ?? null });
119
+ return true;
120
+ }
121
+
122
+ destroy(): void {
123
+ if (this.destroyed) return;
124
+ this.destroyed = true;
125
+ this.unsubscribeCatalog();
126
+ this.host.replaceChildren();
127
+ this.host.classList.remove('d-palette');
128
+ this.host.removeAttribute('role');
129
+ this.clearEventHandlers();
130
+ }
131
+
132
+ private groupNames(): Set<string> {
133
+ return new Set(this.visibleNodeTypes().map((node) => groupNameOf(node)));
134
+ }
135
+
136
+ private visibleNodeTypes(): Node[] {
137
+ return this.catalog.getNodeTypes()
138
+ .filter((node) => !this.excludedTypeIds.has(normalizeTypeId(node.id)));
139
+ }
140
+
141
+ private visibleNodeType(typeId: string): Node | null {
142
+ const node = this.catalog.getNodeType(typeId);
143
+ return node !== null && !this.excludedTypeIds.has(normalizeTypeId(node.id)) ? node : null;
144
+ }
145
+
146
+ private render(): void {
147
+ if (this.destroyed) return;
148
+ this.reconcileSelection();
149
+ this.host.replaceChildren();
150
+ const tree = document.createElement('ul');
151
+ tree.className = 'd-palette-tree';
152
+ tree.setAttribute('role', 'group');
153
+
154
+ const groups = new Map<string, Node[]>();
155
+ for (const node of this.visibleNodeTypes()) {
156
+ const name = groupNameOf(node);
157
+ const items = groups.get(name) ?? [];
158
+ items.push(node);
159
+ groups.set(name, items);
160
+ }
161
+
162
+ for (const [name, items] of groups) tree.appendChild(this.renderGroup(name, items));
163
+ this.host.appendChild(tree);
164
+ this.applyFilter();
165
+ this.syncSelectionClass();
166
+ }
167
+
168
+ private reconcileSelection(): void {
169
+ if (this.selectedTypeId === null || this.visibleNodeType(this.selectedTypeId) !== null) return;
170
+ this.selectedTypeId = null;
171
+ this.emit('selectionChanged', { node: null });
172
+ }
173
+
174
+ private renderGroup(name: string, items: Node[]): HTMLElement {
175
+ const li = document.createElement('li');
176
+ li.className = 'd-palette-group';
177
+ li.dataset.group = name;
178
+ li.dataset.search = name.toLowerCase();
179
+ li.setAttribute('role', 'treeitem');
180
+
181
+ const head = document.createElement('button');
182
+ head.type = 'button';
183
+ head.className = 'd-palette-group-head';
184
+ head.setAttribute('aria-expanded', String(!this.collapsed.has(name)));
185
+
186
+ const chev = document.createElement('span');
187
+ chev.className = 'd-palette-chev';
188
+ chev.setAttribute('aria-hidden', 'true');
189
+
190
+ const label = document.createElement('span');
191
+ label.className = 'd-palette-group-name';
192
+ label.textContent = name;
193
+
194
+ head.appendChild(chev);
195
+ head.appendChild(label);
196
+ head.addEventListener('click', () => {
197
+ if (this.collapsed.has(name)) this.collapsed.delete(name);
198
+ else this.collapsed.add(name);
199
+ this.applyFilter();
200
+ });
201
+
202
+ const list = document.createElement('ul');
203
+ list.className = 'd-palette-group-items';
204
+ list.setAttribute('role', 'group');
205
+ for (const item of items) list.appendChild(this.renderItem(item));
206
+
207
+ li.appendChild(head);
208
+ li.appendChild(list);
209
+ return li;
210
+ }
211
+
212
+ private renderItem(item: Node): HTMLElement {
213
+ const li = document.createElement('li');
214
+ li.className = 'd-palette-item';
215
+ li.draggable = true;
216
+ li.tabIndex = 0;
217
+ li.dataset.typeId = item.id;
218
+ li.dataset.search = `${item.name} ${item.description}`.trim().toLowerCase();
219
+ li.title = item.description.length > 0 ? `${item.name} — ${item.description}` : item.name;
220
+ li.setAttribute('role', 'treeitem');
221
+ li.setAttribute('aria-selected', 'false');
222
+
223
+ if (item.icon.length > 0) {
224
+ const img = document.createElement('img');
225
+ img.className = 'd-palette-icon';
226
+ img.src = item.icon;
227
+ img.alt = '';
228
+ li.appendChild(img);
229
+ } else {
230
+ const placeholder = document.createElement('span');
231
+ placeholder.className = 'd-palette-icon d-palette-icon-placeholder';
232
+ placeholder.setAttribute('aria-hidden', 'true');
233
+ li.appendChild(placeholder);
234
+ }
235
+
236
+ const label = document.createElement('span');
237
+ label.className = 'd-palette-label';
238
+ label.textContent = item.name;
239
+ li.appendChild(label);
240
+
241
+ li.addEventListener('click', () => this.selectNodeType(item.id));
242
+ li.addEventListener('dblclick', (event) => {
243
+ event.preventDefault();
244
+ this.selectNodeType(item.id);
245
+ this.emit('nodeActivated', { node: item.clone() });
246
+ });
247
+ li.addEventListener('keydown', (event) => {
248
+ if (event.key !== 'Enter') return;
249
+ event.preventDefault();
250
+ this.selectNodeType(item.id);
251
+ this.emit('nodeActivated', { node: item.clone() });
252
+ });
253
+ li.addEventListener('contextmenu', (event) => {
254
+ event.preventDefault();
255
+ this.selectNodeType(item.id);
256
+ this.emit('contextMenuRequested', {
257
+ node: item.clone(),
258
+ x: event.clientX,
259
+ y: event.clientY,
260
+ });
261
+ });
262
+ li.addEventListener('dragstart', (event) => {
263
+ if (event.dataTransfer === null) return;
264
+ this.selectNodeType(item.id);
265
+ event.dataTransfer.effectAllowed = 'copy';
266
+ event.dataTransfer.setData(PALETTE_DRAG_MIME, JSON.stringify({ typeId: item.id }));
267
+ event.dataTransfer.setData('text/plain', item.id);
268
+ li.classList.add('is-dragging');
269
+ });
270
+ li.addEventListener('dragend', () => li.classList.remove('is-dragging'));
271
+
272
+ return li;
273
+ }
274
+
275
+ private applyFilter(): void {
276
+ const filtering = this.filter.length > 0;
277
+ for (const group of Array.from(this.host.querySelectorAll<HTMLElement>('.d-palette-group'))) {
278
+ const groupMatches = filtering && (group.dataset.search ?? '').includes(this.filter);
279
+ let groupVisible = false;
280
+ for (const item of Array.from(group.querySelectorAll<HTMLElement>('.d-palette-item'))) {
281
+ const matches = !filtering || groupMatches || (item.dataset.search ?? '').includes(this.filter);
282
+ item.classList.toggle('is-hidden', !matches);
283
+ if (matches) groupVisible = true;
284
+ }
285
+ group.classList.toggle('is-hidden', !groupVisible);
286
+ const collapsed = !filtering && this.collapsed.has(group.dataset.group ?? '');
287
+ group.classList.toggle('is-collapsed', collapsed);
288
+ group.querySelector<HTMLButtonElement>('.d-palette-group-head')
289
+ ?.setAttribute('aria-expanded', String(!collapsed));
290
+ }
291
+ }
292
+
293
+ private syncSelectionClass(): void {
294
+ for (const item of Array.from(this.host.querySelectorAll<HTMLElement>('.d-palette-item'))) {
295
+ const selected = normalizeTypeId(item.dataset.typeId ?? '') === this.selectedTypeId;
296
+ item.classList.toggle('is-selected', selected);
297
+ item.setAttribute('aria-selected', String(selected));
298
+ }
299
+ }
300
+ }
301
+
302
+ function groupNameOf(node: Node): string {
303
+ return node.groupName.length > 0 ? node.groupName : 'Common';
304
+ }
305
+
306
+ function normalizeTypeId(typeId: string): string {
307
+ return typeId.toLowerCase();
308
+ }
309
+
310
+ function setsEqual(left: ReadonlySet<string>, right: ReadonlySet<string>): boolean {
311
+ return left.size === right.size && [...left].every((value) => right.has(value));
312
+ }