mirror of
https://github.com/maputnik/editor.git
synced 2026-08-25 06:27:25 +00:00
Compare commits
5 Commits
533f647c71
...
3ddb55aec7
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ddb55aec7 | |||
| 2fef0467b6 | |||
| eb985f4d95 | |||
| c486aa2139 | |||
| 244dbfb194 |
@@ -0,0 +1,43 @@
|
||||
Maputnik is a MapLibre style editor written using React and TypeScript.
|
||||
|
||||
To get started, install all npm packages:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
Verify code correctness by running ESLint:
|
||||
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
Or try fixing lint issues with:
|
||||
|
||||
```
|
||||
npm run lint -- --fix
|
||||
```
|
||||
|
||||
The project type checked and built with:
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
To run the tests make sure that xvfb is installed:
|
||||
|
||||
```
|
||||
apt run xvfb
|
||||
```
|
||||
|
||||
Run the development server in the background with Vite:
|
||||
|
||||
```
|
||||
nohup npm run start &
|
||||
```
|
||||
|
||||
Then start the Cypress tests with:
|
||||
|
||||
```
|
||||
xvfb-run -a npm run test
|
||||
```
|
||||
@@ -14,6 +14,8 @@
|
||||
- When loading a style into localStorage that causes a QuotaExceededError, purge localStorage and retry
|
||||
- Remove react-autobind dependency
|
||||
- Remove usage of legacy `childContextTypes` API
|
||||
- Refactor Field components to use arrow function syntax
|
||||
- Replace react-autocomplete with Downshift in the autocomplete component
|
||||
- _...Add new stuff here..._
|
||||
|
||||
### 🐞 Bug fixes
|
||||
|
||||
@@ -8,6 +8,7 @@ export default defineConfig({
|
||||
exclude: "cypress/**/*.*",
|
||||
},
|
||||
},
|
||||
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
// implement node event listeners here
|
||||
@@ -20,4 +21,11 @@ export default defineConfig({
|
||||
openMode: 0,
|
||||
},
|
||||
},
|
||||
|
||||
component: {
|
||||
devServer: {
|
||||
framework: "react",
|
||||
bundler: "vite",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Components App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div data-cy-root></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,37 @@
|
||||
// ***********************************************************
|
||||
// This example support/component.ts is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
import { mount } from 'cypress/react'
|
||||
|
||||
// Augment the Cypress namespace to include type definitions for
|
||||
// your custom command.
|
||||
// Alternatively, can be defined in cypress/support/component.d.ts
|
||||
// with a <reference path="./component" /> at the top of your spec.
|
||||
declare global {
|
||||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
mount: typeof mount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add('mount', mount)
|
||||
|
||||
// Example use:
|
||||
// cy.mount(<MyComponent />)
|
||||
Generated
+30
-32
@@ -22,6 +22,7 @@
|
||||
"codemirror": "^5.65.19",
|
||||
"color": "^5.0.0",
|
||||
"detect-browser": "^5.3.0",
|
||||
"downshift": "^9.0.9",
|
||||
"events": "^3.3.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^25.3.1",
|
||||
@@ -46,7 +47,6 @@
|
||||
"react-accessible-accordion": "^5.0.1",
|
||||
"react-aria-menubutton": "^7.0.3",
|
||||
"react-aria-modal": "^5.0.2",
|
||||
"react-autocomplete": "^1.8.1",
|
||||
"react-collapse": "^5.1.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -82,7 +82,6 @@
|
||||
"@types/react": "^18.2.67",
|
||||
"@types/react-aria-menubutton": "^6.2.14",
|
||||
"@types/react-aria-modal": "^5.0.0",
|
||||
"@types/react-autocomplete": "^1.8.11",
|
||||
"@types/react-collapse": "^5.0.4",
|
||||
"@types/react-color": "^3.0.13",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
@@ -3114,16 +3113,6 @@
|
||||
"focus-trap": "^7.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-autocomplete": {
|
||||
"version": "1.8.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-autocomplete/-/react-autocomplete-1.8.11.tgz",
|
||||
"integrity": "sha512-JYaD/OGfVFMK5NaGOCAd25QRs4MEevapn38xvYWjwo5brxrUMo2PucYmShTfuTX99r80UtncCMDrFZ9MGIVyvQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-collapse": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-collapse/-/react-collapse-5.0.4.tgz",
|
||||
@@ -4690,6 +4679,12 @@
|
||||
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/compute-scroll-into-view": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
|
||||
"integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -5211,11 +5206,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-scroll-into-view": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-scroll-into-view/-/dom-scroll-into-view-1.0.1.tgz",
|
||||
"integrity": "sha512-1Dmy6uH1vRcm2+Lvggyrlc04cMh+mr+VA+qcgs085hAEZp+v+6NT/xhRjfc6vRc7965sCSDdQcw063VkG+eNmQ=="
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
@@ -5281,6 +5271,28 @@
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/downshift": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/downshift/-/downshift-9.0.9.tgz",
|
||||
"integrity": "sha512-ygOT8blgiz5liDuEFAIaPeU4dDEa+w9p6PHVUisPIjrkF5wfR59a52HpGWAVVMoWnoFO8po2mZSScKZueihS7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.24.5",
|
||||
"compute-scroll-into-view": "^3.1.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-is": "18.2.0",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/downshift/node_modules/react-is": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -10771,19 +10783,6 @@
|
||||
"react-dom": ">=16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-autocomplete": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/react-autocomplete/-/react-autocomplete-1.8.1.tgz",
|
||||
"integrity": "sha512-YQGVN5POdcI3G89wUVWnJhk9rLF6JeB6Ik6xnNpfvSMG4tJkksBzqOE4mkFNGqEz+2AaQw13xNmVXresg9E3zg==",
|
||||
"dependencies": {
|
||||
"dom-scroll-into-view": "1.0.1",
|
||||
"prop-types": "^15.5.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^0.14.7 || ^15.0.0-0 || ^16.0.0-0",
|
||||
"react-dom": "^0.14.7 || ^15.0.0-0 || ^16.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-collapse": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-collapse/-/react-collapse-5.1.1.tgz",
|
||||
@@ -12662,8 +12661,7 @@
|
||||
"node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
|
||||
+1
-2
@@ -60,7 +60,7 @@
|
||||
"react-accessible-accordion": "^5.0.1",
|
||||
"react-aria-menubutton": "^7.0.3",
|
||||
"react-aria-modal": "^5.0.2",
|
||||
"react-autocomplete": "^1.8.1",
|
||||
"downshift": "^9.0.9",
|
||||
"react-collapse": "^5.1.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -113,7 +113,6 @@
|
||||
"@types/react": "^18.2.67",
|
||||
"@types/react-aria-menubutton": "^6.2.14",
|
||||
"@types/react-aria-modal": "^5.0.0",
|
||||
"@types/react-autocomplete": "^1.8.11",
|
||||
"@types/react-collapse": "^5.0.4",
|
||||
"@types/react-color": "^3.0.13",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
|
||||
@@ -877,8 +877,8 @@ export default class App extends React.Component<any, AppState> {
|
||||
this.setModal(modalName, !this.state.isOpen[modalName]);
|
||||
}
|
||||
|
||||
onSetFileHandle(fileHandle: FileSystemFileHandle | null) {
|
||||
this.setState({fileHandle: fileHandle});
|
||||
onSetFileHandle = (fileHandle: FileSystemFileHandle | null) => {
|
||||
this.setState({ fileHandle });
|
||||
}
|
||||
|
||||
onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import InputArray, { FieldArrayProps as InputArrayProps } from './InputArray'
|
||||
import Fieldset from './Fieldset'
|
||||
|
||||
@@ -9,10 +8,12 @@ type FieldArrayProps = InputArrayProps & {
|
||||
}
|
||||
};
|
||||
|
||||
export default class FieldArray extends React.Component<FieldArrayProps> {
|
||||
render() {
|
||||
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputArray {...this.props} />
|
||||
const FieldArray: React.FC<FieldArrayProps> = (props) => {
|
||||
return (
|
||||
<Fieldset label={props.label} fieldSpec={props.fieldSpec}>
|
||||
<InputArray {...props} />
|
||||
</Fieldset>
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldArray;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
|
||||
|
||||
@@ -8,10 +7,12 @@ type FieldAutocompleteProps = InputAutocompleteProps & {
|
||||
};
|
||||
|
||||
|
||||
export default class FieldAutocomplete extends React.Component<FieldAutocompleteProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label}>
|
||||
<InputAutocomplete {...this.props} />
|
||||
const FieldAutocomplete: React.FC<FieldAutocompleteProps> = (props) => {
|
||||
return (
|
||||
<Block label={props.label}>
|
||||
<InputAutocomplete {...props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldAutocomplete;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import InputCheckbox, {InputCheckboxProps} from './InputCheckbox'
|
||||
|
||||
@@ -8,10 +7,12 @@ type FieldCheckboxProps = InputCheckboxProps & {
|
||||
};
|
||||
|
||||
|
||||
export default class FieldCheckbox extends React.Component<FieldCheckboxProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label}>
|
||||
<InputCheckbox {...this.props} />
|
||||
const FieldCheckbox: React.FC<FieldCheckboxProps> = (props) => {
|
||||
return (
|
||||
<Block label={props.label}>
|
||||
<InputCheckbox {...props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldCheckbox;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import InputColor, {InputColorProps} from './InputColor'
|
||||
|
||||
@@ -11,10 +10,12 @@ type FieldColorProps = InputColorProps & {
|
||||
};
|
||||
|
||||
|
||||
export default class FieldColor extends React.Component<FieldColorProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputColor {...this.props} />
|
||||
const FieldColor: React.FC<FieldColorProps> = (props) => {
|
||||
return (
|
||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
||||
<InputColor {...props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldColor;
|
||||
|
||||
@@ -10,29 +10,31 @@ type FieldCommentInternalProps = {
|
||||
error: {message: string}
|
||||
} & WithTranslation;
|
||||
|
||||
class FieldCommentInternal extends React.Component<FieldCommentInternalProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const fieldSpec = {
|
||||
doc: t("Comments for the current layer. This is non-standard and not in the spec."),
|
||||
};
|
||||
const FieldCommentInternal: React.FC<FieldCommentInternalProps> = (props) => {
|
||||
const t = props.t;
|
||||
const fieldSpec = {
|
||||
doc: t(
|
||||
"Comments for the current layer. This is non-standard and not in the spec."
|
||||
),
|
||||
};
|
||||
|
||||
return <Block
|
||||
return (
|
||||
<Block
|
||||
label={t("Comments")}
|
||||
fieldSpec={fieldSpec}
|
||||
data-wd-key="layer-comment"
|
||||
error={this.props.error}
|
||||
error={props.error}
|
||||
>
|
||||
<InputString
|
||||
multi={true}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
default={t("Comment...")}
|
||||
data-wd-key="layer-comment.input"
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const FieldComment = withTranslation()(FieldCommentInternal);
|
||||
export default FieldComment;
|
||||
|
||||
@@ -9,57 +9,45 @@ type FieldDocLabelProps = {
|
||||
onToggleDoc?(...args: unknown[]): unknown
|
||||
};
|
||||
|
||||
type FieldDocLabelState = {
|
||||
open: boolean
|
||||
};
|
||||
|
||||
export default class FieldDocLabel extends React.Component<FieldDocLabelProps, FieldDocLabelState> {
|
||||
constructor (props: FieldDocLabelProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
open: false,
|
||||
const FieldDocLabel: React.FC<FieldDocLabelProps> = (props) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const onToggleDoc = (state: boolean) => {
|
||||
setOpen(state);
|
||||
if (props.onToggleDoc) {
|
||||
props.onToggleDoc(state);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onToggleDoc = (open: boolean) => {
|
||||
this.setState({
|
||||
open,
|
||||
}, () => {
|
||||
if (this.props.onToggleDoc) {
|
||||
this.props.onToggleDoc(this.state.open);
|
||||
}
|
||||
});
|
||||
}
|
||||
const { label, fieldSpec } = props;
|
||||
const { doc } = fieldSpec || {};
|
||||
|
||||
render() {
|
||||
const {label, fieldSpec} = this.props;
|
||||
const {doc} = fieldSpec || {};
|
||||
|
||||
if (doc) {
|
||||
return <label className="maputnik-doc-wrapper">
|
||||
if (doc) {
|
||||
return (
|
||||
<label className="maputnik-doc-wrapper">
|
||||
<div className="maputnik-doc-target">
|
||||
{label}
|
||||
{'\xa0'}
|
||||
<button
|
||||
aria-label={this.state.open ? "close property documentation" : "open property documentation"}
|
||||
className={`maputnik-doc-button maputnik-doc-button--${this.state.open ? 'open' : 'closed'}`}
|
||||
onClick={() => this.onToggleDoc(!this.state.open)}
|
||||
data-wd-key={'field-doc-button-'+label}
|
||||
aria-label={open ? 'close property documentation' : 'open property documentation'}
|
||||
className={`maputnik-doc-button maputnik-doc-button--${open ? 'open' : 'closed'}`}
|
||||
onClick={() => onToggleDoc(!open)}
|
||||
data-wd-key={'field-doc-button-' + label}
|
||||
>
|
||||
{this.state.open ? <MdHighlightOff /> : <MdInfoOutline />}
|
||||
{open ? <MdHighlightOff /> : <MdInfoOutline />}
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
}
|
||||
else if (label) {
|
||||
return <label className="maputnik-doc-wrapper">
|
||||
<div className="maputnik-doc-target">
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
} else if (label) {
|
||||
return (
|
||||
<label className="maputnik-doc-wrapper">
|
||||
<div className="maputnik-doc-target">{label}</div>
|
||||
</label>
|
||||
}
|
||||
else {
|
||||
<div />
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
return <div />;
|
||||
};
|
||||
|
||||
export default FieldDocLabel;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import InputDynamicArray, {FieldDynamicArrayProps as InputDynamicArrayProps} from './InputDynamicArray'
|
||||
import Fieldset from './Fieldset'
|
||||
|
||||
@@ -6,10 +5,12 @@ type FieldDynamicArrayProps = InputDynamicArrayProps & {
|
||||
name?: string
|
||||
};
|
||||
|
||||
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
|
||||
render() {
|
||||
return <Fieldset label={this.props.label}>
|
||||
<InputDynamicArray {...this.props} />
|
||||
const FieldDynamicArray: React.FC<FieldDynamicArrayProps> = (props) => {
|
||||
return (
|
||||
<Fieldset label={props.label}>
|
||||
<InputDynamicArray {...props} />
|
||||
</Fieldset>
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldDynamicArray;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import InputEnum, {InputEnumProps} from './InputEnum'
|
||||
import Fieldset from './Fieldset';
|
||||
|
||||
@@ -11,10 +10,12 @@ type FieldEnumProps = InputEnumProps & {
|
||||
};
|
||||
|
||||
|
||||
export default class FieldEnum extends React.Component<FieldEnumProps> {
|
||||
render() {
|
||||
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputEnum {...this.props} />
|
||||
const FieldEnum: React.FC<FieldEnumProps> = (props) => {
|
||||
return (
|
||||
<Fieldset label={props.label} fieldSpec={props.fieldSpec}>
|
||||
<InputEnum {...props} />
|
||||
</Fieldset>
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldEnum;
|
||||
|
||||
+188
-216
@@ -111,296 +111,268 @@ type FieldFunctionProps = {
|
||||
value?: any
|
||||
};
|
||||
|
||||
type FieldFunctionState = {
|
||||
dataType: string
|
||||
isEditing: boolean
|
||||
}
|
||||
|
||||
/** Supports displaying spec field for zoom function objects
|
||||
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
||||
*/
|
||||
export default class FieldFunction extends React.Component<FieldFunctionProps, FieldFunctionState> {
|
||||
constructor (props: FieldFunctionProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
dataType: getDataType(props.value, props.fieldSpec),
|
||||
isEditing: false,
|
||||
}
|
||||
}
|
||||
const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
||||
const [dataType, setDataType] = React.useState(
|
||||
getDataType(props.value, props.fieldSpec)
|
||||
);
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
|
||||
static getDerivedStateFromProps(props: Readonly<FieldFunctionProps>, state: FieldFunctionState) {
|
||||
// Because otherwise when editing values we end up accidentally changing field type.
|
||||
if (state.isEditing) {
|
||||
return {};
|
||||
React.useEffect(() => {
|
||||
if (!isEditing) {
|
||||
setDataType(getDataType(props.value, props.fieldSpec));
|
||||
}
|
||||
else {
|
||||
return {
|
||||
isEditing: false,
|
||||
dataType: getDataType(props.value, props.fieldSpec)
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [props.value, props.fieldSpec, isEditing]);
|
||||
|
||||
getFieldFunctionType(fieldSpec: any) {
|
||||
const getFieldFunctionType = (fieldSpec: any) => {
|
||||
if (fieldSpec.expression.interpolated) {
|
||||
return "exponential"
|
||||
return 'exponential';
|
||||
}
|
||||
if (fieldSpec.type === "number") {
|
||||
return "interval"
|
||||
if (fieldSpec.type === 'number') {
|
||||
return 'interval';
|
||||
}
|
||||
return "categorical"
|
||||
}
|
||||
return 'categorical';
|
||||
};
|
||||
|
||||
addStop = () => {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
const lastStop = stops[stops.length - 1]
|
||||
if (typeof lastStop[0] === "object") {
|
||||
const addStop = () => {
|
||||
const stops = props.value.stops.slice(0);
|
||||
const lastStop = stops[stops.length - 1];
|
||||
if (typeof lastStop[0] === 'object') {
|
||||
stops.push([
|
||||
{zoom: lastStop[0].zoom + 1, value: lastStop[0].value},
|
||||
lastStop[1]
|
||||
])
|
||||
}
|
||||
else {
|
||||
stops.push([lastStop[0] + 1, lastStop[1]])
|
||||
{ zoom: lastStop[0].zoom + 1, value: lastStop[0].value },
|
||||
lastStop[1],
|
||||
]);
|
||||
} else {
|
||||
stops.push([lastStop[0] + 1, lastStop[1]]);
|
||||
}
|
||||
|
||||
const changedValue = {
|
||||
...this.props.value,
|
||||
...props.value,
|
||||
stops: stops,
|
||||
}
|
||||
};
|
||||
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
props.onChange(props.fieldName, changedValue);
|
||||
};
|
||||
|
||||
deleteExpression = () => {
|
||||
const {fieldSpec, fieldName} = this.props;
|
||||
this.props.onChange(fieldName, fieldSpec.default);
|
||||
this.setState({
|
||||
dataType: "value",
|
||||
});
|
||||
}
|
||||
const deleteExpression = () => {
|
||||
const { fieldSpec, fieldName } = props;
|
||||
props.onChange(fieldName, fieldSpec.default);
|
||||
setDataType('value');
|
||||
};
|
||||
|
||||
deleteStop = (stopIdx: number) => {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
stops.splice(stopIdx, 1)
|
||||
const deleteStop = (stopIdx: number) => {
|
||||
const stops = props.value.stops.slice(0);
|
||||
stops.splice(stopIdx, 1);
|
||||
|
||||
let changedValue = {
|
||||
...this.props.value,
|
||||
let changedValue: any = {
|
||||
...props.value,
|
||||
stops: stops,
|
||||
};
|
||||
|
||||
if (stops.length === 1) {
|
||||
changedValue = stops[0][1];
|
||||
}
|
||||
|
||||
if(stops.length === 1) {
|
||||
changedValue = stops[0][1]
|
||||
}
|
||||
props.onChange(props.fieldName, changedValue);
|
||||
};
|
||||
|
||||
this.props.onChange(this.props.fieldName, changedValue)
|
||||
}
|
||||
const makeZoomFunction = () => {
|
||||
const { value } = props;
|
||||
|
||||
makeZoomFunction = () => {
|
||||
const {value} = this.props;
|
||||
|
||||
let zoomFunc;
|
||||
if (typeof(value) === "object") {
|
||||
let zoomFunc: any;
|
||||
if (typeof value === 'object') {
|
||||
if (value.stops) {
|
||||
zoomFunc = {
|
||||
base: value.base,
|
||||
stops: value.stops.map((stop: Stop) => {
|
||||
return [stop[0].zoom, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
return [stop[0].zoom, stop[1] || findDefaultFromSpec(props.fieldSpec)];
|
||||
}),
|
||||
};
|
||||
} else {
|
||||
zoomFunc = {
|
||||
base: value.base,
|
||||
stops: [
|
||||
[6, findDefaultFromSpec(this.props.fieldSpec)],
|
||||
[10, findDefaultFromSpec(this.props.fieldSpec)]
|
||||
]
|
||||
}
|
||||
[6, findDefaultFromSpec(props.fieldSpec)],
|
||||
[10, findDefaultFromSpec(props.fieldSpec)],
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
zoomFunc = {
|
||||
stops: [
|
||||
[6, value || findDefaultFromSpec(this.props.fieldSpec)],
|
||||
[10, value || findDefaultFromSpec(this.props.fieldSpec)]
|
||||
]
|
||||
}
|
||||
[6, value || findDefaultFromSpec(props.fieldSpec)],
|
||||
[10, value || findDefaultFromSpec(props.fieldSpec)],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
this.props.onChange(this.props.fieldName, zoomFunc)
|
||||
}
|
||||
props.onChange(props.fieldName, zoomFunc);
|
||||
};
|
||||
|
||||
undoExpression = () => {
|
||||
const {value, fieldName} = this.props;
|
||||
const undoExpression = () => {
|
||||
const { value, fieldName } = props;
|
||||
|
||||
if (isGetExpression(value)) {
|
||||
this.props.onChange(fieldName, {
|
||||
"type": "identity",
|
||||
"property": value[1]
|
||||
});
|
||||
this.setState({
|
||||
dataType: "value",
|
||||
props.onChange(fieldName, {
|
||||
type: 'identity',
|
||||
property: value[1],
|
||||
});
|
||||
setDataType('value');
|
||||
} else if (isLiteralExpression(value)) {
|
||||
props.onChange(fieldName, value[1]);
|
||||
setDataType('value');
|
||||
}
|
||||
else if (isLiteralExpression(value)) {
|
||||
this.props.onChange(fieldName, value[1]);
|
||||
this.setState({
|
||||
dataType: "value",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
canUndo = () => {
|
||||
const {value, fieldSpec} = this.props;
|
||||
const canUndo = () => {
|
||||
const { value, fieldSpec } = props;
|
||||
return (
|
||||
isGetExpression(value) ||
|
||||
isLiteralExpression(value) ||
|
||||
isPrimative(value) ||
|
||||
(Array.isArray(value) && fieldSpec.type === "array")
|
||||
(Array.isArray(value) && fieldSpec.type === 'array')
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
makeExpression = () => {
|
||||
const {value, fieldSpec} = this.props;
|
||||
const makeExpression = () => {
|
||||
const { value, fieldSpec } = props;
|
||||
let expression;
|
||||
|
||||
if (typeof(value) === "object" && 'stops' in value) {
|
||||
if (typeof value === 'object' && 'stops' in value) {
|
||||
expression = styleFunction.convertFunction(value, fieldSpec);
|
||||
} else if (isIdentityProperty(value)) {
|
||||
expression = ['get', value.property];
|
||||
} else {
|
||||
expression = ['literal', value || props.fieldSpec.default];
|
||||
}
|
||||
else if (isIdentityProperty(value)) {
|
||||
expression = ["get", value.property];
|
||||
}
|
||||
else {
|
||||
expression = ["literal", value || this.props.fieldSpec.default];
|
||||
}
|
||||
this.props.onChange(this.props.fieldName, expression);
|
||||
}
|
||||
props.onChange(props.fieldName, expression);
|
||||
};
|
||||
|
||||
makeDataFunction = () => {
|
||||
const functionType = this.getFieldFunctionType(this.props.fieldSpec);
|
||||
const makeDataFunction = () => {
|
||||
const functionType = getFieldFunctionType(props.fieldSpec);
|
||||
const stopValue = functionType === 'categorical' ? '' : 0;
|
||||
const {value} = this.props;
|
||||
const { value } = props;
|
||||
let dataFunc;
|
||||
|
||||
if (typeof(value) === "object") {
|
||||
if (typeof value === 'object') {
|
||||
if (value.stops) {
|
||||
dataFunc = {
|
||||
property: "",
|
||||
property: '',
|
||||
type: functionType,
|
||||
base: value.base,
|
||||
stops: value.stops.map((stop: Stop) => {
|
||||
return [{zoom: stop[0], value: stopValue}, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
return [{ zoom: stop[0], value: stopValue }, stop[1] || findDefaultFromSpec(props.fieldSpec)];
|
||||
}),
|
||||
};
|
||||
} else {
|
||||
dataFunc = {
|
||||
property: "",
|
||||
property: '',
|
||||
type: functionType,
|
||||
base: value.base,
|
||||
stops: [
|
||||
[{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)],
|
||||
[{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)]
|
||||
]
|
||||
}
|
||||
[{ zoom: 6, value: stopValue }, findDefaultFromSpec(props.fieldSpec)],
|
||||
[{ zoom: 10, value: stopValue }, findDefaultFromSpec(props.fieldSpec)],
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
dataFunc = {
|
||||
property: "",
|
||||
property: '',
|
||||
type: functionType,
|
||||
base: value.base,
|
||||
stops: [
|
||||
[{zoom: 6, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)],
|
||||
[{zoom: 10, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)]
|
||||
]
|
||||
}
|
||||
[{ zoom: 6, value: stopValue }, props.value || findDefaultFromSpec(props.fieldSpec)],
|
||||
[{ zoom: 10, value: stopValue }, props.value || findDefaultFromSpec(props.fieldSpec)],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
this.props.onChange(this.props.fieldName, dataFunc)
|
||||
props.onChange(props.fieldName, dataFunc);
|
||||
};
|
||||
|
||||
const onMarkEditing = () => {
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const onUnmarkEditing = () => {
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const propClass =
|
||||
props.fieldSpec.default === props.value ? 'maputnik-default-property' : 'maputnik-modified-property';
|
||||
|
||||
let specField;
|
||||
|
||||
if (dataType === 'expression') {
|
||||
specField = (
|
||||
<ExpressionProperty
|
||||
errors={props.errors}
|
||||
onChange={props.onChange.bind(null, props.fieldName)}
|
||||
canUndo={canUndo}
|
||||
onUndo={undoExpression}
|
||||
onDelete={deleteExpression}
|
||||
fieldType={props.fieldType}
|
||||
fieldName={props.fieldName}
|
||||
fieldSpec={props.fieldSpec}
|
||||
value={props.value}
|
||||
onFocus={onMarkEditing}
|
||||
onBlur={onUnmarkEditing}
|
||||
/>
|
||||
);
|
||||
} else if (dataType === 'zoom_function') {
|
||||
specField = (
|
||||
<ZoomProperty
|
||||
errors={props.errors}
|
||||
onChange={props.onChange.bind(null)}
|
||||
fieldType={props.fieldType}
|
||||
fieldName={props.fieldName}
|
||||
fieldSpec={props.fieldSpec}
|
||||
value={props.value}
|
||||
onDeleteStop={deleteStop}
|
||||
onAddStop={addStop}
|
||||
onChangeToDataFunction={makeDataFunction}
|
||||
onExpressionClick={makeExpression}
|
||||
/>
|
||||
);
|
||||
} else if (dataType === 'data_function') {
|
||||
specField = (
|
||||
<DataProperty
|
||||
errors={props.errors}
|
||||
onChange={props.onChange.bind(null)}
|
||||
fieldType={props.fieldType}
|
||||
fieldName={props.fieldName}
|
||||
fieldSpec={props.fieldSpec}
|
||||
value={props.value}
|
||||
onDeleteStop={deleteStop}
|
||||
onAddStop={addStop}
|
||||
onChangeToZoomFunction={makeZoomFunction}
|
||||
onExpressionClick={makeExpression}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
specField = (
|
||||
<SpecProperty
|
||||
errors={props.errors}
|
||||
onChange={props.onChange.bind(null)}
|
||||
fieldType={props.fieldType}
|
||||
fieldName={props.fieldName}
|
||||
fieldSpec={props.fieldSpec}
|
||||
value={props.value}
|
||||
onZoomClick={makeZoomFunction}
|
||||
onDataClick={makeDataFunction}
|
||||
onExpressionClick={makeExpression}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
onMarkEditing = () => {
|
||||
this.setState({isEditing: true});
|
||||
}
|
||||
|
||||
onUnmarkEditing = () => {
|
||||
this.setState({isEditing: false});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {dataType} = this.state;
|
||||
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
|
||||
let specField;
|
||||
|
||||
if (dataType === "expression") {
|
||||
specField = (
|
||||
<ExpressionProperty
|
||||
errors={this.props.errors}
|
||||
onChange={this.props.onChange.bind(this, this.props.fieldName)}
|
||||
canUndo={this.canUndo}
|
||||
onUndo={this.undoExpression}
|
||||
onDelete={this.deleteExpression}
|
||||
fieldType={this.props.fieldType}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
onFocus={this.onMarkEditing}
|
||||
onBlur={this.onUnmarkEditing}
|
||||
/>
|
||||
);
|
||||
}
|
||||
else if (dataType === "zoom_function") {
|
||||
specField = (
|
||||
<ZoomProperty
|
||||
errors={this.props.errors}
|
||||
onChange={this.props.onChange.bind(this)}
|
||||
fieldType={this.props.fieldType}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
onDeleteStop={this.deleteStop}
|
||||
onAddStop={this.addStop}
|
||||
onChangeToDataFunction={this.makeDataFunction}
|
||||
onExpressionClick={this.makeExpression}
|
||||
/>
|
||||
)
|
||||
}
|
||||
else if (dataType === "data_function") {
|
||||
// TODO: Rename to FieldFunction **this file** shouldn't be called that
|
||||
specField = (
|
||||
<DataProperty
|
||||
errors={this.props.errors}
|
||||
onChange={this.props.onChange.bind(this)}
|
||||
fieldType={this.props.fieldType}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
onDeleteStop={this.deleteStop}
|
||||
onAddStop={this.addStop}
|
||||
onChangeToZoomFunction={this.makeZoomFunction}
|
||||
onExpressionClick={this.makeExpression}
|
||||
/>
|
||||
)
|
||||
}
|
||||
else {
|
||||
specField = (
|
||||
<SpecProperty
|
||||
errors={this.props.errors}
|
||||
onChange={this.props.onChange.bind(this)}
|
||||
fieldType={this.props.fieldType}
|
||||
fieldName={this.props.fieldName}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
onZoomClick={this.makeZoomFunction}
|
||||
onDataClick={this.makeDataFunction}
|
||||
onExpressionClick={this.makeExpression}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <div className={propClass} data-wd-key={"spec-field-container:"+this.props.fieldName}>
|
||||
return (
|
||||
<div className={propClass} data-wd-key={'spec-field-container:' + props.fieldName}>
|
||||
{specField}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldFunction;
|
||||
|
||||
+12
-12
@@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import Block from './Block'
|
||||
@@ -11,18 +10,19 @@ type FieldIdProps = {
|
||||
error?: {message: string}
|
||||
};
|
||||
|
||||
export default class FieldId extends React.Component<FieldIdProps> {
|
||||
render() {
|
||||
return <Block label="ID" fieldSpec={latest.layer.id}
|
||||
|
||||
data-wd-key={this.props.wdKey}
|
||||
error={this.props.error}
|
||||
const FieldId: React.FC<FieldIdProps> = (props) => {
|
||||
return (
|
||||
<Block label="ID" fieldSpec={latest.layer.id}
|
||||
data-wd-key={props.wdKey}
|
||||
error={props.error}
|
||||
>
|
||||
<InputString
|
||||
value={this.props.value}
|
||||
onInput={this.props.onChange}
|
||||
data-wd-key={this.props.wdKey + ".input"}
|
||||
value={props.value}
|
||||
onInput={props.onChange}
|
||||
data-wd-key={props.wdKey + ".input"}
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldId;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import React from 'react'
|
||||
import InputJson, {InputJsonProps} from './InputJson'
|
||||
|
||||
|
||||
type FieldJsonProps = InputJsonProps & {};
|
||||
|
||||
|
||||
export default class FieldJson extends React.Component<FieldJsonProps> {
|
||||
render() {
|
||||
return <InputJson {...this.props} />
|
||||
}
|
||||
}
|
||||
const FieldJson: React.FC<FieldJsonProps> = (props) => {
|
||||
return <InputJson {...props} />;
|
||||
};
|
||||
|
||||
export default FieldJson;
|
||||
|
||||
@@ -11,25 +11,25 @@ type FieldMaxZoomInternalProps = {
|
||||
error?: {message: string}
|
||||
} & WithTranslation;
|
||||
|
||||
class FieldMaxZoomInternal extends React.Component<FieldMaxZoomInternalProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom}
|
||||
error={this.props.error}
|
||||
const FieldMaxZoomInternal: React.FC<FieldMaxZoomInternalProps> = (props) => {
|
||||
const t = props.t;
|
||||
return (
|
||||
<Block label={t('Max Zoom')} fieldSpec={latest.layer.maxzoom}
|
||||
error={props.error}
|
||||
data-wd-key="max-zoom"
|
||||
>
|
||||
<InputNumber
|
||||
allowRange={true}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
min={latest.layer.maxzoom.minimum}
|
||||
max={latest.layer.maxzoom.maximum}
|
||||
default={latest.layer.maxzoom.maximum}
|
||||
data-wd-key="max-zoom.input"
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal);
|
||||
export default FieldMaxZoom;
|
||||
|
||||
@@ -11,25 +11,25 @@ type FieldMinZoomInternalProps = {
|
||||
error?: {message: string}
|
||||
} & WithTranslation;
|
||||
|
||||
class FieldMinZoomInternal extends React.Component<FieldMinZoomInternalProps> {
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom}
|
||||
error={this.props.error}
|
||||
const FieldMinZoomInternal: React.FC<FieldMinZoomInternalProps> = (props) => {
|
||||
const t = props.t;
|
||||
return (
|
||||
<Block label={t('Min Zoom')} fieldSpec={latest.layer.minzoom}
|
||||
error={props.error}
|
||||
data-wd-key="min-zoom"
|
||||
>
|
||||
<InputNumber
|
||||
allowRange={true}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
min={latest.layer.minzoom.minimum}
|
||||
max={latest.layer.minzoom.maximum}
|
||||
default={latest.layer.minzoom.minimum}
|
||||
data-wd-key='min-zoom.input'
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
|
||||
export default FieldMinZoom;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import InputMultiInput, {InputMultiInputProps} from './InputMultiInput'
|
||||
import Fieldset from './Fieldset'
|
||||
|
||||
@@ -8,10 +7,12 @@ type FieldMultiInputProps = InputMultiInputProps & {
|
||||
};
|
||||
|
||||
|
||||
export default class FieldMultiInput extends React.Component<FieldMultiInputProps> {
|
||||
render() {
|
||||
return <Fieldset label={this.props.label}>
|
||||
<InputMultiInput {...this.props} />
|
||||
const FieldMultiInput: React.FC<FieldMultiInputProps> = (props) => {
|
||||
return (
|
||||
<Fieldset label={props.label}>
|
||||
<InputMultiInput {...props} />
|
||||
</Fieldset>
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldMultiInput;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import InputNumber, {InputNumberProps} from './InputNumber'
|
||||
import Block from './Block'
|
||||
|
||||
@@ -11,10 +10,12 @@ type FieldNumberProps = InputNumberProps & {
|
||||
};
|
||||
|
||||
|
||||
export default class FieldNumber extends React.Component<FieldNumberProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputNumber {...this.props} />
|
||||
const FieldNumber: React.FC<FieldNumberProps> = (props) => {
|
||||
return (
|
||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
||||
<InputNumber {...props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldNumber;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import InputSelect, {InputSelectProps} from './InputSelect'
|
||||
|
||||
@@ -11,10 +10,12 @@ type FieldSelectProps = InputSelectProps & {
|
||||
};
|
||||
|
||||
|
||||
export default class FieldSelect extends React.Component<FieldSelectProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputSelect {...this.props}/>
|
||||
const FieldSelect: React.FC<FieldSelectProps> = (props) => {
|
||||
return (
|
||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
||||
<InputSelect {...props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldSelect;
|
||||
|
||||
@@ -13,28 +13,28 @@ type FieldSourceInternalProps = {
|
||||
error?: {message: string}
|
||||
} & WithTranslation;
|
||||
|
||||
class FieldSourceInternal extends React.Component<FieldSourceInternalProps> {
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceIds: [],
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <Block
|
||||
label={t("Source")}
|
||||
const FieldSourceInternal: React.FC<FieldSourceInternalProps> = (props) => {
|
||||
const t = props.t;
|
||||
return (
|
||||
<Block
|
||||
label={t('Source')}
|
||||
fieldSpec={latest.layer.source}
|
||||
error={this.props.error}
|
||||
data-wd-key={this.props.wdKey}
|
||||
error={props.error}
|
||||
data-wd-key={props.wdKey}
|
||||
>
|
||||
<InputAutocomplete
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
options={this.props.sourceIds?.map(src => [src, src])}
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
options={props.sourceIds?.map((src) => [src, src])}
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
FieldSourceInternal.defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceIds: [],
|
||||
};
|
||||
|
||||
const FieldSource = withTranslation()(FieldSourceInternal);
|
||||
export default FieldSource;
|
||||
|
||||
@@ -13,30 +13,29 @@ type FieldSourceLayerInternalProps = {
|
||||
error?: {message: string}
|
||||
} & WithTranslation;
|
||||
|
||||
class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalProps> {
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceLayerIds: [],
|
||||
isFixed: false
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <Block
|
||||
label={t("Source Layer")}
|
||||
const FieldSourceLayerInternal: React.FC<FieldSourceLayerInternalProps> = (props) => {
|
||||
const t = props.t;
|
||||
return (
|
||||
<Block
|
||||
label={t('Source Layer')}
|
||||
fieldSpec={latest.layer['source-layer']}
|
||||
data-wd-key="layer-source-layer"
|
||||
error={this.props.error}
|
||||
error={props.error}
|
||||
>
|
||||
<InputAutocomplete
|
||||
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
options={this.props.sourceLayerIds?.map(l => [l, l])}
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
options={props.sourceLayerIds?.map((l) => [l, l])}
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
FieldSourceLayerInternal.defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceLayerIds: [],
|
||||
isFixed: false,
|
||||
};
|
||||
|
||||
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
|
||||
export default FieldSourceLayer;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import InputString, {InputStringProps} from './InputString'
|
||||
|
||||
@@ -10,10 +9,12 @@ type FieldStringProps = InputStringProps & {
|
||||
}
|
||||
};
|
||||
|
||||
export default class FieldString extends React.Component<FieldStringProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputString {...this.props} />
|
||||
const FieldString: React.FC<FieldStringProps> = (props) => {
|
||||
return (
|
||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
||||
<InputString {...props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldString;
|
||||
|
||||
@@ -14,24 +14,17 @@ type FieldTypeInternalProps = {
|
||||
disabled?: boolean
|
||||
} & WithTranslation;
|
||||
|
||||
class FieldTypeInternal extends React.Component<FieldTypeInternalProps> {
|
||||
static defaultProps = {
|
||||
disabled: false,
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <Block label={t("Type")} fieldSpec={latest.layer.type}
|
||||
data-wd-key={this.props.wdKey}
|
||||
error={this.props.error}
|
||||
const FieldTypeInternal: React.FC<FieldTypeInternalProps> = (props) => {
|
||||
const t = props.t;
|
||||
return (
|
||||
<Block label={t('Type')} fieldSpec={latest.layer.type}
|
||||
data-wd-key={props.wdKey}
|
||||
error={props.error}
|
||||
>
|
||||
{this.props.disabled &&
|
||||
<InputString
|
||||
value={this.props.value}
|
||||
disabled={true}
|
||||
/>
|
||||
}
|
||||
{!this.props.disabled &&
|
||||
{props.disabled && (
|
||||
<InputString value={props.value} disabled={true} />
|
||||
)}
|
||||
{!props.disabled && (
|
||||
<InputSelect
|
||||
options={[
|
||||
['background', 'Background'],
|
||||
@@ -44,14 +37,18 @@ class FieldTypeInternal extends React.Component<FieldTypeInternalProps> {
|
||||
['hillshade', 'Hillshade'],
|
||||
['heatmap', 'Heatmap'],
|
||||
]}
|
||||
onChange={this.props.onChange}
|
||||
value={this.props.value}
|
||||
data-wd-key={this.props.wdKey + ".select"}
|
||||
onChange={props.onChange}
|
||||
value={props.value}
|
||||
data-wd-key={props.wdKey + '.select'}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
FieldTypeInternal.defaultProps = {
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
const FieldType = withTranslation()(FieldTypeInternal);
|
||||
export default FieldType;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import InputUrl, {FieldUrlProps as InputUrlProps} from './InputUrl'
|
||||
import Block from './Block'
|
||||
|
||||
@@ -11,12 +10,12 @@ type FieldUrlProps = InputUrlProps & {
|
||||
};
|
||||
|
||||
|
||||
export default class FieldUrl extends React.Component<FieldUrlProps> {
|
||||
render () {
|
||||
return (
|
||||
<Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputUrl {...this.props} />
|
||||
</Block>
|
||||
);
|
||||
}
|
||||
}
|
||||
const FieldUrl: React.FC<FieldUrlProps> = (props) => {
|
||||
return (
|
||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
||||
<InputUrl {...props} />
|
||||
</Block>
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldUrl;
|
||||
|
||||
+26
-43
@@ -9,57 +9,40 @@ type FieldsetProps = PropsWithChildren & {
|
||||
action?: ReactElement,
|
||||
};
|
||||
|
||||
type FieldsetState = {
|
||||
showDoc: boolean
|
||||
};
|
||||
|
||||
export default class Fieldset extends React.Component<FieldsetProps, FieldsetState> {
|
||||
_labelId: string;
|
||||
const Fieldset: React.FC<FieldsetProps> = (props) => {
|
||||
const [showDoc, setShowDoc] = React.useState(false);
|
||||
const labelId = React.useRef(generateUniqueId('fieldset_label_'));
|
||||
|
||||
constructor (props: FieldsetProps) {
|
||||
super(props);
|
||||
this._labelId = generateUniqueId(`fieldset_label_`);
|
||||
this.state = {
|
||||
showDoc: false,
|
||||
}
|
||||
}
|
||||
const onToggleDoc = (val: boolean) => {
|
||||
setShowDoc(val);
|
||||
};
|
||||
|
||||
onToggleDoc = (val: boolean) => {
|
||||
this.setState({
|
||||
showDoc: val
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
return <div className="maputnik-input-block" role="group" aria-labelledby={this._labelId}>
|
||||
{this.props.fieldSpec &&
|
||||
return (
|
||||
<div className="maputnik-input-block" role="group" aria-labelledby={labelId.current}>
|
||||
{props.fieldSpec && (
|
||||
<div className="maputnik-input-block-label">
|
||||
<FieldDocLabel
|
||||
label={this.props.label}
|
||||
onToggleDoc={this.onToggleDoc}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
label={props.label}
|
||||
onToggleDoc={onToggleDoc}
|
||||
fieldSpec={props.fieldSpec}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{!this.props.fieldSpec &&
|
||||
)}
|
||||
{!props.fieldSpec && (
|
||||
<div className="maputnik-input-block-label">
|
||||
{this.props.label}
|
||||
{props.label}
|
||||
</div>
|
||||
}
|
||||
<div className="maputnik-input-block-action">
|
||||
{this.props.action}
|
||||
</div>
|
||||
<div className="maputnik-input-block-content">
|
||||
{this.props.children}
|
||||
</div>
|
||||
{this.props.fieldSpec &&
|
||||
<div
|
||||
className="maputnik-doc-inline"
|
||||
style={{display: this.state.showDoc ? '' : 'none'}}
|
||||
>
|
||||
<Doc fieldSpec={this.props.fieldSpec} />
|
||||
)}
|
||||
<div className="maputnik-input-block-action">{props.action}</div>
|
||||
<div className="maputnik-input-block-content">{props.children}</div>
|
||||
{props.fieldSpec && (
|
||||
<div className="maputnik-doc-inline" style={{ display: showDoc ? '' : 'none' }}>
|
||||
<Doc fieldSpec={props.fieldSpec} />
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Fieldset;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
import { mount } from 'cypress/react'
|
||||
|
||||
const fruits = ['apple', 'banana', 'cherry'];
|
||||
|
||||
describe('<InputAutocomplete />', () => {
|
||||
it('filters options when typing', () => {
|
||||
mount(
|
||||
<InputAutocomplete aria-label="Fruit" options={fruits.map(f => [f, f])} />
|
||||
);
|
||||
cy.get('input').focus();
|
||||
cy.get('.maputnik-autocomplete-menu-item').should('have.length', 3);
|
||||
cy.get('input').type('ch');
|
||||
cy.get('.maputnik-autocomplete-menu-item').should('have.length', 1).and('contain', 'cherry');
|
||||
cy.get('.maputnik-autocomplete-menu-item').click();
|
||||
cy.get('input').should('have.value', 'cherry');
|
||||
});
|
||||
});
|
||||
@@ -1,100 +1,116 @@
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import Autocomplete from 'react-autocomplete'
|
||||
import {useCombobox} from 'downshift'
|
||||
|
||||
|
||||
const MAX_HEIGHT = 140;
|
||||
const MAX_HEIGHT = 140
|
||||
|
||||
export type InputAutocompleteProps = {
|
||||
value?: string
|
||||
options: any[]
|
||||
onChange(value: string | undefined): unknown
|
||||
keepMenuWithinWindowBounds?: boolean
|
||||
options?: any[]
|
||||
onChange?(value: string | undefined): unknown
|
||||
'aria-label'?: string
|
||||
};
|
||||
|
||||
export default class InputAutocomplete extends React.Component<InputAutocompleteProps> {
|
||||
state = {
|
||||
maxHeight: MAX_HEIGHT
|
||||
}
|
||||
export default function InputAutocomplete({
|
||||
value,
|
||||
options = [],
|
||||
onChange = () => {},
|
||||
'aria-label': ariaLabel,
|
||||
}: InputAutocompleteProps) {
|
||||
const [input, setInput] = React.useState(value || '')
|
||||
const menuRef = React.useRef<HTMLDivElement>(null)
|
||||
const [maxHeight, setMaxHeight] = React.useState(MAX_HEIGHT)
|
||||
|
||||
autocompleteMenuEl: HTMLDivElement | null = null;
|
||||
const filteredItems = React.useMemo(() => {
|
||||
const lv = input.toLowerCase()
|
||||
return options.filter((item) => item[0].toLowerCase().includes(lv))
|
||||
}, [options, input])
|
||||
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
options: [],
|
||||
}
|
||||
|
||||
calcMaxHeight() {
|
||||
if(this.props.keepMenuWithinWindowBounds) {
|
||||
const maxHeight = window.innerHeight - this.autocompleteMenuEl!.getBoundingClientRect().top;
|
||||
const limitedMaxHeight = Math.min(maxHeight, MAX_HEIGHT);
|
||||
|
||||
if(limitedMaxHeight != this.state.maxHeight) {
|
||||
this.setState({
|
||||
maxHeight: limitedMaxHeight
|
||||
})
|
||||
}
|
||||
const calcMaxHeight = React.useCallback(() => {
|
||||
if (menuRef.current) {
|
||||
const space = window.innerHeight - menuRef.current.getBoundingClientRect().top
|
||||
setMaxHeight(Math.min(space, MAX_HEIGHT))
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
componentDidMount() {
|
||||
this.calcMaxHeight();
|
||||
}
|
||||
const {
|
||||
isOpen,
|
||||
getMenuProps,
|
||||
getInputProps,
|
||||
getItemProps,
|
||||
highlightedIndex,
|
||||
openMenu,
|
||||
} = useCombobox({
|
||||
items: filteredItems,
|
||||
inputValue: input,
|
||||
itemToString: (item) => (item ? item[0] : ''),
|
||||
stateReducer: (_state, action) => {
|
||||
if (action.type === useCombobox.stateChangeTypes.InputClick) {
|
||||
return {...action.changes, isOpen: true}
|
||||
}
|
||||
return action.changes
|
||||
},
|
||||
onSelectedItemChange: ({selectedItem}) => {
|
||||
const v = selectedItem ? selectedItem[0] : ''
|
||||
setInput(v)
|
||||
onChange(selectedItem ? selectedItem[0] : undefined)
|
||||
},
|
||||
onInputValueChange: ({inputValue: v}) => {
|
||||
if (typeof v === 'string') {
|
||||
setInput(v)
|
||||
onChange(v === '' ? undefined : v)
|
||||
openMenu()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
componentDidUpdate() {
|
||||
this.calcMaxHeight();
|
||||
}
|
||||
React.useEffect(() => {
|
||||
if (isOpen) {
|
||||
calcMaxHeight()
|
||||
}
|
||||
}, [isOpen, calcMaxHeight])
|
||||
|
||||
onChange(v: string) {
|
||||
this.props.onChange(v === "" ? undefined : v);
|
||||
}
|
||||
React.useEffect(() => {
|
||||
window.addEventListener('resize', calcMaxHeight)
|
||||
return () => window.removeEventListener('resize', calcMaxHeight)
|
||||
}, [calcMaxHeight])
|
||||
|
||||
render() {
|
||||
return <div
|
||||
ref={(el) => {
|
||||
this.autocompleteMenuEl = el;
|
||||
}}
|
||||
>
|
||||
<Autocomplete
|
||||
menuStyle={{
|
||||
position: "fixed",
|
||||
overflow: "auto",
|
||||
maxHeight: this.state.maxHeight,
|
||||
zIndex: '998'
|
||||
}}
|
||||
wrapperProps={{
|
||||
className: "maputnik-autocomplete",
|
||||
style: {}
|
||||
}}
|
||||
inputProps={{
|
||||
'aria-label': this.props['aria-label'],
|
||||
className: "maputnik-string",
|
||||
spellCheck: false
|
||||
}}
|
||||
value={this.props.value}
|
||||
items={this.props.options}
|
||||
getItemValue={(item) => item[0]}
|
||||
onSelect={v => this.onChange(v)}
|
||||
onChange={(_e, v) => this.onChange(v)}
|
||||
shouldItemRender={(item, value="") => {
|
||||
if (typeof(value) === "string") {
|
||||
return item[0].toLowerCase().indexOf(value.toLowerCase()) > -1
|
||||
}
|
||||
return false
|
||||
}}
|
||||
renderItem={(item, isHighlighted) => (
|
||||
<div
|
||||
key={item[0]}
|
||||
className={classnames({
|
||||
"maputnik-autocomplete-menu-item": true,
|
||||
"maputnik-autocomplete-menu-item-selected": isHighlighted,
|
||||
})}
|
||||
>
|
||||
{item[1]}
|
||||
</div>
|
||||
)}
|
||||
React.useEffect(() => {
|
||||
setInput(value || '')
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<div className="maputnik-autocomplete">
|
||||
<input
|
||||
{...getInputProps({
|
||||
'aria-label': ariaLabel,
|
||||
className: 'maputnik-string',
|
||||
spellCheck: false,
|
||||
onFocus: () => openMenu(),
|
||||
})}
|
||||
/>
|
||||
<div
|
||||
{...getMenuProps({}, {suppressRefError: true})}
|
||||
ref={menuRef}
|
||||
style={{position: 'fixed', overflow: 'auto', maxHeight, zIndex: 998}}
|
||||
className="maputnik-autocomplete-menu"
|
||||
>
|
||||
{isOpen &&
|
||||
filteredItems.map((item, index) => (
|
||||
<div
|
||||
key={item[0]}
|
||||
{...getItemProps({
|
||||
item,
|
||||
index,
|
||||
className: classnames('maputnik-autocomplete-menu-item', {
|
||||
'maputnik-autocomplete-menu-item-selected': highlightedIndex === index,
|
||||
}),
|
||||
})}
|
||||
>
|
||||
{item[1]}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import InputSpec, { SpecFieldProps as InputFieldSpecProps } from './InputSpec'
|
||||
import Fieldset from './Fieldset'
|
||||
@@ -20,27 +19,25 @@ export type SpecFieldProps = InputFieldSpecProps & {
|
||||
name?: string
|
||||
};
|
||||
|
||||
export default class SpecField extends React.Component<SpecFieldProps> {
|
||||
render() {
|
||||
const fieldType = this.props.fieldSpec?.type;
|
||||
const SpecField: React.FC<SpecFieldProps> = (props) => {
|
||||
const fieldType = props.fieldSpec?.type;
|
||||
|
||||
const typeBlockFn = typeMap[fieldType!];
|
||||
const typeBlockFn = typeMap[fieldType!];
|
||||
|
||||
let TypeBlock;
|
||||
if (typeBlockFn) {
|
||||
TypeBlock = typeBlockFn(this.props);
|
||||
}
|
||||
else {
|
||||
console.warn("No such type for '%s'", fieldType);
|
||||
TypeBlock = Block;
|
||||
}
|
||||
|
||||
return <TypeBlock
|
||||
label={this.props.label}
|
||||
action={this.props.action}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
>
|
||||
<InputSpec {...this.props} />
|
||||
</TypeBlock>
|
||||
let TypeBlock;
|
||||
if (typeBlockFn) {
|
||||
TypeBlock = typeBlockFn(props);
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.warn("No such type for '%s'", fieldType);
|
||||
TypeBlock = Block;
|
||||
}
|
||||
|
||||
return (
|
||||
<TypeBlock label={props.label} action={props.action} fieldSpec={props.fieldSpec}>
|
||||
<InputSpec {...props} />
|
||||
</TypeBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpecField;
|
||||
|
||||
+12
-24
@@ -11,6 +11,18 @@
|
||||
"url": "https://americanamap.org/style.json",
|
||||
"thumbnail": "https://github.com/maplibre/maputnik/assets/649392/23fa75ad-63e6-43f5-8837-03cdb0428bac"
|
||||
},
|
||||
{
|
||||
"id": "aws-hybrid",
|
||||
"title": "AWS Hybrid",
|
||||
"url": "https://maps.geo.eu-west-1.amazonaws.com/v2/styles/Hybrid/descriptor?key=v1.public.eyJqdGkiOiJiOTNkYjBlZi04OWUzLTQxMGUtODFhMC0zYjZjZjVmZWZmMDgifYtukap0NBaJpcrS6Vit9j03GJgK9Bn-RSu5UCe3jkdSql2kKp3IEgLPtyLssbmKUdVO11sXddjK3ZOZy8V6QG0olv0K_1tOxyMIe4DAO3IV6H4VzHWiaXlbSakGiEgFLuHBdcfLDeMotye7N6rSRxuZb0CN9ytH9VjLly6-NEBRZezO_qPQyvdTFdeZsARIpL0f9YVpxPxPVvUcAWYCk5LpaPseRCDPrY5SlCdA1ZKqUA4F9RzxSTxB73Fel_SoNDkCNaux1VposBu791-uUpDzUpr7leKckrPXrpZ2hwnFbafVxFV9vq4fLTpB5KoBksuLfGNIwAx1RLLxWuMhE4c.ZGQzZDY2OGQtMWQxMy00ZTEwLWIyZGUtOGVjYzUzMjU3OGE4&color-scheme=Light",
|
||||
"thumbnail": "https://maputnik.s3.eu-west-1.amazonaws.com/thumbnails/aws-hybrid.jpg"
|
||||
},
|
||||
{
|
||||
"id": "aws-standard",
|
||||
"title": "AWS Standard",
|
||||
"url": "https://maps.geo.eu-west-1.amazonaws.com/v2/styles/Standard/descriptor?key=v1.public.eyJqdGkiOiJiOTNkYjBlZi04OWUzLTQxMGUtODFhMC0zYjZjZjVmZWZmMDgifYtukap0NBaJpcrS6Vit9j03GJgK9Bn-RSu5UCe3jkdSql2kKp3IEgLPtyLssbmKUdVO11sXddjK3ZOZy8V6QG0olv0K_1tOxyMIe4DAO3IV6H4VzHWiaXlbSakGiEgFLuHBdcfLDeMotye7N6rSRxuZb0CN9ytH9VjLly6-NEBRZezO_qPQyvdTFdeZsARIpL0f9YVpxPxPVvUcAWYCk5LpaPseRCDPrY5SlCdA1ZKqUA4F9RzxSTxB73Fel_SoNDkCNaux1VposBu791-uUpDzUpr7leKckrPXrpZ2hwnFbafVxFV9vq4fLTpB5KoBksuLfGNIwAx1RLLxWuMhE4c.ZGQzZDY2OGQtMWQxMy00ZTEwLWIyZGUtOGVjYzUzMjU3OGE4&color-scheme=Light",
|
||||
"thumbnail": "https://maputnik.s3.eu-west-1.amazonaws.com/thumbnails/aws-standard.jpg"
|
||||
},
|
||||
{
|
||||
"id": "dark-matter",
|
||||
"title": "Dark Matter",
|
||||
@@ -29,30 +41,6 @@
|
||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/toner-gl-style@v1.0/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/toner.png"
|
||||
},
|
||||
{
|
||||
"id": "os-zoomstack-light",
|
||||
"title": "Zoomstack Light",
|
||||
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-light/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-light.png"
|
||||
},
|
||||
{
|
||||
"id": "os-zoomstack-night",
|
||||
"title": "Zoomstack Night",
|
||||
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-night/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-night.png"
|
||||
},
|
||||
{
|
||||
"id": "os-zoomstack-outdoor",
|
||||
"title": "Zoomstack Outdoor",
|
||||
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-outdoor/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-outdoor.png"
|
||||
},
|
||||
{
|
||||
"id": "os-zoomstack-road",
|
||||
"title": "Zoomstack Road",
|
||||
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-road/style.json",
|
||||
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-road.png"
|
||||
},
|
||||
{
|
||||
"id": "osm-bright",
|
||||
"title": "OSM Bright",
|
||||
|
||||
Reference in New Issue
Block a user