diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 8ac122f9..db2db261 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -11,6 +11,14 @@ $colors: ( "magenta": $magenta ); +// TODO: Encontrar la forma de generar esto desde los locales de Rails +$custom-file-text: ( + en: "Browse", + es: "Buscar archivo", + pt: "Buscar arquivo", + pt-BR: "Buscar arquivo" +); + // Redefinir variables de Bootstrap $primary: $magenta; $secondary: $black; @@ -30,6 +38,8 @@ $input-bg: var(--background); $input-color: var(--foreground); $btn-bg-color: var(--btn-bg-color); $btn-color: var(--btn-color); +$input-group-addon-bg: var(--btn-bg-color); +$custom-file-color: var(--btn-color); $spacers: ( 2-plus: 0.75rem @@ -101,12 +111,6 @@ $sizes: ( } } -// TODO: Encontrar la forma de generar esto desde los locales de Rails -$custom-file-text: ( - en: 'Browse', - es: 'Buscar archivo' -); - @font-face { font-family: 'Saira'; font-style: normal; diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 0f3c24d5..70ba2e54 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -128,7 +128,9 @@ class PostsController < ApplicationController # condiciones. if htmx? if post.persisted? - swap_modals + triggers = { 'notification:show' => { 'id' => params.permit(:saved).values.first } } + + swap_modals(triggers) @value = post.title.value @uuid = post.uuid.value @@ -169,7 +171,9 @@ class PostsController < ApplicationController if htmx? if post.persisted? - swap_modals + triggers = { 'notification:show' => params.permit(:saved).values.first } + + swap_modals(triggers) @value = post.title.value @uuid = post.uuid.value diff --git a/app/javascript/controllers/form_validation_controller.js b/app/javascript/controllers/form_validation_controller.js index c9817e27..85e7bb86 100644 --- a/app/javascript/controllers/form_validation_controller.js +++ b/app/javascript/controllers/form_validation_controller.js @@ -3,6 +3,16 @@ import { Controller } from "stimulus"; export default class extends Controller { static targets = ["invalid", "submitting"]; + // @todo Stimulus >1 + get submittingIdValue() { + return this.element.dataset?.formValidationSubmittingIdValue; + } + + // @todo Stimulus >1 + get invalidIdValue() { + return this.element.dataset?.formValidationInvalidIdValue; + } + connect() { this.element.setAttribute("novalidate", true); @@ -19,34 +29,19 @@ export default class extends Controller { submit(event = undefined) { event?.preventDefault(); - event?.stopPropagation(); if (this.element.reportValidity()) { this.element.classList.remove("was-validated"); - this.element.submit(); - this.show(this.submittingTargets); - this.hide(this.invalidTargets); + if (!this.element.hasAttribute("hx-post")) this.element.submit(); + + window.dispatchEvent(new CustomEvent("notification:show", { detail: { id: this.submittingIdValue } })); } else { + event?.stopPropagation(); + this.element.classList.add("was-validated"); - this.hide(this.submittingTargets); - this.show(this.invalidTargets); - } - } - show(elements) { - for (const element of elements) { - element.classList.remove("d-none"); - - setTimeout(() => element.classList.add("show"), 1); - } - } - - hide(elements) { - for (const element of elements) { - element.classList.remove("show"); - - setTimeout(() => element.classList.add("d-none"), 2000); + window.dispatchEvent(new CustomEvent("notification:show", { detail: { id: this.invalidIdValue } })); } } } diff --git a/app/javascript/controllers/notification_controller.js b/app/javascript/controllers/notification_controller.js new file mode 100644 index 00000000..7fbe3b5a --- /dev/null +++ b/app/javascript/controllers/notification_controller.js @@ -0,0 +1,43 @@ +import { Controller } from "stimulus"; + +/* + * Solo se puede mostrar una notificación a la vez + */ +export default class extends Controller { + // @todo Stimulus >1 + get showClasses() { + return (this.element.dataset?.notificationShowClass || "").split(" ").filter(x => x); + } + + // @todo Stimulus >1 + get hideClasses() { + return (this.element.dataset?.notificationHideClass || "").split(" ").filter(x => x); + } + + /* + * Al recibir el evento de mostrar, si no está dirigido al elemento + * actual, se oculta. + */ + show(event = undefined) { + if (event?.detail?.id !== this.element.id) { + this.hide({ detail: { id: this.element.id } }); + return; + } + + this.element.classList.remove("d-none"); + + setTimeout(() => { + this.element.classList.remove(...this.hideClasses); + this.element.classList.add(...this.showClasses); + }, 1); + } + + hide(event = undefined) { + if (event?.detail?.id !== this.element.id) return; + + this.element.classList.remove(...this.showClasses); + this.element.classList.add(...this.hideClasses); + + setTimeout(() => this.element.classList.add("d-none"), 150); + } +} diff --git a/app/views/posts/_form.haml b/app/views/posts/_form.haml index aa8f3d1d..3e09cb72 100644 --- a/app/views/posts/_form.haml +++ b/app/views/posts/_form.haml @@ -31,11 +31,19 @@ end - dir = t("locales.#{@locale}.dir") +- submitting_id = random_id +- invalid_id = random_id +- data = {} +- data[:controller] = 'unsaved-changes form-validation' +- data[:action] = 'unsaved-changes#submit form-validation#submit beforeunload@window->unsaved-changes#unsaved turbolinks:before-visit@window->unsaved-changes#unsavedTurbolinks' +- data[:'unsaved-changes-confirm-value'] = t('.confirm') +- data[:'form-validation-submitting-id-value'] = submitting_id +- data[:'form-validation-invalid-id-value'] = invalid_id -# Comienza el formulario -= form_tag url, method: method, class: "form post #{extra_class}", multipart: true, data: { controller: 'unsaved-changes form-validation', action: 'unsaved-changes#submit form-validation#submit beforeunload@window->unsaved-changes#unsaved turbolinks:before-visit@window->unsaved-changes#unsavedTurbolinks', 'unsaved-changes-confirm-value': t('.confirm') } do += form_tag url, method: method, class: "form post #{extra_class}", multipart: true, data: data do -# Botones de guardado - = render 'posts/submit', site: site, post: post + = render 'posts/submit', site: site, post: post, invalid: invalid_id, submitting: submitting_id = hidden_field_tag 'post[layout]', post.layout.name @@ -43,7 +51,7 @@ = render 'posts/attributes', site: site, post: post, dir: dir, base: 'post', locale: @locale -# Botones de guardado - = render 'posts/submit', site: site, post: post + = render 'posts/submit', site: site, post: post, invalid: invalid_id, submitting: submitting_id -# Formularios usados por los modales = yield(:post_form) diff --git a/app/views/posts/_htmx_form.haml b/app/views/posts/_htmx_form.haml index f4f4a845..1a8c0597 100644 --- a/app/views/posts/_htmx_form.haml +++ b/app/views/posts/_htmx_form.haml @@ -27,7 +27,14 @@ multipart: true, class: 'form post ', 'hx-swap': params.require(:swap), - 'hx-target': "##{params.require(:target)}" + 'hx-target': "##{params.require(:target)}", + 'hx-validate': true, + data: { + controller: 'form-validation', + action: 'form-validation#submit', + 'form-validation-submitting-id-value': params.permit(:submitting).values.first, + 'form-validation-invalid-id-value': params.permit(:invalid).values.first, + } } if post.new? @@ -73,6 +80,8 @@ %input{ type: 'hidden', name: 'swap', value: params.require(:swap) } - if params[:inverse].present? %input{ type: 'hidden', name: 'inverse', value: params.require(:inverse) } + - if params[:saved].present? + %input{ type: 'hidden', name: 'saved', value: params.require(:saved) } = hidden_field_tag "#{base}[layout]", post.layout.name diff --git a/app/views/posts/_submit.haml b/app/views/posts/_submit.haml index c3064c53..41d6f420 100644 --- a/app/views/posts/_submit.haml +++ b/app/views/posts/_submit.haml @@ -1,11 +1,3 @@ -- invalid_help = site.config.fetch('invalid_help', t('.invalid_help')) -- submitting_help = site.config.fetch('submitting_help', t('.submitting_help')) - .d-flex.flex-column.flex-md-row.align-items-start.mb-3 - %div - = submit_tag t('.save'), class: 'btn btn-secondary submit-post' - .d-flex.flex-column.position-relative - = render 'bootstrap/alert', class: 'm-0 d-none fade', data: { target: 'form-validation.invalid' } do - = invalid_help - = render 'bootstrap/alert', class: 'm-0 d-none fade position-absolute top-0 left-0', data: { target: 'form-validation.submitting' } do - = submitting_help + %div= submit_tag t('.save'), class: 'btn btn-secondary submit-post' + = render 'posts/validation', site: site, submitting: { id: submitting }, invalid: { id: invalid } diff --git a/app/views/posts/_validation.haml b/app/views/posts/_validation.haml new file mode 100644 index 00000000..c28a743a --- /dev/null +++ b/app/views/posts/_validation.haml @@ -0,0 +1,16 @@ +- invalid = site.config.fetch('invalid', t('.invalid')) +- submitting = site.config.fetch('submitting', t('.submitting')) +- %i[invalid submitting].each do |key| + - local_assigns[key] ||= {} + - local_assigns[key][:data] ||= {} + - local_assigns[key][:data][:target] ||= "form-validation.#{key}" + - local_assigns[key][:data][:action] ||= 'notification:show@window->notification#show' + - local_assigns[key][:data][:controller] ||= 'notification' + - local_assigns[key][:data][:'notification-hide-class'] ||= 'hide' + - local_assigns[key][:data][:'notification-show-class'] ||= 'show' + +.d-flex.flex-column + = render 'bootstrap/alert', class: 'm-0 d-none fade', **local_assigns[:invalid] do + = invalid + = render 'bootstrap/alert', class: 'm-0 d-none fade', **local_assigns[:submitting] do + = submitting diff --git a/app/views/posts/attributes/_file.haml b/app/views/posts/attributes/_file.haml index 0287366c..2f113463 100644 --- a/app/views/posts/attributes/_file.haml +++ b/app/views/posts/attributes/_file.haml @@ -27,6 +27,7 @@ = file_field(*field_name_for(base, attribute, :path), **field_options(attribute, metadata, required: (metadata.required && !metadata.path?)), class: "custom-file-input #{invalid(post, attribute)}", + lang: locale, data: { target: 'file-preview.input', action: 'file-preview#update' }) = label_tag "#{base}_#{attribute}_path", post_label_t(attribute, :path, post: post), class: 'custom-file-label' diff --git a/app/views/posts/attributes/_image.haml b/app/views/posts/attributes/_image.haml index a0bfebde..aa97a60d 100644 --- a/app/views/posts/attributes/_image.haml +++ b/app/views/posts/attributes/_image.haml @@ -24,6 +24,7 @@ **field_options(attribute, metadata, required: (metadata.required && !metadata.path?)), class: "custom-file-input #{invalid(post, attribute)}", accept: ActiveStorage.web_image_content_types.join(','), + lang: locale, data: { target: 'file-preview.input', action: 'file-preview#update' }) = label_tag "#{base}_#{attribute}_path", post_label_t(attribute, :path, post: post), class: 'custom-file-label' diff --git a/app/views/posts/attributes/_new_has_one.haml b/app/views/posts/attributes/_new_has_one.haml index 58e098b6..87ef0440 100644 --- a/app/views/posts/attributes/_new_has_one.haml +++ b/app/views/posts/attributes/_new_has_one.haml @@ -19,6 +19,9 @@ post_form_loaded_id = random_id value_list_id = random_id layout = metadata.filter[:layout] + invalid_id = random_id + submitting_id = random_id + saved_id = random_id %div{ data: { controller: 'modal' }} .form-group @@ -43,10 +46,13 @@ = render 'bootstrap/modal', id: id, modal_content_attributes: { class: 'h-100' } do - content_for :"#{id}_body" do -# @todo ocultar el modal después de guardar - .placeholder-glow{ 'hx-get': site_posts_form_path(site, layout: layout, base: id, name: name, form: form_id, swap: 'innerHTML', target: target_id, attribute: 'new_has_one', hide: modal_id, uuid: metadata.value), 'hx-trigger': 'load' } + .placeholder-glow{ 'hx-get': site_posts_form_path(site, layout: layout, base: id, name: name, form: form_id, swap: 'innerHTML', target: target_id, attribute: 'new_has_one', hide: modal_id, uuid: metadata.value, invalid: invalid_id, submitting: submitting_id, saved: saved_id), 'hx-trigger': 'load' } %span.placeholder.w-100.h-100 - content_for :"#{id}_footer" do + = render 'posts/validation', site: site, invalid: { id: invalid_id }, submitting: { id: submitting_id } + = render 'bootstrap/alert', class: 'm-0 d-none fade', id: saved_id, data: { controller: 'notification', action: 'notification:show@window->notification#show', 'notification-hide-class': 'hide', 'notification-show-class': 'show' } do + = t('.saved') = render 'bootstrap/btn', form: form_id, content: t('.save'), type: 'submit', class: 'm-0 mt-1 mr-1' = render 'bootstrap/btn', content: t('.close'), action: 'modal#hide', class: 'm-0 mt-1 mr-1' diff --git a/config/locales/en.yml b/config/locales/en.yml index 301e1687..325056a0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -715,8 +715,9 @@ en: submit: save: 'Save' save_draft: 'Save as draft' - invalid_help: "Some fields need attention! Please search for the fields marked as not valid." - submitting_help: "Saving changes, please wait..." + validation: + invalid: "Some fields need attention! Please search for the fields marked as not valid." + submitting: "Saving changes, please wait..." new_array: remove: "Remove" attributes: @@ -966,3 +967,5 @@ en: save: "Save" card: edit: "Edit" + alert: + saved: "Changes were saved!" diff --git a/config/locales/es.yml b/config/locales/es.yml index 1bc51ffc..a28f08cf 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -723,8 +723,9 @@ es: submit: save: 'Guardar' save_draft: 'Guardar como borrador' - invalid_help: "¡Te falta completar algunos campos! Busca los que estén marcados como no válidos." - submitting_help: "Guardando, por favor espera..." + validation: + invalid: "¡Te falta completar algunos campos! Busca los que estén marcados como no válidos." + submitting: "Guardando, por favor espera..." new_array: remove: "Eliminar" attributes: @@ -772,6 +773,8 @@ es: edit: "Editar" new_predefined_array: edit: "Editar" + new_predefined_value: + edit: "Editar" new_array: edit: "Editar" required: "Seleccioná al menos una opción." @@ -974,3 +977,5 @@ es: save: "Guardar" card: edit: "Editar" + alert: + saved: "¡Cambios guardados!"