rewrite of linkfollow
[uzbl-mobile] / examples / scripts / linkfollow.js
1 // link follower for uzbl
2 // requires http://github.com/DuClare/uzbl/commit/6c11777067bdb8aac09bba78d54caea04f85e059
3 //
4 // first, it needs to be loaded before every time it is used.
5 // One way would be to use something like load_start_handler to send
6 // "act script /usr/share/examples/scripts/linkfollow.js"
7 // (currently, it is recommended to use load_finish_handler since the JS code seems to get
8 // flushed. Using a load_start_handler with a 1s delay works but not always)
9 //
10 // when script is loaded, it can be invoked with
11 // bind f* = js hints.set("%s")
12 // bind f_ = js hints.follow("%s")
13 //
14 // At the moment, it may be useful to have way of forcing uzbl to load the script
15 // bind :lf = script /usr/share/examples/scripts/linkfollow.js
16 //
17 // To enable hint highlighting, add:
18 // set stylesheet_uri = /usr/share/uzbl/examples/data/style.css
19 //
20 // based on follow_Numbers.js
21 //
22 // TODO: fix styling for the first element
23 // TODO: load the script as soon as the DOM is ready
24
25
26 function Hints(){
27   var uzblid = 'uzbl_hint';
28   var uzblclass = 'uzbl_highlight';
29   var uzblclassfirst = 'uzbl_h_first';
30   var doc = document;
31   var visible = [];
32   var hintdiv;
33
34   this.set = hint;
35   this.follow = follow;
36   this.keyPressHandler = keyPressHandler;
37
38   function elementPosition(el) {
39     var up = el.offsetTop;
40     var left = el.offsetLeft; var width = el.offsetWidth;
41     var height = el.offsetHeight;
42
43     while (el.offsetParent) {
44       el = el.offsetParent;
45       up += el.offsetTop;
46       left += el.offsetLeft;
47     }
48     return [up, left, width, height];
49   }
50
51   function elementInViewport(offset) {
52     var up = offset[0];
53     var left = offset[1];
54     var width = offset[2];
55     var height = offset[3];
56     return  (up < window.pageYOffset + window.innerHeight && 
57             left < window.pageXOffset + window.innerWidth && 
58             (up + height) > window.pageYOffset && 
59             (left + width) > window.pageXOffset);
60   }
61
62   function isVisible(el) {
63     if (el == doc) { return true; }
64     if (!el) { return false; }
65     if (!el.parentNode) { return false; }
66     if (el.style) {
67       if (el.style.display == 'none') {
68           return false;
69       }
70       if (el.style.visibility == 'hidden') {
71           return false;
72       }
73     }
74     return isVisible(el.parentNode);
75   }
76
77   var hintable = "//a[@href] | //img | //input";
78
79   function Matcher(str){
80     var numbers = str.replace(/[^\d]/g,"");
81     var words = str.replace(/\d/g,"").split(/\s+/).map(function (n) { return new RegExp(n,"i")});
82     this.test = test;
83     this.toString = toString;
84     this.numbers = numbers;
85     function test(element) {
86       // test all the regexp
87       return words.every(function (regex) { return element.node.textContent.match(regex)});
88     }
89   }
90
91   function HintElement(node,pos){
92
93     this.node = node;
94     this.isHinted = false;
95     this.position = pos;
96
97     this.addHint = function (labelNum) {
98       if(!this.isHinted){
99         this.node.className += " " + uzblclass;
100       }
101       this.isHinted = true;
102         
103       // create hint  
104       var hintNode = doc.createElement('div');
105       hintNode.name = uzblid;
106       hintNode.innerText = labelNum;
107       hintNode.style.left = this.position[1] + 'px';
108       hintNode.style.top =  this.position[0] + 'px';
109       hintNode.style.position = "absolute";
110       doc.body.firstChild.appendChild(hintNode);
111         
112     }
113     this.removeHint = function(){
114       if(this.isHinted){
115         var s = (this.num)?uzblclassfirst:uzblclass;
116         this.node.className = this.node.className.replace(new RegExp(" "+s,"g"),"");
117         this.isHinted = false;
118       }
119     }
120   }
121
122   function createHintDiv(){
123     var hintdiv = doc.getElementById(uzblid);
124     if(hintdiv){
125       hintdiv.parentNode.removeChild(hintdiv);
126     }
127     hintdiv = doc.createElement("div");
128     hintdiv.setAttribute('id',uzblid);
129     doc.body.insertBefore(hintdiv,doc.body.firstChild);
130     return hintdiv;
131   }
132
133   function init(){
134     // WHAT?
135     doc.body.setAttribute("onkeyup","hints.keyPressHandler(event)");
136     hintdiv = createHintDiv();
137     visible = [];
138
139     var items = doc.evaluate(hintable,doc,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);
140     for (var i = 0;i<items.snapshotLength;i++){
141       var item = items.snapshotItem(i);
142       var pos = elementPosition(item);
143       if(isVisible && elementInViewport(elementPosition(item))){
144         visible.push(new HintElement(item,pos));
145       }
146     }
147   }
148
149   function clear(){
150
151     visible.forEach(function (n) { n.removeHint(); } );
152     hintdiv = doc.getElementById(uzblid);
153     while(hintdiv){
154       hintdiv.parentNode.removeChild(hintdiv);
155       hintdiv = doc.getElementById(uzblid);
156     }
157   }
158
159   function update(str) {
160     var match = new Matcher(str);
161     hintdiv = createHintDiv();
162     var i = 1;
163     visible.forEach(function (n) {
164       if(match.test(n)) {
165         n.addHint(i);
166         i++;
167       } else {
168         n.removeHint();
169       }});
170   }
171
172   function hint(str){
173     if(str.length == 0) init();
174     update(str);
175   }
176
177   function keyPressHandler(e) {
178     var kC = window.event ? event.keyCode: e.keyCode;
179     var Esc = window.event ? 27 : e.DOM_VK_ESCAPE;
180     if (kC == Esc) {
181         clear();
182         doc.body.removeAttribute("onkeyup");
183     }
184   }
185
186   function follow(str){
187     var m = new Matcher(str);
188
189     var items = visible.filter(function (n) { return n.isHinted });
190     clear();
191     var num = parseInt(m.numbers,10);
192     if(num){
193       var item = items[num-1].node;
194     } else {
195       var item = items[0].node;
196     }
197     if (item) {
198       item.style.borderStyle = "dotted";
199       item.style.borderWidth = "thin";
200
201       var name = item.tagName;
202       if (name == 'A') {
203         if(item.click) {item.click()};
204         window.location = item.href;
205       } else if (name == 'INPUT') {
206         var type = item.getAttribute('type').toUpperCase();
207         if (type == 'TEXT' || type == 'FILE' || type == 'PASSWORD') {
208             item.focus();
209             item.select();
210         } else {
211             item.click();
212         }
213       } else if (name == 'TEXTAREA' || name == 'SELECT') {
214         item.focus();
215         item.select();
216       } else {
217         item.click();
218         window.location = item.href;
219       }
220     }
221   }
222 }
223
224 var hints = new Hints();
225 //document.attachEvent("onKeyUp",hints.keyPressHandler);
226
227 // vim:set et tw=2:
228
229