Позиционирование контрола

В Leaflet позиционирование элементов управления строится вокруг концепции «угловых контейнеров». Карта разделяет интерфейс на четыре фиксированные области, в которые размещаются все стандартные и пользовательские контролы: верхний левый, верхний правый, нижний левый и нижний правый углы. Эти области не являются произвольными координатами — они реализованы через DOM-контейнеры с предсказуемой структурой и CSS-правилами.

Каждый контрол в Leaflet при добавлении на карту попадает в один из четырёх контейнеров:

  • topleft
  • topright
  • bottomleft
  • bottomright

Эти значения задаются через опцию position при создании контрола:

L.control.zoom({
    position: 'topright'
}).addTo(map);

Внутри библиотеки каждому значению соответствует DOM-узел вида:

  • .leaflet-top.leaflet-left
  • .leaflet-top.leaflet-right
  • .leaflet-bottom.leaflet-left
  • .leaflet-bottom.leaflet-right

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

Внутренняя структура контейнеров

Контейнер карты содержит четыре основных блока:

<div class="leaflet-control-container">
    <div class="leaflet-top leaflet-left"></div>
    <div class="leaflet-top leaflet-right"></div>
    <div class="leaflet-bottom leaflet-left"></div>
    <div class="leaflet-bottom leaflet-right"></div>
</div>

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

Порядок контролов внутри одного угла

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

L.control.scale({ position: 'bottomleft' }).addTo(map);
L.control.zoom({ position: 'bottomleft' }).addTo(map);
L.control.layers(baseLayers, overlays, { position: 'bottomleft' }).addTo(map);

Результат: элементы будут расположены друг под другом в порядке добавления.

Факторы, влияющие на порядок:

  • порядок вызова .addTo(map)
  • внутренний z-index контролов
  • отступы CSS (margin, padding)

CSS-механика позиционирования

Leaflet использует фиксированное позиционирование через CSS:

.leaflet-top,
.leaflet-bottom {
    position: absolute;
    z-index: 1000;
    pointer-events: none;
}

.leaflet-top {
    top: 0;
}

.leaflet-bottom {
    bottom: 0;
}

.leaflet-left {
    left: 0;
}

.leaflet-right {
    right: 0;
}

Каждый контрол внутри контейнера имеет:

.leaflet-control {
    pointer-events: auto;
    margin: 10px;
    clear: both;
}

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

Создание собственного контрола с позиционированием

Любой пользовательский контрол наследуется от L.Control и получает стандартную систему позиционирования.

L.Control.CustomButton = L.Control.extend({
    options: {
        position: 'topright'
    },

    onAdd: function(map) {
        const container = L.DomUtil.create('div', 'leaflet-bar custom-button');

        container.innerHTML = 'Click';

        L.DomEvent.disableClickPropagation(container);

        return container;
    }
});

L.control.customButton = function(opts) {
    return new L.Control.CustomButton(opts);
};

L.control.customButton({ position: 'topright' }).addTo(map);

Опция position автоматически определяет, в какой контейнер попадёт контрол.

Управление порядком отображения в одном углу

Порядок контролов регулируется не только добавлением, но и DOM-манипуляцией:

const controlA = L.control({ position: 'topright' });
const controlB = L.control({ position: 'topright' });

controlA.on Add = function() {
    const div = L.DomUtil.create('div', 'box-a');
    div.innerHTML = 'A';
    return div;
};

controlB.on Add = function() {
    const div = L.DomUtil.create('div', 'box-b');
    div.innerHTML = 'B';
    return div;
};

controlB.addTo(map);
controlA.addTo(map);

Даже при обратном порядке добавления можно изменить визуальную последовательность через DOM:

const container = map.getContainer();
const rightTop = container.querySelector('.leaflet-top.leaflet-right');

rightTop.appendChild(rightTop.firstChild);

Стандартные контролы и их позиции

Leaflet по умолчанию использует фиксированные позиции:

  • Zoom Control — topright
  • Attribution Control — bottomright
  • Layers Control — чаще topright или bottomright (зависит от реализации)
  • Scale Control — bottomleft или bottomright

Пример настройки стандартных контролов:

const map = L.map('map', {
    zoomControl: false,
    attributionControl: false
});

L.control.zoom({
    position: 'bottomright'
}).addTo(map);

L.control.attribution({
    position: 'bottomleft'
}).addTo(map);

Перекрытие и z-index слоёв контролов

Контролы Leaflet располагаются выше слоёв карты за счёт z-index: 1000. Однако разные контролы могут перекрывать друг друга, если:

  • используются нестандартные CSS
  • добавлены кастомные абсолютные элементы
  • изменён порядок DOM

Пример повышения приоритета:

.custom-control {
    z-index: 1100;
}

Особенности контейнера карты

Контейнер контролов привязан к элементу карты:

map.getContainer()

Внутри него создаётся структура контролов при инициализации. Прямое изменение DOM вне API Leaflet допустимо, но может привести к рассинхронизации состояния библиотеки.

Динамическое перемещение контрола

Leaflet не предоставляет официального API для изменения позиции уже добавленного контрола. Однако возможно переинициализировать его:

map.removeControl(control);

control.options.position = 'bottomright';

map.addControl(control);

Такой подход пересоздаёт привязку к другому контейнеру.

Группировка контролов в одном контейнере

Несколько элементов можно объединять логически:

const groupControl = L.Control.extend({
    options: { position: 'topright' },

    onAdd: function() {
        const wrapper = L.DomUtil.create('div', 'control-group');

        const btn1 = L.DomUtil.create('button', '', wrapper);
        btn1.innerHTML = '1';

        const btn2 = L.DomUtil.create('button', '', wrapper);
        btn2.innerHTML = '2';

        return wrapper;
    }
});

L.control.groupControl = opts => new groupControl(opts);
L.control.groupControl().addTo(map);

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

Влияние масштабирования и размеров карты

Позиции контролов фиксированы относительно контейнера карты, поэтому при изменении размеров:

  • контролы остаются «прилипшими» к углам
  • пересчёт происходит через CSS, а не JavaScript
  • адаптивность обеспечивается flex- и block-моделями внутри контейнеров

При ресайзе контейнера карты:

map.invalidateSize();

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

Взаимодействие с pointer-events

Контейнеры контролов используют отключение событий:

pointer-events: none;

а сами контролы включают их обратно:

pointer-events: auto;

Это создаёт важный эффект: клики проходят через пустые области интерфейса, но блокируются на самих контролах.