Пользовательские всплывающие окна

Всплывающие окна в Google Maps JavaScript API реализуются через несколько уровней абстракции: стандартный InfoWindow, кастомизированные HTML-вставки внутри него, а также полностью пользовательские оверлеи на основе OverlayView. Каждый подход решает разные задачи — от простого отображения текста до сложных интерактивных интерфейсов, встроенных в карту.


Базовая модель InfoWindow

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

Создание базового всплывающего окна:

const map = new google.maps.Map(document.getElementById("map"), {
  center: { lat: 52.4, lng: 71.5 },
  zoom: 6,
});

const marker = new google.maps.Marker({
  position: { lat: 52.4, lng: 71.5 },
  map: map,
});

const infoWindow = new google.maps.InfoWindow({
  content: "<div><strong>Объект</strong><br>Описание точки</div>",
});

marker.addListener("click", () => {
  infoWindow.open({
    anchor: marker,
    map,
  });
});

InfoWindow поддерживает HTML-разметку, но её сложность ограничена: вложенные интерактивные компоненты, обработчики событий и динамическое обновление DOM требуют дополнительных подходов.


Управление жизненным циклом InfoWindow

Всплывающие окна в Google Maps API не создаются автоматически для каждого маркера. Обычно используется один экземпляр InfoWindow, который переиспользуется для нескольких объектов. Такой подход снижает нагрузку на DOM и предотвращает накопление лишних узлов.

const infoWindow = new google.maps.InfoWindow();

markers.forEach((marker) => {
  marker.addListener("click", () => {
    infoWindow.setContent(marker.title);
    infoWindow.open({
      anchor: marker,
      map,
    });
  });
});

Ключевой момент — переопределение содержимого через setContent перед каждым открытием.


HTML-контент и динамическое обновление

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

infoWindow.setContent(`
  <div class="popup">
    <h3>Заголовок</h3>
    <p>Динамическое описание объекта</p>
    <button id="actionBtn">Действие</button>
  </div>
`);

Обработчики событий для элементов внутри InfoWindow требуют делегирования после открытия окна:

google.maps.event.addListener(infoWindow, "domready", () => {
  document.getElementById("actionBtn").addEventListener("click", () => {
    console.log("Клик внутри всплывающего окна");
  });
});

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


Стилизация InfoWindow

Стандартный внешний вид ограничен встроенными стилями Google Maps. Изменение визуальной части достигается через обёртку HTML и переопределение CSS внутри контента.

infoWindow.setContent(`
  <div class="custom-window">
    <div class="header">Объект</div>
    <div class="body">Информация</div>
  </div>
`);
.custom-window {
  font-family: Arial, sans-serif;
  width: 200px;
}

.custom-window .header {
  font-weight: bold;
  margin-bottom: 5px;
}

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


Ограничения стандартного InfoWindow

Несмотря на удобство, InfoWindow имеет ряд архитектурных ограничений:

  • ограниченный контроль над позиционированием;
  • невозможность полного управления жизненным циклом DOM;
  • сложности с вложенными интерактивными компонентами;
  • ограниченная кастомизация поведения анимации;
  • закрытие окна при взаимодействии с картой происходит автоматически.

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


Пользовательские оверлеи через OverlayView

OverlayView предоставляет доступ к низкоуровневому API, позволяющему размещать произвольные DOM-элементы поверх карты с точным контролем координат и отрисовки.

Базовая структура:

class CustomPopup extends google.maps.OverlayView {
  constructor(position, content) {
    super();
    this.position = position;
    this.content = content;
    this.div = null;
  }

  onAdd() {
    this.div = document.createElement("div");
    this.div.className = "custom-popup";
    this.div.innerHTML = this.content;

    const panes = this.getPanes();
    panes.floatPane.appendChild(this.div);
  }

  draw() {
    const projection = this.getProjection();
    const point = projection.fromLatLngToDivPixel(this.position);

    if (this.div) {
      this.div.style.left = point.x + "px";
      this.div.style.top = point.y + "px";
    }
  }

  onRemove() {
    if (this.div) {
      this.div.remove();
      this.div = null;
    }
  }
}

Использование:

const popup = new CustomPopup(
  { lat: 52.4, lng: 71.5 },
  "<div>Кастомный блок</div>"
);

popup.setMap(map);

Геометрия позиционирования

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

Ключевой момент заключается в том, что позиция должна пересчитываться при каждом изменении состояния карты:

  • панорамирование;
  • масштабирование;
  • вращение (в некоторых режимах);
  • изменение размеров контейнера.

Метод draw() вызывается автоматически системой рендеринга.


Управление слоями отображения

Google Maps использует несколько слоёв (panes):

  • mapPane — базовая карта;
  • overlayLayer — линии и полигоны;
  • overlayMouseTarget — интерактивные элементы;
  • floatPane — всплывающие элементы и окна.

Для всплывающих окон обычно используется floatPane, так как он обеспечивает максимальный приоритет отображения.

this.getPanes().floatPane.appendChild(this.div);

Интерактивность кастомных попапов

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

this.div.addEventListener("click", (e) => {
  e.stopPropagation();
  console.log("Клик по кастомному попапу");
});

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


Обновление данных внутри попапа

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

this.div.querySelector(".content").textContent = "Новые данные";

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


Закрытие и управление состоянием

Закрытие кастомного оверлея реализуется вручную через setMap(null):

popup.setMap(null);

При этом вызывается onRemove, где происходит очистка DOM и освобождение памяти.


Производительность при большом количестве окон

При работе с большим числом всплывающих окон важно учитывать:

  • частые пересчёты draw() создают нагрузку на main thread;
  • DOM-элементы в floatPane могут становиться дорогими при масштабировании;
  • использование одного InfoWindow предпочтительнее множественных экземпляров;
  • для кластеризации маркеров всплывающие окна должны быть централизованы.

Оптимизация часто достигается через:

  • дебаунс пересчёта;
  • ограничение числа активных окон;
  • виртуализацию контента;
  • отказ от тяжёлой анимации.

Сочетание InfoWindow и OverlayView

В практических системах часто используется гибридный подход:

  • InfoWindow — для простых карточек и краткой информации;
  • OverlayView — для сложных интерактивных интерфейсов;
  • единый менеджер состояния — для управления активными окнами.

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