@@ -2,9 +2,24 @@ import Base from './Base.mjs';
22import DomAccess from '../DomAccess.mjs' ;
33
44/**
5- * Syncs the scroll state of 2 DOM nodes
5+ * @summary This main thread addon is responsible for synchronizing the scroll positions of two DOM nodes.
6+ *
7+ * This addon is a key part of Neo.mjs's strategy for creating high-performance, virtualized components like grids and lists.
8+ * In a multi-threaded environment, the component that renders the data (e.g., the grid body, running in the app worker)
9+ * is often decoupled from the element that displays the scrollbar (which might be a custom component).
10+ *
11+ * By centralizing the scroll event handling in the main thread, `ScrollSync` can efficiently listen to DOM scroll events
12+ * and update the scroll position of a target node without the need for expensive, high-frequency communication between workers.
13+ * This makes it essential for implementing custom scrollbars or linking the scroll behavior of separate UI elements.
14+ *
15+ * This class manages registrations for scroll synchronization, allowing for both one-way and two-way binding between elements.
16+ *
17+ * Key concepts: scroll synchronization, custom scrollbar, virtual scrolling, grid, main thread addon, DOM manipulation, event handling.
18+ *
619 * @class Neo.main.addon.ScrollSync
720 * @extends Neo.main.addon.Base
21+ * @see Neo.grid.VerticalScrollbar
22+ * @see Neo.grid.Container
823 */
924class ScrollSync extends Base {
1025 static config = {
@@ -26,13 +41,21 @@ class ScrollSync extends Base {
2641 }
2742 }
2843
44+ /**
45+ * @member {Map<String, Object>} registrations=new Map()
46+ * @protected
47+ */
48+ registrations = new Map ( )
49+
2950 /**
3051 * @param {String } fromId
3152 * @param {String } toId
3253 * @param {String } direction
3354 */
3455 addScrollListener ( fromId , toId , direction ) {
35- DomAccess . getElement ( fromId ) ?. addEventListener ( 'scroll' , this . onScroll . bind ( this , toId , direction ) )
56+ let listener = this . onScroll . bind ( this , toId , direction ) ;
57+ DomAccess . getElement ( fromId ) ?. addEventListener ( 'scroll' , listener ) ;
58+ return listener
3659 }
3760
3861 /**
@@ -60,28 +83,73 @@ class ScrollSync extends Base {
6083 }
6184
6285 /**
86+ * Registers a scroll synchronization between two DOM nodes.
87+ * The method is idempotent: calling it multiple times with the same `id` will not create duplicate listeners.
88+ * If the registration parameters have changed, the old registration will be removed and a new one will be created.
6389 * @param {Object } data
6490 * @param {String } direction='vertical' 'horizontal', 'vertical' or 'both'
6591 * @param {String } fromId
66- * @param {String } id The owner id (e.g. component id)
92+ * @param {String } id A unique identifier for this registration.
6793 * @param {String } toId
6894 * @param {Boolean } twoWay=true Sync the target's scroll state back to the source node
6995 */
7096 register ( { direction= 'vertical' , fromId, id, toId, twoWay= true } ) {
71- let me = this ;
97+ let me = this ,
98+ registration ;
99+
100+ // Ensure idempotency: if the exact same registration already exists, do nothing.
101+ // If a registration with the same ID but different parameters exists, unregister the old one first.
102+ if ( me . registrations . has ( id ) ) {
103+ const oldReg = me . registrations . get ( id ) ;
104+ if ( oldReg . direction === direction && oldReg . fromId === fromId && oldReg . toId === toId && oldReg . twoWay === twoWay ) {
105+ return
106+ }
107+ me . unregister ( { id} )
108+ }
72109
73- me . addScrollListener ( fromId , toId , direction ) ;
110+ registration = {
111+ direction,
112+ fromId,
113+ id,
114+ toId,
115+ twoWay,
116+ listeners : [ ]
117+ } ;
118+
119+ registration . listeners . push ( {
120+ listener : me . addScrollListener ( fromId , toId , direction ) ,
121+ fromId
122+ } ) ;
74123
75124 if ( twoWay ) {
76- me . addScrollListener ( toId , fromId , direction )
125+ registration . listeners . push ( {
126+ listener : me . addScrollListener ( toId , fromId , direction ) ,
127+ fromId : toId
128+ } )
77129 }
130+
131+ me . registrations . set ( id , registration )
78132 }
79133
80134 /**
135+ * Removes a scroll synchronization registration.
136+ * This method is safe to call even if the DOM nodes have already been removed.
81137 * @param {Object } data
138+ * @param {String } data.id The unique identifier for the registration to remove.
82139 */
83- unregister ( data ) {
84- console . log ( 'unregister' , data )
140+ unregister ( { id} ) {
141+ let me = this ,
142+ registration = me . registrations . get ( id ) ;
143+
144+ if ( registration ) {
145+ // It is safe to call removeEventListener even if the element does not exist anymore.
146+ registration . listeners . forEach ( item => {
147+ DomAccess . getElement ( item . fromId ) ?. removeEventListener ( 'scroll' , item . listener )
148+ } ) ;
149+
150+ // Remove the registration from the map to prevent memory leaks.
151+ me . registrations . delete ( id ) ;
152+ }
85153 }
86154}
87155
0 commit comments