aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStephen Blott2014-10-31 12:39:39 +0000
committerStephen Blott2014-10-31 12:39:39 +0000
commit92f177faac3a14e41c033f4bc1ae95909e90c9ce (patch)
treea7b40b2a2d18608a989a7921a55569da5db393e4
parenta277fa6332f3aa3c0aa5f2c541f539fb1569c6b9 (diff)
parentd65f265a6ad137be0db4d8c86879e5123a10087b (diff)
downloadvimium-92f177faac3a14e41c033f4bc1ae95909e90c9ce.tar.bz2
Merge vomnibarToPage from @mrmr1993.
-rw-r--r--background_scripts/main.coffee37
-rw-r--r--content_scripts/vimium.css131
-rw-r--r--content_scripts/vimium_frontend.coffee2
-rw-r--r--content_scripts/vomnibar.coffee294
-rw-r--r--lib/utils.coffee2
-rw-r--r--manifest.json5
-rw-r--r--pages/vomnibar.coffee289
-rw-r--r--pages/vomnibar.css129
-rw-r--r--pages/vomnibar.html21
-rw-r--r--test_harnesses/vomnibar.html2
-rw-r--r--tests/dom_tests/dom_tests.html11
11 files changed, 551 insertions, 372 deletions
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index 898f46f1..0f2c6d85 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -620,6 +620,12 @@ getCurrFrameIndex = (frames) ->
return i if frames[i].id == focusedFrame
frames.length + 1
+# 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)
+
# Port handler mapping
portHandlers =
keyDown: handleKeyDown,
@@ -627,23 +633,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 24f229f3..fd613b31 100644
--- a/content_scripts/vimium.css
+++ b/content_scripts/vimium.css
@@ -266,143 +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 .vomnibarIcon {
- background-position-y: center;
- background-size: 16px;
- background-repeat: no-repeat;
- padding-left: 20px;
-}
-
-#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 4f7becba..81427c1a 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -123,6 +123,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 22b9ed64..10f75652 100644
--- a/content_scripts/vomnibar.coffee
+++ b/content_scripts/vomnibar.coffee
@@ -1,245 +1,75 @@
+#
+# This wraps the vomnibar iframe, which we inject into the page to provide the vomnibar.
+#
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)
- activateEditUrl: -> @activateWithCompleter("omni", 100, window.location.href)
- activateEditUrlInNewTab: -> @activateWithCompleter("omni", 100, window.location.href, false, 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
+ }
+ activateEditUrl: -> @open {
+ completer: "omni"
+ selectFirst: false
+ query: window.location.href
+ }
+ activateEditUrlInNewTab: -> @open {
+ completer: "omni"
+ selectFirst: false
+ query: window.location.href
+ 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"
+ @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()
- # <Enter> 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) -> "<li>#{completion.html}</li>").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(
- """
- <div id="vomnibar" class="vimiumReset">
- <div class="vimiumReset vomnibarSearchArea">
- <input type="text" class="vimiumReset">
- </div>
- <ul class="vimiumReset"></ul>
- </div>
- """)
- @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/lib/utils.coffee b/lib/utils.coffee
index a93831d7..5d93ae70 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/manifest.json b/manifest.json
index 3792458b..d52ac8f6 100644
--- a/manifest.json
+++ b/manifest.json
@@ -57,5 +57,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 00000000..81401623
--- /dev/null
+++ b/pages/vomnibar.coffee
@@ -0,0 +1,289 @@
+#
+# 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: {}
+
+ 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()
+
+ 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
+
+
+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()
+ # <Enter> 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) -> "<li>#{completion.html}</li>").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 that in a new tab.
+ openInNewTab = false if url.startsWith("javascript:")
+ 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 00000000..958445e6
--- /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 00000000..6cba99e6
--- /dev/null
+++ b/pages/vomnibar.html
@@ -0,0 +1,21 @@
+<html>
+ <head>
+ <title>Vomnibar</title>
+ <script type="text/javascript" src="../lib/utils.js"></script>
+ <script type="text/javascript" src="../lib/keyboard_utils.js"></script>
+ <script type="text/javascript" src="../lib/dom_utils.js"></script>
+ <script type="text/javascript" src="../lib/handler_stack.js"></script>
+ <script type="text/javascript" src="../lib/clipboard.js"></script>
+ <script type="text/javascript" src="vomnibar.js"></script>
+ <link rel="stylesheet" type="text/css" href="../content_scripts/vimium.css" />
+ <link rel="stylesheet" type="text/css" href="vomnibar.css" />
+ </head>
+ <body>
+ <div id="vomnibar" class="vimiumReset">
+ <div class="vimiumReset vomnibarSearchArea">
+ <input type="text" class="vimiumReset">
+ </div>
+ <ul class="vimiumReset"></ul>
+ </div>
+ </body>
+</html>
diff --git a/test_harnesses/vomnibar.html b/test_harnesses/vomnibar.html
index 4d50e749..820210b0 100644
--- a/test_harnesses/vomnibar.html
+++ b/test_harnesses/vomnibar.html
@@ -11,7 +11,7 @@
<script type="text/javascript" src="../lib/keyboard_utils.js"></script>
<script type="text/javascript" src="../lib/dom_utils.js"></script>
<script src="https://github.com/ooyala/livecss/raw/master/livecss.js"></script>
- <script type="text/javascript" src="../content_scripts/vomnibar.js"></script>
+ <script type="text/javascript" src="../pages/vomnibar.js"></script>
<link rel="stylesheet" type="text/css" href="../vimium.css" />
<script>
function setup() {
diff --git a/tests/dom_tests/dom_tests.html b/tests/dom_tests/dom_tests.html
index feddafac..e6427e85 100644
--- a/tests/dom_tests/dom_tests.html
+++ b/tests/dom_tests/dom_tests.html
@@ -38,6 +38,7 @@
<script type="text/javascript" src="../../content_scripts/vomnibar.js"></script>
<script type="text/javascript" src="../../content_scripts/scroller.js"></script>
<script type="text/javascript" src="../../content_scripts/vimium_frontend.js"></script>
+ <script type="text/javascript" src="../../pages/vomnibar.js"></script>
<script type="text/javascript" src="../shoulda.js/shoulda.js"></script>
<script type="text/javascript" src="dom_tests.js"></script>
@@ -53,5 +54,15 @@
<div id="output-div"></div>
+ <!-- This is a hack to keep Vomnibar tests working after moving it to an iframe.
+ The following are the elements from the iframe so that we can embed the iframe's script without errors.
+ TODO(mrmr1993): Look into rewriting the Vomnibar tests so we don't need this. -->
+ <div id="vomnibar" class="vimiumReset">
+ <div class="vimiumReset vomnibarSearchArea">
+ <input type="text" class="vimiumReset">
+ </div>
+ <ul class="vimiumReset"></ul>
+ </div>
+
</body>
</html>