Контролы в Leaflet представляют собой расширяемый механизм
интерфейсных элементов, закреплённых в пределах карты и интегрированных
в её жизненный цикл. Архитектура контролов построена вокруг базового
класса L.Control, который обеспечивает единый контракт для
добавления, удаления и управления DOM-элементами, привязанными к
экземпляру карты.
Каждый контрол в Leaflet — это объект, наследующийся от
L.Control. Его основная задача заключается в создании
DOM-узла и его привязке к карте через метод addTo(map).
Контрол не работает изолированно: он связан с экземпляром
L.Map и реагирует на его состояние.
Ключевая особенность заключается в том, что контролы не рисуются внутри canvas или SVG-слоя карты. Они размещаются поверх карты в отдельной DOM-иерархии:
.leaflet-control-container
.leaflet-top.leaflet-bottom.leaflet-left.leaflet-rightКаждый контрол позиционируется в одной из четырёх зон, задаваемых
опцией position.
Создание пользовательского контрола осуществляется через расширение
L.Control.extend. Базовая структура включает реализацию
метода onAdd, который отвечает за создание DOM-элемента, и
опционально onRemove, который вызывается при удалении
контрола с карты.
L.Control.MyControl = L.Control.extend({
options: {
position: 'topright'
},
onAdd: function (map) {
const container = L.DomUtil.create('div', 'leaflet-bar my-control');
container.innerHTML = 'Кнопка';
L.DomEvent.disableClickPropagation(container);
container.oncl ick = function () {
console.log('Контрол активирован');
};
return container;
},
onRemove: function (map) {
// очистка ресурсов, если требуется
}
});
L.control.myControl = function (opts) {
return new L.Control.MyControl(opts);
};
После определения контрол добавляется на карту через:
L.control.myControl({ position: 'topleft' }).addTo(map);
Контрол проходит через несколько стадий:
L.Control.extend.addTo(map).onAdd(map) — создание
DOM-структуры.remove() и затем
onRemove(map).Важный момент: DOM-элемент должен быть полностью автономным и не зависеть от внешнего состояния, кроме объекта карты.
Leaflet предоставляет утилиты L.DomUtil и
L.DomEvent для безопасной работы с DOM.
const container = L.DomUtil.create('div', 'leaflet-control custom-control');
Второй аргумент добавляет CSS-класс, который участвует в стилизации.
L.DomEvent.on(container, 'click', function (e) {
console.log('click');
});
Для предотвращения взаимодействия кликов с картой используется:
L.DomEvent.disableClickPropagation(container);
L.DomEvent.disableScrollPropagation(container);
Без этого контролы могут вызывать нежелательные перемещения карты при взаимодействии.
Опция position определяет размещение контрола:
toplefttoprightbottomleftbottomrightКаждая зона представляет собой стек контролов, добавляемых в порядке регистрации.
Пример:
L.control.zoom({ position: 'bottomright' }).addTo(map);
Контролы внутри одной зоны автоматически выстраиваются вертикально.
Leaflet предоставляет базовый пример контролов — масштабирование карты:
L.control.zoom({
zoomInText: '+',
zoomOutText: '-'
}).addTo(map);
Он реализован тем же механизмом L.Control, что и
пользовательские расширения.
Часто используемый паттерн — одиночная кнопка с состоянием.
L.Control.Toggle = L.Control.extend({
options: {
position: 'topright'
},
onAdd: function (map) {
const btn = L.DomUtil.create('button', 'leaflet-bar toggle-btn');
btn.innerHTML = 'Слой';
let active = false;
L.DomEvent.on(btn, 'click', function () {
active = !active;
if (active) {
btn.classList.add('active');
} else {
btn.classList.remove('active');
}
});
L.DomEvent.disableClickPropagation(btn);
return btn;
}
});
Такая реализация сохраняет состояние внутри замыкания, не требуя глобального хранения.
Контролы часто взаимодействуют с картой: изменяют слои, центр или масштаб.
L.Control.CenterButton = L.Control.extend({
options: { position: 'topleft' },
onAdd: function (map) {
const container = L.DomUtil.create('div', 'leaflet-bar');
container.innerHTML = 'Центр';
L.DomEvent.on(container, 'click', function () {
map.setView([51.505, -0.09], 13);
});
L.DomEvent.disableClickPropagation(container);
return container;
}
});
Контрол получает ссылку на map через аргумент
onAdd, что делает возможным полный доступ к API карты.
Контролы часто используются для управления слоями:
L.Control.LayerSwitcher = L.Control.extend({
onAdd: function (map) {
const container = L.DomUtil.create('div', 'leaflet-bar');
const btn1 = L.DomUtil.create('button', '', container);
btn1.innerHTML = 'OSM';
const btn2 = L.DomUtil.create('button', '', container);
btn2.innerHTML = 'Satellite';
const base1 = L.tileLayer('url1');
const base2 = L.tileLayer('url2');
btn1.oncl ick = () => {
map.addLayer(base1);
map.removeLayer(base2);
};
btn2.oncl ick = () => {
map.addLayer(base2);
map.removeLayer(base1);
};
L.DomEvent.disableClickPropagation(container);
return container;
}
});
Такой подход формирует простую систему переключения базовых слоёв без
использования встроенного L.control.layers.
Контролы в Leaflet используют предсказуемую CSS-систему:
.leaflet-control — базовый стиль.leaflet-bar — рамка с кнопками.leaflet-control-container — контейнер всех
контроловПример кастомного стиля:
.my-control {
background: white;
padding: 6px 10px;
border-radius: 4px;
box-shadow: 0 1px 5px rgba(0,0,0,0.3);
cursor: pointer;
}
Контролы не должны полагаться на глобальные стили страницы, иначе возможны конфликты отображения.
При работе с динамическими контролами важно учитывать освобождение ресурсов:
onRemove: function (map) {
L.DomEvent.off(this._container);
}
Контролы, создающие внешние подписки (например, WebSocket или
интервал), обязаны очищать их в onRemove.
Контролы могут добавляться и удаляться в рантайме:
const ctrl = L.control.myControl().addTo(map);
map.removeControl(ctrl);
Это позволяет строить интерфейсы, зависящие от состояния приложения.
При наличии нескольких контролов одной позиции порядок определяется порядком добавления. Контролы не знают друг о друге, что делает систему слабосвязанной.
При необходимости координации используется:
map как источник истины)Архитектура имеет ряд ограничений:
Несмотря на это, модель остаётся гибкой за счёт минималистичного API и прямого доступа к DOM и карте.