I am building a Chrome extension that creates an iFrame inside any HTML page (i.e. any page being viewed in the browser window).
The iFrame is "injected" into the HTML page by the Chrome extension's content script. The resulting HTML looks like this:
<html>
<head>..</head>
<body>..</body>
<iframe id="myIframe">...</iframe>
</html>
After the content script adds the iFrame to the main page's DOM it goes on to populate the iframe with content by manipulating its own DOM. I use the Zurb CSS to style the iframe content.
And this all works fine.
I am now trying to add the Zurb Joyride to the content of the iframe, and it is this that I cannot get to work.
My manifest's content_scripts declaration looks like this:
"content_scripts": [{
"matches": ["*://*/*"],
"js": ["js/externalJS/jquery-2.1.4.min.js",
"js/externalJS/foundation.min.js",
"js/content_script.js"],
"run_at": "document_end"
}],
I suspect the iframe itself needs to have access to the Zurb scripts, so as well as foundation.min.js being loaded by the manifest as part of the extension, my content script also adds the relevant tags inside the iframe (by manipulating the DOM). My understanding is that the key elements within the iframe are:
- tags for Zurb JS files
- An object to attach the joyride to (id="testjoyride")
- The joyride content itself
- The initialisation call to foundation()
And so, with this in mind, I have the manifest's web_accessible_resources declaration looking like this:
"web_accessible_resources": [
"js/externalJS/jquery-2.1.4.min.js",
"js/externalJS/vendor/modernizr.js",
"js/externalJS/vendor/fastclick.js",
"js/externalJS/foundation.min.js",
"css/foundation.css",
"css/app.css"
],
And then use the content script to build up the following HTML inside the iframe:
<iframe id="myIframe">
<html>
<head>
<!-- the foundation style sheet (which works fine already) -->
<link type="text/css" rel="stylesheet" href="chrome-extension://extension_id/css/foundation.css">
<!-- the first two scripts -->
<script src="chrome-extension://extension_id/js/externalJS/vendor/modernizr.js"></script>
<script src="chrome-extension://extension_id/js/externalJS/jquery-2.1.4.min.js"></script>
</head>
<body>
<!-- a test object to attach the joyride too -->
<input type="submit" id="testjoyride">
<!-- now the rest of the iframe content -->
... content ...
<!-- the other two scripts -->
<script src="chrome-extension://extension_id/js/externalJS/vendor/fastclick.js"></script>
<script src="chrome-extension://extension_id/js/externalJS/foundation.min.js"></script>
<!-- default joyride code from foundation.zurb.com -->
<ol class="joyride-list" data-joyride="">
<li data-id="testjoyride" data-class="custom so-awesome" data-text="Next" data-prev-text="Prev">
<h4>Stop #1</h4>
</li>
<li data-button="end" data-prev-text="Prev">
<h4>Stop #3</h4>
</li>
</ol>
</body>
</html>
</iframe>
Then, finally, when all of the above content is attached to the iframe (and the iframe, of course, is attached to the parent page's DOM) I call foundation(), from the content_script.
I think I need to call it like this: $(window.frames.myIframe.contentWindow.document).foundation('joyride', 'start');
i.e. passing in the document object of the iframe, not the parent page **
So I do all of that, and nothing happens. The joyride does not appear, nor does the HTML get transformed into the auto-generated output (as described here).
I have created a test case in a simple HTML page and it worked fine, I tried to move that test page into an iframe and the joyride stopped working. I therefore suspect, although without much confidence, that the issues lies with the iframe, not the Chrome extension.
** Note, I place this call right at the end of all the DOM manipulation. After I have added the new HTML I've created to the iframe's DOM, and after creating all my listeners etc. So I'm confident the scripts and the joyrides should be "there" by that point. However, if I stick an alert() just before the call to foundation('joyride','start'), the alert displays with an empty iframe behind it, and only after I dismiss the alert does the iframe populate. Whether that's important, or just a strange effect of the alert, I don't know.
Thanks to some helpful comments, I managed to get this working. The exact same solution got QTip2 working as well. So this is a solution for:
I will do my best to describe it below. I am self-taught and still a newbie, so my apologies for any confusing terminology or divergences from best practice.
Solution Overview
I ended up with three levels to my Chrome Extension:
The Manifest
Although the content script (content_script.js) controls all of the injection, it never actually runs any of the 3rd party JS packages itself. They are run by the user's webpage. Therefore my manifest's content_scripts declaration looks like this:
Because the web page needs to access the 3rd party scripts, it must be given permission. It also needs access to our custom iframe script:
Content Script: Inject Joyride HTML into the iframe
The content script creates an iframe element (id="myIframe") and appends it to the user's webpage. It then goes on to programatically fill it with content.
Some of that content will become the anchors for the joyride: HTML elements with their ids set to joyrideStop1, joyrideStop2, etc:
And the content script also builds up the joyride HTML, as per the Zurb docs, at the bottom of the iframe body.
My custom CSS class is simply:
This makes the little pointy arrow on the joyride tooltip disappear. I had significant problems trying to get the joyride to display nicely inside the small iframe. Ideally I would like them to anchor to elements inside the iframe, but display as though they are part of the main page. In the end this seemed either too hard or impossible, so I just removed the nub and dodged the issue. This is a display issue, and only a problem for small iframes, so not a core part of this solution.
Content Script: Inject JS into the iframe
To get Joyride to work, we need to inject the 3rd party scripts and the custom iframe_script.js into the iframe. This is an example of the code from content_script.js for one of the 3rd party scripts:
NB. This part of the solution corrects one of my original mistakes. The scripts do not seem to initialise if you simply append text to innerHTML like so:
body.innerHTML += '<script src=".."></script>;'So I repeat the above code for all the scripts. In the iframe head I put:
And at the bottom of the iframe body I put:
In that order.
Iframe Script: initialise
When iframe_script.js initialises, I immediately get it to add a message listener (so the content script can communicate with it) and then send a message to the content script telling it it's ready (the content script already has its own message listener set up, the code for which is not detailed here):
NB. I struggled to make sure the iframe's listener was set up before my content_script sent its first message to the iframe. In the end this solution (waiting for a message back from the iframe) seemed to work best, although I suspect there may be a better way
Content Script: tell the iframe to activate the joyride
Now that the content script knows the iframe listeners are ready, it can tell it to activate the Zurb joyride (and/or the QTips). I do this by sending a message back to the iframe.
Obviously it would be simpler to activate the joyride directly from the iframe script, without all these messages bouncing back and forth. But that's the whole point: it's the content script that knows whether or not the joyride should even be displayed, let alone all the other config details. Keeping control with the content script (and, ultimately, with the background script) makes for a much tidier implementation over all.
Iframe Script: activate Joyride
My iframe script's message handler receives the message from the content script and calls this function:
NB. I have an outstanding issue here. This can sometimes run before JQuery is loaded, resulting in an error ("$ is not declared"). I have played with window.setTimeout in an effort to resolve this, and it works nearly all the time... but it's not 100%
And that's it!
There's a matching activate function for the QTips, with the appropriate content injected into the iframe earlier. Just like with the joyride, I struggled with the rendering of the Qtips inside the small iframe. QTip2 is more powerful and the docs suggest there are solutions.