Beefy Boxes and Bandwidth Generously Provided by pair Networks
Perl Monk, Perl Meditation
 
PerlMonks  

comment on

( [id://3333]=superdoc: print w/replies, xml ) Need Help??

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. -

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 document.onkeypress event handler.

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) { ... }, }, }, } };

(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.)

Keying those on the page's body-id allows for tweaks for different pages:

'id-3628' : { 'desc' : 'Newest Nodes', 'p' : { 'desc' : 'Collections of Postings', xpath : function () { return "//html/body/center/table/tbody/t +r/td[1]/h3/a" }, 'hl' : function (t) { return t.parentNode }, // different hig +hlighting 'l' : { // list of postings - left column 'desc' : 'posting titles', xpath : function (i) { return "//html/body/center/table/tbod +y/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/tbod +y/tr/td[1]/table["+i+"]/tbody/tr/td[2]/a" }, 'sticky': 'l', }, } },

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.

The complete code ready to be pasted into Free Nodelet Settings (see Free Nodelet freed for other cool hacks):

// $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;

Note: if you want to save the above code elsewhere to tweak it, and reference it via <script> tags, you have to unescape the brackets (`[)

But to save perlmonks a bit of precious bandwith and storage - I urge you to either paste the link

<script type="text/javascript" src="http://cruft.de/keypress.js"></scr +ipt>

into Free Nodelet Settings, 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>
Bug reports, suggestions, improvements (i.e. patches) and new page sub-hashes welcome!

Enjoy,

--shmem

_($_=" "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}

In reply to Better keyboard-driven navigation, any? - yes... by shmem

Title:
Use:  <p> text here (a paragraph) </p>
and:  <code> code here </code>
to format your post; it's "PerlMonks-approved HTML":



  • Are you posting in the right place? Check out Where do I post X? to know for sure.
  • Posts may use any of the Perl Monks Approved HTML tags. Currently these include the following:
    <code> <a> <b> <big> <blockquote> <br /> <dd> <dl> <dt> <em> <font> <h1> <h2> <h3> <h4> <h5> <h6> <hr /> <i> <li> <nbsp> <ol> <p> <small> <strike> <strong> <sub> <sup> <table> <td> <th> <tr> <tt> <u> <ul>
  • Snippets of code should be wrapped in <code> tags not <pre> tags. In fact, <pre> tags should generally be avoided. If they must be used, extreme care should be taken to ensure that their contents do not have long lines (<70 chars), in order to prevent horizontal scrolling (and possible janitor intervention).
  • Want more info? How to link or How to display code and escape characters are good places to start.
Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others imbibing at the Monastery: (6)
As of 2024-04-19 11:00 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found