Поведение всплывающих окон в MapLibre GL JS определяется комбинацией встроенных опций, DOM-событий и пользовательской логики управления состоянием. Закрытие попапов становится критичным элементом интерфейса при работе с интерактивными картами, поскольку напрямую влияет на читаемость слоя данных, предсказуемость взаимодействий и отсутствие визуального «засорения» карты.
Встроенный объект Popup предоставляет стандартные
способы завершения его отображения. Основной метод управления жизненным
циклом — remove(), который удаляет DOM-элемент и отвязывает
его от карты.
const popup = new maplibregl.Popup()
.setLngLat([30.5, 50.5])
.setHTML('<b>Точка интереса</b>')
.addTo(map);
// закрытие
popup.remove();
Этот вызов является финальной точкой жизненного цикла попапа: после него объект перестаёт участвовать в рендере и больше не реагирует на события карты.
Наиболее распространённый сценарий — закрытие при клике вне попапа. Поведение управляется опцией:
const popup = new maplibregl.Popup({
closeOnClick: true
});
При closeOnClick: true библиотека автоматически
подписывается на событие клика по карте и вызывает remove()
у активных попапов.
При closeOnClick: false управление полностью передаётся
разработчику, что требуется в интерфейсах с высокой плотностью
интерактивных элементов.
Опция closeOnMove связывает жизненный цикл попапа с
навигацией по карте:
const popup = new maplibregl.Popup({
closeOnMove: true
});
Любое событие pan/zoom приводит к удалению попапа. Такой подход используется при отображении краткосрочной информации, которая теряет актуальность при смене viewport.
Внутри реализуется подписка на события move,
zoom и rotate, после чего вызывается
remove().
В сложных интерфейсах часто требуется контроль над тем, какие попапы остаются открытыми. Распространённая архитектура — хранение ссылок на активные экземпляры:
const popups = [];
map.on('click', (e) => {
const popup = new maplibregl.Popup()
.setLngLat(e.lngLat)
.setHTML('Данные объекта')
.addTo(map);
popups.push(popup);
});
Для закрытия всех попапов используется итерация:
popups.forEach(p => p.remove());
popups.length = 0;
Такой подход особенно важен при динамическом создании окон на основе слоёв или кластеров.
Часто применяется стратегия единственного активного попапа. Она устраняет необходимость управлять массивом экземпляров:
let activePopup = null;
map.on('click', (e) => {
if (activePopup) {
activePopup.remove();
}
activePopup = new maplibregl.Popup()
.setLngLat(e.lngLat)
.setHTML('Единственный popup')
.addTo(map);
});
В такой модели закрытие нового попапа автоматически приводит к уничтожению предыдущего, что снижает когнитивную нагрузку интерфейса и упрощает контроль состояния.
При добавлении кастомных обработчиков внутри popup DOM может возникнуть ситуация, когда клик внутри окна приводит к его закрытию. Это связано с распространением события на карту.
Корректное решение — остановка всплытия:
const popupContent = document.createElement('div');
popupContent.innerHTML = '<button id="btn">OK</button>';
popupContent.addEventListener('click', (e) => {
e.stopPropagation();
});
Без этого клики по элементам интерфейса могут интерпретироваться как
клик по карте, активируя closeOnClick.
Хотя стандартный popup уже содержит крестик, иногда требуется собственная логика закрытия:
const popup = new maplibregl.Popup({ closeButton: false })
.setLngLat([30, 50])
.setHTML(`
<div class="card">
<button id="close">Закрыть</button>
<div>Контент</div>
</div>
`)
.addTo(map);
document.getElementById('close').addEventListener('click', () => {
popup.remove();
});
При таком подходе управление жизненным циклом полностью переносится в DOM-логику содержимого.
Нативное поведение ESC может быть реализовано вручную, так как библиотека не всегда навязывает глобальные обработчики:
const popup = new maplibregl.Popup().setHTML('ESC закрывает');
function onKeyDown(e) {
if (e.key === 'Escape') {
popup.remove();
window.removeEventListener('keydown', onKeyDown);
}
}
window.addEventListener('keydown', onKeyDown);
Важно учитывать очистку обработчиков, иначе они остаются активными после удаления попапа, создавая утечки логики.
При использовании Marker попап часто привязан к нему
напрямую:
const marker = new maplibregl.Marker()
.setLngLat([30, 50])
.setPopup(new maplibregl.Popup().setHTML('Маркер'))
.addTo(map);
При повторном клике логика закрытия зависит от состояния: библиотека автоматически переключает открытое состояние, но при кастомной реализации требуется явное управление:
marker.getPopup().remove();
В динамических приложениях карта часто обновляет source/layer, что делает попапы неактуальными. В таких случаях используется централизованное удаление:
map.on('data', () => {
if (activePopup) {
activePopup.remove();
activePopup = null;
}
});
Это предотвращает ситуацию, когда попап ссылается на уже удалённые геометрии.
В React/Vue/Angular приложениях карта может уничтожаться при смене маршрута. Попапы должны быть очищены вместе с картой:
function destroyMap() {
if (activePopup) {
activePopup.remove();
activePopup = null;
}
map.remove();
}
Игнорирование этого шага приводит к сохранению DOM-узлов вне контейнера и потенциальным утечкам памяти.
При одновременном использовании нескольких механизмов закрытия возникает порядок приоритетов:
popup.remove()closeOnClickcloseOnMoveЕсли вызывается remove(), остальные механизмы фактически
теряют значение, так как объект уже удалён из DOM-дерева карты.
При загрузке данных асинхронно возможна ситуация, когда попап закрывается и повторно открывается до завершения запроса:
let requestId = 0;
map.on('click', async (e) => {
const id = ++requestId;
const data = await fetchData(e.lngLat);
if (id !== requestId) return;
new maplibregl.Popup()
.setLngLat(e.lngLat)
.setHTML(data)
.addTo(map);
});
Такой контроль предотвращает появление «устаревших» попапов после смены состояния.
При анимации камеры закрытие попапов может происходить до завершения
движения карты. Это особенно заметно при flyTo.
Используется привязка к событиям:
map.on('movestart', () => {
if (activePopup) activePopup.remove();
});
Таким образом попап синхронизируется с визуальным состоянием карты и не «плавает» поверх переходов.
Каждый попап создаёт DOM-узлы и event listeners. При долгоживущих картах важно явно разрывать связи:
popup.remove();
popup = null;
При отсутствии очистки в SPA-навигации постепенно накапливаются «осиротевшие» элементы, которые уже не отображаются, но остаются в памяти.
Закрытие попапов в MapLibre GL JS строится на сочетании трёх уровней:
closeOnClick,
closeOnMove)remove())Стабильная архитектура всегда опирается на явное управление экземплярами, минимизируя зависимость от автоматических механизмов и обеспечивая предсказуемость состояния интерфейса карты.