aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStephen Blott2015-01-01 12:02:16 +0000
committerStephen Blott2015-01-01 16:22:47 +0000
commit2d047e7ee7e77a02ccb29658ada953a092cee20a (patch)
tree9dee44c265595c4864376fd2c736c4cab2508739
parentaed5e2b5e1015a2e581edadbc5dd2d1b5a2719f4 (diff)
downloadvimium-2d047e7ee7e77a02ccb29658ada953a092cee20a.tar.bz2
Modes; implement insert mode.
-rw-r--r--content_scripts/mode.coffee29
-rw-r--r--content_scripts/mode_insert.coffee70
-rw-r--r--content_scripts/vimium_frontend.coffee55
-rw-r--r--lib/handler_stack.coffee2
-rw-r--r--manifest.json1
5 files changed, 98 insertions, 59 deletions
diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee
index e4b6017c..88938e79 100644
--- a/content_scripts/mode.coffee
+++ b/content_scripts/mode.coffee
@@ -3,6 +3,8 @@ class Mode
# Static members.
@modes: []
@current: -> Mode.modes[0]
+
+ # Constants. Static.
@suppressPropagation = false
@propagate = true
@@ -12,8 +14,6 @@ class Mode
keydown: "suppress" # A function, or "suppress" or "pass"; the latter are replaced with suitable functions.
keypress: "suppress" # A function, or "suppress" or "pass"; the latter are replaced with suitable functions.
keyup: "suppress" # A function, or "suppress" or "pass"; the latter are replaced with suitable functions.
- onDeactivate: -> # Called when leaving this mode.
- onReactivate: -> # Called when this mode is reactivated.
constructor: (options) ->
extend @, options
@@ -22,10 +22,6 @@ class Mode
keydown: @checkForBuiltInHandler "keydown", @keydown
keypress: @checkForBuiltInHandler "keypress", @keypress
keyup: @checkForBuiltInHandler "keyup", @keyup
- reactivateMode: =>
- @onReactivate()
- Mode.setBadge()
- return Mode.suppressPropagation
Mode.modes.unshift @
Mode.setBadge()
@@ -37,12 +33,12 @@ class Mode
when "pass" then @generatePassThrough type
else handler
- # Generate a default handler which always passes through; except Esc, which pops the current mode.
+ # Generate a default handler which always passes through to the underlying page; except Esc, which pops the
+ # current mode.
generatePassThrough: (type) ->
- me = @
- (event) ->
+ (event) =>
if type == "keydown" and KeyboardUtils.isEscape event
- me.popMode event
+ @exit()
return Mode.suppressPropagation
handlerStack.passThrough
@@ -51,19 +47,14 @@ class Mode
handler = @generatePassThrough type
(event) -> handler(event) and Mode.suppressPropagation # Always falsy.
- # Leave the current mode; event may or may not be provide. It is the responsibility of the creator of this
- # object to know whether or not an event will be provided. Bubble a "reactivateMode" event to notify the
- # now-active mode that it is once again top dog.
- popMode: (event) ->
- Mode.modes = Mode.modes.filter (mode) => mode != @
+ exit: ->
handlerStack.remove @handlerId
- @onDeactivate event
- handlerStack.bubbleEvent "reactivateMode", event
+ Mode.modes = Mode.modes.filter (mode) => mode != @
+ Mode.setBadge()
# Set the badge on the browser popup to indicate the current mode; static method.
@setBadge: ->
- badge = Mode.getBadge()
- chrome.runtime.sendMessage({ handler: "setBadge", badge: badge })
+ chrome.runtime.sendMessage({ handler: "setBadge", badge: Mode.getBadge() })
# Static convenience methods.
@is: (mode) -> Mode.current()?.name == mode
diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee
new file mode 100644
index 00000000..4a1d4349
--- /dev/null
+++ b/content_scripts/mode_insert.coffee
@@ -0,0 +1,70 @@
+
+class InsertMode extends Mode
+ userActivated: false
+
+ # Input or text elements are considered focusable and able to receieve their own keyboard events, and will
+ # enter insert mode if focused. Also note that the "contentEditable" attribute can be set on any element
+ # which makes it a rich text editor, like the notes on jjot.com.
+ isEditable: (element) ->
+ return true if element.isContentEditable
+ nodeName = element.nodeName?.toLowerCase()
+ # Use a blacklist instead of a whitelist because new form controls are still being implemented for html5.
+ if nodeName == "input" and element.type and not element.type in ["radio", "checkbox"]
+ return true
+ nodeName in ["textarea", "select"]
+
+ # Embedded elements like Flash and quicktime players can obtain focus but cannot be programmatically
+ # unfocused.
+ isEmbed: (element) ->
+ element.nodeName?.toLowerCase() in ["embed", "object"]
+
+ canEditElement: (element) ->
+ element and (@isEditable(element) or @isEmbed element)
+
+ isActive: ->
+ @userActivated or @canEditElement document.activeElement
+
+ generateKeyHandler: (type) ->
+ (event) =>
+ return Mode.propagate unless @isActive()
+ return handlerStack.passThrough unless type == "keydown" and KeyboardUtils.isEscape event
+ # We're now exiting insert mode.
+ if @canEditElement event.srcElement
+ # Remove the focus so the user can't just get himself back into insert mode by typing in the same input
+ # box.
+ # NOTE(smblott, 2014/12/22) Including embeds for .blur() here is experimental. It appears to be the
+ # right thing to do for most common use cases. However, it could also cripple flash-based sites and
+ # games. See discussion in #1211 and #1194.
+ event.srcElement.blur()
+ @userActivated = false
+ @updateBadge()
+ Mode.suppressPropagation
+
+ pickBadge: ->
+ if @isActive() then "I" else ""
+
+ updateBadge: ->
+ badge = @badge
+ @badge = @pickBadge()
+ Mode.setBadge() if badge != @badge
+ Mode.propagate
+
+ activate: ->
+ @userActivated = true
+ @updateBadge()
+
+ constructor: ->
+ super
+ name: "insert"
+ badge: @pickBadge()
+ keydown: @generateKeyHandler "keydown"
+ keypress: @generateKeyHandler "keypress"
+ keyup: @generateKeyHandler "keyup"
+
+ handlerStack.push
+ DOMActivate: => @updateBadge()
+ focus: => @updateBadge()
+ blur: => @updateBadge()
+
+root = exports ? window
+root.InsertMode = InsertMode
diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee
index f7ae3a76..6480d511 100644
--- a/content_scripts/vimium_frontend.coffee
+++ b/content_scripts/vimium_frontend.coffee
@@ -5,6 +5,7 @@
# "domReady".
#
+insertMode = null
insertModeLock = null
findMode = false
findModeQuery = { rawQuery: "", matchCount: 0 }
@@ -133,6 +134,9 @@ initializePreDomReady = ->
keypress: handlePassKeyEvent
keyup: -> true # Allow event to propagate.
+ # Install insert mode.
+ insertMode = new InsertMode()
+
checkIfEnabledForUrl()
refreshCompletionKeys()
@@ -192,9 +196,11 @@ initializeWhenEnabled = (newPassKeys) ->
# can't set handlers to grab the keys before us.
for type in ["keydown", "keypress", "keyup"]
do (type) -> installListener window, type, (event) -> handlerStack.bubbleEvent type, event
- installListener document, "focus", onFocusCapturePhase
+ # installListener document, "focus", onFocusCapturePhase # No longer needed.
installListener document, "blur", onBlurCapturePhase
installListener document, "DOMActivate", onDOMActivate
+ installListener document, "focus", onFocus
+ installListener document, "blur", onBlur
enterInsertModeIfElementIsFocused()
installedListeners = true
@@ -244,6 +250,8 @@ enterInsertModeIfElementIsFocused = ->
enterInsertModeWithoutShowingIndicator(document.activeElement)
onDOMActivate = (event) -> handlerStack.bubbleEvent 'DOMActivate', event
+onFocus = (event) -> handlerStack.bubbleEvent 'focus', event
+onBlur = (event) -> handlerStack.bubbleEvent 'blur', event
executePageCommand = (request) ->
return unless frameId == request.frameId
@@ -325,6 +333,9 @@ extend window,
HUD.showForDuration("Yanked URL", 1000)
+ enterInsertMode: ->
+ insertMode?.activate()
+
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.
@@ -602,14 +613,6 @@ isEditable = (target) ->
focusableElements.indexOf(nodeName) >= 0
#
-# Enters insert mode and show an "Insert mode" message. Showing the UI is only useful when entering insert
-# mode manually by pressing "i". In most cases we do not show any UI (enterInsertModeWithoutShowingIndicator)
-#
-window.enterInsertMode = (target) ->
- enterInsertModeWithoutShowingIndicator(target)
- # HUD.show("Insert mode") # With this proof-of-concept, visual feedback is given via badges on the browser popup.
-
-#
# We cannot count on 'focus' and 'blur' events to happen sequentially. For example, if blurring element A
# causes element B to come into focus, we may get "B focus" before "A blur". Thus we only leave insert mode
# when the last editable element that came into focus -- which insertModeLock points to -- has been blurred.
@@ -618,40 +621,13 @@ window.enterInsertMode = (target) ->
# Note. This returns the truthiness of target, which is required by isInsertMode.
#
enterInsertModeWithoutShowingIndicator = (target) ->
- unless Mode.isInsert()
- insertModeLock = target
- # Install insert-mode handler. Hereafter, all key events will be passed directly to the underlying page.
- # The current isInsertMode logic in the normal-mode handlers is now redundant..
- new Mode
- name: "insert"
- badge: "I"
- keydown: "pass"
- keypress: "pass"
- keyup: "pass"
- onDeactivate: (event) ->
- if isEditable(event.srcElement) or isEmbed(event.srcElement)
- # Remove focus so the user can't just get himself back into insert mode by typing in the same input
- # box.
- # NOTE(smblott, 2014/12/22) Including embeds for .blur() etc. here is experimental. It appears to be
- # the right thing to do for most common use cases. However, it could also cripple flash-based sites and
- # games. See discussion in #1211 and #1194.
- event.srcElement.blur()
- insertModeLock = null
- HUD.hide()
+ return # Disabled.
exitInsertMode = (target) ->
- # This assumes that, if insert mode is active at all, then it *must* be the current mode. That is, we
- # cannot enter any other mode from insert mode.
- if Mode.isInsert() and (target == null or target == insertModeLock)
- Mode.popMode()
+ return # Disabled.
isInsertMode = ->
- return true if Mode.isInsert()
- # 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
+ return false # Disabled.
# should be called whenever rawQuery is modified.
updateFindModeQuery = ->
@@ -705,6 +681,7 @@ updateFindModeQuery = ->
findModeQuery.matchCount = text.match(pattern)?.length
handleKeyCharForFindMode = (keyChar) ->
+ console.log "xxxxxxxxxxxxxxx"
findModeQuery.rawQuery += keyChar
updateFindModeQuery()
performFindInPlace()
diff --git a/lib/handler_stack.coffee b/lib/handler_stack.coffee
index 886a9ece..6f599dc7 100644
--- a/lib/handler_stack.coffee
+++ b/lib/handler_stack.coffee
@@ -7,7 +7,7 @@ class HandlerStack
@counter = 0
@passThrough = new Object() # Used only as a constant, distinct from any other value.
- genId: -> @counter = ++@counter & 0xffff
+ genId: -> @counter = ++@counter
# Adds a handler to the stack. Returns a unique ID for that handler that can be used to remove it later.
push: (handler) ->
diff --git a/manifest.json b/manifest.json
index dca01b49..5f810d54 100644
--- a/manifest.json
+++ b/manifest.json
@@ -44,6 +44,7 @@
"content_scripts/scroller.js",
"content_scripts/marks.js",
"content_scripts/mode.js",
+ "content_scripts/mode_insert.js",
"content_scripts/vimium_frontend.js"
],
"css": ["content_scripts/vimium.css"],