aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStephen Blott2015-02-06 15:34:59 +0000
committerStephen Blott2015-02-06 17:03:18 +0000
commit14f259479bd9e398d477b1d535719407b9c0c618 (patch)
treea830086e3efd9ce0bf6c595efa62912e72b8aa74
parent449462b2580478b9c6d8b35f05231aad989d01aa (diff)
downloadvimium-14f259479bd9e398d477b1d535719407b9c0c618.tar.bz2
Visual/edit modes: code cleanup.
- convert getCaretCoordinates from JS to CS - handle x axis in scrollIntoView - better comments throughout.
-rw-r--r--content_scripts/mode_visual_edit.coffee58
-rw-r--r--content_scripts/scroller.coffee42
-rw-r--r--lib/dom_utils.coffee131
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