Как найти модальное окно

В этой статье мы разберемся как создавать модальные окна с помощью javascript.

Модальные окна которые мы сделаем буду универсальны. Для создания модальных окон в дальнейшем вам не нужно будет дописывать что-то в .js файле.

Демо на codepen (Javascript)
Демо на я codepen (jQuery)

Весь код для копирования в конце статьи.

Создадим разметку:

<!-- Элементы для вызова модальных окон, могут быть любые -->

<a href="#">Открыть окно 1</a>
<a href="#">Открыть окно 2</a>


<!-- Несколько модальных окон -->

<div class="modal" data-modal="1">
   <!--   Svg иконка для закрытия окна  -->
   <svg class="modal__cross" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M23.954 21.03l-9.184-9.095 9.092-9.174-2.832-2.807-9.09 9.179-9.176-9.088-2.81 2.81 9.186 9.105-9.095 9.184 2.81 2.81 9.112-9.192 9.18 9.1z"/></svg>
   <p class="modal__title">Заголовок окна 1</p>
</div>

<div class="modal" data-modal="2">
   <!--   Svg иконка для закрытия окна  -->
   <svg class="modal__cross" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M23.954 21.03l-9.184-9.095 9.092-9.174-2.832-2.807-9.09 9.179-9.176-9.088-2.81 2.81 9.186 9.105-9.095 9.184 2.81 2.81 9.112-9.192 9.18 9.1z"/></svg>
   <p class="modal__title">Заголовок окна 2</p>
</div>

<!-- Подложка под модальным окном -->
<div class="overlay" id="overlay-modal"></div>

Css код:

/* Стили для подложки */

.overlay {
   
   /* Скрываем подложку  */
   opacity: 0;
   visibility: hidden;
   
   position: fixed;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   background-color: rgba(0, 0, 0, .5);
   z-index: 20;
   transition: .3s all;
}


/* Стили для модальных окон */

.modal {
   
   /* Скрываем окна  */
   opacity: 0;
   visibility: hidden;
   
   
   /*  Установаем ширину окна  */
   width: 100%;
   max-width: 500px;
   
   /*  Центрируем и задаем z-index */
   position: fixed;
   top: 50%;
   left: 50%;
   transform: translate(-50%, -50%);
   z-index: 30; /* Должен быть выше чем у подложки*/
   
   /*  Побочные стили   */
   box-shadow: 0 3px 10px -.5px rgba(0, 0, 0, .2); 
   text-align: center;
   padding: 30px;
   border-radius: 3px;
   background-color: #fff;
   transition: 0.3s all;
}

/* Стили для кнопки закрытия */

.modal__cross {
   width: 15px;
   height: 15px;
   position: absolute;
   top: 20px;
   right: 20px;
   fill: #444;
   cursor: pointer;
}


/* Стили для кнопок. Мы ведь порядочные разработчики, негоже простые ссылки оставлять */

a {
   padding: 20px;
   display: inline-block;
   text-decoration: none;
   background-color: #414b52;
   margin: 10px;
   color: #fff;
   border-radius: 3px;
}

Вот что получилось в итоге (это скрин, не кликать :)):

Кнопки открытия окна

План работы скрипта:

  1. Зарегистрировать событие клика на элементы с классом js-open-modal
  2. При клике на кнопку, ищем модальное окно с таким же атрибутом data-modal, добавляем класс .active подложке и этому модальному окну
  3. При клике на крестик удаляем класс у родительского модального окна и подложки

Пишем JavaScript

В начале, повесим на document событие DOMContentLoaded. Это событие сработает когда страница будет загружена.

document.addEventListener('DOMContentLoaded', function() {

});

Затем запишем массив кнопок в переменную используя метод querySelector. Здесь же определим еще 2 переменные: элемент подложки и массив кнопок-крестиков.

document.addEventListener('DOMContentLoaded', function() {

   var modalButtons = document.querySelectorAll('.js-open-modal'),
       overlay      = document.querySelector('#overlay-modal'),
       closeButtons = document.querySelectorAll('.js-modal-close');
});

Заметьте, modalButtons и CloseButtons мы получаем через querySelectorAll, который возвращает массив, мы сделали это потому что нужно обрабатывать клики по всем кнопка, а вот overlay мы получаем через querySelector, он возвращает один элемент.

В html добавим кнопкам классы .js-open-modal. Мы специально будем использовать новый класс с приставкой js, чтобы не путать стили и интерактивность. Все кто будет работать с кодом увидит, что у класса есть приставка js, значит этот класс используется для интерактивности и его лучше не трогать.

<!-- Элементы для вызова модальных окон, могут быть любые -->

<a href="#" class="js-open-modal">Открыть окно 1</a>
<a href="#" class="js-open-modal">Открыть окно 2</a>

После этого нужно повесить событие клика на каждую кнопку. Для этого мы переберем полученный массив кнопок и повесим обработчик на каждый элемент. Перебирать массив мы будем с помощью forEach:

/* Перебираем массив кнопок */
modalButtons.forEach(function(item){

});

В переменной item у нас будет храниться текущий элемент цикла. Повесим обработчик на него:

/* Перебираем массив кнопок */
modalButtons.forEach(function(item){

    /* Назначаем каждой кнопке обработчик клика */
    item.addEventListener('click', function(event) {

     });
});

event или e — объект текущего события. В этом объекте хранятся различные методы и данные. При вызове любого события указание аргумента у функции будет ссылаться на этот объект. Зачем нам нужен этот объект? Об этом чуть ниже.

Что нам нужно сделать теперь?

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

Для этого в объекте события есть метод, который предотвращает стандартное действие элемента.

event.preventDefault();

С предотвращением вопрос решили.

У каждой кнопки есть атрибут data-modal, в нем хранится значение, которое находится у модального окна в таком же атрибуте. Наши действия:

  1. Получить значение атрибута текущей кнопки
  2. Найти модальное окно с помощью этого значения
var modalId = this.getAttribute('data-modal'),
    modalElem = document.querySelector('.modal[data-modal="' + modalId + '"]');

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

В итоге получается такой селектор — ‘.modal=[data-modal=»значение переменной»]’ , который и находит наше модальное окно.

Давайте добавим нашему окну и подложке класс active.

modalElem.classList.add('active');
overlay.classList.add('active');

Напишем стили для классов .active:

.modal.active,
.overlay.active{
   opacity: 1;
   visibility: visible;
}

Весь javascript код который получился:

document.addEventListener('DOMContentLoaded', function() {

   var modalButtons = document.querySelectorAll('.js-open-modal'),
       overlay      = document.querySelector('#overlay-modal'),
       closeButtons = document.querySelector('.js-modal-close');
   
   
   modalButtons.forEach(function(item){
      
      item.addEventListener('click', function(e) {
         
         e.preventDefault();

         var modalId = this.getAttribute('data-modal'),
             modalElem = document.querySelector('.modal[data-modal="' + modalId + '"]');
         
         modalElem.classList.add('active');
         overlay.classList.add('active');

      }); // end click
   }); // end foreach
}); // end ready

Кнопки должны открывать то модальное окно, к которому привязаны. Проверяем:

Демо модального окна
Демо модального окна

Осталось написать закрытие окон по клику на крестик. С перебором элементов и созданием события вы уже знакомы.

closeButtons.forEach(function(item) {

   item.addEventListener('click', function(e) {

   });

}); // end foreach

При клике на крестик нам нужно у этого же элемента найти родителя или деда с классом .modal и удалить у него класс .active.

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

Для таких задач я не буду писать велосипед, а воспользуюсь готовой микро-библиотекой closest. Код библиотеки:

!function(e){"function"!=typeof e.matches&&(e.matches=e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||function(e){for(var t=this,o=(t.document||t.ownerDocument).querySelectorAll(e),n=0;o[n]&&o[n]!==t;)++n;return Boolean(o[n])}),"function"!=typeof e.closest&&(e.closest=function(e){for(var t=this;t&&1===t.nodeType;){if(t.matches(e))return t;t=t.parentNode}return null})}(window.Element.prototype);

Его нужно подключить в отдельном файле или в этом же файле до нашего кода.

Библиотека создает функцию closest. Используя её мы можем искать элемент, который находится выше по дереву и класс которого совпадает с тем который мы ищем.

В нашем случае мы должны найти родителя с классом .modal и не важно является ли он прямым предком или между них есть еще какие-то элементы.

От нашего крестика мы идем вверх и проверяем родителя на класс .modal, не подошел — берем родителя его родителя и опять проверяем, так до самого конца, до window, пока не найдем интересующий нас элемент. В библиотеке jquery такая функция есть изначально, пишется она так же.

closeButtons.forEach(function(item){

   item.addEventListener('click', function(e) {
      console.log(this.closest('.modal'));
   });

}); // end foreach
Элемент в консоли

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

closeButtons.forEach(function(item){

   item.addEventListener('click', function(e) {
      var parentModal = this.closest('.modal');

      parentModal.classList.remove('active');
      overlay.classList.remove('active');
   });

}); // end foreach

Демонстрация работы:

Демо открытия и закрытия окна

Теперь вы можете создавать много много модальных окон не изменяя свой js код.

Демо на codepen

Для многих операция jquery является громоздкой библиотекой. Вы можете использовать вместо этого микро-библиотеки. Сборник библиотек — http://microjs.com/.

Еще один полезный сайт который уже в названии говорит что jquery вам может быть и не нужен. http://youmightnotneedjquery.com/

Спасибо за прочтение статьи! Если материал был полезен для тебя — поставь лайк или напиши об этом в комментариях, это займет 1 минуту, это важно для меня. Спасибо!

Если у тебя есть замечания, пожелания, предложения — можешь так же написать их в комментариях. Я рад новым мнениям и критике.

Дополнение

По просьбе читателя добавляю код для закрытия модального окна по кнопке ESC и при клике на темный фон.

Закрытие окна при клике на ESC:

document.body.addEventListener('keyup', function (e) {
    var key = e.keyCode;

    if (key == 27) {
        document.querySelector('.modal.active').classList.remove('active');
        document.querySelector('.overlay.active').classList.remove('active');
    };
}, false);

Здесь мы создали событие нажатия на поднятия клавиш. В документе будут проверяться любые клавиши, нажатые пользователем. В функции мы указываем параметр, который отвечает за событие нажатия.

У событий связанных с клавишами, есть свойство keycode, которое хранит код нажатой клавиши. У ESC этот код — 27.

Нам остается проверять при каждом нажатии код клавиши с кодом ESC и если есть совпадение — удаляем класс у активного окна и фона.

Скрытие при нажатии

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

Не подглядывать

Не подглядывать

Не подглядывать

Ладно, подглядывай уже 🙂

overlay.addEventListener('click', function() {
    document.querySelector('.modal.active').classList.remove('active');
    this.classList.remove('active');
});

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

То есть при клике на фон, мы говорим: «Удали активный класс у окна и у фона, на который кликнули»

Итоговый код

Html

<!-- Элементы для вызова модальных окон, могут быть любые -->

<a href="#">Открыть окно 1</a>
<a href="#">Открыть окно 2</a>


<!-- Несколько модальных окон -->

<div class="modal" data-modal="1">
   <!--   Svg иконка для закрытия окна  -->
   <svg class="modal__cross" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M23.954 21.03l-9.184-9.095 9.092-9.174-2.832-2.807-9.09 9.179-9.176-9.088-2.81 2.81 9.186 9.105-9.095 9.184 2.81 2.81 9.112-9.192 9.18 9.1z"/></svg>
   <p class="modal__title">Заголовок окна 1</p>
</div>

<div class="modal" data-modal="2">
   <!--   Svg иконка для закрытия окна  -->
   <svg class="modal__cross" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M23.954 21.03l-9.184-9.095 9.092-9.174-2.832-2.807-9.09 9.179-9.176-9.088-2.81 2.81 9.186 9.105-9.095 9.184 2.81 2.81 9.112-9.192 9.18 9.1z"/></svg>
   <p class="modal__title">Заголовок окна 2</p>
</div>

<!-- Подложка под модальным окном -->
<div class="overlay" id="overlay-modal"></div>

Css

/* Стили для подложки */

.overlay {
   
   /* Скрываем подложку  */
   opacity: 0;
   visibility: hidden;
   
   position: fixed;
   top: 0;
   left: 0;
   width: 100%;
   height: 100%;
   background-color: rgba(0, 0, 0, .5);
   z-index: 20;
   transition: .3s all;
}


/* Стили для модальных окон */

.modal {
   
   /* Скрываем окна  */
   opacity: 0;
   visibility: hidden;
   
   
   /*  Установаем ширину окна  */
   width: 100%;
   max-width: 500px;
   
   /*  Центрируем и задаем z-index */
   position: fixed;
   top: 50%;
   left: 50%;
   transform: translate(-50%, -50%);
   z-index: 30; /* Должен быть выше чем у подложки*/
   
   /*  Побочные стили   */
   box-shadow: 0 3px 10px -.5px rgba(0, 0, 0, .2); 
   text-align: center;
   padding: 30px;
   border-radius: 3px;
   background-color: #fff;
   transition: 0.3s all;
}

/* Стили для кнопки закрытия */

.modal__cross {
   width: 15px;
   height: 15px;
   position: absolute;
   top: 20px;
   right: 20px;
   fill: #444;
   cursor: pointer;
}


/* Стили для кнопок. Мы ведь порядочные разработчики, негоже простые ссылки оставлять */

a {
   padding: 20px;
   display: inline-block;
   text-decoration: none;
   background-color: #414b52;
   margin: 10px;
   color: #fff;
   border-radius: 3px;
}

Javascript

document.addEventListener('DOMContentLoaded', function() {
  var modalButtons = document.querySelectorAll('.js-open-modal'),
      overlay      = document.querySelector('#overlay-modal'),
      closeButtons = document.querySelector('.js-modal-close');
  
  /* открытие окон. */
  modalButtons.forEach(function(item){
     
     item.addEventListener('click', function(e) {
        
        e.preventDefault();

        var modalId = this.getAttribute('data-modal'),
            modalElem = document.querySelector('.modal[data-modal="' + modalId + '"]');
        
        modalElem.classList.add('active');
        overlay.classList.add('active');

     }); // end click
  }); // end foreach

  /* закрытие окон */
  closeButtons.forEach(function(item){

    item.addEventListener('click', function(e) {
      var parentModal = this.closest('.modal');

      parentModal.classList.remove('active');
      overlay.classList.remove('active');
    });

  }); // end foreach

  /* закрытие по ESC */
  document.body.addEventListener('keyup', function (e) {
    var key = e.keyCode;

    if (key == 27) {
        document.querySelector('.modal.active').classList.remove('active');
        document.querySelector('.overlay.active').classList.remove('active');
    };
  }, false);

  /* скрытие окна при клике на подложку */
  overlay.addEventListener('click', function() {
    document.querySelector('.modal.active').classList.remove('active');
    this.classList.remove('active');
  });

}); // end ready

How to Build a Modal with JavaScript

It’s probably happened to you before: you unintentionally attempted to perform an action on a webpage, but luckily got a pop-up window asking you to confirm your decision.

This pop-up window is called a modal. It’s a web page element that pops up and displays in front of other page content.

You can use modals for doing things like storing information you don’t want to immediately see on a webpage, creating navigation menus, adding call-to-action elements, and more.

An excellent example is the modal that appears on Twitter when you attempt to close the compose tweet menu.

Twitter warning modal window

You can also use modals for other things like creating call-to-action elements, navigation menus, newsletter widgets, and more.

As a web developer, knowing how to build a modal can be an handy skill to have. In this tutorial, I’ll walk you through the process of how you can create a simple modal using HTML, CSS, and JavaScript.

Here’s a screenshot of what we’ll be building:

A modal built with html, css and javascript

The steps are very easy to follow so you can customize the modal or build your own from scratch – it’s entirely up to you. At the end of this article, I’ll provide the codepen file so you can play around with it.

Step 1 – Add the Markup

Alright, let’s get started with the HTML.

First, you’ll add a section element and give it two classes, modal and hidden. Under this element, you’ll also have a <div> element with a class of overlay and hidden. Then finally, you’ll add a <button> element with a class of btn and btn-open.

Here’s what that looks like:

<section class="modal hidden"></section>
<div class="overlay hidden"></div>
<button class="btn btn-open">Open Modal</button>
  • The section element with a class of modal will serve as your modal container.
  • The div with the class of overlay will serve as your overlay element. This is the dark blurred background you see when the modal is open.
  • The button with the class of btn and btn-open will serve as your modal button so it fires up our modal when you click this button.

Now inside of your modal, add the markup, and also add the X button for closing the modal. This button will be assigned a btn-close class.

So here’s what your complete markup will look like at the end:

<section class="modal hidden">
  <div class="flex">
    <img src="user.png" width="50px" height="50px" alt="user" />
    <button class="btn-close">⨉</button>
  </div>
  <div>
    <h3>Stay in touch</h3>
    <p>
      This is a dummy newsletter form so don't bother trying to test it. Not
      that I expect you to, anyways. :)
    </p>
  </div>

  <input type="email" id="email" placeholder="brendaneich@js.com" />
  <button class="btn">Submit</button>
</section>

<div class="overlay hidden"></div>
<button class="btn btn-open">Open Modal</button>

Important ⚠️ Take note of the hidden class added to the modal and the overlay element. This is very important because you’ll target these classes to hide your modal and overlay using CSS.

Here’s the output:

complete-markup

Step 2 – Style the Modal

Let’s start by resetting the default margin and padding of every element on the page, and then center both the modal and open-modal button.

Now jump over to your CSS and add the following styles:

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: "Inter", sans-serif;
}

body {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: #222;
  position: relative;
  min-height: 100vh;
}

The next step is styling the modal container itself and the elements inside the container. This process is a bit lenghty so I’ll just copy and paste the styling here and then explain it a bit after:

.modal {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.4rem;
  width: 450px;
  padding: 1.3rem;
  min-height: 250px;
  position: absolute;
  top: 20%;
  background-color: white;
  border: 1px solid #ddd;
  border-radius: 15px;
}

.modal .flex {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.modal input {
  padding: 0.7rem 1rem;
  border: 1px solid #ddd;
  border-radius: 5px;
  font-size: 0.9em;
}

.modal p {
  font-size: 0.9rem;
  color: #777;
  margin: 0.4rem 0 0.2rem;
}

button {
  cursor: pointer;
  border: none;
  font-weight: 600;
}

.btn {
  display: inline-block;
  padding: 0.8rem 1.4rem;
  font-weight: 700;
  background-color: black;
  color: white;
  border-radius: 5px;
  text-align: center;
  font-size: 1em;
}

.btn-open {
  position: absolute;
  bottom: 150px;
}

.btn-close {
  transform: translate(10px, -20px);
  padding: 0.5rem 0.7rem;
  background: #eee;
  border-radius: 50%;
}

And here’s the output:

complete-modal-style

What you did was style the modal element and then position it using the absolute property. This works because you added a position relative property to the body element earlier.

You also styled the elements inside of the modal, but I won’t go deep into the details of that because that is not completely important to us here.

Step 3 – Add the Overlay

For the overlay, you want to position it over the entire page with a subtle dark background and blur.

Since you have the position relative to the body element, you can use the position fixed property to add the overlay over the body element. You’ll overlay it 100% of the viewport width and height.

.overlay {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(3px);
  z-index: 1;
}

Here’s the output:

overlay

The overlay works, but you only want it to affect the body element and not the modal. To fix this, add a higher z-index property to the modal container.

.modal {
  z-index: 2;
}

Now the modal should be on the overlay and not behind it.

modal

You’ve successfully created the modal and added an overlay behind it! But you don’t want to show the modal, at least not until the open-modal button is clicked.

To hide it, you need to target the .hidden class you added earlier to the modal and overlay element in your CSS. You’ll also give it a display of none.

.hidden {
  display: none;
}

Now only the button is showing on the page. You can now work on the modal functionality using JavaScript.

Step 4 – Add Modal Functionality

Before we proceed, I believe it is best to explain how the modal works. Remember how you used the hidden class to hide the modal and overlay? To add or remove this class from the elements, you’ll use the DOM’s classList element.

But first, you need to select your classes using the DOM’s querySelector method and store them in variables so they are reusable.

const modal = document.querySelector(".modal");
const overlay = document.querySelector(".overlay");
const openModalBtn = document.querySelector(".btn-open");
const closeModalBtn = document.querySelector(".btn-close");

How to Open the Modal

In other to show the modal, create a function called openModal. Inside this function, you’ll use the DOM classList property which takes in different methods like .remove() and .add() to remove the hidden class from the modal and overlay.

const openModal = function () {
  modal.classList.remove("hidden");
  overlay.classList.remove("hidden");
};

And then you can use an eventListener to tie this function to the open modal button openModalBtn. That way, anytime this button is clicked, the function is executed, which shows the modal.

openModalBtn.addEventListener("click", openModal);

Now when you click on the open modal button, this will remove the hidden class from the modal element and you can see your modal.

Here’s the output:

Open modal

How to Close the Modal

For closing the modal, you’ll also create a function called closeModal. Inside the function, use the .add() method to add back the hidden class you removed.

The classList property also has an add() method which you’ll use to add the hidden class back when you click the closeModal button. Just like you added an eventListener to the button to close the modal, you’ll do the same to the x button – but this time, you’ll add the hidden class back.

const closeModal = function () {
  modal.classList.add("hidden");
  overlay.classList.add("hidden");
};

To close the modal, add an eventListener to the close modal button to execute the function you just wrote now.

closeModalBtn.addEventListener("click", closeModal);

Now when you click the close button, the function will add back the hidden class to the modal and overlay components, thus closing the modal.

Here’s the output

close modal

Usually, modals are also closed when you click outside of the modal container or on the body of the webpage. To do that, add an eventListener to close the modal when you click on the overlay.

overlay.addEventListener("click", closeModal);

close_modal_when_overlay_is_clicked

How to Close the Modal on Key Press

In addition to closing the modal when you click the close button or the overlay, you can also attach an event listener to watch for keyboard events.

In this instance, you want the modal to close when you press the Escape key, much like in the Twitter compose modal example.

document.addEventListener("keydown");

But this time the type of event you want is not the “click” event – you want to use the “keydown” event to execute your function.

Next up, you’ll write a condition that checks if the current key pressed is the Escape key and the modal does not contain the hidden class. So it’s open, and you want to execute the closeModal function (in essence, close the modal).

document.addEventListener("keydown", function (e) {
  if (e.key === "Escape" && !modal.classList.contains("hidden")) {
    modalClose();
  }
});

Now when the modal is open and you hit the <kbd>Esc</kbd> key, it will close the modal.

And with this, you’ve successfully created a modal component with HTML, CSS, and JavaScript and it works just as intended. 🥳

Here’s the codepen file to test this modal in action:

Conclusion

I sincerely hope you found this post interesting or useful. If you did, kindly share with your friends or subscribe to my blog so you won’t miss any future postings. Thanks for reading.

GitHub | Twitter | Blog | Portfolio



Learn to code for free. freeCodeCamp’s open source curriculum has helped more than 40,000 people get jobs as developers. Get started

В этой статье познакомимся со скриптом, предназначенным для динамического создания модальных окон на сайте. Разберём из чего он состоит, как его подключить и использовать.

Что такое модальное окно?

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

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

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

Изображение модального окна:

Вид модального окна, созданного с помощью JavaScript

Оно состоит из заголовка (хедера), основной части и футера.

В заголовке обычно выводят название окна и элемент, с помощью которого его можно закрыть. В основной части располагают содержимое, а в футере – кнопки для выполнения различных действий.

Подключение CSS и JavaScript-файлов к странице

Исходные коды модального окна расположены на GitHub в рамках проекта ui-components в папке modal.

Для установки модального окна на страницу необходимо подключить к ней файлы:

  • CSS: modal.css;
  • JavaScript: modal.js.
<!-- Подключение CSS-файла -->
<link rel="stylesheet" href="modal.css">

<!-- Подключения JavaScript-файла -->
<script src="modal.js"></script>

Создание и настройка модального окна

Этот скрипт создаёт модальное окно динамически. То есть здесь не нужно вставлять какой-то HTML-код непосредственно на страницу. Реализовано это в коде через класс. Шаблон модального окна содержится в приватном свойстве #template:

class ItcModal {
  #template = '<div class="itc-modal-backdrop"><div class="itc-modal-content"><div class="itc-modal-header"><div class="itc-modal-title">{{title}}</div><span class="itc-modal-btn-close" title="Закрыть">×</span></div><div class="itc-modal-body">{{content}}</div>{{footer}}</div></div>';
  // ...
}

Следовательно, для того, чтобы сделать модальное окно достаточно просто создать новый экземпляр класса ItcModal:

const modal = new ItcModal();

При создании окна вы можете сразу же его настроить, для этого в ItcModal необходимо передать аргумент в формате объекта:

const modal = new ItcModal({
  title: 'Заголовок',
  content: '<div>Содержимое модального окна...</div>',
  footerButtons: [
    { class: 'btn btn-close', text: 'Закрыть', action: 'close' },
  ]
});

Ключ title отвечает за заголовок, content – за содержимое, footerButtons – за кнопки в футере окна.

Эти ключи являются не обязательными. Если их не указать, то создастся окно с заголовком «Новое окно», без содержимого и кнопок:

const modal = new ItcModal();

Пример создания модального окна с заголовком «Какой-то текст» и содержимым <p>Мой контент</p>:

const modal = new ItcModal({
  title: 'Какой-то текст',
  content: '<p>Мой контент</p>'
});

В качестве содержимого можно передавать HTML-код.

Добавление кнопок в футер окна осуществляется с помощью ключа footerButtons. Он принимает в качестве значения массив объектов. Каждый объект в этом массиве представляет собой кнопку. Она в свою очередь задаётся посредством ключей text, class и action. С помощью них вы можете кнопке соответственно установить текст, класс и атрибут data-action:

const modal = new ItcModal({
  title: '...',
  content: '<div>...</div>',
  footerButtons: [
    { class: 'btn btn-cancel', text: 'Отмена', action: 'cancel' },
    { class: 'btn btn-ok', text: 'ОК', action: 'ok' }
  ]
});

Методы

Управление созданным модальным окном осуществляется посредством методов:

  • show – открытие;
  • hide – закрытие;
  • dispose – удаление из DOM HTML-элементов модального окна и обработчика события click;
  • setBody – установка основного содержимого;
  • setTitle – изменение заголовка.

Открытие модального окна:

const modal = new ItcModal();
// открыть модальное окно
modal.show();

Скрытие модального окна:

modal.hide();

Изменение заголовка и тела модального окна:

// новый заголовок
modal.setTitle('Текст нового заголовка');
// новое тело
modal.setBody('<div>...</div>');

Уничтожение модального окна:

modal.dispose();

Данную операцию имеет смысл использовать только в том случае, если созданное окно вам больше не нужно на странице.

События

Если вам нужно выполнить какие-то действия при открытии и закрытии модального окна, то можете воспользоваться событиями:

// при открытии модального окна
document.addEventListener('show.itc.modal', (e) => {
  // e.target - содержит ссылку на модальное окно
  e.target.querySelector('.itc-modal-body').innerHTML = 'Содержимое модального окна, добавленное через <code>show.itc.modal</code>...';
});
// при закрытии модального окна
document.addEventListener('hide.itc.modal', (e) => {
  // e.target - содержит ссылку на модальное окно
  e.target.querySelector('.itc-modal-body').innerHTML = '';
});

Примеры

1. Открытие модального окна при нажатии на кнопку:

<button id="show-modal">Открыть</button>

<script>
  // создаём модальное окно
  const modal = new ItcModal();
  // при клике по кнопке #show-modal
  document.querySelector('#show-modal').addEventListener('click', () => {
    // откроем модальное окно
    modal.show();
  });
</script>

2. Открытие одного и того же модального окна при нажатии на разные кнопки (определяется через data-атрибут data-toggle="modal"):

<button data-toggle="modal">Кнопка 1</button>
<button data-toggle="modal">Кнопка 2</button>

<script>
  const modal = new ItcModal({
    content: 'Содержимое модального окна...'
  });
  // при клике на странице
  document.addEventListener('click', (e) => {
    if (e.target.closest('[data-toggle="modal"]')) {
      modal.show();
    }
  });
</script>

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

<button data-toggle="modal" data-title="Заголовок 1" data-content="Содержимое модального окна 1...">Кнопка 1</button>
<button data-toggle="modal" data-title="Заголовок 2" data-content="Содержимое модального окна 2...">Кнопка 2</button>
<script>
  const modal = new ItcModal();
  // при клике на странице
  document.addEventListener('click', (e) => {
    const btn = e.target.closest('[data-toggle="modal"]');
    if (btn) {
      modal.setTitle(btn.dataset.title);
      modal.setBody(btn.dataset.content);
      modal.show();
    }
  });
</script>

4. Обработка события click для кнопок, расположенной в футере модального окна:

<div class="items">
  <div class="item">
    <img src="/examples/images/car-1.jpg" alt="" data-price="22500" data-name="Audi A5 Coupé">
  </div>
  ...
</div>

<script>
  const modal = new ItcModal({
    title: 'Просмотр изображения',
    content: '<img src="" alt="" style="display: block; height: auto; max-width: 100%;">',
    footerButtons: [
      {class: 'btn btn-delete', text: 'Удалить', action: 'delete'},
      {class: 'btn btn-cancel', text: 'Закрыть', action: 'cancel'}
    ]
  });
  // при клике по документу
  document.addEventListener('click', (e) => {
    const img = e.target.closest('img');
    // если мы кликнули на изображение, то...
    if (img) {
      img.classList.add('active');
      // устанавливаем модальному окну title
      modal.setBody(`<div style="flex: 1 0 50%;">
              <img src="${img.src}" alt="${img.alt}" style="display: block; height: auto; max-width: 100%; margin: 0 auto;">
            </div>
            <div style="flex: 1 0 30%; text-align: center;">
              <div style="font-size: 1.125rem; font-weight:bold;">
                ${img.dataset.name}
              </div>
              Цена:<br><b>${img.dataset.price}</b></div>`);
      modal.show();
    }
    if (e.target.closest('[data-action="cancel"]')) {
      modal.hide();
    }
    if (e.target.closest('[data-action="delete"]')) {
      const img = document.querySelector('img.active');
      img.parentElement.remove();
      modal.hide();
    }
  });
</script>

5. Создание 2 разных модальных окон. Первое модальное окно открывается при нажатии на кнопки с data-атрибутом data-toggle="modal-1", а второе – при клике на data-toggle="modal-2":

<button data-toggle="modal-1">Открыть окно 1</button>
<button data-toggle="modal-1">Открыть окно 1</button>
<button data-toggle="modal-2">Открыть окно 2</button>
<button data-toggle="modal-2">Открыть окно 2</button>

<script>
  // создадим модальное окно №1
  const modal1 = new ItcModal({
    title: 'Модальное окно 1',
    content: 'Содержимое модального окна 1'
  });
  // создадим модальное окно №2
  const modal2 = new ItcModal({
    title: 'Модальное окно 2',
    content: 'Содержимое модального окна 2'
  });
  document.addEventListener('click', (e) => {
    // при клике по кнопке data-toggle="modal-1"
    if (e.target.closest('[data-toggle="modal-1"]')) {
      // откроем модальное окно №1
      modal1.show();
    }
    // при клике по кнопке data-toggle="modal-2"
    if (e.target.closest('[data-toggle="modal-2"]')) {
      // откроем модальное окно №2
      modal2.show();
    }
  });
</script>

6. Загрузка данных в модальное окно посредством AJAX:

<a href="#" data-json="/examples/pens/itc-modal/json-1">из json-1</a>
<a href="#" data-json="/examples/pens/itc-modal/json-2">из json-2</a>
...
<script>
  // создадим модальное окно
  const modal = new ItcModal({
    title: 'Модальное окно',
  });
  // при клике по ссылке
  document.addEventListener('click', function (e) {
    const anchor = e.target.closest('a[data-json]');
    if (anchor) {
      e.preventDefault();
      const request = new XMLHttpRequest();
      request.open('GET', 'https://itchief.ru/' + e.target.dataset.json);
      request.send();
      request.onload = () => {
        if (request.status === 200) {
          const data = JSON.parse(request.response);
          let html = '<div style="display: flex; gap: 1rem;"><div style="flex: 1 0 50%;"><img src="{{image}}" alt="" style="display: block; height: auto; max-width: 100%; margin: 0 auto;" width="705" height="440"></div><div style="flex: 1 0 30%; text-align: center;"><div style="font-size: 18px; font-weight:bold;">{{title}}</div>Цена:<br><b>{{price}}</b></div></div>';
          html = html.replace('{{title}}', data.title);
          html = html.replace('{{price}}', data.price);
          html = html.replace('{{image}}', data.image);
          modal.setBody(html);
          // отобразим модальное окно
          modal.show();
        }
      };
    }
  });
</script>

Пример JSON-файла:

{"title":"Audi A5 Coupé","price":"22500$","image":"https://itchief.ru/examples/images/car-1.jpg"}

7. Работа с событиями, возникающими при открытии и закрытии модального окна:

<!-- Кнопки для открытия модального окна -->
<button data-toggle="modal">Кнопка 1</button>
<button data-toggle="modal">Кнопка 2</button>
<div class="message"></div>

<script>
  const modal = new ItcModal({
    title: 'Текст заголовка',
    content: '<p>Содержимое модального окна...</p>',
    footerButtons: [
      {class: 'btn btn-2', text: 'ОК', action: 'ok'},
      {class: 'btn btn-1', text: 'Отмена', action: 'cancel'}
    ]
  });
  document.addEventListener('show.itc.modal', () => {
    document.body.style.backgroundColor = '#fff59d';
  });
  document.addEventListener('hide.itc.modal', () => {
    document.body.style.backgroundColor = '#fff';
  });
  document.addEventListener('click', (e) => {
    document.querySelector('.message').innerHTML = '';
    if (e.target.closest('[data-toggle="modal"]')) {
      modal.setBody(`Вы открыли модальное окно при нажатии на <b>${e.target.textContent}</b>`);
      modal.show();
    }
    if (e.target.closest('[data-action="cancel"]') || e.target.closest('[data-action="ok"]')) {
      const text = (e.target.closest('[data-action="cancel"]') || e.target.closest('[data-action="ok"]')).textContent;
      document.querySelector('.message').innerHTML = `Вы завершили действие с модальным окном посредством кнопки <b>${text}</b>`;
      modal.hide();
    }
    if (e.target.closest('.itc-modal-btn-close')) {
      document.querySelector('.message').textContent = 'Вы закрыли окно с помощью крестика';
    }
  });
</script>

Внутреннее устройство модального окна

Код JavaScript модального окна представлен посредством класса ItcModal:

class ItcModal {
  #elem;
  #template = '<div class="itc-modal-backdrop"><div class="itc-modal-content"><div class="itc-modal-header"><div class="itc-modal-title">{{title}}</div><span class="itc-modal-btn-close" title="Закрыть">×</span></div><div class="itc-modal-body">{{content}}</div>{{footer}}</div></div>';
  #templateFooter = '<div class="itc-modal-footer">{{buttons}}</div>';
  #templateBtn = '<button type="button" class="{{class}}" data-action={{action}}>{{text}}</button>';
  #eventShowModal = new Event('show.itc.modal');
  #eventHideModal = new Event('hide.itc.modal');
  #disposed = false;

  constructor(options = []) { 
    // ...
  }

  #handlerCloseModal(e) {
    // ...
  }

  show() {
    // ...
  }

  hide() {
    // ...
  }

  dispose() {
    // ...
  }

  setBody(html) {
    // ...
  }

  setTitle(text) {
    // ...
  }
};

В конструкторе мы создаём DOM-элемент и формируем его HTML-структуру. Ссылку на созданный элемент мы помещаем в приватное свойство #elem. Данное свойство мы будем использовать в других методах ItcModal. Для вставки на страницу модального окна используется метод append.

Приватное свойство #disposed применяется для хранения состояния. По умолчанию оно имеет значение false. Это свойство связано с методом dispose(). При вызове этого метода, модальное окно удаляется со страницы. Но, кроме этого, также удаляется событие, связанное с ним. Для отметки этого действия, свойству #dispose присваивается значение true:

dispose() {
  this.#elem.remove(this.#elem);
  this.#elem.removeEventListener('click', this.#handlerCloseModal);
  this.#disposed = true;
}

После этого действия мы не сможем открыть модальное окно, так как его уже нет. Чтобы это не приводило к ошибкам в коде, в метод show добавлена следующая проверка:

show() {
  if (this.#disposed) {
    // если модальное окно удалено, то завершаем работу
    return;
  }
  this.#elem.classList.add('itc-modal-show');
  this.#elem.dispatchEvent(this.#eventShowModal);
}

При открытии и закрытии модального окна, код генерирует события show.itc.modal и hide.itc.modal с помощью метода dispatchEvent. Сами события находятся в приватных свойствах #eventShowModal и #eventHideModal. Эти события вызываются для this.#elem. Используя их, вы можете очень просто добавить нужную логику при открытии и закрытии модального окна:

document.addEventListener('show.itc.modal', (e) => {
  // e.target - содержит ссылку на модальное окно
  // ...
});
document.addEventListener('hide.itc.modal', (e) => {
  // e.target - содержит ссылку на модальное окно
  // ...
});

Приватный метод #handlerCloseModal является обработчиком события click и используется для закрытия модального окна. То есть он выполняется при нажатии на крестик или при клике на backdrop. Это действие мы вынесли в отдельный метод для того, чтобы потом мы могли удалить его при вызове метода dispose.

Делаем модальные окна для сайта. Заботимся об удобстве и доступности

Время на прочтение
16 мин

Количество просмотров 143K

Я занимаюсь вёрсткой и программированием сайтов. Почти в каждом макете, который я верстал, были модальные окна. Обычно это формы заказа звонка в лендингах, уведомления о завершении каких-то процессов, или сообщения об ошибках.

Вёрстка таких окон сначала кажется простой задачей. Модальные окна можно сделать даже без помощи JS только лишь с помощью CSS, но на практике они оказываются неудобными, и из-за маленьких недочетов модальные окна раздражают посетителей сайта.

В итоге было задумано сделать собственное простое решение.

Вообще говоря, есть несколько готовых скриптов, JavaScript библиотек, реализующих функционал модальных окон, например:

  • Arctic Modal,
  • jquery-modal,
  • iziModal,
  • Micromodal.js,
  • tingle.js,
  • Bootstrap Modal (из библиотеки Bootstrap) и др.

(в статье не рассматриваем решения на базе Frontend-фреймворков)

Несколькими из них я пользовался сам, но почти у всех находил какие-то недостатки. Некоторые из них требуют подключения библиотеки jQuery, которая есть не на всех проектах. Для разработки своего решения, нужно сначала определиться с требованиями.

Что мы ждём от модальных окон? Отвечая на этот вопрос, я основывался на докладе «Знакомьтесь, модальное окно» Анны Селезнёвой, а так-же на относительно старой статье NikoX «arcticModal — jQuery-плагин для модальных окон».

Итак, чтобы нам хотелось видеть?

  • Окна должны открываться как можно быстрее, без тормозов браузера, с возможностью анимировать открытие и закрытие.
  • Под окном должен быть оверлей. Клик/тап по оверлею должен закрывать окно.
  • Страница под окном не должна прокручиваться.
  • Окон может быть несколько. Открытие одного определенного окна должно осуществляться кликом на любой элемент страницы с data-атрибутом, который мы выберем.
  • Окно может быть длинным – прокручиваемым.
  • Желательно поработать над доступностью, а также с переносом фокуса внутрь окна и обратно.
  • Должно работать на IE11+

Дисклеймер: Прежде чем мы рассмотрим подробности, сразу дам ссылку на готовый код получившейся библиотеки (HystModal) на GitHub, а также ссылку на демо+документацию.

Начнём с разметки.

1. Разметка HTML и CSS

1.1. Каркас модальных окон

Как открыть окно быстро? Самое простое решение: разместить всю разметку модального окна сразу в HTML странице. Затем скрывать/показывать это окно при помощи переключения классов CSS.

Набросаем такую разметку HTML (я назвал этот скрипт «hystmodal»):

<div class="hystmodal" id="myModal">
    <div class="hystmodal__window">
        <button data-hystclose class="hystmodal__close">Close</button>  
        Текст модального окошка.
        <img src="img/photo.jpg" alt="Изображение в окне" />
    </div>
</div>

Итак, разместим перед закрывающим тегом </body> наш блок с окном (.hystmodal). Он будет фоном. Удобно указать уникальный атрибут id (например #myModal) каждому окну (ведь их у нас может быть несколько).

Сделаем так, чтобы .hystmodal растягивался на всё окно браузера и закрывал собой содержимое страницы. Чтобы этого добиться, установим фиксированное позиционирование в CSS и приравняем свойства top, bottom, left и right к нулю.

.hystmodal {
    position: fixed;
    top: 0;
    bottom: 0;
    right: 0;
    left: 0;
    overflow: hidden;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    display: flex;
    flex-flow: column nowrap;
    justify-content: center; /* см. ниже */
    align-items: center;
    z-index: 99;
    /* Чтобы окно не прилипало к границе
    браузера установим отступы */
    padding:30px 0;
}

В этом коде сделаны ещё две вещи:

  1. Так как мы хотим центрировать окно внутри страницы, превращаем .hystmodal в flex-контейнер с выравниваем его потомков по центру по вертикали и горизонтали.
  2. Окно может быть больше высоты экрана браузера, поэтому мы устанавливаем overflow-y: auto, чтобы при переполнении возникала полоса прокрутки. Также, для сенсорных экранов (в основном для Safari) нам стоит установить свойство -webkit-overflow-scrolling: touch, чтобы сенсорная прокрутка работала именно на этом блоке а не на странице.

Теперь установим стили для самого окна.

.hystmodal__window {
    background: #fff;

    /* Установим по умолчанию ширину 600px
    но она будет не больше ширины браузера */
    width: 600px;
    max-width: 100%;

    /* Заготовка для будущих анимаций */
    transition: transform 0.15s ease 0s, opacity 0.15s ease 0s;
    transform: scale(1);
}

Кажется возникли сложности.

Проблема №1. Если высота окна больше высоты окна браузера, то контент окна будет обрезан сверху.

Это возникает из-за свойства justify-content: center. Оно позволяет нам удобно выровнять потомков по основной оси (по вертикали), но если потомок становится больше родителя то часть его становится недоступной даже при прокручиваемом контейнере. Подробнее можно посмотреть на stackoverflow. Решение – установить justify-content: flex-start, а потомку установить margin:auto. Это выровняет его по центру.

Проблема №2. В ie-11 если высота окна больше высоты окна браузера, то фон окна обрезается.

Решение: мы можем установить flex-shrink:0 потомку – тогда обрезки не происходит.

Проблема №3. В браузерах кроме Chrome нет отступа от нижней границы окна (т.е. padding-bottom не сработал).

Сложно сказать баг это браузеров или наоборот соответствует спецификации, но решения два:

  • установить псевдоэлемент ::after после потомка и дать ему высоту вместо padding
  • обернуть элемент в дополнительный блок и дать отступы уже ему.

Воспользуемся вторым методом. Добавим обертку .hystmodal__wrap. Так мы заодно обойдём и проблему №1, а вместо padding у родителя установим margin-top и margin-top у самого .hystmodal__window.

Наш итоговый html:

<div class="hystmodal" id="myModal" aria-hidden="true" >
    <div class="hystmodal__wrap">
        <div class="hystmodal__window" role="dialog" aria-modal="true" >
            <button data-hystclose class="hystmodal__close">Close</button>  
            <h1>Заголовок модального окна</h1>
            <p>Текст модального окна ...</p>
            <img src="img/photo.jpg" alt="Изображение" width="400" />
            <p>Ещё текст модального окна ...</p>
        </div>
    </div>
</div>

В код также добавлены некоторые aria и role атрибуты для обеспечения доступности.

Обновленный код CSS для обертки и окна.

.hystmodal__wrap {
    flex-shrink: 0;
    flex-grow: 0;
    width: 100%;
    min-height: 100%;
    margin: auto;
    display: flex;
    flex-flow: column nowrap;
    align-items: center;
    justify-content: center;
}
.hystmodal__window {
    margin: 50px 0;
    flex-shrink: 0;
    flex-grow: 0;
    background: #fff;
    width: 600px;
    max-width: 100%;
    overflow: visible;
    transition: transform 0.2s ease 0s, opacity 0.2s ease 0s;
    transform: scale(0.9);
    opacity: 0;
}

1.2 Скрываем окно

Сейчас наше окно всегда видно. Когда говорят о скрытии элементов, первое что приходит на ум это переключать свойство display со значения none до нашего значения flex.

Но этот подход нас не устроит, ведь свойство display не анимируется. Все переходы дочерних элементов, указанные в свойстве transition, работать не будут.

Нам поможет другое свойство visibility:hidden. Оно скроет окно визуально, хотя и зарезервирует под него место. А так как все будущие окна на странице имеют фиксированное
позиционирование – они будут полностью скрыты и не повлияют на остальную страницу. Кроме того, на элементы с visibility:hidden нельзя установить фокус с клавиатуры, а от скрин-ридеров мы уже скрыли окна с помощью атрибута aria-hidden="true".

Добавим также классы для открытого окна:

.hystmodal--active{
    visibility: visible;
}
.hystmodal--active .hystmodal__window{
    transform: scale(1);
    opacity: 1;
}

1.3 Оформление подложки

В лучшем случае, нам нужен отдельный html-элемент в качестве оверлея. Можно использовать и имеющийся элемент модального окна .hystmodal в качестве оверлея, но тогда анимация на этом элементе (например переход opacity) будет затрагивать и внутренние элементы. В итоге, не получится анимировать разные свойства для окна и оверлея отдельно.

Просто разместим элемент .hystmodal__shadow прямо перед закрывающим </body>. В будущем, сделаем так, чтобы этот элемент создавался автоматически из js при инициализации библиотеки.

Его свойства:

.hystmodal__shadow{
    position: fixed;
    border:none;
    display: block;
    width: 100%;
    top: 0;
    bottom: 0;
    right: 0;
    left: 0;
    overflow: hidden;
    pointer-events: none;
    z-index: 98;
    opacity: 0;
    transition: opacity 0.15s ease;
    background-color: black;
}
/* активная подложка */
.hystmodal__shadow--show{
    pointer-events: auto;
    opacity: 0.6;
}

1.4 Отключение прокрутки страницы

Когда модальное окна открывается, мы хотим, чтобы страница под ним не прокручивалась.
Самый простой способ этого добиться — повесить overflow:hidden для body или html, когда окно открывается. Однако с этим есть проблема:

Проблема №4. В браузере Safari на iOS страница будет прокручиваться, даже если на тег html или body повешен overflow:hidden.
Решается двумя способами, либо блокированием событий прокрутки (touchmove, touchend или touchsart) из js вида:

targetElement.ontouchend = (e) => {
    e.preventDefault();
};

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

ps: можно конечно применить библиотеку scroll-lock, в которую заложено это решение, но в статье было решено воспользоваться другим вариантом.

Другое решение – основано частично на CSS. Пусть когда окно открывается, на элемент <html> будет добавляться класс .hystmodal__opened:

.hystmodal__opened {
    position: fixed;
    right: 0;
    left: 0;
    overflow: hidden;
}

Благодаря position:fixed, окно не будет прокручиваться даже в safari, однако здесь тоже не всё гладко:

Проблема №5. При открытии/закрытии окна страница прокручивается в начало.
Действительно, это происходит из-за изменения свойства position, текущая прокрутка окна сбрасывается.

Для решения, нам нужно написать следующий JS (упрощенно):

При открытии:

// Находим тег html и сохраняем его
let html = document.documentElement;
//сохраним текущую прокрутку:
let scrollPosition = window.pageYOffset;
//установим свойство top у html равное прокрутке
html.style.top = -scrollPosition + "px";
html.classList.add("hystmodal__opened");

При закрытии:

html.classList.remove("hystmodal__opened");
//прокручиваем окно туда где оно было
window.scrollTo(0, scrollPosition);
html.style.top = "";

Отлично, приступим к JavaScript коду.

2. Код JavaScript

2.2 Каркас библиотеки

Нам нужна совместимость со старыми браузерами включая IE11 поэтому нам нужно выбрать из 2 вариантов кодинга:

  • Разрабатывать на старом стандарте ES5, и использовать только те фичи, которые поддерживают все браузеры.
  • Применить современный синтаксис ES6, но подключить транспайлер Babel, который автоматически преобразует код для всех браузеров и встроит необходимые полифилы.
    Было принято решение использовать второй вариант, с прицелом на будущее.
    Приступим.

Основа нашей библиотеки единственный класс HystModal. Ниже я приведу скелет кода с комментариями, а потом добавим остальной функционал.

class HystModal{
    /**
     * При создании экземпляра класса, мы передаём в него
     * js-объект с настройками. Он становится доступен
     * в конструкторе класса в виде переменной props
     */
    constructor(props){
        /**
         * Для удобства некоторые свойства можно не передавать
         * Мы должны заполнить их начальными значениями
         * Это можно сделать применив метод Object.assign
         */
        let defaultConfig = {
            linkAttributeName: 'data-hystmodal',
            // ... здесь остальные свойства
        }
        this.config = Object.assign(defaultConfig, props);

        // сразу вызываем метод инициализации
        this.init();
    }

    /** 
     * В свойство _shadow будет заложен div с визуальной
     * подложкой. Оно сделано статическим, т.к. при создании
     * нескольких экземпляров класса, эта подложка нужна только
     * одна
     */
    static _shadow = false;

    init(){
        /**
         * Создаём триггеры состояния, полезные переменные и.т.д.
         */
        this.isOpened = false; // открыто ли окно
        this.openedWindow = false; //ссылка на открытый .hystmodal
        this._modalBlock = false; //ссылка на открытый .hystmodal__window
        this.starter = false, //ссылка на элемент "открыватель" текущего окна
        // (он нужен для возвращения фокуса на него)
        this._nextWindows = false; //ссылка на .hystmodal который нужно открыть
        this._scrollPosition = 0; //текущая прокрутка (см. выше)

        /**
         * ... остальное
         */

        // Создаём только одну подложку и вставляем её в конец body
        if(!HystModal._shadow){
            HystModal._shadow = document.createElement('div');
            HystModal._shadow.classList.add('hystmodal__shadow');
            document.body.appendChild(HystModal._shadow);
        }

        //Запускаем метод для обработки событий см. ниже.
        this.eventsFeeler();
    }

    eventsFeeler(){

        /** 
         * Нужно обработать открытие окон по клику на элементы с data-атрибутом
         * который мы установили в конфигурации - this.config.linkAttributeName
         * 
         * Здесь мы используем делегирование события клика, чтобы обойтись одним
         * лишь обработчиком события на элементе html
         * 
         */
        document.addEventListener("click", function (e) {
            /**
             * Определяем попал ли клик на элемент,
             * который открывает окно
             */ 
            const clickedlink = e.target.closest("[" + this.config.linkAttributeName + "]");

            /** Если действительно клик был на 
             * элементе открытия окна, находим 
             * подходящее окно, заполняем свойства
             *  _nextWindows и _starter и вызываем
             *  метод open (см. ниже)
             */
            if (clickedlink) { 
                e.preventDefault();
                this.starter = clickedlink;
                let targetSelector = this.starter.getAttribute(this.config.linkAttributeName);
                this._nextWindows = document.querySelector(targetSelector);
                this.open();
                return;
            }

            /** Если событие вызвано на элементе
             *  с data-атрибутом data-hystclose,
             *  значит вызовем метод закрытия окна
             */
            if (e.target.closest('[data-hystclose]')) {
                this.close();
                return;
            }
        }.bind(this));
        /** По стандарту, в обработчике события в this
         * помещается селектор на котором события обрабатываются.
         * Поэтому нам нужно вручную установить this на наш 
         * экземпляр класса, который мы пишем с помощью .bind().
         */ 

        //обработаем клавишу escape и tab
        window.addEventListener("keydown", function (e) {   
            //закрытие окна по escape
            if (e.which == 27 && this.isOpened) {
                e.preventDefault();
                this.close();
                return;
            }

            /** Вызовем метод для управления фокусом по Tab
             * и всю ответственность переложим на него
             * (создадим его позже)
             */ 
            if (e.which == 9 && this.isOpened) {
                this.focusCatcher(e);
                return;
            }
        }.bind(this));

    }

    open(selector){
        this.openedWindow = this._nextWindows;
        this._modalBlock = this.openedWindow.querySelector('.hystmodal__window');

        /** Вызываем метод управления скроллом
         * он будет блокировать/разблокировать
         * страницу в зависимости от свойства this.isOpened
         */
        this._bodyScrollControl();
        HystModal._shadow.classList.add("hystmodal__shadow--show");
        this.openedWindow.classList.add("hystmodal--active");
        this.openedWindow.setAttribute('aria-hidden', 'false');

        this.focusContol(); //вызываем метод перевода фокуса (см. ниже)
        this.isOpened = true;
    }

    close(){
        /**
         * Метод закрытия текущего окна. Код упрощён
         * подробнее в статье далее.
         */
        if (!this.isOpened) {
            return;
        }
        this.openedWindow.classList.remove("hystmodal--active");
        HystModal._shadow.classList.remove("hystmodal__shadow--show");
        this.openedWindow.setAttribute('aria-hidden', 'true');

        //возвращаем фокус на элемент которым открылось окно
        this.focusContol();

        //возвращаем скролл
        this._bodyScrollControl();
        this.isOpened = false;
    }

    _bodyScrollControl(){

        let html = document.documentElement;
        if (this.isOpened === true) {
            //разблокировка страницы
            html.classList.remove("hystmodal__opened");
            html.style.marginRight = "";
            window.scrollTo(0, this._scrollPosition);
            html.style.top = "";
            return;
        }

        //блокировка страницы
        this._scrollPosition = window.pageYOffset;
        html.style.top = -this._scrollPosition + "px";
        html.classList.add("hystmodal__opened");
    }

}

Итак, мы описали класс HystModal. Чтобы всё работало, нужно всего лишь подключить наш скрипт и создать экземпляр класса:

const myModal = new HystModal({
    linkAttributeName: 'data-hystmodal', 
});

Тогда по клику по ссылке/кнопке с атрибутом data-hystmodal, например такой: <a href="#" data-hystmodal="#myModal">Открыть окно</a> будет
открываться окно. Однако у нас появляются новые нюансы:

Проблема №6: если в браузере есть фиксированный скроллбар (который влияет на ширину страницы), то при открытии/закрытии окна происходит сдвиг контента, когда полоса прокрутки то появляется то пропадает.

Действительно – скроллбар пропадает и контент страницы перераспределяется. Чтобы решить эту проблему, можно добавить отступ справа к тегу html, равный ширине скроллбара когда он пропадает.

Однако ширина скроллбара может быть разной в разных браузерах и операционных системах. Кроме того, скроллбара может не быть на коротких страницах или он может быть плавающим (например, в Chrome на Android). Поэтому ширину нужно вычислять динамически из скрипта.

Дополним метод _bodyScrollControl()

//при открытии окна
let marginSize = window.innerWidth - html.clientWidth;
//ширина скроллбара равна разнице ширины окна и ширины документа (селектора html)
if (marginSize) {
    html.style.marginRight = marginSize + "px";
} 
//при закрытии окна
html.style.marginRight = "";

Почему код метода close() упрощён? Дело в том, что просто убирая классы CSS у элементов, мы не можем анимировать закрытие окна.

Проблема №7. При закрытии окна, свойство visibility:hidden применяется сразу и не даёт возможности анимировать закрытие окна.

Причина этого известна: свойство visibility:hidden не анимируется. Конечно, можно обойтись без анимации, но, если она нужна, сделаем следующее.

  • Создадим дополнительный CSS-класс .hystmodal—moved почти такой-же как и .hystmodal--active

.hystmodal--moved{
    visibility: visible;
}

  • Затем при закрытии сначала добавим этот класс к окну и повесим обработчик события «transitionend» на модальном окне. Затем удалим класс `.hystmodal—active, таким образом вызывая css-переход. Как только переход завершится, сработает обработчик события «transitionend», в котором сделаем всё остальное и удалим сам обработчик события.

Ниже: новая версия методов закрытия окна:

close(){
    if (!this.isOpened) {
        return;
    }
    this.openedWindow.classList.add("hystmodal--moved");
    this.openedWindow.addEventListener("transitionend", this._closeAfterTransition);
    this.openedWindow.classList.remove("hystmodal--active");
}

_closeAfterTransition(){
    this.openedWindow.classList.remove("hystmodal--moved");
    this.openedWindow.removeEventListener("transitionend", this._closeAfterTransition);
    HystModal._shadow.classList.remove("hystmodal__shadow--show");
    this.openedWindow.setAttribute('aria-hidden', 'true');
    this.focusContol();
    this._bodyScrollControl();
    this.isOpened = false;
}

Вы заметили, что мы создали ещё один метод _closeAfterTransition() и перенесли основную логику закрытия туда. Это нужно, чтобы удалить обработчик события transitionend после закрытия окна, ведь в метод removeEventListener необходимо передать именно ту функцию, которую мы привязывали.

Кроме того, если анимация не будет нужна, можно просто вызвать this._closeAfterTransition() не вешая его на событие.

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

//внутри конструктора
this._closeAfterTransition = this._closeAfterTransition.bind(this)

2.2 Закрытие окна по клику на оверлей

Нам нужно обработать ещё одно событие – закрытие окна по клику на элемент подложки .hystmodal__wrap. Мы можем повесить обработчик клика на документ для делегирования события как при открытии и проверить что событие произошло на .hystmodal__wrap примерно так:

document.addEventListener("click", function (e) {
    const wrap = e.target.classList.contains('hystmodal__wrap');
    if(!wrap) return;
    e.preventDefault();
    this.close();
}.bind(this));

Это будет работать, но есть один малозаметный недостаток.

Проблема №8. Если кнопку мыши нажать внутри окна, а отпустить за его пределами (над подложкой), окно закрывается.

Представьте, что в окне форма. Вы выделяете текст в поле ввода и случайно двигаете мышь чуть дальше, курсор заходит на подложку и вдруг окно закрывается, всё вдруг пропадает из виду. Не хотелось бы, чтобы так было.

Окно закрывается потому что по спецификации, если нажатие и отпускание мыши были на разных элементах, то событие click сработает на самом ближайшем общем для них элементе, а у нас это как раз .hystmodal__wrap.

Мы могли бы решить это изменением html, добавляя ещё один div сразу после .hystmodal__window и размещая его визуально под окном. Но нам бы не хотелось добавлять лишний пустой div ещё сильнее усложняя разметку.

Мы можем разбить наш addEventListener на два отдельных обработчика: для событий mousedown и mouseup и будем проверять чтобы оба события происходили именно на .hystmodal__wrap. Добавим новые обработчики событий в наш метод eventsFeeler()

document.addEventListener('mousedown', function (e) {
    /**
    * Проверяем было ли нажатие над .hystmodal__wrap,
    * и отмечаем это в свойстве this._overlayChecker
    */
    if (!e.target.classList.contains('hystmodal__wrap')) return;
    this._overlayChecker = true;
}.bind(this));

document.addEventListener('mouseup', function (e) {
    /**
    * Проверяем было ли отпускание мыши над .hystmodal__wrap,
    * и если нажатие тоже было на нём, то закрываем окно
    * и обнуляем this._overlayChecker в любом случае
    */
    if (this._overlayChecker && e.target.classList.contains('hystmodal__wrap')) {
        e.preventDefault();
        !this._overlayChecker;
        this.close();
        return;
    }
    this._overlayChecker = false;
}.bind(this));

2.3 Управление фокусом

У нас заготовлено два метода для управления фокусом: focusContol() для переноса фокуса внутрь окна и обратно при его закрытии, а также focusCatcher(event) для блокирования ухода фокуса из окна.

Решения для фокуса были реализованы аналогично js-библиотеке «Micromodal» (Indrashish Ghosh). А именно:

1. В служебный массив сохраним все css селекторы на которых может быть установлен фокус (свойство помещаем в init()):

//внутри метода init или конструктора
this._focusElements = [
    'a[href]',
    'area[href]',
    'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',
    'select:not([disabled]):not([aria-hidden])',
    'textarea:not([disabled]):not([aria-hidden])',
    'button:not([disabled]):not([aria-hidden])',
    'iframe',
    'object',
    'embed',
    '[contenteditable]',
    '[tabindex]:not([tabindex^="-"])'
];

2. В методе focusContol() находим первый такой селектор в окне и устанавливаем на него фокус, если окно открывается. Если же окно закрывается – то переводим фокус на this.starter:

focusContol(){
    /** Метод переносит фокус с элемента открывающего окно
     * в само окно, и обратно, когда окно закрывается
     * см. далее в тексте.
     */
    const nodes = this.openedWindow.querySelectorAll(this._focusElements);
    if (this.isOpened && this.starter) {
        this.starter.focus();
    } else {
        if (nodes.length) nodes[0].focus();
    }
}

3. В методе focusCatcher() находим в окне и превращаем в массив коллекцию всех элементов на которых может быть фокус. И проверяем, если фокус должен был выйти на пределы окна, то вместо этого устанавливаем фокус снова на первый или последний элемент (ведь фокус можно переключать как по Tab так и по Shift+Tab в обратную сторону).

Результирующий код метода focusCatcher:

focusCatcher(e){
    /** Метод не позволяет фокусу перейти вне окна при нажатии TAB
     * элементы в окне фокусируются по кругу.
     */

    // Находим все элементы на которые можно сфокусироваться
    const nodes = this.openedWindow.querySelectorAll(this._focusElements);

    //преобразуем в массив
    const nodesArray = Array.prototype.slice.call(nodes);

    //если фокуса нет в окне, то вставляем фокус на первый элемент
    if (!this.openedWindow.contains(document.activeElement)) {
        nodesArray[0].focus();
        e.preventDefault();
    } else {
        const focusedItemIndex = nodesArray.indexOf(document.activeElement)
        if (e.shiftKey && focusedItemIndex === 0) {
            //перенос фокуса на последний элемент
            nodesArray[nodesArray.length - 1].focus();
            e.preventDefault();
        }
        if (!e.shiftKey && focusedItemIndex === nodesArray.length - 1) {
            //перерос фокуса на первый элемент
            nodesArray[0].focus();
            e.preventDefault();
        }
    }
}

По сути мы реализовали все необходимое для успешного создания модальных окон, осталось ещё несколько дел:

Проблема №9. В IE11 не работают методы Element.closest() и Object.assign().

Для поддержки Element.closest, воспользуемся полифилами для closest и matches от MDN.

Можно их вставить просто так, но так как у нас проект всё равно собирается webpack, то удобно воспользоваться пакетом element-closest-polyfill который просто вставляет этот код.

Для поддержки Object.assign, можно воспользоваться уже babel-плагином @babel/plugin-transform-object-assign

3. Заключение и ссылки

Повторяя начало статьи, всё изложенное выше, я оформил в маленькую библиотеку hystModal под MIT-лицензией. Вышло 3 кБ кода при загрузке с gzip. Ещё написал для неё подробную документацию на русском и английском языке.

Что вошло ещё в библиотеку hystModal, чего не было в статье:

  • Настройки (вкл/выкл управление фокусом, варианты закрытия, ожидание анимации закрытия)
  • Коллбеки (функции вызывающиеся перед открытием окна и после его закрытия (в них передаётся объект модального окна))
  • Добавлен запрет на какие-либо действия пока анимация закрытия окна не завершится, а также ожидание анимации закрытия текущего окна перед открытием нового (если окно открывается из другого окна).
  • Оформление кнопки-крестика закрытия в CSS
  • Минификация CSS и JS плагинами Webpack.

Если вам будет интересна эта библиотека, буду рад звёздочке в GitHub, или напишите в Issues о найденных багах. (Особенно большие проблемы, наверное, в грамматике английской версии документации, так как мои знания языка пока на начальном уровне. Связаться со мной также можно в Instagram

Модальное окно — это всплывающее окно, которое появляется поверх основного окна. Обычно оно используется для отображения дополнительной информации или для того, чтобы попросить пользователя подтвердить действие.

Модальные окна часто используются в веб-приложениях для отображения важных сообщений или диалогов подтверждения. Они могут быть очень полезны для того, чтобы убедиться, что пользователь видит содержимое и взаимодействует с ним, прежде чем двигаться дальше.

Однако модальные окна могут быть навязчивыми и мешающими, поэтому важно использовать их экономно и только в случае крайней необходимости. При правильном использовании они могут стать мощным инструментом для улучшения пользовательского опыта.

Полезные ссылки

Пример работы модального окна: https://toni.codelab.pro/modal/index.html

* Модальное окно вызывается при клике на кнопку Click me.

Стартовый проект: https://github.com/toni-wheel/youtube-base-page

Создаем модальное окно

В качестве примера сделал простое модальное окно, которое открывается при клике на кнопку и закрывается также.

В нашем примере модальное окно:

  • Имеет заголовок, текстовое содержимое и две кнопки.
  • Адаптируется под мобильные устройства.
  • Блокирует скролл веб-страницы.
  • При переполнении контентом — появляется скролл в самом модальном окне.

Разметка

В начале сделаем разметку для модального окна и кнопки.

Модальное окно можно разместить в самом начале веб-страницы.

<div class="modal" id="modal">
  <div class="modal__box">
    <div class="modal__title">Some title</div>
    <div class="modal__info">
      Lorem ipsum dolor sit amet consectetur adipisicing elit. Mollitia sunt
      possimus voluptas dignissimos nostrum vero distinctio qui deserunt unde
      rerum.
    </div>
    <div class="modal__bottom">
      <a href="#" class="btn">Go to</a>
      <a href="#" class="btn btn--close" id="btn_close">Close</a>
    </div>
  </div>
</div>

Кнопку для вызова окна разместите в нужном вам месте кода.

<a href="#" class="btn" id="btn">Click me</a>

Стили

Стили для модального окна.

.modal {
  display: none; /* По умолчанию окно закрыто */
  z-index: 10; /* Чтобы сделать окно поверх всех элементов */
  background-color: rgba(0, 0, 0, 0.5); /* Полупрозрачный темный фон */
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  align-items: flex-start;
  justify-content: center;
  overflow-y: scroll; /* При переполнении окна делаем скролл */
  padding: 20px;
}

.modal--open {
  display: flex; /* Модификатор для открытого окна */
}

.modal__box {
  width: 100%;
  max-width: 600px;
  padding: 20px;
  background-color: #fff;
  border-radius: 10px;
  margin-top: auto;
  margin-bottom: auto;
}

.modal__title {
  color: #111;
  font-size: 32px;
  text-align: center;
  margin-bottom: 20px;
}

.modal__info {
  color: #111;
  font-size: 16px;
  margin-bottom: 30px;
}

.modal__bottom {
  display: flex;
  justify-content: center;
  align-items: center;
}

Стили для кнопок.

.btn {
  box-sizing: border-box;
  display: block;
  width: max-content;
  background-color: #0066cc;
  padding: 10px 20px;
  font-size: 1.2rem;
  color: #fff;
  border-radius: 10px;
}

.modal__bottom > .btn {
  margin-right: 20px;
}

.modal__bottom > .btn:last-child {
  margin-right: 0;
}

.modal__bottom > .btn--close {
  background-color: #dedede;
  color: #0066cc;
}

Чтобы заблокировать скролл веб-страницы добавим дополнительный класс lock.

.lock {
  overflow-y: hidden;
}

Скрипт

Скрипт довольно простой.

// считываем все элементы
const body = document.body;
const btn = document.querySelector("#btn");
const btnClose = document.querySelector("#btn_close");
const modal = document.querySelector("#modal");

// обработчики клика на кнопки
btn.addEventListener("click", btnHandler);
btnClose.addEventListener("click", btnCloseHandler);

// при нажатии на кнопку Click me
function btnHandler(e) {
  e.preventDefault();
  body.classList.add("lock"); // блокируем скролл веб-страницы
  modal.classList.add("modal--open"); // открываем модальное окно
}

// при нажатии на кнопку Close
function btnCloseHandler(e) {
  e.preventDefault();
  body.classList.remove("lock"); // разблокируем скролл страницы
  modal.classList.remove("modal--open"); // закрываем окно
}

// при нажатии на пустое пространство - закрываем окно
window.addEventListener("click", function (e) {
  if (e.target == modal) {
    body.classList.remove("lock");
    modal.classList.remove("modal--open");
  }
});

Понравилась статья? Поделить с друзьями:
  • Как найти предел если в степени х
  • Как найти сайт министерства финансов
  • Как найти промокод для wildberries
  • Поилка для грызунов капает постоянно как исправить
  • Она нашла другого как понять