From 2947a542da2220116f0f036ac2257832498ae2ec Mon Sep 17 00:00:00 2001 From: bach Date: Fri, 4 Sep 2026 22:17:52 +0200 Subject: [PATCH] Surface Drupal messages as small top-right toasts, auto-dismissed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two problems: (1) the node form's modal AJAX submit handler (figli_compta_ledger_node_form_ajax_submit) closed the dialog on success without ever surfacing whatever messenger() had queued during save() -- e.g. NodeForm's own "Ligne comptable X has been updated." Since the /lignes table only ever refreshes via AJAX afterwards, never a full page load, that message was silently lost instead of just delayed. Now forwarded via MessageCommand before closing the modal. (2) Gin's default message styling is a full-size flash banner meant for a single admin form, in-flow (pushing the page down) and far too large for frequent background feedback on this dashboard. admin-chrome.css repositions [data-drupal-messages] / [data-drupal-messages-fallback] to fixed top-right (out of document flow, so nothing shifts) and shrinks text/padding/dismiss-button size drastically while keeping them legible. admin-chrome.js auto-dismisses non-error messages after 2s via a MutationObserver (Drupal.Message.add() just appends a DOM node, it never re-fires attachBehaviors(), so behaviors/once() would miss anything added after the initial page load); error messages are left alone, requiring the existing manual click on Gin's own dismiss button. Both attached globally via the existing admin_chrome library (already loaded on every page for the phantom-top-bar fix), consistent with that precedent. Verified: a real full-form save on a clean node returns exactly {message (status, "has been updated"), closeDialog} in that order: a répartition-inconsistent node still correctly blocks the full form (unaffected, skip_validation is never used here); a synthetic status/warning/error trio confirms fixed positioning, no layout shift, ~40% smaller text, and status/warning auto-hiding while error persists until manually dismissed. --- .../figli_compta_ledger/css/admin-chrome.css | 67 +++++++++++++++++++ .../figli_compta_ledger.libraries.yml | 4 ++ .../figli_compta_ledger.module | 13 ++++ .../figli_compta_ledger/js/admin-chrome.js | 64 ++++++++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 web/modules/custom/figli_compta_ledger/js/admin-chrome.js diff --git a/web/modules/custom/figli_compta_ledger/css/admin-chrome.css b/web/modules/custom/figli_compta_ledger/css/admin-chrome.css index 17725a1..e95888b 100644 --- a/web/modules/custom/figli_compta_ledger/css/admin-chrome.css +++ b/web/modules/custom/figli_compta_ledger/css/admin-chrome.css @@ -12,3 +12,70 @@ .top-bar ~ .dialog-off-canvas-main-canvas { margin-block-start: 0 !important; } + +/* Drupal status messages: fixed top-right, out of document flow, so they + never shift the page layout the way the default in-flow placement does + -- and shrunk to a fraction of Gin's default size, which is sized for a + single full-width flash banner on an admin form, not a toast for + frequent background feedback here. Targets both the real region + ([data-drupal-messages], present once a message is queued on a normal + page load) and the JS-only fallback container + ([data-drupal-messages-fallback], used when Drupal.Message.add() -- our + own MessageCommand-driven AJAX messages included -- has no region to + attach to). Auto-dismiss timing for non-error messages is handled in + admin-chrome.js. */ +[data-drupal-messages], +[data-drupal-messages-fallback] { + position: fixed !important; + top: 0.75rem; + right: 0.75rem; + left: auto !important; + width: auto; + max-width: 22rem; + z-index: 1000; +} +[data-drupal-messages] .messages-list__wrapper, +[data-drupal-messages] .messages__wrapper, +[data-drupal-messages-fallback] .messages-list__wrapper, +[data-drupal-messages-fallback] .messages__wrapper { + display: flex; + flex-direction: column; + gap: 0.4rem; +} +[data-drupal-messages] .messages, +[data-drupal-messages-fallback] .messages { + padding: 0.5rem 0.6rem; + font-size: 0.78rem; + line-height: 1.3; + border-radius: 6px; +} +[data-drupal-messages] .messages__header, +[data-drupal-messages-fallback] .messages__header { + margin-block-end: 0.15rem; + margin-inline-end: 1.1em; +} +[data-drupal-messages] .messages__header::before, +[data-drupal-messages-fallback] .messages__header::before { + width: 1rem !important; + height: 1rem !important; +} +[data-drupal-messages] .messages__title, +[data-drupal-messages-fallback] .messages__title { + font-size: 0.78rem; +} +[data-drupal-messages] .messages__title, +[data-drupal-messages] .messages__content, +[data-drupal-messages-fallback] .messages__title, +[data-drupal-messages-fallback] .messages__content { + margin-inline-start: 1.4rem; +} +[data-drupal-messages] .button--dismiss, +[data-drupal-messages-fallback] .button--dismiss { + height: 20px !important; + width: 20px !important; + margin: 0.3rem !important; +} +[data-drupal-messages] .button--dismiss .icon-close, +[data-drupal-messages-fallback] .button--dismiss .icon-close { + mask-size: 11px 11px !important; +} diff --git a/web/modules/custom/figli_compta_ledger/figli_compta_ledger.libraries.yml b/web/modules/custom/figli_compta_ledger/figli_compta_ledger.libraries.yml index 62824e4..e36af0a 100644 --- a/web/modules/custom/figli_compta_ledger/figli_compta_ledger.libraries.yml +++ b/web/modules/custom/figli_compta_ledger/figli_compta_ledger.libraries.yml @@ -29,3 +29,7 @@ admin_chrome: css: theme: css/admin-chrome.css: {} + js: + js/admin-chrome.js: {} + dependencies: + - core/drupal diff --git a/web/modules/custom/figli_compta_ledger/figli_compta_ledger.module b/web/modules/custom/figli_compta_ledger/figli_compta_ledger.module index 1b0fc7f..b683ebc 100644 --- a/web/modules/custom/figli_compta_ledger/figli_compta_ledger.module +++ b/web/modules/custom/figli_compta_ledger/figli_compta_ledger.module @@ -14,6 +14,7 @@ use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\CloseModalDialogCommand; +use Drupal\Core\Ajax\MessageCommand; use Drupal\Core\Ajax\ReplaceCommand; /** @@ -121,6 +122,18 @@ function figli_compta_ledger_node_form_ajax_submit(array $form, FormStateInterfa $response->addCommand(new ReplaceCommand('#' . $form['#id'], $form)); return $response; } + // Surface whatever messenger() queued during save() -- e.g. NodeForm's + // own "Ligne comptable X has been created/updated." -- before closing + // the modal. Without this the message is silently lost: the /lignes + // table only ever refreshes via AJAX afterwards ("dialog:afterclose" -> + // reloadWindow()), never a full page load, so a session-queued message + // would otherwise sit unseen until some unrelated future page render. + foreach (\Drupal::messenger()->all() as $type => $messages) { + foreach ($messages as $message) { + $response->addCommand(new MessageCommand($message, NULL, ['type' => $type], FALSE)); + } + } + \Drupal::messenger()->deleteAll(); $response->addCommand(new CloseModalDialogCommand()); return $response; } diff --git a/web/modules/custom/figli_compta_ledger/js/admin-chrome.js b/web/modules/custom/figli_compta_ledger/js/admin-chrome.js new file mode 100644 index 0000000..2d56da8 --- /dev/null +++ b/web/modules/custom/figli_compta_ledger/js/admin-chrome.js @@ -0,0 +1,64 @@ +/** + * @file + * Non-error Drupal status messages (status/warning) auto-dismiss after a + * couple seconds; error messages always require an explicit click on + * Gin's own dismiss button (.js-message-button-hide) -- something that + * needs attention shouldn't vanish on its own. + * + * Drupal.Message.add() (used both for messages already in the initial + * page render and for MessageCommand-driven AJAX messages) just appends a + * DOM node directly -- it never re-fires Drupal.attachBehaviors(), so a + * behaviors/once()-based approach would miss anything added after the + * initial page load. A MutationObserver sees every insertion path. + */ +(function (Drupal) { + 'use strict'; + + var AUTO_DISMISS_MS = 2000; + + function dismiss(el) { + var btn = el.querySelector('.js-message-button-hide'); + if (btn) { + btn.click(); + } else { + el.style.opacity = 0; + el.classList.add('visually-hidden'); + } + } + + function handleMessage(el) { + if (el.dataset.figliAutoDismissSeen) return; + el.dataset.figliAutoDismissSeen = '1'; + if (el.classList.contains('messages--error')) return; + setTimeout(function () { + dismiss(el); + }, AUTO_DISMISS_MS); + } + + function scan(root) { + root.querySelectorAll('.messages-list__item').forEach(handleMessage); + } + + function init() { + scan(document); + new MutationObserver(function (mutations) { + mutations.forEach(function (mutation) { + mutation.addedNodes.forEach(function (node) { + if (node.nodeType !== 1) return; + if (node.classList && node.classList.contains('messages-list__item')) { + handleMessage(node); + } + if (node.querySelectorAll) { + scan(node); + } + }); + }); + }).observe(document.body, { childList: true, subtree: true }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(Drupal);