UNPKG

chrome-devtools-frontend

Version:
252 lines (214 loc) • 9.51 kB
// Copyright 2016 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* eslint-disable @devtools/no-imperative-dom-api */ import type * as Common from '../../core/common/common.js'; import * as Root from '../../core/root/root.js'; import * as SDK from '../../core/sdk/sdk.js'; import type * as Protocol from '../../generated/protocol.js'; import * as EmulationModel from '../../models/emulation/emulation.js'; import * as UI from '../../ui/legacy/legacy.js'; import {DeviceModeView} from './DeviceModeView.js'; import type {InspectedPagePlaceholder} from './InspectedPagePlaceholder.js'; let deviceModeWrapperInstance: DeviceModeWrapper; export class DeviceModeWrapper extends UI.Widget.VBox { private readonly inspectedPagePlaceholder: InspectedPagePlaceholder; private deviceModeView: DeviceModeView|null; private readonly toggleDeviceModeAction: UI.ActionRegistration.Action; private showDeviceModeSetting: Common.Settings.Setting<boolean>; private constructor(inspectedPagePlaceholder: InspectedPagePlaceholder) { super(); this.inspectedPagePlaceholder = inspectedPagePlaceholder; this.deviceModeView = null; this.toggleDeviceModeAction = UI.ActionRegistry.ActionRegistry.instance().getAction('emulation.toggle-device-mode'); const model = EmulationModel.DeviceModeModel.DeviceModeModel.instance(); this.showDeviceModeSetting = model.enabledSetting(); this.showDeviceModeSetting.setRequiresUserAction(Boolean(Root.Runtime.Runtime.queryParam('hasOtherClients'))); this.showDeviceModeSetting.addChangeListener(this.update.bind(this, false)); SDK.TargetManager.TargetManager.instance().addModelListener( SDK.OverlayModel.OverlayModel, SDK.OverlayModel.Events.SCREENSHOT_REQUESTED, this.screenshotRequestedFromOverlay, this); this.update(true); } static instance(opts: { forceNew: boolean|null, inspectedPagePlaceholder: InspectedPagePlaceholder|null, } = {forceNew: null, inspectedPagePlaceholder: null}): DeviceModeWrapper { const {forceNew, inspectedPagePlaceholder} = opts; if (!deviceModeWrapperInstance || forceNew) { if (!inspectedPagePlaceholder) { throw new Error( `Unable to create DeviceModeWrapper: inspectedPagePlaceholder must be provided: ${new Error().stack}`); } deviceModeWrapperInstance = new DeviceModeWrapper(inspectedPagePlaceholder); } return deviceModeWrapperInstance; } toggleDeviceMode(): void { this.showDeviceModeSetting.set(!this.showDeviceModeSetting.get()); } isDeviceModeOn(): boolean { return this.showDeviceModeSetting.get(); } captureScreenshot(fullSize?: boolean, clip?: Protocol.Page.Viewport): boolean { if (!this.deviceModeView) { this.deviceModeView = new DeviceModeView(); } this.deviceModeView.setNonEmulatedAvailableSize(this.inspectedPagePlaceholder.element); if (fullSize) { void this.deviceModeView.captureFullSizeScreenshot(); } else if (clip) { void this.deviceModeView.captureAreaScreenshot(clip); } else { void this.deviceModeView.captureScreenshot(); } return true; } private screenshotRequestedFromOverlay(event: Common.EventTarget.EventTargetEvent<Protocol.Page.Viewport>): void { const clip = event.data; this.captureScreenshot(false, clip); } update(force?: boolean): void { this.toggleDeviceModeAction.setToggled(this.showDeviceModeSetting.get()); const shouldShow = this.showDeviceModeSetting.get(); if (!force && shouldShow === this.deviceModeView?.isShowing()) { return; } if (shouldShow) { if (!this.deviceModeView) { this.deviceModeView = new DeviceModeView(); } this.deviceModeView.show(this.element); this.inspectedPagePlaceholder.clearMinimumSize(); this.inspectedPagePlaceholder.show(this.deviceModeView.element); } else { if (this.deviceModeView) { this.deviceModeView.exitHingeMode(); this.deviceModeView.detach(); } this.inspectedPagePlaceholder.restoreMinimumSize(); this.inspectedPagePlaceholder.show(this.element); } } } export class ActionDelegate implements UI.ActionRegistration.ActionDelegate { handleAction(context: UI.Context.Context, actionId: string): boolean { switch (actionId) { case 'emulation.capture-screenshot': return DeviceModeWrapper.instance().captureScreenshot(); case 'emulation.capture-node-screenshot': { const node = context.flavor(SDK.DOMModel.DOMNode); if (!node) { return true; } async function captureClip(): Promise<void> { if (!node) { return; } // Resolve to a remote object to ensure the node is alive in the context. const object = await node.resolveToObject(); if (!object) { return; } // Get the Box Model via CDP. // This returns the quads relative to the target's viewport. // We use the 'border' quad to include the border and padding in the screenshot, // matching the 'width' and 'height' properties which are also Border Box dimensions. const nodeBoxModel = await node.boxModel(); if (!nodeBoxModel) { throw new Error(`Unable to get box model of the node: ${new Error().stack}`); } const nodeBorderQuad = nodeBoxModel.border; // Get Layout Metrics to account for the Visual Viewport scroll and zoom. const metrics = await node.domModel().target().pageAgent().invoke_getLayoutMetrics(); if (metrics.getError()) { throw new Error(`Unable to get metrics: ${new Error().stack}`); } const scrollX = metrics.cssVisualViewport.pageX; const scrollY = metrics.cssVisualViewport.pageY; // Calculate the global offset for OOPiFs (Out-of-Process iframes). // This accounts for the position of the target's frame within the main page. const {x: oopifOffsetX, y: oopifOffsetY} = await getOopifOffset(node.domModel().target()); // Assemble the final Clip. // The absolute coordinates are: Global (OOPiF) + Viewport Scroll + Local Node Position (Border Box). const clip = { x: oopifOffsetX + scrollX + nodeBorderQuad[0], y: oopifOffsetY + scrollY + nodeBorderQuad[1], width: nodeBoxModel.width, height: nodeBoxModel.height, scale: 1, }; // Apply Zoom factor. const zoom = metrics.cssVisualViewport.zoom ?? 1; clip.x *= zoom; clip.y *= zoom; clip.width *= zoom; clip.height *= zoom; DeviceModeWrapper.instance().captureScreenshot(false, clip); } void captureClip(); return true; } case 'emulation.capture-full-height-screenshot': return DeviceModeWrapper.instance().captureScreenshot(true); case 'emulation.toggle-device-mode': DeviceModeWrapper.instance().toggleDeviceMode(); return true; } return false; } } /** * Calculate the offset of the "Local Root" frame relative to the "Global Root" (the main frame). * This involves traversing the CDP Targets for OOPiFs. */ async function getOopifOffset(target: SDK.Target.Target|null): Promise<{x: number, y: number}> { if (!target) { return {x: 0, y: 0}; } // Get the parent target. If there's no parent (we are at root) or it's not a frame, we are done. const parentTarget = target.parentTarget(); if (!parentTarget || parentTarget.type() !== SDK.Target.Type.FRAME) { return {x: 0, y: 0}; } // Identify the current frame's ID to find its owner in the parent. const frameId = target.model(SDK.ResourceTreeModel.ResourceTreeModel)?.mainFrame?.id; if (!frameId) { return {x: 0, y: 0}; } // Get the DOMModel of the parent to query the frame owner element. const parentDOMModel = parentTarget.model(SDK.DOMModel.DOMModel); if (!parentDOMModel) { return {x: 0, y: 0}; } // Retrieve the frame owner node (e.g. the <iframe> element) in the parent's document. const frameOwnerDeferred = await parentDOMModel.getOwnerNodeForFrame(frameId); const frameOwner = await frameOwnerDeferred?.resolvePromise(); if (!frameOwner) { return {x: 0, y: 0}; } // Get the content box of the iframe element. // This is relative to the parent target's viewport. const boxModel = await frameOwner.boxModel(); if (!boxModel) { return {x: 0, y: 0}; } // content is a Quad [x1, y1, x2, y2, x3, y3, x4, y4] const contentQuad = boxModel.content; const iframeContentX = contentQuad[0]; const iframeContentY = contentQuad[1]; // Get the scroll position of the parent target to convert viewport-relative coordinates // to document-relative coordinates. const parentMetrics = await parentTarget.pageAgent().invoke_getLayoutMetrics(); if (parentMetrics.getError()) { return {x: 0, y: 0}; } const scrollX = parentMetrics.cssVisualViewport.pageX; const scrollY = parentMetrics.cssVisualViewport.pageY; // Recursively add the offset of the parent target itself (if it is also an OOPiF). const parentOffset = await getOopifOffset(parentTarget); return { x: iframeContentX + scrollX + parentOffset.x, y: iframeContentY + scrollY + parentOffset.y, }; }