UNPKG

realtimecursor

Version:

Real-time collaboration system with cursor tracking and approval workflow

394 lines (337 loc) 12.9 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>RealtimeCursor SDK Demo</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="../test-app/sdk-dist/cursor.css"> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; padding: 20px; } .header { text-align: center; margin-bottom: 40px; } .demo-container { max-width: 1000px; margin: 0 auto; } .editor-container { position: relative; margin-top: 20px; border: 1px solid #e5e7eb; border-radius: 8px; padding: 20px; } .collaborators { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; } .code-block { background-color: #f8f9fa; padding: 15px; border-radius: 8px; margin: 20px 0; overflow-x: auto; } .features { margin: 40px 0; } .feature-item { margin-bottom: 15px; } </style> </head> <body> <div class="demo-container"> <div class="header"> <h1>RealtimeCursor SDK</h1> <p class="lead">A powerful real-time collaboration SDK with live cursor tracking</p> <div class="d-flex justify-content-center gap-2"> <a href="https://github.com/sourabhpunase/realtimecursor" class="btn btn-primary">GitHub</a> <a href="https://www.npmjs.com/package/realtimecursor-sdk" class="btn btn-secondary">npm</a> </div> </div> <div class="row"> <div class="col-md-6"> <h2>Live Demo</h2> <p>Open this page in multiple browser windows to see the real-time collaboration in action.</p> <div class="controls mb-3"> <button id="connect" class="btn btn-success">Connect</button> <button id="disconnect" class="btn btn-danger">Disconnect</button> <span id="status" class="ms-2">Disconnected</span> </div> <div class="collaborators" id="collaborators"></div> <div class="editor-container"> <textarea id="editor" class="form-control" style="min-height: 200px;">Welcome to RealtimeCursor SDK! This is a simple demo application to demonstrate the real-time cursor tracking functionality. 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> <div class="col-md-6"> <h2>Installation</h2> <h4>npm</h4> <div class="code-block"> <code>npm install realtimecursor-sdk</code> </div> <h4>CDN</h4> <div class="code-block"> <code>&lt;script src="https://cdn.jsdelivr.net/npm/realtimecursor-sdk@1.1.0/dist-cdn/realtimecursor.min.js"&gt;&lt;/script&gt;<br> &lt;link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/realtimecursor-sdk@1.1.0/dist-cdn/realtimecursor.min.css"&gt;</code> </div> <h2>Usage</h2> <div class="code-block"> <pre><code>// Initialize the cursor client const cursorClient = new RealtimeCursor({ apiUrl: 'https://your-api-url.com', projectId: 'your-project-id', user: { id: 'user-123', name: 'John Doe' } }); // Connect to the real-time service cursorClient.connect(); // Update cursor position on mouse move editor.addEventListener('mousemove', (e) => { cursorClient.updateCursor({ x: e.clientX, y: e.clientY }); }); // Update content on change editor.addEventListener('input', (e) => { cursorClient.updateContent(e.target.value); });</code></pre> </div> </div> </div> <div class="features"> <h2>Features</h2> <div class="row"> <div class="col-md-6"> <div class="feature-item"> <h4>Real-time Cursor Tracking</h4> <p>See other users' cursors in real-time as they move around the document.</p> </div> <div class="feature-item"> <h4>Collaborative Editing</h4> <p>Edit documents together with multiple users simultaneously.</p> </div> </div> <div class="col-md-6"> <div class="feature-item"> <h4>User Presence</h4> <p>See who's currently viewing and editing the document.</p> </div> <div class="feature-item"> <h4>Typing Indicators</h4> <p>Know when other users are typing with real-time indicators.</p> </div> </div> </div> </div> <footer class="text-center mt-5 pt-3 border-top"> <p>RealtimeCursor SDK &copy; 2023</p> </footer> </div> <!-- Include socket.io --> <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script> <!-- Include the RealtimeCursor SDK --> <script src="../test-app/sdk-dist/realtimecursor-browser.js"></script> <script> // 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: 'demo-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; // Calculate text position const textPosition = getTextPositionFromCoords(editor, relativeX, relativeY); cursorClient.updateCursor({ x: e.clientX, y: e.clientY, textPosition }); }); // 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 = 'badge bg-light text-dark p-2'; collaboratorElement.dataset.socketId = collab.socketId; const avatarElement = document.createElement('span'); avatarElement.className = 'me-1'; avatarElement.style.display = 'inline-block'; avatarElement.style.width = '20px'; avatarElement.style.height = '20px'; avatarElement.style.borderRadius = '50%'; avatarElement.style.backgroundColor = collab.color || '#3b82f6'; avatarElement.style.textAlign = 'center'; avatarElement.style.color = '#fff'; avatarElement.style.fontWeight = 'bold'; avatarElement.textContent = collab.name.charAt(0); const nameElement = document.createElement('span'); nameElement.textContent = collab.name; const typingIndicator = document.createElement('span'); typingIndicator.className = 'typing-indicator ms-1'; 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, textPosition } = 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.className = 'realtimecursor-cursor'; cursorElement.innerHTML = ` <div class="realtimecursor-pointer" style="background-color: ${user.color};"></div> <div class="realtimecursor-name" style="background-color: ${user.color};">${user.name}</div> `; cursorsOverlay.appendChild(cursorElement); } // Update cursor position if (x && y) { cursorElement.style.position = 'fixed'; cursorElement.style.left = `${x}px`; cursorElement.style.top = `${y}px`; cursorElement.style.transform = 'translate(-50%, -50%)'; cursorElement.style.zIndex = '9999'; cursorElement.style.pointerEvents = 'none'; } } function updateTypingIndicators(typingUserIds) { // Reset all typing indicators document.querySelectorAll('.badge .typing-indicator').forEach(el => { el.style.display = 'none'; }); // Show typing indicators for users who are typing typingUserIds.forEach(socketId => { const collaborator = document.querySelector(`.badge[data-socket-id="${socketId}"]`); if (collaborator) { const typingIndicator = collaborator.querySelector('.typing-indicator'); if (typingIndicator) { typingIndicator.style.display = 'inline'; } } }); } function getTextPositionFromCoords(element, x, y) { const content = element.value; if (!content) return 0; try { const lineHeight = 24; // Approximate line height in pixels const charWidth = 8.5; // Approximate character width in pixels const line = Math.max(0, Math.floor(y / lineHeight)); const char = Math.max(0, Math.floor(x / charWidth)); const lines = content.split('\n'); if (lines.length === 0) return 0; let position = 0; for (let i = 0; i < line && i < lines.length; i++) { position += (lines[i] || '').length + 1; } if (line < lines.length && lines[line]) { position += Math.min(char, lines[line].length); } return Math.max(0, Math.min(position, content.length)); } catch (error) { console.warn('Error calculating text position:', error); return 0; } } // Auto-connect on page load connectButton.click(); </script> </body> </html>