diff options
| -rw-r--r-- | background_scripts/completion.coffee | 4 | ||||
| -rw-r--r-- | pages/vomnibar.coffee | 148 | ||||
| -rw-r--r-- | pages/vomnibar.css | 4 | ||||
| -rw-r--r-- | pages/vomnibar.html | 2 | ||||
| -rw-r--r-- | tests/dom_tests/vomnibar_test.coffee | 2 |
5 files changed, 129 insertions, 31 deletions
diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee index 8c73c658..850a257d 100644 --- a/background_scripts/completion.coffee +++ b/background_scripts/completion.coffee @@ -547,7 +547,7 @@ class MultiCompleter unless suggestions.length == 0 and shouldRunContinuations onComplete results: @prepareSuggestions queryTerms, suggestions - mayCacheResult: continuations.length == 0 + mayCacheResults: continuations.length == 0 # Run any continuations, unless there's a pending query. if shouldRunContinuations @@ -560,7 +560,7 @@ class MultiCompleter results: @prepareSuggestions queryTerms, suggestions # FIXME(smblott) This currently assumes that there is at most one continuation. We # should really be counting pending/completed continuations. - mayCacheResult: true + mayCacheResults: true # Admit subsequent queries, and launch any pending query. @filterInProgress = false diff --git a/pages/vomnibar.coffee b/pages/vomnibar.coffee index bb7720e9..db380063 100644 --- a/pages/vomnibar.coffee +++ b/pages/vomnibar.coffee @@ -69,6 +69,7 @@ class VomnibarUI @previousAutoSelect = null @previousInputValue = null @suppressedLeadingKeyword = null + @previousLength = 0 @selection = @initialSelectionValue @keywords = [] @@ -92,7 +93,7 @@ class VomnibarUI # For suggestions from search-engine completion, we copy the suggested text into the input when selected, # and revert when not. This allows the user to select a suggestion and then continue typing. if 0 <= @selection and @completions[@selection].insertText? - @previousInputValue ?= @input.value + @previousInputValue ?= @getInputWithoutSelectionRange() @input.value = @completions[@selection].insertText + " " else if @previousInputValue? @input.value = @previousInputValue @@ -102,8 +103,66 @@ class VomnibarUI for i in [0...@completionList.children.length] @completionList.children[i].className = (if i == @selection then "vomnibarSelected" else "") + highlightCommonMatches: (response) -> + # For custom search engines, add characters to the input which are: + # - not in the query/input + # - in all completions + # and select the added text. + + # Bail if we don't yet have the background completer's final word on the current query. + return unless response.mayCacheResults + + # Bail if there's an update pending (because @input and the correct completion state are out of sync). + return if @updateTimer? + + @previousLength ?= @input.value.length + previousLength = @previousLength + currentLength = @input.value.length + @previousLength = currentLength + + # Bail if the query didn't get longer. + console.log previousLength < currentLength, previousLength, currentLength, @input.value + return unless previousLength < currentLength + + # Bail if these aren't completions from a custom search engine. + return unless @suppressedLeadingKeyword? + + # Bail if there are too few suggestions. + return unless 1 < @completions.length + + # Fetch the query and the suggestion texts. + query = @input.value.ltrim().split(/\s+/).join(" ").toLowerCase() + suggestions = @completions[1..].map (completion) -> completion.title + + # Ensure that the query is a prefix of all suggestions. + for suggestion in suggestions + return unless 0 == suggestion.toLowerCase().indexOf query + + # Calculate the length of the shotest suggestion. + length = suggestions[0].length + length = Math.min length, suggestion.length for suggestion in suggestions + + # Find the thenght of the longest common continuation. + length = do -> + for index in [query.length...length] + for suggestion in suggestions + return index if suggestions[0][index].toLowerCase() != suggestion[index].toLowerCase() + length + + # But don't complete only whitespace. + return if /^\s+$/.test suggestions[0].slice query.length, length + + # Bail if there's nothing to complete. + return unless query.length < length + + # Install completion. + @input.value = suggestions[0].slice 0, length + @input.setSelectionRange query.length, length + # @previousLength = @input.value.length + # - # Returns the user's action ("up", "down", "enter", "dismiss", "delete" or null) based on their keypress. + # Returns the user's action ("up", "down", "tab", "enter", "dismiss", "delete" or null) based on their + # keypress. # We support the arrow keys and other shortcuts for moving, so this method hides that complexity. # actionFromKeyEvent: (event) -> @@ -114,8 +173,9 @@ class VomnibarUI (event.shiftKey && event.keyCode == keyCodes.tab) || (event.ctrlKey && (key == "k" || key == "p"))) return "up" + else if (event.keyCode == keyCodes.tab && !event.shiftKey) + return "tab" else if (key == "down" || - (event.keyCode == keyCodes.tab && !event.shiftKey) || (event.ctrlKey && (key == "j" || key == "n"))) return "down" else if (event.keyCode == keyCodes.enter) @@ -132,19 +192,38 @@ class VomnibarUI (event.shiftKey || event.ctrlKey || KeyboardUtils.isPrimaryModifierKey(event)) if (action == "dismiss") @hide() + else if action in [ "tab", "down" ] + if action == "tab" + if @inputContainsASelectionRange() + # There is a selection: callapse it and update the completions. + window.getSelection().collapseToEnd() + @update true + else + # There is no selection: treat "tab" as "down". + action = "down" + if action == "down" + @selection += 1 + @selection = @initialSelectionValue if @selection == @completions.length + @updateSelection() 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) + if @inputContainsASelectionRange() + # There is selected completion text in the input, put there by highlightCommonMatches(). It looks to + # the user like, if they type "enter", then that's the query which will fire. But we don't actually + # have a URL for this query (it doesn't actually correspond to any of the current completions). So we + # fire off a new query and immediately launch the first resulting URL. + @update true, => + if @completions[0]? + completion = @completions[0] + @hide -> completion.performAction openInNewTab + + # If the user types something and hits enter without selecting a completion from the list, then try to + # open their query as a URL directly. If it doesn't look like a URL, then use the default search + # engine. + else if (@selection == -1) query = @input.value.trim() # <Enter> on an empty vomnibar is a no-op. return unless 0 < query.length @@ -169,23 +248,35 @@ class VomnibarUI event.preventDefault() true - getInputValue: -> + # Test whether the input contains selected text. + inputContainsASelectionRange: -> + @input.selectionStart? and @input.selectionEnd? and @input.selectionStart != @input.selectionEnd + + # Return the text of the input, with any selected text renage removed. + getInputWithoutSelectionRange: -> + if @inputContainsASelectionRange() + @input.value[0...@input.selectionStart] + @input.value[@input.selectionEnd..] + else + @input.value + + # Return the background-page query corresponding to the current input state. In other words, reinstate any + # custom search engine keyword which is currently stripped from the input. + getInputValueAsQuery: -> (if @suppressedLeadingKeyword? then @suppressedLeadingKeyword + " " else "") + @input.value updateCompletions: (callback = null) -> - @clearUpdateTimer() - @completer.filter @getInputValue(), (@completions) => - @populateUiWithCompletions @completions + @completer.filter @getInputValueAsQuery(), (response) => + { results, mayCacheResults } = response + @completions = results + # Update completion list with the new suggestions. + @completionList.innerHTML = @completions.map((completion) -> "<li>#{completion.html}</li>").join("") + @completionList.style.display = if @completions.length > 0 then "block" else "" + @selection = Math.min @completions.length - 1, Math.max @initialSelectionValue, @selection + @previousAutoSelect = null if @completions[0]?.autoSelect and @completions[0]?.forceAutoSelect + @updateSelection() + @highlightCommonMatches response callback?() - populateUiWithCompletions: (completions) -> - # Update completion list with the new suggestions. - @completionList.innerHTML = completions.map((completion) -> "<li>#{completion.html}</li>").join("") - @completionList.style.display = if completions.length > 0 then "block" else "" - @selection = Math.min completions.length - 1, Math.max @initialSelectionValue, @selection - @previousAutoSelect = null if completions[0]?.autoSelect and completions[0]?.forceAutoSelect - @updateSelection() - updateOnInput: => @completer.cancel() # If the user types, then don't reset any previous text, and re-enable auto-select. @@ -209,11 +300,14 @@ class VomnibarUI # interface is snappy). updateSynchronously ||= @isCustomSearch() and not @suppressedLeadingKeyword? if updateSynchronously + @clearUpdateTimer() @updateCompletions callback else if not @updateTimer? # Update asynchronously for better user experience and to take some load off the CPU (not every # keystroke will cause a dedicated update) - @updateTimer = Utils.setTimeout @refreshInterval, => @updateCompletions callback + @updateTimer = Utils.setTimeout @refreshInterval, => + @updateTimer = null + @updateCompletions callback @input.focus() @@ -268,14 +362,14 @@ class BackgroundCompleter # Cache the result -- if we have been told it's ok to do so (it could be that more results will be # posted shortly). We cache the result even if it arrives late. - if msg.mayCacheResult + if msg.mayCacheResults console.log "cache set:", "-#{msg.cacheKey}-" if @debug - @cache[msg.cacheKey] = msg.results + @cache[msg.cacheKey] = msg else console.log "not setting cache:", "-#{msg.cacheKey}-" if @debug # Handle the message, but only if it hasn't arrived too late. - @mostRecentCallback msg.results if msg.id == @messageId + @mostRecentCallback msg if msg.id == @messageId filter: (query, @mostRecentCallback) -> queryTerms = query.trim().split(/\s+/).filter (s) -> 0 < s.length diff --git a/pages/vomnibar.css b/pages/vomnibar.css index 2042a6c4..4b7199e3 100644 --- a/pages/vomnibar.css +++ b/pages/vomnibar.css @@ -134,3 +134,7 @@ font-weight: normal; } +#vomnibarInput::selection { + /* This is the light grey color of the vomnibar border. */ + background-color: #F1F1F1; +} diff --git a/pages/vomnibar.html b/pages/vomnibar.html index 2ca463d0..87acc081 100644 --- a/pages/vomnibar.html +++ b/pages/vomnibar.html @@ -14,7 +14,7 @@ <body> <div id="vomnibar" class="vimiumReset"> <div class="vimiumReset vomnibarSearchArea"> - <input type="text" class="vimiumReset"> + <input id="vomnibarInput" type="text" class="vimiumReset"> </div> <ul class="vimiumReset"></ul> </div> diff --git a/tests/dom_tests/vomnibar_test.coffee b/tests/dom_tests/vomnibar_test.coffee index 0e02bb7b..e32c050d 100644 --- a/tests/dom_tests/vomnibar_test.coffee +++ b/tests/dom_tests/vomnibar_test.coffee @@ -14,7 +14,7 @@ context "Keep selection within bounds", oldGetCompleter = vomnibarFrame.Vomnibar.getCompleter.bind vomnibarFrame.Vomnibar stub vomnibarFrame.Vomnibar, 'getCompleter', (name) => completer = oldGetCompleter name - stub completer, 'filter', (query, callback) => callback(@completions) + stub completer, 'filter', (query, callback) => callback results: @completions completer # Shoulda.js doesn't support async tests, so we have to hack around. |
