I have a content script that injects an iframe into a webpage.
content.js
var iframe = document.createElement('iframe');
iframe.id = "frame";
iframe.style.cssText = "position:fixed;top: 15px;right: 15px;width: 250px;height: 245px;overflow: hidden;background-color:#FFFFFF;border-radius: 5px;";
iframe.src = chrome.runtime.getURL('frame.html');
document.body.appendChild(iframe);
The iframe displays some text values, has a submit and a close button.
part of frame.html
<div class="header">
<span class="close">Name</span>
<span class="close-btn" id="close-btn">×</span>
</div>
<div class="details-container">
<span class="label">First Name : </span>
<span id="fname" type="text" ></span>
</div>
<div class="details-container">
<span class="label">Last Name : </span>
<span id="lname" type="text" /></span>
</div>
<div class="btn-details-container">
<button class="copy" id="copy-name">Copy</button>
</div>
frame.html has frame.js linked to it.
I want to do 2 things here.
Problems:
1)I don't know how to propogate click event on frame.html to content script to close iframe(Unable to establish communication between frame.js and content.js)
2)Not able to set span.textContent for #fname and #lname because frame.js is not able to read webpage DOM.
Use chrome.tabs.sendMessage to communicate with the owner tab of the iframe, which can be retrieved using chrome.tabs.getCurrent inside the iframe.
content.js:
var FRAME_URL = chrome.runtime.getURL('frame.html');
var iframe = document.createElement('iframe');
iframe.src = FRAME_URL;
document.body.appendChild(iframe);
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
switch (msg.cmd) {
case 'close':
iframe.remove();
iframe = null;
break;
case 'getData':
sendResponse([
['fname', document.querySelector('.web.page.selector.for.fname').textContent],
['lname', document.querySelector('.web.page.selector.for.lname').textContent],
]);
break;
}
});
iframe.js:
tellParent({cmd: 'getData'}, data => {
for (const [id, val] of data) {
document.getElementById(id).textContent = val;
}
});
document.querySelector('.close-btn').onclick = () => {
tellParent({cmd: 'close'});
};
function tellParent(msg, callback) {
chrome.tabs.getCurrent(tab => {
chrome.tabs.sendMessage(tab.id, msg, {frameId: 0}, callback);
});
}
Initiate the port using chrome.tabs.connect in the iframe, then use it in the content script.
content script:
let framePort;
chrome.runtime.onConnect.addListener(port => {
if (port.name === 'frame') {
// global framePort can be used by code that will run in the future
framePort = port;
port.postMessage({foo: 'bar'});
}
});
// add iframe element and point it to chrome.runtime.getURL('iframe.html')
//...........
iframe script:
chrome.tabs.getCurrent(tab => {
const port = chrome.tabs.connect(tab.id, {name: 'frame', frameId: 0});
port.onMessage.addListener(msg => {
if (msg.foo === 'bar') {
console.log(msg);
}
});
});
It's super fast and supports binary data types like Blob or ArrayBuffer but requires certain care to avoid interception by the web page:
window[0]
src
, instead navigate its inner location
using a random secret in the url parameters so that its URL won't be spoofed by the web page or other extensions which used chrome.dom.openOrClosedShadowRoot.// content.js
(async () => {
const port = await makeExtensionFramePort('/iframe.html');
port.onmessage = e => {
console.log('from iframe:', e.data);
};
port.postMessage(123);
port.postMessage({ foo: bar });
port.postMessage(new Blob(['foo']));
})();
async function makeExtensionFramePort(path) {
const secret = Math.random().toString(36);
const url = new URL(chrome.runtime.getURL(path));
url.searchParams.set('secret', secret);
const el = document.createElement('div');
const root = el.attachShadow({mode: 'closed'});
const iframe = document.createElement('iframe');
iframe.hidden = true;
root.appendChild(iframe);
(document.body || document.documentElement).appendChild(el);
await new Promise((resolve, reject) => {
iframe.onload = resolve;
iframe.onerror = reject;
iframe.contentWindow.location.href = url;
});
const mc = new MessageChannel();
iframe.contentWindow.postMessage(secret, '*', [mc.port2]);
await new Promise(cb => { mc.port1.onmessage = cb; });
mc.port1.onmessage = null;
return mc.port1;
}
// iframe.html:
<script src="iframe.js"></script>
// iframe.js
let port;
window.onmessage = e => {
if (e.data === new URLSearchParams(location.search).get('secret')) {
window.onmessage = null;
port = e.ports[0];
port.onmessage = onContentMessage;
port.postMessage(null); // resolve makeExtensionFramePort
}
};
function onContentMessage(e) {
console.log('from content:', e.data);
port.postMessage('ok');
}
Modification: a direct two-way port between the content script and the extension's service worker by using navigator.serviceWorker
messaging in the iframe:
// iframe.js
let port;
window.onmessage = e => {
if (e.data === new URLSearchParams(location.search).get('secret')) {
window.onmessage = null;
navigator.serviceWorker.ready.then(swr => {
swr.active.postMessage('port', [e.ports[0]]);
});
}
};
// background.js
self.onmessage = e => {
if (e.data === 'port') {
e.ports[0].onmessage = onContentMessage;
e.ports[0].postMessage(null); // resolve makeExtensionFramePort
}
}
function onContentMessage(e) {
// prints both in the background console and in the iframe's console
console.log('from content:', e.data);
port.postMessage('ok');
}