diff --git a/customize.dist/src/less2/include/colortheme-dark.less b/customize.dist/src/less2/include/colortheme-dark.less
index 588d1888e..3443aaeb0 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;
diff --git a/customize.dist/src/less2/include/colortheme.less b/customize.dist/src/less2/include/colortheme.less
index 3ab249f9f..e56676213 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;
diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js
index 904fe7e45..267316541 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-ui-elements.js b/www/common/common-ui-elements.js
index 887eb147a..e8c0cce4c 100644
--- a/www/common/common-ui-elements.js
+++ b/www/common/common-ui-elements.js
@@ -2050,6 +2050,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',
diff --git a/www/form/app-form.less b/www/form/app-form.less
new file mode 100644
index 000000000..996de4ad7
--- /dev/null
+++ b/www/form/app-form.less
@@ -0,0 +1,92 @@
+@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;
+
+ #cp-app-form-editor {
+ flex: 1;
+ display: flex;
+ flex-flow: row;
+ height: 100%;
+ overflow: hidden;
+ }
+
+ #cp-app-form-container {
+ display: flex;
+ flex: 1;
+ justify-content: center;
+
+ div.cp-form-creator-container {
+ display: flex;
+ flex: 1;
+ max-width: 1300px;
+ div.cp-form-creator-control {
+ padding: 10px;
+ display: flex;
+ flex-flow: column;
+ width: 300px;
+ }
+ div.cp-form-creator-content {
+ padding: 10px;
+ display: flex;
+ flex-flow: column;
+ flex: 1;
+ .cp-form-block {
+ &:not(:last-child) {
+ margin-bottom: 20px;
+ }
+ .cp-form-input-block {
+ display: flex;
+ //width: @form_input-width;
+ &:not(:focus-within) {
+ input {
+ background: transparent;
+ border: none;
+ & ~ button:not(:disabled) {
+ .cp-form-edit { display: inline; }
+ .cp-form-save { display: none; }
+ }
+ }
+ }
+ input {
+ flex: 1;
+ min-width: 100px;
+ }
+ button {
+ .cp-form-edit {
+ display: none;
+ margin: 0 !important;
+ }
+ .cp-form-save { display: inline; }
+ }
+ }
+ }
+ .cp-form-edit-block {
+ .cp-form-edit-block-input {
+ display: flex;
+ width: 400px;
+ input {
+ flex: 1;
+ min-width: 100px;
+ }
+ button {
+ i { margin: 0 !important; }
+ }
+
+ }
+ }
+ }
+ }
+ }
+
+}
+
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..d9393faf1
--- /dev/null
+++ b/www/form/inner.js
@@ -0,0 +1,355 @@
+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/inner/share.js',
+ '/common/inner/access.js',
+ '/common/inner/properties.js',
+
+ '/bower_components/file-saver/FileSaver.min.js',
+ 'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
+ 'less!/form/app-form.less',
+], function (
+ $,
+ JSONSortify,
+ Crypto,
+ Framework,
+ Toolbar,
+ nThen,
+ SFCommon,
+ Util,
+ Hash,
+ UI,
+ UIElements,
+ Clipboard,
+ MT,
+ h,
+ Messages,
+ AppConfig,
+ Share, Access, Properties
+ )
+{
+ var SaveAs = window.saveAs;
+ var APP = window.APP = {
+ };
+
+ Messages.button_newform = "New Form"; // XXX
+ Messages.form_invalid = "Invalid form";
+ Messages.form_editBlock = "Edit options";
+
+ Messages.form_newOption = "New option";
+
+ Messages.form_default = "Your question here?";
+ Messages.form_type_input = "Text"; // XXX
+ Messages.form_type_radio = "Radio"; // XXX
+
+ Messages.form_duplicates = "Duplicate entries have been removed";
+
+ var makeFormSettings = function (framework) {
+ // XXX
+ // Button to set results as public
+ // Checkbox to allow anonymous answers
+ // Button to clear all answers?
+ };
+
+ var TYPES = {
+ input: {
+ get: function () {
+ var tag = h('input');
+ var $tag = $(tag);
+ return {
+ tag: tag,
+ getValue: function () { return $tag.val(); },
+ //setValue: function (val) { $tag.val(val); }
+ };
+ },
+ icon: h('i.fa.fa-font')
+ },
+ radio: {
+ defaultOpts: {
+ values: ["Option 1", "Option 2"] // XXX?
+ },
+ get: function (opts) {
+ if (!opts) { opts = TYPES.radio.defaultOpts; }
+ var name = Util.uid();
+ var els = opts.values.map(function (data, i) {
+ return UI.createRadio(name, 'cp-form-'+name+'-'+i,
+ data, false, { mark: {tabindex:1} });
+ });
+ var tag = h('div.radio-group', els);
+ return {
+ tag: tag,
+ getValue: function () {
+ var res;
+ els.some(function (el, i) {
+ if (Util.isChecked($(el).find('input'))) {
+ res = opts.values[i];
+ }
+ });
+ return res;
+ },
+ edit: function (cb) {
+ var v = opts.values.slice();
+
+ var add = h('button.btn.btn-secondary', [
+ h('i.fa.fa-plus'),
+ h('span', Messages.tag_add)
+ ]);
+
+ // Show existing options
+ var getOption = function (val) {
+ var input = h('input', {value:val});
+ var del = h('button.btn.btn-danger', h('i.fa.fa-times'));
+ var el = h('div.cp-form-edit-block-input', [ input, del ]);
+ $(del).click(function () { $(el).remove(); });
+ return el;
+ };
+ var inputs = v.map(getOption);
+ inputs.push(add);
+ var container = h('div.cp-form-edit-block', inputs);
+
+ // Add option
+ var $add = $(add).click(function () {
+ $add.before(getOption(Messages.form_newOption));
+ });
+
+ // 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');
+ var values = [];
+ var duplicates = false;
+ $(container).find('input').each(function (i, el) {
+ var val = $(el).val().trim();
+ if (values.indexOf(val) === -1) { values.push(val); }
+ else { duplicates = true; }
+ });
+ if (duplicates) {
+ UI.warn(Messages.form_duplicates);
+ }
+ cb({values: values});
+ });
+
+ return [
+ container,
+ h('div', [cancelBlock, saveBlock])
+ ];
+ }
+ //setValue: function (val) {}
+ };
+
+ },
+ icon: h('i.fa.fa-list-ul')
+ }
+ };
+
+ var renderForm = function (content, editable) {
+
+ };
+ var updateForm = function (framework, content, editable) {
+ var $container = $('div.cp-form-creator-content');
+
+ var form = content.form;
+
+ // XXX order array later
+ var elements = Object.keys(form).map(function (uid) {
+ var block = form[uid];
+ var type = block.type;
+ var model = TYPES[type];
+ if (!model) { return; }
+ var data = model.get(block.opts);
+ var q = h('div.cp-form-block-question', block.q || Messages.form_default);
+ var edit, editContainer;
+ if (editable) {
+ // Question
+
+ var inputQ = h('input', {
+ value: block.q || Messages.form_default
+ });
+ var $inputQ = $(inputQ);
+ var saveQ = h('button.btn.btn-primary', [
+ h('i.fa.fa-pencil.cp-form-edit'),
+ h('span.cp-form-save', Messages.settings_save)
+ ]);
+ var $saveQ = $(saveQ).click(function () {
+ var v = $inputQ.val();
+ if (!v || !v.trim() || v === block.q) { return; }
+ block.q = v.trim();
+ framework.localChange();
+ $saveQ.attr('disabled', 'disabled');
+ framework._.cpNfInner.chainpad.onSettle(function () {
+ $saveQ.removeAttr('disabled');
+ $saveQ.blur();
+ UI.log(Messages.saved);
+ });
+ });
+ var onBlur = function (e) {
+ if (e && e.relatedTarget && e.relatedTarget === saveQ) { return; }
+ $inputQ.val(block.q);
+ };
+ $inputQ.keydown(function (e) {
+ if (e.which === 13) { return void $saveQ.click(); }
+ if (e.which === 27) { return void $inputQ.blur(); }
+ });
+ $inputQ.blur(onBlur);
+ q = h('div.cp-form-input-block', [inputQ, saveQ]);
+
+ // Values
+ if (data.edit) {
+ edit = h('button.btn.btn-primary.cp-form-edit-button', [
+ h('i.fa.fa-pencil'),
+ h('span', Messages.form_editBlock)
+ ]);
+ editContainer = h('div');
+ var onSave = function (newOpts) {
+ if (!newOpts) { // Cancel edit
+ $(editContainer).empty();
+ $edit.show();
+ $(data.tag).show();
+ return;
+ }
+ $(editContainer).empty();
+ block.opts = newOpts;
+ var $oldTag = $(data.tag);
+ framework._.cpNfInner.chainpad.onSettle(function () {
+ $edit.show();
+ UI.log(Messages.saved);
+ data = model.get(newOpts);
+ $oldTag.before(data.tag).remove();
+ });
+ };
+ var $edit = $(edit).click(function () {
+ $(data.tag).hide();
+ $(editContainer).append(data.edit(onSave));
+ $edit.hide();
+ });
+ }
+ }
+ return h('div.cp-form-block', [
+ q,
+ h('div.cp-form-block-content', [
+ data.tag,
+ edit
+ ]),
+ editContainer
+ ]);
+ });
+
+ $container.empty().append(elements);
+ };
+
+ var andThen = function (framework) {
+ framework.start();
+ var content = {};
+
+ var $container = $('#cp-app-form-container');
+
+ var makeFormCreator = function () {
+ var controls = Object.keys(TYPES).map(function (type) {
+
+ var btn = h('button.btn', [
+ TYPES[type].icon.cloneNode(),
+ h('span', Messages['form_type_'+type])
+ ]);
+ $(btn).click(function () {
+ var uid = Util.uid();
+ content.form[uid] = {
+ //q: Messages.form_default,
+ //opts: opts
+ type: type,
+ };
+ framework.localChange();
+ updateForm(framework, content, true);
+ });
+ return btn;
+ });
+ var controlContainer = h('div.cp-form-creator-control', controls);
+
+ var contentContainer = h('div.cp-form-creator-content');
+ var div = h('div.cp-form-creator-container', [
+ controlContainer,
+ contentContainer,
+ ]);
+ return div;
+ };
+
+ $container.append(makeFormCreator());
+
+ var sframeChan = framework._.sfCommon.getSframeChannel();
+ var metadataMgr = framework._.cpNfInner.metadataMgr;
+ var priv = metadataMgr.getPrivateData();
+ APP.isEditor = Boolean(priv.form_public);
+
+ framework.onReady(function (isNew) {
+ var priv = metadataMgr.getPrivateData();
+ if (APP.isEditor) {
+ if (!content.form) {
+ content.form = {};
+ framework.localChange();
+ }
+ if (!content.answers || !content.answers.channel || !content.answers.publicKey) {
+ content.answers = {
+ channel: Hash.createChannelId(),
+ publicKey: priv.form_public
+ };
+ framework.localChange();
+ }
+ }
+
+ if (!content.answers || !content.answers.channel || !content.answers.publicKey) {
+ return void UI.errorLoadingScreen(Messages.form_invalid);
+ }
+ // XXX fetch answers and
+ // * viewers ==> check if you've already answered and show form (new or edit)
+ // * editors ==> show schema and warn users if existing questions already have answers
+ if (APP.isEditor) {
+ sframeChan.query("Q_FORM_FETCH_ANSWERS", {
+ channel: content.answers.channel,
+ publicKey: content.answers.publicKey
+ }, function () {
+ updateForm(framework, content, true);
+
+ });
+ return;
+ }
+ updateForm(framework, content, false);
+
+ });
+
+ framework.onContentUpdate(function (newContent) {
+ console.log(newContent);
+ content = newContent;
+ });
+
+ framework.setContentGetter(function () {
+ 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..be47466e3
--- /dev/null
+++ b/www/form/main.js
@@ -0,0 +1,90 @@
+// 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;
+
+ // 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 addData = function (meta, CryptPad, user, Utils) {
+ var keys = Utils.secret && Utils.secret.keys;
+ var secondary = keys && keys.secondaryKey;
+ if (!secondary) { return; }
+ var curvePair = Nacl.box.keyPair.fromSecretKey(Nacl.util.decodeUTF8(secondary).slice(0,32));
+ publicKey = meta.form_public = Nacl.util.encodeBase64(curvePair.publicKey);
+ privateKey = Nacl.util.encodeBase64(curvePair.secretKey);
+ };
+ var addRpc = function (sframeChan, Cryptpad, Utils) {
+ sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, cb) {
+ var keys;
+ var CPNetflux;
+ var network;
+ nThen(function (w) {
+ require([
+ '/bower_components/chainpad-netflux/chainpad-netflux.js',
+ ], w(function (_CPNetflux, _Crypto) {
+ CPNetflux = _CPNetflux;
+ }));
+ Cryptpad.getAccessKeys(w(function (_keys) {
+ keys = _keys;
+ }));
+ Cryptpad.makeNetwork(w(function (err, nw) {
+ network = nw;
+ }));
+ }).nThen(function (w) {
+ if (!network) { return void cb({error: "E_CONNECT"}); }
+
+ var keys = Utils.secret && Utils.secret.keys;
+
+ var crypto = Utils.Crypto.Mailbox.createEncryptor({
+ curvePrivate: privateKey,
+ curvePublic: publicKey || data.publicKey
+ });
+ var config = {
+ network: network,
+ channel: data.channel,
+ noChainPad: true,
+ validateKey: keys.secondaryValidateKey,
+ owners: [], // XXX add pad owner
+ crypto: crypto,
+ // XXX Cache
+ };
+ config.onReady = function () {
+ cb();
+ // XXX
+ };
+ config.onMessage = function () {
+ // XXX
+ };
+ CPNetflux.start(config);
+ });
+ });
+ sframeChan.on('EV_FORM_MAILBOX', function (data) {
+ var curvePair = Nacl.box.keyPair();
+ publicKey = Nacl.util.encodeBase64(curvePair.publicKey);
+ privateKey = Nacl.util.encodeBase64(curvePair.secretKey);
+ });
+ };
+ SFCommonO.start({
+ addData: addData,
+ addRpc: addRpc,
+ cache: true,
+ noDrive: true,
+ hash: hash,
+ href: href,
+ useCreationScreen: true,
+ messaging: true
+ });
+ });
+});