monkdiscuss
shmem
<p>
Well, I have bitten... maybe the following makes life a bit easier for our fellow monk [blazar]
(and other keyboard-centric monks like me :-)
</p>
<p>
- 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. -
</p>
<p>
With XPath (thanks to [Corion], who gave me the hint) it is easy to divide every PM Page
into a tree of collections and lists, which can be browsed via JavaScript's <c>document.onkeypress</c>
event handler.
</p>
<code>
var xpath = {
'node' : { // any node i.e. thread or subthread
'desc' : 'Thread or sub-thread',
'p' : { // key to enter/browse 'Replies' collection
'desc' : 'Replies',
xpath : function () { return "//tr[@class='reply']" },
'l' : {
'desc' : 'go to node title links',
xpath : function (i) {
return <XPath expr selecting links based
on 'Replies' collection index>
},
},
'b' : {
'desc' : 'go to node body links',
xpath : function (i) { ... },
},
},
}
};
</code>
<p>
(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.)
</p>
<p>
Keying those on the page's body-id allows for tweaks for different pages:
</p>
<code>
'id-3628' : {
'desc' : 'Newest Nodes',
'p' : {
'desc' : 'Collections of Postings',
xpath : function () { return "//html/body/center/table/tbody/tr/td[1]/h3/a" },
'hl' : function (t) { return t.parentNode }, // different highlighting
'l' : { // list of postings - left column
'desc' : 'posting titles',
xpath : function (i) { return "//html/body/center/table/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/table/tbody/tr/td[1]/table["+i+"]/tbody/tr/td[2]/a" },
'sticky': 'l',
},
}
},
</code>
<p>
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.
</p>
<p>The complete code ready to be pasted into [id://492700] (see [id://399250] for other cool hacks):
<readmore>
<c>
// $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_NODE_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 "//table`[@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/tbody/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_table']/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/tbody/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/table/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/table/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/tbody/tr/td/h4" },
'l' : {
'desc' : 'posting titles',
xpath : function (i) { return "/html/body/center/table/tbody/tr/td`[1]/table`["+i+"]/tbody/tr/td`[2]/a" },
'sticky' : 'u',
},
'u' : {
'desc' : 'authors',
xpath : function (i) { return "/html/body/center/table/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_container']/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/tr/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//input[@name='node']" },
xpath : function () { return "//textarea[contains(@name,'doctext')]" },
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) curchild.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 elsewhere.
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 collection
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`['index'];
}
}
}
var i = 1;
if (context.parent`['index']) i = context.parent`['index']+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.parent;
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.parent`['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;
</c>
</readmore>
<p>
<small>Note: if you want to save the above code elsewhere to tweak it, and reference it via <c><script></c> tags,
you have to unescape the brackets (<c>`[</c>)</small>
</p>
<p>
But to save perlmonks a bit of precious bandwith and storage - I urge you to either paste the link
</p>
<c>
<script type="text/javascript" src="http://cruft.de/keypress.js"></script>
</c>
<p>
into [id://492700], which ensures you always have the newest version (and the latest breakages, too ;-) <update>
- if you trust me, that is (I could steal your cookie) - or place the code on a webserver of your confidence, and let
the file be served from there. </update><br />
Bug reports, suggestions, improvements (i.e. patches) and new page sub-hashes welcome!
</p>
<p>
Enjoy,
</p>
<div class="pmsig"><div class="pmsig-510280">
<p>--shmem
<small><small><pre>
_($_=" "x(1<<5)."?\n".q·/)Oo. G°\ /
/\_¯/(q /
---------------------------- \__(m.====·.(_("always off the crowd"))."·
");sub _{s./.($e="'Itrs `mnsgdq Gdbj O`qkdq")=~y/"-y/#-z/;$e.e && print}</pre></small></small>
</div></div>