Skip to content

Commit

Permalink
Modes; implement insert mode.
Browse files Browse the repository at this point in the history
  • Loading branch information
smblott-github committed Jan 1, 2015
1 parent aed5e2b commit 2d047e7
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 59 deletions.
29 changes: 10 additions & 19 deletions content_scripts/mode.coffee
Expand Up @@ -3,6 +3,8 @@ class Mode
# Static members.
@modes: []
@current: -> Mode.modes[0]

# Constants. Static.
@suppressPropagation = false
@propagate = true

Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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

Expand All @@ -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
Expand Down
70 changes: 70 additions & 0 deletions 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
55 changes: 16 additions & 39 deletions content_scripts/vimium_frontend.coffee
Expand Up @@ -5,6 +5,7 @@
# "domReady".
#

insertMode = null
insertModeLock = null
findMode = false
findModeQuery = { rawQuery: "", matchCount: 0 }
Expand Down Expand Up @@ -133,6 +134,9 @@ initializePreDomReady = ->
keypress: handlePassKeyEvent
keyup: -> true # Allow event to propagate.

# Install insert mode.
insertMode = new InsertMode()

checkIfEnabledForUrl()

refreshCompletionKeys()
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -601,14 +612,6 @@ isEditable = (target) ->
focusableElements = ["textarea", "select"]
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
Expand All @@ -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 = ->
Expand Down Expand Up @@ -705,6 +681,7 @@ updateFindModeQuery = ->
findModeQuery.matchCount = text.match(pattern)?.length

handleKeyCharForFindMode = (keyChar) ->
console.log "xxxxxxxxxxxxxxx"
findModeQuery.rawQuery += keyChar
updateFindModeQuery()
performFindInPlace()
Expand Down
2 changes: 1 addition & 1 deletion lib/handler_stack.coffee
Expand Up @@ -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) ->
Expand Down
1 change: 1 addition & 0 deletions manifest.json
Expand Up @@ -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"],
Expand Down

0 comments on commit 2d047e7

Please sign in to comment.