1
- import { KeyValuePipe , NgClass , NgFor , NgStyle } from '@angular/common' ;
1
+ import { AsyncPipe , KeyValuePipe , NgClass , NgFor , NgStyle } from '@angular/common' ;
2
2
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
10
7
} 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
+ }
18
26
19
27
/**
20
28
* The Angular component for display a tree-based undo/redo history
@@ -28,69 +36,75 @@ import {LongTouchBinderDirective} from '../../directives/long-touch-binder.direc
28
36
NgClass ,
29
37
NgStyle ,
30
38
NgFor ,
39
+ AsyncPipe ,
31
40
KeyValuePipe ,
32
41
UndoBinderDirective ,
33
42
RedoBinderDirective ,
34
43
ClickBinderDirective ,
35
44
TapsBinderDirective ,
36
45
LongTouchBinderDirective
37
- ] ,
38
- changeDetection : ChangeDetectionStrategy . OnPush
46
+ ]
39
47
} )
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 } ) ;
43
50
44
- @Input ( )
45
- public svgViewportWidth : number = 50 ;
51
+ public readonly svgViewportHeight = input ( 50 , { transform : numberAttribute } ) ;
46
52
47
- @Input ( )
48
- public svgViewportHeight : number = this . svgViewportWidth ;
53
+ public readonly cmdViewWidth = input ( 50 , { transform : numberAttribute } ) ;
49
54
50
- @Input ( )
51
- public cmdViewWidth : number = 50 ;
55
+ public readonly cmdViewHeight = input ( 50 , { transform : numberAttribute } ) ;
52
56
53
- @Input ( )
54
- public cmdViewHeight : number = this . cmdViewWidth ;
57
+ public readonly rootRenderer : InputSignal < UndoableSnapshot | undefined > = input ( ) ;
55
58
56
- @Input ( )
57
- public rootRenderer : UndoableSnapshot = undefined ;
59
+ protected readonly cmdViewWidthPx = computed ( ( ) => `${ this . cmdViewWidth ( ) } px` ) ;
58
60
59
- @HostBinding ( 'style.width' )
60
- public widthcss = "" ;
61
+ protected readonly cmdViewHeightPx = computed ( ( ) => `${ this . cmdViewHeight ( ) } px` ) ;
61
62
62
63
protected cache : Record < number , unknown > = { } ;
63
64
64
65
protected cacheRoot : unknown ;
65
66
66
- private subscriptionUndos : Subscription ;
67
+ protected readonly thumbnails : Signal < Array < Thumbnail > > ;
67
68
68
- private subscriptionRedos : Subscription ;
69
+ private readonly undos : Signal < Undoable | number | undefined > ;
69
70
71
+ @ViewChild ( "divHistory" )
72
+ protected divHistory : ElementRef < HTMLDivElement > ;
70
73
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
- } ) ;
77
74
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 ) ) ;
80
94
} ) ;
81
95
}
82
96
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 "" ;
91
105
}
92
106
93
- public depth ( undoableNode : UndoableTreeNode | undefined ) : number {
107
+ private depth ( undoableNode : UndoableTreeNode | undefined ) : number {
94
108
let depth = - 1 ;
95
109
let n = undoableNode ;
96
110
@@ -102,112 +116,95 @@ export class TreeHistoryComponent implements OnDestroy, AfterViewInit {
102
116
return Math . max ( 0 , depth ) ;
103
117
}
104
118
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 ;
107
121
}
108
122
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 ;
111
125
}
112
126
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 {
123
128
if ( svg ) {
124
- snapshot . setAttribute ( "viewBox" , `0 0 ${ this . svgViewportWidth } ${ this . svgViewportHeight } ` ) ;
129
+ snapshot . setAttribute ( "viewBox" , `0 0 ${ untracked ( this . svgViewportWidth ) } ${ untracked ( this . svgViewportHeight ) } ` ) ;
125
130
}
126
131
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 ;
130
136
}
131
137
132
138
133
- private undoButtonSnapshot_ ( snapshot : unknown ,
134
- txt : string , div : HTMLDivElement ) : string | undefined {
139
+ private undoButtonSnapshot_ ( snapshot : unknown , txt : string ) : string | Element {
135
140
if ( typeof snapshot === 'string' ) {
136
141
return `${ txt } : ${ snapshot } ` ;
137
142
}
138
143
139
144
if ( snapshot instanceof SVGElement ) {
140
- this . createHtmlTag ( snapshot , div , true ) ;
141
- return txt ;
145
+ return this . configureHtmlSvgTag ( snapshot , true ) ;
142
146
}
143
147
144
148
if ( snapshot instanceof HTMLElement ) {
145
- this . createHtmlTag ( snapshot , div , false ) ;
146
- return undefined ;
149
+ return this . configureHtmlSvgTag ( snapshot , false ) ;
147
150
}
148
151
149
152
return txt ;
150
153
}
151
154
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 > {
153
157
if ( node === undefined ) {
154
158
if ( this . cacheRoot === undefined ) {
155
- this . cacheRoot = this . rootRenderer ;
159
+ this . cacheRoot = this . rootRenderer ( ) ;
156
160
}
157
161
} else {
158
162
if ( this . cache [ node . id ] === undefined ) {
159
163
this . cache [ node . id ] = node . visualSnapshot ;
160
164
}
161
165
}
162
166
163
- console . log ( "snap" )
164
- console . log ( node ?. id ) ;
165
-
166
-
167
167
const snapshot = node === undefined ? this . cacheRoot : this . cache [ node . id ] ;
168
168
const txt = node === undefined ? "Root" : node . undoable . getUndoName ( ) ;
169
169
170
- console . log ( snapshot ) ;
171
-
172
170
if ( snapshot === undefined ) {
173
- return txt ;
171
+ return new Promise < string > ( resolve => {
172
+ resolve ( txt ) ;
173
+ } ) ;
174
174
}
175
175
176
176
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
+ } ) ;
187
187
}
188
188
189
189
if ( node ?. id === this . history . currentNode . id ) {
190
- div . scrollIntoView ( ) ;
190
+ this . divHistory . nativeElement ? .scrollIntoView ( ) ;
191
191
}
192
192
193
- return this . undoButtonSnapshot_ ( snapshot , txt , div ) ;
193
+ return this . undoButtonSnapshot_ ( snapshot , txt ) ;
194
194
}
195
195
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 > > {
197
197
return [
198
198
binder
199
199
. toProduceAnon ( ( ) => {
200
200
this . history . delete ( position ) ;
201
201
} )
202
202
. when ( ( ) => ! this . history . keepPath )
203
- . ifHadEffects ( ( ) => {
204
- this . changeDetect . detectChanges ( ) ;
205
- } )
206
203
. bind ( )
207
204
] ;
208
205
}
209
206
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 > > {
211
208
return [
212
209
binder
213
210
. toProduceAnon ( ( ) => {
@@ -217,7 +214,7 @@ export class TreeHistoryComponent implements OnDestroy, AfterViewInit {
217
214
] ;
218
215
}
219
216
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 > > {
221
218
return [
222
219
binder
223
220
. toProduceAnon ( ( ) => {
@@ -230,9 +227,6 @@ export class TreeHistoryComponent implements OnDestroy, AfterViewInit {
230
227
this . history . delete ( position ) ;
231
228
} )
232
229
. when ( i => ! this . history . keepPath && i . button === 2 )
233
- . ifHadEffects ( ( ) => {
234
- this . changeDetect . detectChanges ( ) ;
235
- } )
236
230
. bind ( )
237
231
] ;
238
232
}
0 commit comments