From 1177506ed33dc44c0883e571468ce67718ff9c45 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Mon, 27 Jan 2025 10:47:12 -0600 Subject: [PATCH 01/34] Use quill for rich editor --- .../static/scheduling/js/create_schedule.js | 52 +++++++++++++++++++ .../rich_text_message_configuration.html | 14 +---- package.json | 1 + yarn.lock | 45 ++++++++++++++++ 4 files changed, 100 insertions(+), 12 deletions(-) diff --git a/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js b/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js index c7b55647d7f4..06953a4686ba 100644 --- a/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js +++ b/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js @@ -3,6 +3,16 @@ import ko from "knockout"; import "jquery-ui/ui/widgets/datepicker"; import "bootstrap-timepicker/js/bootstrap-timepicker"; + +import "quill/dist/quill.snow.css"; +import Quill from 'quill'; +import Toolbar from "quill/modules/toolbar"; +import Snow from "quill/themes/snow"; + +import Bold from "quill/formats/bold"; +import Italic from "quill/formats/italic"; +import Header from "quill/formats/header"; + import "hqwebapp/js/components/select_toggle"; import initialPageData from "hqwebapp/js/initial_page_data"; import select2Handler from "hqwebapp/js/select2_handler"; @@ -18,6 +28,48 @@ ko.bindingHandlers.useTimePicker = { update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {}, }; +Quill.register({ + "modules/toolbar": Toolbar, + "themes/snow": Snow, + "formats/bold": Bold, + "formats/italic": Italic, + "formats/header": Header, +}); + +ko.bindingHandlers.richEditor = { + init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { + const editor = new Quill(element, { + theme: 'snow', + }); + + const value = ko.utils.unwrapObservable(valueAccessor()); + editor.clipboard.dangerouslyPasteHTML(value); + + let isSubscriberChange = false; + let isEditorChange = false; + + editor.on('text-change', function(delta, source) { + if (!isSubscriberChange) { + isEditorChange = true; + valueAccessor()(editor.root.innerHTML); + isEditorChange = false; + } + }); + + valueAccessor().subscribe(function (value) { + if (!isEditorChange) { + isSubscriberChange = true; + editor.clipboard.dangerouslyPasteHTML(value); + isSubscriberChange = false; + } + }); + + if (initialPageData.get('read_only_mode')) { + editor.enable(false); + } + }, +}; + var MessageViewModel = function (language_code, message) { var self = this; diff --git a/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_message_configuration.html b/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_message_configuration.html index 8f842ec8fd23..f928a4304f27 100644 --- a/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_message_configuration.html +++ b/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_message_configuration.html @@ -14,12 +14,7 @@
- +
@@ -28,11 +23,6 @@ >{% trans "Translation" %} (): - +
diff --git a/package.json b/package.json index e87c15061baf..b0416ca57e5c 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "nprogress": "0.2.0", "nvd3": "1.1.10", "nvd3-1.8.6": "npm:nvd3#1.8.6", + "quill": "2.0.3", "requirejs": "2.3.7", "requirejs-babel7": "^1.3.2", "select2": "4.1.0-rc.0", diff --git a/yarn.lock b/yarn.lock index 615ae7b30999..402919bc327a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3501,6 +3501,11 @@ eventemitter3@^3.1.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== +eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + events@^3.2.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -3571,6 +3576,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-diff@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -5191,6 +5201,16 @@ locate-path@^7.1.0: dependencies: p-locate "^6.0.0" +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ== + lodash.debounce@^4.0.6: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -6191,6 +6211,11 @@ pacote@^17.0.0, pacote@^17.0.4: ssri "^10.0.0" tar "^6.1.11" +parchment@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/parchment/-/parchment-3.0.0.tgz#2e3a4ada454e1206ae76ea7afcb50e9fb517e7d6" + integrity sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -6603,6 +6628,25 @@ quickselect@^2.0.0: resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018" integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw== +quill-delta@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-5.1.0.tgz#1c4bc08f7c8e5cc4bdc88a15a1a70c1cc72d2b48" + integrity sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA== + dependencies: + fast-diff "^1.3.0" + lodash.clonedeep "^4.5.0" + lodash.isequal "^4.5.0" + +quill@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/quill/-/quill-2.0.3.tgz#752765a31d5a535cdc5717dc49d4e50099365eb1" + integrity sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw== + dependencies: + eventemitter3 "^5.0.1" + lodash-es "^4.17.21" + parchment "^3.0.0" + quill-delta "^5.1.0" + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -7932,6 +7976,7 @@ workerpool@^6.5.1: integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: + name wrap-ansi-cjs version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From f1cf6ee25ccfa30a7fc6384fde73de355efc6aeb Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Wed, 29 Jan 2025 14:28:07 -0600 Subject: [PATCH 02/34] Additional formatting options --- .../static/scheduling/js/create_schedule.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js b/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js index 06953a4686ba..4e9e77ae2a15 100644 --- a/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js +++ b/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js @@ -38,7 +38,28 @@ Quill.register({ ko.bindingHandlers.richEditor = { init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { + const toolbarOptions = [ + [{ 'header': [1, 2, 3, 4, 5, 6, false] }], + ['bold', 'italic', 'underline', 'strike'], // toggled buttons + // ['blockquote', 'code-block'], + ['link', 'image'], + + [{ 'list': 'ordered'}, { 'list': 'bullet' }, { 'list': 'check' }], + // [{ 'script': 'sub'}, { 'script': 'super' }], // superscript/subscript + [{ 'indent': '-1'}, { 'indent': '+1' }], // outdent/indent + // [{ 'direction': 'rtl' }], // text direction + + [{ 'color': [] }, { 'background': [] }], // dropdown with defaults from theme + [{ 'font': [] }], + [{ 'align': [] }], + + ['clean'], // remove formatting button + ]; + const editor = new Quill(element, { + modules: { + toolbar: toolbarOptions, + }, theme: 'snow', }); From 83ca7db22aae3bca9b0fb079a198b79e11fea793 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Thu, 30 Jan 2025 11:48:11 -0600 Subject: [PATCH 03/34] Custom font set for quill * And some cosmetic changes * Still need to figure out where to put the styles --- .../hqwebapp/static/hqwebapp/scss/commcarehq.scss | 1 + .../static/hqwebapp/scss/commcarehq/_quill.scss | 11 +++++++++++ .../static/scheduling/js/create_schedule.js | 10 ++++++++-- .../scheduling/static/scheduling/js/quill.css | 11 +++++++++++ 4 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_quill.scss create mode 100644 corehq/messaging/scheduling/static/scheduling/js/quill.css diff --git a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq.scss b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq.scss index 384127c02c0c..cb6ac421a869 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq.scss +++ b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq.scss @@ -88,6 +88,7 @@ @import "commcarehq/pagination"; @import "commcarehq/panels"; @import "commcarehq/plan_notice"; +@import "commcarehq/quill"; @import "commcarehq/radio_select"; @import "commcarehq/readable_forms"; @import "commcarehq/report"; diff --git a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_quill.scss b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_quill.scss new file mode 100644 index 000000000000..e99a406540f3 --- /dev/null +++ b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_quill.scss @@ -0,0 +1,11 @@ +.ql-snow .ql-picker.ql-font .ql-picker-label[data-value]::before, +.ql-snow .ql-picker.ql-font .ql-picker-item[data-value]::before { + content: attr(data-value) !important; +} +.ql-editor { + height: 250px; +} + +.ql-picker-label { + overflow: hidden; +} diff --git a/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js b/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js index 4e9e77ae2a15..ec883332ff8a 100644 --- a/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js +++ b/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js @@ -5,6 +5,7 @@ import "jquery-ui/ui/widgets/datepicker"; import "bootstrap-timepicker/js/bootstrap-timepicker"; import "quill/dist/quill.snow.css"; +import "scheduling/js/quill.css"; // can be removed once I figure out why quill.scss is not being picked up. import Quill from 'quill'; import Toolbar from "quill/modules/toolbar"; import Snow from "quill/themes/snow"; @@ -38,8 +39,13 @@ Quill.register({ ko.bindingHandlers.richEditor = { init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { + const fontFamilyArr = ["Roboto Condensed", "Times New Roman", "Calibri", "Calibri Light", "Sans-Serif"]; + let fonts = Quill.import("attributors/style/font"); + fonts.whitelist = fontFamilyArr; + Quill.register(fonts, true); + const toolbarOptions = [ - [{ 'header': [1, 2, 3, 4, 5, 6, false] }], + [{ 'header': [false, 1, 2, 3] }], ['bold', 'italic', 'underline', 'strike'], // toggled buttons // ['blockquote', 'code-block'], ['link', 'image'], @@ -50,7 +56,7 @@ ko.bindingHandlers.richEditor = { // [{ 'direction': 'rtl' }], // text direction [{ 'color': [] }, { 'background': [] }], // dropdown with defaults from theme - [{ 'font': [] }], + [{ 'font': fontFamilyArr }], [{ 'align': [] }], ['clean'], // remove formatting button diff --git a/corehq/messaging/scheduling/static/scheduling/js/quill.css b/corehq/messaging/scheduling/static/scheduling/js/quill.css new file mode 100644 index 000000000000..e99a406540f3 --- /dev/null +++ b/corehq/messaging/scheduling/static/scheduling/js/quill.css @@ -0,0 +1,11 @@ +.ql-snow .ql-picker.ql-font .ql-picker-label[data-value]::before, +.ql-snow .ql-picker.ql-font .ql-picker-item[data-value]::before { + content: attr(data-value) !important; +} +.ql-editor { + height: 250px; +} + +.ql-picker-label { + overflow: hidden; +} From d94063b3d47cb9bc1b16aefed60a0b4fdb4da846 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Mon, 3 Feb 2025 16:22:00 -0600 Subject: [PATCH 04/34] Add image upload functionality to quill * Move toolbar into template to make it customizable --- .../static/scheduling/js/create_schedule.js | 76 +++++++++++++------ .../scheduling/partials/rich_text_editor.html | 34 +++++++++ .../rich_text_message_configuration.html | 4 +- 3 files changed, 89 insertions(+), 25 deletions(-) create mode 100644 corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html diff --git a/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js b/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js index ec883332ff8a..468e9e4d3433 100644 --- a/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js +++ b/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js @@ -38,35 +38,65 @@ Quill.register({ }); ko.bindingHandlers.richEditor = { - init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { + init: function (element, valueAccessor) { const fontFamilyArr = ["Roboto Condensed", "Times New Roman", "Calibri", "Calibri Light", "Sans-Serif"]; let fonts = Quill.import("attributors/style/font"); fonts.whitelist = fontFamilyArr; Quill.register(fonts, true); - const toolbarOptions = [ - [{ 'header': [false, 1, 2, 3] }], - ['bold', 'italic', 'underline', 'strike'], // toggled buttons - // ['blockquote', 'code-block'], - ['link', 'image'], - - [{ 'list': 'ordered'}, { 'list': 'bullet' }, { 'list': 'check' }], - // [{ 'script': 'sub'}, { 'script': 'super' }], // superscript/subscript - [{ 'indent': '-1'}, { 'indent': '+1' }], // outdent/indent - // [{ 'direction': 'rtl' }], // text direction - - [{ 'color': [] }, { 'background': [] }], // dropdown with defaults from theme - [{ 'font': fontFamilyArr }], - [{ 'align': [] }], - - ['clean'], // remove formatting button - ]; - + const toolbar = element.parentElement.querySelector("#ql-toolbar"); const editor = new Quill(element, { modules: { - toolbar: toolbarOptions, + toolbar: toolbar, }, - theme: 'snow', + theme: "snow", + }); + + let currentSelectionRange = { index: 0, length: 0}; + const insertImageButton = toolbar.querySelector("#insert-image"); + insertImageButton.addEventListener("click", function (clickEvent) { + clickEvent.stopPropagation(); + const input = document.createElement("input"); + input.onchange = function (onChangeEvent) { + const file = onChangeEvent.target.files[0]; + const uploadUrl = initialPageData.reverse("upload_messaging_image"); + let formData = new FormData(); + + formData.append("upload", file, file.name); + fetch(uploadUrl, { + method: "POST", + body: formData, + headers: { + "X-CSRFTOKEN": $("#csrfTokenContainer").val(), + }, + }) + .then(function (response) { + return response.json(); + }) + .then(function (data) { + const Delta = Quill.import("delta"); + editor.updateContents( + new Delta() + .retain(currentSelectionRange.index) + .delete(currentSelectionRange.length) + .insert({ + image: data.url, + }, { + // link: data.url, + alt: file.name, + }), + ); + }); + }; + input.accept = "image/png, image/jpeg"; + input.type = "file"; + input.click(); + }); + + editor.on("selection-change", (range) => { + if (range) { + currentSelectionRange = range; + } }); const value = ko.utils.unwrapObservable(valueAccessor()); @@ -75,7 +105,7 @@ ko.bindingHandlers.richEditor = { let isSubscriberChange = false; let isEditorChange = false; - editor.on('text-change', function(delta, source) { + editor.on("text-change", function () { if (!isSubscriberChange) { isEditorChange = true; valueAccessor()(editor.root.innerHTML); @@ -91,7 +121,7 @@ ko.bindingHandlers.richEditor = { } }); - if (initialPageData.get('read_only_mode')) { + if (initialPageData.get("read_only_mode")) { editor.enable(false); } }, diff --git a/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html b/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html new file mode 100644 index 000000000000..e202673566c9 --- /dev/null +++ b/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html @@ -0,0 +1,34 @@ +
+ + + + + + + + + + + + + + + + + + +
+
diff --git a/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_message_configuration.html b/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_message_configuration.html index f928a4304f27..5b4d0fdbb68a 100644 --- a/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_message_configuration.html +++ b/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_message_configuration.html @@ -14,7 +14,7 @@
-
+ {% include "scheduling/partials/rich_text_editor.html" with editableText="nonTranslatedMessage" %}
@@ -23,6 +23,6 @@ >{% trans "Translation" %} (): -
+ {% include "scheduling/partials/rich_text_editor.html" with editableText="html_message" %}
From 6c39f6141c4a6fd5360cf76a407ec0937734a77b Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Wed, 5 Feb 2025 07:33:01 -0600 Subject: [PATCH 05/34] Move editor knockout binding to shared file --- .../static/hqwebapp/js/components}/quill.css | 0 .../components/rich_text_knockout_bindings.js | 111 ++++++++++++++++++ .../static/scheduling/js/create_schedule.js | 109 +---------------- 3 files changed, 112 insertions(+), 108 deletions(-) rename corehq/{messaging/scheduling/static/scheduling/js => apps/hqwebapp/static/hqwebapp/js/components}/quill.css (100%) create mode 100644 corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js diff --git a/corehq/messaging/scheduling/static/scheduling/js/quill.css b/corehq/apps/hqwebapp/static/hqwebapp/js/components/quill.css similarity index 100% rename from corehq/messaging/scheduling/static/scheduling/js/quill.css rename to corehq/apps/hqwebapp/static/hqwebapp/js/components/quill.css diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js new file mode 100644 index 000000000000..017ee1aff2d5 --- /dev/null +++ b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js @@ -0,0 +1,111 @@ +import ko from "knockout"; + +import "quill/dist/quill.snow.css"; +import "hqwebapp/js/components/quill.css"; // can be removed once I figure out why quill.scss is not being picked up. +import Quill from 'quill'; +import Toolbar from "quill/modules/toolbar"; +import Snow from "quill/themes/snow"; +import Bold from "quill/formats/bold"; +import Italic from "quill/formats/italic"; +import Header from "quill/formats/header"; + +import initialPageData from "hqwebapp/js/initial_page_data"; + +Quill.register({ + "modules/toolbar": Toolbar, + "themes/snow": Snow, + "formats/bold": Bold, + "formats/italic": Italic, + "formats/header": Header, +}); + +ko.bindingHandlers.richEditor = { + init: function (element, valueAccessor) { + const fontFamilyArr = ["Roboto Condensed", "Times New Roman", "Calibri", "Calibri Light", "Sans-Serif"]; + let fonts = Quill.import("attributors/style/font"); + fonts.whitelist = fontFamilyArr; + Quill.register(fonts, true); + + const toolbar = element.parentElement.querySelector("#ql-toolbar"); + const editor = new Quill(element, { + modules: { + toolbar: toolbar, + }, + theme: "snow", + }); + + let currentSelectionRange = { index: 0, length: 0}; + const insertImageButton = toolbar.querySelector("#insert-image"); + insertImageButton.addEventListener("click", function (clickEvent) { + clickEvent.stopPropagation(); + const input = document.createElement("input"); + input.accept = "image/png, image/jpeg"; + input.type = "file"; + input.onchange = function (onChangeEvent) { + const file = onChangeEvent.target.files[0]; + const uploadUrl = initialPageData.reverse("upload_messaging_image"); + let formData = new FormData(); + + formData.append("upload", file, file.name); + fetch(uploadUrl, { + method: "POST", + body: formData, + headers: { + "X-CSRFTOKEN": $("#csrfTokenContainer").val(), + }, + }) + .then(function (response) { + return response.json(); + }) + .then(function (data) { + const Delta = Quill.import("delta"); + editor.updateContents( + new Delta() + .retain(currentSelectionRange.index) + .delete(currentSelectionRange.length) + .insert({ + image: data.url, + }, { + // link: data.url, + alt: file.name, + }), + ); + }); + }; + + input.click(); + }); + + editor.on("selection-change", (range) => { + if (range) { + currentSelectionRange = range; + } + }); + + const value = ko.utils.unwrapObservable(valueAccessor()); + editor.clipboard.dangerouslyPasteHTML(value); + + let isSubscriberChange = false; + let isEditorChange = false; + + editor.on("text-change", function () { + if (!isSubscriberChange) { + isEditorChange = true; + valueAccessor()(editor.root.innerHTML); + isEditorChange = false; + } + }); + + valueAccessor().subscribe(function (value) { + if (!isEditorChange) { + isSubscriberChange = true; + editor.clipboard.dangerouslyPasteHTML(value); + isSubscriberChange = false; + } + }); + + if (initialPageData.get("read_only_mode")) { + editor.enable(false); + } + }, +}; diff --git a/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js b/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js index 468e9e4d3433..3ba7c5c52cc1 100644 --- a/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js +++ b/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js @@ -4,16 +4,7 @@ import ko from "knockout"; import "jquery-ui/ui/widgets/datepicker"; import "bootstrap-timepicker/js/bootstrap-timepicker"; -import "quill/dist/quill.snow.css"; -import "scheduling/js/quill.css"; // can be removed once I figure out why quill.scss is not being picked up. -import Quill from 'quill'; -import Toolbar from "quill/modules/toolbar"; -import Snow from "quill/themes/snow"; - -import Bold from "quill/formats/bold"; -import Italic from "quill/formats/italic"; -import Header from "quill/formats/header"; - +import "hqwebapp/js/components/rich_text_knockout_bindings"; import "hqwebapp/js/components/select_toggle"; import initialPageData from "hqwebapp/js/initial_page_data"; import select2Handler from "hqwebapp/js/select2_handler"; @@ -29,104 +20,6 @@ ko.bindingHandlers.useTimePicker = { update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {}, }; -Quill.register({ - "modules/toolbar": Toolbar, - "themes/snow": Snow, - "formats/bold": Bold, - "formats/italic": Italic, - "formats/header": Header, -}); - -ko.bindingHandlers.richEditor = { - init: function (element, valueAccessor) { - const fontFamilyArr = ["Roboto Condensed", "Times New Roman", "Calibri", "Calibri Light", "Sans-Serif"]; - let fonts = Quill.import("attributors/style/font"); - fonts.whitelist = fontFamilyArr; - Quill.register(fonts, true); - - const toolbar = element.parentElement.querySelector("#ql-toolbar"); - const editor = new Quill(element, { - modules: { - toolbar: toolbar, - }, - theme: "snow", - }); - - let currentSelectionRange = { index: 0, length: 0}; - const insertImageButton = toolbar.querySelector("#insert-image"); - insertImageButton.addEventListener("click", function (clickEvent) { - clickEvent.stopPropagation(); - const input = document.createElement("input"); - input.onchange = function (onChangeEvent) { - const file = onChangeEvent.target.files[0]; - const uploadUrl = initialPageData.reverse("upload_messaging_image"); - let formData = new FormData(); - - formData.append("upload", file, file.name); - fetch(uploadUrl, { - method: "POST", - body: formData, - headers: { - "X-CSRFTOKEN": $("#csrfTokenContainer").val(), - }, - }) - .then(function (response) { - return response.json(); - }) - .then(function (data) { - const Delta = Quill.import("delta"); - editor.updateContents( - new Delta() - .retain(currentSelectionRange.index) - .delete(currentSelectionRange.length) - .insert({ - image: data.url, - }, { - // link: data.url, - alt: file.name, - }), - ); - }); - }; - input.accept = "image/png, image/jpeg"; - input.type = "file"; - input.click(); - }); - - editor.on("selection-change", (range) => { - if (range) { - currentSelectionRange = range; - } - }); - - const value = ko.utils.unwrapObservable(valueAccessor()); - editor.clipboard.dangerouslyPasteHTML(value); - - let isSubscriberChange = false; - let isEditorChange = false; - - editor.on("text-change", function () { - if (!isSubscriberChange) { - isEditorChange = true; - valueAccessor()(editor.root.innerHTML); - isEditorChange = false; - } - }); - - valueAccessor().subscribe(function (value) { - if (!isEditorChange) { - isSubscriberChange = true; - editor.clipboard.dangerouslyPasteHTML(value); - isSubscriberChange = false; - } - }); - - if (initialPageData.get("read_only_mode")) { - editor.enable(false); - } - }, -}; - var MessageViewModel = function (language_code, message) { var self = this; From 74f7e262b56fc0d09682d1a73315d6cdf1301f4c Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Wed, 5 Feb 2025 07:34:48 -0600 Subject: [PATCH 06/34] Because and indent of 2 is prettier? --- .../static/hqwebapp/js/components/quill.css | 6 +- .../static/hqwebapp/scss/commcarehq.scss | 5 +- .../hqwebapp/scss/commcarehq/_quill.scss | 6 +- .../scheduling/partials/rich_text_editor.html | 64 ++++++++++--------- 4 files changed, 41 insertions(+), 40 deletions(-) diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/components/quill.css b/corehq/apps/hqwebapp/static/hqwebapp/js/components/quill.css index e99a406540f3..92c2974f2480 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/js/components/quill.css +++ b/corehq/apps/hqwebapp/static/hqwebapp/js/components/quill.css @@ -1,11 +1,11 @@ .ql-snow .ql-picker.ql-font .ql-picker-label[data-value]::before, .ql-snow .ql-picker.ql-font .ql-picker-item[data-value]::before { - content: attr(data-value) !important; + content: attr(data-value) !important; } .ql-editor { - height: 250px; + height: 250px; } .ql-picker-label { - overflow: hidden; + overflow: hidden; } diff --git a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq.scss b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq.scss index cb6ac421a869..cb5a488e4a7d 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq.scss +++ b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq.scss @@ -1,10 +1,9 @@ - // Configuration @import "functions"; -@import "commcarehq/variables"; // This comes before Bootstrap 5 variables to override the defaults +@import "commcarehq/variables"; // This comes before Bootstrap 5 variables to override the defaults @import "variables"; @import "variables-dark"; -@import "commcarehq/variables_bootstrap3"; // Variables specific to B3-era Stylesheet +@import "commcarehq/variables_bootstrap3"; // Variables specific to B3-era Stylesheet @import "maps"; @import "mixins"; @import "commcarehq/mixins"; diff --git a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_quill.scss b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_quill.scss index e99a406540f3..92c2974f2480 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_quill.scss +++ b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_quill.scss @@ -1,11 +1,11 @@ .ql-snow .ql-picker.ql-font .ql-picker-label[data-value]::before, .ql-snow .ql-picker.ql-font .ql-picker-item[data-value]::before { - content: attr(data-value) !important; + content: attr(data-value) !important; } .ql-editor { - height: 250px; + height: 250px; } .ql-picker-label { - overflow: hidden; + overflow: hidden; } diff --git a/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html b/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html index e202673566c9..eac5239a22bc 100644 --- a/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html +++ b/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html @@ -1,34 +1,36 @@
- - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + +
From ddeffd0c803efdb7422af79525fd4aef9c090953 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Wed, 5 Feb 2025 08:28:12 -0600 Subject: [PATCH 07/34] Use global _quill.sccs * Started working for some reason now. --- .../hqwebapp/static/hqwebapp/js/components/quill.css | 11 ----------- .../js/components/rich_text_knockout_bindings.js | 1 - 2 files changed, 12 deletions(-) delete mode 100644 corehq/apps/hqwebapp/static/hqwebapp/js/components/quill.css diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/components/quill.css b/corehq/apps/hqwebapp/static/hqwebapp/js/components/quill.css deleted file mode 100644 index 92c2974f2480..000000000000 --- a/corehq/apps/hqwebapp/static/hqwebapp/js/components/quill.css +++ /dev/null @@ -1,11 +0,0 @@ -.ql-snow .ql-picker.ql-font .ql-picker-label[data-value]::before, -.ql-snow .ql-picker.ql-font .ql-picker-item[data-value]::before { - content: attr(data-value) !important; -} -.ql-editor { - height: 250px; -} - -.ql-picker-label { - overflow: hidden; -} diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js index 017ee1aff2d5..a7bc188901d7 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js +++ b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js @@ -1,7 +1,6 @@ import ko from "knockout"; import "quill/dist/quill.snow.css"; -import "hqwebapp/js/components/quill.css"; // can be removed once I figure out why quill.scss is not being picked up. import Quill from 'quill'; import Toolbar from "quill/modules/toolbar"; import Snow from "quill/themes/snow"; From 33292dbeab3edae4fb9d203d0b8964397024567d Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Wed, 5 Feb 2025 08:55:41 -0600 Subject: [PATCH 08/34] Round quill editor corners to match style of forms --- .../static/hqwebapp/scss/commcarehq/_quill.scss | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_quill.scss b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_quill.scss index 92c2974f2480..96c3fd3860de 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_quill.scss +++ b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_quill.scss @@ -6,6 +6,16 @@ height: 250px; } +.ql-toolbar { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} + +.ql-container { + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; +} + .ql-picker-label { overflow: hidden; } From 552c1151cd88ffa10fadd2a31f53fb4132820f68 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Wed, 5 Feb 2025 14:07:46 -0600 Subject: [PATCH 09/34] Add missing fonts --- .../js/components/rich_text_knockout_bindings.js | 11 ++++++++++- .../scheduling/partials/rich_text_editor.html | 11 +++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js index a7bc188901d7..72a30af06df5 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js +++ b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js @@ -20,7 +20,16 @@ Quill.register({ ko.bindingHandlers.richEditor = { init: function (element, valueAccessor) { - const fontFamilyArr = ["Roboto Condensed", "Times New Roman", "Calibri", "Calibri Light", "Sans-Serif"]; + const fontFamilyArr = [ + "Arial", + "Courier New", + "Georgia", + "Lucida Sans Unicode", + "Tahoma", + "Times New Roman", + "Trebuchet MS", + "Verdana", + ]; let fonts = Quill.import("attributors/style/font"); fonts.whitelist = fontFamilyArr; Quill.register(fonts, true); diff --git a/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html b/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html index eac5239a22bc..3cbfc7df2fa1 100644 --- a/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html +++ b/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html @@ -6,10 +6,13 @@ - + From e130c22fbe75103acce9c38c3229bc0a229deec0 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Fri, 7 Feb 2025 16:21:17 -0600 Subject: [PATCH 14/34] Just use css file --- .../components/rich_text_knockout_bindings.js | 2 +- .../static/hqwebapp/scss/commcarehq.scss | 6 +++--- .../hqwebapp/scss/commcarehq/_quill.scss | 21 ------------------- 3 files changed, 4 insertions(+), 25 deletions(-) delete mode 100644 corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_quill.scss diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js index ef9cd168bb8c..a1e90ab2014d 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js +++ b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js @@ -1,7 +1,7 @@ import ko from "knockout"; import "quill/dist/quill.snow.css"; -import "hqwebapp/js/components/quill.css"; // can be removed once I figure out why quill.scss is not being picked up. +import "hqwebapp/js/components/quill.css"; import Quill from 'quill'; import Toolbar from "quill/modules/toolbar"; import Snow from "quill/themes/snow"; diff --git a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq.scss b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq.scss index cb5a488e4a7d..384127c02c0c 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq.scss +++ b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq.scss @@ -1,9 +1,10 @@ + // Configuration @import "functions"; -@import "commcarehq/variables"; // This comes before Bootstrap 5 variables to override the defaults +@import "commcarehq/variables"; // This comes before Bootstrap 5 variables to override the defaults @import "variables"; @import "variables-dark"; -@import "commcarehq/variables_bootstrap3"; // Variables specific to B3-era Stylesheet +@import "commcarehq/variables_bootstrap3"; // Variables specific to B3-era Stylesheet @import "maps"; @import "mixins"; @import "commcarehq/mixins"; @@ -87,7 +88,6 @@ @import "commcarehq/pagination"; @import "commcarehq/panels"; @import "commcarehq/plan_notice"; -@import "commcarehq/quill"; @import "commcarehq/radio_select"; @import "commcarehq/readable_forms"; @import "commcarehq/report"; diff --git a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_quill.scss b/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_quill.scss deleted file mode 100644 index 96c3fd3860de..000000000000 --- a/corehq/apps/hqwebapp/static/hqwebapp/scss/commcarehq/_quill.scss +++ /dev/null @@ -1,21 +0,0 @@ -.ql-snow .ql-picker.ql-font .ql-picker-label[data-value]::before, -.ql-snow .ql-picker.ql-font .ql-picker-item[data-value]::before { - content: attr(data-value) !important; -} -.ql-editor { - height: 250px; -} - -.ql-toolbar { - border-top-left-radius: 3px; - border-top-right-radius: 3px; -} - -.ql-container { - border-bottom-left-radius: 3px; - border-bottom-right-radius: 3px; -} - -.ql-picker-label { - overflow: hidden; -} From 3d5f51e20a74f86fe53301696fe5c9434faef606 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Mon, 10 Feb 2025 13:08:02 -0600 Subject: [PATCH 15/34] Add help link to rich text FF --- corehq/toggles/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/corehq/toggles/__init__.py b/corehq/toggles/__init__.py index 58bc8cf4f27c..31739f97e852 100644 --- a/corehq/toggles/__init__.py +++ b/corehq/toggles/__init__.py @@ -1563,6 +1563,7 @@ def _commtrackify(domain_name, toggle_is_enabled): 'Enable sending rich text HTML emails in conditional alerts and broadcasts', TAG_CUSTOM, [NAMESPACE_DOMAIN], + help_link='https://dimagi.atlassian.net/wiki/spaces/USH/pages/2901835924/Rich+text+emails' ) RUN_AUTO_CASE_UPDATES_ON_SAVE = StaticToggle( From a3b2e24edd784c70da65d672b5de901457846636 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Tue, 11 Feb 2025 13:30:29 -0600 Subject: [PATCH 16/34] Fix quill header menu paragraph option --- .../templates/scheduling/partials/rich_text_editor.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html b/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html index d3b70e8c3a1e..ab9602cee428 100644 --- a/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html +++ b/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html @@ -1,6 +1,6 @@
From 235f478f36580046214d51eed14f9cba0cf366d8 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Thu, 13 Feb 2025 10:49:08 -0600 Subject: [PATCH 20/34] Do not need check box list. --- .../templates/scheduling/partials/rich_text_editor.html | 1 - 1 file changed, 1 deletion(-) diff --git a/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html b/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html index eb5acc1af189..9cb719945a55 100644 --- a/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html +++ b/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html @@ -30,7 +30,6 @@ - From 24575ecf58de524d17dfe769053ab1f83c6221f5 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Thu, 13 Feb 2025 11:17:09 -0600 Subject: [PATCH 21/34] Handle error when uploading image --- .../js/components/rich_text_knockout_bindings.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js index 1ab2e3eccb7d..2d2af91469dc 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js +++ b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js @@ -39,6 +39,15 @@ function imageHandler() { }, }) .then(function (response) { + if (!response.ok) { + if (response.status === 400) { + return response.json().then(function (errorJson) { + throw Error(errorJson.error.message); + }); + } + + throw Error("Error uploading image"); + } return response.json(); }) .then(function (data) { @@ -54,6 +63,9 @@ function imageHandler() { alt: file.name, }), ); + }) + .catch(function (error) { + alert(error); }); }; input.click(); From df4933aed03aa43a4feeb43fae488d65acbd3bf3 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Thu, 13 Feb 2025 12:15:08 -0600 Subject: [PATCH 22/34] Add title to all buttons in the toolbar will be used to show tool tips on hover --- .../scheduling/partials/rich_text_editor.html | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html b/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html index 9cb719945a55..617e555eb2ac 100644 --- a/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html +++ b/corehq/messaging/scheduling/templates/scheduling/partials/rich_text_editor.html @@ -1,11 +1,11 @@
- - @@ -14,25 +14,25 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + +
From af7366ef8004997b772a746646264addf68c100a Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Thu, 13 Feb 2025 12:18:23 -0600 Subject: [PATCH 23/34] making the linter happy --- corehq/messaging/scheduling/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/corehq/messaging/scheduling/const.py b/corehq/messaging/scheduling/const.py index 9ad86f4a591e..573cc24ccb2f 100644 --- a/corehq/messaging/scheduling/const.py +++ b/corehq/messaging/scheduling/const.py @@ -57,7 +57,7 @@ 'img': ['style', 'src', 'width', 'height', 'class'], 'figcaption': ['style', 'class'], 'figure': ['style', 'class'], - 'table': ['class', 'role','cellspacing', 'cellpadding', 'border', 'align', 'width'], + 'table': ['class', 'role', 'cellspacing', 'cellpadding', 'border', 'align', 'width'], 'td': ['valign'], 'meta': ['charset', 'name', 'viewport', 'content', 'initial-scale'] } From b17d8023f32dae528f8c47151560aecd3d69fd57 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Thu, 13 Feb 2025 12:59:40 -0600 Subject: [PATCH 24/34] Eradicate the dust bunnies --- .../static/scheduling/js/create_schedule.js | 292 +++++++++--------- 1 file changed, 146 insertions(+), 146 deletions(-) diff --git a/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js b/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js index d875ffdd8625..e5630906e1a3 100644 --- a/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js +++ b/corehq/messaging/scheduling/static/scheduling/js/create_schedule.js @@ -81,58 +81,58 @@ var TranslationViewModel = function (languageCodes, translations) { }); self.loadInitialTranslatedMessages = function () { - languageCodes.forEach(function (language_code) { - self.translatedMessages.push(new MessageViewModel(language_code, translations[language_code])); + languageCodes.forEach(function (languageCode) { + self.translatedMessages.push(new MessageViewModel(languageCode, translations[languageCode])); }); }; self.loadInitialTranslatedMessages(); }; -var ContentViewModel = function (initial_values) { +var ContentViewModel = function (initialValues) { var self = this; self.subject = new TranslationViewModel( initialPageData.get("language_list"), - initial_values.subject, + initialValues.subject, ); self.message = new TranslationViewModel( initialPageData.get("language_list"), - initial_values.message, + initialValues.message, ); self.html_message = new TranslationViewModel( initialPageData.get("language_list"), - initial_values.html_message, + initialValues.html_message, ); - self.survey_reminder_intervals_enabled = ko.observable(initial_values.survey_reminder_intervals_enabled); - self.fcm_message_type = ko.observable(initial_values.fcm_message_type); + self.survey_reminder_intervals_enabled = ko.observable(initialValues.survey_reminder_intervals_enabled); + self.fcm_message_type = ko.observable(initialValues.fcm_message_type); }; -var EventAndContentViewModel = function (initial_values) { +var EventAndContentViewModel = function (initialValues) { var self = this; - ContentViewModel.call(self, initial_values); + ContentViewModel.call(self, initialValues); - self.day = ko.observable(initial_values.day); - self.time = ko.observable(initial_values.time); - self.case_property_name = ko.observable(initial_values.case_property_name); - self.minutes_to_wait = ko.observable(initial_values.minutes_to_wait); - self.deleted = ko.observable(initial_values.DELETE); - self.order = ko.observable(initial_values.ORDER); + self.day = ko.observable(initialValues.day); + self.time = ko.observable(initialValues.time); + self.case_property_name = ko.observable(initialValues.case_property_name); + self.minutesToWait = ko.observable(initialValues.minutesToWait); + self.deleted = ko.observable(initialValues.DELETE); + self.order = ko.observable(initialValues.ORDER); self.waitTimeDisplay = ko.computed(function () { - var minutes_to_wait = parseInt(self.minutes_to_wait()); - if (minutes_to_wait >= 0) { - var hours = Math.floor(minutes_to_wait / 60); - var minutes = minutes_to_wait % 60; - var hours_text = hours + ' ' + gettext('hour(s)'); - var minutes_text = minutes + ' ' + gettext('minute(s)'); + var minutesToWait = parseInt(self.minutesToWait()); + if (minutesToWait >= 0) { + var hours = Math.floor(minutesToWait / 60); + var minutes = minutesToWait % 60; + var hoursText = hours + ' ' + gettext('hour(s)'); + var minutesText = minutes + ' ' + gettext('minute(s)'); if (hours > 0) { - return hours_text + ', ' + minutes_text; + return hoursText + ', ' + minutesText; } else { - return minutes_text; + return minutesText; } } return ''; @@ -170,67 +170,67 @@ var CustomEventContainer = function (id) { }); }; -var CreateScheduleViewModel = function (initial_values, select2_user_recipients, - select2_user_group_recipients, select2_user_organization_recipients, select2_location_types, - select2_case_group_recipients, current_visit_scheduler_form) { +var CreateScheduleViewModel = function (initialValues, select2UserRecipients, + select2UserGroupRecipients, select2UserOrganizationRecipients, select2LocationTypes, + select2CaseGroupRecipients, currentVisitSchedulerForm) { var self = this; - self.useCase = ko.observable(initial_values.use_case); + self.useCase = ko.observable(initialValues.use_case); self.timestamp = new Date().getTime(); - self.send_frequency = ko.observable(initial_values.send_frequency); - self.weekdays = ko.observableArray(initial_values.weekdays || []); - self.days_of_month = ko.observableArray(initial_values.days_of_month || []); - self.send_time = ko.observable(initial_values.send_time); - self.send_time_type = ko.observable(initial_values.send_time_type); - self.start_date = ko.observable(initial_values.start_date); - self.start_date_type = ko.observable(initial_values.start_date_type); - self.start_offset_type = ko.observable(initial_values.start_offset_type); - self.repeat = ko.observable(initial_values.repeat); - self.repeat_every = ko.observable(initial_values.repeat_every); - self.stop_type = ko.observable(initial_values.stop_type); - self.occurrences = ko.observable(initial_values.occurrences); - self.recipient_types = ko.observableArray(initial_values.recipient_types || []); + self.send_frequency = ko.observable(initialValues.send_frequency); + self.weekdays = ko.observableArray(initialValues.weekdays || []); + self.days_of_month = ko.observableArray(initialValues.days_of_month || []); + self.send_time = ko.observable(initialValues.send_time); + self.send_time_type = ko.observable(initialValues.send_time_type); + self.start_date = ko.observable(initialValues.start_date); + self.start_date_type = ko.observable(initialValues.start_date_type); + self.start_offset_type = ko.observable(initialValues.start_offset_type); + self.repeat = ko.observable(initialValues.repeat); + self.repeat_every = ko.observable(initialValues.repeat_every); + self.stop_type = ko.observable(initialValues.stop_type); + self.occurrences = ko.observable(initialValues.occurrences); + self.recipient_types = ko.observableArray(initialValues.recipient_types || []); $('#id_schedule-recipient_types').select2(); - self.user_recipients = new recipientsSelect2Handler(select2_user_recipients, - initial_values.user_recipients, 'schedule-user_recipients'); + self.user_recipients = new recipientsSelect2Handler(select2UserRecipients, + initialValues.user_recipients, 'schedule-user_recipients'); self.user_recipients.init(); - self.user_group_recipients = new recipientsSelect2Handler(select2_user_group_recipients, - initial_values.user_group_recipients, 'schedule-user_group_recipients'); + self.user_group_recipients = new recipientsSelect2Handler(select2UserGroupRecipients, + initialValues.user_group_recipients, 'schedule-user_group_recipients'); self.user_group_recipients.init(); - self.user_organization_recipients = new recipientsSelect2Handler(select2_user_organization_recipients, - initial_values.user_organization_recipients, 'schedule-user_organization_recipients'); + self.user_organization_recipients = new recipientsSelect2Handler(select2UserOrganizationRecipients, + initialValues.user_organization_recipients, 'schedule-user_organization_recipients'); self.user_organization_recipients.init(); - self.include_descendant_locations = ko.observable(initial_values.include_descendant_locations); - self.restrict_location_types = ko.observable(initial_values.restrict_location_types); + self.include_descendant_locations = ko.observable(initialValues.include_descendant_locations); + self.restrict_location_types = ko.observable(initialValues.restrict_location_types); - self.location_types = new recipientsSelect2Handler(select2_location_types, - initial_values.location_types, 'schedule-location_types'); + self.location_types = new recipientsSelect2Handler(select2LocationTypes, + initialValues.location_types, 'schedule-location_types'); self.location_types.init(); - self.case_group_recipients = new recipientsSelect2Handler(select2_case_group_recipients, - initial_values.case_group_recipients, 'schedule-case_group_recipients'); + self.case_group_recipients = new recipientsSelect2Handler(select2CaseGroupRecipients, + initialValues.case_group_recipients, 'schedule-case_group_recipients'); self.case_group_recipients.init(); - self.reset_case_property_enabled = ko.observable(initial_values.reset_case_property_enabled); - self.stop_date_case_property_enabled = ko.observable(initial_values.stop_date_case_property_enabled); - self.submit_partially_completed_forms = ko.observable(initial_values.submit_partially_completed_forms); + self.reset_case_property_enabled = ko.observable(initialValues.reset_case_property_enabled); + self.stop_date_case_property_enabled = ko.observable(initialValues.stop_date_case_property_enabled); + self.submit_partially_completed_forms = ko.observable(initialValues.submit_partially_completed_forms); - self.is_trial_project = initial_values.is_trial_project; + self.is_trial_project = initialValues.is_trial_project; self.displayed_email_trial_message = false; - self.content = ko.observable(initial_values.content); - self.standalone_content_form = new ContentViewModel(initial_values.standalone_content_form); + self.content = ko.observable(initialValues.content); + self.standalone_content_form = new ContentViewModel(initialValues.standalone_content_form); self.custom_events = ko.observableArray(); - self.visit_scheduler_app_and_form_unique_id = new formSelect2Handler(current_visit_scheduler_form, + self.visit_scheduler_app_and_form_unique_id = new formSelect2Handler(currentVisitSchedulerForm, 'schedule-visit_scheduler_app_and_form_unique_id', self.timestamp); self.visit_scheduler_app_and_form_unique_id.init(); - self.use_user_data_filter = ko.observable(initial_values.use_user_data_filter); - self.capture_custom_metadata_item = ko.observable(initial_values.capture_custom_metadata_item); - self.editing_custom_immediate_schedule = ko.observable(initial_values.editing_custom_immediate_schedule); + self.use_user_data_filter = ko.observable(initialValues.use_user_data_filter); + self.capture_custom_metadata_item = ko.observable(initialValues.capture_custom_metadata_item); + self.editing_custom_immediate_schedule = ko.observable(initialValues.editing_custom_immediate_schedule); self.create_day_of_month_choice = function (value) { if (value === '-1') { @@ -289,66 +289,66 @@ var CreateScheduleViewModel = function (initial_values, select2_user_recipients, return $.inArray(self.send_frequency(), ['daily', 'weekly', 'monthly', 'custom_daily']) !== -1; }); - self.calculateDailyEndDate = function (start_date_milliseconds, repeat_every, occurrences) { - var milliseconds_in_a_day = 24 * 60 * 60 * 1000; - var days_until_end_date = (occurrences - 1) * repeat_every; - return new Date(start_date_milliseconds + (days_until_end_date * milliseconds_in_a_day)); + self.calculateDailyEndDate = function (startDateMilliseconds, repeatEvery, occurrences) { + var millisecondsInADay = 24 * 60 * 60 * 1000; + var daysUntilEndDate = (occurrences - 1) * repeatEvery; + return new Date(startDateMilliseconds + (daysUntilEndDate * millisecondsInADay)); }; - self.calculateWeeklyEndDate = function (start_date_milliseconds, repeat_every, occurrences) { - var milliseconds_in_a_day = 24 * 60 * 60 * 1000; - var js_start_day_of_week = new Date(start_date_milliseconds).getUTCDay(); - var python_start_day_of_week = (js_start_day_of_week + 6) % 7; - var offset_to_last_weekday_in_schedule = null; + self.calculateWeeklyEndDate = function (startDateMilliseconds, repeatEvery, occurrences) { + var millisecondsInADay = 24 * 60 * 60 * 1000; + var jsStartDayOfWeek = new Date(startDateMilliseconds).getUTCDay(); + var pythonStartDayOfWeek = (jsStartDayOfWeek + 6) % 7; + var offsetToLastWeekdayInSchedule = null; for (var i = 0; i < 7; i++) { - var current_weekday = (python_start_day_of_week + i) % 7; - if (self.weekdays().indexOf(current_weekday.toString()) !== -1) { - offset_to_last_weekday_in_schedule = i; + var currentWeekday = (pythonStartDayOfWeek + i) % 7; + if (self.weekdays().indexOf(currentWeekday.toString()) !== -1) { + offsetToLastWeekdayInSchedule = i; } } - if (offset_to_last_weekday_in_schedule === null) { + if (offsetToLastWeekdayInSchedule === null) { return null; } return new Date( - start_date_milliseconds + - offset_to_last_weekday_in_schedule * milliseconds_in_a_day + - (occurrences - 1) * 7 * repeat_every * milliseconds_in_a_day, + startDateMilliseconds + + offsetToLastWeekdayInSchedule * millisecondsInADay + + (occurrences - 1) * 7 * repeatEvery * millisecondsInADay, ); }; - self.calculateMonthlyEndDate = function (start_date_milliseconds, repeat_every, occurrences) { - var last_day = null; + self.calculateMonthlyEndDate = function (startDateMilliseconds, repeatEvery, occurrences) { + var lastDay = null; self.days_of_month().forEach(function (value) { value = parseInt(value); - if (last_day === null) { - last_day = value; - } else if (last_day > 0) { + if (lastDay === null) { + lastDay = value; + } else if (lastDay > 0) { if (value < 0) { - last_day = value; - } else if (value > last_day) { - last_day = value; + lastDay = value; + } else if (value > lastDay) { + lastDay = value; } } else { - if (value < 0 && value > last_day) { - last_day = value; + if (value < 0 && value > lastDay) { + lastDay = value; } } }); - if (last_day === null) { + if (lastDay === null) { return null; } - var end_date = new Date(start_date_milliseconds); - end_date.setUTCMonth(end_date.getUTCMonth() + (occurrences - 1) * repeat_every); - if (last_day < 0) { - end_date.setUTCMonth(end_date.getUTCMonth() + 1); + var endDate = new Date(startDateMilliseconds); + endDate.setUTCMonth(endDate.getUTCMonth() + (occurrences - 1) * repeatEvery); + if (lastDay < 0) { + endDate.setUTCMonth(endDate.getUTCMonth() + 1); // Using a value of 0 sets it to the last day of the previous month - end_date.setUTCDate(last_day + 1); + endDate.setUTCDate(lastDay + 1); } else { - end_date.setUTCDate(last_day); + endDate.setUTCDate(lastDay); } - return end_date; + return endDate; }; self.calculateOccurrences = function () { @@ -378,29 +378,29 @@ var CreateScheduleViewModel = function (initial_values, select2_user_recipients, }; self.computedEndDate = ko.computed(function () { - var start_date_milliseconds = Date.parse(self.start_date()); - var repeat_every = self.calculateRepeatEvery(); + var startDateMilliseconds = Date.parse(self.start_date()); + var repeatEvery = self.calculateRepeatEvery(); var occurrences = self.calculateOccurrences(); if (self.start_date_type() && self.start_date_type() !== 'SPECIFIC_DATE') { return ''; } - if (isNaN(start_date_milliseconds) || isNaN(occurrences) || isNaN(repeat_every)) { + if (isNaN(startDateMilliseconds) || isNaN(occurrences) || isNaN(repeatEvery)) { return ''; } - var end_date = null; + var endDate = null; if (self.send_frequency() === 'daily') { - end_date = self.calculateDailyEndDate(start_date_milliseconds, repeat_every, occurrences); + endDate = self.calculateDailyEndDate(startDateMilliseconds, repeatEvery, occurrences); } else if (self.send_frequency() === 'weekly') { - end_date = self.calculateWeeklyEndDate(start_date_milliseconds, repeat_every, occurrences); + endDate = self.calculateWeeklyEndDate(startDateMilliseconds, repeatEvery, occurrences); } else if (self.send_frequency() === 'monthly') { - end_date = self.calculateMonthlyEndDate(start_date_milliseconds, repeat_every, occurrences); + endDate = self.calculateMonthlyEndDate(startDateMilliseconds, repeatEvery, occurrences); } - if (end_date) { - return end_date.toJSON().substr(0, 10); + if (endDate) { + return endDate.toJSON().substr(0, 10); } return ''; @@ -432,52 +432,52 @@ var CreateScheduleViewModel = function (initial_values, select2_user_recipients, self.custom_events.push(new CustomEventContainer(id)); }; - self.markCustomEventDeleted = function (event_id) { + self.markCustomEventDeleted = function (eventId) { $.each(self.custom_events(), function (index, value) { - if (value.event_id === event_id) { + if (value.event_id === eventId) { value.eventAndContentViewModel.deleted(true); } }); }; - self.getCustomEventIndex = function (event_id, arr) { - var item_index = null; + self.getCustomEventIndex = function (eventId, arr) { + var itemIndex = null; $.each(arr, function (index, value) { - if (value.event_id === event_id) { - item_index = index; + if (value.event_id === eventId) { + itemIndex = index; } }); - return item_index; + return itemIndex; }; - self.moveCustomEventUp = function (event_id) { - var new_array = self.custom_events(); - var item_index = self.getCustomEventIndex(event_id, new_array); - var swapped_item = null; + self.moveCustomEventUp = function (eventId) { + var newArray = self.custom_events(); + var itemIndex = self.getCustomEventIndex(eventId, newArray); + var swappedItem = null; - while (item_index > 0 && (swapped_item === null || swapped_item.eventAndContentViewModel.deleted())) { - swapped_item = new_array[item_index - 1]; - new_array[item_index - 1] = new_array[item_index]; - new_array[item_index] = swapped_item; - item_index -= 1; + while (itemIndex > 0 && (swappedItem === null || swappedItem.eventAndContentViewModel.deleted())) { + swappedItem = newArray[itemIndex - 1]; + newArray[itemIndex - 1] = newArray[itemIndex]; + newArray[itemIndex] = swappedItem; + itemIndex -= 1; } - self.custom_events(new_array); + self.custom_events(newArray); }; - self.moveCustomEventDown = function (event_id) { - var new_array = self.custom_events(); - var item_index = self.getCustomEventIndex(event_id, new_array); - var swapped_item = null; + self.moveCustomEventDown = function (eventId) { + var newArray = self.custom_events(); + var itemIndex = self.getCustomEventIndex(eventId, newArray); + var swappedItem = null; - while ((item_index < (new_array.length - 1)) && (swapped_item === null || swapped_item.eventAndContentViewModel.deleted())) { - swapped_item = new_array[item_index + 1]; - new_array[item_index + 1] = new_array[item_index]; - new_array[item_index] = swapped_item; - item_index += 1; + while ((itemIndex < (newArray.length - 1)) && (swappedItem === null || swappedItem.eventAndContentViewModel.deleted())) { + swappedItem = newArray[itemIndex + 1]; + newArray[itemIndex + 1] = newArray[itemIndex]; + newArray[itemIndex] = swappedItem; + itemIndex += 1; } - self.custom_events(new_array); + self.custom_events(newArray); }; self.custom_events.subscribe(function (newValue) { @@ -499,23 +499,23 @@ var CreateScheduleViewModel = function (initial_values, select2_user_recipients, self.initDatePicker($("#id_schedule-start_date")); self.setRepeatOptionText(self.send_frequency()); - var custom_events = []; + var customEvents = []; for (var i = 0; i < self.getNextCustomEventIndex(); i++) { - custom_events.push(new CustomEventContainer(i)); + customEvents.push(new CustomEventContainer(i)); } - custom_events.sort(function (item1, item2) { + customEvents.sort(function (item1, item2) { return item1.eventAndContentViewModel.order() - item2.eventAndContentViewModel.order(); }); - self.custom_events(custom_events); + self.custom_events(customEvents); }; }; var baseSelect2Handler = select2Handler.baseSelect2Handler, - recipientsSelect2Handler = function (initial_object_list, initial_comma_separated_list, field) { + recipientsSelect2Handler = function (initialObjectList, initialCommaSeparatedList, field) { /* - * initial_object_list is a list of {id: ..., text: ...} objects representing the initial value + * initialObjectList is a list of {id: ..., text: ...} objects representing the initial value * - * intial_comma_separated_list is a string representation of initial_object_list consisting of just + * intial_comma_separated_list is a string representation of initialObjectList consisting of just * the ids separated by a comma */ var self = baseSelect2Handler({ @@ -528,10 +528,10 @@ var baseSelect2Handler = select2Handler.baseSelect2Handler, }; self.getInitialData = function () { - return initial_object_list; + return initialObjectList; }; - self.value(initial_comma_separated_list); + self.value(initialCommaSeparatedList); return self; }; @@ -539,9 +539,9 @@ var baseSelect2Handler = select2Handler.baseSelect2Handler, recipientsSelect2Handler.prototype = Object.create(recipientsSelect2Handler.prototype); recipientsSelect2Handler.prototype.constructor = recipientsSelect2Handler; -var formSelect2Handler = function (initial_object, field, timestamp) { +var formSelect2Handler = function (initialObject, field, timestamp) { /* - * initial_object is an {id: ..., text: ...} object representing the initial value + * initialObject is an {id: ..., text: ...} object representing the initial value */ var self = baseSelect2Handler({ fieldName: field, @@ -557,10 +557,10 @@ var formSelect2Handler = function (initial_object, field, timestamp) { }; self.getInitialData = function () { - return initial_object; + return initialObject; }; - self.value(initial_object ? initial_object.id : ''); + self.value(initialObject ? initialObject.id : ''); return self; }; From b8bf7d31db71fb37cac2b0432bac6bac3ebe9c81 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Thu, 13 Feb 2025 21:27:16 -0600 Subject: [PATCH 25/34] Save whole html as email content * Add style header for font size classes * Clean head and body separately --- .../components/rich_text_knockout_bindings.js | 22 ++++++++++++++++++- corehq/messaging/scheduling/const.py | 3 --- corehq/messaging/scheduling/forms.py | 15 +++++++++++-- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js index 2d2af91469dc..d2280f00a587 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js +++ b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js @@ -110,7 +110,27 @@ ko.bindingHandlers.richEditor = { if (!isSubscriberChange) { isEditorChange = true; const converter = new QuillDeltaToHtmlConverter(editor.getContents().ops, {}); - const html = converter.convert(); + const body = converter.convert(); + const html = ` + + + + + + ${body} + + + `; valueAccessor()(html); isEditorChange = false; } diff --git a/corehq/messaging/scheduling/const.py b/corehq/messaging/scheduling/const.py index 573cc24ccb2f..a127ba58f558 100644 --- a/corehq/messaging/scheduling/const.py +++ b/corehq/messaging/scheduling/const.py @@ -39,11 +39,8 @@ "tbody", "tr", "td", - "html", - "head", "meta", "title", - "body", "style", } diff --git a/corehq/messaging/scheduling/forms.py b/corehq/messaging/scheduling/forms.py index a25032b927db..1cf33e557e8f 100644 --- a/corehq/messaging/scheduling/forms.py +++ b/corehq/messaging/scheduling/forms.py @@ -495,13 +495,24 @@ def _distill_rich_text_email(self): plaintext_message[lang] = soup.find("body").get_text() except AttributeError: plaintext_message[lang] = strip_tags(content) - html_message[lang] = bleach.clean( - content, + + # bleach.clean throws out html, head and body tags not matter what to keep them we need to clean them + # separately + bleached_head = bleach.clean( + soup.head.decode_contents(), + attributes=ALLOWED_HTML_ATTRIBUTES, + tags=ALLOWED_HTML_TAGS, + css_sanitizer=css_sanitizer, + strip=True, + ) + bleached_body = bleach.clean( + soup.body.decode_contents(), attributes=ALLOWED_HTML_ATTRIBUTES, tags=ALLOWED_HTML_TAGS, css_sanitizer=css_sanitizer, strip=True, ) + html_message[lang] = f"{bleached_head}{bleached_body}" return EmailContent( subject=self.cleaned_data['subject'], message=plaintext_message, From a9d8a56f9a97e8476de779a1a3dbc00ca15d8f5a Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Fri, 14 Feb 2025 10:34:18 -0600 Subject: [PATCH 26/34] Inline styles for emails and add tests --- Gruntfile.js | 1 + .../components/rich_text_knockout_bindings.js | 39 ++-- .../static/hqwebapp/spec/components/main.js | 6 + .../spec/components/rich_text_spec.js | 200 ++++++++++++++++++ .../hqwebapp/spec/components/mocha.html | 3 + 5 files changed, 227 insertions(+), 22 deletions(-) create mode 100644 corehq/apps/hqwebapp/static/hqwebapp/spec/components/main.js create mode 100644 corehq/apps/hqwebapp/static/hqwebapp/spec/components/rich_text_spec.js create mode 100644 corehq/apps/hqwebapp/templates/hqwebapp/spec/components/mocha.html diff --git a/Gruntfile.js b/Gruntfile.js index 7ec6298db353..2aa2c1266588 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -38,6 +38,7 @@ module.exports = function (grunt) { 'cloudcare/form_entry', 'hqwebapp/bootstrap3', 'hqwebapp/bootstrap5', + 'hqwebapp/components', 'case_importer', ]; diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js index d2280f00a587..70e0a4c5c94f 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js +++ b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js @@ -71,6 +71,18 @@ function imageHandler() { input.click(); } +const converterOptions = { + inlineStyles: true, +}; + +function deltaToHtml(delta) { + console.log(JSON.stringify(delta, null, 4)); + const converter = new QuillDeltaToHtmlConverter(delta.ops, converterOptions); + const body = converter.convert(); + const html = `${body}`; + return html; +} + ko.bindingHandlers.richEditor = { init: function (element, valueAccessor) { const fontFamilyArr = [ @@ -109,28 +121,7 @@ ko.bindingHandlers.richEditor = { editor.on("text-change", function () { if (!isSubscriberChange) { isEditorChange = true; - const converter = new QuillDeltaToHtmlConverter(editor.getContents().ops, {}); - const body = converter.convert(); - const html = ` - - - - - - ${body} - - - `; + const html = deltaToHtml(editor.getContents()); valueAccessor()(html); isEditorChange = false; } @@ -149,3 +140,7 @@ ko.bindingHandlers.richEditor = { } }, }; + +export { + deltaToHtml, +}; diff --git a/corehq/apps/hqwebapp/static/hqwebapp/spec/components/main.js b/corehq/apps/hqwebapp/static/hqwebapp/spec/components/main.js new file mode 100644 index 000000000000..b9a7a4a3f971 --- /dev/null +++ b/corehq/apps/hqwebapp/static/hqwebapp/spec/components/main.js @@ -0,0 +1,6 @@ +import hqMocha from "mocha/js/main"; +import "commcarehq"; + +import "hqwebapp/spec/components/rich_text_spec"; + +hqMocha.run(); diff --git a/corehq/apps/hqwebapp/static/hqwebapp/spec/components/rich_text_spec.js b/corehq/apps/hqwebapp/static/hqwebapp/spec/components/rich_text_spec.js new file mode 100644 index 000000000000..c7db19f0eab4 --- /dev/null +++ b/corehq/apps/hqwebapp/static/hqwebapp/spec/components/rich_text_spec.js @@ -0,0 +1,200 @@ +import { deltaToHtml } from "hqwebapp/js/components/rich_text_knockout_bindings"; + +describe('Rich Text Editor', function () { + describe('deltaToHtml', function () { + it('unordered list', function () { + const delta = + { + "ops": [ + { + "insert": "item", + }, + { + "attributes": { + "list": "bullet", + }, + "insert": "\n", + }, + { + "insert": "item", + }, + { + "attributes": { + "list": "bullet", + }, + "insert": "\n", + }, + ], + }; + const html = deltaToHtml(delta); + assert.equal("
  • item
  • item
", html); + }); + + it('ordered list', function () { + const delta = + { + "ops": [ + { + "insert": "item", + }, + { + "attributes": { + "list": "ordered", + }, + "insert": "\n", + }, + { + "insert": "item", + }, + { + "attributes": { + "list": "ordered", + }, + "insert": "\n", + }, + ], + }; + const html = deltaToHtml(delta); + assert.equal("
  1. item
  2. item
", html); + }); + + it('text sizes', function () { + const delta = + { + "ops": [ + { + "attributes": { + "size": "small", + }, + "insert": "small", + }, + { + "insert": "\ndefaul\n", + }, + { + "attributes": { + "size": "large", + }, + "insert": "big", + }, + { + "insert": "\n", + }, + { + "attributes": { + "size": "huge", + }, + "insert": "huge", + }, + { + "insert": "\n", + }, + ], + }; + const html = deltaToHtml(delta); + assert.equal("

small
defaul
" + + "big
huge" + + "

", html); + }); + + it('text align', function () { + const delta = + { + "ops": [ + { + "insert": "left\nright", + }, + { + "attributes": { + "align": "right", + }, + "insert": "\n", + }, + { + "insert": "center", + }, + { + "attributes": { + "align": "center", + }, + "insert": "\n", + }, + { + "insert": "justified", + }, + { + "attributes": { + "align": "justify", + }, + "insert": "\n", + }, + ], + }; + const html = deltaToHtml(delta); + console.log(html); + assert.equal("

left

right

" + + "

center

justified

" + + "", html); + }); + + it('text font', function () { + const delta = + { + "ops": [ + { + "attributes": { + "font": "Arial", + }, + "insert": "Arial", + }, + { + "insert": "\n", + }, + { + "attributes": { + "font": "Times New Roman", + }, + "insert": "Times New Roman", + }, + { + "insert": "\n", + }, + ], + }; + const html = deltaToHtml(delta); + console.log(html); + assert.equal("

Arial
" + + "Times New Roman

", html); + }); + + it('text color', function () { + const delta = + { + "ops": [ + { + "attributes": { + "color": "#e60000", + }, + "insert": "color", + }, + { + "insert": "\n", + }, + { + "attributes": { + "background": "#e60000", + }, + "insert": "background", + }, + { + "insert": "\n", + }, + ], + }; + const html = deltaToHtml(delta); + console.log(html); + assert.equal("

color
" + + "background

", html); + }); + }); +}); diff --git a/corehq/apps/hqwebapp/templates/hqwebapp/spec/components/mocha.html b/corehq/apps/hqwebapp/templates/hqwebapp/spec/components/mocha.html new file mode 100644 index 000000000000..f292f450e80f --- /dev/null +++ b/corehq/apps/hqwebapp/templates/hqwebapp/spec/components/mocha.html @@ -0,0 +1,3 @@ +{% extends "mocha/base.html" %} +{% load hq_shared_tags %} +{% js_entry_b3 "hqwebapp/spec/components/main" %} From 6f6e42391f92ad3e9315b1f60a8965b672f785d8 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Fri, 14 Feb 2025 11:38:38 -0600 Subject: [PATCH 27/34] Don't log --- .../hqwebapp/js/components/rich_text_knockout_bindings.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js index 70e0a4c5c94f..4f6fb1d7af1e 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js +++ b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js @@ -76,7 +76,8 @@ const converterOptions = { }; function deltaToHtml(delta) { - console.log(JSON.stringify(delta, null, 4)); + // nice for adding more test data + // console.log(JSON.stringify(delta, null, 4)); const converter = new QuillDeltaToHtmlConverter(delta.ops, converterOptions); const body = converter.convert(); const html = `${body}`; From 18769017ba5b021eb547043640bbcb1469381ce2 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Fri, 14 Feb 2025 11:39:03 -0600 Subject: [PATCH 28/34] Handle case when html does not have a head element --- corehq/messaging/scheduling/forms.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/corehq/messaging/scheduling/forms.py b/corehq/messaging/scheduling/forms.py index 1cf33e557e8f..70b6af878bf1 100644 --- a/corehq/messaging/scheduling/forms.py +++ b/corehq/messaging/scheduling/forms.py @@ -498,13 +498,15 @@ def _distill_rich_text_email(self): # bleach.clean throws out html, head and body tags not matter what to keep them we need to clean them # separately - bleached_head = bleach.clean( - soup.head.decode_contents(), - attributes=ALLOWED_HTML_ATTRIBUTES, - tags=ALLOWED_HTML_TAGS, - css_sanitizer=css_sanitizer, - strip=True, - ) + bleached_head = "" + if soup.head: + bleached_head = bleach.clean( + soup.head.decode_contents(), + attributes=ALLOWED_HTML_ATTRIBUTES, + tags=ALLOWED_HTML_TAGS, + css_sanitizer=css_sanitizer, + strip=True, + ) bleached_body = bleach.clean( soup.body.decode_contents(), attributes=ALLOWED_HTML_ATTRIBUTES, From a0f26422f936e9507771ee7ffe6aff4485dc98e7 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Fri, 14 Feb 2025 13:04:44 -0600 Subject: [PATCH 29/34] Allow style and class attributes for tags a and p --- corehq/messaging/scheduling/const.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/corehq/messaging/scheduling/const.py b/corehq/messaging/scheduling/const.py index a127ba58f558..cc9310f1cafb 100644 --- a/corehq/messaging/scheduling/const.py +++ b/corehq/messaging/scheduling/const.py @@ -46,9 +46,10 @@ ALLOWED_HTML_ATTRIBUTES = { - 'a': ['href', 'title'], + 'a': ['href', 'title', 'style', 'class'], 'abbr': ['title'], 'acronym': ['title'], + 'p': ['style', 'class'], 'div': ['style', 'class'], 'span': ['style', 'class'], 'img': ['style', 'src', 'width', 'height', 'class'], From d9eca034854260897f9ef11d51af5a33766b8297 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Mon, 17 Feb 2025 11:43:28 -0600 Subject: [PATCH 30/34] Set ordered list type by level as quill does --- .../components/rich_text_knockout_bindings.js | 21 +++++- .../spec/components/rich_text_spec.js | 70 +++++++++++++++++-- 2 files changed, 84 insertions(+), 7 deletions(-) diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js index 4f6fb1d7af1e..14be4cdaf82d 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js +++ b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js @@ -75,12 +75,30 @@ const converterOptions = { inlineStyles: true, }; +const orderedListTypes = ["1", "a", "i"]; +function updateListType(element, level) { + element.childNodes.forEach(function (child) { + if (child.tagName && child.tagName.toLowerCase() === 'ol') { + child.type = orderedListTypes[level % orderedListTypes.length]; + updateListType(child, level + 1); + } else { + updateListType(child, level); + } + }); +} + +const parser = new DOMParser(); + function deltaToHtml(delta) { // nice for adding more test data // console.log(JSON.stringify(delta, null, 4)); const converter = new QuillDeltaToHtmlConverter(delta.ops, converterOptions); const body = converter.convert(); - const html = `${body}`; + + const xmlDoc = parser.parseFromString(body, "text/html"); + updateListType(xmlDoc, 0); + const html = `${xmlDoc.querySelector("body").innerHTML}`; + console.log(html); return html; } @@ -144,4 +162,5 @@ ko.bindingHandlers.richEditor = { export { deltaToHtml, + updateListType, }; diff --git a/corehq/apps/hqwebapp/static/hqwebapp/spec/components/rich_text_spec.js b/corehq/apps/hqwebapp/static/hqwebapp/spec/components/rich_text_spec.js index c7db19f0eab4..6f98409c2130 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/spec/components/rich_text_spec.js +++ b/corehq/apps/hqwebapp/static/hqwebapp/spec/components/rich_text_spec.js @@ -1,6 +1,35 @@ -import { deltaToHtml } from "hqwebapp/js/components/rich_text_knockout_bindings"; +import { deltaToHtml, updateListType } from "hqwebapp/js/components/rich_text_knockout_bindings"; describe('Rich Text Editor', function () { + describe('updateListType', function () { + const parser = new DOMParser(); + + it('add type 1 to first level', function () { + const html = "
  1. item
"; + const xmlDoc = parser.parseFromString(html, "text/html"); + updateListType(xmlDoc, 0); + const updatedHtml = xmlDoc.querySelector("body").innerHTML; + assert.equal('
  1. item
', updatedHtml); + }); + + it('add type 1 to first level for multiple lists', function () { + const html = "
  1. item

p

  1. item
"; + const xmlDoc = parser.parseFromString(html, "text/html"); + updateListType(xmlDoc, 0); + const updatedHtml = xmlDoc.querySelector("body").innerHTML; + assert.equal('
  1. item

p

  1. item
', updatedHtml); + }); + + it('add type a to second level', function () { + const html = "
  1. item
    1. item
"; + const xmlDoc = parser.parseFromString(html, "text/html"); + updateListType(xmlDoc, 0); + const updatedHtml = xmlDoc.querySelector("body").innerHTML; + assert.equal('
  1. item
    1. item
', updatedHtml); + }); + + }); + describe('deltaToHtml', function () { it('unordered list', function () { const delta = @@ -55,7 +84,36 @@ describe('Rich Text Editor', function () { ], }; const html = deltaToHtml(delta); - assert.equal("
  1. item
  2. item
", html); + assert.equal('
  1. item
  2. item
', html); + }); + + it('ordered list indent', function () { + const delta = + { + "ops": [ + { + "insert": "item", + }, + { + "attributes": { + "list": "ordered", + }, + "insert": "\n", + }, + { + "insert": "item", + }, + { + "attributes": { + "indent": 1, + "list": "ordered", + }, + "insert": "\n", + }, + ], + }; + const html = deltaToHtml(delta); + assert.equal('
  1. item
    1. item
', html); }); it('text sizes', function () { @@ -92,8 +150,8 @@ describe('Rich Text Editor', function () { ], }; const html = deltaToHtml(delta); - assert.equal("

small
defaul
" + - "big
huge" + + assert.equal("

small
defaul
" + + "big
huge" + "

", html); }); @@ -163,7 +221,7 @@ describe('Rich Text Editor', function () { }; const html = deltaToHtml(delta); console.log(html); - assert.equal("

Arial
" + + assert.equal("

Arial
" + "Times New Roman

", html); }); @@ -193,7 +251,7 @@ describe('Rich Text Editor', function () { }; const html = deltaToHtml(delta); console.log(html); - assert.equal("

color
" + + assert.equal("

color
" + "background

", html); }); }); From 829b2074ff8e5191ab64b33b9531d84d9cc2ec85 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Mon, 17 Feb 2025 12:45:57 -0600 Subject: [PATCH 31/34] Add http:// if no schema is given --- .../components/rich_text_knockout_bindings.js | 12 +++++++++++ .../spec/components/rich_text_spec.js | 20 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js index 14be4cdaf82d..b271add3f1d7 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js +++ b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js @@ -71,8 +71,19 @@ function imageHandler() { input.click(); } +function linkHandler(value) { + if (value) { + let href = prompt('Enter the URL').trim(); + if (!href.match(/^\w+:/)) { + href = "http://" + href; + } + this.quill.format('link', href); + } +} + const converterOptions = { inlineStyles: true, + linkTarget: "", }; const orderedListTypes = ["1", "a", "i"]; @@ -125,6 +136,7 @@ ko.bindingHandlers.richEditor = { container: toolbar, handlers: { image: imageHandler, + link: linkHandler, }, }, }, diff --git a/corehq/apps/hqwebapp/static/hqwebapp/spec/components/rich_text_spec.js b/corehq/apps/hqwebapp/static/hqwebapp/spec/components/rich_text_spec.js index 6f98409c2130..d551d32bca2e 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/spec/components/rich_text_spec.js +++ b/corehq/apps/hqwebapp/static/hqwebapp/spec/components/rich_text_spec.js @@ -254,5 +254,25 @@ describe('Rich Text Editor', function () { assert.equal("

color
" + "background

", html); }); + + it('link', function () { + const delta = + { + "ops": [ + { + "attributes": { + "link": "https://dimagi.com", + }, + "insert": "link", + }, + { + "insert": "\n", + }, + ], + }; + const html = deltaToHtml(delta); + console.log(html); + assert.equal("

link

", html); + }); }); }); From 1e3380f1ae7ccf5062d06c8297abb44d912b2d39 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Tue, 18 Feb 2025 08:49:42 -0600 Subject: [PATCH 32/34] Use updated version of quill-delta-to-html --- .../components/rich_text_knockout_bindings.js | 2 +- package.json | 2 +- yarn.lock | 37 +++---------------- 3 files changed, 8 insertions(+), 33 deletions(-) diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js index b271add3f1d7..5e498665f4fe 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js +++ b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js @@ -8,7 +8,7 @@ import Snow from "quill/themes/snow"; import Bold from "quill/formats/bold"; import Italic from "quill/formats/italic"; import Header from "quill/formats/header"; -import {QuillDeltaToHtmlConverter} from 'quill-delta-to-html'; +import {QuillDeltaToHtmlConverter} from 'quill-delta-to-html-upate'; import initialPageData from "hqwebapp/js/initial_page_data"; diff --git a/package.json b/package.json index 301a4166742a..a08e7c99a4d3 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "nvd3": "1.1.10", "nvd3-1.8.6": "npm:nvd3#1.8.6", "quill": "2.0.3", - "quill-delta-to-html": "^0.12.1", + "quill-delta-to-html-upate": "^0.13.0", "requirejs": "2.3.7", "requirejs-babel7": "^1.3.2", "select2": "4.1.0-rc.0", diff --git a/yarn.lock b/yarn.lock index 56d7732805ee..15ea4e885b1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6626,10 +6626,10 @@ quickselect@^2.0.0: resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018" integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw== -quill-delta-to-html@^0.12.1: - version "0.12.1" - resolved "https://registry.yarnpkg.com/quill-delta-to-html/-/quill-delta-to-html-0.12.1.tgz#48e042c4a14d9ba3eed8b4f28aa77dd9482eea00" - integrity sha512-QhpeMk9+5ge3HYbL5A0Ewz3pXCsbemqGvIF/kw5D6D4V68AtcUp7yt9xNUkzOk/0IQz43hKy3IkzBzRhLIE+oA== +quill-delta-to-html-upate@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/quill-delta-to-html-upate/-/quill-delta-to-html-upate-0.13.0.tgz#ff37eec7276126d96e320afbfa041ecc67d069d1" + integrity sha512-1uJRFq2NO0sl9lO/R0wd00ewPSaX5+hpM+G4M+XSVd6dEuqSwpVP+yGrhQ8bnvzTx6oeURnwXxV9VrE5o19phQ== dependencies: lodash.isequal "^4.5.0" @@ -7284,16 +7284,7 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0= -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -7339,7 +7330,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -7353,13 +7344,6 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -8015,15 +7999,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From fd438ddb5e6d2e19a204f937f32939ad401f1724 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Tue, 18 Feb 2025 16:02:37 -0600 Subject: [PATCH 33/34] Add tests, remove log --- .../components/rich_text_knockout_bindings.js | 4 +- .../spec/components/rich_text_spec.js | 64 +++++++++++++++++-- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js index 5e498665f4fe..76fea3907899 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js +++ b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js @@ -103,13 +103,15 @@ const parser = new DOMParser(); function deltaToHtml(delta) { // nice for adding more test data // console.log(JSON.stringify(delta, null, 4)); + if (!delta) { + return + } const converter = new QuillDeltaToHtmlConverter(delta.ops, converterOptions); const body = converter.convert(); const xmlDoc = parser.parseFromString(body, "text/html"); updateListType(xmlDoc, 0); const html = `${xmlDoc.querySelector("body").innerHTML}`; - console.log(html); return html; } diff --git a/corehq/apps/hqwebapp/static/hqwebapp/spec/components/rich_text_spec.js b/corehq/apps/hqwebapp/static/hqwebapp/spec/components/rich_text_spec.js index d551d32bca2e..26ba9cadddd5 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/spec/components/rich_text_spec.js +++ b/corehq/apps/hqwebapp/static/hqwebapp/spec/components/rich_text_spec.js @@ -28,6 +28,14 @@ describe('Rich Text Editor', function () { assert.equal('
  1. item
    1. item
', updatedHtml); }); + it("handle invalid html", function () { + const html = "
  1. item
  2. item
"; + const xmlDoc = parser.parseFromString(html, "text/html"); + updateListType(xmlDoc, 0); + const updatedHtml = xmlDoc.querySelector("body").innerHTML; + assert.equal('
  1. item
  2. item
', updatedHtml); + }); + }); describe('deltaToHtml', function () { @@ -189,7 +197,6 @@ describe('Rich Text Editor', function () { ], }; const html = deltaToHtml(delta); - console.log(html); assert.equal("

left

right

" + "

center

justified

" + "", html); @@ -220,7 +227,6 @@ describe('Rich Text Editor', function () { ], }; const html = deltaToHtml(delta); - console.log(html); assert.equal("

Arial
" + "Times New Roman

", html); }); @@ -250,7 +256,6 @@ describe('Rich Text Editor', function () { ], }; const html = deltaToHtml(delta); - console.log(html); assert.equal("

color
" + "background

", html); }); @@ -271,8 +276,59 @@ describe('Rich Text Editor', function () { ], }; const html = deltaToHtml(delta); - console.log(html); assert.equal("

link

", html); }); + + it('should handle missing ops', function () { + const delta = {}; + const html = deltaToHtml(delta); + assert.equal("", html); + }); + + it('should handle ops without insert', function () { + const delta = { + "ops": [ + { + "attributes": { + "color": "#e60000", + }, + }, + ], + }; + const html = deltaToHtml(delta); + assert.equal("", html); + }); + + it('should handle invalid color codes', function () { + const delta = { + "ops": [ + { + "attributes": { + "color": "invalid-color", + }, + "insert": "text", + }, + {"insert": "\n"}, + ], + }; + const html = deltaToHtml(delta); + assert.equal("

text

", html); + }); + + it('should sanitize malicious links', function () { + const delta = { + "ops": [ + { + "attributes": { + "link": "javascript:alert('xss')", + }, + "insert": "malicious link", + }, + {"insert": "\n"}, + ], + }; + const html = deltaToHtml(delta); + assert.equal('

malicious link

', html); + }); }); }); From 55ee242b06b14c9d638b9784945cee3329708c38 Mon Sep 17 00:00:00 2001 From: Martin Riese Date: Tue, 18 Feb 2025 16:21:06 -0600 Subject: [PATCH 34/34] Add spinner, better error messages --- .../js/components/rich_text_knockout_bindings.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js index 76fea3907899..5b37df0ac759 100644 --- a/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js +++ b/corehq/apps/hqwebapp/static/hqwebapp/js/components/rich_text_knockout_bindings.js @@ -31,6 +31,7 @@ function imageHandler() { let formData = new FormData(); formData.append("upload", file, file.name); + const spinner = $('
').appendTo('body'); fetch(uploadUrl, { method: "POST", body: formData, @@ -42,11 +43,10 @@ function imageHandler() { if (!response.ok) { if (response.status === 400) { return response.json().then(function (errorJson) { - throw Error(errorJson.error.message); + throw Error(gettext('Failed to upload image: ') + errorJson.error.message); }); } - - throw Error("Error uploading image"); + throw Error(gettext('Failed to upload image. Please try again.')); } return response.json(); }) @@ -65,7 +65,10 @@ function imageHandler() { ); }) .catch(function (error) { - alert(error); + alert(error.message || gettext('Failed to upload image. Please try again.')); + }) + .finally(function () { + spinner.remove(); }); }; input.click();