aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorStephen Blott2015-01-18 10:39:09 +0000
committerStephen Blott2015-01-18 10:39:09 +0000
commita1edae57e2847c2b6ffcae60ea8c9c16216e4692 (patch)
tree30ff186038028f9d0c0d5cc08d572ca56dda8819 /lib
parent8c9e429074580ea20aba662ee430d87bd73ebc4b (diff)
parent5d087c89917e21872711b7b908fcdd3c7e9e7f17 (diff)
downloadvimium-a1edae57e2847c2b6ffcae60ea8c9c16216e4692.tar.bz2
Merge pull request #1413 from smblott-github/modes
A modal-browsing framework
Diffstat (limited to 'lib')
-rw-r--r--lib/dom_utils.coffee42
-rw-r--r--lib/handler_stack.coffee92
-rw-r--r--lib/keyboard_utils.coffee6
3 files changed, 125 insertions, 15 deletions
diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee
index 7a75dd6a..aee2f972 100644
--- a/lib/dom_utils.coffee
+++ b/lib/dom_utils.coffee
@@ -142,6 +142,39 @@ DomUtils =
(element.nodeName.toLowerCase() == "input" && unselectableTypes.indexOf(element.type) == -1) ||
element.nodeName.toLowerCase() == "textarea"
+ # 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 not in ["radio", "checkbox"]
+ return true
+ nodeName in ["textarea", "select"]
+
+ # Embedded elements like Flash and quicktime players can obtain focus.
+ isEmbed: (element) ->
+ element.nodeName?.toLowerCase() in ["embed", "object"]
+
+ isFocusable: (element) ->
+ @isEditable(element) or @isEmbed element
+
+ isDOMDescendant: (parent, child) ->
+ node = child
+ while (node != null)
+ return true if (node == parent)
+ node = node.parentNode
+ false
+
+ # True if element contains the active selection range.
+ isSelected: (element) ->
+ if element.isContentEditable
+ node = document.getSelection()?.anchorNode
+ node and @isDOMDescendant element, node
+ else
+ element.selectionStart? and element.selectionEnd? and element.selectionStart != element.selectionEnd
+
simulateSelect: (element) ->
element.focus()
# When focusing a textbox, put the selection caret at the end of the textbox's contents.
@@ -179,5 +212,14 @@ DomUtils =
event.preventDefault()
@suppressPropagation(event)
+ # Suppress the next keyup event for Escape.
+ suppressKeyupAfterEscape: (handlerStack) ->
+ handlerStack.push
+ _name: "dom_utils/suppressKeyupAfterEscape"
+ keyup: (event) ->
+ return true unless KeyboardUtils.isEscape event
+ @remove()
+ false
+
root = exports ? window
root.DomUtils = DomUtils
diff --git a/lib/handler_stack.coffee b/lib/handler_stack.coffee
index 858f2ec9..76d835b7 100644
--- a/lib/handler_stack.coffee
+++ b/lib/handler_stack.coffee
@@ -1,37 +1,99 @@
root = exports ? window
-class root.HandlerStack
+class HandlerStack
constructor: ->
+ @debug = false
+ @eventNumber = 0
@stack = []
@counter = 0
- genId: -> @counter = ++@counter & 0xffff
+ # A handler should return this value to immediately discontinue bubbling and pass the event on to the
+ # underlying page.
+ @stopBubblingAndTrue = new Object()
- # Adds a handler to the stack. Returns a unique ID for that handler that can be used to remove it later.
+ # A handler should return this value to indicate that the event has been consumed, and no further
+ # processing should take place. The event does not propagate to the underlying page.
+ @stopBubblingAndFalse = new Object()
+
+ # A handler should return this value to indicate that bubbling should be restarted. Typically, this is
+ # used when, while bubbling an event, a new mode is pushed onto the stack.
+ @restartBubbling = new Object()
+
+ # Adds a handler to the top of the stack. Returns a unique ID for that handler that can be used to remove it
+ # later.
push: (handler) ->
- handler.id = @genId()
+ handler._name ||= "anon-#{@counter}"
@stack.push handler
- handler.id
+ handler.id = ++@counter
+
+ # As above, except the new handler is added to the bottom of the stack.
+ unshift: (handler) ->
+ handler._name ||= "anon-#{@counter}"
+ handler._name += "/unshift"
+ @stack.unshift handler
+ handler.id = ++@counter
- # Called whenever we receive a key event. Each individual handler has the option to stop the event's
- # propagation by returning a falsy value.
+ # Called whenever we receive a key or other event. Each individual handler has the option to stop the
+ # event's propagation by returning a falsy value, or stop bubbling by returning @stopBubblingAndFalse or
+ # @stopBubblingAndTrue.
bubbleEvent: (type, event) ->
- for i in [(@stack.length - 1)..0] by -1
- handler = @stack[i]
- # We need to check for existence of handler because the last function call may have caused the release
- # of more than one handler.
- if handler && handler[type]
+ @eventNumber += 1
+ # We take a copy of the array in order to avoid interference from concurrent removes (for example, to
+ # avoid calling the same handler twice, because elements have been spliced out of the array by remove).
+ for handler in @stack[..].reverse()
+ # A handler may have been removed (handler.id == null), so check.
+ if handler?.id and handler[type]
@currentId = handler.id
- passThrough = handler[type].call(@, event)
- if not passThrough
- DomUtils.suppressEvent(event)
+ result = handler[type].call @, event
+ @logResult type, event, handler, result if @debug
+ if not result
+ DomUtils.suppressEvent event if @isChromeEvent event
return false
+ return true if result == @stopBubblingAndTrue
+ return false if result == @stopBubblingAndFalse
+ return @bubbleEvent type, event if result == @restartBubbling
true
remove: (id = @currentId) ->
for i in [(@stack.length - 1)..0] by -1
handler = @stack[i]
if handler.id == id
+ # Mark the handler as removed.
+ handler.id = null
@stack.splice(i, 1)
break
+
+ # The handler stack handles chrome events (which may need to be suppressed) and internal (pseudo) events.
+ # This checks whether the event at hand is a chrome event.
+ isChromeEvent: (event) ->
+ event?.preventDefault? or event?.stopImmediatePropagation?
+
+ # Convenience wrappers. Handlers must return an approriate value. These are wrappers which handlers can
+ # use to always return the same value. This then means that the handler itself can be implemented without
+ # regard to its return value.
+ alwaysContinueBubbling: (handler) ->
+ handler()
+ true
+
+ neverContinueBubbling: (handler) ->
+ handler()
+ false
+
+ # Debugging.
+ logResult: (type, event, handler, result) ->
+ # FIXME(smblott). Badge updating is too noisy, so we filter it out. However, we do need to look at how
+ # many badge update events are happening. It seems to be more than necessary. We also filter out
+ # registerKeyQueue as unnecessarily noisy and not particularly helpful.
+ return if type in [ "updateBadge", "registerKeyQueue" ]
+ label =
+ switch result
+ when @stopBubblingAndTrue then "stop/true"
+ when @stopBubblingAndFalse then "stop/false"
+ when @restartBubbling then "rebubble"
+ when true then "continue"
+ label ||= if result then "continue/truthy" else "suppress"
+ console.log "#{@eventNumber}", type, handler._name, label
+
+root.HandlerStack = HandlerStack
+root.handlerStack = new HandlerStack()
diff --git a/lib/keyboard_utils.coffee b/lib/keyboard_utils.coffee
index d2a843f9..30d99656 100644
--- a/lib/keyboard_utils.coffee
+++ b/lib/keyboard_utils.coffee
@@ -55,6 +55,12 @@ KeyboardUtils =
# c-[ is mapped to ESC in Vim by default.
(event.keyCode == @keyCodes.ESC) || (event.ctrlKey && @getKeyChar(event) == '[')
+ # TODO. This is probably a poor way of detecting printable characters. However, it shouldn't incorrectly
+ # identify any of chrome's own keyboard shortcuts as printable.
+ isPrintable: (event) ->
+ return false if event.metaKey or event.ctrlKey or event.altKey
+ @getKeyChar(event)?.length == 1
+
KeyboardUtils.init()
root = exports ? window