From 42bb33d427e3d8c36c31753bd0c0a81bf330e4ce Mon Sep 17 00:00:00 2001 From: Jez Ng Date: Wed, 25 Jan 2012 23:55:40 -0500 Subject: Factor out common DOM functions. --- lib/domUtils.js | 92 +++++++++++++++++++++++++++++++++++++++++++ lib/utils.js | 63 ----------------------------- linkHints.js | 32 +++------------ manifest.json | 1 + test_harnesses/automated.html | 1 + vimiumFrontend.js | 14 +++---- 6 files changed, 107 insertions(+), 96 deletions(-) create mode 100644 lib/domUtils.js 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": [""], "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 @@ + 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 } -- cgit v1.2.3