diff --git a/assets/scripts/app/app.coffee b/assets/scripts/app/app.coffee
index 5c454d2a..ef4b2682 100644
--- a/assets/scripts/app/app.coffee
+++ b/assets/scripts/app/app.coffee
@@ -35,7 +35,8 @@ unless window.TravisApplication
@slider = new Travis.Slider()
@pusher = new Travis.Pusher(Travis.config.pusher_key) if Travis.config.pusher_key
- @tailing = new Travis.Tailing()
+ @tailing = new Travis.Tailing($(window), '#tail', '#log')
+ @toTop = new Travis.ToTop($(window), '.to-top', '#log-container')
@set('auth', Travis.Auth.create(app: this, endpoint: Travis.config.api_endpoint))
diff --git a/assets/scripts/app/tailing.coffee b/assets/scripts/app/tailing.coffee
index fab3fec8..666c2c51 100644
--- a/assets/scripts/app/tailing.coffee
+++ b/assets/scripts/app/tailing.coffee
@@ -1,12 +1,51 @@
-@Travis.Tailing = ->
- @position = $(window).scrollTop()
- $(window).scroll( $.throttle( 200, @onScroll.bind(this) ) )
- this
+class Travis.ToTop
+ # NOTE: I could have probably extract fixed positioning from
+ # Tailing, but then I would need to parametrize positionElement
+ # function to make it flexible to handle both cases. In that
+ # situation I prefer a bit less DRY code over simplicity of
+ # the calculations.
+ constructor: (@window, @element_selector, @container_selector) ->
+ @position = @window.scrollTop()
+ @window.scroll( $.throttle( 200, @onScroll.bind(this) ) )
+ this
-$.extend Travis.Tailing.prototype,
+ element: ->
+ $(@element_selector)
+ container: ->
+ $(@container_selector)
+
+ onScroll: ->
+ @positionElement()
+
+ positionElement: ->
+ element = @element()
+ container = @container()
+ return if element.length is 0
+ containerHeight = container.height()
+ windowHeight = @window.height()
+ offset = container.offset().top + containerHeight - (@window.scrollTop() + windowHeight)
+ max = containerHeight - windowHeight
+ offset = max if offset > max
+ console.log(offset, max)
+ if offset > 0
+ element.css(bottom: offset)
+ else
+ element.css(bottom: 2)
+
+class @Travis.Tailing
options:
timeout: 200
+ tail: ->
+ $(@tail_selector)
+ log: ->
+ $(@log_selector)
+
+ constructor: (@window, @tail_selector, @log_selector) ->
+ @position = @window.scrollTop()
+ @window.scroll( $.throttle( 200, @onScroll.bind(this) ) )
+ this
+
run: ->
@autoScroll()
@positionButton()
@@ -16,37 +55,43 @@ $.extend Travis.Tailing.prototype,
if @active() then @stop() else @start()
active: ->
- $('#tail').hasClass('active')
+ @tail().hasClass('active')
start: ->
- $('#tail').addClass('active')
+ @tail().addClass('active')
@run()
stop: ->
- $('#tail').removeClass('active')
+ @tail().removeClass('active')
autoScroll: ->
- return unless @active()
- win = $(window)
- log = $('#log')
- logBottom = log.offset().top + log.outerHeight() + 40
- winBottom = win.scrollTop() + win.height()
- win.scrollTop(logBottom - win.height()) if logBottom - winBottom > 0
+ return false unless @active()
+ logBottom = @log().offset().top + @log().outerHeight() + 40
+ winBottom = @window.scrollTop() + @window.height()
+
+ if logBottom - winBottom > 0
+ @window.scrollTop(logBottom - @window.height())
+ true
+ else
+ false
onScroll: ->
@positionButton()
- position = $(window).scrollTop()
+ position = @window.scrollTop()
@stop() if position < @position
@position = position
positionButton: ->
- tail = $('#tail')
- return if tail.length is 0
- offset = $(window).scrollTop() - $('#log').offset().top
- max = $('#log').height() - $('#tail').height() + 5
- offset = max if offset > max
- if offset > 0
- tail.css(top: offset - 2)
- else
- tail.css(top: 0)
+ return if @tail().length is 0
+ offset = @window.scrollTop() - @log().offset().top
+ max = @log().height() - @tail().height() + 5
+ if offset > 0 && offset <= max
+ @tail().removeClass('bottom')
+ @tail().addClass('scrolling')
+ else
+ if offset > max
+ @tail().addClass('bottom')
+ else
+ @tail().removeClass('bottom')
+ @tail().removeClass('scrolling')
diff --git a/assets/scripts/app/templates/jobs/pre.hbs b/assets/scripts/app/templates/jobs/pre.hbs
index e2a33456..0184bee8 100644
--- a/assets/scripts/app/templates/jobs/pre.hbs
+++ b/assets/scripts/app/templates/jobs/pre.hbs
@@ -1,7 +1,14 @@
-
+
+
diff --git a/assets/scripts/spec/unit/tailing_spec.coffee b/assets/scripts/spec/unit/tailing_spec.coffee
new file mode 100644
index 00000000..18a836f0
--- /dev/null
+++ b/assets/scripts/spec/unit/tailing_spec.coffee
@@ -0,0 +1,89 @@
+fakeWindow =
+ scroll: sinon.spy()
+ scrollTop: sinon.stub().returns(0)
+ height: sinon.stub().returns(40)
+element = jQuery('
')
+log = jQuery('
')
+tail = new Travis.Tailing(fakeWindow, '#specTail', '#specLog')
+tail.tail = -> element
+tail.log = -> log
+
+module "Travis.Tailing",
+ setup: ->
+ jQuery('body').append(element)
+ jQuery('body').append(log)
+
+ teardown: ->
+ element.remove()
+ log.remove()
+ tail.stop()
+
+test "toggle", ->
+ equal(element.hasClass('active'), false)
+ tail.toggle()
+ equal(element.hasClass('active'), true)
+ tail.toggle()
+ stop()
+
+ Ember.run.later ->
+ start()
+ equal(element.hasClass('active'), false)
+ , 300
+
+test "active", ->
+ equal(tail.active(), false)
+ element.addClass('active')
+ equal(tail.active(), true)
+
+test "autoscroll when inactive", ->
+ tail.scrollTo = sinon.spy()
+
+ equal(tail.active(), false)
+ equal(tail.autoScroll(), false)
+ equal(tail.scrollTo.called, false)
+
+test "autoscroll", ->
+ element.addClass('active')
+ log.offset = -> {top: 1}
+ log.outerHeight = -> 1
+
+ equal(tail.active(), true)
+ equal(tail.autoScroll(), true)
+ equal(fakeWindow.scrollTop.calledWith(2), true)
+
+test "autoscroll when we're at the bottom", ->
+ element.addClass('active')
+ log.offset = -> {top: 0}
+ log.outerHeight = -> 0
+
+ equal(tail.active(), true)
+ equal(tail.autoScroll(), false)
+ equal(fakeWindow.scrollTop.calledWith(0), false)
+
+test 'should stop scrolling if the position changed', ->
+ element.addClass('active')
+ tail.position = 100
+ tail.onScroll()
+ equal(element.hasClass('active'), false)
+
+test 'positionButton adds the scrolling class', ->
+ log.offset = -> {top: -1}
+
+ tail.positionButton()
+ equal(element.hasClass('scrolling'), true)
+ equal(element.hasClass('bottom'), false)
+
+test 'positionButton removes the scrolling class', ->
+ log.offset = -> {top: 1}
+ tail.positionButton()
+ equal(element.hasClass('scrolling'), false)
+ equal(element.hasClass('bottom'), false)
+
+test 'positionButton sets the button as bottom', ->
+ log.offset = -> {top: -100}
+ log.height = -> 50
+ tail.height = -> 1
+
+ tail.positionButton()
+ equal(element.hasClass('scrolling'), false)
+ equal(element.hasClass('bottom'), true)
diff --git a/assets/styles/main/log.sass b/assets/styles/main/log.sass
index a6083550..cb9afcd8 100644
--- a/assets/styles/main/log.sass
+++ b/assets/styles/main/log.sass
@@ -88,44 +88,67 @@ pre#log
#log-container
position: relative
-#log-container #tail
- z-index: 99
- position: absolute
- display: block
- top: 0
- right: 2px
- margin: 13px 10px 0 0
- padding: 0 2px 0 3px
- color: #666
- text-shadow: 0px 1px 0px #fff
- font-family: "Helvetica Neue", Helvetica, Arial, sans-serif
- font-size: $font-size-tiny
- line-height: 14px
- text-decoration: none
- white-space: nowrap
- border: 1px solid #bbb
- border-top-color: #ddd
- border-bottom-color: #bbb
- @include border-radius(8px)
- @include background(linear-gradient(#fff, #e0e0e0))
+#log-container
+ #tail
+ z-index: 99
+ position: absolute
+ display: block
+ top: 0
+ right: 2px
+ margin: 13px 10px 0 0
+ padding: 0 2px 0 3px
+ color: #666
+ text-shadow: 0px 1px 0px #fff
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif
+ font-size: $font-size-tiny
+ line-height: 14px
+ text-decoration: none
+ white-space: nowrap
+ border: 1px solid #bbb
+ border-top-color: #ddd
+ border-bottom-color: #bbb
+ @include border-radius(8px)
+ @include background(linear-gradient(#fff, #e0e0e0))
- label
- display: none
- cursor: pointer
-
- &:hover
- padding: 1px 4px 1px 6px
label
- display: inline
+ display: none
+ cursor: pointer
- .status
- display: inline-block
- margin-right: 1px
- width: 8px
- height: 8px
- background-color: #aaa
- @include border-radius(4px)
- @include box-shadow(white 1px 1px 2px)
+ &:hover
+ padding: 1px 4px 1px 6px
+ label
+ display: inline
- &.active .status
- background-color: #6b0
+ &.scrolling
+ position: fixed
+ right: 32px
+
+ &.bottom
+ bottom: 45px
+ top: inherit
+
+ .status
+ display: inline-block
+ margin-right: 1px
+ width: 8px
+ height: 8px
+ background-color: #aaa
+ @include border-radius(4px)
+ @include box-shadow(white 1px 1px 2px)
+
+ &.active .status
+ background-color: #6b0
+
+ .to-top
+ z-index: 99
+ position: absolute
+ display: block
+ bottom: 2px
+ right: 2px
+
+ width: 50px
+ margin-right: 2px
+ padding-right: 16px
+ text-align: right
+ color: #999
+ background: inline-image('ui/to-top.png') no-repeat right 6px
diff --git a/assets/styles/main/sponsors.sass b/assets/styles/main/sponsors.sass
index cfbbdc16..e5c79998 100644
--- a/assets/styles/main/sponsors.sass
+++ b/assets/styles/main/sponsors.sass
@@ -3,12 +3,3 @@
// float: left
margin-top: 0
color: #999
- .to-top
- display: inline-block
- width: 50px
- float: right
- margin-right: 2px
- padding-right: 16px
- text-align: right
- color: #999
- background: inline-image('ui/to-top.png') no-repeat right 6px