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 = `
-
📤 Экспорт данных
+
+
📤 Экспорт данных
+
+ ×
+
+
@@ -461,7 +473,7 @@ async function showExportDialog() {
panelBackup.style.display = 'block'; panelCSV.style.display = 'none';
};
- modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
+ dialog.querySelector('#exportX').onclick = () => modal.remove();
dialog.querySelector('#exportClose').onclick = () => modal.remove();
if (hasTable) {
diff --git a/public/js/operations.js b/public/js/operations.js
index ec3c80f..58a355b 100644
--- a/public/js/operations.js
+++ b/public/js/operations.js
@@ -443,7 +443,13 @@ async function promptForRequiredFields(requiredFields, optionalFields) {
`;
let html = `
- ➕ Вставить запись в ${currentSchema}.${currentTable}
+
+
➕ Вставить запись в ${currentSchema}.${currentTable}
+
+ ×
+
+
`;
// Обязательные поля
@@ -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 = {};