mirror of
https://github.com/maputnik/editor.git
synced 2026-08-01 10:47:26 +00:00
Compare commits
2 Commits
v3.1.0
..
ebece40308
| Author | SHA1 | Date | |
|---|---|---|---|
| ebece40308 | |||
| 3e7e1e6ce9 |
@@ -8,3 +8,4 @@ assignees: ''
|
|||||||
---
|
---
|
||||||
|
|
||||||
<!-- Thanks for reaching out! If you are having general Maputnik mapping questions, please asking them at https://gis.stackexchange.com/ using the 'maputnik' tag https://gis.stackexchange.com/questions/tagged/maputnik and read https://gis.stackexchange.com/help/how-to-ask before you do so (please keep in mind that you're asking there in a general GIS forum, not a dedicated support channel) -->
|
<!-- Thanks for reaching out! If you are having general Maputnik mapping questions, please asking them at https://gis.stackexchange.com/ using the 'maputnik' tag https://gis.stackexchange.com/questions/tagged/maputnik and read https://gis.stackexchange.com/help/how-to-ask before you do so (please keep in mind that you're asking there in a general GIS forum, not a dedicated support channel) -->
|
||||||
|
|
||||||
|
|||||||
@@ -8,3 +8,4 @@
|
|||||||
- [ ] Include before/after visuals or gifs if this PR includes visual changes.
|
- [ ] Include before/after visuals or gifs if this PR includes visual changes.
|
||||||
- [ ] Write tests for all new functionality.
|
- [ ] Write tests for all new functionality.
|
||||||
- [ ] Add an entry to `CHANGELOG.md` under the `## main` section.
|
- [ ] Add an entry to `CHANGELOG.md` under the `## main` section.
|
||||||
|
|
||||||
|
|||||||
@@ -11,29 +11,7 @@ updates:
|
|||||||
interval: "daily"
|
interval: "daily"
|
||||||
open-pull-requests-limit: 20
|
open-pull-requests-limit: 20
|
||||||
versioning-strategy: increase
|
versioning-strategy: increase
|
||||||
groups:
|
|
||||||
vitest:
|
|
||||||
patterns:
|
|
||||||
- "*vitest*"
|
|
||||||
react:
|
|
||||||
patterns:
|
|
||||||
- "*react*"
|
|
||||||
cooldown:
|
|
||||||
default-days: 5
|
|
||||||
semver-major-days: 5
|
|
||||||
semver-minor-days: 3
|
|
||||||
semver-patch-days: 3
|
|
||||||
include:
|
|
||||||
- "*"
|
|
||||||
exclude:
|
|
||||||
- "@maplibre/*"
|
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
cooldown:
|
|
||||||
default-days: 3
|
|
||||||
# no semver support for github-actions
|
|
||||||
# => no specific configuration for this
|
|
||||||
include:
|
|
||||||
- "*"
|
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ permissions: write-all
|
|||||||
jobs:
|
jobs:
|
||||||
dependabot:
|
dependabot:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }}
|
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Dependabot metadata
|
- name: Dependabot metadata
|
||||||
id: metadata
|
id: metadata
|
||||||
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
|
uses: dependabot/fetch-metadata@v2.3.0
|
||||||
with:
|
with:
|
||||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
- name: Approve Dependabot PRs
|
- name: Approve Dependabot PRs
|
||||||
|
|||||||
+65
-106
@@ -7,148 +7,107 @@ on:
|
|||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
build-docker:
|
||||||
|
name: build docker
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- run: docker build -t test-docker-image-build .
|
||||||
|
|
||||||
|
# build the editor
|
||||||
build-node:
|
build-node:
|
||||||
name: "build on ${{ matrix.os }}"
|
name: "build on ${{ matrix.os }}"
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@v4
|
||||||
with: { persist-credentials: false }
|
- uses: actions/setup-node@v4
|
||||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
with:
|
||||||
with:
|
node-version-file: '.nvmrc'
|
||||||
node-version-file: '.nvmrc'
|
- run: npm ci
|
||||||
- run: npm ci
|
- run: npm run build
|
||||||
- run: npm run build
|
- run: npm run lint
|
||||||
- run: npm run lint
|
- run: npm run lint-css
|
||||||
- run: npm run lint-css
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
build-artifacts:
|
build-artifacts:
|
||||||
name: "build artifacts"
|
name: "build artifacts"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@v4
|
||||||
with: { persist-credentials: false }
|
- uses: actions/setup-node@v4
|
||||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
with:
|
||||||
with:
|
node-version-file: '.nvmrc'
|
||||||
node-version-file: '.nvmrc'
|
- run: npm ci
|
||||||
- run: npm ci
|
- run: npm run build
|
||||||
- run: npm run build
|
- name: artifacts/maputnik
|
||||||
- name: artifacts/maputnik
|
uses: actions/upload-artifact@v4
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
with:
|
||||||
with:
|
name: maputnik
|
||||||
name: maputnik
|
path: dist
|
||||||
path: dist
|
|
||||||
|
|
||||||
# Build and upload desktop CLI artifacts
|
# Build and upload desktop CLI artifacts
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.23.x
|
go-version: ^1.23.x
|
||||||
cache-dependency-path: desktop/go.sum
|
cache-dependency-path: desktop/go.sum
|
||||||
id: go
|
id: go
|
||||||
|
|
||||||
- name: Build desktop artifacts
|
- name: Build desktop artifacts
|
||||||
run: npm run build-desktop
|
run: npm run build-desktop
|
||||||
|
|
||||||
- name: Artifacts/linux
|
- name: Artifacts/linux
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: maputnik-linux
|
name: maputnik-linux
|
||||||
path: ./desktop/bin/linux/
|
path: ./desktop/bin/linux/
|
||||||
|
|
||||||
- name: Artifacts/darwin
|
- name: Artifacts/darwin
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: maputnik-darwin
|
name: maputnik-darwin
|
||||||
path: ./desktop/bin/darwin/
|
path: ./desktop/bin/darwin/
|
||||||
|
|
||||||
- name: Artifacts/windows
|
- name: Artifacts/windows
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: maputnik-windows
|
name: maputnik-windows
|
||||||
path: ./desktop/bin/windows/
|
path: ./desktop/bin/windows/
|
||||||
|
|
||||||
unit-tests:
|
|
||||||
name: "Unit tests"
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with: { persist-credentials: false }
|
|
||||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm run test-unit-ci
|
|
||||||
- name: Upload coverage reports to Codecov
|
|
||||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
|
||||||
with:
|
|
||||||
files: ${{ github.workspace }}/coverage/coverage-final.json
|
|
||||||
verbose: true
|
|
||||||
|
|
||||||
e2e-tests:
|
e2e-tests:
|
||||||
name: "E2E tests using chrome"
|
name: "E2E tests using ${{ matrix.browser }}"
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
browser: [chrome]
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
with: { persist-credentials: false }
|
|
||||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: Cypress run
|
- name: Cypress run
|
||||||
uses: cypress-io/github-action@fa4a118725a8f001170d49631ea89e5d66fee626 # v7.4.1
|
uses: cypress-io/github-action@v6
|
||||||
with:
|
with:
|
||||||
build: npm run build
|
build: npm run build
|
||||||
start: npm run start
|
start: npm run start
|
||||||
browser: chrome
|
browser: ${{ matrix.browser }}
|
||||||
- name: Upload coverage reports to Codecov
|
- name: Upload coverage reports to Codecov
|
||||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
|
||||||
files: ${{ github.workspace }}/.nyc_output/out.json
|
|
||||||
verbose: true
|
|
||||||
|
|
||||||
e2e-tests-docker:
|
|
||||||
name: "E2E tests using chrome and docker"
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
with: { persist-credentials: false }
|
|
||||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
- run: npm ci
|
|
||||||
- name: Cypress run
|
|
||||||
uses: cypress-io/github-action@fa4a118725a8f001170d49631ea89e5d66fee626 # v7.4.1
|
|
||||||
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@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
|
||||||
with:
|
with:
|
||||||
files: ${{ github.workspace }}/.nyc_output/out.json
|
files: ${{ github.workspace }}/.nyc_output/out.json
|
||||||
verbose: true
|
verbose: true
|
||||||
|
|||||||
@@ -38,11 +38,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# 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)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@@ -67,4 +67,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
uses: github/codeql-action/analyze@v3
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
- name: Use Node.js from nvmrc
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
./build/bump-version-changelog.js ${{ inputs.version }}
|
./build/bump-version-changelog.js ${{ inputs.version }}
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
uses: peter-evans/create-pull-request@v7
|
||||||
with:
|
with:
|
||||||
commit-message: Bump version to ${{ inputs.version }}
|
commit-message: Bump version to ${{ inputs.version }}
|
||||||
branch: bump-version-to-${{ inputs.version }}
|
branch: bump-version-to-${{ inputs.version }}
|
||||||
|
|||||||
@@ -8,15 +8,12 @@ jobs:
|
|||||||
deploy-pages:
|
deploy-pages:
|
||||||
name: deploy/pages
|
name: deploy/pages
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
if: ${{ github.event_name == 'push' }}
|
if: ${{ github.event_name == 'push' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@v4
|
||||||
with: { persist-credentials: false }
|
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
- name: Use Node.js from nvmrc
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
@@ -27,7 +24,7 @@ jobs:
|
|||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Upload to GitHub Pages
|
- name: Upload to GitHub Pages
|
||||||
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0
|
uses: peaceiris/actions-gh-pages@v4
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
publish_dir: dist
|
publish_dir: dist
|
||||||
@@ -36,20 +33,19 @@ jobs:
|
|||||||
deploy-docker:
|
deploy-docker:
|
||||||
name: deploy/docker
|
name: deploy/docker
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
if: ${{ github.event_name == 'push' }}
|
if: ${{ github.event_name == 'push' }}
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@v4
|
||||||
- run: docker build -t ghcr.io/maplibre/maputnik:main .
|
- run: docker build -t ghcr.io/maplibre/maputnik:main .
|
||||||
- run: docker push ghcr.io/maplibre/maputnik:main
|
- run: docker push ghcr.io/maplibre/maputnik:main
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: Release
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [main]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -12,23 +12,20 @@ jobs:
|
|||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: main
|
ref: main
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
- name: Use Node.js from nvmrc
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
|
|
||||||
- name: Check if version has been updated
|
- name: Check if version has been updated
|
||||||
id: check
|
id: check
|
||||||
uses: EndBug/version-check@095362f3cd50f690c8fa0e6afeea81834bd8d320 # latest
|
uses: EndBug/version-check@v2
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
publish: ${{ steps.check.outputs.changed }}
|
publish: ${{ steps.check.outputs.changed }}
|
||||||
@@ -42,19 +39,19 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
- name: Use Node.js from nvmrc
|
- name: Use Node.js from nvmrc
|
||||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: ".nvmrc"
|
node-version-file: ".nvmrc"
|
||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
- name: Set up Go for desktop build
|
- name: Set up Go for desktop build
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.23.x
|
go-version: ^1.23.x
|
||||||
cache-dependency-path: desktop/go.sum
|
cache-dependency-path: desktop/go.sum
|
||||||
@@ -62,7 +59,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Get version
|
- name: Get version
|
||||||
id: package-version
|
id: package-version
|
||||||
uses: martinbeentjes/npm-get-version-action@3cf273023a0dda27efcd3164bdfb51908dd46a5b # v1.3.1
|
uses: martinbeentjes/npm-get-version-action@v1.3.1
|
||||||
|
|
||||||
- name: Install
|
- name: Install
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -73,7 +70,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Tag commit and push
|
- name: Tag commit and push
|
||||||
id: tag_version
|
id: tag_version
|
||||||
uses: mathieudutour/github-tag-action@a22cf08638b34d5badda920f9daf6e72c477b07b # v6.2
|
uses: mathieudutour/github-tag-action@v6.2
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
custom_tag: ${{ steps.package-version.outputs.current-version }}
|
custom_tag: ${{ steps.package-version.outputs.current-version }}
|
||||||
@@ -91,7 +88,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
id: create_regular_release
|
id: create_regular_release
|
||||||
uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0
|
uses: ncipollo/release-action@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
@@ -102,3 +99,4 @@ jobs:
|
|||||||
allowUpdates: true
|
allowUpdates: true
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
# See https://pre-commit.com for more information
|
|
||||||
# See https://pre-commit.com/hooks.html for more hooks
|
|
||||||
|
|
||||||
ci:
|
|
||||||
autoupdate_schedule: monthly
|
|
||||||
|
|
||||||
repos:
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v6.0.0
|
|
||||||
hooks:
|
|
||||||
- id: check-added-large-files
|
|
||||||
- id: check-executables-have-shebangs
|
|
||||||
- id: check-json
|
|
||||||
exclude: 'tsconfig(\.node)?\.json'
|
|
||||||
- id: check-shebang-scripts-are-executable
|
|
||||||
- id: check-symlinks
|
|
||||||
- id: check-toml
|
|
||||||
- id: check-yaml
|
|
||||||
args: [ --allow-multiple-documents ]
|
|
||||||
- id: destroyed-symlinks
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
- id: mixed-line-ending
|
|
||||||
args: [ --fix=lf ]
|
|
||||||
- id: trailing-whitespace
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
Maputnik is a MapLibre style editor written using React and TypeScript.
|
|
||||||
|
|
||||||
To get started, install all npm packages:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify code correctness by running ESLint:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run lint
|
|
||||||
```
|
|
||||||
|
|
||||||
Or try fixing lint issues with:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run lint -- --fix
|
|
||||||
```
|
|
||||||
|
|
||||||
The project type checked and built with:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
To run the tests make sure that xvfb is installed:
|
|
||||||
|
|
||||||
```
|
|
||||||
apt install xvfb
|
|
||||||
```
|
|
||||||
|
|
||||||
Run the development server in the background with Vite:
|
|
||||||
|
|
||||||
```
|
|
||||||
nohup npm run start &
|
|
||||||
```
|
|
||||||
|
|
||||||
Then start the Cypress tests with:
|
|
||||||
|
|
||||||
```
|
|
||||||
xvfb-run -a npm run test
|
|
||||||
```
|
|
||||||
|
|
||||||
## Pull Requests
|
|
||||||
|
|
||||||
- Pull requests should update `CHANGELOG.md` with a short description of the change.
|
|
||||||
+3
-52
@@ -1,46 +1,6 @@
|
|||||||
## main
|
## main
|
||||||
|
|
||||||
### ✨ Features and improvements
|
### ✨ Features and improvements
|
||||||
- _...Add new stuff here..._
|
|
||||||
|
|
||||||
### 🐞 Bug fixes
|
|
||||||
- _...Add new stuff here..._
|
|
||||||
|
|
||||||
## 3.1.0
|
|
||||||
|
|
||||||
### ✨ 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
|
|
||||||
- Set the correct map view when opening a new style on an empty map
|
|
||||||
- Allow root-relative urls in the stylefile
|
|
||||||
|
|
||||||
### 🐞 Bug fixes
|
|
||||||
|
|
||||||
- Improved the local style open modal and local file upload.
|
|
||||||
- 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 position
|
|
||||||
- Fixed an issue with the generation of translations
|
|
||||||
- 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
|
|
||||||
|
|
||||||
## 3.0.0
|
|
||||||
|
|
||||||
### ✨ Features and improvements
|
|
||||||
- Fix radio/delete filter buttons styling regression
|
|
||||||
- Add german translation
|
- Add german translation
|
||||||
- Use same version number for web and desktop versions
|
- Use same version number for web and desktop versions
|
||||||
- Add scheme type options for vector/raster tile
|
- Add scheme type options for vector/raster tile
|
||||||
@@ -50,22 +10,12 @@
|
|||||||
- Upgrade to MapLibre LG JS v5
|
- Upgrade to MapLibre LG JS v5
|
||||||
- Upgrade Vite 6 and Cypress 14 ([#970](https://github.com/maplibre/maputnik/pull/970))
|
- Upgrade Vite 6 and Cypress 14 ([#970](https://github.com/maplibre/maputnik/pull/970))
|
||||||
- Upgrade OpenLayers from v6 to v10
|
- Upgrade OpenLayers from v6 to v10
|
||||||
- When loading a style into localStorage that causes a QuotaExceededError, purge localStorage and retry
|
- _...Add new stuff here..._
|
||||||
- Remove react-autobind dependency
|
|
||||||
- Remove usage of legacy `childContextTypes` API
|
|
||||||
- Refactor Field components to use arrow function syntax
|
|
||||||
- Replace react-autocomplete with Downshift in the autocomplete component
|
|
||||||
- Add LocationIQ as supported map provider with access token field and gallery style
|
|
||||||
- Use maputnik go binary for the docker image to allow file watching
|
|
||||||
- Revmove support for `debug` and `localport` url parameters
|
|
||||||
- Replace react-sortable-hoc with dnd-kit to avoid react console warnings and also use a maintained library
|
|
||||||
|
|
||||||
### 🐞 Bug fixes
|
### 🐞 Bug fixes
|
||||||
|
|
||||||
- Fix incorrect handing of network error response (#944)
|
- Fix incorrect handing of network error response (#944)
|
||||||
- Show an error when adding a layer with a duplicate ID
|
- _...Add new stuff here..._
|
||||||
- Replace deprecated `ReactDOM.render` usage with `createRoot` and drop the
|
|
||||||
`DOMNodeRemoved` cleanup hack
|
|
||||||
|
|
||||||
## 2.1.1
|
## 2.1.1
|
||||||
|
|
||||||
@@ -82,3 +32,4 @@
|
|||||||
## 1.7.0
|
## 1.7.0
|
||||||
|
|
||||||
- See release notes at https://maputnik.github.io/blog/2020/04/23/release-v1.7.0
|
- See release notes at https://maputnik.github.io/blog/2020/04/23/release-v1.7.0
|
||||||
|
|
||||||
|
|||||||
+10
-8
@@ -1,14 +1,16 @@
|
|||||||
FROM golang:1.23-alpine AS builder
|
FROM node:18 as builder
|
||||||
WORKDIR /maputnik
|
WORKDIR /maputnik
|
||||||
|
|
||||||
RUN apk add --no-cache nodejs npm make git gcc g++ libc-dev
|
# Only copy package.json to prevent npm install from running on every build
|
||||||
|
COPY package.json package-lock.json .npmrc ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
# Build maputnik
|
# Build maputnik
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm ci
|
RUN npx vite build
|
||||||
RUN CGO_ENABLED=1 GOOS=linux npm run build-linux
|
|
||||||
|
|
||||||
FROM alpine:latest
|
#---------------------------------------------------------------------------
|
||||||
WORKDIR /app
|
# Create a clean nginx-alpine slim image with just the build results
|
||||||
COPY --from=builder /maputnik/desktop/bin/linux ./
|
FROM nginx:alpine-slim
|
||||||
ENTRYPOINT ["/app/maputnik"]
|
|
||||||
|
COPY --from=builder /maputnik/dist /usr/share/nginx/html/
|
||||||
|
|||||||
@@ -20,3 +20,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
|
|||||||
@@ -18,15 +18,9 @@ targeted at developers and map designers.
|
|||||||
- In a Docker, run this command and browse to http://localhost:8888, Ctrl+C to stop the server.
|
- In a Docker, run this command and browse to http://localhost:8888, Ctrl+C to stop the server.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -it --rm -p 8888:8000 ghcr.io/maplibre/maputnik:main
|
docker run -it --rm -p 8888:80 ghcr.io/maplibre/maputnik:main
|
||||||
```
|
```
|
||||||
|
|
||||||
To see the CLI options (for example file watching or style serving) run:
|
|
||||||
```bash
|
|
||||||
docker run -it --rm -p 8888:8000 ghcr.io/maplibre/maputnik:main --help
|
|
||||||
```
|
|
||||||
You might need to mount a volume (`-v`) to be able to use these options.
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
The documentation can be found in the [Wiki](https://github.com/maplibre/maputnik/wiki). You are welcome to collaborate!
|
The documentation can be found in the [Wiki](https://github.com/maplibre/maputnik/wiki). You are welcome to collaborate!
|
||||||
@@ -81,7 +75,7 @@ npm run sort-styles
|
|||||||
## Tests
|
## Tests
|
||||||
For E2E testing we use [Cypress](https://www.cypress.io/)
|
For E2E testing we use [Cypress](https://www.cypress.io/)
|
||||||
|
|
||||||
[Cypress](https://www.cypress.io/) doesn't start a server so you'll need to start one manually by running `npm run start`.
|
[Cypress](https://www.cypress.io/) doesn't starts a server so you'll need to start one manually by running `npm run start`.
|
||||||
|
|
||||||
Now open a terminal and run the following using *chrome*:
|
Now open a terminal and run the following using *chrome*:
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,12 @@
|
|||||||
* https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.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 changelogPath = 'CHANGELOG.md';
|
||||||
let changelog = fs.readFileSync(changelogPath, "utf8");
|
let changelog = fs.readFileSync(changelogPath, 'utf8');
|
||||||
changelog = changelog.replace("## main", `## ${process.argv[2]}`);
|
changelog = changelog.replace('## main', `## ${process.argv[2]}`);
|
||||||
changelog = changelog.replaceAll("- _...Add new stuff here..._\n", "");
|
changelog = changelog.replaceAll('- _...Add new stuff here..._\n', '');
|
||||||
changelog = `## main
|
changelog = `## main
|
||||||
|
|
||||||
### ✨ Features and improvements
|
### ✨ Features and improvements
|
||||||
@@ -26,4 +26,4 @@ changelog = `## main
|
|||||||
|
|
||||||
` + changelog;
|
` + changelog;
|
||||||
|
|
||||||
fs.writeFileSync(changelogPath, changelog, "utf8");
|
fs.writeFileSync(changelogPath, changelog, 'utf8');
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
// Copied from maplibre/maplibre-gl-js
|
// Copied from maplibre/maplibre-gl-js
|
||||||
// https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.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 changelogPath = 'CHANGELOG.md';
|
||||||
const changelog = fs.readFileSync(changelogPath, "utf8");
|
const changelog = fs.readFileSync(changelogPath, 'utf8');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Parse the raw changelog text and split it into individual releases.
|
Parse the raw changelog text and split it into individual releases.
|
||||||
@@ -25,8 +25,8 @@ let match;
|
|||||||
// eslint-disable-next-line no-cond-assign
|
// eslint-disable-next-line no-cond-assign
|
||||||
while (match = regex.exec(changelog)) {
|
while (match = regex.exec(changelog)) {
|
||||||
releaseNotes.push({
|
releaseNotes.push({
|
||||||
"version": match[1],
|
'version': match[1],
|
||||||
"changelog": match[2].trim(),
|
'changelog': match[2].trim(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,10 +35,10 @@ const previous = releaseNotes[1];
|
|||||||
|
|
||||||
// Print the release notes template.
|
// Print the release notes template.
|
||||||
|
|
||||||
let header = "Changes since previous version";
|
let header = 'Changes since previous version'
|
||||||
if (previous) {
|
if (previous) {
|
||||||
header = `https://github.com/maplibre/maputnik
|
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}
|
const templatedReleaseNotes = `${header}
|
||||||
|
|
||||||
|
|||||||
@@ -8,34 +8,16 @@ export default defineConfig({
|
|||||||
exclude: "cypress/**/*.*",
|
exclude: "cypress/**/*.*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
e2e: {
|
e2e: {
|
||||||
setupNodeEvents(on, config) {
|
setupNodeEvents(on, config) {
|
||||||
// implement node event listeners here
|
// implement node event listeners here
|
||||||
require("@cypress/code-coverage/task")(on, config);
|
require("@cypress/code-coverage/task")(on, config);
|
||||||
on("before:browser:launch", (browser, launchOptions) => {
|
|
||||||
if (browser.family !== "chromium") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
launchOptions.args.push("--disable-gpu");
|
|
||||||
launchOptions.args.push("--enable-features=AllowSwiftShaderFallback,AllowSoftwareGLFallbackDueToCrashes");
|
|
||||||
launchOptions.args.push("--enable-unsafe-swiftshader");
|
|
||||||
return launchOptions;
|
|
||||||
});
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
baseUrl: "http://localhost:8888",
|
baseUrl: "http://localhost:8888",
|
||||||
scrollBehavior: "center",
|
|
||||||
retries: {
|
retries: {
|
||||||
runMode: 2,
|
runMode: 2,
|
||||||
openMode: 0,
|
openMode: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
component: {
|
|
||||||
devServer: {
|
|
||||||
framework: "react",
|
|
||||||
bundler: "vite",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ describe("accessibility", () => {
|
|||||||
then(get.skipTargetLayerList()).shouldBeFocused();
|
then(get.skipTargetLayerList()).shouldBeFocused();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skip link to layer editor", () => {
|
// 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", () => {
|
||||||
const selector = "root:skip:layer-editor";
|
const selector = "root:skip:layer-editor";
|
||||||
then(get.elementByTestId(selector)).shouldExist();
|
then(get.elementByTestId(selector)).shouldExist();
|
||||||
then(get.elementByTestId("skip-target-layer-editor")).shouldExist();
|
|
||||||
when.tab().tab();
|
when.tab().tab();
|
||||||
then(get.elementByTestId(selector)).shouldBeFocused();
|
then(get.elementByTestId(selector)).shouldBeFocused();
|
||||||
when.click(selector);
|
when.click(selector);
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -16,6 +16,7 @@ describe("history", () => {
|
|||||||
it("undo/redo", () => {
|
it("undo/redo", () => {
|
||||||
when.setStyle("geojson");
|
when.setStyle("geojson");
|
||||||
when.modal.open();
|
when.modal.open();
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
|
||||||
|
|
||||||
when.modal.fillLayers({
|
when.modal.fillLayers({
|
||||||
id: "step 1",
|
id: "step 1",
|
||||||
|
|||||||
@@ -1,294 +0,0 @@
|
|||||||
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,541 +0,0 @@
|
|||||||
import { MaputnikDriver } from "./maputnik-driver";
|
|
||||||
|
|
||||||
describe("layers list", () => {
|
|
||||||
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
|
||||||
beforeAndAfter();
|
|
||||||
beforeEach(() => {
|
|
||||||
when.setStyle("both");
|
|
||||||
when.modal.open();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("ops", () => {
|
|
||||||
let id: string;
|
|
||||||
beforeEach(() => {
|
|
||||||
id = when.modal.fillLayers({
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update layers in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when clicking delete", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.click("layer-list-item:" + id + ":delete");
|
|
||||||
});
|
|
||||||
it("should empty layers in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when clicking duplicate", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.click("layer-list-item:" + id + ":copy");
|
|
||||||
});
|
|
||||||
it("should add copy layer in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id + "-copy",
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when clicking hide", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.click("layer-list-item:" + id + ":toggle-visibility");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update visibility to none in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "background",
|
|
||||||
layout: {
|
|
||||||
visibility: "none",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when clicking show", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.click("layer-list-item:" + id + ":toggle-visibility");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should update visibility to visible in local storage", () => {
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "background",
|
|
||||||
layout: {
|
|
||||||
visibility: "visible",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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({
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("modify", () => {});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("fill", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "fill",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "fill",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: Change source
|
|
||||||
it("change source");
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("line", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "line",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "line",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("groups", () => {
|
|
||||||
when.modal.open();
|
|
||||||
const id1 = when.modal.fillLayers({
|
|
||||||
id: "aa",
|
|
||||||
type: "line",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
const id2 = when.modal.fillLayers({
|
|
||||||
id: "aa-2",
|
|
||||||
type: "line",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
const id3 = when.modal.fillLayers({
|
|
||||||
id: "b",
|
|
||||||
type: "line",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id2)).shouldNotBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
|
|
||||||
when.click("layer-list-group:aa-0");
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id1)).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id2)).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:" + id3)).shouldBeVisible();
|
|
||||||
when.click("layer-list-item:" + id2);
|
|
||||||
when.click("skip-target-layer-editor");
|
|
||||||
when.click("menu-move-layer-down");
|
|
||||||
then(get.elementByTestId("layer-list-group:aa-0")).shouldNotExist();
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: "aa",
|
|
||||||
type: "line",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "b",
|
|
||||||
type: "line",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "aa-2",
|
|
||||||
type: "line",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("symbol", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "symbol",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "symbol",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "raster",
|
|
||||||
layer: "raster",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "raster",
|
|
||||||
source: "raster",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("circle", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "circle",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "circle",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("fill extrusion", () => {
|
|
||||||
it("add", () => {
|
|
||||||
const id = when.modal.fillLayers({
|
|
||||||
type: "fill-extrusion",
|
|
||||||
layer: "example",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: id,
|
|
||||||
type: "fill-extrusion",
|
|
||||||
source: "example",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: "foo",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: "foo_bar",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.modal.open();
|
|
||||||
when.modal.fillLayers({
|
|
||||||
id: "foo_bar_baz",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:foo_bar")).shouldNotBeVisible();
|
|
||||||
then(
|
|
||||||
get.elementByTestId("layer-list-item:foo_bar_baz")
|
|
||||||
).shouldNotBeVisible();
|
|
||||||
when.click("layer-list-group:foo-0");
|
|
||||||
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("layer-list-item:foo_bar")).shouldBeVisible();
|
|
||||||
then(
|
|
||||||
get.elementByTestId("layer-list-item:foo_bar_baz")
|
|
||||||
).shouldBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("drag and drop", () => {
|
|
||||||
it("move layer should update local storage", () => {
|
|
||||||
when.modal.open();
|
|
||||||
const firstId = when.modal.fillLayers({
|
|
||||||
id: "a",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
when.modal.open();
|
|
||||||
const secondId = when.modal.fillLayers({
|
|
||||||
id: "b",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
when.modal.open();
|
|
||||||
const thirdId = when.modal.fillLayers({
|
|
||||||
id: "c",
|
|
||||||
type: "background",
|
|
||||||
});
|
|
||||||
|
|
||||||
when.dragAndDropWithWait("layer-list-item:" + firstId, "layer-list-item:" + thirdId);
|
|
||||||
|
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
|
||||||
layers: [
|
|
||||||
{
|
|
||||||
id: secondId,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: thirdId,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: firstId,
|
|
||||||
type: "background",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,526 @@
|
|||||||
|
import { v1 as uuid } from "uuid";
|
||||||
|
import { MaputnikDriver } from "./maputnik-driver";
|
||||||
|
|
||||||
|
describe("layers", () => {
|
||||||
|
const { beforeAndAfter, get, when, then } = new MaputnikDriver();
|
||||||
|
beforeAndAfter();
|
||||||
|
beforeEach(() => {
|
||||||
|
when.setStyle("both");
|
||||||
|
when.modal.open();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ops", () => {
|
||||||
|
let id: string;
|
||||||
|
beforeEach(() => {
|
||||||
|
id = when.modal.fillLayers({
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update layers in local storage", () => {
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when clicking delete", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.click("layer-list-item:" + id + ":delete");
|
||||||
|
});
|
||||||
|
it("should empty layers in local storage", () => {
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when clicking duplicate", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.click("layer-list-item:" + id + ":copy");
|
||||||
|
});
|
||||||
|
it("should add copy layer in local storage", () => {
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id + "-copy",
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when clicking hide", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.click("layer-list-item:" + id + ":toggle-visibility");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update visibility to none in local storage", () => {
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "background",
|
||||||
|
layout: {
|
||||||
|
visibility: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when clicking show", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
when.click("layer-list-item:" + id + ":toggle-visibility");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update visibility to visible in local storage", () => {
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "background",
|
||||||
|
layout: {
|
||||||
|
visibility: "visible",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("background", () => {
|
||||||
|
it("add", () => {
|
||||||
|
const id = when.modal.fillLayers({
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "background",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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("fill", () => {
|
||||||
|
it("add", () => {
|
||||||
|
const id = when.modal.fillLayers({
|
||||||
|
type: "fill",
|
||||||
|
layer: "example",
|
||||||
|
});
|
||||||
|
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "fill",
|
||||||
|
source: "example",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Change source
|
||||||
|
it("change source");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("line", () => {
|
||||||
|
it("add", () => {
|
||||||
|
const id = when.modal.fillLayers({
|
||||||
|
type: "line",
|
||||||
|
layer: "example",
|
||||||
|
});
|
||||||
|
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "line",
|
||||||
|
source: "example",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("groups", () => {
|
||||||
|
// TODO
|
||||||
|
// Click each of the layer groups.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("symbol", () => {
|
||||||
|
it("add", () => {
|
||||||
|
const id = when.modal.fillLayers({
|
||||||
|
type: "symbol",
|
||||||
|
layer: "example",
|
||||||
|
});
|
||||||
|
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "symbol",
|
||||||
|
source: "example",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("raster", () => {
|
||||||
|
it("add", () => {
|
||||||
|
const id = when.modal.fillLayers({
|
||||||
|
type: "raster",
|
||||||
|
layer: "raster",
|
||||||
|
});
|
||||||
|
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "raster",
|
||||||
|
source: "raster",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("circle", () => {
|
||||||
|
it("add", () => {
|
||||||
|
const id = when.modal.fillLayers({
|
||||||
|
type: "circle",
|
||||||
|
layer: "example",
|
||||||
|
});
|
||||||
|
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "circle",
|
||||||
|
source: "example",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fill extrusion", () => {
|
||||||
|
it("add", () => {
|
||||||
|
const id = when.modal.fillLayers({
|
||||||
|
type: "fill-extrusion",
|
||||||
|
layer: "example",
|
||||||
|
});
|
||||||
|
|
||||||
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
id: id,
|
||||||
|
type: "fill-extrusion",
|
||||||
|
source: "example",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("groups", () => {
|
||||||
|
it("simple", () => {
|
||||||
|
when.setStyle("geojson");
|
||||||
|
|
||||||
|
when.modal.open();
|
||||||
|
when.modal.fillLayers({
|
||||||
|
id: "foo",
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
|
||||||
|
when.modal.open();
|
||||||
|
when.modal.fillLayers({
|
||||||
|
id: "foo_bar",
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
|
||||||
|
when.modal.open();
|
||||||
|
when.modal.fillLayers({
|
||||||
|
id: "foo_bar_baz",
|
||||||
|
type: "background",
|
||||||
|
});
|
||||||
|
|
||||||
|
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
|
||||||
|
then(get.elementByTestId("layer-list-item:foo_bar")).shouldNotBeVisible();
|
||||||
|
then(
|
||||||
|
get.elementByTestId("layer-list-item:foo_bar_baz")
|
||||||
|
).shouldNotBeVisible();
|
||||||
|
when.click("layer-list-group:foo-0");
|
||||||
|
then(get.elementByTestId("layer-list-item:foo")).shouldBeVisible();
|
||||||
|
then(get.elementByTestId("layer-list-item:foo_bar")).shouldBeVisible();
|
||||||
|
then(
|
||||||
|
get.elementByTestId("layer-list-item:foo_bar_baz")
|
||||||
|
).shouldBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+1
-36
@@ -22,46 +22,11 @@ describe("map", () => {
|
|||||||
"Zoom: " + (zoomLevel + 1)
|
"Zoom: " + (zoomLevel + 1)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("via style file definition", () => {
|
|
||||||
when.setStyle("zoom_7_center_0_51");
|
|
||||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldBeVisible();
|
|
||||||
then(get.elementByTestId("maplibre:ctrl-zoom")).shouldContainText(
|
|
||||||
"Zoom: " + (7)
|
|
||||||
);
|
|
||||||
then(get.locationHash().should("contain", "#7/51/0"));
|
|
||||||
|
|
||||||
// opening another stylefile does not update the map view again
|
|
||||||
// as discussed in https://github.com/maplibre/maputnik/issues/1546
|
|
||||||
when.openASecondStyleWithDifferentZoomAndCenter();
|
|
||||||
then(get.locationHash().should("contain", "#7/51/0"));
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("search", () => {
|
describe("search", () => {
|
||||||
it("should exist", () => {
|
it('should exist', () => {
|
||||||
then(get.searchControl()).shouldBeVisible();
|
then(get.searchControl()).shouldBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("popup", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.setStyle("rectangles");
|
|
||||||
then(get.locationHash().should("exist"));
|
|
||||||
});
|
|
||||||
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,6 +1,4 @@
|
|||||||
/// <reference types="cypress-real-events" />
|
|
||||||
import { CypressHelper } from "@shellygo/cypress-test-utils";
|
import { CypressHelper } from "@shellygo/cypress-test-utils";
|
||||||
import "cypress-real-events/support";
|
|
||||||
|
|
||||||
export default class MaputnikCypressHelper {
|
export default class MaputnikCypressHelper {
|
||||||
private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" });
|
private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" });
|
||||||
@@ -10,45 +8,10 @@ export default class MaputnikCypressHelper {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public get = {
|
public get = {
|
||||||
locationHash: (): Cypress.Chainable<string> => cy.location("hash"),
|
|
||||||
...this.helper.get,
|
...this.helper.get,
|
||||||
};
|
};
|
||||||
|
|
||||||
public when = {
|
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
dropFileByFixture: (fixture: string, dropzoneTestId: string) => {
|
|
||||||
this.helper.get.elementByTestId(dropzoneTestId).selectFile("cypress/fixtures/" + fixture, {
|
|
||||||
action: "drag-drop",
|
|
||||||
force: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
...this.helper.when,
|
...this.helper.when,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+26
-112
@@ -8,10 +8,8 @@ const baseUrl = "http://localhost:8888/";
|
|||||||
|
|
||||||
const styleFromWindow = (win: Window) => {
|
const styleFromWindow = (win: Window) => {
|
||||||
const styleId = win.localStorage.getItem("maputnik:latest_style");
|
const styleId = win.localStorage.getItem("maputnik:latest_style");
|
||||||
const styleItemKey = `maputnik:style:${styleId}`;
|
const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`);
|
||||||
const styleItem = win.localStorage.getItem(styleItemKey);
|
const obj = JSON.parse(styleItem || "");
|
||||||
if (!styleItem) throw new Error("Could not get styleItem from localStorage");
|
|
||||||
const obj = JSON.parse(styleItem);
|
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,34 +76,6 @@ export class MaputnikDriver {
|
|||||||
fixture: "geojson-raster-style.json",
|
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: baseUrl + "example-style-with-zoom-7-and-center-0-51.json",
|
|
||||||
response: {
|
|
||||||
fixture: "example-style-with-zoom-7-and-center-0-51.json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: baseUrl + "example-style-with-zoom-5-and-center-50-50.json",
|
|
||||||
response: {
|
|
||||||
fixture: "example-style-with-zoom-5-and-center-50-50.json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.helper.given.interceptAndMockResponse({
|
this.helper.given.interceptAndMockResponse({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "*example.local/*",
|
url: "*example.local/*",
|
||||||
@@ -116,79 +86,52 @@ export class MaputnikDriver {
|
|||||||
url: "*example.com/*",
|
url: "*example.com/*",
|
||||||
response: [],
|
response: [],
|
||||||
});
|
});
|
||||||
this.helper.given.interceptAndMockResponse({
|
|
||||||
method: "GET",
|
|
||||||
url: "https://www.glyph-server.com/*",
|
|
||||||
response: ["Font 1", "Font 2", "Font 3"],
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
public when = {
|
public when = {
|
||||||
...this.helper.when,
|
...this.helper.when,
|
||||||
modal: this.modalDriver.when,
|
modal: this.modalDriver.when,
|
||||||
doWithin: (selector: string, fn: () => void) => {
|
within: (selector: string, fn: () => void) => {
|
||||||
this.helper.when.doWithin(fn, selector);
|
this.helper.when.within(fn, selector);
|
||||||
},
|
},
|
||||||
tab: () => this.helper.get.element("body").tab(),
|
tab: () => this.helper.get.element("body").tab(),
|
||||||
waitForExampleFileResponse: () => {
|
waitForExampleFileResponse: () => {
|
||||||
this.helper.when.waitForResponse("example-style.json");
|
this.helper.when.waitForResponse("example-style.json");
|
||||||
},
|
},
|
||||||
openASecondStyleWithDifferentZoomAndCenter: () => {
|
|
||||||
cy.contains("button", "Open").click();
|
|
||||||
cy.get('[data-wd-key="modal:open.url.input"]')
|
|
||||||
.should("be.enabled")
|
|
||||||
.clear()
|
|
||||||
.type("http://localhost:8888/example-style-with-zoom-5-and-center-50-50.json{enter}");
|
|
||||||
},
|
|
||||||
chooseExampleFile: () => {
|
chooseExampleFile: () => {
|
||||||
this.helper.given.fixture("example-style.json", "example-style.json");
|
this.helper.get
|
||||||
this.helper.when.openFileByFixture("example-style.json", "modal:open.dropzone", "modal:open.file.input");
|
.bySelector("type", "file")
|
||||||
this.helper.when.wait(200);
|
.selectFile("cypress/fixtures/example-style.json", { force: true });
|
||||||
},
|
|
||||||
dropExampleFile: () => {
|
|
||||||
this.helper.given.fixture("example-style.json", "example-style.json");
|
|
||||||
this.helper.when.dropFileByFixture("example-style.json", "modal:open.dropzone");
|
|
||||||
this.helper.when.wait(200);
|
|
||||||
},
|
},
|
||||||
setStyle: (
|
setStyle: (
|
||||||
styleProperties: "geojson" | "raster" | "both" | "layer" | "rectangles" | "font" | "zoom_7_center_0_51" | "",
|
styleProperties: "geojson" | "raster" | "both" | "layer" | "",
|
||||||
zoom?: number
|
zoom?: number
|
||||||
) => {
|
) => {
|
||||||
const url = new URL(baseUrl);
|
let url = "?debug";
|
||||||
switch (styleProperties) {
|
switch (styleProperties) {
|
||||||
case "geojson":
|
case "geojson":
|
||||||
url.searchParams.set("style", baseUrl + "geojson-style.json");
|
url += `&style=${baseUrl}geojson-style.json`;
|
||||||
break;
|
break;
|
||||||
case "raster":
|
case "raster":
|
||||||
url.searchParams.set("style", baseUrl + "raster-style.json");
|
url += `&style=${baseUrl}raster-style.json`;
|
||||||
break;
|
break;
|
||||||
case "both":
|
case "both":
|
||||||
url.searchParams.set("style", baseUrl + "geojson-raster-style.json");
|
url += `&style=${baseUrl}geojson-raster-style.json`;
|
||||||
break;
|
break;
|
||||||
case "layer":
|
case "layer":
|
||||||
url.searchParams.set("style", baseUrl + "example-layer-style.json");
|
url += `&style=${baseUrl}/example-layer-style.json`;
|
||||||
break;
|
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;
|
|
||||||
case "zoom_7_center_0_51":
|
|
||||||
url.searchParams.set("style", baseUrl + "example-style-with-zoom-7-and-center-0-51.json");
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (zoom) {
|
if (zoom) {
|
||||||
url.hash = `${zoom}/41.3805/2.1635`;
|
url += `#${zoom}/41.3805/2.1635`;
|
||||||
}
|
}
|
||||||
this.helper.when.visit(url.toString());
|
this.helper.when.visit(baseUrl + url);
|
||||||
if (styleProperties) {
|
if (styleProperties) {
|
||||||
this.helper.when.acceptConfirm();
|
this.helper.when.acceptConfirm();
|
||||||
}
|
}
|
||||||
// when methods should not include assertions
|
// 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.scrollIntoView();
|
||||||
toolbarLink.should("be.visible");
|
toolbarLink.should("be.visible");
|
||||||
},
|
},
|
||||||
@@ -200,7 +143,7 @@ export class MaputnikDriver {
|
|||||||
},
|
},
|
||||||
|
|
||||||
selectWithin: (selector: string, value: string) => {
|
selectWithin: (selector: string, value: string) => {
|
||||||
this.when.doWithin(selector, () => {
|
this.when.within(selector, () => {
|
||||||
this.helper.get.element("select").select(value);
|
this.helper.get.element("select").select(value);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -219,35 +162,6 @@ export class MaputnikDriver {
|
|||||||
.clear()
|
.clear()
|
||||||
.type(text, { parseSpecialCharSequences: false });
|
.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 = {
|
public get = {
|
||||||
@@ -267,6 +181,6 @@ export class MaputnikDriver {
|
|||||||
skipTargetLayerEditor: () =>
|
skipTargetLayerEditor: () =>
|
||||||
this.helper.get.elementByTestId("skip-target-layer-editor"),
|
this.helper.get.elementByTestId("skip-target-layer-editor"),
|
||||||
canvas: () => this.helper.get.element("canvas"),
|
canvas: () => this.helper.get.element("canvas"),
|
||||||
searchControl: () => this.helper.get.element(".maplibregl-ctrl-geocoder")
|
searchControl: () => this.helper.get.element('.maplibregl-ctrl-geocoder')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default class ModalDriver {
|
|||||||
public when = {
|
public when = {
|
||||||
fillLayers: (opts: { type: string; layer?: string; id?: string }) => {
|
fillLayers: (opts: { type: string; layer?: string; id?: string }) => {
|
||||||
// Having logic in test code is an anti pattern.
|
// Having logic in test code is an anti pattern.
|
||||||
// This should be split to multiple single responsibility functions
|
// This should be splitted to multiple single responsibility functions
|
||||||
const type = opts.type;
|
const type = opts.type;
|
||||||
const layer = opts.layer;
|
const layer = opts.layer;
|
||||||
let id;
|
let id;
|
||||||
@@ -20,8 +20,8 @@ export default class ModalDriver {
|
|||||||
this.helper.when.type("add-layer.layer-id.input", id);
|
this.helper.when.type("add-layer.layer-id.input", id);
|
||||||
|
|
||||||
if (layer) {
|
if (layer) {
|
||||||
this.helper.when.doWithin(() => {
|
this.helper.when.within(() => {
|
||||||
this.helper.get.element("input").clear().type(layer!);
|
this.helper.get.element("input").type(layer!);
|
||||||
}, "add-layer.layer-source-block");
|
}, "add-layer.layer-source-block");
|
||||||
}
|
}
|
||||||
this.helper.when.click("add-layer");
|
this.helper.when.click("add-layer");
|
||||||
|
|||||||
+8
-186
@@ -18,14 +18,10 @@ describe("modals", () => {
|
|||||||
then(get.elementByTestId("modal:open")).shouldNotExist();
|
then(get.elementByTestId("modal:open")).shouldNotExist();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("upload", () => {
|
it.skip("upload", () => {
|
||||||
|
// HM: I was not able to make the following choose file actually to select a file and close the modal...
|
||||||
when.chooseExampleFile();
|
when.chooseExampleFile();
|
||||||
then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
|
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
|
||||||
});
|
|
||||||
|
|
||||||
it("upload via drag and drop", () => {
|
|
||||||
when.dropExampleFile();
|
|
||||||
then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when click open url", () => {
|
describe("when click open url", () => {
|
||||||
@@ -175,22 +171,12 @@ describe("modals", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("sprite url", () => {
|
it("sprite url", () => {
|
||||||
when.setTextInJsonEditor("\"http://example.com\"");
|
when.setValue("modal:settings.sprite", "http://example.com");
|
||||||
when.click("modal:settings.name");
|
when.click("modal:settings.name");
|
||||||
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
|
||||||
sprite: "http://example.com",
|
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", () => {
|
it("glyphs url", () => {
|
||||||
const glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
|
const glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
|
||||||
when.setValue("modal:settings.glyphs", glyphsUrl);
|
when.setValue("modal:settings.glyphs", glyphsUrl);
|
||||||
@@ -238,41 +224,6 @@ describe("modals", () => {
|
|||||||
).shouldInclude({ "maputnik:stadia_access_token": apiKey });
|
).shouldInclude({ "maputnik:stadia_access_token": apiKey });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("locationiq access token", () => {
|
|
||||||
const apiKey = "testing123";
|
|
||||||
when.setValue(
|
|
||||||
"modal:settings.maputnik:locationiq_access_token",
|
|
||||||
apiKey
|
|
||||||
);
|
|
||||||
when.click("modal:settings.name");
|
|
||||||
then(
|
|
||||||
get.styleFromLocalStorage().then((style) => style.metadata)
|
|
||||||
).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", () => {
|
it("style renderer", () => {
|
||||||
cy.on("uncaught:exception", () => false); // this is due to the fact that this is an invalid style for openlayers
|
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");
|
when.select("modal:settings.maputnik:renderer", "ol");
|
||||||
@@ -288,13 +239,13 @@ describe("modals", () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
it("include API key when change renderer", () => {
|
it("inlcude API key when change renderer", () => {
|
||||||
|
|
||||||
when.click("modal:settings.close-modal");
|
when.click("modal:settings.close-modal")
|
||||||
when.click("nav:open");
|
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");
|
when.click("nav:settings");
|
||||||
|
|
||||||
when.select("modal:settings.maputnik:renderer", "mlgljs");
|
when.select("modal:settings.maputnik:renderer", "mlgljs");
|
||||||
@@ -321,136 +272,7 @@ describe("modals", () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("add layer", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
when.setStyle("layer");
|
|
||||||
when.modal.open();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows duplicate id error", () => {
|
|
||||||
when.setValue("add-layer.layer-id.input", "background");
|
|
||||||
when.click("add-layer");
|
|
||||||
then(get.elementByTestId("modal:add-layer")).shouldExist();
|
|
||||||
then(get.element(".maputnik-modal-error")).shouldContainText(
|
|
||||||
"Layer ID already exists"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("sources", () => {
|
describe("sources", () => {
|
||||||
it("toggle");
|
it("toggle");
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("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
|
|
||||||
cy.clearLocalStorage();
|
|
||||||
|
|
||||||
// fill localStorage until we get a QuotaExceededError
|
|
||||||
cy.window().then(win => {
|
|
||||||
let chunkSize = 1000;
|
|
||||||
const chunk = new Array(chunkSize).join("x");
|
|
||||||
let index = 0;
|
|
||||||
|
|
||||||
// Keep adding until we hit the quota
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
const key = `maputnik:fill-${index++}`;
|
|
||||||
win.localStorage.setItem(key, chunk);
|
|
||||||
} catch (e: any) {
|
|
||||||
// Verify it's a quota error
|
|
||||||
if (e.name === "QuotaExceededError") {
|
|
||||||
if (chunkSize <= 1) return;
|
|
||||||
else {
|
|
||||||
chunkSize /= 2;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw e; // Unexpected error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Open the style via URL input
|
|
||||||
when.click("nav:open");
|
|
||||||
when.setValue("modal:open.url.input", get.exampleFileUrl());
|
|
||||||
when.click("modal:open.url.button");
|
|
||||||
|
|
||||||
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
|
|
||||||
then(get.styleFromLocalStorage()).shouldExist();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,3 +15,4 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "test-style",
|
|
||||||
"center": [50,50],
|
|
||||||
"zoom": 5,
|
|
||||||
"version": 8,
|
|
||||||
"name": "Test Style",
|
|
||||||
"sources": {
|
|
||||||
"rectangles": {
|
|
||||||
"type": "geojson",
|
|
||||||
"data": {
|
|
||||||
"type": "FeatureCollection",
|
|
||||||
"features": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
|
||||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
|
||||||
"layers": []
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "test-style",
|
|
||||||
"center": [0,51],
|
|
||||||
"zoom": 7,
|
|
||||||
"version": 8,
|
|
||||||
"name": "Test Style",
|
|
||||||
"sources": {
|
|
||||||
"rectangles": {
|
|
||||||
"type": "geojson",
|
|
||||||
"data": {
|
|
||||||
"type": "FeatureCollection",
|
|
||||||
"features": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"layers": []
|
|
||||||
}
|
|
||||||
@@ -1,114 +1,12 @@
|
|||||||
{
|
{
|
||||||
"id": "test-style",
|
"id": "test-style",
|
||||||
"version": 8,
|
"version": 8,
|
||||||
"name": "Test Style",
|
"name": "Test Style",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"maputnik:renderer": "mlgljs",
|
"maputnik:renderer": "mlgljs"
|
||||||
"data": [
|
},
|
||||||
0,
|
"sources": {},
|
||||||
1,
|
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
2,
|
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
3,
|
"layers": []
|
||||||
4,
|
|
||||||
5,
|
|
||||||
6,
|
|
||||||
7,
|
|
||||||
8,
|
|
||||||
9,
|
|
||||||
10,
|
|
||||||
11,
|
|
||||||
12,
|
|
||||||
13,
|
|
||||||
14,
|
|
||||||
15,
|
|
||||||
16,
|
|
||||||
17,
|
|
||||||
18,
|
|
||||||
19,
|
|
||||||
20,
|
|
||||||
21,
|
|
||||||
22,
|
|
||||||
23,
|
|
||||||
24,
|
|
||||||
25,
|
|
||||||
26,
|
|
||||||
27,
|
|
||||||
28,
|
|
||||||
29,
|
|
||||||
30,
|
|
||||||
31,
|
|
||||||
32,
|
|
||||||
33,
|
|
||||||
34,
|
|
||||||
35,
|
|
||||||
36,
|
|
||||||
37,
|
|
||||||
38,
|
|
||||||
39,
|
|
||||||
40,
|
|
||||||
41,
|
|
||||||
42,
|
|
||||||
43,
|
|
||||||
44,
|
|
||||||
45,
|
|
||||||
46,
|
|
||||||
47,
|
|
||||||
48,
|
|
||||||
49,
|
|
||||||
50,
|
|
||||||
51,
|
|
||||||
52,
|
|
||||||
53,
|
|
||||||
54,
|
|
||||||
55,
|
|
||||||
56,
|
|
||||||
57,
|
|
||||||
58,
|
|
||||||
59,
|
|
||||||
60,
|
|
||||||
61,
|
|
||||||
62,
|
|
||||||
63,
|
|
||||||
64,
|
|
||||||
65,
|
|
||||||
66,
|
|
||||||
67,
|
|
||||||
68,
|
|
||||||
69,
|
|
||||||
70,
|
|
||||||
71,
|
|
||||||
72,
|
|
||||||
73,
|
|
||||||
74,
|
|
||||||
75,
|
|
||||||
76,
|
|
||||||
77,
|
|
||||||
78,
|
|
||||||
79,
|
|
||||||
80,
|
|
||||||
81,
|
|
||||||
82,
|
|
||||||
83,
|
|
||||||
84,
|
|
||||||
85,
|
|
||||||
86,
|
|
||||||
87,
|
|
||||||
88,
|
|
||||||
89,
|
|
||||||
90,
|
|
||||||
91,
|
|
||||||
92,
|
|
||||||
93,
|
|
||||||
94,
|
|
||||||
95,
|
|
||||||
96,
|
|
||||||
97,
|
|
||||||
98,
|
|
||||||
99
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"sources": {},
|
|
||||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
|
||||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
|
||||||
"layers": []
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
||||||
<title>Components App</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div data-cy-root></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
// ***********************************************************
|
|
||||||
// This example support/component.ts is processed and
|
|
||||||
// loaded automatically before your test files.
|
|
||||||
//
|
|
||||||
// This is a great place to put global configuration and
|
|
||||||
// behavior that modifies Cypress.
|
|
||||||
//
|
|
||||||
// You can change the location of this file or turn off
|
|
||||||
// automatically serving support files with the
|
|
||||||
// 'supportFile' configuration option.
|
|
||||||
//
|
|
||||||
// You can read more here:
|
|
||||||
// https://on.cypress.io/configuration
|
|
||||||
// ***********************************************************
|
|
||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
|
||||||
import "./commands";
|
|
||||||
|
|
||||||
import { mount } from "cypress/react";
|
|
||||||
|
|
||||||
// Augment the Cypress namespace to include type definitions for
|
|
||||||
// your custom command.
|
|
||||||
// Alternatively, can be defined in cypress/support/component.d.ts
|
|
||||||
// with a <reference path="./component" /> at the top of your spec.
|
|
||||||
declare global {
|
|
||||||
/* eslint-disable @typescript-eslint/no-namespace */
|
|
||||||
namespace Cypress {
|
|
||||||
interface Chainable {
|
|
||||||
mount: typeof mount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Cypress.Commands.add("mount", mount);
|
|
||||||
|
|
||||||
// Example use:
|
|
||||||
// cy.mount(<MyComponent />)
|
|
||||||
+2
-5
@@ -2,7 +2,8 @@ SOURCEDIR=.
|
|||||||
SOURCES := $(shell find $(SOURCEDIR) -name '*.go')
|
SOURCES := $(shell find $(SOURCEDIR) -name '*.go')
|
||||||
BINARY=maputnik
|
BINARY=maputnik
|
||||||
VERSION := $(shell node -p "require('../package.json').version")
|
VERSION := $(shell node -p "require('../package.json').version")
|
||||||
GOBIN := $(or $(shell if [ -d /go/bin ]; then echo "/go/bin"; fi),$(HOME)/go/bin)
|
GOPATH := $(if $(GOPATH),$(GOPATH),$(HOME)/go)
|
||||||
|
GOBIN := $(if $(GOBIN),$(GOBIN),$(HOME)/go/bin)
|
||||||
|
|
||||||
all: $(BINARY)
|
all: $(BINARY)
|
||||||
|
|
||||||
@@ -10,10 +11,6 @@ $(BINARY): $(GOBIN)/gox $(GOBIN)/go-winres $(SOURCES) version.go rice-box.go win
|
|||||||
$(GOBIN)/go-winres make --product-version=$(VERSION)
|
$(GOBIN)/go-winres make --product-version=$(VERSION)
|
||||||
$(GOBIN)/gox -osarch "windows/amd64 linux/amd64 darwin/amd64" -output "bin/{{.OS}}/${BINARY}"
|
$(GOBIN)/gox -osarch "windows/amd64 linux/amd64 darwin/amd64" -output "bin/{{.OS}}/${BINARY}"
|
||||||
|
|
||||||
bin/linux/$(BINARY): $(GOBIN)/gox $(GOBIN)/go-winres $(SOURCES) version.go rice-box.go winres/winres.json
|
|
||||||
$(GOBIN)/go-winres make --product-version=$(VERSION)
|
|
||||||
$(GOBIN)/gox -osarch "linux/amd64" -output "bin/{{.OS}}/${BINARY}"
|
|
||||||
|
|
||||||
winres/winres.json: winres/winres_template.json
|
winres/winres.json: winres/winres_template.json
|
||||||
sed 's/{{.Version}}/$(VERSION)/g' winres/winres_template.json > $@
|
sed 's/{{.Version}}/$(VERSION)/g' winres/winres_template.json > $@
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -5,7 +5,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"github.com/GeertJohan/go.rice"
|
|
||||||
|
"github.com/GeertJohan/go.rice"
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/maputnik/desktop/filewatch"
|
"github.com/maputnik/desktop/filewatch"
|
||||||
|
|||||||
+30
-39
@@ -1,65 +1,56 @@
|
|||||||
import eslint from "@eslint/js";
|
import eslint from '@eslint/js';
|
||||||
import {defineConfig} from "eslint/config";
|
import tseslint from 'typescript-eslint';
|
||||||
import stylisticTs from "@stylistic/eslint-plugin";
|
import reactPlugin from 'eslint-plugin-react';
|
||||||
import tseslint from "typescript-eslint";
|
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
||||||
import reactPlugin from "eslint-plugin-react";
|
import reactRefreshPlugin from 'eslint-plugin-react-refresh';
|
||||||
import reactHooksPlugin from "eslint-plugin-react-hooks";
|
|
||||||
import reactRefreshPlugin from "eslint-plugin-react-refresh";
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default tseslint.config({
|
||||||
extends: [
|
extends: [
|
||||||
eslint.configs.recommended,
|
eslint.configs.recommended,
|
||||||
tseslint.configs.recommended,
|
tseslint.configs.recommended,
|
||||||
],
|
],
|
||||||
files: ["**/*.{js,jsx,ts,tsx}"],
|
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||||
ignores: [
|
ignores: [
|
||||||
"dist/**/*",
|
"dist/**/*",
|
||||||
],
|
],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 2024,
|
ecmaVersion: 2024,
|
||||||
sourceType: "module",
|
sourceType: 'module',
|
||||||
globals: {
|
globals: {
|
||||||
global: "readonly"
|
global: 'readonly'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
react: { version: "18.2" }
|
react: { version: '18.2' }
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
"react": reactPlugin,
|
'react': reactPlugin,
|
||||||
"react-hooks": reactHooksPlugin,
|
'react-hooks': reactHooksPlugin,
|
||||||
"react-refresh": reactRefreshPlugin,
|
'react-refresh': reactRefreshPlugin
|
||||||
"@stylistic": stylisticTs
|
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"react-refresh/only-export-components": [
|
'react-refresh/only-export-components': [
|
||||||
"warn",
|
'warn',
|
||||||
{ allowConstantExport: true }
|
{ allowConstantExport: true }
|
||||||
],
|
],
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
"@typescript-eslint/no-unused-vars": [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
"warn",
|
'warn',
|
||||||
{
|
{
|
||||||
varsIgnorePattern: "^_",
|
varsIgnorePattern: '^_',
|
||||||
caughtErrors: "all",
|
caughtErrors: 'all',
|
||||||
caughtErrorsIgnorePattern: "^_",
|
caughtErrorsIgnorePattern: '^_',
|
||||||
argsIgnorePattern: "^_"
|
argsIgnorePattern: '^_'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"no-unused-vars": "off",
|
'no-unused-vars': 'off',
|
||||||
"react/prop-types": "off",
|
'react/prop-types': 'off',
|
||||||
"no-undef": "off",
|
'no-undef': 'off',
|
||||||
"indent": "off",
|
'indent': ['error', 2],
|
||||||
"@stylistic/indent": ["error", 2],
|
'no-var': 'error',
|
||||||
"semi": "off",
|
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
||||||
"@stylistic/semi": ["error", "always"],
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
"quotes": "off",
|
'@typescript-eslint/no-empty-object-type': '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: {
|
linterOptions: {
|
||||||
@@ -67,4 +58,4 @@ export default defineConfig({
|
|||||||
noInlineConfig: false
|
noInlineConfig: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export default {
|
||||||
|
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.
|
||||||
|
keepRemoved: true,
|
||||||
|
|
||||||
|
// We use plain English keys, so we disable key and namespace separators.
|
||||||
|
keySeparator: false,
|
||||||
|
namespaceSeparator: false,
|
||||||
|
|
||||||
|
defaultValue: (_locale, _ns, _key) => {
|
||||||
|
// The default value is a string that indicates that the string is not translated.
|
||||||
|
return '__STRING_NOT_TRANSLATED__';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { defineConfig } from "i18next-cli";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
locales: ["de", "fr", "he", "it", "ja", "ko", "tr", "zh"],
|
|
||||||
extract: {
|
|
||||||
input: ["src/**/*.{js,jsx,ts,tsx}"],
|
|
||||||
output: "src/locales/{{language}}/{{namespace}}.json",
|
|
||||||
defaultValue: "__STRING_NOT_TRANSLATED__"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Generated
+4662
-7468
File diff suppressed because it is too large
Load Diff
+65
-69
@@ -1,19 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "maputnik",
|
"name": "maputnik",
|
||||||
"version": "3.1.0",
|
"version": "2.1.1",
|
||||||
"description": "A MapLibre GL visual style editor",
|
"description": "A MapLibre GL visual style editor",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "''",
|
"main": "''",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "tsc && vite build --mode=production",
|
"build": "tsc && vite build --base=/maputnik/",
|
||||||
"build-desktop": "tsc && vite build --mode=desktop && cd desktop && make",
|
"build-desktop": "tsc && vite build --base=/ && cd desktop && make",
|
||||||
"build-linux": "tsc && vite build --mode=desktop && cd desktop && make bin/linux/maputnik",
|
"i18n:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'",
|
||||||
"i18n:extract": "npx i18next-cli extract",
|
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"test": "cypress run",
|
"test": "cypress run",
|
||||||
"test-unit": "vitest",
|
|
||||||
"test-unit-ci": "vitest run --coverage --reporter=json",
|
|
||||||
"cy:open": "cypress open",
|
"cy:open": "cypress open",
|
||||||
"lint-css": "stylelint \"src/styles/*.scss\"",
|
"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"
|
"sort-styles": "jq 'sort_by(.id)' src/config/styles.json > tmp.json && mv tmp.json src/config/styles.json"
|
||||||
@@ -26,57 +23,55 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://github.com/maplibre/maputnik#readme",
|
"homepage": "https://github.com/maplibre/maputnik#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-json": "^6.0.2",
|
"@mapbox/mapbox-gl-rtl-text": "^0.3.0",
|
||||||
"@codemirror/lint": "^6.9.7",
|
"@maplibre/maplibre-gl-geocoder": "^1.7.1",
|
||||||
"@codemirror/state": "^6.7.0",
|
"@maplibre/maplibre-gl-inspect": "^1.7.1",
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@maplibre/maplibre-gl-style-spec": "^23.1.0",
|
||||||
"@codemirror/view": "^6.43.4",
|
"@mdi/js": "^7.4.47",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@mdi/react": "^1.6.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@prantlf/jsonlint": "^16.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
|
||||||
"@mapbox/mapbox-gl-rtl-text": "^0.4.0",
|
|
||||||
"@maplibre/maplibre-gl-geocoder": "^1.9.4",
|
|
||||||
"@maplibre/maplibre-gl-inspect": "^1.8.2",
|
|
||||||
"@maplibre/maplibre-gl-style-spec": "^25.0.1",
|
|
||||||
"array-move": "^4.0.0",
|
"array-move": "^4.0.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^5.65.18",
|
||||||
"color": "^5.0.3",
|
"color": "^5.0.0",
|
||||||
"detect-browser": "^5.3.0",
|
"detect-browser": "^5.3.0",
|
||||||
"downshift": "^9.4.0",
|
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"i18next": "^26.3.4",
|
"i18next": "^24.2.2",
|
||||||
"i18next-browser-languagedetector": "^8.2.1",
|
"i18next-browser-languagedetector": "^8.0.4",
|
||||||
"i18next-resources-to-backend": "^1.2.1",
|
"i18next-resources-to-backend": "^1.2.1",
|
||||||
"json-stringify-pretty-compact": "^4.0.0",
|
"json-stringify-pretty-compact": "^4.0.0",
|
||||||
"json-to-ast": "^2.1.0",
|
"json-to-ast": "^2.1.0",
|
||||||
"lodash": "^4.18.1",
|
"lodash": "^4.17.21",
|
||||||
"lodash.capitalize": "^4.2.1",
|
"lodash.capitalize": "^4.2.1",
|
||||||
"lodash.clamp": "^4.0.3",
|
"lodash.clamp": "^4.0.3",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"maplibre-gl": "^5.24.0",
|
"maplibre-gl": "^5.1.0",
|
||||||
"maputnik-design": "github:maputnik/design#172b06c",
|
"maputnik-design": "github:maputnik/design#172b06c",
|
||||||
"ol": "^10.9.0",
|
"ol": "^10.4.0",
|
||||||
"ol-mapbox-style": "^13.4.1",
|
"ol-mapbox-style": "^12.4.0",
|
||||||
"pmtiles": "^4.4.1",
|
"pmtiles": "^4.1.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^19.2.4",
|
"react": "^18.2.0",
|
||||||
"react-accessible-accordion": "^5.0.1",
|
"react-accessible-accordion": "^5.0.0",
|
||||||
"react-aria-menubutton": "^8.0.0",
|
"react-aria-menubutton": "^7.0.3",
|
||||||
"react-aria-modal": "^5.0.2",
|
"react-aria-modal": "^5.0.2",
|
||||||
|
"react-autobind": "^1.0.6",
|
||||||
|
"react-autocomplete": "^1.8.1",
|
||||||
"react-collapse": "^5.1.1",
|
"react-collapse": "^5.1.1",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^18.2.0",
|
||||||
"react-i18next": "^17.0.7",
|
"react-file-reader-input": "^2.0.0",
|
||||||
"react-icons": "^5.6.0",
|
"react-i18next": "^15.4.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-icon-base": "^2.1.2",
|
||||||
|
"react-icons": "^5.4.0",
|
||||||
|
"react-sortable-hoc": "^2.0.0",
|
||||||
"reconnecting-websocket": "^4.4.0",
|
"reconnecting-websocket": "^4.4.0",
|
||||||
"slugify": "^1.6.9",
|
"slugify": "^1.6.6",
|
||||||
"string-hash": "^1.1.3",
|
"string-hash": "^1.1.3",
|
||||||
"url": "^0.11.4"
|
"url": "^0.11.4"
|
||||||
},
|
},
|
||||||
@@ -98,15 +93,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cypress/code-coverage": "^4.0.3",
|
"@cypress/code-coverage": "^3.13.11",
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^9.19.0",
|
||||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||||
"@rollup/plugin-replace": "^6.0.3",
|
"@rollup/plugin-replace": "^6.0.2",
|
||||||
"@shellygo/cypress-test-utils": "^6.0.6",
|
"@shellygo/cypress-test-utils": "^4.1.13",
|
||||||
"@stylistic/eslint-plugin": "^5.10.0",
|
"@types/codemirror": "^5.60.15",
|
||||||
"@types/codemirror": "^5.60.17",
|
"@types/color": "^4.2.0",
|
||||||
"@types/color": "^4.2.1",
|
"@types/cors": "^2.8.17",
|
||||||
"@types/cors": "^2.8.19",
|
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/geojson": "^7946.0.16",
|
"@types/geojson": "^7946.0.16",
|
||||||
"@types/json-to-ast": "^2.1.4",
|
"@types/json-to-ast": "^2.1.4",
|
||||||
@@ -117,37 +111,39 @@
|
|||||||
"@types/lodash.isequal": "^4.5.8",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"@types/lodash.throttle": "^4.1.9",
|
"@types/lodash.throttle": "^4.1.9",
|
||||||
"@types/randomcolor": "^0.5.9",
|
"@types/randomcolor": "^0.5.9",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^18.2.67",
|
||||||
"@types/react-aria-menubutton": "^6.2.14",
|
"@types/react-aria-menubutton": "^6.2.14",
|
||||||
"@types/react-aria-modal": "^5.0.0",
|
"@types/react-aria-modal": "^5.0.0",
|
||||||
|
"@types/react-autocomplete": "^1.8.11",
|
||||||
"@types/react-collapse": "^5.0.4",
|
"@types/react-collapse": "^5.0.4",
|
||||||
"@types/react-color": "^3.0.13",
|
"@types/react-color": "^3.0.13",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^18.2.22",
|
||||||
|
"@types/react-file-reader-input": "^2.0.4",
|
||||||
|
"@types/react-icon-base": "^2.1.6",
|
||||||
"@types/string-hash": "^1.1.3",
|
"@types/string-hash": "^1.1.3",
|
||||||
"@types/wicg-file-system-access": "^2023.10.7",
|
"@types/uuid": "^10.0.0",
|
||||||
"@vitejs/plugin-react": "^5.1.4",
|
"@types/wicg-file-system-access": "^2023.10.5",
|
||||||
"@vitest/coverage-v8": "^4.1.9",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"cors": "^2.8.6",
|
"cors": "^2.8.5",
|
||||||
"cypress": "^15.18.0",
|
"cypress": "^14.0.2",
|
||||||
"cypress-plugin-tab": "^2.0.0",
|
"cypress-plugin-tab": "^1.0.5",
|
||||||
"eslint": "^10.6.0",
|
"eslint": "^9.19.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.4.18",
|
||||||
"i18next-cli": "^1.53.2",
|
"i18next-parser": "^9.1.0",
|
||||||
"istanbul": "^0.4.5",
|
"istanbul": "^0.4.5",
|
||||||
"istanbul-lib-coverage": "^3.2.2",
|
"istanbul-lib-coverage": "^3.2.2",
|
||||||
"postcss": "^8.5.16",
|
"postcss": "^8.5.1",
|
||||||
"react-hot-loader": "^4.13.1",
|
"react-hot-loader": "^4.13.1",
|
||||||
"sass": "^1.101.0",
|
"sass": "^1.83.4",
|
||||||
"stylelint": "^17.14.0",
|
"stylelint": "^16.14.1",
|
||||||
"stylelint-config-recommended-scss": "^17.0.1",
|
"stylelint-config-recommended-scss": "^14.1.0",
|
||||||
"stylelint-scss": "^7.2.0",
|
"stylelint-scss": "^6.11.0",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^5.7.3",
|
||||||
"typescript-eslint": "^8.62.1",
|
"typescript-eslint": "^8.22.0",
|
||||||
"uuid": "^14.0.1",
|
"uuid": "^11.0.5",
|
||||||
"vite": "^7.3.2",
|
"vite": "^6.1.0",
|
||||||
"vite-plugin-istanbul": "^9.0.1",
|
"vite-plugin-istanbul": "^6.0.2"
|
||||||
"vitest": "^4.1.9"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+314
-292
File diff suppressed because it is too large
Load Diff
@@ -1,52 +1,50 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import ScrollContainer from "./ScrollContainer";
|
import PropTypes from 'prop-types'
|
||||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
import ScrollContainer from './ScrollContainer'
|
||||||
import { IconContext } from "react-icons";
|
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
type AppLayoutInternalProps = {
|
type AppLayoutInternalProps = {
|
||||||
toolbar: React.ReactElement
|
toolbar: React.ReactElement
|
||||||
layerList: React.ReactElement
|
layerList: React.ReactElement
|
||||||
layerEditor?: React.ReactElement
|
layerEditor?: React.ReactElement
|
||||||
codeEditor?: React.ReactElement
|
|
||||||
map: React.ReactElement
|
map: React.ReactElement
|
||||||
bottom?: React.ReactElement
|
bottom?: React.ReactElement
|
||||||
modals?: React.ReactNode
|
modals?: React.ReactNode
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
|
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
|
||||||
|
static childContextTypes = {
|
||||||
|
reactIconBase: PropTypes.object
|
||||||
|
}
|
||||||
|
|
||||||
|
getChildContext() {
|
||||||
|
return {
|
||||||
|
reactIconBase: { size: 14 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
document.body.dir = this.props.i18n.dir();
|
document.body.dir = this.props.i18n.dir();
|
||||||
|
|
||||||
return <IconContext.Provider value={{size: "14px"}}>
|
return <div className="maputnik-layout">
|
||||||
<div className="maputnik-layout">
|
{this.props.toolbar}
|
||||||
{this.props.toolbar}
|
<div className="maputnik-layout-main">
|
||||||
<div className="maputnik-layout-main">
|
<div className="maputnik-layout-list">
|
||||||
{this.props.codeEditor && <div className="maputnik-layout-code-editor">
|
{this.props.layerList}
|
||||||
<ScrollContainer>
|
|
||||||
{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>
|
</div>
|
||||||
{this.props.bottom && <div className="maputnik-layout-bottom">
|
<div className="maputnik-layout-drawer">
|
||||||
{this.props.bottom}
|
<ScrollContainer>
|
||||||
|
{this.props.layerEditor}
|
||||||
|
</ScrollContainer>
|
||||||
</div>
|
</div>
|
||||||
}
|
{this.props.map}
|
||||||
{this.props.modals}
|
|
||||||
</div>
|
</div>
|
||||||
</IconContext.Provider>;
|
{this.props.bottom && <div className="maputnik-layout-bottom">
|
||||||
|
{this.props.bottom}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{this.props.modals}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import { formatLayerId } from "../libs/format";
|
import {formatLayerId} from '../libs/format';
|
||||||
import { type LayerSpecification, type StyleSpecification } from "maplibre-gl";
|
import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
|
||||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||||
import { type MappedError } from "../libs/definitions";
|
|
||||||
|
|
||||||
type AppMessagePanelInternalProps = {
|
type AppMessagePanelInternalProps = {
|
||||||
errors?: MappedError[]
|
errors?: unknown[]
|
||||||
infos?: string[]
|
infos?: string[]
|
||||||
mapStyle?: StyleSpecification
|
mapStyle?: StyleSpecification
|
||||||
onLayerSelect?(index: number): void;
|
onLayerSelect?(...args: unknown[]): unknown
|
||||||
currentLayer?: LayerSpecification
|
currentLayer?: LayerSpecification
|
||||||
selectedLayerIndex?: number
|
selectedLayerIndex?: number
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
|
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onLayerSelect: () => { },
|
onLayerSelect: () => {},
|
||||||
};
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { t, selectedLayerIndex } = this.props;
|
const {t, selectedLayerIndex} = this.props;
|
||||||
const errors = this.props.errors?.map((error, idx) => {
|
const errors = this.props.errors?.map((error: any, idx) => {
|
||||||
let content;
|
let content;
|
||||||
if (error.parsed && error.parsed.type === "layer") {
|
if (error.parsed && error.parsed.type === "layer") {
|
||||||
const { parsed } = error;
|
const {parsed} = error;
|
||||||
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
|
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
|
||||||
content = (
|
content = (
|
||||||
<>
|
<>
|
||||||
{t("Layer")} <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
<Trans t={t}>
|
||||||
|
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
||||||
|
</Trans>
|
||||||
{selectedLayerIndex !== parsed.data.index &&
|
{selectedLayerIndex !== parsed.data.index &&
|
||||||
<>
|
<>
|
||||||
—
|
—
|
||||||
@@ -45,19 +46,19 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
|
|||||||
else {
|
else {
|
||||||
content = error.message;
|
content = error.message;
|
||||||
}
|
}
|
||||||
return <p key={"error-" + idx} className="maputnik-message-panel-error">
|
return <p key={"error-"+idx} className="maputnik-message-panel-error">
|
||||||
{content}
|
{content}
|
||||||
</p>;
|
</p>
|
||||||
});
|
})
|
||||||
|
|
||||||
const infos = this.props.infos?.map((m, i) => {
|
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">
|
return <div className="maputnik-message-panel">
|
||||||
{errors}
|
{errors}
|
||||||
{infos}
|
{infos}
|
||||||
</div>;
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import classnames from "classnames";
|
import classnames from 'classnames'
|
||||||
import {detect} from "detect-browser";
|
import {detect} from 'detect-browser';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MdOpenInBrowser,
|
MdOpenInBrowser,
|
||||||
@@ -9,22 +9,18 @@ import {
|
|||||||
MdHelpOutline,
|
MdHelpOutline,
|
||||||
MdFindInPage,
|
MdFindInPage,
|
||||||
MdLanguage,
|
MdLanguage,
|
||||||
MdSave,
|
MdSave
|
||||||
MdPublic,
|
} from 'react-icons/md'
|
||||||
MdCode
|
import pkgJson from '../../package.json'
|
||||||
} from "react-icons/md";
|
|
||||||
import pkgJson from "../../package.json";
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
import maputnikLogo from "maputnik-design/logos/logo-color.svg?inline";
|
import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline'
|
||||||
import { withTranslation, type WithTranslation } from "react-i18next";
|
import { withTranslation, WithTranslation } from 'react-i18next';
|
||||||
import { supportedLanguages } from "../i18n";
|
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.
|
// 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 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 = {
|
type IconTextProps = {
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
@@ -33,7 +29,7 @@ type IconTextProps = {
|
|||||||
|
|
||||||
class IconText extends React.Component<IconTextProps> {
|
class IconText extends React.Component<IconTextProps> {
|
||||||
render() {
|
render() {
|
||||||
return <span className="maputnik-icon-text">{this.props.children}</span>;
|
return <span className="maputnik-icon-text">{this.props.children}</span>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,19 +37,20 @@ type ToolbarLinkProps = {
|
|||||||
className?: string
|
className?: string
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
href?: string
|
href?: string
|
||||||
|
onToggleModal?(...args: unknown[]): unknown
|
||||||
};
|
};
|
||||||
|
|
||||||
class ToolbarLink extends React.Component<ToolbarLinkProps> {
|
class ToolbarLink extends React.Component<ToolbarLinkProps> {
|
||||||
render() {
|
render() {
|
||||||
return <a
|
return <a
|
||||||
className={classnames("maputnik-toolbar-link", this.props.className)}
|
className={classnames('maputnik-toolbar-link', this.props.className)}
|
||||||
href={this.props.href}
|
href={this.props.href}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
data-wd-key="toolbar:link"
|
data-wd-key="toolbar:link"
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</a>;
|
</a>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +66,7 @@ class ToolbarSelect extends React.Component<ToolbarSelectProps> {
|
|||||||
data-wd-key={this.props.wdKey}
|
data-wd-key={this.props.wdKey}
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>;
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +84,7 @@ class ToolbarAction extends React.Component<ToolbarActionProps> {
|
|||||||
onClick={this.props.onClick}
|
onClick={this.props.onClick}
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</button>;
|
</button>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,13 +93,13 @@ export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deut
|
|||||||
type AppToolbarInternalProps = {
|
type AppToolbarInternalProps = {
|
||||||
mapStyle: object
|
mapStyle: object
|
||||||
inspectModeEnabled: boolean
|
inspectModeEnabled: boolean
|
||||||
onStyleChanged: OnStyleChangedCallback
|
onStyleChanged(...args: unknown[]): unknown
|
||||||
// A new style has been uploaded
|
// A new style has been uploaded
|
||||||
onStyleOpen: OnStyleChangedCallback
|
onStyleOpen(...args: unknown[]): unknown
|
||||||
// A dict of source id's and the available source layers
|
// A dict of source id's and the available source layers
|
||||||
sources: object
|
sources: object
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
onToggleModal(modal: ModalTypes): void
|
onToggleModal(...args: unknown[]): unknown
|
||||||
onSetMapState(mapState: MapState): unknown
|
onSetMapState(mapState: MapState): unknown
|
||||||
mapState?: MapState
|
mapState?: MapState
|
||||||
renderer?: string
|
renderer?: string
|
||||||
@@ -117,7 +114,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|||||||
add: false,
|
add: false,
|
||||||
export: false,
|
export: false,
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
handleSelection(val: MapState) {
|
handleSelection(val: MapState) {
|
||||||
this.props.onSetMapState(val);
|
this.props.onSetMapState(val);
|
||||||
@@ -135,7 +132,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|||||||
const el = document.querySelector("#skip-target-"+target) as HTMLButtonElement;
|
const el = document.querySelector("#skip-target-"+target) as HTMLButtonElement;
|
||||||
el.focus();
|
el.focus();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
@@ -149,7 +146,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|||||||
id: "inspect",
|
id: "inspect",
|
||||||
group: "general",
|
group: "general",
|
||||||
title: t("Inspect"),
|
title: t("Inspect"),
|
||||||
disabled: this.props.renderer === "ol",
|
disabled: this.props.renderer === 'ol',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "filter-deuteranopia",
|
id: "filter-deuteranopia",
|
||||||
@@ -222,30 +219,22 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
|
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
|
||||||
<ToolbarAction wdKey="nav:open" onClick={() => this.props.onToggleModal("open")}>
|
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
|
||||||
<MdOpenInBrowser />
|
<MdOpenInBrowser />
|
||||||
<IconText>{t("Open")}</IconText>
|
<IconText>{t("Open")}</IconText>
|
||||||
</ToolbarAction>
|
</ToolbarAction>
|
||||||
<ToolbarAction wdKey="nav:export" onClick={() => this.props.onToggleModal("export")}>
|
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
|
||||||
<MdSave />
|
<MdSave />
|
||||||
<IconText>{t("Save")}</IconText>
|
<IconText>{t("Save")}</IconText>
|
||||||
</ToolbarAction>
|
</ToolbarAction>
|
||||||
<ToolbarAction wdKey="nav:code-editor" onClick={() => this.props.onToggleModal("codeEditor")}>
|
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
|
||||||
<MdCode />
|
|
||||||
<IconText>{t("Code Editor")}</IconText>
|
|
||||||
</ToolbarAction>
|
|
||||||
<ToolbarAction wdKey="nav:sources" onClick={() => this.props.onToggleModal("sources")}>
|
|
||||||
<MdLayers />
|
<MdLayers />
|
||||||
<IconText>{t("Data Sources")}</IconText>
|
<IconText>{t("Data Sources")}</IconText>
|
||||||
</ToolbarAction>
|
</ToolbarAction>
|
||||||
<ToolbarAction wdKey="nav:settings" onClick={() => this.props.onToggleModal("settings")}>
|
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
|
||||||
<MdSettings />
|
<MdSettings />
|
||||||
<IconText>{t("Style Settings")}</IconText>
|
<IconText>{t("Style Settings")}</IconText>
|
||||||
</ToolbarAction>
|
</ToolbarAction>
|
||||||
<ToolbarAction wdKey="nav:global-state" onClick={() => this.props.onToggleModal("globalState")}>
|
|
||||||
<MdPublic />
|
|
||||||
<IconText>{t("Global State")}</IconText>
|
|
||||||
</ToolbarAction>
|
|
||||||
|
|
||||||
<ToolbarSelect wdKey="nav:inspect">
|
<ToolbarSelect wdKey="nav:inspect">
|
||||||
<MdFindInPage />
|
<MdFindInPage />
|
||||||
@@ -302,7 +291,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
|
|||||||
</ToolbarLink>
|
</ToolbarLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>;
|
</nav>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-16
@@ -1,13 +1,14 @@
|
|||||||
import React, {type CSSProperties, type PropsWithChildren, type SyntheticEvent} from "react";
|
import React, {PropsWithChildren, SyntheticEvent} from 'react'
|
||||||
import classnames from "classnames";
|
import classnames from 'classnames'
|
||||||
import FieldDocLabel from "./FieldDocLabel";
|
import FieldDocLabel from './FieldDocLabel'
|
||||||
import Doc from "./Doc";
|
import Doc from './Doc'
|
||||||
|
|
||||||
export type BlockProps = PropsWithChildren & {
|
|
||||||
|
type BlockProps = PropsWithChildren & {
|
||||||
"data-wd-key"?: string
|
"data-wd-key"?: string
|
||||||
label?: string
|
label?: string
|
||||||
action?: React.ReactElement
|
action?: React.ReactElement
|
||||||
style?: CSSProperties
|
style?: object
|
||||||
onChange?(...args: unknown[]): unknown
|
onChange?(...args: unknown[]): unknown
|
||||||
fieldSpec?: object
|
fieldSpec?: object
|
||||||
wideMode?: boolean
|
wideMode?: boolean
|
||||||
@@ -26,13 +27,13 @@ export default class Block extends React.Component<BlockProps, BlockState> {
|
|||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
showDoc: false,
|
showDoc: false,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) {
|
onChange(e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) {
|
||||||
const value = e.target.value;
|
const value = e.target.value
|
||||||
if (this.props.onChange) {
|
if (this.props.onChange) {
|
||||||
return this.props.onChange(value === "" ? undefined : value);
|
return this.props.onChange(value === "" ? undefined : value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ export default class Block extends React.Component<BlockProps, BlockState> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
showDoc: val
|
showDoc: val
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Some fields for example <InputColor/> bind click events inside the element
|
* Some fields for example <InputColor/> bind click events inside the element
|
||||||
@@ -58,7 +59,7 @@ export default class Block extends React.Component<BlockProps, BlockState> {
|
|||||||
if (event.nativeEvent.target.nodeName !== "A") {
|
if (event.nativeEvent.target.nodeName !== "A") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <label style={this.props.style}
|
return <label style={this.props.style}
|
||||||
@@ -66,8 +67,7 @@ export default class Block extends React.Component<BlockProps, BlockState> {
|
|||||||
className={classnames({
|
className={classnames({
|
||||||
"maputnik-input-block": true,
|
"maputnik-input-block": true,
|
||||||
"maputnik-input-block--wide": this.props.wideMode,
|
"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}
|
onClick={this.onLabelClick}
|
||||||
>
|
>
|
||||||
@@ -88,17 +88,18 @@ export default class Block extends React.Component<BlockProps, BlockState> {
|
|||||||
<div className="maputnik-input-block-action">
|
<div className="maputnik-input-block-action">
|
||||||
{this.props.action}
|
{this.props.action}
|
||||||
</div>
|
</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}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
{this.props.fieldSpec &&
|
{this.props.fieldSpec &&
|
||||||
<div
|
<div
|
||||||
className="maputnik-doc-inline"
|
className="maputnik-doc-inline"
|
||||||
style={{display: this.state.showDoc ? "" : "none"}}
|
style={{display: this.state.showDoc ? '' : 'none'}}
|
||||||
>
|
>
|
||||||
<Doc fieldSpec={this.props.fieldSpec} />
|
<Doc fieldSpec={this.props.fieldSpec} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</label>;
|
</label>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
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 React from 'react'
|
||||||
import { Collapse as ReactCollapse } from "react-collapse";
|
import { Collapse as ReactCollapse } from 'react-collapse'
|
||||||
import {reducedMotionEnabled} from "../libs/accessibility";
|
import {reducedMotionEnabled} from '../libs/accessibility'
|
||||||
|
|
||||||
|
|
||||||
type CollapseProps = {
|
type CollapseProps = {
|
||||||
@@ -12,7 +12,7 @@ type CollapseProps = {
|
|||||||
export default class Collapse extends React.Component<CollapseProps> {
|
export default class Collapse extends React.Component<CollapseProps> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
isActive: true
|
isActive: true
|
||||||
};
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (reducedMotionEnabled()) {
|
if (reducedMotionEnabled()) {
|
||||||
@@ -20,14 +20,15 @@ export default class Collapse extends React.Component<CollapseProps> {
|
|||||||
<div style={{display: this.props.isActive ? "block" : "none"}}>
|
<div style={{display: this.props.isActive ? "block" : "none"}}>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return (
|
return (
|
||||||
<ReactCollapse isOpened={this.props.isActive}>
|
<ReactCollapse isOpened={this.props.isActive}>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</ReactCollapse>
|
</ReactCollapse>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import {MdArrowDropDown, MdArrowDropUp} from "react-icons/md";
|
import {MdArrowDropDown, MdArrowDropUp} from 'react-icons/md'
|
||||||
|
|
||||||
type CollapserProps = {
|
type CollapserProps = {
|
||||||
isCollapsed: boolean
|
isCollapsed: boolean
|
||||||
@@ -12,7 +12,8 @@ export default class Collapser extends React.Component<CollapserProps> {
|
|||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
...this.props.style,
|
...this.props.style,
|
||||||
};
|
}
|
||||||
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />;
|
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-19
@@ -1,10 +1,10 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import Markdown from "react-markdown";
|
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
js: "JS",
|
js: "JS",
|
||||||
android: "Android",
|
android: "Android",
|
||||||
ios: "iOS"
|
ios: "iOS",
|
||||||
|
macos: "macOS",
|
||||||
};
|
};
|
||||||
|
|
||||||
type DocProps = {
|
type DocProps = {
|
||||||
@@ -15,7 +15,7 @@ type DocProps = {
|
|||||||
doc?: string
|
doc?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"sdk-support"?: {
|
'sdk-support'?: {
|
||||||
[key: string]: typeof headers
|
[key: string]: typeof headers
|
||||||
}
|
}
|
||||||
docUrl?: string,
|
docUrl?: string,
|
||||||
@@ -28,7 +28,7 @@ export default class Doc extends React.Component<DocProps> {
|
|||||||
const {fieldSpec} = this.props;
|
const {fieldSpec} = this.props;
|
||||||
|
|
||||||
const {doc, values, docUrl, docUrlLinkText} = fieldSpec;
|
const {doc, values, docUrl, docUrlLinkText} = fieldSpec;
|
||||||
const sdkSupport = fieldSpec["sdk-support"];
|
const sdkSupport = fieldSpec['sdk-support'];
|
||||||
|
|
||||||
const renderValues = (
|
const renderValues = (
|
||||||
!!values &&
|
!!values &&
|
||||||
@@ -37,23 +37,11 @@ export default class Doc extends React.Component<DocProps> {
|
|||||||
!Array.isArray(values)
|
!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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{doc &&
|
{doc &&
|
||||||
<div className="SpecDoc">
|
<div className="SpecDoc">
|
||||||
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'>
|
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'>{doc}</div>
|
||||||
<Markdown components={{
|
|
||||||
a: ({node: _node, href, children, ...props}) => <a href={href} target="_blank" {...props}>{children}</a>,
|
|
||||||
}}>{doc}</Markdown>
|
|
||||||
</div>
|
|
||||||
{renderValues &&
|
{renderValues &&
|
||||||
<ul className="SpecDoc__values">
|
<ul className="SpecDoc__values">
|
||||||
{Object.entries(values).map(([key, value]) => {
|
{Object.entries(values).map(([key, value]) => {
|
||||||
@@ -86,7 +74,7 @@ export default class Doc extends React.Component<DocProps> {
|
|||||||
<td>{key}</td>
|
<td>{key}</td>
|
||||||
{Object.keys(headers).map((k) => {
|
{Object.keys(headers).map((k) => {
|
||||||
if (Object.prototype.hasOwnProperty.call(supportObj, k)) {
|
if (Object.prototype.hasOwnProperty.call(supportObj, k)) {
|
||||||
return <td key={k}>{sdkSupportToJsx(supportObj[k as keyof typeof headers])}</td>;
|
return <td key={k}>{supportObj[k as keyof typeof headers]}</td>;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return <td key={k}>no</td>;
|
return <td key={k}>no</td>;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import InputArray, { type InputArrayProps } from "./InputArray";
|
import React from 'react'
|
||||||
import Fieldset from "./Fieldset";
|
import InputArray, { FieldArrayProps as InputArrayProps } from './InputArray'
|
||||||
|
import Fieldset from './Fieldset'
|
||||||
|
|
||||||
type FieldArrayProps = InputArrayProps & {
|
type FieldArrayProps = InputArrayProps & {
|
||||||
name?: string
|
name?: string
|
||||||
@@ -8,12 +9,11 @@ type FieldArrayProps = InputArrayProps & {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const FieldArray: React.FC<FieldArrayProps> = (props) => {
|
export default class FieldArray extends React.Component<FieldArrayProps> {
|
||||||
return (
|
render() {
|
||||||
<Fieldset label={props.label} fieldSpec={props.fieldSpec}>
|
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
<InputArray {...props} />
|
<InputArray {...this.props} />
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldArray;
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Block from "./Block";
|
import React from 'react'
|
||||||
import InputAutocomplete, { type InputAutocompleteProps } from "./InputAutocomplete";
|
import Block from './Block'
|
||||||
|
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
|
||||||
|
|
||||||
|
|
||||||
type FieldAutocompleteProps = InputAutocompleteProps & {
|
type FieldAutocompleteProps = InputAutocompleteProps & {
|
||||||
@@ -7,12 +8,11 @@ type FieldAutocompleteProps = InputAutocompleteProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldAutocomplete: React.FC<FieldAutocompleteProps> = (props) => {
|
export default class FieldAutocomplete extends React.Component<FieldAutocompleteProps> {
|
||||||
return (
|
render() {
|
||||||
<Block label={props.label}>
|
return <Block label={this.props.label}>
|
||||||
<InputAutocomplete {...props} />
|
<InputAutocomplete {...this.props} />
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldAutocomplete;
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Block from "./Block";
|
import React from 'react'
|
||||||
import InputCheckbox, {type InputCheckboxProps} from "./InputCheckbox";
|
import Block from './Block'
|
||||||
|
import InputCheckbox, {InputCheckboxProps} from './InputCheckbox'
|
||||||
|
|
||||||
|
|
||||||
type FieldCheckboxProps = InputCheckboxProps & {
|
type FieldCheckboxProps = InputCheckboxProps & {
|
||||||
@@ -7,12 +8,11 @@ type FieldCheckboxProps = InputCheckboxProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldCheckbox: React.FC<FieldCheckboxProps> = (props) => {
|
export default class FieldCheckbox extends React.Component<FieldCheckboxProps> {
|
||||||
return (
|
render() {
|
||||||
<Block label={props.label}>
|
return <Block label={this.props.label}>
|
||||||
<InputCheckbox {...props} />
|
<InputCheckbox {...this.props} />
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldCheckbox;
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Block from "./Block";
|
import React from 'react'
|
||||||
import InputColor, {type InputColorProps} from "./InputColor";
|
import Block from './Block'
|
||||||
|
import InputColor, {InputColorProps} from './InputColor'
|
||||||
|
|
||||||
|
|
||||||
type FieldColorProps = InputColorProps & {
|
type FieldColorProps = InputColorProps & {
|
||||||
@@ -10,12 +11,11 @@ type FieldColorProps = InputColorProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldColor: React.FC<FieldColorProps> = (props) => {
|
export default class FieldColor extends React.Component<FieldColorProps> {
|
||||||
return (
|
render() {
|
||||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
<InputColor {...props} />
|
<InputColor {...this.props} />
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldColor;
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
|
|
||||||
import Block from "./Block";
|
import Block from './Block'
|
||||||
import InputString from "./InputString";
|
import InputString from './InputString'
|
||||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
type FieldCommentInternalProps = {
|
type FieldCommentInternalProps = {
|
||||||
value?: string
|
value?: string
|
||||||
@@ -10,31 +10,29 @@ type FieldCommentInternalProps = {
|
|||||||
error: {message: string}
|
error: {message: string}
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
const FieldCommentInternal: React.FC<FieldCommentInternalProps> = (props) => {
|
class FieldCommentInternal extends React.Component<FieldCommentInternalProps> {
|
||||||
const t = props.t;
|
render() {
|
||||||
const fieldSpec = {
|
const t = this.props.t;
|
||||||
doc: t(
|
const fieldSpec = {
|
||||||
"Comments for the current layer. This is non-standard and not in the spec."
|
doc: t("Comments for the current layer. This is non-standard and not in the spec."),
|
||||||
),
|
};
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return <Block
|
||||||
<Block
|
|
||||||
label={t("Comments")}
|
label={t("Comments")}
|
||||||
fieldSpec={fieldSpec}
|
fieldSpec={fieldSpec}
|
||||||
data-wd-key="layer-comment"
|
data-wd-key="layer-comment"
|
||||||
error={props.error}
|
error={this.props.error}
|
||||||
>
|
>
|
||||||
<InputString
|
<InputString
|
||||||
multi={true}
|
multi={true}
|
||||||
value={props.value}
|
value={this.props.value}
|
||||||
onChange={props.onChange}
|
onChange={this.props.onChange}
|
||||||
default={t("Comment...")}
|
default={t("Comment...")}
|
||||||
data-wd-key="layer-comment.input"
|
data-wd-key="layer-comment.input"
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const FieldComment = withTranslation()(FieldCommentInternal);
|
const FieldComment = withTranslation()(FieldCommentInternal);
|
||||||
export default FieldComment;
|
export default FieldComment;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { type JSX } from "react";
|
import React from 'react'
|
||||||
import {MdInfoOutline, MdHighlightOff} from "react-icons/md";
|
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
|
||||||
|
|
||||||
type FieldDocLabelProps = {
|
type FieldDocLabelProps = {
|
||||||
label: JSX.Element | string | undefined
|
label: JSX.Element | string | undefined
|
||||||
@@ -9,45 +9,57 @@ type FieldDocLabelProps = {
|
|||||||
onToggleDoc?(...args: unknown[]): unknown
|
onToggleDoc?(...args: unknown[]): unknown
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FieldDocLabelState = {
|
||||||
|
open: boolean
|
||||||
|
};
|
||||||
|
|
||||||
const FieldDocLabel: React.FC<FieldDocLabelProps> = (props) => {
|
export default class FieldDocLabel extends React.Component<FieldDocLabelProps, FieldDocLabelState> {
|
||||||
const [open, setOpen] = React.useState(false);
|
constructor (props: FieldDocLabelProps) {
|
||||||
|
super(props);
|
||||||
const onToggleDoc = (state: boolean) => {
|
this.state = {
|
||||||
setOpen(state);
|
open: false,
|
||||||
if (props.onToggleDoc) {
|
|
||||||
props.onToggleDoc(state);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const { label, fieldSpec } = props;
|
onToggleDoc = (open: boolean) => {
|
||||||
const { doc } = fieldSpec || {};
|
this.setState({
|
||||||
|
open,
|
||||||
|
}, () => {
|
||||||
|
if (this.props.onToggleDoc) {
|
||||||
|
this.props.onToggleDoc(this.state.open);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (doc) {
|
render() {
|
||||||
return (
|
const {label, fieldSpec} = this.props;
|
||||||
<label className="maputnik-doc-wrapper">
|
const {doc} = fieldSpec || {};
|
||||||
|
|
||||||
|
if (doc) {
|
||||||
|
return <label className="maputnik-doc-wrapper">
|
||||||
<div className="maputnik-doc-target">
|
<div className="maputnik-doc-target">
|
||||||
{label}
|
{label}
|
||||||
{"\xa0"}
|
{'\xa0'}
|
||||||
<button
|
<button
|
||||||
aria-label={open ? "close property documentation" : "open property documentation"}
|
aria-label={this.state.open ? "close property documentation" : "open property documentation"}
|
||||||
className={`maputnik-doc-button maputnik-doc-button--${open ? "open" : "closed"}`}
|
className={`maputnik-doc-button maputnik-doc-button--${this.state.open ? 'open' : 'closed'}`}
|
||||||
onClick={() => onToggleDoc(!open)}
|
onClick={() => this.onToggleDoc(!this.state.open)}
|
||||||
data-wd-key={"field-doc-button-" + label}
|
data-wd-key={'field-doc-button-'+label}
|
||||||
>
|
>
|
||||||
{open ? <MdHighlightOff /> : <MdInfoOutline />}
|
{this.state.open ? <MdHighlightOff /> : <MdInfoOutline />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
);
|
}
|
||||||
} else if (label) {
|
else if (label) {
|
||||||
return (
|
return <label className="maputnik-doc-wrapper">
|
||||||
<label className="maputnik-doc-wrapper">
|
<div className="maputnik-doc-target">
|
||||||
<div className="maputnik-doc-target">{label}</div>
|
{label}
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
);
|
}
|
||||||
|
else {
|
||||||
|
<div />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return <div />;
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export default FieldDocLabel;
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import InputDynamicArray, {type InputDynamicArrayProps} from "./InputDynamicArray";
|
import React from 'react'
|
||||||
import Fieldset from "./Fieldset";
|
import InputDynamicArray, {FieldDynamicArrayProps as InputDynamicArrayProps} from './InputDynamicArray'
|
||||||
|
import Fieldset from './Fieldset'
|
||||||
|
|
||||||
type FieldDynamicArrayProps = InputDynamicArrayProps & {
|
type FieldDynamicArrayProps = InputDynamicArrayProps & {
|
||||||
name?: string
|
name?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
const FieldDynamicArray: React.FC<FieldDynamicArrayProps> = (props) => {
|
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
|
||||||
return (
|
render() {
|
||||||
<Fieldset label={props.label}>
|
return <Fieldset label={this.props.label}>
|
||||||
<InputDynamicArray {...props} />
|
<InputDynamicArray {...this.props} />
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldDynamicArray;
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import InputEnum, {type InputEnumProps} from "./InputEnum";
|
import React from 'react'
|
||||||
import Fieldset from "./Fieldset";
|
import InputEnum, {InputEnumProps} from './InputEnum'
|
||||||
|
import Fieldset from './Fieldset';
|
||||||
|
|
||||||
|
|
||||||
type FieldEnumProps = InputEnumProps & {
|
type FieldEnumProps = InputEnumProps & {
|
||||||
@@ -10,12 +11,10 @@ type FieldEnumProps = InputEnumProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldEnum: React.FC<FieldEnumProps> = (props) => {
|
export default class FieldEnum extends React.Component<FieldEnumProps> {
|
||||||
return (
|
render() {
|
||||||
<Fieldset label={props.label} fieldSpec={props.fieldSpec}>
|
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
<InputEnum {...props} />
|
<InputEnum {...this.props} />
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldEnum;
|
|
||||||
+231
-230
@@ -1,12 +1,11 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
|
|
||||||
import SpecProperty from "./_SpecProperty";
|
import SpecProperty from './_SpecProperty'
|
||||||
import DataProperty, { type Stop } from "./_DataProperty";
|
import DataProperty, { Stop } from './_DataProperty'
|
||||||
import ZoomProperty from "./_ZoomProperty";
|
import ZoomProperty from './_ZoomProperty'
|
||||||
import ExpressionProperty from "./_ExpressionProperty";
|
import ExpressionProperty from './_ExpressionProperty'
|
||||||
import {function as styleFunction} from "@maplibre/maplibre-gl-style-spec";
|
import {function as styleFunction} from '@maplibre/maplibre-gl-style-spec';
|
||||||
import {findDefaultFromSpec} from "../libs/spec-helper";
|
import {findDefaultFromSpec} from '../libs/spec-helper';
|
||||||
import { type MappedLayerErrors } from "../libs/definitions";
|
|
||||||
|
|
||||||
|
|
||||||
function isLiteralExpression(value: any) {
|
function isLiteralExpression(value: any) {
|
||||||
@@ -23,9 +22,9 @@ function isGetExpression(value: any) {
|
|||||||
|
|
||||||
function isZoomField(value: any) {
|
function isZoomField(value: any) {
|
||||||
return (
|
return (
|
||||||
typeof(value) === "object" &&
|
typeof(value) === 'object' &&
|
||||||
value.stops &&
|
value.stops &&
|
||||||
typeof(value.property) === "undefined" &&
|
typeof(value.property) === 'undefined' &&
|
||||||
Array.isArray(value.stops) &&
|
Array.isArray(value.stops) &&
|
||||||
value.stops.length > 1 &&
|
value.stops.length > 1 &&
|
||||||
value.stops.every((stop: Stop) => {
|
value.stops.every((stop: Stop) => {
|
||||||
@@ -39,7 +38,7 @@ function isZoomField(value: any) {
|
|||||||
|
|
||||||
function isIdentityProperty(value: any) {
|
function isIdentityProperty(value: any) {
|
||||||
return (
|
return (
|
||||||
typeof(value) === "object" &&
|
typeof(value) === 'object' &&
|
||||||
value.type === "identity" &&
|
value.type === "identity" &&
|
||||||
Object.prototype.hasOwnProperty.call(value, "property")
|
Object.prototype.hasOwnProperty.call(value, "property")
|
||||||
);
|
);
|
||||||
@@ -47,16 +46,16 @@ function isIdentityProperty(value: any) {
|
|||||||
|
|
||||||
function isDataStopProperty(value: any) {
|
function isDataStopProperty(value: any) {
|
||||||
return (
|
return (
|
||||||
typeof(value) === "object" &&
|
typeof(value) === 'object' &&
|
||||||
value.stops &&
|
value.stops &&
|
||||||
typeof(value.property) !== "undefined" &&
|
typeof(value.property) !== 'undefined' &&
|
||||||
value.stops.length > 1 &&
|
value.stops.length > 1 &&
|
||||||
Array.isArray(value.stops) &&
|
Array.isArray(value.stops) &&
|
||||||
value.stops.every((stop: Stop) => {
|
value.stops.every((stop: Stop) => {
|
||||||
return (
|
return (
|
||||||
Array.isArray(stop) &&
|
Array.isArray(stop) &&
|
||||||
stop.length === 2 &&
|
stop.length === 2 &&
|
||||||
typeof(stop[0]) === "object"
|
typeof(stop[0]) === 'object'
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -91,18 +90,6 @@ function getDataType(value: any, fieldSpec={} as any) {
|
|||||||
else if (fieldSpec.type === "array" && isArrayOfPrimatives(value)) {
|
else if (fieldSpec.type === "array" && isArrayOfPrimatives(value)) {
|
||||||
return "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)) {
|
else if (isZoomField(value)) {
|
||||||
return "zoom_function";
|
return "zoom_function";
|
||||||
}
|
}
|
||||||
@@ -120,287 +107,301 @@ type FieldFunctionProps = {
|
|||||||
fieldName: string
|
fieldName: string
|
||||||
fieldType: string
|
fieldType: string
|
||||||
fieldSpec: any
|
fieldSpec: any
|
||||||
errors?: MappedLayerErrors
|
errors?: {[key: string]: {message: string}}
|
||||||
value?: any
|
value?: any
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FieldFunctionState = {
|
||||||
|
dataType: string
|
||||||
|
isEditing: boolean
|
||||||
|
}
|
||||||
|
|
||||||
/** Supports displaying spec field for zoom function objects
|
/** Supports displaying spec field for zoom function objects
|
||||||
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
|
||||||
*/
|
*/
|
||||||
const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
|
export default class FieldFunction extends React.Component<FieldFunctionProps, FieldFunctionState> {
|
||||||
const [dataType, setDataType] = React.useState(
|
constructor (props: FieldFunctionProps) {
|
||||||
getDataType(props.value, props.fieldSpec)
|
super(props);
|
||||||
);
|
this.state = {
|
||||||
const [isEditing, setIsEditing] = React.useState(false);
|
dataType: getDataType(props.value, props.fieldSpec),
|
||||||
|
isEditing: false,
|
||||||
React.useEffect(() => {
|
|
||||||
if (!isEditing) {
|
|
||||||
setDataType(getDataType(props.value, props.fieldSpec));
|
|
||||||
}
|
}
|
||||||
}, [props.value, props.fieldSpec, isEditing]);
|
}
|
||||||
|
|
||||||
const getFieldFunctionType = (fieldSpec: any) => {
|
static getDerivedStateFromProps(props: Readonly<FieldFunctionProps>, state: FieldFunctionState) {
|
||||||
|
// Because otherwise when editing values we end up accidentally changing field type.
|
||||||
|
if (state.isEditing) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return {
|
||||||
|
isEditing: false,
|
||||||
|
dataType: getDataType(props.value, props.fieldSpec)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getFieldFunctionType(fieldSpec: any) {
|
||||||
if (fieldSpec.expression.interpolated) {
|
if (fieldSpec.expression.interpolated) {
|
||||||
return "exponential";
|
return "exponential"
|
||||||
}
|
}
|
||||||
if (fieldSpec.type === "number") {
|
if (fieldSpec.type === "number") {
|
||||||
return "interval";
|
return "interval"
|
||||||
}
|
}
|
||||||
return "categorical";
|
return "categorical"
|
||||||
};
|
}
|
||||||
|
|
||||||
const addStop = () => {
|
addStop = () => {
|
||||||
const stops = props.value.stops.slice(0);
|
const stops = this.props.value.stops.slice(0)
|
||||||
const lastStop = stops[stops.length - 1];
|
const lastStop = stops[stops.length - 1]
|
||||||
if (typeof lastStop[0] === "object") {
|
if (typeof lastStop[0] === "object") {
|
||||||
stops.push([
|
stops.push([
|
||||||
{ zoom: lastStop[0].zoom + 1, value: lastStop[0].value },
|
{zoom: lastStop[0].zoom + 1, value: lastStop[0].value},
|
||||||
lastStop[1],
|
lastStop[1]
|
||||||
]);
|
])
|
||||||
} else {
|
}
|
||||||
stops.push([lastStop[0] + 1, lastStop[1]]);
|
else {
|
||||||
|
stops.push([lastStop[0] + 1, lastStop[1]])
|
||||||
}
|
}
|
||||||
|
|
||||||
const changedValue = {
|
const changedValue = {
|
||||||
...props.value,
|
...this.props.value,
|
||||||
stops: stops,
|
stops: stops,
|
||||||
};
|
|
||||||
|
|
||||||
props.onChange(props.fieldName, changedValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteExpression = () => {
|
|
||||||
const { fieldSpec, fieldName } = props;
|
|
||||||
props.onChange(fieldName, fieldSpec.default);
|
|
||||||
setDataType("value");
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteStop = (stopIdx: number) => {
|
|
||||||
const stops = props.value.stops.slice(0);
|
|
||||||
stops.splice(stopIdx, 1);
|
|
||||||
|
|
||||||
let changedValue: any = {
|
|
||||||
...props.value,
|
|
||||||
stops: stops,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (stops.length === 1) {
|
|
||||||
changedValue = stops[0][1];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
props.onChange(props.fieldName, changedValue);
|
this.props.onChange(this.props.fieldName, changedValue)
|
||||||
};
|
}
|
||||||
|
|
||||||
const makeZoomFunction = () => {
|
deleteExpression = () => {
|
||||||
const { value } = props;
|
const {fieldSpec, fieldName} = this.props;
|
||||||
|
this.props.onChange(fieldName, fieldSpec.default);
|
||||||
|
this.setState({
|
||||||
|
dataType: "value",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let zoomFunc: any;
|
deleteStop = (stopIdx: number) => {
|
||||||
if (typeof value === "object") {
|
const stops = this.props.value.stops.slice(0)
|
||||||
|
stops.splice(stopIdx, 1)
|
||||||
|
|
||||||
|
let changedValue = {
|
||||||
|
...this.props.value,
|
||||||
|
stops: stops,
|
||||||
|
}
|
||||||
|
|
||||||
|
if(stops.length === 1) {
|
||||||
|
changedValue = stops[0][1]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onChange(this.props.fieldName, changedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
makeZoomFunction = () => {
|
||||||
|
const {value} = this.props;
|
||||||
|
|
||||||
|
let zoomFunc;
|
||||||
|
if (typeof(value) === "object") {
|
||||||
if (value.stops) {
|
if (value.stops) {
|
||||||
zoomFunc = {
|
zoomFunc = {
|
||||||
base: value.base,
|
base: value.base,
|
||||||
stops: value.stops.map((stop: Stop) => {
|
stops: value.stops.map((stop: Stop) => {
|
||||||
return [stop[0].zoom, stop[1] || findDefaultFromSpec(props.fieldSpec)];
|
return [stop[0].zoom, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
|
||||||
}),
|
})
|
||||||
};
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
zoomFunc = {
|
zoomFunc = {
|
||||||
base: value.base,
|
base: value.base,
|
||||||
stops: [
|
stops: [
|
||||||
[6, findDefaultFromSpec(props.fieldSpec)],
|
[6, findDefaultFromSpec(this.props.fieldSpec)],
|
||||||
[10, findDefaultFromSpec(props.fieldSpec)],
|
[10, findDefaultFromSpec(this.props.fieldSpec)]
|
||||||
],
|
]
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
zoomFunc = {
|
zoomFunc = {
|
||||||
stops: [
|
stops: [
|
||||||
[6, value || findDefaultFromSpec(props.fieldSpec)],
|
[6, value || findDefaultFromSpec(this.props.fieldSpec)],
|
||||||
[10, value || findDefaultFromSpec(props.fieldSpec)],
|
[10, value || findDefaultFromSpec(this.props.fieldSpec)]
|
||||||
],
|
]
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
props.onChange(props.fieldName, zoomFunc);
|
this.props.onChange(this.props.fieldName, zoomFunc)
|
||||||
};
|
}
|
||||||
|
|
||||||
const undoExpression = () => {
|
undoExpression = () => {
|
||||||
const { value, fieldName } = props;
|
const {value, fieldName} = this.props;
|
||||||
|
|
||||||
if (isGetExpression(value)) {
|
if (isGetExpression(value)) {
|
||||||
props.onChange(fieldName, {
|
this.props.onChange(fieldName, {
|
||||||
type: "identity",
|
"type": "identity",
|
||||||
property: value[1],
|
"property": value[1]
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
dataType: "value",
|
||||||
});
|
});
|
||||||
setDataType("value");
|
|
||||||
} else if (isLiteralExpression(value)) {
|
|
||||||
props.onChange(fieldName, value[1]);
|
|
||||||
setDataType("value");
|
|
||||||
}
|
}
|
||||||
};
|
else if (isLiteralExpression(value)) {
|
||||||
|
this.props.onChange(fieldName, value[1]);
|
||||||
|
this.setState({
|
||||||
|
dataType: "value",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const canUndo = () => {
|
canUndo = () => {
|
||||||
const { value, fieldSpec } = props;
|
const {value, fieldSpec} = this.props;
|
||||||
return (
|
return (
|
||||||
isGetExpression(value) ||
|
isGetExpression(value) ||
|
||||||
isLiteralExpression(value) ||
|
isLiteralExpression(value) ||
|
||||||
isPrimative(value) ||
|
isPrimative(value) ||
|
||||||
(Array.isArray(value) && fieldSpec.type === "array")
|
(Array.isArray(value) && fieldSpec.type === "array")
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const makeExpression = () => {
|
makeExpression = () => {
|
||||||
const { value, fieldSpec } = props;
|
const {value, fieldSpec} = this.props;
|
||||||
let expression;
|
let expression;
|
||||||
|
|
||||||
if (typeof value === "object" && "stops" in value) {
|
if (typeof(value) === "object" && 'stops' in value) {
|
||||||
expression = styleFunction.convertFunction(value, fieldSpec);
|
expression = styleFunction.convertFunction(value, fieldSpec);
|
||||||
} else if (isIdentityProperty(value)) {
|
|
||||||
expression = ["get", value.property];
|
|
||||||
} else {
|
|
||||||
expression = ["literal", value || props.fieldSpec.default];
|
|
||||||
}
|
}
|
||||||
props.onChange(props.fieldName, expression);
|
else if (isIdentityProperty(value)) {
|
||||||
};
|
expression = ["get", value.property];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
expression = ["literal", value || this.props.fieldSpec.default];
|
||||||
|
}
|
||||||
|
this.props.onChange(this.props.fieldName, expression);
|
||||||
|
}
|
||||||
|
|
||||||
const makeDataFunction = () => {
|
makeDataFunction = () => {
|
||||||
const functionType = getFieldFunctionType(props.fieldSpec);
|
const functionType = this.getFieldFunctionType(this.props.fieldSpec);
|
||||||
const stopValue = functionType === "categorical" ? "" : 0;
|
const stopValue = functionType === 'categorical' ? '' : 0;
|
||||||
const { value } = props;
|
const {value} = this.props;
|
||||||
let dataFunc;
|
let dataFunc;
|
||||||
|
|
||||||
if (typeof value === "object") {
|
if (typeof(value) === "object") {
|
||||||
if (value.stops) {
|
if (value.stops) {
|
||||||
dataFunc = {
|
dataFunc = {
|
||||||
property: "",
|
property: "",
|
||||||
type: functionType,
|
type: functionType,
|
||||||
base: value.base,
|
base: value.base,
|
||||||
stops: value.stops.map((stop: Stop) => {
|
stops: value.stops.map((stop: Stop) => {
|
||||||
return [{ zoom: stop[0], value: stopValue }, stop[1] || findDefaultFromSpec(props.fieldSpec)];
|
return [{zoom: stop[0], value: stopValue}, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
|
||||||
}),
|
})
|
||||||
};
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
dataFunc = {
|
dataFunc = {
|
||||||
property: "",
|
property: "",
|
||||||
type: functionType,
|
type: functionType,
|
||||||
base: value.base,
|
base: value.base,
|
||||||
stops: [
|
stops: [
|
||||||
[{ zoom: 6, value: stopValue }, findDefaultFromSpec(props.fieldSpec)],
|
[{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)],
|
||||||
[{ zoom: 10, value: stopValue }, findDefaultFromSpec(props.fieldSpec)],
|
[{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)]
|
||||||
],
|
]
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
dataFunc = {
|
dataFunc = {
|
||||||
property: "",
|
property: "",
|
||||||
type: functionType,
|
type: functionType,
|
||||||
base: value.base,
|
base: value.base,
|
||||||
stops: [
|
stops: [
|
||||||
[{ zoom: 6, value: stopValue }, props.value || findDefaultFromSpec(props.fieldSpec)],
|
[{zoom: 6, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)],
|
||||||
[{ zoom: 10, value: stopValue }, props.value || findDefaultFromSpec(props.fieldSpec)],
|
[{zoom: 10, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)]
|
||||||
],
|
]
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
props.onChange(props.fieldName, dataFunc);
|
this.props.onChange(this.props.fieldName, dataFunc)
|
||||||
};
|
|
||||||
|
|
||||||
const makeElevationFunction = () => {
|
|
||||||
const expression = [
|
|
||||||
"interpolate",
|
|
||||||
["linear"],
|
|
||||||
["elevation"],
|
|
||||||
0,
|
|
||||||
"black",
|
|
||||||
2000,
|
|
||||||
"white"
|
|
||||||
];
|
|
||||||
|
|
||||||
props.onChange(props.fieldName, expression);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMarkEditing = () => {
|
|
||||||
setIsEditing(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onUnmarkEditing = () => {
|
|
||||||
setIsEditing(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const propClass =
|
|
||||||
props.fieldSpec.default === props.value ? "maputnik-default-property" : "maputnik-modified-property";
|
|
||||||
|
|
||||||
let specField;
|
|
||||||
|
|
||||||
if (dataType === "expression") {
|
|
||||||
specField = (
|
|
||||||
<ExpressionProperty
|
|
||||||
errors={props.errors}
|
|
||||||
onChange={props.onChange.bind(null, props.fieldName)}
|
|
||||||
canUndo={canUndo}
|
|
||||||
onUndo={undoExpression}
|
|
||||||
onDelete={deleteExpression}
|
|
||||||
fieldType={props.fieldType}
|
|
||||||
fieldName={props.fieldName}
|
|
||||||
fieldSpec={props.fieldSpec}
|
|
||||||
value={props.value}
|
|
||||||
onFocus={onMarkEditing}
|
|
||||||
onBlur={onUnmarkEditing}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (dataType === "zoom_function") {
|
|
||||||
specField = (
|
|
||||||
<ZoomProperty
|
|
||||||
errors={props.errors}
|
|
||||||
onChange={props.onChange.bind(null)}
|
|
||||||
fieldType={props.fieldType}
|
|
||||||
fieldName={props.fieldName}
|
|
||||||
fieldSpec={props.fieldSpec}
|
|
||||||
value={props.value}
|
|
||||||
onDeleteStop={deleteStop}
|
|
||||||
onAddStop={addStop}
|
|
||||||
onChangeToDataFunction={makeDataFunction}
|
|
||||||
onExpressionClick={makeExpression}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (dataType === "data_function") {
|
|
||||||
specField = (
|
|
||||||
<DataProperty
|
|
||||||
errors={props.errors}
|
|
||||||
onChange={props.onChange.bind(null)}
|
|
||||||
fieldType={props.fieldType}
|
|
||||||
fieldName={props.fieldName}
|
|
||||||
fieldSpec={props.fieldSpec}
|
|
||||||
value={props.value}
|
|
||||||
onDeleteStop={deleteStop}
|
|
||||||
onAddStop={addStop}
|
|
||||||
onChangeToZoomFunction={makeZoomFunction}
|
|
||||||
onExpressionClick={makeExpression}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
specField = (
|
|
||||||
<SpecProperty
|
|
||||||
errors={props.errors}
|
|
||||||
onChange={props.onChange.bind(null)}
|
|
||||||
fieldType={props.fieldType}
|
|
||||||
fieldName={props.fieldName}
|
|
||||||
fieldSpec={props.fieldSpec}
|
|
||||||
value={props.value}
|
|
||||||
onZoomClick={makeZoomFunction}
|
|
||||||
onDataClick={makeDataFunction}
|
|
||||||
onExpressionClick={makeExpression}
|
|
||||||
onElevationClick={makeElevationFunction}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
onMarkEditing = () => {
|
||||||
<div className={propClass} data-wd-key={"spec-field-container:" + props.fieldName}>
|
this.setState({isEditing: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmarkEditing = () => {
|
||||||
|
this.setState({isEditing: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {dataType} = this.state;
|
||||||
|
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
|
||||||
|
let specField;
|
||||||
|
|
||||||
|
if (dataType === "expression") {
|
||||||
|
specField = (
|
||||||
|
<ExpressionProperty
|
||||||
|
errors={this.props.errors}
|
||||||
|
onChange={this.props.onChange.bind(this, this.props.fieldName)}
|
||||||
|
canUndo={this.canUndo}
|
||||||
|
onUndo={this.undoExpression}
|
||||||
|
onDelete={this.deleteExpression}
|
||||||
|
fieldType={this.props.fieldType}
|
||||||
|
fieldName={this.props.fieldName}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
value={this.props.value}
|
||||||
|
onFocus={this.onMarkEditing}
|
||||||
|
onBlur={this.onUnmarkEditing}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (dataType === "zoom_function") {
|
||||||
|
specField = (
|
||||||
|
<ZoomProperty
|
||||||
|
errors={this.props.errors}
|
||||||
|
onChange={this.props.onChange.bind(this)}
|
||||||
|
fieldType={this.props.fieldType}
|
||||||
|
fieldName={this.props.fieldName}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
value={this.props.value}
|
||||||
|
onDeleteStop={this.deleteStop}
|
||||||
|
onAddStop={this.addStop}
|
||||||
|
onChangeToDataFunction={this.makeDataFunction}
|
||||||
|
onExpressionClick={this.makeExpression}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else if (dataType === "data_function") {
|
||||||
|
// TODO: Rename to FieldFunction **this file** shouldn't be called that
|
||||||
|
specField = (
|
||||||
|
<DataProperty
|
||||||
|
errors={this.props.errors}
|
||||||
|
onChange={this.props.onChange.bind(this)}
|
||||||
|
fieldType={this.props.fieldType}
|
||||||
|
fieldName={this.props.fieldName}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
value={this.props.value}
|
||||||
|
onDeleteStop={this.deleteStop}
|
||||||
|
onAddStop={this.addStop}
|
||||||
|
onChangeToZoomFunction={this.makeZoomFunction}
|
||||||
|
onExpressionClick={this.makeExpression}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
specField = (
|
||||||
|
<SpecProperty
|
||||||
|
errors={this.props.errors}
|
||||||
|
onChange={this.props.onChange.bind(this)}
|
||||||
|
fieldType={this.props.fieldType}
|
||||||
|
fieldName={this.props.fieldName}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
value={this.props.value}
|
||||||
|
onZoomClick={this.makeZoomFunction}
|
||||||
|
onDataClick={this.makeDataFunction}
|
||||||
|
onExpressionClick={this.makeExpression}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <div className={propClass} data-wd-key={"spec-field-container:"+this.props.fieldName}>
|
||||||
{specField}
|
{specField}
|
||||||
</div>
|
</div>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldFunction;
|
|
||||||
|
|||||||
+15
-15
@@ -1,7 +1,8 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||||
import Block from "./Block";
|
import Block from './Block'
|
||||||
import InputString from "./InputString";
|
import InputString from './InputString'
|
||||||
|
|
||||||
type FieldIdProps = {
|
type FieldIdProps = {
|
||||||
value: string
|
value: string
|
||||||
@@ -10,19 +11,18 @@ type FieldIdProps = {
|
|||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
};
|
};
|
||||||
|
|
||||||
const FieldId: React.FC<FieldIdProps> = (props) => {
|
export default class FieldId extends React.Component<FieldIdProps> {
|
||||||
return (
|
render() {
|
||||||
<Block label="ID" fieldSpec={latest.layer.id}
|
return <Block label="ID" fieldSpec={latest.layer.id}
|
||||||
data-wd-key={props.wdKey}
|
|
||||||
error={props.error}
|
data-wd-key={this.props.wdKey}
|
||||||
|
error={this.props.error}
|
||||||
>
|
>
|
||||||
<InputString
|
<InputString
|
||||||
value={props.value}
|
value={this.props.value}
|
||||||
onInput={props.onChange}
|
onInput={this.props.onChange}
|
||||||
data-wd-key={props.wdKey + ".input"}
|
data-wd-key={this.props.wdKey + ".input"}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldId;
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import InputJson, {type InputJsonProps} from "./InputJson";
|
import React from 'react'
|
||||||
|
import InputJson, {InputJsonProps} from './InputJson'
|
||||||
|
|
||||||
|
|
||||||
type FieldJsonProps = InputJsonProps & {};
|
type FieldJsonProps = InputJsonProps & {};
|
||||||
|
|
||||||
|
|
||||||
const FieldJson: React.FC<FieldJsonProps> = (props) => {
|
export default class FieldJson extends React.Component<FieldJsonProps> {
|
||||||
return <InputJson {...props} />;
|
render() {
|
||||||
};
|
return <InputJson {...this.props} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default FieldJson;
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
|
|
||||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||||
import Block from "./Block";
|
import Block from './Block'
|
||||||
import InputNumber from "./InputNumber";
|
import InputNumber from './InputNumber'
|
||||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
type FieldMaxZoomInternalProps = {
|
type FieldMaxZoomInternalProps = {
|
||||||
value?: number
|
value?: number
|
||||||
@@ -11,25 +11,25 @@ type FieldMaxZoomInternalProps = {
|
|||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
const FieldMaxZoomInternal: React.FC<FieldMaxZoomInternalProps> = (props) => {
|
class FieldMaxZoomInternal extends React.Component<FieldMaxZoomInternalProps> {
|
||||||
const t = props.t;
|
render() {
|
||||||
return (
|
const t = this.props.t;
|
||||||
<Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom}
|
return <Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom}
|
||||||
error={props.error}
|
error={this.props.error}
|
||||||
data-wd-key="max-zoom"
|
data-wd-key="max-zoom"
|
||||||
>
|
>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
allowRange={true}
|
allowRange={true}
|
||||||
value={props.value}
|
value={this.props.value}
|
||||||
onChange={props.onChange}
|
onChange={this.props.onChange}
|
||||||
min={latest.layer.maxzoom.minimum}
|
min={latest.layer.maxzoom.minimum}
|
||||||
max={latest.layer.maxzoom.maximum}
|
max={latest.layer.maxzoom.maximum}
|
||||||
default={latest.layer.maxzoom.maximum}
|
default={latest.layer.maxzoom.maximum}
|
||||||
data-wd-key="max-zoom.input"
|
data-wd-key="max-zoom.input"
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal);
|
const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal);
|
||||||
export default FieldMaxZoom;
|
export default FieldMaxZoom;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
|
|
||||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||||
import Block from "./Block";
|
import Block from './Block'
|
||||||
import InputNumber from "./InputNumber";
|
import InputNumber from './InputNumber'
|
||||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
type FieldMinZoomInternalProps = {
|
type FieldMinZoomInternalProps = {
|
||||||
value?: number
|
value?: number
|
||||||
@@ -11,25 +11,25 @@ type FieldMinZoomInternalProps = {
|
|||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
const FieldMinZoomInternal: React.FC<FieldMinZoomInternalProps> = (props) => {
|
class FieldMinZoomInternal extends React.Component<FieldMinZoomInternalProps> {
|
||||||
const t = props.t;
|
render() {
|
||||||
return (
|
const t = this.props.t;
|
||||||
<Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom}
|
return <Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom}
|
||||||
error={props.error}
|
error={this.props.error}
|
||||||
data-wd-key="min-zoom"
|
data-wd-key="min-zoom"
|
||||||
>
|
>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
allowRange={true}
|
allowRange={true}
|
||||||
value={props.value}
|
value={this.props.value}
|
||||||
onChange={props.onChange}
|
onChange={this.props.onChange}
|
||||||
min={latest.layer.minzoom.minimum}
|
min={latest.layer.minzoom.minimum}
|
||||||
max={latest.layer.minzoom.maximum}
|
max={latest.layer.minzoom.maximum}
|
||||||
default={latest.layer.minzoom.minimum}
|
default={latest.layer.minzoom.minimum}
|
||||||
data-wd-key='min-zoom.input'
|
data-wd-key='min-zoom.input'
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
|
const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
|
||||||
export default FieldMinZoom;
|
export default FieldMinZoom;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import InputMultiInput, {type InputMultiInputProps} from "./InputMultiInput";
|
import React from 'react'
|
||||||
import Fieldset from "./Fieldset";
|
import InputMultiInput, {InputMultiInputProps} from './InputMultiInput'
|
||||||
|
import Fieldset from './Fieldset'
|
||||||
|
|
||||||
|
|
||||||
type FieldMultiInputProps = InputMultiInputProps & {
|
type FieldMultiInputProps = InputMultiInputProps & {
|
||||||
@@ -7,12 +8,11 @@ type FieldMultiInputProps = InputMultiInputProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldMultiInput: React.FC<FieldMultiInputProps> = (props) => {
|
export default class FieldMultiInput extends React.Component<FieldMultiInputProps> {
|
||||||
return (
|
render() {
|
||||||
<Fieldset label={props.label}>
|
return <Fieldset label={this.props.label}>
|
||||||
<InputMultiInput {...props} />
|
<InputMultiInput {...this.props} />
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldMultiInput;
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import InputNumber, {type InputNumberProps} from "./InputNumber";
|
import React from 'react'
|
||||||
import Block from "./Block";
|
import InputNumber, {InputNumberProps} from './InputNumber'
|
||||||
|
import Block from './Block'
|
||||||
|
|
||||||
|
|
||||||
type FieldNumberProps = InputNumberProps & {
|
type FieldNumberProps = InputNumberProps & {
|
||||||
@@ -10,12 +11,10 @@ type FieldNumberProps = InputNumberProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldNumber: React.FC<FieldNumberProps> = (props) => {
|
export default class FieldNumber extends React.Component<FieldNumberProps> {
|
||||||
return (
|
render() {
|
||||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
<InputNumber {...props} />
|
<InputNumber {...this.props} />
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldNumber;
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Block from "./Block";
|
import React from 'react'
|
||||||
import InputSelect, {type InputSelectProps} from "./InputSelect";
|
import Block from './Block'
|
||||||
|
import InputSelect, {InputSelectProps} from './InputSelect'
|
||||||
|
|
||||||
|
|
||||||
type FieldSelectProps = InputSelectProps & {
|
type FieldSelectProps = InputSelectProps & {
|
||||||
@@ -10,12 +11,12 @@ type FieldSelectProps = InputSelectProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldSelect: React.FC<FieldSelectProps> = (props) => {
|
export default class FieldSelect extends React.Component<FieldSelectProps> {
|
||||||
return (
|
render() {
|
||||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
<InputSelect {...props} />
|
<InputSelect {...this.props}/>
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
export default FieldSelect;
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
|
|
||||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||||
import Block from "./Block";
|
import Block from './Block'
|
||||||
import InputAutocomplete from "./InputAutocomplete";
|
import InputAutocomplete from './InputAutocomplete'
|
||||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
type FieldSourceInternalProps = {
|
type FieldSourceInternalProps = {
|
||||||
value?: string
|
value?: string
|
||||||
@@ -13,30 +13,28 @@ type FieldSourceInternalProps = {
|
|||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
const FieldSourceInternal: React.FC<FieldSourceInternalProps> = ({
|
class FieldSourceInternal extends React.Component<FieldSourceInternalProps> {
|
||||||
onChange = () => {},
|
static defaultProps = {
|
||||||
sourceIds = [],
|
onChange: () => {},
|
||||||
wdKey,
|
sourceIds: [],
|
||||||
value,
|
}
|
||||||
error,
|
|
||||||
t
|
render() {
|
||||||
}) => {
|
const t = this.props.t;
|
||||||
return (
|
return <Block
|
||||||
<Block
|
|
||||||
label={t("Source")}
|
label={t("Source")}
|
||||||
fieldSpec={latest.layer.source}
|
fieldSpec={latest.layer.source}
|
||||||
error={error}
|
error={this.props.error}
|
||||||
data-wd-key={wdKey}
|
data-wd-key={this.props.wdKey}
|
||||||
>
|
>
|
||||||
<InputAutocomplete
|
<InputAutocomplete
|
||||||
value={value}
|
value={this.props.value}
|
||||||
onChange={onChange}
|
onChange={this.props.onChange}
|
||||||
options={sourceIds?.map((src) => [src, src])}
|
options={this.props.sourceIds?.map(src => [src, src])}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
const FieldSource = withTranslation()(FieldSourceInternal);
|
const FieldSource = withTranslation()(FieldSourceInternal);
|
||||||
export default FieldSource;
|
export default FieldSource;
|
||||||
|
|||||||
@@ -1,39 +1,42 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
|
|
||||||
import {latest} from "@maplibre/maplibre-gl-style-spec";
|
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||||
import Block from "./Block";
|
import Block from './Block'
|
||||||
import InputAutocomplete from "./InputAutocomplete";
|
import InputAutocomplete from './InputAutocomplete'
|
||||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
type FieldSourceLayerInternalProps = {
|
type FieldSourceLayerInternalProps = {
|
||||||
value?: string
|
value?: string
|
||||||
onChange?(...args: unknown[]): unknown
|
onChange?(...args: unknown[]): unknown
|
||||||
sourceLayerIds?: unknown[]
|
sourceLayerIds?: unknown[]
|
||||||
|
isFixed?: boolean
|
||||||
error?: {message: string}
|
error?: {message: string}
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
const FieldSourceLayerInternal: React.FC<FieldSourceLayerInternalProps> = ({
|
class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalProps> {
|
||||||
onChange = () => {},
|
static defaultProps = {
|
||||||
sourceLayerIds = [],
|
onChange: () => {},
|
||||||
value,
|
sourceLayerIds: [],
|
||||||
error,
|
isFixed: false
|
||||||
t
|
}
|
||||||
}) => {
|
|
||||||
return (
|
render() {
|
||||||
<Block
|
const t = this.props.t;
|
||||||
|
return <Block
|
||||||
label={t("Source Layer")}
|
label={t("Source Layer")}
|
||||||
fieldSpec={latest.layer["source-layer"]}
|
fieldSpec={latest.layer['source-layer']}
|
||||||
data-wd-key="layer-source-layer"
|
data-wd-key="layer-source-layer"
|
||||||
error={error}
|
error={this.props.error}
|
||||||
>
|
>
|
||||||
<InputAutocomplete
|
<InputAutocomplete
|
||||||
value={value}
|
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
||||||
onChange={onChange}
|
value={this.props.value}
|
||||||
options={sourceLayerIds?.map((l) => [l, l])}
|
onChange={this.props.onChange}
|
||||||
|
options={this.props.sourceLayerIds?.map(l => [l, l])}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
|
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
|
||||||
export default FieldSourceLayer;
|
export default FieldSourceLayer;
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
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,6 @@
|
|||||||
import Block from "./Block";
|
import React from 'react'
|
||||||
import InputString, {type InputStringProps} from "./InputString";
|
import Block from './Block'
|
||||||
|
import InputString, {InputStringProps} from './InputString'
|
||||||
|
|
||||||
type FieldStringProps = InputStringProps & {
|
type FieldStringProps = InputStringProps & {
|
||||||
name?: string
|
name?: string
|
||||||
@@ -9,12 +10,10 @@ type FieldStringProps = InputStringProps & {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const FieldString: React.FC<FieldStringProps> = (props) => {
|
export default class FieldString extends React.Component<FieldStringProps> {
|
||||||
return (
|
render() {
|
||||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
<InputString {...props} />
|
<InputString {...this.props} />
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default FieldString;
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import {v8} from "@maplibre/maplibre-gl-style-spec";
|
|
||||||
import Block from "./Block";
|
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
|
||||||
import InputSelect from "./InputSelect";
|
import Block from './Block'
|
||||||
import InputString from "./InputString";
|
import InputSelect from './InputSelect'
|
||||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
import InputString from './InputString'
|
||||||
import { startCase } from "lodash";
|
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
type FieldTypeInternalProps = {
|
type FieldTypeInternalProps = {
|
||||||
value: string
|
value: string
|
||||||
@@ -14,34 +14,44 @@ type FieldTypeInternalProps = {
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
const FieldTypeInternal: React.FC<FieldTypeInternalProps> = ({
|
class FieldTypeInternal extends React.Component<FieldTypeInternalProps> {
|
||||||
t,
|
static defaultProps = {
|
||||||
value,
|
disabled: false,
|
||||||
wdKey,
|
}
|
||||||
onChange,
|
|
||||||
error,
|
render() {
|
||||||
disabled = false
|
const t = this.props.t;
|
||||||
}) => {
|
return <Block label={t("Type")} fieldSpec={latest.layer.type}
|
||||||
const layerstypes: [string, string][] = Object.keys(v8.layer.type.values || {}).map(v => [v, startCase(v.replace(/-/g, " "))]);
|
data-wd-key={this.props.wdKey}
|
||||||
return (
|
error={this.props.error}
|
||||||
<Block label={t("Type")} fieldSpec={v8.layer.type}
|
|
||||||
data-wd-key={wdKey}
|
|
||||||
error={error}
|
|
||||||
>
|
>
|
||||||
{disabled && (
|
{this.props.disabled &&
|
||||||
<InputString value={value} disabled={true} />
|
<InputString
|
||||||
)}
|
value={this.props.value}
|
||||||
{!disabled && (
|
disabled={true}
|
||||||
<InputSelect
|
|
||||||
options={layerstypes}
|
|
||||||
onChange={onChange}
|
|
||||||
value={value}
|
|
||||||
data-wd-key={wdKey + ".select"}
|
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
|
{!this.props.disabled &&
|
||||||
|
<InputSelect
|
||||||
|
options={[
|
||||||
|
['background', 'Background'],
|
||||||
|
['fill', 'Fill'],
|
||||||
|
['line', 'Line'],
|
||||||
|
['symbol', 'Symbol'],
|
||||||
|
['raster', 'Raster'],
|
||||||
|
['circle', 'Circle'],
|
||||||
|
['fill-extrusion', 'Fill Extrusion'],
|
||||||
|
['hillshade', 'Hillshade'],
|
||||||
|
['heatmap', 'Heatmap'],
|
||||||
|
]}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
value={this.props.value}
|
||||||
|
data-wd-key={this.props.wdKey + ".select"}
|
||||||
|
/>
|
||||||
|
}
|
||||||
</Block>
|
</Block>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const FieldType = withTranslation()(FieldTypeInternal);
|
const FieldType = withTranslation()(FieldTypeInternal);
|
||||||
export default FieldType;
|
export default FieldType;
|
||||||
|
|||||||
+12
-10
@@ -1,5 +1,6 @@
|
|||||||
import InputUrl, {type FieldUrlProps as InputUrlProps} from "./InputUrl";
|
import React from 'react'
|
||||||
import Block from "./Block";
|
import InputUrl, {FieldUrlProps as InputUrlProps} from './InputUrl'
|
||||||
|
import Block from './Block'
|
||||||
|
|
||||||
|
|
||||||
type FieldUrlProps = InputUrlProps & {
|
type FieldUrlProps = InputUrlProps & {
|
||||||
@@ -10,12 +11,13 @@ type FieldUrlProps = InputUrlProps & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const FieldUrl: React.FC<FieldUrlProps> = (props) => {
|
export default class FieldUrl extends React.Component<FieldUrlProps> {
|
||||||
return (
|
render () {
|
||||||
<Block label={props.label} fieldSpec={props.fieldSpec}>
|
return (
|
||||||
<InputUrl {...props} />
|
<Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||||
</Block>
|
<InputUrl {...this.props} />
|
||||||
);
|
</Block>
|
||||||
};
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default FieldUrl;
|
|
||||||
|
|||||||
+49
-37
@@ -1,53 +1,65 @@
|
|||||||
import React, { type PropsWithChildren, type ReactElement } from "react";
|
import React, { PropsWithChildren, ReactElement } from 'react'
|
||||||
import classnames from "classnames";
|
import FieldDocLabel from './FieldDocLabel'
|
||||||
import FieldDocLabel from "./FieldDocLabel";
|
import Doc from './Doc'
|
||||||
import Doc from "./Doc";
|
import generateUniqueId from '../libs/document-uid';
|
||||||
import generateUniqueId from "../libs/document-uid";
|
|
||||||
|
|
||||||
export type FieldsetProps = PropsWithChildren & {
|
type FieldsetProps = PropsWithChildren & {
|
||||||
label?: string,
|
label?: string,
|
||||||
fieldSpec?: { doc?: string },
|
fieldSpec?: { doc?: string },
|
||||||
action?: ReactElement,
|
action?: ReactElement,
|
||||||
error?: {message: string}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FieldsetState = {
|
||||||
|
showDoc: boolean
|
||||||
|
};
|
||||||
|
|
||||||
const Fieldset: React.FC<FieldsetProps> = (props) => {
|
export default class Fieldset extends React.Component<FieldsetProps, FieldsetState> {
|
||||||
const [showDoc, setShowDoc] = React.useState(false);
|
_labelId: string;
|
||||||
const labelId = React.useRef(generateUniqueId("fieldset_label_"));
|
|
||||||
|
|
||||||
const onToggleDoc = (val: boolean) => {
|
constructor (props: FieldsetProps) {
|
||||||
setShowDoc(val);
|
super(props);
|
||||||
};
|
this._labelId = generateUniqueId(`fieldset_label_`);
|
||||||
|
this.state = {
|
||||||
|
showDoc: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
onToggleDoc = (val: boolean) => {
|
||||||
<div className="maputnik-input-block" role="group" aria-labelledby={labelId.current}>
|
this.setState({
|
||||||
{props.fieldSpec && (
|
showDoc: val
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return <div className="maputnik-input-block" role="group" aria-labelledby={this._labelId}>
|
||||||
|
{this.props.fieldSpec &&
|
||||||
<div className="maputnik-input-block-label">
|
<div className="maputnik-input-block-label">
|
||||||
<FieldDocLabel
|
<FieldDocLabel
|
||||||
label={props.label}
|
label={this.props.label}
|
||||||
onToggleDoc={onToggleDoc}
|
onToggleDoc={this.onToggleDoc}
|
||||||
fieldSpec={props.fieldSpec}
|
fieldSpec={this.props.fieldSpec}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
}
|
||||||
{!props.fieldSpec && (
|
{!this.props.fieldSpec &&
|
||||||
<div className={classnames({
|
<div className="maputnik-input-block-label">
|
||||||
"maputnik-input-block-label": true,
|
{this.props.label}
|
||||||
"maputnik-input-block--error": props.error
|
|
||||||
})}>
|
|
||||||
{props.label}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
}
|
||||||
<div className="maputnik-input-block-action">{props.action}</div>
|
<div className="maputnik-input-block-action">
|
||||||
<div className="maputnik-input-block-content">{props.children}</div>
|
{this.props.action}
|
||||||
{props.fieldSpec && (
|
</div>
|
||||||
<div className="maputnik-doc-inline" style={{ display: showDoc ? "" : "none" }}>
|
<div className="maputnik-input-block-content">
|
||||||
<Doc fieldSpec={props.fieldSpec} />
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
{this.props.fieldSpec &&
|
||||||
|
<div
|
||||||
|
className="maputnik-doc-inline"
|
||||||
|
style={{display: this.state.showDoc ? '' : 'none'}}
|
||||||
|
>
|
||||||
|
<Doc fieldSpec={this.props.fieldSpec} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Fieldset;
|
|
||||||
|
|||||||
@@ -1,25 +1,23 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import { TbMathFunction } from "react-icons/tb";
|
import {mdiTableRowPlusAfter} from '@mdi/js';
|
||||||
import { PiListPlusBold } from "react-icons/pi";
|
import {isEqual} from 'lodash';
|
||||||
import {isEqual} from "lodash";
|
import {ExpressionSpecification, LegacyFilterSpecification, StyleSpecification} from 'maplibre-gl'
|
||||||
import {type ExpressionSpecification, type LegacyFilterSpecification} from "maplibre-gl";
|
import {latest, migrate, convertFilter} from '@maplibre/maplibre-gl-style-spec'
|
||||||
import {migrate, convertFilter} from "@maplibre/maplibre-gl-style-spec";
|
import {mdiFunctionVariant} from '@mdi/js';
|
||||||
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
|
||||||
|
|
||||||
import {combiningFilterOps} from "../libs/filterops";
|
import {combiningFilterOps} from '../libs/filterops'
|
||||||
import InputSelect from "./InputSelect";
|
import InputSelect from './InputSelect'
|
||||||
import Block from "./Block";
|
import Block from './Block'
|
||||||
import SingleFilterEditor from "./SingleFilterEditor";
|
import SingleFilterEditor from './SingleFilterEditor'
|
||||||
import FilterEditorBlock from "./FilterEditorBlock";
|
import FilterEditorBlock from './FilterEditorBlock'
|
||||||
import InputButton from "./InputButton";
|
import InputButton from './InputButton'
|
||||||
import Doc from "./Doc";
|
import Doc from './Doc'
|
||||||
import ExpressionProperty from "./_ExpressionProperty";
|
import ExpressionProperty from './_ExpressionProperty';
|
||||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||||
import type { MappedLayerErrors, StyleSpecificationWithId } from "../libs/definitions";
|
|
||||||
|
|
||||||
|
|
||||||
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification {
|
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification {
|
||||||
const filter = props.filter || ["all"];
|
const filter = props.filter || ['all'];
|
||||||
|
|
||||||
if (!Array.isArray(filter)) {
|
if (!Array.isArray(filter)) {
|
||||||
return filter;
|
return filter;
|
||||||
@@ -29,7 +27,7 @@ function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecific
|
|||||||
let filters = filter.slice(1);
|
let filters = filter.slice(1);
|
||||||
|
|
||||||
if(combiningFilterOps.indexOf(combiningOp) < 0) {
|
if(combiningFilterOps.indexOf(combiningOp) < 0) {
|
||||||
combiningOp = "all";
|
combiningOp = 'all';
|
||||||
filters = [filter.slice(0)];
|
filters = [filter.slice(0)];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +39,7 @@ function migrateFilter(filter: LegacyFilterSpecification | ExpressionSpecificati
|
|||||||
return (migrate(createStyleFromFilter(filter) as any).layers[0] as any).filter;
|
return (migrate(createStyleFromFilter(filter) as any).layers[0] as any).filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpecification): StyleSpecificationWithId {
|
function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpecification): StyleSpecification & {id: string} {
|
||||||
return {
|
return {
|
||||||
"id": "tmp",
|
"id": "tmp",
|
||||||
"version": 8,
|
"version": 8,
|
||||||
@@ -50,7 +48,7 @@ function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpe
|
|||||||
"sources": {
|
"sources": {
|
||||||
"tmp": {
|
"tmp": {
|
||||||
"type": "geojson",
|
"type": "geojson",
|
||||||
"data": ""
|
"data": ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sprite": "",
|
"sprite": "",
|
||||||
@@ -82,22 +80,22 @@ function checkIfSimpleFilter (filter: LegacyFilterSpecification | ExpressionSpec
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hasCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
|
function hasCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
|
||||||
return combiningFilterOps.indexOf(filter[0]) >= 0;
|
return combiningFilterOps.indexOf(filter[0]) >= 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasNestedCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
|
function hasNestedCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
|
||||||
if(hasCombiningFilter(filter)) {
|
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 = {
|
type FilterEditorInternalProps = {
|
||||||
/** Properties of the vector layer and the available fields */
|
/** Properties of the vector layer and the available fields */
|
||||||
properties?: {[key:string]: any}
|
properties?: {[key:string]: any}
|
||||||
filter?: any[]
|
filter?: any[]
|
||||||
errors?: MappedLayerErrors
|
errors?: {[key:string]: any}
|
||||||
onChange(value: LegacyFilterSpecification | ExpressionSpecification): void
|
onChange(value: LegacyFilterSpecification | ExpressionSpecification): unknown
|
||||||
} & WithTranslation;
|
} & WithTranslation;
|
||||||
|
|
||||||
type FilterEditorState = {
|
type FilterEditorState = {
|
||||||
@@ -109,7 +107,7 @@ type FilterEditorState = {
|
|||||||
class FilterEditorInternal extends React.Component<FilterEditorInternalProps, FilterEditorState> {
|
class FilterEditorInternal extends React.Component<FilterEditorInternalProps, FilterEditorState> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
filter: ["all"],
|
filter: ["all"],
|
||||||
};
|
}
|
||||||
|
|
||||||
constructor (props: FilterEditorInternalProps) {
|
constructor (props: FilterEditorInternalProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -121,42 +119,42 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
|
|
||||||
// Convert filter to combining filter
|
// Convert filter to combining filter
|
||||||
onFilterPartChanged(filterIdx: number, newPart: any[]) {
|
onFilterPartChanged(filterIdx: number, newPart: any[]) {
|
||||||
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
|
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
|
||||||
newFilter[filterIdx] = newPart;
|
newFilter[filterIdx] = newPart
|
||||||
this.props.onChange(newFilter);
|
this.props.onChange(newFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteFilterItem(filterIdx: number) {
|
deleteFilterItem(filterIdx: number) {
|
||||||
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
|
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
|
||||||
newFilter.splice(filterIdx + 1, 1);
|
newFilter.splice(filterIdx + 1, 1)
|
||||||
this.props.onChange(newFilter);
|
this.props.onChange(newFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
addFilterItem = () => {
|
addFilterItem = () => {
|
||||||
const newFilterItem = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
|
const newFilterItem = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
|
||||||
(newFilterItem as any[]).push(["==", "name", ""]);
|
(newFilterItem as any[]).push(['==', 'name', ''])
|
||||||
this.props.onChange(newFilterItem);
|
this.props.onChange(newFilterItem)
|
||||||
};
|
}
|
||||||
|
|
||||||
onToggleDoc = (val: boolean) => {
|
onToggleDoc = (val: boolean) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
showDoc: val
|
showDoc: val
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
makeFilter = () => {
|
makeFilter = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
displaySimpleFilter: true,
|
displaySimpleFilter: true,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
makeExpression = () => {
|
makeExpression = () => {
|
||||||
const filter = combiningFilter(this.props);
|
const filter = combiningFilter(this.props);
|
||||||
this.props.onChange(migrateFilter(filter));
|
this.props.onChange(migrateFilter(filter));
|
||||||
this.setState({
|
this.setState({
|
||||||
displaySimpleFilter: false,
|
displaySimpleFilter: false,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props: Readonly<FilterEditorInternalProps>, state: FilterEditorState) {
|
static getDerivedStateFromProps(props: Readonly<FilterEditorInternalProps>, state: FilterEditorState) {
|
||||||
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
|
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
|
||||||
@@ -171,7 +169,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
else if (displaySimpleFilter && state.displaySimpleFilter === false) {
|
else if (displaySimpleFilter && state.displaySimpleFilter === false) {
|
||||||
return {
|
return {
|
||||||
valueIsSimpleFilter: true,
|
valueIsSimpleFilter: true,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return {
|
return {
|
||||||
@@ -199,10 +197,12 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
onClick={this.makeExpression}
|
onClick={this.makeExpression}
|
||||||
title={t("Convert to expression")}
|
title={t("Convert to expression")}
|
||||||
>
|
>
|
||||||
<TbMathFunction />
|
<svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||||
|
</svg>
|
||||||
{t("Upgrade to expression")}
|
{t("Upgrade to expression")}
|
||||||
</InputButton>
|
</InputButton>
|
||||||
</div>;
|
</div>
|
||||||
}
|
}
|
||||||
else if (displaySimpleFilter) {
|
else if (displaySimpleFilter) {
|
||||||
const filter = combiningFilter(this.props);
|
const filter = combiningFilter(this.props);
|
||||||
@@ -216,7 +216,9 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
title={t("Convert to expression")}
|
title={t("Convert to expression")}
|
||||||
className="maputnik-make-zoom-function"
|
className="maputnik-make-zoom-function"
|
||||||
>
|
>
|
||||||
<TbMathFunction />
|
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||||
|
</svg>
|
||||||
</InputButton>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -238,7 +240,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
})
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -269,14 +271,15 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
className="maputnik-add-filter"
|
className="maputnik-add-filter"
|
||||||
onClick={this.addFilterItem}
|
onClick={this.addFilterItem}
|
||||||
>
|
>
|
||||||
<PiListPlusBold style={{ verticalAlign: "text-bottom" }} />
|
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||||
{t("Add filter")}
|
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
||||||
|
</svg> {t("Add filter")}
|
||||||
</InputButton>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
key="doc"
|
key="doc"
|
||||||
className="maputnik-doc-inline"
|
className="maputnik-doc-inline"
|
||||||
style={{display: this.state.showDoc ? "" : "none"}}
|
style={{display: this.state.showDoc ? '' : 'none'}}
|
||||||
>
|
>
|
||||||
<Doc fieldSpec={fieldSpec} />
|
<Doc fieldSpec={fieldSpec} />
|
||||||
</div>
|
</div>
|
||||||
@@ -294,6 +297,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
this.props.onChange(defaultFilter);
|
this.props.onChange(defaultFilter);
|
||||||
}}
|
}}
|
||||||
fieldName="filter"
|
fieldName="filter"
|
||||||
|
fieldSpec={fieldSpec}
|
||||||
value={filter}
|
value={filter}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
@@ -301,7 +305,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
|
|||||||
{this.state.valueIsSimpleFilter &&
|
{this.state.valueIsSimpleFilter &&
|
||||||
<div className="maputnik-expr-infobox">
|
<div className="maputnik-expr-infobox">
|
||||||
{t("You've entered an old style filter.")}
|
{t("You've entered an old style filter.")}
|
||||||
{" "}
|
{' '}
|
||||||
<button
|
<button
|
||||||
onClick={this.makeFilter}
|
onClick={this.makeFilter}
|
||||||
className="maputnik-expr-infobox__button"
|
className="maputnik-expr-infobox__button"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { type PropsWithChildren } from "react";
|
import React, { PropsWithChildren } from 'react'
|
||||||
import InputButton from "./InputButton";
|
import InputButton from './InputButton'
|
||||||
import {MdDelete} from "react-icons/md";
|
import {MdDelete} from 'react-icons/md'
|
||||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
type FilterEditorBlockInternalProps = PropsWithChildren & {
|
type FilterEditorBlockInternalProps = PropsWithChildren & {
|
||||||
onDelete(...args: unknown[]): unknown
|
onDelete(...args: unknown[]): unknown
|
||||||
@@ -11,19 +11,19 @@ class FilterEditorBlockInternal extends React.Component<FilterEditorBlockInterna
|
|||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
return <div className="maputnik-filter-editor-block">
|
return <div className="maputnik-filter-editor-block">
|
||||||
<div className="maputnik-filter-editor-block-content">
|
|
||||||
{this.props.children}
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-filter-editor-block-action">
|
<div className="maputnik-filter-editor-block-action">
|
||||||
<InputButton
|
<InputButton
|
||||||
className="maputnik-icon-button"
|
className="maputnik-delete-filter"
|
||||||
onClick={this.props.onDelete}
|
onClick={this.props.onDelete}
|
||||||
title={t("Delete filter block")}
|
title={t("Delete filter block")}
|
||||||
>
|
>
|
||||||
<MdDelete />
|
<MdDelete />
|
||||||
</InputButton>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
<div className="maputnik-filter-editor-block-content">
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
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,33 +1,33 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
|
|
||||||
import type {CSSProperties} from "react";
|
import IconLine from './IconLine'
|
||||||
import { BsBack, BsDiamondFill, BsSunFill } from "react-icons/bs";
|
import IconFill from './IconFill'
|
||||||
import { MdBubbleChart, MdCircle, MdLocationPin, MdPhoto, MdPriorityHigh } from "react-icons/md";
|
import IconSymbol from './IconSymbol'
|
||||||
import { IoAnalyticsOutline } from "react-icons/io5";
|
import IconBackground from './IconBackground'
|
||||||
import { IoMdCube } from "react-icons/io";
|
import IconCircle from './IconCircle'
|
||||||
import { FaMountain } from "react-icons/fa";
|
import IconMissing from './IconMissing'
|
||||||
|
|
||||||
type IconLayerProps = {
|
type IconLayerProps = {
|
||||||
type: string
|
type: string
|
||||||
style?: CSSProperties
|
style?: object
|
||||||
className?: string
|
className?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
const IconLayer: React.FC<IconLayerProps> = (props) => {
|
export default class IconLayer extends React.Component<IconLayerProps> {
|
||||||
const iconProps = { style: props.style };
|
render() {
|
||||||
switch(props.type) {
|
const iconProps = { style: this.props.style }
|
||||||
case "fill-extrusion": return <IoMdCube {...iconProps} />;
|
switch(this.props.type) {
|
||||||
case "raster": return <MdPhoto {...iconProps} />;
|
case 'fill-extrusion': return <IconBackground {...iconProps} />
|
||||||
case "hillshade": return <BsSunFill {...iconProps} />;
|
case 'raster': return <IconFill {...iconProps} />
|
||||||
case "color-relief": return <FaMountain {...iconProps} />;
|
case 'hillshade': return <IconFill {...iconProps} />
|
||||||
case "heatmap": return <MdBubbleChart {...iconProps} />;
|
case 'heatmap': return <IconFill {...iconProps} />
|
||||||
case "fill": return <BsDiamondFill {...iconProps} />;
|
case 'fill': return <IconFill {...iconProps} />
|
||||||
case "background": return <BsBack {...iconProps} />;
|
case 'background': return <IconBackground {...iconProps} />
|
||||||
case "line": return <IoAnalyticsOutline {...iconProps} />;
|
case 'line': return <IconLine {...iconProps} />
|
||||||
case "symbol": return <MdLocationPin {...iconProps} />;
|
case 'symbol': return <IconSymbol {...iconProps} />
|
||||||
case "circle": return <MdCircle {...iconProps} />;
|
case 'circle': return <IconCircle {...iconProps} />
|
||||||
default: return <MdPriorityHigh {...iconProps} />;
|
default: return <IconMissing {...iconProps} />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export default IconLayer;
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {MdPriorityHigh} from 'react-icons/md'
|
||||||
|
|
||||||
|
|
||||||
|
export default class IconMissing extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<MdPriorityHigh {...this.props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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 React from 'react'
|
||||||
import InputString from "./InputString";
|
import InputString from './InputString'
|
||||||
import InputNumber from "./InputNumber";
|
import InputNumber from './InputNumber'
|
||||||
|
|
||||||
export type InputArrayProps = {
|
export type FieldArrayProps = {
|
||||||
value: (string | number | undefined)[]
|
value: (string | number | undefined)[]
|
||||||
type?: string
|
type?: string
|
||||||
length?: number
|
length?: number
|
||||||
default?: (string | number | undefined)[]
|
default?: (string | number | undefined)[]
|
||||||
onChange?(value: (string | number | undefined)[] | undefined): unknown
|
onChange?(value: (string | number | undefined)[] | undefined): unknown
|
||||||
"aria-label"?: string
|
'aria-label'?: string
|
||||||
label?: string
|
label?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
type InputArrayState = {
|
type FieldArrayState = {
|
||||||
value: (string | number | undefined)[]
|
value: (string | number | undefined)[]
|
||||||
initialPropsValue: unknown[]
|
initialPropsValue: unknown[]
|
||||||
};
|
}
|
||||||
|
|
||||||
export default class InputArray extends React.Component<InputArrayProps, InputArrayState> {
|
export default class FieldArray extends React.Component<FieldArrayProps, FieldArrayState> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
value: [],
|
value: [],
|
||||||
default: [],
|
default: [],
|
||||||
};
|
}
|
||||||
|
|
||||||
constructor (props: InputArrayProps) {
|
constructor (props: FieldArrayProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
value: this.props.value.slice(0),
|
value: this.props.value.slice(0),
|
||||||
@@ -32,7 +32,7 @@ export default class InputArray extends React.Component<InputArrayProps, InputAr
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props: Readonly<InputArrayProps>, state: InputArrayState) {
|
static getDerivedStateFromProps(props: Readonly<FieldArrayProps>, state: FieldArrayState) {
|
||||||
const value: any[] = [];
|
const value: any[] = [];
|
||||||
const initialPropsValue = state.initialPropsValue.slice(0);
|
const initialPropsValue = state.initialPropsValue.slice(0);
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ export default class InputArray extends React.Component<InputArrayProps, InputAr
|
|||||||
value[i] = state.value[i];
|
value[i] = state.value[i];
|
||||||
initialPropsValue[i] = state.value[i];
|
initialPropsValue[i] = state.value[i];
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value,
|
value,
|
||||||
@@ -54,7 +54,7 @@ export default class InputArray extends React.Component<InputArrayProps, InputAr
|
|||||||
|
|
||||||
isComplete(value: unknown[]) {
|
isComplete(value: unknown[]) {
|
||||||
return Array(this.props.length).fill(null).every((_, i) => {
|
return Array(this.props.length).fill(null).every((_, i) => {
|
||||||
const val = value[i];
|
const val = value[i]
|
||||||
return !(val === undefined || val === "");
|
return !(val === undefined || val === "");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -82,20 +82,20 @@ export default class InputArray extends React.Component<InputArrayProps, InputAr
|
|||||||
const containsValues = (
|
const containsValues = (
|
||||||
value.length > 0 &&
|
value.length > 0 &&
|
||||||
!value.every(val => {
|
!value.every(val => {
|
||||||
return (val === "" || val === undefined);
|
return (val === "" || val === undefined)
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const inputs = Array(this.props.length).fill(null).map((_, i) => {
|
const inputs = Array(this.props.length).fill(null).map((_, i) => {
|
||||||
if(this.props.type === "number") {
|
if(this.props.type === 'number') {
|
||||||
return <InputNumber
|
return <InputNumber
|
||||||
key={i}
|
key={i}
|
||||||
default={containsValues || !this.props.default ? undefined : this.props.default[i] as number}
|
default={containsValues || !this.props.default ? undefined : this.props.default[i] as number}
|
||||||
value={value[i] as number}
|
value={value[i] as number}
|
||||||
required={containsValues ? true : false}
|
required={containsValues ? true : false}
|
||||||
onChange={(v) => this.changeValue(i, v)}
|
onChange={(v) => this.changeValue(i, v)}
|
||||||
aria-label={this.props["aria-label"] || this.props.label}
|
aria-label={this.props['aria-label'] || this.props.label}
|
||||||
/>;
|
/>
|
||||||
} else {
|
} else {
|
||||||
return <InputString
|
return <InputString
|
||||||
key={i}
|
key={i}
|
||||||
@@ -103,15 +103,16 @@ export default class InputArray extends React.Component<InputArrayProps, InputAr
|
|||||||
value={value[i] as string}
|
value={value[i] as string}
|
||||||
required={containsValues ? true : false}
|
required={containsValues ? true : false}
|
||||||
onChange={this.changeValue.bind(this, i)}
|
onChange={this.changeValue.bind(this, i)}
|
||||||
aria-label={this.props["aria-label"] || this.props.label}
|
aria-label={this.props['aria-label'] || this.props.label}
|
||||||
/>;
|
/>
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="maputnik-array">
|
<div className="maputnik-array">
|
||||||
{inputs}
|
{inputs}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import InputAutocomplete from "./InputAutocomplete";
|
|
||||||
import { mount } from "cypress/react";
|
|
||||||
|
|
||||||
const fruits = ["apple", "banana", "cherry"];
|
|
||||||
|
|
||||||
describe("<InputAutocomplete />", () => {
|
|
||||||
it("filters options when typing", () => {
|
|
||||||
mount(
|
|
||||||
<InputAutocomplete aria-label="Fruit" options={fruits.map(f => [f, f])} />
|
|
||||||
);
|
|
||||||
cy.get("input").focus();
|
|
||||||
cy.get(".maputnik-autocomplete-menu-item").should("have.length", 3);
|
|
||||||
cy.get("input").type("ch");
|
|
||||||
cy.get(".maputnik-autocomplete-menu-item").should("have.length", 1).and("contain", "cherry");
|
|
||||||
cy.get(".maputnik-autocomplete-menu-item").click();
|
|
||||||
cy.get("input").should("have.value", "cherry");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,116 +1,102 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import classnames from "classnames";
|
import classnames from 'classnames'
|
||||||
import {useCombobox} from "downshift";
|
import Autocomplete from 'react-autocomplete'
|
||||||
|
|
||||||
|
|
||||||
const MAX_HEIGHT = 140;
|
const MAX_HEIGHT = 140;
|
||||||
|
|
||||||
export type InputAutocompleteProps = {
|
export type InputAutocompleteProps = {
|
||||||
value?: string
|
value?: string
|
||||||
options?: any[]
|
options: any[]
|
||||||
onChange?(value: string | undefined): unknown
|
onChange(value: string | undefined): unknown
|
||||||
"aria-label"?: string
|
keepMenuWithinWindowBounds?: boolean
|
||||||
|
'aria-label'?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function InputAutocomplete({
|
export default class InputAutocomplete extends React.Component<InputAutocompleteProps> {
|
||||||
value,
|
state = {
|
||||||
options = [],
|
maxHeight: MAX_HEIGHT
|
||||||
onChange = () => {},
|
}
|
||||||
"aria-label": ariaLabel,
|
|
||||||
}: InputAutocompleteProps) {
|
|
||||||
const [input, setInput] = React.useState(value || "");
|
|
||||||
const menuRef = React.useRef<HTMLDivElement>(null);
|
|
||||||
const [maxHeight, setMaxHeight] = React.useState(MAX_HEIGHT);
|
|
||||||
|
|
||||||
const filteredItems = React.useMemo(() => {
|
autocompleteMenuEl: HTMLDivElement | null = null;
|
||||||
const lv = input.toLowerCase();
|
|
||||||
return options.filter((item) => item[0].toLowerCase().includes(lv));
|
|
||||||
}, [options, input]);
|
|
||||||
|
|
||||||
const calcMaxHeight = React.useCallback(() => {
|
static defaultProps = {
|
||||||
if (menuRef.current) {
|
onChange: () => {},
|
||||||
const space = window.innerHeight - menuRef.current.getBoundingClientRect().top;
|
options: [],
|
||||||
setMaxHeight(Math.min(space, MAX_HEIGHT));
|
}
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const {
|
calcMaxHeight() {
|
||||||
isOpen,
|
if(this.props.keepMenuWithinWindowBounds) {
|
||||||
getMenuProps,
|
const maxHeight = window.innerHeight - this.autocompleteMenuEl!.getBoundingClientRect().top;
|
||||||
getInputProps,
|
const limitedMaxHeight = Math.min(maxHeight, MAX_HEIGHT);
|
||||||
getItemProps,
|
|
||||||
highlightedIndex,
|
if(limitedMaxHeight != this.state.maxHeight) {
|
||||||
openMenu,
|
this.setState({
|
||||||
} = useCombobox({
|
maxHeight: limitedMaxHeight
|
||||||
items: filteredItems,
|
})
|
||||||
inputValue: input,
|
|
||||||
itemToString: (item) => (item ? item[0] : ""),
|
|
||||||
stateReducer: (_state, action) => {
|
|
||||||
if (action.type === useCombobox.stateChangeTypes.InputClick) {
|
|
||||||
return {...action.changes, isOpen: true};
|
|
||||||
}
|
}
|
||||||
return action.changes;
|
|
||||||
},
|
|
||||||
onSelectedItemChange: ({selectedItem}) => {
|
|
||||||
const v = selectedItem ? selectedItem[0] : "";
|
|
||||||
setInput(v);
|
|
||||||
onChange(selectedItem ? selectedItem[0] : undefined);
|
|
||||||
},
|
|
||||||
onInputValueChange: ({inputValue: v}) => {
|
|
||||||
if (typeof v === "string") {
|
|
||||||
setInput(v);
|
|
||||||
onChange(v === "" ? undefined : v);
|
|
||||||
openMenu();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
calcMaxHeight();
|
|
||||||
}
|
}
|
||||||
}, [isOpen, calcMaxHeight]);
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
componentDidMount() {
|
||||||
window.addEventListener("resize", calcMaxHeight);
|
this.calcMaxHeight();
|
||||||
return () => window.removeEventListener("resize", calcMaxHeight);
|
}
|
||||||
}, [calcMaxHeight]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
componentDidUpdate() {
|
||||||
setInput(value || "");
|
this.calcMaxHeight();
|
||||||
}, [value]);
|
}
|
||||||
|
|
||||||
return (
|
onChange(v: string) {
|
||||||
<div className="maputnik-autocomplete">
|
this.props.onChange(v === "" ? undefined : v);
|
||||||
<input
|
}
|
||||||
{...getInputProps({
|
|
||||||
"aria-label": ariaLabel,
|
render() {
|
||||||
|
return <div
|
||||||
|
ref={(el) => {
|
||||||
|
this.autocompleteMenuEl = el;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Autocomplete
|
||||||
|
menuStyle={{
|
||||||
|
position: "fixed",
|
||||||
|
overflow: "auto",
|
||||||
|
maxHeight: this.state.maxHeight,
|
||||||
|
zIndex: '998'
|
||||||
|
}}
|
||||||
|
wrapperProps={{
|
||||||
|
className: "maputnik-autocomplete",
|
||||||
|
style: {}
|
||||||
|
}}
|
||||||
|
inputProps={{
|
||||||
|
'aria-label': this.props['aria-label'],
|
||||||
className: "maputnik-string",
|
className: "maputnik-string",
|
||||||
spellCheck: false,
|
spellCheck: false
|
||||||
onFocus: () => openMenu(),
|
}}
|
||||||
})}
|
value={this.props.value}
|
||||||
|
items={this.props.options}
|
||||||
|
getItemValue={(item) => item[0]}
|
||||||
|
onSelect={v => this.onChange(v)}
|
||||||
|
onChange={(_e, v) => this.onChange(v)}
|
||||||
|
shouldItemRender={(item, value="") => {
|
||||||
|
if (typeof(value) === "string") {
|
||||||
|
return item[0].toLowerCase().indexOf(value.toLowerCase()) > -1
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}}
|
||||||
|
renderItem={(item, isHighlighted) => (
|
||||||
|
<div
|
||||||
|
key={item[0]}
|
||||||
|
className={classnames({
|
||||||
|
"maputnik-autocomplete-menu-item": true,
|
||||||
|
"maputnik-autocomplete-menu-item-selected": isHighlighted,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{item[1]}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<div
|
|
||||||
{...getMenuProps({}, {suppressRefError: true})}
|
|
||||||
ref={menuRef}
|
|
||||||
style={{position: "fixed", overflow: "auto", maxHeight, zIndex: 998}}
|
|
||||||
className="maputnik-autocomplete-menu"
|
|
||||||
>
|
|
||||||
{isOpen &&
|
|
||||||
filteredItems.map((item, index) => (
|
|
||||||
<div
|
|
||||||
key={item[0]}
|
|
||||||
{...getItemProps({
|
|
||||||
item,
|
|
||||||
index,
|
|
||||||
className: classnames("maputnik-autocomplete-menu-item", {
|
|
||||||
"maputnik-autocomplete-menu-item-selected": highlightedIndex === index,
|
|
||||||
}),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{item[1]}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import classnames from "classnames";
|
import classnames from 'classnames'
|
||||||
|
|
||||||
type InputButtonProps = {
|
type InputButtonProps = {
|
||||||
"data-wd-key"?: string
|
"data-wd-key"?: string
|
||||||
@@ -28,6 +28,6 @@ export default class InputButton extends React.Component<InputButtonProps> {
|
|||||||
style={this.props.style}
|
style={this.props.style}
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</button>;
|
</button>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
|
|
||||||
export type InputCheckboxProps = {
|
export type InputCheckboxProps = {
|
||||||
value?: boolean
|
value?: boolean
|
||||||
@@ -9,11 +9,11 @@ export type InputCheckboxProps = {
|
|||||||
export default class InputCheckbox extends React.Component<InputCheckboxProps> {
|
export default class InputCheckbox extends React.Component<InputCheckboxProps> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
value: false,
|
value: false,
|
||||||
};
|
}
|
||||||
|
|
||||||
onChange = () => {
|
onChange = () => {
|
||||||
this.props.onChange(!this.props.value);
|
this.props.onChange(!this.props.value);
|
||||||
};
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div className="maputnik-checkbox-wrapper">
|
return <div className="maputnik-checkbox-wrapper">
|
||||||
@@ -27,11 +27,11 @@ export default class InputCheckbox extends React.Component<InputCheckboxProps> {
|
|||||||
/>
|
/>
|
||||||
<div className="maputnik-checkbox-box">
|
<div className="maputnik-checkbox-box">
|
||||||
<svg style={{
|
<svg style={{
|
||||||
display: this.props.value ? "inline" : "none"
|
display: this.props.value ? 'inline' : 'none'
|
||||||
}} className="maputnik-checkbox-icon" viewBox='0 0 32 32'>
|
}} className="maputnik-checkbox-icon" viewBox='0 0 32 32'>
|
||||||
<path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' />
|
<path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import Color from "color";
|
import Color from 'color'
|
||||||
import ChromePicker from "react-color/lib/components/chrome/Chrome";
|
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
|
||||||
import {type ColorResult} from "react-color";
|
import {ColorResult} from 'react-color';
|
||||||
import lodash from "lodash";
|
import lodash from 'lodash';
|
||||||
|
|
||||||
function formatColor(color: ColorResult): string {
|
function formatColor(color: ColorResult): string {
|
||||||
const rgb = color.rgb;
|
const rgb = color.rgb
|
||||||
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`;
|
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InputColorProps = {
|
export type InputColorProps = {
|
||||||
@@ -16,14 +16,14 @@ export type InputColorProps = {
|
|||||||
doc?: string
|
doc?: string
|
||||||
style?: object
|
style?: object
|
||||||
default?: string
|
default?: string
|
||||||
"aria-label"?: string
|
'aria-label'?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
/*** Number fields with support for min, max and units and documentation*/
|
/*** Number fields with support for min, max and units and documentation*/
|
||||||
export default class InputColor extends React.Component<InputColorProps> {
|
export default class InputColor extends React.Component<InputColorProps> {
|
||||||
state = {
|
state = {
|
||||||
pickerOpened: false
|
pickerOpened: false
|
||||||
};
|
}
|
||||||
colorInput: HTMLInputElement | null = null;
|
colorInput: HTMLInputElement | null = null;
|
||||||
|
|
||||||
constructor (props: InputColorProps) {
|
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
|
//but I am too stupid to get it to work together with fixed position
|
||||||
//and scrollbars so I have to fallback to JavaScript
|
//and scrollbars so I have to fallback to JavaScript
|
||||||
calcPickerOffset = () => {
|
calcPickerOffset = () => {
|
||||||
const elem = this.colorInput;
|
const elem = this.colorInput
|
||||||
if(elem) {
|
if(elem) {
|
||||||
const pos = elem.getBoundingClientRect();
|
const pos = elem.getBoundingClientRect()
|
||||||
return {
|
return {
|
||||||
top: pos.top,
|
top: pos.top,
|
||||||
left: pos.left + 196,
|
left: pos.left + 196,
|
||||||
};
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
top: 160,
|
top: 160,
|
||||||
left: 555,
|
left: 555,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
togglePicker = () => {
|
togglePicker = () => {
|
||||||
this.setState({ pickerOpened: !this.state.pickerOpened });
|
this.setState({ pickerOpened: !this.state.pickerOpened })
|
||||||
};
|
}
|
||||||
|
|
||||||
get color() {
|
get color() {
|
||||||
// Catch invalid color.
|
// Catch invalid color.
|
||||||
try {
|
try {
|
||||||
return Color(this.props.value).rgb();
|
return Color(this.props.value).rgb()
|
||||||
}
|
}
|
||||||
catch(err) {
|
catch(err) {
|
||||||
console.warn("Error parsing color: ", err);
|
console.warn("Error parsing color: ", err);
|
||||||
@@ -74,7 +74,7 @@ export default class InputColor extends React.Component<InputColorProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const offset = this.calcPickerOffset();
|
const offset = this.calcPickerOffset()
|
||||||
const currentColor = this.color.object();
|
const currentColor = this.color.object();
|
||||||
const currentChromeColor = {
|
const currentChromeColor = {
|
||||||
r: currentColor.r,
|
r: currentColor.r,
|
||||||
@@ -82,12 +82,12 @@ export default class InputColor extends React.Component<InputColorProps> {
|
|||||||
b: currentColor.b,
|
b: currentColor.b,
|
||||||
// Rename alpha -> a for ChromePicker
|
// Rename alpha -> a for ChromePicker
|
||||||
a: currentColor.alpha!
|
a: currentColor.alpha!
|
||||||
};
|
}
|
||||||
|
|
||||||
const picker = <div
|
const picker = <div
|
||||||
className="maputnik-color-picker-offset"
|
className="maputnik-color-picker-offset"
|
||||||
style={{
|
style={{
|
||||||
position: "fixed",
|
position: 'fixed',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
left: offset.left,
|
left: offset.left,
|
||||||
top: offset.top,
|
top: offset.top,
|
||||||
@@ -101,14 +101,14 @@ export default class InputColor extends React.Component<InputColorProps> {
|
|||||||
onClick={this.togglePicker}
|
onClick={this.togglePicker}
|
||||||
style={{
|
style={{
|
||||||
zIndex: -1,
|
zIndex: -1,
|
||||||
position: "fixed",
|
position: 'fixed',
|
||||||
top: "0px",
|
top: '0px',
|
||||||
right: "0px",
|
right: '0px',
|
||||||
bottom: "0px",
|
bottom: '0px',
|
||||||
left: "0px",
|
left: '0px',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>
|
||||||
|
|
||||||
const swatchStyle = {
|
const swatchStyle = {
|
||||||
backgroundColor: this.props.value
|
backgroundColor: this.props.value
|
||||||
@@ -118,11 +118,11 @@ export default class InputColor extends React.Component<InputColorProps> {
|
|||||||
{this.state.pickerOpened && picker}
|
{this.state.pickerOpened && picker}
|
||||||
<div className="maputnik-color-swatch" style={swatchStyle}></div>
|
<div className="maputnik-color-swatch" style={swatchStyle}></div>
|
||||||
<input
|
<input
|
||||||
aria-label={this.props["aria-label"]}
|
aria-label={this.props['aria-label']}
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className="maputnik-color"
|
className="maputnik-color"
|
||||||
ref={(input) => {this.colorInput = input;}}
|
ref={(input) => this.colorInput = input}
|
||||||
onClick={this.togglePicker}
|
onClick={this.togglePicker}
|
||||||
style={this.props.style}
|
style={this.props.style}
|
||||||
name={this.props.name}
|
name={this.props.name}
|
||||||
@@ -130,6 +130,7 @@ export default class InputColor extends React.Component<InputColorProps> {
|
|||||||
value={this.props.value ? this.props.value : ""}
|
value={this.props.value ? this.props.value : ""}
|
||||||
onChange={(e) => this.onChange(e.target.value)}
|
onChange={(e) => this.onChange(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,67 +1,64 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import capitalize from "lodash.capitalize";
|
import capitalize from 'lodash.capitalize'
|
||||||
import {MdDelete} from "react-icons/md";
|
import {MdDelete} from 'react-icons/md'
|
||||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
import { WithTranslation, withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import InputString from "./InputString";
|
import InputString from './InputString'
|
||||||
import InputNumber from "./InputNumber";
|
import InputNumber from './InputNumber'
|
||||||
import InputButton from "./InputButton";
|
import InputButton from './InputButton'
|
||||||
import FieldDocLabel from "./FieldDocLabel";
|
import FieldDocLabel from './FieldDocLabel'
|
||||||
import InputEnum from "./InputEnum";
|
import InputEnum from './InputEnum'
|
||||||
import InputUrl from "./InputUrl";
|
import InputUrl from './InputUrl'
|
||||||
import InputColor from "./InputColor";
|
|
||||||
|
|
||||||
|
|
||||||
export type InputDynamicArrayProps = {
|
export type FieldDynamicArrayProps = {
|
||||||
value?: (string | number | undefined)[]
|
value?: (string | number | undefined)[]
|
||||||
type?: "url" | "number" | "enum" | "string" | "color"
|
type?: 'url' | 'number' | 'enum' | 'string'
|
||||||
default?: (string | number | undefined)[]
|
default?: (string | number | undefined)[]
|
||||||
onChange?(values: (string | number | undefined)[] | undefined): unknown
|
onChange?(values: (string | number | undefined)[] | undefined): unknown
|
||||||
style?: object
|
style?: object
|
||||||
fieldSpec?: {
|
fieldSpec?: {
|
||||||
values?: any
|
values?: any
|
||||||
}
|
}
|
||||||
"aria-label"?: string
|
'aria-label'?: string
|
||||||
label: string
|
label: string
|
||||||
};
|
}
|
||||||
|
|
||||||
type InputDynamicArrayInternalProps = InputDynamicArrayProps & WithTranslation;
|
type FieldDynamicArrayInternalProps = FieldDynamicArrayProps & WithTranslation;
|
||||||
|
|
||||||
class InputDynamicArrayInternal extends React.Component<InputDynamicArrayInternalProps> {
|
class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInternalProps> {
|
||||||
changeValue(idx: number, newValue: string | number | undefined) {
|
changeValue(idx: number, newValue: string | number | undefined) {
|
||||||
const values = this.values.slice(0);
|
const values = this.values.slice(0)
|
||||||
values[idx] = newValue;
|
values[idx] = newValue
|
||||||
if (this.props.onChange) this.props.onChange(values);
|
if (this.props.onChange) this.props.onChange(values)
|
||||||
}
|
}
|
||||||
|
|
||||||
get values() {
|
get values() {
|
||||||
return this.props.value || this.props.default || [];
|
return this.props.value || this.props.default || []
|
||||||
}
|
}
|
||||||
|
|
||||||
addValue = () => {
|
addValue = () => {
|
||||||
const values = this.values.slice(0);
|
const values = this.values.slice(0)
|
||||||
if (this.props.type === "number") {
|
if (this.props.type === 'number') {
|
||||||
values.push(0);
|
values.push(0)
|
||||||
}
|
}
|
||||||
else if (this.props.type === "url") {
|
else if (this.props.type === 'url') {
|
||||||
values.push("");
|
values.push("");
|
||||||
}
|
}
|
||||||
else if (this.props.type === "enum") {
|
else if (this.props.type === 'enum') {
|
||||||
const {fieldSpec} = this.props;
|
const {fieldSpec} = this.props;
|
||||||
const defaultValue = Object.keys(fieldSpec!.values)[0];
|
const defaultValue = Object.keys(fieldSpec!.values)[0];
|
||||||
values.push(defaultValue);
|
values.push(defaultValue);
|
||||||
} else if (this.props.type === "color") {
|
|
||||||
values.push("#000000");
|
|
||||||
} else {
|
} else {
|
||||||
values.push("");
|
values.push("")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.onChange) this.props.onChange(values);
|
if (this.props.onChange) this.props.onChange(values)
|
||||||
};
|
}
|
||||||
|
|
||||||
deleteValue(valueIdx: number) {
|
deleteValue(valueIdx: number) {
|
||||||
const values = this.values.slice(0);
|
const values = this.values.slice(0)
|
||||||
values.splice(valueIdx, 1);
|
values.splice(valueIdx, 1)
|
||||||
|
|
||||||
if (this.props.onChange) this.props.onChange(values.length > 0 ? values : undefined);
|
if (this.props.onChange) this.props.onChange(values.length > 0 ? values : undefined);
|
||||||
}
|
}
|
||||||
@@ -75,42 +72,35 @@ class InputDynamicArrayInternal extends React.Component<InputDynamicArrayInterna
|
|||||||
{...i18nProps}
|
{...i18nProps}
|
||||||
/>;
|
/>;
|
||||||
let input;
|
let input;
|
||||||
if(this.props.type === "url") {
|
if(this.props.type === 'url') {
|
||||||
input = <InputUrl
|
input = <InputUrl
|
||||||
value={v as string}
|
value={v as string}
|
||||||
onChange={this.changeValue.bind(this, i)}
|
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
|
input = <InputNumber
|
||||||
value={v as number}
|
value={v as number}
|
||||||
onChange={this.changeValue.bind(this, i)}
|
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)]);
|
const options = Object.keys(this.props.fieldSpec?.values).map(v => [v, capitalize(v)]);
|
||||||
input = <InputEnum
|
input = <InputEnum
|
||||||
options={options}
|
options={options}
|
||||||
value={v as string}
|
value={v as string}
|
||||||
onChange={this.changeValue.bind(this, i)}
|
onChange={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 {
|
else {
|
||||||
input = <InputString
|
input = <InputString
|
||||||
value={v as string}
|
value={v as string}
|
||||||
onChange={this.changeValue.bind(this, i)}
|
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
|
return <div
|
||||||
@@ -124,8 +114,8 @@ class InputDynamicArrayInternal extends React.Component<InputDynamicArrayInterna
|
|||||||
<div className="maputnik-array-block-content">
|
<div className="maputnik-array-block-content">
|
||||||
{input}
|
{input}
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>
|
||||||
});
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="maputnik-array">
|
<div className="maputnik-array">
|
||||||
@@ -141,8 +131,8 @@ class InputDynamicArrayInternal extends React.Component<InputDynamicArrayInterna
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const InputDynamicArray = withTranslation()(InputDynamicArrayInternal);
|
const FieldDynamicArray = withTranslation()(FieldDynamicArrayInternal);
|
||||||
export default InputDynamicArray;
|
export default FieldDynamicArray;
|
||||||
|
|
||||||
type DeleteValueInputButtonProps = {
|
type DeleteValueInputButtonProps = {
|
||||||
onClick?(...args: unknown[]): unknown
|
onClick?(...args: unknown[]): unknown
|
||||||
@@ -159,6 +149,7 @@ class DeleteValueInputButton extends React.Component<DeleteValueInputButtonProps
|
|||||||
<FieldDocLabel
|
<FieldDocLabel
|
||||||
label={<MdDelete />}
|
label={<MdDelete />}
|
||||||
/>
|
/>
|
||||||
</InputButton>;
|
</InputButton>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import InputSelect from "./InputSelect";
|
import InputSelect from './InputSelect'
|
||||||
import InputMultiInput from "./InputMultiInput";
|
import InputMultiInput from './InputMultiInput'
|
||||||
|
|
||||||
|
|
||||||
function optionsLabelLength(options: any[]) {
|
function optionsLabelLength(options: any[]) {
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
options.forEach(([_, label]) => {
|
options.forEach(([_, label]) => {
|
||||||
sum += label.length;
|
sum += label.length
|
||||||
});
|
})
|
||||||
return sum;
|
return sum
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ export type InputEnumProps = {
|
|||||||
name?: string
|
name?: string
|
||||||
onChange(...args: unknown[]): unknown
|
onChange(...args: unknown[]): unknown
|
||||||
options: any[]
|
options: any[]
|
||||||
"aria-label"?: string
|
'aria-label'?: string
|
||||||
label?: string
|
label?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,15 +35,16 @@ export default class InputEnum extends React.Component<InputEnumProps> {
|
|||||||
options={options}
|
options={options}
|
||||||
value={(value || this.props.default)!}
|
value={(value || this.props.default)!}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
aria-label={this.props["aria-label"] || label}
|
aria-label={this.props['aria-label'] || label}
|
||||||
/>;
|
/>
|
||||||
} else {
|
} else {
|
||||||
return <InputSelect
|
return <InputSelect
|
||||||
options={options}
|
options={options}
|
||||||
value={(value || this.props.default)!}
|
value={(value || this.props.default)!}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
aria-label={this.props["aria-label"] || label}
|
aria-label={this.props['aria-label'] || label}
|
||||||
/>;
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import InputAutocomplete from "./InputAutocomplete";
|
import InputAutocomplete from './InputAutocomplete'
|
||||||
|
|
||||||
export type InputFontProps = {
|
export type FieldFontProps = {
|
||||||
name: string
|
name: string
|
||||||
value?: string[]
|
value?: string[]
|
||||||
default?: string[]
|
default?: string[]
|
||||||
fonts?: unknown[]
|
fonts?: unknown[]
|
||||||
style?: object
|
style?: object
|
||||||
onChange(...args: unknown[]): unknown
|
onChange(...args: unknown[]): unknown
|
||||||
"aria-label"?: string
|
'aria-label'?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class InputFont extends React.Component<InputFontProps> {
|
export default class FieldFont extends React.Component<FieldFontProps> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
fonts: []
|
fonts: []
|
||||||
};
|
}
|
||||||
|
|
||||||
get values() {
|
get values() {
|
||||||
const out = this.props.value || this.props.default || [];
|
const out = this.props.value || this.props.default || [];
|
||||||
@@ -29,11 +29,11 @@ export default class InputFont extends React.Component<InputFontProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
changeFont(idx: number, newValue: string) {
|
changeFont(idx: number, newValue: string) {
|
||||||
const changedValues = this.values.slice(0);
|
const changedValues = this.values.slice(0)
|
||||||
changedValues[idx] = newValue;
|
changedValues[idx] = newValue
|
||||||
const filteredValues = changedValues
|
const filteredValues = changedValues
|
||||||
.filter(v => v !== undefined)
|
.filter(v => v !== undefined)
|
||||||
.filter(v => v !== "");
|
.filter(v => v !== "")
|
||||||
|
|
||||||
this.props.onChange(filteredValues);
|
this.props.onChange(filteredValues);
|
||||||
}
|
}
|
||||||
@@ -44,13 +44,13 @@ export default class InputFont extends React.Component<InputFontProps> {
|
|||||||
key={i}
|
key={i}
|
||||||
>
|
>
|
||||||
<InputAutocomplete
|
<InputAutocomplete
|
||||||
aria-label={this.props["aria-label"] || this.props.name}
|
aria-label={this.props['aria-label'] || this.props.name}
|
||||||
value={value}
|
value={value}
|
||||||
options={this.props.fonts?.map(f => [f, f])}
|
options={this.props.fonts?.map(f => [f, f])}
|
||||||
onChange={this.changeFont.bind(this, i)}
|
onChange={this.changeFont.bind(this, i)}
|
||||||
/>
|
/>
|
||||||
</li>;
|
</li>
|
||||||
});
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="maputnik-font">
|
<ul className="maputnik-font">
|
||||||
|
|||||||
+103
-58
@@ -1,98 +1,126 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import classnames from "classnames";
|
import classnames from 'classnames';
|
||||||
import { type WithTranslation, withTranslation } from "react-i18next";
|
import CodeMirror, { ModeSpec } from 'codemirror';
|
||||||
|
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { type EditorView } from "@codemirror/view";
|
import 'codemirror/mode/javascript/javascript'
|
||||||
import stringifyPretty from "json-stringify-pretty-compact";
|
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 {createEditor} from "../libs/codemirror-editor-factory";
|
|
||||||
import type { StylePropertySpecification } from "maplibre-gl";
|
|
||||||
import type { TransactionSpec } from "@codemirror/state";
|
|
||||||
|
|
||||||
export type InputJsonProps = {
|
export type InputJsonProps = {
|
||||||
value: object
|
layer: any
|
||||||
|
maxHeight?: number
|
||||||
|
onChange?(...args: unknown[]): unknown
|
||||||
|
lineNumbers?: boolean
|
||||||
|
lineWrapping?: boolean
|
||||||
|
getValue?(data: any): string
|
||||||
|
gutters?: string[]
|
||||||
className?: string
|
className?: string
|
||||||
onChange(object: object): void
|
|
||||||
onFocus?(...args: unknown[]): unknown
|
onFocus?(...args: unknown[]): unknown
|
||||||
onBlur?(...args: unknown[]): unknown
|
onBlur?(...args: unknown[]): unknown
|
||||||
lintType: "layer" | "style" | "expression" | "json"
|
onJSONValid?(...args: unknown[]): unknown
|
||||||
spec?: StylePropertySpecification | undefined
|
onJSONInvalid?(...args: unknown[]): unknown
|
||||||
/**
|
mode?: ModeSpec<any>
|
||||||
* When setting this and using search and replace, the editor will scroll to the selected text
|
lint?: boolean | object
|
||||||
* Use this only when the editor is the only element in the page.
|
|
||||||
*/
|
|
||||||
withScroll?: boolean
|
|
||||||
};
|
};
|
||||||
type InputJsonInternalProps = InputJsonProps & WithTranslation;
|
type InputJsonInternalProps = InputJsonProps & WithTranslation;
|
||||||
|
|
||||||
type InputJsonState = {
|
type InputJsonState = {
|
||||||
isEditing: boolean
|
isEditing: boolean
|
||||||
|
showMessage: boolean
|
||||||
prevValue: string
|
prevValue: string
|
||||||
};
|
};
|
||||||
|
|
||||||
class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJsonState> {
|
class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJsonState> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
lineNumbers: true,
|
||||||
|
lineWrapping: false,
|
||||||
|
gutters: ["CodeMirror-lint-markers"],
|
||||||
|
getValue: (data: any) => {
|
||||||
|
return stringifyPretty(data, {indent: 2, maxLength: 40});
|
||||||
|
},
|
||||||
onFocus: () => {},
|
onFocus: () => {},
|
||||||
onBlur: () => {},
|
onBlur: () => {},
|
||||||
withScroll: false
|
onJSONInvalid: () => {},
|
||||||
};
|
onJSONValid: () => {},
|
||||||
_view: EditorView | undefined;
|
}
|
||||||
|
_keyEvent: string;
|
||||||
|
_doc: CodeMirror.Editor | undefined;
|
||||||
_el: HTMLDivElement | null = null;
|
_el: HTMLDivElement | null = null;
|
||||||
_cancelNextChange: boolean = false;
|
_cancelNextChange: boolean = false;
|
||||||
|
|
||||||
constructor(props: InputJsonInternalProps) {
|
constructor(props: InputJsonInternalProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this._keyEvent = "keyboard";
|
||||||
this.state = {
|
this.state = {
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
prevValue: this.getPrettyJson(this.props.value),
|
showMessage: false,
|
||||||
|
prevValue: this.props.getValue!(this.props.layer),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getPrettyJson(data: any) {
|
componentDidMount () {
|
||||||
return stringifyPretty(data, {indent: 2, maxLength: 40});
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
onPointerDown = () => {
|
||||||
this._view = createEditor({
|
this._keyEvent = "pointer";
|
||||||
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 = () => {
|
onFocus = () => {
|
||||||
if (this.props.onFocus) this.props.onFocus();
|
if (this.props.onFocus) this.props.onFocus();
|
||||||
this.setState({
|
this.setState({
|
||||||
isEditing: true,
|
isEditing: true,
|
||||||
|
showMessage: (this._keyEvent === "keyboard"),
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
onBlur = () => {
|
onBlur = () => {
|
||||||
|
this._keyEvent = "keyboard";
|
||||||
if (this.props.onBlur) this.props.onBlur();
|
if (this.props.onBlur) this.props.onBlur();
|
||||||
this.setState({
|
this.setState({
|
||||||
isEditing: false,
|
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) {
|
componentDidUpdate(prevProps: InputJsonProps) {
|
||||||
if (!this.state.isEditing && prevProps.value !== this.props.value) {
|
if (!this.state.isEditing && prevProps.layer !== this.props.layer) {
|
||||||
this._cancelNextChange = true;
|
this._cancelNextChange = true;
|
||||||
const transactionSpec: TransactionSpec = {
|
this._doc!.setValue(
|
||||||
changes: {
|
this.props.getValue!(this.props.layer),
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,11 +128,11 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
|
|||||||
if (this._cancelNextChange) {
|
if (this._cancelNextChange) {
|
||||||
this._cancelNextChange = false;
|
this._cancelNextChange = false;
|
||||||
this.setState({
|
this.setState({
|
||||||
prevValue: this._view!.state.doc.toString(),
|
prevValue: this._doc!.getValue(),
|
||||||
});
|
})
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newCode = this._view!.state.doc.toString();
|
const newCode = this._doc!.getValue();
|
||||||
|
|
||||||
if (this.state.prevValue !== newCode) {
|
if (this.state.prevValue !== newCode) {
|
||||||
let parsedLayer, err;
|
let parsedLayer, err;
|
||||||
@@ -112,26 +140,43 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
|
|||||||
parsedLayer = JSON.parse(newCode);
|
parsedLayer = JSON.parse(newCode);
|
||||||
} catch(_err) {
|
} catch(_err) {
|
||||||
err = _err;
|
err = _err;
|
||||||
console.warn(_err);
|
console.warn(_err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!err) {
|
if (err && this.props.onJSONInvalid) {
|
||||||
if (this.props.onChange) this.props.onChange(parsedLayer);
|
this.props.onJSONInvalid();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (this.props.onChange) this.props.onChange(parsedLayer)
|
||||||
|
if (this.props.onJSONValid) this.props.onJSONValid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
prevValue: newCode,
|
prevValue: newCode,
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div className="json-editor" data-wd-key="json-editor" aria-hidden="true" style={{cursor: "text"}}>
|
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>
|
||||||
<div
|
<div
|
||||||
className={classnames("codemirror-container", this.props.className)}
|
className={classnames("codemirror-container", this.props.className)}
|
||||||
ref={(el) => {this._el = el;}}
|
ref={(el) => this._el = el}
|
||||||
|
style={style}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
import classnames from "classnames";
|
import classnames from 'classnames'
|
||||||
|
|
||||||
export type InputMultiInputProps = {
|
export type InputMultiInputProps = {
|
||||||
name?: string
|
name?: string
|
||||||
value: string
|
value: string
|
||||||
options: any[]
|
options: any[]
|
||||||
onChange(...args: unknown[]): unknown
|
onChange(...args: unknown[]): unknown
|
||||||
"aria-label"?: string
|
'aria-label'?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class InputMultiInput extends React.Component<InputMultiInputProps> {
|
export default class InputMultiInput extends React.Component<InputMultiInputProps> {
|
||||||
render() {
|
render() {
|
||||||
let options = this.props.options;
|
let options = this.props.options
|
||||||
if(options.length > 0 && !Array.isArray(options[0])) {
|
if(options.length > 0 && !Array.isArray(options[0])) {
|
||||||
options = options.map(v => [v, v]);
|
options = options.map(v => [v, v])
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedValue = this.props.value || options[0][0];
|
const selectedValue = this.props.value || options[0][0]
|
||||||
const radios = options.map(([val, label])=> {
|
const radios = options.map(([val, label])=> {
|
||||||
return <label
|
return <label
|
||||||
key={val}
|
key={val}
|
||||||
className={classnames("maputnik-button", "maputnik-radio-as-button", {"maputnik-button-selected": val === selectedValue})}
|
className={classnames("maputnik-radio-as-button", {"maputnik-button-selected": val === selectedValue})}
|
||||||
>
|
>
|
||||||
<input type="radio"
|
<input type="radio"
|
||||||
name={this.props.name}
|
name={this.props.name}
|
||||||
@@ -29,11 +29,13 @@ export default class InputMultiInput extends React.Component<InputMultiInputProp
|
|||||||
checked={val === selectedValue}
|
checked={val === selectedValue}
|
||||||
/>
|
/>
|
||||||
{label}
|
{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}
|
{radios}
|
||||||
</fieldset>;
|
</fieldset>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { type BaseSyntheticEvent } from "react";
|
import React, { BaseSyntheticEvent } from 'react'
|
||||||
import generateUniqueId from "../libs/document-uid";
|
import generateUniqueId from '../libs/document-uid';
|
||||||
|
|
||||||
export type InputNumberProps = {
|
export type InputNumberProps = {
|
||||||
value?: number
|
value?: number
|
||||||
@@ -23,22 +23,22 @@ type InputNumberState = {
|
|||||||
* This is the value that is currently being edited. It can be an invalid value.
|
* This is the value that is currently being edited. It can be an invalid value.
|
||||||
*/
|
*/
|
||||||
dirtyValue?: number | string | undefined
|
dirtyValue?: number | string | undefined
|
||||||
};
|
}
|
||||||
|
|
||||||
export default class InputNumber extends React.Component<InputNumberProps, InputNumberState> {
|
export default class InputNumber extends React.Component<InputNumberProps, InputNumberState> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
rangeStep: 1
|
rangeStep: 1
|
||||||
};
|
}
|
||||||
_keyboardEvent: boolean = false;
|
_keyboardEvent: boolean = false;
|
||||||
|
|
||||||
constructor(props: InputNumberProps) {
|
constructor(props: InputNumberProps) {
|
||||||
super(props);
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
uuid: +generateUniqueId(),
|
uuid: +generateUniqueId(),
|
||||||
editing: false,
|
editing: false,
|
||||||
value: props.value,
|
value: props.value,
|
||||||
dirtyValue: props.value,
|
dirtyValue: props.value,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props: Readonly<InputNumberProps>, state: InputNumberState) {
|
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;
|
const hasChanged = this.props.value !== value;
|
||||||
if(this.isValid(value) && hasChanged) {
|
if(this.isValid(value) && hasChanged) {
|
||||||
if (this.props.onChange) this.props.onChange(value);
|
if (this.props.onChange) this.props.onChange(value)
|
||||||
this.setState({
|
this.setState({
|
||||||
value: value,
|
value: value,
|
||||||
});
|
});
|
||||||
@@ -70,7 +70,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
|||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
dirtyValue: newValue === "" ? undefined : newValue,
|
dirtyValue: newValue === "" ? undefined : newValue,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
isValid(v: number | string | undefined) {
|
isValid(v: number | string | undefined) {
|
||||||
@@ -80,18 +80,18 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
|||||||
|
|
||||||
const value = +v;
|
const value = +v;
|
||||||
if(isNaN(value)) {
|
if(isNaN(value)) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!isNaN(this.props.min!) && value < this.props.min!) {
|
if(!isNaN(this.props.min!) && value < this.props.min!) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!isNaN(this.props.max!) && value > this.props.max!) {
|
if(!isNaN(this.props.max!) && value > this.props.max!) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
resetValue = () => {
|
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 set value is invalid fall back to the last valid value from props or at last resort the default value
|
||||||
if (!this.isValid(this.state.value)) {
|
if (!this.isValid(this.state.value)) {
|
||||||
if(this.isValid(this.props.value)) {
|
if(this.isValid(this.props.value)) {
|
||||||
this.changeValue(this.props.value);
|
this.changeValue(this.props.value)
|
||||||
this.setState({dirtyValue: this.props.value});
|
this.setState({dirtyValue: this.props.value});
|
||||||
} else {
|
} else {
|
||||||
this.changeValue(undefined);
|
this.changeValue(undefined);
|
||||||
this.setState({dirtyValue: undefined});
|
this.setState({dirtyValue: undefined});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
onChangeRange = (e: BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) => {
|
onChangeRange = (e: BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) => {
|
||||||
let value = parseFloat(e.target.value);
|
let value = parseFloat(e.target.value);
|
||||||
@@ -132,7 +132,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
|||||||
value = this.state.value! - step;
|
value = this.state.value! - step;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
value = this.state.value! + step;
|
value = this.state.value! + step
|
||||||
}
|
}
|
||||||
dirtyValue = value;
|
dirtyValue = value;
|
||||||
}
|
}
|
||||||
@@ -153,7 +153,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
|||||||
|
|
||||||
this.setState({value, dirtyValue});
|
this.setState({value, dirtyValue});
|
||||||
if (this.props.onChange) this.props.onChange(value);
|
if (this.props.onChange) this.props.onChange(value);
|
||||||
};
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if(
|
if(
|
||||||
@@ -217,18 +217,18 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
|||||||
}}
|
}}
|
||||||
onBlur={_e => {
|
onBlur={_e => {
|
||||||
this.setState({editing: false});
|
this.setState({editing: false});
|
||||||
this.resetValue();
|
this.resetValue()
|
||||||
}}
|
}}
|
||||||
data-wd-key={this.props["data-wd-key"] + "-text"}
|
data-wd-key={this.props["data-wd-key"] + "-text"}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const value = this.state.editing ? this.state.dirtyValue : this.state.value;
|
const value = this.state.editing ? this.state.dirtyValue : this.state.value;
|
||||||
|
|
||||||
return <input
|
return <input
|
||||||
aria-label={this.props["aria-label"]}
|
aria-label={this.props['aria-label']}
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
className="maputnik-number"
|
className="maputnik-number"
|
||||||
placeholder={this.props.default?.toString()}
|
placeholder={this.props.default?.toString()}
|
||||||
@@ -240,7 +240,9 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
|
|||||||
onBlur={this.resetValue}
|
onBlur={this.resetValue}
|
||||||
required={this.props.required}
|
required={this.props.required}
|
||||||
data-wd-key={this.props["data-wd-key"]}
|
data-wd-key={this.props["data-wd-key"]}
|
||||||
/>;
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React from 'react'
|
||||||
|
|
||||||
export type InputSelectProps = {
|
export type InputSelectProps = {
|
||||||
value: string
|
value: string
|
||||||
@@ -7,7 +7,7 @@ export type InputSelectProps = {
|
|||||||
style?: object
|
style?: object
|
||||||
onChange(value: string | [string, any]): unknown
|
onChange(value: string | [string, any]): unknown
|
||||||
title?: string
|
title?: string
|
||||||
"aria-label"?: string
|
'aria-label'?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class InputSelect extends React.Component<InputSelectProps> {
|
export default class InputSelect extends React.Component<InputSelectProps> {
|
||||||
@@ -24,9 +24,11 @@ export default class InputSelect extends React.Component<InputSelectProps> {
|
|||||||
title={this.props.title}
|
title={this.props.title}
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onChange={e => this.props.onChange(e.target.value)}
|
onChange={e => this.props.onChange(e.target.value)}
|
||||||
aria-label={this.props["aria-label"]}
|
aria-label={this.props['aria-label']}
|
||||||
>
|
>
|
||||||
{ options.map(([val, label]) => <option key={val} value={val}>{label}</option>) }
|
{ options.map(([val, label]) => <option key={val} value={val}>{label}</option>) }
|
||||||
</select>;
|
</select>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+77
-103
@@ -1,26 +1,24 @@
|
|||||||
import React, { type ReactElement } from "react";
|
import React, { ReactElement } from 'react'
|
||||||
|
|
||||||
import InputColor, { type InputColorProps } from "./InputColor";
|
import InputColor, { InputColorProps } from './InputColor'
|
||||||
import InputNumber, { type InputNumberProps } from "./InputNumber";
|
import InputNumber, { InputNumberProps } from './InputNumber'
|
||||||
import InputCheckbox, { type InputCheckboxProps } from "./InputCheckbox";
|
import InputCheckbox, { InputCheckboxProps } from './InputCheckbox'
|
||||||
import InputString, { type InputStringProps } from "./InputString";
|
import InputString, { InputStringProps } from './InputString'
|
||||||
import InputArray, { type InputArrayProps } from "./InputArray";
|
import InputArray, { FieldArrayProps } from './InputArray'
|
||||||
import InputDynamicArray, { type InputDynamicArrayProps } from "./InputDynamicArray";
|
import InputDynamicArray, { FieldDynamicArrayProps } from './InputDynamicArray'
|
||||||
import InputFont, { type InputFontProps } from "./InputFont";
|
import InputFont, { FieldFontProps } from './InputFont'
|
||||||
import InputAutocomplete, { type InputAutocompleteProps } from "./InputAutocomplete";
|
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
|
||||||
import InputEnum, { type InputEnumProps } from "./InputEnum";
|
import InputEnum, { InputEnumProps } from './InputEnum'
|
||||||
import capitalize from "lodash.capitalize";
|
import capitalize from 'lodash.capitalize'
|
||||||
|
|
||||||
const iconProperties = ["background-pattern", "fill-pattern", "line-pattern", "fill-extrusion-pattern", "icon-image"];
|
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
|
||||||
|
|
||||||
export type FieldSpecType = "number" | "enum" | "resolvedImage" | "formatted" | "string" | "color" | "boolean" | "array" | "numberArray" | "padding" | "colorArray" | "variableAnchorOffsetCollection";
|
export type SpecFieldProps = {
|
||||||
|
|
||||||
export type InputSpecProps = {
|
|
||||||
onChange?(fieldName: string | undefined, value: number | undefined | (string | number | undefined)[]): unknown
|
onChange?(fieldName: string | undefined, value: number | undefined | (string | number | undefined)[]): unknown
|
||||||
fieldName?: string
|
fieldName?: string
|
||||||
fieldSpec?: {
|
fieldSpec?: {
|
||||||
default?: unknown
|
default?: unknown
|
||||||
type?: FieldSpecType
|
type?: 'number' | 'enum' | 'resolvedImage' | 'formatted' | 'string' | 'color' | 'boolean' | 'array'
|
||||||
minimum?: number
|
minimum?: number
|
||||||
maximum?: number
|
maximum?: number
|
||||||
values?: unknown[]
|
values?: unknown[]
|
||||||
@@ -30,7 +28,8 @@ export type InputSpecProps = {
|
|||||||
value?: string | number | unknown[] | boolean
|
value?: string | number | unknown[] | boolean
|
||||||
/** Override the style of the field */
|
/** Override the style of the field */
|
||||||
style?: object
|
style?: object
|
||||||
"aria-label"?: string
|
'aria-label'?: string
|
||||||
|
error?: unknown[]
|
||||||
label?: string
|
label?: string
|
||||||
action?: ReactElement
|
action?: ReactElement
|
||||||
};
|
};
|
||||||
@@ -38,10 +37,11 @@ export type InputSpecProps = {
|
|||||||
/** Display any field from the Maplibre GL style spec and
|
/** Display any field from the Maplibre GL style spec and
|
||||||
* choose the correct field component based on the @{fieldSpec}
|
* choose the correct field component based on the @{fieldSpec}
|
||||||
* to display @{value}. */
|
* to display @{value}. */
|
||||||
export default class InputSpec extends React.Component<InputSpecProps> {
|
export default class SpecField extends React.Component<SpecFieldProps> {
|
||||||
|
|
||||||
childNodes() {
|
childNodes() {
|
||||||
const commonProps = {
|
const commonProps = {
|
||||||
|
error: this.props.error,
|
||||||
fieldSpec: this.props.fieldSpec,
|
fieldSpec: this.props.fieldSpec,
|
||||||
label: this.props.label,
|
label: this.props.label,
|
||||||
action: this.props.action,
|
action: this.props.action,
|
||||||
@@ -51,96 +51,70 @@ export default class InputSpec extends React.Component<InputSpecProps> {
|
|||||||
name: this.props.fieldName,
|
name: this.props.fieldName,
|
||||||
"data-wd-key": "spec-field-input:" + 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),
|
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) {
|
switch(this.props.fieldSpec?.type) {
|
||||||
case "number": return (
|
case 'number': return (
|
||||||
<InputNumber
|
<InputNumber
|
||||||
{...commonProps as InputNumberProps}
|
{...commonProps as InputNumberProps}
|
||||||
min={this.props.fieldSpec.minimum}
|
min={this.props.fieldSpec.minimum}
|
||||||
max={this.props.fieldSpec.maximum}
|
max={this.props.fieldSpec.maximum}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
case "enum": {
|
case 'enum': {
|
||||||
const options = Object.keys(this.props.fieldSpec.values || []).map(v => [v, capitalize(v)]);
|
const options = Object.keys(this.props.fieldSpec.values || []).map(v => [v, capitalize(v)])
|
||||||
|
|
||||||
return <InputEnum
|
return <InputEnum
|
||||||
{...commonProps as Omit<InputEnumProps, "options">}
|
{...commonProps as Omit<InputEnumProps, "options">}
|
||||||
options={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}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
case "resolvedImage":
|
case 'color': return (
|
||||||
case "formatted":
|
<InputColor
|
||||||
case "string":
|
{...commonProps as InputColorProps}
|
||||||
if (iconProperties.indexOf(this.props.fieldName!) >= 0) {
|
/>
|
||||||
const options = this.props.fieldSpec.values || [];
|
)
|
||||||
return <InputAutocomplete
|
case 'boolean': return (
|
||||||
{...commonProps as Omit<InputAutocompleteProps, "options">}
|
<InputCheckbox
|
||||||
options={options.map(f => [f, f])}
|
{...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}
|
||||||
|
/>
|
||||||
} else {
|
} else {
|
||||||
return <InputString
|
return <InputDynamicArray
|
||||||
{...commonProps as InputStringProps}
|
{...commonProps as FieldDynamicArrayProps}
|
||||||
/>;
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
type={this.props.fieldSpec.value as FieldDynamicArrayProps['type']}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
case "color": return (
|
}
|
||||||
<InputColor
|
default: return null
|
||||||
{...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 = {
|
export type InputStringProps = {
|
||||||
"data-wd-key"?: string
|
"data-wd-key"?: string
|
||||||
@@ -11,26 +11,26 @@ export type InputStringProps = {
|
|||||||
required?: boolean
|
required?: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
spellCheck?: boolean
|
spellCheck?: boolean
|
||||||
"aria-label"?: string
|
'aria-label'?: string
|
||||||
title?: string
|
title?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
type InputStringState = {
|
type InputStringState = {
|
||||||
editing: boolean
|
editing: boolean
|
||||||
value?: string
|
value?: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export default class InputString extends React.Component<InputStringProps, InputStringState> {
|
export default class InputString extends React.Component<InputStringProps, InputStringState> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onInput: () => {},
|
onInput: () => {},
|
||||||
};
|
}
|
||||||
|
|
||||||
constructor(props: InputStringProps) {
|
constructor(props: InputStringProps) {
|
||||||
super(props);
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
editing: false,
|
editing: false,
|
||||||
value: props.value || ""
|
value: props.value || ''
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props: Readonly<InputStringProps>, state: InputStringState) {
|
static getDerivedStateFromProps(props: Readonly<InputStringProps>, state: InputStringState) {
|
||||||
@@ -47,17 +47,17 @@ export default class InputString extends React.Component<InputStringProps, Input
|
|||||||
let classes;
|
let classes;
|
||||||
|
|
||||||
if(this.props.multi) {
|
if(this.props.multi) {
|
||||||
tag = "textarea";
|
tag = "textarea"
|
||||||
classes = [
|
classes = [
|
||||||
"maputnik-string",
|
"maputnik-string",
|
||||||
"maputnik-string--multi"
|
"maputnik-string--multi"
|
||||||
];
|
]
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
tag = "input";
|
tag = "input"
|
||||||
classes = [
|
classes = [
|
||||||
"maputnik-string"
|
"maputnik-string"
|
||||||
];
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.props.disabled) {
|
if(this.props.disabled) {
|
||||||
@@ -97,3 +97,5 @@ export default class InputString extends React.Component<InputStringProps, Input
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user