Skip to content

Commit 2725361

Browse files
committed
Implement unregister in ScrollSync addon #7682
1 parent 3ad5472 commit 2725361

1 file changed

Lines changed: 76 additions & 8 deletions

File tree

src/main/addon/ScrollSync.mjs

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,24 @@ import Base from './Base.mjs';
22
import 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
*/
924
class 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

Comments
 (0)