Well, I have bitten... maybe the following makes life a bit easier for our fellow monk blazar
(and other keyboard-centric monks like me :-)
- the problem with key driven navigation via browser key defaults or HTML access keys is that
these mechanisms don't provide for contexts and collections, e.g. the list of links on a page
is a single flat list, which can be browsed with the <Tab> key, but navigation doesn't
follow necessarily the page layout, if the order in which they were found at page loading is
different. -
(Key 'p' selects the 'Replies' section (or advances the cursor in it), 'l' selects the node title links;
while inside 'node title links', 'p' brings you back to 'Replies'. A selected item (except links) will
be relocated to the window top.)
That's it, basically. The 'sticky' key lets link indexes of adjacent collections together, e.g. posting titles and
their authors on Newest Nodes or Best Nodes. The descriptions are also used for an alert box for keys available in
each section or collection.
// $Id: keypress.js,v 0.6 2008/07/27 12:16:05 shmem Exp $
// .
// Key Driven PerlMonks
// For [blazar] and all ye monks
function $ (id) { return document.getElementById(id) }
function xq (query) {
return document.evaluate(query,document,null,XPathResult.ORDERED_N
+ODE_SNAPSHOT_TYPE,null);
}
function xl (query) {
var r = xq(query);
var a = new Array;
for ( var i=0 ; i < r.snapshotLength; i++ ) a.push(r.snapshotItem(
+i));
return a;
}
// globals
var context; // current context
var cursnum; // current prefix number (as string)
var curchild; // current child
var curindex; // current index into collection
var hl = 'border'; // how to highlight
var hc = '1px solid #F00'; // highlight attributes
var cursor = new Object;
// all of PerlMonks (well, some day, hopefully...)
var xpath = {
'node' : { // any node i.e. thread or subthread
'desc' : 'Thread or sub-thread',
'p' : {
'desc' : 'Replies',
xpath : function () { return "//tr`[@class='reply']" },
'hl' : function (t) { return t.childNodes`[0] },
'sticky' : 'r',
'l' : {
'desc' : 'go to node title links',
xpath : function (i) { var j = i * 2; return "//tab
+le`[@id='replies_table']/tbody/tr`["+j+"]/td//a`[@href]" },
},
'b' : {
'desc' : 'go to node body links',
xpath : function (i) { var j = i * 2 + 1; return "/
+/table`[@id='replies_table']/tbody/tr`["+j+"]/td//a`[@href]" },
},
},
'r' : {
'desc' : '`[reply] link',
xpath : function (i) { return "//table/tbody/tr/td`[1]/
+form/center`[1]/table`[@id='replies_table']/tbody/tr/td`[2]/font/a" }
+,
'sticky' : 'p',
//html/body/center/table/tb
+ody/tr/td`[1]/form/center`[1]/table`[@id='replies_table']/tbody/tr`[5
+]/td`[2]/font/a
},
'C' : {
'desc' : 'Comment on',
xpath : function () { return "//table`[@id='replies_tab
+le']/tbody/tr`[1]/th/a" },
},
},
'id-131' : {
'desc' : 'The Monastery Gates',
'p' : {
'desc' : 'Front-paged Posts',
xpath : function () { return "//tr`[@class='post_head']
+" },
'sticky' : 'rl',
'hl' : function (t) { return t.childNodes`[1] },
},
'r' : {
'desc' : 'Offer your reply',
xpath : function () { return "//td`[2]/a/font" },
'sticky' : 'pl',
},
'l' : {
'desc' : 'Link to thread',
xpath : function () { return "//td/table/tbody/tr/td`[1
+]/a`[@id]"},
'sticky' : 'rp',
},
},
'id-479' : {
'desc' : 'SoPW, Meditations, PM Discussion, ...',
'p' : {
'desc' : 'Questions',
xpath : function () { return "//tr`[@class='post_head']
+" },
'sticky' : 'rl',
'hl' : function (t) { return t.childNodes`[1] },
},
'r' : {
'desc' : 'Link `[Offer your reply]',
xpath : function () { return "//td`[2]/a/font" },
'sticky' : 'pl',
},
'l' : {
'desc' : 'Link to thread',
xpath : function () { return "//td/table/tbody/tr/td`[1
+]/a`[@id]"},
'sticky' : 'rp',
},
},
'id-3628' : {
'desc' : 'Newest Nodes',
'p' : {
'desc' : 'Collections of Postings',
xpath : function () { return "//html/body/center/table/t
+body/tr/td`[1]/h3/a" },
'hl' : function (t) { return t.parentNode },
'l' : { // list of postings - left column
'desc' : 'posting titles',
xpath : function (i) { return "//html/body/center/ta
+ble/tbody/tr/td`[1]/table`["+i+"]/tbody/tr/td`[1]/a" },
'sticky': 'u',
},
'u' : { // list of users - right column
'desc' : 'authors',
xpath : function (i) { return "//html/body/center/ta
+ble/tbody/tr/td`[1]/table`["+i+"]/tbody/tr/td`[2]/a" },
'sticky': 'l',
},
}
},
'id-9066' : {
'desc' : 'Best Nodes',
'p' : {
'desc' : 'Collections of Postings',
xpath : function () { return "/html/body/center/table/tb
+ody/tr/td/h4" },
'l' : {
'desc' : 'posting titles',
xpath : function (i) { return "/html/body/center/ta
+ble/tbody/tr/td`[1]/table`["+i+"]/tbody/tr/td`[2]/a" },
'sticky' : 'u',
},
'u' : {
'desc' : 'authors',
xpath : function (i) { return "/html/body/center/ta
+ble/tbody/tr/td`[1]/table`["+i+"]/tbody/tr/td`[3]/a" },
'sticky' : 'l',
},
},
},
'n' : { // nodelets
'desc' : 'Nodelets',
xpath : function () { return "//table`[@id='nodelet_container
+']/tbody/tr`[1]/th" },
'l' : {
'desc' : 'links in nodelet',
xpath :function (i) { return "//table`[@id='nodelet_cont
+ainer']/tbody`["+i+"]/tr/td//a" },
},
},
'T' : {
'desc' : 'Title Bar',
xpath : function () { return "/html/body/table`[@id='titlebar
+-top']/tbody/tr/td`[2]/font/span/a" },
},
'R' : {
'desc' : 'root & parent link (if applicable)',
xpath : function () { return "/html/body/center/table/tbody/t
+r/td`[1]/form/div`[2]/p`[1]/a" },
},
'c' : {
'desc' : 'Chatterbox',
xpath : function () { return "//input`[@id='talkbox']" },
focus : function () { setFocus(talkbox) }, // chatterbox
},
't' : {
'desc' : 'Composition textbox (if any)',
// xpath : function () { return "//table[3]/tbody/tr[2]/td//inpu
+t[@name='node']" },
xpath : function () { return "//textarea[contains(@name,'doct
+ext')]" },
focus : function () { setFocus(context`['colls']`[0]) },
},
};
var bodyid = xq('//body').snapshotItem(0).id;
// nodes that resemble SoPW (#479), i.e. main sections
var javascriptisstupid = new Array(480,1040,1044,1590,1597,21144,23771
+);
//for (i in Array(480,1040,1044,1590,1597,21144,23771)) {
for (i in javascriptisstupid) {
var j = javascriptisstupid`[i];
if(bodyid == 'id-' + j) xpath`['id-' + j] = xpath`['id-479'];
}
var context = xpath`[bodyid] || xpath`['node'];
// these are present in all nodes...
context`['T'] = xpath`['T']; // Monkbar
context`['R'] = xpath`['R']; // link to root|parent node
context`['n'] = xpath`['n']; // nodelets
context`['c'] = xpath`['c']; // Chatterbox
// ...well, except these
context`['t'] = xpath`['t']; // Composition textbox title
var root = context; // save context root
// keys for all contexts and subcontexts (where apropriate)
var keys = {
'j' : {
'desc' : 'move down',
value : 1,
},
'k' : {
'desc' : 'move up',
value : -1,
},
'+' : {
'desc' : 'vote ++',
func : function () { vote(0) },
},
'0' : {
'desc' : 'reset vote',
func : function () { vote(1) },
},
'-' : {
'desc' : 'vote --',
func : function () { vote(2) },
},
'v' : {
'desc' : 'jump to "vote!" button',
func : function () { document.forms`[1]`['sexisgreat'].focus
+() },
},
'h' : {
'desc' : 'Hide the body of a post',
func : hideBody,
},
'm' : {
'desc' : 'go to page top',
func : function () { window.scroll(0,0); if (curchild) curch
+ild.style`[hl] = curchild`['oldstyle']; context = root }
}
};
// scroll element to top of page
function setScroll (i) {
var child = context`['coll']`[i];
if (! child) return;
var helem = child;
if (context`['hl']) helem = context`['hl'](child);
if (curchild) { // unhighlight
curchild.style`[hl] = curchild`['oldstyle'];
}
helem`['oldstyle'] = helem.style`[hl];
helem.style`[hl] = hc;
if(child.nodeName == 'A') child.focus();
else if (child.parentNode.nodeName == 'A') child.parentNode.focus(
+);
else
window.scroll(0,getOffset(child));
curchild = helem;
}
// cast vote on a post in collection
// XXX use xpath proper
function vote(n) {
var t = curchild;
var nm;
if (! t || context['desc'] == 'Thread or sub-thread') { // HACK
var id = 'vote__'+bodyid.split('-')[1];
if(document.forms[1][id])
document.forms[1][id][n].checked = 'checked';
return;
}
if (t.childNodes[0]) // post + replies
var nm = t.childNodes[0].name;
if (! nm) { // section page, e.g. meditations
try {
nm = t.childNodes[1].childNodes[1].href;
nm = nm.slice(nm.indexOf('=')+1);
} catch (ex) {
nm = bodyid.split('-')[1];
if(! document.forms[1]['vote__'+nm]) nm = '';
}
if(! nm) { // HACK
try {
nm = curchild.childNodes[1].href.split('=')[1];
} catch (ex) {}
}
}
var id = 'vote__' + nm;
if(document.forms[1][id])
document.forms[1][id][n].checked = 'checked';
}
function setFocus (t) {
if (! t) return;
document.onkeypress = '';
t.onblur = function () { document.onkeypress = keyDriven };
t.focus();
}
// for threads
function hideBody () {
if (! curchild) return;
var n = curchild.parentNode.nextSibling.nextSibling;
var v = n.style.display;
if (v == 'none')
n.style.display = 'table-row';
else
n.style.display = 'none';
}
// get textareas, text inputs to disable keyDriven
var talkbox = document.getElementById('talkbox');
// get offset of an element
function getOffset (foo) {
if (! foo) return;
var offset = 0;
if (foo.nodeName != 'A') offset = foo.offsetTop;
while(foo.parentNode != foo) {
if(foo.parentNode) {
var p = foo.parentNode;
var o = p.offsetTop;
foo = p;
if (p.nodeName == 'BODY') break;
if (p.nodeName == 'CENTER') continue;
// if (p.nodeName == 'TD') continue;
if (p.nodeName == 'A') continue;
if (p.nodeName == 'TR') continue;
if (p.nodeName == 'TBODY') continue;
if (p.nodeName == 'FORM') continue;
offset += p.offsetTop;
} else break;
}
return offset;
}
// Event handler
function keyDriven (evt) {
evt = evt || window.event;
// don't mess up with the browser shortcuts
if (evt.ctrlKey) return;
if (evt.modifiers)
if (evt.modifiers == 2) return
var k = evt.keyCode ? evt.keyCode : evt.charCode ? evt.charCode :
+evt.which;
var s = String.fromCharCode(k);
var num = parseInt(cursnum,10);
var found = 0;
if (! num) num = 0;
if (k > 47 && k < 59) { // 0 - 9
if ( cursnum || s != '0') {// leading 0 not allowed - used els
+ewhere.
cursnum += s;
return;
}
}
var rel = 0;
// select context
if(s == '?') {
s = 'Current context: '+context`['desc']+"\n\n";
for (var k in context) {
if (k.length == 1) {
s += k +' - '+ context`[k]`['desc'] + "\n";
if (context`[k]`['sticky']) {
ary = context`[k]`['sticky'].split('');
s += ' index linked with key(s) ' + ary.join(',
+') + "\n";
}
}
}
if (context.parent) {
for (var k in context.parent) {
if (k.length == 1)
s += k +' - '+ context.parent`[k]`['desc']+"\n";
}
}
s+= "\nGlobal keys:\n\n";
for (var k in keys)
s += k + ' - ' + keys`[k]`['desc'] + "\n";
s += "\nMovement commands may be preceeded with a number,\n"
+ "e.g. 12j - go 12 items down\n";
alert(s);
} else if (keys`[s]) { // global browsing key pressed
var n = 0;
if(keys`[s].value) {
rel = 1;
n = keys`[s].value;
if(num && n) num *= n;
else num = n
found = 1;
} else {
try {
keys`[s].func();
} catch (ex){
alert("invocation of\n"+keys`[s]+"\nfailed:\n"+ex);
}
}
} else if (context`[s]) { // sub-context or function
try {
context`[s]();
} catch (ex) {
context`[s].parent = context;
var i = 1;
if (context`['index']) i = context`['index']+1;
context = context`[s];
context`['coll'] = xl(context.xpath(i));
found = 1;
}
} else if (context.parent) {
if (context.parent`[s]) {
if(context.parent`[s] == context) { // next item in collec
+tion
if(!num) num = 1;
rel = 1;
} else { // adjacent collection context.parent
context.parent`[s].parent = context.parent;
if (context`['sticky']) {
var ary = context`['sticky'].split('');
for (var i in ary) {
//alert(ary`[i]);
if(ary`[i] == s) {
context.parent`[s]`['index'] = context`['i
+ndex'];
}
}
}
var i = 1;
if (context.parent`['index']) i = context.parent`['ind
+ex']+1;
context = context.parent`[s];
context`['coll'] = xl(context.xpath(i));
}
found = 1;
} else if (context.parent.parent) {
if (context.parent.parent`[s]) {
context.parent.parent`[s].parent = context.parent.pare
+nt;
context = context.parent.parent`[s];
found = 1;
}
}
}
if (! found) { // invalid key pressed | key not defined
if (k < 48 || k > 58) // reset number string
cursnum = '';
return;
}
if (! context`['index']) context`['index'] = 0;
if (! context`['coll']) {
var i = 1;
if (context.parent) {
i = context.parent`['index']+1;
// alert(context.parent`['desc']+' index = '+context.paren
+t`['index']);
}
context`['coll'] = xl(context.xpath(i));
}
var coll = context`['coll'];
var index = context`['index'];
if (rel) num += index;
else num = index;
if (num > context`['coll'].length-1) num = context`['coll'].length
+-1;
if (num < 0) num = 0;
setScroll(num);
context`['index'] = num;
if (context.focus) setFocus(curchild);
if (k < 48 || k > 58) // reset number string
cursnum = '';
}
// get all textareas and disable keyDriven while they have focus.
// XXX use xpath for that
function setSuspendKeyMode (t) {
t.onblur = function () { document.onkeypress = keyDriven };
t.onfocus = function () { document.onkeypress = '' };
}
function applyFuncOnTree(e,f,tag,type) {
if (e.tagName == tag) {
if(type != '') {
if (e.type == type) f(e);
} else {
f(e);
}
}
if (e.childNodes) {
for(var i = 0; i<e.childNodes.length; i++) {
applyFuncOnTree(e.childNodes`[i],f,tag,type);
}
}
}
// find all textarea elements and applyFuncOnTree on them
for (var i = 0; i<document.forms.length; i++) {
f = document.forms`[i];
applyFuncOnTree(f,setSuspendKeyMode,'TEXTAREA','');
applyFuncOnTree(f,setSuspendKeyMode,'INPUT','text');
}
document.onkeypress = keyDriven;
But to save perlmonks a bit of precious bandwith and storage - I urge you to either paste the link