realtimecursor
Version:
Real-time collaboration system with cursor tracking and approval workflow
229 lines • 10.7 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CollaborativeTextarea = exports.CollaboratorsList = exports.CursorOverlay = exports.TextCursor = exports.Cursor = void 0;
const react_1 = __importStar(require("react"));
/**
* Cursor component - displays a cursor at a specific position
*/
const Cursor = ({ x, y, user }) => {
return (react_1.default.createElement("div", { className: "realtimecursor-cursor", style: {
position: 'fixed',
left: x,
top: y,
transform: 'translate(-50%, -50%)',
zIndex: 9999,
pointerEvents: 'none'
} },
react_1.default.createElement("div", { className: "realtimecursor-pointer", style: {
width: '12px',
height: '12px',
borderRadius: '50%',
backgroundColor: user.color,
boxShadow: '0 0 5px rgba(0,0,0,0.3)'
} }),
react_1.default.createElement("div", { className: "realtimecursor-name", style: {
backgroundColor: user.color,
color: '#fff',
padding: '2px 6px',
borderRadius: '4px',
fontSize: '12px',
marginTop: '4px',
whiteSpace: 'nowrap'
} }, user.name)));
};
exports.Cursor = Cursor;
/**
* TextCursor component - displays a cursor at a specific text position
*/
const TextCursor = ({ textPosition, content, editorRef, user }) => {
const [position, setPosition] = (0, react_1.useState)({ left: 0, top: 0 });
(0, react_1.useEffect)(() => {
if (!(editorRef === null || editorRef === void 0 ? void 0 : editorRef.current) || textPosition === undefined)
return;
try {
const beforeCursor = content.substring(0, textPosition) || '';
const lines = beforeCursor.split('\\n');
const line = Math.max(0, lines.length - 1);
const char = (lines[line] || '').length;
// Calculate position based on line and character
const lineHeight = 24; // Approximate line height in pixels
const charWidth = 8.5; // Approximate character width in pixels
setPosition({
left: char * charWidth + 16,
top: line * lineHeight + 16
});
}
catch (error) {
console.warn('Error calculating cursor position:', error);
}
}, [textPosition, content, editorRef]);
return (react_1.default.createElement("div", { className: "realtimecursor-text-cursor", style: {
position: 'absolute',
left: position.left,
top: position.top,
zIndex: 9998,
pointerEvents: 'none'
} },
react_1.default.createElement("div", { className: "realtimecursor-caret", style: {
width: '2px',
height: '20px',
backgroundColor: user.color,
animation: 'realtimecursor-blink 1s infinite'
} }),
react_1.default.createElement("div", { className: "realtimecursor-flag", style: {
position: 'absolute',
top: '-18px',
left: '-8px',
backgroundColor: user.color,
color: '#fff',
width: '18px',
height: '18px',
borderRadius: '4px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '10px',
fontWeight: 'bold'
} }, user.name.charAt(0))));
};
exports.TextCursor = TextCursor;
/**
* CursorOverlay component - displays all cursors
*/
const CursorOverlay = ({ cursors, content, editorRef }) => {
return (react_1.default.createElement(react_1.default.Fragment, null, Object.entries(cursors).map(([socketId, cursor]) => (react_1.default.createElement(react_1.default.Fragment, { key: socketId },
cursor.x && cursor.y && (react_1.default.createElement(exports.Cursor, { x: cursor.x, y: cursor.y, user: cursor.user })),
cursor.textPosition !== undefined && (editorRef === null || editorRef === void 0 ? void 0 : editorRef.current) && content && (react_1.default.createElement(exports.TextCursor, { textPosition: cursor.textPosition, content: content, editorRef: editorRef, user: cursor.user })))))));
};
exports.CursorOverlay = CursorOverlay;
/**
* CollaboratorsList component - displays a list of collaborators
*/
const CollaboratorsList = ({ collaborators, typingUsers = [] }) => {
return (react_1.default.createElement("div", { className: "realtimecursor-collaborators" }, collaborators.map((collab) => (react_1.default.createElement("div", { key: collab.socketId, className: `realtimecursor-collaborator ${typingUsers.includes(collab.socketId) ? 'typing' : ''}`, style: {
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '4px 8px',
borderRadius: '4px',
backgroundColor: typingUsers.includes(collab.socketId) ? '#f0f9ff' : 'transparent'
} },
react_1.default.createElement("div", { className: "realtimecursor-avatar", style: {
width: '24px',
height: '24px',
borderRadius: '50%',
backgroundColor: collab.color || '#3b82f6',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
fontSize: '12px',
fontWeight: 'bold'
} }, (collab.name || 'U').charAt(0)),
react_1.default.createElement("span", { style: { fontSize: '14px' } }, collab.name),
typingUsers.includes(collab.socketId) && (react_1.default.createElement("div", { className: "realtimecursor-typing-indicator" },
react_1.default.createElement("span", { className: "realtimecursor-dot" }),
react_1.default.createElement("span", { className: "realtimecursor-dot" }),
react_1.default.createElement("span", { className: "realtimecursor-dot" }))))))));
};
exports.CollaboratorsList = CollaboratorsList;
/**
* CollaborativeTextarea component - a textarea with real-time collaboration
*/
const CollaborativeTextarea = ({ value, onChange, onCursorMove, onCursorPositionChange, onTypingStatusChange, className, style, placeholder }) => {
const textareaRef = react_1.default.useRef(null);
const [isTyping, setIsTyping] = (0, react_1.useState)(false);
const typingTimeoutRef = react_1.default.useRef(null);
const handleMouseMove = (e) => {
if (!textareaRef.current || !onCursorMove)
return;
const rect = textareaRef.current.getBoundingClientRect();
const relativeX = Math.max(0, e.clientX - rect.left);
const relativeY = Math.max(0, e.clientY - rect.top);
// Calculate text position
const textPosition = getTextPositionFromCoords(textareaRef.current, relativeX, relativeY);
onCursorMove({
x: e.clientX,
y: e.clientY,
textPosition
});
};
const handleKeyUp = (e) => {
if (!textareaRef.current || !onCursorPositionChange)
return;
const cursorPosition = textareaRef.current.selectionStart;
onCursorPositionChange(cursorPosition);
};
const handleChange = (e) => {
onChange(e.target.value);
// Handle typing indicator
if (!isTyping && onTypingStatusChange) {
setIsTyping(true);
onTypingStatusChange(true);
}
// Clear existing timeout
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
// Set new timeout to stop typing indicator
typingTimeoutRef.current = setTimeout(() => {
setIsTyping(false);
if (onTypingStatusChange) {
onTypingStatusChange(false);
}
}, 1000);
};
return (react_1.default.createElement("textarea", { ref: textareaRef, value: value, onChange: handleChange, onMouseMove: handleMouseMove, onKeyUp: handleKeyUp, onSelect: handleKeyUp, className: `realtimecursor-textarea ${className || ''}`, style: Object.assign({ width: '100%', minHeight: '200px', padding: '16px', fontFamily: 'monospace', fontSize: '16px', lineHeight: '1.5', border: '1px solid #e5e7eb', borderRadius: '8px', outline: 'none', resize: 'vertical' }, style), placeholder: placeholder }));
};
exports.CollaborativeTextarea = CollaborativeTextarea;
// Helper function to calculate text position from coordinates
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;
}
}
//# sourceMappingURL=CursorComponents.js.map