Pull to refresh

Как создать кастомизируемый вид для alert(), confirm() и prompt() для использования в JavaScript

Reading time 8 min
Views 23K
Я давно думал о кастомизации внешнего вида типовых функций взаимодействия с пользователем в JavaScript — alert(), confirm() и prompt() (далее модальные окна).
Действительно, они очень удобны в использовании, но разные в различных браузерах и весьма неприглядны на вид.
Наконец руки дошли.
В чём проблема? Обычные средства выдачи диалогов (например, bootstrap) не получится использовать также просто, как и alert, где браузер организует остановку выполнения кода JavaScript и ожидание действия пользователя (клик на кнопке закрытия). Modal в bootstrap потребует отдельную обработку события – клик на кнопке, закрытие модального окна…
Тем ни менее я уже использовал кастомизацию alert в играх для замены стандартных сообщений на соответствующие стилю игрового оформления. Это хорошо работает, включая сообщения об ошибках соединения и других системных ситуациях. Но это не сработает для случая необходимости ожидания ответа пользователя!
image
С появлением Promise в ECMAScript 6 (ES6) всё стало возможным!
Я применил подход разделения дизайна модальных окон и кода (alert(), confirm() и prompt()). Но можно всё упрятать в код. Чем привлекает такой подход – дизайн можно менять в разных проектах, да просто на разных страницах или в зависимости от ситуации.
Плохой момент этого подхода состоит в необходимости использовать имена (id) разметки в коде модальных окон, да ещё и в глобальной области видимости. Но это просто пример принципа, поэтому я не буду заострять на этом внимание.

Начинаем код для alert


Итак, разберём разметку (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


Пойдём дальше. Без сомнения 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


Выше уже реализована логика и с тестом 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, просто для сокращения и альтернативы кода. С её помощью я устанавливаю фокус ввода (хотя можно сделать в едином подходе – либо так, либо как в получении значения).
Испытать этот подход в действии можно по ссылке. Код находится по ссылке.
Выглядит это примерно так (хотя по ссылке выше всё более разнообразно):
image

Вспомогательные средства


Они не относятся непосредственно к теме статьи, но позволяют раскрыть всю гибкость подхода.
Сюда относятся темы bootstrap. Бесплатные темы я взял тут.
Переключение языка с использованием автоматической установки по языку браузера. Тут три режима – автомат (по браузеру), русский или английский (принудительно). Автомат установлен по умолчанию.
Куки (отсюда) я использовал для запоминания темы и переключателя языка.
Темы переключаются просто установкой сегмента 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;
    //};
Tags:
Hubs:
-18
Comments 20
Comments Comments 20

Articles