@@ -123,73 +123,47 @@ export class ActionDelegate implements UI.ActionRegistration.ActionDelegate {
123123 return ;
124124 }
125125
126+ // Resolve to a remote object to ensure the node is alive in the context.
126127 const object = await node . resolveToObject ( ) ;
127128 if ( ! object ) {
128129 return ;
129130 }
130- const result = await object . callFunction ( function ( this : Element ) {
131- function getFrameOffset ( frame : Element | null ) : { x : number , y : number } {
132- if ( ! frame ) {
133- return { x : 0 , y : 0 } ;
134- }
135-
136- // The offset of the frame's content relative to the frame element
137- // contains the border width and the padding.
138- // The border width.
139- const borderTop = frame . clientTop ;
140- const borderLeft = frame . clientLeft ;
141-
142- // The padding can be retrieved via computed styles.
143- const styles = window . getComputedStyle ( frame ) ;
144- const paddingTop = parseFloat ( styles . paddingTop ) ;
145- const paddingLeft = parseFloat ( styles . paddingLeft ) ;
146-
147- // The position of the frame in it's parent.
148- const rect = frame . getBoundingClientRect ( ) ;
149-
150- // The offset of the parent frame's content relative to the
151- // document. If there is no parent frame, the offset is 0.
152- // In case of OOPiF, there is no access to the parent frame's
153- // offset.
154- const parentFrameOffset = getFrameOffset ( frame . ownerDocument . defaultView ?. frameElement ?? null ) ;
155-
156- // The scroll position of the frame.
157- const scrollX = frame . ownerDocument . defaultView ?. scrollX ?? 0 ;
158- const scrollY = frame . ownerDocument . defaultView ?. scrollY ?? 0 ;
159-
160- return {
161- x : parentFrameOffset . x + rect . left + borderLeft + paddingLeft + scrollX ,
162- y : parentFrameOffset . y + rect . top + borderTop + paddingTop + scrollY ,
163- } ;
164- }
165-
166- // The bounding client rect of the node relative to the viewport.
167- const rect = this . getBoundingClientRect ( ) ;
168- const frameOffset = getFrameOffset ( this . ownerDocument . defaultView ?. frameElement ?? null ) ;
169-
170- // The scroll position of the frame.
171- const scrollX = this . ownerDocument . defaultView ?. scrollX ?? 0 ;
172- const scrollY = this . ownerDocument . defaultView ?. scrollY ?? 0 ;
173-
174- // The offset of the node's content relative to the top-level
175- // document is the sum of the element offset relative to the
176- // document's viewport, the document's scroll position, and the
177- // parent's offset relative to the top-level document.
178- return JSON . stringify ( {
179- x : rect . left + frameOffset . x + scrollX ,
180- y : rect . top + frameOffset . y + scrollY ,
181- width : rect . width ,
182- height : rect . height ,
183- scale : 1 ,
184- } ) ;
185- } ) ;
186- if ( ! result . object ) {
187- throw new Error ( 'Clipping error: could not get object data.' ) ;
131+
132+ // Get the Box Model via CDP.
133+ // This returns the quads relative to the target's viewport.
134+ // We use the 'border' quad to include the border and padding in the screenshot,
135+ // matching the 'width' and 'height' properties which are also Border Box dimensions.
136+ const nodeBoxModel = await node . boxModel ( ) ;
137+ if ( ! nodeBoxModel ) {
138+ throw new Error ( `Unable to get box model of the node: ${ new Error ( ) . stack } ` ) ;
139+ }
140+ const nodeBorderQuad = nodeBoxModel . border ;
141+
142+ // Get Layout Metrics to account for the Visual Viewport scroll and zoom.
143+ const metrics = await node . domModel ( ) . target ( ) . pageAgent ( ) . invoke_getLayoutMetrics ( ) ;
144+ if ( metrics . getError ( ) ) {
145+ throw new Error ( `Unable to get metrics: ${ new Error ( ) . stack } ` ) ;
188146 }
189- const clip = ( JSON . parse ( ( result . object . value as string ) ) ) ;
190- const response = await node . domModel ( ) . target ( ) . pageAgent ( ) . invoke_getLayoutMetrics ( ) ;
191- const error = response . getError ( ) ;
192- const zoom = ! error && response . visualViewport . zoom || 1 ;
147+
148+ const scrollX = metrics . cssVisualViewport . pageX ;
149+ const scrollY = metrics . cssVisualViewport . pageY ;
150+
151+ // Calculate the global offset for OOPiFs (Out-of-Process iframes).
152+ // This accounts for the position of the target's frame within the main page.
153+ const { x : oopifOffsetX , y : oopifOffsetY } = await getOopifOffset ( node . domModel ( ) . target ( ) ) ;
154+
155+ // Assemble the final Clip.
156+ // The absolute coordinates are: Global (OOPiF) + Viewport Scroll + Local Node Position (Border Box).
157+ const clip = {
158+ x : oopifOffsetX + scrollX + nodeBorderQuad [ 0 ] ,
159+ y : oopifOffsetY + scrollY + nodeBorderQuad [ 1 ] ,
160+ width : nodeBoxModel . width ,
161+ height : nodeBoxModel . height ,
162+ scale : 1 ,
163+ } ;
164+
165+ // Apply Zoom factor.
166+ const zoom = metrics . cssVisualViewport . zoom ?? 1 ;
193167 clip . x *= zoom ;
194168 clip . y *= zoom ;
195169 clip . width *= zoom ;
@@ -210,3 +184,68 @@ export class ActionDelegate implements UI.ActionRegistration.ActionDelegate {
210184 return false ;
211185 }
212186}
187+
188+ /**
189+ * Calculate the offset of the "Local Root" frame relative to the "Global Root" (the main frame).
190+ * This involves traversing the CDP Targets for OOPiFs.
191+ */
192+ async function getOopifOffset ( target : SDK . Target . Target | null ) : Promise < { x : number , y : number } > {
193+ if ( ! target ) {
194+ return { x : 0 , y : 0 } ;
195+ }
196+
197+ // Get the parent target. If there's no parent (we are at root) or it's not a frame, we are done.
198+ const parentTarget = target . parentTarget ( ) ;
199+ if ( ! parentTarget || parentTarget . type ( ) !== SDK . Target . Type . FRAME ) {
200+ return { x : 0 , y : 0 } ;
201+ }
202+
203+ // Identify the current frame's ID to find its owner in the parent.
204+ const frameId = target . model ( SDK . ResourceTreeModel . ResourceTreeModel ) ?. mainFrame ?. id ;
205+ if ( ! frameId ) {
206+ return { x : 0 , y : 0 } ;
207+ }
208+
209+ // Get the DOMModel of the parent to query the frame owner element.
210+ const parentDOMModel = parentTarget . model ( SDK . DOMModel . DOMModel ) ;
211+ if ( ! parentDOMModel ) {
212+ return { x : 0 , y : 0 } ;
213+ }
214+
215+ // Retrieve the frame owner node (e.g. the <iframe> element) in the parent's document.
216+ const frameOwnerDeferred = await parentDOMModel . getOwnerNodeForFrame ( frameId ) ;
217+ const frameOwner = await frameOwnerDeferred ?. resolvePromise ( ) ;
218+ if ( ! frameOwner ) {
219+ return { x : 0 , y : 0 } ;
220+ }
221+
222+ // Get the content box of the iframe element.
223+ // This is relative to the parent target's viewport.
224+ const boxModel = await frameOwner . boxModel ( ) ;
225+ if ( ! boxModel ) {
226+ return { x : 0 , y : 0 } ;
227+ }
228+
229+ // content is a Quad [x1, y1, x2, y2, x3, y3, x4, y4]
230+ const contentQuad = boxModel . content ;
231+ const iframeContentX = contentQuad [ 0 ] ;
232+ const iframeContentY = contentQuad [ 1 ] ;
233+
234+ // Get the scroll position of the parent target to convert viewport-relative coordinates
235+ // to document-relative coordinates.
236+ const parentMetrics = await parentTarget . pageAgent ( ) . invoke_getLayoutMetrics ( ) ;
237+ if ( parentMetrics . getError ( ) ) {
238+ return { x : 0 , y : 0 } ;
239+ }
240+
241+ const scrollX = parentMetrics . cssVisualViewport . pageX ;
242+ const scrollY = parentMetrics . cssVisualViewport . pageY ;
243+
244+ // Recursively add the offset of the parent target itself (if it is also an OOPiF).
245+ const parentOffset = await getOopifOffset ( parentTarget ) ;
246+
247+ return {
248+ x : iframeContentX + scrollX + parentOffset . x ,
249+ y : iframeContentY + scrollY + parentOffset . y ,
250+ } ;
251+ }
0 commit comments