mirror of
https://github.com/maputnik/editor.git
synced 2026-09-02 02:17:26 +00:00
380 lines
11 KiB
React
380 lines
11 KiB
React
import PropTypes from "prop-types";
|
|
import React from "react";
|
|
import { Button, Menu, MenuItem, Wrapper } from "react-aria-menubutton";
|
|
|
|
import { Accordion } from "react-accessible-accordion";
|
|
import FieldComment from "./FieldComment";
|
|
import FieldId from "./FieldId";
|
|
import FieldJson from "./FieldJson";
|
|
import FieldMaxZoom from "./FieldMaxZoom";
|
|
import FieldMinZoom from "./FieldMinZoom";
|
|
import FieldSource from "./FieldSource";
|
|
import FieldSourceLayer from "./FieldSourceLayer";
|
|
import FieldType from "./FieldType";
|
|
import FilterEditor from "./FilterEditor";
|
|
import LayerEditorGroup from "./LayerEditorGroup";
|
|
import PropertyGroup from "./PropertyGroup";
|
|
|
|
import { MdMoreVert } from "react-icons/md";
|
|
|
|
import layout from "../config/layout.json";
|
|
import { changeProperty, changeType } from "../libs/layer";
|
|
import { formatLayerId } from "../util/format";
|
|
|
|
function getLayoutForType(type) {
|
|
return layout[type] ? layout[type] : layout.invalid;
|
|
}
|
|
|
|
function layoutGroups(layerType) {
|
|
const layerGroup = {
|
|
title: "Layer",
|
|
type: "layer",
|
|
};
|
|
const filterGroup = {
|
|
title: "Filter",
|
|
type: "filter",
|
|
};
|
|
const editorGroup = {
|
|
title: "JSON Editor",
|
|
type: "jsoneditor",
|
|
};
|
|
return [layerGroup, filterGroup]
|
|
.concat(getLayoutForType(layerType).groups)
|
|
.concat([editorGroup]);
|
|
}
|
|
|
|
/** Layer editor supporting multiple types of layers. */
|
|
export default class LayerEditor extends React.Component {
|
|
static propTypes = {
|
|
layer: PropTypes.object.isRequired,
|
|
sources: PropTypes.object,
|
|
vectorLayers: PropTypes.object,
|
|
spec: PropTypes.object.isRequired,
|
|
onLayerChanged: PropTypes.func,
|
|
onLayerIdChange: PropTypes.func,
|
|
onMoveLayer: PropTypes.func,
|
|
onLayerDestroy: PropTypes.func,
|
|
onLayerCopy: PropTypes.func,
|
|
onLayerVisibilityToggle: PropTypes.func,
|
|
isFirstLayer: PropTypes.bool,
|
|
isLastLayer: PropTypes.bool,
|
|
layerIndex: PropTypes.number,
|
|
errors: PropTypes.array,
|
|
};
|
|
|
|
static defaultProps = {
|
|
onLayerChanged: () => {},
|
|
onLayerIdChange: () => {},
|
|
onLayerDestroyed: () => {},
|
|
};
|
|
|
|
static childContextTypes = {
|
|
reactIconBase: PropTypes.object,
|
|
};
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
//TODO: Clean this up and refactor into function
|
|
const editorGroups = {};
|
|
layoutGroups(this.props.layer.type).forEach((group) => {
|
|
editorGroups[group.title] = true;
|
|
});
|
|
|
|
this.state = { editorGroups };
|
|
}
|
|
|
|
static getDerivedStateFromProps(props, state) {
|
|
const additionalGroups = { ...state.editorGroups };
|
|
|
|
getLayoutForType(props.layer.type).groups.forEach((group) => {
|
|
if (!(group.title in additionalGroups)) {
|
|
additionalGroups[group.title] = true;
|
|
}
|
|
});
|
|
|
|
return {
|
|
editorGroups: additionalGroups,
|
|
};
|
|
}
|
|
|
|
getChildContext() {
|
|
return {
|
|
reactIconBase: {
|
|
size: 14,
|
|
color: "#8e8e8e",
|
|
},
|
|
};
|
|
}
|
|
|
|
changeProperty(group, property, newValue) {
|
|
this.props.onLayerChanged(
|
|
this.props.layerIndex,
|
|
changeProperty(this.props.layer, group, property, newValue)
|
|
);
|
|
}
|
|
|
|
onGroupToggle(groupTitle, active) {
|
|
const changedActiveGroups = {
|
|
...this.state.editorGroups,
|
|
[groupTitle]: active,
|
|
};
|
|
this.setState({
|
|
editorGroups: changedActiveGroups,
|
|
});
|
|
}
|
|
|
|
renderGroupType(type, fields) {
|
|
let comment = "";
|
|
if (this.props.layer.metadata) {
|
|
comment = this.props.layer.metadata["maputnik:comment"];
|
|
}
|
|
const { errors, layerIndex } = this.props;
|
|
|
|
const errorData = {};
|
|
errors.forEach((error) => {
|
|
if (
|
|
error.parsed &&
|
|
error.parsed.type === "layer" &&
|
|
error.parsed.data.index == layerIndex
|
|
) {
|
|
errorData[error.parsed.data.key] = {
|
|
message: error.parsed.data.message,
|
|
};
|
|
}
|
|
});
|
|
|
|
let sourceLayerIds;
|
|
if (this.props.sources.hasOwnProperty(this.props.layer.source)) {
|
|
sourceLayerIds = this.props.sources[this.props.layer.source].layers;
|
|
}
|
|
|
|
switch (type) {
|
|
case "layer":
|
|
return (
|
|
<div>
|
|
<FieldId
|
|
value={this.props.layer.id}
|
|
wdKey="layer-editor.layer-id"
|
|
error={errorData.id}
|
|
onChange={(newId) =>
|
|
this.props.onLayerIdChange(
|
|
this.props.layerIndex,
|
|
this.props.layer.id,
|
|
newId
|
|
)
|
|
}
|
|
/>
|
|
<FieldType
|
|
disabled={true}
|
|
error={errorData.type}
|
|
value={this.props.layer.type}
|
|
onChange={(newType) =>
|
|
this.props.onLayerChanged(
|
|
this.props.layerIndex,
|
|
changeType(this.props.layer, newType)
|
|
)
|
|
}
|
|
/>
|
|
{this.props.layer.type !== "background" && (
|
|
<FieldSource
|
|
error={errorData.source}
|
|
sourceIds={Object.keys(this.props.sources)}
|
|
value={this.props.layer.source}
|
|
onChange={(v) => this.changeProperty(null, "source", v)}
|
|
/>
|
|
)}
|
|
{["background", "raster", "hillshade", "heatmap"].indexOf(
|
|
this.props.layer.type
|
|
) < 0 && (
|
|
<FieldSourceLayer
|
|
error={errorData["source-layer"]}
|
|
sourceLayerIds={sourceLayerIds}
|
|
value={this.props.layer["source-layer"]}
|
|
onChange={(v) => this.changeProperty(null, "source-layer", v)}
|
|
/>
|
|
)}
|
|
<FieldMinZoom
|
|
error={errorData.minzoom}
|
|
value={this.props.layer.minzoom}
|
|
onChange={(v) => this.changeProperty(null, "minzoom", v)}
|
|
/>
|
|
<FieldMaxZoom
|
|
error={errorData.maxzoom}
|
|
value={this.props.layer.maxzoom}
|
|
onChange={(v) => this.changeProperty(null, "maxzoom", v)}
|
|
/>
|
|
<FieldComment
|
|
error={errorData.comment}
|
|
value={comment}
|
|
onChange={(v) =>
|
|
this.changeProperty(
|
|
"metadata",
|
|
"maputnik:comment",
|
|
v == "" ? undefined : v
|
|
)
|
|
}
|
|
/>
|
|
</div>
|
|
);
|
|
case "filter":
|
|
return (
|
|
<div>
|
|
<div className="maputnik-filter-editor-wrapper">
|
|
<FilterEditor
|
|
errors={errorData}
|
|
filter={this.props.layer.filter}
|
|
properties={
|
|
this.props.vectorLayers[this.props.layer["source-layer"]]
|
|
}
|
|
onChange={(f) => this.changeProperty(null, "filter", f)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
case "properties":
|
|
return (
|
|
<PropertyGroup
|
|
errors={errorData}
|
|
layer={this.props.layer}
|
|
groupFields={fields}
|
|
spec={this.props.spec}
|
|
onChange={this.changeProperty.bind(this)}
|
|
/>
|
|
);
|
|
case "jsoneditor":
|
|
return (
|
|
<FieldJson
|
|
layer={this.props.layer}
|
|
onChange={(layer) => {
|
|
this.props.onLayerChanged(this.props.layerIndex, layer);
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
|
|
moveLayer(offset) {
|
|
this.props.onMoveLayer({
|
|
oldIndex: this.props.layerIndex,
|
|
newIndex: this.props.layerIndex + offset,
|
|
});
|
|
}
|
|
|
|
render() {
|
|
const groupIds = [];
|
|
const layerType = this.props.layer.type;
|
|
const groups = layoutGroups(layerType)
|
|
.filter((group) => {
|
|
return !(layerType === "background" && group.type === "source");
|
|
})
|
|
.map((group) => {
|
|
const groupId = group.title.replace(/ /g, "_");
|
|
groupIds.push(groupId);
|
|
return (
|
|
<LayerEditorGroup
|
|
data-wd-key={group.title}
|
|
id={groupId}
|
|
key={group.title}
|
|
title={group.title}
|
|
isActive={this.state.editorGroups[group.title]}
|
|
onActiveToggle={this.onGroupToggle.bind(this, group.title)}
|
|
>
|
|
{this.renderGroupType(group.type, group.fields)}
|
|
</LayerEditorGroup>
|
|
);
|
|
});
|
|
|
|
const layout = this.props.layer.layout || {};
|
|
|
|
const items = {
|
|
delete: {
|
|
text: "Delete",
|
|
handler: () => this.props.onLayerDestroy(this.props.layerIndex),
|
|
},
|
|
duplicate: {
|
|
text: "Duplicate",
|
|
handler: () => this.props.onLayerCopy(this.props.layerIndex),
|
|
},
|
|
hide: {
|
|
text: layout.visibility === "none" ? "Show" : "Hide",
|
|
handler: () =>
|
|
this.props.onLayerVisibilityToggle(this.props.layerIndex),
|
|
},
|
|
moveLayerUp: {
|
|
text: "Move layer up",
|
|
// Not actually used...
|
|
disabled: this.props.isFirstLayer,
|
|
handler: () => this.moveLayer(-1),
|
|
},
|
|
moveLayerDown: {
|
|
text: "Move layer down",
|
|
// Not actually used...
|
|
disabled: this.props.isLastLayer,
|
|
handler: () => this.moveLayer(+1),
|
|
},
|
|
};
|
|
|
|
function handleSelection(id, event) {
|
|
event.stopPropagation;
|
|
items[id].handler();
|
|
}
|
|
|
|
return (
|
|
<section
|
|
className="maputnik-layer-editor"
|
|
role="main"
|
|
aria-label="Layer editor"
|
|
>
|
|
<header>
|
|
<div className="layer-header">
|
|
<h2 className="layer-header__title">
|
|
Layer: {formatLayerId(this.props.layer.id)}
|
|
</h2>
|
|
<div className="layer-header__info">
|
|
<Wrapper
|
|
className="more-menu"
|
|
onSelection={handleSelection}
|
|
closeOnSelection={false}
|
|
>
|
|
<Button
|
|
data-wd-key="skip-target-layer-editor"
|
|
id="skip-target-layer-editor"
|
|
className="more-menu__button"
|
|
title="Layer options"
|
|
>
|
|
<MdMoreVert className="more-menu__button__svg" />
|
|
</Button>
|
|
<Menu>
|
|
<ul className="more-menu__menu">
|
|
{Object.keys(items).map((id, idx) => {
|
|
const item = items[id];
|
|
return (
|
|
<li key={id}>
|
|
<MenuItem
|
|
value={id}
|
|
className="more-menu__menu__item"
|
|
>
|
|
{item.text}
|
|
</MenuItem>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</Menu>
|
|
</Wrapper>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<Accordion
|
|
allowMultipleExpanded={true}
|
|
allowZeroExpanded={true}
|
|
preExpanded={groupIds}
|
|
>
|
|
{groups}
|
|
</Accordion>
|
|
</section>
|
|
);
|
|
}
|
|
}
|