mirror of
https://github.com/maputnik/editor.git
synced 2026-09-01 09:57:27 +00:00
Compare commits
257 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4f29cf846 | |||
| f9fc37ebed | |||
| 1958358f7d | |||
| 1a34ec21c0 | |||
| 026cf95390 | |||
| 8eabfa5519 | |||
| ad69cbdb20 | |||
| 17eaa3f204 | |||
| 1df2e36dbb | |||
| 0c46affda9 | |||
| 18f45e932b | |||
| 31d56c9fae | |||
| b7838ad6e1 | |||
| c92fd12854 | |||
| eb55796461 | |||
| 4b97f82980 | |||
| 5d0b6e3201 | |||
| 5ab9be9fdb | |||
| 9659d41b83 | |||
| 52f949e152 | |||
| e4d559f953 | |||
| 577663f706 | |||
| 393f4a38b9 | |||
| 3727c9ad5e | |||
| a99cbc00ba | |||
| fe5f7e8b8c | |||
| 3ed4b8f2d7 | |||
| f17c2e8112 | |||
| 2be447f105 | |||
| 2fe6fa2be6 | |||
| 83dd21414b | |||
| 56d96a248d | |||
| 5b1ee7296b | |||
| 8e0546fba4 | |||
| 2ff3d08bb0 | |||
| afe7a492a7 | |||
| 1f26ab707f | |||
| 233191e27c | |||
| 246f9a191d | |||
| 07f6efe45d | |||
| ccd0402eea | |||
| 8ccee0ba75 | |||
| d6b67be7b2 | |||
| ac56ea4627 | |||
| b00cf66ea6 | |||
| 8e329a0ff9 | |||
| 74cacd5bdf | |||
| 7d5fb23130 | |||
| 08bbd55f13 | |||
| d6d4930513 | |||
| 6220e15723 | |||
| 72053a2dba | |||
| bf27a35ef5 | |||
| 4705bf823a | |||
| a8f6208561 | |||
| af2629be75 | |||
| 8bfad6c9fd | |||
| 5c3713da90 | |||
| 174eae1cf4 | |||
| d73add77e7 | |||
| ab00c9f426 | |||
| d6ab302815 | |||
| f5646f57d1 | |||
| c77d8f6625 | |||
| e34c1ca4be | |||
| 87745f1fc9 | |||
| 9ba0fd5f39 | |||
| 70decbb5c1 | |||
| 51fa4a4377 | |||
| fb6f4d73e2 | |||
| 63b14933ba | |||
| a86c31cefa | |||
| 25e2554412 | |||
| 34bb3bc0a7 | |||
| 852243cd52 | |||
| 40faf86adf | |||
| bb69f143b8 | |||
| bb43200887 | |||
| ae3f79f4ad | |||
| 731a315624 | |||
| 5e441454d5 | |||
| a55716bbd9 | |||
| 44aea3745e | |||
| a572bc02a6 | |||
| 4dee95fa2e | |||
| 381ff6292f | |||
| c12db1703b | |||
| 2676583833 | |||
| 6ca2af7f8a | |||
| 553b17822d | |||
| a6148e5f40 | |||
| 4f77629eb7 | |||
| 9103d9560a | |||
| 06c63509f7 | |||
| bbe0af6c0e | |||
| 7455ccc3b7 | |||
| 8b766777ac | |||
| 8441abe907 | |||
| ca56951256 | |||
| 5981151b27 | |||
| 21dbc6c4d9 | |||
| 6f060c2a0a | |||
| 24327541c5 | |||
| 0d6b9ee9d4 | |||
| 3ad487dce7 | |||
| a46c834874 | |||
| 67bdea1827 | |||
| cc4133aac1 | |||
| 4a6f58d61c | |||
| e3dc98b76d | |||
| 09373dda44 | |||
| c4b05b62b3 | |||
| 06bccfab10 | |||
| b83c9a1ad9 | |||
| 0279daf7bd | |||
| bfada7cace | |||
| 6c751fe1c4 | |||
| 34299c94ee | |||
| 5804b3c72a | |||
| 8ae6e9fc61 | |||
| 40579c3e0c | |||
| f3906c8dd8 | |||
| f911ed3522 | |||
| 2cc179acc1 | |||
| 2912db6e32 | |||
| 70eb3e785a | |||
| 8f944d9973 | |||
| 8faf841f3d | |||
| d8ba8fcbfb | |||
| d6f31ec82e | |||
| b19eacf4f9 | |||
| 3d158a791a | |||
| 04b3b42524 | |||
| af92aac7ec | |||
| 90dfbf37e0 | |||
| e21f412933 | |||
| da297fe82c | |||
| 624ccb5b00 | |||
| 9f0e5641ab | |||
| d07b40ccef | |||
| e0abd8251d | |||
| 324452e714 | |||
| 8d3ad6b1a1 | |||
| 3d4cc34a08 | |||
| ff351716b6 | |||
| c963a8cc59 | |||
| 52ad980aef | |||
| fb04cce650 | |||
| 4b8acb10b0 | |||
| 86d67389fc | |||
| 9dad53e444 | |||
| d5afeb14c1 | |||
| 85bb1d4d40 | |||
| d95e25d185 | |||
| a88f2bc0a3 | |||
| 5a4254d300 | |||
| 6bfe2aa364 | |||
| 0acd1fec0a | |||
| 3046fedb55 | |||
| 1574b49b01 | |||
| 4417a2d8f1 | |||
| 1f34e927e7 | |||
| 9af6a537ef | |||
| 6e07142f13 | |||
| d2853f34a4 | |||
| 7faed0d27e | |||
| 22101f93ad | |||
| 0661899d54 | |||
| 862ac84464 | |||
| 1e4aadbb6d | |||
| ce731e7d6b | |||
| 5448cdbe4e | |||
| 315a9b82c0 | |||
| 9e1c0e4c82 | |||
| 7db675e0d1 | |||
| 0aa629164a | |||
| c2ec77e869 | |||
| b28407a4a0 | |||
| e3e6647e03 | |||
| eb0f833d49 | |||
| c5c1dd12b9 | |||
| b7e414a042 | |||
| 81a6f31803 | |||
| 65cd050a18 | |||
| c426dd7349 | |||
| c5af645546 | |||
| 1bf0abfb5a | |||
| 18338de21a | |||
| 857117eb71 | |||
| 8d86bca8b3 | |||
| dc4e6a0925 | |||
| e9d6119ac6 | |||
| cbdf45c852 | |||
| a191c36f96 | |||
| 0a8d0974ca | |||
| 8e6c54564b | |||
| 4bbe2ce1ea | |||
| 1d48ab7ecf | |||
| d85ed36e70 | |||
| b554f4427b | |||
| 184bfeeaf8 | |||
| e45f8d960d | |||
| 1fede3af3a | |||
| 5ad74048bd | |||
| a0a91474de | |||
| c3670701e5 | |||
| 86923330d9 | |||
| 4517148e5a | |||
| 0433d66f45 | |||
| 0c592bacab | |||
| d98637cb12 | |||
| 1070209cb5 | |||
| b6189f77c4 | |||
| 25322a3952 | |||
| 5943c6f282 | |||
| 090a26bb40 | |||
| af03b010a4 | |||
| 578a920b6d | |||
| 0858a16ffc | |||
| 7cfe0563bc | |||
| ee72389534 | |||
| 8f722c59de | |||
| 94d2e958eb | |||
| d931c7cb38 | |||
| 6da83c4670 | |||
| d26af16003 | |||
| d75b86c927 | |||
| a0cd087ccc | |||
| 313b639a5f | |||
| 93c45d5340 | |||
| 3be6cb5926 | |||
| 9d151fdc1f | |||
| 44d1a7a6b0 | |||
| 0e5676eae0 | |||
| b8739915b2 | |||
| a1dedd1aa6 | |||
| 33b4a40c35 | |||
| a624909819 | |||
| d5d387f349 | |||
| c58ae0f895 | |||
| c9e360d675 | |||
| 75ece350bd | |||
| 45680151ef | |||
| 87bae82b17 | |||
| fcad636f85 | |||
| bac8495b3c | |||
| df98cb9c7b | |||
| 34c3015b42 | |||
| ca7bf9f4a7 | |||
| 61ba399e1c | |||
| b5c09a4f17 | |||
| 6f83839a4c | |||
| 74b47e7e74 | |||
| 6b45dc8b4d | |||
| 2a832955c4 | |||
| 608b836fe0 | |||
| de9c4fcc4a |
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": [
|
|
||||||
"@babel/preset-env",
|
|
||||||
"@babel/preset-react"
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"static-fs",
|
|
||||||
"react-hot-loader/babel",
|
|
||||||
"@babel/plugin-proposal-class-properties"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"test": {
|
|
||||||
"plugins": [
|
|
||||||
["istanbul", {
|
|
||||||
"exclude": ["node_modules/**", "test/**"]
|
|
||||||
}]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
version: 2
|
|
||||||
templates:
|
|
||||||
# Test the build **only** no webdriver
|
|
||||||
build-steps: &build-steps
|
|
||||||
- checkout
|
|
||||||
- run:
|
|
||||||
name: "Create artifacts directory"
|
|
||||||
command: mkdir /tmp/artifacts
|
|
||||||
- restore_cache:
|
|
||||||
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
|
|
||||||
|
|
||||||
- run: npm install
|
|
||||||
|
|
||||||
- save_cache:
|
|
||||||
paths:
|
|
||||||
- node_modules
|
|
||||||
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
|
|
||||||
|
|
||||||
- run: mkdir -p /tmp/artifacts/logs
|
|
||||||
- run: npm run build
|
|
||||||
- run: npm run lint-js
|
|
||||||
- run: npm run lint-css
|
|
||||||
- store_artifacts:
|
|
||||||
path: /tmp/artifacts
|
|
||||||
destination: /artifacts
|
|
||||||
# Test in webdriver
|
|
||||||
wdio-steps: &wdio-steps
|
|
||||||
- checkout
|
|
||||||
- run:
|
|
||||||
name: "Create artifacts directory"
|
|
||||||
command: mkdir /tmp/artifacts
|
|
||||||
- restore_cache:
|
|
||||||
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
|
|
||||||
|
|
||||||
- run: npm install
|
|
||||||
|
|
||||||
- save_cache:
|
|
||||||
paths:
|
|
||||||
- node_modules
|
|
||||||
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
|
|
||||||
|
|
||||||
- run: mkdir -p /tmp/artifacts/logs
|
|
||||||
- run: npm run build
|
|
||||||
- run: npm run profiling-build
|
|
||||||
- run: npm run lint-js
|
|
||||||
- run: npm run lint-css
|
|
||||||
- run: DOCKER_HOST=localhost npm test
|
|
||||||
- run: ./node_modules/.bin/istanbul report --include /tmp/artifacts/coverage/coverage.json --dir /tmp/artifacts/coverage html lcov
|
|
||||||
- store_artifacts:
|
|
||||||
path: /tmp/artifacts
|
|
||||||
destination: /artifacts
|
|
||||||
jobs:
|
|
||||||
build-linux-node-v10:
|
|
||||||
docker:
|
|
||||||
- image: node:10
|
|
||||||
- image: selenium/standalone-chrome:3.141.59
|
|
||||||
working_directory: ~/repo-linux-node-v10
|
|
||||||
steps: *wdio-steps
|
|
||||||
build-linux-node-v12:
|
|
||||||
docker:
|
|
||||||
- image: node:12
|
|
||||||
working_directory: ~/repo-linux-node-v12
|
|
||||||
steps: *build-steps
|
|
||||||
build-linux-node-v13:
|
|
||||||
docker:
|
|
||||||
- image: node:13
|
|
||||||
working_directory: ~/repo-linux-node-v13
|
|
||||||
steps: *build-steps
|
|
||||||
build-osx-node-v10:
|
|
||||||
macos:
|
|
||||||
xcode: "9.0"
|
|
||||||
dependencies:
|
|
||||||
override:
|
|
||||||
- brew install node@10
|
|
||||||
working_directory: ~/repo-osx-node-v10
|
|
||||||
steps: *build-steps
|
|
||||||
build-osx-node-v12:
|
|
||||||
macos:
|
|
||||||
xcode: "9.0"
|
|
||||||
dependencies:
|
|
||||||
override:
|
|
||||||
- brew install node@12
|
|
||||||
working_directory: ~/repo-osx-node-v12
|
|
||||||
steps: *build-steps
|
|
||||||
build-osx-node-v13:
|
|
||||||
macos:
|
|
||||||
xcode: "9.0"
|
|
||||||
dependencies:
|
|
||||||
override:
|
|
||||||
- brew install node@13
|
|
||||||
working_directory: ~/repo-osx-node-v13
|
|
||||||
steps: *build-steps
|
|
||||||
|
|
||||||
workflows:
|
|
||||||
version: 2
|
|
||||||
build:
|
|
||||||
jobs:
|
|
||||||
- build-linux-node-v10
|
|
||||||
- build-linux-node-v12
|
|
||||||
- build-linux-node-v13
|
|
||||||
- build-osx-node-v10
|
|
||||||
- build-osx-node-v12
|
|
||||||
- build-osx-node-v13
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"packages": [],
|
||||||
|
"sandboxes": ["/"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
Dockerfile
|
||||||
|
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# COPIED FROM .gitignore , please keep it in sync
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directory
|
||||||
|
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Ignore build files
|
||||||
|
public
|
||||||
|
/errorShots
|
||||||
|
/old
|
||||||
|
/build
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:react/jsx-runtime',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||||
|
settings: { react: { version: '18.2' } },
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve Maputnik
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Thanks for your feedback! Please complete the following information: -->
|
||||||
|
|
||||||
|
**Maputnik version**:<!-- e.g v1.7.0, main -->
|
||||||
|
**Browser**:
|
||||||
|
**OS**:<!-- (Windows, macOS, Linux) -->
|
||||||
|
|
||||||
|
**Description of the bug**:
|
||||||
|
|
||||||
|
**Steps to reproduce the behavior**:
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
|
||||||
|
**Style file or style URL**:
|
||||||
|
<!-- If applicable, attach a style file (zip) or provide a style URL. -->
|
||||||
|
|
||||||
|
**Screenshots**:
|
||||||
|
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
name: Other issue
|
||||||
|
about: Feature request or other issue which is no bug report
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Thanks for reaching out! If you are having general Maputnik mapping questions, please asking them at https://gis.stackexchange.com/ using the 'maputnik' tag https://gis.stackexchange.com/questions/tagged/maputnik and read https://gis.stackexchange.com/help/how-to-ask before you do so (please keep in mind that you're asking there in a general GIS forum, not a dedicated support channel) -->
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
open-pull-requests-limit: 2
|
||||||
|
versioning-strategy: increase
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
open-pull-requests-limit: 2
|
||||||
|
versioning-strategy: increase
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
name: ci
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-docker:
|
||||||
|
name: build/docker
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:main .
|
||||||
|
|
||||||
|
# build the editor
|
||||||
|
build-node:
|
||||||
|
name: "build/node@${{ matrix.node-version }} (${{ matrix.os }})"
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
node-version: [18.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
- uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-node-
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run build
|
||||||
|
|
||||||
|
|
||||||
|
build-artifacts:
|
||||||
|
name: "build/artifacts (${{ matrix.os }})"
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
node-version: [18.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
- uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-node-
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run build
|
||||||
|
- run: npm run build-storybook
|
||||||
|
- name: artifacts/editor
|
||||||
|
uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: editor
|
||||||
|
path: dist
|
||||||
|
- name: artifacts/storybook
|
||||||
|
uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: storybook
|
||||||
|
path: build/storybook
|
||||||
|
|
||||||
|
# Build and upload desktop CLI artifacts
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ^1.19.x
|
||||||
|
id: go
|
||||||
|
|
||||||
|
- name: Check out code into the Go module directory
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: maputnik/desktop
|
||||||
|
ref: master
|
||||||
|
path: ./src/github.com/maputnik/desktop/
|
||||||
|
|
||||||
|
- name: Make
|
||||||
|
run: cd src/github.com/maputnik/desktop/ && make
|
||||||
|
|
||||||
|
- name: Artifacts/linux
|
||||||
|
uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: maputnik-linux
|
||||||
|
path: ./src/github.com/maputnik/desktop/bin/linux/
|
||||||
|
|
||||||
|
- name: Artifacts/darwin
|
||||||
|
uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: maputnik-darwin
|
||||||
|
path: ./src/github.com/maputnik/desktop/bin/darwin/
|
||||||
|
|
||||||
|
- name: Artifacts/windows
|
||||||
|
uses: actions/upload-artifact@v1
|
||||||
|
with:
|
||||||
|
name: maputnik-windows
|
||||||
|
path: ./src/github.com/maputnik/desktop/bin/windows/
|
||||||
|
|
||||||
|
cypress-run:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
browser: [chrome, firefox]
|
||||||
|
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- run: npm ci
|
||||||
|
- name: Cypress run
|
||||||
|
uses: cypress-io/github-action@v6
|
||||||
|
with:
|
||||||
|
build: npm run build
|
||||||
|
start: npm run start
|
||||||
|
browser: ${{ matrix.browser }}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
name: deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# publish docker to GitHub registry
|
||||||
|
deploy-docker:
|
||||||
|
name: deploy/docker
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
if: ${{ github.event_name == 'push' }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u orangemug --password-stdin
|
||||||
|
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:main .
|
||||||
|
- run: docker push docker.pkg.github.com/maputnik/editor/editor:main
|
||||||
@@ -33,3 +33,5 @@ public
|
|||||||
/errorShots
|
/errorShots
|
||||||
/old
|
/old
|
||||||
/build
|
/build
|
||||||
|
/cypress/screenshots
|
||||||
|
/dist/
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
const config = {
|
||||||
|
stories: ['../stories/**/*.stories.jsx'],
|
||||||
|
addons: ['@storybook/addon-actions', '@storybook/addon-links', '@storybook/addon-a11y/register', '@storybook/addon-storysource'],
|
||||||
|
framework: {
|
||||||
|
name: '@storybook/react-vite',
|
||||||
|
options: {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { addons } from '@storybook/addons';
|
||||||
|
import { themes } from '@storybook/theming';
|
||||||
|
import theme from './maputnik.theme';
|
||||||
|
|
||||||
|
addons.setConfig({
|
||||||
|
theme: theme,
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { create } from '@storybook/theming/create';
|
||||||
|
|
||||||
|
export default create({
|
||||||
|
base: 'light',
|
||||||
|
|
||||||
|
brandTitle: 'Maputnik',
|
||||||
|
brandUrl: 'https://github.com/maputnik/editor',
|
||||||
|
});
|
||||||
+18
-17
@@ -1,21 +1,22 @@
|
|||||||
FROM node:10-slim
|
FROM node:18 as builder
|
||||||
|
WORKDIR /maputnik
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
# Only copy package.json to prevent npm install from running on every build
|
||||||
git \
|
COPY package.json package-lock.json ./
|
||||||
python \
|
RUN npm install
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
EXPOSE 8888
|
# Build maputnik
|
||||||
|
# TODO: we should also do a npm run test here (needs more dependencies)
|
||||||
ENV HOME /maputnik
|
COPY . .
|
||||||
RUN mkdir ${HOME}
|
|
||||||
|
|
||||||
COPY . ${HOME}/
|
|
||||||
|
|
||||||
WORKDIR ${HOME}
|
|
||||||
|
|
||||||
RUN npm install -d
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
WORKDIR ${HOME}/build/build
|
#---------------------------------------------------------------------------
|
||||||
CMD python -m SimpleHTTPServer 8888
|
|
||||||
|
# Create a clean python-based image with just the build results
|
||||||
|
FROM python:3-slim
|
||||||
|
WORKDIR /maputnik
|
||||||
|
|
||||||
|
COPY --from=builder /maputnik/dist .
|
||||||
|
|
||||||
|
EXPOSE 8888
|
||||||
|
CMD python -m http.server 8888
|
||||||
|
|||||||
@@ -1,32 +1,30 @@
|
|||||||
# Maputnik
|
<img width="200" alt="Maputnik logo" src="https://cdn.jsdelivr.net/gh/maputnik/design/logos/logo-color.png" />
|
||||||
|
|
||||||
[][circleci]
|
# Maputnik
|
||||||
[][appveyor]
|
[][github-action-ci]
|
||||||
[][dm-prod]
|
|
||||||
[][dm-dev]
|
|
||||||
[][license]
|
[][license]
|
||||||
|
|
||||||
[circleci]: https://circleci.com/gh/maputnik/editor/tree/master
|
[github-action-ci]: https://github.com/maputnik/editor/actions?query=workflow%3Aci
|
||||||
[appveyor]: https://ci.appveyor.com/project/lukasmartinelli/editor
|
[license]: https://tldrlegal.com/license/mit-license
|
||||||
[dm-prod]: https://david-dm.org/maputnik/editor
|
|
||||||
[dm-dev]: https://david-dm.org/maputnik/editor?type=dev
|
|
||||||
[license]: https://tldrlegal.com/license/mit-license
|
|
||||||
|
|
||||||
<img width="200" align="right" alt="Maputnik" src="https://cdn.jsdelivr.net/gh/maputnik/editor@1.5.0/src/img/maputnik.png" />
|
A free and open visual editor for the [MapLibre GL styles](https://maplibre.org/maplibre-style-spec/)
|
||||||
|
|
||||||
A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/mapbox-gl-style-spec/)
|
|
||||||
targeted at developers and map designers.
|
targeted at developers and map designers.
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
|
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
|
||||||
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
|
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
|
||||||
|
- In a Docker, run this command and browse to http://localhost:8888, Ctrl+C to stop the server.
|
||||||
|
|
||||||
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independence is an OSS map designer.
|
```bash
|
||||||
|
docker run -it --rm -p 8888:8888 maputnik/editor
|
||||||
|
```
|
||||||
|
|
||||||
## Donations
|
## Donations
|
||||||
|
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independence is an OSS map designer.
|
||||||
If you or your organisation has seen value from Maputnik, please consider donating at <https://maputnik.github.io/donate>
|
If you or your organisation has seen value from Maputnik, please consider donating at <https://maputnik.github.io/donate>
|
||||||
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate!
|
The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate!
|
||||||
@@ -38,7 +36,7 @@ The documentation can be found in the [Wiki](https://github.com/maputnik/editor/
|
|||||||
|
|
||||||
## Develop
|
## Develop
|
||||||
|
|
||||||
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react) and [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/api/).
|
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react) and [MapLibre GL JS](https://maplibre.org/projects/maplibre-gl-js/).
|
||||||
|
|
||||||
We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule).
|
We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule).
|
||||||
|
|
||||||
@@ -48,21 +46,17 @@ Install the deps, start the dev server and open the web browser on `http://local
|
|||||||
# install dependencies
|
# install dependencies
|
||||||
npm install
|
npm install
|
||||||
# start dev server
|
# start dev server
|
||||||
npm start
|
npm run start
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want Maputnik to be accessible externally use the [`--host` option](https://webpack.js.org/configuration/dev-server/#devserverhost):
|
If you want Maputnik to be accessible externally use the [`--host` option](https://vitejs.dev/config/server-options.html#server-host):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# start externally accessible dev server
|
# start externally accessible dev server
|
||||||
npm start -- --host 0.0.0.0
|
npm run start -- --host 0.0.0.0
|
||||||
```
|
```
|
||||||
|
|
||||||
The build process will watch for changes to the filesystem, rebuild and autoreload the editor. However note this from the [webpack-dev-server docs](https://webpack.js.org/configuration/dev-server/):
|
The build process will watch for changes to the filesystem, rebuild and autoreload the editor.
|
||||||
|
|
||||||
> webpack uses the file system to get notified of file changes. In some cases this does not work. For example, when using Network File System (NFS). Vagrant also has a lot of problems with this. ([snippet source](https://webpack.js.org/configuration/dev-server/#devserverwatchoptions-))
|
|
||||||
|
|
||||||
To enable polling add `export WEBPACK_DEV_SERVER_POLLING=1` to your environment.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
npm run build
|
npm run build
|
||||||
@@ -78,34 +72,32 @@ npm run lint-styles
|
|||||||
|
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
For testing we use [webdriverio](http://webdriver.io) and [selenium-standalone](https://github.com/vvo/selenium-standalone)
|
For E2E testing we use [Cypress](https://www.cypress.io/)
|
||||||
|
|
||||||
[selenium-standalone](https://github.com/vvo/selenium-standalone) starts a server that will launch browsers on your local machine. We use chrome so you **must** have chrome installed on your machine.
|
[Cypress](https://www.cypress.io/) doesn't starts a server so you'll need to start one manually by running `npm run start`.
|
||||||
|
|
||||||
Now open a terminal and run the following. This will install the drivers on your local machine
|
Now open a terminal and run the following using *chrome*:
|
||||||
|
|
||||||
```
|
```
|
||||||
./node_modules/.bin/selenium-standalone install
|
npm run test
|
||||||
|
```
|
||||||
|
or *firefox*:
|
||||||
|
```
|
||||||
|
npm run test -- --browser firefox
|
||||||
```
|
```
|
||||||
|
|
||||||
Now start the standalone server
|
See the following docs for more info: (Launching Browsers)[https://docs.cypress.io/guides/guides/launching-browsers]
|
||||||
|
|
||||||
|
You can also see the tests as they run or select which suites to run by executing:
|
||||||
|
|
||||||
```
|
```
|
||||||
./node_modules/.bin/selenium-standalone start
|
npm run cy:open
|
||||||
```
|
```
|
||||||
|
|
||||||
Then open another terminal and run
|
|
||||||
|
|
||||||
```
|
|
||||||
npm test
|
|
||||||
```
|
|
||||||
|
|
||||||
After some time you should see a browser launch which will be automated by the test runner.
|
|
||||||
|
|
||||||
|
|
||||||
## Related Projects
|
## Related Projects
|
||||||
|
|
||||||
- [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik.
|
- [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik.
|
||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
@@ -165,6 +157,5 @@ Sina Martinelli, Nicholas Doiron, Neil Cawse, Urs42, Benedikt Groß, Manuel Roth
|
|||||||
|
|
||||||
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and contributors.
|
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and contributors.
|
||||||
|
|
||||||
**Disclaimer** This project is not affiliated with Mapbox or Mapbox Studio. It is an independent style editor for the
|
**Disclaimer** This is an independent style editor.
|
||||||
open source technology in the Mapbox GL ecosystem.
|
|
||||||
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by Mapbox Studio and make your own decisions for a good style editor.
|
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by Mapbox Studio and make your own decisions for a good style editor.
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
image: Visual Studio 2019
|
|
||||||
environment:
|
|
||||||
matrix:
|
|
||||||
- nodejs_version: "10"
|
|
||||||
- nodejs_version: "12"
|
|
||||||
- nodejs_version: "13"
|
|
||||||
platform:
|
|
||||||
- x86
|
|
||||||
- x64
|
|
||||||
install:
|
|
||||||
# https://github.com/appveyor/ci/issues/2921#issuecomment-501016533
|
|
||||||
- ps: |
|
|
||||||
try {
|
|
||||||
Install-Product node $env:nodejs_version $env:platform
|
|
||||||
} catch {
|
|
||||||
echo "Unable to install node $env:nodejs_version, trying update..."
|
|
||||||
Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) $env:platform
|
|
||||||
}
|
|
||||||
- md public
|
|
||||||
- npm install --global windows-build-tools
|
|
||||||
- npm install
|
|
||||||
build_script:
|
|
||||||
- npm run build
|
|
||||||
test_script:
|
|
||||||
- npm run lint-js
|
|
||||||
- npm run lint-css
|
|
||||||
@@ -1,291 +0,0 @@
|
|||||||
var webpack = require("webpack");
|
|
||||||
var WebpackDevServer = require("webpack-dev-server");
|
|
||||||
var webpackConfig = require("./webpack.config");
|
|
||||||
var testConfig = require("../test/config/specs");
|
|
||||||
var artifacts = require("../test/artifacts");
|
|
||||||
var isDocker = require("is-docker");
|
|
||||||
|
|
||||||
|
|
||||||
var server;
|
|
||||||
var SCREENSHOT_PATH = artifacts.pathSync("screenshots");
|
|
||||||
|
|
||||||
exports.config = {
|
|
||||||
//
|
|
||||||
// ====================
|
|
||||||
// Runner Configuration
|
|
||||||
// ====================
|
|
||||||
//
|
|
||||||
// WebdriverIO allows it to run your tests in arbitrary locations (e.g. locally or
|
|
||||||
// on a remote machine).
|
|
||||||
runner: 'local',
|
|
||||||
//
|
|
||||||
// ==================
|
|
||||||
// Specify Test Files
|
|
||||||
// ==================
|
|
||||||
// Define which test specs should run. The pattern is relative to the directory
|
|
||||||
// from which `wdio` was called. Notice that, if you are calling `wdio` from an
|
|
||||||
// NPM script (see https://docs.npmjs.com/cli/run-script) then the current working
|
|
||||||
// directory is where your package.json resides, so `wdio` will be called from there.
|
|
||||||
//
|
|
||||||
specs: [
|
|
||||||
'./test/functional/index.js'
|
|
||||||
],
|
|
||||||
// Patterns to exclude.
|
|
||||||
exclude: [
|
|
||||||
// 'path/to/excluded/files'
|
|
||||||
],
|
|
||||||
//
|
|
||||||
// ============
|
|
||||||
// Capabilities
|
|
||||||
// ============
|
|
||||||
// Define your capabilities here. WebdriverIO can run multiple capabilities at the same
|
|
||||||
// time. Depending on the number of capabilities, WebdriverIO launches several test
|
|
||||||
// sessions. Within your capabilities you can overwrite the spec and exclude options in
|
|
||||||
// order to group specific specs to a specific capability.
|
|
||||||
//
|
|
||||||
// First, you can define how many instances should be started at the same time. Let's
|
|
||||||
// say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have
|
|
||||||
// set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec
|
|
||||||
// files and you set maxInstances to 10, all spec files will get tested at the same time
|
|
||||||
// and 30 processes will get spawned. The property handles how many capabilities
|
|
||||||
// from the same test should run tests.
|
|
||||||
//
|
|
||||||
maxInstances: 10,
|
|
||||||
//
|
|
||||||
// If you have trouble getting all important capabilities together, check out the
|
|
||||||
// Sauce Labs platform configurator - a great tool to configure your capabilities:
|
|
||||||
// https://docs.saucelabs.com/reference/platforms-configurator
|
|
||||||
//
|
|
||||||
capabilities: [{
|
|
||||||
// maxInstances can get overwritten per capability. So if you have an in-house Selenium
|
|
||||||
// grid with only 5 firefox instances available you can make sure that not more than
|
|
||||||
// 5 instances get started at a time.
|
|
||||||
maxInstances: 5,
|
|
||||||
//
|
|
||||||
browserName: 'chrome',
|
|
||||||
// If outputDir is provided WebdriverIO can capture driver session logs
|
|
||||||
// it is possible to configure which logTypes to include/exclude.
|
|
||||||
// excludeDriverLogs: ['*'], // pass '*' to exclude all driver session logs
|
|
||||||
// excludeDriverLogs: ['bugreport', 'server'],
|
|
||||||
}],
|
|
||||||
//
|
|
||||||
// ===================
|
|
||||||
// Test Configurations
|
|
||||||
// ===================
|
|
||||||
// Define all options that are relevant for the WebdriverIO instance here
|
|
||||||
//
|
|
||||||
// Level of logging verbosity: trace | debug | info | warn | error | silent
|
|
||||||
logLevel: 'info',
|
|
||||||
//
|
|
||||||
// Set specific log levels per logger
|
|
||||||
// loggers:
|
|
||||||
// - webdriver, webdriverio
|
|
||||||
// - @wdio/applitools-service, @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service
|
|
||||||
// - @wdio/mocha-framework, @wdio/jasmine-framework
|
|
||||||
// - @wdio/local-runner, @wdio/lambda-runner
|
|
||||||
// - @wdio/sumologic-reporter
|
|
||||||
// - @wdio/cli, @wdio/config, @wdio/sync, @wdio/utils
|
|
||||||
// Level of logging verbosity: trace | debug | info | warn | error | silent
|
|
||||||
// logLevels: {
|
|
||||||
// webdriver: 'debug',
|
|
||||||
// '@wdio/applitools-service': 'info'
|
|
||||||
// },
|
|
||||||
//
|
|
||||||
// If you only want to run your tests until a specific amount of tests have failed use
|
|
||||||
// bail (default is 0 - don't bail, run all tests).
|
|
||||||
bail: 0,
|
|
||||||
//
|
|
||||||
screenshotPath: SCREENSHOT_PATH,
|
|
||||||
// Note: This is here because @orangemug currently runs Maputnik inside a docker container.
|
|
||||||
hostname: process.env.DOCKER_HOST || "0.0.0.0",
|
|
||||||
// Set a base URL in order to shorten url command calls. If your `url` parameter starts
|
|
||||||
// with `/`, the base url gets prepended, not including the path portion of your baseUrl.
|
|
||||||
// If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url
|
|
||||||
// gets prepended directly.
|
|
||||||
baseUrl: 'http://localhost',
|
|
||||||
//
|
|
||||||
// Default timeout for all waitFor* commands.
|
|
||||||
waitforTimeout: 10000,
|
|
||||||
//
|
|
||||||
// Default timeout in milliseconds for request
|
|
||||||
// if Selenium Grid doesn't send response
|
|
||||||
connectionRetryTimeout: 90000,
|
|
||||||
//
|
|
||||||
// Default request retries count
|
|
||||||
connectionRetryCount: 3,
|
|
||||||
//
|
|
||||||
// Test runner services
|
|
||||||
// Services take over a specific job you don't want to take care of. They enhance
|
|
||||||
// your test setup with almost no effort. Unlike plugins, they don't add new
|
|
||||||
// commands. Instead, they hook themselves up into the test process.
|
|
||||||
//
|
|
||||||
// Framework you want to run your specs with.
|
|
||||||
// The following are supported: Mocha, Jasmine, and Cucumber
|
|
||||||
// see also: https://webdriver.io/docs/frameworks.html
|
|
||||||
//
|
|
||||||
// Make sure you have the wdio adapter package for the specific framework installed
|
|
||||||
// before running any tests.
|
|
||||||
framework: 'mocha',
|
|
||||||
//
|
|
||||||
// The number of times to retry the entire specfile when it fails as a whole
|
|
||||||
// specFileRetries: 1,
|
|
||||||
//
|
|
||||||
// Test reporter for stdout.
|
|
||||||
// The only one supported by default is 'dot'
|
|
||||||
// see also: https://webdriver.io/docs/dot-reporter.html
|
|
||||||
reporters: ['spec'],
|
|
||||||
|
|
||||||
//
|
|
||||||
// Options to be passed to Mocha.
|
|
||||||
// See the full list at http://mochajs.org/
|
|
||||||
mochaOpts: {
|
|
||||||
ui: 'bdd',
|
|
||||||
// Because we don't know how long the initial build will take...
|
|
||||||
timeout: 4*60*1000
|
|
||||||
},
|
|
||||||
onPrepare: function (config, capabilities) {
|
|
||||||
return new Promise(function(resolve, reject) {
|
|
||||||
var compiler = webpack(webpackConfig);
|
|
||||||
const serverHost = isDocker() ? "0.0.0.0" : "localhost";
|
|
||||||
|
|
||||||
server = new WebpackDevServer(compiler, {
|
|
||||||
host: serverHost,
|
|
||||||
stats: {
|
|
||||||
colors: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen(testConfig.port, serverHost, function(err) {
|
|
||||||
if(err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onComplete: function(exitCode) {
|
|
||||||
server.close()
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// =====
|
|
||||||
// Hooks
|
|
||||||
// =====
|
|
||||||
// WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
|
|
||||||
// it and to build services around it. You can either apply a single function or an array of
|
|
||||||
// methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
|
|
||||||
// resolved to continue.
|
|
||||||
/**
|
|
||||||
* Gets executed once before all workers get launched.
|
|
||||||
* @param {Object} config wdio configuration object
|
|
||||||
* @param {Array.<Object>} capabilities list of capabilities details
|
|
||||||
*/
|
|
||||||
// onPrepare: function (config, capabilities) {
|
|
||||||
// },
|
|
||||||
/**
|
|
||||||
* Gets executed just before initialising the webdriver session and test framework. It allows you
|
|
||||||
* to manipulate configurations depending on the capability or spec.
|
|
||||||
* @param {Object} config wdio configuration object
|
|
||||||
* @param {Array.<Object>} capabilities list of capabilities details
|
|
||||||
* @param {Array.<String>} specs List of spec file paths that are to be run
|
|
||||||
*/
|
|
||||||
// beforeSession: function (config, capabilities, specs) {
|
|
||||||
// },
|
|
||||||
/**
|
|
||||||
* Gets executed before test execution begins. At this point you can access to all global
|
|
||||||
* variables like `browser`. It is the perfect place to define custom commands.
|
|
||||||
* @param {Array.<Object>} capabilities list of capabilities details
|
|
||||||
* @param {Array.<String>} specs List of spec file paths that are to be run
|
|
||||||
*/
|
|
||||||
// before: function (capabilities, specs) {
|
|
||||||
// },
|
|
||||||
/**
|
|
||||||
* Runs before a WebdriverIO command gets executed.
|
|
||||||
* @param {String} commandName hook command name
|
|
||||||
* @param {Array} args arguments that command would receive
|
|
||||||
*/
|
|
||||||
// beforeCommand: function (commandName, args) {
|
|
||||||
// },
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook that gets executed before the suite starts
|
|
||||||
* @param {Object} suite suite details
|
|
||||||
*/
|
|
||||||
// beforeSuite: function (suite) {
|
|
||||||
// },
|
|
||||||
/**
|
|
||||||
* Function to be executed before a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
|
|
||||||
* @param {Object} test test details
|
|
||||||
*/
|
|
||||||
// beforeTest: function (test) {
|
|
||||||
// },
|
|
||||||
/**
|
|
||||||
* Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling
|
|
||||||
* beforeEach in Mocha)
|
|
||||||
*/
|
|
||||||
// beforeHook: function () {
|
|
||||||
// },
|
|
||||||
/**
|
|
||||||
* Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling
|
|
||||||
* afterEach in Mocha)
|
|
||||||
*/
|
|
||||||
// afterHook: function () {
|
|
||||||
// },
|
|
||||||
/**
|
|
||||||
* Function to be executed after a test (in Mocha/Jasmine) or a step (in Cucumber) starts.
|
|
||||||
* @param {Object} test test details
|
|
||||||
*/
|
|
||||||
// afterTest: function (test) {
|
|
||||||
// },
|
|
||||||
/**
|
|
||||||
* Hook that gets executed after the suite has ended
|
|
||||||
* @param {Object} suite suite details
|
|
||||||
*/
|
|
||||||
// afterSuite: function (suite) {
|
|
||||||
// },
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs after a WebdriverIO command gets executed
|
|
||||||
* @param {String} commandName hook command name
|
|
||||||
* @param {Array} args arguments that command would receive
|
|
||||||
* @param {Number} result 0 - command success, 1 - command error
|
|
||||||
* @param {Object} error error object if any
|
|
||||||
*/
|
|
||||||
// afterCommand: function (commandName, args, result, error) {
|
|
||||||
// },
|
|
||||||
/**
|
|
||||||
* Gets executed after all tests are done. You still have access to all global variables from
|
|
||||||
* the test.
|
|
||||||
* @param {Number} result 0 - test pass, 1 - test fail
|
|
||||||
* @param {Array.<Object>} capabilities list of capabilities details
|
|
||||||
* @param {Array.<String>} specs List of spec file paths that ran
|
|
||||||
*/
|
|
||||||
// after: function (result, capabilities, specs) {
|
|
||||||
// },
|
|
||||||
/**
|
|
||||||
* Gets executed right after terminating the webdriver session.
|
|
||||||
* @param {Object} config wdio configuration object
|
|
||||||
* @param {Array.<Object>} capabilities list of capabilities details
|
|
||||||
* @param {Array.<String>} specs List of spec file paths that ran
|
|
||||||
*/
|
|
||||||
// afterSession: function (config, capabilities, specs) {
|
|
||||||
// },
|
|
||||||
/**
|
|
||||||
* Gets executed after all workers got shut down and the process is about to exit. An error
|
|
||||||
* thrown in the onComplete hook will result in the test run failing.
|
|
||||||
* @param {Object} exitCode 0 - success, 1 - fail
|
|
||||||
* @param {Object} config wdio configuration object
|
|
||||||
* @param {Array.<Object>} capabilities list of capabilities details
|
|
||||||
* @param {<Object>} results object containing test results
|
|
||||||
*/
|
|
||||||
// onComplete: function(exitCode, config, capabilities, results) {
|
|
||||||
// },
|
|
||||||
/**
|
|
||||||
* Gets executed when a refresh happens.
|
|
||||||
* @param {String} oldSessionId session ID of the old session
|
|
||||||
* @param {String} newSessionId session ID of the new session
|
|
||||||
*/
|
|
||||||
//onReload: function(oldSessionId, newSessionId) {
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var webpack = require('webpack');
|
|
||||||
var path = require('path');
|
|
||||||
var rules = require('./webpack.rules');
|
|
||||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
var HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin');
|
|
||||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
|
||||||
|
|
||||||
const HOST = process.env.HOST || "127.0.0.1";
|
|
||||||
const PORT = process.env.PORT || "8888";
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
target: 'web',
|
|
||||||
mode: 'development',
|
|
||||||
entry: [
|
|
||||||
`webpack-dev-server/client?http://${HOST}:${PORT}`,
|
|
||||||
`webpack/hot/only-dev-server`,
|
|
||||||
`./src/index.jsx` // Your appʼs entry point
|
|
||||||
],
|
|
||||||
devtool: process.env.WEBPACK_DEVTOOL || 'cheap-module-source-map',
|
|
||||||
output: {
|
|
||||||
path: path.join(__dirname, '..', 'public'),
|
|
||||||
filename: 'bundle.js'
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.js', '.jsx']
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
noParse: [
|
|
||||||
/mapbox-gl\/dist\/mapbox-gl.js/
|
|
||||||
],
|
|
||||||
rules: rules
|
|
||||||
},
|
|
||||||
node: {
|
|
||||||
fs: "empty",
|
|
||||||
net: 'empty',
|
|
||||||
tls: 'empty'
|
|
||||||
},
|
|
||||||
devServer: {
|
|
||||||
contentBase: "./public",
|
|
||||||
// do not print bundle build stats
|
|
||||||
noInfo: true,
|
|
||||||
// enable HMR
|
|
||||||
hot: true,
|
|
||||||
// embed the webpack-dev-server runtime into the bundle
|
|
||||||
inline: true,
|
|
||||||
// serve index.html in place of 404 responses to allow HTML5 history
|
|
||||||
historyApiFallback: true,
|
|
||||||
port: PORT,
|
|
||||||
host: HOST,
|
|
||||||
watchOptions: {
|
|
||||||
// Disabled polling by default as it causes lots of CPU usage and hence drains laptop batteries. To enable polling add WEBPACK_DEV_SERVER_POLLING to your environment
|
|
||||||
// See <https://webpack.js.org/configuration/watch/#watchoptions-poll> for details
|
|
||||||
poll: (!!process.env.WEBPACK_DEV_SERVER_POLLING ? true : false),
|
|
||||||
watch: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new webpack.NoEmitOnErrorsPlugin(),
|
|
||||||
new webpack.HotModuleReplacementPlugin(),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
title: 'Maputnik',
|
|
||||||
template: './src/template.html'
|
|
||||||
}),
|
|
||||||
new HtmlWebpackInlineSVGPlugin({
|
|
||||||
runPreEmit: true,
|
|
||||||
}),
|
|
||||||
new CopyWebpackPlugin([
|
|
||||||
{
|
|
||||||
from: './src/manifest.json',
|
|
||||||
to: 'manifest.json'
|
|
||||||
}
|
|
||||||
])
|
|
||||||
]
|
|
||||||
};
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
var webpack = require('webpack');
|
|
||||||
var path = require('path');
|
|
||||||
var rules = require('./webpack.rules');
|
|
||||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
var HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin');
|
|
||||||
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
|
|
||||||
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
|
||||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
|
||||||
var artifacts = require("../test/artifacts");
|
|
||||||
|
|
||||||
var OUTPATH = artifacts.pathSync("/build");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: {
|
|
||||||
app: './src/index.jsx',
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
path: OUTPATH,
|
|
||||||
filename: '[name].[contenthash].js',
|
|
||||||
chunkFilename: '[contenthash].js'
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.js', '.jsx']
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
noParse: [
|
|
||||||
/mapbox-gl\/dist\/mapbox-gl.js/
|
|
||||||
],
|
|
||||||
rules: rules
|
|
||||||
},
|
|
||||||
node: {
|
|
||||||
fs: "empty",
|
|
||||||
net: 'empty',
|
|
||||||
tls: 'empty'
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new webpack.NoEmitOnErrorsPlugin(),
|
|
||||||
new WebpackCleanupPlugin(),
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
'process.env': {
|
|
||||||
NODE_ENV: '"production"'
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
template: './src/template.html',
|
|
||||||
title: 'Maputnik'
|
|
||||||
}),
|
|
||||||
new HtmlWebpackInlineSVGPlugin({
|
|
||||||
runPreEmit: true,
|
|
||||||
}),
|
|
||||||
new CopyWebpackPlugin([
|
|
||||||
{
|
|
||||||
from: './src/manifest.json',
|
|
||||||
to: 'manifest.json'
|
|
||||||
}
|
|
||||||
]),
|
|
||||||
new BundleAnalyzerPlugin({
|
|
||||||
analyzerMode: 'static',
|
|
||||||
defaultSizes: 'gzip',
|
|
||||||
openAnalyzer: false,
|
|
||||||
generateStatsFile: true,
|
|
||||||
reportFilename: 'bundle-stats.html',
|
|
||||||
statsFilename: 'bundle-stats.json',
|
|
||||||
})
|
|
||||||
]
|
|
||||||
};
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
const webpackProdConfig = require('./webpack.production.config');
|
|
||||||
const artifacts = require("../test/artifacts");
|
|
||||||
|
|
||||||
const OUTPATH = artifacts.pathSync("/profiling");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
...webpackProdConfig,
|
|
||||||
output: {
|
|
||||||
...webpackProdConfig.output,
|
|
||||||
path: OUTPATH,
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
...webpackProdConfig.resolve,
|
|
||||||
alias: {
|
|
||||||
...webpackProdConfig.resolve.alias,
|
|
||||||
'react-dom$': 'react-dom/profiling',
|
|
||||||
'scheduler/tracing': 'scheduler/tracing-profiling',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
|
|
||||||
module.exports = [
|
|
||||||
{
|
|
||||||
test: /\.jsx?$/,
|
|
||||||
exclude: [
|
|
||||||
path.resolve(__dirname, '../node_modules')
|
|
||||||
],
|
|
||||||
use: 'babel-loader'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(eot|ttf|woff|woff2)$/,
|
|
||||||
use: 'file-loader?name=fonts/[name].[ext]'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.ico$/,
|
|
||||||
use: 'file-loader?name=[name].[ext]'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(gif|jpg|png)$/,
|
|
||||||
use: 'file-loader?name=img/[name].[ext]'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.svg$/,
|
|
||||||
use: [
|
|
||||||
'svg-inline-loader'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.scss$/,
|
|
||||||
use: [
|
|
||||||
'style-loader',
|
|
||||||
"css-loader",
|
|
||||||
"sass-loader"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.css$/,
|
|
||||||
use: [
|
|
||||||
'style-loader',
|
|
||||||
'css-loader'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from "cypress";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
e2e: {
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
// implement node event listeners here
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import MaputnikDriver from "./driver";
|
||||||
|
|
||||||
|
describe("accessibility", () => {
|
||||||
|
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
|
||||||
|
beforeAndAfter();
|
||||||
|
|
||||||
|
describe("skip links", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.setStyle("layer");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skip link to layer list", () => {
|
||||||
|
const selector = "root:skip:layer-list";
|
||||||
|
should.isExists(selector);
|
||||||
|
when.tab();
|
||||||
|
should.beFocused(selector);
|
||||||
|
when.click(selector);
|
||||||
|
should.beFocused("skip-target-layer-list");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skip link to layer editor", () => {
|
||||||
|
const selector = "root:skip:layer-editor";
|
||||||
|
should.isExists(selector);
|
||||||
|
when.tab().tab();
|
||||||
|
should.beFocused(selector);
|
||||||
|
when.click(selector);
|
||||||
|
should.beFocused("skip-target-layer-editor");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skip link to map view", () => {
|
||||||
|
const selector = "root:skip:map-view";
|
||||||
|
should.isExists(selector);
|
||||||
|
when.tab().tab().tab();
|
||||||
|
should.beFocused(selector);
|
||||||
|
when.click(selector);
|
||||||
|
should.canvasBeFocused();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
import { CypressHelper } from "@shellygo/cypress-test-utils";
|
||||||
|
import { v1 as uuid } from "uuid";
|
||||||
|
export default class MaputnikDriver {
|
||||||
|
private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" });
|
||||||
|
public beforeAndAfter = () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
this.given.setupInterception();
|
||||||
|
this.when.setStyle("both");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
public given = {
|
||||||
|
setupInterception: () => {
|
||||||
|
cy.intercept("GET", "http://localhost:8888/example-style.json", {
|
||||||
|
fixture: "example-style.json",
|
||||||
|
}).as("example-style.json");
|
||||||
|
cy.intercept("GET", "http://localhost:8888/example-layer-style.json", {
|
||||||
|
fixture: "example-layer-style.json",
|
||||||
|
});
|
||||||
|
cy.intercept("GET", "http://localhost:8888/geojson-style.json", {
|
||||||
|
fixture: "geojson-style.json",
|
||||||
|
});
|
||||||
|
cy.intercept("GET", "http://localhost:8888/raster-style.json", {
|
||||||
|
fixture: "raster-style.json",
|
||||||
|
});
|
||||||
|
cy.intercept("GET", "http://localhost:8888/geojson-raster-style.json", {
|
||||||
|
fixture: "geojson-raster-style.json",
|
||||||
|
});
|
||||||
|
cy.intercept({ method: "GET", url: "*example.local/*" }, []);
|
||||||
|
cy.intercept({ method: "GET", url: "*example.com/*" }, []);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
public when = {
|
||||||
|
within: (selector: string, fn: () => void) => {
|
||||||
|
this.helper.when.within(fn, selector);
|
||||||
|
},
|
||||||
|
tab: () => cy.get("body").tab(),
|
||||||
|
waitForExampleFileRequset: () => {
|
||||||
|
this.helper.when.waitForResponse("example-style.json");
|
||||||
|
},
|
||||||
|
chooseExampleFile: () => {
|
||||||
|
cy.get("input[type='file']").selectFile(
|
||||||
|
"cypress/fixtures/example-style.json",
|
||||||
|
{ force: true }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
setStyle: (
|
||||||
|
styleProperties: "geojson" | "raster" | "both" | "layer" | "",
|
||||||
|
zoom?: number
|
||||||
|
) => {
|
||||||
|
let url = "?debug";
|
||||||
|
switch (styleProperties) {
|
||||||
|
case "geojson":
|
||||||
|
url += "&style=http://localhost:8888/geojson-style.json";
|
||||||
|
break;
|
||||||
|
case "raster":
|
||||||
|
url += "&style=http://localhost:8888/raster-style.json";
|
||||||
|
break;
|
||||||
|
case "both":
|
||||||
|
url += "&style=http://localhost:8888/geojson-raster-style.json";
|
||||||
|
break;
|
||||||
|
case "layer":
|
||||||
|
url += "&style=http://localhost:8888/example-layer-style.json";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (zoom) {
|
||||||
|
url += "#" + zoom + "/41.3805/2.1635";
|
||||||
|
}
|
||||||
|
cy.visit("http://localhost:8888/" + url);
|
||||||
|
if (styleProperties) {
|
||||||
|
cy.on("window:confirm", () => true);
|
||||||
|
}
|
||||||
|
cy.get(".maputnik-toolbar-link").should("be.visible");
|
||||||
|
},
|
||||||
|
fillLayersModal: (opts: any) => {
|
||||||
|
var type = opts.type;
|
||||||
|
var layer = opts.layer;
|
||||||
|
var id;
|
||||||
|
if (opts.id) {
|
||||||
|
id = opts.id;
|
||||||
|
} else {
|
||||||
|
id = `${type}:${uuid()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
cy.get(
|
||||||
|
this.get.getDataAttribute("add-layer.layer-type", "select")
|
||||||
|
).select(type);
|
||||||
|
cy.get(this.get.getDataAttribute("add-layer.layer-id", "input")).type(id);
|
||||||
|
if (layer) {
|
||||||
|
cy.get(
|
||||||
|
this.get.getDataAttribute("add-layer.layer-source-block", "input")
|
||||||
|
).type(layer);
|
||||||
|
}
|
||||||
|
this.when.click("add-layer");
|
||||||
|
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
|
||||||
|
typeKeys: (keys: string) => {
|
||||||
|
cy.get("body").type(keys);
|
||||||
|
},
|
||||||
|
|
||||||
|
click: (selector: string) => {
|
||||||
|
this.helper.when.click(selector);
|
||||||
|
// cy.get(selector).click({ force: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
select: (selector: string, value: string) => {
|
||||||
|
cy.get(selector).select(value);
|
||||||
|
},
|
||||||
|
|
||||||
|
focus: (selector: string) => {
|
||||||
|
this.helper.when.focus(selector);
|
||||||
|
},
|
||||||
|
|
||||||
|
setValue: (selector: string, text: string) => {
|
||||||
|
cy.get(selector).clear().type(text, { parseSpecialCharSequences: false });
|
||||||
|
},
|
||||||
|
|
||||||
|
closeModal: (key: string) => {
|
||||||
|
this.helper.when.waitUntil(() => this.helper.get.element(key));
|
||||||
|
this.when.click(key + ".close-modal");
|
||||||
|
},
|
||||||
|
|
||||||
|
openLayersModal: () => {
|
||||||
|
this.helper.when.click("layer-list:add-layer");
|
||||||
|
|
||||||
|
cy.get(this.get.getDataAttribute("modal:add-layer")).should("exist");
|
||||||
|
cy.get(this.get.getDataAttribute("modal:add-layer")).should("be.visible");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
public get = {
|
||||||
|
isMac: () => {
|
||||||
|
return Cypress.platform === "darwin";
|
||||||
|
},
|
||||||
|
getStyleFromWindow: (win: Window) => {
|
||||||
|
const styleId = win.localStorage.getItem("maputnik:latest_style");
|
||||||
|
const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`);
|
||||||
|
const obj = JSON.parse(styleItem || "");
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
getExampleFileUrl: () => {
|
||||||
|
return "http://localhost:8888/example-style.json";
|
||||||
|
},
|
||||||
|
getDataAttribute: (key: string, selector?: string): string => {
|
||||||
|
return `*[data-wd-key='${key}'] ${selector || ""}`;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
public should = {
|
||||||
|
canvasBeFocused: () => {
|
||||||
|
this.when.within("maplibre:map", () => {
|
||||||
|
cy.get("canvas").should("be.focused");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
notExist: (selector: string) => {
|
||||||
|
cy.get(selector).should("not.exist");
|
||||||
|
},
|
||||||
|
beFocused: (selector: string) => {
|
||||||
|
this.helper.get.element(selector).should("have.focus");
|
||||||
|
},
|
||||||
|
|
||||||
|
notBeFocused: (selector: string) => {
|
||||||
|
this.helper.get.element(selector).should("not.have.focus");
|
||||||
|
},
|
||||||
|
|
||||||
|
beVisible: (selector: string) => {
|
||||||
|
this.helper.get.element(selector).should("be.visible");
|
||||||
|
},
|
||||||
|
|
||||||
|
notBeVisible: (selector: string) => {
|
||||||
|
this.helper.get.element(selector).should("not.be.visible");
|
||||||
|
},
|
||||||
|
|
||||||
|
equalStyleStore: (getter: (obj: any) => any, styleObj: any) => {
|
||||||
|
cy.window().then((win: any) => {
|
||||||
|
const obj = this.get.getStyleFromWindow(win);
|
||||||
|
assert.deepEqual(getter(obj), styleObj);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
isStyleStoreEqualToExampleFileData: () => {
|
||||||
|
cy.window().then((win: any) => {
|
||||||
|
const obj = this.get.getStyleFromWindow(win);
|
||||||
|
cy.fixture("example-style.json").should("deep.equal", obj);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
isExists: (selector: string) => {
|
||||||
|
this.helper.get.element(selector).should("exist");
|
||||||
|
},
|
||||||
|
isSelected: (selector: string, value: string) => {
|
||||||
|
cy.get(selector).find(`option[value="${value}"]`).should("be.selected");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import MaputnikDriver from "./driver";
|
||||||
|
|
||||||
|
describe("history", () => {
|
||||||
|
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
|
||||||
|
beforeAndAfter();
|
||||||
|
|
||||||
|
let undoKeyCombo: string;
|
||||||
|
let redoKeyCombo: string;
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
const isMac = get.isMac();
|
||||||
|
undoKeyCombo = isMac ? "{meta}z" : "{ctrl}z";
|
||||||
|
redoKeyCombo = isMac ? "{meta}{shift}z" : "{ctrl}y";
|
||||||
|
});
|
||||||
|
|
||||||
|
it("undo/redo", () => {
|
||||||
|
when.setStyle("geojson");
|
||||||
|
when.openLayersModal();
|
||||||
|
|
||||||
|
should.equalStyleStore((a: any) => a.layers, []);
|
||||||
|
|
||||||
|
when.fillLayersModal({
|
||||||
|
id: "step 1",
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "step 1",
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
when.openLayersModal();
|
||||||
|
when.fillLayersModal({
|
||||||
|
id: "step 2",
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "step 1",
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "step 2",
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
when.typeKeys(undoKeyCombo);
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "step 1",
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
when.typeKeys(undoKeyCombo);
|
||||||
|
should.equalStyleStore((a: any) => a.layers, []);
|
||||||
|
|
||||||
|
when.typeKeys(redoKeyCombo);
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "step 1",
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
when.typeKeys(redoKeyCombo);
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "step 1",
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "step 2",
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { default as MaputnikDriver } from "./driver";
|
||||||
|
|
||||||
|
describe("keyboard", () => {
|
||||||
|
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
|
||||||
|
beforeAndAfter();
|
||||||
|
describe("shortcuts", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
given.setupInterception();
|
||||||
|
when.setStyle("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ESC should unfocus", () => {
|
||||||
|
const targetSelector = "maputnik-select";
|
||||||
|
when.focus(targetSelector);
|
||||||
|
should.beFocused(targetSelector);
|
||||||
|
|
||||||
|
when.typeKeys("{esc}");
|
||||||
|
expect(should.notBeFocused(targetSelector));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("'?' should show shortcuts modal", () => {
|
||||||
|
when.typeKeys("?");
|
||||||
|
should.beVisible("modal:shortcuts");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("'o' should show open modal", () => {
|
||||||
|
when.typeKeys("o");
|
||||||
|
should.beVisible("modal:open");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("'e' should show export modal", () => {
|
||||||
|
when.typeKeys("e");
|
||||||
|
should.beVisible("modal:export");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("'d' should show sources modal", () => {
|
||||||
|
when.typeKeys("d");
|
||||||
|
should.beVisible("modal:sources");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("'s' should show settings modal", () => {
|
||||||
|
when.typeKeys("s");
|
||||||
|
should.beVisible("modal:settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("'i' should change map to inspect mode", () => {
|
||||||
|
when.typeKeys("i");
|
||||||
|
should.isSelected(get.getDataAttribute("nav:inspect"), "inspect");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("'m' should focus map", () => {
|
||||||
|
when.typeKeys("m");
|
||||||
|
should.beFocused(".maplibregl-canvas");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("'!' should show debug modal", () => {
|
||||||
|
when.typeKeys("!");
|
||||||
|
should.beVisible("modal:debug");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,497 @@
|
|||||||
|
var assert = require("assert");
|
||||||
|
import { v1 as uuid } from "uuid";
|
||||||
|
import MaputnikDriver from "./driver";
|
||||||
|
|
||||||
|
describe("layers", () => {
|
||||||
|
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
|
||||||
|
beforeAndAfter();
|
||||||
|
beforeEach(() => {
|
||||||
|
when.setStyle("both");
|
||||||
|
when.openLayersModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ops", () => {
|
||||||
|
it("delete", () => {
|
||||||
|
var id = when.fillLayersModal({
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
when.click("layer-list-item:" + id + ":delete");
|
||||||
|
|
||||||
|
should.equalStyleStore((a: any) => a.layers, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("duplicate", () => {
|
||||||
|
var styleObj;
|
||||||
|
var id = when.fillLayersModal({
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
when.click("layer-list-item:" + id + ":copy");
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: id + "-copy",
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hide", () => {
|
||||||
|
var styleObj;
|
||||||
|
var id = when.fillLayersModal({
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
when.click("layer-list-item:" + id + ":toggle-visibility");
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "background",
|
||||||
|
layout: {
|
||||||
|
visibility: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
when.click("layer-list-item:" + id + ":toggle-visibility");
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "background",
|
||||||
|
layout: {
|
||||||
|
visibility: "visible",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("background", () => {
|
||||||
|
it("add", () => {
|
||||||
|
var id = when.fillLayersModal({
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("modify", () => {
|
||||||
|
function createBackground() {
|
||||||
|
// Setup
|
||||||
|
var id = uuid();
|
||||||
|
|
||||||
|
when.select(
|
||||||
|
get.getDataAttribute("add-layer.layer-type", "select"),
|
||||||
|
"background"
|
||||||
|
);
|
||||||
|
when.setValue(
|
||||||
|
get.getDataAttribute("add-layer.layer-id", "input"),
|
||||||
|
"background:" + id
|
||||||
|
);
|
||||||
|
|
||||||
|
when.click("add-layer");
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "background:" + id,
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====> THESE SHOULD BE FROM THE SPEC
|
||||||
|
describe("layer", () => {
|
||||||
|
it("expand/collapse");
|
||||||
|
it("id", () => {
|
||||||
|
var bgId = createBackground();
|
||||||
|
|
||||||
|
when.click("layer-list-item:background:" + bgId);
|
||||||
|
|
||||||
|
var id = uuid();
|
||||||
|
when.setValue(
|
||||||
|
get.getDataAttribute("layer-editor.layer-id", "input"),
|
||||||
|
"foobar:" + id
|
||||||
|
);
|
||||||
|
when.click("min-zoom");
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "foobar:" + id,
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("min-zoom", () => {
|
||||||
|
var bgId = createBackground();
|
||||||
|
|
||||||
|
when.click("layer-list-item:background:" + bgId);
|
||||||
|
when.setValue(
|
||||||
|
get.getDataAttribute("min-zoom", 'input[type="text"]'),
|
||||||
|
"1"
|
||||||
|
);
|
||||||
|
|
||||||
|
when.click("layer-editor.layer-id");
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "background:" + bgId,
|
||||||
|
type: "background",
|
||||||
|
minzoom: 1,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// AND RESET!
|
||||||
|
// driver.setValue(driver.getDataAttribute("min-zoom", "input"), "")
|
||||||
|
// driver.click(driver.getDataAttribute("max-zoom", "input"));
|
||||||
|
|
||||||
|
// driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
// {
|
||||||
|
// "id": 'background:'+bgId,
|
||||||
|
// "type": 'background'
|
||||||
|
// }
|
||||||
|
// ]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("max-zoom", () => {
|
||||||
|
var bgId = createBackground();
|
||||||
|
|
||||||
|
when.click("layer-list-item:background:" + bgId);
|
||||||
|
when.setValue(
|
||||||
|
get.getDataAttribute("max-zoom", 'input[type="text"]'),
|
||||||
|
"1"
|
||||||
|
);
|
||||||
|
|
||||||
|
when.click("layer-editor.layer-id");
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "background:" + bgId,
|
||||||
|
type: "background",
|
||||||
|
maxzoom: 1,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("comments", () => {
|
||||||
|
var bgId = createBackground();
|
||||||
|
var id = uuid();
|
||||||
|
|
||||||
|
when.click("layer-list-item:background:" + bgId);
|
||||||
|
when.setValue(get.getDataAttribute("layer-comment", "textarea"), id);
|
||||||
|
|
||||||
|
when.click("layer-editor.layer-id");
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "background:" + bgId,
|
||||||
|
type: "background",
|
||||||
|
metadata: {
|
||||||
|
"maputnik:comment": id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Unset it again.
|
||||||
|
// TODO: This fails
|
||||||
|
// driver.setValue(driver.getDataAttribute("layer-comment", "textarea"), "");
|
||||||
|
// driver.click(driver.getDataAttribute("min-zoom", "input"));
|
||||||
|
|
||||||
|
// driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||||
|
// {
|
||||||
|
// "id": 'background:'+bgId,
|
||||||
|
// "type": 'background'
|
||||||
|
// }
|
||||||
|
// ]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("color", () => {
|
||||||
|
var bgId = createBackground();
|
||||||
|
|
||||||
|
when.click("layer-list-item:background:" + bgId);
|
||||||
|
|
||||||
|
when.click("spec-field:background-color");
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: "background:" + bgId,
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filter", () => {
|
||||||
|
it("expand/collapse");
|
||||||
|
it("compound filter");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("paint", () => {
|
||||||
|
it("expand/collapse");
|
||||||
|
it("color");
|
||||||
|
it("pattern");
|
||||||
|
it("opacity");
|
||||||
|
});
|
||||||
|
// <=====
|
||||||
|
|
||||||
|
describe("json-editor", () => {
|
||||||
|
it("expand/collapse");
|
||||||
|
it("modify");
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
it.skip("parse error", () => {
|
||||||
|
var bgId = createBackground();
|
||||||
|
|
||||||
|
when.click("layer-list-item:background:" + bgId);
|
||||||
|
|
||||||
|
var errorSelector = ".CodeMirror-lint-marker-error";
|
||||||
|
should.notExist(errorSelector);
|
||||||
|
|
||||||
|
when.click(".CodeMirror");
|
||||||
|
when.typeKeys(
|
||||||
|
"\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {"
|
||||||
|
);
|
||||||
|
should.isExists(errorSelector);
|
||||||
|
|
||||||
|
when.click("layer-editor.layer-id");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fill", () => {
|
||||||
|
it("add", () => {
|
||||||
|
var id = when.fillLayersModal({
|
||||||
|
type: "fill",
|
||||||
|
layer: "example",
|
||||||
|
});
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "fill",
|
||||||
|
source: "example",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Change source
|
||||||
|
it("change source");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("line", () => {
|
||||||
|
it("add", () => {
|
||||||
|
var id = when.fillLayersModal({
|
||||||
|
type: "line",
|
||||||
|
layer: "example",
|
||||||
|
});
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "line",
|
||||||
|
source: "example",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("groups", () => {
|
||||||
|
// TODO
|
||||||
|
// Click each of the layer groups.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("symbol", () => {
|
||||||
|
it("add", () => {
|
||||||
|
var id = when.fillLayersModal({
|
||||||
|
type: "symbol",
|
||||||
|
layer: "example",
|
||||||
|
});
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "symbol",
|
||||||
|
source: "example",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("raster", () => {
|
||||||
|
it("add", () => {
|
||||||
|
var id = when.fillLayersModal({
|
||||||
|
type: "raster",
|
||||||
|
layer: "raster",
|
||||||
|
});
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "raster",
|
||||||
|
source: "raster",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("circle", () => {
|
||||||
|
it("add", () => {
|
||||||
|
var id = when.fillLayersModal({
|
||||||
|
type: "circle",
|
||||||
|
layer: "example",
|
||||||
|
});
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "circle",
|
||||||
|
source: "example",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fill extrusion", () => {
|
||||||
|
it("add", () => {
|
||||||
|
var id = when.fillLayersModal({
|
||||||
|
type: "fill-extrusion",
|
||||||
|
layer: "example",
|
||||||
|
});
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(a: any) => a.layers,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "fill-extrusion",
|
||||||
|
source: "example",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("groups", () => {
|
||||||
|
it("simple", () => {
|
||||||
|
when.setStyle("geojson");
|
||||||
|
|
||||||
|
when.openLayersModal();
|
||||||
|
when.fillLayersModal({
|
||||||
|
id: "foo",
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
|
||||||
|
when.openLayersModal();
|
||||||
|
when.fillLayersModal({
|
||||||
|
id: "foo_bar",
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
|
||||||
|
when.openLayersModal();
|
||||||
|
when.fillLayersModal({
|
||||||
|
id: "foo_bar_baz",
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
|
||||||
|
should.beVisible("layer-list-item:foo");
|
||||||
|
|
||||||
|
should.notBeVisible("layer-list-item:foo_bar");
|
||||||
|
should.notBeVisible("layer-list-item:foo_bar_baz");
|
||||||
|
|
||||||
|
when.click("layer-list-group:foo-0");
|
||||||
|
|
||||||
|
should.beVisible("layer-list-item:foo");
|
||||||
|
should.beVisible("layer-list-item:foo_bar");
|
||||||
|
should.beVisible("layer-list-item:foo_bar_baz");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import MaputnikDriver from "./driver";
|
||||||
|
|
||||||
|
describe("map", () => {
|
||||||
|
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
|
||||||
|
beforeAndAfter();
|
||||||
|
describe("zoom level", () => {
|
||||||
|
it("via url", () => {
|
||||||
|
var zoomLevel = 12.37;
|
||||||
|
when.setStyle("geojson", zoomLevel);
|
||||||
|
should.beVisible("maplibre:ctrl-zoom");
|
||||||
|
// HM TODO
|
||||||
|
//driver.getText(".maplibregl-ctrl-zoom") === "Zoom "+(zoomLevel);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("via map controls", () => {
|
||||||
|
var zoomLevel = 12.37;
|
||||||
|
when.setStyle("geojson", zoomLevel);
|
||||||
|
|
||||||
|
when.click("maplibre:ctrl-zoom");
|
||||||
|
should.beVisible("maplibre:ctrl-zoom");
|
||||||
|
// HM TODO
|
||||||
|
//driver.getText(".maplibregl-ctrl-zoom") === "Zoom "+(zoomLevel + 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import MaputnikDriver from "./driver";
|
||||||
|
|
||||||
|
describe("modals", () => {
|
||||||
|
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
|
||||||
|
beforeAndAfter();
|
||||||
|
beforeEach(() => {
|
||||||
|
when.setStyle("");
|
||||||
|
});
|
||||||
|
describe("open", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.click("nav:open");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("close", () => {
|
||||||
|
when.closeModal("modal:open");
|
||||||
|
should.notExist("modal:open");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip("upload", () => {
|
||||||
|
// HM: I was not able to make the following choose file actually to select a file and close the modal...
|
||||||
|
when.chooseExampleFile();
|
||||||
|
|
||||||
|
should.isStyleStoreEqualToExampleFileData();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("load from url", () => {
|
||||||
|
var styleFileUrl = get.getExampleFileUrl();
|
||||||
|
|
||||||
|
when.setValue(get.getDataAttribute("modal:open.url.input"), styleFileUrl);
|
||||||
|
when.click("modal:open.url.button");
|
||||||
|
when.waitForExampleFileRequset();
|
||||||
|
|
||||||
|
should.isStyleStoreEqualToExampleFileData();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("shortcuts", () => {
|
||||||
|
it("open/close", () => {
|
||||||
|
when.setStyle("");
|
||||||
|
when.typeKeys("?");
|
||||||
|
when.closeModal("modal:shortcuts");
|
||||||
|
should.notExist("modal:shortcuts");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("export", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.click("nav:export");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("close", () => {
|
||||||
|
when.closeModal("modal:export");
|
||||||
|
should.notExist("modal:export");
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Work out how to download a file and check the contents
|
||||||
|
it("download");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sources", () => {
|
||||||
|
it("active sources");
|
||||||
|
it("public source");
|
||||||
|
it("add new source");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("inspect", () => {
|
||||||
|
it("toggle", () => {
|
||||||
|
when.setStyle("geojson");
|
||||||
|
|
||||||
|
when.select(get.getDataAttribute("nav:inspect", "select"), "inspect");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("style settings", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.click("nav:settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("name", () => {
|
||||||
|
when.setValue(get.getDataAttribute("modal:settings.name"), "foobar");
|
||||||
|
when.click("modal:settings.owner");
|
||||||
|
|
||||||
|
should.equalStyleStore((obj) => obj.name, "foobar");
|
||||||
|
});
|
||||||
|
it("owner", () => {
|
||||||
|
when.setValue(get.getDataAttribute("modal:settings.owner"), "foobar");
|
||||||
|
when.click("modal:settings.name");
|
||||||
|
|
||||||
|
should.equalStyleStore((obj) => obj.owner, "foobar");
|
||||||
|
});
|
||||||
|
it("sprite url", () => {
|
||||||
|
when.setValue(
|
||||||
|
get.getDataAttribute("modal:settings.sprite"),
|
||||||
|
"http://example.com"
|
||||||
|
);
|
||||||
|
when.click("modal:settings.name");
|
||||||
|
|
||||||
|
should.equalStyleStore((obj) => obj.sprite, "http://example.com");
|
||||||
|
});
|
||||||
|
it("glyphs url", () => {
|
||||||
|
var glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
|
||||||
|
when.setValue(get.getDataAttribute("modal:settings.glyphs"), glyphsUrl);
|
||||||
|
when.click("modal:settings.name");
|
||||||
|
|
||||||
|
should.equalStyleStore((obj) => obj.glyphs, glyphsUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maptiler access token", () => {
|
||||||
|
var apiKey = "testing123";
|
||||||
|
when.setValue(
|
||||||
|
get.getDataAttribute(
|
||||||
|
"modal:settings.maputnik:openmaptiles_access_token"
|
||||||
|
),
|
||||||
|
apiKey
|
||||||
|
);
|
||||||
|
when.click("modal:settings.name");
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(obj) => obj.metadata["maputnik:openmaptiles_access_token"],
|
||||||
|
apiKey
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("thunderforest access token", () => {
|
||||||
|
var apiKey = "testing123";
|
||||||
|
when.setValue(
|
||||||
|
get.getDataAttribute(
|
||||||
|
"modal:settings.maputnik:thunderforest_access_token"
|
||||||
|
),
|
||||||
|
apiKey
|
||||||
|
);
|
||||||
|
when.click("modal:settings.name");
|
||||||
|
|
||||||
|
should.equalStyleStore(
|
||||||
|
(obj) => obj.metadata["maputnik:thunderforest_access_token"],
|
||||||
|
apiKey
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("style renderer", () => {
|
||||||
|
cy.on("uncaught:exception", () => false); // this is due to the fact that this is an invalid style for openlayers
|
||||||
|
when.select(
|
||||||
|
get.getDataAttribute("modal:settings.maputnik:renderer"),
|
||||||
|
"ol"
|
||||||
|
);
|
||||||
|
should.isSelected(
|
||||||
|
get.getDataAttribute("modal:settings.maputnik:renderer"),
|
||||||
|
"ol"
|
||||||
|
);
|
||||||
|
|
||||||
|
when.click("modal:settings.name");
|
||||||
|
|
||||||
|
should.equalStyleStore((obj) => obj.metadata["maputnik:renderer"], "ol");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sources", () => {
|
||||||
|
it("toggle");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"id": "test-style",
|
||||||
|
"version": 8,
|
||||||
|
"name": "Test Style",
|
||||||
|
"metadata": {
|
||||||
|
"maputnik:renderer": "mlgljs"
|
||||||
|
},
|
||||||
|
"sources": {},
|
||||||
|
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
|
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"id": "background",
|
||||||
|
"type": "background"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": 8,
|
"version": 8,
|
||||||
"name": "Test Style",
|
"name": "Test Style",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"maputnik:renderer": "mbgljs"
|
"maputnik:renderer": "mlgljs"
|
||||||
},
|
},
|
||||||
"sources": {},
|
"sources": {},
|
||||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"id": "test-style",
|
||||||
|
"version": 8,
|
||||||
|
"name": "Test Style",
|
||||||
|
"metadata": {
|
||||||
|
"maputnik:renderer": "mlgljs"
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"example": {
|
||||||
|
"type": "vector",
|
||||||
|
"data": {
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features":[{
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": {
|
||||||
|
"name": "Dinagat Islands"
|
||||||
|
},
|
||||||
|
"geometry":{
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [125.6, 10.1]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"raster": {
|
||||||
|
"tileSize": 256,
|
||||||
|
"tiles": ["http://localhost/example/{x}/{y}/{z}"],
|
||||||
|
"type": "raster"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
|
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
|
"layers": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"id": "test-style",
|
||||||
|
"version": 8,
|
||||||
|
"name": "Test Style",
|
||||||
|
"metadata": {
|
||||||
|
"maputnik:renderer": "mlgljs"
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"example": {
|
||||||
|
"type": "vector",
|
||||||
|
"data": {
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features":[{
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": {
|
||||||
|
"name": "Dinagat Islands"
|
||||||
|
},
|
||||||
|
"geometry":{
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [125.6, 10.1]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
|
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
|
"layers": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"id": "test-style",
|
||||||
|
"version": 8,
|
||||||
|
"name": "Test Style",
|
||||||
|
"metadata": {
|
||||||
|
"maputnik:renderer": "mlgljs"
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"raster": {
|
||||||
|
"tileSize": 256,
|
||||||
|
"tiles": ["http://localhost/example/{x}/{y}/{z}"],
|
||||||
|
"type": "raster"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
|
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
|
"layers": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
// ***********************************************
|
||||||
|
// This example commands.ts shows you how to
|
||||||
|
// create various custom commands and overwrite
|
||||||
|
// existing commands.
|
||||||
|
//
|
||||||
|
// For more comprehensive examples of custom
|
||||||
|
// commands please read more here:
|
||||||
|
// https://on.cypress.io/custom-commands
|
||||||
|
// ***********************************************
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a parent command --
|
||||||
|
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a child command --
|
||||||
|
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a dual command --
|
||||||
|
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This will overwrite an existing command --
|
||||||
|
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||||
|
//
|
||||||
|
// declare global {
|
||||||
|
// namespace Cypress {
|
||||||
|
// interface Chainable {
|
||||||
|
// login(email: string, password: string): Chainable<void>
|
||||||
|
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||||
|
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||||
|
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// ***********************************************************
|
||||||
|
// This example support/e2e.ts is processed and
|
||||||
|
// loaded automatically before your test files.
|
||||||
|
//
|
||||||
|
// This is a great place to put global configuration and
|
||||||
|
// behavior that modifies Cypress.
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off
|
||||||
|
// automatically serving support files with the
|
||||||
|
// 'supportFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/configuration
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// Import commands.js using ES2015 syntax:
|
||||||
|
import "cypress-plugin-tab";
|
||||||
|
import "./commands";
|
||||||
|
|
||||||
|
// Alternatively you can use CommonJS syntax:
|
||||||
|
// require('./commands')
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
<title>Maputnik</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="src/manifest.json">
|
||||||
<style>
|
<style>
|
||||||
html {
|
html {
|
||||||
background-color: rgb(28, 31, 36);
|
background-color: rgb(28, 31, 36);
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading__logo svg {
|
.loading__logo img {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
}
|
}
|
||||||
@@ -38,9 +38,9 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- TODO: Import dynamically -->
|
|
||||||
<!-- From <https://github.com/hail2u/color-blindness-emulation> -->
|
<!-- From <https://github.com/hail2u/color-blindness-emulation> -->
|
||||||
<svg
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
version="1.1">
|
version="1.1">
|
||||||
<defs>
|
<defs>
|
||||||
@@ -122,10 +122,10 @@
|
|||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<div class="loading">
|
<div class="loading">
|
||||||
<div class="loading__logo">
|
<div class="loading__logo">
|
||||||
<!-- replaced by 'html-webpack-inline-svg-plugin' -->
|
|
||||||
<img inline src="node_modules/maputnik-design/logos/logo-loading.svg" />
|
<img inline src="node_modules/maputnik-design/logos/logo-loading.svg" />
|
||||||
</div>
|
</div>
|
||||||
<div class="loading__text">Loading…</div>
|
<div class="loading__text">Loading…</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="module" src="/src/index.jsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Generated
+13449
-15245
File diff suppressed because it is too large
Load Diff
+71
-105
@@ -1,18 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "maputnik",
|
"name": "maputnik",
|
||||||
"version": "1.7.0-beta",
|
"version": "2.0.0-pre.2",
|
||||||
"description": "A MapboxGL visual style editor",
|
"description": "A MapLibre GL visual style editor",
|
||||||
"main": "''",
|
"main": "''",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"stats": "webpack --config config/webpack.production.config.js --profile --json > stats.json",
|
"start": "vite",
|
||||||
"build": "webpack --config config/webpack.production.config.js --progress --profile --colors",
|
"build": "tsc && vite build",
|
||||||
"profiling-build": "webpack --config config/webpack.profiling.config.js --progress --profile --colors",
|
"lint": "eslint ./src --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0 && npm run lint-css",
|
||||||
"test": "cross-env NODE_ENV=test wdio config/wdio.conf.js",
|
"test": "cypress run",
|
||||||
"test-watch": "cross-env NODE_ENV=test wdio config/wdio.conf.js --watch",
|
"cy:open": "cypress open",
|
||||||
"start": "webpack-dev-server --progress --profile --colors --config config/webpack.config.js",
|
|
||||||
"lint-js": "eslint --ext js --ext jsx src test",
|
|
||||||
"lint-css": "stylelint \"src/styles/*.scss\"",
|
"lint-css": "stylelint \"src/styles/*.scss\"",
|
||||||
"lint": "npm run lint-js && npm run lint-css"
|
"storybook": "storybook dev -h 0.0.0.0 -p 6006",
|
||||||
|
"build-storybook": "storybook build -o build/storybook"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -22,46 +21,50 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://github.com/maputnik/editor#readme",
|
"homepage": "https://github.com/maputnik/editor#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.8.4",
|
|
||||||
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
|
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
|
||||||
"@mapbox/mapbox-gl-style-spec": "^13.12.0",
|
"@maplibre/maplibre-gl-style-spec": "^17.0.1",
|
||||||
"@mdi/react": "^1.3.0",
|
"@mdi/js": "^6.6.96",
|
||||||
"classnames": "^2.2.6",
|
"@mdi/react": "^1.5.0",
|
||||||
"codemirror": "^5.52.0",
|
"array-move": "^4.0.0",
|
||||||
"color": "^3.1.2",
|
"buffer": "^6.0.3",
|
||||||
"detect-browser": "^5.0.0",
|
"classnames": "^2.3.1",
|
||||||
"file-saver": "^2.0.2",
|
"codemirror": "^5.65.2",
|
||||||
|
"color": "^4.2.3",
|
||||||
|
"detect-browser": "^5.3.0",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
|
"json-stringify-pretty-compact": "^3.0.0",
|
||||||
"json-to-ast": "^2.1.0",
|
"json-to-ast": "^2.1.0",
|
||||||
"jsonlint": "github:josdejong/jsonlint#85a19d7",
|
"jsonlint": "github:josdejong/jsonlint#85a19d7",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.21",
|
||||||
"lodash.capitalize": "^4.2.1",
|
"lodash.capitalize": "^4.2.1",
|
||||||
"lodash.clamp": "^4.0.3",
|
"lodash.clamp": "^4.0.3",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"mapbox-gl": "^1.9.0",
|
|
||||||
"mapbox-gl-inspect": "^1.3.1",
|
"mapbox-gl-inspect": "^1.3.1",
|
||||||
"maputnik-design": "github:maputnik/design#f7a2b4d",
|
"maplibre-gl": "^2.4.0",
|
||||||
"ol": "^6.2.1",
|
"maputnik-design": "github:maputnik/design#172b06c",
|
||||||
"ol-mapbox-style": "^6.0.1",
|
"ol": "^6.14.1",
|
||||||
"prop-types": "^15.7.2",
|
"ol-mapbox-style": "^7.1.1",
|
||||||
"react": "^16.12.0",
|
"prop-types": "^15.8.1",
|
||||||
"react-accessible-accordion": "^3.0.1",
|
"react": "^16.0.0",
|
||||||
"react-aria-menubutton": "^6.3.0",
|
"react-accessible-accordion": "^4.0.0",
|
||||||
"react-aria-modal": "^4.0.0",
|
"react-aria-menubutton": "^7.0.3",
|
||||||
|
"react-aria-modal": "^4.0.1",
|
||||||
"react-autobind": "^1.0.6",
|
"react-autobind": "^1.0.6",
|
||||||
"react-autocomplete": "^1.8.1",
|
"react-autocomplete": "^1.8.1",
|
||||||
"react-collapse": "^5.0.1",
|
"react-collapse": "^5.1.1",
|
||||||
"react-color": "^2.18.0",
|
"react-color": "^2.19.3",
|
||||||
"react-dom": "^16.12.0",
|
"react-dom": "^16.0.0",
|
||||||
"react-file-reader-input": "^2.0.0",
|
"react-file-reader-input": "^2.0.0",
|
||||||
"react-icon-base": "^2.1.2",
|
"react-icon-base": "^2.1.2",
|
||||||
"react-icons": "^3.9.0",
|
"react-icons": "^4.3.1",
|
||||||
"react-motion": "^0.5.2",
|
"react-sortable-hoc": "^2.0.0",
|
||||||
"react-sortable-hoc": "^1.11.0",
|
|
||||||
"reconnecting-websocket": "^4.4.0",
|
"reconnecting-websocket": "^4.4.0",
|
||||||
"slugify": "^1.3.6",
|
"sass": "^1.50.0",
|
||||||
|
"slugify": "^1.6.5",
|
||||||
|
"string-hash": "^1.1.3",
|
||||||
"url": "^0.11.0"
|
"url": "^0.11.0"
|
||||||
},
|
},
|
||||||
"jshintConfig": {
|
"jshintConfig": {
|
||||||
@@ -81,78 +84,41 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
|
||||||
"plugins": [
|
|
||||||
"react"
|
|
||||||
],
|
|
||||||
"extends": [
|
|
||||||
"plugin:react/recommended"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"node": true,
|
|
||||||
"es6": true
|
|
||||||
},
|
|
||||||
"parser": "babel-eslint",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 6,
|
|
||||||
"sourceType": "module",
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"impliedStrict": true,
|
|
||||||
"experimentalObjectRestSpread": true,
|
|
||||||
"jsx": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.8.4",
|
"@rollup/plugin-replace": "^5.0.5",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.5.5",
|
"@shellygo/cypress-test-utils": "^2.0.9",
|
||||||
"@babel/plugin-transform-runtime": "^7.6.2",
|
"@storybook/addon-a11y": "^7.6.5",
|
||||||
"@babel/preset-env": "^7.6.3",
|
"@storybook/addon-actions": "^7.6.5",
|
||||||
"@babel/preset-flow": "^7.0.0",
|
"@storybook/addon-links": "^7.6.5",
|
||||||
"@babel/preset-react": "^7.6.3",
|
"@storybook/addon-storysource": "^7.6.5",
|
||||||
"@mdi/js": "^5.0.45",
|
"@storybook/addons": "^7.6.5",
|
||||||
"@wdio/cli": "^6.0.5",
|
"@storybook/builder-vite": "^7.6.5",
|
||||||
"@wdio/local-runner": "^6.0.5",
|
"@storybook/react": "^7.6.5",
|
||||||
"@wdio/mocha-framework": "^6.0.4",
|
"@storybook/react-vite": "^7.6.5",
|
||||||
"@wdio/selenium-standalone-service": "^6.0.4",
|
"@storybook/theming": "^7.6.5",
|
||||||
"@wdio/spec-reporter": "^6.0.4",
|
"@types/cors": "^2.8.17",
|
||||||
"@wdio/sync": "^6.0.1",
|
"@types/react": "^16.14.52",
|
||||||
"babel-eslint": "^10.0.3",
|
"@types/react-dom": "^16.9.24",
|
||||||
"babel-loader": "^8.1.0",
|
"@types/uuid": "^9.0.7",
|
||||||
"babel-plugin-istanbul": "^6.0.0",
|
"@vitejs/plugin-react": "^4.2.0",
|
||||||
"babel-plugin-static-fs": "^3.0.0",
|
|
||||||
"copy-webpack-plugin": "^5.1.1",
|
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cross-env": "^7.0.0",
|
"cypress": "^13.6.1",
|
||||||
"css-loader": "^3.4.2",
|
"eslint": "^8.53.0",
|
||||||
"eslint": "^6.8.0",
|
"eslint-plugin-react": "^7.33.2",
|
||||||
"eslint-plugin-react": "^7.18.3",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"express": "^4.17.1",
|
"eslint-plugin-react-refresh": "^0.4.4",
|
||||||
"file-loader": "^6.0.0",
|
"express": "^4.17.3",
|
||||||
"html-webpack-inline-svg-plugin": "^1.3.0",
|
|
||||||
"html-webpack-plugin": "^3.2.0",
|
|
||||||
"is-docker": "^2.0.0",
|
|
||||||
"istanbul": "^0.4.5",
|
"istanbul": "^0.4.5",
|
||||||
"istanbul-lib-coverage": "^3.0.0",
|
"istanbul-lib-coverage": "^3.2.0",
|
||||||
"mkdirp": "^1.0.3",
|
"mocha": "^9.2.2",
|
||||||
"mocha": "^7.0.1",
|
"postcss": "^8.4.12",
|
||||||
"node-sass": "^4.13.1",
|
"react-hot-loader": "^4.13.0",
|
||||||
"react-hot-loader": "^4.12.19",
|
"storybook": "^7.6.5",
|
||||||
"sass-loader": "^8.0.2",
|
"stylelint": "^14.6.1",
|
||||||
"selenium-standalone": "^6.17.0",
|
"stylelint-config-recommended-scss": "^6.0.0",
|
||||||
"style-loader": "^1.1.3",
|
"stylelint-scss": "^4.2.0",
|
||||||
"stylelint": "^13.2.0",
|
"typescript": "^5.3.3",
|
||||||
"stylelint-config-recommended-scss": "^4.2.0",
|
"uuid": "^8.3.2",
|
||||||
"stylelint-scss": "^3.14.2",
|
"vite": "^5.0.0"
|
||||||
"svg-inline-loader": "^0.8.2",
|
|
||||||
"transform-loader": "^0.2.4",
|
|
||||||
"uuid": "^7.0.2",
|
|
||||||
"webdriverio": "^6.0.5",
|
|
||||||
"webpack": "^4.41.6",
|
|
||||||
"webpack-bundle-analyzer": "^3.6.0",
|
|
||||||
"webpack-cleanup-plugin": "^0.5.1",
|
|
||||||
"webpack-cli": "^3.3.11",
|
|
||||||
"webpack-dev-server": "^3.10.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+619
-428
File diff suppressed because it is too large
Load Diff
@@ -26,9 +26,7 @@ class AppLayout extends React.Component {
|
|||||||
return <div className="maputnik-layout">
|
return <div className="maputnik-layout">
|
||||||
{this.props.toolbar}
|
{this.props.toolbar}
|
||||||
<div className="maputnik-layout-list">
|
<div className="maputnik-layout-list">
|
||||||
<ScrollContainer>
|
{this.props.layerList}
|
||||||
{this.props.layerList}
|
|
||||||
</ScrollContainer>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="maputnik-layout-drawer">
|
<div className="maputnik-layout-drawer">
|
||||||
<ScrollContainer>
|
<ScrollContainer>
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
import {formatLayerId} from '../util/format';
|
||||||
|
|
||||||
class MessagePanel extends React.Component {
|
export default class AppMessagePanel extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
errors: PropTypes.array,
|
errors: PropTypes.array,
|
||||||
infos: PropTypes.array,
|
infos: PropTypes.array,
|
||||||
mapStyle: PropTypes.object,
|
mapStyle: PropTypes.object,
|
||||||
onLayerSelect: PropTypes.func,
|
onLayerSelect: PropTypes.func,
|
||||||
currentLayer: PropTypes.object,
|
currentLayer: PropTypes.object,
|
||||||
|
selectedLayerIndex: PropTypes.number,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@@ -15,6 +17,7 @@ class MessagePanel extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {selectedLayerIndex} = this.props;
|
||||||
const errors = this.props.errors.map((error, idx) => {
|
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") {
|
||||||
@@ -23,13 +26,13 @@ class MessagePanel extends React.Component {
|
|||||||
const layerId = mapStyle.layers[parsed.data.index].id;
|
const layerId = mapStyle.layers[parsed.data.index].id;
|
||||||
content = (
|
content = (
|
||||||
<>
|
<>
|
||||||
Layer <span>'{layerId}'</span>: {parsed.data.message}
|
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
||||||
{currentLayer.id !== layerId &&
|
{selectedLayerIndex !== parsed.data.index &&
|
||||||
<>
|
<>
|
||||||
—
|
—
|
||||||
<button
|
<button
|
||||||
className="maputnik-message-panel__switch-button"
|
className="maputnik-message-panel__switch-button"
|
||||||
onClick={() => this.props.onLayerSelect(layerId)}
|
onClick={() => this.props.onLayerSelect(parsed.data.index)}
|
||||||
>
|
>
|
||||||
switch to layer
|
switch to layer
|
||||||
</button>
|
</button>
|
||||||
@@ -57,5 +60,3 @@ class MessagePanel extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default MessagePanel
|
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
import classnames from "classnames";
|
||||||
|
import { detect } from "detect-browser";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
MdAssignmentTurnedIn,
|
||||||
|
MdFileDownload,
|
||||||
|
MdFindInPage,
|
||||||
|
MdHelpOutline,
|
||||||
|
MdLayers,
|
||||||
|
MdOpenInBrowser,
|
||||||
|
MdSettings,
|
||||||
|
} from "react-icons/md";
|
||||||
|
|
||||||
|
import logoImage from "maputnik-design/logos/logo-color.svg";
|
||||||
|
import pkgJson from "../../package.json";
|
||||||
|
|
||||||
|
// This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of.
|
||||||
|
const browser = detect();
|
||||||
|
const colorAccessibilityFiltersEnabled =
|
||||||
|
["chrome", "firefox"].indexOf(browser.name) > -1;
|
||||||
|
|
||||||
|
class IconText extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <span className="maputnik-icon-text">{this.props.children}</span>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToolbarLink extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
children: PropTypes.node,
|
||||||
|
href: PropTypes.string,
|
||||||
|
onToggleModal: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className={classnames("maputnik-toolbar-link", this.props.className)}
|
||||||
|
href={this.props.href}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{this.props.children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToolbarLinkHighlighted extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
children: PropTypes.node,
|
||||||
|
href: PropTypes.string,
|
||||||
|
onToggleModal: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className={classnames(
|
||||||
|
"maputnik-toolbar-link",
|
||||||
|
"maputnik-toolbar-link--highlighted",
|
||||||
|
this.props.className
|
||||||
|
)}
|
||||||
|
href={this.props.href}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span className="maputnik-toolbar-link-wrapper">
|
||||||
|
{this.props.children}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToolbarSelect extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
wdKey: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="maputnik-toolbar-select" data-wd-key={this.props.wdKey}>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToolbarAction extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
wdKey: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="maputnik-toolbar-action"
|
||||||
|
data-wd-key={this.props.wdKey}
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
>
|
||||||
|
{this.props.children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class AppToolbar extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
mapStyle: PropTypes.object.isRequired,
|
||||||
|
inspectModeEnabled: PropTypes.bool.isRequired,
|
||||||
|
onStyleChanged: PropTypes.func.isRequired,
|
||||||
|
// A new style has been uploaded
|
||||||
|
onStyleOpen: PropTypes.func.isRequired,
|
||||||
|
// A dict of source id's and the available source layers
|
||||||
|
sources: PropTypes.object.isRequired,
|
||||||
|
children: PropTypes.node,
|
||||||
|
onToggleModal: PropTypes.func,
|
||||||
|
onSetMapState: PropTypes.func,
|
||||||
|
mapState: PropTypes.string,
|
||||||
|
renderer: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
isOpen: {
|
||||||
|
settings: false,
|
||||||
|
sources: false,
|
||||||
|
open: false,
|
||||||
|
add: false,
|
||||||
|
export: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSelection(val) {
|
||||||
|
this.props.onSetMapState(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSkip = (target) => {
|
||||||
|
if (target === "map") {
|
||||||
|
document.querySelector(".maplibregl-canvas").focus();
|
||||||
|
} else {
|
||||||
|
const el = document.querySelector("#skip-target-" + target);
|
||||||
|
el.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const views = [
|
||||||
|
{
|
||||||
|
id: "map",
|
||||||
|
group: "general",
|
||||||
|
title: "Map",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "inspect",
|
||||||
|
group: "general",
|
||||||
|
title: "Inspect",
|
||||||
|
disabled: this.props.renderer === "ol",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "filter-deuteranopia",
|
||||||
|
group: "color-accessibility",
|
||||||
|
title: "Deuteranopia filter",
|
||||||
|
disabled: !colorAccessibilityFiltersEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "filter-protanopia",
|
||||||
|
group: "color-accessibility",
|
||||||
|
title: "Protanopia filter",
|
||||||
|
disabled: !colorAccessibilityFiltersEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "filter-tritanopia",
|
||||||
|
group: "color-accessibility",
|
||||||
|
title: "Tritanopia filter",
|
||||||
|
disabled: !colorAccessibilityFiltersEnabled,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "filter-achromatopsia",
|
||||||
|
group: "color-accessibility",
|
||||||
|
title: "Achromatopsia filter",
|
||||||
|
disabled: !colorAccessibilityFiltersEnabled,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const currentView = views.find((view) => {
|
||||||
|
return view.id === this.props.mapState;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="maputnik-toolbar">
|
||||||
|
<div className="maputnik-toolbar__inner">
|
||||||
|
<div className="maputnik-toolbar-logo-container">
|
||||||
|
{/* Keyboard accessible quick links */}
|
||||||
|
<button
|
||||||
|
data-wd-key="root:skip:layer-list"
|
||||||
|
className="maputnik-toolbar-skip"
|
||||||
|
onClick={(e) => this.onSkip("layer-list")}
|
||||||
|
>
|
||||||
|
Layers list
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
data-wd-key="root:skip:layer-editor"
|
||||||
|
className="maputnik-toolbar-skip"
|
||||||
|
onClick={(e) => this.onSkip("layer-editor")}
|
||||||
|
>
|
||||||
|
Layer editor
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
data-wd-key="root:skip:map-view"
|
||||||
|
className="maputnik-toolbar-skip"
|
||||||
|
onClick={(e) => this.onSkip("map")}
|
||||||
|
>
|
||||||
|
Map view
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
className="maputnik-toolbar-logo"
|
||||||
|
target="blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
href="https://github.com/maputnik/editor"
|
||||||
|
>
|
||||||
|
<span dangerouslySetInnerHTML={{ __html: logoImage }} />
|
||||||
|
<h1>
|
||||||
|
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
|
||||||
|
<span className="maputnik-toolbar-version">
|
||||||
|
v{pkgJson.version}
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="maputnik-toolbar__actions"
|
||||||
|
role="navigation"
|
||||||
|
aria-label="Toolbar"
|
||||||
|
>
|
||||||
|
<ToolbarAction
|
||||||
|
wdKey="nav:open"
|
||||||
|
onClick={this.props.onToggleModal.bind(this, "open")}
|
||||||
|
>
|
||||||
|
<MdOpenInBrowser />
|
||||||
|
<IconText>Open</IconText>
|
||||||
|
</ToolbarAction>
|
||||||
|
<ToolbarAction
|
||||||
|
wdKey="nav:export"
|
||||||
|
onClick={this.props.onToggleModal.bind(this, "export")}
|
||||||
|
>
|
||||||
|
<MdFileDownload />
|
||||||
|
<IconText>Export</IconText>
|
||||||
|
</ToolbarAction>
|
||||||
|
<ToolbarAction
|
||||||
|
wdKey="nav:sources"
|
||||||
|
onClick={this.props.onToggleModal.bind(this, "sources")}
|
||||||
|
>
|
||||||
|
<MdLayers />
|
||||||
|
<IconText>Data Sources</IconText>
|
||||||
|
</ToolbarAction>
|
||||||
|
<ToolbarAction
|
||||||
|
wdKey="nav:settings"
|
||||||
|
onClick={this.props.onToggleModal.bind(this, "settings")}
|
||||||
|
>
|
||||||
|
<MdSettings />
|
||||||
|
<IconText>Style Settings</IconText>
|
||||||
|
</ToolbarAction>
|
||||||
|
|
||||||
|
<ToolbarSelect wdKey="nav:inspect">
|
||||||
|
<MdFindInPage />
|
||||||
|
<label>
|
||||||
|
View
|
||||||
|
<select
|
||||||
|
className="maputnik-select"
|
||||||
|
data-wd-key="maputnik-select"
|
||||||
|
onChange={(e) => this.handleSelection(e.target.value)}
|
||||||
|
value={currentView.id}
|
||||||
|
>
|
||||||
|
{views
|
||||||
|
.filter((v) => v.group === "general")
|
||||||
|
.map((item) => {
|
||||||
|
return (
|
||||||
|
<option
|
||||||
|
key={item.id}
|
||||||
|
value={item.id}
|
||||||
|
disabled={item.disabled}
|
||||||
|
data-wd-key={item.id}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<optgroup label="Color accessibility">
|
||||||
|
{views
|
||||||
|
.filter((v) => v.group === "color-accessibility")
|
||||||
|
.map((item) => {
|
||||||
|
return (
|
||||||
|
<option
|
||||||
|
key={item.id}
|
||||||
|
value={item.id}
|
||||||
|
disabled={item.disabled}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</ToolbarSelect>
|
||||||
|
|
||||||
|
<ToolbarLink href={"https://github.com/maputnik/editor/wiki"}>
|
||||||
|
<MdHelpOutline />
|
||||||
|
<IconText>Help</IconText>
|
||||||
|
</ToolbarLink>
|
||||||
|
<ToolbarLinkHighlighted
|
||||||
|
href={"https://gregorywolanski.typeform.com/to/cPgaSY"}
|
||||||
|
>
|
||||||
|
<MdAssignmentTurnedIn />
|
||||||
|
<IconText>Take the Maputnik Survey</IconText>
|
||||||
|
</ToolbarLinkHighlighted>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import FieldDocLabel from './FieldDocLabel'
|
||||||
|
import Doc from './Doc'
|
||||||
|
|
||||||
|
|
||||||
|
/** Wrap a component with a label */
|
||||||
|
export default class Block extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
"data-wd-key": PropTypes.string,
|
||||||
|
label: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.element,
|
||||||
|
]),
|
||||||
|
action: PropTypes.element,
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
style: PropTypes.object,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
fieldSpec: PropTypes.object,
|
||||||
|
wideMode: PropTypes.bool,
|
||||||
|
error: PropTypes.array,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
showDoc: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(e) {
|
||||||
|
const value = e.target.value
|
||||||
|
return this.props.onChange(value === "" ? undefined : value)
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggleDoc = (val) => {
|
||||||
|
this.setState({
|
||||||
|
showDoc: val
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some fields for example <InputColor/> bind click events inside the element
|
||||||
|
* to close the picker. This in turn propagates to the <label/> element
|
||||||
|
* causing the picker to reopen. This causes a scenario where the picker can
|
||||||
|
* never be closed once open.
|
||||||
|
*/
|
||||||
|
onLabelClick = (event) => {
|
||||||
|
const el = event.nativeEvent.target;
|
||||||
|
const nativeEvent = event.nativeEvent;
|
||||||
|
const contains = this._blockEl.contains(el);
|
||||||
|
|
||||||
|
if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const errors = [].concat(this.props.error || []);
|
||||||
|
|
||||||
|
return <label style={this.props.style}
|
||||||
|
data-wd-key={this.props["data-wd-key"]}
|
||||||
|
className={classnames({
|
||||||
|
"maputnik-input-block": true,
|
||||||
|
"maputnik-input-block--wide": this.props.wideMode,
|
||||||
|
"maputnik-action-block": this.props.action
|
||||||
|
})}
|
||||||
|
onClick={this.onLabelClick}
|
||||||
|
>
|
||||||
|
{this.props.fieldSpec &&
|
||||||
|
<div className="maputnik-input-block-label">
|
||||||
|
<FieldDocLabel
|
||||||
|
label={this.props.label}
|
||||||
|
onToggleDoc={this.onToggleDoc}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{!this.props.fieldSpec &&
|
||||||
|
<div className="maputnik-input-block-label">
|
||||||
|
{this.props.label}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div className="maputnik-input-block-action">
|
||||||
|
{this.props.action}
|
||||||
|
</div>
|
||||||
|
<div className="maputnik-input-block-content" ref={el => this._blockEl = el}>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
{this.props.fieldSpec &&
|
||||||
|
<div
|
||||||
|
className="maputnik-doc-inline"
|
||||||
|
style={{display: this.state.showDoc ? '' : 'none'}}
|
||||||
|
>
|
||||||
|
<Doc fieldSpec={this.props.fieldSpec} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import Collapse from 'react-collapse'
|
import { Collapse as ReactCollapse } from 'react-collapse'
|
||||||
import accessibility from '../../libs/accessibility'
|
import accessibility from '../../libs/accessibility'
|
||||||
|
|
||||||
|
|
||||||
export default class CollapseAlt extends React.Component {
|
export default class Collapse extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
isActive: PropTypes.bool.isRequired,
|
isActive: PropTypes.bool.isRequired,
|
||||||
children: PropTypes.element.isRequired
|
children: PropTypes.element.isRequired
|
||||||
@@ -24,9 +24,9 @@ export default class CollapseAlt extends React.Component {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return (
|
return (
|
||||||
<Collapse isOpened={this.props.isActive}>
|
<ReactCollapse isOpened={this.props.isActive}>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</Collapse>
|
</ReactCollapse>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
export default class SpecDoc extends React.Component {
|
export default class Doc extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
fieldSpec: PropTypes.object.isRequired,
|
fieldSpec: PropTypes.object.isRequired,
|
||||||
}
|
}
|
||||||
@@ -21,14 +21,14 @@ export default class SpecDoc extends React.Component {
|
|||||||
|
|
||||||
const renderValues = (
|
const renderValues = (
|
||||||
!!values &&
|
!!values &&
|
||||||
// HACK: Currently we merge additional values into the stylespec, so this is required
|
// HACK: Currently we merge additional values into the style spec, so this is required
|
||||||
// See <https://github.com/maputnik/editor/blob/master/src/components/fields/PropertyGroup.jsx#L16>
|
// See <https://github.com/maputnik/editor/blob/main/src/components/PropertyGroup.jsx#L16>
|
||||||
!Array.isArray(values)
|
!Array.isArray(values)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{doc &&
|
{doc &&
|
||||||
<div className="SpecDoc">
|
<div className="SpecDoc">
|
||||||
<div className="SpecDoc__doc">{doc}</div>
|
<div className="SpecDoc__doc">{doc}</div>
|
||||||
{renderValues &&
|
{renderValues &&
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputArray from './InputArray'
|
||||||
|
import Fieldset from './Fieldset'
|
||||||
|
|
||||||
|
export default class FieldArray extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
...InputArray.propTypes,
|
||||||
|
name: PropTypes.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {props} = this;
|
||||||
|
|
||||||
|
return <Fieldset label={props.label}>
|
||||||
|
<InputArray {...props} />
|
||||||
|
</Fieldset>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputAutocomplete from './InputAutocomplete'
|
||||||
|
|
||||||
|
|
||||||
|
export default class FieldAutocomplete extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
...InputAutocomplete.propTypes,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {props} = this;
|
||||||
|
|
||||||
|
return <Block label={props.label}>
|
||||||
|
<InputAutocomplete {...props} />
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputCheckbox from './InputCheckbox'
|
||||||
|
|
||||||
|
|
||||||
|
export default class FieldCheckbox extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
...InputCheckbox.propTypes,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {props} = this;
|
||||||
|
|
||||||
|
return <Block label={this.props.label}>
|
||||||
|
<InputCheckbox {...props} />
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputColor from './InputColor'
|
||||||
|
|
||||||
|
|
||||||
|
export default class FieldColor extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
...InputColor.propTypes,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {props} = this;
|
||||||
|
|
||||||
|
return <Block label={props.label}>
|
||||||
|
<InputColor {...props} />
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import Block from './Block'
|
||||||
|
import InputString from './InputString'
|
||||||
|
|
||||||
|
export default class FieldComment extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: PropTypes.string,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const fieldSpec = {
|
||||||
|
doc: "Comments for the current layer. This is non-standard and not in the spec."
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Block
|
||||||
|
label={"Comments"}
|
||||||
|
fieldSpec={fieldSpec}
|
||||||
|
data-wd-key="layer-comment"
|
||||||
|
>
|
||||||
|
<InputString
|
||||||
|
multi={true}
|
||||||
|
value={this.props.value}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
default="Comment..."
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,14 +3,14 @@ import PropTypes from 'prop-types'
|
|||||||
|
|
||||||
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
|
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
|
||||||
|
|
||||||
export default class DocLabel extends React.Component {
|
export default class FieldDocLabel extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
label: PropTypes.oneOfType([
|
label: PropTypes.oneOfType([
|
||||||
PropTypes.object,
|
PropTypes.object,
|
||||||
PropTypes.string
|
PropTypes.string
|
||||||
]).isRequired,
|
]).isRequired,
|
||||||
fieldSpec: PropTypes.object.isRequired,
|
fieldSpec: PropTypes.object,
|
||||||
onToggleDoc: PropTypes.func.isRequired,
|
onToggleDoc: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputDynamicArray from './InputDynamicArray'
|
||||||
|
import Fieldset from './Fieldset'
|
||||||
|
|
||||||
|
export default class FieldDynamicArray extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
...InputDynamicArray.propTypes,
|
||||||
|
name: PropTypes.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {props} = this;
|
||||||
|
|
||||||
|
return <Fieldset label={props.label}>
|
||||||
|
<InputDynamicArray {...props} />
|
||||||
|
</Fieldset>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import InputEnum from './InputEnum'
|
||||||
|
import Block from './Block';
|
||||||
|
import Fieldset from './Fieldset';
|
||||||
|
|
||||||
|
|
||||||
|
export default class FieldEnum extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
...InputEnum.propTypes,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {props} = this;
|
||||||
|
|
||||||
|
return <Fieldset label={props.label}>
|
||||||
|
<InputEnum {...props} />
|
||||||
|
</Fieldset>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,13 +5,22 @@ import SpecProperty from './_SpecProperty'
|
|||||||
import DataProperty from './_DataProperty'
|
import DataProperty from './_DataProperty'
|
||||||
import ZoomProperty from './_ZoomProperty'
|
import ZoomProperty from './_ZoomProperty'
|
||||||
import ExpressionProperty from './_ExpressionProperty'
|
import ExpressionProperty from './_ExpressionProperty'
|
||||||
import {function as styleFunction} from '@mapbox/mapbox-gl-style-spec';
|
import {function as styleFunction} from '@maplibre/maplibre-gl-style-spec';
|
||||||
|
import {findDefaultFromSpec} from '../util/spec-helper';
|
||||||
|
|
||||||
|
|
||||||
function isLiteralExpression (value) {
|
function isLiteralExpression (value) {
|
||||||
return (Array.isArray(value) && value.length === 2 && value[0] === "literal");
|
return (Array.isArray(value) && value.length === 2 && value[0] === "literal");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isGetExpression (value) {
|
||||||
|
return (
|
||||||
|
Array.isArray(value) &&
|
||||||
|
value.length === 2 &&
|
||||||
|
value[0] === "get"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isZoomField(value) {
|
function isZoomField(value) {
|
||||||
return (
|
return (
|
||||||
typeof(value) === 'object' &&
|
typeof(value) === 'object' &&
|
||||||
@@ -28,7 +37,15 @@ function isZoomField(value) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDataField(value) {
|
function isIdentityProperty (value) {
|
||||||
|
return (
|
||||||
|
typeof(value) === 'object' &&
|
||||||
|
value.type === "identity" &&
|
||||||
|
value.hasOwnProperty("property")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDataStopProperty (value) {
|
||||||
return (
|
return (
|
||||||
typeof(value) === 'object' &&
|
typeof(value) === 'object' &&
|
||||||
value.stops &&
|
value.stops &&
|
||||||
@@ -45,6 +62,13 @@ function isDataField(value) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDataField(value) {
|
||||||
|
return (
|
||||||
|
isIdentityProperty(value) ||
|
||||||
|
isDataStopProperty(value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isPrimative (value) {
|
function isPrimative (value) {
|
||||||
const valid = ["string", "boolean", "number"];
|
const valid = ["string", "boolean", "number"];
|
||||||
return valid.includes(typeof(value));
|
return valid.includes(typeof(value));
|
||||||
@@ -78,29 +102,11 @@ function getDataType (value, fieldSpec={}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If we don't have a default value just make one up
|
|
||||||
*/
|
|
||||||
function findDefaultFromSpec (spec) {
|
|
||||||
if (spec.hasOwnProperty('default')) {
|
|
||||||
return spec.default;
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaults = {
|
|
||||||
'color': '#000000',
|
|
||||||
'string': '',
|
|
||||||
'boolean': false,
|
|
||||||
'number': 0,
|
|
||||||
'array': [],
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaults[spec.type] || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Supports displaying spec field for zoom function objects
|
/** Supports displaying spec field for zoom function objects
|
||||||
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
||||||
*/
|
*/
|
||||||
export default class FunctionSpecProperty extends React.Component {
|
export default class FieldFunction extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
fieldName: PropTypes.string.isRequired,
|
fieldName: PropTypes.string.isRequired,
|
||||||
@@ -194,19 +200,53 @@ export default class FunctionSpecProperty extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
makeZoomFunction = () => {
|
makeZoomFunction = () => {
|
||||||
const zoomFunc = {
|
const {value} = this.props;
|
||||||
stops: [
|
|
||||||
[6, this.props.value || findDefaultFromSpec(this.props.fieldSpec)],
|
let zoomFunc;
|
||||||
[10, this.props.value || findDefaultFromSpec(this.props.fieldSpec)]
|
if (typeof(value) === "object") {
|
||||||
]
|
if (value.stops) {
|
||||||
|
zoomFunc = {
|
||||||
|
base: value.base,
|
||||||
|
stops: value.stops.map(stop => {
|
||||||
|
return [stop[0].zoom, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
zoomFunc = {
|
||||||
|
base: value.base,
|
||||||
|
stops: [
|
||||||
|
[6, findDefaultFromSpec(this.props.fieldSpec)],
|
||||||
|
[10, findDefaultFromSpec(this.props.fieldSpec)]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
zoomFunc = {
|
||||||
|
stops: [
|
||||||
|
[6, value || findDefaultFromSpec(this.props.fieldSpec)],
|
||||||
|
[10, value || findDefaultFromSpec(this.props.fieldSpec)]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.props.onChange(this.props.fieldName, zoomFunc)
|
this.props.onChange(this.props.fieldName, zoomFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
undoExpression = () => {
|
undoExpression = () => {
|
||||||
const {value, fieldName} = this.props;
|
const {value, fieldName} = this.props;
|
||||||
|
|
||||||
if (isLiteralExpression(value)) {
|
if (isGetExpression(value)) {
|
||||||
|
this.props.onChange(fieldName, {
|
||||||
|
"type": "identity",
|
||||||
|
"property": value[1]
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
dataType: "value",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (isLiteralExpression(value)) {
|
||||||
this.props.onChange(fieldName, value[1]);
|
this.props.onChange(fieldName, value[1]);
|
||||||
this.setState({
|
this.setState({
|
||||||
dataType: "value",
|
dataType: "value",
|
||||||
@@ -217,6 +257,7 @@ export default class FunctionSpecProperty extends React.Component {
|
|||||||
canUndo = () => {
|
canUndo = () => {
|
||||||
const {value, fieldSpec} = this.props;
|
const {value, fieldSpec} = this.props;
|
||||||
return (
|
return (
|
||||||
|
isGetExpression(value) ||
|
||||||
isLiteralExpression(value) ||
|
isLiteralExpression(value) ||
|
||||||
isPrimative(value) ||
|
isPrimative(value) ||
|
||||||
(Array.isArray(value) && fieldSpec.type === "array")
|
(Array.isArray(value) && fieldSpec.type === "array")
|
||||||
@@ -230,6 +271,9 @@ export default class FunctionSpecProperty extends React.Component {
|
|||||||
if (typeof(value) === "object" && 'stops' in value) {
|
if (typeof(value) === "object" && 'stops' in value) {
|
||||||
expression = styleFunction.convertFunction(value, fieldSpec);
|
expression = styleFunction.convertFunction(value, fieldSpec);
|
||||||
}
|
}
|
||||||
|
else if (isIdentityProperty(value)) {
|
||||||
|
expression = ["get", value.property];
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
expression = ["literal", value || this.props.fieldSpec.default];
|
expression = ["literal", value || this.props.fieldSpec.default];
|
||||||
}
|
}
|
||||||
@@ -239,14 +283,44 @@ export default class FunctionSpecProperty extends React.Component {
|
|||||||
makeDataFunction = () => {
|
makeDataFunction = () => {
|
||||||
const functionType = this.getFieldFunctionType(this.props.fieldSpec);
|
const functionType = this.getFieldFunctionType(this.props.fieldSpec);
|
||||||
const stopValue = functionType === 'categorical' ? '' : 0;
|
const stopValue = functionType === 'categorical' ? '' : 0;
|
||||||
const dataFunc = {
|
const {value} = this.props;
|
||||||
property: "",
|
let dataFunc;
|
||||||
type: functionType,
|
|
||||||
stops: [
|
if (typeof(value) === "object") {
|
||||||
[{zoom: 6, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)],
|
if (value.stops) {
|
||||||
[{zoom: 10, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)]
|
dataFunc = {
|
||||||
]
|
property: "",
|
||||||
|
type: functionType,
|
||||||
|
base: value.base,
|
||||||
|
stops: value.stops.map(stop => {
|
||||||
|
return [{zoom: stop[0], value: stopValue}, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dataFunc = {
|
||||||
|
property: "",
|
||||||
|
type: functionType,
|
||||||
|
base: value.base,
|
||||||
|
stops: [
|
||||||
|
[{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)],
|
||||||
|
[{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
dataFunc = {
|
||||||
|
property: "",
|
||||||
|
type: functionType,
|
||||||
|
base: value.base,
|
||||||
|
stops: [
|
||||||
|
[{zoom: 6, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)],
|
||||||
|
[{zoom: 10, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.props.onChange(this.props.fieldName, dataFunc)
|
this.props.onChange(this.props.fieldName, dataFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,11 +365,13 @@ export default class FunctionSpecProperty extends React.Component {
|
|||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onDeleteStop={this.deleteStop}
|
onDeleteStop={this.deleteStop}
|
||||||
onAddStop={this.addStop}
|
onAddStop={this.addStop}
|
||||||
onExpressionClick={this.makeExpression}
|
onChangeToDataFunction={this.makeDataFunction}
|
||||||
|
onExpressionClick={this.makeExpression}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else if (dataType === "data_function") {
|
else if (dataType === "data_function") {
|
||||||
|
// TODO: Rename to FieldFunction **this file** shouldn't be called that
|
||||||
specField = (
|
specField = (
|
||||||
<DataProperty
|
<DataProperty
|
||||||
errors={this.props.errors}
|
errors={this.props.errors}
|
||||||
@@ -306,7 +382,8 @@ export default class FunctionSpecProperty extends React.Component {
|
|||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onDeleteStop={this.deleteStop}
|
onDeleteStop={this.deleteStop}
|
||||||
onAddStop={this.addStop}
|
onAddStop={this.addStop}
|
||||||
onExpressionClick={this.makeExpression}
|
onChangeToZoomFunction={this.makeZoomFunction}
|
||||||
|
onExpressionClick={this.makeExpression}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -320,8 +397,8 @@ export default class FunctionSpecProperty extends React.Component {
|
|||||||
fieldSpec={this.props.fieldSpec}
|
fieldSpec={this.props.fieldSpec}
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onZoomClick={this.makeZoomFunction}
|
onZoomClick={this.makeZoomFunction}
|
||||||
onDataClick={this.makeDataFunction}
|
onDataClick={this.makeDataFunction}
|
||||||
onExpressionClick={this.makeExpression}
|
onExpressionClick={this.makeExpression}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputString from './InputString'
|
||||||
|
|
||||||
|
export default class FieldId extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
wdKey: PropTypes.string.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <Block label={"ID"} fieldSpec={latest.layer.id}
|
||||||
|
data-wd-key={this.props.wdKey}
|
||||||
|
error={this.props.error}
|
||||||
|
>
|
||||||
|
<InputString
|
||||||
|
value={this.props.value}
|
||||||
|
onInput={this.props.onChange}
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import InputJson from './InputJson'
|
||||||
|
|
||||||
|
|
||||||
|
export default class FieldJson extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
...InputJson.propTypes,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {props} = this;
|
||||||
|
return <InputJson {...props} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputNumber from './InputNumber'
|
||||||
|
|
||||||
|
export default class FieldMaxZoom extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: PropTypes.number,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <Block label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
|
||||||
|
error={this.props.error}
|
||||||
|
data-wd-key="max-zoom"
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
allowRange={true}
|
||||||
|
value={this.props.value}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
min={latest.layer.maxzoom.minimum}
|
||||||
|
max={latest.layer.maxzoom.maximum}
|
||||||
|
default={latest.layer.maxzoom.maximum}
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputNumber from './InputNumber'
|
||||||
|
|
||||||
|
export default class FieldMinZoom extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: PropTypes.number,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <Block label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
|
||||||
|
error={this.props.error}
|
||||||
|
data-wd-key="min-zoom"
|
||||||
|
>
|
||||||
|
<InputNumber
|
||||||
|
allowRange={true}
|
||||||
|
value={this.props.value}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
min={latest.layer.minzoom.minimum}
|
||||||
|
max={latest.layer.minzoom.maximum}
|
||||||
|
default={latest.layer.minzoom.minimum}
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputMultiInput from './InputMultiInput'
|
||||||
|
import Fieldset from './Fieldset'
|
||||||
|
|
||||||
|
|
||||||
|
export default class FieldMultiInput extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
...InputMultiInput.propTypes,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {props} = this;
|
||||||
|
|
||||||
|
return <Fieldset label={props.label}>
|
||||||
|
<InputMultiInput {...props} />
|
||||||
|
</Fieldset>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import InputNumber from './InputNumber'
|
||||||
|
import Block from './Block'
|
||||||
|
|
||||||
|
|
||||||
|
export default class FieldNumber extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
...InputNumber.propTypes,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {props} = this;
|
||||||
|
return <Block label={props.label}>
|
||||||
|
<InputNumber {...props} />
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputSelect from './InputSelect'
|
||||||
|
|
||||||
|
|
||||||
|
export default class FieldSelect extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
...InputSelect.propTypes,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {props} = this;
|
||||||
|
|
||||||
|
return <Block label={props.label}>
|
||||||
|
<InputSelect {...props}/>
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputAutocomplete from './InputAutocomplete'
|
||||||
|
|
||||||
|
export default class FieldSource extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: PropTypes.string,
|
||||||
|
wdKey: PropTypes.string,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
sourceIds: PropTypes.array,
|
||||||
|
error: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
onChange: () => {},
|
||||||
|
sourceIds: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <Block
|
||||||
|
label={"Source"}
|
||||||
|
fieldSpec={latest.layer.source}
|
||||||
|
error={this.props.error}
|
||||||
|
data-wd-key={this.props.wdKey}
|
||||||
|
>
|
||||||
|
<InputAutocomplete
|
||||||
|
value={this.props.value}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
options={this.props.sourceIds.map(src => [src, src])}
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputAutocomplete from './InputAutocomplete'
|
||||||
|
|
||||||
|
export default class FieldSourceLayer extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: PropTypes.string,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
sourceLayerIds: PropTypes.array,
|
||||||
|
isFixed: PropTypes.bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
onChange: () => {},
|
||||||
|
sourceLayerIds: [],
|
||||||
|
isFixed: false
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <Block label={"Source Layer"} fieldSpec={latest.layer['source-layer']}
|
||||||
|
data-wd-key="layer-source-layer"
|
||||||
|
>
|
||||||
|
<InputAutocomplete
|
||||||
|
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
||||||
|
value={this.props.value}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
options={this.props.sourceLayerIds.map(l => [l, l])}
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputString from './InputString'
|
||||||
|
|
||||||
|
export default class FieldString extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
...InputString.propTypes,
|
||||||
|
name: PropTypes.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {props} = this;
|
||||||
|
|
||||||
|
return <Block label={props.label}>
|
||||||
|
<InputString {...props} />
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputSelect from './InputSelect'
|
||||||
|
import InputString from './InputString'
|
||||||
|
|
||||||
|
export default class FieldType extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
wdKey: PropTypes.string,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
disabled: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <Block label={"Type"} fieldSpec={latest.layer.type}
|
||||||
|
data-wd-key={this.props.wdKey}
|
||||||
|
error={this.props.error}
|
||||||
|
>
|
||||||
|
{this.props.disabled &&
|
||||||
|
<InputString
|
||||||
|
value={this.props.value}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{!this.props.disabled &&
|
||||||
|
<InputSelect
|
||||||
|
options={[
|
||||||
|
['background', 'Background'],
|
||||||
|
['fill', 'Fill'],
|
||||||
|
['line', 'Line'],
|
||||||
|
['symbol', 'Symbol'],
|
||||||
|
['raster', 'Raster'],
|
||||||
|
['circle', 'Circle'],
|
||||||
|
['fill-extrusion', 'Fill Extrusion'],
|
||||||
|
['hillshade', 'Hillshade'],
|
||||||
|
['heatmap', 'Heatmap'],
|
||||||
|
]}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
value={this.props.value}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import React, {Fragment} from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import InputUrl from './InputUrl'
|
||||||
|
import Block from './Block'
|
||||||
|
|
||||||
|
|
||||||
|
export default class FieldUrl extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
...InputUrl.propTypes,
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {props} = this;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Block label={this.props.label}>
|
||||||
|
<InputUrl {...props} />
|
||||||
|
</Block>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import FieldDocLabel from './FieldDocLabel'
|
||||||
|
import Doc from './Doc'
|
||||||
|
|
||||||
|
|
||||||
|
let IDX = 0;
|
||||||
|
|
||||||
|
export default class Fieldset extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
this._labelId = `fieldset_label_${(IDX++)}`;
|
||||||
|
this.state = {
|
||||||
|
showDoc: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggleDoc = (val) => {
|
||||||
|
this.setState({
|
||||||
|
showDoc: val
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {props} = this;
|
||||||
|
|
||||||
|
return <div className="maputnik-input-block" role="group" aria-labelledby={this._labelId}>
|
||||||
|
{this.props.fieldSpec &&
|
||||||
|
<div className="maputnik-input-block-label">
|
||||||
|
<FieldDocLabel
|
||||||
|
label={this.props.label}
|
||||||
|
onToggleDoc={this.onToggleDoc}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{!this.props.fieldSpec &&
|
||||||
|
<div className="maputnik-input-block-label">
|
||||||
|
{props.label}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div className="maputnik-input-block-action">
|
||||||
|
{this.props.action}
|
||||||
|
</div>
|
||||||
|
<div className="maputnik-input-block-content">
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
{this.props.fieldSpec &&
|
||||||
|
<div
|
||||||
|
className="maputnik-doc-inline"
|
||||||
|
style={{display: this.state.showDoc ? '' : 'none'}}
|
||||||
|
>
|
||||||
|
<Doc fieldSpec={this.props.fieldSpec} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { combiningFilterOps } from '../../libs/filterops.js'
|
import { combiningFilterOps } from '../libs/filterops.js'
|
||||||
|
import {mdiTableRowPlusAfter} from '@mdi/js';
|
||||||
|
import {isEqual} from 'lodash';
|
||||||
|
|
||||||
import {latest, validate, migrate} from '@mapbox/mapbox-gl-style-spec'
|
import {latest, migrate, convertFilter} from '@maplibre/maplibre-gl-style-spec'
|
||||||
import DocLabel from '../fields/DocLabel'
|
import InputSelect from './InputSelect'
|
||||||
import SelectInput from '../inputs/SelectInput'
|
import Block from './Block'
|
||||||
import InputBlock from '../inputs/InputBlock'
|
|
||||||
import SingleFilterEditor from './SingleFilterEditor'
|
import SingleFilterEditor from './SingleFilterEditor'
|
||||||
import FilterEditorBlock from './FilterEditorBlock'
|
import FilterEditorBlock from './FilterEditorBlock'
|
||||||
import Button from '../Button'
|
import InputButton from './InputButton'
|
||||||
import SpecDoc from '../inputs/SpecDoc'
|
import Doc from './Doc'
|
||||||
import ExpressionProperty from '../fields/_ExpressionProperty';
|
import ExpressionProperty from './_ExpressionProperty';
|
||||||
import {mdiFunctionVariant} from '@mdi/js';
|
import {mdiFunctionVariant} from '@mdi/js';
|
||||||
|
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ function createStyleFromFilter (filter) {
|
|||||||
"id": "tmp",
|
"id": "tmp",
|
||||||
"version": 8,
|
"version": 8,
|
||||||
"name": "Empty Style",
|
"name": "Empty Style",
|
||||||
"metadata": {"maputnik:renderer": "mbgljs"},
|
"metadata": {"maputnik:renderer": "mlgljs"},
|
||||||
"sources": {
|
"sources": {
|
||||||
"tmp": {
|
"tmp": {
|
||||||
"type": "geojson",
|
"type": "geojson",
|
||||||
@@ -61,24 +62,19 @@ function createStyleFromFilter (filter) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const FILTER_OPS = [
|
||||||
* This is doing way more work than we need it to, however validating a whole
|
"all",
|
||||||
* style if the only thing that's exported from mapbox-gl-style-spec at the
|
"any",
|
||||||
* moment. Not really an issue though as it take ~0.1ms to calculate.
|
"none"
|
||||||
*/
|
];
|
||||||
|
|
||||||
|
// If we convert a filter that is an expression to an expression it'll remain the same in value
|
||||||
function checkIfSimpleFilter (filter) {
|
function checkIfSimpleFilter (filter) {
|
||||||
if (!filter || !combiningFilterOps.includes(filter[0])) {
|
if (filter.length === 1 && FILTER_OPS.includes(filter[0])) {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
const expression = convertFilter(filter);
|
||||||
// Because "none" isn't supported by the next expression syntax we can test
|
return !isEqual(expression, filter);
|
||||||
// with ["none", ...] because it'll return false if it's a new style
|
|
||||||
// expression.
|
|
||||||
const moddedFilter = ["none", ...filter.slice(1)];
|
|
||||||
const tmpStyle = createStyleFromFilter(moddedFilter)
|
|
||||||
|
|
||||||
const errors = validate(tmpStyle);
|
|
||||||
return (errors.length < 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCombiningFilter(filter) {
|
function hasCombiningFilter(filter) {
|
||||||
@@ -93,7 +89,7 @@ function hasNestedCombiningFilter(filter) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CombiningFilterEditor extends React.Component {
|
export default class FilterEditor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
/** Properties of the vector layer and the available fields */
|
/** Properties of the vector layer and the available fields */
|
||||||
properties: PropTypes.object,
|
properties: PropTypes.object,
|
||||||
@@ -191,14 +187,15 @@ export default class CombiningFilterEditor extends React.Component {
|
|||||||
<p>
|
<p>
|
||||||
Nested filters are not supported.
|
Nested filters are not supported.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<InputButton
|
||||||
onClick={this.makeExpression}
|
onClick={this.makeExpression}
|
||||||
|
title="Convert to expression"
|
||||||
>
|
>
|
||||||
<svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
<svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||||
</svg>
|
</svg>
|
||||||
Upgrade to expression
|
Upgrade to expression
|
||||||
</Button>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else if (displaySimpleFilter) {
|
else if (displaySimpleFilter) {
|
||||||
@@ -208,14 +205,15 @@ export default class CombiningFilterEditor extends React.Component {
|
|||||||
|
|
||||||
const actions = (
|
const actions = (
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<InputButton
|
||||||
onClick={this.makeExpression}
|
onClick={this.makeExpression}
|
||||||
|
title="Convert to expression"
|
||||||
className="maputnik-make-zoom-function"
|
className="maputnik-make-zoom-function"
|
||||||
>
|
>
|
||||||
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -223,7 +221,7 @@ export default class CombiningFilterEditor extends React.Component {
|
|||||||
const error = errors[`filter[${idx+1}]`];
|
const error = errors[`filter[${idx+1}]`];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div key={`block-${idx}`}>
|
||||||
<FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
|
<FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
|
||||||
<SingleFilterEditor
|
<SingleFilterEditor
|
||||||
properties={this.props.properties}
|
properties={this.props.properties}
|
||||||
@@ -232,45 +230,48 @@ export default class CombiningFilterEditor extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</FilterEditorBlock>
|
</FilterEditorBlock>
|
||||||
{error &&
|
{error &&
|
||||||
<div className="maputnik-inline-error">{error.message}</div>
|
<div key="error" className="maputnik-inline-error">{error.message}</div>
|
||||||
}
|
}
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<InputBlock
|
<Block
|
||||||
key="top"
|
key="top"
|
||||||
fieldSpec={fieldSpec}
|
fieldSpec={fieldSpec}
|
||||||
label={"Filter"}
|
label={"Filter"}
|
||||||
action={actions}
|
action={actions}
|
||||||
>
|
>
|
||||||
<SelectInput
|
<InputSelect
|
||||||
value={combiningOp}
|
value={combiningOp}
|
||||||
onChange={this.onFilterPartChanged.bind(this, 0)}
|
onChange={this.onFilterPartChanged.bind(this, 0)}
|
||||||
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
|
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</Block>
|
||||||
{editorBlocks}
|
{editorBlocks}
|
||||||
<div
|
<div
|
||||||
key="buttons"
|
key="buttons"
|
||||||
className="maputnik-filter-editor-add-wrapper"
|
className="maputnik-filter-editor-add-wrapper"
|
||||||
>
|
>
|
||||||
<Button
|
<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={this.addFilterItem}
|
||||||
Add filter
|
>
|
||||||
</Button>
|
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
||||||
|
</svg> Add filter
|
||||||
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
key="doc"
|
key="doc"
|
||||||
className="maputnik-doc-inline"
|
className="maputnik-doc-inline"
|
||||||
style={{display: this.state.showDoc ? '' : 'none'}}
|
style={{display: this.state.showDoc ? '' : 'none'}}
|
||||||
>
|
>
|
||||||
<SpecDoc fieldSpec={fieldSpec} />
|
<Doc fieldSpec={fieldSpec} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import Button from '../Button'
|
import InputButton from './InputButton'
|
||||||
import {MdDelete} from 'react-icons/md'
|
import {MdDelete} from 'react-icons/md'
|
||||||
|
|
||||||
class FilterEditorBlock extends React.Component {
|
export default class FilterEditorBlock extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onDelete: PropTypes.func.isRequired,
|
onDelete: PropTypes.func.isRequired,
|
||||||
children: PropTypes.element.isRequired,
|
children: PropTypes.element.isRequired,
|
||||||
@@ -12,12 +12,13 @@ class FilterEditorBlock extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
return <div className="maputnik-filter-editor-block">
|
return <div className="maputnik-filter-editor-block">
|
||||||
<div className="maputnik-filter-editor-block-action">
|
<div className="maputnik-filter-editor-block-action">
|
||||||
<Button
|
<InputButton
|
||||||
className="maputnik-delete-filter"
|
className="maputnik-delete-filter"
|
||||||
onClick={this.props.onDelete}
|
onClick={this.props.onDelete}
|
||||||
|
title="Delete filter block"
|
||||||
>
|
>
|
||||||
<MdDelete />
|
<MdDelete />
|
||||||
</Button>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="maputnik-filter-editor-block-content">
|
<div className="maputnik-filter-editor-block-content">
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
@@ -26,4 +27,3 @@ class FilterEditorBlock extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FilterEditorBlock
|
|
||||||
@@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import IconBase from 'react-icon-base'
|
import IconBase from 'react-icon-base'
|
||||||
|
|
||||||
|
|
||||||
export default class BackgroundIcon extends React.Component {
|
export default class IconBackground extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||||
@@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import IconBase from 'react-icon-base'
|
import IconBase from 'react-icon-base'
|
||||||
|
|
||||||
|
|
||||||
export default class FillIcon extends React.Component {
|
export default class IconCircle extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||||
@@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import IconBase from 'react-icon-base'
|
import IconBase from 'react-icon-base'
|
||||||
|
|
||||||
|
|
||||||
export default class FillIcon extends React.Component {
|
export default class IconFill extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import IconLine from './IconLine.jsx'
|
||||||
|
import IconFill from './IconFill.jsx'
|
||||||
|
import IconSymbol from './IconSymbol.jsx'
|
||||||
|
import IconBackground from './IconBackground.jsx'
|
||||||
|
import IconCircle from './IconCircle.jsx'
|
||||||
|
import IconMissing from './IconMissing.jsx'
|
||||||
|
|
||||||
|
export default class IconLayer extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
type: PropTypes.string.isRequired,
|
||||||
|
style: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const iconProps = { style: this.props.style }
|
||||||
|
switch(this.props.type) {
|
||||||
|
case 'fill-extrusion': return <IconBackground {...iconProps} />
|
||||||
|
case 'raster': return <IconFill {...iconProps} />
|
||||||
|
case 'hillshade': return <IconFill {...iconProps} />
|
||||||
|
case 'heatmap': return <IconFill {...iconProps} />
|
||||||
|
case 'fill': return <IconFill {...iconProps} />
|
||||||
|
case 'background': return <IconBackground {...iconProps} />
|
||||||
|
case 'line': return <IconLine {...iconProps} />
|
||||||
|
case 'symbol': return <IconSymbol {...iconProps} />
|
||||||
|
case 'circle': return <IconCircle {...iconProps} />
|
||||||
|
default: return <IconMissing {...iconProps} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import IconBase from 'react-icon-base'
|
import IconBase from 'react-icon-base'
|
||||||
|
|
||||||
|
|
||||||
export default class FillIcon extends React.Component {
|
export default class IconLine extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||||
@@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import {MdPriorityHigh} from 'react-icons/md'
|
import {MdPriorityHigh} from 'react-icons/md'
|
||||||
|
|
||||||
|
|
||||||
export default class MissingIcon extends React.Component {
|
export default class IconMissing extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<MdPriorityHigh {...this.props} />
|
<MdPriorityHigh {...this.props} />
|
||||||
@@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import IconBase from 'react-icon-base'
|
import IconBase from 'react-icon-base'
|
||||||
|
|
||||||
|
|
||||||
export default class SymbolIcon extends React.Component {
|
export default class IconSymbol extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import StringInput from './StringInput'
|
import InputString from './InputString'
|
||||||
import NumberInput from './NumberInput'
|
import InputNumber from './InputNumber'
|
||||||
|
|
||||||
class ArrayInput extends React.Component {
|
export default class FieldArray extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.array,
|
value: PropTypes.array,
|
||||||
type: PropTypes.string,
|
type: PropTypes.string,
|
||||||
length: PropTypes.number,
|
length: PropTypes.number,
|
||||||
default: PropTypes.array,
|
default: PropTypes.array,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
|
'aria-label': PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@@ -82,28 +83,31 @@ class ArrayInput extends React.Component {
|
|||||||
|
|
||||||
const inputs = Array(this.props.length).fill(null).map((_, i) => {
|
const inputs = Array(this.props.length).fill(null).map((_, i) => {
|
||||||
if(this.props.type === 'number') {
|
if(this.props.type === 'number') {
|
||||||
return <NumberInput
|
return <InputNumber
|
||||||
key={i}
|
key={i}
|
||||||
default={containsValues ? undefined : this.props.default[i]}
|
default={containsValues ? undefined : this.props.default[i]}
|
||||||
value={value[i]}
|
value={value[i]}
|
||||||
required={containsValues ? true : false}
|
required={containsValues ? true : false}
|
||||||
onChange={this.changeValue.bind(this, i)}
|
onChange={this.changeValue.bind(this, i)}
|
||||||
|
aria-label={this.props['aria-label'] || this.props.label}
|
||||||
/>
|
/>
|
||||||
} else {
|
} else {
|
||||||
return <StringInput
|
return <InputString
|
||||||
key={i}
|
key={i}
|
||||||
default={containsValues ? undefined : this.props.default[i]}
|
default={containsValues ? undefined : this.props.default[i]}
|
||||||
value={value[i]}
|
value={value[i]}
|
||||||
required={containsValues ? true : false}
|
required={containsValues ? true : false}
|
||||||
onChange={this.changeValue.bind(this, i)}
|
onChange={this.changeValue.bind(this, i)}
|
||||||
|
aria-label={this.props['aria-label'] || this.props.label}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return <div className="maputnik-array">
|
return (
|
||||||
{inputs}
|
<div className="maputnik-array">
|
||||||
</div>
|
{inputs}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ArrayInput
|
|
||||||
@@ -6,12 +6,13 @@ import Autocomplete from 'react-autocomplete'
|
|||||||
|
|
||||||
const MAX_HEIGHT = 140;
|
const MAX_HEIGHT = 140;
|
||||||
|
|
||||||
class AutocompleteInput extends React.Component {
|
export default class InputAutocomplete extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
options: PropTypes.array,
|
options: PropTypes.array,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
keepMenuWithinWindowBounds: PropTypes.bool
|
keepMenuWithinWindowBounds: PropTypes.bool,
|
||||||
|
'aria-label': PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@@ -66,6 +67,7 @@ class AutocompleteInput extends React.Component {
|
|||||||
style: null
|
style: null
|
||||||
}}
|
}}
|
||||||
inputProps={{
|
inputProps={{
|
||||||
|
'aria-label': this.props['aria-label'],
|
||||||
className: "maputnik-string",
|
className: "maputnik-string",
|
||||||
spellCheck: false
|
spellCheck: false
|
||||||
}}
|
}}
|
||||||
@@ -95,4 +97,4 @@ class AutocompleteInput extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AutocompleteInput
|
|
||||||
@@ -2,7 +2,7 @@ import React from 'react'
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
|
||||||
class Button extends React.Component {
|
export default class InputButton extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
"data-wd-key": PropTypes.string,
|
"data-wd-key": PropTypes.string,
|
||||||
"aria-label": PropTypes.string,
|
"aria-label": PropTypes.string,
|
||||||
@@ -11,10 +11,16 @@ class Button extends React.Component {
|
|||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
|
type: PropTypes.string,
|
||||||
|
id: PropTypes.string,
|
||||||
|
title: PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <button
|
return <button
|
||||||
|
id={this.props.id}
|
||||||
|
title={this.props.title}
|
||||||
|
type={this.props.type}
|
||||||
onClick={this.props.onClick}
|
onClick={this.props.onClick}
|
||||||
disabled={this.props.disabled}
|
disabled={this.props.disabled}
|
||||||
aria-label={this.props["aria-label"]}
|
aria-label={this.props["aria-label"]}
|
||||||
@@ -27,4 +33,3 @@ class Button extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Button
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
class CheckboxInput extends React.Component {
|
export default class InputCheckbox extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.bool,
|
value: PropTypes.bool,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
@@ -12,13 +12,18 @@ class CheckboxInput extends React.Component {
|
|||||||
value: false,
|
value: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onChange = () => {
|
||||||
|
this.props.onChange(!this.props.value);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <label 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={this.props.style}
|
||||||
onChange={e => this.props.onChange(!this.props.value)}
|
onChange={this.onChange}
|
||||||
|
onClick={this.onChange}
|
||||||
checked={this.props.value}
|
checked={this.props.value}
|
||||||
/>
|
/>
|
||||||
<div className="maputnik-checkbox-box">
|
<div className="maputnik-checkbox-box">
|
||||||
@@ -28,8 +33,7 @@ class CheckboxInput extends React.Component {
|
|||||||
<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>
|
||||||
</label>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CheckboxInput
|
|
||||||
@@ -10,7 +10,7 @@ function formatColor(color) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*** Number fields with support for min, max and units and documentation*/
|
/*** Number fields with support for min, max and units and documentation*/
|
||||||
class ColorField extends React.Component {
|
export default class InputColor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
@@ -18,6 +18,7 @@ class ColorField extends React.Component {
|
|||||||
doc: PropTypes.string,
|
doc: PropTypes.string,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
default: PropTypes.string,
|
default: PropTypes.string,
|
||||||
|
'aria-label': PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@@ -116,7 +117,9 @@ class ColorField extends React.Component {
|
|||||||
{this.state.pickerOpened && picker}
|
{this.state.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']}
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
|
autoComplete="off"
|
||||||
className="maputnik-color"
|
className="maputnik-color"
|
||||||
ref={(input) => this.colorInput = input}
|
ref={(input) => this.colorInput = input}
|
||||||
onClick={this.togglePicker}
|
onClick={this.togglePicker}
|
||||||
@@ -130,4 +133,3 @@ class ColorField extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ColorField
|
|
||||||
+44
-29
@@ -1,15 +1,16 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import StringInput from './StringInput'
|
import InputString from './InputString'
|
||||||
import NumberInput from './NumberInput'
|
import InputNumber from './InputNumber'
|
||||||
import Button from '../Button'
|
import InputButton from './InputButton'
|
||||||
import {MdDelete} from 'react-icons/md'
|
import {MdDelete} from 'react-icons/md'
|
||||||
import DocLabel from '../fields/DocLabel'
|
import FieldDocLabel from './FieldDocLabel'
|
||||||
import EnumInput from '../inputs/SelectInput'
|
import InputEnum from './InputEnum'
|
||||||
import capitalize from 'lodash.capitalize'
|
import capitalize from 'lodash.capitalize'
|
||||||
|
import InputUrl from './InputUrl'
|
||||||
|
|
||||||
|
|
||||||
class DynamicArrayInput extends React.Component {
|
export default class FieldDynamicArray extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.array,
|
value: PropTypes.array,
|
||||||
type: PropTypes.string,
|
type: PropTypes.string,
|
||||||
@@ -17,10 +18,10 @@ class DynamicArrayInput extends React.Component {
|
|||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
fieldSpec: PropTypes.object,
|
fieldSpec: PropTypes.object,
|
||||||
|
'aria-label': PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
changeValue(idx, newValue) {
|
changeValue(idx, newValue) {
|
||||||
console.log(idx, newValue)
|
|
||||||
const values = this.values.slice(0)
|
const values = this.values.slice(0)
|
||||||
values[idx] = newValue
|
values[idx] = newValue
|
||||||
this.props.onChange(values)
|
this.props.onChange(values)
|
||||||
@@ -35,6 +36,9 @@ class DynamicArrayInput extends React.Component {
|
|||||||
if (this.props.type === 'number') {
|
if (this.props.type === 'number') {
|
||||||
values.push(0)
|
values.push(0)
|
||||||
}
|
}
|
||||||
|
else if (this.props.type === 'url') {
|
||||||
|
values.push("");
|
||||||
|
}
|
||||||
else if (this.props.type === 'enum') {
|
else if (this.props.type === 'enum') {
|
||||||
const {fieldSpec} = this.props;
|
const {fieldSpec} = this.props;
|
||||||
const defaultValue = Object.keys(fieldSpec.values)[0];
|
const defaultValue = Object.keys(fieldSpec.values)[0];
|
||||||
@@ -50,32 +54,41 @@ class DynamicArrayInput extends React.Component {
|
|||||||
const values = this.values.slice(0)
|
const values = this.values.slice(0)
|
||||||
values.splice(valueIdx, 1)
|
values.splice(valueIdx, 1)
|
||||||
|
|
||||||
this.props.onChange(values)
|
this.props.onChange(values.length > 0 ? values : undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const inputs = this.values.map((v, i) => {
|
const inputs = this.values.map((v, i) => {
|
||||||
const deleteValueBtn= <DeleteValueButton onClick={this.deleteValue.bind(this, i)} />
|
const deleteValueBtn= <DeleteValueInputButton onClick={this.deleteValue.bind(this, i)} />
|
||||||
let input;
|
let input;
|
||||||
if (this.props.type === 'number') {
|
if(this.props.type === 'url') {
|
||||||
input = <NumberInput
|
input = <InputUrl
|
||||||
value={v}
|
value={v}
|
||||||
onChange={this.changeValue.bind(this, i)}
|
onChange={this.changeValue.bind(this, i)}
|
||||||
|
aria-label={this.props['aria-label'] || this.props.label}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
else if (this.props.type === 'number') {
|
||||||
|
input = <InputNumber
|
||||||
|
value={v}
|
||||||
|
onChange={this.changeValue.bind(this, i)}
|
||||||
|
aria-label={this.props['aria-label'] || this.props.label}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
else if (this.props.type === 'enum') {
|
else if (this.props.type === 'enum') {
|
||||||
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)]);
|
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)]);
|
||||||
|
input = <InputEnum
|
||||||
input = <EnumInput
|
|
||||||
options={options}
|
options={options}
|
||||||
value={v}
|
value={v}
|
||||||
onChange={this.changeValue.bind(this, i)}
|
onChange={this.changeValue.bind(this, i)}
|
||||||
|
aria-label={this.props['aria-label'] || this.props.label}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
input = <StringInput
|
input = <InputString
|
||||||
value={v}
|
value={v}
|
||||||
onChange={this.changeValue.bind(this, i)}
|
onChange={this.changeValue.bind(this, i)}
|
||||||
|
aria-label={this.props['aria-label'] || this.props.label}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,34 +106,36 @@ class DynamicArrayInput extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
})
|
})
|
||||||
|
|
||||||
return <div className="maputnik-array">
|
return (
|
||||||
{inputs}
|
<div className="maputnik-array">
|
||||||
<Button
|
{inputs}
|
||||||
className="maputnik-array-add-value"
|
<InputButton
|
||||||
onClick={this.addValue}
|
className="maputnik-array-add-value"
|
||||||
>
|
onClick={this.addValue}
|
||||||
Add value
|
>
|
||||||
</Button>
|
Add value
|
||||||
</div>
|
</InputButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DeleteValueButton extends React.Component {
|
class DeleteValueInputButton extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <Button
|
return <InputButton
|
||||||
className="maputnik-delete-stop"
|
className="maputnik-delete-stop"
|
||||||
onClick={this.props.onClick}
|
onClick={this.props.onClick}
|
||||||
|
title="Remove array item"
|
||||||
>
|
>
|
||||||
<DocLabel
|
<FieldDocLabel
|
||||||
label={<MdDelete />}
|
label={<MdDelete />}
|
||||||
doc={"Remove array entry."}
|
doc={"Remove array item."}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</InputButton>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DynamicArrayInput
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import SelectInput from '../inputs/SelectInput'
|
import InputSelect from './InputSelect'
|
||||||
import MultiButtonInput from '../inputs/MultiButtonInput'
|
import InputMultiInput from './InputMultiInput'
|
||||||
|
|
||||||
|
|
||||||
function optionsLabelLength(options) {
|
function optionsLabelLength(options) {
|
||||||
@@ -13,33 +13,37 @@ function optionsLabelLength(options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class EnumInput extends React.Component {
|
export default class InputEnum extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
"data-wd-key": PropTypes.string,
|
"data-wd-key": PropTypes.string,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
default: PropTypes.string,
|
default: PropTypes.string,
|
||||||
|
name: PropTypes.string,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
options: PropTypes.array,
|
options: PropTypes.array,
|
||||||
|
'aria-label': PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {options, value, onChange} = this.props;
|
const {options, value, onChange, name, label} = this.props;
|
||||||
|
|
||||||
if(options.length <= 3 && optionsLabelLength(options) <= 20) {
|
if(options.length <= 3 && optionsLabelLength(options) <= 20) {
|
||||||
return <MultiButtonInput
|
return <InputMultiInput
|
||||||
|
name={name}
|
||||||
options={options}
|
options={options}
|
||||||
value={value || this.props.default}
|
value={value || this.props.default}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
aria-label={this.props['aria-label'] || label}
|
||||||
/>
|
/>
|
||||||
} else {
|
} else {
|
||||||
return <SelectInput
|
return <InputSelect
|
||||||
options={options}
|
options={options}
|
||||||
value={value || this.props.default}
|
value={value || this.props.default}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
aria-label={this.props['aria-label'] || label}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EnumInput
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import InputAutocomplete from './InputAutocomplete'
|
||||||
|
|
||||||
|
export default class FieldFont extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: PropTypes.array,
|
||||||
|
default: PropTypes.array,
|
||||||
|
fonts: PropTypes.array,
|
||||||
|
style: PropTypes.object,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
'aria-label': PropTypes.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
fonts: []
|
||||||
|
}
|
||||||
|
|
||||||
|
get values() {
|
||||||
|
const out = this.props.value || this.props.default || [];
|
||||||
|
|
||||||
|
// Always put a "" in the last field to you can keep adding entries
|
||||||
|
if (out[out.length-1] !== ""){
|
||||||
|
return out.concat("");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changeFont(idx, newValue) {
|
||||||
|
const changedValues = this.values.slice(0)
|
||||||
|
changedValues[idx] = newValue
|
||||||
|
const filteredValues = changedValues
|
||||||
|
.filter(v => v !== undefined)
|
||||||
|
.filter(v => v !== "")
|
||||||
|
|
||||||
|
this.props.onChange(filteredValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const inputs = this.values.map((value, i) => {
|
||||||
|
return <li
|
||||||
|
key={i}
|
||||||
|
>
|
||||||
|
<InputAutocomplete
|
||||||
|
aria-label={this.props['aria-label'] || this.props.name}
|
||||||
|
value={value}
|
||||||
|
options={this.props.fonts.map(f => [f, f])}
|
||||||
|
onChange={this.changeFont.bind(this, i)}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="maputnik-font">
|
||||||
|
{inputs}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@ import React from 'react'
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
import Block from './Block'
|
||||||
import StringInput from '../inputs/StringInput'
|
import FieldString from './FieldString'
|
||||||
import CodeMirror from 'codemirror';
|
import CodeMirror from 'codemirror';
|
||||||
|
|
||||||
import 'codemirror/mode/javascript/javascript'
|
import 'codemirror/mode/javascript/javascript'
|
||||||
@@ -16,7 +16,7 @@ import stringifyPretty from 'json-stringify-pretty-compact'
|
|||||||
import '../util/codemirror-mgl';
|
import '../util/codemirror-mgl';
|
||||||
|
|
||||||
|
|
||||||
class JSONEditor extends React.Component {
|
export default class InputJson extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
layer: PropTypes.any.isRequired,
|
layer: PropTypes.any.isRequired,
|
||||||
maxHeight: PropTypes.number,
|
maxHeight: PropTypes.number,
|
||||||
@@ -51,9 +51,11 @@ class JSONEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props);
|
||||||
|
this._keyEvent = "keyboard";
|
||||||
this.state = {
|
this.state = {
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
|
showMessage: false,
|
||||||
prevValue: this.props.getValue(this.props.layer),
|
prevValue: this.props.getValue(this.props.layer),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -82,17 +84,24 @@ class JSONEditor extends React.Component {
|
|||||||
this._doc.on('blur', this.onBlur);
|
this._doc.on('blur', this.onBlur);
|
||||||
}
|
}
|
||||||
|
|
||||||
onFocus = () => {
|
onPointerDown = (cm, e) => {
|
||||||
|
this._keyEvent = "pointer";
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocus = (cm, e) => {
|
||||||
this.props.onFocus();
|
this.props.onFocus();
|
||||||
this.setState({
|
this.setState({
|
||||||
isEditing: true
|
isEditing: true,
|
||||||
|
showMessage: (this._keyEvent === "keyboard"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onBlur = () => {
|
onBlur = () => {
|
||||||
|
this._keyEvent = "keyboard";
|
||||||
this.props.onBlur();
|
this.props.onBlur();
|
||||||
this.setState({
|
this.setState({
|
||||||
isEditing: false
|
isEditing: false,
|
||||||
|
showMessage: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,17 +154,21 @@ class JSONEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {showMessage} = this.state;
|
||||||
const style = {};
|
const style = {};
|
||||||
if (this.props.maxHeight) {
|
if (this.props.maxHeight) {
|
||||||
style.maxHeight = this.props.maxHeight;
|
style.maxHeight = this.props.maxHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div
|
return <div className="JSONEditor" onPointerDown={this.onPointerDown} aria-hidden="true">
|
||||||
className={classnames("codemirror-container", this.props.className)}
|
<div className={classnames("JSONEditor__message", {"JSONEditor__message--on": showMessage})}>
|
||||||
ref={(el) => this._el = el}
|
Press <kbd>ESC</kbd> to lose focus
|
||||||
style={style}
|
</div>
|
||||||
/>
|
<div
|
||||||
|
className={classnames("codemirror-container", this.props.className)}
|
||||||
|
ref={(el) => this._el = el}
|
||||||
|
style={style}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default JSONEditor
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import classnames from 'classnames'
|
||||||
|
import InputButton from './InputButton'
|
||||||
|
|
||||||
|
export default class InputMultiInput extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
options: PropTypes.array.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let options = this.props.options
|
||||||
|
if(options.length > 0 && !Array.isArray(options[0])) {
|
||||||
|
options = options.map(v => [v, v])
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedValue = this.props.value || options[0][0]
|
||||||
|
const radios = options.map(([val, label])=> {
|
||||||
|
return <label
|
||||||
|
key={val}
|
||||||
|
className={classnames("maputnik-radio-as-button", {"maputnik-button-selected": val === selectedValue})}
|
||||||
|
>
|
||||||
|
<input type="radio"
|
||||||
|
name={this.props.name}
|
||||||
|
onChange={e => this.props.onChange(val)}
|
||||||
|
value={val}
|
||||||
|
checked={val === selectedValue}
|
||||||
|
/>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
})
|
||||||
|
|
||||||
|
return <fieldset className="maputnik-multibutton" aria-label={this.props['aria-label']}>
|
||||||
|
{radios}
|
||||||
|
</fieldset>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
|
|||||||
|
|
||||||
let IDX = 0;
|
let IDX = 0;
|
||||||
|
|
||||||
class NumberInput extends React.Component {
|
export default class InputNumber extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.number,
|
value: PropTypes.number,
|
||||||
default: PropTypes.number,
|
default: PropTypes.number,
|
||||||
@@ -14,6 +14,7 @@ class NumberInput extends React.Component {
|
|||||||
rangeStep: PropTypes.number,
|
rangeStep: PropTypes.number,
|
||||||
wdKey: PropTypes.string,
|
wdKey: PropTypes.string,
|
||||||
required: PropTypes.bool,
|
required: PropTypes.bool,
|
||||||
|
"aria-label": PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@@ -31,7 +32,7 @@ class NumberInput extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props, state) {
|
static getDerivedStateFromProps(props, state) {
|
||||||
if (!state.editing) {
|
if (!state.editing && props.value !== state.value) {
|
||||||
return {
|
return {
|
||||||
value: props.value,
|
value: props.value,
|
||||||
dirtyValue: props.value,
|
dirtyValue: props.value,
|
||||||
@@ -49,12 +50,17 @@ class NumberInput extends React.Component {
|
|||||||
if(this.isValid(value) && hasChanged) {
|
if(this.isValid(value) && hasChanged) {
|
||||||
this.props.onChange(value)
|
this.props.onChange(value)
|
||||||
this.setState({
|
this.setState({
|
||||||
dirtyValue: newValue,
|
value: newValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (!this.isValid(value) && hasChanged) {
|
||||||
|
this.setState({
|
||||||
|
value: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
value: newValue,
|
dirtyValue: newValue === "" ? undefined : newValue,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,11 +93,13 @@ class NumberInput extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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 (!this.isValid(this.state.value)) {
|
||||||
if(this.isValid(this.props.value)) {
|
if(this.isValid(this.props.value)) {
|
||||||
this.changeValue(this.props.value)
|
this.changeValue(this.props.value)
|
||||||
|
this.setState({dirtyValue: this.props.value});
|
||||||
} else {
|
} else {
|
||||||
this.changeValue(undefined);
|
this.changeValue(undefined);
|
||||||
|
this.setState({dirtyValue: undefined});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,8 +152,15 @@ class NumberInput extends React.Component {
|
|||||||
this.props.min !== undefined && this.props.max !== undefined &&
|
this.props.min !== undefined && this.props.max !== undefined &&
|
||||||
this.props.allowRange
|
this.props.allowRange
|
||||||
) {
|
) {
|
||||||
const dirtyValue = this.state.dirtyValue === undefined ? this.props.default : this.state.dirtyValue
|
const value = this.state.editing ? this.state.dirtyValue : this.state.value;
|
||||||
const value = this.state.value === undefined ? "" : this.state.value;
|
const defaultValue = this.props.default === undefined ? "" : this.props.default;
|
||||||
|
let inputValue;
|
||||||
|
if (this.state.editingRange) {
|
||||||
|
inputValue = this.state.value;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
inputValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
return <div className="maputnik-number-container">
|
return <div className="maputnik-number-container">
|
||||||
<input
|
<input
|
||||||
@@ -156,21 +171,24 @@ class NumberInput extends React.Component {
|
|||||||
min={this.props.min}
|
min={this.props.min}
|
||||||
step="any"
|
step="any"
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
value={dirtyValue}
|
value={value === undefined ? defaultValue : value}
|
||||||
aria-hidden="true"
|
|
||||||
onChange={this.onChangeRange}
|
onChange={this.onChangeRange}
|
||||||
onKeyDown={() => {
|
onKeyDown={() => {
|
||||||
this._keyboardEvent = true;
|
this._keyboardEvent = true;
|
||||||
}}
|
}}
|
||||||
onPointerDown={() => {
|
onPointerDown={() => {
|
||||||
this.setState({editing: true});
|
this.setState({editing: true, editingRange: true});
|
||||||
}}
|
}}
|
||||||
onPointerUp={() => {
|
onPointerUp={() => {
|
||||||
// Safari doesn't get onBlur event
|
// Safari doesn't get onBlur event
|
||||||
this.setState({editing: false});
|
this.setState({editing: false, editingRange: false});
|
||||||
}}
|
}}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
this.setState({editing: false});
|
this.setState({
|
||||||
|
editing: false,
|
||||||
|
editingRange: false,
|
||||||
|
dirtyValue: this.state.value,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
@@ -179,25 +197,33 @@ class NumberInput extends React.Component {
|
|||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
className="maputnik-number"
|
className="maputnik-number"
|
||||||
placeholder={this.props.default}
|
placeholder={this.props.default}
|
||||||
value={value}
|
value={inputValue === undefined ? "" : inputValue}
|
||||||
onChange={e => {
|
onFocus={e => {
|
||||||
if (!this.state.editing) {
|
this.setState({editing: true});
|
||||||
this.changeValue(e.target.value);
|
}}
|
||||||
}
|
onChange={e => {
|
||||||
|
this.changeValue(e.target.value);
|
||||||
|
}}
|
||||||
|
onBlur={e => {
|
||||||
|
this.setState({editing: false});
|
||||||
|
this.resetValue()
|
||||||
}}
|
}}
|
||||||
onBlur={this.resetValue}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const value = this.state.value === undefined ? "" : this.state.value;
|
const value = this.state.editing ? this.state.dirtyValue : this.state.value;
|
||||||
|
|
||||||
return <input
|
return <input
|
||||||
|
aria-label={this.props['aria-label']}
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
className="maputnik-number"
|
className="maputnik-number"
|
||||||
placeholder={this.props.default}
|
placeholder={this.props.default}
|
||||||
value={value}
|
value={value === undefined ? "" : value}
|
||||||
onChange={e => this.changeValue(e.target.value)}
|
onChange={e => this.changeValue(e.target.value)}
|
||||||
|
onFocus={() => {
|
||||||
|
this.setState({editing: true});
|
||||||
|
}}
|
||||||
onBlur={this.resetValue}
|
onBlur={this.resetValue}
|
||||||
required={this.props.required}
|
required={this.props.required}
|
||||||
/>
|
/>
|
||||||
@@ -205,4 +231,4 @@ class NumberInput extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NumberInput
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
class SelectInput extends React.Component {
|
export default class InputSelect extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
"data-wd-key": PropTypes.string,
|
"data-wd-key": PropTypes.string,
|
||||||
@@ -9,6 +9,7 @@ class SelectInput extends React.Component {
|
|||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
|
'aria-label': PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -25,10 +26,11 @@ class SelectInput extends React.Component {
|
|||||||
title={this.props.title}
|
title={this.props.title}
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onChange={e => this.props.onChange(e.target.value)}
|
onChange={e => this.props.onChange(e.target.value)}
|
||||||
|
aria-label={this.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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SelectInput
|
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import ColorField from './ColorField'
|
import InputColor from './InputColor'
|
||||||
import NumberInput from '../inputs/NumberInput'
|
import InputNumber from './InputNumber'
|
||||||
import CheckboxInput from '../inputs/CheckboxInput'
|
import InputCheckbox from './InputCheckbox'
|
||||||
import StringInput from '../inputs/StringInput'
|
import InputString from './InputString'
|
||||||
import SelectInput from '../inputs/SelectInput'
|
import InputSelect from './InputSelect'
|
||||||
import MultiButtonInput from '../inputs/MultiButtonInput'
|
import InputMultiInput from './InputMultiInput'
|
||||||
import ArrayInput from '../inputs/ArrayInput'
|
import InputArray from './InputArray'
|
||||||
import DynamicArrayInput from '../inputs/DynamicArrayInput'
|
import InputDynamicArray from './InputDynamicArray'
|
||||||
import FontInput from '../inputs/FontInput'
|
import InputFont from './InputFont'
|
||||||
import IconInput from '../inputs/IconInput'
|
import InputAutocomplete from './InputAutocomplete'
|
||||||
import EnumInput from '../inputs/EnumInput'
|
import InputEnum from './InputEnum'
|
||||||
import capitalize from 'lodash.capitalize'
|
import capitalize from 'lodash.capitalize'
|
||||||
|
|
||||||
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
|
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
|
||||||
@@ -32,7 +32,7 @@ function optionsLabelLength(options) {
|
|||||||
return sum
|
return sum
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Display any field from the Mapbox 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 default class SpecField extends React.Component {
|
export default class SpecField extends React.Component {
|
||||||
@@ -48,21 +48,27 @@ export default class SpecField extends React.Component {
|
|||||||
]),
|
]),
|
||||||
/** Override the style of the field */
|
/** Override the style of the field */
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
|
'aria-label': PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const commonProps = {
|
const commonProps = {
|
||||||
|
error: this.props.error,
|
||||||
|
fieldSpec: this.props.fieldSpec,
|
||||||
|
label: this.props.label,
|
||||||
|
action: this.props.action,
|
||||||
style: this.props.style,
|
style: this.props.style,
|
||||||
value: this.props.value,
|
value: this.props.value,
|
||||||
default: this.props.fieldSpec.default,
|
default: this.props.fieldSpec.default,
|
||||||
name: this.props.fieldName,
|
name: this.props.fieldName,
|
||||||
onChange: newValue => this.props.onChange(this.props.fieldName, newValue)
|
onChange: newValue => this.props.onChange(this.props.fieldName, newValue),
|
||||||
|
'aria-label': this.props['aria-label'],
|
||||||
}
|
}
|
||||||
|
|
||||||
function childNodes() {
|
function childNodes() {
|
||||||
switch(this.props.fieldSpec.type) {
|
switch(this.props.fieldSpec.type) {
|
||||||
case 'number': return (
|
case 'number': return (
|
||||||
<NumberInput
|
<InputNumber
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
min={this.props.fieldSpec.minimum}
|
min={this.props.fieldSpec.minimum}
|
||||||
max={this.props.fieldSpec.maximum}
|
max={this.props.fieldSpec.maximum}
|
||||||
@@ -71,48 +77,49 @@ export default class SpecField extends React.Component {
|
|||||||
case 'enum':
|
case 'enum':
|
||||||
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)])
|
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)])
|
||||||
|
|
||||||
return <EnumInput
|
return <InputEnum
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
options={options}
|
options={options}
|
||||||
/>
|
/>
|
||||||
case 'resolvedImage':
|
case 'resolvedImage':
|
||||||
case 'formatted':
|
case 'formatted':
|
||||||
case 'string':
|
case 'string':
|
||||||
if(iconProperties.indexOf(this.props.fieldName) >= 0) {
|
if (iconProperties.indexOf(this.props.fieldName) >= 0) {
|
||||||
return <IconInput
|
const options = this.props.fieldSpec.values || [];
|
||||||
|
return <InputAutocomplete
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
icons={this.props.fieldSpec.values}
|
options={options.map(f => [f, f])}
|
||||||
/>
|
/>
|
||||||
} else {
|
} else {
|
||||||
return <StringInput
|
return <InputString
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
case 'color': return (
|
case 'color': return (
|
||||||
<ColorField
|
<InputColor
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case 'boolean': return (
|
case 'boolean': return (
|
||||||
<CheckboxInput
|
<InputCheckbox
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case 'array':
|
case 'array':
|
||||||
if(this.props.fieldName === 'text-font') {
|
if(this.props.fieldName === 'text-font') {
|
||||||
return <FontInput
|
return <InputFont
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
fonts={this.props.fieldSpec.values}
|
fonts={this.props.fieldSpec.values}
|
||||||
/>
|
/>
|
||||||
} else {
|
} else {
|
||||||
if (this.props.fieldSpec.length) {
|
if (this.props.fieldSpec.length) {
|
||||||
return <ArrayInput
|
return <InputArray
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
type={this.props.fieldSpec.value}
|
type={this.props.fieldSpec.value}
|
||||||
length={this.props.fieldSpec.length}
|
length={this.props.fieldSpec.length}
|
||||||
/>
|
/>
|
||||||
} else {
|
} else {
|
||||||
return <DynamicArrayInput
|
return <InputDynamicArray
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
fieldSpec={this.props.fieldSpec}
|
fieldSpec={this.props.fieldSpec}
|
||||||
type={this.props.fieldSpec.value}
|
type={this.props.fieldSpec.value}
|
||||||
@@ -125,7 +132,7 @@ export default class SpecField extends React.Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-wd-key={"spec-field:"+this.props.fieldName}>
|
<div data-wd-key={"spec-field:"+this.props.fieldName}>
|
||||||
{childNodes.call(this)}
|
{childNodes.call(this)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
class StringInput extends React.Component {
|
export default class InputString extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
"data-wd-key": PropTypes.string,
|
"data-wd-key": PropTypes.string,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
@@ -13,6 +13,7 @@ class StringInput extends React.Component {
|
|||||||
required: PropTypes.bool,
|
required: PropTypes.bool,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
spellCheck: PropTypes.bool,
|
spellCheck: PropTypes.bool,
|
||||||
|
'aria-label': PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@@ -33,6 +34,7 @@ class StringInput extends React.Component {
|
|||||||
value: props.value
|
value: props.value
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -58,6 +60,7 @@ class StringInput extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return React.createElement(tag, {
|
return React.createElement(tag, {
|
||||||
|
"aria-label": this.props["aria-label"],
|
||||||
"data-wd-key": this.props["data-wd-key"],
|
"data-wd-key": this.props["data-wd-key"],
|
||||||
spellCheck: this.props.hasOwnProperty("spellCheck") ? this.props.spellCheck : !(tag === "input"),
|
spellCheck: this.props.hasOwnProperty("spellCheck") ? this.props.spellCheck : !(tag === "input"),
|
||||||
disabled: this.props.disabled,
|
disabled: this.props.disabled,
|
||||||
@@ -79,9 +82,14 @@ class StringInput extends React.Component {
|
|||||||
this.props.onChange(this.state.value);
|
this.props.onChange(this.state.value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onKeyDown: (e) => {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
this.props.onChange(this.state.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
required: this.props.required,
|
required: this.props.required,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default StringInput
|
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import React from 'react'
|
import React, {Fragment} from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import StringInput from './StringInput'
|
import InputString from './InputString'
|
||||||
import SmallError from '../util/SmallError'
|
import SmallError from './SmallError'
|
||||||
|
|
||||||
|
|
||||||
function validate (url) {
|
function validate (url) {
|
||||||
|
if (url === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let error;
|
let error;
|
||||||
const getProtocol = (url) => {
|
const getProtocol = (url) => {
|
||||||
try {
|
try {
|
||||||
@@ -16,7 +20,20 @@ function validate (url) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const protocol = getProtocol(url);
|
const protocol = getProtocol(url);
|
||||||
if (
|
const isSsl = window.location.protocol === "https:";
|
||||||
|
|
||||||
|
if (!protocol) {
|
||||||
|
error = (
|
||||||
|
<SmallError>
|
||||||
|
Must provide protocol {
|
||||||
|
isSsl
|
||||||
|
? <code>https://</code>
|
||||||
|
: <><code>http://</code> or <code>https://</code></>
|
||||||
|
}
|
||||||
|
</SmallError>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (
|
||||||
protocol &&
|
protocol &&
|
||||||
protocol === "http:" &&
|
protocol === "http:" &&
|
||||||
window.location.protocol === "https:"
|
window.location.protocol === "https:"
|
||||||
@@ -31,7 +48,7 @@ function validate (url) {
|
|||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
class UrlInput extends React.Component {
|
export default class FieldUrl extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
"data-wd-key": PropTypes.string,
|
"data-wd-key": PropTypes.string,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
@@ -41,6 +58,7 @@ class UrlInput extends React.Component {
|
|||||||
onInput: PropTypes.func,
|
onInput: PropTypes.func,
|
||||||
multi: PropTypes.bool,
|
multi: PropTypes.bool,
|
||||||
required: PropTypes.bool,
|
required: PropTypes.bool,
|
||||||
|
'aria-label': PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@@ -61,12 +79,21 @@ class UrlInput extends React.Component {
|
|||||||
this.props.onInput(url);
|
this.props.onInput(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onChange = (url) => {
|
||||||
|
this.setState({
|
||||||
|
error: validate(url)
|
||||||
|
});
|
||||||
|
this.props.onChange(url);
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<StringInput
|
<InputString
|
||||||
{...this.props}
|
{...this.props}
|
||||||
onInput={this.onInput}
|
onInput={this.onInput}
|
||||||
|
onChange={this.onChange}
|
||||||
|
aria-label={this.props['aria-label']}
|
||||||
/>
|
/>
|
||||||
{this.state.error}
|
{this.state.error}
|
||||||
</div>
|
</div>
|
||||||
@@ -74,4 +101,3 @@ class UrlInput extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UrlInput
|
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
import PropTypes from "prop-types";
|
||||||
|
import React from "react";
|
||||||
|
import { Button, Menu, MenuItem, Wrapper } from "react-aria-menubutton";
|
||||||
|
|
||||||
|
import { Accordion } from "react-accessible-accordion";
|
||||||
|
import FieldComment from "./FieldComment";
|
||||||
|
import FieldId from "./FieldId";
|
||||||
|
import FieldJson from "./FieldJson";
|
||||||
|
import FieldMaxZoom from "./FieldMaxZoom";
|
||||||
|
import FieldMinZoom from "./FieldMinZoom";
|
||||||
|
import FieldSource from "./FieldSource";
|
||||||
|
import FieldSourceLayer from "./FieldSourceLayer";
|
||||||
|
import FieldType from "./FieldType";
|
||||||
|
import FilterEditor from "./FilterEditor";
|
||||||
|
import LayerEditorGroup from "./LayerEditorGroup";
|
||||||
|
import PropertyGroup from "./PropertyGroup";
|
||||||
|
|
||||||
|
import { MdMoreVert } from "react-icons/md";
|
||||||
|
|
||||||
|
import layout from "../config/layout.json";
|
||||||
|
import { changeProperty, changeType } from "../libs/layer";
|
||||||
|
import { formatLayerId } from "../util/format";
|
||||||
|
|
||||||
|
function getLayoutForType(type) {
|
||||||
|
return layout[type] ? layout[type] : layout.invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function layoutGroups(layerType) {
|
||||||
|
const layerGroup = {
|
||||||
|
title: "Layer",
|
||||||
|
type: "layer",
|
||||||
|
};
|
||||||
|
const filterGroup = {
|
||||||
|
title: "Filter",
|
||||||
|
type: "filter",
|
||||||
|
};
|
||||||
|
const editorGroup = {
|
||||||
|
title: "JSON Editor",
|
||||||
|
type: "jsoneditor",
|
||||||
|
};
|
||||||
|
return [layerGroup, filterGroup]
|
||||||
|
.concat(getLayoutForType(layerType).groups)
|
||||||
|
.concat([editorGroup]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Layer editor supporting multiple types of layers. */
|
||||||
|
export default class LayerEditor extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
layer: PropTypes.object.isRequired,
|
||||||
|
sources: PropTypes.object,
|
||||||
|
vectorLayers: PropTypes.object,
|
||||||
|
spec: PropTypes.object.isRequired,
|
||||||
|
onLayerChanged: PropTypes.func,
|
||||||
|
onLayerIdChange: PropTypes.func,
|
||||||
|
onMoveLayer: PropTypes.func,
|
||||||
|
onLayerDestroy: PropTypes.func,
|
||||||
|
onLayerCopy: PropTypes.func,
|
||||||
|
onLayerVisibilityToggle: PropTypes.func,
|
||||||
|
isFirstLayer: PropTypes.bool,
|
||||||
|
isLastLayer: PropTypes.bool,
|
||||||
|
layerIndex: PropTypes.number,
|
||||||
|
errors: PropTypes.array,
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
onLayerChanged: () => {},
|
||||||
|
onLayerIdChange: () => {},
|
||||||
|
onLayerDestroyed: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
static childContextTypes = {
|
||||||
|
reactIconBase: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
//TODO: Clean this up and refactor into function
|
||||||
|
const editorGroups = {};
|
||||||
|
layoutGroups(this.props.layer.type).forEach((group) => {
|
||||||
|
editorGroups[group.title] = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.state = { editorGroups };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromProps(props, state) {
|
||||||
|
const additionalGroups = { ...state.editorGroups };
|
||||||
|
|
||||||
|
getLayoutForType(props.layer.type).groups.forEach((group) => {
|
||||||
|
if (!(group.title in additionalGroups)) {
|
||||||
|
additionalGroups[group.title] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
editorGroups: additionalGroups,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildContext() {
|
||||||
|
return {
|
||||||
|
reactIconBase: {
|
||||||
|
size: 14,
|
||||||
|
color: "#8e8e8e",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
changeProperty(group, property, newValue) {
|
||||||
|
this.props.onLayerChanged(
|
||||||
|
this.props.layerIndex,
|
||||||
|
changeProperty(this.props.layer, group, property, newValue)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onGroupToggle(groupTitle, active) {
|
||||||
|
const changedActiveGroups = {
|
||||||
|
...this.state.editorGroups,
|
||||||
|
[groupTitle]: active,
|
||||||
|
};
|
||||||
|
this.setState({
|
||||||
|
editorGroups: changedActiveGroups,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderGroupType(type, fields) {
|
||||||
|
let comment = "";
|
||||||
|
if (this.props.layer.metadata) {
|
||||||
|
comment = this.props.layer.metadata["maputnik:comment"];
|
||||||
|
}
|
||||||
|
const { errors, layerIndex } = this.props;
|
||||||
|
|
||||||
|
const errorData = {};
|
||||||
|
errors.forEach((error) => {
|
||||||
|
if (
|
||||||
|
error.parsed &&
|
||||||
|
error.parsed.type === "layer" &&
|
||||||
|
error.parsed.data.index == layerIndex
|
||||||
|
) {
|
||||||
|
errorData[error.parsed.data.key] = {
|
||||||
|
message: error.parsed.data.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let sourceLayerIds;
|
||||||
|
if (this.props.sources.hasOwnProperty(this.props.layer.source)) {
|
||||||
|
sourceLayerIds = this.props.sources[this.props.layer.source].layers;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "layer":
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FieldId
|
||||||
|
value={this.props.layer.id}
|
||||||
|
wdKey="layer-editor.layer-id"
|
||||||
|
error={errorData.id}
|
||||||
|
onChange={(newId) =>
|
||||||
|
this.props.onLayerIdChange(
|
||||||
|
this.props.layerIndex,
|
||||||
|
this.props.layer.id,
|
||||||
|
newId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<FieldType
|
||||||
|
disabled={true}
|
||||||
|
error={errorData.type}
|
||||||
|
value={this.props.layer.type}
|
||||||
|
onChange={(newType) =>
|
||||||
|
this.props.onLayerChanged(
|
||||||
|
this.props.layerIndex,
|
||||||
|
changeType(this.props.layer, newType)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{this.props.layer.type !== "background" && (
|
||||||
|
<FieldSource
|
||||||
|
error={errorData.source}
|
||||||
|
sourceIds={Object.keys(this.props.sources)}
|
||||||
|
value={this.props.layer.source}
|
||||||
|
onChange={(v) => this.changeProperty(null, "source", v)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{["background", "raster", "hillshade", "heatmap"].indexOf(
|
||||||
|
this.props.layer.type
|
||||||
|
) < 0 && (
|
||||||
|
<FieldSourceLayer
|
||||||
|
error={errorData["source-layer"]}
|
||||||
|
sourceLayerIds={sourceLayerIds}
|
||||||
|
value={this.props.layer["source-layer"]}
|
||||||
|
onChange={(v) => this.changeProperty(null, "source-layer", v)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<FieldMinZoom
|
||||||
|
error={errorData.minzoom}
|
||||||
|
value={this.props.layer.minzoom}
|
||||||
|
onChange={(v) => this.changeProperty(null, "minzoom", v)}
|
||||||
|
/>
|
||||||
|
<FieldMaxZoom
|
||||||
|
error={errorData.maxzoom}
|
||||||
|
value={this.props.layer.maxzoom}
|
||||||
|
onChange={(v) => this.changeProperty(null, "maxzoom", v)}
|
||||||
|
/>
|
||||||
|
<FieldComment
|
||||||
|
error={errorData.comment}
|
||||||
|
value={comment}
|
||||||
|
onChange={(v) =>
|
||||||
|
this.changeProperty(
|
||||||
|
"metadata",
|
||||||
|
"maputnik:comment",
|
||||||
|
v == "" ? undefined : v
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "filter":
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="maputnik-filter-editor-wrapper">
|
||||||
|
<FilterEditor
|
||||||
|
errors={errorData}
|
||||||
|
filter={this.props.layer.filter}
|
||||||
|
properties={
|
||||||
|
this.props.vectorLayers[this.props.layer["source-layer"]]
|
||||||
|
}
|
||||||
|
onChange={(f) => this.changeProperty(null, "filter", f)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case "properties":
|
||||||
|
return (
|
||||||
|
<PropertyGroup
|
||||||
|
errors={errorData}
|
||||||
|
layer={this.props.layer}
|
||||||
|
groupFields={fields}
|
||||||
|
spec={this.props.spec}
|
||||||
|
onChange={this.changeProperty.bind(this)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "jsoneditor":
|
||||||
|
return (
|
||||||
|
<FieldJson
|
||||||
|
layer={this.props.layer}
|
||||||
|
onChange={(layer) => {
|
||||||
|
this.props.onLayerChanged(this.props.layerIndex, layer);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
moveLayer(offset) {
|
||||||
|
this.props.onMoveLayer({
|
||||||
|
oldIndex: this.props.layerIndex,
|
||||||
|
newIndex: this.props.layerIndex + offset,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const groupIds = [];
|
||||||
|
const layerType = this.props.layer.type;
|
||||||
|
const groups = layoutGroups(layerType)
|
||||||
|
.filter((group) => {
|
||||||
|
return !(layerType === "background" && group.type === "source");
|
||||||
|
})
|
||||||
|
.map((group) => {
|
||||||
|
const groupId = group.title.replace(/ /g, "_");
|
||||||
|
groupIds.push(groupId);
|
||||||
|
return (
|
||||||
|
<LayerEditorGroup
|
||||||
|
data-wd-key={group.title}
|
||||||
|
id={groupId}
|
||||||
|
key={group.title}
|
||||||
|
title={group.title}
|
||||||
|
isActive={this.state.editorGroups[group.title]}
|
||||||
|
onActiveToggle={this.onGroupToggle.bind(this, group.title)}
|
||||||
|
>
|
||||||
|
{this.renderGroupType(group.type, group.fields)}
|
||||||
|
</LayerEditorGroup>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const layout = this.props.layer.layout || {};
|
||||||
|
|
||||||
|
const items = {
|
||||||
|
delete: {
|
||||||
|
text: "Delete",
|
||||||
|
handler: () => this.props.onLayerDestroy(this.props.layerIndex),
|
||||||
|
},
|
||||||
|
duplicate: {
|
||||||
|
text: "Duplicate",
|
||||||
|
handler: () => this.props.onLayerCopy(this.props.layerIndex),
|
||||||
|
},
|
||||||
|
hide: {
|
||||||
|
text: layout.visibility === "none" ? "Show" : "Hide",
|
||||||
|
handler: () =>
|
||||||
|
this.props.onLayerVisibilityToggle(this.props.layerIndex),
|
||||||
|
},
|
||||||
|
moveLayerUp: {
|
||||||
|
text: "Move layer up",
|
||||||
|
// Not actually used...
|
||||||
|
disabled: this.props.isFirstLayer,
|
||||||
|
handler: () => this.moveLayer(-1),
|
||||||
|
},
|
||||||
|
moveLayerDown: {
|
||||||
|
text: "Move layer down",
|
||||||
|
// Not actually used...
|
||||||
|
disabled: this.props.isLastLayer,
|
||||||
|
handler: () => this.moveLayer(+1),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleSelection(id, event) {
|
||||||
|
event.stopPropagation;
|
||||||
|
items[id].handler();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="maputnik-layer-editor"
|
||||||
|
role="main"
|
||||||
|
aria-label="Layer editor"
|
||||||
|
>
|
||||||
|
<header>
|
||||||
|
<div className="layer-header">
|
||||||
|
<h2 className="layer-header__title">
|
||||||
|
Layer: {formatLayerId(this.props.layer.id)}
|
||||||
|
</h2>
|
||||||
|
<div className="layer-header__info">
|
||||||
|
<Wrapper
|
||||||
|
className="more-menu"
|
||||||
|
onSelection={handleSelection}
|
||||||
|
closeOnSelection={false}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
data-wd-key="skip-target-layer-editor"
|
||||||
|
id="skip-target-layer-editor"
|
||||||
|
className="more-menu__button"
|
||||||
|
title="Layer options"
|
||||||
|
>
|
||||||
|
<MdMoreVert className="more-menu__button__svg" />
|
||||||
|
</Button>
|
||||||
|
<Menu>
|
||||||
|
<ul className="more-menu__menu">
|
||||||
|
{Object.keys(items).map((id, idx) => {
|
||||||
|
const item = items[id];
|
||||||
|
return (
|
||||||
|
<li key={id}>
|
||||||
|
<MenuItem
|
||||||
|
value={id}
|
||||||
|
className="more-menu__menu__item"
|
||||||
|
>
|
||||||
|
{item.text}
|
||||||
|
</MenuItem>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</Menu>
|
||||||
|
</Wrapper>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<Accordion
|
||||||
|
allowMultipleExpanded={true}
|
||||||
|
allowZeroExpanded={true}
|
||||||
|
preExpanded={groupIds}
|
||||||
|
>
|
||||||
|
{groups}
|
||||||
|
</Accordion>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,353 @@
|
|||||||
|
import classnames from "classnames";
|
||||||
|
import lodash from "lodash";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import LayerListGroup from "./LayerListGroup";
|
||||||
|
import LayerListItem from "./LayerListItem";
|
||||||
|
import ModalAdd from "./ModalAdd";
|
||||||
|
|
||||||
|
import { SortableContainer } from "react-sortable-hoc";
|
||||||
|
|
||||||
|
const layerListPropTypes = {
|
||||||
|
layers: PropTypes.array.isRequired,
|
||||||
|
selectedLayerIndex: PropTypes.number.isRequired,
|
||||||
|
onLayersChange: PropTypes.func.isRequired,
|
||||||
|
onLayerSelect: PropTypes.func,
|
||||||
|
sources: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function layerPrefix(name) {
|
||||||
|
return name.replace(" ", "-").replace("_", "-").split("-")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function findClosestCommonPrefix(layers, idx) {
|
||||||
|
const currentLayerPrefix = layerPrefix(layers[idx].id);
|
||||||
|
let closestIdx = idx;
|
||||||
|
for (let i = idx; i > 0; i--) {
|
||||||
|
const previousLayerPrefix = layerPrefix(layers[i - 1].id);
|
||||||
|
if (previousLayerPrefix === currentLayerPrefix) {
|
||||||
|
closestIdx = i - 1;
|
||||||
|
} else {
|
||||||
|
return closestIdx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return closestIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
let UID = 0;
|
||||||
|
|
||||||
|
// List of collapsible layer editors
|
||||||
|
class LayerListContainer extends React.Component {
|
||||||
|
static propTypes = { ...layerListPropTypes };
|
||||||
|
static defaultProps = {
|
||||||
|
onLayerSelect: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.selectedItemRef = React.createRef();
|
||||||
|
this.scrollContainerRef = React.createRef();
|
||||||
|
this.state = {
|
||||||
|
collapsedGroups: {},
|
||||||
|
areAllGroupsExpanded: false,
|
||||||
|
keys: {
|
||||||
|
add: UID++,
|
||||||
|
},
|
||||||
|
isOpen: {
|
||||||
|
add: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleModal(modalName) {
|
||||||
|
this.setState({
|
||||||
|
keys: {
|
||||||
|
...this.state.keys,
|
||||||
|
[modalName]: UID++,
|
||||||
|
},
|
||||||
|
isOpen: {
|
||||||
|
...this.state.isOpen,
|
||||||
|
[modalName]: !this.state.isOpen[modalName],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleLayers = () => {
|
||||||
|
let idx = 0;
|
||||||
|
|
||||||
|
let newGroups = [];
|
||||||
|
|
||||||
|
this.groupedLayers().forEach((layers) => {
|
||||||
|
const groupPrefix = layerPrefix(layers[0].id);
|
||||||
|
const lookupKey = [groupPrefix, idx].join("-");
|
||||||
|
|
||||||
|
if (layers.length > 1) {
|
||||||
|
newGroups[lookupKey] = this.state.areAllGroupsExpanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
layers.forEach((layer) => {
|
||||||
|
idx += 1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
collapsedGroups: newGroups,
|
||||||
|
areAllGroupsExpanded: !this.state.areAllGroupsExpanded,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
groupedLayers() {
|
||||||
|
const groups = [];
|
||||||
|
const layerIdCount = new Map();
|
||||||
|
|
||||||
|
for (let i = 0; i < this.props.layers.length; i++) {
|
||||||
|
const origLayer = this.props.layers[i];
|
||||||
|
const previousLayer = this.props.layers[i - 1];
|
||||||
|
layerIdCount.set(
|
||||||
|
origLayer.id,
|
||||||
|
layerIdCount.has(origLayer.id) ? layerIdCount.get(origLayer.id) + 1 : 0
|
||||||
|
);
|
||||||
|
const layer = {
|
||||||
|
...origLayer,
|
||||||
|
key: `layers-list-${origLayer.id}-${layerIdCount.get(origLayer.id)}`,
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
previousLayer &&
|
||||||
|
layerPrefix(previousLayer.id) == layerPrefix(layer.id)
|
||||||
|
) {
|
||||||
|
const lastGroup = groups[groups.length - 1];
|
||||||
|
lastGroup.push(layer);
|
||||||
|
} else {
|
||||||
|
groups.push([layer]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleLayerGroup(groupPrefix, idx) {
|
||||||
|
const lookupKey = [groupPrefix, idx].join("-");
|
||||||
|
const newGroups = { ...this.state.collapsedGroups };
|
||||||
|
if (lookupKey in this.state.collapsedGroups) {
|
||||||
|
newGroups[lookupKey] = !this.state.collapsedGroups[lookupKey];
|
||||||
|
} else {
|
||||||
|
newGroups[lookupKey] = false;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
collapsedGroups: newGroups,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isCollapsed(groupPrefix, idx) {
|
||||||
|
const collapsed = this.state.collapsedGroups[[groupPrefix, idx].join("-")];
|
||||||
|
return collapsed === undefined ? true : collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
|
// Always update on state change
|
||||||
|
if (this.state !== nextState) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This component tree only requires id and visibility from the layers
|
||||||
|
// objects
|
||||||
|
function getRequiredProps(layer) {
|
||||||
|
const out = {
|
||||||
|
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) {
|
||||||
|
const out = {
|
||||||
|
...props,
|
||||||
|
};
|
||||||
|
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) {
|
||||||
|
if (prevProps.selectedLayerIndex !== this.props.selectedLayerIndex) {
|
||||||
|
const selectedItemNode = this.selectedItemRef.current;
|
||||||
|
if (selectedItemNode && selectedItemNode.node) {
|
||||||
|
const target = selectedItemNode.node;
|
||||||
|
const options = {
|
||||||
|
root: this.scrollContainerRef.current,
|
||||||
|
threshold: 1.0,
|
||||||
|
};
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
observer.unobserve(target);
|
||||||
|
if (entries.length > 0 && entries[0].intersectionRatio < 1) {
|
||||||
|
target.scrollIntoView();
|
||||||
|
}
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
observer.observe(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const listItems = [];
|
||||||
|
let idx = 0;
|
||||||
|
const layersByGroup = this.groupedLayers();
|
||||||
|
layersByGroup.forEach((layers) => {
|
||||||
|
const groupPrefix = layerPrefix(layers[0].id);
|
||||||
|
if (layers.length > 1) {
|
||||||
|
const grp = (
|
||||||
|
<LayerListGroup
|
||||||
|
data-wd-key={[groupPrefix, idx].join("-")}
|
||||||
|
aria-controls={layers.map((l) => l.key).join(" ")}
|
||||||
|
key={`group-${groupPrefix}-${idx}`}
|
||||||
|
title={groupPrefix}
|
||||||
|
isActive={
|
||||||
|
!this.isCollapsed(groupPrefix, idx) ||
|
||||||
|
idx === this.props.selectedLayerIndex
|
||||||
|
}
|
||||||
|
onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
listItems.push(grp);
|
||||||
|
}
|
||||||
|
|
||||||
|
layers.forEach((layer, idxInGroup) => {
|
||||||
|
const groupIdx = findClosestCommonPrefix(this.props.layers, idx);
|
||||||
|
|
||||||
|
const layerError = this.props.errors.find((error) => {
|
||||||
|
return (
|
||||||
|
error.parsed &&
|
||||||
|
error.parsed.type === "layer" &&
|
||||||
|
error.parsed.data.index == idx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const additionalProps = {};
|
||||||
|
if (idx === this.props.selectedLayerIndex) {
|
||||||
|
additionalProps.ref = this.selectedItemRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listItem = (
|
||||||
|
<LayerListItem
|
||||||
|
className={classnames({
|
||||||
|
"maputnik-layer-list-item-collapsed":
|
||||||
|
layers.length > 1 &&
|
||||||
|
this.isCollapsed(groupPrefix, groupIdx) &&
|
||||||
|
idx !== this.props.selectedLayerIndex,
|
||||||
|
"maputnik-layer-list-item-group-last":
|
||||||
|
idxInGroup == layers.length - 1 && layers.length > 1,
|
||||||
|
"maputnik-layer-list-item--error": !!layerError,
|
||||||
|
})}
|
||||||
|
index={idx}
|
||||||
|
key={layer.key}
|
||||||
|
id={layer.key}
|
||||||
|
layerId={layer.id}
|
||||||
|
layerIndex={idx}
|
||||||
|
layerType={layer.type}
|
||||||
|
visibility={(layer.layout || {}).visibility}
|
||||||
|
isSelected={idx === this.props.selectedLayerIndex}
|
||||||
|
onLayerSelect={this.props.onLayerSelect}
|
||||||
|
onLayerDestroy={this.props.onLayerDestroy.bind(this)}
|
||||||
|
onLayerCopy={this.props.onLayerCopy.bind(this)}
|
||||||
|
onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(
|
||||||
|
this
|
||||||
|
)}
|
||||||
|
{...additionalProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
listItems.push(listItem);
|
||||||
|
idx += 1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="maputnik-layer-list"
|
||||||
|
role="complementary"
|
||||||
|
aria-label="Layers list"
|
||||||
|
ref={this.scrollContainerRef}
|
||||||
|
>
|
||||||
|
<ModalAdd
|
||||||
|
key={this.state.keys.add}
|
||||||
|
layers={this.props.layers}
|
||||||
|
sources={this.props.sources}
|
||||||
|
isOpen={this.state.isOpen.add}
|
||||||
|
onOpenToggle={this.toggleModal.bind(this, "add")}
|
||||||
|
onLayersChange={this.props.onLayersChange}
|
||||||
|
/>
|
||||||
|
<header className="maputnik-layer-list-header">
|
||||||
|
<span className="maputnik-layer-list-header-title">Layers</span>
|
||||||
|
<span className="maputnik-space" />
|
||||||
|
<div className="maputnik-default-property">
|
||||||
|
<div className="maputnik-multibutton">
|
||||||
|
<button
|
||||||
|
id="skip-target-layer-list"
|
||||||
|
data-wd-key="skip-target-layer-list"
|
||||||
|
onClick={this.toggleLayers}
|
||||||
|
className="maputnik-button"
|
||||||
|
>
|
||||||
|
{this.state.areAllGroupsExpanded === true
|
||||||
|
? "Collapse"
|
||||||
|
: "Expand"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="maputnik-default-property">
|
||||||
|
<div className="maputnik-multibutton">
|
||||||
|
<button
|
||||||
|
onClick={this.toggleModal.bind(this, "add")}
|
||||||
|
data-wd-key="layer-list:add-layer"
|
||||||
|
className="maputnik-button maputnik-button-selected"
|
||||||
|
>
|
||||||
|
Add Layer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div role="navigation" aria-label="Layers list">
|
||||||
|
<ul className="maputnik-layer-list-container">{listItems}</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const LayerListContainerSortable = SortableContainer((props) => (
|
||||||
|
<LayerListContainer {...props} />
|
||||||
|
));
|
||||||
|
|
||||||
|
export default class LayerList extends React.Component {
|
||||||
|
static propTypes = { ...layerListPropTypes };
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<LayerListContainerSortable
|
||||||
|
{...this.props}
|
||||||
|
helperClass="sortableHelper"
|
||||||
|
onSortEnd={this.props.onMoveLayer.bind(this)}
|
||||||
|
useDragHandle={true}
|
||||||
|
shouldCancelStart={() => false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,8 @@ export default class LayerListGroup extends React.Component {
|
|||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
"data-wd-key": PropTypes.string,
|
"data-wd-key": PropTypes.string,
|
||||||
isActive: PropTypes.bool.isRequired,
|
isActive: PropTypes.bool.isRequired,
|
||||||
onActiveToggle: PropTypes.func.isRequired
|
onActiveToggle: PropTypes.func.isRequired,
|
||||||
|
'aria-controls': PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@@ -16,7 +17,13 @@ export default class LayerListGroup extends React.Component {
|
|||||||
data-wd-key={"layer-list-group:"+this.props["data-wd-key"]}
|
data-wd-key={"layer-list-group:"+this.props["data-wd-key"]}
|
||||||
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
|
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
|
||||||
>
|
>
|
||||||
<span className="maputnik-layer-list-group-title">{this.props.title}</span>
|
<button
|
||||||
|
className="maputnik-layer-list-group-title"
|
||||||
|
aria-controls={this.props['aria-controls']}
|
||||||
|
aria-expanded={this.props.isActive}
|
||||||
|
>
|
||||||
|
{this.props.title}
|
||||||
|
</button>
|
||||||
<span className="maputnik-space" />
|
<span className="maputnik-space" />
|
||||||
<Collapser
|
<Collapser
|
||||||
style={{ height: 14, width: 14 }}
|
style={{ height: 14, width: 14 }}
|
||||||
@@ -4,17 +4,19 @@ import classnames from 'classnames'
|
|||||||
|
|
||||||
import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from 'react-icons/md'
|
import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from 'react-icons/md'
|
||||||
|
|
||||||
import LayerIcon from '../icons/LayerIcon'
|
import IconLayer from './IconLayer'
|
||||||
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
|
import {SortableElement, SortableHandle} from 'react-sortable-hoc'
|
||||||
|
|
||||||
|
|
||||||
const DraggableLabel = SortableHandle((props) => {
|
const DraggableLabel = SortableHandle((props) => {
|
||||||
return <div className="maputnik-layer-list-item-handle">
|
return <div className="maputnik-layer-list-item-handle">
|
||||||
<LayerIcon
|
<IconLayer
|
||||||
className="layer-handle__icon"
|
className="layer-handle__icon"
|
||||||
type={props.layerType}
|
type={props.layerType}
|
||||||
/>
|
/>
|
||||||
<span className="maputnik-layer-list-item-id">{props.layerId}</span>
|
<button className="maputnik-layer-list-item-id">
|
||||||
|
{props.layerId}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,6 +56,7 @@ class IconAction extends React.Component {
|
|||||||
className={`maputnik-layer-list-icon-action ${classAdditions}`}
|
className={`maputnik-layer-list-icon-action ${classAdditions}`}
|
||||||
data-wd-key={this.props.wdKey}
|
data-wd-key={this.props.wdKey}
|
||||||
onClick={this.props.onClick}
|
onClick={this.props.onClick}
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
{this.renderIcon()}
|
{this.renderIcon()}
|
||||||
</button>
|
</button>
|
||||||
@@ -62,6 +65,7 @@ class IconAction extends React.Component {
|
|||||||
|
|
||||||
class LayerListItem extends React.Component {
|
class LayerListItem extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
layerIndex: PropTypes.number.isRequired,
|
||||||
layerId: PropTypes.string.isRequired,
|
layerId: PropTypes.string.isRequired,
|
||||||
layerType: PropTypes.string.isRequired,
|
layerType: PropTypes.string.isRequired,
|
||||||
isSelected: PropTypes.bool,
|
isSelected: PropTypes.bool,
|
||||||
@@ -96,8 +100,9 @@ class LayerListItem extends React.Component {
|
|||||||
const visibilityAction = this.props.visibility === 'visible' ? 'show' : 'hide';
|
const visibilityAction = this.props.visibility === 'visible' ? 'show' : 'hide';
|
||||||
|
|
||||||
return <li
|
return <li
|
||||||
|
id={this.props.id}
|
||||||
key={this.props.layerId}
|
key={this.props.layerId}
|
||||||
onClick={e => this.props.onLayerSelect(this.props.layerId)}
|
onClick={e => this.props.onLayerSelect(this.props.layerIndex)}
|
||||||
data-wd-key={"layer-list-item:"+this.props.layerId}
|
data-wd-key={"layer-list-item:"+this.props.layerId}
|
||||||
className={classnames({
|
className={classnames({
|
||||||
"maputnik-layer-list-item": true,
|
"maputnik-layer-list-item": true,
|
||||||
@@ -110,20 +115,20 @@ class LayerListItem extends React.Component {
|
|||||||
wdKey={"layer-list-item:"+this.props.layerId+":delete"}
|
wdKey={"layer-list-item:"+this.props.layerId+":delete"}
|
||||||
action={'delete'}
|
action={'delete'}
|
||||||
classBlockName="delete"
|
classBlockName="delete"
|
||||||
onClick={e => this.props.onLayerDestroy(this.props.layerId)}
|
onClick={e => this.props.onLayerDestroy(this.props.layerIndex)}
|
||||||
/>
|
/>
|
||||||
<IconAction
|
<IconAction
|
||||||
wdKey={"layer-list-item:"+this.props.layerId+":copy"}
|
wdKey={"layer-list-item:"+this.props.layerId+":copy"}
|
||||||
action={'duplicate'}
|
action={'duplicate'}
|
||||||
classBlockName="duplicate"
|
classBlockName="duplicate"
|
||||||
onClick={e => this.props.onLayerCopy(this.props.layerId)}
|
onClick={e => this.props.onLayerCopy(this.props.layerIndex)}
|
||||||
/>
|
/>
|
||||||
<IconAction
|
<IconAction
|
||||||
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
|
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
|
||||||
action={visibilityAction}
|
action={visibilityAction}
|
||||||
classBlockName="visibility"
|
classBlockName="visibility"
|
||||||
classBlockModifier={visibilityAction}
|
classBlockModifier={visibilityAction}
|
||||||
onClick={e => this.props.onLayerVisibilityToggle(this.props.layerId)}
|
onClick={e => this.props.onLayerVisibilityToggle(this.props.layerIndex)}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
import Color from "color";
|
||||||
|
import MapboxInspect from "mapbox-gl-inspect";
|
||||||
|
import colors from "mapbox-gl-inspect/lib/colors";
|
||||||
|
import MapLibreGl from "maplibre-gl";
|
||||||
|
import "maplibre-gl/dist/maplibre-gl.css";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import { colorHighlightedLayer } from "../libs/highlight";
|
||||||
|
import "../libs/maplibre-rtl";
|
||||||
|
import ZoomControl from "../libs/zoomcontrol";
|
||||||
|
import "../maplibregl.css";
|
||||||
|
import MapMaplibreGlFeaturePropertyPopup from "./MapMaplibreGlFeaturePropertyPopup";
|
||||||
|
import MapMaplibreGlLayerPopup from "./MapMaplibreGlLayerPopup";
|
||||||
|
|
||||||
|
const IS_SUPPORTED = MapLibreGl.supported();
|
||||||
|
|
||||||
|
function renderPopup(popup, mountNode) {
|
||||||
|
ReactDOM.render(popup, mountNode);
|
||||||
|
return mountNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
|
||||||
|
const backgroundLayer = {
|
||||||
|
id: "background",
|
||||||
|
type: "background",
|
||||||
|
paint: {
|
||||||
|
"background-color": "#1c1f24",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const layer = colorHighlightedLayer(highlightedLayer);
|
||||||
|
if (layer) {
|
||||||
|
coloredLayers.push(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sources = {};
|
||||||
|
Object.keys(originalMapStyle.sources).forEach((sourceId) => {
|
||||||
|
const source = originalMapStyle.sources[sourceId];
|
||||||
|
if (source.type !== "raster" && source.type !== "raster-dem") {
|
||||||
|
sources[sourceId] = source;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const inspectStyle = {
|
||||||
|
...originalMapStyle,
|
||||||
|
sources: sources,
|
||||||
|
layers: [backgroundLayer].concat(coloredLayers),
|
||||||
|
};
|
||||||
|
return inspectStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class MapMaplibreGl extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
onDataChange: PropTypes.func,
|
||||||
|
onLayerSelect: PropTypes.func.isRequired,
|
||||||
|
mapStyle: PropTypes.object.isRequired,
|
||||||
|
inspectModeEnabled: PropTypes.bool.isRequired,
|
||||||
|
highlightedLayer: PropTypes.object,
|
||||||
|
options: PropTypes.object,
|
||||||
|
replaceAccessTokens: PropTypes.func.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
onMapLoaded: () => {},
|
||||||
|
onDataChange: () => {},
|
||||||
|
onLayerSelect: () => {},
|
||||||
|
onChange: () => {},
|
||||||
|
options: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
map: null,
|
||||||
|
inspect: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMapFromProps(props) {
|
||||||
|
if (!IS_SUPPORTED) return;
|
||||||
|
|
||||||
|
if (!this.state.map) return;
|
||||||
|
|
||||||
|
//Maplibre GL now does diffing natively so we don't need to calculate
|
||||||
|
//the necessary operations ourselves!
|
||||||
|
this.state.map.setStyle(this.props.replaceAccessTokens(props.mapStyle), {
|
||||||
|
diff: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
|
let should = false;
|
||||||
|
try {
|
||||||
|
should =
|
||||||
|
JSON.stringify(this.props) !== JSON.stringify(nextProps) ||
|
||||||
|
JSON.stringify(this.state) !== JSON.stringify(nextState);
|
||||||
|
} catch (e) {
|
||||||
|
// no biggie, carry on
|
||||||
|
}
|
||||||
|
return should;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||||
|
if (!IS_SUPPORTED) return;
|
||||||
|
|
||||||
|
const map = this.state.map;
|
||||||
|
|
||||||
|
this.updateMapFromProps(this.props);
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.state.inspect &&
|
||||||
|
this.props.inspectModeEnabled !== this.state.inspect._showInspectMap
|
||||||
|
) {
|
||||||
|
// HACK: Fix for <https://github.com/maputnik/editor/issues/576>, while we wait for a proper fix.
|
||||||
|
// eslint-disable-next-line
|
||||||
|
this.state.inspect._popupBlocked = false;
|
||||||
|
this.state.inspect.toggleInspector();
|
||||||
|
}
|
||||||
|
if (map) {
|
||||||
|
if (this.props.inspectModeEnabled) {
|
||||||
|
// HACK: We need to work out why we need to do this and what's causing
|
||||||
|
// this error. I'm assuming an issue with maplibre-gl update and
|
||||||
|
// mapbox-gl-inspect.
|
||||||
|
try {
|
||||||
|
this.state.inspect.render();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("FIXME: Caught error", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
map.showTileBoundaries = this.props.options.showTileBoundaries;
|
||||||
|
map.showCollisionBoxes = this.props.options.showCollisionBoxes;
|
||||||
|
map.showOverdrawInspector = this.props.options.showOverdrawInspector;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (!IS_SUPPORTED) return;
|
||||||
|
|
||||||
|
const mapOpts = {
|
||||||
|
...this.props.options,
|
||||||
|
container: this.container,
|
||||||
|
style: this.props.mapStyle,
|
||||||
|
hash: true,
|
||||||
|
maxZoom: 24,
|
||||||
|
};
|
||||||
|
|
||||||
|
const map = new MapLibreGl.Map(mapOpts);
|
||||||
|
|
||||||
|
const mapViewChange = () => {
|
||||||
|
const center = map.getCenter();
|
||||||
|
const zoom = map.getZoom();
|
||||||
|
this.props.onChange({ center, zoom });
|
||||||
|
};
|
||||||
|
mapViewChange();
|
||||||
|
|
||||||
|
map.showTileBoundaries = mapOpts.showTileBoundaries;
|
||||||
|
map.showCollisionBoxes = mapOpts.showCollisionBoxes;
|
||||||
|
map.showOverdrawInspector = mapOpts.showOverdrawInspector;
|
||||||
|
|
||||||
|
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 inspect = new MapboxInspect({
|
||||||
|
popup: new MapLibreGl.Popup({
|
||||||
|
closeOnClick: false,
|
||||||
|
}),
|
||||||
|
showMapPopup: true,
|
||||||
|
showMapPopupOnHover: false,
|
||||||
|
showInspectMapPopupOnHover: true,
|
||||||
|
showInspectButton: false,
|
||||||
|
blockHoverPopupOnClick: true,
|
||||||
|
assignLayerColor: (layerId, alpha) => {
|
||||||
|
return Color(colors.brightColor(layerId, alpha))
|
||||||
|
.desaturate(0.5)
|
||||||
|
.string();
|
||||||
|
},
|
||||||
|
buildInspectStyle: (originalMapStyle, coloredLayers) =>
|
||||||
|
buildInspectStyle(
|
||||||
|
originalMapStyle,
|
||||||
|
coloredLayers,
|
||||||
|
this.props.highlightedLayer
|
||||||
|
),
|
||||||
|
renderPopup: (features) => {
|
||||||
|
if (this.props.inspectModeEnabled) {
|
||||||
|
return renderPopup(
|
||||||
|
<MapMaplibreGlFeaturePropertyPopup features={features} />,
|
||||||
|
tmpNode
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return renderPopup(
|
||||||
|
<MapMaplibreGlLayerPopup
|
||||||
|
features={features}
|
||||||
|
onLayerSelect={this.onLayerSelectById}
|
||||||
|
zoom={this.state.zoom}
|
||||||
|
/>,
|
||||||
|
tmpNode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
map.addControl(inspect);
|
||||||
|
|
||||||
|
map.on("style.load", () => {
|
||||||
|
this.setState({
|
||||||
|
map,
|
||||||
|
inspect,
|
||||||
|
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) => {
|
||||||
|
const index = this.props.mapStyle.layers.findIndex(
|
||||||
|
(layer) => layer.id === id
|
||||||
|
);
|
||||||
|
this.props.onLayerSelect(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (IS_SUPPORTED) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="maputnik-map__map"
|
||||||
|
role="region"
|
||||||
|
aria-label="Map view"
|
||||||
|
ref={(x) => (this.container = x)}
|
||||||
|
data-wd-key="maplibre:map"
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className="maputnik-map maputnik-map--error">
|
||||||
|
<div className="maputnik-map__error-message">
|
||||||
|
Error: Cannot load MaplibreGL, WebGL is either unsupported or
|
||||||
|
disabled
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user