Skip to content

Commit 9cf742d

Browse files
committed
fix(history): tree history component did not work properly
1 parent eeac470 commit 9cf742d

File tree

3 files changed

+111
-114
lines changed

3 files changed

+111
-114
lines changed

projects/interacto-angular/src/lib/components/tree-history/tree-history.component.html

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ <h2>HISTORY</h2>
55
</div>
66

77
<div class="tree" oncontextmenu="return false;">
8-
@for (position of history.getPositions() | keyvalue ; track position.key) {
9-
<div #d class="tree-node-history"
10-
[ngStyle]="{left: getLeft(position.value) + 'px', top: getTop(position.key) + 'px'}"
11-
[ngClass]="history.currentNode.id === position.key ? 'current-node' : ''"
12-
[ioClick] (clickBinder)="clickBinders($event, position.key)"
13-
[ioTaps] (tapsBinder)="tapsBinder($event, position.key)" [nbTaps]="1"
14-
[ioLongTouch] (longTouchBinder)="longTouchBinder($event, position.key)">
15-
{{undoButtonSnapshot(history.undoableNodes[position.key], d)}}
8+
@for (position of thumbnails() ; track position.key) {
9+
<div class="tree-node-history" #divHistory
10+
[ngStyle]="{left: getLeft(position.value) + 'px', top: getTop(position.key) + 'px'}"
11+
[ngClass]="history.currentNode.id === position.key ? 'current-node' : ''"
12+
[ioClick] (clickBinder)="clickBinders($event, position.key)"
13+
[ioTaps] (tapsBinder)="tapsBinder($event, position.key)" [nbTaps]="1"
14+
[ioLongTouch] (longTouchBinder)="longTouchBinder($event, position.key)"
15+
>
16+
<div [ngStyle]="{width: cmdViewWidthPx(), height: cmdViewHeightPx()}"
17+
[innerHTML]="getContent(position.thumbnail | async)"></div>
1618
</div>
1719
}
1820
</div>

projects/interacto-angular/src/lib/components/tree-history/tree-history.component.ts

Lines changed: 99 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
1-
import {KeyValuePipe, NgClass, NgFor, NgStyle} from '@angular/common';
1+
import {AsyncPipe, KeyValuePipe, NgClass, NgFor, NgStyle} from '@angular/common';
22
import {
3-
AfterViewInit,
4-
ChangeDetectionStrategy,
5-
ChangeDetectorRef,
6-
Component,
7-
HostBinding,
8-
Input,
9-
OnDestroy
3+
Component, computed, ElementRef,
4+
input,
5+
InputSignal, numberAttribute,
6+
Signal, untracked, ViewChild
107
} from '@angular/core';
11-
import { Binding, PartialPointTypedBinder, PartialTapsTypedBinder, PartialTouchTypedBinder, TreeUndoHistory, UndoableSnapshot, UndoableTreeNode } from 'interacto';
12-
import { Subscription } from "rxjs";
13-
import {UndoBinderDirective} from '../../directives/undo-binder.directive';
14-
import {RedoBinderDirective} from '../../directives/redo-binder.directive';
15-
import {ClickBinderDirective} from '../../directives/click-binder.directive';
16-
import {TapsBinderDirective} from '../../directives/taps-binder.directive';
17-
import {LongTouchBinderDirective} from '../../directives/long-touch-binder.directive';
8+
import { Binding, Undoable, PartialPointTypedBinder, PartialTapsTypedBinder, PartialTouchTypedBinder,
9+
TreeUndoHistory, UndoableSnapshot, UndoableTreeNode, Command, Interaction } from 'interacto';
10+
import {concat, throttleTime} from 'rxjs';
11+
import {
12+
ClickBinderDirective,
13+
LongTouchBinderDirective,
14+
RedoBinderDirective,
15+
TapsBinderDirective,
16+
UndoBinderDirective
17+
} from 'interacto-angular';
18+
import {DomSanitizer, SafeHtml} from '@angular/platform-browser';
19+
import {toSignal} from '@angular/core/rxjs-interop';
20+
21+
interface Thumbnail {
22+
value: number;
23+
thumbnail: Promise<unknown>;
24+
key: number;
25+
}
1826

1927
/**
2028
* The Angular component for display a tree-based undo/redo history
@@ -28,69 +36,75 @@ import {LongTouchBinderDirective} from '../../directives/long-touch-binder.direc
2836
NgClass,
2937
NgStyle,
3038
NgFor,
39+
AsyncPipe,
3140
KeyValuePipe,
3241
UndoBinderDirective,
3342
RedoBinderDirective,
3443
ClickBinderDirective,
3544
TapsBinderDirective,
3645
LongTouchBinderDirective
37-
],
38-
changeDetection: ChangeDetectionStrategy.OnPush
46+
]
3947
})
40-
export class TreeHistoryComponent implements OnDestroy, AfterViewInit {
41-
@Input()
42-
public width?: string;
48+
export class TreeHistoryComponent {
49+
public readonly svgViewportWidth = input(50, {transform: numberAttribute});
4350

44-
@Input()
45-
public svgViewportWidth: number = 50;
51+
public readonly svgViewportHeight = input(50, {transform: numberAttribute});
4652

47-
@Input()
48-
public svgViewportHeight: number = this.svgViewportWidth;
53+
public readonly cmdViewWidth = input(50, {transform: numberAttribute});
4954

50-
@Input()
51-
public cmdViewWidth: number = 50;
55+
public readonly cmdViewHeight = input(50, {transform: numberAttribute});
5256

53-
@Input()
54-
public cmdViewHeight: number = this.cmdViewWidth;
57+
public readonly rootRenderer: InputSignal<UndoableSnapshot | undefined> = input();
5558

56-
@Input()
57-
public rootRenderer: UndoableSnapshot = undefined;
59+
protected readonly cmdViewWidthPx = computed(() => `${this.cmdViewWidth()}px`);
5860

59-
@HostBinding('style.width')
60-
public widthcss = "";
61+
protected readonly cmdViewHeightPx = computed(() => `${this.cmdViewHeight()}px`);
6162

6263
protected cache: Record<number, unknown> = {};
6364

6465
protected cacheRoot: unknown;
6566

66-
private subscriptionUndos: Subscription;
67+
protected readonly thumbnails: Signal<Array<Thumbnail>>;
6768

68-
private subscriptionRedos: Subscription;
69+
private readonly undos: Signal<Undoable | number | undefined>;
6970

71+
@ViewChild("divHistory")
72+
protected divHistory: ElementRef<HTMLDivElement>;
7073

71-
public constructor(protected history: TreeUndoHistory,
72-
private changeDetect: ChangeDetectorRef) {
73-
// Only updating the view on history changes
74-
this.subscriptionUndos = history.undosObservable().subscribe(() => {
75-
changeDetect.detectChanges();
76-
});
7774

78-
this.subscriptionRedos = history.redosObservable().subscribe(() => {
79-
changeDetect.detectChanges();
75+
public constructor(protected history: TreeUndoHistory,
76+
// private changeDetect: ChangeDetectorRef,
77+
private sanitizer: DomSanitizer) {
78+
// Observing the undo history, but with a throttle to avoid useless updates.
79+
this.undos = toSignal<Undoable | number | undefined>(
80+
concat(this.history.sizeObservable(), this.history.undosObservable(), this.history.redosObservable())
81+
.pipe(throttleTime(200)));
82+
83+
// Computing the list of thumnbails
84+
this.thumbnails = computed(() => {
85+
// Do not need to observe rootRendered.
86+
this.cacheRoot = untracked(this.rootRenderer);
87+
88+
return [...this.history.getPositions().entries()].map(entry => ({
89+
"key": entry[0],
90+
"value": entry[1],
91+
// The use of undos() here is useless, but required to trigger the computation.
92+
"thumbnail": this.undoButtonSnapshot(this.history.undoableNodes[entry[0]], this.undos())
93+
} satisfies Thumbnail));
8094
});
8195
}
8296

83-
public ngAfterViewInit() {
84-
// Preventing the input attributes to update the view
85-
this.changeDetect.detach();
86-
}
87-
88-
public ngOnDestroy(): void {
89-
this.subscriptionUndos.unsubscribe();
90-
this.subscriptionRedos.unsubscribe();
97+
protected getContent(elt: unknown): string | SafeHtml {
98+
if(typeof elt === 'string') {
99+
return elt;
100+
}
101+
if(elt instanceof Element) {
102+
return this.sanitizer.bypassSecurityTrustHtml(elt.outerHTML);
103+
}
104+
return "";
91105
}
92106

93-
public depth(undoableNode: UndoableTreeNode | undefined): number {
107+
private depth(undoableNode: UndoableTreeNode | undefined): number {
94108
let depth = -1;
95109
let n = undoableNode;
96110

@@ -102,112 +116,95 @@ export class TreeHistoryComponent implements OnDestroy, AfterViewInit {
102116
return Math.max(0, depth);
103117
}
104118

105-
public getTop(position: number): number {
106-
return this.depth(this.history.undoableNodes[position]) * (this.cmdViewHeight + 30) + 5;
119+
protected getTop(position: number): number {
120+
return this.depth(this.history.undoableNodes[position]) * (untracked(this.cmdViewHeight) + 30) + 5;
107121
}
108122

109-
public getLeft(position: number): number {
110-
return position * (this.cmdViewWidth + 15) + 5;
123+
protected getLeft(position: number): number {
124+
return position * (this.cmdViewWidth() + 15) + 5;
111125
}
112126

113-
114-
private createHtmlTag(snapshot: Element, div: HTMLDivElement, svg: boolean): void {
115-
div.querySelectorAll('div')[0]?.remove();
116-
const width = `${this.cmdViewWidth}px`;
117-
const height = `${this.cmdViewHeight}px`;
118-
const divpic = document.createElement("div");
119-
divpic.appendChild(snapshot);
120-
divpic.style.width = width;
121-
divpic.style.height = height;
122-
127+
private configureHtmlSvgTag(snapshot: Element, svg: boolean): Element {
123128
if (svg) {
124-
snapshot.setAttribute("viewBox", `0 0 ${this.svgViewportWidth} ${this.svgViewportHeight}`);
129+
snapshot.setAttribute("viewBox", `0 0 ${untracked(this.svgViewportWidth)} ${untracked(this.svgViewportHeight)}`);
125130
}
126131

127-
snapshot.setAttribute("width", width);
128-
snapshot.setAttribute("height", height);
129-
div.appendChild(divpic);
132+
snapshot.setAttribute("width", untracked(this.cmdViewWidthPx));
133+
snapshot.setAttribute("height", untracked(this.cmdViewHeightPx));
134+
135+
return snapshot;
130136
}
131137

132138

133-
private undoButtonSnapshot_(snapshot: unknown,
134-
txt: string, div: HTMLDivElement): string | undefined {
139+
private undoButtonSnapshot_(snapshot: unknown, txt: string): string | Element {
135140
if (typeof snapshot === 'string') {
136141
return `${txt}: ${snapshot}`;
137142
}
138143

139144
if (snapshot instanceof SVGElement) {
140-
this.createHtmlTag(snapshot, div, true);
141-
return txt;
145+
return this.configureHtmlSvgTag(snapshot, true);
142146
}
143147

144148
if (snapshot instanceof HTMLElement) {
145-
this.createHtmlTag(snapshot, div, false);
146-
return undefined;
149+
return this.configureHtmlSvgTag(snapshot, false);
147150
}
148151

149152
return txt;
150153
}
151154

152-
public undoButtonSnapshot(node: UndoableTreeNode | undefined, div: HTMLDivElement): string | undefined {
155+
protected async undoButtonSnapshot(node: UndoableTreeNode | undefined, _: Undoable | number | undefined):
156+
Promise<string | HTMLDivElement | unknown> {
153157
if(node === undefined) {
154158
if (this.cacheRoot === undefined) {
155-
this.cacheRoot = this.rootRenderer;
159+
this.cacheRoot = this.rootRenderer();
156160
}
157161
}else {
158162
if (this.cache[node.id] === undefined) {
159163
this.cache[node.id] = node.visualSnapshot;
160164
}
161165
}
162166

163-
console.log("snap")
164-
console.log(node?.id);
165-
166-
167167
const snapshot = node === undefined ? this.cacheRoot : this.cache[node.id];
168168
const txt = node === undefined ? "Root" : node.undoable.getUndoName();
169169

170-
console.log(snapshot);
171-
172170
if (snapshot === undefined) {
173-
return txt;
171+
return new Promise<string>(resolve => {
172+
resolve(txt);
173+
});
174174
}
175175

176176
if (snapshot instanceof Promise) {
177-
console.log("promise")
178-
void snapshot.then((res: unknown) => {
179-
if (node !== undefined) {
180-
this.cache[node.id] = res;
181-
} else {
182-
this.cacheRoot = res;
183-
}
184-
return this.undoButtonSnapshot_(res, txt, div);
185-
});
186-
return txt;
177+
return snapshot
178+
.then((res: unknown) => {
179+
if (node !== undefined) {
180+
this.cache[node.id] = res;
181+
}
182+
else {
183+
this.cacheRoot = res;
184+
}
185+
return this.undoButtonSnapshot_(res, txt);
186+
});
187187
}
188188

189189
if(node?.id === this.history.currentNode.id) {
190-
div.scrollIntoView();
190+
this.divHistory.nativeElement?.scrollIntoView();
191191
}
192192

193-
return this.undoButtonSnapshot_(snapshot, txt, div);
193+
return this.undoButtonSnapshot_(snapshot, txt);
194194
}
195195

196-
public longTouchBinder(binder: PartialTouchTypedBinder, position: number): Array<Binding<any, any, unknown, any>> {
196+
protected longTouchBinder(binder: PartialTouchTypedBinder, position: number): Array<Binding<Command, Interaction<object>, unknown>> {
197197
return [
198198
binder
199199
.toProduceAnon(() => {
200200
this.history.delete(position);
201201
})
202202
.when(() => !this.history.keepPath)
203-
.ifHadEffects(() => {
204-
this.changeDetect.detectChanges();
205-
})
206203
.bind()
207204
];
208205
}
209206

210-
public tapsBinder(binder: PartialTapsTypedBinder, position: number): Array<Binding<any, any, unknown, any>> {
207+
protected tapsBinder(binder: PartialTapsTypedBinder, position: number): Array<Binding<Command, Interaction<object>, unknown>> {
211208
return [
212209
binder
213210
.toProduceAnon(() => {
@@ -217,7 +214,7 @@ export class TreeHistoryComponent implements OnDestroy, AfterViewInit {
217214
];
218215
}
219216

220-
public clickBinders(binder: PartialPointTypedBinder, position: number): Array<Binding<any, any, unknown, any>> {
217+
protected clickBinders(binder: PartialPointTypedBinder, position: number): Array<Binding<Command, Interaction<object>, unknown>> {
221218
return [
222219
binder
223220
.toProduceAnon(() => {
@@ -230,9 +227,6 @@ export class TreeHistoryComponent implements OnDestroy, AfterViewInit {
230227
this.history.delete(position);
231228
})
232229
.when(i => !this.history.keepPath && i.button === 2)
233-
.ifHadEffects(() => {
234-
this.changeDetect.detectChanges();
235-
})
236230
.bind()
237231
];
238232
}

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
"useDefineForClassFields": false,
3232
"lib": [
3333
"ES2022",
34-
"dom"
34+
"dom",
35+
"DOM.Iterable"
3536
]
3637
},
3738
"angularCompilerOptions": {

0 commit comments

Comments
 (0)