Add code editor for maputnik (#1426)

## Launch Checklist

This PR adds the ability to look at the entire style and edit it in a
code editor that supports syntax highlight, errors, search and more.

- Resolves #820

CC: @Kanahiro as I know you did something similar, probably has better
performance...

After:
<img width="1920" height="937" alt="image"
src="https://github.com/user-attachments/assets/f925cf92-2623-4390-8f75-14d7f6a79171"
/>


 - [x] Briefly describe the changes in this PR.
 - [x] Link to related issues.
- [x] Include before/after visuals or gifs if this PR includes visual
changes.
 - [x] Write tests for all new functionality.
 - [x] Add an entry to `CHANGELOG.md` under the `## main` section.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
This commit is contained in:
Harel M
2025-10-05 16:38:03 +03:00
committed by GitHub
parent 454d8d8b10
commit 39d63ec7b1
15 changed files with 169 additions and 41 deletions

View File

@@ -13,6 +13,7 @@ import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import MapMaplibreGl from "./MapMaplibreGl";
import MapOpenLayers from "./MapOpenLayers";
import CodeEditor from "./CodeEditor";
import LayerList from "./LayerList";
import LayerEditor from "./LayerEditor";
import AppToolbar, { type MapState } from "./AppToolbar";
@@ -116,6 +117,7 @@ type AppState = {
export: boolean
debug: boolean
globalState: boolean
codeEditor: boolean
}
fileHandle: FileSystemFileHandle | null
};
@@ -155,6 +157,7 @@ export default class App extends React.Component<any, AppState> {
export: false,
debug: false,
globalState: false,
codeEditor: false
},
maplibreGlDebugOptions: {
showTileBoundaries: false,
@@ -856,9 +859,15 @@ export default class App extends React.Component<any, AppState> {
onStyleChanged={this.onStyleChanged}
onStyleOpen={this.onStyleChanged}
onSetMapState={this.setMapState}
onToggleModal={this.toggleModal.bind(this)}
onToggleModal={(modal: keyof AppState["isOpen"]) => this.toggleModal(modal)}
/>;
const codeEditor = this.state.isOpen.codeEditor ? <CodeEditor
value={this.state.mapStyle}
onChange={(style) => this.onStyleChanged(style)}
onClose={() => this.setModal("codeEditor", false)}
/> : undefined;
const layerList = <LayerList
onMoveLayer={this.onMoveLayer}
onLayerDestroy={this.onLayerDestroy}
@@ -954,6 +963,7 @@ export default class App extends React.Component<any, AppState> {
toolbar={toolbar}
layerList={layerList}
layerEditor={layerEditor}
codeEditor={codeEditor}
map={this.mapRenderer()}
bottom={bottomPanel}
modals={modals}

View File

@@ -7,6 +7,7 @@ type AppLayoutInternalProps = {
toolbar: React.ReactElement
layerList: React.ReactElement
layerEditor?: React.ReactElement
codeEditor?: React.ReactElement
map: React.ReactElement
bottom?: React.ReactElement
modals?: React.ReactNode
@@ -21,14 +22,22 @@ class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
<div className="maputnik-layout">
{this.props.toolbar}
<div className="maputnik-layout-main">
<div className="maputnik-layout-list">
{this.props.layerList}
</div>
<div className="maputnik-layout-drawer">
{this.props.codeEditor && <div className="maputnik-layout-code-editor">
<ScrollContainer>
{this.props.layerEditor}
{this.props.codeEditor}
</ScrollContainer>
</div>
}
{!this.props.codeEditor && <>
<div className="maputnik-layout-list">
{this.props.layerList}
</div>
<div className="maputnik-layout-drawer">
<ScrollContainer>
{this.props.layerEditor}
</ScrollContainer>
</div>
</>}
{this.props.map}
</div>
{this.props.bottom && <div className="maputnik-layout-bottom">

View File

@@ -10,7 +10,8 @@ import {
MdFindInPage,
MdLanguage,
MdSave,
MdPublic
MdPublic,
MdCode
} from "react-icons/md";
import pkgJson from "../../package.json";
//@ts-ignore
@@ -23,6 +24,7 @@ import type { OnStyleChangedCallback } from "../libs/definitions";
const browser = detect();
const colorAccessibilityFiltersEnabled = ["chrome", "firefox"].indexOf(browser!.name) > -1;
export type ModalTypes = "settings" | "sources" | "open" | "shortcuts" | "export" | "debug" | "globalState" | "codeEditor";
type IconTextProps = {
children?: React.ReactNode
@@ -39,7 +41,6 @@ type ToolbarLinkProps = {
className?: string
children?: React.ReactNode
href?: string
onToggleModal?(...args: unknown[]): unknown
};
class ToolbarLink extends React.Component<ToolbarLinkProps> {
@@ -101,7 +102,7 @@ type AppToolbarInternalProps = {
// A dict of source id's and the available source layers
sources: object
children?: React.ReactNode
onToggleModal(...args: unknown[]): unknown
onToggleModal(modal: ModalTypes): void
onSetMapState(mapState: MapState): unknown
mapState?: MapState
renderer?: string
@@ -221,23 +222,27 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
</a>
</div>
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, "open")}>
<ToolbarAction wdKey="nav:open" onClick={() => this.props.onToggleModal("open")}>
<MdOpenInBrowser />
<IconText>{t("Open")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, "export")}>
<ToolbarAction wdKey="nav:export" onClick={() => this.props.onToggleModal("export")}>
<MdSave />
<IconText>{t("Save")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, "sources")}>
<ToolbarAction wdKey="nav:code-editor" onClick={() => this.props.onToggleModal("codeEditor")}>
<MdCode />
<IconText>{t("Code Editor")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:sources" onClick={() => this.props.onToggleModal("sources")}>
<MdLayers />
<IconText>{t("Data Sources")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, "settings")}>
<ToolbarAction wdKey="nav:settings" onClick={() => this.props.onToggleModal("settings")}>
<MdSettings />
<IconText>{t("Style Settings")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:global-state" onClick={this.props.onToggleModal.bind(this, "globalState")}>
<ToolbarAction wdKey="nav:global-state" onClick={() => this.props.onToggleModal("globalState")}>
<MdPublic />
<IconText>{t("Global State")}</IconText>
</ToolbarAction>

View File

@@ -0,0 +1,28 @@
import InputJson from "./InputJson";
import React from "react";
import { withTranslation, type WithTranslation } from "react-i18next";
import { type StyleSpecification } from "maplibre-gl";
import { type StyleSpecificationWithId } from "../libs/definitions";
export type CodeEditorProps = {
value: StyleSpecification;
onChange: (value: StyleSpecificationWithId) => void;
onClose: () => void;
} & WithTranslation;
const CodeEditorInternal: React.FC<CodeEditorProps> = (props) => {
return <>
<button className="maputnik-button" onClick={props.onClose} aria-label={props.t("Close")} style={{ position: "sticky", top: "0", zIndex: 1 }}>{props.t("Click to close the editor")}</button>
<InputJson
lintType="style"
value={props.value}
onChange={props.onChange}
className={"maputnik-code-editor"}
/>;
</>;
};
const CodeEditor = withTranslation()(CodeEditorInternal);
export default CodeEditor;