@@ -62,6 +62,25 @@ export class KanbanView extends BasesViewBase {
6262 super . onload ( ) ;
6363 }
6464
65+ /**
66+ * BasesView lifecycle: Called when Bases data changes.
67+ * Override to preserve scroll position during re-renders.
68+ */
69+ onDataUpdated ( ) : void {
70+ // Save scroll state before re-render
71+ const savedState = this . getEphemeralState ( ) ;
72+
73+ try {
74+ this . render ( ) ;
75+ } catch ( error ) {
76+ console . error ( `[TaskNotes][${ this . type } ] Render error:` , error ) ;
77+ this . renderError ( error as Error ) ;
78+ }
79+
80+ // Restore scroll state after render
81+ this . setEphemeralState ( savedState ) ;
82+ }
83+
6584 /**
6685 * Read view configuration options from BasesViewConfig.
6786 */
@@ -97,6 +116,116 @@ export class KanbanView extends BasesViewBase {
97116 }
98117 }
99118
119+ /**
120+ * Save ephemeral state including scroll positions for all columns.
121+ * This preserves scroll position when the view is re-rendered (e.g., after task updates).
122+ */
123+ getEphemeralState ( ) : any {
124+ const columnScroll : Record < string , number > = { } ;
125+
126+ // Save scroll position for virtual scrolling columns (from VirtualScroller)
127+ for ( const [ columnKey , scroller ] of this . columnScrollers ) {
128+ const scrollContainer = ( scroller as any ) . scrollContainer as HTMLElement | undefined ;
129+ if ( scrollContainer ) {
130+ columnScroll [ columnKey ] = scrollContainer . scrollTop ;
131+ }
132+ }
133+
134+ // Save scroll position for non-virtual columns (direct DOM elements)
135+ if ( this . boardEl ) {
136+ const columns = this . boardEl . querySelectorAll ( '.kanban-view__column' ) ;
137+ columns . forEach ( ( column ) => {
138+ const groupKey = column . getAttribute ( 'data-group' ) ;
139+ const cardsContainer = column . querySelector ( '.kanban-view__cards' ) as HTMLElement ;
140+ if ( groupKey && cardsContainer && ! ( groupKey in columnScroll ) ) {
141+ columnScroll [ groupKey ] = cardsContainer . scrollTop ;
142+ }
143+ } ) ;
144+
145+ // Also save swimlane cell scroll positions (class is kanban-view__swimlane-column)
146+ const swimlaneCells = this . boardEl . querySelectorAll ( '.kanban-view__swimlane-column' ) ;
147+ swimlaneCells . forEach ( ( cell ) => {
148+ const columnKey = cell . getAttribute ( 'data-column' ) ;
149+ const swimlaneKey = cell . getAttribute ( 'data-swimlane' ) ;
150+ if ( columnKey && swimlaneKey ) {
151+ const cellKey = `${ swimlaneKey } :${ columnKey } ` ;
152+ const tasksContainer = cell . querySelector ( '.kanban-view__tasks-container' ) as HTMLElement ;
153+ if ( tasksContainer && ! ( cellKey in columnScroll ) ) {
154+ columnScroll [ cellKey ] = tasksContainer . scrollTop ;
155+ }
156+ }
157+ } ) ;
158+ }
159+
160+ return {
161+ scrollTop : this . rootElement ?. scrollTop || 0 ,
162+ columnScroll,
163+ } ;
164+ }
165+
166+ /**
167+ * Restore ephemeral state including scroll positions for all columns.
168+ */
169+ setEphemeralState ( state : any ) : void {
170+ if ( ! state ) return ;
171+
172+ // Restore board-level horizontal scroll
173+ if ( state . scrollTop !== undefined && this . rootElement ) {
174+ requestAnimationFrame ( ( ) => {
175+ if ( this . rootElement && this . rootElement . isConnected ) {
176+ this . rootElement . scrollTop = state . scrollTop ;
177+ }
178+ } ) ;
179+ }
180+
181+ // Restore column scroll positions after render completes
182+ if ( state . columnScroll && typeof state . columnScroll === 'object' ) {
183+ // Use requestAnimationFrame to ensure DOM and VirtualScrollers are ready
184+ requestAnimationFrame ( ( ) => {
185+ // Restore virtual scroller positions
186+ for ( const [ columnKey , scroller ] of this . columnScrollers ) {
187+ const scrollTop = state . columnScroll [ columnKey ] ;
188+ if ( scrollTop !== undefined ) {
189+ const scrollContainer = ( scroller as any ) . scrollContainer as HTMLElement | undefined ;
190+ if ( scrollContainer ) {
191+ scrollContainer . scrollTop = scrollTop ;
192+ }
193+ }
194+ }
195+
196+ // Restore non-virtual column positions
197+ if ( this . boardEl ) {
198+ const columns = this . boardEl . querySelectorAll ( '.kanban-view__column' ) ;
199+ columns . forEach ( ( column ) => {
200+ const groupKey = column . getAttribute ( 'data-group' ) ;
201+ if ( groupKey && state . columnScroll [ groupKey ] !== undefined ) {
202+ const cardsContainer = column . querySelector ( '.kanban-view__cards' ) as HTMLElement ;
203+ if ( cardsContainer && ! this . columnScrollers . has ( groupKey ) ) {
204+ cardsContainer . scrollTop = state . columnScroll [ groupKey ] ;
205+ }
206+ }
207+ } ) ;
208+
209+ // Restore swimlane cell positions (class is kanban-view__swimlane-column)
210+ const swimlaneCells = this . boardEl . querySelectorAll ( '.kanban-view__swimlane-column' ) ;
211+ swimlaneCells . forEach ( ( cell ) => {
212+ const columnKey = cell . getAttribute ( 'data-column' ) ;
213+ const swimlaneKey = cell . getAttribute ( 'data-swimlane' ) ;
214+ if ( columnKey && swimlaneKey ) {
215+ const cellKey = `${ swimlaneKey } :${ columnKey } ` ;
216+ if ( state . columnScroll [ cellKey ] !== undefined ) {
217+ const tasksContainer = cell . querySelector ( '.kanban-view__tasks-container' ) as HTMLElement ;
218+ if ( tasksContainer && ! this . columnScrollers . has ( cellKey ) ) {
219+ tasksContainer . scrollTop = state . columnScroll [ cellKey ] ;
220+ }
221+ }
222+ }
223+ } ) ;
224+ }
225+ } ) ;
226+ }
227+ }
228+
100229 async render ( ) : Promise < void > {
101230 if ( ! this . boardEl || ! this . rootElement ) return ;
102231 if ( ! this . data ?. data ) return ;
@@ -1211,6 +1340,26 @@ export class KanbanView extends BasesViewBase {
12111340 this . debouncedRefresh ( ) ;
12121341 }
12131342
1343+ /**
1344+ * Override debouncedRefresh to preserve scroll positions during re-renders.
1345+ * Saves ephemeral state before render and restores it after.
1346+ */
1347+ protected debouncedRefresh ( ) : void {
1348+ if ( ( this as any ) . updateDebounceTimer ) {
1349+ clearTimeout ( ( this as any ) . updateDebounceTimer ) ;
1350+ }
1351+
1352+ // Save current scroll state before the timer fires
1353+ const savedState = this . getEphemeralState ( ) ;
1354+
1355+ ( this as any ) . updateDebounceTimer = window . setTimeout ( async ( ) => {
1356+ await this . render ( ) ;
1357+ ( this as any ) . updateDebounceTimer = null ;
1358+ // Restore scroll state after render completes
1359+ this . setEphemeralState ( savedState ) ;
1360+ } , 150 ) ;
1361+ }
1362+
12141363 private renderEmptyState ( ) : void {
12151364 if ( ! this . boardEl ) return ;
12161365 const empty = document . createElement ( "div" ) ;
0 commit comments