# Flying Widgets v0.1.1
#
# To use, put this file in assets/javascripts/cycleDashboard.coffee. Then find this line in
# application.coffee:
#
# $('.gridster ul:first').gridster
#
# And change it to:
#
# $('.gridster > ul').gridster
#
# Finally, put multiple gridster divs in your dashboard, and add a call to Dashing.cycleDashboards()
# to the javascript at the top of your dashboard:
#
#
#
# <% content_for :title do %>Loop Dashboard<% end %>
#
#
#
#
#
# Some generic helper functions
sleep = (timeInSeconds, fn) -> setTimeout fn, timeInSeconds * 1000
isArray = (obj) -> Object.prototype.toString.call(obj) is '[object Array]'
isString = (obj) -> Object.prototype.toString.call(obj) is '[object String]';
isFunction = (obj) -> obj && obj.constructor && obj.call && obj.apply
#### Show/Hide functions.
#
# Every member of `showFunctions` and `hideFunctions` must be one of:
#
# * A `{start, end, transition}` object (transition defaults to 'all 1s'.)
# * A `{transitionFunction}` object.
# * A `fn($dashboard, widget, originalLocations)` which returns one of the above.
#
# The easiest way to define a transition is just to specify start and end CSS proprties for each
# widget with a `{start, end}` object. The `fadeOut` and `fadeIn` are some of the simplest
# examples below. Sometimes you might need slightly more control, in which case `start` and
# `end` can each be functions of the form `($widget, index)`, where $widget is the jquery object
# for the widget being transformed, and index is the index of the widget within the dashboard.
# The function form is handy when you want to do something different for each widget, depending
# on it's location.
#
# For even more control, you can specify a `fn($dashboard, widgets, originalLocations)` function
# in place of the entire object. This is handy when you have some setup work to do for your
# transition, such as detecting the width of the page so you can move all widgets off-screen.
#
# For the ultimate in control, you can specify a
# `transitionFunction{$dashboard, options, done}` object. It will be up to you to
# do whatever you need to do in order to hide or display the dashboard. The CSS of every widget
# will be reset to something sane when the function completes, but otherwise it's entirely
# up to you. Params are:
#
# * `$dashboard` - jquery object of the dashboard to show/hide.
# * `options.stagger` - True if transition should be staggered.
# * `options.widgets` - An array of all widgets in the dashboard.
# * `options.originalLocations` - An array of CSS data about the location, opacity, etc... of
# each widget.
# * `done()` - Async callback. Make sure you call this!
#
hideFunctions = {
toRight: ($dashboard, widgets, originalLocations) ->
documentWidth = $(document).width()
return {end: (($widget) -> {left: documentWidth, opacity: 0})}
shrink: {
start: {
opacity: 1,
transform: 'scale(1,1)',
"-webkit-transform": 'scale(1,1)'
},
end: {
transform: 'scale(0,0)',
"-webkit-transform": 'scale(0,0)',
opacity: 0
}
}
fadeOut: {
start: {opacity: 1}
end: {opacity: 0}
}
explode: {
start: {
opacity: 1
transform: 'scale(1,1)',
"-webkit-transform": 'scale(1,1)'
}
end: {
opacity: 0
transform: 'scale(2,2)',
"-webkit-transform": 'scale(2,2)'
}
}
# 3D spinning transition. It's cool, but it crashes Chrome if you sleep and wake your machine.
# spin: {
# transitionFunction: ($dashboard, options, done) ->
# # Add perspective to the container, so we can spin the dashboard in 3D
# $parent = $dashboard.parent()
# $parent.css({perspective: 500, "-webkit-perspective": 500})
# # Do the transition
# moveWithTransition [$dashboard], {
# transition: 'all 1s',
# start: {
# transform: 'rotateY(0deg)'
# "-webkit-transform": 'rotateY(0deg)'
# }
# end: {
# transform: 'rotateY(90deg)'
# "-webkit-transform": 'rotateY(90deg)'
# },
# timeInSeconds: 1}, ->
# $dashboard.hide()
# # Remove changes
# sleep 0, ->
# $dashboard.parent().css({perspective: '', "-webkit-perspective": ''})
# $dashboard.css({
# transform: ''
# "-webkit-transform": ''
# })
# done()
# chainsTo: {
# transitionFunction: ($dashboard, options, done) ->
# # Add perspective to the container, so we can spin the dashboard in 3D
# $parent = $dashboard.parent()
# $parent.css({perspective: 500, "-webkit-perspective": 500})
# $dashboard.show()
# # Do the transition
# moveWithTransition [$dashboard], {
# transition: 'all 1s',
# start: {
# transform: 'rotateY(-90deg)'
# "-webkit-transform": 'rotateY(-90deg)'
# }
# end: {
# transform: 'rotateY(0deg)'
# "-webkit-transform": 'rotateY(0deg)'
# },
# timeInSeconds: 1}, ->
# # Remove changes
# sleep 0, ->
# $dashboard.parent().css({perspective: '', "-webkit-perspective": ''})
# $dashboard.css({
# transform: ''
# "-webkit-transform": ''
# })
# done()
# }
# }
}
# Handy function for reversing simple transitions
reverseTransition = (obj) ->
if isFunction(obj) or obj.transitionFunction?
throw new Error("Can't reverse transition")
return {start: obj.end, end: obj.start, transition: obj.transition}
showFunctions = {
fromLeft: ($dashboard, widgets, originalLocations) ->
start: (($widget, index) -> {left: "#{-$widget.width() - $dashboard.width()}px", opacity: 0}),
end: (($widget, index) -> originalLocations[index]),
fromTop: ($dashboard, widgets, originalLocations) ->
start: (($widget, index) -> {top: "#{-$widget.height() - $dashboard.height()}px", opacity: 0}),
end: (($widget, index) -> return originalLocations[index]),
zoom: reverseTransition(hideFunctions.shrink)
fadeIn: reverseTransition(hideFunctions.fadeOut)
implode: reverseTransition(hideFunctions.explode)
}
# Move an element from one place to another using a CSS3 transition.
#
# * `elements` - One or more elements to move, in an array.
# * `transition` - The transition string to apply (e.g.: 'left 1s ease 0s')
# * `start` - This can be an object (e.g. `{left: 0px}`) or a `fn($el, index)`
# which returns such an object. This is the location the object will start at.
# If start is omitted, then the current location of the object will be used
# as the start.
# * `end` - As with `start`, this can be an object or a function. `end` is required.
# * `timeInSeconds` - The time required to complete the transition. This function will
# wait this long before calling `done()`.
# * `offset` is an offset for the index passed into `start()` and `end()`. Handy when
# you want to split up an array of
# * `done()` - Async callback.
moveWithTransition = (elements, {transition, start, end, timeInSeconds, offset}, done) ->
transition = transition or ''
timeInSeconds = timeInSeconds or 0
end = end or {}
offset = offset or 0
origTransitions = []
moveToStart = () ->
for el, index in elements
$el = $(el)
origTransitions[index + offset] = $el.css 'transition'
$el.css transition: 'left 0s ease 0s'
$el.css(if isFunction start then start($el, index + offset) else start)
moveToEnd = () ->
for el, index in elements
$el = $(el)
$el.css transition: transition
$el.css(if isFunction end then end($el, index + offset) else end)
sleep Math.max(0, timeInSeconds), ->
$el.css transition: origTransitions[index + offset]
done? null
if start
moveToStart()
sleep 0, -> moveToEnd()
else
moveToEnd()
# Runs a function which shows or hides the dashboard. This function ensures that all the
# dashboards widgets end up where they started.
#
# Transitions should be a `{start, end}` object suitable for passing to moveWithTransition,
# or a `transitions($dashboard, widgets, originalLocations)` function which returns such an object.
#
showHideDashboard = (visible, stagger, $dashboard, transitions, done) ->
$dashboard = $($dashboard)
$ul = $dashboard.children('ul')
$widgets = $ul.children('li')
# Record the original location, opacity, other CSS attributes we might want to edit
originalLocations = []
$widgets.each (index, widget) ->
$widget = $(widget)
originalLocations[index] = {
left: $widget.css 'left'
top: $widget.css 'top'
width: $widget.css 'width'
height: $widget.css 'height'
opacity: $widget.css 'opacity'
transform: $widget.css 'transform'
"-webkit-transform": $widget.css '-webkit-transform'
}
widgets = $.makeArray($widgets)
if isFunction transitions
transitions = transitions($dashboard, widgets, originalLocations)
origDone = done
done = () ->
sleep 0, () ->
# Make sure the dashboard is in a sane state.
$dashboard.toggle( visible )
sleep 0, () ->
# Clear any styles we've set on the widgets.
#
# TODO: It would be nice to record the styles before we start, and then restore them
# here, but I've found that if my laptop goes to sleep, when it wakes up, when
# displaying the dashboard on Chrome, it sometimes picks up bad values for
# `originalLocations`. By always forcing the style to a sane known value, we know
# everything will work out in the end.
#
$dashboard.children('ul').children('li').attr 'style', 'position: absolute'
origDone?()
transitionString = "all 1s"
if transitions.transitionFunction
# Show/hide the dashboard with a custom function
transitionFunction = transitions.transitionFunction
else if !stagger
transitionFunction = ($dashboard, {widgets, originalLocations}, fnDone) ->
moveWithTransition widgets, {
end: transitions.start
}, -> sleep 0, ->
if visible then $dashboard.show()
moveWithTransition widgets, {
start: transitions.start,
end: transitions.end,
transition: transitions.transition or transitionString,
timeInSeconds: 1
}, fnDone
else
transitionFunction = ($dashboard, {widgets, originalLocations}, fnDone) ->
singleWidgetFn = (widget, index) ->
moveWithTransition [widget], {
end: transitions.start,
offset: index
}, -> sleep 0, ->
if visible then $dashboard.show()
sleep (Math.random()/2), () ->
moveWithTransition [widget], {
start: transitions.start,
end: transitions.end,
transition: transitions.transition or transitionString,
timeInSeconds: 1,
offset: index
}, ->
for widget, index in widgets
singleWidgetFn(widget, index)
sleep 1.5, fnDone
# Show or hide the dashboard
transitionFunction $dashboard, {stagger, widgets, originalLocations}, done
# Select a member at random from an object.
#
# If 'allowedMembers' is an array of strings, then only the corresponding members will be
# considered for selection.
#
# Returns a "{key, value}" object.
pickMember = (object, allowedMembers=null) ->
answer = null
functionArray = []
if allowedMembers?
if not isArray allowedMembers then allowedMembers = [allowedMembers]
for memberName in allowedMembers
if memberName of object then functionArray.push {key: memberName, value: object[memberName]}
else
for memberName, member of object
functionArray.push {key: memberName, value: member}
if functionArray.length > 0
index = Math.floor(Math.random()*functionArray.length);
answer = functionArray[index]
return answer
# Cycle the dashboard to the next dashboard.
#
# If a transition is already in progress, this function does nothing.
Dashing.cycleDashboardsNow = do () ->
transitionInProgress = false
visibleIndex = 0
(options = {}) ->
return if transitionInProgress
transitionInProgress = true
{stagger, fastTransition} = options
stagger = !!stagger
fastTransition = !!fastTransition
$dashboards = $('.gridster')
# Work out which dashboard to show
oldVisibleIndex = visibleIndex
visibleIndex++
if visibleIndex >= $dashboards.length
visibleIndex = 0
if oldVisibleIndex == visibleIndex
# Only one dashboard. Disable fast transitions
fastTransition = false
doneCount = 0
doneFn = () ->
doneCount++
# Only set transitionInProgress to false when both the show and the hide functions
# are finished.
if doneCount is 2
transitionInProgress = false
# Hide the old dashboard
hideFunction = pickMember hideFunctions
showNewDashboard = () ->
options.onTransition?($($dashboards[visibleIndex]))
showFunction = null
chainsTo = hideFunction.value.chainsTo
if isString chainsTo
showFunction = showFunctions[chainsTo]
else if chainsTo?
showFunction = {key: "chainsTo", value: chainsTo}
if !showFunction
showFunction = pickMember showFunctions
# console.log "Showing dashboard #{visibleIndex} #{showFunction.key}"
showHideDashboard true, stagger, $dashboards[visibleIndex], showFunction.value, () ->
doneFn()
# console.log "Hiding dashboard #{oldVisibleIndex} #{hideFunction.key}"
showHideDashboard false, stagger, $dashboards[oldVisibleIndex], hideFunction.value, () ->
if !fastTransition
showNewDashboard()
doneFn()
# If fast transitions are enabled, then don't wait for the hiding animation to complete
# before showing the new dashboard.
if fastTransition then showNewDashboard()
return null
# Adapted from http://stackoverflow.com/questions/1403888/get-url-parameter-with-javascript-or-jquery
getURLParameter = (name) ->
encodedParameter = (RegExp(name + '=' + '(.+?)(&|$)').exec(location.search)||[null,null])[1]
return if encodedParameter? then decodeURI(encodedParameter) else null
# Cause dashing to cycle from one dashboard to the next.
#
# Dashboard cycling can be bypassed by passing a "page" parameter in the url. For example,
# going to http://dashboardserver/mydashboard?page=2 will show the second dashboard in the list
# and will not cycle.
#
# Options:
# * `timeInSeconds` - The time to display each dashboard, in seconds. If 0, then dashboards will
# not automatically cycle, but can be cycled manually by calling `cycleDashboardsNow()`.
# * `stagger` - If this is true, each widget will be transitioned individually at slightly
# randomized times. This gives a more random look. If false, then all wigets will be moved
# at the same time. Note if `timeInSeconds` is 0, then this option is ignored (but can, instead,
# be passed to `cycleDashboardsNow()`.)
# * `fastTransition` - If true, then we will run the show and hide transitions simultaneously.
# This gets your new dashboard up onto the screen faster.
# * `onTransition($newDashboard)` - A function to call before a dashboard is displayed.
#
Dashing.cycleDashboards = (options) ->
timeInSeconds = if options.timeInSeconds? then options.timeInSeconds else 20
$dashboards = $('.gridster')
startDashboardParam = getURLParameter('page')
startDashboard = parseInt(startDashboardParam) or 1
startDashboard = Math.max startDashboard, 1
startDashboard = Math.min startDashboard, $dashboards.length
$dashboards.each (dashboardIndex, dashboard) ->
# Hide all but the first dashboard.
$(dashboard).toggle(dashboardIndex is (startDashboard - 1))
# Set all dashboards to position: absolute so they stack one on top of the other
$(dashboard).css "position": "absolute"
# If the user specified a dashboard, then don't cycle from one dashboard to the next.
if !startDashboardParam? and (timeInSeconds > 0)
cycleFn = () -> Dashing.cycleDashboardsNow(options)
setInterval cycleFn, timeInSeconds * 1000
$(document).keypress (event) ->
# Cycle to next dashboard on space
if event.keyCode is 32 then Dashing.cycleDashboardsNow(options)
return true
# Customized version of `Dashing.gridsterLayout()` which supports multiple dashboards.
Dashing.cycleGridsterLayout = (positions) ->
#positions = positions.replace(/^"|"$/g, '') # ??
positions = JSON.parse(positions)
$dashboards = $(".gridster > ul")
if isArray(positions) and ($dashboards.length == positions.length)
Dashing.customGridsterLayout = true
for position, index in positions
$dashboard = $($dashboards[index])
widgets = $dashboard.children("[data-row^=]")
for widget, index in widgets
$(widget).attr('data-row', position[index].row)
$(widget).attr('data-col', position[index].col)
else
console.log "Warning: Could not apply custom layout!"
# Redefine functions for saving layout
sleep 0.1, () ->
Dashing.getWidgetPositions = ->
dashboardPositions = []
for dashboard in $(".gridster > ul")
dashboardPositions.push $(dashboard).gridster().data('gridster').serialize()
return dashboardPositions
Dashing.showGridsterInstructions = ->
newWidgetPositions = Dashing.getWidgetPositions()
if !isArray(newWidgetPositions[0])
$('#save-gridster').slideDown()
$('#gridster-code').text("
Something went wrong - reload the page and try again.
")
else
unless JSON.stringify(newWidgetPositions) == JSON.stringify(Dashing.currentWidgetPositions)
Dashing.currentWidgetPositions = newWidgetPositions
$('#save-gridster').slideDown()
$('#gridster-code').text("
")