diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 9fec0a999..368027753 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -150,7 +150,7 @@ def question_type_javascript_params(question)
elsif question.question_type == 'textarea'
"form.querySelector(\"##{question.ui_selector}\") && form.querySelector(\"##{question.ui_selector}\").value"
elsif question.question_type == 'rich_textarea'
- "form.querySelector(\"##{question.ui_selector}\") && form.querySelector(\"##{question.ui_selector}\").value"
+ "form.querySelector(\"#hidden-#{question.ui_selector}\") && form.querySelector(\"#hidden-#{question.ui_selector}\").value" # Quill stores rich input text in hidden fields
elsif question.question_type == 'radio_buttons'
"form.querySelector(\"input[name=#{question.ui_selector}]:checked\") && form.querySelector(\"input[name=#{question.ui_selector}]:checked\").value"
elsif question.question_type == 'star_radio_buttons'
diff --git a/app/views/admin/questions/_form.html.erb b/app/views/admin/questions/_form.html.erb
index 01a8026ae..58d243705 100644
--- a/app/views/admin/questions/_form.html.erb
+++ b/app/views/admin/questions/_form.html.erb
@@ -49,7 +49,7 @@
<%= f.text_field :placeholder_text, class: "usa-input" %>
<% end %>
- <%- if ["text_field", "textarea", "text_email_field","text_phone_field"].include?(question.question_type) %>
+ <%- if ["text_field", "textarea", "rich_textarea", "text_email_field", "text_phone_field"].include?(question.question_type) %>
<%= f.label :character_limit, class: "usa-label" %>
<%= f.number_field :character_limit, class: "usa-input", max: Question::MAX_CHARACTERS %>
diff --git a/app/views/components/forms/question_types/_rich_textarea.html.erb b/app/views/components/forms/question_types/_rich_textarea.html.erb
index bfff16c14..b36eb6753 100644
--- a/app/views/components/forms/question_types/_rich_textarea.html.erb
+++ b/app/views/components/forms/question_types/_rich_textarea.html.erb
@@ -16,10 +16,22 @@
<%= render 'components/question_title_label', question: question %>
+ name="hidden-<%= question.ui_selector %>"
+ <%- if question.is_required %>
+ required="true"
+ <% end %>
+ id="hidden-<%= question.ui_selector %>">
+
0 %>
+ >
+ <%= question.max_length %> characters allowed
+
diff --git a/app/views/components/widget/_fba.js.erb b/app/views/components/widget/_fba.js.erb
index 865902d96..997362a33 100644
--- a/app/views/components/widget/_fba.js.erb
+++ b/app/views/components/widget/_fba.js.erb
@@ -27,9 +27,6 @@ function FBAform(d, N) {
},
init: function(options) {
this.javascriptIsEnabled();
- <%- if form.has_rich_text_questions? %>
- this.loadQuill();
- <% end %>
this.options = options;
if (this.options.loadCSS) {
this._loadCss();
@@ -38,6 +35,7 @@ function FBAform(d, N) {
if (!this.options.suppressUI && (this.options.deliveryMethod && this.options.deliveryMethod === 'modal')) {
this.loadButton();
}
+ this.enableLocalStorage();
this._bindEventListeners();
this.successState = false; // initially false
this._pagination();
@@ -47,7 +45,9 @@ function FBAform(d, N) {
<%- if form.enable_turnstile? %>
this.loadTurnstile();
<% end %>
- this.enableLocalStorage();
+ <%- if form.has_rich_text_questions? %>
+ this.loadQuill();
+ <% end %>
d.dispatchEvent(new CustomEvent('onTouchpointsFormLoaded', {
detail: {
formComponent: this
@@ -58,7 +58,7 @@ function FBAform(d, N) {
_bindEventListeners: function() {
var self = this;
- const textareas = this.formComponent().querySelectorAll(".usa-textarea");
+ const textareas = this.formComponent().querySelectorAll(".usa-textarea, .ql-editor");
textareas.forEach(function(textarea) {
if (textarea.getAttribute("maxlength") != '0' && textarea.getAttribute("maxlength") != '10000') {
textarea.addEventListener("keyup", self.textCounter);
@@ -79,6 +79,12 @@ function FBAform(d, N) {
var style = d.createElement('style');
style.innerHTML = this.options.css;
d.head.appendChild(style);
+ <%- if form.has_rich_text_questions? %>
+ var quillStyles = d.createElement('link');
+ quillStyles.setAttribute("href", "<%= asset_path('quill-snow.css') %>")
+ quillStyles.setAttribute("rel", "stylesheet")
+ d.head.appendChild(quillStyles);
+ <% end %>
}
},
_loadHtml: function() {
@@ -250,7 +256,9 @@ function FBAform(d, N) {
if (item.selectedIndex > 0) delete(questions[item.name]);
break;
default:
- if (item.value.length > 0) delete(questions[item.name]);
+ const quillDefaultHTML = "
";
+ if (item.value.length > 0 &&
+ item.value != quillDefaultHTML) delete(questions[item.name]);
}
});
for (var key in questions) {
@@ -671,6 +679,8 @@ function FBAform(d, N) {
document.querySelectorAll(".quill").forEach((wrapper) => {
const editorContainer = wrapper.querySelector(".editor");
const hiddenInput = wrapper.querySelector("input[type=hidden]");
+ const countDisplay = wrapper.querySelector('.usa-character-count__message');
+ const maxLimit = editorContainer.getAttribute("maxlength");
if (editorContainer && hiddenInput) {
const quill = new Quill(editorContainer, {
@@ -684,12 +694,19 @@ function FBAform(d, N) {
}
});
+ quill.root.innerHTML = hiddenInput.value; // Restore values
+
// Sync to hidden field on change
quill.on('text-change', function () {
+ updateCount();
hiddenInput.value = quill.root.innerHTML;
});
- }
+ const updateCount = () => {
+ const html = quill.root.innerHTML;
+ countDisplay.textContent = "" + (maxLimit - html.length) + " <%= t :characters_left %>";
+ };
+ }
});
},
<% end %>
@@ -732,6 +749,21 @@ function FBAform(d, N) {
<%# Save data to localStorage as the user types %>
form.addEventListener('input', (event) => {
const inputData = {};
+
+ <%- if form.has_rich_text_questions? %>
+ document.querySelectorAll(".quill").forEach((wrapper) => {
+ const editorContainer = wrapper.querySelector(".editor");
+ const hiddenInput = wrapper.querySelector("input[type=hidden]");
+
+ if (editorContainer && hiddenInput) {
+ const quillInstance = Quill.find(editorContainer);
+ if (quillInstance) {
+ hiddenInput.value = quillInstance.root.innerHTML;
+ }
+ }
+ });
+ <% end %>
+
const formData = new FormData(form);
formData.forEach((value, key) => {
inputData[key] = value;
diff --git a/app/views/components/widget/_widget.css.erb b/app/views/components/widget/_widget.css.erb
index 25a1b3f44..38de4891f 100644
--- a/app/views/components/widget/_widget.css.erb
+++ b/app/views/components/widget/_widget.css.erb
@@ -1077,6 +1077,6 @@
max-width: 100%;
}
-<%- if form.has_rich_text_questions? %>
- <%= File.read(Rails.root.join("app/assets/stylesheets/quill-snow.css")) %>
-<% end %>
+.ql-editor[contentEditable=true]:focus {
+ outline: none;
+}
\ No newline at end of file
diff --git a/spec/features/touchpoints_spec.rb b/spec/features/touchpoints_spec.rb
index 4ee85b3a4..de21f0ce7 100644
--- a/spec/features/touchpoints_spec.rb
+++ b/spec/features/touchpoints_spec.rb
@@ -272,6 +272,32 @@
end
end
+ describe 'rich text question' do
+ let(:rich_text_form) { FactoryBot.create(:form, organization:) }
+ let!(:rich_text_question_1) { FactoryBot.create(:question, form: rich_text_form, position: 1, form_section: rich_text_form.form_sections.first, question_type: "rich_textarea", answer_field: 'answer_01', text: "Q1" ) }
+ let!(:rich_text_question_2) { FactoryBot.create(:question, form: rich_text_form, position: 2, form_section: rich_text_form.form_sections.first, question_type: "rich_textarea", answer_field: 'answer_02', text: "Q2", is_required: true) }
+ let!(:rich_text_question_3) { FactoryBot.create(:question, form: rich_text_form, position: 3, form_section: rich_text_form.form_sections.first, question_type: "rich_textarea", answer_field: 'answer_03', text: "Q3", character_limit: 300) }
+
+ before do
+ visit touchpoint_path(rich_text_form)
+ find("##{rich_text_question_1.ui_selector} .ql-editor").send_keys("some text goes here")
+ find("##{rich_text_question_2.ui_selector}").click
+ end
+
+ it 'persists rich text values from localStorage' do
+ expect(find("#hidden-#{rich_text_question_1.ui_selector}", visible: false).value).to eq("some text goes here
")
+ visit touchpoint_path(rich_text_form)
+ expect(find("#hidden-#{rich_text_question_1.ui_selector}", visible: false).value).to eq("some text goes here
")
+ find("##{rich_text_question_3.ui_selector} .ql-editor").send_keys("some more text goes here")
+ expect(page).to have_content("269 characters left")
+ click_on "Submit"
+ expect(page).to have_content("A response is required: Q2")
+ find("##{rich_text_question_2.ui_selector} .ql-editor").send_keys("okay now")
+ click_on "Submit"
+ expect(page).to have_content("Thank you. Your feedback has been received.")
+ end
+ end
+
describe 'states dropdown question' do
let!(:dropdown_form) { FactoryBot.create(:form, :states_dropdown_form, organization:) }