diff options
45 files changed, 866 insertions, 606 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 61c32456..e4ed8b8b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,9 +11,16 @@ respective issue so others don't duplicate your effort. Please include the following when reporting an issue: - 1. Chrome and OS Version: `chrome://version` +### Chrome/Chromium + + 1. Chrome/Chromium and OS Version: `chrome://version` 1. Vimium Version: `chrome://extensions` +### Firefox + + 1. Firefox and OS Version: `about:` + 1. Vimium Version: `about:addons`, then click on `More` below Vimium + ## Installing From Source Vimium is written in Coffeescript, which compiles to Javascript. To @@ -77,6 +77,9 @@ task "package", "Builds a zip file for submission to the Chrome store. The outpu spawn "rsync", rsyncOptions, false, true spawn "zip", ["-r", "dist/vimium-#{vimium_version}.zip", "dist/vimium"], false, true + spawn "zip", "-r -FS dist/vimium-ff-#{vimium_version}.zip background_scripts Cakefile content_scripts CONTRIBUTING.md CREDITS icons lib + manifest.json MIT-LICENSE.txt pages README.md -x *.coffee -x Cakefile -x CREDITS -x *.md".split(/\s+/), false, true + # This builds a CRX that's distributable outside of the Chrome web store. Is this used by folks who fork # Vimium and want to distribute their fork? task "package-custom-crx", "build .crx file", -> @@ -157,8 +160,3 @@ task "coverage", "generate coverage report", -> source: (Utils.escapeHtml fs.readFileSync fname, 'utf-8').split '\n' fs.writeFileSync 'jscoverage.json', JSON.stringify(result) - -task "zip", "build Firefox zip file in ../vimium.zip", -> - spawn "zip", "-r -FS ../vimium.zip background_scripts Cakefile content_scripts CONTRIBUTING.md CREDITS icons lib - manifest.json MIT-LICENSE.txt pages README.md -x *.coffee -x Cakefile -x CREDITS -x *.md".split /\s+/ - @@ -141,9 +141,10 @@ The following special keys are available for mapping: - `<c-*>`, `<a-*>`, `<m-*>` for ctrl, alt, and meta (command on Mac) respectively with any key. Replace `*` with the key of choice. -- `<left>`, `<right>`, `<up>`, `<down>` for the arrow keys -- `<space>` and `<backspace>` for the space and backspace keys -- `<f1>` through `<f12>` for the function keys +- `<left>`, `<right>`, `<up>`, `<down>` for the arrow keys. +- `<f1>` through `<f12>` for the function keys. +- `<space>` for the space key. +- `<tab>`, `<enter>`, `<delete>`, `<backspace>`, `<insert>`, `<home>` and `<end>` for the corresponding non-printable keys (version 1.62 onwards). Shifts are automatically detected so, for example, `<c-&>` corresponds to ctrl+shift+7 on an English keyboard. @@ -168,6 +169,26 @@ PRs are welcome. Release Notes ------------- +In `master` (not yet released) + +- Backup and restore Vimium options (see the very bottom of the options page, below *Advanced Options*). +- It is now possible to map `<tab>`, `<enter>`, `<delete>`, `<insert>`, `<home>` and `<end>`. +- New command options for `createTab` to create create new normal and incognito windows + ([examples](https://github.com/philc/vimium/wiki/Tips-and-Tricks#creating-tabs-with-urls-and-windows)). +- When upgrading, you will be asked to re-validate permissions. The only new + permission is "copy and paste to/from clipboard" (the `clipboardWrite` + permission). This is necessary to support copy/paste on Firefox. + +1.61 (2017-10-27) + +- For *filtered hints*, you can now use alphabetical hint characters + instead of digits; use `<Shift>` for hint characters. +- With `map R reload hard`, the reload command now asks Chrome to bypass its cache. +- You can now map `<c-[>` to a command (in which case it will not be treated as `Escape`). +- Various bug fixes, particularly for Firefox. +- Minor versions: + - 1.61.1: Fix `map R reload hard`. + 1.60 (2017-09-14) - Features: diff --git a/background_scripts/bg_utils.coffee b/background_scripts/bg_utils.coffee index b8e618ff..698f5352 100644 --- a/background_scripts/bg_utils.coffee +++ b/background_scripts/bg_utils.coffee @@ -18,7 +18,7 @@ class TabRecency @deregister removedTabId @register addedTabId - chrome.windows.onFocusChanged.addListener (wnd) => + chrome.windows?.onFocusChanged.addListener (wnd) => if wnd != chrome.windows.WINDOW_ID_NONE chrome.tabs.query {windowId: wnd, active: true}, (tabs) => @register tabs[0].id if tabs[0] diff --git a/background_scripts/commands.coffee b/background_scripts/commands.coffee index cda036e6..4d2e1606 100644 --- a/background_scripts/commands.coffee +++ b/background_scripts/commands.coffee @@ -113,6 +113,9 @@ Commands = # We don't need these properties in the content scripts. delete currentMapping[key][prop] for prop in ["keySequence", "description"] chrome.storage.local.set normalModeKeyStateMapping: keyStateMapping + # Inform `KeyboardUtils.isEscape()` whether `<c-[>` should be interpreted as `Escape` (which it is by + # default). + chrome.storage.local.set useVimLikeEscape: "<c-[>" not of keyStateMapping # Build the "helpPageData" data structure which the help page needs and place it in Chrome storage. prepareHelpPageData: -> @@ -337,8 +340,8 @@ commandDescriptions = toggleViewSource: ["View page source", { noRepeat: true }] copyCurrentUrl: ["Copy the current URL to the clipboard", { noRepeat: true }] - openCopiedUrlInCurrentTab: ["Open the clipboard's URL in the current tab", { background: true, noRepeat: true }] - openCopiedUrlInNewTab: ["Open the clipboard's URL in a new tab", { background: true, repeatLimit: 20 }] + openCopiedUrlInCurrentTab: ["Open the clipboard's URL in the current tab", { noRepeat: true }] + openCopiedUrlInNewTab: ["Open the clipboard's URL in a new tab", { repeatLimit: 20 }] enterInsertMode: ["Enter insert mode", { noRepeat: true }] passNextKey: ["Pass the next key to the page"] diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index 97d8fa65..8220545d 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -114,7 +114,7 @@ TabOperations = canUseOpenerTabId = not (Utils.isFirefox() and Utils.compareVersions(Utils.firefoxVersion(), "57") < 0) tabConfig.openerTabId = request.tab.id if canUseOpenerTabId - chrome.tabs.create tabConfig, callback + chrome.tabs.create tabConfig, -> callback request # Opens request.url in new window and switches to it. openUrlInNewWindow: (request, callback = (->)) -> @@ -148,7 +148,7 @@ toggleMuteTab = do -> # selectSpecificTab = (request) -> chrome.tabs.get(request.id, (tab) -> - chrome.windows.update(tab.windowId, { focused: true }) + chrome.windows?.update(tab.windowId, { focused: true }) chrome.tabs.update(request.id, { active: true })) moveTab = ({count, tab, registryEntry}) -> @@ -188,13 +188,20 @@ BackgroundCommands = [if request.tab.incognito then "chrome://newtab" else chrome.runtime.getURL newTabUrl] else [newTabUrl] - urls = request.urls[..].reverse() - do openNextUrl = (request) -> - if 0 < urls.length - TabOperations.openUrlInNewTab (extend request, {url: urls.pop()}), (tab) -> - openNextUrl extend request, {tab, tabId: tab.id} - else - callback request + if request.registryEntry.options.incognito or request.registryEntry.options.window + windowConfig = + url: request.urls + focused: true + incognito: request.registryEntry.options.incognito ? false + chrome.windows.create windowConfig, -> callback request + else + urls = request.urls[..].reverse() + do openNextUrl = (request) -> + if 0 < urls.length + TabOperations.openUrlInNewTab (extend request, {url: urls.pop()}), (tab) -> + openNextUrl extend request, {tab, tabId: tab.id} + else + callback request duplicateTab: mkRepeatCommand (request, callback) -> chrome.tabs.duplicate request.tabId, (tab) -> callback extend request, {tab, tabId: tab.id} moveTabToNewWindow: ({count, tab}) -> @@ -214,8 +221,6 @@ BackgroundCommands = startTabIndex = Math.max 0, Math.min activeTabIndex, tabs.length - count chrome.tabs.remove (tab.id for tab in tabs[startTabIndex...startTabIndex + count]) restoreTab: mkRepeatCommand (request, callback) -> chrome.sessions.restore null, callback request - openCopiedUrlInCurrentTab: (request) -> TabOperations.openUrlInCurrentTab extend request, url: Clipboard.paste() - openCopiedUrlInNewTab: (request) -> @createTab extend request, url: Clipboard.paste() togglePinTab: ({tab}) -> chrome.tabs.update tab.id, {pinned: !tab.pinned} toggleMuteTab: toggleMuteTab moveTabLeft: moveTab @@ -324,7 +329,7 @@ Frames = enabledState = Exclusions.isEnabledForUrl request.url if request.frameIsFocused - chrome.browserAction.setIcon tabId: tabId, imageData: do -> + chrome.browserAction.setIcon? tabId: tabId, imageData: do -> enabledStateIcon = if not enabledState.isEnabledForUrl DISABLED_ICON @@ -355,7 +360,7 @@ handleFrameFocused = ({tabId, frameId}) -> # Rotate through frames to the frame count places after frameId. cycleToFrame = (frames, frameId, count = 0) -> - # We can't always track which frame chrome has focussed, but here we learn that it's frameId; so add an + # We can't always track which frame chrome has focused, but here we learn that it's frameId; so add an # additional offset such that we do indeed start from frameId. count = (count + Math.max 0, frames.indexOf frameId) % frames.length [frames[count..]..., frames[0...count]...] @@ -423,7 +428,7 @@ sendRequestHandlers = # getCurrentTabUrl is used by the content scripts to get their full URL, because window.location cannot help # with Chrome-specific URLs like "view-source:http:..". getCurrentTabUrl: ({tab}) -> tab.url - openUrlInNewTab: (request) -> TabOperations.openUrlInNewTab request + openUrlInNewTab: mkRepeatCommand (request, callback) -> TabOperations.openUrlInNewTab request, callback openUrlInNewWindow: (request) -> TabOperations.openUrlInNewWindow request openUrlInIncognito: (request) -> chrome.windows.create incognito: true, url: Utils.convertToUrl request.url openUrlInCurrentTab: TabOperations.openUrlInCurrentTab @@ -431,8 +436,6 @@ sendRequestHandlers = chrome.tabs.create url: chrome.runtime.getURL("pages/options.html"), index: request.tab.index + 1 frameFocused: handleFrameFocused nextFrame: BackgroundCommands.nextFrame - copyToClipboard: Clipboard.copy.bind Clipboard - pasteFromClipboard: Clipboard.paste.bind Clipboard selectSpecificTab: selectSpecificTab createMark: Marks.create.bind(Marks) gotoMark: Marks.goto.bind(Marks) @@ -449,7 +452,7 @@ chrome.tabs.onRemoved.addListener (tabId) -> delete cache[tabId] for cache in [frameIdsForTab, urlForTab, portsForTab, HintCoordinator.tabState] chrome.storage.local.get "findModeRawQueryListIncognito", (items) -> if items.findModeRawQueryListIncognito - chrome.windows.getAll null, (windows) -> + chrome.windows?.getAll null, (windows) -> for window in windows return if window.incognito # There are no remaining incognito-mode tabs, and findModeRawQueryListIncognito is set. diff --git a/background_scripts/marks.coffee b/background_scripts/marks.coffee index a6491b9e..77b07b41 100644 --- a/background_scripts/marks.coffee +++ b/background_scripts/marks.coffee @@ -82,7 +82,7 @@ Marks = # Given a list of tabs candidate tabs, pick one. Prefer tabs in the current window and tabs with shorter # (matching) URLs. pickTab: (tabs, callback) -> - chrome.windows.getCurrent ({ id }) -> + tabPicker = ({ id }) -> # Prefer tabs in the current window, if there are any. tabsInWindow = tabs.filter (tab) -> tab.windowId == id tabs = tabsInWindow if 0 < tabsInWindow.length @@ -92,6 +92,10 @@ Marks = # Prefer shorter URLs. tabs.sort (a,b) -> a.url.length - b.url.length callback tabs[0] + if chrome.windows? + chrome.windows.getCurrent tabPicker + else + tabPicker({id: undefined}) root = exports ? window root.Marks = Marks 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? diff --git a/lib/clipboard.coffee b/lib/clipboard.coffee index af143dd9..a9e2e82e 100644 --- a/lib/clipboard.coffee +++ b/lib/clipboard.coffee @@ -1,8 +1,9 @@ Clipboard = - _createTextArea: -> - textArea = document.createElement "textarea" + _createTextArea: (tagName = "textarea") -> + textArea = document.createElement tagName textArea.style.position = "absolute" textArea.style.left = "-100%" + textArea.contentEditable = "true" textArea # http://groups.google.com/group/chromium-extensions/browse_thread/thread/49027e7f3b04f68/f6ab2457dee5bf55 @@ -16,14 +17,15 @@ Clipboard = document.body.removeChild(textArea) paste: -> - textArea = @_createTextArea() + textArea = @_createTextArea "div" # Use a <div> so Firefox pastes rich text. document.body.appendChild(textArea) textArea.focus() document.execCommand("Paste") - value = textArea.value + value = textArea.innerText document.body.removeChild(textArea) value -root = exports ? window +root = exports ? (window.root ?= {}) root.Clipboard = Clipboard +extend window, root unless exports? diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index ff5991dc..67d5a44c 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -219,7 +219,7 @@ DomUtils = node = selection.anchorNode node and @isDOMDescendant element, node else - if selection.type == "Range" and selection.isCollapsed + if DomUtils.getSelectionType(selection) == "Range" and selection.isCollapsed # The selection is inside the Shadow DOM of a node. We can check the node it registers as being # before, since this represents the node whose Shadow DOM it's inside. containerNode = selection.anchorNode.childNodes[selection.anchorOffset] @@ -344,7 +344,7 @@ DomUtils = consumeKeyup: do -> handlerId = null - (event, callback = null) -> + (event, callback = null, suppressPropagation) -> unless event.repeat handlerStack.remove handlerId if handlerId? code = event.code @@ -353,17 +353,25 @@ DomUtils = keyup: (event) -> return handlerStack.continueBubbling unless event.code == code @remove() - handlerStack.suppressEvent + if suppressPropagation + DomUtils.suppressPropagation event + else + DomUtils.suppressEvent event + handlerStack.continueBubbling # We cannot track keyup events if we lose the focus. blur: (event) -> @remove() if event.target == window handlerStack.continueBubbling callback?() - @suppressEvent event - handlerStack.suppressEvent + if suppressPropagation + DomUtils.suppressPropagation event + handlerStack.suppressPropagation + else + DomUtils.suppressEvent event + handlerStack.suppressEvent # Polyfill for selection.type (which is not available in Firefox). - getSelectionType: (selection) -> + getSelectionType: (selection = document.getSelection()) -> selection.type or do -> if selection.rangeCount == 0 "None" @@ -376,7 +384,7 @@ DomUtils = # This finds the element containing the selection focus. getElementWithFocus: (selection, backwards) -> r = t = selection.getRangeAt 0 - if selection.type == "Range" + if DomUtils.getSelectionType(selection) == "Range" r = t.cloneRange() r.collapse backwards t = r.startContainer @@ -416,5 +424,6 @@ DomUtils = style.textContent = Settings.get "userDefinedLinkHintCss" document.head.appendChild style -root = exports ? window +root = exports ? (window.root ?= {}) root.DomUtils = DomUtils +extend window, root unless exports? diff --git a/lib/find_mode_history.coffee b/lib/find_mode_history.coffee index ff660bd2..93698266 100644 --- a/lib/find_mode_history.coffee +++ b/lib/find_mode_history.coffee @@ -46,5 +46,6 @@ FindModeHistory = refreshRawQueryList: (query, rawQueryList) -> ([ query ].concat rawQueryList.filter (q) => q != query)[0..@max] -root = exports ? window +root = exports ? (window.root ?= {}) root.FindModeHistory = FindModeHistory +extend window, root unless exports? diff --git a/lib/handler_stack.coffee b/lib/handler_stack.coffee index 806b707f..a43fc356 100644 --- a/lib/handler_stack.coffee +++ b/lib/handler_stack.coffee @@ -1,4 +1,4 @@ -root = exports ? window +root = exports ? (window.root ?= {}) class HandlerStack constructor: -> @@ -57,7 +57,10 @@ class HandlerStack if result == @passEventToPage return true else if result == @suppressPropagation - DomUtils.suppressPropagation event + if type == "keydown" + DomUtils.consumeKeyup event, null, true + else + DomUtils.suppressPropagation event return false else if result == @restartBubbling return @bubbleEvent type, event @@ -65,7 +68,11 @@ class HandlerStack true # Do nothing, but continue bubbling. else # result is @suppressEvent or falsy. - DomUtils.suppressEvent event if @isChromeEvent event + if @isChromeEvent event + if type == "keydown" + DomUtils.consumeKeyup event + else + DomUtils.suppressEvent event return false # None of our handlers care about this event, so pass it to the page. @@ -120,3 +127,4 @@ class HandlerStack root.HandlerStack = HandlerStack root.handlerStack = new HandlerStack() +extend window, root unless exports? diff --git a/lib/keyboard_utils.coffee b/lib/keyboard_utils.coffee index e14e8b3e..09623e50 100644 --- a/lib/keyboard_utils.coffee +++ b/lib/keyboard_utils.coffee @@ -5,7 +5,7 @@ Utils?.monitorChromeStorage "mapKeyRegistry", (value) => mapKeyRegistry = value KeyboardUtils = # This maps event.key key names to Vimium key names. keyNames: - "ArrowLeft": "left", "ArrowUp": "up", "ArrowRight": "right", "ArrowDown": "down", " ": "space", "Backspace": "backspace" + "ArrowLeft": "left", "ArrowUp": "up", "ArrowRight": "right", "ArrowDown": "down", " ": "space" init: -> if (navigator.userAgent.indexOf("Mac") != -1) @@ -31,19 +31,17 @@ KeyboardUtils = else if key.length == 1 and not event.shiftKey key = key.toLowerCase() - if key of @keyNames - @keyNames[key] # It appears that key is not always defined (see #2453). - else if not key? + unless key "" + else if key of @keyNames + @keyNames[key] + else if @isModifier event + "" # Don't resolve modifier keys. else if key.length == 1 key - else if key.length == 2 and "F1" <= key <= "F9" - key.toLowerCase() # F1 to F9. - else if key.length == 3 and "F10" <= key <= "F12" - key.toLowerCase() # F10 to F12. else - "" + key.toLowerCase() getKeyCharString: (event) -> if keyChar = @getKeyChar event @@ -60,9 +58,13 @@ KeyboardUtils = keyChar = mapKeyRegistry[keyChar] ? keyChar keyChar - isEscape: (event) -> - # <c-[> is mapped to Escape in Vim by default. - event.key == "Escape" || @getKeyCharString(event) == "<c-[>" + isEscape: do -> + useVimLikeEscape = true + Utils.monitorChromeStorage "useVimLikeEscape", (value) -> useVimLikeEscape = value + + (event) -> + # <c-[> is mapped to Escape in Vim by default. + event.key == "Escape" or (useVimLikeEscape and @getKeyCharString(event) == "<c-[>") isBackspace: (event) -> event.key in ["Backspace", "Delete"] @@ -70,6 +72,9 @@ KeyboardUtils = isPrintable: (event) -> @getKeyCharString(event)?.length == 1 + isModifier: (event) -> + event.key in ["Control", "Shift", "Alt", "OS", "AltGraph", "Meta"] + enUsTranslations: "Backquote": ["`", "~"] "Minus": ["-", "_"] @@ -97,5 +102,6 @@ KeyboardUtils = KeyboardUtils.init() -root = exports ? window +root = exports ? (window.root ?= {}) root.KeyboardUtils = KeyboardUtils +extend window, root unless exports? diff --git a/lib/rect.coffee b/lib/rect.coffee index d4807cc2..0e9c3417 100644 --- a/lib/rect.coffee +++ b/lib/rect.coffee @@ -67,12 +67,18 @@ Rect = rects.filter (rect) -> rect.height > 0 and rect.width > 0 - contains: (rect1, rect2) -> + # Determine whether two rects overlap. + intersects: (rect1, rect2) -> rect1.right > rect2.left and rect1.left < rect2.right and rect1.bottom > rect2.top and rect1.top < rect2.bottom + # Determine whether two rects overlap, including 0-width intersections at borders. + intersectsStrict: (rect1, rect2) -> + rect1.right >= rect2.left and rect1.left <= rect2.right and + rect1.bottom >= rect2.top and rect1.top <= rect2.bottom + equals: (rect1, rect2) -> for property in ["top", "bottom", "left", "right", "width", "height"] return false if rect1[property] != rect2[property] @@ -82,14 +88,6 @@ Rect = @create (Math.max rect1.left, rect2.left), (Math.max rect1.top, rect2.top), (Math.min rect1.right, rect2.right), (Math.min rect1.bottom, rect2.bottom) - # Determine whether two rects overlap. - rectsOverlap: do -> - halfOverlapChecker = (rect1, rect2) -> - (rect1.left <= rect2.left <= rect1.right or rect1.left <= rect2.right <= rect1.right) and - (rect1.top <= rect2.top <= rect1.bottom or rect1.top <= rect2.bottom <= rect1.bottom) - - (rect1, rect2) -> - halfOverlapChecker(rect1, rect2) or halfOverlapChecker rect2, rect1 - -root = exports ? window +root = exports ? (window.root ?= {}) root.Rect = Rect +extend window, root unless exports? diff --git a/lib/settings.coffee b/lib/settings.coffee index 38718990..fd1ef268 100644 --- a/lib/settings.coffee +++ b/lib/settings.coffee @@ -202,7 +202,7 @@ Settings.init() # Perform migration from old settings versions, if this is the background page. if Utils.isBackgroundPage() - Settings.onLoaded -> + Settings.applyMigrations = -> unless Settings.get "settingsVersion" # This is a new install. For some settings, we retain a legacy default behaviour for existing users but # use a non-default behaviour for new users. @@ -218,5 +218,8 @@ if Utils.isBackgroundPage() # be removed after 1.58 has been out for sufficiently long. Settings.nuke "copyNonDefaultsToChromeStorage-20150717" -root = exports ? window + Settings.onLoaded Settings.applyMigrations.bind Settings + +root = exports ? (window.root ?= {}) root.Settings = Settings +extend window, root unless exports? diff --git a/lib/utils.coffee b/lib/utils.coffee index d0a82cf7..6f38be8f 100644 --- a/lib/utils.coffee +++ b/lib/utils.coffee @@ -32,14 +32,6 @@ Utils = # Returns true whenever the current page is the extension's background page. isBackgroundPage: -> @isExtensionPage() and chrome.extension.getBackgroundPage?() == window - # Takes a dot-notation object string and calls the function that it points to with the correct value for - # 'this'. - invokeCommandString: (str, args...) -> - [names..., name] = str.split '.' - obj = window - obj = obj[component] for component in names - obj[name].apply obj, args - # Escape all special characters, so RegExp will parse the string 'as is'. # Taken from http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex escapeRegexSpecialCharacters: do -> @@ -335,8 +327,11 @@ class JobRunner onReady: (callback) -> @fetcher.use callback -root = exports ? window +root = exports ? (window.root ?= {}) root.Utils = Utils root.SimpleCache = SimpleCache root.AsyncDataFetcher = AsyncDataFetcher root.JobRunner = JobRunner +unless exports? + root.extend = extend + extend window, root diff --git a/manifest.json b/manifest.json index a9629530..ee5b0655 100644 --- a/manifest.json +++ b/manifest.json @@ -1,18 +1,18 @@ { "manifest_version": 2, "name": "Vimium", - "version": "1.60.4", + "version": "1.61.1", "description": "The Hacker's Browser. Vimium provides keyboard shortcuts for navigation and control in the spirit of Vim.", "icons": { "16": "icons/icon16.png", "48": "icons/icon48.png", "128": "icons/icon128.png" }, + "minimum_chrome_version": "51.0", "background": { "scripts": [ "lib/utils.js", "lib/settings.js", "background_scripts/bg_utils.js", "background_scripts/commands.js", - "lib/clipboard.js", "background_scripts/exclusions.js", "background_scripts/completion_engines.js", "background_scripts/completion_search.js", @@ -31,6 +31,7 @@ "bookmarks", "history", "clipboardRead", + "clipboardWrite", "storage", "sessions", "notifications", @@ -60,6 +61,7 @@ "content_scripts/mode_key_handler.js", "content_scripts/mode_visual.js", "content_scripts/hud.js", + "content_scripts/mode_normal.js", "content_scripts/vimium_frontend.js" ], "css": ["content_scripts/vimium.css"], diff --git a/pages/blank.html b/pages/blank.html index 8f10c7f6..d026912e 100644 --- a/pages/blank.html +++ b/pages/blank.html @@ -19,6 +19,7 @@ <script src="../content_scripts/mode_key_handler.js"></script> <script src="../content_scripts/mode_visual.js"></script> <script src="../content_scripts/hud.js"></script> + <script src="../content_scripts/mode_normal.js"></script> <script src="../content_scripts/vimium_frontend.js"></script> <link rel="stylesheet" type="text/css" href="../content_scripts/vimium.css" /> diff --git a/pages/completion_engines.html b/pages/completion_engines.html index 0c86edf7..3313b26c 100644 --- a/pages/completion_engines.html +++ b/pages/completion_engines.html @@ -22,6 +22,7 @@ <script src="../content_scripts/mode_key_handler.js"></script> <script src="../content_scripts/mode_visual.js"></script> <script src="../content_scripts/hud.js"></script> + <script src="../content_scripts/mode_normal.js"></script> <script src="../content_scripts/vimium_frontend.js"></script> <link rel="stylesheet" type="text/css" href="../content_scripts/vimium.css" /> diff --git a/pages/help_dialog.coffee b/pages/help_dialog.coffee index f36155e4..08180a72 100644 --- a/pages/help_dialog.coffee +++ b/pages/help_dialog.coffee @@ -83,7 +83,7 @@ HelpDialog = commandNameElement.textContent = command.command commandNameElement.title = "Click to copy \"#{command.command}\" to clipboard." commandNameElement.addEventListener "click", -> - chrome.runtime.sendMessage handler: "copyToClipboard", data: commandNameElement.textContent + HUD.copyToClipboard commandNameElement.textContent HUD.showForDuration("Yanked #{commandNameElement.textContent}.", 2000) @showAdvancedCommands(@getShowAdvancedCommands()) diff --git a/pages/help_dialog.html b/pages/help_dialog.html index 1da54efd..7f053265 100644 --- a/pages/help_dialog.html +++ b/pages/help_dialog.html @@ -19,6 +19,7 @@ <script src="../content_scripts/mode_key_handler.js"></script> <script src="../content_scripts/mode_visual.js"></script> <script src="../content_scripts/hud.js"></script> + <script src="../content_scripts/mode_normal.js"></script> <script src="../content_scripts/vimium_frontend.js"></script> <link rel="stylesheet" type="text/css" href="../content_scripts/vimium.css" /> diff --git a/pages/hud.coffee b/pages/hud.coffee index 0d2ec2f7..5ff2e07e 100644 --- a/pages/hud.coffee +++ b/pages/hud.coffee @@ -95,5 +95,19 @@ handlers = " (No matches)" countElement.textContent = if showMatchText then countText else "" + copyToClipboard: (data) -> + focusedElement = document.activeElement + Clipboard.copy data + focusedElement?.focus() + window.parent.focus() + UIComponentServer.postMessage {name: "unfocusIfFocused"} + + pasteFromClipboard: -> + focusedElement = document.activeElement + data = Clipboard.paste() + focusedElement?.focus() + window.parent.focus() + UIComponentServer.postMessage {name: "pasteResponse", data} + UIComponentServer.registerHandler ({data}) -> handlers[data.name ? data]? data FindModeHistory.init() diff --git a/pages/hud.html b/pages/hud.html index 3e8cf976..7bd27171 100644 --- a/pages/hud.html +++ b/pages/hud.html @@ -7,6 +7,7 @@ <script type="text/javascript" src="../lib/settings.js"></script> <script type="text/javascript" src="../lib/keyboard_utils.js"></script> <script type="text/javascript" src="../lib/find_mode_history.js"></script> + <script type="text/javascript" src="../lib/clipboard.js"></script> <script type="text/javascript" src="ui_component_server.js"></script> <script type="text/javascript" src="hud.js"></script> </head> diff --git a/pages/logging.html b/pages/logging.html index 6eff58c4..17aafd70 100644 --- a/pages/logging.html +++ b/pages/logging.html @@ -19,6 +19,7 @@ <script src="../content_scripts/mode_key_handler.js"></script> <script src="../content_scripts/mode_visual.js"></script> <script src="../content_scripts/hud.js"></script> + <script src="../content_scripts/mode_normal.js"></script> <script src="../content_scripts/vimium_frontend.js"></script> <link rel="stylesheet" type="text/css" href="../content_scripts/vimium.css" /> diff --git a/pages/options.coffee b/pages/options.coffee index 035dd403..86b6122d 100644 --- a/pages/options.coffee +++ b/pages/options.coffee @@ -39,9 +39,14 @@ class Option bgSettings.clear @field @fetch() + @onSaveCallbacks: [] + @onSave: (callback) -> + @onSaveCallbacks.push callback + # Static method. @saveOptions: -> Option.all.map (option) -> option.save() + callback() for callback in @onSaveCallbacks # Abstract method; only implemented in sub-classes. # Populate the option's DOM element (@element) with the setting's current value. @@ -93,8 +98,10 @@ class ExclusionRulesOption extends Option element populateElement: (rules) -> - for rule in rules - @appendRule rule + # For the case of restoring a backup, we first have to remove existing rules. + exclusionRules = $ "exclusionRules" + exclusionRules.deleteRow 1 while exclusionRules.rows[1] + @appendRule rule for rule in rules # Append a row for a new rule. Return the newly-added element. appendRule: (rule) -> @@ -323,6 +330,52 @@ document.addEventListener "DOMContentLoaded", -> xhr.send() +# +# Backup and restore. "?" is for the tests." +DomUtils?.documentReady -> + restoreSettingsVersion = null + + populateBackupLinkUrl = -> + backup = settingsVersion: bgSettings.get "settingsVersion" + for option in Option.all + backup[option.field] = option.readValueFromElement() + # Create the blob in the background page so it isn't garbage collected when the page closes in FF. + bgWin = chrome.extension.getBackgroundPage() + blob = new bgWin.Blob [ JSON.stringify backup, null, 2 ] + $("backupLink").href = bgWin.URL.createObjectURL blob + + $("backupLink").addEventListener "mousedown", populateBackupLinkUrl, true + + $("chooseFile").addEventListener "change", (event) -> + document.activeElement?.blur() + files = event.target.files + if files.length == 1 + file = files[0] + reader = new FileReader + reader.readAsText file + reader.onload = (event) -> + try + backup = JSON.parse reader.result + catch + alert "Failed to parse Vimium backup." + return + + restoreSettingsVersion = backup["settingsVersion"] if "settingsVersion" of backup + for option in Option.all + if option.field of backup + option.populateElement backup[option.field] + option.onUpdated() + + Option.onSave -> + # If we're restoring a backup, then restore the backed up settingsVersion. + if restoreSettingsVersion? + bgSettings.set "settingsVersion", restoreSettingsVersion + restoreSettingsVersion = null + # Reset the restore-backup input. + $("chooseFile").value = "" + # We need to apply migrations in case we are restoring an old backup. + bgSettings.applyMigrations() + # Exported for tests. root = exports ? window extend root, {Options, isVimiumOptionsPage: true} diff --git a/pages/options.css b/pages/options.css index 5e2a3dfc..dab342a3 100644 --- a/pages/options.css +++ b/pages/options.css @@ -231,3 +231,6 @@ input.pattern, input.passKeys, .exclusionHeaderText { white-space: nowrap; width: 110px; } +#backupLink { + cursor: pointer; +} diff --git a/pages/options.html b/pages/options.html index 46307b6f..b118bbd9 100644 --- a/pages/options.html +++ b/pages/options.html @@ -20,6 +20,7 @@ <script src="../content_scripts/mode_key_handler.js"></script> <script src="../content_scripts/mode_visual.js"></script> <script src="../content_scripts/hud.js"></script> + <script src="../content_scripts/mode_normal.js"></script> <script src="../content_scripts/vimium_frontend.js"></script> <link rel="stylesheet" type="text/css" href="../content_scripts/vimium.css" /> @@ -66,7 +67,7 @@ unmapAll <a href="#" id="showCommands">Show available commands</a>. </div> </div> - <textarea id="keyMappings" type="text"></textarea> + <textarea id="keyMappings" type="text" tabIndex="1"></textarea> </td> </tr> <tr> @@ -271,11 +272,12 @@ b: http://b.com/?q=%s description </td> </tr> <tr> - <td class="caption">CSS for link hints</td> + <td class="caption">CSS for Vimium UI</td> <td verticalAlign="top"> <div class="help"> <div class="example"> - The CSS used to style the characters next to each link hint.<br/><br/> + These styles are applied to link hints, the Vomnibar, the help dialog, the exclusions pop-up and the HUD.<br /> + By default, this CSS is used to style the characters next to each link hint.<br/><br/> These styles are used in addition to and take precedence over Vimium's default styles. </div> @@ -315,6 +317,33 @@ b: http://b.com/?q=%s description </tr> --> </tbody> + <tbody id='backupAndRestore'> + <tr> + <td colspan="2"><header>Backup and Restore</header></td> + </tr> + <tr> + <td class="caption">Backup</td> + <td> + <div class="help"> + <div class="example"> + Click to backup your settings, or right-click and select <i>Save As</i>. + </div> + </div> + <a id="backupLink" download="vimium-options.json">Click to download backup</a> + </td> + </tr> + <tr> + <td class="caption">Restore</td> + <td> + <div class="help"> + <div class="example"> + Choose a backup file to restore, then click <i>Save Changes</i>, below, to confirm. + </div> + </div> + <input id="chooseFile" type="file" accept=".json" style="width: 200px;"/> + </td> + </tr> + </tbody> </table> </div> diff --git a/tests/dom_tests/dom_tests.coffee b/tests/dom_tests/dom_tests.coffee index a5c85606..6e422d46 100644 --- a/tests/dom_tests/dom_tests.coffee +++ b/tests/dom_tests/dom_tests.coffee @@ -436,29 +436,29 @@ context "Input focus", document.getElementById("test-div").innerHTML = "" should "focus the first element", -> - focusInput 1 + NormalModeCommands.focusInput 1 assert.equal "first", document.activeElement.id should "focus the nth element", -> - focusInput 100 + NormalModeCommands.focusInput 100 assert.equal "third", document.activeElement.id should "activate insert mode on the first element", -> - focusInput 1 + NormalModeCommands.focusInput 1 assert.isTrue InsertMode.permanentInstance.isActive() should "activate insert mode on the first element", -> - focusInput 100 + NormalModeCommands.focusInput 100 assert.isTrue InsertMode.permanentInstance.isActive() should "activate the most recently-selected input if the count is 1", -> - focusInput 3 - focusInput 1 + NormalModeCommands.focusInput 3 + NormalModeCommands.focusInput 1 assert.equal "third", document.activeElement.id should "not trigger insert if there are no inputs", -> document.getElementById("test-div").innerHTML = "" - focusInput 1 + NormalModeCommands.focusInput 1 assert.isFalse InsertMode.permanentInstance.isActive() # TODO: these find prev/next link tests could be refactored into unit tests which invoke a function which has @@ -479,7 +479,7 @@ context "Find prev / next links", <a href='#second'>next page</a> """ stubSettings "nextPatterns", "next" - goNext() + NormalModeCommands.goNext() assert.equal '#second', window.location.hash should "match against non-word patterns", -> @@ -487,7 +487,7 @@ context "Find prev / next links", <a href='#first'>>></a> """ stubSettings "nextPatterns", ">>" - goNext() + NormalModeCommands.goNext() assert.equal '#first', window.location.hash should "favor matches with fewer words", -> @@ -496,14 +496,14 @@ context "Find prev / next links", <a href='#second'>next!</a> """ stubSettings "nextPatterns", "next" - goNext() + NormalModeCommands.goNext() assert.equal '#second', window.location.hash should "find link relation in header", -> document.getElementById("test-div").innerHTML = """ <link rel='next' href='#first'> """ - goNext() + NormalModeCommands.goNext() assert.equal '#first', window.location.hash should "favor link relation to text matching", -> @@ -511,14 +511,14 @@ context "Find prev / next links", <link rel='next' href='#first'> <a href='#second'>next</a> """ - goNext() + NormalModeCommands.goNext() assert.equal '#first', window.location.hash should "match mixed case link relation", -> document.getElementById("test-div").innerHTML = """ <link rel='Next' href='#first'> """ - goNext() + NormalModeCommands.goNext() assert.equal '#first', window.location.hash createLinks = (n) -> diff --git a/tests/dom_tests/dom_tests.html b/tests/dom_tests/dom_tests.html index d2e795d1..37cd43e3 100644 --- a/tests/dom_tests/dom_tests.html +++ b/tests/dom_tests/dom_tests.html @@ -49,6 +49,7 @@ <script type="text/javascript" src="../../content_scripts/mode_key_handler.js"></script> <script type="text/javascript" src="../../content_scripts/mode_visual.js"></script> <script type="text/javascript" src="../../content_scripts/hud.js"></script> + <script type="text/javascript" src="../../content_scripts/mode_normal.js"></script> <script type="text/javascript" src="../../content_scripts/vimium_frontend.js"></script> <script type="text/javascript" src="../shoulda.js/shoulda.js"></script> diff --git a/tests/unit_tests/commands_test.coffee b/tests/unit_tests/commands_test.coffee index 0e0be1d6..49dd2570 100644 --- a/tests/unit_tests/commands_test.coffee +++ b/tests/unit_tests/commands_test.coffee @@ -4,6 +4,14 @@ extend global, require "../../background_scripts/bg_utils.js" global.Settings = {postUpdateHooks: {}, get: (-> ""), set: ->} {Commands} = require "../../background_scripts/commands.js" +# Include mode_normal to check that all commands have been implemented. +global.KeyHandlerMode = global.Mode = {} +global.KeyboardUtils = {platform: ""} +extend global, require "../../content_scripts/link_hints.js" +extend global, require "../../content_scripts/marks.js" +extend global, require "../../content_scripts/vomnibar.js" +{NormalModeCommands} = require "../../content_scripts/mode_normal.js" + context "Key mappings", setup -> @testKeySequence = (key, expectedKeyText, expectedKeyLength) -> @@ -114,6 +122,14 @@ context "Parse commands", assert.equal "a", BgUtils.parseLines(" a \n b")[0] assert.equal "b", BgUtils.parseLines(" a \n b")[1] -# TODO (smblott) More tests: -# - Ensure each background command has an implmentation in BackgroundCommands -# - Ensure each foreground command has an implmentation in vimium_frontent.coffee +context "Commands implemented", + (for own command, options of Commands.availableCommands + do (command, options) -> + if options.background + should "#{command} (background command)", -> + # TODO: Import background_scripts/main.js and expose BackgroundCommands from there. + # assert.isTrue BackgroundCommands[command] + else + should "#{command} (foreground command)", -> + assert.isTrue NormalModeCommands[command] + )... diff --git a/tests/unit_tests/handler_stack_test.coffee b/tests/unit_tests/handler_stack_test.coffee index 7b62af07..374c235b 100644 --- a/tests/unit_tests/handler_stack_test.coffee +++ b/tests/unit_tests/handler_stack_test.coffee @@ -4,6 +4,7 @@ extend(global, require "../../lib/handler_stack.js") context "handlerStack", setup -> stub global, "DomUtils", {} + stub DomUtils, "consumeKeyup", -> stub DomUtils, "suppressEvent", -> stub DomUtils, "suppressPropagation", -> @handlerStack = new HandlerStack diff --git a/tests/unit_tests/rect_test.coffee b/tests/unit_tests/rect_test.coffee index 0773dbcf..5054e029 100644 --- a/tests/unit_tests/rect_test.coffee +++ b/tests/unit_tests/rect_test.coffee @@ -201,7 +201,7 @@ context "Rect subtraction", subtractRect = Rect.create x, y, (x + width), (y + height) resultRects = Rect.subtract rect, subtractRect for resultRect in resultRects - assert.isFalse Rect.contains subtractRect, resultRect + assert.isFalse Rect.intersects subtractRect, resultRect should "be contained in original rect", -> rect = Rect.create 0, 0, 3, 3 @@ -212,7 +212,7 @@ context "Rect subtraction", subtractRect = Rect.create x, y, (x + width), (y + height) resultRects = Rect.subtract rect, subtractRect for resultRect in resultRects - assert.isTrue Rect.contains rect, resultRect + assert.isTrue Rect.intersects rect, resultRect should "contain the subtracted rect in the original minus the results", -> rect = Rect.create 0, 0, 3, 3 @@ -229,60 +229,60 @@ context "Rect subtraction", assert.isTrue (resultComplement.length == 0 or resultComplement.length == 1) if resultComplement.length == 1 complementRect = resultComplement[0] - assert.isTrue Rect.contains subtractRect, complementRect + assert.isTrue Rect.intersects subtractRect, complementRect context "Rect overlaps", should "detect that a rect overlaps itself", -> rect = Rect.create 2, 2, 4, 4 - assert.isTrue Rect.rectsOverlap rect, rect + assert.isTrue Rect.intersectsStrict rect, rect should "detect that non-overlapping rectangles do not overlap on the left", -> rect1 = Rect.create 2, 2, 4, 4 rect2 = Rect.create 0, 2, 1, 4 - assert.isFalse Rect.rectsOverlap rect1, rect2 + assert.isFalse Rect.intersectsStrict rect1, rect2 should "detect that non-overlapping rectangles do not overlap on the right", -> rect1 = Rect.create 2, 2, 4, 4 rect2 = Rect.create 5, 2, 6, 4 - assert.isFalse Rect.rectsOverlap rect1, rect2 + assert.isFalse Rect.intersectsStrict rect1, rect2 should "detect that non-overlapping rectangles do not overlap on the top", -> rect1 = Rect.create 2, 2, 4, 4 rect2 = Rect.create 2, 0, 2, 1 - assert.isFalse Rect.rectsOverlap rect1, rect2 + assert.isFalse Rect.intersectsStrict rect1, rect2 should "detect that non-overlapping rectangles do not overlap on the bottom", -> rect1 = Rect.create 2, 2, 4, 4 rect2 = Rect.create 2, 5, 2, 6 - assert.isFalse Rect.rectsOverlap rect1, rect2 + assert.isFalse Rect.intersectsStrict rect1, rect2 should "detect overlapping rectangles on the left", -> rect1 = Rect.create 2, 2, 4, 4 rect2 = Rect.create 0, 2, 2, 4 - assert.isTrue Rect.rectsOverlap rect1, rect2 + assert.isTrue Rect.intersectsStrict rect1, rect2 should "detect overlapping rectangles on the right", -> rect1 = Rect.create 2, 2, 4, 4 rect2 = Rect.create 4, 2, 5, 4 - assert.isTrue Rect.rectsOverlap rect1, rect2 + assert.isTrue Rect.intersectsStrict rect1, rect2 should "detect overlapping rectangles on the top", -> rect1 = Rect.create 2, 2, 4, 4 rect2 = Rect.create 2, 4, 4, 5 - assert.isTrue Rect.rectsOverlap rect1, rect2 + assert.isTrue Rect.intersectsStrict rect1, rect2 should "detect overlapping rectangles on the bottom", -> rect1 = Rect.create 2, 2, 4, 4 rect2 = Rect.create 2, 0, 4, 2 - assert.isTrue Rect.rectsOverlap rect1, rect2 + assert.isTrue Rect.intersectsStrict rect1, rect2 should "detect overlapping rectangles when second rectangle is contained in first", -> rect1 = Rect.create 1, 1, 4, 4 rect2 = Rect.create 2, 2, 3, 3 - assert.isTrue Rect.rectsOverlap rect1, rect2 + assert.isTrue Rect.intersectsStrict rect1, rect2 should "detect overlapping rectangles when first rectangle is contained in second", -> rect1 = Rect.create 1, 1, 4, 4 rect2 = Rect.create 2, 2, 3, 3 - assert.isTrue Rect.rectsOverlap rect2, rect1 + assert.isTrue Rect.intersectsStrict rect2, rect1 diff --git a/tests/unit_tests/utils_test.coffee b/tests/unit_tests/utils_test.coffee index cc1081dd..2794a6d7 100644 --- a/tests/unit_tests/utils_test.coffee +++ b/tests/unit_tests/utils_test.coffee @@ -138,26 +138,6 @@ context "distinctCharacters", should "eliminate duplicate characters", -> assert.equal "abc", Utils.distinctCharacters "bbabaabbacabbbab" -context "invokeCommandString", - setup -> - @beenCalled = false - window.singleComponentCommand = => @beenCalled = true - window.twoComponentCommand = command: window.singleComponentCommand - - tearDown -> - delete window.singleComponentCommand - delete window.twoComponentCommand - - should "invoke single-component commands", -> - assert.isFalse @beenCalled - Utils.invokeCommandString "singleComponentCommand" - assert.isTrue @beenCalled - - should "invoke multi-component commands", -> - assert.isFalse @beenCalled - Utils.invokeCommandString "twoComponentCommand.command" - assert.isTrue @beenCalled - context "escapeRegexSpecialCharacters", should "escape regexp special characters", -> str = "-[]/{}()*+?.^$|" |
