14 במאי 2009

Count - an improved WC bookmarklet


The Hebrew announcement will be followed by an english one ;-)

To install the bookmarklet right-click this: Count and add it to your Bookmarks/Links (or just drag and drop if you use a mac). Make sure the Links tookbar is visible if you use IE.



החלטתי לכתוב בעברית ובאנגלית ביחד, להיות ידידותי לכל הקהלים.
אז אחרי שמימשתי את המועדפון (מילה שהרגע המצאתי עבור bookmarklet) לספירת אותיות נשארתי בהרגשה שזה עדיין לא זה והחלטתי לשפר אותו עוד. רציתי משהו יותר גמיש שסופר כל הזמן ונשאר צף על הדף ובעיקר משהו שישמש כעוגן להמשך פיתוח עתידי, כלומר שהמועדפון יקרא לקוד על השרת ואת הקוד הזה אוכל לשפר עם הזמן ללא צורך להטריד את המשתמש בהסרה והתקנה מחדש.
בנוסף, רציתי גם להשתפשף על jQuery וזו נראתה הזדמנות מתאימה.
התוצאה של כל העניין הזה היא מועדפון חינני שאני מאוד גאה בו. הוא שימושי לספירת תווים (או גם מילים ומשפטים) ולא פחות חשוב, הוא גם נראה טוב. אז כדי שלא ישעמם לי את הפרטים הטכניים אני אכתוב רק באנגלית, מקווה שתהנו מהספרן.



After implementing the first version of WC I was left with the feeling that it wasn't good enough, that something was missing. What I wanted was first, something more flexible in it's look and feel, second will not make you click it again and again, but will simply count chars whenever you select some text and third, is easy to upgrade.
I finally rewrote the bookmarklet and came up with this: Count
To host it I created a google apps application.
The idea is simple - there are cases you want to count characters. For example when using Topify to write a DM. All you need to do is click the bookmarklet and select some text (in any order; you may first select text and only then click the bookmarklet) and you get the char count right there as a floating window in your browser. You don't have to close it to continue your work, it'll just stay floating and you can drag it somewhere else if it gets in your way. What's nice is that next time you select some text it will listen and count the chars again.

Now the tech details.

What is a bookmarklet?
Bookmarklet are small pieces of javascript code wrapped in a bookmark. They are usually small applications or utilities you apply to the current browser page. In some sense they are like browser toolbars of addons or extensions but they usually are more lightweight, their installation process is VERY easy (drag and drop) and they are by definition cross-browser, so the same bookmarklet (when implemented correctly) should work on FF, IE, Opera, Safari etc. Example bookmarklets are bookmarklets that save links to delicious, bookmarklets that alert the page cookie etc.

How can javascript run inside a bookmark?
Bookmarks contain hyperlinks such as http://google.com. A hyperlink consists of several parts, the first being the protocol. In this example the protocol is http. After the protocol comes the colon delimiter and then the rest of the hyperlink. Browsers know many protocol types, to name a few: http, https, ftp and javascript. Yes. javascript is just another protocol the browser knows about which tells the browser to treat the rest of the text as actual javascript code and run it. So the following code will popup an alert box.

<a href="javascript:alert('hi')">Say hi</a>

Try it here: Say hi

Now, the same code can get saved as a bookmark, so if you drag that "Say hi" link to your link toolbar (or right click and add to bookmarks/link), every time you click it you get "hi".
So, this is the trick behind all bookmarklet. They are all hyperlinks using the javascript protocol, so they just run javascript on the current page without a page reload, which is important because they usually want to communicate with the page.

Why do you need the (function(){...})() construct in a bookmarklet?
If you look at real life bookmarklet, they all look like that:

(function(){...bookmarklet code here...})()

If you're wondering why does the bookmarklet code needs to get wrapped inside this weird function definition, wonder no more?
There are two compelling reasons to do this, but first let's see what is actually going on here with all the curly and other braces. What we have here is a function definition (an anonymous function)
function(){...}

This function is wrapped inside braces
(function(){...})

And after the braces we see another couple of braces:
(function(){...})()


What this construct accomplishes is an anonymous function definition and an immediate invocation of this function using the last (). So when the parser gets to the () it simply runs the code inside the function.
OK, so why do we need this? What's wrong with just having the actual code of the bookmarklet? Why do we need to wrap it in a function?
There are two reasons for this
1. the javascript: protocol allows for only one statement. So you can have a link javascript:alert(1) but you may not have javascript:alert(1);alert(2). A non trivial bookmarklet has more than one statement to using the above function construct lets you bundle several statements into one valid javascript statement javascript:(function(){alert(1);alert(2)})()
2. Namespace safety. The bookmarklet code may define local variables, for example var d = document; Now, keep in mind that a bookmarklet runs in the context of another page which you have no control over, so what if the page already has a variable d? Not good... The solution is simple, in javascript (as well as in other languages) variables are scoped, so if a bookmarklet defines a local variable d inside a function block it will not affect nor be affected by another page variable with the same name.

As a matter of fact, the function wrap trick is extensively used not only by bookmarklets, but by many other javascript applications that care about not polluting the default namespace. jQuery uses it as well.

How does one dynamically load server code to the current page?
Let's recall our setting: We write a bookmarklet, say the Count bookmarklet, which runs in the context of some arbitrary page so that when the user operates the bookmarklet by clicking it, the bookmarklet must not reload the page, but it needs to run scripts from a remote host. How do you do that? If the script was a simple alert() then you may simply include the code inline the bookmarklet. But if the code is considerably more complex, and maybe you want to use 3rd party code such as jQuery, what do you do?
As it appears, browsers allow dynamic loading of remote scripts by means of script injection to the head section (or to the body section, all the same). To acheive that you need to create a script dom element and attach it to the document head. The moment you do that, the browser notices that, loads the script and executes it. That's the same trick as jsonp uses, BTW, which I've blogged about in the past.
This is how you load a script dynamically:

var s = document.createElement('script');
s.setAttribute('src', 'http://charcount.appspot.com/s/jquery.js');
s.setAttribute('type', 'text/javascript');
document.body.appendChild(s);


What else is interesting about this bookmarklet?
I've had fun and challenges creating it. I enjoyed learning about jQuery, a kickass javascript library, I fought a lot of browser CSS quirks, learned a lot about how does one find the current selected text on a document (believe me, that's not half as easy as it may sound) and exercised my Fireworks Ninja. I did enough writing for one day, so I won't write about all these but if you have questions ping me at @rantav or just leave a comment here.

Where's your code? Can I see it?
Sure, it's all open sourced, hosted on Google Code here
And the web app that supports is is here.

7 תגובות:

Guy Soffer אמר/ה...

Cool! BTW, when I tried it on this site (gbsheli.com) the borders weren't rendered correctly (left & right border switched places on Chrome/Firefox).

Ran Tavory אמר/ה...

Crap, the page directionality is RTL so it fucked up the table with the borders.
No worries, though, I'll fix it in no time.
There, fixed

פודקאסטים אמר/ה...

מועדפון זה מצוין! ישר לדורבנות. dorbanot.com

eyal. אמר/ה...

thumbs up!

though on some pages, the borders are still rendered incorrectly.
check for example a forum page on tapuz (FF).

Ran Tavory אמר/ה...

Hi Eyal, thanks for the report, I fixed it.
It's was a somewhat bogus CSS declarations on Tapuz that messed with my table layout so I overran them.
See http://code.google.com/p/charcount/source/detail?r=18

amit אמר/ה...

Great post!

didn't know this feature exists

אנונימי אמר/ה...

[url=http://http://1-belt-buckles.blogspot.com/]http://1-belt-buckles.blogspot.com/[/url]