├── screenshot.png ├── poll ├── api │ ├── poll.db │ └── index.php ├── index.php ├── script.js ├── poll.css └── poll.js ├── screenshot_result.png ├── LICENSE └── README.md /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschildgen/reveal.js-poll-plugin/HEAD/screenshot.png -------------------------------------------------------------------------------- /poll/api/poll.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschildgen/reveal.js-poll-plugin/HEAD/poll/api/poll.db -------------------------------------------------------------------------------- /screenshot_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschildgen/reveal.js-poll-plugin/HEAD/screenshot_result.png -------------------------------------------------------------------------------- /poll/index.php: -------------------------------------------------------------------------------- 1 | 2 | Quiz 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Johannes Schildgen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /poll/script.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | get_poll(); 3 | }) 4 | 5 | COLORS = ["success", "danger", "warning", "primary"]; 6 | 7 | current_qid = null; 8 | 9 | function get_poll() { 10 | $.get( "./api/?method=get_poll", function( data ) { 11 | if(!('question' in data)) { // no quiz active 12 | $('#question').html('- currently no poll running -'); 13 | $('#answers').hide(); 14 | } else if(current_qid == data.qid) { // current question 15 | return; 16 | } else { // new question 17 | current_qid = data.qid; 18 | $('#question').html(data.question); 19 | $('#answers').html(''); 20 | for(i in data.answers) { 21 | $('#answers').append( 22 | ' '); 25 | } 26 | $('#answers').show(); 27 | window.navigator.vibrate(200); 28 | } 29 | }); 30 | } 31 | 32 | function respond(aid) { 33 | $('#answers > button').prop('disabled', true); 34 | $('#answers > button')[aid].innerHTML = '→ '+$('#answers > button')[aid].innerHTML+' ←'; 35 | $.get( "./api/?method=respond&aid="+aid, function( data ) { }); 36 | } 37 | 38 | window.setInterval(function(){ 39 | get_poll(); 40 | }, 3000); 41 | -------------------------------------------------------------------------------- /poll/poll.css: -------------------------------------------------------------------------------- 1 | .poll { 2 | display: none; 3 | position: absolute; 4 | right:-160px; 5 | bottom:-151px; 6 | width: 10cm; 7 | background-clip: padding-box; 8 | border: 1px solid rgba(0,0,0,.2); 9 | border-radius: 6px; 10 | outline: 0; 11 | box-shadow: 0 3px 9px rgba(0,0,0,.5); 12 | background-color: #fff; 13 | } 14 | 15 | .poll > h1 { 16 | min-height: 16.43px; 17 | padding: 15px; 18 | border-bottom: 1px solid #e5e5e5; 19 | font-size: 24px; 20 | margin-top: 0px; 21 | margin-bottom: 0px; 22 | } 23 | 24 | .poll > ul { 25 | display: block; 26 | padding: 15px; 27 | margin: 0; 28 | } 29 | 30 | .poll > ul > li { 31 | display: inline-block; 32 | font-size: 18px; 33 | text-align: center; 34 | color:#fff !important; 35 | text-decoration: none !important; 36 | padding:14px 60px; 37 | line-height:1; 38 | overflow: hidden; 39 | position:relative; 40 | width: -webkit-fill-available; 41 | cursor: pointer; 42 | 43 | box-shadow:0 1px 1px #ccc; 44 | border-radius: 6px; 45 | 46 | background-color: #aaa; 47 | background-image:-webkit-linear-gradient(top, #aaa, #aaa); 48 | background-image:-moz-linear-gradient(top, #aaa, #aaa); 49 | background-image:linear-gradient(top, #aaa, #aaa); 50 | } 51 | 52 | .poll > ul > li > .poll-percentage { 53 | display: block; 54 | height: 100%; 55 | border-radius: 2px; 56 | bottom: 0; 57 | left: 0; 58 | position: absolute; 59 | z-index: 1; 60 | transition: width 0.5s, height 0.5s; 61 | width: 100%; 62 | } 63 | 64 | .poll > ul > li:nth-child(4n+1) > .poll-percentage { 65 | background-color: #5cb85c; 66 | } 67 | .poll > ul > li:nth-child(4n+2) > .poll-percentage { 68 | background-color: #d9534f; 69 | } 70 | .poll > ul > li:nth-child(4n+3) > .poll-percentage { 71 | background-color: #f0ad4e; 72 | } 73 | .poll > ul > li:nth-child(4n+4) > .poll-percentage { 74 | background-color: #428bca; 75 | } 76 | 77 | .poll > ul > li > .poll-answer-text { 78 | z-index: 2; 79 | position:relative; 80 | } 81 | 82 | .poll > h2 { 83 | padding: 6px; 84 | border-top: 1px solid #e5e5e5; 85 | font-size:18px; 86 | margin-bottom: 1px; 87 | } 88 | 89 | .poll > .poll-responses { 90 | position: absolute; 91 | top: 3px; 92 | right: 8px; 93 | font-size: 18px; 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reveal.js-poll-plugin 2 | Plugin for embedding polls / quizzes into reveal.js presentations 3 | 4 | ## General Information 5 | 6 | The poll plugin consists of three parts: 7 | 8 | 1. reveal.js plugin to show a poll and the result within the slides 9 | 1. mobile website for end users to participate in the poll 10 | 1. API server and SQLite database to manage the poll data 11 | 12 | When a slide is shown that has a poll, the audience can open the mobile website and vote. When the presenter clicks on one of the answers, the poll is finished and the result is shown on the slides as a bar chart. All the time, when there is at least one response, the number of participations is shown on the top right corner of the poll. 13 | 14 | ![Screenshot](screenshot.png) 15 | 16 | ![Screenshot](screenshot_result.png) 17 | 18 | ## Files 19 | 20 | - index.php Mobile web site for end users to participate in the poll 21 | - script.js Javascript logic for the mobile web site 22 | - poll.css Stylesheet for reveal.js 23 | - poll.js reveal.js Plugin 24 | - api/index.php PHP script that handles API requests (start/stop poll, respond, ...) 25 | - api/poll.db SQLite database that stores the questions, answers and responses 26 | 27 | ## Installation and Usage 28 | 29 | ### Server 30 | 31 | The web server that hosts the API and the database requires PHP and php-sqlite3. The same server can be used to host the mobile website. 32 | 33 | 1. Move the content of the `poll` folder to the root directory of the server, let's call it http://example.com for now 34 | 1. Make the folder `api` and the database file `api/poll.db` writable (777) 35 | 1. Test the API with http://example.com/api, it should show "No method" 36 | 1. Test the mobile website with http://example.com, it should show "currently no poll running" 37 | 1. Test the API again: http://example.com/api/?method=start_poll&data={"question":"Test","answers":["X","Y","Z"],"correct_answers":["B"]}, it should show `{"OK":true}` 38 | 1. Test the mobile website again: http://example.com, It should show the question "Test" and three answers. 39 | 40 | ### Presentation Client 41 | 42 | The presentation client needs a PHP-enabled web server to forward API requests to the server. It's also possible that the presentation client is the same machine as the web server. 43 | 44 | 1. Move the content of the `poll` folder to the reveal.js presentation directory 45 | 1. Write the URL to the server into poll.js 46 | 1. Include the CSS in your slides: `` 47 | 1. Add the jQuery library (download from https://jquery.com, unzip, load: ``) 48 | 1. Initialize reveal.js with the following dependency: `{ src: 'poll/poll.js', async: true }` 49 | 1. Add polls to your slides: 50 | 51 | ``` 52 |
53 |

What is the question?

54 | 58 |

59 |
60 | ``` 61 | 62 | Use the `style="bottom:..., right:..."` to move the poll to the correct position. 63 | -------------------------------------------------------------------------------- /poll/poll.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Poll Plugin 3 | * 4 | * By Johannes Schildgen, 2021 5 | * https://github.com/jschildgen/reveal.js-poll-plugin 6 | * 7 | */ 8 | x= null; 9 | var Poll = (function(){ 10 | 11 | var refresh_interval = null; 12 | var current_poll = null; 13 | var url = "https://fraage.de"; 14 | 15 | function show_status() { 16 | $.get( url+"/api/?method=status", function( res ) { 17 | if(!('count' in res)) { return; } // no active poll 18 | $(current_poll).find("> .poll-responses").html(res.count == 0 ? "" : res.count); 19 | }); 20 | } 21 | 22 | function start_poll() { 23 | var question = $(current_poll).children("h1").text(); 24 | 25 | var answers = []; 26 | $(current_poll).find("ul > li > .poll-answer-text").each(function(i) { 27 | answers.push(this.innerHTML); 28 | }); 29 | 30 | var correct_answers = []; 31 | $(current_poll).find("ul > li[data-poll='correct'] > .poll-answer-text").each(function(i) { 32 | correct_answers.push(this.innerHTML); 33 | }); 34 | 35 | data = { "question" : question, "answers": answers, "correct_answers": correct_answers }; 36 | 37 | $.get( url+"/api/?method=start_poll&data="+encodeURIComponent(JSON.stringify(data)), function( res ) { }); 38 | refresh_interval = window.setInterval(show_status, 1000); 39 | } 40 | 41 | function stop_poll() { 42 | if(current_poll == null) { return; } 43 | clearInterval(refresh_interval); 44 | $(current_poll).find("ul > li > .poll-percentage").css("width","0%"); 45 | $.get( url+"/api/?method=stop_poll", function( res ) { 46 | var total = 0; 47 | for(i in res.answers) { 48 | total += res.answers[i]; 49 | } 50 | $(current_poll).find("ul > li > .poll-percentage").each(function(i) { 51 | percentage = (""+i in res.answers) ? 100*res.answers[i]/total : 0; 52 | $(this).css("width",percentage+"%"); 53 | }) 54 | 55 | $(current_poll).find("ul > li[data-poll='correct'] > .poll-answer-text").css("font-weight", "bold"); 56 | $(current_poll).find("ul > li[data-poll='correct'] > .poll-answer-text").each(function(i) { $(this).html("→ "+$(this).html()+" ←")}); 57 | current_poll = null; 58 | }); 59 | } 60 | 61 | Reveal.addEventListener( 'fragmentshown', function( event ) { 62 | if(!$(event.fragment).hasClass("poll")) { return; } 63 | current_poll = event.fragment; 64 | start_poll(); 65 | } ); 66 | 67 | 68 | return { 69 | init: function() { 70 | if(window.location.search.match( /print-pdf/gi )) { 71 | /* don't show poll in print view */ 72 | return; 73 | } 74 | 75 | $(".poll > ul > li").not(":has(>span)").click(function() { 76 | stop_poll(); 77 | }); 78 | 79 | $(".poll > h2").html(url); 80 | $(".poll > ul > li").not(":has(>span)").each(function(i) { 81 | this.innerHTML = '' 82 | +''+this.innerHTML+''; 83 | }); 84 | 85 | $(".poll").not(":has(>.poll-responses)").each(function(i) { 86 | $(this).append(''); 87 | }); 88 | 89 | $(".poll").show(); 90 | } 91 | } 92 | 93 | })(); 94 | 95 | Reveal.registerPlugin( 'poll', Poll ); -------------------------------------------------------------------------------- /poll/api/index.php: -------------------------------------------------------------------------------- 1 | setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 8 | // automatically deactivate questions after 15 minutes 9 | $DB->exec("UPDATE questions SET active = 0 WHERE active = 1 AND (julianday(CURRENT_TIMESTAMP)-julianday(qtime))*24*60 > 15"); 10 | 11 | function cleanup_db() { 12 | global $DB; 13 | $DB->exec("DROP TABLE IF EXISTS questions"); 14 | $DB->exec("DROP TABLE IF EXISTS answers"); 15 | $DB->exec("DROP TABLE IF EXISTS responses"); 16 | } 17 | 18 | function init_db() { 19 | global $DB; 20 | $DB->exec("CREATE TABLE IF NOT EXISTS questions (qid INTEGER PRIMARY KEY AUTOINCREMENT, qtime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, question VARCHAR(2000), active BOOLEAN DEFAULT 1)"); 21 | $DB->exec("CREATE TABLE IF NOT EXISTS answers (qid INTEGER, aid INTEGER, answer VARCHAR(2000), correct BOOLEAN, PRIMARY KEY(qid, aid))"); 22 | $DB->exec("CREATE TABLE IF NOT EXISTS responses (rid INTEGER PRIMARY KEY AUTOINCREMENT, qid INTEGER, aid INTEGER, rtime TIMESTAMP DEFAULT CURRENT_TIMESTAMP)"); 23 | } 24 | 25 | function auth($token) { 26 | return array("OK" => true); 27 | } 28 | 29 | function start_poll($question, $answers, $correct_answers) { 30 | global $DB; 31 | stop_poll(); 32 | $stmt = $DB->prepare("INSERT INTO questions (question) VALUES (?)"); 33 | $stmt->execute(array($question)); 34 | $qid = $DB->lastInsertId(); 35 | 36 | $stmt = $DB->prepare("INSERT INTO answers (qid, aid, answer, correct) VALUES (?, ?, ?, ?)"); 37 | 38 | $aid=0; 39 | foreach($answers as $answer) { 40 | $stmt->execute(array($qid, $aid, $answer, in_array($aid, $correct_answers))); 41 | $aid++; 42 | } 43 | return array("OK" => true); 44 | } 45 | 46 | function stop_poll() { 47 | global $DB; 48 | $stmt = $DB->prepare("SELECT r.aid, COUNT(*) as cnt FROM questions q JOIN responses r ON q.qid=r.qid WHERE q.active = 1 GROUP BY r.aid"); 49 | $stmt->execute(array()); 50 | $res = array("answers"=>array()); 51 | while ($row = $stmt->fetch()) { 52 | $res["answers"][$row["aid"]] = intval($row["cnt"]); 53 | } 54 | $DB->exec("UPDATE questions SET active = 0 WHERE active = 1"); 55 | return $res; 56 | } 57 | 58 | function status() { 59 | global $DB; 60 | $stmt = $DB->prepare("SELECT 1 FROM questions q WHERE q.active = 1"); 61 | $stmt->execute(array()); 62 | if (!($row = $stmt->fetch())) { 63 | return (object)array(); 64 | } 65 | 66 | $stmt = $DB->prepare("SELECT COUNT(*) as cnt FROM questions q JOIN responses r ON q.qid=r.qid WHERE q.active = 1"); 67 | $stmt->execute(array()); 68 | $res = array("count"=>0); 69 | while ($row = $stmt->fetch()) { 70 | $res["count"] = intval($row["cnt"]); 71 | } 72 | return $res; 73 | } 74 | 75 | function get_poll() { 76 | global $DB; 77 | $stmt = $DB->prepare("SELECT q.qid, q.question, a.aid, a.answer FROM questions q JOIN answers a ON q.qid=a.qid WHERE q.active = 1 ORDER BY q.qid, a.aid"); 78 | $stmt->execute(array()); 79 | 80 | $res = array("question" => "", "answers" => array()); 81 | $found = false; 82 | while ($row = $stmt->fetch()) { 83 | $found = true; 84 | $res["qid"] = $row["qid"]; 85 | $res["question"] = $row["question"]; 86 | $res["answers"][$row["aid"]] = $row["answer"]; 87 | } 88 | if(!$found) { return (object)array(); } 89 | return $res; 90 | } 91 | 92 | function respond($aid) { 93 | global $DB; 94 | 95 | $stmt = $DB->prepare("select q.qid FROM questions q JOIN answers a ON q.qid=a.qid WHERE q.active=1 AND a.aid=?"); 96 | $stmt->execute(array($aid)); 97 | if ($row = $stmt->fetch()) { 98 | $qid = $row["qid"]; 99 | } else { 100 | return array("OK" => false); 101 | } 102 | 103 | if(@$_SESSION['qid'] == $qid) { return array("OK" => false); } /* already voted for that question */ 104 | 105 | $_SESSION['qid'] = $qid; 106 | 107 | $stmt = $DB->prepare("INSERT INTO responses (qid, aid) VALUES (?, ?)"); 108 | $stmt->execute(array($qid, $aid)); 109 | return array("OK" => true); 110 | } 111 | 112 | //cleanup_db(); 113 | init_db(); 114 | //start_poll("What is 5+5?", array("5","10", "15","25"), array(1)); 115 | //stop_poll(); 116 | //print_r(get_poll()); 117 | //respond(2); 118 | 119 | if(!isset($_GET["method"])) { die("No method"); } 120 | $data = isset($_GET["data"]) ? json_decode($_GET["data"]) : array(); 121 | 122 | switch($_GET["method"]) { 123 | case "auth": echo json_encode(auth(@$_GET["token"])); break; 124 | case "start_poll": echo json_encode(start_poll(@$data->question, @$data->answers, $data->correct_answers)); break; 125 | case "stop_poll": echo json_encode(stop_poll()); break; 126 | case "get_poll": echo json_encode(get_poll()); break; 127 | case "respond": echo json_encode(respond(@$_GET["aid"])); break; 128 | case "status": echo json_encode(status()); break; 129 | } --------------------------------------------------------------------------------