# Mice: tooltip
# Inspired by the original jQuery.tipsy by Jason Frame
# https://github.com/jaz303/tipsy
# Copyright (c) 2014 Miclle
# Licensed under MIT (https://github.com/miclle/mice/blob/master/LICENSE)
# Generated markup by the plugin
#
'use strict';
(($) ->
# TOOLTIP PUBLIC CLASS DEFINITION
# ===============================
class Tooltip
constructor: (element, options) ->
@type
@options
@enabled
@timeout
@hoverState
@$element = null
@init('tooltip', element, options)
init: (type, element, options) ->
@enabled = true
@type = type
@$element = $(element)
@options = @getOptions(options)
@$viewport = @options.viewport and $(@options.viewport.selector or @options.viewport)
for trigger in @options.trigger.split(' ')
if trigger == 'click'
@$element.on('click.' + @type, @options.selector, $.proxy(@toggle, @))
else if trigger != 'manual'
eventIn = if trigger == 'hover' then 'mouseenter' else 'focusin'
eventOut = if trigger == 'hover' then 'mouseleave' else 'focusout'
@$element.on((eventIn) + '.' + @type, @options.selector, $.proxy(@enter, @))
@$element.on((eventOut) + '.' + @type, @options.selector, $.proxy(@leave, @))
if @options.selector
@_options = $.extend({}, @options, { trigger: 'manual', selector: '' })
else
@fixTitle()
return
getDefault: ->
$.fn.tooltip.defaults
getOptions: (options) ->
options = $.extend({}, @getDefault(), @$element.data(), options)
options.delay = show: options.delay, hide: options.delay if options.delay and typeof options.delay == 'number'
options
getDelegateOptions: ->
options = {}
defaults = @getDefaults()
@_options and $.each @_options, (key, value) ->
options[key] = value if defaults[key] != value
options
enter: (obj) ->
self = if obj instanceof @constructor then obj else $(obj.currentTarget).data('mice.' + @type)
if !self
self = new @constructor(obj.currentTarget, @getDelegateOptions())
$(obj.currentTarget).data('mice.' + @type, self)
clearTimeout(@timeout)
self.hoverState = 'in'
return self.show() if !self.options.delay or !self.options.delay.show
self.timeout = setTimeout (-> self.show() if self.hoverState == 'in') , self.options.delay.show
leave: (obj) ->
self = if obj instanceof @constructor then obj else $(obj.currentTarget).data('mice.' + @type)
if !self
self = new @constructor(obj.currentTarget, @getDelegateOptions())
$(obj.currentTarget).data('mice.' + @type, self)
clearTimeout(self.timeout)
self.hoverState = 'out'
return self.hide() if !self.options.delay or !self.options.delay.hide
self.timeout = setTimeout (-> self.hide() if self.hoverState == 'out') , self.options.delay.hide
show: ->
e = $.Event('show.mice.'+@type)
if @getTitle() and @enabled
@$element.trigger e
inDom = $.contains(document.documentElement, @$element[0])
return if e.isDefaultPrevented() or !inDom
that = @
$tip = @tip()
@setContent()
$tip.addClass 'fade' if @options.animation
placement = if typeof @options.placement == 'function' then @options.placement.call(@, $tip[0], @$element[0]) else @options.placement
autoToken = /\s?auto?\s?/i
autoPlace = autoToken.test(placement)
placement = placement.replace(autoToken, '') or 'top' if autoPlace
$tip.detach().css({ top: 0, left: 0, display: 'block' }).addClass(placement).data('mice.' + @type, @)
if @options.container then $tip.appendTo(@options.container) else $tip.insertAfter(@$element)
pos = @getPosition()
actualWidth = $tip[0].offsetWidth
actualHeight = $tip[0].offsetHeight
if autoPlace
orgPlacement = placement
$parent = @$element.parent()
parentDim = @getPosition($parent)
placement = if placement == 'bottom' and pos.top + pos.height + actualHeight - parentDim.scroll > parentDim.height then 'top' else
if placement == 'top' and pos.top - parentDim.scroll - actualHeight < 0 then 'bottom' else
if placement == 'right' and pos.right + actualWidth > parentDim.width then 'left' else
if placement == 'left' and pos.left - actualWidth < parentDim.left then 'right' else placement
$tip.removeClass(orgPlacement).addClass(placement)
calculatedOffset = @getCalculatedOffset placement, pos, actualWidth, actualHeight
@applyPlacement calculatedOffset, placement
complete = ->
that.$element.trigger('shown.mice.' + that.type)
that.hoverState = null
if $.support.transition and @$tip.hasClass('fade') then $tip.one('miceTransitionEnd', complete).emulateTransitionEnd(150) else complete()
applyPlacement: (offset, placement) ->
$tip = @tip()
width = $tip[0].offsetWidth
height = $tip[0].offsetHeight
# manually read margins because getBoundingClientRect includes difference
marginTop = parseInt($tip.css('margin-top'), 10)
marginLeft = parseInt($tip.css('margin-left'), 10)
# we must check for NaN for ie 8/9
offset.top = offset.top + if isNaN(marginTop) then 0 else marginTop
offset.left = offset.left + if isNaN(marginLeft) then 0 else marginLeft
# $.fn.offset doesn't round pixel values
# so we use setOffset directly with our own function B-0
$.offset.setOffset($tip[0], $.extend({ using: (props) -> $tip.css({ top: Math.round(props.top), left: Math.round(props.left) }) }, offset), 0)
$tip.addClass 'in'
# check to see if placing tip in new offset caused the tip to resize itself
actualWidth = $tip[0].offsetWidth
actualHeight = $tip[0].offsetHeight
offset.top = offset.top + height - actualHeight if placement == 'top' and actualHeight != height
delta = @getViewportAdjustedDelta placement, offset, actualWidth, actualHeight
if delta.left then offset.left += delta.left else offset.top += delta.top
arrowDelta = if delta.left then delta.left * 2 - width + actualWidth else delta.top * 2 - height + actualHeight
arrowPosition = if delta.left then 'left' else 'top'
arrowOffsetPosition = if delta.left then 'offsetWidth' else 'offsetHeight'
$tip.offset(offset)
@replaceArrow arrowDelta, $tip[0][arrowOffsetPosition], arrowPosition
replaceArrow: (delta, dimension, position) ->
@arrow().css(position, if delta then (50 * (1 - delta / dimension) + '%') else '')
setContent: ->
@tip().find('.inner')[if @options.html then 'html' else 'text'](@getTitle()).removeClass('fade in top bottom left right')
hide: ->
that = @
$tip = @tip()
e = $.Event('hide.mice.' + @type)
complete = ->
$tip.detach() if that.hoverState != 'in'
that.$element.trigger('hidden.mice.' + that.type)
@$element.trigger(e)
return if e.isDefaultPrevented()
$tip.removeClass('in')
if $.support.transition and @$tip.hasClass('fade') then $tip.one('miceTransitionEnd', complete).emulateTransitionEnd(150) else complete()
@hoverState = null
@
fixTitle: ->
@$element.attr('data-original-title', @$element.attr('title') or '').removeAttr('title') if @$element.attr('title') or typeof (@$element.attr('data-original-title')) != 'string'
getPosition: ($element) ->
$element = $element or @$element
el = $element[0]
isBody = el.tagName == 'BODY'
return $.extend({}, (if (typeof el.getBoundingClientRect == 'function') then el.getBoundingClientRect() else null), {
scroll: if isBody then document.documentElement.scrollTop or document.body.scrollTop else $element.scrollTop()
width: if isBody then $(window).width() else $element.outerWidth()
height: if isBody then $(window).height() else $element.outerHeight()
}, if isBody then { top: 0, left: 0 } else $element.offset())
getCalculatedOffset: (placement, pos, actualWidth, actualHeight) ->
switch placement
when 'bottom' then { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 }
when 'top' then { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 }
when 'left' then { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth }
when 'right' then { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }
getViewportAdjustedDelta: (placement, pos, actualWidth, actualHeight) ->
delta = { top: 0, left: 0 }
return delta if !@$viewport
viewportPadding = @options.viewport and @options.viewport.padding or 0
viewportDimensions = @getPosition @$viewport
if /right|left/.test placement
topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll
bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight
if topEdgeOffset < viewportDimensions.top #top overflow
delta.top = viewportDimensions.top - topEdgeOffset
else if bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height #bottom overflow
delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset
else
leftEdgeOffset = pos.left - viewportPadding
rightEdgeOffset = pos.left + viewportPadding + actualWidth
if leftEdgeOffset < viewportDimensions.left #left overflow
delta.left = viewportDimensions.left - leftEdgeOffset
else if rightEdgeOffset > viewportDimensions.width #right overflow
delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset
delta
getTitle: ->
@$element.attr('data-original-title') or (if typeof @options.title == 'function' then @options.title.call(@$element[0]) else @options.title)
tip: ->
@$tip = @$tip or $(@options.template).addClass @options.contextual
arrow: ->
@$arrow = @$arrow or @tip().find('.arrow')
validate: ->
if !@$element[0].parentNode
@hide()
@$element = null
@options = null
enable: ->
@enabled = true
disable: ->
@enabled = false
toggleEnabled: ->
@enabled = !@enabled
toggle: (e) ->
self = @
if e
self = $(e.currentTarget).data('mice.' + @type)
if !self
self = new @constructor(e.currentTarget, @getDelegateOptions())
$(e.currentTarget).data('mice.' + @type, self)
if self.tip().hasClass('in') then self.leave(self) else self.enter(self)
destroy: ->
clearTimeout(@timeout)
@hide().$element.off('.' + @type).removeData('mice.' + @type)
# TOOLTIP PLUGIN DEFINITION
# =========================
$.fn.tooltip = (option) ->
@each ->
$element = $(@)
data = $element.data('mice.tooltip')
options = typeof option == 'object' and option
return if !data and option == 'destroy'
$element.data('mice.tooltip', (data = new Tooltip(@, options))) if !data
data[option]() if typeof option == 'string'
$.fn.tooltip.Constructor = Tooltip
# TOOLTIP PLUGIN DEFAULT OPTIONS
# =========================
$.fn.tooltip.defaults =
animation: true
placement: 'top'
contextual: ''
selector: false
template: ''
trigger: 'hover focus'
title: ''
delay: 0
html: false
container: false
viewport:
selector: 'body'
padding: 0
return
) jQuery