Drag and Drop in Canvas!

This commit is contained in:
codetorso 2024-06-18 23:46:14 -06:00
parent c5361aa24d
commit 770eb99a30
5 changed files with 185 additions and 19 deletions

View file

@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Editor, Tldraw, setUserPreferences, TLStoreWithStatus } from "tldraw";
import { createAssetFromUrl } from "./lib/createAssetUrl";
import "tldraw/tldraw.css";
@ -7,10 +7,53 @@ import { twitterCardUtil } from "./twitterCard";
import createEmbedsFromUrl from "./lib/createEmbeds";
import { loadRemoteSnapshot } from "./lib/loadSnap";
import { SaveStatus } from "./savesnap";
import { getAssetUrls } from '@tldraw/assets/selfHosted'
import { memo } from 'react';
import { getAssetUrls } from "@tldraw/assets/selfHosted";
import { memo } from "react";
import DragContext from "./lib/context";
import DropZone from "./dropComponent";
export const Canvas = memo(()=>{
export const Canvas = memo(() => {
const [isDraggingOver, setIsDraggingOver] = useState<boolean>(false);
const Dragref = useRef<HTMLDivElement | null>(null)
const handleDragOver = (event: any) => {
event.preventDefault();
setIsDraggingOver(true);
console.log("entere")
};
const handleDragLeave = () => {
setIsDraggingOver(false);
console.log("leaver")
};
useEffect(() => {
const divElement = Dragref.current;
if (divElement) {
divElement.addEventListener('dragover', handleDragOver);
divElement.addEventListener('dragleave', handleDragLeave);
}
return () => {
if (divElement) {
divElement.removeEventListener('dragover', handleDragOver);
divElement.removeEventListener('dragleave', handleDragLeave);
}
};
}, []);
return (
<DragContext.Provider value={{ isDraggingOver, setIsDraggingOver }}>
<div
ref={Dragref}
className="w-full h-full"
>
<TldrawComponent />
</div>
</DragContext.Provider>
);
});
const TldrawComponent =memo(() => {
const [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({
status: "loading",
});
@ -38,18 +81,22 @@ export const Canvas = memo(()=>{
setUserPreferences({ id: "supermemory", isDarkMode: true });
const assetUrls = getAssetUrls()
const assetUrls = getAssetUrls();
return (
<Tldraw
assetUrls={assetUrls}
components={components}
store={storeWithStatus}
shapeUtils={[twitterCardUtil]}
onMount={handleMount}
>
<div className="absolute left-1/2 top-0 z-[1000000] flex -translate-x-1/2 gap-2 bg-[#2C3439] text-[#B3BCC5]">
<SaveStatus />
</div>
</Tldraw>
<div className="w-full h-full">
<Tldraw
className="relative"
assetUrls={assetUrls}
components={components}
store={storeWithStatus}
shapeUtils={[twitterCardUtil]}
onMount={handleMount}
>
<div className="absolute left-1/2 top-0 z-[1000000] flex -translate-x-1/2 gap-2 bg-[#2C3439] text-[#B3BCC5]">
<SaveStatus />
</div>
<DropZone />
</Tldraw>
</div>
);
})

View file

@ -0,0 +1,76 @@
import React, { useRef, useCallback, useEffect, useContext } from "react";
import { useEditor } from "tldraw";
import DragContext, { DragContextType } from "./lib/context";
import { handleExternalDroppedContent } from "./lib/createEmbeds";
const stripHtmlTags = (html: string): string => {
const div = document.createElement("div");
div.innerHTML = html;
return div.textContent || div.innerText || "";
};
const useDrag = (): DragContextType => {
const context = useContext(DragContext);
if (!context) {
throw new Error('useCounter must be used within a CounterProvider');
}
return context;
};
function DropZone() {
const dropRef = useRef<HTMLDivElement | null>(null);
const {isDraggingOver, setIsDraggingOver} = useDrag();
const editor = useEditor();
const handleDrop = useCallback((event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
setIsDraggingOver(false);
const dt = event.dataTransfer;
const items = dt.items;
for (let i = 0; i < items.length; i++) {
if (items[i]!.kind === "file" && items[i]!.type.startsWith("image/")) {
const file = items[i]!.getAsFile();
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
if (e.target) {
// setDroppedImage(e.target.result as string);
}
};
reader.readAsDataURL(file);
}
} else if (items[i]!.kind === "string") {
items[i]!.getAsString((data) => {
const cleanText = stripHtmlTags(data);
handleExternalDroppedContent({editor,text:cleanText})
});
}
}
}, []);
useEffect(() => {
const divElement = dropRef.current;
if (divElement) {
// @ts-ignore
divElement.addEventListener("drop", handleDrop);
}
return () => {
if (divElement) {
// @ts-ignore
divElement.removeEventListener("drop", handleDrop);
}
};
}, []);
return (
<div
className={`h-full w-full absolute top-0 left-0 z-[100000] pointer-events-none ${isDraggingOver && "bg-[#2C3439] pointer-events-auto"}`}
ref={dropRef}
></div>
);
}
export default DropZone;

View file

@ -7,12 +7,12 @@ export const components: Partial<TLUiComponents> = {
TopPanel: null,
DebugPanel: null,
DebugMenu: null,
PageMenu: null,
// Minimap: null,
// ContextMenu: null,
// HelpMenu: null,
// ZoomMenu: null,
// StylePanel: null,
// PageMenu: null,
// NavigationPanel: null,
// Toolbar: null,
// KeyboardShortcutsDialog: null,

View file

@ -0,0 +1,11 @@
import { createContext } from 'react';
export interface DragContextType {
isDraggingOver: boolean;
setIsDraggingOver: React.Dispatch<React.SetStateAction<boolean>>;
}
const DragContext = createContext<DragContextType | undefined>(undefined);
export default DragContext;

View file

@ -2,8 +2,8 @@ import { AssetRecordType, Editor, TLAsset, TLAssetId, TLBookmarkShape, TLExterna
export default async function createEmbedsFromUrl({url, point, sources, editor}: {
url: string
point: VecLike | undefined
sources: TLExternalContentSource[] | undefined
point?: VecLike | undefined
sources?: TLExternalContentSource[] | undefined
editor: Editor
}){
@ -87,6 +87,38 @@ export default async function createEmbedsFromUrl({url, point, sources, editor}:
});
}
function isURL(str: string) {
try {
new URL(str);
return true;
} catch {
return false;
}
}
export function handleExternalDroppedContent({text, editor}: {text:string, editor: Editor}){
const position = editor.inputs.shiftKey
? editor.inputs.currentPagePoint
: editor.getViewportPageBounds().center;
if (isURL(text)){
createEmbedsFromUrl({editor, url: text})
} else{
editor.createShape({
type: "text",
x: position.x - 75,
y: position.y - 75,
props: {
text: text,
size: "s",
textAlign: "start",
},
});
}
}
function centerSelectionAroundPoint(editor: Editor, position: VecLike) {
// Re-position shapes so that the center of the group is at the provided point
const viewportPageBounds = editor.getViewportPageBounds()