aboutsummaryrefslogtreecommitdiffstats
path: root/lib/handler_stack.coffee
blob: a43fc35652b3f0b92d727f302b8cb53ad60ed098 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
root = exports ? (window.root ?= {})

class HandlerStack
  constructor: ->
    @debug = false
    @eventNumber = 0
    @stack = []
    @counter = 0

    # A handler should return this value to immediately discontinue bubbling and pass the event on to the
    # underlying page.
    @passEventToPage = new Object()

    # 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.
    @suppressPropagation = 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()

    # A handler should return this value to continue bubbling the event.
    @continueBubbling = true

    # A handler should return this value to suppress an event.
    @suppressEvent = false

  # 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._name ||= "anon-#{@counter}"
    @stack.push handler
    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 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 @suppressPropagation or
  # @passEventToPage.
  bubbleEvent: (type, event) ->
    @eventNumber += 1
    eventNumber = @eventNumber
    for handler in @stack[..].reverse()
      # A handler might have been removed (handler.id == null), so check; or there might just be no handler
      # for this type of event.
      unless handler?.id and handler[type]
        @logResult eventNumber, type, event, handler, "skip [#{handler[type]?}]" if @debug
      else
        @currentId = handler.id
        result = handler[type].call this, event
        @logResult eventNumber, type, event, handler, result if @debug
        if result == @passEventToPage
          return true
        else if result == @suppressPropagation
          if type == "keydown"
            DomUtils.consumeKeyup event, null, true
          else
            DomUtils.suppressPropagation event
          return false
        else if result == @restartBubbling
          return @bubbleEvent type, event
        else if result == @continueBubbling or (result and result != @suppressEvent)
          true # Do nothing, but continue bubbling.
        else
          # result is @suppressEvent or falsy.
          if @isChromeEvent event
            if type == "keydown"
              DomUtils.consumeKeyup event
            else
              DomUtils.suppressEvent event
          return false

    # None of our handlers care about this event, so pass it to the page.
    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 = null) ->
    handler?()
    @continueBubbling

  alwaysSuppressPropagation: (handler = null) ->
    if handler?() == @suppressEvent then @suppressEvent else @suppressPropagation

  # Debugging.
  logResult: (eventNumber, type, event, handler, result) ->
    if event?.type == "keydown" # Tweak this as needed.
      label =
        switch result
          when @passEventToPage then "passEventToPage"
          when @suppressEvent then "suppressEvent"
          when @suppressPropagation then "suppressPropagation"
          when @restartBubbling then "restartBubbling"
          when "skip" then "skip"
          when true then "continue"
      label ||= if result then "continue/truthy" else "suppress"
      console.log "#{eventNumber}", type, handler._name, label

  show: ->
    console.log "#{@eventNumber}:"
    for handler in @stack[..].reverse()
      console.log "  ", handler._name

  # For tests only.
  reset: ->
    @stack = []

root.HandlerStack = HandlerStack
root.handlerStack = new HandlerStack()
extend window, root unless exports?