Public
Edited
Dec 13, 2023
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
api = loadApi({ input: inputApiKey, invalidation })
Insert cell
Insert cell
Insert cell
Insert cell
{
const screenshotContainer = htl.html`<div class="screenshots"><p>Waiting for a new URL...</p></div>`;
const root = htl.html`<div>${screenshotContainer}
<style>
.screenshots {
display: flex;
flex-direction: column;
max-width: 500px;
max-height: 500px;
overflow: auto;
padding: 0.25em;
border: 1px solid gray;
}
.screenshots > img {
display: block;
width: 100%;
}
</style>
</div>`;
yield root;

let prevSubmit = 0;
for await (const info of Generators.input(urlSubmitForm)) {
const { url, focused, width, height, timeout, submit } = await info;
if (prevSubmit === submit) continue;
if (!url) {
screenshotContainer.innerHTML = "";
screenshotContainer.append(
htl.html`<p>URL is empty. Waiting for a new one...</p>`
);
return;
}
prevSubmit = submit;
let win = await api.windows.create({
url,
focused,
type: "popup",
width,
height
});
const close = async () => {
if (win) {
const id = win.id;
win = null;
await api.windows.remove(id);
}
};
invalidation.then(close);
try {
const windowId = win.id;
const tabId = win.tabs[0].id;
screenshotContainer.innerHTML = "";
for await (let {
imgUrl,
top,
left,
right,
bottom
} of makeFullPageScreenshots({
windowId,
tabId,
timeout: 500
})) {
const img = await newImage(imgUrl);
const croppedImg = await cropImage({
img,
top,
left,
right,
bottom
});
screenshotContainer.appendChild(croppedImg);
}
} catch (error) {
console.error(error);
} finally {
await close();
}
}
}
Insert cell
// This method returns an Async Iterator for a series of screenshots of the specified target window.
// Parameters of this method:
// @param options - options object
// @param options.windowId - id of the window to take a snapshot
// @param options.tabId - id of the tab
// @param options.timeout - a timeout to wait before the first screenshot
// Each returned screenshot has the following structure:
// * imgUrl - URL fo the image with base64-encoded content
// * top - top position of this screenshot
// * left - left screenshot position
// * right - right position
// * bottom - bottom screenshot position
async function* makeFullPageScreenshots({ windowId, tabId, timeout = 500 }) {
// Initial step: measure the size of the loaded page
const { result: windowInfo } = await api.custom.injectScript(
{ tabId },
{
args: [{ timeout }],
func: async function ({ timeout = 0 } = {}) {
// Await until the page is loaded
await untilLoaded();
// Create a fixed element and cover with it the page content.
// It is required to block user's interaction during the scroll
const div = document.createElement("div");
Object.assign(div.style, {
backgroundColor: "rgba(255,255,255,0)",
zIndex: 100000,
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0
});
const body = document.body;
body.appendChild(div);

// Await for the content loading before measuring the document size
await new Promise((resolve) => {
timeout
? setTimeout(resolve, timeout)
: requestAnimationFrame(resolve);
});

// Get the page parameters to return
const { x, y, top, right, bottom, left, width, height } =
body.getBoundingClientRect();
const { innerHeight, innerWidth } = window;
const info = {
x,
y,
top,
right,
bottom,
left,
width,
height,
window: {
innerHeight: window.innerHeight,
innerWidth: window.innerWidth
},
body: {
clientWidth: body.clientWidth,
clientHeight: body.clientHeight,
scrollHeight: body.scrollHeight,
scrollWidth: body.scrollWidth
}
};
return info;

async function untilLoaded() {
return new Promise((resolve, reject) => {
if (document.readyState === "complete") {
resolve();
} else {
document.addEventListener("DOMContentLoaded", function onLoad() {
resolve();
document.removeEventListener("DOMContentLoaded", onLoad);
});
}
});
}
}.toString()
}
);
console.log("* [windowInfo]", windowInfo);

const scrollHeight = windowInfo.body.scrollHeight;
const windowHeight = windowInfo.window.innerHeight;
const bodyWidth = windowInfo.body.clientWidth;

for (
let pos = 0, prevPos = 0;
pos < scrollHeight;
prevPos = pos, pos += windowHeight
) {
if (pos > 0) {
await scrollTo({ tabId }, pos);
}
// We have to wait at least 500ms because the default
// MAX_CAPTURE_VISIBLE_TAB_CALLS_PER_SECOND value is 2
// (2 screenshots per second)
await Promises.delay(500);

const imgUrl = await api.tabs.captureVisibleTab(windowId, {
format: "png"
});
const top = windowHeight - Math.min(scrollHeight - pos, windowHeight);
const bottom = windowHeight;
const left = 0;
const right = bodyWidth;
yield {
imgUrl,
top,
right,
bottom,
left
};
}

async function scrollTo(target, pos) {
return await api.custom.injectScript(target, {
args: [pos],
func: async function (pos) {
window.scrollTo(0, pos);
}.toString()
});
}
}
Insert cell
async function cropImage({ img, left, top, bottom, right }) {
const width = right - left;
const height = bottom - top;
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, left, top, width, height, 0, 0, width, height);
const url = ctx.canvas.toDataURL();
return await newImage(url);
}
Insert cell
async function newImage(url) {
return new Promise((resolve, reject) => {
try {
const img = document.createElement("img");
img.src = url;
img.addEventListener("load", () => resolve(img));
img.addEventListener("error", reject);
} catch (error) {
reject(error);
}
});
}
Insert cell
import { loadApi, newApiKeyInput } from "@kotelnikov/webrun-devtools"
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more