sveltekit-sync
Version:
Local-first sync engine for SvelteKit
102 lines (101 loc) • 2.97 kB
JavaScript
/**
* Track cursor positions for collaborative editing
*
* @param presence - The presence hook instance
* @param options - Configuration options
* @returns Cursor tracking API
*
* @example
* ```typescript
* const presence = usePresence(channel, currentUser);
* const cursorTracking = useCursorTracking(presence, {
* throttle: 50,
* container: canvasElement
* });
*
* cursorTracking.startTracking();
*
* // Access cursors map
* const cursors = cursorTracking.cursors;
* for (const [userId, { user, position }] of cursors) {
* renderCursor(user, position);
* }
* ```
*/
export function useCursorTracking(presence, options) {
const opts = {
throttle: 50,
container: typeof document !== 'undefined' ? document.body : null,
...options
};
const cursors = $state(new Map());
let isTracking = $state(false);
let throttleTimer = null;
let unsubscribeJoin = null;
let unsubscribeUpdate = null;
let unsubscribeLeave = null;
function updateCursorsMap() {
cursors.clear();
for (const state of presence.others) {
if (state.cursor) {
cursors.set(state.user.id, {
user: state.user,
position: state.cursor
});
}
}
}
function handleMouseMove(e) {
if (throttleTimer)
return;
const container = opts.container;
const rect = container?.getBoundingClientRect();
const position = {
x: rect ? e.clientX - rect.left : e.clientX,
y: rect ? e.clientY - rect.top : e.clientY
};
presence.updateCursor(position);
throttleTimer = window.setTimeout(() => {
throttleTimer = null;
}, opts.throttle);
}
function startTracking() {
if (isTracking)
return;
isTracking = true;
const container = opts.container;
if (container) {
container.addEventListener('mousemove', handleMouseMove);
}
// Listen for presence changes
unsubscribeJoin = presence.on('join', updateCursorsMap);
unsubscribeUpdate = presence.on('update', updateCursorsMap);
unsubscribeLeave = presence.on('leave', updateCursorsMap);
// Initial update
updateCursorsMap();
}
function stopTracking() {
if (!isTracking)
return;
isTracking = false;
const container = opts.container;
if (container) {
container.removeEventListener('mousemove', handleMouseMove);
}
if (throttleTimer) {
clearTimeout(throttleTimer);
throttleTimer = null;
}
unsubscribeJoin?.();
unsubscribeUpdate?.();
unsubscribeLeave?.();
cursors.clear();
}
return {
get cursors() {
return cursors;
},
startTracking,
stopTracking
};
}