This changeset applies new styles to the poll. it also uses the new asynchronous wrappers around the localStorage api. this is necessary because we're migrating to a storage system that will use an async api. The changes to the poll just happened to coincide with the async stuff. My apologies to anyone who wants to read this whole thing
619 lines
20 KiB
JavaScript
619 lines
20 KiB
JavaScript
define([
|
|
'/api/config?cb=' + Math.random().toString(16).substring(2),
|
|
'/customize/messages.js',
|
|
'/poll/table.js',
|
|
'/poll/wizard.js',
|
|
'/bower_components/textpatcher/TextPatcher.js',
|
|
'/bower_components/chainpad-listmap/chainpad-listmap.js',
|
|
'/bower_components/chainpad-crypto/crypto.js',
|
|
'/common/cryptpad-common.js',
|
|
'/common/visible.js',
|
|
'/common/notify.js',
|
|
'/bower_components/jquery/dist/jquery.min.js',
|
|
'/customize/pad.js'
|
|
], function (Config, Messages, Table, Wizard, TextPatcher, Listmap, Crypto, Cryptpad, Visible, Notify) {
|
|
var $ = window.jQuery;
|
|
|
|
Cryptpad.styleAlerts();
|
|
console.log("Initializing your realtime session...");
|
|
|
|
/* TODO
|
|
* set range of dates/times
|
|
* (pair of date pickers)
|
|
* hide options within that range
|
|
* show hidden options
|
|
* add notes to a particular time slot
|
|
|
|
* check or uncheck options for a particular user
|
|
* mark preference level? (+1, 0, -1)
|
|
|
|
* delete/hide columns/rows
|
|
|
|
// let users choose what they want the default input to be...
|
|
|
|
* date
|
|
- http://foxrunsoftware.github.io/DatePicker/ ?
|
|
* ???
|
|
*/
|
|
|
|
var secret = Cryptpad.getSecrets();
|
|
|
|
var module = window.APP = {};
|
|
|
|
module.Wizard = Wizard;
|
|
|
|
// special UI elements
|
|
var $title = $('#title').attr('placeholder', Messages.dateTitleHint || 'title');
|
|
var $location = $('#location').attr('placeholder', Messages.dateLocationHint || 'location');
|
|
var $description = $('#description').attr('placeholder', Messages.dateDescription || 'description');
|
|
|
|
var items = [$title, $location, $description];
|
|
|
|
var Uid = function (prefix, f) {
|
|
f = f || function () {
|
|
return Number(Math.random() * Number.MAX_SAFE_INTEGER)
|
|
.toString(32).replace(/\./g, '');
|
|
};
|
|
return function () { return prefix + '-' + f(); };
|
|
};
|
|
|
|
var xy = function (x, y) { return x + '_' + y; };
|
|
var parseXY = function (id) {
|
|
var p = id.split('_');
|
|
return {
|
|
x: p[0],
|
|
y: p[1],
|
|
};
|
|
};
|
|
|
|
var find = function (map, path) {
|
|
return (map && path.reduce(function (p, n) {
|
|
return typeof p[n] !== 'undefined' && p[n];
|
|
}, map)) || undefined;
|
|
};
|
|
|
|
var Input = function (opt) { return $('<input>', opt); };
|
|
var Checkbox = function (id) {
|
|
var p = parseXY(id);
|
|
|
|
var proxy = module.rt.proxy;
|
|
|
|
var $div = $('<div>', {
|
|
'class': 'checkbox-contain',
|
|
});
|
|
|
|
var $label = $('<label>', {
|
|
'for': id,
|
|
});
|
|
|
|
var $check = Input({
|
|
id: id,
|
|
name: id,
|
|
type:'checkbox',
|
|
}).on('change', function () {
|
|
console.log("(%s, %s) => %s", p.x, p.y, $check[0].checked);
|
|
var checked = proxy.table.cells[id] = $check[0].checked? 1: 0;
|
|
if (checked) {
|
|
$label.addClass('yes');
|
|
}
|
|
else {
|
|
$label.removeClass('yes');
|
|
}
|
|
});
|
|
|
|
$div.append($check);
|
|
$check.after($label);
|
|
|
|
return $div; //$check;
|
|
};
|
|
var Text = function () { return Input({type:'text'}); };
|
|
|
|
var table = module.table = Table($('#table'), xy);
|
|
|
|
var setEditable = function (bool) {
|
|
module.isEditable = bool;
|
|
$('input, textarea').attr('disabled', !bool);
|
|
};
|
|
|
|
var coluid = Uid('x');
|
|
var rowuid = Uid('y');
|
|
|
|
var addIfAbsent = function (A, e) {
|
|
if (A.indexOf(e) !== -1) { return; }
|
|
A.push(e);
|
|
};
|
|
|
|
var removeRow = function (proxy, uid) {
|
|
// remove proxy.table.rows[uid]
|
|
|
|
proxy.table.rows[uid] = undefined;
|
|
delete proxy.table.rows[uid];
|
|
|
|
// remove proxy.table.rowsOrder
|
|
|
|
var order = proxy.table.rowsOrder;
|
|
order.splice(order.indexOf(uid), 1);
|
|
|
|
// remove all cells including uid
|
|
// proxy.table.cells
|
|
Object.keys(proxy.table.cells).forEach(function (cellUid) {
|
|
if (cellUid.indexOf(uid) === -1) { return; }
|
|
proxy.table.cells[cellUid] = undefined;
|
|
delete proxy.table.cells[cellUid];
|
|
});
|
|
|
|
// remove elements from DOM
|
|
table.removeRow(uid);
|
|
};
|
|
|
|
var removeColumn = function (proxy, uid) {
|
|
// remove proxy.table.cols[uid]
|
|
proxy.table.cols[uid] = undefined;
|
|
delete proxy.table.rows[uid];
|
|
|
|
// remove proxy.table.colsOrder
|
|
var order = proxy.table.colsOrder;
|
|
order.splice(order.indexOf(uid), 1);
|
|
|
|
// remove all cells including uid
|
|
Object.keys(proxy.table.cells).forEach(function (cellUid) {
|
|
if (cellUid.indexOf(uid) === -1) { return; }
|
|
proxy.table.cells[cellUid] = undefined;
|
|
delete proxy.table.cells[cellUid];
|
|
});
|
|
|
|
// remove elements from DOM
|
|
table.removeColumn(uid);
|
|
};
|
|
|
|
var removeFromArray = function (A, e) {
|
|
var i = A.indexOf(e);
|
|
if (i === -1) { return; }
|
|
A.splice(i, 1);
|
|
};
|
|
|
|
var makeUser = function (proxy, id, value) {
|
|
var $user = Input({
|
|
id: id,
|
|
type: 'text',
|
|
placeholder: 'your name',
|
|
}).on('keyup change', function () {
|
|
proxy.table.cols[id] = $user.val() || "";
|
|
});
|
|
|
|
var $wrapper = $('<div>', {
|
|
'class': 'text-cell',
|
|
})
|
|
.append($user)
|
|
.append($('<span>', {
|
|
'class': 'remove',
|
|
'title': 'remove column', // TODO translate
|
|
}).text('✖').click(function () {
|
|
removeColumn(proxy, id);
|
|
table.removeColumn(id);
|
|
}));
|
|
|
|
proxy.table.cols[id] = value || "";
|
|
addIfAbsent(proxy.table.colsOrder, id);
|
|
table.addColumn($wrapper, Checkbox, id);
|
|
return $user;
|
|
};
|
|
|
|
var scrollDown = module.scrollDown = function (px) {
|
|
var top = $(window).scrollTop() + px + 'px';
|
|
$('html, body').animate({
|
|
scrollTop: top,
|
|
}, {
|
|
duration: 200,
|
|
easing: 'swing',
|
|
});
|
|
};
|
|
|
|
var makeOption = function (proxy, id, value) {
|
|
var $option = Input({
|
|
type: 'text',
|
|
placeholder: 'option',
|
|
id: id,
|
|
}).on('keyup change', function () {
|
|
proxy.table.rows[id] = $option.val();
|
|
});
|
|
|
|
var $wrapper = $('<div>', {
|
|
'class': 'text-cell',
|
|
})
|
|
.append($option)
|
|
.append($('<span>', {
|
|
'class': 'remove',
|
|
'title': 'remove row', // TODO translate
|
|
}).text('✖').click(function () {
|
|
removeRow(proxy, id);
|
|
table.removeRow(id);
|
|
}));
|
|
|
|
proxy.table.rows[id] = value || "";
|
|
addIfAbsent(proxy.table.rowsOrder, id);
|
|
|
|
var $row = table.addRow($wrapper, Checkbox, id);
|
|
scrollDown($row.height());
|
|
|
|
return $option;
|
|
};
|
|
|
|
$('#adduser').click(function () {
|
|
if (!module.isEditable) { return; }
|
|
var id = coluid();
|
|
makeUser(module.rt.proxy, id).focus();
|
|
});
|
|
|
|
$('#addoption').click(function () {
|
|
if (!module.isEditable) { return; }
|
|
var id = rowuid();
|
|
makeOption(module.rt.proxy, id).focus();
|
|
});
|
|
|
|
Wizard.$getOptions.click(function () {
|
|
Cryptpad.confirm("Are you really ready to add these options to your poll?", function (yes) {
|
|
if (!yes) { return; }
|
|
var options = Wizard.computeSlots(function (a, b) {
|
|
return a + ' ('+ b + ')';
|
|
});
|
|
|
|
var proxy = module.rt.proxy;
|
|
|
|
options.forEach(function (text) {
|
|
var id = rowuid();
|
|
makeOption(proxy, id, text).val(text);
|
|
});
|
|
console.log(options);
|
|
});
|
|
});
|
|
|
|
// notifications
|
|
var unnotify = function () {
|
|
if (!(module.tabNotification &&
|
|
typeof(module.tabNotification.cancel) === 'function')) { return; }
|
|
module.tabNotification.cancel();
|
|
};
|
|
|
|
var notify = function () {
|
|
if (!(Visible.isSupported() && !Visible.currently())) { return; }
|
|
unnotify();
|
|
module.tabNotification = Notify.tab(document.title, 1000, 10);
|
|
};
|
|
|
|
// don't make changes until the interface is ready
|
|
setEditable(false);
|
|
|
|
var ready = function (info) {
|
|
console.log("Your realtime object is ready");
|
|
|
|
var proxy = module.rt.proxy;
|
|
|
|
var First = false;
|
|
|
|
// ensure that proxy.info and proxy.table exist
|
|
['info', 'table'].forEach(function (k) {
|
|
if (typeof(proxy[k]) === 'undefined') {
|
|
// you seem to be the first person to have visited this pad...
|
|
First = true;
|
|
proxy[k] = {};
|
|
}
|
|
});
|
|
|
|
// table{cols,rows,cells}
|
|
['cols', 'rows', 'cells'].forEach(function (k) {
|
|
if (typeof(proxy.table[k]) === 'undefined') { proxy.table[k] = {}; }
|
|
});
|
|
|
|
// table{rowsOrder,colsOrder}
|
|
['rows', 'cols'].forEach(function (k) {
|
|
var K = k + 'Order';
|
|
|
|
if (typeof(proxy.table[K]) === 'undefined') {
|
|
console.log("Creating %s", K);
|
|
proxy.table[K] = [];
|
|
|
|
Object.keys(proxy.table[k]).forEach(function (uid) {
|
|
addIfAbsent(proxy.table[K], uid);
|
|
});
|
|
}
|
|
});
|
|
|
|
// cols
|
|
proxy.table.colsOrder.forEach(function (uid) {
|
|
var val = proxy.table.cols[uid];
|
|
makeUser(proxy, uid, val).val(val);
|
|
});
|
|
|
|
// rows
|
|
proxy.table.rowsOrder.forEach(function (uid) {
|
|
var val = proxy.table.rows[uid];
|
|
makeOption(proxy, uid, val).val(val);
|
|
});
|
|
|
|
// cells
|
|
Object.keys(proxy.table.cells).forEach(function (uid) {
|
|
//var p = parseXY(uid);
|
|
var box = document.getElementById(uid);
|
|
if (!box) {
|
|
console.log("Couldn't find an element with uid [%s]", uid);
|
|
return;
|
|
}
|
|
var checked = box.checked = proxy.table.cells[uid] ? true : false;
|
|
if (checked) {
|
|
$(box).parent().find('label').addClass('yes');
|
|
}
|
|
});
|
|
|
|
items.forEach(function ($item) {
|
|
var id = $item.attr('id');
|
|
|
|
$item.on('change keyup', function () {
|
|
var val = $item.val();
|
|
proxy.info[id] = val;
|
|
});
|
|
|
|
if (typeof(proxy.info[id]) !== 'undefined') {
|
|
$item.val(proxy.info[id]);
|
|
}
|
|
});
|
|
|
|
// listen for visibility changes
|
|
if (Visible.isSupported()) {
|
|
Visible.onChange(function (yes) {
|
|
if (yes) { unnotify(); }
|
|
});
|
|
}
|
|
|
|
proxy
|
|
.on('change', [], function () {
|
|
notify();
|
|
})
|
|
.on('change', ['info'], function (o, n, p) {
|
|
var $target = $('#' + p[1]);
|
|
var el = $target[0];
|
|
var selects;
|
|
var op;
|
|
|
|
if (el && ['textarea', 'text'].indexOf(el.type) !== -1) {
|
|
op = TextPatcher.diff(o, n);
|
|
selects = ['selectionStart', 'selectionEnd'].map(function (attr) {
|
|
var before = el[attr];
|
|
var after = TextPatcher.transformCursor(el[attr], op);
|
|
return after;
|
|
});
|
|
$target.val(n);
|
|
|
|
if (op) {
|
|
el.selectionStart = selects[0];
|
|
el.selectionEnd = selects[1];
|
|
}
|
|
}
|
|
|
|
console.log("change: (%s, %s, [%s])", o, n, p.join(', '));
|
|
})
|
|
.on('change', ['table'], function (o, n, p) {
|
|
var id = p[p.length -1];
|
|
var type = p[1];
|
|
|
|
if (typeof(o) === 'undefined' &&
|
|
['cols', 'rows', 'cells'].indexOf(type) !== -1) {
|
|
switch (type) {
|
|
case 'cols':
|
|
makeUser(proxy, id, n);
|
|
break;
|
|
case 'rows':
|
|
makeOption(proxy, id, n);
|
|
break;
|
|
case 'cells':
|
|
//
|
|
break;
|
|
default:
|
|
console.log("Unhandled table element creation");
|
|
break;
|
|
}
|
|
}
|
|
|
|
var el = document.getElementById(id);
|
|
if (!el) {
|
|
console.log("Couldn't find the element you wanted!");
|
|
return;
|
|
}
|
|
|
|
switch (p[1]) {
|
|
case 'cols':
|
|
console.log("[Table.cols change] %s (%s => %s)@[%s]", id, o, n, p.slice(0, -1).join(', '));
|
|
el.value = n;
|
|
break;
|
|
case 'rows':
|
|
console.log("[Table.rows change] %s (%s => %s)@[%s]", id, o, n, p.slice(0, -1).join(', '));
|
|
el.value = n;
|
|
break;
|
|
case 'cells':
|
|
console.log("[Table.cell change] %s (%s => %s)@[%s]", id, o, n, p.slice(0, -1).join(', '));
|
|
var checked = el.checked = proxy.table.cells[id] ? true: false;
|
|
|
|
var $parent = $(el).parent();
|
|
|
|
if (!$parent.length) { console.log("couldn't find parent element of checkbox"); return; }
|
|
|
|
if (checked) {
|
|
$parent.find('label').addClass('yes');
|
|
//$(el).parent().
|
|
} else {
|
|
$parent.find('label').removeClass('yes');
|
|
}
|
|
break;
|
|
default:
|
|
console.log("[Table change] (%s => %s)@[%s]", o, n, p.join(', '));
|
|
break;
|
|
}
|
|
})
|
|
.on('remove', [], function (o, p, root) {
|
|
//console.log("remove: (%s, [%s])", o, p.join(', '));
|
|
//console.log(p, o, p.length);
|
|
|
|
switch (p[1]) {
|
|
case 'cols':
|
|
console.log("[Table.cols removal] [%s]", p[2]);
|
|
table.removeColumn(p[2]);
|
|
return false;
|
|
case 'rows':
|
|
console.log("[Table.rows removal] [%s]", p[2]);
|
|
table.removeRow(p[2]);
|
|
return false;
|
|
case 'rowsOrder':
|
|
Object.keys(proxy.table.rows)
|
|
.forEach(function (rowId) {
|
|
if (proxy.table.rowsOrder.indexOf(rowId) === -1) {
|
|
proxy.table.rows[rowId] = undefined;
|
|
delete proxy.table.rows[rowId];
|
|
}
|
|
});
|
|
break;
|
|
case 'colsOrder':
|
|
Object.keys(proxy.table.cols)
|
|
.forEach(function (colId) {
|
|
if (proxy.table.colsOrder.indexOf(colId) === -1) {
|
|
proxy.table.cols[colId] = undefined;
|
|
delete proxy.table.cols[colId];
|
|
}
|
|
|
|
});
|
|
break;
|
|
case 'cells':
|
|
// cool story bro
|
|
break;
|
|
default:
|
|
console.log("[Table removal] [%s]", p.join(', '));
|
|
break;
|
|
}
|
|
|
|
})
|
|
.on('disconnect', function (info) {
|
|
setEditable(false);
|
|
});
|
|
|
|
Cryptpad.getPadTitle(function (err, title) {
|
|
title = document.title = title || window.location.hash.slice(1, 9);
|
|
|
|
Cryptpad.rememberPad(title, function (err, data) {
|
|
if (err) {
|
|
console.log("unable to remember pad");
|
|
console.log(err);
|
|
return;
|
|
}
|
|
});
|
|
});
|
|
|
|
var $toolbar = $('#toolbar');
|
|
|
|
$toolbar.find('sub a').text('⇐ back to Cryptpad');
|
|
|
|
var Button = function (opt) {
|
|
return $('<button>', opt);
|
|
};
|
|
|
|
var suggestName = function () {
|
|
var hash = window.location.hash.slice(1, 9);
|
|
if (document.title === hash) {
|
|
return $title.val() || hash;
|
|
}
|
|
return document.title || $title.val() || hash;
|
|
};
|
|
|
|
$toolbar.append(Button({
|
|
id: 'forget',
|
|
'class': 'forget button action',
|
|
title: Messages.forgetButtonTitle,
|
|
}).text(Messages.forgetButton).click(function () {
|
|
var href = window.location.href;
|
|
Cryptpad.confirm(Messages.forgetPrompt, function (yes) {
|
|
if (!yes) { return; }
|
|
Cryptpad.forgetPad(href, function (err, data) {
|
|
if (err) {
|
|
console.log("unable to forget pad");
|
|
console.error(err);
|
|
return;
|
|
}
|
|
document.title = window.location.hash.slice(1, 9);
|
|
});
|
|
});
|
|
}));
|
|
|
|
$toolbar.append(Button({
|
|
id: 'rename',
|
|
'class': 'rename button action',
|
|
title: Messages.renameButtonTitle,
|
|
}).text(Messages.renameButton).click(function () {
|
|
var suggestion = suggestName();
|
|
Cryptpad.prompt(Messages.renamePrompt,
|
|
suggestion, function (title, ev) {
|
|
if (title === null) { return; }
|
|
|
|
Cryptpad.causesNamingConflict(title, function (err, conflicts) {
|
|
if (conflicts) {
|
|
Cryptpad.alert(Messages.renameConflict);
|
|
return;
|
|
}
|
|
Cryptpad.setPadTitle(title, function (err, data) {
|
|
if (err) {
|
|
console.log("unable to set pad title");
|
|
console.error(err);
|
|
return;
|
|
}
|
|
document.title = title;
|
|
});
|
|
});
|
|
});
|
|
}));
|
|
|
|
$toolbar.append(Button({
|
|
id: 'wizard',
|
|
'class': 'wizard button action',
|
|
title: 'wizard!',
|
|
}).text('WIZARD').click(function () {
|
|
Wizard.show();
|
|
if (Wizard.hasBeenDisplayed) { return; }
|
|
Cryptpad.log("click the button in the top left to return to your poll");
|
|
Wizard.hasBeenDisplayed = true;
|
|
}));
|
|
|
|
setEditable(true);
|
|
|
|
if (First) {
|
|
// assume the first user to the poll wants to be the administrator...
|
|
// TODO prompt them with questions to set up their poll...
|
|
}
|
|
/* TODO
|
|
even if the user isn't the first, check their storage to see if
|
|
they've visited before and if they 'own' a column in the table.
|
|
if they do, make it editable and suggest that they fill it in.
|
|
|
|
if they have not visited, make a column for them.
|
|
don't propogate changes from this column until they make changes
|
|
*/
|
|
};
|
|
|
|
var config = {
|
|
websocketURL: Config.websocketURL,
|
|
channel: secret.channel,
|
|
data: {},
|
|
crypto: Crypto.createEncryptor(secret.key),
|
|
};
|
|
|
|
var rt = module.rt = Listmap.create(config);
|
|
rt.proxy.on('create', function (info) {
|
|
var realtime = module.realtime = info.realtime;
|
|
window.location.hash = info.channel + secret.key;
|
|
module.patchText = TextPatcher.create({
|
|
realtime: realtime,
|
|
logging: true,
|
|
});
|
|
}).on('ready', ready)
|
|
.on('disconnect', function () {
|
|
setEditable(false);
|
|
Cryptpad.alert("Network connection lost!");
|
|
});
|
|
});
|