voterunner

quick and dirty votes and discussions
git clone https://git.ce9e.org/voterunner.git

commit
bfeeea2de1e6c80f442cb81e7950160ff7e560b3
parent
51348885d0468bec263b1bf994e85641a78ec398
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2020-10-17 20:49
split into separate files

Diffstat

M Makefile 4 ++--
C static/src/voterunner.js -> static/src/template.js 196 ++-----------------------------------------------------------
A static/src/utils.js 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
M static/src/voterunner.js 187 +++++--------------------------------------------------------

4 files changed, 89 insertions, 367 deletions


diff --git a/Makefile b/Makefile

@@ -1,7 +1,7 @@
    1     1 all: static/voterunner.js static/style.css
    2     2 
    3    -1 static/voterunner.js: static/src/voterunner.js
    4    -1 	browserify $< -o $@
   -1     3 static/voterunner.js: static/src/*.js
   -1     4 	browserify static/src/voterunner.js -o $@
    5     5 
    6     6 static/style.css: static/scss/*.scss
    7     7 	sassc static/scss/style.scss $@

diff --git a/static/src/voterunner.js b/static/src/template.js

@@ -1,43 +1,8 @@
    1    -1 var preact = require('preact');
    2     1 var h = require('preact').h;
    3     2 var MarkdownIt = require('markdown-it');
    4    -1 var io = require('socket.io-client');
    5     3 
    6     4 var md = new MarkdownIt();
    7     5 
    8    -1 var throttle = function(fn, timeout) {
    9    -1 	var called, blocked;
   10    -1 
   11    -1 	var result = function() {
   12    -1 		if (blocked) {
   13    -1 			called = true;
   14    -1 		} else {
   15    -1 			fn();
   16    -1 			blocked = true;
   17    -1 			called = false;
   18    -1 
   19    -1 			setTimeout(function() {
   20    -1 				blocked = false;
   21    -1 
   22    -1 				if (called) {
   23    -1 					result();
   24    -1 				}
   25    -1 			}, timeout);
   26    -1 		}
   27    -1 	};
   28    -1 
   29    -1 	return result;
   30    -1 };
   31    -1 
   32    -1 var on = function(element, eventType, selector, fn) {
   33    -1 	element.addEventListener(eventType, function(event) {
   34    -1 		var target = event.target.closest(selector);
   35    -1 		if (target && element.contains(target)) {
   36    -1 			return fn.call(target, event);
   37    -1 		}
   38    -1 	});
   39    -1 };
   40    -1 
   41     6 var getVotes = function(nodes, node) {
   42     7 	if (!node.votes) {
   43     8 		node.votes = 1 + nodes
@@ -135,161 +100,8 @@ var template = function(nodes, ID) {
  135   100 	}, tplFollowers(nodes, null, ID));
  136   101 };
  137   102 
  138    -1 var initVDom = function(wrapper, nodes, ID, afterRender) {
  139    -1 	wrapper.innerHTML = '';
  140    -1 	var tree = template(nodes, ID);
  141    -1 	var element = preact.render(tree, wrapper);
  142    -1 	afterRender();
  143    -1 
  144    -1 	return function(newState) {
  145    -1 		var newTree = template(newState, ID);
  146    -1 		preact.render(newTree, wrapper, element);
  147    -1 		afterRender();
  148    -1 	};
  149    -1 };
  150    -1 
  151    -1 var uid = function() {
  152    -1 	// just enough uniqueness
  153    -1 	var a = Math.random() * Date.now() * 0x1000;
  154    -1 	return Math.floor(a).toString(36);
  155    -1 };
  156    -1 var setCookie = function(key, value, days) {
  157    -1 	localStorage[key] = value;
  158    -1 };
  159    -1 
  160    -1 var getCookie = function(key) {
  161    -1 	return localStorage[key];
   -1   103 module.exports = {
   -1   104 	getVotes: getVotes,
   -1   105 	getName: getName,
   -1   106 	template: template,
  162   107 };
  163    -1 
  164    -1 document.addEventListener('DOMContentLoaded', function() {
  165    -1 	var TOPIC = document.URL.split('/')[3];
  166    -1 	var ID = document.URL.split('/')[4];
  167    -1 	if (!ID) ID = getCookie('id');
  168    -1 	if (!ID) ID = uid();
  169    -1 	setCookie('id', ID, 100);
  170    -1 
  171    -1 	var socket = io.connect('/');
  172    -1 	window.socket = socket;  // make available for tests
  173    -1 	socket.emit('register', TOPIC, ID);
  174    -1 
  175    -1 	var nodes = JSON.parse(document.querySelector('#json-nodes').dataset.value);
  176    -1 
  177    -1 	var getNode = function(id) {
  178    -1 		var node = nodes.find(n => n.id === id);
  179    -1 		if (!node) {
  180    -1 			node = {
  181    -1 				id: id,
  182    -1 				delegate: null,
  183    -1 			};
  184    -1 			nodes.push(node);
  185    -1 		}
  186    -1 		return node;
  187    -1 	};
  188    -1 
  189    -1 	var invalidateVotes = function() {
  190    -1 		nodes.forEach(function(node) {
  191    -1 			node.votes = null;
  192    -1 			node.delegationChain = null;
  193    -1 		});
  194    -1 	};
  195    -1 
  196    -1 	var ensureVisible = function(node) {
  197    -1 		if (node && node.delegate) {
  198    -1 			var delegatee = getNode(node.delegate);
  199    -1 			delegatee.expanded = true;
  200    -1 			ensureVisible(delegatee);
  201    -1 		}
  202    -1 	};
  203    -1 
  204    -1 	var user = nodes.find(n => n.id === ID);
  205    -1 	if (user) {
  206    -1 		document.querySelector('.user__name input').value = user.name;
  207    -1 		document.querySelector('.user__comment textarea').value = user.comment;
  208    -1 		ensureVisible(user);
  209    -1 	}
  210    -1 
  211    -1 	var updateUser = function() {
  212    -1 		document.querySelector('.user__votes').textContent = getVotes(nodes, user || {});
  213    -1 
  214    -1 		if (user && user.delegate) {
  215    -1 			var delegatee = getNode(user.delegate);
  216    -1 			document.querySelector('.user__delegation').textContent = 'delegated to: ' + getName(delegatee);
  217    -1 		} else {
  218    -1 			document.querySelector('.user__delegation').textContent = '(no delegation)';
  219    -1 		}
  220    -1 	};
  221    -1 
  222    -1 	var update = initVDom(document.querySelector('#tree'), nodes, ID, function() {
  223    -1 		updateUser();
  224    -1 	});
  225    -1 
  226    -1 	on(document, 'click', '.node__expand', function() {
  227    -1 		var nodeElement = this.parentElement.parentElement.parentElement;
  228    -1 		var id = nodeElement.id.substr(5);
  229    -1 		var node = getNode(id);
  230    -1 		node.expanded = !node.expanded;
  231    -1 		update(nodes);
  232    -1 	});
  233    -1 
  234    -1 	on(document, 'click', '.node__delegate', function() {
  235    -1 		var nodeElement = this.parentElement.parentElement.parentElement;
  236    -1 		var id = nodeElement.id.substr(5);
  237    -1 		socket.emit('setDelegate', id);
  238    -1 	});
  239    -1 
  240    -1 	on(document, 'click', '.user__rm', function() {
  241    -1 		if (confirm('Do you really want to delete this opinion?')) {
  242    -1 			socket.emit('rmNode');
  243    -1 			document.querySelector('.user__name input').value = '';
  244    -1 			document.querySelector('.user__comment textarea').value = '';
  245    -1 		}
  246    -1 	});
  247    -1 
  248    -1 	on(document, 'change', '.user__name input', function() {
  249    -1 		socket.emit('setNodeName', this.value);
  250    -1 	});
  251    -1 
  252    -1 	on(document, 'click', '.user__undelegate', function() {
  253    -1 		socket.emit('rmDelegate');
  254    -1 	});
  255    -1 
  256    -1 	on(document, 'input', '.user__comment textarea', throttle(function() {
  257    -1 		var comment = document.querySelector('.user__comment textarea').value;
  258    -1 		var node = nodes.find(n => n.id === ID);
  259    -1 		// Do not create a new node if the comment is empty.
  260    -1 		// This can happen e.g. on a keydown event from the ctrl or shift keys.
  261    -1 		if (node || comment) {
  262    -1 			socket.emit('setNodeComment', comment);
  263    -1 		}
  264    -1 	}, 1000));
  265    -1 
  266    -1 	socket.on('rmNode', function(id) {
  267    -1 		nodes = nodes.filter(function(node) {
  268    -1 			if (node.delegate === id) {
  269    -1 				node.delegate = null;
  270    -1 			}
  271    -1 			return node.id !== id;
  272    -1 		});
  273    -1 		invalidateVotes();
  274    -1 		update(nodes);
  275    -1 	});
  276    -1 	socket.on('setNodeName', function(id, name) {
  277    -1 		getNode(id).name = name;
  278    -1 		update(nodes);
  279    -1 	});
  280    -1 	socket.on('setNodeComment', function(id, comment) {
  281    -1 		getNode(id).comment = comment;
  282    -1 		update(nodes);
  283    -1 	});
  284    -1 	socket.on('setDelegate', function(id, delegate) {
  285    -1 		getNode(id).delegate = delegate;
  286    -1 		invalidateVotes();
  287    -1 		ensureVisible(user);
  288    -1 		update(nodes);
  289    -1 	});
  290    -1 	socket.on('rmDelegate', function(id) {
  291    -1 		getNode(id).delegate = null;
  292    -1 		invalidateVotes();
  293    -1 		update(nodes);
  294    -1 	});
  295    -1 });

diff --git a/static/src/utils.js b/static/src/utils.js

@@ -0,0 +1,69 @@
   -1     1 var preact = require('preact');
   -1     2 
   -1     3 var throttle = function(fn, timeout) {
   -1     4 	var called, blocked;
   -1     5 
   -1     6 	var result = function() {
   -1     7 		if (blocked) {
   -1     8 			called = true;
   -1     9 		} else {
   -1    10 			fn();
   -1    11 			blocked = true;
   -1    12 			called = false;
   -1    13 
   -1    14 			setTimeout(function() {
   -1    15 				blocked = false;
   -1    16 
   -1    17 				if (called) {
   -1    18 					result();
   -1    19 				}
   -1    20 			}, timeout);
   -1    21 		}
   -1    22 	};
   -1    23 
   -1    24 	return result;
   -1    25 };
   -1    26 
   -1    27 var on = function(element, eventType, selector, fn) {
   -1    28 	element.addEventListener(eventType, function(event) {
   -1    29 		var target = event.target.closest(selector);
   -1    30 		if (target && element.contains(target)) {
   -1    31 			return fn.call(target, event);
   -1    32 		}
   -1    33 	});
   -1    34 };
   -1    35 
   -1    36 var initVDom = function(wrapper, template, nodes, ID, afterRender) {
   -1    37 	wrapper.innerHTML = '';
   -1    38 	var tree = template(nodes, ID);
   -1    39 	var element = preact.render(tree, wrapper);
   -1    40 	afterRender();
   -1    41 
   -1    42 	return function(newState) {
   -1    43 		var newTree = template(newState, ID);
   -1    44 		preact.render(newTree, wrapper, element);
   -1    45 		afterRender();
   -1    46 	};
   -1    47 };
   -1    48 
   -1    49 var randomString = function() {
   -1    50 	var a = Math.random() * Date.now() * 0x1000;
   -1    51 	return Math.floor(a).toString(36);
   -1    52 };
   -1    53 
   -1    54 var setCookie = function(key, value, days) {
   -1    55 	localStorage[key] = value;
   -1    56 };
   -1    57 
   -1    58 var getCookie = function(key) {
   -1    59 	return localStorage[key];
   -1    60 };
   -1    61 
   -1    62 module.exports = {
   -1    63 	throttle: throttle,
   -1    64 	on: on,
   -1    65 	initVDom: initVDom,
   -1    66 	randomString: randomString,
   -1    67 	setCookie: setCookie,
   -1    68 	getCookie: getCookie,
   -1    69 };

diff --git a/static/src/voterunner.js b/static/src/voterunner.js

@@ -1,172 +1,13 @@
    1    -1 var preact = require('preact');
    2    -1 var h = require('preact').h;
    3    -1 var MarkdownIt = require('markdown-it');
    4     1 var io = require('socket.io-client');
    5    -1 
    6    -1 var md = new MarkdownIt();
    7    -1 
    8    -1 var throttle = function(fn, timeout) {
    9    -1 	var called, blocked;
   10    -1 
   11    -1 	var result = function() {
   12    -1 		if (blocked) {
   13    -1 			called = true;
   14    -1 		} else {
   15    -1 			fn();
   16    -1 			blocked = true;
   17    -1 			called = false;
   18    -1 
   19    -1 			setTimeout(function() {
   20    -1 				blocked = false;
   21    -1 
   22    -1 				if (called) {
   23    -1 					result();
   24    -1 				}
   25    -1 			}, timeout);
   26    -1 		}
   27    -1 	};
   28    -1 
   29    -1 	return result;
   30    -1 };
   31    -1 
   32    -1 var on = function(element, eventType, selector, fn) {
   33    -1 	element.addEventListener(eventType, function(event) {
   34    -1 		var target = event.target.closest(selector);
   35    -1 		if (target && element.contains(target)) {
   36    -1 			return fn.call(target, event);
   37    -1 		}
   38    -1 	});
   39    -1 };
   40    -1 
   41    -1 var getVotes = function(nodes, node) {
   42    -1 	if (!node.votes) {
   43    -1 		node.votes = 1 + nodes
   44    -1 			.filter(n => n.delegate === node.id)
   45    -1 			.map(n => getVotes(nodes, n))
   46    -1 			.reduce((sum, n) => sum + n, 0);
   47    -1 	}
   48    -1 
   49    -1 	return node.votes;
   50    -1 };
   51    -1 
   52    -1 var getDelegationChain = function(nodes, node) {
   53    -1 	if (!node.delegationChain) {
   54    -1 		if (node.delegate) {
   55    -1 			var delegate = nodes.find(n => n.id === node.delegate);
   56    -1 			var delegationChain = getDelegationChain(nodes, delegate);
   57    -1 			node.delegationChain = [node.delegate].concat(delegationChain);
   58    -1 		} else {
   59    -1 			node.delegationChain = [];
   60    -1 		}
   61    -1 	}
   62    -1 
   63    -1 	return node.delegationChain;
   64    -1 };
   65    -1 
   66    -1 var getName = function(node) {
   67    -1 	return node.name || 'anonymous';
   68    -1 };
   69    -1 
   70    -1 var tplFollowers = function(nodes, id, ID) {
   71    -1 	return nodes
   72    -1 		.filter(n => n.delegate === id)
   73    -1 		.sort((a, b) => getVotes(nodes, b) - getVotes(nodes, a))
   74    -1 		.map(n => tplNode(nodes, n, ID));
   75    -1 };
   76    -1 
   77    -1 var tplNode = function(nodes, node, ID) {
   78    -1 	var classList = [];
   79    -1 	if (node.expanded) {
   80    -1 		classList.push('is-expanded');
   81    -1 	}
   82    -1 	if (node.id === ID) {
   83    -1 		classList.push('node--self');
   84    -1 	}
   85    -1 
   86    -1 	var delegateAttrs = {};
   87    -1 	if (node.id === ID || getDelegationChain(nodes, node).includes(ID)) {
   88    -1 		delegateAttrs.disabled = true;
   89    -1 	}
   90    -1 
   91    -1 	return h('li', {
   92    -1 		key: 'node-' + node.id,
   93    -1 		id: 'node-' + node.id,
   94    -1 		className: 'node ' + classList.join(' '),
   95    -1 		role: 'treeitem',
   96    -1 		'aria-expanded': '' + !!node.expanded,
   97    -1 	}, [
   98    -1 		h('article', {
   99    -1 			className: 'node__body',
  100    -1 		}, [
  101    -1 			h('header', {
  102    -1 				className: 'node__header bar',
  103    -1 			}, [
  104    -1 				h('button', {
  105    -1 					className: 'node__expand bar__item bar__item--button bar__item--left',
  106    -1 					title: node.expanded ? 'collapse' : 'expand',
  107    -1 				}, node.expanded ? '\u25BC' : '\u25B6'),
  108    -1 				h('button', {
  109    -1 					className: 'node__delegate bar__item bar__item--button bar__item--right',
  110    -1 					title: 'delegate to ' + getName(node),
  111    -1 					attributes: delegateAttrs,
  112    -1 				}, '\u2795'),
  113    -1 				h('div', {className: 'node__votes bar__item bar__item--right'}, '' + getVotes(nodes, node)),
  114    -1 				h('div', {className: 'node__name bar__item' + (!node.expanded && node.comment ? '' : ' bar__item--grow')}, getName(node)),
  115    -1 				!node.expanded && node.comment && h('div', {className: 'node__preview bar__item bar__item--grow'}, node.comment.substr(0, 100)),
  116    -1 			]),
  117    -1 			node.expanded && h('div', {
  118    -1 				className: 'node__comment',
  119    -1 				dangerouslySetInnerHTML: {
  120    -1 					__html: md.render(node.comment || ''),
  121    -1 				},
  122    -1 			}),
  123    -1 		]),
  124    -1 		h('ul', {
  125    -1 			className: 'tree',
  126    -1 			role: 'group',
  127    -1 		}, tplFollowers(nodes, node.id, ID)),
  128    -1 	]);
  129    -1 };
  130    -1 
  131    -1 var template = function(nodes, ID) {
  132    -1 	return h('ul', {
  133    -1 		className: 'tree',
  134    -1 		role: 'tree',
  135    -1 	}, tplFollowers(nodes, null, ID));
  136    -1 };
  137    -1 
  138    -1 var initVDom = function(wrapper, nodes, ID, afterRender) {
  139    -1 	wrapper.innerHTML = '';
  140    -1 	var tree = template(nodes, ID);
  141    -1 	var element = preact.render(tree, wrapper);
  142    -1 	afterRender();
  143    -1 
  144    -1 	return function(newState) {
  145    -1 		var newTree = template(newState, ID);
  146    -1 		preact.render(newTree, wrapper, element);
  147    -1 		afterRender();
  148    -1 	};
  149    -1 };
  150    -1 
  151    -1 var uid = function() {
  152    -1 	// just enough uniqueness
  153    -1 	var a = Math.random() * Date.now() * 0x1000;
  154    -1 	return Math.floor(a).toString(36);
  155    -1 };
  156    -1 var setCookie = function(key, value, days) {
  157    -1 	localStorage[key] = value;
  158    -1 };
  159    -1 
  160    -1 var getCookie = function(key) {
  161    -1 	return localStorage[key];
  162    -1 };
   -1     2 var template = require('./template');
   -1     3 var utils = require('./utils');
  163     4 
  164     5 document.addEventListener('DOMContentLoaded', function() {
  165     6 	var TOPIC = document.URL.split('/')[3];
  166     7 	var ID = document.URL.split('/')[4];
  167    -1 	if (!ID) ID = getCookie('id');
  168    -1 	if (!ID) ID = uid();
  169    -1 	setCookie('id', ID, 100);
   -1     8 	if (!ID) ID = utils.getCookie('id');
   -1     9 	if (!ID) ID = utils.randomString();
   -1    10 	utils.setCookie('id', ID, 100);
  170    11 
  171    12 	var socket = io.connect('/');
  172    13 	window.socket = socket;  // make available for tests
@@ -209,21 +50,21 @@ document.addEventListener('DOMContentLoaded', function() {
  209    50 	}
  210    51 
  211    52 	var updateUser = function() {
  212    -1 		document.querySelector('.user__votes').textContent = getVotes(nodes, user || {});
   -1    53 		document.querySelector('.user__votes').textContent = template.getVotes(nodes, user || {});
  213    54 
  214    55 		if (user && user.delegate) {
  215    56 			var delegatee = getNode(user.delegate);
  216    -1 			document.querySelector('.user__delegation').textContent = 'delegated to: ' + getName(delegatee);
   -1    57 			document.querySelector('.user__delegation').textContent = 'delegated to: ' + template.getName(delegatee);
  217    58 		} else {
  218    59 			document.querySelector('.user__delegation').textContent = '(no delegation)';
  219    60 		}
  220    61 	};
  221    62 
  222    -1 	var update = initVDom(document.querySelector('#tree'), nodes, ID, function() {
   -1    63 	var update = utils.initVDom(document.querySelector('#tree'), template.template, nodes, ID, function() {
  223    64 		updateUser();
  224    65 	});
  225    66 
  226    -1 	on(document, 'click', '.node__expand', function() {
   -1    67 	utils.on(document, 'click', '.node__expand', function() {
  227    68 		var nodeElement = this.parentElement.parentElement.parentElement;
  228    69 		var id = nodeElement.id.substr(5);
  229    70 		var node = getNode(id);
@@ -231,13 +72,13 @@ document.addEventListener('DOMContentLoaded', function() {
  231    72 		update(nodes);
  232    73 	});
  233    74 
  234    -1 	on(document, 'click', '.node__delegate', function() {
   -1    75 	utils.on(document, 'click', '.node__delegate', function() {
  235    76 		var nodeElement = this.parentElement.parentElement.parentElement;
  236    77 		var id = nodeElement.id.substr(5);
  237    78 		socket.emit('setDelegate', id);
  238    79 	});
  239    80 
  240    -1 	on(document, 'click', '.user__rm', function() {
   -1    81 	utils.on(document, 'click', '.user__rm', function() {
  241    82 		if (confirm('Do you really want to delete this opinion?')) {
  242    83 			socket.emit('rmNode');
  243    84 			document.querySelector('.user__name input').value = '';
@@ -245,15 +86,15 @@ document.addEventListener('DOMContentLoaded', function() {
  245    86 		}
  246    87 	});
  247    88 
  248    -1 	on(document, 'change', '.user__name input', function() {
   -1    89 	utils.on(document, 'change', '.user__name input', function() {
  249    90 		socket.emit('setNodeName', this.value);
  250    91 	});
  251    92 
  252    -1 	on(document, 'click', '.user__undelegate', function() {
   -1    93 	utils.on(document, 'click', '.user__undelegate', function() {
  253    94 		socket.emit('rmDelegate');
  254    95 	});
  255    96 
  256    -1 	on(document, 'input', '.user__comment textarea', throttle(function() {
   -1    97 	utils.on(document, 'input', '.user__comment textarea', utils.throttle(function() {
  257    98 		var comment = document.querySelector('.user__comment textarea').value;
  258    99 		var node = nodes.find(n => n.id === ID);
  259   100 		// Do not create a new node if the comment is empty.