- commit
- 6d9fecab0424eebbf7271bde04efbf13dc1d1c62
- parent
- f901f3c2fe366154f47c1f129e1664eed093f9c2
- Author
- Tobias Bengfort <tobias.bengfort@posteo.de>
- Date
- 2020-10-18 07:44
Merge branch 'via'
Diffstat
20 files changed, 157 insertions, 479 deletions
diff --git a/.gitignore b/.gitignore
@@ -1,5 +1,3 @@ 1 1 node_modules2 -1 data3 -1 db.sqlite34 -1 static/style.css5 -1 static/voterunner.js-1 2 /style.css -1 3 /voterunner.js
diff --git a/Makefile b/Makefile
@@ -1,14 +1,10 @@1 -1 all: static/voterunner.js static/style.css-1 1 all: voterunner.js style.css 2 23 -1 static/voterunner.js: static/src/*.js4 -1 browserify static/src/voterunner.js -o $@-1 3 voterunner.js: src/*.js -1 4 browserify src/voterunner.js -o $@ 5 56 -1 static/style.css: static/scss/*.scss7 -1 sassc static/scss/style.scss $@-1 6 style.css: scss/*.scss -1 7 sassc scss/style.scss $@ 8 8 9 9 clean:10 -1 rm static/voterunner.js static/style.css11 -112 -1 .PHONY: server13 -1 server: all14 -1 export DATABASE_URL='sqlite3:db.sqlite3' && node app.js-1 10 rm voterunner.js style.css
diff --git a/Procfile b/Procfile
@@ -1 +0,0 @@1 -1 web: node app.js
diff --git a/README.md b/README.md
@@ -8,8 +8,7 @@ This app tries to allow for quick and dirty votes and discussions. It is 8 8 basically the core concept behind 9 9 [votorolla](http://zelea.com/project/votorola/home.xht) mixed with the 10 10 interface of [etherpad](http://etherpad.org/). Technically, it is a lot11 -1 of client side code with a bit of [socket.io](http://socket.io) magic on12 -1 the server.-1 11 of client side code using a [via](https://github.com/xi/via) server. 13 12 14 13 The voting mechanism 15 14 -------------------- @@ -60,25 +59,6 @@ the delegation. Now you compete directly with other ideas. Maybe you can 60 59 convince others, but maybe you should delegate your vote again in order 61 60 to agree on a compromise. 62 6163 -1 Install64 -1 -------65 -166 -1 Voterunner is a [node.js](http://nodejs.org/) app using67 -1 [PostgreSQL](http://www.postgresql.org/) as a database so the following68 -1 lines will bring it up:69 -170 -1 $ git clone https://github.com/xi/voterunner71 -1 $ cd voterunner72 -1 $ npm install73 -1 $ bin/manage_db.sh init74 -1 $ bin/manage_db.sh start75 -1 $ export DATABASE_URL="postgresql://:@localhost/voterunner"76 -1 $ node app.js77 -1 ... Listening on localhost:500078 -1 $ open http://localhost:5000/ # introduction79 -1 $ open http://localhost:5000/my-topic/ # discuss on a topic80 -181 -182 62 Development 83 63 ----------- 84 64
diff --git a/app.js b/app.js
@@ -1,204 +0,0 @@1 -1 /*2 -1 * voterunner - quick and dirty votes and discussions3 -1 *4 -1 * copyright: 2013 Tobias Bengfort <tobias.bengfort@gmx.net>5 -1 * license: AGPL-3+6 -1 * url: http://voterunner.herokuapp.com/7 -1 */8 -19 -1 var url = require('url');10 -1 var process = require('process');11 -112 -1 var express = require('express');13 -1 var http = require('http');14 -1 var app = express();15 -1 var server = http.Server(app);16 -1 var io = require('socket.io').listen(server);17 -1 var anyDB = require('any-db');18 -1 var fs = require('fs');19 -1 var log4js = require('log4js');20 -1 var MarkdownIt = require('markdown-it');21 -122 -1 var DATABASE_URL = process.env.DATABASE_URL;23 -1 var SQLITE = DATABASE_URL.match(/^sqlite/);24 -1 var PORT = process.env.PORT || 5000;25 -1 var HOST = process.env.HOST || 'localhost';26 -127 -1 var log = log4js.getLogger();28 -1 var md = new MarkdownIt();29 -130 -1 app.use(express.static('static'));31 -1 server.listen(PORT, HOST, function() {32 -1 log.info('Listening on ' + HOST + ':' + PORT);33 -1 });34 -135 -1 var db = anyDB.createPool(DATABASE_URL, {36 -1 max: SQLITE ? 1 : 10,37 -1 });38 -1 process.on('exit', (code) => {39 -1 db.close();40 -1 });41 -142 -1 var queryDB = function(sql, data) {43 -1 if (SQLITE) {44 -1 sql = sql.replace(/\$/g, '?');45 -1 }46 -147 -1 return new Promise(function(resolve, reject) {48 -1 var q = db.query(sql, data, function(err, res) {49 -1 if (err) {50 -1 reject(err);51 -1 } else {52 -1 resolve(res);53 -1 }54 -1 });55 -1 });56 -1 };57 -158 -1 // setup table59 -1 queryDB('CREATE TABLE IF NOT EXISTS nodes (topic TEXT, id TEXT, name TEXT, comment TEXT, delegate TEXT, UNIQUE (topic, id))');60 -161 -1 var escapeHTML = function(unsafe) {62 -1 return unsafe63 -1 .replace(/&/g, '&')64 -1 .replace(/</g, '<')65 -1 .replace(/>/g, '>')66 -1 .replace(/"/g, '"')67 -1 .replace(/'/g, ''');68 -1 };69 -170 -1 var tpl = function(file, data, res) {71 -1 fs.readFile('tpl/' + file, 'utf8', function(err, html) {72 -1 html = html.replace(/{{{ ([^}]*)\|markdown }}}/g, function(match, key) {73 -1 if (data.hasOwnProperty(key)) {74 -1 return md.render(data[key]);75 -1 } else {76 -1 return '';77 -1 }78 -1 });79 -180 -1 html = html.replace(/{{ ([^}]*)\|json }}/g, function(match, key) {81 -1 if (data.hasOwnProperty(key)) {82 -1 return '<div id="json-' + escapeHTML(key) + '" data-value="' + escapeHTML(JSON.stringify(data[key])) + '"></div>';83 -1 } else {84 -1 return '';85 -1 }86 -1 });87 -188 -1 html = html.replace(/{{ ([^}]*) }}/g, function(match, key) {89 -1 if (data.hasOwnProperty(key)) {90 -1 return escapeHTML(data[key].toString());91 -1 } else {92 -1 return '';93 -1 }94 -1 });95 -196 -1 res.send(html);97 -1 });98 -1 };99 -1100 -1101 -1 // welcome view102 -1 app.get('/', function(req, res) {103 -1 fs.readFile('README.md', 'utf8', function(err, markdown) {104 -1 tpl('markdown.html', {'markdown': markdown}, res);105 -1 });106 -1 });107 -1108 -1 // json state109 -1 app.get('/:topic.json', function(req, res) {110 -1 var topic = req.params.topic;111 -1 var sql = 'SELECT id, name, comment, delegate FROM nodes WHERE topic = $1';112 -1113 -1 queryDB(sql, [topic]).then(function(result) {114 -1 res.json(result.rows);115 -1 }).catch(function(err) {116 -1 res.status(500).send(err.toString());117 -1 });118 -1 });119 -1120 -1 // app view121 -1 app.get('/:topic/:id?', function(req, res) {122 -1 var topic = req.params.topic;123 -1124 -1 var sql = 'SELECT id, name, comment, delegate FROM nodes WHERE topic = $1';125 -1 queryDB(sql, [topic]).then(function(result) {126 -1 tpl('app.html', {'nodes': result.rows, 'topic': topic}, res);127 -1 }).catch(function(err) {128 -1 res.status(500).send(err.toString());129 -1 });130 -1 });131 -1132 -1 // socket.io133 -1 io.sockets.on('connection', function(socket) {134 -1 var topic;135 -1 var id;136 -1137 -1 var handleMsg = function(action, sql, v1, v2) {138 -1 // make sure that node exists, ignore error139 -1 return queryDB('INSERT INTO nodes (topic, id) VALUES ($1, $2)', [topic, id]).catch(function() {140 -1 return;141 -1 }).then(function() {142 -1 log.debug('Handeling:', action, topic, id, v1, v2);143 -1 io.to(topic).emit(action, id, v1, v2);144 -1145 -1 if (typeof(sql) === 'string') {146 -1 sql = [sql];147 -1 }148 -1149 -1 return Promise.all(sql.map(function(s) {150 -1 var params = [topic, id];151 -1 var n = s.match(/\$/g).length;152 -1 if (n >= 3) params.push(v1);153 -1 if (n >= 4) params.push(v2);154 -1155 -1 return queryDB(s, params);156 -1 }));157 -1 });158 -1 };159 -1160 -1 socket.on('register', function(_topic, _id) {161 -1 log.debug('Registration:', _topic, _id);162 -1163 -1 topic = _topic;164 -1 id = _id;165 -1 socket.join(topic, function(err) {166 -1 if (err) {167 -1 log.error(err);168 -1 }169 -1 });170 -1 });171 -1172 -1 socket.on('rmNode', function() {173 -1 var sql = [174 -1 'UPDATE nodes SET delegate = null WHERE topic = $1 AND delegate = $2',175 -1 'DELETE FROM nodes WHERE topic = $1 AND id = $2',176 -1 ];177 -1 handleMsg('rmNode', sql);178 -1 });179 -1 socket.on('setNodeName', function(name) {180 -1 var sql = 'UPDATE nodes SET name = $3 WHERE topic = $1 AND id = $2';181 -1 handleMsg('setNodeName', sql, name);182 -1 });183 -1 socket.on('setNodeComment', function(comment) {184 -1 var sql = 'UPDATE nodes SET comment = $3 WHERE topic = $1 AND id = $2';185 -1 handleMsg('setNodeComment', sql, comment);186 -1 });187 -1 socket.on('setDelegate', function(delegate) {188 -1 var sql = 'UPDATE nodes SET delegate = $3 WHERE topic = $1 AND id = $2';189 -1 handleMsg('setDelegate', sql, delegate);190 -1 });191 -1 socket.on('rmDelegate', function() {192 -1 var sql = 'UPDATE nodes SET delegate = null WHERE topic = $1 AND id = $2';193 -1 handleMsg('rmDelegate', sql);194 -1 });195 -1196 -1 socket.on('testClear', function(done) {197 -1 if (topic.substr(0, 4) === 'test') {198 -1 log.debug('Handeling:', 'testClear', topic);199 -1 queryDB("DELETE FROM nodes WHERE topic = $1", [topic]).then(done);200 -1 } else {201 -1 done();202 -1 }203 -1 });204 -1 });
diff --git a/bin/manage_db.sh b/bin/manage_db.sh
@@ -1,33 +0,0 @@1 -1 #!/bin/sh2 -13 -1 cd "$(dirname "$0")/.."4 -1 DB_DIR="$(pwd)/data/postgres"5 -1 mkdir -p $DB_DIR6 -17 -1 start() {8 -1 pg_ctl start -w -D "$DB_DIR" -o "-h localhost" -o "-k '$DB_DIR'"9 -1 }10 -111 -1 stop() {12 -1 pg_ctl stop -D "$DB_DIR"13 -1 }14 -115 -1 if [ "$1" = 'start' ]; then16 -1 start17 -1 elif [ "$1" = 'stop' ]; then18 -1 stop19 -1 elif [ "$1" = 'init' ]; then20 -1 if test ! -d "$DB_DIR/base"; then21 -1 pg_ctl initdb -D "$DB_DIR"22 -1 start23 -1 createdb -h "$DB_DIR" voterunner24 -1 stop25 -1 else26 -1 echo "skipping"27 -1 fi28 -1 elif [ "$1" = 'clean' ]; then29 -1 rm -r "$DB_DIR"30 -1 else31 -1 echo "invalid command"32 -1 exit 133 -1 fi
diff --git a/tpl/app.html b/index.html
@@ -2,7 +2,7 @@ 2 2 <html class="voterunner"> 3 3 <head> 4 4 <meta charset="utf-8">5 -1 <title>voterunner - {{ topic }}</title>-1 5 <title>voterunner</title> 6 6 <meta name="viewport" content="width=device-width" /> 7 7 <meta name="robots" content="noindex" /> 8 8 <link rel="shortcut icon" href="/favicon.ico"/> @@ -36,8 +36,6 @@ 36 36 <div id="tree"></div> 37 37 </div></div> 38 3839 -1 {{ nodes|json }}40 -141 39 <script type="text/javascript" src="/voterunner.js"></script> 42 40 </body> 43 41 </html>
diff --git a/package.json b/package.json
@@ -1,19 +1,9 @@ 1 1 { 2 2 "name": "voterunner", 3 3 "version": "0.0.1",4 -1 "dependencies": {5 -1 "any-db": "^2.2.0",6 -1 "any-db-postgres": "^2.1.5",7 -1 "any-db-sqlite3": "^2.1.4",8 -1 "express": "^4.16.2",9 -1 "log4js": "^0.6.38",10 -1 "markdown-it": "^7.0.1",11 -1 "socket.io": "^1.7.4"12 -1 },13 4 "devDependencies": {14 -1 "mfbs": "^3.1.1",15 -1 "preact": "^8.2.6",16 -1 "socket.io-client": "^1.7.4"-1 5 "preact": "^10.5.4", -1 6 "mfbs": "^4.0.0" 17 7 }, 18 8 "author": "Tobias Bengfort", 19 9 "license": "AGPL-3+"
diff --git a/static/scss/bar.scss b/scss/bar.scss
diff --git a/static/scss/base.scss b/scss/base.scss
diff --git a/static/scss/layout.scss b/scss/layout.scss
diff --git a/static/scss/style.scss b/scss/style.scss
@@ -7,9 +7,9 @@ $color-border: #c0c0c0; 7 7 8 8 $padding: 0.4em; 9 910 -1 @import '../../node_modules/mfbs/sass/variables';11 -1 @import '../../node_modules/mfbs/sass/base';12 -1 @import '../../node_modules/mfbs/sass/layout';-1 10 @import '../node_modules/mfbs/sass/variables'; -1 11 @import '../node_modules/mfbs/sass/base'; -1 12 @import '../node_modules/mfbs/sass/layout'; 13 13 14 14 @import 'base'; 15 15 @import 'layout';
diff --git a/static/scss/tree.scss b/scss/tree.scss
diff --git a/static/scss/user.scss b/scss/user.scss
diff --git a/static/src/template.js b/src/template.js
@@ -25,23 +25,19 @@ var getDelegationChain = function(nodes, node) {
25 25 return node.delegationChain;
26 26 };
27 27
28 -1 var getName = function(node) {
29 -1 return node.name || 'anonymous';
30 -1 };
31 -1
32 -1 var tplFollowers = function(nodes, id, ID) {
33 -1 return nodes
-1 28 var tplFollowers = function(state, id) {
-1 29 return state.nodes
34 30 .filter(n => n.delegate === id)
35 -1 .sort((a, b) => getVotes(nodes, b) - getVotes(nodes, a))
36 -1 .map(n => tplNode(nodes, n, ID));
-1 31 .sort((a, b) => getVotes(state.nodes, b) - getVotes(state.nodes, a))
-1 32 .map(n => tplNode(state, n));
37 33 };
38 34
39 -1 var tplNode = function(nodes, node, ID) {
-1 35 var tplNode = function(state, node) {
40 36 var classList = [];
41 37 if (node.expanded) {
42 38 classList.push('is-expanded');
43 39 }
44 -1 if (node.id === ID) {
-1 40 if (node.id === state.id) {
45 41 classList.push('node--self');
46 42 }
47 43
@@ -64,14 +60,16 @@ var tplNode = function(nodes, node, ID) {
64 60 }, node.expanded ? '\u25BC' : '\u25B6'),
65 61 h('button', {
66 62 className: 'node__delegate bar__item bar__item--button bar__item--right',
67 -1 title: 'delegate to ' + getName(node),
-1 63 title: 'delegate to ' + node.id,
68 64 disabled: (
69 -1 node.id === ID ||
70 -1 getDelegationChain(nodes, node).includes(ID)
-1 65 !navigator.onLine ||
-1 66 !state.id ||
-1 67 node.id === state.id ||
-1 68 getDelegationChain(state.nodes, node).includes(state.id)
71 69 ),
72 70 }, '\u2795'),
73 -1 h('div', {className: 'node__votes bar__item bar__item--right'}, '' + getVotes(nodes, node)),
74 -1 h('div', {className: 'node__name bar__item' + (!node.expanded && node.comment ? '' : ' bar__item--grow')}, getName(node)),
-1 71 h('div', {className: 'node__votes bar__item bar__item--right'}, '' + getVotes(state.nodes, node)),
-1 72 h('div', {className: 'node__name bar__item' + (!node.expanded && node.comment ? '' : ' bar__item--grow')}, node.id),
75 73 !node.expanded && node.comment && h('div', {className: 'node__preview bar__item bar__item--grow'}, node.comment.substr(0, 100)),
76 74 ]),
77 75 node.expanded && h('div', {className: 'node__comment'}, node.comment || ''),
@@ -79,19 +77,18 @@ var tplNode = function(nodes, node, ID) {
79 77 h('ul', {
80 78 className: 'tree',
81 79 role: 'group',
82 -1 }, tplFollowers(nodes, node.id, ID)),
-1 80 }, tplFollowers(state, node.id)),
83 81 ]);
84 82 };
85 83
86 -1 var template = function(nodes, ID) {
-1 84 var template = function(state) {
87 85 return h('ul', {
88 86 className: 'tree',
89 87 role: 'tree',
90 -1 }, tplFollowers(nodes, null, ID));
-1 88 }, tplFollowers(state, null));
91 89 };
92 90
93 91 module.exports = {
94 92 getVotes: getVotes,
95 -1 getName: getName,
96 93 template: template,
97 94 };
diff --git a/static/src/utils.js b/src/utils.js
@@ -33,14 +33,14 @@ var on = function(element, eventType, selector, fn) {
33 33 });
34 34 };
35 35
36 -1 var initVDom = function(wrapper, template, nodes, ID, afterRender) {
-1 36 var initVDom = function(wrapper, template, state, afterRender) {
37 37 wrapper.innerHTML = '';
38 -1 var tree = template(nodes, ID);
-1 38 var tree = template(state);
39 39 var element = preact.render(tree, wrapper);
40 40 afterRender();
41 41
42 42 return function(newState) {
43 -1 var newTree = template(newState, ID);
-1 43 var newTree = template(newState);
44 44 preact.render(newTree, wrapper, element);
45 45 afterRender();
46 46 };
@@ -51,19 +51,9 @@ var randomString = function() {
51 51 return Math.floor(a).toString(36);
52 52 };
53 53
54 -1 var setCookie = function(key, value, days) {
55 -1 localStorage[key] = value;
56 -1 };
57 -1
58 -1 var getCookie = function(key) {
59 -1 return localStorage[key];
60 -1 };
61 -1
62 54 module.exports = {
63 55 throttle: throttle,
64 56 on: on,
65 57 initVDom: initVDom,
66 58 randomString: randomString,
67 -1 setCookie: setCookie,
68 -1 getCookie: getCookie,
69 59 };
diff --git a/static/src/voterunner.js b/src/voterunner.js
@@ -1,34 +1,34 @@1 -1 var io = require('socket.io-client');2 1 var template = require('./template'); 3 2 var utils = require('./utils'); 4 3 5 4 document.addEventListener('DOMContentLoaded', function() {6 -1 var TOPIC = document.URL.split('/')[3];7 -1 var ID = document.URL.split('/')[4];8 -1 if (!ID) ID = utils.getCookie('id');9 -1 if (!ID) ID = utils.randomString();10 -1 utils.setCookie('id', ID, 100);11 -112 -1 var socket = io.connect('/');13 -1 window.socket = socket; // make available for tests14 -1 socket.emit('register', TOPIC, ID);15 -116 -1 var nodes = JSON.parse(document.querySelector('#json-nodes').dataset.value);-1 5 if (!location.hash) { -1 6 location.hash = utils.randomString(); -1 7 } -1 8 var topic = location.hash.substr(1); -1 9 var url = 'https://via.ce9e.org/hmsg/voterunner/' + topic; -1 10 document.title += ' - ' + topic; -1 11 -1 12 var state = { -1 13 nodes: [], -1 14 id: null, -1 15 dirty: false, -1 16 }; 17 17 18 18 var getNode = function(id) {19 -1 var node = nodes.find(n => n.id === id);-1 19 var node = state.nodes.find(n => n.id === id); 20 20 if (!node) { 21 21 node = { 22 22 id: id, 23 23 delegate: null, 24 24 };25 -1 nodes.push(node);-1 25 state.nodes.push(node); 26 26 } 27 27 return node; 28 28 }; 29 29 30 30 var invalidateVotes = function() {31 -1 nodes.forEach(function(node) {-1 31 state.nodes.forEach(function(node) { 32 32 node.votes = null; 33 33 node.delegationChain = null; 34 34 }); @@ -42,26 +42,25 @@ document.addEventListener('DOMContentLoaded', function() { 42 42 } 43 43 }; 44 4445 -1 var user = nodes.find(n => n.id === ID);46 -1 if (user) {47 -1 document.querySelector('.user__name input').value = user.name;48 -1 document.querySelector('.user__comment textarea').value = user.comment;49 -1 ensureVisible(user);50 -1 }-1 45 var update = utils.initVDom(document.querySelector('#tree'), template.template, state, function() { -1 46 var user = state.nodes.find(n => n.id === state.id); 51 4752 -1 var updateUser = function() {53 -1 document.querySelector('.user__votes').textContent = template.getVotes(nodes, user || {});-1 48 document.querySelector('.user__votes').textContent = template.getVotes(state.nodes, user || {}); 54 49 55 50 if (user && user.delegate) {56 -1 var delegatee = getNode(user.delegate);57 -1 document.querySelector('.user__delegation').textContent = 'delegated to: ' + template.getName(delegatee);-1 51 document.querySelector('.user__delegation').textContent = 'delegated to: ' + user.delegate; 58 52 } else { 59 53 document.querySelector('.user__delegation').textContent = '(no delegation)'; 60 54 }61 -1 };62 5563 -1 var update = utils.initVDom(document.querySelector('#tree'), template.template, nodes, ID, function() {64 -1 updateUser();-1 56 if (!state.dirty) { -1 57 document.querySelector('.user__comment textarea').value = user ? user.comment || '' : ''; -1 58 } -1 59 -1 60 var disabled = !state.id || !navigator.onLine; -1 61 document.querySelector('.user__rm').disabled = disabled; -1 62 document.querySelector('.user__undelegate').disabled = disabled; -1 63 document.querySelector('.user__comment textarea').disabled = disabled; 65 64 }); 66 65 67 66 utils.on(document, 'click', '.node__expand', function() { @@ -69,68 +68,101 @@ document.addEventListener('DOMContentLoaded', function() { 69 68 var id = nodeElement.id.substr(5); 70 69 var node = getNode(id); 71 70 node.expanded = !node.expanded;72 -1 update(nodes);-1 71 update(state); 73 72 }); 74 73 75 74 utils.on(document, 'click', '.node__delegate', function() { 76 75 var nodeElement = this.parentElement.parentElement.parentElement; 77 76 var id = nodeElement.id.substr(5);78 -1 socket.emit('setDelegate', id);-1 77 fetch(url, { -1 78 method: 'POST', -1 79 body: JSON.stringify(['setDelegate', state.id, id]), -1 80 }); 79 81 }); 80 82 81 83 utils.on(document, 'click', '.user__rm', function() { 82 84 if (confirm('Do you really want to delete this opinion?')) {83 -1 socket.emit('rmNode');84 -1 document.querySelector('.user__name input').value = '';-1 85 fetch(url, { -1 86 method: 'POST', -1 87 body: JSON.stringify(['rmNode', state.id]), -1 88 }); 85 89 document.querySelector('.user__comment textarea').value = ''; -1 90 state.dirty = false; 86 91 } 87 92 }); 88 9389 -1 utils.on(document, 'change', '.user__name input', function() {90 -1 socket.emit('setNodeName', this.value);-1 94 utils.on(document, 'input', '.user__name input', function() { -1 95 state.id = this.value; -1 96 state.dirty = false; -1 97 update(state); 91 98 }); 92 99 93 100 utils.on(document, 'click', '.user__undelegate', function() {94 -1 socket.emit('rmDelegate');-1 101 fetch(url, { -1 102 method: 'POST', -1 103 body: JSON.stringify(['rmDelegate', state.id]), -1 104 }); 95 105 }); 96 106 97 107 utils.on(document, 'input', '.user__comment textarea', utils.throttle(function() { 98 108 var comment = document.querySelector('.user__comment textarea').value;99 -1 var node = nodes.find(n => n.id === ID);-1 109 var node = state.nodes.find(n => n.id === state.id); 100 110 // Do not create a new node if the comment is empty. 101 111 // This can happen e.g. on a keydown event from the ctrl or shift keys. 102 112 if (node || comment) {103 -1 socket.emit('setNodeComment', comment);-1 113 fetch(url, { -1 114 method: 'POST', -1 115 body: JSON.stringify(['setNodeComment', state.id, comment]), -1 116 }); -1 117 state.dirty = true; 104 118 } 105 119 }, 1000)); 106 120107 -1 socket.on('rmNode', function(id) {108 -1 nodes = nodes.filter(function(node) {109 -1 if (node.delegate === id) {110 -1 node.delegate = null;111 -1 }112 -1 return node.id !== id;113 -1 });114 -1 invalidateVotes();115 -1 update(nodes);116 -1 });117 -1 socket.on('setNodeName', function(id, name) {118 -1 getNode(id).name = name;119 -1 update(nodes);120 -1 });121 -1 socket.on('setNodeComment', function(id, comment) {122 -1 getNode(id).comment = comment;123 -1 update(nodes);124 -1 });125 -1 socket.on('setDelegate', function(id, delegate) {126 -1 getNode(id).delegate = delegate;127 -1 invalidateVotes();128 -1 ensureVisible(user);129 -1 update(nodes);130 -1 });131 -1 socket.on('rmDelegate', function(id) {132 -1 getNode(id).delegate = null;133 -1 invalidateVotes();134 -1 update(nodes);135 -1 });-1 121 var evtSource = new EventSource(url); -1 122 evtSource.onmessage = function(event) { -1 123 var data = JSON.parse(event.data); -1 124 var name = data[0]; -1 125 var id = data[1]; -1 126 -1 127 if (name === 'setNodes') { -1 128 state.nodes = data[2]; -1 129 ensureVisible(state.nodes.find(n => n.id === state.id)); -1 130 } else if (!id) { -1 131 return; -1 132 } else if (name === 'rmNode') { -1 133 state.nodes = state.nodes.filter(function(node) { -1 134 if (node.delegate === id) { -1 135 node.delegate = null; -1 136 } -1 137 return node.id !== id; -1 138 }); -1 139 invalidateVotes(); -1 140 } else if (name === 'setNodeComment') { -1 141 getNode(id).comment = data[2]; -1 142 } else if (name === 'setDelegate') { -1 143 getNode(id).delegate = data[2]; -1 144 invalidateVotes(); -1 145 ensureVisible(state.nodes.find(n => n.id === state.id)); -1 146 } else if (name === 'rmDelegate') { -1 147 getNode(id).delegate = null; -1 148 invalidateVotes(); -1 149 } -1 150 update(state); -1 151 -1 152 if (Math.random() < 0.05) { -1 153 invalidateVotes(); -1 154 fetch(url, { -1 155 method: 'PUT', -1 156 body: JSON.stringify(['setNodes', null, state.nodes]), -1 157 headers: {'Last-Event-ID': event.lastEventId}, -1 158 }); -1 159 } -1 160 }; -1 161 -1 162 window.testClear = function(done) { -1 163 fetch(url, { -1 164 method: 'PUT', -1 165 body: JSON.stringify(['setNodes', null, []]), -1 166 }).then(done); -1 167 }; 136 168 });
diff --git a/static/test/index.html b/test/index.html
diff --git a/static/test/test.js b/test/test.js
@@ -7,29 +7,42 @@ var trigger = function(target, type) {
7 7 target.dispatchEvent(new Event(type, {bubbles: true}));
8 8 };
9 9
10 -1 var setUp = function(url, fn) {
-1 10 var setUpUser = function(browser, id) {
-1 11 var d = browser.contentDocument;
-1 12 var userName = d.querySelector('.user__name input');
-1 13 userName.value = id;
-1 14 trigger(userName, 'input');
-1 15 };
-1 16
-1 17 var setUp = function(topic, fn) {
11 18 var iframe = document.createElement('iframe');
12 -1 iframe.onload = function() {fn(iframe)};
13 -1 iframe.url = url;
14 -1 iframe.src = url;
-1 19 iframe.url = '/#test' + topic;
-1 20
-1 21 iframe.onload = function() {
-1 22 setUpUser(this, ID);
-1 23 fn(iframe);
-1 24 };
15 25
16 26 iframe.tearDown = function(done) {
17 -1 var self = this;
18 -1 self.contentWindow.socket.emit('testClear', function() {
19 -1 self.parentNode.removeChild(self);
-1 27 this.contentWindow.testClear(() => {
-1 28 this.parentNode.removeChild(this);
20 29 done();
21 30 });
22 31 };
23 32
24 33 iframe.reload = function(fn) {
25 34 this.onload = function() {
26 -1 this.onload = fn;
-1 35 this.onload = function() {
-1 36 setUpUser(this, ID);
-1 37 setTimeout(fn, TIMEOUT);
-1 38 };
27 39 this.src = this.url;
28 40 };
29 41 this.src = '';
30 42 };
31 43
32 44 document.getElementById('testarea').appendChild(iframe);
-1 45 iframe.src = iframe.url;
33 46 };
34 47
35 48 describe('load', function() {
@@ -37,7 +50,7 @@ describe('load', function() {
37 50 var browser;
38 51
39 52 before(function(done) {
40 -1 setUp('/test' + test + '/', function(b) {
-1 53 setUp(test, function(b) {
41 54 browser = b;
42 55 done();
43 56 });
@@ -56,59 +69,6 @@ describe('load', function() {
56 69 });
57 70 });
58 71
59 -1 describe('setName', function() {
60 -1 var test = 'setName';
61 -1 var name = 'testName';
62 -1 var browser;
63 -1 var d, userName, node, nodeName;
64 -1
65 -1 before(function(done) {
66 -1 setUp('/test' + test + '/' + ID, function(b) {
67 -1 browser = b;
68 -1 d = browser.contentDocument;
69 -1
70 -1 userName = d.querySelector('.user__name input');
71 -1 userName.value = name;
72 -1 trigger(userName, 'change');
73 -1
74 -1 setTimeout(done, TIMEOUT);
75 -1 });
76 -1 });
77 -1
78 -1 after(function(done) {
79 -1 browser.tearDown(done);
80 -1 });
81 -1
82 -1 it('should set user name', function() {
83 -1 expect(userName.value).to.equal(name);
84 -1 });
85 -1
86 -1 it('node sould exist', function() {
87 -1 node = d.getElementById('node-' + ID);
88 -1 expect(node).to.exist;
89 -1 });
90 -1
91 -1 it('should set node name', function() {
92 -1 node = d.getElementById('node-' + ID);
93 -1 nodeName = node.querySelector('.node__name').textContent;
94 -1 expect(nodeName).to.equal(name);
95 -1 });
96 -1
97 -1 it('should be permanent', function(done) {
98 -1 browser.reload(function() {
99 -1 d = browser.contentDocument;
100 -1 userName = d.querySelector('.user__name input').value;
101 -1 expect(userName).to.equal(name);
102 -1
103 -1 node = d.getElementById('node-' + ID);
104 -1 nodeName = node.querySelector('.node__name').textContent;
105 -1 expect(nodeName).to.equal(name);
106 -1
107 -1 done();
108 -1 });
109 -1 });
110 -1 });
111 -1
112 72 describe('setComment', function() {
113 73 var test = 'setComment';
114 74 var comment = 'testComment';
@@ -116,7 +76,7 @@ describe('setComment', function() {
116 76 var d, userComment, node, nodeComment, nodeExpand;
117 77
118 78 before(function(done) {
119 -1 setUp('/test' + test + '/' + ID, function(b) {
-1 79 setUp(test, function(b) {
120 80 browser = b;
121 81 d = browser.contentDocument;
122 82
@@ -177,18 +137,15 @@ describe('removeDelegate', function() {
177 137 describe('remove', function() {
178 138 var test = 'remove';
179 139 var browser;
180 -1 var d, userName, userComment, userRemove;
-1 140 var d, userComment, userRemove;
181 141
182 142 before(function(done) {
183 -1 setUp('/test' + test + '/' + ID, function(b) {
-1 143 setUp(test, function(b) {
184 144 browser = b;
185 145 d = browser.contentDocument;
186 146 browser.contentWindow.confirm = () => true;
187 147
188 148 // create something to delete
189 -1 userName = d.querySelector('.user__name input');
190 -1 userName.value = 'testName';
191 -1 trigger(userName, 'change');
192 149 userComment = d.querySelector('.user__comment textarea');
193 150 userComment.value = 'testComment';
194 151 trigger(userComment, 'input');
@@ -209,11 +166,6 @@ describe('remove', function() {
209 166 expect(node).to.not.exist;
210 167 });
211 168
212 -1 it('should clear user name', function() {
213 -1 userName = d.querySelector('.user__name input').value;
214 -1 expect(userName).to.equal('');
215 -1 });
216 -1
217 169 it('should clear user comment', function() {
218 170 userComment = d.querySelector('.user__comment textarea').value;
219 171 expect(userComment).to.equal('');
diff --git a/tpl/markdown.html b/tpl/markdown.html
@@ -1,17 +0,0 @@1 -1 <!DOCTYPE html>2 -1 <html xmlns="http://www.w3.org/1999/xhtml">3 -14 -1 <head>5 -1 <meta charset="utf-8">6 -1 <title>voterunner</title>7 -1 <meta name="viewport" content="width=device-width" />8 -1 <link rel="shortcut icon" href="/favicon.ico"/>9 -1 <link rel="stylesheet" type="text/css" href="/style.css" />10 -1 <meta name="description" content="quick and dirty votes and discussions" />11 -1 </head>12 -113 -1 <body>14 -1 {{{ markdown|markdown }}}15 -1 </body>16 -117 -1 </html>