diff options
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 |
