1- import { useContext , useMemo , type CSSProperties } from "react" ;
1+ import { useContext , type CSSProperties } from "react" ;
22import {
33 BaseBoxShapeUtil ,
44 FileHelpers ,
55 HTMLContainer ,
66 T ,
77 type RecordProps ,
88 type TLShape ,
9+ useEditor ,
910 useIsEditing ,
11+ useValue ,
1012} from "tldraw" ;
1113import { CanvasChromeContext } from "./canvasChrome" ;
1214import { local } from "./canvasIndex" ;
@@ -20,6 +22,22 @@ import { injectAgent } from "./inspectorAgent";
2022
2123export const CANVAS_FILE_SHAPE_TYPE = "canvas-file" as const ;
2224
25+ /**
26+ * Outlines the element under the canvas's pointer, which installBoardHover (canvasClicks.ts)
27+ * posts in board px as `sp:at`; a point off the board is (-1, -1) and clears it. A path is its
28+ * icon's, so an svg outlines whole. ES5, since it runs in whatever the board is.
29+ */
30+ const HOVER =
31+ "<script>(function(){var d=document.createElement('div');" +
32+ "d.style.cssText='position:fixed;pointer-events:none;z-index:2147483647;display:none;box-sizing:border-box;box-shadow:0 0 0 1px #006EFE';" +
33+ "document.documentElement.appendChild(d);" +
34+ "addEventListener('message',function(e){var m=e.data;if(!m||m.type!=='sp:at')return;" +
35+ "var el=document.elementFromPoint(m.x,m.y);if(el&&el.closest&&el.closest('svg'))el=el.closest('svg');" +
36+ "if(!el||el===document.body||el===document.documentElement){d.style.display='none';return;}" +
37+ "var r=el.getBoundingClientRect(),s=d.style;s.display='block';s.left=r.left+'px';s.top=r.top+'px';s.width=r.width+'px';s.height=r.height+'px';});" +
38+ "})();</" +
39+ "script>" ;
40+
2341declare module "tldraw" {
2442 export interface TLGlobalShapePropsMap {
2543 [ CANVAS_FILE_SHAPE_TYPE ] : {
@@ -36,28 +54,19 @@ export type CanvasFileShape = TLShape<typeof CANVAS_FILE_SHAPE_TYPE>;
3654// oxlint-disable-next-line react/only-export-components
3755function CanvasFile ( { shape } : { shape : CanvasFileShape } ) {
3856 const isEditing = useIsEditing ( shape . id ) ;
39- const { inspectingPath, setInspectorFrame } = useContext ( CanvasChromeContext ) ;
4057 const html = useCanvasFileHtml ( shape . props . path ) ;
41-
42- /**
43- * The board the inspector has open runs the agent (inspectorAgent.ts), so a click on the mockup
44- * out here picks the element under it. The panel used to load a second copy of the board to do
45- * that, which meant reading one mockup and clicking another.
46- *
47- * The frame still never takes the pointer: inspectorClicks.ts hands the agent the canvas's own
48- * pointer as a board coordinate, so panning, zooming and the comment tool go on working over
49- * the board being read.
50- */
51- const inspected = inspectingPath === shape . props . path ;
52- const agentDoc = useMemo (
53- ( ) => ( html && inspected ? injectAgent ( html ) : null ) ,
54- [ html , inspected ] ,
58+ const { inspectorOn, setInspectorFrame } = useContext ( CanvasChromeContext ) ;
59+ const editor = useEditor ( ) ;
60+ const selected = useValue (
61+ "board selected" ,
62+ ( ) => editor . getOnlySelectedShapeId ( ) === shape . id ,
63+ [ editor , shape . id ] ,
5564 ) ;
5665
57- // Behind the container, which is transparent, so the frames show through it. Safari routes a
66+ // Behind the container, which is transparent, so the frame shows through it. Safari routes a
5867 // wheel to an iframe's own scrolling area whatever pointer-events says, so a two-finger pan
5968 // over a board did nothing there, and a horizontal one chained out to the browser's back
60- // gesture. Behind the container neither frame is a scroll target, and the pan reaches tldraw
69+ // gesture. Behind the container the frame is no scroll target, and the pan reaches tldraw
6170 // wherever the cursor is. tldraw's own embed shape carries this same line:
6271 // <https://stackoverflow.com/a/49150908>.
6372 const frame : CSSProperties = {
@@ -94,31 +103,41 @@ function CanvasFile({ shape }: { shape: CanvasFileShape }) {
94103 sandbox = ""
95104 style = { { ...frame , zIndex : isEditing ? undefined : - 2 } }
96105 />
97- { /* The scripted board is a second document: srcdoc cannot be swapped on the frame above
98- (Chrome drops the second navigation while the first is still pending and leaves the
99- frame blank), and remounting it reloaded the mockup under the very click that opened
100- it, which is the flash. It loads over the board instead, pixel for pixel the same
101- one, so the swap is invisible — and the board underneath stays loaded, so closing
102- the inspector shows nothing either. */ }
103- { agentDoc ? (
106+ { /* The selected board outlines the element under the pointer: with the inspector on it
107+ runs the inspect agent (inspectorAgent.ts), which also picks the element clicked and
108+ reports the board to the panel, and otherwise only HOVER. It loads over the board
109+ rather than into its frame: Chrome drops a second srcdoc navigation while the first
110+ is pending and leaves the frame blank, and a remount reloads the mockup under the
111+ click that selected it. Pixel for pixel the same board, so the swap is invisible.
112+ `allow-scripts` and deliberately not `allow-same-origin`, which together would let
113+ the board reach back out into the canvas. */ }
114+ { selected && inspectorOn ? (
104115 < iframe
105116 ref = { ( el ) => {
106117 setInspectorFrame ( el ) ;
107118 return ( ) => setInspectorFrame ( null ) ;
108119 } }
109120 title = { shape . props . name }
110- srcDoc = { agentDoc }
111- // `allow-scripts` and deliberately not `allow-same-origin`, which together would let
112- // the frame reach back out into the canvas.
121+ srcDoc = { injectAgent ( html ) }
113122 sandbox = "allow-scripts"
123+ data-sp-hover = { shape . id }
114124 // The agent answers with its report; the frame's own load event may have fired
115125 // before the panel was listening.
116126 onLoad = { ( e ) =>
117- e . currentTarget . contentWindow ?. postMessage (
118- { type : "sp:hello" } ,
119- "*" ,
120- )
127+ e . currentTarget . contentWindow ?. postMessage ( { type : "sp:hello" } , "*" )
128+ }
129+ style = { { ...frame , zIndex : isEditing ? undefined : - 1 } }
130+ />
131+ ) : selected ? (
132+ < iframe
133+ title = { shape . props . name }
134+ srcDoc = {
135+ / < \/ b o d y > / i. test ( html )
136+ ? html . replace ( / < \/ b o d y > / i, ( tag ) => HOVER + tag )
137+ : html + HOVER
121138 }
139+ sandbox = "allow-scripts"
140+ data-sp-hover = { shape . id }
122141 style = { { ...frame , zIndex : isEditing ? undefined : - 1 } }
123142 />
124143 ) : null }
0 commit comments