Merge branch 'experimental' of git://github.com/Dieterbe/uzbl into experimental
[uzbl-mobile] / examples / data / uzbl / 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 the load_commit_handler:
6 // set load_commit_handler = sh 'echo "script /usr/share/uzbl/examples/scripts/linkfollow.js" > "$4"'
7 //
8 // when script is loaded, it can be invoked with
9 // bind f* = js hints.set("%s",   hints.open)
10 // bind f_ = js hints.follow("%s",hints.open)
11 //
12 // At the moment, it may be useful to have way of forcing uzbl to load the script
13 // bind :lf = script /usr/share/uzbl/examples/scripts/linkfollow.js
14 //
15 // The default style for the hints are pretty ugly, so it is recommended to add the following
16 // to config file
17 // set stylesheet_uri = /usr/share/uzbl/examples/data/style.css
18 //
19 // based on follow_Numbers.js
20 //
21 // TODO: fix styling for the first element
22 // TODO: emulate mouseover events when visiting some elements
23 // TODO: rewrite the element->action handling
24
25
26 function Hints(){
27
28   // Settings
29   ////////////////////////////////////////////////////////////////////////////
30
31   // if set to true, you must explicitly call hints.follow(), otherwise it will
32   // follow the link if there is only one matching result
33   var requireReturn = true;
34
35   // Case sensitivity flag
36   var matchCase = "i";
37
38   // For case sensitive matching, uncomment:
39   // var matchCase = "";
40
41
42   var uzblid = 'uzbl_hint';
43   var uzblclass = 'uzbl_highlight';
44   var uzblclassfirst = 'uzbl_h_first';
45   var doc = document;
46   var visible = [];
47   var hintdiv;
48
49   this.set = hint;
50   this.follow = follow;
51   this.keyPressHandler = keyPressHandler;
52
53   function elementPosition(el) {
54     var up = el.offsetTop;
55     var left = el.offsetLeft; var width = el.offsetWidth;
56     var height = el.offsetHeight;
57
58     while (el.offsetParent) {
59       el = el.offsetParent;
60       up += el.offsetTop;
61       left += el.offsetLeft;
62     }
63     return {up: up, left: left, width: width, height: height};
64   }
65
66   function elementInViewport(p) {
67     return  (p.up < window.pageYOffset + window.innerHeight &&
68             p.left < window.pageXOffset + window.innerWidth &&
69             (p.up + p.height) > window.pageYOffset &&
70             (p.left + p.width) > window.pageXOffset);
71   }
72
73   function isVisible(el) {
74     if (el == doc) { return true; }
75     if (!el) { return false; }
76     if (!el.parentNode) { return false; }
77     if (el.style) {
78       if (el.style.display == 'none') {
79           return false;
80       }
81       if (el.style.visibility == 'hidden') {
82           return false;
83       }
84     }
85     return isVisible(el.parentNode);
86   }
87
88   // the vimperator defaults minus the xhtml elements, since it gave DOM errors
89   var hintable = " //*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @role='link' or @href] | //input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select";
90
91   function Matcher(str){
92     var numbers = str.replace(/[^\d]/g,"");
93     var words = str.replace(/\d/g,"").split(/\s+/).map(function (n) { return new RegExp(n,matchCase)});
94     this.test = test;
95     this.toString = toString;
96     this.numbers = numbers;
97     function matchAgainst(element){
98       if(element.node.nodeName == "INPUT"){
99         return element.node.value;
100       } else {
101         return element.node.textContent;
102       }
103     }
104     function test(element) {
105       // test all the regexp
106       var item = matchAgainst(element);
107       return words.every(function (regex) { return item.match(regex)});
108     }
109   }
110
111   function HintElement(node,pos){
112
113     this.node = node;
114     this.isHinted = false;
115     this.position = pos;
116     this.num = 0;
117
118     this.addHint = function (labelNum) {
119       // TODO: fix uzblclassfirst
120       if(!this.isHinted){
121         this.node.className += " " + uzblclass;
122       }
123       this.isHinted = true;
124
125       // create hint
126       var hintNode = doc.createElement('div');
127       hintNode.name = uzblid;
128       hintNode.innerText = labelNum;
129       hintNode.style.left = this.position.left + 'px';
130       hintNode.style.top =  this.position.up + 'px';
131       hintNode.style.position = "absolute";
132       doc.body.firstChild.appendChild(hintNode);
133
134     }
135     this.removeHint = function(){
136       if(this.isHinted){
137         var s = (this.num)?uzblclassfirst:uzblclass;
138         this.node.className = this.node.className.replace(new RegExp(" "+s,"g"),"");
139         this.isHinted = false;
140       }
141     }
142   }
143
144   function createHintDiv(){
145     var hintdiv = doc.getElementById(uzblid);
146     if(hintdiv){
147       hintdiv.parentNode.removeChild(hintdiv);
148     }
149     hintdiv = doc.createElement("div");
150     hintdiv.setAttribute('id',uzblid);
151     doc.body.insertBefore(hintdiv,doc.body.firstChild);
152     return hintdiv;
153   }
154
155   function init(){
156     // WHAT?
157     doc.body.setAttribute("onkeyup","hints.keyPressHandler(event)");
158     hintdiv = createHintDiv();
159     visible = [];
160
161     var items = doc.evaluate(hintable,doc,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);
162     for (var i = 0;i<items.snapshotLength;i++){
163       var item = items.snapshotItem(i);
164       var pos = elementPosition(item);
165       if(isVisible && elementInViewport(elementPosition(item))){
166         visible.push(new HintElement(item,pos));
167       }
168     }
169   }
170
171   function clear(){
172
173     visible.forEach(function (n) { n.removeHint(); } );
174     hintdiv = doc.getElementById(uzblid);
175     while(hintdiv){
176       hintdiv.parentNode.removeChild(hintdiv);
177       hintdiv = doc.getElementById(uzblid);
178     }
179   }
180
181   function update(str,openFun) {
182     var match = new Matcher(str);
183     hintdiv = createHintDiv();
184     var i = 1;
185     visible.forEach(function (n) {
186       if(match.test(n)) {
187         n.addHint(i);
188         i++;
189       } else {
190         n.removeHint();
191       }});
192     if(!requireReturn){
193       if(i==2){ //only been incremented once
194         follow(str,openFun);
195       }
196     }
197   }
198
199   function hint(str,openFun){
200     if(str.length == 0) init();
201     update(str,openFun);
202   }
203
204   function keyPressHandler(e) {
205     var kC = window.event ? event.keyCode: e.keyCode;
206     var Esc = window.event ? 27 : e.DOM_VK_ESCAPE;
207     if (kC == Esc) {
208         clear();
209         doc.body.removeAttribute("onkeyup");
210     }
211   }
212
213   this.openNewWindow = function(item){
214     // TODO: this doesn't work yet
215     item.className += " uzbl_follow";
216     window.open(item.href,"uzblnew","");
217   }
218   this.open = function(item){
219     simulateMouseOver(item);
220     item.className += " uzbl_follow";
221     window.location = item.href;
222   }
223
224   function simulateMouseOver(item){
225     var evt = doc.createEvent("MouseEvents");
226     evt.initMouseEvent("MouseOver",true,true,
227         doc.defaultView,1,0,0,0,0,
228         false,false,false,false,0,null);
229     return item.dispatchEvent(evt);
230   }
231
232
233   function follow(str,openFunction){
234     var m = new Matcher(str);
235     var items = visible.filter(function (n) { return n.isHinted });
236     clear();
237     var num = parseInt(m.numbers,10);
238     if(num){
239       var item = items[num-1].node;
240     } else {
241       var item = items[0].node;
242     }
243     if (item) {
244       var name = item.tagName;
245       if (name == 'A') {
246         if(item.click) {item.click()};
247           openFunction(item);
248       } else if (name == 'INPUT') {
249         var type = item.getAttribute('type').toUpperCase();
250         if (type == 'TEXT' || type == 'FILE' || type == 'PASSWORD') {
251             item.focus();
252             item.select();
253         } else {
254             item.click();
255         }
256       } else if (name == 'TEXTAREA' || name == 'SELECT') {
257         item.focus();
258         item.select();
259       } else {
260         item.click();
261         openFunction(item);
262       }
263     }
264   }
265 }
266
267 var hints = new Hints();
268
269 // vim:set et sw=2: