I am creating a chrome extension that requires me to send a message from my event page to a content script that is running on a specific frame
The message passing operation is invoked when a context menu option is clicked. Said option is only available when the element in focus is categorized as editable
by the chrome.contextMenus
API. I need to send it to the innermost frame in which the element is contained. (so I can modify its contents by accessing the document.activeElement
object)
As of Chrome 41, you are able to send a message to a specific frame by filling the value of the frameID
parameter of the options
argument to the chrome.tabs.sendMessage()
function.
However, Chrome 41 is currently in beta, so I obviously cant expect this feature to be compatible with my users' browsers.
So, I planned of sending a message to all of the frames in the tab (by registering listeners on a content script that had the all_frames
property set to true) . Once inside the content script I could check to see if this was the right frame or not by comparing the value of window.location.href
with the value I am expecting.
It appears as If I had a misconception about the way that either event listeners or message passing works. The documentation makes it pretty clear that chrome.tabs.sendMessage()
is supposed to send a message to every frame in the tab, but it appears as if there can only be a single chrome.runtime.onMessage()
event listener invoked per message, even if there are many listeners registered. Which one actually gets invoked appears to be somehow dictated by the initial loading of the page.
This can be demonstrated with a simplified version of my extension:
manifest.json:
{
"manifest_version":2,
"name":"Demo",
"description":"This is a demonstration",
"version":"1.0",
"permissions":[
"<all_urls>",
"contextMenus"
],
"background": {
"page":"background.html",
"persistent":false
},
"content_scripts":[{
"matches":["<all_urls>"],
"js":["content.js"],
"all_frames":true
}]
}
background.html:
<!DOCTYPE text/html>
<html>
<head>
<textarea id="temp"></textarea>
<script src="event.js"></script>
</head>
<body>
</body>
</html>
event.js:
chrome.contextMenus.onClicked.addListener(requestHandler);
chrome.contextMenus.create({
"title":"Click Me!!",
"contexts":["editable"],
"id":"1"
});
function requestHandler(info, tab)
{
chrome.tabs.sendMessage(tab.id, {"destination":info.frameUrl});
//this should get sent to every frame in the tab
}
content.js:
chrome.runtime.onMessage.addListener(handleRequest);
alert("Hi, i'm: " + window.location.host);
function handleRequest(message)
{
alert("You tried to send a message to: " + message.destination);
alert("I am: " + window.location.href);
}
If you visit a page with many frames, like youtube you should notice the following behavior:
You get many alerts upon the initial loading of the page, indicating that there are many frames being loaded (all with listeners)
When you click the context menu option, only a single alert pops up, indicating that only a single frame received the message
The frame that receives the message remains constant throughout multiple clicks of the context option, but is likely to change when you refresh the page. This indicates that the order in which the frames are initially loaded determines which frame is going to get your message.
So, how can I assure that the frame I am trying to talk with actually gets my message, without resorting to the frameID
option that's only available in chrome 41?
At first I thought I could send a response back from the content script, indicating whether or not the message went to the right place. I could just keep doing this in a while loop in the event page until I got back a message telling me to stop, but like I said earlier, the message always seems to go to the same frame until the page is refreshed, so this approach does nothing for me.
Something else I have noticed is that if there are two frames on a page with the same origin, like plus.google.com for example, both frames will respond to the message, if that is the frame set that the message happened to get delivered to. So, the recipient seems to be based on origin rather than by some unique frame ID.
Here is a link to a video I took of the phenomena I am speaking of:
Notice that I got alerts initially from: youtube.com, accounts.google.com, plus.google.com and client6.google.com, but only client6 got my message
Your chrome.runtime.onMessage
event listener calls alert
. This method blocks the runtime until the dialog is closed. For some reason, this also prevents Chrome from correctly triggering the onMessage events in other frames (reported as crbug.com/456482).
A work-around is to use console.log
for debugging instead of alert
, or wrap the code within setTimeout
to asynchronously process the onMessage
event. E.g.
chrome.runtime.onMessage.addListener(function(message) {
setTimeout(function() {
alert('Message: ' + message);
});
});