10
10
* Directions that can be used when setting sticky positioning.
11
11
* @docs -private
12
12
*/
13
+ import { afterNextRender , Injector } from '@angular/core' ;
13
14
import { Direction } from '@angular/cdk/bidi' ;
14
15
import { _CoalescedStyleScheduler } from './coalesced-style-scheduler' ;
15
16
import { StickyPositioningListener } from './sticky-position-listener' ;
@@ -41,6 +42,7 @@ export class StickyStyler {
41
42
private _stickyColumnsReplayTimeout : number | null = null ;
42
43
private _cachedCellWidths : number [ ] = [ ] ;
43
44
private readonly _borderCellCss : Readonly < { [ d in StickyDirection ] : string } > ;
45
+ private _destroyed = false ;
44
46
45
47
/**
46
48
* @param _isNativeHtmlTable Whether the sticky logic should be based on a table
@@ -55,6 +57,7 @@ export class StickyStyler {
55
57
* the component stylesheet for _stickCellCss.
56
58
* @param _positionListener A listener that is notified of changes to sticky rows/columns
57
59
* and their dimensions.
60
+ * @param _tableInjector The table's Injector.
58
61
*/
59
62
constructor (
60
63
private _isNativeHtmlTable : boolean ,
@@ -64,6 +67,7 @@ export class StickyStyler {
64
67
private _isBrowser = true ,
65
68
private readonly _needsPositionStickyOnElement = true ,
66
69
private readonly _positionListener ?: StickyPositioningListener ,
70
+ private readonly _tableInjector ?: Injector ,
67
71
) {
68
72
this . _borderCellCss = {
69
73
'top' : `${ _stickCellCss } -border-elem-top` ,
@@ -92,17 +96,16 @@ export class StickyStyler {
92
96
continue ;
93
97
}
94
98
95
- elementsToClear . push ( row ) ;
96
- for ( let i = 0 ; i < row . children . length ; i ++ ) {
97
- elementsToClear . push ( row . children [ i ] as HTMLElement ) ;
98
- }
99
+ elementsToClear . push ( row , ...( Array . from ( row . children ) as HTMLElement [ ] ) ) ;
99
100
}
100
101
101
102
// Coalesce with sticky row/column updates (and potentially other changes like column resize).
102
- this . _coalescedStyleScheduler . schedule ( ( ) => {
103
- for ( const element of elementsToClear ) {
104
- this . _removeStickyStyle ( element , stickyDirections ) ;
105
- }
103
+ this . _afterNextRender ( {
104
+ write : ( ) => {
105
+ for ( const element of elementsToClear ) {
106
+ this . _removeStickyStyle ( element , stickyDirections ) ;
107
+ }
108
+ } ,
106
109
} ) ;
107
110
}
108
111
@@ -147,53 +150,61 @@ export class StickyStyler {
147
150
}
148
151
149
152
// Coalesce with sticky row updates (and potentially other changes like column resize).
150
- this . _coalescedStyleScheduler . schedule ( ( ) => {
151
- const firstRow = rows [ 0 ] ;
152
- const numCells = firstRow . children . length ;
153
- const cellWidths : number [ ] = this . _getCellWidths ( firstRow , recalculateCellWidths ) ;
154
-
155
- const startPositions = this . _getStickyStartColumnPositions ( cellWidths , stickyStartStates ) ;
156
- const endPositions = this . _getStickyEndColumnPositions ( cellWidths , stickyEndStates ) ;
157
-
158
- const lastStickyStart = stickyStartStates . lastIndexOf ( true ) ;
159
- const firstStickyEnd = stickyEndStates . indexOf ( true ) ;
160
-
161
- const isRtl = this . direction === 'rtl' ;
162
- const start = isRtl ? 'right' : 'left' ;
163
- const end = isRtl ? 'left' : 'right' ;
164
-
165
- for ( const row of rows ) {
166
- for ( let i = 0 ; i < numCells ; i ++ ) {
167
- const cell = row . children [ i ] as HTMLElement ;
168
- if ( stickyStartStates [ i ] ) {
169
- this . _addStickyStyle ( cell , start , startPositions [ i ] , i === lastStickyStart ) ;
170
- }
171
-
172
- if ( stickyEndStates [ i ] ) {
173
- this . _addStickyStyle ( cell , end , endPositions [ i ] , i === firstStickyEnd ) ;
153
+ const firstRow = rows [ 0 ] ;
154
+ const numCells = firstRow . children . length ;
155
+
156
+ const isRtl = this . direction === 'rtl' ;
157
+ const start = isRtl ? 'right' : 'left' ;
158
+ const end = isRtl ? 'left' : 'right' ;
159
+
160
+ const lastStickyStart = stickyStartStates . lastIndexOf ( true ) ;
161
+ const firstStickyEnd = stickyEndStates . indexOf ( true ) ;
162
+
163
+ let cellWidths : number [ ] ;
164
+ let startPositions : number [ ] ;
165
+ let endPositions : number [ ] ;
166
+
167
+ this . _afterNextRender ( {
168
+ earlyRead : ( ) => {
169
+ cellWidths = this . _getCellWidths ( firstRow , recalculateCellWidths ) ;
170
+
171
+ startPositions = this . _getStickyStartColumnPositions ( cellWidths , stickyStartStates ) ;
172
+ endPositions = this . _getStickyEndColumnPositions ( cellWidths , stickyEndStates ) ;
173
+ } ,
174
+ write : ( ) => {
175
+ for ( const row of rows ) {
176
+ for ( let i = 0 ; i < numCells ; i ++ ) {
177
+ const cell = row . children [ i ] as HTMLElement ;
178
+ if ( stickyStartStates [ i ] ) {
179
+ this . _addStickyStyle ( cell , start , startPositions [ i ] , i === lastStickyStart ) ;
180
+ }
181
+
182
+ if ( stickyEndStates [ i ] ) {
183
+ this . _addStickyStyle ( cell , end , endPositions [ i ] , i === firstStickyEnd ) ;
184
+ }
174
185
}
175
186
}
176
- }
177
187
178
- if ( this . _positionListener ) {
179
- this . _positionListener . stickyColumnsUpdated ( {
180
- sizes :
181
- lastStickyStart === - 1
182
- ? [ ]
183
- : cellWidths
184
- . slice ( 0 , lastStickyStart + 1 )
185
- . map ( ( width , index ) => ( stickyStartStates [ index ] ? width : null ) ) ,
186
- } ) ;
187
- this . _positionListener . stickyEndColumnsUpdated ( {
188
- sizes :
189
- firstStickyEnd === - 1
190
- ? [ ]
191
- : cellWidths
192
- . slice ( firstStickyEnd )
193
- . map ( ( width , index ) => ( stickyEndStates [ index + firstStickyEnd ] ? width : null ) )
194
- . reverse ( ) ,
195
- } ) ;
196
- }
188
+ if ( this . _positionListener ) {
189
+ this . _positionListener . stickyColumnsUpdated ( {
190
+ sizes :
191
+ lastStickyStart === - 1
192
+ ? [ ]
193
+ : cellWidths
194
+ . slice ( 0 , lastStickyStart + 1 )
195
+ . map ( ( width , index ) => ( stickyStartStates [ index ] ? width : null ) ) ,
196
+ } ) ;
197
+ this . _positionListener . stickyEndColumnsUpdated ( {
198
+ sizes :
199
+ firstStickyEnd === - 1
200
+ ? [ ]
201
+ : cellWidths
202
+ . slice ( firstStickyEnd )
203
+ . map ( ( width , index ) => ( stickyEndStates [ index + firstStickyEnd ] ? width : null ) )
204
+ . reverse ( ) ,
205
+ } ) ;
206
+ }
207
+ } ,
197
208
} ) ;
198
209
}
199
210
@@ -214,63 +225,66 @@ export class StickyStyler {
214
225
return ;
215
226
}
216
227
228
+ // If positioning the rows to the bottom, reverse their order when evaluating the sticky
229
+ // position such that the last row stuck will be "bottom: 0px" and so on. Note that the
230
+ // sticky states need to be reversed as well.
231
+ const rows = position === 'bottom' ? rowsToStick . slice ( ) . reverse ( ) : rowsToStick ;
232
+ const states = position === 'bottom' ? stickyStates . slice ( ) . reverse ( ) : stickyStates ;
233
+
234
+ // Measure row heights all at once before adding sticky styles to reduce layout thrashing.
235
+ const stickyOffsets : number [ ] = [ ] ;
236
+ const stickyCellHeights : ( number | undefined ) [ ] = [ ] ;
237
+ const elementsToStick : HTMLElement [ ] [ ] = [ ] ;
238
+
217
239
// Coalesce with other sticky row updates (top/bottom), sticky columns updates
218
240
// (and potentially other changes like column resize).
219
- this . _coalescedStyleScheduler . schedule ( ( ) => {
220
- // If positioning the rows to the bottom, reverse their order when evaluating the sticky
221
- // position such that the last row stuck will be "bottom: 0px" and so on. Note that the
222
- // sticky states need to be reversed as well.
223
- const rows = position === 'bottom' ? rowsToStick . slice ( ) . reverse ( ) : rowsToStick ;
224
- const states = position === 'bottom' ? stickyStates . slice ( ) . reverse ( ) : stickyStates ;
225
-
226
- // Measure row heights all at once before adding sticky styles to reduce layout thrashing.
227
- const stickyOffsets : number [ ] = [ ] ;
228
- const stickyCellHeights : ( number | undefined ) [ ] = [ ] ;
229
- const elementsToStick : HTMLElement [ ] [ ] = [ ] ;
230
-
231
- for ( let rowIndex = 0 , stickyOffset = 0 ; rowIndex < rows . length ; rowIndex ++ ) {
232
- if ( ! states [ rowIndex ] ) {
233
- continue ;
234
- }
241
+ this . _afterNextRender ( {
242
+ earlyRead : ( ) => {
243
+ for ( let rowIndex = 0 , stickyOffset = 0 ; rowIndex < rows . length ; rowIndex ++ ) {
244
+ if ( ! states [ rowIndex ] ) {
245
+ continue ;
246
+ }
235
247
236
- stickyOffsets [ rowIndex ] = stickyOffset ;
237
- const row = rows [ rowIndex ] ;
238
- elementsToStick [ rowIndex ] = this . _isNativeHtmlTable
239
- ? ( Array . from ( row . children ) as HTMLElement [ ] )
240
- : [ row ] ;
248
+ stickyOffsets [ rowIndex ] = stickyOffset ;
249
+ const row = rows [ rowIndex ] ;
250
+ elementsToStick [ rowIndex ] = this . _isNativeHtmlTable
251
+ ? ( Array . from ( row . children ) as HTMLElement [ ] )
252
+ : [ row ] ;
241
253
242
- const height = this . _retrieveElementSize ( row ) . height ;
243
- stickyOffset += height ;
244
- stickyCellHeights [ rowIndex ] = height ;
245
- }
254
+ const height = this . _retrieveElementSize ( row ) . height ;
255
+ stickyOffset += height ;
256
+ stickyCellHeights [ rowIndex ] = height ;
257
+ }
258
+ } ,
259
+ write : ( ) => {
260
+ const borderedRowIndex = states . lastIndexOf ( true ) ;
246
261
247
- const borderedRowIndex = states . lastIndexOf ( true ) ;
262
+ for ( let rowIndex = 0 ; rowIndex < rows . length ; rowIndex ++ ) {
263
+ if ( ! states [ rowIndex ] ) {
264
+ continue ;
265
+ }
248
266
249
- for ( let rowIndex = 0 ; rowIndex < rows . length ; rowIndex ++ ) {
250
- if ( ! states [ rowIndex ] ) {
251
- continue ;
267
+ const offset = stickyOffsets [ rowIndex ] ;
268
+ const isBorderedRowIndex = rowIndex === borderedRowIndex ;
269
+ for ( const element of elementsToStick [ rowIndex ] ) {
270
+ this . _addStickyStyle ( element , position , offset , isBorderedRowIndex ) ;
271
+ }
252
272
}
253
273
254
- const offset = stickyOffsets [ rowIndex ] ;
255
- const isBorderedRowIndex = rowIndex === borderedRowIndex ;
256
- for ( const element of elementsToStick [ rowIndex ] ) {
257
- this . _addStickyStyle ( element , position , offset , isBorderedRowIndex ) ;
274
+ if ( position === 'top' ) {
275
+ this . _positionListener ?. stickyHeaderRowsUpdated ( {
276
+ sizes : stickyCellHeights ,
277
+ offsets : stickyOffsets ,
278
+ elements : elementsToStick ,
279
+ } ) ;
280
+ } else {
281
+ this . _positionListener ?. stickyFooterRowsUpdated ( {
282
+ sizes : stickyCellHeights ,
283
+ offsets : stickyOffsets ,
284
+ elements : elementsToStick ,
285
+ } ) ;
258
286
}
259
- }
260
-
261
- if ( position === 'top' ) {
262
- this . _positionListener ?. stickyHeaderRowsUpdated ( {
263
- sizes : stickyCellHeights ,
264
- offsets : stickyOffsets ,
265
- elements : elementsToStick ,
266
- } ) ;
267
- } else {
268
- this . _positionListener ?. stickyFooterRowsUpdated ( {
269
- sizes : stickyCellHeights ,
270
- offsets : stickyOffsets ,
271
- elements : elementsToStick ,
272
- } ) ;
273
- }
287
+ } ,
274
288
} ) ;
275
289
}
276
290
@@ -286,19 +300,30 @@ export class StickyStyler {
286
300
}
287
301
288
302
// Coalesce with other sticky updates (and potentially other changes like column resize).
289
- this . _coalescedStyleScheduler . schedule ( ( ) => {
290
- const tfoot = tableElement . querySelector ( 'tfoot' ) ! ;
291
-
292
- if ( tfoot ) {
293
- if ( stickyStates . some ( state => ! state ) ) {
294
- this . _removeStickyStyle ( tfoot , [ 'bottom' ] ) ;
295
- } else {
296
- this . _addStickyStyle ( tfoot , 'bottom' , 0 , false ) ;
303
+ this . _afterNextRender ( {
304
+ write : ( ) => {
305
+ const tfoot = tableElement . querySelector ( 'tfoot' ) ! ;
306
+
307
+ if ( tfoot ) {
308
+ if ( stickyStates . some ( state => ! state ) ) {
309
+ this . _removeStickyStyle ( tfoot , [ 'bottom' ] ) ;
310
+ } else {
311
+ this . _addStickyStyle ( tfoot , 'bottom' , 0 , false ) ;
312
+ }
297
313
}
298
- }
314
+ } ,
299
315
} ) ;
300
316
}
301
317
318
+ /** Triggered by the table's OnDestroy hook. */
319
+ destroy ( ) {
320
+ if ( this . _stickyColumnsReplayTimeout ) {
321
+ clearTimeout ( this . _stickyColumnsReplayTimeout ) ;
322
+ }
323
+
324
+ this . _destroyed = true ;
325
+ }
326
+
302
327
/**
303
328
* Removes the sticky style on the element by removing the sticky cell CSS class, re-evaluating
304
329
* the zIndex, removing each of the provided sticky directions, and removing the
@@ -516,6 +541,10 @@ export class StickyStyler {
516
541
}
517
542
518
543
this . _stickyColumnsReplayTimeout = setTimeout ( ( ) => {
544
+ if ( this . _destroyed ) {
545
+ return ;
546
+ }
547
+
519
548
for ( const update of this . _updatedStickyColumnsParamsToReplay ) {
520
549
this . updateStickyColumns (
521
550
update . rows ,
@@ -530,6 +559,21 @@ export class StickyStyler {
530
559
} , 0 ) ;
531
560
}
532
561
}
562
+
563
+ /**
564
+ * Invoke afterNextRender with the table's injector, falling back to CoalescedStyleScheduler
565
+ * if the injector was not provided.
566
+ */
567
+ private _afterNextRender ( spec : { earlyRead ?: ( ) => void ; write : ( ) => void } ) {
568
+ if ( this . _tableInjector ) {
569
+ afterNextRender ( spec , { injector : this . _tableInjector } ) ;
570
+ } else {
571
+ this . _coalescedStyleScheduler . schedule ( ( ) => {
572
+ spec . earlyRead ?.( ) ;
573
+ spec . write ( ) ;
574
+ } ) ;
575
+ }
576
+ }
533
577
}
534
578
535
579
function isCell ( element : Element ) {
0 commit comments