UNPKG

realtimecursor

Version:

Real-time collaboration system with cursor tracking and approval workflow

444 lines (380 loc) 14.1 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>RealtimeCursor Test</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } .editor-container { position: relative; margin-top: 20px; } textarea { width: 100%; height: 300px; padding: 16px; font-family: monospace; font-size: 16px; line-height: 1.5; border: 1px solid #ccc; border-radius: 8px; } .collaborators { display: flex; gap: 10px; margin-bottom: 20px; } .collaborator { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background-color: #f0f9ff; border-radius: 20px; } .avatar { width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; } .debug { margin-top: 20px; padding: 10px; background-color: #f8f9fa; border-radius: 8px; font-family: monospace; white-space: pre-wrap; max-height: 200px; overflow: auto; } </style> </head> <body> <h1>RealtimeCursor Test</h1> <p>This page tests the fixed version of the RealtimeCursor SDK.</p> <div class="controls"> <button id="connect">Connect</button> <button id="disconnect">Disconnect</button> <span id="status">Disconnected</span> </div> <div class="collaborators" id="collaborators"></div> <div class="editor-container"> <textarea id="editor" placeholder="Start typing to collaborate...">Welcome to RealtimeCursor SDK! This is a test of the fixed version of the SDK. Try opening this page in multiple browser windows to see the cursors of other users. Features: - Real-time cursor tracking - Collaborative editing - User presence indicators - Typing indicators</textarea> <div id="cursors-overlay"></div> </div> <div class="debug" id="debug">Debug info will appear here...</div> <!-- Include socket.io --> <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script> <script> // Debug logger const debugEl = document.getElementById('debug'); function log(message) { const timestamp = new Date().toISOString().substring(11, 19); debugEl.textContent = `[${timestamp}] ${message}\n` + debugEl.textContent; } // RealtimeCursor implementation (fixed version) class RealtimeCursor { constructor(options) { this.options = { ...options, socketUrl: options.socketUrl || options.apiUrl }; this.socket = null; this.collaborators = []; this.cursors = {}; this.typingUsers = new Set(); this.connected = false; this.projectJoined = false; log(`RealtimeCursor initialized with projectId: ${this.options.projectId}`); } connect() { if (this.socket) { this.disconnect(); } log(`Connecting to ${this.options.socketUrl}...`); // Connect to socket server this.socket = io(this.options.socketUrl, { query: { projectId: this.options.projectId, userId: this.options.user.id }, auth: { apiKey: this.options.apiKey }, transports: ['websocket', 'polling'], reconnectionAttempts: 5, reconnectionDelay: 1000 }); // Set up event listeners this.setupEventListeners(); this.connected = true; // Join project room this.joinProject(); } joinProject() { if (!this.socket || this.projectJoined) return; log(`Joining project: ${this.options.projectId}`); 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() } }); this.projectJoined = true; } disconnect() { if (this.socket) { log('Disconnecting...'); this.socket.disconnect(); this.socket = null; this.collaborators = []; this.cursors = {}; this.typingUsers.clear(); this.connected = false; this.projectJoined = false; } } updateCursor(position) { if (!this.socket || !this.connected) return; this.socket.emit('cursor-move', position); } updateContent(content, cursorPosition) { if (!this.socket || !this.connected) return; this.socket.emit('content-change', { content, cursorPosition }); } setTyping(isTyping) { if (!this.socket || !this.connected) return; this.socket.emit('user-typing', { isTyping }); } setupEventListeners() { if (!this.socket) return; this.socket.on('connect', () => { log('Connected to server'); if (!this.projectJoined) { this.joinProject(); } }); this.socket.on('disconnect', () => { log('Disconnected from server'); this.projectJoined = false; }); this.socket.on('room-users', (users) => { log(`Received room-users event with ${users.length} users`); this.collaborators = users; if (this.onCollaboratorsChange) this.onCollaboratorsChange(users); }); this.socket.on('user-joined', ({ user }) => { log(`User joined: ${user.name}`); // Check if user already exists const existingUser = this.collaborators.find(u => u.id === user.id); if (!existingUser) { this.collaborators.push(user); if (this.onCollaboratorsChange) this.onCollaboratorsChange(this.collaborators); if (this.onUserJoined) this.onUserJoined(user); } }); this.socket.on('user-left', ({ socketId }) => { log(`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) => { log(`Content update from: ${data.user.name}`); if (this.onContentUpdate) this.onContentUpdate(data); }); this.socket.on('cursor-update', (data) => { const { socketId, user } = data; this.cursors[socketId] = data; 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)); }); } getRandomColor() { const colors = [ '#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#06b6d4', '#f97316', '#84cc16', '#ec4899', '#6366f1', '#14b8a6', '#f43f5e' ]; return colors[Math.floor(Math.random() * colors.length)]; } } // Generate a random user ID and name const userId = 'user_' + Math.random().toString(36).substring(2, 10); const userName = 'User ' + Math.floor(Math.random() * 1000); // Create a color for the user const colors = [ '#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#06b6d4', '#f97316', '#84cc16', '#ec4899', '#6366f1', '#14b8a6', '#f43f5e' ]; const userColor = colors[Math.floor(Math.random() * colors.length)]; // Initialize the cursor client const cursorClient = new RealtimeCursor({ apiUrl: 'http://localhost:3001', socketUrl: 'http://localhost:3001', projectId: 'test-project', user: { id: userId, name: userName, color: userColor } }); // DOM elements const editor = document.getElementById('editor'); const connectButton = document.getElementById('connect'); const disconnectButton = document.getElementById('disconnect'); const statusElement = document.getElementById('status'); const collaboratorsElement = document.getElementById('collaborators'); const cursorsOverlay = document.getElementById('cursors-overlay'); // Connect button connectButton.addEventListener('click', () => { cursorClient.connect(); statusElement.textContent = 'Connected'; statusElement.style.color = '#10b981'; }); // Disconnect button disconnectButton.addEventListener('click', () => { cursorClient.disconnect(); statusElement.textContent = 'Disconnected'; statusElement.style.color = '#ef4444'; collaboratorsElement.innerHTML = ''; cursorsOverlay.innerHTML = ''; }); // Update cursor position on mouse move editor.addEventListener('mousemove', (e) => { const rect = editor.getBoundingClientRect(); const relativeX = e.clientX - rect.left; const relativeY = e.clientY - rect.top; cursorClient.updateCursor({ x: e.clientX, y: e.clientY }); }); // Update content on change editor.addEventListener('input', (e) => { cursorClient.updateContent(e.target.value); }); // Set typing indicator let typingTimeout; editor.addEventListener('keydown', () => { cursorClient.setTyping(true); clearTimeout(typingTimeout); typingTimeout = setTimeout(() => { cursorClient.setTyping(false); }, 1000); }); // Event handlers cursorClient.onCollaboratorsChange = (collaborators) => { updateCollaboratorsList(collaborators); }; cursorClient.onCursorUpdate = (data) => { updateCursor(data); }; cursorClient.onContentUpdate = (data) => { if (data.user.id !== userId) { editor.value = data.content; } }; cursorClient.onTypingStatusChange = (typingUserIds) => { updateTypingIndicators(typingUserIds); }; // Helper functions function updateCollaboratorsList(collaborators) { collaboratorsElement.innerHTML = ''; collaborators.forEach(collab => { if (collab.id === userId) return; // Skip current user const collaboratorElement = document.createElement('div'); collaboratorElement.className = 'collaborator'; collaboratorElement.dataset.socketId = collab.socketId; const avatarElement = document.createElement('div'); avatarElement.className = 'avatar'; avatarElement.style.backgroundColor = collab.color || '#3b82f6'; avatarElement.textContent = collab.name.charAt(0); const nameElement = document.createElement('span'); nameElement.textContent = collab.name; const typingIndicator = document.createElement('div'); typingIndicator.className = 'typing-indicator'; typingIndicator.style.display = 'none'; typingIndicator.innerHTML = '...'; collaboratorElement.appendChild(avatarElement); collaboratorElement.appendChild(nameElement); collaboratorElement.appendChild(typingIndicator); collaboratorsElement.appendChild(collaboratorElement); }); } function updateCursor(data) { const { socketId, user, x, y } = data; // Skip current user if (user.id === userId) return; // Find or create cursor element let cursorElement = document.getElementById(`cursor-${socketId}`); if (!cursorElement) { cursorElement = document.createElement('div'); cursorElement.id = `cursor-${socketId}`; cursorElement.style.position = 'fixed'; cursorElement.style.zIndex = '9999'; cursorElement.style.pointerEvents = 'none'; cursorElement.innerHTML = ` <div style="width: 12px; height: 12px; border-radius: 50%; background-color: ${user.color}; box-shadow: 0 0 5px rgba(0,0,0,0.3);"></div> <div style="padding: 2px 6px; border-radius: 4px; background-color: ${user.color}; color: #fff; font-size: 12px; margin-top: 4px; white-space: nowrap;">${user.name}</div> `; document.body.appendChild(cursorElement); } // Update cursor position if (x && y) { cursorElement.style.left = `${x}px`; cursorElement.style.top = `${y}px`; cursorElement.style.transform = 'translate(-50%, -50%)'; } } function updateTypingIndicators(typingUserIds) { // Reset all typing indicators document.querySelectorAll('.collaborator .typing-indicator').forEach(el => { el.style.display = 'none'; }); // Show typing indicators for users who are typing typingUserIds.forEach(socketId => { const collaborator = document.querySelector(`.collaborator[data-socket-id="${socketId}"]`); if (collaborator) { const typingIndicator = collaborator.querySelector('.typing-indicator'); if (typingIndicator) { typingIndicator.style.display = 'inline'; } } }); } // Auto-connect on page load connectButton.click(); </script> </body> </html>