diff options
| -rw-r--r-- | lib/domUtils.js | 92 | ||||
| -rw-r--r-- | lib/utils.js | 63 | ||||
| -rw-r--r-- | linkHints.js | 32 | ||||
| -rw-r--r-- | manifest.json | 1 | ||||
| -rw-r--r-- | test_harnesses/automated.html | 1 | ||||
| -rw-r--r-- | vimiumFrontend.js | 14 | 
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          } | 
