aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lib/domUtils.js92
-rw-r--r--lib/utils.js63
-rw-r--r--linkHints.js32
-rw-r--r--manifest.json1
-rw-r--r--test_harnesses/automated.html1
-rw-r--r--vimiumFrontend.js14
6 files changed, 107 insertions, 96 deletions
diff --git a/lib/domUtils.js b/lib/domUtils.js
new file mode 100644
index 00000000..700e1920
--- /dev/null
+++ b/lib/domUtils.js
@@ -0,0 +1,92 @@
+var domUtils = {
+ /*
+ * Takes an array of XPath selectors, adds the necessary namespaces (currently only XHTML), and applies them
+ * to the document root. The namespaceResolver in evaluateXPath should be kept in sync with the namespaces
+ * here.
+ */
+ makeXPath: function(elementArray) {
+ var xpath = [];
+ for (var i in elementArray)
+ xpath.push("//" + elementArray[i], "//xhtml:" + elementArray[i]);
+ return xpath.join(" | ");
+ },
+
+ evaluateXPath: function(xpath, resultType) {
+ function namespaceResolver(namespace) {
+ return namespace == "xhtml" ? "http://www.w3.org/1999/xhtml" : null;
+ }
+ return document.evaluate(xpath, document.documentElement, namespaceResolver, resultType, null);
+ },
+
+ /**
+ * Returns the first visible clientRect of an element if it exists. Otherwise it returns null.
+ */
+ getVisibleClientRect: function(element) {
+ // Note: this call will be expensive if we modify the DOM in between calls.
+ var clientRects = element.getClientRects();
+ var clientRectsLength = clientRects.length;
+
+ for (var i = 0; i < clientRectsLength; i++) {
+ if (clientRects[i].top < 0 || clientRects[i].top >= window.innerHeight - 4 ||
+ clientRects[i].left < 0 || clientRects[i].left >= window.innerWidth - 4)
+ continue;
+
+ if (clientRects[i].width < 3 || clientRects[i].height < 3)
+ continue;
+
+ // eliminate invisible elements (see test_harnesses/visibility_test.html)
+ var computedStyle = window.getComputedStyle(element, null);
+ if (computedStyle.getPropertyValue('visibility') != 'visible' ||
+ computedStyle.getPropertyValue('display') == 'none')
+ continue;
+
+ return clientRects[i];
+ }
+
+ for (var i = 0; i < clientRectsLength; i++) {
+ // If the link has zero dimensions, it may be wrapping visible
+ // but floated elements. Check for this.
+ if (clientRects[i].width == 0 || clientRects[i].height == 0) {
+ for (var j = 0, childrenCount = element.children.length; j < childrenCount; j++) {
+ var computedStyle = window.getComputedStyle(element.children[j], null);
+ // Ignore child elements which are not floated and not absolutely positioned for parent elements with zero width/height
+ if (computedStyle.getPropertyValue('float') == 'none' && computedStyle.getPropertyValue('position') != 'absolute')
+ continue;
+ var childClientRect = this.getVisibleClientRect(element.children[j]);
+ if (childClientRect === null)
+ continue;
+ return childClientRect;
+ }
+ }
+ };
+ return null;
+ },
+
+ /*
+ * Selectable means the element has a text caret; this is not the same as "focusable".
+ */
+ isSelectable: function(element) {
+ var selectableTypes = ["search", "text", "password"];
+ return (element.nodeName.toLowerCase() == "input" && selectableTypes.indexOf(element.type) >= 0) ||
+ element.nodeName.toLowerCase() == "textarea";
+ },
+
+ simulateSelect: function(element) {
+ element.focus();
+ // When focusing a textbox, put the selection caret at the end of the textbox's contents.
+ element.setSelectionRange(element.value.length, element.value.length);
+ },
+
+ simulateClick: function(element, modifiers) {
+ modifiers = modifiers || {};
+
+ var event = document.createEvent("MouseEvents");
+ event.initMouseEvent("click", true, true, window, 1, 0, 0, 0, 0, modifiers.ctrlKey, false, false,
+ modifiers.metaKey, 0, null);
+
+ // Debugging note: Firefox will not execute the element's default action if we dispatch this click event,
+ // but Webkit will. Dispatching a click on an input box does not seem to focus it; we do that separately
+ element.dispatchEvent(event);
+ },
+
+};
diff --git a/lib/utils.js b/lib/utils.js
index 6526fe89..df325138 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -12,69 +12,6 @@ var utils = {
return func.apply(obj, argArray);
},
- /*
- * Takes an array of XPath selectors, adds the necessary namespaces (currently only XHTML), and applies them
- * to the document root. The namespaceResolver in evaluateXPath should be kept in sync with the namespaces
- * here.
- */
- makeXPath: function(elementArray) {
- var xpath = [];
- for (var i in elementArray)
- xpath.push("//" + elementArray[i], "//xhtml:" + elementArray[i]);
- return xpath.join(" | ");
- },
-
- evaluateXPath: function(xpath, resultType) {
- function namespaceResolver(namespace) {
- return namespace == "xhtml" ? "http://www.w3.org/1999/xhtml" : null;
- }
- return document.evaluate(xpath, document.documentElement, namespaceResolver, resultType, null);
- },
-
- /**
- * Returns the first visible clientRect of an element if it exists. Otherwise it returns null.
- */
- getVisibleClientRect: function(element) {
- // Note: this call will be expensive if we modify the DOM in between calls.
- var clientRects = element.getClientRects();
- var clientRectsLength = clientRects.length;
-
- for (var i = 0; i < clientRectsLength; i++) {
- if (clientRects[i].top < 0 || clientRects[i].top >= window.innerHeight - 4 ||
- clientRects[i].left < 0 || clientRects[i].left >= window.innerWidth - 4)
- continue;
-
- if (clientRects[i].width < 3 || clientRects[i].height < 3)
- continue;
-
- // eliminate invisible elements (see test_harnesses/visibility_test.html)
- var computedStyle = window.getComputedStyle(element, null);
- if (computedStyle.getPropertyValue('visibility') != 'visible' ||
- computedStyle.getPropertyValue('display') == 'none')
- continue;
-
- return clientRects[i];
- }
-
- for (var i = 0; i < clientRectsLength; i++) {
- // If the link has zero dimensions, it may be wrapping visible
- // but floated elements. Check for this.
- if (clientRects[i].width == 0 || clientRects[i].height == 0) {
- for (var j = 0, childrenCount = element.children.length; j < childrenCount; j++) {
- var computedStyle = window.getComputedStyle(element.children[j], null);
- // Ignore child elements which are not floated and not absolutely positioned for parent elements with zero width/height
- if (computedStyle.getPropertyValue('float') == 'none' && computedStyle.getPropertyValue('position') != 'absolute')
- continue;
- var childClientRect = this.getVisibleClientRect(element.children[j]);
- if (childClientRect === null)
- continue;
- return childClientRect;
- }
- }
- };
- return null;
- },
-
/**
* Creates a search URL from the given :query.
*/
diff --git a/linkHints.js b/linkHints.js
index 1fdc0322..509b6c0d 100644
--- a/linkHints.js
+++ b/linkHints.js
@@ -41,7 +41,7 @@ var linkHints = {
* The final expression will be something like "//button | //xhtml:button | ..."
* We use translate() instead of lower-case() because Chrome only supports XPath 1.0.
*/
- clickableElementsXPath: utils.makeXPath(["a", "area[@href]", "textarea", "button", "select","input[not(@type='hidden')]",
+ clickableElementsXPath: domUtils.makeXPath(["a", "area[@href]", "textarea", "button", "select","input[not(@type='hidden')]",
"*[@onclick or @tabindex or @role='link' or @role='button' or " +
"@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"]),
@@ -115,14 +115,14 @@ var linkHints = {
* of digits needed to enumerate all of the links on screen.
*/
getVisibleClickableElements: function() {
- var resultSet = utils.evaluateXPath(this.clickableElementsXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
+ var resultSet = domUtils.evaluateXPath(this.clickableElementsXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE);
var visibleElements = [];
// Find all visible clickable elements.
for (var i = 0, count = resultSet.snapshotLength; i < count; i++) {
var element = resultSet.snapshotItem(i);
- var clientRect = utils.getVisibleClientRect(element, clientRect);
+ var clientRect = domUtils.getVisibleClientRect(element, clientRect);
if (clientRect !== null)
visibleElements.push({element: element, rect: clientRect});
@@ -204,8 +204,8 @@ var linkHints = {
activateLink: function(matchedLink, delay) {
var that = this;
this.delayMode = true;
- if (this.isSelectable(matchedLink)) {
- this.simulateSelect(matchedLink);
+ if (domUtils.isSelectable(matchedLink)) {
+ domUtils.simulateSelect(matchedLink);
this.deactivateMode(delay, function() { that.delayMode = false; });
} else {
if (this.shouldOpenWithQueue) {
@@ -231,25 +231,10 @@ var linkHints = {
}
},
- /*
- * Selectable means the element has a text caret; this is not the same as "focusable".
- */
- isSelectable: function(element) {
- var selectableTypes = ["search", "text", "password"];
- return (element.nodeName.toLowerCase() == "input" && selectableTypes.indexOf(element.type) >= 0) ||
- element.nodeName.toLowerCase() == "textarea";
- },
-
copyLinkUrl: function(link) {
chrome.extension.sendRequest({handler: 'copyLinkUrl', data: link.href});
},
- simulateSelect: function(element) {
- element.focus();
- // When focusing a textbox, put the selection caret at the end of the textbox's contents.
- element.setSelectionRange(element.value.length, element.value.length);
- },
-
/*
* Shows the marker, highlighting matchingCharCount characters.
*/
@@ -265,16 +250,11 @@ var linkHints = {
},
simulateClick: function(link) {
- var event = document.createEvent("MouseEvents");
// When "clicking" on a link, dispatch the event with the appropriate meta key (CMD on Mac, CTRL on windows)
// to open it in a new tab if necessary.
var metaKey = (platform == "Mac" && linkHints.shouldOpenInNewTab);
var ctrlKey = (platform != "Mac" && linkHints.shouldOpenInNewTab);
- event.initMouseEvent("click", true, true, window, 1, 0, 0, 0, 0, ctrlKey, false, false, metaKey, 0, null);
-
- // Debugging note: Firefox will not execute the link's default action if we dispatch this click event,
- // but Webkit will. Dispatching a click on an input box does not seem to focus it; we do that separately
- link.dispatchEvent(event);
+ domUtils.simulateClick(link, { metaKey: metaKey, ctrlKey: ctrlKey });
// TODO(int3): do this for @role='link' and similar elements as well
var nodeName = link.nodeName.toLowerCase();
diff --git a/manifest.json b/manifest.json
index 954d5e35..23f80f77 100644
--- a/manifest.json
+++ b/manifest.json
@@ -18,6 +18,7 @@
"matches": ["<all_urls>"],
"js": ["lib/utils.js",
"lib/keyboardUtils.js",
+ "lib/domUtils.js",
"lib/clipboard.js",
"linkHints.js",
"vimiumFrontend.js",
diff --git a/test_harnesses/automated.html b/test_harnesses/automated.html
index 28af7c0e..c8e8070a 100644
--- a/test_harnesses/automated.html
+++ b/test_harnesses/automated.html
@@ -29,6 +29,7 @@
<link rel="stylesheet" type="text/css" href="../vimium.css" />
<script type="text/javascript" src="../lib/utils.js"></script>
<script type="text/javascript" src="../lib/keyboardUtils.js"></script>
+ <script type="text/javascript" src="../lib/domUtils.js"></script>
<script type="text/javascript" src="../linkHints.js"></script>
<script type="text/javascript" src="../lib/clipboard.js"></script>
<script type="text/javascript" src="../vimiumFrontend.js"></script>
diff --git a/vimiumFrontend.js b/vimiumFrontend.js
index d0daa448..84a03674 100644
--- a/vimiumFrontend.js
+++ b/vimiumFrontend.js
@@ -34,7 +34,7 @@ var textInputXPath = (function() {
var inputElements = ["input[" +
textInputTypes.map(function (type) { return '@type="' + type + '"'; }).join(" or ") + "or not(@type)]",
"textarea", "*[@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"];
- return utils.makeXPath(inputElements);
+ return domUtils.makeXPath(inputElements);
})();
/**
@@ -254,7 +254,7 @@ function scrollActivatedElementBy(x, y) {
return;
}
- if (!activatedElement || utils.getVisibleClientRect(activatedElement) === null)
+ if (!activatedElement || domUtils.getVisibleClientRect(activatedElement) === null)
activatedElement = document.body;
// Chrome does not report scrollHeight accurately for nodes with pseudo-elements of height 0 (bug 110149).
@@ -304,7 +304,7 @@ function scrollLeft() { scrollActivatedElementBy(-1 * settings.get("scrollStepSi
function scrollRight() { scrollActivatedElementBy(settings.get("scrollStepSize"), 0); }
function focusInput(count) {
- var results = utils.evaluateXPath(textInputXPath, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
+ var results = domUtils.evaluateXPath(textInputXPath, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
var lastInputBox;
var i = 0;
@@ -313,7 +313,7 @@ function focusInput(count) {
var currentInputBox = results.iterateNext();
if (!currentInputBox) { break; }
- if (utils.getVisibleClientRect(currentInputBox) === null)
+ if (domUtils.getVisibleClientRect(currentInputBox) === null)
continue;
lastInputBox = currentInputBox;
@@ -766,9 +766,9 @@ function selectFoundInputElement() {
// instead. however, since the last focused element might not be the one currently pointed to by find (e.g.
// the current one might be disabled and therefore unable to receive focus), we use the approximate
// heuristic of checking that the last anchor node is an ancestor of our element.
- if (findModeQueryHasResults && linkHints.isSelectable(document.activeElement) &&
+ if (findModeQueryHasResults && domUtils.isSelectable(document.activeElement) &&
isDOMDescendant(findModeAnchorNode, document.activeElement)) {
- linkHints.simulateSelect(document.activeElement);
+ domUtils.simulateSelect(document.activeElement);
// the element has already received focus via find(), so invoke insert mode manually
enterInsertModeWithoutShowingIndicator(document.activeElement);
}
@@ -812,7 +812,7 @@ function findAndFocus(backwards) {
keydown: function(event) {
handlerStack.pop();
if (isEscape(event)) {
- linkHints.simulateSelect(document.activeElement);
+ domUtils.simulateSelect(document.activeElement);
enterInsertModeWithoutShowingIndicator(document.activeElement);
return false; // we have 'consumed' this event, so do not propagate
}