diff options
Diffstat (limited to 'content_scripts/link_hints.coffee')
| -rw-r--r-- | content_scripts/link_hints.coffee | 199 |
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? |
