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);
|
||||
overflow: hidden;
|
||||
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 { useMemo, useEffect, useState } from "react"
|
||||
import { Tldraw, Editor } from "tldraw"
|
||||
import { Tldraw, Editor, TLShapeId } from "tldraw"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { ChatBoxTool } from "@/tools/ChatBoxTool"
|
||||
import { ChatBoxShape } from "@/shapes/ChatBoxShapeUtil"
|
||||
|
|
@ -30,7 +30,13 @@ import { makeRealSettings, applySettingsMigrations } from "@/lib/settings"
|
|||
import { PromptShapeTool } from "@/tools/PromptShapeTool"
|
||||
import { PromptShape } from "@/shapes/PromptShapeUtil"
|
||||
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
|
||||
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 (
|
||||
<div style={{ position: "fixed", inset: 0 }}>
|
||||
<Tldraw
|
||||
|
|
@ -91,7 +104,16 @@ export function Board() {
|
|||
shapeUtils={customShapeUtils}
|
||||
tools={customTools}
|
||||
components={components}
|
||||
overrides={overrides}
|
||||
overrides={{
|
||||
...overrides,
|
||||
actions: (editor, actions, helpers) => {
|
||||
const customActions = overrides.actions?.(editor, actions, helpers) ?? {}
|
||||
return {
|
||||
...actions,
|
||||
...customActions,
|
||||
}
|
||||
}
|
||||
}}
|
||||
cameraOptions={{
|
||||
zoomSteps: [
|
||||
0.001, // Min zoom
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ import { BaseBoxShapeUtil, TLBaseShape } from "tldraw"
|
|||
import { useCallback, useState } from "react"
|
||||
//import Embed from "react-embed"
|
||||
|
||||
|
||||
//TODO: FIX PEN AND MOBILE INTERACTION WITH EDITING EMBED URL - DEFAULT TO TEXT SELECTED
|
||||
|
||||
export type IEmbedShape = TLBaseShape<
|
||||
"Embed",
|
||||
{
|
||||
|
|
@ -372,27 +375,53 @@ export class EmbedShape extends BaseBoxShapeUtil<IEmbedShape> {
|
|||
return (
|
||||
<div style={wrapperStyle}>
|
||||
{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
|
||||
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
|
||||
type="text"
|
||||
value={inputUrl}
|
||||
onChange={(e) => setInputUrl(e.target.value)}
|
||||
placeholder="Enter URL"
|
||||
placeholder="Enter URL to embed"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
border: "none",
|
||||
padding: "10px",
|
||||
padding: "15px", // Increased padding for better touch target
|
||||
border: "1px solid #ccc",
|
||||
borderRadius: "4px",
|
||||
fontSize: "16px", // Increased font size for better visibility
|
||||
touchAction: 'none',
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSubmit(e)
|
||||
}
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
e.stopPropagation()
|
||||
e.currentTarget.focus()
|
||||
}}
|
||||
/>
|
||||
{error && (
|
||||
<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) => {
|
||||
if (!shape.props.url) {
|
||||
const input = document.querySelector('input')
|
||||
|
|
|
|||
|
|
@ -5,16 +5,13 @@ import {
|
|||
TldrawUiMenuSubmenu,
|
||||
TLGeoShape,
|
||||
TLShape,
|
||||
useDefaultHelpers,
|
||||
} from "tldraw"
|
||||
import { TldrawUiMenuGroup } from "tldraw"
|
||||
import { DefaultContextMenu, DefaultContextMenuContent } from "tldraw"
|
||||
import { TLUiContextMenuProps, useEditor } from "tldraw"
|
||||
import {
|
||||
cameraHistory,
|
||||
copyLinkToCurrentView,
|
||||
lockCameraToFrame,
|
||||
revertCamera,
|
||||
zoomToSelection,
|
||||
} from "./cameraUtils"
|
||||
import { useState, useEffect } from "react"
|
||||
import { saveToPdf } from "../utils/pdfUtils"
|
||||
|
|
@ -22,6 +19,8 @@ import { TLFrameShape } from "tldraw"
|
|||
import { searchText } from "../utils/searchUtils"
|
||||
import { llm } from "../utils/llmUtils"
|
||||
import { getEdge } from "@/propagators/tlgraph"
|
||||
import { getCustomActions } from './overrides'
|
||||
import { overrides } from './overrides'
|
||||
|
||||
const getAllFrames = (editor: Editor) => {
|
||||
return editor
|
||||
|
|
@ -35,6 +34,9 @@ const getAllFrames = (editor: Editor) => {
|
|||
|
||||
export function CustomContextMenu(props: TLUiContextMenuProps) {
|
||||
const editor = useEditor()
|
||||
const helpers = useDefaultHelpers()
|
||||
const tools = overrides.tools?.(editor, {}, helpers) ?? {}
|
||||
const customActions = getCustomActions(editor)
|
||||
const [selectedShapes, setSelectedShapes] = useState<TLShape[]>([])
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
||||
|
||||
|
|
@ -61,170 +63,34 @@ export function CustomContextMenu(props: TLUiContextMenuProps) {
|
|||
const hasSelection = selectedIds.length > 0
|
||||
const hasCameraHistory = cameraHistory.length > 0
|
||||
|
||||
// Check if exactly one frame is selected
|
||||
const hasFrameSelected =
|
||||
selectedShapes.length === 1 && selectedShapes[0].type === "frame"
|
||||
|
||||
//TO DO: Fix camera history for camera revert
|
||||
|
||||
return (
|
||||
<DefaultContextMenu {...props}>
|
||||
<DefaultContextMenuContent />
|
||||
{/* Camera Controls Group */}
|
||||
<TldrawUiMenuGroup id="camera-controls">
|
||||
<TldrawUiMenuItem
|
||||
id="zoom-to-selection"
|
||||
label="Zoom to Selection"
|
||||
icon="zoom-in"
|
||||
kbd="z"
|
||||
disabled={!hasSelection}
|
||||
onSelect={() => zoomToSelection(editor)}
|
||||
/>
|
||||
<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
|
||||
}
|
||||
});
|
||||
}
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<TldrawUiMenuItem {...customActions.zoomToSelection} disabled={!hasSelection} />
|
||||
<TldrawUiMenuItem {...customActions.copyLinkToCurrentView} />
|
||||
<TldrawUiMenuItem {...customActions.revertCamera} disabled={!hasCameraHistory} />
|
||||
<TldrawUiMenuItem {...customActions.lockElement} disabled={!hasSelection} />
|
||||
<TldrawUiMenuItem {...customActions.unlockElement} disabled={!hasSelection} />
|
||||
<TldrawUiMenuItem {...customActions.saveToPdf} disabled={!hasSelection} />
|
||||
<TldrawUiMenuItem {...customActions.llm} disabled={!hasSelection} />
|
||||
</TldrawUiMenuGroup>
|
||||
|
||||
{/* Creation Tools Group */}
|
||||
<TldrawUiMenuGroup id="creation-tools">
|
||||
<TldrawUiMenuItem
|
||||
id="VideoChat"
|
||||
label="Create Video Chat"
|
||||
icon="video"
|
||||
kbd="alt+v"
|
||||
disabled={hasSelection}
|
||||
onSelect={() => {
|
||||
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")
|
||||
}}
|
||||
/>
|
||||
<TldrawUiMenuItem {...tools.VideoChat} disabled={hasSelection} />
|
||||
<TldrawUiMenuItem {...tools.ChatBox} disabled={hasSelection} />
|
||||
<TldrawUiMenuItem {...tools.Embed} disabled={hasSelection} />
|
||||
<TldrawUiMenuItem {...tools.SlideShape} disabled={hasSelection} />
|
||||
<TldrawUiMenuItem {...tools.Markdown} disabled={hasSelection} />
|
||||
<TldrawUiMenuItem {...tools.MycrozineTemplate} disabled={hasSelection} />
|
||||
<TldrawUiMenuItem {...tools.Prompt} disabled={hasSelection} />
|
||||
</TldrawUiMenuGroup>
|
||||
|
||||
{/* 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">
|
||||
<TldrawUiMenuSubmenu id="frames-dropdown" label="Shortcut to Frames">
|
||||
{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 }[] = []
|
||||
const MAX_HISTORY = 10 // Keep last 10 camera positions
|
||||
|
||||
const frameObservers = new Map<string, ResizeObserver>()
|
||||
|
||||
// Helper function to store camera position
|
||||
const storeCameraPosition = (editor: Editor) => {
|
||||
const currentCamera = editor.getCamera()
|
||||
|
|
@ -80,9 +82,9 @@ export const zoomToSelection = (editor: Editor) => {
|
|||
const newCamera = editor.getCamera()
|
||||
const url = new URL(window.location.href)
|
||||
url.searchParams.set("shapeId", selectedIds[0].toString())
|
||||
url.searchParams.set("x", newCamera.x.toString())
|
||||
url.searchParams.set("y", newCamera.y.toString())
|
||||
url.searchParams.set("zoom", newCamera.z.toString())
|
||||
url.searchParams.set("x", Math.round(newCamera.x).toString())
|
||||
url.searchParams.set("y", Math.round(newCamera.y).toString())
|
||||
url.searchParams.set("zoom", Math.round(newCamera.z).toString())
|
||||
window.history.replaceState(null, "", url.toString())
|
||||
}
|
||||
|
||||
|
|
@ -127,22 +129,14 @@ export const copyLinkToCurrentView = async (editor: Editor) => {
|
|||
|
||||
try {
|
||||
const baseUrl = `${window.location.origin}${window.location.pathname}`
|
||||
console.log("Base URL:", baseUrl)
|
||||
|
||||
const url = new URL(baseUrl)
|
||||
const camera = editor.getCamera()
|
||||
console.log("Current camera position:", {
|
||||
x: camera.x,
|
||||
y: camera.y,
|
||||
zoom: camera.z,
|
||||
})
|
||||
|
||||
// Set camera parameters first
|
||||
url.searchParams.set("x", camera.x.toString())
|
||||
url.searchParams.set("y", camera.y.toString())
|
||||
url.searchParams.set("zoom", camera.z.toString())
|
||||
// Round camera values to integers
|
||||
url.searchParams.set("x", Math.round(camera.x).toString())
|
||||
url.searchParams.set("y", Math.round(camera.y).toString())
|
||||
url.searchParams.set("zoom", Math.round(camera.z).toString())
|
||||
|
||||
// Add shape ID last if needed
|
||||
const selectedIds = editor.getSelectedShapeIds()
|
||||
if (selectedIds.length > 0) {
|
||||
url.searchParams.set("shapeId", selectedIds[0].toString())
|
||||
|
|
@ -152,11 +146,8 @@ export const copyLinkToCurrentView = async (editor: Editor) => {
|
|||
console.log("Final URL to copy:", finalUrl)
|
||||
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
console.log("Using modern clipboard API...")
|
||||
await navigator.clipboard.writeText(finalUrl)
|
||||
console.log("URL copied successfully using clipboard API")
|
||||
} else {
|
||||
console.log("Falling back to legacy clipboard method...")
|
||||
const textArea = document.createElement("textarea")
|
||||
textArea.value = finalUrl
|
||||
document.body.appendChild(textArea)
|
||||
|
|
@ -173,56 +164,130 @@ export const copyLinkToCurrentView = async (editor: Editor) => {
|
|||
}
|
||||
}
|
||||
|
||||
/** TODO: doesnt UNlock */
|
||||
export const lockCameraToFrame = async (editor: Editor) => {
|
||||
// Add this function to create lock indicators
|
||||
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()
|
||||
if (selectedShapes.length === 0) return
|
||||
const selectedShape = selectedShapes[0]
|
||||
const isFrame = selectedShape.type === "frame"
|
||||
const bounds = editor.getShapePageBounds(selectedShape)
|
||||
if (!isFrame || !bounds) return
|
||||
|
||||
try {
|
||||
const baseUrl = `${window.location.origin}${window.location.pathname}`
|
||||
const url = new URL(baseUrl)
|
||||
|
||||
// Calculate zoom level to fit the frame (for URL only)
|
||||
const viewportPageBounds = editor.getViewportPageBounds()
|
||||
const targetZoom = Math.min(
|
||||
viewportPageBounds.width / bounds.width,
|
||||
viewportPageBounds.height / bounds.height,
|
||||
1, // Cap at 1x zoom
|
||||
)
|
||||
|
||||
// Set URL parameters without affecting the current view
|
||||
url.searchParams.set("x", Math.round(bounds.x).toString())
|
||||
url.searchParams.set("y", Math.round(bounds.y).toString())
|
||||
url.searchParams.set(
|
||||
"zoom",
|
||||
(Math.round(targetZoom * 100) / 100).toString(),
|
||||
)
|
||||
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)
|
||||
}
|
||||
selectedShapes.forEach(shape => {
|
||||
editor.updateShape({
|
||||
id: shape.id,
|
||||
type: shape.type,
|
||||
isLocked: true,
|
||||
meta: {
|
||||
...shape.meta,
|
||||
isLocked: true,
|
||||
canInteract: true, // Allow interactions
|
||||
canMove: false, // Prevent moving
|
||||
canResize: false, // Prevent resizing
|
||||
canEdit: true, // Allow text editing
|
||||
canUpdateProps: true // Allow updating props (for prompt inputs/outputs)
|
||||
//TO DO: FIX TEXT INPUT ON LOCKED ELEMENTS (e.g. prompt shape) AND ATTACH TO SCREEN EDGE
|
||||
}
|
||||
})
|
||||
createLockIndicator(editor, shape)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to copy frame link:", error)
|
||||
alert("Failed to copy frame link. Please check clipboard permissions.")
|
||||
console.error("Failed to lock elements:", error)
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
const url = new URL(window.location.href)
|
||||
const x = url.searchParams.get("x")
|
||||
|
|
@ -230,7 +295,6 @@ export const setInitialCameraFromUrl = (editor: Editor) => {
|
|||
const zoom = url.searchParams.get("zoom")
|
||||
const shapeId = url.searchParams.get("shapeId")
|
||||
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 })
|
||||
|
||||
|
|
@ -238,9 +302,9 @@ export const setInitialCameraFromUrl = (editor: Editor) => {
|
|||
editor.stopCameraAnimation()
|
||||
editor.setCamera(
|
||||
{
|
||||
x: parseFloat(x),
|
||||
y: parseFloat(y),
|
||||
z: parseFloat(zoom)
|
||||
x: Math.round(parseFloat(x)),
|
||||
y: Math.round(parseFloat(y)),
|
||||
z: Math.round(parseFloat(zoom))
|
||||
},
|
||||
{ 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) => {
|
||||
|
|
@ -288,3 +348,17 @@ export const copyFrameLink = (_editor: Editor, frameId: string) => {
|
|||
url.searchParams.set("frameId", frameId)
|
||||
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 {
|
||||
shapeIdValidator,
|
||||
TLArrowShape,
|
||||
|
|
@ -7,8 +8,9 @@ import {
|
|||
import {
|
||||
cameraHistory,
|
||||
copyLinkToCurrentView,
|
||||
lockCameraToFrame,
|
||||
lockElement,
|
||||
revertCamera,
|
||||
unlockElement,
|
||||
zoomToSelection,
|
||||
} from "./cameraUtils"
|
||||
import { saveToPdf } from "../utils/pdfUtils"
|
||||
|
|
@ -158,8 +160,7 @@ export const overrides: TLUiOverrides = {
|
|||
}
|
||||
},
|
||||
actions(editor, actions) {
|
||||
return {
|
||||
...actions,
|
||||
const customActions = {
|
||||
"zoom-in": {
|
||||
...actions["zoom-in"],
|
||||
kbd: "ctrl+up",
|
||||
|
|
@ -183,9 +184,7 @@ export const overrides: TLUiOverrides = {
|
|||
id: "copy-link-to-current-view",
|
||||
label: "Copy Link to Current View",
|
||||
kbd: "alt+c",
|
||||
onSelect: () => {
|
||||
copyLinkToCurrentView(editor)
|
||||
},
|
||||
onSelect: () => copyLinkToCurrentView(editor),
|
||||
readonlyOk: true,
|
||||
},
|
||||
revertCamera: {
|
||||
|
|
@ -199,11 +198,26 @@ export const overrides: TLUiOverrides = {
|
|||
},
|
||||
readonlyOk: true,
|
||||
},
|
||||
lockToFrame: {
|
||||
id: "lock-to-frame",
|
||||
label: "Lock to Frame",
|
||||
lockElement: {
|
||||
id: "lock-element",
|
||||
label: "Lock Element",
|
||||
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: {
|
||||
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: {
|
||||
id: "search-shapes",
|
||||
label: "Search Shapes",
|
||||
|
|
@ -326,7 +311,7 @@ export const overrides: TLUiOverrides = {
|
|||
},
|
||||
llm: {
|
||||
id: "llm",
|
||||
label: "LLM",
|
||||
label: "Run LLM Prompt",
|
||||
kbd: "g",
|
||||
readonlyOk: true,
|
||||
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