aboutsummaryrefslogtreecommitdiffstats
path: root/content_scripts/hud.coffee
blob: d4c38447333620639f5161f90f37ba95ae5dd008 (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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#
# A heads-up-display (HUD) for showing Vimium page operations.
# Note: you cannot interact with the HUD until document.body is available.
#
HUD =
  tween: null
  hudUI: null
  _displayElement: null
  findMode: null
  abandon: -> @hudUI?.hide false

  pasteListener: null # Set by @pasteFromClipboard to handle the value returned by pasteResponse

  # This HUD is styled to precisely mimick the chrome HUD on Mac. Use the "has_popup_and_link_hud.html"
  # test harness to tweak these styles to match Chrome's. One limitation of our HUD display is that
  # it doesn't sit on top of horizontal scrollbars like Chrome's HUD does.

  init: ->
    @hudUI ?= new UIComponent "pages/hud.html", "vimiumHUDFrame", ({data}) => this[data.name]? data
    @tween ?= new Tween "iframe.vimiumHUDFrame.vimiumUIComponentVisible", @hudUI.shadowDOM

  showForDuration: (text, duration) ->
    @show(text)
    @_showForDurationTimerId = setTimeout((=> @hide()), duration)

  show: (text) ->
    DomUtils.documentComplete =>
      @init()
      clearTimeout(@_showForDurationTimerId)
      @hudUI.activate {name: "show", text}
      @tween.fade 1.0, 150

  showFindMode: (@findMode = null) ->
    DomUtils.documentComplete =>
      @init()
      @hudUI.activate name: "showFindMode"
      @tween.fade 1.0, 150

  search: (data) ->
    # NOTE(mrmr1993): On Firefox, window.find moves the window focus away from the HUD. We use postFindFocus
    # to put it back, so the user can continue typing.
    @findMode.findInPlace data.query, {"postFindFocus": @hudUI.iframeElement.contentWindow}

    # Show the number of matches in the HUD UI.
    matchCount = if FindMode.query.parsedQuery.length > 0 then FindMode.query.matchCount else 0
    showMatchText = FindMode.query.rawQuery.length > 0
    @hudUI.postMessage {name: "updateMatchesCount", matchCount, showMatchText}

  # Hide the HUD.
  # If :immediate is falsy, then the HUD is faded out smoothly (otherwise it is hidden immediately).
  # If :updateIndicator is truthy, then we also refresh the mode indicator.  The only time we don't update the
  # mode indicator, is when hide() is called for the mode indicator itself.
  hide: (immediate = false, updateIndicator = true) ->
    if @hudUI? and @tween?
      clearTimeout @_showForDurationTimerId
      @tween.stop()
      if immediate
        if updateIndicator then Mode.setIndicator() else @hudUI.hide()
      else
        @tween.fade 0, 150, => @hide true, updateIndicator

  # These parameters describe the reason find mode is exiting, and come from the HUD UI component.
  hideFindMode: ({exitEventIsEnter, exitEventIsEscape}) ->
    @findMode.checkReturnToViewPort()

    # An element won't receive a focus event if the search landed on it while we were in the HUD iframe. To
    # end up with the correct modes active, we create a focus/blur event manually after refocusing this
    # window.
    window.focus()

    focusNode = DomUtils.getSelectionFocusElement()
    document.activeElement?.blur()
    focusNode?.focus?()

    if exitEventIsEnter
      FindMode.handleEnter()
      if FindMode.query.hasResults
        postExit = -> new PostFindMode
    else if exitEventIsEscape
      # We don't want FindMode to handle the click events that FindMode.handleEscape can generate, so we
      # wait until the mode is closed before running it.
      postExit = FindMode.handleEscape

    @findMode.exit()
    postExit?()

  # These commands manage copying and pasting from the clipboard in the HUD frame.
  # NOTE(mrmr1993): We need this to copy and paste on Firefox:
  # * an element can't be focused in the background page, so copying/pasting doesn't work
  # * we don't want to disrupt the focus in the page, in case the page is listening for focus/blur events.
  # * the HUD shouldn't be active for this frame while any of the copy/paste commands are running.
  copyToClipboard: (text) ->
    DomUtils.documentComplete =>
      @init()
      @hudUI?.postMessage {name: "copyToClipboard", data: text}

  pasteFromClipboard: (@pasteListener) ->
    DomUtils.documentComplete =>
      @init()
      @hudUI?.postMessage {name: "pasteFromClipboard"}

  pasteResponse: ({data}) ->
    @pasteListener data

class Tween
  opacity: 0
  intervalId: -1
  styleElement: null

  constructor: (@cssSelector, insertionPoint = document.documentElement) ->
    @styleElement = DomUtils.createElement "style"

    unless @styleElement.style
      # We're in an XML document, so we shouldn't inject any elements. See the comment in UIComponent.
      Tween::fade = Tween::stop = Tween::updateStyle = ->
      return

    @styleElement.type = "text/css"
    @styleElement.innerHTML = ""
    insertionPoint.appendChild @styleElement

  fade: (toAlpha, duration, onComplete) ->
    clearInterval @intervalId
    startTime = (new Date()).getTime()
    fromAlpha = @opacity
    alphaStep = toAlpha - fromAlpha

    performStep = =>
      elapsed = (new Date()).getTime() - startTime
      if (elapsed >= duration)
        clearInterval @intervalId
        @updateStyle toAlpha
        onComplete?()
      else
        value = (elapsed / duration) * alphaStep + fromAlpha
        @updateStyle value

    @updateStyle @opacity
    @intervalId = setInterval performStep, 50

  stop: -> clearInterval @intervalId

  updateStyle: (@opacity) ->
    @styleElement.innerHTML = """
      #{@cssSelector} {
        opacity: #{@opacity};
      }
    """

root = exports ? (window.root ?= {})
root.HUD = HUD
extend window, root unless exports?