playful
This commit is contained in:
@@ -17,13 +17,17 @@ import useDocumentDrag from './desktop/useDocumentDrag';
|
||||
import { DesktopProvider, useDesktopContext } from './desktop/context';
|
||||
import PreviewZoomOverlay from './detail/PreviewZoomOverlay';
|
||||
import { getTagColorStyle } from './utils/colors';
|
||||
import {
|
||||
isTagTransferEvent,
|
||||
parseTagTransferPayload,
|
||||
writeTagTransferData,
|
||||
} from './documents/tagTransfer';
|
||||
import './DesktopWorkspace.css';
|
||||
|
||||
const CANVAS_PADDING = 24;
|
||||
const ROTATION_RANGE = 7;
|
||||
const DEFAULT_CANVAS_WIDTH = 1024;
|
||||
const DEFAULT_CANVAS_HEIGHT = 680;
|
||||
const TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag'];
|
||||
|
||||
const CARD_MIN = 240;
|
||||
const CARD_MAX = 340;
|
||||
@@ -111,50 +115,6 @@ const polygonCentroid = (polygon) => {
|
||||
};
|
||||
};
|
||||
|
||||
const readTransferData = (dataTransfer, mimeTypes) => {
|
||||
if (!dataTransfer) {
|
||||
return null;
|
||||
}
|
||||
for (let index = 0; index < mimeTypes.length; index += 1) {
|
||||
const type = mimeTypes[index];
|
||||
try {
|
||||
const raw = dataTransfer.getData(type);
|
||||
if (raw) {
|
||||
return raw;
|
||||
}
|
||||
} catch (error) {
|
||||
if (DEBUG_DROP) {
|
||||
console.warn('[desk] readTransferData failed for type', type, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const parseTagTransferPayload = (event) => {
|
||||
const raw = readTransferData(event?.dataTransfer, [
|
||||
'application/x-papercrate-tag',
|
||||
'text/papercrate-tag',
|
||||
]);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (error) {
|
||||
console.warn('[desk] parseTagTransferPayload failed', error);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const resolveTagKey = (tag) => {
|
||||
if (!tag) {
|
||||
return null;
|
||||
}
|
||||
const key = tag.id ?? tag.uuid ?? tag.slug ?? tag.label;
|
||||
return key != null ? String(key) : null;
|
||||
};
|
||||
|
||||
const DesktopPreviewCard = ({
|
||||
doc,
|
||||
title,
|
||||
@@ -214,9 +174,21 @@ const DesktopPreviewCard = ({
|
||||
const showNav = hasPreview && (cardinality > 1 || canGoPrev || canGoNext);
|
||||
|
||||
return (
|
||||
<div className={cardClasses.join(' ')}>
|
||||
<div
|
||||
className={cardClasses.join(' ')}
|
||||
onDragStart={(event) => {
|
||||
if (event instanceof DragEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{hasPreview ? (
|
||||
<img src={currentUrl} alt={title} />
|
||||
<img
|
||||
src={currentUrl}
|
||||
alt={title}
|
||||
draggable={false}
|
||||
onDragStart={(event) => event.preventDefault()}
|
||||
/>
|
||||
) : (
|
||||
<div className="desk-item__empty">
|
||||
<div className="desk-item__placeholder">DOC</div>
|
||||
@@ -817,21 +789,71 @@ const DesktopWorkspace = ({
|
||||
[updateRemovalCursor],
|
||||
);
|
||||
|
||||
const isTagTransfer = useCallback((event) => {
|
||||
const types = event.dataTransfer?.types;
|
||||
if (!types) return false;
|
||||
return TAG_MIME_TYPES.some((type) =>
|
||||
typeof types.includes === 'function'
|
||||
? types.includes(type)
|
||||
: Array.from(types).includes(type),
|
||||
);
|
||||
}, []);
|
||||
const isTagTransfer = useCallback((event) => isTagTransferEvent(event), []);
|
||||
|
||||
const handleTagDragEnd = useCallback(() => {
|
||||
updateRemovalCursor(false);
|
||||
setTagDropTargetId(null);
|
||||
}, [updateRemovalCursor]);
|
||||
|
||||
const finalizeTagDrag = useCallback(
|
||||
(dropEffect = 'none') => {
|
||||
const state = draggingTagRef.current;
|
||||
if (!state) {
|
||||
updateRemovalCursor(false);
|
||||
return;
|
||||
}
|
||||
|
||||
draggingTagRef.current = null;
|
||||
|
||||
const node = state.element;
|
||||
const showNode = () => {
|
||||
if (node instanceof HTMLElement) {
|
||||
node.classList.remove('is-drag-hidden');
|
||||
}
|
||||
};
|
||||
const scheduleShowNode = () => {
|
||||
if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
|
||||
window.requestAnimationFrame(showNode);
|
||||
} else {
|
||||
setTimeout(showNode, 0);
|
||||
}
|
||||
};
|
||||
|
||||
cleanupPreview(state.previewClone);
|
||||
|
||||
const shouldRemove =
|
||||
!state.dropHandled &&
|
||||
dropEffect === 'none' &&
|
||||
state.sourceDocId &&
|
||||
typeof onRemoveTagFromDocument === 'function' &&
|
||||
(state.distance || 0) >= TAG_REMOVE_DISTANCE;
|
||||
|
||||
if (!shouldRemove) {
|
||||
scheduleShowNode();
|
||||
updateRemovalCursor(false);
|
||||
return;
|
||||
}
|
||||
|
||||
updateRemovalCursor(false);
|
||||
setPendingRemovalTag({ docId: state.sourceDocId, tagId: state.tagId });
|
||||
void (async () => {
|
||||
try {
|
||||
await onRemoveTagFromDocument(state.sourceDocId, state.tagId);
|
||||
if (DEBUG_DROP) {
|
||||
console.log('[desk] finalizeTagDrag -> removed tag due to fling');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to remove tag after drag', error);
|
||||
scheduleShowNode();
|
||||
} finally {
|
||||
setPendingRemovalTag(null);
|
||||
}
|
||||
})();
|
||||
},
|
||||
[onRemoveTagFromDocument, updateRemovalCursor, setPendingRemovalTag],
|
||||
);
|
||||
|
||||
const ensureDocumentSize = useCallback((doc) => {
|
||||
if (!doc?.id) {
|
||||
return null;
|
||||
@@ -897,7 +919,7 @@ const DesktopWorkspace = ({
|
||||
return null;
|
||||
}
|
||||
const doc = documentLookup.get(overlayDocId);
|
||||
const alt = snapshot.alt || doc?.title || doc?.original_name || 'Document preview';
|
||||
const alt = snapshot.alt || doc?.title;
|
||||
return {
|
||||
url: snapshot.url,
|
||||
alt,
|
||||
@@ -1374,7 +1396,6 @@ const syncLayoutSnapshot = useCallback(() => {
|
||||
if (DEBUG_DROP) {
|
||||
console.log('[desk] handleTagDropOnDoc: missing tag id payload', payload);
|
||||
}
|
||||
requestCanvasFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1389,7 +1410,6 @@ const syncLayoutSnapshot = useCallback(() => {
|
||||
if (DEBUG_DROP) {
|
||||
console.log('[desk] handleTagDropOnDoc: drop from same doc ignored', tagId);
|
||||
}
|
||||
requestCanvasFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1401,7 +1421,6 @@ const syncLayoutSnapshot = useCallback(() => {
|
||||
if (DEBUG_DROP) {
|
||||
console.log('[desk] handleTagDropOnDoc: tag already assigned', tagId);
|
||||
}
|
||||
requestCanvasFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1433,7 +1452,8 @@ const syncLayoutSnapshot = useCallback(() => {
|
||||
if (DEBUG_DROP) {
|
||||
console.log('[desk] handleTagDropOnDoc: finalizing drop for tag', tagId);
|
||||
}
|
||||
requestCanvasFocus();
|
||||
handleTagDragEnd();
|
||||
finalizeTagDrag(payload?.sourceDocId ? 'move' : 'copy');
|
||||
}
|
||||
},
|
||||
[
|
||||
@@ -1441,7 +1461,8 @@ const syncLayoutSnapshot = useCallback(() => {
|
||||
markActiveTagDropHandled,
|
||||
onAssignTagToDocument,
|
||||
onRemoveTagFromDocument,
|
||||
requestCanvasFocus,
|
||||
handleTagDragEnd,
|
||||
finalizeTagDrag,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1556,14 +1577,7 @@ const syncLayoutSnapshot = useCallback(() => {
|
||||
console.warn('[desk] Failed to set drag effect', error);
|
||||
}
|
||||
|
||||
const payload = JSON.stringify({ id: tag.id, label: tag.label, sourceDocId: doc.id });
|
||||
try {
|
||||
event.dataTransfer?.setData('application/x-papercrate-tag', payload);
|
||||
event.dataTransfer?.setData('text/papercrate-tag', payload);
|
||||
event.dataTransfer?.setData('text/plain', tag.label || 'Tag');
|
||||
} catch (error) {
|
||||
console.warn('[desk] Failed to populate drag data for tag', error);
|
||||
}
|
||||
writeTagTransferData(event.dataTransfer, tag, doc.id);
|
||||
|
||||
const pending = pendingDocTagDragRef.current;
|
||||
const node = event.currentTarget;
|
||||
@@ -1600,7 +1614,7 @@ const syncLayoutSnapshot = useCallback(() => {
|
||||
draggingTagRef.current = {
|
||||
sourceDocId: doc.id,
|
||||
tagId: tag.id,
|
||||
tagLabel: tag.label || 'Tag',
|
||||
tagLabel: tag.label,
|
||||
startX: initialX,
|
||||
startY: initialY,
|
||||
distance: 0,
|
||||
@@ -1657,61 +1671,9 @@ const syncLayoutSnapshot = useCallback(() => {
|
||||
const handleDocTagDragEnd = useCallback(
|
||||
(event) => {
|
||||
handleTagDragEnd();
|
||||
const state = draggingTagRef.current;
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
draggingTagRef.current = null;
|
||||
|
||||
const node = state.element;
|
||||
const showNode = () => {
|
||||
if (node instanceof HTMLElement) {
|
||||
node.classList.remove('is-drag-hidden');
|
||||
}
|
||||
};
|
||||
cleanupPreview(state.previewClone);
|
||||
|
||||
const scheduleShowNode = () => {
|
||||
if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
|
||||
window.requestAnimationFrame(showNode);
|
||||
} else {
|
||||
setTimeout(showNode, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const dropEffect = event?.dataTransfer?.dropEffect || 'none';
|
||||
console.log('[desk] dragEnd dropEffect', dropEffect, 'dropHandled', state.dropHandled);
|
||||
const shouldRemove =
|
||||
!state.dropHandled &&
|
||||
dropEffect === 'none' &&
|
||||
state.sourceDocId &&
|
||||
typeof onRemoveTagFromDocument === 'function' &&
|
||||
(state.distance || 0) >= TAG_REMOVE_DISTANCE;
|
||||
|
||||
if (!shouldRemove) {
|
||||
console.log('[desk] dragEnd -> no removal. distance:', state.distance);
|
||||
scheduleShowNode();
|
||||
requestCanvasFocus();
|
||||
updateRemovalCursor(false);
|
||||
return;
|
||||
}
|
||||
|
||||
requestCanvasFocus();
|
||||
updateRemovalCursor(false);
|
||||
setPendingRemovalTag({ docId: state.sourceDocId, tagId: state.tagId });
|
||||
void (async () => {
|
||||
try {
|
||||
await onRemoveTagFromDocument(state.sourceDocId, state.tagId);
|
||||
console.log('[desk] dragEnd -> removed tag due to fling');
|
||||
} catch (error) {
|
||||
console.error('Failed to remove tag after drag', error);
|
||||
scheduleShowNode();
|
||||
} finally {
|
||||
setPendingRemovalTag(null);
|
||||
}
|
||||
})();
|
||||
finalizeTagDrag(event?.dataTransfer?.dropEffect || 'none');
|
||||
},
|
||||
[requestCanvasFocus, handleTagDragEnd, onRemoveTagFromDocument, updateRemovalCursor],
|
||||
[handleTagDragEnd, finalizeTagDrag],
|
||||
);
|
||||
|
||||
const contextValue = useMemo(
|
||||
@@ -1906,11 +1868,10 @@ const DesktopWorkspaceView = () => {
|
||||
};
|
||||
const docKey = doc?.id != null ? String(doc.id) : null;
|
||||
const shouldLoad = docKey ? visibleDocIds.has(docKey) : false;
|
||||
const title = doc.title || doc.original_name || 'Document';
|
||||
const dragging = draggingId === doc.id;
|
||||
const tags = Array.isArray(doc.tags) ? doc.tags : [];
|
||||
const docTagKeys = tags
|
||||
.map((tag) => resolveTagKey(tag))
|
||||
.map((tag) => (tag ? tag.id : null))
|
||||
.filter(Boolean);
|
||||
const matchesFilter =
|
||||
activeTagSet.size === 0 || docTagKeys.some((key) => activeTagSet.has(key));
|
||||
@@ -1956,7 +1917,7 @@ const DesktopWorkspaceView = () => {
|
||||
<div className="desk-item__body">
|
||||
<DesktopPreviewCard
|
||||
doc={doc}
|
||||
title={title}
|
||||
title={doc.title}
|
||||
ensureAssetUrl={ensureAssetUrl}
|
||||
getDocumentAsset={getDocumentAsset}
|
||||
onNavigatorSnapshot={handleNavigatorSnapshot}
|
||||
@@ -1965,7 +1926,6 @@ const DesktopWorkspaceView = () => {
|
||||
{tags.length > 0 && (
|
||||
<div className="desk-item__tags" aria-hidden="true">
|
||||
{tags.map((tag) => {
|
||||
const key = tag.id || tag.label || String(tag);
|
||||
if (
|
||||
pendingRemovalTag &&
|
||||
pendingRemovalTag.docId === doc.id &&
|
||||
@@ -1982,17 +1942,17 @@ const DesktopWorkspaceView = () => {
|
||||
if (pendingRemoval) tagClasses.push('tag-chip--tear-pending');
|
||||
return (
|
||||
<span
|
||||
key={key}
|
||||
key={tag.id}
|
||||
className={tagClasses.join(' ')}
|
||||
style={colorStyle || undefined}
|
||||
title={tag.label || 'Tag'}
|
||||
title={tag.label}
|
||||
draggable
|
||||
onPointerDown={(event) => handleDocTagPointerDown(event, doc, tag)}
|
||||
onDragStart={(event) => handleDocTagDragStart(event, doc, tag)}
|
||||
onDrag={handleDocTagDrag}
|
||||
onDragEnd={(event) => handleDocTagDragEnd(event)}
|
||||
>
|
||||
<span className="tag-chip__label">{tag.label || 'Tag'}</span>
|
||||
<span className="tag-chip__label">{tag.label}</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user