refactor(stimulus): use stimulus in message forms

This commit is contained in:
Paul Chavard 2022-05-18 12:41:01 +02:00
parent 8eb4cf6d51
commit 906eea188e
13 changed files with 127 additions and 87 deletions

View file

@ -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

View file

@ -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

View file

@ -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);

View file

@ -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}`;
}
}

View file

@ -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
};
}

View file

@ -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);

View file

@ -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';

View file

@ -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
};
}

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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))

View file

@ -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