@tuidom/elements 0.6.0 → 0.6.1
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.
|
@@ -14,6 +14,13 @@ export declare class TreeViewElement<T> extends ScrollableElement {
|
|
|
14
14
|
private selectedKeys;
|
|
15
15
|
private cutKeys;
|
|
16
16
|
private maxRowWidth;
|
|
17
|
+
/**
|
|
18
|
+
* Ширина метки в колонках по тексту — между пересборками плоского списка.
|
|
19
|
+
* `new DisplayLine` на каждую строку каждой пересборки был основной ценой
|
|
20
|
+
* раскрытия на больших деревьях. Поколенческий: пересборка переносит в
|
|
21
|
+
* новый кэш только встретившиеся метки, поэтому он не растёт без предела.
|
|
22
|
+
*/
|
|
23
|
+
private labelWidths;
|
|
17
24
|
/**
|
|
18
25
|
* Отступ контента строк от левого края элемента. В отличие от внешнего
|
|
19
26
|
* паддинг-контейнера, входит в саму строку, поэтому фон курсора/выделения
|
|
@@ -35,6 +42,13 @@ export declare class TreeViewElement<T> extends ScrollableElement {
|
|
|
35
42
|
toggleExpand(element: T): Promise<void>;
|
|
36
43
|
/** Раскрывает узел (идемпотентно, без прокрутки/выделения) и перестраивает список. */
|
|
37
44
|
expand(element: T): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Раскрывает несколько узлов разом (идемпотентно, без прокрутки/выделения)
|
|
47
|
+
* и перестраивает список один раз. Для «раскрыть все N» вместо N вызовов
|
|
48
|
+
* {@link expand}: каждый из них пересобирает весь плоский список, и на
|
|
49
|
+
* тысячах узлов это квадратичная цена.
|
|
50
|
+
*/
|
|
51
|
+
expandElements(elements: readonly T[]): Promise<void>;
|
|
38
52
|
/** Раскрывает узел (загружая детей в кеш), если он ещё не раскрыт. No-op для уже раскрытого. */
|
|
39
53
|
private expandElement;
|
|
40
54
|
/**
|
|
@@ -64,6 +78,13 @@ export declare class TreeViewElement<T> extends ScrollableElement {
|
|
|
64
78
|
protected performDefaultAction(event: TUIEventBase): void;
|
|
65
79
|
protected renderViewport(context: RenderContext, viewport: ScrollViewportInfo): void;
|
|
66
80
|
private resolveRowColors;
|
|
81
|
+
/**
|
|
82
|
+
* Перечитывает корень и раскрытые поддеревья обходом от корня: раскрытый
|
|
83
|
+
* узел, до которого обход не дошёл (свёрнут предок или узла больше нет),
|
|
84
|
+
* в плоском списке всё равно не виден. Обход вместо поиска каждого
|
|
85
|
+
* раскрытого ключа по плоскому списку — иначе `refresh()` квадратичен по
|
|
86
|
+
* числу раскрытых узлов.
|
|
87
|
+
*/
|
|
67
88
|
private loadRootChildren;
|
|
68
89
|
private reloadExpandedChildren;
|
|
69
90
|
private invalidateSubtreeCache;
|
|
@@ -71,7 +92,6 @@ export declare class TreeViewElement<T> extends ScrollableElement {
|
|
|
71
92
|
private appendChildren;
|
|
72
93
|
/** Колонка (в координатах контента), с которой начинается строка узла данной глубины. */
|
|
73
94
|
private rowIndentX;
|
|
74
|
-
private calculateRowWidth;
|
|
75
95
|
private formatRow;
|
|
76
96
|
private getSelectedKey;
|
|
77
97
|
private restoreSelection;
|
|
@@ -87,7 +107,6 @@ export declare class TreeViewElement<T> extends ScrollableElement {
|
|
|
87
107
|
private toggleSelectionAt;
|
|
88
108
|
private applyCursor;
|
|
89
109
|
private ensureVisible;
|
|
90
|
-
private findElementByKey;
|
|
91
110
|
private handleKeypress;
|
|
92
111
|
/**
|
|
93
112
|
* Быстрый поиск по набору: печатаешь символы — курсор перепрыгивает к первому узлу,
|
|
@@ -19,6 +19,13 @@ export class TreeViewElement extends ScrollableElement {
|
|
|
19
19
|
selectedKeys = new Set();
|
|
20
20
|
cutKeys = new Set();
|
|
21
21
|
maxRowWidth = 0;
|
|
22
|
+
/**
|
|
23
|
+
* Ширина метки в колонках по тексту — между пересборками плоского списка.
|
|
24
|
+
* `new DisplayLine` на каждую строку каждой пересборки был основной ценой
|
|
25
|
+
* раскрытия на больших деревьях. Поколенческий: пересборка переносит в
|
|
26
|
+
* новый кэш только встретившиеся метки, поэтому он не растёт без предела.
|
|
27
|
+
*/
|
|
28
|
+
labelWidths = new Map();
|
|
22
29
|
/**
|
|
23
30
|
* Отступ контента строк от левого края элемента. В отличие от внешнего
|
|
24
31
|
* паддинг-контейнера, входит в саму строку, поэтому фон курсора/выделения
|
|
@@ -52,7 +59,7 @@ export class TreeViewElement extends ScrollableElement {
|
|
|
52
59
|
if (element !== undefined) {
|
|
53
60
|
const key = this.provider.getKey(element);
|
|
54
61
|
this.invalidateSubtreeCache(key);
|
|
55
|
-
await this.reloadExpandedChildren(
|
|
62
|
+
await this.reloadExpandedChildren(element);
|
|
56
63
|
}
|
|
57
64
|
else {
|
|
58
65
|
this.childrenCache.clear();
|
|
@@ -81,9 +88,24 @@ export class TreeViewElement extends ScrollableElement {
|
|
|
81
88
|
}
|
|
82
89
|
/** Раскрывает узел (идемпотентно, без прокрутки/выделения) и перестраивает список. */
|
|
83
90
|
async expand(element) {
|
|
84
|
-
|
|
91
|
+
await this.expandElements([element]);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Раскрывает несколько узлов разом (идемпотентно, без прокрутки/выделения)
|
|
95
|
+
* и перестраивает список один раз. Для «раскрыть все N» вместо N вызовов
|
|
96
|
+
* {@link expand}: каждый из них пересобирает весь плоский список, и на
|
|
97
|
+
* тысячах узлов это квадратичная цена.
|
|
98
|
+
*/
|
|
99
|
+
async expandElements(elements) {
|
|
100
|
+
let changed = false;
|
|
101
|
+
for (const element of elements) {
|
|
102
|
+
if (this.expandedKeys.has(this.provider.getKey(element)))
|
|
103
|
+
continue;
|
|
104
|
+
await this.expandElement(element);
|
|
105
|
+
changed = true;
|
|
106
|
+
}
|
|
107
|
+
if (!changed)
|
|
85
108
|
return;
|
|
86
|
-
await this.expandElement(element);
|
|
87
109
|
this.rebuildFlatList();
|
|
88
110
|
this.markDirty();
|
|
89
111
|
}
|
|
@@ -318,28 +340,28 @@ export class TreeViewElement extends ScrollableElement {
|
|
|
318
340
|
return { bg: resolved.bg, fg: resolved.fg };
|
|
319
341
|
}
|
|
320
342
|
// ─── Private: data loading ───
|
|
343
|
+
/**
|
|
344
|
+
* Перечитывает корень и раскрытые поддеревья обходом от корня: раскрытый
|
|
345
|
+
* узел, до которого обход не дошёл (свёрнут предок или узла больше нет),
|
|
346
|
+
* в плоском списке всё равно не виден. Обход вместо поиска каждого
|
|
347
|
+
* раскрытого ключа по плоскому списку — иначе `refresh()` квадратичен по
|
|
348
|
+
* числу раскрытых узлов.
|
|
349
|
+
*/
|
|
321
350
|
async loadRootChildren() {
|
|
322
351
|
const rootChildren = await this.provider.getChildren();
|
|
323
352
|
this.childrenCache.set("__root__", rootChildren);
|
|
324
|
-
for (const
|
|
325
|
-
await this.reloadExpandedChildren(
|
|
353
|
+
for (const child of rootChildren) {
|
|
354
|
+
await this.reloadExpandedChildren(child);
|
|
326
355
|
}
|
|
327
356
|
}
|
|
328
|
-
async reloadExpandedChildren(
|
|
357
|
+
async reloadExpandedChildren(element) {
|
|
358
|
+
const key = this.provider.getKey(element);
|
|
329
359
|
if (!this.expandedKeys.has(key))
|
|
330
360
|
return;
|
|
331
|
-
const
|
|
332
|
-
/* v8 ignore start -- unreachable: findElementByKey always resolves an expanded key (see its own v8-ignore note) */
|
|
333
|
-
if (node === null)
|
|
334
|
-
return;
|
|
335
|
-
/* v8 ignore stop */
|
|
336
|
-
const children = await this.provider.getChildren(node);
|
|
361
|
+
const children = await this.provider.getChildren(element);
|
|
337
362
|
this.childrenCache.set(key, children);
|
|
338
363
|
for (const child of children) {
|
|
339
|
-
|
|
340
|
-
if (this.expandedKeys.has(childKey)) {
|
|
341
|
-
await this.reloadExpandedChildren(childKey);
|
|
342
|
-
}
|
|
364
|
+
await this.reloadExpandedChildren(child);
|
|
343
365
|
}
|
|
344
366
|
}
|
|
345
367
|
invalidateSubtreeCache(key) {
|
|
@@ -359,14 +381,23 @@ export class TreeViewElement extends ScrollableElement {
|
|
|
359
381
|
const rootChildren = this.childrenCache.get("__root__");
|
|
360
382
|
if (!rootChildren)
|
|
361
383
|
return;
|
|
362
|
-
this.
|
|
384
|
+
const previousWidths = this.labelWidths;
|
|
385
|
+
const labelWidths = new Map();
|
|
386
|
+
this.appendChildren(rootChildren, 0, null, previousWidths, labelWidths);
|
|
387
|
+
this.labelWidths = labelWidths;
|
|
363
388
|
}
|
|
364
|
-
appendChildren(children, depth, parentKey) {
|
|
389
|
+
appendChildren(children, depth, parentKey, previousWidths, labelWidths) {
|
|
365
390
|
for (const element of children) {
|
|
366
391
|
const key = this.provider.getKey(element);
|
|
367
392
|
const item = this.provider.getTreeItem(element);
|
|
368
393
|
this.flatNodes.push({ element, depth, item, parentKey });
|
|
369
|
-
|
|
394
|
+
let labelWidth = labelWidths.get(item.label);
|
|
395
|
+
if (labelWidth === undefined) {
|
|
396
|
+
labelWidth = previousWidths.get(item.label) ?? new DisplayLine(item.label).displayWidth;
|
|
397
|
+
labelWidths.set(item.label, labelWidth);
|
|
398
|
+
}
|
|
399
|
+
// indent + expandIcon + space + icon + space + label
|
|
400
|
+
const rowWidth = this.rowIndentX(depth) + 2 + (item.icon ? 2 : 0) + labelWidth;
|
|
370
401
|
if (rowWidth > this.maxRowWidth) {
|
|
371
402
|
this.maxRowWidth = rowWidth;
|
|
372
403
|
}
|
|
@@ -374,7 +405,7 @@ export class TreeViewElement extends ScrollableElement {
|
|
|
374
405
|
const cachedChildren = this.childrenCache.get(key);
|
|
375
406
|
/* v8 ignore start -- defensive: an expanded key always has its children cached (cache and expandedKeys stay in sync) */
|
|
376
407
|
if (cachedChildren) {
|
|
377
|
-
this.appendChildren(cachedChildren, depth + 1, key);
|
|
408
|
+
this.appendChildren(cachedChildren, depth + 1, key, previousWidths, labelWidths);
|
|
378
409
|
}
|
|
379
410
|
/* v8 ignore stop */
|
|
380
411
|
}
|
|
@@ -384,10 +415,6 @@ export class TreeViewElement extends ScrollableElement {
|
|
|
384
415
|
rowIndentX(depth) {
|
|
385
416
|
return this.leftPadding + depth * INDENT_SIZE;
|
|
386
417
|
}
|
|
387
|
-
calculateRowWidth(depth, item) {
|
|
388
|
-
// indent + expandIcon + space + icon + space + label
|
|
389
|
-
return this.rowIndentX(depth) + 2 + (item.icon ? 2 : 0) + new DisplayLine(item.label).displayWidth;
|
|
390
|
-
}
|
|
391
418
|
formatRow(node) {
|
|
392
419
|
const indent = " ".repeat(this.rowIndentX(node.depth));
|
|
393
420
|
const expandIcon = node.item.collapsible
|
|
@@ -507,25 +534,6 @@ export class TreeViewElement extends ScrollableElement {
|
|
|
507
534
|
this.scrollTo(this.scrollLeft, index - viewportHeight + 1);
|
|
508
535
|
}
|
|
509
536
|
}
|
|
510
|
-
// ─── Private: key helpers ───
|
|
511
|
-
findElementByKey(key) {
|
|
512
|
-
for (const node of this.flatNodes) {
|
|
513
|
-
if (this.provider.getKey(node.element) === key) {
|
|
514
|
-
return node.element;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
/* v8 ignore start -- unreachable: findElementByKey is only called with keys of currently-flattened (visible) nodes, which the loop above always finds */
|
|
518
|
-
// Search in cache values too
|
|
519
|
-
for (const children of this.childrenCache.values()) {
|
|
520
|
-
for (const child of children) {
|
|
521
|
-
if (this.provider.getKey(child) === key) {
|
|
522
|
-
return child;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
return null;
|
|
527
|
-
/* v8 ignore stop */
|
|
528
|
-
}
|
|
529
537
|
// ─── Private: input handlers ───
|
|
530
538
|
handleKeypress(event) {
|
|
531
539
|
switch (event.key) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuidom/elements",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Experimental terminal UI DOM — widgets: lists, trees, menus, inputs, quick pick. API is unstable.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,6 +28,6 @@
|
|
|
28
28
|
"./*": "./dist/*.js"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@tuidom/core": "0.6.
|
|
31
|
+
"@tuidom/core": "0.6.1"
|
|
32
32
|
}
|
|
33
33
|
}
|