From 10f52230a46909551b4f41f02208c048bd445ece Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 20 May 2021 10:43:29 +0200 Subject: [PATCH 01/44] Form app prototype --- .../src/less2/include/colortheme-dark.less | 1 + .../src/less2/include/colortheme.less | 1 + www/common/application_config_internal.js | 3 +- www/common/common-ui-elements.js | 1 + www/form/app-form.less | 92 +++++ www/form/index.html | 12 + www/form/inner.html | 20 + www/form/inner.js | 355 ++++++++++++++++++ www/form/main.js | 90 +++++ 9 files changed, 574 insertions(+), 1 deletion(-) create mode 100644 www/form/app-form.less create mode 100644 www/form/index.html create mode 100644 www/form/inner.html create mode 100644 www/form/inner.js create mode 100644 www/form/main.js 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 + }); + }); +}); From 3184ad419ea9aa9be0adf1daaf8a0bce7f65806d Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 20 May 2021 13:54:20 +0200 Subject: [PATCH 02/44] Ability to submit form answers --- www/common/common-hash.js | 6 + www/common/cryptpad-common.js | 11 ++ www/form/inner.js | 207 ++++++++++++++++++++++------------ www/form/main.js | 26 +++++ 4 files changed, 175 insertions(+), 75 deletions(-) diff --git a/www/common/common-hash.js b/www/common/common-hash.js index cf3ca76a4..c59f5babe 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; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index e9635d5b5..ba0624a08 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -100,6 +100,17 @@ define([ cb(keys); }); }; + common.getFormKeys = function (cb) { + postMessage("GET", { + key: ['curvePrivate'], + }, function (obj) { + if (obj.error) { return void cb(); } + cb({ + curvePrivate: obj, + curvePublic: Hash.getCurvePublicFromPrivate(obj) + }); + }); + }; common.makeNetwork = function (cb) { require([ diff --git a/www/form/inner.js b/www/form/inner.js index d9393faf1..f50583e37 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -59,6 +59,13 @@ define([ Messages.form_duplicates = "Duplicate entries have been removed"; + Messages.form_reset = "Reset"; + Messages.form_sent = "Sent"; + + // XXX to update our own answers, we need to store the server hash of the message + // and we'll be able to use getHistoryRange to fetch this message when we come back + + var makeFormSettings = function (framework) { // XXX // Button to set results as public @@ -66,6 +73,59 @@ define([ // Button to clear all answers? }; + var editOptions = function (v, cb) { + 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]) + ]; + }; + var TYPES = { input: { get: function () { @@ -75,6 +135,7 @@ define([ tag: tag, getValue: function () { return $tag.val(); }, //setValue: function (val) { $tag.val(val); } + reset: function () { $tag.val(''); } }; }, icon: h('i.fa.fa-font') @@ -102,59 +163,10 @@ define([ }); return res; }, + reset: function () { $(tag).find('input').removeAttr('checked'); }, 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]) - ]; + return editOptions(v, cb); } //setValue: function (val) {} }; @@ -164,23 +176,60 @@ define([ } }; - var renderForm = function (content, editable) { + var makeFormControls = function (framework, content) { + var send = h('button.btn.btn-primary', Messages.poll_commit); + var reset = h('button.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'); + if (!Array.isArray(APP.formBlocks)) { return; } + var results = {}; + APP.formBlocks.forEach(function (data) { + results[data.uid] = data.getValue(); + }); + var sframeChan = framework._.sfCommon.getSframeChannel(); + sframeChan.query('Q_FORM_SUBMIT', { + mailbox: content.answers, + results: results + }, function (err, data) { + console.error(data); + if (err || (data && data.error)) { + console.error(err || data.error); + return void UI.warn(Messages.error); + } + UI.alert(Messages.form_sent); + }); + }); + return h('div.cp-form-send-container', [send, reset]); }; var updateForm = function (framework, content, editable) { var $container = $('div.cp-form-creator-content'); var form = content.form; + APP.formBlocks = []; + // 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); + data.uid = uid; + var q = h('div.cp-form-block-question', block.q || Messages.form_default); var edit, editContainer; + + APP.formBlocks.push(data); + if (editable) { // Question @@ -231,6 +280,7 @@ define([ } $(editContainer).empty(); block.opts = newOpts; + framework.localChange(); var $oldTag = $(data.tag); framework._.cpNfInner.chainpad.onSettle(function () { $edit.show(); @@ -257,34 +307,43 @@ define([ }); $container.empty().append(elements); + $container.append(makeFormControls(framework, content)); }; var andThen = function (framework) { framework.start(); var content = {}; - var $container = $('#cp-app-form-container'); + var sframeChan = framework._.sfCommon.getSframeChannel(); + var metadataMgr = framework._.cpNfInner.metadataMgr; + + var priv = metadataMgr.getPrivateData(); + APP.isEditor = Boolean(priv.form_public); 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); + var controlContainer; + if (APP.isEditor) { + 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; }); - return btn; - }); - var controlContainer = h('div.cp-form-creator-control', controls); + controlContainer = h('div.cp-form-creator-control', controls); + } var contentContainer = h('div.cp-form-creator-content'); var div = h('div.cp-form-creator-container', [ @@ -294,12 +353,9 @@ define([ return div; }; + var $container = $('#cp-app-form-container'); $container.append(makeFormCreator()); - - var sframeChan = framework._.sfCommon.getSframeChannel(); - var metadataMgr = framework._.cpNfInner.metadataMgr; - var priv = metadataMgr.getPrivateData(); - APP.isEditor = Boolean(priv.form_public); + if (!APP.isEditor) { makeFormControls(); } framework.onReady(function (isNew) { var priv = metadataMgr.getPrivateData(); @@ -340,6 +396,7 @@ define([ framework.onContentUpdate(function (newContent) { console.log(newContent); content = newContent; + updateForm(framework, content, APP.isEditor); }); framework.setContentGetter(function () { diff --git a/www/form/main.js b/www/form/main.js index be47466e3..d9eb3e660 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -63,6 +63,7 @@ define([ config.onReady = function () { cb(); // XXX + network.disconnect(); }; config.onMessage = function () { // XXX @@ -70,6 +71,31 @@ define([ CPNetflux.start(config); }); }); + sframeChan.on("Q_FORM_SUBMIT", function (data, cb) { + var box = data.mailbox; + var myKeys; + nThen(function (w) { + Cryptpad.getFormKeys(w(function (keys) { + myKeys = keys; + })); + }).nThen(function (w) { + + var keys = Utils.secret && Utils.secret.keys; + myKeys.signingKey = keys.secondarySignKey; + + 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); // XXX use this to recover our previous answers + Cryptpad.anonRpcMsg("WRITE_PRIVATE_MESSAGE", [ + box.channel, + ciphertext + ], function (err, response) { + cb({error: err, response: response, hash: hash}); + }); + }); + }); sframeChan.on('EV_FORM_MAILBOX', function (data) { var curvePair = Nacl.box.keyPair(); publicKey = Nacl.util.encodeBase64(curvePair.publicKey); From 3b30cfcc555c9a4de1e3a6216839657b0a51fc74 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 20 May 2021 16:20:15 +0200 Subject: [PATCH 03/44] Recover previous answers --- www/common/cryptpad-common.js | 18 ++++++++++++ www/common/outer/async-store.js | 1 + www/form/inner.js | 45 +++++++++++++++++++++++------ www/form/main.js | 50 +++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 9 deletions(-) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index ba0624a08..173aa9c9f 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -100,6 +100,7 @@ define([ cb(keys); }); }; + common.getFormKeys = function (cb) { postMessage("GET", { key: ['curvePrivate'], @@ -111,6 +112,23 @@ define([ }); }); }; + 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 + } + }, function (obj) { + if (obj && obj.error) { console.error(obj.error); } + }); + + }; common.makeNetwork = function (cb) { require([ diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 21bc77798..0e1e9684e 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -2696,6 +2696,7 @@ define([ nThen(function (waitFor) { if (!proxy.settings) { proxy.settings = NEW_USER_SETTINGS; } + if (!proxy.forms) { proxy.forms = {}; } if (!proxy.friends_pending) { proxy.friends_pending = {}; } // Call onCacheReady if the manager is not yet defined diff --git a/www/form/inner.js b/www/form/inner.js index f50583e37..b62c4e0da 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -62,6 +62,8 @@ define([ Messages.form_reset = "Reset"; Messages.form_sent = "Sent"; + Messages.form_cantFindAnswers = "Unable to retrieve your existing answers for this form."; + // XXX to update our own answers, we need to store the server hash of the message // and we'll be able to use getHistoryRange to fetch this message when we come back @@ -134,7 +136,7 @@ define([ return { tag: tag, getValue: function () { return $tag.val(); }, - //setValue: function (val) { $tag.val(val); } + setValue: function (val) { $tag.val(val); }, reset: function () { $tag.val(''); } }; }, @@ -148,8 +150,10 @@ define([ 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 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', els); return { @@ -167,8 +171,17 @@ define([ edit: function (cb) { var v = opts.values.slice(); return editOptions(v, cb); + }, + 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; + } + }); } - //setValue: function (val) {} }; }, @@ -208,7 +221,7 @@ define([ }); return h('div.cp-form-send-container', [send, reset]); }; - var updateForm = function (framework, content, editable) { + var updateForm = function (framework, content, editable, answers) { var $container = $('div.cp-form-creator-content'); var form = content.form; @@ -224,6 +237,7 @@ define([ var data = model.get(block.opts); data.uid = uid; + if (answers && answers[uid]) { data.setValue(answers[uid]); } var q = h('div.cp-form-block-question', block.q || Messages.form_default); var edit, editContainer; @@ -364,16 +378,17 @@ define([ content.form = {}; framework.localChange(); } - if (!content.answers || !content.answers.channel || !content.answers.publicKey) { + if (!content.answers || !content.answers.channel || !content.answers.publicKey || !content.answers.validateKey) { content.answers = { channel: Hash.createChannelId(), - publicKey: priv.form_public + publicKey: priv.form_public, + validateKey: priv.form_answerValidateKey }; framework.localChange(); } } - if (!content.answers || !content.answers.channel || !content.answers.publicKey) { + if (!content.answers || !content.answers.channel || !content.answers.publicKey || !content.answers.validateKey) { return void UI.errorLoadingScreen(Messages.form_invalid); } // XXX fetch answers and @@ -389,7 +404,19 @@ define([ }); return; } - updateForm(framework, content, false); + + 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) { + UI.warn(Messages.form_cantFindAnswers); + } + var answers; + if (obj && !obj.error) { answers = obj; } + updateForm(framework, content, false, answers); + }); }); diff --git a/www/form/main.js b/www/form/main.js index d9eb3e660..089c3433e 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -22,6 +22,9 @@ define([ 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 = Nacl.util.encodeBase64(curvePair.secretKey); }; @@ -71,6 +74,44 @@ define([ CPNetflux.start(config); }); }); + sframeChan.on("Q_FETCH_MY_ANSWERS", function (data, cb) { + var keys; + var CPNetflux; + var network; + 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) { + w.abort(); + return void cb(obj); + } + answer = obj; + })); + }).nThen(function (w) { + 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 ephemeral_priv = answer.curvePrivate; + 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) + }); + cb(JSON.parse(res.content)); + }); + + }); + + }); sframeChan.on("Q_FORM_SUBMIT", function (data, cb) { var box = data.mailbox; var myKeys; @@ -83,6 +124,10 @@ define([ 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; + var crypto = Utils.Crypto.Mailbox.createEncryptor(myKeys); var text = JSON.stringify(data.results); var ciphertext = crypto.encrypt(text, box.publicKey); @@ -92,6 +137,11 @@ define([ box.channel, ciphertext ], function (err, response) { + Cryptpad.storeFormAnswer({ + channel: box.channel, + hash: hash, + curvePrivate: ephemeral_private + }); cb({error: err, response: response, hash: hash}); }); }); From 07c90b6a94edea51430cbab4ee4a87d9924e13ac Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 21 May 2021 13:39:33 +0200 Subject: [PATCH 04/44] View responses --- .../src/less2/include/colortheme-dark.less | 4 + .../src/less2/include/colortheme.less | 5 + www/common/cryptpad-common.js | 4 +- www/form/app-form.less | 66 ++++++++- www/form/inner.js | 133 ++++++++++++++++-- www/form/main.js | 31 ++-- 6 files changed, 218 insertions(+), 25 deletions(-) diff --git a/customize.dist/src/less2/include/colortheme-dark.less b/customize.dist/src/less2/include/colortheme-dark.less index 3443aaeb0..875ad004b 100644 --- a/customize.dist/src/less2/include/colortheme-dark.less +++ b/customize.dist/src/less2/include/colortheme-dark.less @@ -427,3 +427,7 @@ @cp_calendar-now: @cryptpad_color_brand_300; @cp_calendar-now-fg: @cryptpad_color_grey_800; +// Forms +@cp_forms-bg1: @cryptpad_color_grey_800; +@cp_forms-bg2: @cryptpad_color_grey_900; +@cp_forms-border: @cryptpad_color_grey_800; diff --git a/customize.dist/src/less2/include/colortheme.less b/customize.dist/src/less2/include/colortheme.less index e56676213..012a66239 100644 --- a/customize.dist/src/less2/include/colortheme.less +++ b/customize.dist/src/less2/include/colortheme.less @@ -426,3 +426,8 @@ @cp_calendar-border: @cryptpad_color_grey_300; @cp_calendar-now: @cryptpad_color_brand; @cp_calendar-now-fg: @cryptpad_color_grey_200; + +// Forms +@cp_forms-bg1: @cryptpad_color_grey_200; +@cp_forms-bg2: @cryptpad_color_grey_100; +@cp_forms-border: @cryptpad_color_grey_200; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 173aa9c9f..8419db9ba 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -69,7 +69,7 @@ define([ }, cb); }; - common.getAccessKeys = function (cb) { + common.getAccessKeys = function (cb, opts) { var keys = []; Nthen(function (waitFor) { // Push account keys @@ -84,6 +84,7 @@ define([ }); } catch (e) { console.error(e); } })); + // Push teams keys postMessage("GET", { key: ['teams'], @@ -92,6 +93,7 @@ define([ 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); }); diff --git a/www/form/app-form.less b/www/form/app-form.less index 996de4ad7..ee62e2c89 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -20,6 +20,17 @@ 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; @@ -34,8 +45,13 @@ 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-content, div.cp-form-creator-results { padding: 10px; display: flex; flex-flow: column; @@ -86,6 +102,54 @@ } } } + div.cp-form-creator-results { + display: flex; + flex-flow: column; + position: relative; + & > div { + background: @cp_forms-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_forms-bg2; + } + .cp-form-results-type-text { + max-height: 300px; + overflow: auto; + .cp-form-results-type-text-data { + padding: 5px 10px; + background: @cp_forms-bg2; + &:not(:last-child) { margin-bottom: 1px; } + } + } + .cp-form-results-type-radio { + display: table; + .cp-form-results-type-radio-data { + display: table-row; + border: 1px solid @cp_forms-border; + & > span { + border: 1px solid @cp_forms-border; + display: table-cell; + padding: 5px 10px; + background: @cp_forms-bg2; + &.cp-value { + min-width: 200px; + } + } + } + } + } } } diff --git a/www/form/inner.js b/www/form/inner.js index b62c4e0da..246d8149f 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -59,21 +59,17 @@ define([ Messages.form_duplicates = "Duplicate entries have been removed"; + Messages.form_submit = "Submit"; + Messages.form_update = "Update"; Messages.form_reset = "Reset"; Messages.form_sent = "Sent"; Messages.form_cantFindAnswers = "Unable to retrieve your existing answers for this form."; - // XXX to update our own answers, we need to store the server hash of the message - // and we'll be able to use getHistoryRange to fetch this message when we come back + Messages.form_viewResults = "Go to responses"; + Messages.form_viewCreator = "Go to form creator"; - - var makeFormSettings = function (framework) { - // XXX - // Button to set results as public - // Checkbox to allow anonymous answers - // Button to clear all answers? - }; + Messages.form_notAnswered = "And {0} empty answers"; var editOptions = function (v, cb) { var add = h('button.btn.btn-secondary', [ @@ -128,6 +124,12 @@ define([ ]; }; + var getEmpty = function (empty) { + if (empty) { + return UI.setHTML(h('div.cp-form-results-type-text-empty'), Messages._getKey('form_notAnswered', [empty])); + } + }; + var TYPES = { input: { get: function () { @@ -140,6 +142,19 @@ define([ 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.fa.fa-font') }, radio: { @@ -185,12 +200,33 @@ define([ }; }, + 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.fa.fa-list-ul') } }; - var makeFormControls = function (framework, content) { - var send = h('button.btn.btn-primary', Messages.poll_commit); + var makeFormControls = function (framework, content, update) { + var send = h('button.btn.btn-primary', update ? Messages.form_update : Messages.form_submit); var reset = h('button.btn.btn-danger-alt', Messages.form_reset); $(reset).click(function () { if (!Array.isArray(APP.formBlocks)) { return; } @@ -211,12 +247,13 @@ define([ mailbox: content.answers, results: results }, function (err, data) { - console.error(data); + $send.attr('disabled', 'disabled'); if (err || (data && data.error)) { console.error(err || data.error); return void UI.warn(Messages.error); } UI.alert(Messages.form_sent); + $send.text(Messages.form_update); }); }); return h('div.cp-form-send-container', [send, reset]); @@ -321,7 +358,30 @@ define([ }); $container.empty().append(elements); - $container.append(makeFormControls(framework, content)); + $container.append(makeFormControls(framework, content, Boolean(answers))); + }; + + var renderResults = function (content, answers) { + var $container = $('div.cp-form-creator-results').empty(); + var form = content.form; + var elements = Object.keys(form).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); + + 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), + ]); + }); + $container.append(elements); }; var andThen = function (framework) { @@ -333,6 +393,39 @@ define([ var priv = metadataMgr.getPrivateData(); APP.isEditor = Boolean(priv.form_public); + var $body = $('body'); + + var makeFormSettings = function () { + var viewResults = h('button.btn.btn-primary', [ + h('span.cp-app-form-button-results', Messages.form_viewResults), + h('span.cp-app-form-button-creator', Messages.form_viewCreator), + ]); + var $v = $(viewResults).click(function () { + if ($body.hasClass('cp-app-form-results')) { + $body.removeClass('cp-app-form-results'); + return; + } + $v.attr('disabled', 'disabled'); + sframeChan.query("Q_FORM_FETCH_ANSWERS", { + channel: content.answers.channel, + validateKey: content.answers.validateKey, + publicKey: content.answers.publicKey + }, function (err, answers) { + $v.removeAttr('disabled'); + $body.addClass('cp-app-form-results'); + renderResults(content, answers); + }); + + }); + return [ + viewResults + ]; + + // XXX + // Button to set results as public + // Checkbox to allow anonymous answers + // Button to clear all answers? + }; var makeFormCreator = function () { @@ -356,13 +449,21 @@ define([ }); return btn; }); - controlContainer = h('div.cp-form-creator-control', controls); + + var settings = makeFormSettings(); + + controlContainer = h('div.cp-form-creator-control', [ + h('div.cp-form-creator-settings', settings), + h('div.cp-form-creator-types', controls) + ]); } 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 ]); return div; }; @@ -397,8 +498,10 @@ define([ if (APP.isEditor) { sframeChan.query("Q_FORM_FETCH_ANSWERS", { channel: content.answers.channel, + validateKey: content.answers.validateKey, publicKey: content.answers.publicKey - }, function () { + }, function (err, obj) { + if (obj) { APP.answers = obj; } updateForm(framework, content, true); }); diff --git a/www/form/main.js b/www/form/main.js index 089c3433e..95f162864 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -30,7 +30,7 @@ define([ }; var addRpc = function (sframeChan, Cryptpad, Utils) { sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, cb) { - var keys; + var myKeys; var CPNetflux; var network; nThen(function (w) { @@ -40,7 +40,15 @@ define([ CPNetflux = _CPNetflux; })); Cryptpad.getAccessKeys(w(function (_keys) { - keys = _keys; + if (!Array.isArray(_keys)) { return; } + + _keys.some(function (_k) { + if ((!Cryptpad.initialTeam && !_k.id) || Cryptpad.initialTeam === _k.id) { + myKeys = _k; + return true; + } + }); + console.error(myKeys); })); Cryptpad.makeNetwork(w(function (err, nw) { network = nw; @@ -52,24 +60,31 @@ define([ var crypto = Utils.Crypto.Mailbox.createEncryptor({ curvePrivate: privateKey, - curvePublic: publicKey || data.publicKey + curvePublic: publicKey || data.publicKey, + validateKey: data.validateKey }); var config = { network: network, channel: data.channel, noChainPad: true, validateKey: keys.secondaryValidateKey, - owners: [], // XXX add pad owner + owners: [myKeys.edPublic], // XXX add pad owner crypto: crypto, // XXX Cache }; + var results = {}; config.onReady = function () { - cb(); - // XXX + cb(results); network.disconnect(); }; - config.onMessage = function () { - // XXX + config.onMessage = function (msg, peer, vKey, isCp, hash, senderCurve, cfg) { + var parsed = Utils.Util.tryParse(msg); + if (!parsed) { return; } + results[senderCurve] = { + msg: parsed, + hash: hash, + time: cfg.time + }; }; CPNetflux.start(config); }); From d06cba0b5ebb2d24fda12a50864bd12d983c2369 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 21 May 2021 17:40:41 +0200 Subject: [PATCH 05/44] Sort and delete form questions --- .../src/less2/include/colortheme-dark.less | 6 +- .../src/less2/include/colortheme.less | 6 +- customize.dist/src/less2/include/forms.less | 8 +- www/common/common-interface.js | 6 +- www/form/app-form.less | 45 ++++-- www/form/inner.js | 128 +++++++++++++++--- 6 files changed, 163 insertions(+), 36 deletions(-) diff --git a/customize.dist/src/less2/include/colortheme-dark.less b/customize.dist/src/less2/include/colortheme-dark.less index 875ad004b..68a456bcc 100644 --- a/customize.dist/src/less2/include/colortheme-dark.less +++ b/customize.dist/src/less2/include/colortheme-dark.less @@ -428,6 +428,6 @@ @cp_calendar-now-fg: @cryptpad_color_grey_800; // Forms -@cp_forms-bg1: @cryptpad_color_grey_800; -@cp_forms-bg2: @cryptpad_color_grey_900; -@cp_forms-border: @cryptpad_color_grey_800; +@cp_form-bg1: @cryptpad_color_grey_800; +@cp_form-bg2: @cryptpad_color_grey_900; +@cp_form-border: @cryptpad_color_grey_800; diff --git a/customize.dist/src/less2/include/colortheme.less b/customize.dist/src/less2/include/colortheme.less index 012a66239..c385cd83c 100644 --- a/customize.dist/src/less2/include/colortheme.less +++ b/customize.dist/src/less2/include/colortheme.less @@ -428,6 +428,6 @@ @cp_calendar-now-fg: @cryptpad_color_grey_200; // Forms -@cp_forms-bg1: @cryptpad_color_grey_200; -@cp_forms-bg2: @cryptpad_color_grey_100; -@cp_forms-border: @cryptpad_color_grey_200; +@cp_form-bg1: @cryptpad_color_grey_200; +@cp_form-bg2: @cryptpad_color_grey_100; +@cp_form-border: @cryptpad_color_grey_200; 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/common-interface.js b/www/common/common-interface.js index dbcb089f3..bba514dbf 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; } diff --git a/www/form/app-form.less b/www/form/app-form.less index ee62e2c89..4ea9d3cf6 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -11,6 +11,8 @@ display: flex; flex-flow: column; + font: @colortheme_app-font; + color: @cryptpad_text_col; #cp-app-form-editor { flex: 1; @@ -56,17 +58,26 @@ display: flex; flex-flow: column; flex: 1; + overflow: auto; + .cp-form-block { + .tools_unselectable(); + background: @cp_form-bg1; + padding: 10px; &:not(:last-child) { margin-bottom: 20px; } + .cp-form-block-question { + margin-bottom: 5px; + } .cp-form-input-block { display: flex; //width: @form_input-width; - &:not(:focus-within) { + &:not(.editing) { input { background: transparent; border: none; + padding: 0 !important; & ~ button:not(:disabled) { .cp-form-edit { display: inline; } .cp-form-save { display: none; } @@ -76,15 +87,24 @@ input { flex: 1; min-width: 100px; + padding: 0 10px !important; + height: auto; } button { .cp-form-edit { display: none; - margin: 0 !important; } .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-block { .cp-form-edit-block-input { @@ -107,7 +127,7 @@ flex-flow: column; position: relative; & > div { - background: @cp_forms-bg1; + background: @cp_form-bg1; padding: 10px; &:not(:last-child) { margin-bottom: 20px; @@ -122,14 +142,14 @@ margin-top: -10px; margin-right: -10px; i { margin-right: 5px; } - background: @cp_forms-bg2; + 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_forms-bg2; + background: @cp_form-bg2; &:not(:last-child) { margin-bottom: 1px; } } } @@ -137,12 +157,12 @@ display: table; .cp-form-results-type-radio-data { display: table-row; - border: 1px solid @cp_forms-border; + border: 1px solid @cp_form-border; & > span { - border: 1px solid @cp_forms-border; + border: 1px solid @cp_form-border; display: table-cell; padding: 5px 10px; - background: @cp_forms-bg2; + background: @cp_form-bg2; &.cp-value { min-width: 200px; } @@ -152,5 +172,14 @@ } } + .cp-form-type-radio { + display: flex; + flex-flow: column; + align-items: baseline; + .cp-radio { + display: inline-flex; + } + } + } diff --git a/www/form/inner.js b/www/form/inner.js index 246d8149f..b65e30278 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -20,6 +20,8 @@ define([ '/common/inner/access.js', '/common/inner/properties.js', + '/bower_components/sortablejs/Sortable.min.js', + '/bower_components/file-saver/FileSaver.min.js', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'less!/form/app-form.less', @@ -40,7 +42,8 @@ define([ h, Messages, AppConfig, - Share, Access, Properties + Share, Access, Properties, + Sortable ) { var SaveAs = window.saveAs; @@ -50,6 +53,7 @@ define([ Messages.button_newform = "New Form"; // XXX Messages.form_invalid = "Invalid form"; Messages.form_editBlock = "Edit options"; + Messages.form_editQuestion = "Edit question"; Messages.form_newOption = "New option"; @@ -63,6 +67,7 @@ define([ Messages.form_update = "Update"; Messages.form_reset = "Reset"; Messages.form_sent = "Sent"; + Messages.form_delete = "Delete block"; Messages.form_cantFindAnswers = "Unable to retrieve your existing answers for this form."; @@ -170,7 +175,7 @@ define([ $(radio).find('input').data('val', data); return radio; }); - var tag = h('div.radio-group', els); + var tag = h('div.radio-group.cp-form-type-radio', els); return { tag: tag, getValue: function () { @@ -266,7 +271,7 @@ define([ APP.formBlocks = []; // XXX order array later - var elements = Object.keys(form).map(function (uid) { + var elements = content.order.map(function (uid) { var block = form[uid]; var type = block.type; var model = TYPES[type]; @@ -277,7 +282,7 @@ define([ if (answers && answers[uid]) { data.setValue(answers[uid]); } var q = h('div.cp-form-block-question', block.q || Messages.form_default); - var edit, editContainer; + var editButtons, editContainer; APP.formBlocks.push(data); @@ -288,32 +293,64 @@ define([ value: block.q || Messages.form_default }); var $inputQ = $(inputQ); - var saveQ = h('button.btn.btn-primary', [ + var saveQ = h('button.btn.btn-primary.small', [ h('i.fa.fa-pencil.cp-form-edit'), + h('span.cp-form-edit', Messages.form_editQuestion), + h('i.fa.fa-floppy-o.cp-form-save'), h('span.cp-form-save', Messages.settings_save) ]); + var dragHandle = h('i.fa.fa-arrows-v.cp-form-block-drag'); + var $saveQ = $(saveQ).click(function () { + if (!$(q).hasClass('editing')) { + $(q).addClass('editing'); + $inputQ.focus(); + return; + } var v = $inputQ.val(); - if (!v || !v.trim() || v === block.q) { return; } + if (!v || !v.trim()) { return void UI.warn(Messages.error); } block.q = v.trim(); framework.localChange(); $saveQ.attr('disabled', 'disabled'); framework._.cpNfInner.chainpad.onSettle(function () { + $(q).removeClass('editing'); $saveQ.removeAttr('disabled'); - $saveQ.blur(); + $inputQ.blur(); UI.log(Messages.saved); }); }); - var onBlur = function (e) { + var onCancelQ = function (e) { if (e && e.relatedTarget && e.relatedTarget === saveQ) { return; } - $inputQ.val(block.q); + $inputQ.val(block.q || Messages.form_default); + if (!e) { $inputQ.blur(); } + $(q).removeClass('editing'); }; $inputQ.keydown(function (e) { if (e.which === 13) { return void $saveQ.click(); } - if (e.which === 27) { return void $inputQ.blur(); } + if (e.which === 27) { return void onCancelQ(); } + }); + $inputQ.focus(function () { + $(q).addClass('editing'); + }); + $inputQ.blur(onCancelQ); + q = h('div.cp-form-input-block', [inputQ, saveQ, dragHandle]); + + // Delete question + var edit; + var del = h('button.btn.btn-danger', [ + 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(); }); - $inputQ.blur(onBlur); - q = h('div.cp-form-input-block', [inputQ, saveQ]); // Values if (data.edit) { @@ -325,7 +362,7 @@ define([ var onSave = function (newOpts) { if (!newOpts) { // Cancel edit $(editContainer).empty(); - $edit.show(); + $(editButtons).show(); $(data.tag).show(); return; } @@ -334,37 +371,62 @@ define([ framework.localChange(); var $oldTag = $(data.tag); framework._.cpNfInner.chainpad.onSettle(function () { - $edit.show(); + $(editButtons).show(); UI.log(Messages.saved); data = model.get(newOpts); $oldTag.before(data.tag).remove(); }); }; - var $edit = $(edit).click(function () { + $(edit).click(function () { $(data.tag).hide(); $(editContainer).append(data.edit(onSave)); - $edit.hide(); + $(editButtons).hide(); }); } + + editButtons = h('div.cp-form-edit-buttons-container', [ + edit, del + ]); } - return h('div.cp-form-block', [ + var editableCls = editable ? ".editable" : ""; + return h('div.cp-form-block'+editableCls, { + 'data-id':uid + }, [ q, h('div.cp-form-block-content', [ data.tag, - edit + editButtons ]), editContainer ]); }); $container.empty().append(elements); + + if (editable) { + Sortable.create($container[0], { + direction: "vertical", + filter: "input, button", + preventOnFilter: false, + store: { + set: function (s) { + content.order = s.toArray(); + framework.localChange(); + } + } + }); + + return; + } + + // In view mode, add "Submit" and "reset" buttons $container.append(makeFormControls(framework, content, Boolean(answers))); }; var renderResults = function (content, answers) { var $container = $('div.cp-form-creator-results').empty(); var form = content.form; - var elements = Object.keys(form).map(function (uid) { + var elements = content.order.map(function (uid) { var block = form[uid]; var type = block.type; var model = TYPES[type]; @@ -427,6 +489,26 @@ define([ // Button to clear all answers? }; + var checkIntegrity = function (getter) { + 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; @@ -444,6 +526,7 @@ define([ //opts: opts type: type, }; + content.order.push(uid); framework.localChange(); updateForm(framework, content, true); }); @@ -479,6 +562,10 @@ define([ 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(), @@ -502,6 +589,7 @@ define([ publicKey: content.answers.publicKey }, function (err, obj) { if (obj) { APP.answers = obj; } + checkIntegrity(false); updateForm(framework, content, true); }); @@ -518,6 +606,7 @@ define([ } var answers; if (obj && !obj.error) { answers = obj; } + checkIntegrity(false); updateForm(framework, content, false, answers); }); @@ -530,6 +619,7 @@ define([ }); framework.setContentGetter(function () { + checkIntegrity(true); return content; }); From 5c402a00a3937ebea622b57bdba953b02e527f2e Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 25 May 2021 13:51:03 +0200 Subject: [PATCH 06/44] Make results public and view results as participant --- www/form/inner.js | 100 ++++++++++++++++++++++++++++++++-------------- www/form/main.js | 4 +- 2 files changed, 73 insertions(+), 31 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index b65e30278..d748c5462 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -76,6 +76,11 @@ define([ Messages.form_notAnswered = "And {0} empty answers"; + Messages.form_makePublic = "Make public"; + 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"; + var editOptions = function (v, cb) { var add = h('button.btn.btn-secondary', [ h('i.fa.fa-plus'), @@ -230,6 +235,29 @@ define([ } }; + var renderResults = function (content, answers) { + var $container = $('div.cp-form-creator-results').empty(); + var form = content.form; + 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); + + 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), + ]); + }); + $container.append(elements); + }; + var makeFormControls = function (framework, content, update) { var send = h('button.btn.btn-primary', update ? Messages.form_update : Messages.form_submit); var reset = h('button.btn.btn-danger-alt', Messages.form_reset); @@ -261,7 +289,22 @@ define([ $send.text(Messages.form_update); }); }); - return h('div.cp-form-send-container', [send, reset]); + + if (content.answers.privateKey) { + var viewResults = h('button.btn.btn-primary', [ + h('span.cp-app-form-button-results', Messages.form_viewResults), + ]); + var sframeChan = framework._.sfCommon.getSframeChannel(); + var $v = $(viewResults).click(function () { + $v.attr('disabled', 'disabled'); + sframeChan.query("Q_FORM_FETCH_ANSWERS", content.answers, function (err, answers) { + $v.removeAttr('disabled'); + $('body').addClass('cp-app-form-results'); + renderResults(content, answers); + }); + }); + } + return h('div.cp-form-send-container', [send, reset, viewResults]); }; var updateForm = function (framework, content, editable, answers) { var $container = $('div.cp-form-creator-content'); @@ -423,29 +466,6 @@ define([ $container.append(makeFormControls(framework, content, Boolean(answers))); }; - var renderResults = function (content, answers) { - var $container = $('div.cp-form-creator-results').empty(); - var form = content.form; - 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); - - 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), - ]); - }); - $container.append(elements); - }; - var andThen = function (framework) { framework.start(); var content = {}; @@ -458,6 +478,27 @@ define([ var $body = $('body'); var makeFormSettings = function () { + var makePublic = h('button.btn.btn-primary', Messages.form_makePublic); + if (content.answers.privateKey) { makePublic = undefined; } + var publicText = content.answers.privateKey ? Messages.form_isPublic : Messages.form_isPrivate; + var resultsType = h('div.cp-form-results-type-container', [ + h('span.cp-form-results-type', publicText), + makePublic + ]); + var $makePublic = $(makePublic).click(function () { + UI.confirm(Messages.form_makePublicWarning, function (yes) { + if (!yes) { return; } + content.answers.privateKey = priv.form_private; + framework.localChange(); + framework._.cpNfInner.chainpad.onSettle(function () { + UI.log(Messages.saved); + $makePublic.remove(); + $(resultsType).find('.cp-form-results-type').text(Messages.form_isPublic); + }); + }); + }); + + var viewResults = h('button.btn.btn-primary', [ h('span.cp-app-form-button-results', Messages.form_viewResults), h('span.cp-app-form-button-creator', Messages.form_viewCreator), @@ -480,7 +521,8 @@ define([ }); return [ - viewResults + resultsType, + viewResults, ]; // XXX @@ -551,12 +593,12 @@ define([ return div; }; - var $container = $('#cp-app-form-container'); - $container.append(makeFormCreator()); - if (!APP.isEditor) { makeFormControls(); } - framework.onReady(function (isNew) { var priv = metadataMgr.getPrivateData(); + + var $container = $('#cp-app-form-container'); + $container.append(makeFormCreator()); + if (APP.isEditor) { if (!content.form) { content.form = {}; diff --git a/www/form/main.js b/www/form/main.js index 95f162864..32810bd1c 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -26,7 +26,7 @@ define([ meta.form_answerValidateKey = validateKey; publicKey = meta.form_public = Nacl.util.encodeBase64(curvePair.publicKey); - privateKey = Nacl.util.encodeBase64(curvePair.secretKey); + privateKey = meta.form_private = Nacl.util.encodeBase64(curvePair.secretKey); }; var addRpc = function (sframeChan, Cryptpad, Utils) { sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, cb) { @@ -59,7 +59,7 @@ define([ var keys = Utils.secret && Utils.secret.keys; var crypto = Utils.Crypto.Mailbox.createEncryptor({ - curvePrivate: privateKey, + curvePrivate: privateKey || data.privateKey, curvePublic: publicKey || data.publicKey, validateKey: data.validateKey }); From 6f64d62698c16e60a89a4c7e2b1665533c65febb Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 25 May 2021 15:15:51 +0200 Subject: [PATCH 07/44] Realtime changes --- www/form/inner.js | 98 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 83 insertions(+), 15 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index d748c5462..d82aeae90 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -27,7 +27,7 @@ define([ 'less!/form/app-form.less', ], function ( $, - JSONSortify, + Sortify, Crypto, Framework, Toolbar, @@ -81,15 +81,29 @@ define([ Messages.form_isPublic = "Results are public"; Messages.form_isPrivate = "Results are private"; - var editOptions = function (v, cb) { + var editOptions = function (v, setCursorGetter, cb, tmp) { var add = h('button.btn.btn-secondary', [ h('i.fa.fa-plus'), h('span', Messages.tag_add) ]); + var cursor; + if (tmp && tmp.content && Sortify(v) === Sortify(tmp.old)) { + v = tmp.content.values; + cursor = tmp.cursor; + } + // Show existing options var getOption = function (val) { var input = h('input', {value:val}); + + // if this element was active before the remote change, restore cursor + if (cursor && cursor.el === val) { + input.selectionStart = cursor.start || 0; + input.selectionEnd = cursor.end || 0; + setTimeout(function () { input.focus(); }); + } + 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(); }); @@ -108,6 +122,26 @@ define([ var cancelBlock = h('button.btn.btn-secondary', Messages.cancel); $(cancelBlock).click(function () { cb(); }); + // 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) { + cursor.el= $(el).val(); + cursor.start = el.selectionStart; + cursor.end = el.selectionEnd; + } + values.push($(el).val()); + }); + return { + old: v, + content: {values: values}, + cursor: cursor + }; + }); + // Save changes var saveBlock = h('button.btn.btn-primary', [ h('i.fa.fa-floppy-o'), @@ -181,6 +215,8 @@ define([ return radio; }); var tag = h('div.radio-group.cp-form-type-radio', els); + var cursorGetter; + var setCursorGetter = function (f) { cursorGetter = f; }; return { tag: tag, getValue: function () { @@ -193,10 +229,11 @@ define([ return res; }, reset: function () { $(tag).find('input').removeAttr('checked'); }, - edit: function (cb) { + edit: function (cb, tmp) { var v = opts.values.slice(); - return editOptions(v, cb); + return editOptions(v, setCursorGetter, cb, tmp); }, + getCursor: function () { return cursorGetter(); }, setValue: function (val) { this.reset(); els.some(function (el) { @@ -258,6 +295,14 @@ define([ $container.append(elements); }; + var getFormResults = function () { + if (!Array.isArray(APP.formBlocks)) { return; } + var results = {}; + APP.formBlocks.forEach(function (data) { + results[data.uid] = data.getValue(); + }); + return results; + }; var makeFormControls = function (framework, content, update) { var send = h('button.btn.btn-primary', update ? Messages.form_update : Messages.form_submit); var reset = h('button.btn.btn-danger-alt', Messages.form_reset); @@ -269,12 +314,8 @@ define([ }); var $send = $(send).click(function () { $send.attr('disabled', 'disabled'); - if (!Array.isArray(APP.formBlocks)) { return; } - var results = {}; - APP.formBlocks.forEach(function (data) { - results[data.uid] = data.getValue(); - }); - + var results = getFormResults(); + if (!results) { return; } var sframeChan = framework._.sfCommon.getSframeChannel(); sframeChan.query('Q_FORM_SUBMIT', { mailbox: content.answers, @@ -306,8 +347,9 @@ define([ } return h('div.cp-form-send-container', [send, reset, viewResults]); }; - var updateForm = function (framework, content, editable, answers) { + 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; @@ -403,6 +445,7 @@ define([ ]); editContainer = h('div'); var onSave = function (newOpts) { + data.editing = false; if (!newOpts) { // Cancel edit $(editContainer).empty(); $(editButtons).show(); @@ -420,11 +463,22 @@ define([ $oldTag.before(data.tag).remove(); }); }; - $(edit).click(function () { + var onEdit = function (tmp) { + data.editing = true; $(data.tag).hide(); - $(editContainer).append(data.edit(onSave)); + $(editContainer).append(data.edit(onSave, tmp)); $(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', [ @@ -458,7 +512,6 @@ define([ } } }); - return; } @@ -466,6 +519,18 @@ define([ $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 content = {}; @@ -657,7 +722,10 @@ define([ framework.onContentUpdate(function (newContent) { console.log(newContent); content = newContent; - updateForm(framework, content, APP.isEditor); + var answers, temp; + if (!APP.isEditor) { answers = getFormResults(); } + else { temp = getTempFields(); } + updateForm(framework, content, APP.isEditor, answers, temp); }); framework.setContentGetter(function () { From 4a1de329946e545bf9adc1393d8d890f84ab5d45 Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 26 May 2021 11:57:14 +0200 Subject: [PATCH 08/44] Dedicated form share modal and auditor role --- www/common/common-hash.js | 17 +++++++++++++++++ www/common/inner/share.js | 32 ++++++++++++++++++++++++++++---- www/common/toolbar.js | 4 +++- www/form/inner.js | 14 ++++++++++++++ www/form/main.js | 19 +++++++++++++++++-- www/secureiframe/inner.js | 1 + 6 files changed, 80 insertions(+), 7 deletions(-) diff --git a/www/common/common-hash.js b/www/common/common-hash.js index c59f5babe..b9cdfa699 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -215,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 @@ -237,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); @@ -278,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 @@ -304,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/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/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/inner.js b/www/form/inner.js index d82aeae90..f37b3f3e4 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -689,6 +689,20 @@ define([ // 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 (priv.form_auditorKey) { + sframeChan.query("Q_FORM_FETCH_ANSWERS", { + channel: content.answers.channel, + validateKey: content.answers.validateKey, + publicKey: content.answers.publicKey, + privateKey: priv.form_auditorKey + }, function (err, obj) { + $body.addClass('cp-app-form-results'); + renderResults(content, obj); + }); + return; + } + if (APP.isEditor) { sframeChan.query("Q_FORM_FETCH_ANSWERS", { channel: content.answers.channel, diff --git a/www/form/main.js b/www/form/main.js index 32810bd1c..4ebe99295 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -19,6 +19,13 @@ define([ var privateKey, publicKey; 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.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)); @@ -27,10 +34,19 @@ define([ 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('Q_FORM_FETCH_ANSWERS', function (data, cb) { - var myKeys; + var myKeys = {}; var CPNetflux; var network; nThen(function (w) { @@ -48,7 +64,6 @@ define([ return true; } }); - console.error(myKeys); })); Cryptpad.makeNetwork(w(function (err, nw) { network = nw; 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 () { From c28cf2046442b2e114dd5afa4292f1409798d182 Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 26 May 2021 12:03:27 +0200 Subject: [PATCH 09/44] Fix form template --- www/common/cryptpad-common.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 8419db9ba..973a27d6b 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -743,6 +743,10 @@ define([ delete meta.chat2; delete meta.chat; delete meta.cursor; + + if (meta.type === "form") { + delete parsed.answers; + } } }; From 9a1a1830fc5e000cf8c8fc5a571f7da9a53c3829 Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 26 May 2021 12:03:32 +0200 Subject: [PATCH 10/44] Fix type error --- www/form/inner.js | 6 +++--- www/form/main.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index f37b3f3e4..bfc7dac3a 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -661,9 +661,6 @@ define([ framework.onReady(function (isNew) { var priv = metadataMgr.getPrivateData(); - var $container = $('#cp-app-form-container'); - $container.append(makeFormCreator()); - if (APP.isEditor) { if (!content.form) { content.form = {}; @@ -683,6 +680,9 @@ define([ } } + 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); } diff --git a/www/form/main.js b/www/form/main.js index 4ebe99295..ca4aea7e6 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -21,7 +21,7 @@ define([ var keys = Utils.secret && Utils.secret.keys; var parsed = Utils.Hash.parseTypeHash('pad', hash.slice(1)); - if (parsed.auditorKey) { + if (parsed && parsed.auditorKey) { meta.form_auditorKey = parsed.auditorKey; meta.form_auditorHash = hash; } From 378cd38fbdf5755bc7092faa320f6c9b4a7e16fe Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 26 May 2021 14:54:56 +0200 Subject: [PATCH 11/44] Access and Properties modals for forms --- www/common/common-ui-elements.js | 3 +++ www/common/inner/access.js | 32 ++++++++++++++++++++++++++++++-- www/common/inner/properties.js | 2 ++ www/common/outer/async-store.js | 12 ++++++++++++ www/common/proxy-manager.js | 5 +++++ www/form/inner.js | 3 +++ www/form/main.js | 22 +++++++++++++++++++--- 7 files changed, 74 insertions(+), 5 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index e8c0cce4c..4e33b83d0 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -3013,6 +3013,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) { @@ -3062,6 +3063,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) { @@ -3078,6 +3080,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/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/outer/async-store.js b/www/common/outer/async-store.js index 0e1e9684e..2d08b52d4 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -2139,11 +2139,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 diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index 9371b9e8b..526677d4b 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -810,6 +810,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); @@ -1176,6 +1177,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/form/inner.js b/www/form/inner.js index bfc7dac3a..f53141842 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -597,6 +597,7 @@ define([ }; var checkIntegrity = function (getter) { + if (!content.order || !content.form) { return; } var changed = false; content.order.forEach(function (uid) { if (!content.form[uid]) { @@ -680,6 +681,8 @@ define([ } } + sframeChan.event('EV_FORM_PIN', {channel: content.answers.channel}); + var $container = $('#cp-app-form-container'); $container.append(makeFormCreator()); diff --git a/www/form/main.js b/www/form/main.js index ca4aea7e6..c02921887 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -17,6 +17,10 @@ define([ 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; @@ -45,6 +49,17 @@ define([ }; 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 () {}); + }); + }); + + }); sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, cb) { var myKeys = {}; var CPNetflux; @@ -83,7 +98,7 @@ define([ channel: data.channel, noChainPad: true, validateKey: keys.secondaryValidateKey, - owners: [myKeys.edPublic], // XXX add pad owner + owners: [myKeys.edPublic], crypto: crypto, // XXX Cache }; @@ -162,7 +177,7 @@ define([ var text = JSON.stringify(data.results); var ciphertext = crypto.encrypt(text, box.publicKey); - var hash = ciphertext.slice(0,64); // XXX use this to recover our previous answers + var hash = ciphertext.slice(0,64); Cryptpad.anonRpcMsg("WRITE_PRIVATE_MESSAGE", [ box.channel, ciphertext @@ -190,7 +205,8 @@ define([ hash: hash, href: href, useCreationScreen: true, - messaging: true + messaging: true, + getPropChannels: getPropChannels }); }); }); From 4577a3baf0ed5953059d370d6030bdf6d83463a3 Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 26 May 2021 17:02:34 +0200 Subject: [PATCH 12/44] Open and close a form --- www/common/common-ui-elements.js | 1 + www/form/app-form.less | 5 +- www/form/inner.js | 126 ++++++++++++++++++++++++++++++- 3 files changed, 129 insertions(+), 3 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 4e33b83d0..41b5dfe62 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1133,6 +1133,7 @@ define([ sheet: 'sheets', poll: 'poll', kanban: 'kanban', + form: 'form', whiteboard: 'whiteboard', }; diff --git a/www/form/app-form.less b/www/form/app-form.less index 4ea9d3cf6..cc67ff4f1 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -38,6 +38,10 @@ flex: 1; justify-content: center; + .cp-form-input-block { + display: flex; + } + div.cp-form-creator-container { display: flex; flex: 1; @@ -71,7 +75,6 @@ margin-bottom: 5px; } .cp-form-input-block { - display: flex; //width: @form_input-width; &:not(.editing) { input { diff --git a/www/form/inner.js b/www/form/inner.js index f53141842..01cb9275b 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -20,9 +20,11 @@ define([ '/common/inner/access.js', '/common/inner/properties.js', + '/lib/datepicker/flatpickr.js', '/bower_components/sortablejs/Sortable.min.js', '/bower_components/file-saver/FileSaver.min.js', + 'css!/lib/datepicker/flatpickr.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'less!/form/app-form.less', ], function ( @@ -43,6 +45,7 @@ define([ Messages, AppConfig, Share, Access, Properties, + Flatpickr, Sortable ) { @@ -81,6 +84,13 @@ define([ 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}"; + var editOptions = function (v, setCursorGetter, cb, tmp) { var add = h('button.btn.btn-secondary', [ h('i.fa.fa-plus'), @@ -304,8 +314,8 @@ define([ return results; }; var makeFormControls = function (framework, content, update) { - var send = h('button.btn.btn-primary', update ? Messages.form_update : Messages.form_submit); - var reset = h('button.btn.btn-danger-alt', Messages.form_reset); + 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) { @@ -345,6 +355,12 @@ define([ }); }); } + + if (APP.isClosed) { + send = undefined; + reset = undefined; + } + return h('div.cp-form-send-container', [send, reset, viewResults]); }; var updateForm = function (framework, content, editable, answers, temp) { @@ -542,6 +558,11 @@ define([ 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 () { var makePublic = h('button.btn.btn-primary', Messages.form_makePublic); if (content.answers.privateKey) { makePublic = undefined; } @@ -563,6 +584,73 @@ define([ }); }); + // 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; + action = function () { + }; + } 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 is24h = false; + var dateFormat = "Y-m-d H:i"; + try { + is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/); + } catch (e) {} + if (!is24h) { dateFormat = "Y-m-d h:i K"; } + 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(); + }); + + $endDate.append(h('div.cp-form-status', text)); + $endDate.append(h('div.cp-form-actions', button)); + + }; + refreshEndDate(); + var viewResults = h('button.btn.btn-primary', [ h('span.cp-app-form-button-results', Messages.form_viewResults), @@ -586,6 +674,7 @@ define([ }); return [ + endDateContainer, resultsType, viewResults, ]; @@ -659,6 +748,35 @@ define([ return div; }; + var endDateEl = h('div.alert.alert-warning.cp-burn-after-reading'); + var endDate; + var endDateTo; + var refreshEndDateBanner = function (force) { + 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 (isNew) { var priv = metadataMgr.getPrivateData(); @@ -720,6 +838,9 @@ define([ return; } + refreshEndDateBanner(); + + sframeChan.query("Q_FETCH_MY_ANSWERS", { channel: content.answers.channel, validateKey: content.answers.validateKey, @@ -739,6 +860,7 @@ define([ framework.onContentUpdate(function (newContent) { console.log(newContent); content = newContent; + refreshEndDateBanner(); var answers, temp; if (!APP.isEditor) { answers = getFormResults(); } else { temp = getTempFields(); } From 3ce5d477a17cb903c30e0631ebae9e25e9281753 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 27 May 2021 12:44:50 +0200 Subject: [PATCH 13/44] Fix issue allowing users to select disabled checkboxes and radio using the spacebar shortcut --- www/common/common-interface.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/www/common/common-interface.js b/www/common/common-interface.js index bba514dbf..4166c0902 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -1177,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(); @@ -1226,6 +1227,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(); From 29dc1a5b3ba2f5c368b374f1dbd4f6108d203e2d Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 27 May 2021 17:46:46 +0200 Subject: [PATCH 14/44] Add more block types in form --- www/form/app-form.less | 35 ++- www/form/inner.js | 503 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 517 insertions(+), 21 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index cc67ff4f1..f0182d9a7 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -109,6 +109,18 @@ } &.editable { cursor: grab; } } + .cp-form-edit-max-options { + display: flex; + align-items: center; + input { + width: 100px; + margin-left: 10px; + } + } + .cp-form-edit-options-block { + display: flex; + flex-wrap: wrap; + } .cp-form-edit-block { .cp-form-edit-block-input { display: flex; @@ -158,6 +170,10 @@ } .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; @@ -175,7 +191,7 @@ } } - .cp-form-type-radio { + .cp-form-type-radio, .cp-form-type-checkbox { display: flex; flex-flow: column; align-items: baseline; @@ -183,6 +199,23 @@ 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; + } + } + } + } } diff --git a/www/form/inner.js b/www/form/inner.js index 01cb9275b..02f052888 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -57,14 +57,17 @@ define([ Messages.form_invalid = "Invalid form"; Messages.form_editBlock = "Edit options"; Messages.form_editQuestion = "Edit question"; - - Messages.form_newOption = "New option"; + Messages.form_editMax = "Max selectable options"; Messages.form_default = "Your question here?"; Messages.form_type_input = "Text"; // 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_duplicates = "Duplicate entries have been removed"; + Messages.form_maxOptions = "{0} answer(s) max"; Messages.form_submit = "Submit"; Messages.form_update = "Update"; @@ -91,42 +94,109 @@ define([ Messages.form_isClosed = "This form was closed on {0}"; Messages.form_willClose = "This form will close on {0}"; + 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"; + + + var MAX_OPTIONS = 10; // XXX + var MAX_ITEMS = 10; // XXX + var editOptions = function (v, setCursorGetter, cb, tmp) { var add = h('button.btn.btn-secondary', [ h('i.fa.fa-plus'), - h('span', Messages.tag_add) + 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.values; + 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 + ]); + } + // Show existing options - var getOption = function (val) { + var $add; + var getOption = function (val, isItem, uid) { var input = h('input', {value:val}); + if (uid) { $(input).data('uid', uid); } // if this element was active before the remote change, restore cursor if (cursor && cursor.el === val) { + console.log(isItem); + console.log(cursor.item); + console.log(Boolean(isItem)); + console.log(Boolean(cursor.item)); + } + + var setCursor = function () { 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', h('i.fa.fa-times')); var el = h('div.cp-form-edit-block-input', [ input, del ]); - $(del).click(function () { $(el).remove(); }); + $(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(); } + }); return el; }; - var inputs = v.map(getOption); + var inputs = v.values.map(function (val) { return getOption(val, false); }); 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)); + 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); + } + + // "Add option" button handler + $add = $(add).click(function () { + $add.before(getOption(Messages.form_newOption, false)); + if ($(container).find('input').length >= 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(); } // Cancel changes var cancelBlock = h('button.btn.btn-secondary', Messages.cancel); @@ -145,9 +215,31 @@ define([ } values.push($(el).val()); }); + var _content = {values: values}; + + if (maxInput) { + _content.max = Number($(maxInput).val()) || 1; + } + + 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: v, - content: {values: values}, + old: (tmp && tmp.old) || v, + content: _content, cursor: cursor }; }); @@ -159,6 +251,8 @@ define([ ]); $(saveBlock).click(function () { $(saveBlock).attr('disabled', 'disabled'); + + // Get values var values = []; var duplicates = false; $(container).find('input').each(function (i, el) { @@ -166,14 +260,43 @@ define([ if (values.indexOf(val) === -1) { values.push(val); } else { duplicates = true; } }); + 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); } - cb({values: values}); + + // 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; + } + + cb(res); }); return [ - container, + maxOptions, + h('div.cp-form-edit-options-block', [containerItems, container]), h('div', [cancelBlock, saveBlock]) ]; }; @@ -184,6 +307,18 @@ define([ } }; + 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 TYPES = { input: { get: function () { @@ -213,10 +348,13 @@ define([ }, radio: { defaultOpts: { - values: ["Option 1", "Option 2"] // XXX? + values: [1,2].map(function (i) { + return Messages._getKey('form_defaultOption', [i]); + }) }, get: function (opts) { 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, @@ -232,15 +370,17 @@ define([ getValue: function () { var res; els.some(function (el, i) { - if (Util.isChecked($(el).find('input'))) { - res = opts.values[i]; + 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 = opts.values.slice(); + var v = Util.clone(opts); return editOptions(v, setCursorGetter, cb, tmp); }, getCursor: function () { return cursorGetter(); }, @@ -279,7 +419,326 @@ define([ return h('div.cp-form-results-type-radio', results); }, icon: h('i.fa.fa-list-ul') - } + }, + 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) { + 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; }; + return { + tag: tag, + getValue: function () { + var res = {}; + var l = lines.slice(1); + l.forEach(function (el, i) { + var $el = $(el); + var uid = $el.attr('data-uid'); + var $l = $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 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.fa.fa-list-ul') + }, + checkbox: { + defaultOpts: { + max: 3, + values: [1, 2, 3].map(function (i) { + return Messages._getKey('form_defaultOption', [i]); + }) + }, + get: function (opts) { + 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'); + } + }); + var cursorGetter; + var setCursorGetter = function (f) { cursorGetter = f; }; + return { + tag: tag, + getValue: function () { + var res = []; + els.forEach(function (el, i) { + 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.fa.fa-check-square-o') + }, + 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) { + 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'); + } + }); + }); + + 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, i) { + var $el = $(el); + var uid = $el.attr('data-uid'); + res[uid] = []; + var $l = $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.fa.fa-list-ul') + }, }; var renderResults = function (content, answers) { @@ -290,7 +749,7 @@ define([ var type = block.type; var model = TYPES[type]; if (!model || !model.printResults) { return; } - var print = model.printResults(answers, uid); + 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', [ @@ -336,6 +795,7 @@ define([ console.error(err || data.error); return void UI.warn(Messages.error); } + $send.removeAttr('disabled'); UI.alert(Messages.form_sent); $send.text(Messages.form_update); }); @@ -379,6 +839,7 @@ define([ if (!model) { return; } var data = model.get(block.opts); + if (!data) { return; } data.uid = uid; if (answers && answers[uid]) { data.setValue(answers[uid]); } @@ -476,6 +937,7 @@ define([ $(editButtons).show(); UI.log(Messages.saved); data = model.get(newOpts); + if (!data) { data = {}; } $oldTag.before(data.tag).remove(); }); }; @@ -752,6 +1214,7 @@ define([ var endDate; var endDateTo; var refreshEndDateBanner = function (force) { + if (APP.isEditor) { return; } var _endDate = content.answers.endDate; if (_endDate === endDate && !force) { return; } endDate = _endDate; From a948237043209254ef9c3996f2c9b051be1520f6 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 28 May 2021 14:04:24 +0200 Subject: [PATCH 15/44] Add Poll block type --- .../src/less2/include/colortheme-dark.less | 5 + .../src/less2/include/colortheme.less | 5 + www/common/common-ui-elements.js | 2 + www/form/app-form.less | 61 ++++ www/form/inner.js | 300 ++++++++++++++++-- 5 files changed, 354 insertions(+), 19 deletions(-) diff --git a/customize.dist/src/less2/include/colortheme-dark.less b/customize.dist/src/less2/include/colortheme-dark.less index 68a456bcc..8dbe77271 100644 --- a/customize.dist/src/less2/include/colortheme-dark.less +++ b/customize.dist/src/less2/include/colortheme-dark.less @@ -431,3 +431,8 @@ @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; diff --git a/customize.dist/src/less2/include/colortheme.less b/customize.dist/src/less2/include/colortheme.less index c385cd83c..e68da8835 100644 --- a/customize.dist/src/less2/include/colortheme.less +++ b/customize.dist/src/less2/include/colortheme.less @@ -431,3 +431,8 @@ @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; diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 41b5dfe62..6485e9c64 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1473,11 +1473,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'); diff --git a/www/form/app-form.less b/www/form/app-form.less index f0182d9a7..8795b71aa 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -217,5 +217,66 @@ } } + .cp-form-type-poll { + display: 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-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-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/inner.js b/www/form/inner.js index 02f052888..4dbe64df8 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -53,11 +53,24 @@ define([ var APP = window.APP = { }; + var is24h = false; + var dayFormat = "Y-m-d"; + var dateFormat = "Y-m-d H:i"; + try { + is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/); + } catch (e) {} + if (!is24h) { dateFormat = "Y-m-d h:i K"; } + Messages.button_newform = "New Form"; // XXX Messages.form_invalid = "Invalid form"; Messages.form_editBlock = "Edit options"; Messages.form_editQuestion = "Edit question"; Messages.form_editMax = "Max selectable options"; + Messages.form_editType = "Options type"; + + Messages.form_poll_text = "Text"; + Messages.form_poll_day = "Day"; + Messages.form_poll_time = "Time"; Messages.form_default = "Your question here?"; Messages.form_type_input = "Text"; // XXX @@ -65,6 +78,7 @@ define([ 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_duplicates = "Duplicate entries have been removed"; Messages.form_maxOptions = "{0} answer(s) max"; @@ -135,21 +149,57 @@ define([ ]); } + 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; var getOption = function (val, isItem, uid) { var input = h('input', {value:val}); if (uid) { $(input).data('uid', uid); } - // if this element was active before the remote change, restore cursor - if (cursor && cursor.el === val) { - console.log(isItem); - console.log(cursor.item); - console.log(Boolean(isItem)); - console.log(Boolean(cursor.item)); + // 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, + }); + } else if (v.type === 'day') { Flatpickr(input); } } + // if this element was active before the remote change, restore cursor var setCursor = function () { + if (v.type !== 'text') { return; } input.selectionStart = cursor.start || 0; input.selectionEnd = cursor.end || 0; setTimeout(function () { input.focus(); }); @@ -175,6 +225,7 @@ define([ inputs.push(add); var container = h('div.cp-form-edit-block', inputs); + var $container = $(container); var containerItems; if (v.items) { @@ -185,17 +236,47 @@ define([ containerItems = h('div.cp-form-edit-block', inputsItems); } + // Doodle type change: empty current values and change input types? + if (typeSelect) { + typeSelect.onChange.reg(function (prettyVal, val) { + $container.find('input').each(function (i, input) { + if (!input._flatpickr && val !== 'text') { + input.value = ""; + } + + if (input._flatpickr) { + input._flatpickr.destroy(); + delete input._flatpickr; + } + if (val === 'time') { + Flatpickr(input, { + enableTime: true, + time_24hr: is24h, + dateFormat: dateFormat, + }); + } + if (val === 'day') { + Flatpickr(input, { + time_24hr: is24h, + }); + } + }); + }); + } + // "Add option" button handler $add = $(add).click(function () { $add.before(getOption(Messages.form_newOption, false)); - if ($(container).find('input').length >= MAX_OPTIONS) { $add.hide(); } + 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 ($container.find('input').length >= MAX_OPTIONS) { $add.hide(); } if ($(containerItems).find('input').length >= MAX_ITEMS) { $addItem.hide(); } // Cancel changes @@ -207,8 +288,8 @@ define([ var values = []; var active = document.activeElement; var cursor = {}; - $(container).find('input').each(function (i, el) { - if (el === active) { + $container.find('input').each(function (i, el) { + if (el === active && !el._flatpickr) { cursor.el= $(el).val(); cursor.start = el.selectionStart; cursor.end = el.selectionEnd; @@ -221,6 +302,10 @@ define([ _content.max = Number($(maxInput).val()) || 1; } + if (typeSelect) { + _content.type = typeSelect.getValue(); + } + if (v.items) { var items = []; $(containerItems).find('input').each(function (i, el) { @@ -255,7 +340,7 @@ define([ // Get values var values = []; var duplicates = false; - $(container).find('input').each(function (i, el) { + $container.find('input').each(function (i, el) { var val = $(el).val().trim(); if (values.indexOf(val) === -1) { values.push(val); } else { duplicates = true; } @@ -291,16 +376,63 @@ define([ res.max = maxVal; } + if (typeSelect) { + res.type = typeSelect.getValue(); + } + cb(res); }); return [ + type, maxOptions, h('div.cp-form-edit-options-block', [containerItems, container]), h('div', [cancelBlock, saveBlock]) ]; }; + var makePollTable = function (answers, opts) { + // Create first line with options + var els = opts.values.map(function (data, i) { + if (opts.type === "day") { + var _date = new Date(data); + data = _date.toLocaleDateString(); + } + return h('div.cp-poll-cell.cp-form-poll-option', data); + }); + // Insert axis switch button + var switchAxis = h('button.btn', [ + h('i.fa.fa-exchange'), + ]); + els.unshift(h('div.cp-poll-cell.cp-poll-switch', switchAxis)); + var lines = [h('div', els)]; + + // 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])); @@ -739,6 +871,99 @@ define([ }, icon: h('i.fa.fa-list-ul') }, + 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) { + if (!opts) { opts = TYPES.poll.defaultOpts; } + if (!Array.isArray(opts.values)) { return; } + var name = Util.uid(); + + var lines = makePollTable(answers, opts); + + // Add form + // XXX only if not already answered! + var addLine = opts.values.map(function (data, i) { + 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); + }); + cell._setValue = function (v) { + val = v; + $c.attr('data-value', val); + }; + return cell; + }); + // Name input + var nameInput = h('input', { value: username }); + addLine.unshift(h('div.cp-poll-cell', nameInput)); + // XXX Submit button here? + 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); + console.log(el, $el.data('option'), val); + 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.fa.fa-check-square-o') + }, }; var renderResults = function (content, answers) { @@ -764,6 +989,14 @@ define([ $container.append(elements); }; + 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 getFormResults = function () { if (!Array.isArray(APP.formBlocks)) { return; } var results = {}; @@ -809,6 +1042,7 @@ define([ var $v = $(viewResults).click(function () { $v.attr('disabled', 'disabled'); sframeChan.query("Q_FORM_FETCH_ANSWERS", content.answers, function (err, answers) { + if (answers) { APP.answers = answers; } $v.removeAttr('disabled'); $('body').addClass('cp-app-form-results'); renderResults(content, answers); @@ -838,7 +1072,17 @@ define([ var model = TYPES[type]; if (!model) { return; } - var data = model.get(block.opts); + 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); if (!data) { return; } data.uid = uid; if (answers && answers[uid]) { data.setValue(answers[uid]); } @@ -936,7 +1180,8 @@ define([ framework._.cpNfInner.chainpad.onSettle(function () { $(editButtons).show(); UI.log(Messages.saved); - data = model.get(newOpts); + var _answers = getBlockAnswers(APP.answers, uid); + data = model.get(newOpts, answers); if (!data) { data = {}; } $oldTag.before(data.tag).remove(); }); @@ -1015,6 +1260,7 @@ define([ var sframeChan = framework._.sfCommon.getSframeChannel(); var metadataMgr = framework._.cpNfInner.metadataMgr; + var user = metadataMgr.getUserData(); var priv = metadataMgr.getPrivateData(); APP.isEditor = Boolean(priv.form_public); @@ -1080,12 +1326,6 @@ define([ } // Otherwise add it var datePicker = h('input'); - var is24h = false; - var dateFormat = "Y-m-d H:i"; - try { - is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/); - } catch (e) {} - if (!is24h) { dateFormat = "Y-m-d h:i K"; } var picker = Flatpickr(datePicker, { enableTime: true, time_24hr: is24h, @@ -1129,6 +1369,7 @@ define([ validateKey: content.answers.validateKey, publicKey: content.answers.publicKey }, function (err, answers) { + if (answers) { APP.answers = answers; } $v.removeAttr('disabled'); $body.addClass('cp-app-form-results'); renderResults(content, answers); @@ -1281,6 +1522,7 @@ define([ publicKey: content.answers.publicKey, privateKey: priv.form_auditorKey }, function (err, obj) { + if (obj) { APP.answers = obj; } $body.addClass('cp-app-form-results'); renderResults(content, obj); }); @@ -1303,6 +1545,26 @@ define([ refreshEndDateBanner(); + // 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) { + if (obj) { APP.answers = obj; } + checkIntegrity(false); + var myAnswers; + if (user.curvePublic && obj && obj[user.curvePublic]) { // XXX ANONYMOUS + myAnswers = obj[user.curvePublic].msg; + } + updateForm(framework, content, false, myAnswers); + }); + return; + } sframeChan.query("Q_FETCH_MY_ANSWERS", { channel: content.answers.channel, From 6694d9df03d49c23b7e41442b8e6e0bdf1c1bcdb Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 28 May 2021 14:13:16 +0200 Subject: [PATCH 16/44] lint compliance --- www/common/common-interface.js | 9 +++--- www/common/cryptpad-common.js | 2 +- www/form/inner.js | 50 ++++++++++++++++------------------ www/form/main.js | 22 +++++---------- 4 files changed, 36 insertions(+), 47 deletions(-) diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 4166c0902..9bdf9ec7a 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -1223,6 +1223,7 @@ 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); @@ -1231,13 +1232,13 @@ define([ 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/cryptpad-common.js b/www/common/cryptpad-common.js index 973a27d6b..69ed47587 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -69,7 +69,7 @@ define([ }, cb); }; - common.getAccessKeys = function (cb, opts) { + common.getAccessKeys = function (cb) { var keys = []; Nthen(function (waitFor) { // Push account keys diff --git a/www/form/inner.js b/www/form/inner.js index 4dbe64df8..48a4bae93 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -23,7 +23,6 @@ define([ '/lib/datepicker/flatpickr.js', '/bower_components/sortablejs/Sortable.min.js', - '/bower_components/file-saver/FileSaver.min.js', 'css!/lib/datepicker/flatpickr.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'less!/form/app-form.less', @@ -49,12 +48,10 @@ define([ Sortable ) { - var SaveAs = window.saveAs; var APP = window.APP = { }; var is24h = false; - var dayFormat = "Y-m-d"; var dateFormat = "Y-m-d H:i"; try { is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/); @@ -142,7 +139,7 @@ define([ value: v.max, min: 1, max: v.values.length - }) + }); maxOptions = h('div.cp-form-edit-max-options', [ h('span', Messages.form_editMax), maxInput @@ -181,7 +178,7 @@ define([ } // Show existing options - var $add; + var $add, $addItem; var getOption = function (val, isItem, uid) { var input = h('input', {value:val}); if (uid) { $(input).data('uid', uid); } @@ -393,7 +390,7 @@ define([ var makePollTable = function (answers, opts) { // Create first line with options - var els = opts.values.map(function (data, i) { + var els = opts.values.map(function (data) { if (opts.type === "day") { var _date = new Date(data); data = _date.toLocaleDateString(); @@ -451,6 +448,15 @@ define([ 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 TYPES = { input: { get: function () { @@ -501,7 +507,7 @@ define([ tag: tag, getValue: function () { var res; - els.some(function (el, i) { + els.some(function (el) { var $i = $(el).find('input'); if (Util.isChecked($i)) { res = $i.data('val'); @@ -592,10 +598,10 @@ define([ getValue: function () { var res = {}; var l = lines.slice(1); - l.forEach(function (el, i) { + l.forEach(function (el) { var $el = $(el); var uid = $el.attr('data-uid'); - var $l = $el.find('input').each(function (i, input) { + $el.find('input').each(function (i, input) { var $i = $(input); if (res[uid]) { return; } if (Util.isChecked($i)) { res[uid] = $i.data('val'); } @@ -696,7 +702,7 @@ define([ tag: tag, getValue: function () { var res = []; - els.forEach(function (el, i) { + els.forEach(function (el) { var $i = $(el).find('input'); if (Util.isChecked($i)) { res.push($i.data('val')); @@ -801,11 +807,11 @@ define([ getValue: function () { var res = {}; var l = lines.slice(1); - l.forEach(function (el, i) { + l.forEach(function (el) { var $el = $(el); var uid = $el.attr('data-uid'); res[uid] = []; - var $l = $el.find('input').each(function (i, input) { + $el.find('input').each(function (i, input) { var $i = $(input); if (Util.isChecked($i)) { res[uid].push($i.data('val')); } }); @@ -881,13 +887,12 @@ define([ get: function (opts, answers, username) { if (!opts) { opts = TYPES.poll.defaultOpts; } if (!Array.isArray(opts.values)) { return; } - var name = Util.uid(); var lines = makePollTable(answers, opts); // Add form // XXX only if not already answered! - var addLine = opts.values.map(function (data, i) { + 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'), @@ -989,14 +994,6 @@ define([ $container.append(elements); }; - 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 getFormResults = function () { if (!Array.isArray(APP.formBlocks)) { return; } var results = {}; @@ -1034,8 +1031,9 @@ define([ }); }); + var viewResults; if (content.answers.privateKey) { - var viewResults = h('button.btn.btn-primary', [ + viewResults = h('button.btn.btn-primary', [ h('span.cp-app-form-button-results', Messages.form_viewResults), ]); var sframeChan = framework._.sfCommon.getSframeChannel(); @@ -1181,7 +1179,7 @@ define([ $(editButtons).show(); UI.log(Messages.saved); var _answers = getBlockAnswers(APP.answers, uid); - data = model.get(newOpts, answers); + data = model.get(newOpts, _answers); if (!data) { data = {}; } $oldTag.before(data.tag).remove(); }); @@ -1306,8 +1304,6 @@ define([ if (endDate <= now) { text = Messages._getKey('form_isClosed', [date]); buttonTxt = Messages.form_open; - action = function () { - }; } else if (endDate > now) { text = Messages._getKey('form_willClose', [date]); buttonTxt = Messages.form_removeEnd; @@ -1481,7 +1477,7 @@ define([ } }; - framework.onReady(function (isNew) { + framework.onReady(function () { var priv = metadataMgr.getPrivateData(); if (APP.isEditor) { diff --git a/www/form/main.js b/www/form/main.js index c02921887..27e6552de 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -8,6 +8,7 @@ define([ ], function (nThen, ApiConfig, DomReady, SFCommonO) { var Nacl = window.nacl; + var href, hash; // Loaded in load #2 nThen(function (waitFor) { DomReady.onReady(waitFor()); @@ -44,8 +45,8 @@ define([ 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 _parsed = Utils.Hash.parseTypeHash('pad', auditorHash); + meta.form_auditorHash = _parsed.getHash({auditorKey: privateKey}); }; var addRpc = function (sframeChan, Cryptpad, Utils) { @@ -67,7 +68,7 @@ define([ nThen(function (w) { require([ '/bower_components/chainpad-netflux/chainpad-netflux.js', - ], w(function (_CPNetflux, _Crypto) { + ], w(function (_CPNetflux) { CPNetflux = _CPNetflux; })); Cryptpad.getAccessKeys(w(function (_keys) { @@ -83,7 +84,7 @@ define([ Cryptpad.makeNetwork(w(function (err, nw) { network = nw; })); - }).nThen(function (w) { + }).nThen(function () { if (!network) { return void cb({error: "E_CONNECT"}); } var keys = Utils.secret && Utils.secret.keys; @@ -120,9 +121,6 @@ define([ }); }); sframeChan.on("Q_FETCH_MY_ANSWERS", function (data, cb) { - var keys; - var CPNetflux; - var network; var answer; var myKeys; nThen(function (w) { @@ -136,7 +134,7 @@ define([ } answer = obj; })); - }).nThen(function (w) { + }).nThen(function () { Cryptpad.getHistoryRange({ channel: data.channel, lastKnownHash: answer.hash, @@ -144,7 +142,6 @@ define([ }, function (obj) { if (obj && obj.error) { return void cb(obj); } var messages = obj.messages; - var ephemeral_priv = answer.curvePrivate; var res = Utils.Crypto.Mailbox.openOwnSecretLetter(messages[0].msg, { validateKey: data.validateKey, ephemeral_private: Nacl.util.decodeBase64(answer.curvePrivate), @@ -164,7 +161,7 @@ define([ Cryptpad.getFormKeys(w(function (keys) { myKeys = keys; })); - }).nThen(function (w) { + }).nThen(function () { var keys = Utils.secret && Utils.secret.keys; myKeys.signingKey = keys.secondarySignKey; @@ -191,11 +188,6 @@ define([ }); }); }); - 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, From ee670350150143b7907a4f092208965949928022 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 28 May 2021 14:22:06 +0200 Subject: [PATCH 17/44] Fix poll forms issues --- www/form/inner.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 48a4bae93..844e27dc0 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -241,6 +241,8 @@ define([ input.value = ""; } + v.type = val; + if (input._flatpickr) { input._flatpickr.destroy(); delete input._flatpickr; @@ -263,6 +265,7 @@ define([ // "Add option" button handler $add = $(add).click(function () { + var txt = v.type ? '' : Messages.form_newOption; $add.before(getOption(Messages.form_newOption, false)); var l = $container.find('input').length; $(maxInput).attr('max', l); @@ -891,7 +894,6 @@ define([ var lines = makePollTable(answers, opts); // Add form - // XXX only if not already answered! 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'), @@ -913,7 +915,7 @@ define([ return cell; }); // Name input - var nameInput = h('input', { value: username }); + var nameInput = h('input', { value: username || '' }); addLine.unshift(h('div.cp-poll-cell', nameInput)); // XXX Submit button here? lines.push(h('div', addLine)); From ed5cc5158bd8b5743fb1ae0c23a679f262f4d421 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 28 May 2021 14:23:02 +0200 Subject: [PATCH 18/44] Fix poll forms issues... --- www/form/inner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/form/inner.js b/www/form/inner.js index 844e27dc0..4e9eee66c 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -266,7 +266,7 @@ define([ // "Add option" button handler $add = $(add).click(function () { var txt = v.type ? '' : Messages.form_newOption; - $add.before(getOption(Messages.form_newOption, false)); + $add.before(getOption(txt, false)); var l = $container.find('input').length; $(maxInput).attr('max', l); if (l >= MAX_OPTIONS) { $add.hide(); } From e68fccc863fe59e3cd26fd8529505f2d932d43b9 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 28 May 2021 15:35:53 +0200 Subject: [PATCH 19/44] Fix cursor in forms --- www/form/inner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/form/inner.js b/www/form/inner.js index 4e9eee66c..71fe9de9b 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -196,7 +196,7 @@ define([ // if this element was active before the remote change, restore cursor var setCursor = function () { - if (v.type !== 'text') { return; } + if (v.type && v.type !== 'text') { return; } input.selectionStart = cursor.start || 0; input.selectionEnd = cursor.end || 0; setTimeout(function () { input.focus(); }); From 8871f41bfef2cf22de8663f7589ae3c45b838d0d Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 28 May 2021 16:58:22 +0200 Subject: [PATCH 20/44] Improve rendering of form polls with time values --- www/form/app-form.less | 18 +++++++++++++++- www/form/inner.js | 48 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index 8795b71aa..efe97ed86 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -74,6 +74,9 @@ .cp-form-block-question { margin-bottom: 5px; } + .cp-form-block-content { + overflow-x: auto; + } .cp-form-input-block { //width: @form_input-width; &:not(.editing) { @@ -218,7 +221,7 @@ } .cp-form-type-poll { - display: flex; + display: inline-flex; flex-flow: column; & > div { display: flex; @@ -236,6 +239,12 @@ 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 { @@ -249,6 +258,13 @@ .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 { diff --git a/www/form/inner.js b/www/form/inner.js index 71fe9de9b..a0a52e673 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -53,10 +53,15 @@ define([ 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) {} - if (!is24h) { dateFormat = "Y-m-d h:i K"; } + 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"; @@ -190,8 +195,13 @@ define([ enableTime: true, time_24hr: is24h, dateFormat: dateFormat, + defaultDate: val ? new Date(val) : undefined }); - } else if (v.type === 'day') { Flatpickr(input); } + } else if (v.type === 'day') { + Flatpickr(input, { + defaultDate: val ? new Date(val) : undefined + }); + } } // if this element was active before the remote change, restore cursor @@ -342,6 +352,7 @@ define([ var duplicates = false; $container.find('input').each(function (i, el) { var val = $(el).val().trim(); + if (v.type === "day" || v.type === "time") { val = +new Date(val); } if (values.indexOf(val) === -1) { values.push(val); } else { duplicates = true; } }); @@ -392,12 +403,22 @@ define([ }; 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 @@ -407,6 +428,28 @@ define([ 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]++; + }); + var dayValues = Object.keys(_days).map(function (d) { return _days[d]; }); + var minDay = Math.min.apply(null, dayValues); + console.log(_days, minDay); + Object.keys(_days).forEach(function (day) { + days.push(h('div.cp-poll-cell.cp-poll-time-day', { + style: 'flex-grow:'+(_days[day]-1)+';' + }, day)); + }); + var width = (opts.values.length + 2)*100; + lines.unshift(h('div', days)); + } + // Add answers if (Array.isArray(answers)) { answers.forEach(function (answer) { @@ -957,7 +1000,6 @@ define([ $tag.find('.cp-form-poll-choice').each(function (i, el) { if (!el._setValue) { return; } var $el = $(el); - console.log(el, $el.data('option'), val); el._setValue(val[$el.data('option')] || 0); }); } From 5d7ab79935142401c4c923e2f9997d7828e121d5 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 28 May 2021 18:23:19 +0200 Subject: [PATCH 21/44] Improve form polls creation (time and day types) --- www/form/inner.js | 143 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 114 insertions(+), 29 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index a0a52e673..37d3d50af 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -116,9 +116,11 @@ define([ 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"; - var MAX_OPTIONS = 10; // XXX + var MAX_OPTIONS = 15; // XXX var MAX_ITEMS = 10; // XXX var editOptions = function (v, setCursorGetter, cb, tmp) { @@ -184,6 +186,7 @@ define([ // 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); } @@ -198,9 +201,9 @@ define([ defaultDate: val ? new Date(val) : undefined }); } else if (v.type === 'day') { - Flatpickr(input, { + /*Flatpickr(input, { defaultDate: val ? new Date(val) : undefined - }); + });*/ } } @@ -224,7 +227,10 @@ define([ // 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 (!isItem && $add) { + $add.show(); + if (v.type === "time") { $(addMultiple).show(); } + } }); return el; }; @@ -243,32 +249,93 @@ define([ containerItems = h('div.cp-form-edit-block', inputsItems); } + // 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 // XXX + 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 && val !== 'text') { - input.value = ""; - } - - v.type = val; - if (input._flatpickr) { input._flatpickr.destroy(); delete input._flatpickr; } - if (val === 'time') { - Flatpickr(input, { - enableTime: true, - time_24hr: is24h, - dateFormat: dateFormat, - }); - } - if (val === 'day') { - Flatpickr(input, { - time_24hr: is24h, - }); - } }); }); } @@ -281,6 +348,7 @@ define([ $(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())); @@ -306,6 +374,12 @@ define([ } 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) { @@ -350,12 +424,22 @@ define([ // Get values var values = []; var duplicates = false; - $container.find('input').each(function (i, el) { - var val = $(el).val().trim(); - if (v.type === "day" || v.type === "time") { val = +new Date(val); } - if (values.indexOf(val) === -1) { values.push(val); } - else { duplicates = true; } - }); + 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") { val = +new Date(val); } + 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 @@ -397,7 +481,9 @@ define([ return [ type, maxOptions, + calendarView, h('div.cp-form-edit-options-block', [containerItems, container]), + addMultiple, h('div', [cancelBlock, saveBlock]) ]; }; @@ -446,7 +532,6 @@ define([ style: 'flex-grow:'+(_days[day]-1)+';' }, day)); }); - var width = (opts.values.length + 2)*100; lines.unshift(h('div', days)); } From 833bcc93cc15939f44468deb7eb48ef575dc441a Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 31 May 2021 15:32:04 +0200 Subject: [PATCH 22/44] Sort options and items in form editor --- www/form/app-form.less | 6 ++++++ www/form/inner.js | 20 +++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index efe97ed86..3636144ee 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -125,6 +125,12 @@ flex-wrap: wrap; } .cp-form-edit-block { + .cp-form-handle { + display: flex; + align-items: center; + justify-content: center; + width: 30px; + } .cp-form-edit-block-input { display: flex; width: 400px; diff --git a/www/form/inner.js b/www/form/inner.js index 37d3d50af..b2406fc93 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -221,7 +221,14 @@ define([ } var del = h('button.btn.btn-danger', h('i.fa.fa-times')); - var el = h('div.cp-form-edit-block-input', [ input, del ]); + 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 @@ -240,6 +247,12 @@ define([ 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", + }); + var containerItems; if (v.items) { var inputsItems = v.items.map(function (itemData) { @@ -247,6 +260,11 @@ define([ }); 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", + }); } // Calendar... From fc8ce9cb0e0e668a349afa2aa635421e735929bc Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 31 May 2021 17:19:14 +0200 Subject: [PATCH 23/44] Allow anonymous answers --- www/common/cryptpad-common.js | 32 +++++--- www/common/outer/async-store.js | 7 +- www/form/inner.js | 134 ++++++++++++++++++++++++-------- www/form/main.js | 45 ++++++++++- 4 files changed, 174 insertions(+), 44 deletions(-) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 69ed47587..dbcbcf8eb 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, @@ -89,7 +89,7 @@ define([ 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 || {}; @@ -104,13 +104,26 @@ define([ }; common.getFormKeys = function (cb) { - postMessage("GET", { - key: ['curvePrivate'], - }, function (obj) { - if (obj.error) { return void 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: obj, - curvePublic: Hash.getCurvePublicFromPrivate(obj) + curvePrivate: curvePrivate, + curvePublic: curvePrivate && Hash.getCurvePublicFromPrivate(curvePrivate), + formSeed: formSeed }); }); }; @@ -124,7 +137,8 @@ define([ key: ['forms', data.channel], value: { hash: data.hash, - curvePrivate: data.curvePrivate + curvePrivate: data.curvePrivate, + anonymous: data.anonymous } }, function (obj) { if (obj && obj.error) { console.error(obj.error); } diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 2d08b52d4..1af15a9ed 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -629,6 +629,7 @@ define([ if (!proxy.uid) { store.noDriveUid = store.noDriveUid || Hash.createChannelId(); } + var metadata = { // "user" is shared with everybody via the userlist user: { @@ -655,7 +656,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))); @@ -2710,6 +2711,10 @@ define([ 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/form/inner.js b/www/form/inner.js index b2406fc93..33ddff44e 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -110,6 +110,12 @@ define([ Messages.form_isClosed = "This form was closed on {0}"; Messages.form_willClose = "This form will close on {0}"; + Messages.form_anonymous_on = "Anonymous answers are allowed"; + Messages.form_anonymous_off = "Anonymous answers are blocked"; + Messages.form_anonymous_button_on = "Block anonymous answers"; + Messages.form_anonymous_button_off = "Allow anonymous answers"; + 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"; @@ -119,6 +125,7 @@ define([ Messages.form_addMultiple = "Add all"; Messages.form_clear = "Clear"; + Messages.form_anonymousBox = "Answer anonymously"; var MAX_OPTIONS = 15; // XXX var MAX_ITEMS = 10; // XXX @@ -544,7 +551,6 @@ define([ }); var dayValues = Object.keys(_days).map(function (d) { return _days[d]; }); var minDay = Math.min.apply(null, dayValues); - console.log(_days, minDay); Object.keys(_days).forEach(function (day) { days.push(h('div.cp-poll-cell.cp-poll-time-day', { style: 'flex-grow:'+(_days[day]-1)+';' @@ -1061,13 +1067,10 @@ define([ return cell; }); // Name input - var nameInput = h('input', { value: username || '' }); + var nameInput = h('input', { value: username || Messages.anonymous }); addLine.unshift(h('div.cp-poll-cell', nameInput)); - // XXX Submit button here? lines.push(h('div', addLine)); - - var tag = h('div.cp-form-type-poll', lines); var $tag = $(tag); @@ -1150,6 +1153,20 @@ define([ return results; }; var makeFormControls = function (framework, content, update) { + var loggedIn = framework._.sfCommon.isLoggedIn(); + + if (!loggedIn && !content.answers.anonymous) { return; } + + var cbox; + if (loggedIn) { + cbox = UI.createCheckbox('cp-form-anonymous', + Messages.form_anonymousBox, false, { mark: { tabindex:1 } }); + if (!content.answers.anonymous) { + $(cbox).find('input').attr('disabled', 'disabled'); + } + + } + 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 () { @@ -1162,10 +1179,12 @@ define([ $send.attr('disabled', 'disabled'); var results = getFormResults(); if (!results) { return; } + var sframeChan = framework._.sfCommon.getSframeChannel(); sframeChan.query('Q_FORM_SUBMIT', { mailbox: content.answers, - results: results + results: results, + anonymous: !loggedIn || Util.isChecked($(cbox).find('input')) }, function (err, data) { $send.attr('disabled', 'disabled'); if (err || (data && data.error)) { @@ -1186,7 +1205,8 @@ define([ var sframeChan = framework._.sfCommon.getSframeChannel(); var $v = $(viewResults).click(function () { $v.attr('disabled', 'disabled'); - sframeChan.query("Q_FORM_FETCH_ANSWERS", content.answers, function (err, answers) { + sframeChan.query("Q_FORM_FETCH_ANSWERS", content.answers, function (err, obj) { + var answers = obj && obj.results; if (answers) { APP.answers = answers; } $v.removeAttr('disabled'); $('body').addClass('cp-app-form-results'); @@ -1200,7 +1220,10 @@ define([ reset = undefined; } - return h('div.cp-form-send-container', [send, reset, viewResults]); + return h('div.cp-form-send-container', [ + cbox ? h('div', cbox) : undefined, + send, reset, viewResults + ]); }; var updateForm = function (framework, content, editable, answers, temp) { var $container = $('div.cp-form-creator-content'); @@ -1401,6 +1424,7 @@ define([ var andThen = function (framework) { framework.start(); + var evOnChange = Util.mkEvent(); var content = {}; var sframeChan = framework._.sfCommon.getSframeChannel(); @@ -1416,26 +1440,54 @@ define([ $toolbarContainer.after(helpMenu.menu); + // XXX refresh form settings on remote change var makeFormSettings = function () { - var makePublic = h('button.btn.btn-primary', Messages.form_makePublic); - if (content.answers.privateKey) { makePublic = undefined; } - var publicText = content.answers.privateKey ? Messages.form_isPublic : Messages.form_isPrivate; - var resultsType = h('div.cp-form-results-type-container', [ - h('span.cp-form-results-type', publicText), - makePublic - ]); - var $makePublic = $(makePublic).click(function () { - UI.confirm(Messages.form_makePublicWarning, function (yes) { - if (!yes) { return; } - content.answers.privateKey = priv.form_private; + // 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-primary', Messages.form_makePublic); + if (content.answers.privateKey) { makePublic = undefined; } + var publicText = content.answers.privateKey ? Messages.form_isPublic : Messages.form_isPrivate; + $results.append(h('span.cp-form-results-type', publicText)); + $results.append(makePublic); + var $makePublic = $(makePublic).click(function () { + UI.confirm(Messages.form_makePublicWarning, function (yes) { + if (!yes) { return; } + $makePublic.attr('disabled', 'disabled'); + 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 key = anonymous ? 'on' : 'off'; + var button = h('button.btn.btn-secondary', Messages['form_anonymous_button_'+key]); + var $b = $(button).click(function () { + $b.attr('disabled', 'disabled'); + content.answers.anonymous = !anonymous; framework.localChange(); framework._.cpNfInner.chainpad.onSettle(function () { UI.log(Messages.saved); - $makePublic.remove(); - $(resultsType).find('.cp-form-results-type').text(Messages.form_isPublic); + refreshPrivacy(); }); }); - }); + $privacy.append(h('div.cp-form-status', Messages['form_anonymous_'+key])); + $privacy.append(h('div.cp-form-actions', button)); + }; + refreshPrivacy(); // End date / Closed state var endDateContainer = h('div.cp-form-status-container'); @@ -1511,7 +1563,8 @@ define([ channel: content.answers.channel, validateKey: content.answers.validateKey, publicKey: content.answers.publicKey - }, function (err, answers) { + }, function (err, obj) { + var answers = obj && obj.results; if (answers) { APP.answers = answers; } $v.removeAttr('disabled'); $body.addClass('cp-app-form-results'); @@ -1519,8 +1572,14 @@ define([ }); }); + + evOnChange.reg(refreshPublic); + evOnChange.reg(refreshPrivacy); + evOnChange.reg(refreshEndDate); + return [ endDateContainer, + privacyContainer, resultsType, viewResults, ]; @@ -1665,9 +1724,10 @@ define([ publicKey: content.answers.publicKey, privateKey: priv.form_auditorKey }, function (err, obj) { - if (obj) { APP.answers = obj; } + var answers = obj && obj.results; + if (answers) { APP.answers = answers; } $body.addClass('cp-app-form-results'); - renderResults(content, obj); + renderResults(content, answers); }); return; } @@ -1678,16 +1738,21 @@ define([ validateKey: content.answers.validateKey, publicKey: content.answers.publicKey }, function (err, obj) { - if (obj) { APP.answers = 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"; @@ -1698,12 +1763,19 @@ define([ publicKey: content.answers.publicKey, privateKey: content.answers.privateKey, }, function (err, obj) { - if (obj) { APP.answers = obj; } + var answers = obj && obj.results; + if (answers) { APP.answers = answers; } checkIntegrity(false); var myAnswers; - if (user.curvePublic && obj && obj[user.curvePublic]) { // XXX ANONYMOUS - myAnswers = obj[user.curvePublic].msg; + 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; + } } + console.warn(obj); updateForm(framework, content, false, myAnswers); }); return; @@ -1726,8 +1798,8 @@ define([ }); framework.onContentUpdate(function (newContent) { - console.log(newContent); content = newContent; + evOnChange.fire(); refreshEndDateBanner(); var answers, temp; if (!APP.isEditor) { answers = getFormResults(); } diff --git a/www/form/main.js b/www/form/main.js index 27e6552de..f51aae4cf 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -63,6 +63,7 @@ define([ }); sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, cb) { var myKeys = {}; + var myFormKeys; var CPNetflux; var network; nThen(function (w) { @@ -81,12 +82,19 @@ define([ } }); })); + Cryptpad.getFormKeys(w(function (keys) { + 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 crypto = Utils.Crypto.Mailbox.createEncryptor({ @@ -105,7 +113,15 @@ define([ }; var results = {}; config.onReady = function () { - cb(results); + var myKey; + // If we have submitted an anonymous answer, retrieve it + if (myFormKeys.curvePublic && results[myFormKeys.curvePublic]) { + myKey = myFormKeys.curvePublic; + } + cb({ + myKey: myKey, + results: results + }); network.disconnect(); }; config.onMessage = function (msg, peer, vKey, isCp, hash, senderCurve, cfg) { @@ -135,6 +151,10 @@ define([ 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, @@ -154,6 +174,16 @@ define([ }); }); + 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, + }; + }; sframeChan.on("Q_FORM_SUBMIT", function (data, cb) { var box = data.mailbox; var myKeys; @@ -162,7 +192,15 @@ define([ myKeys = keys; })); }).nThen(function () { - + // XXX if we are a registered user (myKeys.curvePrivate exists), we may + // have already answered anonymously. We should send a "proof" to show + // that the existing anonymous answer are ours (using myKeys.formSeed). + // Even if we never answered anonymously, the keyPair would be unique to + // the current channel so it wouldn't leak anything. + if (data.anonymous) { + if (!myKeys.formSeed) { return void cb({ error: "ANONYMOUS_ERROR" }); } + myKeys = getAnonymousKeys(myKeys.formSeed, box.channel); + } var keys = Utils.secret && Utils.secret.keys; myKeys.signingKey = keys.secondarySignKey; @@ -182,7 +220,8 @@ define([ Cryptpad.storeFormAnswer({ channel: box.channel, hash: hash, - curvePrivate: ephemeral_private + curvePrivate: ephemeral_private, + anonymous: Boolean(data.anonymous) }); cb({error: err, response: response, hash: hash}); }); From 6021b152136c514308e0f31009ce5b22dae27f6a Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 31 May 2021 18:23:25 +0200 Subject: [PATCH 24/44] Add description block to forms --- www/form/app-form.less | 13 +++++ www/form/inner.js | 112 ++++++++++++++++++++++++++++++++++++----- www/form/main.js | 20 ++++---- 3 files changed, 123 insertions(+), 22 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index 3636144ee..9edc90522 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -123,6 +123,19 @@ .cp-form-edit-options-block { display: flex; flex-wrap: wrap; + .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 { .cp-form-handle { diff --git a/www/form/inner.js b/www/form/inner.js index 33ddff44e..80bd3d2d3 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -15,6 +15,9 @@ define([ '/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', @@ -23,6 +26,13 @@ define([ '/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', @@ -43,6 +53,9 @@ define([ h, Messages, AppConfig, + DiffMd, + SFCodeMirror, + CMeditor, Share, Access, Properties, Flatpickr, Sortable @@ -82,6 +95,8 @@ define([ Messages.form_type_multicheck = "Multiline Checkbox"; // XXX Messages.form_type_poll = "Poll"; // XXX + Messages.form_type_md = "Description"; // XXX + Messages.form_duplicates = "Duplicate entries have been removed"; Messages.form_maxOptions = "{0} answer(s) max"; @@ -550,7 +565,6 @@ define([ _days[day]++; }); var dayValues = Object.keys(_days).map(function (d) { return _days[d]; }); - var minDay = Math.min.apply(null, dayValues); Object.keys(_days).forEach(function (day) { days.push(h('div.cp-poll-cell.cp-poll-time-day', { style: 'flex-grow:'+(_days[day]-1)+';' @@ -612,6 +626,75 @@ define([ }).filter(Boolean); }; + var STATIC_TYPES = { + md: { + defaultOpts: { + text: "Your text here" // XXX + }, + 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); + return { + tag: tag, + edit: function (cb, tmp) { + // XXX use tmp and cursor getter + 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); + console.warn(APP.common); + setTimeout(function () { + editor.setValue(opts.text); + editor.refresh(); + editor.save(); + editor.focus(); + }); + if (APP.common) { + var markdownTb = APP.common.createMarkdownToolbar(editor); + $(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) + ]); + $(saveBlock).click(function () { + $(saveBlock).attr('disabled', 'disabled'); + cb({ + text: editor.getValue() + }); + }); + return [ + block, + h('div', [cancelBlock, saveBlock]) + ]; + }, + getCursor: function () { return cursorGetter(); }, + //getValue: function () { return $tag.val(); }, + //setValue: function (val) { $tag.val(val); }, + //reset: function () { $tag.val(''); } + }; + }, + printResults: function () { + var results = []; + return h('div.cp-form-results-type-text', results); + }, + icon: h('i.fa.fa-info') + }, + }; var TYPES = { input: { get: function () { @@ -1117,7 +1200,7 @@ define([ var lines = makePollTable(_answers, form[uid].opts); return h('div.cp-form-type-poll', lines); }, - icon: h('i.fa.fa-check-square-o') + icon: h('i.cptools.cptools-poll') }, }; @@ -1148,6 +1231,7 @@ define([ if (!Array.isArray(APP.formBlocks)) { return; } var results = {}; APP.formBlocks.forEach(function (data) { + if (!data.getValue) { return; } results[data.uid] = data.getValue(); }); return results; @@ -1237,7 +1321,8 @@ define([ var elements = content.order.map(function (uid) { var block = form[uid]; var type = block.type; - var model = TYPES[type]; + var model = TYPES[type] || STATIC_TYPES[type]; + var isStatic = Boolean(STATIC_TYPES[type]); if (!model) { return; } var _answers, name; @@ -1253,7 +1338,7 @@ define([ var data = model.get(block.opts, _answers, name); if (!data) { return; } data.uid = uid; - if (answers && answers[uid]) { data.setValue(answers[uid]); } + if (answers && answers[uid] && data.setValue) { data.setValue(answers[uid]); } var q = h('div.cp-form-block-question', block.q || Messages.form_default); var editButtons, editContainer; @@ -1380,7 +1465,7 @@ define([ return h('div.cp-form-block'+editableCls, { 'data-id':uid }, [ - q, + isStatic ? undefined : q, h('div.cp-form-block-content', [ data.tag, editButtons @@ -1394,7 +1479,7 @@ define([ if (editable) { Sortable.create($container[0], { direction: "vertical", - filter: "input, button", + filter: "input, button, .CodeMirror", preventOnFilter: false, store: { set: function (s) { @@ -1427,6 +1512,7 @@ define([ var evOnChange = Util.mkEvent(); var content = {}; + APP.common = framework._.sfCommon; var sframeChan = framework._.sfCommon.getSframeChannel(); var metadataMgr = framework._.cpNfInner.metadataMgr; var user = metadataMgr.getUserData(); @@ -1615,10 +1701,9 @@ define([ var controlContainer; if (APP.isEditor) { - var controls = Object.keys(TYPES).map(function (type) { - + var addControl = function (type) { var btn = h('button.btn', [ - TYPES[type].icon.cloneNode(), + (TYPES[type] || STATIC_TYPES[type]).icon.cloneNode(), h('span', Messages['form_type_'+type]) ]); $(btn).click(function () { @@ -1633,13 +1718,16 @@ define([ updateForm(framework, content, true); }); return btn; - }); + }; + var controls = Object.keys(TYPES).map(addControl); + var staticControls = Object.keys(STATIC_TYPES).map(addControl); var settings = makeFormSettings(); controlContainer = h('div.cp-form-creator-control', [ h('div.cp-form-creator-settings', settings), - h('div.cp-form-creator-types', controls) + h('div.cp-form-creator-types', controls), + h('div.cp-form-creator-types', staticControls) ]); } @@ -1767,7 +1855,7 @@ define([ if (answers) { APP.answers = answers; } checkIntegrity(false); var myAnswers; - var curve1 = user.curvePublic + var curve1 = user.curvePublic; var curve2 = obj && obj.myKey; // Anonymous answer key if (answers) { var myAnswersObj = answers[curve1] || answers[curve2] || undefined; diff --git a/www/form/main.js b/www/form/main.js index f51aae4cf..cd96a3367 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -61,6 +61,16 @@ define([ }); }); + 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, + }; + }; sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, cb) { var myKeys = {}; var myFormKeys; @@ -174,16 +184,6 @@ define([ }); }); - 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, - }; - }; sframeChan.on("Q_FORM_SUBMIT", function (data, cb) { var box = data.mailbox; var myKeys; From ce6879fd68727b878d6cfd88a2d453cafb17f93b Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 1 Jun 2021 13:54:04 +0200 Subject: [PATCH 25/44] Fix date parsing in firefox --- www/form/inner.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/www/form/inner.js b/www/form/inner.js index 80bd3d2d3..893e5eddb 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -472,7 +472,12 @@ define([ } else { $container.find('input').each(function (i, el) { var val = $(el).val().trim(); - if (v.type === "day" || v.type === "time") { val = +new Date(val); } + 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; } }); From 21c47f5e57fe38ca44420f00f88fa1dc176e1a26 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 1 Jun 2021 14:20:20 +0200 Subject: [PATCH 26/44] Realtime part of the description block in forms --- www/form/inner.js | 44 ++++++++++++++++++++++++++++++++++++++------ www/form/main.js | 31 ++++++++++++++++++++++++++++--- 2 files changed, 66 insertions(+), 9 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 893e5eddb..941cdfd4c 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -646,7 +646,6 @@ define([ return { tag: tag, edit: function (cb, tmp) { - // XXX use tmp and cursor getter var t = h('textarea'); var block = h('div.cp-form-edit-options-block', [t]); var cm = SFCodeMirror.create("gfm", CMeditor, t); @@ -655,9 +654,23 @@ define([ editor.setOption('lineWrapping', true); editor.setOption('styleActiveLine', true); editor.setOption('readOnly', false); - console.warn(APP.common); + + 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(opts.text); + 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(); @@ -676,12 +689,31 @@ define([ 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({ - text: editor.getValue() - }); + 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', [cancelBlock, saveBlock]) diff --git a/www/form/main.js b/www/form/main.js index cd96a3367..dd6bc0009 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -71,19 +71,24 @@ define([ curvePublic: publicKey, }; }; - sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, cb) { + sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, _cb) { + var cb = Utils.Util.once(_cb); var myKeys = {}; var myFormKeys; - var CPNetflux; + var accessKeys; + var CPNetflux, Pinpad; var network; nThen(function (w) { require([ '/bower_components/chainpad-netflux/chainpad-netflux.js', - ], w(function (_CPNetflux) { + '/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) { @@ -122,6 +127,26 @@ define([ // 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 From 3b85d16cd83eb9fff0b8e7772856291da8881932 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 1 Jun 2021 17:45:04 +0200 Subject: [PATCH 27/44] Add page break --- www/form/app-form.less | 27 +++++++++++++ www/form/inner.js | 86 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 104 insertions(+), 9 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index 9edc90522..7229e70e8 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -64,6 +64,26 @@ flex: 1; overflow: auto; + .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; @@ -76,6 +96,13 @@ } .cp-form-block-content { overflow-x: auto; + .cp-form-page-break-edit { + text-align: center; + padding: 10px; + i { + margin-right: 5px; + } + } } .cp-form-input-block { //width: @form_input-width; diff --git a/www/form/inner.js b/www/form/inner.js index 941cdfd4c..1929038e4 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -96,6 +96,7 @@ define([ Messages.form_type_poll = "Poll"; // XXX Messages.form_type_md = "Description"; // XXX + Messages.form_type_page = "Page break"; // XXX Messages.form_duplicates = "Duplicate entries have been removed"; Messages.form_maxOptions = "{0} answer(s) max"; @@ -140,6 +141,10 @@ define([ 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 @@ -720,17 +725,26 @@ define([ ]; }, getCursor: function () { return cursorGetter(); }, - //getValue: function () { return $tag.val(); }, - //setValue: function (val) { $tag.val(val); }, - //reset: function () { $tag.val(''); } }; }, - printResults: function () { - var results = []; - return h('div.cp-form-results-type-text', results); - }, + printResults: function () { return; }, icon: h('i.fa.fa-info') }, + page: { + get: function () { + var tag = h('div.cp-form-page-break-edit', [ + h('i.fa.fa-hand-o-right'), + h('span', Messages.form_type_page) + ]); + var $tag = $(tag); + return { + tag: tag, + pageBreak: true + }; + }, + printResults: function () { return; }, + icon: h('i.fa.fa-hand-o-right') + }, }; var TYPES = { input: { @@ -1377,6 +1391,11 @@ define([ data.uid = uid; if (answers && answers[uid] && data.setValue) { data.setValue(answers[uid]); } + if (data.pageBreak && !editable) { + return data; + } + + var q = h('div.cp-form-block-question', block.q || Messages.form_default); var editButtons, editContainer; @@ -1511,7 +1530,57 @@ define([ ]); }); - $container.empty().append(elements); + var _content = elements; + if (!editable) { + _content = []; + var div = h('div.cp-form-page'); + var pages = 1; + var wasPage = false; + elements.forEach(function (obj) { + if (obj && obj.pageBreak) { + 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); + + 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); if (editable) { Sortable.create($container[0], { @@ -1900,7 +1969,6 @@ define([ myAnswers = myAnswersObj.msg; } } - console.warn(obj); updateForm(framework, content, false, myAnswers); }); return; From cb6efc042571ede53c90436667518e67a5964888 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 1 Jun 2021 18:37:29 +0200 Subject: [PATCH 28/44] Claim previous anonymous answer --- www/form/app-form.less | 2 +- www/form/inner.js | 16 +++++++--- www/form/main.js | 67 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 79 insertions(+), 6 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index 7229e70e8..5a85fff24 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -76,7 +76,7 @@ justify-content: center; } button { - .cp-next { + &.cp-next { .fa { margin-right: 0; margin-left: 5px; diff --git a/www/form/inner.js b/www/form/inner.js index 1929038e4..9cf85bc64 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -936,7 +936,8 @@ define([ }); }); Object.keys(count).forEach(function (q_uid) { - var q = findItem(structure.opts.items, 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', [ @@ -1296,8 +1297,8 @@ define([ if (loggedIn) { cbox = UI.createCheckbox('cp-form-anonymous', Messages.form_anonymousBox, false, { mark: { tabindex:1 } }); - if (!content.answers.anonymous) { - $(cbox).find('input').attr('disabled', 'disabled'); + if (!content.answers.anonymous || APP.cantAnon) { + $(cbox).hide().find('input').attr('disabled', 'disabled'); } } @@ -1732,6 +1733,7 @@ define([ ]); $button.after(confirmContent); $button.remove(); + picker.open(); }); $endDate.append(h('div.cp-form-status', text)); @@ -1969,6 +1971,8 @@ define([ myAnswers = myAnswersObj.msg; } } + // If we have a non-anon answer, we can't answer anonymously later + if (answers[curve1]) { APP.cantAnon = true; } updateForm(framework, content, false, myAnswers); }); return; @@ -1983,7 +1987,11 @@ define([ UI.warn(Messages.form_cantFindAnswers); } var answers; - if (obj && !obj.error) { answers = obj; } + 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); }); diff --git a/www/form/main.js b/www/form/main.js index dd6bc0009..94b79165e 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -71,6 +71,55 @@ define([ 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) + ); + } catch (e) { console.error(e); } + return channel === Nacl.util.encodeUTF8(u8_plain); + }; sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, _cb) { var cb = Utils.Util.once(_cb); var myKeys = {}; @@ -112,8 +161,9 @@ define([ var keys = Utils.secret && Utils.secret.keys; + var curvePrivate = privateKey || data.privateKey; var crypto = Utils.Crypto.Mailbox.createEncryptor({ - curvePrivate: privateKey || data.privateKey, + curvePrivate: curvePrivate, curvePublic: publicKey || data.publicKey, validateKey: data.validateKey }); @@ -162,6 +212,12 @@ define([ 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, @@ -203,6 +259,7 @@ define([ my_private: Nacl.util.decodeBase64(myKeys.curvePrivate), their_public: Nacl.util.decodeBase64(data.publicKey) }); + res.content._isAnon = answer.anonymous; cb(JSON.parse(res.content)); }); @@ -222,9 +279,12 @@ define([ // that the existing anonymous answer are ours (using myKeys.formSeed). // Even if we never answered anonymously, the keyPair would be unique to // the current channel so it wouldn't leak anything. + 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; @@ -233,6 +293,11 @@ define([ 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); From a522720a592986883eb2224ab9b7f4839570838c Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 2 Jun 2021 15:37:16 +0200 Subject: [PATCH 29/44] Show individual answers --- www/form/app-form.less | 14 ++++ www/form/inner.js | 153 ++++++++++++++++++++++++++++++++++++----- 2 files changed, 148 insertions(+), 19 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index 5a85fff24..6c63d193c 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -237,6 +237,20 @@ } } } + .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; + } + } + } } } diff --git a/www/form/inner.js b/www/form/inner.js index 9cf85bc64..649e8c7b0 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -111,6 +111,13 @@ define([ 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"; @@ -1258,25 +1265,119 @@ define([ 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 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), - ]); + var switchMode = h('button.btn.btn-primary', Messages.form_showIndividual); + $controls.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); + + 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 || 1) { + warning = h('span.cp-form-warning', Messages.form_answerWarning); + } + // XXX back + // XXX confirm curve and ud.curvePublic match + } 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); }); - $container.append(elements); }; var getFormResults = function () { @@ -1290,19 +1391,22 @@ define([ }; 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, false, { mark: { tabindex:1 } }); if (loggedIn) { - cbox = UI.createCheckbox('cp-form-anonymous', - Messages.form_anonymousBox, false, { mark: { tabindex:1 } }); if (!content.answers.anonymous || APP.cantAnon) { $(cbox).hide().find('input').attr('disabled', 'disabled'); } - } + var user = metadataMgr.getUserData(); + console.log(user); + 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 () { @@ -1316,6 +1420,17 @@ define([ 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, From 88f834fbb7e8a07739b03a54083e2f13344637e8 Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 2 Jun 2021 15:40:07 +0200 Subject: [PATCH 30/44] Answer anonymously by default --- www/form/inner.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 649e8c7b0..8e96341ae 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1397,16 +1397,13 @@ define([ var cbox; cbox = UI.createCheckbox('cp-form-anonymous', - Messages.form_anonymousBox, false, { mark: { tabindex:1 } }); + Messages.form_anonymousBox, true, { mark: { tabindex:1 } }); if (loggedIn) { if (!content.answers.anonymous || APP.cantAnon) { - $(cbox).hide().find('input').attr('disabled', 'disabled'); + $(cbox).hide().find('input').attr('disabled', 'disabled').prop('checked', false); } } - var user = metadataMgr.getUserData(); - console.log(user); - 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 () { From 4cc84b8c8038fa9b1f390d0c6d10af36cead6612 Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 2 Jun 2021 16:49:18 +0200 Subject: [PATCH 31/44] Fix forms issues and allow answers in no drive mode --- www/common/cryptpad-common.js | 10 +++- www/common/outer/async-store.js | 1 + www/form/app-form.less | 4 ++ www/form/inner.js | 100 ++++++++++++++++++++------------ www/form/main.js | 34 +++++++++-- 5 files changed, 105 insertions(+), 44 deletions(-) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index dbcbcf8eb..2b12a8855 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -141,7 +141,15 @@ define([ anonymous: data.anonymous } }, function (obj) { - if (obj && obj.error) { console.error(obj.error); } + 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); + } }); }; diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 1af15a9ed..0158d5c51 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); diff --git a/www/form/app-form.less b/www/form/app-form.less index 6c63d193c..18431ead1 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -64,6 +64,10 @@ flex: 1; overflow: auto; + .cp-form-page + .cp-form-send-container { + margin-top: 10px; + } + .cp-form-page-container { display: flex; justify-content: center; diff --git a/www/form/inner.js b/www/form/inner.js index 8e96341ae..8a3baa470 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -108,6 +108,7 @@ define([ Messages.form_delete = "Delete block"; Messages.form_cantFindAnswers = "Unable to retrieve your existing answers for this form."; + Messages.form_answered = "You already answered this form"; Messages.form_viewResults = "Go to responses"; Messages.form_viewCreator = "Go to form creator"; @@ -1274,7 +1275,7 @@ define([ var form = content.form; var switchMode = h('button.btn.btn-primary', Messages.form_showIndividual); - $controls.append(switchMode); + $controls.hide().append(switchMode); var show = function (answers, header) { var elements = content.order.map(function (uid) { @@ -1299,6 +1300,8 @@ define([ }; show(answers); + if (APP.isEditor || APP.isAuditor) { $controls.show(); } + var $s = $(switchMode).click(function () { $results.empty(); if (!summary) { @@ -1436,6 +1439,9 @@ define([ }, 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); } @@ -1663,34 +1669,36 @@ define([ }); _content.push(div); - 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); + 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); @@ -1745,7 +1753,6 @@ define([ $toolbarContainer.after(helpMenu.menu); - // XXX refresh form settings on remote change var makeFormSettings = function () { // Private / public status var resultsType = h('div.cp-form-results-type-container'); @@ -1761,6 +1768,7 @@ define([ 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 () { @@ -1889,11 +1897,6 @@ define([ resultsType, viewResults, ]; - - // XXX - // Button to set results as public - // Checkbox to allow anonymous answers - // Button to clear all answers? }; var checkIntegrity = function (getter) { @@ -2025,18 +2028,22 @@ define([ // * 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 (priv.form_auditorKey) { + var getResults = function (key) { sframeChan.query("Q_FORM_FETCH_ANSWERS", { channel: content.answers.channel, validateKey: content.answers.validateKey, publicKey: content.answers.publicKey, - privateKey: priv.form_auditorKey + 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; } @@ -2073,6 +2080,17 @@ define([ }, 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; @@ -2096,6 +2114,14 @@ define([ 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; diff --git a/www/form/main.js b/www/form/main.js index 94b79165e..0807d5d8f 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -47,7 +47,6 @@ define([ }); 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) { @@ -127,6 +126,7 @@ define([ var accessKeys; var CPNetflux, Pinpad; var network; + var noDriveAnswered = false; nThen(function (w) { require([ '/bower_components/chainpad-netflux/chainpad-netflux.js', @@ -147,6 +147,11 @@ define([ }); })); 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) { @@ -204,6 +209,7 @@ define([ myKey = myFormKeys.curvePublic; } cb({ + noDriveAnswered: noDriveAnswered, myKey: myKey, results: results }); @@ -236,6 +242,15 @@ define([ })); 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); } @@ -266,19 +281,26 @@ define([ }); }); + 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 () { - // XXX if we are a registered user (myKeys.curvePrivate exists), we may - // have already answered anonymously. We should send a "proof" to show - // that the existing anonymous answer are ours (using myKeys.formSeed). - // Even if we never answered anonymously, the keyPair would be unique to - // the current channel so it wouldn't leak anything. var myAnonymousKeys; if (data.anonymous) { if (!myKeys.formSeed) { return void cb({ error: "ANONYMOUS_ERROR" }); } From 9d2b60a0444e72a1ea44755b362746fb0426946c Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 2 Jun 2021 18:50:36 +0200 Subject: [PATCH 32/44] Update form creator UI --- www/form/app-form.less | 55 ++++++++++++++++++++ www/form/inner.js | 113 +++++++++++++++++++++++++++++++---------- 2 files changed, 140 insertions(+), 28 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index 18431ead1..022d11dac 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -64,6 +64,61 @@ flex: 1; overflow: auto; + .cp-form-creator-add-inline { + display: flex; + flex-flow: column; + align-items: center; + margin-bottom: 20px; + button { + width: 50px; + i { + margin-right: 0; + } + } + .cp-form-creator-inline-add { + .add-close { display: none; } + &.displayed { + .add-close { display: inline; } + .add-open { display: none; } + } + } + .cp-form-creator-control-inline { + display: flex; + justify-content: space-around; + margin-top: 10px; + button:not(:last-child) { + margin-right: 5px; + } + .cp-form-creator-types:first-child { + margin-right: 50px; + } + } + } + .cp-form-creator-add-full { + display: flex; + align-items: center; + margin-bottom: 20px; + &> div:first-child { + border-right: 1px solid black; + display: flex; + height: 100%; + align-items: center; + padding-right: 10px; + margin-right: 10px; + } + .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:first-child { + margin-right: 50px; + } + } + } + .cp-form-page + .cp-form-send-container { margin-top: 10px; } diff --git a/www/form/inner.js b/www/form/inner.js index 8a3baa470..d3a849998 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1487,8 +1487,80 @@ define([ APP.formBlocks = []; - // XXX order array later - var elements = content.order.map(function (uid) { + + var getFormCreator = function (uid) { + var full = !uid; + var idx = content.order.indexOf(uid); + var addControl = function (type) { + var btn = h('button.btn.small', { + 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', Messages.tag_add); + 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 () { + console.log($container, $container.find('.cp-form-block')); + $container.find('.cp-form-creator-add-inline').remove(); + $container.find('.cp-form-block').each(function (i, el) { + console.log(i, el); + if (i === 0) { return; } + var $el = $(el); + var uid = $el.attr('data-id'); + $el.before(getFormCreator(uid)); + }); + }; + + + var elements = []; + content.order.forEach(function (uid, i) { var block = form[uid]; var type = block.type; var model = TYPES[type] || STATIC_TYPES[type]; @@ -1511,7 +1583,8 @@ define([ if (answers && answers[uid] && data.setValue) { data.setValue(answers[uid]); } if (data.pageBreak && !editable) { - return data; + elements.push(data); + return; } @@ -1584,6 +1657,7 @@ define([ content.order.splice(idx, 1); $('.cp-form-block[data-id="'+uid+'"]').remove(); framework.localChange(); + updateAddInline(); }); // Values @@ -1637,7 +1711,7 @@ define([ ]); } var editableCls = editable ? ".editable" : ""; - return h('div.cp-form-block'+editableCls, { + elements.push(h('div.cp-form-block'+editableCls, { 'data-id':uid }, [ isStatic ? undefined : q, @@ -1646,9 +1720,13 @@ define([ editButtons ]), editContainer - ]); + ])); }); + if (APP.isEditor) { + elements.push(getFormCreator()); + } + var _content = elements; if (!editable) { _content = []; @@ -1702,6 +1780,7 @@ define([ } $container.empty().append(_content); + updateAddInline(); if (editable) { Sortable.create($container[0], { @@ -1712,6 +1791,7 @@ define([ set: function (s) { content.order = s.toArray(); framework.localChange(); + updateAddInline(); } } }); @@ -1924,33 +2004,10 @@ define([ var controlContainer; if (APP.isEditor) { - var addControl = function (type) { - var btn = h('button.btn', [ - (TYPES[type] || STATIC_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, - }; - content.order.push(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 settings = makeFormSettings(); controlContainer = h('div.cp-form-creator-control', [ h('div.cp-form-creator-settings', settings), - h('div.cp-form-creator-types', controls), - h('div.cp-form-creator-types', staticControls) ]); } From 562bcf64e2ab49ce01e71f2ac825ebb83ff46843 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 3 Jun 2021 17:29:03 +0200 Subject: [PATCH 33/44] Update form UI --- www/form/app-form.less | 56 ++++++++++- www/form/inner.js | 222 ++++++++++++++++++++++------------------- www/form/main.js | 9 +- 3 files changed, 181 insertions(+), 106 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index 022d11dac..484033c90 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -37,6 +37,7 @@ display: flex; flex: 1; justify-content: center; + min-width: 300px; .cp-form-input-block { display: flex; @@ -45,7 +46,22 @@ div.cp-form-creator-container { display: flex; flex: 1; - max-width: 1300px; + justify-content: center; + min-width: 300px; + flex-wrap: wrap; + overflow: auto; + + .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; @@ -58,10 +74,12 @@ } } 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; + flex: 1 1 1000px; overflow: auto; .cp-form-creator-add-inline { @@ -150,6 +168,25 @@ &: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; } @@ -162,9 +199,16 @@ margin-right: 5px; } } + .cp-form-edit-buttons-container { + display: flex; + justify-content: space-between; + } } .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; @@ -181,6 +225,7 @@ min-width: 100px; padding: 0 10px !important; height: auto; + font-size: 20px; } button { .cp-form-edit { @@ -209,6 +254,7 @@ .cp-form-edit-options-block { display: flex; flex-wrap: wrap; + align-items: baseline; .CodeMirror { cursor: default; flex: 1; @@ -229,8 +275,14 @@ align-items: center; justify-content: center; width: 30px; + color: @cp_sidebar-hint; + i:first-child { + margin-right: 1px; + } } .cp-form-edit-block-input { + &.sortable-ghost { visibility: hidden; } + &.sortable-drag { opacity: 0.9 !important; } display: flex; width: 400px; input { diff --git a/www/form/inner.js b/www/form/inner.js index d3a849998..1674cb877 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -78,8 +78,7 @@ define([ Messages.button_newform = "New Form"; // XXX Messages.form_invalid = "Invalid form"; - Messages.form_editBlock = "Edit options"; - Messages.form_editQuestion = "Edit question"; + Messages.form_editBlock = "Edit"; Messages.form_editMax = "Max selectable options"; Messages.form_editType = "Options type"; @@ -105,11 +104,14 @@ define([ Messages.form_update = "Update"; Messages.form_reset = "Reset"; Messages.form_sent = "Sent"; - Messages.form_delete = "Delete block"; + 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"; @@ -122,7 +124,7 @@ define([ Messages.form_notAnswered = "And {0} empty answers"; - Messages.form_makePublic = "Make public"; + 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"; @@ -134,10 +136,9 @@ define([ Messages.form_isClosed = "This form was closed on {0}"; Messages.form_willClose = "This form will close on {0}"; - Messages.form_anonymous_on = "Anonymous answers are allowed"; - Messages.form_anonymous_off = "Anonymous answers are blocked"; - Messages.form_anonymous_button_on = "Block anonymous answers"; - Messages.form_anonymous_button_off = "Allow anonymous answers"; + 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}"; @@ -286,6 +287,7 @@ define([ direction: "vertical", handle: ".cp-form-handle", draggable: ".cp-form-edit-block-input", + forceFallback: true, }); var containerItems; @@ -299,6 +301,7 @@ define([ direction: "vertical", handle: ".cp-form-handle", draggable: ".cp-form-edit-block-input", + forceFallback: true, }); } @@ -320,7 +323,7 @@ define([ }); } - // Calendar time // XXX + // Calendar time if (v.type) { var multipleInput = h('input'); var multipleClearButton = h('button.btn', Messages.form_clear); @@ -566,7 +569,7 @@ define([ return h('div.cp-poll-cell.cp-form-poll-option', data); }); // Insert axis switch button - var switchAxis = h('button.btn', [ + var switchAxis = h('button.btn.btn-default', [ h('i.fa.fa-exchange'), ]); els.unshift(h('div.cp-poll-cell.cp-poll-switch', switchAxis)); @@ -582,7 +585,6 @@ define([ _days[day] = _days[day] || 0; _days[day]++; }); - var dayValues = Object.keys(_days).map(function (d) { return _days[d]; }); Object.keys(_days).forEach(function (day) { days.push(h('div.cp-poll-cell.cp-poll-time-day', { style: 'flex-grow:'+(_days[day]-1)+';' @@ -656,6 +658,7 @@ define([ }, opts.text); var $tag = $(tag); DiffMd.apply(DiffMd.render(opts.text || ''), $tag, APP.common); + var cursorGetter; return { tag: tag, edit: function (cb, tmp) { @@ -718,7 +721,7 @@ define([ cursor = { start: editor.getCursor('from'), end: editor.getCursor('to') - } + }; } return { old: opts, @@ -744,7 +747,6 @@ define([ h('i.fa.fa-hand-o-right'), h('span', Messages.form_type_page) ]); - var $tag = $(tag); return { tag: tag, pageBreak: true @@ -1346,11 +1348,9 @@ define([ user = h('a', { href: getHref(ud.profile) // Only used visually }, Util.fixHTML(ud.name || Messages.anonymous)); - if (curve !== ud.curvePublic || 1) { + if (curve !== ud.curvePublic) { warning = h('span.cp-form-warning', Messages.form_answerWarning); } - // XXX back - // XXX confirm curve and ud.curvePublic match } else { user = h('b', Util.fixHTML(ud.name || Messages.anonymous)); } @@ -1383,6 +1383,38 @@ define([ }); }; + 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(); + // XXX when not APP.isEditor, change icon and text + 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 = {}; @@ -1445,30 +1477,16 @@ define([ 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); }); }); - var viewResults; - if (content.answers.privateKey) { - viewResults = h('button.btn.btn-primary', [ - h('span.cp-app-form-button-results', Messages.form_viewResults), - ]); - var sframeChan = framework._.sfCommon.getSframeChannel(); - var $v = $(viewResults).click(function () { - $v.attr('disabled', 'disabled'); - sframeChan.query("Q_FORM_FETCH_ANSWERS", content.answers, function (err, obj) { - var answers = obj && obj.results; - if (answers) { APP.answers = answers; } - $v.removeAttr('disabled'); - $('body').addClass('cp-app-form-results'); - renderResults(content, answers); - }); - }); - } - if (APP.isClosed) { send = undefined; reset = undefined; @@ -1476,7 +1494,7 @@ define([ return h('div.cp-form-send-container', [ cbox ? h('div', cbox) : undefined, - send, reset, viewResults + send, reset ]); }; var updateForm = function (framework, content, editable, answers, temp) { @@ -1489,6 +1507,7 @@ define([ var getFormCreator = function (uid) { + if (!APP.isEditor) { return; } var full = !uid; var idx = content.order.indexOf(uid); var addControl = function (type) { @@ -1538,7 +1557,7 @@ define([ }); } - var inlineCls = full ? '-full' : '-inline' + var inlineCls = full ? '-full' : '-inline'; return h('div.cp-form-creator-add'+inlineCls, [ h('div', add), buttons @@ -1547,7 +1566,6 @@ define([ }; var updateAddInline = function () { - console.log($container, $container.find('.cp-form-block')); $container.find('.cp-form-creator-add-inline').remove(); $container.find('.cp-form-block').each(function (i, el) { console.log(i, el); @@ -1560,7 +1578,7 @@ define([ var elements = []; - content.order.forEach(function (uid, i) { + content.order.forEach(function (uid) { var block = form[uid]; var type = block.type; var model = TYPES[type] || STATIC_TYPES[type]; @@ -1588,63 +1606,64 @@ define([ } + var dragHandle; var q = h('div.cp-form-block-question', block.q || Messages.form_default); var editButtons, editContainer; APP.formBlocks.push(data); if (editable) { - // Question + // 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 saveQ = h('button.btn.btn-primary.small', [ - h('i.fa.fa-pencil.cp-form-edit'), - h('span.cp-form-edit', Messages.form_editQuestion), - h('i.fa.fa-floppy-o.cp-form-save'), - h('span.cp-form-save', Messages.settings_save) - ]); - var dragHandle = h('i.fa.fa-arrows-v.cp-form-block-drag'); - var $saveQ = $(saveQ).click(function () { - if (!$(q).hasClass('editing')) { - $(q).addClass('editing'); - $inputQ.focus(); + 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); } + if (saving && !e) { return; } // Prevent spam Enter block.q = v.trim(); framework.localChange(); - $saveQ.attr('disabled', 'disabled'); + saving = true; framework._.cpNfInner.chainpad.onSettle(function () { + saving = false; $(q).removeClass('editing'); - $saveQ.removeAttr('disabled'); - $inputQ.blur(); + if (!e) { $inputQ.blur(); } UI.log(Messages.saved); }); - }); - var onCancelQ = function (e) { - if (e && e.relatedTarget && e.relatedTarget === saveQ) { return; } + }; + var onCancelQ = function () { $inputQ.val(block.q || Messages.form_default); - if (!e) { $inputQ.blur(); } + cancel = true; + $inputQ.blur(); $(q).removeClass('editing'); }; $inputQ.keydown(function (e) { - if (e.which === 13) { return void $saveQ.click(); } + if (e.which === 13) { return void onSaveQ(); } if (e.which === 27) { return void onCancelQ(); } }); $inputQ.focus(function () { $(q).addClass('editing'); }); - $inputQ.blur(onCancelQ); - q = h('div.cp-form-input-block', [inputQ, saveQ, dragHandle]); + $inputQ.blur(onSaveQ); + q = h('div.cp-form-input-block', [inputQ]); // Delete question - var edit; - var del = h('button.btn.btn-danger', [ + var edit = h('span'); + var del = h('button.btn.btn-danger-alt', [ h('i.fa.fa-trash-o'), h('span', Messages.form_delete) ]); @@ -1662,7 +1681,7 @@ define([ // Values if (data.edit) { - edit = h('button.btn.btn-primary.cp-form-edit-button', [ + edit = h('button.btn.btn-default.cp-form-edit-button', [ h('i.fa.fa-pencil'), h('span', Messages.form_editBlock) ]); @@ -1714,6 +1733,7 @@ define([ 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, @@ -1733,8 +1753,10 @@ define([ var div = h('div.cp-form-page'); var pages = 1; var wasPage = false; - elements.forEach(function (obj) { + 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++; @@ -1787,6 +1809,11 @@ define([ direction: "vertical", filter: "input, button, .CodeMirror", preventOnFilter: false, + draggable: ".cp-form-block", + forceFallback: true, + onStart: function () { + $container.find('.cp-form-creator-add-inline').remove(); + }, store: { set: function (s) { content.order = s.toArray(); @@ -1839,11 +1866,12 @@ define([ var $results = $(resultsType); var refreshPublic = function () { $results.empty(); - var makePublic = h('button.btn.btn-primary', Messages.form_makePublic); - if (content.answers.privateKey) { makePublic = undefined; } + 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(makePublic); + $results.append(makePublicDiv); var $makePublic = $(makePublic).click(function () { UI.confirm(Messages.form_makePublicWarning, function (yes) { if (!yes) { return; } @@ -1866,19 +1894,28 @@ define([ var refreshPrivacy = function () { $privacy.empty(); var anonymous = content.answers.anonymous; - var key = anonymous ? 'on' : 'off'; - var button = h('button.btn.btn-secondary', Messages['form_anonymous_button_'+key]); - var $b = $(button).click(function () { - $b.attr('disabled', 'disabled'); - content.answers.anonymous = !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); - refreshPrivacy(); }); }); - $privacy.append(h('div.cp-form-status', Messages['form_anonymous_'+key])); - $privacy.append(h('div.cp-form-actions', button)); + $privacy.append(h('div.cp-form-status', Messages.form_anonymous)); + $privacy.append(h('div.cp-form-actions', radioContainer)); }; refreshPrivacy(); @@ -1943,30 +1980,6 @@ define([ refreshEndDate(); - var viewResults = h('button.btn.btn-primary', [ - h('span.cp-app-form-button-results', Messages.form_viewResults), - h('span.cp-app-form-button-creator', Messages.form_viewCreator), - ]); - var $v = $(viewResults).click(function () { - if ($body.hasClass('cp-app-form-results')) { - $body.removeClass('cp-app-form-results'); - return; - } - $v.attr('disabled', 'disabled'); - 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; } - $v.removeAttr('disabled'); - $body.addClass('cp-app-form-results'); - renderResults(content, answers); - }); - - }); - evOnChange.reg(refreshPublic); evOnChange.reg(refreshPrivacy); evOnChange.reg(refreshEndDate); @@ -1975,7 +1988,6 @@ define([ endDateContainer, privacyContainer, resultsType, - viewResults, ]; }; @@ -2003,12 +2015,14 @@ define([ 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'); @@ -2016,7 +2030,8 @@ define([ var div = h('div.cp-form-creator-container', [ controlContainer, contentContainer, - resultsContainer + resultsContainer, + fillerContainer ]); return div; }; @@ -2105,6 +2120,7 @@ define([ } if (APP.isEditor) { + addResultsButton(framework, content); sframeChan.query("Q_FORM_FETCH_ANSWERS", { channel: content.answers.channel, validateKey: content.answers.validateKey, @@ -2160,6 +2176,10 @@ define([ } // 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; diff --git a/www/form/main.js b/www/form/main.js index 0807d5d8f..344c5bb0c 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -116,8 +116,11 @@ define([ Nacl.util.decodeBase64(pub), Nacl.util.decodeBase64(curvePrivate) ); - } catch (e) { console.error(e); } - return channel === Nacl.util.encodeUTF8(u8_plain); + 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); @@ -219,7 +222,7 @@ define([ var parsed = Utils.Util.tryParse(msg); if (!parsed) { return; } if (parsed._proof) { - var check = checkAnonProof(parsed._proof, data.channel, curvePrivate) + var check = checkAnonProof(parsed._proof, data.channel, curvePrivate); if (check) { delete results[parsed._proof.key]; } From a0cd1d7195e1565a9bf99775f44d95c4995df6ea Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 3 Jun 2021 17:44:59 +0200 Subject: [PATCH 34/44] Insert image in description blocks --- www/form/inner.js | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 1674cb877..635c55219 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -646,6 +646,11 @@ define([ }).filter(Boolean); }; + var checkMt = function (framework) { + var cm = $('.CodeMirror').length; + if (!cm) { framework.setMediaTagEmbedder(); } + }; + var STATIC_TYPES = { md: { defaultOpts: { @@ -661,7 +666,7 @@ define([ var cursorGetter; return { tag: tag, - edit: function (cb, tmp) { + edit: function (cb, tmp, framework) { var t = h('textarea'); var block = h('div.cp-form-edit-options-block', [t]); var cm = SFCodeMirror.create("gfm", CMeditor, t); @@ -671,6 +676,14 @@ define([ editor.setOption('styleActiveLine', true); editor.setOption('readOnly', false); + editor.on('focus', function () { + framework.setMediaTagEmbedder(); + framework.setMediaTagEmbedder(function (mt) { + editor.focus(); + editor.replaceSelection($(mt)[0].outerHTML); + }); + }) + var text = opts.text; var cursor; if (tmp && tmp.content && tmp.old.text === text) { @@ -699,7 +712,10 @@ define([ } // Cancel changes var cancelBlock = h('button.btn.btn-secondary', Messages.cancel); - $(cancelBlock).click(function () { cb(); }); + $(cancelBlock).click(function () { + cb(); + checkMt(framework); + }); // Save changes var saveBlock = h('button.btn.btn-primary', [ h('i.fa.fa-floppy-o'), @@ -714,6 +730,7 @@ define([ $(saveBlock).click(function () { $(saveBlock).attr('disabled', 'disabled'); cb(getContent()); + checkMt(framework); }); cursorGetter = function () { @@ -1568,8 +1585,6 @@ define([ var updateAddInline = function () { $container.find('.cp-form-creator-add-inline').remove(); $container.find('.cp-form-block').each(function (i, el) { - console.log(i, el); - if (i === 0) { return; } var $el = $(el); var uid = $el.attr('data-id'); $el.before(getFormCreator(uid)); @@ -1634,6 +1649,12 @@ define([ } 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(); @@ -1677,6 +1698,7 @@ define([ $('.cp-form-block[data-id="'+uid+'"]').remove(); framework.localChange(); updateAddInline(); + checkMt(framework); }); // Values @@ -1710,7 +1732,7 @@ define([ var onEdit = function (tmp) { data.editing = true; $(data.tag).hide(); - $(editContainer).append(data.edit(onSave, tmp)); + $(editContainer).append(data.edit(onSave, tmp, framework)); $(editButtons).hide(); }; $(edit).click(function () { @@ -1811,6 +1833,7 @@ define([ preventOnFilter: false, draggable: ".cp-form-block", forceFallback: true, + fallbackTolerance: 5, onStart: function () { $container.find('.cp-form-creator-add-inline').remove(); }, From 1b56f268098a239c72709fc9e67a61b9a461839f Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 3 Jun 2021 18:02:56 +0200 Subject: [PATCH 35/44] Add a file upload button to the markdown toolbar (optional) --- www/common/common-ui-elements.js | 37 +++++++++++++++++++++++++++++--- www/form/inner.js | 25 ++++++--------------- www/kanban/inner.js | 7 +++++- 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 6485e9c64..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 diff --git a/www/form/inner.js b/www/form/inner.js index 635c55219..b4407c0f6 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -646,11 +646,6 @@ define([ }).filter(Boolean); }; - var checkMt = function (framework) { - var cm = $('.CodeMirror').length; - if (!cm) { framework.setMediaTagEmbedder(); } - }; - var STATIC_TYPES = { md: { defaultOpts: { @@ -666,7 +661,7 @@ define([ var cursorGetter; return { tag: tag, - edit: function (cb, tmp, framework) { + 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); @@ -676,14 +671,6 @@ define([ editor.setOption('styleActiveLine', true); editor.setOption('readOnly', false); - editor.on('focus', function () { - framework.setMediaTagEmbedder(); - framework.setMediaTagEmbedder(function (mt) { - editor.focus(); - editor.replaceSelection($(mt)[0].outerHTML); - }); - }) - var text = opts.text; var cursor; if (tmp && tmp.content && tmp.old.text === text) { @@ -705,7 +692,12 @@ define([ editor.focus(); }); if (APP.common) { - var markdownTb = APP.common.createMarkdownToolbar(editor); + 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 () {}); @@ -714,7 +706,6 @@ define([ var cancelBlock = h('button.btn.btn-secondary', Messages.cancel); $(cancelBlock).click(function () { cb(); - checkMt(framework); }); // Save changes var saveBlock = h('button.btn.btn-primary', [ @@ -730,7 +721,6 @@ define([ $(saveBlock).click(function () { $(saveBlock).attr('disabled', 'disabled'); cb(getContent()); - checkMt(framework); }); cursorGetter = function () { @@ -1698,7 +1688,6 @@ define([ $('.cp-form-block[data-id="'+uid+'"]').remove(); framework.localChange(); updateAddInline(); - checkMt(framework); }); // Values 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(); From 926edc7e5e8cc1a83ca70c4d190fe69889ba6a2d Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 3 Jun 2021 18:31:50 +0200 Subject: [PATCH 36/44] Add onbeforeunload to prevent data loss --- www/form/inner.js | 45 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index b4407c0f6..7b37ff4b1 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -765,9 +765,12 @@ define([ }; var TYPES = { input: { - get: function () { + get: function (opts, a, n, evOnChange) { var tag = h('input'); var $tag = $(tag); + $tag.on('change keypress', Util.throttle(function () { + evOnChange.fire(); + }, 500)); return { tag: tag, getValue: function () { return $tag.val(); }, @@ -796,7 +799,7 @@ define([ return Messages._getKey('form_defaultOption', [i]); }) }, - get: function (opts) { + get: function (opts, a, n, evOnChange) { if (!opts) { opts = TYPES.radio.defaultOpts; } if (!Array.isArray(opts.values)) { return; } var name = Util.uid(); @@ -809,6 +812,9 @@ define([ 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 () { @@ -876,7 +882,7 @@ define([ return Messages._getKey('form_defaultOption', [i]); }) }, - get: function (opts) { + 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) { @@ -899,6 +905,9 @@ define([ 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 () { @@ -980,7 +989,7 @@ define([ return Messages._getKey('form_defaultOption', [i]); }) }, - get: function (opts) { + get: function (opts, a, n, evOnChange) { if (!opts) { opts = TYPES.checkbox.defaultOpts; } if (!Array.isArray(opts.values)) { return; } var name = Util.uid(); @@ -1002,6 +1011,7 @@ define([ } else { $tag.find('input').removeAttr('disabled'); } + evOnChange.fire(); }); var cursorGetter; var setCursorGetter = function (f) { cursorGetter = f; }; @@ -1074,7 +1084,7 @@ define([ return Messages._getKey('form_defaultOption', [i]); }) }, - get: function (opts) { + 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) { @@ -1099,6 +1109,7 @@ define([ } else { $(l).find('input').removeAttr('disabled'); } + evOnChange.fire(); }); }); @@ -1191,7 +1202,7 @@ define([ return Messages._getKey('form_defaultOption', [i]); }) }, - get: function (opts, answers, username) { + get: function (opts, answers, username, evOnChange) { if (!opts) { opts = TYPES.poll.defaultOpts; } if (!Array.isArray(opts.values)) { return; } @@ -1211,6 +1222,7 @@ define([ $c.click(function () { val = (val+1)%3; $c.attr('data-value', val); + evOnChange.fire(); }); cell._setValue = function (v) { val = v; @@ -1512,6 +1524,23 @@ define([ 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; } @@ -1600,7 +1629,7 @@ define([ name = user.name; } - var data = model.get(block.opts, _answers, 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]); } @@ -1713,7 +1742,7 @@ define([ $(editButtons).show(); UI.log(Messages.saved); var _answers = getBlockAnswers(APP.answers, uid); - data = model.get(newOpts, _answers); + data = model.get(newOpts, _answers, null, evOnChange); if (!data) { data = {}; } $oldTag.before(data.tag).remove(); }); From 04b35c05adba37ecb90f9e589372296f58979272 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 4 Jun 2021 10:49:08 +0200 Subject: [PATCH 37/44] Fix XXX --- www/form/inner.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 7b37ff4b1..cea932e3f 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -97,6 +97,8 @@ define([ 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"; @@ -649,7 +651,7 @@ define([ var STATIC_TYPES = { md: { defaultOpts: { - text: "Your text here" // XXX + text: Messages.form_description_default }, get: function (opts) { if (!opts) { opts = STATIC_TYPES.md.defaultOpts; } @@ -1417,7 +1419,6 @@ define([ $('body').addClass('cp-app-form-results'); renderResults(content, answers); $res.remove(); - // XXX when not APP.isEditor, change icon and text 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) @@ -2137,9 +2138,6 @@ define([ if (!content.answers || !content.answers.channel || !content.answers.publicKey || !content.answers.validateKey) { 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 var getResults = function (key) { sframeChan.query("Q_FORM_FETCH_ANSWERS", { From 569a1932cbe33a0f7803e339ebe4ae5fb171db47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Benqu=C3=A9?= Date: Fri, 4 Jun 2021 09:50:24 +0100 Subject: [PATCH 38/44] Update cptools icon font --- .../fonts/cptools/fonts/cptools.svg | 13 ++- .../fonts/cptools/fonts/cptools.ttf | Bin 6320 -> 10000 bytes .../fonts/cptools/fonts/cptools.woff | Bin 6396 -> 10076 bytes customize.dist/fonts/cptools/style.css | 85 ++++++++++++------ 4 files changed, 67 insertions(+), 31 deletions(-) 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 @@ Generated by IcoMoon - + @@ -23,9 +23,18 @@ + - + + + + + + + + + \ 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 cfc9d1e3ca7d09f1e53675b85c93b2bea7528e87..88ab53d943c0aeec680fcd563c2aebb03b5580bb 100644 GIT binary patch delta 4361 zcmbUkZEPIXao&4-yZ7PSy|3M~liZ!{v(HJBJL5b1E=@um+o^FOj$>m{8MV#1iDO6d z$Ca4Kp|p!o9R);GiYXzYs!#z7Dv?mR?JpFl5(%VAZ58pORb3>Cs;W|@R>hzSeCN*F zy*uyOKAW<4Z+GU+%zN`@=FPnKM%&XTbRYoWg2#ZtT?6;GCKaDphdpm%%buPZ%>^I0 z^D+S0gZS|H==mIeJ&5zKeSGrb**E6udl8=lXnc8MZ1haX`?HS$nmB)Y0z0Jt3i}Wr zM!acaYIg3C3${ZDB0pT7oH^Y!`sD8(e+r-(ap;{Ioy$Qh`32&Ah}Tb#PK{l=*YO$R z6FAAG+|2n|R1}}szd0QkyYr*BJ4xsna(Mqi>~Vq}DE= z%9EvOa@C`|;6XUS{?7J76O6)7;W{i6LSp1Tk|U3i>o{S1td%54v{6(@OYticHK0XE zn52^FPVyDfVdG301gbMjl|n&NSm)9WUsRe4qkj8IqZlFq?J!BEJ2LGt)KwBo`k_|% zPbD=qAfgUszVGY%~!^GJZ8m^An85nrP{wvo`pkY|YIc*h;4G z5fucAY61M#Es#BQgAIYqle5Q4HqMH|j93t~VEa?QDo$Kw2544<;*+fxf4 zm9#!<1XVF|`uRxnmblu2+J@xWqKz3V=^DTYaY!iblq82>(`1Q>xJ*=e7oJg*CI#HaA?v~TkDzSFZ#o+qlnMPqp0x7)^xoTX6OP348X8Z%l4ODFaWb~4Q|4R zh9fEryRmX?%)02P3jQjN(YG&+ILprU^K*HxN)^&8OEH~a_a2!k+7=CL_3AnY{mVK> zQ#r41DjT@}v1}BslrsZzl=6;lRu;};NgWmjE4^^d2q+#rM!aAgm1Y2Cj&n;u;1kcQua3xgRUVb~OF5+RV2KwM@>{FIO zs1}b>cfs9oFMJ(NLk=E=%kV6Kzfr_f6IO3LB=H1LMy#;SIHyD#V;$}3poOPGpc2tl zi;S(BOtndV6+e13&KsF5LZq_@tbV5W{fg}O%bR%Cx#NoN^C6QjpV`D!9;q;19EoMj z$x5v2-{U?-!GRRt&HtA}Umv93FPjtbbSxPzSwWZZbbJ5~!dGDkzP)ooCRQ6-ja%Um zZ;4Ci{>0o-k<_ba+ST8b^xbIPQa|goW@fro{v@xJtaRL`Z!}!tlM?Z8InAOqy4U_D zF!lmyXpK0uK?i&pgVYF1Z_3+%;dq#+DO`+^uJC-$5BR|-KkDneW6zQ&uyj81IxnM& z+D9d?0+ZV&X041UULWL1Mbzo8kDd*@UCCZ|JSCMWZLvsWzEN!8E_iV2)^!_!UJ@cle`f6t4|972bS;rP~fVE|}UFm?YW}4D%-qu~DFPvx>-5i7k`?aT9TSfv26huqvoA#Knn#wM37;pipY+y85{Fv$ zg?2|=ez#no?2}~a{&HB;%$@GOy^`B5Mvg|>e=Ip1=Ba{Amk%B24&$-E|I+VIDMvmA zmtf$_@89U;fcag!EuPvgQ#Z7|ad52CaaTN>M-(R#rx^g<7| z0c?H6){Wo6VjEfbrS_%0&gWetLIM7OwG(p!#S)VGZ+Um_?uq%ZLnc@OUjm=Lu2C)PmVr>NpB3s;bEA>3;sh} KaxNUL&HWD$7=ZWy delta 675 zcmZvZPfQb05XQgPS4;nhMR(g;+e3{74H{H598gml^(x)c>|RW9vt8PdZo8o+)QfQ9 zjMT=HhAYUyq=W=xOf`gf^kkx#XyQTlu18pBs|ON0o7vg#H}mGrd%JS)<<&(n00At4 z`81 z>|X#vzbHReBGC7ae+I&rNGD51y)pI5^@MbWbfH|;((1T7l_Ri4;JTqUOe7hN6#hv% zQBe(jXLdLSM7*@gvRSRwX;8A$D^4DquRZRhSZI~qxHduLIJ|JdO@~TYK`Ue**fvqB zv|yd=DrSF=xFejp&@Sy6_H>}V9zMbQ681;_jc+vHM01O8h1hK|9v7cw6s2-LCCi!3 zj4T@$CKN^5loZ+MvRS7jE%dkgM})%ytHgx8gXippC%{|W!{5vf y?uIn8UagjE&|spDDm`V?niq%8=6Ys&!mM7LFROFNLPrtv)Ir5u&&uZ7kogDCeWH>8 diff --git a/customize.dist/fonts/cptools/fonts/cptools.woff b/customize.dist/fonts/cptools/fonts/cptools.woff index 80432d647c818a554ca3f80e53c7bc5b7a2c8429..320f803e6ad92cb55647ccdfc027c127d179a393 100644 GIT binary patch delta 4429 zcmbUkZEPIX@y&aCyZ5>Gxw~h=-Pyj_PMX}g_Stu75^!Rt#sxbju0>@;n{yL?M6zR- zm^7iZTcJ7vl!&%&2@zG57O)Bu3CJx!Dil)168vhbh##%$BBiLRkSg^D236p@>%85& z^WNEKQ}%9l=e?PEZ{EzjnfKP(R*r_gJJ{PxfZ)^e2yq!--@=`6_$PY$E&dUTc$ zn8f~J4%$PvUz!*_!~5RCw!H|xiK+AB*tbkb<4YV~KUd#7IW~HdkeysM%^~9d^~aOg ziG5eGy_19VUt#y;^xQc_QIc@^=4$u7Y$&1sY=WybtpJBV+ zfL3_p^yu^$_Wc$ozrf+j{f^IOXU@#wat@aH7-EdwUT{MfL>Bttz@B~B<3#l-{(#hf z7%Aim1?)O(ctl3XJz0MHB%z%2X_uHn=j z&8?7xc%$frmf}|iC8Wk63aR#V7u*Y-HqMkmq&BluDWsXgI+t$vqS9O#_1goDVgy3! z0ccNmW;&WtS4k}Chg#u3)vhWb5p^i@b)>nzdQ)wU*OzA~xSb9BUt}bmo3+w1s1=&C z)|%Q?wE5O+qQ78Np*F@uI+mm&#jir~8K5G=<~(Jvo6=;G_vtEUw6R3e6fhV~l7j5a zMw1C7<5#6LH_2!sL0Nftlv*DiDfn`W$1X@xS{FVH52PZJf|5_RF7ZVa%yfC4ZO zb%`;D&Ed5>oGRmN4$&3PT{XwSS;tG`FiP2gspCAQ@Sugm(2to*G;!qEjt&C`ht8K) zW0D-8&W7TQamC{v09t1%E=km)cQY02l1pO(4u&gcbj)fcoQa~L zOvGh+1W{3!1_A*lc?y3P(rjsATp8AHD$xhjSWLY>lFgpq+nddfylrB~kB{6K$sRwy z_de`9%USvq@5|nllHUi{$pG%)^JDoAQOjzB&G)&u$(?d_EpU`A;^d>!mP57dWuC z4hvznr|g13GDohE8{`AS5fz55t{l}_7adi>U&Ar_)};|=*}i^$F7MT-LV9H>rt|CG zgPEdj(ZE)(u5-}8uXF4y=k*O`1NT3cjr^r@WI) z7mTA)Z7K#7d|C`W%@0qM(dWw8<#NCnQ8VY$F?Qer&rr29Z~ot##?DcmnJ?fEi-h84FN z=ahJ3b7x0dv+z_1R5o~WncfzX& z0>~7|Wj1h?N6JqWM`9UsvJ&h1w?E+Z;y~WOjsKTJUms*3D4P@UbSxPzSw)xdbi9}B zBVQ#$Nr`x%oMzD)-GzS&jJ-fqv_^uokxudz3{oR3{V9I~hT{QHQn(l+UE%qhAMmv} zKkD^MA)rStLEcp3F7eN@8w>k;$W_mpTzuv_W^IKTPQwD2pnKhsMd#)cCijd09>UgAN~9)v-Jz9Mwv_gE1|eyV+Gm-Bg-+x0Ei4R_T2 zW6y)~Gu|QZUwrMpi@x9Zhj0?Eej{8b?uC1`W#!a~ndzCC)8rIZmKltX8T_4I(83MF jn^HFI92%Q=Y-;pTOon4*f;>j1@Z$gIrko2G!?XVbC4Pz0 delta 750 zcmZvZPiWIn9LImJuXb(PxoOhWtvyuiHW@PT-wso0i@MXW?N*YDbcD7xjHC%`N9V=f zc9&SZ=?L97hsX-XL$Ja`gx1qU7^;B~ic@%Pdp1N#f#y!ZY5e((3*OS%sZuZoS# z>?{z#xNrzTcYF)PN>7MSubgk5`B~Hz6M(3gdB|kv?5`E2##jfv|3t$T?eQaIUjT$U zQ|3wel3G?u0AdPjrkO_9xQ2=l*(&-3)4(VCL)Gi(#TV$Y9!d)zJkKjQ)rq7eWjC{uq+d?Sa&9vxOS;RJSV@{6YzWj(!cpB?7`pJ<)ho=wa4dawrJ4DPR)KIYA@aWzBqLI z0D@jdVXs|0H98$H&@BY)=Dol*bjR5N{17bg%{rZqkzS^U*fR9fv+52jCb( z Date: Fri, 4 Jun 2021 10:14:13 +0100 Subject: [PATCH 39/44] Use new icons --- www/form/inner.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 7b37ff4b1..1baecd238 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -746,12 +746,12 @@ define([ }; }, printResults: function () { return; }, - icon: h('i.fa.fa-info') + icon: h('i.cptools.cptools-form-paragraph') }, page: { get: function () { var tag = h('div.cp-form-page-break-edit', [ - h('i.fa.fa-hand-o-right'), + h('i.cptools.cptools-form-page-break'), h('span', Messages.form_type_page) ]); return { @@ -760,7 +760,7 @@ define([ }; }, printResults: function () { return; }, - icon: h('i.fa.fa-hand-o-right') + icon: h('i.cptools.cptools-form-page-break') }, }; var TYPES = { @@ -791,7 +791,7 @@ define([ return h('div.cp-form-results-type-text', results); }, - icon: h('i.fa.fa-font') + icon: h('i.cptools.cptools-form-text') }, radio: { defaultOpts: { @@ -868,7 +868,7 @@ define([ return h('div.cp-form-results-type-radio', results); }, - icon: h('i.fa.fa-list-ul') + icon: h('i.cptools.cptools-form-list-radio') }, multiradio: { defaultOpts: { @@ -980,7 +980,7 @@ define([ return h('div.cp-form-results-type-radio', results); }, - icon: h('i.fa.fa-list-ul') + icon: h('i.cptools.cptools-form-grid-radio') }, checkbox: { defaultOpts: { @@ -1069,7 +1069,7 @@ define([ return h('div.cp-form-results-type-radio', results); }, - icon: h('i.fa.fa-check-square-o') + icon: h('i.cptools.cptools-form-list-check') }, multicheck: { defaultOpts: { @@ -1193,7 +1193,7 @@ define([ return h('div.cp-form-results-type-radio', results); }, - icon: h('i.fa.fa-list-ul') + icon: h('i.cptools.cptools-form-grid-check') }, poll: { defaultOpts: { @@ -1281,7 +1281,7 @@ define([ var lines = makePollTable(_answers, form[uid].opts); return h('div.cp-form-type-poll', lines); }, - icon: h('i.cptools.cptools-poll') + icon: h('i.cptools.cptools-form-poll') }, }; From 97eea3a7f040a70f5bfbb33e28246a445d74f410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Benqu=C3=A9?= Date: Fri, 4 Jun 2021 13:14:11 +0100 Subject: [PATCH 40/44] Layout of inline add menu - one one line and left aligned - using large cptools icons --- www/form/app-form.less | 20 ++++++++++++++++---- www/form/inner.js | 2 +- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index 484033c90..65672ec26 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -13,6 +13,7 @@ flex-flow: column; font: @colortheme_app-font; color: @cryptpad_text_col; + background-color: @cp_app-bg; #cp-app-form-editor { flex: 1; @@ -84,7 +85,7 @@ .cp-form-creator-add-inline { display: flex; - flex-flow: column; + flex-flow: row; align-items: center; margin-bottom: 20px; button { @@ -94,6 +95,8 @@ } } .cp-form-creator-inline-add { + font-size: 25px; + margin-right: 30px; .add-close { display: none; } &.displayed { .add-close { display: inline; } @@ -103,12 +106,21 @@ .cp-form-creator-control-inline { display: flex; justify-content: space-around; - margin-top: 10px; button:not(:last-child) { margin-right: 5px; } - .cp-form-creator-types:first-child { - margin-right: 50px; + .cp-form-creator-types { + button { + border: 0px; + //padding-bottom: 3px; + i { + font-size: 35px; + line-height: 35px; + } + } + &:first-child { + margin-right: 50px; + } } } } diff --git a/www/form/inner.js b/www/form/inner.js index 1baecd238..6a866c4e0 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1547,7 +1547,7 @@ define([ var full = !uid; var idx = content.order.indexOf(uid); var addControl = function (type) { - var btn = h('button.btn.small', { + var btn = h('button.btn.btn-default', { title: full ? undefined : Messages['form_type_'+type] }, [ (TYPES[type] || STATIC_TYPES[type]).icon.cloneNode(), From 1cd9808f44f26eb7afc090bdabe49207a085ab75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Benqu=C3=A9?= Date: Fri, 4 Jun 2021 14:38:45 +0100 Subject: [PATCH 41/44] Layout of full add menu --- www/form/app-form.less | 24 ++++++++++++++++++++---- www/form/inner.js | 2 +- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index 65672ec26..649bf84d9 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -127,14 +127,18 @@ .cp-form-creator-add-full { display: flex; align-items: center; - margin-bottom: 20px; + margin: 50px 0px 100px 0px; &> div:first-child { - border-right: 1px solid black; + 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; @@ -143,8 +147,20 @@ button:not(:last-child) { margin-right: 5px; } - .cp-form-creator-types:first-child { - margin-right: 50px; + .cp-form-creator-types { + button { + border: 0px; + padding:5px; + margin-right: 10px; + i { + font-size: 35px; + line-height: 35px; + } + } + &:first-child { + margin-bottom: 20px; + margin-right: 50px; + } } } } diff --git a/www/form/inner.js b/www/form/inner.js index 6a866c4e0..9e0bc644a 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1578,7 +1578,7 @@ define([ h('div.cp-form-creator-types', controls), h('div.cp-form-creator-types', staticControls) ]); - var add = h('div', Messages.tag_add); + var add = h('div', [h('i.fa.fa-plus')]); if (!full) { add = h('button.btn.cp-form-creator-inline-add', { title: Messages.tag_add From 8eb5c3e6dbe822347181ad8ef105d6858f1dc51a Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 4 Jun 2021 16:21:54 +0200 Subject: [PATCH 42/44] Add sorted list, textarea and typed input --- .../src/less2/include/colortheme-dark.less | 1 + .../src/less2/include/colortheme.less | 3 +- www/form/app-form.less | 23 +- www/form/inner.js | 306 ++++++++++++++++-- 4 files changed, 313 insertions(+), 20 deletions(-) diff --git a/customize.dist/src/less2/include/colortheme-dark.less b/customize.dist/src/less2/include/colortheme-dark.less index 8dbe77271..5c3091b6b 100644 --- a/customize.dist/src/less2/include/colortheme-dark.less +++ b/customize.dist/src/less2/include/colortheme-dark.less @@ -436,3 +436,4 @@ @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 e68da8835..1102f6074 100644 --- a/customize.dist/src/less2/include/colortheme.less +++ b/customize.dist/src/less2/include/colortheme.less @@ -435,4 +435,5 @@ @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-poll-yes-color: @cryptpad_color_green; +@cp_form-invalid: @cryptpad_color_red; diff --git a/www/form/app-form.less b/www/form/app-form.less index 484033c90..c7a8e5fea 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -203,6 +203,9 @@ display: flex; justify-content: space-between; } + input:invalid { + border: 1px solid @cp_form-invalid; + } } .cp-form-input-block { //width: @form_input-width; @@ -328,6 +331,12 @@ &: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 { @@ -390,6 +399,18 @@ } } } + .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; @@ -461,7 +482,7 @@ border: 5px double @cp_form-bg1; } div.cp-form-poll-answer { - color: @cp_form_poll-yes-color; + color: @cp_form-poll-yes-color; } } diff --git a/www/form/inner.js b/www/form/inner.js index cea932e3f..216d6ac63 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -80,14 +80,23 @@ define([ 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 @@ -161,6 +170,109 @@ define([ 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', [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 < 100) { 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'), @@ -415,10 +527,6 @@ define([ if ($container.find('input').length >= MAX_OPTIONS) { $add.hide(); } if ($(containerItems).find('input').length >= MAX_ITEMS) { $addItem.hide(); } - // Cancel changes - var cancelBlock = h('button.btn.btn-secondary', Messages.cancel); - $(cancelBlock).click(function () { cb(); }); - // Set cursor getter (to handle remote changes to the form) setCursorGetter(function () { var values = []; @@ -471,14 +579,7 @@ define([ }; }); - // 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 getSaveRes = function () { // Get values var values = []; var duplicates = false; @@ -538,8 +639,10 @@ define([ res.type = typeSelect.getValue(); } - cb(res); - }); + return res; + }; + + var saveAndCancel = saveAndCancelOptions(getSaveRes, cb); return [ type, @@ -547,7 +650,7 @@ define([ calendarView, h('div.cp-form-edit-options-block', [containerItems, container]), addMultiple, - h('div', [cancelBlock, saveBlock]) + saveAndCancel ]; }; @@ -767,16 +870,33 @@ define([ }; var TYPES = { input: { + defaultOpts: { + type: 'text' + }, get: function (opts, a, n, evOnChange) { - var tag = h('input'); + 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 () { return $tag.val(); }, + 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(''); } }; }, @@ -795,6 +915,46 @@ define([ }, icon: h('i.fa.fa-font') }, + 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.fa.fa-font') + }, radio: { defaultOpts: { values: [1,2].map(function (i) { @@ -1285,6 +1445,116 @@ define([ }, icon: h('i.cptools.cptools-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.fa.fa-list-ul') + }, }; var renderResults = function (content, answers) { @@ -1848,7 +2118,7 @@ define([ if (editable) { Sortable.create($container[0], { direction: "vertical", - filter: "input, button, .CodeMirror", + filter: "input, button, .CodeMirror, .cp-form-type-sort", preventOnFilter: false, draggable: ".cp-form-block", forceFallback: true, From aaff13795e217c08ad9e0fd98a82f913087a20eb Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 4 Jun 2021 17:17:12 +0200 Subject: [PATCH 43/44] Fix forms UI issues --- .../src/less2/include/colortheme-dark.less | 2 +- www/form/app-form.less | 27 ++++++++++++++++++- www/form/inner.js | 7 ++--- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/customize.dist/src/less2/include/colortheme-dark.less b/customize.dist/src/less2/include/colortheme-dark.less index 5c3091b6b..ba80b2d64 100644 --- a/customize.dist/src/less2/include/colortheme-dark.less +++ b/customize.dist/src/less2/include/colortheme-dark.less @@ -435,5 +435,5 @@ @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-poll-yes-color: @cryptpad_color_green; @cp_form-invalid: @cryptpad_color_red; diff --git a/www/form/app-form.less b/www/form/app-form.less index f9e51c470..abe5dafd8 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -49,9 +49,22 @@ flex: 1; justify-content: center; min-width: 300px; - flex-wrap: wrap; + //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) { @@ -110,6 +123,12 @@ 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; @@ -148,6 +167,12 @@ 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; diff --git a/www/form/inner.js b/www/form/inner.js index d21a3598e..01b4a3055 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -102,6 +102,7 @@ define([ 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 @@ -207,7 +208,7 @@ define([ ]); getLengthVal = function () { var val = Number($(lengthInput).val()) || 1000; - if (val < 100) { val = 1; } + if (val < 1) { val = 1; } if (val > 5000) { val = 5000; } return val; }; @@ -953,7 +954,7 @@ define([ return h('div.cp-form-results-type-text', results); }, - icon: h('i.fa.fa-font') + icon: h('i.cptools.cptools-form-paragraph') }, radio: { defaultOpts: { @@ -1553,7 +1554,7 @@ define([ return h('div.cp-form-results-type-radio', results); }, - icon: h('i.fa.fa-list-ul') + icon: h('i.cptools.cptools-form-list-ordered') }, }; From 1a1ed33db407c9774778294c1f59b5f60647c4f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Benqu=C3=A9?= Date: Mon, 7 Jun 2021 11:06:22 +0100 Subject: [PATCH 44/44] Add spacing to block edit UI --- www/form/app-form.less | 25 ++++++++++++++++++++++++- www/form/inner.js | 6 +++--- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index abe5dafd8..278e1d1db 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -253,6 +253,7 @@ } } .cp-form-edit-buttons-container { + margin-top: 20px; display: flex; justify-content: space-between; } @@ -297,11 +298,26 @@ line-height: 31px; } } - &.editable { cursor: grab; } + &.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; @@ -326,6 +342,10 @@ } } .cp-form-edit-block { + + button.btn-secondary { + margin-left: 30px; + } .cp-form-handle { display: flex; align-items: center; @@ -337,6 +357,7 @@ } } .cp-form-edit-block-input { + margin-bottom: 5px; // XXX DB margin bug &.sortable-ghost { visibility: hidden; } &.sortable-drag { opacity: 0.9 !important; } display: flex; @@ -344,6 +365,8 @@ input { flex: 1; min-width: 100px; + border-color: @cryptpad_text_col; + border-right: 0px; } button { i { margin: 0 !important; } diff --git a/www/form/inner.js b/www/form/inner.js index 01b4a3055..fcf1cc349 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -187,7 +187,7 @@ define([ cb(getRes()); }); - return h('div', [cancelBlock, saveBlock]); + 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)) { @@ -371,7 +371,7 @@ define([ if (cursor && cursor.el === val && !cursor.item) { setCursor(); } } - var del = h('button.btn.btn-danger', h('i.fa.fa-times')); + 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'), @@ -845,7 +845,7 @@ define([ return [ block, - h('div', [cancelBlock, saveBlock]) + h('div.cp-form-edit-save', [cancelBlock, saveBlock]) ]; }, getCursor: function () { return cursorGetter(); },