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