From fee546ee9d35b0f6701b49f2c937e10b656a3411 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 9 Jul 2021 18:12:04 +0530 Subject: [PATCH 001/223] describe the premium support ticket policy on the support page --- www/support/app-support.less | 4 ++++ www/support/inner.js | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/www/support/app-support.less b/www/support/app-support.less index 0aa4c6b61..3d3066bcc 100644 --- a/www/support/app-support.less +++ b/www/support/app-support.less @@ -34,5 +34,9 @@ color: @cryptpad_color_link; text-decoration: underline; } + // add some whitespace to improve readability a bit + br { + margin: 5px; + } } diff --git a/www/support/inner.js b/www/support/inner.js index 6d9e5eae8..ebfe3bcfc 100644 --- a/www/support/inner.js +++ b/www/support/inner.js @@ -166,12 +166,29 @@ define([ return $div; }; + Messages.support_premiumPriority = "Premium users help support improvements to CryptPad's usability and benefit from prioritized responses to their support tickets."; // XXX + Messages.support_premiumLink = 'View subscription options.'; // XXX + // Create a new tickets create['form'] = function () { var key = 'form'; var $div = makeBlock(key, true); // Msg.support_formHint, .support_formTitle, .support_formButton Pages.documentationLink($div.find('a')[0], 'https://docs.cryptpad.fr/en/user_guide/index.html'); + var accountsLink = h('a', { + href: Pages.accounts.upgradeURL, + }, Messages.support_premiumLink,); + + var premium = h("div.alert.alert-info", [ + Messages.support_premiumPriority, + ' ', + accountsLink, + ]); + + if (Pages.areSubscriptionsAllowed()) { + $div.find('.cp-sidebarlayout-description').append(premium); + } + var form = APP.support.makeForm(); var id = Util.uid(); From d22c4a6d687c64ddfe31b8fbbf129dd57574cbf2 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 9 Jul 2021 18:33:14 +0530 Subject: [PATCH 002/223] extra styles for support page enhancements --- www/support/app-support.less | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/www/support/app-support.less b/www/support/app-support.less index 3d3066bcc..d4719956c 100644 --- a/www/support/app-support.less +++ b/www/support/app-support.less @@ -34,6 +34,11 @@ color: @cryptpad_color_link; text-decoration: underline; } + .alert-info { + font-size: 16px; + margin-top: 15px; + margin-bottom: 15px; + } // add some whitespace to improve readability a bit br { margin: 5px; From 22c67e0e9eace9b3a6beb13abd84851de827e1cb Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 12 Aug 2021 19:34:23 +0530 Subject: [PATCH 003/223] WIP unify usage of anonymous/non-registered/unregistered as 'guest' --- www/common/translations/messages.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/www/common/translations/messages.json b/www/common/translations/messages.json index ce2f0ab57..e967f3e7c 100644 --- a/www/common/translations/messages.json +++ b/www/common/translations/messages.json @@ -28,7 +28,7 @@ "onLogout": "You are logged out, {0}click here{1} to log in
or press Escape to access your pad in read-only mode.", "padNotPinned": "This pad will expire after 3 months of inactivity, {0}login{1} or {2}register{3} to preserve it.", "padNotPinnedVariable": "This pad will expire after {4} days of inactivity, {0}login{1} or {2}register{3} to preserve it.", - "anonymousStoreDisabled": "The webmaster of this CryptPad instance has disabled the store for anonymous users. You have to log in to be able to use CryptDrive.", + "anonymousStoreDisabled": "The administrator of this CryptPad instance has disabled storage for guests. Log in to access your own CryptDrive.", "expiredError": "This pad has reached its expiration time and is no longer available.", "deletedError": "This document has been deleted and is no longer available.", "inactiveError": "This pad has been deleted due to inactivity. Press Esc to create a new pad.", @@ -53,7 +53,7 @@ "forgotten": "Moved to the trash", "errorState": "Critical error: {0}", "readonly": "Read only", - "anonymous": "Anonymous", + "anonymous": "Guest", "users": "Users", "viewer": "viewer", "viewers": "viewers", @@ -79,7 +79,7 @@ "exportButton": "Export", "exportButtonTitle": "Export this pad to a local file", "exportPrompt": "What would you like to name your file?", - "changeNamePrompt": "Change your name (leave empty to be anonymous): ", + "changeNamePrompt": "Change your name (leave empty to be known as 'Guest'): ", "user_rename": "Change display name", "user_displayName": "Display name", "user_accountName": "Account name", @@ -339,7 +339,7 @@ "login_invalUser": "Username required", "login_invalPass": "Password required", "login_unhandledError": "An unexpected error occurred :(", - "register_importRecent": "Import documents from your unregistered session", + "register_importRecent": "Import documents from your guest session", "register_acceptTerms": "I accept the terms of service", "register_passwordsDontMatch": "Passwords do not match!", "register_passwordTooShort": "Passwords must be at least {0} characters long.", @@ -520,7 +520,7 @@ "whatis_drive": "Organization with CryptDrive", "features": "Features", "features_title": "Features", - "features_anon": "Non-registered", + "features_anon": "Guest", "features_registered": "Registered", "features_premium": "Premium", "features_f_apps": "Access to all the applications", @@ -532,7 +532,7 @@ "features_f_cryptdrive0_note": "Ability to store visited pads in your browser to be able to open them later", "features_f_storage0": "Limited storage time", "features_f_storage0_note": "Documents are deleted after {0} days of inactivity", - "features_f_anon": "All anonymous user features", + "features_f_anon": "All guest user features", "features_f_anon_note": "With additional functionality", "features_f_cryptdrive1": "Complete CryptDrive functionality", "features_f_cryptdrive1_note": "Folders, shared folders, templates, tags", @@ -884,7 +884,7 @@ "oo_exportInProgress": "Export in progress", "oo_sheetMigration_loading": "Upgrading your spreadsheet to the latest version. Please wait approximately 1 minute.", "oo_sheetMigration_complete": "Updated version available, press OK to reload.", - "oo_sheetMigration_anonymousEditor": "Editing this spreadsheet is disabled for non-registered users until it is upgraded to the latest version by a registered user.", + "oo_sheetMigration_anonymousEditor": "Editing this spreadsheet is disabled for guests until it is upgraded to the latest version by a registered user.", "imprint": "Legal notice", "settings_cat_security": "Confidentiality", "settings_safeLinksTitle": "Safe Links", @@ -1108,7 +1108,7 @@ "home_support_title": "Support CryptPad", "home_support": "

The development team does not profit from user data in any way. This is part of a vision for online services that respect privacy. Unlike the big platforms that pretend to be \"free\" while making profits from personal information, we aim to build a sustainable model funded willingly by users.

You can support the project by making a one-time or recurring donation through our Open Collective. Our budget is transparent and updates are published regularly. There are also a number of non-financial ways to contribute.

", "register_notes_title": "Important notes", - "register_notes": "
  • Your password is the secret key that encrypts all of your documents. If you lose it there is no way we can recover your data.
  • If you are using a shared computer, remember to log out when you are done. Only closing the browser window leaves your account exposed.
  • To keep the documents you created and/or stored without being logged in, tick \"Import documents from your anonymous session\".
", + "register_notes": "
  • Your password is the secret key that encrypts all of your documents. If you lose it there is no way we can recover your data.
  • If you are using a shared computer, remember to log out when you are done. Only closing the browser window leaves your account exposed.
  • To keep the documents you created and/or stored without being logged in, tick \"Import documents from your guest session\".
", "register_warning_note": "Due to the encrypted nature of CryptPad, the service administrators will not be able to recover data in case you forget your username and/or password. Please save them in a safe place.", "whatis_collaboration_info": "

CryptPad is built to enable collaboration. It synchronizes changes to documents in real time. Because all data is encrypted, the service and its administrators have no way of seeing the content being edited and stored.

", "whatis_apps": "A full suite of applications", From cad514cc17501521a1aad826470861e2a9128d2d Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 16 Aug 2021 17:21:45 +0530 Subject: [PATCH 004/223] remove hacky experiments with charts.css ...and finalize forms changes --- customize.dist/src/less2/include/charts.less | 19 -- www/common/inner/charts.js | 54 ------ www/common/toolbar.js | 6 +- www/form/app-form.less | 10 ++ www/form/inner.js | 172 ++++++++----------- www/lib/chart/charts.min.css | 1 - www/test/index.html | 11 -- www/test/inner.js | 68 -------- 8 files changed, 85 insertions(+), 256 deletions(-) delete mode 100644 www/common/inner/charts.js delete mode 100644 www/lib/chart/charts.min.css delete mode 100644 www/test/index.html delete mode 100644 www/test/inner.js diff --git a/customize.dist/src/less2/include/charts.less b/customize.dist/src/less2/include/charts.less index 022a327cf..c8b983707 100644 --- a/customize.dist/src/less2/include/charts.less +++ b/customize.dist/src/less2/include/charts.less @@ -50,23 +50,4 @@ } } } - - - - &.bar { - th { - //width: 200px !important; - font-size: 10px; // XXX - } - - - - - td { - margin-top: 0.25em; - background: @cryptpad_color_brand_fade !important; - color: @cryptpad_color_grey_100 !important; //text_col !important; - font-weight: bold; - } - } } diff --git a/www/common/inner/charts.js b/www/common/inner/charts.js deleted file mode 100644 index f6962b5ad..000000000 --- a/www/common/inner/charts.js +++ /dev/null @@ -1,54 +0,0 @@ -define([ - '/common/hyperscript.js', - -], function (h) { - var Charts = {}; - - Charts.table = function (content, classes) { - var classString = Array.isArray(classes)? '.' + classes.filter(Boolean).join('.'): ''; - return h('table' + classString, content); - }; - - Charts.columns = function (rows) { - return Charts.table([ - //h('caption', "Front-End Developer Salary"), - h('tbody', rows.map(function (n) { - return h('tr', h('td', { - style: '--size: ' + (n / 100), - }, h('span.data', n))); - })), - ], [ - 'charts-css', - 'column', - 'show-heading', - ]); - }; - - Charts.row = function (text, count, data) { - return h('tr', [ - h('th', { - scope: 'row', - }, text), - h('td', { - style: '--size: ' + count, - }, [ - //text, - typeof(data) !== 'undefined'? h('span.data', data): text, - ]) - ]); - }; - - // table.charts-css.bar.reverse -/* - Charts.bars = function (rows) { - return Charts.table([ - - ], [ - - - ]); - }; -*/ - - return Charts; -}); diff --git a/www/common/toolbar.js b/www/common/toolbar.js index ccc1c5e78..47c0b578e 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -455,10 +455,6 @@ MessengerUI, Messages, Pages) { return $container; }; - //Messages.collapse = Messages.admin_support_collapse; - Messages.ui_collapse = "Collapse toolbar"; // XXX - Messages.ui_expand = "Expand toolbar"; // XXX - createCollapse = function (toolbar) { var up = h('i.fa.fa-chevron-up', {title: Messages.ui_collapse}); var down = h('i.fa.fa-chevron-down', {title: Messages.ui_expand}); @@ -1383,7 +1379,7 @@ MessengerUI, Messages, Pages) { toolbar.$file.show(); addElement([ 'chat', - 'collapse', // XXX + 'collapse', 'userlist', 'title', 'useradmin', 'spinner', 'newpad', 'share', 'access', 'limit', 'unpinnedWarning', 'notifications' diff --git a/www/form/app-form.less b/www/form/app-form.less index a38867d34..1621e735d 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -590,6 +590,16 @@ &.cp-value { min-width: 200px; } + &.cp-bar-container { + width: 99%; + padding: 0px; + position: relative; + .cp-bar { + position: absolute; + background: @cryptpad_color_brand; + height: 100%; + } + } } } } diff --git a/www/form/inner.js b/www/form/inner.js index a54fc53c3..8bb47432c 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -23,7 +23,6 @@ define([ '/common/inner/share.js', '/common/inner/access.js', '/common/inner/properties.js', - '/common/inner/charts.js', '/lib/datepicker/flatpickr.js', '/bower_components/sortablejs/Sortable.min.js', @@ -38,7 +37,6 @@ define([ 'css!/bower_components/codemirror/addon/dialog/dialog.css', 'css!/bower_components/codemirror/addon/fold/foldgutter.css', 'css!/lib/datepicker/flatpickr.min.css', - //'css!/lib/chart/charts.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'less!/form/app-form.less', ], function ( @@ -62,7 +60,7 @@ define([ DiffMd, SFCodeMirror, CMeditor, - Share, Access, Properties, Charts, + Share, Access, Properties, Flatpickr, Sortable ) @@ -805,7 +803,7 @@ define([ return total; }; - var getEmpty = function (empty) { // XXX don't include this in the scrollable area + var getEmpty = function (empty) { // TODO don't include this in the scrollable area if (empty) { return UI.setHTML(h('div.cp-form-results-type-text-empty'), Messages._getKey('form_notAnswered', [empty])); } @@ -959,42 +957,28 @@ define([ return Array.isArray(A)? Math.max.apply(null, A): NaN; }; - var CLASSIC_MODE = true; - var renderTally = function (tally, empty, caption) { - var rows = []; - if (CLASSIC_MODE) { - Object.keys(tally).forEach(function (value) { - rows.push(h('div.cp-form-results-type-radio-data', [ - h('span.cp-value', value), - h('span.cp-count', tally[value]) - ])); - }); - if (empty) { rows.push(getEmpty(empty)); } - return rows; - } + var barGraphic = function (itemScale) { + return h('span.cp-bar-container', h('div.cp-bar', { + style: 'width: ' + (itemScale * 100) + '%', + }, ' ')); + }; + var renderTally = function (tally, empty, showBar) { + var rows = []; var counts = Util.values(tally); var max = arrayMax(counts); - - Object.keys(tally).forEach(function (answer) { - rows.push(Charts.row(answer, tally[answer] / max, tally[answer])); + Object.keys(tally).forEach(function (value) { + var itemCount = tally[value]; + var itemScale = (itemCount / max); + + rows.push(h('div.cp-form-results-type-radio-data', [ + h('span.cp-value', value), + h('span.cp-count', itemCount), + showBar? barGraphic(itemScale): undefined, + ])); }); - var hasCaption = typeof(caption) !== 'undefined'; - var table = Charts.table([ - hasCaption ? h('caption', caption): undefined, - h('tbody', rows) - ], [ - 'charts-css', - 'bar', - hasCaption? 'show-heading': undefined, - 'show-labels', - 'show-data-on-hover', - ]); - - return [ - table, - empty? getEmpty(empty): undefined, - ]; + if (empty) { rows.push(getEmpty(empty)); } + return rows; }; var TYPES = { @@ -1042,10 +1026,10 @@ define([ if (!answer || !answer.trim()) { return empty++; } Util.inc(tally, answer); }); - var counts = Util.values(tally); - var max = arrayMax(counts); + //var counts = Util.values(tally); + //var max = arrayMax(counts); - if (CLASSIC_MODE || max < 2) { // there are no duplicates, so just return text + //if (max < 2) { // there are no duplicates, so just return text Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; @@ -1054,10 +1038,11 @@ define([ }); results.push(getEmpty(empty)); return h('div.cp-form-results-type-text', results); - } - - var rendered = renderTally(tally, empty /*, caption */); + //} +/* + var rendered = renderTally(tally, empty); return h('div.cp-form-results-type-text', rendered); +*/ }, icon: h('i.cptools.cptools-form-text') }, @@ -1181,10 +1166,11 @@ define([ }; }, - printResults: function (answers, uid) { + printResults: function (answers, uid, form, content) { // results radio var empty = 0; var count = {}; + var showBars = Boolean(content); Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; @@ -1192,10 +1178,8 @@ define([ Util.inc(count, answer); }); - var rendered = renderTally(count, empty/*, caption */); - return h('div.cp-form-results-type-radio', { - style: CLASSIC_MODE? '': 'width: 100%', - }, rendered); + var rendered = renderTally(count, empty, showBars); + return h('div.cp-form-results-type-radio', rendered); }, icon: h('i.cptools.cptools-form-list-radio') }, @@ -1304,21 +1288,20 @@ define([ var q = findItem(opts.items, q_uid); var c = count[q_uid]; - - if (CLASSIC_MODE) { - 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) - ])); - return; - } - + var values = Object.keys(c).map(function (res) { + var itemCount = c[res]; + return h('div.cp-form-results-type-radio-data', [ + h('span.cp-value', res), + h('span.cp-count', itemCount), + //barGraphic((itemCount / max) * 100) + ]); + }); + results.push(h('div.cp-form-results-type-multiradio-data', [ + h('span.cp-mr-q', q), + h('span.cp-mr-value', values) + ])); + return; +/* var table = Charts.table([ h('caption', { style: 'color: var(--msg-color)', @@ -1337,12 +1320,11 @@ define([ results.push(h('div.cp-form-results-type-multiradio-data', { style: 'width: 100%', }, table)); +*/ }); results.push(getEmpty(empty)); - return h('div.cp-form-results-type-radio', { - style: CLASSIC_MODE? '': 'width: 100%', - }, results); + return h('div.cp-form-results-type-radio', results); }, exportCSV: function (answer, form) { var opts = form.opts || {}; @@ -1424,10 +1406,11 @@ define([ }; }, - printResults: function (answers, uid) { + printResults: function (answers, uid, form, content) { // results checkbox var empty = 0; var count = {}; + var showBars = Boolean(content); Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; @@ -1437,7 +1420,7 @@ define([ }); }); - var rendered = renderTally(count, empty /*, caption */); + var rendered = renderTally(count, empty, showBars); return h('div.cp-form-results-type-radio', rendered); }, icon: h('i.cptools.cptools-form-list-check') @@ -1560,21 +1543,17 @@ define([ var q = findItem(opts.items, q_uid); var c = count[q_uid]; - - if (CLASSIC_MODE) { - 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) - ])); - return; - } - + 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) + ])); +/* var table = Charts.table([ h('caption', { style: 'color: var(--msg-color)', @@ -1593,12 +1572,11 @@ define([ results.push(h('div.cp-form-results-type-multiradio-data', { style: 'width: 100%', }, table)); +*/ }); results.push(getEmpty(empty)); - return h('div.cp-form-results-type-radio', { - style: CLASSIC_MODE? '': 'width: 100%', - }, results); + return h('div.cp-form-results-type-radio', results); }, exportCSV: function (answer, form) { var opts = form.opts || {}; @@ -1636,7 +1614,7 @@ define([ into concatenating strings, which quickly turns the sortable list into complete nonsense. */ - Util.shuffleArray(opts.values); // XXX + Util.shuffleArray(opts.values); } var els = opts.values.map(function (data) { var uid = Util.uid(); @@ -1713,13 +1691,14 @@ define([ }; }, - printResults: function (answers, uid, form) { + printResults: function (answers, uid, form, content) { // results sort var opts = form[uid].opts || TYPES.sort.defaultOpts; var l = (opts.values || []).length; //var results = []; var empty = 0; var count = {}; + var showBars = Boolean(content); Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; @@ -1730,10 +1709,8 @@ define([ }); }); - var rendered = renderTally(count, empty /*, caption */); - return h('div.cp-form-results-type-radio', { - style: CLASSIC_MODE? '': 'width: 100%', - }, rendered); + var rendered = renderTally(count, empty, showBars); + return h('div.cp-form-results-type-radio', rendered); }, icon: h('i.cptools.cptools-form-list-ordered') }, @@ -1897,8 +1874,7 @@ define([ return A; }; - Messages.form_timelineLabel = "{0} ({1})"; // XXX - Messages.form_totalResponses = "Total responses: {0}"; // XXX + Messages.form_timelineLabel = "{0} ({1})"; // TODO investigate whether this needs translation var makeTimeline = APP.makeTimeline = function (answers) { // Randomly changing date of answers to get a more realistic example of timeline @@ -1991,7 +1967,7 @@ define([ var switchMode = h('button.btn.btn-secondary', Messages.form_showIndividual); $controls.hide().append(switchMode); - var show = function (answers, header) { + var show = function (answers, header) { // XXX var elements = content.order.map(function (uid) { var block = form[uid]; var type = block.type; @@ -1999,7 +1975,7 @@ define([ if (!model || !model.printResults) { return; } // Only use content if we're not viewing individual answers - var print = model.printResults(answers, uid, form, !header && content); + var print = model.printResults(answers, uid, form, !header && content); // XXX var q = h('div.cp-form-block-question', block.q || Messages.form_default); @@ -2016,7 +1992,7 @@ define([ $results.empty().append(elements); if (header) { $results.prepend(header); } }; - show(answers); + show(answers); // XXX if (APP.isEditor || APP.isAuditor) { $controls.show(); } @@ -2074,7 +2050,7 @@ define([ } 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]); + var div = h('div.cp-form-individual', [span, viewButton, warning, badge]); // XXX $(viewButton).click(function () { var res = {}; res[curve] = obj; @@ -2083,7 +2059,7 @@ define([ summary = true; $s.click(); }); - var header = h('div.cp-form-individual', [ + var header = h('div.cp-form-individual', [ // XXX span.cloneNode(true), back ]); diff --git a/www/lib/chart/charts.min.css b/www/lib/chart/charts.min.css deleted file mode 100644 index 322319ec5..000000000 --- a/www/lib/chart/charts.min.css +++ /dev/null @@ -1 +0,0 @@ -.charts-css{--color-1:rgba(240,50,50,0.75);--color-2:rgba(255,180,50,0.75);--color-3:rgba(255,220,90,0.75);--color-4:rgba(100,210,80,0.75);--color-5:rgba(90,165,255,0.75);--color-6:rgba(170,90,240,0.75);--color-7:hsla(0,0%,70.6%,0.75);--color-8:hsla(0,0%,43.1%,0.75);--color-9:rgba(170,150,110,0.75);--color-10:rgba(130,50,20,0.75);--chart-bg-color:#f5f5f5;--heading-size:0px;--primary-axis-color:#000;--primary-axis-style:solid;--primary-axis-width:1px;--secondary-axes-color:rgba(0,0,0,0.15);--secondary-axes-style:solid;--secondary-axes-width:1px;--data-axes-color:rgba(0,0,0,0.15);--data-axes-style:solid;--data-axes-width:1px;--legend-border-color:#c8c8c8;position:relative;display:block;width:100%;height:100%;margin:0 auto;padding:0;border:0;-webkit-print-color-adjust:exact;color-adjust:exact}.charts-css,.charts-css *,.charts-css::after,.charts-css ::after,.charts-css::before,.charts-css ::before{-webkit-box-sizing:border-box;box-sizing:border-box}table.charts-css{border-collapse:collapse;border-spacing:0;empty-cells:show;overflow:initial;background-color:transparent}table.charts-css caption,table.charts-css colgroup,table.charts-css tbody,table.charts-css td,table.charts-css th,table.charts-css thead,table.charts-css tr{display:block;margin:0;padding:0;border:0;background-color:transparent}table.charts-css colgroup,table.charts-css tfoot,table.charts-css thead{display:none}ol.charts-css,ul.charts-css{list-style-type:none}ol.charts-css li,ul.charts-css li{margin:0;padding:0;border:0}.charts-css:not(.show-heading) caption{display:none}.charts-css.show-heading{--heading-size:1.5rem}.charts-css.show-heading caption{display:block;width:100%;height:var(--heading-size)}.charts-css.area tbody tr td:nth-of-type(10n+1)::before,.charts-css.bar.multiple tbody tr td:nth-of-type(10n+1),.charts-css.bar tbody tr:nth-of-type(10n+1) td,.charts-css.column.multiple tbody tr td:nth-of-type(10n+1),.charts-css.column tbody tr:nth-of-type(10n+1) td,.charts-css.line tbody tr td:nth-of-type(10n+1)::before{background:var(--color,var(--color-1))}.charts-css.area tbody tr td:nth-of-type(10n+2)::before,.charts-css.bar.multiple tbody tr td:nth-of-type(10n+2),.charts-css.bar tbody tr:nth-of-type(10n+2) td,.charts-css.column.multiple tbody tr td:nth-of-type(10n+2),.charts-css.column tbody tr:nth-of-type(10n+2) td,.charts-css.line tbody tr td:nth-of-type(10n+2)::before{background:var(--color,var(--color-2))}.charts-css.area tbody tr td:nth-of-type(10n+3)::before,.charts-css.bar.multiple tbody tr td:nth-of-type(10n+3),.charts-css.bar tbody tr:nth-of-type(10n+3) td,.charts-css.column.multiple tbody tr td:nth-of-type(10n+3),.charts-css.column tbody tr:nth-of-type(10n+3) td,.charts-css.line tbody tr td:nth-of-type(10n+3)::before{background:var(--color,var(--color-3))}.charts-css.area tbody tr td:nth-of-type(10n+4)::before,.charts-css.bar.multiple tbody tr td:nth-of-type(10n+4),.charts-css.bar tbody tr:nth-of-type(10n+4) td,.charts-css.column.multiple tbody tr td:nth-of-type(10n+4),.charts-css.column tbody tr:nth-of-type(10n+4) td,.charts-css.line tbody tr td:nth-of-type(10n+4)::before{background:var(--color,var(--color-4))}.charts-css.area tbody tr td:nth-of-type(10n+5)::before,.charts-css.bar.multiple tbody tr td:nth-of-type(10n+5),.charts-css.bar tbody tr:nth-of-type(10n+5) td,.charts-css.column.multiple tbody tr td:nth-of-type(10n+5),.charts-css.column tbody tr:nth-of-type(10n+5) td,.charts-css.line tbody tr td:nth-of-type(10n+5)::before{background:var(--color,var(--color-5))}.charts-css.area tbody tr td:nth-of-type(10n+6)::before,.charts-css.bar.multiple tbody tr td:nth-of-type(10n+6),.charts-css.bar tbody tr:nth-of-type(10n+6) td,.charts-css.column.multiple tbody tr td:nth-of-type(10n+6),.charts-css.column tbody tr:nth-of-type(10n+6) td,.charts-css.line tbody tr td:nth-of-type(10n+6)::before{background:var(--color,var(--color-6))}.charts-css.area tbody tr td:nth-of-type(10n+7)::before,.charts-css.bar.multiple tbody tr td:nth-of-type(10n+7),.charts-css.bar tbody tr:nth-of-type(10n+7) td,.charts-css.column.multiple tbody tr td:nth-of-type(10n+7),.charts-css.column tbody tr:nth-of-type(10n+7) td,.charts-css.line tbody tr td:nth-of-type(10n+7)::before{background:var(--color,var(--color-7))}.charts-css.area tbody tr td:nth-of-type(10n+8)::before,.charts-css.bar.multiple tbody tr td:nth-of-type(10n+8),.charts-css.bar tbody tr:nth-of-type(10n+8) td,.charts-css.column.multiple tbody tr td:nth-of-type(10n+8),.charts-css.column tbody tr:nth-of-type(10n+8) td,.charts-css.line tbody tr td:nth-of-type(10n+8)::before{background:var(--color,var(--color-8))}.charts-css.area tbody tr td:nth-of-type(10n+9)::before,.charts-css.bar.multiple tbody tr td:nth-of-type(10n+9),.charts-css.bar tbody tr:nth-of-type(10n+9) td,.charts-css.column.multiple tbody tr td:nth-of-type(10n+9),.charts-css.column tbody tr:nth-of-type(10n+9) td,.charts-css.line tbody tr td:nth-of-type(10n+9)::before{background:var(--color,var(--color-9))}.charts-css.area tbody tr td:nth-of-type(10n+10)::before,.charts-css.bar.multiple tbody tr td:nth-of-type(10n+10),.charts-css.bar tbody tr:nth-of-type(10n+10) td,.charts-css.column.multiple tbody tr td:nth-of-type(10n+10),.charts-css.column tbody tr:nth-of-type(10n+10) td,.charts-css.line tbody tr td:nth-of-type(10n+10)::before{background:var(--color,var(--color-10))}.charts-css.hide-data .data{opacity:0}.charts-css.show-data-on-hover .data{-webkit-transition-duration:.3s;transition-duration:.3s;opacity:0}.charts-css.show-data-on-hover tr:hover .data{-webkit-transition-duration:.3s;transition-duration:.3s;opacity:1}.charts-css.bar:not(.show-labels){--labels-size:0}.charts-css.bar:not(.show-labels) tbody tr th{display:none}.charts-css.bar.show-labels{--labels-size:80px}.charts-css.bar.show-labels tbody tr th{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:var(--labels-align,center);-ms-flex-pack:var(--labels-align,center);justify-content:var(--labels-align,center);-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.charts-css.bar.show-labels th.hide-label,.charts-css.bar.show-labels tr.hide-label th{display:none}.charts-css.bar.labels-align-start tbody tr th{-webkit-box-align:var(--labels-align,flex-start);-ms-flex-align:var(--labels-align,flex-start);align-items:var(--labels-align,flex-start)}.charts-css.bar.labels-align-end tbody tr th{-webkit-box-align:var(--labels-align,flex-end);-ms-flex-align:var(--labels-align,flex-end);align-items:var(--labels-align,flex-end)}.charts-css.bar.labels-align-center tbody tr th{-webkit-box-align:var(--labels-align,center);-ms-flex-align:var(--labels-align,center);align-items:var(--labels-align,center)}.charts-css.area:not(.show-labels),.charts-css.column:not(.show-labels),.charts-css.line:not(.show-labels){--labels-size:0}.charts-css.area:not(.show-labels) tbody tr th,.charts-css.column:not(.show-labels) tbody tr th,.charts-css.line:not(.show-labels) tbody tr th{display:none}.charts-css.area.show-labels,.charts-css.column.show-labels,.charts-css.line.show-labels{--labels-size:1.5rem}.charts-css.area.show-labels tbody tr th,.charts-css.column.show-labels tbody tr th,.charts-css.line.show-labels tbody tr th{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:var(--labels-align,center);-ms-flex-pack:var(--labels-align,center);justify-content:var(--labels-align,center);-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.charts-css.area.show-labels th.hide-label,.charts-css.area.show-labels tr.hide-label th,.charts-css.column.show-labels th.hide-label,.charts-css.column.show-labels tr.hide-label th,.charts-css.line.show-labels th.hide-label,.charts-css.line.show-labels tr.hide-label th{display:none}.charts-css.area.labels-align-start tbody tr th,.charts-css.column.labels-align-start tbody tr th,.charts-css.line.labels-align-start tbody tr th{-webkit-box-pack:var(--labels-align,flex-start);-ms-flex-pack:var(--labels-align,flex-start);justify-content:var(--labels-align,flex-start)}.charts-css.area.labels-align-end tbody tr th,.charts-css.column.labels-align-end tbody tr th,.charts-css.line.labels-align-end tbody tr th{-webkit-box-pack:var(--labels-align,flex-end);-ms-flex-pack:var(--labels-align,flex-end);justify-content:var(--labels-align,flex-end)}.charts-css.area.labels-align-center tbody tr th,.charts-css.column.labels-align-center tbody tr th,.charts-css.line.labels-align-center tbody tr th{-webkit-box-pack:var(--labels-align,center);-ms-flex-pack:var(--labels-align,center);justify-content:var(--labels-align,center)}.charts-css.area.show-primary-axis:not(.reverse) tbody tr,.charts-css.column.show-primary-axis:not(.reverse) tbody tr,.charts-css.line.show-primary-axis:not(.reverse) tbody tr{-webkit-border-after:var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color);border-block-end:var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color)}.charts-css.area.show-primary-axis.reverse tbody tr,.charts-css.column.show-primary-axis.reverse tbody tr,.charts-css.line.show-primary-axis.reverse tbody tr{-webkit-border-before:var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color);border-block-start:var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color)}.charts-css.area.show-1-secondary-axes:not(.reverse) tbody tr,.charts-css.column.show-1-secondary-axes:not(.reverse) tbody tr,.charts-css.line.show-1-secondary-axes:not(.reverse) tbody tr{background-size:100% 100%;background-image:-webkit-gradient(linear,left top,left bottom,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.area.show-1-secondary-axes.reverse tbody tr,.charts-css.column.show-1-secondary-axes.reverse tbody tr,.charts-css.line.show-1-secondary-axes.reverse tbody tr{background-size:100% 100%;background-image:-webkit-gradient(linear,left bottom,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(0deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.area.show-2-secondary-axes:not(.reverse) tbody tr,.charts-css.column.show-2-secondary-axes:not(.reverse) tbody tr,.charts-css.line.show-2-secondary-axes:not(.reverse) tbody tr{background-size:100% 50%;background-image:-webkit-gradient(linear,left top,left bottom,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.area.show-2-secondary-axes.reverse tbody tr,.charts-css.column.show-2-secondary-axes.reverse tbody tr,.charts-css.line.show-2-secondary-axes.reverse tbody tr{background-size:100% 50%;background-image:-webkit-gradient(linear,left bottom,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(0deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.area.show-3-secondary-axes:not(.reverse) tbody tr,.charts-css.column.show-3-secondary-axes:not(.reverse) tbody tr,.charts-css.line.show-3-secondary-axes:not(.reverse) tbody tr{background-size:100% 33.333333%;background-image:-webkit-gradient(linear,left top,left bottom,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.area.show-3-secondary-axes.reverse tbody tr,.charts-css.column.show-3-secondary-axes.reverse tbody tr,.charts-css.line.show-3-secondary-axes.reverse tbody tr{background-size:100% 33.333333%;background-image:-webkit-gradient(linear,left bottom,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(0deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.area.show-4-secondary-axes:not(.reverse) tbody tr,.charts-css.column.show-4-secondary-axes:not(.reverse) tbody tr,.charts-css.line.show-4-secondary-axes:not(.reverse) tbody tr{background-size:100% 25%;background-image:-webkit-gradient(linear,left top,left bottom,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.area.show-4-secondary-axes.reverse tbody tr,.charts-css.column.show-4-secondary-axes.reverse tbody tr,.charts-css.line.show-4-secondary-axes.reverse tbody tr{background-size:100% 25%;background-image:-webkit-gradient(linear,left bottom,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(0deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.area.show-5-secondary-axes:not(.reverse) tbody tr,.charts-css.column.show-5-secondary-axes:not(.reverse) tbody tr,.charts-css.line.show-5-secondary-axes:not(.reverse) tbody tr{background-size:100% 20%;background-image:-webkit-gradient(linear,left top,left bottom,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.area.show-5-secondary-axes.reverse tbody tr,.charts-css.column.show-5-secondary-axes.reverse tbody tr,.charts-css.line.show-5-secondary-axes.reverse tbody tr{background-size:100% 20%;background-image:-webkit-gradient(linear,left bottom,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(0deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.area.show-6-secondary-axes:not(.reverse) tbody tr,.charts-css.column.show-6-secondary-axes:not(.reverse) tbody tr,.charts-css.line.show-6-secondary-axes:not(.reverse) tbody tr{background-size:100% 16.666667%;background-image:-webkit-gradient(linear,left top,left bottom,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.area.show-6-secondary-axes.reverse tbody tr,.charts-css.column.show-6-secondary-axes.reverse tbody tr,.charts-css.line.show-6-secondary-axes.reverse tbody tr{background-size:100% 16.666667%;background-image:-webkit-gradient(linear,left bottom,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(0deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.area.show-7-secondary-axes:not(.reverse) tbody tr,.charts-css.column.show-7-secondary-axes:not(.reverse) tbody tr,.charts-css.line.show-7-secondary-axes:not(.reverse) tbody tr{background-size:100% 14.285714%;background-image:-webkit-gradient(linear,left top,left bottom,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.area.show-7-secondary-axes.reverse tbody tr,.charts-css.column.show-7-secondary-axes.reverse tbody tr,.charts-css.line.show-7-secondary-axes.reverse tbody tr{background-size:100% 14.285714%;background-image:-webkit-gradient(linear,left bottom,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(0deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.area.show-8-secondary-axes:not(.reverse) tbody tr,.charts-css.column.show-8-secondary-axes:not(.reverse) tbody tr,.charts-css.line.show-8-secondary-axes:not(.reverse) tbody tr{background-size:100% 12.5%;background-image:-webkit-gradient(linear,left top,left bottom,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.area.show-8-secondary-axes.reverse tbody tr,.charts-css.column.show-8-secondary-axes.reverse tbody tr,.charts-css.line.show-8-secondary-axes.reverse tbody tr{background-size:100% 12.5%;background-image:-webkit-gradient(linear,left bottom,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(0deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.area.show-9-secondary-axes:not(.reverse) tbody tr,.charts-css.column.show-9-secondary-axes:not(.reverse) tbody tr,.charts-css.line.show-9-secondary-axes:not(.reverse) tbody tr{background-size:100% 11.111111%;background-image:-webkit-gradient(linear,left top,left bottom,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.area.show-9-secondary-axes.reverse tbody tr,.charts-css.column.show-9-secondary-axes.reverse tbody tr,.charts-css.line.show-9-secondary-axes.reverse tbody tr{background-size:100% 11.111111%;background-image:-webkit-gradient(linear,left bottom,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(0deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.area.show-10-secondary-axes:not(.reverse) tbody tr,.charts-css.column.show-10-secondary-axes:not(.reverse) tbody tr,.charts-css.line.show-10-secondary-axes:not(.reverse) tbody tr{background-size:100% 10%;background-image:-webkit-gradient(linear,left top,left bottom,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.area.show-10-secondary-axes.reverse tbody tr,.charts-css.column.show-10-secondary-axes.reverse tbody tr,.charts-css.line.show-10-secondary-axes.reverse tbody tr{background-size:100% 10%;background-image:-webkit-gradient(linear,left bottom,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(0deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.area.show-data-axes tbody tr,.charts-css.column.show-data-axes tbody tr,.charts-css.line.show-data-axes tbody tr{-webkit-border-end:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color);border-inline-end:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color)}.charts-css.area.show-data-axes.reverse-data tbody tr:last-of-type,.charts-css.area.show-data-axes:not(.reverse-data) tbody tr:first-of-type,.charts-css.column.show-data-axes.reverse-data tbody tr:last-of-type,.charts-css.column.show-data-axes:not(.reverse-data) tbody tr:first-of-type,.charts-css.line.show-data-axes.reverse-data tbody tr:last-of-type,.charts-css.line.show-data-axes:not(.reverse-data) tbody tr:first-of-type{-webkit-border-start:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color);border-inline-start:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color)}.charts-css.area.show-dataset-axes tbody tr td,.charts-css.column.show-dataset-axes tbody tr td,.charts-css.line.show-dataset-axes tbody tr td{-webkit-border-end:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color);border-inline-end:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color)}.charts-css.area.show-dataset-axes.reverse-data tbody tr:last-of-type td,.charts-css.area.show-dataset-axes:not(.reverse-data) tbody tr:first-of-type td,.charts-css.column.show-dataset-axes.reverse-data tbody tr:last-of-type td,.charts-css.column.show-dataset-axes:not(.reverse-data) tbody tr:first-of-type td,.charts-css.line.show-dataset-axes.reverse-data tbody tr:last-of-type td,.charts-css.line.show-dataset-axes:not(.reverse-data) tbody tr:first-of-type td{-webkit-border-start:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color);border-inline-start:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color)}.charts-css.bar.show-primary-axis:not(.reverse) tbody tr{-webkit-border-start:var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color);border-inline-start:var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color)}.charts-css.bar.show-primary-axis.reverse tbody tr{-webkit-border-end:var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color);border-inline-end:var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color)}.charts-css.bar.show-1-secondary-axes:not(.reverse) tbody tr{background-size:100% 100%;background-image:-webkit-gradient(linear,right top,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(-90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.bar.show-1-secondary-axes.reverse tbody tr{background-size:100% 100%;background-image:-webkit-gradient(linear,left top,right top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.bar.show-2-secondary-axes:not(.reverse) tbody tr{background-size:50% 100%;background-image:-webkit-gradient(linear,right top,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(-90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.bar.show-2-secondary-axes.reverse tbody tr{background-size:50% 100%;background-image:-webkit-gradient(linear,left top,right top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.bar.show-3-secondary-axes:not(.reverse) tbody tr{background-size:33.333333% 100%;background-image:-webkit-gradient(linear,right top,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(-90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.bar.show-3-secondary-axes.reverse tbody tr{background-size:33.333333% 100%;background-image:-webkit-gradient(linear,left top,right top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.bar.show-4-secondary-axes:not(.reverse) tbody tr{background-size:25% 100%;background-image:-webkit-gradient(linear,right top,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(-90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.bar.show-4-secondary-axes.reverse tbody tr{background-size:25% 100%;background-image:-webkit-gradient(linear,left top,right top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.bar.show-5-secondary-axes:not(.reverse) tbody tr{background-size:20% 100%;background-image:-webkit-gradient(linear,right top,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(-90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.bar.show-5-secondary-axes.reverse tbody tr{background-size:20% 100%;background-image:-webkit-gradient(linear,left top,right top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.bar.show-6-secondary-axes:not(.reverse) tbody tr{background-size:16.666667% 100%;background-image:-webkit-gradient(linear,right top,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(-90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.bar.show-6-secondary-axes.reverse tbody tr{background-size:16.666667% 100%;background-image:-webkit-gradient(linear,left top,right top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.bar.show-7-secondary-axes:not(.reverse) tbody tr{background-size:14.285714% 100%;background-image:-webkit-gradient(linear,right top,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(-90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.bar.show-7-secondary-axes.reverse tbody tr{background-size:14.285714% 100%;background-image:-webkit-gradient(linear,left top,right top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.bar.show-8-secondary-axes:not(.reverse) tbody tr{background-size:12.5% 100%;background-image:-webkit-gradient(linear,right top,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(-90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.bar.show-8-secondary-axes.reverse tbody tr{background-size:12.5% 100%;background-image:-webkit-gradient(linear,left top,right top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.bar.show-9-secondary-axes:not(.reverse) tbody tr{background-size:11.111111% 100%;background-image:-webkit-gradient(linear,right top,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(-90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.bar.show-9-secondary-axes.reverse tbody tr{background-size:11.111111% 100%;background-image:-webkit-gradient(linear,left top,right top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.bar.show-10-secondary-axes:not(.reverse) tbody tr{background-size:10% 100%;background-image:-webkit-gradient(linear,right top,left top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(-90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.bar.show-10-secondary-axes.reverse tbody tr{background-size:10% 100%;background-image:-webkit-gradient(linear,left top,right top,from(var(--secondary-axes-color)),to(transparent));background-image:linear-gradient(90deg,var(--secondary-axes-color) var(--secondary-axes-width),transparent var(--secondary-axes-width))}.charts-css.bar.show-data-axes tbody tr{-webkit-border-after:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color);border-block-end:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color)}.charts-css.bar.show-data-axes.reverse-data tbody tr:last-of-type,.charts-css.bar.show-data-axes:not(.reverse-data) tbody tr:first-of-type{-webkit-border-before:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color);border-block-start:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color)}.charts-css.bar.show-dataset-axes tbody tr td{-webkit-border-after:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color);border-block-end:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color)}.charts-css.bar.show-dataset-axes.reverse-data tbody tr:last-of-type td,.charts-css.bar.show-dataset-axes:not(.reverse-data) tbody tr:first-of-type td{-webkit-border-before:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color);border-block-start:var(--data-axes-width) var(--data-axes-style) var(--data-axes-color)}.charts-css.legend{padding:1rem;border:1px solid var(--legend-border-color);list-style:none;font-size:1rem}.charts-css.legend li{line-height:2;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.charts-css.legend li::before{content:"";display:inline-block;vertical-align:middle;-webkit-margin-end:.5rem;margin-inline-end:.5rem;border-width:2px;border-style:solid}.charts-css.legend li:first-child::before{background-color:var(--color-1,transparent);border-color:var(--border-color-1,var(--border-color,#000))}.charts-css.legend li:nth-child(2)::before{background-color:var(--color-2,transparent);border-color:var(--border-color-2,var(--border-color,#000))}.charts-css.legend li:nth-child(3)::before{background-color:var(--color-3,transparent);border-color:var(--border-color-3,var(--border-color,#000))}.charts-css.legend li:nth-child(4)::before{background-color:var(--color-4,transparent);border-color:var(--border-color-4,var(--border-color,#000))}.charts-css.legend li:nth-child(5)::before{background-color:var(--color-5,transparent);border-color:var(--border-color-5,var(--border-color,#000))}.charts-css.legend li:nth-child(6)::before{background-color:var(--color-6,transparent);border-color:var(--border-color-6,var(--border-color,#000))}.charts-css.legend li:nth-child(7)::before{background-color:var(--color-7,transparent);border-color:var(--border-color-7,var(--border-color,#000))}.charts-css.legend li:nth-child(8)::before{background-color:var(--color-8,transparent);border-color:var(--border-color-8,var(--border-color,#000))}.charts-css.legend li:nth-child(9)::before{background-color:var(--color-9,transparent);border-color:var(--border-color-9,var(--border-color,#000))}.charts-css.legend li:nth-child(10)::before{background-color:var(--color-10,transparent);border-color:var(--border-color-10,var(--border-color,#000))}.charts-css:not(.legend-inline){-webkit-box-orient:vertical;-ms-flex-direction:column;flex-direction:column;-ms-flex-wrap:nowrap;flex-wrap:nowrap}.charts-css.legend-inline,.charts-css:not(.legend-inline){display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-direction:normal}.charts-css.legend-inline{-webkit-box-orient:horizontal;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:wrap;flex-wrap:wrap}.charts-css.legend-inline li{-webkit-margin-end:1rem;margin-inline-end:1rem}.charts-css.legend-circle li::before{width:1rem;height:1rem;border-radius:50%}.charts-css.legend-ellipse li::before{width:2rem;height:1rem;border-radius:50%}.charts-css.legend-rhombus li::before,.charts-css.legend-square li::before{width:1rem;height:1rem;border-radius:3px}.charts-css.legend-rhombus li::before{-webkit-transform:rotate(45deg) scale(.85);transform:rotate(45deg) scale(.85)}.charts-css.legend-rectangle li::before{width:2rem;height:1rem;border-radius:3px}.charts-css.legend-line li::before{width:2rem;height:3px;border-radius:2px;-webkit-box-sizing:content-box;box-sizing:content-box}.charts-css .tooltip{position:absolute;z-index:1;bottom:50%;left:50%;-webkit-transform:translateX(-50%);transform:translateX(-50%);width:-webkit-max-content;width:-moz-max-content;width:max-content;padding:5px 10px;border-radius:6px;visibility:hidden;opacity:0;-webkit-transition:opacity .3s;transition:opacity .3s;background-color:#555;color:#fff;text-align:center;font-size:.9rem}.charts-css .tooltip::after{content:"";position:absolute;top:100%;left:50%;margin-left:-5px;border:5px solid transparent;border-top-color:#555}.charts-css td:hover .tooltip{visibility:visible;opacity:1}.charts-css.bar tbody{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;width:100%;height:calc(100% - var(--heading-size))}.charts-css.bar tbody,.charts-css.bar tbody tr{display:-webkit-box;display:-ms-flexbox;display:flex}.charts-css.bar tbody tr{position:relative;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-negative:1;flex-shrink:1;-ms-flex-preferred-size:0;flex-basis:0;overflow-wrap:anywhere;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.charts-css.bar tbody tr th{position:absolute;top:0;bottom:0}.charts-css.bar tbody tr td{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:calc(100%*var(--size, 1));height:100%;position:relative}.charts-css.bar:not(.reverse) tbody tr{-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-margin-start:var(--labels-size);margin-inline-start:var(--labels-size)}.charts-css.bar:not(.reverse) tbody tr th{left:calc(var(--labels-size)*-1 - var(--primary-axis-width));width:var(--labels-size)}.charts-css.bar:not(.reverse) tbody tr td{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.charts-css.bar.reverse tbody tr{-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end;-webkit-margin-end:var(--labels-size);margin-inline-end:var(--labels-size)}.charts-css.bar.reverse tbody tr th{right:calc(var(--labels-size)*-1 - var(--primary-axis-width));width:var(--labels-size)}.charts-css.bar.reverse tbody tr td{-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.charts-css.bar:not(.stacked) tbody tr td{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-negative:1;flex-shrink:1;-ms-flex-preferred-size:0;flex-basis:0}.charts-css.bar.stacked tbody tr td{-webkit-box-flex:unset;-ms-flex-positive:unset;flex-grow:unset;-ms-flex-negative:unset;flex-shrink:unset;-ms-flex-preferred-size:unset;flex-basis:unset}.charts-css.bar.stacked.reverse-datasets tbody tr{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.charts-css.bar:not(.reverse-data) tbody{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.charts-css.bar.reverse-data tbody{-webkit-box-orient:vertical;-webkit-box-direction:reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse}.charts-css.bar:not(.reverse-datasets):not(.stacked) tbody tr{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.charts-css.bar:not(.reverse-datasets).stacked:not(.reverse) tbody tr{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.charts-css.bar:not(.reverse-datasets).stacked.reverse tbody tr{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.charts-css.bar.reverse-datasets:not(.stacked) tbody tr{-webkit-box-orient:vertical;-webkit-box-direction:reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse}.charts-css.bar.reverse-datasets.stacked:not(.reverse) tbody tr{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.charts-css.bar.reverse-datasets.stacked.reverse tbody tr{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.charts-css.bar.data-spacing-1 tbody tr{-webkit-padding-before:1px;padding-block-start:1px;-webkit-padding-after:1px;padding-block-end:1px}.charts-css.bar.data-spacing-2 tbody tr{-webkit-padding-before:2px;padding-block-start:2px;-webkit-padding-after:2px;padding-block-end:2px}.charts-css.bar.data-spacing-3 tbody tr{-webkit-padding-before:3px;padding-block-start:3px;-webkit-padding-after:3px;padding-block-end:3px}.charts-css.bar.data-spacing-4 tbody tr{-webkit-padding-before:4px;padding-block-start:4px;-webkit-padding-after:4px;padding-block-end:4px}.charts-css.bar.data-spacing-5 tbody tr{-webkit-padding-before:5px;padding-block-start:5px;-webkit-padding-after:5px;padding-block-end:5px}.charts-css.bar.data-spacing-6 tbody tr{-webkit-padding-before:6px;padding-block-start:6px;-webkit-padding-after:6px;padding-block-end:6px}.charts-css.bar.data-spacing-7 tbody tr{-webkit-padding-before:7px;padding-block-start:7px;-webkit-padding-after:7px;padding-block-end:7px}.charts-css.bar.data-spacing-8 tbody tr{-webkit-padding-before:8px;padding-block-start:8px;-webkit-padding-after:8px;padding-block-end:8px}.charts-css.bar.data-spacing-9 tbody tr{-webkit-padding-before:9px;padding-block-start:9px;-webkit-padding-after:9px;padding-block-end:9px}.charts-css.bar.data-spacing-10 tbody tr{-webkit-padding-before:10px;padding-block-start:10px;-webkit-padding-after:10px;padding-block-end:10px}.charts-css.bar.data-spacing-11 tbody tr{-webkit-padding-before:11px;padding-block-start:11px;-webkit-padding-after:11px;padding-block-end:11px}.charts-css.bar.data-spacing-12 tbody tr{-webkit-padding-before:12px;padding-block-start:12px;-webkit-padding-after:12px;padding-block-end:12px}.charts-css.bar.data-spacing-13 tbody tr{-webkit-padding-before:13px;padding-block-start:13px;-webkit-padding-after:13px;padding-block-end:13px}.charts-css.bar.data-spacing-14 tbody tr{-webkit-padding-before:14px;padding-block-start:14px;-webkit-padding-after:14px;padding-block-end:14px}.charts-css.bar.data-spacing-15 tbody tr{-webkit-padding-before:15px;padding-block-start:15px;-webkit-padding-after:15px;padding-block-end:15px}.charts-css.bar.data-spacing-16 tbody tr{-webkit-padding-before:16px;padding-block-start:16px;-webkit-padding-after:16px;padding-block-end:16px}.charts-css.bar.data-spacing-17 tbody tr{-webkit-padding-before:17px;padding-block-start:17px;-webkit-padding-after:17px;padding-block-end:17px}.charts-css.bar.data-spacing-18 tbody tr{-webkit-padding-before:18px;padding-block-start:18px;-webkit-padding-after:18px;padding-block-end:18px}.charts-css.bar.data-spacing-19 tbody tr{-webkit-padding-before:19px;padding-block-start:19px;-webkit-padding-after:19px;padding-block-end:19px}.charts-css.bar.data-spacing-20 tbody tr{-webkit-padding-before:20px;padding-block-start:20px;-webkit-padding-after:20px;padding-block-end:20px}.charts-css.bar.datasets-spacing-1 tbody tr td{-webkit-margin-before:1px;margin-block-start:1px;-webkit-margin-after:1px;margin-block-end:1px}.charts-css.bar.datasets-spacing-2 tbody tr td{-webkit-margin-before:2px;margin-block-start:2px;-webkit-margin-after:2px;margin-block-end:2px}.charts-css.bar.datasets-spacing-3 tbody tr td{-webkit-margin-before:3px;margin-block-start:3px;-webkit-margin-after:3px;margin-block-end:3px}.charts-css.bar.datasets-spacing-4 tbody tr td{-webkit-margin-before:4px;margin-block-start:4px;-webkit-margin-after:4px;margin-block-end:4px}.charts-css.bar.datasets-spacing-5 tbody tr td{-webkit-margin-before:5px;margin-block-start:5px;-webkit-margin-after:5px;margin-block-end:5px}.charts-css.bar.datasets-spacing-6 tbody tr td{-webkit-margin-before:6px;margin-block-start:6px;-webkit-margin-after:6px;margin-block-end:6px}.charts-css.bar.datasets-spacing-7 tbody tr td{-webkit-margin-before:7px;margin-block-start:7px;-webkit-margin-after:7px;margin-block-end:7px}.charts-css.bar.datasets-spacing-8 tbody tr td{-webkit-margin-before:8px;margin-block-start:8px;-webkit-margin-after:8px;margin-block-end:8px}.charts-css.bar.datasets-spacing-9 tbody tr td{-webkit-margin-before:9px;margin-block-start:9px;-webkit-margin-after:9px;margin-block-end:9px}.charts-css.bar.datasets-spacing-10 tbody tr td{-webkit-margin-before:10px;margin-block-start:10px;-webkit-margin-after:10px;margin-block-end:10px}.charts-css.bar.datasets-spacing-11 tbody tr td{-webkit-margin-before:11px;margin-block-start:11px;-webkit-margin-after:11px;margin-block-end:11px}.charts-css.bar.datasets-spacing-12 tbody tr td{-webkit-margin-before:12px;margin-block-start:12px;-webkit-margin-after:12px;margin-block-end:12px}.charts-css.bar.datasets-spacing-13 tbody tr td{-webkit-margin-before:13px;margin-block-start:13px;-webkit-margin-after:13px;margin-block-end:13px}.charts-css.bar.datasets-spacing-14 tbody tr td{-webkit-margin-before:14px;margin-block-start:14px;-webkit-margin-after:14px;margin-block-end:14px}.charts-css.bar.datasets-spacing-15 tbody tr td{-webkit-margin-before:15px;margin-block-start:15px;-webkit-margin-after:15px;margin-block-end:15px}.charts-css.bar.datasets-spacing-16 tbody tr td{-webkit-margin-before:16px;margin-block-start:16px;-webkit-margin-after:16px;margin-block-end:16px}.charts-css.bar.datasets-spacing-17 tbody tr td{-webkit-margin-before:17px;margin-block-start:17px;-webkit-margin-after:17px;margin-block-end:17px}.charts-css.bar.datasets-spacing-18 tbody tr td{-webkit-margin-before:18px;margin-block-start:18px;-webkit-margin-after:18px;margin-block-end:18px}.charts-css.bar.datasets-spacing-19 tbody tr td{-webkit-margin-before:19px;margin-block-start:19px;-webkit-margin-after:19px;margin-block-end:19px}.charts-css.bar.datasets-spacing-20 tbody tr td{-webkit-margin-before:20px;margin-block-start:20px;-webkit-margin-after:20px;margin-block-end:20px}.charts-css.column tbody{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;width:100%;height:calc(100% - var(--heading-size))}.charts-css.column tbody tr{position:relative;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-negative:1;flex-shrink:1;-ms-flex-preferred-size:0;flex-basis:0;overflow-wrap:anywhere;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.charts-css.column tbody tr th{position:absolute;right:0;left:0}.charts-css.column tbody tr td{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:100%;height:calc(100%*var(--size, 1));position:relative}.charts-css.column:not(.reverse) tbody tr{-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end;-webkit-margin-after:var(--labels-size);margin-block-end:var(--labels-size)}.charts-css.column:not(.reverse) tbody tr th{bottom:calc(var(--labels-size)*-1 - var(--primary-axis-width));height:var(--labels-size)}.charts-css.column.reverse tbody tr,.charts-css.column:not(.reverse) tbody tr td{-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.charts-css.column.reverse tbody tr{-webkit-margin-before:var(--labels-size);margin-block-start:var(--labels-size)}.charts-css.column.reverse tbody tr th{top:calc(var(--labels-size)*-1 - var(--primary-axis-width));height:var(--labels-size)}.charts-css.column.reverse tbody tr td{-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end}.charts-css.column:not(.stacked) tbody tr td{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-negative:1;flex-shrink:1;-ms-flex-preferred-size:0;flex-basis:0}.charts-css.column.stacked tbody tr td{-webkit-box-flex:unset;-ms-flex-positive:unset;flex-grow:unset;-ms-flex-negative:unset;flex-shrink:unset;-ms-flex-preferred-size:unset;flex-basis:unset}.charts-css.column.stacked.reverse-datasets tbody tr{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.charts-css.column:not(.reverse-data) tbody{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.charts-css.column.reverse-data tbody{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.charts-css.column:not(.reverse-datasets):not(.stacked) tbody tr{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.charts-css.column:not(.reverse-datasets).stacked:not(.reverse) tbody tr{-webkit-box-orient:vertical;-webkit-box-direction:reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse}.charts-css.column:not(.reverse-datasets).stacked.reverse tbody tr{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.charts-css.column.reverse-datasets:not(.stacked) tbody tr{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.charts-css.column.reverse-datasets.stacked:not(.reverse) tbody tr{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.charts-css.column.reverse-datasets.stacked.reverse tbody tr{-webkit-box-orient:vertical;-webkit-box-direction:reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse}.charts-css.column.data-spacing-1 tbody tr{-webkit-padding-start:1px;padding-inline-start:1px;-webkit-padding-end:1px;padding-inline-end:1px}.charts-css.column.data-spacing-2 tbody tr{-webkit-padding-start:2px;padding-inline-start:2px;-webkit-padding-end:2px;padding-inline-end:2px}.charts-css.column.data-spacing-3 tbody tr{-webkit-padding-start:3px;padding-inline-start:3px;-webkit-padding-end:3px;padding-inline-end:3px}.charts-css.column.data-spacing-4 tbody tr{-webkit-padding-start:4px;padding-inline-start:4px;-webkit-padding-end:4px;padding-inline-end:4px}.charts-css.column.data-spacing-5 tbody tr{-webkit-padding-start:5px;padding-inline-start:5px;-webkit-padding-end:5px;padding-inline-end:5px}.charts-css.column.data-spacing-6 tbody tr{-webkit-padding-start:6px;padding-inline-start:6px;-webkit-padding-end:6px;padding-inline-end:6px}.charts-css.column.data-spacing-7 tbody tr{-webkit-padding-start:7px;padding-inline-start:7px;-webkit-padding-end:7px;padding-inline-end:7px}.charts-css.column.data-spacing-8 tbody tr{-webkit-padding-start:8px;padding-inline-start:8px;-webkit-padding-end:8px;padding-inline-end:8px}.charts-css.column.data-spacing-9 tbody tr{-webkit-padding-start:9px;padding-inline-start:9px;-webkit-padding-end:9px;padding-inline-end:9px}.charts-css.column.data-spacing-10 tbody tr{-webkit-padding-start:10px;padding-inline-start:10px;-webkit-padding-end:10px;padding-inline-end:10px}.charts-css.column.data-spacing-11 tbody tr{-webkit-padding-start:11px;padding-inline-start:11px;-webkit-padding-end:11px;padding-inline-end:11px}.charts-css.column.data-spacing-12 tbody tr{-webkit-padding-start:12px;padding-inline-start:12px;-webkit-padding-end:12px;padding-inline-end:12px}.charts-css.column.data-spacing-13 tbody tr{-webkit-padding-start:13px;padding-inline-start:13px;-webkit-padding-end:13px;padding-inline-end:13px}.charts-css.column.data-spacing-14 tbody tr{-webkit-padding-start:14px;padding-inline-start:14px;-webkit-padding-end:14px;padding-inline-end:14px}.charts-css.column.data-spacing-15 tbody tr{-webkit-padding-start:15px;padding-inline-start:15px;-webkit-padding-end:15px;padding-inline-end:15px}.charts-css.column.data-spacing-16 tbody tr{-webkit-padding-start:16px;padding-inline-start:16px;-webkit-padding-end:16px;padding-inline-end:16px}.charts-css.column.data-spacing-17 tbody tr{-webkit-padding-start:17px;padding-inline-start:17px;-webkit-padding-end:17px;padding-inline-end:17px}.charts-css.column.data-spacing-18 tbody tr{-webkit-padding-start:18px;padding-inline-start:18px;-webkit-padding-end:18px;padding-inline-end:18px}.charts-css.column.data-spacing-19 tbody tr{-webkit-padding-start:19px;padding-inline-start:19px;-webkit-padding-end:19px;padding-inline-end:19px}.charts-css.column.data-spacing-20 tbody tr{-webkit-padding-start:20px;padding-inline-start:20px;-webkit-padding-end:20px;padding-inline-end:20px}.charts-css.column.datasets-spacing-1 tbody tr td{-webkit-margin-start:1px;margin-inline-start:1px;-webkit-margin-end:1px;margin-inline-end:1px}.charts-css.column.datasets-spacing-2 tbody tr td{-webkit-margin-start:2px;margin-inline-start:2px;-webkit-margin-end:2px;margin-inline-end:2px}.charts-css.column.datasets-spacing-3 tbody tr td{-webkit-margin-start:3px;margin-inline-start:3px;-webkit-margin-end:3px;margin-inline-end:3px}.charts-css.column.datasets-spacing-4 tbody tr td{-webkit-margin-start:4px;margin-inline-start:4px;-webkit-margin-end:4px;margin-inline-end:4px}.charts-css.column.datasets-spacing-5 tbody tr td{-webkit-margin-start:5px;margin-inline-start:5px;-webkit-margin-end:5px;margin-inline-end:5px}.charts-css.column.datasets-spacing-6 tbody tr td{-webkit-margin-start:6px;margin-inline-start:6px;-webkit-margin-end:6px;margin-inline-end:6px}.charts-css.column.datasets-spacing-7 tbody tr td{-webkit-margin-start:7px;margin-inline-start:7px;-webkit-margin-end:7px;margin-inline-end:7px}.charts-css.column.datasets-spacing-8 tbody tr td{-webkit-margin-start:8px;margin-inline-start:8px;-webkit-margin-end:8px;margin-inline-end:8px}.charts-css.column.datasets-spacing-9 tbody tr td{-webkit-margin-start:9px;margin-inline-start:9px;-webkit-margin-end:9px;margin-inline-end:9px}.charts-css.column.datasets-spacing-10 tbody tr td{-webkit-margin-start:10px;margin-inline-start:10px;-webkit-margin-end:10px;margin-inline-end:10px}.charts-css.column.datasets-spacing-11 tbody tr td{-webkit-margin-start:11px;margin-inline-start:11px;-webkit-margin-end:11px;margin-inline-end:11px}.charts-css.column.datasets-spacing-12 tbody tr td{-webkit-margin-start:12px;margin-inline-start:12px;-webkit-margin-end:12px;margin-inline-end:12px}.charts-css.column.datasets-spacing-13 tbody tr td{-webkit-margin-start:13px;margin-inline-start:13px;-webkit-margin-end:13px;margin-inline-end:13px}.charts-css.column.datasets-spacing-14 tbody tr td{-webkit-margin-start:14px;margin-inline-start:14px;-webkit-margin-end:14px;margin-inline-end:14px}.charts-css.column.datasets-spacing-15 tbody tr td{-webkit-margin-start:15px;margin-inline-start:15px;-webkit-margin-end:15px;margin-inline-end:15px}.charts-css.column.datasets-spacing-16 tbody tr td{-webkit-margin-start:16px;margin-inline-start:16px;-webkit-margin-end:16px;margin-inline-end:16px}.charts-css.column.datasets-spacing-17 tbody tr td{-webkit-margin-start:17px;margin-inline-start:17px;-webkit-margin-end:17px;margin-inline-end:17px}.charts-css.column.datasets-spacing-18 tbody tr td{-webkit-margin-start:18px;margin-inline-start:18px;-webkit-margin-end:18px;margin-inline-end:18px}.charts-css.column.datasets-spacing-19 tbody tr td{-webkit-margin-start:19px;margin-inline-start:19px;-webkit-margin-end:19px;margin-inline-end:19px}.charts-css.column.datasets-spacing-20 tbody tr td{-webkit-margin-start:20px;margin-inline-start:20px;-webkit-margin-end:20px;margin-inline-end:20px}.charts-css.area tbody{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;width:100%;height:calc(100% - var(--heading-size))}.charts-css.area tbody,.charts-css.area tbody tr{display:-webkit-box;display:-ms-flexbox;display:flex}.charts-css.area tbody tr{position:relative;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-negative:1;flex-shrink:1;-ms-flex-preferred-size:0;flex-basis:0;overflow-wrap:anywhere;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.charts-css.area tbody tr th{position:absolute;right:0;left:0}.charts-css.area tbody tr td{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-flow:column;flex-flow:column;width:100%;height:100%;position:absolute;top:0;right:0;bottom:0;left:0;z-index:0}.charts-css.area tbody tr td::before{content:"";position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1}.charts-css.area tbody tr td::after{content:"";width:100%}.charts-css.area:not(.reverse) tbody tr{-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end;-webkit-margin-after:var(--labels-size);margin-block-end:var(--labels-size)}.charts-css.area:not(.reverse) tbody tr th{bottom:calc(var(--labels-size)*-1 - var(--primary-axis-width));height:var(--labels-size)}.charts-css.area.reverse tbody tr,.charts-css.area:not(.reverse) tbody tr td{-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.charts-css.area.reverse tbody tr{-webkit-margin-before:var(--labels-size);margin-block-start:var(--labels-size)}.charts-css.area.reverse tbody tr th{top:calc(var(--labels-size)*-1 - var(--primary-axis-width));height:var(--labels-size)}.charts-css.area.reverse tbody tr td{-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end}.charts-css.area:not(.reverse-data) tbody{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.charts-css.area.reverse-data tbody{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.charts-css.area:not(.reverse-datasets) tbody tr{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.charts-css.area.reverse-datasets tbody tr{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.charts-css.area:not(.reverse):not(.reverse-data) tbody tr td{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end}.charts-css.area:not(.reverse):not(.reverse-data) tbody tr td::before{-webkit-clip-path:polygon(0 calc(100%*(1 - var(--start, var(--size)))),100% calc(100%*(1 - var(--size))),100% 100%,0 100%);clip-path:polygon(0 calc(100%*(1 - var(--start, var(--size)))),100% calc(100%*(1 - var(--size))),100% 100%,0 100%)}.charts-css.area:not(.reverse):not(.reverse-data) tbody tr td .data{-webkit-transform:translateX(50%);transform:translateX(50%)}.charts-css.area:not(.reverse):not(.reverse-data) tbody tr td::after{height:calc(100%*var(--size))}.charts-css.area:not(.reverse).reverse-data tbody tr td{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.charts-css.area:not(.reverse).reverse-data tbody tr td::before{-webkit-clip-path:polygon(0 calc(100%*(1 - var(--size))),100% calc(100%*(1 - var(--start, var(--size)))),100% 100%,0 100%);clip-path:polygon(0 calc(100%*(1 - var(--size))),100% calc(100%*(1 - var(--start, var(--size)))),100% 100%,0 100%)}.charts-css.area:not(.reverse).reverse-data tbody tr td .data{-webkit-transform:translateX(-50%);transform:translateX(-50%)}.charts-css.area:not(.reverse).reverse-data tbody tr td::after{height:calc(100%*var(--size))}.charts-css.area.reverse:not(.reverse-data) tbody tr td{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end}.charts-css.area.reverse:not(.reverse-data) tbody tr td::before{-webkit-clip-path:polygon(0 0,100% 0,100% calc(100%*var(--size)),0 calc(100%*var(--start, var(--size))));clip-path:polygon(0 0,100% 0,100% calc(100%*var(--size)),0 calc(100%*var(--start, var(--size))))}.charts-css.area.reverse:not(.reverse-data) tbody tr td .data{-webkit-transform:translateX(50%);transform:translateX(50%)}.charts-css.area.reverse:not(.reverse-data) tbody tr td::after{height:calc(100%*(1 - var(--size)))}.charts-css.area.reverse.reverse-data tbody tr td{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.charts-css.area.reverse.reverse-data tbody tr td::before{-webkit-clip-path:polygon(0 0,100% 0,100% calc(100%*var(--start, var(--size))),0 calc(100%*var(--size)));clip-path:polygon(0 0,100% 0,100% calc(100%*var(--start, var(--size))),0 calc(100%*var(--size)))}.charts-css.area.reverse.reverse-data tbody tr td .data{-webkit-transform:translateX(-50%);transform:translateX(-50%)}.charts-css.area.reverse.reverse-data tbody tr td::after{height:calc(100%*(1 - var(--size)))}.charts-css.area.data-spacing-1 tbody tr td::before,.charts-css.area.datasets-spacing-1 tbody tr td::before{-webkit-margin-start:1px;margin-inline-start:1px;-webkit-margin-end:1px;margin-inline-end:1px}.charts-css.area.data-spacing-2 tbody tr td::before,.charts-css.area.datasets-spacing-2 tbody tr td::before{-webkit-margin-start:2px;margin-inline-start:2px;-webkit-margin-end:2px;margin-inline-end:2px}.charts-css.area.data-spacing-3 tbody tr td::before,.charts-css.area.datasets-spacing-3 tbody tr td::before{-webkit-margin-start:3px;margin-inline-start:3px;-webkit-margin-end:3px;margin-inline-end:3px}.charts-css.area.data-spacing-4 tbody tr td::before,.charts-css.area.datasets-spacing-4 tbody tr td::before{-webkit-margin-start:4px;margin-inline-start:4px;-webkit-margin-end:4px;margin-inline-end:4px}.charts-css.area.data-spacing-5 tbody tr td::before,.charts-css.area.datasets-spacing-5 tbody tr td::before{-webkit-margin-start:5px;margin-inline-start:5px;-webkit-margin-end:5px;margin-inline-end:5px}.charts-css.area.data-spacing-6 tbody tr td::before,.charts-css.area.datasets-spacing-6 tbody tr td::before{-webkit-margin-start:6px;margin-inline-start:6px;-webkit-margin-end:6px;margin-inline-end:6px}.charts-css.area.data-spacing-7 tbody tr td::before,.charts-css.area.datasets-spacing-7 tbody tr td::before{-webkit-margin-start:7px;margin-inline-start:7px;-webkit-margin-end:7px;margin-inline-end:7px}.charts-css.area.data-spacing-8 tbody tr td::before,.charts-css.area.datasets-spacing-8 tbody tr td::before{-webkit-margin-start:8px;margin-inline-start:8px;-webkit-margin-end:8px;margin-inline-end:8px}.charts-css.area.data-spacing-9 tbody tr td::before,.charts-css.area.datasets-spacing-9 tbody tr td::before{-webkit-margin-start:9px;margin-inline-start:9px;-webkit-margin-end:9px;margin-inline-end:9px}.charts-css.area.data-spacing-10 tbody tr td::before,.charts-css.area.datasets-spacing-10 tbody tr td::before{-webkit-margin-start:10px;margin-inline-start:10px;-webkit-margin-end:10px;margin-inline-end:10px}.charts-css.area.data-spacing-11 tbody tr td::before,.charts-css.area.datasets-spacing-11 tbody tr td::before{-webkit-margin-start:11px;margin-inline-start:11px;-webkit-margin-end:11px;margin-inline-end:11px}.charts-css.area.data-spacing-12 tbody tr td::before,.charts-css.area.datasets-spacing-12 tbody tr td::before{-webkit-margin-start:12px;margin-inline-start:12px;-webkit-margin-end:12px;margin-inline-end:12px}.charts-css.area.data-spacing-13 tbody tr td::before,.charts-css.area.datasets-spacing-13 tbody tr td::before{-webkit-margin-start:13px;margin-inline-start:13px;-webkit-margin-end:13px;margin-inline-end:13px}.charts-css.area.data-spacing-14 tbody tr td::before,.charts-css.area.datasets-spacing-14 tbody tr td::before{-webkit-margin-start:14px;margin-inline-start:14px;-webkit-margin-end:14px;margin-inline-end:14px}.charts-css.area.data-spacing-15 tbody tr td::before,.charts-css.area.datasets-spacing-15 tbody tr td::before{-webkit-margin-start:15px;margin-inline-start:15px;-webkit-margin-end:15px;margin-inline-end:15px}.charts-css.area.data-spacing-16 tbody tr td::before,.charts-css.area.datasets-spacing-16 tbody tr td::before{-webkit-margin-start:16px;margin-inline-start:16px;-webkit-margin-end:16px;margin-inline-end:16px}.charts-css.area.data-spacing-17 tbody tr td::before,.charts-css.area.datasets-spacing-17 tbody tr td::before{-webkit-margin-start:17px;margin-inline-start:17px;-webkit-margin-end:17px;margin-inline-end:17px}.charts-css.area.data-spacing-18 tbody tr td::before,.charts-css.area.datasets-spacing-18 tbody tr td::before{-webkit-margin-start:18px;margin-inline-start:18px;-webkit-margin-end:18px;margin-inline-end:18px}.charts-css.area.data-spacing-19 tbody tr td::before,.charts-css.area.datasets-spacing-19 tbody tr td::before{-webkit-margin-start:19px;margin-inline-start:19px;-webkit-margin-end:19px;margin-inline-end:19px}.charts-css.area.data-spacing-20 tbody tr td::before,.charts-css.area.datasets-spacing-20 tbody tr td::before{-webkit-margin-start:20px;margin-inline-start:20px;-webkit-margin-end:20px;margin-inline-end:20px}.charts-css.line{--line-size:3px}.charts-css.line tbody{-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;width:100%;height:calc(100% - var(--heading-size))}.charts-css.line tbody,.charts-css.line tbody tr{display:-webkit-box;display:-ms-flexbox;display:flex}.charts-css.line tbody tr{position:relative;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-ms-flex-negative:1;flex-shrink:1;-ms-flex-preferred-size:0;flex-basis:0;overflow-wrap:anywhere;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.charts-css.line tbody tr th{position:absolute;right:0;left:0}.charts-css.line tbody tr td{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-flow:column;flex-flow:column;width:100%;height:100%;position:absolute;top:0;right:0;bottom:0;left:0;z-index:0}.charts-css.line tbody tr td::before{content:"";position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1}.charts-css.line tbody tr td::after{content:"";width:100%}.charts-css.line:not(.reverse) tbody tr{-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end;-webkit-margin-after:var(--labels-size);margin-block-end:var(--labels-size)}.charts-css.line:not(.reverse) tbody tr th{bottom:calc(var(--labels-size)*-1 - var(--primary-axis-width));height:var(--labels-size)}.charts-css.line.reverse tbody tr,.charts-css.line:not(.reverse) tbody tr td{-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.charts-css.line.reverse tbody tr{-webkit-margin-before:var(--labels-size);margin-block-start:var(--labels-size)}.charts-css.line.reverse tbody tr th{top:calc(var(--labels-size)*-1 - var(--primary-axis-width));height:var(--labels-size)}.charts-css.line.reverse tbody tr td{-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end}.charts-css.line:not(.reverse-data) tbody{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.charts-css.line.reverse-data tbody{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.charts-css.line:not(.reverse-datasets) tbody tr{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.charts-css.line.reverse-datasets tbody tr{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.charts-css.line:not(.reverse):not(.reverse-data) tbody tr td{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end}.charts-css.line:not(.reverse):not(.reverse-data) tbody tr td::before{-webkit-clip-path:polygon(0 calc(100%*(1 - var(--start, var(--size)))),100% calc(100%*(1 - var(--size))),100% calc(100%*(1 - var(--size)) - var(--line-size)),0 calc(100%*(1 - var(--start, var(--size))) - var(--line-size)));clip-path:polygon(0 calc(100%*(1 - var(--start, var(--size)))),100% calc(100%*(1 - var(--size))),100% calc(100%*(1 - var(--size)) - var(--line-size)),0 calc(100%*(1 - var(--start, var(--size))) - var(--line-size)))}.charts-css.line:not(.reverse):not(.reverse-data) tbody tr td .data{-webkit-transform:translateX(50%);transform:translateX(50%)}.charts-css.line:not(.reverse):not(.reverse-data) tbody tr td::after{height:calc(100%*var(--size))}.charts-css.line:not(.reverse).reverse-data tbody tr td{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.charts-css.line:not(.reverse).reverse-data tbody tr td::before{-webkit-clip-path:polygon(0 calc(100%*(1 - var(--size))),100% calc(100%*(1 - var(--start, var(--size)))),100% calc(100%*(1 - var(--start, var(--size))) - var(--line-size)),0 calc(100%*(1 - var(--size)) - var(--line-size)));clip-path:polygon(0 calc(100%*(1 - var(--size))),100% calc(100%*(1 - var(--start, var(--size)))),100% calc(100%*(1 - var(--start, var(--size))) - var(--line-size)),0 calc(100%*(1 - var(--size)) - var(--line-size)))}.charts-css.line:not(.reverse).reverse-data tbody tr td .data{-webkit-transform:translateX(-50%);transform:translateX(-50%)}.charts-css.line:not(.reverse).reverse-data tbody tr td::after{height:calc(100%*var(--size))}.charts-css.line.reverse:not(.reverse-data) tbody tr td{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;-webkit-box-align:end;-ms-flex-align:end;align-items:flex-end}.charts-css.line.reverse:not(.reverse-data) tbody tr td::before{-webkit-clip-path:polygon(0 calc(100%*var(--start, var(--size)) - var(--line-size)),100% calc(100%*var(--size) - var(--line-size)),100% calc(100%*var(--size)),0 calc(100%*var(--start, var(--size))));clip-path:polygon(0 calc(100%*var(--start, var(--size)) - var(--line-size)),100% calc(100%*var(--size) - var(--line-size)),100% calc(100%*var(--size)),0 calc(100%*var(--start, var(--size))))}.charts-css.line.reverse:not(.reverse-data) tbody tr td .data{-webkit-transform:translateX(50%);transform:translateX(50%)}.charts-css.line.reverse:not(.reverse-data) tbody tr td::after{height:calc(100%*(1 - var(--size)))}.charts-css.line.reverse.reverse-data tbody tr td{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.charts-css.line.reverse.reverse-data tbody tr td::before{-webkit-clip-path:polygon(0 calc(100%*var(--size) - var(--line-size)),100% calc(100%*var(--start, var(--size)) - var(--line-size)),100% calc(100%*var(--start, var(--size))),0 calc(100%*var(--size)));clip-path:polygon(0 calc(100%*var(--size) - var(--line-size)),100% calc(100%*var(--start, var(--size)) - var(--line-size)),100% calc(100%*var(--start, var(--size))),0 calc(100%*var(--size)))}.charts-css.line.reverse.reverse-data tbody tr td .data{-webkit-transform:translateX(-50%);transform:translateX(-50%)}.charts-css.line.reverse.reverse-data tbody tr td::after{height:calc(100%*(1 - var(--size)))}.charts-css.line.data-spacing-1 tbody tr td::before,.charts-css.line.datasets-spacing-1 tbody tr td::before{-webkit-margin-start:1px;margin-inline-start:1px;-webkit-margin-end:1px;margin-inline-end:1px}.charts-css.line.data-spacing-2 tbody tr td::before,.charts-css.line.datasets-spacing-2 tbody tr td::before{-webkit-margin-start:2px;margin-inline-start:2px;-webkit-margin-end:2px;margin-inline-end:2px}.charts-css.line.data-spacing-3 tbody tr td::before,.charts-css.line.datasets-spacing-3 tbody tr td::before{-webkit-margin-start:3px;margin-inline-start:3px;-webkit-margin-end:3px;margin-inline-end:3px}.charts-css.line.data-spacing-4 tbody tr td::before,.charts-css.line.datasets-spacing-4 tbody tr td::before{-webkit-margin-start:4px;margin-inline-start:4px;-webkit-margin-end:4px;margin-inline-end:4px}.charts-css.line.data-spacing-5 tbody tr td::before,.charts-css.line.datasets-spacing-5 tbody tr td::before{-webkit-margin-start:5px;margin-inline-start:5px;-webkit-margin-end:5px;margin-inline-end:5px}.charts-css.line.data-spacing-6 tbody tr td::before,.charts-css.line.datasets-spacing-6 tbody tr td::before{-webkit-margin-start:6px;margin-inline-start:6px;-webkit-margin-end:6px;margin-inline-end:6px}.charts-css.line.data-spacing-7 tbody tr td::before,.charts-css.line.datasets-spacing-7 tbody tr td::before{-webkit-margin-start:7px;margin-inline-start:7px;-webkit-margin-end:7px;margin-inline-end:7px}.charts-css.line.data-spacing-8 tbody tr td::before,.charts-css.line.datasets-spacing-8 tbody tr td::before{-webkit-margin-start:8px;margin-inline-start:8px;-webkit-margin-end:8px;margin-inline-end:8px}.charts-css.line.data-spacing-9 tbody tr td::before,.charts-css.line.datasets-spacing-9 tbody tr td::before{-webkit-margin-start:9px;margin-inline-start:9px;-webkit-margin-end:9px;margin-inline-end:9px}.charts-css.line.data-spacing-10 tbody tr td::before,.charts-css.line.datasets-spacing-10 tbody tr td::before{-webkit-margin-start:10px;margin-inline-start:10px;-webkit-margin-end:10px;margin-inline-end:10px}.charts-css.line.data-spacing-11 tbody tr td::before,.charts-css.line.datasets-spacing-11 tbody tr td::before{-webkit-margin-start:11px;margin-inline-start:11px;-webkit-margin-end:11px;margin-inline-end:11px}.charts-css.line.data-spacing-12 tbody tr td::before,.charts-css.line.datasets-spacing-12 tbody tr td::before{-webkit-margin-start:12px;margin-inline-start:12px;-webkit-margin-end:12px;margin-inline-end:12px}.charts-css.line.data-spacing-13 tbody tr td::before,.charts-css.line.datasets-spacing-13 tbody tr td::before{-webkit-margin-start:13px;margin-inline-start:13px;-webkit-margin-end:13px;margin-inline-end:13px}.charts-css.line.data-spacing-14 tbody tr td::before,.charts-css.line.datasets-spacing-14 tbody tr td::before{-webkit-margin-start:14px;margin-inline-start:14px;-webkit-margin-end:14px;margin-inline-end:14px}.charts-css.line.data-spacing-15 tbody tr td::before,.charts-css.line.datasets-spacing-15 tbody tr td::before{-webkit-margin-start:15px;margin-inline-start:15px;-webkit-margin-end:15px;margin-inline-end:15px}.charts-css.line.data-spacing-16 tbody tr td::before,.charts-css.line.datasets-spacing-16 tbody tr td::before{-webkit-margin-start:16px;margin-inline-start:16px;-webkit-margin-end:16px;margin-inline-end:16px}.charts-css.line.data-spacing-17 tbody tr td::before,.charts-css.line.datasets-spacing-17 tbody tr td::before{-webkit-margin-start:17px;margin-inline-start:17px;-webkit-margin-end:17px;margin-inline-end:17px}.charts-css.line.data-spacing-18 tbody tr td::before,.charts-css.line.datasets-spacing-18 tbody tr td::before{-webkit-margin-start:18px;margin-inline-start:18px;-webkit-margin-end:18px;margin-inline-end:18px}.charts-css.line.data-spacing-19 tbody tr td::before,.charts-css.line.datasets-spacing-19 tbody tr td::before{-webkit-margin-start:19px;margin-inline-start:19px;-webkit-margin-end:19px;margin-inline-end:19px}.charts-css.line.data-spacing-20 tbody tr td::before,.charts-css.line.datasets-spacing-20 tbody tr td::before{-webkit-margin-start:20px;margin-inline-start:20px;-webkit-margin-end:20px;margin-inline-end:20px}.charts-css.radial tbody{display:block;width:100%;height:0;-webkit-padding-after:100%;padding-block-end:100%;border-radius:50%;background-color:var(--chart-bg-color)}.charts-css.radial tbody tr{display:none}.charts-css.pie tbody{display:block;width:100%;height:0;-webkit-padding-after:100%;padding-block-end:100%;border-radius:50%;background-color:var(--chart-bg-color)}.charts-css.pie tbody tr{display:none}.charts-css.donut tbody{display:block;width:100%;height:0;-webkit-padding-after:100%;padding-block-end:100%;border-radius:50%;background-color:var(--chart-bg-color)}.charts-css.donut tbody tr{display:none}.charts-css.donut tbody::after{content:"";position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);width:var(--donut-inner-size,50%);height:var(--donut-inner-size,50%);display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;border-radius:50%;background-color:var(--donut-inner-color,#fff)}.charts-css.polar tbody{display:block;width:100%;height:0;-webkit-padding-after:100%;padding-block-end:100%;border-radius:50%;background-color:var(--chart-bg-color)}.charts-css.polar tbody tr{display:none}.charts-css.radar tbody{display:block;width:100%;height:0;-webkit-padding-after:100%;padding-block-end:100%;border-radius:50%;background-color:var(--chart-bg-color)}.charts-css.radar tbody tr{display:none} \ No newline at end of file diff --git a/www/test/index.html b/www/test/index.html deleted file mode 100644 index 74f510041..000000000 --- a/www/test/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - CryptPad - - - - - - - diff --git a/www/test/inner.js b/www/test/inner.js deleted file mode 100644 index 4b02d51a1..000000000 --- a/www/test/inner.js +++ /dev/null @@ -1,68 +0,0 @@ -define([ - '/common/hyperscript.js', - '/common/inner/charts.js', -], function (h, Charts) { - var wrap = function (content) { - return h('div', { - style: 'height: 500px; width: 500px; padding: 15px; border: 1px solid #222; margin: 15px;' - }, content); - }; - - var append = function (el) { - document.body.appendChild(el); - }; - - var data = [ - 25, 58, 5, 96, 79, - 23, 75, 13, 44, 29, - 65, 80, 30, 47, 22, - 7, 62, 64, 46, 21, - 29, 31, 76, 65, 61, - 78, 58, 12, 90, 98, - 37, 75, 92, 74, 16, - 17, 52, 42, 71, 19 - ]; - - - append(h('h1', 'Charts')); - append(h('hr')); - - var cell = (function () { - var i = 0; - - return function () { - var val = data[i++]; - return h('td', { - style: '--size: ' + (val / 100), - }, val); - }; - }()); - - var multirow = function (n) { - var cells = []; - while (n--) { - cells.push(cell()); - } - return h('tr', { - style: 'margin: 15px', - }, cells); - }; - - append(wrap(Charts.table([ - h('tbody', [ - multirow(4), - multirow(4), - multirow(4), - multirow(4), - ]), - ], [ - 'charts-css', - 'bar', - 'multiple', - ]))); - - - append(h('hr')); - append(wrap(Charts.columns([ 40, 60, 75, 90, 100]))); - append(wrap(Charts.columns(data.slice(20)))); -}); From 3976bfae841fb78d3f538b3ad700bf0e29de5db6 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 16 Aug 2021 18:24:57 +0530 Subject: [PATCH 005/223] remove unnecessary notes --- www/form/inner.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 8bb47432c..33e2af754 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1967,7 +1967,7 @@ define([ var switchMode = h('button.btn.btn-secondary', Messages.form_showIndividual); $controls.hide().append(switchMode); - var show = function (answers, header) { // XXX + var show = function (answers, header) { var elements = content.order.map(function (uid) { var block = form[uid]; var type = block.type; @@ -1975,7 +1975,7 @@ define([ if (!model || !model.printResults) { return; } // Only use content if we're not viewing individual answers - var print = model.printResults(answers, uid, form, !header && content); // XXX + var print = model.printResults(answers, uid, form, !header && content); var q = h('div.cp-form-block-question', block.q || Messages.form_default); @@ -1992,7 +1992,7 @@ define([ $results.empty().append(elements); if (header) { $results.prepend(header); } }; - show(answers); // XXX + show(answers); if (APP.isEditor || APP.isAuditor) { $controls.show(); } @@ -2050,7 +2050,7 @@ define([ } 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]); // XXX + var div = h('div.cp-form-individual', [span, viewButton, warning, badge]); $(viewButton).click(function () { var res = {}; res[curve] = obj; @@ -2059,7 +2059,7 @@ define([ summary = true; $s.click(); }); - var header = h('div.cp-form-individual', [ // XXX + var header = h('div.cp-form-individual', [ span.cloneNode(true), back ]); From c1f9d113336f21860d3d88997f8d491ad6fadc60 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 16 Aug 2021 18:32:34 +0530 Subject: [PATCH 006/223] delay working on some flagged issues till next release --- lib/env.js | 2 +- www/common/common-ui-elements.js | 4 ++-- www/common/drive-ui.js | 2 +- www/common/outer/cache-store.js | 2 +- www/convert/inner.js | 4 ++-- www/form/inner.js | 12 ++++++------ www/form/main.js | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/env.js b/lib/env.js index 4db282d77..5ad0bbdac 100644 --- a/lib/env.js +++ b/lib/env.js @@ -123,7 +123,7 @@ module.exports.create = function (config) { maxWorkers: config.maxWorkers, disableIntegratedTasks: config.disableIntegratedTasks || false, - disableIntegratedEviction: typeof(config.disableIntegratedEviction) === 'undefined'? true: config.disableIntegratedEviction, // XXX 4.10.0 false, + disableIntegratedEviction: typeof(config.disableIntegratedEviction) === 'undefined'? true: config.disableIntegratedEviction, // XXX 4.11.0 false, lastEviction: +new Date(), evictionReport: {}, commandTimers: {}, diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 81327844f..8315768f1 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -3041,12 +3041,12 @@ define([ // fc_open ("Open") // share_linkOpen ("Preview") // resources_openInNewTab ("Open it in a new tab") - Messages.link_open = Messages.fc_open; // XXX 4.10.0 + Messages.link_open = Messages.fc_open; // XXX 4.11.0 //Messages.link_store = "Store link in drive"; // toolbar_storeInDrive ? ("Store in CryptDrive") // autostore_store ? ("Store") - Messages.link_store = Messages.toolbar_storeInDrive; // XXX 4.10.0 + Messages.link_store = Messages.toolbar_storeInDrive; // XXX 4.11.0 var content = h('div', [ diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index cb70986a4..fe87e23da 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -2783,7 +2783,7 @@ define([ var u = $url.val().trim(); if (!n || !u) { return true; } if (!Util.isValidURL(u)) { - // XXX 4.10.0 add style for invalid input? input:invalid + // XXX 4.11.0 add style for invalid input? input:invalid UI.warn(Messages.fm_link_invalid); return true; } diff --git a/www/common/outer/cache-store.js b/www/common/outer/cache-store.js index 93bc0d0c6..d1c3795c0 100644 --- a/www/common/outer/cache-store.js +++ b/www/common/outer/cache-store.js @@ -97,7 +97,7 @@ define([ var checkCheckpoints = function (array) { if (!Array.isArray(array)) { return; } // Keep the last 100 messages - if (array.length > 100) { // XXX 4.10.0 + if (array.length > 100) { // XXX 4.11.0 array.splice(0, array.length - 100); } // Remove every message before the first checkpoint diff --git a/www/convert/inner.js b/www/convert/inner.js index ba9870ccf..3c885ce57 100644 --- a/www/convert/inner.js +++ b/www/convert/inner.js @@ -192,8 +192,8 @@ define([ }, }; - Messages.convertPage = "Convert"; // XXX 4.10.0 - Messages.convert_hint = "Pick the file you want to convert. The list of output format will be visible afterward."; // XXX 4.10.0 + Messages.convertPage = "Convert"; // XXX 4.11.0 + Messages.convert_hint = "Pick the file you want to convert. The list of output format will be visible afterward."; // XXX 4.11.0 var createToolbar = function () { var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications']; diff --git a/www/form/inner.js b/www/form/inner.js index 33e2af754..089db8b57 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -2739,11 +2739,11 @@ define([ var responseMsg = h('div.cp-form-response-msg-container'); var $responseMsg = $(responseMsg); var refreshResponse = function () { - if (true) { return; } // XXX 4.10.0 + if (true) { return; } // XXX 4.11.0 $responseMsg.empty(); - Messages.form_updateMsg = "Update response message"; // XXX 4.10.0 - Messages.form_addMsg = "Add response message"; // XXX 4.10.0 - Messages.form_responseMsg = "Add a message that will be displayed in the response page."; // XXX 4.10.0 + Messages.form_updateMsg = "Update response message"; // XXX 4.11.0 + Messages.form_addMsg = "Add response message"; // XXX 4.11.0 + Messages.form_responseMsg = "Add a message that will be displayed in the response page."; // XXX 4.11.0 var text = content.answers.msg ? Messages.form_updateMsg : Messages.form_addMsg; var btn = h('button.btn.btn-secondary', text); $(btn).click(function () { @@ -2772,7 +2772,7 @@ define([ name: Messages.settings_save, onClick: function () { var v = editor.getValue(); - content.answers.msg = v.trim(0, 2000); // XXX 4.10.0 max length? + content.answers.msg = v.trim(0, 2000); // XXX 4.11.0 max length? framework.localChange(); framework._.cpNfInner.chainpad.onSettle(function () { UI.log(Messages.saved); @@ -2798,7 +2798,7 @@ define([ } UI.openCustomModal(APP.responseModal); }); - // $responseMsg.append(btn); // XXX 4.10.0 + // $responseMsg.append(btn); // XXX 4.11.0 }; //refreshResponse(); diff --git a/www/form/main.js b/www/form/main.js index de2c311ef..b82397caf 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -176,7 +176,7 @@ define([ validateKey: keys.secondaryValidateKey, owners: [myKeys.edPublic], crypto: crypto, - //Cache: Utils.Cache // XXX 4.10.0 + //Cache: Utils.Cache // XXX 4.11.0 }; var results = {}; config.onError = function (info) { From 89b52b61107b7e8d20048c1c05bcf33bbdfa3f87 Mon Sep 17 00:00:00 2001 From: Weblate Date: Mon, 16 Aug 2021 15:05:22 +0200 Subject: [PATCH 007/223] Translated using Weblate (French) Currently translated at 100.0% (1382 of 1382 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fr/ --- www/common/translations/messages.fr.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/www/common/translations/messages.fr.json b/www/common/translations/messages.fr.json index e814ad87b..1c1eafd38 100644 --- a/www/common/translations/messages.fr.json +++ b/www/common/translations/messages.fr.json @@ -1230,7 +1230,7 @@ "genericCopySuccess": "Copié dans le presse-papiers", "mediatag_defaultImageName": "image", "register_registrationIsClosed": "Les inscriptions sont fermées.", - "oo_conversionSupport": "Votre navigateur ne gère pas la conversion vers et depuis les formats Microsoft Office. Il est recommandé d'utiliser une version récente de Firefox ou Chrome.", + "oo_conversionSupport": "Votre navigateur ne gère pas la conversion vers et depuis les formats office. Il est recommandé d'utiliser une version récente de Firefox ou Chrome.", "oo_importBin": "Cliquez sur OK pour importer au format .bin interne à CryptPad.", "admin_emailButton": "Valider", "admin_supportPrivButton": "Afficher la clé", @@ -1381,5 +1381,6 @@ "fm_link_new": "Nouveau Lien", "notification_openLink": "Vous avez reçu un lien {0} de {1} :", "ui_expand": "Développer", - "ui_collapse": "Réduire" + "ui_collapse": "Réduire", + "form_totalResponses": "Nombre de réponses : {0}" } From 47067b020bb8665ae2214fda42599a52dc2580a7 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 16 Aug 2021 18:38:33 +0530 Subject: [PATCH 008/223] defer some more minor issues till 4.11.0 --- www/common/onlyoffice/inner.js | 2 +- www/common/sframe-common.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index 2afef4978..591eccc2c 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -1939,7 +1939,7 @@ define([ var text = getContent(); var suggestion = Title.suggestTitle(Title.defaultTitle); var ext = ['.xlsx', '.ods', '.bin', - //'.csv', // XXX + //'.csv', // XXX 4.11.0 '.pdf']; var type = common.getMetadataMgr().getPrivateData().ooType; var warning = ''; diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 6a7e3effd..c05bdfe3f 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -921,7 +921,7 @@ define([ }); ctx.sframeChan.on('EV_WORKER_TIMEOUT', function () { - UI.errorLoadingScreen(Messages.timeoutError, false, function () { // XXX mobile users can't necessarily hit 'ESC' as this message suggests. provice a click option + UI.errorLoadingScreen(Messages.timeoutError, false, function () { // XXX 4.11.0 mobile users can't necessarily hit 'ESC' as this message suggests. provice a click option funcs.gotoURL(''); }); }); From 09bdcb20134b03caccf460df8888712a447dfd25 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 17 Aug 2021 10:51:20 +0530 Subject: [PATCH 009/223] update changelog for 4.10.0 --- CHANGELOG.md | 83 ++++++++++++++++++++++++++++------------------------ 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56fae6b54..60f86b192 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,53 +2,58 @@ ## Goals +August is typically a quiet month for CryptPad's development team, as members of our team and many of our users take their (northern hemisphere) summer holidays. We took the opportunity to catch up on some regular maintentance and to review and some prototype branches of our code that had been ready for integration for some time. + +It seems that some browser developers thought to do the same thing, because we noticed some significant regressions in some APIs that we rely on. Some of our time went towards addressing the resulting bugs and restructuring some code to avoid future regressions for browser behaviour that seem likely to be changed again in the near future. ## Update notes +4.10.0 includes some minor changes to [the checkup page](https://docs.cryptpad.fr/fr/admin_guide/installation.html#diagnostics). Some admins have included screenshots of this page in bug reports or requests for support along with details of problems they suspect of being related. Because we've observed that the root of many issues is the browser (sometimes in addition to the server) we have decided to include details about the browser in this page's summary. + +Up until now the checkup page only tested observable behaviour of the server such as HTTP headers on particular resources, configuration parameters distributed to the client, and the availability of essential resources. This practice meant that a report for an instance should have been the same regardless of the device that was used to generate the report. In light of a serious regression in Chrome (and all its derivatives) we decided that objectiveness was less important than utility and introduced some tests which check whether the client running the diagnostics interprets the provided server configuration. Terrible browsers (ie. every browser that is available on iOS) will fail these tests every time because they don't implement the expected APIs, but we've tried to detect these cases and warn that they are expected. + +For the most part you (as an admin) will not need to do anything special for this release as a result. If you notice weird issues on particular browsers in the future, however, it might be helpful to view this page from the affected browser/device and include any information that is provided in bug reports. + +To update from 4.9.0 to 4.10.0: + +1. Stop your server +2. Get the latest code with git +3. Install the latest dependencies with `bower update` and `npm i` +4. Restart your server +5. Confirm that your instance is passing all the tests included on the `/checkup/` page (on whatever devices you intend to support) + ## Features -* screen real-estate - * kanban - * narrower 'add board' button - * 'Tools' menu to collapse the tag and view mode UI - * general - * main toolbar collapse -* remove unused files - * /common/noscriptfix.js -* more detailed inventory of dependencies - * see cryptpad/www/lib/changelog.md -* include vendor and appVersion in support ticket data -* log when trimming history -* rewrite some translation keys to use a single syntax for BR tags -* translations - * more linting - * standardized capitalization of "CryptPad" - * avoid raw injection of HTML strictly for adding line breaks - * remove some unnecessary cases of raw HTML injection -* checkup - * better styles - * improved formatting for returned values in failed tests - * display browser and OS for when people send us screenshots instead of URLs - * test for support of some features in the browser (inside the sandbox) -* mark password inputs as _new passwords_ so that browsers don't suggest you input and share your account password +As noted above, web standards and the browsers that implement them are constantly changing. Web applications like CryptPad which use new and advanced browser features are particularly prone to regressions even when we use browser features exactly as intended and advertized. The "Features" section of each release's notes typically highlights visible things, like clickable buttons or improvements to the interface. This point is included as a reminder that _regular maintenance is at least as important to an open-source software project_, even though it gets little attention and far less funding. The funding bodies that have generously supported our work typically award grants for research and the development of novel features, but we are sorely in need of increased support to allow us the flexibility to deal with unanticipated problems as they arise. If you are fortunate enough to have some disposable income and value the work that keeps CryptPad functional we would greatly appreciate a one-time or recurring donation to [our OpenCollative campaign](https://opencollective.com/cryptpad/contribute). + +* This release coincided the yearly seminar of [XWiki (our parent organization)](https://www.xwiki.com) which always features a day-long hackathon. This year our team was joined by [@aemi-dev](https://github.com/aemi-dev) who has been working as an intern within XWiki's product team. Together we worked on adding some data visualization to our recently introduced _Form_ app. The improvements include a timeline to visualize how many responses were submitted to the form during each day and bar charts for a variety of question types to complement the existing tally of results. There's still more work to be done in this direction, but we established some useful foundations during our relatively short session. +* Frequent users of small screens will be pleased to hear that CryptPad's app toolbar now includes a button to collapse the upper segment of the toolbar which includes CryptPad's logo, the current document's title, status indicator (saved, editing, disconnected, etc.), and the user administration menu. +* Likewise, Kanban users may note that the app's toolbar also features a "Tools" menu (like that in the markdown editor) which toggles display of the controls which filter board items by tag and select view state (detailed or brief). +* Password fields that are specific to files and documents now have the `autocomplete="new-password"` attribute applied to prevent browsers and integrated password managers from suggesting that users enter their account password. This lowers the risk that users will inadvertently reveal their account password in the future. Additionally, Firefox will now prompt users to use a high-entropy password instead. +* Our integrated support ticket functionality automatically includes some commonly needed information about the user's account and browser. As of this release this data will also include the browser's `vendor` and `appVersion`, which are useful hints about the host browser and OS (which we almost always have to ask about when the ticket is for a bug report). This data will also include the browser's current width and height, as some issues only occur at particular resolutions and can otherwise be difficult to reproduce. +* We reviewed a range of third-party dependencies that are included in our repository and updated `cryptpad/www/lib/changelog.md` to better indicate their exact version, source, and any CryptPad-specific modifications we've made to them. + * We found `less.js` had been duplicated, with one version (provided by bower) being used for custom styles in our slide editor while the rest of the platform used a custom version that fixed an apparent bug in the _reference import_ syntax. We've standardized on our custom version and removed the alternative from our `bower.json` file. + * We also identified a few files that were no longer in use and removed them. There's still more work to be done to document the exact versions and source of some dependencies, so we've made this process a part of our regular release checklist. +* During a manual review we noticed some inconsistencies between different translations of CryptPad and have automated these checks by adding them to a script which we use to review translations before each release. These have helped us standardize things like the capitalization of "CryptPad", the syntax for some basic markup like `
` tags, and the consistent use of both dialect-specific suffixes in English and punctuation rules in French. We have only added tests for languages in which members of our team are fluent, so if you maintain a translation in another language and can suggest additional qualities we could test we would welcome your suggestions. +* The improved consistency of our translations has also enabled us to construct some translated UI components programmatically without directly using their inline HTML. This provides an extra layer of security in the event that + 1. malicious code was included in a translation file + 2. our tests failed to identify the code before it was included in a release + 3. the release was deployed by an admin that had failed to take advantage of the sandboxing system that prevents the injection of scripts into the UI ## Bug fixes -* Sheet export - * most exports broken by Chrome 92, mostly fixed - * we discovered that CSV export was not working in any major browser, though it's unclear why. We've disabled CSV export in the meantime - * updated translation to stop referring to Microsoft since we support OpenDocument formats - * some new browser-specific checkup tests to make it easier to detect future regressions in the APIs -* drive bug fixes - * guard against a few possible type errors - * "burn this drive" button works again in Firefox -* clear login token - 1. when you delete your account - 2. when logging in -* use single version of less.js on the client -* abort subsequent actions when metadata fails to load during owned channel archival -* handle warnings when trimming history (not just errors) -* filter channel ids with invalid lengths when generating a list of all channels you use +* The Chrome development team made some changes related to the availability of the `SharedArrayBuffer` API in cross-site-isolated contexts such as that of our sandboxing system which resulted in it being disabled despite the fact that our usage conformed to a specification that should have been supported. We use this modern browser feature (where available) to convert spreadsheets between different formats in the browser itself, whereas other services (even those advertizing their use of encryption for documents) send users' content to their server for conversion. Since Chrome's engine is used as the basis for a wide variety of other browsers, this broke sheet export everywhere except Firefox (which correctly implements the specification). Luckily, we found a simple workaround to use the same underlying feature using an alternate syntax that they had failed to disable. This is only a short-term solution as we have no expectation that it will continue to work, so we are actively investigating making this conversion a trusted process that will be run outside of our sandboxing system. +* On the topic of spreadsheet conversion, we updated our translations of the warning that is displayed in our conversion UI when the required browser features are not available. Rather than referring to "Microsoft Office formats" we now refer to _"Office formats"_ since we offer support for ODS in addition to XLSX. +* We found that CSV export mysteriously stopped working as well (seemingly everywhere, not just Chrome and derivatives). We're still not sure why this is the case, but the option is disabled in the UI until we can find and fix the problem. +* The _drive_ app includes a button that lets guest users wipe their personal data from their browser's session. We noticed that this button did nothing after approximately 50% of page loads in Firefox, suggesting there was an unpredictable quality related to either how the button was being created or how "click handlers" were declared. We traced it back to the jQuery library and rewrote the handler to use "VanillaJS". We don't have the time or budget to dig into why it stopped working, so unless someone else can figure it out for us then you, dear reader, may never learn the answer to this mystery. +* While investigating the drive we also added some guards against some possible type errors. +* We noticed that the `loginToken` attribute was not correctly removed from clients' localStorage when they deleted their account. The value of this token is random and is of no use to attackers (especially when the token belongs to a deleted account), but it was a cause of some inconvenience to us when testing account deletion, as the mismatch between the token stored locally and in accounts (after login) required us to login in a second time before. We've updated the related code to: + 1. correctly delete the token when you delete an account from the settings page + 2. ensure that no such token is present when logging in +* Document ids with invalid lengths are excluded from accounts' lists of "pinned documents" (those which should not be deleted from the server). We recently implemented a similar fix, but found that this list could be constructed in more than one way depending on the context. +* We identified and fixed two problems with our "history trim" functionality (accessible via documents' "Properties" menu): + 1. In the extremely unlikely event that a user requested that the server trim the history of a document and its metadata failed to load, the server would respond to the user with an error but did not correctly abort from the subsequent process to trim the document's history. In theory this could have been used by non-owners to archive parts of the documents history, however, we have no reason to believe that this was possible in practice. In any case, the flaw has been corrected. + 2. Complex documents like spreadsheets that use more than one channel to store different types of content would trim their respective histories in parallel, however, in such cases any errors were returned to the calling function as a list of warnings rather than a singular error. This format was not handled by the UI, resulting in an apparent success in cases of a partial or complete failure for such document types. # 4.9.0 From 554ce6534674bcd37ced88bbe9722a952ae2fa00 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 17 Aug 2021 12:15:07 +0530 Subject: [PATCH 010/223] check for HTML tags across multiple lines --- scripts/lint-translations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/lint-translations.js b/scripts/lint-translations.js index bc18dd724..5e7f81b17 100644 --- a/scripts/lint-translations.js +++ b/scripts/lint-translations.js @@ -68,7 +68,7 @@ var processLang = function (map, lang, primary) { if (typeof(s) !== 'string') { return; } var usesHTML; - s.replace(/<.*?>/g, function (html) { + s.replace(/<[\s\S]*?>/g, function (html) { if (simpleTags.indexOf(html) !== -1) { return; } announce(); usesHTML = true; From 8887d8fc1e74cbfa9d3f05e87bba0832f99fd143 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 17 Aug 2021 12:15:29 +0530 Subject: [PATCH 011/223] remove dead code --- www/form/app-form.less | 41 ----------------------------------------- 1 file changed, 41 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index 1621e735d..06b1c331e 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -74,47 +74,6 @@ height: 200px; } -/* - table.cp-chart-table { - - --color: @colortheme_apps[pad]; - margin-top: 2em; - margin-right: auto; - min-width: 400px; - padding-bottom: 2rem; - width: min-content; - - tr { - min-height: 200px; - min-width: max-content; - position: relative; - } - - td { - margin: 1px; - min-width: 20px; - max-width: 30px; - border: 1px solid @cryptpad_color_brand_fadest; - } - - th { - position: absolute; - bottom: -1.5rem; - left: 50%; - transform: translateX(-50%); - } - - tr:not(:first-of-type):not(:last-of-type):not(:nth-of-type(5n+1)) { - th { - visibility: hidden; - } - } - .cp-bar:not(:hover) { - color: transparent; - } - } -*/ - .cp-form-input-block { display: flex; } From 58eb65e35da92ad09528a8cb8e3f60a9f1cb59a5 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 17 Aug 2021 12:15:55 +0530 Subject: [PATCH 012/223] report screen width and height in support tickets --- www/support/ui.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/www/support/ui.js b/www/support/ui.js index 31555dc64..fac6ef438 100644 --- a/www/support/ui.js +++ b/www/support/ui.js @@ -42,6 +42,8 @@ define([ data.sender.userAgent = Util.find(window, ['navigator', 'userAgent']); data.sender.vendor = Util.find(window, ['navigator', 'vendor']); data.sender.appVersion = Util.find(window, ['navigator', 'appVersion']); + data.sender.appVersion = Util.find(window, ['screen', 'width']); + data.sender.appVersion = Util.find(window, ['screen', 'height']); data.sender.blockLocation = privateData.blockLocation || ''; data.sender.teams = Object.keys(teams).map(function (key) { var team = teams[key]; From 589a09e721c743d6dd2cccbcf7b3791dc0945acc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Aug 2021 09:42:43 +0000 Subject: [PATCH 013/223] Bump jszip from 3.6.0 to 3.7.1 Bumps [jszip](https://github.com/Stuk/jszip) from 3.6.0 to 3.7.1. - [Release notes](https://github.com/Stuk/jszip/releases) - [Changelog](https://github.com/Stuk/jszip/blob/master/CHANGES.md) - [Commits](https://github.com/Stuk/jszip/compare/v3.6.0...v3.7.1) --- updated-dependencies: - dependency-name: jszip dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a3b92de86..790052c6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1591,9 +1591,9 @@ } }, "jszip": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.6.0.tgz", - "integrity": "sha512-jgnQoG9LKnWO3mnVNBnfhkh0QknICd1FGSrXcgrl67zioyJ4wgx25o9ZqwNtrROSflGBCGYnJfjrIyRIby1OoQ==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz", + "integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==", "dev": true, "requires": { "lie": "~3.3.0", From 6a4ef02c413767330333298f3c5e434c7772a39a Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 18 Aug 2021 08:13:06 +0530 Subject: [PATCH 014/223] remove some dead toolbar code --- www/common/toolbar.js | 24 ------------------------ www/file/inner.js | 1 - 2 files changed, 25 deletions(-) diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 47c0b578e..8bd0fcc32 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -53,7 +53,6 @@ MessengerUI, Messages, Pages) { var USERADMIN_CLS = Bar.constants.user = 'cp-toolbar-user-dropdown'; var USERNAME_CLS = Bar.constants.username = 'cp-toolbar-user-name'; /*var READONLY_CLS = */Bar.constants.readonly = 'cp-toolbar-readonly'; - var USERBUTTON_CLS = Bar.constants.changeUsername = "cp-toolbar-user-rename"; // Create the toolbar element @@ -1029,36 +1028,13 @@ MessengerUI, Messages, Pages) { var userMenuCfg = { $initBlock: $userAdmin, }; - if (!config.hideDisplayName) { - $.extend(true, userMenuCfg, { - displayNameCls: USERNAME_CLS, - changeNameButtonCls: USERBUTTON_CLS, - }); - } if (config.readOnly !== 1) { userMenuCfg.displayName = 1; userMenuCfg.displayChangeName = 1; } - /*if (config.displayed.indexOf('userlist') !== -1) { - userMenuCfg.displayChangeName = 0; - }*/ Common.createUserAdminMenu(userMenuCfg); $userAdmin.find('> button').attr('title', Messages.userAccountButton); - var $userButton = toolbar.$userNameButton = $userAdmin.find('a.' + USERBUTTON_CLS); - $userButton.click(function (e) { - e.preventDefault(); - e.stopPropagation(); - var myData = metadataMgr.getUserData(); - var lastName = myData.name; - UI.prompt(Messages.changeNamePrompt, lastName || '', function (newName) { - if (newName === null && typeof(lastName) === "string") { return; } - if (newName === null) { newName = ''; } - else { Feedback.send('NAME_CHANGED'); } - setDisplayName(newName); - }); - }); - return $userAdmin; }; diff --git a/www/file/inner.js b/www/file/inner.js index d69420f7b..2be2759db 100644 --- a/www/file/inner.js +++ b/www/file/inner.js @@ -62,7 +62,6 @@ define([ } var configTb = { displayed: displayed, - //hideDisplayName: true, $container: $bar, metadataMgr: metadataMgr, sfCommon: common, From 74ec0454b8780715a8aa2ba5d5397455ba51a950 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 18 Aug 2021 08:16:25 +0530 Subject: [PATCH 015/223] undo changes to a string that is set to be removed --- www/common/translations/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/translations/messages.json b/www/common/translations/messages.json index e967f3e7c..92de2a243 100644 --- a/www/common/translations/messages.json +++ b/www/common/translations/messages.json @@ -79,7 +79,7 @@ "exportButton": "Export", "exportButtonTitle": "Export this pad to a local file", "exportPrompt": "What would you like to name your file?", - "changeNamePrompt": "Change your name (leave empty to be known as 'Guest'): ", + "changeNamePrompt": "Change your name (leave empty to be anonymous): ", "user_rename": "Change display name", "user_displayName": "Display name", "user_accountName": "Account name", From 00eda9dcac70e601fb3003197111e5ef31e81849 Mon Sep 17 00:00:00 2001 From: Weblate Date: Tue, 17 Aug 2021 14:21:58 +0200 Subject: [PATCH 016/223] Translated using Weblate (Russian) Currently translated at 49.0% (678 of 1382 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/ru/ --- www/common/translations/messages.ru.json | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/www/common/translations/messages.ru.json b/www/common/translations/messages.ru.json index 56245dd92..975dd48f7 100644 --- a/www/common/translations/messages.ru.json +++ b/www/common/translations/messages.ru.json @@ -13,7 +13,8 @@ "todo": "Список дел", "contacts": "Адресная книга", "sheet": "Таблица", - "teams": "Команды" + "teams": "Команды", + "form": "Формы" }, "button_newpad": "Новая документ с форматированием", "button_newcode": "Новый документ с кодом", @@ -31,13 +32,13 @@ "inactiveError": "Этот документ был удалён из-за длительной неактивности. Нажмите Esc чтобы создать новый.", "chainpadError": "Во время обновления вашей информации произошла критическая ошибка. Документ пока доступен только для чтения чтобы вы не потеряли свою информацию.
Нажмите Esc чтобы продолжать чтение или перезагрузите страницу и начните заново.", "invalidHashError": "Запрошенный вами документ имеет неправильный адрес URL.", - "errorCopy": " Вы можете только скопировать содержимое в другое место, нажав Esc.
Если вы закроете страницу, всё содержимое будет утеряно!", + "errorCopy": "Вы все еще можете получить доступ к содержимому, нажав Esc.
Если вы закроете это окно, содержимое будет утеряно.", "errorRedirectToHome": "Нажмите Esc чтобы перейти к вашему хранилищу.", "newVersionError": "Доступна новая версия CryptPad.
Перезагрузите чтобы использовать новую версию или нажмите Esc, чтобы использовать вашу информацию в оффлайн-режиме.", "loading": "Загрузка...", "error": "Ошибка", "saved": "Сохранено", - "deleted": "Документ удалён из вашего CryptDrive", + "deleted": "Удалено", "deletedFromServer": "Документ удалён с сервера", "mustLogin": "Вам нужно войти, чтобы получить доступ к этой странице", "disabledApp": "Приложение было отключено. Свяжитесь с администратором этого CryptPad.", @@ -91,7 +92,7 @@ "userAccountButton": "Ваш профиль", "newButton": "Создать", "uploadButton": "Загрузить файлы", - "uploadButtonTitle": "Загрузить новый файл в эту папку", + "uploadButtonTitle": "Загрузить новый файл в ваш CryptDrive", "saveTemplateButton": "Сохранить как образец", "saveTemplatePrompt": "Выбрать название для образца", "templateSaved": "Образец сохранен!", @@ -120,7 +121,7 @@ "filePicker_description": "Выберите файл из вашего CryptDrive чтобы вставить его или выберите новый", "filePicker_filter": "Филтровать файлы по именам", "tags_title": "Теги (только для вас)", - "tags_add": "Обновить теги страницы", + "tags_add": "Обновить теги выбранных документов", "tags_notShared": "Ваши теги не разделяются с другими пользователями", "button_newsheet": "Новый Лист", "newButtonTitle": "создать новую запись", From 8d5221e6e8a047555b307b846a8f419a8f4b7a8b Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 18 Aug 2021 09:56:48 +0530 Subject: [PATCH 017/223] include bar graphs for multi* form answers --- www/form/app-form.less | 29 ++++++++++++++++++---- www/form/inner.js | 55 +++++++----------------------------------- 2 files changed, 33 insertions(+), 51 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index 06b1c331e..781b8161d 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -532,8 +532,30 @@ background: @cp_form-bg2; &:not(:last-child) { margin-bottom: 1px; } } - .cp-form-results-type-radio { + + .cp-form-results-cell() { + border: 1px solid @cp_form-border; + display: table-cell; + padding: 5px 10px; + background: @cp_form-bg2; + } + + .cp-form-results-type-multiradio-data { + .cp-mr-q { + font-weight: bold; + padding: 5px 10px; + .cp-form-results-cell(); + } + &:not(:first-child) { + .cp-mr-q { + margin-top: 15px; + } + } + } + + .cp-form-results-type-radio { display: table; + width: 100%; .cp-form-results-type-multiradio-data { display: flex; flex-flow: column; @@ -542,10 +564,7 @@ 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-form-results-cell(); &.cp-value { min-width: 200px; } diff --git a/www/form/inner.js b/www/form/inner.js index 089db8b57..ad89ada26 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1255,7 +1255,7 @@ define([ }; }, - printResults: function (answers, uid, form) { + printResults: function (answers, uid, form, content) { // results multiradio var structure = form[uid]; if (!structure) { return; } @@ -1263,6 +1263,7 @@ define([ var results = []; var empty = 0; var count = {}; + var showBars = Boolean(content); Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; @@ -1293,34 +1294,13 @@ define([ return h('div.cp-form-results-type-radio-data', [ h('span.cp-value', res), h('span.cp-count', itemCount), - //barGraphic((itemCount / max) * 100) + showBars? barGraphic((itemCount / max)): undefined, ]); }); results.push(h('div.cp-form-results-type-multiradio-data', [ h('span.cp-mr-q', q), h('span.cp-mr-value', values) ])); - return; -/* - var table = Charts.table([ - h('caption', { - style: 'color: var(--msg-color)', - }, q), - h('tbody', Object.keys(c).map(function (res) { - return Charts.row(res, c[res] / max, c[res]); - })), - ], [ - 'charts-css', - 'bar', - 'show-heading', - 'show-data-on-hover', - 'show-labels', - ]); - - results.push(h('div.cp-form-results-type-multiradio-data', { - style: 'width: 100%', - }, table)); -*/ }); results.push(getEmpty(empty)); @@ -1509,7 +1489,7 @@ define([ }; }, - printResults: function (answers, uid, form) { + printResults: function (answers, uid, form, content ) { // results multicheckbox var structure = form[uid]; if (!structure) { return; } @@ -1517,6 +1497,7 @@ define([ var results = []; var empty = 0; var count = {}; + var showBars = Boolean(content); Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; @@ -1544,35 +1525,17 @@ define([ var c = count[q_uid]; var values = Object.keys(c).map(function (res) { + var val = c[res]; return h('div.cp-form-results-type-radio-data', [ h('span.cp-value', res), - h('span.cp-count', c[res]) + h('span.cp-count', val), + showBars? barGraphic(val / max) : undefined, ]); }); results.push(h('div.cp-form-results-type-multiradio-data', [ h('span.cp-mr-q', q), - h('span.cp-mr-value', values) + h('span.cp-mr-value', values), ])); -/* - var table = Charts.table([ - h('caption', { - style: 'color: var(--msg-color)', - }, q), - h('tbody', Object.keys(c).map(function (res) { - return Charts.row(res, c[res] / max, c[res]); - })), - ], [ - 'charts-css', - 'bar', - 'show-heading', - 'show-data-on-hover', - 'show-labels', - ]); - - results.push(h('div.cp-form-results-type-multiradio-data', { - style: 'width: 100%', - }, table)); -*/ }); results.push(getEmpty(empty)); From 8215bf1c495ed1f4b2a9aa56c95f33797801cbef Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 18 Aug 2021 09:58:03 +0530 Subject: [PATCH 018/223] add draft changelog update --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60f86b192..185a9ae29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# 4.11.0 (WIP) + +## Goals + +## Update notes + +## Features + +* unify unregistered/non-registered/anonymous terminology as 'guest' +* prompt users that need support to subscribe +* include bar graphs for multiple-answer form questions + +## Bug fixes + + # 4.10.0 ## Goals From e82a0c15192f2051d7741b9e97992ca381f865af Mon Sep 17 00:00:00 2001 From: Weblate Date: Wed, 18 Aug 2021 07:36:50 +0200 Subject: [PATCH 019/223] Translated using Weblate (English) Currently translated at 100.0% (1384 of 1384 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ Translated using Weblate (English) Currently translated at 100.0% (1383 of 1383 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ --- www/common/translations/messages.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/www/common/translations/messages.json b/www/common/translations/messages.json index 3f04c2484..1cb85b4f1 100644 --- a/www/common/translations/messages.json +++ b/www/common/translations/messages.json @@ -1382,5 +1382,7 @@ "fm_link_invalid": "Invalid URL", "ui_collapse": "Collapse", "ui_expand": "Expand", - "form_totalResponses": "Total responses: {0}" + "form_totalResponses": "Total responses: {0}", + "support_premiumPriority": "Premium users help support improvements to CryptPad's usability and benefit from prioritized responses to their support tickets.", + "support_premiumLink": "View subscription options" } From 16f59792528db01b898efc1bf0b9575e8d3a84ef Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 18 Aug 2021 11:09:14 +0530 Subject: [PATCH 020/223] present premium support notice as a banner --- www/support/inner.js | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/www/support/inner.js b/www/support/inner.js index ebfe3bcfc..a193a2f77 100644 --- a/www/support/inner.js +++ b/www/support/inner.js @@ -45,6 +45,7 @@ define([ 'cp-support-list', ], 'new': [ // Msg.support_cat_new + 'cp-support-subscribe', 'cp-support-language', 'cp-support-form', ], @@ -166,8 +167,25 @@ define([ return $div; }; - Messages.support_premiumPriority = "Premium users help support improvements to CryptPad's usability and benefit from prioritized responses to their support tickets."; // XXX - Messages.support_premiumLink = 'View subscription options.'; // XXX + create['subscribe'] = function () { + if (!Pages.areSubscriptionsAllowed()) { return; } + var url = Pages.accounts.upgradeURL; + var accountsLink = h('a', { + href: url, + }, Messages.support_premiumLink); + $(accountsLink).click(function (ev) { + ev.preventDefault(); + common.openURL(url); + }); + + return $(h('div.cp-support-subscribe.cp-sidebarlayout-element', [ + h('div.alert.alert-info', [ + Messages.support_premiumPriority, + ' ', + accountsLink, + ]), + ])); + }; // Create a new tickets create['form'] = function () { @@ -175,20 +193,6 @@ define([ var $div = makeBlock(key, true); // Msg.support_formHint, .support_formTitle, .support_formButton Pages.documentationLink($div.find('a')[0], 'https://docs.cryptpad.fr/en/user_guide/index.html'); - var accountsLink = h('a', { - href: Pages.accounts.upgradeURL, - }, Messages.support_premiumLink,); - - var premium = h("div.alert.alert-info", [ - Messages.support_premiumPriority, - ' ', - accountsLink, - ]); - - if (Pages.areSubscriptionsAllowed()) { - $div.find('.cp-sidebarlayout-description').append(premium); - } - var form = APP.support.makeForm(); var id = Util.uid(); From cf7593553bbe8ed980611c29830cb1c44e1d4001 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 18 Aug 2021 14:46:56 +0530 Subject: [PATCH 021/223] polish and re-enable form response messages --- www/form/app-form.less | 9 ++++++++- www/form/inner.js | 24 +++++++++++++++++------- www/form/main.js | 2 +- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index 781b8161d..53acb92af 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -440,7 +440,6 @@ } } .cp-form-edit-block { - button.btn-secondary { margin-left: 30px; margin-bottom: 5px; @@ -487,6 +486,9 @@ p:last-child { margin-bottom: 0; } + * { + max-width: 100%; + } } .cp-form-creator-results-controls { @@ -832,6 +834,11 @@ color: @cp_form-poll-yes-color; } } + .cp-form-response-modal { + .CodeMirror { + border: 1px solid @cp_forms-border; + } + } .charts_main(); } diff --git a/www/form/inner.js b/www/form/inner.js index ad89ada26..ec329830f 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -2702,7 +2702,6 @@ define([ var responseMsg = h('div.cp-form-response-msg-container'); var $responseMsg = $(responseMsg); var refreshResponse = function () { - if (true) { return; } // XXX 4.11.0 $responseMsg.empty(); Messages.form_updateMsg = "Update response message"; // XXX 4.11.0 Messages.form_addMsg = "Add response message"; // XXX 4.11.0 @@ -2713,12 +2712,23 @@ define([ var editor; if (!APP.responseModal) { var t = h('textarea'); + var p = h('p', Messages.form_responseMsg); var div = h('div', [ - h('p', Messages.form_responseMsg), - t + p, + h('div.cp-form-response-modal', t), ]); - var cm = SFCodeMirror.create("gfm", CMeditor, t); + var cm = window.my_cm = SFCodeMirror.create("gfm", CMeditor, t); editor = APP.responseEditor = cm.editor; + var markdownTb = APP.common.createMarkdownToolbar(editor, { + embed: function (mt) { + editor.focus(); + editor.replaceSelection($(mt)[0].outerHTML); + } + }); + $(markdownTb.toolbar).insertAfter($(p)); + $(markdownTb.toolbar).show(); + + cm.configureTheme(APP.common, function () {}); editor.setOption('lineNumbers', true); editor.setOption('lineWrapping', true); editor.setOption('styleActiveLine', true); @@ -2735,7 +2745,7 @@ define([ name: Messages.settings_save, onClick: function () { var v = editor.getValue(); - content.answers.msg = v.trim(0, 2000); // XXX 4.11.0 max length? + content.answers.msg = v.slice(0, 2000); // XXX 4.11.0 max length? framework.localChange(); framework._.cpNfInner.chainpad.onSettle(function () { UI.log(Messages.saved); @@ -2761,9 +2771,9 @@ define([ } UI.openCustomModal(APP.responseModal); }); - // $responseMsg.append(btn); // XXX 4.11.0 + $responseMsg.append(btn); }; - //refreshResponse(); + refreshResponse(); // Allow anonymous answers var privacyContainer = h('div.cp-form-privacy-container'); diff --git a/www/form/main.js b/www/form/main.js index b82397caf..9a017fbe1 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -176,7 +176,7 @@ define([ validateKey: keys.secondaryValidateKey, owners: [myKeys.edPublic], crypto: crypto, - //Cache: Utils.Cache // XXX 4.11.0 + //Cache: Utils.Cache // TODO enable cache for form responses when the cache stops evicting old answers }; var results = {}; config.onError = function (info) { From fe256e8282fc982125a9c3691c03888bdcec1627 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 18 Aug 2021 14:55:31 +0530 Subject: [PATCH 022/223] give mobile users a way to escape from the error screen (aside from ESC) --- www/common/sframe-common.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index c05bdfe3f..206137c86 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -921,9 +921,10 @@ define([ }); ctx.sframeChan.on('EV_WORKER_TIMEOUT', function () { - UI.errorLoadingScreen(Messages.timeoutError, false, function () { // XXX 4.11.0 mobile users can't necessarily hit 'ESC' as this message suggests. provice a click option - funcs.gotoURL(''); - }); + var message = UI.setHTML(h('span'), Messages.timeoutError); + var cb = Util.once(function () { funcs.gotoURL(''); }); + $(message).find('em').on('touchend', cb); + UI.errorLoadingScreen(message, false, cb); }); ctx.sframeChan.on('EV_CHROME_68', function () { From 15b935eadc765debd7afebaef512b3005d662854 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 18 Aug 2021 15:07:19 +0530 Subject: [PATCH 023/223] change an XXX to a FIXME --- www/common/outer/cache-store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/outer/cache-store.js b/www/common/outer/cache-store.js index d1c3795c0..17fea6b16 100644 --- a/www/common/outer/cache-store.js +++ b/www/common/outer/cache-store.js @@ -97,7 +97,7 @@ define([ var checkCheckpoints = function (array) { if (!Array.isArray(array)) { return; } // Keep the last 100 messages - if (array.length > 100) { // XXX 4.11.0 + if (array.length > 100) { // FIXME this behaviour is only valid for chainpad-style documents array.splice(0, array.length - 100); } // Remove every message before the first checkpoint From aa84b625c7cb3363ac38c09ce79c0bc6898e1c19 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 19 Aug 2021 12:56:44 +0200 Subject: [PATCH 024/223] Improve forms participant view --- www/common/sframe-common-outer.js | 17 +++++++++++++++ www/common/toolbar.js | 4 +++- www/form/app-form.less | 20 +++++++++++++++++ www/form/inner.js | 36 +++++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 575971368..5223898d1 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -618,6 +618,7 @@ define([ prefersDriveRedirect: Utils.LocalStore.getDriveRedirectPreference(), isPresent: parsed.hashData && parsed.hashData.present, isEmbed: parsed.hashData && parsed.hashData.embed, + canEdit: hashes && hashes.editHash, oldVersionHash: parsed.hashData && parsed.hashData.version < 2, // password isHistoryVersion: parsed.hashData && parsed.hashData.versionHash, notifications: notifs, @@ -1749,6 +1750,22 @@ define([ }); }); + sframeChan.on('Q_COPY_VIEW_URL', function (data, cb) { + require(['/common/clipboard.js'], function (Clipboard) { + var url = window.location.origin + + Utils.Hash.hashToHref(hashes.viewHash, 'form'); + var success = Clipboard.copy(url); + cb(success); + }); + }); + sframeChan.on('EV_OPEN_VIEW_URL', function () { + var url = Utils.Hash.hashToHref(hashes.viewHash, 'form'); + var a = window.open(url); + if (!a) { + sframeChan.event('EV_POPUP_BLOCKED'); + } + }); + if (cfg.messaging) { sframeChan.on('Q_CHAT_OPENPADCHAT', function (data, cb) { Cryptpad.universal.execCommand({ diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 8bd0fcc32..31a40ffeb 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -64,7 +64,9 @@ MessengerUI, Messages, Pages) { if (!config.$container) { return; } var $container = config.$container; - var isEmbed = Bar.isEmbed = config.metadataMgr.getPrivateData().isEmbed; + var priv = config.metadataMgr.getPrivateData(); + var isEmbed = Bar.isEmbed = priv.isEmbed || + (priv.app === 'form' && priv.readOnly && !priv.form_auditorHash); if (isEmbed) { $container.hide(); } diff --git a/www/form/app-form.less b/www/form/app-form.less index 53acb92af..340cd8878 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -78,6 +78,26 @@ display: flex; } + .cp-form-view-title { + margin-bottom: 20px; + } + div.cp-form-view-logo { + align-items: center; + justify-content: center; + max-height: 140px; + font-size: 50px; + display: flex; + font-family: "IBM Plex Mono"; + .tools_unselectable(); + color: @cp_sidebar-hint; + padding: 20px; + cursor: pointer; + img { + max-height: 100%; + margin-right: 20px; + } + } + div.cp-form-creator-container { display: flex; flex: 1; diff --git a/www/form/inner.js b/www/form/inner.js index ec329830f..b829919ec 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1,6 +1,7 @@ define([ 'jquery', 'json.sortify', + '/api/config', '/bower_components/chainpad-crypto/crypto.js', '/common/sframe-app-framework.js', '/common/toolbar.js', @@ -42,6 +43,7 @@ define([ ], function ( $, Sortify, + ApiConfig, Crypto, Framework, Toolbar, @@ -2608,7 +2610,24 @@ define([ } // In view mode, add "Submit" and "reset" buttons + // Embed mode is enforced so we add the title at the top and a CryptPad logo + // at the bottom + var title = framework._.title.title || framework._.title.defaultTitle; + $container.prepend(h('h1.cp-form-view-title', title)); + $container.append(makeFormControls(framework, content, Boolean(answers), evOnChange)); + + var logo = h('div.cp-form-view-logo', [ + h('img', { + src:'/customize/CryptPad_logo_grey.svg?'+ApiConfig.requireConf.urlArgs, + alt:'CryptPad_logo' + }), + h('span', 'CryptPad') + ]); + $(logo).click(function () { + framework._.sfCommon.gotoURL('/drive/'); + }); + $container.append(logo); if (!answers) { $container.find('.cp-reset-button').attr('disabled', 'disabled'); } @@ -2672,6 +2691,22 @@ define([ } var makeFormSettings = function () { + Messages.form_preview = "Preview participant page"; // XXX + Messages.form_geturl = "Copy participant link"; // XXX + var previewBtn = h('button.btn.btn-primary', Messages.form_preview); + var participantBtn = h('button.btn.btn-primary', Messages.form_geturl); + var preview = h('div.cp-forms-results-participant', [previewBtn, participantBtn]); + $(previewBtn).click(function () { + sframeChan.event('EV_OPEN_VIEW_URL'); + }); + $(participantBtn).click(function () { + sframeChan.query('Q_COPY_VIEW_URL', null, function (err, success) { + if (success) { return void UI.log(Messages.shareSuccess); } + UI.warn(Messages.error); + }); + }); + + // Private / public status var resultsType = h('div.cp-form-results-type-container'); var $results = $(resultsType); @@ -2873,6 +2908,7 @@ define([ //evOnChange.reg(refreshResponse); return [ + preview, endDateContainer, privacyContainer, resultsType, From e414524eab6bfc3f06ed3cd705d67f01c9880d27 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 19 Aug 2021 16:17:59 +0530 Subject: [PATCH 025/223] refactor form results display * improve table component reusability for the rest of the platform * display count of empty results at the top of each section * remove unused styles * fix incorrect methods for counting empty answers for multi-checkbox questions --- customize.dist/src/less2/include/charts.less | 52 ++++++++ www/form/app-form.less | 64 +--------- www/form/inner.js | 120 ++++++++++--------- 3 files changed, 117 insertions(+), 119 deletions(-) diff --git a/customize.dist/src/less2/include/charts.less b/customize.dist/src/less2/include/charts.less index c8b983707..ea402094f 100644 --- a/customize.dist/src/less2/include/charts.less +++ b/customize.dist/src/less2/include/charts.less @@ -50,4 +50,56 @@ } } } + + .cp-charts-cell { + border: 1px solid @cp_form-border; + display: table-cell; + padding: 5px 10px; + background: @cp_form-bg2; + } + + .cp-form-results-type-radio { + .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 { + .cp-charts-cell(); + } + } + } + + .cp-charts.cp-bar-table, .cp-charts.cp-text-table { + display: table; + width: 100%; + .cp-charts-row { + display: table-row; + border: 1px solid @cp_form-border; + &.full { + display: flex; + flex-flow: column; + } + + & > span { + .cp-charts-cell(); + display: table-cell; + &.cp-value { + min-width: 200px; + } + &.cp-bar-container { + width: 99%; + padding: 0px; + position: relative; + .cp-bar { + position: absolute; + background: @cryptpad_color_brand; + height: 100%; + } + } + } + } + } } diff --git a/www/form/app-form.less b/www/form/app-form.less index 53acb92af..5ef211201 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -519,69 +519,9 @@ i { margin-right: 5px; } background: fade(@cryptpad_text_col, 15%); } - .cp-form-results-type-text { - max-height: 300px; + .cp-form-results-contained { + max-height: 350px; // enough for 10 table entries 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; - padding: 5px 10px; - background: @cp_form-bg2; - &:not(:last-child) { margin-bottom: 1px; } - } - - .cp-form-results-cell() { - border: 1px solid @cp_form-border; - display: table-cell; - padding: 5px 10px; - background: @cp_form-bg2; - } - - .cp-form-results-type-multiradio-data { - .cp-mr-q { - font-weight: bold; - padding: 5px 10px; - .cp-form-results-cell(); - } - &:not(:first-child) { - .cp-mr-q { - margin-top: 15px; - } - } - } - - .cp-form-results-type-radio { - display: table; - width: 100%; - .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 { - .cp-form-results-cell(); - &.cp-value { - min-width: 200px; - } - &.cp-bar-container { - width: 99%; - padding: 0px; - position: relative; - .cp-bar { - position: absolute; - background: @cryptpad_color_brand; - height: 100%; - } - } - } - } } .cp-form-individual { background: @cp_form-bg1; diff --git a/www/form/inner.js b/www/form/inner.js index ec329830f..cb85ae29c 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -803,10 +803,31 @@ define([ return total; }; - var getEmpty = function (empty) { // TODO don't include this in the scrollable area - if (empty) { - return UI.setHTML(h('div.cp-form-results-type-text-empty'), Messages._getKey('form_notAnswered', [empty])); - } + var multiAnswerSubHeading = function (content) { + return h('span.cp-charts-row', h('td.cp-charts-cell', { + colspan: 3, + style: 'font-weight: bold;', + }, content)); + }; + + var getEmpty = function (empty) { + if (!empty) { return; } + var msg = UI.setHTML(h('span.cp-form-results-empty-text'), Messages._getKey('form_notAnswered', [empty])); + return multiAnswerSubHeading(msg); + }; + + var barGraphic = function (itemScale) { + return h('span.cp-bar-container', h('div.cp-bar', { + style: 'width: ' + (itemScale * 100) + '%', + }, ' ')); + }; + + var barRow = function (value, count, max, showBar) { + return h('div.cp-charts-row', [ + h('span.cp-value', value), + h('span.cp-count', count), + showBar? barGraphic((count / max)): undefined, + ]); }; var findItem = function (items, uid) { @@ -957,27 +978,21 @@ define([ return Array.isArray(A)? Math.max.apply(null, A): NaN; }; - var barGraphic = function (itemScale) { - return h('span.cp-bar-container', h('div.cp-bar', { - style: 'width: ' + (itemScale * 100) + '%', - }, ' ')); - }; - var renderTally = function (tally, empty, showBar) { var rows = []; var counts = Util.values(tally); var max = arrayMax(counts); + if (empty) { rows.push(getEmpty(empty)); } Object.keys(tally).forEach(function (value) { var itemCount = tally[value]; var itemScale = (itemCount / max); - rows.push(h('div.cp-form-results-type-radio-data', [ + rows.push(h('div.cp-charts-row', [ h('span.cp-value', value), h('span.cp-count', itemCount), showBar? barGraphic(itemScale): undefined, ])); }); - if (empty) { rows.push(getEmpty(empty)); } return rows; }; @@ -1015,34 +1030,35 @@ define([ reset: function () { $tag.val(''); } }; }, - printResults: function (answers, uid) { + printResults: function (answers, uid) { // results text var results = []; var empty = 0; var tally = {}; + var isEmpty = function (answer) { + console.error("EMPTY?", JSON.stringify(answer)); + return !answer || !answer.trim(); + }; + Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; - if (!answer || !answer.trim()) { return empty++; } + if (isEmpty(answer)) { return empty++; } Util.inc(tally, answer); }); //var counts = Util.values(tally); //var max = arrayMax(counts); //if (max < 2) { // there are no duplicates, so just return text + results.push(getEmpty(empty)); 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(h('div.cp-charts-row', h('span.cp-value', answer))); }); - results.push(getEmpty(empty)); - return h('div.cp-form-results-type-text', results); + return h('div.cp-form-results-contained', h('div.cp-charts.cp-text-table', results)); //} -/* - var rendered = renderTally(tally, empty); - return h('div.cp-form-results-type-text', rendered); -*/ }, icon: h('i.cptools.cptools-form-text') }, @@ -1104,11 +1120,11 @@ define([ 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(h('div.cp-charts-row', h('span.cp-value', answer))); }); - results.push(getEmpty(empty)); + results.unshift(getEmpty(empty)); - return h('div.cp-form-results-type-text', results); + return h('div.cp-form-results-contained', h('div.cp-charts.cp-text-table', results)); }, icon: h('i.cptools.cptools-form-paragraph') }, @@ -1179,7 +1195,7 @@ define([ }); var rendered = renderTally(count, empty, showBars); - return h('div.cp-form-results-type-radio', rendered); + return h('div.cp-charts.cp-bar-table', rendered); }, icon: h('i.cptools.cptools-form-list-radio') }, @@ -1285,26 +1301,17 @@ define([ max = arrayMax(counts); }); + results.push(getEmpty(empty)); count_keys.forEach(function (q_uid) { var q = findItem(opts.items, q_uid); var c = count[q_uid]; - - var values = Object.keys(c).map(function (res) { - var itemCount = c[res]; - return h('div.cp-form-results-type-radio-data', [ - h('span.cp-value', res), - h('span.cp-count', itemCount), - showBars? barGraphic((itemCount / max)): undefined, - ]); + results.push(multiAnswerSubHeading(q)); + Object.keys(c).forEach(function (res) { + results.push(barRow(res, c[res], max, showBars)); }); - 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); + return h('div.cp-charts.cp-bar-table', results); }, exportCSV: function (answer, form) { var opts = form.opts || {}; @@ -1401,7 +1408,7 @@ define([ }); var rendered = renderTally(count, empty, showBars); - return h('div.cp-form-results-type-radio', rendered); + return h('div.cp-charts.cp-bar-table', rendered); }, icon: h('i.cptools.cptools-form-list-check') }, @@ -1498,10 +1505,19 @@ define([ var empty = 0; var count = {}; var showBars = Boolean(content); + + var isEmpty = function (answer) { + if (!answer) { return true; } + return !Object.keys(answer).some(function (k) { + var A = answer[k]; + return Array.isArray(A) && A.length; + }); + }; + Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; - if (!answer || !Object.keys(answer).length) { return empty++; } + if (isEmpty(answer)) { return empty++; } Object.keys(answer).forEach(function (q_uid) { var c = count[q_uid] = count[q_uid] || {}; var res = answer[q_uid]; @@ -1520,26 +1536,17 @@ define([ max = arrayMax(counts); }); + results.push(getEmpty(empty)); count_keys.forEach(function (q_uid) { var q = findItem(opts.items, q_uid); var c = count[q_uid]; - - var values = Object.keys(c).map(function (res) { - var val = c[res]; - return h('div.cp-form-results-type-radio-data', [ - h('span.cp-value', res), - h('span.cp-count', val), - showBars? barGraphic(val / max) : undefined, - ]); + results.push(multiAnswerSubHeading(q)); + Object.keys(c).forEach(function (res) { + results.push(barRow(res, c[res], max, showBars)); }); - 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); + return h('div.cp-charts.cp-bar-table', results); }, exportCSV: function (answer, form) { var opts = form.opts || {}; @@ -1658,7 +1665,6 @@ define([ // results sort var opts = form[uid].opts || TYPES.sort.defaultOpts; var l = (opts.values || []).length; - //var results = []; var empty = 0; var count = {}; var showBars = Boolean(content); @@ -1673,7 +1679,7 @@ define([ }); var rendered = renderTally(count, empty, showBars); - return h('div.cp-form-results-type-radio', rendered); + return h('div.cp-charts.cp-bar-table', rendered); }, icon: h('i.cptools.cptools-form-list-ordered') }, From f537d8f65864ffb552c872ffbeb54a25e1226717 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 19 Aug 2021 14:44:14 +0200 Subject: [PATCH 026/223] Improve form editor UX --- www/form/app-form.less | 8 ++++++ www/form/inner.js | 64 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index 340cd8878..a0d0bdd65 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -313,6 +313,14 @@ margin-bottom: 20px; } + .cp-form-preview { + color: @cp_sidebar-hint; + margin-bottom: 10px; + padding: 0; + text-align: center; + font-weight: bold; + } + .cp-form-block-drag-handle { display: flex; flex-flow: column; diff --git a/www/form/inner.js b/www/form/inner.js index b829919ec..bf2228e7b 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1115,6 +1115,7 @@ define([ icon: h('i.cptools.cptools-form-paragraph') }, radio: { + compatible: ['radio', 'checkbox', 'sort'], defaultOpts: { values: [1,2].map(function (i) { return Messages._getKey('form_defaultOption', [i]); @@ -1186,6 +1187,7 @@ define([ icon: h('i.cptools.cptools-form-list-radio') }, multiradio: { + compatible: ['multiradio', 'multicheck'], defaultOpts: { items: [1,2].map(function (i) { return { @@ -1325,6 +1327,7 @@ define([ icon: h('i.cptools.cptools-form-grid-radio') }, checkbox: { + compatible: ['radio', 'checkbox', 'sort'], defaultOpts: { max: 3, values: [1, 2, 3].map(function (i) { @@ -1341,6 +1344,7 @@ define([ $(cbox).find('input').data('val', data); return cbox; }); + if (!opts.max) { opts.max = TYPES.checkbox.defaultOpts.max; } 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) @@ -1408,6 +1412,7 @@ define([ icon: h('i.cptools.cptools-form-list-check') }, multicheck: { + compatible: ['multiradio', 'multicheck'], defaultOpts: { max: 3, items: [1,2].map(function (i) { @@ -1437,6 +1442,8 @@ define([ return h('div.radio-group', {'data-uid':name}, els); }); + if (!opts.max) { opts.max = TYPES.multicheck.defaultOpts.max; } + lines.forEach(function (l) { $(l).find('input').on('change', function () { var selected = $(l).find('input:checked').length; @@ -1560,6 +1567,7 @@ define([ icon: h('i.cptools.cptools-form-grid-check') }, sort: { + compatible: ['radio', 'checkbox', 'sort'], defaultOpts: { values: [1,2].map(function (i) { return Messages._getKey('form_defaultOption', [i]); @@ -2389,6 +2397,9 @@ define([ APP.formBlocks.push(data); + Messages.form_preview = "Preview:"; // XXX + var previewDiv = h('div.cp-form-preview', Messages.form_preview); + if (editable) { // Drag handle dragHandle = h('span.cp-form-block-drag-handle', [ @@ -2446,6 +2457,7 @@ define([ // Delete question var edit = h('span'); + var changeType; var del = h('button.btn.btn-danger-alt', [ h('i.fa.fa-trash-o'), h('span', Messages.form_delete) @@ -2475,6 +2487,7 @@ define([ $(editContainer).empty(); $(editButtons).show(); $(data.tag).show(); + $(previewDiv).show(); return; } $(editContainer).empty(); @@ -2492,6 +2505,7 @@ define([ }; var onEdit = function (tmp) { data.editing = true; + $(previewDiv).hide(); $(data.tag).hide(); $(editContainer).append(data.edit(onSave, tmp, framework)); $(editButtons).hide(); @@ -2506,10 +2520,57 @@ define([ onEdit(temp[uid]); }); } + + Messages.form_changeType = "Change type"; // XXX + Messages.form_changeTypeConfirm = "Select the new type of this question and click OK."; // XXX + if (Array.isArray(model.compatible)) { + changeType = h('button.btn.btn-secondary', [ + h('i.fa.fa-question'), + h('span', Messages.form_changeType) + ]); + $(changeType).click(function () { + var name = Util.uid(); + var els = model.compatible.map(function (data, i) { + var text = Messages['form_type_'+data]; + if (!text) { return; } + var radio = UI.createRadio(name, 'cp-form-changetype-'+i, + text, data===type, { mark: { tabindex:1 } }); + $(radio).find('input').data('val', data); + return radio; + }); + var tag = h('div.radio-group', els); + var changeTypeContent = [ + h('p', Messages.form_changeTypeConfirm), + tag + ]; + UI.confirm(changeTypeContent, function (yes) { + if (!yes) { return; } + var res; + els.some(function (el) { + var $i = $(el).find('input'); + if (Util.isChecked($i)) { + res = $i.data('val'); + return true; + } + }); + if (res === type || !TYPES[res]) { return; } + model = TYPES[res]; + type = res; + if (!data) { data = {}; } + block.type = res; + framework.localChange(); + var $oldTag = $(data.tag); + framework._.cpNfInner.chainpad.onSettle(function () { + data = model.get(block.opts, _answers, null, evOnChange); + $oldTag.before(data.tag).remove(); + }); + }); + }); + } } editButtons = h('div.cp-form-edit-buttons-container', [ - edit, del + edit, changeType, del ]); } var editableCls = editable ? ".editable" : ""; @@ -2519,6 +2580,7 @@ define([ APP.isEditor ? dragHandle : undefined, isStatic ? undefined : q, h('div.cp-form-block-content', [ + isStatic || !APP.isEditor ? undefined : previewDiv, data.tag, editButtons ]), From bfdcf4ec0ce38f9e2930a8d988cc038a4f6f4df9 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 19 Aug 2021 18:20:14 +0530 Subject: [PATCH 027/223] fix user/display name rendering which I accidentally broke in the user admin menu --- www/common/toolbar.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 8bd0fcc32..d103fe410 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -53,6 +53,7 @@ MessengerUI, Messages, Pages) { var USERADMIN_CLS = Bar.constants.user = 'cp-toolbar-user-dropdown'; var USERNAME_CLS = Bar.constants.username = 'cp-toolbar-user-name'; /*var READONLY_CLS = */Bar.constants.readonly = 'cp-toolbar-readonly'; + var USERBUTTON_CLS = Bar.constants.changeUsername = "cp-toolbar-user-rename"; // Create the toolbar element @@ -1028,6 +1029,12 @@ MessengerUI, Messages, Pages) { var userMenuCfg = { $initBlock: $userAdmin, }; + if (!config.hideDisplayName) { + $.extend(true, userMenuCfg, { + displayNameCls: USERNAME_CLS, + changeNameButtonCls: USERBUTTON_CLS, + }); + } if (config.readOnly !== 1) { userMenuCfg.displayName = 1; userMenuCfg.displayChangeName = 1; From eafe27ffb46a00fa16e4ea9369c0b51e58b30dee Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 19 Aug 2021 18:21:48 +0530 Subject: [PATCH 028/223] reuse cp-charts to visualize server task running time as bar charts --- www/admin/app-admin.less | 24 +++++++++++++++++++ www/admin/inner.js | 52 +++++++++++++++++++++++++--------------- 2 files changed, 57 insertions(+), 19 deletions(-) diff --git a/www/admin/app-admin.less b/www/admin/app-admin.less index 227792540..f66eb75f7 100644 --- a/www/admin/app-admin.less +++ b/www/admin/app-admin.less @@ -1,12 +1,14 @@ @import (reference) '../../customize/src/less2/include/framework.less'; @import (reference) '../../customize/src/less2/include/sidebar-layout.less'; @import (reference) '../../customize/src/less2/include/support.less'; +@import (reference) '../../customize/src/less2/include/charts.less'; &.cp-app-admin { .framework_min_main(); .sidebar-layout_main(); .support_main(); + .charts_main(); .cp-hidden { display: none !important; @@ -294,5 +296,27 @@ } } } + span.cp-bar.profiling-percentage { + text-align: center; + padding: 5px; + } + span.profiling-label { + position: absolute; + z-index: 1; + width: 100%; + text-align: center; + padding: 5px; + } + #profiling-chart { + .cp-bar-container { + max-width: 400px; + } + } + .width-constrained { + max-width: 800px; + } + .cp-charts-row.heading { + font-weight: bold; + } } diff --git a/www/admin/inner.js b/www/admin/inner.js index e8d567d41..63c516614 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -1673,34 +1673,51 @@ define([ var $div = makeBlock('performance-profiling'); // Msg.admin_performanceProfilingHint, .admin_performanceProfilingTitle var onRefresh = function () { - var body = h('tbody'); + var createBody = function () { + return h('div#profiling-chart.cp-charts.cp-bar-table', [ + h('span.cp-charts-row.heading', [ + h('span', Messages.admin_performanceKeyHeading), + h('span', Messages.admin_performanceTimeHeading), + h('span', Messages.admin_performancePercentHeading), + //h('span', ''), //Messages.admin_performancePercentHeading), + ]), + ]); + }; - var table = h('table#cp-performance-table', [ - h('thead', [ - h('th', Messages.admin_performanceKeyHeading), - h('th', Messages.admin_performanceTimeHeading), - h('th', Messages.admin_performancePercentHeading), - ]), - body, - ]); - var appendRow = function (key, time, percent) { - console.log("[%s] %ss running time (%s%)", key, time, percent); - body.appendChild(h('tr', [ key, time, percent ].map(function (x) { - return h('td', x); - }))); + var body = createBody(); + var appendRow = function (key, time, percent, scaled) { + //console.log("[%s] %ss running time (%s%)", key, time, percent); + body.appendChild(h('span.cp-charts-row', [ + h('span', key), + h('span', time), + //h('span', percent), + h('span.cp-bar-container', [ + h('span.cp-bar.profiling-percentage', { + style: 'width: ' + scaled + '%', + }, ' ' ), + h('span.profiling-label', percent + '%'), + ]), + ])); }; var process = function (_o) { + $('#profiling-chart').remove(); + body = createBody(); var o = _o[0]; var sorted = Object.keys(o).sort(function (a, b) { if (o[b] - o[a] <= 0) { return -1; } return 1; }); + + var values = sorted.map(function (k) { return o[k]; }); var total = 0; - sorted.forEach(function (k) { total += o[k]; }); + values.forEach(function (value) { total += value; }); + var max = Math.max.apply(null, values); + sorted.forEach(function (k) { var percent = Math.floor((o[k] / total) * 1000) / 10; - appendRow(k, o[k], percent); + appendRow(k, o[k], percent, (o[k] / max) * 100); }); + $div.append(h('div.width-constrained', body)); }; sFrameChan.query('Q_ADMIN_RPC', { @@ -1710,10 +1727,7 @@ define([ UI.warn(Messages.error); return void console.error(e, data); } - //console.info(data); - $div.find("table").remove(); process(data); - $div.append(table); }); }; From 7bbe9059a1be0acbc9d727494e9a23dda16ccd04 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 19 Aug 2021 15:23:49 +0200 Subject: [PATCH 029/223] Tell users when a form has already been submitted --- www/form/inner.js | 22 ++++++++++++++++++---- www/form/main.js | 6 ++++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index bf2228e7b..2f717cb05 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1388,6 +1388,10 @@ define([ $el.prop('checked', true); } }); + var selected = $tag.find('input:checked').length; + if (selected >= opts.max) { + $tag.find('input:not(:checked)').attr('disabled', 'disabled'); + } } }; @@ -2671,14 +2675,22 @@ define([ return; } + // If the form is already submitted, show an info message + Messages.form_alreadyAnswered = "You've submitted answers to this form on {0}"; // XXX + if (answers) { + $container.prepend(h('div.alert.alert-info', + Messages._getKey('form_alreadyAnswered', [ + new Date(answers._time).toLocaleString()]))); + // XXX make the page read-only? + } + // In view mode, add "Submit" and "reset" buttons + $container.append(makeFormControls(framework, content, Boolean(answers), evOnChange)); + // Embed mode is enforced so we add the title at the top and a CryptPad logo // at the bottom var title = framework._.title.title || framework._.title.defaultTitle; $container.prepend(h('h1.cp-form-view-title', title)); - - $container.append(makeFormControls(framework, content, Boolean(answers), evOnChange)); - var logo = h('div.cp-form-view-logo', [ h('img', { src:'/customize/CryptPad_logo_grey.svg?'+ApiConfig.requireConf.urlArgs, @@ -2690,6 +2702,7 @@ define([ framework._.sfCommon.gotoURL('/drive/'); }); $container.append(logo); + if (!answers) { $container.find('.cp-reset-button').attr('disabled', 'disabled'); } @@ -3156,7 +3169,7 @@ define([ } // 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) { + if (0 && content.answers.privateKey && Object.keys(content.form).some(function (uid) { return content.form[uid].type === "poll"; })) { sframeChan.query("Q_FORM_FETCH_ANSWERS", { @@ -3186,6 +3199,7 @@ define([ var myAnswersObj = answers[curve1] || answers[curve2] || undefined; if (myAnswersObj) { myAnswers = myAnswersObj.msg; + myAnswers._time = myAnswersObj.time; } } // If we have a non-anon answer, we can't answer anonymously later diff --git a/www/form/main.js b/www/form/main.js index 9a017fbe1..4c0831fe9 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -272,8 +272,10 @@ define([ 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 parsed = JSON.parse(res.content); + parsed._isAnon = answer.anonymous; + parsed._time = messages[0].time; + cb(parsed); }); }); From 82101bcb9b16ee2a7c187f4eab310bc8789ed358 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 19 Aug 2021 21:16:49 +0530 Subject: [PATCH 030/223] use two characters for the default avatar --- customize.dist/src/less2/include/avatar.less | 2 +- customize.dist/src/less2/include/toolbar.less | 2 +- www/common/inner/common-mediatag.js | 11 ++++++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/customize.dist/src/less2/include/avatar.less b/customize.dist/src/less2/include/avatar.less index 725c7748f..02abc4050 100644 --- a/customize.dist/src/less2/include/avatar.less +++ b/customize.dist/src/less2/include/avatar.less @@ -4,7 +4,7 @@ @width: 30px ) { @avatar-width: @width; - @avatar-font-size: @width / 1.2; + @avatar-font-size: @width / 1.8; } .avatar_main(@width: 30px) { --LessLoader_require: LessLoader_currentFile(); diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index b0f9b5e42..28b513095 100644 --- a/customize.dist/src/less2/include/toolbar.less +++ b/customize.dist/src/less2/include/toolbar.less @@ -855,7 +855,7 @@ span { text-align: center; width: 100%; - font-size: 48px; + font-size: 40px; display: inline-flex; justify-content: center; align-items: center; diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index 1d88e1029..03d85fab0 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -82,7 +82,16 @@ define([ MT.displayAvatar = function (common, $container, href, name, _cb) { var cb = Util.once(Util.mkAsync(_cb || function () {})); var displayDefault = function () { - var text = Util.getFirstCharacter(name || Messages.anonymous); + name = (name || "").trim() || Messages.anonymous; + var parts = name.split(/\s+/); + var text; + if (parts.length > 1) { + text = parts.slice(0, 2).map(Util.getFirstCharacter).join(''); + } else { + text = Util.getFirstCharacter(name); + text += Util.getFirstCharacter(name.replace(text, '')); + } + var $avatar = $('', {'class': 'cp-avatar-default'}).text(text); $container.append($avatar); if (cb) { cb(); } From b8c847bccef0001553cfc09724f38939dd8cd74c Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 19 Aug 2021 22:25:51 +0530 Subject: [PATCH 031/223] prototype animal avatars for guests that haven't set a custom name --- www/common/inner/common-mediatag.js | 34 ++++++++++++++++++++++++++--- www/common/toolbar.js | 5 +++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index 03d85fab0..2dc912e02 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -79,21 +79,48 @@ define([ }); }; - MT.displayAvatar = function (common, $container, href, name, _cb) { + // https://emojipedia.org/nature/ + var ANIMALS = [ '🙈', '🦀', '🐞', '🦋', '🐬', '🐋', '🐢', '🦉', '🦆', '🐧', '🦡', '🦘', '🦨', '🦦', '🦥', '🐼', '🐻', '🦝', '🦄', '🐄', '🐷', '🐐', '🦙', '🦒', '🐘', '🦏', '🐁', '🐹', '🐰', '🦫', '🦔', '🐨']; + + var getRandomAnimal = function () { + return ANIMALS[Math.floor(Math.random() * ANIMALS.length)]; + }; + + var getPseudorandomAnimal = function (seed) { + if (typeof(seed) !== 'string') { return getRandomAnimal(); } + seed = seed.replace(/\D/g, '').slice(0, 10); + seed = parseInt(seed); + if (!seed) { return getRandomAnimal(); } + return ANIMALS[seed % ANIMALS.length]; + }; + + MT.displayAvatar = function (common, $container, href, name, _cb, uid) { var cb = Util.once(Util.mkAsync(_cb || function () {})); var displayDefault = function () { + if (avatars[uid]) { + var nodes = $.parseHTML(avatars[uid]); + var $el = $(nodes[0]); + $container.append($el); + return void cb($el); + } + var animal = false; + name = (name || "").trim() || Messages.anonymous; var parts = name.split(/\s+/); var text; - if (parts.length > 1) { + if (name === Messages.anonymous) { + text = getPseudorandomAnimal(uid); + animal = true; + } else if (parts.length > 1) { text = parts.slice(0, 2).map(Util.getFirstCharacter).join(''); } else { text = Util.getFirstCharacter(name); text += Util.getFirstCharacter(name.replace(text, '')); } - var $avatar = $('', {'class': 'cp-avatar-default'}).text(text); + var $avatar = $('', {'class': 'cp-avatar-default' + (animal? ' animal': '')}).text(text); $container.append($avatar); + avatars[uid] = $avatar[0].outerHTML; if (cb) { cb(); } }; if (!window.Symbol) { return void displayDefault(); } // IE doesn't have Symbol @@ -106,6 +133,7 @@ define([ return void cb($el); } + var centerImage = function ($img, $image) { var img = $image[0]; var w = img.width; diff --git a/www/common/toolbar.js b/www/common/toolbar.js index d103fe410..791857049 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -361,9 +361,10 @@ MessengerUI, Messages, Pages) { Common.openURL(origin+'/profile/#' + data.profile); }); } - Common.displayAvatar($span, data.avatar, name, function () { + console.error("AVATAR", $span, data.uid); + Common.displayAvatar($span, data.avatar, name, function () { // XXX pass a little more info so we can display better (pseudo-random) defaults $span.append($rightCol); - }); + }, data.uid); $span.data('uid', data.uid); $editUsersList.append($span); }); From 904e06091da39b6f56ef88382091e2411a30ff0f Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 20 Aug 2021 11:29:16 +0200 Subject: [PATCH 032/223] Fix calendar .ics import (#784) --- www/calendar/export.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/www/calendar/export.js b/www/calendar/export.js index 64775762b..5967e2027 100644 --- a/www/calendar/export.js +++ b/www/calendar/export.js @@ -123,6 +123,7 @@ define([ var jcalData = ICAL.parse(content); vcalendar = new ICAL.Component(jcalData); } catch (e) { + console.error(e); return void cb(e); } @@ -147,6 +148,18 @@ define([ var isAllDay = false; var start = ev.getFirstPropertyValue('dtstart'); var end = ev.getFirstPropertyValue('dtend'); + var duration = ev.getFirstPropertyValue('duration'); + if (!end && !duration) { + if (start.isDate) { + end = start.clone(); + end.adjust(1); // Add one day + } else { + end = start.clone(); + } + } else if (!end) { + end = start.clone(); + end.addDuration(duration); + } if (start.isDate && end.isDate) { isAllDay = true; start = String(start); @@ -175,7 +188,7 @@ define([ hidden.push(al.toString()); } var trigger = al.getFirstPropertyValue('trigger'); - var minutes = -trigger.toSeconds() / 60; + var minutes = trigger ? (-trigger.toSeconds() / 60) : 0; if (reminders.indexOf(minutes) === -1) { reminders.push(minutes); } }); From c5e6ca646eb0ab2868f3c2c31602537e6cbfed19 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 20 Aug 2021 15:57:14 +0530 Subject: [PATCH 033/223] adjust animal avatar caching system and adjust size in the toolbar --- customize.dist/src/less2/include/toolbar.less | 3 +++ www/common/inner/common-mediatag.js | 21 ++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index 28b513095..d8029f0d2 100644 --- a/customize.dist/src/less2/include/toolbar.less +++ b/customize.dist/src/less2/include/toolbar.less @@ -200,6 +200,9 @@ .avatar_main(30px); .cp-avatar-default, media-tag { margin-right: 5px; + &.animal { + font-size: 20px; + } } &.cp-userlist-clickable { cursor: pointer; diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index 2dc912e02..ece9e3024 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -80,7 +80,7 @@ define([ }; // https://emojipedia.org/nature/ - var ANIMALS = [ '🙈', '🦀', '🐞', '🦋', '🐬', '🐋', '🐢', '🦉', '🦆', '🐧', '🦡', '🦘', '🦨', '🦦', '🦥', '🐼', '🐻', '🦝', '🦄', '🐄', '🐷', '🐐', '🦙', '🦒', '🐘', '🦏', '🐁', '🐹', '🐰', '🦫', '🦔', '🐨']; + var ANIMALS = '🙈 🦀 🐞 🦋 🐬 🐋 🐢 🦉 🦆 🐧 🦡 🦘 🦨 🦦 🦥 🐼 🐻 🦝 🦓 🐄 🐷 🐐 🦙 🦒 🐘 🦏 🐁 🐹 🐰 🦫 🦔 🐨 🐱 🐺 👺 👹 👽 👾 🤖'.split(/\s+/); var getRandomAnimal = function () { return ANIMALS[Math.floor(Math.random() * ANIMALS.length)]; @@ -94,14 +94,13 @@ define([ return ANIMALS[seed % ANIMALS.length]; }; + var animal_avatars = {}; MT.displayAvatar = function (common, $container, href, name, _cb, uid) { var cb = Util.once(Util.mkAsync(_cb || function () {})); var displayDefault = function () { - if (avatars[uid]) { - var nodes = $.parseHTML(avatars[uid]); - var $el = $(nodes[0]); - $container.append($el); - return void cb($el); + var animal_avatar; + if (uid && animal_avatars[uid]) { + animal_avatar = animal_avatars[uid] } var animal = false; @@ -109,7 +108,11 @@ define([ var parts = name.split(/\s+/); var text; if (name === Messages.anonymous) { - text = getPseudorandomAnimal(uid); + if (animal_avatar) { + text = animal_avatar; + } else { + text = animal_avatar = getPseudorandomAnimal(uid); + } animal = true; } else if (parts.length > 1) { text = parts.slice(0, 2).map(Util.getFirstCharacter).join(''); @@ -120,7 +123,9 @@ define([ var $avatar = $('', {'class': 'cp-avatar-default' + (animal? ' animal': '')}).text(text); $container.append($avatar); - avatars[uid] = $avatar[0].outerHTML; + if (uid && animal) { + animal_avatars[uid] = animal_avatar; + } if (cb) { cb(); } }; if (!window.Symbol) { return void displayDefault(); } // IE doesn't have Symbol From ff1c4c9a65971033f085957ea4c4c8c6f3786005 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 20 Aug 2021 16:02:00 +0530 Subject: [PATCH 034/223] update the calendar list when calendar removal is successful also fall back to team.name when team.displayName is not available --- www/calendar/inner.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/www/calendar/inner.js b/www/calendar/inner.js index 21cb12789..a1fd8c8e8 100644 --- a/www/calendar/inner.js +++ b/www/calendar/inner.js @@ -566,7 +566,7 @@ define([ attributes: { 'class': 'fa fa-trash-o', }, - content: h('span', Messages.kanban_delete), + content: h('span', Messages.kanban_delete), // XXX delete key is misleading... "Remove" ? `poll_remove`, `fc_remove` action: function (e) { e.stopPropagation(); var cal = APP.calendars[id]; @@ -586,8 +586,9 @@ define([ }, function (err) { if (err) { console.error(err); - UI.warn(Messages.error); + return void UI.warn(Messages.error); } + renderCalendar(); }); }); } @@ -722,7 +723,7 @@ define([ if (!calendars.length) { return; } var team = privateData.teams[teamId]; var avatar = h('span.cp-avatar'); - common.displayAvatar($(avatar), team.avatar, team.displayName); + common.displayAvatar($(avatar), team.avatar, team.displayName || team.name); APP.$calendars.append(h('div.cp-calendar-team', [ avatar, h('span.cp-name', {title: team.name}, team.name) From f0fad8e95c005474f8a9f248d2140ed1e60ea3be Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 20 Aug 2021 16:03:59 +0530 Subject: [PATCH 035/223] avoid unnecessary use of scrollbars on sidebar apps (settings, admin, support, ...) --- customize.dist/src/less2/include/sidebar-layout.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/customize.dist/src/less2/include/sidebar-layout.less b/customize.dist/src/less2/include/sidebar-layout.less index ef93eae4a..1b0c75932 100644 --- a/customize.dist/src/less2/include/sidebar-layout.less +++ b/customize.dist/src/less2/include/sidebar-layout.less @@ -69,7 +69,7 @@ background: @cp_sidebar-right-bg; color: @cp_sidebar-right-fg; overflow: auto; - padding-bottom: 200px; + //padding-bottom: 200px; // XXX what was the intent behind this? // Following rules are only in settings .cp-sidebarlayout-element { From 925872679a55913db286ea9afa6b1a70ba887948 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 20 Aug 2021 13:22:06 +0200 Subject: [PATCH 036/223] New page when form responses have been submitted --- www/form/app-form.less | 16 ++++ www/form/inner.js | 162 ++++++++++++++++++++++++++++++++++------- www/form/main.js | 1 + 3 files changed, 154 insertions(+), 25 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index a0d0bdd65..ef6d9b839 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -127,6 +127,10 @@ & > div:not(:last-child) { margin-bottom: 20px; } + .cp-forms-results-participant { + display: flex; + flex-flow: column; + } } div.cp-form-filler-container { width: 300px; @@ -313,6 +317,12 @@ margin-bottom: 20px; } + .cp-form-disabled { + .cp-form-poll-choice, .cp-form-type-sort { + cursor: not-allowed !important; + } + } + .cp-form-preview { color: @cp_sidebar-hint; margin-bottom: 10px; @@ -502,6 +512,12 @@ } } } + div.cp-form-creator-answered { + display: flex; + align-items: center; + justify-content: center; + flex: 1; + } div.cp-form-creator-results { display: flex; flex-flow: column; diff --git a/www/form/inner.js b/www/form/inner.js index 2f717cb05..f1cb5adfd 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -68,6 +68,7 @@ define([ ) { var APP = window.APP = { + blocks: {} }; var is24h = UIElements.is24h(); @@ -1009,6 +1010,10 @@ define([ return $tag.val(); }, setValue: function (val) { $tag.val(val); }, + setEditable: function (state) { + if (state) { $tag.removeAttr('disabled'); } + else { $tag.attr('disabled', 'disabled'); } + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editTextOptions(v, setCursorGetter, cb, tmp); @@ -1091,6 +1096,10 @@ define([ $text.val(val); updateChar(); }, + setEditable: function (state) { + if (state) { $(tag).removeAttr('disabled'); } + else { $(tag).attr('disabled', 'disabled'); } + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editTextOptions(v, setCursorGetter, cb, tmp); @@ -1151,6 +1160,10 @@ define([ return res; }, reset: function () { $(tag).find('input').removeAttr('checked'); }, + setEditable: function (state) { + if (state) { $(tag).find('input').removeAttr('disabled'); } + else { $(tag).find('input').attr('disabled', 'disabled'); } + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editOptions(v, setCursorGetter, cb, tmp); @@ -1222,7 +1235,8 @@ define([ var tag = h('div.radio-group.cp-form-type-multiradio', lines); var cursorGetter; var setCursorGetter = function (f) { cursorGetter = f; }; - $(tag).find('input[type="radio"]').on('change', function () { + var $tag = $(tag); + $tag.find('input[type="radio"]').on('change', function () { evOnChange.fire(); }); return { @@ -1242,6 +1256,10 @@ define([ return res; }, reset: function () { $(tag).find('input').removeAttr('checked'); }, + setEditable: function (state) { + if (state) { $tag.find('input').removeAttr('disabled'); } + else { $tag.find('input').attr('disabled', 'disabled'); } + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editOptions(v, setCursorGetter, cb, tmp); @@ -1350,13 +1368,16 @@ define([ h('div.radio-group.cp-form-type-checkbox', els) ]); var $tag = $(tag); - $tag.find('input').on('change', function () { + var checkDisabled = function () { var selected = $tag.find('input:checked').length; if (selected >= opts.max) { $tag.find('input:not(:checked)').attr('disabled', 'disabled'); } else { $tag.find('input').removeAttr('disabled'); } + }; + $tag.find('input').on('change', function () { + checkDisabled(); evOnChange.fire(); }); var cursorGetter; @@ -1374,6 +1395,10 @@ define([ return res; }, reset: function () { $(tag).find('input').removeAttr('checked'); }, + setEditable: function (state) { + if (state) { checkDisabled(); } + else { $tag.find('input').attr('disabled', 'disabled'); } + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editOptions(v, setCursorGetter, cb, tmp); @@ -1388,10 +1413,7 @@ define([ $el.prop('checked', true); } }); - var selected = $tag.find('input:checked').length; - if (selected >= opts.max) { - $tag.find('input:not(:checked)').attr('disabled', 'disabled'); - } + checkDisabled(); } }; @@ -1448,14 +1470,18 @@ define([ if (!opts.max) { opts.max = TYPES.multicheck.defaultOpts.max; } + var checkDisabled = function (l) { + var selected = $(l).find('input:checked').length; + if (selected >= opts.max) { + $(l).find('input:not(:checked)').attr('disabled', 'disabled'); + } else { + $(l).find('input').removeAttr('disabled'); + } + }; + lines.forEach(function (l) { $(l).find('input').on('change', function () { - var selected = $(l).find('input:checked').length; - if (selected >= opts.max) { - $(l).find('input:not(:checked)').attr('disabled', 'disabled'); - } else { - $(l).find('input').removeAttr('disabled'); - } + checkDisabled(l); evOnChange.fire(); }); }); @@ -1484,6 +1510,10 @@ define([ return res; }, reset: function () { $(tag).find('input').removeAttr('checked'); }, + setEditable: function (state) { + if (state) { lines.forEach(checkDisabled); } + else { $(tag).find('input').attr('disabled', 'disabled'); } + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editOptions(v, setCursorGetter, cb, tmp); @@ -1498,6 +1528,7 @@ define([ $(el).prop('checked', true); }); }); + lines.forEach(checkDisabled); } }; @@ -1633,10 +1664,6 @@ define([ } } }); - - $(tag).find('input[type="radio"]').on('change', function () { - evOnChange.fire(); - }); return { tag: tag, getValue: function () { @@ -1653,6 +1680,10 @@ define([ sortable.sort(toSort); reorder(true); }, + setEditable: function (state) { + sortable.options.disabled = !state; + $(tag).toggleClass('cp-form-disabled', !state); + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editOptions(v, setCursorGetter, cb, tmp); @@ -1704,6 +1735,7 @@ define([ var lines = makePollTable(answers, opts, false); + var disabled = false; // Add form var addLine = opts.values.map(function (data) { var cell = h('div.cp-poll-cell.cp-form-poll-choice', [ @@ -1716,6 +1748,7 @@ define([ var val = 0; $c.attr('data-value', val); $c.click(function () { + if (disabled) { return; } val = (val+1)%3; $c.attr('data-value', val); evOnChange.fire(); @@ -1768,6 +1801,10 @@ define([ reset: function () { $tag.find('.cp-form-poll-choice').attr('data-value', 0); }, + setEditable: function (state) { + disabled = !state; + $tag.toggleClass('cp-form-disabled', disabled); + }, edit: function (cb, tmp) { var v = Util.clone(opts); return editOptions(v, setCursorGetter, cb, tmp); @@ -2092,6 +2129,43 @@ define([ framework._.toolbar.$bottomL.append($res); }; + Messages.form_alreadyAnswered = "You've responded to this form on {0}"; // XXX + Messages.form_editAnswer = "Edit my responses"; // XXX + Messages.form_viewAnswer = "View my responses"; // XXX + var showAnsweredPage = function (framework, content, answers) { + var $formContainer = $('div.cp-form-creator-content').hide(); + var $container = $('div.cp-form-creator-answered').empty().css('display', ''); + + var viewOnly = content.answers.cantEdit; + var action = h('button.btn.btn-primary', [ + viewOnly ? h('i.fa.fa-bar-chart') : h('i.fa.fa-pencil'), + h('span', viewOnly ? Messages.form_viewAnswer : Messages.form_editAnswer) + ]); + + $(action).click(function () { + $formContainer.css('display', ''); + $container.hide(); + if (viewOnly) { + $formContainer.find('.cp-form-send-container .cp-open').hide(); + Object.keys(APP.blocks).forEach(function (uid) { + var b = APP.blocks[uid]; + if (!b.setEditable) { return; } + b.setEditable(false); + }); + } + }); + + if (answers._time) { APP.lastAnswerTime = answers._time; } + + var title = framework._.title.title || framework._.title.defaultTitle; + $container.append(h('div.cp-form-submit-success', [ + h('h3.cp-form-view-title', title), + h('div.alert.alert-info', Messages._getKey('form_alreadyAnswered', [ + new Date(APP.lastAnswerTime).toLocaleString()])), + action + ])); + }; + var getFormResults = function () { if (!Array.isArray(APP.formBlocks)) { return; } var results = {}; @@ -2180,8 +2254,9 @@ define([ addResultsButton(framework, content); } $send.removeAttr('disabled'); - UI.alert(Messages.form_sent); + //UI.alert(Messages.form_sent); // XXX not needed anymore? $send.text(Messages.form_update); + showAnsweredPage(framework, content, { '_time': +new Date() }); }); }); @@ -2378,7 +2453,7 @@ define([ name = user.name; } - var data = model.get(block.opts, _answers, name, evOnChange); + var data = APP.blocks[uid] = model.get(block.opts, _answers, name, evOnChange); if (!data) { return; } data.uid = uid; if (answers && answers[uid] && data.setValue) { data.setValue(answers[uid]); } @@ -2502,7 +2577,7 @@ define([ $(editButtons).show(); UI.log(Messages.saved); _answers = getBlockAnswers(APP.answers, uid); - data = model.get(newOpts, _answers, null, evOnChange); + data = APP.blocks[uid] = model.get(newOpts, _answers, null, evOnChange); if (!data) { data = {}; } $oldTag.before(data.tag).remove(); }); @@ -2565,7 +2640,7 @@ define([ framework.localChange(); var $oldTag = $(data.tag); framework._.cpNfInner.chainpad.onSettle(function () { - data = model.get(block.opts, _answers, null, evOnChange); + data = APP.blocks[uid] = model.get(block.opts, _answers, null, evOnChange); $oldTag.before(data.tag).remove(); }); }); @@ -2676,12 +2751,11 @@ define([ } // If the form is already submitted, show an info message - Messages.form_alreadyAnswered = "You've submitted answers to this form on {0}"; // XXX if (answers) { + showAnsweredPage(framework, content, answers); $container.prepend(h('div.alert.alert-info', Messages._getKey('form_alreadyAnswered', [ - new Date(answers._time).toLocaleString()]))); - // XXX make the page read-only? + new Date(answers._time || APP.lastAnswerTime).toLocaleString()]))); } // In view mode, add "Submit" and "reset" buttons @@ -2705,8 +2779,8 @@ define([ if (!answers) { $container.find('.cp-reset-button').attr('disabled', 'disabled'); - } - }; + } +}; var getTempFields = function () { if (!Array.isArray(APP.formBlocks)) { return; } @@ -2916,6 +2990,38 @@ define([ }; refreshPrivacy(); + // Allow responses edition + Messages.form_editable = "Allow users to edit their responses"; // XXX + var editableContainer = h('div.cp-form-editable-container'); + var $editable = $(editableContainer); + var refreshEditable = function () { + $editable.empty(); + var editable = !content.answers.cantEdit; + var radioOn = UI.createRadio('cp-form-editable', 'cp-form-editable-on', + Messages.form_anonymous_on, Boolean(editable), { + input: { value: 1 }, + mark: { tabindex:1 } + }); + var radioOff = UI.createRadio('cp-form-editable', 'cp-form-editable-off', + Messages.form_anonymous_off, !editable, { + input: { value: 0 }, + mark: { tabindex:1 } + }); + var radioContainer = h('div.cp-form-editable-radio', [radioOn, radioOff]); + $(radioContainer).find('input[type="radio"]').on('change', function() { + var val = $('input:radio[name="cp-form-editable"]:checked').val(); + val = Number(val) || 0; + content.answers.cantEdit = !val; + framework.localChange(); + framework._.cpNfInner.chainpad.onSettle(function () { + UI.log(Messages.saved); + }); + }); + $editable.append(h('div.cp-form-status', Messages.form_editable)); + $editable.append(h('div.cp-form-actions', radioContainer)); + }; + refreshEditable(); + // End date / Closed state var endDateContainer = h('div.cp-form-status-container'); var $endDate = $(endDateContainer); @@ -2979,6 +3085,7 @@ define([ evOnChange.reg(refreshPublic); evOnChange.reg(refreshPrivacy); + evOnChange.reg(refreshEditable); evOnChange.reg(refreshEndDate); //evOnChange.reg(refreshResponse); @@ -2986,6 +3093,7 @@ define([ preview, endDateContainer, privacyContainer, + editableContainer, resultsType, responseMsg ]; @@ -3027,10 +3135,14 @@ define([ var contentContainer = h('div.cp-form-creator-content'); var resultsContainer = h('div.cp-form-creator-results'); + var answeredContainer = h('div.cp-form-creator-answered', { + style: 'display: none;' + }); var div = h('div.cp-form-creator-container', [ controlContainer, contentContainer, resultsContainer, + answeredContainer, fillerContainer ]); return div; diff --git a/www/form/main.js b/www/form/main.js index 4c0831fe9..58030f35c 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -266,6 +266,7 @@ define([ if (obj && obj.error) { return void cb(obj); } var messages = obj.messages; if (!messages.length) { return void cb(); } + if (obj.lastKnownHash !== answer.hash) { return void cb(); } var res = Utils.Crypto.Mailbox.openOwnSecretLetter(messages[0].msg, { validateKey: data.validateKey, ephemeral_private: Nacl.util.decodeBase64(answer.curvePrivate), From 0fec83b051f3c44e58b0de205b0d927261b1fe8f Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 20 Aug 2021 17:47:54 +0530 Subject: [PATCH 037/223] factor debugging data generation from support ticket transmission and fix two incorrectly set width/height properties which overwrote 'appVersion' --- www/support/ui.js | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/www/support/ui.js b/www/support/ui.js index fac6ef438..2f97ec412 100644 --- a/www/support/ui.js +++ b/www/support/ui.js @@ -10,14 +10,12 @@ define([ '/customize/messages.js', ], function ($, ApiConfig, h, UI, Hash, Util, Clipboard, UIElements, Messages) { - var send = function (ctx, id, type, data, dest) { + var getDebuggingData = function (ctx, data) { var common = ctx.common; - var supportKey = ApiConfig.supportMailbox; - var supportChannel = Hash.getChannelIdFromKey(supportKey); var metadataMgr = common.getMetadataMgr(); - var user = metadataMgr.getUserData(); var privateData = metadataMgr.getPrivateData(); - + var user = metadataMgr.getUserData(); + var teams = privateData.teams || {}; data = data || {}; data.sender = { @@ -34,16 +32,12 @@ define([ data.sender.quota = ctx.pinUsage; } - data.id = id; - data.time = +new Date(); - - var teams = privateData.teams || {}; if (!ctx.isAdmin) { data.sender.userAgent = Util.find(window, ['navigator', 'userAgent']); data.sender.vendor = Util.find(window, ['navigator', 'vendor']); data.sender.appVersion = Util.find(window, ['navigator', 'appVersion']); - data.sender.appVersion = Util.find(window, ['screen', 'width']); - data.sender.appVersion = Util.find(window, ['screen', 'height']); + data.sender.screenWidth = Util.find(window, ['screen', 'width']); + data.sender.screenHeight = Util.find(window, ['screen', 'height']); data.sender.blockLocation = privateData.blockLocation || ''; data.sender.teams = Object.keys(teams).map(function (key) { var team = teams[key]; @@ -57,7 +51,25 @@ define([ } return ret; }).filter(Boolean); + } + return data; + }; + + var send = function (ctx, id, type, data, dest) { + var common = ctx.common; + var supportKey = ApiConfig.supportMailbox; + var supportChannel = Hash.getChannelIdFromKey(supportKey); + var metadataMgr = common.getMetadataMgr(); + var user = metadataMgr.getUserData(); + var privateData = metadataMgr.getPrivateData(); + + data = getDebuggingData(ctx, data); + + data.id = id; + data.time = +new Date(); + + if (!ctx.isAdmin) { // "dest" is the recipient that is not the admin support mailbox. // In the support page, make sure dest is always ourselves. dest.channel = privateData.support; @@ -474,6 +486,10 @@ define([ ui.makeCloseMessage = function (content, hash) { return makeCloseMessage(ctx, content, hash); }; + ui.getDebuggingData = function (data) { + return getDebuggingData(ctx, data); + }; + return ui; }; From 2df0f6df9b111736b6264747509272d9b69bb3ae Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 20 Aug 2021 14:45:05 +0200 Subject: [PATCH 038/223] Disable answer edition in forms --- www/form/inner.js | 7 +++++-- www/form/main.js | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index f1cb5adfd..1973619db 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -3244,7 +3244,8 @@ define([ channel: content.answers.channel, validateKey: content.answers.validateKey, publicKey: content.answers.publicKey, - privateKey: key + privateKey: key, + cantEdit: content.answers.cantEdit }, function (err, obj) { var answers = obj && obj.results; if (answers) { APP.answers = answers; } @@ -3263,7 +3264,8 @@ define([ sframeChan.query("Q_FORM_FETCH_ANSWERS", { channel: content.answers.channel, validateKey: content.answers.validateKey, - publicKey: content.answers.publicKey + publicKey: content.answers.publicKey, + cantEdit: content.answers.cantEdit }, function (err, obj) { var answers = obj && obj.results; if (answers) { APP.answers = answers; } @@ -3289,6 +3291,7 @@ define([ validateKey: content.answers.validateKey, publicKey: content.answers.publicKey, privateKey: content.answers.privateKey, + cantEdit: content.answers.cantEdit }, function (err, obj) { var answers = obj && obj.results; if (answers) { APP.answers = answers; } diff --git a/www/form/main.js b/www/form/main.js index 58030f35c..7d81b1268 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -221,6 +221,8 @@ define([ delete results[parsed._proof.key]; } } + // XXX If "allow edition" is disabled, don't override here? + // if (data.cantEdit && results[senderCurve]) { return; } results[senderCurve] = { msg: parsed, hash: hash, From 5d04cd0f4ff461ae9fbd165c4728786b463ae3d8 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 20 Aug 2021 14:56:28 +0200 Subject: [PATCH 039/223] Forms code improvements --- www/form/inner.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 1973619db..6fbd495b1 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -2147,11 +2147,12 @@ define([ $container.hide(); if (viewOnly) { $formContainer.find('.cp-form-send-container .cp-open').hide(); - Object.keys(APP.blocks).forEach(function (uid) { - var b = APP.blocks[uid]; - if (!b.setEditable) { return; } - b.setEditable(false); - }); + if (Array.isArray(APP.formBlocks)) { + APP.formBlocks.forEach(function (b) { + if (!b.setEditable) { return; } + b.setEditable(false); + }); + } } }); @@ -2453,7 +2454,7 @@ define([ name = user.name; } - var data = APP.blocks[uid] = model.get(block.opts, _answers, name, evOnChange); + 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]); } @@ -2577,7 +2578,7 @@ define([ $(editButtons).show(); UI.log(Messages.saved); _answers = getBlockAnswers(APP.answers, uid); - data = APP.blocks[uid] = model.get(newOpts, _answers, null, evOnChange); + data = model.get(newOpts, _answers, null, evOnChange); if (!data) { data = {}; } $oldTag.before(data.tag).remove(); }); @@ -2640,7 +2641,7 @@ define([ framework.localChange(); var $oldTag = $(data.tag); framework._.cpNfInner.chainpad.onSettle(function () { - data = APP.blocks[uid] = model.get(block.opts, _answers, null, evOnChange); + data = model.get(block.opts, _answers, null, evOnChange); $oldTag.before(data.tag).remove(); }); }); From 583060d130613d8e3c5f613bf1864a8d933dbbd2 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 20 Aug 2021 17:41:38 +0200 Subject: [PATCH 040/223] Required questions --- www/form/app-form.less | 19 ++++- www/form/inner.js | 154 +++++++++++++++++++++++++++++++++++------ 2 files changed, 149 insertions(+), 24 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index ef6d9b839..441982b5f 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -356,10 +356,25 @@ .cp-form-block-question { margin-bottom: 5px; + display: flex; .cp-form-block-question-number { font-weight: bold; margin-right: 10px; } + .cp-form-block-question-text { + flex: 1; + } + .cp-form-required-tag { + background: fade(@cryptpad_text_col, 15%); + padding: 5px; + margin-top: -10px; + margin-right: -10px; + &.cp-is-empty { + padding: 3px; + border: 2px solid @cryptpad_color_red; + color: @cp_form-invalid; + } + } } .cp-form-block-content { overflow-x: auto; @@ -548,8 +563,8 @@ .cp-form-creator-results-content { padding-bottom: 100px; .cp-form-block { - background: @cp_form-bg1; - padding: 10px; + background: @cp_form-bg1; + padding: 10px; } } .cp-form-block-question { diff --git a/www/form/inner.js b/www/form/inner.js index 6fbd495b1..f50d4266f 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1004,6 +1004,7 @@ define([ var setCursorGetter = function (f) { cursorGetter = f; }; return { tag: tag, + isEmpty: function () { return !$tag.val().trim(); }, getValue: function () { //var invalid = $tag.is(':invalid'); //if (invalid) { return; } @@ -1091,6 +1092,7 @@ define([ var setCursorGetter = function (f) { cursorGetter = f; }; return { tag: tag, + isEmpty: function () { return !$text.val().trim(); }, getValue: function () { return $text.val().slice(0, opts.maxLength); }, setValue: function (val) { $text.val(val); @@ -1148,6 +1150,7 @@ define([ }); return { tag: tag, + isEmpty: function () { return !this.getValue(); }, getValue: function () { var res; els.some(function (el) { @@ -1241,6 +1244,12 @@ define([ }); return { tag: tag, + isEmpty: function () { + var v = this.getValue(); + return !Object.keys(v).length || Object.keys(v).some(function (uid) { + return !v[uid]; + }); + }, getValue: function () { var res = {}; var l = lines.slice(1); @@ -1384,6 +1393,10 @@ define([ var setCursorGetter = function (f) { cursorGetter = f; }; return { tag: tag, + isEmpty: function () { + var v = this.getValue(); + return !v.length; + }, getValue: function () { var res = []; els.forEach(function (el) { @@ -1495,6 +1508,12 @@ define([ var setCursorGetter = function (f) { cursorGetter = f; }; return { tag: tag, + isEmpty: function () { + var v = this.getValue(); + return Object.keys(v).some(function (uid) { + return !v[uid].length; + }); + }, getValue: function () { var res = {}; var l = lines.slice(1); @@ -1659,13 +1678,14 @@ define([ forceFallback: true, store: { set: function () { - evOnChange.fire(); reorder(); + evOnChange.fire(); } } }); return { tag: tag, + isEmpty: function () { return !this.getValue(); }, getValue: function () { if (!sorted) { return; } return sortable.toArray().map(function (id) { @@ -2215,6 +2235,7 @@ define([ if (typeof(data.reset) === "function") { data.reset(); } }); $(reset).attr('disabled', 'disabled'); + evOnChange.fire(); }); var $send = $(send).click(function () { $send.attr('disabled', 'disabled'); @@ -2266,6 +2287,9 @@ define([ reset = undefined; } + Messages.form_requiredWarning = "These questions need an answer:"; // XXX + var errors = h('div.cp-form-invalid-warning'); + var $errors = $(errors); var invalid = h('div.cp-form-invalid-warning'); var $invalid = $(invalid); if (evOnChange) { @@ -2274,7 +2298,28 @@ define([ priv = metadataMgr.getPrivateData(); origin = priv.origin; } - evOnChange.reg(function () { + + var gotoQuestion = function (el) { + var $el = $(el).closest('.cp-form-block'); + var number = $el.find('.cp-form-block-question-number').text(); + var a = h('a', { + href: origin + '#' + Messages._getKey('form_invalidQuestion', [number]) + }, Messages._getKey('form_invalidQuestion', [number])); + $(a).click(function (e) { + e.preventDefault(); + if (!$el.is(':visible')) { + var pages = $el.closest('.cp-form-page').index(); + if (APP.refreshPage) { APP.refreshPage(pages + 1); } + } + $el[0].scrollIntoView(); + }); + return h('li', a); + }; + + if (APP.checkInvalidEvt) { evOnChange.unreg(APP.checkInvalidEvt); } + if (APP.checkErrorEvt) { evOnChange.unreg(APP.checkErrorEvt); } + // Check invalid inputs + APP.checkInvalidEvt = function () { var $container = $('div.cp-form-creator-content'); var $inputs = $container.find('input:invalid'); if (!$inputs.length) { @@ -2284,21 +2329,7 @@ define([ $send.text(update ? Messages.form_updateWarning : Messages.form_submitWarning); var lis = []; $inputs.each(function (i, el) { - var $el = $(el).closest('.cp-form-block'); - var number = $el.find('.cp-form-block-question-number').text(); - var a = h('a', { - href: origin + '#' + Messages._getKey('form_invalidQuestion', [number]) - }, Messages._getKey('form_invalidQuestion', [number])); - $(a).click(function (e) { - e.preventDefault(); - if (!$el.is(':visible')) { - var pages = $el.closest('.cp-form-page').index(); - if (APP.refreshPage) { APP.refreshPage(pages + 1); } - } - $el[0].scrollIntoView(); - }); - var li = h('li', a); - lis.push(li); + lis.push(gotoQuestion(el)); }); var list = h('ul', lis); var content = [ @@ -2306,12 +2337,47 @@ define([ list ]; $invalid.empty().append(content); - }); + }; + // Check empty required questions + APP.checkErrorEvt = function () { + if (!Array.isArray(APP.formBlocks)) { return; } + var form = content.form; + var errorBlocks = APP.formBlocks.filter(function (data) { + var uid = data.uid; + var block = form[uid]; + if (!data.isEmpty) { return; } + if (!block) { return; } + if (!block.opts || !block.opts.required) { return; } + console.error(data.getValue()); + var isEmpty = data.isEmpty(); + var $el = $(data.tag).closest('.cp-form-block'); + $el.find('.cp-form-required-tag').toggleClass('cp-is-empty', isEmpty); + return isEmpty; + }); + if (!errorBlocks.length) { + $send.removeAttr('disabled'); + return void $errors.empty(); + } + $send.attr('disabled', 'disabled'); + var lis = []; + errorBlocks.forEach(function (data) { + lis.push(gotoQuestion(data.tag)); + }); + var list = h('ul', lis); + var divContent = [ + h('span', Messages.form_requiredWarning), + list + ]; + $errors.empty().append(divContent); + }; + evOnChange.reg(APP.checkInvalidEvt); + evOnChange.reg(APP.checkErrorEvt); evOnChange.fire(true); } return h('div.cp-form-send-container', [ invalid, + errors, cbox ? h('div.cp-form-anon-answer', [ cbox, anonName @@ -2465,10 +2531,17 @@ define([ } + Messages.form_required = "Required"; // XXX + var requiredTag; + if (block.opts && block.opts.required) { + requiredTag = h('span.cp-form-required-tag', Messages.form_required); + } + var dragHandle; var q = h('div.cp-form-block-question', [ h('span.cp-form-block-question-number', (n++)+'.'), - h('span', block.q || Messages.form_default) + h('span.cp-form-block-question-text', block.q || Messages.form_default), + requiredTag ]); // Static blocks don't have questions ("q" is not used) so we can decrement n if (isStatic) { n--; } @@ -2480,6 +2553,38 @@ define([ Messages.form_preview = "Preview:"; // XXX var previewDiv = h('div.cp-form-preview', Messages.form_preview); + Messages.form_required_on = "Required answer"; + Messages.form_required_off = "Optional answer"; + // Required radio displayed only for types that have an "isEmpty" function + var requiredDiv; + if (APP.isEditor && !isStatic && data.isEmpty) { + if (!block.opts) { block.opts = {}; } + var isRequired = Boolean(block.opts.required); + var radioOn = UI.createRadio('cp-form-required-'+uid, 'cp-form-required-on', + Messages.form_required_on, isRequired, { + input: { value: 1 }, + mark: { tabindex:1 } + }); + var radioOff = UI.createRadio('cp-form-required-'+uid, 'cp-form-required-off', + Messages.form_required_off, !isRequired, { + input: { value: 0 }, + mark: { tabindex:1 } + }); + var radioContainer = h('div.cp-form-required-radio', [radioOn, radioOff]); + requiredDiv = h('div.cp-form-required', [ + radioContainer + ]); + $(radioContainer).find('input[type="radio"]').on('change', function() { + var val = $('input:radio[name="cp-form-required-'+uid+'"]:checked').val(); + val = Number(val) || 0; + block.opts.required = Boolean(val); + framework.localChange(); + framework._.cpNfInner.chainpad.onSettle(function () { + UI.log(Messages.saved); + }); + }); + } + if (editable) { // Drag handle dragHandle = h('span.cp-form-block-drag-handle', [ @@ -2568,6 +2673,7 @@ define([ $(editButtons).show(); $(data.tag).show(); $(previewDiv).show(); + $(requiredDiv).show(); return; } $(editContainer).empty(); @@ -2576,6 +2682,8 @@ define([ var $oldTag = $(data.tag); framework._.cpNfInner.chainpad.onSettle(function () { $(editButtons).show(); + $(previewDiv).show(); + $(requiredDiv).show(); UI.log(Messages.saved); _answers = getBlockAnswers(APP.answers, uid); data = model.get(newOpts, _answers, null, evOnChange); @@ -2585,6 +2693,7 @@ define([ }; var onEdit = function (tmp) { data.editing = true; + $(requiredDiv).hide(); $(previewDiv).hide(); $(data.tag).hide(); $(editContainer).append(data.edit(onSave, tmp, framework)); @@ -2660,7 +2769,8 @@ define([ APP.isEditor ? dragHandle : undefined, isStatic ? undefined : q, h('div.cp-form-block-content', [ - isStatic || !APP.isEditor ? undefined : previewDiv, + APP.isEditor && !isStatic ? requiredDiv : undefined, + APP.isEditor && !isStatic ? previewDiv : undefined, data.tag, editButtons ]), @@ -2780,8 +2890,8 @@ define([ if (!answers) { $container.find('.cp-reset-button').attr('disabled', 'disabled'); - } -}; + } + }; var getTempFields = function () { if (!Array.isArray(APP.formBlocks)) { return; } From 0c4405d43f43e9f831396a60c8c8472f0154b024 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 23 Aug 2021 11:55:34 +0200 Subject: [PATCH 041/223] Fix textarea issues in forms --- www/form/inner.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index f50d4266f..19e42e131 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1059,7 +1059,7 @@ define([ maxLength: 1000 }, get: function (opts, a, n, evOnChange) { - if (!opts) { opts = TYPES.textarea.defaultOpts; } + if (!opts || typeof(opts.maxLength) === "undefined") { opts = TYPES.textarea.defaultOpts; } var text = h('textarea', {maxlength: opts.maxLength}); var $text = $(text); var charCount = h('div.cp-form-type-textarea-charcount'); @@ -1099,8 +1099,8 @@ define([ updateChar(); }, setEditable: function (state) { - if (state) { $(tag).removeAttr('disabled'); } - else { $(tag).attr('disabled', 'disabled'); } + if (state) { $(tag).find('textarea').removeAttr('disabled'); } + else { $(tag).find('textarea').attr('disabled', 'disabled'); } }, edit: function (cb, tmp) { var v = Util.clone(opts); @@ -2348,7 +2348,6 @@ define([ if (!data.isEmpty) { return; } if (!block) { return; } if (!block.opts || !block.opts.required) { return; } - console.error(data.getValue()); var isEmpty = data.isEmpty(); var $el = $(data.tag).closest('.cp-form-block'); $el.find('.cp-form-required-tag').toggleClass('cp-is-empty', isEmpty); From aef1b22291d36510ec31c755d8565671442be101 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 23 Aug 2021 16:32:25 +0530 Subject: [PATCH 042/223] console.error instead of throwing when unregistering handlers --- www/common/common-util.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/www/common/common-util.js b/www/common/common-util.js index 3d2a8d0a4..fbed064dc 100644 --- a/www/common/common-util.js +++ b/www/common/common-util.js @@ -75,7 +75,9 @@ handlers.push(cb); }, unreg: function (cb) { - if (handlers.indexOf(cb) === -1) { throw new Error("Not registered"); } + if (handlers.indexOf(cb) === -1) { + return void console.error("event handler was already unregistered"); + } handlers.splice(handlers.indexOf(cb), 1); }, fire: function () { From 46e545a976ef9312f9aa0018aaca7dca614cedf1 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 23 Aug 2021 16:34:51 +0530 Subject: [PATCH 043/223] lint compliance --- www/common/inner/common-mediatag.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index ece9e3024..3d4e82caf 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -100,7 +100,7 @@ define([ var displayDefault = function () { var animal_avatar; if (uid && animal_avatars[uid]) { - animal_avatar = animal_avatars[uid] + animal_avatar = animal_avatars[uid]; } var animal = false; From 9af99c1b8a057dac1dc947b4b7f80eb2c2069786 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 23 Aug 2021 14:32:57 +0200 Subject: [PATCH 044/223] Fix tabindex issues in forms --- www/form/inner.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 19e42e131..c797786fa 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1138,7 +1138,7 @@ define([ 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 } }); + data, false, {}); $(radio).find('input').data('val', data); return radio; }); @@ -1223,7 +1223,7 @@ define([ 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 } }); + '', false, {}); $(radio).find('input').data('uid', name); $(radio).find('input').data('val', data); return radio; @@ -1367,7 +1367,7 @@ define([ 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 } }); + data, false, {}); $(cbox).find('input').data('val', data); return cbox; }); @@ -1472,7 +1472,7 @@ define([ var item = itemData.v; var els = opts.values.map(function (data, i) { var cbox = UI.createCheckbox('cp-form-'+name+'-'+i, - '', false, { mark: { tabindex:1 } }); + '', false, {}); $(cbox).find('input').data('uid', name); $(cbox).find('input').data('val', data); return cbox; @@ -2206,7 +2206,7 @@ define([ var cbox; var anonName, $anonName; cbox = UI.createCheckbox('cp-form-anonymous', - Messages.form_anonymousBox, true, { mark: { tabindex:1 } }); + Messages.form_anonymousBox, true, {}); var $anonBox = $(cbox).find('input'); if (loggedIn) { if (!content.answers.anonymous || APP.cantAnon) { @@ -2562,12 +2562,10 @@ define([ var radioOn = UI.createRadio('cp-form-required-'+uid, 'cp-form-required-on', Messages.form_required_on, isRequired, { input: { value: 1 }, - mark: { tabindex:1 } }); var radioOff = UI.createRadio('cp-form-required-'+uid, 'cp-form-required-off', Messages.form_required_off, !isRequired, { input: { value: 0 }, - mark: { tabindex:1 } }); var radioContainer = h('div.cp-form-required-radio', [radioOn, radioOff]); requiredDiv = h('div.cp-form-required', [ @@ -2722,7 +2720,7 @@ define([ var text = Messages['form_type_'+data]; if (!text) { return; } var radio = UI.createRadio(name, 'cp-form-changetype-'+i, - text, data===type, { mark: { tabindex:1 } }); + text, data===type, {}); $(radio).find('input').data('val', data); return radio; }); @@ -3078,12 +3076,10 @@ define([ 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() { @@ -3110,12 +3106,10 @@ define([ var radioOn = UI.createRadio('cp-form-editable', 'cp-form-editable-on', Messages.form_anonymous_on, Boolean(editable), { input: { value: 1 }, - mark: { tabindex:1 } }); var radioOff = UI.createRadio('cp-form-editable', 'cp-form-editable-off', Messages.form_anonymous_off, !editable, { input: { value: 0 }, - mark: { tabindex:1 } }); var radioContainer = h('div.cp-form-editable-radio', [radioOn, radioOff]); $(radioContainer).find('input[type="radio"]').on('change', function() { From 4fe19c1ea4f67c41ab4d5fc34df4ac0da47b6dec Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 23 Aug 2021 18:16:32 +0530 Subject: [PATCH 045/223] remove unnecessary example cards from default kanban board --- www/kanban/inner.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/kanban/inner.js b/www/kanban/inner.js index 38e933cf1..585c7c285 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -563,12 +563,12 @@ define([ "12": { "id": 12, "title": Messages.kanban_working, - "item": [3, 4] + "item": [], }, "13": { "id": 13, "title": Messages.kanban_done, - "item": [5, 6] + "item": [], } }, items: items From 9f52ec8dc713043cac8a41edc34acbeadb060658 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 23 Aug 2021 18:19:35 +0530 Subject: [PATCH 046/223] add new translation check to find duplicates and move all translation scripts into a dedicated folder --- package.json | 4 +- .../find-duplicate-translations.js | 55 +++++++++++++++++++ .../{ => translations}/lint-translations.js | 0 .../{ => translations}/unused-translations.js | 0 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 scripts/translations/find-duplicate-translations.js rename scripts/{ => translations}/lint-translations.js (100%) rename scripts/{ => translations}/unused-translations.js (100%) diff --git a/package.json b/package.json index a844167f5..2d1c3f626 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,8 @@ "lint:js": "jshint --config .jshintrc --exclude-path .jshintignore .", "lint:server": "jshint --config .jshintrc lib", "lint:less": "./node_modules/lesshint/bin/lesshint -c ./.lesshintrc ./customize.dist/src/less2/", - "lint:translations": "node ./scripts/lint-translations.js", - "unused-translations": "node ./scripts/unused-translations.js", + "lint:translations": "node ./scripts/translations/lint-translations.js", + "unused-translations": "node ./scripts/translations/unused-translations.js", "test": "node scripts/TestSelenium.js", "test-rpc": "cd scripts/tests && node test-rpc", "template": "cd customize.dist/src && for page in ../index.html ../privacy.html ../terms.html ../contact.html ../what-is-cryptpad.html ../features.html ../../www/login/index.html ../../www/register/index.html ../../www/user/index.html;do echo $page; cp template.html $page; done;", diff --git a/scripts/translations/find-duplicate-translations.js b/scripts/translations/find-duplicate-translations.js new file mode 100644 index 000000000..f8b25497c --- /dev/null +++ b/scripts/translations/find-duplicate-translations.js @@ -0,0 +1,55 @@ +var Util = require("../lib/common-util"); +var EN = Util.clone(require("../www/common/translations/messages.json")); +var FR = Util.clone(require("../www/common/translations/messages.fr.json")); +var DE = Util.clone(require("../www/common/translations/messages.de.json")); +var JP = Util.clone(require("../www/common/translations/messages.ja.json")); + +var keys = Object.keys(EN); + +var duplicates = {}; +var addIfAbsent = function (A, e) { + if (A.includes(e)) { return; } + A.push(e); +}; +var markDuplicate = function (value, key1, key2) { + //console.log("[%s] === [%s] (%s)", key1, key2, value); + if (!Array.isArray(duplicates[value])) { + duplicates[value] = []; + } + addIfAbsent(duplicates[value], key1); + addIfAbsent(duplicates[value], key2); +}; + +keys.forEach(function (key) { + var value = EN[key]; + + //var duplicates = []; + keys.forEach(function (key2) { + if (key === key2) { return; } + var value2 = EN[key2]; + if (value === value2) { + markDuplicate(value, key, key2); + } + }); +}); + +// indicate which strings are duplicated and could potentially be changed to use one key +Object.keys(duplicates).forEach(function (val) { + console.log('\"%s\" => %s', val, JSON.stringify(duplicates[val])); +}); + +// TODO iterate over all languages and + +// 1) check whether the same mapping exists across languages +// ie. English has "Open" (verb) and "Open" (adjective) +// while French has "Ouvrir" and "Ouvert(s)" +// such keys should not be simplified/deduplicated + + + +// find instances where +// one of the duplicated keys is not translated +// perhaps we could automatically use the translated one everywhere +// and improve the completeness of translations + + diff --git a/scripts/lint-translations.js b/scripts/translations/lint-translations.js similarity index 100% rename from scripts/lint-translations.js rename to scripts/translations/lint-translations.js diff --git a/scripts/unused-translations.js b/scripts/translations/unused-translations.js similarity index 100% rename from scripts/unused-translations.js rename to scripts/translations/unused-translations.js From ead5ad217c25e76b70c1c69fe60fbccf90d48a70 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 23 Aug 2021 14:53:24 +0200 Subject: [PATCH 047/223] Use placeholders when editing form questions --- www/form/inner.js | 52 +++++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index c797786fa..ed1d77cdd 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -189,7 +189,7 @@ define([ saveAndCancel ]; }; - var editOptions = function (v, setCursorGetter, cb, tmp) { + var editOptions = function (v, isDefaultOpts, setCursorGetter, cb, tmp) { var add = h('button.btn.btn-secondary', [ h('i.fa.fa-plus'), h('span', Messages.form_add_option) @@ -254,8 +254,16 @@ define([ // Show existing options var $add, $addItem; var addMultiple; - var getOption = function (val, isItem, uid) { + var getOption = function (val, placeholder, isItem, uid) { var input = h('input', {value:val}); + if (placeholder) { + input.placeholder = val; + input.value = ''; + $(input).change(function () { + input.placeholder = ''; + $(input).off(change); + }); + } if (uid) { $(input).data('uid', uid); } // If the input is a date, initialize flatpickr @@ -316,7 +324,7 @@ define([ }); return el; }; - var inputs = v.values.map(function (val) { return getOption(val, false); }); + var inputs = v.values.map(function (val) { return getOption(val, isDefaultOpts, false); }); inputs.push(add); var container = h('div.cp-form-edit-block', inputs); @@ -332,7 +340,7 @@ define([ var containerItems; if (v.items) { var inputsItems = v.items.map(function (itemData) { - return getOption(itemData.v, true, itemData.uid); + return getOption(itemData.v, isDefaultOpts, true, itemData.uid); }); inputsItems.push(addItem); containerItems = h('div.cp-form-edit-block', inputsItems); @@ -385,7 +393,7 @@ define([ }); $(addMultipleButton).click(function () { multiplePickr.selectedDates.some(function (date) { - $add.before(getOption(date, false)); + $add.before(getOption(date, false, false)); var l = $container.find('input').length; $(maxInput).attr('max', l); if (l >= MAX_OPTIONS) { @@ -440,7 +448,7 @@ define([ // "Add option" button handler $add = $(add).click(function () { var txt = v.type ? '' : Messages.form_newOption; - $add.before(getOption(txt, false)); + $add.before(getOption(txt, true, false)); var l = $container.find('input').length; $(maxInput).attr('max', l); if (l >= MAX_OPTIONS) { $add.hide(); } @@ -448,7 +456,7 @@ define([ // If multiline block, handle "Add item" button $addItem = $(addItem).click(function () { - $addItem.before(getOption(Messages.form_newItem, true, Util.uid())); + $addItem.before(getOption(Messages.form_newItem, true, true, Util.uid())); if ($(containerItems).find('input').length >= MAX_ITEMS) { $addItem.hide(); } }); if ($container.find('input').length >= MAX_OPTIONS) { $add.hide(); } @@ -460,12 +468,13 @@ define([ var active = document.activeElement; var cursor = {}; $container.find('input').each(function (i, el) { + var val = $(el).val() || el.placeholder || ''; if (el === active && !el._flatpickr) { - cursor.el= $(el).val(); + cursor.el = val; cursor.start = el.selectionStart; cursor.end = el.selectionEnd; } - values.push($(el).val()); + values.push(val); }); if (v.type === "day") { var dayPickr = $(calendarView).find('input')[0]._flatpickr; @@ -492,9 +501,10 @@ define([ cursor.start = el.selectionStart; cursor.end = el.selectionEnd; } + var val = $(el).val() || el.placeholder || ''; items.push({ uid: $(el).data('uid'), - v: $(el).val() + v: val }); }); _content.items = items; @@ -517,7 +527,7 @@ define([ }); } else { $container.find('input').each(function (i, el) { - var val = $(el).val().trim(); + var val = ($(el).val() || el.placeholder || '').trim(); if (v.type === "day" || v.type === "time") { var f = el._flatpickr; if (f && f.selectedDates && f.selectedDates.length) { @@ -538,7 +548,7 @@ define([ if (v.items) { var items = []; $(containerItems).find('input').each(function (i, el) { - var val = $(el).val().trim(); + var val = ($(el).val() || el.placeholder || '').trim(); var uid = $(el).data('uid'); if (!items.some(function (i) { return i.uid === uid; })) { items.push({ @@ -1133,6 +1143,7 @@ define([ }) }, get: function (opts, a, n, evOnChange) { + var isDefaultOpts = !opts; if (!opts) { opts = TYPES.radio.defaultOpts; } if (!Array.isArray(opts.values)) { return; } var name = Util.uid(); @@ -1169,7 +1180,7 @@ define([ }, edit: function (cb, tmp) { var v = Util.clone(opts); - return editOptions(v, setCursorGetter, cb, tmp); + return editOptions(v, isDefaultOpts, setCursorGetter, cb, tmp); }, getCursor: function () { return cursorGetter(); }, setValue: function (val) { @@ -1216,6 +1227,7 @@ define([ }) }, get: function (opts, a, n, evOnChange) { + var isDefaultOpts = !opts; if (!opts) { opts = TYPES.multiradio.defaultOpts; } if (!Array.isArray(opts.items) || !Array.isArray(opts.values)) { return; } var lines = opts.items.map(function (itemData) { @@ -1271,7 +1283,7 @@ define([ }, edit: function (cb, tmp) { var v = Util.clone(opts); - return editOptions(v, setCursorGetter, cb, tmp); + return editOptions(v, isDefaultOpts, setCursorGetter, cb, tmp); }, getCursor: function () { return cursorGetter(); }, setValue: function (val) { @@ -1362,6 +1374,7 @@ define([ }) }, get: function (opts, a, n, evOnChange) { + var isDefaultOpts = !opts; if (!opts) { opts = TYPES.checkbox.defaultOpts; } if (!Array.isArray(opts.values)) { return; } var name = Util.uid(); @@ -1414,7 +1427,7 @@ define([ }, edit: function (cb, tmp) { var v = Util.clone(opts); - return editOptions(v, setCursorGetter, cb, tmp); + return editOptions(v, isDefaultOpts, setCursorGetter, cb, tmp); }, getCursor: function () { return cursorGetter(); }, setValue: function (val) { @@ -1465,6 +1478,7 @@ define([ }) }, get: function (opts, a, n, evOnChange) { + var isDefaultOpts = !opts; if (!opts) { opts = TYPES.multicheck.defaultOpts; } if (!Array.isArray(opts.items) || !Array.isArray(opts.values)) { return; } var lines = opts.items.map(function (itemData) { @@ -1535,7 +1549,7 @@ define([ }, edit: function (cb, tmp) { var v = Util.clone(opts); - return editOptions(v, setCursorGetter, cb, tmp); + return editOptions(v, isDefaultOpts, setCursorGetter, cb, tmp); }, getCursor: function () { return cursorGetter(); }, setValue: function (val) { @@ -1628,6 +1642,7 @@ define([ }) }, get: function (opts, a, n, evOnChange) { + var isDefaultOpts = !opts; if (!opts) { opts = TYPES.sort.defaultOpts; } if (!Array.isArray(opts.values)) { return; } var map = {}; @@ -1706,7 +1721,7 @@ define([ }, edit: function (cb, tmp) { var v = Util.clone(opts); - return editOptions(v, setCursorGetter, cb, tmp); + return editOptions(v, isDefaultOpts, setCursorGetter, cb, tmp); }, getCursor: function () { return cursorGetter(); }, setValue: function (val) { @@ -1750,6 +1765,7 @@ define([ }) }, get: function (opts, answers, username, evOnChange) { + var isDefaultOpts = !opts; if (!opts) { opts = TYPES.poll.defaultOpts; } if (!Array.isArray(opts.values)) { return; } @@ -1827,7 +1843,7 @@ define([ }, edit: function (cb, tmp) { var v = Util.clone(opts); - return editOptions(v, setCursorGetter, cb, tmp); + return editOptions(v, isDefaultOpts, setCursorGetter, cb, tmp); }, getCursor: function () { return cursorGetter(); }, setValue: function (res) { From 228a44e36cb15eca9e5b09f78dbbb9bf352c347f Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 23 Aug 2021 14:56:45 +0200 Subject: [PATCH 048/223] Fix form question disappearing --- www/form/inner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/form/inner.js b/www/form/inner.js index ed1d77cdd..973b9e89c 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -2573,7 +2573,7 @@ define([ // Required radio displayed only for types that have an "isEmpty" function var requiredDiv; if (APP.isEditor && !isStatic && data.isEmpty) { - if (!block.opts) { block.opts = {}; } + if (!block.opts) { block.opts = TYPES[type].defaultOpts; } var isRequired = Boolean(block.opts.required); var radioOn = UI.createRadio('cp-form-required-'+uid, 'cp-form-required-on', Messages.form_required_on, isRequired, { From e33179df87a51af62f674d88d1fccf939dcd06a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Benqu=C3=A9?= Date: Mon, 23 Aug 2021 16:57:49 +0100 Subject: [PATCH 049/223] Style and align form editor buttons for preview and link --- www/form/app-form.less | 5 ++++- www/form/inner.js | 14 ++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index 441982b5f..86bd9a2ed 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -120,7 +120,6 @@ } .cp-form-creator-settings { - padding: 30px; .cp-form-actions { margin-top: 5px; } @@ -130,6 +129,9 @@ .cp-forms-results-participant { display: flex; flex-flow: column; + button { + margin-bottom: 20px; + } } } div.cp-form-filler-container { @@ -139,6 +141,7 @@ } div.cp-form-creator-control { padding: 10px; + margin-top: 10px; display: flex; flex-flow: column; width: 300px; diff --git a/www/form/inner.js b/www/form/inner.js index 19e42e131..8e3fb8596 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -2950,10 +2950,16 @@ define([ } var makeFormSettings = function () { - Messages.form_preview = "Preview participant page"; // XXX - Messages.form_geturl = "Copy participant link"; // XXX - var previewBtn = h('button.btn.btn-primary', Messages.form_preview); - var participantBtn = h('button.btn.btn-primary', Messages.form_geturl); + Messages.form_preview = "Preview form"; // XXX + Messages.form_geturl = "Copy link"; // XXX + var previewBtn = h('button.btn.btn-primary', [ + h('i.fa.fa-eye'), + Messages.form_preview + ]); + var participantBtn = h('button.btn.btn-primary',[ + h('i.fa.fa-link'), + Messages.form_geturl + ]); var preview = h('div.cp-forms-results-participant', [previewBtn, participantBtn]); $(previewBtn).click(function () { sframeChan.event('EV_OPEN_VIEW_URL'); From 7db03efdc509be9aba6606b6a5c19aae66efed92 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 23 Aug 2021 18:08:32 +0200 Subject: [PATCH 050/223] Fix type errors when changing question type in forms --- www/form/inner.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 973b9e89c..150e80207 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1204,7 +1204,7 @@ define([ Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; - if (!answer || !answer.trim()) { return empty++; } + if (!answer || !answer.trim || !answer.trim()) { return empty++; } Util.inc(count, answer); }); @@ -1315,7 +1315,7 @@ define([ 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; } + if (!res || !res.trim || !res.trim()) { return; } Util.inc(c, res); }); }); @@ -1452,6 +1452,7 @@ define([ Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; + if (answer && typeof(answer) === "string") { answer = [answer]; } if (!Array.isArray(answer) || !answer.length) { return empty++; } answer.forEach(function (val) { Util.inc(count, val); @@ -1582,6 +1583,7 @@ define([ Object.keys(answer).forEach(function (q_uid) { var c = count[q_uid] = count[q_uid] || {}; var res = answer[q_uid]; + if (res && typeof(res) === "string") { res = [res]; } if (!Array.isArray(res) || !res.length) { return; } res.forEach(function (v) { Util.inc(c, v); @@ -1745,6 +1747,7 @@ define([ Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; + if (answer && typeof(answer) === "string") { answer = [answer]; } if (!Array.isArray(answer) || !answer.length) { return empty++; } answer.forEach(function (el, i) { var score = l - i; @@ -1769,6 +1772,7 @@ define([ if (!opts) { opts = TYPES.poll.defaultOpts; } if (!Array.isArray(opts.values)) { return; } + if (APP.isEditor) { answers = {}; } var lines = makePollTable(answers, opts, false); var disabled = false; From 4e013520c5eb82c5d0cc70a82c331204984903ff Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 23 Aug 2021 18:14:26 +0200 Subject: [PATCH 051/223] Fix forms issues --- www/form/inner.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 150e80207..d16a2b474 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1271,6 +1271,7 @@ define([ $el.find('input').each(function (i, input) { var $i = $(input); if (res[uid]) { return; } + res[uid] = undefined; if (Util.isChecked($i)) { res[uid] = $i.data('val'); } }); }); @@ -1420,7 +1421,10 @@ define([ }); return res; }, - reset: function () { $(tag).find('input').removeAttr('checked'); }, + reset: function () { + $(tag).find('input').removeAttr('checked'); + checkDisabled(); + }, setEditable: function (state) { if (state) { checkDisabled(); } else { $tag.find('input').attr('disabled', 'disabled'); } @@ -1543,7 +1547,10 @@ define([ }); return res; }, - reset: function () { $(tag).find('input').removeAttr('checked'); }, + reset: function () { + $(tag).find('input').removeAttr('checked'); + lines.forEach(checkDisabled); + }, setEditable: function (state) { if (state) { lines.forEach(checkDisabled); } else { $(tag).find('input').attr('disabled', 'disabled'); } From 5a8104e793fdd49bb1910dabb3920387338c49ce Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 23 Aug 2021 18:16:53 +0200 Subject: [PATCH 052/223] lint compliance --- www/form/inner.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index d16a2b474..4120df1df 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -259,9 +259,9 @@ define([ if (placeholder) { input.placeholder = val; input.value = ''; - $(input).change(function () { - input.placeholder = ''; - $(input).off(change); + $(input).on('keypress', function () { + $(input).removeAttr('placeholder'); + $(input).off('keypress'); }); } if (uid) { $(input).data('uid', uid); } From e7eb79c4bc50998b003d396bd2ac63ec0569cefe Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 24 Aug 2021 11:23:15 +0200 Subject: [PATCH 053/223] More privacy settings for forms --- www/form/inner.js | 121 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 93 insertions(+), 28 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index 4120df1df..b4dd66ffb 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -2223,6 +2223,10 @@ define([ }); return results; }; + + Messages.form_anonAnswer = "All answers to this form are anonymous"; // XXX + Messages.form_authAnswer = "You can't answer anonymously to this form"; // XXX + var makeFormControls = function (framework, content, update, evOnChange) { var loggedIn = framework._.sfCommon.isLoggedIn(); var metadataMgr = framework._.cpNfInner.metadataMgr; @@ -2234,23 +2238,45 @@ define([ var anonName, $anonName; cbox = UI.createCheckbox('cp-form-anonymous', Messages.form_anonymousBox, true, {}); - var $anonBox = $(cbox).find('input'); - if (loggedIn) { - if (!content.answers.anonymous || APP.cantAnon) { - $(cbox).hide().find('input').attr('disabled', 'disabled').prop('checked', false); + var $cbox = $(cbox); + var $anonBox = $cbox.find('input'); + if (content.answers.makeAnonymous) { + // If we make all answers anonymous, hide the checkbox and display a message + $cbox.hide(); + $anonBox.attr('disabled', 'disabled').prop('checked', true); + setTimeout(function () { + // We need to wait for cbox to be added into the DOM before using .after() + $cbox.after(h('div.alert.alert-info', Messages.form_anonAnswer)); + }); + } else if (content.answers.anonymous) { + // Answers aren't anonymous and guests are allowed + // Guests can set a username and logged in users can answer anonymously + if (!loggedIn) { + anonName = h('div.cp-form-anon-answer-input', [ + Messages.form_answerAs, + h('input', { + value: user.name || '', + placeholder: Messages.form_anonName + }) + ]); + $anonName = $(anonName).hide(); + $anonBox.on('change', function () { + if (Util.isChecked($anonBox)) { $anonName.hide(); } + else { $anonName.show(); } + }); + } else if (APP.cantAnon) { + // You've already answered with your credentials + $cbox.hide(); + $anonBox.attr('disabled', 'disabled').prop('checked', false); } } else { - anonName = h('div.cp-form-anon-answer-input', [ - Messages.form_answerAs, - h('input', { - value: user.name || '', - placeholder: Messages.form_anonName - }) - ]); - $anonName = $(anonName).hide(); - $anonBox.on('change', function () { - if (Util.isChecked($anonBox)) { $anonName.hide(); } - else { $anonName.show(); } + // Answers don't have to be anonymous and only logged in users can answer + // ==> they have to answer with their keys so we know their name too + $cbox.hide(); + $anonBox.attr('disabled', 'disabled').prop('checked', false); + setTimeout(function () { + // We need to wait for cbox to be added into the DOM before using .after() + $cbox.after(h('div.alert.alert-info', Messages.form_authAnswer)); }); } @@ -2270,7 +2296,7 @@ define([ if (!results) { return; } var user = metadataMgr.getUserData(); - if (!Util.isChecked($anonBox)) { + if (!Util.isChecked($anonBox) && !content.answers.makeAnonymous) { results._userdata = loggedIn ? { avatar: user.avatar, name: user.name, @@ -2286,7 +2312,8 @@ define([ sframeChan.query('Q_FORM_SUBMIT', { mailbox: content.answers, results: results, - anonymous: !loggedIn || Util.isChecked($(cbox).find('input')) + anonymous: content.answers.makeAnonymous || !loggedIn + || (Util.isChecked($anonBox) && !APP.cantAnon) // use ephemeral keys }, function (err, data) { $send.attr('disabled', 'disabled'); if (err || (data && data.error)) { @@ -2305,6 +2332,7 @@ define([ $send.removeAttr('disabled'); //UI.alert(Messages.form_sent); // XXX not needed anymore? $send.text(Messages.form_update); + APP.hasAnswered = true; showAnsweredPage(framework, content, { '_time': +new Date() }); }); }); @@ -2343,10 +2371,8 @@ define([ return h('li', a); }; - if (APP.checkInvalidEvt) { evOnChange.unreg(APP.checkInvalidEvt); } - if (APP.checkErrorEvt) { evOnChange.unreg(APP.checkErrorEvt); } // Check invalid inputs - APP.checkInvalidEvt = function () { + evOnChange.reg(function () { var $container = $('div.cp-form-creator-content'); var $inputs = $container.find('input:invalid'); if (!$inputs.length) { @@ -2364,9 +2390,9 @@ define([ list ]; $invalid.empty().append(content); - }; + }); // Check empty required questions - APP.checkErrorEvt = function () { + evOnChange.reg(function () { if (!Array.isArray(APP.formBlocks)) { return; } var form = content.form; var errorBlocks = APP.formBlocks.filter(function (data) { @@ -2395,9 +2421,7 @@ define([ list ]; $errors.empty().append(divContent); - }; - evOnChange.reg(APP.checkInvalidEvt); - evOnChange.reg(APP.checkErrorEvt); + }); evOnChange.fire(true); } @@ -2886,7 +2910,7 @@ define([ } // If the form is already submitted, show an info message - if (answers) { + if (APP.hasAnswered) { showAnsweredPage(framework, content, answers); $container.prepend(h('div.alert.alert-info', Messages._getKey('form_alreadyAnswered', [ @@ -2896,6 +2920,20 @@ define([ // In view mode, add "Submit" and "reset" buttons $container.append(makeFormControls(framework, content, Boolean(answers), evOnChange)); + // In view mode, tell the user and answers are forced to be anonymous or authenticated + if (!APP.isEditor) { + var infoTxt; + var loggedIn = framework._.sfCommon.isLoggedIn(); + if (content.answers.makeAnonymous) { + infoTxt = Messages.form_anonAnswer; + } else if (!content.answers.anonymous && loggedIn) { + infoTxt = Messages.form_authAnswer; + } + if (infoTxt) { + $container.prepend(h('div.alert.alert-info', infoTxt)); + } + } + // Embed mode is enforced so we add the title at the top and a CryptPad logo // at the bottom var title = framework._.title.title || framework._.title.defaultTitle; @@ -3094,7 +3132,30 @@ define([ }; refreshResponse(); - // Allow anonymous answers + // Make answers anonymous + Messages.form_makeAnon = "Make all answers anonymous"; // XXX + var anonContainer = h('div.cp-form-anon-container'); + var $anon = $(anonContainer); + var refreshAnon = function () { + $anon.empty(); + var anonymous = content.answers.makeAnonymous; + var cbox = UI.createCheckbox('cp-form-make-anon', + Messages.form_makeAnon, anonymous, {}); + var radioContainer = h('div.cp-form-anon-radio', [cbox]); + var $r = $(radioContainer).find('input').on('change', function() { + var val = Util.isChecked($r); + content.answers.makeAnonymous = val; + framework.localChange(); + framework._.cpNfInner.chainpad.onSettle(function () { + UI.log(Messages.saved); + }); + }); + $anon.append(h('div.cp-form-actions', radioContainer)); + }; + refreshAnon(); + + // XXX UPDATE KEYS "form_anonyous_on", "form_anonymous_off" and "form_anonymous" + // Allow guest(anonymous) answers var privacyContainer = h('div.cp-form-privacy-container'); var $privacy = $(privacyContainer); var refreshPrivacy = function () { @@ -3216,6 +3277,7 @@ define([ evOnChange.reg(refreshPublic); evOnChange.reg(refreshPrivacy); + evOnChange.reg(refreshAnon); evOnChange.reg(refreshEditable); evOnChange.reg(refreshEndDate); //evOnChange.reg(refreshResponse); @@ -3224,6 +3286,7 @@ define([ preview, endDateContainer, privacyContainer, + anonContainer, editableContainer, resultsType, responseMsg @@ -3414,7 +3477,7 @@ define([ } // If the results are public and there is at least one doodle, fetch the results now - if (0 && content.answers.privateKey && Object.keys(content.form).some(function (uid) { + if (content.answers.privateKey && Object.keys(content.form).some(function (uid) { return content.form[uid].type === "poll"; })) { sframeChan.query("Q_FORM_FETCH_ANSWERS", { @@ -3444,6 +3507,7 @@ define([ if (answers) { var myAnswersObj = answers[curve1] || answers[curve2] || undefined; if (myAnswersObj) { + APP.hasAnswered = true; myAnswers = myAnswersObj.msg; myAnswers._time = myAnswersObj.time; } @@ -3478,6 +3542,7 @@ define([ var answers; if (obj && !obj.error) { answers = obj; + APP.hasAnswered = true; // If we have a non-anon answer, we can't answer anonymously later if (!obj._isAnon) { APP.cantAnon = true; } From 83a1a2a337b81a299d255d7983dbcdafa169d330 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 24 Aug 2021 13:24:06 +0200 Subject: [PATCH 054/223] Improve Poll template for forms --- www/common/common-ui-elements.js | 7 ++++--- www/form/inner.js | 10 +++++----- www/form/templates.js | 25 ++++++++++++++++++++++++- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 8315768f1..d8b399851 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -649,7 +649,7 @@ define([ if (!AppConfig.enableTemplates) { return; } if (!common.isLoggedIn()) { return; } button = $('