diff options
-rw-r--r-- | examples/data/scripts/follower.js | 420 |
1 files changed, 420 insertions, 0 deletions
diff --git a/examples/data/scripts/follower.js b/examples/data/scripts/follower.js new file mode 100644 index 0000000..604b779 --- /dev/null +++ b/examples/data/scripts/follower.js @@ -0,0 +1,420 @@ +// A Link Follower for Uzbl. +// P.C. Shyamshankar <sykora@lucentbeing.com> +// +// WARNING: this script depends on the Uzbl object which is now disabled for +// WARNING security reasons. So the script currently doesn't work but it's +// WARNING interesting nonetheless +// +// Based extensively (like copy-paste) on the follow_numbers.js and +// linkfollow.js included with uzbl, but modified to be more customizable and +// extensible. +// +// Usage +// ----- +// +// First, you'll need to make sure the script is loaded on each page. This can +// be done with: +// +// @on_event LOAD_COMMIT script /path/to/follower.js +// +// Then you can bind it to a key: +// +// @bind f* = js follower.follow('%s', matchSpec, handler, hintStyler) +// +// where matchSpec, handler and hintStyler are parameters which control the +// operation of follower. If you don't want to customize any further, you can +// set these to follower.genericMatchSpec, follower.genericHandler and +// follower.genericHintStyler respectively. +// +// For example, +// +// @bind f* = js follower.follow('%s', follower.genericMatchSpec, follower.genericHandler, follower.genericHintStyler) +// @bind F* = js follower.follow('%s', follower.onlyLinksMatchSpec, follower.newPageHandler, follower.newPageHintStyler) +// +// In order to make hints disappear when pressing a key (the Escape key, for +// example), you can do this: +// +// @bind <Escape> = js follower.clearHints() +// +// If your Escape is already bound to something like command mode, chain it. +// +// Alternatively, you can tell your <Escape> key to emit an event, and handle +// that instead. +// +// @bind <Escape> = event ESCAPE +// @on_event ESCAPE js follower.clearHints() +// +// Customization +// ------------- +// +// If however you do want to customize, 3 Aspects of the link follower can be +// customized with minimal pain or alteration to the existing code base: +// +// * What elements are hinted. +// * The style of the hints displayed. +// * How the hints are handled. +// +// In order to customize behavior, write an alternative, and pass that in to +// follower.follow invocation. You _will_ have to modify this script, but only +// locally, it beats having to copy the entire script under a new name and +// modify. +// +// TODO: +// * Whatever all the other TODOs in the file say. +// * Find out how to do default arguments in Javascript. +// * Abstract out the hints into a Hint object, make hintables a list of hint +// objects instead of two lists. + +// Helpers +String.prototype.lpad = function(padding, length) { + var padded = this; + while (padded.length < length) { + padded = padding + padded; + } + + return padded; +} + +function Follower() { + + // Globals + var uzblID = 'uzbl-follow'; // ID to apply to each hint. + var uzblContainerID = 'uzbl-follow-container'; // ID to apply to the div containing hints. + + // Translation table, used to display something other than numbers as hint + // labels. Typically set to the ten keys of the home row. + // + // Must have exactly 10 elements. + // + // I haven't parameterized this, to make it customizable. Should I? Do + // people really use more than one set of keys at a time? + var translation = ["a", "r", "s", "t", "d", "h", "n", "e", "i", "o"]; + + // MatchSpecs + // These are XPath expressions which indicate which elements will be hinted. + // Use multiple expressions for different situations, like hinting only form + // elements, or only links, etc. + // + // TODO: Check that these XPath expressions are correct, and optimize/make + // them more elegant. Preferably by someone who actually knows XPath, unlike + // me. + + // Vimperator default (copy-pasted, I never used vimperator). + this.genericMatchSpec = " //*[@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"; + + // Matches only links, suitable for opening in a new instance (I think). + this.onlyLinksMatchSpec = " //*[@href] | //a | //area"; + + // Follow Handlers + // These decide how an element should be 'followed'. The handler is passed + // the element in question. + + // Generic Handler, opens links in the same instance, emits the FORM_ACTIVE + // event if a form element was chosen. Also clears the keycmd. + this.genericHandler = function(node) { + if (node) { + if (window.itemClicker != undefined) { + window.itemClicker(node); + } else { + var tag = node.tagName.toLowerCase(); + if (tag == 'a') { + node.click(); + window.location = node.href; + } else if (tag == 'input') { + var inputType = node.getAttribute('type'); + if (inputType == undefined) + inputType = 'text'; + + inputType = inputType.toLowerCase(); + + if (inputType == 'text' || inputType == 'file' || inputType == 'password') { + node.focus(); + node.select(); + } else { + node.click(); + } + Uzbl.run("event FORM_ACTIVE"); + } else if (tag == 'textarea'|| tag == 'select') { + node.focus(); + node.select(); + Uzbl.run("event FORM_ACTIVE"); + } else { + node.click(); + if ((node.href != undefined) && node.href) + window.location = node.href; + } + } + } + Uzbl.run("event SET_KEYCMD"); + } + + // Handler to open links in a new page. The rest is the same as before. + this.newPageHandler = function(node) { + if (node) { + if (window.itemClicker != undefined) { + window.itemClicker(node); + } else { + var tag = node.tagName.toLowerCase(); + if (tag == 'a') { + node.click(); + Uzbl.run("@new_window " + node.href); + } else if (tag == 'input') { + var inputType = node.getAttribute('type'); + if (inputType == undefined) + inputType = 'text'; + + inputType = inputType.toLowerCase(); + + if (inputType == 'text' || inputType == 'file' || inputType == 'password') { + node.focus(); + node.select(); + } else { + node.click(); + } + Uzbl.run("event FORM_ACTIVE"); + } else if (tag == 'textarea'|| tag == 'select') { + node.focus(); + node.select(); + Uzbl.run("event FORM_ACTIVE"); + } else { + node.click(); + if ((node.href != undefined) && node.href) + window.location = node.href; + } + } + } + Uzbl.run("event SET_KEYCMD"); + }; + + // Hint styling. + // Pretty much any attribute of the hint object can be modified here, but it + // was meant to change the styling. Useful to differentiate between hints + // with different handlers. + // + // Hint stylers are applied at the end of hint creation, so that they + // override the defaults. + + this.genericHintStyler = function(hint) { + hint.style.backgroundColor = '#AAAAAA'; + hint.style.border = '2px solid #4A6600'; + hint.style.color = 'black'; + hint.style.fontSize = '10px'; + hint.style.fontWeight = 'bold'; + hint.style.lineHeight = '12px'; + return hint; + }; + + this.newPageHintStyler = function(hint) { + hint.style.backgroundColor = '#FFCC00'; + hint.style.border = '2px solid #4A6600'; + hint.style.color = 'black'; + hint.style.fontSize = '10px'; + hint.style.fontWeight = 'bold'; + hint.style.lineHeight = '12px'; + return hint; + }; + + // Beyond lies a jungle of pasta and verbosity. + + // Translate a numeric label using the translation table. + function translate(digitLabel, translationTable) { + translatedLabel = ''; + for (var i = 0; i < digitLabel.length; i++) { + translatedLabel += translationTable[digitLabel.charAt(i)]; + } + + return translatedLabel; + } + + function computeElementPosition(element) { + var up = element.offsetTop; + var left = element.offsetLeft; + var width = element.offsetWidth; + var height = element.offsetHeight; + + while (element.offsetParent) { + element = element.offsetParent; + up += element.offsetTop; + left += element.offsetLeft; + } + + return {up: up, left: left, width: width, height: height}; + } + + // Pretty much copy-pasted from every other link following script. + function isInViewport(element) { + offset = computeElementPosition(element); + + var up = offset.up; + var left = offset.left; + var width = offset.width; + var height = offset.height; + + return up < window.pageYOffset + window.innerHeight && + left < window.pageXOffset + window.innerWidth && + (up + height) > window.pageYOffset && + (left + width) > window.pageXOffset; + } + + function isVisible(element) { + if (element == document) { + return true; + } + + if (!element){ + return false; + } + + if (element.style) { + if (element.style.display == 'none' || element.style.visibiilty == 'hidden') { + return false; + } + } + + return isVisible(element.parentNode); + } + + function generateHintContainer() { + var container = document.getElementById(uzblContainerID); + if (container) { + container.parentNode.removeChild(container); + } + + container = document.createElement('div'); + container.id = uzblContainerID; + + if (document.body) { + document.body.appendChild(container); + } + return container; + } + + // Generate everything that is to be hinted, as per the given matchSpec. + // hintables[0] refers to the items, hintables[1] to their labels. + function generateHintables(matchSpec) { + var hintables = [[], []]; + + var itemsFromXPath = document.evaluate(matchSpec, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + + for (var i = 0; i < itemsFromXPath.snapshotLength; ++i) { + var element = itemsFromXPath.snapshotItem(i); + if (element && isVisible(element) && isInViewport(element)) { + hintables[0].push(element); + } + } + + // Assign labels to each hintable. Can't be combined with the previous + // step, because we didn't know how many there were at that time. + var hintLength = hintables.length; + for (var i = 0; i < hintables[0].length; ++i) { + var code = translate(i.toString(), translation); + hintables[1].push(code.lpad(translation[0], hintLength)); + } + + return hintables; + } + + // Filter the hintables based on input from the user. Makes the screen less + // cluttered after the user has typed some prefix of hint labels. + function filterHintables(hintables, target) { + var filtered = [[], []]; + + var targetPattern = new RegExp("^" + target); + + for (var i = 0; i < hintables[0].length; i++) { + if (hintables[1][i].match(targetPattern)) { + filtered[0].push(hintables[0][i]); + filtered[1].push(hintables[1][i].substring(target.length)); + } + } + + return filtered; + } + + // TODO make this use the container variable from main, instead of searching + // for it? + function clearHints() { + var container = document.getElementById(uzblContainerID); + if (container) { + container.parentNode.removeChild(container); + } + } + + // So that we can offer this as a separate function. + this.clearHints = clearHints; + + function makeHint(node, code, styler) { + var position = computeElementPosition(node); + var hint = document.createElement('div'); + + hint.name = uzblID; + hint.innerText = code; + hint.style.display = 'inline'; + + hint.style.margin = '0px'; + hint.style.padding = '1px'; + hint.style.position = 'absolute'; + hint.style.zIndex = '10000'; + + hint.style.left = position.left + 'px'; + hint.style.top = position.up + 'px'; + + var img = node.getElementsByTagName('img'); + if (img.length > 0) { + hint.style.left = position.left + img[0].width / 2 + 'px'; + } + + hint.style.textDecoration = 'none'; + hint.style.webkitBorderRadius = '6px'; + hint.style.webkitTransform = 'scale(1) rotate(0deg) translate(-6px, -5px)'; + + hint = styler(hint); // So that custom hint stylers can override the above. + return hint; + } + + + function drawHints(container, hintables, styler) { + for (var i = 0; i < hintables[0].length; i++) { + hint = makeHint(hintables[0][i], hintables[1][i], styler); + container.appendChild(hint); + } + + if (document.body) { + document.body.appendChild(container); + } + } + + // The main hinting function. I don't know how to do default values to + // functions, so all arguments must be specified. Use generics if you must. + this.follow = function(target, matchSpec, handler, hintStyler) { + var container = generateHintContainer(); // Get a container to hold all hints. + var allHintables = generateHintables(matchSpec); // Get all items that can be hinted. + hintables = filterHintables(allHintables, target); // Filter them based on current input. + + clearHints(); // Clear existing hints, if any. + + if (hintables[0].length == 0) { + // Nothing was hinted, user pressed an unknown key, maybe? + // Do nothing. + } else if (hintables[0].length == 1) { + handler(hintables[0][0]); // Only one hint remains, handle it. + } else { + drawHints(container, hintables, hintStyler); // Draw whatever hints remain. + } + + return; + }; +} + +// Make on-click links clickable. +try { + HTMLElement.prototype.click = function() { + if (typeof this.onclick == 'function') { + this.onclick({ + type: 'click' + }); + } + }; +} catch(e) {} + +follower = new Follower(); |