2015-05-20 07:06:59 +08:00
|
|
|
_ = require 'underscore'
|
2015-03-13 06:09:10 +08:00
|
|
|
React = require 'react/addons'
|
2015-07-22 02:34:47 +08:00
|
|
|
{DOMUtils} = require 'nylas-exports'
|
2015-03-13 06:09:10 +08:00
|
|
|
|
|
|
|
###
|
|
|
|
The Tooltip component displays a consistent hovering tooltip for use when
|
|
|
|
extra context information is required.
|
|
|
|
|
2015-05-01 02:35:38 +08:00
|
|
|
Activate by adding a `data-tooltip="Label"` to any element
|
|
|
|
|
2015-03-13 06:09:10 +08:00
|
|
|
It's a global-level singleton
|
|
|
|
###
|
|
|
|
|
2015-05-01 04:08:29 +08:00
|
|
|
class Tooltip extends React.Component
|
|
|
|
@displayName: "Tooltip"
|
|
|
|
|
|
|
|
constructor: (@props) ->
|
|
|
|
@state =
|
|
|
|
top: 0
|
|
|
|
pos: "below"
|
|
|
|
left: 0
|
|
|
|
width: 0
|
|
|
|
pointerLeft: 0
|
|
|
|
display: false
|
|
|
|
content: ""
|
|
|
|
|
|
|
|
componentWillMount: =>
|
2015-03-13 06:09:10 +08:00
|
|
|
@CONTENT_PADDING = 15
|
2015-07-16 02:56:28 +08:00
|
|
|
@DEFAULT_DELAY = 2000
|
|
|
|
@KEEP_DELAY = 300
|
2015-03-13 06:09:10 +08:00
|
|
|
@_showDelay = @DEFAULT_DELAY
|
|
|
|
@_showTimeout = null
|
|
|
|
@_showDelayTimeout = null
|
|
|
|
|
2015-05-01 04:08:29 +08:00
|
|
|
componentWillUnmount: =>
|
2015-03-13 06:09:10 +08:00
|
|
|
clearTimeout @_showTimeout
|
|
|
|
clearTimeout @_showDelayTimeout
|
2015-07-24 02:47:46 +08:00
|
|
|
@_mutationObserver?.disconnect()
|
2015-03-13 06:09:10 +08:00
|
|
|
|
2015-05-01 04:08:29 +08:00
|
|
|
render: =>
|
2015-03-18 07:19:40 +08:00
|
|
|
<div className="tooltip-wrap #{@state.pos}" style={@_positionStyles()}>
|
2015-03-13 06:09:10 +08:00
|
|
|
<div className="tooltip-content">{@state.content}</div>
|
|
|
|
<div className="tooltip-pointer" style={left: @state.pointerLeft}></div>
|
|
|
|
</div>
|
|
|
|
|
2015-05-01 04:08:29 +08:00
|
|
|
_positionStyles: =>
|
2015-03-13 06:09:10 +08:00
|
|
|
top: @state.top
|
|
|
|
left: @state.left
|
|
|
|
width: @state.width
|
|
|
|
display: if @state.display then "block" else "none"
|
|
|
|
|
|
|
|
# This are public methods so they can be bound to the window event
|
|
|
|
# listeners.
|
2015-05-01 04:08:29 +08:00
|
|
|
onMouseOver: (e) =>
|
2015-07-22 05:16:11 +08:00
|
|
|
elWithTooltip = @_elementWithTooltip(e.target)
|
|
|
|
if elWithTooltip and DOMUtils.nodeIsVisible(elWithTooltip)
|
|
|
|
if elWithTooltip isnt @_lastTarget
|
|
|
|
@_onTooltipEnter(elWithTooltip)
|
|
|
|
else
|
|
|
|
@_hideTooltip() if @state.display
|
2015-03-13 06:09:10 +08:00
|
|
|
|
2015-05-01 04:08:29 +08:00
|
|
|
onMouseOut: (e) =>
|
2015-03-13 06:09:10 +08:00
|
|
|
if @_elementWithTooltip(e.fromElement) and not @_elementWithTooltip(e.toElement)
|
|
|
|
@_onTooltipLeave()
|
|
|
|
|
2015-05-01 04:08:29 +08:00
|
|
|
onMouseDown: (e) =>
|
2015-04-30 08:38:16 +08:00
|
|
|
if @state.display then @_hideTooltip()
|
|
|
|
|
2015-05-01 04:08:29 +08:00
|
|
|
_elementWithTooltip: (target) =>
|
2015-03-13 06:09:10 +08:00
|
|
|
while target
|
|
|
|
break if target?.dataset?.tooltip?
|
|
|
|
target = target.parentNode
|
|
|
|
return target
|
|
|
|
|
2015-05-01 04:08:29 +08:00
|
|
|
_onTooltipEnter: (target) =>
|
2015-07-24 02:47:46 +08:00
|
|
|
# https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
|
|
|
|
@_mutationObserver?.disconnect()
|
|
|
|
@_mutationObserver = new MutationObserver @_onTooltipLeave
|
|
|
|
@_mutationObserver.observe(target.parentNode, attributes: true, subtree: true, childList: true)
|
|
|
|
|
2015-07-22 05:16:11 +08:00
|
|
|
@_lastTarget = target
|
2015-03-13 06:09:10 +08:00
|
|
|
@_enteredTooltip = true
|
|
|
|
clearTimeout(@_showTimeout)
|
|
|
|
clearTimeout(@_showDelayTimeout)
|
|
|
|
@_showTimeout = setTimeout =>
|
|
|
|
@_showTooltip(target)
|
|
|
|
, @_showDelay
|
|
|
|
|
2015-05-01 04:08:29 +08:00
|
|
|
_onTooltipLeave: =>
|
2015-03-13 06:09:10 +08:00
|
|
|
return unless @_enteredTooltip
|
2015-07-24 02:47:46 +08:00
|
|
|
@_mutationObserver?.disconnect()
|
2015-03-13 06:09:10 +08:00
|
|
|
@_enteredTooltip = false
|
|
|
|
clearTimeout(@_showTimeout)
|
|
|
|
@_hideTooltip()
|
|
|
|
|
|
|
|
@_showDelay = 10
|
|
|
|
clearTimeout(@_showDelayTimeout)
|
|
|
|
@_showDelayTimeout = setTimeout =>
|
|
|
|
@_showDelay = @DEFAULT_DELAY
|
|
|
|
, @KEEP_DELAY
|
|
|
|
|
2015-05-01 04:08:29 +08:00
|
|
|
_showTooltip: (target) =>
|
2015-07-22 02:34:47 +08:00
|
|
|
return unless DOMUtils.nodeIsVisible(target)
|
2015-03-13 06:09:10 +08:00
|
|
|
content = target.dataset.tooltip
|
2015-07-22 05:16:11 +08:00
|
|
|
return if (content ? "").trim().toLowerCase().length is 0
|
|
|
|
|
2015-03-13 06:09:10 +08:00
|
|
|
guessedWidth = @_guessWidth(content)
|
|
|
|
dim = target.getBoundingClientRect()
|
|
|
|
left = dim.left + dim.width / 2
|
2015-03-18 07:19:40 +08:00
|
|
|
|
|
|
|
TOOLTIP_HEIGHT = 50
|
|
|
|
FLIP_THRESHOLD = TOOLTIP_HEIGHT + 30
|
|
|
|
top = dim.top + dim.height + 14
|
|
|
|
tooltipPos = "below"
|
|
|
|
if top + FLIP_THRESHOLD > @_windowHeight()
|
|
|
|
tooltipPos = "above"
|
|
|
|
top = dim.top - TOOLTIP_HEIGHT
|
|
|
|
|
2015-07-16 02:56:28 +08:00
|
|
|
# If for some reason the element was removed from underneath us, we
|
|
|
|
# won't know until we get here. The element's dimensions will return 0
|
|
|
|
# ,0, which we can use to filter out bad displays
|
|
|
|
if left < 5 and top < 5
|
|
|
|
@_hideTooltip()
|
|
|
|
return
|
|
|
|
|
2015-03-13 06:09:10 +08:00
|
|
|
@setState
|
|
|
|
top: top
|
2015-03-18 07:19:40 +08:00
|
|
|
pos: tooltipPos
|
2015-03-13 06:09:10 +08:00
|
|
|
left: @_tooltipLeft(left, guessedWidth)
|
|
|
|
width: guessedWidth
|
|
|
|
pointerLeft: @_tooltipPointerLeft(left, guessedWidth)
|
|
|
|
display: true
|
|
|
|
content: target.dataset.tooltip
|
|
|
|
|
2015-05-01 04:08:29 +08:00
|
|
|
_guessWidth: (content) =>
|
2015-03-13 06:09:10 +08:00
|
|
|
# roughly 11px per character
|
|
|
|
guessWidth = content.length * 11
|
|
|
|
return Math.max(Math.min(guessWidth, 250), 50)
|
|
|
|
|
2015-05-01 04:08:29 +08:00
|
|
|
_tooltipLeft: (targetLeft, guessedWidth) =>
|
2015-03-13 06:09:10 +08:00
|
|
|
max = @_windowWidth() - guessedWidth - @CONTENT_PADDING
|
|
|
|
left = Math.min(Math.max(targetLeft - guessedWidth/2, @CONTENT_PADDING), max)
|
|
|
|
return left
|
|
|
|
|
2015-05-01 04:08:29 +08:00
|
|
|
_tooltipPointerLeft: (targetLeft, guessedWidth) =>
|
2015-03-13 06:09:10 +08:00
|
|
|
POINTER_WIDTH = 6 + 2 #2px of border-radius
|
|
|
|
max = @_windowWidth() - @CONTENT_PADDING
|
|
|
|
min = @CONTENT_PADDING
|
|
|
|
absoluteLeft = Math.max(Math.min(targetLeft, max), min)
|
|
|
|
relativeLeft = absoluteLeft - @_tooltipLeft(targetLeft, guessedWidth)
|
|
|
|
|
|
|
|
left = Math.max(Math.min(relativeLeft, guessedWidth-POINTER_WIDTH), POINTER_WIDTH)
|
|
|
|
return left
|
|
|
|
|
2015-05-01 04:08:29 +08:00
|
|
|
_windowWidth: =>
|
2015-07-24 10:42:16 +08:00
|
|
|
document.body.offsetWidth
|
2015-03-13 06:09:10 +08:00
|
|
|
|
2015-05-01 04:08:29 +08:00
|
|
|
_windowHeight: =>
|
2015-07-24 10:42:16 +08:00
|
|
|
document.body.offsetHeight
|
2015-03-18 07:19:40 +08:00
|
|
|
|
2015-05-01 04:08:29 +08:00
|
|
|
_hideTooltip: =>
|
2015-07-22 05:16:11 +08:00
|
|
|
@_lastTarget = null
|
2015-03-13 06:09:10 +08:00
|
|
|
@setState
|
|
|
|
top: 0
|
|
|
|
left: 0
|
|
|
|
width: 0
|
|
|
|
pointerLeft: 0
|
|
|
|
display: false
|
|
|
|
content: ""
|
2015-05-01 04:08:29 +08:00
|
|
|
|
|
|
|
|
|
|
|
module.exports = Tooltip
|