mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-05-10 04:00:11 +00:00
Drag and Drop in Canvas!
This commit is contained in:
parent
c5361aa24d
commit
770eb99a30
5 changed files with 185 additions and 19 deletions
|
|
@ -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>
|
||||
);
|
||||
})
|
||||
|
|
|
|||
76
apps/web/app/(canvas)/dropComponent.tsx
Normal file
76
apps/web/app/(canvas)/dropComponent.tsx
Normal 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;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
11
apps/web/app/(canvas)/lib/context.tsx
Normal file
11
apps/web/app/(canvas)/lib/context.tsx
Normal 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;
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue