mirror of
https://github.com/maputnik/editor.git
synced 2025-12-06 06:10:00 +00:00
Compare commits
155 Commits
v3.0.0
...
c168e65d86
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c168e65d86 | ||
|
|
8095b0d641 | ||
|
|
19d520426f | ||
|
|
74a21e1b77 | ||
|
|
582dd8d6e7 | ||
|
|
9d19ab8606 | ||
|
|
9bdcf0a16a | ||
|
|
8ceabc0310 | ||
|
|
42de84d105 | ||
|
|
b1d0300360 | ||
|
|
1abb126ba5 | ||
|
|
e8dc88cb28 | ||
|
|
5b6aff544e | ||
|
|
d606ad24a6 | ||
|
|
69e3d0aa97 | ||
|
|
b9064e0f3c | ||
|
|
dc7a2c135f | ||
|
|
f816d88946 | ||
|
|
72b71a0c23 | ||
|
|
a621104ac8 | ||
|
|
9a66a23fa3 | ||
|
|
138a25c01c | ||
|
|
2a05aeb2e9 | ||
|
|
e77ab79f5a | ||
|
|
522815b4c5 | ||
|
|
5ccdcd7a72 | ||
|
|
9ea6f7cfa0 | ||
|
|
ab1d4e77b9 | ||
|
|
7647316684 | ||
|
|
156be4be4e | ||
|
|
f08dd3e68f | ||
|
|
84027fbc7a | ||
|
|
75a8f79bc8 | ||
|
|
223b99f24e | ||
|
|
683469d445 | ||
|
|
67f4698ca5 | ||
|
|
f48d7589f7 | ||
|
|
0c39689ef2 | ||
|
|
25fab80219 | ||
|
|
593078ddfa | ||
|
|
3f2bc29de5 | ||
|
|
6f9640e11a | ||
|
|
d1e2c175f7 | ||
|
|
c64528b4de | ||
|
|
f90077ccf7 | ||
|
|
7a93d592ff | ||
|
|
876a3d70df | ||
|
|
99e47cb387 | ||
|
|
9aab5a7865 | ||
|
|
3ed93fbf89 | ||
|
|
50e80b953f | ||
|
|
275edf8651 | ||
|
|
90b4d8ee95 | ||
|
|
0d74cedf4d | ||
|
|
97e5bfcee8 | ||
|
|
0fbba4b362 | ||
|
|
ce3953ea9c | ||
|
|
4ba09144e9 | ||
|
|
85bf0e02a4 | ||
|
|
5b34a3791f | ||
|
|
46f0d7620d | ||
|
|
696e43b474 | ||
|
|
50e9559631 | ||
|
|
72a81b30d3 | ||
|
|
8b9d481233 | ||
|
|
e45cc33463 | ||
|
|
8b3e6c753e | ||
|
|
098881c5c9 | ||
|
|
0008008266 | ||
|
|
0c04dbb24c | ||
|
|
3b4016af92 | ||
|
|
fff7982e85 | ||
|
|
45d0f06e60 | ||
|
|
1636fa11e5 | ||
|
|
c59bf4cc7d | ||
|
|
5de71d0bec | ||
|
|
0804773551 | ||
|
|
2766235948 | ||
|
|
0a2eb99fd1 | ||
|
|
1f015629e6 | ||
|
|
ee03f71318 | ||
|
|
4d2707b6d3 | ||
|
|
52b87a32cc | ||
|
|
4b9dee5994 | ||
|
|
9a1c88a6a8 | ||
|
|
fe2571addb | ||
|
|
7e784f80f6 | ||
|
|
adc7e9d7d2 | ||
|
|
05c55e3bd0 | ||
|
|
3eecd0eaec | ||
|
|
c1514ef270 | ||
|
|
1340e0d78f | ||
|
|
006eb89fae | ||
|
|
8cd5e28f3a | ||
|
|
13ce1039ee | ||
|
|
bfbf6076b0 | ||
|
|
185441d6f6 | ||
|
|
9de163aa91 | ||
|
|
c49e2b5bde | ||
|
|
79ff3e838a | ||
|
|
39d63ec7b1 | ||
|
|
454d8d8b10 | ||
|
|
822a2b7701 | ||
|
|
48cb4e6f37 | ||
|
|
5d75e0b131 | ||
|
|
7515ba38df | ||
|
|
f1a48a0821 | ||
|
|
a2d124c36a | ||
|
|
add967f98f | ||
|
|
6f5802129c | ||
|
|
b5ff6304bf | ||
|
|
2eec8d2fc3 | ||
|
|
dd6a11b8c6 | ||
|
|
4f5d9ad007 | ||
|
|
ec46eab98c | ||
|
|
cff5f3ac0d | ||
|
|
5942fff42f | ||
|
|
d48d4ed0e9 | ||
|
|
a5d8f151f6 | ||
|
|
b832ad5eb6 | ||
|
|
b6217743c6 | ||
|
|
63f8e92aa1 | ||
|
|
d946c7dbf8 | ||
|
|
e1de8b6ae4 | ||
|
|
358df80b65 | ||
|
|
bec44a9ce8 | ||
|
|
cfd915a6f6 | ||
|
|
bddc6ccb7b | ||
|
|
360becc305 | ||
|
|
cd606ee6b0 | ||
|
|
7c498642cd | ||
|
|
1730e9cb1c | ||
|
|
c5608c3ee9 | ||
|
|
157841a538 | ||
|
|
ff857ea79f | ||
|
|
3c3fcadbb6 | ||
|
|
174548944f | ||
|
|
c57d7d3a89 | ||
|
|
58ff17998d | ||
|
|
b42afd0027 | ||
|
|
5312d61598 | ||
|
|
56cdfd23df | ||
|
|
69143ea5d6 | ||
|
|
a322afdcee | ||
|
|
c6f599cc61 | ||
|
|
5fe38bb6ff | ||
|
|
51d063ca5a | ||
|
|
42e1273241 | ||
|
|
d81316435b | ||
|
|
9f84f2989f | ||
|
|
c3c6118df1 | ||
|
|
9c85883b8a | ||
|
|
3725f83b48 | ||
|
|
7bfc3188f7 | ||
|
|
25d6e9693d |
19
.github/dependabot.yml
vendored
19
.github/dependabot.yml
vendored
@@ -11,7 +11,26 @@ updates:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 20
|
||||
versioning-strategy: increase
|
||||
groups:
|
||||
vitest:
|
||||
patterns:
|
||||
- "*vitest*"
|
||||
cooldown:
|
||||
default-days: 5
|
||||
semver-major-days: 5
|
||||
semver-minor-days: 3
|
||||
semver-patch-days: 3
|
||||
include:
|
||||
- "*"
|
||||
exclude:
|
||||
- "@maplibre/*"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
cooldown:
|
||||
default-days: 3
|
||||
# no semver support for github-actions
|
||||
# => no specific configuration for this
|
||||
include:
|
||||
- "*"
|
||||
|
||||
4
.github/workflows/auto-merge-dependabot.yml
vendored
4
.github/workflows/auto-merge-dependabot.yml
vendored
@@ -7,11 +7,11 @@ permissions: write-all
|
||||
jobs:
|
||||
dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }}
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@v2.4.0
|
||||
uses: dependabot/fetch-metadata@08eff52bf64351f401fb50d4972fa95b9f2c2d1b # v2.4.0
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- name: Approve Dependabot PRs
|
||||
|
||||
58
.github/workflows/ci.yml
vendored
58
.github/workflows/ci.yml
vendored
@@ -11,7 +11,8 @@ jobs:
|
||||
build-node:
|
||||
name: "build on ${{ matrix.os }}"
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||
|
||||
strategy:
|
||||
@@ -20,8 +21,9 @@ jobs:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with: { persist-credentials: false }
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm ci
|
||||
@@ -34,24 +36,27 @@ jobs:
|
||||
build-artifacts:
|
||||
name: "build artifacts"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with: { persist-credentials: false }
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- name: artifacts/maputnik
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: maputnik
|
||||
path: dist
|
||||
|
||||
# Build and upload desktop CLI artifacts
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
|
||||
with:
|
||||
go-version: ^1.23.x
|
||||
cache-dependency-path: desktop/go.sum
|
||||
@@ -61,39 +66,59 @@ jobs:
|
||||
run: npm run build-desktop
|
||||
|
||||
- name: Artifacts/linux
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: maputnik-linux
|
||||
path: ./desktop/bin/linux/
|
||||
|
||||
- name: Artifacts/darwin
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: maputnik-darwin
|
||||
path: ./desktop/bin/darwin/
|
||||
|
||||
- name: Artifacts/windows
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: maputnik-windows
|
||||
path: ./desktop/bin/windows/
|
||||
|
||||
unit-tests:
|
||||
name: "Unit tests"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with: { persist-credentials: false }
|
||||
- run: npm ci
|
||||
- run: npm run test-unit-ci
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||
with:
|
||||
files: ${{ github.workspace }}/coverage/coverage-final.json
|
||||
verbose: true
|
||||
|
||||
e2e-tests:
|
||||
name: "E2E tests using chrome"
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with: { persist-credentials: false }
|
||||
- run: npm ci
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v6
|
||||
uses: cypress-io/github-action@7ef72e250a9e564efb4ed4c2433971ada4cc38b4 # v6.10.4
|
||||
with:
|
||||
build: npm run build
|
||||
start: npm run start
|
||||
browser: chrome
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||
with:
|
||||
files: ${{ github.workspace }}/.nyc_output/out.json
|
||||
verbose: true
|
||||
@@ -104,16 +129,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with: { persist-credentials: false }
|
||||
- run: npm ci
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v6
|
||||
uses: cypress-io/github-action@7ef72e250a9e564efb4ed4c2433971ada4cc38b4 # v6.10.4
|
||||
with:
|
||||
build: docker build -t maputnik .
|
||||
start: docker run --rm --network host maputnik --port=8888
|
||||
browser: chrome
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||
with:
|
||||
files: ${{ github.workspace }}/.nyc_output/out.json
|
||||
verbose: true
|
||||
|
||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -38,11 +38,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -67,4 +67,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
|
||||
|
||||
6
.github/workflows/create-bump-version-pr.yml
vendored
6
.github/workflows/create-bump-version-pr.yml
vendored
@@ -16,13 +16,13 @@ jobs:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
./build/bump-version-changelog.js ${{ inputs.version }}
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
|
||||
with:
|
||||
commit-message: Bump version to ${{ inputs.version }}
|
||||
branch: bump-version-to-${{ inputs.version }}
|
||||
|
||||
18
.github/workflows/deploy.yml
vendored
18
.github/workflows/deploy.yml
vendored
@@ -8,12 +8,15 @@ jobs:
|
||||
deploy-pages:
|
||||
name: deploy/pages
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with: { persist-credentials: false }
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
@@ -24,7 +27,7 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Upload to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: dist
|
||||
@@ -33,19 +36,20 @@ jobs:
|
||||
deploy-docker:
|
||||
name: deploy/docker
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- run: docker build -t ghcr.io/maplibre/maputnik:main .
|
||||
- run: docker push ghcr.io/maplibre/maputnik:main
|
||||
|
||||
21
.github/workflows/release.yml
vendored
21
.github/workflows/release.yml
vendored
@@ -12,20 +12,23 @@ jobs:
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
persist-credentials: false
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Check if version has been updated
|
||||
id: check
|
||||
uses: EndBug/version-check@v2
|
||||
uses: EndBug/version-check@d17247dd94ca7b39d0b0691399be8d7c510622c9 # latest
|
||||
|
||||
outputs:
|
||||
publish: ${{ steps.check.outputs.changed }}
|
||||
@@ -39,19 +42,19 @@ jobs:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
|
||||
- name: Use Node.js from nvmrc
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Set up Go for desktop build
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
|
||||
with:
|
||||
go-version: ^1.23.x
|
||||
cache-dependency-path: desktop/go.sum
|
||||
@@ -59,7 +62,7 @@ jobs:
|
||||
|
||||
- name: Get version
|
||||
id: package-version
|
||||
uses: martinbeentjes/npm-get-version-action@v1.3.1
|
||||
uses: martinbeentjes/npm-get-version-action@3cf273023a0dda27efcd3164bdfb51908dd46a5b # v1.3.1
|
||||
|
||||
- name: Install
|
||||
run: npm ci
|
||||
@@ -70,7 +73,7 @@ jobs:
|
||||
|
||||
- name: Tag commit and push
|
||||
id: tag_version
|
||||
uses: mathieudutour/github-tag-action@v6.2
|
||||
uses: mathieudutour/github-tag-action@a22cf08638b34d5badda920f9daf6e72c477b07b # v6.2
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
custom_tag: ${{ steps.package-version.outputs.current-version }}
|
||||
@@ -88,7 +91,7 @@ jobs:
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: create_regular_release
|
||||
uses: ncipollo/release-action@v1
|
||||
uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
||||
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,10 +1,32 @@
|
||||
## main
|
||||
|
||||
### ✨ Features and improvements
|
||||
- Added translation to "Links" in debug modal
|
||||
- Add support for hillshade's color arrays and relief-color elevation expression
|
||||
- Change layers icons to make them a bit more distinct
|
||||
- Remove `@mdi` packages in favor of `react-icons`
|
||||
- Add ability to control the projection of the map - either globe or mercator
|
||||
- Add markdown support for doc related to the style-spec fields
|
||||
- Added global state modal to allow editing the global state
|
||||
- Added color highlight for problematic properties
|
||||
- Upgraded codemirror from version 5 to version 6
|
||||
- Add code editor to allow editing the entire style
|
||||
- Add support for sprite object in setting modal
|
||||
- Allow root-relative urls in the stylefile
|
||||
- _...Add new stuff here..._
|
||||
|
||||
### 🐞 Bug fixes
|
||||
|
||||
- Fixed the Expression editor (for long expressions) being able to be float under other components further down
|
||||
- Fixed an issue when clicking on a popup and then clicking on the map again
|
||||
- Fix modal close button possition
|
||||
- Fixed an issue with the generation of tranlations
|
||||
- Fix missing spec info when clicking next to a property
|
||||
- Fix Firefox open file that stopped working due to react upgrade
|
||||
- Fix issue with missing bottom error panel
|
||||
- Fixed headers in left panes (Layers list and Layer editor) to remain visible when scrolling
|
||||
- Fix error when using a source from localhost
|
||||
- Fix an issue with scrolling when using the code editor
|
||||
- _...Add new stuff here..._
|
||||
|
||||
## 3.0.0
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
* https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as fs from "fs";
|
||||
|
||||
const changelogPath = 'CHANGELOG.md';
|
||||
let changelog = fs.readFileSync(changelogPath, 'utf8');
|
||||
changelog = changelog.replace('## main', `## ${process.argv[2]}`);
|
||||
changelog = changelog.replaceAll('- _...Add new stuff here..._\n', '');
|
||||
const changelogPath = "CHANGELOG.md";
|
||||
let changelog = fs.readFileSync(changelogPath, "utf8");
|
||||
changelog = changelog.replace("## main", `## ${process.argv[2]}`);
|
||||
changelog = changelog.replaceAll("- _...Add new stuff here..._\n", "");
|
||||
changelog = `## main
|
||||
|
||||
### ✨ Features and improvements
|
||||
@@ -26,4 +26,4 @@ changelog = `## main
|
||||
|
||||
` + changelog;
|
||||
|
||||
fs.writeFileSync(changelogPath, changelog, 'utf8');
|
||||
fs.writeFileSync(changelogPath, changelog, "utf8");
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
// Copied from maplibre/maplibre-gl-js
|
||||
// https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as fs from "fs";
|
||||
|
||||
const changelogPath = 'CHANGELOG.md';
|
||||
const changelog = fs.readFileSync(changelogPath, 'utf8');
|
||||
const changelogPath = "CHANGELOG.md";
|
||||
const changelog = fs.readFileSync(changelogPath, "utf8");
|
||||
|
||||
/*
|
||||
Parse the raw changelog text and split it into individual releases.
|
||||
@@ -25,8 +25,8 @@ let match;
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while (match = regex.exec(changelog)) {
|
||||
releaseNotes.push({
|
||||
'version': match[1],
|
||||
'changelog': match[2].trim(),
|
||||
"version": match[1],
|
||||
"changelog": match[2].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,10 +35,10 @@ const previous = releaseNotes[1];
|
||||
|
||||
// Print the release notes template.
|
||||
|
||||
let header = 'Changes since previous version'
|
||||
let header = "Changes since previous version";
|
||||
if (previous) {
|
||||
header = `https://github.com/maplibre/maputnik
|
||||
[Changes](https://github.com/maplibre/maputnik/compare/v${previous.version}...v${latest.version}) since [Maputnik v${previous.version}](https://github.com/maplibre/maputnik/releases/tag/v${previous.version})`
|
||||
[Changes](https://github.com/maplibre/maputnik/compare/v${previous.version}...v${latest.version}) since [Maputnik v${previous.version}](https://github.com/maplibre/maputnik/releases/tag/v${previous.version})`;
|
||||
}
|
||||
const templatedReleaseNotes = `${header}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ export default defineConfig({
|
||||
return config;
|
||||
},
|
||||
baseUrl: "http://localhost:8888",
|
||||
scrollBehavior: "center",
|
||||
retries: {
|
||||
runMode: 2,
|
||||
openMode: 0,
|
||||
|
||||
@@ -18,10 +18,10 @@ describe("accessibility", () => {
|
||||
then(get.skipTargetLayerList()).shouldBeFocused();
|
||||
});
|
||||
|
||||
// This fails for some reason only in Chrome, but passes in firefox. Adding a skip here to allow merge and later on we'll decide if we want to fix this or not.
|
||||
it.skip("skip link to layer editor", () => {
|
||||
it("skip link to layer editor", () => {
|
||||
const selector = "root:skip:layer-editor";
|
||||
then(get.elementByTestId(selector)).shouldExist();
|
||||
then(get.elementByTestId("skip-target-layer-editor")).shouldExist();
|
||||
when.tab().tab();
|
||||
then(get.elementByTestId(selector)).shouldBeFocused();
|
||||
when.click(selector);
|
||||
|
||||
18
cypress/e2e/code-editor.cy.ts
Normal file
18
cypress/e2e/code-editor.cy.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("code editor", () => {
|
||||
const { beforeAndAfter, when, get, then } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
|
||||
it("open code editor", () => {
|
||||
when.click("nav:code-editor");
|
||||
then(get.element(".maputnik-code-editor")).shouldExist();
|
||||
});
|
||||
|
||||
it("closes code editor", () => {
|
||||
when.click("nav:code-editor");
|
||||
then(get.element(".maputnik-code-editor")).shouldExist();
|
||||
when.click("nav:code-editor");
|
||||
then(get.element(".maputnik-code-editor")).shouldNotExist();
|
||||
});
|
||||
});
|
||||
294
cypress/e2e/layer-editor.cy.ts
Normal file
294
cypress/e2e/layer-editor.cy.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
import { v1 as uuid } from "uuid";
|
||||
|
||||
describe("layer editor", () => {
|
||||
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
beforeEach(() => {
|
||||
when.setStyle("both");
|
||||
when.modal.open();
|
||||
});
|
||||
|
||||
function createBackground() {
|
||||
const id = uuid();
|
||||
|
||||
when.selectWithin("add-layer.layer-type", "background");
|
||||
when.setValue("add-layer.layer-id.input", "background:" + id);
|
||||
|
||||
when.click("add-layer");
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
it("expand/collapse");
|
||||
it("id", () => {
|
||||
const bgId = createBackground();
|
||||
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
|
||||
const id = uuid();
|
||||
when.setValue("layer-editor.layer-id.input", "foobar:" + id);
|
||||
when.click("min-zoom");
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "foobar:" + id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("source", () => {
|
||||
it("should show error when the source is invalid", () => {
|
||||
when.modal.fillLayers({
|
||||
type: "circle",
|
||||
layer: "invalid",
|
||||
});
|
||||
then(get.element(".maputnik-input-block--error .maputnik-input-block-label")).shouldHaveCss("color", "rgb(207, 74, 74)");
|
||||
});
|
||||
});
|
||||
|
||||
describe("min-zoom", () => {
|
||||
let bgId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.setValue("min-zoom.input-text", "1");
|
||||
when.click("layer-editor.layer-id");
|
||||
});
|
||||
|
||||
it("should update min-zoom in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
minzoom: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("when clicking next layer should update style on local storage", () => {
|
||||
when.type("min-zoom.input-text", "{backspace}");
|
||||
when.click("max-zoom.input-text");
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
minzoom: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("max-zoom", () => {
|
||||
let bgId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.setValue("max-zoom.input-text", "1");
|
||||
when.click("layer-editor.layer-id");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
maxzoom: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("comments", () => {
|
||||
let bgId: string;
|
||||
const comment = "42";
|
||||
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.setValue("layer-comment.input", comment);
|
||||
when.click("layer-editor.layer-id");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
metadata: {
|
||||
"maputnik:comment": comment,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("when unsetting", () => {
|
||||
beforeEach(() => {
|
||||
when.clear("layer-comment.input");
|
||||
when.click("min-zoom.input-text");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("color", () => {
|
||||
let bgId: string;
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.click("spec-field:background-color");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("opacity", () => {
|
||||
let bgId: string;
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.type("spec-field-input:background-opacity", "0.");
|
||||
});
|
||||
|
||||
it("should keep '.' in the input field", () => {
|
||||
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0.");
|
||||
});
|
||||
|
||||
it("should revert to a valid value when focus out", () => {
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
describe("filter", () => {
|
||||
it("expand/collapse");
|
||||
it("compound filter");
|
||||
});
|
||||
|
||||
describe("layout", () => {
|
||||
it("text-font", () => {
|
||||
when.setStyle("font");
|
||||
when.collapseGroupInLayerEditor();
|
||||
when.collapseGroupInLayerEditor(1);
|
||||
when.collapseGroupInLayerEditor(2);
|
||||
when.doWithin("spec-field:text-font", () => {
|
||||
get.element(".maputnik-autocomplete input").first().click();
|
||||
});
|
||||
then(get.element(".maputnik-autocomplete-menu-item")).shouldBeVisible();
|
||||
then(get.element(".maputnik-autocomplete-menu-item")).shouldHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("paint", () => {
|
||||
it("expand/collapse");
|
||||
it("color");
|
||||
it("pattern");
|
||||
it("opacity");
|
||||
});
|
||||
|
||||
describe("json-editor", () => {
|
||||
it("add", () => {
|
||||
const id = when.modal.fillLayers({
|
||||
type: "circle",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "circle",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const sourceText = get.elementByText('"source"');
|
||||
|
||||
sourceText.click();
|
||||
sourceText.type("\"");
|
||||
|
||||
then(get.element(".cm-lint-marker-error")).shouldExist();
|
||||
});
|
||||
|
||||
|
||||
it("expand/collapse");
|
||||
it("modify");
|
||||
|
||||
it("parse error", () => {
|
||||
const bgId = createBackground();
|
||||
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.collapseGroupInLayerEditor();
|
||||
when.collapseGroupInLayerEditor(1);
|
||||
then(get.element(".cm-lint-marker-error")).shouldNotExist();
|
||||
|
||||
when.appendTextInJsonEditor(
|
||||
"\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {"
|
||||
);
|
||||
then(get.element(".cm-lint-marker-error")).shouldExist();
|
||||
});
|
||||
});
|
||||
|
||||
describe("sticky header", () => {
|
||||
it("should keep layer header visible when scrolling properties", () => {
|
||||
// Setup: Create a layer with many properties (e.g., symbol layer)
|
||||
when.modal.fillLayers({
|
||||
type: "symbol",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
when.wait(500);
|
||||
const header = get.elementByTestId("layer-editor.header");
|
||||
then(header).shouldBeVisible();
|
||||
|
||||
get.element(".maputnik-scroll-container").scrollTo("bottom", { ensureScrollable: false });
|
||||
when.wait(200);
|
||||
|
||||
then(header).shouldBeVisible();
|
||||
then(get.elementByTestId("skip-target-layer-editor")).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
import { v1 as uuid } from "uuid";
|
||||
import { MaputnikDriver } from "./maputnik-driver";
|
||||
|
||||
describe("layers", () => {
|
||||
describe("layers list", () => {
|
||||
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
beforeEach(() => {
|
||||
@@ -97,8 +96,26 @@ describe("layers", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when selecting a layer", () => {
|
||||
let secondId: string;
|
||||
beforeEach(() => {
|
||||
when.modal.open();
|
||||
secondId = when.modal.fillLayers({
|
||||
id: "second-layer",
|
||||
type: "background",
|
||||
});
|
||||
});
|
||||
it("should show the selected layer in the editor", () => {
|
||||
when.realClick("layer-list-item:" + secondId);
|
||||
then(get.elementByTestId("layer-editor.layer-id.input")).shouldHaveValue(secondId);
|
||||
when.realClick("layer-list-item:" + id);
|
||||
then(get.elementByTestId("layer-editor.layer-id.input")).shouldHaveValue(id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("background", () => {
|
||||
it("add", () => {
|
||||
const id = when.modal.fillLayers({
|
||||
@@ -114,227 +131,7 @@ describe("layers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("modify", () => {
|
||||
function createBackground() {
|
||||
// Setup
|
||||
const id = uuid();
|
||||
|
||||
when.selectWithin("add-layer.layer-type", "background");
|
||||
when.setValue("add-layer.layer-id.input", "background:" + id);
|
||||
|
||||
when.click("add-layer");
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
// ====> THESE SHOULD BE FROM THE SPEC
|
||||
describe("layer", () => {
|
||||
it("expand/collapse");
|
||||
it("id", () => {
|
||||
const bgId = createBackground();
|
||||
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
|
||||
const id = uuid();
|
||||
when.setValue("layer-editor.layer-id.input", "foobar:" + id);
|
||||
when.click("min-zoom");
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "foobar:" + id,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("min-zoom", () => {
|
||||
let bgId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.setValue("min-zoom.input-text", "1");
|
||||
when.click("layer-editor.layer-id");
|
||||
});
|
||||
|
||||
it("should update min-zoom in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
minzoom: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("when clicking next layer should update style on local storage", () => {
|
||||
when.type("min-zoom.input-text", "{backspace}");
|
||||
when.click("max-zoom.input-text");
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
minzoom: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("max-zoom", () => {
|
||||
let bgId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.setValue("max-zoom.input-text", "1");
|
||||
when.click("layer-editor.layer-id");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
maxzoom: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("comments", () => {
|
||||
let bgId: string;
|
||||
const comment = "42";
|
||||
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.setValue("layer-comment.input", comment);
|
||||
when.click("layer-editor.layer-id");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
metadata: {
|
||||
"maputnik:comment": comment,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("when unsetting", () => {
|
||||
beforeEach(() => {
|
||||
when.clear("layer-comment.input");
|
||||
when.click("min-zoom.input-text");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("color", () => {
|
||||
let bgId: string;
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.click("spec-field:background-color");
|
||||
});
|
||||
|
||||
it("should update style in local storage", () => {
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("opacity", () => {
|
||||
let bgId: string;
|
||||
beforeEach(() => {
|
||||
bgId = createBackground();
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.type("spec-field-input:background-opacity", "0.");
|
||||
});
|
||||
|
||||
it("should keep '.' in the input field", () => {
|
||||
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue("0.");
|
||||
});
|
||||
|
||||
it("should revert to a valid value when focus out", () => {
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
then(get.elementByTestId("spec-field-input:background-opacity")).shouldHaveValue('0');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
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", () => {
|
||||
const bgId = createBackground();
|
||||
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
|
||||
const errorSelector = ".CodeMirror-lint-marker-error";
|
||||
then(get.elementByTestId(errorSelector)).shouldNotExist();
|
||||
|
||||
when.click(".CodeMirror");
|
||||
when.typeKeys(
|
||||
"\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {"
|
||||
);
|
||||
then(get.elementByTestId(errorSelector)).shouldExist();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("modify", () => {});
|
||||
});
|
||||
|
||||
describe("fill", () => {
|
||||
@@ -449,6 +246,44 @@ describe("layers", () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should show spec info when hovering and clicking single line property", () => {
|
||||
when.modal.fillLayers({
|
||||
type: "symbol",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
when.hover("spec-field-container:text-rotate");
|
||||
then(get.elementByTestId("field-doc-button-Rotate")).shouldBeVisible();
|
||||
when.click("field-doc-button-Rotate", 0);
|
||||
then(get.elementByTestId("spec-field-doc")).shouldContainText("Rotates the ");
|
||||
});
|
||||
|
||||
it("should show spec info when hovering and clicking multi line property", () => {
|
||||
when.modal.fillLayers({
|
||||
type: "symbol",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
when.hover("spec-field-container:text-offset");
|
||||
then(get.elementByTestId("field-doc-button-Offset")).shouldBeVisible();
|
||||
when.click("field-doc-button-Offset", 0);
|
||||
then(get.elementByTestId("spec-field-doc")).shouldContainText("Offset distance");
|
||||
});
|
||||
|
||||
it("should hide spec info when clicking a second time", () => {
|
||||
when.modal.fillLayers({
|
||||
type: "symbol",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
when.hover("spec-field-container:text-rotate");
|
||||
then(get.elementByTestId("field-doc-button-Rotate")).shouldBeVisible();
|
||||
when.click("field-doc-button-Rotate", 0);
|
||||
when.wait(200);
|
||||
when.click("field-doc-button-Rotate", 0);
|
||||
then(get.elementByTestId("spec-field-doc")).shouldNotBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe("raster", () => {
|
||||
@@ -508,6 +343,104 @@ describe("layers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("hillshade", () => {
|
||||
it("add", () => {
|
||||
const id = when.modal.fillLayers({
|
||||
type: "hillshade",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "hillshade",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("set hillshade illumination direction array", () => {
|
||||
const id = when.modal.fillLayers({
|
||||
type: "hillshade",
|
||||
layer: "example",
|
||||
});
|
||||
when.collapseGroupInLayerEditor();
|
||||
when.collapseGroupInLayerEditor(1);
|
||||
when.setValueToPropertyArray("spec-field:hillshade-illumination-direction", "1");
|
||||
when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "2");
|
||||
when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "3");
|
||||
when.addValueToPropertyArray("spec-field:hillshade-illumination-direction", "4");
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "hillshade",
|
||||
source: "example",
|
||||
paint: {
|
||||
"hillshade-illumination-direction": [ 1, 2, 3, 4 ]
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("set hillshade highlight color array", () => {
|
||||
const id = when.modal.fillLayers({
|
||||
type: "hillshade",
|
||||
layer: "example",
|
||||
});
|
||||
when.collapseGroupInLayerEditor();
|
||||
when.setValueToPropertyArray("spec-field:hillshade-highlight-color", "blue");
|
||||
when.addValueToPropertyArray("spec-field:hillshade-highlight-color", "#00ff00");
|
||||
when.addValueToPropertyArray("spec-field:hillshade-highlight-color", "rgba(255, 255, 0, 1)");
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "hillshade",
|
||||
source: "example",
|
||||
paint: {
|
||||
"hillshade-highlight-color": [ "blue", "#00ff00", "rgba(255, 255, 0, 1)" ]
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("color-relief", () => {
|
||||
it("add", () => {
|
||||
const id = when.modal.fillLayers({
|
||||
type: "color-relief",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "color-relief",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("adds elevation expression when clicking the elevation button", () => {
|
||||
when.modal.fillLayers({
|
||||
type: "color-relief",
|
||||
layer: "example",
|
||||
});
|
||||
when.collapseGroupInLayerEditor();
|
||||
when.click("make-elevation-function");
|
||||
then(get.element("[data-wd-key='spec-field-container:color-relief-color'] .cm-line")).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe("groups", () => {
|
||||
it("simple", () => {
|
||||
when.setStyle("geojson");
|
||||
@@ -544,33 +477,6 @@ describe("layers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("layereditor jsonlint should error", ()=>{
|
||||
it("add", () => {
|
||||
const id = when.modal.fillLayers({
|
||||
type: "circle",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
{
|
||||
id: id,
|
||||
type: "circle",
|
||||
source: "example",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const sourceText = get.elementByText('"source"');
|
||||
|
||||
sourceText.click();
|
||||
sourceText.type("\"");
|
||||
|
||||
const error = get.element('.CodeMirror-lint-marker-error');
|
||||
error.should('exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe("drag and drop", () => {
|
||||
it("move layer should update local storage", () => {
|
||||
when.modal.open();
|
||||
@@ -588,7 +494,8 @@ describe("layers", () => {
|
||||
id: "c",
|
||||
type: "background",
|
||||
});
|
||||
when.dragAndDrop(get.elementByTestId("layer-list-item:" + firstId), get.elementByTestId("layer-list-item:" + thirdId));
|
||||
|
||||
when.dragAndDropWithWait("layer-list-item:" + firstId, "layer-list-item:" + thirdId);
|
||||
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
layers: [
|
||||
@@ -608,4 +515,27 @@ describe("layers", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("sticky header", () => {
|
||||
it("should keep header visible when scrolling layer list", () => {
|
||||
// Setup: Create multiple layers to enable scrolling
|
||||
for (let i = 0; i < 20; i++) {
|
||||
when.modal.open();
|
||||
when.modal.fillLayers({
|
||||
id: `layer-${i}`,
|
||||
type: "background",
|
||||
});
|
||||
}
|
||||
|
||||
when.wait(500);
|
||||
const header = get.elementByTestId("layer-list.header");
|
||||
then(header).shouldBeVisible();
|
||||
|
||||
// Scroll the layer list container (use ensureScrollable: false to avoid flakiness)
|
||||
get.elementByTestId("layer-list").scrollTo("bottom", { ensureScrollable: false });
|
||||
when.wait(200);
|
||||
then(header).shouldBeVisible();
|
||||
then(get.elementByTestId("layer-list:add-layer")).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -25,8 +25,27 @@ describe("map", () => {
|
||||
});
|
||||
|
||||
describe("search", () => {
|
||||
it('should exist', () => {
|
||||
it("should exist", () => {
|
||||
then(get.searchControl()).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe("popup", () => {
|
||||
beforeEach(() => {
|
||||
when.setStyle("rectangles");
|
||||
});
|
||||
it("should open on feature click", () => {
|
||||
when.clickCenter("maplibre:map");
|
||||
then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
|
||||
});
|
||||
|
||||
it("should open a second feature after closing popup", () => {
|
||||
when.clickCenter("maplibre:map");
|
||||
then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
|
||||
when.closePopup();
|
||||
then(get.elementByTestId("feature-layer-popup")).shouldNotExist();
|
||||
when.clickCenter("maplibre:map");
|
||||
then(get.elementByTestId("feature-layer-popup")).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/// <reference types="cypress-real-events" />
|
||||
import { CypressHelper } from "@shellygo/cypress-test-utils";
|
||||
import "cypress-real-events/support";
|
||||
|
||||
export default class MaputnikCypressHelper {
|
||||
private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" });
|
||||
@@ -12,6 +14,34 @@ export default class MaputnikCypressHelper {
|
||||
};
|
||||
|
||||
public when = {
|
||||
dragAndDropWithWait: (element: string, targetElement: string) => {
|
||||
this.helper.get.elementByTestId(element).realMouseDown({ button: "left", position: "center" });
|
||||
this.helper.get.elementByTestId(element).realMouseMove(0, 10, { position: "center" });
|
||||
this.helper.get.elementByTestId(targetElement).realMouseMove(0, 0, { position: "center" });
|
||||
this.helper.when.wait(1);
|
||||
this.helper.get.elementByTestId(targetElement).realMouseUp();
|
||||
},
|
||||
clickCenter: (element: string) => {
|
||||
this.helper.get.elementByTestId(element).realMouseDown({ button: "left", position: "center" });
|
||||
this.helper.when.wait(200);
|
||||
this.helper.get.elementByTestId(element).realMouseUp();
|
||||
},
|
||||
openFileByFixture: (fixture: string, buttonTestId: string, inputTestId: string) => {
|
||||
cy.window().then((win) => {
|
||||
const file = {
|
||||
text: cy.stub().resolves(cy.fixture(fixture).then(JSON.stringify)),
|
||||
};
|
||||
const fileHandle = {
|
||||
getFile: cy.stub().resolves(file),
|
||||
};
|
||||
if (!win.showOpenFilePicker) {
|
||||
this.helper.get.elementByTestId(inputTestId).selectFile("cypress/fixtures/" + fixture, { force: true });
|
||||
} else {
|
||||
cy.stub(win, "showOpenFilePicker").resolves([fileHandle]);
|
||||
this.helper.get.elementByTestId(buttonTestId).click();
|
||||
}
|
||||
});
|
||||
},
|
||||
...this.helper.when,
|
||||
};
|
||||
|
||||
|
||||
@@ -78,6 +78,20 @@ export class MaputnikDriver {
|
||||
fixture: "geojson-raster-style.json",
|
||||
},
|
||||
});
|
||||
this.helper.given.interceptAndMockResponse({
|
||||
method: "GET",
|
||||
url: baseUrl + "rectangles-style.json",
|
||||
response: {
|
||||
fixture: "rectangles-style.json",
|
||||
},
|
||||
});
|
||||
this.helper.given.interceptAndMockResponse({
|
||||
method: "GET",
|
||||
url: baseUrl + "example-style-with-fonts.json",
|
||||
response: {
|
||||
fixture: "example-style-with-fonts.json",
|
||||
},
|
||||
});
|
||||
this.helper.given.interceptAndMockResponse({
|
||||
method: "GET",
|
||||
url: "*example.local/*",
|
||||
@@ -88,6 +102,11 @@ export class MaputnikDriver {
|
||||
url: "*example.com/*",
|
||||
response: [],
|
||||
});
|
||||
this.helper.given.interceptAndMockResponse({
|
||||
method: "GET",
|
||||
url: "https://www.glyph-server.com/*",
|
||||
response: ["Font 1", "Font 2", "Font 3"],
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -102,29 +121,36 @@ export class MaputnikDriver {
|
||||
this.helper.when.waitForResponse("example-style.json");
|
||||
},
|
||||
chooseExampleFile: () => {
|
||||
this.helper.get
|
||||
.bySelector("type", "file")
|
||||
.selectFile("cypress/fixtures/example-style.json", { force: true });
|
||||
this.helper.given.fixture("example-style.json", "example-style.json");
|
||||
this.helper.when.openFileByFixture("example-style.json", "modal:open.file.button", "modal:open.file.input");
|
||||
this.helper.when.wait(200);
|
||||
},
|
||||
setStyle: (
|
||||
styleProperties: "geojson" | "raster" | "both" | "layer" | "",
|
||||
styleProperties: "geojson" | "raster" | "both" | "layer" | "rectangles" | "font" | "",
|
||||
zoom?: number
|
||||
) => {
|
||||
const url = new URL(baseUrl);
|
||||
switch (styleProperties) {
|
||||
case "geojson":
|
||||
url.searchParams.set("style", baseUrl + "geojson-style.json");
|
||||
break;
|
||||
case "raster":
|
||||
url.searchParams.set("style", baseUrl + "raster-style.json");
|
||||
break;
|
||||
case "both":
|
||||
url.searchParams.set("style", baseUrl + "geojson-raster-style.json");
|
||||
break;
|
||||
case "layer":
|
||||
url.searchParams.set("style", baseUrl + "example-layer-style.json");
|
||||
break;
|
||||
case "geojson":
|
||||
url.searchParams.set("style", baseUrl + "geojson-style.json");
|
||||
break;
|
||||
case "raster":
|
||||
url.searchParams.set("style", baseUrl + "raster-style.json");
|
||||
break;
|
||||
case "both":
|
||||
url.searchParams.set("style", baseUrl + "geojson-raster-style.json");
|
||||
break;
|
||||
case "layer":
|
||||
url.searchParams.set("style", baseUrl + "example-layer-style.json");
|
||||
break;
|
||||
case "rectangles":
|
||||
url.searchParams.set("style", baseUrl + "rectangles-style.json");
|
||||
break;
|
||||
case "font":
|
||||
url.searchParams.set("style", baseUrl + "example-style-with-fonts.json");
|
||||
break;
|
||||
}
|
||||
|
||||
if (zoom) {
|
||||
url.hash = `${zoom}/41.3805/2.1635`;
|
||||
}
|
||||
@@ -133,7 +159,7 @@ export class MaputnikDriver {
|
||||
this.helper.when.acceptConfirm();
|
||||
}
|
||||
// when methods should not include assertions
|
||||
const toolbarLink = this.helper.get.elementByTestId("toolbar:link")
|
||||
const toolbarLink = this.helper.get.elementByTestId("toolbar:link");
|
||||
toolbarLink.scrollIntoView();
|
||||
toolbarLink.should("be.visible");
|
||||
},
|
||||
@@ -164,6 +190,35 @@ export class MaputnikDriver {
|
||||
.clear()
|
||||
.type(text, { parseSpecialCharSequences: false });
|
||||
},
|
||||
|
||||
setValueToPropertyArray: (selector: string, value: string) => {
|
||||
this.when.doWithin(selector, () => {
|
||||
this.helper.get.element(".maputnik-array-block-content input").last().type("{selectall}"+value, {force: true });
|
||||
});
|
||||
},
|
||||
|
||||
addValueToPropertyArray: (selector: string, value: string) => {
|
||||
this.when.doWithin(selector, () => {
|
||||
this.helper.get.element(".maputnik-array-add-value").click({ force: true });
|
||||
this.helper.get.element(".maputnik-array-block-content input").last().type("{selectall}"+value, {force: true });
|
||||
});
|
||||
},
|
||||
|
||||
closePopup: () => {
|
||||
this.helper.get.element(".maplibregl-popup-close-button").click();
|
||||
},
|
||||
|
||||
collapseGroupInLayerEditor: (index = 0) => {
|
||||
this.helper.get.element(".maputnik-layer-editor-group__button").eq(index).realClick();
|
||||
},
|
||||
|
||||
appendTextInJsonEditor: (text: string) => {
|
||||
this.helper.get.element(".cm-line").first().click().type(text, { parseSpecialCharSequences: false });
|
||||
},
|
||||
|
||||
setTextInJsonEditor: (text: string) => {
|
||||
this.helper.get.element(".cm-line").first().click().clear().type(text, { parseSpecialCharSequences: false });
|
||||
}
|
||||
};
|
||||
|
||||
public get = {
|
||||
@@ -183,6 +238,6 @@ export class MaputnikDriver {
|
||||
skipTargetLayerEditor: () =>
|
||||
this.helper.get.elementByTestId("skip-target-layer-editor"),
|
||||
canvas: () => this.helper.get.element("canvas"),
|
||||
searchControl: () => this.helper.get.element('.maplibregl-ctrl-geocoder')
|
||||
searchControl: () => this.helper.get.element(".maplibregl-ctrl-geocoder")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,10 +18,9 @@ describe("modals", () => {
|
||||
then(get.elementByTestId("modal:open")).shouldNotExist();
|
||||
});
|
||||
|
||||
it.skip("upload", () => {
|
||||
// HM: I was not able to make the following choose file actually to select a file and close the modal...
|
||||
it("upload", () => {
|
||||
when.chooseExampleFile();
|
||||
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
|
||||
then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
|
||||
});
|
||||
|
||||
describe("when click open url", () => {
|
||||
@@ -171,12 +170,22 @@ describe("modals", () => {
|
||||
});
|
||||
|
||||
it("sprite url", () => {
|
||||
when.setValue("modal:settings.sprite", "http://example.com");
|
||||
when.setTextInJsonEditor("\"http://example.com\"");
|
||||
when.click("modal:settings.name");
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
sprite: "http://example.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("sprite object", () => {
|
||||
when.setTextInJsonEditor(JSON.stringify([{ id: "1", url: "2" }]));
|
||||
|
||||
when.click("modal:settings.name");
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
sprite: [{ id: "1", url: "2" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("glyphs url", () => {
|
||||
const glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
|
||||
when.setValue("modal:settings.glyphs", glyphsUrl);
|
||||
@@ -236,6 +245,29 @@ describe("modals", () => {
|
||||
).shouldInclude({ "maputnik:locationiq_access_token": apiKey });
|
||||
});
|
||||
|
||||
it("style projection mercator", () => {
|
||||
when.select("modal:settings.projection", "mercator");
|
||||
then(
|
||||
get.styleFromLocalStorage().then((style) => style.projection)
|
||||
).shouldInclude({ type: "mercator" });
|
||||
});
|
||||
|
||||
it("style projection globe", () => {
|
||||
when.select("modal:settings.projection", "globe");
|
||||
then(
|
||||
get.styleFromLocalStorage().then((style) => style.projection)
|
||||
).shouldInclude({ type: "globe" });
|
||||
});
|
||||
|
||||
|
||||
it("style projection vertical-perspective", () => {
|
||||
when.select("modal:settings.projection", "vertical-perspective");
|
||||
then(
|
||||
get.styleFromLocalStorage().then((style) => style.projection)
|
||||
).shouldInclude({ type: "vertical-perspective" });
|
||||
|
||||
});
|
||||
|
||||
it("style renderer", () => {
|
||||
cy.on("uncaught:exception", () => false); // this is due to the fact that this is an invalid style for openlayers
|
||||
when.select("modal:settings.maputnik:renderer", "ol");
|
||||
@@ -253,10 +285,10 @@ describe("modals", () => {
|
||||
|
||||
it("inlcude API key when change renderer", () => {
|
||||
|
||||
when.click("modal:settings.close-modal")
|
||||
when.click("modal:settings.close-modal");
|
||||
when.click("nav:open");
|
||||
|
||||
get.elementByAttribute('aria-label', "MapTiler Basic").should('exist').click();
|
||||
get.elementByAttribute("aria-label", "MapTiler Basic").should("exist").click();
|
||||
when.wait(1000);
|
||||
when.click("nav:settings");
|
||||
|
||||
@@ -304,6 +336,79 @@ describe("modals", () => {
|
||||
it("toggle");
|
||||
});
|
||||
|
||||
describe("global state", () => {
|
||||
beforeEach(() => {
|
||||
when.click("nav:global-state");
|
||||
});
|
||||
|
||||
it("add variable", () => {
|
||||
when.wait(100);
|
||||
when.click("global-state-add-variable");
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
state: { key1: { default: "value" } },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("add multiple variables", () => {
|
||||
when.click("global-state-add-variable");
|
||||
when.click("global-state-add-variable");
|
||||
when.click("global-state-add-variable");
|
||||
when.wait(100);
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
state: { key1: { default: "value" }, key2: { default: "value" }, key3: { default: "value" } },
|
||||
});
|
||||
});
|
||||
|
||||
it("remove variable", () => {
|
||||
when.click("global-state-add-variable");
|
||||
when.click("global-state-add-variable");
|
||||
when.click("global-state-add-variable");
|
||||
when.click("global-state-remove-variable", 0);
|
||||
when.wait(100);
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
state: { key2: { default: "value" }, key3: { default: "value" } },
|
||||
});
|
||||
});
|
||||
|
||||
it("edit variable key", () => {
|
||||
when.click("global-state-add-variable");
|
||||
when.wait(100);
|
||||
when.setValue("global-state-variable-key:0", "mykey");
|
||||
when.typeKeys("{enter}");
|
||||
when.wait(100);
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
state: { mykey: { default: "value" } },
|
||||
});
|
||||
});
|
||||
|
||||
it("edit variable value", () => {
|
||||
when.click("global-state-add-variable");
|
||||
when.wait(100);
|
||||
when.setValue("global-state-variable-value:0", "myvalue");
|
||||
when.typeKeys("{enter}");
|
||||
when.wait(100);
|
||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||
state: { key1: { default: "myvalue" } },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("error panel", () => {
|
||||
it("not visible when no errors", () => {
|
||||
then(get.element("maputnik-message-panel-error")).shouldNotExist();
|
||||
});
|
||||
|
||||
it("visible on style error", () => {
|
||||
when.modal.open();
|
||||
when.modal.fillLayers({
|
||||
type: "circle",
|
||||
layer: "invalid",
|
||||
});
|
||||
then(get.element(".maputnik-message-panel-error")).shouldBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Handle localStorage QuotaExceededError", () => {
|
||||
it("handles quota exceeded error when opening style from URL", () => {
|
||||
// Clear localStorage to start fresh
|
||||
@@ -322,7 +427,7 @@ describe("modals", () => {
|
||||
win.localStorage.setItem(key, chunk);
|
||||
} catch (e: any) {
|
||||
// Verify it's a quota error
|
||||
if (e.name === 'QuotaExceededError') {
|
||||
if (e.name === "QuotaExceededError") {
|
||||
if (chunkSize <= 1) return;
|
||||
else {
|
||||
chunkSize /= 2;
|
||||
|
||||
38
cypress/fixtures/example-style-with-fonts.json
Normal file
38
cypress/fixtures/example-style-with-fonts.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "test-style",
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"metadata": {
|
||||
"maputnik:renderer": "mlgljs"
|
||||
},
|
||||
"sources": {
|
||||
"example": {
|
||||
"type": "geojson",
|
||||
"data": {
|
||||
"type": "FeatureCollection",
|
||||
"features":[{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"name": "Dinagat Islands"
|
||||
},
|
||||
"geometry":{
|
||||
"type": "Point",
|
||||
"coordinates": [125.6, 10.1]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
},
|
||||
"glyphs": "https://www.glyph-server.com/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": [
|
||||
{
|
||||
"id": "label",
|
||||
"type": "symbol",
|
||||
"source": "example",
|
||||
"layout": {
|
||||
"text-font": ["Font"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
92
cypress/fixtures/rectangles-style.json
Normal file
92
cypress/fixtures/rectangles-style.json
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"version": 8,
|
||||
"sources": {
|
||||
"rectangles": {
|
||||
"type": "geojson",
|
||||
"data": {
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[
|
||||
-130.78125,
|
||||
-33.13755119234615
|
||||
],
|
||||
[
|
||||
-130.78125,
|
||||
63.548552232036414
|
||||
],
|
||||
[
|
||||
15.468749999999998,
|
||||
63.548552232036414
|
||||
],
|
||||
[
|
||||
15.468749999999998,
|
||||
-33.13755119234615
|
||||
],
|
||||
[
|
||||
-130.78125,
|
||||
-33.13755119234615
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[
|
||||
-48.515625,
|
||||
-54.97761367069625
|
||||
],
|
||||
[
|
||||
-48.515625,
|
||||
36.5978891330702
|
||||
],
|
||||
[
|
||||
169.45312499999997,
|
||||
36.5978891330702
|
||||
],
|
||||
[
|
||||
169.45312499999997,
|
||||
-54.97761367069625
|
||||
],
|
||||
[
|
||||
-48.515625,
|
||||
-54.97761367069625
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"id": "background",
|
||||
"type": "background",
|
||||
"paint": {
|
||||
"background-color": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rectangles",
|
||||
"type": "fill",
|
||||
"source": "rectangles",
|
||||
"paint": {
|
||||
"fill-opacity": 0.3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -14,9 +14,9 @@
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
import "./commands";
|
||||
|
||||
import { mount } from 'cypress/react'
|
||||
import { mount } from "cypress/react";
|
||||
|
||||
// Augment the Cypress namespace to include type definitions for
|
||||
// your custom command.
|
||||
@@ -31,7 +31,7 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add('mount', mount)
|
||||
Cypress.Commands.add("mount", mount);
|
||||
|
||||
// Example use:
|
||||
// cy.mount(<MyComponent />)
|
||||
|
||||
@@ -1,56 +1,65 @@
|
||||
import eslint from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
||||
import reactRefreshPlugin from 'eslint-plugin-react-refresh';
|
||||
import eslint from "@eslint/js";
|
||||
import {defineConfig} from "eslint/config";
|
||||
import stylisticTs from "@stylistic/eslint-plugin";
|
||||
import tseslint from "typescript-eslint";
|
||||
import reactPlugin from "eslint-plugin-react";
|
||||
import reactHooksPlugin from "eslint-plugin-react-hooks";
|
||||
import reactRefreshPlugin from "eslint-plugin-react-refresh";
|
||||
|
||||
export default tseslint.config({
|
||||
export default defineConfig({
|
||||
extends: [
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
],
|
||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||
files: ["**/*.{js,jsx,ts,tsx}"],
|
||||
ignores: [
|
||||
"dist/**/*",
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2024,
|
||||
sourceType: 'module',
|
||||
sourceType: "module",
|
||||
globals: {
|
||||
global: 'readonly'
|
||||
global: "readonly"
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
react: { version: '18.2' }
|
||||
react: { version: "18.2" }
|
||||
},
|
||||
plugins: {
|
||||
'react': reactPlugin,
|
||||
'react-hooks': reactHooksPlugin,
|
||||
'react-refresh': reactRefreshPlugin
|
||||
"react": reactPlugin,
|
||||
"react-hooks": reactHooksPlugin,
|
||||
"react-refresh": reactRefreshPlugin,
|
||||
"@stylistic": stylisticTs
|
||||
},
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true }
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrors: 'all',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
argsIgnorePattern: '^_'
|
||||
varsIgnorePattern: "^_",
|
||||
caughtErrors: "all",
|
||||
caughtErrorsIgnorePattern: "^_",
|
||||
argsIgnorePattern: "^_"
|
||||
}
|
||||
],
|
||||
'no-unused-vars': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'no-undef': 'off',
|
||||
'indent': ['error', 2],
|
||||
'no-var': 'error',
|
||||
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
"no-unused-vars": "off",
|
||||
"react/prop-types": "off",
|
||||
"no-undef": "off",
|
||||
"indent": "off",
|
||||
"@stylistic/indent": ["error", 2],
|
||||
"semi": "off",
|
||||
"@stylistic/semi": ["error", "always"],
|
||||
"quotes": "off",
|
||||
"@stylistic/quotes": ["error", "double", { avoidEscape: true }],
|
||||
"no-var": "error",
|
||||
"@typescript-eslint/no-non-null-asserted-optional-chain": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-empty-object-type": "off",
|
||||
"@typescript-eslint/consistent-type-imports": ["error", { "fixStyle": "inline-type-imports" }],
|
||||
|
||||
},
|
||||
linterOptions: {
|
||||
@@ -58,4 +67,4 @@ export default tseslint.config({
|
||||
noInlineConfig: false
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
output: 'src/locales/$LOCALE/$NAMESPACE.json',
|
||||
locales: [ 'de', 'fr', 'he', 'it','ja', 'zh' ],
|
||||
output: "src/locales/$LOCALE/$NAMESPACE.json",
|
||||
locales: [ "de", "fr", "he", "it","ja", "zh" ],
|
||||
|
||||
// Because some keys are dynamically generated, i18next-parser can't detect them.
|
||||
// We add these keys manually, so we don't want to remove them.
|
||||
@@ -12,6 +12,6 @@ export default {
|
||||
|
||||
defaultValue: (_locale, _ns, _key) => {
|
||||
// The default value is a string that indicates that the string is not translated.
|
||||
return '__STRING_NOT_TRANSLATED__';
|
||||
return "__STRING_NOT_TRANSLATED__";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
6509
package-lock.json
generated
6509
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
81
package.json
81
package.json
@@ -12,6 +12,8 @@
|
||||
"i18n:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'",
|
||||
"lint": "eslint",
|
||||
"test": "cypress run",
|
||||
"test-unit": "vitest",
|
||||
"test-unit-ci": "vitest run --coverage --reporter=json",
|
||||
"cy:open": "cypress open",
|
||||
"lint-css": "stylelint \"src/styles/*.scss\"",
|
||||
"sort-styles": "jq 'sort_by(.id)' src/config/styles.json > tmp.json && mv tmp.json src/config/styles.json"
|
||||
@@ -24,26 +26,28 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/maplibre/maputnik#readme",
|
||||
"dependencies": {
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lint": "^6.9.2",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@codemirror/view": "^6.38.8",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@mapbox/mapbox-gl-rtl-text": "^0.3.0",
|
||||
"@maplibre/maplibre-gl-geocoder": "^1.9.0",
|
||||
"@maplibre/maplibre-gl-inspect": "^1.7.1",
|
||||
"@maplibre/maplibre-gl-style-spec": "^23.3.0",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"@prantlf/jsonlint": "^16.0.0",
|
||||
"@maplibre/maplibre-gl-geocoder": "^1.9.1",
|
||||
"@maplibre/maplibre-gl-inspect": "^1.8.1",
|
||||
"@maplibre/maplibre-gl-style-spec": "^24.3.1",
|
||||
"array-move": "^4.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.5.1",
|
||||
"codemirror": "^5.65.20",
|
||||
"color": "^5.0.0",
|
||||
"codemirror": "^6.0.2",
|
||||
"color": "^5.0.3",
|
||||
"detect-browser": "^5.3.0",
|
||||
"downshift": "^9.0.10",
|
||||
"events": "^3.3.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^25.5.2",
|
||||
"i18next": "^25.6.3",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"json-stringify-pretty-compact": "^4.0.0",
|
||||
@@ -55,23 +59,22 @@
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"maplibre-gl": "^5.7.1",
|
||||
"maplibre-gl": "^5.13.0",
|
||||
"maputnik-design": "github:maputnik/design#172b06c",
|
||||
"ol": "^10.6.1",
|
||||
"ol-mapbox-style": "^13.1.0",
|
||||
"ol": "^10.7.0",
|
||||
"ol-mapbox-style": "^13.1.1",
|
||||
"pmtiles": "^4.3.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react": "^19.2.0",
|
||||
"react-accessible-accordion": "^5.0.1",
|
||||
"react-aria-menubutton": "^7.0.3",
|
||||
"react-aria-modal": "^5.0.2",
|
||||
"react-collapse": "^5.1.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-file-reader-input": "^2.0.0",
|
||||
"react-i18next": "^15.7.3",
|
||||
"react-icon-base": "^2.1.2",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-i18next": "^16.3.5",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"slugify": "^1.6.6",
|
||||
"string-hash": "^1.1.3",
|
||||
@@ -95,12 +98,13 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/code-coverage": "^3.14.6",
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@cypress/code-coverage": "^3.14.7",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||
"@rollup/plugin-replace": "^6.0.2",
|
||||
"@shellygo/cypress-test-utils": "^6.0.1",
|
||||
"@types/codemirror": "^5.60.16",
|
||||
"@shellygo/cypress-test-utils": "^6.0.4",
|
||||
"@stylistic/eslint-plugin": "^5.6.1",
|
||||
"@types/codemirror": "^5.60.17",
|
||||
"@types/color": "^4.2.0",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
@@ -113,38 +117,37 @@
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/lodash.throttle": "^4.1.9",
|
||||
"@types/randomcolor": "^0.5.9",
|
||||
"@types/react": "^18.2.67",
|
||||
"@types/react": "^19.2.7",
|
||||
"@types/react-aria-menubutton": "^6.2.14",
|
||||
"@types/react-aria-modal": "^5.0.0",
|
||||
"@types/react-collapse": "^5.0.4",
|
||||
"@types/react-color": "^3.0.13",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@types/react-file-reader-input": "^2.0.4",
|
||||
"@types/react-icon-base": "^2.1.6",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/string-hash": "^1.1.3",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/wicg-file-system-access": "^2023.10.6",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"@types/wicg-file-system-access": "^2023.10.7",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"@vitest/coverage-v8": "^4.0.14",
|
||||
"cors": "^2.8.5",
|
||||
"cypress": "^15.1.0",
|
||||
"cypress": "^15.7.0",
|
||||
"cypress-plugin-tab": "^1.0.5",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.23",
|
||||
"i18next-parser": "^9.3.0",
|
||||
"istanbul": "^0.4.5",
|
||||
"istanbul-lib-coverage": "^3.2.2",
|
||||
"postcss": "^8.5.6",
|
||||
"react-hot-loader": "^4.13.1",
|
||||
"sass": "^1.92.1",
|
||||
"stylelint": "^16.24.0",
|
||||
"stylelint-config-recommended-scss": "^16.0.0",
|
||||
"sass": "^1.94.2",
|
||||
"stylelint": "^16.26.1",
|
||||
"stylelint-config-recommended-scss": "^16.0.2",
|
||||
"stylelint-scss": "^6.12.1",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.43.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.48.0",
|
||||
"uuid": "^13.0.0",
|
||||
"vite": "^7.1.5",
|
||||
"vite-plugin-istanbul": "^7.1.0"
|
||||
"vite": "^7.2.4",
|
||||
"vite-plugin-istanbul": "^7.2.1",
|
||||
"vitest": "^4.0.14"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,43 @@
|
||||
import React from 'react'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import clamp from 'lodash.clamp'
|
||||
import buffer from 'buffer'
|
||||
import get from 'lodash.get'
|
||||
import {unset} from 'lodash'
|
||||
import {arrayMoveMutable} from 'array-move'
|
||||
import React from "react";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import clamp from "lodash.clamp";
|
||||
import buffer from "buffer";
|
||||
import get from "lodash.get";
|
||||
import {unset} from "lodash";
|
||||
import {arrayMoveMutable} from "array-move";
|
||||
import hash from "string-hash";
|
||||
import { PMTiles } from "pmtiles";
|
||||
import {Map, LayerSpecification, StyleSpecification, ValidationError, SourceSpecification} from 'maplibre-gl'
|
||||
import {latest, validateStyleMin} from '@maplibre/maplibre-gl-style-spec'
|
||||
import {type Map, type LayerSpecification, type StyleSpecification, type ValidationError, type SourceSpecification} from "maplibre-gl";
|
||||
import {validateStyleMin} from "@maplibre/maplibre-gl-style-spec";
|
||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
||||
|
||||
import MapMaplibreGl from './MapMaplibreGl'
|
||||
import MapOpenLayers from './MapOpenLayers'
|
||||
import LayerList from './LayerList'
|
||||
import LayerEditor from './LayerEditor'
|
||||
import AppToolbar, { MapState } from './AppToolbar'
|
||||
import AppLayout from './AppLayout'
|
||||
import MessagePanel from './AppMessagePanel'
|
||||
import MapMaplibreGl from "./MapMaplibreGl";
|
||||
import MapOpenLayers from "./MapOpenLayers";
|
||||
import CodeEditor from "./CodeEditor";
|
||||
import LayerList from "./LayerList";
|
||||
import LayerEditor from "./LayerEditor";
|
||||
import AppToolbar, { type MapState } from "./AppToolbar";
|
||||
import AppLayout from "./AppLayout";
|
||||
import MessagePanel from "./AppMessagePanel";
|
||||
|
||||
import ModalSettings from './ModalSettings'
|
||||
import ModalExport from './ModalExport'
|
||||
import ModalSources from './ModalSources'
|
||||
import ModalOpen from './ModalOpen'
|
||||
import ModalShortcuts from './ModalShortcuts'
|
||||
import ModalDebug from './ModalDebug'
|
||||
import ModalSettings from "./modals/ModalSettings";
|
||||
import ModalExport from "./modals/ModalExport";
|
||||
import ModalSources from "./modals/ModalSources";
|
||||
import ModalOpen from "./modals/ModalOpen";
|
||||
import ModalShortcuts from "./modals/ModalShortcuts";
|
||||
import ModalDebug from "./modals/ModalDebug";
|
||||
import ModalGlobalState from "./modals/ModalGlobalState";
|
||||
|
||||
import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata'
|
||||
import style from '../libs/style'
|
||||
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
||||
import { createStyleStore, type IStyleStore } from '../libs/store/style-store-factory'
|
||||
import { RevisionStore } from '../libs/revisions'
|
||||
import LayerWatcher from '../libs/layerwatcher'
|
||||
import tokens from '../config/tokens.json'
|
||||
import isEqual from 'lodash.isequal'
|
||||
import { MapOptions } from 'maplibre-gl';
|
||||
import { OnStyleChangedOpts, StyleSpecificationWithId } from '../libs/definitions'
|
||||
import {downloadGlyphsMetadata, downloadSpriteMetadata} from "../libs/metadata";
|
||||
import style from "../libs/style";
|
||||
import { undoMessages, redoMessages } from "../libs/diffmessage";
|
||||
import { createStyleStore, type IStyleStore } from "../libs/store/style-store-factory";
|
||||
import { RevisionStore } from "../libs/revisions";
|
||||
import LayerWatcher from "../libs/layerwatcher";
|
||||
import tokens from "../config/tokens.json";
|
||||
import isEqual from "lodash.isequal";
|
||||
import { type MapOptions } from "maplibre-gl";
|
||||
import { type MappedError, type OnStyleChangedOpts, type StyleSpecificationWithId } from "../libs/definitions";
|
||||
|
||||
// Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed.
|
||||
window.Buffer = buffer.Buffer;
|
||||
@@ -45,21 +48,21 @@ function setFetchAccessToken(url: string, mapStyle: StyleSpecification) {
|
||||
const matchesThunderforest = url.match(/\.thunderforest\.com/);
|
||||
const matchesLocationIQ = url.match(/\.locationiq\.com/);
|
||||
if (matchesTilehosting || matchesMaptiler) {
|
||||
const accessToken = style.getAccessToken("openmaptiles", mapStyle, {allowFallback: true})
|
||||
const accessToken = style.getAccessToken("openmaptiles", mapStyle, {allowFallback: true});
|
||||
if (accessToken) {
|
||||
return url.replace('{key}', accessToken)
|
||||
return url.replace("{key}", accessToken);
|
||||
}
|
||||
}
|
||||
else if (matchesThunderforest) {
|
||||
const accessToken = style.getAccessToken("thunderforest", mapStyle, {allowFallback: true})
|
||||
const accessToken = style.getAccessToken("thunderforest", mapStyle, {allowFallback: true});
|
||||
if (accessToken) {
|
||||
return url.replace('{key}', accessToken)
|
||||
return url.replace("{key}", accessToken);
|
||||
}
|
||||
}
|
||||
else if (matchesLocationIQ) {
|
||||
const accessToken = style.getAccessToken("locationiq", mapStyle, {allowFallback: true})
|
||||
const accessToken = style.getAccessToken("locationiq", mapStyle, {allowFallback: true});
|
||||
if (accessToken) {
|
||||
return url.replace('{key}', accessToken)
|
||||
return url.replace("{key}", accessToken);
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -77,23 +80,11 @@ function updateRootSpec(spec: any, fieldName: string, newValues: any) {
|
||||
values: newValues
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type MappedErrors = {
|
||||
message: string
|
||||
parsed?: {
|
||||
type: string
|
||||
data: {
|
||||
index: number
|
||||
key: string
|
||||
message: string
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
type AppState = {
|
||||
errors: MappedErrors[],
|
||||
errors: MappedError[],
|
||||
infos: string[],
|
||||
mapStyle: StyleSpecificationWithId,
|
||||
dirtyMapStyle?: StyleSpecification,
|
||||
@@ -125,9 +116,11 @@ type AppState = {
|
||||
shortcuts: boolean
|
||||
export: boolean
|
||||
debug: boolean
|
||||
globalState: boolean
|
||||
codeEditor: boolean
|
||||
}
|
||||
fileHandle: FileSystemFileHandle | null
|
||||
}
|
||||
};
|
||||
|
||||
export default class App extends React.Component<any, AppState> {
|
||||
revisionStore: RevisionStore;
|
||||
@@ -135,7 +128,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
layerWatcher: LayerWatcher;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
super(props);
|
||||
|
||||
this.revisionStore = new RevisionStore();
|
||||
this.configureKeyboardShortcuts();
|
||||
@@ -163,6 +156,8 @@ export default class App extends React.Component<any, AppState> {
|
||||
shortcuts: false,
|
||||
export: false,
|
||||
debug: false,
|
||||
globalState: false,
|
||||
codeEditor: false
|
||||
},
|
||||
maplibreGlDebugOptions: {
|
||||
showTileBoundaries: false,
|
||||
@@ -173,11 +168,11 @@ export default class App extends React.Component<any, AppState> {
|
||||
debugToolbox: false,
|
||||
},
|
||||
fileHandle: null,
|
||||
}
|
||||
};
|
||||
|
||||
this.layerWatcher = new LayerWatcher({
|
||||
onVectorLayersChange: v => this.setState({ vectorLayers: v })
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
configureKeyboardShortcuts = () => {
|
||||
@@ -212,6 +207,12 @@ export default class App extends React.Component<any, AppState> {
|
||||
this.toggleModal("settings");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "g",
|
||||
handler: () => {
|
||||
this.toggleModal("globalState");
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "i",
|
||||
handler: () => {
|
||||
@@ -232,7 +233,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
this.toggleModal("debug");
|
||||
}
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
document.body.addEventListener("keyup", (e) => {
|
||||
if(e.key === "Escape") {
|
||||
@@ -241,19 +242,19 @@ export default class App extends React.Component<any, AppState> {
|
||||
}
|
||||
else if(this.state.isOpen.shortcuts || document.activeElement === document.body) {
|
||||
const shortcut = shortcuts.find((shortcut) => {
|
||||
return (shortcut.key === e.key)
|
||||
})
|
||||
return (shortcut.key === e.key);
|
||||
});
|
||||
|
||||
if(shortcut) {
|
||||
this.setModal("shortcuts", false);
|
||||
shortcut.handler();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleKeyPress = (e: KeyboardEvent) => {
|
||||
if(navigator.platform.toUpperCase().indexOf('MAC') >= 0) {
|
||||
if(navigator.platform.toUpperCase().indexOf("MAC") >= 0) {
|
||||
if(e.metaKey && e.shiftKey && e.keyCode === 90) {
|
||||
e.preventDefault();
|
||||
this.onRedo();
|
||||
@@ -273,7 +274,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
this.onRedo();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async componentDidMount() {
|
||||
this.styleStore = await createStyleStore((mapStyle, opts) => this.onStyleChanged(mapStyle, opts));
|
||||
@@ -285,33 +286,33 @@ export default class App extends React.Component<any, AppState> {
|
||||
}
|
||||
|
||||
saveStyle(snapshotStyle: StyleSpecificationWithId) {
|
||||
this.styleStore?.save(snapshotStyle)
|
||||
this.styleStore?.save(snapshotStyle);
|
||||
}
|
||||
|
||||
updateFonts(urlTemplate: string) {
|
||||
const metadata: {[key: string]: string} = this.state.mapStyle.metadata || {} as any
|
||||
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
||||
const metadata: {[key: string]: string} = this.state.mapStyle.metadata || {} as any;
|
||||
const accessToken = metadata["maputnik:openmaptiles_access_token"] || tokens.openmaptiles;
|
||||
|
||||
const glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
|
||||
downloadGlyphsMetadata(glyphUrl, fonts => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
|
||||
})
|
||||
const glyphUrl = (typeof urlTemplate === "string")? urlTemplate.replace("{key}", accessToken): urlTemplate;
|
||||
downloadGlyphsMetadata(glyphUrl).then(fonts => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, "glyphs", fonts)});
|
||||
});
|
||||
}
|
||||
|
||||
updateIcons(baseUrl: string) {
|
||||
downloadSpriteMetadata(baseUrl, icons => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)})
|
||||
})
|
||||
downloadSpriteMetadata(baseUrl).then(icons => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, "sprite", icons)});
|
||||
});
|
||||
}
|
||||
|
||||
onChangeMetadataProperty = (property: string, value: any) => {
|
||||
// If we're changing renderer reset the map state.
|
||||
if (
|
||||
property === 'maputnik:renderer' &&
|
||||
value !== get(this.state.mapStyle, ['metadata', 'maputnik:renderer'], 'mlgljs')
|
||||
property === "maputnik:renderer" &&
|
||||
value !== get(this.state.mapStyle, ["metadata", "maputnik:renderer"], "mlgljs")
|
||||
) {
|
||||
this.setState({
|
||||
mapState: 'map'
|
||||
mapState: "map"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -321,10 +322,10 @@ export default class App extends React.Component<any, AppState> {
|
||||
...(this.state.mapStyle as any).metadata,
|
||||
[property]: value
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.onStyleChanged(changedStyle)
|
||||
}
|
||||
this.onStyleChanged(changedStyle);
|
||||
};
|
||||
|
||||
onStyleChanged = (newStyle: StyleSpecificationWithId, opts: OnStyleChangedOpts={}): void => {
|
||||
opts = {
|
||||
@@ -337,16 +338,16 @@ export default class App extends React.Component<any, AppState> {
|
||||
// For the style object, find the urls that has "{key}" and insert the correct API keys
|
||||
// Without this, going from e.g. MapTiler to OpenLayers and back will lose the maptlier key.
|
||||
|
||||
if (newStyle.glyphs && typeof newStyle.glyphs === 'string') {
|
||||
if (newStyle.glyphs && typeof newStyle.glyphs === "string") {
|
||||
newStyle.glyphs = setFetchAccessToken(newStyle.glyphs, newStyle);
|
||||
}
|
||||
|
||||
if (newStyle.sprite && typeof newStyle.sprite === 'string') {
|
||||
if (newStyle.sprite && typeof newStyle.sprite === "string") {
|
||||
newStyle.sprite = setFetchAccessToken(newStyle.sprite, newStyle);
|
||||
}
|
||||
|
||||
for (const [_sourceId, source] of Object.entries(newStyle.sources)) {
|
||||
if (source && 'url' in source && typeof source.url === 'string') {
|
||||
if (source && "url" in source && typeof source.url === "string") {
|
||||
source.url = setFetchAccessToken(source.url, newStyle);
|
||||
}
|
||||
}
|
||||
@@ -373,7 +374,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
});
|
||||
}
|
||||
|
||||
const mappedErrors = layerErrors.concat(errors).map(error => {
|
||||
const mappedErrors: MappedError[] = layerErrors.concat(errors).map(error => {
|
||||
// Special case: Duplicate layer id
|
||||
const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/);
|
||||
if (dupMatch) {
|
||||
@@ -388,7 +389,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
message,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Special case: Invalid source
|
||||
@@ -405,7 +406,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
message,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
|
||||
@@ -422,7 +423,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
else {
|
||||
return {
|
||||
@@ -452,10 +453,10 @@ export default class App extends React.Component<any, AppState> {
|
||||
}
|
||||
|
||||
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
||||
this.updateFonts(newStyle.glyphs as string)
|
||||
this.updateFonts(newStyle.glyphs as string);
|
||||
}
|
||||
if(newStyle.sprite !== this.state.mapStyle.sprite) {
|
||||
this.updateIcons(newStyle.sprite as string)
|
||||
this.updateIcons(newStyle.sprite as string);
|
||||
}
|
||||
|
||||
if (opts.addRevision) {
|
||||
@@ -472,28 +473,28 @@ export default class App extends React.Component<any, AppState> {
|
||||
}, () => {
|
||||
this.fetchSources();
|
||||
this.setStateInUrl();
|
||||
})
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
onUndo = () => {
|
||||
const activeStyle = this.revisionStore.undo()
|
||||
const activeStyle = this.revisionStore.undo();
|
||||
|
||||
const messages = undoMessages(this.state.mapStyle, activeStyle)
|
||||
const messages = undoMessages(this.state.mapStyle, activeStyle);
|
||||
this.onStyleChanged(activeStyle, {addRevision: false});
|
||||
this.setState({
|
||||
infos: messages,
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onRedo = () => {
|
||||
const activeStyle = this.revisionStore.redo()
|
||||
const messages = redoMessages(this.state.mapStyle, activeStyle)
|
||||
const activeStyle = this.revisionStore.redo();
|
||||
const messages = redoMessages(this.state.mapStyle, activeStyle);
|
||||
this.onStyleChanged(activeStyle, {addRevision: false});
|
||||
this.setState({
|
||||
infos: messages,
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMoveLayer = (move: {oldIndex: number; newIndex: number}) => {
|
||||
let { oldIndex, newIndex } = move;
|
||||
@@ -511,97 +512,97 @@ export default class App extends React.Component<any, AppState> {
|
||||
layers = layers.slice(0);
|
||||
arrayMoveMutable(layers, oldIndex, newIndex);
|
||||
this.onLayersChange(layers);
|
||||
}
|
||||
};
|
||||
|
||||
onLayersChange = (changedLayers: LayerSpecification[]) => {
|
||||
const changedStyle = {
|
||||
...this.state.mapStyle,
|
||||
layers: changedLayers
|
||||
}
|
||||
this.onStyleChanged(changedStyle)
|
||||
}
|
||||
};
|
||||
this.onStyleChanged(changedStyle);
|
||||
};
|
||||
|
||||
onLayerDestroy = (index: number) => {
|
||||
const layers = this.state.mapStyle.layers;
|
||||
const remainingLayers = layers.slice(0);
|
||||
remainingLayers.splice(index, 1);
|
||||
this.onLayersChange(remainingLayers);
|
||||
}
|
||||
};
|
||||
|
||||
onLayerCopy = (index: number) => {
|
||||
const layers = this.state.mapStyle.layers;
|
||||
const changedLayers = layers.slice(0)
|
||||
const changedLayers = layers.slice(0);
|
||||
|
||||
const clonedLayer = cloneDeep(changedLayers[index])
|
||||
clonedLayer.id = clonedLayer.id + "-copy"
|
||||
changedLayers.splice(index, 0, clonedLayer)
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
const clonedLayer = cloneDeep(changedLayers[index]);
|
||||
clonedLayer.id = clonedLayer.id + "-copy";
|
||||
changedLayers.splice(index, 0, clonedLayer);
|
||||
this.onLayersChange(changedLayers);
|
||||
};
|
||||
|
||||
onLayerVisibilityToggle = (index: number) => {
|
||||
const layers = this.state.mapStyle.layers;
|
||||
const changedLayers = layers.slice(0)
|
||||
const changedLayers = layers.slice(0);
|
||||
|
||||
const layer = { ...changedLayers[index] }
|
||||
const changedLayout = 'layout' in layer ? {...layer.layout} : {}
|
||||
changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none'
|
||||
const layer = { ...changedLayers[index] };
|
||||
const changedLayout = "layout" in layer ? {...layer.layout} : {};
|
||||
changedLayout.visibility = changedLayout.visibility === "none" ? "visible" : "none";
|
||||
|
||||
layer.layout = changedLayout
|
||||
changedLayers[index] = layer
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
layer.layout = changedLayout;
|
||||
changedLayers[index] = layer;
|
||||
this.onLayersChange(changedLayers);
|
||||
};
|
||||
|
||||
|
||||
onLayerIdChange = (index: number, _oldId: string, newId: string) => {
|
||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
||||
const changedLayers = this.state.mapStyle.layers.slice(0);
|
||||
changedLayers[index] = {
|
||||
...changedLayers[index],
|
||||
id: newId
|
||||
}
|
||||
};
|
||||
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
this.onLayersChange(changedLayers);
|
||||
};
|
||||
|
||||
onLayerChanged = (index: number, layer: LayerSpecification) => {
|
||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
||||
changedLayers[index] = layer
|
||||
const changedLayers = this.state.mapStyle.layers.slice(0);
|
||||
changedLayers[index] = layer;
|
||||
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
this.onLayersChange(changedLayers);
|
||||
};
|
||||
|
||||
setMapState = (newState: MapState) => {
|
||||
this.setState({
|
||||
mapState: newState
|
||||
}, this.setStateInUrl);
|
||||
}
|
||||
};
|
||||
|
||||
setDefaultValues = (styleObj: StyleSpecificationWithId) => {
|
||||
const metadata: {[key: string]: string} = styleObj.metadata || {} as any
|
||||
if(metadata['maputnik:renderer'] === undefined) {
|
||||
const metadata: {[key: string]: string} = styleObj.metadata || {} as any;
|
||||
if(metadata["maputnik:renderer"] === undefined) {
|
||||
const changedStyle = {
|
||||
...styleObj,
|
||||
metadata: {
|
||||
...styleObj.metadata as any,
|
||||
'maputnik:renderer': 'mlgljs'
|
||||
"maputnik:renderer": "mlgljs"
|
||||
}
|
||||
}
|
||||
return changedStyle
|
||||
};
|
||||
return changedStyle;
|
||||
} else {
|
||||
return styleObj
|
||||
return styleObj;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
openStyle = (styleObj: StyleSpecificationWithId, fileHandle: FileSystemFileHandle | null) => {
|
||||
this.setState({fileHandle: fileHandle});
|
||||
styleObj = this.setDefaultValues(styleObj)
|
||||
this.onStyleChanged(styleObj)
|
||||
}
|
||||
styleObj = this.setDefaultValues(styleObj);
|
||||
this.onStyleChanged(styleObj);
|
||||
};
|
||||
|
||||
async fetchSources() {
|
||||
const sourceList: {[key: string]: SourceSpecification & {layers: string[]}} = {};
|
||||
for(const key of Object.keys(this.state.mapStyle.sources)) {
|
||||
const source = this.state.mapStyle.sources[key];
|
||||
if(source.type !== "vector" || !('url' in source)) {
|
||||
if(source.type !== "vector" || !("url" in source)) {
|
||||
sourceList[key] = this.state.sources[key] || {...this.state.mapStyle.sources[key]};
|
||||
if (sourceList[key].layers === undefined) {
|
||||
sourceList[key].layers = [];
|
||||
@@ -615,7 +616,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
let url = source.url;
|
||||
|
||||
try {
|
||||
url = setFetchAccessToken(url!, this.state.mapStyle)
|
||||
url = setFetchAccessToken(url!, this.state.mapStyle);
|
||||
} catch(err) {
|
||||
console.warn("Failed to setFetchAccessToken: ", err);
|
||||
}
|
||||
@@ -626,7 +627,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
}
|
||||
|
||||
for(const layer of json.vector_layers) {
|
||||
sourceList[key].layers.push(layer.id)
|
||||
sourceList[key].layers.push(layer.id);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -635,7 +636,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
const json = await (new PMTiles(url!.substring(10))).getTileJson("");
|
||||
setVectorLayers(json);
|
||||
} else {
|
||||
const response = await fetch(url!, { mode: 'cors' });
|
||||
const response = await fetch(url!, { mode: "cors" });
|
||||
const json = await response.json();
|
||||
setVectorLayers(json);
|
||||
}
|
||||
@@ -649,13 +650,13 @@ export default class App extends React.Component<any, AppState> {
|
||||
console.debug("Setting sources", sourceList);
|
||||
this.setState({
|
||||
sources: sourceList
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_getRenderer () {
|
||||
const metadata: {[key:string]: string} = this.state.mapStyle.metadata || {} as any;
|
||||
return metadata['maputnik:renderer'] || 'mlgljs';
|
||||
return metadata["maputnik:renderer"] || "mlgljs";
|
||||
}
|
||||
|
||||
onMapChange = (mapView: {
|
||||
@@ -668,7 +669,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
this.setState({
|
||||
mapView,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
mapRenderer() {
|
||||
const {mapStyle, dirtyMapStyle} = this.state;
|
||||
@@ -681,23 +682,23 @@ export default class App extends React.Component<any, AppState> {
|
||||
});
|
||||
},
|
||||
onDataChange: (e: {map: Map}) => {
|
||||
this.layerWatcher.analyzeMap(e.map)
|
||||
this.layerWatcher.analyzeMap(e.map);
|
||||
this.fetchSources();
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const renderer = this._getRenderer();
|
||||
|
||||
let mapElement;
|
||||
|
||||
// Check if OL code has been loaded?
|
||||
if(renderer === 'ol') {
|
||||
if(renderer === "ol") {
|
||||
mapElement = <MapOpenLayers
|
||||
{...mapProps}
|
||||
onChange={this.onMapChange}
|
||||
debugToolbox={this.state.openlayersDebugOptions.debugToolbox}
|
||||
onLayerSelect={this.onLayerSelect}
|
||||
/>
|
||||
onLayerSelect={(layerId) => this.onLayerSelect(+layerId)}
|
||||
/>;
|
||||
} else {
|
||||
|
||||
mapElement = <MapMaplibreGl {...mapProps}
|
||||
@@ -705,7 +706,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
options={this.state.maplibreGlDebugOptions}
|
||||
inspectModeEnabled={this.state.mapState === "inspect"}
|
||||
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
|
||||
onLayerSelect={this.onLayerSelect} />
|
||||
onLayerSelect={this.onLayerSelect} />;
|
||||
}
|
||||
|
||||
let filterName;
|
||||
@@ -719,7 +720,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
|
||||
return <div style={elementStyle} className="maputnik-map__container" data-wd-key="maplibre:container">
|
||||
{mapElement}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
setStateInUrl = () => {
|
||||
@@ -748,7 +749,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
}
|
||||
|
||||
history.replaceState({selectedLayerIndex}, "Maputnik", url.href);
|
||||
}
|
||||
};
|
||||
|
||||
getInitialStateFromUrl = (mapStyle: StyleSpecification) => {
|
||||
const url = new URL(location.href);
|
||||
@@ -801,14 +802,14 @@ export default class App extends React.Component<any, AppState> {
|
||||
console.warn(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onLayerSelect = (index: number) => {
|
||||
this.setState({
|
||||
selectedLayerIndex: index,
|
||||
selectedLayerOriginalId: this.state.mapStyle.layers[index].id,
|
||||
}, this.setStateInUrl);
|
||||
}
|
||||
};
|
||||
|
||||
setModal(modalName: keyof AppState["isOpen"], value: boolean) {
|
||||
this.setState({
|
||||
@@ -816,7 +817,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
...this.state.isOpen,
|
||||
[modalName]: value
|
||||
}
|
||||
}, this.setStateInUrl)
|
||||
}, this.setStateInUrl);
|
||||
}
|
||||
|
||||
toggleModal(modalName: keyof AppState["isOpen"]) {
|
||||
@@ -825,7 +826,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
|
||||
onSetFileHandle = (fileHandle: FileSystemFileHandle | null) => {
|
||||
this.setState({ fileHandle });
|
||||
}
|
||||
};
|
||||
|
||||
onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => {
|
||||
this.setState({
|
||||
@@ -834,7 +835,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
[key]: value,
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onChangeMaplibreGlDebug = (key: keyof AppState["maplibreGlDebugOptions"], value: any) => {
|
||||
this.setState({
|
||||
@@ -843,11 +844,11 @@ export default class App extends React.Component<any, AppState> {
|
||||
[key]: value,
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const layers = this.state.mapStyle.layers || []
|
||||
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : undefined
|
||||
const layers = this.state.mapStyle.layers || [];
|
||||
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : undefined;
|
||||
|
||||
const toolbar = <AppToolbar
|
||||
renderer={this._getRenderer()}
|
||||
@@ -858,8 +859,14 @@ export default class App extends React.Component<any, AppState> {
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
onStyleOpen={this.onStyleChanged}
|
||||
onSetMapState={this.setMapState}
|
||||
onToggleModal={this.toggleModal.bind(this)}
|
||||
/>
|
||||
onToggleModal={(modal: keyof AppState["isOpen"]) => this.toggleModal(modal)}
|
||||
/>;
|
||||
|
||||
const codeEditor = this.state.isOpen.codeEditor ? <CodeEditor
|
||||
value={this.state.mapStyle}
|
||||
onChange={(style) => this.onStyleChanged(style)}
|
||||
onClose={() => this.setModal("codeEditor", false)}
|
||||
/> : undefined;
|
||||
|
||||
const layerList = <LayerList
|
||||
onMoveLayer={this.onMoveLayer}
|
||||
@@ -872,7 +879,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
layers={layers}
|
||||
sources={this.state.sources}
|
||||
errors={this.state.errors}
|
||||
/>
|
||||
/>;
|
||||
|
||||
const layerEditor = selectedLayer ? <LayerEditor
|
||||
key={this.state.selectedLayerOriginalId}
|
||||
@@ -890,7 +897,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
|
||||
onLayerIdChange={this.onLayerIdChange}
|
||||
errors={this.state.errors}
|
||||
/> : undefined
|
||||
/> : undefined;
|
||||
|
||||
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
|
||||
currentLayer={selectedLayer}
|
||||
@@ -899,7 +906,7 @@ export default class App extends React.Component<any, AppState> {
|
||||
mapStyle={this.state.mapStyle}
|
||||
errors={this.state.errors}
|
||||
infos={this.state.infos}
|
||||
/> : undefined
|
||||
/> : undefined;
|
||||
|
||||
|
||||
const modals = <div>
|
||||
@@ -910,49 +917,56 @@ export default class App extends React.Component<any, AppState> {
|
||||
onChangeMaplibreGlDebug={this.onChangeMaplibreGlDebug}
|
||||
onChangeOpenlayersDebug={this.onChangeOpenlayersDebug}
|
||||
isOpen={this.state.isOpen.debug}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'debug')}
|
||||
onOpenToggle={() => this.toggleModal("debug")}
|
||||
mapView={this.state.mapView}
|
||||
/>
|
||||
<ModalShortcuts
|
||||
isOpen={this.state.isOpen.shortcuts}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
|
||||
onOpenToggle={() => this.toggleModal("shortcuts")}
|
||||
/>
|
||||
<ModalSettings
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
onChangeMetadataProperty={this.onChangeMetadataProperty}
|
||||
isOpen={this.state.isOpen.settings}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'settings')}
|
||||
onOpenToggle={() => this.toggleModal("settings")}
|
||||
/>
|
||||
<ModalExport
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
isOpen={this.state.isOpen.export}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'export')}
|
||||
onOpenToggle={() => this.toggleModal("export")}
|
||||
fileHandle={this.state.fileHandle}
|
||||
onSetFileHandle={this.onSetFileHandle}
|
||||
/>
|
||||
<ModalOpen
|
||||
isOpen={this.state.isOpen.open}
|
||||
onStyleOpen={this.openStyle}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'open')}
|
||||
onOpenToggle={() => this.toggleModal("open")}
|
||||
fileHandle={this.state.fileHandle}
|
||||
/>
|
||||
<ModalSources
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
isOpen={this.state.isOpen.sources}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'sources')}
|
||||
onOpenToggle={() => this.toggleModal("sources")}
|
||||
/>
|
||||
</div>
|
||||
<ModalGlobalState
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged}
|
||||
isOpen={this.state.isOpen.globalState}
|
||||
onOpenToggle={() => this.toggleModal("globalState")}
|
||||
/>
|
||||
</div>;
|
||||
|
||||
return <AppLayout
|
||||
toolbar={toolbar}
|
||||
layerList={layerList}
|
||||
layerEditor={layerEditor}
|
||||
codeEditor={codeEditor}
|
||||
map={this.mapRenderer()}
|
||||
bottom={bottomPanel}
|
||||
modals={modals}
|
||||
/>
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React from 'react'
|
||||
import ScrollContainer from './ScrollContainer'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import { IconContext } from 'react-icons';
|
||||
import React from "react";
|
||||
import ScrollContainer from "./ScrollContainer";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
import { IconContext } from "react-icons";
|
||||
|
||||
type AppLayoutInternalProps = {
|
||||
toolbar: React.ReactElement
|
||||
layerList: React.ReactElement
|
||||
layerEditor?: React.ReactElement
|
||||
codeEditor?: React.ReactElement
|
||||
map: React.ReactElement
|
||||
bottom?: React.ReactElement
|
||||
modals?: React.ReactNode
|
||||
@@ -17,18 +18,26 @@ class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
|
||||
render() {
|
||||
document.body.dir = this.props.i18n.dir();
|
||||
|
||||
return <IconContext.Provider value={{size: '14px'}}>
|
||||
return <IconContext.Provider value={{size: "14px"}}>
|
||||
<div className="maputnik-layout">
|
||||
{this.props.toolbar}
|
||||
<div className="maputnik-layout-main">
|
||||
<div className="maputnik-layout-list">
|
||||
{this.props.layerList}
|
||||
</div>
|
||||
<div className="maputnik-layout-drawer">
|
||||
{this.props.codeEditor && <div className="maputnik-layout-code-editor">
|
||||
<ScrollContainer>
|
||||
{this.props.layerEditor}
|
||||
{this.props.codeEditor}
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
}
|
||||
{!this.props.codeEditor && <>
|
||||
<div className="maputnik-layout-list">
|
||||
{this.props.layerList}
|
||||
</div>
|
||||
<div className="maputnik-layout-drawer">
|
||||
<ScrollContainer>
|
||||
{this.props.layerEditor}
|
||||
</ScrollContainer>
|
||||
</div>
|
||||
</>}
|
||||
{this.props.map}
|
||||
</div>
|
||||
{this.props.bottom && <div className="maputnik-layout-bottom">
|
||||
@@ -37,7 +46,7 @@ class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
|
||||
}
|
||||
{this.props.modals}
|
||||
</div>
|
||||
</IconContext.Provider>
|
||||
</IconContext.Provider>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react'
|
||||
import {formatLayerId} from '../libs/format';
|
||||
import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
|
||||
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||
import React from "react";
|
||||
import {formatLayerId} from "../libs/format";
|
||||
import {type LayerSpecification, type StyleSpecification} from "maplibre-gl";
|
||||
import { Trans, type WithTranslation, withTranslation } from "react-i18next";
|
||||
import { type MappedError } from "../libs/definitions";
|
||||
|
||||
type AppMessagePanelInternalProps = {
|
||||
errors?: unknown[]
|
||||
errors?: MappedError[]
|
||||
infos?: string[]
|
||||
mapStyle?: StyleSpecification
|
||||
onLayerSelect?(...args: unknown[]): unknown
|
||||
onLayerSelect?(index: number): void;
|
||||
currentLayer?: LayerSpecification
|
||||
selectedLayerIndex?: number
|
||||
} & WithTranslation;
|
||||
@@ -15,11 +16,11 @@ type AppMessagePanelInternalProps = {
|
||||
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
|
||||
static defaultProps = {
|
||||
onLayerSelect: () => {},
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {t, selectedLayerIndex} = this.props;
|
||||
const errors = this.props.errors?.map((error: any, idx) => {
|
||||
const errors = this.props.errors?.map((error, idx) => {
|
||||
let content;
|
||||
if (error.parsed && error.parsed.type === "layer") {
|
||||
const {parsed} = error;
|
||||
@@ -48,17 +49,17 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
|
||||
}
|
||||
return <p key={"error-"+idx} className="maputnik-message-panel-error">
|
||||
{content}
|
||||
</p>
|
||||
})
|
||||
</p>;
|
||||
});
|
||||
|
||||
const infos = this.props.infos?.map((m, i) => {
|
||||
return <p key={"info-"+i}>{m}</p>
|
||||
})
|
||||
return <p key={"info-"+i}>{m}</p>;
|
||||
});
|
||||
|
||||
return <div className="maputnik-message-panel">
|
||||
{errors}
|
||||
{infos}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import {detect} from 'detect-browser';
|
||||
import React from "react";
|
||||
import classnames from "classnames";
|
||||
import {detect} from "detect-browser";
|
||||
|
||||
import {
|
||||
MdOpenInBrowser,
|
||||
@@ -9,19 +9,22 @@ import {
|
||||
MdHelpOutline,
|
||||
MdFindInPage,
|
||||
MdLanguage,
|
||||
MdSave
|
||||
} from 'react-icons/md'
|
||||
import pkgJson from '../../package.json'
|
||||
MdSave,
|
||||
MdPublic,
|
||||
MdCode
|
||||
} from "react-icons/md";
|
||||
import pkgJson from "../../package.json";
|
||||
//@ts-ignore
|
||||
import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline'
|
||||
import { withTranslation, WithTranslation } from 'react-i18next';
|
||||
import { supportedLanguages } from '../i18n';
|
||||
import type { OnStyleChangedCallback } from '../libs/definitions';
|
||||
import maputnikLogo from "maputnik-design/logos/logo-color.svg?inline";
|
||||
import { withTranslation, type WithTranslation } from "react-i18next";
|
||||
import { supportedLanguages } from "../i18n";
|
||||
import type { OnStyleChangedCallback } from "../libs/definitions";
|
||||
|
||||
// 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;
|
||||
const colorAccessibilityFiltersEnabled = ["chrome", "firefox"].indexOf(browser!.name) > -1;
|
||||
|
||||
export type ModalTypes = "settings" | "sources" | "open" | "shortcuts" | "export" | "debug" | "globalState" | "codeEditor";
|
||||
|
||||
type IconTextProps = {
|
||||
children?: React.ReactNode
|
||||
@@ -30,7 +33,7 @@ type IconTextProps = {
|
||||
|
||||
class IconText extends React.Component<IconTextProps> {
|
||||
render() {
|
||||
return <span className="maputnik-icon-text">{this.props.children}</span>
|
||||
return <span className="maputnik-icon-text">{this.props.children}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,20 +41,19 @@ type ToolbarLinkProps = {
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
href?: string
|
||||
onToggleModal?(...args: unknown[]): unknown
|
||||
};
|
||||
|
||||
class ToolbarLink extends React.Component<ToolbarLinkProps> {
|
||||
render() {
|
||||
return <a
|
||||
className={classnames('maputnik-toolbar-link', this.props.className)}
|
||||
className={classnames("maputnik-toolbar-link", this.props.className)}
|
||||
href={this.props.href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
data-wd-key="toolbar:link"
|
||||
>
|
||||
{this.props.children}
|
||||
</a>
|
||||
</a>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +69,7 @@ class ToolbarSelect extends React.Component<ToolbarSelectProps> {
|
||||
data-wd-key={this.props.wdKey}
|
||||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +87,7 @@ class ToolbarAction extends React.Component<ToolbarActionProps> {
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
{this.props.children}
|
||||
</button>
|
||||
</button>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +102,7 @@ type AppToolbarInternalProps = {
|
||||
// A dict of source id's and the available source layers
|
||||
sources: object
|
||||
children?: React.ReactNode
|
||||
onToggleModal(...args: unknown[]): unknown
|
||||
onToggleModal(modal: ModalTypes): void
|
||||
onSetMapState(mapState: MapState): unknown
|
||||
mapState?: MapState
|
||||
renderer?: string
|
||||
@@ -115,7 +117,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
add: false,
|
||||
export: false,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleSelection(val: MapState) {
|
||||
this.props.onSetMapState(val);
|
||||
@@ -133,7 +135,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
const el = document.querySelector("#skip-target-"+target) as HTMLButtonElement;
|
||||
el.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
@@ -147,7 +149,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
id: "inspect",
|
||||
group: "general",
|
||||
title: t("Inspect"),
|
||||
disabled: this.props.renderer === 'ol',
|
||||
disabled: this.props.renderer === "ol",
|
||||
},
|
||||
{
|
||||
id: "filter-deuteranopia",
|
||||
@@ -220,22 +222,30 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
</a>
|
||||
</div>
|
||||
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
|
||||
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
|
||||
<ToolbarAction wdKey="nav:open" onClick={() => this.props.onToggleModal("open")}>
|
||||
<MdOpenInBrowser />
|
||||
<IconText>{t("Open")}</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
|
||||
<ToolbarAction wdKey="nav:export" onClick={() => this.props.onToggleModal("export")}>
|
||||
<MdSave />
|
||||
<IconText>{t("Save")}</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
|
||||
<ToolbarAction wdKey="nav:code-editor" onClick={() => this.props.onToggleModal("codeEditor")}>
|
||||
<MdCode />
|
||||
<IconText>{t("Code Editor")}</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:sources" onClick={() => this.props.onToggleModal("sources")}>
|
||||
<MdLayers />
|
||||
<IconText>{t("Data Sources")}</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
|
||||
<ToolbarAction wdKey="nav:settings" onClick={() => this.props.onToggleModal("settings")}>
|
||||
<MdSettings />
|
||||
<IconText>{t("Style Settings")}</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction wdKey="nav:global-state" onClick={() => this.props.onToggleModal("globalState")}>
|
||||
<MdPublic />
|
||||
<IconText>{t("Global State")}</IconText>
|
||||
</ToolbarAction>
|
||||
|
||||
<ToolbarSelect wdKey="nav:inspect">
|
||||
<MdFindInPage />
|
||||
@@ -292,7 +302,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
||||
</ToolbarLink>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</nav>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import React, {PropsWithChildren, SyntheticEvent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import FieldDocLabel from './FieldDocLabel'
|
||||
import Doc from './Doc'
|
||||
import React, {type CSSProperties, type PropsWithChildren, type SyntheticEvent} from "react";
|
||||
import classnames from "classnames";
|
||||
import FieldDocLabel from "./FieldDocLabel";
|
||||
import Doc from "./Doc";
|
||||
|
||||
|
||||
type BlockProps = PropsWithChildren & {
|
||||
export type BlockProps = PropsWithChildren & {
|
||||
"data-wd-key"?: string
|
||||
label?: string
|
||||
action?: React.ReactElement
|
||||
style?: object
|
||||
style?: CSSProperties
|
||||
onChange?(...args: unknown[]): unknown
|
||||
fieldSpec?: object
|
||||
wideMode?: boolean
|
||||
@@ -27,13 +26,13 @@ export default class Block extends React.Component<BlockProps, BlockState> {
|
||||
super(props);
|
||||
this.state = {
|
||||
showDoc: false,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onChange(e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) {
|
||||
const value = e.target.value
|
||||
const value = e.target.value;
|
||||
if (this.props.onChange) {
|
||||
return this.props.onChange(value === "" ? undefined : value)
|
||||
return this.props.onChange(value === "" ? undefined : value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +40,7 @@ export default class Block extends React.Component<BlockProps, BlockState> {
|
||||
this.setState({
|
||||
showDoc: val
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Some fields for example <InputColor/> bind click events inside the element
|
||||
@@ -59,7 +58,7 @@ export default class Block extends React.Component<BlockProps, BlockState> {
|
||||
if (event.nativeEvent.target.nodeName !== "A") {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return <label style={this.props.style}
|
||||
@@ -67,7 +66,8 @@ export default class Block extends React.Component<BlockProps, BlockState> {
|
||||
className={classnames({
|
||||
"maputnik-input-block": true,
|
||||
"maputnik-input-block--wide": this.props.wideMode,
|
||||
"maputnik-action-block": this.props.action
|
||||
"maputnik-action-block": this.props.action,
|
||||
"maputnik-input-block--error": this.props.error
|
||||
})}
|
||||
onClick={this.onLabelClick}
|
||||
>
|
||||
@@ -88,17 +88,17 @@ export default class Block extends React.Component<BlockProps, BlockState> {
|
||||
<div className="maputnik-input-block-action">
|
||||
{this.props.action}
|
||||
</div>
|
||||
<div className="maputnik-input-block-content" ref={el => this._blockEl = el}>
|
||||
<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'}}
|
||||
style={{display: this.state.showDoc ? "" : "none"}}
|
||||
>
|
||||
<Doc fieldSpec={this.props.fieldSpec} />
|
||||
</div>
|
||||
}
|
||||
</label>
|
||||
</label>;
|
||||
}
|
||||
}
|
||||
|
||||
29
src/components/CodeEditor.tsx
Normal file
29
src/components/CodeEditor.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import InputJson from "./InputJson";
|
||||
import React from "react";
|
||||
import { withTranslation, type WithTranslation } from "react-i18next";
|
||||
import { type StyleSpecification } from "maplibre-gl";
|
||||
import { type StyleSpecificationWithId } from "../libs/definitions";
|
||||
|
||||
export type CodeEditorProps = {
|
||||
value: StyleSpecification;
|
||||
onChange: (value: StyleSpecificationWithId) => void;
|
||||
onClose: () => void;
|
||||
} & WithTranslation;
|
||||
|
||||
const CodeEditorInternal: React.FC<CodeEditorProps> = (props) => {
|
||||
|
||||
return <>
|
||||
<button className="maputnik-button" onClick={props.onClose} aria-label={props.t("Close")} style={{ position: "sticky", top: "0", zIndex: 1 }}>{props.t("Click to close the editor")}</button>
|
||||
<InputJson
|
||||
lintType="style"
|
||||
value={props.value}
|
||||
onChange={props.onChange}
|
||||
className={"maputnik-code-editor"}
|
||||
withScroll={true}
|
||||
/>;
|
||||
</>;
|
||||
};
|
||||
|
||||
const CodeEditor = withTranslation()(CodeEditorInternal);
|
||||
|
||||
export default CodeEditor;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import { Collapse as ReactCollapse } from 'react-collapse'
|
||||
import {reducedMotionEnabled} from '../libs/accessibility'
|
||||
import React from "react";
|
||||
import { Collapse as ReactCollapse } from "react-collapse";
|
||||
import {reducedMotionEnabled} from "../libs/accessibility";
|
||||
|
||||
|
||||
type CollapseProps = {
|
||||
@@ -12,7 +12,7 @@ type CollapseProps = {
|
||||
export default class Collapse extends React.Component<CollapseProps> {
|
||||
static defaultProps = {
|
||||
isActive: true
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
if (reducedMotionEnabled()) {
|
||||
@@ -20,14 +20,14 @@ export default class Collapse extends React.Component<CollapseProps> {
|
||||
<div style={{display: this.props.isActive ? "block" : "none"}}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<ReactCollapse isOpened={this.props.isActive}>
|
||||
{this.props.children}
|
||||
</ReactCollapse>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import {MdArrowDropDown, MdArrowDropUp} from 'react-icons/md'
|
||||
import React from "react";
|
||||
import {MdArrowDropDown, MdArrowDropUp} from "react-icons/md";
|
||||
|
||||
type CollapserProps = {
|
||||
isCollapsed: boolean
|
||||
@@ -12,7 +12,7 @@ export default class Collapser extends React.Component<CollapserProps> {
|
||||
width: 20,
|
||||
height: 20,
|
||||
...this.props.style,
|
||||
}
|
||||
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />
|
||||
};
|
||||
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react'
|
||||
import React from "react";
|
||||
import Markdown from "react-markdown";
|
||||
|
||||
const headers = {
|
||||
js: "JS",
|
||||
android: "Android",
|
||||
ios: "iOS",
|
||||
macos: "macOS",
|
||||
ios: "iOS"
|
||||
};
|
||||
|
||||
type DocProps = {
|
||||
@@ -15,7 +15,7 @@ type DocProps = {
|
||||
doc?: string
|
||||
}
|
||||
}
|
||||
'sdk-support'?: {
|
||||
"sdk-support"?: {
|
||||
[key: string]: typeof headers
|
||||
}
|
||||
docUrl?: string,
|
||||
@@ -28,7 +28,7 @@ export default class Doc extends React.Component<DocProps> {
|
||||
const {fieldSpec} = this.props;
|
||||
|
||||
const {doc, values, docUrl, docUrlLinkText} = fieldSpec;
|
||||
const sdkSupport = fieldSpec['sdk-support'];
|
||||
const sdkSupport = fieldSpec["sdk-support"];
|
||||
|
||||
const renderValues = (
|
||||
!!values &&
|
||||
@@ -37,11 +37,23 @@ export default class Doc extends React.Component<DocProps> {
|
||||
!Array.isArray(values)
|
||||
);
|
||||
|
||||
const sdkSupportToJsx = (value: string) => {
|
||||
const supportValue = value.toLowerCase();
|
||||
if (supportValue.startsWith("https://")) {
|
||||
return <a href={supportValue} target="_blank" rel="noreferrer">{"#" + supportValue.split("/").pop()}</a>;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{doc &&
|
||||
<div className="SpecDoc">
|
||||
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'>{doc}</div>
|
||||
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'>
|
||||
<Markdown components={{
|
||||
a: ({node: _node, href, children, ...props}) => <a href={href} target="_blank" {...props}>{children}</a>,
|
||||
}}>{doc}</Markdown>
|
||||
</div>
|
||||
{renderValues &&
|
||||
<ul className="SpecDoc__values">
|
||||
{Object.entries(values).map(([key, value]) => {
|
||||
@@ -74,7 +86,7 @@ export default class Doc extends React.Component<DocProps> {
|
||||
<td>{key}</td>
|
||||
{Object.keys(headers).map((k) => {
|
||||
if (Object.prototype.hasOwnProperty.call(supportObj, k)) {
|
||||
return <td key={k}>{supportObj[k as keyof typeof headers]}</td>;
|
||||
return <td key={k}>{sdkSupportToJsx(supportObj[k as keyof typeof headers])}</td>;
|
||||
}
|
||||
else {
|
||||
return <td key={k}>no</td>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import InputArray, { FieldArrayProps as InputArrayProps } from './InputArray'
|
||||
import Fieldset from './Fieldset'
|
||||
import InputArray, { type InputArrayProps } from "./InputArray";
|
||||
import Fieldset from "./Fieldset";
|
||||
|
||||
type FieldArrayProps = InputArrayProps & {
|
||||
name?: string
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Block from './Block'
|
||||
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
|
||||
import Block from "./Block";
|
||||
import InputAutocomplete, { type InputAutocompleteProps } from "./InputAutocomplete";
|
||||
|
||||
|
||||
type FieldAutocompleteProps = InputAutocompleteProps & {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Block from './Block'
|
||||
import InputCheckbox, {InputCheckboxProps} from './InputCheckbox'
|
||||
import Block from "./Block";
|
||||
import InputCheckbox, {type InputCheckboxProps} from "./InputCheckbox";
|
||||
|
||||
|
||||
type FieldCheckboxProps = InputCheckboxProps & {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Block from './Block'
|
||||
import InputColor, {InputColorProps} from './InputColor'
|
||||
import Block from "./Block";
|
||||
import InputColor, {type InputColorProps} from "./InputColor";
|
||||
|
||||
|
||||
type FieldColorProps = InputColorProps & {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react'
|
||||
import React from "react";
|
||||
|
||||
import Block from './Block'
|
||||
import InputString from './InputString'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import Block from "./Block";
|
||||
import InputString from "./InputString";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
|
||||
type FieldCommentInternalProps = {
|
||||
value?: string
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
|
||||
import React, { type JSX } from "react";
|
||||
import {MdInfoOutline, MdHighlightOff} from "react-icons/md";
|
||||
|
||||
type FieldDocLabelProps = {
|
||||
label: JSX.Element | string | undefined
|
||||
@@ -28,12 +28,12 @@ const FieldDocLabel: React.FC<FieldDocLabelProps> = (props) => {
|
||||
<label className="maputnik-doc-wrapper">
|
||||
<div className="maputnik-doc-target">
|
||||
{label}
|
||||
{'\xa0'}
|
||||
{"\xa0"}
|
||||
<button
|
||||
aria-label={open ? 'close property documentation' : 'open property documentation'}
|
||||
className={`maputnik-doc-button maputnik-doc-button--${open ? 'open' : 'closed'}`}
|
||||
aria-label={open ? "close property documentation" : "open property documentation"}
|
||||
className={`maputnik-doc-button maputnik-doc-button--${open ? "open" : "closed"}`}
|
||||
onClick={() => onToggleDoc(!open)}
|
||||
data-wd-key={'field-doc-button-' + label}
|
||||
data-wd-key={"field-doc-button-" + label}
|
||||
>
|
||||
{open ? <MdHighlightOff /> : <MdInfoOutline />}
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import InputDynamicArray, {FieldDynamicArrayProps as InputDynamicArrayProps} from './InputDynamicArray'
|
||||
import Fieldset from './Fieldset'
|
||||
import InputDynamicArray, {type InputDynamicArrayProps} from "./InputDynamicArray";
|
||||
import Fieldset from "./Fieldset";
|
||||
|
||||
type FieldDynamicArrayProps = InputDynamicArrayProps & {
|
||||
name?: string
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import InputEnum, {InputEnumProps} from './InputEnum'
|
||||
import Fieldset from './Fieldset';
|
||||
import InputEnum, {type InputEnumProps} from "./InputEnum";
|
||||
import Fieldset from "./Fieldset";
|
||||
|
||||
|
||||
type FieldEnumProps = InputEnumProps & {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react'
|
||||
import React from "react";
|
||||
|
||||
import SpecProperty from './_SpecProperty'
|
||||
import DataProperty, { Stop } from './_DataProperty'
|
||||
import ZoomProperty from './_ZoomProperty'
|
||||
import ExpressionProperty from './_ExpressionProperty'
|
||||
import {function as styleFunction} from '@maplibre/maplibre-gl-style-spec';
|
||||
import {findDefaultFromSpec} from '../libs/spec-helper';
|
||||
import SpecProperty from "./_SpecProperty";
|
||||
import DataProperty, { type Stop } from "./_DataProperty";
|
||||
import ZoomProperty from "./_ZoomProperty";
|
||||
import ExpressionProperty from "./_ExpressionProperty";
|
||||
import {function as styleFunction} from "@maplibre/maplibre-gl-style-spec";
|
||||
import {findDefaultFromSpec} from "../libs/spec-helper";
|
||||
import { type MappedLayerErrors } from "../libs/definitions";
|
||||
|
||||
|
||||
function isLiteralExpression(value: any) {
|
||||
@@ -22,9 +23,9 @@ function isGetExpression(value: any) {
|
||||
|
||||
function isZoomField(value: any) {
|
||||
return (
|
||||
typeof(value) === 'object' &&
|
||||
typeof(value) === "object" &&
|
||||
value.stops &&
|
||||
typeof(value.property) === 'undefined' &&
|
||||
typeof(value.property) === "undefined" &&
|
||||
Array.isArray(value.stops) &&
|
||||
value.stops.length > 1 &&
|
||||
value.stops.every((stop: Stop) => {
|
||||
@@ -38,7 +39,7 @@ function isZoomField(value: any) {
|
||||
|
||||
function isIdentityProperty(value: any) {
|
||||
return (
|
||||
typeof(value) === 'object' &&
|
||||
typeof(value) === "object" &&
|
||||
value.type === "identity" &&
|
||||
Object.prototype.hasOwnProperty.call(value, "property")
|
||||
);
|
||||
@@ -46,16 +47,16 @@ function isIdentityProperty(value: any) {
|
||||
|
||||
function isDataStopProperty(value: any) {
|
||||
return (
|
||||
typeof(value) === 'object' &&
|
||||
typeof(value) === "object" &&
|
||||
value.stops &&
|
||||
typeof(value.property) !== 'undefined' &&
|
||||
typeof(value.property) !== "undefined" &&
|
||||
value.stops.length > 1 &&
|
||||
Array.isArray(value.stops) &&
|
||||
value.stops.every((stop: Stop) => {
|
||||
return (
|
||||
Array.isArray(stop) &&
|
||||
stop.length === 2 &&
|
||||
typeof(stop[0]) === 'object'
|
||||
typeof(stop[0]) === "object"
|
||||
);
|
||||
})
|
||||
);
|
||||
@@ -90,6 +91,18 @@ function getDataType(value: any, fieldSpec={} as any) {
|
||||
else if (fieldSpec.type === "array" && isArrayOfPrimatives(value)) {
|
||||
return "value";
|
||||
}
|
||||
else if (fieldSpec.type === "numberArray" && isArrayOfPrimatives(value)) {
|
||||
return "value";
|
||||
}
|
||||
else if (fieldSpec.type === "colorArray") {
|
||||
return "value";
|
||||
}
|
||||
else if (fieldSpec.type === "padding") {
|
||||
return "value";
|
||||
}
|
||||
else if (fieldSpec.type === "variableAnchorOffsetCollection") {
|
||||
return "value";
|
||||
}
|
||||
else if (isZoomField(value)) {
|
||||
return "zoom_function";
|
||||
}
|
||||
@@ -107,7 +120,7 @@ type FieldFunctionProps = {
|
||||
fieldName: string
|
||||
fieldType: string
|
||||
fieldSpec: any
|
||||
errors?: {[key: string]: {message: string}}
|
||||
errors?: MappedLayerErrors
|
||||
value?: any
|
||||
};
|
||||
|
||||
@@ -129,18 +142,18 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
||||
|
||||
const getFieldFunctionType = (fieldSpec: any) => {
|
||||
if (fieldSpec.expression.interpolated) {
|
||||
return 'exponential';
|
||||
return "exponential";
|
||||
}
|
||||
if (fieldSpec.type === 'number') {
|
||||
return 'interval';
|
||||
if (fieldSpec.type === "number") {
|
||||
return "interval";
|
||||
}
|
||||
return 'categorical';
|
||||
return "categorical";
|
||||
};
|
||||
|
||||
const addStop = () => {
|
||||
const stops = props.value.stops.slice(0);
|
||||
const lastStop = stops[stops.length - 1];
|
||||
if (typeof lastStop[0] === 'object') {
|
||||
if (typeof lastStop[0] === "object") {
|
||||
stops.push([
|
||||
{ zoom: lastStop[0].zoom + 1, value: lastStop[0].value },
|
||||
lastStop[1],
|
||||
@@ -160,7 +173,7 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
||||
const deleteExpression = () => {
|
||||
const { fieldSpec, fieldName } = props;
|
||||
props.onChange(fieldName, fieldSpec.default);
|
||||
setDataType('value');
|
||||
setDataType("value");
|
||||
};
|
||||
|
||||
const deleteStop = (stopIdx: number) => {
|
||||
@@ -183,7 +196,7 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
||||
const { value } = props;
|
||||
|
||||
let zoomFunc: any;
|
||||
if (typeof value === 'object') {
|
||||
if (typeof value === "object") {
|
||||
if (value.stops) {
|
||||
zoomFunc = {
|
||||
base: value.base,
|
||||
@@ -217,13 +230,13 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
||||
|
||||
if (isGetExpression(value)) {
|
||||
props.onChange(fieldName, {
|
||||
type: 'identity',
|
||||
type: "identity",
|
||||
property: value[1],
|
||||
});
|
||||
setDataType('value');
|
||||
setDataType("value");
|
||||
} else if (isLiteralExpression(value)) {
|
||||
props.onChange(fieldName, value[1]);
|
||||
setDataType('value');
|
||||
setDataType("value");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -233,7 +246,7 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
||||
isGetExpression(value) ||
|
||||
isLiteralExpression(value) ||
|
||||
isPrimative(value) ||
|
||||
(Array.isArray(value) && fieldSpec.type === 'array')
|
||||
(Array.isArray(value) && fieldSpec.type === "array")
|
||||
);
|
||||
};
|
||||
|
||||
@@ -241,26 +254,26 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
||||
const { value, fieldSpec } = props;
|
||||
let expression;
|
||||
|
||||
if (typeof value === 'object' && 'stops' in value) {
|
||||
if (typeof value === "object" && "stops" in value) {
|
||||
expression = styleFunction.convertFunction(value, fieldSpec);
|
||||
} else if (isIdentityProperty(value)) {
|
||||
expression = ['get', value.property];
|
||||
expression = ["get", value.property];
|
||||
} else {
|
||||
expression = ['literal', value || props.fieldSpec.default];
|
||||
expression = ["literal", value || props.fieldSpec.default];
|
||||
}
|
||||
props.onChange(props.fieldName, expression);
|
||||
};
|
||||
|
||||
const makeDataFunction = () => {
|
||||
const functionType = getFieldFunctionType(props.fieldSpec);
|
||||
const stopValue = functionType === 'categorical' ? '' : 0;
|
||||
const stopValue = functionType === "categorical" ? "" : 0;
|
||||
const { value } = props;
|
||||
let dataFunc;
|
||||
|
||||
if (typeof value === 'object') {
|
||||
if (typeof value === "object") {
|
||||
if (value.stops) {
|
||||
dataFunc = {
|
||||
property: '',
|
||||
property: "",
|
||||
type: functionType,
|
||||
base: value.base,
|
||||
stops: value.stops.map((stop: Stop) => {
|
||||
@@ -269,7 +282,7 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
||||
};
|
||||
} else {
|
||||
dataFunc = {
|
||||
property: '',
|
||||
property: "",
|
||||
type: functionType,
|
||||
base: value.base,
|
||||
stops: [
|
||||
@@ -280,7 +293,7 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
||||
}
|
||||
} else {
|
||||
dataFunc = {
|
||||
property: '',
|
||||
property: "",
|
||||
type: functionType,
|
||||
base: value.base,
|
||||
stops: [
|
||||
@@ -293,6 +306,20 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
||||
props.onChange(props.fieldName, dataFunc);
|
||||
};
|
||||
|
||||
const makeElevationFunction = () => {
|
||||
const expression = [
|
||||
"interpolate",
|
||||
["linear"],
|
||||
["elevation"],
|
||||
0,
|
||||
"black",
|
||||
2000,
|
||||
"white"
|
||||
];
|
||||
|
||||
props.onChange(props.fieldName, expression);
|
||||
};
|
||||
|
||||
const onMarkEditing = () => {
|
||||
setIsEditing(true);
|
||||
};
|
||||
@@ -302,11 +329,11 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
||||
};
|
||||
|
||||
const propClass =
|
||||
props.fieldSpec.default === props.value ? 'maputnik-default-property' : 'maputnik-modified-property';
|
||||
props.fieldSpec.default === props.value ? "maputnik-default-property" : "maputnik-modified-property";
|
||||
|
||||
let specField;
|
||||
|
||||
if (dataType === 'expression') {
|
||||
if (dataType === "expression") {
|
||||
specField = (
|
||||
<ExpressionProperty
|
||||
errors={props.errors}
|
||||
@@ -322,7 +349,7 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
||||
onBlur={onUnmarkEditing}
|
||||
/>
|
||||
);
|
||||
} else if (dataType === 'zoom_function') {
|
||||
} else if (dataType === "zoom_function") {
|
||||
specField = (
|
||||
<ZoomProperty
|
||||
errors={props.errors}
|
||||
@@ -337,7 +364,7 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
||||
onExpressionClick={makeExpression}
|
||||
/>
|
||||
);
|
||||
} else if (dataType === 'data_function') {
|
||||
} else if (dataType === "data_function") {
|
||||
specField = (
|
||||
<DataProperty
|
||||
errors={props.errors}
|
||||
@@ -364,12 +391,13 @@ const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
||||
onZoomClick={makeZoomFunction}
|
||||
onDataClick={makeDataFunction}
|
||||
onExpressionClick={makeExpression}
|
||||
onElevationClick={makeElevationFunction}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={propClass} data-wd-key={'spec-field-container:' + props.fieldName}>
|
||||
<div className={propClass} data-wd-key={"spec-field-container:" + props.fieldName}>
|
||||
{specField}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import Block from './Block'
|
||||
import InputString from './InputString'
|
||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
||||
import Block from "./Block";
|
||||
import InputString from "./InputString";
|
||||
|
||||
type FieldIdProps = {
|
||||
value: string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import InputJson, {InputJsonProps} from './InputJson'
|
||||
import InputJson, {type InputJsonProps} from "./InputJson";
|
||||
|
||||
|
||||
type FieldJsonProps = InputJsonProps & {};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react'
|
||||
import React from "react";
|
||||
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import Block from './Block'
|
||||
import InputNumber from './InputNumber'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
||||
import Block from "./Block";
|
||||
import InputNumber from "./InputNumber";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
|
||||
type FieldMaxZoomInternalProps = {
|
||||
value?: number
|
||||
@@ -14,7 +14,7 @@ type FieldMaxZoomInternalProps = {
|
||||
const FieldMaxZoomInternal: React.FC<FieldMaxZoomInternalProps> = (props) => {
|
||||
const t = props.t;
|
||||
return (
|
||||
<Block label={t('Max Zoom')} fieldSpec={latest.layer.maxzoom}
|
||||
<Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom}
|
||||
error={props.error}
|
||||
data-wd-key="max-zoom"
|
||||
>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react'
|
||||
import React from "react";
|
||||
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import Block from './Block'
|
||||
import InputNumber from './InputNumber'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
||||
import Block from "./Block";
|
||||
import InputNumber from "./InputNumber";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
|
||||
type FieldMinZoomInternalProps = {
|
||||
value?: number
|
||||
@@ -14,7 +14,7 @@ type FieldMinZoomInternalProps = {
|
||||
const FieldMinZoomInternal: React.FC<FieldMinZoomInternalProps> = (props) => {
|
||||
const t = props.t;
|
||||
return (
|
||||
<Block label={t('Min Zoom')} fieldSpec={latest.layer.minzoom}
|
||||
<Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom}
|
||||
error={props.error}
|
||||
data-wd-key="min-zoom"
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import InputMultiInput, {InputMultiInputProps} from './InputMultiInput'
|
||||
import Fieldset from './Fieldset'
|
||||
import InputMultiInput, {type InputMultiInputProps} from "./InputMultiInput";
|
||||
import Fieldset from "./Fieldset";
|
||||
|
||||
|
||||
type FieldMultiInputProps = InputMultiInputProps & {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import InputNumber, {InputNumberProps} from './InputNumber'
|
||||
import Block from './Block'
|
||||
import InputNumber, {type InputNumberProps} from "./InputNumber";
|
||||
import Block from "./Block";
|
||||
|
||||
|
||||
type FieldNumberProps = InputNumberProps & {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Block from './Block'
|
||||
import InputSelect, {InputSelectProps} from './InputSelect'
|
||||
import Block from "./Block";
|
||||
import InputSelect, {type InputSelectProps} from "./InputSelect";
|
||||
|
||||
|
||||
type FieldSelectProps = InputSelectProps & {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react'
|
||||
import React from "react";
|
||||
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import Block from './Block'
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
||||
import Block from "./Block";
|
||||
import InputAutocomplete from "./InputAutocomplete";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
|
||||
type FieldSourceInternalProps = {
|
||||
value?: string
|
||||
@@ -23,7 +23,7 @@ const FieldSourceInternal: React.FC<FieldSourceInternalProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<Block
|
||||
label={t('Source')}
|
||||
label={t("Source")}
|
||||
fieldSpec={latest.layer.source}
|
||||
error={error}
|
||||
data-wd-key={wdKey}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react'
|
||||
import React from "react";
|
||||
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import Block from './Block'
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import {latest} from "@maplibre/maplibre-gl-style-spec";
|
||||
import Block from "./Block";
|
||||
import InputAutocomplete from "./InputAutocomplete";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
|
||||
type FieldSourceLayerInternalProps = {
|
||||
value?: string
|
||||
@@ -21,8 +21,8 @@ const FieldSourceLayerInternal: React.FC<FieldSourceLayerInternalProps> = ({
|
||||
}) => {
|
||||
return (
|
||||
<Block
|
||||
label={t('Source Layer')}
|
||||
fieldSpec={latest.layer['source-layer']}
|
||||
label={t("Source Layer")}
|
||||
fieldSpec={latest.layer["source-layer"]}
|
||||
data-wd-key="layer-source-layer"
|
||||
error={error}
|
||||
>
|
||||
|
||||
49
src/components/FieldSpec.tsx
Normal file
49
src/components/FieldSpec.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import Block, { type BlockProps } from "./Block";
|
||||
import InputSpec, { type FieldSpecType, type InputSpecProps } from "./InputSpec";
|
||||
import Fieldset, { type FieldsetProps } from "./Fieldset";
|
||||
|
||||
function getElementFromType(fieldSpec: { type?: FieldSpecType, values?: unknown[] }): typeof Fieldset | typeof Block {
|
||||
switch(fieldSpec.type) {
|
||||
case "color":
|
||||
return Block;
|
||||
case "enum":
|
||||
return (Object.keys(fieldSpec.values!).length <= 3 ? Fieldset : Block);
|
||||
case "boolean":
|
||||
return Block;
|
||||
case "array":
|
||||
return Fieldset;
|
||||
case "resolvedImage":
|
||||
return Block;
|
||||
case "number":
|
||||
return Block;
|
||||
case "string":
|
||||
return Block;
|
||||
case "formatted":
|
||||
return Block;
|
||||
case "padding":
|
||||
return Block;
|
||||
case "numberArray":
|
||||
return Fieldset;
|
||||
case "colorArray":
|
||||
return Fieldset;
|
||||
case "variableAnchorOffsetCollection":
|
||||
return Fieldset;
|
||||
default:
|
||||
console.warn("No such type for: " + fieldSpec.type);
|
||||
return Block;
|
||||
}
|
||||
}
|
||||
|
||||
export type FieldSpecProps = InputSpecProps & BlockProps & FieldsetProps;
|
||||
|
||||
const FieldSpec: React.FC<FieldSpecProps> = (props) => {
|
||||
const TypeBlock = getElementFromType(props.fieldSpec!);
|
||||
|
||||
return (
|
||||
<TypeBlock label={props.label} action={props.action} fieldSpec={props.fieldSpec} error={props.error}>
|
||||
<InputSpec {...props} />
|
||||
</TypeBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldSpec;
|
||||
@@ -1,5 +1,5 @@
|
||||
import Block from './Block'
|
||||
import InputString, {InputStringProps} from './InputString'
|
||||
import Block from "./Block";
|
||||
import InputString, {type InputStringProps} from "./InputString";
|
||||
|
||||
type FieldStringProps = InputStringProps & {
|
||||
name?: string
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react'
|
||||
import {v8} from '@maplibre/maplibre-gl-style-spec'
|
||||
import Block from './Block'
|
||||
import InputSelect from './InputSelect'
|
||||
import InputString from './InputString'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import { startCase } from 'lodash'
|
||||
import React from "react";
|
||||
import {v8} from "@maplibre/maplibre-gl-style-spec";
|
||||
import Block from "./Block";
|
||||
import InputSelect from "./InputSelect";
|
||||
import InputString from "./InputString";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
import { startCase } from "lodash";
|
||||
|
||||
type FieldTypeInternalProps = {
|
||||
value: string
|
||||
@@ -22,9 +22,9 @@ const FieldTypeInternal: React.FC<FieldTypeInternalProps> = ({
|
||||
error,
|
||||
disabled = false
|
||||
}) => {
|
||||
const layerstypes: [string, string][] = Object.keys(v8.layer.type.values || {}).map(v => [v, startCase(v.replace(/-/g, ' '))]);
|
||||
const layerstypes: [string, string][] = Object.keys(v8.layer.type.values || {}).map(v => [v, startCase(v.replace(/-/g, " "))]);
|
||||
return (
|
||||
<Block label={t('Type')} fieldSpec={v8.layer.type}
|
||||
<Block label={t("Type")} fieldSpec={v8.layer.type}
|
||||
data-wd-key={wdKey}
|
||||
error={error}
|
||||
>
|
||||
@@ -36,7 +36,7 @@ const FieldTypeInternal: React.FC<FieldTypeInternalProps> = ({
|
||||
options={layerstypes}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
data-wd-key={wdKey + '.select'}
|
||||
data-wd-key={wdKey + ".select"}
|
||||
/>
|
||||
)}
|
||||
</Block>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import InputUrl, {FieldUrlProps as InputUrlProps} from './InputUrl'
|
||||
import Block from './Block'
|
||||
import InputUrl, {type FieldUrlProps as InputUrlProps} from "./InputUrl";
|
||||
import Block from "./Block";
|
||||
|
||||
|
||||
type FieldUrlProps = InputUrlProps & {
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import React, { PropsWithChildren, ReactElement } from 'react'
|
||||
import FieldDocLabel from './FieldDocLabel'
|
||||
import Doc from './Doc'
|
||||
import generateUniqueId from '../libs/document-uid';
|
||||
import React, { type PropsWithChildren, type ReactElement } from "react";
|
||||
import classnames from "classnames";
|
||||
import FieldDocLabel from "./FieldDocLabel";
|
||||
import Doc from "./Doc";
|
||||
import generateUniqueId from "../libs/document-uid";
|
||||
|
||||
type FieldsetProps = PropsWithChildren & {
|
||||
export type FieldsetProps = PropsWithChildren & {
|
||||
label?: string,
|
||||
fieldSpec?: { doc?: string },
|
||||
action?: ReactElement,
|
||||
error?: {message: string}
|
||||
};
|
||||
|
||||
|
||||
const Fieldset: React.FC<FieldsetProps> = (props) => {
|
||||
const [showDoc, setShowDoc] = React.useState(false);
|
||||
const labelId = React.useRef(generateUniqueId('fieldset_label_'));
|
||||
const labelId = React.useRef(generateUniqueId("fieldset_label_"));
|
||||
|
||||
const onToggleDoc = (val: boolean) => {
|
||||
setShowDoc(val);
|
||||
@@ -30,14 +32,17 @@ const Fieldset: React.FC<FieldsetProps> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
{!props.fieldSpec && (
|
||||
<div className="maputnik-input-block-label">
|
||||
<div className={classnames({
|
||||
"maputnik-input-block-label": true,
|
||||
"maputnik-input-block--error": props.error
|
||||
})}>
|
||||
{props.label}
|
||||
</div>
|
||||
)}
|
||||
<div className="maputnik-input-block-action">{props.action}</div>
|
||||
<div className="maputnik-input-block-content">{props.children}</div>
|
||||
{props.fieldSpec && (
|
||||
<div className="maputnik-doc-inline" style={{ display: showDoc ? '' : 'none' }}>
|
||||
<div className="maputnik-doc-inline" style={{ display: showDoc ? "" : "none" }}>
|
||||
<Doc fieldSpec={props.fieldSpec} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import React from 'react'
|
||||
import {mdiTableRowPlusAfter} from '@mdi/js';
|
||||
import {isEqual} from 'lodash';
|
||||
import {ExpressionSpecification, LegacyFilterSpecification} from 'maplibre-gl'
|
||||
import {latest, migrate, convertFilter} from '@maplibre/maplibre-gl-style-spec'
|
||||
import {mdiFunctionVariant} from '@mdi/js';
|
||||
import React from "react";
|
||||
import { TbMathFunction } from "react-icons/tb";
|
||||
import { PiListPlusBold } from "react-icons/pi";
|
||||
import {isEqual} from "lodash";
|
||||
import {type ExpressionSpecification, type LegacyFilterSpecification} from "maplibre-gl";
|
||||
import {migrate, convertFilter} from "@maplibre/maplibre-gl-style-spec";
|
||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
||||
|
||||
import {combiningFilterOps} from '../libs/filterops'
|
||||
import InputSelect from './InputSelect'
|
||||
import Block from './Block'
|
||||
import SingleFilterEditor from './SingleFilterEditor'
|
||||
import FilterEditorBlock from './FilterEditorBlock'
|
||||
import InputButton from './InputButton'
|
||||
import Doc from './Doc'
|
||||
import ExpressionProperty from './_ExpressionProperty';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import type { StyleSpecificationWithId } from '../libs/definitions';
|
||||
import {combiningFilterOps} from "../libs/filterops";
|
||||
import InputSelect from "./InputSelect";
|
||||
import Block from "./Block";
|
||||
import SingleFilterEditor from "./SingleFilterEditor";
|
||||
import FilterEditorBlock from "./FilterEditorBlock";
|
||||
import InputButton from "./InputButton";
|
||||
import Doc from "./Doc";
|
||||
import ExpressionProperty from "./_ExpressionProperty";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
import type { MappedLayerErrors, StyleSpecificationWithId } from "../libs/definitions";
|
||||
|
||||
|
||||
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification {
|
||||
const filter = props.filter || ['all'];
|
||||
const filter = props.filter || ["all"];
|
||||
|
||||
if (!Array.isArray(filter)) {
|
||||
return filter;
|
||||
@@ -28,7 +29,7 @@ function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecific
|
||||
let filters = filter.slice(1);
|
||||
|
||||
if(combiningFilterOps.indexOf(combiningOp) < 0) {
|
||||
combiningOp = 'all';
|
||||
combiningOp = "all";
|
||||
filters = [filter.slice(0)];
|
||||
}
|
||||
|
||||
@@ -49,7 +50,7 @@ function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpe
|
||||
"sources": {
|
||||
"tmp": {
|
||||
"type": "geojson",
|
||||
"data": ''
|
||||
"data": ""
|
||||
}
|
||||
},
|
||||
"sprite": "",
|
||||
@@ -81,22 +82,22 @@ function checkIfSimpleFilter (filter: LegacyFilterSpecification | ExpressionSpec
|
||||
}
|
||||
|
||||
function hasCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
|
||||
return combiningFilterOps.indexOf(filter[0]) >= 0
|
||||
return combiningFilterOps.indexOf(filter[0]) >= 0;
|
||||
}
|
||||
|
||||
function hasNestedCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
|
||||
if(hasCombiningFilter(filter)) {
|
||||
return filter.slice(1).map(f => hasCombiningFilter(f as any)).filter(f => f == true).length > 0
|
||||
return filter.slice(1).map(f => hasCombiningFilter(f as any)).filter(f => f == true).length > 0;
|
||||
}
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
type FilterEditorInternalProps = {
|
||||
/** Properties of the vector layer and the available fields */
|
||||
properties?: {[key:string]: any}
|
||||
filter?: any[]
|
||||
errors?: {[key:string]: any}
|
||||
onChange(value: LegacyFilterSpecification | ExpressionSpecification): unknown
|
||||
errors?: MappedLayerErrors
|
||||
onChange(value: LegacyFilterSpecification | ExpressionSpecification): void
|
||||
} & WithTranslation;
|
||||
|
||||
type FilterEditorState = {
|
||||
@@ -108,7 +109,7 @@ type FilterEditorState = {
|
||||
class FilterEditorInternal extends React.Component<FilterEditorInternalProps, FilterEditorState> {
|
||||
static defaultProps = {
|
||||
filter: ["all"],
|
||||
}
|
||||
};
|
||||
|
||||
constructor (props: FilterEditorInternalProps) {
|
||||
super(props);
|
||||
@@ -120,42 +121,42 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
|
||||
// Convert filter to combining filter
|
||||
onFilterPartChanged(filterIdx: number, newPart: any[]) {
|
||||
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
|
||||
newFilter[filterIdx] = newPart
|
||||
this.props.onChange(newFilter)
|
||||
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
|
||||
newFilter[filterIdx] = newPart;
|
||||
this.props.onChange(newFilter);
|
||||
}
|
||||
|
||||
deleteFilterItem(filterIdx: number) {
|
||||
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
|
||||
newFilter.splice(filterIdx + 1, 1)
|
||||
this.props.onChange(newFilter)
|
||||
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
|
||||
newFilter.splice(filterIdx + 1, 1);
|
||||
this.props.onChange(newFilter);
|
||||
}
|
||||
|
||||
addFilterItem = () => {
|
||||
const newFilterItem = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
|
||||
(newFilterItem as any[]).push(['==', 'name', ''])
|
||||
this.props.onChange(newFilterItem)
|
||||
}
|
||||
const newFilterItem = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
|
||||
(newFilterItem as any[]).push(["==", "name", ""]);
|
||||
this.props.onChange(newFilterItem);
|
||||
};
|
||||
|
||||
onToggleDoc = (val: boolean) => {
|
||||
this.setState({
|
||||
showDoc: val
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
makeFilter = () => {
|
||||
this.setState({
|
||||
displaySimpleFilter: true,
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
makeExpression = () => {
|
||||
const filter = combiningFilter(this.props);
|
||||
this.props.onChange(migrateFilter(filter));
|
||||
this.setState({
|
||||
displaySimpleFilter: false,
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(props: Readonly<FilterEditorInternalProps>, state: FilterEditorState) {
|
||||
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
|
||||
@@ -170,7 +171,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
else if (displaySimpleFilter && state.displaySimpleFilter === false) {
|
||||
return {
|
||||
valueIsSimpleFilter: true,
|
||||
}
|
||||
};
|
||||
}
|
||||
else {
|
||||
return {
|
||||
@@ -198,12 +199,10 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
onClick={this.makeExpression}
|
||||
title={t("Convert to expression")}
|
||||
>
|
||||
<svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||
</svg>
|
||||
<TbMathFunction />
|
||||
{t("Upgrade to expression")}
|
||||
</InputButton>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
else if (displaySimpleFilter) {
|
||||
const filter = combiningFilter(this.props);
|
||||
@@ -217,9 +216,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
title={t("Convert to expression")}
|
||||
className="maputnik-make-zoom-function"
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||
</svg>
|
||||
<TbMathFunction />
|
||||
</InputButton>
|
||||
</div>
|
||||
);
|
||||
@@ -241,7 +238,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
@@ -272,15 +269,14 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
className="maputnik-add-filter"
|
||||
onClick={this.addFilterItem}
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
||||
</svg> {t("Add filter")}
|
||||
<PiListPlusBold style={{ verticalAlign: "text-bottom" }} />
|
||||
{t("Add filter")}
|
||||
</InputButton>
|
||||
</div>
|
||||
<div
|
||||
key="doc"
|
||||
className="maputnik-doc-inline"
|
||||
style={{display: this.state.showDoc ? '' : 'none'}}
|
||||
style={{display: this.state.showDoc ? "" : "none"}}
|
||||
>
|
||||
<Doc fieldSpec={fieldSpec} />
|
||||
</div>
|
||||
@@ -298,7 +294,6 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
this.props.onChange(defaultFilter);
|
||||
}}
|
||||
fieldName="filter"
|
||||
fieldSpec={fieldSpec}
|
||||
value={filter}
|
||||
errors={errors}
|
||||
onChange={this.props.onChange}
|
||||
@@ -306,7 +301,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
||||
{this.state.valueIsSimpleFilter &&
|
||||
<div className="maputnik-expr-infobox">
|
||||
{t("You've entered an old style filter.")}
|
||||
{' '}
|
||||
{" "}
|
||||
<button
|
||||
onClick={this.makeFilter}
|
||||
className="maputnik-expr-infobox__button"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { PropsWithChildren } from 'react'
|
||||
import InputButton from './InputButton'
|
||||
import {MdDelete} from 'react-icons/md'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import React, { type PropsWithChildren } from "react";
|
||||
import InputButton from "./InputButton";
|
||||
import {MdDelete} from "react-icons/md";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
|
||||
type FilterEditorBlockInternalProps = PropsWithChildren & {
|
||||
onDelete(...args: unknown[]): unknown
|
||||
@@ -23,7 +23,7 @@ class FilterEditorBlockInternal extends React.Component<FilterEditorBlockInterna
|
||||
<MdDelete />
|
||||
</InputButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from 'react'
|
||||
import IconBase from 'react-icon-base'
|
||||
|
||||
|
||||
export default class IconBackground extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||
<path d="m 1.821019,10.255581 7.414535,5.020197 c 0.372277,0.25206 0.958697,0.239771 1.30985,-0.02745 L 17.539255,9.926162 C 17.89041,9.658941 17.873288,9.238006 17.501015,8.985946 L 10.08648,3.9657402 C 9.714204,3.7136802 9.127782,3.7259703 8.776627,3.9931918 L 1.782775,9.315365 c -0.3511551,0.267221 -0.3340331,0.688156 0.03824,0.940216 z" />
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from 'react'
|
||||
import IconBase from 'react-icon-base'
|
||||
|
||||
|
||||
export default class IconCircle extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||
<path transform="translate(2 2)" d="M7.5,0C11.6422,0,15,3.3578,15,7.5S11.6422,15,7.5,15 S0,11.6422,0,7.5S3.3578,0,7.5,0z M7.5,1.6666c-3.2217,0-5.8333,2.6117-5.8333,5.8334S4.2783,13.3334,7.5,13.3334 s5.8333-2.6117,5.8333-5.8334S10.7217,1.6666,7.5,1.6666z"></path>
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from 'react'
|
||||
import IconBase from 'react-icon-base'
|
||||
|
||||
|
||||
export default class IconFill extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||
<path d="M 2.84978,9.763512 9.462149,4.7316391 16.47225,9.478015 9.859886,14.509879 2.84978,9.763512 m -1.028761,0.492069 7.414535,5.020197 c 0.372277,0.25206 0.958697,0.239771 1.30985,-0.02745 L 17.539255,9.926162 C 17.89041,9.658941 17.873288,9.238006 17.501015,8.985946 L 10.08648,3.9657402 C 9.714204,3.7136802 9.127782,3.7259703 8.776627,3.9931918 L 1.782775,9.315365 c -0.3511551,0.267221 -0.3340331,0.688156 0.03824,0.940216 l 0,0 z" />
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,33 @@
|
||||
import React from 'react'
|
||||
import React from "react";
|
||||
|
||||
import IconLine from './IconLine'
|
||||
import IconFill from './IconFill'
|
||||
import IconSymbol from './IconSymbol'
|
||||
import IconBackground from './IconBackground'
|
||||
import IconCircle from './IconCircle'
|
||||
import IconMissing from './IconMissing'
|
||||
import type {CSSProperties} from "react";
|
||||
import { BsBack, BsDiamondFill, BsSunFill } from "react-icons/bs";
|
||||
import { MdBubbleChart, MdCircle, MdLocationPin, MdPhoto, MdPriorityHigh } from "react-icons/md";
|
||||
import { IoAnalyticsOutline } from "react-icons/io5";
|
||||
import { IoMdCube } from "react-icons/io";
|
||||
import { FaMountain } from "react-icons/fa";
|
||||
|
||||
type IconLayerProps = {
|
||||
type: string
|
||||
style?: object
|
||||
style?: CSSProperties
|
||||
className?: string
|
||||
};
|
||||
|
||||
export default class IconLayer extends React.Component<IconLayerProps> {
|
||||
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} />
|
||||
}
|
||||
const IconLayer: React.FC<IconLayerProps> = (props) => {
|
||||
const iconProps = { style: props.style };
|
||||
switch(props.type) {
|
||||
case "fill-extrusion": return <IoMdCube {...iconProps} />;
|
||||
case "raster": return <MdPhoto {...iconProps} />;
|
||||
case "hillshade": return <BsSunFill {...iconProps} />;
|
||||
case "color-relief": return <FaMountain {...iconProps} />;
|
||||
case "heatmap": return <MdBubbleChart {...iconProps} />;
|
||||
case "fill": return <BsDiamondFill {...iconProps} />;
|
||||
case "background": return <BsBack {...iconProps} />;
|
||||
case "line": return <IoAnalyticsOutline {...iconProps} />;
|
||||
case "symbol": return <MdLocationPin {...iconProps} />;
|
||||
case "circle": return <MdCircle {...iconProps} />;
|
||||
default: return <MdPriorityHigh {...iconProps} />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default IconLayer;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from 'react'
|
||||
import IconBase from 'react-icon-base'
|
||||
|
||||
|
||||
export default class IconLine extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||
<path d="M 12.34,1.29 C 12.5114,1.1076 12.7497,1.0029 13,1 13.5523,1 14,1.4477 14,2 14.0047,2.2478 13.907,2.4866 13.73,2.66 9.785626,6.5516986 6.6148407,9.7551593 2.65,13.72 2.4793,13.8963 2.2453,13.9971 2,14 1.4477,14 1,13.5523 1,13 0.9953,12.7522 1.093,12.5134 1.27,12.34 4.9761967,8.7018093 9.0356422,4.5930579 12.34,1.29 Z" transform="translate(2,2)" />
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import React from 'react'
|
||||
import {MdPriorityHigh} from 'react-icons/md'
|
||||
|
||||
|
||||
export default class IconMissing extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<MdPriorityHigh {...this.props} />
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react'
|
||||
import IconBase from 'react-icon-base'
|
||||
|
||||
|
||||
export default class IconSymbol extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<IconBase viewBox="0 0 20 20" {...this.props}>
|
||||
<g transform="matrix(1.2718518,0,0,1.2601269,16.559526,-7.4065264)">
|
||||
<path d="m -9.7959773,11.060163 c -0.3734787,-0.724437 -0.3580577,-1.2147051 -0.00547,-1.8767873 l 8.6034029,-0.019416 c 0.39670292,0.6865644 0.38365934,1.4750693 -0.011097,1.8864953 l -3.1359613,-0.0033 -0.013695,7.1305 c -0.4055357,0.397083 -1.3146432,0.397083 -1.7769191,-0.02274 l 0.030226,-7.104422 z" />
|
||||
</g>
|
||||
</IconBase>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,29 @@
|
||||
import React from 'react'
|
||||
import InputString from './InputString'
|
||||
import InputNumber from './InputNumber'
|
||||
import React from "react";
|
||||
import InputString from "./InputString";
|
||||
import InputNumber from "./InputNumber";
|
||||
|
||||
export type FieldArrayProps = {
|
||||
export type InputArrayProps = {
|
||||
value: (string | number | undefined)[]
|
||||
type?: string
|
||||
length?: number
|
||||
default?: (string | number | undefined)[]
|
||||
onChange?(value: (string | number | undefined)[] | undefined): unknown
|
||||
'aria-label'?: string
|
||||
"aria-label"?: string
|
||||
label?: string
|
||||
};
|
||||
|
||||
type FieldArrayState = {
|
||||
type InputArrayState = {
|
||||
value: (string | number | undefined)[]
|
||||
initialPropsValue: unknown[]
|
||||
}
|
||||
};
|
||||
|
||||
export default class FieldArray extends React.Component<FieldArrayProps, FieldArrayState> {
|
||||
export default class InputArray extends React.Component<InputArrayProps, InputArrayState> {
|
||||
static defaultProps = {
|
||||
value: [],
|
||||
default: [],
|
||||
}
|
||||
};
|
||||
|
||||
constructor (props: FieldArrayProps) {
|
||||
constructor (props: InputArrayProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: this.props.value.slice(0),
|
||||
@@ -32,7 +32,7 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: Readonly<FieldArrayProps>, state: FieldArrayState) {
|
||||
static getDerivedStateFromProps(props: Readonly<InputArrayProps>, state: InputArrayState) {
|
||||
const value: any[] = [];
|
||||
const initialPropsValue = state.initialPropsValue.slice(0);
|
||||
|
||||
@@ -44,7 +44,7 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
|
||||
value[i] = state.value[i];
|
||||
initialPropsValue[i] = state.value[i];
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
value,
|
||||
@@ -54,7 +54,7 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
|
||||
|
||||
isComplete(value: unknown[]) {
|
||||
return Array(this.props.length).fill(null).every((_, i) => {
|
||||
const val = value[i]
|
||||
const val = value[i];
|
||||
return !(val === undefined || val === "");
|
||||
});
|
||||
}
|
||||
@@ -82,20 +82,20 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
|
||||
const containsValues = (
|
||||
value.length > 0 &&
|
||||
!value.every(val => {
|
||||
return (val === "" || val === undefined)
|
||||
return (val === "" || val === undefined);
|
||||
})
|
||||
);
|
||||
|
||||
const inputs = Array(this.props.length).fill(null).map((_, i) => {
|
||||
if(this.props.type === 'number') {
|
||||
if(this.props.type === "number") {
|
||||
return <InputNumber
|
||||
key={i}
|
||||
default={containsValues || !this.props.default ? undefined : this.props.default[i] as number}
|
||||
value={value[i] as number}
|
||||
required={containsValues ? true : false}
|
||||
onChange={(v) => this.changeValue(i, v)}
|
||||
aria-label={this.props['aria-label'] || this.props.label}
|
||||
/>
|
||||
aria-label={this.props["aria-label"] || this.props.label}
|
||||
/>;
|
||||
} else {
|
||||
return <InputString
|
||||
key={i}
|
||||
@@ -103,15 +103,15 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
|
||||
value={value[i] as string}
|
||||
required={containsValues ? true : false}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
aria-label={this.props['aria-label'] || this.props.label}
|
||||
/>
|
||||
aria-label={this.props["aria-label"] || this.props.label}
|
||||
/>;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="maputnik-array">
|
||||
{inputs}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
import { mount } from 'cypress/react'
|
||||
import InputAutocomplete from "./InputAutocomplete";
|
||||
import { mount } from "cypress/react";
|
||||
|
||||
const fruits = ['apple', 'banana', 'cherry'];
|
||||
const fruits = ["apple", "banana", "cherry"];
|
||||
|
||||
describe('<InputAutocomplete />', () => {
|
||||
it('filters options when typing', () => {
|
||||
describe("<InputAutocomplete />", () => {
|
||||
it("filters options when typing", () => {
|
||||
mount(
|
||||
<InputAutocomplete aria-label="Fruit" options={fruits.map(f => [f, f])} />
|
||||
);
|
||||
cy.get('input').focus();
|
||||
cy.get('.maputnik-autocomplete-menu-item').should('have.length', 3);
|
||||
cy.get('input').type('ch');
|
||||
cy.get('.maputnik-autocomplete-menu-item').should('have.length', 1).and('contain', 'cherry');
|
||||
cy.get('.maputnik-autocomplete-menu-item').click();
|
||||
cy.get('input').should('have.value', 'cherry');
|
||||
cy.get("input").focus();
|
||||
cy.get(".maputnik-autocomplete-menu-item").should("have.length", 3);
|
||||
cy.get("input").type("ch");
|
||||
cy.get(".maputnik-autocomplete-menu-item").should("have.length", 1).and("contain", "cherry");
|
||||
cy.get(".maputnik-autocomplete-menu-item").click();
|
||||
cy.get("input").should("have.value", "cherry");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import {useCombobox} from 'downshift'
|
||||
import React from "react";
|
||||
import classnames from "classnames";
|
||||
import {useCombobox} from "downshift";
|
||||
|
||||
const MAX_HEIGHT = 140
|
||||
const MAX_HEIGHT = 140;
|
||||
|
||||
export type InputAutocompleteProps = {
|
||||
value?: string
|
||||
options?: any[]
|
||||
onChange?(value: string | undefined): unknown
|
||||
'aria-label'?: string
|
||||
"aria-label"?: string
|
||||
};
|
||||
|
||||
export default function InputAutocomplete({
|
||||
value,
|
||||
options = [],
|
||||
onChange = () => {},
|
||||
'aria-label': ariaLabel,
|
||||
"aria-label": ariaLabel,
|
||||
}: InputAutocompleteProps) {
|
||||
const [input, setInput] = React.useState(value || '')
|
||||
const menuRef = React.useRef<HTMLDivElement>(null)
|
||||
const [maxHeight, setMaxHeight] = React.useState(MAX_HEIGHT)
|
||||
const [input, setInput] = React.useState(value || "");
|
||||
const menuRef = React.useRef<HTMLDivElement>(null);
|
||||
const [maxHeight, setMaxHeight] = React.useState(MAX_HEIGHT);
|
||||
|
||||
const filteredItems = React.useMemo(() => {
|
||||
const lv = input.toLowerCase()
|
||||
return options.filter((item) => item[0].toLowerCase().includes(lv))
|
||||
}, [options, input])
|
||||
const lv = input.toLowerCase();
|
||||
return options.filter((item) => item[0].toLowerCase().includes(lv));
|
||||
}, [options, input]);
|
||||
|
||||
const calcMaxHeight = React.useCallback(() => {
|
||||
if (menuRef.current) {
|
||||
const space = window.innerHeight - menuRef.current.getBoundingClientRect().top
|
||||
setMaxHeight(Math.min(space, MAX_HEIGHT))
|
||||
const space = window.innerHeight - menuRef.current.getBoundingClientRect().top;
|
||||
setMaxHeight(Math.min(space, MAX_HEIGHT));
|
||||
}
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
@@ -43,48 +43,48 @@ export default function InputAutocomplete({
|
||||
} = useCombobox({
|
||||
items: filteredItems,
|
||||
inputValue: input,
|
||||
itemToString: (item) => (item ? item[0] : ''),
|
||||
itemToString: (item) => (item ? item[0] : ""),
|
||||
stateReducer: (_state, action) => {
|
||||
if (action.type === useCombobox.stateChangeTypes.InputClick) {
|
||||
return {...action.changes, isOpen: true}
|
||||
return {...action.changes, isOpen: true};
|
||||
}
|
||||
return action.changes
|
||||
return action.changes;
|
||||
},
|
||||
onSelectedItemChange: ({selectedItem}) => {
|
||||
const v = selectedItem ? selectedItem[0] : ''
|
||||
setInput(v)
|
||||
onChange(selectedItem ? selectedItem[0] : undefined)
|
||||
const v = selectedItem ? selectedItem[0] : "";
|
||||
setInput(v);
|
||||
onChange(selectedItem ? selectedItem[0] : undefined);
|
||||
},
|
||||
onInputValueChange: ({inputValue: v}) => {
|
||||
if (typeof v === 'string') {
|
||||
setInput(v)
|
||||
onChange(v === '' ? undefined : v)
|
||||
openMenu()
|
||||
if (typeof v === "string") {
|
||||
setInput(v);
|
||||
onChange(v === "" ? undefined : v);
|
||||
openMenu();
|
||||
}
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isOpen) {
|
||||
calcMaxHeight()
|
||||
calcMaxHeight();
|
||||
}
|
||||
}, [isOpen, calcMaxHeight])
|
||||
}, [isOpen, calcMaxHeight]);
|
||||
|
||||
React.useEffect(() => {
|
||||
window.addEventListener('resize', calcMaxHeight)
|
||||
return () => window.removeEventListener('resize', calcMaxHeight)
|
||||
}, [calcMaxHeight])
|
||||
window.addEventListener("resize", calcMaxHeight);
|
||||
return () => window.removeEventListener("resize", calcMaxHeight);
|
||||
}, [calcMaxHeight]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setInput(value || '')
|
||||
}, [value])
|
||||
setInput(value || "");
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className="maputnik-autocomplete">
|
||||
<input
|
||||
{...getInputProps({
|
||||
'aria-label': ariaLabel,
|
||||
className: 'maputnik-string',
|
||||
"aria-label": ariaLabel,
|
||||
className: "maputnik-string",
|
||||
spellCheck: false,
|
||||
onFocus: () => openMenu(),
|
||||
})}
|
||||
@@ -92,7 +92,7 @@ export default function InputAutocomplete({
|
||||
<div
|
||||
{...getMenuProps({}, {suppressRefError: true})}
|
||||
ref={menuRef}
|
||||
style={{position: 'fixed', overflow: 'auto', maxHeight, zIndex: 998}}
|
||||
style={{position: "fixed", overflow: "auto", maxHeight, zIndex: 998}}
|
||||
className="maputnik-autocomplete-menu"
|
||||
>
|
||||
{isOpen &&
|
||||
@@ -102,8 +102,8 @@ export default function InputAutocomplete({
|
||||
{...getItemProps({
|
||||
item,
|
||||
index,
|
||||
className: classnames('maputnik-autocomplete-menu-item', {
|
||||
'maputnik-autocomplete-menu-item-selected': highlightedIndex === index,
|
||||
className: classnames("maputnik-autocomplete-menu-item", {
|
||||
"maputnik-autocomplete-menu-item-selected": highlightedIndex === index,
|
||||
}),
|
||||
})}
|
||||
>
|
||||
@@ -112,5 +112,5 @@ export default function InputAutocomplete({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import React from "react";
|
||||
import classnames from "classnames";
|
||||
|
||||
type InputButtonProps = {
|
||||
"data-wd-key"?: string
|
||||
@@ -28,6 +28,6 @@ export default class InputButton extends React.Component<InputButtonProps> {
|
||||
style={this.props.style}
|
||||
>
|
||||
{this.props.children}
|
||||
</button>
|
||||
</button>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React from "react";
|
||||
|
||||
export type InputCheckboxProps = {
|
||||
value?: boolean
|
||||
@@ -9,11 +9,11 @@ export type InputCheckboxProps = {
|
||||
export default class InputCheckbox extends React.Component<InputCheckboxProps> {
|
||||
static defaultProps = {
|
||||
value: false,
|
||||
}
|
||||
};
|
||||
|
||||
onChange = () => {
|
||||
this.props.onChange(!this.props.value);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return <div className="maputnik-checkbox-wrapper">
|
||||
@@ -27,11 +27,11 @@ export default class InputCheckbox extends React.Component<InputCheckboxProps> {
|
||||
/>
|
||||
<div className="maputnik-checkbox-box">
|
||||
<svg style={{
|
||||
display: this.props.value ? 'inline' : 'none'
|
||||
display: this.props.value ? "inline" : "none"
|
||||
}} className="maputnik-checkbox-icon" viewBox='0 0 32 32'>
|
||||
<path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react'
|
||||
import Color from 'color'
|
||||
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
|
||||
import {ColorResult} from 'react-color';
|
||||
import lodash from 'lodash';
|
||||
import React from "react";
|
||||
import Color from "color";
|
||||
import ChromePicker from "react-color/lib/components/chrome/Chrome";
|
||||
import {type ColorResult} from "react-color";
|
||||
import lodash from "lodash";
|
||||
|
||||
function formatColor(color: ColorResult): string {
|
||||
const rgb = color.rgb
|
||||
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`
|
||||
const rgb = color.rgb;
|
||||
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`;
|
||||
}
|
||||
|
||||
export type InputColorProps = {
|
||||
@@ -16,14 +16,14 @@ export type InputColorProps = {
|
||||
doc?: string
|
||||
style?: object
|
||||
default?: string
|
||||
'aria-label'?: string
|
||||
"aria-label"?: string
|
||||
};
|
||||
|
||||
/*** Number fields with support for min, max and units and documentation*/
|
||||
export default class InputColor extends React.Component<InputColorProps> {
|
||||
state = {
|
||||
pickerOpened: false
|
||||
}
|
||||
};
|
||||
colorInput: HTMLInputElement | null = null;
|
||||
|
||||
constructor (props: InputColorProps) {
|
||||
@@ -39,29 +39,29 @@ export default class InputColor extends React.Component<InputColorProps> {
|
||||
//but I am too stupid to get it to work together with fixed position
|
||||
//and scrollbars so I have to fallback to JavaScript
|
||||
calcPickerOffset = () => {
|
||||
const elem = this.colorInput
|
||||
const elem = this.colorInput;
|
||||
if(elem) {
|
||||
const pos = elem.getBoundingClientRect()
|
||||
const pos = elem.getBoundingClientRect();
|
||||
return {
|
||||
top: pos.top,
|
||||
left: pos.left + 196,
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
top: 160,
|
||||
left: 555,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
togglePicker = () => {
|
||||
this.setState({ pickerOpened: !this.state.pickerOpened })
|
||||
}
|
||||
this.setState({ pickerOpened: !this.state.pickerOpened });
|
||||
};
|
||||
|
||||
get color() {
|
||||
// Catch invalid color.
|
||||
try {
|
||||
return Color(this.props.value).rgb()
|
||||
return Color(this.props.value).rgb();
|
||||
}
|
||||
catch(err) {
|
||||
console.warn("Error parsing color: ", err);
|
||||
@@ -74,7 +74,7 @@ export default class InputColor extends React.Component<InputColorProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const offset = this.calcPickerOffset()
|
||||
const offset = this.calcPickerOffset();
|
||||
const currentColor = this.color.object();
|
||||
const currentChromeColor = {
|
||||
r: currentColor.r,
|
||||
@@ -82,12 +82,12 @@ export default class InputColor extends React.Component<InputColorProps> {
|
||||
b: currentColor.b,
|
||||
// Rename alpha -> a for ChromePicker
|
||||
a: currentColor.alpha!
|
||||
}
|
||||
};
|
||||
|
||||
const picker = <div
|
||||
className="maputnik-color-picker-offset"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
position: "fixed",
|
||||
zIndex: 1,
|
||||
left: offset.left,
|
||||
top: offset.top,
|
||||
@@ -101,14 +101,14 @@ export default class InputColor extends React.Component<InputColorProps> {
|
||||
onClick={this.togglePicker}
|
||||
style={{
|
||||
zIndex: -1,
|
||||
position: 'fixed',
|
||||
top: '0px',
|
||||
right: '0px',
|
||||
bottom: '0px',
|
||||
left: '0px',
|
||||
position: "fixed",
|
||||
top: "0px",
|
||||
right: "0px",
|
||||
bottom: "0px",
|
||||
left: "0px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
const swatchStyle = {
|
||||
backgroundColor: this.props.value
|
||||
@@ -118,11 +118,11 @@ export default class InputColor extends React.Component<InputColorProps> {
|
||||
{this.state.pickerOpened && picker}
|
||||
<div className="maputnik-color-swatch" style={swatchStyle}></div>
|
||||
<input
|
||||
aria-label={this.props['aria-label']}
|
||||
aria-label={this.props["aria-label"]}
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
className="maputnik-color"
|
||||
ref={(input) => this.colorInput = input}
|
||||
ref={(input) => {this.colorInput = input;}}
|
||||
onClick={this.togglePicker}
|
||||
style={this.props.style}
|
||||
name={this.props.name}
|
||||
@@ -130,6 +130,6 @@ export default class InputColor extends React.Component<InputColorProps> {
|
||||
value={this.props.value ? this.props.value : ""}
|
||||
onChange={(e) => this.onChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,67 @@
|
||||
import React from 'react'
|
||||
import capitalize from 'lodash.capitalize'
|
||||
import {MdDelete} from 'react-icons/md'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import React from "react";
|
||||
import capitalize from "lodash.capitalize";
|
||||
import {MdDelete} from "react-icons/md";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
|
||||
import InputString from './InputString'
|
||||
import InputNumber from './InputNumber'
|
||||
import InputButton from './InputButton'
|
||||
import FieldDocLabel from './FieldDocLabel'
|
||||
import InputEnum from './InputEnum'
|
||||
import InputUrl from './InputUrl'
|
||||
import InputString from "./InputString";
|
||||
import InputNumber from "./InputNumber";
|
||||
import InputButton from "./InputButton";
|
||||
import FieldDocLabel from "./FieldDocLabel";
|
||||
import InputEnum from "./InputEnum";
|
||||
import InputUrl from "./InputUrl";
|
||||
import InputColor from "./InputColor";
|
||||
|
||||
|
||||
export type FieldDynamicArrayProps = {
|
||||
export type InputDynamicArrayProps = {
|
||||
value?: (string | number | undefined)[]
|
||||
type?: 'url' | 'number' | 'enum' | 'string'
|
||||
type?: "url" | "number" | "enum" | "string" | "color"
|
||||
default?: (string | number | undefined)[]
|
||||
onChange?(values: (string | number | undefined)[] | undefined): unknown
|
||||
style?: object
|
||||
fieldSpec?: {
|
||||
values?: any
|
||||
}
|
||||
'aria-label'?: string
|
||||
"aria-label"?: string
|
||||
label: string
|
||||
}
|
||||
};
|
||||
|
||||
type FieldDynamicArrayInternalProps = FieldDynamicArrayProps & WithTranslation;
|
||||
type InputDynamicArrayInternalProps = InputDynamicArrayProps & WithTranslation;
|
||||
|
||||
class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInternalProps> {
|
||||
class InputDynamicArrayInternal extends React.Component<InputDynamicArrayInternalProps> {
|
||||
changeValue(idx: number, newValue: string | number | undefined) {
|
||||
const values = this.values.slice(0)
|
||||
values[idx] = newValue
|
||||
if (this.props.onChange) this.props.onChange(values)
|
||||
const values = this.values.slice(0);
|
||||
values[idx] = newValue;
|
||||
if (this.props.onChange) this.props.onChange(values);
|
||||
}
|
||||
|
||||
get values() {
|
||||
return this.props.value || this.props.default || []
|
||||
return this.props.value || this.props.default || [];
|
||||
}
|
||||
|
||||
addValue = () => {
|
||||
const values = this.values.slice(0)
|
||||
if (this.props.type === 'number') {
|
||||
values.push(0)
|
||||
const values = this.values.slice(0);
|
||||
if (this.props.type === "number") {
|
||||
values.push(0);
|
||||
}
|
||||
else if (this.props.type === 'url') {
|
||||
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 defaultValue = Object.keys(fieldSpec!.values)[0];
|
||||
values.push(defaultValue);
|
||||
} else if (this.props.type === "color") {
|
||||
values.push("#000000");
|
||||
} else {
|
||||
values.push("")
|
||||
values.push("");
|
||||
}
|
||||
|
||||
if (this.props.onChange) this.props.onChange(values)
|
||||
}
|
||||
if (this.props.onChange) this.props.onChange(values);
|
||||
};
|
||||
|
||||
deleteValue(valueIdx: number) {
|
||||
const values = this.values.slice(0)
|
||||
values.splice(valueIdx, 1)
|
||||
const values = this.values.slice(0);
|
||||
values.splice(valueIdx, 1);
|
||||
|
||||
if (this.props.onChange) this.props.onChange(values.length > 0 ? values : undefined);
|
||||
}
|
||||
@@ -72,35 +75,42 @@ class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInterna
|
||||
{...i18nProps}
|
||||
/>;
|
||||
let input;
|
||||
if(this.props.type === 'url') {
|
||||
if(this.props.type === "url") {
|
||||
input = <InputUrl
|
||||
value={v as string}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
aria-label={this.props['aria-label'] || this.props.label}
|
||||
/>
|
||||
aria-label={this.props["aria-label"] || this.props.label}
|
||||
/>;
|
||||
}
|
||||
else if (this.props.type === 'number') {
|
||||
else if (this.props.type === "number") {
|
||||
input = <InputNumber
|
||||
value={v as number}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
aria-label={this.props['aria-label'] || this.props.label}
|
||||
/>
|
||||
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)]);
|
||||
input = <InputEnum
|
||||
options={options}
|
||||
value={v as string}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
aria-label={this.props['aria-label'] || this.props.label}
|
||||
/>
|
||||
aria-label={this.props["aria-label"] || this.props.label}
|
||||
/>;
|
||||
}
|
||||
else if (this.props.type === "color") {
|
||||
input = <InputColor
|
||||
value={v as string}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
aria-label={this.props["aria-label"] || this.props.label}
|
||||
/>;
|
||||
}
|
||||
else {
|
||||
input = <InputString
|
||||
value={v as string}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
aria-label={this.props['aria-label'] || this.props.label}
|
||||
/>
|
||||
aria-label={this.props["aria-label"] || this.props.label}
|
||||
/>;
|
||||
}
|
||||
|
||||
return <div
|
||||
@@ -114,8 +124,8 @@ class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInterna
|
||||
<div className="maputnik-array-block-content">
|
||||
{input}
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
</div>;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="maputnik-array">
|
||||
@@ -131,8 +141,8 @@ class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInterna
|
||||
}
|
||||
}
|
||||
|
||||
const FieldDynamicArray = withTranslation()(FieldDynamicArrayInternal);
|
||||
export default FieldDynamicArray;
|
||||
const InputDynamicArray = withTranslation()(InputDynamicArrayInternal);
|
||||
export default InputDynamicArray;
|
||||
|
||||
type DeleteValueInputButtonProps = {
|
||||
onClick?(...args: unknown[]): unknown
|
||||
@@ -149,6 +159,6 @@ class DeleteValueInputButton extends React.Component<DeleteValueInputButtonProps
|
||||
<FieldDocLabel
|
||||
label={<MdDelete />}
|
||||
/>
|
||||
</InputButton>
|
||||
</InputButton>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from 'react'
|
||||
import InputSelect from './InputSelect'
|
||||
import InputMultiInput from './InputMultiInput'
|
||||
import React from "react";
|
||||
import InputSelect from "./InputSelect";
|
||||
import InputMultiInput from "./InputMultiInput";
|
||||
|
||||
|
||||
function optionsLabelLength(options: any[]) {
|
||||
let sum = 0;
|
||||
options.forEach(([_, label]) => {
|
||||
sum += label.length
|
||||
})
|
||||
return sum
|
||||
sum += label.length;
|
||||
});
|
||||
return sum;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export type InputEnumProps = {
|
||||
name?: string
|
||||
onChange(...args: unknown[]): unknown
|
||||
options: any[]
|
||||
'aria-label'?: string
|
||||
"aria-label"?: string
|
||||
label?: string
|
||||
};
|
||||
|
||||
@@ -35,15 +35,15 @@ export default class InputEnum extends React.Component<InputEnumProps> {
|
||||
options={options}
|
||||
value={(value || this.props.default)!}
|
||||
onChange={onChange}
|
||||
aria-label={this.props['aria-label'] || label}
|
||||
/>
|
||||
aria-label={this.props["aria-label"] || label}
|
||||
/>;
|
||||
} else {
|
||||
return <InputSelect
|
||||
options={options}
|
||||
value={(value || this.props.default)!}
|
||||
onChange={onChange}
|
||||
aria-label={this.props['aria-label'] || label}
|
||||
/>
|
||||
aria-label={this.props["aria-label"] || label}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import React from 'react'
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
import React from "react";
|
||||
import InputAutocomplete from "./InputAutocomplete";
|
||||
|
||||
export type FieldFontProps = {
|
||||
export type InputFontProps = {
|
||||
name: string
|
||||
value?: string[]
|
||||
default?: string[]
|
||||
fonts?: unknown[]
|
||||
style?: object
|
||||
onChange(...args: unknown[]): unknown
|
||||
'aria-label'?: string
|
||||
"aria-label"?: string
|
||||
};
|
||||
|
||||
export default class FieldFont extends React.Component<FieldFontProps> {
|
||||
export default class InputFont extends React.Component<InputFontProps> {
|
||||
static defaultProps = {
|
||||
fonts: []
|
||||
}
|
||||
};
|
||||
|
||||
get values() {
|
||||
const out = this.props.value || this.props.default || [];
|
||||
@@ -29,11 +29,11 @@ export default class FieldFont extends React.Component<FieldFontProps> {
|
||||
}
|
||||
|
||||
changeFont(idx: number, newValue: string) {
|
||||
const changedValues = this.values.slice(0)
|
||||
changedValues[idx] = newValue
|
||||
const changedValues = this.values.slice(0);
|
||||
changedValues[idx] = newValue;
|
||||
const filteredValues = changedValues
|
||||
.filter(v => v !== undefined)
|
||||
.filter(v => v !== "")
|
||||
.filter(v => v !== "");
|
||||
|
||||
this.props.onChange(filteredValues);
|
||||
}
|
||||
@@ -44,13 +44,13 @@ export default class FieldFont extends React.Component<FieldFontProps> {
|
||||
key={i}
|
||||
>
|
||||
<InputAutocomplete
|
||||
aria-label={this.props['aria-label'] || this.props.name}
|
||||
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>
|
||||
})
|
||||
</li>;
|
||||
});
|
||||
|
||||
return (
|
||||
<ul className="maputnik-font">
|
||||
|
||||
@@ -1,126 +1,98 @@
|
||||
import React from 'react'
|
||||
import classnames from 'classnames';
|
||||
import CodeMirror, { ModeSpec } from 'codemirror';
|
||||
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||
import React from "react";
|
||||
import classnames from "classnames";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
|
||||
import 'codemirror/mode/javascript/javascript'
|
||||
import 'codemirror/addon/lint/lint'
|
||||
import 'codemirror/addon/edit/matchbrackets'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import 'codemirror/addon/lint/lint.css'
|
||||
import stringifyPretty from 'json-stringify-pretty-compact'
|
||||
import '../libs/codemirror-mgl';
|
||||
import { type EditorView } from "@codemirror/view";
|
||||
import stringifyPretty from "json-stringify-pretty-compact";
|
||||
|
||||
import {createEditor} from "../libs/codemirror-editor-factory";
|
||||
import type { StylePropertySpecification } from "maplibre-gl";
|
||||
import type { TransactionSpec } from "@codemirror/state";
|
||||
|
||||
export type InputJsonProps = {
|
||||
layer: any
|
||||
maxHeight?: number
|
||||
onChange?(...args: unknown[]): unknown
|
||||
lineNumbers?: boolean
|
||||
lineWrapping?: boolean
|
||||
getValue?(data: any): string
|
||||
gutters?: string[]
|
||||
value: object
|
||||
className?: string
|
||||
onChange(object: object): void
|
||||
onFocus?(...args: unknown[]): unknown
|
||||
onBlur?(...args: unknown[]): unknown
|
||||
onJSONValid?(...args: unknown[]): unknown
|
||||
onJSONInvalid?(...args: unknown[]): unknown
|
||||
mode?: ModeSpec<any>
|
||||
lint?: boolean | object
|
||||
lintType: "layer" | "style" | "expression" | "json"
|
||||
spec?: StylePropertySpecification | undefined
|
||||
/**
|
||||
* When setting this and using search and replace, the editor will scroll to the selected text
|
||||
* Use this only when the editor is the only element in the page.
|
||||
*/
|
||||
withScroll?: boolean
|
||||
};
|
||||
type InputJsonInternalProps = InputJsonProps & WithTranslation;
|
||||
|
||||
type InputJsonState = {
|
||||
isEditing: boolean
|
||||
showMessage: boolean
|
||||
prevValue: string
|
||||
};
|
||||
|
||||
class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJsonState> {
|
||||
static defaultProps = {
|
||||
lineNumbers: true,
|
||||
lineWrapping: false,
|
||||
gutters: ["CodeMirror-lint-markers"],
|
||||
getValue: (data: any) => {
|
||||
return stringifyPretty(data, {indent: 2, maxLength: 40});
|
||||
},
|
||||
onFocus: () => {},
|
||||
onBlur: () => {},
|
||||
onJSONInvalid: () => {},
|
||||
onJSONValid: () => {},
|
||||
}
|
||||
_keyEvent: string;
|
||||
_doc: CodeMirror.Editor | undefined;
|
||||
withScroll: false
|
||||
};
|
||||
_view: EditorView | undefined;
|
||||
_el: HTMLDivElement | null = null;
|
||||
_cancelNextChange: boolean = false;
|
||||
|
||||
constructor(props: InputJsonInternalProps) {
|
||||
super(props);
|
||||
this._keyEvent = "keyboard";
|
||||
this.state = {
|
||||
isEditing: false,
|
||||
showMessage: false,
|
||||
prevValue: this.props.getValue!(this.props.layer),
|
||||
prevValue: this.getPrettyJson(this.props.value),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this._doc = CodeMirror(this._el!, {
|
||||
value: this.props.getValue!(this.props.layer),
|
||||
mode: this.props.mode || {
|
||||
name: "mgl",
|
||||
},
|
||||
lineWrapping: this.props.lineWrapping,
|
||||
tabSize: 2,
|
||||
theme: 'maputnik',
|
||||
viewportMargin: Infinity,
|
||||
lineNumbers: this.props.lineNumbers,
|
||||
lint: this.props.lint || {
|
||||
context: "layer"
|
||||
},
|
||||
matchBrackets: true,
|
||||
gutters: this.props.gutters,
|
||||
scrollbarStyle: "null",
|
||||
});
|
||||
|
||||
this._doc.on('change', this.onChange);
|
||||
this._doc.on('focus', this.onFocus);
|
||||
this._doc.on('blur', this.onBlur);
|
||||
getPrettyJson(data: any) {
|
||||
return stringifyPretty(data, {indent: 2, maxLength: 40});
|
||||
}
|
||||
|
||||
onPointerDown = () => {
|
||||
this._keyEvent = "pointer";
|
||||
componentDidMount () {
|
||||
this._view = createEditor({
|
||||
parent: this._el!,
|
||||
value: this.getPrettyJson(this.props.value),
|
||||
lintType: this.props.lintType || "layer",
|
||||
onChange: (value:string) => this.onChange(value),
|
||||
onFocus: () => this.onFocus(),
|
||||
onBlur: () => this.onBlur(),
|
||||
spec: this.props.spec
|
||||
});
|
||||
}
|
||||
|
||||
onFocus = () => {
|
||||
if (this.props.onFocus) this.props.onFocus();
|
||||
this.setState({
|
||||
isEditing: true,
|
||||
showMessage: (this._keyEvent === "keyboard"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onBlur = () => {
|
||||
this._keyEvent = "keyboard";
|
||||
if (this.props.onBlur) this.props.onBlur();
|
||||
this.setState({
|
||||
isEditing: false,
|
||||
showMessage: false,
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnMount () {
|
||||
this._doc!.off('change', this.onChange);
|
||||
this._doc!.off('focus', this.onFocus);
|
||||
this._doc!.off('blur', this.onBlur);
|
||||
}
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps: InputJsonProps) {
|
||||
if (!this.state.isEditing && prevProps.layer !== this.props.layer) {
|
||||
if (!this.state.isEditing && prevProps.value !== this.props.value) {
|
||||
this._cancelNextChange = true;
|
||||
this._doc!.setValue(
|
||||
this.props.getValue!(this.props.layer),
|
||||
)
|
||||
const transactionSpec: TransactionSpec = {
|
||||
changes: {
|
||||
from: 0,
|
||||
to: this._view!.state.doc.length,
|
||||
insert: this.getPrettyJson(this.props.value)
|
||||
}
|
||||
};
|
||||
if (this.props.withScroll) {
|
||||
transactionSpec.selection = this._view!.state.selection;
|
||||
transactionSpec.scrollIntoView = true;
|
||||
}
|
||||
this._view!.dispatch(transactionSpec);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,11 +100,11 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
|
||||
if (this._cancelNextChange) {
|
||||
this._cancelNextChange = false;
|
||||
this.setState({
|
||||
prevValue: this._doc!.getValue(),
|
||||
})
|
||||
prevValue: this._view!.state.doc.toString(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const newCode = this._doc!.getValue();
|
||||
const newCode = this._view!.state.doc.toString();
|
||||
|
||||
if (this.state.prevValue !== newCode) {
|
||||
let parsedLayer, err;
|
||||
@@ -140,43 +112,26 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
|
||||
parsedLayer = JSON.parse(newCode);
|
||||
} catch(_err) {
|
||||
err = _err;
|
||||
console.warn(_err)
|
||||
console.warn(_err);
|
||||
}
|
||||
|
||||
if (err && this.props.onJSONInvalid) {
|
||||
this.props.onJSONInvalid();
|
||||
}
|
||||
else {
|
||||
if (this.props.onChange) this.props.onChange(parsedLayer)
|
||||
if (this.props.onJSONValid) this.props.onJSONValid();
|
||||
if (!err) {
|
||||
if (this.props.onChange) this.props.onChange(parsedLayer);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
prevValue: newCode,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
const {showMessage} = this.state;
|
||||
const style = {} as {maxHeight?: number};
|
||||
if (this.props.maxHeight) {
|
||||
style.maxHeight = this.props.maxHeight;
|
||||
}
|
||||
|
||||
return <div className="JSONEditor" onPointerDown={this.onPointerDown} aria-hidden="true">
|
||||
<div className={classnames("JSONEditor__message", {"JSONEditor__message--on": showMessage})}>
|
||||
<Trans t={t}>
|
||||
Press <kbd>ESC</kbd> to lose focus
|
||||
</Trans>
|
||||
</div>
|
||||
return <div className="json-editor" data-wd-key="json-editor" aria-hidden="true" style={{cursor: "text"}}>
|
||||
<div
|
||||
className={classnames("codemirror-container", this.props.className)}
|
||||
ref={(el) => this._el = el}
|
||||
style={style}
|
||||
ref={(el) => {this._el = el;}}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import React from "react";
|
||||
import classnames from "classnames";
|
||||
|
||||
export type InputMultiInputProps = {
|
||||
name?: string
|
||||
value: string
|
||||
options: any[]
|
||||
onChange(...args: unknown[]): unknown
|
||||
'aria-label'?: string
|
||||
"aria-label"?: string
|
||||
};
|
||||
|
||||
export default class InputMultiInput extends React.Component<InputMultiInputProps> {
|
||||
render() {
|
||||
let options = this.props.options
|
||||
let options = this.props.options;
|
||||
if(options.length > 0 && !Array.isArray(options[0])) {
|
||||
options = options.map(v => [v, v])
|
||||
options = options.map(v => [v, v]);
|
||||
}
|
||||
|
||||
const selectedValue = this.props.value || options[0][0]
|
||||
const selectedValue = this.props.value || options[0][0];
|
||||
const radios = options.map(([val, label])=> {
|
||||
return <label
|
||||
key={val}
|
||||
@@ -29,11 +29,11 @@ export default class InputMultiInput extends React.Component<InputMultiInputProp
|
||||
checked={val === selectedValue}
|
||||
/>
|
||||
{label}
|
||||
</label>
|
||||
})
|
||||
</label>;
|
||||
});
|
||||
|
||||
return <fieldset className="maputnik-multibutton" aria-label={this.props['aria-label']}>
|
||||
return <fieldset className="maputnik-multibutton" aria-label={this.props["aria-label"]}>
|
||||
{radios}
|
||||
</fieldset>
|
||||
</fieldset>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { BaseSyntheticEvent } from 'react'
|
||||
import generateUniqueId from '../libs/document-uid';
|
||||
import React, { type BaseSyntheticEvent } from "react";
|
||||
import generateUniqueId from "../libs/document-uid";
|
||||
|
||||
export type InputNumberProps = {
|
||||
value?: number
|
||||
@@ -23,22 +23,22 @@ type InputNumberState = {
|
||||
* This is the value that is currently being edited. It can be an invalid value.
|
||||
*/
|
||||
dirtyValue?: number | string | undefined
|
||||
}
|
||||
};
|
||||
|
||||
export default class InputNumber extends React.Component<InputNumberProps, InputNumberState> {
|
||||
static defaultProps = {
|
||||
rangeStep: 1
|
||||
}
|
||||
};
|
||||
_keyboardEvent: boolean = false;
|
||||
|
||||
constructor(props: InputNumberProps) {
|
||||
super(props)
|
||||
super(props);
|
||||
this.state = {
|
||||
uuid: +generateUniqueId(),
|
||||
editing: false,
|
||||
value: props.value,
|
||||
dirtyValue: props.value,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: Readonly<InputNumberProps>, state: InputNumberState) {
|
||||
@@ -57,7 +57,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
||||
|
||||
const hasChanged = this.props.value !== value;
|
||||
if(this.isValid(value) && hasChanged) {
|
||||
if (this.props.onChange) this.props.onChange(value)
|
||||
if (this.props.onChange) this.props.onChange(value);
|
||||
this.setState({
|
||||
value: value,
|
||||
});
|
||||
@@ -70,7 +70,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
||||
|
||||
this.setState({
|
||||
dirtyValue: newValue === "" ? undefined : newValue,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
isValid(v: number | string | undefined) {
|
||||
@@ -80,18 +80,18 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
||||
|
||||
const value = +v;
|
||||
if(isNaN(value)) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!isNaN(this.props.min!) && value < this.props.min!) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!isNaN(this.props.max!) && value > this.props.max!) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
|
||||
resetValue = () => {
|
||||
@@ -104,14 +104,14 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
||||
// 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.props.value)) {
|
||||
this.changeValue(this.props.value)
|
||||
this.changeValue(this.props.value);
|
||||
this.setState({dirtyValue: this.props.value});
|
||||
} else {
|
||||
this.changeValue(undefined);
|
||||
this.setState({dirtyValue: undefined});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onChangeRange = (e: BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) => {
|
||||
let value = parseFloat(e.target.value);
|
||||
@@ -132,7 +132,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
||||
value = this.state.value! - step;
|
||||
}
|
||||
else {
|
||||
value = this.state.value! + step
|
||||
value = this.state.value! + step;
|
||||
}
|
||||
dirtyValue = value;
|
||||
}
|
||||
@@ -153,7 +153,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
||||
|
||||
this.setState({value, dirtyValue});
|
||||
if (this.props.onChange) this.props.onChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
if(
|
||||
@@ -217,18 +217,18 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
||||
}}
|
||||
onBlur={_e => {
|
||||
this.setState({editing: false});
|
||||
this.resetValue()
|
||||
this.resetValue();
|
||||
}}
|
||||
data-wd-key={this.props["data-wd-key"] + "-text"}
|
||||
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
else {
|
||||
const value = this.state.editing ? this.state.dirtyValue : this.state.value;
|
||||
|
||||
return <input
|
||||
aria-label={this.props['aria-label']}
|
||||
aria-label={this.props["aria-label"]}
|
||||
spellCheck="false"
|
||||
className="maputnik-number"
|
||||
placeholder={this.props.default?.toString()}
|
||||
@@ -240,7 +240,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
||||
onBlur={this.resetValue}
|
||||
required={this.props.required}
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
/>
|
||||
/>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React from "react";
|
||||
|
||||
export type InputSelectProps = {
|
||||
value: string
|
||||
@@ -7,7 +7,7 @@ export type InputSelectProps = {
|
||||
style?: object
|
||||
onChange(value: string | [string, any]): unknown
|
||||
title?: string
|
||||
'aria-label'?: string
|
||||
"aria-label"?: string
|
||||
};
|
||||
|
||||
export default class InputSelect extends React.Component<InputSelectProps> {
|
||||
@@ -24,9 +24,9 @@ export default class InputSelect extends React.Component<InputSelectProps> {
|
||||
title={this.props.title}
|
||||
value={this.props.value}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
aria-label={this.props['aria-label']}
|
||||
aria-label={this.props["aria-label"]}
|
||||
>
|
||||
{ options.map(([val, label]) => <option key={val} value={val}>{label}</option>) }
|
||||
</select>
|
||||
</select>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import React, { ReactElement } from 'react'
|
||||
import React, { type ReactElement } from "react";
|
||||
|
||||
import InputColor, { InputColorProps } from './InputColor'
|
||||
import InputNumber, { InputNumberProps } from './InputNumber'
|
||||
import InputCheckbox, { InputCheckboxProps } from './InputCheckbox'
|
||||
import InputString, { InputStringProps } from './InputString'
|
||||
import InputArray, { FieldArrayProps } from './InputArray'
|
||||
import InputDynamicArray, { FieldDynamicArrayProps } from './InputDynamicArray'
|
||||
import InputFont, { FieldFontProps } from './InputFont'
|
||||
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
|
||||
import InputEnum, { InputEnumProps } from './InputEnum'
|
||||
import capitalize from 'lodash.capitalize'
|
||||
import InputColor, { type InputColorProps } from "./InputColor";
|
||||
import InputNumber, { type InputNumberProps } from "./InputNumber";
|
||||
import InputCheckbox, { type InputCheckboxProps } from "./InputCheckbox";
|
||||
import InputString, { type InputStringProps } from "./InputString";
|
||||
import InputArray, { type InputArrayProps } from "./InputArray";
|
||||
import InputDynamicArray, { type InputDynamicArrayProps } from "./InputDynamicArray";
|
||||
import InputFont, { type InputFontProps } from "./InputFont";
|
||||
import InputAutocomplete, { type InputAutocompleteProps } from "./InputAutocomplete";
|
||||
import InputEnum, { type InputEnumProps } from "./InputEnum";
|
||||
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"];
|
||||
|
||||
export type SpecFieldProps = {
|
||||
export type FieldSpecType = "number" | "enum" | "resolvedImage" | "formatted" | "string" | "color" | "boolean" | "array" | "numberArray" | "padding" | "colorArray" | "variableAnchorOffsetCollection";
|
||||
|
||||
export type InputSpecProps = {
|
||||
onChange?(fieldName: string | undefined, value: number | undefined | (string | number | undefined)[]): unknown
|
||||
fieldName?: string
|
||||
fieldSpec?: {
|
||||
default?: unknown
|
||||
type?: 'number' | 'enum' | 'resolvedImage' | 'formatted' | 'string' | 'color' | 'boolean' | 'array'
|
||||
type?: FieldSpecType
|
||||
minimum?: number
|
||||
maximum?: number
|
||||
values?: unknown[]
|
||||
@@ -28,8 +30,7 @@ export type SpecFieldProps = {
|
||||
value?: string | number | unknown[] | boolean
|
||||
/** Override the style of the field */
|
||||
style?: object
|
||||
'aria-label'?: string
|
||||
error?: unknown[]
|
||||
"aria-label"?: string
|
||||
label?: string
|
||||
action?: ReactElement
|
||||
};
|
||||
@@ -37,11 +38,10 @@ export type SpecFieldProps = {
|
||||
/** Display any field from the Maplibre GL style spec and
|
||||
* choose the correct field component based on the @{fieldSpec}
|
||||
* to display @{value}. */
|
||||
export default class SpecField extends React.Component<SpecFieldProps> {
|
||||
export default class InputSpec extends React.Component<InputSpecProps> {
|
||||
|
||||
childNodes() {
|
||||
const commonProps = {
|
||||
error: this.props.error,
|
||||
fieldSpec: this.props.fieldSpec,
|
||||
label: this.props.label,
|
||||
action: this.props.action,
|
||||
@@ -51,70 +51,96 @@ export default class SpecField extends React.Component<SpecFieldProps> {
|
||||
name: this.props.fieldName,
|
||||
"data-wd-key": "spec-field-input:" + this.props.fieldName,
|
||||
onChange: (newValue: number | undefined | (string | number | undefined)[]) => this.props.onChange!(this.props.fieldName, newValue),
|
||||
'aria-label': this.props['aria-label'],
|
||||
}
|
||||
"aria-label": this.props["aria-label"],
|
||||
};
|
||||
switch(this.props.fieldSpec?.type) {
|
||||
case 'number': return (
|
||||
<InputNumber
|
||||
{...commonProps as InputNumberProps}
|
||||
min={this.props.fieldSpec.minimum}
|
||||
max={this.props.fieldSpec.maximum}
|
||||
/>
|
||||
)
|
||||
case 'enum': {
|
||||
const options = Object.keys(this.props.fieldSpec.values || []).map(v => [v, capitalize(v)])
|
||||
case "number": return (
|
||||
<InputNumber
|
||||
{...commonProps as InputNumberProps}
|
||||
min={this.props.fieldSpec.minimum}
|
||||
max={this.props.fieldSpec.maximum}
|
||||
/>
|
||||
);
|
||||
case "enum": {
|
||||
const options = Object.keys(this.props.fieldSpec.values || []).map(v => [v, capitalize(v)]);
|
||||
|
||||
return <InputEnum
|
||||
{...commonProps as Omit<InputEnumProps, "options">}
|
||||
options={options}
|
||||
/>
|
||||
}
|
||||
case 'resolvedImage':
|
||||
case 'formatted':
|
||||
case 'string':
|
||||
if (iconProperties.indexOf(this.props.fieldName!) >= 0) {
|
||||
const options = this.props.fieldSpec.values || [];
|
||||
return <InputAutocomplete
|
||||
{...commonProps as Omit<InputAutocompleteProps, "options">}
|
||||
options={options.map(f => [f, f])}
|
||||
/>
|
||||
} else {
|
||||
return <InputString
|
||||
{...commonProps as InputStringProps}
|
||||
/>
|
||||
return <InputEnum
|
||||
{...commonProps as Omit<InputEnumProps, "options">}
|
||||
options={options}
|
||||
/>;
|
||||
}
|
||||
case 'color': return (
|
||||
<InputColor
|
||||
{...commonProps as InputColorProps}
|
||||
/>
|
||||
)
|
||||
case 'boolean': return (
|
||||
<InputCheckbox
|
||||
{...commonProps as InputCheckboxProps}
|
||||
/>
|
||||
)
|
||||
case 'array':
|
||||
if(this.props.fieldName === 'text-font') {
|
||||
return <InputFont
|
||||
{...commonProps as FieldFontProps}
|
||||
fonts={this.props.fieldSpec.values}
|
||||
/>
|
||||
} else {
|
||||
if (this.props.fieldSpec.length) {
|
||||
return <InputArray
|
||||
{...commonProps as FieldArrayProps}
|
||||
type={this.props.fieldSpec.value}
|
||||
length={this.props.fieldSpec.length}
|
||||
/>
|
||||
case "resolvedImage":
|
||||
case "formatted":
|
||||
case "string":
|
||||
if (iconProperties.indexOf(this.props.fieldName!) >= 0) {
|
||||
const options = this.props.fieldSpec.values || [];
|
||||
return <InputAutocomplete
|
||||
{...commonProps as Omit<InputAutocompleteProps, "options">}
|
||||
options={options.map(f => [f, f])}
|
||||
/>;
|
||||
} else {
|
||||
return <InputDynamicArray
|
||||
{...commonProps as FieldDynamicArrayProps}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
type={this.props.fieldSpec.value as FieldDynamicArrayProps['type']}
|
||||
/>
|
||||
return <InputString
|
||||
{...commonProps as InputStringProps}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
default: return null
|
||||
case "color": return (
|
||||
<InputColor
|
||||
{...commonProps as InputColorProps}
|
||||
/>
|
||||
);
|
||||
case "boolean": return (
|
||||
<InputCheckbox
|
||||
{...commonProps as InputCheckboxProps}
|
||||
/>
|
||||
);
|
||||
case "array":
|
||||
if(this.props.fieldName === "text-font") {
|
||||
return <InputFont
|
||||
{...commonProps as InputFontProps}
|
||||
fonts={this.props.fieldSpec.values}
|
||||
/>;
|
||||
} else {
|
||||
if (this.props.fieldSpec.length) {
|
||||
return <InputArray
|
||||
{...commonProps as InputArrayProps}
|
||||
type={this.props.fieldSpec.value}
|
||||
length={this.props.fieldSpec.length}
|
||||
/>;
|
||||
} else {
|
||||
return <InputDynamicArray
|
||||
{...commonProps as InputDynamicArrayProps}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
type={this.props.fieldSpec.value as InputDynamicArrayProps["type"]}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
case "numberArray": return (
|
||||
<InputDynamicArray
|
||||
{...commonProps as InputDynamicArrayProps}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
type="number"
|
||||
value={(Array.isArray(this.props.value) ? this.props.value : [this.props.value]) as (string | number | undefined)[]}
|
||||
/>
|
||||
);
|
||||
case "colorArray": return (
|
||||
<InputDynamicArray
|
||||
{...commonProps as InputDynamicArrayProps}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
type="color"
|
||||
value={(Array.isArray(this.props.value) ? this.props.value : [this.props.value]) as (string | number | undefined)[]}
|
||||
/>
|
||||
);
|
||||
case "padding": return (
|
||||
<InputArray
|
||||
{...commonProps as InputArrayProps}
|
||||
type="number"
|
||||
value={(Array.isArray(this.props.value) ? this.props.value : [this.props.value]) as (string | number | undefined)[]}
|
||||
length={4}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
console.warn(`No proper field input for ${this.props.fieldName} type: ${this.props.fieldSpec?.type}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React from "react";
|
||||
|
||||
export type InputStringProps = {
|
||||
"data-wd-key"?: string
|
||||
@@ -11,26 +11,26 @@ export type InputStringProps = {
|
||||
required?: boolean
|
||||
disabled?: boolean
|
||||
spellCheck?: boolean
|
||||
'aria-label'?: string
|
||||
"aria-label"?: string
|
||||
title?: string
|
||||
};
|
||||
|
||||
type InputStringState = {
|
||||
editing: boolean
|
||||
value?: string
|
||||
}
|
||||
};
|
||||
|
||||
export default class InputString extends React.Component<InputStringProps, InputStringState> {
|
||||
static defaultProps = {
|
||||
onInput: () => {},
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props: InputStringProps) {
|
||||
super(props)
|
||||
super(props);
|
||||
this.state = {
|
||||
editing: false,
|
||||
value: props.value || ''
|
||||
}
|
||||
value: props.value || ""
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: Readonly<InputStringProps>, state: InputStringState) {
|
||||
@@ -47,17 +47,17 @@ export default class InputString extends React.Component<InputStringProps, Input
|
||||
let classes;
|
||||
|
||||
if(this.props.multi) {
|
||||
tag = "textarea"
|
||||
tag = "textarea";
|
||||
classes = [
|
||||
"maputnik-string",
|
||||
"maputnik-string--multi"
|
||||
]
|
||||
];
|
||||
}
|
||||
else {
|
||||
tag = "input"
|
||||
tag = "input";
|
||||
classes = [
|
||||
"maputnik-string"
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
if(this.props.disabled) {
|
||||
|
||||
@@ -1,57 +1,33 @@
|
||||
import React from 'react'
|
||||
import InputString from './InputString'
|
||||
import SmallError from './SmallError'
|
||||
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||
import { TFunction } from 'i18next';
|
||||
import React, { type JSX } from "react";
|
||||
import InputString from "./InputString";
|
||||
import SmallError from "./SmallError";
|
||||
import { Trans, type WithTranslation, withTranslation } from "react-i18next";
|
||||
import { type TFunction } from "i18next";
|
||||
import { ErrorType, validate } from "../libs/urlopen";
|
||||
|
||||
function validate(url: string, t: TFunction): JSX.Element | undefined {
|
||||
if (url === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
let error;
|
||||
const getProtocol = (url: string) => {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
return urlObj.protocol;
|
||||
}
|
||||
catch (_err) {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
const protocol = getProtocol(url);
|
||||
const isSsl = window.location.protocol === "https:";
|
||||
|
||||
if (!protocol) {
|
||||
if (isSsl) {
|
||||
error = (
|
||||
function errorTypeToJsx(errorType: ErrorType | undefined, t: TFunction): JSX.Element | undefined {
|
||||
switch (errorType) {
|
||||
case ErrorType.EmptyHttpsProtocol:
|
||||
return (
|
||||
<SmallError>
|
||||
<Trans t={t}>Must provide protocol: <code>https://</code></Trans>
|
||||
</SmallError>
|
||||
);
|
||||
} else {
|
||||
error = (
|
||||
case ErrorType.EmptyHttpOrHttpsProtocol:
|
||||
return (
|
||||
<SmallError>
|
||||
<Trans t={t}>Must provide protocol: <code>http://</code> or <code>https://</code></Trans>
|
||||
</SmallError>
|
||||
);
|
||||
}
|
||||
case ErrorType.CorsError:
|
||||
return (
|
||||
<SmallError>
|
||||
<Trans t={t}>CORS policy won't allow fetching resources served over http from https, use a <code>https://</code> domain</Trans>
|
||||
</SmallError>
|
||||
);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
else if (
|
||||
protocol &&
|
||||
protocol === "http:" &&
|
||||
window.location.protocol === "https:"
|
||||
) {
|
||||
error = (
|
||||
<SmallError>
|
||||
<Trans t={t}>
|
||||
CORS policy won't allow fetching resources served over http from https, use a <code>https://</code> domain
|
||||
</Trans>
|
||||
</SmallError>
|
||||
);
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
export type FieldUrlProps = {
|
||||
@@ -63,42 +39,42 @@ export type FieldUrlProps = {
|
||||
onInput?(...args: unknown[]): unknown
|
||||
multi?: boolean
|
||||
required?: boolean
|
||||
'aria-label'?: string
|
||||
"aria-label"?: string
|
||||
type?: string
|
||||
className?: string
|
||||
};
|
||||
|
||||
type FieldUrlInternalProps = FieldUrlProps & WithTranslation;
|
||||
type InputUrlInternalProps = FieldUrlProps & WithTranslation;
|
||||
|
||||
type FieldUrlState = {
|
||||
error?: React.ReactNode
|
||||
}
|
||||
type InputUrlState = {
|
||||
error?: ErrorType
|
||||
};
|
||||
|
||||
class FieldUrlInternal extends React.Component<FieldUrlInternalProps, FieldUrlState> {
|
||||
class InputUrlInternal extends React.Component<InputUrlInternalProps, InputUrlState> {
|
||||
static defaultProps = {
|
||||
onInput: () => {},
|
||||
}
|
||||
};
|
||||
|
||||
constructor (props: FieldUrlInternalProps) {
|
||||
constructor (props: InputUrlInternalProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
error: validate(props.value, props.t),
|
||||
error: validate(props.value),
|
||||
};
|
||||
}
|
||||
|
||||
onInput = (url: string) => {
|
||||
this.setState({
|
||||
error: validate(url, this.props.t),
|
||||
error: validate(url),
|
||||
});
|
||||
if (this.props.onInput) this.props.onInput(url);
|
||||
}
|
||||
};
|
||||
|
||||
onChange = (url: string) => {
|
||||
this.setState({
|
||||
error: validate(url, this.props.t),
|
||||
error: validate(url),
|
||||
});
|
||||
this.props.onChange(url);
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
return (
|
||||
@@ -107,13 +83,13 @@ class FieldUrlInternal extends React.Component<FieldUrlInternalProps, FieldUrlSt
|
||||
{...this.props}
|
||||
onInput={this.onInput}
|
||||
onChange={this.onChange}
|
||||
aria-label={this.props['aria-label']}
|
||||
aria-label={this.props["aria-label"]}
|
||||
/>
|
||||
{this.state.error}
|
||||
{errorTypeToJsx(this.state.error, this.props.t)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const FieldUrl = withTranslation()(FieldUrlInternal);
|
||||
export default FieldUrl;
|
||||
const InputUrl = withTranslation()(InputUrlInternal);
|
||||
export default InputUrl;
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
import React, {type JSX} from 'react'
|
||||
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
|
||||
import {Accordion} from 'react-accessible-accordion';
|
||||
import {MdMoreVert} from 'react-icons/md'
|
||||
import { IconContext } from 'react-icons'
|
||||
import {BackgroundLayerSpecification, LayerSpecification, SourceSpecification} from 'maplibre-gl';
|
||||
import {v8} from '@maplibre/maplibre-gl-style-spec';
|
||||
import React, {type JSX} from "react";
|
||||
import { Wrapper, Button, Menu, MenuItem } from "react-aria-menubutton";
|
||||
import {Accordion} from "react-accessible-accordion";
|
||||
import {MdMoreVert} from "react-icons/md";
|
||||
import { IconContext } from "react-icons";
|
||||
import {type BackgroundLayerSpecification, type LayerSpecification, type SourceSpecification} from "maplibre-gl";
|
||||
import {v8} from "@maplibre/maplibre-gl-style-spec";
|
||||
|
||||
import FieldJson from './FieldJson'
|
||||
import FilterEditor from './FilterEditor'
|
||||
import PropertyGroup from './PropertyGroup'
|
||||
import LayerEditorGroup from './LayerEditorGroup'
|
||||
import FieldType from './FieldType'
|
||||
import FieldId from './FieldId'
|
||||
import FieldMinZoom from './FieldMinZoom'
|
||||
import FieldMaxZoom from './FieldMaxZoom'
|
||||
import FieldComment from './FieldComment'
|
||||
import FieldSource from './FieldSource'
|
||||
import FieldSourceLayer from './FieldSourceLayer'
|
||||
import { changeType, changeProperty } from '../libs/layer'
|
||||
import {formatLayerId} from '../libs/format';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import { TFunction } from 'i18next';
|
||||
import { NON_SOURCE_LAYERS } from '../libs/non-source-layers';
|
||||
import { OnMoveLayerCallback } from '../libs/definitions';
|
||||
import FieldJson from "./FieldJson";
|
||||
import FilterEditor from "./FilterEditor";
|
||||
import PropertyGroup from "./PropertyGroup";
|
||||
import LayerEditorGroup from "./LayerEditorGroup";
|
||||
import FieldType from "./FieldType";
|
||||
import FieldId from "./FieldId";
|
||||
import FieldMinZoom from "./FieldMinZoom";
|
||||
import FieldMaxZoom from "./FieldMaxZoom";
|
||||
import FieldComment from "./FieldComment";
|
||||
import FieldSource from "./FieldSource";
|
||||
import FieldSourceLayer from "./FieldSourceLayer";
|
||||
import { changeType, changeProperty } from "../libs/layer";
|
||||
import {formatLayerId} from "../libs/format";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
import { type TFunction } from "i18next";
|
||||
import { NON_SOURCE_LAYERS } from "../libs/non-source-layers";
|
||||
import { type MappedError, type MappedLayerErrors, type OnMoveLayerCallback } from "../libs/definitions";
|
||||
|
||||
type MaputnikLayoutGroup = {
|
||||
id: string;
|
||||
title: string;
|
||||
type: string;
|
||||
fields: string[];
|
||||
}
|
||||
};
|
||||
|
||||
function getLayoutForSymbolType(t: TFunction): MaputnikLayoutGroup[] {
|
||||
const groups: MaputnikLayoutGroup[] = [];
|
||||
@@ -68,7 +68,7 @@ function getLayoutForSymbolType(t: TFunction): MaputnikLayoutGroup[] {
|
||||
|
||||
function getLayoutForType(type: LayerSpecification["type"], t: TFunction): MaputnikLayoutGroup[] {
|
||||
if (Object.keys(v8.layer.type.values).indexOf(type) < 0) {
|
||||
return []
|
||||
return [];
|
||||
}
|
||||
if (type === "symbol") {
|
||||
return getLayoutForSymbolType(t);
|
||||
@@ -95,31 +95,31 @@ function getLayoutForType(type: LayerSpecification["type"], t: TFunction): Maput
|
||||
|
||||
function layoutGroups(layerType: LayerSpecification["type"], t: TFunction): {id: string, title: string, type: string, fields?: string[]}[] {
|
||||
const layerGroup = {
|
||||
id: 'layer',
|
||||
title: t('Layer'),
|
||||
type: 'layer'
|
||||
}
|
||||
id: "layer",
|
||||
title: t("Layer"),
|
||||
type: "layer"
|
||||
};
|
||||
const filterGroup = {
|
||||
id: 'filter',
|
||||
title: t('Filter'),
|
||||
type: 'filter'
|
||||
}
|
||||
id: "filter",
|
||||
title: t("Filter"),
|
||||
type: "filter"
|
||||
};
|
||||
const editorGroup = {
|
||||
id: 'jsoneditor',
|
||||
title: t('JSON Editor'),
|
||||
type: 'jsoneditor'
|
||||
}
|
||||
id: "jsoneditor",
|
||||
title: t("JSON Editor"),
|
||||
type: "jsoneditor"
|
||||
};
|
||||
return [layerGroup, filterGroup]
|
||||
.concat(getLayoutForType(layerType, t))
|
||||
.concat([editorGroup])
|
||||
.concat([editorGroup]);
|
||||
}
|
||||
|
||||
type LayerEditorInternalProps = {
|
||||
layer: LayerSpecification
|
||||
sources: {[key: string]: SourceSpecification & {layers: string[]}}
|
||||
vectorLayers: {[key: string]: any}
|
||||
spec: object
|
||||
onLayerChanged(...args: unknown[]): unknown
|
||||
spec: any
|
||||
onLayerChanged(index: number, layer: LayerSpecification): void
|
||||
onLayerIdChange(...args: unknown[]): unknown
|
||||
onMoveLayer: OnMoveLayerCallback
|
||||
onLayerDestroy(...args: unknown[]): unknown
|
||||
@@ -128,7 +128,7 @@ type LayerEditorInternalProps = {
|
||||
isFirstLayer?: boolean
|
||||
isLastLayer?: boolean
|
||||
layerIndex: number
|
||||
errors?: any[]
|
||||
errors?: MappedError[]
|
||||
} & WithTranslation;
|
||||
|
||||
type LayerEditorState = {
|
||||
@@ -141,25 +141,25 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
onLayerChanged: () => {},
|
||||
onLayerIdChange: () => {},
|
||||
onLayerDestroyed: () => {},
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props: LayerEditorInternalProps) {
|
||||
super(props)
|
||||
super(props);
|
||||
|
||||
const editorGroups: {[keys:string]: boolean} = {}
|
||||
const editorGroups: {[keys:string]: boolean} = {};
|
||||
for (const group of layoutGroups(this.props.layer.type, props.t)) {
|
||||
editorGroups[group.title] = true
|
||||
editorGroups[group.title] = true;
|
||||
}
|
||||
|
||||
this.state = { editorGroups }
|
||||
this.state = { editorGroups };
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: Readonly<LayerEditorInternalProps>, state: LayerEditorState) {
|
||||
const additionalGroups = { ...state.editorGroups }
|
||||
const additionalGroups = { ...state.editorGroups };
|
||||
|
||||
for (const group of getLayoutForType(props.layer.type, props.t)) {
|
||||
if(!(group.title in additionalGroups)) {
|
||||
additionalGroups[group.title] = true
|
||||
additionalGroups[group.title] = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,27 +173,27 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
this.props.onLayerChanged(
|
||||
this.props.layerIndex,
|
||||
changeProperty(this.props.layer, group, property, newValue)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
onGroupToggle(groupTitle: string, active: boolean) {
|
||||
const changedActiveGroups = {
|
||||
...this.state.editorGroups,
|
||||
[groupTitle]: active,
|
||||
}
|
||||
};
|
||||
this.setState({
|
||||
editorGroups: changedActiveGroups
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
renderGroupType(type: string, fields?: string[]): JSX.Element {
|
||||
let comment = ""
|
||||
let comment = "";
|
||||
if(this.props.layer.metadata) {
|
||||
comment = (this.props.layer.metadata as any)['maputnik:comment']
|
||||
comment = (this.props.layer.metadata as any)["maputnik:comment"];
|
||||
}
|
||||
const {errors, layerIndex} = this.props;
|
||||
|
||||
const errorData: {[key in LayerSpecification as string]: {message: string}} = {};
|
||||
const errorData: MappedLayerErrors = {};
|
||||
errors!.forEach(error => {
|
||||
if (
|
||||
error.parsed &&
|
||||
@@ -204,7 +204,7 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
message: error.parsed.data.message
|
||||
};
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let sourceLayerIds;
|
||||
const layer = this.props.layer as Exclude<LayerSpecification, BackgroundLayerSpecification>;
|
||||
@@ -213,82 +213,83 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
}
|
||||
|
||||
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)}
|
||||
/>
|
||||
}
|
||||
{!NON_SOURCE_LAYERS.includes(this.props.layer.type) &&
|
||||
<FieldSourceLayer
|
||||
error={errorData['source-layer']}
|
||||
sourceLayerIds={sourceLayerIds}
|
||||
value={(this.props.layer as any)['source-layer']}
|
||||
onChange={v => this.changeProperty(null, 'source-layer', v)}
|
||||
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)}
|
||||
/>
|
||||
}
|
||||
<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 as any).filter}
|
||||
properties={this.props.vectorLayers[(this.props.layer as any)['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(
|
||||
<FieldType
|
||||
disabled={true}
|
||||
error={errorData.type}
|
||||
value={this.props.layer.type}
|
||||
onChange={newType => this.props.onLayerChanged(
|
||||
this.props.layerIndex,
|
||||
layer
|
||||
);
|
||||
}}
|
||||
/>
|
||||
default: return <></>
|
||||
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)}
|
||||
/>
|
||||
}
|
||||
{!NON_SOURCE_LAYERS.includes(this.props.layer.type) &&
|
||||
<FieldSourceLayer
|
||||
error={errorData["source-layer"]}
|
||||
sourceLayerIds={sourceLayerIds}
|
||||
value={(this.props.layer as any)["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 as any).filter}
|
||||
properties={this.props.vectorLayers[(this.props.layer as any)["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
|
||||
lintType="layer"
|
||||
value={this.props.layer}
|
||||
onChange={(layer: LayerSpecification) => {
|
||||
this.props.onLayerChanged(
|
||||
this.props.layerIndex,
|
||||
layer
|
||||
);
|
||||
}}
|
||||
/>;
|
||||
default: return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,16 +297,16 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
this.props.onMoveLayer({
|
||||
oldIndex: this.props.layerIndex,
|
||||
newIndex: this.props.layerIndex+offset
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const groupIds: string[] = [];
|
||||
const layerType = this.props.layer.type
|
||||
const layerType = this.props.layer.type;
|
||||
const groups = layoutGroups(layerType, t).filter(group => {
|
||||
return !(layerType === 'background' && group.type === 'source')
|
||||
return !(layerType === "background" && group.type === "source");
|
||||
}).map(group => {
|
||||
const groupId = group.id;
|
||||
groupIds.push(groupId);
|
||||
@@ -318,10 +319,10 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
onActiveToggle={this.onGroupToggle.bind(this, group.title)}
|
||||
>
|
||||
{this.renderGroupType(group.type, group.fields)}
|
||||
</LayerEditorGroup>
|
||||
})
|
||||
</LayerEditorGroup>;
|
||||
});
|
||||
|
||||
const layout = this.props.layer.layout || {}
|
||||
const layout = this.props.layer.layout || {};
|
||||
|
||||
const items: {[key: string]: {
|
||||
text: string,
|
||||
@@ -356,19 +357,20 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
handler: () => this.moveLayer(+1),
|
||||
wdKey: "menu-move-layer-down"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function handleSelection(id: string, event: React.SyntheticEvent) {
|
||||
event.stopPropagation();
|
||||
items[id].handler();
|
||||
}
|
||||
|
||||
return <IconContext.Provider value={{size: '14px', color: '#8e8e8e'}}>
|
||||
return <IconContext.Provider value={{size: "14px", color: "#8e8e8e"}}>
|
||||
<section className="maputnik-layer-editor"
|
||||
role="main"
|
||||
aria-label={t("Layer editor")}
|
||||
data-wd-key="layer-editor"
|
||||
>
|
||||
<header>
|
||||
<header data-wd-key="layer-editor.header">
|
||||
<div className="layer-header">
|
||||
<h2 className="layer-header__title">
|
||||
{t("Layer: {{layerId}}", { layerId: formatLayerId(this.props.layer.id) })}
|
||||
@@ -394,7 +396,7 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
<MenuItem value={id} className='more-menu__menu__item' data-wd-key={item.wdKey}>
|
||||
{item.text}
|
||||
</MenuItem>
|
||||
</li>
|
||||
</li>;
|
||||
})}
|
||||
</ul>
|
||||
</Menu>
|
||||
@@ -411,7 +413,7 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
|
||||
{groups}
|
||||
</Accordion>
|
||||
</section>
|
||||
</IconContext.Provider>
|
||||
</IconContext.Provider>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import React from 'react'
|
||||
import Icon from '@mdi/react'
|
||||
import {
|
||||
mdiMenuDown,
|
||||
mdiMenuUp
|
||||
} from '@mdi/js';
|
||||
import React from "react";
|
||||
import {MdArrowDropDown, MdArrowDropUp} from "react-icons/md";
|
||||
import {
|
||||
AccordionItem,
|
||||
AccordionItemHeading,
|
||||
AccordionItemButton,
|
||||
AccordionItemPanel,
|
||||
} from 'react-accessible-accordion';
|
||||
} from "react-accessible-accordion";
|
||||
|
||||
|
||||
type LayerEditorGroupProps = {
|
||||
@@ -30,22 +26,14 @@ export default class LayerEditorGroup extends React.Component<LayerEditorGroupPr
|
||||
onClick={_e => this.props.onActiveToggle(!this.props.isActive)}
|
||||
>
|
||||
<AccordionItemButton className="maputnik-layer-editor-group__button">
|
||||
<span style={{flexGrow: 1}}>{this.props.title}</span>
|
||||
<Icon
|
||||
path={mdiMenuUp}
|
||||
size={1}
|
||||
className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--up"
|
||||
/>
|
||||
<Icon
|
||||
path={mdiMenuDown}
|
||||
size={1}
|
||||
className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--down"
|
||||
/>
|
||||
<span style={{flexGrow: 1, alignContent: "center"}}>{this.props.title}</span>
|
||||
<MdArrowDropUp size={"2em"} className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--up"></MdArrowDropUp>
|
||||
<MdArrowDropDown size={"2em"} className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--down"></MdArrowDropDown>
|
||||
</AccordionItemButton>
|
||||
</AccordionItemHeading>
|
||||
<AccordionItemPanel>
|
||||
{this.props.children}
|
||||
</AccordionItemPanel>
|
||||
</AccordionItem>
|
||||
</AccordionItem>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
import React, {type JSX} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import lodash from 'lodash';
|
||||
import React, {type JSX} from "react";
|
||||
import classnames from "classnames";
|
||||
import lodash from "lodash";
|
||||
import {
|
||||
DndContext,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
closestCenter,
|
||||
DragEndEvent,
|
||||
} from '@dnd-kit/core';
|
||||
type DragEndEvent,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
verticalListSortingStrategy,
|
||||
} from '@dnd-kit/sortable';
|
||||
} from "@dnd-kit/sortable";
|
||||
|
||||
import LayerListGroup from './LayerListGroup'
|
||||
import LayerListItem from './LayerListItem'
|
||||
import ModalAdd from './ModalAdd'
|
||||
import LayerListGroup from "./LayerListGroup";
|
||||
import LayerListItem from "./LayerListItem";
|
||||
import ModalAdd from "./modals/ModalAdd";
|
||||
|
||||
import type {LayerSpecification, SourceSpecification} from 'maplibre-gl';
|
||||
import generateUniqueId from '../libs/document-uid';
|
||||
import { findClosestCommonPrefix, layerPrefix } from '../libs/layer';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import { OnMoveLayerCallback } from '../libs/definitions';
|
||||
import type {LayerSpecification, SourceSpecification} from "maplibre-gl";
|
||||
import generateUniqueId from "../libs/document-uid";
|
||||
import { findClosestCommonPrefix, layerPrefix } from "../libs/layer";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
import { type MappedError, type OnMoveLayerCallback } from "../libs/definitions";
|
||||
|
||||
type LayerListContainerProps = {
|
||||
layers: LayerSpecification[]
|
||||
selectedLayerIndex: number
|
||||
onLayersChange(layers: LayerSpecification[]): unknown
|
||||
onLayerSelect(...args: unknown[]): unknown
|
||||
onLayerSelect(index: number): void;
|
||||
onLayerDestroy?(...args: unknown[]): unknown
|
||||
onLayerCopy(...args: unknown[]): unknown
|
||||
onLayerVisibilityToggle(...args: unknown[]): unknown
|
||||
sources: Record<string, SourceSpecification & {layers: string[]}>;
|
||||
errors: any[]
|
||||
errors: MappedError[]
|
||||
};
|
||||
type LayerListContainerInternalProps = LayerListContainerProps & WithTranslation;
|
||||
|
||||
@@ -48,9 +48,9 @@ type LayerListContainerState = {
|
||||
class LayerListContainerInternal extends React.Component<LayerListContainerInternalProps, LayerListContainerState> {
|
||||
static defaultProps = {
|
||||
onLayerSelect: () => {},
|
||||
}
|
||||
};
|
||||
selectedItemRef: React.RefObject<any>;
|
||||
scrollContainerRef: React.RefObject<HTMLElement>;
|
||||
scrollContainerRef: React.RefObject<HTMLElement | null>;
|
||||
|
||||
constructor(props: LayerListContainerInternalProps) {
|
||||
super(props);
|
||||
@@ -65,7 +65,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
||||
isOpen: {
|
||||
add: false,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
toggleModal(modalName: string) {
|
||||
@@ -78,74 +78,74 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
||||
...this.state.isOpen,
|
||||
[modalName]: !this.state.isOpen[modalName]
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
toggleLayers = () => {
|
||||
let idx = 0
|
||||
let idx = 0;
|
||||
|
||||
const newGroups: {[key:string]: boolean} = {}
|
||||
const newGroups: {[key:string]: boolean} = {};
|
||||
|
||||
this.groupedLayers().forEach(layers => {
|
||||
const groupPrefix = layerPrefix(layers[0].id)
|
||||
const lookupKey = [groupPrefix, idx].join('-')
|
||||
const groupPrefix = layerPrefix(layers[0].id);
|
||||
const lookupKey = [groupPrefix, idx].join("-");
|
||||
|
||||
|
||||
if (layers.length > 1) {
|
||||
newGroups[lookupKey] = this.state.areAllGroupsExpanded
|
||||
newGroups[lookupKey] = this.state.areAllGroupsExpanded;
|
||||
}
|
||||
|
||||
layers.forEach((_layer) => {
|
||||
idx += 1
|
||||
})
|
||||
idx += 1;
|
||||
});
|
||||
});
|
||||
|
||||
this.setState({
|
||||
collapsedGroups: newGroups,
|
||||
areAllGroupsExpanded: !this.state.areAllGroupsExpanded
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
groupedLayers(): (LayerSpecification & {key: string})[][] {
|
||||
const groups = []
|
||||
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]
|
||||
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)
|
||||
const lastGroup = groups[groups.length - 1];
|
||||
lastGroup.push(layer);
|
||||
} else {
|
||||
groups.push([layer])
|
||||
groups.push([layer]);
|
||||
}
|
||||
}
|
||||
return groups
|
||||
return groups;
|
||||
}
|
||||
|
||||
toggleLayerGroup(groupPrefix: string, idx: number) {
|
||||
const lookupKey = [groupPrefix, idx].join('-')
|
||||
const newGroups = { ...this.state.collapsedGroups }
|
||||
const lookupKey = [groupPrefix, idx].join("-");
|
||||
const newGroups = { ...this.state.collapsedGroups };
|
||||
if(lookupKey in this.state.collapsedGroups) {
|
||||
newGroups[lookupKey] = !this.state.collapsedGroups[lookupKey]
|
||||
newGroups[lookupKey] = !this.state.collapsedGroups[lookupKey];
|
||||
} else {
|
||||
newGroups[lookupKey] = false
|
||||
newGroups[lookupKey] = false;
|
||||
}
|
||||
this.setState({
|
||||
collapsedGroups: newGroups
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
isCollapsed(groupPrefix: string, idx: number) {
|
||||
const collapsed = this.state.collapsedGroups[[groupPrefix, idx].join('-')]
|
||||
return collapsed === undefined ? true : collapsed
|
||||
const collapsed = this.state.collapsedGroups[[groupPrefix, idx].join("-")];
|
||||
return collapsed === undefined ? true : collapsed;
|
||||
}
|
||||
|
||||
shouldComponentUpdate (nextProps: LayerListContainerProps, nextState: LayerListContainerState) {
|
||||
@@ -177,7 +177,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
||||
const out = {
|
||||
...props
|
||||
} as LayerListContainerProps & { layers?: any };
|
||||
delete out['layers'];
|
||||
delete out["layers"];
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
||||
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) {
|
||||
@@ -215,25 +215,25 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
||||
|
||||
render() {
|
||||
|
||||
const listItems: JSX.Element[] = []
|
||||
let idx = 0
|
||||
const listItems: JSX.Element[] = [];
|
||||
let idx = 0;
|
||||
const layersByGroup = this.groupedLayers();
|
||||
layersByGroup.forEach(layers => {
|
||||
const groupPrefix = layerPrefix(layers[0].id)
|
||||
const groupPrefix = layerPrefix(layers[0].id);
|
||||
if(layers.length > 1) {
|
||||
const grp = <LayerListGroup
|
||||
data-wd-key={[groupPrefix, idx].join('-')}
|
||||
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)
|
||||
/>;
|
||||
listItems.push(grp);
|
||||
}
|
||||
|
||||
layers.forEach((layer, idxInGroup) => {
|
||||
const groupIdx = findClosestCommonPrefix(this.props.layers, idx)
|
||||
const groupIdx = findClosestCommonPrefix(this.props.layers, idx);
|
||||
|
||||
const layerError = this.props.errors.find(error => {
|
||||
return (
|
||||
@@ -250,9 +250,9 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
||||
|
||||
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
|
||||
"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
|
||||
})}
|
||||
key={layer.key}
|
||||
id={layer.key}
|
||||
@@ -266,16 +266,17 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
||||
onLayerCopy={this.props.onLayerCopy.bind(this)}
|
||||
onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(this)}
|
||||
{...additionalProps}
|
||||
/>
|
||||
listItems.push(listItem)
|
||||
idx += 1
|
||||
})
|
||||
})
|
||||
/>;
|
||||
listItems.push(listItem);
|
||||
idx += 1;
|
||||
});
|
||||
});
|
||||
|
||||
const t = this.props.t;
|
||||
|
||||
return <section
|
||||
className="maputnik-layer-list"
|
||||
data-wd-key="layer-list"
|
||||
role="complementary"
|
||||
aria-label={t("Layers list")}
|
||||
ref={this.scrollContainerRef}
|
||||
@@ -285,10 +286,10 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
||||
layers={this.props.layers}
|
||||
sources={this.props.sources}
|
||||
isOpen={this.state.isOpen.add}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'add')}
|
||||
onOpenToggle={this.toggleModal.bind(this, "add")}
|
||||
onLayersChange={this.props.onLayersChange}
|
||||
/>
|
||||
<header className="maputnik-layer-list-header">
|
||||
<header className="maputnik-layer-list-header" data-wd-key="layer-list.header">
|
||||
<span className="maputnik-layer-list-header-title">{t("Layers")}</span>
|
||||
<span className="maputnik-space" />
|
||||
<div className="maputnik-default-property">
|
||||
@@ -309,7 +310,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
||||
<div className="maputnik-default-property">
|
||||
<div className="maputnik-multibutton">
|
||||
<button
|
||||
onClick={this.toggleModal.bind(this, 'add')}
|
||||
onClick={this.toggleModal.bind(this, "add")}
|
||||
data-wd-key="layer-list:add-layer"
|
||||
className="maputnik-button maputnik-button-selected">
|
||||
{t("Add Layer")}
|
||||
@@ -325,7 +326,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
|
||||
{listItems}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</section>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,7 +337,7 @@ type LayerListProps = LayerListContainerProps & {
|
||||
};
|
||||
|
||||
const LayerList: React.FC<LayerListProps> = (props) => {
|
||||
const sensors = useSensors(useSensor(PointerSensor));
|
||||
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }));
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const {active, over} = event;
|
||||
@@ -359,6 +360,6 @@ const LayerList: React.FC<LayerListProps> = (props) => {
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default LayerList;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react'
|
||||
import Collapser from './Collapser'
|
||||
import React from "react";
|
||||
import Collapser from "./Collapser";
|
||||
|
||||
type LayerListGroupProps = {
|
||||
title: string
|
||||
"data-wd-key"?: string
|
||||
isActive: boolean
|
||||
onActiveToggle(...args: unknown[]): unknown
|
||||
'aria-controls'?: string
|
||||
"aria-controls"?: string
|
||||
};
|
||||
|
||||
export default class LayerListGroup extends React.Component<LayerListGroupProps> {
|
||||
@@ -18,7 +18,7 @@ export default class LayerListGroup extends React.Component<LayerListGroupProps>
|
||||
>
|
||||
<button
|
||||
className="maputnik-layer-list-group-title"
|
||||
aria-controls={this.props['aria-controls']}
|
||||
aria-controls={this.props["aria-controls"]}
|
||||
aria-expanded={this.props.isActive}
|
||||
>
|
||||
{this.props.title}
|
||||
@@ -29,6 +29,6 @@ export default class LayerListGroup extends React.Component<LayerListGroupProps>
|
||||
isCollapsed={this.props.isActive}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</li>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from 'react-icons/md'
|
||||
import { IconContext } from 'react-icons'
|
||||
import {useSortable} from '@dnd-kit/sortable'
|
||||
import {CSS} from '@dnd-kit/utilities'
|
||||
import React from "react";
|
||||
import classnames from "classnames";
|
||||
import {MdContentCopy, MdVisibility, MdVisibilityOff, MdDelete} from "react-icons/md";
|
||||
import { IconContext } from "react-icons";
|
||||
import {useSortable} from "@dnd-kit/sortable";
|
||||
import {CSS} from "@dnd-kit/utilities";
|
||||
|
||||
import IconLayer from './IconLayer'
|
||||
import IconLayer from "./IconLayer";
|
||||
|
||||
|
||||
type DraggableLabelProps = {
|
||||
@@ -21,11 +21,12 @@ const DraggableLabel: React.FC<DraggableLabelProps> = (props) => {
|
||||
<IconLayer
|
||||
className="layer-handle__icon"
|
||||
type={props.layerType}
|
||||
style={{ width: "1em", height: "1em", verticalAlign: "middle" }}
|
||||
/>
|
||||
<button className="maputnik-layer-list-item-id">
|
||||
{props.layerId}
|
||||
</button>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
type IconActionProps = {
|
||||
@@ -39,17 +40,17 @@ type IconActionProps = {
|
||||
class IconAction extends React.Component<IconActionProps> {
|
||||
renderIcon() {
|
||||
switch(this.props.action) {
|
||||
case 'duplicate': return <MdContentCopy />
|
||||
case 'show': return <MdVisibility />
|
||||
case 'hide': return <MdVisibilityOff />
|
||||
case 'delete': return <MdDelete />
|
||||
case "duplicate": return <MdContentCopy />;
|
||||
case "show": return <MdVisibility />;
|
||||
case "hide": return <MdVisibilityOff />;
|
||||
case "delete": return <MdDelete />;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classBlockName, classBlockModifier} = this.props;
|
||||
|
||||
let classAdditions = '';
|
||||
let classAdditions = "";
|
||||
if (classBlockName) {
|
||||
classAdditions = `maputnik-layer-list-icon-action__${classBlockName}`;
|
||||
|
||||
@@ -67,7 +68,7 @@ class IconAction extends React.Component<IconActionProps> {
|
||||
aria-hidden="true"
|
||||
>
|
||||
{this.renderIcon()}
|
||||
</button>
|
||||
</button>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +80,7 @@ type LayerListItemProps = {
|
||||
isSelected?: boolean
|
||||
visibility?: string
|
||||
className?: string
|
||||
onLayerSelect(...args: unknown[]): unknown
|
||||
onLayerSelect(index: number): void;
|
||||
onLayerCopy?(...args: unknown[]): unknown
|
||||
onLayerDestroy?(...args: unknown[]): unknown
|
||||
onLayerVisibilityToggle?(...args: unknown[]): unknown
|
||||
@@ -88,7 +89,7 @@ type LayerListItemProps = {
|
||||
const LayerListItem = React.forwardRef<HTMLLIElement, LayerListItemProps>((props, ref) => {
|
||||
const {
|
||||
isSelected = false,
|
||||
visibility = 'visible',
|
||||
visibility = "visible",
|
||||
onLayerCopy = () => {},
|
||||
onLayerDestroy = () => {},
|
||||
onLayerVisibilityToggle = () => {},
|
||||
@@ -109,12 +110,12 @@ const LayerListItem = React.forwardRef<HTMLLIElement, LayerListItemProps>((props
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
|
||||
const visibilityAction = visibility === 'visible' ? 'show' : 'hide';
|
||||
const visibilityAction = visibility === "visible" ? "show" : "hide";
|
||||
|
||||
// Cast ref to MutableRefObject since we know from the codebase that's what's always passed
|
||||
const refObject = ref as React.MutableRefObject<HTMLLIElement | null> | null;
|
||||
|
||||
return <IconContext.Provider value={{size: '14px'}}>
|
||||
return <IconContext.Provider value={{size: "14px"}}>
|
||||
<li
|
||||
ref={(node) => {
|
||||
setNodeRef(node);
|
||||
@@ -140,13 +141,13 @@ const LayerListItem = React.forwardRef<HTMLLIElement, LayerListItemProps>((props
|
||||
<span style={{flexGrow: 1}} />
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:" + props.layerId+":delete"}
|
||||
action={'delete'}
|
||||
action={"delete"}
|
||||
classBlockName="delete"
|
||||
onClick={_e => onLayerDestroy!(props.layerIndex)}
|
||||
/>
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:" + props.layerId+":copy"}
|
||||
action={'duplicate'}
|
||||
action={"duplicate"}
|
||||
classBlockName="duplicate"
|
||||
onClick={_e => onLayerCopy!(props.layerIndex)}
|
||||
/>
|
||||
@@ -158,7 +159,7 @@ const LayerListItem = React.forwardRef<HTMLLIElement, LayerListItemProps>((props
|
||||
onClick={_e => onLayerVisibilityToggle!(props.layerIndex)}
|
||||
/>
|
||||
</li>
|
||||
</IconContext.Provider>
|
||||
</IconContext.Provider>;
|
||||
});
|
||||
|
||||
export default LayerListItem;
|
||||
|
||||
@@ -1,67 +1,56 @@
|
||||
import React, {type JSX} from 'react'
|
||||
import {createRoot} from 'react-dom/client'
|
||||
import MapLibreGl, {LayerSpecification, LngLat, Map, MapOptions, SourceSpecification, StyleSpecification} from 'maplibre-gl'
|
||||
import MaplibreInspect from '@maplibre/maplibre-gl-inspect'
|
||||
import colors from '@maplibre/maplibre-gl-inspect/lib/colors'
|
||||
import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup'
|
||||
import MapMaplibreGlFeaturePropertyPopup, { InspectFeature } from './MapMaplibreGlFeaturePropertyPopup'
|
||||
import Color from 'color'
|
||||
import ZoomControl from '../libs/zoomcontrol'
|
||||
import { HighlightedLayer, colorHighlightedLayer } from '../libs/highlight'
|
||||
import 'maplibre-gl/dist/maplibre-gl.css'
|
||||
import '../maplibregl.css'
|
||||
import '../libs/maplibre-rtl'
|
||||
import MaplibreGeocoder, { MaplibreGeocoderApi, MaplibreGeocoderApiConfig } from '@maplibre/maplibre-gl-geocoder';
|
||||
import '@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css';
|
||||
import { withTranslation, WithTranslation } from 'react-i18next'
|
||||
import i18next from 'i18next'
|
||||
import React from "react";
|
||||
import {createRoot} from "react-dom/client";
|
||||
import MapLibreGl, {type LayerSpecification, type LngLat, type Map, type MapOptions, type SourceSpecification, type StyleSpecification} from "maplibre-gl";
|
||||
import MaplibreInspect from "@maplibre/maplibre-gl-inspect";
|
||||
import colors from "@maplibre/maplibre-gl-inspect/lib/colors";
|
||||
import MapMaplibreGlLayerPopup from "./MapMaplibreGlLayerPopup";
|
||||
import MapMaplibreGlFeaturePropertyPopup, { type InspectFeature } from "./MapMaplibreGlFeaturePropertyPopup";
|
||||
import Color from "color";
|
||||
import ZoomControl from "../libs/zoomcontrol";
|
||||
import { type HighlightedLayer, colorHighlightedLayer } from "../libs/highlight";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
import "../maplibregl.css";
|
||||
import "../libs/maplibre-rtl";
|
||||
import MaplibreGeocoder, { type MaplibreGeocoderApi, type MaplibreGeocoderApiConfig } from "@maplibre/maplibre-gl-geocoder";
|
||||
import "@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css";
|
||||
import { withTranslation, type WithTranslation } from "react-i18next";
|
||||
import i18next from "i18next";
|
||||
import { Protocol } from "pmtiles";
|
||||
|
||||
function renderPopup(
|
||||
popupElement: JSX.Element,
|
||||
mountNode: HTMLElement,
|
||||
popup: MapLibreGl.Popup
|
||||
): HTMLElement {
|
||||
const root = createRoot(mountNode);
|
||||
popup.once('close', () => root.unmount());
|
||||
root.render(popupElement);
|
||||
return mountNode as HTMLElement;
|
||||
}
|
||||
|
||||
function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[], highlightedLayer?: HighlightedLayer) {
|
||||
const backgroundLayer = {
|
||||
"id": "background",
|
||||
"type": "background",
|
||||
"paint": {
|
||||
"background-color": '#1c1f24',
|
||||
"background-color": "#1c1f24",
|
||||
}
|
||||
} as LayerSpecification
|
||||
} as LayerSpecification;
|
||||
|
||||
const layer = colorHighlightedLayer(highlightedLayer)
|
||||
const layer = colorHighlightedLayer(highlightedLayer);
|
||||
if(layer) {
|
||||
coloredLayers.push(layer)
|
||||
coloredLayers.push(layer);
|
||||
}
|
||||
|
||||
const sources: {[key:string]: SourceSpecification} = {}
|
||||
const sources: {[key:string]: SourceSpecification} = {};
|
||||
|
||||
Object.keys(originalMapStyle.sources).forEach(sourceId => {
|
||||
const source = originalMapStyle.sources[sourceId]
|
||||
if(source.type !== 'raster' && source.type !== 'raster-dem') {
|
||||
sources[sourceId] = source
|
||||
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 as LayerSpecification[])
|
||||
}
|
||||
return inspectStyle
|
||||
};
|
||||
return inspectStyle;
|
||||
}
|
||||
|
||||
type MapMaplibreGlInternalProps = {
|
||||
onDataChange?(event: {map: Map | null}): unknown
|
||||
onLayerSelect(...args: unknown[]): unknown
|
||||
onLayerSelect(index: number): void
|
||||
mapStyle: StyleSpecification
|
||||
inspectModeEnabled: boolean
|
||||
highlightedLayer?: HighlightedLayer
|
||||
@@ -89,20 +78,20 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
|
||||
onLayerSelect: () => {},
|
||||
onChange: () => {},
|
||||
options: {} as MapOptions,
|
||||
}
|
||||
container: HTMLDivElement | null = null
|
||||
};
|
||||
container: HTMLDivElement | null = null;
|
||||
|
||||
constructor(props: MapMaplibreGlInternalProps) {
|
||||
super(props)
|
||||
super(props);
|
||||
this.state = {
|
||||
map: null,
|
||||
inspect: null,
|
||||
geocoder: null,
|
||||
zoomControl: null,
|
||||
}
|
||||
i18next.on('languageChanged', () => {
|
||||
};
|
||||
i18next.on("languageChanged", () => {
|
||||
this.forceUpdate();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -131,7 +120,7 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
|
||||
}
|
||||
|
||||
if(this.state.inspect && this.props.inspectModeEnabled !== this.state.inspect._showInspectMap) {
|
||||
this.state.inspect.toggleInspector()
|
||||
this.state.inspect.toggleInspector();
|
||||
}
|
||||
if (this.state.inspect && this.props.inspectModeEnabled) {
|
||||
this.state.inspect.setOriginalStyle(styleWithTokens);
|
||||
@@ -163,7 +152,7 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
|
||||
const center = map.getCenter();
|
||||
const zoom = map.getZoom();
|
||||
this.props.onChange({center, zoom});
|
||||
}
|
||||
};
|
||||
mapViewChange();
|
||||
|
||||
map.showTileBoundaries = mapOpts.showTileBoundaries!;
|
||||
@@ -173,12 +162,13 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
|
||||
const geocoder = this.initGeocoder(map);
|
||||
|
||||
const zoomControl = new ZoomControl();
|
||||
map.addControl(zoomControl, 'top-right');
|
||||
map.addControl(zoomControl, "top-right");
|
||||
|
||||
const nav = new MapLibreGl.NavigationControl({visualizePitch:true});
|
||||
map.addControl(nav, 'top-right');
|
||||
map.addControl(nav, "top-right");
|
||||
|
||||
const tmpNode = document.createElement('div');
|
||||
const tmpNode = document.createElement("div");
|
||||
const root = createRoot(tmpNode);
|
||||
|
||||
const inspectPopup = new MapLibreGl.Popup({
|
||||
closeOnClick: false
|
||||
@@ -192,30 +182,28 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
|
||||
showInspectButton: false,
|
||||
blockHoverPopupOnClick: true,
|
||||
assignLayerColor: (layerId: string, alpha: number) => {
|
||||
return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string()
|
||||
return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string();
|
||||
},
|
||||
buildInspectStyle: (originalMapStyle: StyleSpecification, coloredLayers: HighlightedLayer[]) => buildInspectStyle(originalMapStyle, coloredLayers, this.props.highlightedLayer),
|
||||
renderPopup: (features: InspectFeature[]) => {
|
||||
if(this.props.inspectModeEnabled) {
|
||||
return renderPopup(
|
||||
<MapMaplibreGlFeaturePropertyPopup features={features} />,
|
||||
tmpNode,
|
||||
inspectPopup
|
||||
);
|
||||
inspectPopup.once("open", () => {
|
||||
root.render(<MapMaplibreGlFeaturePropertyPopup features={features} />);
|
||||
});
|
||||
return tmpNode;
|
||||
} else {
|
||||
return renderPopup(
|
||||
<MapMaplibreGlLayerPopup
|
||||
inspectPopup.once("open", () => {
|
||||
root.render(<MapMaplibreGlLayerPopup
|
||||
features={features}
|
||||
onLayerSelect={this.onLayerSelectById}
|
||||
zoom={this.state.zoom}
|
||||
/>,
|
||||
tmpNode,
|
||||
inspectPopup
|
||||
);
|
||||
/>,);
|
||||
});
|
||||
return tmpNode;
|
||||
}
|
||||
}
|
||||
})
|
||||
map.addControl(inspect)
|
||||
});
|
||||
map.addControl(inspect);
|
||||
|
||||
map.on("style.load", () => {
|
||||
this.setState({
|
||||
@@ -225,18 +213,18 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
|
||||
zoomControl,
|
||||
zoom: map.getZoom()
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
map.on("data", e => {
|
||||
if(e.dataType !== 'tile') return
|
||||
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({
|
||||
@@ -251,7 +239,7 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
|
||||
onLayerSelectById = (id: string) => {
|
||||
const index = this.props.mapStyle.layers.findIndex(layer => layer.id === id);
|
||||
this.props.onLayerSelect(index);
|
||||
}
|
||||
};
|
||||
|
||||
initGeocoder(map: Map) {
|
||||
const geocoderConfig = {
|
||||
@@ -269,15 +257,15 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
|
||||
(feature.bbox[3] - feature.bbox[1]) / 2
|
||||
];
|
||||
const point = {
|
||||
type: 'Feature',
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
type: "Point",
|
||||
coordinates: center
|
||||
},
|
||||
place_name: feature.properties.display_name,
|
||||
properties: feature.properties,
|
||||
text: feature.properties.display_name,
|
||||
place_type: ['place'],
|
||||
place_type: ["place"],
|
||||
center
|
||||
};
|
||||
features.push(point);
|
||||
@@ -294,7 +282,7 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
|
||||
placeholder: this.props.t("Search"),
|
||||
maplibregl: MapLibreGl,
|
||||
});
|
||||
map.addControl(geocoder, 'top-left');
|
||||
map.addControl(geocoder, "top-left");
|
||||
return geocoder;
|
||||
}
|
||||
|
||||
@@ -306,9 +294,9 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
|
||||
className="maputnik-map__map"
|
||||
role="region"
|
||||
aria-label={t("Map view")}
|
||||
ref={x => this.container = x}
|
||||
ref={x => {this.container = x;}}
|
||||
data-wd-key="maplibre:map"
|
||||
></div>
|
||||
></div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react'
|
||||
import type { GeoJSONFeatureWithSourceLayer } from '@maplibre/maplibre-gl-inspect'
|
||||
import React from "react";
|
||||
import type { GeoJSONFeatureWithSourceLayer } from "@maplibre/maplibre-gl-inspect";
|
||||
|
||||
export type InspectFeature = GeoJSONFeatureWithSourceLayer & {
|
||||
inspectModeCounter?: number
|
||||
counter?: number
|
||||
}
|
||||
};
|
||||
|
||||
function displayValue(value: string | number | Date | object | undefined) {
|
||||
if (typeof value === 'undefined' || value === null) return value;
|
||||
if (typeof value === "undefined" || value === null) return value;
|
||||
if (value instanceof Date) return value.toLocaleString();
|
||||
if (typeof value === 'object' ||
|
||||
typeof value === 'number' ||
|
||||
typeof value === 'string') return value.toString();
|
||||
if (typeof value === "object" ||
|
||||
typeof value === "number" ||
|
||||
typeof value === "string") return value.toString();
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -19,21 +19,21 @@ function renderKeyValueTableRow(key: string, value: string | undefined) {
|
||||
return <tr key={key}>
|
||||
<td className="maputnik-popup-table-cell">{key}</td>
|
||||
<td className="maputnik-popup-table-cell">{value}</td>
|
||||
</tr>
|
||||
</tr>;
|
||||
}
|
||||
|
||||
function renderFeature(feature: InspectFeature, idx: number) {
|
||||
return <React.Fragment key={idx}>
|
||||
<tr>
|
||||
<td colSpan={2} className="maputnik-popup-layer-id">{feature.layer['source']}: {feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</td>
|
||||
<td colSpan={2} className="maputnik-popup-layer-id">{feature.layer["source"]}: {feature.layer["source-layer"]}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</td>
|
||||
</tr>
|
||||
{renderKeyValueTableRow("$type", feature.geometry.type)}
|
||||
{renderKeyValueTableRow("$id", displayValue(feature.id))}
|
||||
{Object.keys(feature.properties).map(propertyName => {
|
||||
const property = feature.properties[propertyName];
|
||||
return renderKeyValueTableRow(propertyName, displayValue(property))
|
||||
return renderKeyValueTableRow(propertyName, displayValue(property));
|
||||
})}
|
||||
</React.Fragment>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
function removeDuplicatedFeatures(features: InspectFeature[]) {
|
||||
@@ -41,22 +41,22 @@ function removeDuplicatedFeatures(features: InspectFeature[]) {
|
||||
|
||||
features.forEach(feature => {
|
||||
const featureIndex = uniqueFeatures.findIndex(feature2 => {
|
||||
return feature.layer['source-layer'] === feature2.layer['source-layer']
|
||||
&& JSON.stringify(feature.properties) === JSON.stringify(feature2.properties)
|
||||
})
|
||||
return feature.layer["source-layer"] === feature2.layer["source-layer"]
|
||||
&& JSON.stringify(feature.properties) === JSON.stringify(feature2.properties);
|
||||
});
|
||||
|
||||
if(featureIndex === -1) {
|
||||
uniqueFeatures.push(feature)
|
||||
uniqueFeatures.push(feature);
|
||||
} else {
|
||||
if('inspectModeCounter' in uniqueFeatures[featureIndex]) {
|
||||
uniqueFeatures[featureIndex].inspectModeCounter!++
|
||||
if("inspectModeCounter" in uniqueFeatures[featureIndex]) {
|
||||
uniqueFeatures[featureIndex].inspectModeCounter!++;
|
||||
} else {
|
||||
uniqueFeatures[featureIndex].inspectModeCounter = 2
|
||||
uniqueFeatures[featureIndex].inspectModeCounter = 2;
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return uniqueFeatures
|
||||
return uniqueFeatures;
|
||||
}
|
||||
|
||||
type FeaturePropertyPopupProps = {
|
||||
@@ -65,16 +65,16 @@ type FeaturePropertyPopupProps = {
|
||||
|
||||
class FeaturePropertyPopup extends React.Component<FeaturePropertyPopupProps> {
|
||||
render() {
|
||||
const features = removeDuplicatedFeatures(this.props.features)
|
||||
return <div className="maputnik-feature-property-popup">
|
||||
const features = removeDuplicatedFeatures(this.props.features);
|
||||
return <div className="maputnik-feature-property-popup" dir="ltr" data-wd-key="feature-property-popup">
|
||||
<table className="maputnik-popup-table">
|
||||
<tbody>
|
||||
{features.map(renderFeature)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default FeaturePropertyPopup
|
||||
export default FeaturePropertyPopup;
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import React from 'react'
|
||||
import IconLayer from './IconLayer'
|
||||
import type {InspectFeature} from './MapMaplibreGlFeaturePropertyPopup';
|
||||
import React from "react";
|
||||
import IconLayer from "./IconLayer";
|
||||
import type {InspectFeature} from "./MapMaplibreGlFeaturePropertyPopup";
|
||||
|
||||
function groupFeaturesBySourceLayer(features: InspectFeature[]) {
|
||||
const sources: {[key: string]: InspectFeature[]} = {}
|
||||
const sources: {[key: string]: InspectFeature[]} = {};
|
||||
|
||||
const returnedFeatures: {[key: string]: number} = {}
|
||||
const returnedFeatures: {[key: string]: number} = {};
|
||||
|
||||
features.forEach(feature => {
|
||||
const sourceKey = feature.layer['source-layer'] as string;
|
||||
const sourceKey = feature.layer["source-layer"] as string;
|
||||
if(Object.prototype.hasOwnProperty.call(returnedFeatures, feature.layer.id)) {
|
||||
returnedFeatures[feature.layer.id]++
|
||||
returnedFeatures[feature.layer.id]++;
|
||||
|
||||
const featureObject = sources[sourceKey].find((f: InspectFeature) => f.layer.id === feature.layer.id)
|
||||
const featureObject = sources[sourceKey].find((f: InspectFeature) => f.layer.id === feature.layer.id);
|
||||
|
||||
featureObject!.counter = returnedFeatures[feature.layer.id]
|
||||
featureObject!.counter = returnedFeatures[feature.layer.id];
|
||||
} else {
|
||||
sources[sourceKey] = sources[sourceKey] || []
|
||||
sources[sourceKey].push(feature)
|
||||
sources[sourceKey] = sources[sourceKey] || [];
|
||||
sources[sourceKey].push(feature);
|
||||
|
||||
returnedFeatures[feature.layer.id] = 1
|
||||
returnedFeatures[feature.layer.id] = 1;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return sources
|
||||
return sources;
|
||||
}
|
||||
|
||||
type FeatureLayerPopupProps = {
|
||||
@@ -66,7 +66,7 @@ class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const sources = groupFeaturesBySourceLayer(this.props.features)
|
||||
const sources = groupFeaturesBySourceLayer(this.props.features);
|
||||
|
||||
const items = Object.keys(sources).map(vectorLayerId => {
|
||||
const layers = sources[vectorLayerId].map((feature: InspectFeature, idx: number) => {
|
||||
@@ -83,7 +83,7 @@ class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
|
||||
<label
|
||||
className="maputnik-popup-layer__label"
|
||||
onClick={() => {
|
||||
this.props.onLayerSelect(feature.layer.id)
|
||||
this.props.onLayerSelect(feature.layer.id);
|
||||
}}
|
||||
>
|
||||
{feature.layer.type &&
|
||||
@@ -96,19 +96,19 @@ class FeatureLayerPopup extends React.Component<FeatureLayerPopupProps> {
|
||||
{feature.layer.id}
|
||||
{feature.counter && <span> × {feature.counter}</span>}
|
||||
</label>
|
||||
</div>
|
||||
})
|
||||
</div>;
|
||||
});
|
||||
return <div key={vectorLayerId}>
|
||||
<div className="maputnik-popup-layer-id">{vectorLayerId}</div>
|
||||
{layers}
|
||||
</div>
|
||||
})
|
||||
</div>;
|
||||
});
|
||||
|
||||
return <div className="maputnik-feature-layer-popup">
|
||||
return <div className="maputnik-feature-layer-popup" data-wd-key="feature-layer-popup" dir="ltr">
|
||||
{items}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default FeatureLayerPopup
|
||||
export default FeatureLayerPopup;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from 'react'
|
||||
import {throttle} from 'lodash';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import React from "react";
|
||||
import {throttle} from "lodash";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
|
||||
import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup';
|
||||
import MapMaplibreGlLayerPopup from "./MapMaplibreGlLayerPopup";
|
||||
|
||||
import 'ol/ol.css'
|
||||
import "ol/ol.css";
|
||||
//@ts-ignore
|
||||
import {apply} from 'ol-mapbox-style';
|
||||
import {Map, View, Overlay} from 'ol';
|
||||
import {apply} from "ol-mapbox-style";
|
||||
import {Map, View, Overlay} from "ol";
|
||||
|
||||
import {toLonLat} from 'ol/proj';
|
||||
import type {StyleSpecification} from 'maplibre-gl';
|
||||
import {toLonLat} from "ol/proj";
|
||||
import type {StyleSpecification} from "maplibre-gl";
|
||||
|
||||
|
||||
function renderCoords (coords: string[]) {
|
||||
@@ -19,8 +19,8 @@ function renderCoords (coords: string[]) {
|
||||
}
|
||||
else {
|
||||
return <span className="maputnik-coords">
|
||||
{coords.map((coord) => String(coord).padStart(7, "\u00A0")).join(', ')}
|
||||
</span>
|
||||
{coords.map((coord) => String(coord).padStart(7, "\u00A0")).join(", ")}
|
||||
</span>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ type MapOpenLayersInternalProps = {
|
||||
mapStyle: object
|
||||
accessToken?: string
|
||||
style?: object
|
||||
onLayerSelect(...args: unknown[]): unknown
|
||||
onLayerSelect(layerId: string): void
|
||||
debugToolbox: boolean
|
||||
replaceAccessTokens(...args: unknown[]): unknown
|
||||
onChange(...args: unknown[]): unknown
|
||||
@@ -48,7 +48,7 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
|
||||
onMapLoaded: () => {},
|
||||
onDataChange: () => {},
|
||||
onLayerSelect: () => {},
|
||||
}
|
||||
};
|
||||
updateStyle: any;
|
||||
map: any;
|
||||
container: HTMLDivElement | null = null;
|
||||
@@ -101,15 +101,15 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
|
||||
})
|
||||
});
|
||||
|
||||
map.on('pointermove', (evt) => {
|
||||
map.on("pointermove", (evt) => {
|
||||
const coords = toLonLat(evt.coordinate);
|
||||
this.setState({
|
||||
cursor: [
|
||||
coords[0].toFixed(2),
|
||||
coords[1].toFixed(2)
|
||||
]
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
const onMoveEnd = () => {
|
||||
const zoom = map.getView().getZoom();
|
||||
@@ -122,12 +122,12 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
|
||||
lat: center[1],
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMoveEnd();
|
||||
map.on('moveend', onMoveEnd);
|
||||
map.on("moveend", onMoveEnd);
|
||||
|
||||
map.on('postrender', (_e) => {
|
||||
map.on("postrender", (_e) => {
|
||||
const center = toLonLat(map.getView().getCenter()!);
|
||||
this.setState({
|
||||
center: [
|
||||
@@ -150,13 +150,13 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
|
||||
closeOverlay = (e: any) => {
|
||||
e.target.blur();
|
||||
this.overlay!.setPosition(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
return <div className="maputnik-ol-container">
|
||||
<div
|
||||
ref={x => this.popupContainer = x}
|
||||
ref={x => {this.popupContainer = x;}}
|
||||
style={{background: "black"}}
|
||||
className="maputnik-popup"
|
||||
>
|
||||
@@ -193,14 +193,14 @@ class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps,
|
||||
}
|
||||
<div
|
||||
className="maputnik-ol"
|
||||
ref={x => this.container = x}
|
||||
ref={x => {this.container = x;}}
|
||||
role="region"
|
||||
aria-label={t("Map view")}
|
||||
style={{
|
||||
...this.props.style,
|
||||
}}>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +1,35 @@
|
||||
import React from 'react'
|
||||
import React from "react";
|
||||
|
||||
import FieldFunction from './FieldFunction'
|
||||
import type {LayerSpecification} from 'maplibre-gl'
|
||||
import FieldFunction from "./FieldFunction";
|
||||
import type {LayerSpecification} from "maplibre-gl";
|
||||
import { type MappedLayerErrors } from "../libs/definitions";
|
||||
|
||||
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"];
|
||||
|
||||
/** Extract field spec by {@fieldName} from the {@layerType} in the
|
||||
* style specification from either the paint or layout group */
|
||||
function getFieldSpec(spec: any, layerType: LayerSpecification["type"], fieldName: string) {
|
||||
const groupName = getGroupName(spec, layerType, fieldName)
|
||||
const group = spec[groupName + '_' + layerType]
|
||||
const fieldSpec = group[fieldName]
|
||||
const groupName = getGroupName(spec, layerType, fieldName);
|
||||
const group = spec[groupName + "_" + layerType];
|
||||
const fieldSpec = group[fieldName];
|
||||
if(iconProperties.indexOf(fieldName) >= 0) {
|
||||
return {
|
||||
...fieldSpec,
|
||||
values: spec.$root.sprite.values
|
||||
}
|
||||
};
|
||||
}
|
||||
if(fieldName === 'text-font') {
|
||||
if(fieldName === "text-font") {
|
||||
return {
|
||||
...fieldSpec,
|
||||
values: spec.$root.glyphs.values
|
||||
}
|
||||
};
|
||||
}
|
||||
return fieldSpec
|
||||
return fieldSpec;
|
||||
}
|
||||
|
||||
function getGroupName(spec: any, layerType: LayerSpecification["type"], fieldName: string) {
|
||||
const paint = spec['paint_' + layerType] || {}
|
||||
if (fieldName in paint) {
|
||||
return 'paint'
|
||||
} else {
|
||||
return 'layout'
|
||||
}
|
||||
function getGroupName(spec: any, layerType: LayerSpecification["type"], fieldName: string): "paint" | "layout" {
|
||||
const paint = spec["paint_" + layerType] || {};
|
||||
return (fieldName in paint) ? "paint" : "layout";
|
||||
}
|
||||
|
||||
type PropertyGroupProps = {
|
||||
@@ -40,26 +37,26 @@ type PropertyGroupProps = {
|
||||
groupFields: string[]
|
||||
onChange(...args: unknown[]): unknown
|
||||
spec: any
|
||||
errors?: {[key: string]: {message: string}}
|
||||
errors?: MappedLayerErrors
|
||||
};
|
||||
|
||||
export default class PropertyGroup extends React.Component<PropertyGroupProps> {
|
||||
onPropertyChange = (property: string, newValue: any) => {
|
||||
const group = getGroupName(this.props.spec, this.props.layer.type, property)
|
||||
this.props.onChange(group ,property, newValue)
|
||||
}
|
||||
const group = getGroupName(this.props.spec, this.props.layer.type, property);
|
||||
this.props.onChange(group ,property, newValue);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {errors} = this.props;
|
||||
const fields = this.props.groupFields.map(fieldName => {
|
||||
const fieldSpec = getFieldSpec(this.props.spec, this.props.layer.type, fieldName)
|
||||
const fieldSpec = getFieldSpec(this.props.spec, this.props.layer.type, fieldName);
|
||||
|
||||
const paint = this.props.layer.paint || {}
|
||||
const layout = this.props.layer.layout || {}
|
||||
const paint = this.props.layer.paint || {};
|
||||
const layout = this.props.layer.layout || {};
|
||||
const fieldValue = fieldName in paint
|
||||
? paint[fieldName as keyof typeof paint]
|
||||
: layout[fieldName as keyof typeof layout]
|
||||
const fieldType = fieldName in paint ? 'paint' : 'layout';
|
||||
: layout[fieldName as keyof typeof layout];
|
||||
const fieldType = fieldName in paint ? "paint" : "layout";
|
||||
|
||||
return <FieldFunction
|
||||
errors={errors}
|
||||
@@ -69,11 +66,11 @@ export default class PropertyGroup extends React.Component<PropertyGroupProps> {
|
||||
value={fieldValue}
|
||||
fieldType={fieldType}
|
||||
fieldSpec={fieldSpec}
|
||||
/>
|
||||
})
|
||||
/>;
|
||||
});
|
||||
|
||||
return <div className="maputnik-property-group">
|
||||
{fields}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React from "react";
|
||||
|
||||
type ScrollContainerProps = {
|
||||
children?: React.ReactNode
|
||||
@@ -8,6 +8,6 @@ export default class ScrollContainer extends React.Component<ScrollContainerProp
|
||||
render() {
|
||||
return <div className="maputnik-scroll-container">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from 'react'
|
||||
import React from "react";
|
||||
|
||||
import {otherFilterOps} from '../libs/filterops'
|
||||
import InputString from './InputString'
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
import InputSelect from './InputSelect'
|
||||
import {otherFilterOps} from "../libs/filterops";
|
||||
import InputString from "./InputString";
|
||||
import InputAutocomplete from "./InputAutocomplete";
|
||||
import InputSelect from "./InputSelect";
|
||||
|
||||
function tryParseInt(v: string | number) {
|
||||
if (v === '') return v
|
||||
if (isNaN(v as number)) return v
|
||||
return parseFloat(v as string)
|
||||
if (v === "") return v;
|
||||
if (isNaN(v as number)) return v;
|
||||
return parseFloat(v as string);
|
||||
}
|
||||
|
||||
function tryParseBool(v: string | boolean) {
|
||||
@@ -43,23 +43,23 @@ type SingleFilterEditorProps = {
|
||||
export default class SingleFilterEditor extends React.Component<SingleFilterEditorProps> {
|
||||
static defaultProps = {
|
||||
properties: {},
|
||||
}
|
||||
};
|
||||
|
||||
onFilterPartChanged(filterOp: string, propertyName: string, filterArgs: string[]) {
|
||||
let newFilter = [filterOp, propertyName, ...filterArgs.map(parseFilter)]
|
||||
if(filterOp === 'has' || filterOp === '!has') {
|
||||
newFilter = [filterOp, propertyName]
|
||||
let newFilter = [filterOp, propertyName, ...filterArgs.map(parseFilter)];
|
||||
if(filterOp === "has" || filterOp === "!has") {
|
||||
newFilter = [filterOp, propertyName];
|
||||
} else if(filterArgs.length === 0) {
|
||||
newFilter = [filterOp, propertyName, '']
|
||||
newFilter = [filterOp, propertyName, ""];
|
||||
}
|
||||
this.props.onChange(newFilter)
|
||||
this.props.onChange(newFilter);
|
||||
}
|
||||
|
||||
render() {
|
||||
const f = this.props.filter
|
||||
const filterOp = f[0]
|
||||
const propertyName = f[1]
|
||||
const filterArgs = f.slice(2)
|
||||
const f = this.props.filter;
|
||||
const filterOp = f[0];
|
||||
const propertyName = f[1];
|
||||
const filterArgs = f.slice(2);
|
||||
|
||||
return <div className="maputnik-filter-editor-single">
|
||||
<div className="maputnik-filter-editor-property">
|
||||
@@ -82,11 +82,11 @@ export default class SingleFilterEditor extends React.Component<SingleFilterEdit
|
||||
<div className="maputnik-filter-editor-args">
|
||||
<InputString
|
||||
aria-label="value"
|
||||
value={filterArgs.join(',')}
|
||||
onChange={(v: string) => this.onFilterPartChanged(filterOp, propertyName, v.split(','))}
|
||||
value={filterArgs.join(",")}
|
||||
onChange={(v: string) => this.onFilterPartChanged(filterOp, propertyName, v.split(","))}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import './SmallError.scss';
|
||||
import React from "react";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
import "./SmallError.scss";
|
||||
|
||||
|
||||
type SmallErrorInternalProps = {
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import Block from './Block'
|
||||
import InputSpec, { SpecFieldProps as InputFieldSpecProps } from './InputSpec'
|
||||
import Fieldset from './Fieldset'
|
||||
|
||||
|
||||
const typeMap = {
|
||||
color: () => Block,
|
||||
enum: ({fieldSpec}: any) => (Object.keys(fieldSpec.values).length <= 3 ? Fieldset : Block),
|
||||
boolean: () => Block,
|
||||
array: () => Fieldset,
|
||||
resolvedImage: () => Block,
|
||||
number: () => Block,
|
||||
string: () => Block,
|
||||
formatted: () => Block,
|
||||
padding: () => Block,
|
||||
};
|
||||
|
||||
export type SpecFieldProps = InputFieldSpecProps & {
|
||||
name?: string
|
||||
};
|
||||
|
||||
const SpecField: React.FC<SpecFieldProps> = (props) => {
|
||||
const fieldType = props.fieldSpec?.type;
|
||||
|
||||
const typeBlockFn = typeMap[fieldType!];
|
||||
|
||||
let TypeBlock;
|
||||
if (typeBlockFn) {
|
||||
TypeBlock = typeBlockFn(props);
|
||||
}
|
||||
else {
|
||||
console.warn("No such type for '%s'", fieldType);
|
||||
TypeBlock = Block;
|
||||
}
|
||||
|
||||
return (
|
||||
<TypeBlock label={props.label} action={props.action} fieldSpec={props.fieldSpec}>
|
||||
<InputSpec {...props} />
|
||||
</TypeBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpecField;
|
||||
@@ -1,20 +1,22 @@
|
||||
import React from 'react'
|
||||
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
|
||||
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||
import React from "react";
|
||||
import {PiListPlusBold} from "react-icons/pi";
|
||||
import {TbMathFunction} from "react-icons/tb";
|
||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
||||
|
||||
import InputButton from './InputButton'
|
||||
import InputSpec from './InputSpec'
|
||||
import InputNumber from './InputNumber'
|
||||
import InputString from './InputString'
|
||||
import InputSelect from './InputSelect'
|
||||
import Block from './Block'
|
||||
import docUid from '../libs/document-uid'
|
||||
import sortNumerically from '../libs/sort-numerically'
|
||||
import {findDefaultFromSpec} from '../libs/spec-helper';
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import InputButton from "./InputButton";
|
||||
import InputSpec from "./InputSpec";
|
||||
import InputNumber from "./InputNumber";
|
||||
import InputString from "./InputString";
|
||||
import InputSelect from "./InputSelect";
|
||||
import Block from "./Block";
|
||||
import docUid from "../libs/document-uid";
|
||||
import sortNumerically from "../libs/sort-numerically";
|
||||
import {findDefaultFromSpec} from "../libs/spec-helper";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
|
||||
import labelFromFieldName from '../libs/label-from-field-name'
|
||||
import DeleteStopButton from './_DeleteStopButton'
|
||||
import labelFromFieldName from "../libs/label-from-field-name";
|
||||
import DeleteStopButton from "./_DeleteStopButton";
|
||||
import { type MappedLayerErrors } from "../libs/definitions";
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +32,7 @@ function setStopRefs(props: DataPropertyInternalProps, state: DataPropertyState)
|
||||
}
|
||||
newRefs[idx] = docUid("stop-");
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return newRefs;
|
||||
@@ -46,7 +48,7 @@ type DataPropertyInternalProps = {
|
||||
fieldType?: string
|
||||
fieldSpec?: object
|
||||
value?: DataPropertyValue
|
||||
errors?: object
|
||||
errors?: MappedLayerErrors
|
||||
} & WithTranslation;
|
||||
|
||||
type DataPropertyState = {
|
||||
@@ -59,17 +61,17 @@ type DataPropertyValue = {
|
||||
base?: number
|
||||
type?: string
|
||||
stops: Stop[]
|
||||
}
|
||||
};
|
||||
|
||||
export type Stop = [{
|
||||
zoom: number
|
||||
value: number
|
||||
}, number]
|
||||
}, number];
|
||||
|
||||
class DataPropertyInternal extends React.Component<DataPropertyInternalProps, DataPropertyState> {
|
||||
state = {
|
||||
refs: {} as {[key: number]: string}
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const newRefs = setStopRefs(this.props, this.state);
|
||||
@@ -77,7 +79,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
||||
if(newRefs) {
|
||||
this.setState({
|
||||
refs: newRefs
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,20 +95,20 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
||||
|
||||
getFieldFunctionType(fieldSpec: any) {
|
||||
if (fieldSpec.expression.interpolated) {
|
||||
return "exponential"
|
||||
return "exponential";
|
||||
}
|
||||
if (fieldSpec.type === "number") {
|
||||
return "interval"
|
||||
return "interval";
|
||||
}
|
||||
return "categorical"
|
||||
return "categorical";
|
||||
}
|
||||
|
||||
getDataFunctionTypes(fieldSpec: any) {
|
||||
if (fieldSpec.expression.interpolated) {
|
||||
return ["interpolate", "categorical", "interval", "exponential", "identity"]
|
||||
return ["interpolate", "categorical", "interval", "exponential", "identity"];
|
||||
}
|
||||
else {
|
||||
return ["categorical", "interval", "identity"]
|
||||
return ["categorical", "interval", "identity"];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +119,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
||||
return {
|
||||
ref: this.state.refs[idx],
|
||||
data: stop
|
||||
}
|
||||
};
|
||||
})
|
||||
// Sort by zoom
|
||||
.sort((a, b) => sortNumerically(a.data[0].zoom, b.data[0].zoom));
|
||||
@@ -127,7 +129,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
||||
mappedWithRef
|
||||
.forEach((stop, idx) =>{
|
||||
newRefs[idx] = stop.ref;
|
||||
})
|
||||
});
|
||||
|
||||
this.setState({
|
||||
refs: newRefs
|
||||
@@ -144,7 +146,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
||||
};
|
||||
}
|
||||
else {
|
||||
const stopValue = value.type === 'categorical' ? '' : 0;
|
||||
const stopValue = value.type === "categorical" ? "" : 0;
|
||||
value = {
|
||||
property: "",
|
||||
type: value.type,
|
||||
@@ -154,13 +156,13 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
||||
[{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec as any)]
|
||||
],
|
||||
...value,
|
||||
}
|
||||
};
|
||||
}
|
||||
this.props.onChange!(fieldName, value);
|
||||
}
|
||||
};
|
||||
|
||||
changeStop(changeIdx: number, stopData: { zoom: number | undefined, value: number }, value: number) {
|
||||
const stops = this.props.value?.stops.slice(0) || []
|
||||
const stops = this.props.value?.stops.slice(0) || [];
|
||||
// const changedStop = stopData.zoom === undefined ? stopData.value : stopData
|
||||
stops[changeIdx] = [
|
||||
{
|
||||
@@ -175,20 +177,20 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
||||
const changedValue = {
|
||||
...this.props.value,
|
||||
stops: orderedStops,
|
||||
}
|
||||
this.onChange(this.props.fieldName, changedValue)
|
||||
};
|
||||
this.onChange(this.props.fieldName, changedValue);
|
||||
}
|
||||
|
||||
changeBase(newValue: number | undefined) {
|
||||
const changedValue = {
|
||||
...this.props.value,
|
||||
base: newValue
|
||||
}
|
||||
};
|
||||
|
||||
if (changedValue.base === undefined) {
|
||||
delete changedValue["base"];
|
||||
}
|
||||
this.props.onChange!(this.props.fieldName, changedValue)
|
||||
this.props.onChange!(this.props.fieldName, changedValue);
|
||||
}
|
||||
|
||||
changeDataType(propVal: string) {
|
||||
@@ -205,43 +207,43 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
||||
|
||||
changeDataProperty(propName: "property" | "default", propVal: any) {
|
||||
if (propVal) {
|
||||
this.props.value![propName] = propVal
|
||||
this.props.value![propName] = propVal;
|
||||
}
|
||||
else {
|
||||
delete this.props.value![propName]
|
||||
delete this.props.value![propName];
|
||||
}
|
||||
this.onChange(this.props.fieldName, this.props.value)
|
||||
this.onChange(this.props.fieldName, this.props.value);
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
if (typeof this.props.value?.type === "undefined") {
|
||||
this.props.value!.type = this.getFieldFunctionType(this.props.fieldSpec)
|
||||
this.props.value!.type = this.getFieldFunctionType(this.props.fieldSpec);
|
||||
}
|
||||
|
||||
let dataFields;
|
||||
if (this.props.value?.stops) {
|
||||
dataFields = this.props.value.stops.map((stop, idx) => {
|
||||
const zoomLevel = typeof stop[0] === 'object' ? stop[0].zoom : undefined;
|
||||
const zoomLevel = typeof stop[0] === "object" ? stop[0].zoom : undefined;
|
||||
const key = this.state.refs[idx];
|
||||
const dataLevel = typeof stop[0] === 'object' ? stop[0].value : stop[0];
|
||||
const value = stop[1]
|
||||
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />
|
||||
const dataLevel = typeof stop[0] === "object" ? stop[0].value : stop[0];
|
||||
const value = stop[1];
|
||||
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />;
|
||||
|
||||
const dataProps = {
|
||||
'aria-label': t("Input value"),
|
||||
"aria-label": t("Input value"),
|
||||
label: t("Data value"),
|
||||
value: dataLevel as any,
|
||||
onChange: (newData: string | number | undefined) => this.changeStop(idx, { zoom: zoomLevel, value: newData as number }, value)
|
||||
}
|
||||
};
|
||||
|
||||
let dataInput;
|
||||
if(this.props.value?.type === "categorical") {
|
||||
dataInput = <InputString {...dataProps} />
|
||||
dataInput = <InputString {...dataProps} />;
|
||||
}
|
||||
else {
|
||||
dataInput = <InputNumber {...dataProps} />
|
||||
dataInput = <InputNumber {...dataProps} />;
|
||||
}
|
||||
|
||||
let zoomInput = null;
|
||||
@@ -254,7 +256,7 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
||||
min={0}
|
||||
max={22}
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <tr key={key}>
|
||||
@@ -276,8 +278,8 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
||||
<td>
|
||||
{deleteStopBtn}
|
||||
</td>
|
||||
</tr>
|
||||
})
|
||||
</tr>;
|
||||
});
|
||||
}
|
||||
|
||||
return <div className="maputnik-data-spec-block">
|
||||
@@ -360,23 +362,21 @@ class DataPropertyInternal extends React.Component<DataPropertyInternalProps, Da
|
||||
className="maputnik-add-stop"
|
||||
onClick={this.props.onAddStop?.bind(this)}
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
||||
</svg> {t("Add stop")}
|
||||
<PiListPlusBold style={{ verticalAlign: "text-bottom" }} />
|
||||
{t("Add stop")}
|
||||
</InputButton>
|
||||
}
|
||||
<InputButton
|
||||
className="maputnik-add-stop"
|
||||
onClick={this.props.onExpressionClick?.bind(this)}
|
||||
>
|
||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||
</svg> {t("Convert to expression")}
|
||||
<TbMathFunction style={{ verticalAlign: "text-bottom" }} />
|
||||
{t("Convert to expression")}
|
||||
</InputButton>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react'
|
||||
import React from "react";
|
||||
|
||||
import InputButton from './InputButton'
|
||||
import {MdDelete} from 'react-icons/md'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import InputButton from "./InputButton";
|
||||
import {MdDelete} from "react-icons/md";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
|
||||
|
||||
type DeleteStopButtonInternalProps = {
|
||||
@@ -19,7 +19,7 @@ class DeleteStopButtonInternal extends React.Component<DeleteStopButtonInternalP
|
||||
title={t("Remove zoom level from stop")}
|
||||
>
|
||||
<MdDelete />
|
||||
</InputButton>
|
||||
</InputButton>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,38 +1,35 @@
|
||||
import React from 'react'
|
||||
import {MdDelete, MdUndo} from 'react-icons/md'
|
||||
import stringifyPretty from 'json-stringify-pretty-compact'
|
||||
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||
import React from "react";
|
||||
import {MdDelete, MdUndo} from "react-icons/md";
|
||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
||||
|
||||
import Block from './Block'
|
||||
import InputButton from './InputButton'
|
||||
import labelFromFieldName from '../libs/label-from-field-name'
|
||||
import FieldJson from './FieldJson'
|
||||
import Block from "./Block";
|
||||
import InputButton from "./InputButton";
|
||||
import labelFromFieldName from "../libs/label-from-field-name";
|
||||
import FieldJson from "./FieldJson";
|
||||
import type { StylePropertySpecification } from "maplibre-gl";
|
||||
import { type MappedLayerErrors } from "../libs/definitions";
|
||||
|
||||
|
||||
type ExpressionPropertyInternalProps = {
|
||||
onDelete?(...args: unknown[]): unknown
|
||||
fieldName: string
|
||||
fieldType?: string
|
||||
fieldSpec?: object
|
||||
fieldSpec?: StylePropertySpecification
|
||||
value?: any
|
||||
errors?: {[key: string]: {message: string}}
|
||||
onChange?(...args: unknown[]): unknown
|
||||
errors?: MappedLayerErrors
|
||||
onDelete?(...args: unknown[]): unknown
|
||||
onChange(value: object): void
|
||||
onUndo?(...args: unknown[]): unknown
|
||||
canUndo?(...args: unknown[]): unknown
|
||||
onFocus?(...args: unknown[]): unknown
|
||||
onBlur?(...args: unknown[]): unknown
|
||||
} & WithTranslation;
|
||||
|
||||
type ExpressionPropertyState = {
|
||||
jsonError: boolean
|
||||
};
|
||||
|
||||
class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInternalProps, ExpressionPropertyState> {
|
||||
class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInternalProps> {
|
||||
static defaultProps = {
|
||||
errors: {},
|
||||
onFocus: () => {},
|
||||
onBlur: () => {},
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props: ExpressionPropertyInternalProps) {
|
||||
super(props);
|
||||
@@ -41,21 +38,8 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
|
||||
};
|
||||
}
|
||||
|
||||
onJSONInvalid = (_err: Error) => {
|
||||
this.setState({
|
||||
jsonError: true,
|
||||
})
|
||||
}
|
||||
|
||||
onJSONValid = () => {
|
||||
this.setState({
|
||||
jsonError: false,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {t, errors, fieldName, fieldType, value, canUndo} = this.props;
|
||||
const {jsonError} = this.state;
|
||||
const {t, value, canUndo} = this.props;
|
||||
const undoDisabled = canUndo ? !canUndo() : true;
|
||||
|
||||
const deleteStopBtn = (
|
||||
@@ -81,61 +65,28 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
|
||||
</InputButton>
|
||||
</>
|
||||
);
|
||||
|
||||
const fieldKey = fieldType === undefined ? fieldName : `${fieldType}.${fieldName}`;
|
||||
|
||||
const fieldError = errors![fieldKey];
|
||||
const errorKeyStart = `${fieldKey}[`;
|
||||
const foundErrors = [];
|
||||
|
||||
function getValue(data: any) {
|
||||
return stringifyPretty(data, {indent: 2, maxLength: 38})
|
||||
let error = undefined;
|
||||
if (this.props.errors) {
|
||||
const fieldKey = this.props.fieldType ? this.props.fieldType + "." + this.props.fieldName : this.props.fieldName;
|
||||
error = this.props.errors[fieldKey];
|
||||
}
|
||||
|
||||
if (jsonError) {
|
||||
foundErrors.push({message: "Invalid JSON"});
|
||||
}
|
||||
else {
|
||||
Object.entries(errors!)
|
||||
.filter(([key, _error]) => {
|
||||
return key.startsWith(errorKeyStart);
|
||||
})
|
||||
.forEach(([_key, error]) => {
|
||||
return foundErrors.push(error);
|
||||
})
|
||||
|
||||
if (fieldError) {
|
||||
foundErrors.push(fieldError);
|
||||
}
|
||||
}
|
||||
|
||||
return <Block
|
||||
// this feels like an incorrect type...? `foundErrors` is an array of objects, not a single object
|
||||
error={foundErrors as any}
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
label={t(labelFromFieldName(this.props.fieldName))}
|
||||
action={deleteStopBtn}
|
||||
wideMode={true}
|
||||
error={error}
|
||||
>
|
||||
<FieldJson
|
||||
mode={{name: "mgl"}}
|
||||
lint={{
|
||||
context: "expression",
|
||||
spec: this.props.fieldSpec,
|
||||
}}
|
||||
lintType="expression"
|
||||
spec={this.props.fieldSpec}
|
||||
className="maputnik-expression-editor"
|
||||
onFocus={this.props.onFocus}
|
||||
onBlur={this.props.onBlur}
|
||||
onJSONInvalid={this.onJSONInvalid}
|
||||
onJSONValid={this.onJSONValid}
|
||||
layer={value}
|
||||
lineNumbers={false}
|
||||
maxHeight={200}
|
||||
lineWrapping={true}
|
||||
getValue={getValue}
|
||||
value={value}
|
||||
onChange={this.props.onChange}
|
||||
/>
|
||||
</Block>
|
||||
</Block>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user