diff --git a/customize.dist/fonts/cptools/fonts/cptools.svg b/customize.dist/fonts/cptools/fonts/cptools.svg
index 776b66ba3..08d0c5040 100644
--- a/customize.dist/fonts/cptools/fonts/cptools.svg
+++ b/customize.dist/fonts/cptools/fonts/cptools.svg
@@ -3,7 +3,7 @@
\ No newline at end of file
diff --git a/customize.dist/fonts/cptools/fonts/cptools.ttf b/customize.dist/fonts/cptools/fonts/cptools.ttf
index cfc9d1e3c..88ab53d94 100644
Binary files a/customize.dist/fonts/cptools/fonts/cptools.ttf and b/customize.dist/fonts/cptools/fonts/cptools.ttf differ
diff --git a/customize.dist/fonts/cptools/fonts/cptools.woff b/customize.dist/fonts/cptools/fonts/cptools.woff
index 80432d647..320f803e6 100644
Binary files a/customize.dist/fonts/cptools/fonts/cptools.woff and b/customize.dist/fonts/cptools/fonts/cptools.woff differ
diff --git a/customize.dist/fonts/cptools/style.css b/customize.dist/fonts/cptools/style.css
index 7fad69146..9333fa1e7 100644
--- a/customize.dist/fonts/cptools/style.css
+++ b/customize.dist/fonts/cptools/style.css
@@ -1,9 +1,9 @@
@font-face {
font-family: 'cptools';
src:
- url('fonts/cptools.ttf?n9y2kz') format('truetype'),
- url('fonts/cptools.woff?n9y2kz') format('woff'),
- url('fonts/cptools.svg?n9y2kz#cptools') format('svg');
+ url('fonts/cptools.ttf?am461j') format('truetype'),
+ url('fonts/cptools.woff?am461j') format('woff'),
+ url('fonts/cptools.svg?am461j#cptools') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
@@ -25,11 +25,35 @@
-moz-osx-font-smoothing: grayscale;
}
-.cptools-sheet:before {
- content: "\e908";
+.cptools-form-list-check:before {
+ content: "\e916";
}
-.cptools-slide:before {
- content: "\e907";
+.cptools-form-grid-check:before {
+ content: "\e917";
+}
+.cptools-form-poll:before {
+ content: "\e910";
+}
+.cptools-form-grid-radio:before {
+ content: "\e918";
+}
+.cptools-form-list-radio:before {
+ content: "\e919";
+}
+.cptools-form-page-break:before {
+ content: "\e91a";
+}
+.cptools-form-paragraph:before {
+ content: "\e91b";
+}
+.cptools-form-text:before {
+ content: "\e91c";
+}
+.cptools-form-list-ordered:before {
+ content: "\e91d";
+}
+.cptools-folder-no-color:before {
+ content: "\e900";
}
.cptools-whiteboard:before {
content: "\e901";
@@ -37,6 +61,9 @@
.cptools-new-template:before {
content: "\e902";
}
+.cptools-shared-folder:before {
+ content: "\e903";
+}
.cptools-file-upload:before {
content: "\e904";
}
@@ -46,9 +73,24 @@
.cptools-poll:before {
content: "\e906";
}
+.cptools-slide:before {
+ content: "\e907";
+}
+.cptools-sheet:before {
+ content: "\e908";
+}
+.cptools-folder-open:before {
+ content: "\e909";
+}
.cptools-kanban:before {
content: "\e90a";
}
+.cptools-folder:before {
+ content: "\e90b";
+}
+.cptools-shared-folder-open:before {
+ content: "\e90c";
+}
.cptools-code:before {
content: "\e90d";
}
@@ -58,8 +100,11 @@
.cptools-file:before {
content: "\e90f";
}
-.cptools-destroy:before {
- content: "\e915";
+.cptools-palette:before {
+ content: "\e911";
+}
+.cptools-folder-upload:before {
+ content: "\e912";
}
.cptools-add-bottom:before {
content: "\e913";
@@ -67,24 +112,6 @@
.cptools-add-top:before {
content: "\e914";
}
-.cptools-folder-upload:before {
- content: "\e912";
-}
-.cptools-folder-no-color:before {
- content: "\e900";
-}
-.cptools-shared-folder:before {
- content: "\e903";
-}
-.cptools-folder-open:before {
- content: "\e909";
-}
-.cptools-folder:before {
- content: "\e90b";
-}
-.cptools-shared-folder-open:before {
- content: "\e90c";
-}
-.cptools-palette:before {
- content: "\e911";
+.cptools-destroy:before {
+ content: "\e915";
}
diff --git a/customize.dist/src/less2/include/colortheme-dark.less b/customize.dist/src/less2/include/colortheme-dark.less
index 588d1888e..ba80b2d64 100644
--- a/customize.dist/src/less2/include/colortheme-dark.less
+++ b/customize.dist/src/less2/include/colortheme-dark.less
@@ -10,6 +10,7 @@
code: #EAA000;
slide: #e57614;
poll: #2c9e98;
+ form: #2c9e98;
whiteboard: #a72ba7;
kanban: #8C4;
sheet: #40865c;
@@ -426,3 +427,13 @@
@cp_calendar-now: @cryptpad_color_brand_300;
@cp_calendar-now-fg: @cryptpad_color_grey_800;
+// Forms
+@cp_form-bg1: @cryptpad_color_grey_800;
+@cp_form-bg2: @cryptpad_color_grey_900;
+@cp_form-border: @cryptpad_color_grey_800;
+@cp_form-poll-color: @cryptpad_color_grey_800;
+@cp_form-poll-no: @cryptpad_color_light_red;
+@cp_form-poll-yes: @cryptpad_color_light_green;
+@cp_form-poll-maybe: @cryptpad_color_light_yellow;
+@cp_form-poll-yes-color: @cryptpad_color_green;
+@cp_form-invalid: @cryptpad_color_red;
diff --git a/customize.dist/src/less2/include/colortheme.less b/customize.dist/src/less2/include/colortheme.less
index 3ab249f9f..1102f6074 100644
--- a/customize.dist/src/less2/include/colortheme.less
+++ b/customize.dist/src/less2/include/colortheme.less
@@ -10,6 +10,7 @@
code: #EAA000;
slide: #e57614;
poll: #2c9e98;
+ form: #2c9e98;
whiteboard: #a72ba7;
kanban: #8C4;
sheet: #40865c;
@@ -425,3 +426,14 @@
@cp_calendar-border: @cryptpad_color_grey_300;
@cp_calendar-now: @cryptpad_color_brand;
@cp_calendar-now-fg: @cryptpad_color_grey_200;
+
+// Forms
+@cp_form-bg1: @cryptpad_color_grey_200;
+@cp_form-bg2: @cryptpad_color_grey_100;
+@cp_form-border: @cryptpad_color_grey_200;
+@cp_form-poll-color: @cryptpad_color_grey_800;
+@cp_form-poll-no: @cryptpad_color_light_red;
+@cp_form-poll-yes: @cryptpad_color_light_green;
+@cp_form-poll-maybe: @cryptpad_color_light_yellow;
+@cp_form-poll-yes-color: @cryptpad_color_green;
+@cp_form-invalid: @cryptpad_color_red;
diff --git a/customize.dist/src/less2/include/forms.less b/customize.dist/src/less2/include/forms.less
index ffe061fa3..4fb799e13 100644
--- a/customize.dist/src/less2/include/forms.less
+++ b/customize.dist/src/less2/include/forms.less
@@ -71,6 +71,12 @@
div.cp-button-confirm {
display: inline-block;
+ &.new {
+ vertical-align: top;
+ button {
+ height: 35px;
+ }
+ }
button {
margin: 0 !important;
}
@@ -85,7 +91,7 @@
}
}
}
- button.cp-button-confirm-placeholder {
+ button.cp-button-confirm-placeholder:not(.new) {
margin-bottom: 3px !important;
}
diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js
index dcfb3872b..ad5f726f8 100644
--- a/www/common/application_config_internal.js
+++ b/www/common/application_config_internal.js
@@ -12,7 +12,7 @@ define(function() {
* You should never remove the drive from this list.
*/
AppConfig.availablePadTypes = ['drive', 'teams', 'pad', 'sheet', 'code', 'slide', 'poll', 'kanban', 'whiteboard',
- /*'doc', 'presentation',*/ 'file', /*'todo',*/ 'contacts' /*, 'calendar' */];
+ /*'doc', 'presentation',*/ 'file', /*'todo',*/ 'contacts', 'form'];
/* The registered only types are apps restricted to registered users.
* You should never remove apps from this list unless you know what you're doing. The apps
* listed here by default can't work without a user account.
@@ -117,6 +117,7 @@ define(function() {
code: 'cptools-code',
slide: 'cptools-slide',
poll: 'cptools-poll',
+ form: 'cptools-poll',
whiteboard: 'cptools-whiteboard',
todo: 'cptools-todo',
contacts: 'fa-address-book',
diff --git a/www/common/common-hash.js b/www/common/common-hash.js
index cf3ca76a4..b9cdfa699 100644
--- a/www/common/common-hash.js
+++ b/www/common/common-hash.js
@@ -34,6 +34,12 @@ var factory = function (Util, Crypto, Keys, Nacl) {
var keyPair = Nacl.sign.keyPair.fromSecretKey(privateKey);
return Nacl.util.encodeBase64(keyPair.publicKey);
};
+ Hash.getCurvePublicFromPrivate = function (curvePrivateSafeStr) {
+ var curvePrivateStr = Crypto.b64AddSlashes(curvePrivateSafeStr);
+ var privateKey = Nacl.util.decodeBase64(curvePrivateStr);
+ var keyPair = Nacl.box.keyPair.fromSecretKey(privateKey);
+ return Nacl.util.encodeBase64(keyPair.publicKey);
+ };
var getEditHashFromKeys = Hash.getEditHashFromKeys = function (secret) {
var version = secret.version;
@@ -209,6 +215,17 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app)
});
return k ? Crypto.b64AddSlashes(k) : '';
};
+ var getAuditorKey = function (hashArr) {
+ var k;
+ // Check if we have a ownerKey for this pad
+ hashArr.some(function (data) {
+ if (/^auditor=/.test(data)) {
+ k = data.slice(8);
+ return true;
+ }
+ });
+ return k ? Crypto.b64AddSlashes(k) : '';
+ };
var getOwnerKey = function (hashArr) {
var k;
// Check if we have a ownerKey for this pad
@@ -231,6 +248,7 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app)
parsed.present = options.indexOf('present') !== -1;
parsed.embed = options.indexOf('embed') !== -1;
parsed.versionHash = getVersionHash(options);
+ parsed.auditorKey = getAuditorKey(options);
parsed.newPadOpts = getNewPadOpts(options);
parsed.loginOpts = getLoginOpts(options);
parsed.ownerKey = getOwnerKey(options);
@@ -272,6 +290,7 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app)
present: parsed.present,
ownerKey: parsed.ownerKey,
versionHash: parsed.versionHash,
+ auditorKey: parsed.auditorKey,
newPadOpts: parsed.newPadOpts,
loginOpts: parsed.loginOpts,
password: parsed.password
@@ -298,6 +317,10 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app)
if (versionHash) {
hash += 'hash=' + Crypto.b64RemoveSlashes(versionHash) + '/';
}
+ var auditorKey = typeof(opts.auditorKey) !== "undefined" ? opts.auditorKey : parsed.auditorKey;
+ if (auditorKey) {
+ hash += 'auditor=' + Crypto.b64RemoveSlashes(auditorKey) + '/';
+ }
if (opts.newPadOpts) { hash += 'newpad=' + opts.newPadOpts + '/'; }
if (opts.loginOpts) { hash += 'login=' + opts.loginOpts + '/'; }
return hash;
diff --git a/www/common/common-interface.js b/www/common/common-interface.js
index dbcb089f3..9bdf9ec7a 100644
--- a/www/common/common-interface.js
+++ b/www/common/common-interface.js
@@ -747,6 +747,7 @@ define([
cb = Util.once(cb);
}
var classes = 'btn ' + (config.classes || 'btn-primary');
+ var newCls = config.new ? '.new' : '';
var button = h('button', {
"class": classes,
@@ -759,7 +760,7 @@ define([
});
var timer = h('div.cp-button-timer', div);
- var content = h('div.cp-button-confirm', [
+ var content = h('div.cp-button-confirm'+newCls, [
button,
timer
]);
@@ -795,7 +796,8 @@ define([
to = setTimeout(todo, INTERVAL);
};
- $(originalBtn).addClass('cp-button-confirm-placeholder').click(function (e) {
+ var newCls2 = config.new ? 'new' : '';
+ $(originalBtn).addClass('cp-button-confirm-placeholder').addClass(newCls2).click(function (e) {
e.stopPropagation();
// If we have a validation function, continue only if it's true
if (config.validate && !config.validate()) { return; }
@@ -1175,6 +1177,7 @@ define([
var label = h('span.cp-checkmark-label', labelTxt);
$mark.keydown(function (e) {
+ if ($input.is(':disabled')) { return; }
if (e.which === 32) {
e.stopPropagation();
e.preventDefault();
@@ -1220,20 +1223,22 @@ define([
$.extend(markOpts, opts.mark || {});
var input = h('input', inputOpts);
+ var $input = $(input);
var mark = h('span.cp-radio-mark', markOpts);
var label = h('span.cp-checkmark-label', labelTxt);
$(mark).keydown(function (e) {
+ if ($input.is(':disabled')) { return; }
if (e.which === 32) {
e.stopPropagation();
e.preventDefault();
- if ($(input).is(':checked')) { return; }
- $(input).prop('checked', !$(input).is(':checked'));
- $(input).change();
+ if ($input.is(':checked')) { return; }
+ $input.prop('checked', !$input.is(':checked'));
+ $input.change();
}
});
- $(input).change(function () { $(mark).focus(); });
+ $input.change(function () { $(mark).focus(); });
var radio = h('label', labelOpts, [
input,
diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js
index 887eb147a..011bb587b 100644
--- a/www/common/common-ui-elements.js
+++ b/www/common/common-ui-elements.js
@@ -936,7 +936,8 @@ define([
return button;
};
- var createMdToolbar = function (common, editor) {
+ var createMdToolbar = function (common, editor, cfg) {
+ cfg = cfg || {};
var $toolbar = $('
', {
'class': 'cp-markdown-toolbar'
});
@@ -1025,9 +1026,39 @@ define([
icon: 'fa-newspaper-o'
}
};
+
+ if (typeof(cfg.embed) === "function") {
+ actions.embed = {
+ icon: 'fa-picture-o',
+ action: function () {
+ var _cfg = {
+ types: ['file'],
+ where: ['root']
+ };
+ common.openFilePicker(_cfg, function (data) {
+ if (data.type !== 'file') {
+ console.log("Unexpected data type picked " + data.type);
+ return;
+ }
+ if (data.type !== 'file') { console.log('unhandled embed type ' + data.type); return; }
+ common.setPadAttribute('atime', +new Date(), null, data.href);
+ var privateDat = common.getMetadataMgr().getPrivateData();
+ var origin = privateDat.fileHost || privateDat.origin;
+ var src = data.src = data.src.slice(0,1) === '/' ? origin + data.src : data.src;
+ cfg.embed($('
'), data);
+ });
+
+ }
+ };
+ }
+
var onClick = function () {
var type = $(this).attr('data-type');
var texts = editor.getSelections();
+ if (actions[type].action) {
+ return actions[type].action();
+ }
var newTexts = texts.map(function (str) {
str = str || Messages.mdToolbar_defaultText;
if (actions[type].apply) {
@@ -1054,7 +1085,7 @@ define([
}).appendTo($toolbar);
return $toolbar;
};
- UIElements.createMarkdownToolbar = function (common, editor) {
+ UIElements.createMarkdownToolbar = function (common, editor, opts) {
var readOnly = common.getMetadataMgr().getPrivateData().readOnly;
if (readOnly) {
return {
@@ -1064,7 +1095,7 @@ define([
};
}
- var $toolbar = createMdToolbar(common, editor);
+ var $toolbar = createMdToolbar(common, editor, opts);
var cfg = {
title: Messages.mdToolbar_button,
element: $toolbar
@@ -1133,6 +1164,7 @@ define([
sheet: 'sheets',
poll: 'poll',
kanban: 'kanban',
+ form: 'form',
whiteboard: 'whiteboard',
};
@@ -1472,11 +1504,13 @@ define([
if (config.isSelect) {
var pressed = '';
var to;
+ $container.onChange = Util.mkEvent();
$container.on('click', 'a', function () {
value = $(this).data('value');
var $val = $(this);
var textValue = $val.html() || value;
$button.find('.cp-dropdown-button-title').html(textValue);
+ $container.onChange.fire(textValue, value);
});
$container.keydown(function (e) {
var $value = $innerblock.find('[data-value].cp-dropdown-element-active:visible');
@@ -2050,6 +2084,7 @@ define([
AppConfig.registeredOnlyTypes.indexOf(p) !== -1) { return; }
return true;
});
+ Messages.type.form = "Form"; // XXX
types.forEach(function (p) {
var $element = $('
', {
'class': 'cp-icons-element',
@@ -3012,6 +3047,7 @@ define([
// ACCEPT
sframeChan.query('Q_SET_PAD_METADATA', {
channel: msg.content.channel,
+ channels: msg.content.channels,
command: 'ADD_OWNERS',
value: [priv.edPublic]
}, function (err, res) {
@@ -3061,6 +3097,7 @@ define([
// Remove yourself from the pending owners
sframeChan.query('Q_SET_PAD_METADATA', {
channel: msg.content.channel,
+ channels: msg.content.channels,
command: 'RM_PENDING_OWNERS',
value: [priv.edPublic]
}, function (err, res) {
@@ -3077,6 +3114,7 @@ define([
// Remove yourself from the pending owners
sframeChan.query('Q_SET_PAD_METADATA', {
channel: msg.content.channel,
+ channels: msg.content.channels,
command: 'RM_PENDING_OWNERS',
value: [priv.edPublic]
}, function (err, res) {
diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js
index fd2d29cd2..6c1828897 100644
--- a/www/common/cryptpad-common.js
+++ b/www/common/cryptpad-common.js
@@ -76,7 +76,7 @@ define([
postMessage("GET", {
key: ['edPrivate'],
}, waitFor(function (obj) {
- if (obj.error) { return; }
+ if (!obj || obj.error) { return; }
try {
keys.push({
edPrivate: obj,
@@ -84,14 +84,16 @@ define([
});
} catch (e) { console.error(e); }
}));
+
// Push teams keys
postMessage("GET", {
key: ['teams'],
}, waitFor(function (obj) {
- if (obj.error) { return; }
+ if (!obj || obj.error) { return; }
Object.keys(obj || {}).forEach(function (id) {
var t = obj[id];
var _keys = t.keys.drive || {};
+ _keys.id = id;
if (!_keys.edPrivate) { return; }
keys.push(t.keys.drive);
});
@@ -101,6 +103,57 @@ define([
});
};
+ common.getFormKeys = function (cb) {
+ var curvePrivate;
+ var formSeed;
+ Nthen(function (waitFor) {
+ postMessage("GET", {
+ key: ['curvePrivate'],
+ }, waitFor(function (obj) {
+ if (!obj || obj.error) { return; }
+ curvePrivate = obj;
+ }));
+ postMessage("GET", {
+ key: ['form_seed'],
+ }, waitFor(function (obj) {
+ if (!obj || obj.error) { return; }
+ formSeed = obj;
+ }));
+ }).nThen(function () {
+ cb({
+ curvePrivate: curvePrivate,
+ curvePublic: curvePrivate && Hash.getCurvePublicFromPrivate(curvePrivate),
+ formSeed: formSeed
+ });
+ });
+ };
+ common.getFormAnswer = function (data, cb) {
+ postMessage("GET", {
+ key: ['forms', data.channel],
+ }, cb);
+ };
+ common.storeFormAnswer = function (data) {
+ postMessage("SET", {
+ key: ['forms', data.channel],
+ value: {
+ hash: data.hash,
+ curvePrivate: data.curvePrivate,
+ anonymous: data.anonymous
+ }
+ }, function (obj) {
+ if (obj && obj.error) {
+ if (obj.error === "ENODRIVE") {
+ var answered = JSON.parse(localStorage.CP_formAnswered || "[]");
+ if (answered.indexOf(data.channel) === -1) { answered.push(data.channel); }
+ localStorage.CP_formAnswered = JSON.stringify(answered);
+ return;
+ }
+ console.error(obj.error);
+ }
+ });
+
+ };
+
common.makeNetwork = function (cb) {
require([
'/bower_components/netflux-websocket/netflux-client.js',
@@ -712,6 +765,10 @@ define([
delete meta.chat2;
delete meta.chat;
delete meta.cursor;
+
+ if (meta.type === "form") {
+ delete parsed.answers;
+ }
}
};
diff --git a/www/common/inner/access.js b/www/common/inner/access.js
index 693b8d236..a1d2c37a1 100644
--- a/www/common/inner/access.js
+++ b/www/common/inner/access.js
@@ -32,6 +32,12 @@ define([
var teamOwner = data.teamId;
var title = opts.title;
+ var p = priv.propChannels;
+ var otherChan;
+ if (p && p.answersChannel) {
+ otherChan = [p.answersChannel];
+ }
+
opts = opts || {};
var redrawAll = function () {};
@@ -255,6 +261,7 @@ define([
// Send the command
sframeChan.query('Q_SET_PAD_METADATA', {
channel: channel,
+ channels: otherChan,
command: 'ADD_OWNERS',
value: toAddTeams.map(function (obj) { return obj.edPublic; }),
teamId: teamOwner
@@ -290,6 +297,7 @@ define([
// Send the command
sframeChan.query('Q_SET_PAD_METADATA', {
channel: channel,
+ channels: otherChan,
command: 'ADD_PENDING_OWNERS',
value: toAdd,
teamId: teamOwner
@@ -310,6 +318,7 @@ define([
// Send the command
sframeChan.query('Q_SET_PAD_METADATA', {
channel: channel,
+ channels: otherChan,
command: 'ADD_OWNERS',
value: [priv.edPublic],
teamId: teamOwner
@@ -338,6 +347,7 @@ define([
if (!friend) { return; }
common.mailbox.sendTo("ADD_OWNER", {
channel: channel,
+ channels: otherChan,
href: href,
calendar: opts.calendar,
password: data.password || priv.password,
@@ -417,6 +427,12 @@ define([
var allowed = data.allowed || [];
var teamOwner = data.teamId;
+ var p = priv.propChannels;
+ var otherChan;
+ if (p && p.answersChannel) {
+ otherChan = [p.answersChannel];
+ }
+
var redrawAll = function () {};
var addBtn = h('button.btn.btn-primary.cp-access-add', [h('i.fa.fa-arrow-left'), h('i.fa.fa-arrow-up')]);
@@ -495,6 +511,7 @@ define([
// Send the command
sframeChan.query('Q_SET_PAD_METADATA', {
channel: channel,
+ channels: otherChan,
command: 'RM_ALLOWED',
value: [ed],
teamId: teamOwner
@@ -524,6 +541,7 @@ define([
var val = $checkbox.is(':checked');
sframeChan.query('Q_SET_PAD_METADATA', {
channel: channel,
+ channels: otherChan,
command: 'RESTRICT_ACCESS',
value: [Boolean(val)],
teamId: teamOwner
@@ -659,6 +677,7 @@ define([
// Send the command
sframeChan.query('Q_SET_PAD_METADATA', {
channel: channel,
+ channels: otherChan,
command: 'ADD_ALLOWED',
value: toAdd,
teamId: teamOwner
@@ -987,6 +1006,15 @@ define([
UI.findCancelButton().click();
if (err || (obj && obj.error)) { UI.warn(Messages.error); }
});
+
+ // If this is a form wiht a answer channel, delete it too
+ var p = priv.propChannels;
+ if (p.answersChannel) {
+ sframeChan.query('Q_DELETE_OWNED', {
+ teamId: typeof(owned) !== "boolean" ? owned : undefined,
+ channel: p.answersChannel
+ }, function () {});
+ }
});
if (!opts.noEditPassword) { $d.append(h('br')); }
$d.append(h('div', [
@@ -1020,7 +1048,7 @@ define([
var owned = Modal.isOwned(Env, data);
// Request edit access
- if (common.isLoggedIn() && ((data.roHref && !data.href) || data.fakeHref) && !owned && !opts.calendar) {
+ if (common.isLoggedIn() && ((data.roHref && !data.href) || data.fakeHref) && !owned && !opts.calendar && priv.app !== 'form') {
var requestButton = h('button.btn.btn-secondary.no-margin.cp-access-margin-right',
Messages.requestEdit_button);
var requestBlock = h('p', requestButton);
@@ -1058,7 +1086,7 @@ define([
var canMute = data.mailbox && owned === true && (
(typeof (data.mailbox) === "string" && data.owners[0] === edPublic) ||
data.mailbox[edPublic]);
- if (owned === true && !opts.calendar) {
+ if (owned === true && !opts.calendar && priv.app !== 'form') {
var cbox = UI.createCheckbox('cp-access-mute', Messages.access_muteRequests, !canMute);
var $cbox = $(cbox);
var spinner = UI.makeSpinner($cbox);
diff --git a/www/common/inner/properties.js b/www/common/inner/properties.js
index ddbf017fc..c946ed208 100644
--- a/www/common/inner/properties.js
+++ b/www/common/inner/properties.js
@@ -24,6 +24,7 @@ define([
if (privateData.propChannels) {
var p = privateData.propChannels;
data.channel = data.channel || p.channel;
+ data.answersChannel = data.answersChannel || p.answersChannel;
data.rtChannel = data.rtChannel || p.rtChannel;
data.lastVersion = data.lastVersion || p.lastVersion;
data.lastCpHash = data.lastCpHash || p.lastCpHash;
@@ -75,6 +76,7 @@ define([
var bytes = 0;
var historyBytes;
var chan = [data.channel];
+ if (data.answersChannel) { chan.push(data.answersChannel); }
if (data.rtChannel) { chan.push(data.rtChannel); }
if (data.lastVersion) { chan.push(Hash.hrefToHexChannelId(data.lastVersion)); }
diff --git a/www/common/inner/share.js b/www/common/inner/share.js
index f2eb2e953..5db88b672 100644
--- a/www/common/inner/share.js
+++ b/www/common/inner/share.js
@@ -494,7 +494,23 @@ define([
var parsed = Hash.parsePadUrl(pathname);
var canPresent = ['code', 'slide'].indexOf(parsed.type) !== -1;
var versionHash = hashes.viewHash && opts.versionHash;
- var canBAR = parsed.type !== 'drive' && !versionHash;
+ var isForm = parsed.type === "form"; // && opts.auditorHash;
+ var canBAR = parsed.type !== 'drive' && !versionHash && !isForm;
+
+ var labelEdit = Messages.share_linkEdit;
+ var labelView = Messages.share_linkView;
+
+ var auditor;
+ if (isForm) {
+ Messages.share_formEdit = "Author"; // XXX
+ Messages.share_formView = "Participant"; // XXX
+ Messages.share_formAuditor = "Auditor"; // XXX
+ labelEdit = Messages.share_formEdit;
+ labelView = Messages.share_formView;
+ auditor = UI.createRadio('accessRights', 'cp-share-form', Messages.share_formAuditor, false, {
+ mark: {tabindex:1},
+ });
+ }
var burnAfterReading = (hashes.viewHash && canBAR) ?
UI.createRadio('accessRights', 'cp-share-bar', Messages.burnAfterReading_linkBurnAfterReading, false, {
@@ -505,12 +521,13 @@ define([
h('label', Messages.share_linkAccess),
h('div.radio-group',[
UI.createRadio('accessRights', 'cp-share-editable-false',
- Messages.share_linkView, true, { mark: {tabindex:1} }),
+ labelView, true, { mark: {tabindex:1} }),
canPresent ? UI.createRadio('accessRights', 'cp-share-present',
Messages.share_linkPresent, false, { mark: {tabindex:1} }) : undefined,
UI.createRadio('accessRights', 'cp-share-editable-true',
- Messages.share_linkEdit, false, { mark: {tabindex:1} })]),
- burnAfterReading
+ labelEdit, false, { mark: {tabindex:1} }),
+ auditor]),
+ burnAfterReading,
]);
// Burn after reading
@@ -553,6 +570,7 @@ define([
var embed = val.embed;
var present = val.present !== undefined ? val.present : Util.isChecked($rights.find('#cp-share-present'));
var burnAfterReading = Util.isChecked($rights.find('#cp-share-bar'));
+ var formAuditor = Util.isChecked($rights.find('#cp-share-form'));
if (versionHash) {
edit = false;
present = false;
@@ -569,6 +587,9 @@ define([
}
var hash = (!hashes.viewHash || (edit && hashes.editHash)) ? hashes.editHash
: hashes.viewHash;
+ if (formAuditor && opts.auditorHash) {
+ hash = opts.auditorHash;
+ }
var href = burnAfterReading ? opts.burnAfterReadingUrl
: (origin + pathname + '#' + hash);
var parsed = Hash.parsePadUrl(href);
@@ -594,6 +615,9 @@ define([
$rights.find('#cp-share-present').removeAttr('checked').attr('disabled', true);
$rights.find('#cp-share-editable-true').attr('checked', true);
}
+ if (isForm && !opts.auditorHash) {
+ $rights.find('#cp-share-form').removeAttr('checked').attr('disabled', true);
+ }
var getLink = function () {
return $rights.parent().find('#cp-share-link-preview');
diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js
index cec0d7d73..d13cb09fb 100644
--- a/www/common/outer/async-store.js
+++ b/www/common/outer/async-store.js
@@ -112,6 +112,7 @@ define([
Store.set = function (clientId, data, cb) {
var s = getStore(data.teamId);
if (!s) { return void cb({ error: 'ENOTFOUND' }); }
+ if (!s.proxy) { return void cb({ error: 'ENODRIVE' }); }
var path = data.key.slice();
var key = path.pop();
var obj = Util.find(s.proxy, path);
@@ -629,6 +630,7 @@ define([
if (!proxy.uid) {
store.noDriveUid = store.noDriveUid || Hash.createChannelId();
}
+
var metadata = {
// "user" is shared with everybody via the userlist
user: {
@@ -655,7 +657,7 @@ define([
accountName: proxy.login_name || '',
offline: store.proxy && store.offline,
teams: teams,
- plan: account.plan
+ plan: account.plan,
}
};
cb(JSON.parse(JSON.stringify(metadata)));
@@ -2139,11 +2141,23 @@ define([
if (!data.channel) { return void cb({ error: 'ENOTFOUND'}); }
if (!data.command) { return void cb({ error: 'EINVAL' }); }
var s = getStore(data.teamId);
+ var otherChannels = data.channels;
+ delete data.channels;
s.rpc.setMetadata(data, function (err, res) {
if (err) { return void cb({ error: err }); }
if (!Array.isArray(res) || !res.length) { return void cb({}); }
cb(res[0]);
});
+ // If we have other related channels, send the command for them too
+ if (Array.isArray(otherChannels)) {
+ otherChannels.forEach(function (chan) {
+ var _d = Util.clone(data);
+ _d.channel = chan;
+ Store.setPadMetadata(clientId, _d, function () {
+
+ });
+ });
+ }
};
// GET_FULL_HISTORY from sframe-common-outer
@@ -2696,7 +2710,12 @@ define([
nThen(function (waitFor) {
if (!proxy.settings) { proxy.settings = NEW_USER_SETTINGS; }
+ if (!proxy.forms) { proxy.forms = {}; }
if (!proxy.friends_pending) { proxy.friends_pending = {}; }
+ // Form seed is used to generate a box encryption keypair when
+ // answering a form anonymously
+ if (!proxy.form_seed) { proxy.form_seed = Hash.createChannelId(); }
+
// Call onCacheReady if the manager is not yet defined
if (!manager) {
diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js
index ccb6eabdf..0e799de56 100644
--- a/www/common/proxy-manager.js
+++ b/www/common/proxy-manager.js
@@ -818,6 +818,7 @@ define([
_findChannels(Env, toUnpin).forEach(function (id) {
var data = _getFileData(Env, id);
var arr = [data.channel];
+ if (data.answersChannel) { arr.push(data.answersChannel); }
if (data.rtChannel) { arr.push(data.rtChannel); }
if (data.lastVersion) { arr.push(Hash.hrefToHexChannelId(data.lastVersion)); }
Array.prototype.push.apply(toKeep, arr);
@@ -1184,6 +1185,10 @@ define([
result.push(otherChan);
}
}
+ // Pin form answers channels
+ if (data.answersChannel && result.indexOf(data.answersChannel) === -1) {
+ result.push(data.answersChannel);
+ }
// Pin onlyoffice realtime patches
if (data.rtChannel && result.indexOf(data.rtChannel) === -1) {
result.push(data.rtChannel);
diff --git a/www/common/toolbar.js b/www/common/toolbar.js
index 119d7fb3d..26ff870f6 100644
--- a/www/common/toolbar.js
+++ b/www/common/toolbar.js
@@ -553,11 +553,13 @@ MessengerUI, Messages, Pages) {
if (toolbar.isDeleted) {
return void UI.warn(Messages.deletedFromServer);
}
+ var privateData = config.metadataMgr.getPrivateData();
var title = (config.title && config.title.getTitle && config.title.getTitle())
|| (config.title && config.title.defaultName)
|| "";
Common.getSframeChannel().event('EV_SHARE_OPEN', {
- title: title
+ title: title,
+ auditorHash: privateData.form_auditorHash
});
});
diff --git a/www/form/app-form.less b/www/form/app-form.less
new file mode 100644
index 000000000..278e1d1db
--- /dev/null
+++ b/www/form/app-form.less
@@ -0,0 +1,566 @@
+@import (reference) '../../customize/src/less2/include/framework.less';
+@import (reference) '../../customize/src/less2/include/tools.less';
+@import (reference) '../../customize/src/less2/include/avatar.less';
+
+&.cp-app-form {
+ @form_input-width: 400px;
+
+ .framework_main(
+ @bg-color: @colortheme_apps[form]
+ );
+
+ display: flex;
+ flex-flow: column;
+ font: @colortheme_app-font;
+ color: @cryptpad_text_col;
+ background-color: @cp_app-bg;
+
+ #cp-app-form-editor {
+ flex: 1;
+ display: flex;
+ flex-flow: row;
+ height: 100%;
+ overflow: hidden;
+ }
+
+ &.cp-app-form-results {
+ div.cp-form-creator-content, .cp-app-form-button-results {
+ display: none !important;
+ }
+ }
+ &:not(.cp-app-form-results) {
+ div.cp-form-creator-results, .cp-app-form-button-creator {
+ display: none !important;
+ }
+ }
+
+ #cp-app-form-container {
+ display: flex;
+ flex: 1;
+ justify-content: center;
+ min-width: 300px;
+
+ .cp-form-input-block {
+ display: flex;
+ }
+
+ div.cp-form-creator-container {
+ display: flex;
+ flex: 1;
+ justify-content: center;
+ min-width: 300px;
+ //flex-wrap: wrap;
+ overflow: auto;
+
+ @media screen and (max-width: 1000px) {
+ flex-wrap: wrap;
+ justify-content: flex-start;
+ .cp-form-creator-control {
+ width: 100% !important;
+ .cp-form-creator-settings {
+ display: flex;
+ justify-content: space-evenly;
+ flex-wrap: wrap;
+ }
+ }
+ }
+
+ .cp-form-creator-settings {
+ padding: 30px;
+ & > div:not(:last-child) {
+ margin-bottom: 20px;
+ }
+ }
+ div.cp-form-filler-container {
+ width: 300px;
+ min-width: 0;
+ flex: 0 300 300px;
+ }
+ div.cp-form-creator-control {
+ padding: 10px;
+ display: flex;
+ flex-flow: column;
+ width: 300px;
+ .cp-form-creator-types {
+ margin-top: 20px;
+ display: flex;
+ flex-flow: column;
+ }
+ }
+ div.cp-form-creator-content, div.cp-form-creator-results {
+ max-width: 1000px;
+ min-width: 300px;
+ padding: 10px;
+ display: flex;
+ flex-flow: column;
+ flex: 1 1 1000px;
+ overflow: auto;
+
+ .cp-form-creator-add-inline {
+ display: flex;
+ flex-flow: row;
+ align-items: center;
+ margin-bottom: 20px;
+ button {
+ width: 50px;
+ i {
+ margin-right: 0;
+ }
+ }
+ .cp-form-creator-inline-add {
+ font-size: 25px;
+ margin-right: 30px;
+ .add-close { display: none; }
+ &.displayed {
+ .add-close { display: inline; }
+ .add-open { display: none; }
+ }
+ }
+ .cp-form-creator-control-inline {
+ display: flex;
+ justify-content: space-around;
+ button:not(:last-child) {
+ margin-right: 5px;
+ }
+ .cp-form-creator-types {
+ .btn-default {
+ background: transparent;
+ &:hover, &:not(:disabled):active, &:focus {
+ background-color: @cp_buttons-default;
+ }
+ }
+ button {
+ border: 0px;
+ //padding-bottom: 3px;
+ i {
+ font-size: 35px;
+ line-height: 35px;
+ }
+ }
+ &:first-child {
+ margin-right: 50px;
+ }
+ }
+ }
+ }
+ .cp-form-creator-add-full {
+ display: flex;
+ align-items: center;
+ margin: 50px 0px 100px 0px;
+ &> div:first-child {
+ border-right: 1px solid fade(@cryptpad_text_col, 25%);
+ display: flex;
+ height: 100%;
+ align-items: center;
+ padding-right: 10px;
+ margin-right: 10px;
+ i {
+ color: fade(@cryptpad_text_col, 25%);
+ font-size: 30px;
+ }
+ }
+ .cp-form-creator-control-inline {
+ display: flex;
+ flex-flow: column;
+ justify-content: space-around;
+ button:not(:last-child) {
+ margin-right: 5px;
+ }
+ .cp-form-creator-types {
+ .btn-default {
+ background: transparent;
+ &:hover, &:not(:disabled):active, &:focus {
+ background-color: @cp_buttons-default;
+ }
+ }
+ button {
+ border: 0px;
+ padding:5px;
+ margin-right: 10px;
+ i {
+ font-size: 35px;
+ line-height: 35px;
+ }
+ }
+ &:first-child {
+ margin-bottom: 20px;
+ margin-right: 50px;
+ }
+ }
+ }
+ }
+
+ .cp-form-page + .cp-form-send-container {
+ margin-top: 10px;
+ }
+
+ .cp-form-page-container {
+ display: flex;
+ justify-content: center;
+ margin: 10px 0;
+ & > span {
+ margin: 0 20px;
+ width: 100px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ }
+ button {
+ &.cp-next {
+ .fa {
+ margin-right: 0;
+ margin-left: 5px;
+ }
+ }
+ }
+ }
+ .cp-form-block {
+ .tools_unselectable();
+ background: @cp_form-bg1;
+ padding: 10px;
+ &:not(:last-child) {
+ margin-bottom: 20px;
+ }
+
+ .cp-form-block-drag-handle {
+ display: flex;
+ flex-flow: column;
+ align-items: center;
+ color: @cp_sidebar-hint;
+ i {
+ cursor: grab;
+ &:first-child {
+ height: 3px;
+ margin-top: -10px;
+ margin-bottom: 1px;
+ }
+ }
+ }
+
+ &.sortable-ghost { visibility: hidden; }
+ &.sortable-drag { opacity: 0.9 !important; }
+
+ .cp-form-block-question {
+ margin-bottom: 5px;
+ }
+ .cp-form-block-content {
+ overflow-x: auto;
+ .cp-form-page-break-edit {
+ text-align: center;
+ padding: 10px;
+ i {
+ margin-right: 5px;
+ }
+ }
+ .cp-form-edit-buttons-container {
+ margin-top: 20px;
+ display: flex;
+ justify-content: space-between;
+ }
+ input:invalid {
+ border: 1px solid @cp_form-invalid;
+ }
+ }
+ .cp-form-input-block {
+ //width: @form_input-width;
+ padding-bottom: 10px;
+ border-bottom: 2px solid @cp_sidebar-hint;
+ margin-bottom: 10px;
+ &:not(.editing) {
+ input {
+ background: transparent;
+ border: none;
+ padding: 0 !important;
+ & ~ button:not(:disabled) {
+ .cp-form-edit { display: inline; }
+ .cp-form-save { display: none; }
+ }
+ }
+ }
+ input {
+ flex: 1;
+ min-width: 100px;
+ padding: 0 10px !important;
+ height: auto;
+ font-size: 20px;
+ }
+ button {
+ .cp-form-edit {
+ display: none;
+ }
+ .cp-form-save { display: inline; }
+ }
+ .cp-form-block-drag {
+ font-size: 22px;
+ width: 20px;
+ margin-left: 5px;
+ text-align: center;
+ line-height: 31px;
+ }
+ }
+ &.editable {
+ cursor: grab;
+ .cp-form-edit-save {
+ margin-top: 20px;
+ button {
+ margin-right: 10px;
+ }
+ }
+ .cp-form-edit-type {
+ margin-bottom: 10px;
+ .cp-dropdown-container {
+ margin-left: 10px;
+ }
+ }
+ }
+ }
+ .cp-form-edit-max-options {
+ display: flex;
+ align-items: center;
+ margin-bottom: 10px;
+ input {
+ width: 100px;
+ margin-left: 10px;
+ }
+ }
+ .cp-form-edit-options-block {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+ .CodeMirror {
+ cursor: default;
+ flex: 1;
+ margin: auto;
+ min-width: 80%;
+ width: 80%;
+ min-height: 200px;
+ height: 200px;
+ border: 1px solid @cp_forms-border;
+ .CodeMirror-placeholder {
+ color: #777;
+ }
+ }
+ }
+ .cp-form-edit-block {
+
+ button.btn-secondary {
+ margin-left: 30px;
+ }
+ .cp-form-handle {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ color: @cp_sidebar-hint;
+ i:first-child {
+ margin-right: 1px;
+ }
+ }
+ .cp-form-edit-block-input {
+ margin-bottom: 5px; // XXX DB margin bug
+ &.sortable-ghost { visibility: hidden; }
+ &.sortable-drag { opacity: 0.9 !important; }
+ display: flex;
+ width: 400px;
+ input {
+ flex: 1;
+ min-width: 100px;
+ border-color: @cryptpad_text_col;
+ border-right: 0px;
+ }
+ button {
+ i { margin: 0 !important; }
+ }
+
+ }
+ }
+ }
+ }
+ div.cp-form-creator-results {
+ display: flex;
+ flex-flow: column;
+ position: relative;
+ & > div {
+ background: @cp_form-bg1;
+ padding: 10px;
+ &:not(:last-child) {
+ margin-bottom: 20px;
+ }
+ }
+ .cp-form-block-question {
+ margin-bottom: 5px;
+ }
+ .cp-form-block-type {
+ float: right;
+ padding: 5px;
+ margin-top: -10px;
+ margin-right: -10px;
+ i { margin-right: 5px; }
+ background: @cp_form-bg2;
+ }
+ .cp-form-results-type-text {
+ max-height: 300px;
+ overflow: auto;
+ .cp-form-results-type-text-data {
+ padding: 5px 10px;
+ background: @cp_form-bg2;
+ &:not(:last-child) { margin-bottom: 1px; }
+ }
+ }
+ .cp-form-results-type-textarea-data {
+ white-space: pre-wrap;
+ font-size: 14px;
+ border: 1px solid @cp_profile-hint;
+ padding: 0 5px;
+ }
+ .cp-form-results-type-radio {
+ display: table;
+ .cp-form-results-type-multiradio-data {
+ display: flex;
+ flex-flow: column;
+ }
+ .cp-form-results-type-radio-data {
+ display: table-row;
+ border: 1px solid @cp_form-border;
+ & > span {
+ border: 1px solid @cp_form-border;
+ display: table-cell;
+ padding: 5px 10px;
+ background: @cp_form-bg2;
+ &.cp-value {
+ min-width: 200px;
+ }
+ }
+ }
+ }
+ .cp-form-individual {
+ & > *:not(:last-child) {
+ margin-right: 10px;
+ }
+ .cp-form-warning {
+ color: @cp-limit-bar-warning;
+ }
+ .cp-form-friend {
+ color: @cp_profile-hint;
+ .fa {
+ margin-right: 5px;
+ }
+ }
+ }
+ }
+ }
+
+ .cp-form-type-radio, .cp-form-type-checkbox {
+ display: flex;
+ flex-flow: column;
+ align-items: baseline;
+ .cp-radio {
+ display: inline-flex;
+ }
+ }
+ .cp-form-type-multiradio {
+ display: table;
+ & > * {
+ display: table-row;
+ & > * {
+ display: table-cell;
+ padding: 5px 20px;
+ vertical-align: middle;
+ &:first-child {
+ min-width: 200px;
+ }
+ .cp-radio-mark {
+ margin: auto;
+ }
+ }
+ }
+ }
+ .cp-form-type-sort {
+ cursor: grab;
+ padding: 2px;
+ .cp-form-handle {
+ margin-right: 5px;
+ }
+ .cp-form-sort-order {
+ border: 1px solid @cp_profile-hint;
+ padding: 0 5px;
+ margin-right: 5px;
+ }
+ }
+
+ .cp-form-type-poll {
+ display: inline-flex;
+ flex-flow: column;
+ & > div {
+ display: flex;
+ }
+ .cp-poll-cell {
+ width: 100px;
+ height: 40px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ &:first-child {
+ width: 200px;
+ }
+ button {
+ width: 100%;
+ }
+ }
+ .cp-poll-time-day {
+ flex-basis: 100px;
+ border-right: 1px solid @cryptpad_text_col;
+ border-left: 1px solid @cryptpad_text_col;
+ border-top: 1px solid @cryptpad_text_col;
+ }
+ &.cp-form-poll-switch {
+ flex-flow: row;
+ & > div {
+ flex-flow: column;
+ }
+ .cp-poll-cell:not(.cp-poll-switch) {
+ &:first-child {
+ width: 100px;
+ }
+ }
+ .cp-form-poll-option, .cp-poll-switch {
+ width: 200px;
+ }
+ .cp-poll-time-day {
+ flex-basis: 40px;
+ border-right: none;
+ border-bottom: 1px solid @cryptpad_text_col;
+ border-left: 1px solid @cryptpad_text_col;
+ border-top: 1px solid @cryptpad_text_col;
+ }
+ }
+ .cp-form-poll-choice, .cp-form-poll-answer {
+ .fa {
+ display: none;
+ }
+ color: @cp_form-poll-color;
+ &[data-value="0"] {
+ background: @cp_form-poll-no;
+ .cp-no { display: inline; }
+ }
+ &[data-value="1"] {
+ background: @cp_form-poll-yes;
+ .cp-yes { display: inline; }
+ }
+ &[data-value="2"] {
+ background: @cp_form-poll-maybe;
+ .cp-maybe { display: inline; }
+ }
+ }
+ div.cp-form-poll-choice {
+ cursor: pointer;
+ padding: 5px;
+ border: 5px double @cp_form-bg1;
+ }
+ div.cp-form-poll-answer {
+ color: @cp_form-poll-yes-color;
+ }
+ }
+
+}
+
diff --git a/www/form/index.html b/www/form/index.html
new file mode 100644
index 000000000..96a3cce86
--- /dev/null
+++ b/www/form/index.html
@@ -0,0 +1,12 @@
+
+
+
+ CryptPad
+
+
+
+
+
+
+
+
diff --git a/www/form/inner.html b/www/form/inner.html
new file mode 100644
index 000000000..de37af4f6
--- /dev/null
+++ b/www/form/inner.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/www/form/inner.js b/www/form/inner.js
new file mode 100644
index 000000000..fcf1cc349
--- /dev/null
+++ b/www/form/inner.js
@@ -0,0 +1,2547 @@
+define([
+ 'jquery',
+ 'json.sortify',
+ '/bower_components/chainpad-crypto/crypto.js',
+ '/common/sframe-app-framework.js',
+ '/common/toolbar.js',
+ '/bower_components/nthen/index.js',
+ '/common/sframe-common.js',
+ '/common/common-util.js',
+ '/common/common-hash.js',
+ '/common/common-interface.js',
+ '/common/common-ui-elements.js',
+ '/common/clipboard.js',
+ '/common/inner/common-mediatag.js',
+ '/common/hyperscript.js',
+ '/customize/messages.js',
+ '/customize/application_config.js',
+ '/common/diffMarked.js',
+ '/common/sframe-common-codemirror.js',
+ 'cm/lib/codemirror',
+
+ '/common/inner/share.js',
+ '/common/inner/access.js',
+ '/common/inner/properties.js',
+
+ '/lib/datepicker/flatpickr.js',
+ '/bower_components/sortablejs/Sortable.min.js',
+
+ 'cm/addon/display/placeholder',
+ 'cm/mode/markdown/markdown',
+ 'css!cm/lib/codemirror.css',
+
+ 'css!/bower_components/codemirror/lib/codemirror.css',
+ 'css!/bower_components/codemirror/addon/dialog/dialog.css',
+ 'css!/bower_components/codemirror/addon/fold/foldgutter.css',
+ 'css!/lib/datepicker/flatpickr.min.css',
+ 'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
+ 'less!/form/app-form.less',
+], function (
+ $,
+ Sortify,
+ Crypto,
+ Framework,
+ Toolbar,
+ nThen,
+ SFCommon,
+ Util,
+ Hash,
+ UI,
+ UIElements,
+ Clipboard,
+ MT,
+ h,
+ Messages,
+ AppConfig,
+ DiffMd,
+ SFCodeMirror,
+ CMeditor,
+ Share, Access, Properties,
+ Flatpickr,
+ Sortable
+ )
+{
+ var APP = window.APP = {
+ };
+
+ var is24h = false;
+ var dateFormat = "Y-m-d H:i";
+ var timeFormat = "H:i";
+ try {
+ is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/);
+ } catch (e) {}
+ is24h = false;
+ if (!is24h) {
+ dateFormat = "Y-m-d h:i K";
+ timeFormat = "h:i K";
+ }
+
+ Messages.button_newform = "New Form"; // XXX
+ Messages.form_invalid = "Invalid form";
+ Messages.form_editBlock = "Edit";
+ Messages.form_editMax = "Max selectable options";
+ Messages.form_editMaxLength = "Maximum characters";
+ Messages.form_editType = "Options type";
+
+ Messages.form_poll_text = "Text";
+ Messages.form_poll_day = "Day";
+ Messages.form_poll_time = "Time";
+
+
+ Messages.form_textType = "Text type";
+ Messages.form_text_text = "Text";
+ Messages.form_text_url = "URL";
+ Messages.form_text_email = "Email";
+ Messages.form_text_number = "Number";
+
+ Messages.form_default = "Your question here?";
+ Messages.form_type_input = "Text"; // XXX
+ Messages.form_type_textarea = "Textarea"; // XXX
+ Messages.form_type_radio = "Radio"; // XXX
+ Messages.form_type_multiradio = "Multiline Radio"; // XXX
+ Messages.form_type_checkbox = "Checkbox"; // XXX
+ Messages.form_type_multicheck = "Multiline Checkbox"; // XXX
+ Messages.form_type_poll = "Poll"; // XXX
+ Messages.form_type_sort = "Ordered list"; // XXX
+
+ Messages.form_type_md = "Description"; // XXX
+ Messages.form_type_page = "Page break"; // XXX
+
+ Messages.form_description_default = "Your text here";
+
+ Messages.form_duplicates = "Duplicate entries have been removed";
+ Messages.form_maxOptions = "{0} answer(s) max";
+
+ Messages.form_submit = "Submit";
+ Messages.form_update = "Update";
+ Messages.form_reset = "Reset";
+ Messages.form_sent = "Sent";
+ Messages.form_delete = "Delete";
+
+ Messages.form_cantFindAnswers = "Unable to retrieve your existing answers for this form.";
+ Messages.form_answered = "You already answered this form";
+
+ Messages.form_results = "Responses";
+ Messages.form_editor = "Editor";
+ Messages.form_form = "Form";
+ Messages.form_viewResults = "Go to responses";
+ Messages.form_viewCreator = "Go to form creator";
+ Messages.form_showIndividual = "Show individual answers";
+ Messages.form_showSummary = "Show summary";
+ Messages.form_answerAnonymous = "Anonymous answer on {0}";
+ Messages.form_viewButton = "View";
+ Messages.form_backButton = "Back";
+ Messages.form_answerName = "Answer from {0} on {1}";
+ Messages.form_answerWarning = "Unconfirmed identity";
+
+ Messages.form_notAnswered = "And {0} empty answers";
+
+ Messages.form_makePublic = "Publish results";
+ Messages.form_makePublicWarning = "Are you sure you want to make the results of this form public? This can't be undone.";
+ Messages.form_isPublic = "Results are public";
+ Messages.form_isPrivate = "Results are private";
+
+ Messages.form_open = "Open";
+ Messages.form_setEnd = "Set closing date";
+ Messages.form_removeEnd = "Remove closing date";
+ Messages.form_isOpen = "This form is open";
+ Messages.form_isClosed = "This form was closed on {0}";
+ Messages.form_willClose = "This form will close on {0}";
+
+ Messages.form_anonymous = "Anonymous answers";
+ Messages.form_anonymous_on = "Allowed";
+ Messages.form_anonymous_off = "Blocked";
+ Messages.form_anonymous_blocked = "Anonymous responses are blocked for this form. You must log in or register to submit answers.";
+
+ Messages.form_defaultOption = "Option {0}";
+ Messages.form_defaultItem = "Item {0}";
+ Messages.form_newOption = "New option";
+ Messages.form_newItem = "New item";
+ Messages.form_add_option = "Add option";
+ Messages.form_add_item = "Add item";
+ Messages.form_addMultiple = "Add all";
+ Messages.form_clear = "Clear";
+
+ Messages.form_page_prev = "Previous";
+ Messages.form_page = "Page {0}/{1}";
+ Messages.form_page_next = "Next";
+
+ Messages.form_anonymousBox = "Answer anonymously";
+
+ var MAX_OPTIONS = 15; // XXX
+ var MAX_ITEMS = 10; // XXX
+
+ var saveAndCancelOptions = function (getRes, cb) {
+ // Cancel changes
+ var cancelBlock = h('button.btn.btn-secondary', Messages.cancel);
+ $(cancelBlock).click(function () { cb(); });
+
+ // Save changes
+ var saveBlock = h('button.btn.btn-primary', [
+ h('i.fa.fa-floppy-o'),
+ h('span', Messages.settings_save)
+ ]);
+
+ $(saveBlock).click(function () {
+ $(saveBlock).attr('disabled', 'disabled');
+ cb(getRes());
+ });
+
+ return h('div.cp-form-edit-save', [cancelBlock, saveBlock]);
+ };
+ var editTextOptions = function (opts, setCursorGetter, cb, tmp) {
+ if (tmp && tmp.content && Sortify(opts) === Sortify(tmp.old)) {
+ opts = tmp.content;
+ }
+
+ var maxLength, getLengthVal;
+ if (opts.maxLength) {
+ var lengthInput = h('input', {
+ type:"number",
+ value: opts.maxLength,
+ min: 100,
+ max: 5000
+ });
+ maxLength = h('div.cp-form-edit-max-options', [
+ h('span', Messages.form_editMaxLength),
+ lengthInput
+ ]);
+ getLengthVal = function () {
+ var val = Number($(lengthInput).val()) || 1000;
+ if (val < 1) { val = 1; }
+ if (val > 5000) { val = 5000; }
+ return val;
+ };
+
+ var $l = $(lengthInput).on('input', Util.throttle(function () {
+ $l.val(getLengthVal());
+ }, 500));
+
+ }
+
+ var type, typeSelect;
+ if (opts.type) {
+ var options = ['text', 'number', 'url', 'email'].map(function (t) {
+ return {
+ tag: 'a',
+ attributes: {
+ 'class': 'cp-form-type-value',
+ 'data-value': t,
+ 'href': '#',
+ },
+ content: Messages['form_text_'+t]
+ };
+ });
+ var dropdownConfig = {
+ text: '', // Button initial text
+ options: options, // Entries displayed in the menu
+ //left: true, // Open to the left of the button
+ //container: $(type),
+ isSelect: true,
+ caretDown: true,
+ buttonCls: 'btn btn-secondary'
+ };
+ typeSelect = UIElements.createDropdown(dropdownConfig);
+ typeSelect.setValue(opts.type);
+
+ type = h('div.cp-form-edit-type', [
+ h('span', Messages.form_textType),
+ typeSelect[0]
+ ]);
+ }
+
+ setCursorGetter(function () {
+ return {
+ old: (tmp && tmp.old) || opts,
+ content: {
+ maxLength: getLengthVal ? getLengthVal() : undefined,
+ type: typeSelect ? typeSelect.getValue() : undefined
+ }
+ };
+ });
+
+ var getSaveRes = function () {
+ return {
+ maxLength: getLengthVal ? getLengthVal() : undefined,
+ type: typeSelect ? typeSelect.getValue() : undefined
+ };
+ };
+ var saveAndCancel = saveAndCancelOptions(getSaveRes, cb);
+
+ return [
+ maxLength,
+ type,
+ saveAndCancel
+ ];
+ };
+ var editOptions = function (v, setCursorGetter, cb, tmp) {
+ var add = h('button.btn.btn-secondary', [
+ h('i.fa.fa-plus'),
+ h('span', Messages.form_add_option)
+ ]);
+ var addItem = h('button.btn.btn-secondary', [
+ h('i.fa.fa-plus'),
+ h('span', Messages.form_add_item)
+ ]);
+
+ var cursor;
+ if (tmp && tmp.content && Sortify(v) === Sortify(tmp.old)) {
+ v = tmp.content;
+ cursor = tmp.cursor;
+ }
+
+ var maxOptions, maxInput;
+ if (typeof(v.max) === "number") {
+ maxInput = h('input', {
+ type:"number",
+ value: v.max,
+ min: 1,
+ max: v.values.length
+ });
+ maxOptions = h('div.cp-form-edit-max-options', [
+ h('span', Messages.form_editMax),
+ maxInput
+ ]);
+ }
+
+ var type, typeSelect;
+ if (v.type) {
+ var options = ['text', 'day', 'time'].map(function (t) {
+ return {
+ tag: 'a',
+ attributes: {
+ 'class': 'cp-form-type-value',
+ 'data-value': t,
+ 'href': '#',
+ },
+ content: Messages['form_poll_'+t]
+ };
+ });
+ var dropdownConfig = {
+ text: '', // Button initial text
+ options: options, // Entries displayed in the menu
+ //left: true, // Open to the left of the button
+ //container: $(type),
+ isSelect: true,
+ caretDown: true,
+ buttonCls: 'btn btn-secondary'
+ };
+ typeSelect = UIElements.createDropdown(dropdownConfig);
+ typeSelect.setValue(v.type);
+
+ type = h('div.cp-form-edit-type', [
+ h('span', Messages.form_editType),
+ typeSelect[0]
+ ]);
+ }
+
+ // Show existing options
+ var $add, $addItem;
+ var addMultiple;
+ var getOption = function (val, isItem, uid) {
+ var input = h('input', {value:val});
+ if (uid) { $(input).data('uid', uid); }
+
+ // If the input is a date, initialize flatpickr
+ if (v.type && v.type !== 'text') {
+ if (v.type === 'time') {
+ Flatpickr(input, {
+ enableTime: true,
+ time_24hr: is24h,
+ dateFormat: dateFormat,
+ defaultDate: val ? new Date(val) : undefined
+ });
+ } else if (v.type === 'day') {
+ /*Flatpickr(input, {
+ defaultDate: val ? new Date(val) : undefined
+ });*/
+ }
+ }
+
+ // if this element was active before the remote change, restore cursor
+ var setCursor = function () {
+ if (v.type && v.type !== 'text') { return; }
+ input.selectionStart = cursor.start || 0;
+ input.selectionEnd = cursor.end || 0;
+ setTimeout(function () { input.focus(); });
+ };
+ if (isItem) {
+ if (cursor && cursor.uid === uid && cursor.item) { setCursor(); }
+ } else {
+ if (cursor && cursor.el === val && !cursor.item) { setCursor(); }
+ }
+
+ var del = h('button.btn.btn-danger-outline', h('i.fa.fa-times'));
+ var el = h('div.cp-form-edit-block-input', [
+ h('span.cp-form-handle', [
+ h('i.fa.fa-ellipsis-v'),
+ h('i.fa.fa-ellipsis-v'),
+ ]),
+ input,
+ del
+ ]);
+ $(del).click(function () {
+ $(el).remove();
+ // We've just deleted an item/option so we should be under the MAX limit and
+ // we can show the "add" button again
+ if (isItem && $addItem) { $addItem.show(); }
+ if (!isItem && $add) {
+ $add.show();
+ if (v.type === "time") { $(addMultiple).show(); }
+ }
+ });
+ return el;
+ };
+ var inputs = v.values.map(function (val) { return getOption(val, false); });
+ inputs.push(add);
+
+ var container = h('div.cp-form-edit-block', inputs);
+ var $container = $(container);
+
+ Sortable.create(container, {
+ direction: "vertical",
+ handle: ".cp-form-handle",
+ draggable: ".cp-form-edit-block-input",
+ forceFallback: true,
+ });
+
+ var containerItems;
+ if (v.items) {
+ var inputsItems = v.items.map(function (itemData) {
+ return getOption(itemData.v, true, itemData.uid);
+ });
+ inputsItems.push(addItem);
+ containerItems = h('div.cp-form-edit-block', inputsItems);
+ Sortable.create(containerItems, {
+ direction: "vertical",
+ handle: ".cp-form-handle",
+ draggable: ".cp-form-edit-block-input",
+ forceFallback: true,
+ });
+ }
+
+ // Calendar...
+ var calendarView;
+ if (v.type) {
+ var calendarInput = h('input');
+ calendarView = h('div', calendarInput);
+ var calendarDefault = v.type === "day" ? v.values.map(function (time) {
+ if (!time) { return; }
+ var d = new Date(time);
+ if (!isNaN(d)) { return d; }
+ }).filter(Boolean) : undefined;
+ Flatpickr(calendarInput, {
+ mode: 'multiple',
+ inline: true,
+ defaultDate: calendarDefault,
+ appendTo: calendarView
+ });
+ }
+
+ // Calendar time
+ if (v.type) {
+ var multipleInput = h('input');
+ var multipleClearButton = h('button.btn', Messages.form_clear);
+ var addMultipleButton = h('button.btn', [
+ h('i.fa.fa-plus'),
+ h('span', Messages.form_addMultiple)
+ ]);
+ addMultiple = h('div', { style: "display: none;" }, [
+ multipleInput,
+ addMultipleButton,
+ multipleClearButton
+ ]);
+ var multiplePickr = Flatpickr(multipleInput, {
+ mode: 'multiple',
+ enableTime: true,
+ dateFormat: dateFormat,
+ });
+ $(multipleClearButton).click(function () {
+ multiplePickr.clear();
+ });
+ $(addMultipleButton).click(function () {
+ multiplePickr.selectedDates.some(function (date) {
+ $add.before(getOption(date, false));
+ var l = $container.find('input').length;
+ $(maxInput).attr('max', l);
+ if (l >= MAX_OPTIONS) {
+ $add.hide();
+ $(addMultiple).hide();
+ return true;
+ }
+ });
+ multiplePickr.clear();
+ });
+ }
+
+ var refreshView = function () {
+ if (!v.type) { return; }
+ var $calendar = $(calendarView);
+ if (v.type !== "day") {
+ $calendar.hide();
+ $container.show();
+ var l = $container.find('input').length;
+ if (v.type === "time" && l < MAX_OPTIONS) {
+ $(addMultiple).show();
+ } else {
+ $(addMultiple).hide();
+ }
+ } else {
+ $calendar.show();
+ $container.hide();
+ }
+ };
+ refreshView();
+
+ // Doodle type change: empty current values and change input types?
+ if (typeSelect) {
+ typeSelect.onChange.reg(function (prettyVal, val) {
+ v.type = val;
+ refreshView();
+ if (val !== "text") {
+ $container.find('.cp-form-edit-block-input').remove();
+ return;
+ }
+ $container.find('input').each(function (i, input) {
+ if (input._flatpickr) {
+ input._flatpickr.destroy();
+ delete input._flatpickr;
+ }
+ });
+ });
+ }
+
+ // "Add option" button handler
+ $add = $(add).click(function () {
+ var txt = v.type ? '' : Messages.form_newOption;
+ $add.before(getOption(txt, false));
+ var l = $container.find('input').length;
+ $(maxInput).attr('max', l);
+ if (l >= MAX_OPTIONS) { $add.hide(); }
+ });
+
+ // If multiline block, handle "Add item" button
+ $addItem = $(addItem).click(function () {
+ $addItem.before(getOption(Messages.form_newItem, true, Util.uid()));
+ if ($(containerItems).find('input').length >= MAX_ITEMS) { $addItem.hide(); }
+ });
+ if ($container.find('input').length >= MAX_OPTIONS) { $add.hide(); }
+ if ($(containerItems).find('input').length >= MAX_ITEMS) { $addItem.hide(); }
+
+ // Set cursor getter (to handle remote changes to the form)
+ setCursorGetter(function () {
+ var values = [];
+ var active = document.activeElement;
+ var cursor = {};
+ $container.find('input').each(function (i, el) {
+ if (el === active && !el._flatpickr) {
+ cursor.el= $(el).val();
+ cursor.start = el.selectionStart;
+ cursor.end = el.selectionEnd;
+ }
+ values.push($(el).val());
+ });
+ if (v.type === "day") {
+ var dayPickr = $(calendarView).find('input')[0]._flatpickr;
+ values = dayPickr.selectedDates.map(function (date) {
+ return +date;
+ });
+ }
+ var _content = {values: values};
+
+ if (maxInput) {
+ _content.max = Number($(maxInput).val()) || 1;
+ }
+
+ if (typeSelect) {
+ _content.type = typeSelect.getValue();
+ }
+
+ if (v.items) {
+ var items = [];
+ $(containerItems).find('input').each(function (i, el) {
+ if (el === active) {
+ cursor.item = true;
+ cursor.uid= $(el).data('uid');
+ cursor.start = el.selectionStart;
+ cursor.end = el.selectionEnd;
+ }
+ items.push({
+ uid: $(el).data('uid'),
+ v: $(el).val()
+ });
+ });
+ _content.items = items;
+ }
+ return {
+ old: (tmp && tmp.old) || v,
+ content: _content,
+ cursor: cursor
+ };
+ });
+
+ var getSaveRes = function () {
+ // Get values
+ var values = [];
+ var duplicates = false;
+ if (v.type === "day") {
+ var dayPickr = $(calendarView).find('input')[0]._flatpickr;
+ values = dayPickr.selectedDates.map(function (date) {
+ return +date;
+ });
+ } else {
+ $container.find('input').each(function (i, el) {
+ var val = $(el).val().trim();
+ if (v.type === "day" || v.type === "time") {
+ var f = el._flatpickr;
+ if (f && f.selectedDates && f.selectedDates.length) {
+ val = +f.selectedDates[0];
+ }
+ }
+ if (values.indexOf(val) === -1) { values.push(val); }
+ else { duplicates = true; }
+ });
+ }
+ if (!values.length) {
+ return void UI.warn(Messages.error); // XXX error message: no values
+ }
+ var res = { values: values };
+
+ // If multiline block, get items
+ if (v.items) {
+ var items = [];
+ $(containerItems).find('input').each(function (i, el) {
+ var val = $(el).val().trim();
+ var uid = $(el).data('uid');
+ if (!items.some(function (i) { return i.uid === uid; })) {
+ items.push({
+ uid: $(el).data('uid'),
+ v: val
+ });
+ }
+ else { duplicates = true; }
+ });
+ res.items = items;
+ }
+
+ // Show duplicates warning
+ if (duplicates) {
+ UI.warn(Messages.form_duplicates);
+ }
+
+ // If checkboxes, get the maximum number of values the users can select
+ if (maxInput) {
+ var maxVal = Number($(maxInput).val());
+ if (isNaN(maxVal)) { maxVal = values.length; }
+ res.max = maxVal;
+ }
+
+ if (typeSelect) {
+ res.type = typeSelect.getValue();
+ }
+
+ return res;
+ };
+
+ var saveAndCancel = saveAndCancelOptions(getSaveRes, cb);
+
+ return [
+ type,
+ maxOptions,
+ calendarView,
+ h('div.cp-form-edit-options-block', [containerItems, container]),
+ addMultiple,
+ saveAndCancel
+ ];
+ };
+
+ var makePollTable = function (answers, opts) {
+ // Sort date values
+ if (opts.type !== "text") {
+ opts.values.sort(function (a, b) {
+ return +new Date(a) - +new Date(b);
+ });
+ }
+ // Create first line with options
+ var els = opts.values.map(function (data) {
+ if (opts.type === "day") {
+ var _date = new Date(data);
+ data = _date.toLocaleDateString();
+ }
+ if (opts.type === "time") {
+ var _dateT = new Date(data);
+ data = Flatpickr.formatDate(_dateT, timeFormat);
+ }
+ return h('div.cp-poll-cell.cp-form-poll-option', data);
+ });
+ // Insert axis switch button
+ var switchAxis = h('button.btn.btn-default', [
+ h('i.fa.fa-exchange'),
+ ]);
+ els.unshift(h('div.cp-poll-cell.cp-poll-switch', switchAxis));
+ var lines = [h('div', els)];
+
+ // Add an initial row to "time" values containing the days
+ if (opts.type === "time") {
+ var days = [h('div.cp-poll-cell')];
+ var _days = {};
+ opts.values.forEach(function (d) {
+ var date = new Date(d);
+ var day = date.toLocaleDateString();
+ _days[day] = _days[day] || 0;
+ _days[day]++;
+ });
+ Object.keys(_days).forEach(function (day) {
+ days.push(h('div.cp-poll-cell.cp-poll-time-day', {
+ style: 'flex-grow:'+(_days[day]-1)+';'
+ }, day));
+ });
+ lines.unshift(h('div', days));
+ }
+
+ // Add answers
+ if (Array.isArray(answers)) {
+ answers.forEach(function (answer) {
+ if (!answer.name || !answer.values) { return; }
+ var _name = answer.name;
+ var values = answer.values || {};
+ var els = opts.values.map(function (data) {
+ var res = values[data] || 0;
+ var v = (Number(res) === 1) ? h('i.fa.fa-check.cp-yes') : undefined;
+ var cell = h('div.cp-poll-cell.cp-form-poll-answer', {
+ 'data-value': res
+ }, v);
+ return cell;
+ });
+ els.unshift(h('div.cp-poll-cell.cp-poll-answer-name', _name));
+ lines.push(h('div', els));
+ });
+ }
+
+ var $s = $(switchAxis).click(function () {
+ $s.closest('.cp-form-type-poll').toggleClass('cp-form-poll-switch');
+ });
+
+ return lines;
+ };
+
+ var getEmpty = function (empty) {
+ if (empty) {
+ return UI.setHTML(h('div.cp-form-results-type-text-empty'), Messages._getKey('form_notAnswered', [empty]));
+ }
+ };
+
+ var findItem = function (items, uid) {
+ if (!Array.isArray(items)) { return; }
+ var res;
+ items.some(function (item) {
+ if (item.uid === uid) {
+ res = item.v;
+ return true;
+ }
+ });
+ return res;
+ };
+
+ var getBlockAnswers = function (answers, uid, filterCurve) {
+ return Object.keys(answers || {}).map(function (user) {
+ if (filterCurve && user === filterCurve) { return; }
+ try {
+ return answers[user].msg[uid];
+ } catch (e) { console.error(e); }
+ }).filter(Boolean);
+ };
+
+ var STATIC_TYPES = {
+ md: {
+ defaultOpts: {
+ text: Messages.form_description_default
+ },
+ get: function (opts) {
+ if (!opts) { opts = STATIC_TYPES.md.defaultOpts; }
+ var tag = h('div', {
+ id: 'form'+Util.uid()
+ }, opts.text);
+ var $tag = $(tag);
+ DiffMd.apply(DiffMd.render(opts.text || ''), $tag, APP.common);
+ var cursorGetter;
+ return {
+ tag: tag,
+ edit: function (cb, tmp) {
+ var t = h('textarea');
+ var block = h('div.cp-form-edit-options-block', [t]);
+ var cm = SFCodeMirror.create("gfm", CMeditor, t);
+ var editor = cm.editor;
+ editor.setOption('lineNumbers', true);
+ editor.setOption('lineWrapping', true);
+ editor.setOption('styleActiveLine', true);
+ editor.setOption('readOnly', false);
+
+ var text = opts.text;
+ var cursor;
+ if (tmp && tmp.content && tmp.old.text === text) {
+ text = tmp.content.text;
+ cursor = tmp.cursor;
+ }
+
+ setTimeout(function () {
+ editor.setValue(text);
+ if (cursor) {
+ if (Sortify(cursor.start) === Sortify(cursor.end)) {
+ editor.setCursor(cursor.start);
+ } else {
+ editor.setSelection(cursor.start, cursor.end);
+ }
+ }
+ editor.refresh();
+ editor.save();
+ editor.focus();
+ });
+ if (APP.common) {
+ var markdownTb = APP.common.createMarkdownToolbar(editor, {
+ embed: function (mt) {
+ editor.focus();
+ editor.replaceSelection($(mt)[0].outerHTML);
+ }
+ });
+ $(block).prepend(markdownTb.toolbar);
+ $(markdownTb.toolbar).show();
+ cm.configureTheme(APP.common, function () {});
+ }
+ // Cancel changes
+ var cancelBlock = h('button.btn.btn-secondary', Messages.cancel);
+ $(cancelBlock).click(function () {
+ cb();
+ });
+ // Save changes
+ var saveBlock = h('button.btn.btn-primary', [
+ h('i.fa.fa-floppy-o'),
+ h('span', Messages.settings_save)
+ ]);
+
+ var getContent = function () {
+ return {
+ text: editor.getValue()
+ };
+ };
+ $(saveBlock).click(function () {
+ $(saveBlock).attr('disabled', 'disabled');
+ cb(getContent());
+ });
+
+ cursorGetter = function () {
+ if (document.activeElement && block.contains(document.activeElement)) {
+ cursor = {
+ start: editor.getCursor('from'),
+ end: editor.getCursor('to')
+ };
+ }
+ return {
+ old: opts,
+ content: getContent(),
+ cursor: cursor
+ };
+ };
+
+ return [
+ block,
+ h('div.cp-form-edit-save', [cancelBlock, saveBlock])
+ ];
+ },
+ getCursor: function () { return cursorGetter(); },
+ };
+ },
+ printResults: function () { return; },
+ icon: h('i.cptools.cptools-form-paragraph')
+ },
+ page: {
+ get: function () {
+ var tag = h('div.cp-form-page-break-edit', [
+ h('i.cptools.cptools-form-page-break'),
+ h('span', Messages.form_type_page)
+ ]);
+ return {
+ tag: tag,
+ pageBreak: true
+ };
+ },
+ printResults: function () { return; },
+ icon: h('i.cptools.cptools-form-page-break')
+ },
+ };
+ var TYPES = {
+ input: {
+ defaultOpts: {
+ type: 'text'
+ },
+ get: function (opts, a, n, evOnChange) {
+ if (!opts) { opts = TYPES.input.defaultOpts; }
+ var tag = h('input', {
+ type: opts.type
+ });
+ var $tag = $(tag);
+ $tag.on('change keypress', Util.throttle(function () {
+ evOnChange.fire();
+ }, 500));
+ var cursorGetter;
+ var setCursorGetter = function (f) { cursorGetter = f; };
+ return {
+ tag: tag,
+ getValue: function () {
+ var invalid = $tag.is(':invalid');
+ if (invalid) { return; } // XXX invalid answers are ignored?
+ return $tag.val();
+ },
+ setValue: function (val) { $tag.val(val); },
+ edit: function (cb, tmp) {
+ var v = Util.clone(opts);
+ return editTextOptions(v, setCursorGetter, cb, tmp);
+ },
+ getCursor: function () { return cursorGetter(); },
+ reset: function () { $tag.val(''); }
+ };
+ },
+ printResults: function (answers, uid) {
+ var results = [];
+ var empty = 0;
+ Object.keys(answers).forEach(function (author) {
+ var obj = answers[author];
+ var answer = obj.msg[uid];
+ if (!answer || !answer.trim()) { return empty++; }
+ results.push(h('div.cp-form-results-type-text-data', answer));
+ });
+ results.push(getEmpty(empty));
+
+ return h('div.cp-form-results-type-text', results);
+ },
+ icon: h('i.cptools.cptools-form-text')
+ },
+ textarea: {
+ defaultOpts: {
+ maxLength: 1000
+ },
+ get: function (opts, a, n, evOnChange) {
+ if (!opts) { opts = TYPES.textarea.defaultOpts; }
+ var tag = h('textarea', {maxlength: opts.maxLength});
+ var $tag = $(tag);
+ $tag.on('change keypress', Util.throttle(function () {
+ evOnChange.fire();
+ }, 500));
+ var cursorGetter;
+ var setCursorGetter = function (f) { cursorGetter = f; };
+ return {
+ tag: tag,
+ getValue: function () { return $tag.val(); },
+ setValue: function (val) { $tag.val(val); },
+ edit: function (cb, tmp) {
+ var v = Util.clone(opts);
+ return editTextOptions(v, setCursorGetter, cb, tmp);
+ },
+ getCursor: function () { return cursorGetter(); },
+ reset: function () { $tag.val(''); }
+ };
+ },
+ printResults: function (answers, uid) {
+ var results = [];
+ var empty = 0;
+ Object.keys(answers).forEach(function (author) {
+ var obj = answers[author];
+ var answer = obj.msg[uid];
+ if (!answer || !answer.trim()) { return empty++; }
+ results.push(h('div.cp-form-results-type-textarea-data', answer));
+ });
+ results.push(getEmpty(empty));
+
+ return h('div.cp-form-results-type-text', results);
+ },
+ icon: h('i.cptools.cptools-form-paragraph')
+ },
+ radio: {
+ defaultOpts: {
+ values: [1,2].map(function (i) {
+ return Messages._getKey('form_defaultOption', [i]);
+ })
+ },
+ get: function (opts, a, n, evOnChange) {
+ if (!opts) { opts = TYPES.radio.defaultOpts; }
+ if (!Array.isArray(opts.values)) { return; }
+ var name = Util.uid();
+ var els = opts.values.map(function (data, i) {
+ var radio = UI.createRadio(name, 'cp-form-'+name+'-'+i,
+ data, false, { mark: { tabindex:1 } });
+ $(radio).find('input').data('val', data);
+ return radio;
+ });
+ var tag = h('div.radio-group.cp-form-type-radio', els);
+ var cursorGetter;
+ var setCursorGetter = function (f) { cursorGetter = f; };
+ $(tag).find('input[type="radio"]').on('change', function () {
+ evOnChange.fire();
+ });
+ return {
+ tag: tag,
+ getValue: function () {
+ var res;
+ els.some(function (el) {
+ var $i = $(el).find('input');
+ if (Util.isChecked($i)) {
+ res = $i.data('val');
+ return true;
+ }
+ });
+ return res;
+ },
+ reset: function () { $(tag).find('input').removeAttr('checked'); },
+ edit: function (cb, tmp) {
+ var v = Util.clone(opts);
+ return editOptions(v, setCursorGetter, cb, tmp);
+ },
+ getCursor: function () { return cursorGetter(); },
+ setValue: function (val) {
+ this.reset();
+ els.some(function (el) {
+ var $el = $(el).find('input');
+ if ($el.data('val') === val) {
+ $el.prop('checked', true);
+ return true;
+ }
+ });
+ }
+ };
+
+ },
+ printResults: function (answers, uid) {
+ var results = [];
+ var empty = 0;
+ var count = {};
+ Object.keys(answers).forEach(function (author) {
+ var obj = answers[author];
+ var answer = obj.msg[uid];
+ if (!answer || !answer.trim()) { return empty++; }
+ count[answer] = count[answer] || 0;
+ count[answer]++;
+ });
+ Object.keys(count).forEach(function (value) {
+ results.push(h('div.cp-form-results-type-radio-data', [
+ h('span.cp-value', value),
+ h('span.cp-count', count[value])
+ ]));
+ });
+ results.push(getEmpty(empty));
+
+ return h('div.cp-form-results-type-radio', results);
+ },
+ icon: h('i.cptools.cptools-form-list-radio')
+ },
+ multiradio: {
+ defaultOpts: {
+ items: [1,2].map(function (i) {
+ return {
+ uid: Util.uid(),
+ v: Messages._getKey('form_defaultItem', [i])
+ };
+ }),
+ values: [1,2].map(function (i) {
+ return Messages._getKey('form_defaultOption', [i]);
+ })
+ },
+ get: function (opts, a, n, evOnChange) {
+ if (!opts) { opts = TYPES.multiradio.defaultOpts; }
+ if (!Array.isArray(opts.items) || !Array.isArray(opts.values)) { return; }
+ var lines = opts.items.map(function (itemData) {
+ var name = itemData.uid;
+ var item = itemData.v;
+ var els = opts.values.map(function (data, i) {
+ var radio = UI.createRadio(name, 'cp-form-'+name+'-'+i,
+ '', false, { mark: { tabindex:1 } });
+ $(radio).find('input').data('uid', name);
+ $(radio).find('input').data('val', data);
+ return radio;
+ });
+ els.unshift(h('div.cp-form-multiradio-item', item));
+ return h('div.radio-group', {'data-uid':name}, els);
+ });
+ var header = opts.values.map(function (v) { return h('span', v); });
+ header.unshift(h('span'));
+ lines.unshift(h('div.cp-form-multiradio-header', header));
+
+ var tag = h('div.radio-group.cp-form-type-multiradio', lines);
+ var cursorGetter;
+ var setCursorGetter = function (f) { cursorGetter = f; };
+ $(tag).find('input[type="radio"]').on('change', function () {
+ evOnChange.fire();
+ });
+ return {
+ tag: tag,
+ getValue: function () {
+ var res = {};
+ var l = lines.slice(1);
+ l.forEach(function (el) {
+ var $el = $(el);
+ var uid = $el.attr('data-uid');
+ $el.find('input').each(function (i, input) {
+ var $i = $(input);
+ if (res[uid]) { return; }
+ if (Util.isChecked($i)) { res[uid] = $i.data('val'); }
+ });
+ });
+ return res;
+ },
+ reset: function () { $(tag).find('input').removeAttr('checked'); },
+ edit: function (cb, tmp) {
+ var v = Util.clone(opts);
+ return editOptions(v, setCursorGetter, cb, tmp);
+ },
+ getCursor: function () { return cursorGetter(); },
+ setValue: function (val) {
+ this.reset();
+ Object.keys(val || {}).forEach(function (uid) {
+ $(tag).find('[name="'+uid+'"]').each(function (i, el) {
+ if ($(el).data('val') !== val[uid]) { return; }
+ $(el).prop('checked', true);
+ });
+ });
+ }
+ };
+
+ },
+ printResults: function (answers, uid, form) {
+ var structure = form[uid];
+ if (!structure) { return; }
+ var results = [];
+ var empty = 0;
+ var count = {};
+ Object.keys(answers).forEach(function (author) {
+ var obj = answers[author];
+ var answer = obj.msg[uid];
+ if (!answer || !Object.keys(answer).length) { return empty++; }
+ //count[answer] = count[answer] || {};
+ Object.keys(answer).forEach(function (q_uid) {
+ var c = count[q_uid] = count[q_uid] || {};
+ var res = answer[q_uid];
+ if (!res || !res.trim()) { return; }
+ c[res] = c[res] || 0;
+ c[res]++;
+ });
+ });
+ Object.keys(count).forEach(function (q_uid) {
+ var opts = structure.opts || TYPES.multiradio.defaultOpts;
+ var q = findItem(opts.items, q_uid);
+ var c = count[q_uid];
+ var values = Object.keys(c).map(function (res) {
+ return h('div.cp-form-results-type-radio-data', [
+ h('span.cp-value', res),
+ h('span.cp-count', c[res])
+ ]);
+ });
+ results.push(h('div.cp-form-results-type-multiradio-data', [
+ h('span.cp-mr-q', q),
+ h('span.cp-mr-value', values)
+ ]));
+ });
+ results.push(getEmpty(empty));
+
+ return h('div.cp-form-results-type-radio', results);
+ },
+ icon: h('i.cptools.cptools-form-grid-radio')
+ },
+ checkbox: {
+ defaultOpts: {
+ max: 3,
+ values: [1, 2, 3].map(function (i) {
+ return Messages._getKey('form_defaultOption', [i]);
+ })
+ },
+ get: function (opts, a, n, evOnChange) {
+ if (!opts) { opts = TYPES.checkbox.defaultOpts; }
+ if (!Array.isArray(opts.values)) { return; }
+ var name = Util.uid();
+ var els = opts.values.map(function (data, i) {
+ var cbox = UI.createCheckbox('cp-form-'+name+'-'+i,
+ data, false, { mark: { tabindex:1 } });
+ $(cbox).find('input').data('val', data);
+ return cbox;
+ });
+ var tag = h('div', [
+ h('div.cp-form-max-options', Messages._getKey('form_maxOptions', [opts.max])),
+ h('div.radio-group.cp-form-type-checkbox', els)
+ ]);
+ var $tag = $(tag);
+ $tag.find('input').on('change', function () {
+ var selected = $tag.find('input:checked').length;
+ if (selected >= opts.max) {
+ $tag.find('input:not(:checked)').attr('disabled', 'disabled');
+ } else {
+ $tag.find('input').removeAttr('disabled');
+ }
+ evOnChange.fire();
+ });
+ var cursorGetter;
+ var setCursorGetter = function (f) { cursorGetter = f; };
+ return {
+ tag: tag,
+ getValue: function () {
+ var res = [];
+ els.forEach(function (el) {
+ var $i = $(el).find('input');
+ if (Util.isChecked($i)) {
+ res.push($i.data('val'));
+ }
+ });
+ return res;
+ },
+ reset: function () { $(tag).find('input').removeAttr('checked'); },
+ edit: function (cb, tmp) {
+ var v = Util.clone(opts);
+ return editOptions(v, setCursorGetter, cb, tmp);
+ },
+ getCursor: function () { return cursorGetter(); },
+ setValue: function (val) {
+ this.reset();
+ if (!Array.isArray(val)) { return; }
+ els.forEach(function (el) {
+ var $el = $(el).find('input');
+ if (val.indexOf($el.data('val')) !== -1) {
+ $el.prop('checked', true);
+ }
+ });
+ }
+ };
+
+ },
+ printResults: function (answers, uid) {
+ var results = [];
+ var empty = 0;
+ var count = {};
+ Object.keys(answers).forEach(function (author) {
+ var obj = answers[author];
+ var answer = obj.msg[uid];
+ if (!Array.isArray(answer) || !answer.length) { return empty++; }
+ answer.forEach(function (val) {
+ count[val] = count[val] || 0;
+ count[val]++;
+ });
+ });
+ Object.keys(count).forEach(function (value) {
+ results.push(h('div.cp-form-results-type-radio-data', [
+ h('span.cp-value', value),
+ h('span.cp-count', count[value])
+ ]));
+ });
+ results.push(getEmpty(empty));
+
+ return h('div.cp-form-results-type-radio', results);
+ },
+ icon: h('i.cptools.cptools-form-list-check')
+ },
+ multicheck: {
+ defaultOpts: {
+ max: 3,
+ items: [1,2].map(function (i) {
+ return {
+ uid: Util.uid(),
+ v: Messages._getKey('form_defaultItem', [i])
+ };
+ }),
+ values: [1,2,3].map(function (i) {
+ return Messages._getKey('form_defaultOption', [i]);
+ })
+ },
+ get: function (opts, a, n, evOnChange) {
+ if (!opts) { opts = TYPES.multicheck.defaultOpts; }
+ if (!Array.isArray(opts.items) || !Array.isArray(opts.values)) { return; }
+ var lines = opts.items.map(function (itemData) {
+ var name = itemData.uid;
+ var item = itemData.v;
+ var els = opts.values.map(function (data, i) {
+ var cbox = UI.createCheckbox('cp-form-'+name+'-'+i,
+ '', false, { mark: { tabindex:1 } });
+ $(cbox).find('input').data('uid', name);
+ $(cbox).find('input').data('val', data);
+ return cbox;
+ });
+ els.unshift(h('div.cp-form-multiradio-item', item));
+ return h('div.radio-group', {'data-uid':name}, els);
+ });
+
+ lines.forEach(function (l) {
+ $(l).find('input').on('change', function () {
+ var selected = $(l).find('input:checked').length;
+ if (selected >= opts.max) {
+ $(l).find('input:not(:checked)').attr('disabled', 'disabled');
+ } else {
+ $(l).find('input').removeAttr('disabled');
+ }
+ evOnChange.fire();
+ });
+ });
+
+ var header = opts.values.map(function (v) { return h('span', v); });
+ header.unshift(h('span'));
+ lines.unshift(h('div.cp-form-multiradio-header', header));
+
+ var tag = h('div.radio-group.cp-form-type-multiradio', lines);
+ var cursorGetter;
+ var setCursorGetter = function (f) { cursorGetter = f; };
+ return {
+ tag: tag,
+ getValue: function () {
+ var res = {};
+ var l = lines.slice(1);
+ l.forEach(function (el) {
+ var $el = $(el);
+ var uid = $el.attr('data-uid');
+ res[uid] = [];
+ $el.find('input').each(function (i, input) {
+ var $i = $(input);
+ if (Util.isChecked($i)) { res[uid].push($i.data('val')); }
+ });
+ });
+ return res;
+ },
+ reset: function () { $(tag).find('input').removeAttr('checked'); },
+ edit: function (cb, tmp) {
+ var v = Util.clone(opts);
+ return editOptions(v, setCursorGetter, cb, tmp);
+ },
+ getCursor: function () { return cursorGetter(); },
+ setValue: function (val) {
+ this.reset();
+ Object.keys(val || {}).forEach(function (uid) {
+ if (!Array.isArray(val[uid])) { return; }
+ $(tag).find('[data-uid="'+uid+'"] input').each(function (i, el) {
+ if (val[uid].indexOf($(el).data('val')) === -1) { return; }
+ $(el).prop('checked', true);
+ });
+ });
+ }
+ };
+
+ },
+ printResults: function (answers, uid, form) {
+ var structure = form[uid];
+ if (!structure) { return; }
+ var results = [];
+ var empty = 0;
+ var count = {};
+ Object.keys(answers).forEach(function (author) {
+ var obj = answers[author];
+ var answer = obj.msg[uid];
+ if (!answer || !Object.keys(answer).length) { return empty++; }
+ Object.keys(answer).forEach(function (q_uid) {
+ var c = count[q_uid] = count[q_uid] || {};
+ var res = answer[q_uid];
+ if (!Array.isArray(res) || !res.length) { return; }
+ res.forEach(function (v) {
+ c[v] = c[v] || 0;
+ c[v]++;
+ });
+ });
+ });
+ Object.keys(count).forEach(function (q_uid) {
+ var q = findItem(structure.opts.items, q_uid);
+ var c = count[q_uid];
+ var values = Object.keys(c).map(function (res) {
+ return h('div.cp-form-results-type-radio-data', [
+ h('span.cp-value', res),
+ h('span.cp-count', c[res])
+ ]);
+ });
+ results.push(h('div.cp-form-results-type-multiradio-data', [
+ h('span.cp-mr-q', q),
+ h('span.cp-mr-value', values)
+ ]));
+ });
+ results.push(getEmpty(empty));
+
+ return h('div.cp-form-results-type-radio', results);
+ },
+ icon: h('i.cptools.cptools-form-grid-check')
+ },
+ poll: {
+ defaultOpts: {
+ type: 'text', // Text or Days or Time
+ values: [1, 2, 3].map(function (i) {
+ return Messages._getKey('form_defaultOption', [i]);
+ })
+ },
+ get: function (opts, answers, username, evOnChange) {
+ if (!opts) { opts = TYPES.poll.defaultOpts; }
+ if (!Array.isArray(opts.values)) { return; }
+
+ var lines = makePollTable(answers, opts);
+
+ // Add form
+ var addLine = opts.values.map(function (data) {
+ var cell = h('div.cp-poll-cell.cp-form-poll-choice', [
+ h('i.fa.fa-times.cp-no'),
+ h('i.fa.fa-check.cp-yes'),
+ h('i.fa.fa-question.cp-maybe'),
+ ]);
+ var $c = $(cell);
+ $c.data('option', data);
+ var val = 0;
+ $c.attr('data-value', val);
+ $c.click(function () {
+ val = (val+1)%3;
+ $c.attr('data-value', val);
+ evOnChange.fire();
+ });
+ cell._setValue = function (v) {
+ val = v;
+ $c.attr('data-value', val);
+ };
+ return cell;
+ });
+ // Name input
+ var nameInput = h('input', { value: username || Messages.anonymous });
+ addLine.unshift(h('div.cp-poll-cell', nameInput));
+ lines.push(h('div', addLine));
+
+ var tag = h('div.cp-form-type-poll', lines);
+ var $tag = $(tag);
+
+ var cursorGetter;
+ var setCursorGetter = function (f) { cursorGetter = f; };
+ return {
+ tag: tag,
+ getValue: function () {
+ var res = {};
+ var name = $(nameInput).val().trim() || Messages.anonymous;
+ $tag.find('.cp-form-poll-choice').each(function (i, el) {
+ var $el = $(el);
+ res[$el.data('option')] = $el.attr('data-value');
+ });
+ return {
+ name: name,
+ values: res
+ };
+ },
+ reset: function () {
+ $tag.find('.cp-form-poll-choice').attr('data-value', 0);
+ },
+ edit: function (cb, tmp) {
+ var v = Util.clone(opts);
+ return editOptions(v, setCursorGetter, cb, tmp);
+ },
+ getCursor: function () { return cursorGetter(); },
+ setValue: function (res) {
+ this.reset();
+ if (!res || !res.values || !res.name) { return; }
+ var val = res.values;
+ $(nameInput).val(res.name);
+ $tag.find('.cp-form-poll-choice').each(function (i, el) {
+ if (!el._setValue) { return; }
+ var $el = $(el);
+ el._setValue(val[$el.data('option')] || 0);
+ });
+ }
+ };
+
+ },
+ printResults: function (answers, uid, form) {
+ var _answers = getBlockAnswers(answers, uid);
+ var lines = makePollTable(_answers, form[uid].opts);
+ return h('div.cp-form-type-poll', lines);
+ },
+ icon: h('i.cptools.cptools-form-poll')
+ },
+ sort: {
+ defaultOpts: {
+ values: [1,2].map(function (i) {
+ return Messages._getKey('form_defaultOption', [i]);
+ })
+ },
+ get: function (opts, a, n, evOnChange) {
+ if (!opts) { opts = TYPES.radio.defaultOpts; }
+ if (!Array.isArray(opts.values)) { return; }
+ var map = {};
+ var invMap = {};
+ var els = opts.values.map(function (data, i) {
+ var uid = Util.uid();
+ map[uid] = data;
+ invMap[data] = uid;
+ var div = h('div.cp-form-type-sort', {'data-id': uid}, [
+ h('span.cp-form-handle', [
+ h('i.fa.fa-ellipsis-v'),
+ h('i.fa.fa-ellipsis-v'),
+ ]),
+ h('span.cp-form-sort-order', (i+1)),
+ h('span', data)
+ ]);
+ $(div).data('val', data);
+ return div;
+ });
+ var tag = h('div.cp-form-type-sort-container', els);
+ var $tag = $(tag);
+ var reorder = function () {
+ $tag.find('.cp-form-type-sort').each(function (i, el) {
+ $(el).find('.cp-form-sort-order').text(i+1);
+ });
+ };
+ var cursorGetter;
+ var setCursorGetter = function (f) { cursorGetter = f; };
+
+ var sortable = Sortable.create(tag, {
+ direction: "vertical",
+ draggable: ".cp-form-type-sort",
+ forceFallback: true,
+ store: {
+ set: function () {
+ evOnChange.fire();
+ reorder();
+ }
+ }
+ });
+
+ $(tag).find('input[type="radio"]').on('change', function () {
+ evOnChange.fire();
+ });
+ return {
+ tag: tag,
+ getValue: function () {
+ return sortable.toArray().map(function (id) {
+ return map[id];
+ });
+ },
+ reset: function () {
+ var toSort = (opts.values).map(function (val) {
+ return invMap[val];
+ });
+ sortable.sort(toSort);
+ reorder();
+ },
+ edit: function (cb, tmp) {
+ var v = Util.clone(opts);
+ return editOptions(v, setCursorGetter, cb, tmp);
+ },
+ getCursor: function () { return cursorGetter(); },
+ setValue: function (val) {
+ var toSort = (val || []).map(function (val) {
+ return invMap[val];
+ });
+ sortable.sort(toSort);
+ reorder();
+ }
+ };
+
+ },
+ printResults: function (answers, uid, form) {
+ var opts = form[uid].opts || TYPES.radio.defaultOpts;
+ var l = (opts.values || []).length;
+ var results = [];
+ var empty = 0;
+ var count = {};
+ Object.keys(answers).forEach(function (author) {
+ var obj = answers[author];
+ var answer = obj.msg[uid];
+ if (!Array.isArray(answer) || !answer.length) { return empty++; }
+ answer.forEach(function (el, i) {
+ var score = l - i;
+ count[el] = (count[el] || 0) + score;
+ });
+ });
+ var sorted = Object.keys(count).sort(function (a, b) {
+ return count[b] - count[a];
+ });
+ sorted.forEach(function (value) {
+ results.push(h('div.cp-form-results-type-radio-data', [
+ h('span.cp-value', value),
+ h('span.cp-count', count[value])
+ ]));
+ });
+ results.push(getEmpty(empty));
+
+ return h('div.cp-form-results-type-radio', results);
+ },
+ icon: h('i.cptools.cptools-form-list-ordered')
+ },
+ };
+
+ var renderResults = function (content, answers) {
+ var $container = $('div.cp-form-creator-results').empty();
+ var controls = h('div.cp-form-creator-results-controls');
+ var $controls = $(controls).appendTo($container);
+ var results = h('div.cp-form-creator-results-content');
+ var $results = $(results).appendTo($container);
+
+ var summary = true;
+ var form = content.form;
+
+ var switchMode = h('button.btn.btn-primary', Messages.form_showIndividual);
+ $controls.hide().append(switchMode);
+
+ var show = function (answers, header) {
+ var elements = content.order.map(function (uid) {
+ var block = form[uid];
+ var type = block.type;
+ var model = TYPES[type];
+ if (!model || !model.printResults) { return; }
+ var print = model.printResults(answers, uid, form);
+
+ var q = h('div.cp-form-block-question', block.q || Messages.form_default);
+ return h('div.cp-form-block', [
+ h('div.cp-form-block-type', [
+ TYPES[type].icon.cloneNode(),
+ h('span', Messages['form_type_'+type])
+ ]),
+ q,
+ h('div.cp-form-block-content', print),
+ ]);
+ });
+ $results.empty().append(elements);
+ if (header) { $results.prepend(header); }
+ };
+ show(answers);
+
+ if (APP.isEditor || APP.isAuditor) { $controls.show(); }
+
+ var $s = $(switchMode).click(function () {
+ $results.empty();
+ if (!summary) {
+ $s.text(Messages.form_showIndividual);
+ summary = true;
+ show(answers);
+ return;
+ }
+ summary = false;
+ $s.text(Messages.form_showSummary);
+
+ var origin, priv;
+ if (APP.common) {
+ var metadataMgr = APP.common.getMetadataMgr();
+ priv = metadataMgr.getPrivateData();
+ origin = priv.origin;
+ }
+ var getHref = function (hash) {
+ if (APP.common) {
+ return origin + Hash.hashToHref(hash, 'profile');
+ }
+ return '#';
+ };
+
+ var els = Object.keys(answers).map(function (curve) {
+ var obj = answers[curve];
+ var answer = obj.msg;
+ var date = new Date(obj.time).toLocaleString();
+ var text, warning, badge;
+ if (!answer._userdata || !answer._userdata.name) {
+ text = Messages._getKey('form_answerAnonymous', [date]);
+ } else {
+ var ud = answer._userdata;
+ var user;
+ if (ud.profile) {
+ if (priv && priv.friends[curve]) {
+ badge = h('span.cp-form-friend', [
+ h('i.fa.fa-address-book'),
+ Messages._getKey('isContact', [ud.name || Messages.anonymous])
+ ]);
+ }
+ user = h('a', {
+ href: getHref(ud.profile) // Only used visually
+ }, Util.fixHTML(ud.name || Messages.anonymous));
+ if (curve !== ud.curvePublic) {
+ warning = h('span.cp-form-warning', Messages.form_answerWarning);
+ }
+ } else {
+ user = h('b', Util.fixHTML(ud.name || Messages.anonymous));
+ }
+ text = Messages._getKey('form_answerName', [user.outerHTML, date]);
+ }
+ var span = UI.setHTML(h('span'), text);
+ var viewButton = h('button.btn.btn-secondary.small', Messages.form_viewButton);
+ var div = h('div.cp-form-individual', [span, viewButton, warning, badge]);
+ $(viewButton).click(function () {
+ var res = {};
+ res[curve] = obj;
+ var back = h('button.btn.btn-secondary.small', Messages.form_backButton);
+ $(back).click(function () {
+ summary = true;
+ $s.click();
+ });
+ var header = h('div.cp-form-individual', [
+ span.cloneNode(true),
+ back
+ ]);
+ show(res, header);
+ });
+ $(div).find('a').click(function (e) {
+ e.preventDefault();
+ APP.common.openURL(Hash.hashToHref(ud.profile, 'profile'));
+ });
+ return div;
+ });
+ $results.append(els);
+ });
+ };
+
+ var addResultsButton = function (framework, content) {
+ var $res = $(h('button.cp-toolbar-appmenu', [
+ h('i.fa.fa-bar-chart'),
+ h('span.cp-button-name', Messages.form_results)
+ ]));
+ $res.click(function () {
+ $res.attr('disabled', 'disabled');
+ var sframeChan = framework._.sfCommon.getSframeChannel();
+ sframeChan.query("Q_FORM_FETCH_ANSWERS", content.answers, function (err, obj) {
+ var answers = obj && obj.results;
+ if (answers) { APP.answers = answers; }
+ $res.removeAttr('disabled');
+ $('body').addClass('cp-app-form-results');
+ renderResults(content, answers);
+ $res.remove();
+ var $editor = $(h('button.cp-toolbar-appmenu', [
+ h('i.fa.fa-pencil'),
+ h('span.cp-button-name', APP.isEditor ? Messages.form_editor : Messages.form_form)
+ ]));
+ $editor.click(function () {
+ $('body').removeClass('cp-app-form-results');
+ $editor.remove();
+ addResultsButton(framework, content);
+ });
+ framework._.toolbar.$bottomL.append($editor);
+ });
+
+ });
+ framework._.toolbar.$bottomL.append($res);
+ };
+
+ var getFormResults = function () {
+ if (!Array.isArray(APP.formBlocks)) { return; }
+ var results = {};
+ APP.formBlocks.forEach(function (data) {
+ if (!data.getValue) { return; }
+ results[data.uid] = data.getValue();
+ });
+ return results;
+ };
+ var makeFormControls = function (framework, content, update) {
+ var loggedIn = framework._.sfCommon.isLoggedIn();
+ var metadataMgr = framework._.cpNfInner.metadataMgr;
+
+ if (!loggedIn && !content.answers.anonymous) { return; }
+
+ var cbox;
+ cbox = UI.createCheckbox('cp-form-anonymous',
+ Messages.form_anonymousBox, true, { mark: { tabindex:1 } });
+ if (loggedIn) {
+ if (!content.answers.anonymous || APP.cantAnon) {
+ $(cbox).hide().find('input').attr('disabled', 'disabled').prop('checked', false);
+ }
+ }
+
+ var send = h('button.cp-open.btn.btn-primary', update ? Messages.form_update : Messages.form_submit);
+ var reset = h('button.cp-open.btn.btn-danger-alt', Messages.form_reset);
+ $(reset).click(function () {
+ if (!Array.isArray(APP.formBlocks)) { return; }
+ APP.formBlocks.forEach(function (data) {
+ if (typeof(data.reset) === "function") { data.reset(); }
+ });
+ });
+ var $send = $(send).click(function () {
+ $send.attr('disabled', 'disabled');
+ var results = getFormResults();
+ if (!results) { return; }
+
+ var user = metadataMgr.getUserData();
+ if (!Util.isChecked($(cbox).find('input'))) {
+ results._userdata = loggedIn ? {
+ avatar: user.avatar,
+ name: user.name,
+ notifications: user.notifications,
+ curvePublic: user.curvePublic,
+ profile: user.profile
+ } : { name: user.name };
+ }
+
+ var sframeChan = framework._.sfCommon.getSframeChannel();
+ sframeChan.query('Q_FORM_SUBMIT', {
+ mailbox: content.answers,
+ results: results,
+ anonymous: !loggedIn || Util.isChecked($(cbox).find('input'))
+ }, function (err, data) {
+ $send.attr('disabled', 'disabled');
+ if (err || (data && data.error)) {
+ if (data.error === "EANSWERED") {
+ return void UI.warn(Messages.form_answered);
+ }
+ console.error(err || data.error);
+ return void UI.warn(Messages.error);
+ }
+ if (!update) {
+ // Add results button
+ addResultsButton(framework, content);
+ }
+ $send.removeAttr('disabled');
+ UI.alert(Messages.form_sent);
+ $send.text(Messages.form_update);
+ });
+ });
+
+ if (APP.isClosed) {
+ send = undefined;
+ reset = undefined;
+ }
+
+ return h('div.cp-form-send-container', [
+ cbox ? h('div', cbox) : undefined,
+ send, reset
+ ]);
+ };
+ var updateForm = function (framework, content, editable, answers, temp) {
+ var $container = $('div.cp-form-creator-content');
+ if (!$container.length) { return; } // Not ready
+
+ var form = content.form;
+
+ APP.formBlocks = [];
+
+ var evOnChange = Util.mkEvent();
+ if (!APP.isEditor) {
+ var _answers = Util.clone(answers || {});
+ delete _answers._proof;
+ delete _answers._userdata;
+ evOnChange.reg(function () {
+ var results = getFormResults();
+ if (!answers || Sortify(_answers) !== Sortify(results)) {
+ window.onbeforeunload = function () {
+ return true;
+ };
+ } else {
+ window.onbeforeunload = undefined;
+ }
+ });
+ }
+
+
+ var getFormCreator = function (uid) {
+ if (!APP.isEditor) { return; }
+ var full = !uid;
+ var idx = content.order.indexOf(uid);
+ var addControl = function (type) {
+ var btn = h('button.btn.btn-default', {
+ title: full ? undefined : Messages['form_type_'+type]
+ }, [
+ (TYPES[type] || STATIC_TYPES[type]).icon.cloneNode(),
+ full ? h('span', Messages['form_type_'+type]) : undefined
+ ]);
+ $(btn).click(function () {
+ var uid = Util.uid();
+ content.form[uid] = {
+ //q: Messages.form_default,
+ //opts: opts
+ type: type,
+ };
+ if (full) {
+ content.order.push(uid);
+ } else {
+ content.order.splice(idx, 0, uid);
+ }
+ framework.localChange();
+ updateForm(framework, content, true);
+ });
+ return btn;
+ };
+
+ var controls = Object.keys(TYPES).map(addControl);
+ var staticControls = Object.keys(STATIC_TYPES).map(addControl);
+
+ var buttons = h('div.cp-form-creator-control-inline', [
+ h('div.cp-form-creator-types', controls),
+ h('div.cp-form-creator-types', staticControls)
+ ]);
+ var add = h('div', [h('i.fa.fa-plus')]);
+ if (!full) {
+ add = h('button.btn.cp-form-creator-inline-add', {
+ title: Messages.tag_add
+ }, [
+ h('i.fa.fa-plus.add-open'),
+ h('i.fa.fa-times.add-close')
+ ]);
+ var $b = $(buttons).hide();
+ $(add).click(function () {
+ $b.toggle();
+ $(add).toggleClass('displayed');
+ });
+ }
+
+ var inlineCls = full ? '-full' : '-inline';
+ return h('div.cp-form-creator-add'+inlineCls, [
+ h('div', add),
+ buttons
+ ]);
+
+ };
+
+ var updateAddInline = function () {
+ $container.find('.cp-form-creator-add-inline').remove();
+ $container.find('.cp-form-block').each(function (i, el) {
+ var $el = $(el);
+ var uid = $el.attr('data-id');
+ $el.before(getFormCreator(uid));
+ });
+ };
+
+
+ var elements = [];
+ content.order.forEach(function (uid) {
+ var block = form[uid];
+ var type = block.type;
+ var model = TYPES[type] || STATIC_TYPES[type];
+ var isStatic = Boolean(STATIC_TYPES[type]);
+ if (!model) { return; }
+
+ var _answers, name;
+ if (type === 'poll') {
+ var metadataMgr = framework._.cpNfInner.metadataMgr;
+ var user = metadataMgr.getUserData();
+ // If we are a participant, our results shouldn't be in the table but in the
+ // editable part: remove them from _answers
+ _answers = getBlockAnswers(APP.answers, uid, !editable && user.curvePublic);
+ name = user.name;
+ }
+
+ var data = model.get(block.opts, _answers, name, evOnChange);
+ if (!data) { return; }
+ data.uid = uid;
+ if (answers && answers[uid] && data.setValue) { data.setValue(answers[uid]); }
+
+ if (data.pageBreak && !editable) {
+ elements.push(data);
+ return;
+ }
+
+
+ var dragHandle;
+ var q = h('div.cp-form-block-question', block.q || Messages.form_default);
+ var editButtons, editContainer;
+
+ APP.formBlocks.push(data);
+
+ if (editable) {
+ // Drag handle
+ dragHandle = h('span.cp-form-block-drag-handle', [
+ h('i.fa.fa-ellipsis-h'),
+ h('i.fa.fa-ellipsis-h'),
+ ]);
+
+ // Question
+ var inputQ = h('input', {
+ value: block.q || Messages.form_default
+ });
+ var $inputQ = $(inputQ);
+
+ var saving = false;
+ var cancel = false;
+ var onSaveQ = function (e) {
+ if (cancel) {
+ cancel = false;
+ return;
+ }
+ var v = $inputQ.val();
+ if (!v || !v.trim()) { return void UI.warn(Messages.error); }
+ // Don't save if no change
+ if (v.trim() === block.q) {
+ $(q).removeClass('editing');
+ if (!e) { $inputQ.blur(); }
+ return;
+ }
+ if (saving && !e) { return; } // Prevent spam Enter
+ block.q = v.trim();
+ framework.localChange();
+ saving = true;
+ framework._.cpNfInner.chainpad.onSettle(function () {
+ saving = false;
+ $(q).removeClass('editing');
+ if (!e) { $inputQ.blur(); }
+ UI.log(Messages.saved);
+ });
+ };
+ var onCancelQ = function () {
+ $inputQ.val(block.q || Messages.form_default);
+ cancel = true;
+ $inputQ.blur();
+ $(q).removeClass('editing');
+ };
+ $inputQ.keydown(function (e) {
+ if (e.which === 13) { return void onSaveQ(); }
+ if (e.which === 27) { return void onCancelQ(); }
+ });
+ $inputQ.focus(function () {
+ $(q).addClass('editing');
+ });
+ $inputQ.blur(onSaveQ);
+ q = h('div.cp-form-input-block', [inputQ]);
+
+ // Delete question
+ var edit = h('span');
+ var del = h('button.btn.btn-danger-alt', [
+ h('i.fa.fa-trash-o'),
+ h('span', Messages.form_delete)
+ ]);
+ UI.confirmButton(del, {
+ classes: 'btn-danger',
+ new: true
+ }, function () {
+ delete content.form[uid];
+ var idx = content.order.indexOf(uid);
+ content.order.splice(idx, 1);
+ $('.cp-form-block[data-id="'+uid+'"]').remove();
+ framework.localChange();
+ updateAddInline();
+ });
+
+ // Values
+ if (data.edit) {
+ edit = h('button.btn.btn-default.cp-form-edit-button', [
+ h('i.fa.fa-pencil'),
+ h('span', Messages.form_editBlock)
+ ]);
+ editContainer = h('div');
+ var onSave = function (newOpts) {
+ data.editing = false;
+ if (!newOpts) { // Cancel edit
+ $(editContainer).empty();
+ $(editButtons).show();
+ $(data.tag).show();
+ return;
+ }
+ $(editContainer).empty();
+ block.opts = newOpts;
+ framework.localChange();
+ var $oldTag = $(data.tag);
+ framework._.cpNfInner.chainpad.onSettle(function () {
+ $(editButtons).show();
+ UI.log(Messages.saved);
+ var _answers = getBlockAnswers(APP.answers, uid);
+ data = model.get(newOpts, _answers, null, evOnChange);
+ if (!data) { data = {}; }
+ $oldTag.before(data.tag).remove();
+ });
+ };
+ var onEdit = function (tmp) {
+ data.editing = true;
+ $(data.tag).hide();
+ $(editContainer).append(data.edit(onSave, tmp, framework));
+ $(editButtons).hide();
+ };
+ $(edit).click(function () {
+ onEdit();
+ });
+
+ // If we were editing this field, recover our unsaved changes
+ if (temp && temp[uid]) {
+ setTimeout(function () {
+ onEdit(temp[uid]);
+ });
+ }
+ }
+
+ editButtons = h('div.cp-form-edit-buttons-container', [
+ edit, del
+ ]);
+ }
+ var editableCls = editable ? ".editable" : "";
+ elements.push(h('div.cp-form-block'+editableCls, {
+ 'data-id':uid
+ }, [
+ APP.isEditor ? dragHandle : undefined,
+ isStatic ? undefined : q,
+ h('div.cp-form-block-content', [
+ data.tag,
+ editButtons
+ ]),
+ editContainer
+ ]));
+ });
+
+ if (APP.isEditor) {
+ elements.push(getFormCreator());
+ }
+
+ var _content = elements;
+ if (!editable) {
+ _content = [];
+ var div = h('div.cp-form-page');
+ var pages = 1;
+ var wasPage = false;
+ elements.forEach(function (obj, i) {
+ if (obj && obj.pageBreak) {
+ if (i === 0) { return; } // Can't start with a page break
+ if (i === (elements.length - 1)) { return; } // Can't end with a page break
+ if (wasPage) { return; } // Prevent double page break
+ _content.push(div);
+ pages++;
+ div = h('div.cp-form-page');
+ wasPage = true;
+ return;
+ }
+ wasPage = false;
+ $(div).append(obj);
+ });
+ _content.push(div);
+
+ if (pages > 1) {
+ var pageContainer = h('div.cp-form-page-container');
+ var $page = $(pageContainer);
+ _content.push(pageContainer);
+ var refreshPage = function (current) {
+ $page.empty();
+ if (!current || current < 1) { current = 1; }
+ if (current > pages) { current = pages; }
+ var left = h('button.btn.btn-secondary.small.cp-prev', [
+ h('i.fa.fa-chevron-left'),
+ h('span', Messages.form_page_prev)
+ ]);
+ var state = h('span', Messages._getKey('form_page', [current, pages]));
+ var right = h('button.btn.btn-secondary.small.cp-next', [
+ h('span', Messages.form_page_next),
+ h('i.fa.fa-chevron-right'),
+ ]);
+ $(left).click(function () { refreshPage(current - 1); });
+ $(right).click(function () { refreshPage(current + 1); });
+ $page.append([left, state, right]);
+ $container.find('.cp-form-page').hide();
+ $($container.find('.cp-form-page').get(current-1)).show();
+ if (current !== pages) {
+ $container.find('.cp-form-send-container').hide();
+ } else {
+ $container.find('.cp-form-send-container').show();
+ }
+ };
+ setTimeout(refreshPage);
+ }
+ }
+
+ $container.empty().append(_content);
+ updateAddInline();
+
+ if (editable) {
+ Sortable.create($container[0], {
+ direction: "vertical",
+ filter: "input, button, .CodeMirror, .cp-form-type-sort",
+ preventOnFilter: false,
+ draggable: ".cp-form-block",
+ forceFallback: true,
+ fallbackTolerance: 5,
+ onStart: function () {
+ $container.find('.cp-form-creator-add-inline').remove();
+ },
+ store: {
+ set: function (s) {
+ content.order = s.toArray();
+ framework.localChange();
+ updateAddInline();
+ }
+ }
+ });
+ return;
+ }
+
+ // In view mode, add "Submit" and "reset" buttons
+ $container.append(makeFormControls(framework, content, Boolean(answers)));
+ };
+
+ var getTempFields = function () {
+ if (!Array.isArray(APP.formBlocks)) { return; }
+ var temp = {};
+ APP.formBlocks.forEach(function (data) {
+ if (data.editing) {
+ var cursor = data.getCursor && data.getCursor();
+ temp[data.uid] = cursor;
+ }
+ });
+ return temp;
+ };
+
+ var andThen = function (framework) {
+ framework.start();
+ var evOnChange = Util.mkEvent();
+ var content = {};
+
+ APP.common = framework._.sfCommon;
+ var sframeChan = framework._.sfCommon.getSframeChannel();
+ var metadataMgr = framework._.cpNfInner.metadataMgr;
+ var user = metadataMgr.getUserData();
+
+ var priv = metadataMgr.getPrivateData();
+ APP.isEditor = Boolean(priv.form_public);
+ var $body = $('body');
+
+ var $toolbarContainer = $('#cp-toolbar');
+ var helpMenu = framework._.sfCommon.createHelpMenu(['text', 'pad']);
+ $toolbarContainer.after(helpMenu.menu);
+
+
+ var makeFormSettings = function () {
+ // Private / public status
+ var resultsType = h('div.cp-form-results-type-container');
+ var $results = $(resultsType);
+ var refreshPublic = function () {
+ $results.empty();
+ var makePublic = h('button.btn.btn-secondary', Messages.form_makePublic);
+ var makePublicDiv = h('div', makePublic);
+ if (content.answers.privateKey) { makePublicDiv = undefined; }
+ var publicText = content.answers.privateKey ? Messages.form_isPublic : Messages.form_isPrivate;
+ $results.append(h('span.cp-form-results-type', publicText));
+ $results.append(makePublicDiv);
+ var $makePublic = $(makePublic).click(function () {
+ UI.confirm(Messages.form_makePublicWarning, function (yes) {
+ if (!yes) { return; }
+ $makePublic.attr('disabled', 'disabled');
+ var priv = metadataMgr.getPrivateData();
+ content.answers.privateKey = priv.form_private;
+ framework.localChange();
+ framework._.cpNfInner.chainpad.onSettle(function () {
+ UI.log(Messages.saved);
+ refreshPublic();
+ });
+ });
+ });
+ };
+ refreshPublic();
+
+ // Allow anonymous answers
+ var privacyContainer = h('div.cp-form-privacy-container');
+ var $privacy = $(privacyContainer);
+ var refreshPrivacy = function () {
+ $privacy.empty();
+ var anonymous = content.answers.anonymous;
+ var radioOn = UI.createRadio('cp-form-privacy', 'cp-form-privacy-on',
+ Messages.form_anonymous_on, Boolean(anonymous), {
+ input: { value: 1 },
+ mark: { tabindex:1 }
+ });
+ var radioOff = UI.createRadio('cp-form-privacy', 'cp-form-privacy-off',
+ Messages.form_anonymous_off, !anonymous, {
+ input: { value: 0 },
+ mark: { tabindex:1 }
+ });
+ var radioContainer = h('div.cp-form-privacy-radio', [radioOn, radioOff]);
+ $(radioContainer).find('input[type="radio"]').on('change', function() {
+ var val = $('input:radio[name="cp-form-privacy"]:checked').val();
+ val = Number(val) || 0;
+ content.answers.anonymous = Boolean(val);
+ framework.localChange();
+ framework._.cpNfInner.chainpad.onSettle(function () {
+ UI.log(Messages.saved);
+ });
+ });
+ $privacy.append(h('div.cp-form-status', Messages.form_anonymous));
+ $privacy.append(h('div.cp-form-actions', radioContainer));
+ };
+ refreshPrivacy();
+
+ // End date / Closed state
+ var endDateContainer = h('div.cp-form-status-container');
+ var $endDate = $(endDateContainer);
+ var refreshEndDate = function () {
+ $endDate.empty();
+
+ var endDate = content.answers.endDate;
+ var date = new Date(endDate).toLocaleString();
+ var now = +new Date();
+ var text = Messages.form_isOpen;
+ var buttonTxt = Messages.form_setEnd;
+ if (endDate <= now) {
+ text = Messages._getKey('form_isClosed', [date]);
+ buttonTxt = Messages.form_open;
+ } else if (endDate > now) {
+ text = Messages._getKey('form_willClose', [date]);
+ buttonTxt = Messages.form_removeEnd;
+ }
+
+ var button = h('button.btn.btn-secondary', buttonTxt);
+
+ var $button = $(button).click(function () {
+ $button.attr('disabled', 'disabled');
+ // If there is an end date, remove it
+ if (endDate) {
+ delete content.answers.endDate;
+ framework.localChange();
+ refreshEndDate();
+ return;
+ }
+ // Otherwise add it
+ var datePicker = h('input');
+ var picker = Flatpickr(datePicker, {
+ enableTime: true,
+ time_24hr: is24h,
+ dateFormat: dateFormat,
+ minDate: new Date()
+ });
+ var save = h('button.btn.btn-primary', Messages.settings_save);
+ $(save).click(function () {
+ var d = picker.parseDate(datePicker.value);
+ content.answers.endDate = +d;
+ framework.localChange();
+ refreshEndDate();
+ });
+ var confirmContent = h('div', [
+ h('div', Messages.form_setEnd),
+ h('div.cp-form-input-block', [datePicker, save]),
+ ]);
+ $button.after(confirmContent);
+ $button.remove();
+ picker.open();
+ });
+
+ $endDate.append(h('div.cp-form-status', text));
+ $endDate.append(h('div.cp-form-actions', button));
+
+ };
+ refreshEndDate();
+
+
+ evOnChange.reg(refreshPublic);
+ evOnChange.reg(refreshPrivacy);
+ evOnChange.reg(refreshEndDate);
+
+ return [
+ endDateContainer,
+ privacyContainer,
+ resultsType,
+ ];
+ };
+
+ var checkIntegrity = function (getter) {
+ if (!content.order || !content.form) { return; }
+ var changed = false;
+ content.order.forEach(function (uid) {
+ if (!content.form[uid]) {
+ var idx = content.order.indexOf(uid);
+ content.order.splice(idx, 1);
+ changed = true;
+ }
+ });
+ Object.keys(content.form).forEach(function (uid) {
+ var idx = content.order.indexOf(uid);
+ if (idx === -1) {
+ changed = true;
+ content.order.push(uid);
+ }
+ });
+
+ if (!getter && changed) { framework.localChange(); }
+ };
+
+ var makeFormCreator = function () {
+
+ var controlContainer;
+ var fillerContainer;
+ if (APP.isEditor) {
+ var settings = makeFormSettings();
+
+ controlContainer = h('div.cp-form-creator-control', [
+ h('div.cp-form-creator-settings', settings),
+ ]);
+ fillerContainer = h('div.cp-form-filler-container');
+ }
+
+ var contentContainer = h('div.cp-form-creator-content');
+ var resultsContainer = h('div.cp-form-creator-results');
+ var div = h('div.cp-form-creator-container', [
+ controlContainer,
+ contentContainer,
+ resultsContainer,
+ fillerContainer
+ ]);
+ return div;
+ };
+
+ var endDateEl = h('div.alert.alert-warning.cp-burn-after-reading');
+ var endDate;
+ var endDateTo;
+ var refreshEndDateBanner = function (force) {
+ if (APP.isEditor) { return; }
+ var _endDate = content.answers.endDate;
+ if (_endDate === endDate && !force) { return; }
+ endDate = _endDate;
+ var date = new Date(endDate).toLocaleString();
+ var text = Messages._getKey('form_isClosed', [date]);
+ if (endDate > +new Date()) {
+ text = Messages._getKey('form_willClose', [date]);
+ }
+ if ($('.cp-help-container').length && endDate) {
+ $(endDateEl).text(text);
+ $('.cp-help-container').before(endDateEl);
+ } else {
+ $(endDateEl).remove();
+ }
+
+ APP.isClosed = endDate && endDate < (+new Date());
+ clearTimeout(endDateTo);
+ if (!APP.isClosed && endDate) {
+ setTimeout(function () {
+ refreshEndDateBanner(true);
+ $('.cp-form-send-container').find('.cp-open').remove();
+ },(endDate - +new Date() + 100));
+ }
+ };
+
+ framework.onReady(function () {
+ var priv = metadataMgr.getPrivateData();
+
+ if (APP.isEditor) {
+ if (!content.form) {
+ content.form = {};
+ framework.localChange();
+ }
+ if (!content.order) {
+ content.order = [];
+ framework.localChange();
+ }
+ if (!content.answers || !content.answers.channel || !content.answers.publicKey || !content.answers.validateKey) {
+ content.answers = {
+ channel: Hash.createChannelId(),
+ publicKey: priv.form_public,
+ validateKey: priv.form_answerValidateKey
+ };
+ framework.localChange();
+ }
+ }
+
+ sframeChan.event('EV_FORM_PIN', {channel: content.answers.channel});
+
+ var $container = $('#cp-app-form-container');
+ $container.append(makeFormCreator());
+
+ if (!content.answers || !content.answers.channel || !content.answers.publicKey || !content.answers.validateKey) {
+ return void UI.errorLoadingScreen(Messages.form_invalid);
+ }
+
+ var getResults = function (key) {
+ sframeChan.query("Q_FORM_FETCH_ANSWERS", {
+ channel: content.answers.channel,
+ validateKey: content.answers.validateKey,
+ publicKey: content.answers.publicKey,
+ privateKey: key
+ }, function (err, obj) {
+ var answers = obj && obj.results;
+ if (answers) { APP.answers = answers; }
+ $body.addClass('cp-app-form-results');
+ renderResults(content, answers);
+ });
+ };
+ if (priv.form_auditorKey) {
+ APP.isAuditor = true;
+ getResults(priv.form_auditorKey);
+ return;
+ }
+
+ if (APP.isEditor) {
+ addResultsButton(framework, content);
+ sframeChan.query("Q_FORM_FETCH_ANSWERS", {
+ channel: content.answers.channel,
+ validateKey: content.answers.validateKey,
+ publicKey: content.answers.publicKey
+ }, function (err, obj) {
+ var answers = obj && obj.results;
+ if (answers) { APP.answers = answers; }
+ checkIntegrity(false);
+ updateForm(framework, content, true);
+ });
+ return;
+ }
+
+ refreshEndDateBanner();
+
+ var loggedIn = framework._.sfCommon.isLoggedIn();
+ if (!loggedIn && !content.answers.anonymous) {
+ UI.alert(Messages.form_anonymous_blocked);
+ }
+
+ // If the results are public and there is at least one doodle, fetch the results now
+ if (content.answers.privateKey && Object.keys(content.form).some(function (uid) {
+ return content.form[uid].type === "poll";
+ })) {
+ sframeChan.query("Q_FORM_FETCH_ANSWERS", {
+ channel: content.answers.channel,
+ validateKey: content.answers.validateKey,
+ publicKey: content.answers.publicKey,
+ privateKey: content.answers.privateKey,
+ }, function (err, obj) {
+ var answers = obj && obj.results;
+ if (answers) { APP.answers = answers; }
+
+ if (obj && obj.noDriveAnswered) {
+ // No drive mode already answered: can't answer again
+ if (answers) {
+ $body.addClass('cp-app-form-results');
+ renderResults(content, answers);
+ } else {
+ return void UI.errorLoadingScreen(Messages.form_answered);
+ }
+ return;
+ }
+ checkIntegrity(false);
+ var myAnswers;
+ var curve1 = user.curvePublic;
+ var curve2 = obj && obj.myKey; // Anonymous answer key
+ if (answers) {
+ var myAnswersObj = answers[curve1] || answers[curve2] || undefined;
+ if (myAnswersObj) {
+ myAnswers = myAnswersObj.msg;
+ }
+ }
+ // If we have a non-anon answer, we can't answer anonymously later
+ if (answers[curve1]) { APP.cantAnon = true; }
+
+ // Add results button
+ if (myAnswers) { addResultsButton(framework, content); }
+
+ updateForm(framework, content, false, myAnswers);
+ });
+ return;
+ }
+
+ sframeChan.query("Q_FETCH_MY_ANSWERS", {
+ channel: content.answers.channel,
+ validateKey: content.answers.validateKey,
+ publicKey: content.answers.publicKey
+ }, function (err, obj) {
+ if (obj && obj.error) {
+ if (obj.error === "EANSWERED") {
+ // No drive mode already answered: can't answer again
+ if (content.answers.privateKey) {
+ return void getResults(content.answers.privateKey);
+ }
+ // Here, we know results are private so we can use an error screen
+ return void UI.errorLoadingScreen(Messages.form_answered);
+ }
+ UI.warn(Messages.form_cantFindAnswers);
+ }
+ var answers;
+ if (obj && !obj.error) {
+ answers = obj;
+ // If we have a non-anon answer, we can't answer anonymously later
+ if (!obj._isAnon) { APP.cantAnon = true; }
+ }
+ checkIntegrity(false);
+ updateForm(framework, content, false, answers);
+ });
+
+ });
+
+ framework.onContentUpdate(function (newContent) {
+ content = newContent;
+ evOnChange.fire();
+ refreshEndDateBanner();
+ var answers, temp;
+ if (!APP.isEditor) { answers = getFormResults(); }
+ else { temp = getTempFields(); }
+ updateForm(framework, content, APP.isEditor, answers, temp);
+ });
+
+ framework.setContentGetter(function () {
+ checkIntegrity(true);
+ return content;
+ });
+
+ };
+
+ Framework.create({
+ toolbarContainer: '#cp-toolbar',
+ contentContainer: '#cp-app-form-editor',
+ }, andThen);
+});
diff --git a/www/form/main.js b/www/form/main.js
new file mode 100644
index 000000000..344c5bb0c
--- /dev/null
+++ b/www/form/main.js
@@ -0,0 +1,358 @@
+// Load #1, load as little as possible because we are in a race to get the loading screen up.
+define([
+ '/bower_components/nthen/index.js',
+ '/api/config',
+ '/common/dom-ready.js',
+ '/common/sframe-common-outer.js',
+ '/bower_components/tweetnacl/nacl-fast.min.js',
+], function (nThen, ApiConfig, DomReady, SFCommonO) {
+ var Nacl = window.nacl;
+
+ var href, hash;
+ // Loaded in load #2
+ nThen(function (waitFor) {
+ DomReady.onReady(waitFor());
+ }).nThen(function (waitFor) {
+ var obj = SFCommonO.initIframe(waitFor, true);
+ href = obj.href;
+ hash = obj.hash;
+ }).nThen(function (/*waitFor*/) {
+ var privateKey, publicKey;
+ var channels = {};
+ var getPropChannels = function () {
+ return channels;
+ };
+ var addData = function (meta, CryptPad, user, Utils) {
+ var keys = Utils.secret && Utils.secret.keys;
+
+ var parsed = Utils.Hash.parseTypeHash('pad', hash.slice(1));
+ if (parsed && parsed.auditorKey) {
+ meta.form_auditorKey = parsed.auditorKey;
+ meta.form_auditorHash = hash;
+ }
+
+ var secondary = keys && keys.secondaryKey;
+ if (!secondary) { return; }
+ var curvePair = Nacl.box.keyPair.fromSecretKey(Nacl.util.decodeUTF8(secondary).slice(0,32));
+ var validateKey = keys.secondaryValidateKey;
+ meta.form_answerValidateKey = validateKey;
+
+ publicKey = meta.form_public = Nacl.util.encodeBase64(curvePair.publicKey);
+ privateKey = meta.form_private = Nacl.util.encodeBase64(curvePair.secretKey);
+
+ var auditorHash = Utils.Hash.getViewHashFromKeys({
+ version: 1,
+ channel: Utils.secret.channel,
+ keys: { viewKeyStr: Nacl.util.encodeBase64(keys.cryptKey) }
+ });
+ var _parsed = Utils.Hash.parseTypeHash('pad', auditorHash);
+ meta.form_auditorHash = _parsed.getHash({auditorKey: privateKey});
+ };
+ var addRpc = function (sframeChan, Cryptpad, Utils) {
+ sframeChan.on('EV_FORM_PIN', function (data) {
+ channels.answersChannel = data.channel;
+ Cryptpad.getPadAttribute('answersChannel', function (err, res) {
+ // If already stored, don't pin it again
+ if (res && res === data.channel) { return; }
+ Cryptpad.pinPads([data.channel], function () {
+ Cryptpad.setPadAttribute('answersChannel', data.channel, function () {});
+ });
+ });
+
+ });
+ var getAnonymousKeys = function (formSeed, channel) {
+ var array = Nacl.util.decodeBase64(formSeed + channel);
+ var hash = Nacl.hash(array);
+ var secretKey = Nacl.util.encodeBase64(hash.subarray(32));
+ var publicKey = Utils.Hash.getCurvePublicFromPrivate(secretKey);
+ return {
+ curvePrivate: secretKey,
+ curvePublic: publicKey,
+ };
+ };
+ var u8_slice = function (A, start, end) {
+ return new Uint8Array(Array.prototype.slice.call(A, start, end));
+ };
+ var u8_concat = function (A) {
+ var length = 0;
+ A.forEach(function (a) { length += a.length; });
+ var total = new Uint8Array(length);
+
+ var offset = 0;
+ A.forEach(function (a) {
+ total.set(a, offset);
+ offset += a.length;
+ });
+ return total;
+ };
+ var anonProof = function (channel, theirPub, anonKeys) {
+ var u8_plain = Nacl.util.decodeUTF8(channel);
+ var u8_nonce = Nacl.randomBytes(Nacl.box.nonceLength);
+ var u8_cipher = Nacl.box(
+ u8_plain,
+ u8_nonce,
+ Nacl.util.decodeBase64(theirPub),
+ Nacl.util.decodeBase64(anonKeys.curvePrivate)
+ );
+ var u8_bundle = u8_concat([
+ u8_nonce, // 24 uint8s
+ u8_cipher, // arbitrary length
+ ]);
+ return {
+ key: anonKeys.curvePublic,
+ proof: Nacl.util.encodeBase64(u8_bundle)
+ };
+ };
+ var checkAnonProof = function (proofObj, channel, curvePrivate) {
+ var pub = proofObj.key;
+ var proofTxt = proofObj.proof;
+ try {
+ var u8_bundle = Nacl.util.decodeBase64(proofTxt);
+ var u8_nonce = u8_slice(u8_bundle, 0, Nacl.box.nonceLength);
+ var u8_cipher = u8_slice(u8_bundle, Nacl.box.nonceLength);
+ var u8_plain = Nacl.box.open(
+ u8_cipher,
+ u8_nonce,
+ Nacl.util.decodeBase64(pub),
+ Nacl.util.decodeBase64(curvePrivate)
+ );
+ return channel === Nacl.util.encodeUTF8(u8_plain);
+ } catch (e) {
+ console.error(e);
+ return false;
+ }
+ };
+ sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, _cb) {
+ var cb = Utils.Util.once(_cb);
+ var myKeys = {};
+ var myFormKeys;
+ var accessKeys;
+ var CPNetflux, Pinpad;
+ var network;
+ var noDriveAnswered = false;
+ nThen(function (w) {
+ require([
+ '/bower_components/chainpad-netflux/chainpad-netflux.js',
+ '/common/pinpad.js',
+ ], w(function (_CPNetflux, _Pinpad) {
+ CPNetflux = _CPNetflux;
+ Pinpad = _Pinpad;
+ }));
+ Cryptpad.getAccessKeys(w(function (_keys) {
+ if (!Array.isArray(_keys)) { return; }
+ accessKeys = _keys;
+
+ _keys.some(function (_k) {
+ if ((!Cryptpad.initialTeam && !_k.id) || Cryptpad.initialTeam === _k.id) {
+ myKeys = _k;
+ return true;
+ }
+ });
+ }));
+ Cryptpad.getFormKeys(w(function (keys) {
+ if (!keys.curvePublic && !keys.formSeed) {
+ // No drive mode
+ var answered = JSON.parse(localStorage.CP_formAnswered || "[]");
+ noDriveAnswered = answered.indexOf(data.channel) !== -1;
+ }
+ myFormKeys = keys;
+ }));
+ Cryptpad.makeNetwork(w(function (err, nw) {
+ network = nw;
+ }));
+ }).nThen(function () {
+ if (!network) { return void cb({error: "E_CONNECT"}); }
+
+ if (myFormKeys.formSeed) {
+ myFormKeys = getAnonymousKeys(myFormKeys.formSeed, data.channel);
+ }
+
+ var keys = Utils.secret && Utils.secret.keys;
+
+ var curvePrivate = privateKey || data.privateKey;
+ var crypto = Utils.Crypto.Mailbox.createEncryptor({
+ curvePrivate: curvePrivate,
+ curvePublic: publicKey || data.publicKey,
+ validateKey: data.validateKey
+ });
+ var config = {
+ network: network,
+ channel: data.channel,
+ noChainPad: true,
+ validateKey: keys.secondaryValidateKey,
+ owners: [myKeys.edPublic],
+ crypto: crypto,
+ // XXX Cache
+ };
+ var results = {};
+ config.onError = function (info) {
+ cb({ error: info.type });
+ };
+ config.onRejected = function (data, cb) {
+ if (!Array.isArray(data) || !data.length || data[0].length !== 16) {
+ return void cb(true);
+ }
+ if (!Array.isArray(accessKeys)) { return void cb(true); }
+ network.historyKeeper = data[0];
+ nThen(function (waitFor) {
+ accessKeys.forEach(function (obj) {
+ Pinpad.create(network, obj, waitFor(function (e) {
+ console.log('done', obj);
+ if (e) { console.error(e); }
+ }));
+ });
+ }).nThen(function () {
+ cb();
+ });
+ };
+ config.onReady = function () {
+ var myKey;
+ // If we have submitted an anonymous answer, retrieve it
+ if (myFormKeys.curvePublic && results[myFormKeys.curvePublic]) {
+ myKey = myFormKeys.curvePublic;
+ }
+ cb({
+ noDriveAnswered: noDriveAnswered,
+ myKey: myKey,
+ results: results
+ });
+ network.disconnect();
+ };
+ config.onMessage = function (msg, peer, vKey, isCp, hash, senderCurve, cfg) {
+ var parsed = Utils.Util.tryParse(msg);
+ if (!parsed) { return; }
+ if (parsed._proof) {
+ var check = checkAnonProof(parsed._proof, data.channel, curvePrivate);
+ if (check) {
+ delete results[parsed._proof.key];
+ }
+ }
+ results[senderCurve] = {
+ msg: parsed,
+ hash: hash,
+ time: cfg.time
+ };
+ };
+ CPNetflux.start(config);
+ });
+ });
+ sframeChan.on("Q_FETCH_MY_ANSWERS", function (data, cb) {
+ var answer;
+ var myKeys;
+ nThen(function (w) {
+ Cryptpad.getFormKeys(w(function (keys) {
+ myKeys = keys;
+ }));
+ Cryptpad.getFormAnswer({channel: data.channel}, w(function (obj) {
+ if (!obj || obj.error) {
+ if (obj && obj.error === "ENODRIVE") {
+ var answered = JSON.parse(localStorage.CP_formAnswered || "[]");
+ if (answered.indexOf(data.channel) !== -1) {
+ cb({error:'EANSWERED'});
+ } else {
+ cb();
+ }
+ return void w.abort();
+ }
+ w.abort();
+ return void cb(obj);
+ }
+ answer = obj;
+ }));
+ }).nThen(function () {
+ if (answer.anonymous) {
+ if (!myKeys.formSeed) { return void cb({ error: "ANONYMOUS_ERROR" }); }
+ myKeys = getAnonymousKeys(myKeys.formSeed, data.channel);
+ }
+ Cryptpad.getHistoryRange({
+ channel: data.channel,
+ lastKnownHash: answer.hash,
+ toHash: answer.hash,
+ }, function (obj) {
+ if (obj && obj.error) { return void cb(obj); }
+ var messages = obj.messages;
+ var res = Utils.Crypto.Mailbox.openOwnSecretLetter(messages[0].msg, {
+ validateKey: data.validateKey,
+ ephemeral_private: Nacl.util.decodeBase64(answer.curvePrivate),
+ my_private: Nacl.util.decodeBase64(myKeys.curvePrivate),
+ their_public: Nacl.util.decodeBase64(data.publicKey)
+ });
+ res.content._isAnon = answer.anonymous;
+ cb(JSON.parse(res.content));
+ });
+
+ });
+
+ });
+ var noDriveSeed = Utils.Hash.createChannelId();
+ sframeChan.on("Q_FORM_SUBMIT", function (data, cb) {
+ var box = data.mailbox;
+ var myKeys;
+ nThen(function (w) {
+ Cryptpad.getFormKeys(w(function (keys) {
+ // If formSeed doesn't exists, it means we're probably in noDrive mode.
+ // We can create a seed in localStorage.
+ if (!keys.formSeed) {
+ // No drive mode
+ var answered = JSON.parse(localStorage.CP_formAnswered || "[]");
+ if(answered.indexOf(data.channel) !== -1) {
+ // Already answered: abort
+ return void cb({ error: "EANSWERED" });
+ }
+ keys = { formSeed: noDriveSeed };
+ }
+ myKeys = keys;
+ }));
+ }).nThen(function () {
+ var myAnonymousKeys;
+ if (data.anonymous) {
+ if (!myKeys.formSeed) { return void cb({ error: "ANONYMOUS_ERROR" }); }
+ myKeys = getAnonymousKeys(myKeys.formSeed, box.channel);
+ } else {
+ myAnonymousKeys = getAnonymousKeys(myKeys.formSeed, box.channel);
+ }
+ var keys = Utils.secret && Utils.secret.keys;
+ myKeys.signingKey = keys.secondarySignKey;
+
+ var ephemeral_keypair = Nacl.box.keyPair();
+ var ephemeral_private = Nacl.util.encodeBase64(ephemeral_keypair.secretKey);
+ myKeys.ephemeral_keypair = ephemeral_keypair;
+
+ if (myAnonymousKeys) {
+ var proof = anonProof(box.channel, box.publicKey, myAnonymousKeys);
+ data.results._proof = proof;
+ }
+
+ var crypto = Utils.Crypto.Mailbox.createEncryptor(myKeys);
+ var text = JSON.stringify(data.results);
+ var ciphertext = crypto.encrypt(text, box.publicKey);
+
+ var hash = ciphertext.slice(0,64);
+ Cryptpad.anonRpcMsg("WRITE_PRIVATE_MESSAGE", [
+ box.channel,
+ ciphertext
+ ], function (err, response) {
+ Cryptpad.storeFormAnswer({
+ channel: box.channel,
+ hash: hash,
+ curvePrivate: ephemeral_private,
+ anonymous: Boolean(data.anonymous)
+ });
+ cb({error: err, response: response, hash: hash});
+ });
+ });
+ });
+ };
+ SFCommonO.start({
+ addData: addData,
+ addRpc: addRpc,
+ cache: true,
+ noDrive: true,
+ hash: hash,
+ href: href,
+ useCreationScreen: true,
+ messaging: true,
+ getPropChannels: getPropChannels
+ });
+ });
+});
diff --git a/www/kanban/inner.js b/www/kanban/inner.js
index 126ffd000..76a9fe1cc 100644
--- a/www/kanban/inner.js
+++ b/www/kanban/inner.js
@@ -241,7 +241,12 @@ define([
e.stopPropagation();
});
var common = framework._.sfCommon;
- var markdownTb = common.createMarkdownToolbar(editor);
+ var markdownTb = common.createMarkdownToolbar(editor, {
+ embed: function (mt) {
+ editor.focus();
+ editor.replaceSelection($(mt)[0].outerHTML);
+ }
+ });
$(text).before(markdownTb.toolbar);
$(markdownTb.toolbar).show();
editor.refresh();
diff --git a/www/secureiframe/inner.js b/www/secureiframe/inner.js
index 1c5af4a57..4226c7f82 100644
--- a/www/secureiframe/inner.js
+++ b/www/secureiframe/inner.js
@@ -58,6 +58,7 @@ define([
hashes: data.hashes || priv.hashes,
common: common,
title: data.title,
+ auditorHash: data.auditorHash,
versionHash: data.versionHash,
friends: friends,
onClose: function () {