diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 1036 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.js | 1183 |
3 files changed, 1037 insertions, 1183 deletions
@@ -2,6 +2,7 @@ background_scripts/completion.js background_scripts/commands.js background_scripts/settings.js content_scripts/link_hints.js +content_scripts/vimium_frontend.js tests/completion_test.js tests/test_helper.js tests/utils_test.js 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 }); -} |
