ilyachalov (ilyachalov) wrote,
ilyachalov
ilyachalov

Category:

JavaScript: модальное диалоговое окно из HTML-формы (решение задачи)

Начало:
1. JavaScript: модальное диалоговое окно из HTML-формы (каркас)
2. JavaScript: модальное диалоговое окно из HTML-формы (стили CSS)

Заканчиваю решать задачу, начатую в предыдущих двух постах. Там я разбирал код HTML тестовой HTML-страницы и стили CSS к этому коду, с помощью которых создано содержимое «главного окна» и модальное «дочернее диалоговое окно». Здесь я буду именно решать задачу, в основном с помощью написания скрипта на языке JavaScript.

В тексте постановки задачи ставится цель — создать функцию showPrompt(html, callback), которая показывает модальное диалоговое окно. Пишем код:
function showPrompt(html, callback) {
    //...
}

В содержимом «главного окна» есть кнопка для вызова модального диалогового окна. Следовательно, мы должны повесить вызов нашей функции showPrompt на событие click этой кнопки. Кроме того, в тексте постановки задачи приведен пример использования нашей функции showPrompt:
showPrompt("Введите что-нибудь<br>...умное :)", function(value) {
  alert(value);
});

Объединим эти два момента в одну инструкцию и дополним наш код:
function showPrompt(html, callback) { // вызов модального «дочернего диалогового окна»
    //...
}

// функционал кнопки «главного окна», вызывающей модальное «дочернее диалоговое окно»
button.addEventListener("click", () => showPrompt("Введите что-нибудь<br>...умное :)", function(value) {
  alert(value);
}));

Теперь пропишем в нашей функции запуск отображения модального «дочернего диалогового окна» (оно уже существует, но пока скрыто). Тут нужно помнить, что некоторые действия с окном можно производить до его отображения на экране, а некоторые действия можно производить только тогда, когда окно уже отображено. Например, текст надписи модального диалогового окна можно установить до того, как оно станет видимым, а постановку фокуса в текстовое поле ввода (требуемую по условиям задачи) необходимо произвести после того, как окно станет видимым. Дополним код:
function showPrompt(html, callback) { // вызов модального «дочернего диалогового окна»
    window["prompt-message"].innerHTML = html;      // надпись на окне

    // (1) функции-обработчики, которым нужен доступ к функции callback
    //...

    window["prompt-form-container"].hidden = false; // сделаем окно видимым
    window["prompt-form"].text.focus();             // установка фокуса
}

// (2) функции-обработчики, которым НЕ нужен доступ к функции callback
//...

// функционал кнопки «главного окна», вызывающей модальное «дочернее диалоговое окно»
button.addEventListener("click", () => showPrompt("Введите что-нибудь<br>...умное :)", function(value) {
  alert(value);
}));

Теперь по нажатию кнопки «главного окна» у нас на экране уже появляется «дочернее диалоговое окно», которое является модальным (запрещает доступ к содержимому «главного окна», пока открыто «дочернее диалоговое окно»). Но пока что не работают кнопки «дочернего диалогового окна», а также табуляция работает не так, как требуется по условиям задачи.

И тут надо решить, где располагать функции и другие инструкции, которые будут обеспечивать функционал «дочернего диалогового окна», так как есть два варианта: внутри функции showPrompt или снаружи ее.

Мы будем вешать функции на некие события. Если выполнять это внутри функции showPrompt, то нужно учитывать, что эту функцию пользователь может запускать с помощью кнопки из содержимого «главного окна» столько раз, сколько он захочет. Следовательно, при закрытии «дочернего диалогового окна» потребуется снимать функции, которые ранее были повешены на события внутри функции showPrompt, иначе при втором и последующем вызовах «дочернего диалогового окна» на одной кнопке этого окна, к примеру, будет висеть уже несколько функций-обработчиков (с учетом не удаленных в прошлые вызовы «дочернего диалогового окна»).

Если же функции-обработчики вешать на события вне функции showPrompt, то их снятие с событий прописывать не придется, потому что в этом месте функции-обработчики будут повешены на события только один раз, при загрузке тестовой HTML-страницы.

Я разделил функции-обработчики на две части: те, которым нужен доступ к функции callback, я определю внутри функции showPrompt, а те, которым не нужен доступ к функции callback, я определю вне функции showPrompt. Эти два места я пометил в коде выше. Далее я не буду копировать вышеописанный код, чтобы не увеличивать размер поста понапрасну. Но я буду отмечать код соответствующими комментариями с номерами, чтобы его можно было вставить в вышеописанный.

Табуляция

На данный момент мы можем открыть «дочернее диалоговое окно» кнопкой из содержимого «главного окна». Фокус после открытия дочернего окна программно устанавливается в текстовое поле ввода.

При этом работает механизм табуляции, обеспечиваемый браузером по умолчанию: при нажатии клавиши Tab с клавиатуры фокус переместится из текстового поля ввода на кнопку «Ok», затем — на кнопку «Отмена». При дальнейшем нажатии клавиши Tab фокус уйдет из «дочернего диалогового окна». Если нажимать сочетание клавиш Shift+Tab, то фокус будет перемещаться в противоположном направлении: от кнопки «Отмена» к кнопке «Ok», затем в текстовое поле ввода, а затем — уйдет из «дочернего диалогового окна».

По условиям задачи требуется сделать так, чтобы фокус при нажатии клавиши Tab и сочетания клавиш Shift+Tab не уходил из «дочернего диалогового окна», а ходил по кругу, составленному из трех элементов: текстового поля ввода, кнопок «Ok» и «Отмена».

Как это сделать? Я решил повесить на «крайние» звенья этой цепи табуляции соответствующие функции-обработчики, которые будут устанавливать фокус в нужный момент программно на нужные элементы, а не оставлять это браузеру. Тут важно не забыть отключить умолчательное поведение браузера, иначе программное переключение фокуса сработает не так, как мы хотим (я сначала про это забыл и долго тупил над кодом). Пишем код:
// (2) функции-обработчики, которым НЕ нужен доступ к функции callback

// замыкаем цепь табуляции со стороны кнопки «Отмена» при нажатии клавиши Tab
window["prompt-form"].cancel.addEventListener("keydown", function(event) {
    if (event.code == "Tab" && !event.shiftKey) { // Tab без Shift
        window["prompt-form"].text.focus();       // фокус — на текстовое поле ввода
        event.preventDefault();                   // отменяем действие браузера
    }
});

// замыкаем цепь табуляции со стороны текстового поля ввода при нажатии Shift+Tab
window["prompt-form"].text.addEventListener("keydown", function(event) {
    if (event.code == "Tab" && event.shiftKey) { // Tab с Shift
        window["prompt-form"].cancel.focus();    // фокус — на кнопку «Отмена»
        event.preventDefault();                  // отменяем действие браузера
    }
});

Теперь табуляция (смена фокуса) происходит по кругу из трех нужных нам элементов, то есть так, как и требовалось по условиям задачи.

Функционал «дочернего диалогового окна»

Нам нужно обработать не только нажатия на кнопки «Ok» и «Отмена», а также нажатие на клавишу Enter в текстовом поле ввода и нажатие на клавишу Esc в любом месте «дочернего диалогового окна».

Нажатие кнопки «Ok» и нажатие клавиши Enter перехватим на событии submit HTML-формы (эти два действия по условиям задачи должны вызвать одну и ту же реакцию). Нажатие кнопки «Отмена» перехватим на событии click этой кнопки, а нажатие клавиши Esc перехватим на событии keydown HTML-страницы (эти два действия по условиям задачи тоже должны вызвать одну и ту же реакцию).

Все функции-обработчики здесь должны иметь имена, потому что при закрытии (выходе) «дочернего диалогового окна» их нужно будет снять с соответствующих событий по причине, уже поясненной ранее.

При обработке события submit следует отменить действие браузера по умолчанию (по умолчанию при этом событии HTML-страница будет обновлена, а на сервер будет отправлен соответствующий запрос, после чего адрес HTML-страницы в адресной строке браузера изменится и станет содержать параметры этого запроса). В данном случае по условиям задачи отправка запроса на сервер не требуется.

Я в самом начале решил описать функцию onExit, которая будет выполняться в конце каждого обрабатываемого нами действия (ведь в конце каждого обрабатываемого действия должно быть сделано одно и то же: очищено текстовое поле ввода, скрыто «дочернее диалоговое окно», с соответствующих событий сняты функции-обработчики). Пишем код:
    // (1) функции-обработчики, которым нужен доступ к функции callback

    // функция, которая выполняется в конце каждого обрабатываемого действия
    function onExit() {                                // при выходе из диалогового окна
        window["prompt-form"].text.value = "";         // очистить поле ввода
        window["prompt-form-container"].hidden = true; // скрыть диалоговое окно
                                                       // снять обработчики
        window["prompt-form"].removeEventListener("submit", onSubmit);
        window["prompt-form"].cancel.removeEventListener("click", onCancel);
        document.removeEventListener("keydown", onEscape);
    }

    // при нажатии кнопки «Ok» и при нажатии клавиши Enter в текстовом поле ввода
    window["prompt-form"].addEventListener("submit", onSubmit);
    function onSubmit() {
        event.preventDefault();                     // отменить действие браузера
        callback(window["prompt-form"].text.value); // вызвать функцию callback
        onExit();                                   // вызвать действия при выходе
    }

    // при нажатии кнопки «Отмена»
    window["prompt-form"].cancel.addEventListener("click", onCancel);
    function onCancel() {
        callback(null); // вызвать функцию callback
        onExit();       // вызвать действия при выходе
    }
    
    // при нажатии клавиши Esc
    document.addEventListener("keydown", onEscape);
    function onEscape(event) {                  // сделать то же, что и при
        if (event.code == "Escape") onCancel(); // нажатии на кнопку «Отмена»
    }

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

Визуализация модальности

При создании подобных интерфейсов полезно показать пользователю визуально, что открывшееся «дочернее диалоговое окно» является модальным. Как это сделать?

Сначала я при открытии «дочернего диалогового окна» просто устанавливал для «главного окна» серый фон вместо белого:
document.body.style.backgroundColor = "silver";
а при закрытии «дочернего диалогового окна» возвращал цвет фона к исходному:
document.body.style.backgroundColor = "";

Пользователь видит, что при открытии «дочернего диалогового окна» цвет фона «главного окна» меняется и это служит ему подсказкой, что содержимое «главного окна» временно недоступно.

Позже я нашел более изящное решение: вместо изменения цвета фона «главного окна» можно менять степень прозрачности фона контейнера HTML-формы «дочернего диалогового окна» (раньше я никогда не манипулировал прозрачностью элементов, поэтому не сразу до этого додумался). То есть вместо вышеописанных двух строк кода можно использовать следующие. При открытии «дочернего диалогового окна»:
window["prompt-form-container"].style.background = "rgba(0, 0, 0, 0.4)";
и при его закрытии:
 window["prompt-form-container"].style.background = "";

Первую из этих строк кода можно добавить самой последней в функцию showPrompt. Вторую из этих строк кода можно добавить в функцию onExit.

Авторы задачи в своем решении пошли более сложным путем: они создают еще один HTML-элемент div, у которого и регулируют свойство (степень непрозрачности) opacity. Им приходится обходить то, что это свойство действует и на дочерние элементы. Я же руководствовался следующим вопросом и ответами к нему на известном портале «Stackoverflow.com»:

https://stackoverflow.com/questions/13508877/resetting-the-opacity-of-a-child-element-maple-browser-samsung-tv-app
Tags: Образование, Программирование
Subscribe

  • Кэнтаро Миура умер

    Оказывается, 6 мая этого ( 2021) года умер от разрыва аорты японский мангака Кэнтаро Миура. Земля пухом. Ему было всего лишь 54 года. Я его знал…

  • С распадом СССР левая идея не умерла

    По этому поводу можно написать несколько книг, но я к этому не готов, просто хочу чиркнуть пару строк. Что я подразумеваю под «левой идеей»? По…

  • Роскомнадзор и DeviantArt.com, 2021 год

    Система блокировки сайтов в России в некоторых случаях уже работает настолько четко (не прошло и десяти лет с ее создания), что люди не успевают за…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments