(($, window) ->
document = window.document
class Tour
constructor: (options) ->
try
storage = window.localStorage
catch
# localStorage may be unavailable due to security settings
storage = false
@_options = $.extend
name: 'tour'
steps: []
container: 'body'
autoscroll: true
keyboard: true
storage: storage
debug: false
backdrop: false
backdropPadding: 0
redirect: true
orphan: false
duration: false
delay: false
basePath: ''
template: '
').parent().html()
_reflexEvent: (reflex) ->
if ({}).toString.call(reflex) is '[object Boolean]' then 'click' else reflex
# Prevent popover from crossing over the edge of the window
_reposition: ($tip, step) ->
offsetWidth = $tip[0].offsetWidth
offsetHeight = $tip[0].offsetHeight
tipOffset = $tip.offset()
originalLeft = tipOffset.left
originalTop = tipOffset.top
offsetBottom = $(document).outerHeight() - tipOffset.top - $tip.outerHeight()
tipOffset.top = tipOffset.top + offsetBottom if offsetBottom < 0
offsetRight = $('html').outerWidth() - tipOffset.left - $tip.outerWidth()
tipOffset.left = tipOffset.left + offsetRight if offsetRight < 0
tipOffset.top = 0 if tipOffset.top < 0
tipOffset.left = 0 if tipOffset.left < 0
$tip.offset(tipOffset)
# Reposition the arrow
if step.placement is 'bottom' or step.placement is 'top'
if originalLeft isnt tipOffset.left
@_replaceArrow $tip, (tipOffset.left - originalLeft) * 2, offsetWidth, 'left'
else
if originalTop isnt tipOffset.top
@_replaceArrow $tip, (tipOffset.top - originalTop) * 2, offsetHeight, 'top'
# Center popover in the page
_center: ($tip) ->
$tip.css('top', $(window).outerHeight() / 2 - $tip.outerHeight() / 2)
# Copy pasted from bootstrap-tooltip.js with some alterations
_replaceArrow: ($tip, delta, dimension, position)->
$tip.find('.arrow').css position, if delta then 50 * (1 - delta / dimension) + '%' else ''
# Scroll to the popup if it is not in the viewport
_scrollIntoView: (element, callback) ->
$element = $(element)
return callback() unless $element.length
$window = $(window)
offsetTop = $element.offset().top
windowHeight = $window.height()
scrollTop = Math.max(0, offsetTop - (windowHeight / 2))
@_debug "Scroll into view. ScrollTop: #{scrollTop}. Element offset: #{offsetTop}. Window height: #{windowHeight}."
counter = 0
$('body, html').stop(true, true).animate
scrollTop: Math.ceil(scrollTop),
=>
if ++counter is 2
callback()
@_debug """Scroll into view.
Animation end element offset: #{$element.offset().top}.
Window height: #{$window.height()}."""
# Debounced window resize
_onResize: (callback, timeout) ->
$(window).on "resize.tour-#{@_options.name}", ->
clearTimeout(timeout)
timeout = setTimeout(callback, 100)
# Event bindings for mouse navigation
_initMouseNavigation: ->
_this = @
# Go to next step after click on element with attribute 'data-role=next'
# Go to previous step after click on element with attribute 'data-role=prev'
# End tour after click on element with attribute 'data-role=end'
# Pause/resume tour after click on element with attribute 'data-role=pause-resume'
$(document)
.off("click.tour-#{@_options.name}", ".popover.tour-#{@_options.name} *[data-role='prev']")
.off("click.tour-#{@_options.name}", ".popover.tour-#{@_options.name} *[data-role='next']")
.off("click.tour-#{@_options.name}", ".popover.tour-#{@_options.name} *[data-role='end']")
.off("click.tour-#{@_options.name}", ".popover.tour-#{@_options.name} *[data-role='pause-resume']")
.on "click.tour-#{@_options.name}", ".popover.tour-#{@_options.name} *[data-role='next']", (e) =>
e.preventDefault()
@next()
.on "click.tour-#{@_options.name}", ".popover.tour-#{@_options.name} *[data-role='prev']", (e) =>
e.preventDefault()
@prev()
.on "click.tour-#{@_options.name}", ".popover.tour-#{@_options.name} *[data-role='end']", (e) =>
e.preventDefault()
@end()
.on "click.tour-#{@_options.name}", ".popover.tour-#{@_options.name} *[data-role='pause-resume']", (e) ->
e.preventDefault()
$this = $ @
$this.text if _this._paused then $this.data 'pause-text' else $this.data 'resume-text'
if _this._paused then _this.resume() else _this.pause()
# Keyboard navigation
_initKeyboardNavigation: ->
return unless @_options.keyboard
$(document).on "keyup.tour-#{@_options.name}", (e) =>
return unless e.which
switch e.which
when 39
e.preventDefault()
if @_isLast() then @next() else @end()
when 37
e.preventDefault()
@prev() if @_current > 0
when 27
e.preventDefault()
@end()
# Checks if the result of a callback is a promise
_makePromise: (result) ->
if result and $.isFunction(result.then) then result else null
_callOnPromiseDone: (promise, cb, arg) ->
if promise
promise.then (e) =>
cb.call(@, arg)
else
cb.call(@, arg)
_showBackdrop: (element) ->
return if @backdrop.backgroundShown
@backdrop = $ '
', class: 'tour-backdrop'
@backdrop.backgroundShown = true
$('body').append @backdrop
_hideBackdrop: ->
@_hideOverlayElement()
@_hideBackground()
_hideBackground: ->
if @backdrop
@backdrop.remove()
@backdrop.overlay = null
@backdrop.backgroundShown = false
_showOverlayElement: (step) ->
$element = $ step.element
return if not $element or $element.length is 0 or @backdrop.overlayElementShown
@backdrop.overlayElementShown = true
@backdrop.$element = $element.addClass 'tour-step-backdrop'
@backdrop.$background = $ '
', class: 'tour-step-background'
elementData =
width: $element.innerWidth()
height: $element.innerHeight()
offset: $element.offset()
@backdrop.$background.appendTo('body')
elementData = @_applyBackdropPadding step.backdropPadding, elementData if step.backdropPadding
@backdrop
.$background
.width(elementData.width)
.height(elementData.height)
.offset(elementData.offset)
_hideOverlayElement: ->
return unless @backdrop.overlayElementShown
@backdrop.$element.removeClass 'tour-step-backdrop'
@backdrop.$background.remove()
@backdrop.$element = null
@backdrop.$background = null
@backdrop.overlayElementShown = false
_applyBackdropPadding: (padding, data) ->
if typeof padding is 'object'
padding.top ?= 0
padding.right ?= 0
padding.bottom ?= 0
padding.left ?= 0
data.offset.top = data.offset.top - padding.top
data.offset.left = data.offset.left - padding.left
data.width = data.width + padding.left + padding.right
data.height = data.height + padding.top + padding.bottom
else
data.offset.top = data.offset.top - padding
data.offset.left = data.offset.left - padding
data.width = data.width + (padding * 2)
data.height = data.height + (padding * 2)
data
_clearTimer: ->
window.clearTimeout @_timer
@_timer = null
@_duration = null
window.Tour = Tour
) jQuery, window