showoff 0.9.8.1 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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