diff --git a/.gitignore b/.gitignore index d87d4be..11e079c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +.tmp/ +chromedriver.log +node_modules/ *.gem *.rbc .bundle diff --git a/Gemfile b/Gemfile index d90922d..1e50a97 100644 --- a/Gemfile +++ b/Gemfile @@ -2,3 +2,7 @@ source 'https://rubygems.org' # Specify your gem's dependencies in attentive.gemspec gemspec + +gem 'flowerbox', :path => '../flowerbox' +gem 'flowerbox-delivery', :path => '../flowerbox-delivery' +gem 'guard' diff --git a/Guardfile b/Guardfile new file mode 100644 index 0000000..0ed9c8d --- /dev/null +++ b/Guardfile @@ -0,0 +1,33 @@ +require 'guard' +require 'guard/guard' + +require 'flowerbox' + +class ::Guard::FlowerboxUnit < ::Guard::Guard + def run_all + ::Flowerbox.run('spec/javascripts') + end + + def run_on_change(files) + run_all + end +end + +class ::Guard::FlowerboxIntegration < ::Guard::Guard + def run_all + ::Flowerbox.run('js-features') + end + + def run_on_change(files) + run_all + end +end + +guard 'flowerbox-unit' do + watch(%r{^spec/javascripts}) +end + +guard 'flowerbox-integration' do + watch(%r{^js-features}) +end + diff --git a/js-features/presentation_timer.feature b/js-features/presentation_timer.feature new file mode 100644 index 0000000..5d202c0 --- /dev/null +++ b/js-features/presentation_timer.feature @@ -0,0 +1,13 @@ +Feature: Presentation Timer + Background: + Given I have a presentation counter + + Scenario: Render + When I render the counter + Then the counter should show the time "00:00" + And the counter should not be running + + Scenario: Start the timer + When I start the timer + Then the counter should be running + diff --git a/js-features/spec_helper.rb b/js-features/spec_helper.rb new file mode 100644 index 0000000..f22a66f --- /dev/null +++ b/js-features/spec_helper.rb @@ -0,0 +1,13 @@ +Flowerbox.configure do |c| + c.test_with :cucumber + c.run_with :chrome + + c.asset_paths << "lib/assets/javascripts" + c.spec_patterns << "**/*.js*" + c.spec_patterns << "*.js*" + c.spec_patterns << "**/*.feature" + c.spec_patterns << "*.feature" + + c.report_with :verbose +end + diff --git a/js-features/steps/given/i_have_a_presentation_counter.js.coffee b/js-features/steps/given/i_have_a_presentation_counter.js.coffee new file mode 100644 index 0000000..152fca7 --- /dev/null +++ b/js-features/steps/given/i_have_a_presentation_counter.js.coffee @@ -0,0 +1,5 @@ +#= require attentive/presentation_timer +# +Flowerbox.Given /I have a presentation counter/, -> + @timer = new Attentive.PresentationTimer() + @timer.render() diff --git a/js-features/steps/then/counter_should_have_state.js.coffee b/js-features/steps/then/counter_should_have_state.js.coffee new file mode 100644 index 0000000..b2df97c --- /dev/null +++ b/js-features/steps/then/counter_should_have_state.js.coffee @@ -0,0 +1,7 @@ +Flowerbox.Then /^the counter (should|should not) be running$/, (state) -> + switch state + when 'should' + @expect(@timer).toBeRunning() + when 'should not' + @expect(@timer).not.toBeRunning() + diff --git a/js-features/steps/then/counter_should_show_time.js.coffee b/js-features/steps/then/counter_should_show_time.js.coffee new file mode 100644 index 0000000..3e641bc --- /dev/null +++ b/js-features/steps/then/counter_should_show_time.js.coffee @@ -0,0 +1,8 @@ +Flowerbox.Then /^the counter (should|should not) show the time "([^"]+)"$/, (state, time) -> + content = @timer.ensureEl().innerHTML + + switch state + when 'should' + @expect(content).toEqual(time) + when 'should not' + @expect(content).not.toEqual(time) diff --git a/js-features/steps/then/it_should_count_up.js.coffee b/js-features/steps/then/it_should_count_up.js.coffee new file mode 100644 index 0000000..a1c491a --- /dev/null +++ b/js-features/steps/then/it_should_count_up.js.coffee @@ -0,0 +1,3 @@ +Flowerbox.Then /^it should count up$/, -> + @pending() + diff --git a/js-features/steps/when/i_render_the_counter.js.coffee b/js-features/steps/when/i_render_the_counter.js.coffee new file mode 100644 index 0000000..6a21d5e --- /dev/null +++ b/js-features/steps/when/i_render_the_counter.js.coffee @@ -0,0 +1,3 @@ +Flowerbox.When /^I render the counter$/, -> + @timer.render() + diff --git a/js-features/steps/when/i_start_the_counter.js.coffee b/js-features/steps/when/i_start_the_counter.js.coffee new file mode 100644 index 0000000..73922c5 --- /dev/null +++ b/js-features/steps/when/i_start_the_counter.js.coffee @@ -0,0 +1,3 @@ +Flowerbox.When /^I start the timer$/, -> + @timer.start() + diff --git a/js-features/steps/when/i_wait_time.js.coffee b/js-features/steps/when/i_wait_time.js.coffee new file mode 100644 index 0000000..33e0228 --- /dev/null +++ b/js-features/steps/when/i_wait_time.js.coffee @@ -0,0 +1,3 @@ +Flowerbox.When /^I wait (\d+) seconds?$/, (secs) -> + Flowerbox.pause(Number(secs) * 1000) + diff --git a/js-features/support/env.js.coffee b/js-features/support/env.js.coffee new file mode 100644 index 0000000..eb6b134 --- /dev/null +++ b/js-features/support/env.js.coffee @@ -0,0 +1,7 @@ +Flowerbox.World -> + @addMatchers( + toBeRunning: () -> + @message = "Expected #{@notMessage} be running" + @actual._runner? + ) + diff --git a/lib/assets/javascripts/attentive.js.coffee b/lib/assets/javascripts/attentive.js.coffee index e288e09..b541a33 100644 --- a/lib/assets/javascripts/attentive.js.coffee +++ b/lib/assets/javascripts/attentive.js.coffee @@ -1,212 +1,5 @@ +#= require attentive/presentation +#= require attentive/slide +#= require attentive/presentation_timer #= require underscore -# -class PresentationTimer - constructor: -> - @time = 0 - @el = document.createElement('div') - @el.classList.add('timer') - this.render() - - render: -> - @el.innerHTML = this.formattedTime() - - start: -> - @_runner = this.runner() - @el.classList.add('running') - - runner: -> - setTimeout( - => - this.render() - @time += 1 - this.runner() if @_runner? - , 1000 - ) - - stop: -> - clearTimeout(@_runner) - @el.classList.remove('running') - @_runner = null - - reset: -> - this.stop() - @time = 0 - this.render() - - toggle: -> - if @_runner? - this.stop() - else - this.start() - - toggleVisible: -> - @el.classList.toggle('hide') - - isVisible: -> - !@el.classList.contains('hide') - - hide: -> - @el.classList.add('hide') - - formattedTime: -> - minute = "00#{Math.floor(@time / 60)}".slice(-2) - second = "00#{@time % 60}".slice(-2) - - "#{minute}:#{second}" - -class Slide - @fromList: (list) -> - result = (new Slide(slide) for slide in list) - - constructor: (@dom) -> - - recalculate: => - @dom.style['width'] = "#{window.innerWidth}px" - - currentMarginTop = Number(@dom.style['marginTop'].replace(/[^\d\.]/g, '')) - height = (window.innerHeight - @dom.querySelector('.content').clientHeight) / 2 - - if height != currentMarginTop - @dom.style['marginTop'] = "#{height}px" - true - - activate: => - @dom.classList.add('active') - - deactivate: => - @dom.classList.remove('active') - -class this.Attentive - @setup: (identifier) -> - starter = -> - setTimeout( - -> - (new Attentive(identifier)).start() - , 250 - ) - window.addEventListener('DOMContentLoaded', starter, false) - - constructor: (@identifier) -> - @length = @allSlides().length - @priorSlide = null - @initialRender = true - - @timer = new PresentationTimer() - @timer.hide() - - @currentWindowHeight = null - - document.querySelector('body').appendChild(@timer.el) - - bodyClassList: -> - @_bodyClassList ||= document.querySelector('body').classList - - allSlides: -> - @_allSlides ||= Slide.fromList(@slidesViewer().querySelectorAll('.slide')) - - slidesViewer: -> - @_slidesViewer ||= document.querySelector(@identifier) - - start: -> - if !this.isFile() - window.addEventListener('popstate', @handlePopState, false) - - document.addEventListener('click', @handleClick, false) - document.addEventListener('keydown', @handleKeyDown, false) - window.addEventListener('resize', _.throttle(@calculate, 500), false) - - imageWait = null - imageWait = => - wait = false - - for slide in @allSlides() - for img in slide.dom.getElementsByTagName('img') - wait = true if !img.complete - - if wait - setTimeout(imageWait, 100) - else - this.advanceTo(this.slideFromLocation()) - - imageWait() - - slideFromLocation: -> - value = if this.isFile() - location.hash - else - location.pathname - - Number(value.substr(1)) - - handlePopState: (e) => - this.advanceTo(if e.state then e.state.index else this.slideFromLocation()) - - handleClick: (e) => - this.advance() if e.target.tagName != 'A' - - handleKeyDown: (e) => - switch e.keyCode - when 72 - this.advanceTo(0) - when 37 - this.advance(-1) - when 39, 32 - this.advance() - when 220 - @timer.reset() - when 84 - if e.shiftKey - @timer.toggleVisible() - else - @timer.toggle() if @timer.isVisible() - - advance: (offset = 1) => - this.advanceTo(Math.max(Math.min(@currentSlide + offset, @length - 1), 0)) - - isFile: => location.href.slice(0, 4) == 'file' - - advanceTo: (index) => - @priorSlide = @currentSlide - @currentSlide = index || 0 - - this.calculate() - - if this.isFile() - location.hash = @currentSlide - else - history.pushState({ index: @currentSlide }, '', @currentSlide) - - calculate: => - if @currentWindowHeight != window.innerHeight - recalculate = true - times = 3 - - while recalculate and times > 0 - recalculate = false - times -= 1 - - for slide in @allSlides() - recalculate = true if slide.recalculate() - - @currentWindowHeight = window.innerHeight - - @slidesViewer().style['width'] = "#{window.innerWidth * @allSlides().length}px" - - this.align() - - getCurrentSlide: => - @allSlides()[@currentSlide] - - align: => - @allSlides()[@priorSlide].deactivate() if @priorSlide - this.getCurrentSlide().activate() - - @slidesViewer().style['left'] = "-#{@currentSlide * window.innerWidth}px" - - if @initialRender - @bodyClassList().remove('loading') - - @initialRender = false - @currentWindowHeight = null - this.calculate() diff --git a/lib/assets/javascripts/attentive/presentation.js.coffee b/lib/assets/javascripts/attentive/presentation.js.coffee new file mode 100644 index 0000000..3ce1896 --- /dev/null +++ b/lib/assets/javascripts/attentive/presentation.js.coffee @@ -0,0 +1,137 @@ +if !Attentive? then Attentive = {} + +class Attentive.Presentation + @setup: (identifier) -> + starter = -> + setTimeout( + -> + (new Attentive.Presentation(identifier)).start() + , 250 + ) + window.addEventListener('DOMContentLoaded', starter, false) + + constructor: (@identifier) -> + @length = @allSlides().length + @priorSlide = null + @initialRender = true + + @timer = new Attentive.PresentationTimer() + @timer.hide() + + @currentWindowHeight = null + + document.querySelector('body').appendChild(@timer.el) + + bodyClassList: -> + @_bodyClassList ||= document.querySelector('body').classList + + allSlides: -> + @_allSlides ||= Attentive.Slide.fromList(@slidesViewer().querySelectorAll('.slide')) + + slidesViewer: -> + @_slidesViewer ||= document.querySelector(@identifier) + + start: -> + if !this.isFile() + window.addEventListener('popstate', @handlePopState, false) + + @timer.render() + + document.addEventListener('click', @handleClick, false) + document.addEventListener('keydown', @handleKeyDown, false) + window.addEventListener('resize', _.throttle(@calculate, 500), false) + + imageWait = null + imageWait = => + wait = false + + for slide in @allSlides() + for img in slide.dom.getElementsByTagName('img') + wait = true if !img.complete + + if wait + setTimeout(imageWait, 100) + else + this.advanceTo(this.slideFromLocation()) + + imageWait() + + slideFromLocation: -> + value = if this.isFile() + location.hash + else + location.pathname + + Number(value.substr(1)) + + handlePopState: (e) => + this.advanceTo(if e.state then e.state.index else this.slideFromLocation()) + + handleClick: (e) => + this.advance() if e.target.tagName != 'A' + + handleKeyDown: (e) => + switch e.keyCode + when 72 + this.advanceTo(0) + when 37 + this.advance(-1) + when 39, 32 + this.advance() + when 220 + @timer.reset() + when 84 + if e.shiftKey + @timer.toggleVisible() + else + @timer.toggle() if @timer.isVisible() + + advance: (offset = 1) => + this.advanceTo(Math.max(Math.min(@currentSlide + offset, @length - 1), 0)) + + isFile: => location.href.slice(0, 4) == 'file' + + advanceTo: (index) => + @priorSlide = @currentSlide + @currentSlide = index || 0 + + this.calculate() + + if this.isFile() + location.hash = @currentSlide + else + history.pushState({ index: @currentSlide }, '', @currentSlide) + + calculate: => + if @currentWindowHeight != window.innerHeight + recalculate = true + times = 3 + + while recalculate and times > 0 + recalculate = false + times -= 1 + + for slide in @allSlides() + recalculate = true if slide.recalculate() + + @currentWindowHeight = window.innerHeight + + @slidesViewer().style['width'] = "#{window.innerWidth * @allSlides().length}px" + + this.align() + + getCurrentSlide: => + @allSlides()[@currentSlide] + + align: => + @allSlides()[@priorSlide].deactivate() if @priorSlide + this.getCurrentSlide().activate() + + @slidesViewer().style['left'] = "-#{@currentSlide * window.innerWidth}px" + + if @initialRender + @bodyClassList().remove('loading') + + @initialRender = false + @currentWindowHeight = null + this.calculate() diff --git a/lib/assets/javascripts/attentive/presentation_timer.js.coffee b/lib/assets/javascripts/attentive/presentation_timer.js.coffee new file mode 100644 index 0000000..b6eab2a --- /dev/null +++ b/lib/assets/javascripts/attentive/presentation_timer.js.coffee @@ -0,0 +1,59 @@ +if !Attentive? then Attentive = {} + +class Attentive.PresentationTimer + constructor: -> + @time = 0 + @el = null + + render: -> + @ensureEl().innerHTML = this.formattedTime() + + ensureEl: -> + if !@el + @el = document.createElement('div') + @el.classList.add('timer') + @el + + start: -> + @_runner = this.runner() + @ensureEl().classList.add('running') + + runner: -> + setTimeout( + => + this.render() + @time += 1 + this.runner() if @_runner? + , 1000 + ) + + stop: -> + clearTimeout(@_runner) + @ensureEl().classList.remove('running') + @_runner = null + + reset: -> + this.stop() + @time = 0 + this.render() + + toggle: -> + if @_runner? + this.stop() + else + this.start() + + toggleVisible: -> + @ensureEl().classList.toggle('hide') + + isVisible: -> + !@ensureEl().classList.contains('hide') + + hide: -> + @ensureEl().classList.add('hide') + + formattedTime: -> + minute = "00#{Math.floor(@time / 60)}".slice(-2) + second = "00#{@time % 60}".slice(-2) + + "#{minute}:#{second}" diff --git a/lib/assets/javascripts/attentive/slide.js.coffee b/lib/assets/javascripts/attentive/slide.js.coffee new file mode 100644 index 0000000..2c43160 --- /dev/null +++ b/lib/assets/javascripts/attentive/slide.js.coffee @@ -0,0 +1,24 @@ +if !Attentive? then Attentive = {} + +class Attentive.Slide + @fromList: (list) -> + result = (new Attentive.Slide(slide) for slide in list) + + constructor: (@dom) -> + + recalculate: => + @dom.style['width'] = "#{window.innerWidth}px" + + currentMarginTop = Number(@dom.style['marginTop'].replace(/[^\d\.]/g, '')) + height = (window.innerHeight - @dom.querySelector('.content').clientHeight) / 2 + + if height != currentMarginTop + @dom.style['marginTop'] = "#{height}px" + true + + activate: => + @dom.classList.add('active') + + deactivate: => + @dom.classList.remove('active') + diff --git a/lib/attentive/server.rb b/lib/attentive/server.rb index c66f2da..c7a9b0c 100644 --- a/lib/attentive/server.rb +++ b/lib/attentive/server.rb @@ -20,6 +20,8 @@ module Attentive require 'coffee_script' require 'sass' + Tilt::CoffeeScriptTemplate.default_bare = true + # make sure pygments is ready before starting a new thread Pygments.highlight("attentive") diff --git a/skel/.gitignore b/skel/.gitignore index e69de29..7b4f00c 100644 --- a/skel/.gitignore +++ b/skel/.gitignore @@ -0,0 +1,2 @@ +.sass-cache/ + diff --git a/skel/assets/javascripts/application.js.coffee b/skel/assets/javascripts/application.js.coffee index 17d140a..3a40d0b 100644 --- a/skel/assets/javascripts/application.js.coffee +++ b/skel/assets/javascripts/application.js.coffee @@ -1,4 +1,4 @@ #= require attentive # -Attentive.setup('#slides') +Attentive.Presentation.setup('#slides') diff --git a/spec/javascripts/attentive/presentation_timer_spec.js.coffee b/spec/javascripts/attentive/presentation_timer_spec.js.coffee new file mode 100644 index 0000000..3e16bd4 --- /dev/null +++ b/spec/javascripts/attentive/presentation_timer_spec.js.coffee @@ -0,0 +1,5 @@ +#= require attentive/presentation_timer + +describe 'Attentive.PresentationTimer', -> + describe '#render', -> + it 'should do something', -> diff --git a/spec/javascripts/spec_helper.rb b/spec/javascripts/spec_helper.rb new file mode 100644 index 0000000..094259c --- /dev/null +++ b/spec/javascripts/spec_helper.rb @@ -0,0 +1,9 @@ +Flowerbox.configure do |c| + c.test_with :jasmine + c.run_with :node + + c.asset_paths << "lib/assets/javascripts" + + c.report_with :verbose +end +