From 29dc1a5b3ba2f5c368b374f1dbd4f6108d203e2d Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 27 May 2021 17:46:46 +0200 Subject: [PATCH] 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;