mirror of
https://github.com/maputnik/editor.git
synced 2026-07-07 14:37:26 +00:00
Merge branch 'main' into dependabot/npm_and_yarn/maplibre/maplibre-gl-style-spec-23.3.0
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
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 install 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pull Requests
|
||||||
|
|
||||||
|
- Pull requests should update `CHANGELOG.md` with a short description of the change.
|
||||||
@@ -11,11 +11,17 @@
|
|||||||
- Upgrade to MapLibre LG JS v5
|
- Upgrade to MapLibre LG JS v5
|
||||||
- Upgrade Vite 6 and Cypress 14 ([#970](https://github.com/maplibre/maputnik/pull/970))
|
- Upgrade Vite 6 and Cypress 14 ([#970](https://github.com/maplibre/maputnik/pull/970))
|
||||||
- Upgrade OpenLayers from v6 to v10
|
- Upgrade OpenLayers from v6 to v10
|
||||||
|
- 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..._
|
- _...Add new stuff here..._
|
||||||
|
|
||||||
### 🐞 Bug fixes
|
### 🐞 Bug fixes
|
||||||
|
|
||||||
- Fix incorrect handing of network error response (#944)
|
- Fix incorrect handing of network error response (#944)
|
||||||
|
- Show an error when adding a layer with a duplicate ID
|
||||||
- _...Add new stuff here..._
|
- _...Add new stuff here..._
|
||||||
|
|
||||||
## 2.1.1
|
## 2.1.1
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM node:18 as builder
|
FROM node:22 as builder
|
||||||
WORKDIR /maputnik
|
WORKDIR /maputnik
|
||||||
|
|
||||||
# Only copy package.json to prevent npm install from running on every build
|
# Only copy package.json to prevent npm install from running on every build
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export default defineConfig({
|
|||||||
exclude: "cypress/**/*.*",
|
exclude: "cypress/**/*.*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
e2e: {
|
e2e: {
|
||||||
setupNodeEvents(on, config) {
|
setupNodeEvents(on, config) {
|
||||||
// implement node event listeners here
|
// implement node event listeners here
|
||||||
@@ -20,4 +21,11 @@ export default defineConfig({
|
|||||||
openMode: 0,
|
openMode: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
component: {
|
||||||
|
devServer: {
|
||||||
|
framework: "react",
|
||||||
|
bundler: "vite",
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ const baseUrl = "http://localhost:8888/";
|
|||||||
|
|
||||||
const styleFromWindow = (win: Window) => {
|
const styleFromWindow = (win: Window) => {
|
||||||
const styleId = win.localStorage.getItem("maputnik:latest_style");
|
const styleId = win.localStorage.getItem("maputnik:latest_style");
|
||||||
const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`);
|
const styleItemKey = `maputnik:style:${styleId}`;
|
||||||
const obj = JSON.parse(styleItem || "");
|
const styleItem = win.localStorage.getItem(styleItemKey);
|
||||||
|
if (!styleItem) throw new Error("Could not get styleItem from localStorage");
|
||||||
|
const obj = JSON.parse(styleItem);
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -272,7 +272,63 @@ describe("modals", () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("add layer", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.setStyle("layer");
|
||||||
|
when.modal.open();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows duplicate id error", () => {
|
||||||
|
when.setValue("add-layer.layer-id.input", "background");
|
||||||
|
when.click("add-layer");
|
||||||
|
then(get.elementByTestId("modal:add-layer")).shouldExist();
|
||||||
|
then(get.element(".maputnik-modal-error")).shouldContainText(
|
||||||
|
"Layer ID already exists"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("sources", () => {
|
describe("sources", () => {
|
||||||
it("toggle");
|
it("toggle");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Handle localStorage QuotaExceededError", () => {
|
||||||
|
it("handles quota exceeded error when opening style from URL", () => {
|
||||||
|
// Clear localStorage to start fresh
|
||||||
|
cy.clearLocalStorage();
|
||||||
|
|
||||||
|
// fill localStorage until we get a QuotaExceededError
|
||||||
|
cy.window().then(win => {
|
||||||
|
let chunkSize = 1000;
|
||||||
|
const chunk = new Array(chunkSize).join("x");
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
// Keep adding until we hit the quota
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
const key = `maputnik:fill-${index++}`;
|
||||||
|
win.localStorage.setItem(key, chunk);
|
||||||
|
} catch (e: any) {
|
||||||
|
// Verify it's a quota error
|
||||||
|
if (e.name === 'QuotaExceededError') {
|
||||||
|
if (chunkSize <= 1) return;
|
||||||
|
else {
|
||||||
|
chunkSize /= 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e; // Unexpected error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open the style via URL input
|
||||||
|
when.click("nav:open");
|
||||||
|
when.setValue("modal:open.url.input", get.exampleFileUrl());
|
||||||
|
when.click("modal:open.url.button");
|
||||||
|
|
||||||
|
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
|
||||||
|
then(get.styleFromLocalStorage()).shouldExist();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,114 @@
|
|||||||
{
|
{
|
||||||
"id": "test-style",
|
"id": "test-style",
|
||||||
"version": 8,
|
"version": 8,
|
||||||
"name": "Test Style",
|
"name": "Test Style",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"maputnik:renderer": "mlgljs"
|
"maputnik:renderer": "mlgljs",
|
||||||
},
|
"data": [
|
||||||
"sources": {},
|
0,
|
||||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
1,
|
||||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
2,
|
||||||
"layers": []
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
8,
|
||||||
|
9,
|
||||||
|
10,
|
||||||
|
11,
|
||||||
|
12,
|
||||||
|
13,
|
||||||
|
14,
|
||||||
|
15,
|
||||||
|
16,
|
||||||
|
17,
|
||||||
|
18,
|
||||||
|
19,
|
||||||
|
20,
|
||||||
|
21,
|
||||||
|
22,
|
||||||
|
23,
|
||||||
|
24,
|
||||||
|
25,
|
||||||
|
26,
|
||||||
|
27,
|
||||||
|
28,
|
||||||
|
29,
|
||||||
|
30,
|
||||||
|
31,
|
||||||
|
32,
|
||||||
|
33,
|
||||||
|
34,
|
||||||
|
35,
|
||||||
|
36,
|
||||||
|
37,
|
||||||
|
38,
|
||||||
|
39,
|
||||||
|
40,
|
||||||
|
41,
|
||||||
|
42,
|
||||||
|
43,
|
||||||
|
44,
|
||||||
|
45,
|
||||||
|
46,
|
||||||
|
47,
|
||||||
|
48,
|
||||||
|
49,
|
||||||
|
50,
|
||||||
|
51,
|
||||||
|
52,
|
||||||
|
53,
|
||||||
|
54,
|
||||||
|
55,
|
||||||
|
56,
|
||||||
|
57,
|
||||||
|
58,
|
||||||
|
59,
|
||||||
|
60,
|
||||||
|
61,
|
||||||
|
62,
|
||||||
|
63,
|
||||||
|
64,
|
||||||
|
65,
|
||||||
|
66,
|
||||||
|
67,
|
||||||
|
68,
|
||||||
|
69,
|
||||||
|
70,
|
||||||
|
71,
|
||||||
|
72,
|
||||||
|
73,
|
||||||
|
74,
|
||||||
|
75,
|
||||||
|
76,
|
||||||
|
77,
|
||||||
|
78,
|
||||||
|
79,
|
||||||
|
80,
|
||||||
|
81,
|
||||||
|
82,
|
||||||
|
83,
|
||||||
|
84,
|
||||||
|
85,
|
||||||
|
86,
|
||||||
|
87,
|
||||||
|
88,
|
||||||
|
89,
|
||||||
|
90,
|
||||||
|
91,
|
||||||
|
92,
|
||||||
|
93,
|
||||||
|
94,
|
||||||
|
95,
|
||||||
|
96,
|
||||||
|
97,
|
||||||
|
98,
|
||||||
|
99
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sources": {},
|
||||||
|
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
|
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
|
"layers": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
+456
-641
File diff suppressed because it is too large
Load Diff
+17
-19
@@ -38,8 +38,8 @@
|
|||||||
"detect-browser": "^5.3.0",
|
"detect-browser": "^5.3.0",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"i18next": "^25.2.1",
|
"i18next": "^25.3.2",
|
||||||
"i18next-browser-languagedetector": "^8.1.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"i18next-resources-to-backend": "^1.2.1",
|
"i18next-resources-to-backend": "^1.2.1",
|
||||||
"json-stringify-pretty-compact": "^4.0.0",
|
"json-stringify-pretty-compact": "^4.0.0",
|
||||||
"json-to-ast": "^2.1.0",
|
"json-to-ast": "^2.1.0",
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"maplibre-gl": "^5.5.0",
|
"maplibre-gl": "^5.5.0",
|
||||||
"maputnik-design": "github:maputnik/design#172b06c",
|
"maputnik-design": "github:maputnik/design#172b06c",
|
||||||
"ol": "^10.5.0",
|
"ol": "^10.6.1",
|
||||||
"ol-mapbox-style": "^13.0.1",
|
"ol-mapbox-style": "^13.0.1",
|
||||||
"pmtiles": "^4.3.0",
|
"pmtiles": "^4.3.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
@@ -60,13 +60,12 @@
|
|||||||
"react-accessible-accordion": "^5.0.1",
|
"react-accessible-accordion": "^5.0.1",
|
||||||
"react-aria-menubutton": "^7.0.3",
|
"react-aria-menubutton": "^7.0.3",
|
||||||
"react-aria-modal": "^5.0.2",
|
"react-aria-modal": "^5.0.2",
|
||||||
"react-autobind": "^1.0.6",
|
"downshift": "^9.0.10",
|
||||||
"react-autocomplete": "^1.8.1",
|
|
||||||
"react-collapse": "^5.1.1",
|
"react-collapse": "^5.1.1",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-file-reader-input": "^2.0.0",
|
"react-file-reader-input": "^2.0.0",
|
||||||
"react-i18next": "^15.5.2",
|
"react-i18next": "^15.6.1",
|
||||||
"react-icon-base": "^2.1.2",
|
"react-icon-base": "^2.1.2",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-sortable-hoc": "^2.0.0",
|
"react-sortable-hoc": "^2.0.0",
|
||||||
@@ -93,11 +92,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cypress/code-coverage": "^3.14.4",
|
"@cypress/code-coverage": "^3.14.5",
|
||||||
"@eslint/js": "^9.28.0",
|
"@eslint/js": "^9.32.0",
|
||||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||||
"@rollup/plugin-replace": "^6.0.2",
|
"@rollup/plugin-replace": "^6.0.2",
|
||||||
"@shellygo/cypress-test-utils": "^5.0.0",
|
"@shellygo/cypress-test-utils": "^5.0.2",
|
||||||
"@types/codemirror": "^5.60.15",
|
"@types/codemirror": "^5.60.15",
|
||||||
"@types/color": "^4.2.0",
|
"@types/color": "^4.2.0",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
@@ -114,7 +113,6 @@
|
|||||||
"@types/react": "^18.2.67",
|
"@types/react": "^18.2.67",
|
||||||
"@types/react-aria-menubutton": "^6.2.14",
|
"@types/react-aria-menubutton": "^6.2.14",
|
||||||
"@types/react-aria-modal": "^5.0.0",
|
"@types/react-aria-modal": "^5.0.0",
|
||||||
"@types/react-autocomplete": "^1.8.11",
|
|
||||||
"@types/react-collapse": "^5.0.4",
|
"@types/react-collapse": "^5.0.4",
|
||||||
"@types/react-color": "^3.0.13",
|
"@types/react-color": "^3.0.13",
|
||||||
"@types/react-dom": "^18.2.22",
|
"@types/react-dom": "^18.2.22",
|
||||||
@@ -123,27 +121,27 @@
|
|||||||
"@types/string-hash": "^1.1.3",
|
"@types/string-hash": "^1.1.3",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@types/wicg-file-system-access": "^2023.10.6",
|
"@types/wicg-file-system-access": "^2023.10.6",
|
||||||
"@vitejs/plugin-react": "^4.5.1",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cypress": "^14.4.1",
|
"cypress": "^14.5.3",
|
||||||
"cypress-plugin-tab": "^1.0.5",
|
"cypress-plugin-tab": "^1.0.5",
|
||||||
"eslint": "^9.28.0",
|
"eslint": "^9.32.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.20",
|
"eslint-plugin-react-refresh": "^0.4.20",
|
||||||
"i18next-parser": "^9.3.0",
|
"i18next-parser": "^9.3.0",
|
||||||
"istanbul": "^0.4.5",
|
"istanbul": "^0.4.5",
|
||||||
"istanbul-lib-coverage": "^3.2.2",
|
"istanbul-lib-coverage": "^3.2.2",
|
||||||
"postcss": "^8.5.4",
|
"postcss": "^8.5.6",
|
||||||
"react-hot-loader": "^4.13.1",
|
"react-hot-loader": "^4.13.1",
|
||||||
"sass": "^1.89.1",
|
"sass": "^1.90.0",
|
||||||
"stylelint": "^16.20.0",
|
"stylelint": "^16.23.0",
|
||||||
"stylelint-config-recommended-scss": "^15.0.1",
|
"stylelint-config-recommended-scss": "^15.0.1",
|
||||||
"stylelint-scss": "^6.12.0",
|
"stylelint-scss": "^6.12.1",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.33.1",
|
"typescript-eslint": "^8.38.0",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5",
|
||||||
"vite-plugin-istanbul": "^7.0.0"
|
"vite-plugin-istanbul": "^7.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// @ts-ignore - this can be easily replaced with arrow functions
|
|
||||||
import autoBind from 'react-autobind';
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import cloneDeep from 'lodash.clonedeep'
|
import cloneDeep from 'lodash.clonedeep'
|
||||||
import clamp from 'lodash.clamp'
|
import clamp from 'lodash.clamp'
|
||||||
@@ -140,7 +138,6 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
|
|
||||||
constructor(props: any) {
|
constructor(props: any) {
|
||||||
super(props)
|
super(props)
|
||||||
autoBind(this);
|
|
||||||
|
|
||||||
this.revisionStore = new RevisionStore()
|
this.revisionStore = new RevisionStore()
|
||||||
const params = new URLSearchParams(window.location.search.substring(1))
|
const params = new URLSearchParams(window.location.search.substring(1))
|
||||||
@@ -880,8 +877,8 @@ export default class App extends React.Component<any, AppState> {
|
|||||||
this.setModal(modalName, !this.state.isOpen[modalName]);
|
this.setModal(modalName, !this.state.isOpen[modalName]);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSetFileHandle(fileHandle: FileSystemFileHandle | null) {
|
onSetFileHandle = (fileHandle: FileSystemFileHandle | null) => {
|
||||||
this.setState({fileHandle: fileHandle});
|
this.setState({ fileHandle });
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => {
|
onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import ScrollContainer from './ScrollContainer'
|
import ScrollContainer from './ScrollContainer'
|
||||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||||
|
import { IconContext } from 'react-icons';
|
||||||
|
|
||||||
type AppLayoutInternalProps = {
|
type AppLayoutInternalProps = {
|
||||||
toolbar: React.ReactElement
|
toolbar: React.ReactElement
|
||||||
@@ -13,38 +13,31 @@ type AppLayoutInternalProps = {
|
|||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
|
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
|
||||||
static childContextTypes = {
|
|
||||||
reactIconBase: PropTypes.object
|
|
||||||
}
|
|
||||||
|
|
||||||
getChildContext() {
|
|
||||||
return {
|
|
||||||
reactIconBase: { size: 14 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
document.body.dir = this.props.i18n.dir();
|
document.body.dir = this.props.i18n.dir();
|
||||||
|
|
||||||
return <div className="maputnik-layout">
|
return <IconContext.Provider value={{size: '14px'}}>
|
||||||
{this.props.toolbar}
|
<div className="maputnik-layout">
|
||||||
<div className="maputnik-layout-main">
|
{this.props.toolbar}
|
||||||
<div className="maputnik-layout-list">
|
<div className="maputnik-layout-main">
|
||||||
{this.props.layerList}
|
<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>
|
||||||
<div className="maputnik-layout-drawer">
|
{this.props.bottom && <div className="maputnik-layout-bottom">
|
||||||
<ScrollContainer>
|
{this.props.bottom}
|
||||||
{this.props.layerEditor}
|
|
||||||
</ScrollContainer>
|
|
||||||
</div>
|
</div>
|
||||||
{this.props.map}
|
}
|
||||||
|
{this.props.modals}
|
||||||
</div>
|
</div>
|
||||||
{this.props.bottom && <div className="maputnik-layout-bottom">
|
</IconContext.Provider>
|
||||||
{this.props.bottom}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{this.props.modals}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import InputArray, { FieldArrayProps as InputArrayProps } from './InputArray'
|
import InputArray, { FieldArrayProps as InputArrayProps } from './InputArray'
|
||||||
import Fieldset from './Fieldset'
|
import Fieldset from './Fieldset'
|
||||||
|
|
||||||
@@ -9,10 +8,12 @@ type FieldArrayProps = InputArrayProps & {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class FieldArray extends React.Component<FieldArrayProps> {
|
const FieldArray: React.FC<FieldArrayProps> = (props) => {
|
||||||
render() {
|
return (
|
||||||
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
<Fieldset label={props.label} fieldSpec={props.fieldSpec}>
|
||||||
<InputArray {...this.props} />
|
<InputArray {...props} />
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default FieldArray;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
|
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
|
||||||
|
|
||||||
@@ -8,10 +7,12 @@ type FieldAutocompleteProps = InputAutocompleteProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default class FieldAutocomplete extends React.Component<FieldAutocompleteProps> {
|
const FieldAutocomplete: React.FC<FieldAutocompleteProps> = (props) => {
|
||||||
render() {
|
return (
|
||||||
return <Block label={this.props.label}>
|
<Block label={props.label}>
|
||||||
<InputAutocomplete {...this.props} />
|
<InputAutocomplete {...props} />
|
||||||
</Block>
|
</Block>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default FieldAutocomplete;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputCheckbox, {InputCheckboxProps} from './InputCheckbox'
|
import InputCheckbox, {InputCheckboxProps} from './InputCheckbox'
|
||||||
|
|
||||||
@@ -8,10 +7,12 @@ type FieldCheckboxProps = InputCheckboxProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default class FieldCheckbox extends React.Component<FieldCheckboxProps> {
|
const FieldCheckbox: React.FC<FieldCheckboxProps> = (props) => {
|
||||||
render() {
|
return (
|
||||||
return <Block label={this.props.label}>
|
<Block label={props.label}>
|
||||||
<InputCheckbox {...this.props} />
|
<InputCheckbox {...props} />
|
||||||
</Block>
|
</Block>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default FieldCheckbox;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputColor, {InputColorProps} from './InputColor'
|
import InputColor, {InputColorProps} from './InputColor'
|
||||||
|
|
||||||
@@ -11,10 +10,12 @@ type FieldColorProps = InputColorProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default class FieldColor extends React.Component<FieldColorProps> {
|
const FieldColor: React.FC<FieldColorProps> = (props) => {
|
||||||
render() {
|
return (
|
||||||
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
||||||
<InputColor {...this.props} />
|
<InputColor {...props} />
|
||||||
</Block>
|
</Block>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default FieldColor;
|
||||||
|
|||||||
@@ -10,29 +10,31 @@ type FieldCommentInternalProps = {
|
|||||||
error: {message: string}
|
error: {message: string}
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class FieldCommentInternal extends React.Component<FieldCommentInternalProps> {
|
const FieldCommentInternal: React.FC<FieldCommentInternalProps> = (props) => {
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
const fieldSpec = {
|
||||||
const fieldSpec = {
|
doc: t(
|
||||||
doc: t("Comments for the current layer. This is non-standard and not in the spec."),
|
"Comments for the current layer. This is non-standard and not in the spec."
|
||||||
};
|
),
|
||||||
|
};
|
||||||
|
|
||||||
return <Block
|
return (
|
||||||
|
<Block
|
||||||
label={t("Comments")}
|
label={t("Comments")}
|
||||||
fieldSpec={fieldSpec}
|
fieldSpec={fieldSpec}
|
||||||
data-wd-key="layer-comment"
|
data-wd-key="layer-comment"
|
||||||
error={this.props.error}
|
error={props.error}
|
||||||
>
|
>
|
||||||
<InputString
|
<InputString
|
||||||
multi={true}
|
multi={true}
|
||||||
value={this.props.value}
|
value={props.value}
|
||||||
onChange={this.props.onChange}
|
onChange={props.onChange}
|
||||||
default={t("Comment...")}
|
default={t("Comment...")}
|
||||||
data-wd-key="layer-comment.input"
|
data-wd-key="layer-comment.input"
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const FieldComment = withTranslation()(FieldCommentInternal);
|
const FieldComment = withTranslation()(FieldCommentInternal);
|
||||||
export default FieldComment;
|
export default FieldComment;
|
||||||
|
|||||||
@@ -9,57 +9,45 @@ type FieldDocLabelProps = {
|
|||||||
onToggleDoc?(...args: unknown[]): unknown
|
onToggleDoc?(...args: unknown[]): unknown
|
||||||
};
|
};
|
||||||
|
|
||||||
type FieldDocLabelState = {
|
|
||||||
open: boolean
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class FieldDocLabel extends React.Component<FieldDocLabelProps, FieldDocLabelState> {
|
const FieldDocLabel: React.FC<FieldDocLabelProps> = (props) => {
|
||||||
constructor (props: FieldDocLabelProps) {
|
const [open, setOpen] = React.useState(false);
|
||||||
super(props);
|
|
||||||
this.state = {
|
const onToggleDoc = (state: boolean) => {
|
||||||
open: false,
|
setOpen(state);
|
||||||
|
if (props.onToggleDoc) {
|
||||||
|
props.onToggleDoc(state);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
onToggleDoc = (open: boolean) => {
|
const { label, fieldSpec } = props;
|
||||||
this.setState({
|
const { doc } = fieldSpec || {};
|
||||||
open,
|
|
||||||
}, () => {
|
|
||||||
if (this.props.onToggleDoc) {
|
|
||||||
this.props.onToggleDoc(this.state.open);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
if (doc) {
|
||||||
const {label, fieldSpec} = this.props;
|
return (
|
||||||
const {doc} = fieldSpec || {};
|
<label className="maputnik-doc-wrapper">
|
||||||
|
|
||||||
if (doc) {
|
|
||||||
return <label className="maputnik-doc-wrapper">
|
|
||||||
<div className="maputnik-doc-target">
|
<div className="maputnik-doc-target">
|
||||||
{label}
|
{label}
|
||||||
{'\xa0'}
|
{'\xa0'}
|
||||||
<button
|
<button
|
||||||
aria-label={this.state.open ? "close property documentation" : "open property documentation"}
|
aria-label={open ? 'close property documentation' : 'open property documentation'}
|
||||||
className={`maputnik-doc-button maputnik-doc-button--${this.state.open ? 'open' : 'closed'}`}
|
className={`maputnik-doc-button maputnik-doc-button--${open ? 'open' : 'closed'}`}
|
||||||
onClick={() => this.onToggleDoc(!this.state.open)}
|
onClick={() => onToggleDoc(!open)}
|
||||||
data-wd-key={'field-doc-button-'+label}
|
data-wd-key={'field-doc-button-' + label}
|
||||||
>
|
>
|
||||||
{this.state.open ? <MdHighlightOff /> : <MdInfoOutline />}
|
{open ? <MdHighlightOff /> : <MdInfoOutline />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
}
|
);
|
||||||
else if (label) {
|
} else if (label) {
|
||||||
return <label className="maputnik-doc-wrapper">
|
return (
|
||||||
<div className="maputnik-doc-target">
|
<label className="maputnik-doc-wrapper">
|
||||||
{label}
|
<div className="maputnik-doc-target">{label}</div>
|
||||||
</div>
|
|
||||||
</label>
|
</label>
|
||||||
}
|
);
|
||||||
else {
|
|
||||||
<div />
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
return <div />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FieldDocLabel;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import InputDynamicArray, {FieldDynamicArrayProps as InputDynamicArrayProps} from './InputDynamicArray'
|
import InputDynamicArray, {FieldDynamicArrayProps as InputDynamicArrayProps} from './InputDynamicArray'
|
||||||
import Fieldset from './Fieldset'
|
import Fieldset from './Fieldset'
|
||||||
|
|
||||||
@@ -6,10 +5,12 @@ type FieldDynamicArrayProps = InputDynamicArrayProps & {
|
|||||||
name?: string
|
name?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
|
const FieldDynamicArray: React.FC<FieldDynamicArrayProps> = (props) => {
|
||||||
render() {
|
return (
|
||||||
return <Fieldset label={this.props.label}>
|
<Fieldset label={props.label}>
|
||||||
<InputDynamicArray {...this.props} />
|
<InputDynamicArray {...props} />
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default FieldDynamicArray;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import InputEnum, {InputEnumProps} from './InputEnum'
|
import InputEnum, {InputEnumProps} from './InputEnum'
|
||||||
import Fieldset from './Fieldset';
|
import Fieldset from './Fieldset';
|
||||||
|
|
||||||
@@ -11,10 +10,12 @@ type FieldEnumProps = InputEnumProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default class FieldEnum extends React.Component<FieldEnumProps> {
|
const FieldEnum: React.FC<FieldEnumProps> = (props) => {
|
||||||
render() {
|
return (
|
||||||
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
<Fieldset label={props.label} fieldSpec={props.fieldSpec}>
|
||||||
<InputEnum {...this.props} />
|
<InputEnum {...props} />
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default FieldEnum;
|
||||||
|
|||||||
+188
-216
@@ -111,296 +111,268 @@ type FieldFunctionProps = {
|
|||||||
value?: any
|
value?: any
|
||||||
};
|
};
|
||||||
|
|
||||||
type FieldFunctionState = {
|
|
||||||
dataType: string
|
|
||||||
isEditing: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Supports displaying spec field for zoom function objects
|
/** Supports displaying spec field for zoom function objects
|
||||||
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
||||||
*/
|
*/
|
||||||
export default class FieldFunction extends React.Component<FieldFunctionProps, FieldFunctionState> {
|
const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
||||||
constructor (props: FieldFunctionProps) {
|
const [dataType, setDataType] = React.useState(
|
||||||
super(props);
|
getDataType(props.value, props.fieldSpec)
|
||||||
this.state = {
|
);
|
||||||
dataType: getDataType(props.value, props.fieldSpec),
|
const [isEditing, setIsEditing] = React.useState(false);
|
||||||
isEditing: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromProps(props: Readonly<FieldFunctionProps>, state: FieldFunctionState) {
|
React.useEffect(() => {
|
||||||
// Because otherwise when editing values we end up accidentally changing field type.
|
if (!isEditing) {
|
||||||
if (state.isEditing) {
|
setDataType(getDataType(props.value, props.fieldSpec));
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
else {
|
}, [props.value, props.fieldSpec, isEditing]);
|
||||||
return {
|
|
||||||
isEditing: false,
|
|
||||||
dataType: getDataType(props.value, props.fieldSpec)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getFieldFunctionType(fieldSpec: any) {
|
const getFieldFunctionType = (fieldSpec: any) => {
|
||||||
if (fieldSpec.expression.interpolated) {
|
if (fieldSpec.expression.interpolated) {
|
||||||
return "exponential"
|
return 'exponential';
|
||||||
}
|
}
|
||||||
if (fieldSpec.type === "number") {
|
if (fieldSpec.type === 'number') {
|
||||||
return "interval"
|
return 'interval';
|
||||||
}
|
}
|
||||||
return "categorical"
|
return 'categorical';
|
||||||
}
|
};
|
||||||
|
|
||||||
addStop = () => {
|
const addStop = () => {
|
||||||
const stops = this.props.value.stops.slice(0)
|
const stops = props.value.stops.slice(0);
|
||||||
const lastStop = stops[stops.length - 1]
|
const lastStop = stops[stops.length - 1];
|
||||||
if (typeof lastStop[0] === "object") {
|
if (typeof lastStop[0] === 'object') {
|
||||||
stops.push([
|
stops.push([
|
||||||
{zoom: lastStop[0].zoom + 1, value: lastStop[0].value},
|
{ zoom: lastStop[0].zoom + 1, value: lastStop[0].value },
|
||||||
lastStop[1]
|
lastStop[1],
|
||||||
])
|
]);
|
||||||
}
|
} else {
|
||||||
else {
|
stops.push([lastStop[0] + 1, lastStop[1]]);
|
||||||
stops.push([lastStop[0] + 1, lastStop[1]])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const changedValue = {
|
const changedValue = {
|
||||||
...this.props.value,
|
...props.value,
|
||||||
stops: stops,
|
stops: stops,
|
||||||
}
|
};
|
||||||
|
|
||||||
this.props.onChange(this.props.fieldName, changedValue)
|
props.onChange(props.fieldName, changedValue);
|
||||||
}
|
};
|
||||||
|
|
||||||
deleteExpression = () => {
|
const deleteExpression = () => {
|
||||||
const {fieldSpec, fieldName} = this.props;
|
const { fieldSpec, fieldName } = props;
|
||||||
this.props.onChange(fieldName, fieldSpec.default);
|
props.onChange(fieldName, fieldSpec.default);
|
||||||
this.setState({
|
setDataType('value');
|
||||||
dataType: "value",
|
};
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteStop = (stopIdx: number) => {
|
const deleteStop = (stopIdx: number) => {
|
||||||
const stops = this.props.value.stops.slice(0)
|
const stops = props.value.stops.slice(0);
|
||||||
stops.splice(stopIdx, 1)
|
stops.splice(stopIdx, 1);
|
||||||
|
|
||||||
let changedValue = {
|
let changedValue: any = {
|
||||||
...this.props.value,
|
...props.value,
|
||||||
stops: stops,
|
stops: stops,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (stops.length === 1) {
|
||||||
|
changedValue = stops[0][1];
|
||||||
}
|
}
|
||||||
|
|
||||||
if(stops.length === 1) {
|
props.onChange(props.fieldName, changedValue);
|
||||||
changedValue = stops[0][1]
|
};
|
||||||
}
|
|
||||||
|
|
||||||
this.props.onChange(this.props.fieldName, changedValue)
|
const makeZoomFunction = () => {
|
||||||
}
|
const { value } = props;
|
||||||
|
|
||||||
makeZoomFunction = () => {
|
let zoomFunc: any;
|
||||||
const {value} = this.props;
|
if (typeof value === 'object') {
|
||||||
|
|
||||||
let zoomFunc;
|
|
||||||
if (typeof(value) === "object") {
|
|
||||||
if (value.stops) {
|
if (value.stops) {
|
||||||
zoomFunc = {
|
zoomFunc = {
|
||||||
base: value.base,
|
base: value.base,
|
||||||
stops: value.stops.map((stop: Stop) => {
|
stops: value.stops.map((stop: Stop) => {
|
||||||
return [stop[0].zoom, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
|
return [stop[0].zoom, stop[1] || findDefaultFromSpec(props.fieldSpec)];
|
||||||
})
|
}),
|
||||||
}
|
};
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
zoomFunc = {
|
zoomFunc = {
|
||||||
base: value.base,
|
base: value.base,
|
||||||
stops: [
|
stops: [
|
||||||
[6, findDefaultFromSpec(this.props.fieldSpec)],
|
[6, findDefaultFromSpec(props.fieldSpec)],
|
||||||
[10, findDefaultFromSpec(this.props.fieldSpec)]
|
[10, findDefaultFromSpec(props.fieldSpec)],
|
||||||
]
|
],
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
zoomFunc = {
|
zoomFunc = {
|
||||||
stops: [
|
stops: [
|
||||||
[6, value || findDefaultFromSpec(this.props.fieldSpec)],
|
[6, value || findDefaultFromSpec(props.fieldSpec)],
|
||||||
[10, value || findDefaultFromSpec(this.props.fieldSpec)]
|
[10, value || findDefaultFromSpec(props.fieldSpec)],
|
||||||
]
|
],
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onChange(this.props.fieldName, zoomFunc)
|
props.onChange(props.fieldName, zoomFunc);
|
||||||
}
|
};
|
||||||
|
|
||||||
undoExpression = () => {
|
const undoExpression = () => {
|
||||||
const {value, fieldName} = this.props;
|
const { value, fieldName } = props;
|
||||||
|
|
||||||
if (isGetExpression(value)) {
|
if (isGetExpression(value)) {
|
||||||
this.props.onChange(fieldName, {
|
props.onChange(fieldName, {
|
||||||
"type": "identity",
|
type: 'identity',
|
||||||
"property": value[1]
|
property: value[1],
|
||||||
});
|
|
||||||
this.setState({
|
|
||||||
dataType: "value",
|
|
||||||
});
|
});
|
||||||
|
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 canUndo = () => {
|
||||||
const {value, fieldSpec} = this.props;
|
const { value, fieldSpec } = props;
|
||||||
return (
|
return (
|
||||||
isGetExpression(value) ||
|
isGetExpression(value) ||
|
||||||
isLiteralExpression(value) ||
|
isLiteralExpression(value) ||
|
||||||
isPrimative(value) ||
|
isPrimative(value) ||
|
||||||
(Array.isArray(value) && fieldSpec.type === "array")
|
(Array.isArray(value) && fieldSpec.type === 'array')
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
makeExpression = () => {
|
const makeExpression = () => {
|
||||||
const {value, fieldSpec} = this.props;
|
const { value, fieldSpec } = props;
|
||||||
let expression;
|
let expression;
|
||||||
|
|
||||||
if (typeof(value) === "object" && 'stops' in value) {
|
if (typeof value === 'object' && 'stops' in value) {
|
||||||
expression = styleFunction.convertFunction(value, fieldSpec);
|
expression = styleFunction.convertFunction(value, fieldSpec);
|
||||||
|
} else if (isIdentityProperty(value)) {
|
||||||
|
expression = ['get', value.property];
|
||||||
|
} else {
|
||||||
|
expression = ['literal', value || props.fieldSpec.default];
|
||||||
}
|
}
|
||||||
else if (isIdentityProperty(value)) {
|
props.onChange(props.fieldName, expression);
|
||||||
expression = ["get", value.property];
|
};
|
||||||
}
|
|
||||||
else {
|
|
||||||
expression = ["literal", value || this.props.fieldSpec.default];
|
|
||||||
}
|
|
||||||
this.props.onChange(this.props.fieldName, expression);
|
|
||||||
}
|
|
||||||
|
|
||||||
makeDataFunction = () => {
|
const makeDataFunction = () => {
|
||||||
const functionType = this.getFieldFunctionType(this.props.fieldSpec);
|
const functionType = getFieldFunctionType(props.fieldSpec);
|
||||||
const stopValue = functionType === 'categorical' ? '' : 0;
|
const stopValue = functionType === 'categorical' ? '' : 0;
|
||||||
const {value} = this.props;
|
const { value } = props;
|
||||||
let dataFunc;
|
let dataFunc;
|
||||||
|
|
||||||
if (typeof(value) === "object") {
|
if (typeof value === 'object') {
|
||||||
if (value.stops) {
|
if (value.stops) {
|
||||||
dataFunc = {
|
dataFunc = {
|
||||||
property: "",
|
property: '',
|
||||||
type: functionType,
|
type: functionType,
|
||||||
base: value.base,
|
base: value.base,
|
||||||
stops: value.stops.map((stop: Stop) => {
|
stops: value.stops.map((stop: Stop) => {
|
||||||
return [{zoom: stop[0], value: stopValue}, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
|
return [{ zoom: stop[0], value: stopValue }, stop[1] || findDefaultFromSpec(props.fieldSpec)];
|
||||||
})
|
}),
|
||||||
}
|
};
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
dataFunc = {
|
dataFunc = {
|
||||||
property: "",
|
property: '',
|
||||||
type: functionType,
|
type: functionType,
|
||||||
base: value.base,
|
base: value.base,
|
||||||
stops: [
|
stops: [
|
||||||
[{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)],
|
[{ zoom: 6, value: stopValue }, findDefaultFromSpec(props.fieldSpec)],
|
||||||
[{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)]
|
[{ zoom: 10, value: stopValue }, findDefaultFromSpec(props.fieldSpec)],
|
||||||
]
|
],
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
dataFunc = {
|
dataFunc = {
|
||||||
property: "",
|
property: '',
|
||||||
type: functionType,
|
type: functionType,
|
||||||
base: value.base,
|
base: value.base,
|
||||||
stops: [
|
stops: [
|
||||||
[{zoom: 6, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)],
|
[{ zoom: 6, value: stopValue }, props.value || findDefaultFromSpec(props.fieldSpec)],
|
||||||
[{zoom: 10, value: stopValue}, this.props.value || findDefaultFromSpec(this.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 = () => {
|
return (
|
||||||
this.setState({isEditing: true});
|
<div className={propClass} data-wd-key={'spec-field-container:' + props.fieldName}>
|
||||||
}
|
|
||||||
|
|
||||||
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}>
|
|
||||||
{specField}
|
{specField}
|
||||||
</div>
|
</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 latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
@@ -11,18 +10,19 @@ type FieldIdProps = {
|
|||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class FieldId extends React.Component<FieldIdProps> {
|
const FieldId: React.FC<FieldIdProps> = (props) => {
|
||||||
render() {
|
return (
|
||||||
return <Block label="ID" fieldSpec={latest.layer.id}
|
<Block label="ID" fieldSpec={latest.layer.id}
|
||||||
|
data-wd-key={props.wdKey}
|
||||||
data-wd-key={this.props.wdKey}
|
error={props.error}
|
||||||
error={this.props.error}
|
|
||||||
>
|
>
|
||||||
<InputString
|
<InputString
|
||||||
value={this.props.value}
|
value={props.value}
|
||||||
onInput={this.props.onChange}
|
onInput={props.onChange}
|
||||||
data-wd-key={this.props.wdKey + ".input"}
|
data-wd-key={props.wdKey + ".input"}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default FieldId;
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import React from 'react'
|
|
||||||
import InputJson, {InputJsonProps} from './InputJson'
|
import InputJson, {InputJsonProps} from './InputJson'
|
||||||
|
|
||||||
|
|
||||||
type FieldJsonProps = InputJsonProps & {};
|
type FieldJsonProps = InputJsonProps & {};
|
||||||
|
|
||||||
|
|
||||||
export default class FieldJson extends React.Component<FieldJsonProps> {
|
const FieldJson: React.FC<FieldJsonProps> = (props) => {
|
||||||
render() {
|
return <InputJson {...props} />;
|
||||||
return <InputJson {...this.props} />
|
};
|
||||||
}
|
|
||||||
}
|
export default FieldJson;
|
||||||
|
|||||||
@@ -11,25 +11,25 @@ type FieldMaxZoomInternalProps = {
|
|||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class FieldMaxZoomInternal extends React.Component<FieldMaxZoomInternalProps> {
|
const FieldMaxZoomInternal: React.FC<FieldMaxZoomInternalProps> = (props) => {
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
return (
|
||||||
return <Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom}
|
<Block label={t('Max Zoom')} fieldSpec={latest.layer.maxzoom}
|
||||||
error={this.props.error}
|
error={props.error}
|
||||||
data-wd-key="max-zoom"
|
data-wd-key="max-zoom"
|
||||||
>
|
>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
allowRange={true}
|
allowRange={true}
|
||||||
value={this.props.value}
|
value={props.value}
|
||||||
onChange={this.props.onChange}
|
onChange={props.onChange}
|
||||||
min={latest.layer.maxzoom.minimum}
|
min={latest.layer.maxzoom.minimum}
|
||||||
max={latest.layer.maxzoom.maximum}
|
max={latest.layer.maxzoom.maximum}
|
||||||
default={latest.layer.maxzoom.maximum}
|
default={latest.layer.maxzoom.maximum}
|
||||||
data-wd-key="max-zoom.input"
|
data-wd-key="max-zoom.input"
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal);
|
const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal);
|
||||||
export default FieldMaxZoom;
|
export default FieldMaxZoom;
|
||||||
|
|||||||
@@ -11,25 +11,25 @@ type FieldMinZoomInternalProps = {
|
|||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class FieldMinZoomInternal extends React.Component<FieldMinZoomInternalProps> {
|
const FieldMinZoomInternal: React.FC<FieldMinZoomInternalProps> = (props) => {
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
return (
|
||||||
return <Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom}
|
<Block label={t('Min Zoom')} fieldSpec={latest.layer.minzoom}
|
||||||
error={this.props.error}
|
error={props.error}
|
||||||
data-wd-key="min-zoom"
|
data-wd-key="min-zoom"
|
||||||
>
|
>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
allowRange={true}
|
allowRange={true}
|
||||||
value={this.props.value}
|
value={props.value}
|
||||||
onChange={this.props.onChange}
|
onChange={props.onChange}
|
||||||
min={latest.layer.minzoom.minimum}
|
min={latest.layer.minzoom.minimum}
|
||||||
max={latest.layer.minzoom.maximum}
|
max={latest.layer.minzoom.maximum}
|
||||||
default={latest.layer.minzoom.minimum}
|
default={latest.layer.minzoom.minimum}
|
||||||
data-wd-key='min-zoom.input'
|
data-wd-key='min-zoom.input'
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
|
const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
|
||||||
export default FieldMinZoom;
|
export default FieldMinZoom;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import InputMultiInput, {InputMultiInputProps} from './InputMultiInput'
|
import InputMultiInput, {InputMultiInputProps} from './InputMultiInput'
|
||||||
import Fieldset from './Fieldset'
|
import Fieldset from './Fieldset'
|
||||||
|
|
||||||
@@ -8,10 +7,12 @@ type FieldMultiInputProps = InputMultiInputProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default class FieldMultiInput extends React.Component<FieldMultiInputProps> {
|
const FieldMultiInput: React.FC<FieldMultiInputProps> = (props) => {
|
||||||
render() {
|
return (
|
||||||
return <Fieldset label={this.props.label}>
|
<Fieldset label={props.label}>
|
||||||
<InputMultiInput {...this.props} />
|
<InputMultiInput {...props} />
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default FieldMultiInput;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import InputNumber, {InputNumberProps} from './InputNumber'
|
import InputNumber, {InputNumberProps} from './InputNumber'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
|
|
||||||
@@ -11,10 +10,12 @@ type FieldNumberProps = InputNumberProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default class FieldNumber extends React.Component<FieldNumberProps> {
|
const FieldNumber: React.FC<FieldNumberProps> = (props) => {
|
||||||
render() {
|
return (
|
||||||
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
||||||
<InputNumber {...this.props} />
|
<InputNumber {...props} />
|
||||||
</Block>
|
</Block>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default FieldNumber;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputSelect, {InputSelectProps} from './InputSelect'
|
import InputSelect, {InputSelectProps} from './InputSelect'
|
||||||
|
|
||||||
@@ -11,10 +10,12 @@ type FieldSelectProps = InputSelectProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default class FieldSelect extends React.Component<FieldSelectProps> {
|
const FieldSelect: React.FC<FieldSelectProps> = (props) => {
|
||||||
render() {
|
return (
|
||||||
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
||||||
<InputSelect {...this.props}/>
|
<InputSelect {...props} />
|
||||||
</Block>
|
</Block>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default FieldSelect;
|
||||||
|
|||||||
@@ -13,28 +13,28 @@ type FieldSourceInternalProps = {
|
|||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class FieldSourceInternal extends React.Component<FieldSourceInternalProps> {
|
const FieldSourceInternal: React.FC<FieldSourceInternalProps> = (props) => {
|
||||||
static defaultProps = {
|
const t = props.t;
|
||||||
onChange: () => {},
|
return (
|
||||||
sourceIds: [],
|
<Block
|
||||||
}
|
label={t('Source')}
|
||||||
|
|
||||||
render() {
|
|
||||||
const t = this.props.t;
|
|
||||||
return <Block
|
|
||||||
label={t("Source")}
|
|
||||||
fieldSpec={latest.layer.source}
|
fieldSpec={latest.layer.source}
|
||||||
error={this.props.error}
|
error={props.error}
|
||||||
data-wd-key={this.props.wdKey}
|
data-wd-key={props.wdKey}
|
||||||
>
|
>
|
||||||
<InputAutocomplete
|
<InputAutocomplete
|
||||||
value={this.props.value}
|
value={props.value}
|
||||||
onChange={this.props.onChange}
|
onChange={props.onChange}
|
||||||
options={this.props.sourceIds?.map(src => [src, src])}
|
options={props.sourceIds?.map((src) => [src, src])}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
FieldSourceInternal.defaultProps = {
|
||||||
|
onChange: () => {},
|
||||||
|
sourceIds: [],
|
||||||
|
};
|
||||||
|
|
||||||
const FieldSource = withTranslation()(FieldSourceInternal);
|
const FieldSource = withTranslation()(FieldSourceInternal);
|
||||||
export default FieldSource;
|
export default FieldSource;
|
||||||
|
|||||||
@@ -13,30 +13,29 @@ type FieldSourceLayerInternalProps = {
|
|||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalProps> {
|
const FieldSourceLayerInternal: React.FC<FieldSourceLayerInternalProps> = (props) => {
|
||||||
static defaultProps = {
|
const t = props.t;
|
||||||
onChange: () => {},
|
return (
|
||||||
sourceLayerIds: [],
|
<Block
|
||||||
isFixed: false
|
label={t('Source Layer')}
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const t = this.props.t;
|
|
||||||
return <Block
|
|
||||||
label={t("Source Layer")}
|
|
||||||
fieldSpec={latest.layer['source-layer']}
|
fieldSpec={latest.layer['source-layer']}
|
||||||
data-wd-key="layer-source-layer"
|
data-wd-key="layer-source-layer"
|
||||||
error={this.props.error}
|
error={props.error}
|
||||||
>
|
>
|
||||||
<InputAutocomplete
|
<InputAutocomplete
|
||||||
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
value={props.value}
|
||||||
value={this.props.value}
|
onChange={props.onChange}
|
||||||
onChange={this.props.onChange}
|
options={props.sourceLayerIds?.map((l) => [l, l])}
|
||||||
options={this.props.sourceLayerIds?.map(l => [l, l])}
|
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
FieldSourceLayerInternal.defaultProps = {
|
||||||
|
onChange: () => {},
|
||||||
|
sourceLayerIds: [],
|
||||||
|
isFixed: false,
|
||||||
|
};
|
||||||
|
|
||||||
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
|
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
|
||||||
export default FieldSourceLayer;
|
export default FieldSourceLayer;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputString, {InputStringProps} from './InputString'
|
import InputString, {InputStringProps} from './InputString'
|
||||||
|
|
||||||
@@ -10,10 +9,12 @@ type FieldStringProps = InputStringProps & {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class FieldString extends React.Component<FieldStringProps> {
|
const FieldString: React.FC<FieldStringProps> = (props) => {
|
||||||
render() {
|
return (
|
||||||
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
||||||
<InputString {...this.props} />
|
<InputString {...props} />
|
||||||
</Block>
|
</Block>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default FieldString;
|
||||||
|
|||||||
@@ -14,24 +14,17 @@ type FieldTypeInternalProps = {
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class FieldTypeInternal extends React.Component<FieldTypeInternalProps> {
|
const FieldTypeInternal: React.FC<FieldTypeInternalProps> = (props) => {
|
||||||
static defaultProps = {
|
const t = props.t;
|
||||||
disabled: false,
|
return (
|
||||||
}
|
<Block label={t('Type')} fieldSpec={latest.layer.type}
|
||||||
|
data-wd-key={props.wdKey}
|
||||||
render() {
|
error={props.error}
|
||||||
const t = this.props.t;
|
|
||||||
return <Block label={t("Type")} fieldSpec={latest.layer.type}
|
|
||||||
data-wd-key={this.props.wdKey}
|
|
||||||
error={this.props.error}
|
|
||||||
>
|
>
|
||||||
{this.props.disabled &&
|
{props.disabled && (
|
||||||
<InputString
|
<InputString value={props.value} disabled={true} />
|
||||||
value={this.props.value}
|
)}
|
||||||
disabled={true}
|
{!props.disabled && (
|
||||||
/>
|
|
||||||
}
|
|
||||||
{!this.props.disabled &&
|
|
||||||
<InputSelect
|
<InputSelect
|
||||||
options={[
|
options={[
|
||||||
['background', 'Background'],
|
['background', 'Background'],
|
||||||
@@ -44,14 +37,18 @@ class FieldTypeInternal extends React.Component<FieldTypeInternalProps> {
|
|||||||
['hillshade', 'Hillshade'],
|
['hillshade', 'Hillshade'],
|
||||||
['heatmap', 'Heatmap'],
|
['heatmap', 'Heatmap'],
|
||||||
]}
|
]}
|
||||||
onChange={this.props.onChange}
|
onChange={props.onChange}
|
||||||
value={this.props.value}
|
value={props.value}
|
||||||
data-wd-key={this.props.wdKey + ".select"}
|
data-wd-key={props.wdKey + '.select'}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
</Block>
|
</Block>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
FieldTypeInternal.defaultProps = {
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
|
||||||
const FieldType = withTranslation()(FieldTypeInternal);
|
const FieldType = withTranslation()(FieldTypeInternal);
|
||||||
export default FieldType;
|
export default FieldType;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import InputUrl, {FieldUrlProps as InputUrlProps} from './InputUrl'
|
import InputUrl, {FieldUrlProps as InputUrlProps} from './InputUrl'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
|
|
||||||
@@ -11,12 +10,12 @@ type FieldUrlProps = InputUrlProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default class FieldUrl extends React.Component<FieldUrlProps> {
|
const FieldUrl: React.FC<FieldUrlProps> = (props) => {
|
||||||
render () {
|
return (
|
||||||
return (
|
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
||||||
<Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
<InputUrl {...props} />
|
||||||
<InputUrl {...this.props} />
|
</Block>
|
||||||
</Block>
|
);
|
||||||
);
|
};
|
||||||
}
|
|
||||||
}
|
export default FieldUrl;
|
||||||
|
|||||||
+26
-43
@@ -9,57 +9,40 @@ type FieldsetProps = PropsWithChildren & {
|
|||||||
action?: ReactElement,
|
action?: ReactElement,
|
||||||
};
|
};
|
||||||
|
|
||||||
type FieldsetState = {
|
|
||||||
showDoc: boolean
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class Fieldset extends React.Component<FieldsetProps, FieldsetState> {
|
const Fieldset: React.FC<FieldsetProps> = (props) => {
|
||||||
_labelId: string;
|
const [showDoc, setShowDoc] = React.useState(false);
|
||||||
|
const labelId = React.useRef(generateUniqueId('fieldset_label_'));
|
||||||
|
|
||||||
constructor (props: FieldsetProps) {
|
const onToggleDoc = (val: boolean) => {
|
||||||
super(props);
|
setShowDoc(val);
|
||||||
this._labelId = generateUniqueId(`fieldset_label_`);
|
};
|
||||||
this.state = {
|
|
||||||
showDoc: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggleDoc = (val: boolean) => {
|
return (
|
||||||
this.setState({
|
<div className="maputnik-input-block" role="group" aria-labelledby={labelId.current}>
|
||||||
showDoc: val
|
{props.fieldSpec && (
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
return <div className="maputnik-input-block" role="group" aria-labelledby={this._labelId}>
|
|
||||||
{this.props.fieldSpec &&
|
|
||||||
<div className="maputnik-input-block-label">
|
<div className="maputnik-input-block-label">
|
||||||
<FieldDocLabel
|
<FieldDocLabel
|
||||||
label={this.props.label}
|
label={props.label}
|
||||||
onToggleDoc={this.onToggleDoc}
|
onToggleDoc={onToggleDoc}
|
||||||
fieldSpec={this.props.fieldSpec}
|
fieldSpec={props.fieldSpec}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
{!this.props.fieldSpec &&
|
{!props.fieldSpec && (
|
||||||
<div className="maputnik-input-block-label">
|
<div className="maputnik-input-block-label">
|
||||||
{this.props.label}
|
{props.label}
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
<div className="maputnik-input-block-action">
|
<div className="maputnik-input-block-action">{props.action}</div>
|
||||||
{this.props.action}
|
<div className="maputnik-input-block-content">{props.children}</div>
|
||||||
</div>
|
{props.fieldSpec && (
|
||||||
<div className="maputnik-input-block-content">
|
<div className="maputnik-doc-inline" style={{ display: showDoc ? '' : 'none' }}>
|
||||||
{this.props.children}
|
<Doc fieldSpec={props.fieldSpec} />
|
||||||
</div>
|
|
||||||
{this.props.fieldSpec &&
|
|
||||||
<div
|
|
||||||
className="maputnik-doc-inline"
|
|
||||||
style={{display: this.state.showDoc ? '' : 'none'}}
|
|
||||||
>
|
|
||||||
<Doc fieldSpec={this.props.fieldSpec} />
|
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</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 React from 'react'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import Autocomplete from 'react-autocomplete'
|
import {useCombobox} from 'downshift'
|
||||||
|
|
||||||
|
const MAX_HEIGHT = 140
|
||||||
const MAX_HEIGHT = 140;
|
|
||||||
|
|
||||||
export type InputAutocompleteProps = {
|
export type InputAutocompleteProps = {
|
||||||
value?: string
|
value?: string
|
||||||
options: any[]
|
options?: any[]
|
||||||
onChange(value: string | undefined): unknown
|
onChange?(value: string | undefined): unknown
|
||||||
keepMenuWithinWindowBounds?: boolean
|
|
||||||
'aria-label'?: string
|
'aria-label'?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class InputAutocomplete extends React.Component<InputAutocompleteProps> {
|
export default function InputAutocomplete({
|
||||||
state = {
|
value,
|
||||||
maxHeight: MAX_HEIGHT
|
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 = {
|
const calcMaxHeight = React.useCallback(() => {
|
||||||
onChange: () => {},
|
if (menuRef.current) {
|
||||||
options: [],
|
const space = window.innerHeight - menuRef.current.getBoundingClientRect().top
|
||||||
}
|
setMaxHeight(Math.min(space, MAX_HEIGHT))
|
||||||
|
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}, [])
|
||||||
|
|
||||||
componentDidMount() {
|
const {
|
||||||
this.calcMaxHeight();
|
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() {
|
React.useEffect(() => {
|
||||||
this.calcMaxHeight();
|
if (isOpen) {
|
||||||
}
|
calcMaxHeight()
|
||||||
|
}
|
||||||
|
}, [isOpen, calcMaxHeight])
|
||||||
|
|
||||||
onChange(v: string) {
|
React.useEffect(() => {
|
||||||
this.props.onChange(v === "" ? undefined : v);
|
window.addEventListener('resize', calcMaxHeight)
|
||||||
}
|
return () => window.removeEventListener('resize', calcMaxHeight)
|
||||||
|
}, [calcMaxHeight])
|
||||||
|
|
||||||
render() {
|
React.useEffect(() => {
|
||||||
return <div
|
setInput(value || '')
|
||||||
ref={(el) => {
|
}, [value])
|
||||||
this.autocompleteMenuEl = el;
|
|
||||||
}}
|
return (
|
||||||
>
|
<div className="maputnik-autocomplete">
|
||||||
<Autocomplete
|
<input
|
||||||
menuStyle={{
|
{...getInputProps({
|
||||||
position: "fixed",
|
'aria-label': ariaLabel,
|
||||||
overflow: "auto",
|
className: 'maputnik-string',
|
||||||
maxHeight: this.state.maxHeight,
|
spellCheck: false,
|
||||||
zIndex: '998'
|
onFocus: () => openMenu(),
|
||||||
}}
|
})}
|
||||||
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>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
<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>
|
</div>
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, {type JSX} from 'react'
|
import React, {type JSX} from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
|
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
|
||||||
import {Accordion} from 'react-accessible-accordion';
|
import {Accordion} from 'react-accessible-accordion';
|
||||||
import {MdMoreVert} from 'react-icons/md'
|
import {MdMoreVert} from 'react-icons/md'
|
||||||
|
import { IconContext } from 'react-icons'
|
||||||
import {BackgroundLayerSpecification, LayerSpecification, SourceSpecification} from 'maplibre-gl';
|
import {BackgroundLayerSpecification, LayerSpecification, SourceSpecification} from 'maplibre-gl';
|
||||||
|
|
||||||
import FieldJson from './FieldJson'
|
import FieldJson from './FieldJson'
|
||||||
@@ -86,10 +86,6 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
|||||||
onLayerDestroyed: () => {},
|
onLayerDestroyed: () => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
static childContextTypes = {
|
|
||||||
reactIconBase: PropTypes.object
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props: LayerEditorInternalProps) {
|
constructor(props: LayerEditorInternalProps) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
@@ -116,14 +112,6 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getChildContext () {
|
|
||||||
return {
|
|
||||||
reactIconBase: {
|
|
||||||
size: 14,
|
|
||||||
color: '#8e8e8e',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
changeProperty(group: keyof LayerSpecification | null, property: string, newValue: any) {
|
changeProperty(group: keyof LayerSpecification | null, property: string, newValue: any) {
|
||||||
this.props.onLayerChanged(
|
this.props.onLayerChanged(
|
||||||
@@ -311,53 +299,55 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
|||||||
items[id].handler();
|
items[id].handler();
|
||||||
}
|
}
|
||||||
|
|
||||||
return <section className="maputnik-layer-editor"
|
return <IconContext.Provider value={{size: '14px', color: '#8e8e8e'}}>
|
||||||
role="main"
|
<section className="maputnik-layer-editor"
|
||||||
aria-label={t("Layer editor")}
|
role="main"
|
||||||
>
|
aria-label={t("Layer editor")}
|
||||||
<header>
|
|
||||||
<div className="layer-header">
|
|
||||||
<h2 className="layer-header__title">
|
|
||||||
{t("Layer: {{layerId}}", { layerId: formatLayerId(this.props.layer.id) })}
|
|
||||||
</h2>
|
|
||||||
<div className="layer-header__info">
|
|
||||||
<Wrapper
|
|
||||||
className='more-menu'
|
|
||||||
onSelection={handleSelection}
|
|
||||||
closeOnSelection={false}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
<ul className="more-menu__menu">
|
|
||||||
{Object.keys(items).map((id) => {
|
|
||||||
const item = items[id];
|
|
||||||
return <li key={id}>
|
|
||||||
<MenuItem value={id} className='more-menu__menu__item'>
|
|
||||||
{item.text}
|
|
||||||
</MenuItem>
|
|
||||||
</li>
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</Menu>
|
|
||||||
</Wrapper>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</header>
|
|
||||||
<Accordion
|
|
||||||
allowMultipleExpanded={true}
|
|
||||||
allowZeroExpanded={true}
|
|
||||||
preExpanded={groupIds}
|
|
||||||
>
|
>
|
||||||
{groups}
|
<header>
|
||||||
</Accordion>
|
<div className="layer-header">
|
||||||
</section>
|
<h2 className="layer-header__title">
|
||||||
|
{t("Layer: {{layerId}}", { layerId: formatLayerId(this.props.layer.id) })}
|
||||||
|
</h2>
|
||||||
|
<div className="layer-header__info">
|
||||||
|
<Wrapper
|
||||||
|
className='more-menu'
|
||||||
|
onSelection={handleSelection}
|
||||||
|
closeOnSelection={false}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<ul className="more-menu__menu">
|
||||||
|
{Object.keys(items).map((id) => {
|
||||||
|
const item = items[id];
|
||||||
|
return <li key={id}>
|
||||||
|
<MenuItem value={id} className='more-menu__menu__item'>
|
||||||
|
{item.text}
|
||||||
|
</MenuItem>
|
||||||
|
</li>
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</Menu>
|
||||||
|
</Wrapper>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</header>
|
||||||
|
<Accordion
|
||||||
|
allowMultipleExpanded={true}
|
||||||
|
allowZeroExpanded={true}
|
||||||
|
preExpanded={groupIds}
|
||||||
|
>
|
||||||
|
{groups}
|
||||||
|
</Accordion>
|
||||||
|
</section>
|
||||||
|
</IconContext.Provider>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
|
||||||
import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from 'react-icons/md'
|
import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from 'react-icons/md'
|
||||||
|
import { IconContext } from 'react-icons'
|
||||||
|
|
||||||
import IconLayer from './IconLayer'
|
import IconLayer from './IconLayer'
|
||||||
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
|
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
|
||||||
@@ -91,51 +91,43 @@ class LayerListItem extends React.Component<LayerListItemProps> {
|
|||||||
onLayerVisibilityToggle: () => {},
|
onLayerVisibilityToggle: () => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
static childContextTypes = {
|
|
||||||
reactIconBase: PropTypes.object
|
|
||||||
}
|
|
||||||
|
|
||||||
getChildContext() {
|
|
||||||
return {
|
|
||||||
reactIconBase: { size: 14 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const visibilityAction = this.props.visibility === 'visible' ? 'show' : 'hide';
|
const visibilityAction = this.props.visibility === 'visible' ? 'show' : 'hide';
|
||||||
|
|
||||||
return <li
|
return <IconContext.Provider value={{size: '14px'}}>
|
||||||
id={this.props.id}
|
<li
|
||||||
key={this.props.layerId}
|
id={this.props.id}
|
||||||
onClick={_e => this.props.onLayerSelect(this.props.layerIndex)}
|
key={this.props.layerId}
|
||||||
data-wd-key={"layer-list-item:"+this.props.layerId}
|
onClick={_e => this.props.onLayerSelect(this.props.layerIndex)}
|
||||||
className={classnames({
|
data-wd-key={"layer-list-item:"+this.props.layerId}
|
||||||
"maputnik-layer-list-item": true,
|
className={classnames({
|
||||||
"maputnik-layer-list-item-selected": this.props.isSelected,
|
"maputnik-layer-list-item": true,
|
||||||
[this.props.className!]: true,
|
"maputnik-layer-list-item-selected": this.props.isSelected,
|
||||||
})}>
|
[this.props.className!]: true,
|
||||||
<DraggableLabel {...this.props} />
|
})}>
|
||||||
<span style={{flexGrow: 1}} />
|
<DraggableLabel {...this.props} />
|
||||||
<IconAction
|
<span style={{flexGrow: 1}} />
|
||||||
wdKey={"layer-list-item:"+this.props.layerId+":delete"}
|
<IconAction
|
||||||
action={'delete'}
|
wdKey={"layer-list-item:"+this.props.layerId+":delete"}
|
||||||
classBlockName="delete"
|
action={'delete'}
|
||||||
onClick={_e => this.props.onLayerDestroy!(this.props.layerIndex)}
|
classBlockName="delete"
|
||||||
/>
|
onClick={_e => this.props.onLayerDestroy!(this.props.layerIndex)}
|
||||||
<IconAction
|
/>
|
||||||
wdKey={"layer-list-item:"+this.props.layerId+":copy"}
|
<IconAction
|
||||||
action={'duplicate'}
|
wdKey={"layer-list-item:"+this.props.layerId+":copy"}
|
||||||
classBlockName="duplicate"
|
action={'duplicate'}
|
||||||
onClick={_e => this.props.onLayerCopy!(this.props.layerIndex)}
|
classBlockName="duplicate"
|
||||||
/>
|
onClick={_e => this.props.onLayerCopy!(this.props.layerIndex)}
|
||||||
<IconAction
|
/>
|
||||||
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
|
<IconAction
|
||||||
action={visibilityAction}
|
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
|
||||||
classBlockName="visibility"
|
action={visibilityAction}
|
||||||
classBlockModifier={visibilityAction}
|
classBlockName="visibility"
|
||||||
onClick={_e => this.props.onLayerVisibilityToggle!(this.props.layerIndex)}
|
classBlockModifier={visibilityAction}
|
||||||
/>
|
onClick={_e => this.props.onLayerVisibilityToggle!(this.props.layerIndex)}
|
||||||
</li>
|
/>
|
||||||
|
</li>
|
||||||
|
</IconContext.Provider>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,10 +23,16 @@ type ModalAddState = {
|
|||||||
id: string
|
id: string
|
||||||
source?: string
|
source?: string
|
||||||
'source-layer'?: string
|
'source-layer'?: string
|
||||||
|
error?: string | null
|
||||||
};
|
};
|
||||||
|
|
||||||
class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddState> {
|
class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddState> {
|
||||||
addLayer = () => {
|
addLayer = () => {
|
||||||
|
if (this.props.layers.some(l => l.id === this.state.id)) {
|
||||||
|
this.setState({ error: this.props.t('Layer ID already exists') })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const changedLayers = this.props.layers.slice(0)
|
const changedLayers = this.props.layers.slice(0)
|
||||||
const layer: ModalAddState = {
|
const layer: ModalAddState = {
|
||||||
id: this.state.id,
|
id: this.state.id,
|
||||||
@@ -41,9 +47,10 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
changedLayers.push(layer as LayerSpecification)
|
changedLayers.push(layer as LayerSpecification)
|
||||||
|
this.setState({ error: null }, () => {
|
||||||
this.props.onLayersChange(changedLayers)
|
this.props.onLayersChange(changedLayers)
|
||||||
this.props.onOpenToggle(false)
|
this.props.onOpenToggle(false)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props: ModalAddInternalProps) {
|
constructor(props: ModalAddInternalProps) {
|
||||||
@@ -51,6 +58,7 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
|||||||
const state: ModalAddState = {
|
const state: ModalAddState = {
|
||||||
type: 'fill',
|
type: 'fill',
|
||||||
id: '',
|
id: '',
|
||||||
|
error: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
if(props.sources.length > 0) {
|
if(props.sources.length > 0) {
|
||||||
@@ -129,6 +137,21 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
|||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
const sources = this.getSources(this.state.type);
|
const sources = this.getSources(this.state.type);
|
||||||
const layers = this.getLayersForSource(this.state.source!);
|
const layers = this.getLayersForSource(this.state.source!);
|
||||||
|
let errorElement;
|
||||||
|
if (this.state.error) {
|
||||||
|
errorElement = (
|
||||||
|
<div className="maputnik-modal-error">
|
||||||
|
{this.state.error}
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onClick={() => this.setState({ error: null })}
|
||||||
|
className="maputnik-modal-error-close"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return <Modal
|
return <Modal
|
||||||
isOpen={this.props.isOpen}
|
isOpen={this.props.isOpen}
|
||||||
@@ -137,12 +160,13 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
|||||||
data-wd-key="modal:add-layer"
|
data-wd-key="modal:add-layer"
|
||||||
className="maputnik-add-modal"
|
className="maputnik-add-modal"
|
||||||
>
|
>
|
||||||
|
{errorElement}
|
||||||
<div className="maputnik-add-layer">
|
<div className="maputnik-add-layer">
|
||||||
<FieldId
|
<FieldId
|
||||||
value={this.state.id}
|
value={this.state.id}
|
||||||
wdKey="add-layer.layer-id"
|
wdKey="add-layer.layer-id"
|
||||||
onChange={(v: string) => {
|
onChange={(v: string) => {
|
||||||
this.setState({ id: v })
|
this.setState({ id: v, error: null })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FieldType
|
<FieldType
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import InputSpec, { SpecFieldProps as InputFieldSpecProps } from './InputSpec'
|
import InputSpec, { SpecFieldProps as InputFieldSpecProps } from './InputSpec'
|
||||||
import Fieldset from './Fieldset'
|
import Fieldset from './Fieldset'
|
||||||
@@ -20,27 +19,25 @@ export type SpecFieldProps = InputFieldSpecProps & {
|
|||||||
name?: string
|
name?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class SpecField extends React.Component<SpecFieldProps> {
|
const SpecField: React.FC<SpecFieldProps> = (props) => {
|
||||||
render() {
|
const fieldType = props.fieldSpec?.type;
|
||||||
const fieldType = this.props.fieldSpec?.type;
|
|
||||||
|
|
||||||
const typeBlockFn = typeMap[fieldType!];
|
const typeBlockFn = typeMap[fieldType!];
|
||||||
|
|
||||||
let TypeBlock;
|
let TypeBlock;
|
||||||
if (typeBlockFn) {
|
if (typeBlockFn) {
|
||||||
TypeBlock = typeBlockFn(this.props);
|
TypeBlock = typeBlockFn(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>
|
|
||||||
}
|
}
|
||||||
}
|
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",
|
"url": "https://americanamap.org/style.json",
|
||||||
"thumbnail": "https://github.com/maplibre/maputnik/assets/649392/23fa75ad-63e6-43f5-8837-03cdb0428bac"
|
"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",
|
"id": "dark-matter",
|
||||||
"title": "Dark Matter",
|
"title": "Dark Matter",
|
||||||
@@ -29,30 +41,6 @@
|
|||||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/toner-gl-style@v1.0/style.json",
|
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/toner-gl-style@v1.0/style.json",
|
||||||
"thumbnail": "https://maputnik.github.io/thumbnails/toner.png"
|
"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",
|
"id": "osm-bright",
|
||||||
"title": "OSM Bright",
|
"title": "OSM Bright",
|
||||||
|
|||||||
+22
-2
@@ -91,8 +91,28 @@ export class StyleStore {
|
|||||||
save(mapStyle: StyleSpecification & { id: string }) {
|
save(mapStyle: StyleSpecification & { id: string }) {
|
||||||
mapStyle = style.ensureStyleValidity(mapStyle)
|
mapStyle = style.ensureStyleValidity(mapStyle)
|
||||||
const key = styleKey(mapStyle.id)
|
const key = styleKey(mapStyle.id)
|
||||||
window.localStorage.setItem(key, JSON.stringify(mapStyle))
|
|
||||||
window.localStorage.setItem(storageKeys.latest, mapStyle.id)
|
const saveFn = () => {
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(mapStyle))
|
||||||
|
window.localStorage.setItem(storageKeys.latest, mapStyle.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
saveFn()
|
||||||
|
} catch (e) {
|
||||||
|
// Handle quota exceeded error
|
||||||
|
if (e instanceof DOMException && (
|
||||||
|
e.code === 22 || // Firefox
|
||||||
|
e.code === 1014 || // Firefox
|
||||||
|
e.name === 'QuotaExceededError' ||
|
||||||
|
e.name === 'NS_ERROR_DOM_QUOTA_REACHED'
|
||||||
|
)) {
|
||||||
|
this.purge()
|
||||||
|
saveFn() // Retry after clearing
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
return mapStyle
|
return mapStyle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"Collapse": "Einklappen",
|
"Collapse": "Einklappen",
|
||||||
"Expand": "Ausklappen",
|
"Expand": "Ausklappen",
|
||||||
"Add Layer": "Ebene hinzufügen",
|
"Add Layer": "Ebene hinzufügen",
|
||||||
|
"Layer ID already exists": "Layer-ID existiert bereits",
|
||||||
"Search": "Suche",
|
"Search": "Suche",
|
||||||
"Zoom:": "Zoom:",
|
"Zoom:": "Zoom:",
|
||||||
"Close popup": "Popup schließen",
|
"Close popup": "Popup schließen",
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"Collapse": "Réduire",
|
"Collapse": "Réduire",
|
||||||
"Expand": "Développer",
|
"Expand": "Développer",
|
||||||
"Add Layer": "Ajouter un calque",
|
"Add Layer": "Ajouter un calque",
|
||||||
|
"Layer ID already exists": "L'identifiant du calque existe déjà",
|
||||||
"Search": "Recherche",
|
"Search": "Recherche",
|
||||||
"Zoom:": "Zoom :",
|
"Zoom:": "Zoom :",
|
||||||
"Close popup": "Fermer la fenêtre",
|
"Close popup": "Fermer la fenêtre",
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"Collapse": "הקטנה",
|
"Collapse": "הקטנה",
|
||||||
"Expand": "הגדלה",
|
"Expand": "הגדלה",
|
||||||
"Add Layer": "הוספת שכבה",
|
"Add Layer": "הוספת שכבה",
|
||||||
|
"Layer ID already exists": "מזהה השכבה כבר קיים",
|
||||||
"Search": "חיפוש",
|
"Search": "חיפוש",
|
||||||
"Zoom:": "זום:",
|
"Zoom:": "זום:",
|
||||||
"Close popup": "סגירת החלון",
|
"Close popup": "סגירת החלון",
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"Collapse": "Coprimi",
|
"Collapse": "Coprimi",
|
||||||
"Expand": "Espandi",
|
"Expand": "Espandi",
|
||||||
"Add Layer": "Aggiungi Livello",
|
"Add Layer": "Aggiungi Livello",
|
||||||
|
"Layer ID already exists": "L'ID del layer esiste già",
|
||||||
"Search": "Cerca",
|
"Search": "Cerca",
|
||||||
"Zoom:": "Zoom:",
|
"Zoom:": "Zoom:",
|
||||||
"Close popup": "Chiudi popup",
|
"Close popup": "Chiudi popup",
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"Collapse": "畳む",
|
"Collapse": "畳む",
|
||||||
"Expand": "展開",
|
"Expand": "展開",
|
||||||
"Add Layer": "レイヤー追加",
|
"Add Layer": "レイヤー追加",
|
||||||
|
"Layer ID already exists": "レイヤーIDは既に存在します",
|
||||||
"Search": "検索",
|
"Search": "検索",
|
||||||
"Zoom:": "ズーム:",
|
"Zoom:": "ズーム:",
|
||||||
"Close popup": "ポップアップを閉じる",
|
"Close popup": "ポップアップを閉じる",
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"Collapse": "折叠",
|
"Collapse": "折叠",
|
||||||
"Expand": "展开",
|
"Expand": "展开",
|
||||||
"Add Layer": "添加图层",
|
"Add Layer": "添加图层",
|
||||||
|
"Layer ID already exists": "图层ID已存在",
|
||||||
"Search": "搜索",
|
"Search": "搜索",
|
||||||
"Zoom:": "缩放:",
|
"Zoom:": "缩放:",
|
||||||
"Close popup": "关闭弹出窗口",
|
"Close popup": "关闭弹出窗口",
|
||||||
@@ -149,7 +150,7 @@
|
|||||||
"Raster (Tile URLs)": "栅格数据 (Tile URLs)",
|
"Raster (Tile URLs)": "栅格数据 (Tile URLs)",
|
||||||
"Raster DEM (TileJSON URL)": "栅格高程数据 (TileJSON URL)",
|
"Raster DEM (TileJSON URL)": "栅格高程数据 (TileJSON URL)",
|
||||||
"Raster DEM (XYZ URLs)": "栅格高程数据 (XYZ URLs)",
|
"Raster DEM (XYZ URLs)": "栅格高程数据 (XYZ URLs)",
|
||||||
"Vector (PMTiles)": "__STRING_NOT_TRANSLATED__",
|
"Vector (PMTiles)": "矢量数据 (PMTiles)",
|
||||||
"Image": "图像",
|
"Image": "图像",
|
||||||
"Video": "视频",
|
"Video": "视频",
|
||||||
"Add Source": "添加源",
|
"Add Source": "添加源",
|
||||||
@@ -161,7 +162,7 @@
|
|||||||
"Add a new source to your style. You can only choose the source type and id at creation time!": "向您的样式添加新源。在创建时,您只能选择源类型和ID!",
|
"Add a new source to your style. You can only choose the source type and id at creation time!": "向您的样式添加新源。在创建时,您只能选择源类型和ID!",
|
||||||
"TileJSON URL": "TileJSON URL",
|
"TileJSON URL": "TileJSON URL",
|
||||||
"Tile URL": "瓦片URL",
|
"Tile URL": "瓦片URL",
|
||||||
"Scheme Type": "__STRING_NOT_TRANSLATED__",
|
"Scheme Type": "瓦片方案",
|
||||||
"Coord top left": "左上角坐标",
|
"Coord top left": "左上角坐标",
|
||||||
"Coord top right": "右上角坐标",
|
"Coord top right": "右上角坐标",
|
||||||
"Coord bottom right": "右下角坐标",
|
"Coord bottom right": "右下角坐标",
|
||||||
@@ -171,13 +172,13 @@
|
|||||||
"GeoJSON URL": "GeoJSON URL",
|
"GeoJSON URL": "GeoJSON URL",
|
||||||
"GeoJSON": "GeoJSON",
|
"GeoJSON": "GeoJSON",
|
||||||
"Cluster": "聚合",
|
"Cluster": "聚合",
|
||||||
"PMTiles URL": "__STRING_NOT_TRANSLATED__",
|
"PMTiles URL": "PMTiles URL",
|
||||||
"Tile Size": "__STRING_NOT_TRANSLATED__",
|
"Tile Size": "瓦片大小",
|
||||||
"Encoding": "编码",
|
"Encoding": "编码",
|
||||||
"Error:": "错误:",
|
"Error:": "错误:",
|
||||||
"MapTiler Access Token": "MapTiler 访问令牌",
|
"MapTiler Access Token": "MapTiler 访问令牌",
|
||||||
"Public access token for MapTiler Cloud.": "MapTiler Cloud 的公共访问令牌。",
|
"Public access token for MapTiler Cloud.": "MapTiler Cloud 的公共访问令牌。",
|
||||||
"Learn More": "__STRING_NOT_TRANSLATED__",
|
"Learn More": "了解更多",
|
||||||
"Thunderforest Access Token": "Thunderforest 访问令牌",
|
"Thunderforest Access Token": "Thunderforest 访问令牌",
|
||||||
"Public access token for Thunderforest services.": "Thunderforest 服务的公共访问令牌。",
|
"Public access token for Thunderforest services.": "Thunderforest 服务的公共访问令牌。",
|
||||||
"Stadia Maps API Key": "Stadia Maps API 密钥",
|
"Stadia Maps API Key": "Stadia Maps API 密钥",
|
||||||
|
|||||||
Reference in New Issue
Block a user