null

Pop-up блок, исчезающий по клику за его пределами

Чтобы сделать интерфейс менее нагруженным, второстепенную информацию можно спрятать во всплывающий блок. Так поступают, например, интернет-магазины, показывая список магазинов, в которых есть интересующий вас товар, только после того, как вы кликнули по надписи “В наличии”. В этой статье будет показано, как сделать блок, появляющийся по клику на другом элементе и исчезающий по клику на том же элементе или в любом месте за пределами блока. Для эффектов появления блока используется библиотека JQuery.

Информация о наличии товара в интернет-магазине Юлмарт

Заготовка

Начнём с простой заготовки. В ней есть надпись “Show block” и блок, который будет показываться по нажатию на надпись.

Код заготовки приведён ниже, посмотреть на него в действии можно на JSFiddle. Сразу подключаем библиотеку JQuery.

<!DOCTYPE>
<html>
  <head>
    <script src="jquery-3.2.1.min.js" type="text/javascript"></script>
    <style>
      .pop-up-label {
        display: inline-block;
        position: relative; /*относительное позиционирование*/
        color: #147aaa;
        text-decoration: none;
        border-bottom: 1px dashed #147aaa;
        cursor: pointer;
      }
      .pop-up-block {
        width: 300px;
        background: #4bae73;
        border-radius: 10px;
        padding: 15px;
        font-size: 14px;
        color: black; /*сбрасываем цвет, унаследованный от надписи*/
        cursor: text;
        position: absolute; /*абсолютное позиционирование*/
        top: 30px; /*смещение блока по вертикали*/
        left: 0px; /*смещение блока по горизонтали*/
      }
    </style>
  </head>
 
  <body>
    <div class="pop-up-label">
      Show block
      <div class="pop-up-block">
        Lorem ipsum dolor sit amet, dicant constituto sed et, 
        molestie offendit quaerendum eu mea, pri ut ferri zril.
        Delenit legendos est ut, no mea esse dicunt reprehendunt. 
        Ut omnis habemus omittantur vix, per eu iriure
        nonumes facilis, cu propriae apeirian sea.
      </div>
    </div>
    <script>/*место для js-кода*/</script>
  </body>
</html>

Почему разметка выглядит именно так? Почему не использовать для надписи теги <a> или <p>? Проблема заключается во вложенном <div class="pop-up-block">. Но обо всём по порядку.
Допустим, мы хотим показывать всплывающий блок не снизу, а сверху или сбоку от надписи. Чтобы задавать ему положение относительно надписи, блоку ставится position: absolute, а надписи в свою очередь position: relative. Абсолютно позиционированный блок, родитель которого имеет позиционирование fixed, relative или absolute, рассчитывает своё положение относительно родителя. Именно поэтому блок и вложен в надпись, чтобы она стала его родительским элементом (в противном случае блок позиционируется относительно окна браузера). Это накладывает некоторые ограничения на набор используемых для надписи тегов и значение свойства display самой надписи.

Так, если использовать тег <a> (с заданным атрибутом href), то выделить текст в блоке станет непросто, вместо этого он начнёт “перетаскиваться” вместе с текстом надписи. Вложить блок в тег <p> не получится в силу ограничений самого элемента абзаца, который согласно спецификации не допускает внутри себя <div> (см. список допустимых тегов). Браузер обработает эту ситуацию сгеренировав последовательность <p>Show block</p><div></div><p></p> вместо ожидаемой <p>Show block<div></div></p>. При использовании display: block (значение по умолчанию для тега div) всплывающий блок исчезнет, поскольку находится за границами родительского элемента, поэтому мы назначаем ему display: inline-block.

Немного поменяв параметры смещения блока, можно поместить его справа от надписи.

.pop-up-block {
  ...
  top: -50px;
  left: calc(100% + 20px);
}

Эффекты

Для показа и скрытия блока воспользуемся функцией библиотеки JQuery slideToggle(), которая плавно изменяет размер элемента от 0 до 100% (и наоборот при повторном вызове).
Для начала делаем блок невидимым по умолчанию.

.pop-up-block {
  ...
  display: none;
}

Затем назначаем надписи обработчик события click, в котором вызываем упомянутую функцию (пример).

<script>
  $(".pop-up-label").on("click", function() {
    $(this).children('.pop-up-block').slideToggle();
  });
</script>

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

Сейчас блок показывается и скрывается только по клику по надписи. Сделаем так, чтобы он исчезал по клику в любом месте страницы за пределами самого блока.

$(".pop-up-label").on("click", function(source) {
  var label = $(this);
  var block = $(this).children('.pop-up-block');
 
  if (!block.is(source.target)) {
    block.slideToggle(function() {
      if (block.is(":visible")) {
        $(document).bind('mouseup.hidePopUp', function(e) {
          if (!block.is(e.target) /*если клик не по блоку*/
              && block.has(e.target).length === 0) { /*и не по его потомкам*/
                $(document).unbind('mouseup.hidePopUp');
                if (!label.is(e.target)) block.slideToggle();
          }
        });
      }
    });
   }
});

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

$(".pop-up-label").on("click", function(source) {...});

Чтобы избежать такого поведения, добавлена проверка того, что клик не пришёлся на блок.

if (!block.is(sourse.target))

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

block.slideToggle(function() {

И если после выполнения функции блок стал видимым...

if (block.is(":visible")) {

...начинаем слушать события мыши.

$(document).bind('mouseup.hidePopUp', function(e) {
  if (!block.is(e.target) /*если клик не по блоку*/
      && block.has(e.target).length === 0) { /*и не по его потомкам*/
        $(document).unbind('mouseup.hidePopUp');
        if (!label.is(e.target)) block.slideToggle();
  }
});

Если произошло событие клика мышью не по самому блоку или любому вложенному в него элементу, скрываем блок и перестаём слушать события мыши. Исключение составляет случай, когда клик вне блока пришёлся на надпись, тогда нам не нужно дополнительно скрывать блок - это сделает обработчик события click по надписи. Проверяем на живом примере.

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

Оформление

В довершение внешнего вида добавим блоку небольшой треугольник. Самый простой способ ㅡ использование псевдокласса :before. Треугольник будем рисовать границами, сделав цветной нижнюю границу и прозрачными правую и левую. При нулевых ширине и высоте контента все четыре границы сходятся в центр элемента, образуя треугольники.

.pop-up-block:before {
  display: block;
  content: "";
  border-left: 10px solid transparent;
  border-right: 10px solid transparent;
  border-bottom: 10px solid #0015ff;
  position: absolute;
  top: -10px;
  left: 40px;
}

Главный недостаток такого способа, как и всех прочих, в которых “треугольник” выходит за границы анимированного блока, заключается в исчезновении треугольника на время анимации (пример). Чтобы это исправить, добавим внутрь нашего блока ещё один элемент <div>, отступающий от верхней границы родителя таким образом, чтобы хватило места для треугольника.

<div class="pop-up-label">
  Show block
  <div class="pop-up-block">
    <div class="pop-up-content">
      Lorem ipsum dolor sit amet, dicant constituto sed et, 
      molestie offendit quaerendum eu mea, pri ut ferri zril. 
      Delenit legendos est ut, no mea esse dicunt reprehendunt. 
      Ut omnis habemus omittantur vix, per eu iriure nonumes 
      facilis, cu propriae apeiria sea.
    </div>
  </div>
</div>

Перенесём часть стилей верхнего блока (pop-up-block) на внутренний блок (pop-up-content), например, скругление рамок и отступы от рамки верхнему блоку больше не нужны. Внутреннему блоку добавим отступ от родителя с помощью margin. Сделать тот же отступ через свойство padding родителя не получится: внутренний блок будет смещаться вверх-вниз во время анимации относительно зафиксированного треугольника.

.pop-up-block {
  width: 300px;
  background: #7ad59f;
  font-size: 14px;
  color: black;
  cursor: text;
  position: absolute;
  top: 20px;
  left: 0px;
}
.pop-up-content {
  background: #c6efec;
  border-radius: 10px;
  padding: 15px;
  margin-top: 10px;
}
.pop-up-block:before {
  display: block;
  content: "";
  border-left: 10px solid transparent;
  border-right: 10px solid transparent;
  border-bottom: 10px solid #0015ff;
  position: absolute;
  top: 0px;
  left: 40px;
}

Из-за появления вложенных элементов внутри блока, придётся добавить ещё одно условие в проверку срабатывания анимации.

$(".pop-up-label").on("click", function(source) {
  var label = $(this);
  var block = $(this).children('.pop-up-block');
 
  if (!block.is(source.target) && block.has(source.target).length === 0) {
    ...
  }
});

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

Осталось убрать фон внешнего блока и сделать треугольник в тон внутреннему блоку. Окончательный вариант выглядит так (пример).

Полный код примера приведён ниже.

<!DOCTYPE>
<html>
  <head>
    <script src="jquery-3.2.1.min.js" type="text/javascript"></script>
    <style>
      .pop-up-label {
        display: inline-block;
        position: relative;
        color: #147aaa;
        text-decoration: none;
        border-bottom: 1px dashed #147aaa;
        cursor: pointer;
      }
      .pop-up-block {
        display: none;
        width: 300px;
        font-size: 14px;
        color: black;
        cursor: text;
        position: absolute;
        top: 20px;
        left: 0px;
      }
      .pop-up-content {
          background: #c6efec;
          border-radius: 10px;
          padding: 15px;
          margin-top: 10px;
        }
        .pop-up-block:before {
          display: block;
          content: "";
          border-left: 10px solid transparent;
          border-right: 10px solid transparent;
          border-bottom: 10px solid #c6efec;
          position: absolute;
          top: 0px;
          left: 40px;
        }
    </style>
  </head>
 
  <body>
    <div class="pop-up-label">
      Show block
      <div class="pop-up-block">
        <div class="pop-up-content">
          Lorem ipsum dolor sit amet, dicant constituto sed et, 
          molestie offendit quaerendum eu mea, pri ut ferri zril. 
          Delenit legendos est ut, no mea esse dicunt reprehendunt. 
          Ut omnis habemus omittantur vix, per eu iriure nonumes 
          facilis, cu propriae apeirian sea.
        </div>
      </div>
    </div>
 
    <script type="text/javascript">
      $(".pop-up-label").on("click", function(source) {
        var label = $(this);
        var block = $(this).children('.pop-up-block');
 
        if (!block.is(source.target) &&
            block.has(source.target).length === 0) {
          block.slideToggle(function() {
            if (block.is(":visible")) {
              $(document).bind('mouseup.hidePopUp', function(e) {
                if (!block.is(e.target) && block.has(e.target).length === 0) {
                  $(document).unbind('mouseup.hidePopUp');
                  if (!label.is(e.target)) block.slideToggle();
                }
              });
            }
          });
        }
      });
    </script>
  </body>
</html>

Направление

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

.pop-up-block {
  ...
  top: -60px; /*сдвигаем блок вверх и вправо от надписи*/
  left: 110px;
}
.pop-up-content {
  margin-left: 10px; /*меняем отступ сверху на отступ слева*/
}
.pop-up-block:before {
  border-top: 10px solid transparent; /*перерисовываем треугольник*/
  border-bottom: 10px solid transparent; /*чтобы он смотрел*/
  border-right: 10px solid #c6efec; /*углом налево*/
  top: 60px; /*сдвигаем треугольник по вертикали*/
  left: 0px; /*сдвиг по горизонтали не нужен*/
}

Можно изменить эффект появления блока, например, на слева-направо (пример). Дополнительные эффекты станут доступны после подключения jQuery UI.

block.toggle('slide');