diff options
| author | Stephen Blott | 2015-01-06 05:39:25 +0000 |
|---|---|---|
| committer | Stephen Blott | 2015-01-06 05:39:25 +0000 |
| commit | 30dee76c6ab1de9e2a62701dacffc29fa5be0866 (patch) | |
| tree | 1aab7586b612a92222a4cfe85f4d4f5173e236bc /pages | |
| parent | 3620fec662ab89bd4f7827e66deec49ff4d11b8e (diff) | |
| parent | fc2201b996e47ca06090e10e4ebfcd9f4b345fde (diff) | |
| download | vimium-30dee76c6ab1de9e2a62701dacffc29fa5be0866.tar.bz2 | |
Merge pull request #1407 from smblott-github/post-1.46
Merge post-1.46 in its entirety
Diffstat (limited to 'pages')
| -rw-r--r-- | pages/options.html | 12 | ||||
| -rw-r--r-- | pages/ui_component_server.coffee | 27 | ||||
| -rw-r--r-- | pages/vomnibar.coffee | 235 | ||||
| -rw-r--r-- | pages/vomnibar.css | 136 | ||||
| -rw-r--r-- | pages/vomnibar.html | 22 |
5 files changed, 427 insertions, 5 deletions
diff --git a/pages/options.html b/pages/options.html index ae45537a..d37646c4 100644 --- a/pages/options.html +++ b/pages/options.html @@ -54,11 +54,13 @@ unmapAll <td verticalAlign="top"> <div class="help"> <div class="example"> - This adds search-engine shortcuts to the Vomnibar.<br/><br/> - The format is:<br/> - <pre>your-keyword: http://the-site.com/?q=%s</pre> - %s will be replaced with your search terms.<br/> - Lines which start with "#" are comments. + Add search-engine shortcuts to the Vomnibar. Format:<br/> + <pre> +a: http://a.com/?q=%s +b: http://b.com/?q=%s description +" this is a comment +# this is also a comment</pre> + %s is replaced with the search terms. </div> </div> <textarea id="searchEngines"></textarea> diff --git a/pages/ui_component_server.coffee b/pages/ui_component_server.coffee new file mode 100644 index 00000000..8b43095b --- /dev/null +++ b/pages/ui_component_server.coffee @@ -0,0 +1,27 @@ + +# Fetch the Vimium secret, register the port recieved from the parent window, and stop listening for messages +# on the window object. vimiumSecret is accessible only within the current instantion of Vimium. So a +# malicious host page trying to register its own port can do no better than guessing. +registerPort = (event) -> + chrome.storage.local.get "vimiumSecret", ({vimiumSecret: secret}) -> + return unless event.source == window.parent and event.data == secret + UIComponentServer.portOpen event.ports[0] + window.removeEventListener "message", registerPort + +window.addEventListener "message", registerPort + +UIComponentServer = + ownerPagePort: null + handleMessage: null + + portOpen: (@ownerPagePort) -> + @ownerPagePort.onmessage = (event) => + @handleMessage event if @handleMessage + + registerHandler: (@handleMessage) -> + + postMessage: (message) -> + @ownerPagePort.postMessage message if @ownerPagePort + +root = exports ? window +root.UIComponentServer = UIComponentServer diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee new file mode 100644 index 00000000..0ade7f0e --- /dev/null +++ b/pages/vomnibar.coffee @@ -0,0 +1,235 @@ +# +# 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 + getUI: -> @vomnibarUI + completers: {} + + getCompleter: (name) -> + if (!(name of @completers)) + @completers[name] = new BackgroundCompleter(name) + @completers[name] + + # + # Activate the Vomnibox. + # + activate: (userOptions) -> + options = + completer: "omni" + query: "" + newTab: false + selectFirst: false + extend options, userOptions + + options.refreshInterval = switch options.completer + when "omni" then 100 + else 0 + + 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.setQuery(options.query) + @vomnibarUI.update() + +class VomnibarUI + constructor: -> + @refreshInterval = 0 + @initDom() + + setQuery: (query) -> @input.value = query + + setInitialSelectionValue: (initialSelectionValue) -> + @initialSelectionValue = initialSelectionValue + + setCompleter: (completer) -> + @completer = completer + @reset() + @update(true) + + setRefreshInterval: (refreshInterval) -> @refreshInterval = refreshInterval + + setForceNewTab: (forceNewTab) -> @forceNewTab = forceNewTab + + hide: -> + UIComponentServer.postMessage "hide" + @reset() + + reset: -> + @completionList.style.display = "" + @input.value = "" + @updateTimer = null + @completions = [] + @selection = @initialSelectionValue + + 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.stopImmediatePropagation() + 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 "" + @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) + + @input.focus() + + initDom: -> + @box = document.getElementById("vomnibar") + + @input = @box.querySelector("input") + @input.addEventListener "input", @update + @input.addEventListener "keydown", @onKeydown + @completionList = @box.querySelector("ul") + @completionList.style.display = "" + + 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 }) + +UIComponentServer.registerHandler (event) -> Vomnibar.activate event.data + +root = exports ? window +root.Vomnibar = Vomnibar diff --git a/pages/vomnibar.css b/pages/vomnibar.css new file mode 100644 index 00000000..2042a6c4 --- /dev/null +++ b/pages/vomnibar.css @@ -0,0 +1,136 @@ + +/* Vomnibar CSS */ + +#vomnibar ol, #vomnibar ul { + list-style: none; + display: none; +} + +#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: 2147483646; +} + +#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; +} + diff --git a/pages/vomnibar.html b/pages/vomnibar.html new file mode 100644 index 00000000..2ca463d0 --- /dev/null +++ b/pages/vomnibar.html @@ -0,0 +1,22 @@ +<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="ui_component_server.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> |
