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