Prevent accidental modal close on backdrop

This commit is contained in:
Mikhail Chusavitin
2026-02-25 17:12:43 +03:00
parent c97d49e762
commit 20d96306e4
3 changed files with 46 additions and 25 deletions

View File

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

View File

@@ -244,7 +244,13 @@ function showImportErrorDialog(result, headers) {
});
dialog.innerHTML = `
<h3 style="margin-top: 0; color: #d32f2f;">📊 Результат импорта</h3>
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px;">
<h3 style="margin-top: 0; margin-bottom: 0; color: #d32f2f;">📊 Результат импорта</h3>
<button id="importErrorX" type="button" aria-label="Закрыть"
style="padding: 0; width: 32px; height: 32px; border: none; border-radius: 4px; background: transparent; cursor: pointer; font-size: 22px; line-height: 1; color: #666;">
×
</button>
</div>
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
<div style="flex: 1; padding: 15px; background: #e8f5e9; border-radius: 8px; text-align: center;">
@@ -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 = `
<h3 style="margin-top: 0;">📤 Экспорт данных</h3>
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px;">
<h3 style="margin-top: 0; margin-bottom: 0;">📤 Экспорт данных</h3>
<button id="exportX" type="button" aria-label="Закрыть"
style="padding: 0; width: 32px; height: 32px; border: none; border-radius: 4px; background: transparent; cursor: pointer; font-size: 22px; line-height: 1; color: #666;">
×
</button>
</div>
<div style="display: flex; border-bottom: 2px solid #ddd; margin-bottom: 15px;">
<button id="tabCSV" class="export-tab" style="padding: 10px 20px; border: none; background: #4CAF50; color: white; cursor: pointer; border-radius: 4px 4px 0 0; margin-right: 5px;">
@@ -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) {

View File

@@ -443,7 +443,13 @@ async function promptForRequiredFields(requiredFields, optionalFields) {
`;
let html = `
<h3 style="margin-top: 0; margin-bottom: 15px;"> Вставить запись в ${currentSchema}.${currentTable}</h3>
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 15px;">
<h3 style="margin: 0;"> Вставить запись в ${currentSchema}.${currentTable}</h3>
<button id="insertClose" type="button" aria-label="Закрыть"
style="padding: 0; width: 32px; height: 32px; border: none; border-radius: 4px; background: transparent; cursor: pointer; font-size: 22px; line-height: 1; color: #666;">
×
</button>
</div>
`;
// Обязательные поля
@@ -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 = `
<h3 style="margin-top: 0; margin-bottom: 10px;">✏️ Редактирование ${isSingleRow ? 'записи' : `${count} записей`}</h3>
<div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 10px;">
<h3 style="margin: 0;">✏️ Редактирование ${isSingleRow ? 'записи' : `${count} записей`}</h3>
<button id="editClose" type="button" aria-label="Закрыть"
style="padding: 0; width: 32px; height: 32px; border: none; border-radius: 4px; background: transparent; cursor: pointer; font-size: 22px; line-height: 1; color: #666;">
×
</button>
</div>
`;
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 = {};