aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore15
-rw-r--r--Cakefile2
-rw-r--r--background_scripts/main.coffee610
-rw-r--r--background_scripts/main.js783
-rw-r--r--content_scripts/vimium_frontend.coffee42
-rw-r--r--help_dialog.html36
-rw-r--r--lib/dom_utils.coffee4
-rw-r--r--options/options.coffee2
-rw-r--r--options/options.html4
9 files changed, 655 insertions, 843 deletions
diff --git a/.gitignore b/.gitignore
index 1925b316..e5702ba4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,17 +2,4 @@
*.swo
*.swp
*.crx
-background_scripts/completion.js
-background_scripts/commands.js
-background_scripts/settings.js
-content_scripts/link_hints.js
-content_scripts/vimium_frontend.js
-content_scripts/vomnibar.js
-tests/completion_test.js
-tests/test_helper.js
-tests/utils_test.js
-lib/clipboard.js
-lib/dom_utils.js
-lib/keyboard_utils.js
-lib/utils.js
-options/options.js
+*.js
diff --git a/Cakefile b/Cakefile
index 6e7893cf..55653b7b 100644
--- a/Cakefile
+++ b/Cakefile
@@ -13,6 +13,7 @@ src_directories = ["tests", "background_scripts", "content_scripts", "lib", "opt
task "build", "compile all coffeescript files to javascript", ->
coffee = spawn "coffee", ["-c"].concat(src_directories)
coffee.stdout.on "data", (data) -> console.log data.toString().trim()
+ coffee.stderr.on "data", (data) -> console.log data.toString().trim()
task "clean", "removes any js files which were compiled from coffeescript", ->
src_directories.forEach (directory) ->
@@ -32,6 +33,7 @@ task "clean", "removes any js files which were compiled from coffeescript", ->
task "autobuild", "continually rebuild coffeescript files using coffee --watch", ->
coffee = spawn "coffee", ["-cw"].concat(src_directories)
coffee.stdout.on "data", (data) -> console.log data.toString().trim()
+ coffee.stderr.on "data", (data) -> console.log data.toString().trim()
task "package", "build .crx file", ->
invoke "build"
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
new file mode 100644
index 00000000..24e7dc79
--- /dev/null
+++ b/background_scripts/main.coffee
@@ -0,0 +1,610 @@
+root = exports ? window
+
+currentVersion = Utils.getCurrentVersion()
+
+tabQueue = {} # windowId -> Array
+openTabs = {} # tabId -> object with various tab properties
+keyQueue = "" # Queue of keys typed
+validFirstKeys = {}
+singleKeyCommands = []
+focusedFrame = null
+framesForTab = {}
+
+# Keys are either literal characters, or "named" - for example <a-b> (alt+b), <left> (left arrow) or <f12>
+# This regular expression captures two groups: the first is a named key, the second is the remainder of
+# the string.
+namedKeyRegex = /^(<(?:[amc]-.|(?:[amc]-)?[a-z0-9]{2,5})>)(.*)$/
+
+# Event handlers
+selectionChangedHandlers = []
+tabLoadedHandlers = {} # tabId -> function()
+
+completionSources =
+ bookmarks: new BookmarkCompleter()
+ history: new HistoryCompleter()
+ domains: new DomainCompleter()
+ tabs: new TabCompleter()
+
+completers =
+ omni: new MultiCompleter([
+ completionSources.bookmarks,
+ completionSources.history,
+ completionSources.domains])
+ bookmarks: new MultiCompleter([completionSources.bookmarks])
+ tabs: new MultiCompleter([completionSources.tabs])
+
+chrome.extension.onConnect.addListener((port, name) ->
+ senderTabId = if port.sender.tab then port.sender.tab.id else null
+ # If this is a tab we've been waiting to open, execute any "tab loaded" handlers, e.g. to restore
+ # the tab's scroll position. Wait until domReady before doing this; otherwise operations like restoring
+ # the scroll position will not be possible.
+ if (port.name == "domReady" && senderTabId != null)
+ if (tabLoadedHandlers[senderTabId])
+ toCall = tabLoadedHandlers[senderTabId]
+ # Delete first to be sure there's no circular events.
+ delete tabLoadedHandlers[senderTabId]
+ toCall.call()
+
+ # domReady is the appropriate time to show the "vimium has been upgraded" message.
+ # TODO: This might be broken on pages with frames.
+ if (shouldShowUpgradeMessage())
+ chrome.tabs.sendRequest(senderTabId, { name: "showUpgradeNotification", version: currentVersion })
+
+ if (portHandlers[port.name])
+ port.onMessage.addListener(portHandlers[port.name])
+)
+
+chrome.extension.onRequest.addListener((request, sender, sendResponse) ->
+ if (sendRequestHandlers[request.handler])
+ sendResponse(sendRequestHandlers[request.handler](request, sender))
+ # Ensure the sendResponse callback is freed.
+ return false)
+
+#
+# Used by the content scripts to get their full URL. This is needed for URLs like "view-source:http:# .."
+# because window.location doesn't know anything about the Chrome-specific "view-source:".
+#
+getCurrentTabUrl = (request, sender) -> sender.tab.url
+
+#
+# Checks the user's preferences in local storage to determine if Vimium is enabled for the given URL.
+#
+isEnabledForUrl = (request) ->
+ # excludedUrls are stored as a series of URL expressions separated by newlines.
+ excludedUrls = Settings.get("excludedUrls").split("\n")
+ isEnabled = true
+ for url in excludedUrls
+ # The user can add "*" to the URL which means ".*"
+ regexp = new RegExp("^" + url.replace(/\*/g, ".*") + "$")
+ isEnabled = false if request.url.match(regexp)
+ { isEnabledForUrl: isEnabled }
+
+# Called by the popup UI. Strips leading/trailing whitespace and ignores empty strings.
+root.addExcludedUrl = (url) ->
+ return unless url = url.trim()
+
+ excludedUrls = Settings.get("excludedUrls")
+ excludedUrls += "\n" + url
+ Settings.set("excludedUrls", excludedUrls)
+
+ chrome.tabs.query({ windowId: chrome.windows.WINDOW_ID_CURRENT, active: true },
+ (tabs) -> updateActiveState(tabs[0].id))
+
+getShowAdvancedCommands = (request) -> Settings.get("helpDialog_showAdvancedCommands")
+
+saveHelpDialogSettings = (request) ->
+ Settings.set("helpDialog_showAdvancedCommands", request.showAdvancedCommands)
+
+# Retrieves the help dialog HTML template from a file, and populates it with the latest keybindings.
+# This is called by options.coffee.
+root.helpDialogHtml = (showUnboundCommands, showCommandNames, customTitle) ->
+ commandsToKey = {}
+ for key of Commands.keyToCommandRegistry
+ command = Commands.keyToCommandRegistry[key].command
+ commandsToKey[command] = (commandsToKey[command] || []).concat(key)
+
+ dialogHtml = fetchFileContents("help_dialog.html")
+ for group of Commands.commandGroups
+ dialogHtml = dialogHtml.replace("{{#{group}}}",
+ helpDialogHtmlForCommandGroup(group, commandsToKey, Commands.availableCommands,
+ showUnboundCommands, showCommandNames))
+ dialogHtml = dialogHtml.replace("{{version}}", currentVersion)
+ dialogHtml = dialogHtml.replace("{{title}}", customTitle || "Help")
+ dialogHtml = dialogHtml.replace("{{showAdvancedCommands}}",
+ Settings.get("helpDialog_showAdvancedCommands"))
+ dialogHtml
+
+#
+# Generates HTML for a given set of commands. commandGroups are defined in commands.js
+#
+helpDialogHtmlForCommandGroup = (group, commandsToKey, availableCommands,
+ showUnboundCommands, showCommandNames) ->
+ html = []
+ for command in Commands.commandGroups[group]
+ bindings = (commandsToKey[command] || [""]).join(", ")
+ if (showUnboundCommands || commandsToKey[command])
+ isAdvanced = Commands.advancedCommands.indexOf(command) >= 0
+ html.push(
+ "<tr class='vimiumReset #{"advanced" if isAdvanced}'>",
+ "<td class='vimiumReset'>", Utils.escapeHtml(bindings), "</td>",
+ "<td class='vimiumReset'>:</td><td class='vimiumReset'>", availableCommands[command].description)
+
+ if (showCommandNames)
+ html.push("<span class='vimiumReset commandName'>(#{command})</span>")
+
+ html.push("</td></tr>")
+ html.join("\n")
+
+#
+# Fetches the contents of a file bundled with this extension.
+#
+fetchFileContents = (extensionFileName) ->
+ req = new XMLHttpRequest()
+ req.open("GET", chrome.extension.getURL(extensionFileName), false) # false => synchronous
+ req.send()
+ req.responseText
+
+#
+# Returns the keys that can complete a valid command given the current key queue.
+#
+getCompletionKeysRequest = (request) ->
+ name: "refreshCompletionKeys"
+ completionKeys: generateCompletionKeys()
+ validFirstKeys: validFirstKeys
+
+#
+# Opens the url in the current tab.
+#
+openUrlInCurrentTab = (request) ->
+ chrome.tabs.getSelected(null,
+ (tab) -> chrome.tabs.update(tab.id, { url: Utils.convertToUrl(request.url) }))
+
+#
+# Opens request.url in new tab and switches to it if request.selected is true.
+#
+openUrlInNewTab = (request) ->
+ chrome.tabs.getSelected(null, (tab) ->
+ chrome.tabs.create({ url: Utils.convertToUrl(request.url), index: tab.index + 1, selected: true }))
+
+#
+# Called when the user has clicked the close icon on the "Vimium has been updated" message.
+# We should now dismiss that message in all tabs.
+#
+upgradeNotificationClosed = (request) ->
+ Settings.set("previousVersion", currentVersion)
+ sendRequestToAllTabs({ name: "hideUpgradeNotification" })
+
+#
+# Copies some data (request.data) to the clipboard.
+#
+copyToClipboard = (request) -> Clipboard.copy(request.data)
+
+#
+# Selects the tab with the ID specified in request.id
+#
+selectSpecificTab = (request) -> chrome.tabs.update(request.id, { selected: true })
+
+#
+# Used by the content scripts to get settings from the local storage.
+#
+handleSettings = (args, port) ->
+ if (args.operation == "get")
+ value = Settings.get(args.key)
+ port.postMessage({ key: args.key, value: value })
+ else # operation == "set"
+ Settings.set(args.key, args.value)
+
+refreshCompleter = (request) -> completers[request.name].refresh()
+
+filterCompleter = (args, port) ->
+ queryTerms = if (args.query == "") then [] else args.query.split(" ")
+ completers[args.name].filter(queryTerms, (results) -> port.postMessage({ id: args.id, results: results }))
+
+#
+# Used by everyone to get settings from local storage.
+#
+getSettingFromLocalStorage = (setting) ->
+ if (localStorage[setting] != "" && !localStorage[setting])
+ defaultSettings[setting]
+ else
+ localStorage[setting]
+
+getCurrentTimeInSeconds = -> Math.floor((new Date()).getTime() / 1000)
+
+chrome.tabs.onSelectionChanged.addListener((tabId, selectionInfo) ->
+ if (selectionChangedHandlers.length > 0)
+ selectionChangedHandlers.pop().call())
+
+repeatFunction = (func, totalCount, currentCount, frameId) ->
+ if (currentCount < totalCount)
+ func(
+ -> repeatFunction(func, totalCount, currentCount + 1, frameId),
+ frameId)
+
+# Start action functions
+
+# These are commands which are bound to keystroke which must be handled by the background page. They are
+# mapped in commands.coffee.
+BackgroundCommands =
+ createTab: (callback) -> chrome.tabs.create({ url: "chrome://newtab" }, (tab) -> callback())
+ nextTab: (callback) -> selectTab(callback, "next")
+ previousTab: (callback) -> selectTab(callback, "previous")
+ firstTab: (callback) -> selectTab(callback, "first")
+ lastTab: (callback) -> selectTab(callback, "last")
+ removeTab: (callback) ->
+ chrome.tabs.getSelected(null, (tab) ->
+ chrome.tabs.remove(tab.id)
+ # We can't just call the callback here because we need to wait
+ # for the selection to change to consider this action done.
+ selectionChangedHandlers.push(callback))
+ restoreTab: (callback) ->
+ # TODO(ilya): Should this be getLastFocused instead?
+ chrome.windows.getCurrent((window) ->
+ return unless (tabQueue[window.id] && tabQueue[window.id].length > 0)
+ tabQueueEntry = tabQueue[window.id].pop()
+ # Clean out the tabQueue so we don't have unused windows laying about.
+ delete tabQueue[window.id] if (tabQueue[window.id].length == 0)
+
+ # We have to chain a few callbacks to set the appropriate scroll position. We can't just wait until the
+ # tab is created because the content script is not available during the "loading" state. We need to
+ # wait until that's over before we can call setScrollPosition.
+ chrome.tabs.create({ url: tabQueueEntry.url, index: tabQueueEntry.positionIndex }, (tab) ->
+ tabLoadedHandlers[tab.id] = ->
+ scrollPort = chrome.tabs.sendRequest(tab.id,
+ name: "setScrollPosition",
+ scrollX: tabQueueEntry.scrollX,
+ scrollY: tabQueueEntry.scrollY)
+ callback()))
+ openCopiedUrlInCurrentTab: (request) -> openUrlInCurrentTab({ url: Clipboard.paste() })
+ openCopiedUrlInNewTab: (request) -> openUrlInNewTab({ url: Clipboard.paste() })
+ showHelp: (callback, frameId) ->
+ chrome.tabs.getSelected(null, (tab) ->
+ chrome.tabs.sendRequest(tab.id,
+ { name: "toggleHelpDialog", dialogHtml: helpDialogHtml(), frameId:frameId }))
+
+# Selects a tab before or after the currently selected tab.
+# - direction: "next", "previous", "first" or "last".
+selectTab = (callback, direction) ->
+ chrome.tabs.getAllInWindow(null, (tabs) ->
+ return unless tabs.length > 1
+ chrome.tabs.getSelected(null, (currentTab) ->
+ switch direction
+ when "next"
+ toSelect = tabs[(currentTab.index + 1 + tabs.length) % tabs.length]
+ when "previous"
+ toSelect = tabs[(currentTab.index - 1 + tabs.length) % tabs.length]
+ when "first"
+ toSelect = tabs[0]
+ when "last"
+ toSelect = tabs[tabs.length - 1]
+ selectionChangedHandlers.push(callback)
+ chrome.tabs.update(toSelect.id, { selected: true })))
+
+updateOpenTabs = (tab) ->
+ openTabs[tab.id] = { url: tab.url, positionIndex: tab.index, windowId: tab.windowId }
+ # Frames are recreated on refresh
+ delete framesForTab[tab.id]
+
+# Updates the browserAction icon to indicated whether Vimium is enabled or disabled on the current page.
+# Also disables Vimium if it is currently enabled but should be disabled according to the url blacklist.
+# This lets you disable Vimium on a page without needing to reload.
+#
+# Three situations are considered:
+# 1. Active tab is disabled -> disable icon
+# 2. Active tab is enabled and should be enabled -> enable icon
+# 3. Active tab is enabled but should be disabled -> disable icon and disable vimium
+updateActiveState = (tabId) ->
+ enabledIcon = "icons/browser_action_enabled.png"
+ disabledIcon = "icons/browser_action_disabled.png"
+ chrome.tabs.get(tabId, (tab) ->
+ # Default to disabled state in case we can't connect to Vimium, primarily for the "New Tab" page.
+ chrome.browserAction.setIcon({ path: disabledIcon })
+ chrome.tabs.sendRequest(tabId, { name: "getActiveState" }, (response) ->
+ isCurrentlyEnabled = (response? && response.enabled)
+ shouldBeEnabled = isEnabledForUrl({url: tab.url}).isEnabledForUrl
+
+ if (isCurrentlyEnabled)
+ if (shouldBeEnabled)
+ chrome.browserAction.setIcon({ path: enabledIcon })
+ else
+ chrome.browserAction.setIcon({ path: disabledIcon })
+ chrome.tabs.sendRequest(tabId, { name: "disableVimium" })
+ else
+ chrome.browserAction.setIcon({ path: disabledIcon })))
+
+handleUpdateScrollPosition = (request, sender) ->
+ updateScrollPosition(sender.tab, request.scrollX, request.scrollY)
+
+updateScrollPosition = (tab, scrollX, scrollY) ->
+ openTabs[tab.id].scrollX = scrollX
+ openTabs[tab.id].scrollY = scrollY
+
+chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) ->
+ return unless changeInfo.status == "loading" # only do this once per URL change
+ updateOpenTabs(tab)
+ updateActiveState(tabId))
+
+chrome.tabs.onAttached.addListener((tabId, attachedInfo) ->
+ # We should update all the tabs in the old window and the new window.
+ if openTabs[tabId]
+ updatePositionsAndWindowsForAllTabsInWindow(openTabs[tabId].windowId)
+ updatePositionsAndWindowsForAllTabsInWindow(attachedInfo.newWindowId))
+
+chrome.tabs.onMoved.addListener((tabId, moveInfo) ->
+ updatePositionsAndWindowsForAllTabsInWindow(moveInfo.windowId))
+
+chrome.tabs.onRemoved.addListener((tabId) ->
+ openTabInfo = openTabs[tabId]
+ updatePositionsAndWindowsForAllTabsInWindow(openTabInfo.windowId)
+
+ # If we restore chrome:# pages, they'll ignore Vimium keystrokes when they reappear.
+ # Pretend they never existed and adjust tab indices accordingly.
+ # Could possibly expand this into a blacklist in the future
+ if (/^chrome[^:]*:\/\/.*/.test(openTabInfo.url))
+ for i of tabQueue[openTabInfo.windowId]
+ if (tabQueue[openTabInfo.windowId][i].positionIndex > openTabInfo.positionIndex)
+ tabQueue[openTabInfo.windowId][i].positionIndex--
+ return
+
+ if (tabQueue[openTabInfo.windowId])
+ tabQueue[openTabInfo.windowId].push(openTabInfo)
+ else
+ tabQueue[openTabInfo.windowId] = [openTabInfo]
+
+ delete openTabs[tabId]
+ delete framesForTab[tabId])
+
+chrome.tabs.onActiveChanged.addListener((tabId, selectInfo) -> updateActiveState(tabId))
+
+chrome.windows.onRemoved.addListener((windowId) -> delete tabQueue[windowId])
+
+# End action functions
+
+updatePositionsAndWindowsForAllTabsInWindow = (windowId) ->
+ chrome.tabs.getAllInWindow(windowId, (tabs) ->
+ for tab in tabs
+ openTabInfo = openTabs[tab.id]
+ if (openTabInfo)
+ openTabInfo.positionIndex = tab.index
+ openTabInfo.windowId = tab.windowId)
+
+splitKeyIntoFirstAndSecond = (key) ->
+ if (key.search(namedKeyRegex) == 0)
+ { first: RegExp.$1, second: RegExp.$2 }
+ else
+ { first: key[0], second: key.slice(1) }
+
+getActualKeyStrokeLength = (key) ->
+ if (key.search(namedKeyRegex) == 0)
+ 1 + getActualKeyStrokeLength(RegExp.$2)
+ else
+ key.length
+
+populateValidFirstKeys = ->
+ for key of Commands.keyToCommandRegistry
+ if (getActualKeyStrokeLength(key) == 2)
+ validFirstKeys[splitKeyIntoFirstAndSecond(key).first] = true
+
+populateSingleKeyCommands = ->
+ for key of Commands.keyToCommandRegistry
+ if (getActualKeyStrokeLength(key) == 1)
+ singleKeyCommands.push(key)
+
+# Invoked by options.coffee.
+root.refreshCompletionKeysAfterMappingSave = ->
+ validFirstKeys = {}
+ singleKeyCommands = []
+
+ populateValidFirstKeys()
+ populateSingleKeyCommands()
+
+ sendRequestToAllTabs(getCompletionKeysRequest())
+
+# Generates a list of keys that can complete a valid command given the current key queue or the one passed in
+generateCompletionKeys = (keysToCheck) ->
+ splitHash = splitKeyQueue(keysToCheck || keyQueue)
+ command = splitHash.command
+ count = splitHash.count
+
+ completionKeys = singleKeyCommands.slice(0)
+
+ if (getActualKeyStrokeLength(command) == 1)
+ for key of Commands.keyToCommandRegistry
+ splitKey = splitKeyIntoFirstAndSecond(key)
+ if (splitKey.first == command)
+ completionKeys.push(splitKey.second)
+
+ completionKeys
+
+splitKeyQueue = (queue) ->
+ match = /([1-9][0-9]*)?(.*)/.exec(queue)
+ count = parseInt(match[1], 10)
+ command = match[2]
+
+ { count: count, command: command }
+
+handleKeyDown = (request, port) ->
+ key = request.keyChar
+ if (key == "<ESC>")
+ console.log("clearing keyQueue")
+ keyQueue = ""
+ else
+ console.log("checking keyQueue: [", keyQueue + key, "]")
+ keyQueue = checkKeyQueue(keyQueue + key, port.sender.tab.id, request.frameId)
+ console.log("new KeyQueue: " + keyQueue)
+
+checkKeyQueue = (keysToCheck, tabId, frameId) ->
+ refreshedCompletionKeys = false
+ splitHash = splitKeyQueue(keysToCheck)
+ command = splitHash.command
+ count = splitHash.count
+
+ return keysToCheck if command.length == 0
+ count = 1 if isNaN(count)
+
+ if (Commands.keyToCommandRegistry[command])
+ registryEntry = Commands.keyToCommandRegistry[command]
+
+ if !registryEntry.isBackgroundCommand
+ chrome.tabs.sendRequest(tabId,
+ name: "executePageCommand",
+ command: registryEntry.command,
+ frameId: frameId,
+ count: count,
+ passCountToFunction: registryEntry.passCountToFunction,
+ completionKeys: generateCompletionKeys(""))
+ refreshedCompletionKeys = true
+ else
+ if registryEntry.passCountToFunction
+ BackgroundCommands[registryEntry.command](count)
+ else
+ repeatFunction(BackgroundCommands[registryEntry.command], count, 0, frameId)
+
+ newKeyQueue = ""
+ else if (getActualKeyStrokeLength(command) > 1)
+ splitKey = splitKeyIntoFirstAndSecond(command)
+
+ # The second key might be a valid command by its self.
+ if (Commands.keyToCommandRegistry[splitKey.second])
+ newKeyQueue = checkKeyQueue(splitKey.second, tabId, frameId)
+ else
+ newKeyQueue = (if validFirstKeys[splitKey.second] then splitKey.second else "")
+ else
+ newKeyQueue = (if validFirstKeys[command] then count.toString() + command else "")
+
+ # If we haven't sent the completion keys piggybacked on executePageCommand,
+ # send them by themselves.
+ unless refreshedCompletionKeys
+ chrome.tabs.sendRequest(tabId, getCompletionKeysRequest(), null)
+
+ newKeyQueue
+
+#
+# Message all tabs. Args should be the arguments hash used by the Chrome sendRequest API.
+#
+sendRequestToAllTabs = (args) ->
+ chrome.windows.getAll({ populate: true }, (windows) ->
+ for window in windows
+ for tab in window.tabs
+ chrome.tabs.sendRequest(tab.id, args, null))
+
+# Compares two version strings (e.g. "1.1" and "1.5") and returns
+# -1 if versionA is < versionB, 0 if they're equal, and 1 if versionA is > versionB.
+compareVersions = (versionA, versionB) ->
+ versionA = versionA.split(".")
+ versionB = versionB.split(".")
+ for i in [0...(Math.max(versionA.length, versionB.length))]
+ a = parseInt(versionA[i] || 0, 10)
+ b = parseInt(versionB[i] || 0, 10)
+ if (a < b)
+ return -1
+ else if (a > b)
+ return 1
+ 0
+
+#
+# Returns true if the current extension version is greater than the previously recorded version in
+# localStorage, and false otherwise.
+#
+shouldShowUpgradeMessage = ->
+ # Avoid showing the upgrade notification when previousVersion is undefined, which is the case for new
+ # installs.
+ Settings.set("previousVersion", currentVersion) unless Settings.get("previousVersion")
+ compareVersions(currentVersion, Settings.get("previousVersion")) == 1
+
+openOptionsPageInNewTab = ->
+ chrome.tabs.getSelected(null, (tab) ->
+ chrome.tabs.create({ url: chrome.extension.getURL("options/options.html"), index: tab.index + 1 }))
+
+registerFrame = (request, sender) ->
+ unless framesForTab[sender.tab.id]
+ framesForTab[sender.tab.id] = { frames: [] }
+
+ if (request.is_top)
+ focusedFrame = request.frameId
+ framesForTab[sender.tab.id].total = request.total
+
+ framesForTab[sender.tab.id].frames.push({ id: request.frameId, area: request.area })
+
+ # We've seen all the frames. Time to focus the largest one.
+ # NOTE: Disabled because it's buggy with iframes.
+ # if (framesForTab[sender.tab.id].frames.length >= framesForTab[sender.tab.id].total)
+ # focusLargestFrame(sender.tab.id)
+
+focusLargestFrame = (tabId) ->
+ mainFrameId = null
+ mainFrameArea = 0
+
+ for frame in framesForTab[tabId]
+ if (frame.area > mainFrameArea)
+ mainFrameId = frame.id
+ mainFrameArea = frame.area
+
+ chrome.tabs.sendRequest(tabId, { name: "focusFrame", frameId: mainFrameId, highlight: false })
+
+handleFrameFocused = (request, sender) -> focusedFrame = request.frameId
+
+nextFrame = (count) ->
+ chrome.tabs.getSelected(null, (tab) ->
+ frames = framesForTab[tab.id].frames
+ curr_index = getCurrFrameIndex(frames)
+
+ # TODO: Skip the "top" frame (which doesn't actually have a <frame> tag),
+ # since it exists only to contain the other frames.
+ new_index = (curr_index + count) % frames.length
+
+ chrome.tabs.sendRequest(tab.id, { name: "focusFrame", frameId: frames[new_index].id, highlight: true }))
+
+getCurrFrameIndex = (frames) ->
+ for i in [0...frames.length]
+ return i if frames[i].id == focusedFrame
+ frames.length + 1
+
+# Port handler mapping
+portHandlers =
+ keyDown: handleKeyDown,
+ settings: handleSettings,
+ filterCompleter: filterCompleter
+
+sendRequestHandlers =
+ getCompletionKeys: getCompletionKeysRequest,
+ getCurrentTabUrl: getCurrentTabUrl,
+ getShowAdvancedCommands: getShowAdvancedCommands,
+ openUrlInNewTab: openUrlInNewTab,
+ openUrlInCurrentTab: openUrlInCurrentTab,
+ openOptionsPageInNewTab: openOptionsPageInNewTab,
+ registerFrame: registerFrame,
+ frameFocused: handleFrameFocused,
+ upgradeNotificationClosed: upgradeNotificationClosed,
+ updateScrollPosition: handleUpdateScrollPosition,
+ copyToClipboard: copyToClipboard,
+ isEnabledForUrl: isEnabledForUrl,
+ saveHelpDialogSettings: saveHelpDialogSettings,
+ selectSpecificTab: selectSpecificTab,
+ refreshCompleter: refreshCompleter
+
+init = ->
+ Commands.clearKeyMappingsAndSetDefaults()
+
+ if Settings.has("keyMappings")
+ Commands.parseCustomKeyMappings(Settings.get("keyMappings"))
+
+ populateValidFirstKeys()
+ populateSingleKeyCommands()
+ if shouldShowUpgradeMessage()
+ sendRequestToAllTabs({ name: "showUpgradeNotification", version: currentVersion })
+
+ # Ensure that openTabs is populated when Vimium is installed.
+ chrome.windows.getAll({ populate: true }, (windows) ->
+ for window in windows
+ for tab in window.tabs
+ updateOpenTabs(tab)
+ createScrollPositionHandler = ->
+ (response) -> updateScrollPosition(tab, response.scrollX, response.scrollY) if response?
+ chrome.tabs.sendRequest(tab.id, { name: "getScrollPosition" }, createScrollPositionHandler()))
+
+
+init()
+
+# Convenience function for development use.
+runTests = -> open(chrome.extension.getURL('test_harnesses/automated.html'))
diff --git a/background_scripts/main.js b/background_scripts/main.js
deleted file mode 100644
index 6e6a3978..00000000
--- a/background_scripts/main.js
+++ /dev/null
@@ -1,783 +0,0 @@
-var currentVersion = Utils.getCurrentVersion();
-
-var tabQueue = {}; // windowId -> Array
-var openTabs = {}; // tabId -> object with various tab properties
-var keyQueue = ""; // Queue of keys typed
-var validFirstKeys = {};
-var singleKeyCommands = [];
-var focusedFrame = null;
-var framesForTab = {};
-
-// Keys are either literal characters, or "named" - for example <a-b> (alt+b), <left> (left arrow) or <f12>
-// This regular expression captures two groups: the first is a named key, the second is the remainder of
-// the string.
-var namedKeyRegex = /^(<(?:[amc]-.|(?:[amc]-)?[a-z0-9]{2,5})>)(.*)$/;
-
-// Port handler mapping
-var portHandlers = {
- keyDown: handleKeyDown,
- settings: handleSettings,
- filterCompleter: filterCompleter
-};
-
-var sendRequestHandlers = {
- getCompletionKeys: getCompletionKeysRequest,
- getCurrentTabUrl: getCurrentTabUrl,
- openUrlInNewTab: openUrlInNewTab,
- openUrlInCurrentTab: openUrlInCurrentTab,
- openOptionsPageInNewTab: openOptionsPageInNewTab,
- registerFrame: registerFrame,
- frameFocused: handleFrameFocused,
- upgradeNotificationClosed: upgradeNotificationClosed,
- updateScrollPosition: handleUpdateScrollPosition,
- copyToClipboard: copyToClipboard,
- isEnabledForUrl: isEnabledForUrl,
- saveHelpDialogSettings: saveHelpDialogSettings,
- selectSpecificTab: selectSpecificTab,
- refreshCompleter: refreshCompleter
-};
-
-// Event handlers
-var selectionChangedHandlers = [];
-var tabLoadedHandlers = {}; // tabId -> function()
-
-var completionSources = {
- bookmarks: new BookmarkCompleter(),
- history: new HistoryCompleter(),
- domains: new DomainCompleter(),
- tabs: new TabCompleter()
-};
-
-var completers = {
- omni: new MultiCompleter([
- completionSources.bookmarks,
- completionSources.history,
- completionSources.domains]),
- bookmarks: new MultiCompleter([completionSources.bookmarks]),
- tabs: new MultiCompleter([completionSources.tabs])
-};
-
-chrome.extension.onConnect.addListener(function(port, name) {
- var senderTabId = port.sender.tab ? port.sender.tab.id : null;
- // If this is a tab we've been waiting to open, execute any "tab loaded" handlers, e.g. to restore
- // the tab's scroll position. Wait until domReady before doing this; otherwise operations like restoring
- // the scroll position will not be possible.
- if (port.name === "domReady" && senderTabId !== null) {
- if (tabLoadedHandlers[senderTabId]) {
- var toCall = tabLoadedHandlers[senderTabId];
- // Delete first to be sure there's no circular events.
- delete tabLoadedHandlers[senderTabId];
- toCall.call();
- }
-
- // domReady is the appropriate time to show the "vimium has been upgraded" message.
- // TODO: This might be broken on pages with frames.
- if (shouldShowUpgradeMessage())
- chrome.tabs.sendRequest(senderTabId, { name: "showUpgradeNotification", version: currentVersion });
- }
-
- if (portHandlers[port.name])
- port.onMessage.addListener(portHandlers[port.name]);
-});
-
-chrome.extension.onRequest.addListener(function (request, sender, sendResponse) {
- var senderTabId = sender.tab ? sender.tab.id : null;
- if (sendRequestHandlers[request.handler])
- sendResponse(sendRequestHandlers[request.handler](request, sender));
- // Ensure the sendResponse callback is freed.
- return false;
-});
-
-/*
- * Used by the content scripts to get their full URL. This is needed for URLs like "view-source:http:// .."
- * because window.location doesn't know anything about the Chrome-specific "view-source:".
- */
-function getCurrentTabUrl(request, sender) {
- return sender.tab.url;
-}
-
-/*
- * Checks the user's preferences in local storage to determine if Vimium is enabled for the given URL.
- */
-function isEnabledForUrl(request) {
- // excludedUrls are stored as a series of URL expressions separated by newlines.
- var excludedUrls = Settings.get("excludedUrls").split("\n");
- var isEnabled = true;
- for (var i = 0; i < excludedUrls.length; i++) {
- // The user can add "*" to the URL which means ".*"
- var regexp = new RegExp("^" + excludedUrls[i].replace(/\*/g, ".*") + "$");
- if (request.url.match(regexp))
- isEnabled = false;
- }
- return { isEnabledForUrl: isEnabled };
-}
-
-/*
- * Called by the popup UI. Strips leading/trailing whitespace and ignores empty strings.
- */
-function addExcludedUrl(url) {
- url = trim(url);
- if (url === "") { return; }
-
- var excludedUrls = Settings.get("excludedUrls");
- excludedUrls += "\n" + url;
- Settings.set("excludedUrls", excludedUrls);
-
- chrome.tabs.query({ windowId: chrome.windows.WINDOW_ID_CURRENT, active: true }, function(tabs) {
- updateActiveState(tabs[0].id);
- });
-}
-
-function saveHelpDialogSettings(request) {
- Settings.set("helpDialog_showAdvancedCommands", request.showAdvancedCommands);
-}
-
-function showHelp(callback, frameId) {
- chrome.tabs.getSelected(null, function(tab) {
- chrome.tabs.sendRequest(tab.id,
- { name: "toggleHelpDialog", dialogHtml: helpDialogHtml(), frameId:frameId });
- });
-}
-
-/*
- * Retrieves the help dialog HTML template from a file, and populates it with the latest keybindings.
- */
-function helpDialogHtml(showUnboundCommands, showCommandNames, customTitle) {
- var commandsToKey = {};
- for (var key in Commands.keyToCommandRegistry) {
- var command = Commands.keyToCommandRegistry[key].command;
- commandsToKey[command] = (commandsToKey[command] || []).concat(key);
- }
- var dialogHtml = fetchFileContents("help_dialog.html");
- for (var group in Commands.commandGroups)
- dialogHtml = dialogHtml.replace("{{" + group + "}}",
- helpDialogHtmlForCommandGroup(group, commandsToKey, Commands.availableCommands,
- showUnboundCommands, showCommandNames));
- dialogHtml = dialogHtml.replace("{{version}}", currentVersion);
- dialogHtml = dialogHtml.replace("{{title}}", customTitle || "Help");
- dialogHtml = dialogHtml.replace("{{showAdvancedCommands}}",
- Settings.get("helpDialog_showAdvancedCommands"));
- return dialogHtml;
-}
-
-/*
- * Generates HTML for a given set of commands. commandGroups are defined in commands.js
- */
-function helpDialogHtmlForCommandGroup(group, commandsToKey, availableCommands,
- showUnboundCommands, showCommandNames) {
- var html = [];
- for (var i = 0; i < Commands.commandGroups[group].length; i++) {
- var command = Commands.commandGroups[group][i];
- bindings = (commandsToKey[command] || [""]).join(", ");
- if (showUnboundCommands || commandsToKey[command]) {
- html.push(
- "<tr class='vimiumReset " +
- (Commands.advancedCommands.indexOf(command) >= 0 ? "advanced" : "") + "'>",
- "<td class='vimiumReset'>", Utils.escapeHtml(bindings), "</td>",
- "<td class='vimiumReset'>:</td><td class='vimiumReset'>", availableCommands[command].description);
-
- if (showCommandNames)
- html.push("<span class='vimiumReset commandName'>(" + command + ")</span>");
-
- html.push("</td></tr>");
- }
- }
- return html.join("\n");
-}
-
-/*
- * Fetches the contents of a file bundled with this extension.
- */
-function fetchFileContents(extensionFileName) {
- var req = new XMLHttpRequest();
- req.open("GET", chrome.extension.getURL(extensionFileName), false); // false => synchronous
- req.send();
- return req.responseText;
-}
-
-/**
- * Returns the keys that can complete a valid command given the current key queue.
- */
-function getCompletionKeysRequest(request) {
- return { name: "refreshCompletionKeys",
- completionKeys: generateCompletionKeys(),
- validFirstKeys: validFirstKeys
- };
-}
-
-/*
- * Opens the url in the current tab.
- */
- function openUrlInCurrentTab(request) {
- chrome.tabs.getSelected(null, function(tab) {
- chrome.tabs.update(tab.id, { url: Utils.convertToUrl(request.url) });
- });
- }
-
-/*
- * Opens request.url in new tab and switches to it if request.selected is true.
- */
-function openUrlInNewTab(request) {
- chrome.tabs.getSelected(null, function(tab) {
- chrome.tabs.create({ url: Utils.convertToUrl(request.url), index: tab.index + 1, selected: true });
- });
-}
-
-function openCopiedUrlInCurrentTab(request) { openUrlInCurrentTab({ url: Clipboard.paste() }); }
-
-function openCopiedUrlInNewTab(request) { openUrlInNewTab({ url: Clipboard.paste() }); }
-
-/*
- * Called when the user has clicked the close icon on the "Vimium has been updated" message.
- * We should now dismiss that message in all tabs.
- */
-function upgradeNotificationClosed(request) {
- Settings.set("previousVersion", currentVersion);
- sendRequestToAllTabs({ name: "hideUpgradeNotification" });
-}
-
-/*
- * Copies some data (request.data) to the clipboard.
- */
-function copyToClipboard(request) {
- Clipboard.copy(request.data);
-}
-
-/**
- * Selects the tab with the ID specified in request.id
- */
-function selectSpecificTab(request) {
- chrome.tabs.update(request.id, { selected: true });
-}
-
-/*
- * Used by the content scripts to get settings from the local storage.
- */
-function handleSettings(args, port) {
- if (args.operation == "get") {
- var value = Settings.get(args.key);
- port.postMessage({ key: args.key, value: value });
- }
- else { // operation == "set"
- Settings.set(args.key, args.value);
- }
-}
-
-function refreshCompleter(request) {
- completers[request.name].refresh();
-}
-
-function filterCompleter(args, port) {
- var queryTerms = args.query === "" ? [] : args.query.split(" ");
- completers[args.name].filter(queryTerms, function(results) {
- port.postMessage({ id: args.id, results: results });
- });
-}
-
-/*
- * Used by everyone to get settings from local storage.
- */
-function getSettingFromLocalStorage(setting) {
- if (localStorage[setting] !== "" && !localStorage[setting]) {
- return defaultSettings[setting];
- } else {
- return localStorage[setting];
- }
-}
-
-function getCurrentTimeInSeconds() { Math.floor((new Date()).getTime() / 1000); }
-
-chrome.tabs.onSelectionChanged.addListener(function(tabId, selectionInfo) {
- if (selectionChangedHandlers.length > 0) { selectionChangedHandlers.pop().call(); }
-});
-
-function repeatFunction(func, totalCount, currentCount, frameId) {
- if (currentCount < totalCount)
- func(function() { repeatFunction(func, totalCount, currentCount + 1, frameId); }, frameId);
-}
-
-// Start action functions
-function createTab(callback) {
- chrome.tabs.create({}, function(tab) { callback(); });
-}
-
-function nextTab(callback) { selectTab(callback, "next"); }
-function previousTab(callback) { selectTab(callback, "previous"); }
-function firstTab(callback) { selectTab(callback, "first"); }
-function lastTab(callback) { selectTab(callback, "last"); }
-
-/*
- * Selects a tab before or after the currently selected tab. Direction is either "next", "previous", "first" or "last".
- */
-function selectTab(callback, direction) {
- chrome.tabs.getAllInWindow(null, function(tabs) {
- if (tabs.length <= 1)
- return;
- chrome.tabs.getSelected(null, function(currentTab) {
- switch (direction) {
- case "next":
- toSelect = tabs[(currentTab.index + 1 + tabs.length) % tabs.length];
- break;
- case "previous":
- toSelect = tabs[(currentTab.index - 1 + tabs.length) % tabs.length];
- break;
- case "first":
- toSelect = tabs[0];
- break;
- case "last":
- toSelect = tabs[tabs.length - 1];
- break;
- }
- selectionChangedHandlers.push(callback);
- chrome.tabs.update(toSelect.id, { selected: true });
- });
- });
-}
-
-function removeTab(callback) {
- chrome.tabs.getSelected(null, function(tab) {
- chrome.tabs.remove(tab.id);
- // We can't just call the callback here because we actually need to wait
- // for the selection to change to consider this action done.
- selectionChangedHandlers.push(callback);
- });
-}
-
-function updateOpenTabs(tab) {
- openTabs[tab.id] = { url: tab.url, positionIndex: tab.index, windowId: tab.windowId };
- // Frames are recreated on refresh
- delete framesForTab[tab.id];
-}
-
-/* Updates the browserAction icon to indicated whether Vimium is enabled or disabled on the current page.
- * Also disables Vimium if it is currently enabled but should be disabled according to the url blacklist.
- * This lets you disable Vimium on a page without needing to reload.
- *
- * Three situations are considered:
- * 1. Active tab is disabled -> disable icon
- * 2. Active tab is enabled and should be enabled -> enable icon
- * 3. Active tab is enabled but should be disabled -> disable icon and disable vimium
- */
-function updateActiveState(tabId) {
- var enabledIcon = "icons/browser_action_enabled.png";
- var disabledIcon = "icons/browser_action_disabled.png";
- chrome.tabs.get(tabId, function(tab) {
- // Default to disabled state in case we can't connect to Vimium, primarily for the "New Tab" page.
- chrome.browserAction.setIcon({ path: disabledIcon });
- chrome.tabs.sendRequest(tabId, { name: "getActiveState" }, function(response) {
- var isCurrentlyEnabled = response !== undefined && response.enabled;
- var shouldBeEnabled = isEnabledForUrl({url: tab.url}).isEnabledForUrl;
-
- if (isCurrentlyEnabled) {
- if (shouldBeEnabled) {
- chrome.browserAction.setIcon({ path: enabledIcon });
- } else {
- chrome.browserAction.setIcon({ path: disabledIcon });
- chrome.tabs.sendRequest(tabId, { name: "disableVimium" });
- }
- } else {
- chrome.browserAction.setIcon({ path: disabledIcon });
- }
- });
- });
-}
-
-function handleUpdateScrollPosition(request, sender) {
- updateScrollPosition(sender.tab, request.scrollX, request.scrollY);
-}
-
-function updateScrollPosition(tab, scrollX, scrollY) {
- openTabs[tab.id].scrollX = scrollX;
- openTabs[tab.id].scrollY = scrollY;
-}
-
-
-chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
- if (changeInfo.status != "loading") { return; } // only do this once per URL change
- updateOpenTabs(tab);
- updateActiveState(tabId);
-});
-
-chrome.tabs.onAttached.addListener(function(tabId, attachedInfo) {
- // We should update all the tabs in the old window and the new window.
- if (openTabs[tabId]) {
- updatePositionsAndWindowsForAllTabsInWindow(openTabs[tabId].windowId);
- }
- updatePositionsAndWindowsForAllTabsInWindow(attachedInfo.newWindowId);
-});
-
-chrome.tabs.onMoved.addListener(function(tabId, moveInfo) {
- updatePositionsAndWindowsForAllTabsInWindow(moveInfo.windowId);
-});
-
-chrome.tabs.onRemoved.addListener(function(tabId) {
- var openTabInfo = openTabs[tabId];
- updatePositionsAndWindowsForAllTabsInWindow(openTabInfo.windowId);
-
- // If we restore chrome:// pages, they'll ignore Vimium keystrokes when they reappear.
- // Pretend they never existed and adjust tab indices accordingly.
- // Could possibly expand this into a blacklist in the future
- if (/^chrome[^:]*:\/\/.*/.test(openTabInfo.url)) {
- for (var i in tabQueue[openTabInfo.windowId]) {
- if (tabQueue[openTabInfo.windowId][i].positionIndex > openTabInfo.positionIndex)
- tabQueue[openTabInfo.windowId][i].positionIndex--;
- }
- return;
- }
-
- if (tabQueue[openTabInfo.windowId])
- tabQueue[openTabInfo.windowId].push(openTabInfo);
- else
- tabQueue[openTabInfo.windowId] = [openTabInfo];
-
- delete openTabs[tabId];
- delete framesForTab[tabId];
-});
-
-chrome.tabs.onActiveChanged.addListener(function(tabId, selectInfo) {
- updateActiveState(tabId);
-});
-
-chrome.windows.onRemoved.addListener(function(windowId) {
- delete tabQueue[windowId];
-});
-
-function restoreTab(callback) {
- // TODO(ilya): Should this be getLastFocused instead?
- chrome.windows.getCurrent(function(window) {
- if (tabQueue[window.id] && tabQueue[window.id].length > 0)
- {
- var tabQueueEntry = tabQueue[window.id].pop();
-
- // Clean out the tabQueue so we don't have unused windows laying about.
- if (tabQueue[window.id].length === 0)
- delete tabQueue[window.id];
-
- // We have to chain a few callbacks to set the appropriate scroll position. We can't just wait until the
- // tab is created because the content script is not available during the "loading" state. We need to
- // wait until that's over before we can call setScrollPosition.
- chrome.tabs.create({ url: tabQueueEntry.url, index: tabQueueEntry.positionIndex }, function(tab) {
- tabLoadedHandlers[tab.id] = function() {
- var scrollPort = chrome.tabs.sendRequest(tab.id, {
- name: "setScrollPosition",
- scrollX: tabQueueEntry.scrollX,
- scrollY: tabQueueEntry.scrollY
- });
- };
- callback();
- });
- }
- });
-}
-// End action functions
-
-function updatePositionsAndWindowsForAllTabsInWindow(windowId) {
- chrome.tabs.getAllInWindow(windowId, function (tabs) {
- for (var i = 0; i < tabs.length; i++) {
- var tab = tabs[i];
- var openTabInfo = openTabs[tab.id];
- if (openTabInfo) {
- openTabInfo.positionIndex = tab.index;
- openTabInfo.windowId = tab.windowId;
- }
- }
- });
-}
-
-function splitKeyIntoFirstAndSecond(key) {
- if (key.search(namedKeyRegex) === 0)
- return { first: RegExp.$1, second: RegExp.$2 };
- else
- return { first: key[0], second: key.slice(1) };
-}
-
-function getActualKeyStrokeLength(key) {
- if (key.search(namedKeyRegex) === 0)
- return 1 + getActualKeyStrokeLength(RegExp.$2);
- else
- return key.length;
-}
-
-function populateValidFirstKeys() {
- for (var key in Commands.keyToCommandRegistry)
- {
- if (getActualKeyStrokeLength(key) == 2)
- validFirstKeys[splitKeyIntoFirstAndSecond(key).first] = true;
- }
-}
-
-function populateSingleKeyCommands() {
- for (var key in Commands.keyToCommandRegistry)
- {
- if (getActualKeyStrokeLength(key) == 1)
- singleKeyCommands.push(key);
- }
-}
-
-function refreshCompletionKeysAfterMappingSave() {
- validFirstKeys = {};
- singleKeyCommands = [];
-
- populateValidFirstKeys();
- populateSingleKeyCommands();
-
- sendRequestToAllTabs(getCompletionKeysRequest());
-}
-
-/*
- * Generates a list of keys that can complete a valid command given the current key queue or the one passed
- * in.
- */
-function generateCompletionKeys(keysToCheck) {
- var splitHash = splitKeyQueue(keysToCheck || keyQueue);
- command = splitHash.command;
- count = splitHash.count;
-
- var completionKeys = singleKeyCommands.slice(0);
-
- if (getActualKeyStrokeLength(command) == 1)
- {
- for (var key in Commands.keyToCommandRegistry)
- {
- var splitKey = splitKeyIntoFirstAndSecond(key);
- if (splitKey.first == command)
- completionKeys.push(splitKey.second);
- }
- }
-
- return completionKeys;
-}
-
-function splitKeyQueue(queue) {
- var match = /([1-9][0-9]*)?(.*)/.exec(queue);
- var count = parseInt(match[1], 10);
- var command = match[2];
-
- return {count: count, command: command};
-}
-
-function handleKeyDown(request, port) {
- var key = request.keyChar;
- if (key == "<ESC>") {
- console.log("clearing keyQueue");
- keyQueue = "";
- }
- else {
- console.log("checking keyQueue: [", keyQueue + key, "]");
- keyQueue = checkKeyQueue(keyQueue + key, port.sender.tab.id, request.frameId);
- console.log("new KeyQueue: " + keyQueue);
- }
-}
-
-function checkKeyQueue(keysToCheck, tabId, frameId) {
- var refreshedCompletionKeys = false;
- var splitHash = splitKeyQueue(keysToCheck);
- command = splitHash.command;
- count = splitHash.count;
-
- if (command.length === 0) { return keysToCheck; }
- if (isNaN(count)) { count = 1; }
-
- if (Commands.keyToCommandRegistry[command]) {
- registryEntry = Commands.keyToCommandRegistry[command];
-
- if (!registryEntry.isBackgroundCommand) {
- chrome.tabs.sendRequest(tabId, {
- name: "executePageCommand",
- command: registryEntry.command,
- frameId: frameId,
- count: count,
- passCountToFunction: registryEntry.passCountToFunction,
- completionKeys: generateCompletionKeys("")
- });
- refreshedCompletionKeys = true;
- } else {
- if(registryEntry.passCountToFunction){
- this[registryEntry.command](count);
- } else {
- repeatFunction(this[registryEntry.command], count, 0, frameId);
- }
- }
-
- newKeyQueue = "";
- } else if (getActualKeyStrokeLength(command) > 1) {
- var splitKey = splitKeyIntoFirstAndSecond(command);
-
- // The second key might be a valid command by its self.
- if (Commands.keyToCommandRegistry[splitKey.second])
- newKeyQueue = checkKeyQueue(splitKey.second, tabId, frameId);
- else
- newKeyQueue = (validFirstKeys[splitKey.second] ? splitKey.second : "");
- } else {
- newKeyQueue = (validFirstKeys[command] ? count.toString() + command : "");
- }
-
- // If we haven't sent the completion keys piggybacked on executePageCommand,
- // send them by themselves.
- if (!refreshedCompletionKeys) {
- chrome.tabs.sendRequest(tabId, getCompletionKeysRequest(), null);
- }
-
- return newKeyQueue;
-}
-
-/*
- * Message all tabs. Args should be the arguments hash used by the Chrome sendRequest API.
- */
-function sendRequestToAllTabs(args) {
- chrome.windows.getAll({ populate: true }, function(windows) {
- for (var i = 0; i < windows.length; i++)
- for (var j = 0; j < windows[i].tabs.length; j++)
- chrome.tabs.sendRequest(windows[i].tabs[j].id, args, null);
- });
-}
-
-// Compares two version strings (e.g. "1.1" and "1.5") and returns
-// -1 if versionA is < versionB, 0 if they're equal, and 1 if versionA is > versionB.
-function compareVersions(versionA, versionB) {
- versionA = versionA.split(".");
- versionB = versionB.split(".");
- for (var i = 0; i < Math.max(versionA.length, versionB.length); i++) {
- var a = parseInt(versionA[i] || 0, 10);
- var b = parseInt(versionB[i] || 0, 10);
- if (a < b) return -1;
- else if (a > b) return 1;
- }
- return 0;
-}
-
-/*
- * Returns true if the current extension version is greater than the previously recorded version in
- * localStorage, and false otherwise.
- */
-function shouldShowUpgradeMessage() {
- // Avoid showing the upgrade notification when previousVersion is undefined, which is the case for new
- // installs.
- if (!Settings.get("previousVersion"))
- Settings.set("previousVersion", currentVersion);
- return compareVersions(currentVersion, Settings.get("previousVersion")) == 1;
-}
-
-function openOptionsPageInNewTab() {
- chrome.tabs.getSelected(null, function(tab) {
- chrome.tabs.create({ url: chrome.extension.getURL("options/options.html"), index: tab.index + 1 });
- });
-}
-
-function registerFrame(request, sender) {
- if (!framesForTab[sender.tab.id])
- framesForTab[sender.tab.id] = { frames: [] };
-
- if (request.is_top) {
- focusedFrame = request.frameId;
- framesForTab[sender.tab.id].total = request.total;
- }
-
- framesForTab[sender.tab.id].frames.push({ id: request.frameId, area: request.area });
-
- // We've seen all the frames. Time to focus the largest one.
- // NOTE: Disabled because it's buggy with iframes.
- // if (framesForTab[sender.tab.id].frames.length >= framesForTab[sender.tab.id].total)
- // focusLargestFrame(sender.tab.id);
-}
-
-function focusLargestFrame(tabId) {
- var mainFrameId = null;
- var mainFrameArea = 0;
-
- for (var i = 0; i < framesForTab[tabId].frames.length; i++) {
- var currentFrame = framesForTab[tabId].frames[i];
-
- if (currentFrame.area > mainFrameArea) {
- mainFrameId = currentFrame.id;
- mainFrameArea = currentFrame.area;
- }
- }
-
- chrome.tabs.sendRequest(tabId, { name: "focusFrame", frameId: mainFrameId, highlight: false });
-}
-
-function handleFrameFocused(request, sender) {
- focusedFrame = request.frameId;
-}
-
-function nextFrame(count) {
- chrome.tabs.getSelected(null, function(tab) {
- var frames = framesForTab[tab.id].frames;
- var curr_index = getCurrFrameIndex(frames);
-
- // TODO: Skip the "top" frame (which doesn't actually have a <frame> tag),
- // since it exists only to contain the other frames.
- var new_index = (curr_index + count) % frames.length;
-
- chrome.tabs.sendRequest(tab.id, { name: "focusFrame", frameId: frames[new_index].id, highlight: true });
- });
-}
-
-function getCurrFrameIndex(frames) {
- var index;
- for (index=0; index < frames.length; index++) {
- if (frames[index].id == focusedFrame)
- break;
- }
- return index;
-}
-
-/*
- * Convenience function for trimming leading and trailing whitespace.
- */
-function trim(str) {
- return str.replace(/^\s*/, "").replace(/\s*$/, "");
-}
-
-function init() {
- Commands.clearKeyMappingsAndSetDefaults();
-
- if (Settings.has("keyMappings"))
- Commands.parseCustomKeyMappings(Settings.get("keyMappings"));
-
- // In version 1.22, we changed the mapping for "d" and "u" to be scroll page down/up instead of close
- // and restore tab. For existing users, we want to preserve existing behavior for them by adding some
- // custom key mappings on their behalf.
- if (Settings.get("previousVersion") == "1.21") {
- var customKeyMappings = Settings.get("keyMappings") || "";
- if ((Commands.keyToCommandRegistry["d"] || {}).command == "scrollPageDown")
- customKeyMappings += "\nmap d removeTab";
- if ((Commands.keyToCommandRegistry["u"] || {}).command == "scrollPageUp")
- customKeyMappings += "\nmap u restoreTab";
- if (customKeyMappings !== "") {
- Settings.set("keyMappings", customKeyMappings);
- Commands.parseCustomKeyMappings(customKeyMappings);
- }
- }
-
- populateValidFirstKeys();
- populateSingleKeyCommands();
- if (shouldShowUpgradeMessage())
- sendRequestToAllTabs({ name: "showUpgradeNotification", version: currentVersion });
-
- // Ensure that openTabs is populated when Vimium is installed.
- chrome.windows.getAll({ populate: true }, function(windows) {
- for (var i in windows) {
- for (var j in windows[i].tabs) {
- var tab = windows[i].tabs[j];
- updateOpenTabs(tab);
- chrome.tabs.sendRequest(tab.id, { name: "getScrollPosition" }, function() {
- return function(response) {
- if (response === undefined)
- return;
- updateScrollPosition(tab, response.scrollX, response.scrollY);
- };
- }());
- }
- }
- });
-}
-init();
-
-/**
- * Convenience function for development use.
- */
-function runTests() {
- open(chrome.extension.getURL('test_harnesses/automated.html'));
-}
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index 3f73919b..577aa0c5 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -694,7 +694,8 @@ selectFoundInputElement = ->
# instead. however, since the last focused element might not be the one currently pointed to by find (e.g.
# the current one might be disabled and therefore unable to receive focus), we use the approximate
# heuristic of checking that the last anchor node is an ancestor of our element.
- if (findModeQueryHasResults && DomUtils.isSelectable(document.activeElement) &&
+ if (findModeQueryHasResults && document.activeElement &&
+ 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
@@ -732,7 +733,8 @@ findAndFocus = (backwards) ->
# if we have found an input element via 'n', pressing <esc> immediately afterwards sends us into insert
# mode
- elementCanTakeInput = DomUtils.isSelectable(document.activeElement) &&
+ elementCanTakeInput = document.activeElement &&
+ DomUtils.isSelectable(document.activeElement) &&
isDOMDescendant(findModeAnchorNode, document.activeElement)
if (elementCanTakeInput)
handlerStack.push({
@@ -864,7 +866,7 @@ exitFindMode = ->
findMode = false
HUD.hide()
-showHelpDialog = (html, fid) ->
+window.showHelpDialog = (html, fid) ->
return if (isShowingHelpDialog || !document.body || fid != frameId)
isShowingHelpDialog = true
container = document.createElement("div")
@@ -875,13 +877,41 @@ showHelpDialog = (html, fid) ->
container.innerHTML = html
container.getElementsByClassName("closeButton")[0].addEventListener("click", hideHelpDialog, false)
+
+ VimiumHelpDialog =
+ # This setting is pulled out of local storage. It's false by default.
+ getShowAdvancedCommands: (callback) ->
+ chrome.extension.sendRequest({ handler: "getShowAdvancedCommands"}, callback)
+
+ init: () ->
+ this.dialogElement = document.getElementById("vimiumHelpDialog")
+ this.dialogElement.getElementsByClassName("toggleAdvancedCommands")[0].addEventListener("click",
+ VimiumHelpDialog.toggleAdvancedCommands, false)
+ this.dialogElement.style.maxHeight = window.innerHeight - 80
+ this.getShowAdvancedCommands(this.showAdvancedCommands)
+
+ #
+ # Advanced commands are hidden by default so they don't overwhelm new and casual users.
+ #
+ toggleAdvancedCommands: (event) ->
+ event.preventDefault()
+ VimiumHelpDialog.getShowAdvancedCommands((value) ->
+ VimiumHelpDialog.showAdvancedCommands(!value)
+ chrome.extension.sendRequest({ handler: "saveHelpDialogSettings", showAdvancedCommands: !value }))
+
+ showAdvancedCommands: (visible) ->
+ VimiumHelpDialog.dialogElement.getElementsByClassName("toggleAdvancedCommands")[0].innerHTML =
+ if visible then "Hide advanced commands" else "Show advanced commands"
+ advancedEls = VimiumHelpDialog.dialogElement.getElementsByClassName("advanced")
+ for el in advancedEls
+ el.style.display = if visible then "table-row" else "none"
+
+ VimiumHelpDialog.init()
+
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
diff --git a/help_dialog.html b/help_dialog.html
index 5e188406..2a69ea03 100644
--- a/help_dialog.html
+++ b/help_dialog.html
@@ -48,40 +48,4 @@
<span class="vimiumReset">Version {{version}}</span><br/>
</div>
</div>
-
- <script>
- VimiumHelpDialog = {
- // This setting is pulled out of local storage. It's false by default.
- advancedCommandsVisible: {{showAdvancedCommands}},
-
- init: function() {
- this.dialogElement = document.getElementById("vimiumHelpDialog");
- this.dialogElement.getElementsByClassName("toggleAdvancedCommands")[0].addEventListener("click",
- VimiumHelpDialog.toggleAdvancedCommands, false);
- this.dialogElement.style.maxHeight = window.innerHeight - 80;
- this.showAdvancedCommands(this.advancedCommandsVisible);
- },
-
- /*
- * Advanced commands are hidden by default so they don't overwhelm new and casual users.
- */
- toggleAdvancedCommands: function(event) {
- event.preventDefault();
- VimiumHelpDialog.advancedCommandsVisible = !VimiumHelpDialog.advancedCommandsVisible;
- chrome.extension.sendRequest({ handler: "saveHelpDialogSettings",
- showAdvancedCommands: VimiumHelpDialog.advancedCommandsVisible });
- VimiumHelpDialog.showAdvancedCommands(VimiumHelpDialog.advancedCommandsVisible);
- },
-
- showAdvancedCommands: function(visible) {
- VimiumHelpDialog.dialogElement.getElementsByClassName("toggleAdvancedCommands")[0].innerHTML =
- visible ? "Hide advanced commands" : "Show advanced commands";
- var advanced = VimiumHelpDialog.dialogElement.getElementsByClassName("advanced");
- for (var i = 0; i < advanced.length; i++)
- advanced[i].style.display = (visible ? "table-row" : "none");
- }
- };
-
- VimiumHelpDialog.init();
- </script>
</div>
diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee
index 57930f80..d4a4d379 100644
--- a/lib/dom_utils.coffee
+++ b/lib/dom_utils.coffee
@@ -128,8 +128,8 @@ DomUtils =
flashEl.style.top = rect.top + window.scrollY + "px"
flashEl.style.width = rect.width + "px"
flashEl.style.height = rect.height + "px"
- document.body.appendChild(flashEl)
- setTimeout((-> flashEl.parentNode.removeChild(flashEl)), 400)
+ document.documentElement.appendChild(flashEl)
+ setTimeout((-> DomUtils.removeElement flashEl), 400)
root = exports ? window
root.DomUtils = DomUtils
diff --git a/options/options.coffee b/options/options.coffee
index 9f43defd..2b06e5f7 100644
--- a/options/options.coffee
+++ b/options/options.coffee
@@ -28,6 +28,8 @@ document.addEventListener "DOMContentLoaded", ->
document.getElementById("restoreSettings").addEventListener "click", restoreToDefaults
document.getElementById("saveOptions").addEventListener "click", saveOptions
+window.onbeforeunload = -> "You have unsaved changes to options." unless $("saveOptions").disabled
+
onOptionKeyup = (event) ->
if (event.target.getAttribute("type") isnt "checkbox" and
event.target.getAttribute("savedValue") isnt event.target.value)
diff --git a/options/options.html b/options/options.html
index 4a616328..089de394 100644
--- a/options/options.html
+++ b/options/options.html
@@ -79,7 +79,7 @@
/* Horizontal resizing is pretty screwy-looking. */
resize: vertical;
}
- table {
+ table#options{
width: 100%;
font-size: 14px;
position: relative;
@@ -179,7 +179,7 @@
<body>
<div id="wrapper">
<header>Vimium options</header>
- <table>
+ <table id="options">
<tr>
<td class="caption">Scroll step size</td>
<td>