userscript: I lost some JS? - javascript

I do not understand what's going on...
I have a simple userscript, that add couple DIVs, css styles and JS functions in the pages I visit
In particular, I have one DIV that trigger a JS function with a onClick listener - this function is a "toggle" function (display/hide an other DIV):
function togglegm(etat) {
if (etat = 'on') {
document.getElementById('greasemky').style.display = 'block';
document.getElementById('greasemkytoggle').innerHTML = '';
} else if (etat = 'off') {
document.getElementById('greasemky').style.display = 'none';
document.getElementById('greasemkytoggle').innerHTML = '';
}
}
var script2 = d.createElement('script');
script2.appendChild(d.createTextNode(togglegm));
(d.body || d.head || d.documentElement).appendChild(script2);
The DIV "greasemkytoggle" only contains a link with a onClick that trigger "togglegm('on'), and my objective is that when togglegm(on) is executed, the innerHTML of this DIV becomes a trigger for togglegm(off).
Now the weird part... when I click on my DIV greasemkytoggle, the function togglegm(on) is perfectly executed (greasemky is displayed), and the innerHTML is perfectly changed with a link for "togglegm(off)", BUT if I click again, then nothing happens.
I looked at the source code, and discovered that my JS function just disappeared (that's why nothing happened on the last click)! Now, there is an empty function replacing my togglegm():
<script>
scriptHolderArray1
</script>
Do you understand that kind of behaviour...?
I found nothing online for that kind of situation...

GreaseMonkey runs under a much more security conscience set of rules.
Attach the event listeners using the proper DOM3 (addEventListener) method.
It is never a good idea (in user scripts or general scripting) to assign Javascript through innerHTML.
It is never a good idea to use the "javascript:" pseudo-protocol.

The problems are etat = 'on' and etat = 'off'.
If you want to set values, use
etat = 'on'
etat = 'off'
If you want to compare, use:
etat == 'on'
etat == 'off'
Moreover, href="javascript:return(false);" throws an error on Firefox because there is a return outside a function (SyntaxError: return not in function). You should do href="javascript:void(0);", or return false at the end of the onclick event.
Anyway, I don't understand very well what you are doing here:
var script2 = d.createElement('script');
script2.appendChild(d.createTextNode(togglegm));
(d.body || d.head || d.documentElement).appendChild(script2);
You have a function togglegm loaded to browser's memory by a <script> element.
Then, you create a new <script> with that function and append it to the document, in order to load it to browser's memory again (I guess).
Why?

Related

Calling two functions from same onclick failure

I have a problem when calling 2 JS functions that work fine separately. I am not well versed in JS: I took the scripts from some posts here and adapted them, and I cannot make them work together. My goal is to make appear the side panel and to change the color of a box within that panel when clicking on the related link in the main text (and to undo it by clicking again on it).
This is the function that I use for showing the side panel:
function showRightPanel() {
var elem = document.getElementById("right-panel");
if (elem.classList) {
console.log("classList supported");
elem.classList.toggle("show");
} else {
var classes = elem.className;
if (classes.indexOf("show") >= 0) {
elem.className = classes.replace("show", "");
} else {
elem.className = classes + " show";
}
console.log(elem.className);
}
}
And this is the function for changing the color:
var els = document.getElementsByClassName('change-color'),
target = document.getElementById('footnotes'),
changeColor = function() {
target.style.backgroundColor = this.getAttribute('data-color');
};
for(var i=els.length-1; i>=0; --i) {
els[i].onclick = changeColor;
}
And this is the html that calls both functions it:
<a href="#footnote1-chapter1" class="change-color" data-color="#E0FFC2"
onclick="showRightPanel();changeColor();"></a>
And this is the box that has to appear and change color:
<div id="footnotes"><p class="footnote" data-id="footnote1-chapter1">
</p></div>
Both scripts are in separate .js files that are referred to in the header, and I know this might be the problem for the second script, as it was mentioned that:
"You should put the javascript at the end of your body (just before ), or wrap it in a function listening load or DOMContentLoaded event (e.g. using addEventListener). If not, document.getElementById is executed before the element is loaded to the DOM, so it return null. – " in this post change background color of div.
But I do not know how to "wrap it in a function listening load or DOMContentLoaded event".
Can someone please help me?
Thank you for your help in advance!
You could wrap your code in an on load function, but the general consensus seems to suggest putting your scripts at the end of the body tag instead, according to this StackOverflow answer and the YUI team. This accomplishes the same thing as wrapping your code, and might fix your problem if it's related to DOM dependencies.
So I would recommend you put your scripts at the end of your body tag instead of in your head tag, and see if that helps.
As Nathan mentioned, your anchor has the attribute onclick as onclick="showRightPanel();changeColor();"
But your javascript replaces this attribute, see: els[i].onclick = changeColor;
So only changeColor function will be called.
[update]
As in the comments, the accepted answer:
els[i].onclick = function(){showRightPanel();changeColor.apply(this);}

setting onclick in javascript is not working

So I'm writing some function that is working with Facebook's API. I was previously setting the onclick attribute by taking the parent and saying something along the lines of parent.innerHTML += "<a onclick = 'test("+parameter+")'>Previous</a>" and that worked fine. But I wanted to make it safer and more to standard so it is styled as follows:
function myMethod(link){
...
FB.api(link, function(response){
...
if(value != null){
var prev = document.createElement("a");
prev.innerText = "previous";
prev.setAttribute("id", "previous");
//prev.onclick = function(){test(this);}; doesn't work here
document.getElementById("facebook-photos").appendChild(prev);
}
//some other code (loops and stuff) including this
for(...){
var container = document.createElement("div");
container.classList.add("container");
container.classList.add("thing");
container.onclick = function(){test(this);}; // works here
}
//
if(document.getElementById("previous")){
document.getElementById("previous").onclick = function(){test(this);}; //works here
}
}
}
yet for some reason whenever I try and use this, clicking the element does nothing. Inspecting the element shows no "onclick" field but displaying the element in the console shows that the onclick field is not null. Nothing is covering the element and I've tried it as a div and as a button. When I try and do document.getElementById("previous") earlier, it still doesn't work. Why does this happen? Is it just the asynchronous nature of Javascript? The assignment in the middle works even though its relatively soon after the creation of the element, but the one at the beginning does not.

Updating page with "document.write" removes formatting

I've been wondering if there is a way to prevent my functions hiding any current text/formatting.
<body>
<script type="text/javascript">
document.body.style.backgroundColor = "#F5F0EB";
var page = 0
function flip(){
document.write(page);
page++;
}
</script>
<input type=button value="Next" onClick="flip()">
</body>
When I press the button I lose the coloured background and the text appears. Is there a way to make the background stay and the text appear?
Yes, by not using document.write(). MDN explains it clearly:
Writing to a document that has already loaded without calling
document.open() will automatically perform a document.open call.
And about document.open() it says:
If a document exists in the target, this method clears it.
What you should be doing, is manipulate nodes in the DOM. For example, you could change the inner HTML of the body:
document.body.innerHTML += page;
Alternatively, you could create a text node and append it:
var textNode = document.createTextNode(page);
document.body.appendChild(textNode);
In both cases, the current document is not flushed, it is only modified.
This is happening because you're writing non-html to a document which should be html. As indicated by others, it may also be clearing your existing html. Instead of using document.write, you may want to append new elements to your document.
You can do that using the document.createElement function and document.appendChild function.
Here's what a quick Google search brought back:
http://www.dustindiaz.com/add-and-remove-html-elements-dynamically-with-javascript/
You are writing the page to document which is overwriting all over your HTML. Instead, write out the content to a DIV.
This should fix your background color problem as well.
Here is a JS Fiddle with an example.
<script type="text/javascript">
document.body.style.backgroundColor = "#F5F0EB";
var page = 0
function flip(){
document.getElementById("contentgoeshere").innerHTML=page;
page++;
}
</script>
<input type=button value="Next" onClick="flip()">
<div id="contentgoeshere">
</div>
Good luck.
As Mattias Buelens explained, document.write calls the open method, which clears the DOM, simply because -after loading- the document.close method is called automatically. There are a number of alternatives you can use, of course.
Use the innerHTML attribute of the body element, for example.
Using document.createElement and document.body.appendChild is an option, too
But perhaps it's worth taking into consideration that both methods have their downsides: using innerHTML allows you to inject badly formatted markup into the DOM, and could leave you vulnerable to XSS attacks.
Using document.createElement is slower (generally) and often requires more code, which in turn makes your script(s) less maintainable.
You could use something like this:
var flip = (function(tempDiv)
{//create a div once
var page = 0,
targetDiv = document.getElementById('contentgoeshere');//closure variables
//page is now no longer an evil global, and the target element into which
//we will inject new data is referenced, so we don't have to scan the DOM
//on each function call
return function(overridePage)//optional argument, just in case
{//actual flip function is returned here
overridePage = overridePage || page++;//new content
tempDiv.innerHTML = overridePage;//render in div that isn't part of the DOM
//any number of checks can go here, like:
if (tempDiv.getElementsByTagName('script').length > 0)
{
alert('Naughty client, trying to inject your own scripts to my site');
return;
}
targetDiv.innerHTML = tempDiv.innerHTML;
//or, depending on your needs:
targetDiv.innerHTML = tempDiv.innerText;//or the other way 'round
};
}(document.createElement('div')));
A couple of side-notes: as it now stands, this function won't work because the DOM has to be fully loaded for the closure to work. A quick fix would be this:
var flip;//undefined
window.onload = function()
{
flip = (function(tempDiv)
{
var page = 0,
targetDiv = document.getElementById('contentgoeshere');
return function(e)
{
tempDiv.innerHTML = e instanceof Event ? page++ : (e || page++);//ternary + logical or
//your [optional] checks here
targetDiv.innerHTML = tempDiv.innerHTML;
if (e instanceof Event)
{//this part is optional, but look it up if you want to: it's good stuff
e.returnValue = false;
e.cancelBubble = true;
if (e.preventDefault)
{
e.preventDefault();
e.stopPropagation();
}
}
return e;
};
}(document.createElement('div')));
//instead of using the HTML onclick attribute:
document.getElementById('buttonID').onclick = flip;//use as event handler
};
Note that window.onload causes memory leaks on IE<9, check this link for the solution to this issue

Conflict between Pinterest and Fancybox

I have a page that has both Fancybox and a Pinterest pin button. Both seem to work as they should, but when I close the Fancybox overlay I see the following JavaScript error:
Uncaught TypeError: Cannot read property 'data-pin-aha' of null
My Pinterest button renders as this:
<span class="PIN_1354830754034_pin_it_button_count" id="PIN_1354830754034_pin_count_0"><i></i>3</span>
Just for fun, my Pinterest button is being loaded asynchronously with this:
(function () {
window.PinIt = window.PinIt || { loaded: false };
if (window.PinIt.loaded) return;
window.PinIt.loaded = true;
function async_load() {
var s = document.createElement("script");
s.type = "text/javascript";
s.async = true;
if (window.location.protocol == "https:")
s.src = "https://assets.pinterest.com/js/pinit.js";
else
s.src = "http://assets.pinterest.com/js/pinit.js";
var x = document.getElementsByTagName("script")[0];
x.parentNode.insertBefore(s, x);
}
if (window.attachEvent)
window.attachEvent("onload", async_load);
else
window.addEventListener("load", async_load, false);
})();
And my Fancybox link:
<span>Watch Our Story</span>
Overall it's a pretty basic setup. Just for kicks I used the normal inline script tag for Pinterest, but got the same error.
Has anyone ever seen this error and know how to fix it?
I had a similar issue with the Pinterest button. It turned out to be a problem with Pinterest's pinit.js script.
I’ve reported the issue to Pinterest (sign in required), and they’re looking at it.
We were using the Pinterest button on a page that had another link on it with a click event assigned. The link's HTML was, roughly speaking, like this:
<span>Select</span>
The click event handler was like this:
$('.youCanClickThis').click(function (e) {
$(this).html('Selected');
});
This click handler was somehow causing an error in pinit.js (Uncaught TypeError: Cannot read property 'data-pin-log' of null).
I unminified https://assets.pinterest.com/js/pinit.js in order to trace the error more easily. It was showing up in this function in the unminified pinit.js:
get: function (b, c) {
var d = null;
return d = typeof b[c] === "string" ? b[c] : b.getAttribute(c)
},
Specifically, typeof b[c]. get() is called from getData(), and the value of b is passed straight through from whatever called getData(). That turned out to be this function:
click: function (b) {
if ((b = a.f.getEl(b || a.w.event)) && b !== a.d.b) {
if (!a.f.getData(b, "log")) b = b.parentNode;
This appears to fire whenever the user clicks on anything on a page that has a Pinterest button. The last bit - b.parentNode - is what caused us problems.
By that point, b has been assigned to the element that the click event originated on. In our case, that was the <span> inside the <a>. However, because our JavaScript replaced the contents of the <a> tag with text, the <span> was no longer part of the document, and thus no longer had a parentNode.
Thus b = b.parentNode caused b’s value to be null.
We worked around this by not removing the <span> from the document on click. However, it would be good if Pinterest could add a check to see if b.parentNode exists, or something.
(I don’t understand the Pinterest script well enough to know what the best fix would be, but it’s built with the assumption that the source of a click event will always have a parentNode, which, as this demonstrates, isn’t a safe assumption).
I'm getting the Uncaught TypeError: Cannot read property 'data-pin-log' of null error on that page.
A hacky way to fix the issue would be to add an afterShow event that adds the data-pin-log attribute to the close button
$(".watch").fancybox({
helpers: {
media: {}
},
afterShow: function(e){
$('.fancybox-close').attr('data-pin-log', false);
}
});
This would also work for the OP's problem by replacing data-pin-log with data-pin-aha
We were having the same issue using jQuery Modal. We solved the problem by adding an onHide handler that hid the modal window, waited a few seconds, and then deleted the modal window. Not the prettiest solution, but it worked.
Fix for this is pushing today; very sorry for the trouble. If you run into difficulties with pinit.js in the future, please let us know here:
https://github.com/pinterest/widgets/

Javascript - JQuery - clearInterval/setInterval - iframe cycle won't stop on click

I'm fairly new to Javascript in general, and I cobbled together a small script from things found mostly on this site to try to get a small iframe to cycle through a bunch of links, which it does brilliantly. However, I also want it to stop cycling when the user clicks on the iframe or any of its contents.
Here is the code I have so far. There is only one iframe on the HTML page.
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
<script type="text/javascript" charset="utf-8">
<!--
var sites = [
"side/html5.html",
"side/silverlight.html",
"side/wordpress.html",
"side/mysql.html",
"side/php.html",
"side/css3.html"
];
var currentSite = sites.length;
var timerId;
var iframeDoc = $("iframe").contents().get(0);
$(document).ready(function ()
{
var $iframe = $("iframe").attr("src","side/html5.html");
timerId = setInterval(function()
{
(currentSite == 0) ? currentSite = sites.length - 1 : currentSite = currentSite -1;
$iframe.attr("src",sites[currentSite]);
}, 4000);
$(iframeDoc).bind('click', function()
{
clearInterval(timerId);
});
});
//-->
</script>
</head>
<body>
<sidebar>
<iframe name="sideframe" src="side/html5.html"></iframe>
</sidebar> ..etc..
Please help, I am trying to learn Javascript as fast as I can but as far as I can see, it should work, but it really doesn't.
Thanks for any help you can give me, it's really appreciated.
EDIT:
Okay, I've got a new script now, mostly based off of Elias' work, but it doesn't work either. I've pinned it down in Firebug and it's saying that the timerCallback.currentSite value IS updating properly, though I can't find the $iframe's src value to check for it explicitly. As far as I can tell, it is updating the variables properly, it's just not updating the iframe properly. Am I calling/setting the iframe correctly in this code? Also, is the linked in jquery library sufficient for my purposes? I'm a little lost of all these libraries to link to...
<script type="text/javascript" src="http://code.jquery.com/jquery.js"></script>
<script type="text/javascript" charset="utf-8">
<!--
var sites =
[
"side/html5.html",
"side/silverlight.html",
"side/wordpress.html",
"side/mysql.html",
"side/php.html",
"side/css3.html"
];
var $iframe = $("iframe").attr("src","side/html5.html");
function timerCallback()
{
timerCallback.currentSite = (timerCallback.currentSite ? timerCallback.currentSite : sites.length) -1;
$iframe.attr("src",sites[timerCallback.currentSite]);
$($('iframe').contents().get('body')).ready(function()
{
$(this).unbind('click').bind('click',function()
{
var theWindow = (window.location !== window.parent.location ? window.parent : window);
theWindow.clearInterval(theWindow.timerId);
});
});
}
var timerId = setInterval(timerCallback, 4000);
//-->
</script>
If I were you, I'd play it safe. Since you say you're fairly new to JS, it might prove very informative.
function timerCallback()
{
timerCallback.currentSite = (timerCallback.currentSite ? timerCallback.currentSite : sites.length) -1;
$iframe.attr("src",sites[timerCallback.currentSite]);
}
var timerId = setInterval(timerCallback, 4000);
$($('iframe').contents().get('body')).unbind('click').bind('click', function()
{
var theWindow = (window.location !== window.parent.location ? window.parent : window);
theWindow.clearInterval(theWindow.timerId);
});
Now I must admit that this code is not tested, at all. Though I think it provides a couple of things to get you on your way:
1) the interval is set using a callback function, because it's just better for tons of reasons
1b) in that Callback, I took advantage of the fact that functions are objects, and created a static var, that is set to either the length of your sites array (when undefined or 0), in both cases 1 is substracted
2) jQuery's ,get() method returns a DOM element, not a jquery object, that's why I wrapped it in $(), a new jQ obj, giving you the methods you need.
3) since you're manipulating the dom inside the iFrame, it's best to unbind events you want to bind
4) inside the iFrame, you don't have direct access to the parent window, where your interval is.
You might want to read up on how to deal with iFrames, because they can be a bit of a faff from time to time
EDIT:
David Diez is right, easiest way around this is to incorporate the binding in the timeout function:
function timerCallback()
{
timerCallback.currentSide = ...//uncanged
//add this:
$($('iframe').contents().get('body')).ready(function()
{
$(this).unbind('click').bind('click',function()
{
//this needn't change
});
});
}
In theory, this should bind the click event to the body after it has been loaded
Edit2
I've been messing around a bit, you could keep your code, as is. just add a function:
function unsetInterval()
{
window.clearInterval(window.timerId);
}
and add this to your setInterval function:
$('#idOfIframe').load(function()
{
var parentWindow = window.parent;
$('body').on('click',function()
{
parentWindow.clearInterval();
});
});
this will get triggered as soon as the iFrame content is loaded, and bind the click event and unset the timer, like you wanted to
I think your code is not working because of this
var iframeDoc = $("iframe").contents().get(0);
This could be getting the header of the iFrame because you are saving the iframeDoc value to the first child of the iframe, the head tag, actually if you have more than 1 iframe in your window iframeDoc would be undefined because $("iframe") gets all the iframes of your document.
BTW your currentSite value condition is wrong, you asign the same value for both conditions
Now I give you an example:
<iframe id="myFrame" src="http://www.google.com/"></iframe>
and the script:
<script type="text/javascript" src="http://code.jquery.com/jquery.js"></script>
<script type="text/javascript">
var sites = [
"site1",
"site2"
];
var myFrame = document.getElementsByTagName('iframe')[0];
var currentSite = myFrame.getAttribute('src');
var timerId;
var myFrameDoc = window.frames[0].document;
$(document).ready(function()
{
myFrame.setAttribute('src', 'side/html5.html');
timerId = setInterval(function()
{
//WRONGG
(currentSite == 0) ? currentSite = sites.length - 1 : currentSite = currentSite -1;
myFrame.setAttribute('src', sites[currentSite]);
$(myFrame).off("click").on("click", clearInterval(timerId));
}, 4000);
//Won't work because you are trying to get events from an inside of a iframe
//$(myFrameDoc).on("click", clearInterval(timerId));
//This may work
$(myFrameDoc).off("click").on("click", clearInterval(timerId));
});
</script>
When you try to track the events of an iframe you have to be carefull because an iframe contains a totally different document for javascript purprouses so basically you have to get inside the new document, unbind the events you need to use, and bind them again against your functionality, as #Elias points out. but be aware that you are changing constantly the src of your iframe, so if yu really need to do that you will have to separate the code that unbinds and binds again your clearInterval, and for that matter maybe $.on() could work for you.
EDIT: Calling to the iframe should work this way IF the src of the iframe is inside the same domain, with the same port and with the same protocol:
var myFrameDoc = window.frames[0].document;
I Added a new variable because we want to bind and unbind the click event to the iframe's document, not to the iframe, we use for that the window.frames collection property, but modern browsers throw an exception and denies acces if you try to access to a frame and you are not on the same domain with using the same port and the same protocol...
Additionaly the use of $.on() and $.off() instead of $.bind and $.unbind() is because the first ones are the new ones and despite we are not using it here, they are capable of watch constantly the DOM for new elements to bind if added; that could be useful to this case if this code still doesn't work. If that is the case you can still change this:
var myFrameDoc = window.frames[0].window;
and this:
$(myFrameDoc).off("click", "document").on("click", "document", clearInterval(timerId));
This will re-bind the event handler to new documents additions. Not tested but could work.

Categories