diff options
Diffstat (limited to 'content_scripts')
| -rw-r--r-- | content_scripts/hud.coffee | 6 | ||||
| -rw-r--r-- | content_scripts/mode_find.coffee | 52 | ||||
| -rw-r--r-- | content_scripts/mode_normal.coffee | 346 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 395 | 
4 files changed, 414 insertions, 385 deletions
| diff --git a/content_scripts/hud.coffee b/content_scripts/hud.coffee index 6185d786..7c983cfa 100644 --- a/content_scripts/hud.coffee +++ b/content_scripts/hud.coffee @@ -71,13 +71,13 @@ HUD =      focusNode?.focus?()      if exitEventIsEnter -      handleEnterForFindMode() +      FindMode.handleEnter()        if FindMode.query.hasResults          postExit = -> new PostFindMode      else if exitEventIsEscape -      # We don't want FindMode to handle the click events that handleEscapeForFindMode can generate, so we +      # We don't want FindMode to handle the click events that FindMode.handleEscape can generate, so we        # wait until the mode is closed before running it. -      postExit = handleEscapeForFindMode +      postExit = FindMode.handleEscape      @findMode.exit()      postExit?() diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 5238ab34..b6c80cec 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -79,7 +79,7 @@ class FindMode extends Mode    exit: (event) ->      super() -    handleEscapeForFindMode() if event +    FindMode.handleEscape() if event    restoreSelection: ->      range = @initialRange @@ -201,6 +201,34 @@ class FindMode extends Mode    @restoreDefaultSelectionHighlight: forTrusted -> document.body.classList.remove("vimiumFindMode") +  # The user has found what they're looking for and is finished searching. We enter insert mode, if possible. +  @handleEscape: -> +    document.body.classList.remove("vimiumFindMode") +    # Removing the class does not re-color existing selections. we recreate the current selection so it reverts +    # back to the default color. +    selection = window.getSelection() +    unless selection.isCollapsed +      range = window.getSelection().getRangeAt(0) +      window.getSelection().removeAllRanges() +      window.getSelection().addRange(range) +    focusFoundLink() || selectFoundInputElement() + +  # Save the query so the user can do further searches with it. +  @handleEnter: -> +    focusFoundLink() +    document.body.classList.add("vimiumFindMode") +    FindMode.saveQuery() + +  @findNext: (backwards) -> +    Marks.setPreviousPosition() +    FindMode.query.hasResults = FindMode.execute null, {backwards} + +    if FindMode.query.hasResults +      focusFoundLink() +      new PostFindMode() +    else +      HUD.showForDuration("No matches for '#{FindMode.query.rawQuery}'", 1000) +    checkReturnToViewPort: ->      window.scrollTo @scrollX, @scrollY if @options.returnToViewport @@ -215,6 +243,28 @@ getCurrentRange = ->      selection.collapseToStart() if selection.type == "Range"      selection.getRangeAt 0 +getLinkFromSelection = -> +  node = window.getSelection().anchorNode +  while (node && node != document.body) +    return node if (node.nodeName.toLowerCase() == "a") +    node = node.parentNode +  null + +focusFoundLink = -> +  if (FindMode.query.hasResults) +    link = getLinkFromSelection() +    link.focus() if link + +selectFoundInputElement = -> +  # 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. +  findModeAnchorNode = document.getSelection().anchorNode +  if (FindMode.query.hasResults && document.activeElement && +      DomUtils.isSelectable(document.activeElement) && +      DomUtils.isDOMDescendant(findModeAnchorNode, document.activeElement)) +    DomUtils.simulateSelect(document.activeElement) +  root = exports ? (window.root ?= {})  root.PostFindMode = PostFindMode  root.FindMode = FindMode diff --git a/content_scripts/mode_normal.coffee b/content_scripts/mode_normal.coffee new file mode 100644 index 00000000..ee05f4b0 --- /dev/null +++ b/content_scripts/mode_normal.coffee @@ -0,0 +1,346 @@ +class NormalMode extends KeyHandlerMode +  constructor: (options = {}) -> +    defaults = +      name: "normal" +      indicator: false # There is normally no mode indicator in normal mode. +      commandHandler: @commandHandler.bind this + +    super extend defaults, options + +    chrome.storage.local.get "normalModeKeyStateMapping", (items) => +      @setKeyMapping items.normalModeKeyStateMapping + +    chrome.storage.onChanged.addListener (changes, area) => +      if area == "local" and changes.normalModeKeyStateMapping?.newValue +        @setKeyMapping changes.normalModeKeyStateMapping.newValue + +  commandHandler: ({command: registryEntry, count}) -> +    count *= registryEntry.options.count ? 1 +    count = 1 if registryEntry.noRepeat + +    if registryEntry.repeatLimit? and registryEntry.repeatLimit < count +      return unless confirm """ +        You have asked Vimium to perform #{count} repetitions of the command: #{registryEntry.description}.\n +        Are you sure you want to continue?""" + +    if registryEntry.topFrame +      # We never return to a UI-component frame (e.g. the help dialog), it might have lost the focus. +      sourceFrameId = if window.isVimiumUIComponent then 0 else frameId +      chrome.runtime.sendMessage +        handler: "sendMessageToFrames", message: {name: "runInTopFrame", sourceFrameId, registryEntry} +    else if registryEntry.background +      chrome.runtime.sendMessage {handler: "runBackgroundCommand", registryEntry, count} +    else +      NormalModeCommands[registryEntry.command] count, {registryEntry} + +enterNormalMode = (count) -> +  new NormalMode +    indicator: "Normal mode (pass keys disabled)" +    exitOnEscape: true +    singleton: "enterNormalMode" +    count: count + +NormalModeCommands = +  # Scrolling. +  scrollToBottom: -> +    Marks.setPreviousPosition() +    Scroller.scrollTo "y", "max" +  scrollToTop: (count) -> +    Marks.setPreviousPosition() +    Scroller.scrollTo "y", (count - 1) * Settings.get("scrollStepSize") +  scrollToLeft: -> Scroller.scrollTo "x", 0 +  scrollToRight: -> Scroller.scrollTo "x", "max" +  scrollUp: (count) -> Scroller.scrollBy "y", -1 * Settings.get("scrollStepSize") * count +  scrollDown: (count) -> Scroller.scrollBy "y", Settings.get("scrollStepSize") * count +  scrollPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1/2 * count +  scrollPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1/2 * count +  scrollFullPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1 * count +  scrollFullPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1 * count +  scrollLeft: (count) -> Scroller.scrollBy "x", -1 * Settings.get("scrollStepSize") * count +  scrollRight: (count) -> Scroller.scrollBy "x", Settings.get("scrollStepSize") * count + +  # Page state. +  reload: (count, options) -> +    hard = options.registryEntry.options.hard ? false +    window.location.reload(hard) +  goBack: (count) -> history.go(-count) +  goForward: (count) -> history.go(count) + +  # Url manipulation. +  goUp: (count) -> +    url = window.location.href +    if (url[url.length - 1] == "/") +      url = url.substring(0, url.length - 1) + +    urlsplit = url.split("/") +    # make sure we haven't hit the base domain yet +    if (urlsplit.length > 3) +      urlsplit = urlsplit.slice(0, Math.max(3, urlsplit.length - count)) +      window.location.href = urlsplit.join('/') + +  goToRoot: -> +    window.location.href = window.location.origin + +  toggleViewSource: -> +    chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) -> +      if (url.substr(0, 12) == "view-source:") +        url = url.substr(12, url.length - 12) +      else +        url = "view-source:" + url +      chrome.runtime.sendMessage {handler: "openUrlInNewTab", url} + +  copyCurrentUrl: -> +    chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) -> +      chrome.runtime.sendMessage { handler: "copyToClipboard", data: url } +      url = url[0..25] + "...." if 28 < url.length +      HUD.showForDuration("Yanked #{url}", 2000) + +  # Mode changes. +  enterInsertMode: -> +    # If a focusable element receives the focus, then we exit and leave the permanently-installed insert-mode +    # instance to take over. +    new InsertMode global: true, exitOnFocus: true + +  enterVisualMode: -> +    new VisualMode userLaunchedMode: true + +  enterVisualLineMode: -> +    new VisualLineMode userLaunchedMode: true + +  enterFindMode: -> +    Marks.setPreviousPosition() +    new FindMode() + +  # Find. +  performFind: (count) -> FindMode.findNext false for [0...count] by 1 +  performBackwardsFind: (count) -> FindMode.findNext true for [0...count] by 1 + +  # Misc. +  mainFrame: -> focusThisFrame highlight: true, forceFocusThisFrame: true +  showHelp: (sourceFrameId) -> HelpDialog.toggle {sourceFrameId, showAllCommandDetails: false} + +  passNextKey: (count, options) -> +    if options.registryEntry.options.normal +      enterNormalMode count +    else +      new PassNextKeyMode count + +  goPrevious: -> +    previousPatterns = Settings.get("previousPatterns") || "" +    previousStrings = previousPatterns.split(",").filter( (s) -> s.trim().length ) +    findAndFollowRel("prev") || findAndFollowLink(previousStrings) + +  goNext: -> +    nextPatterns = Settings.get("nextPatterns") || "" +    nextStrings = nextPatterns.split(",").filter( (s) -> s.trim().length ) +    findAndFollowRel("next") || findAndFollowLink(nextStrings) + +  focusInput: (count) -> +    # Focus the first input element on the page, and create overlays to highlight all the input elements, with +    # the currently-focused element highlighted specially. Tabbing will shift focus to the next input element. +    # Pressing any other key will remove the overlays and the special tab behavior. +    resultSet = DomUtils.evaluateXPath textInputXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE +    visibleInputs = +      for i in [0...resultSet.snapshotLength] by 1 +        element = resultSet.snapshotItem i +        continue unless DomUtils.getVisibleClientRect element, true +        { element, rect: Rect.copy element.getBoundingClientRect() } + +    if visibleInputs.length == 0 +      HUD.showForDuration("There are no inputs to focus.", 1000) +      return + +    # This is a hack to improve usability on the Vimium options page.  We prime the recently-focused input +    # to be the key-mappings input.  Arguably, this is the input that the user is most likely to use. +    recentlyFocusedElement = lastFocusedInput() +    recentlyFocusedElement ?= document.getElementById "keyMappings" if window.isVimiumOptionsPage + +    selectedInputIndex = +      if count == 1 +        # As the starting index, we pick that of the most recently focused input element (or 0). +        elements = visibleInputs.map (visibleInput) -> visibleInput.element +        Math.max 0, elements.indexOf recentlyFocusedElement +      else +        Math.min(count, visibleInputs.length) - 1 + +    hints = for tuple in visibleInputs +      hint = DomUtils.createElement "div" +      hint.className = "vimiumReset internalVimiumInputHint vimiumInputHint" + +      # minus 1 for the border +      hint.style.left = (tuple.rect.left - 1) + window.scrollX + "px" +      hint.style.top = (tuple.rect.top - 1) + window.scrollY  + "px" +      hint.style.width = tuple.rect.width + "px" +      hint.style.height = tuple.rect.height + "px" + +      hint + +    new FocusSelector hints, visibleInputs, selectedInputIndex + +if LinkHints? +  extend NormalModeCommands, +    "LinkHints.activateMode": LinkHints.activateMode.bind LinkHints +    "LinkHints.activateModeToOpenInNewTab": LinkHints.activateModeToOpenInNewTab.bind LinkHints +    "LinkHints.activateModeToOpenInNewForegroundTab": LinkHints.activateModeToOpenInNewForegroundTab.bind LinkHints +    "LinkHints.activateModeWithQueue": LinkHints.activateModeWithQueue.bind LinkHints +    "LinkHints.activateModeToOpenIncognito": LinkHints.activateModeToOpenIncognito.bind LinkHints +    "LinkHints.activateModeToDownloadLink": LinkHints.activateModeToDownloadLink.bind LinkHints +    "LinkHints.activateModeToCopyLinkUrl": LinkHints.activateModeToCopyLinkUrl.bind LinkHints + +if Vomnibar? +  extend NormalModeCommands, +    "Vomnibar.activate": Vomnibar.activate.bind Vomnibar +    "Vomnibar.activateInNewTab": Vomnibar.activateInNewTab.bind Vomnibar +    "Vomnibar.activateTabSelection": Vomnibar.activateTabSelection.bind Vomnibar +    "Vomnibar.activateBookmarks": Vomnibar.activateBookmarks.bind Vomnibar +    "Vomnibar.activateBookmarksInNewTab": Vomnibar.activateBookmarksInNewTab.bind Vomnibar +    "Vomnibar.activateEditUrl": Vomnibar.activateEditUrl.bind Vomnibar +    "Vomnibar.activateEditUrlInNewTab": Vomnibar.activateEditUrlInNewTab.bind Vomnibar + +if Marks? +  extend NormalModeCommands, +    "Marks.activateCreateMode": Marks.activateCreateMode.bind Marks +    "Marks.activateGotoMode": Marks.activateGotoMode.bind Marks + +# The types in <input type="..."> that we consider for focusInput command. Right now this is recalculated in +# each content script. Alternatively we could calculate it once in the background page and use a request to +# fetch it each time. +# Should we include the HTML5 date pickers here? + +# The corresponding XPath for such elements. +textInputXPath = (-> +  textInputTypes = [ "text", "search", "email", "url", "number", "password", "date", "tel" ] +  inputElements = ["input[" + +    "(" + textInputTypes.map((type) -> '@type="' + type + '"').join(" or ") + "or not(@type))" + +    " and not(@disabled or @readonly)]", +    "textarea", "*[@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"] +  DomUtils?.makeXPath(inputElements) +)() + +# used by the findAndFollow* functions. +followLink = (linkElement) -> +  if (linkElement.nodeName.toLowerCase() == "link") +    window.location.href = linkElement.href +  else +    # if we can click on it, don't simply set location.href: some next/prev links are meant to trigger AJAX +    # calls, like the 'more' button on GitHub's newsfeed. +    linkElement.scrollIntoView() +    DomUtils.simulateClick(linkElement) + +# +# Find and follow a link which matches any one of a list of strings. If there are multiple such links, they +# are prioritized for shortness, by their position in :linkStrings, how far down the page they are located, +# and finally by whether the match is exact. Practically speaking, this means we favor 'next page' over 'the +# next big thing', and 'more' over 'nextcompany', even if 'next' occurs before 'more' in :linkStrings. +# +findAndFollowLink = (linkStrings) -> +  linksXPath = DomUtils.makeXPath(["a", "*[@onclick or @role='link' or contains(@class, 'button')]"]) +  links = DomUtils.evaluateXPath(linksXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) +  candidateLinks = [] + +  # at the end of this loop, candidateLinks will contain all visible links that match our patterns +  # links lower in the page are more likely to be the ones we want, so we loop through the snapshot backwards +  for i in [(links.snapshotLength - 1)..0] by -1 +    link = links.snapshotItem(i) + +    # ensure link is visible (we don't mind if it is scrolled offscreen) +    boundingClientRect = link.getBoundingClientRect() +    if (boundingClientRect.width == 0 || boundingClientRect.height == 0) +      continue +    computedStyle = window.getComputedStyle(link, null) +    if (computedStyle.getPropertyValue("visibility") != "visible" || +        computedStyle.getPropertyValue("display") == "none") +      continue + +    linkMatches = false +    for linkString in linkStrings +      if link.innerText.toLowerCase().indexOf(linkString) != -1 || +          0 <= link.value?.indexOf? linkString +        linkMatches = true +        break +    continue unless linkMatches + +    candidateLinks.push(link) + +  return if (candidateLinks.length == 0) + +  for link in candidateLinks +    link.wordCount = link.innerText.trim().split(/\s+/).length + +  # We can use this trick to ensure that Array.sort is stable. We need this property to retain the reverse +  # in-page order of the links. + +  candidateLinks.forEach((a,i) -> a.originalIndex = i) + +  # favor shorter links, and ignore those that are more than one word longer than the shortest link +  candidateLinks = +    candidateLinks +      .sort((a, b) -> +        if (a.wordCount == b.wordCount) then a.originalIndex - b.originalIndex else a.wordCount - b.wordCount +      ) +      .filter((a) -> a.wordCount <= candidateLinks[0].wordCount + 1) + +  for linkString in linkStrings +    exactWordRegex = +      if /\b/.test(linkString[0]) or /\b/.test(linkString[linkString.length - 1]) +        new RegExp "\\b" + linkString + "\\b", "i" +      else +        new RegExp linkString, "i" +    for candidateLink in candidateLinks +      if exactWordRegex.test(candidateLink.innerText) || +          (candidateLink.value && exactWordRegex.test(candidateLink.value)) +        followLink(candidateLink) +        return true +  false + +findAndFollowRel = (value) -> +  relTags = ["link", "a", "area"] +  for tag in relTags +    elements = document.getElementsByTagName(tag) +    for element in elements +      if (element.hasAttribute("rel") && element.rel.toLowerCase() == value) +        followLink(element) +        return true + +class FocusSelector extends Mode +  constructor: (hints, visibleInputs, selectedInputIndex) -> +    super +      name: "focus-selector" +      exitOnClick: true +      keydown: (event) => +        if event.key == "Tab" +          hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint' +          selectedInputIndex += hints.length + (if event.shiftKey then -1 else 1) +          selectedInputIndex %= hints.length +          hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' +          DomUtils.simulateSelect visibleInputs[selectedInputIndex].element +          @suppressEvent +        else unless event.key == "Shift" +          @exit() +          # Give the new mode the opportunity to handle the event. +          @restartBubbling + +    @hintContainingDiv = DomUtils.addElementList hints, +      id: "vimiumInputMarkerContainer" +      className: "vimiumReset" + +    DomUtils.simulateSelect visibleInputs[selectedInputIndex].element +    if visibleInputs.length == 1 +      @exit() +      return +    else +      hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' + +  exit: -> +    super() +    DomUtils.removeElement @hintContainingDiv +    if document.activeElement and DomUtils.isEditable document.activeElement +      new InsertMode +        singleton: "post-find-mode/focus-input" +        targetElement: document.activeElement +        indicator: false + +root = exports ? (window.root ?= {}) +root.NormalMode = NormalMode +root.NormalModeCommands = NormalModeCommands +extend window, root unless exports? diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 86e3b682..bbc3e4f1 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -22,21 +22,6 @@ windowIsFocused = do ->      windowHasFocus = false if event.target == window; true    -> windowHasFocus -# The types in <input type="..."> that we consider for focusInput command. Right now this is recalculated in -# each content script. Alternatively we could calculate it once in the background page and use a request to -# fetch it each time. -# Should we include the HTML5 date pickers here? - -# The corresponding XPath for such elements. -textInputXPath = (-> -  textInputTypes = [ "text", "search", "email", "url", "number", "password", "date", "tel" ] -  inputElements = ["input[" + -    "(" + textInputTypes.map((type) -> '@type="' + type + '"').join(" or ") + "or not(@type))" + -    " and not(@disabled or @readonly)]", -    "textarea", "*[@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"] -  DomUtils.makeXPath(inputElements) -)() -  # This is set by Frame.registerFrameId(). A frameId of 0 indicates that this is the top frame in the tab.  frameId = null @@ -117,41 +102,6 @@ handlerStack.push          target = target.parentElement      true -class NormalMode extends KeyHandlerMode -  constructor: (options = {}) -> -    defaults = -      name: "normal" -      indicator: false # There is normally no mode indicator in normal mode. -      commandHandler: @commandHandler.bind this - -    super extend defaults, options - -    chrome.storage.local.get "normalModeKeyStateMapping", (items) => -      @setKeyMapping items.normalModeKeyStateMapping - -    chrome.storage.onChanged.addListener (changes, area) => -      if area == "local" and changes.normalModeKeyStateMapping?.newValue -        @setKeyMapping changes.normalModeKeyStateMapping.newValue - -  commandHandler: ({command: registryEntry, count}) -> -    count *= registryEntry.options.count ? 1 -    count = 1 if registryEntry.noRepeat - -    if registryEntry.repeatLimit? and registryEntry.repeatLimit < count -      return unless confirm """ -        You have asked Vimium to perform #{count} repetitions of the command: #{registryEntry.description}.\n -        Are you sure you want to continue?""" - -    if registryEntry.topFrame -      # We never return to a UI-component frame (e.g. the help dialog), it might have lost the focus. -      sourceFrameId = if window.isVimiumUIComponent then 0 else frameId -      chrome.runtime.sendMessage -        handler: "sendMessageToFrames", message: {name: "runInTopFrame", sourceFrameId, registryEntry} -    else if registryEntry.background -      chrome.runtime.sendMessage {handler: "runBackgroundCommand", registryEntry, count} -    else -      Utils.invokeCommandString registryEntry.command, count, {registryEntry} -  installModes = ->    # Install the permanent modes. The permanently-installed insert mode tracks focus/blur events, and    # activates/deactivates itself accordingly. @@ -192,7 +142,7 @@ initializePreDomReady = ->      frameFocused: -> # A frame has received the focus; we don't care here (UI components handle this).      checkEnabledAfterURLChange: checkEnabledAfterURLChange      runInTopFrame: ({sourceFrameId, registryEntry}) -> -      Utils.invokeCommandString registryEntry.command, sourceFrameId, registryEntry if DomUtils.isTopFrame() +      NormalModeCommands[registryEntry.command] sourceFrameId, registryEntry if DomUtils.isTopFrame()      linkHintsMessage: (request) -> HintCoordinator[request.messageType] request    chrome.runtime.onMessage.addListener (request, sender, sendResponse) -> @@ -333,176 +283,17 @@ focusThisFrame = (request) ->    document.activeElement.blur() if document.activeElement.tagName.toLowerCase() == "iframe"    flashFrame() if request.highlight -extend root, -  scrollToBottom: -> -    Marks.setPreviousPosition() -    Scroller.scrollTo "y", "max" -  scrollToTop: (count) -> -    Marks.setPreviousPosition() -    Scroller.scrollTo "y", (count - 1) * Settings.get("scrollStepSize") -  scrollToLeft: -> Scroller.scrollTo "x", 0 -  scrollToRight: -> Scroller.scrollTo "x", "max" -  scrollUp: (count) -> Scroller.scrollBy "y", -1 * Settings.get("scrollStepSize") * count -  scrollDown: (count) -> Scroller.scrollBy "y", Settings.get("scrollStepSize") * count -  scrollPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1/2 * count -  scrollPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1/2 * count -  scrollFullPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1 * count -  scrollFullPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1 * count -  scrollLeft: (count) -> Scroller.scrollBy "x", -1 * Settings.get("scrollStepSize") * count -  scrollRight: (count) -> Scroller.scrollBy "x", Settings.get("scrollStepSize") * count - -extend root, -  reload: (count, options) -> -    hard = options.registryEntry.options.hard ? false -    window.location.reload(hard) -  goBack: (count) -> history.go(-count) -  goForward: (count) -> history.go(count) - -  goUp: (count) -> -    url = window.location.href -    if (url[url.length - 1] == "/") -      url = url.substring(0, url.length - 1) - -    urlsplit = url.split("/") -    # make sure we haven't hit the base domain yet -    if (urlsplit.length > 3) -      urlsplit = urlsplit.slice(0, Math.max(3, urlsplit.length - count)) -      window.location.href = urlsplit.join('/') - -  goToRoot: -> -    window.location.href = window.location.origin - -  mainFrame: -> focusThisFrame highlight: true, forceFocusThisFrame: true - -  toggleViewSource: -> -    chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) -> -      if (url.substr(0, 12) == "view-source:") -        url = url.substr(12, url.length - 12) -      else -        url = "view-source:" + url -      chrome.runtime.sendMessage {handler: "openUrlInNewTab", url} - -  copyCurrentUrl: -> -    # TODO(ilya): When the following bug is fixed, revisit this approach of sending back to the background -    # page to copy. -    # http://code.google.com/p/chromium/issues/detail?id=55188 -    chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) -> -      chrome.runtime.sendMessage { handler: "copyToClipboard", data: url } -      url = url[0..25] + "...." if 28 < url.length -      HUD.showForDuration("Yanked #{url}", 2000) - -  enterInsertMode: -> -    # If a focusable element receives the focus, then we exit and leave the permanently-installed insert-mode -    # instance to take over. -    new InsertMode global: true, exitOnFocus: true - -  enterVisualMode: -> -    new VisualMode userLaunchedMode: true - -  enterVisualLineMode: -> -    new VisualLineMode userLaunchedMode: true - -  passNextKey: (count, options) -> -    if options.registryEntry.options.normal -      enterNormalMode count -    else -      new PassNextKeyMode count - -  enterNormalMode: (count) -> -    new NormalMode -      indicator: "Normal mode (pass keys disabled)" -      exitOnEscape: true -      singleton: "enterNormalMode" -      count: count - -  focusInput: do -> -    # Track the most recently focused input element. -    recentlyFocusedElement = null -    window.addEventListener "focus", -      forTrusted (event) -> -        DomUtils = window.DomUtils ? root.DomUtils # Workaround FF bug 1408996. -        if DomUtils.isEditable event.target -          recentlyFocusedElement = event.target -    , true - -    (count) -> -      mode = InsertMode -      # Focus the first input element on the page, and create overlays to highlight all the input elements, with -      # the currently-focused element highlighted specially. Tabbing will shift focus to the next input element. -      # Pressing any other key will remove the overlays and the special tab behavior. -      # The mode argument is the mode to enter once an input is selected. -      resultSet = DomUtils.evaluateXPath textInputXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE -      visibleInputs = -        for i in [0...resultSet.snapshotLength] by 1 -          element = resultSet.snapshotItem i -          continue unless DomUtils.getVisibleClientRect element, true -          { element, rect: Rect.copy element.getBoundingClientRect() } - -      if visibleInputs.length == 0 -        HUD.showForDuration("There are no inputs to focus.", 1000) -        return - -      # This is a hack to improve usability on the Vimium options page.  We prime the recently-focused input -      # to be the key-mappings input.  Arguably, this is the input that the user is most likely to use. -      recentlyFocusedElement ?= document.getElementById "keyMappings" if window.isVimiumOptionsPage - -      selectedInputIndex = -        if count == 1 -          # As the starting index, we pick that of the most recently focused input element (or 0). -          elements = visibleInputs.map (visibleInput) -> visibleInput.element -          Math.max 0, elements.indexOf recentlyFocusedElement -        else -          Math.min(count, visibleInputs.length) - 1 - -      hints = for tuple in visibleInputs -        hint = DomUtils.createElement "div" -        hint.className = "vimiumReset internalVimiumInputHint vimiumInputHint" - -        # minus 1 for the border -        hint.style.left = (tuple.rect.left - 1) + window.scrollX + "px" -        hint.style.top = (tuple.rect.top - 1) + window.scrollY  + "px" -        hint.style.width = tuple.rect.width + "px" -        hint.style.height = tuple.rect.height + "px" - -        hint - -      new class FocusSelector extends Mode -        constructor: -> -          super -            name: "focus-selector" -            exitOnClick: true -            keydown: (event) => -              if event.key == "Tab" -                hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint' -                selectedInputIndex += hints.length + (if event.shiftKey then -1 else 1) -                selectedInputIndex %= hints.length -                hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' -                DomUtils.simulateSelect visibleInputs[selectedInputIndex].element -                @suppressEvent -              else unless event.key == "Shift" -                @exit() -                # Give the new mode the opportunity to handle the event. -                @restartBubbling - -          @hintContainingDiv = DomUtils.addElementList hints, -            id: "vimiumInputMarkerContainer" -            className: "vimiumReset" - -          DomUtils.simulateSelect visibleInputs[selectedInputIndex].element -          if visibleInputs.length == 1 -            @exit() -            return -          else -            hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' - -        exit: -> -          super() -          DomUtils.removeElement @hintContainingDiv -          if mode and document.activeElement and DomUtils.isEditable document.activeElement -            new mode -              singleton: "post-find-mode/focus-input" -              targetElement: document.activeElement -              indicator: false +# Used by focusInput command. +root.lastFocusedInput = do -> +  # Track the most recently focused input element. +  recentlyFocusedElement = null +  window.addEventListener "focus", +    forTrusted (event) -> +      DomUtils = window.DomUtils ? root.DomUtils # Workaround FF bug 1408996. +      if DomUtils.isEditable event.target +        recentlyFocusedElement = event.target +  , true +  -> recentlyFocusedElement  # Checks if Vimium should be enabled or not in this frame.  As a side effect, it also informs the background  # page whether this frame has the focus, allowing the background page to track the active frame's URL and set @@ -524,163 +315,6 @@ checkIfEnabledForUrl = do ->  checkEnabledAfterURLChange = forTrusted ->    checkIfEnabledForUrl() if windowIsFocused() -handleEscapeForFindMode = -> -  document.body.classList.remove("vimiumFindMode") -  # removing the class does not re-color existing selections. we recreate the current selection so it reverts -  # back to the default color. -  selection = window.getSelection() -  unless selection.isCollapsed -    range = window.getSelection().getRangeAt(0) -    window.getSelection().removeAllRanges() -    window.getSelection().addRange(range) -  focusFoundLink() || selectFoundInputElement() - -# <esc> sends us into insert mode if possible, but <cr> does not. -# <esc> corresponds approximately to 'nevermind, I have found it already' while <cr> means 'I want to save -# this query and do more searches with it' -handleEnterForFindMode = -> -  focusFoundLink() -  document.body.classList.add("vimiumFindMode") -  FindMode.saveQuery() - -focusFoundLink = -> -  if (FindMode.query.hasResults) -    link = getLinkFromSelection() -    link.focus() if link - -selectFoundInputElement = -> -  # 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. -  findModeAnchorNode = document.getSelection().anchorNode -  if (FindMode.query.hasResults && document.activeElement && -      DomUtils.isSelectable(document.activeElement) && -      DomUtils.isDOMDescendant(findModeAnchorNode, document.activeElement)) -    DomUtils.simulateSelect(document.activeElement) - -findAndFocus = (backwards) -> -  Marks.setPreviousPosition() -  FindMode.query.hasResults = FindMode.execute null, {backwards} - -  if FindMode.query.hasResults -    focusFoundLink() -    new PostFindMode() -  else -    HUD.showForDuration("No matches for '#{FindMode.query.rawQuery}'", 1000) - -performFind = (count) -> findAndFocus false for [0...count] by 1 -performBackwardsFind = (count) -> findAndFocus true for [0...count] by 1 - -getLinkFromSelection = -> -  node = window.getSelection().anchorNode -  while (node && node != document.body) -    return node if (node.nodeName.toLowerCase() == "a") -    node = node.parentNode -  null - -# used by the findAndFollow* functions. -followLink = (linkElement) -> -  if (linkElement.nodeName.toLowerCase() == "link") -    window.location.href = linkElement.href -  else -    # if we can click on it, don't simply set location.href: some next/prev links are meant to trigger AJAX -    # calls, like the 'more' button on GitHub's newsfeed. -    linkElement.scrollIntoView() -    DomUtils.simulateClick(linkElement) - -# -# Find and follow a link which matches any one of a list of strings. If there are multiple such links, they -# are prioritized for shortness, by their position in :linkStrings, how far down the page they are located, -# and finally by whether the match is exact. Practically speaking, this means we favor 'next page' over 'the -# next big thing', and 'more' over 'nextcompany', even if 'next' occurs before 'more' in :linkStrings. -# -findAndFollowLink = (linkStrings) -> -  linksXPath = DomUtils.makeXPath(["a", "*[@onclick or @role='link' or contains(@class, 'button')]"]) -  links = DomUtils.evaluateXPath(linksXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) -  candidateLinks = [] - -  # at the end of this loop, candidateLinks will contain all visible links that match our patterns -  # links lower in the page are more likely to be the ones we want, so we loop through the snapshot backwards -  for i in [(links.snapshotLength - 1)..0] by -1 -    link = links.snapshotItem(i) - -    # ensure link is visible (we don't mind if it is scrolled offscreen) -    boundingClientRect = link.getBoundingClientRect() -    if (boundingClientRect.width == 0 || boundingClientRect.height == 0) -      continue -    computedStyle = window.getComputedStyle(link, null) -    if (computedStyle.getPropertyValue("visibility") != "visible" || -        computedStyle.getPropertyValue("display") == "none") -      continue - -    linkMatches = false -    for linkString in linkStrings -      if link.innerText.toLowerCase().indexOf(linkString) != -1 || -          0 <= link.value?.indexOf? linkString -        linkMatches = true -        break -    continue unless linkMatches - -    candidateLinks.push(link) - -  return if (candidateLinks.length == 0) - -  for link in candidateLinks -    link.wordCount = link.innerText.trim().split(/\s+/).length - -  # We can use this trick to ensure that Array.sort is stable. We need this property to retain the reverse -  # in-page order of the links. - -  candidateLinks.forEach((a,i) -> a.originalIndex = i) - -  # favor shorter links, and ignore those that are more than one word longer than the shortest link -  candidateLinks = -    candidateLinks -      .sort((a, b) -> -        if (a.wordCount == b.wordCount) then a.originalIndex - b.originalIndex else a.wordCount - b.wordCount -      ) -      .filter((a) -> a.wordCount <= candidateLinks[0].wordCount + 1) - -  for linkString in linkStrings -    exactWordRegex = -      if /\b/.test(linkString[0]) or /\b/.test(linkString[linkString.length - 1]) -        new RegExp "\\b" + linkString + "\\b", "i" -      else -        new RegExp linkString, "i" -    for candidateLink in candidateLinks -      if exactWordRegex.test(candidateLink.innerText) || -          (candidateLink.value && exactWordRegex.test(candidateLink.value)) -        followLink(candidateLink) -        return true -  false - -findAndFollowRel = (value) -> -  relTags = ["link", "a", "area"] -  for tag in relTags -    elements = document.getElementsByTagName(tag) -    for element in elements -      if (element.hasAttribute("rel") && element.rel.toLowerCase() == value) -        followLink(element) -        return true - -root.goPrevious = -> -  previousPatterns = Settings.get("previousPatterns") || "" -  previousStrings = previousPatterns.split(",").filter( (s) -> s.trim().length ) -  findAndFollowRel("prev") || findAndFollowLink(previousStrings) - -root.goNext = -> -  nextPatterns = Settings.get("nextPatterns") || "" -  nextStrings = nextPatterns.split(",").filter( (s) -> s.trim().length ) -  findAndFollowRel("next") || findAndFollowLink(nextStrings) - -# Enters find mode.  Returns the new find-mode instance. -enterFindMode = -> -  Marks.setPreviousPosition() -  new FindMode() - -root.showHelp = (sourceFrameId) -> -  HelpDialog.toggle {sourceFrameId, showAllCommandDetails: false} -  # If we are in the help dialog iframe, then HelpDialog is already defined with the necessary functions.  root.HelpDialog ?=    helpUI: null @@ -704,9 +338,8 @@ root.frameId = frameId  root.Frame = Frame  root.windowIsFocused = windowIsFocused  root.bgLog = bgLog -# These are exported for find mode and link-hints mode. -extend root, {handleEscapeForFindMode, handleEnterForFindMode, performFind, performBackwardsFind, -  enterFindMode, focusThisFrame} +# These are exported for normal mode and link-hints mode. +extend root, {focusThisFrame}  # These are exported only for the tests.  extend root, {installModes}  extend window, root unless exports? | 
