Skip to content
Merged

0.383.0 #1842

Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion app/views/admin/questions/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
<%= f.text_field :placeholder_text, class: "usa-input" %>
</div>
<% 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) %>
<div class="field character-limit">
<%= f.label :character_limit, class: "usa-label" %>
<%= f.number_field :character_limit, class: "usa-input", max: Question::MAX_CHARACTERS %>
Expand Down
18 changes: 15 additions & 3 deletions app/views/components/forms/question_types/_rich_textarea.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,22 @@
<%= render 'components/question_title_label', question: question %>
<input
type="hidden"
name="my_model[rich_text]"
id="<%= question.ui_selector %>">
name="hidden-<%= question.ui_selector %>"
<%- if question.is_required %>
required="true"
<% end %>
id="hidden-<%= question.ui_selector %>">
<div
id="quill-editor-<%= question.ui_selector %>"
id="<%= question.ui_selector %>"
class="editor"
name="<%= question.ui_selector %>"
maxlength="<%= question.max_length %>"
style="min-height: 100px;"></div>
<span
class="counter-msg usa-hint usa-character-count__message"
aria-live="polite"
<%= "hidden=true" unless question.character_limit && question.character_limit > 0 %>
>
<%= question.max_length %> characters allowed
</span>
</div>
46 changes: 39 additions & 7 deletions app/views/components/widget/_fba.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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
Expand All @@ -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);
Expand All @@ -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() {
Expand Down Expand Up @@ -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 = "<p><br></p>";
if (item.value.length > 0 &&
item.value != quillDefaultHTML) delete(questions[item.name]);
}
});
for (var key in questions) {
Expand Down Expand Up @@ -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, {
Expand All @@ -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 %>
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions app/views/components/widget/_widget.css.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
26 changes: 26 additions & 0 deletions spec/features/touchpoints_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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("<p>some text goes here</p>")
visit touchpoint_path(rich_text_form)
expect(find("#hidden-#{rich_text_question_1.ui_selector}", visible: false).value).to eq("<p>some text goes here</p>")
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:) }

Expand Down