showoff 0.9.8.1 → 0.9.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fca9e8bcaae915b49026c82cc34298fd4413505c
4
- data.tar.gz: 8aef14fae196f0aeadefbe9831419a57c3352021
3
+ metadata.gz: 43fa218d1a12936c933c64b8819f1f6c65812b71
4
+ data.tar.gz: b6a65b1480ca48095b43930af822a951daf0a9d6
5
5
  SHA512:
6
- metadata.gz: 17c1bca3813b395bbc4e5d3e281a73cea6ebba13637e77b90a026fa65be83759e226ab514b9b45be0b2d1a31550dbc35a4bb89515b2674e69a22e4cf9e87b441
7
- data.tar.gz: edcf51738ef555ddcb356a69a39069ab68deae628ab785eed81114e229c3570f1f552bd03119d270b8e9c9ea72f49b88dd37f21db65d91d07c107a242191f115
6
+ metadata.gz: 071852ca2bc0e2a8c1ebc36b73fab308c7b23f89905b25a5fca3a41010bda34f613312c3f1d60969d7772b64dce0d143cd4dc8c21191dc502fb86e9ca69733ff
7
+ data.tar.gz: ee1903e661ee16456371ac02717b59d6c8a0bb3524cf03abe696fab532c26dac9c31d2403d37bb7315246921ba3adb49e13eba6cf91b118d7f7c336226df08fe
data/README.rdoc CHANGED
@@ -1,6 +1,6 @@
1
- = ShowOff Presentation Software
1
+ = Showoff Presentation Software
2
2
 
3
- ShowOff is a Sinatra web app that reads simple configuration files for a
3
+ Showoff is a Sinatra web app that reads simple configuration files for a
4
4
  presentation. It is sort of like a Keynote web app engine - think S5 +
5
5
  Slidedown. I am using it to do all my talks in 2010, because I have a deep
6
6
  hatred in my heart for Keynote and yet it is by far the best in the field.
@@ -50,7 +50,7 @@ Please see the documentation in <tt>./documentation</tt> for further information
50
50
 
51
51
  = Real World Usage
52
52
 
53
- So far, ShowOff has been used in the following presentations (and many others):
53
+ So far, Showoff has been used in the following presentations (and many others):
54
54
 
55
55
  * LinuxConf.au 2010 - Wrangling Git - Scott Chacon
56
56
  http://github.com/schacon/showoff-wrangling-git
@@ -1,3 +1,3 @@
1
1
  # No namespace here since ShowOff is a class and I'd have to inherit from
2
2
  # Sinatra::Application (which we don't want to load here)
3
- SHOWOFF_VERSION = '0.9.8.1'
3
+ SHOWOFF_VERSION = '0.9.9'
data/lib/showoff.rb CHANGED
@@ -37,6 +37,7 @@ class ShowOff < Sinatra::Application
37
37
  set :statsdir, "stats"
38
38
  set :viewstats, "viewstats.json"
39
39
  set :feedback, "feedback.json"
40
+ set :forms, "forms.json"
40
41
 
41
42
  set :server, 'thin'
42
43
  set :sockets, []
@@ -61,6 +62,13 @@ class ShowOff < Sinatra::Application
61
62
  @@counter = Hash.new
62
63
  end
63
64
 
65
+ # keeps track of form responses. In memory to avoid concurrence issues.
66
+ begin
67
+ @@forms = JSON.parse(File.read("#{settings.statsdir}/#{settings.forms}"))
68
+ rescue
69
+ @@forms = Hash.new
70
+ end
71
+
64
72
  @@downloads = Hash.new # Track downloadable files
65
73
  @@cookie = nil # presenter cookie. Identifies the presenter for control messages
66
74
  @@current = Hash.new # The current slide that the presenter is viewing
@@ -282,6 +290,7 @@ class ShowOff < Sinatra::Application
282
290
  # Apply the template to the slide and replace the key to generate the content of the slide
283
291
  sl = process_content_for_replacements(template.gsub(/~~~CONTENT~~~/, slide.text))
284
292
  sl = Tilt[:markdown].new(nil, nil, engine_options) { sl }.render
293
+ sl = build_forms(sl, content_classes)
285
294
  sl = update_p_classes(sl)
286
295
  sl = process_content_for_section_tags(sl)
287
296
  sl = update_special_content(sl, @slide_count, name) # TODO: deprecated
@@ -314,6 +323,9 @@ class ShowOff < Sinatra::Application
314
323
  # scan for pagebreak tags. Should really only be used for handout notes or supplemental materials
315
324
  result.gsub!("~~~PAGEBREAK~~~", '<div class="break">continued...</div>')
316
325
 
326
+ # replace with form rendering placeholder
327
+ result.gsub!(/~~~FORM:([^~]*)~~~/, '<div class="form wrapper" title="\1"></div>')
328
+
317
329
  # Now check for any kind of options
318
330
  content.scan(/(~~~CONFIG:(.*?)~~~)/).each do |match|
319
331
  result.gsub!(match[0], settings.showoff_config[match[1]]) if settings.showoff_config.key?(match[1])
@@ -382,6 +394,163 @@ class ShowOff < Sinatra::Application
382
394
  markdown.gsub(/<p>\.(.*?) /, '<p class="\1">')
383
395
  end
384
396
 
397
+ # replace custom markup with html forms
398
+ def build_forms(content, classes=[])
399
+ classes.select { |cl| cl =~ /^form=(\w+)$/ }
400
+ # only process slides marked as forms
401
+ return content unless $1
402
+
403
+ begin
404
+ tools = '<div class="tools">'
405
+ tools << '<input type="button" class="display" value="Display Results">'
406
+ tools << '<input type="submit" value="Save" disabled="disabled">'
407
+ tools << '</div>'
408
+ form = "<form id='#{$1}' action='/form/#{$1}' method='POST'>#{content}#{tools}</form>"
409
+ doc = Nokogiri::HTML::DocumentFragment.parse(form)
410
+ doc.css('p').each do |p|
411
+ if p.text =~ /^(\w*) ?(?:->)? ?([^\*]*)? ?(\*?)= ?(.*)?$/
412
+ id = $1
413
+ name = $2.empty? ? $1 : $2
414
+ required = ! $3.empty?
415
+ rhs = $4
416
+
417
+ p.replace form_element(id, name, required, rhs, p.text)
418
+ end
419
+ end
420
+ doc.to_html
421
+ rescue Exception => e
422
+ @logger.warn "Form parsing failed: #{e.message}"
423
+ @logger.debug "Backtrace:\n\t#{e.backtrace.join("\n\t")}"
424
+ content
425
+ end
426
+ end
427
+
428
+ def form_element(id, name, required, rhs, text)
429
+ required = required ? 'required' : ''
430
+ str = "<div class='form element #{required}' id='#{id}'>"
431
+ str << "<label for='#{id}'>#{name}</label>"
432
+ case rhs
433
+ when /^\[\s+(\d*)\]$$/ # value = [ 5] (textarea)
434
+ str << form_element_textarea(id, name, $1)
435
+ when /^___+(?:\[(\d+)\])?$/ # value = ___[50] (text)
436
+ str << form_element_text(id, name, $1)
437
+ when /^\(x?\)/ # value = (x) option one () opt2 () opt3 -> option 3 (radio)
438
+ str << form_element_radio(id, name, rhs.scan(/\((x?)\)\s*([^()]+)\s*/))
439
+ when /^\[x?\]/ # value = [x] option one [] opt2 [] opt3 -> option 3 (checkboxes)
440
+ str << form_element_checkboxes(id, name, rhs.scan(/\[(x?)\] ?([^\[\]]+)/))
441
+ when /^{(.*)}$/ # value = {BOS, SFO, (NYC)} (select shorthand)
442
+ str << form_element_select(id, name, rhs.scan(/\(?\w+\)?/))
443
+ when /^{$/ # value = { (select)
444
+ str << form_element_select_multiline(id, name, text)
445
+ when '' # value = (radio/checkbox list)
446
+ str << form_element_multiline(id, name, text)
447
+ else
448
+ @logger.warn "Unmatched form element: #{rhs}"
449
+ end
450
+ str << '</div>'
451
+ end
452
+
453
+ def form_element_text(id, name, length)
454
+ "<input type='text' id='#{id}' name='#{id}' size='#{length}' />"
455
+ end
456
+
457
+ def form_element_textarea(id, name, rows)
458
+ rows = 3 if rows.empty?
459
+ "<textarea id='#{id}' name='#{id}' rows='#{rows}'></textarea>"
460
+ end
461
+
462
+ def form_element_radio(id, name, items)
463
+ form_element_check_or_radio_set('radio', id, name, items)
464
+ end
465
+
466
+ def form_element_checkboxes(id, name, items)
467
+ form_element_check_or_radio_set('checkbox', id, name, items)
468
+ end
469
+
470
+ def form_element_select(id, name, items)
471
+ str = "<select id='#{id}' name='#{name}'>"
472
+ str << '<option value="">----</option>'
473
+
474
+ items.each do |item|
475
+ if item =~ /\((\w+)\)/
476
+ item = $1
477
+ selected = 'selected'
478
+ else
479
+ selected = ''
480
+ end
481
+ str << "<option value='#{item}' #{selected}>#{item}</option>"
482
+ end
483
+ str << '</select>'
484
+ end
485
+
486
+ def form_element_select_multiline(id, name, text)
487
+ str = "<select id='#{id}' name='#{id}'>"
488
+ str << '<option value="">----</option>'
489
+
490
+ text.split("\n")[1..-1].each do |item|
491
+ case item
492
+ when /^ +\((\w+) -> (.+)\),?$/ # (NYC -> New York City)
493
+ str << "<option value='#{$1}' selected>#{$2}</option>"
494
+ when /^ +(\w+) -> (.+),?$/ # NYC -> New, York City
495
+ str << "<option value='#{$1}'>#{$2}</option>"
496
+ when /^ +\((.+)[^,],?$/ # (Boston)
497
+ str << "<option value='#{$1}' selected>#{$1}</option>"
498
+ when /^ +([^\(].+[^\),]),?$/ # Boston
499
+ str << "<option value='#{$1}'>#{$1}</option>"
500
+ end
501
+ end
502
+ str << '</select>'
503
+ end
504
+
505
+ def form_element_multiline(id, name, text)
506
+ str = '<ul>'
507
+
508
+ text.split("\n")[1..-1].each do |item|
509
+ case item
510
+ when /\((x?)\)\s*(\w+)\s*(?:->\s*(.*)?)?/
511
+ checked = $1.empty? ? '': "checked='checked'"
512
+ type = 'radio'
513
+ value = $2
514
+ label = $3 || $2
515
+ when /\[(x?)\]\s*(\w+)\s*(?:->\s*(.*)?)?/
516
+ checked = $1.empty? ? '': "checked='checked'"
517
+ type = 'checkbox'
518
+ value = $2
519
+ label = $3 || $2
520
+ end
521
+
522
+ str << '<li>'
523
+ str << form_element_check_or_radio(type, id, value, label, checked)
524
+ str << '</li>'
525
+ end
526
+ str << '</ul>'
527
+ end
528
+
529
+ def form_element_check_or_radio_set(type, id, name, items)
530
+ str = ''
531
+ items.each do |item|
532
+ checked = item[0].empty? ? '': "checked='checked'"
533
+
534
+ if item[1] =~ /^(\w*) -> (.*)$/
535
+ value = $1
536
+ label = $2
537
+ else
538
+ value = label = item[1]
539
+ end
540
+
541
+ str << form_element_check_or_radio(type, id, value, label, checked)
542
+ end
543
+ str
544
+ end
545
+
546
+ def form_element_check_or_radio(type, id, value, label, checked)
547
+ # yes, value and id are conflated, because this is the id of the parent widget
548
+
549
+ id = "#{id}[]" if type == 'checkbox'
550
+ str = "<input type='#{type}' name='#{id}' id='#{value}' value='#{value}' #{checked} />"
551
+ str << "<label for='#{value}'>#{label}</label>"
552
+ end
553
+
385
554
  # TODO: deprecated
386
555
  def update_special_content(content, seq, name)
387
556
  doc = Nokogiri::HTML::DocumentFragment.parse(content)
@@ -471,7 +640,7 @@ class ShowOff < Sinatra::Application
471
640
  end
472
641
 
473
642
  def update_commandline_code(slide)
474
- html = Nokogiri::XML.parse(slide)
643
+ html = Nokogiri::HTML.parse(slide)
475
644
  parser = CommandlineParser.new
476
645
 
477
646
  html.css('pre').each do |pre|
@@ -845,6 +1014,39 @@ class ShowOff < Sinatra::Application
845
1014
  (request.cookies['presenter'] == @@cookie)
846
1015
  end
847
1016
 
1017
+ post '/form/:id' do |id|
1018
+ @logger.warn("Saving form answers from ip:#{request.ip} for id:##{id}")
1019
+
1020
+ form = params.reject { |k,v| ['splat', 'captures', 'id'].include? k }
1021
+
1022
+ # make sure we've got a bucket for this form, then save our answers
1023
+ @@forms[id] ||= {}
1024
+ @@forms[id][request.ip] = form
1025
+
1026
+ form.to_json
1027
+ end
1028
+
1029
+ # Return a list of the totals for each alternative for each question of a form
1030
+ get '/form/:id' do |id|
1031
+ return nil unless @@forms.has_key? id
1032
+
1033
+ @@forms[id].each_with_object({}) do |(ip,form), sum|
1034
+ form.each do |key, val|
1035
+ sum[key] ||= {}
1036
+
1037
+ if val.class == Array
1038
+ val.each do |item|
1039
+ sum[key][item] ||= 0
1040
+ sum[key][item] += 1
1041
+ end
1042
+ else
1043
+ sum[key][val] ||= 0
1044
+ sum[key][val] += 1
1045
+ end
1046
+ end
1047
+ end.to_json
1048
+ end
1049
+
848
1050
  get '/eval_ruby' do
849
1051
  return eval_ruby(params[:code]) if ENV['SHOWOFF_EVAL_RUBY']
850
1052
 
@@ -1010,12 +1212,24 @@ class ShowOff < Sinatra::Application
1010
1212
 
1011
1213
  at_exit do
1012
1214
  if defined?(@@counter)
1013
- filename = "#{settings.statsdir}/#{settings.viewstats}"
1014
- if settings.verbose then
1015
- File.write(filename, JSON.pretty_generate(@@counter))
1016
- else
1017
- File.write(filename, @@counter.to_json)
1215
+ File.open("#{settings.statsdir}/#{settings.viewstats}", 'w') do |f|
1216
+ if settings.verbose then
1217
+ f.write(JSON.pretty_generate(@@counter))
1218
+ else
1219
+ f.write(@@counter.to_json)
1220
+ end
1221
+ end
1222
+ end
1223
+
1224
+ if defined?(@@forms)
1225
+ File.open("#{settings.statsdir}/#{settings.forms}", 'w') do |f|
1226
+ if settings.verbose then
1227
+ f.write(JSON.pretty_generate(@@forms))
1228
+ else
1229
+ f.write(@@forms.to_json)
1230
+ end
1018
1231
  end
1019
1232
  end
1233
+
1020
1234
  end
1021
1235
  end
@@ -157,6 +157,13 @@ div.zoomed {
157
157
  background: #eee;
158
158
  }
159
159
 
160
+ #preview .content form div.tools input[type=button].display {
161
+ display: inline;
162
+ }
163
+ #preview .content form div.tools input[type=submit] {
164
+ display: none;
165
+ }
166
+
160
167
  img#disconnected {
161
168
  margin: 0.5em 1em;
162
169
  }
@@ -61,16 +61,19 @@
61
61
  }
62
62
 
63
63
  /* plain (non-bullet) text */
64
- .content > p {
65
- font-size: 2em;
64
+ .content > p,
65
+ .content > form > p {
66
+ font-size: 2em;
66
67
  margin: 1em;
67
- text-align: center;
68
+ text-align: center;
68
69
  }
69
70
 
70
- .content > pre {
71
+ .content > pre,
72
+ .content > form > pre {
71
73
  font-size: 300%;
72
74
  }
73
- .content > blockquote {
75
+ .content > blockquote,
76
+ .content > form > blockquote {
74
77
  font-size: 250%;
75
78
  margin: 2em;
76
79
  }
@@ -92,9 +95,9 @@
92
95
  /* numbered lists are numbered */
93
96
  .content ol {
94
97
  margin-left: 40px;
95
- font-size: 3em;
96
- text-align: left;
97
- padding-left: 40px;
98
+ font-size: 3em;
99
+ text-align: left;
100
+ padding-left: 40px;
98
101
  }
99
102
  .content ol > li {
100
103
  list-style: decimal;
@@ -103,15 +106,15 @@
103
106
 
104
107
 
105
108
  /* ironically, normal lists have bullets and 'bullets' lists don't */
106
- .content > ul {
109
+ .content > ul,
110
+ .content > form > ul {
107
111
  list-style: disc;
112
+ font-size: 3em;
113
+ text-align: left;
114
+ padding-left: 40px;
108
115
  }
109
- .content > ul {
110
- font-size: 3em;
111
- text-align: left;
112
- padding-left: 40px;
113
- }
114
- .content > ul > li {
116
+ .content > ul > li,
117
+ .content > form > ul > li {
115
118
  padding: .5em;
116
119
  margin-left: 40px;
117
120
  }
@@ -308,7 +311,7 @@ img#disconnected {
308
311
  text-align: center;
309
312
  }
310
313
 
311
- #feedbackSidebar div.row.tools button#editSlide {
314
+ #feedbackSidebar div.row > button {
312
315
  width: 90%%;
313
316
  margin: auto 5%;
314
317
  }
@@ -448,6 +451,81 @@ a.fg-button { float:left; }
448
451
  content: leader(".") target-counter(attr(href), page);
449
452
  }
450
453
 
454
+ /**********************************
455
+ *** form widgets ***
456
+ **********************************/
457
+ .content div.form.element {
458
+ text-align: left;
459
+ padding-left: 40px;
460
+ }
461
+ .content div.form.element label {
462
+ margin-right: 0.5em;
463
+ }
464
+ .content div.form.element select {
465
+ font-size: 2em;
466
+ }
467
+ .content div.form.element textarea {
468
+ display: block;
469
+ width: 85%;
470
+ }
471
+ .content div.form.element ul {
472
+ list-style: disc;
473
+ }
474
+ .content div.form.element ul > li {
475
+ margin-left: 40px;
476
+ }
477
+ /* TODO: not sure why the preview window honks things up */
478
+ #preview .content div.form.element select {
479
+ font-size: 0.8em;
480
+ }
481
+ .content div.form.element.warning {
482
+ background-color: #ff7373;
483
+ }
484
+ .content form div.tools {
485
+ font-size: 2em;
486
+ }
487
+ .content form div.tools * {
488
+ float: right;
489
+ }
490
+ .content form div.tools input[type=submit],
491
+ .content form div.tools input[type=button] {
492
+ margin: 1em;
493
+ }
494
+ .content form div.tools input[type=submit].dirty {
495
+ color: red;
496
+ }
497
+ .content form div.tools input[type=button].display {
498
+ display: none;
499
+ }
500
+
501
+ div.rendered.form {
502
+ border: 1px solid #ccc;
503
+ border-radius: 0.5em;
504
+ margin: 1em;
505
+ padding: 0.25em;
506
+ }
507
+ div.rendered.form label {
508
+ display: block;
509
+ font-weight: bold;
510
+ border-bottom: 1px solid #999;
511
+ }
512
+ div.rendered.form .item {
513
+ display: block;
514
+ width: 0;
515
+ /* overflow: hidden; */
516
+ height: 1.25em;
517
+ white-space: nowrap;
518
+ }
519
+ div.rendered.form .item.barstyle0 { background-color: #bb73bb; }
520
+ div.rendered.form .item.barstyle1 { background-color: #59b859; }
521
+ div.rendered.form .item.barstyle2 { background-color: #e3742f; }
522
+ div.rendered.form .item.barstyle3 { background-color: #4848e8; }
523
+ div.rendered.form .item.barstyle4 { background-color: #f75d5d; }
524
+
525
+ #notes .form.wrapper {
526
+ display: none;
527
+ }
528
+
451
529
  /**********************************
452
530
  *** supplemental materials ***
453
531
  **********************************/
@@ -357,11 +357,20 @@ function postSlide()
357
357
  var notes = getCurrentNotes()
358
358
  }
359
359
  */
360
- var notes = getCurrentNotes()
361
- $('#notes').html(notes.html())
360
+ // clear out any existing rendered forms
361
+ try { clearInterval(renderFormInterval) } catch(e) {}
362
+ $('#notes div.form').empty();
363
+
364
+ var notes = getCurrentNotes();
365
+ $('#notes').html(notes.html());
366
+
367
+ var fileName = currentSlide.children().first().attr('ref');
368
+ $('#slideFile').text(fileName);
369
+
370
+ $("#notes div.form.wrapper").each(function(e) {
371
+ renderFormInterval = renderFormWatcher($(this));
372
+ });
362
373
 
363
- var fileName = currentSlide.children().first().attr('ref')
364
- $('#slideFile').text(fileName)
365
374
  }
366
375
  }
367
376
 
data/public/js/showoff.js CHANGED
@@ -40,7 +40,7 @@ function setupPreso(load_slides, prefix) {
40
40
 
41
41
  // Load slides fetches images
42
42
  loadSlidesBool = load_slides
43
- loadSlidesPrefix = prefix
43
+ loadSlidesPrefix = prefix || '/'
44
44
  loadSlides(loadSlidesBool, loadSlidesPrefix)
45
45
 
46
46
  doDebugStuff()
@@ -155,6 +155,36 @@ function initializePresentation(prefix) {
155
155
  } catch(e) {
156
156
  sh_highlightDocument();
157
157
  }
158
+
159
+ $(".content form").submit(function(e) {
160
+ e.preventDefault();
161
+ submitForm($(this));
162
+ });
163
+
164
+ // suspend hotkey handling
165
+ $(".content form :input").focus( function() {
166
+ document.onkeydown = null;
167
+ document.onkeyup = null;
168
+ });
169
+ $(".content form :input").blur( function() {
170
+ document.onkeydown = keyDown;
171
+ document.onkeyup = keyUp;
172
+ });
173
+
174
+ $(".content form :input").change(function(e) {
175
+ enableForm($(this));
176
+ });
177
+
178
+ $(".content form div.tools input.display").click(function(e) {
179
+ try {
180
+ // If we're a presenter, try to bust open the slave display
181
+ slaveWindow.renderForm($(this).closest('form').attr('id'));
182
+ }
183
+ catch (e) {
184
+ renderForm($(this).closest('form'));
185
+ }
186
+ });
187
+
158
188
  $("#preso").trigger("showoff:loaded");
159
189
  }
160
190
 
@@ -203,13 +233,15 @@ function checkSlideParameter() {
203
233
 
204
234
  function currentSlideFromName(name) {
205
235
  var count = 0;
206
- slides.each(function(s, slide) {
207
- if (name == $(slide).find(".content").attr("ref") ) {
208
- found = count;
209
- return false;
210
- }
211
- count++;
212
- });
236
+ if(name.length > 0 ) {
237
+ slides.each(function(s, slide) {
238
+ if (name == $(slide).find(".content").attr("ref") ) {
239
+ found = count;
240
+ return false;
241
+ }
242
+ count++;
243
+ });
244
+ }
213
245
  return count;
214
246
  }
215
247
 
@@ -381,6 +413,168 @@ function clearIf(elem, val) {
381
413
  if(elem.val() == val ) { elem.val(''); }
382
414
  }
383
415
 
416
+
417
+ // form handling
418
+ function submitForm(form) {
419
+ if(validateForm(form)) {
420
+ var dataString = form.serialize();
421
+ var formAction = form.attr("action");
422
+
423
+ $.post(formAction, dataString, function( data ) {
424
+ var submit = form.find("input[type=submit]")
425
+ submit.attr("disabled", "disabled");
426
+ submit.removeClass("dirty");
427
+ });
428
+ }
429
+ }
430
+
431
+ function validateForm(form) {
432
+ var success = true;
433
+
434
+ form.children('div.form.element.required').each(function() {
435
+ var count = $(this).find(':input:checked').length;
436
+ var value = $.trim($(this).children('input:text, textarea, select').first().val());
437
+
438
+ // if we have no checked inputs or content, then flag it
439
+ if(count || (value && value)) {
440
+ $(this).closest('div.form.element').removeClass('warning');
441
+ }
442
+ else {
443
+ $(this).closest('div.form.element').addClass('warning');
444
+ success = false;
445
+ }
446
+
447
+ });
448
+
449
+ return success;
450
+ }
451
+
452
+ function enableForm(element) {
453
+ var submit = element.closest('form').find(':submit')
454
+ submit.removeAttr("disabled");
455
+ submit.addClass("dirty")
456
+ }
457
+
458
+ function renderFormWatcher(element) {
459
+ var form = element.attr('title');
460
+ var action = $('.content form#'+form).attr('action');
461
+
462
+ element.empty();
463
+ element.attr('action', action); // yes, we're putting an action on a div. Sue me.
464
+ $('.content form#'+form+' div.form.element').each(function() {
465
+ $(this).clone().appendTo(element);
466
+ });
467
+
468
+ renderForm(element);
469
+ // short pause to let the form be rebuilt. Prevents screen flashing.
470
+ setTimeout(function() { element.show(); }, 100);
471
+ return setInterval(function() { renderForm(element); }, 3000);
472
+ }
473
+
474
+ function renderForm(form) {
475
+ if(typeof(form) == 'string') {
476
+ form = $('form#'+form);
477
+ }
478
+ var action = form.attr("action");
479
+ $.getJSON(action, function( data ) {
480
+ //console.log(data);
481
+ form.children('div.form.element').each(function() {
482
+ var key = $(this).attr('id');
483
+ var sum = 0;
484
+
485
+ $(this).find('ul > li > *').each(function() {
486
+ $(this).parent().parent().before(this);
487
+ });
488
+ $(this).children('ul').each(function() {
489
+ $(this).remove();
490
+ });
491
+
492
+ // replace all input widgets with spans for the bar chart
493
+ var max = 5;
494
+ var style = 0;
495
+ $(this).children(':input').each(function() {
496
+ switch( $(this).attr('type') ) {
497
+ case 'text':
498
+ case 'button':
499
+ case 'submit':
500
+ case 'textarea':
501
+ // we don't render these
502
+ $(this).parent().remove();
503
+ break;
504
+
505
+ case 'radio':
506
+ case 'checkbox':
507
+ // Just render these directly and migrate the label to inside the span
508
+ var name = $(this).attr('id');
509
+ var label = $(this).next('label');
510
+ var text = label.text();
511
+
512
+ if(text.match(/^-+$/)) {
513
+ $(this).remove();
514
+ }
515
+ else{
516
+ $(this).replaceWith('<div class="item barstyle'+style+'" id="'+name+'">'+text+'</div>');
517
+ }
518
+ label.remove();
519
+ break;
520
+
521
+ default:
522
+ // select doesn't have a type attribute... yay html
523
+ // poke inside to get options, then render each as a span and replace the select
524
+ parent = $(this).parent();
525
+
526
+ $(this).children('option').each(function() {
527
+ var value = $(this).val();
528
+ var text = $(this).text();
529
+
530
+ if(! text.match(/^-+$/)) {
531
+ parent.append('<div class="item barstyle'+style+'" id="'+value+'">'+text+'</div>');
532
+
533
+ // loop style counter
534
+ style++; style %= max;
535
+ }
536
+ });
537
+ $(this).remove();
538
+ break;
539
+ }
540
+
541
+ // loop style counter
542
+ style++; style %= max;
543
+ });
544
+
545
+ // only start counting and sizing bars if we actually have usable data
546
+ if(data) {
547
+ // double loop so we can handle re-renderings of the form
548
+ $(this).find('.item').each(function() {
549
+ var name = $(this).attr('id');
550
+ var count = data[key][name];
551
+
552
+ if(count) { sum += count; }
553
+ });
554
+
555
+
556
+ $(this).find('.item').each(function() {
557
+ var name = $(this).attr('id');
558
+ var oldCount = $(this).attr('data-count');
559
+ var oldSum = $(this).attr('data-sum');
560
+ var count = data[key][name] || 0;
561
+
562
+ if(count != oldCount || sum != oldSum) {
563
+ var percent = (sum) ? ((count/sum)*100)+'%' : '0%';
564
+
565
+ $(this).attr('data-count', count);
566
+ $(this).attr('data-sum', sum);
567
+ $(this).animate({width: percent});
568
+ }
569
+ });
570
+ }
571
+
572
+ $(this).addClass('rendered');
573
+ });
574
+
575
+ });
576
+ }
577
+
384
578
  function connectControlChannel() {
385
579
  ws = new WebSocket('ws://' + location.host + '/control');
386
580
  ws.onopen = function() { connected(); };
data/views/index.erb CHANGED
@@ -16,7 +16,7 @@
16
16
  <% if @feedback then %>
17
17
  <div id="feedbackWrapper">
18
18
  <div id="feedbackSidebar">
19
- <img id="feedbackActivity" src="css/spinner.gif" />
19
+ <img id="feedbackActivity" src="<%= @asset_path %>/css/spinner.gif" />
20
20
  <h3>Live Interaction</h3>
21
21
  <div class="row">
22
22
  <h4>The presenter should...</h4>
@@ -41,12 +41,19 @@
41
41
  <textarea id="feedback"></textarea>
42
42
  <button id="sendFeedback">Send Feedback</button>
43
43
  </div>
44
- <% if @edit then %>
44
+
45
45
  <div class="row tools">
46
+ <button id="fileDownloads" onclick="window.open('/download');">File Downloads</button>
47
+ <% if @edit then %>
46
48
  <button id="editSlide">Edit Current Slide</button>
49
+ <% end %>
50
+ <hr />
51
+ </div>
52
+
53
+ <div id="disclaimer">
54
+ <p>Press <code>?</code> for help.</p>
55
+ <p>All features are anonymous</p>
47
56
  </div>
48
- <% end %>
49
- <div id="disclaimer">All features are anonymous</div>
50
57
  </div>
51
58
  <div id="feedbackHandle"></div>
52
59
  </div>
@@ -57,6 +64,7 @@
57
64
  <tr><td class="key">z, ?</td><td>toggle help (this)</td></tr>
58
65
  <tr><td class="key">space, &rarr;</td><td>next slide</td></tr>
59
66
  <tr><td class="key">shift-space, &larr;</td><td>previous slide</td></tr>
67
+ <tr><td class="key">b</td><td>blank screen</td></tr>
60
68
  <tr><td class="key">d</td><td>toggle debug mode</td></tr>
61
69
  <tr><td class="key">## &lt;ret&gt;</td><td>go to slide #</td></tr>
62
70
  <tr><td class="key">c, t</td><td>table of contents (vi)</td></tr>
@@ -82,7 +90,7 @@
82
90
  <span id="debugInfo"></span>
83
91
  <span id="notesInfo"></span>
84
92
  <span id="slideFilename"></span>
85
- <img id="disconnected" src="/css/disconnected.png" />
93
+ <img id="disconnected" src="<%= @asset_path %>/css/disconnected.png" />
86
94
  </div>
87
95
 
88
96
  <div id="slides" class="offscreen" <%= 'style="display:none;"' if @slides %>>
data/views/presenter.erb CHANGED
@@ -19,6 +19,7 @@
19
19
  <tr><td class="key">z, ?</td><td>toggle help (this)</td></tr>
20
20
  <tr><td class="key">space, &rarr;</td><td>next slide</td></tr>
21
21
  <tr><td class="key">shift-space, &larr;</td><td>previous slide</td></tr>
22
+ <tr><td class="key">b</td><td>blank screen</td></tr>
22
23
  <tr><td class="key">d</td><td>toggle debug mode</td></tr>
23
24
  <tr><td class="key">## &lt;ret&gt;</td><td>go to slide #</td></tr>
24
25
  <tr><td class="key">c, t</td><td>table of contents (vi)</td></tr>
@@ -68,14 +69,14 @@
68
69
  <div id="feedbackPace">
69
70
  <span id="paceSlow">Speed Up!</span>
70
71
  <span id="paceFast">Slow Down!</span>
71
- <img id="paceMarker" src="css/paceMarker.png" />
72
+ <img id="paceMarker" src="<%= @asset_path %>/css/paceMarker.png" />
72
73
  </div>
73
74
  <div id="slidemenu">
74
75
  <div id="navigation" class="menu"></div>
75
76
  </div>
76
77
  </div>
77
78
  <div id="preview" class="grid_8">
78
- <img id="disconnected" src="/css/disconnected-large.png" />
79
+ <img id="disconnected" src="<%= @asset_path %>/css/disconnected-large.png" />
79
80
  <div id="preso" class="zoomed">loading presentation...</div>
80
81
  </div>
81
82
  <div id="statusbar">
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: showoff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.8.1
4
+ version: 0.9.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott Chacon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-04-28 00:00:00.000000000 Z
11
+ date: 2014-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sinatra
@@ -81,7 +81,7 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: sinatra-websocket
84
+ name: redcarpet
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - '>='
@@ -95,21 +95,21 @@ dependencies:
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
- name: thin
98
+ name: nokogiri
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - ~>
101
+ - - '>='
102
102
  - !ruby/object:Gem::Version
103
- version: '1.3'
103
+ version: '0'
104
104
  type: :runtime
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - ~>
108
+ - - '>='
109
109
  - !ruby/object:Gem::Version
110
- version: '1.3'
110
+ version: '0'
111
111
  - !ruby/object:Gem::Dependency
112
- name: redcarpet
112
+ name: sinatra-websocket
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - '>='
@@ -123,19 +123,19 @@ dependencies:
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
125
  - !ruby/object:Gem::Dependency
126
- name: nokogiri
126
+ name: thin
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
- - - '>='
129
+ - - ~>
130
130
  - !ruby/object:Gem::Version
131
- version: '0'
131
+ version: '1.3'
132
132
  type: :runtime
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
- - - '>='
136
+ - - ~>
137
137
  - !ruby/object:Gem::Version
138
- version: '0'
138
+ version: '1.3'
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: mg
141
141
  requirement: !ruby/object:Gem::Requirement