-
-
Notifications
You must be signed in to change notification settings - Fork 221
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
base: master
Are you sure you want to change the base?
Changes from all commits
1177506
f1cf6ee
83ca7db
d94063b
6c39f61
74f7e26
ddeffd0
33292db
552c115
1afde94
98f30d0
720cc1e
e92c7ff
e130c22
3d5f51e
bcbe4ef
a3b2e24
4580f02
45df21f
48c29bf
c060d26
8f91064
235f478
1be6a8b
24575ec
df4933a
af7366e
b17d802
691caa1
b8bf7d3
a9d8a56
6f6e423
1876901
a0f2642
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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"; | ||
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 = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 incorehq/messaging/scheduling/static/scheduling/css
and I could not figure out why. Let me try not that it is n hqwebappThere was a problem hiding this comment.
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.