diff options
Diffstat (limited to 'content_scripts')
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 1036 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.js | 1183 | 
2 files changed, 1036 insertions, 1183 deletions
| diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee new file mode 100644 index 00000000..cd1da3e8 --- /dev/null +++ b/content_scripts/vimium_frontend.coffee @@ -0,0 +1,1036 @@ +# +# This content script takes input from its webpage and executes commands locally on behalf of the background +# page. It must be run prior to domReady so that we perform some operations very early. We tell the +# background page that we're in domReady and ready to accept normal commands by connectiong to a port named +# "domReady". +# +getCurrentUrlHandlers = []; # function(url) + +insertModeLock = null +findMode = false +findModeQuery = { rawQuery: "" } +findModeQueryHasResults = false +findModeAnchorNode = null +isShowingHelpDialog = false +handlerStack = [] +keyPort = null +# Users can disable Vimium on URL patterns via the settings page. +isEnabledForUrl = true +# The user's operating system. +currentCompletionKeys = null +validFirstKeys = null +linkHintCss = null +activatedElement = null + +# 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"] +  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) +)() + +# +# settings provides a browser-global localStorage-backed dict. get() and set() are synchronous, but load() +# must be called beforehand to ensure get() will return up-to-date values. +# +settings = +  port: null +  values: {} +  loadedValues: 0 +  valuesToLoad: ["scrollStepSize", "linkHintCharacters", "filterLinkHints", "hideHud", "previousPatterns", +      "nextPatterns", "findModeRawQuery"] +  isLoaded: false +  eventListeners: {} + +  init: -> +    @port = chrome.extension.connect({ name: "settings" }) +    @port.onMessage.addListener(@receiveMessage) + +  get: (key) -> @values[key] + +  set: (key, value) -> +    @init() unless @port + +    @values[key] = value +    @port.postMessage({ operation: "set", key: key, value: value }) + +  load: -> +    @init() unless @port + +    for i of @valuesToLoad +      @port.postMessage({ operation: "get", key: @valuesToLoad[i] }) + +  receiveMessage: (args) -> +    # not using 'this' due to issues with binding on callback +    settings.values[args.key] = args.value +    # since load() can be called more than once, loadedValues can be greater than valuesToLoad, but we test +    # for equality so initializeOnReady only runs once +    if (++settings.loadedValues == settings.valuesToLoad.length) +      settings.isLoaded = true +      listener = null +      while (listener = settings.eventListeners["load"].pop()) +        listener() + +  addEventListener: (eventName, callback) -> +    if (!(eventName of @eventListeners)) +      @eventListeners[eventName] = [] +    @eventListeners[eventName].push(callback) + +# +# Give this frame a unique id. +# +frameId = Math.floor(Math.random()*999999999) + +hasModifiersRegex = /^<([amc]-)+.>/ + +# +# Complete initialization work that sould be done prior to DOMReady. +# +initializePreDomReady = -> +  settings.addEventListener("load", LinkHints.init.bind(LinkHints)) +  settings.load() + +  checkIfEnabledForUrl() + +  chrome.extension.sendRequest { handler: "getLinkHintCss" }, (response) -> +    linkHintCss = response.linkHintCss + +  refreshCompletionKeys() + +  # Send the key to the key handler in the background page. +  keyPort = chrome.extension.connect({ name: "keyDown" }) + +  chrome.extension.onRequest.addListener (request, sender, sendResponse) -> +    if (request.name == "hideUpgradeNotification") +      HUD.hideUpgradeNotification() +    else if (request.name == "showUpgradeNotification" && isEnabledForUrl) +      HUD.showUpgradeNotification(request.version) +    else if (request.name == "showHelpDialog") +      if (isShowingHelpDialog) +        hideHelpDialog() +      else +        showHelpDialog(request.dialogHtml, request.frameId) +    else if (request.name == "focusFrame") +      if (frameId == request.frameId) +        focusThisFrame(request.highlight) +    else if (request.name == "refreshCompletionKeys") +      refreshCompletionKeys(request) + +    # Free up the resources used by this open connection. +    sendResponse({}) + +  chrome.extension.onConnect.addListener (port, name) -> +    if (port.name == "executePageCommand") +      port.onMessage.addListener (args) -> +        if (frameId == args.frameId) +          if (args.passCountToFunction) +            Utils.invokeCommandString(args.command, [args.count]) +          else +            Utils.invokeCommandString(args.command) for i in [0...args.count] + +        refreshCompletionKeys(args) +    else if (port.name == "getScrollPosition") +      port.onMessage.addListener (args) -> +        scrollPort = chrome.extension.connect({ name: "returnScrollPosition" }) +        scrollPort.postMessage +          scrollX: window.scrollX, +          scrollY: window.scrollY, +          currentTab: args.currentTab +    else if (port.name == "setScrollPosition") +      port.onMessage.addListener (args) -> +        if (args.scrollX > 0 || args.scrollY > 0) +          DomUtils.documentReady(-> window.scrollBy(args.scrollX, args.scrollY)) +    else if (port.name == "returnCurrentTabUrl") +      port.onMessage.addListener (args) -> +        getCurrentUrlHandlers.pop()(args.url) if (getCurrentUrlHandlers.length > 0) +    else if (port.name == "refreshCompletionKeys") +      port.onMessage.addListener (args) -> refreshCompletionKeys(args.completionKeys) +    else if (port.name == "getActiveState") +      port.onMessage.addListener (args) -> port.postMessage({ enabled: isEnabledForUrl }) +    else if (port.name == "disableVimium") +      port.onMessage.addListener (args) -> disableVimium() + +# +# This is called once the background page has told us that Vimium should be enabled for the current URL. +# +initializeWhenEnabled = -> +  document.addEventListener("keydown", onKeydown, true) +  document.addEventListener("keypress", onKeypress, true) +  document.addEventListener("keyup", onKeyup, true) +  document.addEventListener("focus", onFocusCapturePhase, true) +  document.addEventListener("blur", onBlurCapturePhase, true) +  document.addEventListener("DOMActivate", onDOMActivate, true) +  enterInsertModeIfElementIsFocused() + +# +# Used to disable Vimium without needing to reload the page. +# This is called if the current page's url is blacklisted using the popup UI. +# +disableVimium = -> +  document.removeEventListener("keydown", onKeydown, true) +  document.removeEventListener("keypress", onKeypress, true) +  document.removeEventListener("keyup", onKeyup, true) +  document.removeEventListener("focus", onFocusCapturePhase, true) +  document.removeEventListener("blur", onBlurCapturePhase, true) +  document.removeEventListener("DOMActivate", onDOMActivate, true) +  isEnabledForUrl = false + +# +# The backend needs to know which frame has focus. +# +window.addEventListener "focus", -> +  # settings may have changed since the frame last had focus +  settings.load() +  chrome.extension.sendRequest({ handler: "frameFocused", frameId: frameId }) + +# +# Initialization tasks that must wait for the document to be ready. +# +initializeOnDomReady = -> +  registerFrameIfSizeAvailable(window.top == window.self) + +  enterInsertModeIfElementIsFocused() if isEnabledForUrl + +  # Tell the background page we're in the dom ready state. +  chrome.extension.connect({ name: "domReady" }) + +# This is a little hacky but sometimes the size wasn't available on domReady? +registerFrameIfSizeAvailable = (is_top) -> +  if (innerWidth != undefined && innerWidth != 0 && innerHeight != undefined && innerHeight != 0) +    chrome.extension.sendRequest( +      handler: "registerFrame" +      frameId: frameId +      area: innerWidth * innerHeight +      is_top: is_top +      total: frames.length + 1) +  else +    setTimeout((-> registerFrameIfSizeAvailable(is_top)), 100) + +# +# Enters insert mode if the currently focused element in the DOM is focusable. +# +enterInsertModeIfElementIsFocused = -> +  if (document.activeElement && isEditable(document.activeElement) && !findMode) +    enterInsertModeWithoutShowingIndicator(document.activeElement) + +onDOMActivate = (event) -> activatedElement = event.target + +# +# activatedElement is different from document.activeElement -- the latter seems to be reserved mostly for +# input elements. This mechanism allows us to decide whether to scroll a div or to scroll the whole document. +# +scrollActivatedElementBy= (direction, amount) -> +  # if this is called before domReady, just use the window scroll function +  if (!document.body) +    if (direction == "x") +      window.scrollBy(amount, 0) +    else +      window.scrollBy(0, amount) +    return + +  # TODO refactor and put this together with the code in getVisibleClientRect +  isRendered = (element) -> +    computedStyle = window.getComputedStyle(element, null) +    return !(computedStyle.getPropertyValue("visibility") != "visible" || +        computedStyle.getPropertyValue("display") == "none") + +  if (!activatedElement || !isRendered(activatedElement)) +    activatedElement = document.body + +  scrollName = if (direction == "x") then "scrollLeft" else "scrollTop" + +  # Chrome does not report scrollHeight accurately for nodes with pseudo-elements of height 0 (bug 110149). +  # Therefore we just try to increase scrollTop blindly -- if it fails we know we have reached the end of the +  # content. +  if (amount != 0) +    element = activatedElement +    loop +      oldScrollValue = element[scrollName] +      element[scrollName] += amount +      lastElement = element +      # we may have an orphaned element. if so, just scroll the body element. +      element = element.parentElement || document.body +      break unless (lastElement[scrollName] == oldScrollValue && lastElement != document.body) + +  # if the activated element has been scrolled completely offscreen, subsequent changes in its scroll +  # position will not provide any more visual feedback to the user. therefore we deactivate it so that +  # subsequent scrolls only move the parent element. +  rect = activatedElement.getBoundingClientRect() +  if (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth) +    activatedElement = lastElement + +# +# Called from the backend in order to change frame focus. +# +window.focusThisFrame = (shouldHighlight) -> +  window.focus() +  if (document.body && shouldHighlight) +    borderWas = document.body.style.border +    document.body.style.border = '5px solid yellow' +    setTimeout((-> document.body.style.border = borderWas), 200) + +extend window, +  scrollToBottom: -> window.scrollTo(window.pageXOffset, document.body.scrollHeight) +  scrollToTop: -> window.scrollTo(window.pageXOffset, 0) +  scrollToLeft: -> window.scrollTo(0, window.pageYOffset) +  scrollToRight: -> window.scrollTo(document.body.scrollWidth, window.pageYOffset) +  scrollUp: -> scrollActivatedElementBy("y", -1 * settings.get("scrollStepSize")) +  scrollDown: -> +    scrollActivatedElementBy("y", parseFloat(settings.get("scrollStepSize"))) +  scrollPageUp: -> scrollActivatedElementBy("y", -1 * window.innerHeight / 2) +  scrollPageDown: -> scrollActivatedElementBy("y", window.innerHeight / 2) +  scrollFullPageUp: -> scrollActivatedElementBy("y", -window.innerHeight) +  scrollFullPageDown: -> scrollActivatedElementBy("y", window.innerHeight) +  scrollLeft: -> scrollActivatedElementBy("x", -1 * settings.get("scrollStepSize")) +  scrollRight: -> scrollActivatedElementBy("x", parseFloat(settings.get("scrollStepSize"))) + +focusInput = (count) -> +  results = DomUtils.evaluateXPath(textInputXPath, XPathResult.ORDERED_NODE_ITERATOR_TYPE) + +  lastInputBox +  i = 0 + +  while (i < count) +    currentInputBox = results.iterateNext() +    break unless currentInputBox +    continue if (DomUtils.getVisibleClientRect(currentInputBox) == null) +    lastInputBox = currentInputBox +    i += 1 + +  lastInputBox.focus() if lastInputBox + +extend window, +  reload: -> window.location.reload() +  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('/') + +  toggleViewSource: -> +    toggleViewSourceCallback = (url) -> +      if (url.substr(0, 12) == "view-source:") +        url = url.substr(12, url.length - 12) +      else +        url = "view-source:" + url +      chrome.extension.sendRequest({ handler: "openUrlInNewTab", url: url, selected: true }) +    getCurrentUrlHandlers.push(toggleViewSourceCallback) +    getCurrentUrlPort = chrome.extension.connect({ name: "getCurrentTabUrl" }) +    getCurrentUrlPort.postMessage({}) + +  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 +    # getCurrentUrlHandlers.push(function (url) { Clipboard.copy(url); }) +    getCurrentUrlHandlers.push((url) -> chrome.extension.sendRequest({ handler: "copyToClipboard", data: url })) + +    # TODO(ilya): Convert to sendRequest. +    getCurrentUrlPort = chrome.extension.connect({ name: "getCurrentTabUrl" }) +    getCurrentUrlPort.postMessage({}) + +    HUD.showForDuration("Yanked URL", 1000) + +# +# Sends everything except i & ESC to the handler in background_page. i & ESC are special because they control +# insert mode which is local state to the page. The key will be are either a single ascii letter or a +# key-modifier pair, e.g. <c-a> for control a. +# +# Note that some keys will only register keydown events and not keystroke events, e.g. ESC. +# +onKeypress = (event) -> +  return unless bubbleEvent('keypress', event) + +  keyChar = "" + +  # Ignore modifier keys by themselves. +  if (event.keyCode > 31) +    keyChar = String.fromCharCode(event.charCode) + +    # Enter insert mode when the user enables the native find interface. +    if (keyChar == "f" && KeyboardUtils.isPrimaryModifierKey(event)) +      enterInsertModeWithoutShowingIndicator() +      return + +    if (keyChar) +      if (findMode) +        handleKeyCharForFindMode(keyChar) +        suppressEvent(event) +      else if (!isInsertMode() && !findMode) +        if (currentCompletionKeys.indexOf(keyChar) != -1) +          suppressEvent(event) + +        keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) + +# +# Called whenever we receive a key event.  Each individual handler has the option to stop the event's +# propagation by returning a falsy value. +# +bubbleEvent = (type, event) -> +  for i in [(handlerStack.length - 1)..0] +    # We need to check for existence of handler because the last function call may have caused the release of +    # more than one handler. +    if (handlerStack[i] && handlerStack[i][type] && !handlerStack[i][type](event)) +      suppressEvent(event) +      return false +  true + +suppressEvent = (event) -> +  event.preventDefault() +  event.stopPropagation() + +onKeydown = (event) -> +  return unless bubbleEvent('keydown', event) + +  keyChar = "" + +  # handle special keys, and normal input keys with modifiers being pressed. don't handle shiftKey alone (to +  # avoid / being interpreted as ? +  if (((event.metaKey || event.ctrlKey || event.altKey) && event.keyCode > 31) || +      event.keyIdentifier.slice(0, 2) != "U+") +    keyChar = KeyboardUtils.getKeyChar(event) +    # Again, ignore just modifiers. Maybe this should replace the keyCode>31 condition. +    if (keyChar != "") +      modifiers = [] + +      if (event.shiftKey) +        keyChar = keyChar.toUpperCase() +      if (event.metaKey) +        modifiers.push("m") +      if (event.ctrlKey) +        modifiers.push("c") +      if (event.altKey) +        modifiers.push("a") + +      for i of modifiers +        keyChar = modifiers[i] + "-" + keyChar + +      if (modifiers.length > 0 || keyChar.length > 1) +        keyChar = "<" + keyChar + ">" + +  if (isInsertMode() && KeyboardUtils.isEscape(event)) +    # Note that we can't programmatically blur out of Flash embeds from Javascript. +    if (!isEmbed(event.srcElement)) +      # Remove focus so the user can't just get himself back into insert mode by typing in the same input +      # box. +      if (isEditable(event.srcElement)) +        event.srcElement.blur() +      exitInsertMode() +      suppressEvent(event) + +  else if (findMode) +    if (KeyboardUtils.isEscape(event)) +      handleEscapeForFindMode() +      suppressEvent(event) + +    else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) +      handleDeleteForFindMode() +      suppressEvent(event) + +    else if (event.keyCode == keyCodes.enter) +      handleEnterForFindMode() +      suppressEvent(event) + +    else if (!modifiers) +      event.stopPropagation() + +  else if (isShowingHelpDialog && KeyboardUtils.isEscape(event)) +    hideHelpDialog() + +  else if (!isInsertMode() && !findMode) +    if (keyChar) +      if (currentCompletionKeys.indexOf(keyChar) != -1) +        suppressEvent(event) + +      keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) + +    else if (KeyboardUtils.isEscape(event)) +      keyPort.postMessage({ keyChar:"<ESC>", frameId:frameId }) + +  # Added to prevent propagating this event to other listeners if it's one that'll trigger a Vimium command. +  # The goal is to avoid the scenario where Google Instant Search uses every keydown event to dump us +  # back into the search box. As a side effect, this should also prevent overriding by other sites. +  # +  # Subject to internationalization issues since we're using keyIdentifier instead of charCode (in keypress). +  # +  # TOOD(ilya): Revisit @ Not sure it's the absolute best approach. +  if (keyChar == "" && !isInsertMode() && +     (currentCompletionKeys.indexOf(KeyboardUtils.getKeyChar(event)) != -1 || +      isValidFirstKey(KeyboardUtils.getKeyChar(event)))) +    event.stopPropagation() + +onKeyup = () -> return unless bubbleEvent('keyup', event) + +checkIfEnabledForUrl = -> +  url = window.location.toString() + +  chrome.extension.sendRequest { handler: "isEnabledForUrl", url: url }, (response) -> +    isEnabledForUrl = response.isEnabledForUrl +    if (isEnabledForUrl) +      initializeWhenEnabled() +    else if (HUD.isReady()) +      # Quickly hide any HUD we might already be showing, e.g. if we entered insert mode on page load. +      HUD.hide() + +refreshCompletionKeys = (response) -> +  if (response) +    currentCompletionKeys = response.completionKeys + +    if (response.validFirstKeys) +      validFirstKeys = response.validFirstKeys +  else +    chrome.extension.sendRequest({ handler: "getCompletionKeys" }, refreshCompletionKeys) + +isValidFirstKey = (keyChar) -> +  validFirstKeys[keyChar] || /[1-9]/.test(keyChar) + +onFocusCapturePhase = (event) -> +  if (isFocusable(event.target) && !findMode) +    enterInsertModeWithoutShowingIndicator(event.target) + +onBlurCapturePhase = (event) -> +  if (isFocusable(event.target)) +    exitInsertMode(event.target) + +# +# Returns true if the element is focusable. This includes embeds like Flash, which steal the keybaord focus. +# +isFocusable = (element) -> isEditable(element) || isEmbed(element) + +# +# Embedded elements like Flash and quicktime players can obtain focus but cannot be programmatically +# unfocused. +# +isEmbed = (element) -> ["embed", "object"].indexOf(element.nodeName.toLowerCase()) > 0 + +# +# Input or text elements are considered focusable and able to receieve their own keyboard events, +# and will enter enter mode if focused. Also note that the "contentEditable" attribute can be set on +# any element which makes it a rich text editor, like the notes on jjot.com. +# +isEditable = (target) -> +  return true if target.isContentEditable +  nodeName = target.nodeName.toLowerCase() +  # use a blacklist instead of a whitelist because new form controls are still being implemented for html5 +  noFocus = ["radio", "checkbox"] +  if (nodeName == "input" && noFocus.indexOf(target.type) == -1) +    return true +  focusableElements = ["textarea", "select"] +  focusableElements.indexOf(nodeName) >= 0 + +# +# Enters insert mode and show an "Insert mode" message. Showing the UI is only useful when entering insert +# mode manually by pressing "i". In most cases we do not show any UI (enterInsertModeWithoutShowingIndicator) +# +window.enterInsertMode = (target) -> +  enterInsertModeWithoutShowingIndicator(target) +  HUD.show("Insert mode") + +# +# We cannot count on 'focus' and 'blur' events to happen sequentially. For example, if blurring element A +# causes element B to come into focus, we may get "B focus" before "A blur". Thus we only leave insert mode +# when the last editable element that came into focus -- which insertModeLock points to -- has been blurred. +# If insert mode is entered manually (via pressing 'i'), then we set insertModeLock to 'undefined', and only +# leave insert mode when the user presses <ESC>. +# +enterInsertModeWithoutShowingIndicator = (target) -> insertModeLock = target + +exitInsertMode = (target) -> +  if (target == undefined || insertModeLock == target) +    insertModeLock = null +    HUD.hide() + +isInsertMode = -> insertModeLock != null + +# should be called whenever rawQuery is modified. +updateFindModeQuery = -> +  # the query can be treated differently (e.g. as a plain string versus regex depending on the presence of +  # escape sequences. '\' is the escape character and needs to be escaped itself to be used as a normal +  # character. here we grep for the relevant escape sequences. +  findModeQuery.isRegex = false +  hasNoIgnoreCaseFlag = false +  findModeQuery.parsedQuery = findModeQuery.rawQuery.replace /\\./g, (match) -> +    switch (match) +      when "\\r" +        findModeQuery.isRegex = true +        return "" +      when "\\I" +        hasNoIgnoreCaseFlag = true +        return "" +      when "\\\\" +        return "\\" +      else +        return match + +  # default to 'smartcase' mode, unless noIgnoreCase is explicitly specified +  findModeQuery.ignoreCase = !hasNoIgnoreCaseFlag && !/[A-Z]/.test(findModeQuery.parsedQuery) + +  # if we are dealing with a regex, grep for all matches in the text, and then call window.find() on them +  # sequentially so the browser handles the scrolling / text selection. +  if findModeQuery.isRegex +    try +      pattern = new RegExp(findModeQuery.parsedQuery, "g" + (findModeQuery.ignoreCase ? "i" : "")) +    catch error +      # if we catch a SyntaxError, assume the user is not done typing yet and return quietly +      return +    # innerText will not return the text of hidden elements, and strip out tags while preserving newlines +    text = document.body.innerText +    findModeQuery.regexMatches = text.match(pattern) +    findModeQuery.activeRegexIndex = 0 + +handleKeyCharForFindMode = (keyChar) -> +  findModeQuery.rawQuery += keyChar +  updateFindModeQuery() +  performFindInPlace() +  showFindModeHUDForQuery() + +handleEscapeForFindMode = -> +  exitFindMode() +  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() + +handleDeleteForFindMode = -> +  if (findModeQuery.rawQuery.length == 0) +    exitFindMode() +    performFindInPlace() +  else +    findModeQuery.rawQuery = findModeQuery.rawQuery.substring(0, findModeQuery.rawQuery.length - 1) +    updateFindModeQuery() +    performFindInPlace() +    showFindModeHUDForQuery() + +# <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 = -> +  exitFindMode() +  focusFoundLink() +  document.body.classList.add("vimiumFindMode") +  settings.set("findModeRawQuery", findModeQuery.rawQuery) + +performFindInPlace = -> +  cachedScrollX = window.scrollX +  cachedScrollY = window.scrollY + +  query = if findModeQuery.isRegex then getNextQueryFromRegexMatches(0) else findModeQuery.parsedQuery + +  # Search backwards first to "free up" the current word as eligible for the real forward search. This allows +  # us to search in place without jumping around between matches as the query grows. +  executeFind(query, { backwards: true, caseSensitive: !findModeQuery.ignoreCase }) + +  # We need to restore the scroll position because we might've lost the right position by searching +  # backwards. +  window.scrollTo(cachedScrollX, cachedScrollY) + +  findModeQueryHasResults = executeFind(query, { caseSensitive: !findModeQuery.ignoreCase }) + +# :options is an optional dict. valid parameters are 'caseSensitive' and 'backwards'. +executeFind = (query, options) -> +  options = options || {} + +  # rather hacky, but this is our way of signalling to the insertMode listener not to react to the focus +  # changes that find() induces. +  oldFindMode = findMode +  findMode = true + +  document.body.classList.add("vimiumFindMode") + +  # prevent find from matching its own search query in the HUD +  HUD.hide(true) +  # ignore the selectionchange event generated by find() +  document.removeEventListener("selectionchange",restoreDefaultSelectionHighlight, true) +  result = window.find(query, options.caseSensitive, options.backwards, true, false, true, false) +  setTimeout( +    -> document.addEventListener("selectionchange", restoreDefaultSelectionHighlight, true) +    0) + +  findMode = oldFindMode +  # we need to save the anchor node here because <esc> seems to nullify it, regardless of whether we do +  # preventDefault() +  findModeAnchorNode = document.getSelection().anchorNode +  result + +restoreDefaultSelectionHighlight = -> document.body.classList.remove("vimiumFindMode") + +focusFoundLink = -> +  if (findModeQueryHasResults) +    link = getLinkFromSelection() +    link.focus() if link + +isDOMDescendant = (parent, child) -> +  node = child +  while (node != null) +    return true if (node == parent) +    node = node.parentNode +  false + +selectFoundInputElement = -> +  # if the found text is in an input element, getSelection().anchorNode will be null, so we use activeElement +  # 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 && DomUtils.isSelectable(document.activeElement) && +      isDOMDescendant(findModeAnchorNode, document.activeElement)) +    DomUtils.simulateSelect(document.activeElement) +    # the element has already received focus via find(), so invoke insert mode manually +    enterInsertModeWithoutShowingIndicator(document.activeElement) + +getNextQueryFromRegexMatches = (stepSize) -> +  # find()ing an empty query always returns false +  return "" unless findModeQuery.regexMatches + +  totalMatches = findModeQuery.regexMatches.length +  findModeQuery.activeRegexIndex += stepSize + totalMatches +  findModeQuery.activeRegexIndex %= totalMatches + +  findModeQuery.regexMatches[findModeQuery.activeRegexIndex] + +findAndFocus = (backwards) -> +  # check if the query has been changed by a script in another frame +  mostRecentQuery = settings.get("findModeRawQuery") || "" +  if (mostRecentQuery != findModeQuery.rawQuery) +    findModeQuery.rawQuery = mostRecentQuery +    updateFindModeQuery() + +  query = +    if findModeQuery.isRegex +      getNextQueryFromRegexMatches(if backwards then -1 else 1) +    else +      findModeQuery.parsedQuery + +  findModeQueryHasResults = +    executeFind(query, { backwards: backwards, caseSensitive: !findModeQuery.ignoreCase }) + +  if (!findModeQueryHasResults) +    HUD.showForDuration("No matches for '" + findModeQuery.rawQuery + "'", 1000) +    return + +  # if we have found an input element via 'n', pressing <esc> immediately afterwards sends us into insert +  # mode +  elementCanTakeInput = DomUtils.isSelectable(document.activeElement) && +    isDOMDescendant(findModeAnchorNode, document.activeElement) +  if (elementCanTakeInput) +    handlerStack.push({ +      keydown: (event) -> +        handlerStack.pop() +        if (KeyboardUtils.isEscape(event)) +          DomUtils.simulateSelect(document.activeElement) +          enterInsertModeWithoutShowingIndicator(document.activeElement) +          return false # we have "consumed" this event, so do not propagate +        return true +    }) + +  focusFoundLink() + +window.performFind = -> findAndFocus() + +window.performBackwardsFind = -> findAndFocus(true) + +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() +    linkElement.focus() +    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] +    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) +        linkMatches = true +        break +    continue unless linkMatches + +    candidateLinks.push(link) + +  return if (candidateLinks.length == 0) + +  wordCount = (link) -> 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) -> +        wcA = wordCount(a) +        wcB = wordCount(b) +        if (wcA == wcB) then a.originalIndex - b.originalIndex else wcA - wcB +      ) +      .filter((a) -> wordCount(a) <= wordCount(candidateLinks[0]) + 1) + +  # try to get exact word matches first +  for linkString in linkStrings +    for candidateLink in candidateLinks +      exactWordRegex = new RegExp("\\b" + linkString + "\\b", "i") +      if (exactWordRegex.test(candidateLink.innerText)) +        followLink(candidateLink) +        return true + +  for linkString in linkStrings +    for candidateLink in candidateLinks +      if (candidateLink.innerText.toLowerCase().indexOf(linkString) != -1) +        followLink(candidateLink) +        return true + +  false + +findAndFollowRel = (value) -> +  relTags = ["link", "a", "area"] +  for tag in relTags +    elements = document.getElementsByTagName(relTag) +    for element in elements +      if (element.hasAttribute("rel") && element.rel == value) +        followLink(element) +        return true + +window.goPrevious = -> +  previousPatterns = settings.get("previousPatterns") || "" +  previousStrings = previousPatterns.split(",") +  findAndFollowRel("prev") || findAndFollowLink(previousStrings) + +window.goNext = -> +  nextPatterns = settings.get("nextPatterns") || "" +  nextStrings = nextPatterns.split(",") +  findAndFollowRel("next") || findAndFollowLink(nextStrings) + +showFindModeHUDForQuery = -> +  if (findModeQueryHasResults || findModeQuery.parsedQuery.length == 0) +    HUD.show("/" + findModeQuery.rawQuery) +  else +    HUD.show("/" + findModeQuery.rawQuery + " (No Matches)") + +window.enterFindMode = -> +  findModeQuery = { rawQuery: "" } +  findMode = true +  HUD.show("/") + +exitFindMode = -> +  findMode = false +  HUD.hide() + +window.showHelpDialog = (html, fid) -> +  return if (isShowingHelpDialog || !document.body || fid != frameId) +  isShowingHelpDialog = true +  container = document.createElement("div") +  container.id = "vimiumHelpDialogContainer" +  container.className = "vimiumReset" + +  document.body.appendChild(container) + +  container.innerHTML = html +  container.getElementsByClassName("closeButton")[0].addEventListener("click", hideHelpDialog, false) +  container.getElementsByClassName("optionsPage")[0].addEventListener("click", +    -> chrome.extension.sendRequest({ handler: "openOptionsPageInNewTab" }) +    false) + +  # This is necessary because innerHTML does not evaluate javascript embedded in <script> tags. +  scripts = Array.prototype.slice.call(container.getElementsByTagName("script")) +  scripts.forEach((script) -> eval(script.text)) + +hideHelpDialog = (clickEvent) -> +  isShowingHelpDialog = false +  helpDialog = document.getElementById("vimiumHelpDialogContainer") +  if (helpDialog) +    helpDialog.parentNode.removeChild(helpDialog) +  if (clickEvent) +    clickEvent.preventDefault() + +# +# A heads-up-display (HUD) for showing Vimium page operations. +# Note: you cannot interact with the HUD until document.body is available. +# +HUD = +  _tweenId: -1 +  _displayElement: null +  _upgradeNotificationElement: null + +  # This HUD is styled to precisely mimick the chrome HUD on Mac. Use the "has_popup_and_link_hud.html" +  # test harness to tweak these styles to match Chrome's. One limitation of our HUD display is that +  # it doesn't sit on top of horizontal scrollbars like Chrome's HUD does. + +  showForDuration: (text, duration) -> +    HUD.show(text) +    HUD._showForDurationTimerId = setTimeout((-> HUD.hide()), duration) + +  show: (text) -> +    return unless HUD.enabled() +    clearTimeout(HUD._showForDurationTimerId) +    HUD.displayElement().innerHTML = text +    clearInterval(HUD._tweenId) +    HUD._tweenId = Tween.fade(HUD.displayElement(), 1.0, 150) +    HUD.displayElement().style.display = "" + +  showUpgradeNotification: (version) -> +    HUD.upgradeNotificationElement().innerHTML = "Vimium has been updated to " + +      "<a class='vimiumReset' href='https://chrome.google.com/extensions/detail/dbepggeogbaibhgnhhndojpepiihcmeb'>" + +      version + "</a>.<a class='vimiumReset close-button' href='#'>x</a>" +    links = HUD.upgradeNotificationElement().getElementsByTagName("a") +    links[0].addEventListener("click", HUD.onUpdateLinkClicked, false) +    links[1].addEventListener "click", (event) -> +      event.preventDefault() +      HUD.onUpdateLinkClicked() +    Tween.fade(HUD.upgradeNotificationElement(), 1.0, 150) + +  onUpdateLinkClicked: (event) -> +    HUD.hideUpgradeNotification() +    chrome.extension.sendRequest({ handler: "upgradeNotificationClosed" }) + +  hideUpgradeNotification: (clickEvent) -> +    Tween.fade(HUD.upgradeNotificationElement(), 0, 150, +      -> HUD.upgradeNotificationElement().style.display = "none") + +  # +  # Retrieves the HUD HTML element. +  # +  displayElement: -> +    if (!HUD._displayElement) +      HUD._displayElement = HUD.createHudElement() +      # Keep this far enough to the right so that it doesn't collide with the "popups blocked" chrome HUD. +      HUD._displayElement.style.right = "150px" +    HUD._displayElement + +  upgradeNotificationElement: -> +    if (!HUD._upgradeNotificationElement) +      HUD._upgradeNotificationElement = HUD.createHudElement() +      # Position this just to the left of our normal HUD. +      HUD._upgradeNotificationElement.style.right = "315px" +    HUD._upgradeNotificationElement + +  createHudElement: -> +    element = document.createElement("div") +    element.className = "vimiumReset vimiumHUD" +    document.body.appendChild(element) +    element + +  hide: (immediate) -> +    clearInterval(HUD._tweenId) +    if (immediate) +      HUD.displayElement().style.display = "none" +    else +      HUD._tweenId = Tween.fade(HUD.displayElement(), 0, 150, +        -> HUD.displayElement().style.display = "none") + +  isReady: -> document.body != null + +  # A preference which can be toggled in the Options page. */ +  enabled: -> !settings.get("hideHud") + +Tween = +  # +  # Fades an element's alpha. Returns a timer ID which can be used to stop the tween via clearInterval. +  # +  fade: (element, toAlpha, duration, onComplete) -> +    state = {} +    state.duration = duration +    state.startTime = (new Date()).getTime() +    state.from = parseInt(element.style.opacity) || 0 +    state.to = toAlpha +    state.onUpdate = (value) -> +      element.style.opacity = value +      if (value == state.to && onComplete) +        onComplete() +    state.timerId = setInterval((-> Tween.performTweenStep(state)), 50) +    state.timerId + +  performTweenStep: (state) -> +    elapsed = (new Date()).getTime() - state.startTime +    if (elapsed >= state.duration) +      clearInterval(state.timerId) +      state.onUpdate(state.to) +    else +      value = (elapsed / state.duration)  * (state.to - state.from) + state.from +      state.onUpdate(value) + +# +# Adds the given CSS to the page. +# +addCssToPage = (css, id) -> +  head = document.getElementsByTagName("head")[0] +  if (!head) +    head = document.createElement("head") +    document.documentElement.appendChild(head) +  style = document.createElement("style") +  style.id = id +  style.type = "text/css" +  style.appendChild(document.createTextNode(css)) +  head.appendChild(style) + +initializePreDomReady() +window.addEventListener("DOMContentLoaded", initializeOnDomReady) + +window.onbeforeunload = -> +  chrome.extension.sendRequest( +    handler: "updateScrollPosition" +    scrollX: window.scrollX +    scrollY: window.scrollY) + +# TODO(philc): Export a more tighter, more coherent interface. +root = exports ? window +root.window = window +root.settings = settings +root.linkHintCss = linkHintCss +root.addCssToPage = addCssToPage +root.HUD = HUD +root.handlerStack = handlerStack diff --git a/content_scripts/vimium_frontend.js b/content_scripts/vimium_frontend.js deleted file mode 100644 index d205ef1c..00000000 --- a/content_scripts/vimium_frontend.js +++ /dev/null @@ -1,1183 +0,0 @@ -/* - * This content script takes input from its webpage and executes commands locally on behalf of the background - * page. It must be run prior to domReady so that we perform some operations very early. We tell the - * background page that we're in domReady and ready to accept normal commands by connectiong to a port named - * "domReady". - */ -var getCurrentUrlHandlers = []; // function(url) - -var insertModeLock = null; -var findMode = false; -var findModeQuery = { rawQuery: "" }; -var findModeQueryHasResults = false; -var findModeAnchorNode = null; -var isShowingHelpDialog = false; -var handlerStack = []; -var keyPort; -// Users can disable Vimium on URL patterns via the settings page. -var isEnabledForUrl = true; -// The user's operating system. -var currentCompletionKeys; -var validFirstKeys; -var linkHintCss; -var activatedElement; - -// 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. -var textInputXPath = (function() { -  var textInputTypes = ["text", "search", "email", "url", "number", "password"]; -  var inputElements = ["input[" + -    "(" + textInputTypes.map(function(type) {return '@type="' + type + '"'}).join(" or ") + "or not(@type))" + -    " and not(@disabled or @readonly)]", -    "textarea", "*[@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"]; -  return DomUtils.makeXPath(inputElements); -})(); - -/** - * settings provides a browser-global localStorage-backed dict. get() and set() are synchronous, but load() - * must be called beforehand to ensure get() will return up-to-date values. - */ -var settings = { -  port: null, -  values: {}, -  loadedValues: 0, -  valuesToLoad: ["scrollStepSize", "linkHintCharacters", "filterLinkHints", "hideHud", "previousPatterns", -      "nextPatterns", "findModeRawQuery"], -  isLoaded: false, -  eventListeners: {}, - -  init: function () { -    this.port = chrome.extension.connect({ name: "settings" }); -    this.port.onMessage.addListener(this.receiveMessage); -  }, - -  get: function (key) { return this.values[key]; }, - -  set: function (key, value) { -    if (!this.port) -      this.init(); - -    this.values[key] = value; -    this.port.postMessage({ operation: "set", key: key, value: value }); -  }, - -  load: function() { -    if (!this.port) -      this.init(); - -    for (var i in this.valuesToLoad) { -      this.port.postMessage({ operation: "get", key: this.valuesToLoad[i] }); -    } -  }, - -  receiveMessage: function (args) { -    // not using 'this' due to issues with binding on callback -    settings.values[args.key] = args.value; -    // since load() can be called more than once, loadedValues can be greater than valuesToLoad, but we test -    // for equality so initializeOnReady only runs once -    if (++settings.loadedValues == settings.valuesToLoad.length) { -      settings.isLoaded = true; -      var listener; -      while (listener = settings.eventListeners["load"].pop()) -        listener(); -    } -  }, - -  addEventListener: function(eventName, callback) { -    if (!(eventName in this.eventListeners)) -      this.eventListeners[eventName] = []; -    this.eventListeners[eventName].push(callback); -  }, - -}; - -/* - * Give this frame a unique id. - */ -frameId = Math.floor(Math.random()*999999999) - -var hasModifiersRegex = /^<([amc]-)+.>/; - -/* - * Complete initialization work that sould be done prior to DOMReady. - */ -function initializePreDomReady() { -  settings.addEventListener("load", LinkHints.init.bind(LinkHints)); -  settings.load(); - -  checkIfEnabledForUrl(); - -  chrome.extension.sendRequest({handler: "getLinkHintCss"}, function (response) { -    linkHintCss = response.linkHintCss; -  }); - -  refreshCompletionKeys(); - -  // Send the key to the key handler in the background page. -  keyPort = chrome.extension.connect({ name: "keyDown" }); - -  chrome.extension.onRequest.addListener(function(request, sender, sendResponse) { -    if (request.name == "hideUpgradeNotification") { -      HUD.hideUpgradeNotification(); -    } else if (request.name == "showUpgradeNotification" && isEnabledForUrl) { -      HUD.showUpgradeNotification(request.version); -    } else if (request.name == "showHelpDialog") { -      if (isShowingHelpDialog) -        hideHelpDialog(); -      else -        showHelpDialog(request.dialogHtml, request.frameId); -    } else if (request.name == "focusFrame") { -      if (frameId == request.frameId) -        focusThisFrame(request.highlight); -    } else if (request.name == "refreshCompletionKeys") { -      refreshCompletionKeys(request); -    } -    sendResponse({}); // Free up the resources used by this open connection. -  }); - -  chrome.extension.onConnect.addListener(function(port, name) { -    if (port.name == "executePageCommand") { -      port.onMessage.addListener(function(args) { -        if (frameId == args.frameId) { -          if (args.passCountToFunction) { -            Utils.invokeCommandString(args.command, [args.count]); -          } else { -            for (var i = 0; i < args.count; i++) { Utils.invokeCommandString(args.command); } -          } -        } - -        refreshCompletionKeys(args); -      }); -    } -    else if (port.name == "getScrollPosition") { -      port.onMessage.addListener(function(args) { -        var scrollPort = chrome.extension.connect({ name: "returnScrollPosition" }); -        scrollPort.postMessage({ -          scrollX: window.scrollX, -          scrollY: window.scrollY, -          currentTab: args.currentTab -        }); -      }); -    } else if (port.name == "setScrollPosition") { -      port.onMessage.addListener(function(args) { -        if (args.scrollX > 0 || args.scrollY > 0) { -          DomUtils.documentReady(function() { window.scrollBy(args.scrollX, args.scrollY); }); -        } -      }); -    } else if (port.name == "returnCurrentTabUrl") { -      port.onMessage.addListener(function(args) { -        if (getCurrentUrlHandlers.length > 0) { getCurrentUrlHandlers.pop()(args.url); } -      }); -    } else if (port.name == "refreshCompletionKeys") { -      port.onMessage.addListener(function (args) { -        refreshCompletionKeys(args.completionKeys); -      }); -    } else if (port.name == "getActiveState") { -      port.onMessage.addListener(function(args) { -        port.postMessage({ enabled: isEnabledForUrl }); -      }); -    } else if (port.name == "disableVimium") { -      port.onMessage.addListener(function(args) { disableVimium(); }); -    } -  }); -} - -/* - * This is called once the background page has told us that Vimium should be enabled for the current URL. - */ -function initializeWhenEnabled() { -  document.addEventListener("keydown", onKeydown, true); -  document.addEventListener("keypress", onKeypress, true); -  document.addEventListener("keyup", onKeyup, true); -  document.addEventListener("focus", onFocusCapturePhase, true); -  document.addEventListener("blur", onBlurCapturePhase, true); -  document.addEventListener("DOMActivate", onDOMActivate, true); -  enterInsertModeIfElementIsFocused(); -} - -/* - * Used to disable Vimium without needing to reload the page. - * This is called if the current page's url is blacklisted using the popup UI. - */ -function disableVimium() { -  document.removeEventListener("keydown", onKeydown, true); -  document.removeEventListener("keypress", onKeypress, true); -  document.removeEventListener("keyup", onKeyup, true); -  document.removeEventListener("focus", onFocusCapturePhase, true); -  document.removeEventListener("blur", onBlurCapturePhase, true); -  document.removeEventListener("DOMActivate", onDOMActivate, true); -  isEnabledForUrl = false; -} - -/* - * The backend needs to know which frame has focus. - */ -window.addEventListener("focus", function(e) { -  // settings may have changed since the frame last had focus -  settings.load(); -  chrome.extension.sendRequest({ handler: "frameFocused", frameId: frameId }); -}); - -/* - * Called from the backend in order to change frame focus. - */ -function focusThisFrame(shouldHighlight) { -  window.focus(); -  if (document.body && shouldHighlight) { -    var borderWas = document.body.style.border; -    document.body.style.border = '5px solid yellow'; -    setTimeout(function(){document.body.style.border = borderWas}, 200); -  } -} - -/* - * Initialization tasks that must wait for the document to be ready. - */ -function initializeOnDomReady() { -  registerFrameIfSizeAvailable(window.top == window.self); - -  if (isEnabledForUrl) -    enterInsertModeIfElementIsFocused(); - -  // Tell the background page we're in the dom ready state. -  chrome.extension.connect({ name: "domReady" }); -}; - -// This is a little hacky but sometimes the size wasn't available on domReady? -function registerFrameIfSizeAvailable (is_top) { -  if (innerWidth != undefined && innerWidth != 0 && innerHeight != undefined && innerHeight != 0) -    chrome.extension.sendRequest({ handler: "registerFrame", frameId: frameId, -        area: innerWidth * innerHeight, is_top: is_top, total: frames.length + 1 }); -  else -    setTimeout(function () { registerFrameIfSizeAvailable(is_top); }, 100); -} - -/* - * Enters insert mode if the currently focused element in the DOM is focusable. - */ -function enterInsertModeIfElementIsFocused() { -  if (document.activeElement && isEditable(document.activeElement) && !findMode) -    enterInsertModeWithoutShowingIndicator(document.activeElement); -} - -function onDOMActivate(event) { -  activatedElement = event.target; -} - -/** - * activatedElement is different from document.activeElement -- the latter seems to be reserved mostly for - * input elements. This mechanism allows us to decide whether to scroll a div or to scroll the whole document. - */ -function scrollActivatedElementBy(direction, amount) { -  // if this is called before domReady, just use the window scroll function -  if (!document.body) { -    if (direction === "x") -      window.scrollBy(amount, 0); -    else // "y" -      window.scrollBy(0, amount); -    return; -  } - -  // TODO refactor and put this together with the code in getVisibleClientRect -  function isRendered(element) { -    var computedStyle = window.getComputedStyle(element, null); -    return !(computedStyle.getPropertyValue('visibility') != 'visible' || -        computedStyle.getPropertyValue('display') == 'none'); -  } - -  if (!activatedElement || !isRendered(activatedElement)) -    activatedElement = document.body; - -  scrollName = direction === "x" ? "scrollLeft" : "scrollTop"; - -  // Chrome does not report scrollHeight accurately for nodes with pseudo-elements of height 0 (bug 110149). -  // Therefore we just try to increase scrollTop blindly -- if it fails we know we have reached the end of the -  // content. -  if (amount !== 0) { -    var element = activatedElement; -    do { -      var oldScrollValue = element[scrollName]; -      element[scrollName] += amount; -      var lastElement = element; -      // we may have an orphaned element. if so, just scroll the body element. -      element = element.parentElement || document.body; -    } while(lastElement[scrollName] == oldScrollValue && lastElement != document.body); -  } - -  // if the activated element has been scrolled completely offscreen, subsequent changes in its scroll -  // position will not provide any more visual feedback to the user. therefore we deactivate it so that -  // subsequent scrolls only move the parent element. -  var rect = activatedElement.getBoundingClientRect(); -  if (rect.bottom < 0 || rect.top > window.innerHeight || -      rect.right < 0 || rect.left > window.innerWidth) -    activatedElement = lastElement; -} - -function scrollToBottom() { window.scrollTo(window.pageXOffset, document.body.scrollHeight); } -function scrollToTop() { window.scrollTo(window.pageXOffset, 0); } -function scrollToLeft() { window.scrollTo(0, window.pageYOffset); } -function scrollToRight() { window.scrollTo(document.body.scrollWidth, window.pageYOffset); } -function scrollUp() { scrollActivatedElementBy("y", -1 * settings.get("scrollStepSize")); } -function scrollDown() { scrollActivatedElementBy("y", parseFloat(settings.get("scrollStepSize"))); } -function scrollPageUp() { scrollActivatedElementBy("y", -1 * window.innerHeight / 2); } -function scrollPageDown() { scrollActivatedElementBy("y", window.innerHeight / 2); } -function scrollFullPageUp() { scrollActivatedElementBy("y", -window.innerHeight); } -function scrollFullPageDown() { scrollActivatedElementBy("y", window.innerHeight); } -function scrollLeft() { scrollActivatedElementBy("x", -1 * settings.get("scrollStepSize")); } -function scrollRight() { scrollActivatedElementBy("x", parseFloat(settings.get("scrollStepSize"))); } - -function focusInput(count) { -  var results = DomUtils.evaluateXPath(textInputXPath, XPathResult.ORDERED_NODE_ITERATOR_TYPE); - -  var lastInputBox; -  var i = 0; - -  while (i < count) { -    var currentInputBox = results.iterateNext(); -    if (!currentInputBox) { break; } - -    if (DomUtils.getVisibleClientRect(currentInputBox) === null) -        continue; - -    lastInputBox = currentInputBox; - -    i += 1; -  } - -  if (lastInputBox) { lastInputBox.focus(); } -} - -function reload() { window.location.reload(); } -function goBack(count) { history.go(-count); } -function goForward(count) { history.go(count); } - -function goUp(count) { -  var url = window.location.href; -  if (url[url.length-1] == '/') -    url = url.substring(0, url.length - 1); - -  var 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('/'); -  } -} - -function toggleViewSource() { -  getCurrentUrlHandlers.push(toggleViewSourceCallback); - -  var getCurrentUrlPort = chrome.extension.connect({ name: "getCurrentTabUrl" }); -  getCurrentUrlPort.postMessage({}); -} - -function 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 -  //getCurrentUrlHandlers.push(function (url) { Clipboard.copy(url); }); -  getCurrentUrlHandlers.push(function (url) { chrome.extension.sendRequest({ handler: "copyToClipboard", data: url }); }); - -  // TODO(ilya): Convert to sendRequest. -  var getCurrentUrlPort = chrome.extension.connect({ name: "getCurrentTabUrl" }); -  getCurrentUrlPort.postMessage({}); - -  HUD.showForDuration("Yanked URL", 1000); -} - -function toggleViewSourceCallback(url) { -  if (url.substr(0, 12) == "view-source:") -  { -    url = url.substr(12, url.length - 12); -  } -  else { url = "view-source:" + url; } -  chrome.extension.sendRequest({handler: "openUrlInNewTab", url: url, selected: true}); -} - -/** - * Sends everything except i & ESC to the handler in background_page. i & ESC are special because they control - * insert mode which is local state to the page. The key will be are either a single ascii letter or a - * key-modifier pair, e.g. <c-a> for control a. - * - * Note that some keys will only register keydown events and not keystroke events, e.g. ESC. - */ -function onKeypress(event) { -  if (!bubbleEvent('keypress', event)) -    return; - -  var keyChar = ""; - -  // Ignore modifier keys by themselves. -  if (event.keyCode > 31) { -    keyChar = String.fromCharCode(event.charCode); - -    // Enter insert mode when the user enables the native find interface. -    if (keyChar == "f" && KeyboardUtils.isPrimaryModifierKey(event)) { -      enterInsertModeWithoutShowingIndicator(); -      return; -    } - -    if (keyChar) { -      if (findMode) { -        handleKeyCharForFindMode(keyChar); -        suppressEvent(event); -      } else if (!isInsertMode() && !findMode) { -        if (currentCompletionKeys.indexOf(keyChar) != -1) -          suppressEvent(event); - -        keyPort.postMessage({keyChar:keyChar, frameId:frameId}); -      } -    } -  } -} - -/** - * Called whenever we receive a key event.  Each individual handler has the option to stop the event's - * propagation by returning a falsy value. - */ -function bubbleEvent(type, event) { -  for (var i = handlerStack.length-1; i >= 0; i--) { -    // We need to check for existence of handler because the last function call may have caused the release of -    // more than one handler. -    if (handlerStack[i] && handlerStack[i][type] && !handlerStack[i][type](event)) { -      suppressEvent(event); -      return false; -    } -  } -  return true; -} - -function suppressEvent(event) { -  event.preventDefault(); -  event.stopPropagation(); -} - -function onKeydown(event) { -  if (!bubbleEvent('keydown', event)) -    return; - -  var keyChar = ""; - -  // handle special keys, and normal input keys with modifiers being pressed. don't handle shiftKey alone (to -  // avoid / being interpreted as ? -  if (((event.metaKey || event.ctrlKey || event.altKey) && event.keyCode > 31) -      || event.keyIdentifier.slice(0, 2) != "U+") { -    keyChar = KeyboardUtils.getKeyChar(event); - -    if (keyChar != "") { // Again, ignore just modifiers. Maybe this should replace the keyCode>31 condition. -      var modifiers = []; - -      if (event.shiftKey) -          keyChar = keyChar.toUpperCase(); -      if (event.metaKey) -          modifiers.push("m"); -      if (event.ctrlKey) -          modifiers.push("c"); -      if (event.altKey) -          modifiers.push("a"); - -      for (var i in modifiers) -          keyChar = modifiers[i] + "-" + keyChar; - -      if (modifiers.length > 0 || keyChar.length > 1) -          keyChar = "<" + keyChar + ">"; -    } -  } - -  if (isInsertMode() && KeyboardUtils.isEscape(event)) { -    // Note that we can't programmatically blur out of Flash embeds from Javascript. -    if (!isEmbed(event.srcElement)) { -      // Remove focus so the user can't just get himself back into insert mode by typing in the same input -      // box. -      if (isEditable(event.srcElement)) -        event.srcElement.blur(); -      exitInsertMode(); -      suppressEvent(event); -    } -  } -  else if (findMode) { -    if (KeyboardUtils.isEscape(event)) { -      handleEscapeForFindMode(); -      suppressEvent(event); -    } -    else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey) { -      handleDeleteForFindMode(); -      suppressEvent(event); -    } -    else if (event.keyCode == keyCodes.enter) { -      handleEnterForFindMode(); -      suppressEvent(event); -    } -    else if (!modifiers) { -      event.stopPropagation(); -    } -  } -  else if (isShowingHelpDialog && KeyboardUtils.isEscape(event)) { -    hideHelpDialog(); -  } -  else if (!isInsertMode() && !findMode) { -    if (keyChar) { -      if (currentCompletionKeys.indexOf(keyChar) != -1) -        suppressEvent(event); - -      keyPort.postMessage({keyChar:keyChar, frameId:frameId}); -    } -    else if (KeyboardUtils.isEscape(event)) { -      keyPort.postMessage({keyChar:"<ESC>", frameId:frameId}); -    } -  } - -  // Added to prevent propagating this event to other listeners if it's one that'll trigger a Vimium command. -  // The goal is to avoid the scenario where Google Instant Search uses every keydown event to dump us -  // back into the search box. As a side effect, this should also prevent overriding by other sites. -  // -  // Subject to internationalization issues since we're using keyIdentifier instead of charCode (in keypress). -  // -  // TOOD(ilya): Revisit this. Not sure it's the absolute best approach. -  if (keyChar == "" && !isInsertMode() && -     (currentCompletionKeys.indexOf(KeyboardUtils.getKeyChar(event)) != -1 || -      isValidFirstKey(KeyboardUtils.getKeyChar(event)))) -    event.stopPropagation(); -} - -function onKeyup() { -  if (!bubbleEvent('keyup', event)) -    return; -} - -function checkIfEnabledForUrl() { -  var url = window.location.toString(); - -  chrome.extension.sendRequest({ handler: "isEnabledForUrl", url: url }, function (response) { -    isEnabledForUrl = response.isEnabledForUrl; -    if (isEnabledForUrl) -      initializeWhenEnabled(); -    else if (HUD.isReady()) -      // Quickly hide any HUD we might already be showing, e.g. if we entered insert mode on page load. -      HUD.hide(); -  }); -} - -function refreshCompletionKeys(response) { -  if (response) { -    currentCompletionKeys = response.completionKeys; - -    if (response.validFirstKeys) -      validFirstKeys = response.validFirstKeys; -  } -  else { -    chrome.extension.sendRequest({ handler: "getCompletionKeys" }, refreshCompletionKeys); -  } -} - -function isValidFirstKey(keyChar) { -  return validFirstKeys[keyChar] || /[1-9]/.test(keyChar); -} - -function onFocusCapturePhase(event) { -  if (isFocusable(event.target) && !findMode) -    enterInsertModeWithoutShowingIndicator(event.target); -} - -function onBlurCapturePhase(event) { -  if (isFocusable(event.target)) -    exitInsertMode(event.target); -} - -/* - * Returns true if the element is focusable. This includes embeds like Flash, which steal the keybaord focus. - */ -function isFocusable(element) { return isEditable(element) || isEmbed(element); } - -/* - * Embedded elements like Flash and quicktime players can obtain focus but cannot be programmatically - * unfocused. - */ -function isEmbed(element) { return ["embed", "object"].indexOf(element.nodeName.toLowerCase()) > 0; } - -/* - * Input or text elements are considered focusable and able to receieve their own keyboard events, - * and will enter enter mode if focused. Also note that the "contentEditable" attribute can be set on - * any element which makes it a rich text editor, like the notes on jjot.com. - */ -function isEditable(target) { -  if (target.isContentEditable) -    return true; -  var nodeName = target.nodeName.toLowerCase(); -  // use a blacklist instead of a whitelist because new form controls are still being implemented for html5 -  var noFocus = ["radio", "checkbox"]; -  if (nodeName == "input" && noFocus.indexOf(target.type) == -1) -    return true; -  var focusableElements = ["textarea", "select"]; -  return focusableElements.indexOf(nodeName) >= 0; -} - -/* - * Enters insert mode and show an "Insert mode" message. Showing the UI is only useful when entering insert - * mode manually by pressing "i". In most cases we do not show any UI (enterInsertModeWithoutShowingIndicator) - */ -function enterInsertMode(target) { -  enterInsertModeWithoutShowingIndicator(target); -  HUD.show("Insert mode"); -} - -/* - * We cannot count on 'focus' and 'blur' events to happen sequentially. For example, if blurring element A - * causes element B to come into focus, we may get "B focus" before "A blur". Thus we only leave insert mode - * when the last editable element that came into focus -- which insertModeLock points to -- has been blurred. - * If insert mode is entered manually (via pressing 'i'), then we set insertModeLock to 'undefined', and only - * leave insert mode when the user presses <ESC>. - */ -function enterInsertModeWithoutShowingIndicator(target) { insertModeLock = target; } - -function exitInsertMode(target) { -  if (target === undefined || insertModeLock === target) { -    insertModeLock = null; -    HUD.hide(); -  } -} - -function isInsertMode() { return insertModeLock !== null; } - -// should be called whenever rawQuery is modified. -function updateFindModeQuery() { -  // the query can be treated differently (e.g. as a plain string versus regex depending on the presence of -  // escape sequences. '\' is the escape character and needs to be escaped itself to be used as a normal -  // character. here we grep for the relevant escape sequences. -  findModeQuery.isRegex = false; -  var hasNoIgnoreCaseFlag = false; -  findModeQuery.parsedQuery = findModeQuery.rawQuery.replace(/\\./g, function(match) { -    switch (match) { -      case "\\r": -        findModeQuery.isRegex = true; -        return ''; -      case "\\I": -        hasNoIgnoreCaseFlag = true; -        return ''; -      case "\\\\": -        return "\\"; -      default: -        return match; -    } -  }); - -  // default to 'smartcase' mode, unless noIgnoreCase is explicitly specified -  findModeQuery.ignoreCase = !hasNoIgnoreCaseFlag && !/[A-Z]/.test(findModeQuery.parsedQuery); - -  // if we are dealing with a regex, grep for all matches in the text, and then call window.find() on them -  // sequentially so the browser handles the scrolling / text selection. -  if (findModeQuery.isRegex) { -    try { -      var pattern = new RegExp(findModeQuery.parsedQuery, "g" + (findModeQuery.ignoreCase ? "i" : "")); -    } -    catch (e) { -      // if we catch a SyntaxError, assume the user is not done typing yet and return quietly -      return; -    } -    // innerText will not return the text of hidden elements, and strip out tags while preserving newlines -    var text = document.body.innerText; -    findModeQuery.regexMatches = text.match(pattern); -    findModeQuery.activeRegexIndex = 0; -  } -} - -function handleKeyCharForFindMode(keyChar) { -  findModeQuery.rawQuery += keyChar; -  updateFindModeQuery(); -  performFindInPlace(); -  showFindModeHUDForQuery(); -} - -function handleEscapeForFindMode() { -  exitFindMode(); -  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. -  var selection = window.getSelection(); -  if (!selection.isCollapsed) { -    var range = window.getSelection().getRangeAt(0); -    window.getSelection().removeAllRanges(); -    window.getSelection().addRange(range); -  } -  focusFoundLink() || selectFoundInputElement(); -} - -function handleDeleteForFindMode() { -  if (findModeQuery.rawQuery.length == 0) { -    exitFindMode(); -    performFindInPlace(); -  } -  else { -    findModeQuery.rawQuery = findModeQuery.rawQuery.substring(0, findModeQuery.rawQuery.length - 1); -    updateFindModeQuery(); -    performFindInPlace(); -    showFindModeHUDForQuery(); -  } -} - -// <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' -function handleEnterForFindMode() { -  exitFindMode(); -  focusFoundLink(); -  document.body.classList.add("vimiumFindMode"); -  settings.set("findModeRawQuery", findModeQuery.rawQuery); -} - -function performFindInPlace() { -  var cachedScrollX = window.scrollX; -  var cachedScrollY = window.scrollY; - -  var query = findModeQuery.isRegex ? getNextQueryFromRegexMatches(0) : findModeQuery.parsedQuery; - -  // Search backwards first to "free up" the current word as eligible for the real forward search. This allows -  // us to search in place without jumping around between matches as the query grows. -  executeFind(query, { backwards: true, caseSensitive: !findModeQuery.ignoreCase }); - -  // We need to restore the scroll position because we might've lost the right position by searching -  // backwards. -  window.scrollTo(cachedScrollX, cachedScrollY); - -  findModeQueryHasResults = executeFind(query, { caseSensitive: !findModeQuery.ignoreCase }); -} - -// :options is an optional dict. valid parameters are 'caseSensitive' and 'backwards'. -function executeFind(query, options) { -  options = options || {}; - -  // rather hacky, but this is our way of signalling to the insertMode listener not to react to the focus -  // changes that find() induces. -  var oldFindMode = findMode; -  findMode = true; - -  document.body.classList.add("vimiumFindMode"); - -  // prevent find from matching its own search query in the HUD -  HUD.hide(true); -  // ignore the selectionchange event generated by find() -  document.removeEventListener("selectionchange",restoreDefaultSelectionHighlight, true); -  var rv = window.find(query, options.caseSensitive, options.backwards, true, false, true, false); -  setTimeout(function() { -    document.addEventListener("selectionchange", restoreDefaultSelectionHighlight, true); -  }, 0); - -  findMode = oldFindMode; -  // we need to save the anchor node here because <esc> seems to nullify it, regardless of whether we do -  // preventDefault() -  findModeAnchorNode = document.getSelection().anchorNode; -  return rv; -} - -function restoreDefaultSelectionHighlight() { -  document.body.classList.remove("vimiumFindMode"); -} - -function focusFoundLink() { -  if (findModeQueryHasResults) { -    var link = getLinkFromSelection(); -    if (link) -      link.focus(); -  } -} - -function isDOMDescendant(parent, child) { -  var node = child; -  while (node !== null) { -    if (node === parent) -      return true; -    node = node.parentNode; -  } -  return false; -} - -function selectFoundInputElement() { -  // if the found text is in an input element, getSelection().anchorNode will be null, so we use activeElement -  // 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 && DomUtils.isSelectable(document.activeElement) && -      isDOMDescendant(findModeAnchorNode, document.activeElement)) { -    DomUtils.simulateSelect(document.activeElement); -    // the element has already received focus via find(), so invoke insert mode manually -    enterInsertModeWithoutShowingIndicator(document.activeElement); -  } -} - -function getNextQueryFromRegexMatches(stepSize) { -  if (!findModeQuery.regexMatches) -    return ""; // find()ing an empty query always returns false - -  var totalMatches = findModeQuery.regexMatches.length; -  findModeQuery.activeRegexIndex += stepSize + totalMatches; -  findModeQuery.activeRegexIndex %= totalMatches; - -  return findModeQuery.regexMatches[findModeQuery.activeRegexIndex]; -} - -function findAndFocus(backwards) { -  // check if the query has been changed by a script in another frame -  var mostRecentQuery = settings.get("findModeRawQuery") || ""; -  if (mostRecentQuery !== findModeQuery.rawQuery) { -    findModeQuery.rawQuery = mostRecentQuery; -    updateFindModeQuery(); -  } - -  var query = findModeQuery.isRegex ? getNextQueryFromRegexMatches(backwards ? -1 : 1) : -                                      findModeQuery.parsedQuery; - -  findModeQueryHasResults = executeFind(query, { backwards: backwards, caseSensitive: !findModeQuery.ignoreCase }); - -  if (!findModeQueryHasResults) { -    HUD.showForDuration("No matches for '" + findModeQuery.rawQuery + "'", 1000); -    return; -  } - -  // if we have found an input element via 'n', pressing <esc> immediately afterwards sends us into insert -  // mode -  var elementCanTakeInput = DomUtils.isSelectable(document.activeElement) && -    isDOMDescendant(findModeAnchorNode, document.activeElement); -  if (elementCanTakeInput) { -    handlerStack.push({ -      keydown: function(event) { -        handlerStack.pop(); -        if (KeyboardUtils.isEscape(event)) { -          DomUtils.simulateSelect(document.activeElement); -          enterInsertModeWithoutShowingIndicator(document.activeElement); -          return false; // we have 'consumed' this event, so do not propagate -        } -        return true; -      } -    }); -  } - -  focusFoundLink(); -} - -function performFind() { findAndFocus(); } - -function performBackwardsFind() { findAndFocus(true); } - -function getLinkFromSelection() { -  var node = window.getSelection().anchorNode; -  while (node && node !== document.body) { -    if (node.nodeName.toLowerCase() === 'a') return node; -    node = node.parentNode; -  } -  return null; -} - -// used by the findAndFollow* functions. -function 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(); -    linkElement.focus(); -    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. - */ -function findAndFollowLink(linkStrings) { -  var linksXPath = DomUtils.makeXPath(["a", "*[@onclick or @role='link' or contains(@class, 'button')]"]); -  var links = DomUtils.evaluateXPath(linksXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE); -  var 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 (var i = links.snapshotLength - 1; i >= 0; i--) { -    var link = links.snapshotItem(i); - -    // ensure link is visible (we don't mind if it is scrolled offscreen) -    var boundingClientRect = link.getBoundingClientRect(); -    if (boundingClientRect.width == 0 || boundingClientRect.height == 0) -      continue; -    var computedStyle = window.getComputedStyle(link, null); -    if (computedStyle.getPropertyValue('visibility') != 'visible' || -        computedStyle.getPropertyValue('display') == 'none') -      continue; - -    var linkMatches = false; -    for (var j = 0; j < linkStrings.length; j++) { -      if (link.innerText.toLowerCase().indexOf(linkStrings[j]) !== -1) { -        linkMatches = true; -        break; -      } -    } -    if (!linkMatches) continue; - -    candidateLinks.push(link); -  } - -  if (candidateLinks.length === 0) return; - -  function wordCount(link) { return 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(function(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(function(a,b) { -        var wcA = wordCount(a), wcB = wordCount(b); -        return wcA === wcB ? a.originalIndex - b.originalIndex : wcA - wcB; -      }) -      .filter(function(a){return wordCount(a) <= wordCount(candidateLinks[0]) + 1}); - -  // try to get exact word matches first -  for (var i = 0; i < linkStrings.length; i++) -    for (var j = 0; j < candidateLinks.length; j++) { -      var exactWordRegex = new RegExp("\\b" + linkStrings[i] + "\\b", "i"); -      if (exactWordRegex.test(candidateLinks[j].innerText)) { -        followLink(candidateLinks[j]); -        return true; -      } -    } - -  for (var i = 0; i < linkStrings.length; i++) -    for (var j = 0; j < candidateLinks.length; j++) { -      if (candidateLinks[j].innerText.toLowerCase().indexOf(linkStrings[i]) !== -1) { -        followLink(candidateLinks[j]); -        return true; -      } -    } - -  return false; -} - -function findAndFollowRel(value) { -  var relTags = ['link', 'a', 'area']; -  for (i = 0; i < relTags.length; i++) { -    var elements = document.getElementsByTagName(relTags[i]); -    for (j = 0; j < elements.length; j++) { -      if (elements[j].hasAttribute('rel') && elements[j].rel == value) { -        followLink(elements[j]); -        return true; -      } -    } -  } -} - -function goPrevious() { -  var previousPatterns = settings.get("previousPatterns") || ""; -  var previousStrings = previousPatterns.split(","); -  findAndFollowRel('prev') || findAndFollowLink(previousStrings); -} - -function goNext() { -  var nextPatterns = settings.get("nextPatterns") || ""; -  var nextStrings = nextPatterns.split(","); -  findAndFollowRel('next') || findAndFollowLink(nextStrings); -} - -function showFindModeHUDForQuery() { -  if (findModeQueryHasResults || findModeQuery.parsedQuery.length == 0) -    HUD.show("/" + findModeQuery.rawQuery); -  else -    HUD.show("/" + findModeQuery.rawQuery + " (No Matches)"); -} - -function enterFindMode() { -  findModeQuery = { rawQuery: "" }; -  findMode = true; -  HUD.show("/"); -} - -function exitFindMode() { -  findMode = false; -  HUD.hide(); -} - -function showHelpDialog(html, fid) { -  if (isShowingHelpDialog || !document.body || fid != frameId) -    return; -  isShowingHelpDialog = true; -  var container = document.createElement("div"); -  container.id = "vimiumHelpDialogContainer"; -  container.className = "vimiumReset"; - -  document.body.appendChild(container); - -  container.innerHTML = html; -  container.getElementsByClassName("closeButton")[0].addEventListener("click", hideHelpDialog, false); -  container.getElementsByClassName("optionsPage")[0].addEventListener("click", -      function() { chrome.extension.sendRequest({ handler: "openOptionsPageInNewTab" }); }, false); - -  // This is necessary because innerHTML does not evaluate javascript embedded in <script> tags. -  var scripts = Array.prototype.slice.call(container.getElementsByTagName("script")); -  scripts.forEach(function(script) { eval(script.text); }); - -} - -function hideHelpDialog(clickEvent) { -  isShowingHelpDialog = false; -  var helpDialog = document.getElementById("vimiumHelpDialogContainer"); -  if (helpDialog) -    helpDialog.parentNode.removeChild(helpDialog); -  if (clickEvent) -    clickEvent.preventDefault(); -} - -/* - * A heads-up-display (HUD) for showing Vimium page operations. - * Note: you cannot interact with the HUD until document.body is available. - */ -HUD = { -  _tweenId: -1, -  _displayElement: null, -  _upgradeNotificationElement: null, - -  // This HUD is styled to precisely mimick the chrome HUD on Mac. Use the "has_popup_and_link_hud.html" -  // test harness to tweak these styles to match Chrome's. One limitation of our HUD display is that -  // it doesn't sit on top of horizontal scrollbars like Chrome's HUD does. - -  showForDuration: function(text, duration) { -    HUD.show(text); -    HUD._showForDurationTimerId = setTimeout(function() { HUD.hide(); }, duration); -  }, - -  show: function(text) { -    if (!HUD.enabled()) return; -    clearTimeout(HUD._showForDurationTimerId); -    HUD.displayElement().innerHTML = text; -    clearInterval(HUD._tweenId); -    HUD._tweenId = Tween.fade(HUD.displayElement(), 1.0, 150); -    HUD.displayElement().style.display = ""; -  }, - -  showUpgradeNotification: function(version) { -    HUD.upgradeNotificationElement().innerHTML = "Vimium has been updated to " + -      "<a class='vimiumReset' href='https://chrome.google.com/extensions/detail/dbepggeogbaibhgnhhndojpepiihcmeb'>" + -      version + "</a>.<a class='vimiumReset close-button' href='#'>x</a>"; -    var links = HUD.upgradeNotificationElement().getElementsByTagName("a"); -    links[0].addEventListener("click", HUD.onUpdateLinkClicked, false); -    links[1].addEventListener("click", function(event) { -      event.preventDefault(); -      HUD.onUpdateLinkClicked(); -    }); -    Tween.fade(HUD.upgradeNotificationElement(), 1.0, 150); -  }, - -  onUpdateLinkClicked: function(event) { -    HUD.hideUpgradeNotification(); -    chrome.extension.sendRequest({ handler: "upgradeNotificationClosed" }); -  }, - -  hideUpgradeNotification: function(clickEvent) { -    Tween.fade(HUD.upgradeNotificationElement(), 0, 150, -      function() { HUD.upgradeNotificationElement().style.display = "none"; }); -  }, - -  /* -   * Retrieves the HUD HTML element. -   */ -  displayElement: function() { -    if (!HUD._displayElement) { -      HUD._displayElement = HUD.createHudElement(); -      // Keep this far enough to the right so that it doesn't collide with the "popups blocked" chrome HUD. -      HUD._displayElement.style.right = "150px"; -    } -    return HUD._displayElement; -  }, - -  upgradeNotificationElement: function() { -    if (!HUD._upgradeNotificationElement) { -      HUD._upgradeNotificationElement = HUD.createHudElement(); -      // Position this just to the left of our normal HUD. -      HUD._upgradeNotificationElement.style.right = "315px"; -    } -    return HUD._upgradeNotificationElement; -  }, - -  createHudElement: function() { -    var element = document.createElement("div"); -    element.className = "vimiumReset vimiumHUD"; -    document.body.appendChild(element); -    return element; -  }, - -  hide: function(immediate) { -    clearInterval(HUD._tweenId); -    if (immediate) -      HUD.displayElement().style.display = "none"; -    else -      HUD._tweenId = Tween.fade(HUD.displayElement(), 0, 150, -        function() { HUD.displayElement().style.display = "none"; }); -  }, - -  isReady: function() { return document.body != null; }, - -  /* A preference which can be toggled in the Options page. */ -  enabled: function() { return !settings.get("hideHud"); } - -}; - -Tween = { -  /* -   * Fades an element's alpha. Returns a timer ID which can be used to stop the tween via clearInterval. -   */ -  fade: function(element, toAlpha, duration, onComplete) { -    var state = {}; -    state.duration = duration; -    state.startTime = (new Date()).getTime(); -    state.from = parseInt(element.style.opacity) || 0; -    state.to = toAlpha; -    state.onUpdate = function(value) { -      element.style.opacity = value; -      if (value == state.to && onComplete) -        onComplete(); -    }; -    state.timerId = setInterval(function() { Tween.performTweenStep(state); }, 50); -    return state.timerId; -  }, - -  performTweenStep: function(state) { -    var elapsed = (new Date()).getTime() - state.startTime; -    if (elapsed >= state.duration) { -      clearInterval(state.timerId); -      state.onUpdate(state.to) -    } else { -      var value = (elapsed / state.duration)  * (state.to - state.from) + state.from; -      state.onUpdate(value); -    } -  } -}; - -/* - * Adds the given CSS to the page. - */ -function addCssToPage(css, id) { -  var head = document.getElementsByTagName("head")[0]; -  if (!head) { -    head = document.createElement("head"); -    document.documentElement.appendChild(head); -  } -  var style = document.createElement("style"); -  style.id = id; -  style.type = "text/css"; -  style.appendChild(document.createTextNode(css)); -  head.appendChild(style); -} - -initializePreDomReady(); -window.addEventListener("DOMContentLoaded", initializeOnDomReady); - -window.onbeforeunload = function() { -  chrome.extension.sendRequest({ handler: "updateScrollPosition", -      scrollX: window.scrollX, scrollY: window.scrollY }); -} | 
