chrisrutz
9/11/2013 - 6:00 PM

Dashing News Ticker Widget

Dashing News Ticker Widget

// ----------------------------------------------------------------------------
// Sass declarations
// ----------------------------------------------------------------------------
$background-color: rgba(0, 0, 0, .6);
$text-color:       #ffff00;

$border-color:  #ffffff;
$border:        1px solid $border-color;

$height: 60px;
$width: 100%;

// ----------------------------------------------------------------------------
// Widget-ticker styles
// ----------------------------------------------------------------------------
.widget-ticker { 

  position: fixed;
	bottom:  10px;
	z-index: 1000;

	padding: 0px !important;
	width: $width;
	height: $height;
	color: $text-color;
	background: $background-color;

	/* the outer div with the border */
	.tickercontainer {
		border: $border;
		background: $background-color;
		height: $height;
		margin: 0; 
		padding: 0;
		overflow: hidden; 

	    li {
			float: left;
		}

	    .mask { 
			position: relative;
			left: 0px;
			top: 0px;
			overflow: hidden;
			height: $height;
		}
		
	}

	ul.newsticker { 
		position: relative;
		list-style-type: none;

		-webkit-transform: translate3d(0, 0, 0);
		-webkit-backface-visibility: hidden;
		-webkit-perspective: 1000;
	}

	ul.newsticker li {
		float: left;
		font-size: 20px;
		font-weight: bold;
		font-style: italic;
		line-height: $height;
		height: $height;
		white-space: nowrap;
		text-align: center;
	}
}
ticker_items = [
  "Staff meeting today in the cafeteria today at 10am.     Don't forget to give yourself 20 minutes to walk over there.",
	"The Dashing Widget Challenge deadline has been moved to September 26th.      Don't delay in getting your awesome widget submitted!"
]

SCHEDULER.every '10m', :first_in => 0 do
	send_event( 'ticker', { :items => ticker_items } )
end
<ul id="news_ticker">
    <li data-foreach-item="items" data-bind="item | raw"></li>
</ul>
class Dashing.Ticker extends Dashing.Widget

  # pixels per second
  @SPEED:  70

  # easing function to use for the animation 
  @EASING: "linear"

  # Whether to use CSS3 animations.  If false, jQuery animate() will be used.
  @CSS_ANIMATION: true

  # Number of milliseconds between jQuery animation frames.  The
  # default is 13, which causes this ticker to have pretty high cpu usage.
  # 40 is about the highest I've gone before animations start to look bad.
  # Lower numbers will cause smoother animations.
  # NOTE: this will cause a change to GLOBAL jQuery animations on the page.
  # Only used if CSS_ANIMATION is false.
  @FX_INTERVAL: 30

  # Used for vertical scrolling, the number of seconds to show an alert before
  # scrolling to the next one.
  @DELAY = 10

  getTicker: ->
    $("#news_ticker")

  # should be either "vertical" or "horizontal"
  getScrollOrientation: ->
    if $(@node).data('scroll_orientation')
      $(@node).data('scroll_orientation')
    else
      "vertical"

  initialize: ->
    return true if @initialized
    if not $(".gridster.ready").length or not @getTicker().length
      @log( "not ready yet..." )
      return false

    @log( "initializing..." )

    if not Ticker.CSS_ANIMATION and @getScrollOrientation() == 'horizontal'
      @log( "setting jQuery fx interval to #{Ticker.FX_INTERVAL}" )
      jQuery.fx.interval = Ticker.FX_INTERVAL
      $.fn.transition = $.fn.animate

    @ticker = @getTicker()
    # Set a width first based on the board size
    @log( "setting node width to " + $(".gridster.ready").width() )
    $(@node).width( $(".gridster.ready").width() )

    # Wrap the ticker in some extra divs
    @log( "adding wrapper divs" )
    @ticker.addClass('newsticker')
    @ticker.wrap('<div class="mask" />')
    @ticker.parent().wrap('<div class="tickercontainer" />')

    @initialized = true


  ready: ->
    # This is fired when the widget is done being rendered
    @log("ready")
    @doTicker()

  onData: (data) ->
    @log( "onData" )
    @doTicker()

  doTicker: ->
    @initialize() if not @initialized
    if @initialized
      if @hasChanged() and @hasItems()
        @resetTicker()
      else if not @hasItems()
        @stopTicker()

  startTicker: ->
    @log( "" )
    @log("starting ticker")
    @should_stop = false
    $(@node).show()

    if @getScrollOrientation() == "horizontal"
      @doHorizontalScroll()
    else
      @doVerticalScroll()


  doHorizontalScroll: ->
    @log( "doing horizontal scroll" )

    viewWidth = @ticker.parent().width()
    @log( "viewWidth: #{viewWidth}" )

    # Add a spacer between ticker elements so that only one of them is showing at once
    @insertSpacers( viewWidth )

    # calculate content width
    @log( "calculating content width..." )
    contentWidth = @contentWidth()

    # set ticker width to the sum of the viewport and the content
    tickerWidth = viewWidth + contentWidth
    @log( "setting ticker width to #{tickerWidth}" )
    @ticker.css('width', tickerWidth)
    # set ticker left value to viewWidth
    @ticker.css('left', viewWidth)

    # then animate the left value to -contentWidth
    animationSettings = { 'left': -contentWidth }
    @log( "animating 'left' value from #{viewWidth} to #{-contentWidth}" )
    duration = tickerWidth * 1000 / Ticker.SPEED
    console.log( "duration: #{duration} ms" )
    @ticker.transition animationSettings, duration, Ticker.EASING, =>
      if not @should_stop
        @log( "loop complete, do it again!" )
        @startTicker()

  doVerticalScroll: ->
    @log( "doing vertical scroll" )
    @itemHeight = @ticker.children('li').first().outerHeight()
    contentHeight = @itemHeight * @ticker.children('li').size()

    # set ticker height to the content height
    @ticker.css('height', contentHeight)

    # set ticker top value to @itemHeight
    @ticker.css('top', @itemHeight)

    @ticker.children('li').width('100%')

    @nextItem()

  nextItem: =>
    @log( "scrolling to next item" )
    animationSettings = { 'top': @ticker.position().top - @itemHeight }
    duration = 1000
    @ticker.transition animationSettings, duration, "linear", =>
      if not @should_stop 
        @log( "comparing #{@ticker.position().top - 2} to #{-@ticker.outerHeight()}" )
        if @ticker.position().top - 2 > -@ticker.outerHeight()
          @log( "scheduling the next item" )
          @scrollNext = setTimeout( @nextItem, Ticker.DELAY * 1000 )
        else
          @log( "loop complete, do it again!" )
          @startTicker()

  stopTicker: ->
    @log("stopping ticker")
    @should_stop = true
    if @ticker
      @ticker.stop( true )
      clearTimeout( @scrollNext )
      @ticker.removeAttr('style')
    $(@node).hide()

  resetTicker: ->
    @stopTicker()
    @startTicker()

  contentWidth: ->
    contentWidth = 0
    for child in @ticker.children() 
      contentWidth += $(child).outerWidth( true )
    return contentWidth

  hasItems: ->
    return @get('items' ) and @get('items').length > 0

  hasChanged: ->
    new_items = @get('items')
    changed = false
    if @old_items == new_items
      # short circuit, they're probably both null
      return false
    if @old_items and not new_items
      changed = true
    else if new_items and not @old_items
      changed = true
    else if @old_items.toString() != new_items.toString()
      changed = true
    if changed
      @log( "items changed" )
    else
      @log( "items the same" )
    @old_items = new_items
    return changed

  insertSpacers: (width) ->
    @log( "inserting spacers" )
    @ticker.children().not(':last-child').css('margin-right', width)

  log: (msg) ->
    # console.log( "[ticker] #{msg}" )
    return

##Description A Dashing widget that overlays a scrolling news ticker on the bottom of your Dashboard.

This widget is a little different, in that it doesn't occupy a typical space in the Dashing grid, but instead overlays the bottom of the dashboard. If it doesn't have any data, it will be hidden. If you send it an array of Strings, it will scroll through them either vertically or horizontally. Our office uses it on our dashboards to occasionally flash important notices or reminders in an eye catching fashion, most of the time it is not displayed.

See http://widget-challenge.herokuapp.com/vertical-ticker and http://widget-challenge.herokuapp.com/horizontal-ticker for a live demo.

##Screenshots

##Dependencies jQuery Transit

Put jquery.transit.js in your assets/javascripts/ directory to use CSS3 animations (the default). Or change @CSS_ANIMATION to false in ticker.coffee and jQuery's animate() will be used instead.

##Usage To install this widget, copy ticker.html, ticker.scss, and ticker.coffee in to the widgets/ticker directory. Optionally, copy the ticker.rb file into your jobs folder for example usage.

To include the widget on a dashboard, add the following snippet to your dashboard layout file BETWEEN the closing ul tag and closing div tag with the class gridster like this:

    </ul>
    <div data-id="ticker" data-view="Ticker" style="display: none;" ></div>
</div>

By default, items will scroll vertically, pausing for 10 seconds on each item. You also have the option of a continuous horizontal scrolling animation if you specify:

<div data-id="ticker" data-view="Ticker" data-scroll_orientation="horizontal" style="display: none;" ></div>

The widget is not displayed by default, and will only display if it has content. You send it an array of any number of Strings, and it will rotate through them. If you send it an empty array, it will hide itself again.

curl -d '{ "auth_token": "YOUR_AUTH_TOKEN", "items": ["Hello World!", "Well Hello Yourself!"] }' http://localhost:3030/widgets/ticker

curl -d '{ "auth_token": "YOUR_AUTH_TOKEN", "items": [] }' http://localhost:3030/widgets/ticker