PDA

View Full Version : deconstructing jquery



traq
07-27-2011, 04:35 PM
hi all,
yes, I have only myself to blame for "learning" javascript by learning jquery. you can slap me later.

I've got a function here; I'm trying to rewrite it in standard javascript (it's for a bookmarklet and I'm pretty sure jquery won't be loaded on the page where it is to be used).


function(){
var can = $('.class1');
$(can).live(
'click'
,function(){
var txt = $('.class2 textarea').val();
alert(txt);
}
);
}
So far, Ive got this:

function(){
var can = document.getElementsByClassName('class1');
can.addEventListener(
'click'
,function(){
var txt = document.getElementsByClassName('class2').getElementsByTagName('textarea').nodeValue;
alert(txt);
}
,false
);
}
It doesn't like when I call getElementsByTagName - it's "not a function" (I guess that means it's not a valid method of the nodelist that getElementsByClassName returns?). can anyone help me out with how to approach this?

I need to get the contents of the <textarea> that's inside the element with class="class2". (also, if nodeValue is not the way to get those contents, please LMK.)

--------------------------------
Second issue (less important): I'm aware that IE < 9 doesn't support getElementsByClassName. How can I find them, in that case?

--------------------------------
thanks, everyone : )

jscheuer1
07-28-2011, 01:51 AM
I'm not sure where you're getting that error. One obvious issue is that with:



var can = document.getElementsByClassName('class1');

can is a nodeList. You cannot addEventListener to a nodeList, only to a single element.

Are there/can there be more than one element with that class? I know there could be, but in your scenario is there likely to be. If not, use id - in ordinary javascript it's much easier to deal with.

Otherwise you have to loop through the nodeList performing the same action on each member. In this case adding the event listener to each one.

Also the implications of 'live' are different than addEventListener. Using 'live' means that all elements that are currently in the DOM that qualify and any that might arrive later. Using addEventListener only affects a single element and it must currently be in the DOM.

Oh and the equivalent of .val() in ordinary javascript is .value, not .nodeValue.

The way your jQuery code is constructed implies that there's probably only one element with a class name of class1, and probably only one with the class of class2, which probably has only one child that's a textarea. If that's all true, and they're all already there, you can specify them via their index in their respective nodeLists (ByTagName is also a nodeList), and we could do:


function(){
var can = document.getElementsByClassName('class1')[0];
can.addEventListener(
'click'
,function(){
var txt = document.getElementsByClassName('class2')[0].getElementsByTagName('textarea')[0].value;
alert(txt);
}
,false
);
}

Other things to consider -

If the page has jQuery on it your code can use it.

How many bookmarklets have you used/written? I've never seen a bookmarklet constructed like this. Are you sure your jQuery one could work even if jQuery were on the page?

IE < 9 doesn't support addEventListener.

traq
07-28-2011, 02:31 AM
Hi John,

thanks for the clarifications. as a result of learning jQuery, I'm not as familiar as I ought to be with regular javascript properties and methods.

I've used a few bookmarklets, and written a few from tutorials. This is my first experiment without a model.

yes, the jQuery version works (not in the format I posted, of course, but like so:
<a href="javascript:(function(){var can=$('.class1');$(can).live('click',function(){var txt=$('.class2 textarea').val();alert(txt);});})();">Bookmarklet</a>as to your other questions:

The page in question will never include jQuery. I don't control the page, either; so IDs are out (too bad - as you say, they're very quick and easy. I've already got this working on a mockup using IDs).

it's actually quite possible that there will be at least one other .class1 element on the page - but only one will be available to be clicked at any given time.

there is usually only one instance of .class2 (I've never found two on the same page, but I don't know for certain). it always shares a common ancestor with .class1, but they're never siblings.

the textarea is several generations below the .class2 element, and it's always the only textarea in that element.

I think they're all always at static positions - but I'd have to do more checking to be sure - so finding them by index is a possibility. (I was hoping I wouldn't have to - it seems fragile to me...? I guess only if their positions vary.)

I'm aware of the differences between jQuery's .live() and regular event listeners, but I don't know how to construct the latter in ordinary js. .live() is preferable, since there's lots of generated content I'm working with.

jscheuer1
07-28-2011, 03:34 AM
Because of the exigencies of that difference (live vs addEventListener) and other differences between generic javascript and jQuery, a slightly different approach seems desirable:


<a href="javascript:(function(){
var re1 = /\bclass1\b/, re2 = /\bclass2\b/;
function func(e){
e = e || event;
var t = e.target || e.srcElement, t1, t2, t3, pn;
while(t.parentNode){
if(!t1 && re1.test(t.className)){
t1 = t;
}
t = t.parentNode;
pn = t.getElementsByTagName('*');
for(var i = 0; i < pn.length; ++i){
if(!t2 && re2.test(pn[i].className)){
t2 = pn[i];
}
}
}
if(t1 && t2 && (t3 = t2.getElementsByTagName('textarea')[0])){
alert(t3.value);
}
}
if (window.addEventListener){
document.addEventListener('click', func, false);
}
else if (window.attachEvent){
document.attachEvent('onclick', func);
}
})();">Bookmarklet</a>

That's cross browser, doesn't require ByClassName, or standards mode, and acts like 'live' because it listens for clicks on the document and then evaluates/works off of what was clicked.

If you have trouble following it, read it over a few times.

Still having trouble, ask.

It checks to see if what was clicked has the class1 and then finds the class2 if any that's a child of a common ancestor (all tags on a page have at least one common ancestor, this stops at the nearest one as you seemed to indicate would be good), and then alerts the value of that class2's first if any textarea.

Sound about right?

traq
07-28-2011, 03:44 AM
...wow.

it looks good. totally backwards from the approach I had, but straightforward and very simple. I'll see if it works as expected.

thanks a bunch!
well, it didn't quite work yet, but it's because I didn't have the correct dom. I'm going to try to work things out a little more. I think there may also be other scripts (on the live target page) that are canceling the event I want before it bubbles up to my listeners. We'll see what happens.

thanks, in any case, your code was very helpful!

jscheuer1
07-28-2011, 01:37 PM
Could I have a link to a page you're trying to run this bookmarklet on?

traq
07-28-2011, 02:07 PM
yeah, sure.

I'm messing around with twitter. don't know if you have an account or not, but you'd need one to see what I'm working with: I want to see if I can capture the contents of an @-reply when I cancel sending it.

it's just for fun/ education, so I would like to try and figure out as much of it as possible, but obviously I do need some guidance.

I think part of the issue might be in the 1,000 lines of other javascript on the page (and that's just the inline scripts!). if this doesn't work out, it's no big deal: as I said, I'm just trying to learn from it.

here's the relevant section of the dom
(on the fourth line is div.twttr-dialog-close, which is the click event I'm listening for; and further down is the textbox.twitter-anywhere-tweet-box-editor, which I want to get the contents of when canceling):
<div class="twttr-dialog-container" style="visibility: hidden;">
<div type="reply" style="width: 500px; height: auto; left: 454px; top: 225px;" class="twttr-dialog draggable ui-draggable">
<div class="twttr-dialog-header">
<h3>Reply to @meyerweb</h3> <div class="twttr-dialog-close"><b></b></div>
</div>
<div class="twttr-dialog-inside">
<div class="twttr-dialog-body clearfix">
<div class="twttr-dialog-content">
<div class="tweet-box">
<div class="tweet-box-title">
<h2>Reply to @meyerweb</h2>
</div>
<div class="text-area">
<div class="text-area-editor twttr-editor">
<textarea style="width: 452px; height: 56px;" class="twitter-anywhere-tweet-box-editor"></textarea>
<ul style="width: 468px; top: 73px; left: 0px; visibility: hidden;" class="autocomplete-container"></ul>
</div>
</div>
<div class="tweet-button-container">
<span class="geo-control">
<a href="#" class="geo-location">
<span original-title="" class="geo-icon">&nbsp;</span>
<span style="" class="geo-text ellipsify-container">
<span style="" class="content"></span>
</span>
<span style="display: none; visibility: visible;" class="geo-dropdown-icon">&nbsp;</span>
</a>
</span>
<div class="tweet-button-sub-container">
<img src="/images/spinner.gif" class="tweet-spinner" style="display: none;">
<span style="opacity: 0;" class="tweetbox-counter-tipsy"></span><input class="tweet-counter" value="140" disabled="disabled">
<a href="#" class="tweet-button button">Tweet</a>
</div>
</div>
</div>
</div>
</div>

jscheuer1
07-28-2011, 03:54 PM
I'm not on Twitter and have no intention of joining for this.

But I would want to know if the code in your post is in an iframe. If it is we would need to get to the iframe document in our code.

I'd also like to know if the twitter-anywhere-tweet-box-editor is really an ordinary textarea. Like are you looking at 'view source' or at a DOM inspector? There's a good chance that the editor is enhanced and changed via javascript and that the element twitter-anywhere-tweet-box-editor may no longer exist in the DOM.

One way to answer both questions would be to use Firefox with the Developer Tools extension installed. Use its 'view source' > 'view generated source'.

Then you will see if the elements you are looking for are even there in the generated source (the actual DOM).

Use its DOM Inspector to try to find them as well. It has a find element by clicking on it, and a find element by attribute. If they're in an iframe, you can walk up the tree to the iframe, find its name if any or at least its position in the window.frames collection.

You could also experiment with the code:


<a href="javascript:(function(){
function func(e){
alert('here');
}
if (window.addEventListener){
document.addEventListener('click', func, false);
}
else if (window.attachEvent){
document.attachEvent('onclick', func);
}
})();">Bookmarklet</a>

Click on your button or whatever it is. If it doesn't alert, the event isn't bubbling, or has been removed, or the element is in an iframe.

If it does alert try:


<a href="javascript:(function(){
function func(e){
e = e || event;
var t = e.target || e.srcElement, t1, t2, t3, pn;
alert(t.tagName);
alert(t.parentNode.innerHTML);
alert(t.className);
}
if (window.addEventListener){
document.addEventListener('click', func, false);
}
else if (window.attachEvent){
document.attachEvent('onclick', func);
}
})();">Bookmarklet</a>

If those work, tell me what they alert.

traq
07-29-2011, 02:15 AM
I'm not on Twitter and have no intention of joining for this.of course; I wouldn't expect you to. in fact I'd object if you were signing up solely for this.


But I would want to know if the code in your post is in an iframe. If it is we would need to get to the iframe document in our code.

I'd also like to know if the twitter-anywhere-tweet-box-editor is really an ordinary textarea. Like are you looking at 'view source' or at a DOM inspector? There's a good chance that the editor is enhanced and changed via javascript and that the element twitter-anywhere-tweet-box-editor may no longer exist in the DOM.
no, it's not in an iframe; yes, my excerpt is from the actual DOM ("view generated source").

I tried your bookmarklet and there was no response from the "close" dialog div. it alerts on most clicks, up until I click on "reply" - which opens a modal reply window - from then until I cancel the reply, nothing alerts.

jscheuer1
07-29-2011, 04:03 AM
I'm not saying that this one does, but modal windows can contain iframes.


Is the cancel button inside the modal?


Does the modal have an overlay that dims out the rest of the page?


If so, does clicking on the overlay produce the alert?

jscheuer1
07-29-2011, 12:54 PM
Here's another thing we could try:


<a href="javascript:(function(){
if(typeof jQuery === 'undefined' || parseFloat(jQuery.fn.jquery) < 1.3){
var args = arguments;
if(typeof jq162appended === 'undefined'){
var s = document.createElement('script');
s.type = 'text/javascript';
s.src = 'http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js';
document.getElementsByTagName('head')[0].appendChild(s);
window.jq162appended = true;
}
setTimeout(function(){args.callee();}, 300);
return;
}
var $ = jQuery, close = $('.twttr-dialog-close').live('click', function(){
alert($('.twitter-anywhere-tweet-box-editor').eq(close.index(this)).val());
});
})();">Bookmarklet</a>

BTW, from the markup you pasted:



<textarea style="width: 452px; height: 56px;" class="twitter-anywhere-tweet-box-editor"></textarea>

There's a class on the textarea. So we should go for it directly, not as you originally had:



var txt = $('.class2 textarea').val();

Also, in the present code in this post the bit about index might not be required or even desired. In which case:


.eq(close.index(this))

may be removed.

What that does, assuming everything else is working, is choose the textarea that has the same index as the close button. This assumes that if there are more than one of either, that there are an equal number of each. Removing that part will get you the value of the first one.

If there's another script library on the page that uses $, even pre 1.3 versions of jQuery, it will lose control of $. But that can be fixed, if this at least works for what you want alerted.

traq
07-30-2011, 01:59 AM
nope... no result. but, whilst searching for the generated jQuery <script> from that last bookmarklet, I found this:
<script type="text/javascript">(function(){window.setTimeout=window.setTimeout;window.setInterval=window.setInterval;window.WATCH=function(label,block){if(typeof block==="undefined"){block=label;label=undefined}if(typeof label==="string"){WATCH._didExecute[label]=true}WATCH._attempt(this,block)};WATCH._didExecute={};WATCH._reportCount=0;WATCH._reportLimit=25;WATCH._reportInterval=60*1000;WATCH._active=false;WATCH.activa te=function(setting){if(typeof setting==="undefined"){setting=true}WATCH._active=setting;extend(WATCH,WATCH._active?WATCH.actives:WATCH.inactives)};WATCH.actives={};WATCH.inactives={};var extend=function(destination,source){for(var key in source){destination[key]=source[key]}};var noop=function(){};WATCH._attempt=function(that,block){if(arguments.length<2){block=that;that=window}if(WATCH._active){try{block.apply(that)}catch(error){WATCH._triggerError(error)}}else{block.apply(that)}};WATCH.inactives.end=noop;WAT CH.actives.end=function(label){if(typeof label==="undefined"){throw new Error("WATCH.end() requires a label")}if(WATCH._didExecute[label]){WATCH._didExecute[label]=false}else{if(WATCH._active){WATCH._triggerError(new Error('WATCH.end("'+label+'") called without successful call to WATCH("'+label+'", fn(){...}) - a SyntaxError probably just happened'))}}WATCH._didExecute[label]=false};WATCH.inactives.callback=function(that,callback){return typeof callback==="undefined"?that:callback};WATCH.actives.callback=function(that,callback){if(arguments.length===1){callback=that;that=this}if(typeof callback==="string"){callback=(function(stringVersion){return function(){eval(stringVersion)}}(callback))}var watchedCallback=function(){var that=this,args=arguments,result;WATCH._attempt(function(){result=callback.apply(that,args)});return result};watchedCallback.isWatched=true;return watchedCallback};WATCH._onErrorCallbacks={};WATCH.inactives._addOnError=noop;WATCH.actives._addOnError=function(callback){var unique=WATCH._unique();WATCH._onErrorCallbacks[unique]=callback;return unique};WATCH.inactives._removeOnError=noop;WATCH.actives._removeOnError=function(id){delete WATCH._onErrorCallbacks[id]};WATCH._lastUnique=-1;WATCH._unique=function(){return ++WATCH._lastUnique};WATCH.inactives.jQuery=noop;WATCH.actives.jQuery=function(){WATCH._originalJQueryEventAdd=WATCH._originalJQueryEventAdd||jQuery.event.add;j Query.event.add=function(){var newArgs=Array.prototype.slice.call(arguments);if(typeof newArgs[2]==="function"){newArgs[2]=WATCH.callback(newArgs[2])}else{if(newArgs&&typeof newArgs[2]==="object"&&newArgs[2].handler){newArgs[2].handler=WATCH.callback(newArgs[2].handler)}}return WATCH._originalJQueryEventAdd.apply(this,newArgs)};WATCH._originalJQueryAjax=WATCH._originalJQueryAjax||jQuery.ajax;jQuery.ajax=function(options){jQuery.each(["complete","error","success"],function(which,key){if(!options[key]){return }options[key]=WATCH.callback(options[key])});return WATCH._originalJQueryAjax.apply(this,arguments)}};WATCH.inactives.undoJQuery=noop;WATCH.actives.undoJQuery=function(){jQuery.event.add=WATCH._originalJQueryEven tAdd;jQuery.ajax=WATCH._originalJQueryAjax};WATCH._previousErrors={};var escapeDoubleQuotes=function(string){return string.toString().replace('"','\\"')};var stringifyLite=function(object){var result="{",hasProperty=false;for(var key in object){if(typeof object[key]==="undefined"||object[key]===null){continue}result+=(hasProperty?',"':'"')+escapeDoubleQuotes(key)+'":"'+escapeDoubleQuotes(object[key])+'"';hasProperty=true}return result+"}"};WATCH._scribeError=function(report){if(WATCH._previousErrors[report.error]&&(new Date())-WATCH._previousErrors[report.error]<WATCH._reportInterval){return false}if(WATCH.reportLimit<=WATCH.reportCount){return }WATCH.reportCount++;if(!document.location.hostname.match(/(^(www|api)\.)?twitter\.com$/)){return }WATCH._previousErrors[report.error]=new Date();report.product_name="webclient";report.type="js_error";report.url=window.location.href;report.event_name="test";var isProduction=document.location.hostname.match(/(^(www|api|staging\d+.local)\.)twitter\.com$/)!=null;var scribeHost=isProduction?"scribe.twitter.com":window.location.host;var scribeUrl=(window.location.protocol.match(/s\:$/)?"https":"http")+"://"+scribeHost;scribeUrl+=isProduction?"/":"/scribe";scribeUrl+="?category=client_watch_error&log="+encodeURIComponent(stringifyLite(report))+"&ts="+(new Date()).getTime();(new Image()).src=scribeUrl};WATCH._triggerError=function(error){var reraise=true;var report={error:error};for(var key in WATCH._onErrorCallbacks){try{if(WATCH._onErrorCallbacks[key](report)===false){reraise=false}}catch(callbackError){report.callbackFailure=true}}WATCH._scribeError(report);if(reraise){throw error}};WATCH.inactives.natives=noop;WATCH.actives.natives=function(){window.setInterval=WATCH._watchedSetInterval;window.setTimeout=WATCH._watchedSetTimeout};W ATCH.inactives.undoNatives=noop;WATCH.actives.undoNatives=function(){window.setInterval=WATCH._originalSetInterval;window.setTimeout=WATCH._originalSetTimeout}; WATCH._originalSetInterval=window.setInterval;WATCH._originalSetTimeout=window.setTimeout;WATCH._watchedSetInterval=function(callback,timeout){var setInterval=WATCH._originalSetInterval;return setInterval(WATCH.callback(callback),timeout)};WATCH._watchedSetTimeout=function(callback,timeout){var setTimeout=WATCH._originalSetTimeout;return setTimeout(WATCH.callback(callback),timeout)};WATCH.activate(false)}());</script>
I didn't really try to figure out how it's working, but it seems like it might watch for and kill non-twitter scripts.

I think this particular experiment is done with - I set out to see if I could write something interesting myself, not to reverse-engineer twitter's scripts.
I originally wrote "hack at," but in the "I'm an amateur making a mess of things" sense.
also, maybe the experiment isn't done. : )

I really appreciate all your help. I've learned a lot about how to better approach "normal" javascript.

jscheuer1
07-30-2011, 04:03 AM
Probably not. They have that running on their home page. It doesn't prevent me from (in Firefox) pasting in commands to the address bar and executing them. I found I could add events and alter the appearance of the page. I also found that they're running jQuery 1.5.2 and that it's available for use from the address bar.

I wonder about your reluctance to "hack" them. Running a bookmarklet on a page isn't hacking. It's your browser, you can do whatever you like. It doesn't hack or change their page for anyone else.

I think there's something about the script and/or markup that makes what you've proposed difficult. Or it might just be something simple we're overlooking. I'm still not convinced there's no iframe involved. In any case, there's still a good chance it can be done. The fact that it sort of works on other parts of the page tends to indicate that we're just going about it wrong, not that it's blocked, though it might be. But if you want to stop trying, that's fine with me.

traq
07-30-2011, 05:11 AM
hmm... I didn't mean "hack" as in "crack," just as in "making a mess of things." If you think it's still achievable, I'm still interested - and if you're interested. This isn't a critical exercise, and twitter is a huge mass of javascript to figure out.

re jQuery: my mistake #1; I just assumed that since my jQuery bookmarklet, which worked in test cases sans twitter javascript, didn't work on the actual site, that jQuery wasn't available. very bad of me.

I was also worried that it might be slower/less reliable if it required jQuery. Is that a valid concern?

about iframes: I've only found one (and the js that creates it), just after the <body> tag:
<iframe tabindex="-1" role="presentation" style="position: absolute; top: -9999px;" src="http://api.twitter.com/receiver.html"></iframe>

jscheuer1
07-30-2011, 12:56 PM
If jQuery is already loaded it's probably faster to use it rather than create functions to handle its routines via ordinary javascript. But speed differences are negligible with such a small script - once again if jQuery is already available.

So lets test that assumption for that page. If at any point (with the one exception noted below) you don't get what I expect, stop and report back. Load the page in question in Firefox and paste this into the address bar:


javascript:alert(typeof $)

and hit enter. It should alert:


function

Either way (this is the exception), then try:


javascript:alert(typeof jQuery)

It should alert the same.

If not tell me what each did.

But if they were both 'function', then do:


javascript:alert(typeof $.fn)

should be 'object'. If so then do (all lower case):


javascript:alert($.fn.jquery)

should be '1.5.2'. If so we have jQuery 1.5.2 at our disposal on that page via the usual $ shortcut.

Then, making sure your close button is visible, do:


javascript:alert($('.twttr-dialog-close').size())

If it's 0, then it's in an iframe or otherwise not part of that page's DOM, or has a different class than expected, or no class.

If its 1 or greater, it's there and we just need to find a way to get at it's click event, or if it's in fact what's being clicked. We could try mousedown. But I'm getting ahead of things. Let's first see what the above gets us.



In a completely unrelated matter, you might want to check out this thread:

http://www.dynamicdrive.com/forums/showthread.php?t=63649

The person seems willing to pay for a simple script installation. I often might do something like that, but they seem so lost, I'm not sure I want to step in it. Thought you might want to give it a shot.

traq
07-30-2011, 01:42 PM
function,function,object,1.5.2,1. cool! and thanks for the very detailed explanations

and thanks, I'll look at that other thread

jscheuer1
07-30-2011, 03:15 PM
Great! Now do:


javascript:void($('.twttr-dialog-close').live('click mousedown mouseup mouseenter mouseleave',function(e){alert(e.type)}))

Try those things. I'm thinking it will register mousedown, mouseenter and mouseleave, as the click and mouseup will be lost to the mousedown. But, if there's some other event already associated with it, other than an existing click, the results may vary. If we get no mouseenter and no mouseleave, it might not actually be that button.

After refreshing the page try just:


javascript:void($('.twttr-dialog-close').live('click',function(e){alert(e.type)}))

to see if we can actually get the click.