@geoleaf-plugins/table
v1.2.2
Published
GeoLeaf table plugin
Readme
@geoleaf-plugins/table
GeoLeaf plugin that adds a data table panel to an interactive map. The user opens a bottom-sheet panel, picks a layer, browses its features as a sortable, searchable table, selects rows (the selection is highlighted on the map and can be zoomed to), and exports the chosen rows — or the whole layer — to GeoJSON, CSV, KML, GPX or Excel (XLSX). All in the browser.
- MIT License — public registry (npmjs.org)
- Requires
@geoleaf/coreloaded before this plugin - ESM only — no CommonJS/UMD
- Zero runtime npm dependencies — Excel export uses an inlined, lazily-loaded OOXML writer (no SheetJS)
Extracted from
@geoleaf/core. The data table used to be part of the core and configured via the roottableConfigkey and theui.showTableflag. It is now this plugin, configured undermodules.table.*. See Migration from core — this is a breaking change.
Installation
npm install @geoleaf-plugins/tableLoad in your HTML after @geoleaf/core and before GeoLeaf.boot():
<script
type="module"
src="node_modules/@geoleaf-plugins/table/dist/geoleaf-table.plugin.js"
></script>The plugin mounts the GeoLeaf.Table namespace at load and, when enabled, injects its own UI: a "Table" tab in the desktop panel and a table icon in the mobile toolbar (both gated by modules.table.showButton).
Quick start
Enable the table in the active profile under modules.table (file config/plugins/table.json, referenced by Files.modules.table):
{
"enabled": true,
"showButton": true,
"defaultVisible": false
}Per layer, declare which columns the table shows under the layer's config.table (this binding stays on the layer):
{
"table": {
"enabled": true,
"title": "Communes",
"columns": [
{ "field": "properties.name", "label": "Name", "sortable": true },
{ "field": "properties.population", "label": "Population", "type": "number" }
]
}
}You can also drive the table programmatically:
<script type="module">
GeoLeaf.Table.setLayer("communes");
GeoLeaf.Table.show();
GeoLeaf.Table.setSelection(["feat-1", "feat-2"]);
GeoLeaf.Table.zoomToSelection();
GeoLeaf.Table.exportSelection("csv", { csvSeparator: ";" });
document.addEventListener("geoleaf:table:selectionChanged", (e) => {
console.log("selection:", e.detail);
});
</script>Configuration (modules.table.*)
| Key | Type | Default | Description |
| -------------------- | ------------ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| enabled | boolean | true | Mounts the module on geoleaf:map:ready when true. |
| showButton | boolean | true | Shows the desktop tab + mobile icon (read by the core registry). |
| defaultVisible | boolean | false | Opens the panel at boot. |
| pageSize | number | — | Deprecated, inert. No default, no read site. |
| maxRowsPerLayer | number | 30000 | Caps the rows kept per layer. The cut warns the user, once per layer and per session. 0 means zero. |
| enableExportButton | boolean | true | Shows the export buttons. |
| exportFormats | string[] | ["geojson", "csv", "kml", "gpx", "excel"] | Export buttons offered, in this order (panel.ts). |
| csvSeparator | "," \| ";" | "," | Column separator of the CSV export. ";" suits spreadsheet locales that read , as the decimal mark. |
| csvIncludeGeometry | boolean | false | Adds a __geometry column to the CSV export. |
| virtualScrolling | boolean | — | Deprecated, inert. Virtualisation is decided by a row-count threshold, never by this key. |
| defaultHeight | string | "40%" | Initial bottom-sheet height. |
| minHeight | string | "20%" | Minimum height (resize). |
| maxHeight | string | "60%" | Maximum height (resize). |
| resizable | boolean | true | Allows resizing via the drag handle. |
Per-layer (layer.config.table, stays on the layer): enabled, title, columns (field, label, width, sortable, type), defaultSort (field, direction), searchFields (the properties the search interrogates; absent or empty means every column — both ["properties.name"] and ["name"] are accepted).
Public API (GeoLeaf.Table)
| Method | Description |
| ------------------------------------ | ------------------------------------------------------------- |
| show() | Shows the table panel. |
| hide() | Hides the panel and clears the highlight overlay. |
| toggle() / open() | Toggles the panel (open is the toolbar action entry point). |
| setLayer(layerId) | Switches the active layer (null/"" clears). |
| refresh() | Reloads the current layer's features. |
| sortByField(field) | Cycles the sort for a field (none → asc → desc → none). |
| setSelection(ids, add?) | Selects rows by id (replace, or append when add). |
| getSelectedIds() | Returns the selected feature ids. |
| clearSelection() | Clears the selection. |
| zoomToSelection() | Fits the map to the selected features. |
| highlightSelection(active) | Toggles the map highlight overlay. |
| exportSelection(format?, options?) | Exports the selected rows (default geojson). |
| exportLayer(format?, options?) | Exports all rows of the active layer. |
format: "geojson" \| "csv" \| "kml" \| "gpx" \| "excel". options: { csvSeparator?: "," \| ";", csvIncludeGeometry?: boolean }.
Events
Dispatched on document, and replicated on the map when it exposes fire — subscribe to
one of the two, not both. The payloads are typed by GeoLeafEventMap in @geoleaf/core:
| Event | Detail |
| ---------------------------------- | ---------------------------------------- |
| geoleaf:table:opened | {} |
| geoleaf:table:closed | {} |
| geoleaf:table:layerChanged | { layerId } |
| geoleaf:table:sortChanged | { field, direction } |
| geoleaf:table:selectionChanged | { layerId, selectedIds } |
| geoleaf:table:zoomToSelection | { layerId, selectedIds } |
| geoleaf:table:highlightSelection | { layerId, selectedIds, active } |
| geoleaf:table:exportSelection | { layerId, format, selectedIds, rows } |
| geoleaf:table:exportLayer | { layerId, format, count } |
| geoleaf:table:rowActivated | { layerId, featureId, properties } |
Row activation
geoleaf:table:rowActivated says that the user activated a row: a double-click on it, or
Enter while the row has the focus (every row is a Tab stop). The checkbox cell and
the sort header never activate. The table opens nothing itself — what an activation means is
the subscriber's decision.
document.addEventListener("geoleaf:table:rowActivated", (e) => {
const { layerId, featureId, properties } = e.detail;
console.log("activated:", layerId, featureId, properties);
});featureIdis the row's identifier, the oneselectionChangedlists inselectedIds. It is always a string, and a synthetic one (__gl_row_<n>) when the feature carries no identifier of its own — read the record's key offpropertiesin that case.propertiesare the feature's complete properties, not only the displayed columns, as a shallow copy. The payload holds no DOM node.- A double-click leaves the selection as it was. Its first click toggles the selection
like any click — no click is delayed — and the activation restores the selection of before
that click: a
selectionChangedsubscriber sees two emissions around a double-click, the second carrying the initial selection. Enter emits none. - A row re-created by the virtual scroll loses the focus: past the virtual threshold, the focused row does not survive a scroll.
Migration from core
Before the extraction (@geoleaf/core ≤ v3), the table was part of the core: GeoLeaf.Table was provided by the core and configured via the root tableConfig key plus the ui.showTable flag. Now the core no longer ships the table — this plugin does.
There is no compatibility shim. To migrate:
- Move the global
tableConfigblock out of the profile root intoconfig/plugins/table.json. - Declare it in the profile manifest under
Files.modules.table. - Replace
ui.showTablewithmodules.table.showButton. - Load this plugin's script on the page, after
@geoleaf/core.
| Before (core) | After (plugin) |
| -------------------------------------------- | ----------------------------------------------------- |
| tableConfig at the profile root | modules.table (config/plugins/table.json) |
| ui.showTable: true | modules.table.showButton: true |
| GeoLeaf.Config.get("tableConfig") → object | GeoLeaf.Config.get("tableConfig") → undefined |
| GeoLeaf.Table present with the core | requires this plugin's script after geoleaf.esm.js |
The per-layer binding is kept. The
layer.config.table.*block (columns, sort, title) stays on each GeoJSON layer —layer-config.schema.jsonis unchanged. Only the global config (tableConfig+ui.showTable) moves tomodules.table.
Limitations
- The panel mounts on the map returned by
GeoLeaf.Core.getMap()— one map at a time (no per-mapIdscoping yet). pageSizeandvirtualScrollingare deprecated and inert. They are kept in the publishedTableConfigtype, marked, rather than removed: deleting a member of a published type breaks the compilation of an integrator who wrote it, whereas deprecating it tells them. Neither has a replacement — there is no pagination at all, and virtualisation is decided by a row-count threshold — so neither is a deprecation in the sense ofVERSIONING_POLICY.md.- The Excel writer is write-only (no read, no formulas) — sufficient for tabular export.
Bundle budget
| Part | Size (gzip) | | ------------- | ------------------------- | | Plugin bundle | budget 50 KB (warn 38 KB) |
No third-party runtime dependency is bundled (the OOXML writer is inlined and lazily imported). @geoleaf/core is accessed via globalThis.GeoLeaf; maplibre-gl is a peer dependency provided by the host page.
MIT License
Copyright © 2026 Mattieu Pottier. See LICENSE for details.
