aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CREDITS1
-rw-r--r--README.md2
-rw-r--r--background_scripts/bg_utils.coffee39
-rw-r--r--background_scripts/commands.coffee140
-rw-r--r--background_scripts/main.coffee84
-rw-r--r--content_scripts/link_hints.coffee2
-rw-r--r--content_scripts/vimium.css5
-rw-r--r--content_scripts/vimium_frontend.coffee3
-rw-r--r--content_scripts/vomnibar.coffee13
-rw-r--r--lib/utils.coffee34
-rw-r--r--pages/completion_engines.html5
-rw-r--r--pages/help_dialog.coffee83
-rw-r--r--pages/help_dialog.html23
-rw-r--r--pages/hud.coffee1
-rw-r--r--pages/options.coffee4
-rw-r--r--tests/dom_tests/dom_tests.coffee5
-rw-r--r--tests/dom_tests/vomnibar_test.coffee5
-rw-r--r--tests/unit_tests/commands_test.coffee17
18 files changed, 263 insertions, 203 deletions
diff --git a/CREDITS b/CREDITS
index 0d23d364..a7877ff8 100644
--- a/CREDITS
+++ b/CREDITS
@@ -48,5 +48,6 @@ Contributors:
Scott Pinkelman <scott@scottpinkelman.com> (github: sco-tt)
Darryl Pogue <darryl@dpogue.ca> (github: dpogue)
tobimensch
+ Ramiro Araujo <rama.araujo@gmail.com> (github: ramiroaraujo)
Feel free to add real names in addition to GitHub usernames.
diff --git a/README.md b/README.md
index 78bbba57..7fcafdfe 100644
--- a/README.md
+++ b/README.md
@@ -162,11 +162,13 @@ Release Notes
Changes since the previous release (not yet in the Chrome Store version)
- Features:
+ - The `createTab` command can now open specific URLs (e.g, `map X createTab http://www.bbc.com/news`).
- You can now map multi-modifier keys, for example: `<c-a-X>`.
- Vimium can now do simple key mapping in some modes; see
[here](https://github.com/philc/vimium/wiki/Tips-and-Tricks#key-mapping).
This can be helpful with some non-English keyboards (and can also be used
to remap `Escape`).
+ - For *Custom key mappings* on the options page, lines which end with `\` are now continued on the following line.
- Process:
- In order to provide faster bug fixes, we may in future push new releases without the noisy notification.
diff --git a/background_scripts/bg_utils.coffee b/background_scripts/bg_utils.coffee
index ea54c900..b8e618ff 100644
--- a/background_scripts/bg_utils.coffee
+++ b/background_scripts/bg_utils.coffee
@@ -77,4 +77,43 @@ BgUtils =
logElement.value += "#{dateString}: #{message}\n"
logElement.scrollTop = 2000000000
+ # Remove comments and leading/trailing whitespace from a list of lines, and merge lines where the last
+ # character on the preceding line is "\".
+ parseLines: (text) ->
+ for line in text.replace(/\\\n/g, "").split("\n").map((line) -> line.trim())
+ continue if line.length == 0
+ continue if line[0] in '#"'
+ line
+
+# Utility for parsing and using the custom search-engine configuration. We re-use the previous parse if the
+# search-engine configuration is unchanged.
+SearchEngines =
+ previousSearchEngines: null
+ searchEngines: null
+
+ refresh: (searchEngines) ->
+ unless @previousSearchEngines? and searchEngines == @previousSearchEngines
+ @previousSearchEngines = searchEngines
+ @searchEngines = new AsyncDataFetcher (callback) ->
+ engines = {}
+ for line in BgUtils.parseLines searchEngines
+ tokens = line.split /\s+/
+ if 2 <= tokens.length
+ keyword = tokens[0].split(":")[0]
+ searchUrl = tokens[1]
+ description = tokens[2..].join(" ") || "search (#{keyword})"
+ engines[keyword] = {keyword, searchUrl, description} if Utils.hasFullUrlPrefix searchUrl
+
+ callback engines
+
+ # Use the parsed search-engine configuration, possibly asynchronously.
+ use: (callback) ->
+ @searchEngines.use callback
+
+ # Both set (refresh) the search-engine configuration and use it at the same time.
+ refreshAndUse: (searchEngines, callback) ->
+ @refresh searchEngines
+ @use callback
+
+root.SearchEngines = SearchEngines
root.BgUtils = BgUtils
diff --git a/background_scripts/commands.coffee b/background_scripts/commands.coffee
index d12b704d..8d3808a2 100644
--- a/background_scripts/commands.coffee
+++ b/background_scripts/commands.coffee
@@ -1,36 +1,55 @@
Commands =
+ availableCommands: {}
+ keyToCommandRegistry: null
+ mapKeyRegistry: null
+
init: ->
- for own command, descriptor of commandDescriptions
- @addCommand(command, descriptor[0], descriptor[1])
- @loadKeyMappings Settings.get "keyMappings"
+ for own command, [description, options] of commandDescriptions
+ @availableCommands[command] = extend (options ? {}), description: description
+
Settings.postUpdateHooks["keyMappings"] = @loadKeyMappings.bind this
+ @loadKeyMappings Settings.get "keyMappings"
+ @prepareHelpPageData()
loadKeyMappings: (customKeyMappings) ->
- @clearKeyMappingsAndSetDefaults()
- @parseCustomKeyMappings customKeyMappings
- @generateKeyStateMapping()
- chrome.storage.local.set mapKeyRegistry: @mapKeyRegistry
-
- availableCommands: {}
- keyToCommandRegistry: {}
-
- # Registers a command, making it available to be optionally bound to a key.
- # options:
- # - background: whether this command needs to be run against the background page.
- addCommand: (command, description, options = {}) ->
- if command of @availableCommands
- BgUtils.log "#{command} is already defined! Check commands.coffee for duplicates."
- return
+ @keyToCommandRegistry = {}
+ @mapKeyRegistry = {}
- @availableCommands[command] = extend options, description: description
+ configLines = ("map #{key} #{command}" for own key, command of defaultKeyMappings)
+ configLines.push BgUtils.parseLines(customKeyMappings)...
+ seen = {}
+ unmapAll = false
+ for line in configLines.reverse()
+ tokens = line.split /\s+/
+ switch tokens[0]
+ when "map"
+ if 3 <= tokens.length and not unmapAll
+ [_, key, command, optionList...] = tokens
+ if not seen[key] and registryEntry = @availableCommands[command]
+ seen[key] = true
+ keySequence = @parseKeySequence key
+ options = @parseCommandOptions command, optionList
+ @keyToCommandRegistry[key] = extend {keySequence, command, options, optionList}, @availableCommands[command]
+ when "unmap"
+ if tokens.length == 2
+ seen[tokens[1]] = true
+ when "unmapAll"
+ unmapAll = true
+ when "mapkey"
+ if tokens.length == 3
+ fromChar = @parseKeySequence tokens[1]
+ toChar = @parseKeySequence tokens[2]
+ @mapKeyRegistry[fromChar[0]] ?= toChar[0] if fromChar.length == toChar.length == 1
- mapKeyToCommand: ({ key, keySequence, command, options }) ->
- unless @availableCommands[command]
- BgUtils.log "#{command} doesn't exist!"
- return
+ chrome.storage.local.set mapKeyRegistry: @mapKeyRegistry
+ @installKeyStateMapping()
- options ?= {}
- @keyToCommandRegistry[key] = extend { keySequence, command, options }, @availableCommands[command]
+ # Push the key mapping for passNextKey into Settings so that it's available in the front end for insert
+ # mode. We exclude single-key mappings (that is, printable keys) because when users press printable keys
+ # in insert mode they expect the character to be input, not to be droppped into some special Vimium
+ # mode.
+ Settings.set "passNextKeyKeys",
+ (key for own key of @keyToCommandRegistry when @keyToCommandRegistry[key].command == "passNextKey" and 1 < key.length)
# Lower-case the appropriate portions of named keys.
#
@@ -60,44 +79,6 @@ Commands =
else
[key[0], @parseKeySequence(key[1..])...]
- parseCustomKeyMappings: (customKeyMappings) ->
- for line in customKeyMappings.split "\n"
- unless line[0] == "\"" or line[0] == "#"
- tokens = line.replace(/\s+$/, "").split /\s+/
- switch tokens[0]
- when "map"
- [ _, key, command, optionList... ] = tokens
- keySequence = @parseKeySequence key
- if command? and @availableCommands[command]
- key = keySequence.join ""
- BgUtils.log "mapping [\"#{keySequence.join '", "'}\"] to #{command}"
- @mapKeyToCommand { key, command, keySequence, options: @parseCommandOptions command, optionList }
- else
- BgUtils.log "skipping [\"#{keySequence.join '", "'}\"] for #{command} -- something is not right"
-
- when "unmap"
- if tokens.length == 2
- keySequence = @parseKeySequence tokens[1]
- key = keySequence.join ""
- BgUtils.log "Unmapping #{key}"
- delete @keyToCommandRegistry[key]
-
- when "unmapAll"
- @keyToCommandRegistry = {}
-
- when "mapkey"
- if tokens.length == 3
- fromChar = @parseKeySequence tokens[1]
- toChar = @parseKeySequence tokens[2]
- @mapKeyRegistry[fromChar[0]] = toChar[0] if fromChar.length == toChar.length == 1
-
- # Push the key mapping for passNextKey into Settings so that it's available in the front end for insert
- # mode. We exclude single-key mappings (that is, printable keys) because when users press printable keys
- # in insert mode they expect the character to be input, not to be droppped into some special Vimium
- # mode.
- Settings.set "passNextKeyKeys",
- (key for own key of @keyToCommandRegistry when @keyToCommandRegistry[key].command == "passNextKey" and 1 < key.length)
-
# Command options follow command mappings, and are of one of two forms:
# key=value - a value
# key - a flag
@@ -114,16 +95,9 @@ Commands =
options
- clearKeyMappingsAndSetDefaults: ->
- @keyToCommandRegistry = {}
- @mapKeyRegistry = {}
- for own key, command of defaultKeyMappings
- keySequence = @parseKeySequence key
- key = keySequence.join ""
- @mapKeyToCommand { key, command, keySequence }
-
- # This generates a nested key-to-command mapping structure. There is an example in mode_key_handler.coffee.
- generateKeyStateMapping: ->
+ # This generates and installs a nested key-to-command mapping structure. There is an example in
+ # mode_key_handler.coffee.
+ installKeyStateMapping: ->
keyStateMapping = {}
for own keys, registryEntry of @keyToCommandRegistry
currentMapping = keyStateMapping
@@ -137,9 +111,25 @@ Commands =
else
currentMapping[key] = extend {}, registryEntry
# We don't need these properties in the content scripts.
- delete registryEntry[prop] for prop in ["keySequence", "description"]
+ delete currentMapping[key][prop] for prop in ["keySequence", "description"]
chrome.storage.local.set normalModeKeyStateMapping: keyStateMapping
+ # Build the "helpPageData" data structure which the help page needs and place it in Chrome storage.
+ prepareHelpPageData: ->
+ commandToKey = {}
+ for own key, registryEntry of @keyToCommandRegistry
+ (commandToKey[registryEntry.command] ?= []).push key
+ commandGroups = {}
+ for own group, commands of @commandGroups
+ commandGroups[group] = []
+ for command in commands
+ commandGroups[group].push
+ command: command
+ description: @availableCommands[command].description
+ keys: commandToKey[command] ? []
+ advanced: command in @advancedCommands
+ chrome.storage.local.set helpPageData: commandGroups
+
# An ordered listing of all available commands, grouped by type. This is the order they will
# be shown in the help page.
commandGroups:
@@ -351,7 +341,7 @@ commandDescriptions =
openCopiedUrlInNewTab: ["Open the clipboard's URL in a new tab", { background: true, repeatLimit: 20 }]
enterInsertMode: ["Enter insert mode", { noRepeat: true }]
- passNextKey: ["Pass the next key to Chrome"]
+ passNextKey: ["Pass the next key to the page"]
enterVisualMode: ["Enter visual mode", { noRepeat: true }]
enterVisualLineMode: ["Enter visual line mode", { noRepeat: true }]
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index e57e061d..72f87a7e 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -80,56 +80,6 @@ onURLChange = (details) ->
chrome.webNavigation.onHistoryStateUpdated.addListener onURLChange # history.pushState.
chrome.webNavigation.onReferenceFragmentUpdated.addListener onURLChange # Hash changed.
-# Retrieves the help dialog HTML template from a file, and populates it with the latest keybindings.
-getHelpDialogHtml = ({showUnboundCommands, showCommandNames, customTitle}) ->
- commandsToKey = {}
- for own key of Commands.keyToCommandRegistry
- command = Commands.keyToCommandRegistry[key].command
- commandsToKey[command] = (commandsToKey[command] || []).concat(key)
-
- replacementStrings =
- version: Utils.getCurrentVersion()
- title: customTitle || "Help"
- tip: if showCommandNames then "Tip: click command names to yank them to the clipboard." else "&nbsp;"
-
- for own group of Commands.commandGroups
- replacementStrings[group] =
- helpDialogHtmlForCommandGroup(group, commandsToKey, Commands.availableCommands,
- showUnboundCommands, showCommandNames)
-
- replacementStrings
-
-#
-# Generates HTML for a given set of commands. commandGroups are defined in commands.js
-#
-helpDialogHtmlForCommandGroup = (group, commandsToKey, availableCommands,
- showUnboundCommands, showCommandNames) ->
- html = []
- for command in Commands.commandGroups[group]
- keys = commandsToKey[command] || []
- bindings = ("<span class='vimiumHelpDialogKey'>#{Utils.escapeHtml key}</span>" for key in keys).join ", "
- if (showUnboundCommands || commandsToKey[command])
- isAdvanced = Commands.advancedCommands.indexOf(command) >= 0
- description = availableCommands[command].description
- if keys.join(", ").length < 12
- helpDialogHtmlForCommand html, isAdvanced, bindings, description, showCommandNames, command
- else
- # If the length of the bindings is too long, then we display the bindings on a separate row from the
- # description. This prevents the column alignment from becoming out of whack.
- helpDialogHtmlForCommand html, isAdvanced, bindings, "", false, ""
- helpDialogHtmlForCommand html, isAdvanced, "", description, showCommandNames, command
- html.join("\n")
-
-helpDialogHtmlForCommand = (html, isAdvanced, bindings, description, showCommandNames, command) ->
- html.push "<tr class='vimiumReset #{"advanced" if isAdvanced}'>"
- if description
- html.push "<td class='vimiumReset'>#{bindings}</td>"
- html.push "<td class='vimiumReset'></td><td class='vimiumReset vimiumHelpDescription'>", description
- html.push("(<span class='vimiumReset commandName'>#{command}</span>)") if showCommandNames
- else
- html.push "<td class='vimiumReset' colspan='3' style='text-align: left;'>", bindings
- html.push("</td></tr>")
-
# Cache "content_scripts/vimium.css" in chrome.storage.local for UI components.
do ->
req = new XMLHttpRequest()
@@ -195,15 +145,34 @@ mkRepeatCommand = (command) -> (request) ->
# These are commands which are bound to keystrokes which must be handled by the background page. They are
# mapped in commands.coffee.
BackgroundCommands =
+ # Create a new tab. Also, with:
+ # map X createTab http://www.bbc.com/news
+ # create a new tab with the given URL.
createTab: mkRepeatCommand (request, callback) ->
- request.url ?= do ->
- url = Settings.get "newTabUrl"
- if url == "pages/blank.html"
- # "pages/blank.html" does not work in incognito mode, so fall back to "chrome://newtab" instead.
- if request.tab.incognito then "chrome://newtab" else chrome.runtime.getURL newTabUrl
+ request.urls ?=
+ if request.url
+ # If the request contains a URL, then use it.
+ [request.url]
+ else
+ # Otherwise, if we have a registryEntry containing URLs, then use them.
+ urlList = (opt for opt in request.registryEntry.optionList when Utils.isUrl opt)
+ if 0 < urlList.length
+ urlList
+ else
+ # Otherwise, just create a new tab.
+ newTabUrl = Settings.get "newTabUrl"
+ if newTabUrl == "pages/blank.html"
+ # "pages/blank.html" does not work in incognito mode, so fall back to "chrome://newtab" instead.
+ [if request.tab.incognito then "chrome://newtab" else chrome.runtime.getURL newTabUrl]
+ else
+ [newTabUrl]
+ urls = request.urls[..].reverse()
+ do openNextUrl = (request) ->
+ if 0 < urls.length
+ TabOperations.openUrlInNewTab (extend request, {url: urls.pop()}), (tab) ->
+ openNextUrl extend request, {tab, tabId: tab.id}
else
- url
- TabOperations.openUrlInNewTab request, (tab) -> callback extend request, {tab, tabId: tab.id}
+ callback request
duplicateTab: mkRepeatCommand (request, callback) ->
chrome.tabs.duplicate request.tabId, (tab) -> callback extend request, {tab, tabId: tab.id}
moveTabToNewWindow: ({count, tab}) ->
@@ -429,7 +398,6 @@ portHandlers =
sendRequestHandlers =
runBackgroundCommand: (request) -> BackgroundCommands[request.registryEntry.command] request
- getHelpDialogHtml: getHelpDialogHtml
# getCurrentTabUrl is used by the content scripts to get their full URL, because window.location cannot help
# with Chrome-specific URLs like "view-source:http:..".
getCurrentTabUrl: ({tab}) -> tab.url
diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee
index ea002cfe..3a8d0786 100644
--- a/content_scripts/link_hints.coffee
+++ b/content_scripts/link_hints.coffee
@@ -394,7 +394,7 @@ class LinkHintsMode
clickActivator = (modifiers) -> (link) -> DomUtils.simulateClick link, modifiers
linkActivator = @mode.linkActivator ? clickActivator @mode.clickModifiers
# TODO: Are there any other input elements which should not receive focus?
- if clickEl.nodeName.toLowerCase() == "input" and clickEl.type not in ["button", "submit"]
+ if clickEl.nodeName.toLowerCase() in ["input", "select"] and clickEl.type not in ["button", "submit"]
clickEl.focus()
linkActivator clickEl
diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css
index edb1ab85..3e8f65d6 100644
--- a/content_scripts/vimium.css
+++ b/content_scripts/vimium.css
@@ -243,7 +243,7 @@ div#vimiumHelpDialog td.vimiumHelpDescription {
font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;
font-size:14px;
}
-div#vimiumHelpDialog span.commandName {
+div#vimiumHelpDialog span.vimiumCopyCommandNameName {
font-style: italic;
cursor: pointer;
font-size: 12px;
@@ -275,6 +275,9 @@ div#vimiumHelpDialogFooter {
position: relative;
margin-bottom: 37px;
}
+table.helpDialogBottom {
+ width:100%;
+}
td.helpDialogBottomRight {
width:100%;
float:right;
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index fcca98ab..21826944 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -646,8 +646,7 @@ enterFindMode = ->
new FindMode()
window.showHelp = (sourceFrameId) ->
- chrome.runtime.sendMessage handler: "getHelpDialogHtml", (response) ->
- HelpDialog.toggle {sourceFrameId, html: response}
+ HelpDialog.toggle {sourceFrameId, showAllCommandDetails: false}
# If we are in the help dialog iframe, then HelpDialog is already defined with the necessary functions.
window.HelpDialog ?=
diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee
index 49c4288e..04499523 100644
--- a/content_scripts/vomnibar.coffee
+++ b/content_scripts/vomnibar.coffee
@@ -4,22 +4,19 @@
Vomnibar =
vomnibarUI: null
- # Parse any additional options from the command's registry entry. Currently, this only includes a flag of
- # the form "keyword=X", for direct activation of a custom search engine.
- parseRegistryEntry: (registryEntry = { options: [] }, callback = null) ->
- searchEngines = Settings.get("searchEngines") ? ""
- SearchEngines.refreshAndUse searchEngines, (engines) ->
- callback? registryEntry.options
+ # Extract any additional options from the command's registry entry.
+ extractOptionsFromRegistryEntry: (registryEntry, callback) ->
+ callback? extend {}, registryEntry.options
# sourceFrameId here (and below) is the ID of the frame from which this request originates, which may be different
# from the current frame.
activate: (sourceFrameId, registryEntry) ->
- @parseRegistryEntry registryEntry, (options) =>
+ @extractOptionsFromRegistryEntry registryEntry, (options) =>
@open sourceFrameId, extend options, completer:"omni"
activateInNewTab: (sourceFrameId, registryEntry) ->
- @parseRegistryEntry registryEntry, (options) =>
+ @extractOptionsFromRegistryEntry registryEntry, (options) =>
@open sourceFrameId, extend options, completer:"omni", newTab: true
activateTabSelection: (sourceFrameId) -> @open sourceFrameId, {
diff --git a/lib/utils.coffee b/lib/utils.coffee
index 5a028186..babb5f96 100644
--- a/lib/utils.coffee
+++ b/lib/utils.coffee
@@ -216,39 +216,6 @@ Utils =
chrome.storage.onChanged.addListener (changes, area) =>
setter changes[key].newValue if changes[key]?.newValue?
-# Utility for parsing and using the custom search-engine configuration. We re-use the previous parse if the
-# search-engine configuration is unchanged.
-SearchEngines =
- previousSearchEngines: null
- searchEngines: null
-
- refresh: (searchEngines) ->
- unless @previousSearchEngines? and searchEngines == @previousSearchEngines
- @previousSearchEngines = searchEngines
- @searchEngines = new AsyncDataFetcher (callback) ->
- engines = {}
- for line in searchEngines.split "\n"
- line = line.trim()
- continue if /^[#"]/.test line
- tokens = line.split /\s+/
- continue unless 2 <= tokens.length
- keyword = tokens[0].split(":")[0]
- searchUrl = tokens[1]
- description = tokens[2..].join(" ") || "search (#{keyword})"
- continue unless Utils.hasFullUrlPrefix searchUrl
- engines[keyword] = { keyword, searchUrl, description }
-
- callback engines
-
- # Use the parsed search-engine configuration, possibly asynchronously.
- use: (callback) ->
- @searchEngines.use callback
-
- # Both set (refresh) the search-engine configuration and use it at the same time.
- refreshAndUse: (searchEngines, callback) ->
- @refresh searchEngines
- @use callback
-
# This creates a new function out of an existing function, where the new function takes fewer arguments. This
# allows us to pass around functions instead of functions + a partial list of arguments.
Function::curry = ->
@@ -347,7 +314,6 @@ class JobRunner
root = exports ? window
root.Utils = Utils
-root.SearchEngines = SearchEngines
root.SimpleCache = SimpleCache
root.AsyncDataFetcher = AsyncDataFetcher
root.JobRunner = JobRunner
diff --git a/pages/completion_engines.html b/pages/completion_engines.html
index d47bb87b..0c350664 100644
--- a/pages/completion_engines.html
+++ b/pages/completion_engines.html
@@ -20,10 +20,13 @@
<p>
Custom search engines can be configured on the <a href="options.html" target="_blank">options</a>
page. <br/>
- Further information is available on the <a href="https://github.com/philc/vimium/wiki/Search-Completion">wiki</a>.
+ Further information is available on the <a href="https://github.com/philc/vimium/wiki/Search-Completion" target="_blank">wiki</a>.
</p>
<header>Available Completion Engines</header>
<p>
+ Search completion is available in this version of Vimium for the the following custom search engines.
+ </p>
+ <p>
<dl id="engineList"></dl>
</p>
</div>
diff --git a/pages/help_dialog.coffee b/pages/help_dialog.coffee
index 28aafb4a..4ac9116b 100644
--- a/pages/help_dialog.coffee
+++ b/pages/help_dialog.coffee
@@ -1,3 +1,12 @@
+$ = (id) -> document.getElementById id
+$$ = (element, selector) -> element.querySelector selector
+
+# The ordering we show key bindings is alphanumerical, except that special keys sort to the end.
+compareKeys = (a,b) ->
+ a = a.replace "<","~"
+ b = b.replace "<", "~"
+ if a < b then -1 else if b < a then 1 else 0
+
# This overrides the HelpDialog implementation in vimium_frontend.coffee. We provide aliases for the two
# HelpDialog methods required by normalMode (isShowing() and toggle()).
HelpDialog =
@@ -20,30 +29,67 @@ HelpDialog =
chrome.runtime.sendMessage({handler: "openOptionsPageInNewTab"})
false)
document.getElementById("toggleAdvancedCommands").addEventListener("click",
- HelpDialog.toggleAdvancedCommands, false)
+ HelpDialog.toggleAdvancedCommands.bind(HelpDialog), false)
document.documentElement.addEventListener "click", (event) =>
@hide() unless @dialogElement.contains event.target
, false
- show: ({html}) ->
- for own placeholder, htmlString of html
- @dialogElement.querySelector("#help-dialog-#{placeholder}").innerHTML = htmlString
+ instantiateHtmlTemplate: (parentNode, templateId, callback) ->
+ templateContent = document.querySelector(templateId).content
+ node = document.importNode templateContent, true
+ parentNode.appendChild node
+ callback parentNode.lastElementChild
+
+ show: ({showAllCommandDetails}) ->
+ $("help-dialog-title").textContent = if showAllCommandDetails then "Command Listing" else "Help"
+ $("help-dialog-version").textContent = Utils.getCurrentVersion()
+
+ chrome.storage.local.get "helpPageData", ({helpPageData}) =>
+ for own group, commands of helpPageData
+ container = @dialogElement.querySelector("#help-dialog-#{group}")
+ container.innerHTML = ""
+ for command in commands when showAllCommandDetails or 0 < command.keys.length
+ keysElement = null
+ descriptionElement = null
+
+ useTwoRows = 12 <= command.keys.join(", ").length
+ unless useTwoRows
+ @instantiateHtmlTemplate container, "#helpDialogEntry", (element) ->
+ element.classList.add "advanced" if command.advanced
+ keysElement = descriptionElement = element
+ else
+ @instantiateHtmlTemplate container, "#helpDialogEntryBindingsOnly", (element) ->
+ element.classList.add "advanced" if command.advanced
+ keysElement = element
+ @instantiateHtmlTemplate container, "#helpDialogEntry", (element) ->
+ element.classList.add "advanced" if command.advanced
+ descriptionElement = element
+
+ $$(descriptionElement, ".vimiumHelpDescription").textContent = command.description
+
+ keysElement = $$(keysElement, ".vimiumKeyBindings")
+ lastElement = null
+ for key in command.keys.sort compareKeys
+ @instantiateHtmlTemplate keysElement, "#keysTemplate", (element) ->
+ lastElement = element
+ $$(element, ".vimiumHelpDialogKey").innerHTML = Utils.escapeHtml key
+ # And strip off the trailing ", ", if necessary.
+ lastElement.removeChild $$ lastElement, ".commaSeparator" if lastElement
- @showAdvancedCommands(@getShowAdvancedCommands())
+ if showAllCommandDetails
+ @instantiateHtmlTemplate $$(descriptionElement, ".vimiumHelpDescription"), "#commandNameTemplate", (element) ->
+ commandNameElement = $$ element, ".vimiumCopyCommandNameName"
+ commandNameElement.textContent = command.command
+ commandNameElement.title = "Click to copy \"#{command.command}\" to clipboard."
+ commandNameElement.addEventListener "click", ->
+ chrome.runtime.sendMessage handler: "copyToClipboard", data: commandNameElement.textContent
+ HUD.showForDuration("Yanked #{commandNameElement.textContent}.", 2000)
- # When command names are shown, clicking on them copies their text to the clipboard (and they can be
- # clicked with link hints).
- for element in @dialogElement.getElementsByClassName "commandName"
- do (element) ->
- element.setAttribute "role", "link"
- element.addEventListener "click", ->
- commandName = element.textContent
- chrome.runtime.sendMessage handler: "copyToClipboard", data: commandName
- HUD.showForDuration("Yanked #{commandName}.", 2000)
+ @showAdvancedCommands(@getShowAdvancedCommands())
- # "Click" the dialog element (so that it becomes scrollable).
- DomUtils.simulateClick @dialogElement
+ # "Click" the dialog element (so that it becomes scrollable).
+ DomUtils.simulateClick @dialogElement
hide: -> UIComponentServer.hide()
toggle: -> @hide()
@@ -52,10 +98,15 @@ HelpDialog =
# Advanced commands are hidden by default so they don't overwhelm new and casual users.
#
toggleAdvancedCommands: (event) ->
+ vimiumHelpDialogContainer = $ "vimiumHelpDialogContainer"
+ scrollHeightBefore = vimiumHelpDialogContainer.scrollHeight
event.preventDefault()
showAdvanced = HelpDialog.getShowAdvancedCommands()
HelpDialog.showAdvancedCommands(!showAdvanced)
Settings.set("helpDialog_showAdvancedCommands", !showAdvanced)
+ # Try to keep the "show advanced commands" button in the same scroll position.
+ scrollHeightDelta = vimiumHelpDialogContainer.scrollHeight - scrollHeightBefore
+ vimiumHelpDialogContainer.scrollTop += scrollHeightDelta if 0 < scrollHeightDelta
showAdvancedCommands: (visible) ->
document.getElementById("toggleAdvancedCommands").innerHTML =
diff --git a/pages/help_dialog.html b/pages/help_dialog.html
index c23b2ac1..7bc0d86c 100644
--- a/pages/help_dialog.html
+++ b/pages/help_dialog.html
@@ -66,7 +66,7 @@
</div>
<div>
- <table>
+ <table class="helpDialogBottom">
<tr>
<td class="helpDialogBottomLeft">
<span id="help-dialog-tip"></span>
@@ -96,5 +96,26 @@
</div>
</div>
</div>
+
+
+ <template id="helpDialogEntry">
+ <tr class="vimiumReset">
+ <td class="vimiumReset vimiumKeyBindings"></td>
+ <td class="vimiumReset"></td>
+ <td class="vimiumReset vimiumHelpDescription"></td>
+ </tr>
+ </template>
+
+ <template id="helpDialogEntryBindingsOnly">
+ <tr>
+ <td class="vimiumReset vimiumKeyBindings" colspan="3" style="text-align: left"></td>
+ </tr>
+ </template>
+
+ <template id="keysTemplate"><span><span class="vimiumHelpDialogKey"></span><span class="commaSeparator">, </span></span></template>
+
+ <template id="commandNameTemplate">
+ <span class="vimiumReset vimiumCopyCommandName">(<span class="vimiumCopyCommandNameName" role="link"></span>)</span>
+ </template>
</body>
</html>
diff --git a/pages/hud.coffee b/pages/hud.coffee
index 77114a18..36e4cbd2 100644
--- a/pages/hud.coffee
+++ b/pages/hud.coffee
@@ -69,6 +69,7 @@ handlers =
countElement = document.createElement "span"
countElement.id = "hud-match-count"
+ countElement.style.float = "right"
hud.appendChild countElement
inputElement.focus()
diff --git a/pages/options.coffee b/pages/options.coffee
index 9947de67..0a71611a 100644
--- a/pages/options.coffee
+++ b/pages/options.coffee
@@ -234,9 +234,7 @@ initOptionsPage = ->
event.preventDefault()
activateHelpDialog = ->
- request = showUnboundCommands: true, showCommandNames: true, customTitle: "Command Listing"
- chrome.runtime.sendMessage extend(request, handler: "getHelpDialogHtml"), (response) ->
- HelpDialog.toggle {html: response}
+ HelpDialog.toggle showAllCommandDetails: true
saveOptions = ->
$("linkHintCharacters").value = $("linkHintCharacters").value.toLowerCase()
diff --git a/tests/dom_tests/dom_tests.coffee b/tests/dom_tests/dom_tests.coffee
index eab47546..9088fe30 100644
--- a/tests/dom_tests/dom_tests.coffee
+++ b/tests/dom_tests/dom_tests.coffee
@@ -188,6 +188,11 @@ context "Test link hints for focusing input elements correctly",
testDiv.appendChild input
inputs.push input
+ # Manually add also a select element to test focus.
+ input = document.createElement "select"
+ testDiv.appendChild input
+ inputs.push input
+
tearDown ->
document.getElementById("test-div").innerHTML = ""
diff --git a/tests/dom_tests/vomnibar_test.coffee b/tests/dom_tests/vomnibar_test.coffee
index 501fd5dd..a8a02f2b 100644
--- a/tests/dom_tests/vomnibar_test.coffee
+++ b/tests/dom_tests/vomnibar_test.coffee
@@ -1,5 +1,4 @@
vomnibarFrame = null
-SearchEngines.refresh ""
Vomnibar.init()
context "Keep selection within bounds",
@@ -30,7 +29,7 @@ context "Keep selection within bounds",
Vomnibar.vomnibarUI.hide()
should "set selection to position -1 for omni completion by default", ->
- Vomnibar.activate()
+ Vomnibar.activate 0, options: {}
ui = vomnibarFrame.Vomnibar.vomnibarUI
@completions = []
@@ -62,7 +61,7 @@ context "Keep selection within bounds",
assert.equal -1, ui.selection
should "keep selection within bounds", ->
- Vomnibar.activate()
+ Vomnibar.activate 0, options: {}
ui = vomnibarFrame.Vomnibar.vomnibarUI
@completions = []
diff --git a/tests/unit_tests/commands_test.coffee b/tests/unit_tests/commands_test.coffee
index 2c2e9542..0e0be1d6 100644
--- a/tests/unit_tests/commands_test.coffee
+++ b/tests/unit_tests/commands_test.coffee
@@ -1,5 +1,6 @@
require "./test_helper.js"
extend global, require "./test_chrome_stubs.js"
+extend global, require "../../background_scripts/bg_utils.js"
global.Settings = {postUpdateHooks: {}, get: (-> ""), set: ->}
{Commands} = require "../../background_scripts/commands.js"
@@ -97,6 +98,22 @@ context "Validate advanced commands",
for command in Commands.advancedCommands
assert.isTrue 0 <= @allCommands.indexOf command
+context "Parse commands",
+ should "omit whitespace", ->
+ assert.equal 0, BgUtils.parseLines(" \n \n ").length
+
+ should "omit comments", ->
+ assert.equal 0, BgUtils.parseLines(" # comment \n \" comment \n ").length
+
+ should "join lines", ->
+ assert.equal 1, BgUtils.parseLines("a\\\nb").length
+ assert.equal "ab", BgUtils.parseLines("a\\\nb")[0]
+
+ should "trim lines", ->
+ assert.equal 2, BgUtils.parseLines(" a \n b").length
+ assert.equal "a", BgUtils.parseLines(" a \n b")[0]
+ assert.equal "b", BgUtils.parseLines(" a \n b")[1]
+
# TODO (smblott) More tests:
# - Ensure each background command has an implmentation in BackgroundCommands
# - Ensure each foreground command has an implmentation in vimium_frontent.coffee