Skip to content

Commit a0973fc

Browse files
committed
fix(history): fixing SVG thumbnails in linear history
1 parent 54effcd commit a0973fc

File tree

2 files changed

+130
-46
lines changed

2 files changed

+130
-46
lines changed

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,21 @@ <h2>HISTORY</h2>
77

88
<div class="buttons">
99
<div #undoButtonContainer>
10-
@for (undoable of undoHistory.getUndo(); track $index) {
11-
<button #b class="history-button-active" [attr.data-index]="undoHistory.getUndo().length - $index">
12-
{{undoButtonSnapshot(undoable, b)}}
10+
@for (thumbnail of thumbnailsUndo(); track $index) {
11+
<button class="history-button-active" [attr.data-index]="history.getUndo().length - $index"
12+
[innerHTML]="getContent(thumbnail | async)">
1313
</button>
1414
}
1515
<br/>
1616
</div>
1717

1818
<div #redoButtonContainer>
19-
@if (undoHistory.getRedo().length > 0) {
19+
@if (history.getRedo().length > 0) {
2020
<p>Redo:</p>
2121
}
22-
@for (_ of undoHistory.getRedo(); track $index) {
23-
<button #b class="history-button-inactive" [attr.data-index]="$index + 1">
24-
{{undoButtonSnapshot(undoHistory.getRedo()[undoHistory.getRedo().length - $index - 1], b)}}
22+
@for (thumbnail of thumbnailsRedo().slice().reverse(); track $index) {
23+
<button class="history-button-inactive" [attr.data-index]="$index + 1"
24+
[innerHTML]="getContent(thumbnail | async)">
2525
</button>
2626
}
2727
<br/>
Lines changed: 123 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import {ClickBinderDirective} from "../../directives/click-binder.directive";
12
import {RedoBinderDirective} from "../../directives/redo-binder.directive";
23
import {UndoBinderDirective} from "../../directives/undo-binder.directive";
3-
import {NgFor, NgIf} from "@angular/common";
4-
import {Component, Input, Optional, ViewChild, AfterViewInit, ElementRef} from "@angular/core";
4+
import {AsyncPipe, NgFor, NgIf, NgStyle} from "@angular/common";
5+
import {Component, AfterViewInit, input, numberAttribute, untracked, inject, viewChild, Signal, computed} from "@angular/core";
6+
import {toSignal} from "@angular/core/rxjs-interop";
7+
import {DomSanitizer, SafeHtml} from "@angular/platform-browser";
58
import {RedoNTimes, UndoNTimes, Bindings, Undoable, UndoHistory, UndoHistoryBase} from "interacto";
9+
import {throttleTime} from "rxjs";
610

711
@Component({
812
selector: "io-linear-history",
@@ -13,73 +17,153 @@ import {RedoNTimes, UndoNTimes, Bindings, Undoable, UndoHistory, UndoHistoryBase
1317
NgFor,
1418
NgIf,
1519
UndoBinderDirective,
16-
RedoBinderDirective
20+
RedoBinderDirective,
21+
AsyncPipe,
22+
NgStyle,
23+
ClickBinderDirective
1724
]
1825
})
1926
export class LinearHistoryComponent implements AfterViewInit {
20-
@ViewChild("undoButtonContainer")
21-
protected undoButtonContainer: ElementRef<HTMLElement>;
27+
public readonly svgViewportWidth = input(50, {transform: numberAttribute});
2228

23-
@ViewChild("redoButtonContainer")
24-
protected redoButtonContainer: ElementRef<HTMLElement>;
29+
public readonly svgViewportHeight = input(50, {transform: numberAttribute});
2530

26-
@Input()
27-
@Optional()
28-
public svgViewportWidth = 50;
31+
public readonly svgIconSize = input(50, {transform: numberAttribute});
2932

30-
@Input()
31-
@Optional()
32-
public svgViewportHeight = 50;
33+
public readonly cmdViewWidth = input(50, {transform: numberAttribute});
3334

34-
@Input()
35-
@Optional()
36-
public svgIconSize = 50;
35+
public readonly cmdViewHeight = input(50, {transform: numberAttribute});
3736

38-
public constructor(protected undoHistory: UndoHistory, protected bindings: Bindings<UndoHistoryBase>) {
37+
protected readonly undoButtonContainer: Signal<HTMLElement> = viewChild.required<HTMLElement>("undoButtonContainer");
38+
39+
protected readonly redoButtonContainer: Signal<HTMLElement> = viewChild.required<HTMLElement>("redoButtonContainer");
40+
41+
protected readonly history: UndoHistory = inject(UndoHistory);
42+
43+
protected readonly bindings: Bindings<UndoHistoryBase> = inject<Bindings<UndoHistoryBase>>(Bindings<UndoHistoryBase>);
44+
45+
protected readonly cmdViewWidthPx = computed(() => `${String(this.cmdViewWidth())}px`);
46+
47+
protected readonly cmdViewHeightPx = computed(() => `${String(this.cmdViewHeight())}px`);
48+
49+
protected readonly thumbnailsUndo: Signal<Array<Promise<unknown>>>;
50+
51+
protected readonly thumbnailsRedo: Signal<Array<Promise<unknown>>>;
52+
53+
private readonly sanitizer = inject(DomSanitizer);
54+
55+
private readonly undos: Signal<[number, number] | undefined>;
56+
57+
protected cache: Record<number, unknown> = {};
58+
59+
public constructor() {
60+
this.undos = toSignal<[number, number] | undefined>(this.history.sizeObservable().pipe(throttleTime(200)));
61+
62+
this.thumbnailsUndo = computed(() => {
63+
this.undos();
64+
return this.history.getUndo().map(async (entry, index) =>
65+
this.undoButtonSnapshot(entry, index));
66+
});
67+
68+
this.thumbnailsRedo = computed(() => {
69+
const sizes = this.undos();
70+
return this.history.getRedo().map(async (entry, index) =>
71+
this.undoButtonSnapshot(entry, index + (sizes?.[0] ?? 0)));
72+
});
3973
}
4074

4175
public ngAfterViewInit(): void {
4276
this.bindings.buttonBinder()
43-
.onDynamic(this.undoButtonContainer)
77+
.onDynamic(this.undoButtonContainer())
4478
.toProduce(i => new UndoNTimes(
45-
this.undoHistory,
79+
this.history,
4680
parseInt(i.widget?.getAttribute("data-index") ?? "-1", 10)))
4781
.bind();
4882

4983
this.bindings.buttonBinder()
50-
.onDynamic(this.redoButtonContainer)
84+
.onDynamic(this.redoButtonContainer())
5185
.toProduce(i => new RedoNTimes(
52-
this.undoHistory,
86+
this.history,
5387
parseInt(i.widget?.getAttribute("data-index") ?? "-1", 10)))
5488
.bind();
5589
}
5690

57-
public undoButtonSnapshot(command: Undoable, button: HTMLButtonElement): unknown {
58-
const snapshot = command.getVisualSnapshot();
91+
protected async undoButtonSnapshot(command: Undoable, index: number): Promise<unknown> {
92+
const snapshot = this.cache[index] ?? command.getVisualSnapshot();
93+
const txt = command.getUndoName();
94+
5995
if (snapshot === undefined) {
60-
return command.getUndoName();
96+
return new Promise<string>(resolve => {
97+
resolve(txt);
98+
});
6199
}
62100

63101
if (typeof snapshot === "string") {
64-
return `${command.getUndoName()}: ${snapshot}`;
102+
return new Promise<string>(resolve => {
103+
resolve(`${txt}: ${snapshot}`);
104+
});
105+
}
106+
107+
if (snapshot instanceof Promise) {
108+
return snapshot
109+
.then((res: unknown) => {
110+
this.cache[index] = res;
111+
return this.undoButtonSnapshot_(res, txt);
112+
});
65113
}
66114

67115
if (snapshot instanceof SVGElement) {
68-
button.querySelectorAll("div")[0].remove();
69-
70-
const size = `${String(this.svgIconSize)}px`;
71-
const div = document.createElement("div");
72-
div.appendChild(snapshot);
73-
div.style.width = size;
74-
div.style.height = size;
75-
snapshot.setAttribute("viewBox", `0 0 ${String(this.svgViewportWidth)} ${String(this.svgViewportHeight)}`);
76-
snapshot.setAttribute("width", size);
77-
snapshot.setAttribute("height", size);
78-
button.querySelectorAll("div")[0].remove();
79-
button.appendChild(div);
80-
return command.getUndoName();
116+
return new Promise<Element>(resolve => {
117+
resolve(this.configureHtmlSvgTag(snapshot, true));
118+
});
119+
}
120+
121+
if (snapshot instanceof Element) {
122+
return new Promise<Element>(resolve => {
123+
resolve(this.configureHtmlSvgTag(snapshot, false));
124+
});
125+
}
126+
127+
return new Promise<string>(resolve => {
128+
resolve(txt);
129+
});
130+
}
131+
132+
protected getContent(elt: unknown): string | SafeHtml {
133+
if (typeof elt === "string") {
134+
return elt;
135+
}
136+
if (elt instanceof Element) {
137+
return this.sanitizer.bypassSecurityTrustHtml(elt.outerHTML);
138+
}
139+
return "";
140+
}
141+
142+
private configureHtmlSvgTag(snapshot: Element | SVGElement, svg: boolean): Element {
143+
if (svg) {
144+
snapshot.setAttribute("viewBox", `0 0 ${String(untracked(this.svgViewportWidth))} ${String(untracked(this.svgViewportHeight))}`);
145+
}
146+
147+
snapshot.setAttribute("pointer-events", "none");
148+
snapshot.setAttribute("width", String(untracked(this.cmdViewWidthPx)));
149+
snapshot.setAttribute("height", String(untracked(this.cmdViewHeightPx)));
150+
151+
return snapshot;
152+
}
153+
154+
private undoButtonSnapshot_(snapshot: unknown, txt: string): string | Element {
155+
if (typeof snapshot === "string") {
156+
return `${txt}: ${snapshot}`;
157+
}
158+
159+
if (snapshot instanceof SVGElement) {
160+
return this.configureHtmlSvgTag(snapshot, true);
161+
}
162+
163+
if (snapshot instanceof HTMLElement) {
164+
return this.configureHtmlSvgTag(snapshot, false);
81165
}
82166

83-
return command.getUndoName();
167+
return txt;
84168
}
85169
}

0 commit comments

Comments
 (0)