aboutsummaryrefslogtreecommitdiffstats
path: root/content_scripts/link_hints.coffee
diff options
context:
space:
mode:
Diffstat (limited to 'content_scripts/link_hints.coffee')
-rw-r--r--content_scripts/link_hints.coffee199
1 files changed, 88 insertions, 111 deletions
diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee
index 0014e20a..0592c96d 100644
--- a/content_scripts/link_hints.coffee
+++ b/content_scripts/link_hints.coffee
@@ -31,7 +31,7 @@ COPY_LINK_URL =
indicator: "Copy link URL to Clipboard"
linkActivator: (link) ->
if link.href?
- chrome.runtime.sendMessage handler: "copyToClipboard", data: link.href
+ HUD.copyToClipboard link.href
url = link.href
url = url[0..25] + "...." if 28 < url.length
HUD.showForDuration "Yanked #{url}", 2000
@@ -128,7 +128,7 @@ LinkHints =
if isSuccess
# Wait for the next tick to allow the previous mode to exit. It might yet generate a click event,
# which would cause our new mode to exit immediately.
- Utils.nextTick -> LinkHints.activateMode count-1, mode
+ Utils.nextTick -> LinkHints.activateMode count-1, {mode}
activateModeToOpenInNewTab: (count) -> @activateMode count, mode: OPEN_IN_NEW_BG_TAB
activateModeToOpenInNewForegroundTab: (count) -> @activateMode count, mode: OPEN_IN_NEW_FG_TAB
@@ -166,17 +166,15 @@ class LinkHintsMode
name: "hint/#{@mode.name}"
indicator: false
singleton: "link-hints-mode"
- passInitialKeyupEvents: true
suppressAllKeyboardEvents: true
suppressTrailingKeyEvents: true
exitOnEscape: true
exitOnClick: true
keydown: @onKeyDownInMode.bind this
- keypress: @onKeyPressInMode.bind this
@hintMode.onExit (event) =>
if event?.type == "click" or (event?.type == "keydown" and
- (KeyboardUtils.isEscape(event) or event.keyCode in [keyCodes.backspace, keyCodes.deleteKey]))
+ (KeyboardUtils.isEscape(event) or KeyboardUtils.isBackspace event))
HintCoordinator.sendMessage "exit", isSuccess: false
# Note(philc): Append these markers as top level children instead of as child nodes to the link itself,
@@ -230,86 +228,72 @@ class LinkHintsMode
linkText: desc.linkText
stableSortCount: ++@stableSortCount
- # Handles <Shift> and <Ctrl>.
+ # Handles all keyboard events.
onKeyDownInMode: (event) ->
return if event.repeat
- @keydownKeyChar = KeyboardUtils.getKeyChar(event).toLowerCase()
- previousTabCount = @tabCount
- @tabCount = 0
-
- # NOTE(smblott) As of 1.54, the Ctrl modifier doesn't work for filtered link hints; therefore we only
- # offer the control modifier for alphabet hints. It is not clear whether we should fix this. As of
- # 16-03-28, nobody has complained.
- modifiers = [keyCodes.shiftKey]
- modifiers.push keyCodes.ctrlKey unless Settings.get "filterLinkHints"
-
- if event.keyCode in modifiers and
+ # NOTE(smblott) The modifier behaviour here applies only to alphabet hints.
+ if event.key in ["Control", "Shift"] and not Settings.get("filterLinkHints") 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
+ key = event.key
- switch keyCode
- when keyCodes.shiftKey
+ switch key
+ when "Shift"
@setOpenLinkMode(if @mode is OPEN_IN_CURRENT_TAB then OPEN_IN_NEW_BG_TAB else OPEN_IN_CURRENT_TAB)
- when keyCodes.ctrlKey
+ when "Control"
@setOpenLinkMode(if @mode is OPEN_IN_NEW_FG_TAB then OPEN_IN_NEW_BG_TAB else OPEN_IN_NEW_FG_TAB)
- handlerId = handlerStack.push
+ handlerId = @hintMode.push
keyup: (event) =>
- if event.keyCode == keyCode
+ if event.key == key
handlerStack.remove()
@setOpenLinkMode previousMode
true # Continue bubbling the event.
- # For some (unknown) reason, we don't always receive the keyup event needed to remove this handler.
- # Therefore, we ensure that it's always removed when hint mode exits. See #1911 and #1926.
- @hintMode.onExit -> handlerStack.remove handlerId
-
- else if event.keyCode in [ keyCodes.backspace, keyCodes.deleteKey ]
+ else if KeyboardUtils.isBackspace event
if @markerMatcher.popKeyChar()
+ @tabCount = 0
@updateVisibleMarkers()
else
# Exit via @hintMode.exit(), so that the LinkHints.activate() "onExit" callback sees the key event and
# knows not to restart hints mode.
@hintMode.exit event
- else if event.keyCode == keyCodes.enter
+ else if event.key == "Enter"
# Activate the active hint, if there is one. Only FilterHints uses an active hint.
HintCoordinator.sendMessage "activateActiveHintMarker" if @markerMatcher.activeHintMarker
- else if event.keyCode == keyCodes.tab
- @tabCount = previousTabCount + (if event.shiftKey then -1 else 1)
- @updateVisibleMarkers @tabCount
+ else if event.key == "Tab"
+ if event.shiftKey then @tabCount-- else @tabCount++
+ @updateVisibleMarkers()
- else if event.keyCode == keyCodes.space and @markerMatcher.shouldRotateHints event
- @tabCount = previousTabCount
+ else if event.key == " " and @markerMatcher.shouldRotateHints event
HintCoordinator.sendMessage "rotateHints"
else
- @tabCount = previousTabCount if event.ctrlKey or event.metaKey or event.altKey
- return
-
- # We've handled the event, so suppress it and update the mode indicator.
- DomUtils.suppressEvent event
-
- # Handles normal input.
- onKeyPressInMode: (event) ->
- return if event.repeat
-
- keyChar = String.fromCharCode(event.charCode).toLowerCase()
- if keyChar
- @markerMatcher.pushKeyChar keyChar, @keydownKeyChar
- @updateVisibleMarkers()
+ unless event.repeat
+ keyChar =
+ if Settings.get "filterLinkHints"
+ KeyboardUtils.getKeyChar(event)
+ else
+ KeyboardUtils.getKeyChar(event).toLowerCase()
+ if keyChar
+ keyChar = " " if keyChar == "space"
+ if keyChar.length == 1
+ @tabCount = 0
+ @markerMatcher.pushKeyChar keyChar
+ @updateVisibleMarkers()
+ else
+ return handlerStack.suppressPropagation
- # We've handled the event, so suppress it.
- DomUtils.suppressEvent event
+ handlerStack.suppressEvent
- updateVisibleMarkers: (tabCount = 0) ->
+ updateVisibleMarkers: ->
{hintKeystrokeQueue, linkTextKeystrokeQueue} = @markerMatcher
- HintCoordinator.sendMessage "updateKeyState", {hintKeystrokeQueue, linkTextKeystrokeQueue, tabCount}
+ HintCoordinator.sendMessage "updateKeyState",
+ {hintKeystrokeQueue, linkTextKeystrokeQueue, tabCount: @tabCount}
updateKeyState: ({hintKeystrokeQueue, linkTextKeystrokeQueue, tabCount}) ->
extend @markerMatcher, {hintKeystrokeQueue, linkTextKeystrokeQueue}
@@ -318,7 +302,7 @@ class LinkHintsMode
if linksMatched.length == 0
@deactivateMode()
else if linksMatched.length == 1
- @activateLink linksMatched[0], userMightOverType ? false
+ @activateLink linksMatched[0], userMightOverType
else
@hideMarker marker for marker in @hintMarkers
@showMarker matched, @markerMatcher.hintKeystrokeQueue.length for matched in linksMatched
@@ -329,7 +313,7 @@ class LinkHintsMode
rotateHints: do ->
markerOverlapsStack = (marker, stack) ->
for otherMarker in stack
- return true if Rect.rectsOverlap marker.markerRect, otherMarker.markerRect
+ return true if Rect.intersects marker.markerRect, otherMarker.markerRect
false
->
@@ -372,7 +356,7 @@ class LinkHintsMode
# When only one hint remains, activate it in the appropriate way. The current frame may or may not contain
# the matched link, and may or may not have the focus. The resulting four cases are accounted for here by
# selectively pushing the appropriate HintCoordinator.onExit handlers.
- activateLink: (linkMatched, userMightOverType=false) ->
+ activateLink: (linkMatched, userMightOverType = false) ->
@removeHintMarkers()
if linkMatched.isLocalMarker
@@ -398,25 +382,26 @@ class LinkHintsMode
clickEl.focus()
linkActivator clickEl
- installKeyboardBlocker = (startKeyboardBlocker) ->
- if linkMatched.isLocalMarker
- {top: viewportTop, left: viewportLeft} = DomUtils.getViewportTopLeft()
- for rect in (Rect.copy rect for rect in clickEl.getClientRects())
- extend rect, top: rect.top + viewportTop, left: rect.left + viewportLeft
- flashEl = DomUtils.addFlashRect rect
- do (flashEl) -> HintCoordinator.onExit.push -> DomUtils.removeElement flashEl
-
- if windowIsFocused()
- startKeyboardBlocker (isSuccess) -> HintCoordinator.sendMessage "exit", {isSuccess}
+ # If flash elements are created, then this function can be used later to remove them.
+ removeFlashElements = ->
+ if linkMatched.isLocalMarker
+ {top: viewportTop, left: viewportLeft} = DomUtils.getViewportTopLeft()
+ flashElements = for rect in clickEl.getClientRects()
+ DomUtils.addFlashRect Rect.translate rect, viewportLeft, viewportTop
+ removeFlashElements = -> DomUtils.removeElement flashEl for flashEl in flashElements
# If we're using a keyboard blocker, then the frame with the focus sends the "exit" message, otherwise the
# frame containing the matched link does.
- if userMightOverType and Settings.get "waitForEnterForFilteredHints"
- installKeyboardBlocker (callback) -> new WaitForEnter callback
- else if userMightOverType
- installKeyboardBlocker (callback) -> new TypingProtector 200, callback
+ if userMightOverType
+ HintCoordinator.onExit.push removeFlashElements
+ if windowIsFocused()
+ callback = (isSuccess) -> HintCoordinator.sendMessage "exit", {isSuccess}
+ if Settings.get "waitForEnterForFilteredHints"
+ new WaitForEnter callback
+ else
+ new TypingProtector 200, callback
else if linkMatched.isLocalMarker
- DomUtils.flashRect linkMatched.rect
+ Utils.setTimeout 400, removeFlashElements
HintCoordinator.sendMessage "exit", isSuccess: true
#
@@ -444,12 +429,7 @@ class LinkHintsMode
# Use characters for hints, and do not filter links by their text.
class AlphabetHints
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
+ @linkHintCharacters = Settings.get("linkHintCharacters").toLowerCase()
@hintKeystrokeQueue = []
fillInMarkers: (hintMarkers) ->
@@ -478,17 +458,17 @@ class AlphabetHints
matchString = @hintKeystrokeQueue.join ""
linksMatched: hintMarkers.filter (linkMarker) -> linkMarker.hintString.startsWith matchString
- pushKeyChar: (keyChar, keydownKeyChar) ->
- @hintKeystrokeQueue.push (if @useKeydown then keydownKeyChar else keyChar)
+ pushKeyChar: (keyChar) ->
+ @hintKeystrokeQueue.push keyChar
popKeyChar: -> @hintKeystrokeQueue.pop()
# For alphabet hints, <Space> always rotates the hints, regardless of modifiers.
shouldRotateHints: -> true
-# Use numbers (usually) for hints, and also filter links by their text.
+# Use characters for hints, and also filter links by their text.
class FilterHints
constructor: ->
- @linkHintNumbers = Settings.get "linkHintNumbers"
+ @linkHintNumbers = Settings.get("linkHintNumbers").toUpperCase()
@hintKeystrokeQueue = []
@linkTextKeystrokeQueue = []
@activeHintMarker = null
@@ -535,17 +515,18 @@ class FilterHints
linksMatched: linksMatched
userMightOverType: @hintKeystrokeQueue.length == 0 and 0 < @linkTextKeystrokeQueue.length
- 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).
+ pushKeyChar: (keyChar) ->
if 0 <= @linkHintNumbers.indexOf keyChar
@hintKeystrokeQueue.push keyChar
+ else if keyChar.toLowerCase() != keyChar and @linkHintNumbers.toLowerCase() != @linkHintNumbers.toUpperCase()
+ # The the keyChar is upper case and the link hint "numbers" contain characters (e.g. [a-zA-Z]). We don't want
+ # some upper-case letters matching hints (above) and some matching text (below), so we ignore such keys.
+ return
# We only accept <Space> and characters which are not used for splitting (e.g. "a", "b", etc., but not "-").
else if keyChar == " " or not @splitRegexp.test keyChar
# Since we might renumber the hints, we should reset the current hintKeyStrokeQueue.
@hintKeystrokeQueue = []
- @linkTextKeystrokeQueue.push keyChar
+ @linkTextKeystrokeQueue.push keyChar.toLowerCase()
popKeyChar: ->
@hintKeystrokeQueue.pop() or @linkTextKeystrokeQueue.pop()
@@ -626,7 +607,9 @@ LocalHints =
# image), therefore we always return a array of element/rect pairs (which may also be a singleton or empty).
#
getVisibleClickable: (element) ->
- tagName = element.tagName.toLowerCase()
+ # Get the tag name. However, `element.tagName` can be an element (not a string, see #2305), so we guard
+ # against that.
+ tagName = element.tagName.toLowerCase?() ? ""
isClickable = false
onlyHasTabIndex = false
possibleFalsePositive = false
@@ -668,9 +651,12 @@ LocalHints =
isClickable ||= @checkForAngularJs element
# Check for attributes that make an element clickable regardless of its tagName.
- if (element.hasAttribute("onclick") or
- element.getAttribute("role")?.toLowerCase() in ["button", "link"] or
- element.getAttribute("contentEditable")?.toLowerCase() in ["", "contentEditable", "true"])
+ if element.hasAttribute("onclick") or
+ (role = element.getAttribute "role") and role.toLowerCase() in [
+ "button" , "tab" , "link", "checkbox", "menuitem", "menuitemcheckbox", "menuitemradio"
+ ] or
+ (contentEditable = element.getAttribute "contentEditable") and
+ contentEditable.toLowerCase() in ["", "contenteditable", "true"]
isClickable = true
# Check for jsaction event listeners on the element.
@@ -821,25 +807,10 @@ LocalHints =
hint.rect.left += left
if Settings.get "filterLinkHints"
- @withLabelMap (labelMap) =>
- extend hint, @generateLinkText labelMap, hint for hint in localHints
+ extend hint, @generateLinkText hint for hint in localHints
localHints
- # Generate a map of input element => label text, call a callback with it.
- withLabelMap: (callback) ->
- labelMap = {}
- labels = document.querySelectorAll "label"
- for label in labels
- forElement = label.getAttribute "for"
- if forElement
- labelText = label.textContent.trim()
- # Remove trailing ":" commonly found in labels.
- if labelText[labelText.length-1] == ":"
- labelText = labelText.substr 0, labelText.length-1
- labelMap[forElement] = labelText
- callback labelMap
-
- generateLinkText: (labelMap, hint) ->
+ generateLinkText: (hint) ->
element = hint.element
linkText = ""
showLinkText = false
@@ -847,9 +818,14 @@ LocalHints =
nodeName = element.nodeName.toLowerCase()
if nodeName == "input"
- if labelMap[element.id]
- linkText = labelMap[element.id]
+ if element.labels? and element.labels.length > 0
+ linkText = element.labels[0].textContent.trim()
+ # Remove trailing ":" commonly found in labels.
+ if linkText[linkText.length-1] == ":"
+ linkText = linkText[...linkText.length-1]
showLinkText = true
+ else if element.getAttribute("type")?.toLowerCase() == "file"
+ linkText = "Choose File"
else if element.type != "password"
linkText = element.value
if not linkText and 'placeholder' of element
@@ -899,15 +875,16 @@ class WaitForEnter extends Mode
@push
keydown: (event) =>
- if event.keyCode == keyCodes.enter
+ if event.key == "Enter"
@exit()
callback true # true -> isSuccess.
else if KeyboardUtils.isEscape event
@exit()
callback false # false -> isSuccess.
-root = exports ? window
+root = exports ? (window.root ?= {})
root.LinkHints = LinkHints
root.HintCoordinator = HintCoordinator
# For tests:
extend root, {LinkHintsMode, LocalHints, AlphabetHints, WaitForEnter}
+extend window, root unless exports?