diff options
| -rw-r--r-- | CONTRIBUTING.md | 48 | ||||
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | background_scripts/completion.coffee | 40 | ||||
| -rw-r--r-- | background_scripts/main.coffee | 57 | ||||
| -rw-r--r-- | background_scripts/settings.coffee | 1 | ||||
| -rw-r--r-- | content_scripts/scroller.coffee | 251 | ||||
| -rw-r--r-- | content_scripts/vimium.css | 17 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 41 | ||||
| -rw-r--r-- | lib/dom_utils.coffee | 4 | ||||
| -rw-r--r-- | pages/options.coffee | 1 | ||||
| -rw-r--r-- | pages/options.html | 13 | ||||
| -rw-r--r-- | pages/popup.coffee | 16 | ||||
| -rw-r--r-- | tests/unit_tests/completion_test.coffee | 34 |
13 files changed, 368 insertions, 167 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9382a020..a417caf5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,6 +74,34 @@ reports: When you're done with your changes, send us a pull request on Github. Feel free to include a change to the CREDITS file with your patch. +Vimium design goals +------------------- + +When improving Vimium it's helpful to know what design goals we're optimizing for. + +The core goal is to make it easy to navigate the web using just the keyboard. When people first start using +Vimium, it provides an incredibly powerful workflow improvement and it makes them feel awesome. And it turns +out that Vimium is applicable to a huge, broad population of people, not just users of Vim, which is great. + +A secondary goal is to make Vimium approachable, or in other words, to minimize the barriers which will +prevent a new user from feeling awesome. Many of Vimium's users haven't used Vim before (about 1 in 5 app +store reviews say this), and most people have strong web browsing habits forged from years of browsing that +they rely on. Given that, it's a great experience when Vimium feels like a natural addition to Chrome which +augments but doesn't break their current browsing habits. + +In some ways, making software approachable is even harder than just enabling the core use case. But in this +area, Vimium really shines. It's approachable today because: + +1. It's simple to understand (even if you're not very familiar with Vim). The Vimium video shows you all you + need to know to start using Vimium and feel awesome. +2. The core feature set works in almost all cases on all sites, so Vimium feels reliable. +3. Requires no configuration or doc-reading before it's useful. Just watch the video or hit `?`. +4. Doesn't drastically change the way Chrome looks or behaves. You can transition into using Vimium piecemeal; + you don't need to jump in whole-hog from the start. +5. The core feature set isn't overwhelming. This is easy to degrade as we evolve Vimium, so it requires active + effort to maintain this feel. +6. Developers find the code is relatively simple and easy to jump into, so we have an active dev community. + ## What makes for a good feature request/contribution to Vimium? Good features: @@ -96,23 +124,3 @@ We use these guidelines, in addition to the code complexity, when deciding wheth If you're worried that a feature you plan to build won't be a good fit for core Vimium, just open a github issue for discussion or send an email to the Vimium mailing list. - -## How to release Vimium to the Chrome Store - -This process is currently only done by Phil or Ilya. - -1. Increment the version number in manifest.json -2. Update the Changelog in README.md - - You can see a summary of commits since the last version: `git log --oneline v1.45..` - -3. Push your commits -4. Create a git tag for this newly released version - - git tag -a v1.45 -m "v1.45 release" - -5. Run `cake package` -6. Take the distributable found in `dist` and upload it - [here](https://chrome.google.com/webstore/developer/dashboard) -7. Update the description in the Chrome store to include the latest version's release notes -8. Celebrate @@ -3,8 +3,8 @@ Vimium - The Hacker's Browser [](https://travis-ci.org/philc/vimium) -Vimium is a Chrome extension that provides keyboard-based navigation and control in the spirit of the Vim -editor. +Vimium is a Chrome extension that provides keyboard-based navigation and control of the web in the spirit of +the Vim editor. __Installation instructions:__ @@ -15,14 +15,16 @@ Please see [CONTRIBUTING.md](https://github.com/philc/vimium/blob/master/CONTRIBUTING.md#installing-from-source) for instructions on how you can install Vimium from source. -The Options page can be reached via a link on the help dialog (hit `?`) or via the button next to Vimium on +The Options page can be reached via a link on the help dialog (type `?`) or via the button next to Vimium on the Chrome Extensions page (`chrome://extensions`). Keyboard Bindings ----------------- Modifier keys are specified as `<c-x>`, `<m-x>`, and `<a-x>` for ctrl+x, meta+x, and alt+x -respectively. See the next section for instructions on customizing these bindings. +respectively. See the next section for how to customize these bindings. + +Once you have Vimium installed, you can see this list of key bindings at any time by typing `?`. Navigating the current page: @@ -90,7 +92,7 @@ Additional advanced browsing commands: Vimium supports command repetition so, for example, hitting '5t' will open 5 tabs in rapid succession. `<ESC>` (or `<c-[>`) will clear any partial commands in the queue and will also exit insert and find modes. -There are some advanced commands which aren't documented here. Refer to the help dialog (type `?`) for a full +There are some advanced commands which aren't documented here; refer to the help dialog (type `?`) for a full list. Custom Key Mappings diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee index b411bcba..dc5519d5 100644 --- a/background_scripts/completion.coffee +++ b/background_scripts/completion.coffee @@ -259,26 +259,44 @@ class DomainCompleter # Suggestions from the Domain completer have the maximum relevancy. They should be shown first in the list. computeRelevancy: -> 1 -# TabRecency associates a logical timestamp with each tab id. +# TabRecency associates a logical timestamp with each tab id. These are used to provide an initial +# recency-based ordering in the tabs vomnibar (which allows jumping quickly between recently-visited tabs). class TabRecency - constructor: -> - @timestamp = 1 - @cache = {} + timestamp: 1 + current: -1 + cache: {} + lastVisited: null + lastVisitedTime: null + timeDelta: 500 # Milliseconds. - chrome.tabs.onActivated.addListener (activeInfo) => @add activeInfo.tabId - chrome.tabs.onRemoved.addListener (tabId) => @remove tabId + constructor: -> + chrome.tabs.onActivated.addListener (activeInfo) => @register activeInfo.tabId + chrome.tabs.onRemoved.addListener (tabId) => @deregister tabId chrome.tabs.onReplaced.addListener (addedTabId, removedTabId) => - @remove removedTabId - @add addedTabId + @deregister removedTabId + @register addedTabId + + register: (tabId) -> + currentTime = new Date() + # Register tabId if it has been visited for at least @timeDelta ms. Tabs which are visited only for a + # very-short time (e.g. those passed through with `5J`) aren't registered as visited at all. + if @lastVisitedTime? and @timeDelta <= currentTime - @lastVisitedTime + @cache[@lastVisited] = ++@timestamp + + @current = @lastVisited = tabId + @lastVisitedTime = currentTime - add: (tabId) -> @cache[tabId] = ++@timestamp - remove: (tabId) -> delete @cache[tabId] + deregister: (tabId) -> + if tabId == @lastVisited + # Ensure we don't register this tab, since it's going away. + @lastVisited = @lastVisitedTime = null + delete @cache[tabId] # Recently-visited tabs get a higher score (except the current tab, which gets a low score). recencyScore: (tabId) -> @cache[tabId] ||= 1 - if @cache[tabId] == @timestamp then 0.0 else @cache[tabId] / @timestamp + if tabId == @current then 0.0 else @cache[tabId] / @timestamp tabRecency = new TabRecency() diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index b40907fb..3ec618c9 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -8,7 +8,7 @@ keyQueue = "" # Queue of keys typed validFirstKeys = {} singleKeyCommands = [] focusedFrame = null -framesForTab = {} +frameIdsForTab = {} # Keys are either literal characters, or "named" - for example <a-b> (alt+b), <left> (left arrow) or <f12> # This regular expression captures two groups: the first is a named key, the second is the remainder of @@ -282,16 +282,14 @@ BackgroundCommands = { name: "toggleHelpDialog", dialogHtml: helpDialogHtml(), frameId:frameId })) moveTabLeft: (count) -> moveTab(null, -count) moveTabRight: (count) -> moveTab(null, count) - nextFrame: (count) -> + nextFrame: (count,frameId) -> chrome.tabs.getSelected(null, (tab) -> - frames = framesForTab[tab.id].frames - currIndex = getCurrFrameIndex(frames) - - # TODO: Skip the "top" frame (which doesn't actually have a <frame> tag), - # since it exists only to contain the other frames. - newIndex = (currIndex + count) % frames.length - - chrome.tabs.sendMessage(tab.id, { name: "focusFrame", frameId: frames[newIndex].id, highlight: true })) + frames = frameIdsForTab[tab.id] + # We can't always track which frame chrome has focussed, 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, frameIdsForTab[tab.id].indexOf frameId) % frames.length + frames = frameIdsForTab[tab.id] = [frames[count..]..., frames[0...count]...] + chrome.tabs.sendMessage(tab.id, { name: "focusFrame", frameId: frames[0], highlight: true })) closeTabsOnLeft: -> removeTabsRelative "before" closeTabsOnRight: -> removeTabsRelative "after" @@ -347,7 +345,7 @@ updateOpenTabs = (tab) -> scrollY: null deletor: null # Frames are recreated on refresh - delete framesForTab[tab.id] + delete frameIdsForTab[tab.id] setBrowserActionIcon = (tabId,path) -> chrome.browserAction.setIcon({ tabId: tabId, path: path }) @@ -394,7 +392,7 @@ chrome.tabs.onUpdated.addListener (tabId, changeInfo, tab) -> code: Settings.get("userDefinedLinkHintCss") runAt: "document_start" chrome.tabs.insertCSS tabId, cssConf, -> chrome.runtime.lastError - updateOpenTabs(tab) + updateOpenTabs(tab) if changeInfo.url? updateActiveState(tabId) chrome.tabs.onAttached.addListener (tabId, attachedInfo) -> @@ -429,7 +427,7 @@ chrome.tabs.onRemoved.addListener (tabId) -> # scroll position) tabInfoMap.deletor = -> delete tabInfoMap[tabId] setTimeout tabInfoMap.deletor, 1000 - delete framesForTab[tabId] + delete frameIdsForTab[tabId] chrome.tabs.onActiveChanged.addListener (tabId, selectInfo) -> updateActiveState(tabId) @@ -554,9 +552,9 @@ checkKeyQueue = (keysToCheck, tabId, frameId) -> refreshedCompletionKeys = true else if registryEntry.passCountToFunction - BackgroundCommands[registryEntry.command](count) + BackgroundCommands[registryEntry.command](count, frameId) else if registryEntry.noRepeat - BackgroundCommands[registryEntry.command]() + BackgroundCommands[registryEntry.command](frameId) else repeatFunction(BackgroundCommands[registryEntry.command], count, 0, frameId) @@ -603,21 +601,21 @@ openOptionsPageInNewTab = -> chrome.tabs.create({ url: chrome.runtime.getURL("pages/options.html"), index: tab.index + 1 })) registerFrame = (request, sender) -> - unless framesForTab[sender.tab.id] - framesForTab[sender.tab.id] = { frames: [] } - - if (request.is_top) - focusedFrame = request.frameId - framesForTab[sender.tab.id].total = request.total + (frameIdsForTab[sender.tab.id] ?= []).push request.frameId - framesForTab[sender.tab.id].frames.push({ id: request.frameId }) - -handleFrameFocused = (request, sender) -> focusedFrame = request.frameId +unregisterFrame = (request, sender) -> + tabId = sender.tab.id + if frameIdsForTab[tabId]? + if request.tab_is_closing + updateOpenTabs sender.tab + else + frameIdsForTab[tabId] = frameIdsForTab[tabId].filter (id) -> id != request.frameId -getCurrFrameIndex = (frames) -> - for i in [0...frames.length] - return i if frames[i].id == focusedFrame - frames.length + 1 +handleFrameFocused = (request, sender) -> + tabId = sender.tab.id + if frameIdsForTab[tabId]? + frameIdsForTab[tabId] = + [request.frameId, (frameIdsForTab[tabId].filter (id) -> id != request.frameId)...] # Port handler mapping portHandlers = @@ -633,6 +631,7 @@ sendRequestHandlers = openUrlInCurrentTab: openUrlInCurrentTab, openOptionsPageInNewTab: openOptionsPageInNewTab, registerFrame: registerFrame, + unregisterFrame: unregisterFrame, frameFocused: handleFrameFocused, upgradeNotificationClosed: upgradeNotificationClosed, updateScrollPosition: handleUpdateScrollPosition, @@ -640,7 +639,7 @@ sendRequestHandlers = isEnabledForUrl: isEnabledForUrl, saveHelpDialogSettings: saveHelpDialogSettings, selectSpecificTab: selectSpecificTab, - refreshCompleter: refreshCompleter + refreshCompleter: refreshCompleter, createMark: Marks.create.bind(Marks), gotoMark: Marks.goto.bind(Marks) diff --git a/background_scripts/settings.coffee b/background_scripts/settings.coffee index d6e8fcde..4a63a5fb 100644 --- a/background_scripts/settings.coffee +++ b/background_scripts/settings.coffee @@ -61,6 +61,7 @@ root.Settings = Settings = # or strings defaults: scrollStepSize: 60 + smoothScroll: true keyMappings: "# Insert your prefered key mappings here." linkHintCharacters: "sadfjklewcmpgh" linkHintNumbers: "0123456789" diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index b3a14c78..0964a289 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -1,14 +1,9 @@ -window.Scroller = root = {} - # # activatedElement is different from document.activeElement -- the latter seems to be reserved mostly for # input elements. This mechanism allows us to decide whether to scroll a div or to scroll the whole document. # activatedElement = null -root.init = -> - handlerStack.push DOMActivate: -> activatedElement = event.target - scrollProperties = x: { axisName: 'scrollLeft' @@ -21,74 +16,202 @@ scrollProperties = viewSize: 'clientWidth' } -getDimension = (el, direction, name) -> - # the clientSizes of the body are the dimensions of the entire page, but the viewport should only be the - # part visible through the window - if name is 'viewSize' and el is document.body - if direction is 'x' then window.innerWidth else window.innerHeight +# Translate a scroll request into a number (which will be interpreted by `scrollBy` as a relative amount, or +# by `scrollTo` as an absolute amount). :direction must be "x" or "y". :amount may be either a number (in +# which case it is simply returned) or a string. If :amount is a string, then it is either "max" (meaning the +# height or width of element), or "viewSize". In both cases, we look up and return the requested amount, +# either in `element` or in `window`, as appropriate. +getDimension = (el, direction, amount) -> + if Utils.isString amount + name = amount + # the clientSizes of the body are the dimensions of the entire page, but the viewport should only be the + # part visible through the window + if name is 'viewSize' and el is document.body + # TODO(smblott) Should we not be returning the width/height of element, here? + if direction is 'x' then window.innerWidth else window.innerHeight + else + el[scrollProperties[direction][name]] else - el[scrollProperties[direction][name]] + amount -# Chrome does not report scrollHeight accurately for nodes with pseudo-elements of height 0 (bug 110149). -# Therefore we cannot figure out if we have scrolled to the bottom of an element by testing if scrollTop + -# clientHeight == scrollHeight. So just try to increase scrollTop blindly -- if it fails we know we have -# reached the end of the content. -ensureScrollChange = (direction, changeFn) -> +# Perform a scroll. Return true if we successfully scrolled by the requested amount, and false otherwise. +performScroll = (element, direction, amount) -> axisName = scrollProperties[direction].axisName - element = activatedElement - loop - oldScrollValue = element[axisName] - # Elements with `overflow: hidden` should not be scrolled. - overflow = window.getComputedStyle(element).getPropertyValue("overflow-#{direction}") - changeFn(element, axisName) unless overflow == "hidden" - break unless element[axisName] == oldScrollValue && element != document.body - # we may have an orphaned element. if so, just scroll the body element. - element = element.parentElement || document.body - - # if the activated element has been scrolled completely offscreen, subsequent changes in its scroll - # position will not provide any more visual feedback to the user. therefore we deactivate it so that - # subsequent scrolls only move the parent element. + before = element[axisName] + element[axisName] += amount + element[axisName] == amount + before + +# Test whether `element` should be scrolled. E.g. hidden elements should not be scrolled. +shouldScroll = (element, direction) -> + computedStyle = window.getComputedStyle(element) + # Elements with `overflow: hidden` must not be scrolled. + return false if computedStyle.getPropertyValue("overflow-#{direction}") == "hidden" + # Elements which are not visible should not be scrolled. + return false if computedStyle.getPropertyValue("visibility") in ["hidden", "collapse"] + return false if computedStyle.getPropertyValue("display") == "none" + true + +# Test whether element does actually scroll in the direction required when asked to do so. Due to chrome bug +# 110149, scrollHeight and clientHeight cannot be used to reliably determine whether an element will scroll. +# Instead, we scroll the element by 1 or -1 and see if it moved (then put it back). :factor is the factor by +# which :scrollBy and :scrollTo will later scale the scroll amount. :factor can be negative, so we need it +# here in order to decide whether we should test a forward scroll or a backward scroll. +# Bug verified in Chrome 38.0.2125.104. +doesScroll = (element, direction, amount, factor) -> + # amount is treated as a relative amount, which is correct for relative scrolls. For absolute scrolls (only + # gg, G, and friends), amount can be either a string ("max" or "viewSize") or zero. In the former case, + # we're definitely scrolling forwards, so any positive value will do for delta. In the latter, we're + # definitely scrolling backwards, so a delta of -1 will do. For absolute scrolls, factor is always 1. + delta = factor * getDimension(element, direction, amount) || -1 + delta = Math.sign delta # 1 or -1 + performScroll(element, direction, delta) and performScroll(element, direction, -delta) + +# From element and its parents, find the first which we should scroll and which does scroll. +findScrollableElement = (element, direction, amount, factor) -> + while element != document.body and + not (doesScroll(element, direction, amount, factor) and shouldScroll(element, direction)) + element = element.parentElement || document.body + element + +checkVisibility = (element) -> + # If the activated element has been scrolled completely offscreen, then subsequent changes in its scroll + # position will not provide any more visual feedback to the user. Therefore, we deactivate it so that + # subsequent scrolls affect the parent element. rect = activatedElement.getBoundingClientRect() if (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth) activatedElement = element -# scroll the active element in :direction by :amount * :factor. -# :factor is needed because :amount can take on string values, which scrollBy converts to element dimensions. -root.scrollBy = (direction, amount, factor = 1) -> - # if this is called before domReady, just use the window scroll function - if (!document.body and amount instanceof Number) - if (direction == "x") - window.scrollBy(amount, 0) - else - window.scrollBy(0, amount) - return +# How scrolling is handled by CoreScroller. +# - For jump scrolling, the entire scroll happens immediately. +# - For smooth scrolling with distinct key presses, a separate animator is initiated for each key press. +# Therefore, several animators may be active at the same time. This ensures that two quick taps on `j` +# scroll to the same position as two slower taps. +# - For smooth scrolling with keyboard repeat (continuous scrolling), the most recently-activated animator +# continues scrolling at least until its keyup event is received. We never initiate a new animator on +# keyboard repeat. - if (!activatedElement || !isRendered(activatedElement)) - activatedElement = document.body +# CoreScroller contains the core function (scroll) and logic for relative scrolls. All scrolls are ultimately +# translated to relative scrolls. CoreScroller is not exported. +CoreScroller = + init: (frontendSettings) -> + @settings = frontendSettings + @time = 0 + @lastEvent = null + @keyIsDown = false - ensureScrollChange direction, (element, axisName) -> - if Utils.isString amount - elementAmount = getDimension element, direction, amount - else - elementAmount = amount - elementAmount *= factor - element[axisName] += elementAmount + handlerStack.push + keydown: (event) => + @keyIsDown = true + @lastEvent = event + keyup: => + @keyIsDown = false + @time += 1 -root.scrollTo = (direction, pos) -> - return unless document.body + # Return true if CoreScroller would not initiate a new scroll right now. + wouldNotInitiateScroll: -> @lastEvent?.repeat and @settings.get "smoothScroll" - if (!activatedElement || !isRendered(activatedElement)) - activatedElement = document.body + # Calibration fudge factors for continuous scrolling. The calibration value starts at 1.0. We then + # increase it (until it exceeds @maxCalibration) if we guess that the scroll is too slow, or decrease it + # (until it is less than @minCalibration) if we guess that the scroll is too fast. The cutoff point for + # which guess we make is @calibrationBoundary. We require: 0 < @minCalibration <= 1 <= @maxCalibration. + minCalibration: 0.5 # Controls how much we're willing to slow scrolls down; smaller means more slow down. + maxCalibration: 1.6 # Controls how much we're willing to speed scrolls up; bigger means more speed up. + calibrationBoundary: 150 # Boundary between scrolls which are considered too slow, or too fast. - ensureScrollChange direction, (element, axisName) -> - if Utils.isString pos - elementPos = getDimension element, direction, pos - else - elementPos = pos - element[axisName] = elementPos - -# TODO refactor and put this together with the code in getVisibleClientRect -isRendered = (element) -> - computedStyle = window.getComputedStyle(element, null) - return !(computedStyle.getPropertyValue("visibility") != "visible" || - computedStyle.getPropertyValue("display") == "none") + # Scroll element by a relative amount (a number) in some direction. + scroll: (element, direction, amount) -> + return unless amount + + unless @settings.get "smoothScroll" + # Jump scrolling. + performScroll element, direction, amount + checkVisibility element + return + + # We don't activate new animators on keyboard repeats; rather, the most-recently activated animator + # continues scrolling. + return if @lastEvent?.repeat + + activationTime = ++@time + myKeyIsStillDown = => @time == activationTime and @keyIsDown + + # Store amount's sign and make amount positive; the arithmetic is clearer when amount is positive. + sign = Math.sign amount + amount = Math.abs amount + + # Initial intended scroll duration (in ms). We allow a bit longer for longer scrolls. + duration = Math.max 100, 20 * Math.log amount + + totalDelta = 0 + totalElapsed = 0.0 + calibration = 1.0 + previousTimestamp = null + + animate = (timestamp) => + previousTimestamp ?= timestamp + return requestAnimationFrame(animate) if timestamp == previousTimestamp + + # The elapsed time is typically about 16ms. + elapsed = timestamp - previousTimestamp + totalElapsed += elapsed + previousTimestamp = timestamp + + # The constants in the duration calculation, above, are chosen to provide reasonable scroll speeds for + # distinct keypresses. For continuous scrolls, some scrolls are too slow, and others too fast. Here, we + # speed up the slower scrolls, and slow down the faster scrolls. + if myKeyIsStillDown() and 75 <= totalElapsed and @minCalibration <= calibration <= @maxCalibration + calibration *= 1.05 if 1.05 * calibration * amount < @calibrationBoundary # Speed up slow scrolls. + calibration *= 0.95 if @calibrationBoundary < 0.95 * calibration * amount # Slow down fast scrolls. + + # Calculate the initial delta, rounding up to ensure progress. Then, adjust delta to account for the + # current scroll state. + delta = Math.ceil amount * (elapsed / duration) * calibration + delta = if myKeyIsStillDown() then delta else Math.max 0, Math.min delta, amount - totalDelta + + if delta and performScroll element, direction, sign * delta + totalDelta += delta + requestAnimationFrame animate + else + # We're done. + checkVisibility element + + # Launch animator. + requestAnimationFrame animate + +# Scroller contains the two main scroll functions (scrollBy and scrollTo) which are exported to clients. +Scroller = + init: (frontendSettings) -> + handlerStack.push DOMActivate: -> activatedElement = event.target + CoreScroller.init frontendSettings + + # scroll the active element in :direction by :amount * :factor. + # :factor is needed because :amount can take on string values, which scrollBy converts to element dimensions. + scrollBy: (direction, amount, factor = 1) -> + # if this is called before domReady, just use the window scroll function + if (!document.body and amount instanceof Number) + if (direction == "x") + window.scrollBy(amount, 0) + else + window.scrollBy(0, amount) + return + + activatedElement ||= document.body + return unless activatedElement + + # Avoid the expensive scroll calculation if it will not be used. This reduces costs during smooth, + # continuous scrolls, and is just an optimization. + unless CoreScroller.wouldNotInitiateScroll() + element = findScrollableElement activatedElement, direction, amount, factor + elementAmount = factor * getDimension element, direction, amount + CoreScroller.scroll element, direction, elementAmount + + scrollTo: (direction, pos) -> + return unless document.body or activatedElement + activatedElement ||= document.body + + element = findScrollableElement activatedElement, direction, pos, 1 + amount = getDimension(element,direction,pos) - element[scrollProperties[direction].axisName] + CoreScroller.scroll element, direction, amount + +root = exports ? window +root.Scroller = Scroller diff --git a/content_scripts/vimium.css b/content_scripts/vimium.css index 7998fe5c..a8ae5583 100644 --- a/content_scripts/vimium.css +++ b/content_scripts/vimium.css @@ -2,6 +2,10 @@ * Many CSS class names in this file use the verbose "vimiumXYZ" as the class name. This is so we don't * use the same CSS class names that the page is using, so the page's CSS doesn't mess with the style of our * Vimium dialogs. + * + * The z-indexes of Vimium elements are very large, because we always want them to show on top. Chrome may + * support up to Number.MAX_VALUE, which is approximately 1.7976e+308. We're using 2**31, which is the max value of a singed 32 bit int. + * Let's use try larger valeus if 2**31 empirically isn't large enough. */ /* @@ -54,7 +58,7 @@ tr.vimiumReset { vertical-align: baseline; white-space: normal; width: auto; - z-index: 99999999; + z-index: 2147483648; } /* Linkhints CSS */ @@ -122,7 +126,8 @@ div#vimiumHelpDialog { top:50px; -webkit-box-shadow: rgba(0, 0, 0, 0.4) 0px 0px 6px; overflow-y: auto; - z-index:99999998; + /* One less than vimiumReset */ + z-index: 2147483647; } div#vimiumHelpDialog a { color:blue; } @@ -233,8 +238,8 @@ div.vimiumHUD { border-radius: 4px 4px 0 0; font-family: "Lucida Grande", "Arial", "Sans"; font-size: 12px; - /* One less than vimium's hint markers, so link hints can be shown e.g. for the panel's close button. */ - z-index: 99999997; + /* One less than vimium's hint markers, so link hints can be shown e.g. for the HUD panel's close button. */ + z-index: 2147483646; text-shadow: 0px 1px 2px #FFF; line-height: 1.0; opacity: 0; @@ -291,7 +296,7 @@ body.vimiumFindMode ::selection { box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.8); border: 1px solid #aaa; /* One less than hint markers and the help dialog. */ - z-index: 99999996; + z-index: 2147483645; } #vomnibar input { @@ -401,5 +406,5 @@ div#vimiumFlash { padding: 1px; background-color: transparent; position: absolute; - z-index: 99999; + z-index: 2147483648; } diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index 118f985e..469afe71 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -49,7 +49,7 @@ settings = loadedValues: 0 valuesToLoad: ["scrollStepSize", "linkHintCharacters", "linkHintNumbers", "filterLinkHints", "hideHud", "previousPatterns", "nextPatterns", "findModeRawQuery", "regexFindMode", "userDefinedLinkHintCss", - "helpDialog_showAdvancedCommands"] + "helpDialog_showAdvancedCommands", "smoothScroll"] isLoaded: false eventListeners: {} @@ -101,7 +101,7 @@ initializePreDomReady = -> settings.addEventListener("load", LinkHints.init.bind(LinkHints)) settings.load() - Scroller.init() + Scroller.init settings checkIfEnabledForUrl() @@ -179,19 +179,24 @@ window.addEventListener "focus", -> # Initialization tasks that must wait for the document to be ready. # initializeOnDomReady = -> - registerFrame(window.top == window.self) - enterInsertModeIfElementIsFocused() if isEnabledForUrl # Tell the background page we're in the dom ready state. chrome.runtime.connect({ name: "domReady" }) -registerFrame = (is_top) -> - chrome.runtime.sendMessage( - handler: "registerFrame" +registerFrame = -> + # Don't register frameset containers; focusing them is no use. + if document.body.tagName != "FRAMESET" + chrome.runtime.sendMessage + handler: "registerFrame" + frameId: frameId + +# Unregister the frame if we're going to exit. +unregisterFrame = -> + chrome.runtime.sendMessage + handler: "unregisterFrame" frameId: frameId - is_top: is_top - total: frames.length + 1) + tab_is_closing: window.top == window.self # # Enters insert mode if the currently focused element in the DOM is focusable. @@ -367,7 +372,7 @@ onKeypress = (event) -> else if (!isInsertMode() && !findMode) if (isPassKey keyChar) return undefined - if (currentCompletionKeys.indexOf(keyChar) != -1) + if (currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar)) DomUtils.suppressEvent(event) keyPort.postMessage({ keyChar:keyChar, frameId:frameId }) @@ -440,7 +445,7 @@ onKeydown = (event) -> else if (!isInsertMode() && !findMode) if (keyChar) - if (currentCompletionKeys.indexOf(keyChar) != -1) + if (currentCompletionKeys.indexOf(keyChar) != -1 or isValidFirstKey(keyChar)) DomUtils.suppressEvent event handledKeydownEvents.push event @@ -502,7 +507,7 @@ refreshCompletionKeys = (response) -> chrome.runtime.sendMessage({ handler: "getCompletionKeys" }, refreshCompletionKeys) isValidFirstKey = (keyChar) -> - validFirstKeys[keyChar] || /[1-9]/.test(keyChar) + validFirstKeys[keyChar] || /^[1-9]/.test(keyChar) onFocusCapturePhase = (event) -> if (isFocusable(event.target) && !findMode) @@ -529,6 +534,7 @@ isEmbed = (element) -> ["embed", "object"].indexOf(element.nodeName.toLowerCase( # any element which makes it a rich text editor, like the notes on jjot.com. # isEditable = (target) -> + # Note: document.activeElement.isContentEditable is also rechecked in isInsertMode() dynamically. return true if target.isContentEditable nodeName = target.nodeName.toLowerCase() # use a blacklist instead of a whitelist because new form controls are still being implemented for html5 @@ -552,6 +558,7 @@ window.enterInsertMode = (target) -> # when the last editable element that came into focus -- which insertModeLock points to -- has been blurred. # If insert mode is entered manually (via pressing 'i'), then we set insertModeLock to 'undefined', and only # leave insert mode when the user presses <ESC>. +# Note. This returns the truthiness of target, which is required by isInsertMode. # enterInsertModeWithoutShowingIndicator = (target) -> insertModeLock = target @@ -560,7 +567,13 @@ exitInsertMode = (target) -> insertModeLock = null HUD.hide() -isInsertMode = -> insertModeLock != null +isInsertMode = -> + return true if insertModeLock != null + # Some sites (e.g. inbox.google.com) change the contentEditable attribute on the fly (see #1245); and + # unfortunately, isEditable() is called *before* the change is made. Therefore, we need to re-check whether + # the active element is contentEditable. + document.activeElement and document.activeElement.isContentEditable and + enterInsertModeWithoutShowingIndicator document.activeElement # should be called whenever rawQuery is modified. updateFindModeQuery = -> @@ -1055,6 +1068,8 @@ Tween = state.onUpdate(value) initializePreDomReady() +window.addEventListener("DOMContentLoaded", registerFrame) +window.addEventListener("unload", unregisterFrame) window.addEventListener("DOMContentLoaded", initializeOnDomReady) window.onbeforeunload = -> diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index 62e655e7..21018049 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -32,8 +32,8 @@ DomUtils = # makeXPath: (elementArray) -> xpath = [] - for i of elementArray - xpath.push("//" + elementArray[i], "//xhtml:" + elementArray[i]) + for element in elementArray + xpath.push("//" + element, "//xhtml:" + element) xpath.join(" | ") evaluateXPath: (xpath, resultType) -> diff --git a/pages/options.coffee b/pages/options.coffee index 674f0b98..cd19fa37 100644 --- a/pages/options.coffee +++ b/pages/options.coffee @@ -177,6 +177,7 @@ document.addEventListener "DOMContentLoaded", -> previousPatterns: NonEmptyTextOption regexFindMode: CheckBoxOption scrollStepSize: NumberOption + smoothScroll: CheckBoxOption searchEngines: TextOption searchUrl: NonEmptyTextOption userDefinedLinkHintCss: TextOption diff --git a/pages/options.html b/pages/options.html index 68b615e3..b52974d6 100644 --- a/pages/options.html +++ b/pages/options.html @@ -361,6 +361,15 @@ unmapAll </td> </tr> <tr> + <td class="caption" verticalAlign="top">Miscellaneous<br/>options</td> + <td verticalAlign="top" class="booleanOption"> + <label> + <input id="smoothScroll" type="checkbox"/> + Use smooth scrolling + </label> + </td> + </tr> + <tr> <td class="caption"></td> <td verticalAlign="top" class="booleanOption"> <div class="help"> @@ -375,7 +384,7 @@ unmapAll </td> </tr> <tr> - <td class="caption">Miscellaneous<br/>options</td> + <td class="caption"></td> <td verticalAlign="top" class="booleanOption"> <div class="help"> <div class="example"> @@ -398,7 +407,7 @@ unmapAll </div> <label> <input id="regexFindMode" type="checkbox"/> - Treat find queries as regular expressions. + Treat find queries as regular expressions </label> </td> </tr> diff --git a/pages/popup.coffee b/pages/popup.coffee index 2ab97bef..99a4eb87 100644 --- a/pages/popup.coffee +++ b/pages/popup.coffee @@ -3,6 +3,17 @@ originalRule = undefined originalPattern = undefined originalPassKeys = undefined +generateDefaultPattern = (url) -> + if /^https?:\/\/./.test url + # The common use case is to disable Vimium at the domain level. + # Generate "https?://www.example.com/*" from "http://www.example.com/path/to/page.html". + "https?:/" + url.split("/",3)[1..].join("/") + "/*" + else if /^[a-z]{3,}:\/\/./.test url + # Anything else which seems to be a URL. + url.split("/",3).join("/") + "/*" + else + url + "*" + reset = (initialize=false) -> document.getElementById("optionsLink").setAttribute "href", chrome.runtime.getURL("pages/options.html") chrome.tabs.getSelected null, (tab) -> @@ -13,11 +24,8 @@ reset = (initialize=false) -> originalPattern = originalRule.pattern originalPassKeys = originalRule.passKeys else - # The common use case is to disable Vimium at the domain level. - # This regexp will match "http://www.example.com/" from "http://www.example.com/path/to/page.html". - domain = (tab.url.match(/[^\/]*\/\/[^\/]*\//) or tab.url) + "*" originalRule = null - originalPattern = domain + originalPattern = generateDefaultPattern tab.url originalPassKeys = "" patternElement = document.getElementById("popupPattern") passKeysElement = document.getElementById("popupPassKeys") diff --git a/tests/unit_tests/completion_test.coffee b/tests/unit_tests/completion_test.coffee index 88f59b7e..e4966016 100644 --- a/tests/unit_tests/completion_test.coffee +++ b/tests/unit_tests/completion_test.coffee @@ -399,21 +399,32 @@ context "RegexpCache", should "search for a string with a prefix/suffix (negative case)", -> assert.isTrue "hound dog".search(RegexpCache.get("do", "\\b", "\\b")) == -1 +fakeTimeDeltaElapsing = -> + context "TabRecency", setup -> @tabRecency = new TabRecency() - @tabRecency.add 3 - @tabRecency.add 2 - @tabRecency.add 9 - @tabRecency.add 1 - @tabRecency.remove 9 - @tabRecency.add 4 - - should "have entries for active tabs", -> + + fakeTimeDeltaElapsing = => + if @tabRecency.lastVisitedTime? + @tabRecency.lastVisitedTime = new Date(@tabRecency.lastVisitedTime - @tabRecency.timeDelta) + + @tabRecency.register 3 + fakeTimeDeltaElapsing() + @tabRecency.register 2 + fakeTimeDeltaElapsing() + @tabRecency.register 9 + fakeTimeDeltaElapsing() + @tabRecency.register 1 + @tabRecency.deregister 9 + fakeTimeDeltaElapsing() + @tabRecency.register 4 + fakeTimeDeltaElapsing() + + should "have entries for recently active tabs", -> assert.isTrue @tabRecency.cache[1] assert.isTrue @tabRecency.cache[2] assert.isTrue @tabRecency.cache[3] - assert.isTrue @tabRecency.cache[4] should "not have entries for removed tabs", -> assert.isFalse @tabRecency.cache[9] @@ -431,8 +442,9 @@ context "TabRecency", should "rank tabs by recency", -> assert.isTrue @tabRecency.recencyScore(3) < @tabRecency.recencyScore 2 assert.isTrue @tabRecency.recencyScore(2) < @tabRecency.recencyScore 1 - @tabRecency.add 3 - @tabRecency.add 4 # Making 3 the most recent tab which isn't the current tab. + @tabRecency.register 3 + fakeTimeDeltaElapsing() + @tabRecency.register 4 # Making 3 the most recent tab which isn't the current tab. assert.isTrue @tabRecency.recencyScore(1) < @tabRecency.recencyScore 3 assert.isTrue @tabRecency.recencyScore(2) < @tabRecency.recencyScore 3 assert.isTrue @tabRecency.recencyScore(4) < @tabRecency.recencyScore 3 |
