mirror of
https://github.com/maputnik/editor.git
synced 2026-02-09 22:20:03 +00:00
Merge branch 'main' into main
This commit is contained in:
@@ -24,7 +24,6 @@ import ModalExport from './ModalExport'
|
||||
import ModalSources from './ModalSources'
|
||||
import ModalOpen from './ModalOpen'
|
||||
import ModalShortcuts from './ModalShortcuts'
|
||||
import ModalSurvey from './ModalSurvey'
|
||||
import ModalDebug from './ModalDebug'
|
||||
|
||||
import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata'
|
||||
@@ -128,7 +127,6 @@ type AppState = {
|
||||
open: boolean
|
||||
shortcuts: boolean
|
||||
export: boolean
|
||||
survey: boolean
|
||||
debug: boolean
|
||||
}
|
||||
}
|
||||
@@ -137,7 +135,6 @@ export default class App extends React.Component<any, AppState> {
|
||||
revisionStore: RevisionStore;
|
||||
styleStore: StyleStore | ApiStyleStore;
|
||||
layerWatcher: LayerWatcher;
|
||||
shortcutEl: ModalShortcuts | null = null;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
@@ -277,7 +274,6 @@ export default class App extends React.Component<any, AppState> {
|
||||
shortcuts: false,
|
||||
export: false,
|
||||
// TODO: Disabled for now, this should be opened on the Nth visit to the editor
|
||||
survey: false,
|
||||
debug: false,
|
||||
},
|
||||
maplibreGlDebugOptions: {
|
||||
@@ -839,10 +835,6 @@ export default class App extends React.Component<any, AppState> {
|
||||
}
|
||||
|
||||
setModal(modalName: keyof AppState["isOpen"], value: boolean) {
|
||||
if(modalName === 'survey' && value === false) {
|
||||
localStorage.setItem('survey', '');
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isOpen: {
|
||||
...this.state.isOpen,
|
||||
@@ -942,7 +934,6 @@ export default class App extends React.Component<any, AppState> {
|
||||
mapView={this.state.mapView}
|
||||
/>
|
||||
<ModalShortcuts
|
||||
ref={(el) => this.shortcutEl = el}
|
||||
isOpen={this.state.isOpen.shortcuts}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
|
||||
/>
|
||||
@@ -970,10 +961,6 @@ export default class App extends React.Component<any, AppState> {
|
||||
isOpen={this.state.isOpen.sources}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'sources')}
|
||||
/>
|
||||
<ModalSurvey
|
||||
isOpen={this.state.isOpen.survey}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'survey')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
return <AppLayout
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ScrollContainer from './ScrollContainer'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type AppLayoutProps = {
|
||||
type AppLayoutInternalProps = {
|
||||
toolbar: React.ReactElement
|
||||
layerList: React.ReactElement
|
||||
layerEditor?: React.ReactElement
|
||||
map: React.ReactElement
|
||||
bottom?: React.ReactElement
|
||||
modals?: React.ReactNode
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
class AppLayout extends React.Component<AppLayoutProps> {
|
||||
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
|
||||
static childContextTypes = {
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
@@ -23,17 +24,21 @@ class AppLayout extends React.Component<AppLayoutProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
document.body.dir = this.props.i18n.dir();
|
||||
|
||||
return <div className="maputnik-layout">
|
||||
{this.props.toolbar}
|
||||
<div className="maputnik-layout-list">
|
||||
{this.props.layerList}
|
||||
<div className="maputnik-layout-main">
|
||||
<div className="maputnik-layout-list">
|
||||
{this.props.layerList}
|
||||
</div>
|
||||
<div className="maputnik-layout-drawer">
|
||||
<ScrollContainer>
|
||||
{this.props.layerEditor}
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
{this.props.map}
|
||||
</div>
|
||||
<div className="maputnik-layout-drawer">
|
||||
<ScrollContainer>
|
||||
{this.props.layerEditor}
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
{this.props.map}
|
||||
{this.props.bottom && <div className="maputnik-layout-bottom">
|
||||
{this.props.bottom}
|
||||
</div>
|
||||
@@ -43,4 +48,5 @@ class AppLayout extends React.Component<AppLayoutProps> {
|
||||
}
|
||||
}
|
||||
|
||||
export default AppLayout
|
||||
const AppLayout = withTranslation()(AppLayoutInternal);
|
||||
export default AppLayout;
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import React from 'react'
|
||||
import {formatLayerId} from '../libs/format';
|
||||
import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
|
||||
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type AppMessagePanelProps = {
|
||||
type AppMessagePanelInternalProps = {
|
||||
errors?: unknown[]
|
||||
infos?: string[]
|
||||
mapStyle?: StyleSpecification
|
||||
onLayerSelect?(...args: unknown[]): unknown
|
||||
currentLayer?: LayerSpecification
|
||||
selectedLayerIndex?: number
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class AppMessagePanel extends React.Component<AppMessagePanelProps> {
|
||||
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
|
||||
static defaultProps = {
|
||||
onLayerSelect: () => {},
|
||||
}
|
||||
|
||||
render() {
|
||||
const {selectedLayerIndex} = this.props;
|
||||
const {t, selectedLayerIndex} = this.props;
|
||||
const errors = this.props.errors?.map((error: any, idx) => {
|
||||
let content;
|
||||
if (error.parsed && error.parsed.type === "layer") {
|
||||
@@ -25,7 +26,9 @@ export default class AppMessagePanel extends React.Component<AppMessagePanelProp
|
||||
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
|
||||
content = (
|
||||
<>
|
||||
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
||||
<Trans t={t}>
|
||||
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
||||
</Trans>
|
||||
{selectedLayerIndex !== parsed.data.index &&
|
||||
<>
|
||||
—
|
||||
@@ -33,7 +36,7 @@ export default class AppMessagePanel extends React.Component<AppMessagePanelProp
|
||||
className="maputnik-message-panel__switch-button"
|
||||
onClick={() => this.props.onLayerSelect!(parsed.data.index)}
|
||||
>
|
||||
switch to layer
|
||||
{t("switch to layer")}
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
@@ -59,3 +62,5 @@ export default class AppMessagePanel extends React.Component<AppMessagePanelProp
|
||||
}
|
||||
}
|
||||
|
||||
const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
|
||||
export default AppMessagePanel;
|
||||
|
||||
@@ -2,10 +2,12 @@ import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import {detect} from 'detect-browser';
|
||||
|
||||
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage} from 'react-icons/md'
|
||||
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdLanguage} from 'react-icons/md'
|
||||
import pkgJson from '../../package.json'
|
||||
//@ts-ignore
|
||||
import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline'
|
||||
import { withTranslation, WithTranslation } from 'react-i18next';
|
||||
import { supportedLanguages } from '../i18n';
|
||||
|
||||
// This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of.
|
||||
const browser = detect();
|
||||
@@ -80,7 +82,7 @@ class ToolbarAction extends React.Component<ToolbarActionProps> {
|
||||
|
||||
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
|
||||
|
||||
type AppToolbarProps = {
|
||||
type AppToolbarInternalProps = {
|
||||
mapStyle: object
|
||||
inspectModeEnabled: boolean
|
||||
onStyleChanged(...args: unknown[]): unknown
|
||||
@@ -93,9 +95,9 @@ type AppToolbarProps = {
|
||||
onSetMapState(mapState: MapState): unknown
|
||||
mapState?: MapState
|
||||
renderer?: string
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class AppToolbar extends React.Component<AppToolbarProps> {
|
||||
class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
state = {
|
||||
isOpen: {
|
||||
settings: false,
|
||||
@@ -110,6 +112,10 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
|
||||
this.props.onSetMapState(val);
|
||||
}
|
||||
|
||||
handleLanguageChange(val: string) {
|
||||
this.props.i18n.changeLanguage(val);
|
||||
}
|
||||
|
||||
onSkip = (target: string) => {
|
||||
if (target === "map") {
|
||||
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
|
||||
@@ -121,40 +127,41 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const views = [
|
||||
{
|
||||
id: "map",
|
||||
group: "general",
|
||||
title: "Map",
|
||||
title: t("Map"),
|
||||
},
|
||||
{
|
||||
id: "inspect",
|
||||
group: "general",
|
||||
title: "Inspect",
|
||||
title: t("Inspect"),
|
||||
disabled: this.props.renderer === 'ol',
|
||||
},
|
||||
{
|
||||
id: "filter-deuteranopia",
|
||||
group: "color-accessibility",
|
||||
title: "Deuteranopia filter",
|
||||
title: t("Deuteranopia filter"),
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-protanopia",
|
||||
group: "color-accessibility",
|
||||
title: "Protanopia filter",
|
||||
title: t("Protanopia filter"),
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-tritanopia",
|
||||
group: "color-accessibility",
|
||||
title: "Tritanopia filter",
|
||||
title: t("Tritanopia filter"),
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
{
|
||||
id: "filter-achromatopsia",
|
||||
group: "color-accessibility",
|
||||
title: "Achromatopsia filter",
|
||||
title: t("Achromatopsia filter"),
|
||||
disabled: !colorAccessibilityFiltersEnabled,
|
||||
},
|
||||
];
|
||||
@@ -174,21 +181,21 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={_e => this.onSkip("layer-list")}
|
||||
>
|
||||
Layers list
|
||||
{t("Layers list")}
|
||||
</button>
|
||||
<button
|
||||
data-wd-key="root:skip:layer-editor"
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={_e => this.onSkip("layer-editor")}
|
||||
>
|
||||
Layer editor
|
||||
{t("Layer editor")}
|
||||
</button>
|
||||
<button
|
||||
data-wd-key="root:skip:map-view"
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={_e => this.onSkip("map")}
|
||||
>
|
||||
Map view
|
||||
{t("Map view")}
|
||||
</button>
|
||||
<a
|
||||
className="maputnik-toolbar-logo"
|
||||
@@ -196,7 +203,7 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
|
||||
rel="noreferrer noopener"
|
||||
href="https://github.com/maplibre/maputnik"
|
||||
>
|
||||
<img src={maputnikLogo} alt="Maputnik on GitHub" />
|
||||
<img src={maputnikLogo} alt={t("Maputnik on GitHub")} />
|
||||
<h1>
|
||||
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
|
||||
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
|
||||
@@ -206,24 +213,24 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
|
||||
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
|
||||
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
|
||||
<MdOpenInBrowser />
|
||||
<IconText>Open</IconText>
|
||||
<IconText>{t("Open")}</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
|
||||
<MdFileDownload />
|
||||
<IconText>Export</IconText>
|
||||
<IconText>{t("Export")}</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
|
||||
<MdLayers />
|
||||
<IconText>Data Sources</IconText>
|
||||
<IconText>{t("Data Sources")}</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
|
||||
<MdSettings />
|
||||
<IconText>Style Settings</IconText>
|
||||
<IconText>{t("Style Settings")}</IconText>
|
||||
</ToolbarAction>
|
||||
|
||||
<ToolbarSelect wdKey="nav:inspect">
|
||||
<MdFindInPage />
|
||||
<label>View
|
||||
<label>{t("View")}
|
||||
<select
|
||||
className="maputnik-select"
|
||||
data-wd-key="maputnik-select"
|
||||
@@ -237,7 +244,7 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
<optgroup label="Color accessibility">
|
||||
<optgroup label={t("Color accessibility")}>
|
||||
{views.filter(v => v.group === "color-accessibility").map((item) => {
|
||||
return (
|
||||
<option key={item.id} value={item.id} disabled={item.disabled}>
|
||||
@@ -250,12 +257,35 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
|
||||
</label>
|
||||
</ToolbarSelect>
|
||||
|
||||
<ToolbarSelect wdKey="nav:language">
|
||||
<MdLanguage />
|
||||
<label>{t("Language")}
|
||||
<select
|
||||
className="maputnik-select"
|
||||
data-wd-key="maputnik-lang-select"
|
||||
onChange={(e) => this.handleLanguageChange(e.target.value)}
|
||||
value={this.props.i18n.language}
|
||||
>
|
||||
{Object.entries(supportedLanguages).map(([code, name]) => {
|
||||
return (
|
||||
<option key={code} value={code}>
|
||||
{name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</label>
|
||||
</ToolbarSelect>
|
||||
|
||||
<ToolbarLink href={"https://github.com/maplibre/maputnik/wiki"}>
|
||||
<MdHelpOutline />
|
||||
<IconText>Help</IconText>
|
||||
<IconText>{t("Help")}</IconText>
|
||||
</ToolbarLink>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
|
||||
const AppToolbar = withTranslation()(AppToolbarInternal);
|
||||
export default AppToolbar;
|
||||
|
||||
@@ -2,21 +2,23 @@ import React from 'react'
|
||||
|
||||
import Block from './Block'
|
||||
import InputString from './InputString'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FieldCommentProps = {
|
||||
type FieldCommentInternalProps = {
|
||||
value?: string
|
||||
onChange(value: string | undefined): unknown
|
||||
error: {message: string}
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class FieldComment extends React.Component<FieldCommentProps> {
|
||||
class FieldCommentInternal extends React.Component<FieldCommentInternalProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const fieldSpec = {
|
||||
doc: "Comments for the current layer. This is non-standard and not in the spec."
|
||||
doc: t("Comments for the current layer. This is non-standard and not in the spec."),
|
||||
};
|
||||
|
||||
return <Block
|
||||
label={"Comments"}
|
||||
label={t("Comments")}
|
||||
fieldSpec={fieldSpec}
|
||||
data-wd-key="layer-comment"
|
||||
error={this.props.error}
|
||||
@@ -25,9 +27,12 @@ export default class FieldComment extends React.Component<FieldCommentProps> {
|
||||
multi={true}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
default="Comment..."
|
||||
default={t("Comment...")}
|
||||
data-wd-key="layer-comment.input"
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldComment = withTranslation()(FieldCommentInternal);
|
||||
export default FieldComment;
|
||||
|
||||
@@ -128,7 +128,7 @@ export default class FieldFunction extends React.Component<FieldFunctionProps, F
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: FieldFunctionProps, state: FieldFunctionState) {
|
||||
static getDerivedStateFromProps(props: Readonly<FieldFunctionProps>, state: FieldFunctionState) {
|
||||
// Because otherwise when editing values we end up accidentally changing field type.
|
||||
if (state.isEditing) {
|
||||
return {};
|
||||
|
||||
@@ -13,7 +13,8 @@ type FieldIdProps = {
|
||||
|
||||
export default class FieldId extends React.Component<FieldIdProps> {
|
||||
render() {
|
||||
return <Block label={"ID"} fieldSpec={latest.layer.id}
|
||||
return <Block label="ID" fieldSpec={latest.layer.id}
|
||||
|
||||
data-wd-key={this.props.wdKey}
|
||||
error={this.props.error}
|
||||
>
|
||||
|
||||
@@ -3,16 +3,18 @@ import React from 'react'
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import Block from './Block'
|
||||
import InputNumber from './InputNumber'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FieldMaxZoomProps = {
|
||||
type FieldMaxZoomInternalProps = {
|
||||
value?: number
|
||||
onChange(value: number | undefined): unknown
|
||||
error?: {message: string}
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class FieldMaxZoom extends React.Component<FieldMaxZoomProps> {
|
||||
class FieldMaxZoomInternal extends React.Component<FieldMaxZoomInternalProps> {
|
||||
render() {
|
||||
return <Block label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
|
||||
const t = this.props.t;
|
||||
return <Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom}
|
||||
error={this.props.error}
|
||||
data-wd-key="max-zoom"
|
||||
>
|
||||
@@ -28,3 +30,6 @@ export default class FieldMaxZoom extends React.Component<FieldMaxZoomProps> {
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal);
|
||||
export default FieldMaxZoom;
|
||||
|
||||
@@ -3,16 +3,18 @@ import React from 'react'
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import Block from './Block'
|
||||
import InputNumber from './InputNumber'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FieldMinZoomProps = {
|
||||
type FieldMinZoomInternalProps = {
|
||||
value?: number
|
||||
onChange(...args: unknown[]): unknown
|
||||
error?: {message: string}
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class FieldMinZoom extends React.Component<FieldMinZoomProps> {
|
||||
class FieldMinZoomInternal extends React.Component<FieldMinZoomInternalProps> {
|
||||
render() {
|
||||
return <Block label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
|
||||
const t = this.props.t;
|
||||
return <Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom}
|
||||
error={this.props.error}
|
||||
data-wd-key="min-zoom"
|
||||
>
|
||||
@@ -28,3 +30,6 @@ export default class FieldMinZoom extends React.Component<FieldMinZoomProps> {
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
|
||||
export default FieldMinZoom;
|
||||
|
||||
@@ -3,24 +3,26 @@ import React from 'react'
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import Block from './Block'
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FieldSourceProps = {
|
||||
type FieldSourceInternalProps = {
|
||||
value?: string
|
||||
wdKey?: string
|
||||
onChange?(value: string| undefined): unknown
|
||||
sourceIds?: unknown[]
|
||||
error?: {message: string}
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class FieldSource extends React.Component<FieldSourceProps> {
|
||||
class FieldSourceInternal extends React.Component<FieldSourceInternalProps> {
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceIds: [],
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <Block
|
||||
label={"Source"}
|
||||
label={t("Source")}
|
||||
fieldSpec={latest.layer.source}
|
||||
error={this.props.error}
|
||||
data-wd-key={this.props.wdKey}
|
||||
@@ -33,3 +35,6 @@ export default class FieldSource extends React.Component<FieldSourceProps> {
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldSource = withTranslation()(FieldSourceInternal);
|
||||
export default FieldSource;
|
||||
|
||||
@@ -3,16 +3,17 @@ import React from 'react'
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import Block from './Block'
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FieldSourceLayerProps = {
|
||||
type FieldSourceLayerInternalProps = {
|
||||
value?: string
|
||||
onChange?(...args: unknown[]): unknown
|
||||
sourceLayerIds?: unknown[]
|
||||
isFixed?: boolean
|
||||
error?: {message: string}
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class FieldSourceLayer extends React.Component<FieldSourceLayerProps> {
|
||||
class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalProps> {
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceLayerIds: [],
|
||||
@@ -20,8 +21,9 @@ export default class FieldSourceLayer extends React.Component<FieldSourceLayerPr
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <Block
|
||||
label={"Source Layer"}
|
||||
label={t("Source Layer")}
|
||||
fieldSpec={latest.layer['source-layer']}
|
||||
data-wd-key="layer-source-layer"
|
||||
error={this.props.error}
|
||||
@@ -35,3 +37,6 @@ export default class FieldSourceLayer extends React.Component<FieldSourceLayerPr
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
|
||||
export default FieldSourceLayer;
|
||||
|
||||
@@ -4,22 +4,24 @@ import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import Block from './Block'
|
||||
import InputSelect from './InputSelect'
|
||||
import InputString from './InputString'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FieldTypeProps = {
|
||||
type FieldTypeInternalProps = {
|
||||
value: string
|
||||
wdKey?: string
|
||||
onChange(value: string): unknown
|
||||
error?: {message: string}
|
||||
disabled?: boolean
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class FieldType extends React.Component<FieldTypeProps> {
|
||||
class FieldTypeInternal extends React.Component<FieldTypeInternalProps> {
|
||||
static defaultProps = {
|
||||
disabled: false,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Block label={"Type"} fieldSpec={latest.layer.type}
|
||||
const t = this.props.t;
|
||||
return <Block label={t("Type")} fieldSpec={latest.layer.type}
|
||||
data-wd-key={this.props.wdKey}
|
||||
error={this.props.error}
|
||||
>
|
||||
@@ -50,3 +52,6 @@ export default class FieldType extends React.Component<FieldTypeProps> {
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const FieldType = withTranslation()(FieldTypeInternal);
|
||||
export default FieldType;
|
||||
|
||||
@@ -13,9 +13,10 @@ import FilterEditorBlock from './FilterEditorBlock'
|
||||
import InputButton from './InputButton'
|
||||
import Doc from './Doc'
|
||||
import ExpressionProperty from './_ExpressionProperty';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
function combiningFilter(props: FilterEditorProps): LegacyFilterSpecification | ExpressionSpecification {
|
||||
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification {
|
||||
const filter = props.filter || ['all'];
|
||||
|
||||
if (!Array.isArray(filter)) {
|
||||
@@ -89,13 +90,13 @@ function hasNestedCombiningFilter(filter: LegacyFilterSpecification | Expression
|
||||
return false
|
||||
}
|
||||
|
||||
type FilterEditorProps = {
|
||||
type FilterEditorInternalProps = {
|
||||
/** Properties of the vector layer and the available fields */
|
||||
properties?: {[key:string]: any}
|
||||
filter?: any[]
|
||||
errors?: {[key:string]: any}
|
||||
onChange(value: LegacyFilterSpecification | ExpressionSpecification): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
type FilterEditorState = {
|
||||
showDoc: boolean
|
||||
@@ -103,12 +104,12 @@ type FilterEditorState = {
|
||||
valueIsSimpleFilter?: boolean
|
||||
};
|
||||
|
||||
export default class FilterEditor extends React.Component<FilterEditorProps, FilterEditorState> {
|
||||
class FilterEditorInternal extends React.Component<FilterEditorInternalProps, FilterEditorState> {
|
||||
static defaultProps = {
|
||||
filter: ["all"],
|
||||
}
|
||||
|
||||
constructor (props: FilterEditorProps) {
|
||||
constructor (props: FilterEditorInternalProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showDoc: false,
|
||||
@@ -155,17 +156,17 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
|
||||
})
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: FilterEditorProps, currentState: FilterEditorState) {
|
||||
static getDerivedStateFromProps(props: Readonly<FilterEditorInternalProps>, state: FilterEditorState) {
|
||||
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
|
||||
|
||||
// Upgrade but never downgrade
|
||||
if (!displaySimpleFilter && currentState.displaySimpleFilter === true) {
|
||||
if (!displaySimpleFilter && state.displaySimpleFilter === true) {
|
||||
return {
|
||||
displaySimpleFilter: false,
|
||||
valueIsSimpleFilter: false,
|
||||
};
|
||||
}
|
||||
else if (displaySimpleFilter && currentState.displaySimpleFilter === false) {
|
||||
else if (displaySimpleFilter && state.displaySimpleFilter === false) {
|
||||
return {
|
||||
valueIsSimpleFilter: true,
|
||||
}
|
||||
@@ -178,7 +179,7 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
|
||||
}
|
||||
|
||||
render() {
|
||||
const {errors} = this.props;
|
||||
const {errors, t} = this.props;
|
||||
const {displaySimpleFilter} = this.state;
|
||||
const fieldSpec={
|
||||
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
|
||||
@@ -190,16 +191,16 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
|
||||
if (isNestedCombiningFilter) {
|
||||
return <div className="maputnik-filter-editor-unsupported">
|
||||
<p>
|
||||
Nested filters are not supported.
|
||||
{t("Nested filters are not supported.")}
|
||||
</p>
|
||||
<InputButton
|
||||
onClick={this.makeExpression}
|
||||
title="Convert to expression"
|
||||
title={t("Convert to expression")}
|
||||
>
|
||||
<svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||
</svg>
|
||||
Upgrade to expression
|
||||
{t("Upgrade to expression")}
|
||||
</InputButton>
|
||||
</div>
|
||||
}
|
||||
@@ -212,7 +213,7 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
|
||||
<div>
|
||||
<InputButton
|
||||
onClick={this.makeExpression}
|
||||
title="Convert to expression"
|
||||
title={t("Convert to expression")}
|
||||
className="maputnik-make-zoom-function"
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||
@@ -247,13 +248,17 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
|
||||
<Block
|
||||
key="top"
|
||||
fieldSpec={fieldSpec}
|
||||
label={"Filter"}
|
||||
label={t("Filter")}
|
||||
action={actions}
|
||||
>
|
||||
<InputSelect
|
||||
value={combiningOp}
|
||||
onChange={(v: [string, any]) => this.onFilterPartChanged(0, v)}
|
||||
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
|
||||
options={[
|
||||
["all", t("every filter matches")],
|
||||
["none", t("no filter matches")],
|
||||
["any", t("any filter matches")]
|
||||
]}
|
||||
/>
|
||||
</Block>
|
||||
{editorBlocks}
|
||||
@@ -268,7 +273,7 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
||||
</svg> Add filter
|
||||
</svg> {t("Add filter")}
|
||||
</InputButton>
|
||||
</div>
|
||||
<div
|
||||
@@ -299,12 +304,13 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
|
||||
/>
|
||||
{this.state.valueIsSimpleFilter &&
|
||||
<div className="maputnik-expr-infobox">
|
||||
You've entered a old style filter,{' '}
|
||||
{t("You've entered an old style filter.")}
|
||||
{' '}
|
||||
<button
|
||||
onClick={this.makeFilter}
|
||||
className="maputnik-expr-infobox__button"
|
||||
>
|
||||
switch to filter editor
|
||||
{t("Switch to filter editor.")}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@@ -313,3 +319,6 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const FilterEditor = withTranslation()(FilterEditorInternal);
|
||||
export default FilterEditor;
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import React, { PropsWithChildren } from 'react'
|
||||
import InputButton from './InputButton'
|
||||
import {MdDelete} from 'react-icons/md'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FilterEditorBlockProps = PropsWithChildren & {
|
||||
type FilterEditorBlockInternalProps = PropsWithChildren & {
|
||||
onDelete(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class FilterEditorBlock extends React.Component<FilterEditorBlockProps> {
|
||||
class FilterEditorBlockInternal extends React.Component<FilterEditorBlockInternalProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <div className="maputnik-filter-editor-block">
|
||||
<div className="maputnik-filter-editor-block-action">
|
||||
<InputButton
|
||||
className="maputnik-delete-filter"
|
||||
onClick={this.props.onDelete}
|
||||
title="Delete filter block"
|
||||
title={t("Delete filter block")}
|
||||
>
|
||||
<MdDelete />
|
||||
</InputButton>
|
||||
@@ -25,3 +27,5 @@ export default class FilterEditorBlock extends React.Component<FilterEditorBlock
|
||||
}
|
||||
}
|
||||
|
||||
const FilterEditorBlock = withTranslation()(FilterEditorBlockInternal);
|
||||
export default FilterEditorBlock;
|
||||
|
||||
@@ -32,7 +32,7 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: FieldArrayProps, state: FieldArrayState) {
|
||||
static getDerivedStateFromProps(props: Readonly<FieldArrayProps>, state: FieldArrayState) {
|
||||
const value: any[] = [];
|
||||
const initialPropsValue = state.initialPropsValue.slice(0);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
import capitalize from 'lodash.capitalize'
|
||||
import {MdDelete} from 'react-icons/md'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import InputString from './InputString'
|
||||
import InputNumber from './InputNumber'
|
||||
@@ -21,10 +22,11 @@ export type FieldDynamicArrayProps = {
|
||||
}
|
||||
'aria-label'?: string
|
||||
label: string
|
||||
};
|
||||
}
|
||||
|
||||
type FieldDynamicArrayInternalProps = FieldDynamicArrayProps & WithTranslation;
|
||||
|
||||
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
|
||||
class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInternalProps> {
|
||||
changeValue(idx: number, newValue: string | number | undefined) {
|
||||
const values = this.values.slice(0)
|
||||
values[idx] = newValue
|
||||
@@ -62,8 +64,13 @@ export default class FieldDynamicArray extends React.Component<FieldDynamicArray
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const i18nProps = { t, i18n: this.props.i18n, tReady: this.props.tReady };
|
||||
const inputs = this.values.map((v, i) => {
|
||||
const deleteValueBtn= <DeleteValueInputButton onClick={this.deleteValue.bind(this, i)} />
|
||||
const deleteValueBtn= <DeleteValueInputButton
|
||||
onClick={this.deleteValue.bind(this, i)}
|
||||
{...i18nProps}
|
||||
/>;
|
||||
let input;
|
||||
if(this.props.type === 'url') {
|
||||
input = <InputUrl
|
||||
@@ -117,23 +124,27 @@ export default class FieldDynamicArray extends React.Component<FieldDynamicArray
|
||||
className="maputnik-array-add-value"
|
||||
onClick={this.addValue}
|
||||
>
|
||||
Add value
|
||||
{t("Add value")}
|
||||
</InputButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const FieldDynamicArray = withTranslation()(FieldDynamicArrayInternal);
|
||||
export default FieldDynamicArray;
|
||||
|
||||
type DeleteValueInputButtonProps = {
|
||||
onClick?(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
class DeleteValueInputButton extends React.Component<DeleteValueInputButtonProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <InputButton
|
||||
className="maputnik-delete-stop"
|
||||
onClick={this.props.onClick}
|
||||
title="Remove array item"
|
||||
title={t("Remove array item")}
|
||||
>
|
||||
<FieldDocLabel
|
||||
label={<MdDelete />}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
import classnames from 'classnames';
|
||||
import CodeMirror, { ModeSpec } from 'codemirror';
|
||||
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import 'codemirror/mode/javascript/javascript'
|
||||
import 'codemirror/addon/lint/lint'
|
||||
@@ -27,6 +28,7 @@ export type InputJsonProps = {
|
||||
mode?: ModeSpec<any>
|
||||
lint?: boolean | object
|
||||
};
|
||||
type InputJsonInternalProps = InputJsonProps & WithTranslation;
|
||||
|
||||
type InputJsonState = {
|
||||
isEditing: boolean
|
||||
@@ -34,7 +36,7 @@ type InputJsonState = {
|
||||
prevValue: string
|
||||
};
|
||||
|
||||
export default class InputJson extends React.Component<InputJsonProps, InputJsonState> {
|
||||
class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJsonState> {
|
||||
static defaultProps = {
|
||||
lineNumbers: true,
|
||||
lineWrapping: false,
|
||||
@@ -52,7 +54,7 @@ export default class InputJson extends React.Component<InputJsonProps, InputJson
|
||||
_el: HTMLDivElement | null = null;
|
||||
_cancelNextChange: boolean = false;
|
||||
|
||||
constructor(props: InputJsonProps) {
|
||||
constructor(props: InputJsonInternalProps) {
|
||||
super(props);
|
||||
this._keyEvent = "keyboard";
|
||||
this.state = {
|
||||
@@ -156,6 +158,7 @@ export default class InputJson extends React.Component<InputJsonProps, InputJson
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const {showMessage} = this.state;
|
||||
const style = {} as {maxHeight?: number};
|
||||
if (this.props.maxHeight) {
|
||||
@@ -164,7 +167,9 @@ export default class InputJson extends React.Component<InputJsonProps, InputJson
|
||||
|
||||
return <div className="JSONEditor" onPointerDown={this.onPointerDown} aria-hidden="true">
|
||||
<div className={classnames("JSONEditor__message", {"JSONEditor__message--on": showMessage})}>
|
||||
Press <kbd>ESC</kbd> to lose focus
|
||||
<Trans t={t}>
|
||||
Press <kbd>ESC</kbd> to lose focus
|
||||
</Trans>
|
||||
</div>
|
||||
<div
|
||||
className={classnames("codemirror-container", this.props.className)}
|
||||
@@ -174,3 +179,6 @@ export default class InputJson extends React.Component<InputJsonProps, InputJson
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const InputJson = withTranslation()(InputJsonInternal);
|
||||
export default InputJson;
|
||||
|
||||
@@ -41,7 +41,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: InputNumberProps, state: InputNumberState) {
|
||||
static getDerivedStateFromProps(props: Readonly<InputNumberProps>, state: InputNumberState) {
|
||||
if (!state.editing && props.value !== state.value) {
|
||||
return {
|
||||
value: props.value,
|
||||
|
||||
@@ -33,7 +33,7 @@ export default class InputString extends React.Component<InputStringProps, Input
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: InputStringProps, state: InputStringState) {
|
||||
static getDerivedStateFromProps(props: Readonly<InputStringProps>, state: InputStringState) {
|
||||
if (!state.editing) {
|
||||
return {
|
||||
value: props.value
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react'
|
||||
import InputString from './InputString'
|
||||
import SmallError from './SmallError'
|
||||
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||
import { TFunction } from 'i18next';
|
||||
|
||||
|
||||
function validate(url: string) {
|
||||
function validate(url: string, t: TFunction): JSX.Element | undefined {
|
||||
if (url === "") {
|
||||
return;
|
||||
}
|
||||
@@ -22,15 +23,19 @@ function validate(url: string) {
|
||||
const isSsl = window.location.protocol === "https:";
|
||||
|
||||
if (!protocol) {
|
||||
error = (
|
||||
<SmallError>
|
||||
Must provide protocol {
|
||||
isSsl
|
||||
? <code>https://</code>
|
||||
: <><code>http://</code> or <code>https://</code></>
|
||||
}
|
||||
</SmallError>
|
||||
);
|
||||
if (isSsl) {
|
||||
error = (
|
||||
<SmallError>
|
||||
<Trans t={t}>Must provide protocol: <code>https://</code></Trans>
|
||||
</SmallError>
|
||||
);
|
||||
} else {
|
||||
error = (
|
||||
<SmallError>
|
||||
<Trans t={t}>Must provide protocol: <code>http://</code> or <code>https://</code></Trans>
|
||||
</SmallError>
|
||||
);
|
||||
}
|
||||
}
|
||||
else if (
|
||||
protocol &&
|
||||
@@ -39,7 +44,9 @@ function validate(url: string) {
|
||||
) {
|
||||
error = (
|
||||
<SmallError>
|
||||
CORS policy won't allow fetching resources served over http from https, use a <code>https://</code> domain
|
||||
<Trans t={t}>
|
||||
CORS policy won't allow fetching resources served over http from https, use a <code>https://</code> domain
|
||||
</Trans>
|
||||
</SmallError>
|
||||
);
|
||||
}
|
||||
@@ -61,32 +68,34 @@ export type FieldUrlProps = {
|
||||
className?: string
|
||||
};
|
||||
|
||||
type FieldUrlInternalProps = FieldUrlProps & WithTranslation;
|
||||
|
||||
type FieldUrlState = {
|
||||
error?: React.ReactNode
|
||||
}
|
||||
|
||||
export default class FieldUrl extends React.Component<FieldUrlProps, FieldUrlState> {
|
||||
class FieldUrlInternal extends React.Component<FieldUrlInternalProps, FieldUrlState> {
|
||||
static defaultProps = {
|
||||
onInput: () => {},
|
||||
}
|
||||
|
||||
constructor (props: FieldUrlProps) {
|
||||
constructor (props: FieldUrlInternalProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
error: validate(props.value)
|
||||
error: validate(props.value, props.t),
|
||||
};
|
||||
}
|
||||
|
||||
onInput = (url: string) => {
|
||||
this.setState({
|
||||
error: validate(url)
|
||||
error: validate(url, this.props.t),
|
||||
});
|
||||
if (this.props.onInput) this.props.onInput(url);
|
||||
}
|
||||
|
||||
onChange = (url: string) => {
|
||||
this.setState({
|
||||
error: validate(url)
|
||||
error: validate(url, this.props.t),
|
||||
});
|
||||
this.props.onChange(url);
|
||||
}
|
||||
@@ -106,3 +115,5 @@ export default class FieldUrl extends React.Component<FieldUrlProps, FieldUrlSta
|
||||
}
|
||||
}
|
||||
|
||||
const FieldUrl = withTranslation()(FieldUrlInternal);
|
||||
export default FieldUrl;
|
||||
|
||||
@@ -19,31 +19,45 @@ import FieldSourceLayer from './FieldSourceLayer'
|
||||
import { changeType, changeProperty } from '../libs/layer'
|
||||
import layout from '../config/layout.json'
|
||||
import {formatLayerId} from '../libs/format';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import { TFunction } from 'i18next';
|
||||
|
||||
|
||||
function getLayoutForType(type: LayerSpecification["type"]) {
|
||||
return layout[type] ? layout[type] : layout.invalid;
|
||||
function getLayoutForType(type: LayerSpecification["type"], t: TFunction) {
|
||||
return layout[type] ? {
|
||||
...layout[type],
|
||||
groups: layout[type].groups.map(group => {
|
||||
return {
|
||||
...group,
|
||||
id: group.title.replace(/ /g, "_"),
|
||||
title: t(group.title)
|
||||
};
|
||||
}),
|
||||
} : layout.invalid;
|
||||
}
|
||||
|
||||
function layoutGroups(layerType: LayerSpecification["type"]): {title: string, type: string, fields?: string[]}[] {
|
||||
function layoutGroups(layerType: LayerSpecification["type"], t: TFunction): {id: string, title: string, type: string, fields?: string[]}[] {
|
||||
const layerGroup = {
|
||||
title: 'Layer',
|
||||
id: 'layer',
|
||||
title: t('Layer'),
|
||||
type: 'layer'
|
||||
}
|
||||
const filterGroup = {
|
||||
title: 'Filter',
|
||||
id: 'filter',
|
||||
title: t('Filter'),
|
||||
type: 'filter'
|
||||
}
|
||||
const editorGroup = {
|
||||
title: 'JSON Editor',
|
||||
id: 'jsoneditor',
|
||||
title: t('JSON Editor'),
|
||||
type: 'jsoneditor'
|
||||
}
|
||||
return [layerGroup, filterGroup]
|
||||
.concat(getLayoutForType(layerType).groups)
|
||||
.concat(getLayoutForType(layerType, t).groups)
|
||||
.concat([editorGroup])
|
||||
}
|
||||
|
||||
type LayerEditorProps = {
|
||||
type LayerEditorInternalProps = {
|
||||
layer: LayerSpecification
|
||||
sources: {[key: string]: SourceSpecification}
|
||||
vectorLayers: {[key: string]: any}
|
||||
@@ -58,14 +72,14 @@ type LayerEditorProps = {
|
||||
isLastLayer?: boolean
|
||||
layerIndex: number
|
||||
errors?: any[]
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
type LayerEditorState = {
|
||||
editorGroups: {[keys:string]: boolean}
|
||||
};
|
||||
|
||||
/** Layer editor supporting multiple types of layers. */
|
||||
export default class LayerEditor extends React.Component<LayerEditorProps, LayerEditorState> {
|
||||
class LayerEditorInternal extends React.Component<LayerEditorInternalProps, LayerEditorState> {
|
||||
static defaultProps = {
|
||||
onLayerChanged: () => {},
|
||||
onLayerIdChange: () => {},
|
||||
@@ -76,22 +90,22 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
|
||||
constructor(props: LayerEditorProps) {
|
||||
constructor(props: LayerEditorInternalProps) {
|
||||
super(props)
|
||||
|
||||
//TODO: Clean this up and refactor into function
|
||||
const editorGroups: {[keys:string]: boolean} = {}
|
||||
layoutGroups(this.props.layer.type).forEach(group => {
|
||||
layoutGroups(this.props.layer.type, props.t).forEach(group => {
|
||||
editorGroups[group.title] = true
|
||||
})
|
||||
|
||||
this.state = { editorGroups }
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: LayerEditorProps, state: LayerEditorState) {
|
||||
static getDerivedStateFromProps(props: Readonly<LayerEditorInternalProps>, state: LayerEditorState) {
|
||||
const additionalGroups = { ...state.editorGroups }
|
||||
|
||||
getLayoutForType(props.layer.type).groups.forEach(group => {
|
||||
getLayoutForType(props.layer.type, props.t).groups.forEach(group => {
|
||||
if(!(group.title in additionalGroups)) {
|
||||
additionalGroups[group.title] = true
|
||||
}
|
||||
@@ -242,17 +256,19 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const groupIds: string[] = [];
|
||||
const layerType = this.props.layer.type
|
||||
const groups = layoutGroups(layerType).filter(group => {
|
||||
const groups = layoutGroups(layerType, t).filter(group => {
|
||||
return !(layerType === 'background' && group.type === 'source')
|
||||
}).map(group => {
|
||||
const groupId = group.title.replace(/ /g, "_");
|
||||
const groupId = group.id;
|
||||
groupIds.push(groupId);
|
||||
return <LayerEditorGroup
|
||||
data-wd-key={group.title}
|
||||
id={groupId}
|
||||
key={group.title}
|
||||
key={groupId}
|
||||
title={group.title}
|
||||
isActive={this.state.editorGroups[group.title]}
|
||||
onActiveToggle={this.onGroupToggle.bind(this, group.title)}
|
||||
@@ -265,25 +281,25 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
|
||||
|
||||
const items: {[key: string]: {text: string, handler: () => void, disabled?: boolean}} = {
|
||||
delete: {
|
||||
text: "Delete",
|
||||
text: t("Delete"),
|
||||
handler: () => this.props.onLayerDestroy(this.props.layerIndex)
|
||||
},
|
||||
duplicate: {
|
||||
text: "Duplicate",
|
||||
text: t("Duplicate"),
|
||||
handler: () => this.props.onLayerCopy(this.props.layerIndex)
|
||||
},
|
||||
hide: {
|
||||
text: (layout.visibility === "none") ? "Show" : "Hide",
|
||||
text: (layout.visibility === "none") ? t("Show") : t("Hide"),
|
||||
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex)
|
||||
},
|
||||
moveLayerUp: {
|
||||
text: "Move layer up",
|
||||
text: t("Move layer up"),
|
||||
// Not actually used...
|
||||
disabled: this.props.isFirstLayer,
|
||||
handler: () => this.moveLayer(-1)
|
||||
},
|
||||
moveLayerDown: {
|
||||
text: "Move layer down",
|
||||
text: t("Move layer down"),
|
||||
// Not actually used...
|
||||
disabled: this.props.isLastLayer,
|
||||
handler: () => this.moveLayer(+1)
|
||||
@@ -297,12 +313,12 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
|
||||
|
||||
return <section className="maputnik-layer-editor"
|
||||
role="main"
|
||||
aria-label="Layer editor"
|
||||
aria-label={t("Layer editor")}
|
||||
>
|
||||
<header>
|
||||
<div className="layer-header">
|
||||
<h2 className="layer-header__title">
|
||||
Layer: {formatLayerId(this.props.layer.id)}
|
||||
{t("Layer: {{layerId}}", { layerId: formatLayerId(this.props.layer.id) })}
|
||||
</h2>
|
||||
<div className="layer-header__info">
|
||||
<Wrapper
|
||||
@@ -310,7 +326,11 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
|
||||
onSelection={handleSelection}
|
||||
closeOnSelection={false}
|
||||
>
|
||||
<Button id="skip-target-layer-editor" data-wd-key="skip-target-layer-editor" className='more-menu__button' title="Layer options">
|
||||
<Button
|
||||
id="skip-target-layer-editor"
|
||||
data-wd-key="skip-target-layer-editor"
|
||||
className='more-menu__button'
|
||||
title={"Layer options"}>
|
||||
<MdMoreVert className="more-menu__button__svg" />
|
||||
</Button>
|
||||
<Menu>
|
||||
@@ -340,3 +360,6 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
const LayerEditor = withTranslation()(LayerEditorInternal);
|
||||
export default LayerEditor;
|
||||
|
||||
@@ -10,6 +10,7 @@ import {SortEndHandler, SortableContainer} from 'react-sortable-hoc';
|
||||
import type {LayerSpecification} from 'maplibre-gl';
|
||||
import generateUniqueId from '../libs/document-uid';
|
||||
import { findClosestCommonPrefix, layerPrefix } from '../libs/layer';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type LayerListContainerProps = {
|
||||
layers: LayerSpecification[]
|
||||
@@ -22,6 +23,7 @@ type LayerListContainerProps = {
|
||||
sources: object
|
||||
errors: any[]
|
||||
};
|
||||
type LayerListContainerInternalProps = LayerListContainerProps & WithTranslation;
|
||||
|
||||
type LayerListContainerState = {
|
||||
collapsedGroups: {[ket: string]: boolean}
|
||||
@@ -31,14 +33,14 @@ type LayerListContainerState = {
|
||||
};
|
||||
|
||||
// List of collapsible layer editors
|
||||
class LayerListContainer extends React.Component<LayerListContainerProps, LayerListContainerState> {
|
||||
class LayerListContainerInternal extends React.Component<LayerListContainerInternalProps, LayerListContainerState> {
|
||||
static defaultProps = {
|
||||
onLayerSelect: () => {},
|
||||
}
|
||||
selectedItemRef: React.RefObject<any>;
|
||||
scrollContainerRef: React.RefObject<HTMLElement>;
|
||||
|
||||
constructor(props: LayerListContainerProps) {
|
||||
constructor(props: LayerListContainerInternalProps) {
|
||||
super(props);
|
||||
this.selectedItemRef = React.createRef();
|
||||
this.scrollContainerRef = React.createRef();
|
||||
@@ -259,10 +261,12 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
|
||||
})
|
||||
})
|
||||
|
||||
const t = this.props.t;
|
||||
|
||||
return <section
|
||||
className="maputnik-layer-list"
|
||||
role="complementary"
|
||||
aria-label="Layers list"
|
||||
aria-label={t("Layers list")}
|
||||
ref={this.scrollContainerRef}
|
||||
>
|
||||
<ModalAdd
|
||||
@@ -274,7 +278,7 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
|
||||
onLayersChange={this.props.onLayersChange}
|
||||
/>
|
||||
<header className="maputnik-layer-list-header">
|
||||
<span className="maputnik-layer-list-header-title">Layers</span>
|
||||
<span className="maputnik-layer-list-header-title">{t("Layers")}</span>
|
||||
<span className="maputnik-space" />
|
||||
<div className="maputnik-default-property">
|
||||
<div className="maputnik-multibutton">
|
||||
@@ -283,7 +287,11 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
|
||||
data-wd-key="skip-target-layer-list"
|
||||
onClick={this.toggleLayers}
|
||||
className="maputnik-button">
|
||||
{this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"}
|
||||
{this.state.areAllGroupsExpanded === true ?
|
||||
t("Collapse")
|
||||
:
|
||||
t("Expand")
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -293,14 +301,14 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
|
||||
onClick={this.toggleModal.bind(this, 'add')}
|
||||
data-wd-key="layer-list:add-layer"
|
||||
className="maputnik-button maputnik-button-selected">
|
||||
Add Layer
|
||||
{t("Add Layer")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
role="navigation"
|
||||
aria-label="Layers list"
|
||||
aria-label={t("Layers list")}
|
||||
>
|
||||
<ul className="maputnik-layer-list-container">
|
||||
{listItems}
|
||||
@@ -310,6 +318,13 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
|
||||
}
|
||||
}
|
||||
|
||||
// The next two lines have react-refresh/only-export-components disabled because they are
|
||||
// internal components that are not intended to be used outside of this file.
|
||||
// For some reason, the linter is not recognizing these components correctly.
|
||||
// When these components are migrated to functional components, the HOCs will no longer be needed
|
||||
// and the comments can be removed.
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
const LayerListContainer = withTranslation()(LayerListContainerInternal);
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
const LayerListContainerSortable = SortableContainer((props: LayerListContainerProps) => <LayerListContainer {...props} />)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import '../libs/maplibre-rtl'
|
||||
//@ts-ignore
|
||||
import MaplibreGeocoder from '@maplibre/maplibre-gl-geocoder';
|
||||
import '@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css';
|
||||
import { withTranslation, WithTranslation } from 'react-i18next'
|
||||
|
||||
function renderPopup(popup: JSX.Element, mountNode: ReactDOM.Container): HTMLElement {
|
||||
ReactDOM.render(popup, mountNode);
|
||||
@@ -51,7 +52,7 @@ function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers:
|
||||
return inspectStyle
|
||||
}
|
||||
|
||||
type MapMaplibreGlProps = {
|
||||
type MapMaplibreGlInternalProps = {
|
||||
onDataChange?(event: {map: Map | null}): unknown
|
||||
onLayerSelect(...args: unknown[]): unknown
|
||||
mapStyle: StyleSpecification
|
||||
@@ -64,7 +65,7 @@ type MapMaplibreGlProps = {
|
||||
}
|
||||
replaceAccessTokens(mapStyle: StyleSpecification): StyleSpecification
|
||||
onChange(value: {center: LngLat, zoom: number}): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
type MapMaplibreGlState = {
|
||||
map: Map | null
|
||||
@@ -72,7 +73,7 @@ type MapMaplibreGlState = {
|
||||
zoom?: number
|
||||
};
|
||||
|
||||
export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, MapMaplibreGlState> {
|
||||
class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps, MapMaplibreGlState> {
|
||||
static defaultProps = {
|
||||
onMapLoaded: () => {},
|
||||
onDataChange: () => {},
|
||||
@@ -82,7 +83,7 @@ export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, M
|
||||
}
|
||||
container: HTMLDivElement | null = null
|
||||
|
||||
constructor(props: MapMaplibreGlProps) {
|
||||
constructor(props: MapMaplibreGlInternalProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
map: null,
|
||||
@@ -91,7 +92,7 @@ export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, M
|
||||
}
|
||||
|
||||
|
||||
shouldComponentUpdate(nextProps: MapMaplibreGlProps, nextState: MapMaplibreGlState) {
|
||||
shouldComponentUpdate(nextProps: MapMaplibreGlInternalProps, nextState: MapMaplibreGlState) {
|
||||
let should = false;
|
||||
try {
|
||||
should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState);
|
||||
@@ -154,7 +155,7 @@ export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, M
|
||||
|
||||
this.initGeocoder(map);
|
||||
|
||||
const zoomControl = new ZoomControl;
|
||||
const zoomControl = new ZoomControl(this.props.t("Zoom:"));
|
||||
map.addControl(zoomControl, 'top-right');
|
||||
|
||||
const nav = new MapLibreGl.NavigationControl({visualizePitch:true});
|
||||
@@ -256,18 +257,24 @@ export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, M
|
||||
};
|
||||
}
|
||||
};
|
||||
const geocoder = new MaplibreGeocoder(geocoderConfig, {maplibregl: MapLibreGl});
|
||||
const geocoder = new MaplibreGeocoder(geocoderConfig, {
|
||||
placeholder: this.props.t("Search"),
|
||||
maplibregl: MapLibreGl,
|
||||
});
|
||||
map.addControl(geocoder, 'top-left');
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <div
|
||||
className="maputnik-map__map"
|
||||
role="region"
|
||||
aria-label="Map view"
|
||||
aria-label={t("Map view")}
|
||||
ref={x => this.container = x}
|
||||
data-wd-key="maplibre:map"
|
||||
></div>
|
||||
}
|
||||
}
|
||||
|
||||
const MapMaplibreGl = withTranslation()(MapMaplibreGlInternal);
|
||||
export default MapMaplibreGl;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import {throttle} from 'lodash';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup';
|
||||
|
||||
@@ -23,7 +24,7 @@ function renderCoords (coords: string[]) {
|
||||
}
|
||||
}
|
||||
|
||||
type MapOpenLayersProps = {
|
||||
type MapOpenLayersInternalProps = {
|
||||
onDataChange?(...args: unknown[]): unknown
|
||||
mapStyle: object
|
||||
accessToken?: string
|
||||
@@ -32,7 +33,7 @@ type MapOpenLayersProps = {
|
||||
debugToolbox: boolean
|
||||
replaceAccessTokens(...args: unknown[]): unknown
|
||||
onChange(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
type MapOpenLayersState = {
|
||||
zoom: string
|
||||
@@ -42,7 +43,7 @@ type MapOpenLayersState = {
|
||||
selectedFeatures?: any[]
|
||||
};
|
||||
|
||||
export default class MapOpenLayers extends React.Component<MapOpenLayersProps, MapOpenLayersState> {
|
||||
class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps, MapOpenLayersState> {
|
||||
static defaultProps = {
|
||||
onMapLoaded: () => {},
|
||||
onDataChange: () => {},
|
||||
@@ -54,7 +55,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
|
||||
overlay: Overlay | undefined;
|
||||
popupContainer: HTMLElement | null = null;
|
||||
|
||||
constructor(props: MapOpenLayersProps) {
|
||||
constructor(props: MapOpenLayersInternalProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
zoom: "0",
|
||||
@@ -73,7 +74,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
|
||||
apply(this.map, newMapStyle);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: MapOpenLayersProps) {
|
||||
componentDidUpdate(prevProps: MapOpenLayersInternalProps) {
|
||||
if (this.props.mapStyle !== prevProps.mapStyle) {
|
||||
this.updateStyle(
|
||||
this.props.replaceAccessTokens(this.props.mapStyle)
|
||||
@@ -151,6 +152,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <div className="maputnik-ol-container">
|
||||
<div
|
||||
ref={x => this.popupContainer = x}
|
||||
@@ -160,7 +162,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
|
||||
<button
|
||||
className="maplibregl-popup-close-button"
|
||||
onClick={this.closeOverlay}
|
||||
aria-label="Close popup"
|
||||
aria-label={t("Close popup")}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -170,20 +172,20 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
|
||||
/>
|
||||
</div>
|
||||
<div className="maputnik-ol-zoom">
|
||||
Zoom: {this.state.zoom}
|
||||
{t("Zoom:")} {this.state.zoom}
|
||||
</div>
|
||||
{this.props.debugToolbox &&
|
||||
<div className="maputnik-ol-debug">
|
||||
<div>
|
||||
<label>cursor: </label>
|
||||
<label>{t("cursor:")} </label>
|
||||
<span>{renderCoords(this.state.cursor)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label>center: </label>
|
||||
<label>{t("center:")} </label>
|
||||
<span>{renderCoords(this.state.center)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<label>rotation: </label>
|
||||
<label>{t("rotation:")} </label>
|
||||
<span>{this.state.rotation}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -192,7 +194,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
|
||||
className="maputnik-ol"
|
||||
ref={x => this.container = x}
|
||||
role="region"
|
||||
aria-label="Map view"
|
||||
aria-label={t("Map view")}
|
||||
style={{
|
||||
...this.props.style,
|
||||
}}>
|
||||
@@ -201,3 +203,5 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
|
||||
}
|
||||
}
|
||||
|
||||
const MapOpenLayers = withTranslation()(MapOpenLayersInternal);
|
||||
export default MapOpenLayers;
|
||||
|
||||
@@ -2,9 +2,9 @@ import React, { PropsWithChildren } from 'react'
|
||||
import {MdClose} from 'react-icons/md'
|
||||
import AriaModal from 'react-aria-modal'
|
||||
import classnames from 'classnames';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
type ModalProps = PropsWithChildren & {
|
||||
type ModalInternalProps = PropsWithChildren & {
|
||||
"data-wd-key"?: string
|
||||
isOpen: boolean
|
||||
title: string
|
||||
@@ -12,10 +12,10 @@ type ModalProps = PropsWithChildren & {
|
||||
underlayClickExits?: boolean
|
||||
underlayProps?: any
|
||||
className?: string
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
|
||||
export default class Modal extends React.Component<ModalProps> {
|
||||
class ModalInternal extends React.Component<ModalInternalProps> {
|
||||
static defaultProps = {
|
||||
underlayClickExits: true
|
||||
}
|
||||
@@ -32,6 +32,7 @@ export default class Modal extends React.Component<ModalProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
if(this.props.isOpen) {
|
||||
return <AriaModal
|
||||
titleText={this.props.title}
|
||||
@@ -49,7 +50,7 @@ export default class Modal extends React.Component<ModalProps> {
|
||||
<h1 className="maputnik-modal-header-title">{this.props.title}</h1>
|
||||
<span className="maputnik-modal-header-space"></span>
|
||||
<button className="maputnik-modal-header-toggle"
|
||||
title="Close modal"
|
||||
title={t("Close modal")}
|
||||
onClick={this.onClose}
|
||||
data-wd-key={this.props["data-wd-key"]+".close-modal"}
|
||||
>
|
||||
@@ -68,3 +69,5 @@ export default class Modal extends React.Component<ModalProps> {
|
||||
}
|
||||
}
|
||||
|
||||
const Modal = withTranslation()(ModalInternal);
|
||||
export default Modal;
|
||||
|
||||
@@ -7,15 +7,16 @@ import FieldId from './FieldId'
|
||||
import FieldSource from './FieldSource'
|
||||
import FieldSourceLayer from './FieldSourceLayer'
|
||||
import type {LayerSpecification} from 'maplibre-gl'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type ModalAddProps = {
|
||||
type ModalAddInternalProps = {
|
||||
layers: LayerSpecification[]
|
||||
onLayersChange(layers: LayerSpecification[]): unknown
|
||||
isOpen: boolean
|
||||
onOpenToggle(open: boolean): unknown
|
||||
// A dict of source id's and the available source layers
|
||||
sources: any
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
type ModalAddState = {
|
||||
type: LayerSpecification["type"]
|
||||
@@ -24,7 +25,7 @@ type ModalAddState = {
|
||||
'source-layer'?: string
|
||||
};
|
||||
|
||||
export default class ModalAdd extends React.Component<ModalAddProps, ModalAddState> {
|
||||
class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddState> {
|
||||
addLayer = () => {
|
||||
const changedLayers = this.props.layers.slice(0)
|
||||
const layer: ModalAddState = {
|
||||
@@ -45,7 +46,7 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
|
||||
this.props.onOpenToggle(false)
|
||||
}
|
||||
|
||||
constructor(props: ModalAddProps) {
|
||||
constructor(props: ModalAddInternalProps) {
|
||||
super(props)
|
||||
const state: ModalAddState = {
|
||||
type: 'fill',
|
||||
@@ -54,12 +55,12 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
|
||||
|
||||
if(props.sources.length > 0) {
|
||||
state.source = Object.keys(this.props.sources)[0];
|
||||
state['source-layer'] = this.props.sources[state.source as keyof ModalAddProps["sources"]][0]
|
||||
state['source-layer'] = this.props.sources[state.source as keyof ModalAddInternalProps["sources"]][0]
|
||||
}
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
componentDidUpdate(_prevProps: ModalAddProps, prevState: ModalAddState) {
|
||||
componentDidUpdate(_prevProps: ModalAddInternalProps, prevState: ModalAddState) {
|
||||
// Check if source is valid for new type
|
||||
const oldType = prevState.type;
|
||||
const newType = this.state.type;
|
||||
@@ -125,13 +126,14 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
|
||||
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const sources = this.getSources(this.state.type);
|
||||
const layers = this.getLayersForSource(this.state.source!);
|
||||
|
||||
return <Modal
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Add Layer'}
|
||||
title={t('Add Layer')}
|
||||
data-wd-key="modal:add-layer"
|
||||
className="maputnik-add-modal"
|
||||
>
|
||||
@@ -169,10 +171,12 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
|
||||
onClick={this.addLayer}
|
||||
data-wd-key="add-layer"
|
||||
>
|
||||
Add Layer
|
||||
{t("Add Layer")}
|
||||
</InputButton>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
const ModalAdd = withTranslation()(ModalAddInternal);
|
||||
export default ModalAdd;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||
import Modal from './Modal'
|
||||
|
||||
|
||||
type ModalDebugProps = {
|
||||
type ModalDebugInternalProps = {
|
||||
isOpen: boolean
|
||||
renderer: string
|
||||
onChangeMaplibreGlDebug(key: string, checked: boolean): unknown
|
||||
@@ -18,12 +19,12 @@ type ModalDebugProps = {
|
||||
lat: number
|
||||
}
|
||||
}
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
|
||||
export default class ModalDebug extends React.Component<ModalDebugProps> {
|
||||
class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
|
||||
render() {
|
||||
const {mapView} = this.props;
|
||||
const {t, mapView} = this.props;
|
||||
|
||||
const osmZoom = Math.round(mapView.zoom)+1;
|
||||
const osmLon = +(mapView.center.lng).toFixed(5);
|
||||
@@ -33,10 +34,10 @@ export default class ModalDebug extends React.Component<ModalDebugProps> {
|
||||
data-wd-key="modal:debug"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Debug'}
|
||||
title={t('Debug')}
|
||||
>
|
||||
<section className="maputnik-modal-section maputnik-modal-shortcuts">
|
||||
<h1>Options</h1>
|
||||
<h1>{t("Options")}</h1>
|
||||
{this.props.renderer === 'mlgljs' &&
|
||||
<ul>
|
||||
{Object.entries(this.props.maplibreGlDebugOptions!).map(([key, val]) => {
|
||||
@@ -63,16 +64,20 @@ export default class ModalDebug extends React.Component<ModalDebugProps> {
|
||||
<section className="maputnik-modal-section">
|
||||
<h1>Links</h1>
|
||||
<p>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={`https://www.openstreetmap.org/#map=${osmZoom}/${osmLat}/${osmLon}`}
|
||||
>
|
||||
Open in OSM
|
||||
</a> — Opens the current view on openstreetmap.org
|
||||
<Trans t={t}>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={`https://www.openstreetmap.org/#map=${osmZoom}/${osmLat}/${osmLon}`}
|
||||
>
|
||||
Open in OSM
|
||||
</a> — Opens the current view on openstreetmap.org
|
||||
</Trans>
|
||||
</p>
|
||||
</section>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
const ModalDebug = withTranslation()(ModalDebugInternal);
|
||||
export default ModalDebug;
|
||||
|
||||
@@ -5,6 +5,7 @@ import {version} from 'maplibre-gl/package.json'
|
||||
import {format} from '@maplibre/maplibre-gl-style-spec'
|
||||
import type {StyleSpecification} from 'maplibre-gl'
|
||||
import {MdFileDownload} from 'react-icons/md'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import FieldString from './FieldString'
|
||||
import InputButton from './InputButton'
|
||||
@@ -16,15 +17,15 @@ import fieldSpecAdditional from '../libs/field-spec-additional'
|
||||
const MAPLIBRE_GL_VERSION = version;
|
||||
|
||||
|
||||
type ModalExportProps = {
|
||||
type ModalExportInternalProps = {
|
||||
mapStyle: StyleSpecification & { id: string }
|
||||
onStyleChanged(...args: unknown[]): unknown
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
|
||||
export default class ModalExport extends React.Component<ModalExportProps> {
|
||||
class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
||||
|
||||
tokenizedStyle () {
|
||||
return format(
|
||||
@@ -48,7 +49,7 @@ export default class ModalExport extends React.Component<ModalExportProps> {
|
||||
|
||||
downloadHtml() {
|
||||
const tokenStyle = this.tokenizedStyle();
|
||||
const htmlTitle = this.props.mapStyle.name || "Map";
|
||||
const htmlTitle = this.props.mapStyle.name || this.props.t("Map");
|
||||
const html = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -100,18 +101,20 @@ export default class ModalExport extends React.Component<ModalExportProps> {
|
||||
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const fsa = fieldSpecAdditional(t);
|
||||
return <Modal
|
||||
data-wd-key="modal:export"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Export Style'}
|
||||
title={t('Export Style')}
|
||||
className="maputnik-export-modal"
|
||||
>
|
||||
|
||||
<section className="maputnik-modal-section">
|
||||
<h1>Download Style</h1>
|
||||
<h1>{t("Download Style")}</h1>
|
||||
<p>
|
||||
Download a JSON style to your computer.
|
||||
{t("Download a JSON style to your computer.")}
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://docs.maptiler.com/cloud/api/authentication-key/" target="_blank" rel="noreferrer">MapTiler</a>,
|
||||
@@ -123,14 +126,14 @@ export default class ModalExport extends React.Component<ModalExportProps> {
|
||||
|
||||
<div>
|
||||
<FieldString
|
||||
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
|
||||
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
|
||||
label={fsa.maputnik.maptiler_access_token.label}
|
||||
fieldSpec={fsa.maputnik.maptiler_access_token}
|
||||
value={(this.props.mapStyle.metadata || {} as any)['maputnik:openmaptiles_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
|
||||
/>
|
||||
<FieldString
|
||||
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
|
||||
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
|
||||
label={fsa.maputnik.thunderforest_access_token.label}
|
||||
fieldSpec={fsa.maputnik.thunderforest_access_token}
|
||||
value={(this.props.mapStyle.metadata || {} as any)['maputnik:thunderforest_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
|
||||
/>
|
||||
@@ -141,14 +144,14 @@ export default class ModalExport extends React.Component<ModalExportProps> {
|
||||
onClick={this.downloadStyle.bind(this)}
|
||||
>
|
||||
<MdFileDownload />
|
||||
Download Style
|
||||
{t("Download Style")}
|
||||
</InputButton>
|
||||
|
||||
<InputButton
|
||||
onClick={this.downloadHtml.bind(this)}
|
||||
>
|
||||
<MdFileDownload />
|
||||
Download HTML
|
||||
{t("Download HTML")}
|
||||
</InputButton>
|
||||
</div>
|
||||
</section>
|
||||
@@ -157,3 +160,5 @@ export default class ModalExport extends React.Component<ModalExportProps> {
|
||||
}
|
||||
}
|
||||
|
||||
const ModalExport = withTranslation()(ModalExportInternal);
|
||||
export default ModalExport;
|
||||
|
||||
@@ -2,23 +2,25 @@ import React from 'react'
|
||||
|
||||
import InputButton from './InputButton'
|
||||
import Modal from './Modal'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
type ModalLoadingProps = {
|
||||
type ModalLoadingInternalProps = {
|
||||
isOpen: boolean
|
||||
onCancel(...args: unknown[]): unknown
|
||||
title: string
|
||||
message: React.ReactNode
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
|
||||
export default class ModalLoading extends React.Component<ModalLoadingProps> {
|
||||
class ModalLoadingInternal extends React.Component<ModalLoadingInternalProps> {
|
||||
underlayOnClick(e: Event) {
|
||||
// This stops click events falling through to underlying modals.
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <Modal
|
||||
data-wd-key="modal:loading"
|
||||
isOpen={this.props.isOpen}
|
||||
@@ -35,10 +37,12 @@ export default class ModalLoading extends React.Component<ModalLoadingProps> {
|
||||
</p>
|
||||
<p className="maputnik-dialog__buttons">
|
||||
<InputButton onClick={(e) => this.props.onCancel(e)}>
|
||||
Cancel
|
||||
{t("Cancel")}
|
||||
</InputButton>
|
||||
</p>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
const ModalLoading = withTranslation()(ModalLoadingInternal);
|
||||
export default ModalLoading;
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { FormEvent } from 'react'
|
||||
import {MdFileUpload} from 'react-icons/md'
|
||||
import {MdAddCircleOutline} from 'react-icons/md'
|
||||
import FileReaderInput, { Result } from 'react-file-reader-input'
|
||||
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import ModalLoading from './ModalLoading'
|
||||
import Modal from './Modal'
|
||||
@@ -42,11 +43,11 @@ class PublicStyle extends React.Component<PublicStyleProps> {
|
||||
}
|
||||
}
|
||||
|
||||
type ModalOpenProps = {
|
||||
type ModalOpenInternalProps = {
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
onStyleOpen(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
type ModalOpenState = {
|
||||
styleUrl: string
|
||||
@@ -55,8 +56,8 @@ type ModalOpenState = {
|
||||
activeRequestUrl?: string | null
|
||||
};
|
||||
|
||||
export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpenState> {
|
||||
constructor(props: ModalOpenProps) {
|
||||
class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpenState> {
|
||||
constructor(props: ModalOpenInternalProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
styleUrl: ""
|
||||
@@ -174,6 +175,7 @@ export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpen
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const styleOptions = publicStyles.map(style => {
|
||||
return <PublicStyle
|
||||
key={style.id}
|
||||
@@ -200,29 +202,31 @@ export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpen
|
||||
data-wd-key="modal:open"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={() => this.onOpenToggle()}
|
||||
title={'Open Style'}
|
||||
title={t('Open Style')}
|
||||
>
|
||||
{errorElement}
|
||||
<section className="maputnik-modal-section">
|
||||
<h1>Upload Style</h1>
|
||||
<p>Upload a JSON style from your computer.</p>
|
||||
<FileReaderInput onChange={this.onUpload} tabIndex={-1} aria-label="Style file">
|
||||
<InputButton className="maputnik-upload-button"><MdFileUpload /> Upload</InputButton>
|
||||
<h1>{t("Upload Style")}</h1>
|
||||
<p>{t("Upload a JSON style from your computer.")}</p>
|
||||
<FileReaderInput onChange={this.onUpload} tabIndex={-1} aria-label={t("Style file")}>
|
||||
<InputButton className="maputnik-upload-button"><MdFileUpload /> {t("Upload")}</InputButton>
|
||||
</FileReaderInput>
|
||||
</section>
|
||||
|
||||
<section className="maputnik-modal-section">
|
||||
<form onSubmit={this.onSubmitUrl}>
|
||||
<h1>Load from URL</h1>
|
||||
<h1>{t("Load from URL")}</h1>
|
||||
<p>
|
||||
Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>.
|
||||
<Trans t={t}>
|
||||
Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>.
|
||||
</Trans>
|
||||
</p>
|
||||
<InputUrl
|
||||
aria-label="Style URL"
|
||||
aria-label={t("Style URL")}
|
||||
data-wd-key="modal:open.url.input"
|
||||
type="text"
|
||||
className="maputnik-input"
|
||||
default="Enter URL..."
|
||||
default={t("Enter URL...")}
|
||||
value={this.state.styleUrl}
|
||||
onInput={this.onChangeUrl}
|
||||
onChange={this.onChangeUrl}
|
||||
@@ -239,9 +243,9 @@ export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpen
|
||||
</section>
|
||||
|
||||
<section className="maputnik-modal-section maputnik-modal-section--shrink">
|
||||
<h1>Gallery Styles</h1>
|
||||
<h1>{t("Gallery Styles")}</h1>
|
||||
<p>
|
||||
Open one of the publicly available styles to start from.
|
||||
{t("Open one of the publicly available styles to start from.")}
|
||||
</p>
|
||||
<div className="maputnik-style-gallery-container">
|
||||
{styleOptions}
|
||||
@@ -251,12 +255,14 @@ export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpen
|
||||
|
||||
<ModalLoading
|
||||
isOpen={!!this.state.activeRequest}
|
||||
title={'Loading style'}
|
||||
title={t('Loading style')}
|
||||
onCancel={(e: Event) => this.onCancelActiveRequest(e)}
|
||||
message={"Loading: "+this.state.activeRequestUrl}
|
||||
message={t("Loading: {{requestUrl}}", { requestUrl: this.state.activeRequestUrl })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const ModalOpen = withTranslation()(ModalOpenInternal);
|
||||
export default ModalOpen;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import type {LightSpecification, StyleSpecification, TerrainSpecification, TransitionSpecification} from 'maplibre-gl'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import FieldArray from './FieldArray'
|
||||
import FieldNumber from './FieldNumber'
|
||||
@@ -12,15 +13,15 @@ import FieldColor from './FieldColor'
|
||||
import Modal from './Modal'
|
||||
import fieldSpecAdditional from '../libs/field-spec-additional'
|
||||
|
||||
type ModalSettingsProps = {
|
||||
type ModalSettingsInternalProps = {
|
||||
mapStyle: StyleSpecification
|
||||
onStyleChanged(...args: unknown[]): unknown
|
||||
onChangeMetadataProperty(...args: unknown[]): unknown
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps> {
|
||||
changeTransitionProperty(property: keyof TransitionSpecification, value: number | undefined) {
|
||||
const transition = {
|
||||
...this.props.mapStyle.transition,
|
||||
@@ -95,7 +96,8 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
|
||||
render() {
|
||||
const metadata = this.props.mapStyle.metadata || {} as any;
|
||||
const {onChangeMetadataProperty, mapStyle} = this.props;
|
||||
const {t, onChangeMetadataProperty, mapStyle} = this.props;
|
||||
const fsa = fieldSpecAdditional(t);
|
||||
|
||||
const light = this.props.mapStyle.light || {};
|
||||
const transition = this.props.mapStyle.transition || {};
|
||||
@@ -105,33 +107,33 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
data-wd-key="modal:settings"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Style Settings'}
|
||||
title={t('Style Settings')}
|
||||
>
|
||||
<div className="modal:settings">
|
||||
<FieldString
|
||||
label={"Name"}
|
||||
label={t("Name")}
|
||||
fieldSpec={latest.$root.name}
|
||||
data-wd-key="modal:settings.name"
|
||||
value={this.props.mapStyle.name}
|
||||
onChange={this.changeStyleProperty.bind(this, "name")}
|
||||
/>
|
||||
<FieldString
|
||||
label={"Owner"}
|
||||
fieldSpec={{doc: "Owner ID of the style. Used by Mapbox or future style APIs."}}
|
||||
label={t("Owner")}
|
||||
fieldSpec={{doc: t("Owner ID of the style. Used by Mapbox or future style APIs.")}}
|
||||
data-wd-key="modal:settings.owner"
|
||||
value={(this.props.mapStyle as any).owner}
|
||||
onChange={this.changeStyleProperty.bind(this, "owner")}
|
||||
/>
|
||||
<FieldUrl
|
||||
fieldSpec={latest.$root.sprite}
|
||||
label="Sprite URL"
|
||||
label={t("Sprite URL")}
|
||||
data-wd-key="modal:settings.sprite"
|
||||
value={this.props.mapStyle.sprite as string}
|
||||
onChange={this.changeStyleProperty.bind(this, "sprite")}
|
||||
/>
|
||||
|
||||
<FieldUrl
|
||||
label="Glyphs URL"
|
||||
label={t("Glyphs URL")}
|
||||
fieldSpec={latest.$root.glyphs}
|
||||
data-wd-key="modal:settings.glyphs"
|
||||
value={this.props.mapStyle.glyphs as string}
|
||||
@@ -139,23 +141,23 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
/>
|
||||
|
||||
<FieldString
|
||||
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
|
||||
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
|
||||
label={fsa.maputnik.maptiler_access_token.label}
|
||||
fieldSpec={fsa.maputnik.maptiler_access_token}
|
||||
data-wd-key="modal:settings.maputnik:openmaptiles_access_token"
|
||||
value={metadata['maputnik:openmaptiles_access_token']}
|
||||
onChange={onChangeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
|
||||
/>
|
||||
|
||||
<FieldString
|
||||
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
|
||||
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
|
||||
label={fsa.maputnik.thunderforest_access_token.label}
|
||||
fieldSpec={fsa.maputnik.thunderforest_access_token}
|
||||
data-wd-key="modal:settings.maputnik:thunderforest_access_token"
|
||||
value={metadata['maputnik:thunderforest_access_token']}
|
||||
onChange={onChangeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
|
||||
/>
|
||||
|
||||
<FieldArray
|
||||
label={"Center"}
|
||||
label={t("Center")}
|
||||
fieldSpec={latest.$root.center}
|
||||
length={2}
|
||||
type="number"
|
||||
@@ -165,7 +167,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
/>
|
||||
|
||||
<FieldNumber
|
||||
label={"Zoom"}
|
||||
label={t("Zoom")}
|
||||
fieldSpec={latest.$root.zoom}
|
||||
value={mapStyle.zoom}
|
||||
default={0}
|
||||
@@ -173,7 +175,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
/>
|
||||
|
||||
<FieldNumber
|
||||
label={"Bearing"}
|
||||
label={t("Bearing")}
|
||||
fieldSpec={latest.$root.bearing}
|
||||
value={mapStyle.bearing}
|
||||
default={latest.$root.bearing.default}
|
||||
@@ -181,7 +183,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
/>
|
||||
|
||||
<FieldNumber
|
||||
label={"Pitch"}
|
||||
label={t("Pitch")}
|
||||
fieldSpec={latest.$root.pitch}
|
||||
value={mapStyle.pitch}
|
||||
default={latest.$root.pitch.default}
|
||||
@@ -189,7 +191,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
/>
|
||||
|
||||
<FieldEnum
|
||||
label={"Light anchor"}
|
||||
label={t("Light anchor")}
|
||||
fieldSpec={latest.light.anchor}
|
||||
name="light-anchor"
|
||||
value={light.anchor as string}
|
||||
@@ -199,7 +201,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
/>
|
||||
|
||||
<FieldColor
|
||||
label={"Light color"}
|
||||
label={t("Light color")}
|
||||
fieldSpec={latest.light.color}
|
||||
value={light.color as string}
|
||||
default={latest.light.color.default}
|
||||
@@ -207,7 +209,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
/>
|
||||
|
||||
<FieldNumber
|
||||
label={"Light intensity"}
|
||||
label={t("Light intensity")}
|
||||
fieldSpec={latest.light.intensity}
|
||||
value={light.intensity as number}
|
||||
default={latest.light.intensity.default}
|
||||
@@ -215,7 +217,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
/>
|
||||
|
||||
<FieldArray
|
||||
label={"Light position"}
|
||||
label={t("Light position")}
|
||||
fieldSpec={latest.light.position}
|
||||
type="number"
|
||||
length={latest.light.position.length}
|
||||
@@ -225,7 +227,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
/>
|
||||
|
||||
<FieldString
|
||||
label={"Terrain source"}
|
||||
label={t("Terrain source")}
|
||||
fieldSpec={latest.terrain.source}
|
||||
data-wd-key="modal:settings.maputnik:terrain_source"
|
||||
value={terrain.source}
|
||||
@@ -233,7 +235,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
/>
|
||||
|
||||
<FieldNumber
|
||||
label={"Terrain exaggeration"}
|
||||
label={t("Terrain exaggeration")}
|
||||
fieldSpec={latest.terrain.exaggeration}
|
||||
value={terrain.exaggeration}
|
||||
default={latest.terrain.exaggeration.default}
|
||||
@@ -241,7 +243,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
/>
|
||||
|
||||
<FieldNumber
|
||||
label={"Transition delay"}
|
||||
label={t("Transition delay")}
|
||||
fieldSpec={latest.transition.delay}
|
||||
value={transition.delay}
|
||||
default={latest.transition.delay.default}
|
||||
@@ -249,7 +251,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
/>
|
||||
|
||||
<FieldNumber
|
||||
label={"Transition duration"}
|
||||
label={t("Transition duration")}
|
||||
fieldSpec={latest.transition.duration}
|
||||
value={transition.duration}
|
||||
default={latest.transition.duration.default}
|
||||
@@ -257,12 +259,12 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
/>
|
||||
|
||||
<FieldSelect
|
||||
label={fieldSpecAdditional.maputnik.style_renderer.label}
|
||||
fieldSpec={fieldSpecAdditional.maputnik.style_renderer}
|
||||
label={fsa.maputnik.style_renderer.label}
|
||||
fieldSpec={fsa.maputnik.style_renderer}
|
||||
data-wd-key="modal:settings.maputnik:renderer"
|
||||
options={[
|
||||
['mlgljs', 'MapLibreGL JS'],
|
||||
['ol', 'Open Layers (experimental)'],
|
||||
['ol', t('Open Layers (experimental)')],
|
||||
]}
|
||||
value={metadata['maputnik:renderer'] || 'mlgljs'}
|
||||
onChange={onChangeMetadataProperty.bind(this, 'maputnik:renderer')}
|
||||
@@ -272,3 +274,5 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
|
||||
}
|
||||
}
|
||||
|
||||
const ModalSettings = withTranslation()(ModalSettingsInternal)
|
||||
export default ModalSettings;
|
||||
|
||||
@@ -1,48 +1,50 @@
|
||||
import React from 'react'
|
||||
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import Modal from './Modal'
|
||||
|
||||
|
||||
type ModalShortcutsProps = {
|
||||
type ModalShortcutsInternalProps = {
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
|
||||
export default class ModalShortcuts extends React.Component<ModalShortcutsProps> {
|
||||
class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const help = [
|
||||
{
|
||||
key: <kbd>?</kbd>,
|
||||
text: "Shortcuts menu"
|
||||
text: t("Shortcuts menu")
|
||||
},
|
||||
{
|
||||
key: <kbd>o</kbd>,
|
||||
text: "Open modal"
|
||||
text: t("Open modal")
|
||||
},
|
||||
{
|
||||
key: <kbd>e</kbd>,
|
||||
text: "Export modal"
|
||||
text: t("Export modal")
|
||||
},
|
||||
{
|
||||
key: <kbd>d</kbd>,
|
||||
text: "Data Sources modal"
|
||||
text: t("Data Sources modal")
|
||||
},
|
||||
{
|
||||
key: <kbd>s</kbd>,
|
||||
text: "Style Settings modal"
|
||||
text: t("Style Settings modal")
|
||||
},
|
||||
{
|
||||
key: <kbd>i</kbd>,
|
||||
text: "Toggle inspect"
|
||||
text: t("Toggle inspect")
|
||||
},
|
||||
{
|
||||
key: <kbd>m</kbd>,
|
||||
text: "Focus map"
|
||||
text: t("Focus map")
|
||||
},
|
||||
{
|
||||
key: <kbd>!</kbd>,
|
||||
text: "Debug modal"
|
||||
text: t("Debug modal")
|
||||
},
|
||||
]
|
||||
|
||||
@@ -50,51 +52,51 @@ export default class ModalShortcuts extends React.Component<ModalShortcutsProps>
|
||||
const mapShortcuts = [
|
||||
{
|
||||
key: <kbd>+</kbd>,
|
||||
text: "Increase the zoom level by 1.",
|
||||
text: t("Increase the zoom level by 1.",)
|
||||
},
|
||||
{
|
||||
key: <><kbd>Shift</kbd> + <kbd>+</kbd></>,
|
||||
text: "Increase the zoom level by 2.",
|
||||
text: t("Increase the zoom level by 2.",)
|
||||
},
|
||||
{
|
||||
key: <kbd>-</kbd>,
|
||||
text: "Decrease the zoom level by 1.",
|
||||
text: t("Decrease the zoom level by 1.",)
|
||||
},
|
||||
{
|
||||
key: <><kbd>Shift</kbd> + <kbd>-</kbd></>,
|
||||
text: "Decrease the zoom level by 2.",
|
||||
text: t("Decrease the zoom level by 2.",)
|
||||
},
|
||||
{
|
||||
key: <kbd>Up</kbd>,
|
||||
text: "Pan up by 100 pixels.",
|
||||
text: t("Pan up by 100 pixels.",)
|
||||
},
|
||||
{
|
||||
key: <kbd>Down</kbd>,
|
||||
text: "Pan down by 100 pixels.",
|
||||
text: t("Pan down by 100 pixels.",)
|
||||
},
|
||||
{
|
||||
key: <kbd>Left</kbd>,
|
||||
text: "Pan left by 100 pixels.",
|
||||
text: t("Pan left by 100 pixels.",)
|
||||
},
|
||||
{
|
||||
key: <kbd>Right</kbd>,
|
||||
text: "Pan right by 100 pixels.",
|
||||
text: t("Pan right by 100 pixels.",)
|
||||
},
|
||||
{
|
||||
key: <><kbd>Shift</kbd> + <kbd>Right</kbd></>,
|
||||
text: "Increase the rotation by 15 degrees.",
|
||||
text: t("Increase the rotation by 15 degrees.",)
|
||||
},
|
||||
{
|
||||
key: <><kbd>Shift</kbd> + <kbd>Left</kbd></>,
|
||||
text: "Decrease the rotation by 15 degrees."
|
||||
text: t("Decrease the rotation by 15 degrees.")
|
||||
},
|
||||
{
|
||||
key: <><kbd>Shift</kbd> + <kbd>Up</kbd></>,
|
||||
text: "Increase the pitch by 10 degrees."
|
||||
text: t("Increase the pitch by 10 degrees.")
|
||||
},
|
||||
{
|
||||
key: <><kbd>Shift</kbd> + <kbd>Down</kbd></>,
|
||||
text: "Decrease the pitch by 10 degrees."
|
||||
text: t("Decrease the pitch by 10 degrees.")
|
||||
},
|
||||
]
|
||||
|
||||
@@ -103,11 +105,13 @@ export default class ModalShortcuts extends React.Component<ModalShortcutsProps>
|
||||
data-wd-key="modal:shortcuts"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Shortcuts'}
|
||||
title={t('Shortcuts')}
|
||||
>
|
||||
<section className="maputnik-modal-section maputnik-modal-shortcuts">
|
||||
<p>
|
||||
Press <code>ESC</code> to lose focus of any active elements, then press one of:
|
||||
<Trans t={t}>
|
||||
Press <code>ESC</code> to lose focus of any active elements, then press one of:
|
||||
</Trans>
|
||||
</p>
|
||||
<dl>
|
||||
{help.map((item, idx) => {
|
||||
@@ -117,7 +121,7 @@ export default class ModalShortcuts extends React.Component<ModalShortcutsProps>
|
||||
</div>
|
||||
})}
|
||||
</dl>
|
||||
<p>If the Map is in focused you can use the following shortcuts</p>
|
||||
<p>{t("If the Map is in focused you can use the following shortcuts")}</p>
|
||||
<ul>
|
||||
{mapShortcuts.map((item, idx) => {
|
||||
return <li key={idx}>
|
||||
@@ -130,3 +134,5 @@ export default class ModalShortcuts extends React.Component<ModalShortcutsProps>
|
||||
}
|
||||
}
|
||||
|
||||
const ModalShortcuts = withTranslation()(ModalShortcutsInternal);
|
||||
export default ModalShortcuts;
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react'
|
||||
import {MdAddCircleOutline, MdDelete} from 'react-icons/md'
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import type {GeoJSONSourceSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, SourceSpecification, StyleSpecification, VectorSourceSpecification} from 'maplibre-gl'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import Modal from './Modal'
|
||||
import InputButton from './InputButton'
|
||||
@@ -74,16 +75,17 @@ type ActiveModalSourcesTypeEditorProps = {
|
||||
source: SourceSpecification
|
||||
onDelete(...args: unknown[]): unknown
|
||||
onChange(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
class ActiveModalSourcesTypeEditor extends React.Component<ActiveModalSourcesTypeEditorProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <div className="maputnik-active-source-type-editor">
|
||||
<div className="maputnik-active-source-type-editor-header">
|
||||
<span className="maputnik-active-source-type-editor-header-id">#{this.props.sourceId}</span>
|
||||
<span className="maputnik-space" />
|
||||
<InputButton
|
||||
aria-label={`Remove '${this.props.sourceId}' source`}
|
||||
aria-label={t("Remove '{{sourceId}}' source", {sourceId: this.props.sourceId})}
|
||||
className="maputnik-active-source-type-editor-header-delete"
|
||||
onClick={()=> this.props.onDelete(this.props.sourceId)}
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
@@ -104,7 +106,7 @@ class ActiveModalSourcesTypeEditor extends React.Component<ActiveModalSourcesTyp
|
||||
|
||||
type AddSourceProps = {
|
||||
onAdd(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
type AddSourceState = {
|
||||
mode: EditorMode
|
||||
@@ -202,6 +204,7 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
// Kind of a hack because the type changes, however maputnik has 1..n
|
||||
// options per type, for example
|
||||
//
|
||||
@@ -215,25 +218,25 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
|
||||
|
||||
return <div className="maputnik-add-source">
|
||||
<FieldString
|
||||
label={"Source ID"}
|
||||
fieldSpec={{doc: "Unique ID that identifies the source and is used in the layer to reference the source."}}
|
||||
label={t("Source ID")}
|
||||
fieldSpec={{doc: t("Unique ID that identifies the source and is used in the layer to reference the source.")}}
|
||||
value={this.state.sourceId}
|
||||
onChange={(v: string) => this.setState({ sourceId: v})}
|
||||
/>
|
||||
<FieldSelect
|
||||
label={"Source Type"}
|
||||
label={t("Source Type")}
|
||||
fieldSpec={sourceTypeFieldSpec}
|
||||
options={[
|
||||
['geojson_json', 'GeoJSON (JSON)'],
|
||||
['geojson_url', 'GeoJSON (URL)'],
|
||||
['tilejson_vector', 'Vector (TileJSON URL)'],
|
||||
['tilexyz_vector', 'Vector (XYZ URLs)'],
|
||||
['tilejson_raster', 'Raster (TileJSON URL)'],
|
||||
['tilexyz_raster', 'Raster (XYZ URL)'],
|
||||
['tilejson_raster-dem', 'Raster DEM (TileJSON URL)'],
|
||||
['tilexyz_raster-dem', 'Raster DEM (XYZ URLs)'],
|
||||
['image', 'Image'],
|
||||
['video', 'Video'],
|
||||
['geojson_json', t('GeoJSON (JSON)')],
|
||||
['geojson_url', t('GeoJSON (URL)')],
|
||||
['tilejson_vector', t('Vector (TileJSON URL)')],
|
||||
['tilexyz_vector', t('Vector (XYZ URLs)')],
|
||||
['tilejson_raster', t('Raster (TileJSON URL)')],
|
||||
['tilexyz_raster', t('Raster (XYZ URL)')],
|
||||
['tilejson_raster-dem', t('Raster DEM (TileJSON URL)')],
|
||||
['tilexyz_raster-dem', t('Raster DEM (XYZ URLs)')],
|
||||
['image', t('Image')],
|
||||
['video', t('Video')],
|
||||
]}
|
||||
onChange={mode => this.setState({mode: mode as EditorMode, source: this.defaultSource(mode as EditorMode)})}
|
||||
value={this.state.mode as string}
|
||||
@@ -247,20 +250,20 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
|
||||
className="maputnik-add-source-button"
|
||||
onClick={this.onAdd}
|
||||
>
|
||||
Add Source
|
||||
{t("Add Source")}
|
||||
</InputButton>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
type ModalSourcesProps = {
|
||||
type ModalSourcesInternalProps = {
|
||||
mapStyle: StyleSpecification
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
onStyleChanged(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class ModalSources extends React.Component<ModalSourcesProps> {
|
||||
class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
|
||||
stripTitle(source: SourceSpecification & {title?: string}): SourceSpecification {
|
||||
const strippedSource = {...source}
|
||||
delete strippedSource['title']
|
||||
@@ -268,7 +271,8 @@ export default class ModalSources extends React.Component<ModalSourcesProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const mapStyle = this.props.mapStyle
|
||||
const {t, mapStyle} = this.props;
|
||||
const i18nProps = {t, i18n: this.props.i18n, tReady: this.props.tReady};
|
||||
const activeSources = Object.keys(mapStyle.sources).map(sourceId => {
|
||||
const source = mapStyle.sources[sourceId]
|
||||
return <ActiveModalSourcesTypeEditor
|
||||
@@ -277,6 +281,7 @@ export default class ModalSources extends React.Component<ModalSourcesProps> {
|
||||
source={source}
|
||||
onChange={(src: SourceSpecification) => this.props.onStyleChanged(changeSource(mapStyle, sourceId, src))}
|
||||
onDelete={() => this.props.onStyleChanged(deleteSource(mapStyle, sourceId))}
|
||||
{...i18nProps}
|
||||
/>
|
||||
})
|
||||
|
||||
@@ -295,17 +300,17 @@ export default class ModalSources extends React.Component<ModalSourcesProps> {
|
||||
data-wd-key="modal:sources"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Sources'}
|
||||
title={t('Sources')}
|
||||
>
|
||||
<section className="maputnik-modal-section">
|
||||
<h1>Active Sources</h1>
|
||||
<h1>{t("Active Sources")}</h1>
|
||||
{activeSources}
|
||||
</section>
|
||||
|
||||
<section className="maputnik-modal-section">
|
||||
<h1>Choose Public Source</h1>
|
||||
<h1>{t("Choose Public Source")}</h1>
|
||||
<p>
|
||||
Add one of the publicly available sources to your style.
|
||||
{t("Add one of the publicly available sources to your style.")}
|
||||
</p>
|
||||
<div className="maputnik-public-sources" style={{maxWidth: 500}}>
|
||||
{tilesetOptions}
|
||||
@@ -313,13 +318,16 @@ export default class ModalSources extends React.Component<ModalSourcesProps> {
|
||||
</section>
|
||||
|
||||
<section className="maputnik-modal-section">
|
||||
<h1>Add New Source</h1>
|
||||
<p>Add a new source to your style. You can only choose the source type and id at creation time!</p>
|
||||
<h1>{t("Add New Source")}</h1>
|
||||
<p>{t("Add a new source to your style. You can only choose the source type and id at creation time!")}</p>
|
||||
<AddSource
|
||||
onAdd={(sourceId: string, source: SourceSpecification) => this.props.onStyleChanged(addSource(mapStyle, sourceId, source))}
|
||||
{...i18nProps}
|
||||
/>
|
||||
</section>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
const ModalSources = withTranslation()(ModalSourcesInternal);
|
||||
export default ModalSources;
|
||||
|
||||
@@ -8,6 +8,8 @@ import FieldDynamicArray from './FieldDynamicArray'
|
||||
import FieldArray from './FieldArray'
|
||||
import FieldJson from './FieldJson'
|
||||
import FieldCheckbox from './FieldCheckbox'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import { TFunction } from 'i18next'
|
||||
|
||||
export type EditorMode = "video" | "image" | "tilejson_vector" | "tilexyz_raster" | "tilejson_raster" | "tilexyz_raster-dem" | "tilejson_raster-dem" | "tilexyz_vector" | "geojson_url" | "geojson_json" | null;
|
||||
|
||||
@@ -17,14 +19,15 @@ type TileJSONSourceEditorProps = {
|
||||
}
|
||||
onChange(...args: unknown[]): unknown
|
||||
children?: React.ReactNode
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
|
||||
class TileJSONSourceEditor extends React.Component<TileJSONSourceEditorProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <div>
|
||||
<FieldUrl
|
||||
label={"TileJSON URL"}
|
||||
label={t("TileJSON URL")}
|
||||
fieldSpec={latest.source_vector.url}
|
||||
value={this.props.source.url}
|
||||
onChange={url => this.props.onChange({
|
||||
@@ -45,7 +48,7 @@ type TileURLSourceEditorProps = {
|
||||
}
|
||||
onChange(...args: unknown[]): unknown
|
||||
children?: React.ReactNode
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
|
||||
changeTileUrls(tiles: string[]) {
|
||||
@@ -58,7 +61,7 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
|
||||
renderTileUrls() {
|
||||
const tiles = this.props.source.tiles || [];
|
||||
return <FieldDynamicArray
|
||||
label={"Tile URL"}
|
||||
label={this.props.t("Tile URL")}
|
||||
fieldSpec={latest.source_vector.tiles}
|
||||
type="url"
|
||||
value={tiles}
|
||||
@@ -67,10 +70,11 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <div>
|
||||
{this.renderTileUrls()}
|
||||
<FieldNumber
|
||||
label={"Min Zoom"}
|
||||
label={t("Min Zoom")}
|
||||
fieldSpec={latest.source_vector.minzoom}
|
||||
value={this.props.source.minzoom || 0}
|
||||
onChange={minzoom => this.props.onChange({
|
||||
@@ -79,7 +83,7 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
|
||||
})}
|
||||
/>
|
||||
<FieldNumber
|
||||
label={"Max Zoom"}
|
||||
label={t("Max Zoom")}
|
||||
fieldSpec={latest.source_vector.maxzoom}
|
||||
value={this.props.source.maxzoom || 22}
|
||||
onChange={maxzoom => this.props.onChange({
|
||||
@@ -93,16 +97,24 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
|
||||
}
|
||||
}
|
||||
|
||||
const createCornerLabels: (t: TFunction) => { label: string, key: string }[] = (t) => ([
|
||||
{ label: t("Coord top left"), key: "top left" },
|
||||
{ label: t("Coord top right"), key: "top right" },
|
||||
{ label: t("Coord bottom right"), key: "bottom right" },
|
||||
{ label: t("Coord bottom left"), key: "bottom left" },
|
||||
]);
|
||||
|
||||
type ImageSourceEditorProps = {
|
||||
source: {
|
||||
coordinates: [number, number][]
|
||||
url: string
|
||||
}
|
||||
onChange(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const changeCoord = (idx: number, val: [number, number]) => {
|
||||
const coordinates = this.props.source.coordinates.slice(0);
|
||||
coordinates[idx] = val;
|
||||
@@ -115,7 +127,7 @@ class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
|
||||
|
||||
return <div>
|
||||
<FieldUrl
|
||||
label={"Image URL"}
|
||||
label={t("Image URL")}
|
||||
fieldSpec={latest.source_image.url}
|
||||
value={this.props.source.url}
|
||||
onChange={url => this.props.onChange({
|
||||
@@ -123,11 +135,11 @@ class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
|
||||
url,
|
||||
})}
|
||||
/>
|
||||
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
|
||||
{createCornerLabels(t).map(({label, key}, idx) => {
|
||||
return (
|
||||
<FieldArray
|
||||
label={`Coord ${label}`}
|
||||
key={label}
|
||||
label={label}
|
||||
key={key}
|
||||
length={2}
|
||||
type="number"
|
||||
value={this.props.source.coordinates[idx]}
|
||||
@@ -146,10 +158,11 @@ type VideoSourceEditorProps = {
|
||||
urls: string[]
|
||||
}
|
||||
onChange(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
class VideoSourceEditor extends React.Component<VideoSourceEditorProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const changeCoord = (idx: number, val: [number, number]) => {
|
||||
const coordinates = this.props.source.coordinates.slice(0);
|
||||
coordinates[idx] = val;
|
||||
@@ -169,18 +182,18 @@ class VideoSourceEditor extends React.Component<VideoSourceEditorProps> {
|
||||
|
||||
return <div>
|
||||
<FieldDynamicArray
|
||||
label={"Video URL"}
|
||||
label={t("Video URL")}
|
||||
fieldSpec={latest.source_video.urls}
|
||||
type="string"
|
||||
value={this.props.source.urls}
|
||||
default={[]}
|
||||
onChange={changeUrls}
|
||||
/>
|
||||
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
|
||||
{createCornerLabels(t).map(({label, key}, idx) => {
|
||||
return (
|
||||
<FieldArray
|
||||
label={`Coord ${label}`}
|
||||
key={label}
|
||||
label={label}
|
||||
key={key}
|
||||
length={2}
|
||||
type="number"
|
||||
value={this.props.source.coordinates[idx]}
|
||||
@@ -198,12 +211,13 @@ type GeoJSONSourceUrlEditorProps = {
|
||||
data: string
|
||||
}
|
||||
onChange(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
class GeoJSONSourceUrlEditor extends React.Component<GeoJSONSourceUrlEditorProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <FieldUrl
|
||||
label={"GeoJSON URL"}
|
||||
label={t("GeoJSON URL")}
|
||||
fieldSpec={latest.source_geojson.data}
|
||||
value={this.props.source.data}
|
||||
onChange={data => this.props.onChange({
|
||||
@@ -220,12 +234,13 @@ type GeoJSONSourceFieldJsonEditorProps = {
|
||||
cluster: boolean
|
||||
}
|
||||
onChange(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJsonEditorProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <div>
|
||||
<Block label={"GeoJSON"} fieldSpec={latest.source_geojson.data}>
|
||||
<Block label={t("GeoJSON")} fieldSpec={latest.source_geojson.data}>
|
||||
<FieldJson
|
||||
layer={this.props.source.data}
|
||||
maxHeight={200}
|
||||
@@ -243,7 +258,7 @@ class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJso
|
||||
/>
|
||||
</Block>
|
||||
<FieldCheckbox
|
||||
label={'Cluster'}
|
||||
label={t('Cluster')}
|
||||
value={this.props.source.cluster}
|
||||
onChange={cluster => {
|
||||
this.props.onChange({
|
||||
@@ -256,18 +271,22 @@ class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJso
|
||||
}
|
||||
}
|
||||
|
||||
type ModalSourcesTypeEditorProps = {
|
||||
type ModalSourcesTypeEditorInternalProps = {
|
||||
mode: EditorMode
|
||||
source: any
|
||||
onChange(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class ModalSourcesTypeEditor extends React.Component<ModalSourcesTypeEditorProps> {
|
||||
class ModalSourcesTypeEditorInternal extends React.Component<ModalSourcesTypeEditorInternalProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const commonProps = {
|
||||
source: this.props.source,
|
||||
onChange: this.props.onChange,
|
||||
}
|
||||
t: this.props.t,
|
||||
i18n: this.props.i18n,
|
||||
tReady: this.props.tReady,
|
||||
};
|
||||
switch(this.props.mode) {
|
||||
case 'geojson_url': return <GeoJSONSourceUrlEditor {...commonProps} />
|
||||
case 'geojson_json': return <GeoJSONSourceFieldJsonEditor {...commonProps} />
|
||||
@@ -278,7 +297,7 @@ export default class ModalSourcesTypeEditor extends React.Component<ModalSources
|
||||
case 'tilejson_raster-dem': return <TileJSONSourceEditor {...commonProps} />
|
||||
case 'tilexyz_raster-dem': return <TileURLSourceEditor {...commonProps}>
|
||||
<FieldSelect
|
||||
label={"Encoding"}
|
||||
label={t("Encoding")}
|
||||
fieldSpec={latest.source_raster_dem.encoding}
|
||||
options={Object.keys(latest.source_raster_dem.encoding.values)}
|
||||
onChange={encoding => this.props.onChange({
|
||||
@@ -295,3 +314,5 @@ export default class ModalSourcesTypeEditor extends React.Component<ModalSources
|
||||
}
|
||||
}
|
||||
|
||||
const ModalSourcesTypeEditor = withTranslation()(ModalSourcesTypeEditorInternal);
|
||||
export default ModalSourcesTypeEditor;
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import InputButton from './InputButton'
|
||||
import Modal from './Modal'
|
||||
|
||||
// @ts-ignore
|
||||
import logoImage from 'maputnik-design/logos/logo-color.svg'
|
||||
|
||||
type ModalSurveyProps = {
|
||||
isOpen: boolean
|
||||
onOpenToggle(...args: unknown[]): unknown
|
||||
};
|
||||
|
||||
export default class ModalSurvey extends React.Component<ModalSurveyProps> {
|
||||
onClick = () => {
|
||||
window.open('https://gregorywolanski.typeform.com/to/cPgaSY', '_blank');
|
||||
|
||||
this.props.onOpenToggle();
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Modal
|
||||
data-wd-key="modal:survey"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title="Maputnik Survey"
|
||||
>
|
||||
<div className="maputnik-modal-survey">
|
||||
<img src={logoImage} className="maputnik-modal-survey__logo" />
|
||||
<h1>You + Maputnik = Maputnik better for you</h1>
|
||||
<p className="maputnik-modal-survey__description">We don’t track you, so we don’t know how you use Maputnik. Help us make Maputnik better for you by completing a 7–minute survey carried out by our contributing designer.</p>
|
||||
<InputButton onClick={this.onClick} className="maputnik-big-button maputnik-white-button maputnik-wide-button">Take the Maputnik Survey</InputButton>
|
||||
<p className="maputnik-modal-survey__footnote">It takes 7 minutes, tops! Every question is optional.</p>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import React from 'react'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import './SmallError.scss';
|
||||
|
||||
|
||||
type SmallErrorProps = {
|
||||
type SmallErrorInternalProps = {
|
||||
children?: React.ReactNode
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
|
||||
export default class SmallError extends React.Component<SmallErrorProps> {
|
||||
class SmallErrorInternal extends React.Component<SmallErrorInternalProps> {
|
||||
render () {
|
||||
const t = this.props.t;
|
||||
return (
|
||||
<div className="SmallError">
|
||||
Error: {this.props.children}
|
||||
{t("Error:")} {this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SmallError = withTranslation()(SmallErrorInternal);
|
||||
export default SmallError;
|
||||
|
||||
@@ -11,13 +11,14 @@ import Block from './Block'
|
||||
import docUid from '../libs/document-uid'
|
||||
import sortNumerically from '../libs/sort-numerically'
|
||||
import {findDefaultFromSpec} from '../libs/spec-helper';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import labelFromFieldName from '../libs/label-from-field-name'
|
||||
import DeleteStopButton from './_DeleteStopButton'
|
||||
|
||||
|
||||
|
||||
function setStopRefs(props: DataPropertyProps, state: DataPropertyState) {
|
||||
function setStopRefs(props: DataPropertyInternalProps, state: DataPropertyState) {
|
||||
// This is initialsed below only if required to improved performance.
|
||||
let newRefs: {[key: number]: string} | undefined;
|
||||
|
||||
@@ -35,7 +36,7 @@ function setStopRefs(props: DataPropertyProps, state: DataPropertyState) {
|
||||
return newRefs;
|
||||
}
|
||||
|
||||
type DataPropertyProps = {
|
||||
type DataPropertyInternalProps = {
|
||||
onChange?(fieldName: string, value: any): unknown
|
||||
onDeleteStop?(...args: unknown[]): unknown
|
||||
onAddStop?(...args: unknown[]): unknown
|
||||
@@ -46,7 +47,7 @@ type DataPropertyProps = {
|
||||
fieldSpec?: object
|
||||
value?: DataPropertyValue
|
||||
errors?: object
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
type DataPropertyState = {
|
||||
refs: {[key: number]: string}
|
||||
@@ -65,7 +66,7 @@ export type Stop = [{
|
||||
value: number
|
||||
}, number]
|
||||
|
||||
export default class DataProperty extends React.Component<DataPropertyProps, DataPropertyState> {
|
||||
class DataPropertyInternal extends React.Component<DataPropertyInternalProps, DataPropertyState> {
|
||||
state = {
|
||||
refs: {} as {[key: number]: string}
|
||||
}
|
||||
@@ -80,7 +81,7 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: DataPropertyProps, state: DataPropertyState) {
|
||||
static getDerivedStateFromProps(props: Readonly<DataPropertyInternalProps>, state: DataPropertyState) {
|
||||
const newRefs = setStopRefs(props, state);
|
||||
if(newRefs) {
|
||||
return {
|
||||
@@ -213,6 +214,8 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
if (typeof this.props.value?.type === "undefined") {
|
||||
this.props.value!.type = this.getFieldFunctionType(this.props.fieldSpec)
|
||||
}
|
||||
@@ -227,8 +230,8 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
|
||||
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />
|
||||
|
||||
const dataProps = {
|
||||
'aria-label': "Input value",
|
||||
label: "Data value",
|
||||
'aria-label': t("Input value"),
|
||||
label: t("Data value"),
|
||||
value: dataLevel as any,
|
||||
onChange: (newData: string | number | undefined) => this.changeStop(idx, { zoom: zoomLevel, value: newData as number }, value)
|
||||
}
|
||||
@@ -263,7 +266,7 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
|
||||
</td>
|
||||
<td>
|
||||
<InputSpec
|
||||
aria-label="Output value"
|
||||
aria-label={t("Output value")}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={value}
|
||||
@@ -282,21 +285,21 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
|
||||
<legend>{labelFromFieldName(this.props.fieldName)}</legend>
|
||||
<div className="maputnik-data-fieldset-inner">
|
||||
<Block
|
||||
label={"Function"}
|
||||
label={t("Function")}
|
||||
key="function"
|
||||
>
|
||||
<div className="maputnik-data-spec-property-input">
|
||||
<InputSelect
|
||||
value={this.props.value!.type}
|
||||
onChange={(propVal: string) => this.changeDataType(propVal)}
|
||||
title={"Select a type of data scale (default is 'categorical')."}
|
||||
title={t("Select a type of data scale (default is 'categorical').")}
|
||||
options={this.getDataFunctionTypes(this.props.fieldSpec)}
|
||||
/>
|
||||
</div>
|
||||
</Block>
|
||||
{this.props.value?.type !== "identity" &&
|
||||
<Block
|
||||
label={"Base"}
|
||||
label={t("Base")}
|
||||
key="base"
|
||||
>
|
||||
<div className="maputnik-data-spec-property-input">
|
||||
@@ -316,14 +319,14 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
|
||||
<div className="maputnik-data-spec-property-input">
|
||||
<InputString
|
||||
value={this.props.value?.property}
|
||||
title={"Input a data property to base styles off of."}
|
||||
title={t("Input a data property to base styles off of.")}
|
||||
onChange={propVal => this.changeDataProperty("property", propVal)}
|
||||
/>
|
||||
</div>
|
||||
</Block>
|
||||
{dataFields &&
|
||||
<Block
|
||||
label={"Default"}
|
||||
label={t("Default")}
|
||||
key="default"
|
||||
>
|
||||
<InputSpec
|
||||
@@ -337,12 +340,12 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
|
||||
{dataFields &&
|
||||
<div className="maputnik-function-stop">
|
||||
<table className="maputnik-function-stop-table">
|
||||
<caption>Stops</caption>
|
||||
<caption>{t("Stops")}</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zoom</th>
|
||||
<th>Input value</th>
|
||||
<th rowSpan={2}>Output value</th>
|
||||
<th>{t("Zoom")}</th>
|
||||
<th>{t("Input value")}</th>
|
||||
<th rowSpan={2}>{t("Output value")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -359,7 +362,7 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
||||
</svg> Add stop
|
||||
</svg> {t("Add stop")}
|
||||
</InputButton>
|
||||
}
|
||||
<InputButton
|
||||
@@ -368,7 +371,7 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||
</svg> Convert to expression
|
||||
</svg> {t("Convert to expression")}
|
||||
</InputButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -376,3 +379,6 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const DataProperty = withTranslation()(DataPropertyInternal);
|
||||
export default DataProperty;
|
||||
|
||||
@@ -2,21 +2,26 @@ import React from 'react'
|
||||
|
||||
import InputButton from './InputButton'
|
||||
import {MdDelete} from 'react-icons/md'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
type DeleteStopButtonProps = {
|
||||
type DeleteStopButtonInternalProps = {
|
||||
onClick?(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
|
||||
export default class DeleteStopButton extends React.Component<DeleteStopButtonProps> {
|
||||
class DeleteStopButtonInternal extends React.Component<DeleteStopButtonInternalProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <InputButton
|
||||
className="maputnik-delete-stop"
|
||||
onClick={this.props.onClick}
|
||||
title={"Remove zoom level from stop"}
|
||||
title={t("Remove zoom level from stop")}
|
||||
>
|
||||
<MdDelete />
|
||||
</InputButton>
|
||||
}
|
||||
}
|
||||
|
||||
const DeleteStopButton = withTranslation()(DeleteStopButtonInternal);
|
||||
export default DeleteStopButton;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
import {MdDelete, MdUndo} from 'react-icons/md'
|
||||
import stringifyPretty from 'json-stringify-pretty-compact'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import Block from './Block'
|
||||
import InputButton from './InputButton'
|
||||
@@ -8,7 +9,7 @@ import labelFromFieldName from '../libs/label-from-field-name'
|
||||
import FieldJson from './FieldJson'
|
||||
|
||||
|
||||
type ExpressionPropertyProps = {
|
||||
type ExpressionPropertyInternalProps = {
|
||||
onDelete?(...args: unknown[]): unknown
|
||||
fieldName: string
|
||||
fieldType?: string
|
||||
@@ -20,20 +21,20 @@ type ExpressionPropertyProps = {
|
||||
canUndo?(...args: unknown[]): unknown
|
||||
onFocus?(...args: unknown[]): unknown
|
||||
onBlur?(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
type ExpressionPropertyState = {
|
||||
jsonError: boolean
|
||||
};
|
||||
|
||||
export default class ExpressionProperty extends React.Component<ExpressionPropertyProps, ExpressionPropertyState> {
|
||||
class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInternalProps, ExpressionPropertyState> {
|
||||
static defaultProps = {
|
||||
errors: {},
|
||||
onFocus: () => {},
|
||||
onBlur: () => {},
|
||||
}
|
||||
|
||||
constructor (props:ExpressionPropertyProps) {
|
||||
constructor(props: ExpressionPropertyInternalProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
jsonError: false,
|
||||
@@ -53,7 +54,7 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
|
||||
}
|
||||
|
||||
render() {
|
||||
const {errors, fieldName, fieldType, value, canUndo} = this.props;
|
||||
const {t, errors, fieldName, fieldType, value, canUndo} = this.props;
|
||||
const {jsonError} = this.state;
|
||||
const undoDisabled = canUndo ? !canUndo() : true;
|
||||
|
||||
@@ -65,7 +66,7 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
|
||||
onClick={this.props.onUndo}
|
||||
disabled={undoDisabled}
|
||||
className="maputnik-delete-stop"
|
||||
title="Revert from expression"
|
||||
title={t("Revert from expression")}
|
||||
>
|
||||
<MdUndo />
|
||||
</InputButton>
|
||||
@@ -74,7 +75,7 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
|
||||
key="delete_action"
|
||||
onClick={this.props.onDelete}
|
||||
className="maputnik-delete-stop"
|
||||
title="Delete expression"
|
||||
title={t("Delete expression")}
|
||||
>
|
||||
<MdDelete />
|
||||
</InputButton>
|
||||
@@ -112,7 +113,7 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
|
||||
// this feels like an incorrect type...? `foundErrors` is an array of objects, not a single object
|
||||
error={foundErrors as any}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
label={labelFromFieldName(this.props.fieldName)}
|
||||
label={t(labelFromFieldName(this.props.fieldName))}
|
||||
action={deleteStopBtn}
|
||||
wideMode={true}
|
||||
>
|
||||
@@ -137,3 +138,6 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
const ExpressionProperty = withTranslation()(ExpressionPropertyInternal);
|
||||
export default ExpressionProperty;
|
||||
|
||||
@@ -3,16 +3,18 @@ import React from 'react'
|
||||
import InputButton from './InputButton'
|
||||
import {MdFunctions, MdInsertChart} from 'react-icons/md'
|
||||
import {mdiFunctionVariant} from '@mdi/js';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
type FunctionInputButtonsProps = {
|
||||
type FunctionInputButtonsInternalProps = {
|
||||
fieldSpec?: any
|
||||
onZoomClick?(...args: unknown[]): unknown
|
||||
onDataClick?(...args: unknown[]): unknown
|
||||
onExpressionClick?(...args: unknown[]): unknown
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
export default class FunctionInputButtons extends React.Component<FunctionInputButtonsProps> {
|
||||
class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsInternalProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
let makeZoomInputButton, makeDataInputButton, expressionInputButton;
|
||||
|
||||
if (this.props.fieldSpec.expression.parameters.includes('zoom')) {
|
||||
@@ -20,7 +22,7 @@ export default class FunctionInputButtons extends React.Component<FunctionInputB
|
||||
<InputButton
|
||||
className="maputnik-make-zoom-function"
|
||||
onClick={this.props.onExpressionClick}
|
||||
title="Convert to expression"
|
||||
title={t("Convert to expression")}
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||
@@ -31,7 +33,7 @@ export default class FunctionInputButtons extends React.Component<FunctionInputB
|
||||
makeZoomInputButton = <InputButton
|
||||
className="maputnik-make-zoom-function"
|
||||
onClick={this.props.onZoomClick}
|
||||
title="Convert property into a zoom function"
|
||||
title={t("Convert property into a zoom function")}
|
||||
>
|
||||
<MdFunctions />
|
||||
</InputButton>
|
||||
@@ -40,7 +42,7 @@ export default class FunctionInputButtons extends React.Component<FunctionInputB
|
||||
makeDataInputButton = <InputButton
|
||||
className="maputnik-make-data-function"
|
||||
onClick={this.props.onDataClick}
|
||||
title="Convert property to data function"
|
||||
title={t("Convert property to data function")}
|
||||
>
|
||||
<MdInsertChart />
|
||||
</InputButton>
|
||||
@@ -56,3 +58,6 @@ export default class FunctionInputButtons extends React.Component<FunctionInputB
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const FunctionInputButtons = withTranslation()(FunctionInputButtonsInternal);
|
||||
export default FunctionInputButtons;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
|
||||
import InputButton from './InputButton'
|
||||
import InputSpec from './InputSpec'
|
||||
@@ -20,7 +21,7 @@ import sortNumerically from '../libs/sort-numerically'
|
||||
*
|
||||
* When the stops are reordered the references are also updated (see this.orderStops) this allows React to use the same key for the element and keep keyboard focus.
|
||||
*/
|
||||
function setStopRefs(props: ZoomPropertyProps, state: ZoomPropertyState) {
|
||||
function setStopRefs(props: ZoomPropertyInternalProps, state: ZoomPropertyState) {
|
||||
// This is initialsed below only if required to improved performance.
|
||||
let newRefs: {[key: number]: string} = {};
|
||||
|
||||
@@ -45,7 +46,7 @@ type ZoomWithStops = {
|
||||
}
|
||||
|
||||
|
||||
type ZoomPropertyProps = {
|
||||
type ZoomPropertyInternalProps = {
|
||||
onChange?(...args: unknown[]): unknown
|
||||
onChangeToDataFunction?(...args: unknown[]): unknown
|
||||
onDeleteStop?(...args: unknown[]): unknown
|
||||
@@ -59,13 +60,13 @@ type ZoomPropertyProps = {
|
||||
}
|
||||
errors?: object
|
||||
value?: ZoomWithStops
|
||||
};
|
||||
} & WithTranslation;
|
||||
|
||||
type ZoomPropertyState = {
|
||||
refs: {[key: number]: string}
|
||||
}
|
||||
|
||||
export default class ZoomProperty extends React.Component<ZoomPropertyProps, ZoomPropertyState> {
|
||||
class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, ZoomPropertyState> {
|
||||
static defaultProps = {
|
||||
errors: {},
|
||||
}
|
||||
@@ -84,7 +85,7 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: ZoomPropertyProps, state: ZoomPropertyState) {
|
||||
static getDerivedStateFromProps(props: Readonly<ZoomPropertyInternalProps>, state: ZoomPropertyState) {
|
||||
const newRefs = setStopRefs(props, state);
|
||||
if(newRefs) {
|
||||
return {
|
||||
@@ -152,17 +153,17 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const zoomFields = this.props.value?.stops.map((stop, idx) => {
|
||||
const zoomLevel = stop[0]
|
||||
const key = this.state.refs[idx];
|
||||
const value = stop[1]
|
||||
const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />
|
||||
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />
|
||||
return <tr
|
||||
key={key}
|
||||
key={`${stop[0]}-${stop[1]}`}
|
||||
>
|
||||
<td>
|
||||
<InputNumber
|
||||
aria-label="Zoom"
|
||||
aria-label={t("Zoom")}
|
||||
value={zoomLevel}
|
||||
onChange={changedStop => this.changeZoomStop(idx, changedStop, value)}
|
||||
min={0}
|
||||
@@ -171,7 +172,7 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
|
||||
</td>
|
||||
<td>
|
||||
<InputSpec
|
||||
aria-label="Output value"
|
||||
aria-label={t("Output value")}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec as any}
|
||||
value={value}
|
||||
@@ -190,19 +191,19 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
|
||||
<legend>{labelFromFieldName(this.props.fieldName)}</legend>
|
||||
<div className="maputnik-data-fieldset-inner">
|
||||
<Block
|
||||
label={"Function"}
|
||||
label={t("Function")}
|
||||
>
|
||||
<div className="maputnik-data-spec-property-input">
|
||||
<InputSelect
|
||||
value={"interpolate"}
|
||||
onChange={(propVal: string) => this.changeDataType(propVal)}
|
||||
title={"Select a type of data scale (default is 'categorical')."}
|
||||
title={t("Select a type of data scale (default is 'categorical').")}
|
||||
options={this.getDataFunctionTypes(this.props.fieldSpec!)}
|
||||
/>
|
||||
</div>
|
||||
</Block>
|
||||
<Block
|
||||
label={"Base"}
|
||||
label={t("Base")}
|
||||
>
|
||||
<div className="maputnik-data-spec-property-input">
|
||||
<InputSpec
|
||||
@@ -215,11 +216,11 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
|
||||
</Block>
|
||||
<div className="maputnik-function-stop">
|
||||
<table className="maputnik-function-stop-table maputnik-function-stop-table--zoom">
|
||||
<caption>Stops</caption>
|
||||
<caption>{t("Stops")}</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zoom</th>
|
||||
<th rowSpan={2}>Output value</th>
|
||||
<th>{t("Zoom")}</th>
|
||||
<th rowSpan={2}>{t("Output value")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -234,7 +235,7 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
||||
</svg> Add stop
|
||||
</svg> {t("Add stop")}
|
||||
</InputButton>
|
||||
<InputButton
|
||||
className="maputnik-add-stop"
|
||||
@@ -242,7 +243,7 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||
</svg> Convert to expression
|
||||
</svg> {t("Convert to expression")}
|
||||
</InputButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -262,3 +263,6 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ZoomProperty = withTranslation()(ZoomPropertyInternal);
|
||||
export default ZoomProperty;
|
||||
|
||||
Reference in New Issue
Block a user