diff options
| -rw-r--r-- | .gitignore | 15 | ||||
| -rw-r--r-- | Cakefile | 2 | ||||
| -rw-r--r-- | background_scripts/main.coffee | 610 | ||||
| -rw-r--r-- | background_scripts/main.js | 783 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 42 | ||||
| -rw-r--r-- | help_dialog.html | 36 | ||||
| -rw-r--r-- | lib/dom_utils.coffee | 4 | ||||
| -rw-r--r-- | options/options.coffee | 2 | ||||
| -rw-r--r-- | options/options.html | 4 |
9 files changed, 655 insertions, 843 deletions
@@ -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 @@ -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> |
