Merge branch 'develop' into ui-fix
This commit is contained in:
74
src/components/atoms/DatePicker.tsx
Normal file
74
src/components/atoms/DatePicker.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { Coordinates } from './canvas';
|
||||
|
||||
interface DatePickerProps {
|
||||
coordinates: Coordinates;
|
||||
selector: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DatePicker: React.FC<DatePickerProps> = ({ coordinates, selector, onClose }) => {
|
||||
const { socket } = useSocketStore();
|
||||
const [selectedDate, setSelectedDate] = useState<string>('');
|
||||
|
||||
const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSelectedDate(e.target.value);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (socket && selectedDate) {
|
||||
socket.emit('input:date', {
|
||||
selector,
|
||||
value: selectedDate
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${coordinates.x}px`,
|
||||
top: `${coordinates.y}px`,
|
||||
zIndex: 1000,
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
|
||||
padding: '10px',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<input
|
||||
type="date"
|
||||
onChange={handleDateChange}
|
||||
value={selectedDate}
|
||||
className="p-2 border rounded"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-3 py-1 text-sm text-gray-600 hover:text-gray-800 border rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirm}
|
||||
disabled={!selectedDate}
|
||||
className={`px-3 py-1 text-sm rounded ${
|
||||
selectedDate
|
||||
? 'bg-blue-500 text-white hover:bg-blue-600'
|
||||
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DatePicker;
|
||||
74
src/components/atoms/DateTimeLocalPicker.tsx
Normal file
74
src/components/atoms/DateTimeLocalPicker.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { Coordinates } from './canvas';
|
||||
|
||||
interface DateTimeLocalPickerProps {
|
||||
coordinates: Coordinates;
|
||||
selector: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DateTimeLocalPicker: React.FC<DateTimeLocalPickerProps> = ({ coordinates, selector, onClose }) => {
|
||||
const { socket } = useSocketStore();
|
||||
const [selectedDateTime, setSelectedDateTime] = useState<string>('');
|
||||
|
||||
const handleDateTimeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSelectedDateTime(e.target.value);
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (socket && selectedDateTime) {
|
||||
socket.emit('input:datetime-local', {
|
||||
selector,
|
||||
value: selectedDateTime
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${coordinates.x}px`,
|
||||
top: `${coordinates.y}px`,
|
||||
zIndex: 1000,
|
||||
backgroundColor: 'white',
|
||||
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
|
||||
padding: '10px',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col space-y-2">
|
||||
<input
|
||||
type="datetime-local"
|
||||
onChange={handleDateTimeChange}
|
||||
value={selectedDateTime}
|
||||
className="p-2 border rounded"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-3 py-1 text-sm text-gray-600 hover:text-gray-800 border rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleConfirm}
|
||||
disabled={!selectedDateTime}
|
||||
className={`px-3 py-1 text-sm rounded ${
|
||||
selectedDateTime
|
||||
? 'bg-blue-500 text-white hover:bg-blue-600'
|
||||
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateTimeLocalPicker;
|
||||
85
src/components/atoms/Dropdown.tsx
Normal file
85
src/components/atoms/Dropdown.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { Coordinates } from './canvas';
|
||||
|
||||
interface DropdownProps {
|
||||
coordinates: Coordinates;
|
||||
selector: string;
|
||||
options: Array<{
|
||||
value: string;
|
||||
text: string;
|
||||
disabled: boolean;
|
||||
selected: boolean;
|
||||
}>;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const Dropdown = ({ coordinates, selector, options, onClose }: DropdownProps) => {
|
||||
const { socket } = useSocketStore();
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
|
||||
const handleSelect = (value: string) => {
|
||||
if (socket) {
|
||||
socket.emit('input:dropdown', { selector, value });
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
const containerStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
left: coordinates.x,
|
||||
top: coordinates.y,
|
||||
zIndex: 1000,
|
||||
width: '200px',
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid rgb(169, 169, 169)',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.15)',
|
||||
};
|
||||
|
||||
const scrollContainerStyle: React.CSSProperties = {
|
||||
maxHeight: '180px',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
};
|
||||
|
||||
const getOptionStyle = (option: any, index: number): React.CSSProperties => ({
|
||||
fontSize: '13.333px',
|
||||
lineHeight: '18px',
|
||||
padding: '0 3px',
|
||||
cursor: option.disabled ? 'default' : 'default',
|
||||
backgroundColor: hoveredIndex === index ? '#0078D7' :
|
||||
option.selected ? '#0078D7' :
|
||||
option.disabled ? '#f8f8f8' : 'white',
|
||||
color: (hoveredIndex === index || option.selected) ? 'white' :
|
||||
option.disabled ? '#a0a0a0' : 'black',
|
||||
userSelect: 'none',
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
style={containerStyle}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div style={scrollContainerStyle}>
|
||||
{options.map((option, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={getOptionStyle(option, index)}
|
||||
onMouseEnter={() => !option.disabled && setHoveredIndex(index)}
|
||||
onMouseLeave={() => setHoveredIndex(null)}
|
||||
onClick={() => !option.disabled && handleSelect(option.value)}
|
||||
>
|
||||
{option.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dropdown;
|
||||
130
src/components/atoms/TimePicker.tsx
Normal file
130
src/components/atoms/TimePicker.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { Coordinates } from './canvas';
|
||||
|
||||
interface TimePickerProps {
|
||||
coordinates: Coordinates;
|
||||
selector: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const TimePicker = ({ coordinates, selector, onClose }: TimePickerProps) => {
|
||||
const { socket } = useSocketStore();
|
||||
const [hoveredHour, setHoveredHour] = useState<number | null>(null);
|
||||
const [hoveredMinute, setHoveredMinute] = useState<number | null>(null);
|
||||
const [selectedHour, setSelectedHour] = useState<number | null>(null);
|
||||
const [selectedMinute, setSelectedMinute] = useState<number | null>(null);
|
||||
|
||||
const handleHourSelect = (hour: number) => {
|
||||
setSelectedHour(hour);
|
||||
// If minute is already selected, complete the selection
|
||||
if (selectedMinute !== null) {
|
||||
const formattedHour = hour.toString().padStart(2, '0');
|
||||
const formattedMinute = selectedMinute.toString().padStart(2, '0');
|
||||
if (socket) {
|
||||
socket.emit('input:time', {
|
||||
selector,
|
||||
value: `${formattedHour}:${formattedMinute}`
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleMinuteSelect = (minute: number) => {
|
||||
setSelectedMinute(minute);
|
||||
// If hour is already selected, complete the selection
|
||||
if (selectedHour !== null) {
|
||||
const formattedHour = selectedHour.toString().padStart(2, '0');
|
||||
const formattedMinute = minute.toString().padStart(2, '0');
|
||||
if (socket) {
|
||||
socket.emit('input:time', {
|
||||
selector,
|
||||
value: `${formattedHour}:${formattedMinute}`
|
||||
});
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const containerStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
left: coordinates.x,
|
||||
top: coordinates.y,
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid rgb(169, 169, 169)',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.15)',
|
||||
};
|
||||
|
||||
const columnStyle: React.CSSProperties = {
|
||||
width: '60px',
|
||||
maxHeight: '180px',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
borderRight: '1px solid rgb(169, 169, 169)',
|
||||
};
|
||||
|
||||
const getOptionStyle = (value: number, isHour: boolean): React.CSSProperties => {
|
||||
const isHovered = isHour ? hoveredHour === value : hoveredMinute === value;
|
||||
const isSelected = isHour ? selectedHour === value : selectedMinute === value;
|
||||
|
||||
return {
|
||||
fontSize: '13.333px',
|
||||
lineHeight: '18px',
|
||||
padding: '0 3px',
|
||||
cursor: 'default',
|
||||
backgroundColor: isSelected ? '#0078D7' : isHovered ? '#0078D7' : 'white',
|
||||
color: (isSelected || isHovered) ? 'white' : 'black',
|
||||
userSelect: 'none',
|
||||
};
|
||||
};
|
||||
|
||||
const hours = Array.from({ length: 24 }, (_, i) => i);
|
||||
const minutes = Array.from({ length: 60 }, (_, i) => i);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
style={containerStyle}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{/* Hours column */}
|
||||
<div style={columnStyle}>
|
||||
{hours.map((hour) => (
|
||||
<div
|
||||
key={hour}
|
||||
style={getOptionStyle(hour, true)}
|
||||
onMouseEnter={() => setHoveredHour(hour)}
|
||||
onMouseLeave={() => setHoveredHour(null)}
|
||||
onClick={() => handleHourSelect(hour)}
|
||||
>
|
||||
{hour.toString().padStart(2, '0')}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Minutes column */}
|
||||
<div style={{...columnStyle, borderRight: 'none'}}>
|
||||
{minutes.map((minute) => (
|
||||
<div
|
||||
key={minute}
|
||||
style={getOptionStyle(minute, false)}
|
||||
onMouseEnter={() => setHoveredMinute(minute)}
|
||||
onMouseLeave={() => setHoveredMinute(null)}
|
||||
onClick={() => handleMinuteSelect(minute)}
|
||||
>
|
||||
{minute.toString().padStart(2, '0')}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimePicker;
|
||||
@@ -1,17 +1,147 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useMemo, Suspense } from 'react';
|
||||
import { useSocketStore } from '../../context/socket';
|
||||
import { getMappedCoordinates } from "../../helpers/inputHelpers";
|
||||
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||
import { useActionContext } from '../../context/browserActions';
|
||||
const DatePicker = React.lazy(() => import('./DatePicker'));
|
||||
const Dropdown = React.lazy(() => import('./Dropdown'));
|
||||
const TimePicker = React.lazy(() => import('./TimePicker'));
|
||||
const DateTimeLocalPicker = React.lazy(() => import('./DateTimeLocalPicker'));
|
||||
|
||||
interface CreateRefCallback {
|
||||
(ref: React.RefObject<HTMLCanvasElement>): void;
|
||||
class RAFScheduler {
|
||||
private queue: Set<() => void> = new Set();
|
||||
private isProcessing: boolean = false;
|
||||
private frameId: number | null = null;
|
||||
|
||||
schedule(callback: () => void): void {
|
||||
this.queue.add(callback);
|
||||
if (!this.isProcessing) {
|
||||
this.process();
|
||||
}
|
||||
}
|
||||
|
||||
private process = (): void => {
|
||||
this.isProcessing = true;
|
||||
this.frameId = requestAnimationFrame(() => {
|
||||
const callbacks = Array.from(this.queue);
|
||||
this.queue.clear();
|
||||
|
||||
callbacks.forEach(callback => {
|
||||
try {
|
||||
callback();
|
||||
} catch (error) {
|
||||
console.error('RAF Scheduler error:', error);
|
||||
}
|
||||
});
|
||||
|
||||
this.isProcessing = false;
|
||||
this.frameId = null;
|
||||
|
||||
if (this.queue.size > 0) {
|
||||
this.process();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.queue.clear();
|
||||
if (this.frameId !== null) {
|
||||
cancelAnimationFrame(this.frameId);
|
||||
this.frameId = null;
|
||||
}
|
||||
this.isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
class EventDebouncer {
|
||||
private highPriorityQueue: Array<() => void> = [];
|
||||
private lowPriorityQueue: Array<() => void> = [];
|
||||
private processing: boolean = false;
|
||||
private scheduler: RAFScheduler;
|
||||
|
||||
constructor(scheduler: RAFScheduler) {
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
add(callback: () => void, highPriority: boolean = false): void {
|
||||
if (highPriority) {
|
||||
this.highPriorityQueue.push(callback);
|
||||
} else {
|
||||
this.lowPriorityQueue.push(callback);
|
||||
}
|
||||
|
||||
if (!this.processing) {
|
||||
this.process();
|
||||
}
|
||||
}
|
||||
|
||||
private process(): void {
|
||||
this.processing = true;
|
||||
this.scheduler.schedule(() => {
|
||||
while (this.highPriorityQueue.length > 0) {
|
||||
const callback = this.highPriorityQueue.shift();
|
||||
callback?.();
|
||||
}
|
||||
|
||||
if (this.lowPriorityQueue.length > 0) {
|
||||
const callback = this.lowPriorityQueue.shift();
|
||||
callback?.();
|
||||
|
||||
if (this.lowPriorityQueue.length > 0) {
|
||||
this.process();
|
||||
}
|
||||
}
|
||||
|
||||
this.processing = false;
|
||||
});
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.highPriorityQueue = [];
|
||||
this.lowPriorityQueue = [];
|
||||
this.processing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Optimized measurement cache with LRU
|
||||
class MeasurementCache {
|
||||
private cache: Map<HTMLElement, DOMRect>;
|
||||
private maxSize: number;
|
||||
|
||||
constructor(maxSize: number = 100) {
|
||||
this.cache = new Map();
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
get(element: HTMLElement): DOMRect | undefined {
|
||||
const cached = this.cache.get(element);
|
||||
if (cached) {
|
||||
// Refresh the entry
|
||||
this.cache.delete(element);
|
||||
this.cache.set(element, cached);
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
set(element: HTMLElement, rect: DOMRect): void {
|
||||
if (this.cache.size >= this.maxSize) {
|
||||
// Remove oldest entry
|
||||
const firstKey = this.cache.keys().next().value;
|
||||
if (firstKey !== undefined) {
|
||||
this.cache.delete(firstKey);
|
||||
}
|
||||
}
|
||||
this.cache.set(element, rect);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
interface CanvasProps {
|
||||
width: number;
|
||||
height: number;
|
||||
onCreateRef: CreateRefCallback;
|
||||
onCreateRef: (ref: React.RefObject<HTMLCanvasElement>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,134 +152,229 @@ export interface Coordinates {
|
||||
y: number;
|
||||
};
|
||||
|
||||
const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
|
||||
|
||||
const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const { socket } = useSocketStore();
|
||||
const { setLastAction, lastAction } = useGlobalInfoStore();
|
||||
const { getText, getList } = useActionContext();
|
||||
const getTextRef = useRef(getText);
|
||||
const getListRef = useRef(getList);
|
||||
|
||||
const notifyLastAction = (action: string) => {
|
||||
if (lastAction !== action) {
|
||||
setLastAction(action);
|
||||
const scheduler = useRef(new RAFScheduler());
|
||||
const debouncer = useRef(new EventDebouncer(scheduler.current));
|
||||
const measurementCache = useRef(new MeasurementCache(50));
|
||||
//const performanceMonitor = useRef(new FrontendPerformanceMonitor());
|
||||
|
||||
const refs = useRef({
|
||||
getText,
|
||||
getList,
|
||||
lastMousePosition: { x: 0, y: 0 },
|
||||
lastFrameTime: 0,
|
||||
context: null as CanvasRenderingContext2D | null,
|
||||
});
|
||||
|
||||
const [state, dispatch] = React.useReducer((state: any, action: any) => {
|
||||
switch (action.type) {
|
||||
case 'BATCH_UPDATE':
|
||||
return { ...state, ...action.payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
}, {
|
||||
datePickerInfo: null,
|
||||
dropdownInfo: null,
|
||||
timePickerInfo: null,
|
||||
dateTimeLocalInfo: null
|
||||
});
|
||||
|
||||
const lastMousePosition = useRef<Coordinates>({ x: 0, y: 0 });
|
||||
const getEventCoordinates = useCallback((event: MouseEvent): { x: number; y: number } => {
|
||||
if (!canvasRef.current) return { x: 0, y: 0 };
|
||||
|
||||
useEffect(() => {
|
||||
getTextRef.current = getText;
|
||||
getListRef.current = getList;
|
||||
}, [getText, getList]);
|
||||
let rect = measurementCache.current.get(canvasRef.current);
|
||||
if (!rect) {
|
||||
rect = canvasRef.current.getBoundingClientRect();
|
||||
measurementCache.current.set(canvasRef.current, rect);
|
||||
}
|
||||
|
||||
const onMouseEvent = useCallback((event: MouseEvent) => {
|
||||
if (socket && canvasRef.current) {
|
||||
// Get the canvas bounding rectangle
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const clickCoordinates = {
|
||||
x: event.clientX - rect.left, // Use relative x coordinate
|
||||
y: event.clientY - rect.top, // Use relative y coordinate
|
||||
};
|
||||
return {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top
|
||||
};
|
||||
}, []);
|
||||
|
||||
switch (event.type) {
|
||||
case 'mousedown':
|
||||
if (getTextRef.current === true) {
|
||||
const handleMouseEvent = useCallback((event: MouseEvent) => {
|
||||
if (!socket || !canvasRef.current) return;
|
||||
|
||||
//performanceMonitor.current.measureEventLatency(event);
|
||||
const coordinates = getEventCoordinates(event);
|
||||
|
||||
switch (event.type) {
|
||||
case 'mousedown':
|
||||
debouncer.current.add(() => {
|
||||
if (refs.current.getText) {
|
||||
console.log('Capturing Text...');
|
||||
} else if (getListRef.current === true) {
|
||||
} else if (refs.current.getList) {
|
||||
console.log('Capturing List...');
|
||||
} else {
|
||||
socket.emit('input:mousedown', clickCoordinates);
|
||||
socket.emit('input:mousedown', coordinates);
|
||||
}
|
||||
notifyLastAction('click');
|
||||
break;
|
||||
case 'mousemove':
|
||||
if (lastMousePosition.current.x !== clickCoordinates.x ||
|
||||
lastMousePosition.current.y !== clickCoordinates.y) {
|
||||
lastMousePosition.current = {
|
||||
x: clickCoordinates.x,
|
||||
y: clickCoordinates.y,
|
||||
};
|
||||
socket.emit('input:mousemove', {
|
||||
x: clickCoordinates.x,
|
||||
y: clickCoordinates.y,
|
||||
});
|
||||
notifyLastAction('move');
|
||||
}
|
||||
break;
|
||||
case 'wheel':
|
||||
const wheelEvent = event as WheelEvent;
|
||||
const deltas = {
|
||||
deltaX: Math.round(wheelEvent.deltaX),
|
||||
deltaY: Math.round(wheelEvent.deltaY),
|
||||
};
|
||||
socket.emit('input:wheel', deltas);
|
||||
notifyLastAction('scroll');
|
||||
break;
|
||||
default:
|
||||
console.log('Default mouseEvent registered');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [socket]);
|
||||
setLastAction('click');
|
||||
}, true); // High priority
|
||||
break;
|
||||
|
||||
const onKeyboardEvent = useCallback((event: KeyboardEvent) => {
|
||||
if (socket) {
|
||||
case 'mousemove':
|
||||
if (refs.current.lastMousePosition.x !== coordinates.x ||
|
||||
refs.current.lastMousePosition.y !== coordinates.y) {
|
||||
debouncer.current.add(() => {
|
||||
refs.current.lastMousePosition = coordinates;
|
||||
socket.emit('input:mousemove', coordinates);
|
||||
setLastAction('move');
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'wheel':
|
||||
const wheelEvent = event as WheelEvent;
|
||||
debouncer.current.add(() => {
|
||||
socket.emit('input:wheel', {
|
||||
deltaX: Math.round(wheelEvent.deltaX),
|
||||
deltaY: Math.round(wheelEvent.deltaY)
|
||||
});
|
||||
setLastAction('scroll');
|
||||
});
|
||||
break;
|
||||
}
|
||||
}, [socket, getEventCoordinates]);
|
||||
|
||||
const handleKeyboardEvent = useCallback((event: KeyboardEvent) => {
|
||||
if (!socket) return;
|
||||
|
||||
debouncer.current.add(() => {
|
||||
switch (event.type) {
|
||||
case 'keydown':
|
||||
socket.emit('input:keydown', { key: event.key, coordinates: lastMousePosition.current });
|
||||
notifyLastAction(`${event.key} pressed`);
|
||||
socket.emit('input:keydown', {
|
||||
key: event.key,
|
||||
coordinates: refs.current.lastMousePosition
|
||||
});
|
||||
setLastAction(`${event.key} pressed`);
|
||||
break;
|
||||
case 'keyup':
|
||||
socket.emit('input:keyup', event.key);
|
||||
break;
|
||||
default:
|
||||
console.log('Default keyEvent registered');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, event.type === 'keydown'); // High priority for keydown
|
||||
}, [socket]);
|
||||
|
||||
// Setup and cleanup
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current) return;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
refs.current.context = canvas.getContext('2d', {
|
||||
alpha: false,
|
||||
desynchronized: true
|
||||
});
|
||||
|
||||
onCreateRef(canvasRef);
|
||||
|
||||
const options = { passive: true };
|
||||
canvas.addEventListener('mousedown', handleMouseEvent, options);
|
||||
canvas.addEventListener('mousemove', handleMouseEvent, options);
|
||||
canvas.addEventListener('wheel', handleMouseEvent, options);
|
||||
canvas.addEventListener('keydown', handleKeyboardEvent, options);
|
||||
canvas.addEventListener('keyup', handleKeyboardEvent, options);
|
||||
|
||||
return () => {
|
||||
canvas.removeEventListener('mousedown', handleMouseEvent);
|
||||
canvas.removeEventListener('mousemove', handleMouseEvent);
|
||||
canvas.removeEventListener('wheel', handleMouseEvent);
|
||||
canvas.removeEventListener('keydown', handleKeyboardEvent);
|
||||
canvas.removeEventListener('keyup', handleKeyboardEvent);
|
||||
|
||||
scheduler.current.clear();
|
||||
debouncer.current.clear();
|
||||
measurementCache.current.clear();
|
||||
};
|
||||
}, [handleMouseEvent, handleKeyboardEvent, onCreateRef]);
|
||||
|
||||
// Performance monitoring
|
||||
// useEffect(() => {
|
||||
// const intervalId = setInterval(() => {
|
||||
// console.log('Performance Report:', performanceMonitor.current.getPerformanceReport());
|
||||
// }, 20000);
|
||||
|
||||
// return () => clearInterval(intervalId);
|
||||
// }, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (canvasRef.current) {
|
||||
onCreateRef(canvasRef);
|
||||
canvasRef.current.addEventListener('mousedown', onMouseEvent);
|
||||
canvasRef.current.addEventListener('mousemove', onMouseEvent);
|
||||
canvasRef.current.addEventListener('wheel', onMouseEvent, { passive: true });
|
||||
canvasRef.current.addEventListener('keydown', onKeyboardEvent);
|
||||
canvasRef.current.addEventListener('keyup', onKeyboardEvent);
|
||||
if (!socket) return;
|
||||
|
||||
return () => {
|
||||
if (canvasRef.current) {
|
||||
canvasRef.current.removeEventListener('mousedown', onMouseEvent);
|
||||
canvasRef.current.removeEventListener('mousemove', onMouseEvent);
|
||||
canvasRef.current.removeEventListener('wheel', onMouseEvent);
|
||||
canvasRef.current.removeEventListener('keydown', onKeyboardEvent);
|
||||
canvasRef.current.removeEventListener('keyup', onKeyboardEvent);
|
||||
}
|
||||
const handlers = {
|
||||
showDatePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { datePickerInfo: info } }),
|
||||
showDropdown: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { dropdownInfo: info } }),
|
||||
showTimePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { timePickerInfo: info } }),
|
||||
showDateTimePicker: (info: any) => dispatch({ type: 'BATCH_UPDATE', payload: { dateTimeLocalInfo: info } })
|
||||
};
|
||||
|
||||
};
|
||||
} else {
|
||||
console.log('Canvas not initialized');
|
||||
}
|
||||
Object.entries(handlers).forEach(([event, handler]) => socket.on(event, handler));
|
||||
return () => {
|
||||
Object.keys(handlers).forEach(event => socket.off(event));
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
}, [onMouseEvent]);
|
||||
const memoizedDimensions = useMemo(() => ({
|
||||
width: width || 900,
|
||||
height: height || 400
|
||||
}), [width, height]);
|
||||
|
||||
return (
|
||||
<div style={{ borderRadius: '0px 0px 5px 5px', overflow: 'hidden', backgroundColor: 'white' }}>
|
||||
<div className="relative bg-white rounded-b-md overflow-hidden">
|
||||
<canvas
|
||||
tabIndex={0}
|
||||
ref={canvasRef}
|
||||
height={1000}
|
||||
width={900}
|
||||
style={{ display: 'block' }}
|
||||
height={memoizedDimensions.height}
|
||||
width={memoizedDimensions.width}
|
||||
className="block"
|
||||
/>
|
||||
<Suspense fallback={null}>
|
||||
{state.datePickerInfo && (
|
||||
<DatePicker
|
||||
coordinates={state.datePickerInfo.coordinates}
|
||||
selector={state.datePickerInfo.selector}
|
||||
onClose={() => dispatch({
|
||||
type: 'BATCH_UPDATE',
|
||||
payload: { datePickerInfo: null }
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{state.dropdownInfo && (
|
||||
<Dropdown
|
||||
coordinates={state.dropdownInfo.coordinates}
|
||||
selector={state.dropdownInfo.selector}
|
||||
options={state.dropdownInfo.options}
|
||||
onClose={() => dispatch({
|
||||
type: 'BATCH_UPDATE',
|
||||
payload: { dropdownInfo: null }
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{state.timePickerInfo && (
|
||||
<TimePicker
|
||||
coordinates={state.timePickerInfo.coordinates}
|
||||
selector={state.timePickerInfo.selector}
|
||||
onClose={() => dispatch({ type: 'SET_TIME_PICKER', payload: null })}
|
||||
/>
|
||||
)}
|
||||
{state.dateTimeLocalInfo && (
|
||||
<DateTimeLocalPicker
|
||||
coordinates={state.dateTimeLocalInfo.coordinates}
|
||||
selector={state.dateTimeLocalInfo.selector}
|
||||
onClose={() => dispatch({ type: 'SET_DATETIME_PICKER', payload: null })}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
Canvas.displayName = 'Canvas';
|
||||
|
||||
export default Canvas;
|
||||
Reference in New Issue
Block a user