aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md8
-rw-r--r--content_scripts/link_hints.coffee385
-rw-r--r--content_scripts/mode.coffee6
-rw-r--r--content_scripts/vimium.css4
-rw-r--r--content_scripts/vimium_frontend.coffee2
-rw-r--r--tests/dom_tests/dom_tests.coffee32
6 files changed, 227 insertions, 210 deletions
diff --git a/README.md b/README.md
index 097a0ede..dc161ca6 100644
--- a/README.md
+++ b/README.md
@@ -159,13 +159,13 @@ Release Notes
([here](https://github.com/philc/vimium/wiki/Search-Completion) and
[here](https://github.com/philc/vimium/wiki/Tips-and-Tricks#repeat-recent-queries)).
- A much improved interface for custom search engines.
-- Added <tt>\`\`</tt> to jump back to the previous position after selected
- jump-like movements (`gg`, `G`, `/`, `n`, `N` and local mark movements).
-- Global marks are now persistent across tab closes and browser sessions, and
- are synced between browser instances.
+- Added <tt>\`\`</tt> to jump back to the previous position after selected jump-like movements.
+- Global marks are now persistent (across tab closes and browser sessions) and synced.
+- For filtered link hints (not the default), you can now use `Tab` to select hints.
- Bug fixes, including:
- Bookmarklets accessed from the vomnibar.
- Global marks on non-Windows platforms.
+ - Link-hints for non-Latin keyboards.
1.51 (2015-05-02)
diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee
index 2bcc7508..107a292e 100644
--- a/content_scripts/link_hints.coffee
+++ b/content_scripts/link_hints.coffee
@@ -19,36 +19,35 @@ OPEN_INCOGNITO = name: "incognito"
DOWNLOAD_LINK_URL = name: "download"
LinkHints =
+ activateMode: (mode = OPEN_IN_CURRENT_TAB) -> new LinkHintsMode mode
+
+ activateModeToOpenInNewTab: -> @activateMode OPEN_IN_NEW_BG_TAB
+ activateModeToOpenInNewForegroundTab: -> @activateMode OPEN_IN_NEW_FG_TAB
+ activateModeToCopyLinkUrl: -> @activateMode COPY_LINK_URL
+ activateModeWithQueue: -> @activateMode OPEN_WITH_QUEUE
+ activateModeToOpenIncognito: -> @activateMode OPEN_INCOGNITO
+ activateModeToDownloadLink: -> @activateMode DOWNLOAD_LINK_URL
+
+class LinkHintsMode
hintMarkerContainingDiv: null
- # one of the enums listed at the top of this file
+ # One of the enums listed at the top of this file.
mode: undefined
- # function that does the appropriate action on the selected link
+ # Function that does the appropriate action on the selected link.
linkActivator: undefined
# While in delayMode, all keypresses have no effect.
delayMode: false
- # Handle the link hinting marker generation and matching. Must be initialized after Settings have been
- # loaded, so that we can retrieve the option setting.
- getMarkerMatcher: ->
- if Settings.get("filterLinkHints") then filterHints else alphabetHints
- # lock to ensure only one instance runs at a time
+ # Lock to ensure only one instance runs at a time.
isActive: false
+ # The link-hints "mode" (in the key-handler, indicator sense).
+ hintMode: null
# Call this function on exit (if defined).
onExit: null
+ # A count of the number of Tab presses since the last non-Tab keyboard event.
+ tabCount: 0
- # We need this as a top-level function because our command system doesn't yet support arguments.
- activateModeToOpenInNewTab: -> @activateMode(OPEN_IN_NEW_BG_TAB)
- activateModeToOpenInNewForegroundTab: -> @activateMode(OPEN_IN_NEW_FG_TAB)
- activateModeToCopyLinkUrl: -> @activateMode(COPY_LINK_URL)
- activateModeWithQueue: -> @activateMode(OPEN_WITH_QUEUE)
- activateModeToOpenIncognito: -> @activateMode(OPEN_INCOGNITO)
- activateModeToDownloadLink: -> @activateMode(DOWNLOAD_LINK_URL)
-
- activateMode: (mode = OPEN_IN_CURRENT_TAB) ->
+ constructor: (mode = OPEN_IN_CURRENT_TAB) ->
# we need documentElement to be ready in order to append links
return unless document.documentElement
-
- if @isActive
- return
@isActive = true
elements = @getVisibleClickableElements()
@@ -62,33 +61,38 @@ LinkHints =
length = (el) -> el.element.innerHTML?.length ? 0
elements.sort (a,b) -> length(a) - length b
hintMarkers = (@createMarkerFor(el) for el in elements)
- @getMarkerMatcher().fillInMarkers(hintMarkers)
+ @markerMatcher = new (if Settings.get "filterLinkHints" then FilterHints else AlphabetHints)
+ @markerMatcher.fillInMarkers hintMarkers
@hintMode = new Mode
name: "hint/#{mode.name}"
indicator: false
passInitialKeyupEvents: true
+ suppressAllKeyboardEvents: true
+ exitOnEscape: true
+ exitOnClick: true
+ exitOnScroll: true
keydown: @onKeyDownInMode.bind this, hintMarkers
- # Trap all other key events.
- keypress: -> false
- keyup: -> false
+ keypress: @onKeyPressInMode.bind this, hintMarkers
+
+ @hintMode.onExit =>
+ @deactivateMode() if @isActive
@setOpenLinkMode mode
# Note(philc): Append these markers as top level children instead of as child nodes to the link itself,
- # because some clickable elements cannot contain children, e.g. submit buttons. This has the caveat
- # that if you scroll the page and the link has position=fixed, the marker will not stay fixed.
- @hintMarkerContainingDiv = DomUtils.addElementList(hintMarkers,
- { id: "vimiumHintMarkerContainer", className: "vimiumReset" })
+ # because some clickable elements cannot contain children, e.g. submit buttons.
+ @hintMarkerContainingDiv = DomUtils.addElementList hintMarkers,
+ id: "vimiumHintMarkerContainer", className: "vimiumReset"
setOpenLinkMode: (@mode) ->
if @mode is OPEN_IN_NEW_BG_TAB or @mode is OPEN_IN_NEW_FG_TAB or @mode is OPEN_WITH_QUEUE
if @mode is OPEN_IN_NEW_BG_TAB
- @hintMode.setIndicator "Open link in new tab"
+ @hintMode.setIndicator "Open link in new tab."
else if @mode is OPEN_IN_NEW_FG_TAB
- @hintMode.setIndicator "Open link in new tab and switch to it"
+ @hintMode.setIndicator "Open link in new tab and switch to it."
else
- @hintMode.setIndicator "Open multiple links in a new tab"
+ @hintMode.setIndicator "Open multiple links in new tabs."
@linkActivator = (link) ->
# When "clicking" on a link, dispatch the event with the appropriate meta key (CMD on Mac, CTRL on
# windows) to open it in a new tab if necessary.
@@ -98,7 +102,7 @@ LinkHints =
ctrlKey: KeyboardUtils.platform != "Mac"
altKey: false
else if @mode is COPY_LINK_URL
- @hintMode.setIndicator "Copy link URL to Clipboard"
+ @hintMode.setIndicator "Copy link URL to Clipboard."
@linkActivator = (link) =>
if link.href?
chrome.runtime.sendMessage handler: "copyToClipboard", data: link.href
@@ -108,15 +112,15 @@ LinkHints =
else
@onExit = -> HUD.showForDuration "No link to yank.", 2000
else if @mode is OPEN_INCOGNITO
- @hintMode.setIndicator "Open link in incognito window"
+ @hintMode.setIndicator "Open link in incognito window."
@linkActivator = (link) ->
chrome.runtime.sendMessage handler: 'openUrlInIncognito', url: link.href
else if @mode is DOWNLOAD_LINK_URL
- @hintMode.setIndicator "Download link URL"
+ @hintMode.setIndicator "Download link URL."
@linkActivator = (link) ->
DomUtils.simulateClick link, altKey: true, ctrlKey: false, metaKey: false
else # OPEN_IN_CURRENT_TAB
- @hintMode.setIndicator "Open link in current tab"
+ @hintMode.setIndicator "Open link in current tab."
@linkActivator = (link) -> DomUtils.simulateClick.bind(DomUtils, link)()
#
@@ -257,62 +261,85 @@ LinkHints =
nonOverlappingElements
- #
- # Handles shift and esc keys. The other keys are passed to getMarkerMatcher().matchHintsByKey.
- #
+ # Handles <Shift> and <Ctrl>.
onKeyDownInMode: (hintMarkers, event) ->
return if @delayMode or event.repeat
+ @keydownKeyChar = KeyboardUtils.getKeyChar(event).toLowerCase()
+
+ previousTabCount = @tabCount
+ @tabCount = 0
+
+ if event.keyCode in [ keyCodes.shiftKey, keyCodes.ctrlKey ] and
+ @mode in [ OPEN_IN_CURRENT_TAB, OPEN_WITH_QUEUE, OPEN_IN_NEW_BG_TAB, OPEN_IN_NEW_FG_TAB ]
+ @tabCount = previousTabCount
+ # Toggle whether to open the link in a new or current tab.
+ previousMode = @mode
+ keyCode = event.keyCode
+
+ switch keyCode
+ when keyCodes.shiftKey
+ @setOpenLinkMode(if @mode is OPEN_IN_CURRENT_TAB then OPEN_IN_NEW_BG_TAB else OPEN_IN_CURRENT_TAB)
+ when keyCodes.ctrlKey
+ @setOpenLinkMode(if @mode is OPEN_IN_NEW_FG_TAB then OPEN_IN_NEW_BG_TAB else OPEN_IN_NEW_FG_TAB)
+
+ handlerStack.push
+ keyup: (event) =>
+ if event.keyCode == keyCode
+ handlerStack.remove()
+ @setOpenLinkMode previousMode if @isActive
+
+ else if event.keyCode in [ keyCodes.backspace, keyCodes.deleteKey ]
+ if @markerMatcher.popKeyChar()
+ @updateVisibleMarkers hintMarkers
+ else
+ @deactivateMode()
+
+ else if event.keyCode == keyCodes.enter
+ # Activate the active hint, if there is one. Only FilterHints uses an active hint.
+ @activateLink @markerMatcher.activeHintMarker if @markerMatcher.activeHintMarker
- if ((event.keyCode == keyCodes.shiftKey or event.keyCode == keyCodes.ctrlKey) and
- (@mode == OPEN_IN_CURRENT_TAB or
- @mode == OPEN_WITH_QUEUE or
- @mode == OPEN_IN_NEW_BG_TAB or
- @mode == OPEN_IN_NEW_FG_TAB))
- # Toggle whether to open the link in a new or current tab.
- previousMode = @mode
- keyCode = event.keyCode
-
- switch keyCode
- when keyCodes.shiftKey
- @setOpenLinkMode(if @mode is OPEN_IN_CURRENT_TAB then OPEN_IN_NEW_BG_TAB else OPEN_IN_CURRENT_TAB)
- when keyCodes.ctrlKey
- @setOpenLinkMode(if @mode is OPEN_IN_NEW_FG_TAB then OPEN_IN_NEW_BG_TAB else OPEN_IN_NEW_FG_TAB)
-
- handlerStack.push
- keyup: (event) =>
- if event.keyCode == keyCode
- handlerStack.remove()
- @setOpenLinkMode previousMode if @isActive
- true
-
- # TODO(philc): Ignore keys that have modifiers.
- if (KeyboardUtils.isEscape(event))
- DomUtils.suppressKeyupAfterEscape handlerStack
+ else if event.keyCode == keyCodes.tab
+ @tabCount = previousTabCount + (if event.shiftKey then -1 else 1)
+ @updateVisibleMarkers hintMarkers, @tabCount
+
+ else
+ return
+
+ # We've handled the event, so suppress it.
+ DomUtils.suppressEvent event
+
+ # Handles normal input.
+ onKeyPressInMode: (hintMarkers, event) ->
+ return if @delayMode or event.repeat
+
+ keyChar = String.fromCharCode(event.charCode).toLowerCase()
+ if keyChar
+ @markerMatcher.pushKeyChar keyChar, @keydownKeyChar
+ @updateVisibleMarkers hintMarkers
+
+ # We've handled the event, so suppress it.
+ DomUtils.suppressEvent event
+
+ updateVisibleMarkers: (hintMarkers, tabCount = 0) ->
+ keyResult = @markerMatcher.getMatchingHints hintMarkers, tabCount
+ linksMatched = keyResult.linksMatched
+ if linksMatched.length == 0
@deactivateMode()
- else if (event.keyCode != keyCodes.shiftKey and event.keyCode != keyCodes.ctrlKey)
- keyResult = @getMarkerMatcher().matchHintsByKey(hintMarkers, event)
- linksMatched = keyResult.linksMatched
- delay = keyResult.delay ? 0
- if (linksMatched.length == 0)
- @deactivateMode()
- else if (linksMatched.length == 1)
- @activateLink(linksMatched[0], delay)
- else
- for marker in hintMarkers
- @hideMarker(marker)
- for matched in linksMatched
- @showMarker(matched, @getMarkerMatcher().hintKeystrokeQueue.length)
- false # We've handled this key, so prevent propagation.
+ else if linksMatched.length == 1
+ @activateLink linksMatched[0], keyResult.delay ? 0
+ else
+ @hideMarker marker for marker in hintMarkers
+ @showMarker matched, @markerMatcher.hintKeystrokeQueue.length for matched in linksMatched
#
# When only one link hint remains, this function activates it in the appropriate way.
#
- activateLink: (matchedLink, delay) ->
+ activateLink: (matchedLink, delay = 0) ->
@delayMode = true
clickEl = matchedLink.clickableItem
if (DomUtils.isSelectable(clickEl))
DomUtils.simulateSelect(clickEl)
- @deactivateMode(delay, -> LinkHints.delayMode = false)
+ @deactivateMode delay
else
# TODO figure out which other input elements should not receive focus
if (clickEl.nodeName.toLowerCase() == "input" and clickEl.type not in ["button", "submit"])
@@ -320,18 +347,15 @@ LinkHints =
DomUtils.flashRect(matchedLink.rect)
@linkActivator(clickEl)
if @mode is OPEN_WITH_QUEUE
- @deactivateMode delay, ->
- LinkHints.delayMode = false
- LinkHints.activateModeWithQueue()
+ @deactivateMode delay, -> LinkHints.activateModeWithQueue()
else
- @deactivateMode(delay, -> LinkHints.delayMode = false)
+ @deactivateMode delay
#
# Shows the marker, highlighting matchingCharCount characters.
#
showMarker: (linkMarker, matchingCharCount) ->
linkMarker.style.display = ""
- # TODO(philc):
for j in [0...linkMarker.childNodes.length]
if (j < matchingCharCount)
linkMarker.childNodes[j].classList.add("matchingCharacter")
@@ -340,37 +364,41 @@ LinkHints =
hideMarker: (linkMarker) -> linkMarker.style.display = "none"
- #
- # If called without arguments, it executes immediately. Othewise, it
- # executes after 'delay' and invokes 'callback' when it is finished.
- #
- deactivateMode: (delay, callback) ->
+ deactivateMode: (delay = 0, callback = null) ->
deactivate = =>
- if (LinkHints.getMarkerMatcher().deactivate)
- LinkHints.getMarkerMatcher().deactivate()
- if (LinkHints.hintMarkerContainingDiv)
- DomUtils.removeElement LinkHints.hintMarkerContainingDiv
- LinkHints.hintMarkerContainingDiv = null
- @hintMode.exit()
+ DomUtils.removeElement @hintMarkerContainingDiv if @hintMarkerContainingDiv
+ @hintMarkerContainingDiv = null
+ @markerMatcher = null
+ @isActive = false
+ @hintMode?.exit()
+ @hintMode = null
@onExit?()
@onExit = null
- @isActive = false
+ @tabCount = 0
- # we invoke the deactivate() function directly instead of using setTimeout(callback, 0) so that
- # deactivateMode can be tested synchronously
- if (!delay)
- deactivate()
- callback() if (callback)
- else
- setTimeout(->
+ if delay
+ Utils.setTimeout delay, ->
deactivate()
- callback() if callback
- delay)
+ callback?()
+ else
+ # We invoke deactivate() directly (instead of setting a timeout of 0) so that deactivateMode() can be
+ # tested synchronously.
+ deactivate()
+ callback?()
-alphabetHints =
- hintKeystrokeQueue: []
+# Use characters for hints, and do not filter links by their text.
+class AlphabetHints
logXOfBase: (x, base) -> Math.log(x) / Math.log(base)
+ constructor: ->
+ @linkHintCharacters = Settings.get "linkHintCharacters"
+ # We use the keyChar from keydown if the link-hint characters are all "a-z0-9". This is the default
+ # settings value, and preserves the legacy behavior (which always used keydown) for users which are
+ # familiar with that behavior. Otherwise, we use keyChar from keypress, which admits non-Latin
+ # characters. See #1722.
+ @useKeydown = /^[a-z0-9]*$/.test @linkHintCharacters
+ @hintKeystrokeQueue = []
+
fillInMarkers: (hintMarkers) ->
hintStrings = @hintStrings(hintMarkers.length)
for marker, idx in hintMarkers
@@ -384,27 +412,26 @@ alphabetHints =
# may be of different lengths.
#
hintStrings: (linkCount) ->
- linkHintCharacters = Settings.get("linkHintCharacters")
# Determine how many digits the link hints will require in the worst case. Usually we do not need
# all of these digits for every link single hint, so we can show shorter hints for a few of the links.
- digitsNeeded = Math.ceil(@logXOfBase(linkCount, linkHintCharacters.length))
+ digitsNeeded = Math.ceil(@logXOfBase(linkCount, @linkHintCharacters.length))
# Short hints are the number of hints we can possibly show which are (digitsNeeded - 1) digits in length.
shortHintCount = Math.floor(
- (Math.pow(linkHintCharacters.length, digitsNeeded) - linkCount) /
- linkHintCharacters.length)
+ (Math.pow(@linkHintCharacters.length, digitsNeeded) - linkCount) /
+ @linkHintCharacters.length)
longHintCount = linkCount - shortHintCount
hintStrings = []
if (digitsNeeded > 1)
for i in [0...shortHintCount]
- hintStrings.push(numberToHintString(i, linkHintCharacters, digitsNeeded - 1))
+ hintStrings.push(numberToHintString(i, @linkHintCharacters, digitsNeeded - 1))
- start = shortHintCount * linkHintCharacters.length
+ start = shortHintCount * @linkHintCharacters.length
for i in [start...(start + longHintCount)]
- hintStrings.push(numberToHintString(i, linkHintCharacters, digitsNeeded))
+ hintStrings.push(numberToHintString(i, @linkHintCharacters, digitsNeeded))
- @shuffleHints(hintStrings, linkHintCharacters.length)
+ @shuffleHints(hintStrings, @linkHintCharacters.length)
#
# This shuffles the given set of hints so that they're scattered -- hints starting with the same character
@@ -419,26 +446,22 @@ alphabetHints =
result = result.concat(bucket)
result
- matchHintsByKey: (hintMarkers, event) ->
- # If a shifted-character is typed, treat it as lowerase for the purposes of matching hints.
- keyChar = KeyboardUtils.getKeyChar(event).toLowerCase()
-
- if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey)
- if (!@hintKeystrokeQueue.pop())
- return { linksMatched: [] }
- else if keyChar
- @hintKeystrokeQueue.push(keyChar)
+ getMatchingHints: (hintMarkers) ->
+ matchString = @hintKeystrokeQueue.join ""
+ linksMatched: hintMarkers.filter (linkMarker) -> linkMarker.hintString.startsWith matchString
- matchString = @hintKeystrokeQueue.join("")
- linksMatched = hintMarkers.filter((linkMarker) -> linkMarker.hintString.indexOf(matchString) == 0)
- { linksMatched: linksMatched }
+ pushKeyChar: (keyChar, keydownKeyChar) ->
+ @hintKeystrokeQueue.push (if @useKeydown then keydownKeyChar else keyChar)
+ popKeyChar: -> @hintKeystrokeQueue.pop()
- deactivate: -> @hintKeystrokeQueue = []
-
-filterHints =
- hintKeystrokeQueue: []
- linkTextKeystrokeQueue: []
- labelMap: {}
+# Use numbers (usually) for hints, and also filter links by their text.
+class FilterHints
+ constructor: ->
+ @linkHintNumbers = Settings.get "linkHintNumbers"
+ @hintKeystrokeQueue = []
+ @linkTextKeystrokeQueue = []
+ @labelMap = {}
+ @activeHintMarker = null
#
# Generate a map of input element => label
@@ -455,7 +478,7 @@ filterHints =
@labelMap[forElement] = labelText
generateHintString: (linkHintNumber) ->
- (numberToHintString linkHintNumber + 1, Settings.get "linkHintNumbers").toUpperCase()
+ numberToHintString linkHintNumber + 1, @linkHintNumbers.toUpperCase()
generateLinkText: (element) ->
linkText = ""
@@ -496,74 +519,58 @@ filterHints =
marker.showLinkText = linkTextObject.show
@renderMarker(marker)
+ @activeHintMarker = hintMarkers[0]
+ @activeHintMarker?.classList.add "vimiumActiveHintMarker"
+
hintMarkers
- matchHintsByKey: (hintMarkers, event) ->
- keyChar = KeyboardUtils.getKeyChar(event)
+ getMatchingHints: (hintMarkers, tabCount = 0) ->
delay = 0
- userIsTypingLinkText = false
-
- if (event.keyCode == keyCodes.enter)
- # activate the lowest-numbered link hint that is visible
- for marker in hintMarkers
- if (marker.style.display != "none")
- return { linksMatched: [ marker ] }
- else if (event.keyCode == keyCodes.backspace || event.keyCode == keyCodes.deleteKey)
- # backspace clears hint key queue first, then acts on link text key queue.
- # if both queues are empty. exit hinting mode
- if (!@hintKeystrokeQueue.pop() && !@linkTextKeystrokeQueue.pop())
- return { linksMatched: [] }
- else if (keyChar)
- if (Settings.get("linkHintNumbers").indexOf(keyChar) >= 0)
- @hintKeystrokeQueue.push(keyChar)
- else
- # since we might renumber the hints, the current hintKeyStrokeQueue
- # should be rendered invalid (i.e. reset).
- @hintKeystrokeQueue = []
- @linkTextKeystrokeQueue.push(keyChar)
- userIsTypingLinkText = true
- # at this point, linkTextKeystrokeQueue and hintKeystrokeQueue have been updated to reflect the latest
+ # At this point, linkTextKeystrokeQueue and hintKeystrokeQueue have been updated to reflect the latest
# input. use them to filter the link hints accordingly.
- linksMatched = @filterLinkHints(hintMarkers)
- matchString = @hintKeystrokeQueue.join("")
- linksMatched = linksMatched.filter((linkMarker) ->
- !linkMarker.filtered && linkMarker.hintString.indexOf(matchString) == 0)
-
- if (linksMatched.length == 1 && userIsTypingLinkText)
- # In filter mode, people tend to type out words past the point
- # needed for a unique match. Hence we should avoid passing
- # control back to command mode immediately after a match is found.
+ matchString = @hintKeystrokeQueue.join ""
+ linksMatched = @filterLinkHints hintMarkers
+ linksMatched = linksMatched.filter (linkMarker) -> linkMarker.hintString.startsWith matchString
+
+ if linksMatched.length == 1 && @hintKeystrokeQueue.length == 0 and 0 < @linkTextKeystrokeQueue.length
+ # In filter mode, people tend to type out words past the point needed for a unique match. Hence we
+ # should avoid passing control back to command mode immediately after a match is found.
delay = 200
+ # Visually highlight of the active hint (that is, the one that will be activated if the user
+ # types <Enter>).
+ tabCount = ((linksMatched.length * Math.abs tabCount) + tabCount) % linksMatched.length
+ @activeHintMarker?.classList.remove "vimiumActiveHintMarker"
+ @activeHintMarker = linksMatched[tabCount]
+ @activeHintMarker?.classList.add "vimiumActiveHintMarker"
+
{ linksMatched: linksMatched, delay: delay }
- #
- # Marks the links that do not match the linkText search string with the 'filtered' DOM property. Renumbers
- # the remainder if necessary.
- #
- filterLinkHints: (hintMarkers) ->
- linksMatched = []
- linkSearchString = @linkTextKeystrokeQueue.join("")
+ pushKeyChar: (keyChar, keydownKeyChar) ->
+ # For filtered hints, we *always* use the keyChar value from keypress, because there is no obvious and
+ # easy-to-understand meaning for choosing one of keyChar or keydownKeyChar (as there is for alphabet
+ # hints).
+ if 0 <= @linkHintNumbers.indexOf keyChar
+ @hintKeystrokeQueue.push keyChar
+ else
+ # Since we might renumber the hints, we should reset the current hintKeyStrokeQueue.
+ @hintKeystrokeQueue = []
+ @linkTextKeystrokeQueue.push keyChar
- for linkMarker in hintMarkers
- matchedLink = linkMarker.linkText.toLowerCase().indexOf(linkSearchString.toLowerCase()) >= 0
+ popKeyChar: ->
+ @hintKeystrokeQueue.pop() or @linkTextKeystrokeQueue.pop()
- if (!matchedLink)
- linkMarker.filtered = true
- else
- linkMarker.filtered = false
- oldHintString = linkMarker.hintString
- linkMarker.hintString = @generateHintString(linksMatched.length)
- @renderMarker(linkMarker) if (linkMarker.hintString != oldHintString)
- linksMatched.push(linkMarker)
-
- linksMatched
+ # Filter link hints by search string, renumbering the hints as necessary.
+ filterLinkHints: (hintMarkers) ->
+ idx = 0
+ linkSearchString = @linkTextKeystrokeQueue.join("").toLowerCase()
- deactivate: (delay, callback) ->
- @hintKeystrokeQueue = []
- @linkTextKeystrokeQueue = []
- @labelMap = {}
+ for linkMarker in hintMarkers
+ continue unless 0 <= linkMarker.linkText.toLowerCase().indexOf linkSearchString
+ linkMarker.hintString = @generateHintString idx++
+ @renderMarker linkMarker
+ linkMarker
#
# Make each hint character a span, so that we can highlight the typed characters as you type them.
diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee
index ffabc111..508b1b2c 100644
--- a/content_scripts/mode.coffee
+++ b/content_scripts/mode.coffee
@@ -101,6 +101,12 @@ class Mode
"focus": (event) => @alwaysContinueBubbling =>
@exit event if DomUtils.isFocusable event.target
+ # If @options.exitOnScroll is truthy, then the mode will exit on any scroll event.
+ if @options.exitOnScroll
+ @push
+ _name: "mode-#{@id}/exitOnScroll"
+ "scroll": (event) => @alwaysContinueBubbling => @exit event
+
# Some modes are singletons: there may be at most one instance active at any time. A mode is a singleton
# if @options.singleton is truthy. The value of @options.singleton should be the key which is intended to
# be unique. New instances deactivate existing instances with the same key.
diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css
index b4bce776..38a968fc 100644
--- a/content_scripts/vimium.css
+++ b/content_scripts/vimium.css
@@ -90,6 +90,10 @@ div.internalVimiumHintMarker > .matchingCharacter {
color: #D4AC3A;
}
+div > .vimiumActiveHintMarker span {
+ color: #A07555 !important;
+}
+
/* Input hints CSS */
div.internalVimiumInputHint {
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index 3055ecea..8c28b4e6 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -192,7 +192,7 @@ window.installListeners = ->
unless installedListeners
# Key event handlers fire on window before they do on document. Prefer window for key events so the page
# can't set handlers to grab the keys before us.
- for type in [ "keydown", "keypress", "keyup", "click", "focus", "blur", "mousedown" ]
+ for type in [ "keydown", "keypress", "keyup", "click", "focus", "blur", "mousedown", "scroll" ]
do (type) -> installListener window, type, (event) -> handlerStack.bubbleEvent type, event
installListener document, "DOMActivate", (event) -> handlerStack.bubbleEvent 'DOMActivate', event
installedListeners = true
diff --git a/tests/dom_tests/dom_tests.coffee b/tests/dom_tests/dom_tests.coffee
index 8f293075..dd2f5a5d 100644
--- a/tests/dom_tests/dom_tests.coffee
+++ b/tests/dom_tests/dom_tests.coffee
@@ -67,27 +67,27 @@ createGeneralHintTests = (isFilteredMode) ->
document.getElementById("test-div").innerHTML = ""
should "create hints when activated, discard them when deactivated", ->
- LinkHints.activateMode()
- assert.isFalse not LinkHints.hintMarkerContainingDiv?
- LinkHints.deactivateMode()
- assert.isTrue not LinkHints.hintMarkerContainingDiv?
+ linkHints = LinkHints.activateMode()
+ assert.isFalse not linkHints.hintMarkerContainingDiv?
+ linkHints.deactivateMode()
+ assert.isTrue not linkHints.hintMarkerContainingDiv?
should "position items correctly", ->
assertStartPosition = (element1, element2) ->
assert.equal element1.getClientRects()[0].left, element2.getClientRects()[0].left
assert.equal element1.getClientRects()[0].top, element2.getClientRects()[0].top
stub document.body, "style", "static"
- LinkHints.activateMode()
+ linkHints = LinkHints.activateMode()
hintMarkers = getHintMarkers()
assertStartPosition document.getElementsByTagName("a")[0], hintMarkers[0]
assertStartPosition document.getElementsByTagName("a")[1], hintMarkers[1]
- LinkHints.deactivateMode()
+ linkHints.deactivateMode()
stub document.body.style, "position", "relative"
- LinkHints.activateMode()
+ linkHints = LinkHints.activateMode()
hintMarkers = getHintMarkers()
assertStartPosition document.getElementsByTagName("a")[0], hintMarkers[0]
assertStartPosition document.getElementsByTagName("a")[1], hintMarkers[1]
- LinkHints.deactivateMode()
+ linkHints.deactivateMode()
createGeneralHintTests false
createGeneralHintTests true
@@ -142,10 +142,10 @@ context "Alphabetical link hints",
# Three hints will trigger double hint chars.
createLinks 3
- LinkHints.activateMode()
+ @linkHints = LinkHints.activateMode()
tearDown ->
- LinkHints.deactivateMode()
+ @linkHints.deactivateMode()
document.getElementById("test-div").innerHTML = ""
should "label the hints correctly", ->
@@ -177,11 +177,11 @@ context "Filtered link hints",
initializeModeState()
testContent = "<a>test</a>" + "<a>tress</a>" + "<a>trait</a>" + "<a>track<img alt='alt text'/></a>"
document.getElementById("test-div").innerHTML = testContent
- LinkHints.activateMode()
+ @linkHints = LinkHints.activateMode()
tearDown ->
document.getElementById("test-div").innerHTML = ""
- LinkHints.deactivateMode()
+ @linkHints.deactivateMode()
should "label the hints", ->
hintMarkers = getHintMarkers()
@@ -205,11 +205,11 @@ context "Filtered link hints",
testContent = "<a><img alt='alt text'/></a><a><img alt='alt text' title='some title'/></a>
<a><img title='some title'/></a>" + "<a><img src='' width='320px' height='100px'/></a>"
document.getElementById("test-div").innerHTML = testContent
- LinkHints.activateMode()
+ @linkHints = LinkHints.activateMode()
tearDown ->
document.getElementById("test-div").innerHTML = ""
- LinkHints.deactivateMode()
+ @linkHints.deactivateMode()
should "label the images", ->
hintMarkers = getHintMarkers()
@@ -227,11 +227,11 @@ context "Filtered link hints",
<input type='text' id='test-input' value='some value'/>
<label for='test-input-2'/>a label: </label><input type='text' id='test-input-2' value='some value'/>"
document.getElementById("test-div").innerHTML = testContent
- LinkHints.activateMode()
+ @linkHints = LinkHints.activateMode()
tearDown ->
document.getElementById("test-div").innerHTML = ""
- LinkHints.deactivateMode()
+ @linkHints.deactivateMode()
should "label the input elements", ->
hintMarkers = getHintMarkers()