Настройка содержимого попапов

Попапы в MapLibre GL JS строятся вокруг класса maplibregl.Popup, который представляет всплывающее окно, привязанное к координатам карты или к конкретному объекту слоя. Основной принцип работы — создание экземпляра попапа, его конфигурация и последующее связывание с координатами или геометрией объекта.

Типовой цикл выглядит так:

const popup = new maplibregl.Popup({
  closeButton: true,
  closeOnClick: true,
  maxWidth: "300px"
})
  .setLngLat([37.6173, 55.7558])
  .setHTML("<h3>Москва</h3><p>Столица</p>")
  .addTo(map);

Ключевые элементы:

  • setLngLat — фиксирует географическую позицию
  • setHTML — задаёт содержимое в виде строки HTML
  • addTo(map) — добавляет попап на карту

Формирование содержимого через setHTML

Метод setHTML применяется для быстрого рендеринга содержимого попапа в виде строки. Это удобно при работе с простыми шаблонами, где данные уже подготовлены.

popup.setHTML(`
  <div class="popup">
    <strong>${feature.properties.name}</strong>
    <div>${feature.properties.description}</div>
  </div>
`);

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

Частые источники риска:

  • свойства feature.properties
  • внешние API
  • пользовательский ввод

Использование setDOMContent для сложной структуры

Когда содержимое попапа выходит за рамки простого текста, предпочтительнее использовать setDOMContent, позволяющий передать готовый DOM-элемент.

const container = document.createElement("div");
container.className = "popup-container";

const title = document.createElement("h3");
title.textContent = feature.properties.name;

const description = document.createElement("p");
description.textContent = feature.properties.description;

container.appendChild(title);
container.appendChild(description);

popup.setDOMContent(container);

Преимущества:

  • исключение риска XSS при использовании textContent
  • возможность динамического управления узлами DOM
  • удобство интеграции с компонентной архитектурой

Привязка попапов к слоям и объектам карты

Наиболее распространённый сценарий — отображение попапа при клике по объекту слоя.

map.on("click", "cities-layer", (e) => {
  const feature = e.features[0];

  new maplibregl.Popup()
    .setLngLat(feature.geometry.coordinates)
    .setHTML(`
      <strong>${feature.properties.name}</strong>
    `)
    .addTo(map);
});

Важно учитывать:

  • слой должен быть кликабельным (interactive: true)
  • необходимо использовать e.features
  • координаты берутся из геометрии объекта

Работа с множественными объектами в точке клика

При использовании кластеризации или плотных слоёв в одной точке может находиться несколько объектов. В этом случае необходимо явно выбирать нужный feature.

map.on("click", "points-layer", (e) => {
  const features = map.queryRenderedFeatures(e.point, {
    layers: ["points-layer"]
  });

  if (!features.length) return;

  const feature = features[0];

  new maplibregl.Popup()
    .setLngLat(feature.geometry.coordinates)
    .setHTML(`<div>${feature.properties.title}</div>`)
    .addTo(map);
});

Для более сложных интерфейсов может быть реализован список объектов внутри одного попапа.


Переиспользование экземпляра Popup

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

const popup = new maplibregl.Popup({
  closeButton: true,
  closeOnClick: true
});

map.on("click", "cities-layer", (e) => {
  const feature = e.features[0];

  popup
    .setLngLat(feature.geometry.coordinates)
    .setHTML(`<strong>${feature.properties.name}</strong>`)
    .addTo(map);
});

Такой подход:

  • снижает количество DOM-операций
  • упрощает управление состоянием
  • предотвращает накопление неиспользуемых объектов

Настройка поведения закрытия и позиционирования

MapLibre GL JS предоставляет несколько параметров, влияющих на поведение попапа:

const popup = new maplibregl.Popup({
  closeButton: false,
  closeOnClick: false,
  anchor: "top",
  offset: 10
});

Основные параметры:

  • closeButton — отображение кнопки закрытия
  • closeOnClick — закрытие при клике по карте
  • anchor — позиция относительно точки привязки (top, bottom, left, right и др.)
  • offset — смещение попапа

Параметр anchor особенно важен при размещении попапов над маркерами, чтобы избежать перекрытия элементов.


Динамическое содержимое и асинхронная загрузка данных

Попапы часто используются как контейнер для данных, которые подгружаются по API.

map.on("click", "cities-layer", async (e) => {
  const feature = e.features[0];

  const popup = new maplibregl.Popup()
    .setLngLat(feature.geometry.coordinates)
    .setHTML("Загрузка...")
    .addTo(map);

  const response = await fetch(`/api/city/${feature.properties.id}`);
  const data = await response.json();

  popup.setHTML(`
    <div>
      <h3>${data.name}</h3>
      <p>Население: ${data.population}</p>
    </div>
  `);
});

Ключевая особенность — возможность обновлять содержимое попапа после его создания через повторный вызов setHTML или setDOMContent.


Стилизация содержимого попапов

Попапы используют собственную структуру DOM, в которую можно внедрять стили через CSS.

.maplibregl-popup-content {
  font-family: Arial, sans-serif;
  padding: 12px;
  border-radius: 8px;
}

.popup-container h3 {
  margin: 0 0 6px 0;
  font-size: 16px;
}

.popup-container p {
  margin: 0;
  font-size: 13px;
  color: #555;
}

Также можно управлять внешним контейнером попапа:

.maplibregl-popup {
  max-width: 320px;
}

HTML против DOM: выбор подхода

Использование setHTML подходит для:

  • статических шаблонов
  • простых карточек
  • быстрого прототипирования

Использование setDOMContent предпочтительно для:

  • сложных интерфейсов
  • динамических компонентов
  • безопасной вставки данных

Комбинирование подходов возможно, но обычно приводит к усложнению поддержки кода.


Обработка событий внутри попапа

Так как попап является частью DOM, внутри него можно регистрировать события:

const container = document.createElement("div");
container.innerHTML = `
  <button id="details-btn">Подробнее</button>
`;

container.querySelector("#details-btn").addEventListener("click", () => {
  console.log("Переход к деталям объекта");
});

popup.setDOMContent(container);

Важно учитывать:

  • обработчики нужно навешивать после создания элементов
  • при повторном рендеринге DOM события необходимо пересоздавать

Попапы при наведении курсора

Помимо кликов, попапы часто используются при hover-сценариях.

map.on("mouseenter", "cities-layer", (e) => {
  map.getCanvas().style.cursor = "pointer";

  const feature = e.features[0];

  popup
    .setLngLat(feature.geometry.coordinates)
    .setHTML(feature.properties.name)
    .addTo(map);
});

map.on("mouseleave", "cities-layer", () => {
  map.getCanvas().style.cursor = "";
  popup.remove();
});

Особенности:

  • требуется ручное управление удалением попапа
  • важно контролировать курсор для UX
  • возможны частые перерисовки при движении мыши

Оптимизация производительности при большом количестве попапов

При работе с большим количеством объектов критично избегать создания множества экземпляров Popup.

Подходы оптимизации:

  • использование одного глобального попапа
  • минимизация HTML-рендеринга
  • переход на textContent вместо innerHTML
  • отказ от сложных DOM-структур внутри popups

Дополнительно:

  • не создавать попапы в mousemove без необходимости
  • избегать частого обновления содержимого при движении курсора

Интеграция с кастомными маркерами

Попапы часто привязываются к Marker:

const marker = new maplibregl.Marker()
  .setLngLat([37.6, 55.75])
  .addTo(map);

const popup = new maplibregl.Popup({ offset: 25 })
  .setHTML("<strong>Точка интереса</strong>");

marker.setPopup(popup);

Особенность такого подхода:

  • попап открывается автоматически при клике на маркер
  • управление жизненным циклом упрощается
  • логика связывается с объектом маркера

Динамическое обновление состояния попапа

Попап может служить реактивным контейнером:

function updatePopup(feature) {
  popup.setHTML(`
    <div>
      <strong>${feature.properties.name}</strong>
      <div>Обновлено: ${new Date().toLocaleTimeString()}</div>
    </div>
  `);
}

Такой подход используется при:

  • потоковых данных
  • обновлении состояния объекта в реальном времени
  • интерактивных дашбордах