lock & unlock shapes, clean up overrides & context menu, make embed element easier to interact with
This commit is contained in:
parent
a770d516df
commit
0add9bd514
|
|
@ -376,4 +376,24 @@ p:has(+ ol) {
|
||||||
box-shadow: 0 0px 16px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 0px 16px rgba(0, 0, 0, 0.15);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lock-indicator {
|
||||||
|
position: absolute;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1000;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lock-indicator:hover {
|
||||||
|
transform: scale(1.1) !important;
|
||||||
|
background: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useSync } from "@tldraw/sync"
|
import { useSync } from "@tldraw/sync"
|
||||||
import { useMemo, useEffect, useState } from "react"
|
import { useMemo, useEffect, useState } from "react"
|
||||||
import { Tldraw, Editor } from "tldraw"
|
import { Tldraw, Editor, TLShapeId } from "tldraw"
|
||||||
import { useParams } from "react-router-dom"
|
import { useParams } from "react-router-dom"
|
||||||
import { ChatBoxTool } from "@/tools/ChatBoxTool"
|
import { ChatBoxTool } from "@/tools/ChatBoxTool"
|
||||||
import { ChatBoxShape } from "@/shapes/ChatBoxShapeUtil"
|
import { ChatBoxShape } from "@/shapes/ChatBoxShapeUtil"
|
||||||
|
|
@ -30,7 +30,13 @@ import { makeRealSettings, applySettingsMigrations } from "@/lib/settings"
|
||||||
import { PromptShapeTool } from "@/tools/PromptShapeTool"
|
import { PromptShapeTool } from "@/tools/PromptShapeTool"
|
||||||
import { PromptShape } from "@/shapes/PromptShapeUtil"
|
import { PromptShape } from "@/shapes/PromptShapeUtil"
|
||||||
import { llm } from "@/utils/llmUtils"
|
import { llm } from "@/utils/llmUtils"
|
||||||
import { setInitialCameraFromUrl } from "@/ui/cameraUtils"
|
import {
|
||||||
|
lockElement,
|
||||||
|
unlockElement,
|
||||||
|
setInitialCameraFromUrl,
|
||||||
|
initLockIndicators,
|
||||||
|
watchForLockedShapes,
|
||||||
|
} from "@/ui/cameraUtils"
|
||||||
|
|
||||||
// Default to production URL if env var isn't available
|
// Default to production URL if env var isn't available
|
||||||
export const WORKER_URL = "https://jeffemmett-canvas.jeffemmett.workers.dev"
|
export const WORKER_URL = "https://jeffemmett-canvas.jeffemmett.workers.dev"
|
||||||
|
|
@ -84,6 +90,13 @@ export function Board() {
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Remove the URL-based locking effect and replace with store-based initialization
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editor) return
|
||||||
|
initLockIndicators(editor)
|
||||||
|
watchForLockedShapes(editor)
|
||||||
|
}, [editor])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: "fixed", inset: 0 }}>
|
<div style={{ position: "fixed", inset: 0 }}>
|
||||||
<Tldraw
|
<Tldraw
|
||||||
|
|
@ -91,7 +104,16 @@ export function Board() {
|
||||||
shapeUtils={customShapeUtils}
|
shapeUtils={customShapeUtils}
|
||||||
tools={customTools}
|
tools={customTools}
|
||||||
components={components}
|
components={components}
|
||||||
overrides={overrides}
|
overrides={{
|
||||||
|
...overrides,
|
||||||
|
actions: (editor, actions, helpers) => {
|
||||||
|
const customActions = overrides.actions?.(editor, actions, helpers) ?? {}
|
||||||
|
return {
|
||||||
|
...actions,
|
||||||
|
...customActions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
cameraOptions={{
|
cameraOptions={{
|
||||||
zoomSteps: [
|
zoomSteps: [
|
||||||
0.001, // Min zoom
|
0.001, // Min zoom
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ import { BaseBoxShapeUtil, TLBaseShape } from "tldraw"
|
||||||
import { useCallback, useState } from "react"
|
import { useCallback, useState } from "react"
|
||||||
//import Embed from "react-embed"
|
//import Embed from "react-embed"
|
||||||
|
|
||||||
|
|
||||||
|
//TODO: FIX PEN AND MOBILE INTERACTION WITH EDITING EMBED URL - DEFAULT TO TEXT SELECTED
|
||||||
|
|
||||||
export type IEmbedShape = TLBaseShape<
|
export type IEmbedShape = TLBaseShape<
|
||||||
"Embed",
|
"Embed",
|
||||||
{
|
{
|
||||||
|
|
@ -372,27 +375,53 @@ export class EmbedShape extends BaseBoxShapeUtil<IEmbedShape> {
|
||||||
return (
|
return (
|
||||||
<div style={wrapperStyle}>
|
<div style={wrapperStyle}>
|
||||||
{controls("")}
|
{controls("")}
|
||||||
<div style={contentStyle}>
|
<div
|
||||||
|
style={{
|
||||||
|
...contentStyle,
|
||||||
|
cursor: 'text', // Add text cursor to indicate clickable
|
||||||
|
touchAction: 'none', // Prevent touch scrolling
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
const input = e.currentTarget.querySelector('input')
|
||||||
|
input?.focus()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
style={{ width: "100%", height: "100%", padding: "10px" }}
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
padding: "10px",
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={inputUrl}
|
value={inputUrl}
|
||||||
onChange={(e) => setInputUrl(e.target.value)}
|
onChange={(e) => setInputUrl(e.target.value)}
|
||||||
placeholder="Enter URL"
|
placeholder="Enter URL to embed"
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
padding: "15px", // Increased padding for better touch target
|
||||||
border: "none",
|
border: "1px solid #ccc",
|
||||||
padding: "10px",
|
borderRadius: "4px",
|
||||||
|
fontSize: "16px", // Increased font size for better visibility
|
||||||
|
touchAction: 'none',
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
handleSubmit(e)
|
handleSubmit(e)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.currentTarget.focus()
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{error && (
|
{error && (
|
||||||
<div style={{ color: "red", marginTop: "10px" }}>{error}</div>
|
<div style={{ color: "red", marginTop: "10px" }}>{error}</div>
|
||||||
|
|
@ -555,7 +584,7 @@ export class EmbedShape extends BaseBoxShapeUtil<IEmbedShape> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new method to handle all pointer interactions
|
// Update the pointer down handler
|
||||||
onPointerDown = (shape: IEmbedShape) => {
|
onPointerDown = (shape: IEmbedShape) => {
|
||||||
if (!shape.props.url) {
|
if (!shape.props.url) {
|
||||||
const input = document.querySelector('input')
|
const input = document.querySelector('input')
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,13 @@ import {
|
||||||
TldrawUiMenuSubmenu,
|
TldrawUiMenuSubmenu,
|
||||||
TLGeoShape,
|
TLGeoShape,
|
||||||
TLShape,
|
TLShape,
|
||||||
|
useDefaultHelpers,
|
||||||
} from "tldraw"
|
} from "tldraw"
|
||||||
import { TldrawUiMenuGroup } from "tldraw"
|
import { TldrawUiMenuGroup } from "tldraw"
|
||||||
import { DefaultContextMenu, DefaultContextMenuContent } from "tldraw"
|
import { DefaultContextMenu, DefaultContextMenuContent } from "tldraw"
|
||||||
import { TLUiContextMenuProps, useEditor } from "tldraw"
|
import { TLUiContextMenuProps, useEditor } from "tldraw"
|
||||||
import {
|
import {
|
||||||
cameraHistory,
|
cameraHistory,
|
||||||
copyLinkToCurrentView,
|
|
||||||
lockCameraToFrame,
|
|
||||||
revertCamera,
|
|
||||||
zoomToSelection,
|
|
||||||
} from "./cameraUtils"
|
} from "./cameraUtils"
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { saveToPdf } from "../utils/pdfUtils"
|
import { saveToPdf } from "../utils/pdfUtils"
|
||||||
|
|
@ -22,6 +19,8 @@ import { TLFrameShape } from "tldraw"
|
||||||
import { searchText } from "../utils/searchUtils"
|
import { searchText } from "../utils/searchUtils"
|
||||||
import { llm } from "../utils/llmUtils"
|
import { llm } from "../utils/llmUtils"
|
||||||
import { getEdge } from "@/propagators/tlgraph"
|
import { getEdge } from "@/propagators/tlgraph"
|
||||||
|
import { getCustomActions } from './overrides'
|
||||||
|
import { overrides } from './overrides'
|
||||||
|
|
||||||
const getAllFrames = (editor: Editor) => {
|
const getAllFrames = (editor: Editor) => {
|
||||||
return editor
|
return editor
|
||||||
|
|
@ -35,6 +34,9 @@ const getAllFrames = (editor: Editor) => {
|
||||||
|
|
||||||
export function CustomContextMenu(props: TLUiContextMenuProps) {
|
export function CustomContextMenu(props: TLUiContextMenuProps) {
|
||||||
const editor = useEditor()
|
const editor = useEditor()
|
||||||
|
const helpers = useDefaultHelpers()
|
||||||
|
const tools = overrides.tools?.(editor, {}, helpers) ?? {}
|
||||||
|
const customActions = getCustomActions(editor)
|
||||||
const [selectedShapes, setSelectedShapes] = useState<TLShape[]>([])
|
const [selectedShapes, setSelectedShapes] = useState<TLShape[]>([])
|
||||||
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
||||||
|
|
||||||
|
|
@ -61,170 +63,34 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
|
||||||
const hasSelection = selectedIds.length > 0
|
const hasSelection = selectedIds.length > 0
|
||||||
const hasCameraHistory = cameraHistory.length > 0
|
const hasCameraHistory = cameraHistory.length > 0
|
||||||
|
|
||||||
// Check if exactly one frame is selected
|
//TO DO: Fix camera history for camera revert
|
||||||
const hasFrameSelected =
|
|
||||||
selectedShapes.length === 1 && selectedShapes[0].type === "frame"
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DefaultContextMenu {...props}>
|
<DefaultContextMenu {...props}>
|
||||||
<DefaultContextMenuContent />
|
<DefaultContextMenuContent />
|
||||||
{/* Camera Controls Group */}
|
{/* Camera Controls Group */}
|
||||||
<TldrawUiMenuGroup id="camera-controls">
|
<TldrawUiMenuGroup id="camera-controls">
|
||||||
<TldrawUiMenuItem
|
<TldrawUiMenuItem {...customActions.zoomToSelection} disabled={!hasSelection} />
|
||||||
id="zoom-to-selection"
|
<TldrawUiMenuItem {...customActions.copyLinkToCurrentView} />
|
||||||
label="Zoom to Selection"
|
<TldrawUiMenuItem {...customActions.revertCamera} disabled={!hasCameraHistory} />
|
||||||
icon="zoom-in"
|
<TldrawUiMenuItem {...customActions.lockElement} disabled={!hasSelection} />
|
||||||
kbd="z"
|
<TldrawUiMenuItem {...customActions.unlockElement} disabled={!hasSelection} />
|
||||||
disabled={!hasSelection}
|
<TldrawUiMenuItem {...customActions.saveToPdf} disabled={!hasSelection} />
|
||||||
onSelect={() => zoomToSelection(editor)}
|
<TldrawUiMenuItem {...customActions.llm} disabled={!hasSelection} />
|
||||||
/>
|
|
||||||
<TldrawUiMenuItem
|
|
||||||
id="copy-link-to-current-view"
|
|
||||||
label="Copy Link to Current View"
|
|
||||||
icon="link"
|
|
||||||
kbd="alt+c"
|
|
||||||
onSelect={() => copyLinkToCurrentView(editor)}
|
|
||||||
/>
|
|
||||||
<TldrawUiMenuItem
|
|
||||||
id="revert-camera"
|
|
||||||
label="Revert Camera"
|
|
||||||
icon="undo"
|
|
||||||
kbd="alt+b"
|
|
||||||
disabled={!hasCameraHistory}
|
|
||||||
onSelect={() => revertCamera(editor)}
|
|
||||||
/>
|
|
||||||
<TldrawUiMenuItem
|
|
||||||
id="save-to-pdf"
|
|
||||||
label="Save Selection as PDF"
|
|
||||||
icon="file"
|
|
||||||
kbd="alt+s"
|
|
||||||
disabled={!hasSelection}
|
|
||||||
onSelect={() => saveToPdf(editor)}
|
|
||||||
/>
|
|
||||||
<TldrawUiMenuItem
|
|
||||||
id="run-llm-prompt"
|
|
||||||
label="Run LLM Prompt"
|
|
||||||
icon="file"
|
|
||||||
kbd="g"
|
|
||||||
disabled={!hasSelection}
|
|
||||||
onSelect={() => {
|
|
||||||
const selectedShape = editor.getSelectedShapes()[0];
|
|
||||||
if (!selectedShape || selectedShape.type !== 'arrow') return;
|
|
||||||
|
|
||||||
const edge = getEdge(selectedShape, editor);
|
|
||||||
if (!edge) return;
|
|
||||||
|
|
||||||
const sourceShape = editor.getShape(edge.from);
|
|
||||||
const sourceText =
|
|
||||||
sourceShape && sourceShape.type === "geo"
|
|
||||||
? (sourceShape as TLGeoShape).props.text
|
|
||||||
: "";
|
|
||||||
|
|
||||||
llm(
|
|
||||||
`Instruction: ${edge.text}
|
|
||||||
${sourceText ? `Context: ${sourceText}` : ""}`,
|
|
||||||
localStorage.getItem("openai_api_key") || "",
|
|
||||||
(partialResponse: string) => {
|
|
||||||
editor.updateShape({
|
|
||||||
id: edge.to,
|
|
||||||
type: "geo",
|
|
||||||
props: {
|
|
||||||
...(editor.getShape(edge.to) as TLGeoShape).props,
|
|
||||||
text: partialResponse
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TldrawUiMenuGroup>
|
</TldrawUiMenuGroup>
|
||||||
|
|
||||||
{/* Creation Tools Group */}
|
{/* Creation Tools Group */}
|
||||||
<TldrawUiMenuGroup id="creation-tools">
|
<TldrawUiMenuGroup id="creation-tools">
|
||||||
<TldrawUiMenuItem
|
<TldrawUiMenuItem {...tools.VideoChat} disabled={hasSelection} />
|
||||||
id="VideoChat"
|
<TldrawUiMenuItem {...tools.ChatBox} disabled={hasSelection} />
|
||||||
label="Create Video Chat"
|
<TldrawUiMenuItem {...tools.Embed} disabled={hasSelection} />
|
||||||
icon="video"
|
<TldrawUiMenuItem {...tools.SlideShape} disabled={hasSelection} />
|
||||||
kbd="alt+v"
|
<TldrawUiMenuItem {...tools.Markdown} disabled={hasSelection} />
|
||||||
disabled={hasSelection}
|
<TldrawUiMenuItem {...tools.MycrozineTemplate} disabled={hasSelection} />
|
||||||
onSelect={() => {
|
<TldrawUiMenuItem {...tools.Prompt} disabled={hasSelection} />
|
||||||
editor.setCurrentTool("VideoChat")
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TldrawUiMenuItem
|
|
||||||
id="ChatBox"
|
|
||||||
label="Create Chat Box"
|
|
||||||
icon="chat"
|
|
||||||
kbd="alt+c"
|
|
||||||
disabled={hasSelection}
|
|
||||||
onSelect={() => {
|
|
||||||
editor.setCurrentTool("ChatBox")
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TldrawUiMenuItem
|
|
||||||
id="Embed"
|
|
||||||
label="Create Embed"
|
|
||||||
icon="embed"
|
|
||||||
kbd="alt+e"
|
|
||||||
disabled={hasSelection}
|
|
||||||
onSelect={() => {
|
|
||||||
editor.setCurrentTool("Embed")
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TldrawUiMenuItem
|
|
||||||
id="Slide"
|
|
||||||
label="Create Slide"
|
|
||||||
icon="slides"
|
|
||||||
kbd="alt+s"
|
|
||||||
disabled={hasSelection}
|
|
||||||
onSelect={() => {
|
|
||||||
editor.setCurrentTool("Slide")
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TldrawUiMenuItem
|
|
||||||
id="MycrozineTemplate"
|
|
||||||
label="Create Mycrozine Template"
|
|
||||||
icon="rectangle"
|
|
||||||
kbd="alt+z"
|
|
||||||
disabled={hasSelection}
|
|
||||||
onSelect={() => {
|
|
||||||
editor.setCurrentTool("MycrozineTemplate")
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TldrawUiMenuItem
|
|
||||||
id="Markdown"
|
|
||||||
label="Create Markdown"
|
|
||||||
icon="markdown"
|
|
||||||
kbd="alt+m"
|
|
||||||
disabled={hasSelection}
|
|
||||||
onSelect={() => {
|
|
||||||
editor.setCurrentTool("Markdown")
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TldrawUiMenuItem
|
|
||||||
id="Prompt"
|
|
||||||
label="Create LLM Chat Prompt"
|
|
||||||
icon="prompt"
|
|
||||||
kbd="alt+p"
|
|
||||||
disabled={hasSelection}
|
|
||||||
onSelect={() => {
|
|
||||||
editor.setCurrentTool("Prompt")
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TldrawUiMenuGroup>
|
</TldrawUiMenuGroup>
|
||||||
|
|
||||||
{/* Frame Controls */}
|
{/* Frame Controls */}
|
||||||
<TldrawUiMenuGroup id="frame-controls">
|
|
||||||
<TldrawUiMenuItem
|
|
||||||
id="lock-to-frame"
|
|
||||||
label="Lock to Frame"
|
|
||||||
icon="lock"
|
|
||||||
kbd="shift+l"
|
|
||||||
disabled={!hasFrameSelected}
|
|
||||||
onSelect={() => lockCameraToFrame(editor)}
|
|
||||||
/>
|
|
||||||
</TldrawUiMenuGroup>
|
|
||||||
|
|
||||||
<TldrawUiMenuGroup id="frames-list">
|
<TldrawUiMenuGroup id="frames-list">
|
||||||
<TldrawUiMenuSubmenu id="frames-dropdown" label="Shortcut to Frames">
|
<TldrawUiMenuSubmenu id="frames-dropdown" label="Shortcut to Frames">
|
||||||
{getAllFrames(editor).map((frame) => (
|
{getAllFrames(editor).map((frame) => (
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { Editor, TLFrameShape, TLParentId, TLShape, TLShapeId } from "tldraw"
|
||||||
export const cameraHistory: { x: number; y: number; z: number }[] = []
|
export const cameraHistory: { x: number; y: number; z: number }[] = []
|
||||||
const MAX_HISTORY = 10 // Keep last 10 camera positions
|
const MAX_HISTORY = 10 // Keep last 10 camera positions
|
||||||
|
|
||||||
|
const frameObservers = new Map<string, ResizeObserver>()
|
||||||
|
|
||||||
// Helper function to store camera position
|
// Helper function to store camera position
|
||||||
const storeCameraPosition = (editor: Editor) => {
|
const storeCameraPosition = (editor: Editor) => {
|
||||||
const currentCamera = editor.getCamera()
|
const currentCamera = editor.getCamera()
|
||||||
|
|
@ -80,9 +82,9 @@ export const zoomToSelection = (editor: Editor) => {
|
||||||
const newCamera = editor.getCamera()
|
const newCamera = editor.getCamera()
|
||||||
const url = new URL(window.location.href)
|
const url = new URL(window.location.href)
|
||||||
url.searchParams.set("shapeId", selectedIds[0].toString())
|
url.searchParams.set("shapeId", selectedIds[0].toString())
|
||||||
url.searchParams.set("x", newCamera.x.toString())
|
url.searchParams.set("x", Math.round(newCamera.x).toString())
|
||||||
url.searchParams.set("y", newCamera.y.toString())
|
url.searchParams.set("y", Math.round(newCamera.y).toString())
|
||||||
url.searchParams.set("zoom", newCamera.z.toString())
|
url.searchParams.set("zoom", Math.round(newCamera.z).toString())
|
||||||
window.history.replaceState(null, "", url.toString())
|
window.history.replaceState(null, "", url.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,22 +129,14 @@ export const copyLinkToCurrentView = async (editor: Editor) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const baseUrl = `${window.location.origin}${window.location.pathname}`
|
const baseUrl = `${window.location.origin}${window.location.pathname}`
|
||||||
console.log("Base URL:", baseUrl)
|
|
||||||
|
|
||||||
const url = new URL(baseUrl)
|
const url = new URL(baseUrl)
|
||||||
const camera = editor.getCamera()
|
const camera = editor.getCamera()
|
||||||
console.log("Current camera position:", {
|
|
||||||
x: camera.x,
|
|
||||||
y: camera.y,
|
|
||||||
zoom: camera.z,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Set camera parameters first
|
// Round camera values to integers
|
||||||
url.searchParams.set("x", camera.x.toString())
|
url.searchParams.set("x", Math.round(camera.x).toString())
|
||||||
url.searchParams.set("y", camera.y.toString())
|
url.searchParams.set("y", Math.round(camera.y).toString())
|
||||||
url.searchParams.set("zoom", camera.z.toString())
|
url.searchParams.set("zoom", Math.round(camera.z).toString())
|
||||||
|
|
||||||
// Add shape ID last if needed
|
|
||||||
const selectedIds = editor.getSelectedShapeIds()
|
const selectedIds = editor.getSelectedShapeIds()
|
||||||
if (selectedIds.length > 0) {
|
if (selectedIds.length > 0) {
|
||||||
url.searchParams.set("shapeId", selectedIds[0].toString())
|
url.searchParams.set("shapeId", selectedIds[0].toString())
|
||||||
|
|
@ -152,11 +146,8 @@ export const copyLinkToCurrentView = async (editor: Editor) => {
|
||||||
console.log("Final URL to copy:", finalUrl)
|
console.log("Final URL to copy:", finalUrl)
|
||||||
|
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
console.log("Using modern clipboard API...")
|
|
||||||
await navigator.clipboard.writeText(finalUrl)
|
await navigator.clipboard.writeText(finalUrl)
|
||||||
console.log("URL copied successfully using clipboard API")
|
|
||||||
} else {
|
} else {
|
||||||
console.log("Falling back to legacy clipboard method...")
|
|
||||||
const textArea = document.createElement("textarea")
|
const textArea = document.createElement("textarea")
|
||||||
textArea.value = finalUrl
|
textArea.value = finalUrl
|
||||||
document.body.appendChild(textArea)
|
document.body.appendChild(textArea)
|
||||||
|
|
@ -173,56 +164,130 @@ export const copyLinkToCurrentView = async (editor: Editor) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** TODO: doesnt UNlock */
|
// Add this function to create lock indicators
|
||||||
export const lockCameraToFrame = async (editor: Editor) => {
|
const createLockIndicator = (editor: Editor, shape: TLShape) => {
|
||||||
|
const lockIndicator = document.createElement('div')
|
||||||
|
lockIndicator.id = `lock-indicator-${shape.id}`
|
||||||
|
lockIndicator.className = 'lock-indicator'
|
||||||
|
lockIndicator.innerHTML = '🔒'
|
||||||
|
|
||||||
|
// Set styles to position at top-right of shape
|
||||||
|
lockIndicator.style.position = 'absolute'
|
||||||
|
lockIndicator.style.right = '3px'
|
||||||
|
lockIndicator.style.top = '3px'
|
||||||
|
lockIndicator.style.pointerEvents = 'all'
|
||||||
|
lockIndicator.style.zIndex = '99999'
|
||||||
|
lockIndicator.style.background = 'white'
|
||||||
|
lockIndicator.style.border = '1px solid #ddd'
|
||||||
|
lockIndicator.style.borderRadius = '4px'
|
||||||
|
lockIndicator.style.padding = '4px'
|
||||||
|
lockIndicator.style.cursor = 'pointer'
|
||||||
|
lockIndicator.style.boxShadow = '0 1px 3px rgba(0,0,0,0.12)'
|
||||||
|
lockIndicator.style.fontSize = '12px'
|
||||||
|
lockIndicator.style.lineHeight = '1'
|
||||||
|
lockIndicator.style.display = 'flex'
|
||||||
|
lockIndicator.style.alignItems = 'center'
|
||||||
|
lockIndicator.style.justifyContent = 'center'
|
||||||
|
lockIndicator.style.width = '20px'
|
||||||
|
lockIndicator.style.height = '20px'
|
||||||
|
lockIndicator.style.userSelect = 'none'
|
||||||
|
|
||||||
|
// Add hover effect
|
||||||
|
lockIndicator.onmouseenter = () => {
|
||||||
|
lockIndicator.style.backgroundColor = '#f0f0f0'
|
||||||
|
}
|
||||||
|
lockIndicator.onmouseleave = () => {
|
||||||
|
lockIndicator.style.backgroundColor = 'white'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tooltip and click handlers with stopPropagation
|
||||||
|
lockIndicator.title = 'Unlock shape'
|
||||||
|
|
||||||
|
lockIndicator.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
unlockElement(editor, shape.id)
|
||||||
|
}, true)
|
||||||
|
|
||||||
|
lockIndicator.addEventListener('mousedown', (e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
}, true)
|
||||||
|
|
||||||
|
lockIndicator.addEventListener('pointerdown', (e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
}, true)
|
||||||
|
|
||||||
|
const shapeElement = document.querySelector(`[data-shape-id="${shape.id}"]`)
|
||||||
|
if (shapeElement) {
|
||||||
|
shapeElement.appendChild(lockIndicator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify lockElement to use the new function
|
||||||
|
export const lockElement = async (editor: Editor) => {
|
||||||
const selectedShapes = editor.getSelectedShapes()
|
const selectedShapes = editor.getSelectedShapes()
|
||||||
if (selectedShapes.length === 0) return
|
if (selectedShapes.length === 0) return
|
||||||
const selectedShape = selectedShapes[0]
|
|
||||||
const isFrame = selectedShape.type === "frame"
|
|
||||||
const bounds = editor.getShapePageBounds(selectedShape)
|
|
||||||
if (!isFrame || !bounds) return
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const baseUrl = `${window.location.origin}${window.location.pathname}`
|
selectedShapes.forEach(shape => {
|
||||||
const url = new URL(baseUrl)
|
editor.updateShape({
|
||||||
|
id: shape.id,
|
||||||
// Calculate zoom level to fit the frame (for URL only)
|
type: shape.type,
|
||||||
const viewportPageBounds = editor.getViewportPageBounds()
|
isLocked: true,
|
||||||
const targetZoom = Math.min(
|
meta: {
|
||||||
viewportPageBounds.width / bounds.width,
|
...shape.meta,
|
||||||
viewportPageBounds.height / bounds.height,
|
isLocked: true,
|
||||||
1, // Cap at 1x zoom
|
canInteract: true, // Allow interactions
|
||||||
)
|
canMove: false, // Prevent moving
|
||||||
|
canResize: false, // Prevent resizing
|
||||||
// Set URL parameters without affecting the current view
|
canEdit: true, // Allow text editing
|
||||||
url.searchParams.set("x", Math.round(bounds.x).toString())
|
canUpdateProps: true // Allow updating props (for prompt inputs/outputs)
|
||||||
url.searchParams.set("y", Math.round(bounds.y).toString())
|
//TO DO: FIX TEXT INPUT ON LOCKED ELEMENTS (e.g. prompt shape) AND ATTACH TO SCREEN EDGE
|
||||||
url.searchParams.set(
|
}
|
||||||
"zoom",
|
})
|
||||||
(Math.round(targetZoom * 100) / 100).toString(),
|
createLockIndicator(editor, shape)
|
||||||
)
|
})
|
||||||
url.searchParams.set("frameId", selectedShape.id)
|
|
||||||
url.searchParams.set("isLocked", "true")
|
|
||||||
|
|
||||||
const finalUrl = url.toString()
|
|
||||||
|
|
||||||
// Copy URL to clipboard without modifying the current view
|
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
|
||||||
await navigator.clipboard.writeText(finalUrl)
|
|
||||||
} else {
|
|
||||||
const textArea = document.createElement("textarea")
|
|
||||||
textArea.value = finalUrl
|
|
||||||
document.body.appendChild(textArea)
|
|
||||||
textArea.select()
|
|
||||||
document.execCommand("copy")
|
|
||||||
document.body.removeChild(textArea)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to copy frame link:", error)
|
console.error("Failed to lock elements:", error)
|
||||||
alert("Failed to copy frame link. Please check clipboard permissions.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const unlockElement = (editor: Editor, shapeId: string) => {
|
||||||
|
const indicator = document.getElementById(`lock-indicator-${shapeId}`)
|
||||||
|
if (indicator) {
|
||||||
|
indicator.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
const shape = editor.getShape(shapeId as TLShapeId)
|
||||||
|
if (shape) {
|
||||||
|
editor.updateShape({
|
||||||
|
id: shapeId as TLShapeId,
|
||||||
|
type: shape.type,
|
||||||
|
isLocked: false,
|
||||||
|
meta: {
|
||||||
|
...shape.meta,
|
||||||
|
isLocked: false,
|
||||||
|
canInteract: true,
|
||||||
|
canMove: true,
|
||||||
|
canResize: true,
|
||||||
|
canEdit: true,
|
||||||
|
canUpdateProps: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize lock indicators based on stored state
|
||||||
|
export const initLockIndicators = (editor: Editor) => {
|
||||||
|
editor.getCurrentPageShapes().forEach(shape => {
|
||||||
|
if (shape.isLocked || shape.meta?.isLocked) {
|
||||||
|
createLockIndicator(editor, shape)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const setInitialCameraFromUrl = (editor: Editor) => {
|
export const setInitialCameraFromUrl = (editor: Editor) => {
|
||||||
const url = new URL(window.location.href)
|
const url = new URL(window.location.href)
|
||||||
const x = url.searchParams.get("x")
|
const x = url.searchParams.get("x")
|
||||||
|
|
@ -230,7 +295,6 @@ export const setInitialCameraFromUrl = (editor: Editor) => {
|
||||||
const zoom = url.searchParams.get("zoom")
|
const zoom = url.searchParams.get("zoom")
|
||||||
const shapeId = url.searchParams.get("shapeId")
|
const shapeId = url.searchParams.get("shapeId")
|
||||||
const frameId = url.searchParams.get("frameId")
|
const frameId = url.searchParams.get("frameId")
|
||||||
//const isLocked = url.searchParams.get("isLocked") === "true"
|
|
||||||
|
|
||||||
console.log('Setting initial camera from URL:', { x, y, zoom, shapeId, frameId })
|
console.log('Setting initial camera from URL:', { x, y, zoom, shapeId, frameId })
|
||||||
|
|
||||||
|
|
@ -238,9 +302,9 @@ export const setInitialCameraFromUrl = (editor: Editor) => {
|
||||||
editor.stopCameraAnimation()
|
editor.stopCameraAnimation()
|
||||||
editor.setCamera(
|
editor.setCamera(
|
||||||
{
|
{
|
||||||
x: parseFloat(x),
|
x: Math.round(parseFloat(x)),
|
||||||
y: parseFloat(y),
|
y: Math.round(parseFloat(y)),
|
||||||
z: parseFloat(zoom)
|
z: Math.round(parseFloat(zoom))
|
||||||
},
|
},
|
||||||
{ animation: { duration: 0 } }
|
{ animation: { duration: 0 } }
|
||||||
)
|
)
|
||||||
|
|
@ -266,10 +330,6 @@ export const setInitialCameraFromUrl = (editor: Editor) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (isLocked) {
|
|
||||||
// editor.setCameraOptions({ isLocked: true })
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const zoomToFrame = (editor: Editor, frameId: string) => {
|
export const zoomToFrame = (editor: Editor, frameId: string) => {
|
||||||
|
|
@ -288,3 +348,17 @@ export const copyFrameLink = (_editor: Editor, frameId: string) => {
|
||||||
url.searchParams.set("frameId", frameId)
|
url.searchParams.set("frameId", frameId)
|
||||||
navigator.clipboard.writeText(url.toString())
|
navigator.clipboard.writeText(url.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize lock indicators and watch for changes
|
||||||
|
export const watchForLockedShapes = (editor: Editor) => {
|
||||||
|
editor.on('change', () => {
|
||||||
|
editor.getCurrentPageShapes().forEach(shape => {
|
||||||
|
const hasIndicator = document.getElementById(`lock-indicator-${shape.id}`)
|
||||||
|
if (shape.isLocked && !hasIndicator) {
|
||||||
|
createLockIndicator(editor, shape)
|
||||||
|
} else if (!shape.isLocked && hasIndicator) {
|
||||||
|
hasIndicator.remove()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Editor, useDefaultHelpers } from "tldraw"
|
||||||
import {
|
import {
|
||||||
shapeIdValidator,
|
shapeIdValidator,
|
||||||
TLArrowShape,
|
TLArrowShape,
|
||||||
|
|
@ -7,8 +8,9 @@ import {
|
||||||
import {
|
import {
|
||||||
cameraHistory,
|
cameraHistory,
|
||||||
copyLinkToCurrentView,
|
copyLinkToCurrentView,
|
||||||
lockCameraToFrame,
|
lockElement,
|
||||||
revertCamera,
|
revertCamera,
|
||||||
|
unlockElement,
|
||||||
zoomToSelection,
|
zoomToSelection,
|
||||||
} from "./cameraUtils"
|
} from "./cameraUtils"
|
||||||
import { saveToPdf } from "../utils/pdfUtils"
|
import { saveToPdf } from "../utils/pdfUtils"
|
||||||
|
|
@ -158,8 +160,7 @@ export const overrides: TLUiOverrides = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions(editor, actions) {
|
actions(editor, actions) {
|
||||||
return {
|
const customActions = {
|
||||||
...actions,
|
|
||||||
"zoom-in": {
|
"zoom-in": {
|
||||||
...actions["zoom-in"],
|
...actions["zoom-in"],
|
||||||
kbd: "ctrl+up",
|
kbd: "ctrl+up",
|
||||||
|
|
@ -183,9 +184,7 @@ export const overrides: TLUiOverrides = {
|
||||||
id: "copy-link-to-current-view",
|
id: "copy-link-to-current-view",
|
||||||
label: "Copy Link to Current View",
|
label: "Copy Link to Current View",
|
||||||
kbd: "alt+c",
|
kbd: "alt+c",
|
||||||
onSelect: () => {
|
onSelect: () => copyLinkToCurrentView(editor),
|
||||||
copyLinkToCurrentView(editor)
|
|
||||||
},
|
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
},
|
},
|
||||||
revertCamera: {
|
revertCamera: {
|
||||||
|
|
@ -199,11 +198,26 @@ export const overrides: TLUiOverrides = {
|
||||||
},
|
},
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
},
|
},
|
||||||
lockToFrame: {
|
lockElement: {
|
||||||
id: "lock-to-frame",
|
id: "lock-element",
|
||||||
label: "Lock to Frame",
|
label: "Lock Element",
|
||||||
kbd: "shift+l",
|
kbd: "shift+l",
|
||||||
onSelect: () => lockCameraToFrame(editor),
|
onSelect: () => {
|
||||||
|
const selectedShapes = editor.getSelectedShapes()
|
||||||
|
if (selectedShapes.length > 0) {
|
||||||
|
lockElement(editor)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
readonlyOk: true,
|
||||||
|
},
|
||||||
|
unlockElement: {
|
||||||
|
id: "unlock-element",
|
||||||
|
label: "Unlock Element",
|
||||||
|
onSelect: () => {
|
||||||
|
if (editor.getSelectedShapeIds().length > 0) {
|
||||||
|
unlockElement(editor, editor.getSelectedShapeIds()[0])
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
saveToPdf: {
|
saveToPdf: {
|
||||||
id: "save-to-pdf",
|
id: "save-to-pdf",
|
||||||
|
|
@ -288,35 +302,6 @@ export const overrides: TLUiOverrides = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
//TODO: MAKE THIS WORK, ADD USER PERMISSIONING TO JOIN BROADCAST?
|
|
||||||
startBroadcast: {
|
|
||||||
id: "start-broadcast",
|
|
||||||
label: "Start Broadcasting",
|
|
||||||
kbd: "alt+b",
|
|
||||||
readonlyOk: true,
|
|
||||||
onSelect: () => {
|
|
||||||
editor.markHistoryStoppingPoint('start-broadcast')
|
|
||||||
editor.updateInstanceState({ isBroadcasting: true })
|
|
||||||
const url = new URL(window.location.href)
|
|
||||||
url.searchParams.set("followId", editor.user.getId())
|
|
||||||
window.history.replaceState(null, "", url.toString())
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stopBroadcast: {
|
|
||||||
id: "stop-broadcast",
|
|
||||||
label: "Stop Broadcasting",
|
|
||||||
kbd: "alt+shift+b",
|
|
||||||
readonlyOk: true,
|
|
||||||
onSelect: () => {
|
|
||||||
editor.updateInstanceState({ isBroadcasting: false })
|
|
||||||
editor.stopFollowingUser()
|
|
||||||
|
|
||||||
// Remove followId from URL
|
|
||||||
const url = new URL(window.location.href)
|
|
||||||
url.searchParams.delete("followId")
|
|
||||||
window.history.replaceState(null, "", url.toString())
|
|
||||||
},
|
|
||||||
},
|
|
||||||
searchShapes: {
|
searchShapes: {
|
||||||
id: "search-shapes",
|
id: "search-shapes",
|
||||||
label: "Search Shapes",
|
label: "Search Shapes",
|
||||||
|
|
@ -326,7 +311,7 @@ export const overrides: TLUiOverrides = {
|
||||||
},
|
},
|
||||||
llm: {
|
llm: {
|
||||||
id: "llm",
|
id: "llm",
|
||||||
label: "LLM",
|
label: "Run LLM Prompt",
|
||||||
kbd: "g",
|
kbd: "g",
|
||||||
readonlyOk: true,
|
readonlyOk: true,
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
|
|
@ -424,5 +409,16 @@ export const overrides: TLUiOverrides = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...actions,
|
||||||
|
...customActions,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Export actions for use in context menu
|
||||||
|
export const getCustomActions = (editor: Editor) => {
|
||||||
|
const helpers = useDefaultHelpers()
|
||||||
|
return overrides.actions?.(editor, {}, helpers) ?? {}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue