From 5885bc779bed46dfcff4b1e82968151448569f9f Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Tue, 19 Aug 2014 18:15:13 +0100 Subject: [PATCH 1/4] Move the vomnibar to an iframe --- .gitignore | 2 + background_scripts/main.coffee | 39 ++-- content_scripts/vimium.css | 124 +---------- content_scripts/vimium_frontend.coffee | 2 + content_scripts/vomnibar.coffee | 273 ++++--------------------- manifest.json | 5 +- pages/vomnibar.coffee | 266 ++++++++++++++++++++++++ pages/vomnibar.css | 129 ++++++++++++ pages/vomnibar.html | 21 ++ 9 files changed, 499 insertions(+), 362 deletions(-) create mode 100644 pages/vomnibar.coffee create mode 100644 pages/vomnibar.css create mode 100644 pages/vomnibar.html diff --git a/.gitignore b/.gitignore index 9df0d5596..2595878c1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ *.sublime* node_modules/* dist +jscoverage.json +tags diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index dda1beaef..49417d7ac 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -119,7 +119,7 @@ root.addExcludedUrl = (url) -> continue # And just keep everything else. newExcludedUrls.push(spec) - + Settings.set("excludedUrls", newExcludedUrls.join("\n")) chrome.tabs.query({ windowId: chrome.windows.WINDOW_ID_CURRENT, active: true }, @@ -635,6 +635,12 @@ getCurrFrameIndex = (frames) -> return i if frames[i].id == focusedFrame frames.length + 1 +# Send message back to the tab unchanged. +# Frames in the same tab can use this to communicate securely. +echo = (request, sender) -> + delete request.handler # No need to send this information + chrome.tabs.sendMessage(sender.tab.id, request) + # Port handler mapping portHandlers = keyDown: handleKeyDown, @@ -642,23 +648,24 @@ portHandlers = filterCompleter: filterCompleter sendRequestHandlers = - getCompletionKeys: getCompletionKeysRequest, - getCurrentTabUrl: getCurrentTabUrl, - openUrlInNewTab: openUrlInNewTab, - openUrlInIncognito: openUrlInIncognito, - openUrlInCurrentTab: openUrlInCurrentTab, - openOptionsPageInNewTab: openOptionsPageInNewTab, - registerFrame: registerFrame, - frameFocused: handleFrameFocused, - upgradeNotificationClosed: upgradeNotificationClosed, - updateScrollPosition: handleUpdateScrollPosition, - copyToClipboard: copyToClipboard, - isEnabledForUrl: isEnabledForUrl, - saveHelpDialogSettings: saveHelpDialogSettings, - selectSpecificTab: selectSpecificTab, + getCompletionKeys: getCompletionKeysRequest + getCurrentTabUrl: getCurrentTabUrl + openUrlInNewTab: openUrlInNewTab + openUrlInIncognito: openUrlInIncognito + openUrlInCurrentTab: openUrlInCurrentTab + openOptionsPageInNewTab: openOptionsPageInNewTab + registerFrame: registerFrame + frameFocused: handleFrameFocused + upgradeNotificationClosed: upgradeNotificationClosed + updateScrollPosition: handleUpdateScrollPosition + copyToClipboard: copyToClipboard + isEnabledForUrl: isEnabledForUrl + saveHelpDialogSettings: saveHelpDialogSettings + selectSpecificTab: selectSpecificTab refreshCompleter: refreshCompleter - createMark: Marks.create.bind(Marks), + createMark: Marks.create.bind(Marks) gotoMark: Marks.goto.bind(Marks) + echo: echo # Convenience function for development use. window.runTests = -> open(chrome.runtime.getURL('tests/dom_tests/dom_tests.html')) diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css index 7998fe5cc..fd613b315 100644 --- a/content_scripts/vimium.css +++ b/content_scripts/vimium.css @@ -266,136 +266,30 @@ div.vimiumHUD a.close-button:hover { body.vimiumFindMode ::selection { background: #ff9632; -}; +} -/* Vomnibar CSS */ +/* Vomnibar Frame CSS */ -#vomnibar ol, #vomnibar ul { - list-style: none; - display: block; -} +iframe.vomnibarFrame { + background-color: transparent; + padding: 0px; + overflow: hidden; -#vomnibar { display: block; position: fixed; - width: 80%; + width: calc(80% + 20px); /* same adjustment as in pages/vomnibar.coffee */ min-width: 400px; + height: calc(100% - 70px); top: 70px; left: 50%; margin: 0 0 0 -40%; + border: none; font-family: sans-serif; - background: #F1F1F1; - text-align: left; - border-radius: 4px; - box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.8); - border: 1px solid #aaa; /* One less than hint markers and the help dialog. */ z-index: 99999996; } -#vomnibar input { - color: #000; - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; - font-size: 20px; - height: 34px; - margin-bottom: 0; - padding: 4px; - background-color: white; - border-radius: 3px; - border: 1px solid #E8E8E8; - box-shadow: #444 0px 0px 1px; - width: 100%; - outline: none; - box-sizing: border-box; -} - -#vomnibar .vomnibarSearchArea { - display: block; - padding: 10px; - background-color: #F1F1F1; - border-radius: 4px 4px 0 0; - border-bottom: 1px solid #C6C9CE; -} - -#vomnibar ul { - background-color: white; - border-radius: 0 0 4px 4px; - list-style: none; - padding: 10px 0; - padding-top: 0; -} - -#vomnibar li { - border-bottom: 1px solid #ddd; - line-height: 1.1em; - padding: 7px 10px; - font-size: 16px; - color: black; - position: relative; - display: list-item; - margin: auto; -} - -#vomnibar li:last-of-type { - border-bottom: none; -} - -#vomnibar li .vomnibarTopHalf, #vomnibar li .vomnibarBottomHalf { - display: block; - overflow: hidden; -} - -#vomnibar li .vomnibarBottomHalf { - font-size: 15px; - margin-top: 3px; - padding: 2px 0; -} - -#vomnibar li .vomnibarSource { - color: #777; - margin-right: 4px; -} -#vomnibar li .vomnibarRelevancy { - position: absolute; - right: 0; - top: 0; - padding: 5px; - background-color: white; - color: black; - font-family: monospace; - width: 100px; - overflow: hidden; -} - -#vomnibar li .vomnibarUrl { - white-space: nowrap; - color: #224684; -} - -#vomnibar li .vomnibarMatch { - font-weight: bold; - color: black; -} - -#vomnibar li em, #vomnibar li .vomnibarTitle { - color: black; - margin-left: 4px; - font-weight: normal; -} -#vomnibar li em { font-style: italic; } -#vomnibar li em .vomnibarMatch, #vomnibar li .vomnibarTitle .vomnibarMatch { - color: #333; - text-decoration: underline; -} - -#vomnibar li.vomnibarSelected { - background-color: #BBCEE9; - font-weight: normal; -} - - - div#vimiumFlash { box-shadow: 0px 0px 4px 2px #4183C4; padding: 1px; diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 137b9d1aa..0a835de9e 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -121,6 +121,8 @@ initializePreDomReady = -> getActiveState: -> { enabled: isEnabledForUrl, passKeys: passKeys } setState: setState currentKeyQueue: (request) -> keyQueue = request.keyQueue + vomnibarShow: -> Vomnibar.show() + vomnibarClose: -> Vomnibar.close() chrome.runtime.onMessage.addListener (request, sender, sendResponse) -> # In the options page, we will receive requests from both content and background scripts. ignore those diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee index 9c13cd6d1..0e19f194d 100644 --- a/content_scripts/vomnibar.coffee +++ b/content_scripts/vomnibar.coffee @@ -1,243 +1,56 @@ Vomnibar = - vomnibarUI: null # the dialog instance for this window - completers: {} - - getCompleter: (name) -> - if (!(name of @completers)) - @completers[name] = new BackgroundCompleter(name) - @completers[name] - - # - # Activate the Vomnibox. - # - activateWithCompleter: (completerName, refreshInterval, initialQueryValue, selectFirstResult, forceNewTab) -> - completer = @getCompleter(completerName) - @vomnibarUI = new VomnibarUI() unless @vomnibarUI - completer.refresh() - @vomnibarUI.setInitialSelectionValue(if selectFirstResult then 0 else -1) - @vomnibarUI.setCompleter(completer) - @vomnibarUI.setRefreshInterval(refreshInterval) - @vomnibarUI.setForceNewTab(forceNewTab) - @vomnibarUI.show() - if (initialQueryValue) - @vomnibarUI.setQuery(initialQueryValue) - @vomnibarUI.update() - - activate: -> @activateWithCompleter("omni", 100) - activateInNewTab: -> @activateWithCompleter("omni", 100, null, false, true) - activateTabSelection: -> @activateWithCompleter("tabs", 0, null, true) - activateBookmarks: -> @activateWithCompleter("bookmarks", 0, null, true) - activateBookmarksInNewTab: -> @activateWithCompleter("bookmarks", 0, null, true, true) - getUI: -> @vomnibarUI - - -class VomnibarUI - constructor: -> - @refreshInterval = 0 - @initDom() + vomnibarElement: null + + activate: -> @open {completer:"omni"} + activateInNewTab: -> @open { + completer: "omni" + selectFirst: false + newTab: true + } + activateTabSelection: -> @open { + completer: "tabs" + selectFirst: true + } + activateBookmarks: -> @open { + completer: "bookmarks" + selectFirst: true + } + activateBookmarksInNewTab: -> @open { + completer: "bookmarks" + selectFirst: true + newTab: true + } + + open: (options) -> + unless @vomnibarElement? + @vomnibarElement = document.createElement "iframe" + @vomnibarElement.className = "vomnibarFrame" + @vomnibarElement.seamless = "seamless" + @hide() - setQuery: (query) -> @input.value = query + options.frameId = frameId - setInitialSelectionValue: (initialSelectionValue) -> - @initialSelectionValue = initialSelectionValue + optionStrings = [] + for option of options + if typeof options[option] == "boolean" + optionStrings.push option if options[option] + else + optionStrings.push "#{option}=#{escape(options[option])}" - setCompleter: (completer) -> - @completer = completer - @reset() + @vomnibarElement.src = "#{chrome.runtime.getURL "pages/vomnibar.html"}?#{optionStrings.join "&"}" + document.documentElement.appendChild @vomnibarElement - setRefreshInterval: (refreshInterval) -> @refreshInterval = refreshInterval + @vomnibarElement.focus() - setForceNewTab: (forceNewTab) -> @forceNewTab = forceNewTab + close: -> + @hide() + @vomnibarElement?.remove() show: -> - @box.style.display = "block" - @input.focus() - @handlerId = handlerStack.push keydown: @onKeydown.bind @ + @vomnibarElement?.style.display = "block" hide: -> - @box.style.display = "none" - @completionList.style.display = "none" - @input.blur() - handlerStack.remove @handlerId - - reset: -> - @input.value = "" - @updateTimer = null - @completions = [] - @selection = @initialSelectionValue - @update(true) - - updateSelection: -> - # We have taken the option to add some global state here (previousCompletionType) to tell if a search - # item has just appeared or disappeared, if that happens we either set the initialSelectionValue to 0 or 1 - # I feel that this approach is cleaner than bubbling the state up from the suggestion level - # so we just inspect it afterwards - if @completions[0] - if @previousCompletionType != "search" && @completions[0].type == "search" - @selection = 0 - else if @previousCompletionType == "search" && @completions[0].type != "search" - @selection = -1 - for i in [0...@completionList.children.length] - @completionList.children[i].className = (if i == @selection then "vomnibarSelected" else "") - @previousCompletionType = @completions[0].type if @completions[0] - - # - # Returns the user's action ("up", "down", "enter", "dismiss" or null) based on their keypress. - # We support the arrow keys and other shortcuts for moving, so this method hides that complexity. - # - actionFromKeyEvent: (event) -> - key = KeyboardUtils.getKeyChar(event) - if (KeyboardUtils.isEscape(event)) - return "dismiss" - else if (key == "up" || - (event.shiftKey && event.keyCode == keyCodes.tab) || - (event.ctrlKey && (key == "k" || key == "p"))) - return "up" - else if (key == "down" || - (event.keyCode == keyCodes.tab && !event.shiftKey) || - (event.ctrlKey && (key == "j" || key == "n"))) - return "down" - else if (event.keyCode == keyCodes.enter) - return "enter" - - onKeydown: (event) -> - action = @actionFromKeyEvent(event) - return true unless action # pass through - - openInNewTab = @forceNewTab || - (event.shiftKey || event.ctrlKey || KeyboardUtils.isPrimaryModifierKey(event)) - if (action == "dismiss") - @hide() - else if (action == "up") - @selection -= 1 - @selection = @completions.length - 1 if @selection < @initialSelectionValue - @input.value = @completions[@selection].url - @updateSelection() - else if (action == "down") - @selection += 1 - @selection = @initialSelectionValue if @selection == @completions.length - @input.value = @completions[@selection].url - @updateSelection() - else if (action == "enter") - # If they type something and hit enter without selecting a completion from our list of suggestions, - # try to open their query as a URL directly. If it doesn't look like a URL, we will search using - # google. - if (@selection == -1) - query = @input.value.trim() - # on an empty vomnibar is a no-op. - return unless 0 < query.length - @hide() - chrome.runtime.sendMessage({ - handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab" - url: query }) - else - @update true, => - # Shift+Enter will open the result in a new tab instead of the current tab. - @completions[@selection].performAction(openInNewTab) - @hide() - - # It seems like we have to manually suppress the event here and still return true. - event.stopPropagation() - event.preventDefault() - true - - updateCompletions: (callback) -> - query = @input.value.trim() - - @completer.filter query, (completions) => - @completions = completions - @populateUiWithCompletions(completions) - callback() if callback - - populateUiWithCompletions: (completions) -> - # update completion list with the new data - @completionList.innerHTML = completions.map((completion) -> "
  • #{completion.html}
  • ").join("") - @completionList.style.display = if completions.length > 0 then "block" else "none" - @selection = Math.min(Math.max(@initialSelectionValue, @selection), @completions.length - 1) - @updateSelection() - - update: (updateSynchronously, callback) -> - if (updateSynchronously) - # cancel scheduled update - if (@updateTimer != null) - window.clearTimeout(@updateTimer) - @updateCompletions(callback) - else if (@updateTimer != null) - # an update is already scheduled, don't do anything - return - else - # always update asynchronously for better user experience and to take some load off the CPU - # (not every keystroke will cause a dedicated update) - @updateTimer = setTimeout(=> - @updateCompletions(callback) - @updateTimer = null - @refreshInterval) - - initDom: -> - @box = Utils.createElementFromHtml( - """ -
    -
    - -
    -
      -
      - """) - @box.style.display = "none" - document.body.appendChild(@box) - - @input = document.querySelector("#vomnibar input") - @input.addEventListener "input", => @update() - @completionList = document.querySelector("#vomnibar ul") - @completionList.style.display = "none" - -# -# Sends filter and refresh requests to a Vomnibox completer on the background page. -# -class BackgroundCompleter - # - name: The background page completer that you want to interface with. Either "omni", "tabs", or - # "bookmarks". */ - constructor: (@name) -> - @filterPort = chrome.runtime.connect({ name: "filterCompleter" }) - - refresh: -> chrome.runtime.sendMessage({ handler: "refreshCompleter", name: @name }) - - filter: (query, callback) -> - id = Utils.createUniqueId() - @filterPort.onMessage.addListener (msg) => - @filterPort.onMessage.removeListener(arguments.callee) - # The result objects coming from the background page will be of the form: - # { html: "", type: "", url: "" } - # type will be one of [tab, bookmark, history, domain]. - results = msg.results.map (result) -> - functionToCall = if (result.type == "tab") - BackgroundCompleter.completionActions.switchToTab.curry(result.tabId) - else - BackgroundCompleter.completionActions.navigateToUrl.curry(result.url) - result.performAction = functionToCall - result - callback(results) - - @filterPort.postMessage({ id: id, name: @name, query: query }) - -extend BackgroundCompleter, - # - # These are the actions we can perform when the user selects a result in the Vomnibox. - # - completionActions: - navigateToUrl: (url, openInNewTab) -> - # If the URL is a bookmarklet prefixed with javascript:, we shouldn't open that in a new tab. - if url.startsWith "javascript:" - script = document.createElement 'script' - script.textContent = decodeURIComponent(url["javascript:".length..]) - (document.head || document.documentElement).appendChild script - else - chrome.runtime.sendMessage( - handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab" - url: url, - selected: openInNewTab) - - switchToTab: (tabId) -> chrome.runtime.sendMessage({ handler: "selectSpecificTab", id: tabId }) + @vomnibarElement?.style.display = "none" root = exports ? window root.Vomnibar = Vomnibar diff --git a/manifest.json b/manifest.json index 9a4e01678..84b91f0e9 100644 --- a/manifest.json +++ b/manifest.json @@ -55,5 +55,8 @@ "browser_action": { "default_icon": "icons/browser_action_disabled.png", "default_popup": "pages/popup.html" - } + }, + "web_accessible_resources": [ + "pages/vomnibar.html" + ] } diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee new file mode 100644 index 000000000..11fb4b651 --- /dev/null +++ b/pages/vomnibar.coffee @@ -0,0 +1,266 @@ +Vomnibar = + vomnibarUI: null # the dialog instance for this window + completers: {} + + getCompleter: (name) -> + if (!(name of @completers)) + @completers[name] = new BackgroundCompleter(name) + @completers[name] + + # + # Activate the Vomnibox. + # + activateWithCompleter: (options) -> + completer = @getCompleter(options.completer) + @vomnibarUI ?= new VomnibarUI() + completer.refresh() + @vomnibarUI.setInitialSelectionValue(if options.selectFirst then 0 else -1) + @vomnibarUI.setCompleter(completer) + @vomnibarUI.setRefreshInterval(options.refreshInterval) + @vomnibarUI.setForceNewTab(options.newTab) + @vomnibarUI.setFrameId(options.frameId) + @vomnibarUI.show() + if (options.query) + @vomnibarUI.setQuery(options.query) + @vomnibarUI.update() + + getUI: -> @vomnibarUI + + +class VomnibarUI + constructor: -> + @refreshInterval = 0 + @initDom() + + setQuery: (query) -> @input.value = query + + setInitialSelectionValue: (initialSelectionValue) -> + @initialSelectionValue = initialSelectionValue + + setCompleter: (completer) -> + @completer = completer + @reset() + + setRefreshInterval: (refreshInterval) -> @refreshInterval = refreshInterval + + setForceNewTab: (forceNewTab) -> @forceNewTab = forceNewTab + + setFrameId: (frameId) -> @frameId = frameId + + show: -> + @box.style.display = "block" + @input.focus() + @input.addEventListener "keydown", @onKeydown + + chrome.runtime.sendMessage + handler: "echo" + name: "vomnibarShow" + frameId: @frameId + + hide: -> + @box.style.display = "none" + @completionList.style.display = "none" + @input.blur() + @input.removeEventListener "keydown", @onKeydown + window.parent.focus() + chrome.runtime.sendMessage + handler: "echo" + name: "vomnibarClose" + frameId: @frameId + + reset: -> + @input.value = "" + @updateTimer = null + @completions = [] + @selection = @initialSelectionValue + @update(true) + + updateSelection: -> + # We have taken the option to add some global state here (previousCompletionType) to tell if a search + # item has just appeared or disappeared, if that happens we either set the initialSelectionValue to 0 or 1 + # I feel that this approach is cleaner than bubbling the state up from the suggestion level + # so we just inspect it afterwards + if @completions[0] + if @previousCompletionType != "search" && @completions[0].type == "search" + @selection = 0 + else if @previousCompletionType == "search" && @completions[0].type != "search" + @selection = -1 + for i in [0...@completionList.children.length] + @completionList.children[i].className = (if i == @selection then "vomnibarSelected" else "") + @previousCompletionType = @completions[0].type if @completions[0] + + # + # Returns the user's action ("up", "down", "enter", "dismiss" or null) based on their keypress. + # We support the arrow keys and other shortcuts for moving, so this method hides that complexity. + # + actionFromKeyEvent: (event) -> + key = KeyboardUtils.getKeyChar(event) + if (KeyboardUtils.isEscape(event)) + return "dismiss" + else if (key == "up" || + (event.shiftKey && event.keyCode == keyCodes.tab) || + (event.ctrlKey && (key == "k" || key == "p"))) + return "up" + else if (key == "down" || + (event.keyCode == keyCodes.tab && !event.shiftKey) || + (event.ctrlKey && (key == "j" || key == "n"))) + return "down" + else if (event.keyCode == keyCodes.enter) + return "enter" + + onKeydown: (event) => + action = @actionFromKeyEvent(event) + return true unless action # pass through + + openInNewTab = @forceNewTab || + (event.shiftKey || event.ctrlKey || KeyboardUtils.isPrimaryModifierKey(event)) + if (action == "dismiss") + @hide() + else if (action == "up") + @selection -= 1 + @selection = @completions.length - 1 if @selection < @initialSelectionValue + @updateSelection() + else if (action == "down") + @selection += 1 + @selection = @initialSelectionValue if @selection == @completions.length + @updateSelection() + else if (action == "enter") + # If they type something and hit enter without selecting a completion from our list of suggestions, + # try to open their query as a URL directly. If it doesn't look like a URL, we will search using + # google. + if (@selection == -1) + query = @input.value.trim() + # on an empty vomnibar is a no-op. + return unless 0 < query.length + @hide() + chrome.runtime.sendMessage({ + handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab" + url: query }) + else + @update true, => + # Shift+Enter will open the result in a new tab instead of the current tab. + @completions[@selection].performAction(openInNewTab) + @hide() + + # It seems like we have to manually suppress the event here and still return true. + event.stopPropagation() + event.preventDefault() + true + + updateCompletions: (callback) -> + query = @input.value.trim() + + @completer.filter query, (completions) => + @completions = completions + @populateUiWithCompletions(completions) + callback() if callback + + populateUiWithCompletions: (completions) -> + # update completion list with the new data + @completionList.innerHTML = completions.map((completion) -> "
    • #{completion.html}
    • ").join("") + @completionList.style.display = if completions.length > 0 then "block" else "none" + @selection = Math.min(Math.max(@initialSelectionValue, @selection), @completions.length - 1) + @updateSelection() + + update: (updateSynchronously, callback) -> + if (updateSynchronously) + # cancel scheduled update + if (@updateTimer != null) + window.clearTimeout(@updateTimer) + @updateCompletions(callback) + else if (@updateTimer != null) + # an update is already scheduled, don't do anything + return + else + # always update asynchronously for better user experience and to take some load off the CPU + # (not every keystroke will cause a dedicated update) + @updateTimer = setTimeout(=> + @updateCompletions(callback) + @updateTimer = null + @refreshInterval) + + initDom: -> + @box = document.getElementById("vomnibar") + + @input = @box.querySelector("input") + @input.addEventListener "input", => @update() + @completionList = @box.querySelector("ul") + @completionList.style.display = "none" + + window.addEventListener "focus", => @input.focus() + +# +# Sends filter and refresh requests to a Vomnibox completer on the background page. +# +class BackgroundCompleter + # - name: The background page completer that you want to interface with. Either "omni", "tabs", or + # "bookmarks". */ + constructor: (@name) -> + @filterPort = chrome.runtime.connect({ name: "filterCompleter" }) + + refresh: -> chrome.runtime.sendMessage({ handler: "refreshCompleter", name: @name }) + + filter: (query, callback) -> + id = Utils.createUniqueId() + @filterPort.onMessage.addListener (msg) => + @filterPort.onMessage.removeListener(arguments.callee) + # The result objects coming from the background page will be of the form: + # { html: "", type: "", url: "" } + # type will be one of [tab, bookmark, history, domain]. + results = msg.results.map (result) -> + functionToCall = if (result.type == "tab") + BackgroundCompleter.completionActions.switchToTab.curry(result.tabId) + else + BackgroundCompleter.completionActions.navigateToUrl.curry(result.url) + result.performAction = functionToCall + result + callback(results) + + @filterPort.postMessage({ id: id, name: @name, query: query }) + +extend BackgroundCompleter, + # + # These are the actions we can perform when the user selects a result in the Vomnibox. + # + completionActions: + navigateToUrl: (url, openInNewTab) -> + # If the URL is a bookmarklet prefixed with javascript:, we shouldn't open it in a new tab. + if url.startsWith "javascript:" + openInNewTab = false + chrome.runtime.sendMessage( + handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab" + url: url, + selected: openInNewTab) + + switchToTab: (tabId) -> chrome.runtime.sendMessage({ handler: "selectSpecificTab", id: tabId }) + +initializeOnDomReady = -> + options = + completer: "omni" + query: null + frameId: -1 + + booleanOptions = ["selectFirst", "newTab"] + + # Convert options in URL to options object + document.location.search + .split(/[\?&]/) + .map((option) -> + [name, value] = option.split "=" + options[name] = value + ) + + # Set boolean options + for option in booleanOptions + options[option] = option of options and options[option] != "false" + + options.refreshInterval = switch options.completer + when "omni" then 100 + else 0 + + Vomnibar.activateWithCompleter options + +window.addEventListener "DOMContentLoaded", initializeOnDomReady + +root = exports ? window +root.Vomnibar = Vomnibar diff --git a/pages/vomnibar.css b/pages/vomnibar.css new file mode 100644 index 000000000..958445e62 --- /dev/null +++ b/pages/vomnibar.css @@ -0,0 +1,129 @@ + +/* Vomnibar CSS */ + +#vomnibar ol, #vomnibar ul { + list-style: none; + display: block; +} + +#vomnibar { + display: block; + position: fixed; + width: calc(100% - 20px); /* adjusted to keep border radius and box-shadow visible*/ + /*min-width: 400px; + top: 70px; + left: 50%;*/ + top: 8px; + left: 8px; + /*margin: 0 0 0 -40%;*/ + font-family: sans-serif; + + background: #F1F1F1; + text-align: left; + border-radius: 4px; + box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.8); + border: 1px solid #aaa; + /* One less than hint markers and the help dialog. */ + z-index: 99999996; +} + +#vomnibar input { + color: #000; + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-size: 20px; + height: 34px; + margin-bottom: 0; + padding: 4px; + background-color: white; + border-radius: 3px; + border: 1px solid #E8E8E8; + box-shadow: #444 0px 0px 1px; + width: 100%; + outline: none; + box-sizing: border-box; +} + +#vomnibar .vomnibarSearchArea { + display: block; + padding: 10px; + background-color: #F1F1F1; + border-radius: 4px 4px 0 0; + border-bottom: 1px solid #C6C9CE; +} + +#vomnibar ul { + background-color: white; + border-radius: 0 0 4px 4px; + list-style: none; + padding: 10px 0; + padding-top: 0; +} + +#vomnibar li { + border-bottom: 1px solid #ddd; + line-height: 1.1em; + padding: 7px 10px; + font-size: 16px; + color: black; + position: relative; + display: list-item; + margin: auto; +} + +#vomnibar li:last-of-type { + border-bottom: none; +} + +#vomnibar li .vomnibarTopHalf, #vomnibar li .vomnibarBottomHalf { + display: block; + overflow: hidden; +} + +#vomnibar li .vomnibarBottomHalf { + font-size: 15px; + margin-top: 3px; + padding: 2px 0; +} + +#vomnibar li .vomnibarSource { + color: #777; + margin-right: 4px; +} +#vomnibar li .vomnibarRelevancy { + position: absolute; + right: 0; + top: 0; + padding: 5px; + background-color: white; + color: black; + font-family: monospace; + width: 100px; + overflow: hidden; +} + +#vomnibar li .vomnibarUrl { + white-space: nowrap; + color: #224684; +} + +#vomnibar li .vomnibarMatch { + font-weight: bold; + color: black; +} + +#vomnibar li em, #vomnibar li .vomnibarTitle { + color: black; + margin-left: 4px; + font-weight: normal; +} +#vomnibar li em { font-style: italic; } +#vomnibar li em .vomnibarMatch, #vomnibar li .vomnibarTitle .vomnibarMatch { + color: #333; + text-decoration: underline; +} + +#vomnibar li.vomnibarSelected { + background-color: #BBCEE9; + font-weight: normal; +} + diff --git a/pages/vomnibar.html b/pages/vomnibar.html new file mode 100644 index 000000000..6cba99e6e --- /dev/null +++ b/pages/vomnibar.html @@ -0,0 +1,21 @@ + + + Vomnibar + + + + + + + + + + +
      +
      + +
      +
        +
        + + From f9cb424fad03042353e8720398b390dd8b2281d0 Mon Sep 17 00:00:00 2001 From: Shrikant Sharat Date: Fri, 27 Dec 2013 10:05:41 +0530 Subject: [PATCH 2/4] Allow open popups from bookmarklets. Fix #968. Use the `openUrlInCurrentTab` message to open bookmarklets so they are opened via `chrome.tabs.update` instead of an injected script element. This allows bookmarklets to open popups. --- lib/utils.coffee | 2 +- pages/vomnibar.coffee | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/utils.coffee b/lib/utils.coffee index a93831d73..5d93ae706 100644 --- a/lib/utils.coffee +++ b/lib/utils.coffee @@ -26,7 +26,7 @@ Utils = -> id += 1 hasChromePrefix: (url) -> - chromePrefixes = [ "about", "view-source", "chrome-extension", "data" ] + chromePrefixes = ["about:", "view-source:", "chrome-extension:", "data:", "javascript:"] for prefix in chromePrefixes return true if url.startsWith prefix false diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee index 11fb4b651..5cd37db64 100644 --- a/pages/vomnibar.coffee +++ b/pages/vomnibar.coffee @@ -224,9 +224,8 @@ extend BackgroundCompleter, # completionActions: navigateToUrl: (url, openInNewTab) -> - # If the URL is a bookmarklet prefixed with javascript:, we shouldn't open it in a new tab. - if url.startsWith "javascript:" - openInNewTab = false + # If the URL is a bookmarklet prefixed with javascript:, we shouldn't open that in a new tab. + openInNewTab = false if url.startsWith("javascript:") chrome.runtime.sendMessage( handler: if openInNewTab then "openUrlInNewTab" else "openUrlInCurrentTab" url: url, From fdf592b25ead4c4ec919c1d0e6322d2dba48c1f8 Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Thu, 21 Aug 2014 19:50:09 +0100 Subject: [PATCH 3/4] Hacks to make Vomnibar tests work again --- pages/vomnibar.coffee | 19 +++++++++++++++++++ test_harnesses/vomnibar.html | 2 +- tests/dom_tests/dom_tests.html | 8 ++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee index 5cd37db64..2e51082a7 100644 --- a/pages/vomnibar.coffee +++ b/pages/vomnibar.coffee @@ -24,6 +24,25 @@ Vomnibar = @vomnibarUI.setQuery(options.query) @vomnibarUI.update() + activate: -> @activateWithCompleter {completer:"omni"} + activateInNewTab: -> @activateWithCompleter { + completer: "omni" + selectFirst: false + newTab: true + } + activateTabSelection: -> @activateWithCompleter { + completer: "tabs" + selectFirst: true + } + activateBookmarks: -> @activateWithCompleter { + completer: "bookmarks" + selectFirst: true + } + activateBookmarksInNewTab: -> @activateWithCompleter { + completer: "bookmarks" + selectFirst: true + newTab: true + } getUI: -> @vomnibarUI diff --git a/test_harnesses/vomnibar.html b/test_harnesses/vomnibar.html index 4d50e7496..820210b0a 100644 --- a/test_harnesses/vomnibar.html +++ b/test_harnesses/vomnibar.html @@ -11,7 +11,7 @@ - + + @@ -53,5 +54,12 @@

        Vimium Tests

        +
        +
        + +
        +
          +
          + From d65f265a6ad137be0db4d8c86879e5123a10087b Mon Sep 17 00:00:00 2001 From: mrmr1993 Date: Tue, 2 Sep 2014 17:43:41 +0100 Subject: [PATCH 4/4] Add comments about moving the Vomnibar to an iframe --- background_scripts/main.coffee | 4 ++-- content_scripts/vomnibar.coffee | 8 ++++++++ pages/vomnibar.coffee | 5 +++++ tests/dom_tests/dom_tests.html | 3 +++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index 49417d7ac..03d6143df 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -635,8 +635,8 @@ getCurrFrameIndex = (frames) -> return i if frames[i].id == focusedFrame frames.length + 1 -# Send message back to the tab unchanged. -# Frames in the same tab can use this to communicate securely. +# Send message back to the tab unchanged. This allows different frames from the same tab to message eachother +# while avoiding security concerns such as eavesdropping or message spoofing. echo = (request, sender) -> delete request.handler # No need to send this information chrome.tabs.sendMessage(sender.tab.id, request) diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee index 0e19f194d..7b3cfdbec 100644 --- a/content_scripts/vomnibar.coffee +++ b/content_scripts/vomnibar.coffee @@ -1,3 +1,6 @@ +# +# This wraps the vomnibar iframe, which we inject into the page to provide the vomnibar. +# Vomnibar = vomnibarElement: null @@ -21,6 +24,11 @@ Vomnibar = newTab: true } + # This function opens the vomnibar. It accepts options, a map with the values: + # completer - The completer to fetch results from. + # query - Optional. Text to prefill the Vomnibar with. + # selectFirst - Optional, boolean. Whether to select the first entry. + # newTab - Optional, boolean. Whether to open the result in a new tab. open: (options) -> unless @vomnibarElement? @vomnibarElement = document.createElement "iframe" diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee index 2e51082a7..814016238 100644 --- a/pages/vomnibar.coffee +++ b/pages/vomnibar.coffee @@ -1,3 +1,8 @@ +# +# This controls the contents of the Vomnibar iframe. We use an iframe to avoid changing the selection on the +# page (useful for bookmarklets), ensure that the Vomnibar style is unaffected by the page, and simplify key +# handling in vimium_frontend.coffee +# Vomnibar = vomnibarUI: null # the dialog instance for this window completers: {} diff --git a/tests/dom_tests/dom_tests.html b/tests/dom_tests/dom_tests.html index 72f54c9dd..e6427e85c 100644 --- a/tests/dom_tests/dom_tests.html +++ b/tests/dom_tests/dom_tests.html @@ -54,6 +54,9 @@

          Vimium Tests

          +