Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use QuillJS as rich text editor #35695

Draft
wants to merge 34 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1177506
Use quill for rich editor
MartinRiese Jan 27, 2025
f1cf6ee
Additional formatting options
MartinRiese Jan 29, 2025
83ca7db
Custom font set for quill
MartinRiese Jan 30, 2025
d94063b
Add image upload functionality to quill
MartinRiese Feb 3, 2025
6c39f61
Move editor knockout binding to shared file
MartinRiese Feb 5, 2025
74f7e26
Because and indent of 2 is prettier?
MartinRiese Feb 5, 2025
ddeffd0
Use global _quill.sccs
MartinRiese Feb 5, 2025
33292db
Round quill editor corners to match style of forms
MartinRiese Feb 5, 2025
552c115
Add missing fonts
MartinRiese Feb 5, 2025
1afde94
Revert "Use global _quill.sccs"
MartinRiese Feb 5, 2025
98f30d0
Add font to font drop down items
MartinRiese Feb 5, 2025
720cc1e
Fix lint issues
MartinRiese Feb 6, 2025
e92c7ff
Use quill handler instead of listening directly
MartinRiese Feb 7, 2025
e130c22
Just use css file
MartinRiese Feb 7, 2025
3d5f51e
Add help link to rich text FF
MartinRiese Feb 10, 2025
bcbe4ef
Merge branch 'master' into riese/quill
MartinRiese Feb 11, 2025
a3b2e24
Fix quill header menu paragraph option
MartinRiese Feb 11, 2025
4580f02
Allow H1 headings
MartinRiese Feb 11, 2025
45df21f
Use quill-delta-to-html to render HTML
MartinRiese Feb 12, 2025
48c29bf
Merge branch 'riese/quill' of github.com:dimagi/commcare-hq into ries…
MartinRiese Feb 12, 2025
c060d26
Merge branch 'master' into riese/quill
orangejenny Feb 12, 2025
8f91064
Fix alignment button
MartinRiese Feb 13, 2025
235f478
Do not need check box list.
MartinRiese Feb 13, 2025
1be6a8b
Merge branch 'riese/quill' of github.com:dimagi/commcare-hq into ries…
MartinRiese Feb 13, 2025
24575ec
Handle error when uploading image
MartinRiese Feb 13, 2025
df4933a
Add title to all buttons in the toolbar
MartinRiese Feb 13, 2025
af7366e
making the linter happy
MartinRiese Feb 13, 2025
b17d802
Eradicate the dust bunnies
MartinRiese Feb 13, 2025
691caa1
Merge branch 'master' into riese/quill
MartinRiese Feb 13, 2025
b8bf7d3
Save whole html as email content
MartinRiese Feb 14, 2025
a9d8a56
Inline styles for emails and add tests
MartinRiese Feb 14, 2025
6f6e423
Don't log
MartinRiese Feb 14, 2025
1876901
Handle case when html does not have a head element
MartinRiese Feb 14, 2025
a0f2642
Allow style and class attributes for tags a and p
MartinRiese Feb 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* globals module, process, require */
'use strict';

Check warning on line 2 in Gruntfile.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

'use strict' is unnecessary inside of modules
module.exports = function (grunt) {
var headless = require('mocha-headless-chrome'),
_ = require('lodash'),
Expand Down Expand Up @@ -38,6 +38,7 @@
'cloudcare/form_entry',
'hqwebapp/bootstrap3',
'hqwebapp/bootstrap5',
'hqwebapp/components',
'case_importer',
];

Expand Down Expand Up @@ -91,7 +92,7 @@
error && grunt.log.write(error));
}
fs.writeFile(filePath, JSON.stringify(data.coverage), { flag: 'w+' }, error =>
error && grunt.log.write(error)

Check warning on line 95 in Gruntfile.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

Missing trailing comma
);
}
finishedTests.push(currentApp);
Expand All @@ -99,7 +100,7 @@
_.without(queuedTests, currentApp),
taskPromise,
finishedTests,
failures

Check warning on line 103 in Gruntfile.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

Missing trailing comma
);
});
};
Expand Down Expand Up @@ -134,7 +135,7 @@
var testStatement = "Running tests: " + paths.join(', ');
grunt.log.writeln(testStatement.bold.green);
runTest(paths, this.async());
}

Check warning on line 138 in Gruntfile.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

Missing trailing comma
);

grunt.registerTask('list', 'Lists all available apps to test', function () {
Expand Down
50 changes: 50 additions & 0 deletions corehq/apps/hqwebapp/static/hqwebapp/js/components/quill.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
.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;
}

.ql-picker-item[data-value="Ariel"] {
font-family: "Arial", sans-serif;
}

.ql-picker-item[data-value="Courier New"] {
font-family: "Courier New", sans-serif;
}

.ql-picker-item[data-value="Georgia"] {
font-family: "Georgia", serif;
}

.ql-picker-item[data-value="Lucida Sans Unicode"] {
font-family: "Lucida Sans Unicode", sans-serif;
}

.ql-picker-item[data-value="Tahoma"] {
font-family: "Tahoma", sans-serif;
}

.ql-picker-item[data-value="Times New Roman"] {
font-family: "Times New Roman", serif;
}

.ql-picker-item[data-value="Verdana"] {
font-family: "Verdana", sans-serif;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import ko from "knockout";

import "quill/dist/quill.snow.css";
import "hqwebapp/js/components/quill.css";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i find it strange that a component's css is living in the js folder

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally I had the binding in corehq/messaging/scheduling/static/scheduling/js/create_schedule.js and webpack would not find the css file in corehq/messaging/scheduling/static/scheduling/css and I could not figure out why. Let me try not that it is n hqwebapp

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what I am doing wrong. If you can tell me how to reference it once moved I am happy to do it.

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 {QuillDeltaToHtmlConverter} from 'quill-delta-to-html';

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,
});

function imageHandler() {
const self = this;
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) {
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) {
const Delta = Quill.import("delta");
const selectionRange = self.quill.getSelection(true);
self.quill.updateContents(
new Delta()
.retain(selectionRange.index)
.delete(selectionRange.length)
.insert({
image: data.url,
}, {
alt: file.name,
}),
);
})
.catch(function (error) {
alert(error);
});
};
input.click();
}

const converterOptions = {
inlineStyles: true,
};

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 = `<html><body>${body}</body></html>`;
return html;
}

ko.bindingHandlers.richEditor = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can use alpine js on HQ now. Can you please consider looking at ways to extend alpine as an alternative to creating a new knockout binding handler? This is creating more future tech debt, and I really want to discourage this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is fair. I had not even thought about alpine without htmx. Is there another place where we use it in this way, that I can have a look at?

init: function (element, valueAccessor) {
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);

const toolbar = element.parentElement.querySelector("#ql-toolbar");
const editor = new Quill(element, {
modules: {
toolbar: {
container: toolbar,
handlers: {
image: imageHandler,
},
},
},
theme: "snow",
});

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;
const html = deltaToHtml(editor.getContents());
valueAccessor()(html);
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);
}
},
};

export {
deltaToHtml,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import hqMocha from "mocha/js/main";
import "commcarehq";

import "hqwebapp/spec/components/rich_text_spec";

hqMocha.run();
Loading