Я давно думал о кастомизации внешнего вида типовых функций взаимодействия с пользователем в JavaScript — alert(), confirm() и prompt() (далее модальные окна).
Действительно, они очень удобны в использовании, но разные в различных браузерах и весьма неприглядны на вид.
Наконец руки дошли.
В чём проблема? Обычные средства выдачи диалогов (например, bootstrap) не получится использовать также просто, как и alert, где браузер организует остановку выполнения кода JavaScript и ожидание действия пользователя (клик на кнопке закрытия). Modal в bootstrap потребует отдельную обработку события – клик на кнопке, закрытие модального окна…
С появлением Promise в ECMAScript 6 (ES6) всё стало возможным!
Я применил подход разделения дизайна модальных окон и кода (alert(), confirm() и prompt()). Но можно всё упрятать в код. Чем привлекает такой подход – дизайн можно менять в разных проектах, да просто на разных страницах или в зависимости от ситуации.
Плохой момент этого подхода состоит в необходимости использовать имена (id) разметки в коде модальных окон, да ещё и в глобальной области видимости. Но это просто пример принципа, поэтому я не буду заострять на этом внимание.
Итак, разберём разметку (bootstrap и Font Awesome для шрифтовых икон) и код alert (я использую jQuery):
<div id=«PromiseAlert» class=«modal»>
<div class=«modal-dialog» role=«document»>
<div class=«modal-content»>
<div class=«modal-header»>
<h5 class=«modal-title»><i class=«fas fa-exclamation-triangle text-warning»></i> <span>The app reports</span></h5>
<button type=«button» class=«close» data-dismiss=«modal» aria-label=«Close»>
<span aria-hidden=«true»>×</span>
</button>
</div>
<div class=«modal-body»>
<p></p>
</div>
<div class=«modal-footer»>
<button type=«button» class=«btn btn-secondary» data-dismiss=«modal»>OK</button>
</div>
</div>
</div>
</div>
window.alert = (message) => {
$('#PromiseAlert .modal-body p').html(message);
var PromiseAlert = $('#PromiseAlert').modal({
keyboard: false,
backdrop: 'static'
}).modal('show');
return new Promise(function (resolve, reject) {
PromiseAlert.on('hidden.bs.modal', resolve);
});
};
Как я говорил выше, к коде используется глобальное имя PromiseAlert и классы html разметки. В первой строке кода телу сообщения передаётся параметр функции alert. После этого методом bootstrap выводится модальное окно с определёнными опциями (они делают его более приближенным к нативному alert). Важно! Модальное окно запоминается в локальной переменной, которая ниже используется через замыкание.
Наконец, создаётся и возвращается, как результат alert Promise, в котором в результате закрытия модального окна выполняется resolve этого Promise.
Теперь посмотрим, как можно использовать этот alert:
$('p a[href="#"]').on('click', async (e) => {
e.preventDefault();
await alert('Promise based alert sample');
});
В данном примере при клике на пустые ссылки внутри параграфов выводится сообщение. Обращаю внимание! Для соответствия спецификации функция alert должна предваряться ключевым словом await, а оно может быть использовано только внутри функции с ключевым словом async. Это позволяет в данном месте ожидать (скрипт остановится, как и в случае с нативным alert) закрытия модального окна.
Что будет если этого не сделать? Зависит от логики вашего приложения. Если это конец кода или дальнейшие действия кода не перегружают страницу, то вероятно, всё будет нормально! Модальное окно провисит пока его не закроет пользователь. Но если будут ещё модальные окна или если страница перезагрузится, произойдёт переход на другую страницу, то пользователь просто не увидит вашего модального окна и логика будет разрушена. Могу сказать, что по опыту, сообщения о различных серверных ошибках (состояниях) или из библиотек кода вполне хорошо работают с нашим новым alert, хотя и не используют await.
Пойдём дальше. Без сомнения confirm может использоваться только в обвязке async/await, т.к. он должен сообщить коду результат выбора пользователя. Это относится и к prompt. Итак, confirm:
<div id=«PromiseConfirm» class=«modal»>
<div class=«modal-dialog» role=«document»>
<div class=«modal-content»>
<div class=«modal-header»>
<h5 class=«modal-title»><i class=«fas fa-check-circle text-success»></i> <span>Confirm app request</span></h5>
<button type=«button» class=«close» data-dismiss=«modal» aria-label=«Close»>
<span aria-hidden=«true»>×</span>
</button>
</div>
<div class=«modal-body»>
<p></p>
</div>
<div class=«modal-footer»>
<button type=«button» class=«btn btn-success» data-dismiss=«modal»>OK</button>
<button type=«button» class=«btn btn-danger» data-dismiss=«modal»>Cancel</button>
</div>
</div>
</div>
</div>
window.confirm = (message) => {
$('#PromiseConfirm .modal-body p').html(message);
var PromiseConfirm = $('#PromiseConfirm').modal({
keyboard: false,
backdrop: 'static'
}).modal('show');
let confirm = false;
$('#PromiseConfirm .btn-success').on('click', e => {
confirm = true;
});
return new Promise(function (resolve, reject) {
PromiseConfirm.on('hidden.bs.modal', (e) => {
resolve(confirm);
});
});
};
Тут есть отличие только в одном – нам нужно сообщить о выборе пользователя. Это делается с помощью ещё одной локальной переменной в замыкании – confirm. В случае нажатия подтверждающей кнопки, переменная устанавливается в true, а по умолчанию её значение false. Ну и при обработке закрытия модального окна resolve отдаёт эту переменную.
Вот использование (обязательно с async/await):
$('p a[href="#"]').on('click', async (e) => {
e.preventDefault();
if (await confirm('Want to test the Prompt?')) {
let prmpt = await prompt('Entered value:');
if (prmpt) await alert(`entered: «${prmpt}»`);
else await alert('Do not enter a value');
}
else await alert('Promise based alert sample');
});
Тут уже реализована логика и с тестом prompt. А его разметка и логика такие:
<div id=«PromisePrompt» class=«modal»>
<div class=«modal-dialog» role=«document»>
<div class=«modal-content»>
<div class=«modal-header»>
<h5 class=«modal-title»><i class=«fas fa-question-circle text-primary»></i> <span>Prompt request</span></h5>
<button type=«button» class=«close» data-dismiss=«modal» aria-label=«Close»>
<span aria-hidden=«true»>×</span>
</button>
</div>
<div class=«modal-body»>
<div class=«form-group»>
<label for=«PromisePromptInput»></label>
<input type=«text» class=«form-control» id=«PromisePromptInput»>
</div>
</div>
<div class=«modal-footer»>
<button type=«button» class=«btn btn-success» data-dismiss=«modal»>OK</button>
<button type=«button» class=«btn btn-danger» data-dismiss=«modal»>Cancel</button>
</div>
</div>
</div>
</div>
window.prompt = (message) => {
$('#PromisePrompt .modal-body label').html(message);
var PromisePrompt = $('#PromisePrompt').modal({
keyboard: false,
backdrop: 'static'
}).modal('show');
$('#PromisePromptInput').focus();
let prmpt = null;
$('#PromisePrompt .btn-success').on('click', e => {
prmpt = $('#PromisePrompt .modal-body input').val();
});
return new Promise(function (resolve, reject) {
PromisePrompt.on('hidden.bs.modal', (e) => {
resolve(prmpt);
});
});
};
Отличие логики от confirm минимальное. Дополнительная локальная переменная в замыкании – prmpt. И у неё не логическое значение, а строка, которую вводит пользователь. Через замыкание её значение отдаёт resolve. А значение ей присваивается только при нажатии кнопки подтверждения (из поля input). Кстати, тут я разбазарил ещё одну глобальную переменную PromisePromptInput, просто для сокращения и альтернативы кода. С её помощью я устанавливаю фокус ввода (хотя можно сделать в едином подходе – либо так, либо как в получении значения).
Испытать этот подход в действии можно по ссылке https://promisealert.web2each.net/. Код находится по ссылке https://promisealert.web2each.net/js/site.js.
Вспомогательные средства. Они не относятся непосредственно к теме статьи, но позволяют раскрыть всю гибкость подхода.
Сюда относятся темы bootstrap. Бесплатные темы я взял из https://bootswatch.com/.
Переключение языка с использованием автоматической установки по языку браузера. Тут три режима – автомат (по браузеру), русский или английский (принудительно). Автомат установлен по умолчанию.
Куки (отсюда https://ruseller.com/lessons.php?id=593) я использовал для запоминания темы и переключателя языка.
Темы переключаются просто установкой сегмента href css с вышеупомянутого сайта:
$('#themes a.dropdown-item').on('click', (e) => {
e.preventDefault();
$('#themes a.dropdown-item').removeClass('active');
e.currentTarget.classList.add('active');
var cur = e.currentTarget.getAttribute('href');
document.head.children[4].href = 'https://stackpath.bootstrapcdn.com/bootswatch/4.4.1/' + cur + 'bootstrap.min.css';
var ed = new Date();
ed.setFullYear(ed.getFullYear() + 1);
setCookie('WebApplicationPromiseAlertTheme', cur, ed);
});
Ну и запоминаю в Куки для восстановления при загрузке:
var cookie = getCookie('WebApplicationPromiseAlertTheme');
if (cookie) {
$('#themes a.dropdown-item').removeClass('active');
$('#themes a.dropdown-item[href="' + cookie + '"]').addClass('active');
document.head.children[4].href = 'https://stackpath.bootstrapcdn.com/bootswatch/4.4.1/' + cookie + 'bootstrap.min.css';
}
Для локализации я использовал файл localization.json в котором создал словарь ключей на английском и их значений на русском. Для простоты (хотя разметка кое-где усложнилась) я проверяю при переводе только чисто текстовые узлы, заменяя из значения по ключу.
var translate = () => {
$('#cultures .dropdown-toggle samp').text({ ru: ' Русский ', en: ' English ' }[culture]);
if (culture == 'ru') {
let int;
if (localization) {
for (let el of document.all)
if (el.childElementCount == 0 && el.textContent) {
let text = localization[el.textContent];
if (text) el.textContent = text;
}
}
else int = setInterval(() => {
if (localization) {
translate();
clearInterval(int);
}
}, 100);
}
else location.reload();
};
if (culture == 'ru') translate();
так вряд ли хорошо делать в продакшене (лучше на сервере), но тут я могу всё продемонстрировать на клиенте. К серверу я обращаюсь только при смене с русского на английский – просто перегружаю исходную разметку (location.reload).
Последнее, как и предполагалось, сообщение в onbeforeunload выдаётся по алгоритму браузера и наш confirm на это не оказывает влияние.
//window.onbeforeunload = function (e) {
// e.returnValue = 'Do you really want to finish the job?';
// return e.returnValue;
//};
В конце кода есть закомментированный вариант такого сообщения – можно попробовать при переносе его себе.
kocherman
Так много зависимостей. А чем оно лучше стандартных диалогов браузера?
Enmar
имхо автор просто про них не слышал)
ну еще диалоги ввели вроде в HTML 5.1
Если браузер не поддерживает эту версию, то можно использовать диалоги на JS
kocherman
Да уже года 3 как поддерживается везде кроме проприетарных недобраузеров. Но и для них полифил есть на той же странице MDN. Не вижу причин не пользоваться ими…
galaxy
Ну да, всего-то не поддерживают Firefox, Safari и IE (<Edge).
degorov Автор
Действительно не слышал — использую только уже распространенные технологии для большинства современных браузеров. Но сейчас посмотрел — принцип тот-же, что и в других модальных окнах — для обработки нужно реагировать на элементы управления в модальном окне. А в стандартных функциях (и в моём случае) — не нужно разделять код запроса окна, реакцию на ответ пользователя и его обработку в разных фрагментах кода.
degorov Автор
Лучше тем, что одинаковые в различных браузерах, содержат настраиваемую под проект и задачу информацию и приводятся к единому дизайну страницы.
kocherman
У меня и стандартные HTML одинаковые во всех браузерах, содержат настраиваемую под проект и задачу информацию и приводятся к единому дизайну включенной темы GTK. Весьма спорный аргумент,
<dialog>
может содержать любую информацию и мало чем отличается от, например,<div>
или<body>
. И свойства CSS на них распространяются не хуже других. Модальное окно не останавливает работу JS, но при этом также может ожидать промиса ответа диалога.Я вот одного не понимаю, зачем для
alert, confirm
иprompt
подключать jQuery, да еще и с bootstrap и Font Awesome.Вам не кажется это перебором, особенно для реализации в качестве достойной замены базовым возможностям HTML?
degorov Автор
Как я вижу, темы GTK тут вообще не причём — речь о стилизации под темы внутри страниц HTML.
Повторюсь — есть масса возможностей создавать диалоги средствами и jQuery и bootstrap и др. но alert, confirm и prompt позволяют писать непрерывную логику программы. В отличие от других средств, разрывающих логику на «до запроса диалога» и коллбэк реакции на действия пользователя. Но дизайн и содержимое alert, confirm и prompt не соответствуют дизайну страницы и имеют разный дизайн и содержимое в разных браузерах (это особенно плохо для игр). Я объединяю функциональные аспекты alert, confirm и prompt (очень удобные) и аспекты дизайна простым приёмом…
vlreshet
Для того чтобы избежать коллбеков придумали async-await
degorov Автор
Так статья о них!