diff options
| author | Jez Ng | 2012-07-26 02:04:31 -0700 |
|---|---|---|
| committer | Jez Ng | 2012-07-26 02:14:00 -0700 |
| commit | 8c60e25b52933fadbf45da891bfdee91400ede68 (patch) | |
| tree | c54489ac705584e85d3753feeb095f806c44bf8d | |
| parent | 73a73ac467d14be8f9dd4a635f5a84cbc95fb630 (diff) | |
| download | vimium-8c60e25b52933fadbf45da891bfdee91400ede68.tar.bz2 | |
Upgrade to manifest version 2.
I use the beta channel by default, and it no longer allows me to run
development builds with the old manifest version.
| -rw-r--r-- | background_page.html | 822 | ||||
| -rw-r--r-- | background_scripts/main.js | 810 | ||||
| -rw-r--r-- | manifest.json | 12 | ||||
| -rw-r--r-- | options.html | 114 | ||||
| -rw-r--r-- | options.js | 109 |
5 files changed, 932 insertions, 935 deletions
diff --git a/background_page.html b/background_page.html deleted file mode 100644 index 8215b389..00000000 --- a/background_page.html +++ /dev/null @@ -1,822 +0,0 @@ -<html> -<head> -<script type="text/javascript" src="lib/utils.js"></script> -<script type="text/javascript" src="background_scripts/commands.js"></script> -<script type="text/javascript" src="lib/clipboard.js"></script> -<script type="text/javascript" src="background_scripts/settings.js"></script> -<script type="text/javascript" src="background_scripts/completion.js"></script> -<script type="text/javascript" charset="utf-8"> - 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, - returnScrollPosition: handleReturnScrollPosition, - getCurrentTabUrl: getCurrentTabUrl, - settings: handleSettings, - filterCompleter: filterCompleter, - }; - - var sendRequestHandlers = { - getCompletionKeys: getCompletionKeysRequest, - getLinkHintCss: getLinkHintCss, - 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 getScrollPositionHandlers = {}; // tabId -> function(tab, scrollX, scrollY); - 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)); - }); - - function handleReturnScrollPosition(args) { - if (getScrollPositionHandlers[args.currentTab.id]) { - // Delete first to be sure there's no circular events. - var toCall = getScrollPositionHandlers[args.currentTab.id]; - delete getScrollPositionHandlers[args.currentTab.id]; - toCall(args.currentTab, args.scrollX, args.scrollY); - } - } - - /* - * 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(args, port) { - var returnPort = chrome.tabs.connect(port.tab.id, { name: "returnCurrentTabUrl" }); - returnPort.postMessage({ url: port.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: "showHelpDialog", 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() }); } - - /* - * Returns the user-provided CSS overrides. - */ - function getLinkHintCss(request) { - return { linkHintCss: (Settings.get("userDefinedLinkHintCss") || "") }; - } - - /* - * 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); - } - - // Returns the scroll coordinates of the given tab. Pass in a callback of the form: - // function(tab, scrollX, scrollY) { .. } - function getScrollPosition(tab, callback) { - getScrollPositionHandlers[tab.id] = callback; - var scrollPort = chrome.tabs.connect(tab.id, { name: "getScrollPosition" }); - scrollPort.postMessage({currentTab: tab}); - } - - // 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) { - // TODO(philc): Re-enable once we've restyled the browser action icon. - return; - var enabledIcon = "icons/icon48.png"; - var disabledIcon = "icons/icon48disabled.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. - // TODO(philc): Re-enable once we've restyled the browser action icon. - // chrome.browserAction.setIcon({ path: disabledIcon }); - var returnPort = chrome.tabs.connect(tabId, { name: "getActiveState" }); - returnPort.onMessage.addListener(function(response) { - var isCurrentlyEnabled = 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.connect(tabId, { name: "disableVimium" }).postMessage(); - } - } else { - chrome.browserAction.setIcon({ path: disabledIcon }); - } - }); - returnPort.postMessage(); - }); - } - - 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.connect(tab.id, {name: "setScrollPosition"}); - scrollPort.postMessage({ 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]); - 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.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) { - var port = chrome.tabs.connect(tabId, { name: "executePageCommand" }); - port.postMessage({ 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); - var b = parseInt(versionB[i] || 0); - 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.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); - getScrollPosition(tab, function(tab, scrollX, scrollY) { - // Not using the tab defined in the for loop because - // it might have changed by the time this callback is activated. - updateScrollPosition(tab, scrollX, scrollY); - }); - } - } - }); - } - init(); - - /** - * Convenience function for development use. - */ - function runTests() { - open(chrome.extension.getURL('test_harnesses/automated.html')); - } - -</script> -</head> -</html> diff --git a/background_scripts/main.js b/background_scripts/main.js new file mode 100644 index 00000000..49790b87 --- /dev/null +++ b/background_scripts/main.js @@ -0,0 +1,810 @@ +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, + returnScrollPosition: handleReturnScrollPosition, + getCurrentTabUrl: getCurrentTabUrl, + settings: handleSettings, + filterCompleter: filterCompleter +}; + +var sendRequestHandlers = { + getCompletionKeys: getCompletionKeysRequest, + getLinkHintCss: getLinkHintCss, + 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 getScrollPositionHandlers = {}; // tabId -> function(tab, scrollX, scrollY); +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)); +}); + +function handleReturnScrollPosition(args) { + if (getScrollPositionHandlers[args.currentTab.id]) { + // Delete first to be sure there's no circular events. + var toCall = getScrollPositionHandlers[args.currentTab.id]; + delete getScrollPositionHandlers[args.currentTab.id]; + toCall(args.currentTab, args.scrollX, args.scrollY); + } +} + +/* + * 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(args, port) { + var returnPort = chrome.tabs.connect(port.sender.tab.id, { name: "returnCurrentTabUrl" }); + returnPort.postMessage({ url: port.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: "showHelpDialog", 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() }); } + +/* + * Returns the user-provided CSS overrides. + */ +function getLinkHintCss(request) { + return { linkHintCss: (Settings.get("userDefinedLinkHintCss") || "") }; +} + +/* + * 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); +} + +// Returns the scroll coordinates of the given tab. Pass in a callback of the form: +// function(tab, scrollX, scrollY) { .. } +function getScrollPosition(tab, callback) { + getScrollPositionHandlers[tab.id] = callback; + var scrollPort = chrome.tabs.connect(tab.id, { name: "getScrollPosition" }); + scrollPort.postMessage({currentTab: tab}); +} + +// 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) { + // TODO(philc): Re-enable once we've restyled the browser action icon. + return; + var enabledIcon = "icons/icon48.png"; + var disabledIcon = "icons/icon48disabled.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. + // TODO(philc): Re-enable once we've restyled the browser action icon. + // chrome.browserAction.setIcon({ path: disabledIcon }); + var returnPort = chrome.tabs.connect(tabId, { name: "getActiveState" }); + returnPort.onMessage.addListener(function(response) { + var isCurrentlyEnabled = 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.connect(tabId, { name: "disableVimium" }).postMessage(); + } + } else { + chrome.browserAction.setIcon({ path: disabledIcon }); + } + }); + returnPort.postMessage(); + }); +} + +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.connect(tab.id, {name: "setScrollPosition"}); + scrollPort.postMessage({ 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) { + var port = chrome.tabs.connect(tabId, { name: "executePageCommand" }); + port.postMessage({ 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.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); + getScrollPosition(tab, function(tab, scrollX, scrollY) { + // Not using the tab defined in the for loop because + // it might have changed by the time this callback is activated. + updateScrollPosition(tab, scrollX, scrollY); + }); + } + } + }); +} +init(); + +/** + * Convenience function for development use. + */ +function runTests() { + open(chrome.extension.getURL('test_harnesses/automated.html')); +} diff --git a/manifest.json b/manifest.json index ed27c157..5c451700 100644 --- a/manifest.json +++ b/manifest.json @@ -1,11 +1,21 @@ { + "manifest_version": 2, "name": "Vimium", "version": "1.37", "description": "The Hacker's Browser. Vimium provides keyboard shortcuts for navigation and control in the spirit of Vim.", "icons": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" }, - "background_page": "background_page.html", + "background": { + "scripts": [ + "lib/utils.js", + "background_scripts/commands.js", + "lib/clipboard.js", + "background_scripts/settings.js", + "background_scripts/completion.js", + "background_scripts/main.js" + ] + }, "options_page": "options.html", "permissions": [ "tabs", diff --git a/options.html b/options.html index f4b17997..52dbdef2 100644 --- a/options.html +++ b/options.html @@ -84,121 +84,11 @@ </style> <link rel="stylesheet" type="text/css" href="vimium.css" /> - <script type="text/javascript"> - $ = function(id) { return document.getElementById(id); }; - var bgSettings = chrome.extension.getBackgroundPage().Settings; - - var editableFields = ["scrollStepSize", "excludedUrls", "linkHintCharacters", "userDefinedLinkHintCss", - "keyMappings", "filterLinkHints", "previousPatterns", "nextPatterns", "hideHud"]; - - var canBeEmptyFields = ["excludedUrls", "keyMappings", "userDefinedLinkHintCss"]; - - var postSaveHooks = { - keyMappings: function (value) { - commands = chrome.extension.getBackgroundPage().Commands; - commands.clearKeyMappingsAndSetDefaults(); - commands.parseCustomKeyMappings(value); - chrome.extension.getBackgroundPage().refreshCompletionKeysAfterMappingSave(); - } - }; - - function initializeOptions() { - populateOptions(); - - for (var i = 0; i < editableFields.length; i++) { - $(editableFields[i]).addEventListener("keyup", onOptionKeyup, false); - $(editableFields[i]).addEventListener("change", enableSaveButton, false); - $(editableFields[i]).addEventListener("change", onDataLoaded, false); - } - - $("advancedOptions").addEventListener("click", openAdvancedOptions, false); - $("showCommands").addEventListener("click", function () { - showHelpDialog( - chrome.extension.getBackgroundPage().helpDialogHtml(true, true, "Command Listing"), frameId); - }, false); - } - - function onOptionKeyup(event) { - if (event.target.getAttribute("type") !== "checkbox" && - event.target.getAttribute("savedValue") != event.target.value) - enableSaveButton(); - } - - function onDataLoaded() { - $("linkHintCharacters").readOnly = $("filterLinkHints").checked; - } - - function enableSaveButton() { $("saveOptions").removeAttribute("disabled"); } - - // Saves options to localStorage. - function saveOptions() { - // If the value is unchanged from the default, delete the preference from localStorage; this gives us - // the freedom to change the defaults in the future. - for (var i = 0; i < editableFields.length; i++) { - var fieldName = editableFields[i]; - var field = $(fieldName); - - var fieldValue; - if (field.getAttribute("type") == "checkbox") { - fieldValue = field.checked; - } else { - fieldValue = field.value.trim(); - field.value = fieldValue; - } - - // If it's empty and not a field that we allow to be empty, restore to the default value - if (!fieldValue && canBeEmptyFields.indexOf(fieldName) == -1) { - bgSettings.clear(fieldName); - fieldValue = bgSettings.get(fieldName); - } else - bgSettings.set(fieldName, fieldValue); - - $(fieldName).value = fieldValue; - $(fieldName).setAttribute("savedValue", fieldValue); - - if (postSaveHooks[fieldName]) { postSaveHooks[fieldName](fieldValue); } - } - $("saveOptions").disabled = true; - } - - // Restores select box state to saved value from localStorage. - function populateOptions() { - for (var i = 0; i < editableFields.length; i++) { - var val = bgSettings.get(editableFields[i]) || ""; - setFieldValue($(editableFields[i]), val); - } - onDataLoaded(); - } - - function restoreToDefaults() { - for (var i = 0; i < editableFields.length; i++) { - var val = bgSettings.defaults[editableFields[i]] || ""; - setFieldValue($(editableFields[i]), val); - } - onDataLoaded(); - enableSaveButton(); - } - - function setFieldValue(field, value) { - if (field.getAttribute('type') == 'checkbox') - field.checked = value; - else { - field.value = value; - field.setAttribute("savedValue", value); - } - } - - function openAdvancedOptions(event) { - var elements = document.getElementsByClassName("advancedOption"); - for (var i = 0; i < elements.length; i++) - elements[i].style.display = (elements[i].style.display == "table-row") ? "none" : "table-row"; - event.preventDefault(); - } - </script> + <script type="text/javascript" src="options.js"></script> </head> - <body onload="initializeOptions()"> + <body> <h1>Vimium - Options</h1> <table style="position:relative"> <tr> diff --git a/options.js b/options.js new file mode 100644 index 00000000..ff5427a8 --- /dev/null +++ b/options.js @@ -0,0 +1,109 @@ +$ = function(id) { return document.getElementById(id); }; +var bgSettings = chrome.extension.getBackgroundPage().Settings; + +var editableFields = ["scrollStepSize", "excludedUrls", "linkHintCharacters", "userDefinedLinkHintCss", + "keyMappings", "filterLinkHints", "previousPatterns", "nextPatterns", "hideHud"]; + +var canBeEmptyFields = ["excludedUrls", "keyMappings", "userDefinedLinkHintCss"]; + +var postSaveHooks = { + keyMappings: function (value) { + commands = chrome.extension.getBackgroundPage().Commands; + commands.clearKeyMappingsAndSetDefaults(); + commands.parseCustomKeyMappings(value); + chrome.extension.getBackgroundPage().refreshCompletionKeysAfterMappingSave(); + } +}; + +document.addEventListener("DOMContentLoaded", function() { + populateOptions(); + + for (var i = 0; i < editableFields.length; i++) { + $(editableFields[i]).addEventListener("keyup", onOptionKeyup, false); + $(editableFields[i]).addEventListener("change", enableSaveButton, false); + $(editableFields[i]).addEventListener("change", onDataLoaded, false); + } + + $("advancedOptions").addEventListener("click", openAdvancedOptions, false); + $("showCommands").addEventListener("click", function () { + showHelpDialog( + chrome.extension.getBackgroundPage().helpDialogHtml(true, true, "Command Listing"), frameId); + }, false); +}); + +function onOptionKeyup(event) { + if (event.target.getAttribute("type") !== "checkbox" && + event.target.getAttribute("savedValue") != event.target.value) + enableSaveButton(); +} + +function onDataLoaded() { + $("linkHintCharacters").readOnly = $("filterLinkHints").checked; +} + +function enableSaveButton() { $("saveOptions").removeAttribute("disabled"); } + +// Saves options to localStorage. +function saveOptions() { + // If the value is unchanged from the default, delete the preference from localStorage; this gives us + // the freedom to change the defaults in the future. + for (var i = 0; i < editableFields.length; i++) { + var fieldName = editableFields[i]; + var field = $(fieldName); + + var fieldValue; + if (field.getAttribute("type") == "checkbox") { + fieldValue = field.checked; + } else { + fieldValue = field.value.trim(); + field.value = fieldValue; + } + + // If it's empty and not a field that we allow to be empty, restore to the default value + if (!fieldValue && canBeEmptyFields.indexOf(fieldName) == -1) { + bgSettings.clear(fieldName); + fieldValue = bgSettings.get(fieldName); + } else + bgSettings.set(fieldName, fieldValue); + + $(fieldName).value = fieldValue; + $(fieldName).setAttribute("savedValue", fieldValue); + + if (postSaveHooks[fieldName]) { postSaveHooks[fieldName](fieldValue); } + } + $("saveOptions").disabled = true; +} + +// Restores select box state to saved value from localStorage. +function populateOptions() { + for (var i = 0; i < editableFields.length; i++) { + var val = bgSettings.get(editableFields[i]) || ""; + setFieldValue($(editableFields[i]), val); + } + onDataLoaded(); +} + +function restoreToDefaults() { + for (var i = 0; i < editableFields.length; i++) { + var val = bgSettings.defaults[editableFields[i]] || ""; + setFieldValue($(editableFields[i]), val); + } + onDataLoaded(); + enableSaveButton(); +} + +function setFieldValue(field, value) { + if (field.getAttribute('type') == 'checkbox') + field.checked = value; + else { + field.value = value; + field.setAttribute("savedValue", value); + } +} + +function openAdvancedOptions(event) { + var elements = document.getElementsByClassName("advancedOption"); + for (var i = 0; i < elements.length; i++) + elements[i].style.display = (elements[i].style.display == "table-row") ? "none" : "table-row"; + event.preventDefault(); +} |
