From 1605a841e152065dafda0b82063ddbc2d0d824f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Benqu=C3=A9?= Date: Wed, 25 Aug 2021 15:52:54 +0100 Subject: [PATCH 01/21] Show required setting on one line --- www/form/app-form.less | 10 ++++++++++ www/form/inner.js | 11 ++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index e22f3030d..b447dcc59 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -398,6 +398,16 @@ color: @cryptpad_color_link; } } + .cp-form-required-radio { + flex-direction: row; + display: flex; + span { + margin-right: 10px; + &.cp-radio-mark { + margin-right: 5px; + } + } + } .cp-form-page-break-edit { font-size: 20px; text-align: center; diff --git a/www/form/inner.js b/www/form/inner.js index 1a776facc..c98a9f830 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -2603,8 +2603,9 @@ define([ Messages.form_preview = "Preview:"; // XXX var previewDiv = h('div.cp-form-preview', Messages.form_preview); - Messages.form_required_on = "Required answer"; - Messages.form_required_off = "Optional answer"; + Messages.form_required_answer = "Answer: " + Messages.form_required_on = "required"; + Messages.form_required_off = "optional"; // Required radio displayed only for types that have an "isEmpty" function var requiredDiv; if (APP.isEditor && !isStatic && data.isEmpty) { @@ -2618,7 +2619,11 @@ define([ Messages.form_required_off, !isRequired, { input: { value: 0 }, }); - var radioContainer = h('div.cp-form-required-radio', [radioOn, radioOff]); + var radioContainer = h('div.cp-form-required-radio', [ + h('span', Messages.form_required_answer), + radioOff, + radioOn + ]); requiredDiv = h('div.cp-form-required', [ radioContainer ]); From 59b407c913a347a945c2c1c48503dc7174309bcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Benqu=C3=A9?= Date: Wed, 25 Aug 2021 17:40:45 +0100 Subject: [PATCH 02/21] Fix logo and alert in participant view --- www/form/app-form.less | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index b447dcc59..e630c8ca1 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -16,6 +16,10 @@ color: @cryptpad_text_col; background-color: @cp_app-bg; + div.alert.cp-burn-after-reading { + margin: 10px !important; + } + #cp-app-form-editor { flex: 1; display: flex; @@ -84,17 +88,16 @@ div.cp-form-view-logo { align-items: center; justify-content: center; - max-height: 140px; - font-size: 50px; + font-size: 40px; display: flex; font-family: "IBM Plex Mono"; .tools_unselectable(); - color: @cp_sidebar-hint; - padding: 20px; + color: @cryptpad_color_grey_500; + padding: 20px 20px 150px 20px; cursor: pointer; img { - max-height: 100%; - margin-right: 20px; + max-height: 60px; + margin: 0px 20px; } } From 20b0b4b9b21b70811db9df14e89af26b84582721 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 26 Aug 2021 11:21:00 +0200 Subject: [PATCH 03/21] Fix type error in forms --- www/form/inner.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/www/form/inner.js b/www/form/inner.js index c98a9f830..f440dd49a 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1734,7 +1734,8 @@ define([ }, getCursor: function () { return cursorGetter(); }, setValue: function (val) { - var toSort = (val || []).map(function (val) { + if (!Array.isArray(val)) { val = []; } + var toSort = val.map(function (val) { return invMap[val]; }); sortable.sort(toSort); From 20e9b2ca878c40a09c4e597ca9830387c3a55d4a Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 26 Aug 2021 11:25:51 +0200 Subject: [PATCH 04/21] Make the form readonly when unregistered users can't send responses --- www/form/inner.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/www/form/inner.js b/www/form/inner.js index f440dd49a..11d4b665f 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -2949,6 +2949,13 @@ define([ if (infoTxt) { $container.prepend(h('div.alert.alert-info', infoTxt)); } + + if (!loggedIn && !content.answers.anonymous) { + APP.formBlocks.forEach(function (b) { + if (!b.setEditable) { return; } + b.setEditable(false); + }); + } } // Embed mode is enforced so we add the title at the top and a CryptPad logo From 8f1e8f893420d0582d73427afc14aac4c8c6f203 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 26 Aug 2021 11:36:53 +0200 Subject: [PATCH 05/21] Move response message in forms --- www/form/inner.js | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 11d4b665f..855b6a1c8 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1992,12 +1992,6 @@ define([ return; } - if (content.answers.msg) { - var description = h('div.cp-form-creator-results-description#cp-form-response-msg'); - var $desc = $(description).appendTo($container); - DiffMd.apply(DiffMd.render(content.answers.msg), $desc, APP.common); - } - var heading = h('h2#cp-title', Messages._getKey('form_totalResponses', [answerCount])); $(heading).appendTo($container); var timeline = h('div.cp-form-creator-results-timeline'); @@ -2206,12 +2200,40 @@ define([ if (answers._time) { APP.lastAnswerTime = answers._time; } + // If responses are public, show button to view them + var responses; + if (content.answers.privateKey) { + responses = h('button.btn.btn-default', Messages.form_results); + $(responses).click(function () { + 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; } + $('body').addClass('cp-app-form-results'); + renderResults(content, answers); + $container.hide(); + }); + }); + } + + var description = h('div.cp-form-creator-results-description#cp-form-response-msg'); + if (content.answers.msg) { + var $desc = $(description); + DiffMd.apply(DiffMd.render(content.answers.msg), $desc, APP.common); + } + + var actions = h('div.cp-form-submit-actions', [ + action, + responses || undefined + ]); + var title = framework._.title.title || framework._.title.defaultTitle; $container.append(h('div.cp-form-submit-success', [ h('h3.cp-form-view-title', title), + description, h('div.alert.alert-info', Messages._getKey('form_alreadyAnswered', [ new Date(APP.lastAnswerTime).toLocaleString()])), - action + actions ])); }; From ad50be03082ada21545319140eb387a25a3446f2 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 26 Aug 2021 11:44:06 +0200 Subject: [PATCH 06/21] Improve focus and shortcuts when editing form options --- www/form/inner.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 855b6a1c8..f832caf7b 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -322,6 +322,19 @@ define([ $maxInput.val(Math.min(inputs, currentMax)); } }); + + if (!v.type || v.type === "text") { + $(input).keyup(function (e) { + if (e.which === 13) { + if (isItem && $addItem && $addItem.is(':visible')) { $addItem.click(); } + if (!isItem && $add && $add.is(':visible')) { $add.click(); } + } + if (e.which === 27 && !$(input).val()) { + $(del).click(); + } + }); + } + return el; }; var inputs = v.values.map(function (val) { return getOption(val, isDefaultOpts, false); }); @@ -448,7 +461,9 @@ define([ // "Add option" button handler $add = $(add).click(function () { var txt = v.type ? '' : Messages.form_newOption; - $add.before(getOption(txt, true, false)); + var el = getOption(txt, true, false); + $add.before(el); + $(el).find('input').focus(); var l = $container.find('input').length; $(maxInput).attr('max', l); if (l >= MAX_OPTIONS) { $add.hide(); } @@ -456,7 +471,9 @@ define([ // If multiline block, handle "Add item" button $addItem = $(addItem).click(function () { - $addItem.before(getOption(Messages.form_newItem, true, true, Util.uid())); + var el = getOption(Messages.form_newItem, true, true, Util.uid()); + $addItem.before(el); + $(el).find('input').focus(); if ($(containerItems).find('input').length >= MAX_ITEMS) { $addItem.hide(); } }); if ($container.find('input').length >= MAX_OPTIONS) { $add.hide(); } From 9d195ed7be24803cbf4adb6fb8e680a1b3dae6ca Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 26 Aug 2021 14:35:22 +0200 Subject: [PATCH 07/21] Autosave in forms --- www/form/inner.js | 161 +++++++++++++++++++++++++--------------------- 1 file changed, 89 insertions(+), 72 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index f832caf7b..0b7db96b7 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -85,28 +85,16 @@ define([ var MAX_OPTIONS = 15; var MAX_ITEMS = 10; - 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()); - }); + var saveAndCancelOptions = function (cb) { + Messages.form_preview_button = "Preview"; // XXX + var cancelBlock = h('button.btn.btn-secondary.cp-form-preview-button', + Messages.form_preview_button); + $(cancelBlock).click(function () { cb(undefined, true); }); - return h('div.cp-form-edit-save', [cancelBlock, saveBlock]); + return cancelBlock; }; - var editTextOptions = function (opts, setCursorGetter, cb, tmp) { - if (tmp && tmp.content && Sortify(opts) === Sortify(tmp.old)) { - opts = tmp.content; - } + var editTextOptions = function (opts, setCursorGetter, cb) { + var evOnSave = Util.mkEvent(); var maxLength, getLengthVal; if (opts.maxLength) { @@ -129,8 +117,8 @@ define([ var $l = $(lengthInput).on('input', Util.throttle(function () { $l.val(getLengthVal()); + evOnSave.fire(); }, 500)); - } var type, typeSelect; @@ -163,16 +151,11 @@ define([ h('span', Messages.form_textType), typeSelect[0] ]); + typeSelect.onChange.reg(evOnSave.fire()); } setCursorGetter(function () { - return { - old: (tmp && tmp.old) || opts, - content: { - maxLength: getLengthVal ? getLengthVal() : undefined, - type: typeSelect ? typeSelect.getValue() : undefined - } - }; + return {}; }); var getSaveRes = function () { @@ -181,7 +164,14 @@ define([ type: typeSelect ? typeSelect.getValue() : undefined }; }; - var saveAndCancel = saveAndCancelOptions(getSaveRes, cb); + + evOnSave.reg(function () { + var res = getSaveRes(); + if (!res) { return; } + cb(res); + }); + + var saveAndCancel = saveAndCancelOptions(cb); return [ maxLength, @@ -190,6 +180,8 @@ define([ ]; }; var editOptions = function (v, isDefaultOpts, setCursorGetter, cb, tmp) { + var evOnSave = Util.mkEvent(); + var add = h('button.btn.btn-secondary', [ h('i.fa.fa-plus'), h('span', Messages.form_add_option) @@ -200,11 +192,17 @@ define([ ]); var cursor; + if (tmp && tmp.cursor) { + cursor = tmp.cursor; + } + /* if (tmp && tmp.content && Sortify(v) === Sortify(tmp.old)) { v = tmp.content; cursor = tmp.cursor; } + */ + // Checkbox: max options var maxOptions, maxInput; if (typeof(v.max) === "number") { maxInput = h('input', { @@ -217,8 +215,12 @@ define([ h('span', Messages.form_editMax), maxInput ]); + $(maxInput).on('input', function () { + setTimeout(evOnSave.fire); + }); } + // Poll: type (text/day/time) var type, typeSelect; if (v.type) { // Messages.form_poll_text.form_poll_day.form_poll_time @@ -321,6 +323,8 @@ define([ var currentMax = Number($maxInput.val()); $maxInput.val(Math.min(inputs, currentMax)); } + + evOnSave.fire(); }); if (!v.type || v.type === "text") { @@ -335,6 +339,10 @@ define([ }); } + $(input).on('input', function () { + evOnSave.fire(); + }); + return el; }; var inputs = v.values.map(function (val) { return getOption(val, isDefaultOpts, false); }); @@ -367,7 +375,8 @@ define([ // Calendar... var calendarView; - if (v.type) { + if (v.type) { // Polls + // Calendar inline for "day" type var calendarInput = h('input'); calendarView = h('div', calendarInput); var calendarDefault = v.type === "day" ? v.values.map(function (time) { @@ -379,12 +388,13 @@ define([ mode: 'multiple', inline: true, defaultDate: calendarDefault, - appendTo: calendarView + appendTo: calendarView, + onChange: function () { + evOnSave.fire(); + } }); - } - // Calendar time - if (v.type) { + // Calendar popup for "time" var multipleInput = h('input', {placeholder: Messages.form_addMultipleHint}); var multipleClearButton = h('button.btn', Messages.form_clear); var addMultipleButton = h('button.btn', [ @@ -416,6 +426,7 @@ define([ } }); multiplePickr.clear(); + evOnSave.fire(); }); } @@ -444,6 +455,7 @@ define([ typeSelect.onChange.reg(function (prettyVal, val) { v.type = val; refreshView(); + setTimeout(evOnSave.fire); if (val !== "text") { $container.find('.cp-form-edit-block-input').remove(); $(add).click(); @@ -555,9 +567,10 @@ define([ else { duplicates = true; } }); } - values = values.filter(Boolean); // Block empty or undeinfed options + values = values.filter(Boolean); // Block empty or undefined options if (!values.length) { - return void UI.warn(Messages.error); + return; + //return void UI.warn(Messages.error); // XXX not anymore with autosave? } var res = { values: values }; @@ -575,12 +588,13 @@ define([ } else { duplicates = true; } }); + items = items.filter(Boolean); res.items = items; } // Show duplicates warning if (duplicates) { - UI.warn(Messages.form_duplicates); + UI.warn(Messages.form_duplicates); // XXX } // If checkboxes, get the maximum number of values the users can select @@ -597,7 +611,13 @@ define([ return res; }; - var saveAndCancel = saveAndCancelOptions(getSaveRes, cb); + evOnSave.reg(function () { + var res = getSaveRes(); + if (!res) { return; } + cb(res); + }); + + var saveAndCancel = saveAndCancelOptions(cb); return [ type, @@ -892,8 +912,7 @@ define([ var text = opts.text; var cursor; - if (tmp && tmp.content && tmp.old.text === text) { - text = tmp.content.text; + if (tmp && tmp.cursor) { cursor = tmp.cursor; } @@ -922,23 +941,15 @@ define([ cm.configureTheme(APP.common, function () {}); } // Cancel changes - var cancelBlock = h('button.btn.btn-secondary', Messages.cancel); - $(cancelBlock).click(function () { - cb(); - }); - // Save changes - var saveBlock = h('button.btn.btn-primary', [ - h('i.fa.fa-floppy-o'), - h('span', Messages.settings_save) - ]); + var cancelBlock = saveAndCancelOptions(cb); var getContent = function () { return { text: editor.getValue() }; }; - $(saveBlock).click(function () { - $(saveBlock).attr('disabled', 'disabled'); + + editor.on('change', function () { cb(getContent()); }); @@ -958,7 +969,7 @@ define([ return [ block, - h('div.cp-form-edit-save', [cancelBlock, saveBlock]) + cancelBlock ]; }, getCursor: function () { return cursorGetter(); }, @@ -2643,7 +2654,7 @@ define([ Messages.form_preview = "Preview:"; // XXX var previewDiv = h('div.cp-form-preview', Messages.form_preview); - Messages.form_required_answer = "Answer: " + Messages.form_required_answer = "Answer: "; Messages.form_required_on = "required"; Messages.form_required_off = "optional"; // Required radio displayed only for types that have an "isEmpty" function @@ -2759,30 +2770,29 @@ define([ h('span', Messages.form_editBlock) ]); editContainer = h('div'); - var onSave = function (newOpts) { - data.editing = false; - if (!newOpts) { // Cancel edit + var onSave = function (newOpts, close) { + if (close) { // Cancel edit + data.editing = false; $(editContainer).empty(); - $(editButtons).show(); - $(data.tag).show(); + var $oldTag = $(data.tag); + $(edit).show(); $(previewDiv).show(); $(requiredDiv).show(); + + $(editButtons).find('.cp-form-preview-button').remove(); + + _answers = getBlockAnswers(APP.answers, uid); + data = model.get(block.opts, _answers, null, evOnChange); + if (!data) { data = {}; } + $oldTag.before(data.tag).remove(); + return; + } + if (!newOpts) { + // invalid options, nothing to save return; } - $(editContainer).empty(); block.opts = newOpts; framework.localChange(); - var $oldTag = $(data.tag); - framework._.cpNfInner.chainpad.onSettle(function () { - $(editButtons).show(); - $(previewDiv).show(); - $(requiredDiv).show(); - UI.log(Messages.saved); - _answers = getBlockAnswers(APP.answers, uid); - data = model.get(newOpts, _answers, null, evOnChange); - if (!data) { data = {}; } - $oldTag.before(data.tag).remove(); - }); }; var onEdit = function (tmp) { data.editing = true; @@ -2790,7 +2800,10 @@ define([ $(previewDiv).hide(); $(data.tag).hide(); $(editContainer).append(data.edit(onSave, tmp, framework)); - $(editButtons).hide(); + + $(editContainer).find('.cp-form-preview-button').prependTo(editButtons); + + $(edit).hide(); }; $(edit).click(function () { onEdit(); @@ -2876,9 +2889,9 @@ define([ APP.isEditor && !isStatic ? requiredDiv : undefined, APP.isEditor && !isStatic ? previewDiv : undefined, data.tag, + editContainer, editButtons ]), - editContainer ])); }); @@ -3631,7 +3644,11 @@ define([ var answers, temp; if (!APP.isEditor) { answers = getFormResults(); } else { temp = getTempFields(); } + + var $main = $('.cp-form-creator-container'); + var sTop = $main.scrollTop(); updateForm(framework, content, APP.isEditor, answers, temp); + $main.scrollTop(sTop); }); framework.setContentGetter(function () { From ccc5e92659a092607db58c7a747e076557699850 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 26 Aug 2021 14:47:46 +0200 Subject: [PATCH 08/21] Keep 'required' radio in form edit mode --- www/form/inner.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 0b7db96b7..ecc4db7b5 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -2777,7 +2777,6 @@ define([ var $oldTag = $(data.tag); $(edit).show(); $(previewDiv).show(); - $(requiredDiv).show(); $(editButtons).find('.cp-form-preview-button').remove(); @@ -2796,7 +2795,6 @@ define([ }; var onEdit = function (tmp) { data.editing = true; - $(requiredDiv).hide(); $(previewDiv).hide(); $(data.tag).hide(); $(editContainer).append(data.edit(onSave, tmp, framework)); From fe9d17cd17c94c2dc20e0dcf4bbae6d4b2268e51 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 26 Aug 2021 14:57:18 +0200 Subject: [PATCH 09/21] Hide 'required' radio in preview mode --- www/form/inner.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/www/form/inner.js b/www/form/inner.js index ecc4db7b5..3c3aca31b 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -2777,6 +2777,7 @@ define([ var $oldTag = $(data.tag); $(edit).show(); $(previewDiv).show(); + $(requiredDiv).hide(); $(editButtons).find('.cp-form-preview-button').remove(); @@ -2795,6 +2796,7 @@ define([ }; var onEdit = function (tmp) { data.editing = true; + $(requiredDiv).show(); $(previewDiv).hide(); $(data.tag).hide(); $(editContainer).append(data.edit(onSave, tmp, framework)); @@ -2806,6 +2808,7 @@ define([ $(edit).click(function () { onEdit(); }); + $(requiredDiv).hide(); // If we were editing this field, recover our unsaved changes if (temp && temp[uid]) { From ae95f06bdba97266267ddfab5a68dcd7fc773d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Benqu=C3=A9?= Date: Thu, 26 Aug 2021 14:11:40 +0100 Subject: [PATCH 10/21] Style tweaks for question edit and preview --- www/form/app-form.less | 3 +-- www/form/inner.js | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index e630c8ca1..d763ed204 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -344,8 +344,6 @@ color: @cp_sidebar-hint; margin-bottom: 10px; padding: 0; - text-align: center; - font-weight: bold; } .cp-form-block-drag-handle { @@ -404,6 +402,7 @@ .cp-form-required-radio { flex-direction: row; display: flex; + margin-bottom: 20px; span { margin-right: 10px; &.cp-radio-mark { diff --git a/www/form/inner.js b/www/form/inner.js index 3c3aca31b..b1904bb8e 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -87,8 +87,10 @@ define([ var saveAndCancelOptions = function (cb) { Messages.form_preview_button = "Preview"; // XXX - var cancelBlock = h('button.btn.btn-secondary.cp-form-preview-button', - Messages.form_preview_button); + var cancelBlock = h('button.btn.btn-default.cp-form-preview-button',[ + h('i.fa.fa-eye'), + Messages.form_preview_button + ]); $(cancelBlock).click(function () { cb(undefined, true); }); return cancelBlock; From 663e7fbf1d5108f4fd86272abe64bfe7b32c74c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Benqu=C3=A9?= Date: Thu, 26 Aug 2021 14:24:09 +0100 Subject: [PATCH 11/21] Adjust "response message" button and dialog --- www/form/inner.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index f832caf7b..1a1ab6759 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -3129,9 +3129,9 @@ define([ var $responseMsg = $(responseMsg); var refreshResponse = function () { $responseMsg.empty(); - Messages.form_updateMsg = "Update response message"; // XXX 4.11.0 - Messages.form_addMsg = "Add response message"; // XXX 4.11.0 - Messages.form_responseMsg = "Add a message that will be displayed in the response page."; // XXX 4.11.0 + Messages.form_updateMsg = "Update submit message"; // XXX 4.11.0 + Messages.form_addMsg = "Add submit message"; // XXX 4.11.0 + Messages.form_responseMsg = "Add a message that will be displayed after participants submit the form."; // XXX 4.11.0 var text = content.answers.msg ? Messages.form_updateMsg : Messages.form_addMsg; var btn = h('button.btn.btn-secondary', text); $(btn).click(function () { @@ -3167,6 +3167,12 @@ define([ }); var buttons = [{ + className: 'cancel', + name: Messages.cancel, + onClick: function () {}, + keys: [27] + }, + { className: 'primary', name: Messages.settings_save, onClick: function () { @@ -3179,11 +3185,6 @@ define([ }); }, //keys: [] - }, { - className: 'cancel', - name: Messages.cancel, - onClick: function () {}, - keys: [27] }]; APP.responseModal = UI.dialog.customModal(div, { buttons: buttons }); } else { From 9f5cf8daf64947de74b31330b53f3ed159182a29 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 26 Aug 2021 15:40:40 +0200 Subject: [PATCH 12/21] Fix cursor issues in forms realtime edition --- www/form/inner.js | 103 +++++++++++++++++++++------------------------- 1 file changed, 48 insertions(+), 55 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 3c3aca31b..e1dadd81d 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -19,7 +19,9 @@ define([ '/customize/application_config.js', '/common/diffMarked.js', '/common/sframe-common-codemirror.js', + '/common/text-cursor.js', 'cm/lib/codemirror', + '/bower_components/chainpad/chainpad.dist.js', '/common/inner/share.js', '/common/inner/access.js', @@ -61,7 +63,9 @@ define([ AppConfig, DiffMd, SFCodeMirror, + TextCursor, CMeditor, + ChainPad, Share, Access, Properties, Flatpickr, Sortable @@ -195,12 +199,6 @@ define([ if (tmp && tmp.cursor) { cursor = tmp.cursor; } - /* - if (tmp && tmp.content && Sortify(v) === Sortify(tmp.old)) { - v = tmp.content; - cursor = tmp.cursor; - } - */ // Checkbox: max options var maxOptions, maxInput; @@ -287,6 +285,12 @@ define([ // if this element was active before the remote change, restore cursor var setCursor = function () { if (v.type && v.type !== 'text') { return; } + try { + var ops = ChainPad.Diff.diff(cursor.el, val); + ['start', 'end'].forEach(function (attr) { + cursor[attr] = TextCursor.transformCursor(cursor[attr], ops); + }); + } catch (e) { console.error(e); } input.selectionStart = cursor.start || 0; input.selectionEnd = cursor.end || 0; setTimeout(function () { input.focus(); }); @@ -493,7 +497,6 @@ define([ // 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) { @@ -503,44 +506,21 @@ define([ cursor.start = el.selectionStart; cursor.end = el.selectionEnd; } - values.push(val); }); - if (v.type === "day") { - var dayPickr = $(calendarView).find('input')[0]._flatpickr; - values = dayPickr.selectedDates.map(function (date) { - return +date; - }); - } - var _content = {values: values}; - - if (maxInput) { - _content.max = Number($(maxInput).val()) || 1; - } - - if (typeSelect) { - _content.type = typeSelect.getValue(); - } - if (v.items) { var items = []; $(containerItems).find('input').each(function (i, el) { + var val = $(el).val() || el.placeholder || ''; if (el === active) { cursor.item = true; cursor.uid= $(el).data('uid'); cursor.start = el.selectionStart; cursor.end = el.selectionEnd; + cursor.el = val; } - var val = $(el).val() || el.placeholder || ''; - items.push({ - uid: $(el).data('uid'), - v: val - }); }); - _content.items = items; } return { - old: (tmp && tmp.old) || v, - content: _content, cursor: cursor }; }); @@ -901,14 +881,8 @@ define([ return { tag: tag, edit: function (cb, tmp) { - var t = h('textarea'); - var block = h('div.cp-form-edit-options-block', [t]); - var cm = SFCodeMirror.create("gfm", CMeditor, t); - var editor = cm.editor; - editor.setOption('lineNumbers', true); - editor.setOption('lineWrapping', true); - editor.setOption('styleActiveLine', true); - editor.setOption('readOnly', false); + // Cancel changes + var cancelBlock = saveAndCancelOptions(cb); var text = opts.text; var cursor; @@ -916,20 +890,36 @@ define([ cursor = tmp.cursor; } + var block, editor; + if (tmp && tmp.block) { + block = tmp.block; + editor = tmp.editor; + } + + if (!block || !editor) { + var t = h('textarea'); + block = h('div.cp-form-edit-options-block', [t]); + var cm = SFCodeMirror.create("gfm", CMeditor, t); + editor = cm.editor; + editor.setOption('lineNumbers', true); + editor.setOption('lineWrapping', true); + editor.setOption('styleActiveLine', true); + editor.setOption('readOnly', false); + } + setTimeout(function () { - editor.setValue(text); - if (cursor) { - if (Sortify(cursor.start) === Sortify(cursor.end)) { - editor.setCursor(cursor.start); - } else { - editor.setSelection(cursor.start, cursor.end); - } + editor.focus(); + if (!(tmp && tmp.editor)) { + editor.setValue(text); + } else { + SFCodeMirror.setValueAndCursor(editor, editor.getValue(), text); } editor.refresh(); editor.save(); editor.focus(); }); - if (APP.common) { + + if (APP.common && !(tmp && tmp.block)) { var markdownTb = APP.common.createMarkdownToolbar(editor, { embed: function (mt) { editor.focus(); @@ -940,8 +930,6 @@ define([ $(markdownTb.toolbar).show(); cm.configureTheme(APP.common, function () {}); } - // Cancel changes - var cancelBlock = saveAndCancelOptions(cb); var getContent = function () { return { @@ -949,9 +937,13 @@ define([ }; }; - editor.on('change', function () { + if (tmp && tmp.onChange) { + editor.off('change', tmp.onChange); + } + var on = function () { cb(getContent()); - }); + }; + editor.on('change', on); cursorGetter = function () { if (document.activeElement && block.contains(document.activeElement)) { @@ -961,9 +953,10 @@ define([ }; } return { - old: opts, - content: getContent(), - cursor: cursor + cursor: cursor, + block: block, + editor: editor, + onChange: on }; }; From 2655a99b80c8b36a3dee9b2f35e6694f8241ea6b Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 26 Aug 2021 15:43:16 +0200 Subject: [PATCH 13/21] Allow guest answers by default in forms --- www/form/inner.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/www/form/inner.js b/www/form/inner.js index c22cb7e8e..b83728c0e 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -3477,7 +3477,7 @@ define([ UI.alert(content); }; - framework.onReady(function () { + framework.onReady(function (isNew) { var priv = metadataMgr.getPrivateData(); if (APP.isEditor) { @@ -3502,6 +3502,9 @@ define([ } checkIntegrity(); } + if (isNew && content.answers && typeof(content.answers.anonymous) === "undefined") { + content.answers.anonymous = true; + } sframeChan.event('EV_FORM_PIN', {channel: content.answers.channel}); From f97ca8e4cd4c366b8bf62b02b32422b7805641ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Benqu=C3=A9?= Date: Thu, 26 Aug 2021 14:44:10 +0100 Subject: [PATCH 14/21] Test new names for form editor settings --- www/form/inner.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/www/form/inner.js b/www/form/inner.js index 1a1ab6759..c6ec069a1 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -3225,6 +3225,7 @@ define([ refreshAnon(); // XXX UPDATE KEYS "form_anonyous_on", "form_anonymous_off" and "form_anonymous" + Messages.form_anonymous = "Guest access (not logged in)"; // XXX existing key // Allow guest(anonymous) answers var privacyContainer = h('div.cp-form-privacy-container'); var $privacy = $(privacyContainer); @@ -3255,7 +3256,7 @@ define([ refreshPrivacy(); // Allow responses edition - Messages.form_editable = "Allow users to edit their responses"; // XXX + Messages.form_editable = "Editing after submit"; // XXX var editableContainer = h('div.cp-form-editable-container'); var $editable = $(editableContainer); var refreshEditable = function () { From 7a1bc5bc40d457820bd880f7fda9fd3ded1e4864 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 26 Aug 2021 16:03:33 +0200 Subject: [PATCH 15/21] Improve keyboard shortcuts in form edition --- www/form/inner.js | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index b83728c0e..1ca03f832 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -258,15 +258,16 @@ define([ var addMultiple; var getOption = function (val, placeholder, isItem, uid) { var input = h('input', {value:val}); + var $input = $(input); if (placeholder) { input.placeholder = val; input.value = ''; - $(input).on('keypress', function () { - $(input).removeAttr('placeholder'); - $(input).off('keypress'); + $input.on('keypress', function () { + $input.removeAttr('placeholder'); + $input.off('keypress'); }); } - if (uid) { $(input).data('uid', uid); } + if (uid) { $input.data('uid', uid); } // If the input is a date, initialize flatpickr if (v.type && v.type !== 'text') { @@ -334,14 +335,26 @@ define([ }); if (!v.type || v.type === "text") { - $(input).keyup(function (e) { - if (e.which === 13) { - if (isItem && $addItem && $addItem.is(':visible')) { $addItem.click(); } - if (!isItem && $add && $add.is(':visible')) { $add.click(); } - } - if (e.which === 27 && !$(input).val()) { - $(del).click(); - } + $input.keyup(function (e) { + try { + if (e.which === 13) { + var $line = $input.closest('.cp-form-edit-block-input'); + if ($input.closest('.cp-form-edit-block') + .find('.cp-form-edit-block-input').last()[0] === $line[0]) { + // If we're the last input, add a new one + if (isItem && $addItem && $addItem.is(':visible')) { + $addItem.click(); + } + if (!isItem && $add && $add.is(':visible')) { $add.click(); } + } else { + // Otherwise focus the next one + $line.next().find('input').focus(); + } + } + if (e.which === 27 && !$(input).val()) { + $(del).click(); + } + } catch (err) { console.error(err); } }); } @@ -510,7 +523,6 @@ define([ } }); if (v.items) { - var items = []; $(containerItems).find('input').each(function (i, el) { var val = $(el).val() || el.placeholder || ''; if (el === active) { @@ -898,10 +910,11 @@ define([ editor = tmp.editor; } + var cm; if (!block || !editor) { var t = h('textarea'); block = h('div.cp-form-edit-options-block', [t]); - var cm = SFCodeMirror.create("gfm", CMeditor, t); + cm = SFCodeMirror.create("gfm", CMeditor, t); editor = cm.editor; editor.setOption('lineNumbers', true); editor.setOption('lineWrapping', true); @@ -921,7 +934,7 @@ define([ editor.focus(); }); - if (APP.common && !(tmp && tmp.block)) { + if (APP.common && !(tmp && tmp.block) && cm) { var markdownTb = APP.common.createMarkdownToolbar(editor, { embed: function (mt) { editor.focus(); From ee878ca830eb9b48b267fa56e0c0d47fb1313280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Benqu=C3=A9?= Date: Thu, 26 Aug 2021 15:06:41 +0100 Subject: [PATCH 16/21] Change settings order and test microcopy --- www/form/inner.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 8bd1d34ec..4351c8d17 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -3106,7 +3106,7 @@ define([ }); }); - + Messages.form_makePublicWarning = "Are you sure you want to make responses to this form public? Past and future responses will be visible by participants. This cannot be undone." // XXX existing key // Private / public status var resultsType = h('div.cp-form-results-type-container'); var $results = $(resultsType); @@ -3140,7 +3140,7 @@ define([ $responseMsg.empty(); Messages.form_updateMsg = "Update submit message"; // XXX 4.11.0 Messages.form_addMsg = "Add submit message"; // XXX 4.11.0 - Messages.form_responseMsg = "Add a message that will be displayed after participants submit the form."; // XXX 4.11.0 + Messages.form_responseMsg = "This message will be displayed after participants submit the form."; // XXX 4.11.0 var text = content.answers.msg ? Messages.form_updateMsg : Messages.form_addMsg; var btn = h('button.btn.btn-secondary', text); $(btn).click(function () { @@ -3212,7 +3212,7 @@ define([ refreshResponse(); // Make answers anonymous - Messages.form_makeAnon = "Make all answers anonymous"; // XXX + Messages.form_makeAnon = "Anonymous responses"; // XXX var anonContainer = h('div.cp-form-anon-container'); var $anon = $(anonContainer); var refreshAnon = function () { @@ -3365,8 +3365,8 @@ define([ return [ preview, endDateContainer, - privacyContainer, anonContainer, + privacyContainer, editableContainer, resultsType, responseMsg From 324e41b86625e1c1e47122536f365f3bce6924c2 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 27 Aug 2021 15:31:34 +0200 Subject: [PATCH 17/21] Fix flickering when redrawing form --- www/form/inner.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 1ca03f832..d57f90a3c 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -2754,7 +2754,7 @@ define([ q = h('div.cp-form-input-block', [inputQ]); // Delete question - var edit = h('span'); + var fakeEdit = h('span'); var del = h('button.btn.btn-danger-alt', [ h('i.fa.fa-trash-o'), h('span', Messages.form_delete) @@ -2771,12 +2771,15 @@ define([ updateAddInline(); }); + editButtons = h('div.cp-form-edit-buttons-container', [ fakeEdit, del ]); + // Values if (data.edit) { - edit = h('button.btn.btn-default.cp-form-edit-button', [ + var edit = h('button.btn.btn-default.cp-form-edit-button', [ h('i.fa.fa-pencil'), h('span', Messages.form_editBlock) ]); + editButtons = h('div.cp-form-edit-buttons-container', [ edit, del ]); editContainer = h('div'); var onSave = function (newOpts, close) { if (close) { // Cancel edit @@ -2820,9 +2823,7 @@ define([ // If we were editing this field, recover our unsaved changes if (temp && temp[uid]) { - setTimeout(function () { - onEdit(temp[uid]); - }); + onEdit(temp[uid]); } changeType = h('div.cp-form-block-type', [ @@ -2882,10 +2883,6 @@ define([ }); } } - - editButtons = h('div.cp-form-edit-buttons-container', [ - edit, del - ]); } var editableCls = editable ? ".editable" : ""; elements.push(h('div.cp-form-block'+editableCls, { From 28db6956dd250879d86e4fd3d1f186eed7f7d61d Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 30 Aug 2021 11:45:45 +0200 Subject: [PATCH 18/21] Improve collaborative edition of forms --- www/common/common-util.js | 2 +- www/form/inner.js | 28 ++++++++++++++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/www/common/common-util.js b/www/common/common-util.js index 3d2a8d0a4..ea8d54256 100644 --- a/www/common/common-util.js +++ b/www/common/common-util.js @@ -437,7 +437,7 @@ var now = +new Date(); if (last && now <= last + t) { return t - (now - last); } last = now; - f(); + f.apply(null, Util.slice(arguments)); return null; }; }; diff --git a/www/form/inner.js b/www/form/inner.js index 9d7ab56ca..adf092aa8 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -3648,6 +3648,29 @@ define([ }); + // Only redraw once every 500ms on remote change. + // If we try to redraw in the first 500ms following a redraw, we'll + // redraw again when the timer allows it. If we call "redrawRemote" + // repeatedly, we'll only "redraw" once with the latest content. + var _redraw = Util.notAgainForAnother(updateForm, 500); + var redrawTo; + var redrawRemote = function () { + var $main = $('.cp-form-creator-container'); + var args = Array.prototype.slice.call(arguments); + var sTop = $main.scrollTop(); + var until = _redraw.apply(null, args); + if (until) { + clearTimeout(redrawTo); + redrawTo = setTimeout(function (){ + sTop = $main.scrollTop(); + _redraw.apply(null, args); + $main.scrollTop(sTop); + }, until+1); + return; + } + // Only restore scroll if we were able to redraw + $main.scrollTop(sTop); + }; framework.onContentUpdate(function (newContent) { content = newContent; evOnChange.fire(); @@ -3656,10 +3679,7 @@ define([ if (!APP.isEditor) { answers = getFormResults(); } else { temp = getTempFields(); } - var $main = $('.cp-form-creator-container'); - var sTop = $main.scrollTop(); - updateForm(framework, content, APP.isEditor, answers, temp); - $main.scrollTop(sTop); + redrawRemote(framework, content, APP.isEditor, answers, temp); }); framework.setContentGetter(function () { From a59047364cacb1fc1ed97bffdc20bd9bbe7b3044 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 31 Aug 2021 10:43:17 +0200 Subject: [PATCH 19/21] Update XXX in forms --- www/form/inner.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index adf092aa8..4f3df414b 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -564,7 +564,6 @@ define([ values = values.filter(Boolean); // Block empty or undefined options if (!values.length) { return; - //return void UI.warn(Messages.error); // XXX not anymore with autosave? } var res = { values: values }; @@ -588,7 +587,7 @@ define([ // Show duplicates warning if (duplicates) { - UI.warn(Messages.form_duplicates); // XXX + UI.warn(Messages.form_duplicates); // XXX autosave } // If checkboxes, get the maximum number of values the users can select @@ -2831,9 +2830,8 @@ define([ h('span', Messages['form_type_'+type]) ]); - //Messages.form_changeType = "Change type"; // XXX Messages.form_changeTypeConfirm = "Select the new type of this question and click OK."; // XXX - Messages.form_breakAnswers = "Changing the type may corrupt existing answers"; + Messages.form_corruptAnswers = "Changing the type may corrupt existing answers"; if (Array.isArray(model.compatible)) { changeType = h('div.cp-form-block-type.editable', [ model.icon.cloneNode(), @@ -2853,7 +2851,7 @@ define([ var tag = h('div.radio-group', els); var changeTypeContent = [ APP.answers && Object.keys(APP.answers).length ? - h('div.alert.alert-warning', Messages.form_breakAnswers) : + h('div.alert.alert-warning', Messages.form_corruptAnswers) : undefined, h('p', Messages.form_changeTypeConfirm), tag @@ -3065,9 +3063,12 @@ define([ var $body = $('body'); var $toolbarContainer = $('#cp-toolbar'); - var helpMenu = framework._.sfCommon.createHelpMenu(['text', 'pad']); - $toolbarContainer.after(helpMenu.menu); - framework._.toolbar.$drawer.append(helpMenu.button); + + if (APP.isEditor || priv.form_auditorKey) { + var helpMenu = framework._.sfCommon.createHelpMenu(['text', 'pad']); + $toolbarContainer.after(helpMenu.menu); + framework._.toolbar.$drawer.append(helpMenu.button); + } var offlineEl = h('div.alert.alert-danger.cp-burn-after-reading', Messages.disconnected); framework.onEditableChange(function (editable) { From 96132d78f9236302e9e78df25a7d1b9a1ff1b24d Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 31 Aug 2021 10:47:56 +0200 Subject: [PATCH 20/21] Update tag when changing the type of a question in forms --- www/form/inner.js | 1 + 1 file changed, 1 insertion(+) diff --git a/www/form/inner.js b/www/form/inner.js index 4f3df414b..7524cd57b 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -2874,6 +2874,7 @@ define([ framework.localChange(); var $oldTag = $(data.tag); framework._.cpNfInner.chainpad.onSettle(function () { + $(changeType).find('span').text(Messages['form_type_'+type]); data = model.get(block.opts, _answers, null, evOnChange); $oldTag.before(data.tag).remove(); }); From 669297db4cd3c04c2d1991b9fc0b7787726b44fa Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 1 Sep 2021 12:11:52 +0200 Subject: [PATCH 21/21] Fix UI issues when editing responses is blocked in forms --- www/form/inner.js | 8 ++++++++ www/form/main.js | 3 +-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 7524cd57b..f1ad9d89d 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -2337,6 +2337,10 @@ define([ $cbox.after(h('div.alert.alert-info', Messages.form_authAnswer)); }); } + if (update && content.answers.cantEdit) { + $cbox.hide(); + anonName = undefined; + } var send = h('button.cp-open.btn.btn-primary', update ? Messages.form_update : Messages.form_submit); var reset = h('button.cp-open.cp-reset-button.btn.btn-danger-alt', Messages.form_reset); @@ -2392,6 +2396,10 @@ define([ $send.text(Messages.form_update); APP.hasAnswered = true; showAnsweredPage(framework, content, { '_time': +new Date() }); + if (content.answers.cantEdit) { + $cbox.hide(); + if ($anonName) { $anonName.hide(); } + } }); }); diff --git a/www/form/main.js b/www/form/main.js index 7d81b1268..8c794a19a 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -221,8 +221,7 @@ define([ delete results[parsed._proof.key]; } } - // XXX If "allow edition" is disabled, don't override here? - // if (data.cantEdit && results[senderCurve]) { return; } + if (data.cantEdit && results[senderCurve]) { return; } results[senderCurve] = { msg: parsed, hash: hash,