@haiyue/ui 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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -0
  3. package/dist/checkbox.d.ts +30 -0
  4. package/dist/checkbox.js +204 -0
  5. package/dist/context-menu.d.ts +41 -0
  6. package/dist/context-menu.js +225 -0
  7. package/dist/dialog.d.ts +30 -0
  8. package/dist/dialog.js +183 -0
  9. package/dist/dropdown.d.ts +45 -0
  10. package/dist/dropdown.js +228 -0
  11. package/dist/index.d.ts +32 -0
  12. package/dist/index.js +110 -0
  13. package/dist/input.d.ts +30 -0
  14. package/dist/input.js +131 -0
  15. package/dist/menu-shared.d.ts +14 -0
  16. package/dist/menu-shared.js +54 -0
  17. package/dist/radio.d.ts +30 -0
  18. package/dist/radio.js +205 -0
  19. package/dist/select.d.ts +40 -0
  20. package/dist/select.js +253 -0
  21. package/dist/split.d.ts +46 -0
  22. package/dist/split.js +277 -0
  23. package/dist/tabs.d.ts +28 -0
  24. package/dist/tabs.js +161 -0
  25. package/dist/tooltip.d.ts +37 -0
  26. package/dist/tooltip.js +286 -0
  27. package/dist/tree-model.d.ts +35 -0
  28. package/dist/tree-model.js +281 -0
  29. package/dist/tree-node.d.ts +11 -0
  30. package/dist/tree-node.js +63 -0
  31. package/dist/tree-types.d.ts +41 -0
  32. package/dist/tree-types.js +2 -0
  33. package/dist/tree.d.ts +102 -0
  34. package/dist/tree.js +942 -0
  35. package/package.json +46 -0
  36. package/src/checkbox.ts +215 -0
  37. package/src/context-menu.ts +248 -0
  38. package/src/dialog.ts +195 -0
  39. package/src/dropdown.ts +249 -0
  40. package/src/index.ts +192 -0
  41. package/src/input.ts +150 -0
  42. package/src/menu-shared.ts +65 -0
  43. package/src/radio.ts +216 -0
  44. package/src/select.ts +271 -0
  45. package/src/split.ts +306 -0
  46. package/src/tabs.ts +182 -0
  47. package/src/tooltip.ts +321 -0
  48. package/src/tree-model.ts +305 -0
  49. package/src/tree-node.ts +67 -0
  50. package/src/tree-types.ts +47 -0
  51. package/src/tree.ts +1002 -0
package/dist/tree.js ADDED
@@ -0,0 +1,942 @@
1
+ import { GETreeModel } from './tree-model.js';
2
+ import { GETreeNode } from './tree-node.js';
3
+ export { GETreeNode } from './tree-node.js';
4
+ const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
5
+ const TREE_TOGGLE_ICON_PATHS = [
6
+ 'M318.57 223.95l322.99 322.99c21.87 21.87 57.33 21.87 79.2 0 21.87-21.87 21.87-57.33 0-79.2l-323-322.99c-21.87-21.87-57.33-21.87-79.2 0-21.86 21.87-21.86 57.33 0.01 79.2z',
7
+ 'M729.75 555.95L406.76 878.93c-21.87 21.87-57.33 21.87-79.2 0-21.87-21.87-21.87-57.33 0-79.2l322.99-322.99c21.87-21.87 57.33-21.87 79.2 0 21.87 21.88 21.87 57.34 0 79.21z',
8
+ ];
9
+ const HIERARCHY_MEASURE_PREFIX = 'editor.hierarchy.';
10
+ function measureHierarchyTreeStage(stage, operation) {
11
+ const startedAt = performance.now();
12
+ try {
13
+ return operation();
14
+ }
15
+ finally {
16
+ const name = `${HIERARCHY_MEASURE_PREFIX}${stage}`;
17
+ performance.clearMeasures(name);
18
+ performance.measure(name, { start: startedAt, end: performance.now() });
19
+ }
20
+ }
21
+ function createTreeToggleIconSvg() {
22
+ const svg = document.createElementNS(SVG_NAMESPACE, 'svg');
23
+ svg.setAttribute('class', 'toggle-icon');
24
+ svg.setAttribute('viewBox', '0 0 1024 1024');
25
+ svg.setAttribute('aria-hidden', 'true');
26
+ svg.setAttribute('focusable', 'false');
27
+ for (const pathData of TREE_TOGGLE_ICON_PATHS) {
28
+ const path = document.createElementNS(SVG_NAMESPACE, 'path');
29
+ path.setAttribute('d', pathData);
30
+ path.setAttribute('fill', 'currentColor');
31
+ svg.append(path);
32
+ }
33
+ return svg;
34
+ }
35
+ export class GETree extends HTMLElement {
36
+ static VIRTUALIZATION_THRESHOLD = 160;
37
+ static ROW_HEIGHT = 28;
38
+ static OVERSCAN_ROWS = 8;
39
+ _root;
40
+ _style = document.createElement('style');
41
+ _list = document.createElement('div');
42
+ _rowCache = new Map();
43
+ _clipboard = [];
44
+ _data = [];
45
+ _selectedId = null;
46
+ _selectedIds = new Set();
47
+ _selectionAnchorId = null;
48
+ _activeId = null;
49
+ _expandedIds = new Set();
50
+ _model = new GETreeModel(this._expandedIds);
51
+ _dragNodeId = null;
52
+ _syncingSelectionAttributes = false;
53
+ _resizeObserver = null;
54
+ static get observedAttributes() {
55
+ return ['data', 'selected-id', 'selected-ids', 'allow-drag'];
56
+ }
57
+ constructor() {
58
+ super();
59
+ this._root = this.attachShadow({ mode: 'open' });
60
+ this._style.textContent = `
61
+ :host {
62
+ box-sizing: border-box;
63
+ display: flex;
64
+ flex-direction: column;
65
+ align-items: stretch;
66
+ justify-content: flex-start;
67
+ width: 100%;
68
+ min-height: 0;
69
+ color: var(--ge-tree-text-color, var(--ge-text-color, #c8d3e4));
70
+ background: var(--ge-panel-bg-color, #171d28);
71
+ font: 12px system-ui, sans-serif;
72
+ text-align: left;
73
+ user-select: none;
74
+ overflow: auto;
75
+ }
76
+ .list {
77
+ box-sizing: border-box;
78
+ width: 100%;
79
+ flex: 0 0 auto;
80
+ padding: 4px;
81
+ }
82
+ .row {
83
+ height: 28px;
84
+ display: flex;
85
+ align-items: center;
86
+ gap: 4px;
87
+ border-radius: 4px;
88
+ color: var(--ge-tree-text-color, var(--ge-text-color, #c8d3e4));
89
+ cursor: default;
90
+ }
91
+ .virtual-spacer {
92
+ width: 1px;
93
+ pointer-events: none;
94
+ }
95
+ .row:hover {
96
+ background: var(--ge-hover-bg-color, #222b3a);
97
+ }
98
+ .row.selected {
99
+ background: var(--ge-selected-bg-color, var(--ge-accent-strong-color, #255a91));
100
+ color: var(--ge-selected-text-color, #fff);
101
+ }
102
+ .row.active:not(.selected) {
103
+ background: var(--ge-tree-active-bg-color, var(--ge-hover-bg-color, #222b3a));
104
+ color: var(--ge-tree-active-text-color, var(--ge-tree-text-color, #dce8fb));
105
+ }
106
+ .row.active {
107
+ outline: var(--ge-tree-active-ring-width, 1px) solid var(--ge-tree-active-ring-color, var(--ge-focus-border-color, #3d6fa8));
108
+ outline-offset: -1px;
109
+ }
110
+ .toggle {
111
+ width: 18px;
112
+ height: 18px;
113
+ flex: 0 0 18px;
114
+ border: 0;
115
+ padding: 0;
116
+ background: transparent;
117
+ color: var(--ge-tree-icon-color, var(--ge-secondary-text-color, #8fa7c8));
118
+ cursor: pointer;
119
+ display: inline-grid;
120
+ place-items: center;
121
+ }
122
+ .toggle.empty {
123
+ cursor: default;
124
+ visibility: hidden;
125
+ }
126
+ .toggle-icon {
127
+ width: 12px;
128
+ height: 12px;
129
+ color: var(--ge-tree-icon-color, var(--ge-secondary-text-color, #8fa7c8));
130
+ transition: transform 0.16s ease, color 0.16s ease;
131
+ }
132
+ .toggle.expanded .toggle-icon {
133
+ transform: rotate(90deg);
134
+ color: var(--ge-tree-icon-active-color, #b9d4ff);
135
+ }
136
+ .row:hover .toggle-icon {
137
+ color: var(--ge-tree-icon-active-color, #b9d4ff);
138
+ }
139
+ .content {
140
+ min-width: 0;
141
+ flex: 1 1 auto;
142
+ }
143
+ .row.drop-before {
144
+ box-shadow: inset 0 2px 0 var(--ge-drop-indicator-color, #62a8ff);
145
+ }
146
+ .row.drop-after {
147
+ box-shadow: inset 0 -2px 0 var(--ge-drop-indicator-color, #62a8ff);
148
+ }
149
+ .row.drop-inside {
150
+ outline: 1px solid var(--ge-drop-indicator-color, #62a8ff);
151
+ background: var(--ge-drop-target-bg-color, #223650);
152
+ }
153
+ `;
154
+ this._list.className = 'list';
155
+ this._root.append(this._style, this._list);
156
+ }
157
+ connectedCallback() {
158
+ if (!this.hasAttribute('tabindex'))
159
+ this.tabIndex = 0;
160
+ if (!this.hasAttribute('role'))
161
+ this.setAttribute('role', 'tree');
162
+ this._syncFromAttributes();
163
+ this.addEventListener('keydown', this._onKeyDown);
164
+ this._list.addEventListener('click', this._onListClick);
165
+ this._list.addEventListener('contextmenu', this._onListContextMenu);
166
+ this._list.addEventListener('dragstart', this._onListDragStart);
167
+ this._list.addEventListener('dragover', this._onListDragOver);
168
+ this._list.addEventListener('dragleave', this._onListDragLeave);
169
+ this._list.addEventListener('drop', this._onListDrop);
170
+ this._list.addEventListener('dragend', this._onListDragEnd);
171
+ this.addEventListener('scroll', this._onScroll, { passive: true });
172
+ if (typeof ResizeObserver !== 'undefined') {
173
+ this._resizeObserver = new ResizeObserver(() => this._render());
174
+ this._resizeObserver.observe(this);
175
+ }
176
+ this._render();
177
+ }
178
+ disconnectedCallback() {
179
+ this.removeEventListener('keydown', this._onKeyDown);
180
+ this._list.removeEventListener('click', this._onListClick);
181
+ this._list.removeEventListener('contextmenu', this._onListContextMenu);
182
+ this._list.removeEventListener('dragstart', this._onListDragStart);
183
+ this._list.removeEventListener('dragover', this._onListDragOver);
184
+ this._list.removeEventListener('dragleave', this._onListDragLeave);
185
+ this._list.removeEventListener('drop', this._onListDrop);
186
+ this._list.removeEventListener('dragend', this._onListDragEnd);
187
+ this.removeEventListener('scroll', this._onScroll);
188
+ this._resizeObserver?.disconnect();
189
+ this._resizeObserver = null;
190
+ }
191
+ attributeChangedCallback(name) {
192
+ if (name === 'data')
193
+ this._readDataAttribute();
194
+ if (this._syncingSelectionAttributes)
195
+ return;
196
+ if (!this._syncingSelectionAttributes) {
197
+ if (name === 'selected-id')
198
+ this._setSelectionFromSelectedIdAttribute();
199
+ if (name === 'selected-ids')
200
+ this._setSelectionFromSelectedIdsAttribute();
201
+ }
202
+ this._render();
203
+ }
204
+ get data() {
205
+ return this._data;
206
+ }
207
+ set data(value) {
208
+ this._data = Array.isArray(value) ? value : [];
209
+ this._seedExpanded(this._data);
210
+ this._model.setData(this._data);
211
+ this._render();
212
+ }
213
+ get selectedId() {
214
+ return this._selectedId;
215
+ }
216
+ set selectedId(value) {
217
+ this._setSelection(value ? [value] : [], value, false);
218
+ }
219
+ get selectedIds() {
220
+ return [...this._selectedIds];
221
+ }
222
+ set selectedIds(value) {
223
+ const ids = Array.isArray(value) ? value : [];
224
+ this._setSelection(ids, ids[ids.length - 1] ?? null, false);
225
+ }
226
+ get allowDrag() {
227
+ return this.hasAttribute('allow-drag');
228
+ }
229
+ set allowDrag(value) {
230
+ if (value)
231
+ this.setAttribute('allow-drag', '');
232
+ else
233
+ this.removeAttribute('allow-drag');
234
+ }
235
+ updateData(value) {
236
+ this.data = value;
237
+ }
238
+ expand(id) {
239
+ this._expandedIds.add(id);
240
+ this._model.invalidateVisibility();
241
+ this._render();
242
+ }
243
+ collapse(id) {
244
+ this._expandedIds.delete(id);
245
+ this._model.invalidateVisibility();
246
+ this._render();
247
+ }
248
+ toggle(id) {
249
+ if (this._expandedIds.has(id))
250
+ this._expandedIds.delete(id);
251
+ else
252
+ this._expandedIds.add(id);
253
+ this._model.invalidateVisibility();
254
+ this._render();
255
+ }
256
+ reveal(id) {
257
+ const indexed = this._model.getIndexedNode(id);
258
+ if (!indexed)
259
+ return false;
260
+ for (const ancestorId of indexed.ancestorIds)
261
+ this._expandedIds.add(ancestorId);
262
+ this._model.invalidateVisibility();
263
+ this._render();
264
+ this._setActiveId(id, true);
265
+ return this._model.visibleIndex.has(id);
266
+ }
267
+ copySelection() {
268
+ return this._copySelection();
269
+ }
270
+ pasteClipboard() {
271
+ return this._pasteClipboard();
272
+ }
273
+ deleteSelection() {
274
+ return this._deleteSelection();
275
+ }
276
+ cutSelection() {
277
+ if (!this._copySelection())
278
+ return false;
279
+ return this._deleteSelection();
280
+ }
281
+ _syncFromAttributes() {
282
+ this._readDataAttribute();
283
+ if (this.hasAttribute('selected-ids'))
284
+ this._setSelectionFromSelectedIdsAttribute();
285
+ else
286
+ this._setSelectionFromSelectedIdAttribute();
287
+ }
288
+ _readDataAttribute() {
289
+ const raw = this.getAttribute('data');
290
+ if (!raw) {
291
+ this._data = [];
292
+ this._model.setData(this._data);
293
+ return;
294
+ }
295
+ try {
296
+ const parsed = JSON.parse(raw);
297
+ this._data = Array.isArray(parsed) ? parsed : [];
298
+ this._seedExpanded(this._data);
299
+ this._model.setData(this._data);
300
+ }
301
+ catch (error) {
302
+ console.warn('Invalid ge-tree data attribute.', error);
303
+ }
304
+ }
305
+ _seedExpanded(nodes) {
306
+ const visit = (items) => {
307
+ for (const node of items) {
308
+ if (node.expanded)
309
+ this._expandedIds.add(node.id);
310
+ if (node.children)
311
+ visit(node.children);
312
+ }
313
+ };
314
+ visit(nodes);
315
+ }
316
+ _render() {
317
+ this._ensureActiveVisible();
318
+ const visible = this._getVisibleFlatNodes();
319
+ const window = this._getRenderWindow(visible.length);
320
+ const visibleIds = new Set();
321
+ const fragment = document.createDocumentFragment();
322
+ if (window.start > 0)
323
+ fragment.append(this._createVirtualSpacer(window.start * GETree.ROW_HEIGHT));
324
+ for (let index = window.start; index < window.end; index++) {
325
+ const item = visible[index];
326
+ if (!item)
327
+ continue;
328
+ visibleIds.add(item.node.id);
329
+ const rendered = this._getOrCreateRow(item.node.id);
330
+ this._syncRow(rendered, item);
331
+ fragment.append(rendered.row);
332
+ }
333
+ if (window.end < visible.length) {
334
+ fragment.append(this._createVirtualSpacer((visible.length - window.end) * GETree.ROW_HEIGHT));
335
+ }
336
+ for (const [id, rendered] of this._rowCache) {
337
+ if (visibleIds.has(id))
338
+ continue;
339
+ rendered.row.remove();
340
+ this._rowCache.delete(id);
341
+ }
342
+ this._list.replaceChildren(fragment);
343
+ }
344
+ _getRenderWindow(itemCount) {
345
+ const viewportHeight = this.clientHeight;
346
+ if (itemCount < GETree.VIRTUALIZATION_THRESHOLD || viewportHeight <= 0)
347
+ return { start: 0, end: itemCount };
348
+ const first = Math.floor(this.scrollTop / GETree.ROW_HEIGHT);
349
+ const last = Math.ceil((this.scrollTop + viewportHeight) / GETree.ROW_HEIGHT);
350
+ return {
351
+ start: Math.max(0, first - GETree.OVERSCAN_ROWS),
352
+ end: Math.min(itemCount, last + GETree.OVERSCAN_ROWS),
353
+ };
354
+ }
355
+ _createVirtualSpacer(height) {
356
+ const spacer = document.createElement('div');
357
+ spacer.className = 'virtual-spacer';
358
+ spacer.style.height = `${height}px`;
359
+ spacer.setAttribute('aria-hidden', 'true');
360
+ return spacer;
361
+ }
362
+ _getVisibleFlatNodes() {
363
+ return this._model.getVisibleNodes();
364
+ }
365
+ _getVisibleIds() {
366
+ return this._model.getVisibleIds();
367
+ }
368
+ _getOrCreateRow(id) {
369
+ const cached = this._rowCache.get(id);
370
+ if (cached)
371
+ return cached;
372
+ const row = document.createElement('div');
373
+ const toggle = document.createElement('button');
374
+ const content = document.createElement('div');
375
+ const renderer = document.createElement('ge-tree-node');
376
+ row.className = 'row';
377
+ row.dataset.id = id;
378
+ toggle.type = 'button';
379
+ toggle.append(createTreeToggleIconSvg());
380
+ content.className = 'content';
381
+ content.append(renderer);
382
+ row.append(toggle, content);
383
+ const rendered = { row, toggle, content, renderer, rendererTag: 'ge-tree-node' };
384
+ this._rowCache.set(id, rendered);
385
+ return rendered;
386
+ }
387
+ _syncRow(rendered, item) {
388
+ const { node, depth } = item;
389
+ const hasChildren = Boolean(node.children?.length);
390
+ const expanded = this._expandedIds.has(node.id);
391
+ const selected = this._selectedIds.has(node.id);
392
+ const active = this._activeId === node.id;
393
+ const rendererTag = this._getRendererTag(node);
394
+ const { row, toggle, content } = rendered;
395
+ row.className = `row${selected ? ' selected' : ''}${active ? ' active' : ''}`;
396
+ row.dataset.id = node.id;
397
+ row.style.paddingLeft = `${depth * 16 + 4}px`;
398
+ row.draggable = this.allowDrag;
399
+ toggle.className = `${hasChildren ? 'toggle' : 'toggle empty'}${expanded ? ' expanded' : ''}`;
400
+ toggle.disabled = !hasChildren;
401
+ toggle.setAttribute('aria-hidden', hasChildren ? 'false' : 'true');
402
+ toggle.tabIndex = hasChildren ? 0 : -1;
403
+ if (rendered.rendererTag !== rendererTag) {
404
+ rendered.renderer = document.createElement(rendererTag);
405
+ rendered.rendererTag = rendererTag;
406
+ content.replaceChildren(rendered.renderer);
407
+ }
408
+ this._syncRenderer(rendered.renderer, node, depth, expanded, hasChildren, selected);
409
+ }
410
+ _getRendererTag(node) {
411
+ return typeof node.renderer === 'string' && node.renderer ? node.renderer : 'ge-tree-node';
412
+ }
413
+ _syncRenderer(el, node, depth, expanded, hasChildren, selected) {
414
+ el.node = node;
415
+ el.depth = depth;
416
+ el.selected = selected;
417
+ el.expanded = expanded;
418
+ el.hasChildren = hasChildren;
419
+ el.id = this._getTreeItemElementId(node.id);
420
+ if (!el.hasAttribute('role'))
421
+ el.setAttribute('role', 'treeitem');
422
+ el.setAttribute('aria-level', String(depth + 1));
423
+ if (hasChildren)
424
+ el.setAttribute('aria-expanded', expanded ? 'true' : 'false');
425
+ else
426
+ el.removeAttribute('aria-expanded');
427
+ el.setAttribute('label', String(node.label ?? node.id));
428
+ if (node.icon)
429
+ el.setAttribute('icon', String(node.icon));
430
+ else
431
+ el.removeAttribute('icon');
432
+ if (selected) {
433
+ el.setAttribute('selected', '');
434
+ el.setAttribute('aria-selected', 'true');
435
+ }
436
+ else {
437
+ el.removeAttribute('selected');
438
+ el.setAttribute('aria-selected', 'false');
439
+ }
440
+ }
441
+ _syncRenderedSelection() {
442
+ for (const [id, rendered] of this._rowCache) {
443
+ const selected = this._selectedIds.has(id);
444
+ rendered.row.classList.toggle('selected', selected);
445
+ rendered.renderer.selected = selected;
446
+ if (selected) {
447
+ rendered.renderer.setAttribute('selected', '');
448
+ rendered.renderer.setAttribute('aria-selected', 'true');
449
+ }
450
+ else {
451
+ rendered.renderer.removeAttribute('selected');
452
+ rendered.renderer.setAttribute('aria-selected', 'false');
453
+ }
454
+ }
455
+ }
456
+ _syncRenderedActive() {
457
+ for (const [id, rendered] of this._rowCache) {
458
+ rendered.row.classList.toggle('active', this._activeId === id);
459
+ }
460
+ this._syncActiveDescendant();
461
+ }
462
+ _select(id, event) {
463
+ const visibleIds = this._getVisibleIds();
464
+ let ids;
465
+ if (event.shiftKey && this._selectionAnchorId) {
466
+ ids = this._getRangeSelection(visibleIds, this._selectionAnchorId, id);
467
+ }
468
+ else if (event.metaKey || event.ctrlKey) {
469
+ const next = new Set(this._selectedIds);
470
+ if (next.has(id))
471
+ next.delete(id);
472
+ else
473
+ next.add(id);
474
+ ids = [...next];
475
+ this._selectionAnchorId = id;
476
+ }
477
+ else {
478
+ ids = [id];
479
+ this._selectionAnchorId = id;
480
+ }
481
+ this._setSelection(ids, id, true);
482
+ }
483
+ _setSelection(ids, selectedId, emit) {
484
+ const uniqueIds = [...new Set(ids)].filter(id => this._model.index.has(id));
485
+ this._selectedIds = new Set(uniqueIds);
486
+ this._selectedId = selectedId && this._selectedIds.has(selectedId)
487
+ ? selectedId
488
+ : uniqueIds[uniqueIds.length - 1] ?? null;
489
+ if (this._selectedId && !this._selectionAnchorId)
490
+ this._selectionAnchorId = this._selectedId;
491
+ this._setActiveId(this._selectedId, false);
492
+ this._syncSelectionAttributes();
493
+ if (this._model.isVisibilityDirty)
494
+ this._render();
495
+ else
496
+ this._syncRenderedSelection();
497
+ if (!emit)
498
+ return;
499
+ const node = this._selectedId ? this._model.getIndexedNode(this._selectedId)?.node ?? null : null;
500
+ const nodes = uniqueIds
501
+ .map(id => this._model.getIndexedNode(id)?.node ?? null)
502
+ .filter((item) => item !== null);
503
+ this.dispatchEvent(new CustomEvent('selection-change', {
504
+ detail: { selectedId: this._selectedId, node, selectedIds: uniqueIds, nodes },
505
+ bubbles: true,
506
+ composed: true,
507
+ }));
508
+ }
509
+ _syncSelectionAttributes() {
510
+ this._syncingSelectionAttributes = true;
511
+ if (this._selectedId === null)
512
+ this.removeAttribute('selected-id');
513
+ else
514
+ this.setAttribute('selected-id', this._selectedId);
515
+ if (this._selectedIds.size === 0)
516
+ this.removeAttribute('selected-ids');
517
+ else
518
+ this.setAttribute('selected-ids', JSON.stringify([...this._selectedIds]));
519
+ this._syncingSelectionAttributes = false;
520
+ }
521
+ _setSelectionFromSelectedIdAttribute() {
522
+ const id = this.getAttribute('selected-id');
523
+ this._selectedIds = id ? new Set([id]) : new Set();
524
+ this._selectedId = id;
525
+ this._selectionAnchorId = id;
526
+ this._activeId = id;
527
+ }
528
+ _setSelectionFromSelectedIdsAttribute() {
529
+ const raw = this.getAttribute('selected-ids');
530
+ if (!raw) {
531
+ this._setSelectionFromSelectedIdAttribute();
532
+ return;
533
+ }
534
+ try {
535
+ const parsed = JSON.parse(raw);
536
+ const ids = Array.isArray(parsed) ? parsed.map(String) : [];
537
+ this._selectedIds = new Set(ids);
538
+ this._selectedId = ids[ids.length - 1] ?? null;
539
+ this._selectionAnchorId = this._selectedId;
540
+ this._activeId = this._selectedId;
541
+ }
542
+ catch (error) {
543
+ console.warn('Invalid ge-tree selected-ids attribute.', error);
544
+ this._setSelectionFromSelectedIdAttribute();
545
+ }
546
+ }
547
+ _getRangeSelection(visibleIds, fromId, toId) {
548
+ this._getVisibleFlatNodes();
549
+ const fromIndex = this._model.visibleIndex.get(fromId) ?? -1;
550
+ const toIndex = this._model.visibleIndex.get(toId) ?? -1;
551
+ if (fromIndex < 0 || toIndex < 0)
552
+ return [toId];
553
+ const start = Math.min(fromIndex, toIndex);
554
+ const end = Math.max(fromIndex, toIndex);
555
+ return visibleIds.slice(start, end + 1);
556
+ }
557
+ _getEventRow(target) {
558
+ if (!(target instanceof Element))
559
+ return null;
560
+ const row = target.closest('.row');
561
+ return row && this._list.contains(row) ? row : null;
562
+ }
563
+ _getRowNode(row) {
564
+ const id = row.dataset.id;
565
+ return id ? this._model.getIndexedNode(id)?.node ?? null : null;
566
+ }
567
+ _onListClick = (event) => {
568
+ const row = this._getEventRow(event.target);
569
+ if (!row)
570
+ return;
571
+ const node = this._getRowNode(row);
572
+ if (!node)
573
+ return;
574
+ const toggle = event.target instanceof Element ? event.target.closest('.toggle') : null;
575
+ if (toggle && row.contains(toggle)) {
576
+ event.stopPropagation();
577
+ if (node.children?.length)
578
+ this.toggle(node.id);
579
+ return;
580
+ }
581
+ this.focus();
582
+ this._setActiveId(node.id, false);
583
+ this._select(node.id, event);
584
+ };
585
+ _onListContextMenu = (event) => {
586
+ const row = this._getEventRow(event.target);
587
+ if (!row)
588
+ return;
589
+ const node = this._getRowNode(row);
590
+ if (!node)
591
+ return;
592
+ event.preventDefault();
593
+ event.stopPropagation();
594
+ this.focus();
595
+ this._setActiveId(node.id, false);
596
+ if (!this._selectedIds.has(node.id))
597
+ this._setSelection([node.id], node.id, true);
598
+ this.dispatchEvent(new CustomEvent('node-context-menu', {
599
+ detail: {
600
+ id: node.id,
601
+ node,
602
+ selectedIds: this.selectedIds,
603
+ clientX: event.clientX,
604
+ clientY: event.clientY,
605
+ },
606
+ bubbles: true,
607
+ composed: true,
608
+ }));
609
+ };
610
+ _onListDragStart = (event) => {
611
+ const row = this._getEventRow(event.target);
612
+ if (!row || !this.allowDrag || !row.dataset.id)
613
+ return;
614
+ this._onDragStart(event, row.dataset.id);
615
+ };
616
+ _onListDragOver = (event) => {
617
+ const row = this._getEventRow(event.target);
618
+ if (!row || !this.allowDrag)
619
+ return;
620
+ this._onDragOver(event, row);
621
+ };
622
+ _onListDragLeave = (event) => {
623
+ const row = this._getEventRow(event.target);
624
+ if (row)
625
+ this._clearDropClass(row);
626
+ };
627
+ _onListDrop = (event) => {
628
+ const row = this._getEventRow(event.target);
629
+ if (!row || !this.allowDrag || !row.dataset.id)
630
+ return;
631
+ this._onDrop(event, row, row.dataset.id);
632
+ };
633
+ _onListDragEnd = () => {
634
+ this._clearAllDropClasses();
635
+ };
636
+ _onDragStart(event, id) {
637
+ this._dragNodeId = id;
638
+ event.dataTransfer?.setData('text/plain', id);
639
+ if (event.dataTransfer)
640
+ event.dataTransfer.effectAllowed = 'move';
641
+ }
642
+ _onDragOver(event, row) {
643
+ event.preventDefault();
644
+ if (event.dataTransfer)
645
+ event.dataTransfer.dropEffect = 'move';
646
+ this._clearAllDropClasses();
647
+ row.classList.add(this._getDropClass(event, row));
648
+ }
649
+ _onDrop(event, row, targetId) {
650
+ performance.clearMarks(`${HIERARCHY_MEASURE_PREFIX}input-start`);
651
+ performance.mark(`${HIERARCHY_MEASURE_PREFIX}input-start`);
652
+ event.preventDefault();
653
+ const sourceId = this._dragNodeId ?? event.dataTransfer?.getData('text/plain') ?? null;
654
+ this._clearAllDropClasses();
655
+ this._dragNodeId = null;
656
+ if (!sourceId || sourceId === targetId)
657
+ return;
658
+ const position = this._getDropPosition(event, row);
659
+ if (measureHierarchyTreeStage('index-update', () => this._model.moveNode(sourceId, targetId, position))) {
660
+ measureHierarchyTreeStage('tree-rebuild', () => this._render());
661
+ this.dispatchEvent(new CustomEvent('data-change', {
662
+ detail: { data: this._data, action: 'drop', sourceId, targetId, dropPosition: position },
663
+ bubbles: true,
664
+ composed: true,
665
+ }));
666
+ }
667
+ }
668
+ _onKeyDown = (event) => {
669
+ if (event.defaultPrevented || this._isEditableTarget(event.target))
670
+ return;
671
+ const key = event.key.toLowerCase();
672
+ const command = event.metaKey || event.ctrlKey;
673
+ if (command && key === 'c') {
674
+ if (this._copySelection())
675
+ event.preventDefault();
676
+ return;
677
+ }
678
+ if (command && key === 'v') {
679
+ if (this._pasteClipboard())
680
+ event.preventDefault();
681
+ return;
682
+ }
683
+ if (event.key === 'Delete' || event.key === 'Backspace') {
684
+ if (this._deleteSelection())
685
+ event.preventDefault();
686
+ return;
687
+ }
688
+ if (this._handleNavigationKey(event))
689
+ event.preventDefault();
690
+ };
691
+ _handleNavigationKey(event) {
692
+ if (event.altKey || event.metaKey || event.ctrlKey)
693
+ return false;
694
+ const visibleIds = this._getVisibleIds();
695
+ if (visibleIds.length === 0)
696
+ return false;
697
+ const activeId = this._getActiveVisibleId() ?? visibleIds[0];
698
+ if (activeId === undefined)
699
+ return false;
700
+ const activeIndex = this._model.visibleIndex.get(activeId) ?? 0;
701
+ if (event.key === 'ArrowDown') {
702
+ this._setActiveId(visibleIds[Math.min(activeIndex + 1, visibleIds.length - 1)] ?? activeId);
703
+ return true;
704
+ }
705
+ if (event.key === 'ArrowUp') {
706
+ this._setActiveId(visibleIds[Math.max(activeIndex - 1, 0)] ?? activeId);
707
+ return true;
708
+ }
709
+ if (event.key === 'Home') {
710
+ this._setActiveId(visibleIds[0] ?? activeId);
711
+ return true;
712
+ }
713
+ if (event.key === 'End') {
714
+ this._setActiveId(visibleIds[visibleIds.length - 1] ?? activeId);
715
+ return true;
716
+ }
717
+ if (event.key === 'ArrowRight') {
718
+ return this._moveActiveRight(activeId);
719
+ }
720
+ if (event.key === 'ArrowLeft') {
721
+ return this._moveActiveLeft(activeId);
722
+ }
723
+ if (event.key === 'Enter' || event.key === ' ') {
724
+ this._setSelection([activeId], activeId, true);
725
+ return true;
726
+ }
727
+ return false;
728
+ }
729
+ _moveActiveRight(activeId) {
730
+ const indexed = this._model.getIndexedNode(activeId);
731
+ if (!indexed?.node.children?.length)
732
+ return false;
733
+ if (!this._expandedIds.has(activeId)) {
734
+ this._expandedIds.add(activeId);
735
+ this._model.invalidateVisibility();
736
+ this._render();
737
+ this._scrollActiveIntoView();
738
+ return true;
739
+ }
740
+ const visibleIds = this._getVisibleIds();
741
+ const activeIndex = this._model.visibleIndex.get(activeId) ?? -1;
742
+ const nextId = visibleIds[activeIndex + 1];
743
+ if (nextId)
744
+ this._setActiveId(nextId);
745
+ return Boolean(nextId);
746
+ }
747
+ _moveActiveLeft(activeId) {
748
+ const indexed = this._model.getIndexedNode(activeId);
749
+ if (!indexed)
750
+ return false;
751
+ if (indexed.node.children?.length && this._expandedIds.has(activeId)) {
752
+ this._expandedIds.delete(activeId);
753
+ this._model.invalidateVisibility();
754
+ this._render();
755
+ this._scrollActiveIntoView();
756
+ return true;
757
+ }
758
+ const parentId = indexed.parent?.id;
759
+ if (parentId)
760
+ this._setActiveId(parentId);
761
+ return Boolean(parentId);
762
+ }
763
+ _getActiveVisibleId() {
764
+ this._getVisibleFlatNodes();
765
+ if (this._activeId && this._model.visibleIndex.has(this._activeId))
766
+ return this._activeId;
767
+ if (this._selectedId && this._model.visibleIndex.has(this._selectedId))
768
+ return this._selectedId;
769
+ return null;
770
+ }
771
+ _ensureActiveVisible() {
772
+ const visibleIds = this._getVisibleIds();
773
+ if (this._activeId && this._model.visibleIndex.has(this._activeId)) {
774
+ this._syncActiveDescendant();
775
+ return;
776
+ }
777
+ this._activeId = this._selectedId && this._model.visibleIndex.has(this._selectedId)
778
+ ? this._selectedId
779
+ : visibleIds[0] ?? null;
780
+ this._syncActiveDescendant();
781
+ }
782
+ _setActiveId(id, scroll = true) {
783
+ this._getVisibleFlatNodes();
784
+ const nextId = id && this._model.visibleIndex.has(id) ? id : this._getActiveVisibleId();
785
+ if (this._activeId === nextId) {
786
+ if (scroll)
787
+ this._scrollActiveIntoView();
788
+ return;
789
+ }
790
+ this._activeId = nextId;
791
+ this._syncRenderedActive();
792
+ if (scroll)
793
+ this._scrollActiveIntoView();
794
+ }
795
+ _syncActiveDescendant() {
796
+ if (this._activeId)
797
+ this.setAttribute('aria-activedescendant', this._getTreeItemElementId(this._activeId));
798
+ else
799
+ this.removeAttribute('aria-activedescendant');
800
+ }
801
+ _getTreeItemElementId(id) {
802
+ return `ge-tree-item-${encodeURIComponent(id)}`;
803
+ }
804
+ _scrollActiveIntoView() {
805
+ if (!this._activeId)
806
+ return;
807
+ let rendered = this._rowCache.get(this._activeId);
808
+ if (!rendered && this.clientHeight > 0) {
809
+ this._getVisibleFlatNodes();
810
+ const index = this._model.visibleIndex.get(this._activeId);
811
+ if (index !== undefined) {
812
+ const rowTop = index * GETree.ROW_HEIGHT;
813
+ const rowBottom = rowTop + GETree.ROW_HEIGHT;
814
+ if (rowTop < this.scrollTop)
815
+ this.scrollTop = rowTop;
816
+ else if (rowBottom > this.scrollTop + this.clientHeight)
817
+ this.scrollTop = rowBottom - this.clientHeight;
818
+ this._render();
819
+ rendered = this._rowCache.get(this._activeId);
820
+ }
821
+ }
822
+ rendered?.row.scrollIntoView({ block: 'nearest' });
823
+ }
824
+ _onScroll = () => {
825
+ if (this._getVisibleFlatNodes().length >= GETree.VIRTUALIZATION_THRESHOLD)
826
+ this._render();
827
+ };
828
+ _isEditableTarget(target) {
829
+ if (!(target instanceof HTMLElement))
830
+ return false;
831
+ const tag = target.tagName.toLowerCase();
832
+ return tag === 'input' || tag === 'textarea' || tag === 'select' || target.isContentEditable;
833
+ }
834
+ _copySelection() {
835
+ const nodes = this._getSelectedTopLevelNodes();
836
+ if (nodes.length === 0)
837
+ return false;
838
+ this._clipboard = nodes.map(node => this._cloneNode(node));
839
+ return true;
840
+ }
841
+ _pasteClipboard() {
842
+ if (this._clipboard.length === 0)
843
+ return false;
844
+ const target = this._selectedId ? this._model.findNode(this._selectedId) : null;
845
+ const targetList = target ? (target.node.children ??= []) : this._data;
846
+ if (target) {
847
+ this._expandedIds.add(target.node.id);
848
+ }
849
+ const existingIds = new Set(this._model.index.keys());
850
+ const pastedNodes = this._clipboard.map(node => this._cloneNodeWithUniqueIds(node, existingIds));
851
+ targetList.push(...pastedNodes);
852
+ this._model.setData(this._data);
853
+ const pastedIds = pastedNodes.map(node => node.id);
854
+ this._setSelection(pastedIds, pastedIds[pastedIds.length - 1] ?? null, true);
855
+ this.dispatchEvent(new CustomEvent('data-change', {
856
+ detail: { data: this._data, action: 'paste', targetId: target?.node.id ?? null, pastedNodes },
857
+ bubbles: true,
858
+ composed: true,
859
+ }));
860
+ return true;
861
+ }
862
+ _deleteSelection() {
863
+ const ids = this._getTopLevelSelectedIds();
864
+ if (ids.length === 0)
865
+ return false;
866
+ const deletedIds = [];
867
+ for (const id of ids) {
868
+ if (this._model.removeNode(id))
869
+ deletedIds.push(id);
870
+ }
871
+ if (deletedIds.length === 0)
872
+ return false;
873
+ this._model.setData(this._data);
874
+ this._setSelection([], null, true);
875
+ this.dispatchEvent(new CustomEvent('data-change', {
876
+ detail: { data: this._data, action: 'delete', deletedIds },
877
+ bubbles: true,
878
+ composed: true,
879
+ }));
880
+ return true;
881
+ }
882
+ _cloneNode(node) {
883
+ return typeof structuredClone === 'function'
884
+ ? structuredClone(node)
885
+ : JSON.parse(JSON.stringify(node));
886
+ }
887
+ _cloneNodeWithUniqueIds(node, existingIds) {
888
+ const clone = this._cloneNode(node);
889
+ const assignIds = (item) => {
890
+ item.id = this._makeUniqueId(item.id, existingIds);
891
+ existingIds.add(item.id);
892
+ item.children?.forEach(assignIds);
893
+ };
894
+ assignIds(clone);
895
+ return clone;
896
+ }
897
+ _makeUniqueId(baseId, existingIds) {
898
+ const base = `${baseId}-copy`;
899
+ if (!existingIds.has(base))
900
+ return base;
901
+ let index = 2;
902
+ while (existingIds.has(`${base}-${index}`))
903
+ index++;
904
+ return `${base}-${index}`;
905
+ }
906
+ _getSelectedTopLevelNodes() {
907
+ return this._getTopLevelSelectedIds()
908
+ .map(id => this._model.getIndexedNode(id)?.node ?? null)
909
+ .filter((node) => node !== null);
910
+ }
911
+ _getTopLevelSelectedIds() {
912
+ return this._model.topLevelSelection(this._selectedIds);
913
+ }
914
+ _getDropPosition(event, row) {
915
+ const rect = row.getBoundingClientRect();
916
+ const y = event.clientY - rect.top;
917
+ if (y < rect.height * 0.28)
918
+ return 'before';
919
+ if (y > rect.height * 0.72)
920
+ return 'after';
921
+ return 'inside';
922
+ }
923
+ _getDropClass(event, row) {
924
+ const position = this._getDropPosition(event, row);
925
+ return `drop-${position}`;
926
+ }
927
+ _clearDropClass(row) {
928
+ row.classList.remove('drop-before', 'drop-after', 'drop-inside');
929
+ }
930
+ _clearAllDropClasses() {
931
+ this._list.querySelectorAll('.row').forEach(row => this._clearDropClass(row));
932
+ }
933
+ }
934
+ export function defineTreeComponents() {
935
+ if (!customElements.get('ge-tree-node')) {
936
+ customElements.define('ge-tree-node', GETreeNode);
937
+ }
938
+ if (!customElements.get('ge-tree')) {
939
+ customElements.define('ge-tree', GETree);
940
+ }
941
+ }
942
+ //# sourceMappingURL=tree.js.map