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

Я давно думал о кастомизации внешнего вида типовых функций взаимодействия с пользователем в 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;
    //};

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 20

    0
    С появлением Promise в ECMAScript 6 (ES6) всё стало возможным!

    разберём разметку (bootstrap и Font Awesome для шрифтовых икон) и код alert (я использую jQuery)

    Так много зависимостей. А чем оно лучше стандартных диалогов браузера?
      0

      имхо автор просто про них не слышал)
      ну еще диалоги ввели вроде в HTML 5.1
      Если браузер не поддерживает эту версию, то можно использовать диалоги на JS

        +1
        Да уже года 3 как поддерживается везде кроме проприетарных недобраузеров. Но и для них полифил есть на той же странице MDN. Не вижу причин не пользоваться ими…
          0
          Ну да, всего-то не поддерживают Firefox, Safari и IE (<Edge).
          0
          Действительно не слышал — использую только уже распространенные технологии для большинства современных браузеров. Но сейчас посмотрел — принцип тот-же, что и в других модальных окнах — для обработки нужно реагировать на элементы управления в модальном окне. А в стандартных функциях (и в моём случае) — не нужно разделять код запроса окна, реакцию на ответ пользователя и его обработку в разных фрагментах кода.
          0
          Лучше тем, что одинаковые в различных браузерах, содержат настраиваемую под проект и задачу информацию и приводятся к единому дизайну страницы.
            0
            Лучше тем, что одинаковые в различных браузерах.

            У меня и стандартные HTML одинаковые во всех браузерах, содержат настраиваемую под проект и задачу информацию и приводятся к единому дизайну включенной темы GTK. Весьма спорный аргумент, <dialog> может содержать любую информацию и мало чем отличается от, например, <div> или <body>. И свойства CSS на них распространяются не хуже других. Модальное окно не останавливает работу JS, но при этом также может ожидать промиса ответа диалога.

            Я вот одного не понимаю, зачем для alert, confirm и prompt подключать jQuery, да еще и с bootstrap и Font Awesome.

            Вам не кажется это перебором, особенно для реализации в качестве достойной замены базовым возможностям HTML?
              0
              Как я вижу, темы GTK тут вообще не причём — речь о стилизации под темы внутри страниц HTML.
              Повторюсь — есть масса возможностей создавать диалоги средствами и jQuery и bootstrap и др. но alert, confirm и prompt позволяют писать непрерывную логику программы. В отличие от других средств, разрывающих логику на «до запроса диалога» и коллбэк реакции на действия пользователя. Но дизайн и содержимое alert, confirm и prompt не соответствуют дизайну страницы и имеют разный дизайн и содержимое в разных браузерах (это особенно плохо для игр). Я объединяю функциональные аспекты alert, confirm и prompt (очень удобные) и аспекты дизайна простым приёмом…
                0
                Для того чтобы избежать коллбеков придумали async-await
                  0
                  Так статья о них!
          +6
          Шёл 2020-ый год, люди изобретали свои модалки на jQuery
            –2
            Если ВЫ сделали вёрстку в Bootstrap, то модалка в jQuery UI вряд ли кому понравится (без очень трудоёмких настроек CSS). В самом Bootstrap есть модалки! О это открытие! Я их и использую. Весь вопрос в ОСТАНОВКЕ программы в месте вызова окна, а не в танцах с обратными вызовами, разрывающими логику программы.
              0
              Открыл я только что документацию jQuery UI. Глянул их пример. Через минуту написал вот этот код.
              пример
              const confirm = id => new Promise((resolve, reject) => {
                $(id).dialog({
                  resizable: false,
                  height: "auto",
                  width: 400,
                  modal: true,
                  buttons: {
                    "Delete all items": function() {
                      resolve(true);
                      $( this ).dialog( "close" );
                    },
                    Cancel: function() {
                      resolve(false)
                      $( this ).dialog( "close" );
                    }
                  }
                });
              })
              
              // использование без "танцев" и "разрываний логики"
              
              async function init () {
                const confirmed = await confirm("#my-modal");
              
                if (confirmed) {
                  //...
                }
              }
              
              init();
              


              Скажу честно — не тестил, но на 99% уверен что заработает. Ещё один процент на синтакс-ошибку, или ещё какую мелочь. Чем не устраивает?
                0
                Всем устраивает! Это примерно то, что я и предлагаю, только немного другая реализация и без акцента на дизайн…
                0
                А где здесь ОСТАНОВКА программы?
                  0
                  await это ОСТАНОВКА программы до действия пользователя
              +2

              Переопределение стандартных функций (в вашем случае alert, confirm и prompt) – это за гранью добра и зла.

                0
                Конечно, НЕ ОБЯЗАТЕЛЬНО переопределять стандартные функции — можно сделать другие. Но тогда сообщения сервера и встроенных библиотек будут очень выделяться. Особенно это плохо в играх с очень специфичным дизайном(((
                  0

                  Это не то что не обязательно, так делать нельзя в принципе, это самая что ни на есть bad practice.
                  Единственная легальная причина переопределять стандартные методы/функции/классы/… это полифилы.


                  Но тогда сообщения сервера и встроенных библиотек будут очень выделяться

                  Связи между этим и переопределением функций alert, confirm и prompt я вообще не вижу.

                    –1
                    Вот как выглядит alert в моей игре:
                    image
                    В этом его преимущество!

              Only users with full accounts can post comments. Log in, please.