diff --git a/src/ui/CustomContextMenu.tsx b/src/ui/CustomContextMenu.tsx index 137b61e..c7ee1b2 100644 --- a/src/ui/CustomContextMenu.tsx +++ b/src/ui/CustomContextMenu.tsx @@ -18,6 +18,7 @@ import { import { useState, useEffect } from "react" import { saveToPdf } from "../utils/pdfUtils" import { TLFrameShape } from "tldraw" +import { searchText } from "../utils/searchUtils" const getAllFrames = (editor: Editor) => { return editor @@ -217,6 +218,16 @@ export function CustomContextMenu(props: TLUiContextMenuProps) { }} /> + + + searchText(editor)} + /> + ) } diff --git a/src/ui/overrides.tsx b/src/ui/overrides.tsx index 7e68537..20c1ede 100644 --- a/src/ui/overrides.tsx +++ b/src/ui/overrides.tsx @@ -7,6 +7,7 @@ import { zoomToSelection, } from "./cameraUtils" import { saveToPdf } from "../utils/pdfUtils" +import { searchText } from "../utils/searchUtils" export const overrides: TLUiOverrides = { tools(editor, tools) { @@ -38,6 +39,22 @@ export const overrides: TLUiOverrides = { // Otherwise, use default select tool behavior ;(tools.select as any).onPointerDown?.(info) }, + + //TODO: Fix double click to zoom on selector tool later... + onDoubleClick: (info: any) => { + // Prevent default double-click behavior (which would start text editing) + info.preventDefault?.() + + // Handle all pointer types (mouse, touch, pen) + const point = info.point || (info.touches && info.touches[0]) || info + + // Zoom in at the clicked/touched point + editor.zoomIn(point, { animation: { duration: 200 } }) + + // Stop event propagation and prevent default handling + info.stopPropagation?.() + return false + }, }, VideoChat: { id: "VideoChat", @@ -81,6 +98,12 @@ export const overrides: TLUiOverrides = { onSelect: () => editor.setCurrentTool("MycrozineTemplate"), }, */ + hand: { + ...tools.hand, + onDoubleClick: (info: any) => { + editor.zoomIn(info.point, { animation: { duration: 200 } }) + }, + }, } }, actions(editor, actions) { @@ -317,6 +340,13 @@ export const overrides: TLUiOverrides = { editor.stopFollowingUser() }, }, + searchShapes: { + id: "search-shapes", + label: "Search Shapes", + kbd: "s", + readonlyOk: true, + onSelect: () => searchText(editor), + }, } }, } diff --git a/src/utils/searchUtils.ts b/src/utils/searchUtils.ts new file mode 100644 index 0000000..b20acbd --- /dev/null +++ b/src/utils/searchUtils.ts @@ -0,0 +1,87 @@ +import { Editor } from "tldraw" + +export const searchText = (editor: Editor) => { + // Switch to select tool first + editor.setCurrentTool('select') + + const searchTerm = prompt("Enter search text:") + if (!searchTerm) return + + const shapes = editor.getCurrentPageShapes() + const matchingShapes = shapes.filter(shape => { + if (!shape.props) return false + + const textProperties = [ + (shape.props as any).text, + (shape.props as any).name, + (shape.props as any).value, + (shape.props as any).url, + (shape.props as any).description, + (shape.props as any).content, + ] + + const termLower = searchTerm.toLowerCase() + return textProperties.some(prop => + typeof prop === 'string' && + prop.toLowerCase().includes(termLower) + ) + }) + + if (matchingShapes.length > 0) { + editor.selectNone() + editor.setSelectedShapes(matchingShapes) + + const commonBounds = editor.getSelectionPageBounds() + if (!commonBounds) return + + // Calculate viewport dimensions + const viewportPageBounds = editor.getViewportPageBounds() + + // Calculate the ratio of selection size to viewport size + const widthRatio = commonBounds.width / viewportPageBounds.width + const heightRatio = commonBounds.height / viewportPageBounds.height + + // Calculate target zoom based on selection size + let targetZoom + if (widthRatio < 0.1 || heightRatio < 0.1) { + targetZoom = Math.min( + (viewportPageBounds.width * 0.8) / commonBounds.width, + (viewportPageBounds.height * 0.8) / commonBounds.height, + 40 + ) + } else if (widthRatio > 1 || heightRatio > 1) { + targetZoom = Math.min( + (viewportPageBounds.width * 0.7) / commonBounds.width, + (viewportPageBounds.height * 0.7) / commonBounds.height, + 0.125 + ) + } else { + targetZoom = Math.min( + (viewportPageBounds.width * 0.8) / commonBounds.width, + (viewportPageBounds.height * 0.8) / commonBounds.height, + 20 + ) + } + + // Zoom to the common bounds + editor.zoomToBounds(commonBounds, { + targetZoom, + inset: widthRatio > 1 || heightRatio > 1 ? 20 : 50, + animation: { + duration: 400, + easing: (t) => t * (2 - t), + }, + }) + + // Update URL with new camera position and first selected shape ID + const newCamera = editor.getCamera() + const url = new URL(window.location.href) + url.searchParams.set("shapeId", matchingShapes[0].id) + url.searchParams.set("x", newCamera.x.toString()) + url.searchParams.set("y", newCamera.y.toString()) + url.searchParams.set("zoom", newCamera.z.toString()) + window.history.replaceState(null, "", url.toString()) + } else { + alert("No matches found") + } +} \ No newline at end of file