mirror <details> and checkbox state across iframe - javascript

I have an iFrame that loads its parent page within itself. So, two copies of the same page; one in an iFrame, one not.
I'm trying to mirror the state of <input type="checkbox"> checked/unchecked and <details> open/closed between the main page and the iFrame.
I have solved each partway (see // comments for the problems), firing on click events:
For the checkboxes, I have
let boxes = document.querySelectorAll('input[type="checkbox"]');
for (let box of boxes)) {
myIFrame.getElementById(box.id).checked = box.checked; // works
if (inIFrame) {parent.getElementById(box.id).checked = document.getElementById(box.id).checked; // doesn't work
}
(the condition inIFrame above is just shorthand for a test checking whether the page is loaded in an iFrame)
And for the <details>
let detailEls = document.querySelectorAll('details');
for (let i = 0; i < detailEls.length; i++) {
myIFrame.querySelectorAll('details')[i].open = querySelectorAll('details')[i].open; // works, but 1-click behind
}
But strangely, this lags one click behind. So if I click, click, click to open details A,B,C on the main page, only A,B will open in the iFrame -- and the next click, C will open.
In case it wasn't clear, here's the summary of my questions:
Why does the <details> state lag? It seems like the same strategy as for checkboxes, but the result is different.
Why does the checked state only mirror from the main page to the iFrame, but not vice versa?
Thanks!

Here is a reproduction that demonstrates the behavior you want, generally following your patterns:
https://stackblitz.com/edit/so-mirror-iframe?file=lib/script.js
It's hard to tell without your full code, but here are some things to consider:
If you're using the click event on the details, that's probably giving you the "lagging" behavior. It turns out that for details, the click event comes before its open property is updated. The one you just clicked is always read in the wrong state when your synchronizer runs.
Use the toggle event instead, this fires after the open property is updated.
Assuming its not a typo in the first code block, parent is a reference to the parent window, not its document.
Use parent.document.getElementById instead.

Ok, the logic that you use will not be able to mirror vice-versa as you would like it to, so I will make it on click bases ON BOTH WINDOW AND IFRAME
Here's my repl and here's a link(please open this in new tab)
Here's the concept:
I make arrays of the elements I want mirrored on both iframe AND window. These arrays are made in the same way so that the keys/indexes each relate to their equivalent in the separate window(window.boxes[0] is the first box in window and childWindow.boxes[0] is the first box in iframe)
Now the most important part is the async part you would see in the addEventListener blocks. you would see me awaiting a promise of a timeout that lasts 0 ms but how asynchronous functions like that work is that it would wait until it isn't blocking anything and THEN RUN. That's why it ignores that lag effect the detail bars give
window.html
<html><h2>WINDOW</h2>
<iframe id="iframe" width="500" height="300" src="/"></iframe>
<details>.</details>
<details>..</details>
<details>...</details>
<details>....</details>
<details>.....</details>
<input type="checkbox">Hm</input>
<input type="checkbox">Hmm</input>
<input type="checkbox">Hmmm</input>
<input type="checkbox">Hmmmm</input>
<input type="checkbox">Hmmmmm</input>
<script>(async()=>{
window.iframe=document.getElementById('iframe') //iframe
window.childWindow=iframe.contentWindow //window to iframe
window.waitFinish=async function(){ //needful waiting
await new Promise(r=>setTimeout(r,0))
}
window.boxes = [...document.querySelectorAll('input[type="checkbox"]')] //checkboxes
window.details = [...document.querySelectorAll('details')] //details
//wait for iframe to finish loading(if you're doing this manually by the time you begin it'd be finished loading so no worries)
await new Promise(r=>{
let s=setInterval(()=>{
if(typeof childWindow.details=="object"){
clearInterval(s); return r(1)
}
},0)
})
//window to iframe
boxes.forEach((box,index)=>{
box.addEventListener("click",async(ev)=>{
await waitFinish()
childWindow.boxes[index].checked=box.checked
})
})
details.forEach((detail,index)=>{
detail.addEventListener("click",async(ev)=>{
await waitFinish()
childWindow.details[index].open=detail.open
})
})
//iframe to window
childWindow.boxes.forEach((box,index)=>{
box.addEventListener("click",async(ev)=>{
await waitFinish()
window.boxes[index].checked=box.checked
})
})
childWindow.details.forEach((detail,index)=>{
detail.addEventListener("click",async(ev)=>{
await waitFinish()
window.details[index].open=detail.open
})
})
})()</script>
</html>
iframe.html
<html><h2>IFRAME</h2>
<i><a target="_blank" href="https://iframe-mirror.paultaylor2.repl.co">Full Page</a></i>
<details>.</details>
<details>..</details>
<details>...</details>
<details>....</details>
<details>.....</details>
<input type="checkbox">Hm</input>
<input type="checkbox">Hmm</input>
<input type="checkbox">Hmmm</input>
<input type="checkbox">Hmmmm</input>
<input type="checkbox">Hmmmmm</input>
<script>
window.boxes = [...document.querySelectorAll('input[type="checkbox"]')] //checkboxes
window.details = [...document.querySelectorAll('details')] //details
</script>
</html>

You made a tiny mistake.
Your code was:
for (let box of querySelectorAll('input[type="checkbox"]')) {
myIFrame.getElementById(box.id).checked = box.checked; // works
if (inIFrame) {parent.getElementById(box.id).checked = getElementById(box.id).checked; // doesn't work
}
when it needs to be
for (let box of querySelectorAll('input[type="checkbox"]')) {
myIFrame.getElementById(box.id).checked = box.checked; // works
if (inIFrame) {parent.getElementById(box.id).checked = document.getElementById(box.id).checked; // works now that it's fixed
}
You forgot the document object as the parent.

Related

jQuery form input field present even before document.ready function is called

I am facing a weird issue. I am relatively new to JavaScript jQuery.
When I refresh the page the address input field doesn't get cleared, while zip code and email fields do get cleared.
I tried $('#input_address').get(0).value='';
which clears the field. But I don't want it to happen when the user comes back from page 2 to page 1. Only on refresh should the fields be cleared.
The email and zip code works perfectly in both scenarios: refresh page and page2 to page1 navigation.
$(document).ready(function() {
console.log("doc ready function");
// $('#input_address').get(0).value='';
// togglePlaceholder($('#input_email').get(0));
// togglePlaceholder($('#input_zip').get(0));
togglePlaceholder($('#input_address').get(0));
$('input, select, textarea').each(
function() {
var val = $(this).val().trim();
if (val.length) {
$(this).addClass('sample');
}
});
$('input, select, textarea').blur(function() {
if ($(this).val())
$(this).addClass('sample');
else
$(this).removeClass('sample');
});
$('input, select, textarea').focus(function() {
console.log("focused");
if ($(this).val() == '') {
$(this).removeClass('invalid');
$(this).addClass('sample');
}
});
})
function togglePlaceholder(inputElement) {
var inputAttr = inputElement.getAttribute("placeholder");
inputElement.placeholder = "";
inputElement.onblur = function() {
this.placeholder = "";
}
inputElement.onfocus = function() {
this.placeholder = inputAttr;
}
}
.sample ~ label {
font-size: 1em;
top: -10px;
left: 0;
font-size: 1em;
color: #F47B20;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="input-field col s6 col-xs-12">
<input type="text" onblur="togglePlaceholder(this);" onfocus="togglePlaceholder(this);" placeholder="123 Example Street" id="input_address" />
<label for="input_address">Street Address</label>
</div>
So... you have two problems.
(1) Auto-completion is what refills the widgets automatically,
(2) You need to know what button was clicked to react accordingly.
Auto-Completion
In regard to the auto-completion, it most certainly happens right after the first set of scripts ran within the jQuery ready() function.
There are two ways to remove auto-completion, but really, I do not recommend either one, although I would imagine that you'll need to if your requirements are set in stones...
(a) Ask for the input widget to not even autocomplete
<input ... autocomplete="off" .../>
(b) Run your script with a timer so it happens after the auto-completion. Instead of initializing in the ready() function, you initialize in a sub-function that runs after a timer times out.
$(document).ready(function() {
setTimeout(function(){
// ...put your initialization here...
// the autocompletion data should have been taken care of at this point
}, 0);
});
Note that you can use a number large than 0 for the timeout delay, but in most cases 0 will work just fine to run the sub-function after releasing the current thread once and thus given the system time to work on the auto-completion and then call your function. With 0 it should be so fast that you should not even see the <input .../> tag flash.
Side note: you may also want to place the inner function in an actual function as in:
function init_stuff()
{
// ...your initialization code goes here...
}
$(document).ready(function() {
setTimeout(init_stuff, 0);
});
If you expect your initialization to continue to grow, this can be a lot cleaner long term.
Which button gets clicked
The next problem is to know whether that code should run or not. So you need an extra if() statement for that purpose.
There are several hacks on this stackoverflow page in that regard. However, I'm not exactly sure how you really know in the newly loaded page, that you had a Refresh or a Back button click.
From the code I see there, the loading of the page's content would 100% happen in AJAX and therefore you perfectly know which button was clicked, you just reimplemented the functionality. You'll have to search stackoverflow some more to find out how to do that. I strongly suggest that you write tests with one piece of functionality at a time to determine what is going on.
Note that will make having the initialization function separate quite useful since after reloading the page, you will be responsible to call that function (when you want the reset to happen) or not! In other words, if the Back button was clicked, load the HTML of the previous page (i.e. Page 1 in your example) and display it. Done. When clicking the Refresh button, load the HTML of the current page and call the reset function (it could also be that the Refresh is the default and you do not want to handle that button since it will anyway clear as expected.)
For a beginner, that's going to be an interesting piece of work!

Button.onclick automatically triggers, then will not trigger again

I have a script, which I'm using to try and display only one section of a webpage at a time.
function showMe(id){ clearPage(); changeDisplay(id, "block"); console.log(id)}
Currently, I'm using buttons to change which section is displayed.
var aBtn = document.getElementById("a-btn");
var otherBtn = document.getElementById("other-btn");
aBtn.onclick=showMe("a-btn-section-id");
otherBtn.onclick=showMe("other-btn-section-id");
However, when I load the page, the following happens:
I see the function attached to each button activate once in sequence in the console.
The page refuses to respond to further button inputs.
Testing with the console shows that showMe() and the functions it calls still all work properly. I'm sure I'm making a very basic, beginner mistake (which, hopefully, is why I can't find this problem when I Google/search StackOverflow/read event handling docs), but I'm at a loss for what that mistake is. Why would my script assume my buttons are clicked on load, and why won't it let me click them again?
You're calling the function an assign the value to onclick property instead of attach the function, try defining your onclick property as:
aBtn.onclick=function(){showMe("a-btn-section-id");};
otherBtn.onclick=function(){showMe("other-btn-section-id");};
Try the follow jsfiddle:
function showMe(id){ // some stuff..
console.log(id)
}
var aBtn = document.getElementById("a-btn");
var otherBtn = document.getElementById("other-btn");
aBtn.onclick=function(){showMe("a-btn-section-id");};
otherBtn.onclick=function(){showMe("other-btn-section-id");};
<input type="button" value="a-btn" id="a-btn"/>
<input type="button" value="other-btn" id="other-btn"/>
Hope this helps,

Making a basic Javascript calculator

jsfiddle demo
Bear with me, total newb here.
I'm trying to make a simple multiplication calculator, as a experimentation with Javascript.
The catch is that -
No libraries, just pure javascript.
Javascript must be unobtrusive.
Now, the problem arises, that it doesn't give the value out.
When I do this locally, answer has a value of NaN, and if you hit Submit it stays that way, BUT, if you press the back button, you see the actual result.
In the JSFiddle, much is not shown, except for the fact that it simply doesn't work.
Please tell me, is it even possible to make an unobtrusive calculator? How?
(PS. I was taking a bit of help from sciencebuddies, just to see basic syntax and stuff, but I found it can't be done without code being obtrusive)
I realize you're probably just getting started and don't know what to include, remove, and whatnot. But, good advice here, clearly label your elements so you can understand them, and pare it down to the smallest possible code you need for it to work (even less, so you can build it up).
Here is your code reworked:
HTML
<div>
<input type="text" id="multiplicand" value="4">
<input type="text" id="multiplier" value="10">
<button type="button" id="multiply">Multiply</button>
</div>
<p id="result">
The product is: <span id="product"> </span>
</p>
Javascript
window.onload = function(){
var button = el('multiply'),
multiplicand = el('multiplicand'),
multiplier = el('multiplier'),
product = el('product');
function el(id) {
return document.getElementById(id);
};
function multiply() {
var x = parseFloat(multiplicand.value) || 0,
y = parseFloat(multiplier.value) || 0;
product.innerHTML = x * y;
}
button.onclick = multiply;
};
http://jsfiddle.net/userdude/EptAN/6/
A slightly more sophisticated approach, with add/subtract/multiply/divide:
http://jsfiddle.net/userdude/EptAN/9/
You have to change the submit button so that it doesn't submit the form. Right now clicking "Submit" causes the form submits to the same page which involves a page reload.
Change the <input type="submit" id="submitt"> to <button type=button> and it should work.
You can probably do without the <form> element in the first place. That'll stop clicking enter in your text input from reloading the page.
Your example has a couple of problems:
The form still submits. After the JS changes the value, the submit will cause the page to reload, and that work you've done setting the answer value is wasted.
You're trying to do this stuff right away. In the header, none of the body has been parsed yet (and thus, the form elements don't even exist). You'll want to wait til the page is loaded.
The script hijacks window.onload. If you don't have any other scripts on the page, that's fine...but the whole point of unobtrusive JS (IMO) is that nothing breaks whether the script is there or not.
Fixed, we have something kinda like:
// Wrap this onload in an IIFE that we pass the old onload to, so we can
// let it run too (rather than just replacing it)
(function(old_onload) {
// attach this code to onload, so it'll run after everything exists
window.onload = function(event) {
// run the previous onload
if (old_onload) old_onload.call(window, event);
document.getElementById('Xintox').onsubmit = function() {
var multiplier = +this.multiplier.value;
var multiplicand = +this.multiplicand.value;
this.answer.value = multiplier * multiplicand;
return false; // keep the form from submitting
};
};​
})(window.onload);
Note i'm attaching the meat code to the form, rather than the button, because hitting Enter in either of the factor boxes will trigger a submit as well. You could still attach to the button if you wanted, and just add a submit handler that returns false. But IMO it's better this way -- that way the form works just the same with JS as without (assuming the script on the server fills in the boxes appropriately), except it won't require a round trip to the server.

How to move an iFrame in the DOM without losing its state?

Take a look at this simple HTML:
<div id="wrap1">
<iframe id="iframe1"></iframe>
</div>
<div id="warp2">
<iframe id="iframe2"></iframe>
</div>
Let's say I wanted to move the wraps so that the #wrap2 would be before the #wrap1. The iframes are polluted by JavaScript. I am aware of jQuery's .insertAfter() and .insertBefore(). However, when I use those, the iFrame loses all of its HTML, and JavaScript variables and events.
Lets say the following was the iFrame's HTML:
<html>
<head>
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript">
// The variable below would change on click
// This represents changes on variables after the code is loaded
// These changes should remain after the iFrame is moved
variableThatChanges = false;
$(function(){
$("body").click(function(){
variableThatChanges = true;
});
});
</script>
</head>
<body>
<div id='anything'>Illustrative Example</div>
</body>
</html>
In the above code, the variable variableThatChanges would...change if the user clicked on the body. This variable, and the click event, should remain after the iFrame is moved (along with any other variables/events that have been started)
My question is the following: with JavaScript (with or without jQuery), how can I move the wrap nodes in the DOM (and their iframe childs) so that the iFrame's window stays the same, and the iFrame's events/variables/etc stay the same?
It isn't possible to move an iframe from one place in the dom to another without it reloading.
Here is an example to show that even using native JavaScript the iFrames still reload:
http://jsfiddle.net/pZ23B/
var wrap1 = document.getElementById('wrap1');
var wrap2 = document.getElementById('wrap2');
setTimeout(function(){
document.getElementsByTagName('body')[0].appendChild(wrap1);
},10000);
This answer is related to the bounty by #djechlin
A lot of search on the w3/dom specs and didn't find anything final that specifically says that iframe should be reloaded while moving in the DOM tree, however I did find lots of references and comments in the webkit's trac/bugzilla/microsoft regarding different behavior changes over the years.
I hope someone will find anything specific regarding this issue, but for now here are my findings:
According to Ryosuke Niwa - "That's the expected behavior".
There was a "magic iframe" (webkit, 2010), but it was removed in 2012.
According to MS - "iframe resources are freed when removed from the DOM". When you appendChild(node) of existing node - that node is first removed from the dom.
Interesting thing here - IE<=8 didn't reload the iframe - this behavior is (somewhat) new (since IE>=9).
According to Hallvord R. M. Steen comment, this is a quote from the iframe specs
When an iframe element is inserted into a document that has a browsing context, the user agent must create a new browsing context, set the element's nested browsing context to the newly-created browsing context, and then process the iframe attributes for the "first time".
This is the most close thing I found in the specs, however it's still require some interpretation (since when we move the iframe element in the DOM we don't really do a full remove, even if the browsers uses the node.removeChild method).
Whenever an iframe is appended and has a src attribute applied it fires a load action similarly to when creating an Image tag via JS. So when you remove and then append them they are completely new entities and they refresh. Its kind of how window.location = window.location will reload a page.
The only way I know to reposition iframes is via CSS. Here is an example I put together showing one way to handle this with flex-box:
https://jsfiddle.net/3g73sz3k/15/
The basic idea is to create a flex-box wrapper and then define an specific order for the iframes using the order attribute on each iframe wrapper.
<style>
.container{
display: flex;
flex-direction: column;
}
</style>
<div class="container">
<div id="wrap1" style="order: 0" class="iframe-wrapper">
<iframe id="iframe1" src="https://google.com"></iframe>
</div>
<div id="warp2" style="order: 1" class="iframe-wrapper">
<iframe id="iframe2" src="https://bing.com"></iframe>
</div>
</div>
As you can see in the JS fiddle these order styles are inline to simplify the flip button so rotate the iframes.
I sourced the solution from this StackOverflow question: Swap DIV position with CSS only
Hope that helps.
If you have created the iFrame on the page and simply need to move it's position later try this approach:
Append the iFrame to the body and use a high z-index and top,left,width,height to put the iFrame where you want.
Even CSS zoom works on the body without reloading which is awesome!
I maintain two states for my "widget" and it is either injected in place in the DOM or to the body using this method.
This is useful when other content or libraries will squish or squash your iFrame.
BOOM!
Unfortunately, the parentNode property of an HTML DOM element is read-only. You can adjust the positions of the iframes, of course, but you can't change their location in the DOM and preserve their states.
See this jsfiddle I created that provides a good test bed. http://jsfiddle.net/RpHTj/1/
Click on the box to toggle the value. Click on the "move" to run the javascript.
This question is pretty old... but I did find a way to move an iframe without it reloading. CSS only. I have multiple iframes with camera streams, I dont like when they reload when i swap them. So i used a combination of float, position:absolute, and some dummy blocks to move them around without reloading them and having the desired layout on demand (resizing and all).
If you are using the iframe to access pages you control, you could create some javascript to allow your parent to communicate with the iframe via postMessage
From there, you could build login inside the iframe to record state changes, and before moving dom, request that as a json object.
Once moved, the iframe will reload, you can pass the state data into the iframe and the iframe listening can parse the data back into the previous state.
PaulSCoder has the right solution. Never manipulate the DOM for this purpose. The classic approach for this is to have a relative position and "flip" the positions in the click event. It's only not wise to put the click event on the body, because it bubbles from other elements too.
$("body").click(function () {
var frame1Height = $(frame1).outerHeight(true);
var frame2Height = $(frame2).outerHeight(true);
var pos = $(frame1).css("top");
if (pos === "0px") {
$(frame1).css("top", frame2Height);
$(frame2).css("top", -frame1Height);
} else {
$(frame1).css("top", 0);
$(frame2).css("top", 0);
}
});
If you only have content that is not cross-domain you could save and restore the HTML:
var htmlContent = $(frame).contents().find("html").children();
// do something
$(frame).contents().find("html").html(htmlContent);
The advantage of the first method is, that the frame keeps on doing what it was doing. With the second method, the frame gets reloaded and starts it's code again.
At least in some circumstances a shadow dom with slotting might be an option.
<template>
<style>div {outline:1px solid black; height:45px}</style>
<div><slot name="a" /></div>
<div><slot name="b" /></div>
</template>
<div id="shadowhost">
<iframe src="data:text/html,<button onclick='this.innerText+=`!`'>!</button>"
slot="a" height=40px ></iframe>
</div>
<button onclick="ifr.slot= (ifr.slot=='a') ? 'b' : 'a';">swap</button>
<script>
document.querySelector('#shadowhost').attachShadow({mode: 'open'}).appendChild(
document.querySelector('template').content
);
ifr=document.querySelector('iframe');
</script>
In response to the bounty #djechlin placed on this question, I have forked the jsfiddle posted by #matt-h and have come to the conclusion that this is still not possible.
http://jsfiddle.net/gr3wo9u6/
//this does not work, the frames reload when appended back to the DOM
function swapFrames() {
var w1 = document.getElementById('wrap1');
var w2 = document.getElementById('wrap2');
var f1 = w1.querySelector('iframe');
var f2 = w2.querySelector('iframe');
w1.removeChild(f1);
w2.removeChild(f2);
w1.appendChild(f2);
w2.appendChild(f1);
//f1.parentNode = w2;
//f2.parentNode = w1;
//alert(f1.parentNode.id);
}

Change iframe source from another iframe

Ok, I have 2 iframes inside a parent page (for whatever reason).
I have a navigation menu on parent page, which changes the source of iframe #1...
iFrame #1's job, is to display ANOTHER navigation menu... Like a subnavigation menu...
Now, how can I upon clicking an li inside iFrame #1, change the source of iframe #2? They're both on the same parent page...
Aside from failing miserably, I also get a warning from Chrome's Dev tools -
Unsafe JavaScript attempt to access frame with URL file:///C:/website/index.html from frame with URL file:///C:/website/news/navigator.html. Domains, protocols and ports must match.
Here's some code to make things slightly clearer:
The HTML
<!-- HTML for the parent page itself -->
<iframe id="frameone" src=""></iframe>
<iframe id="frametwo" src=""></iframe>
<button onclick="onenav('test.html')">Change 1st frame</button>
<!-- The following is the HTML that is loaded inside "frameone" -->
<button onclick="twonav('test2.html')">Change 2nd frame</button>
// Javascript
var one = document.getElementById('frameone');
var two = document.getElementById('frametwo');
function onenav(x){
one.src = x;
}
function twonav(y){
two.src = y;
}
To me, this makes sense, since this is all being executed on the parent page... On loading, I query the dev tools and I can see that both, 'one' and 'two' have frame elements... The first button works, the second one, doesn't...
Works for me when using parent.twonav
DEMO
var links = [
'javascript:\'<button onclick="parent.twonav(1)">Change 2nd frame</button>\'',
'javascript:\'Hello\''
];
var one, two;
window.onload=function() {
one = document.getElementById('frameone');
two = document.getElementById('frametwo');
}
function onenav(idx) {
one.src=links[idx];
}
function twonav(idx) {
two.src=links[idx];
}
How did you try to change the iframe source?
parent.document.getElementById('2').src = "the new url";
Did you try something like this? I assumed from your message that the id of the 2nd iframe is 2.

Categories