mirror of
https://github.com/maputnik/editor.git
synced 2026-07-18 20:07:27 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f88e1b04fa |
@@ -23,7 +23,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with: { persist-credentials: false }
|
with: { persist-credentials: false }
|
||||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with: { persist-credentials: false }
|
with: { persist-credentials: false }
|
||||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
@@ -92,7 +92,7 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with: { persist-credentials: false }
|
with: { persist-credentials: false }
|
||||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
@@ -113,7 +113,7 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with: { persist-credentials: false }
|
with: { persist-credentials: false }
|
||||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
@@ -141,7 +141,7 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
with: { persist-credentials: false }
|
with: { persist-credentials: false }
|
||||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
- name: Use Node.js from nvmrc
|
||||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
with: { persist-credentials: false }
|
with: { persist-credentials: false }
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
- name: Use Node.js from nvmrc
|
||||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
- name: Use Node.js from nvmrc
|
||||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
- name: Use Node.js from nvmrc
|
||||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|||||||
+2
-2
@@ -32,10 +32,10 @@ export default defineConfig({
|
|||||||
"@stylistic": stylisticTs
|
"@stylistic": stylisticTs
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
"react-hooks/rules-of-hooks": "error",
|
||||||
|
"react-hooks/exhaustive-deps": "warn",
|
||||||
"react-refresh/only-export-components": [
|
"react-refresh/only-export-components": [
|
||||||
"warn",
|
"warn",
|
||||||
// Many components are exported as withTranslation()(Component); without
|
|
||||||
// this the rule cannot tell the HOC's result is still a component.
|
|
||||||
{ allowConstantExport: true, extraHOCs: ["withTranslation"] }
|
{ allowConstantExport: true, extraHOCs: ["withTranslation"] }
|
||||||
],
|
],
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
|||||||
Generated
+82
-82
@@ -20,7 +20,7 @@
|
|||||||
"@mapbox/mapbox-gl-rtl-text": "^0.4.0",
|
"@mapbox/mapbox-gl-rtl-text": "^0.4.0",
|
||||||
"@maplibre/maplibre-gl-geocoder": "^1.9.4",
|
"@maplibre/maplibre-gl-geocoder": "^1.9.4",
|
||||||
"@maplibre/maplibre-gl-inspect": "^1.8.2",
|
"@maplibre/maplibre-gl-inspect": "^1.8.2",
|
||||||
"@maplibre/maplibre-gl-style-spec": "^26.1.0",
|
"@maplibre/maplibre-gl-style-spec": "^25.0.2",
|
||||||
"array-move": "^4.0.0",
|
"array-move": "^4.0.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
"downshift": "^9.4.0",
|
"downshift": "^9.4.0",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"i18next": "^26.3.6",
|
"i18next": "^26.3.4",
|
||||||
"i18next-browser-languagedetector": "^8.2.1",
|
"i18next-browser-languagedetector": "^8.2.1",
|
||||||
"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",
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
"@vitejs/plugin-react": "5.2",
|
"@vitejs/plugin-react": "5.2",
|
||||||
"@vitest/coverage-v8": "^4.1.10",
|
"@vitest/coverage-v8": "^4.1.10",
|
||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"eslint": "^10.7.0",
|
"eslint": "^10.6.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.3",
|
"eslint-plugin-react-refresh": "^0.5.3",
|
||||||
@@ -100,14 +100,14 @@
|
|||||||
"istanbul": "^0.4.5",
|
"istanbul": "^0.4.5",
|
||||||
"istanbul-lib-coverage": "^3.2.2",
|
"istanbul-lib-coverage": "^3.2.2",
|
||||||
"nyc": "^18.0.0",
|
"nyc": "^18.0.0",
|
||||||
"postcss": "^8.5.19",
|
"postcss": "^8.5.16",
|
||||||
"react-hot-loader": "^4.13.1",
|
"react-hot-loader": "^4.13.1",
|
||||||
"sass": "^1.101.0",
|
"sass": "^1.101.0",
|
||||||
"stylelint": "^17.14.0",
|
"stylelint": "^17.14.0",
|
||||||
"stylelint-config-recommended-scss": "^17.0.1",
|
"stylelint-config-recommended-scss": "^17.0.1",
|
||||||
"stylelint-scss": "^7.2.0",
|
"stylelint-scss": "^7.2.0",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"typescript-eslint": "^8.64.0",
|
"typescript-eslint": "^8.62.1",
|
||||||
"uuid": "^14.0.1",
|
"uuid": "^14.0.1",
|
||||||
"vite": "^7.3.2",
|
"vite": "^7.3.2",
|
||||||
"vite-plugin-istanbul": "^9.0.1",
|
"vite-plugin-istanbul": "^9.0.1",
|
||||||
@@ -2179,9 +2179,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@maplibre/maplibre-gl-style-spec": {
|
"node_modules/@maplibre/maplibre-gl-style-spec": {
|
||||||
"version": "26.1.0",
|
"version": "25.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-26.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-25.0.2.tgz",
|
||||||
"integrity": "sha512-JaVj0XqDWFb2xfaRFCvF2Gi7qSpCYXXJlVghDDfRUV2dks61YsQKDjlZKT3H+oVwFBxX7CjErz9zjdGTEP/Lug==",
|
"integrity": "sha512-hTNlIVG1gaLKz6UvOV23PYcT8wVS69LLMK9jbGT2hDWzs6hlTbMejeMyQsAF0rebj9czVp77Vhp4KRaUHHUp9g==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mapbox/jsonlint-lines-primitives": "^2.0.3",
|
"@mapbox/jsonlint-lines-primitives": "^2.0.3",
|
||||||
@@ -3741,17 +3741,17 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.64.0",
|
"version": "8.62.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz",
|
||||||
"integrity": "sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==",
|
"integrity": "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.12.2",
|
"@eslint-community/regexpp": "^4.12.2",
|
||||||
"@typescript-eslint/scope-manager": "8.64.0",
|
"@typescript-eslint/scope-manager": "8.62.1",
|
||||||
"@typescript-eslint/type-utils": "8.64.0",
|
"@typescript-eslint/type-utils": "8.62.1",
|
||||||
"@typescript-eslint/utils": "8.64.0",
|
"@typescript-eslint/utils": "8.62.1",
|
||||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
"@typescript-eslint/visitor-keys": "8.62.1",
|
||||||
"ignore": "^7.0.5",
|
"ignore": "^7.0.5",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
"ts-api-utils": "^2.5.0"
|
"ts-api-utils": "^2.5.0"
|
||||||
@@ -3764,15 +3764,15 @@
|
|||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@typescript-eslint/parser": "^8.64.0",
|
"@typescript-eslint/parser": "^8.62.1",
|
||||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||||
"typescript": ">=4.8.4 <6.1.0"
|
"typescript": ">=4.8.4 <6.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
|
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
||||||
"integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
|
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3780,16 +3780,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "8.64.0",
|
"version": "8.62.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.64.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz",
|
||||||
"integrity": "sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==",
|
"integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.64.0",
|
"@typescript-eslint/scope-manager": "8.62.1",
|
||||||
"@typescript-eslint/types": "8.64.0",
|
"@typescript-eslint/types": "8.62.1",
|
||||||
"@typescript-eslint/typescript-estree": "8.64.0",
|
"@typescript-eslint/typescript-estree": "8.62.1",
|
||||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
"@typescript-eslint/visitor-keys": "8.62.1",
|
||||||
"debug": "^4.4.3"
|
"debug": "^4.4.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3805,14 +3805,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/project-service": {
|
"node_modules/@typescript-eslint/project-service": {
|
||||||
"version": "8.64.0",
|
"version": "8.62.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.64.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz",
|
||||||
"integrity": "sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==",
|
"integrity": "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/tsconfig-utils": "^8.64.0",
|
"@typescript-eslint/tsconfig-utils": "^8.62.1",
|
||||||
"@typescript-eslint/types": "^8.64.0",
|
"@typescript-eslint/types": "^8.62.1",
|
||||||
"debug": "^4.4.3"
|
"debug": "^4.4.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3827,14 +3827,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "8.64.0",
|
"version": "8.62.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz",
|
||||||
"integrity": "sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==",
|
"integrity": "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.64.0",
|
"@typescript-eslint/types": "8.62.1",
|
||||||
"@typescript-eslint/visitor-keys": "8.64.0"
|
"@typescript-eslint/visitor-keys": "8.62.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -3845,9 +3845,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||||
"version": "8.64.0",
|
"version": "8.62.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz",
|
||||||
"integrity": "sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==",
|
"integrity": "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3862,15 +3862,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "8.64.0",
|
"version": "8.62.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz",
|
||||||
"integrity": "sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==",
|
"integrity": "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.64.0",
|
"@typescript-eslint/types": "8.62.1",
|
||||||
"@typescript-eslint/typescript-estree": "8.64.0",
|
"@typescript-eslint/typescript-estree": "8.62.1",
|
||||||
"@typescript-eslint/utils": "8.64.0",
|
"@typescript-eslint/utils": "8.62.1",
|
||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
"ts-api-utils": "^2.5.0"
|
"ts-api-utils": "^2.5.0"
|
||||||
},
|
},
|
||||||
@@ -3887,9 +3887,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "8.64.0",
|
"version": "8.62.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.64.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz",
|
||||||
"integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==",
|
"integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3901,16 +3901,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "8.64.0",
|
"version": "8.62.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz",
|
||||||
"integrity": "sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==",
|
"integrity": "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/project-service": "8.64.0",
|
"@typescript-eslint/project-service": "8.62.1",
|
||||||
"@typescript-eslint/tsconfig-utils": "8.64.0",
|
"@typescript-eslint/tsconfig-utils": "8.62.1",
|
||||||
"@typescript-eslint/types": "8.64.0",
|
"@typescript-eslint/types": "8.62.1",
|
||||||
"@typescript-eslint/visitor-keys": "8.64.0",
|
"@typescript-eslint/visitor-keys": "8.62.1",
|
||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
"minimatch": "^10.2.2",
|
"minimatch": "^10.2.2",
|
||||||
"semver": "^7.7.3",
|
"semver": "^7.7.3",
|
||||||
@@ -3968,16 +3968,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "8.64.0",
|
"version": "8.62.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.64.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.1.tgz",
|
||||||
"integrity": "sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==",
|
"integrity": "sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.9.1",
|
"@eslint-community/eslint-utils": "^4.9.1",
|
||||||
"@typescript-eslint/scope-manager": "8.64.0",
|
"@typescript-eslint/scope-manager": "8.62.1",
|
||||||
"@typescript-eslint/types": "8.64.0",
|
"@typescript-eslint/types": "8.62.1",
|
||||||
"@typescript-eslint/typescript-estree": "8.64.0"
|
"@typescript-eslint/typescript-estree": "8.62.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -3992,13 +3992,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "8.64.0",
|
"version": "8.62.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz",
|
||||||
"integrity": "sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==",
|
"integrity": "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.64.0",
|
"@typescript-eslint/types": "8.62.1",
|
||||||
"eslint-visitor-keys": "^5.0.0"
|
"eslint-visitor-keys": "^5.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -5857,9 +5857,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "10.7.0",
|
"version": "10.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz",
|
||||||
"integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==",
|
"integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
@@ -7230,9 +7230,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/i18next": {
|
"node_modules/i18next": {
|
||||||
"version": "26.3.6",
|
"version": "26.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.4.tgz",
|
||||||
"integrity": "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==",
|
"integrity": "sha512-pa7m0d7pBDqGHZxljT+WPFeyFgQ7P7SciPPo1tTqYuO0z4sqADYhwnBESmmGp/wEof1inwdls/k8ZgTg8rxFHA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "individual",
|
"type": "individual",
|
||||||
@@ -7249,7 +7249,7 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5 || ^6 || ^7"
|
"typescript": "^5 || ^6"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"typescript": {
|
"typescript": {
|
||||||
@@ -10755,9 +10755,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.19",
|
"version": "8.5.16",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
||||||
"integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==",
|
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -12985,16 +12985,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript-eslint": {
|
"node_modules/typescript-eslint": {
|
||||||
"version": "8.64.0",
|
"version": "8.62.1",
|
||||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.64.0.tgz",
|
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.1.tgz",
|
||||||
"integrity": "sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==",
|
"integrity": "sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "8.64.0",
|
"@typescript-eslint/eslint-plugin": "8.62.1",
|
||||||
"@typescript-eslint/parser": "8.64.0",
|
"@typescript-eslint/parser": "8.62.1",
|
||||||
"@typescript-eslint/typescript-estree": "8.64.0",
|
"@typescript-eslint/typescript-estree": "8.62.1",
|
||||||
"@typescript-eslint/utils": "8.64.0"
|
"@typescript-eslint/utils": "8.62.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|||||||
+5
-5
@@ -37,7 +37,7 @@
|
|||||||
"@mapbox/mapbox-gl-rtl-text": "^0.4.0",
|
"@mapbox/mapbox-gl-rtl-text": "^0.4.0",
|
||||||
"@maplibre/maplibre-gl-geocoder": "^1.9.4",
|
"@maplibre/maplibre-gl-geocoder": "^1.9.4",
|
||||||
"@maplibre/maplibre-gl-inspect": "^1.8.2",
|
"@maplibre/maplibre-gl-inspect": "^1.8.2",
|
||||||
"@maplibre/maplibre-gl-style-spec": "^26.1.0",
|
"@maplibre/maplibre-gl-style-spec": "^25.0.2",
|
||||||
"array-move": "^4.0.0",
|
"array-move": "^4.0.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
"downshift": "^9.4.0",
|
"downshift": "^9.4.0",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"i18next": "^26.3.6",
|
"i18next": "^26.3.4",
|
||||||
"i18next-browser-languagedetector": "^8.2.1",
|
"i18next-browser-languagedetector": "^8.2.1",
|
||||||
"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",
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
"@vitejs/plugin-react": "5.2",
|
"@vitejs/plugin-react": "5.2",
|
||||||
"@vitest/coverage-v8": "^4.1.10",
|
"@vitest/coverage-v8": "^4.1.10",
|
||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"eslint": "^10.7.0",
|
"eslint": "^10.6.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"eslint-plugin-react-hooks": "^7.1.1",
|
||||||
"eslint-plugin-react-refresh": "^0.5.3",
|
"eslint-plugin-react-refresh": "^0.5.3",
|
||||||
@@ -134,14 +134,14 @@
|
|||||||
"istanbul": "^0.4.5",
|
"istanbul": "^0.4.5",
|
||||||
"istanbul-lib-coverage": "^3.2.2",
|
"istanbul-lib-coverage": "^3.2.2",
|
||||||
"nyc": "^18.0.0",
|
"nyc": "^18.0.0",
|
||||||
"postcss": "^8.5.19",
|
"postcss": "^8.5.16",
|
||||||
"react-hot-loader": "^4.13.1",
|
"react-hot-loader": "^4.13.1",
|
||||||
"sass": "^1.101.0",
|
"sass": "^1.101.0",
|
||||||
"stylelint": "^17.14.0",
|
"stylelint": "^17.14.0",
|
||||||
"stylelint-config-recommended-scss": "^17.0.1",
|
"stylelint-config-recommended-scss": "^17.0.1",
|
||||||
"stylelint-scss": "^7.2.0",
|
"stylelint-scss": "^7.2.0",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"typescript-eslint": "^8.64.0",
|
"typescript-eslint": "^8.62.1",
|
||||||
"uuid": "^14.0.1",
|
"uuid": "^14.0.1",
|
||||||
"vite": "^7.3.2",
|
"vite": "^7.3.2",
|
||||||
"vite-plugin-istanbul": "^9.0.1",
|
"vite-plugin-istanbul": "^9.0.1",
|
||||||
|
|||||||
+548
-505
File diff suppressed because it is too large
Load Diff
@@ -13,41 +13,38 @@ type AppLayoutInternalProps = {
|
|||||||
modals?: React.ReactNode
|
modals?: React.ReactNode
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
|
const AppLayoutInternal: React.FC<AppLayoutInternalProps> = (props) => {
|
||||||
|
document.body.dir = props.i18n.dir();
|
||||||
render() {
|
|
||||||
document.body.dir = this.props.i18n.dir();
|
|
||||||
|
|
||||||
return <IconContext.Provider value={{size: "14px"}}>
|
return <IconContext.Provider value={{size: "14px"}}>
|
||||||
<div className="maputnik-layout">
|
<div className="maputnik-layout">
|
||||||
{this.props.toolbar}
|
{props.toolbar}
|
||||||
<div className="maputnik-layout-main">
|
<div className="maputnik-layout-main">
|
||||||
{this.props.codeEditor && <div className="maputnik-layout-code-editor">
|
{props.codeEditor && <div className="maputnik-layout-code-editor">
|
||||||
<ScrollContainer>
|
<ScrollContainer>
|
||||||
{this.props.codeEditor}
|
{props.codeEditor}
|
||||||
</ScrollContainer>
|
</ScrollContainer>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{!this.props.codeEditor && <>
|
{!props.codeEditor && <>
|
||||||
<div className="maputnik-layout-list">
|
<div className="maputnik-layout-list">
|
||||||
{this.props.layerList}
|
{props.layerList}
|
||||||
</div>
|
</div>
|
||||||
<div className="maputnik-layout-drawer">
|
<div className="maputnik-layout-drawer">
|
||||||
<ScrollContainer>
|
<ScrollContainer>
|
||||||
{this.props.layerEditor}
|
{props.layerEditor}
|
||||||
</ScrollContainer>
|
</ScrollContainer>
|
||||||
</div>
|
</div>
|
||||||
</>}
|
</>}
|
||||||
{this.props.map}
|
{props.map}
|
||||||
</div>
|
</div>
|
||||||
{this.props.bottom && <div className="maputnik-layout-bottom">
|
{props.bottom && <div className="maputnik-layout-bottom">
|
||||||
{this.props.bottom}
|
{props.bottom}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{this.props.modals}
|
{props.modals}
|
||||||
</div>
|
</div>
|
||||||
</IconContext.Provider>;
|
</IconContext.Provider>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const AppLayout = withTranslation()(AppLayoutInternal);
|
export const AppLayout = withTranslation()(AppLayoutInternal);
|
||||||
|
|||||||
@@ -13,18 +13,16 @@ type AppMessagePanelInternalProps = {
|
|||||||
selectedLayerIndex?: number
|
selectedLayerIndex?: number
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
|
const AppMessagePanelInternal: React.FC<AppMessagePanelInternalProps> = ({
|
||||||
static defaultProps = {
|
onLayerSelect = () => { },
|
||||||
onLayerSelect: () => { },
|
...props
|
||||||
};
|
}) => {
|
||||||
|
const { t, selectedLayerIndex } = props;
|
||||||
render() {
|
const errors = props.errors?.map((error, idx) => {
|
||||||
const { t, selectedLayerIndex } = this.props;
|
|
||||||
const errors = this.props.errors?.map((error, idx) => {
|
|
||||||
let content;
|
let content;
|
||||||
if (error.parsed && error.parsed.type === "layer") {
|
if (error.parsed && error.parsed.type === "layer") {
|
||||||
const { parsed } = error;
|
const { parsed } = error;
|
||||||
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
|
const layerId = props.mapStyle?.layers[parsed.data.index].id;
|
||||||
content = (
|
content = (
|
||||||
<>
|
<>
|
||||||
{t("Layer")} <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
{t("Layer")} <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
||||||
@@ -33,7 +31,7 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
|
|||||||
—
|
—
|
||||||
<button
|
<button
|
||||||
className="maputnik-message-panel__switch-button"
|
className="maputnik-message-panel__switch-button"
|
||||||
onClick={() => this.props.onLayerSelect!(parsed.data.index)}
|
onClick={() => onLayerSelect!(parsed.data.index)}
|
||||||
>
|
>
|
||||||
{t("switch to layer")}
|
{t("switch to layer")}
|
||||||
</button>
|
</button>
|
||||||
@@ -50,7 +48,7 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
|
|||||||
</p>;
|
</p>;
|
||||||
});
|
});
|
||||||
|
|
||||||
const infos = this.props.infos?.map((m, i) => {
|
const infos = props.infos?.map((m, i) => {
|
||||||
return <p key={"info-" + i}>{m}</p>;
|
return <p key={"info-" + i}>{m}</p>;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,7 +56,6 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
|
|||||||
{errors}
|
{errors}
|
||||||
{infos}
|
{infos}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
|
export const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
|
||||||
|
|||||||
@@ -31,11 +31,9 @@ type IconTextProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
class IconText extends React.Component<IconTextProps> {
|
const IconText: React.FC<IconTextProps> = (props) => {
|
||||||
render() {
|
return <span className="maputnik-icon-text">{props.children}</span>;
|
||||||
return <span className="maputnik-icon-text">{this.props.children}</span>;
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ToolbarLinkProps = {
|
type ToolbarLinkProps = {
|
||||||
className?: string
|
className?: string
|
||||||
@@ -43,35 +41,31 @@ type ToolbarLinkProps = {
|
|||||||
href?: string
|
href?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
class ToolbarLink extends React.Component<ToolbarLinkProps> {
|
const ToolbarLink: React.FC<ToolbarLinkProps> = (props) => {
|
||||||
render() {
|
|
||||||
return <a
|
return <a
|
||||||
className={classnames("maputnik-toolbar-link", this.props.className)}
|
className={classnames("maputnik-toolbar-link", props.className)}
|
||||||
href={this.props.href}
|
href={props.href}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
data-wd-key="toolbar:link"
|
data-wd-key="toolbar:link"
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{props.children}
|
||||||
</a>;
|
</a>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
type ToolbarSelectProps = {
|
type ToolbarSelectProps = {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
wdKey?: string
|
wdKey?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
class ToolbarSelect extends React.Component<ToolbarSelectProps> {
|
const ToolbarSelect: React.FC<ToolbarSelectProps> = (props) => {
|
||||||
render() {
|
|
||||||
return <div
|
return <div
|
||||||
className='maputnik-toolbar-select'
|
className='maputnik-toolbar-select'
|
||||||
data-wd-key={this.props.wdKey}
|
data-wd-key={props.wdKey}
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{props.children}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
type ToolbarActionProps = {
|
type ToolbarActionProps = {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
@@ -79,17 +73,15 @@ type ToolbarActionProps = {
|
|||||||
wdKey?: string
|
wdKey?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
class ToolbarAction extends React.Component<ToolbarActionProps> {
|
const ToolbarAction: React.FC<ToolbarActionProps> = (props) => {
|
||||||
render() {
|
|
||||||
return <button
|
return <button
|
||||||
className='maputnik-toolbar-action'
|
className='maputnik-toolbar-action'
|
||||||
data-wd-key={this.props.wdKey}
|
data-wd-key={props.wdKey}
|
||||||
onClick={this.props.onClick}
|
onClick={props.onClick}
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{props.children}
|
||||||
</button>;
|
</button>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
|
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
|
||||||
|
|
||||||
@@ -108,26 +100,16 @@ type AppToolbarInternalProps = {
|
|||||||
renderer?: string
|
renderer?: string
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
const AppToolbarInternal: React.FC<AppToolbarInternalProps> = (props) => {
|
||||||
state = {
|
function handleSelection(val: MapState) {
|
||||||
isOpen: {
|
props.onSetMapState(val);
|
||||||
settings: false,
|
|
||||||
sources: false,
|
|
||||||
open: false,
|
|
||||||
add: false,
|
|
||||||
export: false,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSelection(val: MapState) {
|
|
||||||
this.props.onSetMapState(val);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLanguageChange(val: string) {
|
function handleLanguageChange(val: string) {
|
||||||
this.props.i18n.changeLanguage(val);
|
props.i18n.changeLanguage(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSkip = (target: string) => {
|
const onSkip = (target: string) => {
|
||||||
if (target === "map") {
|
if (target === "map") {
|
||||||
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
|
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
|
||||||
}
|
}
|
||||||
@@ -137,8 +119,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
|
||||||
const views = [
|
const views = [
|
||||||
{
|
{
|
||||||
id: "map",
|
id: "map",
|
||||||
@@ -149,7 +130,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|||||||
id: "inspect",
|
id: "inspect",
|
||||||
group: "general",
|
group: "general",
|
||||||
title: t("Inspect"),
|
title: t("Inspect"),
|
||||||
disabled: this.props.renderer === "ol",
|
disabled: props.renderer === "ol",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "filter-deuteranopia",
|
id: "filter-deuteranopia",
|
||||||
@@ -178,7 +159,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const currentView = views.find((view) => {
|
const currentView = views.find((view) => {
|
||||||
return view.id === this.props.mapState;
|
return view.id === props.mapState;
|
||||||
});
|
});
|
||||||
|
|
||||||
return <nav className='maputnik-toolbar'>
|
return <nav className='maputnik-toolbar'>
|
||||||
@@ -190,21 +171,21 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|||||||
<button
|
<button
|
||||||
data-wd-key="root:skip:layer-list"
|
data-wd-key="root:skip:layer-list"
|
||||||
className="maputnik-toolbar-skip"
|
className="maputnik-toolbar-skip"
|
||||||
onClick={_e => this.onSkip("layer-list")}
|
onClick={_e => onSkip("layer-list")}
|
||||||
>
|
>
|
||||||
{t("Layers list")}
|
{t("Layers list")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
data-wd-key="root:skip:layer-editor"
|
data-wd-key="root:skip:layer-editor"
|
||||||
className="maputnik-toolbar-skip"
|
className="maputnik-toolbar-skip"
|
||||||
onClick={_e => this.onSkip("layer-editor")}
|
onClick={_e => onSkip("layer-editor")}
|
||||||
>
|
>
|
||||||
{t("Layer editor")}
|
{t("Layer editor")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
data-wd-key="root:skip:map-view"
|
data-wd-key="root:skip:map-view"
|
||||||
className="maputnik-toolbar-skip"
|
className="maputnik-toolbar-skip"
|
||||||
onClick={_e => this.onSkip("map")}
|
onClick={_e => onSkip("map")}
|
||||||
>
|
>
|
||||||
{t("Map view")}
|
{t("Map view")}
|
||||||
</button>
|
</button>
|
||||||
@@ -222,27 +203,27 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
|
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
|
||||||
<ToolbarAction wdKey="nav:open" onClick={() => this.props.onToggleModal("open")}>
|
<ToolbarAction wdKey="nav:open" onClick={() => props.onToggleModal("open")}>
|
||||||
<MdOpenInBrowser />
|
<MdOpenInBrowser />
|
||||||
<IconText>{t("Open")}</IconText>
|
<IconText>{t("Open")}</IconText>
|
||||||
</ToolbarAction>
|
</ToolbarAction>
|
||||||
<ToolbarAction wdKey="nav:export" onClick={() => this.props.onToggleModal("export")}>
|
<ToolbarAction wdKey="nav:export" onClick={() => props.onToggleModal("export")}>
|
||||||
<MdSave />
|
<MdSave />
|
||||||
<IconText>{t("Save")}</IconText>
|
<IconText>{t("Save")}</IconText>
|
||||||
</ToolbarAction>
|
</ToolbarAction>
|
||||||
<ToolbarAction wdKey="nav:code-editor" onClick={() => this.props.onToggleModal("codeEditor")}>
|
<ToolbarAction wdKey="nav:code-editor" onClick={() => props.onToggleModal("codeEditor")}>
|
||||||
<MdCode />
|
<MdCode />
|
||||||
<IconText>{t("Code Editor")}</IconText>
|
<IconText>{t("Code Editor")}</IconText>
|
||||||
</ToolbarAction>
|
</ToolbarAction>
|
||||||
<ToolbarAction wdKey="nav:sources" onClick={() => this.props.onToggleModal("sources")}>
|
<ToolbarAction wdKey="nav:sources" onClick={() => props.onToggleModal("sources")}>
|
||||||
<MdLayers />
|
<MdLayers />
|
||||||
<IconText>{t("Data Sources")}</IconText>
|
<IconText>{t("Data Sources")}</IconText>
|
||||||
</ToolbarAction>
|
</ToolbarAction>
|
||||||
<ToolbarAction wdKey="nav:settings" onClick={() => this.props.onToggleModal("settings")}>
|
<ToolbarAction wdKey="nav:settings" onClick={() => props.onToggleModal("settings")}>
|
||||||
<MdSettings />
|
<MdSettings />
|
||||||
<IconText>{t("Style Settings")}</IconText>
|
<IconText>{t("Style Settings")}</IconText>
|
||||||
</ToolbarAction>
|
</ToolbarAction>
|
||||||
<ToolbarAction wdKey="nav:global-state" onClick={() => this.props.onToggleModal("globalState")}>
|
<ToolbarAction wdKey="nav:global-state" onClick={() => props.onToggleModal("globalState")}>
|
||||||
<MdPublic />
|
<MdPublic />
|
||||||
<IconText>{t("Global State")}</IconText>
|
<IconText>{t("Global State")}</IconText>
|
||||||
</ToolbarAction>
|
</ToolbarAction>
|
||||||
@@ -253,7 +234,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|||||||
<select
|
<select
|
||||||
className="maputnik-select"
|
className="maputnik-select"
|
||||||
data-wd-key="maputnik-select"
|
data-wd-key="maputnik-select"
|
||||||
onChange={(e) => this.handleSelection(e.target.value as MapState)}
|
onChange={(e) => handleSelection(e.target.value as MapState)}
|
||||||
value={currentView?.id}
|
value={currentView?.id}
|
||||||
>
|
>
|
||||||
{views.filter(v => v.group === "general").map((item) => {
|
{views.filter(v => v.group === "general").map((item) => {
|
||||||
@@ -282,8 +263,8 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|||||||
<select
|
<select
|
||||||
className="maputnik-select"
|
className="maputnik-select"
|
||||||
data-wd-key="maputnik-lang-select"
|
data-wd-key="maputnik-lang-select"
|
||||||
onChange={(e) => this.handleLanguageChange(e.target.value)}
|
onChange={(e) => handleLanguageChange(e.target.value)}
|
||||||
value={this.props.i18n.language}
|
value={props.i18n.language}
|
||||||
>
|
>
|
||||||
{Object.entries(supportedLanguages).map(([code, name]) => {
|
{Object.entries(supportedLanguages).map(([code, name]) => {
|
||||||
return (
|
return (
|
||||||
@@ -303,7 +284,6 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>;
|
</nav>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const AppToolbar = withTranslation()(AppToolbarInternal);
|
export const AppToolbar = withTranslation()(AppToolbarInternal);
|
||||||
|
|||||||
+27
-48
@@ -1,4 +1,4 @@
|
|||||||
import React, {type CSSProperties, type PropsWithChildren, type SyntheticEvent} from "react";
|
import React, {type CSSProperties, type PropsWithChildren, type SyntheticEvent, useRef, useState} from "react";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
import { FieldDocLabel } from "./FieldDocLabel";
|
import { FieldDocLabel } from "./FieldDocLabel";
|
||||||
import { Doc } from "./Doc";
|
import { Doc } from "./Doc";
|
||||||
@@ -14,32 +14,13 @@ export type BlockProps = PropsWithChildren & {
|
|||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
};
|
};
|
||||||
|
|
||||||
type BlockState = {
|
|
||||||
showDoc: boolean
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Wrap a component with a label */
|
/** Wrap a component with a label */
|
||||||
export class Block extends React.Component<BlockProps, BlockState> {
|
export const Block: React.FC<BlockProps> = (props) => {
|
||||||
_blockEl: HTMLDivElement | null = null;
|
const [showDoc, setShowDoc] = useState(false);
|
||||||
|
const blockEl = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
constructor (props: BlockProps) {
|
const onToggleDoc = (val: boolean) => {
|
||||||
super(props);
|
setShowDoc(val);
|
||||||
this.state = {
|
|
||||||
showDoc: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) {
|
|
||||||
const value = e.target.value;
|
|
||||||
if (this.props.onChange) {
|
|
||||||
return this.props.onChange(value === "" ? undefined : value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggleDoc = (val: boolean) => {
|
|
||||||
this.setState({
|
|
||||||
showDoc: val
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,9 +29,9 @@ export class Block extends React.Component<BlockProps, BlockState> {
|
|||||||
* causing the picker to reopen. This causes a scenario where the picker can
|
* causing the picker to reopen. This causes a scenario where the picker can
|
||||||
* never be closed once open.
|
* never be closed once open.
|
||||||
*/
|
*/
|
||||||
onLabelClick = (event: SyntheticEvent<any, any>) => {
|
const onLabelClick = (event: SyntheticEvent<any, any>) => {
|
||||||
const el = event.nativeEvent.target;
|
const el = event.nativeEvent.target;
|
||||||
const contains = this._blockEl?.contains(el);
|
const contains = blockEl.current?.contains(el);
|
||||||
|
|
||||||
if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) {
|
if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -60,45 +41,43 @@ export class Block extends React.Component<BlockProps, BlockState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
return <label style={props.style}
|
||||||
return <label style={this.props.style}
|
data-wd-key={props["data-wd-key"]}
|
||||||
data-wd-key={this.props["data-wd-key"]}
|
|
||||||
className={classnames({
|
className={classnames({
|
||||||
"maputnik-input-block": true,
|
"maputnik-input-block": true,
|
||||||
"maputnik-input-block--wide": this.props.wideMode,
|
"maputnik-input-block--wide": props.wideMode,
|
||||||
"maputnik-action-block": this.props.action,
|
"maputnik-action-block": props.action,
|
||||||
"maputnik-input-block--error": this.props.error
|
"maputnik-input-block--error": props.error
|
||||||
})}
|
})}
|
||||||
onClick={this.onLabelClick}
|
onClick={onLabelClick}
|
||||||
>
|
>
|
||||||
{this.props.fieldSpec &&
|
{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">
|
||||||
{this.props.action}
|
{props.action}
|
||||||
</div>
|
</div>
|
||||||
<div className="maputnik-input-block-content" ref={el => {this._blockEl = el;}}>
|
<div className="maputnik-input-block-content" ref={blockEl}>
|
||||||
{this.props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
{this.props.fieldSpec &&
|
{props.fieldSpec &&
|
||||||
<div
|
<div
|
||||||
className="maputnik-doc-inline"
|
className="maputnik-doc-inline"
|
||||||
style={{display: this.state.showDoc ? "" : "none"}}
|
style={{display: showDoc ? "" : "none"}}
|
||||||
>
|
>
|
||||||
<Doc fieldSpec={this.props.fieldSpec} />
|
<Doc fieldSpec={props.fieldSpec} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</label>;
|
</label>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,25 +9,19 @@ type CollapseProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export class Collapse extends React.Component<CollapseProps> {
|
export const Collapse: React.FC<CollapseProps> = ({isActive = true, children}) => {
|
||||||
static defaultProps = {
|
|
||||||
isActive: true
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (reducedMotionEnabled()) {
|
if (reducedMotionEnabled()) {
|
||||||
return (
|
return (
|
||||||
<div style={{display: this.props.isActive ? "block" : "none"}}>
|
<div style={{display: isActive ? "block" : "none"}}>
|
||||||
{this.props.children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return (
|
return (
|
||||||
<ReactCollapse isOpened={this.props.isActive}>
|
<ReactCollapse isOpened={isActive}>
|
||||||
{this.props.children}
|
{children}
|
||||||
</ReactCollapse>
|
</ReactCollapse>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ type CollapserProps = {
|
|||||||
style?: object
|
style?: object
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Collapser extends React.Component<CollapserProps> {
|
export const Collapser: React.FC<CollapserProps> = (props) => {
|
||||||
render() {
|
|
||||||
const iconStyle = {
|
const iconStyle = {
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
...this.props.style,
|
...props.style,
|
||||||
|
};
|
||||||
|
return props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />;
|
||||||
};
|
};
|
||||||
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useRef } from "react";
|
||||||
import {PiListPlusBold} from "react-icons/pi";
|
import {PiListPlusBold} from "react-icons/pi";
|
||||||
import {TbMathFunction} from "react-icons/tb";
|
import {TbMathFunction} from "react-icons/tb";
|
||||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
||||||
@@ -68,32 +68,19 @@ export type Stop = [{
|
|||||||
value: number
|
value: number
|
||||||
}, number];
|
}, number];
|
||||||
|
|
||||||
class DataPropertyInternal extends React.Component<DataPropertyInternalProps, DataPropertyState> {
|
const DataPropertyInternal: React.FC<DataPropertyInternalProps> = (props) => {
|
||||||
state = {
|
// Kept in a ref rather than state: the original recomputed these on every
|
||||||
refs: {} as {[key: number]: string}
|
// render via getDerivedStateFromProps, which as state would mean setting
|
||||||
};
|
// state during render on every pass.
|
||||||
|
const refs = useRef<{[key: number]: string}>({});
|
||||||
componentDidMount() {
|
// setStopRefs returns undefined when no new stop needs a ref; as in the
|
||||||
const newRefs = setStopRefs(this.props, this.state);
|
// original, only replace the map when it actually produced one.
|
||||||
|
const newStopRefs = setStopRefs(props, { refs: refs.current });
|
||||||
if(newRefs) {
|
if (newStopRefs) {
|
||||||
this.setState({
|
refs.current = newStopRefs;
|
||||||
refs: newRefs
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props: Readonly<DataPropertyInternalProps>, state: DataPropertyState) {
|
function getFieldFunctionType(fieldSpec: any) {
|
||||||
const newRefs = setStopRefs(props, state);
|
|
||||||
if(newRefs) {
|
|
||||||
return {
|
|
||||||
refs: newRefs
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getFieldFunctionType(fieldSpec: any) {
|
|
||||||
if (fieldSpec.expression.interpolated) {
|
if (fieldSpec.expression.interpolated) {
|
||||||
return "exponential";
|
return "exponential";
|
||||||
}
|
}
|
||||||
@@ -103,7 +90,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
|||||||
return "categorical";
|
return "categorical";
|
||||||
}
|
}
|
||||||
|
|
||||||
getDataFunctionTypes(fieldSpec: any) {
|
function getDataFunctionTypes(fieldSpec: any) {
|
||||||
if (fieldSpec.expression.interpolated) {
|
if (fieldSpec.expression.interpolated) {
|
||||||
return ["interpolate", "categorical", "interval", "exponential", "identity"];
|
return ["interpolate", "categorical", "interval", "exponential", "identity"];
|
||||||
}
|
}
|
||||||
@@ -113,11 +100,11 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Order the stops altering the refs to reflect their new position.
|
// Order the stops altering the refs to reflect their new position.
|
||||||
orderStopsByZoom(stops: Stop[]) {
|
function orderStopsByZoom(stops: Stop[]) {
|
||||||
const mappedWithRef = stops
|
const mappedWithRef = stops
|
||||||
.map((stop, idx) => {
|
.map((stop, idx) => {
|
||||||
return {
|
return {
|
||||||
ref: this.state.refs[idx],
|
ref: refs.current[idx],
|
||||||
data: stop
|
data: stop
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -131,14 +118,12 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
|||||||
newRefs[idx] = stop.ref;
|
newRefs[idx] = stop.ref;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setState({
|
refs.current = newRefs;
|
||||||
refs: newRefs
|
|
||||||
});
|
|
||||||
|
|
||||||
return mappedWithRef.map((item) => item.data);
|
return mappedWithRef.map((item) => item.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange = (fieldName: string, value: any) => {
|
const onChange = (fieldName: string, value: any) => {
|
||||||
if (value.type === "identity") {
|
if (value.type === "identity") {
|
||||||
value = {
|
value = {
|
||||||
type: value.type,
|
type: value.type,
|
||||||
@@ -152,17 +137,17 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
|||||||
type: value.type,
|
type: value.type,
|
||||||
// Default props if they don't already exist.
|
// Default props if they don't already exist.
|
||||||
stops: [
|
stops: [
|
||||||
[{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec as any)],
|
[{zoom: 6, value: stopValue}, findDefaultFromSpec(props.fieldSpec as any)],
|
||||||
[{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec as any)]
|
[{zoom: 10, value: stopValue}, findDefaultFromSpec(props.fieldSpec as any)]
|
||||||
],
|
],
|
||||||
...value,
|
...value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
this.props.onChange!(fieldName, value);
|
props.onChange!(fieldName, value);
|
||||||
};
|
};
|
||||||
|
|
||||||
changeStop(changeIdx: number, stopData: { zoom: number | undefined, value: number }, value: number) {
|
function changeStop(changeIdx: number, stopData: { zoom: number | undefined, value: number }, value: number) {
|
||||||
const stops = this.props.value?.stops.slice(0) || [];
|
const stops = props.value?.stops.slice(0) || [];
|
||||||
// const changedStop = stopData.zoom === undefined ? stopData.value : stopData
|
// const changedStop = stopData.zoom === undefined ? stopData.value : stopData
|
||||||
stops[changeIdx] = [
|
stops[changeIdx] = [
|
||||||
{
|
{
|
||||||
@@ -172,74 +157,73 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
|||||||
value
|
value
|
||||||
];
|
];
|
||||||
|
|
||||||
const orderedStops = this.orderStopsByZoom(stops);
|
const orderedStops = orderStopsByZoom(stops);
|
||||||
|
|
||||||
const changedValue = {
|
const changedValue = {
|
||||||
...this.props.value,
|
...props.value,
|
||||||
stops: orderedStops,
|
stops: orderedStops,
|
||||||
};
|
};
|
||||||
this.onChange(this.props.fieldName, changedValue);
|
onChange(props.fieldName, changedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
changeBase(newValue: number | undefined) {
|
function changeBase(newValue: number | undefined) {
|
||||||
const changedValue = {
|
const changedValue = {
|
||||||
...this.props.value,
|
...props.value,
|
||||||
base: newValue
|
base: newValue
|
||||||
};
|
};
|
||||||
|
|
||||||
if (changedValue.base === undefined) {
|
if (changedValue.base === undefined) {
|
||||||
delete changedValue["base"];
|
delete changedValue["base"];
|
||||||
}
|
}
|
||||||
this.props.onChange!(this.props.fieldName, changedValue);
|
props.onChange!(props.fieldName, changedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
changeDataType(propVal: string) {
|
function changeDataType(propVal: string) {
|
||||||
if (propVal === "interpolate" && this.props.onChangeToZoomFunction) {
|
if (propVal === "interpolate" && props.onChangeToZoomFunction) {
|
||||||
this.props.onChangeToZoomFunction();
|
props.onChangeToZoomFunction();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.onChange(this.props.fieldName, {
|
onChange(props.fieldName, {
|
||||||
...this.props.value,
|
...props.value,
|
||||||
type: propVal,
|
type: propVal,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
changeDataProperty(propName: "property" | "default", propVal: any) {
|
function changeDataProperty(propName: "property" | "default", propVal: any) {
|
||||||
if (propVal) {
|
if (propVal) {
|
||||||
this.props.value![propName] = propVal;
|
props.value![propName] = propVal;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
delete this.props.value![propName];
|
delete props.value![propName];
|
||||||
}
|
}
|
||||||
this.onChange(this.props.fieldName, this.props.value);
|
onChange(props.fieldName, props.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
|
||||||
|
|
||||||
if (typeof this.props.value?.type === "undefined") {
|
if (typeof props.value?.type === "undefined") {
|
||||||
this.props.value!.type = this.getFieldFunctionType(this.props.fieldSpec);
|
props.value!.type = getFieldFunctionType(props.fieldSpec);
|
||||||
}
|
}
|
||||||
|
|
||||||
let dataFields;
|
let dataFields;
|
||||||
if (this.props.value?.stops) {
|
if (props.value?.stops) {
|
||||||
dataFields = this.props.value.stops.map((stop, idx) => {
|
dataFields = props.value.stops.map((stop, idx) => {
|
||||||
const zoomLevel = typeof stop[0] === "object" ? stop[0].zoom : undefined;
|
const zoomLevel = typeof stop[0] === "object" ? stop[0].zoom : undefined;
|
||||||
const key = this.state.refs[idx];
|
const key = refs.current[idx];
|
||||||
const dataLevel = typeof stop[0] === "object" ? stop[0].value : stop[0];
|
const dataLevel = typeof stop[0] === "object" ? stop[0].value : stop[0];
|
||||||
const value = stop[1];
|
const value = stop[1];
|
||||||
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />;
|
const deleteStopBtn = <DeleteStopButton onClick={props.onDeleteStop?.bind(null, idx)} />;
|
||||||
|
|
||||||
const dataProps = {
|
const dataProps = {
|
||||||
"aria-label": t("Input value"),
|
"aria-label": t("Input value"),
|
||||||
label: t("Data value"),
|
label: t("Data value"),
|
||||||
value: dataLevel as any,
|
value: dataLevel as any,
|
||||||
onChange: (newData: string | number | undefined) => this.changeStop(idx, { zoom: zoomLevel, value: newData as number }, value)
|
onChange: (newData: string | number | undefined) => changeStop(idx, { zoom: zoomLevel, value: newData as number }, value)
|
||||||
};
|
};
|
||||||
|
|
||||||
let dataInput;
|
let dataInput;
|
||||||
if(this.props.value?.type === "categorical") {
|
if(props.value?.type === "categorical") {
|
||||||
dataInput = <InputString {...dataProps} />;
|
dataInput = <InputString {...dataProps} />;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -252,7 +236,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
|||||||
<InputNumber
|
<InputNumber
|
||||||
aria-label="Zoom"
|
aria-label="Zoom"
|
||||||
value={zoomLevel}
|
value={zoomLevel}
|
||||||
onChange={newZoom => this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)}
|
onChange={newZoom => changeStop(idx, {zoom: newZoom, value: dataLevel}, value)}
|
||||||
min={0}
|
min={0}
|
||||||
max={22}
|
max={22}
|
||||||
/>
|
/>
|
||||||
@@ -269,10 +253,10 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
|||||||
<td>
|
<td>
|
||||||
<InputSpec
|
<InputSpec
|
||||||
aria-label={t("Output value")}
|
aria-label={t("Output value")}
|
||||||
fieldName={this.props.fieldName}
|
fieldName={props.fieldName}
|
||||||
fieldSpec={this.props.fieldSpec}
|
fieldSpec={props.fieldSpec}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(_, newValue) => this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue as number)}
|
onChange={(_, newValue) => changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue as number)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -284,7 +268,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
|||||||
|
|
||||||
return <div className="maputnik-data-spec-block">
|
return <div className="maputnik-data-spec-block">
|
||||||
<fieldset className="maputnik-data-spec-property">
|
<fieldset className="maputnik-data-spec-property">
|
||||||
<legend>{labelFromFieldName(this.props.fieldName)}</legend>
|
<legend>{labelFromFieldName(props.fieldName)}</legend>
|
||||||
<div className="maputnik-data-fieldset-inner">
|
<div className="maputnik-data-fieldset-inner">
|
||||||
<Block
|
<Block
|
||||||
label={t("Function")}
|
label={t("Function")}
|
||||||
@@ -293,14 +277,14 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
|||||||
>
|
>
|
||||||
<div className="maputnik-data-spec-property-input">
|
<div className="maputnik-data-spec-property-input">
|
||||||
<InputSelect
|
<InputSelect
|
||||||
value={this.props.value!.type}
|
value={props.value!.type}
|
||||||
onChange={(propVal: string) => this.changeDataType(propVal)}
|
onChange={(propVal: string) => changeDataType(propVal)}
|
||||||
title={t("Select a type of data scale (default is 'categorical').")}
|
title={t("Select a type of data scale (default is 'categorical').")}
|
||||||
options={this.getDataFunctionTypes(this.props.fieldSpec)}
|
options={getDataFunctionTypes(props.fieldSpec)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Block>
|
</Block>
|
||||||
{this.props.value?.type !== "identity" &&
|
{props.value?.type !== "identity" &&
|
||||||
<Block
|
<Block
|
||||||
label={t("Base")}
|
label={t("Base")}
|
||||||
key="base"
|
key="base"
|
||||||
@@ -310,8 +294,8 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
|||||||
<InputSpec
|
<InputSpec
|
||||||
fieldName={"base"}
|
fieldName={"base"}
|
||||||
fieldSpec={latest.function.base as typeof latest.function.base & { type: "number" }}
|
fieldSpec={latest.function.base as typeof latest.function.base & { type: "number" }}
|
||||||
value={this.props.value?.base}
|
value={props.value?.base}
|
||||||
onChange={(_, newValue) => this.changeBase(newValue as number)}
|
onChange={(_, newValue) => changeBase(newValue as number)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Block>
|
</Block>
|
||||||
@@ -323,9 +307,9 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
|||||||
>
|
>
|
||||||
<div className="maputnik-data-spec-property-input">
|
<div className="maputnik-data-spec-property-input">
|
||||||
<InputString
|
<InputString
|
||||||
value={this.props.value?.property}
|
value={props.value?.property}
|
||||||
title={t("Input a data property to base styles off of.")}
|
title={t("Input a data property to base styles off of.")}
|
||||||
onChange={propVal => this.changeDataProperty("property", propVal)}
|
onChange={propVal => changeDataProperty("property", propVal)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Block>
|
</Block>
|
||||||
@@ -336,10 +320,10 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
|||||||
data-wd-key="function-default"
|
data-wd-key="function-default"
|
||||||
>
|
>
|
||||||
<InputSpec
|
<InputSpec
|
||||||
fieldName={this.props.fieldName}
|
fieldName={props.fieldName}
|
||||||
fieldSpec={this.props.fieldSpec}
|
fieldSpec={props.fieldSpec}
|
||||||
value={this.props.value?.default}
|
value={props.value?.default}
|
||||||
onChange={(_, propVal) => this.changeDataProperty("default", propVal)}
|
onChange={(_, propVal) => changeDataProperty("default", propVal)}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
}
|
}
|
||||||
@@ -364,7 +348,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
|||||||
{dataFields &&
|
{dataFields &&
|
||||||
<InputButton
|
<InputButton
|
||||||
className="maputnik-add-stop"
|
className="maputnik-add-stop"
|
||||||
onClick={this.props.onAddStop?.bind(this)}
|
onClick={props.onAddStop?.bind(null)}
|
||||||
>
|
>
|
||||||
<PiListPlusBold style={{ verticalAlign: "text-bottom" }} />
|
<PiListPlusBold style={{ verticalAlign: "text-bottom" }} />
|
||||||
{t("Add stop")}
|
{t("Add stop")}
|
||||||
@@ -373,7 +357,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
|||||||
<InputButton
|
<InputButton
|
||||||
className="maputnik-add-stop"
|
className="maputnik-add-stop"
|
||||||
data-wd-key="convert-to-expression"
|
data-wd-key="convert-to-expression"
|
||||||
onClick={this.props.onExpressionClick?.bind(this)}
|
onClick={props.onExpressionClick?.bind(null)}
|
||||||
>
|
>
|
||||||
<TbMathFunction style={{ verticalAlign: "text-bottom" }} />
|
<TbMathFunction style={{ verticalAlign: "text-bottom" }} />
|
||||||
{t("Convert to expression")}
|
{t("Convert to expression")}
|
||||||
@@ -382,7 +366,6 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const DataProperty = withTranslation()(DataPropertyInternal);
|
export const DataProperty = withTranslation()(DataPropertyInternal);
|
||||||
|
|||||||
@@ -10,17 +10,15 @@ type DeleteStopButtonInternalProps = {
|
|||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
|
|
||||||
class DeleteStopButtonInternal extends React.Component<DeleteStopButtonInternalProps> {
|
const DeleteStopButtonInternal: React.FC<DeleteStopButtonInternalProps> = (props) => {
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
|
||||||
return <InputButton
|
return <InputButton
|
||||||
className="maputnik-delete-stop"
|
className="maputnik-delete-stop"
|
||||||
onClick={this.props.onClick}
|
onClick={props.onClick}
|
||||||
title={t("Remove zoom level from stop")}
|
title={t("Remove zoom level from stop")}
|
||||||
>
|
>
|
||||||
<MdDelete />
|
<MdDelete />
|
||||||
</InputButton>;
|
</InputButton>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const DeleteStopButton = withTranslation()(DeleteStopButtonInternal);
|
export const DeleteStopButton = withTranslation()(DeleteStopButtonInternal);
|
||||||
|
|||||||
@@ -23,10 +23,7 @@ type DocProps = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Doc extends React.Component<DocProps> {
|
export const Doc: React.FC<DocProps> = ({fieldSpec}) => {
|
||||||
render () {
|
|
||||||
const {fieldSpec} = this.props;
|
|
||||||
|
|
||||||
const {doc, values, docUrl, docUrlLinkText} = fieldSpec;
|
const {doc, values, docUrl, docUrlLinkText} = fieldSpec;
|
||||||
const sdkSupport = fieldSpec["sdk-support"];
|
const sdkSupport = fieldSpec["sdk-support"];
|
||||||
|
|
||||||
@@ -106,5 +103,4 @@ export class Doc extends React.Component<DocProps> {
|
|||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -24,30 +24,21 @@ type ExpressionPropertyInternalProps = {
|
|||||||
onBlur?(...args: unknown[]): unknown
|
onBlur?(...args: unknown[]): unknown
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInternalProps> {
|
const ExpressionPropertyInternal: React.FC<ExpressionPropertyInternalProps> = ({
|
||||||
static defaultProps = {
|
errors = {},
|
||||||
errors: {},
|
onFocus = () => {},
|
||||||
onFocus: () => {},
|
onBlur = () => {},
|
||||||
onBlur: () => {},
|
...props
|
||||||
};
|
}) => {
|
||||||
|
const {t, value, canUndo} = props;
|
||||||
constructor(props: ExpressionPropertyInternalProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
jsonError: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {t, value, canUndo} = this.props;
|
|
||||||
const undoDisabled = canUndo ? !canUndo() : true;
|
const undoDisabled = canUndo ? !canUndo() : true;
|
||||||
|
|
||||||
const deleteStopBtn = (
|
const deleteStopBtn = (
|
||||||
<>
|
<>
|
||||||
{this.props.onUndo &&
|
{props.onUndo &&
|
||||||
<InputButton
|
<InputButton
|
||||||
key="undo_action"
|
key="undo_action"
|
||||||
onClick={this.props.onUndo}
|
onClick={props.onUndo}
|
||||||
disabled={undoDisabled}
|
disabled={undoDisabled}
|
||||||
className="maputnik-delete-stop"
|
className="maputnik-delete-stop"
|
||||||
data-wd-key="undo-expression"
|
data-wd-key="undo-expression"
|
||||||
@@ -58,7 +49,7 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
|
|||||||
}
|
}
|
||||||
<InputButton
|
<InputButton
|
||||||
key="delete_action"
|
key="delete_action"
|
||||||
onClick={this.props.onDelete}
|
onClick={props.onDelete}
|
||||||
className="maputnik-delete-stop"
|
className="maputnik-delete-stop"
|
||||||
data-wd-key="delete-expression"
|
data-wd-key="delete-expression"
|
||||||
title={t("Delete expression")}
|
title={t("Delete expression")}
|
||||||
@@ -68,28 +59,27 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
let error = undefined;
|
let error = undefined;
|
||||||
if (this.props.errors) {
|
if (errors) {
|
||||||
const fieldKey = this.props.fieldType ? this.props.fieldType + "." + this.props.fieldName : this.props.fieldName;
|
const fieldKey = props.fieldType ? props.fieldType + "." + props.fieldName : props.fieldName;
|
||||||
error = this.props.errors[fieldKey];
|
error = errors[fieldKey];
|
||||||
}
|
}
|
||||||
return <Block
|
return <Block
|
||||||
fieldSpec={this.props.fieldSpec}
|
fieldSpec={props.fieldSpec}
|
||||||
label={t(labelFromFieldName(this.props.fieldName))}
|
label={t(labelFromFieldName(props.fieldName))}
|
||||||
action={deleteStopBtn}
|
action={deleteStopBtn}
|
||||||
wideMode={true}
|
wideMode={true}
|
||||||
error={error}
|
error={error}
|
||||||
>
|
>
|
||||||
<FieldJson
|
<FieldJson
|
||||||
lintType="expression"
|
lintType="expression"
|
||||||
spec={this.props.fieldSpec}
|
spec={props.fieldSpec}
|
||||||
className="maputnik-expression-editor"
|
className="maputnik-expression-editor"
|
||||||
onFocus={this.props.onFocus}
|
onFocus={onFocus}
|
||||||
onBlur={this.props.onBlur}
|
onBlur={onBlur}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={this.props.onChange}
|
onChange={props.onChange}
|
||||||
/>
|
/>
|
||||||
</Block>;
|
</Block>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const ExpressionProperty = withTranslation()(ExpressionPropertyInternal);
|
export const ExpressionProperty = withTranslation()(ExpressionPropertyInternal);
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ import { InputDynamicArray, type InputDynamicArrayProps } from "./InputDynamicAr
|
|||||||
import { Fieldset } from "./Fieldset";
|
import { Fieldset } from "./Fieldset";
|
||||||
|
|
||||||
type FieldDynamicArrayProps = InputDynamicArrayProps & {
|
type FieldDynamicArrayProps = InputDynamicArrayProps & {
|
||||||
name?: string;
|
name?: string
|
||||||
fieldSpec?: {
|
|
||||||
value?: string;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FieldDynamicArray: React.FC<FieldDynamicArrayProps> = (props) => {
|
export const FieldDynamicArray: React.FC<FieldDynamicArrayProps> = (props) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { TbMathFunction } from "react-icons/tb";
|
import { TbMathFunction } from "react-icons/tb";
|
||||||
import { PiListPlusBold } from "react-icons/pi";
|
import { PiListPlusBold } from "react-icons/pi";
|
||||||
import {isEqual} from "lodash";
|
import {isEqual} from "lodash";
|
||||||
@@ -100,95 +100,64 @@ type FilterEditorInternalProps = {
|
|||||||
onChange(value: LegacyFilterSpecification | ExpressionSpecification): void
|
onChange(value: LegacyFilterSpecification | ExpressionSpecification): void
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
type FilterEditorState = {
|
const FilterEditorInternal: React.FC<FilterEditorInternalProps> = ({ filter = ["all"], ...rest }) => {
|
||||||
showDoc: boolean
|
const props = { filter, ...rest } as FilterEditorInternalProps;
|
||||||
displaySimpleFilter: boolean
|
|
||||||
valueIsSimpleFilter?: boolean
|
|
||||||
};
|
|
||||||
|
|
||||||
class FilterEditorInternal extends React.Component<FilterEditorInternalProps, FilterEditorState> {
|
// Nothing ever toggles this: the Block below renders its own documentation
|
||||||
static defaultProps = {
|
// toggle, so this component's inline doc panel stays hidden (as it did before).
|
||||||
filter: ["all"],
|
const [showDoc] = useState(false);
|
||||||
};
|
const [displaySimpleFilter, setDisplaySimpleFilter] = useState(() =>
|
||||||
|
checkIfSimpleFilter(combiningFilter(props))
|
||||||
|
);
|
||||||
|
|
||||||
constructor (props: FilterEditorInternalProps) {
|
// Replaces getDerivedStateFromProps. "Upgrade but never downgrade": once the
|
||||||
super(props);
|
// filter stops being expressible in the simple editor, switch to the
|
||||||
this.state = {
|
// expression editor and stay there.
|
||||||
showDoc: false,
|
const isSimpleFilter = checkIfSimpleFilter(combiningFilter(props));
|
||||||
displaySimpleFilter: checkIfSimpleFilter(combiningFilter(props)),
|
if (!isSimpleFilter && displaySimpleFilter) {
|
||||||
};
|
setDisplaySimpleFilter(false);
|
||||||
}
|
}
|
||||||
|
// In the original this was state, but every branch derived it from these two
|
||||||
|
// values alone.
|
||||||
|
const valueIsSimpleFilter = isSimpleFilter && !displaySimpleFilter;
|
||||||
|
|
||||||
// Convert filter to combining filter
|
// Convert filter to combining filter
|
||||||
onFilterPartChanged(filterIdx: number, newPart: any[]) {
|
function onFilterPartChanged(filterIdx: number, newPart: any[]) {
|
||||||
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
|
const newFilter = combiningFilter(props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
|
||||||
newFilter[filterIdx] = newPart;
|
newFilter[filterIdx] = newPart;
|
||||||
this.props.onChange(newFilter);
|
props.onChange(newFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteFilterItem(filterIdx: number) {
|
function deleteFilterItem(filterIdx: number) {
|
||||||
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
|
const newFilter = combiningFilter(props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
|
||||||
newFilter.splice(filterIdx + 1, 1);
|
newFilter.splice(filterIdx + 1, 1);
|
||||||
this.props.onChange(newFilter);
|
props.onChange(newFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
addFilterItem = () => {
|
const addFilterItem = () => {
|
||||||
const newFilterItem = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
|
const newFilterItem = combiningFilter(props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
|
||||||
(newFilterItem as any[]).push(["==", "name", ""]);
|
(newFilterItem as any[]).push(["==", "name", ""]);
|
||||||
this.props.onChange(newFilterItem);
|
props.onChange(newFilterItem);
|
||||||
};
|
};
|
||||||
|
|
||||||
onToggleDoc = (val: boolean) => {
|
const makeFilter = () => {
|
||||||
this.setState({
|
setDisplaySimpleFilter(true);
|
||||||
showDoc: val
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
makeFilter = () => {
|
const makeExpression = () => {
|
||||||
this.setState({
|
const currentFilter = combiningFilter(props);
|
||||||
displaySimpleFilter: true,
|
props.onChange(migrateFilter(currentFilter));
|
||||||
});
|
setDisplaySimpleFilter(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
makeExpression = () => {
|
|
||||||
const filter = combiningFilter(this.props);
|
|
||||||
this.props.onChange(migrateFilter(filter));
|
|
||||||
this.setState({
|
|
||||||
displaySimpleFilter: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
static getDerivedStateFromProps(props: Readonly<FilterEditorInternalProps>, state: FilterEditorState) {
|
const {errors, t} = props;
|
||||||
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
|
|
||||||
|
|
||||||
// Upgrade but never downgrade
|
|
||||||
if (!displaySimpleFilter && state.displaySimpleFilter === true) {
|
|
||||||
return {
|
|
||||||
displaySimpleFilter: false,
|
|
||||||
valueIsSimpleFilter: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if (displaySimpleFilter && state.displaySimpleFilter === false) {
|
|
||||||
return {
|
|
||||||
valueIsSimpleFilter: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return {
|
|
||||||
valueIsSimpleFilter: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {errors, t} = this.props;
|
|
||||||
const {displaySimpleFilter} = this.state;
|
|
||||||
const fieldSpec={
|
const fieldSpec={
|
||||||
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
|
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
|
||||||
};
|
};
|
||||||
const defaultFilter = ["all"] as LegacyFilterSpecification | ExpressionSpecification;
|
const defaultFilter = ["all"] as LegacyFilterSpecification | ExpressionSpecification;
|
||||||
|
|
||||||
const isNestedCombiningFilter = displaySimpleFilter && hasNestedCombiningFilter(combiningFilter(this.props));
|
const isNestedCombiningFilter = displaySimpleFilter && hasNestedCombiningFilter(combiningFilter(props));
|
||||||
|
|
||||||
if (isNestedCombiningFilter) {
|
if (isNestedCombiningFilter) {
|
||||||
return <div className="maputnik-filter-editor-unsupported">
|
return <div className="maputnik-filter-editor-unsupported">
|
||||||
@@ -196,7 +165,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
{t("Nested filters are not supported.")}
|
{t("Nested filters are not supported.")}
|
||||||
</p>
|
</p>
|
||||||
<InputButton
|
<InputButton
|
||||||
onClick={this.makeExpression}
|
onClick={makeExpression}
|
||||||
title={t("Convert to expression")}
|
title={t("Convert to expression")}
|
||||||
>
|
>
|
||||||
<TbMathFunction />
|
<TbMathFunction />
|
||||||
@@ -205,14 +174,14 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
else if (displaySimpleFilter) {
|
else if (displaySimpleFilter) {
|
||||||
const filter = combiningFilter(this.props);
|
const filter = combiningFilter(props);
|
||||||
const combiningOp = filter[0];
|
const combiningOp = filter[0];
|
||||||
const filters = filter.slice(1) as (LegacyFilterSpecification | ExpressionSpecification)[];
|
const filters = filter.slice(1) as (LegacyFilterSpecification | ExpressionSpecification)[];
|
||||||
|
|
||||||
const actions = (
|
const actions = (
|
||||||
<div>
|
<div>
|
||||||
<InputButton
|
<InputButton
|
||||||
onClick={this.makeExpression}
|
onClick={makeExpression}
|
||||||
title={t("Convert to expression")}
|
title={t("Convert to expression")}
|
||||||
className="maputnik-make-zoom-function"
|
className="maputnik-make-zoom-function"
|
||||||
data-wd-key="filter-convert-to-expression"
|
data-wd-key="filter-convert-to-expression"
|
||||||
@@ -227,11 +196,11 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={`block-${idx}`}>
|
<div key={`block-${idx}`}>
|
||||||
<FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
|
<FilterEditorBlock key={idx} onDelete={deleteFilterItem.bind(null, idx)}>
|
||||||
<SingleFilterEditor
|
<SingleFilterEditor
|
||||||
properties={this.props.properties}
|
properties={props.properties}
|
||||||
filter={f}
|
filter={f}
|
||||||
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
|
onChange={onFilterPartChanged.bind(null, idx + 1)}
|
||||||
/>
|
/>
|
||||||
</FilterEditorBlock>
|
</FilterEditorBlock>
|
||||||
{error &&
|
{error &&
|
||||||
@@ -253,7 +222,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
>
|
>
|
||||||
<InputSelect
|
<InputSelect
|
||||||
value={combiningOp}
|
value={combiningOp}
|
||||||
onChange={(v: [string, any]) => this.onFilterPartChanged(0, v)}
|
onChange={(v: [string, any]) => onFilterPartChanged(0, v)}
|
||||||
options={[
|
options={[
|
||||||
["all", t("every filter matches")],
|
["all", t("every filter matches")],
|
||||||
["none", t("no filter matches")],
|
["none", t("no filter matches")],
|
||||||
@@ -269,7 +238,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
<InputButton
|
<InputButton
|
||||||
data-wd-key="layer-filter-button"
|
data-wd-key="layer-filter-button"
|
||||||
className="maputnik-add-filter"
|
className="maputnik-add-filter"
|
||||||
onClick={this.addFilterItem}
|
onClick={addFilterItem}
|
||||||
>
|
>
|
||||||
<PiListPlusBold style={{ verticalAlign: "text-bottom" }} />
|
<PiListPlusBold style={{ verticalAlign: "text-bottom" }} />
|
||||||
{t("Add filter")}
|
{t("Add filter")}
|
||||||
@@ -278,7 +247,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
<div
|
<div
|
||||||
key="doc"
|
key="doc"
|
||||||
className="maputnik-doc-inline"
|
className="maputnik-doc-inline"
|
||||||
style={{display: this.state.showDoc ? "" : "none"}}
|
style={{display: showDoc ? "" : "none"}}
|
||||||
>
|
>
|
||||||
<Doc fieldSpec={fieldSpec} />
|
<Doc fieldSpec={fieldSpec} />
|
||||||
</div>
|
</div>
|
||||||
@@ -286,26 +255,26 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const {filter} = this.props;
|
const {filter} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ExpressionProperty
|
<ExpressionProperty
|
||||||
onDelete={() => {
|
onDelete={() => {
|
||||||
this.setState({displaySimpleFilter: true});
|
setDisplaySimpleFilter(true);
|
||||||
this.props.onChange(defaultFilter);
|
props.onChange(defaultFilter);
|
||||||
}}
|
}}
|
||||||
fieldName="filter"
|
fieldName="filter"
|
||||||
value={filter}
|
value={filter}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
onChange={this.props.onChange}
|
onChange={props.onChange}
|
||||||
/>
|
/>
|
||||||
{this.state.valueIsSimpleFilter &&
|
{valueIsSimpleFilter &&
|
||||||
<div className="maputnik-expr-infobox">
|
<div className="maputnik-expr-infobox">
|
||||||
{t("You've entered an old style filter.")}
|
{t("You've entered an old style filter.")}
|
||||||
{" "}
|
{" "}
|
||||||
<button
|
<button
|
||||||
onClick={this.makeFilter}
|
onClick={makeFilter}
|
||||||
className="maputnik-expr-infobox__button"
|
className="maputnik-expr-infobox__button"
|
||||||
>
|
>
|
||||||
{t("Switch to filter editor.")}
|
{t("Switch to filter editor.")}
|
||||||
@@ -315,7 +284,6 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const FilterEditor = withTranslation()(FilterEditorInternal);
|
export const FilterEditor = withTranslation()(FilterEditorInternal);
|
||||||
|
|||||||
@@ -7,24 +7,22 @@ type FilterEditorBlockInternalProps = PropsWithChildren & {
|
|||||||
onDelete(...args: unknown[]): unknown
|
onDelete(...args: unknown[]): unknown
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class FilterEditorBlockInternal extends React.Component<FilterEditorBlockInternalProps> {
|
const FilterEditorBlockInternal: React.FC<FilterEditorBlockInternalProps> = (props) => {
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
|
||||||
return <div className="maputnik-filter-editor-block">
|
return <div className="maputnik-filter-editor-block">
|
||||||
<div className="maputnik-filter-editor-block-content">
|
<div className="maputnik-filter-editor-block-content">
|
||||||
{this.props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
<div className="maputnik-filter-editor-block-action">
|
<div className="maputnik-filter-editor-block-action">
|
||||||
<InputButton
|
<InputButton
|
||||||
className="maputnik-icon-button"
|
className="maputnik-icon-button"
|
||||||
onClick={this.props.onDelete}
|
onClick={props.onDelete}
|
||||||
title={t("Delete filter block")}
|
title={t("Delete filter block")}
|
||||||
>
|
>
|
||||||
<MdDelete />
|
<MdDelete />
|
||||||
</InputButton>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const FilterEditorBlock = withTranslation()(FilterEditorBlockInternal);
|
export const FilterEditorBlock = withTranslation()(FilterEditorBlockInternal);
|
||||||
|
|||||||
@@ -13,15 +13,14 @@ type FunctionInputButtonsInternalProps = {
|
|||||||
onElevationClick?(): void
|
onElevationClick?(): void
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsInternalProps> {
|
const FunctionInputButtonsInternal: React.FC<FunctionInputButtonsInternalProps> = (props) => {
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
|
||||||
|
|
||||||
if (this.props.fieldSpec.expression?.parameters.includes("zoom")) {
|
if (props.fieldSpec.expression?.parameters.includes("zoom")) {
|
||||||
const expressionInputButton = (
|
const expressionInputButton = (
|
||||||
<InputButton
|
<InputButton
|
||||||
className="maputnik-make-zoom-function"
|
className="maputnik-make-zoom-function"
|
||||||
onClick={this.props.onExpressionClick}
|
onClick={props.onExpressionClick}
|
||||||
title={t("Convert to expression")}
|
title={t("Convert to expression")}
|
||||||
>
|
>
|
||||||
<TbMathFunction />
|
<TbMathFunction />
|
||||||
@@ -30,17 +29,17 @@ class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsI
|
|||||||
|
|
||||||
const makeZoomInputButton = <InputButton
|
const makeZoomInputButton = <InputButton
|
||||||
className="maputnik-make-zoom-function"
|
className="maputnik-make-zoom-function"
|
||||||
onClick={this.props.onZoomClick}
|
onClick={props.onZoomClick}
|
||||||
title={t("Convert property into a zoom function")}
|
title={t("Convert property into a zoom function")}
|
||||||
>
|
>
|
||||||
<MdFunctions />
|
<MdFunctions />
|
||||||
</InputButton>;
|
</InputButton>;
|
||||||
|
|
||||||
let makeDataInputButton;
|
let makeDataInputButton;
|
||||||
if (this.props.fieldSpec["property-type"] === "data-driven") {
|
if (props.fieldSpec["property-type"] === "data-driven") {
|
||||||
makeDataInputButton = <InputButton
|
makeDataInputButton = <InputButton
|
||||||
className="maputnik-make-data-function"
|
className="maputnik-make-data-function"
|
||||||
onClick={this.props.onDataClick}
|
onClick={props.onDataClick}
|
||||||
title={t("Convert property to data function")}
|
title={t("Convert property to data function")}
|
||||||
>
|
>
|
||||||
<MdInsertChart />
|
<MdInsertChart />
|
||||||
@@ -51,10 +50,10 @@ class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsI
|
|||||||
{makeDataInputButton}
|
{makeDataInputButton}
|
||||||
{makeZoomInputButton}
|
{makeZoomInputButton}
|
||||||
</div>;
|
</div>;
|
||||||
} else if (this.props.fieldSpec.expression?.parameters.includes("elevation")) {
|
} else if (props.fieldSpec.expression?.parameters.includes("elevation")) {
|
||||||
const inputElevationButton = <InputButton
|
const inputElevationButton = <InputButton
|
||||||
className="maputnik-make-elevation-function"
|
className="maputnik-make-elevation-function"
|
||||||
onClick={this.props.onElevationClick}
|
onClick={props.onElevationClick}
|
||||||
title={t("Convert property into a elevation function")}
|
title={t("Convert property into a elevation function")}
|
||||||
data-wd-key='make-elevation-function'
|
data-wd-key='make-elevation-function'
|
||||||
>
|
>
|
||||||
@@ -64,7 +63,6 @@ class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsI
|
|||||||
} else {
|
} else {
|
||||||
return <div></div>;
|
return <div></div>;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const FunctionInputButtons = withTranslation()(FunctionInputButtonsInternal);
|
export const FunctionInputButtons = withTranslation()(FunctionInputButtonsInternal);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import { InputString } from "./InputString";
|
import { InputString } from "./InputString";
|
||||||
import { InputNumber } from "./InputNumber";
|
import { InputNumber } from "./InputNumber";
|
||||||
|
|
||||||
@@ -12,73 +12,40 @@ export type InputArrayProps = {
|
|||||||
label?: string
|
label?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
type InputArrayState = {
|
export const InputArray: React.FC<InputArrayProps> = ({
|
||||||
value: (string | number | undefined)[]
|
value: propsValue = [],
|
||||||
initialPropsValue: unknown[]
|
default: propsDefault = [],
|
||||||
};
|
...rest
|
||||||
|
}) => {
|
||||||
|
const props = { value: propsValue, default: propsDefault, ...rest };
|
||||||
|
|
||||||
export class InputArray extends React.Component<InputArrayProps, InputArrayState> {
|
// The original seeded this from props and then never let props overwrite it
|
||||||
static defaultProps = {
|
// again (its getDerivedStateFromProps assigned the existing state back in
|
||||||
value: [],
|
// both branches), so the value is owned by this component after mount.
|
||||||
default: [],
|
const [value, setValue] = useState<(string | number | undefined)[]>(() => propsValue.slice(0));
|
||||||
};
|
|
||||||
|
|
||||||
constructor (props: InputArrayProps) {
|
function isComplete(val: unknown[]) {
|
||||||
super(props);
|
return Array(props.length).fill(null).every((_, i) => {
|
||||||
this.state = {
|
const v = val[i];
|
||||||
value: this.props.value.slice(0),
|
return !(v === undefined || v === "");
|
||||||
// This is so we can compare changes in getDerivedStateFromProps
|
|
||||||
initialPropsValue: this.props.value.slice(0),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromProps(props: Readonly<InputArrayProps>, state: InputArrayState) {
|
|
||||||
const value: any[] = [];
|
|
||||||
const initialPropsValue = state.initialPropsValue.slice(0);
|
|
||||||
|
|
||||||
Array(props.length).fill(null).map((_, i) => {
|
|
||||||
if (props.value[i] === state.initialPropsValue[i]) {
|
|
||||||
value[i] = state.value[i];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
value[i] = state.value[i];
|
|
||||||
initialPropsValue[i] = state.value[i];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
value,
|
|
||||||
initialPropsValue,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
isComplete(value: unknown[]) {
|
|
||||||
return Array(this.props.length).fill(null).every((_, i) => {
|
|
||||||
const val = value[i];
|
|
||||||
return !(val === undefined || val === "");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
changeValue(idx: number, newValue: string | number | undefined) {
|
function changeValue(idx: number, newValue: string | number | undefined) {
|
||||||
const value = this.state.value.slice(0);
|
const nextValue = value.slice(0);
|
||||||
value[idx] = newValue;
|
nextValue[idx] = newValue;
|
||||||
|
|
||||||
this.setState({
|
setValue(nextValue);
|
||||||
value,
|
|
||||||
}, () => {
|
if (isComplete(nextValue) && props.onChange) {
|
||||||
if (this.isComplete(value) && this.props.onChange) {
|
props.onChange(nextValue);
|
||||||
this.props.onChange(value);
|
|
||||||
}
|
}
|
||||||
else if (this.props.onChange){
|
else if (props.onChange) {
|
||||||
// Unset until complete
|
// Unset until complete
|
||||||
this.props.onChange(undefined);
|
props.onChange(undefined);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
|
||||||
const {value} = this.state;
|
|
||||||
|
|
||||||
const containsValues = (
|
const containsValues = (
|
||||||
value.length > 0 &&
|
value.length > 0 &&
|
||||||
!value.every(val => {
|
!value.every(val => {
|
||||||
@@ -86,24 +53,24 @@ export class InputArray extends React.Component<InputArrayProps, InputArrayState
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const inputs = Array(this.props.length).fill(null).map((_, i) => {
|
const inputs = Array(props.length).fill(null).map((_, i) => {
|
||||||
if(this.props.type === "number") {
|
if(props.type === "number") {
|
||||||
return <InputNumber
|
return <InputNumber
|
||||||
key={i}
|
key={i}
|
||||||
default={containsValues || !this.props.default ? undefined : this.props.default[i] as number}
|
default={containsValues || !props.default ? undefined : props.default[i] as number}
|
||||||
value={value[i] as number}
|
value={value[i] as number}
|
||||||
required={containsValues ? true : false}
|
required={containsValues ? true : false}
|
||||||
onChange={(v) => this.changeValue(i, v)}
|
onChange={(v) => changeValue(i, v)}
|
||||||
aria-label={this.props["aria-label"] || this.props.label}
|
aria-label={props["aria-label"] || props.label}
|
||||||
/>;
|
/>;
|
||||||
} else {
|
} else {
|
||||||
return <InputString
|
return <InputString
|
||||||
key={i}
|
key={i}
|
||||||
default={containsValues || !this.props.default ? undefined : this.props.default[i] as string}
|
default={containsValues || !props.default ? undefined : props.default[i] as string}
|
||||||
value={value[i] as string}
|
value={value[i] as string}
|
||||||
required={containsValues ? true : false}
|
required={containsValues ? true : false}
|
||||||
onChange={this.changeValue.bind(this, i)}
|
onChange={(v) => changeValue(i, v)}
|
||||||
aria-label={this.props["aria-label"] || this.props.label}
|
aria-label={props["aria-label"] || props.label}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -113,5 +80,4 @@ export class InputArray extends React.Component<InputArrayProps, InputArrayState
|
|||||||
{inputs}
|
{inputs}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -14,20 +14,18 @@ type InputButtonProps = {
|
|||||||
title?: string
|
title?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
export class InputButton extends React.Component<InputButtonProps> {
|
export const InputButton: React.FC<InputButtonProps> = (props) => {
|
||||||
render() {
|
|
||||||
return <button
|
return <button
|
||||||
id={this.props.id}
|
id={props.id}
|
||||||
title={this.props.title}
|
title={props.title}
|
||||||
type={this.props.type}
|
type={props.type}
|
||||||
onClick={this.props.onClick}
|
onClick={props.onClick}
|
||||||
disabled={this.props.disabled}
|
disabled={props.disabled}
|
||||||
aria-label={this.props["aria-label"]}
|
aria-label={props["aria-label"]}
|
||||||
className={classnames("maputnik-button", this.props.className)}
|
className={classnames("maputnik-button", props.className)}
|
||||||
data-wd-key={this.props["data-wd-key"]}
|
data-wd-key={props["data-wd-key"]}
|
||||||
style={this.props.style}
|
style={props.style}
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{props.children}
|
||||||
</button>;
|
</button>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,32 +6,26 @@ export type InputCheckboxProps = {
|
|||||||
onChange(...args: unknown[]): unknown
|
onChange(...args: unknown[]): unknown
|
||||||
};
|
};
|
||||||
|
|
||||||
export class InputCheckbox extends React.Component<InputCheckboxProps> {
|
export const InputCheckbox: React.FC<InputCheckboxProps> = ({value = false, ...props}) => {
|
||||||
static defaultProps = {
|
const onChange = () => {
|
||||||
value: false,
|
props.onChange(!value);
|
||||||
};
|
};
|
||||||
|
|
||||||
onChange = () => {
|
|
||||||
this.props.onChange(!this.props.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <div className="maputnik-checkbox-wrapper">
|
return <div className="maputnik-checkbox-wrapper">
|
||||||
<input
|
<input
|
||||||
className="maputnik-checkbox"
|
className="maputnik-checkbox"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
style={this.props.style}
|
style={props.style}
|
||||||
onChange={this.onChange}
|
onChange={onChange}
|
||||||
onClick={this.onChange}
|
onClick={onChange}
|
||||||
checked={this.props.value}
|
checked={value}
|
||||||
/>
|
/>
|
||||||
<div className="maputnik-checkbox-box">
|
<div className="maputnik-checkbox-box">
|
||||||
<svg style={{
|
<svg style={{
|
||||||
display: this.props.value ? "inline" : "none"
|
display: value ? "inline" : "none"
|
||||||
}} className="maputnik-checkbox-icon" viewBox='0 0 32 32'>
|
}} className="maputnik-checkbox-icon" viewBox='0 0 32 32'>
|
||||||
<path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' />
|
<path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import Color from "color";
|
import Color from "color";
|
||||||
import ChromePicker from "react-color/lib/components/chrome/Chrome";
|
import ChromePicker from "react-color/lib/components/chrome/Chrome";
|
||||||
import {type ColorResult} from "react-color";
|
import {type ColorResult} from "react-color";
|
||||||
@@ -20,26 +20,27 @@ export type InputColorProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/*** Number fields with support for min, max and units and documentation*/
|
/*** Number fields with support for min, max and units and documentation*/
|
||||||
export class InputColor extends React.Component<InputColorProps> {
|
export const InputColor: React.FC<InputColorProps> = (props) => {
|
||||||
state = {
|
const [pickerOpened, setPickerOpened] = useState(false);
|
||||||
pickerOpened: false
|
const colorInput = useRef<HTMLInputElement | null>(null);
|
||||||
};
|
|
||||||
colorInput: HTMLInputElement | null = null;
|
|
||||||
|
|
||||||
constructor (props: InputColorProps) {
|
// Keep the latest `onChange` available to the throttled callback, which is
|
||||||
super(props);
|
// created only once so that throttling actually takes effect.
|
||||||
this.onChangeNoCheck = lodash.throttle(this.onChangeNoCheck, 1000/30);
|
const onChangeProp = useRef(props.onChange);
|
||||||
}
|
useEffect(() => {
|
||||||
|
onChangeProp.current = props.onChange;
|
||||||
|
});
|
||||||
|
|
||||||
onChangeNoCheck(v: string) {
|
const onChangeNoCheck = useMemo(
|
||||||
this.props.onChange(v);
|
() => lodash.throttle((v: string) => onChangeProp.current(v), 1000/30),
|
||||||
}
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
//TODO: I much rather would do this with absolute positioning
|
//TODO: I much rather would do this with absolute positioning
|
||||||
//but I am too stupid to get it to work together with fixed position
|
//but I am too stupid to get it to work together with fixed position
|
||||||
//and scrollbars so I have to fallback to JavaScript
|
//and scrollbars so I have to fallback to JavaScript
|
||||||
calcPickerOffset = () => {
|
const calcPickerOffset = () => {
|
||||||
const elem = this.colorInput;
|
const elem = colorInput.current;
|
||||||
if(elem) {
|
if(elem) {
|
||||||
const pos = elem.getBoundingClientRect();
|
const pos = elem.getBoundingClientRect();
|
||||||
return {
|
return {
|
||||||
@@ -54,28 +55,27 @@ export class InputColor extends React.Component<InputColorProps> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
togglePicker = () => {
|
const togglePicker = () => {
|
||||||
this.setState({ pickerOpened: !this.state.pickerOpened });
|
setPickerOpened(opened => !opened);
|
||||||
};
|
};
|
||||||
|
|
||||||
get color() {
|
const getColor = () => {
|
||||||
// Catch invalid color.
|
// Catch invalid color.
|
||||||
try {
|
try {
|
||||||
return Color(this.props.value).rgb();
|
return Color(props.value).rgb();
|
||||||
}
|
}
|
||||||
catch(err) {
|
catch(err) {
|
||||||
console.warn("Error parsing color: ", err);
|
console.warn("Error parsing color: ", err);
|
||||||
return Color("rgb(255,255,255)");
|
return Color("rgb(255,255,255)");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
onChange (v: string) {
|
const onChange = (v: string) => {
|
||||||
this.props.onChange(v === "" ? undefined : v);
|
props.onChange(v === "" ? undefined : v);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
const offset = calcPickerOffset();
|
||||||
const offset = this.calcPickerOffset();
|
const currentColor = getColor().object();
|
||||||
const currentColor = this.color.object();
|
|
||||||
const currentChromeColor = {
|
const currentChromeColor = {
|
||||||
r: currentColor.r,
|
r: currentColor.r,
|
||||||
g: currentColor.g,
|
g: currentColor.g,
|
||||||
@@ -94,11 +94,11 @@ export class InputColor extends React.Component<InputColorProps> {
|
|||||||
}}>
|
}}>
|
||||||
<ChromePicker
|
<ChromePicker
|
||||||
color={currentChromeColor}
|
color={currentChromeColor}
|
||||||
onChange={c => this.onChangeNoCheck(formatColor(c))}
|
onChange={c => onChangeNoCheck(formatColor(c))}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="maputnik-color-picker-offset"
|
className="maputnik-color-picker-offset"
|
||||||
onClick={this.togglePicker}
|
onClick={togglePicker}
|
||||||
style={{
|
style={{
|
||||||
zIndex: -1,
|
zIndex: -1,
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
@@ -111,25 +111,24 @@ export class InputColor extends React.Component<InputColorProps> {
|
|||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
const swatchStyle = {
|
const swatchStyle = {
|
||||||
backgroundColor: this.props.value
|
backgroundColor: props.value
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div className="maputnik-color-wrapper">
|
return <div className="maputnik-color-wrapper">
|
||||||
{this.state.pickerOpened && picker}
|
{pickerOpened && picker}
|
||||||
<div className="maputnik-color-swatch" style={swatchStyle}></div>
|
<div className="maputnik-color-swatch" style={swatchStyle}></div>
|
||||||
<input
|
<input
|
||||||
aria-label={this.props["aria-label"]}
|
aria-label={props["aria-label"]}
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className="maputnik-color"
|
className="maputnik-color"
|
||||||
ref={(input) => {this.colorInput = input;}}
|
ref={colorInput}
|
||||||
onClick={this.togglePicker}
|
onClick={togglePicker}
|
||||||
style={this.props.style}
|
style={props.style}
|
||||||
name={this.props.name}
|
name={props.name}
|
||||||
placeholder={this.props.default}
|
placeholder={props.default}
|
||||||
value={this.props.value ? this.props.value : ""}
|
value={props.value ? props.value : ""}
|
||||||
onChange={(e) => this.onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -27,94 +27,91 @@ export type InputDynamicArrayProps = {
|
|||||||
|
|
||||||
type InputDynamicArrayInternalProps = InputDynamicArrayProps & WithTranslation;
|
type InputDynamicArrayInternalProps = InputDynamicArrayProps & WithTranslation;
|
||||||
|
|
||||||
class InputDynamicArrayInternal extends React.Component<InputDynamicArrayInternalProps> {
|
const InputDynamicArrayInternal: React.FC<InputDynamicArrayInternalProps> = (props) => {
|
||||||
changeValue(idx: number, newValue: string | number | undefined) {
|
const values = props.value || props.default || [];
|
||||||
const values = this.values.slice(0);
|
|
||||||
values[idx] = newValue;
|
|
||||||
if (this.props.onChange) this.props.onChange(values);
|
|
||||||
}
|
|
||||||
|
|
||||||
get values() {
|
const changeValue = (idx: number, newValue: string | number | undefined) => {
|
||||||
return this.props.value || this.props.default || [];
|
const newValues = values.slice(0);
|
||||||
}
|
newValues[idx] = newValue;
|
||||||
|
if (props.onChange) props.onChange(newValues);
|
||||||
addValue = () => {
|
|
||||||
const values = this.values.slice(0);
|
|
||||||
if (this.props.type === "number") {
|
|
||||||
values.push(0);
|
|
||||||
}
|
|
||||||
else if (this.props.type === "url") {
|
|
||||||
values.push("");
|
|
||||||
}
|
|
||||||
else if (this.props.type === "enum") {
|
|
||||||
const {fieldSpec} = this.props;
|
|
||||||
const defaultValue = Object.keys(fieldSpec!.values)[0];
|
|
||||||
values.push(defaultValue);
|
|
||||||
} else if (this.props.type === "color") {
|
|
||||||
values.push("#000000");
|
|
||||||
} else {
|
|
||||||
values.push("");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.onChange) this.props.onChange(values);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteValue(valueIdx: number) {
|
const addValue = () => {
|
||||||
const values = this.values.slice(0);
|
const newValues = values.slice(0);
|
||||||
values.splice(valueIdx, 1);
|
if (props.type === "number") {
|
||||||
|
newValues.push(0);
|
||||||
if (this.props.onChange) this.props.onChange(values.length > 0 ? values : undefined);
|
}
|
||||||
|
else if (props.type === "url") {
|
||||||
|
newValues.push("");
|
||||||
|
}
|
||||||
|
else if (props.type === "enum") {
|
||||||
|
const {fieldSpec} = props;
|
||||||
|
const defaultValue = Object.keys(fieldSpec!.values)[0];
|
||||||
|
newValues.push(defaultValue);
|
||||||
|
} else if (props.type === "color") {
|
||||||
|
newValues.push("#000000");
|
||||||
|
} else {
|
||||||
|
newValues.push("");
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
if (props.onChange) props.onChange(newValues);
|
||||||
const t = this.props.t;
|
};
|
||||||
const i18nProps = { t, i18n: this.props.i18n, tReady: this.props.tReady };
|
|
||||||
const inputs = this.values.map((v, i) => {
|
const deleteValue = (valueIdx: number) => {
|
||||||
|
const newValues = values.slice(0);
|
||||||
|
newValues.splice(valueIdx, 1);
|
||||||
|
|
||||||
|
if (props.onChange) props.onChange(newValues.length > 0 ? newValues : undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const t = props.t;
|
||||||
|
const i18nProps = { t, i18n: props.i18n, tReady: props.tReady };
|
||||||
|
const inputs = values.map((v, i) => {
|
||||||
const deleteValueBtn= <DeleteValueInputButton
|
const deleteValueBtn= <DeleteValueInputButton
|
||||||
onClick={this.deleteValue.bind(this, i)}
|
onClick={deleteValue.bind(null, i)}
|
||||||
{...i18nProps}
|
{...i18nProps}
|
||||||
/>;
|
/>;
|
||||||
let input;
|
let input;
|
||||||
if(this.props.type === "url") {
|
if(props.type === "url") {
|
||||||
input = <InputUrl
|
input = <InputUrl
|
||||||
value={v as string}
|
value={v as string}
|
||||||
onChange={this.changeValue.bind(this, i)}
|
onChange={changeValue.bind(null, i)}
|
||||||
aria-label={this.props["aria-label"] || this.props.label}
|
aria-label={props["aria-label"] || props.label}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
else if (this.props.type === "number") {
|
else if (props.type === "number") {
|
||||||
input = <InputNumber
|
input = <InputNumber
|
||||||
value={v as number}
|
value={v as number}
|
||||||
onChange={this.changeValue.bind(this, i)}
|
onChange={changeValue.bind(null, i)}
|
||||||
aria-label={this.props["aria-label"] || this.props.label}
|
aria-label={props["aria-label"] || props.label}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
else if (this.props.type === "enum") {
|
else if (props.type === "enum") {
|
||||||
const options = Object.keys(this.props.fieldSpec?.values).map(v => [v, capitalize(v)]);
|
const options = Object.keys(props.fieldSpec?.values).map(v => [v, capitalize(v)]);
|
||||||
input = <InputEnum
|
input = <InputEnum
|
||||||
options={options}
|
options={options}
|
||||||
value={v as string}
|
value={v as string}
|
||||||
onChange={this.changeValue.bind(this, i)}
|
onChange={changeValue.bind(null, i)}
|
||||||
aria-label={this.props["aria-label"] || this.props.label}
|
aria-label={props["aria-label"] || props.label}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
else if (this.props.type === "color") {
|
else if (props.type === "color") {
|
||||||
input = <InputColor
|
input = <InputColor
|
||||||
value={v as string}
|
value={v as string}
|
||||||
onChange={this.changeValue.bind(this, i)}
|
onChange={changeValue.bind(null, i)}
|
||||||
aria-label={this.props["aria-label"] || this.props.label}
|
aria-label={props["aria-label"] || props.label}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
input = <InputString
|
input = <InputString
|
||||||
value={v as string}
|
value={v as string}
|
||||||
onChange={this.changeValue.bind(this, i)}
|
onChange={changeValue.bind(null, i)}
|
||||||
aria-label={this.props["aria-label"] || this.props.label}
|
aria-label={props["aria-label"] || props.label}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div
|
return <div
|
||||||
style={this.props.style}
|
style={props.style}
|
||||||
key={i}
|
key={i}
|
||||||
className="maputnik-array-block"
|
className="maputnik-array-block"
|
||||||
>
|
>
|
||||||
@@ -132,31 +129,28 @@ class InputDynamicArrayInternal extends React.Component<InputDynamicArrayInterna
|
|||||||
{inputs}
|
{inputs}
|
||||||
<InputButton
|
<InputButton
|
||||||
className="maputnik-array-add-value"
|
className="maputnik-array-add-value"
|
||||||
onClick={this.addValue}
|
onClick={addValue}
|
||||||
>
|
>
|
||||||
{t("Add value")}
|
{t("Add value")}
|
||||||
</InputButton>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const InputDynamicArray = withTranslation()(InputDynamicArrayInternal);
|
export const InputDynamicArray = withTranslation()(InputDynamicArrayInternal);
|
||||||
type DeleteValueInputButtonProps = {
|
type DeleteValueInputButtonProps = {
|
||||||
onClick?(...args: unknown[]): unknown
|
onClick?(...args: unknown[]): unknown
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class DeleteValueInputButton extends React.Component<DeleteValueInputButtonProps> {
|
const DeleteValueInputButton: React.FC<DeleteValueInputButtonProps> = (props) => {
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
|
||||||
return <InputButton
|
return <InputButton
|
||||||
className="maputnik-delete-stop"
|
className="maputnik-delete-stop"
|
||||||
onClick={this.props.onClick}
|
onClick={props.onClick}
|
||||||
title={t("Remove array item")}
|
title={t("Remove array item")}
|
||||||
>
|
>
|
||||||
<FieldDocLabel
|
<FieldDocLabel
|
||||||
label={<MdDelete />}
|
label={<MdDelete />}
|
||||||
/>
|
/>
|
||||||
</InputButton>;
|
</InputButton>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -25,25 +25,23 @@ export type InputEnumProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export class InputEnum extends React.Component<InputEnumProps> {
|
export const InputEnum: React.FC<InputEnumProps> = (props) => {
|
||||||
render() {
|
const {options, value, onChange, name, label} = props;
|
||||||
const {options, value, onChange, name, label} = this.props;
|
|
||||||
|
|
||||||
if(options.length <= 3 && optionsLabelLength(options) <= 20) {
|
if(options.length <= 3 && optionsLabelLength(options) <= 20) {
|
||||||
return <InputMultiInput
|
return <InputMultiInput
|
||||||
name={name}
|
name={name}
|
||||||
options={options}
|
options={options}
|
||||||
value={(value || this.props.default)!}
|
value={(value || props.default)!}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
aria-label={this.props["aria-label"] || label}
|
aria-label={props["aria-label"] || label}
|
||||||
/>;
|
/>;
|
||||||
} else {
|
} else {
|
||||||
return <InputSelect
|
return <InputSelect
|
||||||
options={options}
|
options={options}
|
||||||
value={(value || this.props.default)!}
|
value={(value || props.default)!}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
aria-label={this.props["aria-label"] || label}
|
aria-label={props["aria-label"] || label}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,13 +11,9 @@ export type InputFontProps = {
|
|||||||
"aria-label"?: string
|
"aria-label"?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
export class InputFont extends React.Component<InputFontProps> {
|
export const InputFont: React.FC<InputFontProps> = ({fonts = [], ...props}) => {
|
||||||
static defaultProps = {
|
const getValues = () => {
|
||||||
fonts: []
|
const out = props.value || props.default || [];
|
||||||
};
|
|
||||||
|
|
||||||
get values() {
|
|
||||||
const out = this.props.value || this.props.default || [];
|
|
||||||
|
|
||||||
// Always put a "" in the last field to you can keep adding entries
|
// Always put a "" in the last field to you can keep adding entries
|
||||||
if (out[out.length-1] !== ""){
|
if (out[out.length-1] !== ""){
|
||||||
@@ -26,28 +22,29 @@ export class InputFont extends React.Component<InputFontProps> {
|
|||||||
else {
|
else {
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
changeFont(idx: number, newValue: string) {
|
const values = getValues();
|
||||||
const changedValues = this.values.slice(0);
|
|
||||||
|
const changeFont = (idx: number, newValue: string) => {
|
||||||
|
const changedValues = values.slice(0);
|
||||||
changedValues[idx] = newValue;
|
changedValues[idx] = newValue;
|
||||||
const filteredValues = changedValues
|
const filteredValues = changedValues
|
||||||
.filter(v => v !== undefined)
|
.filter(v => v !== undefined)
|
||||||
.filter(v => v !== "");
|
.filter(v => v !== "");
|
||||||
|
|
||||||
this.props.onChange(filteredValues);
|
props.onChange(filteredValues);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
const inputs = values.map((value, i) => {
|
||||||
const inputs = this.values.map((value, i) => {
|
|
||||||
return <li
|
return <li
|
||||||
key={i}
|
key={i}
|
||||||
>
|
>
|
||||||
<InputAutocomplete
|
<InputAutocomplete
|
||||||
aria-label={this.props["aria-label"] || this.props.name}
|
aria-label={props["aria-label"] || props.name}
|
||||||
value={value}
|
value={value}
|
||||||
options={this.props.fonts?.map(f => [f, f])}
|
options={fonts.map(f => [f, f])}
|
||||||
onChange={this.changeFont.bind(this, i)}
|
onChange={changeFont.bind(null, i)}
|
||||||
/>
|
/>
|
||||||
</li>;
|
</li>;
|
||||||
});
|
});
|
||||||
@@ -57,5 +54,4 @@ export class InputFont extends React.Component<InputFontProps> {
|
|||||||
{inputs}
|
{inputs}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||||
|
|
||||||
@@ -25,88 +25,49 @@ export type InputJsonProps = {
|
|||||||
};
|
};
|
||||||
type InputJsonInternalProps = InputJsonProps & WithTranslation;
|
type InputJsonInternalProps = InputJsonProps & WithTranslation;
|
||||||
|
|
||||||
type InputJsonState = {
|
function getPrettyJson(data: any) {
|
||||||
isEditing: boolean
|
|
||||||
prevValue: string
|
|
||||||
};
|
|
||||||
|
|
||||||
class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJsonState> {
|
|
||||||
static defaultProps = {
|
|
||||||
onFocus: () => {},
|
|
||||||
onBlur: () => {},
|
|
||||||
withScroll: false
|
|
||||||
};
|
|
||||||
_view: EditorView | undefined;
|
|
||||||
_el: HTMLDivElement | null = null;
|
|
||||||
_cancelNextChange: boolean = false;
|
|
||||||
|
|
||||||
constructor(props: InputJsonInternalProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
isEditing: false,
|
|
||||||
prevValue: this.getPrettyJson(this.props.value),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getPrettyJson(data: any) {
|
|
||||||
return stringifyPretty(data, {indent: 2, maxLength: 40});
|
return stringifyPretty(data, {indent: 2, maxLength: 40});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
const InputJsonInternal: React.FC<InputJsonInternalProps> = ({
|
||||||
this._view = createEditor({
|
value,
|
||||||
parent: this._el!,
|
className,
|
||||||
value: this.getPrettyJson(this.props.value),
|
onChange,
|
||||||
lintType: this.props.lintType || "layer",
|
onFocus = () => {},
|
||||||
onChange: (value:string) => this.onChange(value),
|
onBlur = () => {},
|
||||||
onFocus: () => this.onFocus(),
|
lintType,
|
||||||
onBlur: () => this.onBlur(),
|
spec,
|
||||||
spec: this.props.spec
|
withScroll = false,
|
||||||
});
|
}) => {
|
||||||
}
|
const el = useRef<HTMLDivElement | null>(null);
|
||||||
|
const view = useRef<EditorView | undefined>(undefined);
|
||||||
|
const cancelNextChange = useRef<boolean>(false);
|
||||||
|
// `isEditing` and `prevValue` are never rendered, they are only read from the
|
||||||
|
// CodeMirror callbacks, so refs keep them up to date without re-rendering.
|
||||||
|
const isEditing = useRef<boolean>(false);
|
||||||
|
const prevValue = useRef<string>(getPrettyJson(value));
|
||||||
|
// Mirrors `prevProps.value` from the previous `componentDidUpdate`.
|
||||||
|
const prevValueProp = useRef<object>(value);
|
||||||
|
|
||||||
onFocus = () => {
|
const handleFocus = () => {
|
||||||
if (this.props.onFocus) this.props.onFocus();
|
if (onFocus) onFocus();
|
||||||
this.setState({
|
isEditing.current = true;
|
||||||
isEditing: true,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onBlur = () => {
|
const handleBlur = () => {
|
||||||
if (this.props.onBlur) this.props.onBlur();
|
if (onBlur) onBlur();
|
||||||
this.setState({
|
isEditing.current = false;
|
||||||
isEditing: false,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidUpdate(prevProps: InputJsonProps) {
|
const handleChange = () => {
|
||||||
if (!this.state.isEditing && prevProps.value !== this.props.value) {
|
if (cancelNextChange.current) {
|
||||||
this._cancelNextChange = true;
|
cancelNextChange.current = false;
|
||||||
const transactionSpec: TransactionSpec = {
|
prevValue.current = view.current!.state.doc.toString();
|
||||||
changes: {
|
|
||||||
from: 0,
|
|
||||||
to: this._view!.state.doc.length,
|
|
||||||
insert: this.getPrettyJson(this.props.value)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (this.props.withScroll) {
|
|
||||||
transactionSpec.selection = this._view!.state.selection;
|
|
||||||
transactionSpec.scrollIntoView = true;
|
|
||||||
}
|
|
||||||
this._view!.dispatch(transactionSpec);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange = (_e: unknown) => {
|
|
||||||
if (this._cancelNextChange) {
|
|
||||||
this._cancelNextChange = false;
|
|
||||||
this.setState({
|
|
||||||
prevValue: this._view!.state.doc.toString(),
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newCode = this._view!.state.doc.toString();
|
const newCode = view.current!.state.doc.toString();
|
||||||
|
|
||||||
if (this.state.prevValue !== newCode) {
|
if (prevValue.current !== newCode) {
|
||||||
let parsedLayer, err;
|
let parsedLayer, err;
|
||||||
try {
|
try {
|
||||||
parsedLayer = JSON.parse(newCode);
|
parsedLayer = JSON.parse(newCode);
|
||||||
@@ -116,23 +77,63 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!err) {
|
if (!err) {
|
||||||
if (this.props.onChange) this.props.onChange(parsedLayer);
|
if (onChange) onChange(parsedLayer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
prevValue.current = newCode;
|
||||||
prevValue: newCode,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
// The editor is created once on mount, so its callbacks have to go through a
|
||||||
|
// ref to always see the latest props.
|
||||||
|
const handlers = useRef({handleChange, handleFocus, handleBlur});
|
||||||
|
useEffect(() => {
|
||||||
|
handlers.current = {handleChange, handleFocus, handleBlur};
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
view.current = createEditor({
|
||||||
|
parent: el.current!,
|
||||||
|
value: getPrettyJson(value),
|
||||||
|
lintType: lintType || "layer",
|
||||||
|
onChange: () => handlers.current.handleChange(),
|
||||||
|
onFocus: () => handlers.current.handleFocus(),
|
||||||
|
onBlur: () => handlers.current.handleBlur(),
|
||||||
|
spec: spec
|
||||||
|
});
|
||||||
|
// Runs once on mount, mirroring the previous componentDidMount.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only: adding deps would rebuild the editor on every change
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Only react to an actual change of the `value` prop, which also skips the
|
||||||
|
// initial mount (the editor is created with the value already).
|
||||||
|
if (prevValueProp.current === value) return;
|
||||||
|
prevValueProp.current = value;
|
||||||
|
|
||||||
|
if (isEditing.current) return;
|
||||||
|
|
||||||
|
cancelNextChange.current = true;
|
||||||
|
const transactionSpec: TransactionSpec = {
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: view.current!.state.doc.length,
|
||||||
|
insert: getPrettyJson(value)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (withScroll) {
|
||||||
|
transactionSpec.selection = view.current!.state.selection;
|
||||||
|
transactionSpec.scrollIntoView = true;
|
||||||
|
}
|
||||||
|
view.current!.dispatch(transactionSpec);
|
||||||
|
}, [value, withScroll]);
|
||||||
|
|
||||||
return <div className="json-editor" data-wd-key="json-editor" aria-hidden="true" style={{cursor: "text"}}>
|
return <div className="json-editor" data-wd-key="json-editor" aria-hidden="true" style={{cursor: "text"}}>
|
||||||
<div
|
<div
|
||||||
className={classnames("codemirror-container", this.props.className)}
|
className={classnames("codemirror-container", className)}
|
||||||
ref={(el) => {this._el = el;}}
|
ref={el}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const InputJson = withTranslation()(InputJsonInternal);
|
export const InputJson = withTranslation()(InputJsonInternal);
|
||||||
|
|||||||
@@ -9,22 +9,21 @@ export type InputMultiInputProps = {
|
|||||||
"aria-label"?: string
|
"aria-label"?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
export class InputMultiInput extends React.Component<InputMultiInputProps> {
|
export const InputMultiInput: React.FC<InputMultiInputProps> = (props) => {
|
||||||
render() {
|
let options = props.options;
|
||||||
let options = this.props.options;
|
|
||||||
if(options.length > 0 && !Array.isArray(options[0])) {
|
if(options.length > 0 && !Array.isArray(options[0])) {
|
||||||
options = options.map(v => [v, v]);
|
options = options.map(v => [v, v]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedValue = this.props.value || options[0][0];
|
const selectedValue = props.value || options[0][0];
|
||||||
const radios = options.map(([val, label])=> {
|
const radios = options.map(([val, label])=> {
|
||||||
return <label
|
return <label
|
||||||
key={val}
|
key={val}
|
||||||
className={classnames("maputnik-button", "maputnik-radio-as-button", {"maputnik-button-selected": val === selectedValue})}
|
className={classnames("maputnik-button", "maputnik-radio-as-button", {"maputnik-button-selected": val === selectedValue})}
|
||||||
>
|
>
|
||||||
<input type="radio"
|
<input type="radio"
|
||||||
name={this.props.name}
|
name={props.name}
|
||||||
onChange={_e => this.props.onChange(val)}
|
onChange={_e => props.onChange(val)}
|
||||||
value={val}
|
value={val}
|
||||||
checked={val === selectedValue}
|
checked={val === selectedValue}
|
||||||
/>
|
/>
|
||||||
@@ -32,8 +31,7 @@ export class InputMultiInput extends React.Component<InputMultiInputProps> {
|
|||||||
</label>;
|
</label>;
|
||||||
});
|
});
|
||||||
|
|
||||||
return <fieldset className="maputnik-multibutton" aria-label={this.props["aria-label"]}>
|
return <fieldset className="maputnik-multibutton" aria-label={props["aria-label"]}>
|
||||||
{radios}
|
{radios}
|
||||||
</fieldset>;
|
</fieldset>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
+99
-127
@@ -1,5 +1,4 @@
|
|||||||
import React, { type BaseSyntheticEvent } from "react";
|
import React, { type BaseSyntheticEvent, useRef, useState } from "react";
|
||||||
import { generateUniqueId } from "../libs/document-uid";
|
|
||||||
|
|
||||||
export type InputNumberProps = {
|
export type InputNumberProps = {
|
||||||
value?: number
|
value?: number
|
||||||
@@ -14,162 +13,136 @@ export type InputNumberProps = {
|
|||||||
"aria-label"?: string
|
"aria-label"?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
type InputNumberState = {
|
export const InputNumber: React.FC<InputNumberProps> = (props) => {
|
||||||
uuid: number
|
const { rangeStep = 1 } = props;
|
||||||
editing: boolean
|
|
||||||
editingRange?: boolean
|
|
||||||
value?: number
|
|
||||||
/**
|
|
||||||
* This is the value that is currently being edited. It can be an invalid value.
|
|
||||||
*/
|
|
||||||
dirtyValue?: number | string | undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
export class InputNumber extends React.Component<InputNumberProps, InputNumberState> {
|
const [editing, setEditing] = useState(false);
|
||||||
static defaultProps = {
|
const [editingRange, setEditingRange] = useState(false);
|
||||||
rangeStep: 1
|
const [value, setValue] = useState<number | undefined>(props.value);
|
||||||
};
|
/** The value currently being edited. It can be an invalid value. */
|
||||||
_keyboardEvent: boolean = false;
|
const [dirtyValue, setDirtyValue] = useState<number | string | undefined>(props.value);
|
||||||
|
|
||||||
constructor(props: InputNumberProps) {
|
const keyboardEvent = useRef(false);
|
||||||
super(props);
|
|
||||||
this.state = {
|
// Replaces getDerivedStateFromProps: while not editing, track the prop.
|
||||||
uuid: +generateUniqueId(),
|
if (!editing && props.value !== value) {
|
||||||
editing: false,
|
setValue(props.value);
|
||||||
value: props.value,
|
setDirtyValue(props.value);
|
||||||
dirtyValue: props.value,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props: Readonly<InputNumberProps>, state: InputNumberState) {
|
function isValid(v: number | string | undefined) {
|
||||||
if (!state.editing && props.value !== state.value) {
|
|
||||||
return {
|
|
||||||
value: props.value,
|
|
||||||
dirtyValue: props.value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
changeValue(newValue: number | string | undefined) {
|
|
||||||
const value = (newValue === "" || newValue === undefined) ?
|
|
||||||
undefined : +newValue;
|
|
||||||
|
|
||||||
const hasChanged = this.props.value !== value;
|
|
||||||
if(this.isValid(value) && hasChanged) {
|
|
||||||
if (this.props.onChange) this.props.onChange(value);
|
|
||||||
this.setState({
|
|
||||||
value: value,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (!this.isValid(value) && hasChanged) {
|
|
||||||
this.setState({
|
|
||||||
value: undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
dirtyValue: newValue === "" ? undefined : newValue,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
isValid(v: number | string | undefined) {
|
|
||||||
if (v === undefined) {
|
if (v === undefined) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = +v;
|
const val = +v;
|
||||||
if(isNaN(value)) {
|
if(isNaN(val)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!isNaN(this.props.min!) && value < this.props.min!) {
|
if(!isNaN(props.min!) && val < props.min!) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!isNaN(this.props.max!) && value > this.props.max!) {
|
if(!isNaN(props.max!) && val > props.max!) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
resetValue = () => {
|
function changeValue(newValue: number | string | undefined) {
|
||||||
this.setState({editing: false});
|
const val = (newValue === "" || newValue === undefined) ?
|
||||||
|
undefined : +newValue;
|
||||||
|
|
||||||
|
const hasChanged = props.value !== val;
|
||||||
|
if(isValid(val) && hasChanged) {
|
||||||
|
if (props.onChange) props.onChange(val);
|
||||||
|
setValue(val);
|
||||||
|
}
|
||||||
|
else if (!isValid(val) && hasChanged) {
|
||||||
|
setValue(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDirtyValue(newValue === "" ? undefined : newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetValue = () => {
|
||||||
|
setEditing(false);
|
||||||
// Reset explicitly to default value if value has been cleared
|
// Reset explicitly to default value if value has been cleared
|
||||||
if(!this.state.value) {
|
if(!value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If set value is invalid fall back to the last valid value from props or at last resort the default value
|
// If set value is invalid fall back to the last valid value from props or at last resort the default value
|
||||||
if (!this.isValid(this.state.value)) {
|
if (!isValid(value)) {
|
||||||
if(this.isValid(this.props.value)) {
|
if(isValid(props.value)) {
|
||||||
this.changeValue(this.props.value);
|
changeValue(props.value);
|
||||||
this.setState({dirtyValue: this.props.value});
|
setDirtyValue(props.value);
|
||||||
} else {
|
} else {
|
||||||
this.changeValue(undefined);
|
changeValue(undefined);
|
||||||
this.setState({dirtyValue: undefined});
|
setDirtyValue(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeRange = (e: BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) => {
|
const onChangeRange = (e: BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) => {
|
||||||
let value = parseFloat(e.target.value);
|
let newValue = parseFloat(e.target.value);
|
||||||
const step = this.props.rangeStep;
|
const step = rangeStep;
|
||||||
let dirtyValue = value;
|
let newDirtyValue = newValue;
|
||||||
|
|
||||||
if(step) {
|
if(step) {
|
||||||
// Can't do this with the <input/> range step attribute else we won't be able to set a high precision value via the text input.
|
// Can't do this with the <input/> range step attribute else we won't be able to set a high precision value via the text input.
|
||||||
const snap = value % step;
|
const snap = newValue % step;
|
||||||
|
|
||||||
// Round up/down to step
|
// Round up/down to step
|
||||||
if (this._keyboardEvent) {
|
if (keyboardEvent.current) {
|
||||||
// If it's keyboard event we might get a low positive/negative value,
|
// If it's keyboard event we might get a low positive/negative value,
|
||||||
// for example we might go from 13 to 13.23, however because we know
|
// for example we might go from 13 to 13.23, however because we know
|
||||||
// that came from a keyboard event we always want to increase by a
|
// that came from a keyboard event we always want to increase by a
|
||||||
// single step value.
|
// single step value.
|
||||||
if (value < +this.state.dirtyValue!) {
|
if (newValue < +dirtyValue!) {
|
||||||
value = this.state.value! - step;
|
newValue = value! - step;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
value = this.state.value! + step;
|
newValue = value! + step;
|
||||||
}
|
}
|
||||||
dirtyValue = value;
|
newDirtyValue = newValue;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (snap < step/2) {
|
if (snap < step/2) {
|
||||||
value = value - snap;
|
newValue = newValue - snap;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
value = value + (step - snap);
|
newValue = newValue + (step - snap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._keyboardEvent = false;
|
keyboardEvent.current = false;
|
||||||
|
|
||||||
// Clamp between min/max
|
// Clamp between min/max
|
||||||
value = Math.max(this.props.min!, Math.min(this.props.max!, value));
|
newValue = Math.max(props.min!, Math.min(props.max!, newValue));
|
||||||
|
|
||||||
this.setState({value, dirtyValue});
|
setValue(newValue);
|
||||||
if (this.props.onChange) this.props.onChange(value);
|
setDirtyValue(newDirtyValue);
|
||||||
|
if (props.onChange) props.onChange(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
|
||||||
if(
|
if(
|
||||||
Object.prototype.hasOwnProperty.call(this.props, "min") &&
|
Object.prototype.hasOwnProperty.call(props, "min") &&
|
||||||
Object.prototype.hasOwnProperty.call(this.props, "max") &&
|
Object.prototype.hasOwnProperty.call(props, "max") &&
|
||||||
this.props.min !== undefined && this.props.max !== undefined &&
|
props.min !== undefined && props.max !== undefined &&
|
||||||
this.props.allowRange
|
props.allowRange
|
||||||
) {
|
) {
|
||||||
const value = this.state.editing ? this.state.dirtyValue : this.state.value;
|
const currentValue = editing ? dirtyValue : value;
|
||||||
const defaultValue = this.props.default === undefined ? "" : this.props.default;
|
const defaultValue = props.default === undefined ? "" : props.default;
|
||||||
let inputValue;
|
let inputValue;
|
||||||
if (this.state.editingRange) {
|
if (editingRange) {
|
||||||
inputValue = this.state.value;
|
inputValue = value;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
inputValue = value;
|
inputValue = currentValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="maputnik-number-container">
|
return <div className="maputnik-number-container">
|
||||||
@@ -177,70 +150,69 @@ export class InputNumber extends React.Component<InputNumberProps, InputNumberSt
|
|||||||
className="maputnik-number-range"
|
className="maputnik-number-range"
|
||||||
key="range"
|
key="range"
|
||||||
type="range"
|
type="range"
|
||||||
max={this.props.max}
|
max={props.max}
|
||||||
min={this.props.min}
|
min={props.min}
|
||||||
step="any"
|
step="any"
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
value={value === undefined ? defaultValue : value}
|
value={currentValue === undefined ? defaultValue : currentValue}
|
||||||
onChange={this.onChangeRange}
|
onChange={onChangeRange}
|
||||||
onKeyDown={() => {
|
onKeyDown={() => {
|
||||||
this._keyboardEvent = true;
|
keyboardEvent.current = true;
|
||||||
}}
|
}}
|
||||||
onPointerDown={() => {
|
onPointerDown={() => {
|
||||||
this.setState({editing: true, editingRange: true});
|
setEditing(true);
|
||||||
|
setEditingRange(true);
|
||||||
}}
|
}}
|
||||||
onPointerUp={() => {
|
onPointerUp={() => {
|
||||||
// Safari doesn't get onBlur event
|
// Safari doesn't get onBlur event
|
||||||
this.setState({editing: false, editingRange: false});
|
setEditing(false);
|
||||||
|
setEditingRange(false);
|
||||||
}}
|
}}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
this.setState({
|
setEditing(false);
|
||||||
editing: false,
|
setEditingRange(false);
|
||||||
editingRange: false,
|
setDirtyValue(value);
|
||||||
dirtyValue: this.state.value,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
data-wd-key={this.props["data-wd-key"] + "-range"}
|
data-wd-key={props["data-wd-key"] + "-range"}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
key="text"
|
key="text"
|
||||||
type="text"
|
type="text"
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
className="maputnik-number"
|
className="maputnik-number"
|
||||||
placeholder={this.props.default?.toString()}
|
placeholder={props.default?.toString()}
|
||||||
value={inputValue === undefined ? "" : inputValue}
|
value={inputValue === undefined ? "" : inputValue}
|
||||||
onFocus={_e => {
|
onFocus={_e => {
|
||||||
this.setState({editing: true});
|
setEditing(true);
|
||||||
}}
|
}}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
this.changeValue(e.target.value);
|
changeValue(e.target.value);
|
||||||
}}
|
}}
|
||||||
onBlur={_e => {
|
onBlur={_e => {
|
||||||
this.setState({editing: false});
|
setEditing(false);
|
||||||
this.resetValue();
|
resetValue();
|
||||||
}}
|
}}
|
||||||
data-wd-key={this.props["data-wd-key"] + "-text"}
|
data-wd-key={props["data-wd-key"] + "-text"}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const value = this.state.editing ? this.state.dirtyValue : this.state.value;
|
const currentValue = editing ? dirtyValue : value;
|
||||||
|
|
||||||
return <input
|
return <input
|
||||||
aria-label={this.props["aria-label"]}
|
aria-label={props["aria-label"]}
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
className="maputnik-number"
|
className="maputnik-number"
|
||||||
placeholder={this.props.default?.toString()}
|
placeholder={props.default?.toString()}
|
||||||
value={value === undefined ? "" : value}
|
value={currentValue === undefined ? "" : currentValue}
|
||||||
onChange={e => this.changeValue(e.target.value)}
|
onChange={e => changeValue(e.target.value)}
|
||||||
onFocus={() => {
|
onFocus={() => {
|
||||||
this.setState({editing: true});
|
setEditing(true);
|
||||||
}}
|
}}
|
||||||
onBlur={this.resetValue}
|
onBlur={resetValue}
|
||||||
required={this.props.required}
|
required={props.required}
|
||||||
data-wd-key={this.props["data-wd-key"]}
|
data-wd-key={props["data-wd-key"]}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,23 +10,21 @@ export type InputSelectProps = {
|
|||||||
"aria-label"?: string
|
"aria-label"?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
export class InputSelect extends React.Component<InputSelectProps> {
|
export const InputSelect: React.FC<InputSelectProps> = (props) => {
|
||||||
render() {
|
let options = props.options;
|
||||||
let options = this.props.options;
|
|
||||||
if(options.length > 0 && !Array.isArray(options[0])) {
|
if(options.length > 0 && !Array.isArray(options[0])) {
|
||||||
options = options.map((v) => [v, v]) as [string, any][];
|
options = options.map((v) => [v, v]) as [string, any][];
|
||||||
}
|
}
|
||||||
|
|
||||||
return <select
|
return <select
|
||||||
className="maputnik-select"
|
className="maputnik-select"
|
||||||
data-wd-key={this.props["data-wd-key"]}
|
data-wd-key={props["data-wd-key"]}
|
||||||
style={this.props.style}
|
style={props.style}
|
||||||
title={this.props.title}
|
title={props.title}
|
||||||
value={this.props.value}
|
value={props.value}
|
||||||
onChange={e => this.props.onChange(e.target.value)}
|
onChange={e => props.onChange(e.target.value)}
|
||||||
aria-label={this.props["aria-label"]}
|
aria-label={props["aria-label"]}
|
||||||
>
|
>
|
||||||
{ options.map(([val, label]) => <option key={val} value={val}>{label}</option>) }
|
{ options.map(([val, label]) => <option key={val} value={val}>{label}</option>) }
|
||||||
</select>;
|
</select>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -38,31 +38,31 @@ export type InputSpecProps = {
|
|||||||
/** Display any field from the Maplibre GL style spec and
|
/** Display any field from the Maplibre GL style spec and
|
||||||
* choose the correct field component based on the @{fieldSpec}
|
* choose the correct field component based on the @{fieldSpec}
|
||||||
* to display @{value}. */
|
* to display @{value}. */
|
||||||
export class InputSpec extends React.Component<InputSpecProps> {
|
export const InputSpec: React.FC<InputSpecProps> = (props) => {
|
||||||
|
|
||||||
childNodes() {
|
const childNodes = () => {
|
||||||
const commonProps = {
|
const commonProps = {
|
||||||
fieldSpec: this.props.fieldSpec,
|
fieldSpec: props.fieldSpec,
|
||||||
label: this.props.label,
|
label: props.label,
|
||||||
action: this.props.action,
|
action: props.action,
|
||||||
style: this.props.style,
|
style: props.style,
|
||||||
value: this.props.value,
|
value: props.value,
|
||||||
default: this.props.fieldSpec?.default,
|
default: props.fieldSpec?.default,
|
||||||
name: this.props.fieldName,
|
name: props.fieldName,
|
||||||
"data-wd-key": "spec-field-input:" + this.props.fieldName,
|
"data-wd-key": "spec-field-input:" + props.fieldName,
|
||||||
onChange: (newValue: number | undefined | (string | number | undefined)[]) => this.props.onChange!(this.props.fieldName, newValue),
|
onChange: (newValue: number | undefined | (string | number | undefined)[]) => props.onChange!(props.fieldName, newValue),
|
||||||
"aria-label": this.props["aria-label"],
|
"aria-label": props["aria-label"],
|
||||||
};
|
};
|
||||||
switch(this.props.fieldSpec?.type) {
|
switch(props.fieldSpec?.type) {
|
||||||
case "number": return (
|
case "number": return (
|
||||||
<InputNumber
|
<InputNumber
|
||||||
{...commonProps as InputNumberProps}
|
{...commonProps as InputNumberProps}
|
||||||
min={this.props.fieldSpec.minimum}
|
min={props.fieldSpec.minimum}
|
||||||
max={this.props.fieldSpec.maximum}
|
max={props.fieldSpec.maximum}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "enum": {
|
case "enum": {
|
||||||
const options = Object.keys(this.props.fieldSpec.values || []).map(v => [v, capitalize(v)]);
|
const options = Object.keys(props.fieldSpec.values || []).map(v => [v, capitalize(v)]);
|
||||||
|
|
||||||
return <InputEnum
|
return <InputEnum
|
||||||
{...commonProps as Omit<InputEnumProps, "options">}
|
{...commonProps as Omit<InputEnumProps, "options">}
|
||||||
@@ -72,8 +72,8 @@ export class InputSpec extends React.Component<InputSpecProps> {
|
|||||||
case "resolvedImage":
|
case "resolvedImage":
|
||||||
case "formatted":
|
case "formatted":
|
||||||
case "string":
|
case "string":
|
||||||
if (iconProperties.indexOf(this.props.fieldName!) >= 0) {
|
if (iconProperties.indexOf(props.fieldName!) >= 0) {
|
||||||
const options = this.props.fieldSpec.values || [];
|
const options = props.fieldSpec.values || [];
|
||||||
return <InputAutocomplete
|
return <InputAutocomplete
|
||||||
{...commonProps as Omit<InputAutocompleteProps, "options">}
|
{...commonProps as Omit<InputAutocompleteProps, "options">}
|
||||||
options={options.map(f => [f, f])}
|
options={options.map(f => [f, f])}
|
||||||
@@ -94,61 +94,59 @@ export class InputSpec extends React.Component<InputSpecProps> {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "array":
|
case "array":
|
||||||
if(this.props.fieldName === "text-font") {
|
if(props.fieldName === "text-font") {
|
||||||
return <InputFont
|
return <InputFont
|
||||||
{...commonProps as InputFontProps}
|
{...commonProps as InputFontProps}
|
||||||
fonts={this.props.fieldSpec.values}
|
fonts={props.fieldSpec.values}
|
||||||
/>;
|
/>;
|
||||||
} else {
|
} else {
|
||||||
if (this.props.fieldSpec.length) {
|
if (props.fieldSpec.length) {
|
||||||
return <InputArray
|
return <InputArray
|
||||||
{...commonProps as InputArrayProps}
|
{...commonProps as InputArrayProps}
|
||||||
type={this.props.fieldSpec.value}
|
type={props.fieldSpec.value}
|
||||||
length={this.props.fieldSpec.length}
|
length={props.fieldSpec.length}
|
||||||
/>;
|
/>;
|
||||||
} else {
|
} else {
|
||||||
return <InputDynamicArray
|
return <InputDynamicArray
|
||||||
{...commonProps as InputDynamicArrayProps}
|
{...commonProps as InputDynamicArrayProps}
|
||||||
fieldSpec={this.props.fieldSpec}
|
fieldSpec={props.fieldSpec}
|
||||||
type={this.props.fieldSpec.value as InputDynamicArrayProps["type"]}
|
type={props.fieldSpec.value as InputDynamicArrayProps["type"]}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "numberArray": return (
|
case "numberArray": return (
|
||||||
<InputDynamicArray
|
<InputDynamicArray
|
||||||
{...commonProps as InputDynamicArrayProps}
|
{...commonProps as InputDynamicArrayProps}
|
||||||
fieldSpec={this.props.fieldSpec}
|
fieldSpec={props.fieldSpec}
|
||||||
type="number"
|
type="number"
|
||||||
value={(Array.isArray(this.props.value) ? this.props.value : [this.props.value]) as (string | number | undefined)[]}
|
value={(Array.isArray(props.value) ? props.value : [props.value]) as (string | number | undefined)[]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "colorArray": return (
|
case "colorArray": return (
|
||||||
<InputDynamicArray
|
<InputDynamicArray
|
||||||
{...commonProps as InputDynamicArrayProps}
|
{...commonProps as InputDynamicArrayProps}
|
||||||
fieldSpec={this.props.fieldSpec}
|
fieldSpec={props.fieldSpec}
|
||||||
type="color"
|
type="color"
|
||||||
value={(Array.isArray(this.props.value) ? this.props.value : [this.props.value]) as (string | number | undefined)[]}
|
value={(Array.isArray(props.value) ? props.value : [props.value]) as (string | number | undefined)[]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "padding": return (
|
case "padding": return (
|
||||||
<InputArray
|
<InputArray
|
||||||
{...commonProps as InputArrayProps}
|
{...commonProps as InputArrayProps}
|
||||||
type="number"
|
type="number"
|
||||||
value={(Array.isArray(this.props.value) ? this.props.value : [this.props.value]) as (string | number | undefined)[]}
|
value={(Array.isArray(props.value) ? props.value : [props.value]) as (string | number | undefined)[]}
|
||||||
length={4}
|
length={4}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
console.warn(`No proper field input for ${this.props.fieldName} type: ${this.props.fieldSpec?.type}`);
|
console.warn(`No proper field input for ${props.fieldName} type: ${props.fieldSpec?.type}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
return (
|
||||||
<div data-wd-key={"spec-field:"+this.props.fieldName}>
|
<div data-wd-key={"spec-field:"+props.fieldName}>
|
||||||
{this.childNodes()}
|
{childNodes()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
export type InputStringProps = {
|
export type InputStringProps = {
|
||||||
"data-wd-key"?: string
|
"data-wd-key"?: string
|
||||||
@@ -15,38 +15,21 @@ export type InputStringProps = {
|
|||||||
title?: string
|
title?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
type InputStringState = {
|
export const InputString: React.FC<InputStringProps> = (props) => {
|
||||||
editing: boolean
|
const { onInput = () => {} } = props;
|
||||||
value?: string
|
const [editing, setEditing] = useState(false);
|
||||||
};
|
const [value, setValue] = useState<string | undefined>(props.value);
|
||||||
|
|
||||||
export class InputString extends React.Component<InputStringProps, InputStringState> {
|
// Replaces getDerivedStateFromProps: while the field is not being edited its
|
||||||
static defaultProps = {
|
// value tracks the prop, so an external change to the style is picked up.
|
||||||
onInput: () => {},
|
if (!editing && value !== props.value) {
|
||||||
};
|
setValue(props.value);
|
||||||
|
|
||||||
constructor(props: InputStringProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
editing: false,
|
|
||||||
value: props.value || ""
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props: Readonly<InputStringProps>, state: InputStringState) {
|
|
||||||
if (!state.editing) {
|
|
||||||
return {
|
|
||||||
value: props.value
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let tag;
|
let tag;
|
||||||
let classes;
|
let classes;
|
||||||
|
|
||||||
if(this.props.multi) {
|
if(props.multi) {
|
||||||
tag = "textarea";
|
tag = "textarea";
|
||||||
classes = [
|
classes = [
|
||||||
"maputnik-string",
|
"maputnik-string",
|
||||||
@@ -60,40 +43,38 @@ export class InputString extends React.Component<InputStringProps, InputStringSt
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.props.disabled) {
|
if(props.disabled) {
|
||||||
classes.push("maputnik-string--disabled");
|
classes.push("maputnik-string--disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
return React.createElement(tag, {
|
return React.createElement(tag, {
|
||||||
"aria-label": this.props["aria-label"],
|
"aria-label": props["aria-label"],
|
||||||
"data-wd-key": this.props["data-wd-key"],
|
"data-wd-key": props["data-wd-key"],
|
||||||
spellCheck: Object.prototype.hasOwnProperty.call(this.props, "spellCheck") ? this.props.spellCheck : !(tag === "input"),
|
spellCheck: Object.prototype.hasOwnProperty.call(props, "spellCheck") ? props.spellCheck : !(tag === "input"),
|
||||||
disabled: this.props.disabled,
|
disabled: props.disabled,
|
||||||
className: classes.join(" "),
|
className: classes.join(" "),
|
||||||
style: this.props.style,
|
style: props.style,
|
||||||
value: this.state.value === undefined ? "" : this.state.value,
|
value: value === undefined ? "" : value,
|
||||||
placeholder: this.props.default,
|
placeholder: props.default,
|
||||||
title: this.props.title,
|
title: props.title,
|
||||||
onChange: (e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) => {
|
onChange: (e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) => {
|
||||||
this.setState({
|
setEditing(true);
|
||||||
editing: true,
|
setValue(e.target.value);
|
||||||
value: e.target.value
|
onInput(e.target.value);
|
||||||
}, () => {
|
|
||||||
if (this.props.onInput) this.props.onInput(this.state.value);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
onBlur: () => {
|
onBlur: () => {
|
||||||
if(this.state.value!==this.props.value) {
|
// Note: editing is only cleared when the value actually changed; this
|
||||||
this.setState({editing: false});
|
// mirrors the original and keeps a no-op blur from resyncing the value.
|
||||||
if (this.props.onChange) this.props.onChange(this.state.value);
|
if(value !== props.value) {
|
||||||
|
setEditing(false);
|
||||||
|
if (props.onChange) props.onChange(value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onKeyDown: (e) => {
|
onKeyDown: (e: React.KeyboardEvent) => {
|
||||||
if (e.keyCode === 13 && this.props.onChange) {
|
if (e.keyCode === 13 && props.onChange) {
|
||||||
this.props.onChange(this.state.value);
|
props.onChange(value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
required: this.props.required,
|
required: props.required,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
+16
-35
@@ -1,4 +1,4 @@
|
|||||||
import React, { type JSX } from "react";
|
import React, { type JSX, useState } from "react";
|
||||||
import { InputString } from "./InputString";
|
import { InputString } from "./InputString";
|
||||||
import { SmallError } from "./SmallError";
|
import { SmallError } from "./SmallError";
|
||||||
import { Trans, type WithTranslation, withTranslation } from "react-i18next";
|
import { Trans, type WithTranslation, withTranslation } from "react-i18next";
|
||||||
@@ -46,49 +46,30 @@ export type FieldUrlProps = {
|
|||||||
|
|
||||||
type InputUrlInternalProps = FieldUrlProps & WithTranslation;
|
type InputUrlInternalProps = FieldUrlProps & WithTranslation;
|
||||||
|
|
||||||
type InputUrlState = {
|
const InputUrlInternal: React.FC<InputUrlInternalProps> = ({onInput = () => {}, ...props}) => {
|
||||||
error?: ErrorType
|
const [error, setError] = useState<ErrorType | undefined>(() => validate(props.value));
|
||||||
|
|
||||||
|
const handleInput = (url: string) => {
|
||||||
|
setError(validate(url));
|
||||||
|
onInput(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
class InputUrlInternal extends React.Component<InputUrlInternalProps, InputUrlState> {
|
const handleChange = (url: string) => {
|
||||||
static defaultProps = {
|
setError(validate(url));
|
||||||
onInput: () => {},
|
props.onChange(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor (props: InputUrlInternalProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
error: validate(props.value),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onInput = (url: string) => {
|
|
||||||
this.setState({
|
|
||||||
error: validate(url),
|
|
||||||
});
|
|
||||||
if (this.props.onInput) this.props.onInput(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
onChange = (url: string) => {
|
|
||||||
this.setState({
|
|
||||||
error: validate(url),
|
|
||||||
});
|
|
||||||
this.props.onChange(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<InputString
|
<InputString
|
||||||
{...this.props}
|
{...props}
|
||||||
onInput={this.onInput}
|
onInput={handleInput}
|
||||||
onChange={this.onChange}
|
onChange={handleChange}
|
||||||
aria-label={this.props["aria-label"]}
|
aria-label={props["aria-label"]}
|
||||||
/>
|
/>
|
||||||
{errorTypeToJsx(this.state.error, this.props.t)}
|
{errorTypeToJsx(error, props.t)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const InputUrl = withTranslation()(InputUrlInternal);
|
export const InputUrl = withTranslation()(InputUrlInternal);
|
||||||
|
|||||||
+89
-100
@@ -1,4 +1,4 @@
|
|||||||
import React, { type JSX } from "react";
|
import React, { type JSX, useState } from "react";
|
||||||
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";
|
||||||
@@ -17,7 +17,9 @@ import { FieldMaxZoom } from "./FieldMaxZoom";
|
|||||||
import { FieldComment } from "./FieldComment";
|
import { FieldComment } from "./FieldComment";
|
||||||
import { FieldSource } from "./FieldSource";
|
import { FieldSource } from "./FieldSource";
|
||||||
import { FieldSourceLayer } from "./FieldSourceLayer";
|
import { FieldSourceLayer } from "./FieldSourceLayer";
|
||||||
import { changeType, changeProperty } from "../libs/layer";
|
// Aliased: the component defines its own changeProperty, which would otherwise
|
||||||
|
// shadow this import (as a class method there was no collision).
|
||||||
|
import { changeType, changeProperty as changeLayerProperty } from "../libs/layer";
|
||||||
import { formatLayerId } from "../libs/format";
|
import { formatLayerId } from "../libs/format";
|
||||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||||
import { type TFunction } from "i18next";
|
import { type TFunction } from "i18next";
|
||||||
@@ -74,20 +76,20 @@ function getLayoutForType(type: LayerSpecification["type"], t: TFunction): Maput
|
|||||||
return getLayoutForSymbolType(t);
|
return getLayoutForSymbolType(t);
|
||||||
}
|
}
|
||||||
const groups: MaputnikLayoutGroup[] = [];
|
const groups: MaputnikLayoutGroup[] = [];
|
||||||
if (Object.keys(v8["paint_" + type as keyof typeof v8]).length > 0) {
|
if (Object.keys(v8["paint_" + type]).length > 0) {
|
||||||
groups.push({
|
groups.push({
|
||||||
title: t("Paint properties"),
|
title: t("Paint properties"),
|
||||||
id: "Paint_properties",
|
id: "Paint_properties",
|
||||||
type: "properties",
|
type: "properties",
|
||||||
fields: Object.keys(v8["paint_" + type as keyof typeof v8]),
|
fields: Object.keys(v8["paint_" + type]),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (Object.keys(v8["layout_" + type as keyof typeof v8]).length > 0) {
|
if (Object.keys(v8["layout_" + type]).length > 0) {
|
||||||
groups.push({
|
groups.push({
|
||||||
title: t("Layout properties"),
|
title: t("Layout properties"),
|
||||||
id: "Layout_properties",
|
id: "Layout_properties",
|
||||||
type: "properties",
|
type: "properties",
|
||||||
fields: Object.keys(v8["layout_" + type as keyof typeof v8])
|
fields: Object.keys(v8["layout_" + type])
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return groups;
|
return groups;
|
||||||
@@ -131,67 +133,56 @@ type LayerEditorInternalProps = {
|
|||||||
errors?: MappedError[]
|
errors?: MappedError[]
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
type LayerEditorState = {
|
|
||||||
editorGroups: { [keys: string]: boolean }
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Layer editor supporting multiple types of layers. */
|
/** Layer editor supporting multiple types of layers. */
|
||||||
class LayerEditorInternal extends React.Component<LayerEditorInternalProps, LayerEditorState> {
|
const LayerEditorInternal: React.FC<LayerEditorInternalProps> = ({
|
||||||
static defaultProps = {
|
onLayerChanged = () => { },
|
||||||
onLayerChanged: () => { },
|
onLayerIdChange = () => { },
|
||||||
onLayerIdChange: () => { },
|
...rest
|
||||||
onLayerDestroyed: () => { },
|
}) => {
|
||||||
};
|
const props = { onLayerChanged, onLayerIdChange, ...rest } as LayerEditorInternalProps;
|
||||||
|
|
||||||
constructor(props: LayerEditorInternalProps) {
|
const [editorGroups, setEditorGroups] = useState<{ [keys: string]: boolean }>(() => {
|
||||||
super(props);
|
const groups: { [keys: string]: boolean } = {};
|
||||||
|
for (const group of layoutGroups(props.layer.type, props.t)) {
|
||||||
const editorGroups: { [keys: string]: boolean } = {};
|
groups[group.title] = true;
|
||||||
for (const group of layoutGroups(this.props.layer.type, props.t)) {
|
|
||||||
editorGroups[group.title] = true;
|
|
||||||
}
|
}
|
||||||
|
return groups;
|
||||||
|
});
|
||||||
|
|
||||||
this.state = { editorGroups };
|
// Replaces getDerivedStateFromProps: groups that appear after mount (because
|
||||||
}
|
// the layer type changed) start out expanded. Guarded so it only sets state
|
||||||
|
// when a group is genuinely new, otherwise this would loop every render.
|
||||||
static getDerivedStateFromProps(props: Readonly<LayerEditorInternalProps>, state: LayerEditorState) {
|
const newGroups = getLayoutForType(props.layer.type, props.t)
|
||||||
const additionalGroups = { ...state.editorGroups };
|
.filter(group => !(group.title in editorGroups));
|
||||||
|
if (newGroups.length > 0) {
|
||||||
for (const group of getLayoutForType(props.layer.type, props.t)) {
|
const additionalGroups = { ...editorGroups };
|
||||||
if (!(group.title in additionalGroups)) {
|
for (const group of newGroups) {
|
||||||
additionalGroups[group.title] = true;
|
additionalGroups[group.title] = true;
|
||||||
}
|
}
|
||||||
|
setEditorGroups(additionalGroups);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
function changeProperty(group: keyof LayerSpecification | null, property: string, newValue: any) {
|
||||||
editorGroups: additionalGroups
|
props.onLayerChanged(
|
||||||
};
|
props.layerIndex,
|
||||||
}
|
changeLayerProperty(props.layer, group, property, newValue)
|
||||||
|
|
||||||
|
|
||||||
changeProperty(group: keyof LayerSpecification | null, property: string, newValue: any) {
|
|
||||||
this.props.onLayerChanged(
|
|
||||||
this.props.layerIndex,
|
|
||||||
changeProperty(this.props.layer, group, property, newValue)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onGroupToggle(groupTitle: string, active: boolean) {
|
function onGroupToggle(groupTitle: string, active: boolean) {
|
||||||
const changedActiveGroups = {
|
const changedActiveGroups = {
|
||||||
...this.state.editorGroups,
|
...editorGroups,
|
||||||
[groupTitle]: active,
|
[groupTitle]: active,
|
||||||
};
|
};
|
||||||
this.setState({
|
setEditorGroups(changedActiveGroups);
|
||||||
editorGroups: changedActiveGroups
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderGroupType(type: string, fields?: string[]): JSX.Element {
|
function renderGroupType(type: string, fields?: string[]): JSX.Element {
|
||||||
let comment = "";
|
let comment = "";
|
||||||
if (this.props.layer.metadata) {
|
if (props.layer.metadata) {
|
||||||
comment = (this.props.layer.metadata as any)["maputnik:comment"];
|
comment = (props.layer.metadata as any)["maputnik:comment"];
|
||||||
}
|
}
|
||||||
const { errors, layerIndex } = this.props;
|
const { errors, layerIndex } = props;
|
||||||
|
|
||||||
const errorData: MappedLayerErrors = {};
|
const errorData: MappedLayerErrors = {};
|
||||||
errors!.forEach(error => {
|
errors!.forEach(error => {
|
||||||
@@ -207,85 +198,85 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
|||||||
});
|
});
|
||||||
|
|
||||||
let sourceLayerIds;
|
let sourceLayerIds;
|
||||||
const layer = this.props.layer as Exclude<LayerSpecification, BackgroundLayerSpecification>;
|
const layer = props.layer as Exclude<LayerSpecification, BackgroundLayerSpecification>;
|
||||||
if (Object.prototype.hasOwnProperty.call(this.props.sources, layer.source)) {
|
if (Object.prototype.hasOwnProperty.call(props.sources, layer.source)) {
|
||||||
sourceLayerIds = this.props.sources[layer.source].layers;
|
sourceLayerIds = props.sources[layer.source].layers;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "layer": return <div>
|
case "layer": return <div>
|
||||||
<FieldId
|
<FieldId
|
||||||
value={this.props.layer.id}
|
value={props.layer.id}
|
||||||
wdKey="layer-editor.layer-id"
|
wdKey="layer-editor.layer-id"
|
||||||
error={errorData.id}
|
error={errorData.id}
|
||||||
onChange={newId => this.props.onLayerIdChange(this.props.layerIndex, this.props.layer.id, newId)}
|
onChange={newId => props.onLayerIdChange(props.layerIndex, props.layer.id, newId)}
|
||||||
/>
|
/>
|
||||||
<FieldType
|
<FieldType
|
||||||
disabled={true}
|
disabled={true}
|
||||||
error={errorData.type}
|
error={errorData.type}
|
||||||
value={this.props.layer.type}
|
value={props.layer.type}
|
||||||
onChange={newType => this.props.onLayerChanged(
|
onChange={newType => props.onLayerChanged(
|
||||||
this.props.layerIndex,
|
props.layerIndex,
|
||||||
changeType(this.props.layer, newType)
|
changeType(props.layer, newType)
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{this.props.layer.type !== "background" && <FieldSource
|
{props.layer.type !== "background" && <FieldSource
|
||||||
wdKey="layer-editor.layer-source"
|
wdKey="layer-editor.layer-source"
|
||||||
error={errorData.source}
|
error={errorData.source}
|
||||||
sourceIds={Object.keys(this.props.sources!)}
|
sourceIds={Object.keys(props.sources!)}
|
||||||
value={this.props.layer.source}
|
value={props.layer.source}
|
||||||
onChange={v => this.changeProperty(null, "source", v)}
|
onChange={v => changeProperty(null, "source", v)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{!NON_SOURCE_LAYERS.includes(this.props.layer.type) &&
|
{!NON_SOURCE_LAYERS.includes(props.layer.type) &&
|
||||||
<FieldSourceLayer
|
<FieldSourceLayer
|
||||||
error={errorData["source-layer"]}
|
error={errorData["source-layer"]}
|
||||||
sourceLayerIds={sourceLayerIds}
|
sourceLayerIds={sourceLayerIds}
|
||||||
value={(this.props.layer as any)["source-layer"]}
|
value={(props.layer as any)["source-layer"]}
|
||||||
onChange={v => this.changeProperty(null, "source-layer", v)}
|
onChange={v => changeProperty(null, "source-layer", v)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<FieldMinZoom
|
<FieldMinZoom
|
||||||
error={errorData.minzoom}
|
error={errorData.minzoom}
|
||||||
value={this.props.layer.minzoom}
|
value={props.layer.minzoom}
|
||||||
onChange={v => this.changeProperty(null, "minzoom", v)}
|
onChange={v => changeProperty(null, "minzoom", v)}
|
||||||
/>
|
/>
|
||||||
<FieldMaxZoom
|
<FieldMaxZoom
|
||||||
error={errorData.maxzoom}
|
error={errorData.maxzoom}
|
||||||
value={this.props.layer.maxzoom}
|
value={props.layer.maxzoom}
|
||||||
onChange={v => this.changeProperty(null, "maxzoom", v)}
|
onChange={v => changeProperty(null, "maxzoom", v)}
|
||||||
/>
|
/>
|
||||||
<FieldComment
|
<FieldComment
|
||||||
error={errorData.comment}
|
error={errorData.comment}
|
||||||
value={comment}
|
value={comment}
|
||||||
onChange={v => this.changeProperty("metadata", "maputnik:comment", v == "" ? undefined : v)}
|
onChange={v => changeProperty("metadata", "maputnik:comment", v == "" ? undefined : v)}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
case "filter": return <div>
|
case "filter": return <div>
|
||||||
<div className="maputnik-filter-editor-wrapper">
|
<div className="maputnik-filter-editor-wrapper">
|
||||||
<FilterEditor
|
<FilterEditor
|
||||||
errors={errorData}
|
errors={errorData}
|
||||||
filter={(this.props.layer as any).filter}
|
filter={(props.layer as any).filter}
|
||||||
properties={this.props.vectorLayers[(this.props.layer as any)["source-layer"]]}
|
properties={props.vectorLayers[(props.layer as any)["source-layer"]]}
|
||||||
onChange={f => this.changeProperty(null, "filter", f)}
|
onChange={f => changeProperty(null, "filter", f)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
case "properties":
|
case "properties":
|
||||||
return <PropertyGroup
|
return <PropertyGroup
|
||||||
errors={errorData}
|
errors={errorData}
|
||||||
layer={this.props.layer}
|
layer={props.layer}
|
||||||
groupFields={fields!}
|
groupFields={fields!}
|
||||||
spec={this.props.spec}
|
spec={props.spec}
|
||||||
onChange={this.changeProperty.bind(this)}
|
onChange={changeProperty.bind(null)}
|
||||||
/>;
|
/>;
|
||||||
case "jsoneditor":
|
case "jsoneditor":
|
||||||
return <FieldJson
|
return <FieldJson
|
||||||
lintType="layer"
|
lintType="layer"
|
||||||
value={this.props.layer}
|
value={props.layer}
|
||||||
onChange={(layer: LayerSpecification) => {
|
onChange={(layer: LayerSpecification) => {
|
||||||
this.props.onLayerChanged(
|
props.onLayerChanged(
|
||||||
this.props.layerIndex,
|
props.layerIndex,
|
||||||
layer
|
layer
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -294,18 +285,17 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
moveLayer(offset: number) {
|
function moveLayer(offset: number) {
|
||||||
this.props.onMoveLayer({
|
props.onMoveLayer({
|
||||||
oldIndex: this.props.layerIndex,
|
oldIndex: props.layerIndex,
|
||||||
newIndex: this.props.layerIndex + offset
|
newIndex: props.layerIndex + offset
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
|
||||||
|
|
||||||
const groupIds: string[] = [];
|
const groupIds: string[] = [];
|
||||||
const layerType = this.props.layer.type;
|
const layerType = props.layer.type;
|
||||||
const groups = layoutGroups(layerType, t).filter(group => {
|
const groups = layoutGroups(layerType, t).filter(group => {
|
||||||
return !(layerType === "background" && group.type === "source");
|
return !(layerType === "background" && group.type === "source");
|
||||||
}).map(group => {
|
}).map(group => {
|
||||||
@@ -316,14 +306,14 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
|||||||
id={groupId}
|
id={groupId}
|
||||||
key={groupId}
|
key={groupId}
|
||||||
title={group.title}
|
title={group.title}
|
||||||
isActive={this.state.editorGroups[group.title]}
|
isActive={editorGroups[group.title]}
|
||||||
onActiveToggle={this.onGroupToggle.bind(this, group.title)}
|
onActiveToggle={onGroupToggle.bind(null, group.title)}
|
||||||
>
|
>
|
||||||
{this.renderGroupType(group.type, group.fields)}
|
{renderGroupType(group.type, group.fields)}
|
||||||
</LayerEditorGroup>;
|
</LayerEditorGroup>;
|
||||||
});
|
});
|
||||||
|
|
||||||
const layout = this.props.layer.layout || {};
|
const layout = props.layer.layout || {};
|
||||||
|
|
||||||
const items: {
|
const items: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
@@ -335,29 +325,29 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
|||||||
} = {
|
} = {
|
||||||
delete: {
|
delete: {
|
||||||
text: t("Delete"),
|
text: t("Delete"),
|
||||||
handler: () => this.props.onLayerDestroy(this.props.layerIndex),
|
handler: () => props.onLayerDestroy(props.layerIndex),
|
||||||
wdKey: "menu-delete-layer"
|
wdKey: "menu-delete-layer"
|
||||||
},
|
},
|
||||||
duplicate: {
|
duplicate: {
|
||||||
text: t("Duplicate"),
|
text: t("Duplicate"),
|
||||||
handler: () => this.props.onLayerCopy(this.props.layerIndex),
|
handler: () => props.onLayerCopy(props.layerIndex),
|
||||||
wdKey: "menu-duplicate-layer"
|
wdKey: "menu-duplicate-layer"
|
||||||
},
|
},
|
||||||
hide: {
|
hide: {
|
||||||
text: (layout.visibility === "none") ? t("Show") : t("Hide"),
|
text: (layout.visibility === "none") ? t("Show") : t("Hide"),
|
||||||
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex),
|
handler: () => props.onLayerVisibilityToggle(props.layerIndex),
|
||||||
wdKey: "menu-hide-layer"
|
wdKey: "menu-hide-layer"
|
||||||
},
|
},
|
||||||
moveLayerUp: {
|
moveLayerUp: {
|
||||||
text: t("Move layer up"),
|
text: t("Move layer up"),
|
||||||
disabled: this.props.isFirstLayer,
|
disabled: props.isFirstLayer,
|
||||||
handler: () => this.moveLayer(-1),
|
handler: () => moveLayer(-1),
|
||||||
wdKey: "menu-move-layer-up"
|
wdKey: "menu-move-layer-up"
|
||||||
},
|
},
|
||||||
moveLayerDown: {
|
moveLayerDown: {
|
||||||
text: t("Move layer down"),
|
text: t("Move layer down"),
|
||||||
disabled: this.props.isLastLayer,
|
disabled: props.isLastLayer,
|
||||||
handler: () => this.moveLayer(+1),
|
handler: () => moveLayer(+1),
|
||||||
wdKey: "menu-move-layer-down"
|
wdKey: "menu-move-layer-down"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -376,7 +366,7 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
|||||||
<header data-wd-key="layer-editor.header">
|
<header data-wd-key="layer-editor.header">
|
||||||
<div className="layer-header">
|
<div className="layer-header">
|
||||||
<h2 className="layer-header__title">
|
<h2 className="layer-header__title">
|
||||||
{t("Layer")}: {formatLayerId(this.props.layer.id)}
|
{t("Layer")}: {formatLayerId(props.layer.id)}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="layer-header__info">
|
<div className="layer-header__info">
|
||||||
<Wrapper
|
<Wrapper
|
||||||
@@ -417,7 +407,6 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
</section>
|
</section>
|
||||||
</IconContext.Provider>;
|
</IconContext.Provider>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const LayerEditor = withTranslation()(LayerEditorInternal);
|
export const LayerEditor = withTranslation()(LayerEditorInternal);
|
||||||
|
|||||||
@@ -18,22 +18,20 @@ type LayerEditorGroupProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export class LayerEditorGroup extends React.Component<LayerEditorGroupProps> {
|
export const LayerEditorGroup: React.FC<LayerEditorGroupProps> = (props) => {
|
||||||
render() {
|
return <AccordionItem uuid={props.id}>
|
||||||
return <AccordionItem uuid={this.props.id}>
|
|
||||||
<AccordionItemHeading className="maputnik-layer-editor-group"
|
<AccordionItemHeading className="maputnik-layer-editor-group"
|
||||||
data-wd-key={"layer-editor-group:"+this.props["data-wd-key"]}
|
data-wd-key={"layer-editor-group:"+props["data-wd-key"]}
|
||||||
onClick={_e => this.props.onActiveToggle(!this.props.isActive)}
|
onClick={_e => props.onActiveToggle(!props.isActive)}
|
||||||
>
|
>
|
||||||
<AccordionItemButton className="maputnik-layer-editor-group__button">
|
<AccordionItemButton className="maputnik-layer-editor-group__button">
|
||||||
<span style={{flexGrow: 1, alignContent: "center"}}>{this.props.title}</span>
|
<span style={{flexGrow: 1, alignContent: "center"}}>{props.title}</span>
|
||||||
<MdArrowDropUp size={"2em"} className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--up"></MdArrowDropUp>
|
<MdArrowDropUp size={"2em"} className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--up"></MdArrowDropUp>
|
||||||
<MdArrowDropDown size={"2em"} className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--down"></MdArrowDropDown>
|
<MdArrowDropDown size={"2em"} className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--down"></MdArrowDropDown>
|
||||||
</AccordionItemButton>
|
</AccordionItemButton>
|
||||||
</AccordionItemHeading>
|
</AccordionItemHeading>
|
||||||
<AccordionItemPanel>
|
<AccordionItemPanel>
|
||||||
{this.props.children}
|
{props.children}
|
||||||
</AccordionItemPanel>
|
</AccordionItemPanel>
|
||||||
</AccordionItem>;
|
</AccordionItem>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
+122
-133
@@ -1,4 +1,4 @@
|
|||||||
import React, {type JSX} from "react";
|
import React, {type JSX, useEffect, useRef, useState} from "react";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
import lodash from "lodash";
|
import lodash from "lodash";
|
||||||
import {
|
import {
|
||||||
@@ -37,62 +37,98 @@ type LayerListContainerProps = {
|
|||||||
};
|
};
|
||||||
type LayerListContainerInternalProps = LayerListContainerProps & WithTranslation;
|
type LayerListContainerInternalProps = LayerListContainerProps & WithTranslation;
|
||||||
|
|
||||||
type LayerListContainerState = {
|
const noopLayerSelect = () => {};
|
||||||
collapsedGroups: {[ket: string]: boolean}
|
|
||||||
areAllGroupsExpanded: boolean
|
// Replaces the previous `shouldComponentUpdate`. Note the inversion: this
|
||||||
keys: {[key: string]: number}
|
// returns true when the props are EQUAL (i.e. no re-render is needed), whereas
|
||||||
isOpen: {[key: string]: boolean}
|
// `shouldComponentUpdate` returned true when a re-render WAS needed.
|
||||||
|
function arePropsEqual(prevProps: LayerListContainerInternalProps, nextProps: LayerListContainerInternalProps) {
|
||||||
|
// This component tree only requires id and visibility from the layers
|
||||||
|
// objects
|
||||||
|
function getRequiredProps(layer: LayerSpecification) {
|
||||||
|
const out: {id: string, layout?: { visibility: any}} = {
|
||||||
|
id: layer.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (layer.layout) {
|
||||||
|
out.layout = {
|
||||||
|
visibility: layer.layout.visibility
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
const layersEqual = lodash.isEqual(
|
||||||
|
nextProps.layers.map(getRequiredProps),
|
||||||
|
prevProps.layers.map(getRequiredProps),
|
||||||
|
);
|
||||||
|
|
||||||
|
function withoutLayers(props: LayerListContainerInternalProps) {
|
||||||
|
const out = {
|
||||||
|
...props
|
||||||
|
} as LayerListContainerInternalProps & { layers?: any };
|
||||||
|
delete out["layers"];
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare the props without layers because we've already compared them
|
||||||
|
// efficiently above.
|
||||||
|
const propsEqual = lodash.isEqual(
|
||||||
|
withoutLayers(prevProps),
|
||||||
|
withoutLayers(nextProps)
|
||||||
|
);
|
||||||
|
|
||||||
|
return layersEqual && propsEqual;
|
||||||
|
}
|
||||||
|
|
||||||
// List of collapsible layer editors
|
// List of collapsible layer editors
|
||||||
class LayerListContainerInternal extends React.Component<LayerListContainerInternalProps, LayerListContainerState> {
|
function LayerListContainerInternal({
|
||||||
static defaultProps = {
|
layers: propsLayers,
|
||||||
onLayerSelect: () => {},
|
selectedLayerIndex,
|
||||||
};
|
onLayersChange,
|
||||||
selectedItemRef: React.RefObject<any>;
|
onLayerSelect = noopLayerSelect,
|
||||||
scrollContainerRef: React.RefObject<HTMLElement | null>;
|
onLayerDestroy,
|
||||||
|
onLayerCopy,
|
||||||
|
onLayerVisibilityToggle,
|
||||||
|
sources,
|
||||||
|
errors,
|
||||||
|
t,
|
||||||
|
}: LayerListContainerInternalProps) {
|
||||||
|
const selectedItemRef = useRef<any>(null);
|
||||||
|
const scrollContainerRef = useRef<HTMLElement | null>(null);
|
||||||
|
const hasMountedRef = useRef(false);
|
||||||
|
|
||||||
constructor(props: LayerListContainerInternalProps) {
|
const [collapsedGroups, setCollapsedGroups] = useState<{[key: string]: boolean}>({});
|
||||||
super(props);
|
const [areAllGroupsExpanded, setAreAllGroupsExpanded] = useState(false);
|
||||||
this.selectedItemRef = React.createRef();
|
const [keys, setKeys] = useState<{[key: string]: number}>(() => ({
|
||||||
this.scrollContainerRef = React.createRef();
|
|
||||||
this.state = {
|
|
||||||
collapsedGroups: {},
|
|
||||||
areAllGroupsExpanded: false,
|
|
||||||
keys: {
|
|
||||||
add: +generateUniqueId(),
|
add: +generateUniqueId(),
|
||||||
},
|
}));
|
||||||
isOpen: {
|
const [isOpen, setIsOpen] = useState<{[key: string]: boolean}>({
|
||||||
add: false,
|
add: false,
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleModal(modalName: string) {
|
|
||||||
this.setState({
|
|
||||||
keys: {
|
|
||||||
...this.state.keys,
|
|
||||||
[modalName]: +generateUniqueId(),
|
|
||||||
},
|
|
||||||
isOpen: {
|
|
||||||
...this.state.isOpen,
|
|
||||||
[modalName]: !this.state.isOpen[modalName]
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function toggleModal(modalName: string) {
|
||||||
|
setKeys(prevKeys => ({
|
||||||
|
...prevKeys,
|
||||||
|
[modalName]: +generateUniqueId(),
|
||||||
|
}));
|
||||||
|
setIsOpen(prevIsOpen => ({
|
||||||
|
...prevIsOpen,
|
||||||
|
[modalName]: !prevIsOpen[modalName]
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleLayers = () => {
|
const toggleLayers = () => {
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
|
|
||||||
const newGroups: {[key:string]: boolean} = {};
|
const newGroups: {[key:string]: boolean} = {};
|
||||||
|
|
||||||
this.groupedLayers().forEach(layers => {
|
groupedLayers().forEach(layers => {
|
||||||
const groupPrefix = layerPrefix(layers[0].id);
|
const groupPrefix = layerPrefix(layers[0].id);
|
||||||
const lookupKey = [groupPrefix, idx].join("-");
|
const lookupKey = [groupPrefix, idx].join("-");
|
||||||
|
|
||||||
|
|
||||||
if (layers.length > 1) {
|
if (layers.length > 1) {
|
||||||
newGroups[lookupKey] = this.state.areAllGroupsExpanded;
|
newGroups[lookupKey] = areAllGroupsExpanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
layers.forEach((_layer) => {
|
layers.forEach((_layer) => {
|
||||||
@@ -100,19 +136,17 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setState({
|
setCollapsedGroups(newGroups);
|
||||||
collapsedGroups: newGroups,
|
setAreAllGroupsExpanded(!areAllGroupsExpanded);
|
||||||
areAllGroupsExpanded: !this.state.areAllGroupsExpanded
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
groupedLayers(): (LayerSpecification & {key: string})[][] {
|
function groupedLayers(): (LayerSpecification & {key: string})[][] {
|
||||||
const groups = [];
|
const groups = [];
|
||||||
const layerIdCount = new Map();
|
const layerIdCount = new Map();
|
||||||
|
|
||||||
for (let i = 0; i < this.props.layers.length; i++) {
|
for (let i = 0; i < propsLayers.length; i++) {
|
||||||
const origLayer = this.props.layers[i];
|
const origLayer = propsLayers[i];
|
||||||
const previousLayer = this.props.layers[i-1];
|
const previousLayer = propsLayers[i-1];
|
||||||
layerIdCount.set(origLayer.id,
|
layerIdCount.set(origLayer.id,
|
||||||
layerIdCount.has(origLayer.id) ? layerIdCount.get(origLayer.id) + 1 : 0
|
layerIdCount.has(origLayer.id) ? layerIdCount.get(origLayer.id) + 1 : 0
|
||||||
);
|
);
|
||||||
@@ -130,75 +164,35 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
|||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleLayerGroup(groupPrefix: string, idx: number) {
|
function toggleLayerGroup(groupPrefix: string, idx: number) {
|
||||||
const lookupKey = [groupPrefix, idx].join("-");
|
const lookupKey = [groupPrefix, idx].join("-");
|
||||||
const newGroups = { ...this.state.collapsedGroups };
|
setCollapsedGroups(prevCollapsedGroups => {
|
||||||
if(lookupKey in this.state.collapsedGroups) {
|
const newGroups = { ...prevCollapsedGroups };
|
||||||
newGroups[lookupKey] = !this.state.collapsedGroups[lookupKey];
|
if(lookupKey in prevCollapsedGroups) {
|
||||||
|
newGroups[lookupKey] = !prevCollapsedGroups[lookupKey];
|
||||||
} else {
|
} else {
|
||||||
newGroups[lookupKey] = false;
|
newGroups[lookupKey] = false;
|
||||||
}
|
}
|
||||||
this.setState({
|
return newGroups;
|
||||||
collapsedGroups: newGroups
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
isCollapsed(groupPrefix: string, idx: number) {
|
function isCollapsed(groupPrefix: string, idx: number) {
|
||||||
const collapsed = this.state.collapsedGroups[[groupPrefix, idx].join("-")];
|
const collapsed = collapsedGroups[[groupPrefix, idx].join("-")];
|
||||||
return collapsed === undefined ? true : collapsed;
|
return collapsed === undefined ? true : collapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate (nextProps: LayerListContainerProps, nextState: LayerListContainerState) {
|
useEffect(() => {
|
||||||
// Always update on state change
|
// `componentDidUpdate` did not run on mount, so skip the first run here too.
|
||||||
if (this.state !== nextState) {
|
if (!hasMountedRef.current) {
|
||||||
return true;
|
hasMountedRef.current = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
const selectedItemNode = selectedItemRef.current;
|
||||||
// This component tree only requires id and visibility from the layers
|
|
||||||
// objects
|
|
||||||
function getRequiredProps(layer: LayerSpecification) {
|
|
||||||
const out: {id: string, layout?: { visibility: any}} = {
|
|
||||||
id: layer.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (layer.layout) {
|
|
||||||
out.layout = {
|
|
||||||
visibility: layer.layout.visibility
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
const layersEqual = lodash.isEqual(
|
|
||||||
nextProps.layers.map(getRequiredProps),
|
|
||||||
this.props.layers.map(getRequiredProps),
|
|
||||||
);
|
|
||||||
|
|
||||||
function withoutLayers(props: LayerListContainerProps) {
|
|
||||||
const out = {
|
|
||||||
...props
|
|
||||||
} as LayerListContainerProps & { layers?: any };
|
|
||||||
delete out["layers"];
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare the props without layers because we've already compared them
|
|
||||||
// efficiently above.
|
|
||||||
const propsEqual = lodash.isEqual(
|
|
||||||
withoutLayers(this.props),
|
|
||||||
withoutLayers(nextProps)
|
|
||||||
);
|
|
||||||
|
|
||||||
const propsChanged = !(layersEqual && propsEqual);
|
|
||||||
return propsChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate (prevProps: LayerListContainerProps) {
|
|
||||||
if (prevProps.selectedLayerIndex !== this.props.selectedLayerIndex) {
|
|
||||||
const selectedItemNode = this.selectedItemRef.current;
|
|
||||||
if (selectedItemNode && selectedItemNode.node) {
|
if (selectedItemNode && selectedItemNode.node) {
|
||||||
const target = selectedItemNode.node;
|
const target = selectedItemNode.node;
|
||||||
const options = {
|
const options = {
|
||||||
root: this.scrollContainerRef.current,
|
root: scrollContainerRef.current,
|
||||||
threshold: 1.0
|
threshold: 1.0
|
||||||
};
|
};
|
||||||
const observer = new IntersectionObserver(entries => {
|
const observer = new IntersectionObserver(entries => {
|
||||||
@@ -210,32 +204,30 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
|||||||
|
|
||||||
observer.observe(target);
|
observer.observe(target);
|
||||||
}
|
}
|
||||||
}
|
}, [selectedLayerIndex]);
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
const listItems: JSX.Element[] = [];
|
const listItems: JSX.Element[] = [];
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
const layersByGroup = this.groupedLayers();
|
const layersByGroup = groupedLayers();
|
||||||
layersByGroup.forEach(layers => {
|
layersByGroup.forEach(layers => {
|
||||||
const groupPrefix = layerPrefix(layers[0].id);
|
const groupPrefix = layerPrefix(layers[0].id);
|
||||||
if(layers.length > 1) {
|
if(layers.length > 1) {
|
||||||
|
const currentIdx = idx;
|
||||||
const grp = <LayerListGroup
|
const grp = <LayerListGroup
|
||||||
data-wd-key={[groupPrefix, idx].join("-")}
|
data-wd-key={[groupPrefix, idx].join("-")}
|
||||||
aria-controls={layers.map(l => l.key).join(" ")}
|
aria-controls={layers.map(l => l.key).join(" ")}
|
||||||
key={`group-${groupPrefix}-${idx}`}
|
key={`group-${groupPrefix}-${idx}`}
|
||||||
title={groupPrefix}
|
title={groupPrefix}
|
||||||
isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex}
|
isActive={!isCollapsed(groupPrefix, idx) || idx === selectedLayerIndex}
|
||||||
onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)}
|
onActiveToggle={() => toggleLayerGroup(groupPrefix, currentIdx)}
|
||||||
/>;
|
/>;
|
||||||
listItems.push(grp);
|
listItems.push(grp);
|
||||||
}
|
}
|
||||||
|
|
||||||
layers.forEach((layer, idxInGroup) => {
|
layers.forEach((layer, idxInGroup) => {
|
||||||
const groupIdx = findClosestCommonPrefix(this.props.layers, idx);
|
const groupIdx = findClosestCommonPrefix(propsLayers, idx);
|
||||||
|
|
||||||
const layerError = this.props.errors.find(error => {
|
const layerError = errors.find(error => {
|
||||||
return (
|
return (
|
||||||
error.parsed &&
|
error.parsed &&
|
||||||
error.parsed.type === "layer" &&
|
error.parsed.type === "layer" &&
|
||||||
@@ -244,13 +236,13 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
|||||||
});
|
});
|
||||||
|
|
||||||
const additionalProps: {ref?: React.RefObject<any>} = {};
|
const additionalProps: {ref?: React.RefObject<any>} = {};
|
||||||
if (idx === this.props.selectedLayerIndex) {
|
if (idx === selectedLayerIndex) {
|
||||||
additionalProps.ref = this.selectedItemRef;
|
additionalProps.ref = selectedItemRef;
|
||||||
}
|
}
|
||||||
|
|
||||||
const listItem = <LayerListItem
|
const listItem = <LayerListItem
|
||||||
className={classnames({
|
className={classnames({
|
||||||
"maputnik-layer-list-item-collapsed": layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex,
|
"maputnik-layer-list-item-collapsed": layers.length > 1 && isCollapsed(groupPrefix, groupIdx) && idx !== selectedLayerIndex,
|
||||||
"maputnik-layer-list-item-group-last": idxInGroup == layers.length - 1 && layers.length > 1,
|
"maputnik-layer-list-item-group-last": idxInGroup == layers.length - 1 && layers.length > 1,
|
||||||
"maputnik-layer-list-item--error": !!layerError
|
"maputnik-layer-list-item--error": !!layerError
|
||||||
})}
|
})}
|
||||||
@@ -260,11 +252,11 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
|||||||
layerIndex={idx}
|
layerIndex={idx}
|
||||||
layerType={layer.type}
|
layerType={layer.type}
|
||||||
visibility={(layer.layout || {}).visibility}
|
visibility={(layer.layout || {}).visibility}
|
||||||
isSelected={idx === this.props.selectedLayerIndex}
|
isSelected={idx === selectedLayerIndex}
|
||||||
onLayerSelect={this.props.onLayerSelect}
|
onLayerSelect={onLayerSelect}
|
||||||
onLayerDestroy={this.props.onLayerDestroy?.bind(this)}
|
onLayerDestroy={onLayerDestroy}
|
||||||
onLayerCopy={this.props.onLayerCopy.bind(this)}
|
onLayerCopy={onLayerCopy}
|
||||||
onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(this)}
|
onLayerVisibilityToggle={onLayerVisibilityToggle}
|
||||||
{...additionalProps}
|
{...additionalProps}
|
||||||
/>;
|
/>;
|
||||||
listItems.push(listItem);
|
listItems.push(listItem);
|
||||||
@@ -272,22 +264,20 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const t = this.props.t;
|
|
||||||
|
|
||||||
return <section
|
return <section
|
||||||
className="maputnik-layer-list"
|
className="maputnik-layer-list"
|
||||||
data-wd-key="layer-list"
|
data-wd-key="layer-list"
|
||||||
role="complementary"
|
role="complementary"
|
||||||
aria-label={t("Layers list")}
|
aria-label={t("Layers list")}
|
||||||
ref={this.scrollContainerRef}
|
ref={scrollContainerRef}
|
||||||
>
|
>
|
||||||
<ModalAdd
|
<ModalAdd
|
||||||
key={this.state.keys.add}
|
key={keys.add}
|
||||||
layers={this.props.layers}
|
layers={propsLayers}
|
||||||
sources={this.props.sources}
|
sources={sources}
|
||||||
isOpen={this.state.isOpen.add}
|
isOpen={isOpen.add}
|
||||||
onOpenToggle={this.toggleModal.bind(this, "add")}
|
onOpenToggle={() => toggleModal("add")}
|
||||||
onLayersChange={this.props.onLayersChange}
|
onLayersChange={onLayersChange}
|
||||||
/>
|
/>
|
||||||
<header className="maputnik-layer-list-header" data-wd-key="layer-list.header">
|
<header className="maputnik-layer-list-header" data-wd-key="layer-list.header">
|
||||||
<span className="maputnik-layer-list-header-title">{t("Layers")}</span>
|
<span className="maputnik-layer-list-header-title">{t("Layers")}</span>
|
||||||
@@ -297,9 +287,9 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
|||||||
<button
|
<button
|
||||||
id="skip-target-layer-list"
|
id="skip-target-layer-list"
|
||||||
data-wd-key="skip-target-layer-list"
|
data-wd-key="skip-target-layer-list"
|
||||||
onClick={this.toggleLayers}
|
onClick={toggleLayers}
|
||||||
className="maputnik-button">
|
className="maputnik-button">
|
||||||
{this.state.areAllGroupsExpanded === true ?
|
{areAllGroupsExpanded === true ?
|
||||||
t("Collapse")
|
t("Collapse")
|
||||||
:
|
:
|
||||||
t("Expand")
|
t("Expand")
|
||||||
@@ -310,7 +300,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
|||||||
<div className="maputnik-default-property">
|
<div className="maputnik-default-property">
|
||||||
<div className="maputnik-multibutton">
|
<div className="maputnik-multibutton">
|
||||||
<button
|
<button
|
||||||
onClick={this.toggleModal.bind(this, "add")}
|
onClick={() => toggleModal("add")}
|
||||||
data-wd-key="layer-list:add-layer"
|
data-wd-key="layer-list:add-layer"
|
||||||
className="maputnik-button maputnik-button-selected">
|
className="maputnik-button maputnik-button-selected">
|
||||||
{t("Add Layer")}
|
{t("Add Layer")}
|
||||||
@@ -328,9 +318,8 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
|||||||
</div>
|
</div>
|
||||||
</section>;
|
</section>;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const LayerListContainer = withTranslation()(LayerListContainerInternal);
|
const LayerListContainer = withTranslation()(React.memo(LayerListContainerInternal, arePropsEqual));
|
||||||
|
|
||||||
type LayerListProps = LayerListContainerProps & {
|
type LayerListProps = LayerListContainerProps & {
|
||||||
onMoveLayer: OnMoveLayerCallback
|
onMoveLayer: OnMoveLayerCallback
|
||||||
|
|||||||
@@ -9,26 +9,24 @@ type LayerListGroupProps = {
|
|||||||
"aria-controls"?: string
|
"aria-controls"?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
export class LayerListGroup extends React.Component<LayerListGroupProps> {
|
export const LayerListGroup: React.FC<LayerListGroupProps> = (props) => {
|
||||||
render() {
|
|
||||||
return <li className="maputnik-layer-list-group">
|
return <li className="maputnik-layer-list-group">
|
||||||
<div className="maputnik-layer-list-group-header"
|
<div className="maputnik-layer-list-group-header"
|
||||||
data-wd-key={"layer-list-group:"+this.props["data-wd-key"]}
|
data-wd-key={"layer-list-group:"+props["data-wd-key"]}
|
||||||
onClick={_e => this.props.onActiveToggle(!this.props.isActive)}
|
onClick={_e => props.onActiveToggle(!props.isActive)}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="maputnik-layer-list-group-title"
|
className="maputnik-layer-list-group-title"
|
||||||
aria-controls={this.props["aria-controls"]}
|
aria-controls={props["aria-controls"]}
|
||||||
aria-expanded={this.props.isActive}
|
aria-expanded={props.isActive}
|
||||||
>
|
>
|
||||||
{this.props.title}
|
{props.title}
|
||||||
</button>
|
</button>
|
||||||
<span className="maputnik-space" />
|
<span className="maputnik-space" />
|
||||||
<Collapser
|
<Collapser
|
||||||
style={{ height: 14, width: 14 }}
|
style={{ height: 14, width: 14 }}
|
||||||
isCollapsed={this.props.isActive}
|
isCollapsed={props.isActive}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</li>;
|
</li>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ type IconActionProps = {
|
|||||||
classBlockModifier?: string
|
classBlockModifier?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
class IconAction extends React.Component<IconActionProps> {
|
const IconAction: React.FC<IconActionProps> = (props) => {
|
||||||
renderIcon() {
|
function renderIcon() {
|
||||||
switch (this.props.action) {
|
switch (props.action) {
|
||||||
case "duplicate": return <MdContentCopy />;
|
case "duplicate": return <MdContentCopy />;
|
||||||
case "show": return <MdVisibility />;
|
case "show": return <MdVisibility />;
|
||||||
case "hide": return <MdVisibilityOff />;
|
case "hide": return <MdVisibilityOff />;
|
||||||
@@ -48,8 +48,7 @@ class IconAction extends React.Component<IconActionProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
const { classBlockName, classBlockModifier } = props;
|
||||||
const { classBlockName, classBlockModifier } = this.props;
|
|
||||||
|
|
||||||
let classAdditions = "";
|
let classAdditions = "";
|
||||||
if (classBlockName) {
|
if (classBlockName) {
|
||||||
@@ -62,16 +61,15 @@ class IconAction extends React.Component<IconActionProps> {
|
|||||||
|
|
||||||
return <button
|
return <button
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
title={this.props.action}
|
title={props.action}
|
||||||
className={`maputnik-layer-list-icon-action ${classAdditions}`}
|
className={`maputnik-layer-list-icon-action ${classAdditions}`}
|
||||||
data-wd-key={this.props.wdKey}
|
data-wd-key={props.wdKey}
|
||||||
onClick={this.props.onClick}
|
onClick={props.onClick}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
{this.renderIcon()}
|
{renderIcon()}
|
||||||
</button>;
|
</button>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
type LayerListItemProps = {
|
type LayerListItemProps = {
|
||||||
id?: string
|
id?: string
|
||||||
|
|||||||
+223
-193
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, {useCallback, useEffect, useReducer, useRef} from "react";
|
||||||
import {createRoot} from "react-dom/client";
|
import {createRoot} from "react-dom/client";
|
||||||
import MapLibreGl, {type LayerSpecification, type LngLat, type Map, type MapOptions, type SourceSpecification, type StyleSpecification} from "maplibre-gl";
|
import MapLibreGl, {type LayerSpecification, type LngLat, type Map, type MapOptions, type SourceSpecification, type StyleSpecification} from "maplibre-gl";
|
||||||
import MaplibreInspect from "@maplibre/maplibre-gl-inspect";
|
import MaplibreInspect from "@maplibre/maplibre-gl-inspect";
|
||||||
@@ -71,199 +71,65 @@ type MapMaplibreGlInternalProps = {
|
|||||||
onChange(value: {center: LngLat, zoom: number, _from: "map" | "app"}): unknown
|
onChange(value: {center: LngLat, zoom: number, _from: "map" | "app"}): unknown
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
type MapMaplibreGlState = {
|
/**
|
||||||
map: Map | null;
|
* Replacement for the previous `shouldComponentUpdate`. `React.memo` expects the
|
||||||
inspect: MaplibreInspect | null;
|
* inverse: `true` means "props are equal, skip the render", whereas
|
||||||
geocoder: MaplibreGeocoder | null;
|
* `shouldComponentUpdate` returned `true` when it *should* re-render.
|
||||||
zoomControl: ZoomControl | null;
|
* As before, if the props cannot be serialized we treat them as equal and skip
|
||||||
zoom?: number;
|
* the render ("no biggie, carry on").
|
||||||
};
|
*/
|
||||||
|
function arePropsEqual(prevProps: MapMaplibreGlInternalProps, nextProps: MapMaplibreGlInternalProps) {
|
||||||
class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps, MapMaplibreGlState> {
|
|
||||||
static defaultProps = {
|
|
||||||
onMapLoaded: () => {},
|
|
||||||
onDataChange: () => {},
|
|
||||||
onLayerSelect: () => {},
|
|
||||||
onChange: () => {},
|
|
||||||
options: {} as MapOptions,
|
|
||||||
};
|
|
||||||
container: HTMLDivElement | null = null;
|
|
||||||
|
|
||||||
constructor(props: MapMaplibreGlInternalProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
map: null,
|
|
||||||
inspect: null,
|
|
||||||
geocoder: null,
|
|
||||||
zoomControl: null,
|
|
||||||
};
|
|
||||||
i18next.on("languageChanged", () => {
|
|
||||||
this.forceUpdate();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps: MapMaplibreGlInternalProps, nextState: MapMaplibreGlState) {
|
|
||||||
let should = false;
|
let should = false;
|
||||||
try {
|
try {
|
||||||
should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState);
|
should = JSON.stringify(prevProps) !== JSON.stringify(nextProps);
|
||||||
} catch(_e) {
|
} catch(_e) {
|
||||||
// no biggie, carry on
|
// no biggie, carry on
|
||||||
}
|
}
|
||||||
return should;
|
return !should;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
const MapMaplibreGlInternal = ({
|
||||||
const map = this.state.map;
|
onDataChange = () => {},
|
||||||
|
onLayerSelect = () => {},
|
||||||
|
onChange = () => {},
|
||||||
|
options = {},
|
||||||
|
mapStyle,
|
||||||
|
mapView,
|
||||||
|
inspectModeEnabled,
|
||||||
|
highlightedLayer,
|
||||||
|
replaceAccessTokens,
|
||||||
|
t,
|
||||||
|
}: MapMaplibreGlInternalProps) => {
|
||||||
|
const container = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const styleWithTokens = this.props.replaceAccessTokens(this.props.mapStyle);
|
// These used to live in `this.state`, but were never able to trigger a
|
||||||
if (map) {
|
// re-render: `shouldComponentUpdate` stringified the state, which throws for
|
||||||
// Maplibre GL now does diffing natively so we don't need to calculate
|
// the (circular) maplibre `Map`, so the comparison was swallowed and the
|
||||||
// the necessary operations ourselves!
|
// component only ever re-rendered on prop changes. They are imperative
|
||||||
// We also need to update the style for inspect to work properly
|
// handles, so they are refs here and mutating them does not re-render.
|
||||||
map.setStyle(styleWithTokens, {diff: true});
|
const map = useRef<Map | null>(null);
|
||||||
map.showTileBoundaries = this.props.options?.showTileBoundaries!;
|
const inspect = useRef<MaplibreInspect | null>(null);
|
||||||
map.showCollisionBoxes = this.props.options?.showCollisionBoxes!;
|
const geocoder = useRef<MaplibreGeocoder | null>(null);
|
||||||
map.showOverdrawInspector = this.props.options?.showOverdrawInspector!;
|
const zoomControl = useRef<ZoomControl | null>(null);
|
||||||
|
const zoom = useRef<number | undefined>(undefined);
|
||||||
|
|
||||||
// set the map view when the prop was updated from outside
|
// `componentDidUpdate` did not run on mount, so neither may the effect below.
|
||||||
if (this.props.mapView._from === "app") {
|
const hasMounted = useRef(false);
|
||||||
map.jumpTo(this.props.mapView);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.state.inspect && this.props.inspectModeEnabled !== this.state.inspect._showInspectMap) {
|
const [, forceUpdate] = useReducer((tick: number) => tick + 1, 0);
|
||||||
this.state.inspect.toggleInspector();
|
|
||||||
}
|
|
||||||
if (this.state.inspect && this.props.inspectModeEnabled) {
|
|
||||||
this.state.inspect.setOriginalStyle(styleWithTokens);
|
|
||||||
// In case the sources are the same, there's a need to refresh the style
|
|
||||||
setTimeout(() => {
|
|
||||||
this.state.inspect!.render();
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
// The maplibre event handlers/callbacks below are registered once (on mount)
|
||||||
|
// but read `this.props` at call time in the class version, so they must not
|
||||||
|
// close over the props of the first render.
|
||||||
|
const latestProps = useRef({onDataChange, onLayerSelect, onChange, mapStyle, inspectModeEnabled, highlightedLayer, t});
|
||||||
|
latestProps.current = {onDataChange, onLayerSelect, onChange, mapStyle, inspectModeEnabled, highlightedLayer, t};
|
||||||
|
|
||||||
componentDidMount() {
|
const onLayerSelectById = useCallback((id: string) => {
|
||||||
const mapOpts = {
|
const index = latestProps.current.mapStyle.layers.findIndex(layer => layer.id === id);
|
||||||
...this.props.options,
|
latestProps.current.onLayerSelect(index);
|
||||||
container: this.container!,
|
}, []);
|
||||||
style: this.props.mapStyle,
|
|
||||||
hash: true,
|
|
||||||
maxZoom: 24,
|
|
||||||
// make root relative urls in stylefiles work as maplibre gl js does
|
|
||||||
// not support this for everything:
|
|
||||||
// https://github.com/maplibre/maplibre-gl-js/issues/6818
|
|
||||||
transformRequest: (url) => {
|
|
||||||
if (url.startsWith("/")) {
|
|
||||||
url = `${window.location.origin}${url}`;
|
|
||||||
}
|
|
||||||
return { url };
|
|
||||||
},
|
|
||||||
// setting to always load glyphs of CJK fonts from server
|
|
||||||
// https://maplibre.org/maplibre-gl-js/docs/examples/local-ideographs/
|
|
||||||
localIdeographFontFamily: false
|
|
||||||
} satisfies MapOptions;
|
|
||||||
|
|
||||||
const protocol = new Protocol({metadata: true});
|
const initGeocoder = useCallback((mapInstance: Map) => {
|
||||||
MapLibreGl.addProtocol("pmtiles",protocol.tile);
|
|
||||||
const map = new MapLibreGl.Map(mapOpts);
|
|
||||||
|
|
||||||
const mapViewChange = () => {
|
|
||||||
const center = map.getCenter();
|
|
||||||
const zoom = map.getZoom();
|
|
||||||
this.props.onChange({center, zoom, _from: "map"});
|
|
||||||
};
|
|
||||||
mapViewChange();
|
|
||||||
|
|
||||||
map.showTileBoundaries = mapOpts.showTileBoundaries!;
|
|
||||||
map.showCollisionBoxes = mapOpts.showCollisionBoxes!;
|
|
||||||
map.showOverdrawInspector = mapOpts.showOverdrawInspector!;
|
|
||||||
|
|
||||||
const geocoder = this.initGeocoder(map);
|
|
||||||
|
|
||||||
const zoomControl = new ZoomControl();
|
|
||||||
map.addControl(zoomControl, "top-right");
|
|
||||||
|
|
||||||
const nav = new MapLibreGl.NavigationControl({visualizePitch:true});
|
|
||||||
map.addControl(nav, "top-right");
|
|
||||||
|
|
||||||
const tmpNode = document.createElement("div");
|
|
||||||
const root = createRoot(tmpNode);
|
|
||||||
|
|
||||||
const inspectPopup = new MapLibreGl.Popup({
|
|
||||||
closeOnClick: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const inspect = new MaplibreInspect({
|
|
||||||
popup: inspectPopup,
|
|
||||||
showMapPopup: true,
|
|
||||||
showMapPopupOnHover: false,
|
|
||||||
showInspectMapPopupOnHover: true,
|
|
||||||
showInspectButton: false,
|
|
||||||
blockHoverPopupOnClick: true,
|
|
||||||
assignLayerColor: (layerId: string, alpha: number) => {
|
|
||||||
return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string();
|
|
||||||
},
|
|
||||||
buildInspectStyle: (originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[]) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer),
|
|
||||||
renderPopup: (features: InspectFeature[]) => {
|
|
||||||
if(this.props.inspectModeEnabled) {
|
|
||||||
inspectPopup.once("open", () => {
|
|
||||||
root.render(<MapMaplibreGlFeaturePropertyPopup features={features} />);
|
|
||||||
});
|
|
||||||
return tmpNode;
|
|
||||||
} else {
|
|
||||||
inspectPopup.once("open", () => {
|
|
||||||
root.render(<MapMaplibreGlLayerPopup
|
|
||||||
features={features}
|
|
||||||
onLayerSelect={this.onLayerSelectById}
|
|
||||||
zoom={this.state.zoom}
|
|
||||||
/>,);
|
|
||||||
});
|
|
||||||
return tmpNode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
map.addControl(inspect);
|
|
||||||
|
|
||||||
map.on("style.load", () => {
|
|
||||||
this.setState({
|
|
||||||
map,
|
|
||||||
inspect,
|
|
||||||
geocoder,
|
|
||||||
zoomControl,
|
|
||||||
zoom: map.getZoom()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
map.on("data", e => {
|
|
||||||
if(e.dataType !== "tile") return;
|
|
||||||
this.props.onDataChange!({
|
|
||||||
map: this.state.map
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
map.on("error", e => {
|
|
||||||
console.log("ERROR", e);
|
|
||||||
});
|
|
||||||
|
|
||||||
map.on("zoom", _e => {
|
|
||||||
this.setState({
|
|
||||||
zoom: map.getZoom()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
map.on("dragend", mapViewChange);
|
|
||||||
map.on("zoomend", mapViewChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
onLayerSelectById = (id: string) => {
|
|
||||||
const index = this.props.mapStyle.layers.findIndex(layer => layer.id === id);
|
|
||||||
this.props.onLayerSelect(index);
|
|
||||||
};
|
|
||||||
|
|
||||||
initGeocoder(map: Map) {
|
|
||||||
const geocoderConfig = {
|
const geocoderConfig = {
|
||||||
forwardGeocode: async (config: MaplibreGeocoderApiConfig) => {
|
forwardGeocode: async (config: MaplibreGeocoderApiConfig) => {
|
||||||
const features = [];
|
const features = [];
|
||||||
@@ -300,26 +166,190 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
} as unknown as MaplibreGeocoderApi;
|
} as unknown as MaplibreGeocoderApi;
|
||||||
const geocoder = new MaplibreGeocoder(geocoderConfig, {
|
const geocoderInstance = new MaplibreGeocoder(geocoderConfig, {
|
||||||
placeholder: this.props.t("Search"),
|
placeholder: latestProps.current.t("Search"),
|
||||||
maplibregl: MapLibreGl,
|
maplibregl: MapLibreGl,
|
||||||
});
|
});
|
||||||
map.addControl(geocoder, "top-left");
|
mapInstance.addControl(geocoderInstance, "top-left");
|
||||||
return geocoder;
|
return geocoderInstance;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Was the `i18next.on("languageChanged", () => this.forceUpdate())` in the
|
||||||
|
// constructor. `forceUpdate` bypassed `shouldComponentUpdate`; a state update
|
||||||
|
// likewise bypasses `React.memo`.
|
||||||
|
useEffect(() => {
|
||||||
|
const onLanguageChanged = () => {
|
||||||
|
forceUpdate();
|
||||||
|
};
|
||||||
|
i18next.on("languageChanged", onLanguageChanged);
|
||||||
|
return () => {
|
||||||
|
i18next.off("languageChanged", onLanguageChanged);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// componentDidMount
|
||||||
|
useEffect(() => {
|
||||||
|
const mapOpts = {
|
||||||
|
...options,
|
||||||
|
container: container.current!,
|
||||||
|
style: mapStyle,
|
||||||
|
hash: true,
|
||||||
|
maxZoom: 24,
|
||||||
|
// make root relative urls in stylefiles work as maplibre gl js does
|
||||||
|
// not support this for everything:
|
||||||
|
// https://github.com/maplibre/maplibre-gl-js/issues/6818
|
||||||
|
transformRequest: (url) => {
|
||||||
|
if (url.startsWith("/")) {
|
||||||
|
url = `${window.location.origin}${url}`;
|
||||||
|
}
|
||||||
|
return { url };
|
||||||
|
},
|
||||||
|
// setting to always load glyphs of CJK fonts from server
|
||||||
|
// https://maplibre.org/maplibre-gl-js/docs/examples/local-ideographs/
|
||||||
|
localIdeographFontFamily: false
|
||||||
|
} satisfies MapOptions;
|
||||||
|
|
||||||
|
const protocol = new Protocol({metadata: true});
|
||||||
|
MapLibreGl.addProtocol("pmtiles",protocol.tile);
|
||||||
|
const mapInstance = new MapLibreGl.Map(mapOpts);
|
||||||
|
|
||||||
|
const mapViewChange = () => {
|
||||||
|
const center = mapInstance.getCenter();
|
||||||
|
const currentZoom = mapInstance.getZoom();
|
||||||
|
latestProps.current.onChange({center, zoom: currentZoom, _from: "map"});
|
||||||
|
};
|
||||||
|
mapViewChange();
|
||||||
|
|
||||||
|
mapInstance.showTileBoundaries = mapOpts.showTileBoundaries!;
|
||||||
|
mapInstance.showCollisionBoxes = mapOpts.showCollisionBoxes!;
|
||||||
|
mapInstance.showOverdrawInspector = mapOpts.showOverdrawInspector!;
|
||||||
|
|
||||||
|
const geocoderInstance = initGeocoder(mapInstance);
|
||||||
|
|
||||||
|
const zoomControlInstance = new ZoomControl();
|
||||||
|
mapInstance.addControl(zoomControlInstance, "top-right");
|
||||||
|
|
||||||
|
const nav = new MapLibreGl.NavigationControl({visualizePitch:true});
|
||||||
|
mapInstance.addControl(nav, "top-right");
|
||||||
|
|
||||||
|
const tmpNode = document.createElement("div");
|
||||||
|
const root = createRoot(tmpNode);
|
||||||
|
|
||||||
|
const inspectPopup = new MapLibreGl.Popup({
|
||||||
|
closeOnClick: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const inspectInstance = new MaplibreInspect({
|
||||||
|
popup: inspectPopup,
|
||||||
|
showMapPopup: true,
|
||||||
|
showMapPopupOnHover: false,
|
||||||
|
showInspectMapPopupOnHover: true,
|
||||||
|
showInspectButton: false,
|
||||||
|
blockHoverPopupOnClick: true,
|
||||||
|
assignLayerColor: (layerId: string, alpha: number) => {
|
||||||
|
return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string();
|
||||||
|
},
|
||||||
|
buildInspectStyle: (originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[]) => buildInspectStyle(originalMapStyle, coloredLayers, latestProps.current.highlightedLayer),
|
||||||
|
renderPopup: (features: InspectFeature[]) => {
|
||||||
|
if(latestProps.current.inspectModeEnabled) {
|
||||||
|
inspectPopup.once("open", () => {
|
||||||
|
root.render(<MapMaplibreGlFeaturePropertyPopup features={features} />);
|
||||||
|
});
|
||||||
|
return tmpNode;
|
||||||
|
} else {
|
||||||
|
inspectPopup.once("open", () => {
|
||||||
|
root.render(<MapMaplibreGlLayerPopup
|
||||||
|
features={features}
|
||||||
|
onLayerSelect={onLayerSelectById}
|
||||||
|
zoom={zoom.current}
|
||||||
|
/>,);
|
||||||
|
});
|
||||||
|
return tmpNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mapInstance.addControl(inspectInstance);
|
||||||
|
|
||||||
|
mapInstance.on("style.load", () => {
|
||||||
|
map.current = mapInstance;
|
||||||
|
inspect.current = inspectInstance;
|
||||||
|
geocoder.current = geocoderInstance;
|
||||||
|
zoomControl.current = zoomControlInstance;
|
||||||
|
zoom.current = mapInstance.getZoom();
|
||||||
|
});
|
||||||
|
|
||||||
|
mapInstance.on("data", e => {
|
||||||
|
if(e.dataType !== "tile") return;
|
||||||
|
latestProps.current.onDataChange!({
|
||||||
|
map: map.current
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mapInstance.on("error", e => {
|
||||||
|
console.log("ERROR", e);
|
||||||
|
});
|
||||||
|
|
||||||
|
mapInstance.on("zoom", _e => {
|
||||||
|
zoom.current = mapInstance.getZoom();
|
||||||
|
});
|
||||||
|
|
||||||
|
mapInstance.on("dragend", mapViewChange);
|
||||||
|
mapInstance.on("zoomend", mapViewChange);
|
||||||
|
// Mount only, exactly like componentDidMount: the map is created from the
|
||||||
|
// props of the first render and kept up to date imperatively below (the
|
||||||
|
// handlers read `latestProps`, so no prop belongs in the dependencies). The
|
||||||
|
// class had no componentWillUnmount, so there is no teardown here either.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only: adding mapStyle/options would re-create the map
|
||||||
|
}, [initGeocoder, onLayerSelectById]);
|
||||||
|
|
||||||
|
// componentDidUpdate. It had no `prevProps` guards, so it ran after *every*
|
||||||
|
// re-render; the equivalent is an effect without a dependency array. Since the
|
||||||
|
// component only re-renders when `arePropsEqual` reports a prop change (or on
|
||||||
|
// a language change, as before), this runs exactly as often as it used to.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasMounted.current) {
|
||||||
|
// componentDidUpdate does not run on mount.
|
||||||
|
hasMounted.current = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
const styleWithTokens = replaceAccessTokens(mapStyle);
|
||||||
const t = this.props.t;
|
if (map.current) {
|
||||||
this.state.geocoder?.setPlaceholder(t("Search"));
|
// Maplibre GL now does diffing natively so we don't need to calculate
|
||||||
this.state.zoomControl?.setLabel(t("Zoom:"));
|
// the necessary operations ourselves!
|
||||||
|
// We also need to update the style for inspect to work properly
|
||||||
|
map.current.setStyle(styleWithTokens, {diff: true});
|
||||||
|
map.current.showTileBoundaries = options?.showTileBoundaries!;
|
||||||
|
map.current.showCollisionBoxes = options?.showCollisionBoxes!;
|
||||||
|
map.current.showOverdrawInspector = options?.showOverdrawInspector!;
|
||||||
|
|
||||||
|
// set the map view when the prop was updated from outside
|
||||||
|
if (mapView._from === "app") {
|
||||||
|
map.current.jumpTo(mapView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(inspect.current && inspectModeEnabled !== inspect.current._showInspectMap) {
|
||||||
|
inspect.current.toggleInspector();
|
||||||
|
}
|
||||||
|
if (inspect.current && inspectModeEnabled) {
|
||||||
|
inspect.current.setOriginalStyle(styleWithTokens);
|
||||||
|
// In case the sources are the same, there's a need to refresh the style
|
||||||
|
setTimeout(() => {
|
||||||
|
inspect.current!.render();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
geocoder.current?.setPlaceholder(t("Search"));
|
||||||
|
zoomControl.current?.setLabel(t("Zoom:"));
|
||||||
return <div
|
return <div
|
||||||
className="maputnik-map__map"
|
className="maputnik-map__map"
|
||||||
role="region"
|
role="region"
|
||||||
aria-label={t("Map view")}
|
aria-label={t("Map view")}
|
||||||
ref={x => {this.container = x;}}
|
ref={container}
|
||||||
data-wd-key="maplibre:map"
|
data-wd-key="maplibre:map"
|
||||||
></div>;
|
></div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const MapMaplibreGl = withTranslation()(MapMaplibreGlInternal);
|
export const MapMaplibreGl = withTranslation()(React.memo(MapMaplibreGlInternal, arePropsEqual));
|
||||||
|
|||||||
@@ -63,9 +63,8 @@ type FeaturePropertyPopupProps = {
|
|||||||
features: InspectFeature[]
|
features: InspectFeature[]
|
||||||
};
|
};
|
||||||
|
|
||||||
export class FeaturePropertyPopup extends React.Component<FeaturePropertyPopupProps> {
|
export const FeaturePropertyPopup: React.FC<FeaturePropertyPopupProps> = (props) => {
|
||||||
render() {
|
const features = removeDuplicatedFeatures(props.features);
|
||||||
const features = removeDuplicatedFeatures(this.props.features);
|
|
||||||
return <div className="maputnik-feature-property-popup" dir="ltr" data-wd-key="feature-property-popup">
|
return <div className="maputnik-feature-property-popup" dir="ltr" data-wd-key="feature-property-popup">
|
||||||
<table className="maputnik-popup-table">
|
<table className="maputnik-popup-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -73,5 +72,4 @@ export class FeaturePropertyPopup extends React.Component<FeaturePropertyPopupPr
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ type FeatureLayerPopupProps = {
|
|||||||
zoom?: number
|
zoom?: number
|
||||||
};
|
};
|
||||||
|
|
||||||
export class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
|
export const FeatureLayerPopup: React.FC<FeatureLayerPopupProps> = (props) => {
|
||||||
_getFeatureColor(feature: InspectFeature, _zoom?: number) {
|
function _getFeatureColor(feature: InspectFeature, _zoom?: number) {
|
||||||
// Guard because openlayers won't have this
|
// Guard because openlayers won't have this
|
||||||
if (!feature.layer.paint) {
|
if (!feature.layer.paint) {
|
||||||
return;
|
return;
|
||||||
@@ -65,12 +65,11 @@ export class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
const sources = groupFeaturesBySourceLayer(props.features);
|
||||||
const sources = groupFeaturesBySourceLayer(this.props.features);
|
|
||||||
|
|
||||||
const items = Object.keys(sources).map(vectorLayerId => {
|
const items = Object.keys(sources).map(vectorLayerId => {
|
||||||
const layers = sources[vectorLayerId].map((feature: InspectFeature, idx: number) => {
|
const layers = sources[vectorLayerId].map((feature: InspectFeature, idx: number) => {
|
||||||
const featureColor = this._getFeatureColor(feature, this.props.zoom);
|
const featureColor = _getFeatureColor(feature, props.zoom);
|
||||||
|
|
||||||
return <div
|
return <div
|
||||||
key={idx}
|
key={idx}
|
||||||
@@ -83,7 +82,7 @@ export class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
|
|||||||
<label
|
<label
|
||||||
className="maputnik-popup-layer__label"
|
className="maputnik-popup-layer__label"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
this.props.onLayerSelect(feature.layer.id);
|
props.onLayerSelect(feature.layer.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{feature.layer.type &&
|
{feature.layer.type &&
|
||||||
@@ -107,5 +106,4 @@ export class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
|
|||||||
return <div className="maputnik-feature-layer-popup" data-wd-key="feature-layer-popup" dir="ltr">
|
return <div className="maputnik-feature-layer-popup" data-wd-key="feature-layer-popup" dir="ltr">
|
||||||
{items}
|
{items}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import {useCallback, useEffect, useMemo, useRef, useState} from "react";
|
||||||
import {throttle} from "lodash";
|
import {throttle} from "lodash";
|
||||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||||
|
|
||||||
@@ -35,56 +35,52 @@ type MapOpenLayersInternalProps = {
|
|||||||
onChange(...args: unknown[]): unknown
|
onChange(...args: unknown[]): unknown
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
type MapOpenLayersState = {
|
const MapOpenLayersInternal = ({
|
||||||
zoom: string
|
onLayerSelect = () => {},
|
||||||
rotation: string
|
mapStyle,
|
||||||
cursor: string[]
|
style,
|
||||||
center: string[]
|
debugToolbox,
|
||||||
selectedFeatures?: any[]
|
replaceAccessTokens,
|
||||||
};
|
onChange,
|
||||||
|
t,
|
||||||
|
}: MapOpenLayersInternalProps) => {
|
||||||
|
const [zoom, setZoom] = useState("0");
|
||||||
|
const [rotation, setRotation] = useState("0");
|
||||||
|
const [cursor, setCursor] = useState<string[]>([]);
|
||||||
|
const [center, setCenter] = useState<string[]>([]);
|
||||||
|
// Never assigned in the class version either, kept for the popup below.
|
||||||
|
const [selectedFeatures] = useState<any[] | undefined>(undefined);
|
||||||
|
|
||||||
class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps, MapOpenLayersState> {
|
// Imperative handles: they were plain instance fields, so they are refs and
|
||||||
static defaultProps = {
|
// mutating them must not re-render.
|
||||||
onMapLoaded: () => {},
|
const map = useRef<Map | null>(null);
|
||||||
onDataChange: () => {},
|
const container = useRef<HTMLDivElement | null>(null);
|
||||||
onLayerSelect: () => {},
|
const overlay = useRef<Overlay | undefined>(undefined);
|
||||||
};
|
const popupContainer = useRef<HTMLDivElement | null>(null);
|
||||||
updateStyle: any;
|
|
||||||
map: any;
|
|
||||||
container: HTMLDivElement | null = null;
|
|
||||||
overlay: Overlay | undefined;
|
|
||||||
popupContainer: HTMLElement | null = null;
|
|
||||||
|
|
||||||
constructor(props: MapOpenLayersInternalProps) {
|
// componentDidUpdate did not run on mount, and componentDidMount already
|
||||||
super(props);
|
// applies the initial style, so the style effect below must skip the mount.
|
||||||
this.state = {
|
const hasMounted = useRef(false);
|
||||||
zoom: "0",
|
|
||||||
rotation: "0",
|
|
||||||
cursor: [] as string[],
|
|
||||||
center: [],
|
|
||||||
};
|
|
||||||
this.updateStyle = throttle(this._updateStyle.bind(this), 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
_updateStyle(newMapStyle: StyleSpecification) {
|
// The openlayers event handlers are registered on mount but read `this.props`
|
||||||
if(!this.map) return;
|
// at call time in the class version.
|
||||||
|
const latestProps = useRef({mapStyle, replaceAccessTokens, onChange});
|
||||||
|
latestProps.current = {mapStyle, replaceAccessTokens, onChange};
|
||||||
|
|
||||||
|
// Was `this.updateStyle = throttle(this._updateStyle.bind(this), 200)` in the
|
||||||
|
// constructor: created once per instance, so it is memoized once here.
|
||||||
|
const updateStyle = useMemo(() => throttle((newMapStyle: StyleSpecification) => {
|
||||||
|
if(!map.current) return;
|
||||||
|
|
||||||
// See <https://github.com/openlayers/ol-mapbox-style/issues/215#issuecomment-493198815>
|
// See <https://github.com/openlayers/ol-mapbox-style/issues/215#issuecomment-493198815>
|
||||||
this.map.getLayers().clear();
|
map.current.getLayers().clear();
|
||||||
apply(this.map, newMapStyle);
|
apply(map.current, newMapStyle);
|
||||||
}
|
}, 200), []);
|
||||||
|
|
||||||
componentDidUpdate(prevProps: MapOpenLayersInternalProps) {
|
// componentDidMount
|
||||||
if (this.props.mapStyle !== prevProps.mapStyle) {
|
useEffect(() => {
|
||||||
this.updateStyle(
|
overlay.current = new Overlay({
|
||||||
this.props.replaceAccessTokens(this.props.mapStyle)
|
element: popupContainer.current!,
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.overlay = new Overlay({
|
|
||||||
element: this.popupContainer!,
|
|
||||||
autoPan: {
|
autoPan: {
|
||||||
animation: {
|
animation: {
|
||||||
duration: 250
|
duration: 250
|
||||||
@@ -92,116 +88,131 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const map = new Map({
|
const mapInstance = new Map({
|
||||||
target: this.container!,
|
target: container.current!,
|
||||||
overlays: [this.overlay],
|
overlays: [overlay.current],
|
||||||
view: new View({
|
view: new View({
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
center: [180, -90],
|
center: [180, -90],
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
map.on("pointermove", (evt) => {
|
mapInstance.on("pointermove", (evt) => {
|
||||||
const coords = toLonLat(evt.coordinate);
|
const coords = toLonLat(evt.coordinate);
|
||||||
this.setState({
|
setCursor([
|
||||||
cursor: [
|
|
||||||
coords[0].toFixed(2),
|
coords[0].toFixed(2),
|
||||||
coords[1].toFixed(2)
|
coords[1].toFixed(2)
|
||||||
]
|
]);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const onMoveEnd = () => {
|
const onMoveEnd = () => {
|
||||||
const zoom = map.getView().getZoom();
|
const currentZoom = mapInstance.getView().getZoom();
|
||||||
const center = toLonLat(map.getView().getCenter()!);
|
const currentCenter = toLonLat(mapInstance.getView().getCenter()!);
|
||||||
|
|
||||||
this.props.onChange({
|
latestProps.current.onChange({
|
||||||
zoom,
|
zoom: currentZoom,
|
||||||
center: {
|
center: {
|
||||||
lng: center[0],
|
lng: currentCenter[0],
|
||||||
lat: center[1],
|
lat: currentCenter[1],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onMoveEnd();
|
onMoveEnd();
|
||||||
map.on("moveend", onMoveEnd);
|
mapInstance.on("moveend", onMoveEnd);
|
||||||
|
|
||||||
map.on("postrender", (_e) => {
|
mapInstance.on("postrender", (_e) => {
|
||||||
const center = toLonLat(map.getView().getCenter()!);
|
const currentCenter = toLonLat(mapInstance.getView().getCenter()!);
|
||||||
this.setState({
|
setCenter([
|
||||||
center: [
|
currentCenter[0].toFixed(2),
|
||||||
center[0].toFixed(2),
|
currentCenter[1].toFixed(2),
|
||||||
center[1].toFixed(2),
|
]);
|
||||||
],
|
setRotation(mapInstance.getView().getRotation().toFixed(2));
|
||||||
rotation: map.getView().getRotation().toFixed(2),
|
setZoom(mapInstance.getView().getZoom()!.toFixed(2));
|
||||||
zoom: map.getView().getZoom()!.toFixed(2)
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.map = map;
|
map.current = mapInstance;
|
||||||
this.updateStyle(
|
updateStyle(
|
||||||
this.props.replaceAccessTokens(this.props.mapStyle)
|
latestProps.current.replaceAccessTokens(latestProps.current.mapStyle) as StyleSpecification
|
||||||
);
|
);
|
||||||
|
// Mount only, like componentDidMount. The class had no componentWillUnmount,
|
||||||
|
// so there is no teardown here either. The props read above are read from
|
||||||
|
// latestProps/the first render on purpose, so they are not dependencies.
|
||||||
|
}, [updateStyle]);
|
||||||
|
|
||||||
|
// componentDidUpdate: `if (this.props.mapStyle !== prevProps.mapStyle)`.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasMounted.current) {
|
||||||
|
// componentDidUpdate does not run on mount; componentDidMount already
|
||||||
|
// applied the initial style, and applying it twice would push a second
|
||||||
|
// (throttled) clear + apply through ol-mapbox-style.
|
||||||
|
hasMounted.current = true;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
updateStyle(
|
||||||
|
replaceAccessTokens(mapStyle) as StyleSpecification
|
||||||
|
);
|
||||||
|
// `replaceAccessTokens` is deliberately *not* a dependency: the parent
|
||||||
|
// re-creates it on every render, so depending on it would re-apply the style
|
||||||
|
// on every render instead of only when `mapStyle` changes, which is exactly
|
||||||
|
// what the `this.props.mapStyle !== prevProps.mapStyle` guard checked.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- see above: replaceAccessTokens is a new identity each render
|
||||||
|
}, [mapStyle, updateStyle]);
|
||||||
|
|
||||||
closeOverlay = (e: any) => {
|
const closeOverlay = useCallback((e: any) => {
|
||||||
e.target.blur();
|
e.target.blur();
|
||||||
this.overlay!.setPosition(undefined);
|
overlay.current!.setPosition(undefined);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
render() {
|
|
||||||
const t = this.props.t;
|
|
||||||
return <div className="maputnik-ol-container">
|
return <div className="maputnik-ol-container">
|
||||||
<div
|
<div
|
||||||
ref={x => {this.popupContainer = x;}}
|
ref={popupContainer}
|
||||||
style={{background: "black"}}
|
style={{background: "black"}}
|
||||||
className="maputnik-popup"
|
className="maputnik-popup"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="maplibregl-popup-close-button"
|
className="maplibregl-popup-close-button"
|
||||||
onClick={this.closeOverlay}
|
onClick={closeOverlay}
|
||||||
aria-label={t("Close popup")}
|
aria-label={t("Close popup")}
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
<MapMaplibreGlLayerPopup
|
<MapMaplibreGlLayerPopup
|
||||||
features={this.state.selectedFeatures || []}
|
features={selectedFeatures || []}
|
||||||
onLayerSelect={this.props.onLayerSelect}
|
onLayerSelect={onLayerSelect}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="maputnik-ol-zoom">
|
<div className="maputnik-ol-zoom">
|
||||||
{t("Zoom:")} {this.state.zoom}
|
{t("Zoom:")} {zoom}
|
||||||
</div>
|
</div>
|
||||||
{this.props.debugToolbox &&
|
{debugToolbox &&
|
||||||
<div className="maputnik-ol-debug">
|
<div className="maputnik-ol-debug">
|
||||||
<div>
|
<div>
|
||||||
<label>{t("cursor:")} </label>
|
<label>{t("cursor:")} </label>
|
||||||
<span>{renderCoords(this.state.cursor)}</span>
|
<span>{renderCoords(cursor)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>{t("center:")} </label>
|
<label>{t("center:")} </label>
|
||||||
<span>{renderCoords(this.state.center)}</span>
|
<span>{renderCoords(center)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>{t("rotation:")} </label>
|
<label>{t("rotation:")} </label>
|
||||||
<span>{this.state.rotation}</span>
|
<span>{rotation}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div
|
<div
|
||||||
className="maputnik-ol"
|
className="maputnik-ol"
|
||||||
ref={x => {this.container = x;}}
|
ref={container}
|
||||||
role="region"
|
role="region"
|
||||||
aria-label={t("Map view")}
|
aria-label={t("Map view")}
|
||||||
style={{
|
style={{
|
||||||
...this.props.style,
|
...style,
|
||||||
}}>
|
}}>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const MapOpenLayers = withTranslation()(MapOpenLayersInternal);
|
export const MapOpenLayers = withTranslation()(MapOpenLayersInternal);
|
||||||
|
|||||||
@@ -40,19 +40,18 @@ type PropertyGroupProps = {
|
|||||||
errors?: MappedLayerErrors
|
errors?: MappedLayerErrors
|
||||||
};
|
};
|
||||||
|
|
||||||
export class PropertyGroup extends React.Component<PropertyGroupProps> {
|
export const PropertyGroup: React.FC<PropertyGroupProps> = (props) => {
|
||||||
onPropertyChange = (property: string, newValue: any) => {
|
const onPropertyChange = (property: string, newValue: any) => {
|
||||||
const group = getGroupName(this.props.spec, this.props.layer.type, property);
|
const group = getGroupName(props.spec, props.layer.type, property);
|
||||||
this.props.onChange(group ,property, newValue);
|
props.onChange(group ,property, newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const {errors} = props;
|
||||||
const {errors} = this.props;
|
const fields = props.groupFields.map(fieldName => {
|
||||||
const fields = this.props.groupFields.map(fieldName => {
|
const fieldSpec = getFieldSpec(props.spec, props.layer.type, fieldName);
|
||||||
const fieldSpec = getFieldSpec(this.props.spec, this.props.layer.type, fieldName);
|
|
||||||
|
|
||||||
const paint = this.props.layer.paint || {};
|
const paint = props.layer.paint || {};
|
||||||
const layout = this.props.layer.layout || {};
|
const layout = props.layer.layout || {};
|
||||||
const fieldValue = fieldName in paint
|
const fieldValue = fieldName in paint
|
||||||
? paint[fieldName as keyof typeof paint]
|
? paint[fieldName as keyof typeof paint]
|
||||||
: layout[fieldName as keyof typeof layout];
|
: layout[fieldName as keyof typeof layout];
|
||||||
@@ -60,7 +59,7 @@ export class PropertyGroup extends React.Component<PropertyGroupProps> {
|
|||||||
|
|
||||||
return <FieldFunction
|
return <FieldFunction
|
||||||
errors={errors}
|
errors={errors}
|
||||||
onChange={this.onPropertyChange}
|
onChange={onPropertyChange}
|
||||||
key={fieldName}
|
key={fieldName}
|
||||||
fieldName={fieldName}
|
fieldName={fieldName}
|
||||||
value={fieldValue}
|
value={fieldValue}
|
||||||
@@ -72,5 +71,4 @@ export class PropertyGroup extends React.Component<PropertyGroupProps> {
|
|||||||
return <div className="maputnik-property-group">
|
return <div className="maputnik-property-group">
|
||||||
{fields}
|
{fields}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ type ScrollContainerProps = {
|
|||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ScrollContainer extends React.Component<ScrollContainerProps> {
|
export const ScrollContainer: React.FC<ScrollContainerProps> = (props) => {
|
||||||
render() {
|
|
||||||
return <div className="maputnik-scroll-container">
|
return <div className="maputnik-scroll-container">
|
||||||
{this.props.children}
|
{props.children}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -40,23 +40,21 @@ type SingleFilterEditorProps = {
|
|||||||
properties?: {[key: string]: string}
|
properties?: {[key: string]: string}
|
||||||
};
|
};
|
||||||
|
|
||||||
export class SingleFilterEditor extends React.Component<SingleFilterEditorProps> {
|
export const SingleFilterEditor: React.FC<SingleFilterEditorProps> = ({
|
||||||
static defaultProps = {
|
properties = {},
|
||||||
properties: {},
|
...props
|
||||||
};
|
}) => {
|
||||||
|
function onFilterPartChanged(filterOp: string, propertyName: string, filterArgs: string[]) {
|
||||||
onFilterPartChanged(filterOp: string, propertyName: string, filterArgs: string[]) {
|
|
||||||
let newFilter = [filterOp, propertyName, ...filterArgs.map(parseFilter)];
|
let newFilter = [filterOp, propertyName, ...filterArgs.map(parseFilter)];
|
||||||
if(filterOp === "has" || filterOp === "!has") {
|
if(filterOp === "has" || filterOp === "!has") {
|
||||||
newFilter = [filterOp, propertyName];
|
newFilter = [filterOp, propertyName];
|
||||||
} else if(filterArgs.length === 0) {
|
} else if(filterArgs.length === 0) {
|
||||||
newFilter = [filterOp, propertyName, ""];
|
newFilter = [filterOp, propertyName, ""];
|
||||||
}
|
}
|
||||||
this.props.onChange(newFilter);
|
props.onChange(newFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
const f = props.filter;
|
||||||
const f = this.props.filter;
|
|
||||||
const filterOp = f[0];
|
const filterOp = f[0];
|
||||||
const propertyName = f[1];
|
const propertyName = f[1];
|
||||||
const filterArgs = f.slice(2);
|
const filterArgs = f.slice(2);
|
||||||
@@ -66,15 +64,15 @@ export class SingleFilterEditor extends React.Component<SingleFilterEditorProps>
|
|||||||
<InputAutocomplete
|
<InputAutocomplete
|
||||||
aria-label="key"
|
aria-label="key"
|
||||||
value={propertyName}
|
value={propertyName}
|
||||||
options={Object.keys(this.props.properties!).map(propName => [propName, propName])}
|
options={Object.keys(properties).map(propName => [propName, propName])}
|
||||||
onChange={(newPropertyName: string) => this.onFilterPartChanged(filterOp, newPropertyName, filterArgs)}
|
onChange={(newPropertyName: string) => onFilterPartChanged(filterOp, newPropertyName, filterArgs)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="maputnik-filter-editor-operator">
|
<div className="maputnik-filter-editor-operator">
|
||||||
<InputSelect
|
<InputSelect
|
||||||
aria-label="function"
|
aria-label="function"
|
||||||
value={filterOp}
|
value={filterOp}
|
||||||
onChange={(newFilterOp: string) => this.onFilterPartChanged(newFilterOp, propertyName, filterArgs)}
|
onChange={(newFilterOp: string) => onFilterPartChanged(newFilterOp, propertyName, filterArgs)}
|
||||||
options={otherFilterOps}
|
options={otherFilterOps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,10 +81,9 @@ export class SingleFilterEditor extends React.Component<SingleFilterEditorProps>
|
|||||||
<InputString
|
<InputString
|
||||||
aria-label="value"
|
aria-label="value"
|
||||||
value={filterArgs.join(",")}
|
value={filterArgs.join(",")}
|
||||||
onChange={(v: string) => this.onFilterPartChanged(filterOp, propertyName, v.split(","))}
|
onChange={(v: string) => onFilterPartChanged(filterOp, propertyName, v.split(","))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,15 +7,13 @@ type SmallErrorInternalProps = {
|
|||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class SmallErrorInternal extends React.Component<SmallErrorInternalProps> {
|
const SmallErrorInternal: React.FC<SmallErrorInternalProps> = (props) => {
|
||||||
render () {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
|
||||||
return (
|
return (
|
||||||
<div className="SmallError">
|
<div className="SmallError">
|
||||||
{t("Error:")} {this.props.children}
|
{t("Error:")} {props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const SmallError = withTranslation()(SmallErrorInternal);
|
export const SmallError = withTranslation()(SmallErrorInternal);
|
||||||
|
|||||||
@@ -19,30 +19,26 @@ type SpecPropertyProps = FieldSpecProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export class SpecProperty extends React.Component<SpecPropertyProps> {
|
export const SpecProperty: React.FC<SpecPropertyProps> = (props) => {
|
||||||
static defaultProps = {
|
const {errors = {}, fieldName, fieldType} = props;
|
||||||
errors: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {errors, fieldName, fieldType} = this.props;
|
|
||||||
|
|
||||||
const functionBtn = <FunctionButtons
|
const functionBtn = <FunctionButtons
|
||||||
fieldSpec={this.props.fieldSpec}
|
fieldSpec={props.fieldSpec}
|
||||||
onZoomClick={this.props.onZoomClick}
|
onZoomClick={props.onZoomClick}
|
||||||
onDataClick={this.props.onDataClick}
|
onDataClick={props.onDataClick}
|
||||||
onExpressionClick={this.props.onExpressionClick}
|
onExpressionClick={props.onExpressionClick}
|
||||||
onElevationClick={this.props.onElevationClick}
|
onElevationClick={props.onElevationClick}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
const error = errors![fieldType+"."+fieldName as any] as any;
|
const error = errors[fieldType+"."+fieldName as any] as any;
|
||||||
|
|
||||||
|
const propsWithDefaults = {...props, errors};
|
||||||
|
|
||||||
return <FieldSpec
|
return <FieldSpec
|
||||||
{...this.props}
|
{...propsWithDefaults}
|
||||||
error={error}
|
error={error}
|
||||||
fieldSpec={this.props.fieldSpec}
|
fieldSpec={props.fieldSpec}
|
||||||
label={labelFromFieldName(this.props.fieldName || "")}
|
label={labelFromFieldName(props.fieldName || "")}
|
||||||
action={functionBtn}
|
action={functionBtn}
|
||||||
/>;
|
/>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useRef } from "react";
|
||||||
import { PiListPlusBold } from "react-icons/pi";
|
import { PiListPlusBold } from "react-icons/pi";
|
||||||
import { TbMathFunction } from "react-icons/tb";
|
import { TbMathFunction } from "react-icons/tb";
|
||||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
||||||
@@ -68,41 +68,21 @@ type ZoomPropertyState = {
|
|||||||
refs: {[key: number]: string}
|
refs: {[key: number]: string}
|
||||||
};
|
};
|
||||||
|
|
||||||
class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, ZoomPropertyState> {
|
const ZoomPropertyInternal: React.FC<ZoomPropertyInternalProps> = ({ errors = {}, ...rest }) => {
|
||||||
static defaultProps = {
|
const props = { errors, ...rest } as ZoomPropertyInternalProps;
|
||||||
errors: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
// The stop refs never reach the rendered output (the row key is derived from
|
||||||
refs: {} as {[key: number]: string}
|
// the stop itself), so they live in a ref rather than state: keeping them in
|
||||||
};
|
// state would mean setting state during render on every pass.
|
||||||
|
const refs = useRef<{[key: number]: string}>({});
|
||||||
componentDidMount() {
|
refs.current = setStopRefs(props, { refs: refs.current });
|
||||||
const newRefs = setStopRefs(this.props, this.state);
|
|
||||||
|
|
||||||
if(newRefs) {
|
|
||||||
this.setState({
|
|
||||||
refs: newRefs
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static getDerivedStateFromProps(props: Readonly<ZoomPropertyInternalProps>, state: ZoomPropertyState) {
|
|
||||||
const newRefs = setStopRefs(props, state);
|
|
||||||
if(newRefs) {
|
|
||||||
return {
|
|
||||||
refs: newRefs
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Order the stops altering the refs to reflect their new position.
|
// Order the stops altering the refs to reflect their new position.
|
||||||
orderStopsByZoom(stops: ZoomWithStops["stops"]) {
|
function orderStopsByZoom(stops: ZoomWithStops["stops"]) {
|
||||||
const mappedWithRef = stops
|
const mappedWithRef = stops
|
||||||
.map((stop, idx) => {
|
.map((stop, idx) => {
|
||||||
return {
|
return {
|
||||||
ref: this.state.refs[idx],
|
ref: refs.current[idx],
|
||||||
data: stop
|
data: stop
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
@@ -116,50 +96,59 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
|
|||||||
newRefs[idx] = stop.ref;
|
newRefs[idx] = stop.ref;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setState({
|
refs.current = newRefs;
|
||||||
refs: newRefs
|
|
||||||
});
|
|
||||||
|
|
||||||
return mappedWithRef.map((item) => item.data);
|
return mappedWithRef.map((item) => item.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
changeZoomStop(changeIdx: number, stopData: number | undefined, value: number) {
|
function changeZoomStop(changeIdx: number, stopData: number | undefined, value: number) {
|
||||||
const stops = (this.props.value as ZoomWithStops).stops.slice(0);
|
const stops = (props.value as ZoomWithStops).stops.slice(0);
|
||||||
stops[changeIdx] = [stopData, value];
|
stops[changeIdx] = [stopData, value];
|
||||||
|
|
||||||
const orderedStops = this.orderStopsByZoom(stops);
|
const orderedStops = orderStopsByZoom(stops);
|
||||||
|
|
||||||
const changedValue = {
|
const changedValue = {
|
||||||
...this.props.value as ZoomWithStops,
|
...props.value as ZoomWithStops,
|
||||||
stops: orderedStops
|
stops: orderedStops
|
||||||
};
|
};
|
||||||
this.props.onChange!(this.props.fieldName, changedValue);
|
props.onChange!(props.fieldName, changedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
changeBase(newValue: number | undefined) {
|
function changeBase(newValue: number | undefined) {
|
||||||
const changedValue = {
|
const changedValue = {
|
||||||
...this.props.value,
|
...props.value,
|
||||||
base: newValue
|
base: newValue
|
||||||
};
|
};
|
||||||
|
|
||||||
if (changedValue.base === undefined) {
|
if (changedValue.base === undefined) {
|
||||||
delete changedValue["base"];
|
delete changedValue["base"];
|
||||||
}
|
}
|
||||||
this.props.onChange!(this.props.fieldName, changedValue);
|
props.onChange!(props.fieldName, changedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
changeDataType = (type: string) => {
|
const changeDataType = (type: string) => {
|
||||||
if (type !== "interpolate" && this.props.onChangeToDataFunction) {
|
if (type !== "interpolate" && props.onChangeToDataFunction) {
|
||||||
this.props.onChangeToDataFunction(type);
|
props.onChangeToDataFunction(type);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
function getDataFunctionTypes(fieldSpec: {
|
||||||
const t = this.props.t;
|
"property-type"?: string
|
||||||
const zoomFields = this.props.value?.stops.map((stop, idx) => {
|
"function-type"?: string
|
||||||
|
}) {
|
||||||
|
if (fieldSpec["property-type"] === "data-driven") {
|
||||||
|
return ["interpolate", "categorical", "interval", "exponential", "identity"];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return ["interpolate"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = props.t;
|
||||||
|
const zoomFields = props.value?.stops.map((stop, idx) => {
|
||||||
const zoomLevel = stop[0];
|
const zoomLevel = stop[0];
|
||||||
const value = stop[1];
|
const value = stop[1];
|
||||||
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />;
|
const deleteStopBtn = <DeleteStopButton onClick={props.onDeleteStop?.bind(null, idx)} />;
|
||||||
return <tr
|
return <tr
|
||||||
key={`${stop[0]}-${stop[1]}`}
|
key={`${stop[0]}-${stop[1]}`}
|
||||||
>
|
>
|
||||||
@@ -167,7 +156,7 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
|
|||||||
<InputNumber
|
<InputNumber
|
||||||
aria-label={t("Zoom")}
|
aria-label={t("Zoom")}
|
||||||
value={zoomLevel}
|
value={zoomLevel}
|
||||||
onChange={changedStop => this.changeZoomStop(idx, changedStop, value)}
|
onChange={changedStop => changeZoomStop(idx, changedStop, value)}
|
||||||
min={0}
|
min={0}
|
||||||
max={22}
|
max={22}
|
||||||
/>
|
/>
|
||||||
@@ -175,10 +164,10 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
|
|||||||
<td>
|
<td>
|
||||||
<InputSpec
|
<InputSpec
|
||||||
aria-label={t("Output value")}
|
aria-label={t("Output value")}
|
||||||
fieldName={this.props.fieldName}
|
fieldName={props.fieldName}
|
||||||
fieldSpec={this.props.fieldSpec as any}
|
fieldSpec={props.fieldSpec as any}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(_, newValue) => this.changeZoomStop(idx, zoomLevel, newValue as number)}
|
onChange={(_, newValue) => changeZoomStop(idx, zoomLevel, newValue as number)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -190,7 +179,7 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
|
|||||||
// return <div className="maputnik-zoom-spec-property">
|
// return <div className="maputnik-zoom-spec-property">
|
||||||
return <div className="maputnik-data-spec-block">
|
return <div className="maputnik-data-spec-block">
|
||||||
<fieldset className="maputnik-data-spec-property">
|
<fieldset className="maputnik-data-spec-property">
|
||||||
<legend>{labelFromFieldName(this.props.fieldName)}</legend>
|
<legend>{labelFromFieldName(props.fieldName)}</legend>
|
||||||
<div className="maputnik-data-fieldset-inner">
|
<div className="maputnik-data-fieldset-inner">
|
||||||
<Block
|
<Block
|
||||||
label={t("Function")}
|
label={t("Function")}
|
||||||
@@ -199,9 +188,9 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
|
|||||||
<div className="maputnik-data-spec-property-input">
|
<div className="maputnik-data-spec-property-input">
|
||||||
<InputSelect
|
<InputSelect
|
||||||
value={"interpolate"}
|
value={"interpolate"}
|
||||||
onChange={(propVal: string) => this.changeDataType(propVal)}
|
onChange={(propVal: string) => changeDataType(propVal)}
|
||||||
title={t("Select a type of data scale (default is 'categorical').")}
|
title={t("Select a type of data scale (default is 'categorical').")}
|
||||||
options={this.getDataFunctionTypes(this.props.fieldSpec!)}
|
options={getDataFunctionTypes(props.fieldSpec!)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Block>
|
</Block>
|
||||||
@@ -213,8 +202,8 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
|
|||||||
<InputSpec
|
<InputSpec
|
||||||
fieldName={"base"}
|
fieldName={"base"}
|
||||||
fieldSpec={latest.function.base as typeof latest.function.base & { type: "number" }}
|
fieldSpec={latest.function.base as typeof latest.function.base & { type: "number" }}
|
||||||
value={this.props.value?.base}
|
value={props.value?.base}
|
||||||
onChange={(_, newValue) => this.changeBase(newValue as number | undefined)}
|
onChange={(_, newValue) => changeBase(newValue as number | undefined)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Block>
|
</Block>
|
||||||
@@ -235,7 +224,7 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
|
|||||||
<div className="maputnik-toolbox">
|
<div className="maputnik-toolbox">
|
||||||
<InputButton
|
<InputButton
|
||||||
className="maputnik-add-stop"
|
className="maputnik-add-stop"
|
||||||
onClick={this.props.onAddStop?.bind(this)}
|
onClick={props.onAddStop?.bind(null)}
|
||||||
>
|
>
|
||||||
<PiListPlusBold style={{ verticalAlign: "text-bottom" }} />
|
<PiListPlusBold style={{ verticalAlign: "text-bottom" }} />
|
||||||
{t("Add stop")}
|
{t("Add stop")}
|
||||||
@@ -243,7 +232,7 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
|
|||||||
<InputButton
|
<InputButton
|
||||||
className="maputnik-add-stop"
|
className="maputnik-add-stop"
|
||||||
data-wd-key="convert-to-expression"
|
data-wd-key="convert-to-expression"
|
||||||
onClick={this.props.onExpressionClick?.bind(this)}
|
onClick={props.onExpressionClick?.bind(null)}
|
||||||
>
|
>
|
||||||
<TbMathFunction style={{ verticalAlign: "text-bottom" }} />
|
<TbMathFunction style={{ verticalAlign: "text-bottom" }} />
|
||||||
{t("Convert to expression")}
|
{t("Convert to expression")}
|
||||||
@@ -252,19 +241,6 @@ class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, Zo
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
|
|
||||||
getDataFunctionTypes(fieldSpec: {
|
|
||||||
"property-type"?: string
|
|
||||||
"function-type"?: string
|
|
||||||
}) {
|
|
||||||
if (fieldSpec["property-type"] === "data-driven") {
|
|
||||||
return ["interpolate", "categorical", "interval", "exponential", "identity"];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return ["interpolate"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ZoomProperty = withTranslation()(ZoomPropertyInternal);
|
export const ZoomProperty = withTranslation()(ZoomPropertyInternal);
|
||||||
|
|||||||
@@ -14,49 +14,44 @@ type ModalInternalProps = PropsWithChildren & {
|
|||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
|
|
||||||
class ModalInternal extends React.Component<ModalInternalProps> {
|
const ModalInternal: React.FC<ModalInternalProps> = ({underlayClickExits = true, ...props}) => {
|
||||||
static defaultProps = {
|
|
||||||
underlayClickExits: true
|
|
||||||
};
|
|
||||||
|
|
||||||
// See <https://github.com/maplibre/maputnik/issues/416>
|
// See <https://github.com/maplibre/maputnik/issues/416>
|
||||||
onClose = () => {
|
const onClose = () => {
|
||||||
if (document.activeElement) {
|
if (document.activeElement) {
|
||||||
(document.activeElement as HTMLElement).blur();
|
(document.activeElement as HTMLElement).blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.props.onOpenToggle();
|
props.onOpenToggle();
|
||||||
}, 0);
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
if(props.isOpen) {
|
||||||
if(this.props.isOpen) {
|
|
||||||
return <AriaModal
|
return <AriaModal
|
||||||
titleText={this.props.title}
|
titleText={props.title}
|
||||||
underlayClickExits={this.props.underlayClickExits}
|
underlayClickExits={underlayClickExits}
|
||||||
data-wd-key={this.props["data-wd-key"]}
|
data-wd-key={props["data-wd-key"]}
|
||||||
verticallyCenter={true}
|
verticallyCenter={true}
|
||||||
onExit={this.onClose}
|
onExit={onClose}
|
||||||
dialogClass='maputnik-modal-container'
|
dialogClass='maputnik-modal-container'
|
||||||
>
|
>
|
||||||
<div className={classnames("maputnik-modal", this.props.className)}
|
<div className={classnames("maputnik-modal", props.className)}
|
||||||
data-wd-key={this.props["data-wd-key"]}
|
data-wd-key={props["data-wd-key"]}
|
||||||
>
|
>
|
||||||
<header className="maputnik-modal-header">
|
<header className="maputnik-modal-header">
|
||||||
<h1 className="maputnik-modal-header-title">{this.props.title}</h1>
|
<h1 className="maputnik-modal-header-title">{props.title}</h1>
|
||||||
<span className="maputnik-space"></span>
|
<span className="maputnik-space"></span>
|
||||||
<button className="maputnik-modal-header-toggle"
|
<button className="maputnik-modal-header-toggle"
|
||||||
title={t("Close modal")}
|
title={t("Close modal")}
|
||||||
onClick={this.onClose}
|
onClick={onClose}
|
||||||
data-wd-key={this.props["data-wd-key"]+".close-modal"}
|
data-wd-key={props["data-wd-key"]+".close-modal"}
|
||||||
>
|
>
|
||||||
<MdClose />
|
<MdClose />
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<div className="maputnik-modal-scroller">
|
<div className="maputnik-modal-scroller">
|
||||||
<div className="maputnik-modal-content">{this.props.children}</div>
|
<div className="maputnik-modal-content">{props.children}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AriaModal>;
|
</AriaModal>;
|
||||||
@@ -64,7 +59,6 @@ class ModalInternal extends React.Component<ModalInternalProps> {
|
|||||||
else {
|
else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const Modal = withTranslation()(ModalInternal);
|
export const Modal = withTranslation()(ModalInternal);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||||
import type {LayerSpecification, SourceSpecification} from "maplibre-gl";
|
import type {LayerSpecification, SourceSpecification} from "maplibre-gl";
|
||||||
|
|
||||||
@@ -27,115 +27,112 @@ type ModalAddState = {
|
|||||||
error?: string | null
|
error?: string | null
|
||||||
};
|
};
|
||||||
|
|
||||||
class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddState> {
|
type ModalAddSources = ModalAddInternalProps["sources"];
|
||||||
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);
|
function getLayersForSource(allSources: ModalAddSources, source: string) {
|
||||||
const layer: ModalAddState = {
|
const sourceObj = allSources[source] || {};
|
||||||
id: this.state.id,
|
|
||||||
type: this.state.type,
|
|
||||||
};
|
|
||||||
|
|
||||||
if(this.state.type !== "background") {
|
|
||||||
layer.source = this.state.source;
|
|
||||||
if(!NON_SOURCE_LAYERS.includes(this.state.type) && this.state["source-layer"]) {
|
|
||||||
layer["source-layer"] = this.state["source-layer"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
changedLayers.push(layer as LayerSpecification);
|
|
||||||
this.setState({ error: null }, () => {
|
|
||||||
this.props.onLayersChange(changedLayers);
|
|
||||||
this.props.onOpenToggle();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props: ModalAddInternalProps) {
|
|
||||||
super(props);
|
|
||||||
const state: ModalAddState = {
|
|
||||||
type: "fill",
|
|
||||||
id: "",
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
if(Object.keys(props.sources).length > 0) {
|
|
||||||
state.source = Object.keys(this.props.sources)[0];
|
|
||||||
const sourceLayers = this.props.sources[state.source].layers || [];
|
|
||||||
if (sourceLayers.length > 0) {
|
|
||||||
state["source-layer"] = sourceLayers[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.state = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(_prevProps: ModalAddInternalProps, prevState: ModalAddState) {
|
|
||||||
// Check if source is valid for new type
|
|
||||||
const oldType = prevState.type;
|
|
||||||
const newType = this.state.type;
|
|
||||||
|
|
||||||
const availableSourcesOld = this.getSources(oldType);
|
|
||||||
const availableSourcesNew = this.getSources(newType);
|
|
||||||
|
|
||||||
if(
|
|
||||||
// Type has changed
|
|
||||||
oldType !== newType
|
|
||||||
&& prevState.source !== ""
|
|
||||||
// Was a valid source previously
|
|
||||||
&& availableSourcesOld.indexOf(prevState.source!) > -1
|
|
||||||
// And is not a valid source now
|
|
||||||
&& availableSourcesNew.indexOf(this.state.source!) < 0
|
|
||||||
) {
|
|
||||||
// Clear the source
|
|
||||||
this.setState({
|
|
||||||
source: ""
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getLayersForSource(source: string) {
|
|
||||||
const sourceObj = this.props.sources[source] || {};
|
|
||||||
return sourceObj.layers || [];
|
return sourceObj.layers || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getSources(type: LayerSpecification["type"]) {
|
function getSources(allSources: ModalAddSources, type: LayerSpecification["type"]) {
|
||||||
|
|
||||||
switch(type) {
|
switch(type) {
|
||||||
case "background":
|
case "background":
|
||||||
return [];
|
return [];
|
||||||
case "hillshade":
|
case "hillshade":
|
||||||
case "color-relief":
|
case "color-relief":
|
||||||
return Object.entries(this.props.sources).filter(([_, v]) => v.type === "raster-dem").map(([k, _]) => k);
|
return Object.entries(allSources).filter(([_, v]) => v.type === "raster-dem").map(([k, _]) => k);
|
||||||
case "raster":
|
case "raster":
|
||||||
return Object.entries(this.props.sources).filter(([_, v]) => v.type === "raster").map(([k, _]) => k);
|
return Object.entries(allSources).filter(([_, v]) => v.type === "raster").map(([k, _]) => k);
|
||||||
case "heatmap":
|
case "heatmap":
|
||||||
case "circle":
|
case "circle":
|
||||||
case "fill":
|
case "fill":
|
||||||
case "fill-extrusion":
|
case "fill-extrusion":
|
||||||
case "line":
|
case "line":
|
||||||
case "symbol":
|
case "symbol":
|
||||||
return Object.entries(this.props.sources).filter(([_, v]) => v.type === "vector" || v.type === "geojson").map(([k, _]) => k);
|
return Object.entries(allSources).filter(([_, v]) => v.type === "vector" || v.type === "geojson").map(([k, _]) => k);
|
||||||
default:
|
default:
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ModalAddInternal: React.FC<ModalAddInternalProps> = (props) => {
|
||||||
|
const [type, setType] = useState<LayerSpecification["type"]>("fill");
|
||||||
|
const [id, setId] = useState<string>("");
|
||||||
|
const [error, setError] = useState<string | null | undefined>(null);
|
||||||
|
const [source, setSource] = useState<string | undefined>(() => {
|
||||||
|
const sourceIds = Object.keys(props.sources);
|
||||||
|
return sourceIds.length > 0 ? sourceIds[0] : undefined;
|
||||||
|
});
|
||||||
|
const [sourceLayer, setSourceLayer] = useState<string | undefined>(() => {
|
||||||
|
const sourceIds = Object.keys(props.sources);
|
||||||
|
if (sourceIds.length === 0) return undefined;
|
||||||
|
const sourceLayers = props.sources[sourceIds[0]].layers || [];
|
||||||
|
return sourceLayers.length > 0 ? sourceLayers[0] : undefined;
|
||||||
|
});
|
||||||
|
|
||||||
render() {
|
// Mirrors `prevState.type` from the previous `componentDidUpdate`.
|
||||||
const t = this.props.t;
|
const prevType = useRef<LayerSpecification["type"]>(type);
|
||||||
const sources = this.getSources(this.state.type);
|
|
||||||
const layers = this.getLayersForSource(this.state.source!);
|
useEffect(() => {
|
||||||
|
// Check if source is valid for new type
|
||||||
|
const oldType = prevType.current;
|
||||||
|
prevType.current = type;
|
||||||
|
|
||||||
|
// Only when the type has changed (also skips the initial mount)
|
||||||
|
if (oldType === type) return;
|
||||||
|
|
||||||
|
const availableSourcesOld = getSources(props.sources, oldType);
|
||||||
|
const availableSourcesNew = getSources(props.sources, type);
|
||||||
|
|
||||||
|
if(
|
||||||
|
source !== ""
|
||||||
|
// Was a valid source previously
|
||||||
|
&& availableSourcesOld.indexOf(source!) > -1
|
||||||
|
// And is not a valid source now
|
||||||
|
&& availableSourcesNew.indexOf(source!) < 0
|
||||||
|
) {
|
||||||
|
// Clear the source
|
||||||
|
setSource("");
|
||||||
|
}
|
||||||
|
}, [type, source, props.sources]);
|
||||||
|
|
||||||
|
const addLayer = () => {
|
||||||
|
if (props.layers.some(l => l.id === id)) {
|
||||||
|
setError(props.t("Layer ID already exists"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const changedLayers = props.layers.slice(0);
|
||||||
|
const layer: ModalAddState = {
|
||||||
|
id: id,
|
||||||
|
type: type,
|
||||||
|
};
|
||||||
|
|
||||||
|
if(type !== "background") {
|
||||||
|
layer.source = source;
|
||||||
|
if(!NON_SOURCE_LAYERS.includes(type) && sourceLayer) {
|
||||||
|
layer["source-layer"] = sourceLayer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changedLayers.push(layer as LayerSpecification);
|
||||||
|
setError(null);
|
||||||
|
props.onLayersChange(changedLayers);
|
||||||
|
props.onOpenToggle();
|
||||||
|
};
|
||||||
|
|
||||||
|
const t = props.t;
|
||||||
|
const sources = getSources(props.sources, type);
|
||||||
|
const layers = getLayersForSource(props.sources, source!);
|
||||||
let errorElement;
|
let errorElement;
|
||||||
if (this.state.error) {
|
if (error) {
|
||||||
errorElement = (
|
errorElement = (
|
||||||
<div className="maputnik-modal-error">
|
<div className="maputnik-modal-error">
|
||||||
{this.state.error}
|
{error}
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
onClick={() => this.setState({ error: null })}
|
onClick={() => setError(null)}
|
||||||
className="maputnik-modal-error-close"
|
className="maputnik-modal-error-close"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
@@ -145,8 +142,8 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <Modal
|
return <Modal
|
||||||
isOpen={this.props.isOpen}
|
isOpen={props.isOpen}
|
||||||
onOpenToggle={this.props.onOpenToggle}
|
onOpenToggle={props.onOpenToggle}
|
||||||
title={t("Add Layer")}
|
title={t("Add Layer")}
|
||||||
data-wd-key="modal:add-layer"
|
data-wd-key="modal:add-layer"
|
||||||
className="maputnik-add-modal"
|
className="maputnik-add-modal"
|
||||||
@@ -154,42 +151,42 @@ class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddSt
|
|||||||
{errorElement}
|
{errorElement}
|
||||||
<div className="maputnik-add-layer">
|
<div className="maputnik-add-layer">
|
||||||
<FieldId
|
<FieldId
|
||||||
value={this.state.id}
|
value={id}
|
||||||
wdKey="add-layer.layer-id"
|
wdKey="add-layer.layer-id"
|
||||||
onChange={(v: string) => {
|
onChange={(v: string) => {
|
||||||
this.setState({ id: v, error: null });
|
setId(v);
|
||||||
|
setError(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FieldType
|
<FieldType
|
||||||
value={this.state.type}
|
value={type}
|
||||||
wdKey="add-layer.layer-type"
|
wdKey="add-layer.layer-type"
|
||||||
onChange={(v: LayerSpecification["type"]) => this.setState({ type: v })}
|
onChange={(v: LayerSpecification["type"]) => setType(v)}
|
||||||
/>
|
/>
|
||||||
{this.state.type !== "background" &&
|
{type !== "background" &&
|
||||||
<FieldSource
|
<FieldSource
|
||||||
sourceIds={sources}
|
sourceIds={sources}
|
||||||
wdKey="add-layer.layer-source-block"
|
wdKey="add-layer.layer-source-block"
|
||||||
value={this.state.source}
|
value={source}
|
||||||
onChange={(v: string) => this.setState({ source: v })}
|
onChange={(v: string) => setSource(v)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{!NON_SOURCE_LAYERS.includes(this.state.type) &&
|
{!NON_SOURCE_LAYERS.includes(type) &&
|
||||||
<FieldSourceLayer
|
<FieldSourceLayer
|
||||||
sourceLayerIds={layers}
|
sourceLayerIds={layers}
|
||||||
value={this.state["source-layer"]}
|
value={sourceLayer}
|
||||||
onChange={(v: string) => this.setState({ "source-layer": v })}
|
onChange={(v: string) => setSourceLayer(v)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<InputButton
|
<InputButton
|
||||||
className="maputnik-add-layer-button"
|
className="maputnik-add-layer-button"
|
||||||
onClick={this.addLayer}
|
onClick={addLayer}
|
||||||
data-wd-key="add-layer"
|
data-wd-key="add-layer"
|
||||||
>
|
>
|
||||||
{t("Add Layer")}
|
{t("Add Layer")}
|
||||||
</InputButton>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
</Modal>;
|
</Modal>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const ModalAdd = withTranslation()(ModalAddInternal);
|
export const ModalAdd = withTranslation()(ModalAddInternal);
|
||||||
|
|||||||
@@ -22,9 +22,8 @@ type ModalDebugInternalProps = {
|
|||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
|
|
||||||
class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
|
const ModalDebugInternal: React.FC<ModalDebugInternalProps> = (props) => {
|
||||||
render() {
|
const {t, mapView} = props;
|
||||||
const {t, mapView} = this.props;
|
|
||||||
|
|
||||||
const osmZoom = Math.round(mapView.zoom)+1;
|
const osmZoom = Math.round(mapView.zoom)+1;
|
||||||
const osmLon = +(mapView.center.lng).toFixed(5);
|
const osmLon = +(mapView.center.lng).toFixed(5);
|
||||||
@@ -32,29 +31,29 @@ class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
|
|||||||
|
|
||||||
return <Modal
|
return <Modal
|
||||||
data-wd-key="modal:debug"
|
data-wd-key="modal:debug"
|
||||||
isOpen={this.props.isOpen}
|
isOpen={props.isOpen}
|
||||||
onOpenToggle={this.props.onOpenToggle}
|
onOpenToggle={props.onOpenToggle}
|
||||||
title={t("Debug")}
|
title={t("Debug")}
|
||||||
>
|
>
|
||||||
<section className="maputnik-modal-section maputnik-modal-shortcuts">
|
<section className="maputnik-modal-section maputnik-modal-shortcuts">
|
||||||
<h1>{t("Options")}</h1>
|
<h1>{t("Options")}</h1>
|
||||||
{this.props.renderer === "mlgljs" &&
|
{props.renderer === "mlgljs" &&
|
||||||
<ul>
|
<ul>
|
||||||
{Object.entries(this.props.maplibreGlDebugOptions!).map(([key, val]) => {
|
{Object.entries(props.maplibreGlDebugOptions!).map(([key, val]) => {
|
||||||
return <li key={key}>
|
return <li key={key}>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" checked={val} onChange={(e) => this.props.onChangeMaplibreGlDebug(key, e.target.checked)} /> {key}
|
<input type="checkbox" checked={val} onChange={(e) => props.onChangeMaplibreGlDebug(key, e.target.checked)} /> {key}
|
||||||
</label>
|
</label>
|
||||||
</li>;
|
</li>;
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
}
|
}
|
||||||
{this.props.renderer === "ol" &&
|
{props.renderer === "ol" &&
|
||||||
<ul>
|
<ul>
|
||||||
{Object.entries(this.props.openlayersDebugOptions!).map(([key, val]) => {
|
{Object.entries(props.openlayersDebugOptions!).map(([key, val]) => {
|
||||||
return <li key={key}>
|
return <li key={key}>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" checked={val} onChange={(e) => this.props.onChangeOpenlayersDebug(key, e.target.checked)} /> {key}
|
<input type="checkbox" checked={val} onChange={(e) => props.onChangeOpenlayersDebug(key, e.target.checked)} /> {key}
|
||||||
</label>
|
</label>
|
||||||
</li>;
|
</li>;
|
||||||
})}
|
})}
|
||||||
@@ -76,7 +75,6 @@ class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</Modal>;
|
</Modal>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const ModalDebug = withTranslation()(ModalDebugInternal);
|
export const ModalDebug = withTranslation()(ModalDebugInternal);
|
||||||
|
|||||||
@@ -28,31 +28,31 @@ type ModalExportInternalProps = {
|
|||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
|
|
||||||
class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
const ModalExportInternal: React.FC<ModalExportInternalProps> = (props) => {
|
||||||
|
|
||||||
tokenizedStyle() {
|
function tokenizedStyle() {
|
||||||
return format(
|
return format(
|
||||||
stripAccessTokens(
|
stripAccessTokens(
|
||||||
replaceAccessTokens(this.props.mapStyle)
|
replaceAccessTokens(props.mapStyle)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
exportName() {
|
function exportName() {
|
||||||
if (this.props.mapStyle.name) {
|
if (props.mapStyle.name) {
|
||||||
return Slugify(this.props.mapStyle.name, {
|
return Slugify(props.mapStyle.name, {
|
||||||
replacement: "_",
|
replacement: "_",
|
||||||
remove: /[*\-+~.()'"!:]/g,
|
remove: /[*\-+~.()'"!:]/g,
|
||||||
lower: true
|
lower: true
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return this.props.mapStyle.id;
|
return props.mapStyle.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createHtml() {
|
function createHtml() {
|
||||||
const tokenStyle = this.tokenizedStyle();
|
const tokenStyle = tokenizedStyle();
|
||||||
const htmlTitle = this.props.mapStyle.name || this.props.t("Map");
|
const htmlTitle = props.mapStyle.name || props.t("Map");
|
||||||
const html = `<!DOCTYPE html>
|
const html = `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -80,49 +80,11 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const blob = new Blob([html], {type: "text/html;charset=utf-8"});
|
const blob = new Blob([html], {type: "text/html;charset=utf-8"});
|
||||||
const exportName = this.exportName();
|
const name = exportName();
|
||||||
saveAs(blob, exportName + ".html");
|
saveAs(blob, name + ".html");
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveStyle() {
|
async function createFileHandle(): Promise<FileSystemFileHandle | null> {
|
||||||
const tokenStyle = this.tokenizedStyle();
|
|
||||||
|
|
||||||
// it is not guaranteed that the File System Access API is available on all
|
|
||||||
// browsers. If the function is not available, a fallback behavior is used.
|
|
||||||
if (!showSaveFilePickerAvailable) {
|
|
||||||
const blob = new Blob([tokenStyle], {type: "application/json;charset=utf-8"});
|
|
||||||
const exportName = this.exportName();
|
|
||||||
saveAs(blob, exportName + ".json");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileHandle = this.props.fileHandle;
|
|
||||||
if (fileHandle == null) {
|
|
||||||
fileHandle = await this.createFileHandle();
|
|
||||||
this.props.onSetFileHandle(fileHandle);
|
|
||||||
if (fileHandle == null) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const writable = await fileHandle.createWritable();
|
|
||||||
await writable.write(tokenStyle);
|
|
||||||
await writable.close();
|
|
||||||
this.props.onOpenToggle();
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveStyleAs() {
|
|
||||||
const tokenStyle = this.tokenizedStyle();
|
|
||||||
|
|
||||||
const fileHandle = await this.createFileHandle();
|
|
||||||
this.props.onSetFileHandle(fileHandle);
|
|
||||||
if (fileHandle == null) return;
|
|
||||||
|
|
||||||
const writable = await fileHandle.createWritable();
|
|
||||||
await writable.write(tokenStyle);
|
|
||||||
await writable.close();
|
|
||||||
this.props.onOpenToggle();
|
|
||||||
}
|
|
||||||
|
|
||||||
async createFileHandle(): Promise<FileSystemFileHandle | null> {
|
|
||||||
const pickerOpts: SaveFilePickerOptions = {
|
const pickerOpts: SaveFilePickerOptions = {
|
||||||
types: [
|
types: [
|
||||||
{
|
{
|
||||||
@@ -130,33 +92,69 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
|||||||
accept: {"application/json": [".json"]},
|
accept: {"application/json": [".json"]},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
suggestedName: this.exportName(),
|
suggestedName: exportName(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const fileHandle = await window.showSaveFilePicker(pickerOpts) as FileSystemFileHandle;
|
const fileHandle = await window.showSaveFilePicker(pickerOpts) as FileSystemFileHandle;
|
||||||
this.props.onSetFileHandle(fileHandle);
|
props.onSetFileHandle(fileHandle);
|
||||||
return fileHandle;
|
return fileHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
changeMetadataProperty(property: string, value: any) {
|
async function saveStyle() {
|
||||||
|
const tokenStyle = tokenizedStyle();
|
||||||
|
|
||||||
|
// it is not guaranteed that the File System Access API is available on all
|
||||||
|
// browsers. If the function is not available, a fallback behavior is used.
|
||||||
|
if (!showSaveFilePickerAvailable) {
|
||||||
|
const blob = new Blob([tokenStyle], {type: "application/json;charset=utf-8"});
|
||||||
|
const name = exportName();
|
||||||
|
saveAs(blob, name + ".json");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileHandle = props.fileHandle;
|
||||||
|
if (fileHandle == null) {
|
||||||
|
fileHandle = await createFileHandle();
|
||||||
|
props.onSetFileHandle(fileHandle);
|
||||||
|
if (fileHandle == null) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const writable = await fileHandle.createWritable();
|
||||||
|
await writable.write(tokenStyle);
|
||||||
|
await writable.close();
|
||||||
|
props.onOpenToggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveStyleAs() {
|
||||||
|
const tokenStyle = tokenizedStyle();
|
||||||
|
|
||||||
|
const fileHandle = await createFileHandle();
|
||||||
|
props.onSetFileHandle(fileHandle);
|
||||||
|
if (fileHandle == null) return;
|
||||||
|
|
||||||
|
const writable = await fileHandle.createWritable();
|
||||||
|
await writable.write(tokenStyle);
|
||||||
|
await writable.close();
|
||||||
|
props.onOpenToggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeMetadataProperty(property: string, value: any) {
|
||||||
const changedStyle = {
|
const changedStyle = {
|
||||||
...this.props.mapStyle,
|
...props.mapStyle,
|
||||||
metadata: {
|
metadata: {
|
||||||
...this.props.mapStyle.metadata as any,
|
...props.mapStyle.metadata as any,
|
||||||
[property]: value
|
[property]: value
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.props.onStyleChanged(changedStyle);
|
props.onStyleChanged(changedStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const t = props.t;
|
||||||
render() {
|
|
||||||
const t = this.props.t;
|
|
||||||
const fsa = fieldSpecAdditional(t);
|
const fsa = fieldSpecAdditional(t);
|
||||||
return <Modal
|
return <Modal
|
||||||
data-wd-key="modal:export"
|
data-wd-key="modal:export"
|
||||||
isOpen={this.props.isOpen}
|
isOpen={props.isOpen}
|
||||||
onOpenToggle={this.props.onOpenToggle}
|
onOpenToggle={props.onOpenToggle}
|
||||||
title={t("Save Style")}
|
title={t("Save Style")}
|
||||||
className="maputnik-export-modal"
|
className="maputnik-export-modal"
|
||||||
>
|
>
|
||||||
@@ -171,42 +169,42 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
|||||||
<FieldString
|
<FieldString
|
||||||
label={fsa.maputnik.maptiler_access_token.label}
|
label={fsa.maputnik.maptiler_access_token.label}
|
||||||
fieldSpec={fsa.maputnik.maptiler_access_token}
|
fieldSpec={fsa.maputnik.maptiler_access_token}
|
||||||
value={(this.props.mapStyle.metadata || {} as any)["maputnik:openmaptiles_access_token"]}
|
value={(props.mapStyle.metadata || {} as any)["maputnik:openmaptiles_access_token"]}
|
||||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
|
onChange={changeMetadataProperty.bind(null, "maputnik:openmaptiles_access_token")}
|
||||||
/>
|
/>
|
||||||
<FieldString
|
<FieldString
|
||||||
label={fsa.maputnik.thunderforest_access_token.label}
|
label={fsa.maputnik.thunderforest_access_token.label}
|
||||||
fieldSpec={fsa.maputnik.thunderforest_access_token}
|
fieldSpec={fsa.maputnik.thunderforest_access_token}
|
||||||
value={(this.props.mapStyle.metadata || {} as any)["maputnik:thunderforest_access_token"]}
|
value={(props.mapStyle.metadata || {} as any)["maputnik:thunderforest_access_token"]}
|
||||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
|
onChange={changeMetadataProperty.bind(null, "maputnik:thunderforest_access_token")}
|
||||||
/>
|
/>
|
||||||
<FieldString
|
<FieldString
|
||||||
label={fsa.maputnik.stadia_access_token.label}
|
label={fsa.maputnik.stadia_access_token.label}
|
||||||
fieldSpec={fsa.maputnik.stadia_access_token}
|
fieldSpec={fsa.maputnik.stadia_access_token}
|
||||||
value={(this.props.mapStyle.metadata || {} as any)["maputnik:stadia_access_token"]}
|
value={(props.mapStyle.metadata || {} as any)["maputnik:stadia_access_token"]}
|
||||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:stadia_access_token")}
|
onChange={changeMetadataProperty.bind(null, "maputnik:stadia_access_token")}
|
||||||
/>
|
/>
|
||||||
<FieldString
|
<FieldString
|
||||||
label={fsa.maputnik.locationiq_access_token.label}
|
label={fsa.maputnik.locationiq_access_token.label}
|
||||||
fieldSpec={fsa.maputnik.locationiq_access_token}
|
fieldSpec={fsa.maputnik.locationiq_access_token}
|
||||||
value={(this.props.mapStyle.metadata || {} as any)["maputnik:locationiq_access_token"]}
|
value={(props.mapStyle.metadata || {} as any)["maputnik:locationiq_access_token"]}
|
||||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:locationiq_access_token")}
|
onChange={changeMetadataProperty.bind(null, "maputnik:locationiq_access_token")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="maputnik-modal-export-buttons">
|
<div className="maputnik-modal-export-buttons">
|
||||||
<InputButton onClick={this.saveStyle.bind(this)}>
|
<InputButton onClick={saveStyle}>
|
||||||
<MdSave/>
|
<MdSave/>
|
||||||
{t("Save")}
|
{t("Save")}
|
||||||
</InputButton>
|
</InputButton>
|
||||||
{showSaveFilePickerAvailable && (
|
{showSaveFilePickerAvailable && (
|
||||||
<InputButton onClick={this.saveStyleAs.bind(this)}>
|
<InputButton onClick={saveStyleAs}>
|
||||||
<MdSave/>
|
<MdSave/>
|
||||||
{t("Save as")}
|
{t("Save as")}
|
||||||
</InputButton>
|
</InputButton>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<InputButton onClick={this.createHtml.bind(this)}>
|
<InputButton onClick={createHtml}>
|
||||||
<MdMap/>
|
<MdMap/>
|
||||||
{t("Create HTML")}
|
{t("Create HTML")}
|
||||||
</InputButton>
|
</InputButton>
|
||||||
@@ -214,7 +212,6 @@ class ModalExportInternal extends React.Component<ModalExportInternalProps> {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
</Modal>;
|
</Modal>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const ModalExport = withTranslation()(ModalExportInternal);
|
export const ModalExport = withTranslation()(ModalExportInternal);
|
||||||
|
|||||||
@@ -13,26 +13,24 @@ type ModalLoadingInternalProps = {
|
|||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
|
|
||||||
class ModalLoadingInternal extends React.Component<ModalLoadingInternalProps> {
|
const ModalLoadingInternal: React.FC<ModalLoadingInternalProps> = (props) => {
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
|
||||||
return <Modal
|
return <Modal
|
||||||
data-wd-key="modal:loading"
|
data-wd-key="modal:loading"
|
||||||
isOpen={this.props.isOpen}
|
isOpen={props.isOpen}
|
||||||
underlayClickExits={false}
|
underlayClickExits={false}
|
||||||
title={this.props.title}
|
title={props.title}
|
||||||
onOpenToggle={() => this.props.onCancel()}
|
onOpenToggle={() => props.onCancel()}
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
{this.props.message}
|
{props.message}
|
||||||
</p>
|
</p>
|
||||||
<p className="maputnik-dialog__buttons">
|
<p className="maputnik-dialog__buttons">
|
||||||
<InputButton onClick={(e) => this.props.onCancel(e)}>
|
<InputButton onClick={(e) => props.onCancel(e)}>
|
||||||
{t("Cancel")}
|
{t("Cancel")}
|
||||||
</InputButton>
|
</InputButton>
|
||||||
</p>
|
</p>
|
||||||
</Modal>;
|
</Modal>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const ModalLoading = withTranslation()(ModalLoadingInternal);
|
export const ModalLoading = withTranslation()(ModalLoadingInternal);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { type DragEvent, type FormEvent } from "react";
|
import React, { type DragEvent, type FormEvent, useRef, useState } from "react";
|
||||||
import { MdFileUpload } from "react-icons/md";
|
import { MdFileUpload } from "react-icons/md";
|
||||||
import { MdAddCircleOutline } from "react-icons/md";
|
import { MdAddCircleOutline } from "react-icons/md";
|
||||||
import { Trans, type WithTranslation, withTranslation } from "react-i18next";
|
import { Trans, type WithTranslation, withTranslation } from "react-i18next";
|
||||||
@@ -18,29 +18,27 @@ type PublicStyleProps = {
|
|||||||
onSelect(...args: unknown[]): unknown
|
onSelect(...args: unknown[]): unknown
|
||||||
};
|
};
|
||||||
|
|
||||||
class PublicStyle extends React.Component<PublicStyleProps> {
|
const PublicStyle: React.FC<PublicStyleProps> = (props) => {
|
||||||
render() {
|
|
||||||
return <div className="maputnik-public-style">
|
return <div className="maputnik-public-style">
|
||||||
<InputButton
|
<InputButton
|
||||||
className="maputnik-public-style-button"
|
className="maputnik-public-style-button"
|
||||||
aria-label={this.props.title}
|
aria-label={props.title}
|
||||||
onClick={() => this.props.onSelect(this.props.url)}
|
onClick={() => props.onSelect(props.url)}
|
||||||
>
|
>
|
||||||
<div className="maputnik-public-style-header">
|
<div className="maputnik-public-style-header">
|
||||||
<div>{this.props.title}</div>
|
<div>{props.title}</div>
|
||||||
<span className="maputnik-space" />
|
<span className="maputnik-space" />
|
||||||
<MdAddCircleOutline />
|
<MdAddCircleOutline />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="maputnik-public-style-thumbnail"
|
className="maputnik-public-style-thumbnail"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(${this.props.thumbnailUrl})`
|
backgroundImage: `url(${props.thumbnailUrl})`
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
</InputButton>
|
</InputButton>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
type ModalOpenInternalProps = {
|
type ModalOpenInternalProps = {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@@ -49,46 +47,32 @@ type ModalOpenInternalProps = {
|
|||||||
fileHandle: FileSystemFileHandle | null
|
fileHandle: FileSystemFileHandle | null
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
type ModalOpenState = {
|
const ModalOpenInternal: React.FC<ModalOpenInternalProps> = (props) => {
|
||||||
styleUrl: string
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
isDragOver: boolean
|
|
||||||
error?: string | null
|
const [styleUrl, setStyleUrl] = useState<string>("");
|
||||||
activeRequest?: any
|
const [isDragOver, setIsDragOver] = useState<boolean>(false);
|
||||||
activeRequestUrl?: string | null
|
const [error, setError] = useState<string | null | undefined>(undefined);
|
||||||
|
const [activeRequest, setActiveRequest] = useState<any>(undefined);
|
||||||
|
const [activeRequestUrl, setActiveRequestUrl] = useState<string | null | undefined>(undefined);
|
||||||
|
|
||||||
|
const clearError = () => {
|
||||||
|
setError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpenState> {
|
const onCancelActiveRequest = (e: Event) => {
|
||||||
private fileInputRef = React.createRef<HTMLInputElement>();
|
|
||||||
|
|
||||||
constructor(props: ModalOpenInternalProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
styleUrl: "",
|
|
||||||
isDragOver: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
clearError() {
|
|
||||||
this.setState({
|
|
||||||
error: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onCancelActiveRequest(e: Event) {
|
|
||||||
// Else the click propagates to the underlying modal
|
// Else the click propagates to the underlying modal
|
||||||
if (e) e.stopPropagation();
|
if (e) e.stopPropagation();
|
||||||
|
|
||||||
if (this.state.activeRequest) {
|
if (activeRequest) {
|
||||||
this.state.activeRequest.abort();
|
activeRequest.abort();
|
||||||
this.setState({
|
setActiveRequest(null);
|
||||||
activeRequest: null,
|
setActiveRequestUrl(null);
|
||||||
activeRequestUrl: null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onStyleSelect = (styleUrl: string) => {
|
const onStyleSelect = (styleUrl: string) => {
|
||||||
this.clearError();
|
clearError();
|
||||||
|
|
||||||
let canceled: boolean = false;
|
let canceled: boolean = false;
|
||||||
|
|
||||||
@@ -104,43 +88,37 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
setActiveRequest(null);
|
||||||
activeRequest: null,
|
setActiveRequestUrl(null);
|
||||||
activeRequestUrl: null
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStyle = ensureStyleValidity(body);
|
const mapStyle = ensureStyleValidity(body);
|
||||||
console.log("Loaded style ", mapStyle.id);
|
console.log("Loaded style ", mapStyle.id);
|
||||||
this.props.onStyleOpen(mapStyle);
|
props.onStyleOpen(mapStyle);
|
||||||
this.onOpenToggle();
|
onOpenToggle();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
this.setState({
|
setError(`Failed to load: '${styleUrl}'`);
|
||||||
error: `Failed to load: '${styleUrl}'`,
|
setActiveRequest(null);
|
||||||
activeRequest: null,
|
setActiveRequestUrl(null);
|
||||||
activeRequestUrl: null
|
|
||||||
});
|
|
||||||
console.error(err);
|
console.error(err);
|
||||||
console.warn("Could not open the style URL", styleUrl);
|
console.warn("Could not open the style URL", styleUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setState({
|
setActiveRequest({
|
||||||
activeRequest: {
|
|
||||||
abort: function () {
|
abort: function () {
|
||||||
canceled = true;
|
canceled = true;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
activeRequestUrl: styleUrl
|
|
||||||
});
|
});
|
||||||
|
setActiveRequestUrl(styleUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmitUrl = (e: FormEvent<HTMLFormElement>) => {
|
const onSubmitUrl = (e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.onStyleSelect(this.state.styleUrl);
|
onStyleSelect(styleUrl);
|
||||||
};
|
};
|
||||||
|
|
||||||
onOpenFile = async () => {
|
const onOpenFile = async () => {
|
||||||
this.clearError();
|
clearError();
|
||||||
|
|
||||||
const pickerOpts: OpenFilePickerOptions = {
|
const pickerOpts: OpenFilePickerOptions = {
|
||||||
types: [
|
types: [
|
||||||
@@ -160,26 +138,24 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
|
|||||||
try {
|
try {
|
||||||
mapStyle = JSON.parse(content);
|
mapStyle = JSON.parse(content);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({
|
setError((err as Error).toString());
|
||||||
error: (err as Error).toString()
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mapStyle = ensureStyleValidity(mapStyle);
|
mapStyle = ensureStyleValidity(mapStyle);
|
||||||
|
|
||||||
this.props.onStyleOpen(mapStyle, fileHandle);
|
props.onStyleOpen(mapStyle, fileHandle);
|
||||||
this.onOpenToggle();
|
onOpenToggle();
|
||||||
return file;
|
return file;
|
||||||
};
|
};
|
||||||
|
|
||||||
// it is not guaranteed that the File System Access API is available on all
|
// it is not guaranteed that the File System Access API is available on all
|
||||||
// browsers. If the function is not available, a fallback behavior is used.
|
// browsers. If the function is not available, a fallback behavior is used.
|
||||||
onFileChanged = (files: FileList | null) => {
|
const onFileChanged = (files: FileList | null) => {
|
||||||
if (!files) return;
|
if (!files) return;
|
||||||
if (files.length === 0) return;
|
if (files.length === 0) return;
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
this.clearError();
|
clearError();
|
||||||
|
|
||||||
reader.readAsText(file, "UTF-8");
|
reader.readAsText(file, "UTF-8");
|
||||||
reader.onload = e => {
|
reader.onload = e => {
|
||||||
@@ -188,83 +164,76 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
|
|||||||
mapStyle = JSON.parse(e.target?.result as string);
|
mapStyle = JSON.parse(e.target?.result as string);
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
this.setState({
|
setError((err as Error).toString());
|
||||||
error: (err as Error).toString()
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mapStyle = ensureStyleValidity(mapStyle);
|
mapStyle = ensureStyleValidity(mapStyle);
|
||||||
this.props.onStyleOpen(mapStyle);
|
props.onStyleOpen(mapStyle);
|
||||||
this.onOpenToggle();
|
onOpenToggle();
|
||||||
};
|
};
|
||||||
reader.onerror = e => console.log(e.target);
|
reader.onerror = e => console.log(e.target);
|
||||||
};
|
};
|
||||||
|
|
||||||
onOpenToggle() {
|
const onOpenToggle = () => {
|
||||||
this.setState({
|
setStyleUrl("");
|
||||||
styleUrl: "",
|
setIsDragOver(false);
|
||||||
isDragOver: false,
|
clearError();
|
||||||
});
|
props.onOpenToggle();
|
||||||
this.clearError();
|
};
|
||||||
this.props.onOpenToggle();
|
|
||||||
}
|
|
||||||
|
|
||||||
onBrowseClick = async () => {
|
const onBrowseClick = async () => {
|
||||||
if (typeof window.showOpenFilePicker === "function") {
|
if (typeof window.showOpenFilePicker === "function") {
|
||||||
await this.onOpenFile();
|
await onOpenFile();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
onFileDragOver = (e: DragEvent<HTMLDivElement>) => {
|
const onFileDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
if (!this.state.isDragOver) {
|
if (!isDragOver) {
|
||||||
this.setState({ isDragOver: true });
|
setIsDragOver(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onFileDragLeave = (e: DragEvent<HTMLDivElement>) => {
|
const onFileDragLeave = (e: DragEvent<HTMLDivElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.setState({ isDragOver: false });
|
setIsDragOver(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
onFileDrop = (e: DragEvent<HTMLDivElement>) => {
|
const onFileDrop = (e: DragEvent<HTMLDivElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
this.setState({ isDragOver: false });
|
setIsDragOver(false);
|
||||||
this.onFileChanged(e.dataTransfer.files);
|
onFileChanged(e.dataTransfer.files);
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeUrl = (url: string) => {
|
const onChangeUrl = (url: string) => {
|
||||||
this.setState({
|
setStyleUrl(url);
|
||||||
styleUrl: url,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
|
||||||
const styleOptions = publicStyles.map(style => {
|
const styleOptions = publicStyles.map(style => {
|
||||||
return <PublicStyle
|
return <PublicStyle
|
||||||
key={style.id}
|
key={style.id}
|
||||||
url={style.url}
|
url={style.url}
|
||||||
title={style.title}
|
title={style.title}
|
||||||
thumbnailUrl={style.thumbnail}
|
thumbnailUrl={style.thumbnail}
|
||||||
onSelect={this.onStyleSelect}
|
onSelect={onStyleSelect}
|
||||||
/>;
|
/>;
|
||||||
});
|
});
|
||||||
|
|
||||||
let errorElement;
|
let errorElement;
|
||||||
if (this.state.error) {
|
if (error) {
|
||||||
errorElement = (
|
errorElement = (
|
||||||
<div className="maputnik-modal-error">
|
<div className="maputnik-modal-error">
|
||||||
{this.state.error}
|
{error}
|
||||||
<a href="#" onClick={() => this.clearError()} className="maputnik-modal-error-close">×</a>
|
<a href="#" onClick={() => clearError()} className="maputnik-modal-error-close">×</a>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -273,8 +242,8 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
|
|||||||
<div>
|
<div>
|
||||||
<Modal
|
<Modal
|
||||||
data-wd-key="modal:open"
|
data-wd-key="modal:open"
|
||||||
isOpen={this.props.isOpen}
|
isOpen={props.isOpen}
|
||||||
onOpenToggle={() => this.onOpenToggle()}
|
onOpenToggle={() => onOpenToggle()}
|
||||||
title={t("Open Style")}
|
title={t("Open Style")}
|
||||||
>
|
>
|
||||||
{errorElement}
|
{errorElement}
|
||||||
@@ -283,17 +252,17 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
|
|||||||
<p>{t("Open a local JSON style from your computer.")}</p>
|
<p>{t("Open a local JSON style from your computer.")}</p>
|
||||||
<div
|
<div
|
||||||
data-wd-key="modal:open.dropzone"
|
data-wd-key="modal:open.dropzone"
|
||||||
className={`maputnik-upload-dropzone${this.state.isDragOver ? " maputnik-upload-dropzone--active" : ""}`}
|
className={`maputnik-upload-dropzone${isDragOver ? " maputnik-upload-dropzone--active" : ""}`}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onDragOver={this.onFileDragOver}
|
onDragOver={onFileDragOver}
|
||||||
onDragLeave={this.onFileDragLeave}
|
onDragLeave={onFileDragLeave}
|
||||||
onDrop={this.onFileDrop}
|
onDrop={onFileDrop}
|
||||||
onClick={() => void this.onBrowseClick()}
|
onClick={() => void onBrowseClick()}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
void this.onBrowseClick();
|
void onBrowseClick();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -304,17 +273,17 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
ref={this.fileInputRef}
|
ref={fileInputRef}
|
||||||
data-wd-key="modal:open.file.input"
|
data-wd-key="modal:open.file.input"
|
||||||
type="file"
|
type="file"
|
||||||
style={{ display: "none" }}
|
style={{ display: "none" }}
|
||||||
onChange={(e) => this.onFileChanged(e.target.files)}
|
onChange={(e) => onFileChanged(e.target.files)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="maputnik-modal-section">
|
<section className="maputnik-modal-section">
|
||||||
<form onSubmit={this.onSubmitUrl}>
|
<form onSubmit={onSubmitUrl}>
|
||||||
<h1>{t("Load from URL")}</h1>
|
<h1>{t("Load from URL")}</h1>
|
||||||
<p>
|
<p>
|
||||||
<Trans t={t}>
|
<Trans t={t}>
|
||||||
@@ -327,16 +296,16 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
|
|||||||
type="text"
|
type="text"
|
||||||
className="maputnik-input"
|
className="maputnik-input"
|
||||||
default={t("Enter URL...")}
|
default={t("Enter URL...")}
|
||||||
value={this.state.styleUrl}
|
value={styleUrl}
|
||||||
onInput={this.onChangeUrl}
|
onInput={onChangeUrl}
|
||||||
onChange={this.onChangeUrl}
|
onChange={onChangeUrl}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<InputButton
|
<InputButton
|
||||||
data-wd-key="modal:open.url.button"
|
data-wd-key="modal:open.url.button"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="maputnik-big-button"
|
className="maputnik-big-button"
|
||||||
disabled={this.state.styleUrl.length < 1}
|
disabled={styleUrl.length < 1}
|
||||||
>Load from URL</InputButton>
|
>Load from URL</InputButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -354,14 +323,13 @@ class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpe
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<ModalLoading
|
<ModalLoading
|
||||||
isOpen={!!this.state.activeRequest}
|
isOpen={!!activeRequest}
|
||||||
title={t("Loading style")}
|
title={t("Loading style")}
|
||||||
onCancel={(e: Event) => this.onCancelActiveRequest(e)}
|
onCancel={(e: Event) => onCancelActiveRequest(e)}
|
||||||
message={t("Loading") + ": " + this.state.activeRequestUrl}
|
message={t("Loading") + ": " + activeRequestUrl}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const ModalOpen = withTranslation()(ModalOpenInternal);
|
export const ModalOpen = withTranslation()(ModalOpenInternal);
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ type ModalSettingsInternalProps = {
|
|||||||
onOpenToggle(): void
|
onOpenToggle(): void
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps> {
|
const ModalSettingsInternal: React.FC<ModalSettingsInternalProps> = (props) => {
|
||||||
changeTransitionProperty(property: keyof TransitionSpecification, value: number | undefined) {
|
function changeTransitionProperty(property: keyof TransitionSpecification, value: number | undefined) {
|
||||||
const transition = {
|
const transition = {
|
||||||
...this.props.mapStyle.transition,
|
...props.mapStyle.transition,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
@@ -37,15 +37,15 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
transition[property] = value;
|
transition[property] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onStyleChanged({
|
props.onStyleChanged({
|
||||||
...this.props.mapStyle,
|
...props.mapStyle,
|
||||||
transition,
|
transition,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
changeLightProperty(property: keyof LightSpecification, value: any) {
|
function changeLightProperty(property: keyof LightSpecification, value: any) {
|
||||||
const light = {
|
const light = {
|
||||||
...this.props.mapStyle.light,
|
...props.mapStyle.light,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
@@ -56,15 +56,15 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
light[property] = value;
|
light[property] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onStyleChanged({
|
props.onStyleChanged({
|
||||||
...this.props.mapStyle,
|
...props.mapStyle,
|
||||||
light,
|
light,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
changeTerrainProperty(property: keyof TerrainSpecification, value: any) {
|
function changeTerrainProperty(property: keyof TerrainSpecification, value: any) {
|
||||||
const terrain = {
|
const terrain = {
|
||||||
...this.props.mapStyle.terrain,
|
...props.mapStyle.terrain,
|
||||||
} as TerrainSpecification;
|
} as TerrainSpecification;
|
||||||
|
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
@@ -75,15 +75,15 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
terrain[property] = value;
|
terrain[property] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onStyleChanged({
|
props.onStyleChanged({
|
||||||
...this.props.mapStyle,
|
...props.mapStyle,
|
||||||
terrain,
|
terrain,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
changeProjectionType(value: any) {
|
function changeProjectionType(value: any) {
|
||||||
const projection = {
|
const projection = {
|
||||||
...this.props.mapStyle.projection,
|
...props.mapStyle.projection,
|
||||||
} as ProjectionSpecification;
|
} as ProjectionSpecification;
|
||||||
|
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
@@ -93,15 +93,15 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
projection.type = value;
|
projection.type = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onStyleChanged({
|
props.onStyleChanged({
|
||||||
...this.props.mapStyle,
|
...props.mapStyle,
|
||||||
projection,
|
projection,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
changeStyleProperty(property: keyof StyleSpecification | "owner", value: any) {
|
function changeStyleProperty(property: keyof StyleSpecification | "owner", value: any) {
|
||||||
const changedStyle = {
|
const changedStyle = {
|
||||||
...this.props.mapStyle,
|
...props.mapStyle,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
@@ -112,23 +112,22 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
changedStyle[property] = value;
|
changedStyle[property] = value;
|
||||||
}
|
}
|
||||||
this.props.onStyleChanged(changedStyle);
|
props.onStyleChanged(changedStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
const metadata = props.mapStyle.metadata || {} as any;
|
||||||
const metadata = this.props.mapStyle.metadata || {} as any;
|
const {t, onChangeMetadataProperty, mapStyle} = props;
|
||||||
const {t, onChangeMetadataProperty, mapStyle} = this.props;
|
|
||||||
const fsa = fieldSpecAdditional(t);
|
const fsa = fieldSpecAdditional(t);
|
||||||
|
|
||||||
const light = this.props.mapStyle.light || {};
|
const light = props.mapStyle.light || {};
|
||||||
const transition = this.props.mapStyle.transition || {};
|
const transition = props.mapStyle.transition || {};
|
||||||
const terrain = this.props.mapStyle.terrain || {} as TerrainSpecification;
|
const terrain = props.mapStyle.terrain || {} as TerrainSpecification;
|
||||||
const projection = this.props.mapStyle.projection || {} as ProjectionSpecification;
|
const projection = props.mapStyle.projection || {} as ProjectionSpecification;
|
||||||
|
|
||||||
return <Modal
|
return <Modal
|
||||||
data-wd-key="modal:settings"
|
data-wd-key="modal:settings"
|
||||||
isOpen={this.props.isOpen}
|
isOpen={props.isOpen}
|
||||||
onOpenToggle={this.props.onOpenToggle}
|
onOpenToggle={props.onOpenToggle}
|
||||||
title={t("Style Settings")}
|
title={t("Style Settings")}
|
||||||
>
|
>
|
||||||
<div className="modal:settings">
|
<div className="modal:settings">
|
||||||
@@ -136,21 +135,21 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
label={t("Name")}
|
label={t("Name")}
|
||||||
fieldSpec={latest.$root.name}
|
fieldSpec={latest.$root.name}
|
||||||
data-wd-key="modal:settings.name"
|
data-wd-key="modal:settings.name"
|
||||||
value={this.props.mapStyle.name}
|
value={props.mapStyle.name}
|
||||||
onChange={(value) => this.changeStyleProperty("name", value)}
|
onChange={(value) => changeStyleProperty("name", value)}
|
||||||
/>
|
/>
|
||||||
<FieldString
|
<FieldString
|
||||||
label={t("Owner")}
|
label={t("Owner")}
|
||||||
fieldSpec={{doc: t("Owner ID of the style. Used by Mapbox or future style APIs.")}}
|
fieldSpec={{doc: t("Owner ID of the style. Used by Mapbox or future style APIs.")}}
|
||||||
data-wd-key="modal:settings.owner"
|
data-wd-key="modal:settings.owner"
|
||||||
value={(this.props.mapStyle as any).owner}
|
value={(props.mapStyle as any).owner}
|
||||||
onChange={(value) => this.changeStyleProperty("owner", value)}
|
onChange={(value) => changeStyleProperty("owner", value)}
|
||||||
/>
|
/>
|
||||||
<Block label={t("Sprite URL")} fieldSpec={latest.$root.sprite} data-wd-key="modal:settings.sprite">
|
<Block label={t("Sprite URL")} fieldSpec={latest.$root.sprite} data-wd-key="modal:settings.sprite">
|
||||||
<FieldJson
|
<FieldJson
|
||||||
lintType="json"
|
lintType="json"
|
||||||
value={this.props.mapStyle.sprite as any}
|
value={props.mapStyle.sprite as any}
|
||||||
onChange={(value) => this.changeStyleProperty("sprite", value)}
|
onChange={(value) => changeStyleProperty("sprite", value)}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
|
|
||||||
@@ -158,8 +157,8 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
label={t("Glyphs URL")}
|
label={t("Glyphs URL")}
|
||||||
fieldSpec={latest.$root.glyphs}
|
fieldSpec={latest.$root.glyphs}
|
||||||
data-wd-key="modal:settings.glyphs"
|
data-wd-key="modal:settings.glyphs"
|
||||||
value={this.props.mapStyle.glyphs as string}
|
value={props.mapStyle.glyphs as string}
|
||||||
onChange={(value) => this.changeStyleProperty("glyphs", value)}
|
onChange={(value) => changeStyleProperty("glyphs", value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FieldString
|
<FieldString
|
||||||
@@ -201,7 +200,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
type="number"
|
type="number"
|
||||||
value={mapStyle.center || []}
|
value={mapStyle.center || []}
|
||||||
default={[0, 0]}
|
default={[0, 0]}
|
||||||
onChange={(value) => this.changeStyleProperty("center", value)}
|
onChange={(value) => changeStyleProperty("center", value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FieldNumber
|
<FieldNumber
|
||||||
@@ -210,7 +209,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
fieldSpec={latest.$root.zoom}
|
fieldSpec={latest.$root.zoom}
|
||||||
value={mapStyle.zoom}
|
value={mapStyle.zoom}
|
||||||
default={0}
|
default={0}
|
||||||
onChange={(value) => this.changeStyleProperty("zoom", value)}
|
onChange={(value) => changeStyleProperty("zoom", value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FieldNumber
|
<FieldNumber
|
||||||
@@ -219,7 +218,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
fieldSpec={latest.$root.bearing}
|
fieldSpec={latest.$root.bearing}
|
||||||
value={mapStyle.bearing}
|
value={mapStyle.bearing}
|
||||||
default={latest.$root.bearing.default}
|
default={latest.$root.bearing.default}
|
||||||
onChange={(value) => this.changeStyleProperty("bearing", value)}
|
onChange={(value) => changeStyleProperty("bearing", value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FieldNumber
|
<FieldNumber
|
||||||
@@ -228,7 +227,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
fieldSpec={latest.$root.pitch}
|
fieldSpec={latest.$root.pitch}
|
||||||
value={mapStyle.pitch}
|
value={mapStyle.pitch}
|
||||||
default={latest.$root.pitch.default}
|
default={latest.$root.pitch.default}
|
||||||
onChange={(value) => this.changeStyleProperty("pitch", value)}
|
onChange={(value) => changeStyleProperty("pitch", value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FieldEnum
|
<FieldEnum
|
||||||
@@ -238,7 +237,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
value={light.anchor as string}
|
value={light.anchor as string}
|
||||||
options={Object.keys(latest.light.anchor.values)}
|
options={Object.keys(latest.light.anchor.values)}
|
||||||
default={latest.light.anchor.default}
|
default={latest.light.anchor.default}
|
||||||
onChange={(value) => this.changeLightProperty("anchor", value)}
|
onChange={(value) => changeLightProperty("anchor", value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FieldColor
|
<FieldColor
|
||||||
@@ -246,7 +245,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
fieldSpec={latest.light.color}
|
fieldSpec={latest.light.color}
|
||||||
value={light.color as string}
|
value={light.color as string}
|
||||||
default={latest.light.color.default}
|
default={latest.light.color.default}
|
||||||
onChange={(value) => this.changeLightProperty("color", value)}
|
onChange={(value) => changeLightProperty("color", value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FieldNumber
|
<FieldNumber
|
||||||
@@ -255,7 +254,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
fieldSpec={latest.light.intensity}
|
fieldSpec={latest.light.intensity}
|
||||||
value={light.intensity as number}
|
value={light.intensity as number}
|
||||||
default={latest.light.intensity.default}
|
default={latest.light.intensity.default}
|
||||||
onChange={(value) => this.changeLightProperty("intensity", value)}
|
onChange={(value) => changeLightProperty("intensity", value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FieldArray
|
<FieldArray
|
||||||
@@ -265,7 +264,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
length={latest.light.position.length}
|
length={latest.light.position.length}
|
||||||
value={light.position as number[]}
|
value={light.position as number[]}
|
||||||
default={latest.light.position.default}
|
default={latest.light.position.default}
|
||||||
onChange={(value) => this.changeLightProperty("position", value)}
|
onChange={(value) => changeLightProperty("position", value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FieldString
|
<FieldString
|
||||||
@@ -273,7 +272,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
fieldSpec={latest.terrain.source}
|
fieldSpec={latest.terrain.source}
|
||||||
data-wd-key="modal:settings.maputnik:terrain_source"
|
data-wd-key="modal:settings.maputnik:terrain_source"
|
||||||
value={terrain.source}
|
value={terrain.source}
|
||||||
onChange={(value) => this.changeTerrainProperty("source", value)}
|
onChange={(value) => changeTerrainProperty("source", value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FieldNumber
|
<FieldNumber
|
||||||
@@ -282,7 +281,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
fieldSpec={latest.terrain.exaggeration}
|
fieldSpec={latest.terrain.exaggeration}
|
||||||
value={terrain.exaggeration}
|
value={terrain.exaggeration}
|
||||||
default={latest.terrain.exaggeration.default}
|
default={latest.terrain.exaggeration.default}
|
||||||
onChange={(value) => this.changeTerrainProperty("exaggeration", value)}
|
onChange={(value) => changeTerrainProperty("exaggeration", value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FieldNumber
|
<FieldNumber
|
||||||
@@ -291,7 +290,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
fieldSpec={latest.transition.delay}
|
fieldSpec={latest.transition.delay}
|
||||||
value={transition.delay}
|
value={transition.delay}
|
||||||
default={latest.transition.delay.default}
|
default={latest.transition.delay.default}
|
||||||
onChange={(value) => this.changeTransitionProperty("delay", value)}
|
onChange={(value) => changeTransitionProperty("delay", value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FieldNumber
|
<FieldNumber
|
||||||
@@ -300,7 +299,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
fieldSpec={latest.transition.duration}
|
fieldSpec={latest.transition.duration}
|
||||||
value={transition.duration}
|
value={transition.duration}
|
||||||
default={latest.transition.duration.default}
|
default={latest.transition.duration.default}
|
||||||
onChange={(value) => this.changeTransitionProperty("duration", value)}
|
onChange={(value) => changeTransitionProperty("duration", value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FieldSelect
|
<FieldSelect
|
||||||
@@ -313,7 +312,7 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
["vertical-perspective", "Vertical Perspective"]
|
["vertical-perspective", "Vertical Perspective"]
|
||||||
]}
|
]}
|
||||||
value={projection?.type?.toString() || ""}
|
value={projection?.type?.toString() || ""}
|
||||||
onChange={(value) => this.changeProjectionType(value)}
|
onChange={(value) => changeProjectionType(value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FieldSelect
|
<FieldSelect
|
||||||
@@ -329,7 +328,6 @@ class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps>
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Modal>;
|
</Modal>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const ModalSettings = withTranslation()(ModalSettingsInternal);
|
export const ModalSettings = withTranslation()(ModalSettingsInternal);
|
||||||
|
|||||||
@@ -10,9 +10,8 @@ type ModalShortcutsInternalProps = {
|
|||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
|
|
||||||
class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps> {
|
const ModalShortcutsInternal: React.FC<ModalShortcutsInternalProps> = (props) => {
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
|
||||||
const help = [
|
const help = [
|
||||||
{
|
{
|
||||||
key: <kbd>?</kbd>,
|
key: <kbd>?</kbd>,
|
||||||
@@ -103,8 +102,8 @@ class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps
|
|||||||
|
|
||||||
return <Modal
|
return <Modal
|
||||||
data-wd-key="modal:shortcuts"
|
data-wd-key="modal:shortcuts"
|
||||||
isOpen={this.props.isOpen}
|
isOpen={props.isOpen}
|
||||||
onOpenToggle={this.props.onOpenToggle}
|
onOpenToggle={props.onOpenToggle}
|
||||||
title={t("Shortcuts")}
|
title={t("Shortcuts")}
|
||||||
>
|
>
|
||||||
<section className="maputnik-modal-section maputnik-modal-shortcuts">
|
<section className="maputnik-modal-section maputnik-modal-shortcuts">
|
||||||
@@ -131,7 +130,6 @@ class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
</Modal>;
|
</Modal>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const ModalShortcuts = withTranslation()(ModalShortcutsInternal);
|
export const ModalShortcuts = withTranslation()(ModalShortcutsInternal);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import {MdAddCircleOutline, MdDelete} from "react-icons/md";
|
import {MdAddCircleOutline, MdDelete} from "react-icons/md";
|
||||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
||||||
import type {GeoJSONSourceSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, SourceSpecification, VectorSourceSpecification} from "maplibre-gl";
|
import type {GeoJSONSourceSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, SourceSpecification, VectorSourceSpecification} from "maplibre-gl";
|
||||||
@@ -23,23 +23,21 @@ type PublicSourceProps = {
|
|||||||
onSelect(...args: unknown[]): unknown
|
onSelect(...args: unknown[]): unknown
|
||||||
};
|
};
|
||||||
|
|
||||||
class PublicSource extends React.Component<PublicSourceProps> {
|
const PublicSource: React.FC<PublicSourceProps> = (props) => {
|
||||||
render() {
|
|
||||||
return <div className="maputnik-public-source">
|
return <div className="maputnik-public-source">
|
||||||
<InputButton
|
<InputButton
|
||||||
className="maputnik-public-source-select"
|
className="maputnik-public-source-select"
|
||||||
onClick={() => this.props.onSelect(this.props.id)}
|
onClick={() => props.onSelect(props.id)}
|
||||||
>
|
>
|
||||||
<div className="maputnik-public-source-info">
|
<div className="maputnik-public-source-info">
|
||||||
<p className="maputnik-public-source-name">{this.props.title}</p>
|
<p className="maputnik-public-source-name">{props.title}</p>
|
||||||
<p className="maputnik-public-source-id">#{this.props.id}</p>
|
<p className="maputnik-public-source-id">#{props.id}</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="maputnik-space" />
|
<span className="maputnik-space" />
|
||||||
<MdAddCircleOutline />
|
<MdAddCircleOutline />
|
||||||
</InputButton>
|
</InputButton>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
function editorMode(source: SourceSpecification) {
|
function editorMode(source: SourceSpecification) {
|
||||||
if(source.type === "raster") {
|
if(source.type === "raster") {
|
||||||
@@ -79,17 +77,16 @@ type ActiveModalSourcesTypeEditorProps = {
|
|||||||
onChange(...args: unknown[]): unknown
|
onChange(...args: unknown[]): unknown
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class ActiveModalSourcesTypeEditor extends React.Component<ActiveModalSourcesTypeEditorProps> {
|
const ActiveModalSourcesTypeEditor: React.FC<ActiveModalSourcesTypeEditorProps> = (props) => {
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
|
||||||
return <div className="maputnik-active-source-type-editor">
|
return <div className="maputnik-active-source-type-editor">
|
||||||
<div className="maputnik-active-source-type-editor-header">
|
<div className="maputnik-active-source-type-editor-header">
|
||||||
<span className="maputnik-active-source-type-editor-header-id">#{this.props.sourceId}</span>
|
<span className="maputnik-active-source-type-editor-header-id">#{props.sourceId}</span>
|
||||||
<span className="maputnik-space" />
|
<span className="maputnik-space" />
|
||||||
<InputButton
|
<InputButton
|
||||||
aria-label={t("Remove '{{sourceId}}' source", {sourceId: this.props.sourceId})}
|
aria-label={t("Remove '{{sourceId}}' source", {sourceId: props.sourceId})}
|
||||||
className="maputnik-active-source-type-editor-header-delete"
|
className="maputnik-active-source-type-editor-header-delete"
|
||||||
onClick={()=> this.props.onDelete(this.props.sourceId)}
|
onClick={()=> props.onDelete(props.sourceId)}
|
||||||
style={{backgroundColor: "transparent"}}
|
style={{backgroundColor: "transparent"}}
|
||||||
>
|
>
|
||||||
<MdDelete />
|
<MdDelete />
|
||||||
@@ -97,37 +94,20 @@ class ActiveModalSourcesTypeEditor extends React.Component<ActiveModalSourcesTyp
|
|||||||
</div>
|
</div>
|
||||||
<div className="maputnik-active-source-type-editor-content">
|
<div className="maputnik-active-source-type-editor-content">
|
||||||
<ModalSourcesTypeEditor
|
<ModalSourcesTypeEditor
|
||||||
onChange={this.props.onChange}
|
onChange={props.onChange}
|
||||||
mode={editorMode(this.props.source)}
|
mode={editorMode(props.source)}
|
||||||
source={this.props.source}
|
source={props.source}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
type AddSourceProps = {
|
type AddSourceProps = {
|
||||||
onAdd(...args: unknown[]): unknown
|
onAdd(...args: unknown[]): unknown
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
type AddSourceState = {
|
function defaultSource(mode: EditorMode, currentSource: SourceSpecification | undefined): SourceSpecification {
|
||||||
mode: EditorMode
|
const source = currentSource || {};
|
||||||
sourceId: string
|
|
||||||
source: SourceSpecification
|
|
||||||
};
|
|
||||||
|
|
||||||
class AddSource extends React.Component<AddSourceProps, AddSourceState> {
|
|
||||||
constructor(props: AddSourceProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
mode: "tilejson_vector",
|
|
||||||
sourceId: generateId(),
|
|
||||||
source: this.defaultSource("tilejson_vector"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultSource(mode: EditorMode): SourceSpecification {
|
|
||||||
const source = (this.state || {}).source || {};
|
|
||||||
const {protocol} = window.location;
|
const {protocol} = window.location;
|
||||||
|
|
||||||
switch(mode) {
|
switch(mode) {
|
||||||
@@ -204,17 +184,20 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onAdd = () => {
|
const AddSource: React.FC<AddSourceProps> = (props) => {
|
||||||
const {source, sourceId} = this.state;
|
const [mode, setMode] = useState<EditorMode>("tilejson_vector");
|
||||||
this.props.onAdd(sourceId, source);
|
const [sourceId, setSourceId] = useState<string>(() => generateId());
|
||||||
|
const [source, setSource] = useState<SourceSpecification>(() => defaultSource("tilejson_vector", undefined));
|
||||||
|
|
||||||
|
const onAdd = () => {
|
||||||
|
props.onAdd(sourceId, source);
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeSource = (source: SourceSpecification) => {
|
const onChangeSource = (newSource: SourceSpecification) => {
|
||||||
this.setState({source});
|
setSource(newSource);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
|
||||||
// Kind of a hack because the type changes, however maputnik has 1..n
|
// Kind of a hack because the type changes, however maputnik has 1..n
|
||||||
// options per type, for example
|
// options per type, for example
|
||||||
//
|
//
|
||||||
@@ -230,8 +213,8 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
|
|||||||
<FieldString
|
<FieldString
|
||||||
label={t("Source ID")}
|
label={t("Source ID")}
|
||||||
fieldSpec={{doc: t("Unique ID that identifies the source and is used in the layer to reference the source.")}}
|
fieldSpec={{doc: t("Unique ID that identifies the source and is used in the layer to reference the source.")}}
|
||||||
value={this.state.sourceId}
|
value={sourceId}
|
||||||
onChange={(v: string) => this.setState({ sourceId: v})}
|
onChange={(v: string) => setSourceId(v)}
|
||||||
data-wd-key="modal:sources.add.source_id"
|
data-wd-key="modal:sources.add.source_id"
|
||||||
/>
|
/>
|
||||||
<FieldSelect
|
<FieldSelect
|
||||||
@@ -250,25 +233,27 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
|
|||||||
["image", t("Image")],
|
["image", t("Image")],
|
||||||
["video", t("Video")],
|
["video", t("Video")],
|
||||||
]}
|
]}
|
||||||
onChange={mode => this.setState({mode: mode as EditorMode, source: this.defaultSource(mode as EditorMode)})}
|
onChange={newMode => {
|
||||||
value={this.state.mode as string}
|
setMode(newMode as EditorMode);
|
||||||
|
setSource(defaultSource(newMode as EditorMode, source));
|
||||||
|
}}
|
||||||
|
value={mode as string}
|
||||||
data-wd-key="modal:sources.add.source_type"
|
data-wd-key="modal:sources.add.source_type"
|
||||||
/>
|
/>
|
||||||
<ModalSourcesTypeEditor
|
<ModalSourcesTypeEditor
|
||||||
onChange={this.onChangeSource}
|
onChange={onChangeSource}
|
||||||
mode={this.state.mode}
|
mode={mode}
|
||||||
source={this.state.source}
|
source={source}
|
||||||
/>
|
/>
|
||||||
<InputButton
|
<InputButton
|
||||||
className="maputnik-add-source-button"
|
className="maputnik-add-source-button"
|
||||||
onClick={this.onAdd}
|
onClick={onAdd}
|
||||||
data-wd-key="modal:sources.add.add_source"
|
data-wd-key="modal:sources.add.add_source"
|
||||||
>
|
>
|
||||||
{t("Add Source")}
|
{t("Add Source")}
|
||||||
</InputButton>
|
</InputButton>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
type ModalSourcesInternalProps = {
|
type ModalSourcesInternalProps = {
|
||||||
mapStyle: StyleSpecificationWithId
|
mapStyle: StyleSpecificationWithId
|
||||||
@@ -277,24 +262,23 @@ type ModalSourcesInternalProps = {
|
|||||||
onStyleChanged: OnStyleChangedCallback
|
onStyleChanged: OnStyleChangedCallback
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
|
function stripTitle(source: SourceSpecification & {title?: string}): SourceSpecification {
|
||||||
stripTitle(source: SourceSpecification & {title?: string}): SourceSpecification {
|
|
||||||
const strippedSource = {...source};
|
const strippedSource = {...source};
|
||||||
delete strippedSource["title"];
|
delete strippedSource["title"];
|
||||||
return strippedSource;
|
return strippedSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
const ModalSourcesInternal: React.FC<ModalSourcesInternalProps> = (props) => {
|
||||||
const {t, mapStyle} = this.props;
|
const {t, mapStyle} = props;
|
||||||
const i18nProps = {t, i18n: this.props.i18n, tReady: this.props.tReady};
|
const i18nProps = {t, i18n: props.i18n, tReady: props.tReady};
|
||||||
const activeSources = Object.keys(mapStyle.sources).map(sourceId => {
|
const activeSources = Object.keys(mapStyle.sources).map(sourceId => {
|
||||||
const source = mapStyle.sources[sourceId];
|
const source = mapStyle.sources[sourceId];
|
||||||
return <ActiveModalSourcesTypeEditor
|
return <ActiveModalSourcesTypeEditor
|
||||||
key={sourceId}
|
key={sourceId}
|
||||||
sourceId={sourceId}
|
sourceId={sourceId}
|
||||||
source={source}
|
source={source}
|
||||||
onChange={(src: SourceSpecification) => this.props.onStyleChanged(changeSource(mapStyle, sourceId, src))}
|
onChange={(src: SourceSpecification) => props.onStyleChanged(changeSource(mapStyle, sourceId, src))}
|
||||||
onDelete={() => this.props.onStyleChanged(deleteSource(mapStyle, sourceId))}
|
onDelete={() => props.onStyleChanged(deleteSource(mapStyle, sourceId))}
|
||||||
{...i18nProps}
|
{...i18nProps}
|
||||||
/>;
|
/>;
|
||||||
});
|
});
|
||||||
@@ -306,14 +290,14 @@ class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
|
|||||||
id={sourceId}
|
id={sourceId}
|
||||||
type={source.type}
|
type={source.type}
|
||||||
title={source.title}
|
title={source.title}
|
||||||
onSelect={() => this.props.onStyleChanged(addSource(mapStyle, sourceId, this.stripTitle(source)))}
|
onSelect={() => props.onStyleChanged(addSource(mapStyle, sourceId, stripTitle(source)))}
|
||||||
/>;
|
/>;
|
||||||
});
|
});
|
||||||
|
|
||||||
return <Modal
|
return <Modal
|
||||||
data-wd-key="modal:sources"
|
data-wd-key="modal:sources"
|
||||||
isOpen={this.props.isOpen}
|
isOpen={props.isOpen}
|
||||||
onOpenToggle={this.props.onOpenToggle}
|
onOpenToggle={props.onOpenToggle}
|
||||||
title={t("Sources")}
|
title={t("Sources")}
|
||||||
>
|
>
|
||||||
<section className="maputnik-modal-section">
|
<section className="maputnik-modal-section">
|
||||||
@@ -335,12 +319,11 @@ class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
|
|||||||
<h1>{t("Add New Source")}</h1>
|
<h1>{t("Add New Source")}</h1>
|
||||||
<p>{t("Add a new source to your style. You can only choose the source type and id at creation time!")}</p>
|
<p>{t("Add a new source to your style. You can only choose the source type and id at creation time!")}</p>
|
||||||
<AddSource
|
<AddSource
|
||||||
onAdd={(sourceId: string, source: SourceSpecification) => this.props.onStyleChanged(addSource(mapStyle, sourceId, source))}
|
onAdd={(sourceId: string, source: SourceSpecification) => props.onStyleChanged(addSource(mapStyle, sourceId, source))}
|
||||||
{...i18nProps}
|
{...i18nProps}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</Modal>;
|
</Modal>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const ModalSources = withTranslation()(ModalSourcesInternal);
|
export const ModalSources = withTranslation()(ModalSourcesInternal);
|
||||||
|
|||||||
@@ -24,23 +24,21 @@ type TileJSONSourceEditorProps = {
|
|||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
|
|
||||||
class TileJSONSourceEditor extends React.Component<TileJSONSourceEditorProps> {
|
const TileJSONSourceEditor: React.FC<TileJSONSourceEditorProps> = (props) => {
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
|
||||||
return <div>
|
return <div>
|
||||||
<FieldUrl
|
<FieldUrl
|
||||||
label={t("TileJSON URL")}
|
label={t("TileJSON URL")}
|
||||||
fieldSpec={latest.source_vector.url}
|
fieldSpec={latest.source_vector.url}
|
||||||
value={this.props.source.url}
|
value={props.source.url}
|
||||||
onChange={url => this.props.onChange({
|
onChange={url => props.onChange({
|
||||||
...this.props.source,
|
...props.source,
|
||||||
url: url
|
url: url
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
{this.props.children}
|
{props.children}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
type TileURLSourceEditorProps = {
|
type TileURLSourceEditorProps = {
|
||||||
source: {
|
source: {
|
||||||
@@ -53,29 +51,28 @@ type TileURLSourceEditorProps = {
|
|||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
|
const TileURLSourceEditor: React.FC<TileURLSourceEditorProps> = (props) => {
|
||||||
changeTileUrls(tiles: string[]) {
|
function changeTileUrls(tiles: string[]) {
|
||||||
this.props.onChange({
|
props.onChange({
|
||||||
...this.props.source,
|
...props.source,
|
||||||
tiles,
|
tiles,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTileUrls() {
|
function renderTileUrls() {
|
||||||
const tiles = this.props.source.tiles || [];
|
const tiles = props.source.tiles || [];
|
||||||
return <FieldDynamicArray
|
return <FieldDynamicArray
|
||||||
label={this.props.t("Tile URL")}
|
label={props.t("Tile URL")}
|
||||||
fieldSpec={latest.source_vector.tiles}
|
fieldSpec={latest.source_vector.tiles}
|
||||||
type="url"
|
type="url"
|
||||||
value={tiles}
|
value={tiles}
|
||||||
onChange={this.changeTileUrls.bind(this)}
|
onChange={changeTileUrls}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
|
||||||
return <div>
|
return <div>
|
||||||
{this.renderTileUrls()}
|
{renderTileUrls()}
|
||||||
<FieldSelect
|
<FieldSelect
|
||||||
label={t("Scheme Type")}
|
label={t("Scheme Type")}
|
||||||
fieldSpec={latest.source_vector.scheme}
|
fieldSpec={latest.source_vector.scheme}
|
||||||
@@ -83,36 +80,34 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
|
|||||||
["xyz", "xyz (Slippy map tilenames scheme)"],
|
["xyz", "xyz (Slippy map tilenames scheme)"],
|
||||||
["tms", "tms (OSGeo spec scheme)"],
|
["tms", "tms (OSGeo spec scheme)"],
|
||||||
]}
|
]}
|
||||||
onChange={scheme => this.props.onChange({
|
onChange={scheme => props.onChange({
|
||||||
...this.props.source,
|
...props.source,
|
||||||
scheme
|
scheme
|
||||||
})}
|
})}
|
||||||
value={this.props.source.scheme}
|
value={props.source.scheme}
|
||||||
data-wd-key="modal:sources.add.scheme_type"
|
data-wd-key="modal:sources.add.scheme_type"
|
||||||
/>
|
/>
|
||||||
<FieldNumber
|
<FieldNumber
|
||||||
label={t("Min Zoom")}
|
label={t("Min Zoom")}
|
||||||
fieldSpec={latest.source_vector.minzoom}
|
fieldSpec={latest.source_vector.minzoom}
|
||||||
value={this.props.source.minzoom || 0}
|
value={props.source.minzoom || 0}
|
||||||
onChange={minzoom => this.props.onChange({
|
onChange={minzoom => props.onChange({
|
||||||
...this.props.source,
|
...props.source,
|
||||||
minzoom: minzoom
|
minzoom: minzoom
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<FieldNumber
|
<FieldNumber
|
||||||
label={t("Max Zoom")}
|
label={t("Max Zoom")}
|
||||||
fieldSpec={latest.source_vector.maxzoom}
|
fieldSpec={latest.source_vector.maxzoom}
|
||||||
value={this.props.source.maxzoom || 22}
|
value={props.source.maxzoom || 22}
|
||||||
onChange={maxzoom => this.props.onChange({
|
onChange={maxzoom => props.onChange({
|
||||||
...this.props.source,
|
...props.source,
|
||||||
maxzoom: maxzoom
|
maxzoom: maxzoom
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
{this.props.children}
|
{props.children}
|
||||||
</div>;
|
</div>;
|
||||||
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createCornerLabels: (t: TFunction) => { label: string, key: string }[] = (t) => ([
|
const createCornerLabels: (t: TFunction) => { label: string, key: string }[] = (t) => ([
|
||||||
{ label: t("Coord top left"), key: "top left" },
|
{ label: t("Coord top left"), key: "top left" },
|
||||||
@@ -129,15 +124,14 @@ type ImageSourceEditorProps = {
|
|||||||
onChange(...args: unknown[]): unknown
|
onChange(...args: unknown[]): unknown
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
|
const ImageSourceEditor: React.FC<ImageSourceEditorProps> = (props) => {
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
|
||||||
const changeCoord = (idx: number, val: [number, number]) => {
|
const changeCoord = (idx: number, val: [number, number]) => {
|
||||||
const coordinates = this.props.source.coordinates.slice(0);
|
const coordinates = props.source.coordinates.slice(0);
|
||||||
coordinates[idx] = val;
|
coordinates[idx] = val;
|
||||||
|
|
||||||
this.props.onChange({
|
props.onChange({
|
||||||
...this.props.source,
|
...props.source,
|
||||||
coordinates,
|
coordinates,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -146,9 +140,9 @@ class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
|
|||||||
<FieldUrl
|
<FieldUrl
|
||||||
label={t("Image URL")}
|
label={t("Image URL")}
|
||||||
fieldSpec={latest.source_image.url}
|
fieldSpec={latest.source_image.url}
|
||||||
value={this.props.source.url}
|
value={props.source.url}
|
||||||
onChange={url => this.props.onChange({
|
onChange={url => props.onChange({
|
||||||
...this.props.source,
|
...props.source,
|
||||||
url,
|
url,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -159,15 +153,14 @@ class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
|
|||||||
key={key}
|
key={key}
|
||||||
length={2}
|
length={2}
|
||||||
type="number"
|
type="number"
|
||||||
value={this.props.source.coordinates[idx]}
|
value={props.source.coordinates[idx]}
|
||||||
default={[0, 0]}
|
default={[0, 0]}
|
||||||
onChange={(val: [number, number]) => changeCoord(idx, val)}
|
onChange={(val: [number, number]) => changeCoord(idx, val)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
type VideoSourceEditorProps = {
|
type VideoSourceEditorProps = {
|
||||||
source: {
|
source: {
|
||||||
@@ -177,22 +170,21 @@ type VideoSourceEditorProps = {
|
|||||||
onChange(...args: unknown[]): unknown
|
onChange(...args: unknown[]): unknown
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class VideoSourceEditor extends React.Component<VideoSourceEditorProps> {
|
const VideoSourceEditor: React.FC<VideoSourceEditorProps> = (props) => {
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
|
||||||
const changeCoord = (idx: number, val: [number, number]) => {
|
const changeCoord = (idx: number, val: [number, number]) => {
|
||||||
const coordinates = this.props.source.coordinates.slice(0);
|
const coordinates = props.source.coordinates.slice(0);
|
||||||
coordinates[idx] = val;
|
coordinates[idx] = val;
|
||||||
|
|
||||||
this.props.onChange({
|
props.onChange({
|
||||||
...this.props.source,
|
...props.source,
|
||||||
coordinates,
|
coordinates,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeUrls = (urls: string[]) => {
|
const changeUrls = (urls: string[]) => {
|
||||||
this.props.onChange({
|
props.onChange({
|
||||||
...this.props.source,
|
...props.source,
|
||||||
urls,
|
urls,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -202,7 +194,7 @@ class VideoSourceEditor extends React.Component<VideoSourceEditorProps> {
|
|||||||
label={t("Video URL")}
|
label={t("Video URL")}
|
||||||
fieldSpec={latest.source_video.urls}
|
fieldSpec={latest.source_video.urls}
|
||||||
type="string"
|
type="string"
|
||||||
value={this.props.source.urls}
|
value={props.source.urls}
|
||||||
default={[]}
|
default={[]}
|
||||||
onChange={changeUrls}
|
onChange={changeUrls}
|
||||||
/>
|
/>
|
||||||
@@ -213,15 +205,14 @@ class VideoSourceEditor extends React.Component<VideoSourceEditorProps> {
|
|||||||
key={key}
|
key={key}
|
||||||
length={2}
|
length={2}
|
||||||
type="number"
|
type="number"
|
||||||
value={this.props.source.coordinates[idx]}
|
value={props.source.coordinates[idx]}
|
||||||
default={[0, 0]}
|
default={[0, 0]}
|
||||||
onChange={(val: [number, number]) => changeCoord(idx, val)}
|
onChange={(val: [number, number]) => changeCoord(idx, val)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
type GeoJSONSourceUrlEditorProps = {
|
type GeoJSONSourceUrlEditorProps = {
|
||||||
source: {
|
source: {
|
||||||
@@ -230,20 +221,18 @@ type GeoJSONSourceUrlEditorProps = {
|
|||||||
onChange(...args: unknown[]): unknown
|
onChange(...args: unknown[]): unknown
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class GeoJSONSourceUrlEditor extends React.Component<GeoJSONSourceUrlEditorProps> {
|
const GeoJSONSourceUrlEditor: React.FC<GeoJSONSourceUrlEditorProps> = (props) => {
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
|
||||||
return <FieldUrl
|
return <FieldUrl
|
||||||
label={t("GeoJSON URL")}
|
label={t("GeoJSON URL")}
|
||||||
fieldSpec={latest.source_geojson.data}
|
fieldSpec={latest.source_geojson.data}
|
||||||
value={this.props.source.data}
|
value={props.source.data}
|
||||||
onChange={data => this.props.onChange({
|
onChange={data => props.onChange({
|
||||||
...this.props.source,
|
...props.source,
|
||||||
data: data
|
data: data
|
||||||
})}
|
})}
|
||||||
/>;
|
/>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
type GeoJSONSourceFieldJsonEditorProps = {
|
type GeoJSONSourceFieldJsonEditorProps = {
|
||||||
source: {
|
source: {
|
||||||
@@ -253,17 +242,16 @@ type GeoJSONSourceFieldJsonEditorProps = {
|
|||||||
onChange(...args: unknown[]): unknown
|
onChange(...args: unknown[]): unknown
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJsonEditorProps> {
|
const GeoJSONSourceFieldJsonEditor: React.FC<GeoJSONSourceFieldJsonEditorProps> = (props) => {
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
|
||||||
return <div>
|
return <div>
|
||||||
<Block label={t("GeoJSON")} fieldSpec={latest.source_geojson.data}>
|
<Block label={t("GeoJSON")} fieldSpec={latest.source_geojson.data}>
|
||||||
<FieldJson
|
<FieldJson
|
||||||
value={this.props.source.data}
|
value={props.source.data}
|
||||||
lintType="json"
|
lintType="json"
|
||||||
onChange={data => {
|
onChange={data => {
|
||||||
this.props.onChange({
|
props.onChange({
|
||||||
...this.props.source,
|
...props.source,
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -271,17 +259,16 @@ class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJso
|
|||||||
</Block>
|
</Block>
|
||||||
<FieldCheckbox
|
<FieldCheckbox
|
||||||
label={t("Cluster")}
|
label={t("Cluster")}
|
||||||
value={this.props.source.cluster}
|
value={props.source.cluster}
|
||||||
onChange={cluster => {
|
onChange={cluster => {
|
||||||
this.props.onChange({
|
props.onChange({
|
||||||
...this.props.source,
|
...props.source,
|
||||||
cluster: cluster,
|
cluster: cluster,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
type PMTilesSourceEditorProps = {
|
type PMTilesSourceEditorProps = {
|
||||||
source: {
|
source: {
|
||||||
@@ -291,24 +278,22 @@ type PMTilesSourceEditorProps = {
|
|||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class PMTilesSourceEditor extends React.Component<PMTilesSourceEditorProps> {
|
const PMTilesSourceEditor: React.FC<PMTilesSourceEditorProps> = (props) => {
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
|
||||||
return <div>
|
return <div>
|
||||||
<FieldUrl
|
<FieldUrl
|
||||||
label={t("PMTiles URL")}
|
label={t("PMTiles URL")}
|
||||||
fieldSpec={latest.source_vector.url}
|
fieldSpec={latest.source_vector.url}
|
||||||
value={this.props.source.url}
|
value={props.source.url}
|
||||||
data-wd-key="modal:sources.add.source_url"
|
data-wd-key="modal:sources.add.source_url"
|
||||||
onChange={(url: string) => this.props.onChange({
|
onChange={(url: string) => props.onChange({
|
||||||
...this.props.source,
|
...props.source,
|
||||||
url: url.startsWith("pmtiles://") ? url : `pmtiles://${url}`
|
url: url.startsWith("pmtiles://") ? url : `pmtiles://${url}`
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
{this.props.children}
|
{props.children}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
type ModalSourcesTypeEditorInternalProps = {
|
type ModalSourcesTypeEditorInternalProps = {
|
||||||
mode: EditorMode
|
mode: EditorMode
|
||||||
@@ -316,17 +301,16 @@ type ModalSourcesTypeEditorInternalProps = {
|
|||||||
onChange(...args: unknown[]): unknown
|
onChange(...args: unknown[]): unknown
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class ModalSourcesTypeEditorInternal extends React.Component<ModalSourcesTypeEditorInternalProps> {
|
const ModalSourcesTypeEditorInternal: React.FC<ModalSourcesTypeEditorInternalProps> = (props) => {
|
||||||
render() {
|
const t = props.t;
|
||||||
const t = this.props.t;
|
|
||||||
const commonProps = {
|
const commonProps = {
|
||||||
source: this.props.source,
|
source: props.source,
|
||||||
onChange: this.props.onChange,
|
onChange: props.onChange,
|
||||||
t: this.props.t,
|
t: props.t,
|
||||||
i18n: this.props.i18n,
|
i18n: props.i18n,
|
||||||
tReady: this.props.tReady,
|
tReady: props.tReady,
|
||||||
};
|
};
|
||||||
switch(this.props.mode) {
|
switch(props.mode) {
|
||||||
case "geojson_url": return <GeoJSONSourceUrlEditor {...commonProps} />;
|
case "geojson_url": return <GeoJSONSourceUrlEditor {...commonProps} />;
|
||||||
case "geojson_json": return <GeoJSONSourceFieldJsonEditor {...commonProps} />;
|
case "geojson_json": return <GeoJSONSourceFieldJsonEditor {...commonProps} />;
|
||||||
case "tilejson_vector": return <TileJSONSourceEditor {...commonProps} />;
|
case "tilejson_vector": return <TileJSONSourceEditor {...commonProps} />;
|
||||||
@@ -336,11 +320,11 @@ class ModalSourcesTypeEditorInternal extends React.Component<ModalSourcesTypeEdi
|
|||||||
<FieldNumber
|
<FieldNumber
|
||||||
label={t("Tile Size")}
|
label={t("Tile Size")}
|
||||||
fieldSpec={latest.source_raster.tileSize}
|
fieldSpec={latest.source_raster.tileSize}
|
||||||
onChange={tileSize => this.props.onChange({
|
onChange={tileSize => props.onChange({
|
||||||
...this.props.source,
|
...props.source,
|
||||||
tileSize: tileSize
|
tileSize: tileSize
|
||||||
})}
|
})}
|
||||||
value={this.props.source.tileSize || latest.source_raster.tileSize.default}
|
value={props.source.tileSize || latest.source_raster.tileSize.default}
|
||||||
data-wd-key="modal:sources.add.tile_size"
|
data-wd-key="modal:sources.add.tile_size"
|
||||||
/>
|
/>
|
||||||
</TileURLSourceEditor>;
|
</TileURLSourceEditor>;
|
||||||
@@ -349,22 +333,22 @@ class ModalSourcesTypeEditorInternal extends React.Component<ModalSourcesTypeEdi
|
|||||||
<FieldNumber
|
<FieldNumber
|
||||||
label={t("Tile Size")}
|
label={t("Tile Size")}
|
||||||
fieldSpec={latest.source_raster_dem.tileSize}
|
fieldSpec={latest.source_raster_dem.tileSize}
|
||||||
onChange={tileSize => this.props.onChange({
|
onChange={tileSize => props.onChange({
|
||||||
...this.props.source,
|
...props.source,
|
||||||
tileSize: tileSize
|
tileSize: tileSize
|
||||||
})}
|
})}
|
||||||
value={this.props.source.tileSize || latest.source_raster_dem.tileSize.default}
|
value={props.source.tileSize || latest.source_raster_dem.tileSize.default}
|
||||||
data-wd-key="modal:sources.add.tile_size"
|
data-wd-key="modal:sources.add.tile_size"
|
||||||
/>
|
/>
|
||||||
<FieldSelect
|
<FieldSelect
|
||||||
label={t("Encoding")}
|
label={t("Encoding")}
|
||||||
fieldSpec={latest.source_raster_dem.encoding}
|
fieldSpec={latest.source_raster_dem.encoding}
|
||||||
options={Object.keys(latest.source_raster_dem.encoding.values)}
|
options={Object.keys(latest.source_raster_dem.encoding.values)}
|
||||||
onChange={encoding => this.props.onChange({
|
onChange={encoding => props.onChange({
|
||||||
...this.props.source,
|
...props.source,
|
||||||
encoding: encoding
|
encoding: encoding
|
||||||
})}
|
})}
|
||||||
value={this.props.source.encoding || latest.source_raster_dem.encoding.default}
|
value={props.source.encoding || latest.source_raster_dem.encoding.default}
|
||||||
/>
|
/>
|
||||||
</TileURLSourceEditor>;
|
</TileURLSourceEditor>;
|
||||||
case "pmtiles_vector": return <PMTilesSourceEditor {...commonProps} />;
|
case "pmtiles_vector": return <PMTilesSourceEditor {...commonProps} />;
|
||||||
@@ -372,7 +356,6 @@ class ModalSourcesTypeEditorInternal extends React.Component<ModalSourcesTypeEdi
|
|||||||
case "video": return <VideoSourceEditor {...commonProps} />;
|
case "video": return <VideoSourceEditor {...commonProps} />;
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const ModalSourcesTypeEditor = withTranslation()(ModalSourcesTypeEditorInternal);
|
export const ModalSourcesTypeEditor = withTranslation()(ModalSourcesTypeEditorInternal);
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ function createMaplibreExpressionLinter(spec?: StylePropertySpecification) {
|
|||||||
const text = view.state.doc.toString();
|
const text = view.state.doc.toString();
|
||||||
const parsedJson = JSON.parse(text);
|
const parsedJson = JSON.parse(text);
|
||||||
const ast = jsonToAst(text);
|
const ast = jsonToAst(text);
|
||||||
const out = expression.createExpression(parsedJson, "expression", spec);
|
const out = expression.createExpression(parsedJson, spec);
|
||||||
if (out?.result !== "error") {
|
if (out?.result !== "error") {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -5,14 +5,14 @@ import { type LayerSpecification } from "maplibre-gl";
|
|||||||
export function changeType(layer: LayerSpecification, newType: string): LayerSpecification {
|
export function changeType(layer: LayerSpecification, newType: string): LayerSpecification {
|
||||||
const changedPaintProps: LayerSpecification["paint"] = { ...layer.paint };
|
const changedPaintProps: LayerSpecification["paint"] = { ...layer.paint };
|
||||||
Object.keys(changedPaintProps).forEach(propertyName => {
|
Object.keys(changedPaintProps).forEach(propertyName => {
|
||||||
if(!(propertyName in ((latest["paint_" + newType as keyof typeof latest]) as string[]))) {
|
if(!(propertyName in latest["paint_" + newType])) {
|
||||||
delete changedPaintProps[propertyName as keyof LayerSpecification["paint"]];
|
delete changedPaintProps[propertyName as keyof LayerSpecification["paint"]];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const changedLayoutProps: LayerSpecification["layout"] = { ...layer.layout };
|
const changedLayoutProps: LayerSpecification["layout"] = { ...layer.layout };
|
||||||
Object.keys(changedLayoutProps).forEach(propertyName => {
|
Object.keys(changedLayoutProps).forEach(propertyName => {
|
||||||
if(!(propertyName in ((latest["layout_" + newType as keyof typeof latest]) as string[]))) {
|
if(!(propertyName in latest["layout_" + newType])) {
|
||||||
delete changedLayoutProps[propertyName as keyof LayerSpecification["layout"]];
|
delete changedLayoutProps[propertyName as keyof LayerSpecification["layout"]];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user