cleanup
This commit is contained in:
@@ -1,224 +0,0 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import {
|
||||
isTagTransferEvent,
|
||||
parseTagTransferPayload,
|
||||
writeTagTransferData,
|
||||
} from '../../documents/tagTransfer';
|
||||
|
||||
const preventAll = (event) => {
|
||||
if (!event) return;
|
||||
if (typeof event.preventDefault === 'function') event.preventDefault();
|
||||
if (typeof event.stopPropagation === 'function') event.stopPropagation();
|
||||
};
|
||||
|
||||
const createDragPreview = (node, clientX, clientY) => {
|
||||
if (!(node instanceof HTMLElement)) {
|
||||
return null;
|
||||
}
|
||||
const rect = node.getBoundingClientRect();
|
||||
const safeClientX = Number.isFinite(clientX) ? clientX : rect.left + rect.width / 2;
|
||||
const safeClientY = Number.isFinite(clientY) ? clientY : rect.top + rect.height / 2;
|
||||
const offsetX = Math.min(Math.max(safeClientX - rect.left, 0), rect.width);
|
||||
const offsetY = Math.min(Math.max(safeClientY - rect.top, 0), rect.height);
|
||||
const clone = node.cloneNode(true);
|
||||
clone.style.position = 'absolute';
|
||||
clone.style.top = '-9999px';
|
||||
clone.style.left = '-9999px';
|
||||
clone.style.pointerEvents = 'none';
|
||||
clone.style.opacity = '1';
|
||||
clone.style.transform = 'none';
|
||||
document.body.appendChild(clone);
|
||||
return { clone, offsetX, offsetY };
|
||||
};
|
||||
|
||||
const cleanupPreview = (previewNode) => {
|
||||
if (previewNode && previewNode.parentNode) {
|
||||
previewNode.parentNode.removeChild(previewNode);
|
||||
}
|
||||
};
|
||||
|
||||
export const useDeskTagInteractions = ({
|
||||
onAssignTagToDocument,
|
||||
onRemoveTagFromDocument,
|
||||
requestCanvasFocus,
|
||||
}) => {
|
||||
const draggingTagRef = useRef(null);
|
||||
|
||||
const isTagTransfer = useCallback((event) => isTagTransferEvent(event), []);
|
||||
|
||||
const handleTagDragEnterDoc = useCallback(
|
||||
(event) => {
|
||||
if (!isTagTransfer(event)) return;
|
||||
preventAll(event);
|
||||
event.currentTarget.classList.add('is-tag-target');
|
||||
},
|
||||
[isTagTransfer],
|
||||
);
|
||||
|
||||
const handleTagDragOverDoc = useCallback(
|
||||
(event, docId) => {
|
||||
if (!isTagTransfer(event)) return;
|
||||
preventAll(event);
|
||||
event.currentTarget.classList.add('is-tag-target');
|
||||
|
||||
if (event.dataTransfer) {
|
||||
// If dragging over source, copy (no removal). Else move (removal).
|
||||
const isSource = draggingTagRef.current?.sourceDocId === docId;
|
||||
event.dataTransfer.dropEffect = isSource ? 'copy' : 'move';
|
||||
}
|
||||
},
|
||||
[isTagTransfer],
|
||||
);
|
||||
|
||||
const handleTagDragLeaveDoc = useCallback(
|
||||
(event) => {
|
||||
if (!isTagTransfer(event)) return;
|
||||
// Ignore if leaving to a child element
|
||||
if (event.relatedTarget && event.currentTarget.contains(event.relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
event.currentTarget.classList.remove('is-tag-target');
|
||||
},
|
||||
[isTagTransfer],
|
||||
);
|
||||
|
||||
const handleCanvasDragOver = useCallback(
|
||||
(event) => {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleCanvasDrop = useCallback(
|
||||
(event) => {
|
||||
// Implicit removal via dragend (dropEffect='move')
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
preventAll(event);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleTagDropOnDoc = useCallback(
|
||||
(event, doc) => {
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
if (!isTagTransfer(event)) {
|
||||
return;
|
||||
}
|
||||
preventAll(event);
|
||||
event.currentTarget.classList.remove('is-tag-target');
|
||||
|
||||
// Add to target
|
||||
const payload = parseTagTransferPayload(event);
|
||||
|
||||
setTimeout(() => {
|
||||
if (!payload || !payload.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.sourceDocId === doc.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestCanvasFocus?.();
|
||||
|
||||
if (onAssignTagToDocument) {
|
||||
onAssignTagToDocument(doc.id, payload.id);
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
[isTagTransfer, onAssignTagToDocument, requestCanvasFocus],
|
||||
);
|
||||
|
||||
const handleDocTagDragStart = useCallback(
|
||||
(event, doc, tag) => {
|
||||
console.log('[Tag] handleDocTagDragStart', { docId: doc?.id, tagId: tag?.id });
|
||||
if (!event?.dataTransfer || !doc || !tag) {
|
||||
return;
|
||||
}
|
||||
event.dataTransfer.effectAllowed = 'copyMove';
|
||||
writeTagTransferData(event.dataTransfer, tag, doc.id);
|
||||
|
||||
const pointerX = event.clientX;
|
||||
const pointerY = event.clientY;
|
||||
const { clone, offsetX, offsetY } = createDragPreview(event.currentTarget, pointerX, pointerY) || {};
|
||||
if (clone) {
|
||||
event.dataTransfer.setDragImage(clone, offsetX || 0, offsetY || 0);
|
||||
}
|
||||
|
||||
const element = event.currentTarget instanceof HTMLElement ? event.currentTarget : null;
|
||||
if (element) {
|
||||
element.classList.add('is-drag-hidden');
|
||||
}
|
||||
|
||||
draggingTagRef.current = {
|
||||
element,
|
||||
previewClone: clone,
|
||||
sourceDocId: doc.id,
|
||||
tagId: tag.id,
|
||||
initialX: pointerX,
|
||||
initialY: pointerY,
|
||||
distance: 0,
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleDocTagDragEnd = useCallback(
|
||||
(event) => {
|
||||
console.log('[Tag] handleDocTagDragEnd', { dropEffect: event?.dataTransfer?.dropEffect });
|
||||
const dropEffect = event?.dataTransfer?.dropEffect;
|
||||
|
||||
setTimeout(() => {
|
||||
const state = draggingTagRef.current;
|
||||
if (state) {
|
||||
const element = state.element;
|
||||
if (element) {
|
||||
element.classList.remove('is-drag-hidden');
|
||||
}
|
||||
cleanupPreview(state.previewClone);
|
||||
|
||||
// Remove if move operation completed
|
||||
if (dropEffect === 'move') {
|
||||
if (onRemoveTagFromDocument && state.sourceDocId && state.tagId) {
|
||||
onRemoveTagFromDocument(state.sourceDocId, state.tagId);
|
||||
}
|
||||
}
|
||||
}
|
||||
draggingTagRef.current = null;
|
||||
}, 0);
|
||||
},
|
||||
[onRemoveTagFromDocument],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (draggingTagRef.current && draggingTagRef.current.previewClone) {
|
||||
cleanupPreview(draggingTagRef.current.previewClone);
|
||||
}
|
||||
draggingTagRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
handleTagDragEnterDoc,
|
||||
handleTagDragOverDoc,
|
||||
handleTagDragLeaveDoc,
|
||||
handleTagDropOnDoc,
|
||||
handleDocTagDragStart,
|
||||
handleDocTagDragEnd,
|
||||
handleCanvasDragOver,
|
||||
handleCanvasDrop,
|
||||
};
|
||||
};
|
||||
|
||||
export default useDeskTagInteractions;
|
||||
@@ -0,0 +1,243 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
isTagTransferEvent,
|
||||
parseTagTransferPayload,
|
||||
writeTagTransferData,
|
||||
} from '../../documents/tagTransfer';
|
||||
import type { Identifier } from '../../types/identifiers';
|
||||
import type { Document, DocumentTag } from '../../types/documents';
|
||||
|
||||
const preventAll = (event?: React.SyntheticEvent | Event | null) => {
|
||||
if (!event) return;
|
||||
if (typeof event.preventDefault === 'function') event.preventDefault();
|
||||
if (typeof event.stopPropagation === 'function') event.stopPropagation();
|
||||
};
|
||||
|
||||
const createDragPreview = (node: EventTarget | null, clientX: number, clientY: number) => {
|
||||
if (!(node instanceof HTMLElement)) {
|
||||
return null;
|
||||
}
|
||||
const rect = node.getBoundingClientRect();
|
||||
const safeClientX = Number.isFinite(clientX) ? clientX : rect.left + rect.width / 2;
|
||||
const safeClientY = Number.isFinite(clientY) ? clientY : rect.top + rect.height / 2;
|
||||
const offsetX = Math.min(Math.max(safeClientX - rect.left, 0), rect.width);
|
||||
const offsetY = Math.min(Math.max(safeClientY - rect.top, 0), rect.height);
|
||||
const clone = node.cloneNode(true) as HTMLElement;
|
||||
clone.style.position = 'absolute';
|
||||
clone.style.top = '-9999px';
|
||||
clone.style.left = '-9999px';
|
||||
clone.style.pointerEvents = 'none';
|
||||
clone.style.opacity = '1';
|
||||
clone.style.transform = 'none';
|
||||
document.body.appendChild(clone);
|
||||
return { clone, offsetX, offsetY };
|
||||
};
|
||||
|
||||
const cleanupPreview = (previewNode: HTMLElement | null) => {
|
||||
if (previewNode && previewNode.parentNode) {
|
||||
previewNode.parentNode.removeChild(previewNode);
|
||||
}
|
||||
};
|
||||
|
||||
interface UseDeskTagInteractionsArgs {
|
||||
onAssignTagToDocument?: (docId: Identifier, tagId: Identifier) => void;
|
||||
onRemoveTagFromDocument?: (docId: Identifier, tagId: Identifier) => void;
|
||||
requestCanvasFocus?: () => void;
|
||||
}
|
||||
|
||||
interface DraggingTagState {
|
||||
element: HTMLElement | null;
|
||||
previewClone: HTMLElement | null;
|
||||
sourceDocId: Identifier;
|
||||
tagId: Identifier;
|
||||
initialX: number;
|
||||
initialY: number;
|
||||
distance: number;
|
||||
}
|
||||
|
||||
export const useDeskTagInteractions = ({
|
||||
onAssignTagToDocument,
|
||||
onRemoveTagFromDocument,
|
||||
requestCanvasFocus,
|
||||
}: UseDeskTagInteractionsArgs) => {
|
||||
const draggingTagRef = useRef<DraggingTagState | null>(null);
|
||||
|
||||
const isTagTransfer = useCallback((event: React.DragEvent) => isTagTransferEvent(event), []);
|
||||
|
||||
const handleTagDragEnterDoc = useCallback(
|
||||
(event: React.DragEvent<HTMLDivElement>, _docId: Identifier) => {
|
||||
if (!isTagTransfer(event)) return;
|
||||
preventAll(event);
|
||||
event.currentTarget.classList.add('is-tag-target');
|
||||
},
|
||||
[isTagTransfer],
|
||||
);
|
||||
|
||||
const handleTagDragOverDoc = useCallback(
|
||||
(event: React.DragEvent<HTMLDivElement>, docId: Identifier) => {
|
||||
if (!isTagTransfer(event)) return;
|
||||
preventAll(event);
|
||||
event.currentTarget.classList.add('is-tag-target');
|
||||
|
||||
if (event.dataTransfer) {
|
||||
// If dragging over source, copy (no removal). Else move (removal).
|
||||
const isSource = draggingTagRef.current?.sourceDocId === docId;
|
||||
event.dataTransfer.dropEffect = isSource ? 'copy' : 'move';
|
||||
}
|
||||
},
|
||||
[isTagTransfer],
|
||||
);
|
||||
|
||||
const handleTagDragLeaveDoc = useCallback(
|
||||
(event: React.DragEvent<HTMLDivElement>, _docId: Identifier) => {
|
||||
if (!isTagTransfer(event)) return;
|
||||
// Ignore if leaving to a child element
|
||||
if (event.relatedTarget && event.currentTarget.contains(event.relatedTarget as Node)) {
|
||||
return;
|
||||
}
|
||||
event.currentTarget.classList.remove('is-tag-target');
|
||||
},
|
||||
[isTagTransfer],
|
||||
);
|
||||
|
||||
const handleCanvasDragOver = useCallback(
|
||||
(event: React.DragEvent<HTMLDivElement>) => {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleCanvasDrop = useCallback(
|
||||
(event: React.DragEvent<HTMLDivElement>) => {
|
||||
// Implicit removal via dragend (dropEffect='move')
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
preventAll(event);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleTagDropOnDoc = useCallback(
|
||||
(event: React.DragEvent<HTMLDivElement>, doc: Document) => {
|
||||
if (!doc || !doc.id) {
|
||||
return;
|
||||
}
|
||||
if (!isTagTransfer(event)) {
|
||||
return;
|
||||
}
|
||||
preventAll(event);
|
||||
event.currentTarget.classList.remove('is-tag-target');
|
||||
|
||||
// Add to target
|
||||
const payload = parseTagTransferPayload(event);
|
||||
|
||||
setTimeout(() => {
|
||||
if (!payload || !payload.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.sourceDocId === doc.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestCanvasFocus?.();
|
||||
|
||||
if (onAssignTagToDocument && doc.id) {
|
||||
onAssignTagToDocument(doc.id, payload.id);
|
||||
}
|
||||
}, 0);
|
||||
},
|
||||
[isTagTransfer, onAssignTagToDocument, requestCanvasFocus],
|
||||
);
|
||||
|
||||
const handleDocTagDragStart = useCallback(
|
||||
(event: React.DragEvent<HTMLElement>, doc: Document, tag: DocumentTag) => {
|
||||
console.log('[Tag] handleDocTagDragStart', { docId: doc?.id, tagId: tag?.id });
|
||||
if (!event?.dataTransfer || !doc?.id || !tag?.id) {
|
||||
return;
|
||||
}
|
||||
event.dataTransfer.effectAllowed = 'copyMove';
|
||||
writeTagTransferData(event.dataTransfer, tag, doc.id);
|
||||
|
||||
const pointerX = event.clientX;
|
||||
const pointerY = event.clientY;
|
||||
const { clone, offsetX, offsetY } = createDragPreview(event.currentTarget, pointerX, pointerY) || {};
|
||||
if (clone) {
|
||||
event.dataTransfer.setDragImage(clone, offsetX || 0, offsetY || 0);
|
||||
}
|
||||
|
||||
const element = event.currentTarget instanceof HTMLElement ? event.currentTarget : null;
|
||||
if (element) {
|
||||
element.classList.add('is-drag-hidden');
|
||||
}
|
||||
|
||||
draggingTagRef.current = {
|
||||
element,
|
||||
previewClone: clone || null,
|
||||
sourceDocId: doc.id,
|
||||
tagId: tag.id,
|
||||
initialX: pointerX,
|
||||
initialY: pointerY,
|
||||
distance: 0,
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleDocTagDragEnd = useCallback(
|
||||
(event: React.DragEvent<HTMLElement>) => {
|
||||
console.log('[Tag] handleDocTagDragEnd', { dropEffect: event?.dataTransfer?.dropEffect });
|
||||
const dropEffect = event?.dataTransfer?.dropEffect;
|
||||
|
||||
setTimeout(() => {
|
||||
const state = draggingTagRef.current;
|
||||
if (state) {
|
||||
const element = state.element;
|
||||
if (element) {
|
||||
element.classList.remove('is-drag-hidden');
|
||||
}
|
||||
cleanupPreview(state.previewClone);
|
||||
|
||||
// Remove if move operation completed
|
||||
if (dropEffect === 'move') {
|
||||
if (onRemoveTagFromDocument && state.sourceDocId && state.tagId) {
|
||||
onRemoveTagFromDocument(state.sourceDocId, state.tagId);
|
||||
}
|
||||
}
|
||||
}
|
||||
draggingTagRef.current = null;
|
||||
}, 0);
|
||||
},
|
||||
[onRemoveTagFromDocument],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (draggingTagRef.current && draggingTagRef.current.previewClone) {
|
||||
cleanupPreview(draggingTagRef.current.previewClone);
|
||||
}
|
||||
draggingTagRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
handleTagDragEnterDoc,
|
||||
handleTagDragOverDoc,
|
||||
handleTagDragLeaveDoc,
|
||||
handleTagDropOnDoc,
|
||||
handleDocTagDragStart,
|
||||
handleDocTagDragEnd,
|
||||
handleCanvasDragOver,
|
||||
handleCanvasDrop,
|
||||
};
|
||||
};
|
||||
|
||||
export default useDeskTagInteractions;
|
||||
Reference in New Issue
Block a user