UNPKG

realtimecursor

Version:

Real-time collaboration system with cursor tracking and approval workflow

229 lines 10.7 kB
"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