aboutsummaryrefslogtreecommitdiffstats
path: root/content_scripts/mode_insert.coffee
blob: 144b0be667ebb8a3c7bd613a45a814c3adec99e1 (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
# This mode is installed only when insert mode is active.
class InsertMode extends Mode
  constructor: (options = {}) ->
    defaults =
      name: "insert"
      badge: "I"
      singleton: InsertMode
      keydown: (event) => @stopBubblingAndTrue
      keypress: (event) => @stopBubblingAndTrue
      keyup: (event) => @stopBubblingAndTrue
      exitOnEscape: true
      blurOnExit: true
      targetElement: null

    options.exitOnBlur ||= options.targetElement
    super extend defaults, options
    triggerSuppressor.suppress()

  exit: (event = null) ->
    triggerSuppressor.unsuppress()
    super()
    if @options.blurOnExit
      element = event?.srcElement
      if element and DomUtils.isFocusable element
        # 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.
        element.blur()

# Automatically trigger insert mode:
#   - On a keydown event in a contentEditable element.
#   - When a focusable element receives the focus.
#
# The trigger can be suppressed via triggerSuppressor; see InsertModeBlocker, below.  This mode is permanently
# installed (just above normal mode and passkeys mode) on the handler stack.
class InsertModeTrigger extends Mode
  constructor: ->
    super
      name: "insert-trigger"
      keydown: (event) =>
        triggerSuppressor.unlessSuppressed =>
          # Some sites (e.g. inbox.google.com) change the contentEditable attribute on the fly (see #1245);
          # and unfortunately, the focus event happens *before* the change is made.  Therefore, we need to
          # check (on every keydown) whether the active element is contentEditable.
          return @continueBubbling unless document.activeElement?.isContentEditable
          new InsertMode
            targetElement: document.activeElement
          @stopBubblingAndTrue

    @push
      focus: (event) =>
        triggerSuppressor.unlessSuppressed =>
          @alwaysContinueBubbling =>
            if DomUtils.isFocusable event.target
              new InsertMode
                targetElement: event.target

    # We may have already focussed an input element, so check.
    if document.activeElement and DomUtils.isEditable document.activeElement
      new InsertMode
        targetElement: document.activeElement

# Used by InsertModeBlocker to suppress InsertModeTrigger; see below.
triggerSuppressor = new Utils.Suppressor true # Note: true == @continueBubbling

# Suppresses InsertModeTrigger.  This is used by various modes (usually via inheritance) to prevent
# unintentionally dropping into insert mode on focusable elements.
class InsertModeBlocker extends Mode
  constructor: (options = {}) ->
    triggerSuppressor.suppress()
    options.name ||= "insert-blocker"
    # See "click" handler below for an explanation of options.onClickMode.
    options.onClickMode ||= InsertMode
    super options
    @onExit -> triggerSuppressor.unsuppress()

    @push
      "click": (event) =>
        @alwaysContinueBubbling =>
          # The user knows best; so, if the user clicks on something, the insert-mode blocker gets out of the
          # way.
          @exit event
          # However, there's a corner case.  If the active element is focusable, then, had we not been
          # blocking the trigger, we would already have been in insert mode.  Now, a click on that element
          # will not generate a new focus event, so the insert-mode trigger will not fire.  We have to handle
          # this case specially.  @options.onClickMode specifies the mode to use (by default, insert mode).
          if document.activeElement and
             event.target == document.activeElement and DomUtils.isEditable document.activeElement
            new @options.onClickMode
              targetElement: document.activeElement

# There's some unfortunate feature interaction with chrome's content editable handling.  If the selection is
# content editable and a descendant of the active element, then chrome focuses it on any unsuppressed keyboard
# event.  This has the unfortunate effect of dropping us unintentally into insert mode.  See #1415.
# A single instance of this mode sits near the bottom of the handler stack and suppresses keyboard events if:
#   - they haven't been handled by any other mode (so not by normal mode, passkeys mode, insert mode, and so
#     on),
#   - the selection is content editable, and
#   - the selection is a descendant of the active element.
# This should rarely fire, typically only on fudged keypresses in normal mode.  And, even then, only in the
# circumstances outlined above.  So, we shouldn't usually be blocking keyboard events for other extensions or
# the page itself.
# handling keyboard events.
new class ContentEditableTrap extends Mode
  constructor: ->
    super
      name: "content-editable-trap"
      keydown: (event) => @handle => DomUtils.suppressPropagation event
      keypress: (event) => @handle => @suppressEvent
      keyup: (event) => @handle => @suppressEvent

  handle: (func) -> if @isContentEditableFocused() then func() else @continueBubbling

  # True if the selection is content editable and a descendant of the active element.
  isContentEditableFocused: ->
    element = document.getSelection()?.anchorNode?.parentElement
    return element?.isContentEditable and
             document.activeElement and
             DomUtils.isDOMDescendant document.activeElement, element

root = exports ? window
root.InsertMode = InsertMode
root.InsertModeTrigger = InsertModeTrigger
root.InsertModeBlocker = InsertModeBlocker