Перейти к содержанию

Свой alert и confirm вместо стандартных: модальное окно на dialog

Стандартный 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> всё это даёт бесплатно.

Урок: прежде чем рисовать свой компонент, стоит проверить, нет ли его уже в браузере. За семь лет между версиями этого поста браузеры научились делать модальные окна сами — и лучше, чем мой самописный блок.

С вами был Доктор Лексиум.

Хорошего утра, дня, вечера, ночи (нужное подчеркнуть).

3 комментария

  1. Интересный вопрос. Плохо что увидел его лишь спустя неделю.
    С ответом нужно подумать. Честно говоря, обычно подобные вещи делаю с помощью самописных модальных окон и нескольких строчек кода. Так проще.

  2. То же самое можно сделать для окна confirm? Там нужна лишь кнопка подтверждения, которая возвращает true. А заменить alert на confirm я догадался. В качестве же кнопки отмены я, например, мог бы приспособить крестик закрытия окна, вот этот contact-remove-sign. Его легко переделать в прямоугольную кнопку, написать на нём «Отмена» и передвинуть куда надо.
    Скрипт то что надо. Другие решения, что я рассматривал, все какие-то замороченные. Вот только кнопочки true не хватает.

  3. Я в коде везде заменил alert на confirm, и у меня кастомизация стандартного окна confirm сработала. Но у него нет кнопок согласия и отмены. Какой код туда нужно добавить для кнопок?

Написать комментарий

Адрес почты не публикуется. Обязательные поля отмечены звёздочкой.

Возможно, вы не читали