From 20d96306e47f2d41fe44e1284f7dfea3016e4f02 Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Wed, 25 Feb 2026 17:12:43 +0300 Subject: [PATCH] Prevent accidental modal close on backdrop --- bible/frontend.md | 8 ++++++++ public/js/io.js | 20 +++++++++++++++---- public/js/operations.js | 43 +++++++++++++++++++++-------------------- 3 files changed, 46 insertions(+), 25 deletions(-) diff --git a/bible/frontend.md b/bible/frontend.md index fb13aa5..186e567 100644 --- a/bible/frontend.md +++ b/bible/frontend.md @@ -87,3 +87,11 @@ The frontend is a **single-page application** with no build step — plain HTML, | Tabulator | latest stable | CDN (`tabulator.info`) | No bundler, no transpiler, no node_modules. + +--- + +## Modal UX Rules + +- Destructive or data-entry modals must not close on backdrop click. +- Close such modals only via explicit controls: `Отмена`, `Закрыть`, or a top-right `×` button. +- If a modal can be closed, provide a visible close button in the header (top-right `×`) in addition to the footer action when applicable. diff --git a/public/js/io.js b/public/js/io.js index 407638f..80d6570 100644 --- a/public/js/io.js +++ b/public/js/io.js @@ -244,7 +244,13 @@ function showImportErrorDialog(result, headers) { }); dialog.innerHTML = ` -

📊 Результат импорта

+
+

📊 Результат импорта

+ +
@@ -277,7 +283,7 @@ function showImportErrorDialog(result, headers) { modal.appendChild(dialog); document.body.appendChild(modal); - modal.onclick = (e) => { if (e.target === modal) modal.remove(); }; + dialog.querySelector('#importErrorX').onclick = () => modal.remove(); dialog.querySelector('#importErrorClose').onclick = () => modal.remove(); dialog.querySelector('#importErrorDownload').onclick = () => { @@ -371,7 +377,13 @@ async function showExportDialog() { } dialog.innerHTML = ` -

📤 Экспорт данных

+
+

📤 Экспорт данных

+ +
+
`; // Обязательные поля @@ -478,21 +484,15 @@ async function promptForRequiredFields(requiredFields, optionalFields) { document.body.appendChild(modal); return new Promise((resolve) => { - document.getElementById('insertCancel').addEventListener('click', () => { + const closeInsertModal = () => { if (document.body.contains(modal)) { document.body.removeChild(modal); } resolve(null); - }); + }; - modal.addEventListener('click', (e) => { - if (e.target === modal) { - if (document.body.contains(modal)) { - document.body.removeChild(modal); - } - resolve(null); - } - }); + document.getElementById('insertCancel').addEventListener('click', closeInsertModal); + document.getElementById('insertClose').addEventListener('click', closeInsertModal); document.getElementById('insertSubmit').addEventListener('click', () => { const values = {}; @@ -626,7 +626,13 @@ async function showEditModal(selectedRows) { `; let html = ` -

✏️ Редактирование ${isSingleRow ? 'записи' : `${count} записей`}

+
+

✏️ Редактирование ${isSingleRow ? 'записи' : `${count} записей`}

+ +
`; if (!isSingleRow) { @@ -738,19 +744,14 @@ async function showEditModal(selectedRows) { modal.appendChild(dialog); document.body.appendChild(modal); - document.getElementById('editCancel').addEventListener('click', () => { + const closeEditModal = () => { if (document.body.contains(modal)) { document.body.removeChild(modal); } - }); + }; - modal.addEventListener('click', (e) => { - if (e.target === modal) { - if (document.body.contains(modal)) { - document.body.removeChild(modal); - } - } - }); + document.getElementById('editCancel').addEventListener('click', closeEditModal); + document.getElementById('editClose').addEventListener('click', closeEditModal); document.getElementById('editSubmit').addEventListener('click', async () => { const changes = {};