realtimecursor
Version:
Real-time collaboration system with cursor tracking and approval workflow
301 lines (255 loc) • 9.61 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RealtimeCursor SDK Test</title>
<!-- SDK styles will be included inline -->
<link rel="stylesheet" href="./sdk-dist/cursor.css">
<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;
}
</style>
</head>
<body>
<h1>RealtimeCursor SDK Test</h1>
<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 simple test 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>
<!-- Include socket.io first -->
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<!-- Use the browser-compatible version of the SDK -->
<script src="./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: 'https://api.realtimecursor.com',
socketUrl: 'https://api.realtimecursor.com',
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;
// 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 = '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, 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('.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';
}
}
});
}
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>