Label Button Manager
Product Version: GeoLeaf Platform V2
Module: packages/core/src/modules/optional/labels/label-button-manager.tsVersion: 2.1.5 Last Updated: March 2026
Table of Contents
Purpose & Architecture
Overview
The Label Button Manager is a centralized controller for label toggle buttons in the Layer Manager. It provides a single source of truth for button creation, state synchronization, and decision logic.
Responsibilities
- Create label buttons during first render of a layer in Layer Manager
- Synchronize button state (enabled/disabled, active/inactive)
- Apply consistent decision logic across all layers
- Manage debouncing to prevent excessive updates
Key Design Principles
- Single Responsibility: One module handles ALL label button logic
- Centralized State: No scattered button management code
- Debounced Updates: 300ms debounce for non-critical updates
- Immediate Sync: Optional immediate updates for critical changes (layer visibility)
- Defensive Coding: Handles missing DOM elements gracefully
Source file structure
optional/labels/
├── label-button-manager.ts // Centralized button manager (this module)
├── label-button-manager-lite.ts // Lite variant (bundle-core-lite)
├── labels.ts // Label rendering engine
├── labels-lite.ts // Lite variant
└── label-renderer.ts // Low-level label renderingAPI Reference
Public Methods
createButton(layerId, controlsContainer)
Creates a label button for a layer during first render.
Parameters:
layerId(string, required) - Layer identifiercontrolsContainer(HTMLElement, required) - DOM container for layer controls
Returns:
HTMLElement- The created button elementnull- If parameters are missing or creation fails
Behavior:
- Checks if button already exists (prevents duplicates)
- Creates button with label icon
- Uses i18n key
aria.labels.toggleforaria-labelandtitle - Attaches click handler for label toggle
- Inserts button before visibility toggle in controls
- Initially disabled until first sync
Example:
// Called by Layer Manager during first render
const button = GeoLeaf._LabelButtonManager.createButton("poi-restaurants", controlsContainer);DOM Structure:
<button
class="gl-layer-manager__label-toggle gl-layer-manager__label-toggle--disabled"
type="button"
disabled
aria-label="Afficher/masquer les étiquettes"
aria-pressed="false"
>
<span class="gl-layer-manager__label-toggle-icon">🏷️</span>
</button>sync(layerId)
Synchronizes button state with debouncing (300ms delay).
Parameters:
layerId(string, required) - Layer identifier
Returns: void
Behavior:
- Cancels any pending sync for this layer
- Schedules new sync after 300ms
- Prevents excessive DOM updates during rapid changes
- Ideal for non-critical updates (style changes, config updates)
Example:
// Called after style change (non-urgent)
GeoLeaf._LabelButtonManager.sync("poi-restaurants");Use Cases:
- Style file loaded
- Configuration updated
- Theme changed
- Non-critical state changes
syncImmediate(layerId)
Synchronizes button state immediately without debouncing.
Parameters:
layerId(string, required) - Layer identifier
Returns: void
Behavior:
- Cancels any pending debounced sync
- Executes sync immediately
- Use for critical updates requiring instant feedback
Example:
// Called after layer visibility toggle (urgent)
GeoLeaf._LabelButtonManager.syncImmediate("poi-restaurants");Use Cases:
- Layer visibility toggled
- User action requiring immediate visual feedback
- Critical state changes
Internal Methods
_doSync(layerId) (private)
Executes the actual synchronization logic.
Process:
- Find button in DOM (with fallback to create if missing)
- Collect current state using
_getState() - Apply state to button using
_applyState()
Fallback Behavior:
- If button not found but layer item exists, creates button on-the-fly
- Logs debug messages for troubleshooting
- Handles missing DOM elements gracefully
_getState(layerId) (private)
Collects current state from all relevant components.
Returns:
{
layerId: "poi-restaurants",
layerExists: true,
layerVisible: true,
labelEnabled: true,
areLabelsActive: false
}State Properties:
layerId- Layer identifierlayerExists- Layer found inGeoJSONCore.getLayerById()layerVisible- Layer visibility from_visibility.currentlabelEnabled- Style haslabel.enabled: trueareLabelsActive- Labels currently displayed for this layer
Dependencies:
GeoJSONCore.getLayerById()- Layer data access (internal modulebuilt-in/geojson/core.ts)Labels.areLabelsEnabled()- Label visibility status (internal moduleoptional/labels/labels.ts)
_applyState(button, state) (private)
Applies decision logic to button based on collected state.
Parameters:
button(HTMLButtonElement) - Button to updatestate(Object) - State from_getState()
Decision Logic:
Can Use Labels = layerVisible AND labelEnabled
IF Can Use Labels:
- button.disabled = false
- Remove "gl-layer-manager__label-toggle--disabled"
- IF areLabelsActive AND layerVisible:
- Add "gl-layer-manager__label-toggle--on"
- aria-pressed = "true"
- ELSE:
- Remove "gl-layer-manager__label-toggle--on"
- aria-pressed = "false"
ELSE:
- button.disabled = true
- Add "gl-layer-manager__label-toggle--disabled"
- Remove "gl-layer-manager__label-toggle--on"
- aria-pressed = "false"CSS Classes:
gl-layer-manager__label-toggle- Base class (always present)gl-layer-manager__label-toggle--disabled- Button is disabled (grayed out)gl-layer-manager__label-toggle--on- Labels are active (highlighted)
State Management
Button Registry
The module maintains internal state for each button:
{
_syncTimeouts: Map<layerId, timeoutId>
}Purpose:
- Track pending debounced syncs
- Allow cancellation before execution
- Prevent duplicate updates
Lifecycle:
- Timeout created on
sync()call - Timeout cancelled if new
sync()called before execution - Timeout deleted after execution or cancellation
State Sources
The module does NOT store layer state. It queries live state from:
GeoJSONCore.getLayerById()(imported frombuilt-in/geojson/core.ts)_visibility.current- Layer visibilitycurrentStyle.label.enabled- Label configuration
Labels.areLabelsEnabled()(imported fromoptional/labels/labels.ts)- Current label visibility for layer
Benefits:
- Single source of truth (no synchronization issues)
- Always reflects latest state
- No stale data
Decision Logic
Rules
The button follows these simple rules:
- Button is ALWAYS visible for all layers
- Button is clickable IF:
- Layer is visible (
_visibility.current = true) - AND style has
label.enabled: true
- Layer is visible (
- Button is disabled (grayed) IF:
- Layer is hidden
- OR style has
label.enabled: false
- Button shows active state IF:
- Button is clickable
- AND labels are currently displayed
Visual States
| Layer Visible | label.enabled | Button State |
|---|---|---|
| ✅ | ✅ | Enabled, can click |
| ✅ | ❌ | Disabled, grayed |
| ❌ | ✅ | Disabled, grayed |
| ❌ | ❌ | Disabled, grayed |
Flowchart
┌─────────────────┐
│ Button Render │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Is Layer ON? │
└────────┬────────┘
│
┌────┴────┐
│ NO │ YES
▼ ▼
┌──────┐ ┌──────────────────┐
│ Gray │ │ label.enabled? │
│ OUT │ └────────┬─────────┘
└──────┘ │
┌────┴────┐
│ NO │ YES
▼ ▼
┌──────┐ ┌──────────┐
│ Gray │ │ ENABLED │
│ OUT │ │ clickable│
└──────┘ └─────┬────┘
│
▼
┌───────────────┐
│ areLabelsOn? │
└───────┬───────┘
│
┌────┴────┐
│ NO │ YES
▼ ▼
┌──────┐ ┌──────┐
│Normal│ │Active│
└──────┘ └──────┘Integration Examples
Example 1: Layer Manager Initial Render
File: built-in/layer-manager/renderer.ts
// During first render of layer in Layer Manager
function _renderLayerItem(layerId) {
const layerItem = document.createElement("div");
layerItem.classList.add("gl-layer-manager__item");
layerItem.setAttribute("data-layer-id", layerId);
const controls = document.createElement("div");
controls.classList.add("gl-layer-manager__item-controls");
// Create label button
if (GeoLeaf._LabelButtonManager) {
GeoLeaf._LabelButtonManager.createButton(layerId, controls);
}
// Create visibility toggle
const visibilityToggle = _createVisibilityToggle(layerId);
controls.appendChild(visibilityToggle);
layerItem.appendChild(controls);
return layerItem;
}Example 2: Style Loader Sync
File: optional/labels/labels.ts
// After loading new style file
async function loadStyle(layerId, styleId) {
const styleData = await fetchStyle(layerId, styleId);
// Apply style to layer
applyStyle(layerId, styleData);
// Sync label button (debounced, non-urgent)
if (GeoLeaf._LabelButtonManager) {
GeoLeaf._LabelButtonManager.sync(layerId);
}
}Example 3: Layer Visibility Toggle
File: built-in/layer-manager/visibility-checker.ts
// User toggles layer visibility
function toggleLayerVisibility(layerId) {
const layerData = GeoJSONCore.getLayerById(layerId);
if (layerData._visibility.current) {
// Hide MapLibre layer
map.setLayoutProperty(layerId, "visibility", "none");
layerData._visibility.current = false;
} else {
// Show MapLibre layer
map.setLayoutProperty(layerId, "visibility", "visible");
layerData._visibility.current = true;
}
// Sync label button IMMEDIATELY (urgent update)
if (GeoLeaf._LabelButtonManager) {
GeoLeaf._LabelButtonManager.syncImmediate(layerId);
}
}Example 4: Label Toggle Click Handler
Internal to createButton():
const onLabelToggle = function (ev) {
ev.stopPropagation();
ev.preventDefault();
if (button.disabled) return;
// Verify label.enabled in current style
const layerData = GeoJSONCore.getLayerById(layerId);
const labelEnabled = layerData?.currentStyle?.label?.enabled === true;
if (!labelEnabled) return;
// Toggle labels via Labels module
const newState = Labels.toggleLabels(layerId);
// Update button visual immediately
if (newState) {
button.classList.add("gl-layer-manager__label-toggle--on");
button.setAttribute("aria-pressed", "true");
} else {
button.classList.remove("gl-layer-manager__label-toggle--on");
button.setAttribute("aria-pressed", "false");
}
};Testing
Test File
Location: packages/core/__tests__/labels/label-button-visibility.test.js
Test Coverage
The test suite covers:
Button Creation
- Creates button with correct structure
- Prevents duplicate buttons
- Inserts button in correct position
- Returns null for invalid parameters
State Synchronization
sync()debounces updates (300ms)syncImmediate()executes without delay- Cancels pending syncs correctly
- Handles missing buttons gracefully
Decision Logic
- Button enabled when layer visible + label.enabled
- Button disabled when layer hidden
- Button disabled when label.enabled = false
- Active state reflects label visibility
Integration
- Works with Layer Manager render
- Responds to visibility toggles
- Updates on style changes
- Handles missing GeoLeaf modules
Running Tests
# Run all label tests
npm test -- labels
# Run button manager tests specifically
npm test -- label-button-visibility.test.js
# Run with coverage
npm run test:coverageSample Test
describe("LabelButtonManager", () => {
test("button enabled when layer visible and label.enabled", () => {
const layerId = "test-layer";
// Setup layer
mockLayer(layerId, {
visible: true,
style: { label: { enabled: true } },
});
// Create button
const button = GeoLeaf._LabelButtonManager.createButton(layerId, container);
// Sync state
GeoLeaf._LabelButtonManager.syncImmediate(layerId);
// Assertions
expect(button.disabled).toBe(false);
expect(button.classList.contains("gl-layer-manager__label-toggle--disabled")).toBe(false);
});
});Best Practices
When to Use sync() vs syncImmediate()
| Scenario | Method | Reason |
|---|---|---|
| Style file loaded | sync() | Non-urgent, debounce OK |
| Theme changed | sync() | Non-urgent, multiple layers |
| Config updated | sync() | Non-urgent |
| Layer visibility toggle | syncImmediate() | User action, needs instant feedback |
| User clicks button | Internal | Handled by click handler |
| Layer removed from map | syncImmediate() | Critical state change |
Performance Tips
Prefer sync() for batch updates
javascript// Good: debounced layerIds.forEach((id) => manager.sync(id)); // Bad: too many immediate updates layerIds.forEach((id) => manager.syncImmediate(id));Trust the decision logic
javascript// Do not manually disable/enable buttons // Let _applyState() handle it
Debugging
Enable debug logging:
GeoLeaf.Config.setDebug({
enabled: true,
modules: ["labels", "ui"],
});Check button state in console:
// Get button
const button = document.querySelector(
'[data-layer-id="poi-restaurants"] .gl-layer-manager__label-toggle'
);
// Inspect state
console.log({
disabled: button.disabled,
classes: button.className,
ariaPressed: button.getAttribute("aria-pressed"),
});
// Get layer state (internal)
const state = GeoLeaf._LabelButtonManager._getState("poi-restaurants");
console.log("Layer state:", state);Related Documentation
- Layer Manager — Layer Manager integration
- GeoLeaf.Legend — Legend module
Last Updated: March 2026 Module Version: 2.1.5
