diff --git a/www/form/app-form.less b/www/form/app-form.less index a0d0bdd65..ef6d9b839 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -127,6 +127,10 @@ & > div:not(:last-child) { margin-bottom: 20px; } + .cp-forms-results-participant { + display: flex; + flex-flow: column; + } } div.cp-form-filler-container { width: 300px; @@ -313,6 +317,12 @@ margin-bottom: 20px; } + .cp-form-disabled { + .cp-form-poll-choice, .cp-form-type-sort { + cursor: not-allowed !important; + } + } + .cp-form-preview { color: @cp_sidebar-hint; margin-bottom: 10px; @@ -502,6 +512,12 @@ } } } + div.cp-form-creator-answered { + display: flex; + align-items: center; + justify-content: center; + flex: 1; + } div.cp-form-creator-results { display: flex; flex-flow: column; diff --git a/www/form/inner.js b/www/form/inner.js index 2f717cb05..f1cb5adfd 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -68,6 +68,7 @@ define([ ) { var APP = window.APP = { + blocks: {} }; var is24h = UIElements.is24h(); @@ -1009,6 +1010,10 @@ define([ return $tag.val(); }, setValue: function (val) { $tag.val(val); }, + setEditable: function (state) { + if (state) { $tag.removeAttr('disabled'); } + else { $tag.attr('disabled', 'disabled'); } + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editTextOptions(v, setCursorGetter, cb, tmp); @@ -1091,6 +1096,10 @@ define([ $text.val(val); updateChar(); }, + setEditable: function (state) { + if (state) { $(tag).removeAttr('disabled'); } + else { $(tag).attr('disabled', 'disabled'); } + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editTextOptions(v, setCursorGetter, cb, tmp); @@ -1151,6 +1160,10 @@ define([ return res; }, reset: function () { $(tag).find('input').removeAttr('checked'); }, + setEditable: function (state) { + if (state) { $(tag).find('input').removeAttr('disabled'); } + else { $(tag).find('input').attr('disabled', 'disabled'); } + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editOptions(v, setCursorGetter, cb, tmp); @@ -1222,7 +1235,8 @@ 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 () { + var $tag = $(tag); + $tag.find('input[type="radio"]').on('change', function () { evOnChange.fire(); }); return { @@ -1242,6 +1256,10 @@ define([ return res; }, reset: function () { $(tag).find('input').removeAttr('checked'); }, + setEditable: function (state) { + if (state) { $tag.find('input').removeAttr('disabled'); } + else { $tag.find('input').attr('disabled', 'disabled'); } + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editOptions(v, setCursorGetter, cb, tmp); @@ -1350,13 +1368,16 @@ define([ h('div.radio-group.cp-form-type-checkbox', els) ]); var $tag = $(tag); - $tag.find('input').on('change', function () { + var checkDisabled = 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'); } + }; + $tag.find('input').on('change', function () { + checkDisabled(); evOnChange.fire(); }); var cursorGetter; @@ -1374,6 +1395,10 @@ define([ return res; }, reset: function () { $(tag).find('input').removeAttr('checked'); }, + setEditable: function (state) { + if (state) { checkDisabled(); } + else { $tag.find('input').attr('disabled', 'disabled'); } + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editOptions(v, setCursorGetter, cb, tmp); @@ -1388,10 +1413,7 @@ define([ $el.prop('checked', true); } }); - var selected = $tag.find('input:checked').length; - if (selected >= opts.max) { - $tag.find('input:not(:checked)').attr('disabled', 'disabled'); - } + checkDisabled(); } }; @@ -1448,14 +1470,18 @@ define([ if (!opts.max) { opts.max = TYPES.multicheck.defaultOpts.max; } + var checkDisabled = function (l) { + 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'); + } + }; + 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'); - } + checkDisabled(l); evOnChange.fire(); }); }); @@ -1484,6 +1510,10 @@ define([ return res; }, reset: function () { $(tag).find('input').removeAttr('checked'); }, + setEditable: function (state) { + if (state) { lines.forEach(checkDisabled); } + else { $(tag).find('input').attr('disabled', 'disabled'); } + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editOptions(v, setCursorGetter, cb, tmp); @@ -1498,6 +1528,7 @@ define([ $(el).prop('checked', true); }); }); + lines.forEach(checkDisabled); } }; @@ -1633,10 +1664,6 @@ define([ } } }); - - $(tag).find('input[type="radio"]').on('change', function () { - evOnChange.fire(); - }); return { tag: tag, getValue: function () { @@ -1653,6 +1680,10 @@ define([ sortable.sort(toSort); reorder(true); }, + setEditable: function (state) { + sortable.options.disabled = !state; + $(tag).toggleClass('cp-form-disabled', !state); + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editOptions(v, setCursorGetter, cb, tmp); @@ -1704,6 +1735,7 @@ define([ var lines = makePollTable(answers, opts, false); + var disabled = false; // Add form var addLine = opts.values.map(function (data) { var cell = h('div.cp-poll-cell.cp-form-poll-choice', [ @@ -1716,6 +1748,7 @@ define([ var val = 0; $c.attr('data-value', val); $c.click(function () { + if (disabled) { return; } val = (val+1)%3; $c.attr('data-value', val); evOnChange.fire(); @@ -1768,6 +1801,10 @@ define([ reset: function () { $tag.find('.cp-form-poll-choice').attr('data-value', 0); }, + setEditable: function (state) { + disabled = !state; + $tag.toggleClass('cp-form-disabled', disabled); + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editOptions(v, setCursorGetter, cb, tmp); @@ -2092,6 +2129,43 @@ define([ framework._.toolbar.$bottomL.append($res); }; + Messages.form_alreadyAnswered = "You've responded to this form on {0}"; // XXX + Messages.form_editAnswer = "Edit my responses"; // XXX + Messages.form_viewAnswer = "View my responses"; // XXX + var showAnsweredPage = function (framework, content, answers) { + var $formContainer = $('div.cp-form-creator-content').hide(); + var $container = $('div.cp-form-creator-answered').empty().css('display', ''); + + var viewOnly = content.answers.cantEdit; + var action = h('button.btn.btn-primary', [ + viewOnly ? h('i.fa.fa-bar-chart') : h('i.fa.fa-pencil'), + h('span', viewOnly ? Messages.form_viewAnswer : Messages.form_editAnswer) + ]); + + $(action).click(function () { + $formContainer.css('display', ''); + $container.hide(); + if (viewOnly) { + $formContainer.find('.cp-form-send-container .cp-open').hide(); + Object.keys(APP.blocks).forEach(function (uid) { + var b = APP.blocks[uid]; + if (!b.setEditable) { return; } + b.setEditable(false); + }); + } + }); + + if (answers._time) { APP.lastAnswerTime = answers._time; } + + var title = framework._.title.title || framework._.title.defaultTitle; + $container.append(h('div.cp-form-submit-success', [ + h('h3.cp-form-view-title', title), + h('div.alert.alert-info', Messages._getKey('form_alreadyAnswered', [ + new Date(APP.lastAnswerTime).toLocaleString()])), + action + ])); + }; + var getFormResults = function () { if (!Array.isArray(APP.formBlocks)) { return; } var results = {}; @@ -2180,8 +2254,9 @@ define([ addResultsButton(framework, content); } $send.removeAttr('disabled'); - UI.alert(Messages.form_sent); + //UI.alert(Messages.form_sent); // XXX not needed anymore? $send.text(Messages.form_update); + showAnsweredPage(framework, content, { '_time': +new Date() }); }); }); @@ -2378,7 +2453,7 @@ define([ name = user.name; } - var data = model.get(block.opts, _answers, name, evOnChange); + var data = APP.blocks[uid] = model.get(block.opts, _answers, name, evOnChange); if (!data) { return; } data.uid = uid; if (answers && answers[uid] && data.setValue) { data.setValue(answers[uid]); } @@ -2502,7 +2577,7 @@ define([ $(editButtons).show(); UI.log(Messages.saved); _answers = getBlockAnswers(APP.answers, uid); - data = model.get(newOpts, _answers, null, evOnChange); + data = APP.blocks[uid] = model.get(newOpts, _answers, null, evOnChange); if (!data) { data = {}; } $oldTag.before(data.tag).remove(); }); @@ -2565,7 +2640,7 @@ define([ framework.localChange(); var $oldTag = $(data.tag); framework._.cpNfInner.chainpad.onSettle(function () { - data = model.get(block.opts, _answers, null, evOnChange); + data = APP.blocks[uid] = model.get(block.opts, _answers, null, evOnChange); $oldTag.before(data.tag).remove(); }); }); @@ -2676,12 +2751,11 @@ define([ } // If the form is already submitted, show an info message - Messages.form_alreadyAnswered = "You've submitted answers to this form on {0}"; // XXX if (answers) { + showAnsweredPage(framework, content, answers); $container.prepend(h('div.alert.alert-info', Messages._getKey('form_alreadyAnswered', [ - new Date(answers._time).toLocaleString()]))); - // XXX make the page read-only? + new Date(answers._time || APP.lastAnswerTime).toLocaleString()]))); } // In view mode, add "Submit" and "reset" buttons @@ -2705,8 +2779,8 @@ define([ if (!answers) { $container.find('.cp-reset-button').attr('disabled', 'disabled'); - } - }; + } +}; var getTempFields = function () { if (!Array.isArray(APP.formBlocks)) { return; } @@ -2916,6 +2990,38 @@ define([ }; refreshPrivacy(); + // Allow responses edition + Messages.form_editable = "Allow users to edit their responses"; // XXX + var editableContainer = h('div.cp-form-editable-container'); + var $editable = $(editableContainer); + var refreshEditable = function () { + $editable.empty(); + var editable = !content.answers.cantEdit; + var radioOn = UI.createRadio('cp-form-editable', 'cp-form-editable-on', + Messages.form_anonymous_on, Boolean(editable), { + input: { value: 1 }, + mark: { tabindex:1 } + }); + var radioOff = UI.createRadio('cp-form-editable', 'cp-form-editable-off', + Messages.form_anonymous_off, !editable, { + input: { value: 0 }, + mark: { tabindex:1 } + }); + var radioContainer = h('div.cp-form-editable-radio', [radioOn, radioOff]); + $(radioContainer).find('input[type="radio"]').on('change', function() { + var val = $('input:radio[name="cp-form-editable"]:checked').val(); + val = Number(val) || 0; + content.answers.cantEdit = !val; + framework.localChange(); + framework._.cpNfInner.chainpad.onSettle(function () { + UI.log(Messages.saved); + }); + }); + $editable.append(h('div.cp-form-status', Messages.form_editable)); + $editable.append(h('div.cp-form-actions', radioContainer)); + }; + refreshEditable(); + // End date / Closed state var endDateContainer = h('div.cp-form-status-container'); var $endDate = $(endDateContainer); @@ -2979,6 +3085,7 @@ define([ evOnChange.reg(refreshPublic); evOnChange.reg(refreshPrivacy); + evOnChange.reg(refreshEditable); evOnChange.reg(refreshEndDate); //evOnChange.reg(refreshResponse); @@ -2986,6 +3093,7 @@ define([ preview, endDateContainer, privacyContainer, + editableContainer, resultsType, responseMsg ]; @@ -3027,10 +3135,14 @@ define([ var contentContainer = h('div.cp-form-creator-content'); var resultsContainer = h('div.cp-form-creator-results'); + var answeredContainer = h('div.cp-form-creator-answered', { + style: 'display: none;' + }); var div = h('div.cp-form-creator-container', [ controlContainer, contentContainer, resultsContainer, + answeredContainer, fillerContainer ]); return div; diff --git a/www/form/main.js b/www/form/main.js index 4c0831fe9..58030f35c 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -266,6 +266,7 @@ define([ if (obj && obj.error) { return void cb(obj); } var messages = obj.messages; if (!messages.length) { return void cb(); } + if (obj.lastKnownHash !== answer.hash) { return void cb(); } var res = Utils.Crypto.Mailbox.openOwnSecretLetter(messages[0].msg, { validateKey: data.validateKey, ephemeral_private: Nacl.util.decodeBase64(answer.curvePrivate),