diff options
Diffstat (limited to 'content_scripts')
| -rw-r--r-- | content_scripts/hud.coffee | 38 | ||||
| -rw-r--r-- | content_scripts/link_hints.coffee | 84 | ||||
| -rw-r--r-- | content_scripts/marks.coffee | 7 | ||||
| -rw-r--r-- | content_scripts/mode.coffee | 18 | ||||
| -rw-r--r-- | content_scripts/mode_find.coffee | 66 | ||||
| -rw-r--r-- | content_scripts/mode_insert.coffee | 6 | ||||
| -rw-r--r-- | content_scripts/mode_key_handler.coffee | 13 | ||||
| -rw-r--r-- | content_scripts/mode_normal.coffee | 369 | ||||
| -rw-r--r-- | content_scripts/mode_visual.coffee | 5 | ||||
| -rw-r--r-- | content_scripts/scroller.coffee | 12 | ||||
| -rw-r--r-- | content_scripts/ui_component.coffee | 3 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 406 | ||||
| -rw-r--r-- | content_scripts/vomnibar.coffee | 3 |
13 files changed, 564 insertions, 466 deletions
diff --git a/content_scripts/hud.coffee b/content_scripts/hud.coffee index c2170914..42a960da 100644 --- a/content_scripts/hud.coffee +++ b/content_scripts/hud.coffee @@ -9,6 +9,8 @@ HUD = findMode: null abandon: -> @hudUI?.hide false + pasteListener: null # Set by @pasteFromClipboard to handle the value returned by pasteResponse + # This HUD is styled to precisely mimick the chrome HUD on Mac. Use the "has_popup_and_link_hud.html" # test harness to tweak these styles to match Chrome's. One limitation of our HUD display is that # it doesn't sit on top of horizontal scrollbars like Chrome's HUD does. @@ -71,17 +73,44 @@ HUD = focusNode?.focus?() if exitEventIsEnter - handleEnterForFindMode() + FindMode.handleEnter() if FindMode.query.hasResults postExit = -> new PostFindMode else if exitEventIsEscape - # We don't want FindMode to handle the click events that handleEscapeForFindMode can generate, so we + # We don't want FindMode to handle the click events that FindMode.handleEscape can generate, so we # wait until the mode is closed before running it. - postExit = handleEscapeForFindMode + postExit = FindMode.handleEscape @findMode.exit() postExit?() + # These commands manage copying and pasting from the clipboard in the HUD frame. + # NOTE(mrmr1993): We need this to copy and paste on Firefox: + # * an element can't be focused in the background page, so copying/pasting doesn't work + # * we don't want to disrupt the focus in the page, in case the page is listening for focus/blur events. + # * the HUD shouldn't be active for this frame while any of the copy/paste commands are running. + copyToClipboard: (text) -> + DomUtils.documentComplete => + @init() + @hudUI?.postMessage {name: "copyToClipboard", data: text} + + pasteFromClipboard: (@pasteListener) -> + DomUtils.documentComplete => + @init() + # Show the HUD frame, so Firefox will actually perform the paste. + @hudUI.toggleIframeElementClasses "vimiumUIComponentHidden", "vimiumUIComponentVisible" + @tween.fade 0, 0 + @hudUI.postMessage {name: "pasteFromClipboard"} + + pasteResponse: ({data}) -> + # Hide the HUD frame again. + @hudUI.toggleIframeElementClasses "vimiumUIComponentVisible", "vimiumUIComponentHidden" + @unfocusIfFocused() + @pasteListener data + + unfocusIfFocused: -> + document.activeElement.blur() if document.activeElement == @hudUI?.iframeElement + class Tween opacity: 0 intervalId: -1 @@ -127,5 +156,6 @@ class Tween } """ -root = exports ? window +root = exports ? (window.root ?= {}) root.HUD = HUD +extend window, root unless exports? diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index d9ce5d06..d4a95d36 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 @@ -166,7 +166,6 @@ class LinkHintsMode name: "hint/#{@mode.name}" indicator: false singleton: "link-hints-mode" - passInitialKeyupEvents: true suppressAllKeyboardEvents: true suppressTrailingKeyEvents: true exitOnEscape: true @@ -233,13 +232,9 @@ class LinkHintsMode onKeyDownInMode: (event) -> return if event.repeat - previousTabCount = @tabCount - @tabCount = 0 - # 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 key = event.key @@ -250,19 +245,16 @@ class LinkHintsMode 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.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 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 @@ -274,15 +266,13 @@ class LinkHintsMode HintCoordinator.sendMessage "activateActiveHintMarker" if @markerMatcher.activeHintMarker else if event.key == "Tab" - @tabCount = previousTabCount + (if event.shiftKey then -1 else 1) - @updateVisibleMarkers @tabCount + if event.shiftKey then @tabCount-- else @tabCount++ + @updateVisibleMarkers() else if event.key == " " and @markerMatcher.shouldRotateHints event - @tabCount = previousTabCount HintCoordinator.sendMessage "rotateHints" else - @tabCount = previousTabCount if event.ctrlKey or event.metaKey or event.altKey unless event.repeat keyChar = if Settings.get "filterLinkHints" @@ -292,17 +282,18 @@ class LinkHintsMode if keyChar keyChar = " " if keyChar == "space" if keyChar.length == 1 + @tabCount = 0 @markerMatcher.pushKeyChar keyChar @updateVisibleMarkers() - DomUtils.consumeKeyup event - return + else + return handlerStack.suppressPropagation - # We've handled the event, so suppress it and update the mode indicator. - 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} @@ -311,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 @@ -322,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 -> @@ -365,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 @@ -391,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 # @@ -659,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. @@ -841,6 +836,8 @@ LocalHints = if labelMap[element.id] linkText = labelMap[element.id] 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 @@ -897,8 +894,9 @@ class WaitForEnter extends Mode @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? diff --git a/content_scripts/marks.coffee b/content_scripts/marks.coffee index 6eab3be6..ac653a52 100644 --- a/content_scripts/marks.coffee +++ b/content_scripts/marks.coffee @@ -52,7 +52,7 @@ Marks = else localStorage[@getLocationKey keyChar] = @getMarkString() @showMessage "Created local mark", keyChar - DomUtils.consumeKeyup event + handlerStack.suppressEvent activateGotoMode: -> @mode = new Mode @@ -82,7 +82,8 @@ Marks = @showMessage "Jumped to local mark", keyChar else @showMessage "Local mark not set", keyChar - DomUtils.consumeKeyup event + handlerStack.suppressEvent -root = exports ? window +root = exports ? (window.root ?= {}) root.Marks = Marks +extend window, root unless exports? diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index 9de423ff..a4a91c1f 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -55,7 +55,7 @@ class Mode # the need for modes which suppress all keyboard events 1) to provide handlers for all of those events, # or 2) to worry about event suppression and event-handler return values. if @options.suppressAllKeyboardEvents - for type in [ "keydown", "keypress", "keyup" ] + for type in [ "keydown", "keypress" ] do (handler = @options[type]) => @options[type] = (event) => @alwaysSuppressPropagation => handler? event @@ -82,7 +82,7 @@ class Mode "keydown": (event) => return @continueBubbling unless KeyboardUtils.isEscape event @exit event, event.target - DomUtils.consumeKeyup event + @suppressEvent # If @options.exitOnBlur is truthy, then it should be an element. The mode will exit when that element # loses the focus. @@ -120,16 +120,6 @@ class Mode singletons[key]?.exit() singletons[key] = this - # If @options.passInitialKeyupEvents is set, then we pass initial non-printable keyup events to the page - # or to other extensions (because the corresponding keydown events were passed). This is used when - # activating link hints, see #1522. - if @options.passInitialKeyupEvents - @push - _name: "mode-#{@id}/passInitialKeyupEvents" - keydown: => @alwaysContinueBubbling -> handlerStack.remove() - keyup: (event) => - if KeyboardUtils.isPrintable event then @suppressPropagation else @passEventToPage - # if @options.suppressTrailingKeyEvents is set, then -- on exit -- we suppress all key events until a # subsquent (non-repeat) keydown or keypress. In particular, the intention is to catch keyup events for # keys which we have handled, but which otherwise might trigger page actions (if the page is listening for @@ -147,7 +137,6 @@ class Mode name: "suppress-trailing-key-events" keydown: handler keypress: handler - keyup: -> handlerStack.suppressPropagation Mode.modes.push this @setIndicator() @@ -209,5 +198,6 @@ class SuppressAllKeyboardEvents extends Mode suppressAllKeyboardEvents: true super extend defaults, options -root = exports ? window +root = exports ? (window.root ?= {}) extend root, {Mode, SuppressAllKeyboardEvents} +extend window, root unless exports? diff --git a/content_scripts/mode_find.coffee b/content_scripts/mode_find.coffee index 5a2da741..f19b5db4 100644 --- a/content_scripts/mode_find.coffee +++ b/content_scripts/mode_find.coffee @@ -6,7 +6,7 @@ class SuppressPrintable extends Mode constructor: (options) -> super options handler = (event) => if KeyboardUtils.isPrintable event then @suppressEvent else @continueBubbling - type = document.getSelection().type + type = DomUtils.getSelectionType() # We use unshift here, so we see events after normal mode, so we only see unmapped keys. @unshift @@ -16,7 +16,7 @@ class SuppressPrintable extends Mode keyup: (event) => # If the selection type has changed (usually, no longer "Range"), then the user is interacting with # the input element, so we get out of the way. See discussion of option 5c from #1415. - if document.getSelection().type != type then @exit() else handler event + @exit() if DomUtils.getSelectionType() != type # When we use find, the selection/focus can land in a focusable/editable element. In this situation, special # considerations apply. We implement three special cases: @@ -48,7 +48,7 @@ class PostFindMode extends SuppressPrintable keydown: (event) => if KeyboardUtils.isEscape event @exit() - DomUtils.consumeKeyup event + @suppressEvent else handlerStack.remove() @continueBubbling @@ -79,9 +79,10 @@ class FindMode extends Mode exit: (event) -> super() - handleEscapeForFindMode() if event + FindMode.handleEscape() if event restoreSelection: -> + return unless @initialRange range = @initialRange selection = getSelection() selection.removeAllRanges() @@ -201,20 +202,71 @@ class FindMode extends Mode @restoreDefaultSelectionHighlight: forTrusted -> document.body.classList.remove("vimiumFindMode") + # The user has found what they're looking for and is finished searching. We enter insert mode, if possible. + @handleEscape: -> + document.body.classList.remove("vimiumFindMode") + # Removing the class does not re-color existing selections. we recreate the current selection so it reverts + # back to the default color. + selection = window.getSelection() + unless selection.isCollapsed + range = window.getSelection().getRangeAt(0) + window.getSelection().removeAllRanges() + window.getSelection().addRange(range) + focusFoundLink() || selectFoundInputElement() + + # Save the query so the user can do further searches with it. + @handleEnter: -> + focusFoundLink() + document.body.classList.add("vimiumFindMode") + FindMode.saveQuery() + + @findNext: (backwards) -> + Marks.setPreviousPosition() + FindMode.query.hasResults = FindMode.execute null, {backwards} + + if FindMode.query.hasResults + focusFoundLink() + new PostFindMode() + else + HUD.showForDuration("No matches for '#{FindMode.query.rawQuery}'", 1000) + checkReturnToViewPort: -> window.scrollTo @scrollX, @scrollY if @options.returnToViewport getCurrentRange = -> selection = getSelection() - if selection.type == "None" + if DomUtils.getSelectionType(selection) == "None" range = document.createRange() range.setStart document.body, 0 range.setEnd document.body, 0 range else - selection.collapseToStart() if selection.type == "Range" + selection.collapseToStart() if DomUtils.getSelectionType(selection) == "Range" selection.getRangeAt 0 -root = exports ? window +getLinkFromSelection = -> + node = window.getSelection().anchorNode + while (node && node != document.body) + return node if (node.nodeName.toLowerCase() == "a") + node = node.parentNode + null + +focusFoundLink = -> + if (FindMode.query.hasResults) + link = getLinkFromSelection() + link.focus() if link + +selectFoundInputElement = -> + # Since the last focused element might not be the one currently pointed to by find (e.g. the current one + # might be disabled and therefore unable to receive focus), we use the approximate heuristic of checking + # that the last anchor node is an ancestor of our element. + findModeAnchorNode = document.getSelection().anchorNode + if (FindMode.query.hasResults && document.activeElement && + DomUtils.isSelectable(document.activeElement) && + DomUtils.isDOMDescendant(findModeAnchorNode, document.activeElement)) + DomUtils.simulateSelect(document.activeElement) + +root = exports ? (window.root ?= {}) root.PostFindMode = PostFindMode root.FindMode = FindMode +extend window, root unless exports? diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 1dc66d52..d2a33091 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -26,13 +26,12 @@ class InsertMode extends Mode # An editable element in a shadow DOM is focused; blur it. @insertModeLock.blur() @exit event, event.target - DomUtils.consumeKeyup event + @suppressEvent defaults = name: "insert" indicator: if not @permanent and not Settings.get "hideHud" then "Insert mode" keypress: handleKeyEvent - keyup: handleKeyEvent keydown: handleKeyEvent super extend defaults, options @@ -129,6 +128,7 @@ class PassNextKeyMode extends Mode @exit() @passEventToPage -root = exports ? window +root = exports ? (window.root ?= {}) root.InsertMode = InsertMode root.PassNextKeyMode = PassNextKeyMode +extend window, root unless exports? diff --git a/content_scripts/mode_key_handler.coffee b/content_scripts/mode_key_handler.coffee index 1b3b21e7..cca6b77a 100644 --- a/content_scripts/mode_key_handler.coffee +++ b/content_scripts/mode_key_handler.coffee @@ -27,8 +27,6 @@ class KeyHandlerMode extends Mode super extend options, keydown: @onKeydown.bind this - # We cannot track keyup events if we lose the focus. - blur: (event) => @alwaysContinueBubbling => @keydownEvents = {} if event.target == window if options.exitOnEscape # If we're part way through a command's key sequence, then a first Escape should reset the key state, @@ -38,7 +36,7 @@ class KeyHandlerMode extends Mode keydown: (event) => if KeyboardUtils.isEscape(event) and not @isInResetState() @reset() - DomUtils.consumeKeyup event + @suppressEvent else @continueBubbling @@ -49,11 +47,13 @@ class KeyHandlerMode extends Mode DomUtils.consumeKeyup event, => @reset() # If the help dialog loses the focus, then Escape should hide it; see point 2 in #2045. else if isEscape and HelpDialog?.isShowing() - DomUtils.consumeKeyup event, -> HelpDialog.toggle() + HelpDialog.toggle() + @suppressEvent else if isEscape @continueBubbling else if @isMappedKey keyChar - DomUtils.consumeKeyup event, => @handleKeyChar keyChar + @handleKeyChar keyChar + @suppressEvent else if @isCountKey keyChar digit = parseInt keyChar @reset if @keyState.length == 1 then @countPrefix * 10 + digit else digit @@ -93,5 +93,6 @@ class KeyHandlerMode extends Mode @exit() if @options.count? and --@options.count <= 0 @suppressEvent -root = exports ? window +root = exports ? (window.root ?= {}) root.KeyHandlerMode = KeyHandlerMode +extend window, root unless exports? diff --git a/content_scripts/mode_normal.coffee b/content_scripts/mode_normal.coffee new file mode 100644 index 00000000..1fe0618e --- /dev/null +++ b/content_scripts/mode_normal.coffee @@ -0,0 +1,369 @@ +class NormalMode extends KeyHandlerMode + constructor: (options = {}) -> + defaults = + name: "normal" + indicator: false # There is normally no mode indicator in normal mode. + commandHandler: @commandHandler.bind this + + super extend defaults, options + + chrome.storage.local.get "normalModeKeyStateMapping", (items) => + @setKeyMapping items.normalModeKeyStateMapping + + chrome.storage.onChanged.addListener (changes, area) => + if area == "local" and changes.normalModeKeyStateMapping?.newValue + @setKeyMapping changes.normalModeKeyStateMapping.newValue + + commandHandler: ({command: registryEntry, count}) -> + count *= registryEntry.options.count ? 1 + count = 1 if registryEntry.noRepeat + + if registryEntry.repeatLimit? and registryEntry.repeatLimit < count + return unless confirm """ + You have asked Vimium to perform #{count} repetitions of the command: #{registryEntry.description}.\n + Are you sure you want to continue?""" + + if registryEntry.topFrame + # We never return to a UI-component frame (e.g. the help dialog), it might have lost the focus. + sourceFrameId = if window.isVimiumUIComponent then 0 else frameId + chrome.runtime.sendMessage + handler: "sendMessageToFrames", message: {name: "runInTopFrame", sourceFrameId, registryEntry} + else if registryEntry.background + chrome.runtime.sendMessage {handler: "runBackgroundCommand", registryEntry, count} + else + NormalModeCommands[registryEntry.command] count, {registryEntry} + +enterNormalMode = (count) -> + new NormalMode + indicator: "Normal mode (pass keys disabled)" + exitOnEscape: true + singleton: "enterNormalMode" + count: count + +NormalModeCommands = + # Scrolling. + scrollToBottom: -> + Marks.setPreviousPosition() + Scroller.scrollTo "y", "max" + scrollToTop: (count) -> + Marks.setPreviousPosition() + Scroller.scrollTo "y", (count - 1) * Settings.get("scrollStepSize") + scrollToLeft: -> Scroller.scrollTo "x", 0 + scrollToRight: -> Scroller.scrollTo "x", "max" + scrollUp: (count) -> Scroller.scrollBy "y", -1 * Settings.get("scrollStepSize") * count + scrollDown: (count) -> Scroller.scrollBy "y", Settings.get("scrollStepSize") * count + scrollPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1/2 * count + scrollPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1/2 * count + scrollFullPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1 * count + scrollFullPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1 * count + scrollLeft: (count) -> Scroller.scrollBy "x", -1 * Settings.get("scrollStepSize") * count + scrollRight: (count) -> Scroller.scrollBy "x", Settings.get("scrollStepSize") * count + + # Page state. + reload: (count, options) -> + hard = options.registryEntry.options.hard ? false + window.location.reload(hard) + goBack: (count) -> history.go(-count) + goForward: (count) -> history.go(count) + + # Url manipulation. + goUp: (count) -> + url = window.location.href + if (url[url.length - 1] == "/") + url = url.substring(0, url.length - 1) + + urlsplit = url.split("/") + # make sure we haven't hit the base domain yet + if (urlsplit.length > 3) + urlsplit = urlsplit.slice(0, Math.max(3, urlsplit.length - count)) + window.location.href = urlsplit.join('/') + + goToRoot: -> + window.location.href = window.location.origin + + toggleViewSource: -> + chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) -> + if (url.substr(0, 12) == "view-source:") + url = url.substr(12, url.length - 12) + else + url = "view-source:" + url + chrome.runtime.sendMessage {handler: "openUrlInNewTab", url} + + copyCurrentUrl: -> + chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) -> + HUD.copyToClipboard url + url = url[0..25] + "...." if 28 < url.length + HUD.showForDuration("Yanked #{url}", 2000) + + openCopiedUrlInNewTab: (count) -> + HUD.pasteFromClipboard (url) -> + chrome.runtime.sendMessage { handler: "openUrlInNewTab", url, count } + + openCopiedUrlInCurrentTab: -> + HUD.pasteFromClipboard (url) -> + chrome.runtime.sendMessage { handler: "openUrlInCurrentTab", url } + + # Mode changes. + enterInsertMode: -> + # If a focusable element receives the focus, then we exit and leave the permanently-installed insert-mode + # instance to take over. + new InsertMode global: true, exitOnFocus: true + + enterVisualMode: -> + new VisualMode userLaunchedMode: true + + enterVisualLineMode: -> + new VisualLineMode userLaunchedMode: true + + enterFindMode: -> + Marks.setPreviousPosition() + new FindMode() + + # Find. + performFind: (count) -> FindMode.findNext false for [0...count] by 1 + performBackwardsFind: (count) -> FindMode.findNext true for [0...count] by 1 + + # Misc. + mainFrame: -> focusThisFrame highlight: true, forceFocusThisFrame: true + showHelp: (sourceFrameId) -> HelpDialog.toggle {sourceFrameId, showAllCommandDetails: false} + + passNextKey: (count, options) -> + if options.registryEntry.options.normal + enterNormalMode count + else + new PassNextKeyMode count + + goPrevious: -> + previousPatterns = Settings.get("previousPatterns") || "" + previousStrings = previousPatterns.split(",").filter( (s) -> s.trim().length ) + findAndFollowRel("prev") || findAndFollowLink(previousStrings) + + goNext: -> + nextPatterns = Settings.get("nextPatterns") || "" + nextStrings = nextPatterns.split(",").filter( (s) -> s.trim().length ) + findAndFollowRel("next") || findAndFollowLink(nextStrings) + + focusInput: (count) -> + # Focus the first input element on the page, and create overlays to highlight all the input elements, with + # the currently-focused element highlighted specially. Tabbing will shift focus to the next input element. + # Pressing any other key will remove the overlays and the special tab behavior. + resultSet = DomUtils.evaluateXPath textInputXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE + visibleInputs = + for i in [0...resultSet.snapshotLength] by 1 + element = resultSet.snapshotItem i + continue unless DomUtils.getVisibleClientRect element, true + { element, index: i, rect: Rect.copy element.getBoundingClientRect() } + + visibleInputs.sort ({element: element1, index: i1}, {element: element2, index: i2}) -> + # Put elements with a lower positive tabIndex first, keeping elements in DOM order. + if element1.tabIndex > 0 + if element2.tabIndex > 0 + tabDifference = element1.tabIndex - element2.tabIndex + if tabDifference != 0 + tabDifference + else + i1 - i2 + else + -1 + else if element2.tabIndex > 0 + 1 + else + i1 - i2 + + if visibleInputs.length == 0 + HUD.showForDuration("There are no inputs to focus.", 1000) + return + + # This is a hack to improve usability on the Vimium options page. We prime the recently-focused input + # to be the key-mappings input. Arguably, this is the input that the user is most likely to use. + recentlyFocusedElement = lastFocusedInput() + + selectedInputIndex = + if count == 1 + # As the starting index, we pick that of the most recently focused input element (or 0). + elements = visibleInputs.map (visibleInput) -> visibleInput.element + Math.max 0, elements.indexOf recentlyFocusedElement + else + Math.min(count, visibleInputs.length) - 1 + + hints = for tuple in visibleInputs + hint = DomUtils.createElement "div" + hint.className = "vimiumReset internalVimiumInputHint vimiumInputHint" + + # minus 1 for the border + hint.style.left = (tuple.rect.left - 1) + window.scrollX + "px" + hint.style.top = (tuple.rect.top - 1) + window.scrollY + "px" + hint.style.width = tuple.rect.width + "px" + hint.style.height = tuple.rect.height + "px" + + hint + + new FocusSelector hints, visibleInputs, selectedInputIndex + +if LinkHints? + extend NormalModeCommands, + "LinkHints.activateMode": LinkHints.activateMode.bind LinkHints + "LinkHints.activateModeToOpenInNewTab": LinkHints.activateModeToOpenInNewTab.bind LinkHints + "LinkHints.activateModeToOpenInNewForegroundTab": LinkHints.activateModeToOpenInNewForegroundTab.bind LinkHints + "LinkHints.activateModeWithQueue": LinkHints.activateModeWithQueue.bind LinkHints + "LinkHints.activateModeToOpenIncognito": LinkHints.activateModeToOpenIncognito.bind LinkHints + "LinkHints.activateModeToDownloadLink": LinkHints.activateModeToDownloadLink.bind LinkHints + "LinkHints.activateModeToCopyLinkUrl": LinkHints.activateModeToCopyLinkUrl.bind LinkHints + +if Vomnibar? + extend NormalModeCommands, + "Vomnibar.activate": Vomnibar.activate.bind Vomnibar + "Vomnibar.activateInNewTab": Vomnibar.activateInNewTab.bind Vomnibar + "Vomnibar.activateTabSelection": Vomnibar.activateTabSelection.bind Vomnibar + "Vomnibar.activateBookmarks": Vomnibar.activateBookmarks.bind Vomnibar + "Vomnibar.activateBookmarksInNewTab": Vomnibar.activateBookmarksInNewTab.bind Vomnibar + "Vomnibar.activateEditUrl": Vomnibar.activateEditUrl.bind Vomnibar + "Vomnibar.activateEditUrlInNewTab": Vomnibar.activateEditUrlInNewTab.bind Vomnibar + +if Marks? + extend NormalModeCommands, + "Marks.activateCreateMode": Marks.activateCreateMode.bind Marks + "Marks.activateGotoMode": Marks.activateGotoMode.bind Marks + +# The types in <input type="..."> that we consider for focusInput command. Right now this is recalculated in +# each content script. Alternatively we could calculate it once in the background page and use a request to +# fetch it each time. +# Should we include the HTML5 date pickers here? + +# The corresponding XPath for such elements. +textInputXPath = (-> + textInputTypes = [ "text", "search", "email", "url", "number", "password", "date", "tel" ] + inputElements = ["input[" + + "(" + textInputTypes.map((type) -> '@type="' + type + '"').join(" or ") + "or not(@type))" + + " and not(@disabled or @readonly)]", + "textarea", "*[@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"] + DomUtils?.makeXPath(inputElements) +)() + +# used by the findAndFollow* functions. +followLink = (linkElement) -> + if (linkElement.nodeName.toLowerCase() == "link") + window.location.href = linkElement.href + else + # if we can click on it, don't simply set location.href: some next/prev links are meant to trigger AJAX + # calls, like the 'more' button on GitHub's newsfeed. + linkElement.scrollIntoView() + DomUtils.simulateClick(linkElement) + +# +# Find and follow a link which matches any one of a list of strings. If there are multiple such links, they +# are prioritized for shortness, by their position in :linkStrings, how far down the page they are located, +# and finally by whether the match is exact. Practically speaking, this means we favor 'next page' over 'the +# next big thing', and 'more' over 'nextcompany', even if 'next' occurs before 'more' in :linkStrings. +# +findAndFollowLink = (linkStrings) -> + linksXPath = DomUtils.makeXPath(["a", "*[@onclick or @role='link' or contains(@class, 'button')]"]) + links = DomUtils.evaluateXPath(linksXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) + candidateLinks = [] + + # at the end of this loop, candidateLinks will contain all visible links that match our patterns + # links lower in the page are more likely to be the ones we want, so we loop through the snapshot backwards + for i in [(links.snapshotLength - 1)..0] by -1 + link = links.snapshotItem(i) + + # ensure link is visible (we don't mind if it is scrolled offscreen) + boundingClientRect = link.getBoundingClientRect() + if (boundingClientRect.width == 0 || boundingClientRect.height == 0) + continue + computedStyle = window.getComputedStyle(link, null) + if (computedStyle.getPropertyValue("visibility") != "visible" || + computedStyle.getPropertyValue("display") == "none") + continue + + linkMatches = false + for linkString in linkStrings + if link.innerText.toLowerCase().indexOf(linkString) != -1 || + 0 <= link.value?.indexOf? linkString + linkMatches = true + break + continue unless linkMatches + + candidateLinks.push(link) + + return if (candidateLinks.length == 0) + + for link in candidateLinks + link.wordCount = link.innerText.trim().split(/\s+/).length + + # We can use this trick to ensure that Array.sort is stable. We need this property to retain the reverse + # in-page order of the links. + + candidateLinks.forEach((a,i) -> a.originalIndex = i) + + # favor shorter links, and ignore those that are more than one word longer than the shortest link + candidateLinks = + candidateLinks + .sort((a, b) -> + if (a.wordCount == b.wordCount) then a.originalIndex - b.originalIndex else a.wordCount - b.wordCount + ) + .filter((a) -> a.wordCount <= candidateLinks[0].wordCount + 1) + + for linkString in linkStrings + exactWordRegex = + if /\b/.test(linkString[0]) or /\b/.test(linkString[linkString.length - 1]) + new RegExp "\\b" + linkString + "\\b", "i" + else + new RegExp linkString, "i" + for candidateLink in candidateLinks + if exactWordRegex.test(candidateLink.innerText) || + (candidateLink.value && exactWordRegex.test(candidateLink.value)) + followLink(candidateLink) + return true + false + +findAndFollowRel = (value) -> + relTags = ["link", "a", "area"] + for tag in relTags + elements = document.getElementsByTagName(tag) + for element in elements + if (element.hasAttribute("rel") && element.rel.toLowerCase() == value) + followLink(element) + return true + +class FocusSelector extends Mode + constructor: (hints, visibleInputs, selectedInputIndex) -> + super + name: "focus-selector" + exitOnClick: true + keydown: (event) => + if event.key == "Tab" + hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint' + selectedInputIndex += hints.length + (if event.shiftKey then -1 else 1) + selectedInputIndex %= hints.length + hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' + DomUtils.simulateSelect visibleInputs[selectedInputIndex].element + @suppressEvent + else unless event.key == "Shift" + @exit() + # Give the new mode the opportunity to handle the event. + @restartBubbling + + @hintContainingDiv = DomUtils.addElementList hints, + id: "vimiumInputMarkerContainer" + className: "vimiumReset" + + DomUtils.simulateSelect visibleInputs[selectedInputIndex].element + if visibleInputs.length == 1 + @exit() + return + else + hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' + + exit: -> + super() + DomUtils.removeElement @hintContainingDiv + if document.activeElement and DomUtils.isEditable document.activeElement + new InsertMode + singleton: "post-find-mode/focus-input" + targetElement: document.activeElement + indicator: false + +root = exports ? (window.root ?= {}) +root.NormalMode = NormalMode +root.NormalModeCommands = NormalModeCommands +extend window, root unless exports? diff --git a/content_scripts/mode_visual.coffee b/content_scripts/mode_visual.coffee index 28097005..4c6578cd 100644 --- a/content_scripts/mode_visual.coffee +++ b/content_scripts/mode_visual.coffee @@ -312,7 +312,7 @@ class VisualMode extends KeyHandlerMode yank: (args = {}) -> @yankedText = @selection.toString() @exit() - chrome.runtime.sendMessage handler: "copyToClipboard", data: @yankedText + HUD.copyToClipboard @yankedText message = @yankedText.replace /\s+/g, " " message = message[...12] + "..." if 15 < @yankedText.length @@ -380,6 +380,7 @@ class CaretMode extends VisualMode return true false -root = exports ? window +root = exports ? (window.root ?= {}) root.VisualMode = VisualMode root.VisualLineMode = VisualLineMode +extend window, root unless exports? diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index 56862d49..f65062e4 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -95,7 +95,14 @@ findScrollableElement = (element, direction, amount, factor) -> # On some pages, the scrolling element is not actually scrollable. Here, we search the document for the # largest visible element which does scroll vertically. This is used to initialize activatedElement. See # #1358. -firstScrollableElement = (element=getScrollingElement()) -> +firstScrollableElement = (element = null) -> + unless element + scrollingElement = getScrollingElement() + if doesScroll(scrollingElement, "y", 1, 1) or doesScroll(scrollingElement, "y", -1, 1) + return scrollingElement + else + element = document.body ? getScrollingElement() + if doesScroll(element, "y", 1, 1) or doesScroll(element, "y", -1, 1) element else @@ -301,5 +308,6 @@ Scroller = element = findScrollableElement element, "x", amount, 1 CoreScroller.scroll element, "x", amount, false -root = exports ? window +root = exports ? (window.root ?= {}) root.Scroller = Scroller +extend window, root unless exports? diff --git a/content_scripts/ui_component.coffee b/content_scripts/ui_component.coffee index 203f0c8c..c71bfb35 100644 --- a/content_scripts/ui_component.coffee +++ b/content_scripts/ui_component.coffee @@ -96,5 +96,6 @@ class UIComponent @options = null @postMessage "hidden" # Inform the UI component that it is hidden. -root = exports ? window +root = exports ? (window.root ?= {}) root.UIComponent = UIComponent +extend window, root unless exports? diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 4f2e805d..432fa7a2 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -2,6 +2,12 @@ # This content script must be run prior to domReady so that we perform some operations very early. # +root = exports ? (window.root ?= {}) +# On Firefox, sometimes the variables assigned to window are lost (bug 1408996), so we reinstall them. +# NOTE(mrmr1993): This bug leads to catastrophic failure (ie. nothing works and errors abound). +DomUtils.documentReady -> + root.extend window, root unless extend? + isEnabledForUrl = true isIncognitoMode = chrome.extension.inIncognitoContext normalMode = null @@ -16,21 +22,6 @@ windowIsFocused = do -> windowHasFocus = false if event.target == window; true -> windowHasFocus -# The types in <input type="..."> that we consider for focusInput command. Right now this is recalculated in -# each content script. Alternatively we could calculate it once in the background page and use a request to -# fetch it each time. -# Should we include the HTML5 date pickers here? - -# The corresponding XPath for such elements. -textInputXPath = (-> - textInputTypes = [ "text", "search", "email", "url", "number", "password", "date", "tel" ] - inputElements = ["input[" + - "(" + textInputTypes.map((type) -> '@type="' + type + '"').join(" or ") + "or not(@type))" + - " and not(@disabled or @readonly)]", - "textarea", "*[@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"] - DomUtils.makeXPath(inputElements) -)() - # This is set by Frame.registerFrameId(). A frameId of 0 indicates that this is the top frame in the tab. frameId = null @@ -111,41 +102,6 @@ handlerStack.push target = target.parentElement true -class NormalMode extends KeyHandlerMode - constructor: (options = {}) -> - defaults = - name: "normal" - indicator: false # There is normally no mode indicator in normal mode. - commandHandler: @commandHandler.bind this - - super extend defaults, options - - chrome.storage.local.get "normalModeKeyStateMapping", (items) => - @setKeyMapping items.normalModeKeyStateMapping - - chrome.storage.onChanged.addListener (changes, area) => - if area == "local" and changes.normalModeKeyStateMapping?.newValue - @setKeyMapping changes.normalModeKeyStateMapping.newValue - - commandHandler: ({command: registryEntry, count}) -> - count *= registryEntry.options.count ? 1 - count = 1 if registryEntry.noRepeat - - if registryEntry.repeatLimit? and registryEntry.repeatLimit < count - return unless confirm """ - You have asked Vimium to perform #{count} repetitions of the command: #{registryEntry.description}.\n - Are you sure you want to continue?""" - - if registryEntry.topFrame - # We never return to a UI-component frame (e.g. the help dialog), it might have lost the focus. - sourceFrameId = if window.isVimiumUIComponent then 0 else frameId - chrome.runtime.sendMessage - handler: "sendMessageToFrames", message: {name: "runInTopFrame", sourceFrameId, registryEntry} - else if registryEntry.background - chrome.runtime.sendMessage {handler: "runBackgroundCommand", registryEntry, count} - else - Utils.invokeCommandString registryEntry.command, count, {registryEntry} - installModes = -> # Install the permanent modes. The permanently-installed insert mode tracks focus/blur events, and # activates/deactivates itself accordingly. @@ -186,7 +142,7 @@ initializePreDomReady = -> frameFocused: -> # A frame has received the focus; we don't care here (UI components handle this). checkEnabledAfterURLChange: checkEnabledAfterURLChange runInTopFrame: ({sourceFrameId, registryEntry}) -> - Utils.invokeCommandString registryEntry.command, sourceFrameId, registryEntry if DomUtils.isTopFrame() + NormalModeCommands[registryEntry.command] sourceFrameId, registryEntry if DomUtils.isTopFrame() linkHintsMessage: (request) -> HintCoordinator[request.messageType] request chrome.runtime.onMessage.addListener (request, sender, sendResponse) -> @@ -201,6 +157,7 @@ initializePreDomReady = -> # Wrapper to install event listeners. Syntactic sugar. installListener = (element, event, callback) -> element.addEventListener(event, forTrusted(-> + root.extend window, root unless extend? # See #2800. if isEnabledForUrl then callback.apply(this, arguments) else true ), true) @@ -245,7 +202,7 @@ Frame = postMessage: (handler, request = {}) -> @port.postMessage extend request, {handler} linkHintsMessage: (request) -> HintCoordinator[request.messageType] request registerFrameId: ({chromeFrameId}) -> - frameId = window.frameId = chromeFrameId + frameId = root.frameId = window.frameId = chromeFrameId # We register a frame immediately only if it is focused or its window isn't tiny. We register tiny # frames later, when necessary. This affects focusFrame() and link hints. if windowIsFocused() or not DomUtils.windowIsTooSmall() @@ -264,6 +221,7 @@ Frame = @port = chrome.runtime.connect name: "frames" @port.onMessage.addListener (request) => + root.extend window, root unless extend? # See #2800 and #2831. (@listeners[request.handler] ? this[request.handler]) request # We disable the content scripts when we lose contact with the background page, or on unload. @@ -280,7 +238,7 @@ Frame = handlerStack.reset() isEnabledForUrl = false window.removeEventListener "focus", onFocus - window.removeEventListener "hashchange", onFocus + window.removeEventListener "hashchange", checkEnabledAfterURLChange setScrollPosition = ({ scrollX, scrollY }) -> DomUtils.documentReady -> @@ -327,171 +285,17 @@ focusThisFrame = (request) -> document.activeElement.blur() if document.activeElement.tagName.toLowerCase() == "iframe" flashFrame() if request.highlight -extend window, - scrollToBottom: -> - Marks.setPreviousPosition() - Scroller.scrollTo "y", "max" - scrollToTop: (count) -> - Marks.setPreviousPosition() - Scroller.scrollTo "y", (count - 1) * Settings.get("scrollStepSize") - scrollToLeft: -> Scroller.scrollTo "x", 0 - scrollToRight: -> Scroller.scrollTo "x", "max" - scrollUp: (count) -> Scroller.scrollBy "y", -1 * Settings.get("scrollStepSize") * count - scrollDown: (count) -> Scroller.scrollBy "y", Settings.get("scrollStepSize") * count - scrollPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1/2 * count - scrollPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1/2 * count - scrollFullPageUp: (count) -> Scroller.scrollBy "y", "viewSize", -1 * count - scrollFullPageDown: (count) -> Scroller.scrollBy "y", "viewSize", 1 * count - scrollLeft: (count) -> Scroller.scrollBy "x", -1 * Settings.get("scrollStepSize") * count - scrollRight: (count) -> Scroller.scrollBy "x", Settings.get("scrollStepSize") * count - -extend window, - reload: -> window.location.reload() - goBack: (count) -> history.go(-count) - goForward: (count) -> history.go(count) - - goUp: (count) -> - url = window.location.href - if (url[url.length - 1] == "/") - url = url.substring(0, url.length - 1) - - urlsplit = url.split("/") - # make sure we haven't hit the base domain yet - if (urlsplit.length > 3) - urlsplit = urlsplit.slice(0, Math.max(3, urlsplit.length - count)) - window.location.href = urlsplit.join('/') - - goToRoot: -> - window.location.href = window.location.origin - - mainFrame: -> focusThisFrame highlight: true, forceFocusThisFrame: true - - toggleViewSource: -> - chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) -> - if (url.substr(0, 12) == "view-source:") - url = url.substr(12, url.length - 12) - else - url = "view-source:" + url - chrome.runtime.sendMessage {handler: "openUrlInNewTab", url} - - copyCurrentUrl: -> - # TODO(ilya): When the following bug is fixed, revisit this approach of sending back to the background - # page to copy. - # http://code.google.com/p/chromium/issues/detail?id=55188 - chrome.runtime.sendMessage { handler: "getCurrentTabUrl" }, (url) -> - chrome.runtime.sendMessage { handler: "copyToClipboard", data: url } - url = url[0..25] + "...." if 28 < url.length - HUD.showForDuration("Yanked #{url}", 2000) - - enterInsertMode: -> - # If a focusable element receives the focus, then we exit and leave the permanently-installed insert-mode - # instance to take over. - new InsertMode global: true, exitOnFocus: true - - enterVisualMode: -> - new VisualMode userLaunchedMode: true - - enterVisualLineMode: -> - new VisualLineMode userLaunchedMode: true - - passNextKey: (count, options) -> - if options.registryEntry.options.normal - enterNormalMode count - else - new PassNextKeyMode count - - enterNormalMode: (count) -> - new NormalMode - indicator: "Normal mode (pass keys disabled)" - exitOnEscape: true - singleton: "enterNormalMode" - count: count - - focusInput: do -> - # Track the most recently focused input element. - recentlyFocusedElement = null - window.addEventListener "focus", - forTrusted (event) -> recentlyFocusedElement = event.target if DomUtils.isEditable event.target - , true - - (count) -> - mode = InsertMode - # Focus the first input element on the page, and create overlays to highlight all the input elements, with - # the currently-focused element highlighted specially. Tabbing will shift focus to the next input element. - # Pressing any other key will remove the overlays and the special tab behavior. - # The mode argument is the mode to enter once an input is selected. - resultSet = DomUtils.evaluateXPath textInputXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE - visibleInputs = - for i in [0...resultSet.snapshotLength] by 1 - element = resultSet.snapshotItem i - continue unless DomUtils.getVisibleClientRect element, true - { element, rect: Rect.copy element.getBoundingClientRect() } - - if visibleInputs.length == 0 - HUD.showForDuration("There are no inputs to focus.", 1000) - return - - # This is a hack to improve usability on the Vimium options page. We prime the recently-focused input - # to be the key-mappings input. Arguably, this is the input that the user is most likely to use. - recentlyFocusedElement ?= document.getElementById "keyMappings" if window.isVimiumOptionsPage - - selectedInputIndex = - if count == 1 - # As the starting index, we pick that of the most recently focused input element (or 0). - elements = visibleInputs.map (visibleInput) -> visibleInput.element - Math.max 0, elements.indexOf recentlyFocusedElement - else - Math.min(count, visibleInputs.length) - 1 - - hints = for tuple in visibleInputs - hint = DomUtils.createElement "div" - hint.className = "vimiumReset internalVimiumInputHint vimiumInputHint" - - # minus 1 for the border - hint.style.left = (tuple.rect.left - 1) + window.scrollX + "px" - hint.style.top = (tuple.rect.top - 1) + window.scrollY + "px" - hint.style.width = tuple.rect.width + "px" - hint.style.height = tuple.rect.height + "px" - - hint - - new class FocusSelector extends Mode - constructor: -> - super - name: "focus-selector" - exitOnClick: true - keydown: (event) => - if event.key == "Tab" - hints[selectedInputIndex].classList.remove 'internalVimiumSelectedInputHint' - selectedInputIndex += hints.length + (if event.shiftKey then -1 else 1) - selectedInputIndex %= hints.length - hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' - DomUtils.simulateSelect visibleInputs[selectedInputIndex].element - @suppressEvent - else unless event.key == "Shift" - @exit() - # Give the new mode the opportunity to handle the event. - @restartBubbling - - @hintContainingDiv = DomUtils.addElementList hints, - id: "vimiumInputMarkerContainer" - className: "vimiumReset" - - DomUtils.simulateSelect visibleInputs[selectedInputIndex].element - if visibleInputs.length == 1 - @exit() - return - else - hints[selectedInputIndex].classList.add 'internalVimiumSelectedInputHint' - - exit: -> - super() - DomUtils.removeElement @hintContainingDiv - if mode and document.activeElement and DomUtils.isEditable document.activeElement - new mode - singleton: "post-find-mode/focus-input" - targetElement: document.activeElement - indicator: false +# Used by focusInput command. +root.lastFocusedInput = do -> + # Track the most recently focused input element. + recentlyFocusedElement = null + window.addEventListener "focus", + forTrusted (event) -> + DomUtils = window.DomUtils ? root.DomUtils # Workaround FF bug 1408996. + if DomUtils.isEditable event.target + recentlyFocusedElement = event.target + , true + -> recentlyFocusedElement # Checks if Vimium should be enabled or not in this frame. As a side effect, it also informs the background # page whether this frame has the focus, allowing the background page to track the active frame's URL and set @@ -513,165 +317,8 @@ checkIfEnabledForUrl = do -> checkEnabledAfterURLChange = forTrusted -> checkIfEnabledForUrl() if windowIsFocused() -handleEscapeForFindMode = -> - document.body.classList.remove("vimiumFindMode") - # removing the class does not re-color existing selections. we recreate the current selection so it reverts - # back to the default color. - selection = window.getSelection() - unless selection.isCollapsed - range = window.getSelection().getRangeAt(0) - window.getSelection().removeAllRanges() - window.getSelection().addRange(range) - focusFoundLink() || selectFoundInputElement() - -# <esc> sends us into insert mode if possible, but <cr> does not. -# <esc> corresponds approximately to 'nevermind, I have found it already' while <cr> means 'I want to save -# this query and do more searches with it' -handleEnterForFindMode = -> - focusFoundLink() - document.body.classList.add("vimiumFindMode") - FindMode.saveQuery() - -focusFoundLink = -> - if (FindMode.query.hasResults) - link = getLinkFromSelection() - link.focus() if link - -selectFoundInputElement = -> - # Since the last focused element might not be the one currently pointed to by find (e.g. the current one - # might be disabled and therefore unable to receive focus), we use the approximate heuristic of checking - # that the last anchor node is an ancestor of our element. - findModeAnchorNode = document.getSelection().anchorNode - if (FindMode.query.hasResults && document.activeElement && - DomUtils.isSelectable(document.activeElement) && - DomUtils.isDOMDescendant(findModeAnchorNode, document.activeElement)) - DomUtils.simulateSelect(document.activeElement) - -findAndFocus = (backwards) -> - Marks.setPreviousPosition() - FindMode.query.hasResults = FindMode.execute null, {backwards} - - if FindMode.query.hasResults - focusFoundLink() - new PostFindMode() - else - HUD.showForDuration("No matches for '#{FindMode.query.rawQuery}'", 1000) - -performFind = (count) -> findAndFocus false for [0...count] by 1 -performBackwardsFind = (count) -> findAndFocus true for [0...count] by 1 - -getLinkFromSelection = -> - node = window.getSelection().anchorNode - while (node && node != document.body) - return node if (node.nodeName.toLowerCase() == "a") - node = node.parentNode - null - -# used by the findAndFollow* functions. -followLink = (linkElement) -> - if (linkElement.nodeName.toLowerCase() == "link") - window.location.href = linkElement.href - else - # if we can click on it, don't simply set location.href: some next/prev links are meant to trigger AJAX - # calls, like the 'more' button on GitHub's newsfeed. - linkElement.scrollIntoView() - DomUtils.simulateClick(linkElement) - -# -# Find and follow a link which matches any one of a list of strings. If there are multiple such links, they -# are prioritized for shortness, by their position in :linkStrings, how far down the page they are located, -# and finally by whether the match is exact. Practically speaking, this means we favor 'next page' over 'the -# next big thing', and 'more' over 'nextcompany', even if 'next' occurs before 'more' in :linkStrings. -# -findAndFollowLink = (linkStrings) -> - linksXPath = DomUtils.makeXPath(["a", "*[@onclick or @role='link' or contains(@class, 'button')]"]) - links = DomUtils.evaluateXPath(linksXPath, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE) - candidateLinks = [] - - # at the end of this loop, candidateLinks will contain all visible links that match our patterns - # links lower in the page are more likely to be the ones we want, so we loop through the snapshot backwards - for i in [(links.snapshotLength - 1)..0] by -1 - link = links.snapshotItem(i) - - # ensure link is visible (we don't mind if it is scrolled offscreen) - boundingClientRect = link.getBoundingClientRect() - if (boundingClientRect.width == 0 || boundingClientRect.height == 0) - continue - computedStyle = window.getComputedStyle(link, null) - if (computedStyle.getPropertyValue("visibility") != "visible" || - computedStyle.getPropertyValue("display") == "none") - continue - - linkMatches = false - for linkString in linkStrings - if link.innerText.toLowerCase().indexOf(linkString) != -1 || - 0 <= link.value?.indexOf? linkString - linkMatches = true - break - continue unless linkMatches - - candidateLinks.push(link) - - return if (candidateLinks.length == 0) - - for link in candidateLinks - link.wordCount = link.innerText.trim().split(/\s+/).length - - # We can use this trick to ensure that Array.sort is stable. We need this property to retain the reverse - # in-page order of the links. - - candidateLinks.forEach((a,i) -> a.originalIndex = i) - - # favor shorter links, and ignore those that are more than one word longer than the shortest link - candidateLinks = - candidateLinks - .sort((a, b) -> - if (a.wordCount == b.wordCount) then a.originalIndex - b.originalIndex else a.wordCount - b.wordCount - ) - .filter((a) -> a.wordCount <= candidateLinks[0].wordCount + 1) - - for linkString in linkStrings - exactWordRegex = - if /\b/.test(linkString[0]) or /\b/.test(linkString[linkString.length - 1]) - new RegExp "\\b" + linkString + "\\b", "i" - else - new RegExp linkString, "i" - for candidateLink in candidateLinks - if exactWordRegex.test(candidateLink.innerText) || - (candidateLink.value && exactWordRegex.test(candidateLink.value)) - followLink(candidateLink) - return true - false - -findAndFollowRel = (value) -> - relTags = ["link", "a", "area"] - for tag in relTags - elements = document.getElementsByTagName(tag) - for element in elements - if (element.hasAttribute("rel") && element.rel.toLowerCase() == value) - followLink(element) - return true - -window.goPrevious = -> - previousPatterns = Settings.get("previousPatterns") || "" - previousStrings = previousPatterns.split(",").filter( (s) -> s.trim().length ) - findAndFollowRel("prev") || findAndFollowLink(previousStrings) - -window.goNext = -> - nextPatterns = Settings.get("nextPatterns") || "" - nextStrings = nextPatterns.split(",").filter( (s) -> s.trim().length ) - findAndFollowRel("next") || findAndFollowLink(nextStrings) - -# Enters find mode. Returns the new find-mode instance. -enterFindMode = -> - Marks.setPreviousPosition() - new FindMode() - -window.showHelp = (sourceFrameId) -> - HelpDialog.toggle {sourceFrameId, showAllCommandDetails: false} - # If we are in the help dialog iframe, then HelpDialog is already defined with the necessary functions. -window.HelpDialog ?= +root.HelpDialog ?= helpUI: null isShowing: -> @helpUI?.showing abort: -> @helpUI.hide false if @isShowing() @@ -688,14 +335,13 @@ window.HelpDialog ?= initializePreDomReady() DomUtils.documentReady initializeOnDomReady -root = exports ? window root.handlerStack = handlerStack root.frameId = frameId root.Frame = Frame root.windowIsFocused = windowIsFocused root.bgLog = bgLog -# These are exported for find mode and link-hints mode. -extend root, {handleEscapeForFindMode, handleEnterForFindMode, performFind, performBackwardsFind, - enterFindMode, focusThisFrame} +# These are exported for normal mode and link-hints mode. +extend root, {focusThisFrame} # These are exported only for the tests. extend root, {installModes} +extend window, root unless exports? diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee index 14d72e87..ad98aa48 100644 --- a/content_scripts/vomnibar.coffee +++ b/content_scripts/vomnibar.coffee @@ -58,5 +58,6 @@ Vomnibar = HelpDialog.abort() @vomnibarUI.activate extend options, { name: "activate", sourceFrameId, focus: true } -root = exports ? window +root = exports ? (window.root ?= {}) root.Vomnibar = Vomnibar +extend window, root unless exports? |
