diff options
| author | Stephen Blott | 2015-01-18 10:39:09 +0000 | 
|---|---|---|
| committer | Stephen Blott | 2015-01-18 10:39:09 +0000 | 
| commit | a1edae57e2847c2b6ffcae60ea8c9c16216e4692 (patch) | |
| tree | 30ff186038028f9d0c0d5cc08d572ca56dda8819 /lib | |
| parent | 8c9e429074580ea20aba662ee430d87bd73ebc4b (diff) | |
| parent | 5d087c89917e21872711b7b908fcdd3c7e9e7f17 (diff) | |
| download | vimium-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.coffee | 42 | ||||
| -rw-r--r-- | lib/handler_stack.coffee | 92 | ||||
| -rw-r--r-- | lib/keyboard_utils.coffee | 6 | 
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 | 
