Стилизация пользовательских контролов

DOM-элементы пользовательских контролов в Google Maps JavaScript API представляют собой обычные HTML-узлы, которые добавляются в специальную систему слоёв управления картой. В отличие от встроенных элементов интерфейса, такие контролы полностью управляются разработчиком: структура, стилизация, поведение и состояние определяются вручную, а карта лишь предоставляет механизм их размещения через map.controls.


Каждый контрол встраивается в одну из заранее определённых позиций интерфейса карты. Эти позиции задаются перечислением google.maps.ControlPosition и определяют, где именно будет отображаться DOM-элемент:

  • TOP_LEFT
  • TOP_CENTER
  • TOP_RIGHT
  • LEFT_TOP
  • RIGHT_TOP
  • LEFT_CENTER
  • RIGHT_CENTER
  • LEFT_BOTTOM
  • RIGHT_BOTTOM
  • BOTTOM_CENTER
  • BOTTOM_LEFT
  • BOTTOM_RIGHT

Каждая позиция представляет собой массив DOM-элементов, который Google Maps самостоятельно размещает поверх карты.

Добавление контролов происходит через коллекцию:

map.controls[google.maps.ControlPosition.TOP_RIGHT].push(controlDiv);

controlDiv — обычный HTML-элемент, который становится частью интерфейса карты.


Базовая структура пользовательского контрола

Пользовательский контрол обычно состоит из трёх уровней:

  1. Внешний контейнер (обёртка)
  2. Внутренний элемент (кнопка, панель, поле ввода)
  3. Стилизация и обработчики событий

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

function createCustomControl(map) {
  const controlDiv = document.createElement("div");

  const controlUI = document.createElement("div");
  const controlText = document.createElement("div");

  controlUI.appendChild(controlText);
  controlDiv.appendChild(controlUI);

  map.controls[google.maps.ControlPosition.TOP_LEFT].push(controlDiv);
}

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


Базовая стилизация через inline-стили

Наиболее прямой способ управления внешним видом — установка стилей через style:

controlDiv.style.margin = "10px";
controlDiv.style.backgroundColor = "#fff";
controlDiv.style.borderRadius = "8px";
controlDiv.style.boxShadow = "0 2px 6px rgba(0,0,0,0.3)";
controlDiv.style.cursor = "pointer";
controlDiv.style.userSelect = "none";

Такой подход гарантирует независимость от внешних CSS-файлов, но плохо масштабируется при усложнении интерфейса.


Стилизация через CSS-классы

Более устойчивый подход — назначение классов:

controlDiv.className = "custom-map-control";
controlUI.className = "custom-map-control__ui";
controlText.className = "custom-map-control__text";

И соответствующий CSS:

.custom-map-control {
  margin: 10px;
}

.custom-map-control__ui {
  background: #ffffff;
  border-radius: 10px;
  padding: 8px 12px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
  display: flex;
  align-items: center;
}

.custom-map-control__text {
  font-size: 14px;
  font-family: Roboto, Arial, sans-serif;
  color: #333;
}

Такой подход позволяет:

  • централизованно управлять стилями
  • использовать темы
  • переиспользовать компоненты
  • подключать препроцессоры (SASS/LESS)

Поведение и взаимодействие

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

Базовая обработка клика

controlUI.addEventListener("click", () => {
  map.setZoom(map.getZoom() + 1);
});

Отключение всплытия событий

Поскольку контрол находится поверх карты, любые события могут «проваливаться» в слой карты (drag, zoom, click). Чтобы этого избежать:

controlUI.addEventListener("mousedown", (e) => {
  e.stopPropagation();
});

controlUI.addEventListener("click", (e) => {
  e.stopPropagation();
});

Это критично для корректного поведения сложных интерфейсов (поисковые поля, формы, фильтры).


Поддержка hover- и active-состояний

Интерактивность обычно реализуется через CSS:

.custom-map-control__ui:hover {
  background: #f5f5f5;
}

.custom-map-control__ui:active {
  transform: scale(0.98);
}

Также можно управлять состояниями через классы:

controlUI.addEventListener("mouseenter", () => {
  controlUI.classList.add("is-hovered");
});

controlUI.addEventListener("mouseleave", () => {
  controlUI.classList.remove("is-hovered");
});

Стилизация под темную тему

Контролы не адаптируются автоматически под тему карты. Поэтому требуется ручная синхронизация.

Пример базовой тёмной темы:

.custom-map-control--dark .custom-map-control__ui {
  background: #2b2b2b;
  color: #ffffff;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
}

Переключение темы:

function setDarkMode(enabled) {
  if (enabled) {
    controlDiv.classList.add("custom-map-control--dark");
  } else {
    controlDiv.classList.remove("custom-map-control--dark");
  }
}

Контролы с динамическим состоянием

Часто контролы отражают состояние карты: масштаб, центр, активные слои.

Пример отображения масштаба

map.addListener("zoom_changed", () => {
  controlText.textContent = `Zoom: ${map.getZoom()}`;
});

Пример отображения координат центра

map.addListener("center_changed", () => {
  const center = map.getCenter();
  controlText.textContent =
    `${center.lat().toFixed(4)}, ${center.lng().toFixed(4)}`;
});

Компоновка сложных контролов

Контролы могут включать сложные интерфейсы: формы поиска, фильтры, списки.

Пример поискового поля

const input = document.createElement("input");
input.type = "text";
input.placeholder = "Поиск";

input.addEventListener("keydown", (e) => {
  if (e.key === "Enter") {
    const geocoder = new google.maps.Geocoder();

    geocoder.geocode({ address: input.value }, (results, status) => {
      if (status === "OK") {
        map.setCenter(results[0].geometry.location);
      }
    });
  }
});

controlUI.appendChild(input);

Стилизация input требует отдельного внимания:

.custom-map-control input {
  border: none;
  outline: none;
  font-size: 14px;
  background: transparent;
  width: 180px;
}

Контроль размеров и адаптивность

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

  • ограничение ширины
  • перенос элементов
  • поведение на мобильных устройствах
.custom-map-control {
  max-width: 90vw;
}

@media (max-width: 600px) {
  .custom-map-control__ui {
    padding: 6px 10px;
    font-size: 13px;
  }
}

Позиционирование и взаимодействие с другими контролами

Порядок отображения контролов внутри одной позиции определяется порядком добавления:

map.controls[google.maps.ControlPosition.TOP_RIGHT].push(controlA);
map.controls[google.maps.ControlPosition.TOP_RIGHT].push(controlB);

controlB окажется ниже controlA.

Для сложных интерфейсов важно избегать перекрытий и учитывать z-index:

.custom-map-control {
  position: relative;
  z-index: 5;
}

Использование Shadow DOM (изолированная стилизация)

Для предотвращения конфликтов CSS можно использовать Shadow DOM:

const shadow = controlDiv.attachShadow({ mode: "open" });

shadow.innerHTML = `
  <style>
    .btn {
      background: #fff;
      padding: 8px 12px;
      border-radius: 8px;
      cursor: pointer;
    }
  </style>
  <div class="btn">Контрол</div>
`;

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

  • изоляция стилей
  • отсутствие конфликтов с глобальным CSS
  • предсказуемость отображения

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

При использовании современных фреймворков контролы часто генерируются как отдельные компоненты, которые затем монтируются в DOM-контейнер карты.

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

map.controls[google.maps.ControlPosition.BOTTOM_LEFT].push(element);

Типичные ошибки стилизации

Часто возникающие проблемы:

  • отсутствие stopPropagation, из-за чего карта перетаскивается при работе с контролом
  • использование фиксированных размеров без адаптивности
  • конфликт z-index с элементами карты
  • отсутствие hover/focus состояний
  • смешивание inline-стилей и CSS без структуры

Поведение при масштабировании карты

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

background: rgba(255, 255, 255, 0.9);

или динамическая адаптация:

map.addListener("zoom_changed", () => {
  const zoom = map.getZoom();

  if (zoom > 12) {
    controlDiv.style.opacity = "1";
  } else {
    controlDiv.style.opacity = "0.85";
  }
});

Минимальный паттерн создания стилизованного контрола

function createStyledControl(map) {
  const controlDiv = document.createElement("div");
  controlDiv.className = "custom-map-control";

  const button = document.createElement("button");
  button.className = "custom-map-control__button";
  button.textContent = "Центрировать";

  button.addEventListener("click", (e) => {
    e.stopPropagation();
    map.setCenter({ lat: 0, lng: 0 });
  });

  controlDiv.appendChild(button);

  map.controls[google.maps.ControlPosition.TOP_RIGHT].push(controlDiv);
}
.custom-map-control__button {
  padding: 8px 14px;
  border-radius: 8px;
  border: none;
  background: #4285f4;
  color: #fff;
  cursor: pointer;
}

Система пользовательских контролов в Google Maps JavaScript API опирается на прямое управление DOM-структурой и требует строгого разделения логики, поведения и визуального слоя.