realtimecursor
Version:
Real-time collaboration system with cursor tracking and approval workflow
156 lines (133 loc) • 4.7 kB
JavaScript
// Browser-compatible version of the SDK
(function(global, factory) {
if (typeof exports === 'object' && typeof module !== 'undefined') {
// CommonJS
module.exports = factory();
} else {
// Browser globals
global.RealtimeCursor = factory().RealtimeCursor;
}
}(this, function() {
'use strict';
// Simple implementation of the RealtimeCursor class for the browser
class RealtimeCursor {
constructor(options) {
this.options = {
...options,
socketUrl: options.socketUrl || options.apiUrl
};
this.socket = null;
this.collaborators = [];
this.cursors = {};
this.typingUsers = new Set();
}
connect() {
if (this.socket) {
this.disconnect();
}
// Connect to socket server using socket.io
this.socket = io(this.options.socketUrl);
// Join project room
this.socket.emit('join-project', {
projectId: this.options.projectId,
user: {
id: this.options.user.id,
name: this.options.user.name,
color: this.options.user.color || this.getRandomColor()
}
});
// Set up event listeners
this.setupEventListeners();
}
disconnect() {
if (this.socket) {
this.socket.disconnect();
this.socket = null;
this.collaborators = [];
this.cursors = {};
this.typingUsers.clear();
}
}
updateCursor(position) {
if (!this.socket) throw new Error('Socket not connected');
this.socket.emit('cursor-move', position);
}
updateCursorPosition(textPosition) {
if (!this.socket) throw new Error('Socket not connected');
this.socket.emit('cursor-position', { textPosition });
}
updateContent(content, cursorPosition) {
if (!this.socket) throw new Error('Socket not connected');
this.socket.emit('content-change', { content, cursorPosition });
}
setTyping(isTyping) {
if (!this.socket) throw new Error('Socket not connected');
this.socket.emit('user-typing', { isTyping });
}
setupEventListeners() {
if (!this.socket) return;
this.socket.on('room-users', (users) => {
this.collaborators = users;
if (this.onCollaboratorsChange) this.onCollaboratorsChange(users);
});
this.socket.on('user-joined', ({ user }) => {
this.collaborators.push(user);
if (this.onCollaboratorsChange) this.onCollaboratorsChange(this.collaborators);
if (this.onUserJoined) this.onUserJoined(user);
});
this.socket.on('user-left', ({ socketId }) => {
this.collaborators = this.collaborators.filter(u => u.socketId !== socketId);
delete this.cursors[socketId];
this.typingUsers.delete(socketId);
if (this.onCollaboratorsChange) this.onCollaboratorsChange(this.collaborators);
if (this.onUserLeft) this.onUserLeft({ socketId });
});
this.socket.on('content-update', (data) => {
if (this.onContentUpdate) this.onContentUpdate(data);
});
this.socket.on('cursor-update', (data) => {
const { socketId, user, x, y, textPosition } = data;
this.cursors[socketId] = { x, y, user, textPosition };
if (this.onCursorUpdate) this.onCursorUpdate(data);
});
this.socket.on('user-typing', ({ socketId, isTyping }) => {
if (isTyping) {
this.typingUsers.add(socketId);
} else {
this.typingUsers.delete(socketId);
}
if (this.onTypingStatusChange) this.onTypingStatusChange(Array.from(this.typingUsers));
});
this.socket.on('cursor-position', (data) => {
const { socketId, textPosition, user } = data;
if (this.cursors[socketId]) {
this.cursors[socketId].textPosition = textPosition;
this.cursors[socketId].user = user;
} else {
this.cursors[socketId] = { textPosition, user };
}
if (this.onCursorPositionUpdate) this.onCursorPositionUpdate(data);
});
}
getRandomColor() {
const colors = [
'#3b82f6', '#ef4444', '#10b981', '#f59e0b',
'#8b5cf6', '#06b6d4', '#f97316', '#84cc16',
'#ec4899', '#6366f1', '#14b8a6', '#f43f5e'
];
return colors[Math.floor(Math.random() * colors.length)];
}
getCollaborators() {
return this.collaborators;
}
getCursors() {
return this.cursors;
}
getTypingUsers() {
return Array.from(this.typingUsers);
}
}
return {
RealtimeCursor: RealtimeCursor
};
}));