1
0
mirror of https://github.com/gryf/pentadactyl-pm.git synced 2025-12-21 21:28:00 +01:00

Document hints.js

Adding documentation to hints.js. Also split getAreaOffset and
getInputHint out of generate() to make that function easier to read.
This commit is contained in:
Conrad Irwin
2009-04-04 23:36:43 +01:00
parent 3800f10144
commit 7fc5d0b175

View File

@@ -83,7 +83,11 @@ function Hints() //{{{
Y: Mode("Yank hint description", function (elem) util.copyToClipboard(elem.textContent || "", true), extended) Y: Mode("Yank hint description", function (elem) util.copyToClipboard(elem.textContent || "", true), extended)
}; };
// Used to open multiple hints /**
* Used to open multiple hints
*
* @param {node} elem the previously selected hint
*/
function hintAction_F(elem) function hintAction_F(elem)
{ {
buffer.followLink(elem, liberator.NEW_BACKGROUND_TAB); buffer.followLink(elem, liberator.NEW_BACKGROUND_TAB);
@@ -94,7 +98,9 @@ function Hints() //{{{
hints.show("F"); hints.show("F");
} }
// reset all important variables /**
* Reset hints, so that they can be cleanly used again.
*/
function reset() function reset()
{ {
statusline.updateInputBuffer(""); statusline.updateInputBuffer("");
@@ -113,11 +119,164 @@ function Hints() //{{{
activeTimeout = null; activeTimeout = null;
} }
/**
* Display the current status to the user.
*/
function updateStatusline() function updateStatusline()
{ {
statusline.updateInputBuffer((hints.escNumbers ? mappings.getMapLeader() : "") + (hintNumber || "")); statusline.updateInputBuffer((hints.escNumbers ? mappings.getMapLeader() : "") + (hintNumber || ""));
} }
/**
* Get a hint for "input", "textarea" and "select".
*
* Tries to use <label>s if possible but does not try to guess that a
* neighbouring element might look like a label. Only called by generate()
*
* If it finds a hint it returns it, if the hint is not the caption of the
* element it will return showtext=true.
*
* @param {object} elem The element.
* @param {string} tagname Its tagname.
* @param {doc} The document it is in.
*
* @return [text, showtext]
*/
function getInputHint(elem, tagname, doc)
{
// <input type="submit|button|reset"> Always use the value
// <input type="radio|checkbox"> Use the value if it is not numeric or label or name
// <input type="password"> Never use the value, use label or name
// <input type="text|file"> <textarea> Use value if set or label or name
// <input type="image"> Use the alt text if present (showtext) or label or name
// <input type="hidden"> Never gets here
// <select> Use the text of the selected item or label or name
text = "";
let type = elem.type ? elem.type.toLowerCase() : "";
if (tagname == "input" && (type == "submit" || type == "button" || type == "reset"))
return [elem.value, true]
else
{
for each (let option in options["hintinputs"].split(","))
{
if (option == "value")
{
if (tagname == "select")
{
if (elem.selectedIndex >= 0)
return [elem.item(elem.selectedIndex).text.toLowerCase(), false]
}
else if (type == "image")
{
if (elem.alt)
return [elem.alt.toLowerCase(), true]
}
else if (elem.value && type != "password")
{
// radio's and checkboxes often use internal ids as values - maybe make this an option too...
if (! ((type == "radio" || type == "checkbox") && !isNaN(elem.value)))
return [elem.value.toLowerCase(), (type == "radio" || type == "checkbox")]
}
}
else if (option == "label")
{
if (elem.id)
{
//TODO: (possibly) do some guess work for label-like objects
let label = buffer.evaluateXPath("//label[@for='"+elem.id+"']", doc).snapshotItem(0);
if (label)
return [label.textContent.toLowerCase(),true]
}
}
else if (option == "name")
return [elem.name.toLowerCase(), true]
}
}
return ["", false]
}
/**
* Gets the actual offset of an imagemap area.
*
* Only called by generate()
*
* @param {object} elem The <area> element.
* @param {Number} leftpos The left offset of the image.
* @param {Number} toppos The top offset of the image.
*
* @return [leftpos, toppos] The updated offsets.
*/
function getAreaOffset(elem, leftpos, toppos)
{
try
{
// Need to add the offset to the area element.
// Always try to find the top-left point, as per vimperator default.
let shape = elem.getAttribute("shape").toLowerCase();
let coordstr = elem.getAttribute("coords");
// Technically it should be only commas, but hey
coordstr = coordstr.replace(/\s+[;,]\s+/g, ",").replace(/\s+/g, ",");
let coords = coordstr.split(",").map(Number);
if ((shape == "rect" || shape == "rectangle") && coords.length == 4)
{
leftpos += coords[0];
toppos += coords[1];
}
else if (shape == "circle" && coords.length == 3)
{
leftpos += coords[0] - coords[2] / Math.sqrt(2);
toppos += coords[1] - coords[2] / Math.sqrt(2);
}
else if ((shape == "poly" || shape == "polygon") && coords.length % 2 == 0)
{
let leftbound = Infinity;
let topbound = Infinity;
// First find the top-left corner of the bounding rectangle (offset from image topleft can be noticably suboptimal)
for (let i = 0; i < coords.length; i += 2)
{
leftbound = Math.min(coords[i], leftbound);
topbound = Math.min(coords[i+1], topbound);
}
let curtop = null;
let curleft = null;
let curdist = Infinity;
// Then find the closest vertex. (we could generalise to nearest point on an edge, but I doubt there is a need)
for (let i = 0; i < coords.length; i += 2)
{
let leftoffset = coords[i] - leftbound;
let topoffset = coords[i+1] - topbound;
let dist = Math.sqrt(leftoffset * leftoffset + topoffset * topoffset);
if (dist < curdist)
{
curdist = dist;
curleft = coords[i];
curtop = coords[i+1];
}
}
// If we found a satisfactory offset, let's use it.
if (curdist < Infinity)
{
return [leftpos + curleft, toppos + curtop]
}
}
}
catch (e) {} //badly formed document, or shape == "default" in which case we don't move the hint
return [leftpos, toppos]
}
/**
* Generate the hints in a window.
*
* Pushes the hints into the pageHints object, but does not display them.
*
* @param {object} win The window,defaults to window.content
*/
function generate(win) function generate(win)
{ {
if (!win) if (!win)
@@ -155,75 +314,7 @@ function Hints() //{{{
tagname = elem.localName.toLowerCase(); tagname = elem.localName.toLowerCase();
if (tagname == "input" || tagname == "select" || tagname == "textarea") if (tagname == "input" || tagname == "select" || tagname == "textarea")
{ [text, showtext] = getInputHint(elem, tagname, doc);
//TODO: Split this out somewhere...
// <input type="submit|button|reset"> Always use the value
// <input type="radio|checkbox"> Use the value if it is not numeric or label or name
// <input type="password"> Never use the value, use label or name
// <input type="text|file"> <textarea> Use value if set or label or name
// <input type="image"> Use the alt text if present (showtext) or label or name
// <input type="hidden"> Never gets here
// <select> Use the text of the selected item or label or name
text = "";
let type = elem.type ? elem.type.toLowerCase() : "";
if (tagname == "input" && (type == "submit" || type == "button" || type == "reset"))
{
text = elem.value
}
else
{
for each (let option in options["hintinputs"].split(","))
{
if (option == "value")
{
if (tagname == "select")
{
if (elem.selectedIndex >= 0)
text = elem.item(elem.selectedIndex).text;
}
else if (type == "image")
{
if (elem.alt)
{
text = elem.alt;
showtext = true;
}
}
else if (elem.value && type != "password")
{
// radio's and checkboxes often use internal ids as values - maybe make this an option too...
if (! ((type == "radio" || type == "checkbox") && !isNaN(elem.value)))
{
text = elem.value
showtext = true;
}
}
}
else if (option == "label")
{
if (elem.id)
{
//TODO: (possibly) do some guess work for label-like objects
let label = buffer.evaluateXPath("//label[@for='"+elem.id+"']", doc).snapshotItem(0);
if (label)
{
text = label.textContent.toLowerCase();
showtext = true;
}
}
}
else if (option == "name")
{
text = elem.name;
showtext = true;
}
if(text)
break;
}
}
}
else else
text = elem.textContent.toLowerCase(); text = elem.textContent.toLowerCase();
@@ -234,66 +325,7 @@ function Hints() //{{{
if (tagname == "area") if (tagname == "area")
{ {
// TODO: Maybe put the following into a seperate function [leftpos, toppos] = getAreaOffset(elem, leftpos, toppos);
try
{
// Need to add the offset to the area element.
// Always try to find the top-left point, as per vimperator default.
let shape = elem.getAttribute("shape").toLowerCase();
let coordstr = elem.getAttribute("coords");
// Technically it should be only commas, but hey
coordstr = coordstr.replace(/\s+[;,]\s+/g, ",").replace(/\s+/g, ",");
let coords = coordstr.split(",").map(Number);
if ((shape == "rect" || shape == "rectangle") && coords.length == 4)
{
leftpos += coords[0];
toppos += coords[1];
}
else if (shape == "circle" && coords.length == 3)
{
leftpos += coords[0] - coords[2] / Math.sqrt(2);
toppos += coords[1] - coords[2] / Math.sqrt(2);
}
else if ((shape == "poly" || shape == "polygon") && coords.length % 2 == 0)
{
let leftbound = Infinity;
let topbound = Infinity;
// First find the top-left corner of the bounding rectangle (offset from image topleft can be noticably suboptimal)
for (let i = 0; i < coords.length; i += 2)
{
leftbound = Math.min(coords[i], leftbound);
topbound = Math.min(coords[i+1], topbound);
}
let curtop = null;
let curleft = null;
let curdist = Infinity;
// Then find the closest vertex. (we could generalise to nearest point on an edge, but I doubt there is a need)
for (let i = 0; i < coords.length; i += 2)
{
let leftoffset = coords[i] - leftbound;
let topoffset = coords[i+1] - topbound;
let dist = Math.sqrt(leftoffset * leftoffset + topoffset * topoffset);
if (dist < curdist)
{
curdist = dist;
curleft = coords[i];
curtop = coords[i+1];
}
}
// If we found a satisfactory offset, let's use it.
if (curdist < Infinity)
{
leftpos += curleft;
toppos += curtop;
}
}
}
catch (e) {} //badly formed document, or shape == "default" in which case we don't move the hint
} }
span.style.left = leftpos + "px"; span.style.left = leftpos + "px";
@@ -315,7 +347,14 @@ function Hints() //{{{
return true; return true;
} }
// TODO: make it aware of imgspans /**
* Update the activeHint.
*
* By default highlights it green instead of yellow.
*
* @param {Number} newID The hint to make active
* @param {Number} oldID The currently active hint
*/
function showActiveHint(newID, oldID) function showActiveHint(newID, oldID)
{ {
let oldElem = validHints[oldID - 1]; let oldElem = validHints[oldID - 1];
@@ -327,6 +366,12 @@ function Hints() //{{{
setClass(newElem, true); setClass(newElem, true);
} }
/**
* Toggle the highlight of a hint.
*
* @param {object} elem The element to toggle.
* @param {boolean} active Whether it is the currently active hint or not.
*/
function setClass(elem, active) function setClass(elem, active)
{ {
let prefix = (elem.getAttributeNS(NS.uri, "class") || "") + " "; let prefix = (elem.getAttributeNS(NS.uri, "class") || "") + " ";
@@ -336,6 +381,9 @@ function Hints() //{{{
elem.setAttributeNS(NS.uri, "highlight", prefix + "HintElem"); elem.setAttributeNS(NS.uri, "highlight", prefix + "HintElem");
} }
/**
* Display the hints in pageHints that are still valid.
*/
function showHints() function showHints()
{ {
@@ -419,6 +467,14 @@ function Hints() //{{{
return true; return true;
} }
/**
* Remove all hints from the document, and reset the completions.
*
* Lingers on the active hint briefly to confirm the selection to the user.
*
* @param {Number} timeout The number of milliseconds before the active
* hint disappears.
*/
function removeHints(timeout) function removeHints(timeout)
{ {
let firstElem = validHints[0] || null; let firstElem = validHints[0] || null;
@@ -443,6 +499,16 @@ function Hints() //{{{
reset(); reset();
} }
/**
* Finish hinting.
*
* Called when there are one or zero hints in order to possibly activate it
* and, if activated, to clean up the rest of the hinting system.
*
* @param {boolean} followFirst Whether to force the following of the
* first link (when 'followhints' is 1 or 2)
*
*/
function processHints(followFirst) function processHints(followFirst)
{ {
if (validHints.length == 0) if (validHints.length == 0)
@@ -492,6 +558,14 @@ function Hints() //{{{
return true; return true;
} }
/**
* Handle user input.
*
* Will update the filter on displayed hints and follow the final hint if
* necessary.
*
* @param {Event} event The keypress event.
*/
function onInput (event) function onInput (event)
{ {
prevInput = "text"; prevInput = "text";
@@ -511,9 +585,38 @@ function Hints() //{{{
processHints(false); processHints(false);
} }
/**
* Get the hintMatcher according to user preference.
*
* @param {string} hintString The currently typed hint.
*
* @return {hintMatcher}
*/
function hintMatcher(hintString) //{{{ function hintMatcher(hintString) //{{{
{ {
/**
* Divide a string by a regular expression.
*
* @param {RegExp|String} pat The pattern to split on
* @param {String} string The string to split
*
* @return {Array(string)} The lowercased splits of the stting.
*/
function tokenize(pat, string) string.split(pat).map(String.toLowerCase); function tokenize(pat, string) string.split(pat).map(String.toLowerCase);
/**
* Get a hint matcher for hintmatching=contains
*
* The hintMatcher expects the user input to be space delimited and it
* returns true if each set of characters typed can be found, in any
* order, in the link.
*
* @param {String} hintString The string typed by the user.
*
* @return {function(String):bool} A function that takes the text of a
* hint and returns true if all the (space-delimited) sets of
* characters typed by the user can be found in it.
*/
function containsMatcher(hintString) //{{{ function containsMatcher(hintString) //{{{
{ {
let tokens = tokenize(/\s+/, hintString); let tokens = tokenize(/\s+/, hintString);
@@ -524,14 +627,40 @@ function Hints() //{{{
}; };
} //}}} } //}}}
/**
* Get a hintMatcher for hintmatching=firstletters|wordstartswith
*
* The hintMatcher will look for any division of the user input that
* would match the first letters of words. It will always only match
* words in order.
*
* @param {String} hintString The string typed by the user.
* @param {boolean} allowWordOverleaping Whether to allow non-contiguous
* words to match.
*
* @return {function(String):bool} A function that will filter only
* hints that match as above.
*/
function wordStartsWithMatcher(hintString, allowWordOverleaping) //{{{ function wordStartsWithMatcher(hintString, allowWordOverleaping) //{{{
{ {
let hintStrings = tokenize(/\s+/, hintString); let hintStrings = tokenize(/\s+/, hintString);
let wordSplitRegex = RegExp(options["wordseparators"]); let wordSplitRegex = RegExp(options["wordseparators"]);
// What the **** does this do? --Kris /**
// * Match a set of characters to the start of words.
// This function matches hintStrings like 'hekho' to links like 'Hey Kris, how are you?' -> [HE]y [K]ris [HO]w are you --Daniel *
* What the **** does this do? --Kris
* This function matches hintStrings like 'hekho' to links
* like 'Hey Kris, how are you?' -> [HE]y [K]ris [HO]w are you
* --Daniel
*
* @param {String} chars The characters to match
* @param {Array(String)} words The words to match them against
* @param {boolean} allowWordOverleaping Whether words may be
* skipped during matching.
*
* @return {boolean} Whether a match can be found.
*/
function charsAtBeginningOfWords(chars, words, allowWordOverleaping) function charsAtBeginningOfWords(chars, words, allowWordOverleaping)
{ {
function charMatches(charIdx, chars, wordIdx, words, inWordIdx, allowWordOverleaping) function charMatches(charIdx, chars, wordIdx, words, inWordIdx, allowWordOverleaping)
@@ -577,6 +706,21 @@ function Hints() //{{{
return charMatches(0, chars, 0, words, 0, allowWordOverleaping); return charMatches(0, chars, 0, words, 0, allowWordOverleaping);
} }
/**
* Check whether the array of strings all exist at the start of the words.
*
* i.e. ['ro', 'e'] would match ['rollover', 'effect']
*
* The matches must be in order, and, if allowWordOverleaping is false,
* contiguous.
*
* @param {Array(String)} strings The strings to search for.
* @param {Array(String)} words The words to search in.
* @param {boolean} allowWordOverleaping Whether matches may be
* non-contiguous.
*
* @return boolean Whether all the strings matched.
*/
function stringsAtBeginningOfWords(strings, words, allowWordOverleaping) function stringsAtBeginningOfWords(strings, words, allowWordOverleaping)
{ {
let strIdx = 0; let strIdx = 0;
@@ -738,11 +882,28 @@ function Hints() //{{{
return { return {
/**
* Create a new hint mode
*
* @param {String} mode The letter that identifies this mode.
* @param {String} description The description to display to the user about this mode.
* @param {function(Node)} callback The function to be called with the element that matches.
* @param {function():String} selector The function that returns an XPath selector to decide
* which elements can be hinted (the default returns
* options.hinttags)
*/
addMode: function (mode) addMode: function (mode)
{ {
hintModes[mode] = Mode.apply(Mode, Array.slice(arguments, 1)); hintModes[mode] = Mode.apply(Mode, Array.slice(arguments, 1));
}, },
/**
* Update the display of hints.
*
* @param {String} minor Which hint mode to use.
* @param {String} filter The filter to use.
* @param {Object} win The window in which we are showing hints
*/
show: function (minor, filter, win) show: function (minor, filter, win)
{ {
hintMode = hintModes[minor]; hintMode = hintModes[minor];
@@ -784,11 +945,19 @@ function Hints() //{{{
return true; return true;
}, },
/**
* Cancel all hinting.
*/
hide: function () hide: function ()
{ {
removeHints(0); removeHints(0);
}, },
/**
* Handle an event
*
* @param {Event} event The event to handle.
*/
onEvent: function (event) onEvent: function (event)
{ {
let key = events.toString(event); let key = events.toString(event);