# 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(" ")