Стандартный alert в JavaScript выглядит по-разному в каждом браузере, не подстраивается под дизайн сайта и в некоторых браузерах подписан адресом сайта. В 2019 году я заменял его своим модальным окном на jQuery. Сейчас для этого есть элемент <dialog>, встроенный во все современные браузеры: затемнение фона, закрытие по Esc и фокус на кнопке он делает сам.
Ниже — замена alert и confirm в полсотни строк без библиотек и что было не так с моей версией 2019 года.
JS
(function () {
const dlg = document.createElement('dialog');
dlg.className = 'custom-alert';
dlg.innerHTML = '<p class="custom-alert__text"></p><form method="dialog"></form>';
const text = dlg.querySelector('.custom-alert__text');
const form = dlg.querySelector('form');
const queue = [];
let current = null;
function next() {
if (current || !queue.length) return;
current = queue.shift();
if (!dlg.isConnected) document.body.append(dlg);
text.textContent = current.text;
form.innerHTML = (current.cancel ? '<button value="cancel" class="custom-alert__cancel">Отмена</button>' : '') +
'<button value="ok" class="custom-alert__ok">OK</button>';
dlg.returnValue = ''; // иначе останется ответ из прошлого окна
dlg.showModal();
form.querySelector('.custom-alert__ok').focus();
}
dlg.addEventListener('close', () => {
const done = current.done;
current = null;
done(dlg.returnValue === 'ok'); // Esc и клик по фону — как «Отмена»
next();
});
// Клик по затемнённому фону вокруг окна тоже закрывает
dlg.addEventListener('click', e => { if (e.target === dlg) dlg.close(); });
function show(text, cancel) {
return new Promise(done => {
queue.push({ text: String(text), cancel, done });
next();
});
}
window.realAlert = window.alert;
window.alertAsync = text => show(text, false).then(() => {});
window.confirmAsync = text => show(text, true);
window.alert = text => { alertAsync(text); };
})();
CSS
.custom-alert {
padding: 0; /* иначе клик по отступу окна считался бы кликом по фону */
max-width: min(90vw, 380px);
border: 1px solid #999;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, .5);
}
.custom-alert::backdrop {
background: rgba(0, 0, 0, .4);
}
.custom-alert[open] {
animation: custom-alert-in .2s ease-out;
}
@keyframes custom-alert-in {
from { opacity: 0; transform: translateY(-10px); }
}
.custom-alert__text {
margin: 0;
padding: 20px 20px 0;
font-size: 18px;
white-space: pre-line; /* \n в тексте — перенос строки, как у обычного alert */
}
.custom-alert form {
margin: 0;
padding: 15px 20px 20px;
text-align: right;
}
.custom-alert__ok,
.custom-alert__cancel {
min-width: 80px;
margin-left: 8px;
padding: 6px 16px;
font-size: 16px;
cursor: pointer;
}
Как это работает
showModal()открывает окно поверх страницы и затемняет фон (::backdrop). Esc закрывает окно само.- Кнопка OK лежит в форме с
method="dialog": нажатие закрывает окно без единой строки JS. - Сообщения встают в очередь: второй
alertподождёт, пока закроют первый. - Текст вставляется через
textContent, а не как HTML. window.alertподменяется, поэтому старый код менять не нужно. Оригинал остаётся вwindow.realAlert.- Перед каждым показом
returnValueокна сбрасывается: иначе Esc после прошлого «OK» вернул быtrue.
А confirm?
В комментариях к первой версии поста спрашивали, как сделать то же для confirm — с кнопками «OK» и «Отмена». Отвечаю спустя три года: confirmAsync выше показывает обе кнопки и возвращает Promise с true или false. Esc и клик мимо окна считаются отменой, как у настоящего confirm.
if (await confirmAsync('Удалить все файлы?')) {
deleteAll();
}
Подменить сам window.confirm, как alert, нельзя: код ждёт от него ответ сразу, в той же строке, а своё окно отвечает только после клика. Поэтому вызовы confirm(…) придётся заменить на await confirmAsync(…), а функцию вокруг пометить async.
Грабли №1: alert больше не останавливает скрипт
Обычный alert замораживает выполнение кода, пока пользователь не нажмёт OK. Свой — нет, и это не обойти: так браузер ведёт себя только со встроенными окнами. Код вроде
alert('Сохранено');
location.reload();
с подменённым alert перезагрузит страницу сразу, и сообщение никто не увидит. Для таких мест есть alertAsync — он возвращает Promise, который выполнится, когда окно закроют:
await alertAsync('Сохранено');
location.reload();
Грабли №2: в моей версии 2019 года
Тогда я писал на jQuery, и в коде было три проблемы, которые теперь исправлены:
- Текст вставлялся как HTML (
.html(s)). Если в alert попадало что-то из пользовательского ввода, вроде<img src=x onerror=…>, браузер выполнял чужой код. Это классическая XSS-уязвимость. - Второе сообщение затирало первое, если первое ещё не закрыли.
- Не было ни затемнения, ни Esc, ни фокуса на кнопке — с клавиатуры окно было не закрыть, а экранная читалка его не замечала.
<dialog>всё это даёт бесплатно.
Урок: прежде чем рисовать свой компонент, стоит проверить, нет ли его уже в браузере. За семь лет между версиями этого поста браузеры научились делать модальные окна сами — и лучше, чем мой самописный блок.
С вами был Доктор Лексиум.
Хорошего утра, дня, вечера, ночи (нужное подчеркнуть).
Интересный вопрос. Плохо что увидел его лишь спустя неделю.
С ответом нужно подумать. Честно говоря, обычно подобные вещи делаю с помощью самописных модальных окон и нескольких строчек кода. Так проще.
То же самое можно сделать для окна confirm? Там нужна лишь кнопка подтверждения, которая возвращает true. А заменить alert на confirm я догадался. В качестве же кнопки отмены я, например, мог бы приспособить крестик закрытия окна, вот этот contact-remove-sign. Его легко переделать в прямоугольную кнопку, написать на нём «Отмена» и передвинуть куда надо.
Скрипт то что надо. Другие решения, что я рассматривал, все какие-то замороченные. Вот только кнопочки true не хватает.
Я в коде везде заменил alert на confirm, и у меня кастомизация стандартного окна confirm сработала. Но у него нет кнопок согласия и отмены. Какой код туда нужно добавить для кнопок?