Persistent variable on iframe? - javascript

I have a <div> being dynamically created, and it contains an <iframe>. The <iframe> may close itself, at which point the <div> is removed.
So far I have:
var div = document.createElement('div'), ifr = document.createElement('iframe');
// some styles and stuff here, including ifr.src
ifr.contentWindow.container = div; // Note that domains are the same
// within the iframe's code, possibly a "close" link or after completing an operation
container.parentNode.removeChild(container);
It works. But only if the page within the iframe is the one that was there to start with. If a link is clicked to another page, window.container is no longer defined.
I know I can use window.name to store data persistent to a window, but that it limited to data that can be serialised. To my knowledge, you can't serialise a DOM node, other than by assigning it an ID and storing that. I would like to avoid such arbitrary IDs, so if anyone can suggest a better solution I would be very grateful.

Use this code:
//At the frame:
var parentFrames = parent.document.getElementsByTagName("iframe");
for(var i=parentFrames.length-1; i>=0; i--){
if(parentFrames[i].contentWindow == window) {
parentFrames[i].parentNode.removeChild(parentFrames[i]); //Removes frame
//Add an extra `.parent` at each side of the expression to remove the `div`.
break;
}
}

Pages loaded into your <iframe> can use "window.parent" to get to the containing page. Thus, instead of keeping some "magic" reference in the <iframe>, keep it on the containing page and have the "child" pages look for it.
function closeMe() { // inside the iframe page
window.parent.frameContainer.removeChild(window.parent.removableFrame);
}
In addition to "parent", the "top" property of "window" references the top-most context when there's a chain of windows (frames) longer than just one step (so, an <iframe> in an <iframe> etc).

Related

How to capture iframe nested within Shadow DOM's "#shadow-root (open)" element?

I need to inject an element inside an <iframe>, after a <button> within this iframe is clicked.
This <iframe>exists within a page (e. g. somewebpage.com) that is not under my control.
The <iframe>'s content belongs to the same domain as the main document's body/DOM structure.
To achieve this I need to use a Content Script file from a Chrome Extension as per the below V3 manifest's "content_scripts" configuration:
"content_scripts": [
{
"type": "module", // I added this items "type", "run_at", "all_frames"
"run_at": "document_end", // and "match_origin_as_fallback" hoping they
"all_frames": true, // would help.
"match_origin_as_fallback": true,
"js": [
"foreground.js" // File containing the pure JS code
],
"matches": [
"https://somewebpage.com/*" // The page where I want to inject the element
]
The extension's content script "foreground.js" file contains the code that should detect the iframe, listen to the click event, and then inject the element within the iframe after click is detected.
After the page is fully loaded, when I inspect the elements on this page, it basically looks like this:
<body>
<macroponent>
#shadow-root (open) <!-- Shadow DOM element. I believe this is what is my main obstacle -->
<div>
<sn-canvas-appshell-root>
<sn-canvas-appshell-layout>
<sn-polaris-layout>
<iframe id="myIframe"> <!-- to access button I need to find the iframe 1st -->
<button id="myButton">TAKE</button> <!-- I need to get that click event -->
</iframe>
</sn-polaris-layout>
</sn-canvas-appshell-root>
</sn-canvas-appshell-root>
</div>
</macroponent>
</body>
I have tried a lot of different approaches and none were able to find the <iframe>. The error messages on the console are returning the getElementById as null and getElementsByTagName as undefined.
As a standard method I tried this with some variations like window.onload:
document.onreadystatechange = () => {
if (document.readyState === 'complete') {
let iframe = document.getElementsByTagName('iframe')[0];
iframe.contentDocument.getElementById("myButton").addEventListener("click", function () {
alert("Hurray!");
// Injected element code here
});
}
I have also tried some more advanced solutions like this:
function searchFrame(id) { // id = the id of the wanted (i)frame
var result = null, // Stores the result
search = function (iframes) { // Recursively called function
var n; // General loop counter
for (n = 0; n < iframes.length; n++) { // Iterate through all passed windows in (i)frames
if (iframes[n].frameElement.id === id) { // Check the id of the (i)frame
result = iframes[n]; // If found the wanted id, store the window to result
}
if (!result && iframes[n].frames.length > 0) { // Check if result not found and current window has (i)frames
search(iframes[n].frames); // Call search again, pass the windows in current window
}
}
};
search(window.top.frames); // Start searching from the topmost window
return result; // Returns the wanted window if found, null otherwise
}
Source:
Get element value inside iframe which is nested inside Frame in javascript?
In my opinion, this issue persists perhaps because the <iframe> is a child to a "Shadow DOM" element, which makes the method of finding the <iframe> different from the standard.
After realizing that probably the issue is that the <iframe> is a child to a "Shadow DOM" element, I tried this approach from another question:
this.windows = this.shadowRoot.getElementsByTagName('app-window')
Source:
GetElementById from within Shadow DOM
Or even this one:
(although I suspect it might be a bit overkill for my use case)
customElements.define("my-component", class extends HTMLElement {
constructor() {
super().attachShadow({mode:"open"}).innerHTML = `<slot></slot>`;
}
})
const shadowDive = (
el,
selector,
match = (el, root) => {
console.warn('match', el, root);
},
root = el.shadowRoot || el
) => {
root.querySelector(selector) && match(root.querySelector(selector), root);
[...root.querySelectorAll("*")].map(el => shadowDive(el, selector, match));
}
shadowDive(document.body, "content"); // note optional parameters
Source:
How to select element tag from shadow root
And again I am getting the same null or undefined errors.
But I think that perhaps I am on the right track now that I know that the <iframe> is nested within a "Shadow DOM" element.
Perhaps there is a way to bypass or respect the "Shadow DOM" and get to that <iframe> but to be honest I am out of ideas.
Thank you
A shadowRoot doesn't have getElementsByTagName.
ShadowDOM frames aren't exposed in the global window or frames (it's the same thing BTW)
customElements.define doesn't work inside content scripts (due to "world isolation").
You can use querySelector + contentDocument:
const el = document.querySelector('macroponent').shadowRoot.querySelector('iframe');
const elInner = el.contentDocument.querySelector('div');
Note that you don't need all_frames or match_origin_as_fallback here because the iframe is same-origin, hence it's directly accessible from the main document's content script as shown above.
One pitfall is that modern sites generate the contents of the page dynamically, often depending on a network response, which can happen long time after all your content scripts ran. In this case you can use MutationObserver to detect it or simply re-check periodically inside setInterval.

Reset iframe's window object

I have an iframe, without any src specified.
I change the content dynamically with javascript using this code:
let doc = Paste.render_iframe.contentWindow.document
doc.open()
doc.write(Paste.get_value())
doc.close()
Which works. But the only problem is that if some javascript was used before, it stays inside the window object, and redefining them again causes errors.
I haven't found a way to reset/replace the iframe's window object.
What I did before was create a new iframe and replace the old one.
I could also try to change the src and then change the content.
The problem with those is that, 1) replacing iframes very often seems expensive, 2) I would have to deal with race conditions, having to check when iframe is fully loaded to accept new content.
So my question is, is there a way to replace or reset the iframe's window object without replacing it or modifying the src?
Guess I'm going to try changing the src dynamically and waiting for the load event to change the content.
Paste.setup_render = function()
{
Paste.render_iframe.addEventListener("load", function()
{
if(Paste.render_mode)
{
Paste.do_render()
}
})
}
Paste.reset_render_iframe = function()
{
Paste.render_iframe.src = `about:blank?t=${Date.now()}_${Paste.get_random_string(4)}`
}
Paste.render = function()
{
Paste.reset_render_iframe()
}
Paste.do_render = function()
{
let doc = Paste.render_iframe.contentWindow.document
doc.open()
doc.write(Paste.get_value())
doc.close()
}

Button is always null even after window is loaded [duplicate]

I'm trying to attach an eventlistener to the "click" event of a button on a page in an IFrame. The page in the iframe belongs to the same domain as the parent window.
window.frames["iframe_Id"].document.getElementById("button_id").addEventListener("click",functionToRun,false);
The above sample is not working for me. It could just be a a syntax error in my actual code but I wanted to see if this is the general idea.
Note: I found plenty of examples on adding the click event to the entire Iframe but I'm not sure it works the same for buttons within the Iframe.
Using jQuery:
$("#iframe_Id").contents("#button_id").click(functionToRun);
Or without jQuery:
document.getElementById("iframe_Id").contentWindow.document.getElementById("button_id").addEventListener("click", functionToRun, false);
This will only work if the iframe meets the same origin policy.
window.frames is an array-like object, so its elements can be accessed by indexes only.
You should loop through them, and check their id, e.g.:
var frame; for(i = 0; i < frames.length; i++) {
frame = frames[i];
if (frame.id === 'iframe_id') {
frame.document
.getElementById("button_id")
.addEventListener("click",functionToRun,false);
}
}
Have you tried executing the event in the "capturing" vs. "bubbling" phase?
document.getElementById("iframe_Id")
.document.addEventListener("click", functionToRun, true);
Note the true instead of false as the final parameter.
window.frames["iframe_Id"].contentWindow.document.getElementById("button_id").addEventListener("click",functionToRun,false);
^
The contentWindow property.
From the DOM iframe element, scripts can get access to the window
object of the included HTML page via the contentWindow property.

Does anyone know a way to print the loaded page HTML after DOM is completed?

I'm looking for a way to read the source code of a page after it finished loading and inspect the code to see if it contains a specific text.
I found this reference but this only returns the text visible in the page and not the whole HTML code.
For instance, if the html source code is:
<html>
<header>
<header>
<body>
<p> This is a paragraph</a>
<body>
</html>
I want the script to print exactly the same thing.
Your help is appreciated.
I think you are over-complicating this problem. You don't need to "print" the page's HTML or "inspect the code".
In a comment, you said:
Check if page contains an iframe [and] Display a message if the iframe is found
You can just use DOM traversal functions to examine the DOM.
Try something like this:
window.addEventListener('load', function() {
if(document.getElementsByTagName('iframe').length){
console.log('Found an iframe');
}
});
Or with jQuery:
$(function() {
if($('iframe').length){
console.log('Found an iframe');
}
});
That's so simple, you can use this method to run a script after a page is fully loaded window.onload
function load(){
console.log(document.getElementsByTagName('html')[0].innerHTML);
}
window.onload = load;
For further explanations, check this post
Do like this, call this function on load
Fiddle Demo
function printBody() {
// store oiginal content
var originalContents = document.body.innerHTML;
// get the outer html of the document element
document.body.innerText = document.documentElement.outerHTML;
// call window.print if you want it on paper
window.print();
// or put it into an iframe
// var ifr = document.createElement('iframe');
// ifr.src = 'data:text/plain;charset=utf-8,' + encodeURI(document.documentElement.outerHTML);
// document.body.appendChild(iframe);
// a small delay is needed so window.print does not get the original
setTimeout(function(){
document.body.innerHTML = originalContents;
}, 2000);
}
Src: Print <div id=printarea></div> only?
Assuming that by 'print' you don't actually mean to transfer it to a paper copy, you can add some script like:
window.addEventListener('load', function() {
var content = document.documentElement.innerHTML,
pre = document.createElement('pre'),
body = document.body;
pre.innerText = content;
body.insertBefore(pre, body.firstChild);
});
What this does, step by step is:
window.addEventListener('load', function() > Wait for the page to be fully loaded and then execute the function
content = document.documentElement.innerHTML > store the actual page source in the content variable (document.documentElement refers to the 'root'-node, usually <html> in html documents
pre = document.createElement('pre') > create a new <pre>-element
body = document.body > create a reference to the <body> element
pre.innerText = content > assign the HTML-structure we've stored earlier as text to the <pre>-element
body.insertBefore(pre, body.firstChild) > put the <pre>-element (now with contents) before any other element in the body (usually on top of the page).
This leaves you with the entire source (as it was before creating the <pre>-element containing the source) on top of you page.
Edit: Added <iframe> workflow
It was not clear to me you actually wanted to target an <iframe>, so here's how to do that (using a naive approach, more on that further on):
window.addEventListener('load', function() {
var iframeList = document.getElementsByTagName('iframe'),
body = document.body,
content, pre, i;
for (i = 0; i < iframeList.length; ++i) {
content = iframeList[i].documentElement.innerHTML;
pre = document.createElement('pre');
pre.innerText = content;
body.insertBefore(pre, body.firstChild);
}
});
why is this approach naive?
There is a thing called Same-Origin-Policy in javascript, which prevents you from accessing <iframe>-content which if the contents do not originate from the same domain as the page containing the <iframe>.
There are several ways to take this into consideration, you could wrap the inside of the for-loop in try/catch-blocks, though I prefer to use a more subtle approach by not even considering <iframes> which do not match the Same-Origin-Policy.
In order to do this, you can swap the getElementsByTagName method with the querySelectorAll method (please note the compatibility table at the bottom of that page, see if it matches your requirements).
The querySelectorAll accepts a valid CSS selector and will return a NodeList containing all matching elements.
A simple selector to use would be
'iframe[src]:not([src^="//"]):not(src^="http")' which selects all iframe with a src attribute which does not start with either // or http
Disclaimer: I never use a <base>-tag (which changes all relative paths within the HTML) or refer to the current website using a path containing the domain, so the example CSS-selector does not consider these aberrations.
Can you use :not()
IE9 or better
Can you use document.querySelector(All)
IE8 or better (in order to use with :not(), IE9 or better)
hover/click the boxes above to show the spoiler

Is it possible to send javascript & CSS from a child frame up the DOM to the Parent

I am inserting content into the LMS (learning management system) Desire2Learn and wish to implement the jquery lightbox clone colorbox.
The LMS uses a variety of frames and only allows for html content to be placed in a designated 'content' frame. I would like to use javascript to sent 3 include files to the top level of the DOM from the bottom level.
I have previously attempted the following with no success:
<script type="text/javascript">
/* locate the parent frame */
var par = parent;
while (par !== par.parent) {
par = par.parent;
}
/* inject the css and scripts in the top frame */
var scr = par.document.createElement('script');
scr.setAttribute('type','text/javascript');
scr.setAttribute('src','jquery-1.4.3.min.js');
par.document.head.appendChild(scr);
var lnk = par.document.createElement('link');
lnk.setAttribute('type','text/css');
lnk.setAttribute('rel','stylesheet');
lnk.setAttribute('href','colorbox.css');
par.document.head.appendChild(lnk);
var scr = par.document.createElement('script');
scr.setAttribute('type','text/javascript');
scr.setAttribute('src','jquery.colorbox-min.js');
par.document.head.appendChild(scr);
</script>
Am i at all heading in the right direction or is there simply no way do what i am asking?
The code works for me, except that the colorbox JS is loaded before JQuery, and hence a call to JQuery fails. I put the last script element writing inside a setTimeout and it got rid of that error. To verify it had loaded fully in the parent frame, I put in the console:
$(document).ready(function() { alert('hi'); });
and it popped the alert.
If you're trying to get anchors to open the lightbox from the inner frame to the outer one, it might be easier to alter the colorbox code to target the top-level frame rather than try to work outward-in.

Categories

Resources