diff options
| author | codetorso <[email protected]> | 2024-06-18 23:46:14 -0600 |
|---|---|---|
| committer | codetorso <[email protected]> | 2024-06-18 23:46:14 -0600 |
| commit | 770eb99a30e884d4eef8acdcd6556f2e91df7aee (patch) | |
| tree | 5cb8324b9138cdc5d6c0a031f657e50c4c9c1fd4 | |
| parent | Create Embeddings for Canvas (diff) | |
| download | supermemory-770eb99a30e884d4eef8acdcd6556f2e91df7aee.tar.xz supermemory-770eb99a30e884d4eef8acdcd6556f2e91df7aee.zip | |
Drag and Drop in Canvas!
| -rw-r--r-- | apps/web/app/(canvas)/canvas.tsx | 79 | ||||
| -rw-r--r-- | apps/web/app/(canvas)/dropComponent.tsx | 76 | ||||
| -rw-r--r-- | apps/web/app/(canvas)/enabledComp.tsx | 2 | ||||
| -rw-r--r-- | apps/web/app/(canvas)/lib/context.tsx | 11 | ||||
| -rw-r--r-- | apps/web/app/(canvas)/lib/createEmbeds.ts | 36 |
5 files changed, 185 insertions, 19 deletions
diff --git a/apps/web/app/(canvas)/canvas.tsx b/apps/web/app/(canvas)/canvas.tsx index 9ec57d6d..498ab1eb 100644 --- a/apps/web/app/(canvas)/canvas.tsx +++ b/apps/web/app/(canvas)/canvas.tsx @@ -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> ); }) diff --git a/apps/web/app/(canvas)/dropComponent.tsx b/apps/web/app/(canvas)/dropComponent.tsx new file mode 100644 index 00000000..03a32358 --- /dev/null +++ b/apps/web/app/(canvas)/dropComponent.tsx @@ -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; diff --git a/apps/web/app/(canvas)/enabledComp.tsx b/apps/web/app/(canvas)/enabledComp.tsx index 5dbe6ee7..85811b82 100644 --- a/apps/web/app/(canvas)/enabledComp.tsx +++ b/apps/web/app/(canvas)/enabledComp.tsx @@ -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, diff --git a/apps/web/app/(canvas)/lib/context.tsx b/apps/web/app/(canvas)/lib/context.tsx new file mode 100644 index 00000000..36a106cf --- /dev/null +++ b/apps/web/app/(canvas)/lib/context.tsx @@ -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;
\ No newline at end of file diff --git a/apps/web/app/(canvas)/lib/createEmbeds.ts b/apps/web/app/(canvas)/lib/createEmbeds.ts index 53d81533..0db3c71b 100644 --- a/apps/web/app/(canvas)/lib/createEmbeds.ts +++ b/apps/web/app/(canvas)/lib/createEmbeds.ts @@ -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() |