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 +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
|