diff options
| -rw-r--r-- | content_scripts/mode_visual_edit.coffee | 58 | ||||
| -rw-r--r-- | content_scripts/scroller.coffee | 42 | ||||
| -rw-r--r-- | lib/dom_utils.coffee | 131 |
3 files changed, 99 insertions, 132 deletions
diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index ba0bc307..e11c29ec 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -26,7 +26,7 @@ # - ..., EditMode, VisualLineMode # -# This prevents printable characters from being passed through to underlying modes or to the underlying page. +# This prevents printable characters from being passed through to underlying modes or the underlying page. class SuppressPrintable extends Mode constructor: (options = {}) -> handler = (event) => @@ -44,7 +44,8 @@ class SuppressPrintable extends Mode class CountPrefix extends SuppressPrintable constructor: (options) -> @countPrefix = "" - # This allows us to implement both "d3w" and "3dw". Also, "3d2w" deletes six words. + # This is an initial multiplier for the first count. It allows edit mode to implement both "d3w" and + # "3dw". Also, "3d2w" deletes six words. @countPrefixFactor = options.initialCountPrefix || 1 super options @@ -61,9 +62,8 @@ class CountPrefix extends SuppressPrintable "" getCountPrefix: -> - count = @countPrefixFactor * if 0 < @countPrefix?.length then parseInt @countPrefix else 1 - @countPrefix = "" - @countPrefixFactor = 1 + count = @countPrefixFactor * (if 0 < @countPrefix.length then parseInt @countPrefix else 1) + @countPrefix = ""; @countPrefixFactor = 1 count # Symbolic names for some common strings. @@ -81,9 +81,11 @@ lineboundary= "lineboundary" class Movement extends CountPrefix opposite: forward: backward, backward: forward + # Paste from clipboard. paste: (callback) -> chrome.runtime.sendMessage handler: "pasteFromClipboard", (response) -> callback response + # Copy to clipboard. copy: (text, isFinalUserCopy = false) -> chrome.runtime.sendMessage handler: "copyToClipboard", data: text # If isFinalUserCopy is set, then we're copying the final text selected by the user (and exiting). @@ -105,7 +107,8 @@ class Movement extends CountPrefix @paste (text) => func(); @copy text; locked = false - # Replace the current mode with another. For example, replace visual mode with visual-line mode. + # Replace the current mode with another. For example, replace caret mode with visual mode, or replace visual + # mode with visual-line mode. changeMode: (mode, options = {}) -> @exit() if @options.parentMode @@ -114,7 +117,7 @@ class Movement extends CountPrefix new mode options # Return the character following (to the right of) the focus, and leave the selection unchanged. Returns - # undefined if there is no such character. + # undefined if no such character exists. getNextForwardCharacter: -> beforeText = @selection.toString() if beforeText.length == 0 or @getDirection() == forward @@ -124,7 +127,7 @@ class Movement extends CountPrefix @selection.modify "extend", backward, character afterText[afterText.length - 1] else - beforeText[0] + beforeText[0] # Existing range selection is backwards. # As above, but backwards. getNextBackwardCharacter: -> @@ -136,9 +139,9 @@ class Movement extends CountPrefix @selection.modify "extend", forward, character afterText[0] else - beforeText[beforeText.length - 1] + beforeText[beforeText.length - 1] # Existing range selection is forwards. - # Test whether the character following the focus is a word character. Leave the selection unchanged. + # Test whether the character following the focus is a word character (and leave the selection unchanged). nextCharacterIsWordCharacter: do -> regexp = /[A-Za-z0-9_]/; -> regexp.test @getNextForwardCharacter() @@ -148,8 +151,8 @@ class Movement extends CountPrefix # @runMovement [ "forward", "word" ] # @runMovement "forward", "word" # - # The granularities are word, "line", "lineboundary", "sentence" and "paragraph". In addition, we implement - # the pseudo granularity "vimword", which implements vim-like word movement (for "w"). + # The granularities are word, "character", "line", "lineboundary", "sentence" and "paragraph". In addition, + # we implement the pseudo granularity "vimword", which implements vim-like word movement (for "w"). # runMovement: (args...) -> # Normalize the various argument forms. @@ -217,8 +220,8 @@ class Movement extends CountPrefix which = if direction == forward then "start" else "end" @selection.extend original["#{which}Container"], original["#{which}Offset"] - # Try to extend the selection one character in direction. Return 1, -1 or 0, indicating whether the - # selection got bigger, or smaller, or is unchanged. + # Try to extend the selection one character in direction. Return positive, negative or 0, indicating + # whether the selection got bigger, or smaller, or is unchanged. extendByOneCharacter: (direction) -> length = @selection.toString().length @selection.modify "extend", direction, character @@ -226,8 +229,8 @@ class Movement extends CountPrefix # Get the direction of the selection. The selection is "forward" if the focus is at or after the anchor, # and "backward" otherwise. - # NOTE(smblott). This could be better, see: https://dom.spec.whatwg.org/#interface-range (haowever, that probably - # wouldn't work for text inputs). + # NOTE(smblott). This could be better, see: https://dom.spec.whatwg.org/#interface-range (however, that + # probably wouldn't work for text inputs). getDirection: -> # Try to move the selection forward or backward, check whether it got bigger or smaller (then restore it). for direction in [ forward, backward ] @@ -248,8 +251,8 @@ class Movement extends CountPrefix @selection.removeAllRanges() @selection.addRange range - # A movement can be a string (which will be passed to @runMovement count times), or a function (which will - # be called once with count as its argument). + # A movement can be either a string (which will be passed to @runMovement count times), or a function (which + # will be called once with count as its argument). movements: "l": "forward character" "h": "backward character" @@ -309,7 +312,8 @@ class Movement extends CountPrefix @runMovementKeyChar @options.immediateMovement, @getCountPrefix() return - # This is the main keyboard-event handler for movements and commands. + # This is the main keyboard-event handler for movements and commands for all user modes (visual, + # visual-line, caret and edit). @push _name: "#{@id}/keypress" keypress: (event) => @@ -357,24 +361,24 @@ class Movement extends CountPrefix @movements.n = (count) -> executeFind count, false @movements.N = (count) -> executeFind count, true @movements["/"] = -> - @findMode = enterFindMode() + @findMode = window.enterFindMode() @findMode.onExit => @changeMode VisualMode # # End of Movement constructor. - # Yank the selection; always exits; either deletes the selection or collapses it; set @yankedText and - # returns it. + # Yank the selection; always exits; either deletes the selection or removes it; set @yankedText and return + # it. yank: (args = {}) -> @yankedText = @selection.toString() @selection.deleteFromDocument() if @options.deleteFromDocument or args.deleteFromDocument - @selection.removeAllRanges() + @selection.removeAllRanges() unless @options.parentMode message = @yankedText.replace /\s+/g, " " message = message[...12] + "..." if 15 < @yankedText.length plural = if @yankedText.length == 1 then "" else "s" HUD.showForDuration "Yanked #{@yankedText.length} character#{plural}: \"#{message}\".", 2500 - @options.onYank.call @, @yankedText if @options.onYank + @options.onYank?.call @, @yankedText @exit() @yankedText @@ -434,15 +438,14 @@ class Movement extends CountPrefix char = @getNextForwardCharacter() @runMovement forward, character - # Try to scroll the focus into view. + # Scroll the focus into view. scrollIntoView: -> @protectClipboard => if @element and DomUtils.isEditable @element if @element.clientHeight < @element.scrollHeight if @element.isContentEditable - # WIP... + # WIP (edit mode only)... elementWithFocus = DomUtils.getElementWithFocus @selection, @getDirection() == backward - console.log elementWithFocus.innerHTML # position = @element.getClientRects()[0].top - elementWithFocus.getClientRects()[0].top # console.log "top", position # Scroller.scrollToPosition @element, position, 0 @@ -547,6 +550,7 @@ class VisualMode extends Movement console.log "yank:", @yankedText if @debug @copy @yankedText, true + # Call sub-class; then yank, if we've only been created for a single movement. handleMovementKeyChar: (args...) -> super args... @yank() if @options.oneMovementOnly or @options.immediateMovement diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index 6d224814..08cc0779 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -209,7 +209,7 @@ CoreScroller = # Launch animator. requestAnimationFrame animate -# Scroller contains the two main scroll functions (scrollBy and scrollTo) which are exported to clients. +# Scroller contains the two main scroll functions which are used by clients. Scroller = init: (frontendSettings) -> handlerStack.push @@ -246,31 +246,43 @@ Scroller = amount = getDimension(element,direction,pos) - element[scrollProperties[direction].axisName] CoreScroller.scroll element, direction, amount - # FIXME(smblott). We should also scroll in the "x" dimension. + # Scroll the top, bottom, left and right of element into view. The is used by visual mode to ensure the + # focus remains visible. scrollIntoView: (element) -> activatedElement ||= document.body and firstScrollableElement() - rect = element.getBoundingClientRect() - direction = "y" - if rect.top < 0 - amount = rect.top - 10 - element = findScrollableElement element, direction, amount, 1 - CoreScroller.scroll element, direction, amount, false - else if window.innerHeight < rect.bottom - amount = rect.bottom - window.innerHeight + 10 - element = findScrollableElement element, direction, amount, 1 - CoreScroller.scroll element, direction, amount, false - + rect = element. getClientRects()?[0] + if rect? + # Scroll y axis. + if rect.top < 0 + amount = rect.top - 10 + element = findScrollableElement element, "y", amount, 1 + CoreScroller.scroll element, "y", amount, false + else if window.innerHeight < rect.bottom + amount = rect.bottom - window.innerHeight + 10 + element = findScrollableElement element, "y", amount, 1 + CoreScroller.scroll element, "y", amount, false + + # Scroll x axis. + if rect.left < 0 + amount = rect.left - 10 + element = findScrollableElement element, "x", amount, 1 + CoreScroller.scroll element, "x", amount, false + else if window.innerWidth < rect.right + amount = rect.right - window.innerWidth + 10 + element = findScrollableElement element, "x", amount, 1 + CoreScroller.scroll element, "x", amount, false + + # Scroll element to position top, left. This is used by edit mode to ensure that the caret remains visible + # in text inputs (not contentEditable). scrollToPosition: (element, top, left) -> activatedElement ||= document.body and firstScrollableElement() # Scroll down, "y". amount = top + 20 - (element.clientHeight + element.scrollTop) - console.log "y down", amount, 0 < amount CoreScroller.scroll element, "y", amount, false if 0 < amount # Scroll up, "y". amount = top - (element.scrollTop) - 5 - console.log "y up", amount, amount < 0 CoreScroller.scroll element, "y", amount, false if amount < 0 # Scroll down, "x". diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index 9360bb95..2ae9412e 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -250,99 +250,50 @@ DomUtils = t = o || t?.parentNode t -extend DomUtils, + # This calculates the caret coordinates within an input element. It is used by edit mode to calculate the + # caret position for scrolling. It creates a hidden div contain a mirror of element, and all of the text + # from element up to position, then calculates the scroll position. # From: https://github.com/component/textarea-caret-position/blob/master/index.js getCaretCoordinates: do -> - # The properties that we copy into a mirrored div. - # Note that some browsers, such as Firefox, - # do not concatenate properties, i.e. padding-top, bottom etc. -> padding, - # so we have to do every single property specifically. + # The properties that we copy to the mirrored div. properties = [ - 'direction', # RTL support - 'boxSizing', - 'width', # on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does - 'height', - 'overflowX', - 'overflowY', # copy the scrollbar for IE - - 'borderTopWidth', - 'borderRightWidth', - 'borderBottomWidth', - 'borderLeftWidth', - - 'paddingTop', - 'paddingRight', - 'paddingBottom', - 'paddingLeft', - - # https://developer.mozilla.org/en-US/docs/Web/CSS/font - 'fontStyle', - 'fontVariant', - 'fontWeight', - 'fontStretch', - 'fontSize', - 'fontSizeAdjust', - 'lineHeight', - 'fontFamily', - - 'textAlign', - 'textTransform', - 'textIndent', - 'textDecoration', # might not make a difference, but better be safe - - 'letterSpacing', - 'wordSpacing' - ] - - `function (element, position, recalculate) { - // mirrored div - var div = document.createElement('div'); - div.id = 'input-textarea-caret-position-mirror-div'; - document.body.appendChild(div); - - var style = div.style; - var computed = window.getComputedStyle? getComputedStyle(element) : element.currentStyle; // currentStyle for IE < 9 - - // default textarea styles - style.whiteSpace = 'pre-wrap'; - if (element.nodeName !== 'INPUT') - style.wordWrap = 'break-word'; // only for textarea-s - - // position off-screen - style.position = 'absolute'; // required to return coordinates properly - style.visibility = 'hidden'; // not 'display: none' because we want rendering - - // transfer the element's properties to the div - properties.forEach(function (prop) { - style[prop] = computed[prop]; - }); - - style.overflow = 'hidden'; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll' - - div.textContent = element.value.substring(0, position); - // the second special handling for input type="text" vs textarea: spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037 - if (element.nodeName === 'INPUT') - div.textContent = div.textContent.replace(/\s/g, "\u00a0"); - - var span = document.createElement('span'); - // Wrapping must be replicated *exactly*, including when a long word gets - // onto the next line, with whitespace at the end of the line before (#7). - // The *only* reliable way to do that is to copy the *entire* rest of the - // textarea's content into the <span> created at the caret position. - // for inputs, just '.' would be enough, but why bother? - span.textContent = element.value.substring(position) || '.'; // || because a completely empty faux span doesn't render at all - div.appendChild(span); - - var coordinates = { - top: span.offsetTop + parseInt(computed['borderTopWidth']), - left: span.offsetLeft + parseInt(computed['borderLeftWidth']) - }; - - document.body.removeChild(div); - - return coordinates; - } - ` + 'direction', 'boxSizing', 'width', 'height', 'overflowX', 'overflowY', + 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth', + 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', + 'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch', 'fontSize', 'fontSizeAdjust', + 'lineHeight', 'fontFamily', + 'textAlign', 'textTransform', 'textIndent', 'textDecoration', + 'letterSpacing', 'wordSpacing' ] + + (element, position) -> + div = document.createElement "div" + div.id = "vimium-input-textarea-caret-position-mirror-div" + document.body.appendChild div + + style = div.style + computed = getComputedStyle element + + style.whiteSpace = "pre-wrap" + style.wordWrap = "break-word" if element.nodeName.toLowerCase() != "input" + style.position = "absolute" + style.visibility = "hidden" + style[prop] = computed[prop] for prop in properties + style.overflow = "hidden" + + div.textContent = element.value.substring 0, position + if element.nodeName.toLowerCase() == "input" + div.textContent = div.textContent.replace /\s/g, "\u00a0" + + span = document.createElement "span" + span.textContent = element.value.substring(position) || "." + div.appendChild span + + coordinates = + top: span.offsetTop + parseInt computed["borderTopWidth"] + left: span.offsetLeft + parseInt computed["borderLeftWidth"] + + document.body.removeChild div + coordinates root = exports ? window root.DomUtils = DomUtils |
