Convert a command from Javascript to Greasmonkey - javascript

I am trying to add a character counter to a page, on this page i enter in three values and it returns a large string in the innerHTML of a div with the ID of 'AnswerBoxID', now i want my script to obviously count the number of characters in it to do this i have written
var submit=document.getElementsByClassName('Row3');
function countChars(){
count = document.getElementById('AnswerBoxID').innerHTML.length;
document.title ="Chars "+count+"/160";
}
Which returns a ROUGH approximate of the chars, when i then paste it into an editor or something else that counts chars i get a different result again, counting with this method gets within 5 chars of what other things are reporting (specifically notepad++).
BUT my biggest problem is I have been unable to get countChars() to update
when the value of document.getElementById('AnswerBoxID').innerHTML updates, in javascript I overcame that using the following code
var submit=document.getElementsByClassName('Row3');
for (i=0; i<submit.length; i++){
submit[i].firstChild.setAttribute('onclick','countChars()');
}
After reading GM Pitfalls 2 i then modified my approach to the following
for (i=0; i<submit.length; i++){
submit[i].firstChild.addEventListener('click',countChars(),true);
}
But it still doesnt work!
And before anyone asks yes I do define the count variable before the function. I don't really mind the mostly accurate length thing I would prefer it to be more precise but I do really want to add onclick elements that run countChars() to the submit buttons.

You seem to add the event handler wrong.
elm.setAttribute('onclick','countChars()');
would set an attribute, and eval 'countChars()' in the global scope when the element is clicked. Yet, Greasemonkey scripts run sandboxed to their own global object, and your declared function "countChars" is not available to the eval.
elm.addEventListener('click',countChars(),true);
executes the function immediately and adds the return value as a handler. To make it work, just remove the brackets and pass the function itself. See also element.addEventListener

Greasemonkey scripts run sandboxed. That means, the script runs in the page's onload event then is removed. Calling your Greasemonkey script's countChars() function when the form is submitted will return an undefined error, as your function object is no longer present in the DOM.
Here's my simplified contentEval function (based on GM wiki's Content Script Injection's function):
function contentEval(source) {
var script = document.createElement('script');
script.setAttribute("type", "text/javascript");
script.textContent = source;
document.head.appendChild(script);
}
You can also set the text property instead of textContent for compatibility with older IE versions, thanks RogG!
Put that function in your GM script, it will serve to append any function you need to user later to the DOM's head.
Pass your countChars function as a parameter to the contentEval function:
contentEval(function countChars() {
var count = document.getElementById('AnswerBoxID').textContent.length;
document.title ='Chars '+count+'/160';
});
This way, your countChars function will be placed inside a script element appended to the document's head, which will be accessible after the GM script's execution time ends.
If you want to check a demo of the code above, use a DOM Inspector in this fiddle. It creates a new script element which is appended to the (in JSfiddle's case, the iframe's) document's head, meaning it will be accessible whenever you need it.
You could also simply attach the function to the unsafeWindow, but you should avoid using unsafeWindow unless strictly necessary, and note that unsafeWindow is not supported in Google Chrome.
Use this page for reference: http://wiki.greasespot.net/Content_Script_Injection
Also use the .textContent method to get the text content of an element (and its descendants) as noted by RobG.

In GreaseMonkey, you should be able to use:
var count = document.getElementById('AnswerBoxID').textContent.length;

Related

Does JavaScript bookmark(javascript:) work with YouTube?

I want to write a js code which will un-translate video names
It is copied from title to video block name, but without 10 last chars(without "- YouTube").
And when I try this code (I know that code must be inline but it's for greater review)
javascript:var tttl = document.createElement('script');
document.getElementsByTagName("body")[0].appendChild(tttl);
tttl.innerText = "document.getElementsByClassName(\"style-scope ytd-video-primary-info-renderer\")[5].innerText = document.title.slice(0,-10);";
In browser console it works, but with bookmark (add bookmark and in address paste my code) it isn't working correctly.
Based on your question, it seems like you want to execute a javascript code while browsing youtube.com.
For doing so you need to wrap your code inside a self-executing function.
javascript:(function(){var tttl = document.createElement('script');
document.getElementsByTagName("body")[0].appendChild(tttl);
tttl.innerText = "document.getElementsByClassName(\"style-scope ytd-video-primary-info-renderer\")[5].innerText = document.title.slice(0,-10);";})();
Edit 1:
The reason why this code is wrapped inside a self-executing function is that it defines the scope for the executing code and doesn't try to write on DOM with the return value of the last statement.
Example code for better understanding:
javascript:var name;name="Ashish";
As the statement name="Ashish" returns a value in javascript this will be printed on the browser if executed through address bar.
Same is the case with the last statement in your code.

Run script after appending it to the HTML

I have a string with HTML:
var str = '<div><p>Examplee</p></div><script>alert("testing!")</script>';
and then I append it to the HTML:
document.body.innerHTML += str;
and the content is appended but the script does not execute, is there a way to force it?
First, a caveat: Naturally, only do this with scripts you trust. :-)
There are a couple of ways. Basically, you need to:
Get the text of the script, and
Run it
Getting the text
One option is just to go ahead and add it, then find it and grab its text:
document.body.innerHTML += str;
var scripts = document.querySelectorAll("script");
var text = scripts[scripts.length - 1].textContent;
On obsolete browsers, you may need to feature-detect textContent vs. innerText.
You might want to give it an identifying characteristic (id, class, etc.) so you don't have to find it by position like that.
Alternately, you could do what the PrototypeJS lib does and try go get it from the string with regex. You can find their source code for doing that here (look for extractScripts).
Running it
Once you have the text of the script, you have several options:
Use indirect eval (aka "global eval") on it: (0, eval)(text). This is not the same as eval(text); it runs the code in global scope, not local scope.
Create a new script element, set its text to the text, and add it
var newScript = document.createElement("script");
newScript.textContent = text;
document.body.appendChild(newScript);
If doing that, might make sense to remove the original, though it doesn't really matter.
Example that grabs the text of the script element after adding it and uses indirect eval:
var str = '<div><p>Examplee</p></div><script>alert("testing!")<\/script>';
document.body.innerHTML += str;
var scripts = document.querySelectorAll("script");
(0, eval)(scripts[scripts.length - 1].textContent);
Presumably you don't really use += on document.body.innerHTML, which builds an HTML string for the whole page, appends to it, tears the whole page down, and then parses the new HTML to build a new one. I assume that was just an example in your question.
jQuery provides the $.getScript(url [,success]) function. You can then load and execute your code from a separate jquery file which helps to manage and control the flow of execution.
basically put your alert("testing!") script inside a separate file, for instance alert.js in the same directory.
Then you can run the script when adding your employee to the HTML.
var str = '<div><p>Examplee</p></div>';
var url = 'alert.js';
document.body.innerHTML += str;
$.getScript(url);
I know this may seem like more work, but it is better practice to keep your javascript out of your HTML. You can also use a callback to gather user data after the alert or notify another process that the user has been alerted.
$.getScript(url, function(){
//do something after the alert is executed.
});
For instance maybe it would be a better design to add the employee after the alert is executed.
var str = '<div><p>Examplee</p></div>';
var url = 'alert.js';
$.getScript(url, function(){
document.body.innerHTML += str;
});
Edit: I know jQuery is not tagged, but I am also no petitioning to be the accepted answer to this question. I am only offering another alternative for someone who may run into the same issue and may be using jQuery. If that is the case $.getScript is a very useful tool designed for this exact problem.
You should change the HTML after it was loaded.
Try this:
document.addEventListener("DOMContentLoaded", function(event) {
document.body.innerHTML += str;
});

Use JavaScript to update the contents of a script tag dynamically

This click event works:
document.getElementById('control').addEventListener('click', function(){
alert('test');
});
Why can't I make it the content of a script tag that already exists on the page?
var s = document.getElementsByTagName("script")[0];
s.innerHTML = "document.getElementById('control').addEventListener('click', function(){alert('test');});";
I've tried enclosing the attempt to do something in an IIFE:
s.innerHTML = "(function(){alert('test')}());";
so that it will call itself.
Also:
s.innerHTML = "var f = function(){alert('f')}; f();"
I'm only interested theoretically and in no way am saying this is a good or bad idea, but how could I make it work?
Script tags are evaluated once and code is not replaced if you change their contents (they work differently than other types of tags in this regard). If you want to add new code to the page, you can simply add new script tags to the page that contains the new code. If you want to redefine existing publicly accessible functions, you can simply redefine them with new code.
So, if you want to replace a previous definition of function f(), you can simply assign new code to that symbol:
f = function() {alert("new f");}
You can insert new scripts from a remote source at any time with a code snippet like this:
var s = document.createElement("script");
s.type = "text/javascript";
s.src = url;
document.getElementsByTagName("head")[0].appendChild(s);
It is also possible to use eval() to pass text to the javascript engine that you want it to parse and then run, but there are rarely any good reasons to do it this way as there are generally better ways to solve a problem like this.
In addition to what jfriend00 said. If you want to dynamically execute scripts. Use should probably use JavaScript's eval function.
Note: But use it carefully!!!
you can't do this with an existing script tag, you have to create a new script tag and push your script before adding it to the DOM:
var sc =document.createElement("script");
sc.type="text/javascript";
var scriptText=document.createTextNode("document.getElementById('control').addEventListener('click', function(){"+
"alert('test');" +
"});");
sc.appendChild(scriptText);
document.querySelector("head").appendChild(sc);
and it will do what you want.
The important point here, once the first script in a script tag is executed, you no longer can inject any script in it, actually you can, but it won't work.

Javascript SetInverval() fireing only once in Firefox [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
setInterval not working (firing only once) in Google Chrome extension
This error only is a problem when document.write() is used.
I am on Mac 10.6.8 Intel.
Here is my script:
setInterval(function(){myFun()},1000);
function myFun() {
document.write('something');
}
The behavior is different in various browsers:
Firefox 12: Only happens once. The browser says "connecting" in the status bar.
Google Chrome and safari: Seems to work correctly.
Note: using setTimeout instead causes everything to behave like firefox (not working).
Using setInterval(myFun},1000), which is supposedly the source of so much error, behaves identically.
Many beginning tutorials use document.write() for "hellow world". However, this funciton is dangerous because it may mess up the script (by nuking the entire program). Here is a safe way to do debug printouts:
Before the script, in between the and in the html, add this:
<div id="myDebug"></div>
In the script, first convert it to a variable that can be called upon:
var myDebug = document.getElementById("myDebug");
When you need to show something, do this:
debug.innerHTML = "some string";
This will show the string in the browser window.
try this:
setInterval(function(){myFun()},1000);
function myFun() {
// Return all elements in document of type 'body'
// (there will only be one)
var bodyArray = document.getElementsByTagName('body');
// get the body element out of the array
var body = bodyArray[0];
// save the old html of the body element
var oldhtml=body.innerHTML;
// add something to the end
body.innerHTML=oldhtml+"somrthing";
}
As others have mentioned, using document.write() will (in some browsers) nuke the script used to update the document, writing to the body (and not the head, where the javascript should be stored) will stop this from happing.
Seems that you miss the semicolon after myFun()
setInterval(function(){myFun();},1000);
function myFun() {
document.write('something');
}
Also make sure you put the function in <body></body>
document.open() results in clearing the document. This function gets called by Firefox before document.write(). Thus your interval is lost.
see W3C Document Object Model HTML

onchange attribute won't call function

I have an HTML document (here), which creates an iframe-based media player for a collection of songs within albums (I just used letters to define these albums and songs in the mymusic array, for simplicity).
Focusing on the top 3 iframes, the way I have set out the user interaction is to generate the HTML for forms of available albums and songs using Javascript, and write them to the iframes in the body. If you run it and make a selection in the Albums menu, you will see that the options in the Songs menu correspond with the mymusic array, so this works.
However, when I choose a song, the function nowplaying(trackindex,albumindex) should be called using an onchange event in the Songs form, the same way as in the form generated using showinitial() ... but the function does not get called.
I have ruled out the coding of nowplaying itself as a cause, because even when I change nowplaying to alert("hello"), it does not get called. So this leads me to think the problem is with the onchange attribute in "anything", but I can't see the problem. The way I coded it is no different to before, and that worked fine, so why won't this work?
Any help would be much appreciated!
Firebug is your friend....
i is not defined
function
onchange(event) {
parent.nowplaying(this.SelectedIndex,
i); }(change )
onchange is getting called, but i is not defined when calling nowplaying.
This is the result of this line:
p+="<html><head></head><body><form><select onchange='parent.nowplaying(this.SelectedIndex,i);' size='";
which is using "i" in the string, when it should append it as a variable:
p+="<html><head></head><body><form><select onchange='parent.nowplaying(this.SelectedIndex," + i + ");' size='";
To clarify, i is defined when anything(i) is called, but you aren't writing i into the code, just the letter "i". When nowplaying(this.SelectedIndex,i) is called, i is no longer defined, because you aren't inside of the anything() function anymore. You need to expand i when you append the html to p, so that the value is there and not the variable i.
function anything(i){
p+="...<select onchange='parent.nowplaying(this.SelectedIndex,i);'...";
Your onchange event handler is set from a string. When run, it will not have access to i, which is a local variable from the anything function that has long since gone away.
The simple fix would be:
p+="...<select onchange='parent.nowplaying(this.SelectedIndex,'+i+');'...";
which turns the current value of i at string-making time into an integer literal inside the string.
However, it's not generally a good idea to be creating code from strings. It's normally better to write the event handler as a normal function object:
// You will need the below workaround to get the iframe document in IE too
//
var iframe= document.getElementById('songs');
var idoc= 'contentDocument' in iframe? iframe.contentDocument : iframe.contentWindow.document;
idoc.open();
idoc.write(s);
idoc.close();
idoc.getElementsByTagName('select')[0].onchange= function() {
// This is a closure. The 'i' variable from the parent 'anything' function is
// still visible in here
//
parent.nowplaying(this.selectedIndex, i);
};
However you would generally want to avoid setting handlers from one frame on a different one. I'm not really sure what the iframes are gaining you here other than headaches. Why not just simply use positioned divs with overflow? You can still rewrite their content through innerHTML if you need to... though I would prefer to populate them using DOM methods, to avoid all the HTML-injection problems your current script has.

Categories

Resources