You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cryptpad/www/form/inner.js

3196 lines
126 KiB
JavaScript

define([
'jquery',
'json.sortify',
'/bower_components/chainpad-crypto/crypto.js',
'/common/sframe-app-framework.js',
'/common/toolbar.js',
3 years ago
'/form/export.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/tippy/tippy.min.js',
'/common/clipboard.js',
'/common/inner/common-mediatag.js',
'/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',
'/common/inner/properties.js',
'/common/inner/charts.js',
'/lib/datepicker/flatpickr.js',
'/bower_components/sortablejs/Sortable.min.js',
'cm/addon/display/placeholder',
'cm/mode/gfm/gfm',
'css!cm/lib/codemirror.css',
3 years ago
'/bower_components/file-saver/FileSaver.min.js',
'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!/lib/chart/charts.min.css',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
'less!/form/app-form.less',
], function (
$,
4 years ago
Sortify,
Crypto,
Framework,
Toolbar,
3 years ago
Exporter,
nThen,
SFCommon,
Util,
Hash,
UI,
UIElements,
tippy,
Clipboard,
MT,
h,
Messages,
AppConfig,
DiffMd,
SFCodeMirror,
CMeditor,
Share, Access, Properties, Charts,
Flatpickr,
Sortable
)
{
var APP = window.APP = {
};
var is24h = UIElements.is24h();
var dateFormat = "Y-m-d H:i";
var timeFormat = "H:i";
if (!is24h) {
dateFormat = "Y-m-d h:i K";
timeFormat = "h:i K";
}
// multi-line radio, checkboxes, and possibly other things have a max number of items
// we'll consider increasing this restriction if people are unhappy with it
// but as a general rule we expect users will appreciate having simpler questions
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());
});
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)) {
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 < 1) { 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) {
// Messages.form_text_text.form_text_number.form_text_url.form_text_email
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
];
};
4 years ago
var editOptions = function (v, setCursorGetter, cb, tmp) {
var add = h('button.btn.btn-secondary', [
h('i.fa.fa-plus'),
h('span', Messages.form_add_option)
]);
var addItem = h('button.btn.btn-secondary', [
h('i.fa.fa-plus'),
h('span', Messages.form_add_item)
]);
4 years ago
var cursor;
if (tmp && tmp.content && Sortify(v) === Sortify(tmp.old)) {
v = tmp.content;
4 years ago
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
4 years ago
});
maxOptions = h('div.cp-form-edit-max-options', [
h('span', Messages.form_editMax),
maxInput
]);
}
var type, typeSelect;
if (v.type) {
// Messages.form_poll_text.form_poll_day.form_poll_time
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
4 years ago
var $add, $addItem;
var addMultiple;
var getOption = function (val, isItem, uid) {
var input = h('input', {value:val});
if (uid) { $(input).data('uid', uid); }
4 years ago
// 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,
defaultDate: val ? new Date(val) : undefined
});
} else if (v.type === 'day') {
/*Flatpickr(input, {
defaultDate: val ? new Date(val) : undefined
});*/
}
}
// if this element was active before the remote change, restore cursor
var setCursor = function () {
if (v.type && v.type !== 'text') { return; }
4 years ago
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(); }
4 years ago
}
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'),
h('i.fa.fa-ellipsis-v'),
]),
input,
del
]);
$(del).click(function () {
var $block = $(el).closest('.cp-form-edit-block');
$(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();
if (v.type === "time") { $(addMultiple).show(); }
}
// decrement the max choices input when there are fewer options than the current maximum
if (maxInput) {
var inputs = $block.find('input').length;
var $maxInput = $(maxInput);
var currentMax = Number($maxInput.val());
$maxInput.val(Math.min(inputs, currentMax));
}
});
return el;
};
var inputs = v.values.map(function (val) { return getOption(val, false); });
inputs.push(add);
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",
4 years ago
forceFallback: true,
});
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);
Sortable.create(containerItems, {
direction: "vertical",
handle: ".cp-form-handle",
draggable: ".cp-form-edit-block-input",
4 years ago
forceFallback: true,
});
}
// 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
});
}
4 years ago
// Calendar time
if (v.type) {
4 years ago
var multipleInput = h('input', {placeholder: Messages.form_addMultipleHint});
var multipleClearButton = h('button.btn', Messages.form_clear);
var addMultipleButton = h('button.btn', [
h('i.fa.fa-plus'),
h('span', Messages.form_addMultiple)
]);
4 years ago
addMultiple = h('div.cp-form-multiple-picker', { 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 {
$(addMultiple).hide();
$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();
$(add).click();
return;
}
$container.find('input').each(function (i, input) {
if (input._flatpickr) {
input._flatpickr.destroy();
delete input._flatpickr;
}
});
});
}
// "Add option" button handler
$add = $(add).click(function () {
var txt = v.type ? '' : Messages.form_newOption;
$add.before(getOption(txt, false));
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 ($(containerItems).find('input').length >= MAX_ITEMS) { $addItem.hide(); }
4 years ago
// 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 && !el._flatpickr) {
4 years ago
cursor.el= $(el).val();
cursor.start = el.selectionStart;
cursor.end = el.selectionEnd;
}
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) {
_content.max = Number($(maxInput).val()) || 1;
}
if (typeSelect) {
_content.type = typeSelect.getValue();
}
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;
}
4 years ago
return {
old: (tmp && tmp.old) || v,
content: _content,
4 years ago
cursor: cursor
};
});
var getSaveRes = function () {
// Get values
var values = [];
var duplicates = false;
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") {
var f = el._flatpickr;
if (f && f.selectedDates && f.selectedDates.length) {
val = +f.selectedDates[0];
}
}
if (val && values.indexOf(val) === -1) { values.push(val); }
else { duplicates = true; }
});
}
values = values.filter(Boolean); // Block empty or undeinfed options
if (!values.length) {
return void UI.warn(Messages.error);
}
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);
}
// 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;
}
if (typeSelect) {
res.type = typeSelect.getValue();
}
return res;
};
var saveAndCancel = saveAndCancelOptions(getSaveRes, cb);
return [
type,
maxOptions,
calendarView,
h('div.cp-form-edit-options-block', [containerItems, container]),
addMultiple,
saveAndCancel
];
};
var getWeekDays = function (large) {
var baseDate = new Date(2017, 0, 1); // just a Sunday
var weekDays = [];
for(var i = 0; i < 7; i++) {
weekDays.push(baseDate.toLocaleDateString(undefined, { weekday: 'long' }));
baseDate.setDate(baseDate.getDate() + 1);
}
if (!large) {
weekDays = weekDays.map(function (day) { return day.slice(0,3); });
}
return weekDays.map(function (day) { return day.replace(/^./, function (str) { return str.toUpperCase(); }); });
};
// "resultsPageObj" is an object with "content" and "answers"
// only available when viewing the Responses page
var makePollTable = function (answers, opts, resultsPageObj) {
// 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 allDays = getWeekDays(true);
4 years ago
var els = opts.values.map(function (data) {
var _date;
if (opts.type === "day") {
_date = new Date(data);
data = _date.toLocaleDateString();
}
if (opts.type === "time") {
_date = new Date(data);
data = Flatpickr.formatDate(_date, timeFormat);
}
var day = _date && allDays[_date.getDay()];
return h('div.cp-poll-cell.cp-form-poll-option', {
title: Util.fixHTML(data)
}, [
opts.type === 'day' ? h('span.cp-form-weekday', day) : undefined,
opts.type === 'day' ? h('span.cp-form-weekday-separator', ' - ') : undefined,
h('span', data)
]);
});
// Insert axis switch button
4 years ago
var switchAxis = h('button.btn.btn-default', [
4 years ago
Messages.form_poll_switch,
h('i.fa.fa-exchange'),
]);
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] = {
n: (_days[day] && _days[day].n) || 0,
name: allDays[date.getDay()]
};
_days[day].n++;
});
Object.keys(_days).forEach(function (day) {
days.push(h('div.cp-poll-cell.cp-poll-time-day', {
style: 'flex-grow:'+(_days[day].n - 1)+';'
}, [
h('span.cp-form-weekday', _days[day].name),
h('span.cp-form-weekday-separator', ' - '),
h('span', day)
]));
});
lines.unshift(h('div', days));
}
// Add answers
var bodyEls = [];
if (Array.isArray(answers)) {
answers.forEach(function (answerObj) {
var answer = answerObj.results;
if (!answer || !answer.values) { return; }
var name = Util.find(answerObj, ['user', 'name']) || answer.name || Messages.anonymous;
var avatar = h('span.cp-avatar');
APP.common.displayAvatar($(avatar), Util.find(answerObj, ['user', 'avatar']), 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;
});
var nameCell;
els.unshift(nameCell = h('div.cp-poll-cell.cp-poll-answer-name', {
title: Util.fixHTML(name)
}, [
avatar,
h('span', name)
]));
bodyEls.push(h('div', els));
if (resultsPageObj && (APP.isEditor || APP.isAuditor)) {
$(nameCell).addClass('cp-clickable').click(function () {
APP.renderResults(resultsPageObj.content, resultsPageObj.answers, answerObj.curve);
});
}
});
}
var body = h('div.cp-form-poll-body', bodyEls);
lines.push(body);
var $s = $(switchAxis).click(function () {
$s.closest('.cp-form-type-poll').toggleClass('cp-form-poll-switch');
});
return lines;
};
var makePollTotal = function (answers, opts, myLine, evOnChange) {
if (!Array.isArray(answers)) { return; }
var totals = {};
var myTotals = {};
var updateMyTotals = function () {
if (!myLine) { return; }
opts.values.forEach(function (data) {
myLine.some(function (el) {
if ($(el).data('option') !== data) { return; }
var res = Number($(el).attr('data-value')) || 0;
if (res === 1) {
myTotals[data] = {
y: 1,
m: 0
};
}
else if (res === 2) {
myTotals[data] = {
y: 0,
m: 1
};
} else {
delete myTotals[data];
}
return true;
});
});
};
var totalEls = opts.values.map(function (data) {
var y = 0; // Yes
var m = 0; // Maybe
answers.forEach(function (answerObj) {
var answer = answerObj.results;
if (!answer || !answer.values) { return; }
var values = answer.values || {};
var res = Number(values[data]) || 0;
if (res === 1) { y++; }
else if (res === 2) { m++; }
});
totals[data] = {
y: y,
m: m
};
return h('div.cp-poll-cell', {
'data-id': data
}, [
h('span.cp-form-total-yes', y),
h('span.cp-form-total-maybe', '('+m+')'),
]);
});
totalEls.unshift(h('div.cp-poll-cell', Messages.form_pollTotal));
var total = h('div.cp-poll-total', totalEls);
var $total = $(total);
var refreshBest = function () {
var totalMax = {
value: 0,
data: []
};
Object.keys(totals).forEach(function (k) {
var obj = Util.clone(totals[k]);
if (myTotals[k]) {
obj.y += myTotals[k].y || 0;
obj.m += myTotals[k].m || 0;
}
if (obj.y === totalMax.value) {
totalMax.data.push(k);
} else if (obj.y > totalMax.value) {
totalMax.value = obj.y;
totalMax.data = [k];
}
});
if (totalMax.value) {
$total.find('[data-id]').removeClass('cp-poll-best');
totalMax.data.forEach(function (k) {
$total.find('[data-id="'+k+'"]').addClass('cp-poll-best');
});
}
};
refreshBest();
if (myLine && evOnChange) {
var updateValues = function () {
totalEls.forEach(function (cell) {
var $c = $(cell);
var data = $c.attr('data-id');
if (!data) { return; }
var y = totals[data].y + ((myTotals[data] || {}).y || 0);
var m = totals[data].m + ((myTotals[data] || {}).m || 0);
$c.find('.cp-form-total-yes').text(y);
$c.find('.cp-form-total-maybe').text('('+m+')');
});
};
evOnChange.reg(function () {
updateMyTotals();
updateValues();
refreshBest();
});
}
return total;
};
4 years ago
var getEmpty = function (empty) {
if (empty) {
return UI.setHTML(h('div.cp-form-results-type-text-empty'), Messages._getKey('form_notAnswered', [empty]));
}
};
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;
};
4 years ago
var getBlockAnswers = function (answers, uid, filterCurve) {
if (!answers) { return; }
4 years ago
return Object.keys(answers || {}).map(function (user) {
if (filterCurve && user === filterCurve) { return; }
try {
return {
curve: user,
user: answers[user].msg._userdata,
results: answers[user].msg[uid]
};
4 years ago
} catch (e) { console.error(e); }
}).filter(Boolean);
};
var STATIC_TYPES = {
md: {
defaultOpts: {
4 years ago
text: Messages.form_description_default
},
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);
4 years ago
var cursorGetter;
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);
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(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();
});
if (APP.common) {
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 () {});
}
// 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 getContent = function () {
return {
text: editor.getValue()
};
};
$(saveBlock).click(function () {
$(saveBlock).attr('disabled', 'disabled');
cb(getContent());
});
cursorGetter = function () {
if (document.activeElement && block.contains(document.activeElement)) {
cursor = {
start: editor.getCursor('from'),
end: editor.getCursor('to')
4 years ago
};
}
return {
old: opts,
content: getContent(),
cursor: cursor
};
};
return [
block,
h('div.cp-form-edit-save', [cancelBlock, saveBlock])
];
},
getCursor: function () { return cursorGetter(); },
};
},
4 years ago
printResults: function () { return; },
4 years ago
icon: h('i.cptools.cptools-form-paragraph')
},
4 years ago
page: {
get: function () {
var tag = h('div.cp-form-page-break-edit', [
4 years ago
h('i.cptools.cptools-form-page-break'),
4 years ago
h('span', Messages.form_type_page)
]);
return {
tag: tag,
pageBreak: true
};
},
printResults: function () { return; },
4 years ago
icon: h('i.cptools.cptools-form-page-break')
4 years ago
},
};
var arrayMax = function (A) {
return Array.isArray(A)? Math.max.apply(null, A): NaN;
};
var TYPES = { // XXX hackathon insert useful charts for each of type of answer
input: {
defaultOpts: {
type: 'text'
},
get: function (opts, a, n, evOnChange) {
if (!opts) { opts = TYPES.input.defaultOpts; }
// Messages.form_input_ph_email.form_input_ph_url
var tag = h('input', {
type: opts.type,
placeholder: Messages['form_input_ph_'+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 () {
//var invalid = $tag.is(':invalid');
//if (invalid) { return; }
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) { // XXX hackathon each question format has a 'printResults' method
4 years ago
var results = [];
var empty = 0;
var tally = {};
Object.keys(answers).forEach(function (author) {
4 years ago
var obj = answers[author];
var answer = obj.msg[uid];
if (!answer || !answer.trim()) { return empty++; }
Util.inc(tally, answer);
4 years ago
});
var counts = Util.values(tally);
var max = arrayMax(counts);
if (max < 2) { // there are no duplicates, so just return text
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);
}
4 years ago
// increase the scale of the bar chart if there are more empty answers than anything else
max = Math.max(max, empty);
// there are duplicates, so return a bar chart
var rows = []; // XXX
Object.keys(tally).forEach(function (answer) {
rows.push(Charts.row(answer, tally[answer] / max, tally[answer]));
});
var table = Charts.table([
//h('caption', ''), // XXX
h('tbody', rows)
], [
'charts-css',
'bar',
'show-heading',
'show-labels',
'show-data-on-hover',
]);
return h('div.cp-form-results-type-text', [
table,
empty? getEmpty(empty): undefined,
]);
4 years ago
},
4 years ago
icon: h('i.cptools.cptools-form-text')
},
textarea: {
defaultOpts: {
maxLength: 1000
},
get: function (opts, a, n, evOnChange) {
if (!opts) { opts = TYPES.textarea.defaultOpts; }
var text = h('textarea', {maxlength: opts.maxLength});
var $text = $(text);
var charCount = h('div.cp-form-type-textarea-charcount');
var updateChar = function () {
var l = $text.val().length;
if (l > opts.maxLength) {
$text.val($text.val().slice(0, opts.maxLength));
l = $text.val().length;
}
$(charCount).text(Messages._getKey('form_maxLength', [
$text.val().length,
opts.maxLength
]));
};
updateChar();
var tag = h('div.cp-form-type-textarea', [
text,
charCount
]);
var evChange = Util.throttle(function () {
evOnChange.fire();
}, 500);
$text.on('change keypress keyup keydown', function () {
setTimeout(updateChar);
evChange();
});
var cursorGetter;
var setCursorGetter = function (f) { cursorGetter = f; };
return {
tag: tag,
getValue: function () { return $text.val().slice(0, opts.maxLength); },
setValue: function (val) {
$text.val(val);
updateChar();
},
edit: function (cb, tmp) {
var v = Util.clone(opts);
return editTextOptions(v, setCursorGetter, cb, tmp);
},
getCursor: function () { return cursorGetter(); },
reset: function () { $text.val(''); }
};
},
printResults: function (answers, uid) {
var results = [];
var empty = 0;
// XXX hackathon like text inputs, charts are only useful if we tally duplicates
// (or if there are duplicates)
// https://chartscss.org/charts/bar/
Object.keys(answers).forEach(function (author) { // TODO deduplicate these
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.cptools.cptools-form-paragraph')
},
radio: {
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 name = Util.uid();
var els = opts.values.map(function (data, i) {
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.cp-form-type-radio', els);
4 years ago
var cursorGetter;
var setCursorGetter = function (f) { cursorGetter = f; };
$(tag).find('input[type="radio"]').on('change', function () {
evOnChange.fire();
});
return {
tag: tag,
getValue: function () {
var res;
4 years ago
els.some(function (el) {
var $i = $(el).find('input');
if (Util.isChecked($i)) {
res = $i.data('val');
return true;
}
});
return res;
},
reset: function () { $(tag).find('input').removeAttr('checked'); },
4 years ago
edit: function (cb, tmp) {
var v = Util.clone(opts);
4 years ago
return editOptions(v, setCursorGetter, cb, tmp);
},
4 years ago
getCursor: function () { return cursorGetter(); },
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;
}
});
}
};
},
4 years ago
printResults: function (answers, uid) {
// XXX hackathon radio https://chartscss.org/charts/bar/
4 years ago
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++; }
Util.inc(count, answer);
4 years ago
});
var counts = Util.values(count);
var max = arrayMax(counts);
var rows = [];
Object.keys(count).forEach(function (text) {
rows.push(Charts.row(text, count[text] / max, count[text]));
});
var table = Charts.table([
h('tbody', rows)
], [
'charts-css',
'bar',
'show-labels',
//'show-heading',
'show-data-on-hover',
]);
return h('div.cp-form-results-type-radio', {
style: 'width: 100%',
}, table);
/*
4 years ago
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)); // XXX show number of empty answers
4 years ago
return h('div.cp-form-results-type-radio', results);
*/
4 years ago
},
4 years ago
icon: h('i.cptools.cptools-form-list-radio')
},
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, 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) {
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; };
$(tag).find('input[type="radio"]').on('change', function () {
evOnChange.fire();
});
return {
tag: tag,
getValue: function () {
var res = {};
var l = lines.slice(1);
4 years ago
l.forEach(function (el) {
var $el = $(el);
var uid = $el.attr('data-uid');
4 years ago
$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) {
// XXX hackathon multiradio https://chartscss.org/components/stacked/
var structure = form[uid];
if (!structure) { return; }
var opts = structure.opts || TYPES.multiradio.defaultOpts;
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; }
Util.inc(c, res);
});
});
var max = 0;
var count_keys = Object.keys(count);
count_keys.forEach(function (q_uid) {
var counts = Object.values(count[q_uid]);
counts.push(max);
max = arrayMax(counts);
});
count_keys.forEach(function (q_uid) {
var q = findItem(opts.items, q_uid);
var c = count[q_uid];
var table = Charts.table([
h('caption', {
style: 'color: var(--msg-color)', // XXX light/dark modes
}, q),
h('tbody', Object.keys(c).map(function (res) {
return Charts.row(res, c[res] / max, c[res]);
})),
], [
'charts-css',
'bar',
'show-heading',
'show-data-on-hover',
'show-labels',
]);
results.push(h('div.cp-form-results-type-multiradio-data', {
style: 'width: 100%',
}, table));
/*
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', {
style: 'width: 100%',
}, results);
},
exportCSV: function (answer, form) {
var opts = form.opts || {};
var q = form.q || Messages.form_default;
if (answer === false) {
return (opts.items || []).map(function (obj) {
return q + ' | ' + obj.v;
});
}
if (!answer) { return ['']; }
return (opts.items || []).map(function (obj) {
var uid = obj.uid;
return String(answer[uid] || '');
});
},
4 years ago
icon: h('i.cptools.cptools-form-grid-radio')
},
checkbox: {
defaultOpts: {
max: 3,
values: [1, 2, 3].map(function (i) {
return Messages._getKey('form_defaultOption', [i]);
})
},
get: function (opts, a, n, evOnChange) {
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');
}
evOnChange.fire();
});
var cursorGetter;
var setCursorGetter = function (f) { cursorGetter = f; };
return {
tag: tag,
getValue: function () {
var res = [];
4 years ago
els.forEach(function (el) {
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) {
// XXX hackathon radio https://chartscss.org/charts/bar/
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) {
Util.inc(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);
},
4 years ago
icon: h('i.cptools.cptools-form-list-check')
},
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, 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) {
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');
}
evOnChange.fire();
});
});
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);
4 years ago
l.forEach(function (el) {
var $el = $(el);
var uid = $el.attr('data-uid');
res[uid] = [];
4 years ago
$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) {
// XXX hackathon multicheck
// stacked: https://chartscss.org/components/stacked/
// or multiple bars: https://chartscss.org/charts/bar/#multiple-datasets
var structure = form[uid];
if (!structure) { return; }
var opts = structure.opts || TYPES.multicheck.defaultOpts;
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) {
Util.inc(c, v);
});
});
});
var max = 0;
var count_keys = Object.keys(count);
count_keys.forEach(function (q_uid) {
var counts = Object.values(count[q_uid]);
counts.push(max);
max = arrayMax(counts);
});
count_keys.forEach(function (q_uid) {
var q = findItem(opts.items, q_uid);
var c = count[q_uid];
var table = Charts.table([
h('caption', {
style: 'color: var(--msg-color)', // XXX light/dark modes
}, q),
h('tbody', Object.keys(c).map(function (res) {
return Charts.row(res, c[res] / max, c[res]);
})),
], [
'charts-css',
'bar',
'show-heading',
'show-data-on-hover',
'show-labels',
]);
results.push(h('div.cp-form-results-type-multiradio-data', {
style: 'width: 100%',
}, table));
/*
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', {
style: 'width: 100%',
}, results);
},
exportCSV: function (answer, form) {
var opts = form.opts || {};
var q = form.q || Messages.form_default;
if (answer === false) {
return (opts.items || []).map(function (obj) {
return q + ' | ' + obj.v;
});
}
if (!answer) { return ['']; }
return (opts.items || []).map(function (obj) {
var uid = obj.uid;
return String(answer[uid] || '');
});
},
4 years ago
icon: h('i.cptools.cptools-form-grid-check')
},
sort: {
defaultOpts: {
values: [1,2].map(function (i) {
return Messages._getKey('form_defaultOption', [i]);
})
},
get: function (opts, a, n, evOnChange) {
if (!opts) { opts = TYPES.sort.defaultOpts; }
if (!Array.isArray(opts.values)) { return; }
var map = {};
var invMap = {};
var sorted = false;
Util.shuffleArray(opts.values);
3 years ago
var els = opts.values.map(function (data) {
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', '?'),
h('span', data)
]);
$(div).data('val', data);
return div;
});
var tag = h('div.cp-form-type-sort-container', [
h('div.cp-form-sort-hint', Messages._getKey('form_sort_hint', [els.length])),
els
]);
var $tag = $(tag);
var reorder = function (reset) {
$tag.find('.cp-form-type-sort').each(function (i, el) {
$(el).find('.cp-form-sort-order').text(reset ? '?' : i+1);
});
sorted = !reset;
};
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 () {
if (!sorted) { return; }
return sortable.toArray().map(function (id) {
return map[id];
});
},
reset: function () {
Util.shuffleArray(opts.values);
var toSort = (opts.values).map(function (val) {
return invMap[val];
});
sortable.sort(toSort);
reorder(true);
},
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) {
// XXX hackathon sortable list
// bars again? https://chartscss.org/charts/bar/
var opts = form[uid].opts || TYPES.sort.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;
Util.inc(count, el, score);
});
});
var counts = Util.values(count);
var max = arrayMax(counts);
var rows = [];
Object.keys(count).forEach(function (text) {
rows.push(Charts.row(text, count[text] / max, count[text]));
});
var table = Charts.table([
h('tbody', rows),
], [
'charts-css',
'bar',
'show-labels',
'show-data-on-hover',
//'show-heading',
]);
return h('div.cp-form-results-type-radio', {
style: 'width: 100%',
}, table);
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.cptools.cptools-form-list-ordered')
},
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, evOnChange) {
if (!opts) { opts = TYPES.poll.defaultOpts; }
if (!Array.isArray(opts.values)) { return; }
var lines = makePollTable(answers, opts, false);
// Add form
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'),
3 years ago
h('i.cptools.cptools-form-poll-maybe.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);
evOnChange.fire();
});
cell._setValue = function (v) {
val = v;
$c.attr('data-value', val);
};
return cell;
});
// Name input
//var nameInput = h('input', { value: username || Messages.anonymous });
4 years ago
var nameInput = h('span.cp-poll-your-answers', Messages.form_pollYourAnswers);
addLine.unshift(h('div.cp-poll-cell', nameInput));
lines.push(h('div', addLine));
var total = makePollTotal(answers, opts, addLine, evOnChange);
if (total) { lines.push(h('div', total)); }
var pollHint = UI.setHTML(h('div.cp-form-poll-hint'), Messages.form_poll_hint);
var classes = [
'fa fa-check cp-yes',
'fa fa-times cp-no',
'cptools cptools-form-poll-maybe cp-maybe',
];
$(pollHint).find('i').each(function (index) {
this.setAttribute('class', classes[index]);
});
var tag = h('div.cp-form-type-poll-container', [
pollHint,
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 = {};
$tag.find('.cp-form-poll-choice').each(function (i, el) {
var $el = $(el);
res[$el.data('option')] = $el.attr('data-value');
});
return {
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) { return; }
var val = res.values;
$tag.find('.cp-form-poll-choice').each(function (i, el) {
if (!el._setValue) { return; }
var $el = $(el);
el._setValue(val[$el.data('option')] || 0);
});
}
};
},
printResults: function (answers, uid, form, content) {
// XXX hackathon: not really anything to do here?
var opts = form[uid].opts || TYPES.poll.defaultOpts;
var _answers = getBlockAnswers(answers, uid);
// If content is defined, we'll be able to click on a row to display
// all the answers of this user
var lines = makePollTable(_answers, opts, content && {
content: content,
answers: answers
});
var total = makePollTotal(_answers, opts);
if (total) { lines.push(h('div', total)); }
return h('div.cp-form-type-poll', lines);
},
exportCSV: function (answer, form) {
var opts = form.opts || TYPES.poll.defaultOpts;
var q = form.q || Messages.form_default;
if (answer === false) {
var cols = opts.values.map(function (key) {
return q + ' | ' + key;
});
cols.unshift(q);
return cols;
}
if (!answer || !answer.values) {
var empty = opts.values.map(function () { return ''; });
empty.unshift('');
return empty;
}
var str = '';
Object.keys(answer.values).sort().forEach(function (k, i) {
if (i !== 0) { str += ';'; }
str += k.replace(';', '').replace(':', '') + ':' + answer.values[k];
});
var res = opts.values.map(function (key) {
return answer.values[key] || '';
});
res.unshift(str);
return res;
},
icon: h('i.cptools.cptools-form-poll')
},
};
var makeTimeline = APP.makeTimeline = function (answers) {
// Here for mockup purpose
Object.keys(answers).forEach(function (k) {
console.log(answers[k].time);
answers[k].time += Math.floor(Math.random() * 10 - 5) * 24 * 3600 * 1000;
console.log(answers[k].time);
});
var answersByTime = {};
Object.keys(answers).forEach(function (curve) {
var obj = answers[curve];
var key = new Date(obj.time).toLocaleDateString();
if (!answersByTime[key]) {
answersByTime[key] = {date: obj.time, count: 1};
} else {
answersByTime[key].count++;
}
});
var dates = Object.keys(answersByTime).sort(function (a, b) {
return answersByTime[a].time - answersByTime[b].time;
});
var maxCount = 0;
Object.keys(answersByTime).forEach(function (date) {
var count = answersByTime[date].count;
if (count > maxCount) {
maxCount = count;
}
});
Object.keys(answersByTime).forEach(function (date) {
var answer = answersByTime[date];
answer.percent = answer.count / maxCount;
answer.count = answer.count || "";
});
console.log(answersByTime);
console.log(dates);
console.log('done');
return Charts.table(h('tbody',dates.map(function (date) {
var count = answersByTime[date].count;
var percent = answersByTime[date].percent;
var bar = h('td.cp-bar', { style: '--size: ' + Number(percent).toFixed(2), "data-tippy-placement": "top", title: `${count} - ${date}` });
var dateEl = h('th', { scope: "row" }, date);
return h('tr', bar, dateEl );
})), ["charts-css", "cp-chart-table", "column", "data-spacing-2", "show-labels", "labels-align-center"]);
};
var renderResults = APP.renderResults = function (content, answers, showUser) { // XXX hackathon
var $container = $('div.cp-form-creator-results').empty();
if (!Object.keys(answers || {}).length) {
$container.append(h('div.alert.alert-info', Messages.form_results_empty));
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);
}
// XXX hackathon display answer times as column bar chart with first and last date indicated
// https://chartscss.org/charts/column/
// XXX hackathon set column colours with the cryptpad brand color in app-form.less
var heading = h('h2#cp-title', `Total answers count: ${Object.keys(answers).length}`);
$(heading).appendTo($container);
var timeline = h('div.cp-form-creator-results-timeline');
var $timeline = $(timeline).appendTo($container);
$timeline.append(makeTimeline(answers));
var controls = h('div.cp-form-creator-results-controls');
var $controls = $(controls).appendTo($container);
var exportButton = h('button.btn.btn-secondary', Messages.form_exportCSV);
3 years ago
var exportCSV = h('div.cp-form-creator-results-export', exportButton);
$(exportCSV).appendTo($container);
var results = h('div.cp-form-creator-results-content');
var $results = $(results).appendTo($container);
3 years ago
$(exportButton).click(function () {
var csv = Exporter.results(content, answers, TYPES);
if (!csv) { return void UI.warn(Messages.error); }
var suggestion = APP.framework._.title.suggestTitle('cryptpad-document');
var title = Util.fixFileName(suggestion) + '.csv';
window.saveAs(new Blob([csv], {
type: 'text/csv'
}), title);
});
var summary = true;
var form = content.form;
var switchMode = h('button.btn.btn-primary', Messages.form_showIndividual);
$controls.hide().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; }
// Only use content if we're not viewing individual answers
var print = model.printResults(answers, uid, form, !header && content);
var q = h('div.cp-form-block-question', block.q || Messages.form_default);
//Messages.form_type_checkbox.form_type_input.form_type_md.form_type_multicheck.form_type_multiradio.form_type_poll.form_type_radio.form_type_sort.form_type_textarea
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);
if (APP.isEditor || APP.isAuditor) { $controls.show(); }
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));
4 years ago
if (curve !== ud.curvePublic) {
warning = h('span.cp-form-warning', Messages.form_answerWarning);
}
} 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'));
});
if (showUser === curve) {
setTimeout(function () {
showUser = undefined;
$(viewButton).click();
});
}
return div;
});
$results.append(els);
});
if (showUser) {
$s.click();
}
};
4 years ago
var addResultsButton = function (framework, content) {
var $res = $(h('button.cp-toolbar-appmenu.cp-toolbar-form-button', [
4 years ago
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();
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);
};
4 years ago
var getFormResults = function () {
if (!Array.isArray(APP.formBlocks)) { return; }
var results = {};
APP.formBlocks.forEach(function (data) {
if (!data.getValue) { return; }
4 years ago
results[data.uid] = data.getValue();
});
return results;
};
var makeFormControls = function (framework, content, update, evOnChange) {
var loggedIn = framework._.sfCommon.isLoggedIn();
var metadataMgr = framework._.cpNfInner.metadataMgr;
var user = metadataMgr.getUserData();
if (!loggedIn && !content.answers.anonymous) { return; }
var cbox;
var anonName, $anonName;
cbox = UI.createCheckbox('cp-form-anonymous',
Messages.form_anonymousBox, true, { mark: { tabindex:1 } });
var $anonBox = $(cbox).find('input');
if (loggedIn) {
if (!content.answers.anonymous || APP.cantAnon) {
$(cbox).hide().find('input').attr('disabled', 'disabled').prop('checked', false);
}
} else {
anonName = h('div.cp-form-anon-answer-input', [
Messages.form_answerAs,
h('input', {
value: user.name || '',
placeholder: Messages.form_anonName
})
]);
$anonName = $(anonName).hide();
$anonBox.on('change', function () {
if (Util.isChecked($anonBox)) { $anonName.hide(); }
else { $anonName.show(); }
});
}
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);
$(reset).click(function () {
if (!Array.isArray(APP.formBlocks)) { return; }
APP.formBlocks.forEach(function (data) {
if (typeof(data.reset) === "function") { data.reset(); }
});
$(reset).attr('disabled', 'disabled');
});
var $send = $(send).click(function () {
$send.attr('disabled', 'disabled');
4 years ago
var results = getFormResults();
if (!results) { return; }
var user = metadataMgr.getUserData();
if (!Util.isChecked($anonBox)) {
results._userdata = loggedIn ? {
avatar: user.avatar,
name: user.name,
notifications: user.notifications,
curvePublic: user.curvePublic,
profile: user.profile
} : {
name: $anonName ? $anonName.find('input').val() : user.name
};
}
var sframeChan = framework._.sfCommon.getSframeChannel();
sframeChan.query('Q_FORM_SUBMIT', {
mailbox: content.answers,
results: results,
anonymous: !loggedIn || Util.isChecked($(cbox).find('input'))
}, function (err, data) {
4 years ago
$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);
}
evOnChange.fire(false, true);
window.onbeforeunload = undefined;
if (!update && content.answers.privateKey) {
4 years ago
// Add results button
addResultsButton(framework, content);
}
$send.removeAttr('disabled');
UI.alert(Messages.form_sent);
4 years ago
$send.text(Messages.form_update);
});
});
if (APP.isClosed) {
send = undefined;
reset = undefined;
}
var invalid = h('div.cp-form-invalid-warning');
var $invalid = $(invalid);
if (evOnChange) {
var origin, priv;
if (APP.common) {
priv = metadataMgr.getPrivateData();
origin = priv.origin;
}
evOnChange.reg(function () {
var $container = $('div.cp-form-creator-content');
var $inputs = $container.find('input:invalid');
if (!$inputs.length) {
$send.text(update ? Messages.form_update : Messages.form_submit);
return void $invalid.empty();
}
$send.text(update ? Messages.form_updateWarning : Messages.form_submitWarning);
var lis = [];
$inputs.each(function (i, el) {
var $el = $(el).closest('.cp-form-block');
var number = $el.find('.cp-form-block-question-number').text();
var a = h('a', {
href: origin + '#' + Messages._getKey('form_invalidQuestion', [number])
}, Messages._getKey('form_invalidQuestion', [number]));
$(a).click(function (e) {
e.preventDefault();
if (!$el.is(':visible')) {
var pages = $el.closest('.cp-form-page').index();
if (APP.refreshPage) { APP.refreshPage(pages + 1); }
}
$el[0].scrollIntoView();
});
var li = h('li', a);
lis.push(li);
});
var list = h('ul', lis);
var content = [
h('span', Messages.form_invalidWarning),
list
];
$invalid.empty().append(content);
});
evOnChange.fire(true);
}
return h('div.cp-form-send-container', [
invalid,
cbox ? h('div.cp-form-anon-answer', [
cbox,
anonName
]) : undefined,
reset, send
]);
};
4 years ago
var updateForm = function (framework, content, editable, answers, temp) {
var $container = $('div.cp-form-creator-content');
4 years ago
if (!$container.length) { return; } // Not ready
var form = content.form;
APP.formBlocks = [];
if (APP.isClosed && content.answers.privateKey && !APP.isEditor) {
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');
$('.cp-toolbar-form-button').remove();
renderResults(content, answers);
});
return;
}
var evOnChange = Util.mkEvent();
if (!APP.isEditor) {
var _answers = Util.clone(answers || {});
delete _answers._proof;
delete _answers._userdata;
evOnChange.reg(function (noBeforeUnload, isSave) {
if (noBeforeUnload) { return; }
$container.find('.cp-reset-button').removeAttr('disabled');
var results = getFormResults();
if (isSave) {
answers = Util.clone(results || {});
_answers = Util.clone(answers);
}
if (!answers || Sortify(_answers) !== Sortify(results)) {
window.onbeforeunload = function () {
return true;
};
} else {
window.onbeforeunload = undefined;
}
});
}
var getFormCreator = function (uid) {
4 years ago
if (!APP.isEditor) { return; }
var full = !uid;
var idx = content.order.indexOf(uid);
var addControl = function (type) {
var btn = h('button.btn.btn-secondary', {
title: full ? '' : 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', [h('i.fa.fa-plus')]);
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');
});
}
else {
$(add).append(h('span', Messages.tag_add));
}
4 years ago
var inlineCls = full ? '-full' : '-inline';
return h('div.cp-form-creator-add'+inlineCls, [
add,
buttons
]);
};
var updateAddInline = function () {
$container.find('.cp-form-creator-add-inline').remove();
$container.find('.cp-form-block').each(function (i, el) {
var $el = $(el);
var uid = $el.attr('data-id');
$el.before(getFormCreator(uid));
});
};
var elements = [];
var n = 1; // Question number
4 years ago
content.order.forEach(function (uid) {
var block = form[uid];
var type = block.type;
var model = TYPES[type] || STATIC_TYPES[type];
var isStatic = Boolean(STATIC_TYPES[type]);
if (!model) { return; }
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, evOnChange);
if (!data) { return; }
data.uid = uid;
if (answers && answers[uid] && data.setValue) { data.setValue(answers[uid]); }
4 years ago
if (data.pageBreak && !editable) {
elements.push(data);
return;
4 years ago
}
4 years ago
var dragHandle;
var q = h('div.cp-form-block-question', [
h('span.cp-form-block-question-number', (n++)+'.'),
h('span', block.q || Messages.form_default)
]);
// Static blocks don't have questions ("q" is not used) so we can decrement n
if (isStatic) { n--; }
var editButtons, editContainer;
APP.formBlocks.push(data);
if (editable) {
4 years ago
// Drag handle
dragHandle = h('span.cp-form-block-drag-handle', [
h('i.fa.fa-ellipsis-h'),
h('i.fa.fa-ellipsis-h'),
]);
4 years ago
// Question
var inputQ = h('input', {
value: block.q || Messages.form_default
});
var $inputQ = $(inputQ);
4 years ago
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); }
// Don't save if no change
if (v.trim() === block.q) {
$(q).removeClass('editing');
if (!e) { $inputQ.blur(); }
return;
}
4 years ago
if (saving && !e) { return; } // Prevent spam Enter
block.q = v.trim();
framework.localChange();
4 years ago
saving = true;
framework._.cpNfInner.chainpad.onSettle(function () {
4 years ago
saving = false;
$(q).removeClass('editing');
4 years ago
if (!e) { $inputQ.blur(); }
UI.log(Messages.saved);
});
4 years ago
};
var onCancelQ = function () {
$inputQ.val(block.q || Messages.form_default);
4 years ago
cancel = true;
$inputQ.blur();
$(q).removeClass('editing');
};
$inputQ.keydown(function (e) {
4 years ago
if (e.which === 13) { return void onSaveQ(); }
if (e.which === 27) { return void onCancelQ(); }
});
$inputQ.focus(function () {
$(q).addClass('editing');
});
4 years ago
$inputQ.blur(onSaveQ);
q = h('div.cp-form-input-block', [inputQ]);
// Delete question
4 years ago
var edit = h('span');
var del = h('button.btn.btn-danger-alt', [
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();
updateAddInline();
});
// Values
if (data.edit) {
4 years ago
edit = h('button.btn.btn-default.cp-form-edit-button', [
h('i.fa.fa-pencil'),
h('span', Messages.form_editBlock)
]);
editContainer = h('div');
var onSave = function (newOpts) {
4 years ago
data.editing = false;
if (!newOpts) { // Cancel edit
$(editContainer).empty();
$(editButtons).show();
$(data.tag).show();
return;
}
$(editContainer).empty();
block.opts = newOpts;
framework.localChange();
var $oldTag = $(data.tag);
framework._.cpNfInner.chainpad.onSettle(function () {
$(editButtons).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();
});
};
4 years ago
var onEdit = function (tmp) {
data.editing = true;
$(data.tag).hide();
$(editContainer).append(data.edit(onSave, tmp, framework));
$(editButtons).hide();
4 years ago
};
$(edit).click(function () {
onEdit();
});
4 years ago
// 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', [
edit, del
]);
}
var editableCls = editable ? ".editable" : "";
elements.push(h('div.cp-form-block'+editableCls, {
'data-id':uid
}, [
4 years ago
APP.isEditor ? dragHandle : undefined,
isStatic ? undefined : q,
h('div.cp-form-block-content', [
data.tag,
editButtons
]),
editContainer
]));
});
if (APP.isEditor) {
elements.push(getFormCreator());
}
4 years ago
var _content = elements;
if (!editable) {
_content = [];
var div = h('div.cp-form-page');
var pages = 1;
var wasPage = false;
4 years ago
elements.forEach(function (obj, i) {
4 years ago
if (obj && obj.pageBreak) {
4 years ago
if (i === 0) { return; } // Can't start with a page break
if (i === (elements.length - 1)) { return; } // Can't end with a page break
4 years ago
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);
if (pages > 1) {
var pageContainer = h('div.cp-form-page-container');
var $page = $(pageContainer);
_content.push(pageContainer);
var refreshPage = APP.refreshPage = function (current) {
$page.empty();
if (!current || current < 1) { current = 1; }
if (current > pages) { current = pages; }
4 years ago
var left = h('button.btn.btn-secondary.cp-prev', [
h('i.fa.fa-arrow-left'),
]);
var state = h('span', Messages._getKey('form_page', [current, pages]));
4 years ago
var right = h('button.btn.btn-secondary.cp-next', [
h('i.fa.fa-arrow-right'),
]);
if (current === pages) { $(right).css('visibility', 'hidden'); }
if (current === 1) { $(left).css('visibility', 'hidden'); }
$(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);
}
4 years ago
}
$container.empty().append(_content);
updateAddInline();
if (editable) {
APP.mainSortable = Sortable.create($container[0], {
direction: "vertical",
filter: "input, button, .CodeMirror, .cp-form-type-sort",
preventOnFilter: false,
4 years ago
draggable: ".cp-form-block",
//forceFallback: true,
fallbackTolerance: 5,
4 years ago
onStart: function () {
$container.find('.cp-form-creator-add-inline').remove();
},
store: {
set: function (s) {
content.order = s.toArray();
framework.localChange();
updateAddInline();
}
}
});
return;
}
// In view mode, add "Submit" and "reset" buttons
$container.append(makeFormControls(framework, content, Boolean(answers), evOnChange));
if (!answers) {
$container.find('.cp-reset-button').attr('disabled', 'disabled');
}
4 years ago
};
4 years ago
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();
3 years ago
APP.framework = framework;
var evOnChange = Util.mkEvent();
var content = {};
APP.common = framework._.sfCommon;
var sframeChan = framework._.sfCommon.getSframeChannel();
var metadataMgr = framework._.cpNfInner.metadataMgr;
var user = metadataMgr.getUserData();
var priv = metadataMgr.getPrivateData();
APP.isEditor = Boolean(priv.form_public);
4 years ago
var $body = $('body');
var $toolbarContainer = $('#cp-toolbar');
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) {
if (editable) {
if (APP.mainSortable) {
APP.mainSortable.options.disabled = false;
}
if (!APP.isEditor) { $(offlineEl).remove(); }
$body.removeClass('cp-form-readonly');
$('.cp-form-creator-settings').find('input, button').removeAttr('disabled');
} else {
if (APP.mainSortable) {
APP.mainSortable.options.disabled = true;
}
if (!APP.isEditor) { $('.cp-help-container').before(offlineEl); }
$body.addClass('cp-form-readonly');
$('.cp-form-creator-settings').find('input, button').attr('disabled', 'disabled');
}
});
if (!APP.isEditor) {
framework._.toolbar.alone();
$('.cp-toolbar-icon-history').hide();
$('.cp-toolbar-icon-snapshots').hide();
}
4 years ago
var makeFormSettings = function () {
// Private / public status
var resultsType = h('div.cp-form-results-type-container');
var $results = $(resultsType);
var refreshPublic = function () {
$results.empty();
4 years ago
var makePublic = h('button.btn.btn-secondary', Messages.form_makePublic);
4 years ago
var makePublicDiv = h('div.cp-form-actions', makePublic);
4 years ago
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));
4 years ago
$results.append(makePublicDiv);
var $makePublic = $(makePublic).click(function () {
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 () {
UI.log(Messages.saved);
refreshPublic();
});
});
});
};
refreshPublic();
var responseMsg = h('div.cp-form-response-msg-container');
var $responseMsg = $(responseMsg);
var refreshResponse = function () {
if (true) { return; } // XXX 4.10.0
// $responseMsg.append(btn); // XXX 4.10.0
};
//refreshResponse();
// Allow anonymous answers
var privacyContainer = h('div.cp-form-privacy-container');
var $privacy = $(privacyContainer);
var refreshPrivacy = function () {
$privacy.empty();
var anonymous = content.answers.anonymous;
4 years ago
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);
});
});
4 years ago
$privacy.append(h('div.cp-form-status', Messages.form_anonymous));
$privacy.append(h('div.cp-form-actions', radioContainer));
};
refreshPrivacy();
// 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;
} 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 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();
picker.open();
});
$endDate.append(h('div.cp-form-status', text));
$endDate.append(h('div.cp-form-actions', button));
};
refreshEndDate();
evOnChange.reg(refreshPublic);
evOnChange.reg(refreshPrivacy);
evOnChange.reg(refreshEndDate);
//evOnChange.reg(refreshResponse);
4 years ago
return [
endDateContainer,
privacyContainer,
resultsType,
responseMsg
4 years ago
];
};
var checkIntegrity = function (getter) {
if (!content.order || !content.form) { return; }
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;
4 years ago
var fillerContainer;
if (APP.isEditor) {
4 years ago
var settings = makeFormSettings();
controlContainer = h('div.cp-form-creator-control', [
h('div.cp-form-creator-settings', settings),
]);
4 years ago
fillerContainer = h('div.cp-form-filler-container');
}
var contentContainer = h('div.cp-form-creator-content');
4 years ago
var resultsContainer = h('div.cp-form-creator-results');
var div = h('div.cp-form-creator-container', [
controlContainer,
contentContainer,
4 years ago
resultsContainer,
fillerContainer
]);
return div;
};
var endDateEl = h('div.alert.alert-warning.cp-burn-after-reading');
var endDate;
var endDateTo;
// numbers greater than this overflow the maximum delay for a setTimeout
// which results in it being executed immediately (oops)
// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#maximum_delay_value
var MAX_TIMEOUT_DELAY = 2147483647;
var refreshEndDateBanner = function (force) {
if (APP.isEditor) { return; }
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) {
// calculate how many ms in the future the poll will be closed
var diff = (endDate - +new Date() + 100);
// if that value would overflow, then check again in a day
// (if the tab is still open)
if (diff > MAX_TIMEOUT_DELAY) {
endDateTo = setTimeout(function () {
refreshEndDateBanner(true);
}, 1000 * 3600 * 24);
return;
}
endDateTo = setTimeout(function () {
refreshEndDateBanner(true);
$('.cp-form-send-container').find('.cp-open').hide();
}, diff);
}
};
var showAnonBlockedAlert = function () {
var content = UI.setHTML(h('span.cp-anon-blocked-msg'), Messages.form_anonymous_blocked);
$(content).find('a').click(function (ev) {
ev.preventDefault();
var href = ($(this).attr('href') || '').replace(/\//g, '');
APP.common.setLoginRedirect(href || 'login');
});
UI.alert(content);
};
4 years ago
framework.onReady(function () {
var priv = metadataMgr.getPrivateData();
if (APP.isEditor) {
if (!content.form) {
content.form = {
"1": { type: 'md' },
"2": { type: 'radio' }
};
framework.localChange();
}
if (!content.order) {
content.order = ["1", "2"];
framework.localChange();
}
if (!content.answers || !content.answers.channel || !content.answers.publicKey || !content.answers.validateKey) {
content.answers = {
channel: Hash.createChannelId(),
publicKey: priv.form_public,
validateKey: priv.form_answerValidateKey
};
framework.localChange();
}
checkIntegrity();
}
sframeChan.event('EV_FORM_PIN', {channel: content.answers.channel});
4 years ago
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);
}
var getResults = function (key) {
sframeChan.query("Q_FORM_FETCH_ANSWERS", {
channel: content.answers.channel,
validateKey: content.answers.validateKey,
publicKey: content.answers.publicKey,
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;
}
if (APP.isEditor) {
4 years ago
addResultsButton(framework, content);
sframeChan.query("Q_FORM_FETCH_ANSWERS", {
channel: content.answers.channel,
4 years ago
validateKey: content.answers.validateKey,
publicKey: content.answers.publicKey
4 years ago
}, function (err, 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) {
showAnonBlockedAlert();
}
// 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) {
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;
var curve2 = obj && obj.myKey; // Anonymous answer key
if (answers) {
var myAnswersObj = answers[curve1] || answers[curve2] || undefined;
if (myAnswersObj) {
myAnswers = myAnswersObj.msg;
}
}
// If we have a non-anon answer, we can't answer anonymously later
if (answers[curve1]) { APP.cantAnon = true; }
4 years ago
// Add results button
if (myAnswers) { addResultsButton(framework, content); }
updateForm(framework, content, false, myAnswers);
});
return;
}
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) {
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;
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; }
// Add results button
if (content.answers.privateKey) { addResultsButton(framework, content); }
}
checkIntegrity(false);
updateForm(framework, content, false, answers);
});
});
framework.onContentUpdate(function (newContent) {
content = newContent;
evOnChange.fire();
refreshEndDateBanner();
4 years ago
var answers, temp;
if (!APP.isEditor) { answers = getFormResults(); }
else { temp = getTempFields(); }
updateForm(framework, content, APP.isEditor, answers, temp);
});
framework.setContentGetter(function () {
checkIntegrity(true);
return content;
});
3 years ago
framework.setFileImporter({ accept: ['.json'] }, function (newContent) {
var parsed = JSON.parse(newContent || {});
parsed.answers = content.answers;
return parsed;
});
framework.setFileExporter(['.json'], function(cb, ext) {
Exporter.main(content, cb, ext);
}, true);
};
Framework.create({
toolbarContainer: '#cp-toolbar',
contentContainer: '#cp-app-form-editor',
}, andThen);
});