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 +4 -4
- data/README.rdoc +3 -3
- data/lib/showoff/version.rb +1 -1
- data/lib/showoff.rb +220 -6
- data/public/css/presenter.css +7 -0
- data/public/css/showoff.css +94 -16
- data/public/js/presenter.js +13 -4
- data/public/js/showoff.js +202 -8
- data/views/index.erb +13 -5
- data/views/presenter.erb +3 -2
- metadata +14 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 43fa218d1a12936c933c64b8819f1f6c65812b71
|
4
|
+
data.tar.gz: b6a65b1480ca48095b43930af822a951daf0a9d6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 071852ca2bc0e2a8c1ebc36b73fab308c7b23f89905b25a5fca3a41010bda34f613312c3f1d60969d7772b64dce0d143cd4dc8c21191dc502fb86e9ca69733ff
|
7
|
+
data.tar.gz: ee1903e661ee16456371ac02717b59d6c8a0bb3524cf03abe696fab532c26dac9c31d2403d37bb7315246921ba3adb49e13eba6cf91b118d7f7c336226df08fe
|
data/README.rdoc
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
=
|
1
|
+
= Showoff Presentation Software
|
2
2
|
|
3
|
-
|
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,
|
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
|
data/lib/showoff/version.rb
CHANGED
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::
|
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
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
1017
|
-
|
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
|
data/public/css/presenter.css
CHANGED
@@ -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
|
}
|
data/public/css/showoff.css
CHANGED
@@ -61,16 +61,19 @@
|
|
61
61
|
}
|
62
62
|
|
63
63
|
/* plain (non-bullet) text */
|
64
|
-
.content > p
|
65
|
-
|
64
|
+
.content > p,
|
65
|
+
.content > form > p {
|
66
|
+
font-size: 2em;
|
66
67
|
margin: 1em;
|
67
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
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
|
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
|
**********************************/
|
data/public/js/presenter.js
CHANGED
@@ -357,11 +357,20 @@ function postSlide()
|
|
357
357
|
var notes = getCurrentNotes()
|
358
358
|
}
|
359
359
|
*/
|
360
|
-
|
361
|
-
|
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
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
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
|
-
|
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, →</td><td>next slide</td></tr>
|
59
66
|
<tr><td class="key">shift-space, ←</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">## <ret></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="
|
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, →</td><td>next slide</td></tr>
|
21
21
|
<tr><td class="key">shift-space, ←</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">## <ret></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="
|
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.
|
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-
|
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:
|
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:
|
98
|
+
name: nokogiri
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
|
-
- -
|
101
|
+
- - '>='
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: '
|
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: '
|
110
|
+
version: '0'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
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:
|
126
|
+
name: thin
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
128
128
|
requirements:
|
129
|
-
- -
|
129
|
+
- - ~>
|
130
130
|
- !ruby/object:Gem::Version
|
131
|
-
version: '
|
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: '
|
138
|
+
version: '1.3'
|
139
139
|
- !ruby/object:Gem::Dependency
|
140
140
|
name: mg
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|