diff --git a/customize.dist/fonts/cptools/fonts/cptools.svg b/customize.dist/fonts/cptools/fonts/cptools.svg index 776b66ba3..08d0c5040 100644 --- a/customize.dist/fonts/cptools/fonts/cptools.svg +++ b/customize.dist/fonts/cptools/fonts/cptools.svg @@ -3,7 +3,7 @@ Generated by IcoMoon - + @@ -23,9 +23,18 @@ + - + + + + + + + + + \ No newline at end of file diff --git a/customize.dist/fonts/cptools/fonts/cptools.ttf b/customize.dist/fonts/cptools/fonts/cptools.ttf index cfc9d1e3c..88ab53d94 100644 Binary files a/customize.dist/fonts/cptools/fonts/cptools.ttf and b/customize.dist/fonts/cptools/fonts/cptools.ttf differ diff --git a/customize.dist/fonts/cptools/fonts/cptools.woff b/customize.dist/fonts/cptools/fonts/cptools.woff index 80432d647..320f803e6 100644 Binary files a/customize.dist/fonts/cptools/fonts/cptools.woff and b/customize.dist/fonts/cptools/fonts/cptools.woff differ diff --git a/customize.dist/fonts/cptools/style.css b/customize.dist/fonts/cptools/style.css index 7fad69146..9333fa1e7 100644 --- a/customize.dist/fonts/cptools/style.css +++ b/customize.dist/fonts/cptools/style.css @@ -1,9 +1,9 @@ @font-face { font-family: 'cptools'; src: - url('fonts/cptools.ttf?n9y2kz') format('truetype'), - url('fonts/cptools.woff?n9y2kz') format('woff'), - url('fonts/cptools.svg?n9y2kz#cptools') format('svg'); + url('fonts/cptools.ttf?am461j') format('truetype'), + url('fonts/cptools.woff?am461j') format('woff'), + url('fonts/cptools.svg?am461j#cptools') format('svg'); font-weight: normal; font-style: normal; font-display: block; @@ -25,11 +25,35 @@ -moz-osx-font-smoothing: grayscale; } -.cptools-sheet:before { - content: "\e908"; +.cptools-form-list-check:before { + content: "\e916"; } -.cptools-slide:before { - content: "\e907"; +.cptools-form-grid-check:before { + content: "\e917"; +} +.cptools-form-poll:before { + content: "\e910"; +} +.cptools-form-grid-radio:before { + content: "\e918"; +} +.cptools-form-list-radio:before { + content: "\e919"; +} +.cptools-form-page-break:before { + content: "\e91a"; +} +.cptools-form-paragraph:before { + content: "\e91b"; +} +.cptools-form-text:before { + content: "\e91c"; +} +.cptools-form-list-ordered:before { + content: "\e91d"; +} +.cptools-folder-no-color:before { + content: "\e900"; } .cptools-whiteboard:before { content: "\e901"; @@ -37,6 +61,9 @@ .cptools-new-template:before { content: "\e902"; } +.cptools-shared-folder:before { + content: "\e903"; +} .cptools-file-upload:before { content: "\e904"; } @@ -46,9 +73,24 @@ .cptools-poll:before { content: "\e906"; } +.cptools-slide:before { + content: "\e907"; +} +.cptools-sheet:before { + content: "\e908"; +} +.cptools-folder-open:before { + content: "\e909"; +} .cptools-kanban:before { content: "\e90a"; } +.cptools-folder:before { + content: "\e90b"; +} +.cptools-shared-folder-open:before { + content: "\e90c"; +} .cptools-code:before { content: "\e90d"; } @@ -58,8 +100,11 @@ .cptools-file:before { content: "\e90f"; } -.cptools-destroy:before { - content: "\e915"; +.cptools-palette:before { + content: "\e911"; +} +.cptools-folder-upload:before { + content: "\e912"; } .cptools-add-bottom:before { content: "\e913"; @@ -67,24 +112,6 @@ .cptools-add-top:before { content: "\e914"; } -.cptools-folder-upload:before { - content: "\e912"; -} -.cptools-folder-no-color:before { - content: "\e900"; -} -.cptools-shared-folder:before { - content: "\e903"; -} -.cptools-folder-open:before { - content: "\e909"; -} -.cptools-folder:before { - content: "\e90b"; -} -.cptools-shared-folder-open:before { - content: "\e90c"; -} -.cptools-palette:before { - content: "\e911"; +.cptools-destroy:before { + content: "\e915"; } diff --git a/customize.dist/src/less2/include/colortheme-dark.less b/customize.dist/src/less2/include/colortheme-dark.less index 588d1888e..ba80b2d64 100644 --- a/customize.dist/src/less2/include/colortheme-dark.less +++ b/customize.dist/src/less2/include/colortheme-dark.less @@ -10,6 +10,7 @@ code: #EAA000; slide: #e57614; poll: #2c9e98; + form: #2c9e98; whiteboard: #a72ba7; kanban: #8C4; sheet: #40865c; @@ -426,3 +427,13 @@ @cp_calendar-now: @cryptpad_color_brand_300; @cp_calendar-now-fg: @cryptpad_color_grey_800; +// Forms +@cp_form-bg1: @cryptpad_color_grey_800; +@cp_form-bg2: @cryptpad_color_grey_900; +@cp_form-border: @cryptpad_color_grey_800; +@cp_form-poll-color: @cryptpad_color_grey_800; +@cp_form-poll-no: @cryptpad_color_light_red; +@cp_form-poll-yes: @cryptpad_color_light_green; +@cp_form-poll-maybe: @cryptpad_color_light_yellow; +@cp_form-poll-yes-color: @cryptpad_color_green; +@cp_form-invalid: @cryptpad_color_red; diff --git a/customize.dist/src/less2/include/colortheme.less b/customize.dist/src/less2/include/colortheme.less index 3ab249f9f..1102f6074 100644 --- a/customize.dist/src/less2/include/colortheme.less +++ b/customize.dist/src/less2/include/colortheme.less @@ -10,6 +10,7 @@ code: #EAA000; slide: #e57614; poll: #2c9e98; + form: #2c9e98; whiteboard: #a72ba7; kanban: #8C4; sheet: #40865c; @@ -425,3 +426,14 @@ @cp_calendar-border: @cryptpad_color_grey_300; @cp_calendar-now: @cryptpad_color_brand; @cp_calendar-now-fg: @cryptpad_color_grey_200; + +// Forms +@cp_form-bg1: @cryptpad_color_grey_200; +@cp_form-bg2: @cryptpad_color_grey_100; +@cp_form-border: @cryptpad_color_grey_200; +@cp_form-poll-color: @cryptpad_color_grey_800; +@cp_form-poll-no: @cryptpad_color_light_red; +@cp_form-poll-yes: @cryptpad_color_light_green; +@cp_form-poll-maybe: @cryptpad_color_light_yellow; +@cp_form-poll-yes-color: @cryptpad_color_green; +@cp_form-invalid: @cryptpad_color_red; diff --git a/customize.dist/src/less2/include/forms.less b/customize.dist/src/less2/include/forms.less index ffe061fa3..4fb799e13 100644 --- a/customize.dist/src/less2/include/forms.less +++ b/customize.dist/src/less2/include/forms.less @@ -71,6 +71,12 @@ div.cp-button-confirm { display: inline-block; + &.new { + vertical-align: top; + button { + height: 35px; + } + } button { margin: 0 !important; } @@ -85,7 +91,7 @@ } } } - button.cp-button-confirm-placeholder { + button.cp-button-confirm-placeholder:not(.new) { margin-bottom: 3px !important; } diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js index dcfb3872b..ad5f726f8 100644 --- a/www/common/application_config_internal.js +++ b/www/common/application_config_internal.js @@ -12,7 +12,7 @@ define(function() { * You should never remove the drive from this list. */ AppConfig.availablePadTypes = ['drive', 'teams', 'pad', 'sheet', 'code', 'slide', 'poll', 'kanban', 'whiteboard', - /*'doc', 'presentation',*/ 'file', /*'todo',*/ 'contacts' /*, 'calendar' */]; + /*'doc', 'presentation',*/ 'file', /*'todo',*/ 'contacts', 'form']; /* The registered only types are apps restricted to registered users. * You should never remove apps from this list unless you know what you're doing. The apps * listed here by default can't work without a user account. @@ -117,6 +117,7 @@ define(function() { code: 'cptools-code', slide: 'cptools-slide', poll: 'cptools-poll', + form: 'cptools-poll', whiteboard: 'cptools-whiteboard', todo: 'cptools-todo', contacts: 'fa-address-book', diff --git a/www/common/common-hash.js b/www/common/common-hash.js index cf3ca76a4..b9cdfa699 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -34,6 +34,12 @@ var factory = function (Util, Crypto, Keys, Nacl) { var keyPair = Nacl.sign.keyPair.fromSecretKey(privateKey); return Nacl.util.encodeBase64(keyPair.publicKey); }; + Hash.getCurvePublicFromPrivate = function (curvePrivateSafeStr) { + var curvePrivateStr = Crypto.b64AddSlashes(curvePrivateSafeStr); + var privateKey = Nacl.util.decodeBase64(curvePrivateStr); + var keyPair = Nacl.box.keyPair.fromSecretKey(privateKey); + return Nacl.util.encodeBase64(keyPair.publicKey); + }; var getEditHashFromKeys = Hash.getEditHashFromKeys = function (secret) { var version = secret.version; @@ -209,6 +215,17 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app) }); return k ? Crypto.b64AddSlashes(k) : ''; }; + var getAuditorKey = function (hashArr) { + var k; + // Check if we have a ownerKey for this pad + hashArr.some(function (data) { + if (/^auditor=/.test(data)) { + k = data.slice(8); + return true; + } + }); + return k ? Crypto.b64AddSlashes(k) : ''; + }; var getOwnerKey = function (hashArr) { var k; // Check if we have a ownerKey for this pad @@ -231,6 +248,7 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app) parsed.present = options.indexOf('present') !== -1; parsed.embed = options.indexOf('embed') !== -1; parsed.versionHash = getVersionHash(options); + parsed.auditorKey = getAuditorKey(options); parsed.newPadOpts = getNewPadOpts(options); parsed.loginOpts = getLoginOpts(options); parsed.ownerKey = getOwnerKey(options); @@ -272,6 +290,7 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app) present: parsed.present, ownerKey: parsed.ownerKey, versionHash: parsed.versionHash, + auditorKey: parsed.auditorKey, newPadOpts: parsed.newPadOpts, loginOpts: parsed.loginOpts, password: parsed.password @@ -298,6 +317,10 @@ Version 4: Data URL when not a realtime link yet (new pad or "static" app) if (versionHash) { hash += 'hash=' + Crypto.b64RemoveSlashes(versionHash) + '/'; } + var auditorKey = typeof(opts.auditorKey) !== "undefined" ? opts.auditorKey : parsed.auditorKey; + if (auditorKey) { + hash += 'auditor=' + Crypto.b64RemoveSlashes(auditorKey) + '/'; + } if (opts.newPadOpts) { hash += 'newpad=' + opts.newPadOpts + '/'; } if (opts.loginOpts) { hash += 'login=' + opts.loginOpts + '/'; } return hash; diff --git a/www/common/common-interface.js b/www/common/common-interface.js index dbcb089f3..9bdf9ec7a 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -747,6 +747,7 @@ define([ cb = Util.once(cb); } var classes = 'btn ' + (config.classes || 'btn-primary'); + var newCls = config.new ? '.new' : ''; var button = h('button', { "class": classes, @@ -759,7 +760,7 @@ define([ }); var timer = h('div.cp-button-timer', div); - var content = h('div.cp-button-confirm', [ + var content = h('div.cp-button-confirm'+newCls, [ button, timer ]); @@ -795,7 +796,8 @@ define([ to = setTimeout(todo, INTERVAL); }; - $(originalBtn).addClass('cp-button-confirm-placeholder').click(function (e) { + var newCls2 = config.new ? 'new' : ''; + $(originalBtn).addClass('cp-button-confirm-placeholder').addClass(newCls2).click(function (e) { e.stopPropagation(); // If we have a validation function, continue only if it's true if (config.validate && !config.validate()) { return; } @@ -1175,6 +1177,7 @@ define([ var label = h('span.cp-checkmark-label', labelTxt); $mark.keydown(function (e) { + if ($input.is(':disabled')) { return; } if (e.which === 32) { e.stopPropagation(); e.preventDefault(); @@ -1220,20 +1223,22 @@ define([ $.extend(markOpts, opts.mark || {}); var input = h('input', inputOpts); + var $input = $(input); var mark = h('span.cp-radio-mark', markOpts); var label = h('span.cp-checkmark-label', labelTxt); $(mark).keydown(function (e) { + if ($input.is(':disabled')) { return; } if (e.which === 32) { e.stopPropagation(); e.preventDefault(); - if ($(input).is(':checked')) { return; } - $(input).prop('checked', !$(input).is(':checked')); - $(input).change(); + if ($input.is(':checked')) { return; } + $input.prop('checked', !$input.is(':checked')); + $input.change(); } }); - $(input).change(function () { $(mark).focus(); }); + $input.change(function () { $(mark).focus(); }); var radio = h('label', labelOpts, [ input, diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 887eb147a..011bb587b 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -936,7 +936,8 @@ define([ return button; }; - var createMdToolbar = function (common, editor) { + var createMdToolbar = function (common, editor, cfg) { + cfg = cfg || {}; var $toolbar = $('
', { 'class': 'cp-markdown-toolbar' }); @@ -1025,9 +1026,39 @@ define([ icon: 'fa-newspaper-o' } }; + + if (typeof(cfg.embed) === "function") { + actions.embed = { + icon: 'fa-picture-o', + action: function () { + var _cfg = { + types: ['file'], + where: ['root'] + }; + common.openFilePicker(_cfg, function (data) { + if (data.type !== 'file') { + console.log("Unexpected data type picked " + data.type); + return; + } + if (data.type !== 'file') { console.log('unhandled embed type ' + data.type); return; } + common.setPadAttribute('atime', +new Date(), null, data.href); + var privateDat = common.getMetadataMgr().getPrivateData(); + var origin = privateDat.fileHost || privateDat.origin; + var src = data.src = data.src.slice(0,1) === '/' ? origin + data.src : data.src; + cfg.embed($(''), data); + }); + + } + }; + } + var onClick = function () { var type = $(this).attr('data-type'); var texts = editor.getSelections(); + if (actions[type].action) { + return actions[type].action(); + } var newTexts = texts.map(function (str) { str = str || Messages.mdToolbar_defaultText; if (actions[type].apply) { @@ -1054,7 +1085,7 @@ define([ }).appendTo($toolbar); return $toolbar; }; - UIElements.createMarkdownToolbar = function (common, editor) { + UIElements.createMarkdownToolbar = function (common, editor, opts) { var readOnly = common.getMetadataMgr().getPrivateData().readOnly; if (readOnly) { return { @@ -1064,7 +1095,7 @@ define([ }; } - var $toolbar = createMdToolbar(common, editor); + var $toolbar = createMdToolbar(common, editor, opts); var cfg = { title: Messages.mdToolbar_button, element: $toolbar @@ -1133,6 +1164,7 @@ define([ sheet: 'sheets', poll: 'poll', kanban: 'kanban', + form: 'form', whiteboard: 'whiteboard', }; @@ -1472,11 +1504,13 @@ define([ if (config.isSelect) { var pressed = ''; var to; + $container.onChange = Util.mkEvent(); $container.on('click', 'a', function () { value = $(this).data('value'); var $val = $(this); var textValue = $val.html() || value; $button.find('.cp-dropdown-button-title').html(textValue); + $container.onChange.fire(textValue, value); }); $container.keydown(function (e) { var $value = $innerblock.find('[data-value].cp-dropdown-element-active:visible'); @@ -2050,6 +2084,7 @@ define([ AppConfig.registeredOnlyTypes.indexOf(p) !== -1) { return; } return true; }); + Messages.type.form = "Form"; // XXX types.forEach(function (p) { var $element = $('
  • ', { 'class': 'cp-icons-element', @@ -3012,6 +3047,7 @@ define([ // ACCEPT sframeChan.query('Q_SET_PAD_METADATA', { channel: msg.content.channel, + channels: msg.content.channels, command: 'ADD_OWNERS', value: [priv.edPublic] }, function (err, res) { @@ -3061,6 +3097,7 @@ define([ // Remove yourself from the pending owners sframeChan.query('Q_SET_PAD_METADATA', { channel: msg.content.channel, + channels: msg.content.channels, command: 'RM_PENDING_OWNERS', value: [priv.edPublic] }, function (err, res) { @@ -3077,6 +3114,7 @@ define([ // Remove yourself from the pending owners sframeChan.query('Q_SET_PAD_METADATA', { channel: msg.content.channel, + channels: msg.content.channels, command: 'RM_PENDING_OWNERS', value: [priv.edPublic] }, function (err, res) { diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index fd2d29cd2..6c1828897 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -76,7 +76,7 @@ define([ postMessage("GET", { key: ['edPrivate'], }, waitFor(function (obj) { - if (obj.error) { return; } + if (!obj || obj.error) { return; } try { keys.push({ edPrivate: obj, @@ -84,14 +84,16 @@ define([ }); } catch (e) { console.error(e); } })); + // Push teams keys postMessage("GET", { key: ['teams'], }, waitFor(function (obj) { - if (obj.error) { return; } + if (!obj || obj.error) { return; } Object.keys(obj || {}).forEach(function (id) { var t = obj[id]; var _keys = t.keys.drive || {}; + _keys.id = id; if (!_keys.edPrivate) { return; } keys.push(t.keys.drive); }); @@ -101,6 +103,57 @@ define([ }); }; + common.getFormKeys = function (cb) { + var curvePrivate; + var formSeed; + Nthen(function (waitFor) { + postMessage("GET", { + key: ['curvePrivate'], + }, waitFor(function (obj) { + if (!obj || obj.error) { return; } + curvePrivate = obj; + })); + postMessage("GET", { + key: ['form_seed'], + }, waitFor(function (obj) { + if (!obj || obj.error) { return; } + formSeed = obj; + })); + }).nThen(function () { + cb({ + curvePrivate: curvePrivate, + curvePublic: curvePrivate && Hash.getCurvePublicFromPrivate(curvePrivate), + formSeed: formSeed + }); + }); + }; + common.getFormAnswer = function (data, cb) { + postMessage("GET", { + key: ['forms', data.channel], + }, cb); + }; + common.storeFormAnswer = function (data) { + postMessage("SET", { + key: ['forms', data.channel], + value: { + hash: data.hash, + curvePrivate: data.curvePrivate, + anonymous: data.anonymous + } + }, function (obj) { + if (obj && obj.error) { + if (obj.error === "ENODRIVE") { + var answered = JSON.parse(localStorage.CP_formAnswered || "[]"); + if (answered.indexOf(data.channel) === -1) { answered.push(data.channel); } + localStorage.CP_formAnswered = JSON.stringify(answered); + return; + } + console.error(obj.error); + } + }); + + }; + common.makeNetwork = function (cb) { require([ '/bower_components/netflux-websocket/netflux-client.js', @@ -712,6 +765,10 @@ define([ delete meta.chat2; delete meta.chat; delete meta.cursor; + + if (meta.type === "form") { + delete parsed.answers; + } } }; diff --git a/www/common/inner/access.js b/www/common/inner/access.js index 693b8d236..a1d2c37a1 100644 --- a/www/common/inner/access.js +++ b/www/common/inner/access.js @@ -32,6 +32,12 @@ define([ var teamOwner = data.teamId; var title = opts.title; + var p = priv.propChannels; + var otherChan; + if (p && p.answersChannel) { + otherChan = [p.answersChannel]; + } + opts = opts || {}; var redrawAll = function () {}; @@ -255,6 +261,7 @@ define([ // Send the command sframeChan.query('Q_SET_PAD_METADATA', { channel: channel, + channels: otherChan, command: 'ADD_OWNERS', value: toAddTeams.map(function (obj) { return obj.edPublic; }), teamId: teamOwner @@ -290,6 +297,7 @@ define([ // Send the command sframeChan.query('Q_SET_PAD_METADATA', { channel: channel, + channels: otherChan, command: 'ADD_PENDING_OWNERS', value: toAdd, teamId: teamOwner @@ -310,6 +318,7 @@ define([ // Send the command sframeChan.query('Q_SET_PAD_METADATA', { channel: channel, + channels: otherChan, command: 'ADD_OWNERS', value: [priv.edPublic], teamId: teamOwner @@ -338,6 +347,7 @@ define([ if (!friend) { return; } common.mailbox.sendTo("ADD_OWNER", { channel: channel, + channels: otherChan, href: href, calendar: opts.calendar, password: data.password || priv.password, @@ -417,6 +427,12 @@ define([ var allowed = data.allowed || []; var teamOwner = data.teamId; + var p = priv.propChannels; + var otherChan; + if (p && p.answersChannel) { + otherChan = [p.answersChannel]; + } + var redrawAll = function () {}; var addBtn = h('button.btn.btn-primary.cp-access-add', [h('i.fa.fa-arrow-left'), h('i.fa.fa-arrow-up')]); @@ -495,6 +511,7 @@ define([ // Send the command sframeChan.query('Q_SET_PAD_METADATA', { channel: channel, + channels: otherChan, command: 'RM_ALLOWED', value: [ed], teamId: teamOwner @@ -524,6 +541,7 @@ define([ var val = $checkbox.is(':checked'); sframeChan.query('Q_SET_PAD_METADATA', { channel: channel, + channels: otherChan, command: 'RESTRICT_ACCESS', value: [Boolean(val)], teamId: teamOwner @@ -659,6 +677,7 @@ define([ // Send the command sframeChan.query('Q_SET_PAD_METADATA', { channel: channel, + channels: otherChan, command: 'ADD_ALLOWED', value: toAdd, teamId: teamOwner @@ -987,6 +1006,15 @@ define([ UI.findCancelButton().click(); if (err || (obj && obj.error)) { UI.warn(Messages.error); } }); + + // If this is a form wiht a answer channel, delete it too + var p = priv.propChannels; + if (p.answersChannel) { + sframeChan.query('Q_DELETE_OWNED', { + teamId: typeof(owned) !== "boolean" ? owned : undefined, + channel: p.answersChannel + }, function () {}); + } }); if (!opts.noEditPassword) { $d.append(h('br')); } $d.append(h('div', [ @@ -1020,7 +1048,7 @@ define([ var owned = Modal.isOwned(Env, data); // Request edit access - if (common.isLoggedIn() && ((data.roHref && !data.href) || data.fakeHref) && !owned && !opts.calendar) { + if (common.isLoggedIn() && ((data.roHref && !data.href) || data.fakeHref) && !owned && !opts.calendar && priv.app !== 'form') { var requestButton = h('button.btn.btn-secondary.no-margin.cp-access-margin-right', Messages.requestEdit_button); var requestBlock = h('p', requestButton); @@ -1058,7 +1086,7 @@ define([ var canMute = data.mailbox && owned === true && ( (typeof (data.mailbox) === "string" && data.owners[0] === edPublic) || data.mailbox[edPublic]); - if (owned === true && !opts.calendar) { + if (owned === true && !opts.calendar && priv.app !== 'form') { var cbox = UI.createCheckbox('cp-access-mute', Messages.access_muteRequests, !canMute); var $cbox = $(cbox); var spinner = UI.makeSpinner($cbox); diff --git a/www/common/inner/properties.js b/www/common/inner/properties.js index ddbf017fc..c946ed208 100644 --- a/www/common/inner/properties.js +++ b/www/common/inner/properties.js @@ -24,6 +24,7 @@ define([ if (privateData.propChannels) { var p = privateData.propChannels; data.channel = data.channel || p.channel; + data.answersChannel = data.answersChannel || p.answersChannel; data.rtChannel = data.rtChannel || p.rtChannel; data.lastVersion = data.lastVersion || p.lastVersion; data.lastCpHash = data.lastCpHash || p.lastCpHash; @@ -75,6 +76,7 @@ define([ var bytes = 0; var historyBytes; var chan = [data.channel]; + if (data.answersChannel) { chan.push(data.answersChannel); } if (data.rtChannel) { chan.push(data.rtChannel); } if (data.lastVersion) { chan.push(Hash.hrefToHexChannelId(data.lastVersion)); } diff --git a/www/common/inner/share.js b/www/common/inner/share.js index f2eb2e953..5db88b672 100644 --- a/www/common/inner/share.js +++ b/www/common/inner/share.js @@ -494,7 +494,23 @@ define([ var parsed = Hash.parsePadUrl(pathname); var canPresent = ['code', 'slide'].indexOf(parsed.type) !== -1; var versionHash = hashes.viewHash && opts.versionHash; - var canBAR = parsed.type !== 'drive' && !versionHash; + var isForm = parsed.type === "form"; // && opts.auditorHash; + var canBAR = parsed.type !== 'drive' && !versionHash && !isForm; + + var labelEdit = Messages.share_linkEdit; + var labelView = Messages.share_linkView; + + var auditor; + if (isForm) { + Messages.share_formEdit = "Author"; // XXX + Messages.share_formView = "Participant"; // XXX + Messages.share_formAuditor = "Auditor"; // XXX + labelEdit = Messages.share_formEdit; + labelView = Messages.share_formView; + auditor = UI.createRadio('accessRights', 'cp-share-form', Messages.share_formAuditor, false, { + mark: {tabindex:1}, + }); + } var burnAfterReading = (hashes.viewHash && canBAR) ? UI.createRadio('accessRights', 'cp-share-bar', Messages.burnAfterReading_linkBurnAfterReading, false, { @@ -505,12 +521,13 @@ define([ h('label', Messages.share_linkAccess), h('div.radio-group',[ UI.createRadio('accessRights', 'cp-share-editable-false', - Messages.share_linkView, true, { mark: {tabindex:1} }), + labelView, true, { mark: {tabindex:1} }), canPresent ? UI.createRadio('accessRights', 'cp-share-present', Messages.share_linkPresent, false, { mark: {tabindex:1} }) : undefined, UI.createRadio('accessRights', 'cp-share-editable-true', - Messages.share_linkEdit, false, { mark: {tabindex:1} })]), - burnAfterReading + labelEdit, false, { mark: {tabindex:1} }), + auditor]), + burnAfterReading, ]); // Burn after reading @@ -553,6 +570,7 @@ define([ var embed = val.embed; var present = val.present !== undefined ? val.present : Util.isChecked($rights.find('#cp-share-present')); var burnAfterReading = Util.isChecked($rights.find('#cp-share-bar')); + var formAuditor = Util.isChecked($rights.find('#cp-share-form')); if (versionHash) { edit = false; present = false; @@ -569,6 +587,9 @@ define([ } var hash = (!hashes.viewHash || (edit && hashes.editHash)) ? hashes.editHash : hashes.viewHash; + if (formAuditor && opts.auditorHash) { + hash = opts.auditorHash; + } var href = burnAfterReading ? opts.burnAfterReadingUrl : (origin + pathname + '#' + hash); var parsed = Hash.parsePadUrl(href); @@ -594,6 +615,9 @@ define([ $rights.find('#cp-share-present').removeAttr('checked').attr('disabled', true); $rights.find('#cp-share-editable-true').attr('checked', true); } + if (isForm && !opts.auditorHash) { + $rights.find('#cp-share-form').removeAttr('checked').attr('disabled', true); + } var getLink = function () { return $rights.parent().find('#cp-share-link-preview'); diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index cec0d7d73..d13cb09fb 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -112,6 +112,7 @@ define([ Store.set = function (clientId, data, cb) { var s = getStore(data.teamId); if (!s) { return void cb({ error: 'ENOTFOUND' }); } + if (!s.proxy) { return void cb({ error: 'ENODRIVE' }); } var path = data.key.slice(); var key = path.pop(); var obj = Util.find(s.proxy, path); @@ -629,6 +630,7 @@ define([ if (!proxy.uid) { store.noDriveUid = store.noDriveUid || Hash.createChannelId(); } + var metadata = { // "user" is shared with everybody via the userlist user: { @@ -655,7 +657,7 @@ define([ accountName: proxy.login_name || '', offline: store.proxy && store.offline, teams: teams, - plan: account.plan + plan: account.plan, } }; cb(JSON.parse(JSON.stringify(metadata))); @@ -2139,11 +2141,23 @@ define([ if (!data.channel) { return void cb({ error: 'ENOTFOUND'}); } if (!data.command) { return void cb({ error: 'EINVAL' }); } var s = getStore(data.teamId); + var otherChannels = data.channels; + delete data.channels; s.rpc.setMetadata(data, function (err, res) { if (err) { return void cb({ error: err }); } if (!Array.isArray(res) || !res.length) { return void cb({}); } cb(res[0]); }); + // If we have other related channels, send the command for them too + if (Array.isArray(otherChannels)) { + otherChannels.forEach(function (chan) { + var _d = Util.clone(data); + _d.channel = chan; + Store.setPadMetadata(clientId, _d, function () { + + }); + }); + } }; // GET_FULL_HISTORY from sframe-common-outer @@ -2696,7 +2710,12 @@ define([ nThen(function (waitFor) { if (!proxy.settings) { proxy.settings = NEW_USER_SETTINGS; } + if (!proxy.forms) { proxy.forms = {}; } if (!proxy.friends_pending) { proxy.friends_pending = {}; } + // Form seed is used to generate a box encryption keypair when + // answering a form anonymously + if (!proxy.form_seed) { proxy.form_seed = Hash.createChannelId(); } + // Call onCacheReady if the manager is not yet defined if (!manager) { diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index ccb6eabdf..0e799de56 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -818,6 +818,7 @@ define([ _findChannels(Env, toUnpin).forEach(function (id) { var data = _getFileData(Env, id); var arr = [data.channel]; + if (data.answersChannel) { arr.push(data.answersChannel); } if (data.rtChannel) { arr.push(data.rtChannel); } if (data.lastVersion) { arr.push(Hash.hrefToHexChannelId(data.lastVersion)); } Array.prototype.push.apply(toKeep, arr); @@ -1184,6 +1185,10 @@ define([ result.push(otherChan); } } + // Pin form answers channels + if (data.answersChannel && result.indexOf(data.answersChannel) === -1) { + result.push(data.answersChannel); + } // Pin onlyoffice realtime patches if (data.rtChannel && result.indexOf(data.rtChannel) === -1) { result.push(data.rtChannel); diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 119d7fb3d..26ff870f6 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -553,11 +553,13 @@ MessengerUI, Messages, Pages) { if (toolbar.isDeleted) { return void UI.warn(Messages.deletedFromServer); } + var privateData = config.metadataMgr.getPrivateData(); var title = (config.title && config.title.getTitle && config.title.getTitle()) || (config.title && config.title.defaultName) || ""; Common.getSframeChannel().event('EV_SHARE_OPEN', { - title: title + title: title, + auditorHash: privateData.form_auditorHash }); }); diff --git a/www/form/app-form.less b/www/form/app-form.less new file mode 100644 index 000000000..278e1d1db --- /dev/null +++ b/www/form/app-form.less @@ -0,0 +1,566 @@ +@import (reference) '../../customize/src/less2/include/framework.less'; +@import (reference) '../../customize/src/less2/include/tools.less'; +@import (reference) '../../customize/src/less2/include/avatar.less'; + +&.cp-app-form { + @form_input-width: 400px; + + .framework_main( + @bg-color: @colortheme_apps[form] + ); + + display: flex; + flex-flow: column; + font: @colortheme_app-font; + color: @cryptpad_text_col; + background-color: @cp_app-bg; + + #cp-app-form-editor { + flex: 1; + display: flex; + flex-flow: row; + height: 100%; + overflow: hidden; + } + + &.cp-app-form-results { + div.cp-form-creator-content, .cp-app-form-button-results { + display: none !important; + } + } + &:not(.cp-app-form-results) { + div.cp-form-creator-results, .cp-app-form-button-creator { + display: none !important; + } + } + + #cp-app-form-container { + display: flex; + flex: 1; + justify-content: center; + min-width: 300px; + + .cp-form-input-block { + display: flex; + } + + div.cp-form-creator-container { + display: flex; + flex: 1; + justify-content: center; + min-width: 300px; + //flex-wrap: wrap; + overflow: auto; + + @media screen and (max-width: 1000px) { + flex-wrap: wrap; + justify-content: flex-start; + .cp-form-creator-control { + width: 100% !important; + .cp-form-creator-settings { + display: flex; + justify-content: space-evenly; + flex-wrap: wrap; + } + } + } + + .cp-form-creator-settings { + padding: 30px; + & > div:not(:last-child) { + margin-bottom: 20px; + } + } + div.cp-form-filler-container { + width: 300px; + min-width: 0; + flex: 0 300 300px; + } + div.cp-form-creator-control { + padding: 10px; + display: flex; + flex-flow: column; + width: 300px; + .cp-form-creator-types { + margin-top: 20px; + display: flex; + flex-flow: column; + } + } + div.cp-form-creator-content, div.cp-form-creator-results { + max-width: 1000px; + min-width: 300px; + padding: 10px; + display: flex; + flex-flow: column; + flex: 1 1 1000px; + overflow: auto; + + .cp-form-creator-add-inline { + display: flex; + flex-flow: row; + align-items: center; + margin-bottom: 20px; + button { + width: 50px; + i { + margin-right: 0; + } + } + .cp-form-creator-inline-add { + font-size: 25px; + margin-right: 30px; + .add-close { display: none; } + &.displayed { + .add-close { display: inline; } + .add-open { display: none; } + } + } + .cp-form-creator-control-inline { + display: flex; + justify-content: space-around; + button:not(:last-child) { + margin-right: 5px; + } + .cp-form-creator-types { + .btn-default { + background: transparent; + &:hover, &:not(:disabled):active, &:focus { + background-color: @cp_buttons-default; + } + } + button { + border: 0px; + //padding-bottom: 3px; + i { + font-size: 35px; + line-height: 35px; + } + } + &:first-child { + margin-right: 50px; + } + } + } + } + .cp-form-creator-add-full { + display: flex; + align-items: center; + margin: 50px 0px 100px 0px; + &> div:first-child { + border-right: 1px solid fade(@cryptpad_text_col, 25%); + display: flex; + height: 100%; + align-items: center; + padding-right: 10px; + margin-right: 10px; + i { + color: fade(@cryptpad_text_col, 25%); + font-size: 30px; + } + } + .cp-form-creator-control-inline { + display: flex; + flex-flow: column; + justify-content: space-around; + button:not(:last-child) { + margin-right: 5px; + } + .cp-form-creator-types { + .btn-default { + background: transparent; + &:hover, &:not(:disabled):active, &:focus { + background-color: @cp_buttons-default; + } + } + button { + border: 0px; + padding:5px; + margin-right: 10px; + i { + font-size: 35px; + line-height: 35px; + } + } + &:first-child { + margin-bottom: 20px; + margin-right: 50px; + } + } + } + } + + .cp-form-page + .cp-form-send-container { + margin-top: 10px; + } + + .cp-form-page-container { + display: flex; + justify-content: center; + margin: 10px 0; + & > span { + margin: 0 20px; + width: 100px; + display: inline-flex; + align-items: center; + justify-content: center; + } + button { + &.cp-next { + .fa { + margin-right: 0; + margin-left: 5px; + } + } + } + } + .cp-form-block { + .tools_unselectable(); + background: @cp_form-bg1; + padding: 10px; + &:not(:last-child) { + margin-bottom: 20px; + } + + .cp-form-block-drag-handle { + display: flex; + flex-flow: column; + align-items: center; + color: @cp_sidebar-hint; + i { + cursor: grab; + &:first-child { + height: 3px; + margin-top: -10px; + margin-bottom: 1px; + } + } + } + + &.sortable-ghost { visibility: hidden; } + &.sortable-drag { opacity: 0.9 !important; } + + .cp-form-block-question { + margin-bottom: 5px; + } + .cp-form-block-content { + overflow-x: auto; + .cp-form-page-break-edit { + text-align: center; + padding: 10px; + i { + margin-right: 5px; + } + } + .cp-form-edit-buttons-container { + margin-top: 20px; + display: flex; + justify-content: space-between; + } + input:invalid { + border: 1px solid @cp_form-invalid; + } + } + .cp-form-input-block { + //width: @form_input-width; + padding-bottom: 10px; + border-bottom: 2px solid @cp_sidebar-hint; + margin-bottom: 10px; + &:not(.editing) { + input { + background: transparent; + border: none; + padding: 0 !important; + & ~ button:not(:disabled) { + .cp-form-edit { display: inline; } + .cp-form-save { display: none; } + } + } + } + input { + flex: 1; + min-width: 100px; + padding: 0 10px !important; + height: auto; + font-size: 20px; + } + button { + .cp-form-edit { + display: none; + } + .cp-form-save { display: inline; } + } + .cp-form-block-drag { + font-size: 22px; + width: 20px; + margin-left: 5px; + text-align: center; + line-height: 31px; + } + } + &.editable { + cursor: grab; + .cp-form-edit-save { + margin-top: 20px; + button { + margin-right: 10px; + } + } + .cp-form-edit-type { + margin-bottom: 10px; + .cp-dropdown-container { + margin-left: 10px; + } + } + } + } + .cp-form-edit-max-options { + display: flex; + align-items: center; + margin-bottom: 10px; + input { + width: 100px; + margin-left: 10px; + } + } + .cp-form-edit-options-block { + display: flex; + flex-wrap: wrap; + align-items: baseline; + .CodeMirror { + cursor: default; + flex: 1; + margin: auto; + min-width: 80%; + width: 80%; + min-height: 200px; + height: 200px; + border: 1px solid @cp_forms-border; + .CodeMirror-placeholder { + color: #777; + } + } + } + .cp-form-edit-block { + + button.btn-secondary { + margin-left: 30px; + } + .cp-form-handle { + display: flex; + align-items: center; + justify-content: center; + width: 30px; + color: @cp_sidebar-hint; + i:first-child { + margin-right: 1px; + } + } + .cp-form-edit-block-input { + margin-bottom: 5px; // XXX DB margin bug + &.sortable-ghost { visibility: hidden; } + &.sortable-drag { opacity: 0.9 !important; } + display: flex; + width: 400px; + input { + flex: 1; + min-width: 100px; + border-color: @cryptpad_text_col; + border-right: 0px; + } + button { + i { margin: 0 !important; } + } + + } + } + } + } + div.cp-form-creator-results { + display: flex; + flex-flow: column; + position: relative; + & > div { + background: @cp_form-bg1; + padding: 10px; + &:not(:last-child) { + margin-bottom: 20px; + } + } + .cp-form-block-question { + margin-bottom: 5px; + } + .cp-form-block-type { + float: right; + padding: 5px; + margin-top: -10px; + margin-right: -10px; + i { margin-right: 5px; } + background: @cp_form-bg2; + } + .cp-form-results-type-text { + max-height: 300px; + overflow: auto; + .cp-form-results-type-text-data { + padding: 5px 10px; + background: @cp_form-bg2; + &:not(:last-child) { margin-bottom: 1px; } + } + } + .cp-form-results-type-textarea-data { + white-space: pre-wrap; + font-size: 14px; + border: 1px solid @cp_profile-hint; + padding: 0 5px; + } + .cp-form-results-type-radio { + display: table; + .cp-form-results-type-multiradio-data { + display: flex; + flex-flow: column; + } + .cp-form-results-type-radio-data { + display: table-row; + border: 1px solid @cp_form-border; + & > span { + border: 1px solid @cp_form-border; + display: table-cell; + padding: 5px 10px; + background: @cp_form-bg2; + &.cp-value { + min-width: 200px; + } + } + } + } + .cp-form-individual { + & > *:not(:last-child) { + margin-right: 10px; + } + .cp-form-warning { + color: @cp-limit-bar-warning; + } + .cp-form-friend { + color: @cp_profile-hint; + .fa { + margin-right: 5px; + } + } + } + } + } + + .cp-form-type-radio, .cp-form-type-checkbox { + display: flex; + flex-flow: column; + align-items: baseline; + .cp-radio { + 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; + } + } + } + } + .cp-form-type-sort { + cursor: grab; + padding: 2px; + .cp-form-handle { + margin-right: 5px; + } + .cp-form-sort-order { + border: 1px solid @cp_profile-hint; + padding: 0 5px; + margin-right: 5px; + } + } + + .cp-form-type-poll { + display: inline-flex; + flex-flow: column; + & > div { + display: flex; + } + .cp-poll-cell { + width: 100px; + height: 40px; + display: inline-flex; + align-items: center; + justify-content: center; + &:first-child { + width: 200px; + } + button { + width: 100%; + } + } + .cp-poll-time-day { + flex-basis: 100px; + border-right: 1px solid @cryptpad_text_col; + border-left: 1px solid @cryptpad_text_col; + border-top: 1px solid @cryptpad_text_col; + } + &.cp-form-poll-switch { + flex-flow: row; + & > div { + flex-flow: column; + } + .cp-poll-cell:not(.cp-poll-switch) { + &:first-child { + width: 100px; + } + } + .cp-form-poll-option, .cp-poll-switch { + width: 200px; + } + .cp-poll-time-day { + flex-basis: 40px; + border-right: none; + border-bottom: 1px solid @cryptpad_text_col; + border-left: 1px solid @cryptpad_text_col; + border-top: 1px solid @cryptpad_text_col; + } + } + .cp-form-poll-choice, .cp-form-poll-answer { + .fa { + display: none; + } + color: @cp_form-poll-color; + &[data-value="0"] { + background: @cp_form-poll-no; + .cp-no { display: inline; } + } + &[data-value="1"] { + background: @cp_form-poll-yes; + .cp-yes { display: inline; } + } + &[data-value="2"] { + background: @cp_form-poll-maybe; + .cp-maybe { display: inline; } + } + } + div.cp-form-poll-choice { + cursor: pointer; + padding: 5px; + border: 5px double @cp_form-bg1; + } + div.cp-form-poll-answer { + color: @cp_form-poll-yes-color; + } + } + +} + diff --git a/www/form/index.html b/www/form/index.html new file mode 100644 index 000000000..96a3cce86 --- /dev/null +++ b/www/form/index.html @@ -0,0 +1,12 @@ + + + + CryptPad + + + + + + + + diff --git a/www/form/inner.html b/www/form/inner.html new file mode 100644 index 000000000..de37af4f6 --- /dev/null +++ b/www/form/inner.html @@ -0,0 +1,20 @@ + + + + + + + + +
    +
    +
    +
    + + + diff --git a/www/form/inner.js b/www/form/inner.js new file mode 100644 index 000000000..fcf1cc349 --- /dev/null +++ b/www/form/inner.js @@ -0,0 +1,2547 @@ +define([ + 'jquery', + 'json.sortify', + '/bower_components/chainpad-crypto/crypto.js', + '/common/sframe-app-framework.js', + '/common/toolbar.js', + '/bower_components/nthen/index.js', + '/common/sframe-common.js', + '/common/common-util.js', + '/common/common-hash.js', + '/common/common-interface.js', + '/common/common-ui-elements.js', + '/common/clipboard.js', + '/common/inner/common-mediatag.js', + '/common/hyperscript.js', + '/customize/messages.js', + '/customize/application_config.js', + '/common/diffMarked.js', + '/common/sframe-common-codemirror.js', + 'cm/lib/codemirror', + + '/common/inner/share.js', + '/common/inner/access.js', + '/common/inner/properties.js', + + '/lib/datepicker/flatpickr.js', + '/bower_components/sortablejs/Sortable.min.js', + + 'cm/addon/display/placeholder', + 'cm/mode/markdown/markdown', + 'css!cm/lib/codemirror.css', + + 'css!/bower_components/codemirror/lib/codemirror.css', + 'css!/bower_components/codemirror/addon/dialog/dialog.css', + 'css!/bower_components/codemirror/addon/fold/foldgutter.css', + 'css!/lib/datepicker/flatpickr.min.css', + 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', + 'less!/form/app-form.less', +], function ( + $, + Sortify, + Crypto, + Framework, + Toolbar, + nThen, + SFCommon, + Util, + Hash, + UI, + UIElements, + Clipboard, + MT, + h, + Messages, + AppConfig, + DiffMd, + SFCodeMirror, + CMeditor, + Share, Access, Properties, + Flatpickr, + Sortable + ) +{ + var APP = window.APP = { + }; + + var is24h = false; + var dateFormat = "Y-m-d H:i"; + var timeFormat = "H:i"; + try { + is24h = !new Intl.DateTimeFormat(navigator.language, { hour: 'numeric' }).format(0).match(/AM/); + } catch (e) {} + is24h = false; + if (!is24h) { + dateFormat = "Y-m-d h:i K"; + timeFormat = "h:i K"; + } + + Messages.button_newform = "New Form"; // XXX + Messages.form_invalid = "Invalid form"; + Messages.form_editBlock = "Edit"; + Messages.form_editMax = "Max selectable options"; + Messages.form_editMaxLength = "Maximum characters"; + Messages.form_editType = "Options type"; + + Messages.form_poll_text = "Text"; + Messages.form_poll_day = "Day"; + Messages.form_poll_time = "Time"; + + + Messages.form_textType = "Text type"; + Messages.form_text_text = "Text"; + Messages.form_text_url = "URL"; + Messages.form_text_email = "Email"; + Messages.form_text_number = "Number"; + + Messages.form_default = "Your question here?"; + Messages.form_type_input = "Text"; // XXX + Messages.form_type_textarea = "Textarea"; // XXX + Messages.form_type_radio = "Radio"; // XXX + Messages.form_type_multiradio = "Multiline Radio"; // XXX + Messages.form_type_checkbox = "Checkbox"; // XXX + Messages.form_type_multicheck = "Multiline Checkbox"; // XXX + Messages.form_type_poll = "Poll"; // XXX + Messages.form_type_sort = "Ordered list"; // XXX + + Messages.form_type_md = "Description"; // XXX + Messages.form_type_page = "Page break"; // XXX + + Messages.form_description_default = "Your text here"; + + Messages.form_duplicates = "Duplicate entries have been removed"; + Messages.form_maxOptions = "{0} answer(s) max"; + + Messages.form_submit = "Submit"; + Messages.form_update = "Update"; + Messages.form_reset = "Reset"; + Messages.form_sent = "Sent"; + Messages.form_delete = "Delete"; + + Messages.form_cantFindAnswers = "Unable to retrieve your existing answers for this form."; + Messages.form_answered = "You already answered this form"; + + Messages.form_results = "Responses"; + Messages.form_editor = "Editor"; + Messages.form_form = "Form"; + Messages.form_viewResults = "Go to responses"; + Messages.form_viewCreator = "Go to form creator"; + Messages.form_showIndividual = "Show individual answers"; + Messages.form_showSummary = "Show summary"; + Messages.form_answerAnonymous = "Anonymous answer on {0}"; + Messages.form_viewButton = "View"; + Messages.form_backButton = "Back"; + Messages.form_answerName = "Answer from {0} on {1}"; + Messages.form_answerWarning = "Unconfirmed identity"; + + Messages.form_notAnswered = "And {0} empty answers"; + + Messages.form_makePublic = "Publish results"; + Messages.form_makePublicWarning = "Are you sure you want to make the results of this form public? This can't be undone."; + Messages.form_isPublic = "Results are public"; + Messages.form_isPrivate = "Results are private"; + + Messages.form_open = "Open"; + Messages.form_setEnd = "Set closing date"; + Messages.form_removeEnd = "Remove closing date"; + Messages.form_isOpen = "This form is open"; + Messages.form_isClosed = "This form was closed on {0}"; + Messages.form_willClose = "This form will close on {0}"; + + Messages.form_anonymous = "Anonymous answers"; + Messages.form_anonymous_on = "Allowed"; + Messages.form_anonymous_off = "Blocked"; + Messages.form_anonymous_blocked = "Anonymous responses are blocked for this form. You must log in or register to submit answers."; + + Messages.form_defaultOption = "Option {0}"; + 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"; + Messages.form_addMultiple = "Add all"; + Messages.form_clear = "Clear"; + + Messages.form_page_prev = "Previous"; + Messages.form_page = "Page {0}/{1}"; + Messages.form_page_next = "Next"; + + Messages.form_anonymousBox = "Answer anonymously"; + + var MAX_OPTIONS = 15; // XXX + var MAX_ITEMS = 10; // XXX + + var saveAndCancelOptions = function (getRes, cb) { + // Cancel changes + var cancelBlock = h('button.btn.btn-secondary', Messages.cancel); + $(cancelBlock).click(function () { cb(); }); + + // Save changes + var saveBlock = h('button.btn.btn-primary', [ + h('i.fa.fa-floppy-o'), + h('span', Messages.settings_save) + ]); + + $(saveBlock).click(function () { + $(saveBlock).attr('disabled', 'disabled'); + cb(getRes()); + }); + + return h('div.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) { + var options = ['text', 'number', 'url', 'email'].map(function (t) { + return { + tag: 'a', + attributes: { + 'class': 'cp-form-type-value', + 'data-value': t, + 'href': '#', + }, + content: Messages['form_text_'+t] + }; + }); + var dropdownConfig = { + text: '', // Button initial text + options: options, // Entries displayed in the menu + //left: true, // Open to the left of the button + //container: $(type), + isSelect: true, + caretDown: true, + buttonCls: 'btn btn-secondary' + }; + typeSelect = UIElements.createDropdown(dropdownConfig); + typeSelect.setValue(opts.type); + + type = h('div.cp-form-edit-type', [ + h('span', Messages.form_textType), + typeSelect[0] + ]); + } + + setCursorGetter(function () { + return { + old: (tmp && tmp.old) || opts, + content: { + maxLength: getLengthVal ? getLengthVal() : undefined, + type: typeSelect ? typeSelect.getValue() : undefined + } + }; + }); + + var getSaveRes = function () { + return { + maxLength: getLengthVal ? getLengthVal() : undefined, + type: typeSelect ? typeSelect.getValue() : undefined + }; + }; + var saveAndCancel = saveAndCancelOptions(getSaveRes, cb); + + return [ + maxLength, + type, + saveAndCancel + ]; + }; + var editOptions = function (v, setCursorGetter, cb, tmp) { + var add = h('button.btn.btn-secondary', [ + h('i.fa.fa-plus'), + 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; + 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 + ]); + } + + var type, typeSelect; + if (v.type) { + var options = ['text', 'day', 'time'].map(function (t) { + return { + tag: 'a', + attributes: { + 'class': 'cp-form-type-value', + 'data-value': t, + 'href': '#', + }, + content: Messages['form_poll_'+t] + }; + }); + var dropdownConfig = { + text: '', // Button initial text + options: options, // Entries displayed in the menu + //left: true, // Open to the left of the button + //container: $(type), + isSelect: true, + caretDown: true, + buttonCls: 'btn btn-secondary' + }; + typeSelect = UIElements.createDropdown(dropdownConfig); + typeSelect.setValue(v.type); + + type = h('div.cp-form-edit-type', [ + h('span', Messages.form_editType), + typeSelect[0] + ]); + } + + // Show existing options + var $add, $addItem; + var addMultiple; + var getOption = function (val, isItem, uid) { + var input = h('input', {value:val}); + if (uid) { $(input).data('uid', uid); } + + // 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; } + 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-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 () { + $(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(); } + } + }); + 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", + 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", + 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 + }); + } + + // Calendar time + if (v.type) { + var multipleInput = h('input'); + var multipleClearButton = h('button.btn', Messages.form_clear); + var addMultipleButton = h('button.btn', [ + h('i.fa.fa-plus'), + h('span', Messages.form_addMultiple) + ]); + addMultiple = h('div', { style: "display: none;" }, [ + multipleInput, + addMultipleButton, + multipleClearButton + ]); + var multiplePickr = Flatpickr(multipleInput, { + mode: 'multiple', + enableTime: true, + dateFormat: dateFormat, + }); + $(multipleClearButton).click(function () { + multiplePickr.clear(); + }); + $(addMultipleButton).click(function () { + multiplePickr.selectedDates.some(function (date) { + $add.before(getOption(date, false)); + var l = $container.find('input').length; + $(maxInput).attr('max', l); + if (l >= MAX_OPTIONS) { + $add.hide(); + $(addMultiple).hide(); + return true; + } + }); + multiplePickr.clear(); + }); + } + + var refreshView = function () { + if (!v.type) { return; } + var $calendar = $(calendarView); + if (v.type !== "day") { + $calendar.hide(); + $container.show(); + var l = $container.find('input').length; + if (v.type === "time" && l < MAX_OPTIONS) { + $(addMultiple).show(); + } else { + $(addMultiple).hide(); + } + } else { + $calendar.show(); + $container.hide(); + } + }; + refreshView(); + + // Doodle type change: empty current values and change input types? + if (typeSelect) { + typeSelect.onChange.reg(function (prettyVal, val) { + v.type = val; + refreshView(); + if (val !== "text") { + $container.find('.cp-form-edit-block-input').remove(); + return; + } + $container.find('input').each(function (i, input) { + if (input._flatpickr) { + 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(); } + + // 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) { + 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; + } + return { + old: (tmp && tmp.old) || v, + content: _content, + 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 (values.indexOf(val) === -1) { values.push(val); } + else { duplicates = true; } + }); + } + if (!values.length) { + return void UI.warn(Messages.error); // XXX error message: no values + } + var res = { values: values }; + + // If multiline block, get items + 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 makePollTable = function (answers, opts) { + // Sort date values + if (opts.type !== "text") { + opts.values.sort(function (a, b) { + return +new Date(a) - +new Date(b); + }); + } + // Create first line with options + var els = opts.values.map(function (data) { + if (opts.type === "day") { + var _date = new Date(data); + data = _date.toLocaleDateString(); + } + if (opts.type === "time") { + var _dateT = new Date(data); + data = Flatpickr.formatDate(_dateT, timeFormat); + } + return h('div.cp-poll-cell.cp-form-poll-option', data); + }); + // Insert axis switch button + var switchAxis = h('button.btn.btn-default', [ + 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] = _days[day] || 0; + _days[day]++; + }); + Object.keys(_days).forEach(function (day) { + days.push(h('div.cp-poll-cell.cp-poll-time-day', { + style: 'flex-grow:'+(_days[day]-1)+';' + }, day)); + }); + lines.unshift(h('div', days)); + } + + // Add answers + if (Array.isArray(answers)) { + answers.forEach(function (answer) { + if (!answer.name || !answer.values) { return; } + var _name = answer.name; + var values = answer.values || {}; + var els = opts.values.map(function (data) { + var res = values[data] || 0; + var v = (Number(res) === 1) ? h('i.fa.fa-check.cp-yes') : undefined; + var cell = h('div.cp-poll-cell.cp-form-poll-answer', { + 'data-value': res + }, v); + return cell; + }); + els.unshift(h('div.cp-poll-cell.cp-poll-answer-name', _name)); + lines.push(h('div', els)); + }); + } + + var $s = $(switchAxis).click(function () { + $s.closest('.cp-form-type-poll').toggleClass('cp-form-poll-switch'); + }); + + return lines; + }; + + var getEmpty = function (empty) { + if (empty) { + return UI.setHTML(h('div.cp-form-results-type-text-empty'), Messages._getKey('form_notAnswered', [empty])); + } + }; + + 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 getBlockAnswers = function (answers, uid, filterCurve) { + return Object.keys(answers || {}).map(function (user) { + if (filterCurve && user === filterCurve) { return; } + try { + return answers[user].msg[uid]; + } catch (e) { console.error(e); } + }).filter(Boolean); + }; + + var STATIC_TYPES = { + md: { + defaultOpts: { + 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); + 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') + }; + } + return { + old: opts, + content: getContent(), + cursor: cursor + }; + }; + + return [ + block, + h('div.cp-form-edit-save', [cancelBlock, saveBlock]) + ]; + }, + getCursor: function () { return cursorGetter(); }, + }; + }, + printResults: function () { return; }, + icon: h('i.cptools.cptools-form-paragraph') + }, + page: { + get: function () { + var tag = h('div.cp-form-page-break-edit', [ + h('i.cptools.cptools-form-page-break'), + h('span', Messages.form_type_page) + ]); + return { + tag: tag, + pageBreak: true + }; + }, + printResults: function () { return; }, + icon: h('i.cptools.cptools-form-page-break') + }, + }; + var TYPES = { + input: { + defaultOpts: { + type: 'text' + }, + get: function (opts, a, n, evOnChange) { + if (!opts) { opts = TYPES.input.defaultOpts; } + var tag = h('input', { + type: opts.type + }); + var $tag = $(tag); + $tag.on('change keypress', Util.throttle(function () { + evOnChange.fire(); + }, 500)); + var cursorGetter; + var setCursorGetter = function (f) { cursorGetter = f; }; + return { + tag: tag, + getValue: function () { + var invalid = $tag.is(':invalid'); + if (invalid) { return; } // XXX invalid answers are ignored? + return $tag.val(); + }, + setValue: function (val) { $tag.val(val); }, + edit: function (cb, tmp) { + var v = Util.clone(opts); + return editTextOptions(v, setCursorGetter, cb, tmp); + }, + getCursor: function () { return cursorGetter(); }, + reset: function () { $tag.val(''); } + }; + }, + printResults: function (answers, uid) { + var results = []; + var empty = 0; + Object.keys(answers).forEach(function (author) { + var obj = answers[author]; + var answer = obj.msg[uid]; + if (!answer || !answer.trim()) { return empty++; } + results.push(h('div.cp-form-results-type-text-data', answer)); + }); + results.push(getEmpty(empty)); + + return h('div.cp-form-results-type-text', results); + }, + icon: h('i.cptools.cptools-form-text') + }, + textarea: { + defaultOpts: { + maxLength: 1000 + }, + get: function (opts, a, n, evOnChange) { + if (!opts) { opts = TYPES.textarea.defaultOpts; } + var tag = h('textarea', {maxlength: opts.maxLength}); + var $tag = $(tag); + $tag.on('change keypress', Util.throttle(function () { + evOnChange.fire(); + }, 500)); + var cursorGetter; + var setCursorGetter = function (f) { cursorGetter = f; }; + return { + tag: tag, + getValue: function () { return $tag.val(); }, + setValue: function (val) { $tag.val(val); }, + edit: function (cb, tmp) { + var v = Util.clone(opts); + return editTextOptions(v, setCursorGetter, cb, tmp); + }, + getCursor: function () { return cursorGetter(); }, + reset: function () { $tag.val(''); } + }; + }, + printResults: function (answers, uid) { + var results = []; + var empty = 0; + Object.keys(answers).forEach(function (author) { + var obj = answers[author]; + var answer = obj.msg[uid]; + if (!answer || !answer.trim()) { return empty++; } + results.push(h('div.cp-form-results-type-textarea-data', answer)); + }); + results.push(getEmpty(empty)); + + return h('div.cp-form-results-type-text', results); + }, + icon: h('i.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); + 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; + 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'); }, + edit: function (cb, tmp) { + var v = Util.clone(opts); + return editOptions(v, setCursorGetter, cb, tmp); + }, + 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; + } + }); + } + }; + + }, + printResults: function (answers, uid) { + var results = []; + var empty = 0; + var count = {}; + Object.keys(answers).forEach(function (author) { + var obj = answers[author]; + var answer = obj.msg[uid]; + if (!answer || !answer.trim()) { return empty++; } + count[answer] = count[answer] || 0; + count[answer]++; + }); + Object.keys(count).forEach(function (value) { + results.push(h('div.cp-form-results-type-radio-data', [ + h('span.cp-value', value), + h('span.cp-count', count[value]) + ])); + }); + results.push(getEmpty(empty)); + + return h('div.cp-form-results-type-radio', results); + }, + icon: h('i.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); + l.forEach(function (el) { + var $el = $(el); + var uid = $el.attr('data-uid'); + $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 opts = structure.opts || TYPES.multiradio.defaultOpts; + var q = findItem(opts.items, q_uid); + var c = count[q_uid]; + var values = Object.keys(c).map(function (res) { + return h('div.cp-form-results-type-radio-data', [ + 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.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 = []; + 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) { + 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.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); + l.forEach(function (el) { + var $el = $(el); + var uid = $el.attr('data-uid'); + res[uid] = []; + $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.cptools.cptools-form-grid-check') + }, + 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); + + // 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'), + h('i.fa.fa-question.cp-maybe'), + ]); + var $c = $(cell); + $c.data('option', data); + var val = 0; + $c.attr('data-value', val); + $c.click(function () { + val = (val+1)%3; + $c.attr('data-value', val); + 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 }); + addLine.unshift(h('div.cp-poll-cell', nameInput)); + lines.push(h('div', addLine)); + + var tag = h('div.cp-form-type-poll', lines); + var $tag = $(tag); + + var cursorGetter; + var setCursorGetter = function (f) { cursorGetter = f; }; + return { + tag: tag, + getValue: function () { + var res = {}; + var name = $(nameInput).val().trim() || Messages.anonymous; + $tag.find('.cp-form-poll-choice').each(function (i, el) { + var $el = $(el); + res[$el.data('option')] = $el.attr('data-value'); + }); + return { + name: name, + values: res + }; + }, + reset: function () { + $tag.find('.cp-form-poll-choice').attr('data-value', 0); + }, + edit: function (cb, tmp) { + var v = Util.clone(opts); + return editOptions(v, setCursorGetter, cb, tmp); + }, + getCursor: function () { return cursorGetter(); }, + setValue: function (res) { + this.reset(); + if (!res || !res.values || !res.name) { return; } + var val = res.values; + $(nameInput).val(res.name); + $tag.find('.cp-form-poll-choice').each(function (i, el) { + if (!el._setValue) { return; } + var $el = $(el); + el._setValue(val[$el.data('option')] || 0); + }); + } + }; + + }, + printResults: function (answers, uid, form) { + var _answers = getBlockAnswers(answers, uid); + var lines = makePollTable(_answers, form[uid].opts); + return h('div.cp-form-type-poll', lines); + }, + icon: h('i.cptools.cptools-form-poll') + }, + sort: { + defaultOpts: { + values: [1,2].map(function (i) { + return Messages._getKey('form_defaultOption', [i]); + }) + }, + get: function (opts, a, n, evOnChange) { + if (!opts) { opts = TYPES.radio.defaultOpts; } + if (!Array.isArray(opts.values)) { return; } + var map = {}; + var invMap = {}; + var els = opts.values.map(function (data, i) { + var uid = Util.uid(); + map[uid] = data; + invMap[data] = uid; + var div = h('div.cp-form-type-sort', {'data-id': uid}, [ + h('span.cp-form-handle', [ + h('i.fa.fa-ellipsis-v'), + h('i.fa.fa-ellipsis-v'), + ]), + h('span.cp-form-sort-order', (i+1)), + h('span', data) + ]); + $(div).data('val', data); + return div; + }); + var tag = h('div.cp-form-type-sort-container', els); + var $tag = $(tag); + var reorder = function () { + $tag.find('.cp-form-type-sort').each(function (i, el) { + $(el).find('.cp-form-sort-order').text(i+1); + }); + }; + var cursorGetter; + var setCursorGetter = function (f) { cursorGetter = f; }; + + var sortable = Sortable.create(tag, { + direction: "vertical", + draggable: ".cp-form-type-sort", + forceFallback: true, + store: { + set: function () { + evOnChange.fire(); + reorder(); + } + } + }); + + $(tag).find('input[type="radio"]').on('change', function () { + evOnChange.fire(); + }); + return { + tag: tag, + getValue: function () { + return sortable.toArray().map(function (id) { + return map[id]; + }); + }, + reset: function () { + var toSort = (opts.values).map(function (val) { + return invMap[val]; + }); + sortable.sort(toSort); + reorder(); + }, + edit: function (cb, tmp) { + var v = Util.clone(opts); + return editOptions(v, setCursorGetter, cb, tmp); + }, + getCursor: function () { return cursorGetter(); }, + setValue: function (val) { + var toSort = (val || []).map(function (val) { + return invMap[val]; + }); + sortable.sort(toSort); + reorder(); + } + }; + + }, + printResults: function (answers, uid, form) { + var opts = form[uid].opts || TYPES.radio.defaultOpts; + var l = (opts.values || []).length; + var results = []; + var empty = 0; + var count = {}; + Object.keys(answers).forEach(function (author) { + var obj = answers[author]; + var answer = obj.msg[uid]; + if (!Array.isArray(answer) || !answer.length) { return empty++; } + answer.forEach(function (el, i) { + var score = l - i; + count[el] = (count[el] || 0) + score; + }); + }); + var sorted = Object.keys(count).sort(function (a, b) { + return count[b] - count[a]; + }); + sorted.forEach(function (value) { + results.push(h('div.cp-form-results-type-radio-data', [ + h('span.cp-value', value), + h('span.cp-count', count[value]) + ])); + }); + results.push(getEmpty(empty)); + + return h('div.cp-form-results-type-radio', results); + }, + icon: h('i.cptools.cptools-form-list-ordered') + }, + }; + + var renderResults = function (content, answers) { + var $container = $('div.cp-form-creator-results').empty(); + var controls = h('div.cp-form-creator-results-controls'); + var $controls = $(controls).appendTo($container); + var results = h('div.cp-form-creator-results-content'); + var $results = $(results).appendTo($container); + + var summary = true; + var form = content.form; + + var 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; } + var print = model.printResults(answers, uid, form); + + var q = h('div.cp-form-block-question', block.q || Messages.form_default); + return h('div.cp-form-block', [ + h('div.cp-form-block-type', [ + TYPES[type].icon.cloneNode(), + h('span', Messages['form_type_'+type]) + ]), + q, + h('div.cp-form-block-content', print), + ]); + }); + $results.empty().append(elements); + if (header) { $results.prepend(header); } + }; + show(answers); + + 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)); + 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')); + }); + return div; + }); + $results.append(els); + }); + }; + + var addResultsButton = function (framework, content) { + var $res = $(h('button.cp-toolbar-appmenu', [ + h('i.fa.fa-bar-chart'), + h('span.cp-button-name', Messages.form_results) + ])); + $res.click(function () { + $res.attr('disabled', 'disabled'); + var sframeChan = framework._.sfCommon.getSframeChannel(); + sframeChan.query("Q_FORM_FETCH_ANSWERS", content.answers, function (err, obj) { + var answers = obj && obj.results; + if (answers) { APP.answers = answers; } + $res.removeAttr('disabled'); + $('body').addClass('cp-app-form-results'); + renderResults(content, answers); + $res.remove(); + var $editor = $(h('button.cp-toolbar-appmenu', [ + h('i.fa.fa-pencil'), + h('span.cp-button-name', APP.isEditor ? Messages.form_editor : Messages.form_form) + ])); + $editor.click(function () { + $('body').removeClass('cp-app-form-results'); + $editor.remove(); + addResultsButton(framework, content); + }); + framework._.toolbar.$bottomL.append($editor); + }); + + }); + framework._.toolbar.$bottomL.append($res); + }; + + var getFormResults = function () { + if (!Array.isArray(APP.formBlocks)) { return; } + var results = {}; + APP.formBlocks.forEach(function (data) { + if (!data.getValue) { return; } + results[data.uid] = data.getValue(); + }); + return results; + }; + var makeFormControls = function (framework, content, update) { + var loggedIn = framework._.sfCommon.isLoggedIn(); + var metadataMgr = framework._.cpNfInner.metadataMgr; + + if (!loggedIn && !content.answers.anonymous) { return; } + + var cbox; + cbox = UI.createCheckbox('cp-form-anonymous', + Messages.form_anonymousBox, true, { mark: { tabindex:1 } }); + if (loggedIn) { + if (!content.answers.anonymous || APP.cantAnon) { + $(cbox).hide().find('input').attr('disabled', 'disabled').prop('checked', false); + } + } + + var send = h('button.cp-open.btn.btn-primary', update ? Messages.form_update : Messages.form_submit); + var reset = h('button.cp-open.btn.btn-danger-alt', Messages.form_reset); + $(reset).click(function () { + if (!Array.isArray(APP.formBlocks)) { return; } + APP.formBlocks.forEach(function (data) { + if (typeof(data.reset) === "function") { data.reset(); } + }); + }); + var $send = $(send).click(function () { + $send.attr('disabled', 'disabled'); + var results = getFormResults(); + if (!results) { return; } + + var user = metadataMgr.getUserData(); + if (!Util.isChecked($(cbox).find('input'))) { + results._userdata = loggedIn ? { + avatar: user.avatar, + name: user.name, + notifications: user.notifications, + curvePublic: user.curvePublic, + profile: user.profile + } : { name: user.name }; + } + + var sframeChan = framework._.sfCommon.getSframeChannel(); + sframeChan.query('Q_FORM_SUBMIT', { + mailbox: content.answers, + results: results, + anonymous: !loggedIn || Util.isChecked($(cbox).find('input')) + }, function (err, data) { + $send.attr('disabled', 'disabled'); + if (err || (data && data.error)) { + if (data.error === "EANSWERED") { + return void UI.warn(Messages.form_answered); + } + console.error(err || data.error); + return void UI.warn(Messages.error); + } + if (!update) { + // Add results button + addResultsButton(framework, content); + } + $send.removeAttr('disabled'); + UI.alert(Messages.form_sent); + $send.text(Messages.form_update); + }); + }); + + if (APP.isClosed) { + send = undefined; + reset = undefined; + } + + return h('div.cp-form-send-container', [ + cbox ? h('div', cbox) : undefined, + send, reset + ]); + }; + var updateForm = function (framework, content, editable, answers, temp) { + var $container = $('div.cp-form-creator-content'); + if (!$container.length) { return; } // Not ready + + var form = content.form; + + APP.formBlocks = []; + + var evOnChange = Util.mkEvent(); + if (!APP.isEditor) { + var _answers = Util.clone(answers || {}); + delete _answers._proof; + delete _answers._userdata; + evOnChange.reg(function () { + var results = getFormResults(); + if (!answers || Sortify(_answers) !== Sortify(results)) { + window.onbeforeunload = function () { + return true; + }; + } else { + window.onbeforeunload = undefined; + } + }); + } + + + var getFormCreator = function (uid) { + if (!APP.isEditor) { return; } + var full = !uid; + var idx = content.order.indexOf(uid); + var addControl = function (type) { + var btn = h('button.btn.btn-default', { + title: full ? undefined : Messages['form_type_'+type] + }, [ + (TYPES[type] || STATIC_TYPES[type]).icon.cloneNode(), + full ? h('span', Messages['form_type_'+type]) : undefined + ]); + $(btn).click(function () { + var uid = Util.uid(); + content.form[uid] = { + //q: Messages.form_default, + //opts: opts + type: type, + }; + if (full) { + content.order.push(uid); + } else { + content.order.splice(idx, 0, uid); + } + framework.localChange(); + updateForm(framework, content, true); + }); + return btn; + }; + + var controls = Object.keys(TYPES).map(addControl); + var staticControls = Object.keys(STATIC_TYPES).map(addControl); + + var buttons = h('div.cp-form-creator-control-inline', [ + h('div.cp-form-creator-types', controls), + h('div.cp-form-creator-types', staticControls) + ]); + var add = h('div', [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'); + }); + } + + var inlineCls = full ? '-full' : '-inline'; + return h('div.cp-form-creator-add'+inlineCls, [ + h('div', 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 = []; + 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]); } + + if (data.pageBreak && !editable) { + elements.push(data); + return; + } + + + var dragHandle; + var q = h('div.cp-form-block-question', block.q || Messages.form_default); + var editButtons, editContainer; + + APP.formBlocks.push(data); + + if (editable) { + // Drag handle + dragHandle = h('span.cp-form-block-drag-handle', [ + h('i.fa.fa-ellipsis-h'), + h('i.fa.fa-ellipsis-h'), + ]); + + // Question + var inputQ = h('input', { + value: block.q || Messages.form_default + }); + var $inputQ = $(inputQ); + + var 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; + } + if (saving && !e) { return; } // Prevent spam Enter + block.q = v.trim(); + framework.localChange(); + saving = true; + framework._.cpNfInner.chainpad.onSettle(function () { + saving = false; + $(q).removeClass('editing'); + if (!e) { $inputQ.blur(); } + UI.log(Messages.saved); + }); + }; + var onCancelQ = function () { + $inputQ.val(block.q || Messages.form_default); + cancel = true; + $inputQ.blur(); + $(q).removeClass('editing'); + }; + $inputQ.keydown(function (e) { + if (e.which === 13) { return void onSaveQ(); } + if (e.which === 27) { return void onCancelQ(); } + }); + $inputQ.focus(function () { + $(q).addClass('editing'); + }); + $inputQ.blur(onSaveQ); + q = h('div.cp-form-input-block', [inputQ]); + + // Delete question + 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) { + 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) { + 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); + var _answers = getBlockAnswers(APP.answers, uid); + data = model.get(newOpts, _answers, null, evOnChange); + if (!data) { data = {}; } + $oldTag.before(data.tag).remove(); + }); + }; + var onEdit = function (tmp) { + data.editing = true; + $(data.tag).hide(); + $(editContainer).append(data.edit(onSave, tmp, framework)); + $(editButtons).hide(); + }; + $(edit).click(function () { + onEdit(); + }); + + // If we were editing this field, recover our unsaved changes + if (temp && temp[uid]) { + setTimeout(function () { + onEdit(temp[uid]); + }); + } + } + + editButtons = h('div.cp-form-edit-buttons-container', [ + edit, del + ]); + } + var editableCls = editable ? ".editable" : ""; + elements.push(h('div.cp-form-block'+editableCls, { + 'data-id':uid + }, [ + APP.isEditor ? dragHandle : undefined, + isStatic ? undefined : q, + h('div.cp-form-block-content', [ + data.tag, + editButtons + ]), + editContainer + ])); + }); + + if (APP.isEditor) { + elements.push(getFormCreator()); + } + + var _content = elements; + if (!editable) { + _content = []; + var div = h('div.cp-form-page'); + var pages = 1; + var wasPage = false; + elements.forEach(function (obj, i) { + if (obj && obj.pageBreak) { + if (i === 0) { return; } // Can't start with a page break + if (i === (elements.length - 1)) { return; } // Can't end with a page break + if (wasPage) { return; } // Prevent double page break + _content.push(div); + pages++; + 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 = function (current) { + $page.empty(); + if (!current || current < 1) { current = 1; } + if (current > pages) { current = pages; } + var left = h('button.btn.btn-secondary.small.cp-prev', [ + h('i.fa.fa-chevron-left'), + h('span', Messages.form_page_prev) + ]); + var state = h('span', Messages._getKey('form_page', [current, pages])); + var right = h('button.btn.btn-secondary.small.cp-next', [ + h('span', Messages.form_page_next), + h('i.fa.fa-chevron-right'), + ]); + $(left).click(function () { refreshPage(current - 1); }); + $(right).click(function () { refreshPage(current + 1); }); + $page.append([left, state, right]); + $container.find('.cp-form-page').hide(); + $($container.find('.cp-form-page').get(current-1)).show(); + if (current !== pages) { + $container.find('.cp-form-send-container').hide(); + } else { + $container.find('.cp-form-send-container').show(); + } + }; + setTimeout(refreshPage); + } + } + + $container.empty().append(_content); + updateAddInline(); + + if (editable) { + Sortable.create($container[0], { + direction: "vertical", + filter: "input, button, .CodeMirror, .cp-form-type-sort", + preventOnFilter: false, + draggable: ".cp-form-block", + forceFallback: true, + fallbackTolerance: 5, + 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))); + }; + + var getTempFields = function () { + if (!Array.isArray(APP.formBlocks)) { return; } + var temp = {}; + APP.formBlocks.forEach(function (data) { + if (data.editing) { + var cursor = data.getCursor && data.getCursor(); + temp[data.uid] = cursor; + } + }); + return temp; + }; + + var andThen = function (framework) { + framework.start(); + var 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); + var $body = $('body'); + + var $toolbarContainer = $('#cp-toolbar'); + var helpMenu = framework._.sfCommon.createHelpMenu(['text', 'pad']); + $toolbarContainer.after(helpMenu.menu); + + + var makeFormSettings = function () { + // Private / public status + var resultsType = h('div.cp-form-results-type-container'); + var $results = $(resultsType); + var refreshPublic = function () { + $results.empty(); + var makePublic = h('button.btn.btn-secondary', Messages.form_makePublic); + var makePublicDiv = h('div', makePublic); + if (content.answers.privateKey) { makePublicDiv = undefined; } + var publicText = content.answers.privateKey ? Messages.form_isPublic : Messages.form_isPrivate; + $results.append(h('span.cp-form-results-type', publicText)); + $results.append(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(); + + // Allow anonymous answers + var privacyContainer = h('div.cp-form-privacy-container'); + var $privacy = $(privacyContainer); + var refreshPrivacy = function () { + $privacy.empty(); + var anonymous = content.answers.anonymous; + var 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); + }); + }); + $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); + + return [ + endDateContainer, + privacyContainer, + resultsType, + ]; + }; + + 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; + var fillerContainer; + if (APP.isEditor) { + var settings = makeFormSettings(); + + controlContainer = h('div.cp-form-creator-control', [ + h('div.cp-form-creator-settings', settings), + ]); + fillerContainer = h('div.cp-form-filler-container'); + } + + var contentContainer = h('div.cp-form-creator-content'); + var resultsContainer = h('div.cp-form-creator-results'); + var div = h('div.cp-form-creator-container', [ + controlContainer, + contentContainer, + resultsContainer, + fillerContainer + ]); + return div; + }; + + var endDateEl = h('div.alert.alert-warning.cp-burn-after-reading'); + var endDate; + var endDateTo; + 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) { + setTimeout(function () { + refreshEndDateBanner(true); + $('.cp-form-send-container').find('.cp-open').remove(); + },(endDate - +new Date() + 100)); + } + }; + + framework.onReady(function () { + var priv = metadataMgr.getPrivateData(); + + if (APP.isEditor) { + if (!content.form) { + content.form = {}; + framework.localChange(); + } + if (!content.order) { + content.order = []; + framework.localChange(); + } + if (!content.answers || !content.answers.channel || !content.answers.publicKey || !content.answers.validateKey) { + content.answers = { + channel: Hash.createChannelId(), + publicKey: priv.form_public, + validateKey: priv.form_answerValidateKey + }; + framework.localChange(); + } + } + + sframeChan.event('EV_FORM_PIN', {channel: content.answers.channel}); + + 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) { + addResultsButton(framework, content); + sframeChan.query("Q_FORM_FETCH_ANSWERS", { + channel: content.answers.channel, + validateKey: content.answers.validateKey, + publicKey: content.answers.publicKey + }, function (err, obj) { + var answers = obj && obj.results; + if (answers) { APP.answers = answers; } + checkIntegrity(false); + updateForm(framework, content, true); + }); + return; + } + + refreshEndDateBanner(); + + var loggedIn = framework._.sfCommon.isLoggedIn(); + if (!loggedIn && !content.answers.anonymous) { + UI.alert(Messages.form_anonymous_blocked); + } + + // If the results are public and there is at least one doodle, fetch the results now + if (content.answers.privateKey && Object.keys(content.form).some(function (uid) { + return content.form[uid].type === "poll"; + })) { + 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; } + + // 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; } + } + checkIntegrity(false); + updateForm(framework, content, false, answers); + }); + + }); + + framework.onContentUpdate(function (newContent) { + content = newContent; + evOnChange.fire(); + refreshEndDateBanner(); + 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; + }); + + }; + + Framework.create({ + toolbarContainer: '#cp-toolbar', + contentContainer: '#cp-app-form-editor', + }, andThen); +}); diff --git a/www/form/main.js b/www/form/main.js new file mode 100644 index 000000000..344c5bb0c --- /dev/null +++ b/www/form/main.js @@ -0,0 +1,358 @@ +// Load #1, load as little as possible because we are in a race to get the loading screen up. +define([ + '/bower_components/nthen/index.js', + '/api/config', + '/common/dom-ready.js', + '/common/sframe-common-outer.js', + '/bower_components/tweetnacl/nacl-fast.min.js', +], function (nThen, ApiConfig, DomReady, SFCommonO) { + var Nacl = window.nacl; + + var href, hash; + // Loaded in load #2 + nThen(function (waitFor) { + DomReady.onReady(waitFor()); + }).nThen(function (waitFor) { + var obj = SFCommonO.initIframe(waitFor, true); + href = obj.href; + hash = obj.hash; + }).nThen(function (/*waitFor*/) { + var privateKey, publicKey; + var channels = {}; + var getPropChannels = function () { + return channels; + }; + var addData = function (meta, CryptPad, user, Utils) { + var keys = Utils.secret && Utils.secret.keys; + + var parsed = Utils.Hash.parseTypeHash('pad', hash.slice(1)); + if (parsed && parsed.auditorKey) { + meta.form_auditorKey = parsed.auditorKey; + meta.form_auditorHash = hash; + } + + var secondary = keys && keys.secondaryKey; + if (!secondary) { return; } + var curvePair = Nacl.box.keyPair.fromSecretKey(Nacl.util.decodeUTF8(secondary).slice(0,32)); + var validateKey = keys.secondaryValidateKey; + meta.form_answerValidateKey = validateKey; + + publicKey = meta.form_public = Nacl.util.encodeBase64(curvePair.publicKey); + privateKey = meta.form_private = Nacl.util.encodeBase64(curvePair.secretKey); + + var auditorHash = Utils.Hash.getViewHashFromKeys({ + version: 1, + channel: Utils.secret.channel, + keys: { viewKeyStr: Nacl.util.encodeBase64(keys.cryptKey) } + }); + var _parsed = Utils.Hash.parseTypeHash('pad', auditorHash); + meta.form_auditorHash = _parsed.getHash({auditorKey: privateKey}); + }; + var addRpc = function (sframeChan, Cryptpad, Utils) { + sframeChan.on('EV_FORM_PIN', function (data) { + channels.answersChannel = data.channel; + Cryptpad.getPadAttribute('answersChannel', function (err, res) { + // If already stored, don't pin it again + if (res && res === data.channel) { return; } + Cryptpad.pinPads([data.channel], function () { + Cryptpad.setPadAttribute('answersChannel', data.channel, function () {}); + }); + }); + + }); + var getAnonymousKeys = function (formSeed, channel) { + var array = Nacl.util.decodeBase64(formSeed + channel); + var hash = Nacl.hash(array); + var secretKey = Nacl.util.encodeBase64(hash.subarray(32)); + var publicKey = Utils.Hash.getCurvePublicFromPrivate(secretKey); + return { + curvePrivate: secretKey, + curvePublic: publicKey, + }; + }; + var u8_slice = function (A, start, end) { + return new Uint8Array(Array.prototype.slice.call(A, start, end)); + }; + var u8_concat = function (A) { + var length = 0; + A.forEach(function (a) { length += a.length; }); + var total = new Uint8Array(length); + + var offset = 0; + A.forEach(function (a) { + total.set(a, offset); + offset += a.length; + }); + return total; + }; + var anonProof = function (channel, theirPub, anonKeys) { + var u8_plain = Nacl.util.decodeUTF8(channel); + var u8_nonce = Nacl.randomBytes(Nacl.box.nonceLength); + var u8_cipher = Nacl.box( + u8_plain, + u8_nonce, + Nacl.util.decodeBase64(theirPub), + Nacl.util.decodeBase64(anonKeys.curvePrivate) + ); + var u8_bundle = u8_concat([ + u8_nonce, // 24 uint8s + u8_cipher, // arbitrary length + ]); + return { + key: anonKeys.curvePublic, + proof: Nacl.util.encodeBase64(u8_bundle) + }; + }; + var checkAnonProof = function (proofObj, channel, curvePrivate) { + var pub = proofObj.key; + var proofTxt = proofObj.proof; + try { + var u8_bundle = Nacl.util.decodeBase64(proofTxt); + var u8_nonce = u8_slice(u8_bundle, 0, Nacl.box.nonceLength); + var u8_cipher = u8_slice(u8_bundle, Nacl.box.nonceLength); + var u8_plain = Nacl.box.open( + u8_cipher, + u8_nonce, + Nacl.util.decodeBase64(pub), + Nacl.util.decodeBase64(curvePrivate) + ); + return channel === Nacl.util.encodeUTF8(u8_plain); + } catch (e) { + console.error(e); + return false; + } + }; + sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, _cb) { + var cb = Utils.Util.once(_cb); + var myKeys = {}; + var myFormKeys; + var accessKeys; + var CPNetflux, Pinpad; + var network; + var noDriveAnswered = false; + nThen(function (w) { + require([ + '/bower_components/chainpad-netflux/chainpad-netflux.js', + '/common/pinpad.js', + ], w(function (_CPNetflux, _Pinpad) { + CPNetflux = _CPNetflux; + Pinpad = _Pinpad; + })); + Cryptpad.getAccessKeys(w(function (_keys) { + if (!Array.isArray(_keys)) { return; } + accessKeys = _keys; + + _keys.some(function (_k) { + if ((!Cryptpad.initialTeam && !_k.id) || Cryptpad.initialTeam === _k.id) { + myKeys = _k; + return true; + } + }); + })); + Cryptpad.getFormKeys(w(function (keys) { + if (!keys.curvePublic && !keys.formSeed) { + // No drive mode + var answered = JSON.parse(localStorage.CP_formAnswered || "[]"); + noDriveAnswered = answered.indexOf(data.channel) !== -1; + } + myFormKeys = keys; + })); + Cryptpad.makeNetwork(w(function (err, nw) { + network = nw; + })); + }).nThen(function () { + if (!network) { return void cb({error: "E_CONNECT"}); } + + if (myFormKeys.formSeed) { + myFormKeys = getAnonymousKeys(myFormKeys.formSeed, data.channel); + } + + var keys = Utils.secret && Utils.secret.keys; + + var curvePrivate = privateKey || data.privateKey; + var crypto = Utils.Crypto.Mailbox.createEncryptor({ + curvePrivate: curvePrivate, + curvePublic: publicKey || data.publicKey, + validateKey: data.validateKey + }); + var config = { + network: network, + channel: data.channel, + noChainPad: true, + validateKey: keys.secondaryValidateKey, + owners: [myKeys.edPublic], + crypto: crypto, + // XXX Cache + }; + var results = {}; + config.onError = function (info) { + cb({ error: info.type }); + }; + config.onRejected = function (data, cb) { + if (!Array.isArray(data) || !data.length || data[0].length !== 16) { + return void cb(true); + } + if (!Array.isArray(accessKeys)) { return void cb(true); } + network.historyKeeper = data[0]; + nThen(function (waitFor) { + accessKeys.forEach(function (obj) { + Pinpad.create(network, obj, waitFor(function (e) { + console.log('done', obj); + if (e) { console.error(e); } + })); + }); + }).nThen(function () { + cb(); + }); + }; + config.onReady = function () { + var myKey; + // If we have submitted an anonymous answer, retrieve it + if (myFormKeys.curvePublic && results[myFormKeys.curvePublic]) { + myKey = myFormKeys.curvePublic; + } + cb({ + noDriveAnswered: noDriveAnswered, + myKey: myKey, + results: results + }); + network.disconnect(); + }; + config.onMessage = function (msg, peer, vKey, isCp, hash, senderCurve, cfg) { + var parsed = Utils.Util.tryParse(msg); + if (!parsed) { return; } + if (parsed._proof) { + var check = checkAnonProof(parsed._proof, data.channel, curvePrivate); + if (check) { + delete results[parsed._proof.key]; + } + } + results[senderCurve] = { + msg: parsed, + hash: hash, + time: cfg.time + }; + }; + CPNetflux.start(config); + }); + }); + sframeChan.on("Q_FETCH_MY_ANSWERS", function (data, cb) { + var answer; + var myKeys; + nThen(function (w) { + Cryptpad.getFormKeys(w(function (keys) { + myKeys = keys; + })); + Cryptpad.getFormAnswer({channel: data.channel}, w(function (obj) { + if (!obj || obj.error) { + if (obj && obj.error === "ENODRIVE") { + var answered = JSON.parse(localStorage.CP_formAnswered || "[]"); + if (answered.indexOf(data.channel) !== -1) { + cb({error:'EANSWERED'}); + } else { + cb(); + } + return void w.abort(); + } + w.abort(); + return void cb(obj); + } + answer = obj; + })); + }).nThen(function () { + if (answer.anonymous) { + if (!myKeys.formSeed) { return void cb({ error: "ANONYMOUS_ERROR" }); } + myKeys = getAnonymousKeys(myKeys.formSeed, data.channel); + } + Cryptpad.getHistoryRange({ + channel: data.channel, + lastKnownHash: answer.hash, + toHash: answer.hash, + }, function (obj) { + if (obj && obj.error) { return void cb(obj); } + var messages = obj.messages; + var res = Utils.Crypto.Mailbox.openOwnSecretLetter(messages[0].msg, { + validateKey: data.validateKey, + ephemeral_private: Nacl.util.decodeBase64(answer.curvePrivate), + my_private: Nacl.util.decodeBase64(myKeys.curvePrivate), + their_public: Nacl.util.decodeBase64(data.publicKey) + }); + res.content._isAnon = answer.anonymous; + cb(JSON.parse(res.content)); + }); + + }); + + }); + var noDriveSeed = Utils.Hash.createChannelId(); + sframeChan.on("Q_FORM_SUBMIT", function (data, cb) { + var box = data.mailbox; + var myKeys; + nThen(function (w) { + Cryptpad.getFormKeys(w(function (keys) { + // If formSeed doesn't exists, it means we're probably in noDrive mode. + // We can create a seed in localStorage. + if (!keys.formSeed) { + // No drive mode + var answered = JSON.parse(localStorage.CP_formAnswered || "[]"); + if(answered.indexOf(data.channel) !== -1) { + // Already answered: abort + return void cb({ error: "EANSWERED" }); + } + keys = { formSeed: noDriveSeed }; + } + myKeys = keys; + })); + }).nThen(function () { + var myAnonymousKeys; + if (data.anonymous) { + if (!myKeys.formSeed) { return void cb({ error: "ANONYMOUS_ERROR" }); } + myKeys = getAnonymousKeys(myKeys.formSeed, box.channel); + } else { + myAnonymousKeys = getAnonymousKeys(myKeys.formSeed, box.channel); + } + var keys = Utils.secret && Utils.secret.keys; + myKeys.signingKey = keys.secondarySignKey; + + var ephemeral_keypair = Nacl.box.keyPair(); + var ephemeral_private = Nacl.util.encodeBase64(ephemeral_keypair.secretKey); + myKeys.ephemeral_keypair = ephemeral_keypair; + + if (myAnonymousKeys) { + var proof = anonProof(box.channel, box.publicKey, myAnonymousKeys); + data.results._proof = proof; + } + + var crypto = Utils.Crypto.Mailbox.createEncryptor(myKeys); + var text = JSON.stringify(data.results); + var ciphertext = crypto.encrypt(text, box.publicKey); + + var hash = ciphertext.slice(0,64); + Cryptpad.anonRpcMsg("WRITE_PRIVATE_MESSAGE", [ + box.channel, + ciphertext + ], function (err, response) { + Cryptpad.storeFormAnswer({ + channel: box.channel, + hash: hash, + curvePrivate: ephemeral_private, + anonymous: Boolean(data.anonymous) + }); + cb({error: err, response: response, hash: hash}); + }); + }); + }); + }; + SFCommonO.start({ + addData: addData, + addRpc: addRpc, + cache: true, + noDrive: true, + hash: hash, + href: href, + useCreationScreen: true, + messaging: true, + getPropChannels: getPropChannels + }); + }); +}); diff --git a/www/kanban/inner.js b/www/kanban/inner.js index 126ffd000..76a9fe1cc 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -241,7 +241,12 @@ define([ e.stopPropagation(); }); var common = framework._.sfCommon; - var markdownTb = common.createMarkdownToolbar(editor); + var markdownTb = common.createMarkdownToolbar(editor, { + embed: function (mt) { + editor.focus(); + editor.replaceSelection($(mt)[0].outerHTML); + } + }); $(text).before(markdownTb.toolbar); $(markdownTb.toolbar).show(); editor.refresh(); diff --git a/www/secureiframe/inner.js b/www/secureiframe/inner.js index 1c5af4a57..4226c7f82 100644 --- a/www/secureiframe/inner.js +++ b/www/secureiframe/inner.js @@ -58,6 +58,7 @@ define([ hashes: data.hashes || priv.hashes, common: common, title: data.title, + auditorHash: data.auditorHash, versionHash: data.versionHash, friends: friends, onClose: function () {