From 92039201be19cb0bb3ba80a76f1f40e42db2bc11 Mon Sep 17 00:00:00 2001 From: Ryan Wold <64987852+ryanwoldatwork@users.noreply.github.com> Date: Wed, 9 Apr 2025 18:53:47 -0700 Subject: [PATCH] load quill css with a link tag * update quill text counter * use localStorge for quill text areas * don't outline quill inputs * validate quill text --- app/helpers/application_helper.rb | 2 +- app/views/admin/questions/_form.html.erb | 2 +- .../question_types/_rich_textarea.html.erb | 18 ++++++-- app/views/components/widget/_fba.js.erb | 46 ++++++++++++++++--- app/views/components/widget/_widget.css.erb | 6 +-- spec/features/touchpoints_spec.rb | 26 +++++++++++ 6 files changed, 85 insertions(+), 15 deletions(-) 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:) }