diff options
| -rw-r--r-- | CREDITS | 1 | ||||
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | background_scripts/bg_utils.coffee | 39 | ||||
| -rw-r--r-- | background_scripts/commands.coffee | 140 | ||||
| -rw-r--r-- | background_scripts/main.coffee | 84 | ||||
| -rw-r--r-- | content_scripts/link_hints.coffee | 2 | ||||
| -rw-r--r-- | content_scripts/vimium.css | 5 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 3 | ||||
| -rw-r--r-- | content_scripts/vomnibar.coffee | 13 | ||||
| -rw-r--r-- | lib/utils.coffee | 34 | ||||
| -rw-r--r-- | pages/completion_engines.html | 5 | ||||
| -rw-r--r-- | pages/help_dialog.coffee | 83 | ||||
| -rw-r--r-- | pages/help_dialog.html | 23 | ||||
| -rw-r--r-- | pages/hud.coffee | 1 | ||||
| -rw-r--r-- | pages/options.coffee | 4 | ||||
| -rw-r--r-- | tests/dom_tests/dom_tests.coffee | 5 | ||||
| -rw-r--r-- | tests/dom_tests/vomnibar_test.coffee | 5 | ||||
| -rw-r--r-- | tests/unit_tests/commands_test.coffee | 17 |
18 files changed, 263 insertions, 203 deletions
@@ -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. @@ -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 " " - - 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 |
