From 906eea188ef13e9aacb9fae75989426732db391e Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Wed, 18 May 2022 12:41:01 +0200 Subject: [PATCH] refactor(stimulus): use stimulus in message forms --- app/components/dossiers/message_component.rb | 16 ++++- .../message_component.html.haml | 2 +- app/javascript/controllers/index.ts | 4 ++ .../controllers/persisted_form_controller.ts | 59 +++++++++++++++++++ .../controllers/scroll_to_controller.ts | 39 ++++++++++++ app/javascript/new_design/messagerie.js | 58 ------------------ app/javascript/packs/application.js | 1 - app/javascript/shared/utils.ts | 17 ------ app/views/experts/avis/instruction.html.haml | 4 +- app/views/experts/shared/avis/_form.html.haml | 4 +- .../instructeurs/shared/avis/_form.html.haml | 4 +- .../shared/dossiers/_messagerie.html.haml | 2 +- .../shared/dossiers/messages/_form.html.haml | 4 +- 13 files changed, 127 insertions(+), 87 deletions(-) create mode 100644 app/javascript/controllers/persisted_form_controller.ts create mode 100644 app/javascript/controllers/scroll_to_controller.ts delete mode 100644 app/javascript/new_design/messagerie.js diff --git a/app/components/dossiers/message_component.rb b/app/components/dossiers/message_component.rb index 8e2a185a1..23c959bcb 100644 --- a/app/components/dossiers/message_component.rb +++ b/app/components/dossiers/message_component.rb @@ -15,7 +15,15 @@ class Dossiers::MessageComponent < ApplicationComponent end def highlight_if_unseen_class - helpers.highlight_if_unseen_class(@messagerie_seen_at, commentaire.created_at) + if highlight? + 'highlighted' + end + end + + def scroll_to_target + if highlight? + { scroll_to_target: 'to' } + end end def icon_path @@ -55,4 +63,10 @@ class Dossiers::MessageComponent < ApplicationComponent sanitize(body_formatted) end end + + private + + def highlight? + commentaire.created_at.present? && @messagerie_seen_at&.<(commentaire.created_at) + end end diff --git a/app/components/dossiers/message_component/message_component.html.haml b/app/components/dossiers/message_component/message_component.html.haml index 622e4d09e..1e75d85b4 100644 --- a/app/components/dossiers/message_component/message_component.html.haml +++ b/app/components/dossiers/message_component/message_component.html.haml @@ -6,7 +6,7 @@ = commentaire_issuer - if commentaire_from_guest? %span.guest= t('.guest') - %span.date{ class: highlight_if_unseen_class } + %span.date{ class: highlight_if_unseen_class, data: scroll_to_target } = commentaire_date .rich-text= commentaire_body diff --git a/app/javascript/controllers/index.ts b/app/javascript/controllers/index.ts index f235d0069..d5a98251d 100644 --- a/app/javascript/controllers/index.ts +++ b/app/javascript/controllers/index.ts @@ -4,7 +4,9 @@ import { AutosaveController } from './autosave_controller'; import { AutosaveStatusController } from './autosave_status_controller'; import { GeoAreaController } from './geo_area_controller'; import { MenuButtonController } from './menu_button_controller'; +import { PersistedFormController } from './persisted_form_controller'; import { ReactController } from './react_controller'; +import { ScrollToController } from './scroll_to_controller'; import { TurboEventController } from './turbo_event_controller'; import { TurboInputController } from './turbo_input_controller'; import { TurboPollController } from './turbo_poll_controller'; @@ -14,7 +16,9 @@ Stimulus.register('autosave-status', AutosaveStatusController); Stimulus.register('autosave', AutosaveController); Stimulus.register('geo-area', GeoAreaController); Stimulus.register('menu-button', MenuButtonController); +Stimulus.register('persisted-form', PersistedFormController); Stimulus.register('react', ReactController); +Stimulus.register('scroll-to', ScrollToController); Stimulus.register('turbo-event', TurboEventController); Stimulus.register('turbo-input', TurboInputController); Stimulus.register('turbo-poll', TurboPollController); diff --git a/app/javascript/controllers/persisted_form_controller.ts b/app/javascript/controllers/persisted_form_controller.ts new file mode 100644 index 000000000..f6714848d --- /dev/null +++ b/app/javascript/controllers/persisted_form_controller.ts @@ -0,0 +1,59 @@ +import { ApplicationController } from './application_controller'; + +export class PersistedFormController extends ApplicationController { + static values = { + key: String + }; + + declare readonly keyValue: string; + + connect() { + this.on('submit', () => this.onSubmit()); + this.on('input', () => this.debounce(this.onInput, 500)); + + this.restoreInputValues(); + } + + onSubmit() { + try { + for (const input of this.inputs) { + localStorage.removeItem(this.storageKey(input.name)); + } + } catch (error) { + console.error(error); + } + } + + onInput() { + try { + for (const input of this.inputs) { + localStorage.setItem(this.storageKey(input.name), input.value); + } + } catch (error) { + console.error(error); + } + } + + private restoreInputValues() { + try { + for (const input of this.inputs) { + const value = localStorage.getItem(this.storageKey(input.name)); + if (value) { + input.value = value; + } + } + } catch (error) { + console.error(error); + } + } + + private get inputs() { + return this.element.querySelectorAll< + HTMLInputElement | HTMLTextAreaElement + >('input[type="text"], input[type="email"], textarea'); + } + + private storageKey(name: string) { + return `persisted-value-${this.keyValue}:${name}`; + } +} diff --git a/app/javascript/controllers/scroll_to_controller.ts b/app/javascript/controllers/scroll_to_controller.ts new file mode 100644 index 000000000..ba9a9a878 --- /dev/null +++ b/app/javascript/controllers/scroll_to_controller.ts @@ -0,0 +1,39 @@ +import { ApplicationController } from './application_controller'; + +export class ScrollToController extends ApplicationController { + static targets = ['to']; + declare readonly toTarget: HTMLInputElement; + declare readonly hasToTarget: boolean; + + connect() { + if (this.hasToTarget) { + this.scrollToElement(); + } else { + this.scrollToBottom(); + } + } + + private scrollTo(top: number) { + this.element.scrollTop = top; + } + + private scrollToElement() { + this.scrollTo( + offset(this.toTarget).top - + offset(this.element).top + + this.element.scrollTop + ); + } + + private scrollToBottom() { + this.scrollTo(this.element.scrollHeight); + } +} + +function offset(element: Element) { + const rect = element.getBoundingClientRect(); + return { + top: rect.top + document.body.scrollTop, + left: rect.left + document.body.scrollLeft + }; +} diff --git a/app/javascript/new_design/messagerie.js b/app/javascript/new_design/messagerie.js deleted file mode 100644 index 6f1a010da..000000000 --- a/app/javascript/new_design/messagerie.js +++ /dev/null @@ -1,58 +0,0 @@ -import { scrollTo, scrollToBottom } from '@utils'; - -function scrollMessagerie() { - const ul = document.querySelector('.messagerie ul'); - - if (ul) { - const elementToScroll = document.querySelector('.date.highlighted'); - - if (elementToScroll) { - scrollTo(ul, elementToScroll); - } else { - scrollToBottom(ul); - } - } -} - -function saveMessageContent() { - const commentaireForms = Array.from( - document.querySelectorAll('form[data-persisted-content-id]') - ); - - if (commentaireForms.length) { - const commentaireInputs = Array.from( - document.querySelectorAll('.persisted-input') - ); - - const persistedContentIds = commentaireForms.map( - (form) => form.dataset.persistedContentId - ); - - const keys = persistedContentIds.map((key) => `persisted-value-${key}`); - - const object = commentaireInputs.map((input, index) => { - return { - input: input, - form: commentaireForms[index], - key: keys[index] - }; - }); - - for (const el of object) { - if (localStorage.getItem(el.key)) { - el.input.value = localStorage.getItem(el.key); - } - - el.input.addEventListener('change', (event) => { - localStorage.setItem(el.key, event.target.value); - }); - - el.form.addEventListener('submit', () => { - localStorage.removeItem(el.key); - }); - } - } -} - -addEventListener('DOMContentLoaded', scrollMessagerie); -addEventListener('DOMContentLoaded', saveMessageContent); diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 549033697..07c6aa3d2 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -18,7 +18,6 @@ import '../new_design/procedure-context'; import '../new_design/procedure-form'; import '../new_design/spinner'; import '../new_design/support'; -import '../new_design/messagerie'; import '../new_design/champs/linked-drop-down-list'; import '../new_design/champs/drop-down-list'; diff --git a/app/javascript/shared/utils.ts b/app/javascript/shared/utils.ts index 25cc4afa2..1e4c46767 100644 --- a/app/javascript/shared/utils.ts +++ b/app/javascript/shared/utils.ts @@ -203,24 +203,7 @@ function createAbortController(controller?: AbortController) { return; } -export function scrollTo(container: HTMLElement, scrollTo: HTMLElement) { - container.scrollTop = - offset(scrollTo).top - offset(container).top + container.scrollTop; -} - -export function scrollToBottom(container: HTMLElement) { - container.scrollTop = container.scrollHeight; -} - export function isNumeric(s: string) { const n = parseFloat(s); return !isNaN(n) && isFinite(n); } - -function offset(element: HTMLElement) { - const rect = element.getBoundingClientRect(); - return { - top: rect.top + document.body.scrollTop, - left: rect.left + document.body.scrollLeft - }; -} diff --git a/app/views/experts/avis/instruction.html.haml b/app/views/experts/avis/instruction.html.haml index 4fd0e2205..2e827f62a 100644 --- a/app/views/experts/avis/instruction.html.haml +++ b/app/views/experts/avis/instruction.html.haml @@ -15,8 +15,8 @@ = render Attachment::ShowComponent.new(attachment: @avis.introduction_file.attachment) %br/ - = form_for @avis, url: expert_avis_path(@avis.procedure, @avis), html: { class: 'form', data: { persisted_content_id: @avis.id } } do |f| - = f.text_area :answer, rows: 3, placeholder: 'Votre avis', required: true, class: 'persisted-input' + = form_for @avis, url: expert_avis_path(@avis.procedure, @avis), html: { class: 'form', data: { controller: 'persisted-form', persisted_form_key_value: dom_id(@avis) } } do |f| + = f.text_area :answer, rows: 3, placeholder: 'Votre avis', required: true = render Attachment::EditComponent.text(f, @avis.piece_justificative_file) .flex.justify-between.align-baseline diff --git a/app/views/experts/shared/avis/_form.html.haml b/app/views/experts/shared/avis/_form.html.haml index 1430c83ca..971397e0b 100644 --- a/app/views/experts/shared/avis/_form.html.haml +++ b/app/views/experts/shared/avis/_form.html.haml @@ -3,7 +3,7 @@ %h1.tab-title Inviter des personnes à donner leur avis %p.avis-notice Les invités pourront consulter le dossier, donner un avis et contribuer au fil de messagerie. Ils ne pourront pas modifier le dossier. - = form_for avis, url: url, html: { class: 'form', data: { persisted_content_id: "expert-ask-avis-for-dossier-#{@avis.dossier.id}" } } do |f| + = form_for avis, url: url, html: { class: 'form', data: { controller: 'persisted-form', persisted_form_key_value: dom_id(@avis.dossier, :avis_by_expert) } } do |f| = hidden_field_tag 'avis[emails]', nil = react_component("ComboMultiple", options: [], selected: [], disabled: [], @@ -11,7 +11,7 @@ name: 'emails', label: 'Emails', acceptNewValues: true) - = f.text_area :introduction, rows: 3, value: avis.introduction || 'Bonjour, merci de me donner votre avis sur ce dossier.', required: true, class: 'persisted-input' + = f.text_area :introduction, rows: 3, value: avis.introduction || 'Bonjour, merci de me donner votre avis sur ce dossier.', required: true %p.tab-title Ajouter une pièce jointe .form-group = render Attachment::EditComponent.text(f, avis.introduction_file) diff --git a/app/views/instructeurs/shared/avis/_form.html.haml b/app/views/instructeurs/shared/avis/_form.html.haml index 4bd16c19c..637af9dd2 100644 --- a/app/views/instructeurs/shared/avis/_form.html.haml +++ b/app/views/instructeurs/shared/avis/_form.html.haml @@ -8,7 +8,7 @@ %p#avis-emails-description.avis-notice Entrez les adresses email des experts à qui vous souhaitez demander un avis - = form_for avis, url: url, html: { class: 'form', data: { persisted_content_id: "instructeur-ask-avis-for-dossier-#{@dossier.id}" } } do |f| + = form_for avis, url: url, html: { class: 'form', data: { controller: 'persisted-form', persisted_form_key_value: dom_id(@dossier, :avis_by_instructeur) } } do |f| = hidden_field_tag 'avis[emails]', nil = react_component("ComboMultiple", options: @dossier.procedure.experts_require_administrateur_invitation ? @experts_emails : [], @@ -18,7 +18,7 @@ name: 'emails', describedby: 'avis-emails-description', acceptNewValues: !@dossier.procedure.experts_require_administrateur_invitation) - = f.text_area :introduction, rows: 3, value: avis.introduction || 'Bonjour, merci de me donner votre avis sur ce dossier.', required: true, class: "persisted-input" + = f.text_area :introduction, rows: 3, value: avis.introduction || 'Bonjour, merci de me donner votre avis sur ce dossier.', required: true %p.tab-title Ajouter une pièce jointe .form-group = render Attachment::EditComponent.text(f, avis.introduction_file) diff --git a/app/views/shared/dossiers/_messagerie.html.haml b/app/views/shared/dossiers/_messagerie.html.haml index e72046db4..56eba7d7e 100644 --- a/app/views/shared/dossiers/_messagerie.html.haml +++ b/app/views/shared/dossiers/_messagerie.html.haml @@ -1,5 +1,5 @@ .messagerie.container - %ul.messages-list + %ul.messages-list{ data: { controller: 'scroll-to' } } - dossier.commentaires.with_attached_piece_jointe.each do |commentaire| %li.message{ class: commentaire_is_from_me_class(commentaire, connected_user), id: dom_id(commentaire) } = render Dossiers::MessageComponent.new(commentaire: commentaire, connected_user: connected_user, messagerie_seen_at: messagerie_seen_at, show_reply_button: show_reply_button(commentaire, connected_user)) diff --git a/app/views/shared/dossiers/messages/_form.html.haml b/app/views/shared/dossiers/messages/_form.html.haml index 332ad1ea8..bbac759cb 100644 --- a/app/views/shared/dossiers/messages/_form.html.haml +++ b/app/views/shared/dossiers/messages/_form.html.haml @@ -1,9 +1,9 @@ -= form_for(commentaire, url: form_url, html: { class: 'form', data: { persisted_content_id: @dossier.present? ? @dossier.id : "bulk-message-#{@procedure.id}" } }) do |f| += form_for(commentaire, url: form_url, html: { class: 'form', data: { controller: 'persisted-form', persisted_form_key_value: @dossier.present? ? dom_id(@dossier) : dom_id(@procedure, :bulk_message) } }) do |f| - dossier = commentaire.dossier - placeholder = t('views.shared.dossiers.messages.form.write_message_to_administration_placeholder') - if instructeur_signed_in? || administrateur_signed_in? || expert_signed_in? - placeholder = t('views.shared.dossiers.messages.form.write_message_placeholder') - = f.text_area :body, rows: 5, placeholder: placeholder, title: placeholder, required: true, class: 'message-textarea persisted-input' + = f.text_area :body, rows: 5, placeholder: placeholder, title: placeholder, required: true, class: 'message-textarea' .flex.justify-between.wrap - disable_piece_jointe = defined?(disable_piece_jointe) ? disable_piece_jointe : false %div