Compare commits

..

2 Commits

Author SHA1 Message Date
dependabot[bot] bdff66a44b Merge 3e7e1e6ce9 into 4760da28ac 2025-02-25 13:55:45 +00:00
dependabot[bot] 3e7e1e6ce9 Bump @shellygo/cypress-test-utils from 4.1.12 to 4.1.13
Bumps [@shellygo/cypress-test-utils](https://github.com/shellyDCMS/cypress-test-utils) from 4.1.12 to 4.1.13.
- [Commits](https://github.com/shellyDCMS/cypress-test-utils/commits)

---
updated-dependencies:
- dependency-name: "@shellygo/cypress-test-utils"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 13:37:58 +00:00
185 changed files with 10838 additions and 15347 deletions
-22
View File
@@ -11,29 +11,7 @@ updates:
interval: "daily"
open-pull-requests-limit: 20
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"
directory: "/"
schedule:
interval: "daily"
cooldown:
default-days: 3
# no semver support for github-actions
# => no specific configuration for this
include:
- "*"
+2 -2
View File
@@ -7,11 +7,11 @@ permissions: write-all
jobs:
dependabot:
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }}
if: ${{ github.actor == 'dependabot[bot]' }}
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
uses: dependabot/fetch-metadata@v2.3.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Approve Dependabot PRs
+30 -71
View File
@@ -7,12 +7,21 @@ on:
branches: [ main ]
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:
name: "build on ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
permissions:
contents: read
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
strategy:
@@ -21,9 +30,8 @@ jobs:
os: [ ubuntu-latest, windows-latest, macos-latest ]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: { persist-credentials: false }
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- run: npm ci
@@ -36,27 +44,24 @@ jobs:
build-artifacts:
name: "build artifacts"
runs-on: ubuntu-latest
permissions:
contents: read
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: { persist-credentials: false }
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- run: npm ci
- run: npm run build
- name: artifacts/maputnik
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@v4
with:
name: maputnik
path: dist
# Build and upload desktop CLI artifacts
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
uses: actions/setup-go@v5
with:
go-version: ^1.23.x
cache-dependency-path: desktop/go.sum
@@ -66,89 +71,43 @@ jobs:
run: npm run build-desktop
- name: Artifacts/linux
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@v4
with:
name: maputnik-linux
path: ./desktop/bin/linux/
- name: Artifacts/darwin
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@v4
with:
name: maputnik-darwin
path: ./desktop/bin/darwin/
- name: Artifacts/windows
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@v4
with:
name: maputnik-windows
path: ./desktop/bin/windows/
unit-tests:
name: "Unit tests"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@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:
name: "E2E tests using chrome"
name: "E2E tests using ${{ matrix.browser }}"
strategy:
fail-fast: false
matrix:
browser: [ chrome ]
runs-on: ubuntu-latest
permissions:
contents: read
runs-on: ubuntu-22.04
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'
uses: actions/checkout@v4
- run: npm ci
- name: Cypress run
uses: cypress-io/github-action@fa4a118725a8f001170d49631ea89e5d66fee626 # v7.4.1
uses: cypress-io/github-action@v6
with:
build: npm run build
start: npm run start
browser: chrome
browser: ${{ matrix.browser }}
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
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
uses: codecov/codecov-action@v5
with:
files: ${{ github.workspace }}/.nyc_output/out.json
verbose: true
+4 -4
View File
@@ -38,11 +38,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -53,7 +53,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/autobuild@v3
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -67,4 +67,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
uses: github/codeql-action/analyze@v3
+3 -3
View File
@@ -16,13 +16,13 @@ jobs:
run:
shell: bash
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
@@ -32,7 +32,7 @@ jobs:
./build/bump-version-changelog.js ${{ inputs.version }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
uses: peter-evans/create-pull-request@v7
with:
commit-message: Bump version to ${{ inputs.version }}
branch: bump-version-to-${{ inputs.version }}
+7 -11
View File
@@ -8,15 +8,12 @@ jobs:
deploy-pages:
name: deploy/pages
runs-on: ubuntu-latest
permissions:
contents: write
if: ${{ github.event_name == 'push' }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with: { persist-credentials: false }
- uses: actions/checkout@v4
- name: Use Node.js from nvmrc
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -27,7 +24,7 @@ jobs:
run: npm run build
- name: Upload to GitHub Pages
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: dist
@@ -36,20 +33,19 @@ jobs:
deploy-docker:
name: deploy/docker
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
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 push ghcr.io/maplibre/maputnik:main
+9 -12
View File
@@ -12,23 +12,20 @@ jobs:
defaults:
run:
shell: bash
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
persist-credentials: false
- name: Use Node.js from nvmrc
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Check if version has been updated
id: check
uses: EndBug/version-check@095362f3cd50f690c8fa0e6afeea81834bd8d320 # latest
uses: EndBug/version-check@v2
outputs:
publish: ${{ steps.check.outputs.changed }}
@@ -42,19 +39,19 @@ jobs:
run:
shell: bash
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
- name: Use Node.js from nvmrc
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
registry-url: "https://registry.npmjs.org"
- name: Set up Go for desktop build
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
uses: actions/setup-go@v5
with:
go-version: ^1.23.x
cache-dependency-path: desktop/go.sum
@@ -62,7 +59,7 @@ jobs:
- name: Get 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
run: npm ci
@@ -73,7 +70,7 @@ jobs:
- name: Tag commit and push
id: tag_version
uses: mathieudutour/github-tag-action@a22cf08638b34d5badda920f9daf6e72c477b07b # v6.2
uses: mathieudutour/github-tag-action@v6.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
custom_tag: ${{ steps.package-version.outputs.current-version }}
@@ -91,7 +88,7 @@ jobs:
- name: Create GitHub Release
id: create_regular_release
uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0
uses: ncipollo/release-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
+1 -1
View File
@@ -6,7 +6,7 @@ ci:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
rev: v5.0.0
hooks:
- id: check-added-large-files
- id: check-executables-have-shebangs
-47
View File
@@ -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.
+2 -52
View File
@@ -1,46 +1,6 @@
## main
### ✨ 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
- Use same version number for web and desktop versions
- Add scheme type options for vector/raster tile
@@ -50,22 +10,12 @@
- Upgrade to MapLibre LG JS v5
- Upgrade Vite 6 and Cypress 14 ([#970](https://github.com/maplibre/maputnik/pull/970))
- Upgrade OpenLayers from v6 to v10
- When loading a style into localStorage that causes a QuotaExceededError, purge localStorage and retry
- Remove react-autobind dependency
- Remove usage of legacy `childContextTypes` API
- Refactor Field components to use arrow function syntax
- Replace react-autocomplete with Downshift in the autocomplete component
- Add 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
- _...Add new stuff here..._
### 🐞 Bug fixes
- Fix incorrect handing of network error response (#944)
- Show an error when adding a layer with a duplicate ID
- Replace deprecated `ReactDOM.render` usage with `createRoot` and drop the
`DOMNodeRemoved` cleanup hack
- _...Add new stuff here..._
## 2.1.1
+10 -8
View File
@@ -1,14 +1,16 @@
FROM golang:1.23-alpine AS builder
FROM node:18 as builder
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
COPY . .
RUN npm ci
RUN CGO_ENABLED=1 GOOS=linux npm run build-linux
RUN npx vite build
FROM alpine:latest
WORKDIR /app
COPY --from=builder /maputnik/desktop/bin/linux ./
ENTRYPOINT ["/app/maputnik"]
#---------------------------------------------------------------------------
# Create a clean nginx-alpine slim image with just the build results
FROM nginx:alpine-slim
COPY --from=builder /maputnik/dist /usr/share/nginx/html/
+2 -8
View File
@@ -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.
```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
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
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*:
+6 -6
View File
@@ -10,12 +10,12 @@
* https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
*/
import * as fs from "fs";
import * as fs from 'fs';
const changelogPath = "CHANGELOG.md";
let changelog = fs.readFileSync(changelogPath, "utf8");
changelog = changelog.replace("## main", `## ${process.argv[2]}`);
changelog = changelog.replaceAll("- _...Add new stuff here..._\n", "");
const changelogPath = 'CHANGELOG.md';
let changelog = fs.readFileSync(changelogPath, 'utf8');
changelog = changelog.replace('## main', `## ${process.argv[2]}`);
changelog = changelog.replaceAll('- _...Add new stuff here..._\n', '');
changelog = `## main
### ✨ Features and improvements
@@ -26,4 +26,4 @@ changelog = `## main
` + changelog;
fs.writeFileSync(changelogPath, changelog, "utf8");
fs.writeFileSync(changelogPath, changelog, 'utf8');
+7 -7
View File
@@ -3,10 +3,10 @@
// Copied from maplibre/maplibre-gl-js
// https://github.com/maplibre/maplibre-gl-js/blob/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build/release-notes.js
import * as fs from "fs";
import * as fs from 'fs';
const changelogPath = "CHANGELOG.md";
const changelog = fs.readFileSync(changelogPath, "utf8");
const changelogPath = 'CHANGELOG.md';
const changelog = fs.readFileSync(changelogPath, 'utf8');
/*
Parse the raw changelog text and split it into individual releases.
@@ -25,8 +25,8 @@ let match;
// eslint-disable-next-line no-cond-assign
while (match = regex.exec(changelog)) {
releaseNotes.push({
"version": match[1],
"changelog": match[2].trim(),
'version': match[1],
'changelog': match[2].trim(),
});
}
@@ -35,10 +35,10 @@ const previous = releaseNotes[1];
// Print the release notes template.
let header = "Changes since previous version";
let header = 'Changes since previous version'
if (previous) {
header = `https://github.com/maplibre/maputnik
[Changes](https://github.com/maplibre/maputnik/compare/v${previous.version}...v${latest.version}) since [Maputnik v${previous.version}](https://github.com/maplibre/maputnik/releases/tag/v${previous.version})`;
[Changes](https://github.com/maplibre/maputnik/compare/v${previous.version}...v${latest.version}) since [Maputnik v${previous.version}](https://github.com/maplibre/maputnik/releases/tag/v${previous.version})`
}
const templatedReleaseNotes = `${header}
-18
View File
@@ -8,34 +8,16 @@ export default defineConfig({
exclude: "cypress/**/*.*",
},
},
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
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;
},
baseUrl: "http://localhost:8888",
scrollBehavior: "center",
retries: {
runMode: 2,
openMode: 0,
},
},
component: {
devServer: {
framework: "react",
bundler: "vite",
},
},
});
+2 -2
View File
@@ -18,10 +18,10 @@ describe("accessibility", () => {
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";
then(get.elementByTestId(selector)).shouldExist();
then(get.elementByTestId("skip-target-layer-editor")).shouldExist();
when.tab().tab();
then(get.elementByTestId(selector)).shouldBeFocused();
when.click(selector);
-18
View File
@@ -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();
});
});
+1
View File
@@ -16,6 +16,7 @@ describe("history", () => {
it("undo/redo", () => {
when.setStyle("geojson");
when.modal.open();
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({ layers: [] });
when.modal.fillLayers({
id: "step 1",
-294
View File
@@ -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();
});
});
});
-541
View File
@@ -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();
});
});
});
+526
View File
@@ -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
View File
@@ -22,46 +22,11 @@ describe("map", () => {
"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", () => {
it("should exist", () => {
it('should exist', () => {
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();
});
});
});
-37
View File
@@ -1,6 +1,4 @@
/// <reference types="cypress-real-events" />
import { CypressHelper } from "@shellygo/cypress-test-utils";
import "cypress-real-events/support";
export default class MaputnikCypressHelper {
private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" });
@@ -10,45 +8,10 @@ export default class MaputnikCypressHelper {
};
public get = {
locationHash: (): Cypress.Chainable<string> => cy.location("hash"),
...this.helper.get,
};
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,
};
+26 -112
View File
@@ -8,10 +8,8 @@ const baseUrl = "http://localhost:8888/";
const styleFromWindow = (win: Window) => {
const styleId = win.localStorage.getItem("maputnik:latest_style");
const styleItemKey = `maputnik:style:${styleId}`;
const styleItem = win.localStorage.getItem(styleItemKey);
if (!styleItem) throw new Error("Could not get styleItem from localStorage");
const obj = JSON.parse(styleItem);
const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`);
const obj = JSON.parse(styleItem || "");
return obj;
};
@@ -78,34 +76,6 @@ export class MaputnikDriver {
fixture: "geojson-raster-style.json",
},
});
this.helper.given.interceptAndMockResponse({
method: "GET",
url: baseUrl + "rectangles-style.json",
response: {
fixture: "rectangles-style.json",
},
});
this.helper.given.interceptAndMockResponse({
method: "GET",
url: baseUrl + "example-style-with-fonts.json",
response: {
fixture: "example-style-with-fonts.json",
},
});
this.helper.given.interceptAndMockResponse({
method: "GET",
url: 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({
method: "GET",
url: "*example.local/*",
@@ -116,79 +86,52 @@ export class MaputnikDriver {
url: "*example.com/*",
response: [],
});
this.helper.given.interceptAndMockResponse({
method: "GET",
url: "https://www.glyph-server.com/*",
response: ["Font 1", "Font 2", "Font 3"],
});
},
};
public when = {
...this.helper.when,
modal: this.modalDriver.when,
doWithin: (selector: string, fn: () => void) => {
this.helper.when.doWithin(fn, selector);
within: (selector: string, fn: () => void) => {
this.helper.when.within(fn, selector);
},
tab: () => this.helper.get.element("body").tab(),
waitForExampleFileResponse: () => {
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: () => {
this.helper.given.fixture("example-style.json", "example-style.json");
this.helper.when.openFileByFixture("example-style.json", "modal:open.dropzone", "modal:open.file.input");
this.helper.when.wait(200);
},
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);
this.helper.get
.bySelector("type", "file")
.selectFile("cypress/fixtures/example-style.json", { force: true });
},
setStyle: (
styleProperties: "geojson" | "raster" | "both" | "layer" | "rectangles" | "font" | "zoom_7_center_0_51" | "",
styleProperties: "geojson" | "raster" | "both" | "layer" | "",
zoom?: number
) => {
const url = new URL(baseUrl);
let url = "?debug";
switch (styleProperties) {
case "geojson":
url.searchParams.set("style", baseUrl + "geojson-style.json");
break;
case "raster":
url.searchParams.set("style", baseUrl + "raster-style.json");
break;
case "both":
url.searchParams.set("style", baseUrl + "geojson-raster-style.json");
break;
case "layer":
url.searchParams.set("style", baseUrl + "example-layer-style.json");
break;
case "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;
case "geojson":
url += `&style=${baseUrl}geojson-style.json`;
break;
case "raster":
url += `&style=${baseUrl}raster-style.json`;
break;
case "both":
url += `&style=${baseUrl}geojson-raster-style.json`;
break;
case "layer":
url += `&style=${baseUrl}/example-layer-style.json`;
break;
}
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) {
this.helper.when.acceptConfirm();
}
// when methods should not include assertions
const toolbarLink = this.helper.get.elementByTestId("toolbar:link");
const toolbarLink = this.helper.get.elementByTestId("toolbar:link")
toolbarLink.scrollIntoView();
toolbarLink.should("be.visible");
},
@@ -200,7 +143,7 @@ export class MaputnikDriver {
},
selectWithin: (selector: string, value: string) => {
this.when.doWithin(selector, () => {
this.when.within(selector, () => {
this.helper.get.element("select").select(value);
});
},
@@ -219,35 +162,6 @@ export class MaputnikDriver {
.clear()
.type(text, { parseSpecialCharSequences: false });
},
setValueToPropertyArray: (selector: string, value: string) => {
this.when.doWithin(selector, () => {
this.helper.get.element(".maputnik-array-block-content input").last().type("{selectall}"+value, {force: true });
});
},
addValueToPropertyArray: (selector: string, value: string) => {
this.when.doWithin(selector, () => {
this.helper.get.element(".maputnik-array-add-value").click({ force: true });
this.helper.get.element(".maputnik-array-block-content input").last().type("{selectall}"+value, {force: true });
});
},
closePopup: () => {
this.helper.get.element(".maplibregl-popup-close-button").click();
},
collapseGroupInLayerEditor: (index = 0) => {
this.helper.get.element(".maputnik-layer-editor-group__button").eq(index).realClick();
},
appendTextInJsonEditor: (text: string) => {
this.helper.get.element(".cm-line").first().click().type(text, { parseSpecialCharSequences: false });
},
setTextInJsonEditor: (text: string) => {
this.helper.get.element(".cm-line").first().click().clear().type(text, { parseSpecialCharSequences: false });
}
};
public get = {
@@ -267,6 +181,6 @@ export class MaputnikDriver {
skipTargetLayerEditor: () =>
this.helper.get.elementByTestId("skip-target-layer-editor"),
canvas: () => this.helper.get.element("canvas"),
searchControl: () => this.helper.get.element(".maplibregl-ctrl-geocoder")
searchControl: () => this.helper.get.element('.maplibregl-ctrl-geocoder')
};
}
+3 -3
View File
@@ -7,7 +7,7 @@ export default class ModalDriver {
public when = {
fillLayers: (opts: { type: string; layer?: string; id?: string }) => {
// 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 layer = opts.layer;
let id;
@@ -20,8 +20,8 @@ export default class ModalDriver {
this.helper.when.type("add-layer.layer-id.input", id);
if (layer) {
this.helper.when.doWithin(() => {
this.helper.get.element("input").clear().type(layer!);
this.helper.when.within(() => {
this.helper.get.element("input").type(layer!);
}, "add-layer.layer-source-block");
}
this.helper.when.click("add-layer");
+7 -185
View File
@@ -18,14 +18,10 @@ describe("modals", () => {
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();
then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
});
it("upload via drag and drop", () => {
when.dropExampleFile();
then(get.fixture("example-style.json")).shouldEqualToStoredStyle();
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
});
describe("when click open url", () => {
@@ -175,22 +171,12 @@ describe("modals", () => {
});
it("sprite url", () => {
when.setTextInJsonEditor("\"http://example.com\"");
when.setValue("modal:settings.sprite", "http://example.com");
when.click("modal:settings.name");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
sprite: "http://example.com",
});
});
it("sprite object", () => {
when.setTextInJsonEditor(JSON.stringify([{ id: "1", url: "2" }]));
when.click("modal:settings.name");
then(get.styleFromLocalStorage()).shouldDeepNestedInclude({
sprite: [{ id: "1", url: "2" }],
});
});
it("glyphs url", () => {
const glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
when.setValue("modal:settings.glyphs", glyphsUrl);
@@ -238,41 +224,6 @@ describe("modals", () => {
).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", () => {
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");
@@ -288,12 +239,12 @@ 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");
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");
@@ -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", () => {
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();
});
});
});
@@ -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": []
}
+10 -112
View File
@@ -1,114 +1,12 @@
{
"id": "test-style",
"version": 8,
"name": "Test Style",
"metadata": {
"maputnik:renderer": "mlgljs",
"data": [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50,
51,
52,
53,
54,
55,
56,
57,
58,
59,
60,
61,
62,
63,
64,
65,
66,
67,
68,
69,
70,
71,
72,
73,
74,
75,
76,
77,
78,
79,
80,
81,
82,
83,
84,
85,
86,
87,
88,
89,
90,
91,
92,
93,
94,
95,
96,
97,
98,
99
]
},
"sources": {},
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": []
"id": "test-style",
"version": 8,
"name": "Test Style",
"metadata": {
"maputnik:renderer": "mlgljs"
},
"sources": {},
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": []
}
-92
View File
@@ -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
}
}
]
}
-12
View File
@@ -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>
-37
View File
@@ -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
View File
@@ -2,7 +2,8 @@ SOURCEDIR=.
SOURCES := $(shell find $(SOURCEDIR) -name '*.go')
BINARY=maputnik
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)
@@ -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)/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
sed 's/{{.Version}}/$(VERSION)/g' winres/winres_template.json > $@
+2 -1
View File
@@ -5,7 +5,8 @@ import (
"net/http"
"os"
"path/filepath"
"github.com/GeertJohan/go.rice"
"github.com/GeertJohan/go.rice"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/maputnik/desktop/filewatch"
+30 -39
View File
@@ -1,65 +1,56 @@
import eslint from "@eslint/js";
import {defineConfig} from "eslint/config";
import stylisticTs from "@stylistic/eslint-plugin";
import tseslint from "typescript-eslint";
import reactPlugin from "eslint-plugin-react";
import reactHooksPlugin from "eslint-plugin-react-hooks";
import reactRefreshPlugin from "eslint-plugin-react-refresh";
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
import reactRefreshPlugin from 'eslint-plugin-react-refresh';
export default defineConfig({
export default tseslint.config({
extends: [
eslint.configs.recommended,
tseslint.configs.recommended,
],
files: ["**/*.{js,jsx,ts,tsx}"],
files: ['**/*.{js,jsx,ts,tsx}'],
ignores: [
"dist/**/*",
],
languageOptions: {
ecmaVersion: 2024,
sourceType: "module",
sourceType: 'module',
globals: {
global: "readonly"
global: 'readonly'
}
},
settings: {
react: { version: "18.2" }
react: { version: '18.2' }
},
plugins: {
"react": reactPlugin,
"react-hooks": reactHooksPlugin,
"react-refresh": reactRefreshPlugin,
"@stylistic": stylisticTs
'react': reactPlugin,
'react-hooks': reactHooksPlugin,
'react-refresh': reactRefreshPlugin
},
rules: {
"react-refresh/only-export-components": [
"warn",
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true }
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
'@typescript-eslint/no-unused-vars': [
'warn',
{
varsIgnorePattern: "^_",
caughtErrors: "all",
caughtErrorsIgnorePattern: "^_",
argsIgnorePattern: "^_"
varsIgnorePattern: '^_',
caughtErrors: 'all',
caughtErrorsIgnorePattern: '^_',
argsIgnorePattern: '^_'
}
],
"no-unused-vars": "off",
"react/prop-types": "off",
"no-undef": "off",
"indent": "off",
"@stylistic/indent": ["error", 2],
"semi": "off",
"@stylistic/semi": ["error", "always"],
"quotes": "off",
"@stylistic/quotes": ["error", "double", { avoidEscape: true }],
"no-var": "error",
"@typescript-eslint/no-non-null-asserted-optional-chain": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-object-type": "off",
"@typescript-eslint/consistent-type-imports": ["error", { "fixStyle": "inline-type-imports" }],
'no-unused-vars': 'off',
'react/prop-types': 'off',
'no-undef': 'off',
'indent': ['error', 2],
'no-var': 'error',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
},
linterOptions: {
@@ -67,4 +58,4 @@ export default defineConfig({
noInlineConfig: false
}
}
);
)
+17
View File
@@ -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__';
}
}
-10
View File
@@ -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__"
}
});
+4877 -7387
View File
File diff suppressed because it is too large Load Diff
+65 -69
View File
@@ -1,19 +1,16 @@
{
"name": "maputnik",
"version": "3.1.0",
"version": "2.1.1",
"description": "A MapLibre GL visual style editor",
"type": "module",
"main": "''",
"scripts": {
"start": "vite",
"build": "tsc && vite build --mode=production",
"build-desktop": "tsc && vite build --mode=desktop && cd desktop && make",
"build-linux": "tsc && vite build --mode=desktop && cd desktop && make bin/linux/maputnik",
"i18n:extract": "npx i18next-cli extract",
"build": "tsc && vite build --base=/maputnik/",
"build-desktop": "tsc && vite build --base=/ && cd desktop && make",
"i18n:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'",
"lint": "eslint",
"test": "cypress run",
"test-unit": "vitest",
"test-unit-ci": "vitest run --coverage --reporter=json",
"cy:open": "cypress open",
"lint-css": "stylelint \"src/styles/*.scss\"",
"sort-styles": "jq 'sort_by(.id)' src/config/styles.json > tmp.json && mv tmp.json src/config/styles.json"
@@ -26,57 +23,55 @@
"license": "MIT",
"homepage": "https://github.com/maplibre/maputnik#readme",
"dependencies": {
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lint": "^6.9.7",
"@codemirror/state": "^6.7.0",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.43.4",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.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",
"@mapbox/mapbox-gl-rtl-text": "^0.3.0",
"@maplibre/maplibre-gl-geocoder": "^1.7.1",
"@maplibre/maplibre-gl-inspect": "^1.7.1",
"@maplibre/maplibre-gl-style-spec": "^23.1.0",
"@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1",
"@prantlf/jsonlint": "^16.0.0",
"array-move": "^4.0.0",
"buffer": "^6.0.3",
"classnames": "^2.5.1",
"codemirror": "^6.0.2",
"color": "^5.0.3",
"codemirror": "^5.65.18",
"color": "^5.0.0",
"detect-browser": "^5.3.0",
"downshift": "^9.4.0",
"events": "^3.3.0",
"file-saver": "^2.0.5",
"i18next": "^26.3.4",
"i18next-browser-languagedetector": "^8.2.1",
"i18next": "^24.2.2",
"i18next-browser-languagedetector": "^8.0.4",
"i18next-resources-to-backend": "^1.2.1",
"json-stringify-pretty-compact": "^4.0.0",
"json-to-ast": "^2.1.0",
"lodash": "^4.18.1",
"lodash": "^4.17.21",
"lodash.capitalize": "^4.2.1",
"lodash.clamp": "^4.0.3",
"lodash.clonedeep": "^4.5.0",
"lodash.get": "^4.4.2",
"lodash.isequal": "^4.5.0",
"lodash.throttle": "^4.1.1",
"maplibre-gl": "^5.24.0",
"maplibre-gl": "^5.1.0",
"maputnik-design": "github:maputnik/design#172b06c",
"ol": "^10.9.0",
"ol-mapbox-style": "^13.4.1",
"pmtiles": "^4.4.1",
"ol": "^10.4.0",
"ol-mapbox-style": "^12.4.0",
"pmtiles": "^4.1.0",
"prop-types": "^15.8.1",
"react": "^19.2.4",
"react-accessible-accordion": "^5.0.1",
"react-aria-menubutton": "^8.0.0",
"react": "^18.2.0",
"react-accessible-accordion": "^5.0.0",
"react-aria-menubutton": "^7.0.3",
"react-aria-modal": "^5.0.2",
"react-autobind": "^1.0.6",
"react-autocomplete": "^1.8.1",
"react-collapse": "^5.1.1",
"react-color": "^2.19.3",
"react-dom": "^19.2.4",
"react-i18next": "^17.0.7",
"react-icons": "^5.6.0",
"react-markdown": "^10.1.0",
"react-dom": "^18.2.0",
"react-file-reader-input": "^2.0.0",
"react-i18next": "^15.4.0",
"react-icon-base": "^2.1.2",
"react-icons": "^5.4.0",
"react-sortable-hoc": "^2.0.0",
"reconnecting-websocket": "^4.4.0",
"slugify": "^1.6.9",
"slugify": "^1.6.6",
"string-hash": "^1.1.3",
"url": "^0.11.4"
},
@@ -98,15 +93,14 @@
}
},
"devDependencies": {
"@cypress/code-coverage": "^4.0.3",
"@eslint/js": "^10.0.1",
"@cypress/code-coverage": "^3.13.11",
"@eslint/js": "^9.19.0",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@rollup/plugin-replace": "^6.0.3",
"@shellygo/cypress-test-utils": "^6.0.6",
"@stylistic/eslint-plugin": "^5.10.0",
"@types/codemirror": "^5.60.17",
"@types/color": "^4.2.1",
"@types/cors": "^2.8.19",
"@rollup/plugin-replace": "^6.0.2",
"@shellygo/cypress-test-utils": "^4.1.13",
"@types/codemirror": "^5.60.15",
"@types/color": "^4.2.0",
"@types/cors": "^2.8.17",
"@types/file-saver": "^2.0.7",
"@types/geojson": "^7946.0.16",
"@types/json-to-ast": "^2.1.4",
@@ -117,37 +111,39 @@
"@types/lodash.isequal": "^4.5.8",
"@types/lodash.throttle": "^4.1.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-modal": "^5.0.0",
"@types/react-autocomplete": "^1.8.11",
"@types/react-collapse": "^5.0.4",
"@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/wicg-file-system-access": "^2023.10.7",
"@vitejs/plugin-react": "^5.1.4",
"@vitest/coverage-v8": "^4.1.9",
"cors": "^2.8.6",
"cypress": "^15.18.0",
"cypress-plugin-tab": "^2.0.0",
"eslint": "^10.6.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"i18next-cli": "^1.53.2",
"@types/uuid": "^10.0.0",
"@types/wicg-file-system-access": "^2023.10.5",
"@vitejs/plugin-react": "^4.3.4",
"cors": "^2.8.5",
"cypress": "^14.0.3",
"cypress-plugin-tab": "^1.0.5",
"eslint": "^9.21.0",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"i18next-parser": "^9.3.0",
"istanbul": "^0.4.5",
"istanbul-lib-coverage": "^3.2.2",
"postcss": "^8.5.16",
"postcss": "^8.5.1",
"react-hot-loader": "^4.13.1",
"sass": "^1.101.0",
"stylelint": "^17.14.0",
"stylelint-config-recommended-scss": "^17.0.1",
"stylelint-scss": "^7.2.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.62.1",
"uuid": "^14.0.1",
"vite": "^7.3.2",
"vite-plugin-istanbul": "^9.0.1",
"vitest": "^4.1.9"
"sass": "^1.85.1",
"stylelint": "^16.14.1",
"stylelint-config-recommended-scss": "^14.1.0",
"stylelint-scss": "^6.11.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.25.0",
"uuid": "^11.1.0",
"vite": "^6.2.0",
"vite-plugin-istanbul": "^7.0.0"
}
}
+314 -292
View File
File diff suppressed because it is too large Load Diff
+29 -31
View File
@@ -1,52 +1,50 @@
import React from "react";
import ScrollContainer from "./ScrollContainer";
import { type WithTranslation, withTranslation } from "react-i18next";
import { IconContext } from "react-icons";
import React from 'react'
import PropTypes from 'prop-types'
import ScrollContainer from './ScrollContainer'
import { WithTranslation, withTranslation } from 'react-i18next';
type AppLayoutInternalProps = {
toolbar: React.ReactElement
layerList: React.ReactElement
layerEditor?: React.ReactElement
codeEditor?: React.ReactElement
map: React.ReactElement
bottom?: React.ReactElement
modals?: React.ReactNode
} & WithTranslation;
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
static childContextTypes = {
reactIconBase: PropTypes.object
}
getChildContext() {
return {
reactIconBase: { size: 14 }
}
}
render() {
document.body.dir = this.props.i18n.dir();
return <IconContext.Provider value={{size: "14px"}}>
<div className="maputnik-layout">
{this.props.toolbar}
<div className="maputnik-layout-main">
{this.props.codeEditor && <div className="maputnik-layout-code-editor">
<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}
return <div className="maputnik-layout">
{this.props.toolbar}
<div className="maputnik-layout-main">
<div className="maputnik-layout-list">
{this.props.layerList}
</div>
{this.props.bottom && <div className="maputnik-layout-bottom">
{this.props.bottom}
<div className="maputnik-layout-drawer">
<ScrollContainer>
{this.props.layerEditor}
</ScrollContainer>
</div>
}
{this.props.modals}
{this.props.map}
</div>
</IconContext.Provider>;
{this.props.bottom && <div className="maputnik-layout-bottom">
{this.props.bottom}
</div>
}
{this.props.modals}
</div>
}
}
+20 -19
View File
@@ -1,33 +1,34 @@
import React from "react";
import { formatLayerId } from "../libs/format";
import { type LayerSpecification, type StyleSpecification } from "maplibre-gl";
import { type WithTranslation, withTranslation } from "react-i18next";
import { type MappedError } from "../libs/definitions";
import React from 'react'
import {formatLayerId} from '../libs/format';
import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
type AppMessagePanelInternalProps = {
errors?: MappedError[]
errors?: unknown[]
infos?: string[]
mapStyle?: StyleSpecification
onLayerSelect?(index: number): void;
onLayerSelect?(...args: unknown[]): unknown
currentLayer?: LayerSpecification
selectedLayerIndex?: number
} & WithTranslation;
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
static defaultProps = {
onLayerSelect: () => { },
};
onLayerSelect: () => {},
}
render() {
const { t, selectedLayerIndex } = this.props;
const errors = this.props.errors?.map((error, idx) => {
const {t, selectedLayerIndex} = this.props;
const errors = this.props.errors?.map((error: any, idx) => {
let content;
if (error.parsed && error.parsed.type === "layer") {
const { parsed } = error;
const {parsed} = error;
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
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 &&
<>
&nbsp;&mdash;&nbsp;
@@ -45,19 +46,19 @@ class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalPro
else {
content = error.message;
}
return <p key={"error-" + idx} className="maputnik-message-panel-error">
return <p key={"error-"+idx} className="maputnik-message-panel-error">
{content}
</p>;
});
</p>
})
const infos = this.props.infos?.map((m, i) => {
return <p key={"info-" + i}>{m}</p>;
});
return <p key={"info-"+i}>{m}</p>
})
return <div className="maputnik-message-panel">
{errors}
{infos}
</div>;
</div>
}
}
+27 -38
View File
@@ -1,6 +1,6 @@
import React from "react";
import classnames from "classnames";
import {detect} from "detect-browser";
import React from 'react'
import classnames from 'classnames'
import {detect} from 'detect-browser';
import {
MdOpenInBrowser,
@@ -9,22 +9,18 @@ import {
MdHelpOutline,
MdFindInPage,
MdLanguage,
MdSave,
MdPublic,
MdCode
} from "react-icons/md";
import pkgJson from "../../package.json";
MdSave
} from 'react-icons/md'
import pkgJson from '../../package.json'
//@ts-ignore
import maputnikLogo from "maputnik-design/logos/logo-color.svg?inline";
import { withTranslation, type WithTranslation } from "react-i18next";
import { supportedLanguages } from "../i18n";
import type { OnStyleChangedCallback } from "../libs/definitions";
import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline'
import { withTranslation, WithTranslation } from 'react-i18next';
import { supportedLanguages } from '../i18n';
// This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of.
const browser = detect();
const colorAccessibilityFiltersEnabled = ["chrome", "firefox"].indexOf(browser!.name) > -1;
const colorAccessibilityFiltersEnabled = ['chrome', 'firefox'].indexOf(browser!.name) > -1;
export type ModalTypes = "settings" | "sources" | "open" | "shortcuts" | "export" | "debug" | "globalState" | "codeEditor";
type IconTextProps = {
children?: React.ReactNode
@@ -33,7 +29,7 @@ type IconTextProps = {
class IconText extends React.Component<IconTextProps> {
render() {
return <span className="maputnik-icon-text">{this.props.children}</span>;
return <span className="maputnik-icon-text">{this.props.children}</span>
}
}
@@ -41,19 +37,20 @@ type ToolbarLinkProps = {
className?: string
children?: React.ReactNode
href?: string
onToggleModal?(...args: unknown[]): unknown
};
class ToolbarLink extends React.Component<ToolbarLinkProps> {
render() {
return <a
className={classnames("maputnik-toolbar-link", this.props.className)}
className={classnames('maputnik-toolbar-link', this.props.className)}
href={this.props.href}
rel="noopener noreferrer"
target="_blank"
data-wd-key="toolbar:link"
>
{this.props.children}
</a>;
</a>
}
}
@@ -69,7 +66,7 @@ class ToolbarSelect extends React.Component<ToolbarSelectProps> {
data-wd-key={this.props.wdKey}
>
{this.props.children}
</div>;
</div>
}
}
@@ -87,7 +84,7 @@ class ToolbarAction extends React.Component<ToolbarActionProps> {
onClick={this.props.onClick}
>
{this.props.children}
</button>;
</button>
}
}
@@ -96,13 +93,13 @@ export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deut
type AppToolbarInternalProps = {
mapStyle: object
inspectModeEnabled: boolean
onStyleChanged: OnStyleChangedCallback
onStyleChanged(...args: unknown[]): unknown
// A new style has been uploaded
onStyleOpen: OnStyleChangedCallback
onStyleOpen(...args: unknown[]): unknown
// A dict of source id's and the available source layers
sources: object
children?: React.ReactNode
onToggleModal(modal: ModalTypes): void
onToggleModal(...args: unknown[]): unknown
onSetMapState(mapState: MapState): unknown
mapState?: MapState
renderer?: string
@@ -117,7 +114,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
add: false,
export: false,
}
};
}
handleSelection(val: MapState) {
this.props.onSetMapState(val);
@@ -135,7 +132,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
const el = document.querySelector("#skip-target-"+target) as HTMLButtonElement;
el.focus();
}
};
}
render() {
const t = this.props.t;
@@ -149,7 +146,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
id: "inspect",
group: "general",
title: t("Inspect"),
disabled: this.props.renderer === "ol",
disabled: this.props.renderer === 'ol',
},
{
id: "filter-deuteranopia",
@@ -222,30 +219,22 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
</a>
</div>
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
<ToolbarAction wdKey="nav:open" onClick={() => this.props.onToggleModal("open")}>
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
<MdOpenInBrowser />
<IconText>{t("Open")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:export" onClick={() => this.props.onToggleModal("export")}>
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
<MdSave />
<IconText>{t("Save")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:code-editor" onClick={() => this.props.onToggleModal("codeEditor")}>
<MdCode />
<IconText>{t("Code Editor")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:sources" onClick={() => this.props.onToggleModal("sources")}>
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
<MdLayers />
<IconText>{t("Data Sources")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:settings" onClick={() => this.props.onToggleModal("settings")}>
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
<MdSettings />
<IconText>{t("Style Settings")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:global-state" onClick={() => this.props.onToggleModal("globalState")}>
<MdPublic />
<IconText>{t("Global State")}</IconText>
</ToolbarAction>
<ToolbarSelect wdKey="nav:inspect">
<MdFindInPage />
@@ -302,7 +291,7 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
</ToolbarLink>
</div>
</div>
</nav>;
</nav>
}
}
+16 -16
View File
@@ -1,13 +1,14 @@
import React, {type CSSProperties, type PropsWithChildren, type SyntheticEvent} from "react";
import classnames from "classnames";
import FieldDocLabel from "./FieldDocLabel";
import Doc from "./Doc";
import React, {PropsWithChildren, SyntheticEvent} from 'react'
import classnames from 'classnames'
import FieldDocLabel from './FieldDocLabel'
import Doc from './Doc'
export type BlockProps = PropsWithChildren & {
type BlockProps = PropsWithChildren & {
"data-wd-key"?: string
label?: string
action?: React.ReactElement
style?: CSSProperties
style?: object
onChange?(...args: unknown[]): unknown
fieldSpec?: object
wideMode?: boolean
@@ -26,13 +27,13 @@ export default class Block extends React.Component<BlockProps, BlockState> {
super(props);
this.state = {
showDoc: false,
};
}
}
onChange(e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) {
const value = e.target.value;
const value = e.target.value
if (this.props.onChange) {
return this.props.onChange(value === "" ? undefined : value);
return this.props.onChange(value === "" ? undefined : value)
}
}
@@ -40,7 +41,7 @@ export default class Block extends React.Component<BlockProps, BlockState> {
this.setState({
showDoc: val
});
};
}
/**
* Some fields for example <InputColor/> bind click events inside the element
@@ -58,7 +59,7 @@ export default class Block extends React.Component<BlockProps, BlockState> {
if (event.nativeEvent.target.nodeName !== "A") {
event.preventDefault();
}
};
}
render() {
return <label style={this.props.style}
@@ -66,8 +67,7 @@ export default class Block extends React.Component<BlockProps, BlockState> {
className={classnames({
"maputnik-input-block": true,
"maputnik-input-block--wide": this.props.wideMode,
"maputnik-action-block": this.props.action,
"maputnik-input-block--error": this.props.error
"maputnik-action-block": this.props.action
})}
onClick={this.onLabelClick}
>
@@ -88,17 +88,17 @@ export default class Block extends React.Component<BlockProps, BlockState> {
<div className="maputnik-input-block-action">
{this.props.action}
</div>
<div className="maputnik-input-block-content" ref={el => {this._blockEl = el;}}>
<div className="maputnik-input-block-content" ref={el => this._blockEl = el}>
{this.props.children}
</div>
{this.props.fieldSpec &&
<div
className="maputnik-doc-inline"
style={{display: this.state.showDoc ? "" : "none"}}
style={{display: this.state.showDoc ? '' : 'none'}}
>
<Doc fieldSpec={this.props.fieldSpec} />
</div>
}
</label>;
</label>
}
}
-29
View File
@@ -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;
+6 -6
View File
@@ -1,6 +1,6 @@
import React from "react";
import { Collapse as ReactCollapse } from "react-collapse";
import {reducedMotionEnabled} from "../libs/accessibility";
import React from 'react'
import { Collapse as ReactCollapse } from 'react-collapse'
import {reducedMotionEnabled} from '../libs/accessibility'
type CollapseProps = {
@@ -12,7 +12,7 @@ type CollapseProps = {
export default class Collapse extends React.Component<CollapseProps> {
static defaultProps = {
isActive: true
};
}
render() {
if (reducedMotionEnabled()) {
@@ -20,14 +20,14 @@ export default class Collapse extends React.Component<CollapseProps> {
<div style={{display: this.props.isActive ? "block" : "none"}}>
{this.props.children}
</div>
);
)
}
else {
return (
<ReactCollapse isOpened={this.props.isActive}>
{this.props.children}
</ReactCollapse>
);
)
}
}
}
+4 -4
View File
@@ -1,5 +1,5 @@
import React from "react";
import {MdArrowDropDown, MdArrowDropUp} from "react-icons/md";
import React from 'react'
import {MdArrowDropDown, MdArrowDropUp} from 'react-icons/md'
type CollapserProps = {
isCollapsed: boolean
@@ -12,7 +12,7 @@ export default class Collapser extends React.Component<CollapserProps> {
width: 20,
height: 20,
...this.props.style,
};
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />;
}
return this.props.isCollapsed ? <MdArrowDropUp style={iconStyle}/> : <MdArrowDropDown style={iconStyle} />
}
}
+7 -19
View File
@@ -1,10 +1,10 @@
import React from "react";
import Markdown from "react-markdown";
import React from 'react'
const headers = {
js: "JS",
android: "Android",
ios: "iOS"
ios: "iOS",
macos: "macOS",
};
type DocProps = {
@@ -15,7 +15,7 @@ type DocProps = {
doc?: string
}
}
"sdk-support"?: {
'sdk-support'?: {
[key: string]: typeof headers
}
docUrl?: string,
@@ -28,7 +28,7 @@ export default class Doc extends React.Component<DocProps> {
const {fieldSpec} = this.props;
const {doc, values, docUrl, docUrlLinkText} = fieldSpec;
const sdkSupport = fieldSpec["sdk-support"];
const sdkSupport = fieldSpec['sdk-support'];
const renderValues = (
!!values &&
@@ -37,23 +37,11 @@ export default class Doc extends React.Component<DocProps> {
!Array.isArray(values)
);
const sdkSupportToJsx = (value: string) => {
const supportValue = value.toLowerCase();
if (supportValue.startsWith("https://")) {
return <a href={supportValue} target="_blank" rel="noreferrer">{"#" + supportValue.split("/").pop()}</a>;
}
return value;
};
return (
<>
{doc &&
<div className="SpecDoc">
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'>
<Markdown components={{
a: ({node: _node, href, children, ...props}) => <a href={href} target="_blank" {...props}>{children}</a>,
}}>{doc}</Markdown>
</div>
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'>{doc}</div>
{renderValues &&
<ul className="SpecDoc__values">
{Object.entries(values).map(([key, value]) => {
@@ -86,7 +74,7 @@ export default class Doc extends React.Component<DocProps> {
<td>{key}</td>
{Object.keys(headers).map((k) => {
if (Object.prototype.hasOwnProperty.call(supportObj, k)) {
return <td key={k}>{sdkSupportToJsx(supportObj[k as keyof typeof headers])}</td>;
return <td key={k}>{supportObj[k as keyof typeof headers]}</td>;
}
else {
return <td key={k}>no</td>;
+9 -10
View File
@@ -1,5 +1,6 @@
import InputArray, { type InputArrayProps } from "./InputArray";
import Fieldset from "./Fieldset";
import React from 'react'
import InputArray, { FieldArrayProps as InputArrayProps } from './InputArray'
import Fieldset from './Fieldset'
type FieldArrayProps = InputArrayProps & {
name?: string
@@ -8,12 +9,10 @@ type FieldArrayProps = InputArrayProps & {
}
};
const FieldArray: React.FC<FieldArrayProps> = (props) => {
return (
<Fieldset label={props.label} fieldSpec={props.fieldSpec}>
<InputArray {...props} />
export default class FieldArray extends React.Component<FieldArrayProps> {
render() {
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputArray {...this.props} />
</Fieldset>
);
};
export default FieldArray;
}
}
+9 -10
View File
@@ -1,5 +1,6 @@
import Block from "./Block";
import InputAutocomplete, { type InputAutocompleteProps } from "./InputAutocomplete";
import React from 'react'
import Block from './Block'
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
type FieldAutocompleteProps = InputAutocompleteProps & {
@@ -7,12 +8,10 @@ type FieldAutocompleteProps = InputAutocompleteProps & {
};
const FieldAutocomplete: React.FC<FieldAutocompleteProps> = (props) => {
return (
<Block label={props.label}>
<InputAutocomplete {...props} />
export default class FieldAutocomplete extends React.Component<FieldAutocompleteProps> {
render() {
return <Block label={this.props.label}>
<InputAutocomplete {...this.props} />
</Block>
);
};
export default FieldAutocomplete;
}
}
+9 -10
View File
@@ -1,5 +1,6 @@
import Block from "./Block";
import InputCheckbox, {type InputCheckboxProps} from "./InputCheckbox";
import React from 'react'
import Block from './Block'
import InputCheckbox, {InputCheckboxProps} from './InputCheckbox'
type FieldCheckboxProps = InputCheckboxProps & {
@@ -7,12 +8,10 @@ type FieldCheckboxProps = InputCheckboxProps & {
};
const FieldCheckbox: React.FC<FieldCheckboxProps> = (props) => {
return (
<Block label={props.label}>
<InputCheckbox {...props} />
export default class FieldCheckbox extends React.Component<FieldCheckboxProps> {
render() {
return <Block label={this.props.label}>
<InputCheckbox {...this.props} />
</Block>
);
};
export default FieldCheckbox;
}
}
+9 -10
View File
@@ -1,5 +1,6 @@
import Block from "./Block";
import InputColor, {type InputColorProps} from "./InputColor";
import React from 'react'
import Block from './Block'
import InputColor, {InputColorProps} from './InputColor'
type FieldColorProps = InputColorProps & {
@@ -10,12 +11,10 @@ type FieldColorProps = InputColorProps & {
};
const FieldColor: React.FC<FieldColorProps> = (props) => {
return (
<Block label={props.label} fieldSpec={props.fieldSpec}>
<InputColor {...props} />
export default class FieldColor extends React.Component<FieldColorProps> {
render() {
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputColor {...this.props} />
</Block>
);
};
export default FieldColor;
}
}
+16 -18
View File
@@ -1,8 +1,8 @@
import React from "react";
import React from 'react'
import Block from "./Block";
import InputString from "./InputString";
import { type WithTranslation, withTranslation } from "react-i18next";
import Block from './Block'
import InputString from './InputString'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldCommentInternalProps = {
value?: string
@@ -10,31 +10,29 @@ type FieldCommentInternalProps = {
error: {message: string}
} & WithTranslation;
const FieldCommentInternal: React.FC<FieldCommentInternalProps> = (props) => {
const t = props.t;
const fieldSpec = {
doc: t(
"Comments for the current layer. This is non-standard and not in the spec."
),
};
class FieldCommentInternal extends React.Component<FieldCommentInternalProps> {
render() {
const t = this.props.t;
const fieldSpec = {
doc: t("Comments for the current layer. This is non-standard and not in the spec."),
};
return (
<Block
return <Block
label={t("Comments")}
fieldSpec={fieldSpec}
data-wd-key="layer-comment"
error={props.error}
error={this.props.error}
>
<InputString
multi={true}
value={props.value}
onChange={props.onChange}
value={this.props.value}
onChange={this.props.onChange}
default={t("Comment...")}
data-wd-key="layer-comment.input"
/>
</Block>
);
};
}
}
const FieldComment = withTranslation()(FieldCommentInternal);
export default FieldComment;
+43 -31
View File
@@ -1,5 +1,5 @@
import React, { type JSX } from "react";
import {MdInfoOutline, MdHighlightOff} from "react-icons/md";
import React from 'react'
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
type FieldDocLabelProps = {
label: JSX.Element | string | undefined
@@ -9,45 +9,57 @@ type FieldDocLabelProps = {
onToggleDoc?(...args: unknown[]): unknown
};
type FieldDocLabelState = {
open: boolean
};
const FieldDocLabel: React.FC<FieldDocLabelProps> = (props) => {
const [open, setOpen] = React.useState(false);
const onToggleDoc = (state: boolean) => {
setOpen(state);
if (props.onToggleDoc) {
props.onToggleDoc(state);
export default class FieldDocLabel extends React.Component<FieldDocLabelProps, FieldDocLabelState> {
constructor (props: FieldDocLabelProps) {
super(props);
this.state = {
open: false,
}
};
}
const { label, fieldSpec } = props;
const { doc } = fieldSpec || {};
onToggleDoc = (open: boolean) => {
this.setState({
open,
}, () => {
if (this.props.onToggleDoc) {
this.props.onToggleDoc(this.state.open);
}
});
}
if (doc) {
return (
<label className="maputnik-doc-wrapper">
render() {
const {label, fieldSpec} = this.props;
const {doc} = fieldSpec || {};
if (doc) {
return <label className="maputnik-doc-wrapper">
<div className="maputnik-doc-target">
{label}
{"\xa0"}
{'\xa0'}
<button
aria-label={open ? "close property documentation" : "open property documentation"}
className={`maputnik-doc-button maputnik-doc-button--${open ? "open" : "closed"}`}
onClick={() => onToggleDoc(!open)}
data-wd-key={"field-doc-button-" + label}
aria-label={this.state.open ? "close property documentation" : "open property documentation"}
className={`maputnik-doc-button maputnik-doc-button--${this.state.open ? 'open' : 'closed'}`}
onClick={() => this.onToggleDoc(!this.state.open)}
data-wd-key={'field-doc-button-'+label}
>
{open ? <MdHighlightOff /> : <MdInfoOutline />}
{this.state.open ? <MdHighlightOff /> : <MdInfoOutline />}
</button>
</div>
</label>
);
} else if (label) {
return (
<label className="maputnik-doc-wrapper">
<div className="maputnik-doc-target">{label}</div>
}
else if (label) {
return <label className="maputnik-doc-wrapper">
<div className="maputnik-doc-target">
{label}
</div>
</label>
);
}
else {
<div />
}
}
return <div />;
};
export default FieldDocLabel;
}
+9 -10
View File
@@ -1,16 +1,15 @@
import InputDynamicArray, {type InputDynamicArrayProps} from "./InputDynamicArray";
import Fieldset from "./Fieldset";
import React from 'react'
import InputDynamicArray, {FieldDynamicArrayProps as InputDynamicArrayProps} from './InputDynamicArray'
import Fieldset from './Fieldset'
type FieldDynamicArrayProps = InputDynamicArrayProps & {
name?: string
};
const FieldDynamicArray: React.FC<FieldDynamicArrayProps> = (props) => {
return (
<Fieldset label={props.label}>
<InputDynamicArray {...props} />
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
render() {
return <Fieldset label={this.props.label}>
<InputDynamicArray {...this.props} />
</Fieldset>
);
};
export default FieldDynamicArray;
}
}
+9 -10
View File
@@ -1,5 +1,6 @@
import InputEnum, {type InputEnumProps} from "./InputEnum";
import Fieldset from "./Fieldset";
import React from 'react'
import InputEnum, {InputEnumProps} from './InputEnum'
import Fieldset from './Fieldset';
type FieldEnumProps = InputEnumProps & {
@@ -10,12 +11,10 @@ type FieldEnumProps = InputEnumProps & {
};
const FieldEnum: React.FC<FieldEnumProps> = (props) => {
return (
<Fieldset label={props.label} fieldSpec={props.fieldSpec}>
<InputEnum {...props} />
export default class FieldEnum extends React.Component<FieldEnumProps> {
render() {
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputEnum {...this.props} />
</Fieldset>
);
};
export default FieldEnum;
}
}
+231 -231
View File
@@ -1,12 +1,11 @@
import React from "react";
import React from 'react'
import SpecProperty from "./_SpecProperty";
import DataProperty, { type Stop } from "./_DataProperty";
import ZoomProperty from "./_ZoomProperty";
import ExpressionProperty from "./_ExpressionProperty";
import {function as styleFunction} from "@maplibre/maplibre-gl-style-spec";
import {findDefaultFromSpec} from "../libs/spec-helper";
import { type MappedLayerErrors } from "../libs/definitions";
import SpecProperty from './_SpecProperty'
import DataProperty, { Stop } from './_DataProperty'
import ZoomProperty from './_ZoomProperty'
import ExpressionProperty from './_ExpressionProperty'
import {function as styleFunction} from '@maplibre/maplibre-gl-style-spec';
import {findDefaultFromSpec} from '../libs/spec-helper';
function isLiteralExpression(value: any) {
@@ -23,9 +22,9 @@ function isGetExpression(value: any) {
function isZoomField(value: any) {
return (
typeof(value) === "object" &&
typeof(value) === 'object' &&
value.stops &&
typeof(value.property) === "undefined" &&
typeof(value.property) === 'undefined' &&
Array.isArray(value.stops) &&
value.stops.length > 1 &&
value.stops.every((stop: Stop) => {
@@ -39,7 +38,7 @@ function isZoomField(value: any) {
function isIdentityProperty(value: any) {
return (
typeof(value) === "object" &&
typeof(value) === 'object' &&
value.type === "identity" &&
Object.prototype.hasOwnProperty.call(value, "property")
);
@@ -47,16 +46,16 @@ function isIdentityProperty(value: any) {
function isDataStopProperty(value: any) {
return (
typeof(value) === "object" &&
typeof(value) === 'object' &&
value.stops &&
typeof(value.property) !== "undefined" &&
typeof(value.property) !== 'undefined' &&
value.stops.length > 1 &&
Array.isArray(value.stops) &&
value.stops.every((stop: Stop) => {
return (
Array.isArray(stop) &&
stop.length === 2 &&
typeof(stop[0]) === "object"
typeof(stop[0]) === 'object'
);
})
);
@@ -91,18 +90,6 @@ function getDataType(value: any, fieldSpec={} as any) {
else if (fieldSpec.type === "array" && isArrayOfPrimatives(value)) {
return "value";
}
else if (fieldSpec.type === "numberArray" && isArrayOfPrimatives(value)) {
return "value";
}
else if (fieldSpec.type === "colorArray") {
return "value";
}
else if (fieldSpec.type === "padding") {
return "value";
}
else if (fieldSpec.type === "variableAnchorOffsetCollection") {
return "value";
}
else if (isZoomField(value)) {
return "zoom_function";
}
@@ -120,287 +107,300 @@ type FieldFunctionProps = {
fieldName: string
fieldType: string
fieldSpec: any
errors?: MappedLayerErrors
errors?: {[key: string]: {message: string}}
value?: any
};
type FieldFunctionState = {
dataType: string
isEditing: boolean
}
/** Supports displaying spec field for zoom function objects
* https://www.mapbox.com/mapbox-gl-style-spec/#types-function-zoom-property
*/
const FieldFunction: React.FC<FieldFunctionProps> = (props) => {
const [dataType, setDataType] = React.useState(
getDataType(props.value, props.fieldSpec)
);
const [isEditing, setIsEditing] = React.useState(false);
React.useEffect(() => {
if (!isEditing) {
setDataType(getDataType(props.value, props.fieldSpec));
export default class FieldFunction extends React.Component<FieldFunctionProps, FieldFunctionState> {
constructor (props: FieldFunctionProps) {
super(props);
this.state = {
dataType: getDataType(props.value, props.fieldSpec),
isEditing: false,
}
}, [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) {
return "exponential";
return "exponential"
}
if (fieldSpec.type === "number") {
return "interval";
return "interval"
}
return "categorical";
};
return "categorical"
}
const addStop = () => {
const stops = props.value.stops.slice(0);
const lastStop = stops[stops.length - 1];
addStop = () => {
const stops = this.props.value.stops.slice(0)
const lastStop = stops[stops.length - 1]
if (typeof lastStop[0] === "object") {
stops.push([
{ zoom: lastStop[0].zoom + 1, value: lastStop[0].value },
lastStop[1],
]);
} else {
stops.push([lastStop[0] + 1, lastStop[1]]);
{zoom: lastStop[0].zoom + 1, value: lastStop[0].value},
lastStop[1]
])
}
else {
stops.push([lastStop[0] + 1, lastStop[1]])
}
const changedValue = {
...props.value,
...this.props.value,
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 = () => {
const { value } = props;
deleteExpression = () => {
const {fieldSpec, fieldName} = this.props;
this.props.onChange(fieldName, fieldSpec.default);
this.setState({
dataType: "value",
});
}
let zoomFunc: any;
if (typeof value === "object") {
deleteStop = (stopIdx: number) => {
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) {
zoomFunc = {
base: value.base,
stops: value.stops.map((stop: Stop) => {
return [stop[0].zoom, stop[1] || findDefaultFromSpec(props.fieldSpec)];
}),
};
} else {
return [stop[0].zoom, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
})
}
}
else {
zoomFunc = {
base: value.base,
stops: [
[6, findDefaultFromSpec(props.fieldSpec)],
[10, findDefaultFromSpec(props.fieldSpec)],
],
};
[6, findDefaultFromSpec(this.props.fieldSpec)],
[10, findDefaultFromSpec(this.props.fieldSpec)]
]
}
}
} else {
}
else {
zoomFunc = {
stops: [
[6, value || findDefaultFromSpec(props.fieldSpec)],
[10, value || findDefaultFromSpec(props.fieldSpec)],
],
};
[6, value || findDefaultFromSpec(this.props.fieldSpec)],
[10, value || findDefaultFromSpec(this.props.fieldSpec)]
]
}
}
props.onChange(props.fieldName, zoomFunc);
};
this.props.onChange(this.props.fieldName, zoomFunc)
}
const undoExpression = () => {
const { value, fieldName } = props;
undoExpression = () => {
const {value, fieldName} = this.props;
if (isGetExpression(value)) {
props.onChange(fieldName, {
type: "identity",
property: value[1],
this.props.onChange(fieldName, {
"type": "identity",
"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 = () => {
const { value, fieldSpec } = props;
canUndo = () => {
const {value, fieldSpec} = this.props;
return (
isGetExpression(value) ||
isLiteralExpression(value) ||
isPrimative(value) ||
(Array.isArray(value) && fieldSpec.type === "array")
);
};
}
const makeExpression = () => {
const { value, fieldSpec } = props;
makeExpression = () => {
const {value, fieldSpec} = this.props;
let expression;
if (typeof value === "object" && "stops" in value) {
if (typeof(value) === "object" && 'stops' in value) {
expression = styleFunction.convertFunction(value, fieldSpec);
} else if (isIdentityProperty(value)) {
expression = ["get", value.property];
} 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 = () => {
const functionType = getFieldFunctionType(props.fieldSpec);
const stopValue = functionType === "categorical" ? "" : 0;
const { value } = props;
makeDataFunction = () => {
const functionType = this.getFieldFunctionType(this.props.fieldSpec);
const stopValue = functionType === 'categorical' ? '' : 0;
const {value} = this.props;
let dataFunc;
if (typeof value === "object") {
if (typeof(value) === "object") {
if (value.stops) {
dataFunc = {
property: "",
type: functionType,
base: value.base,
stops: value.stops.map((stop: Stop) => {
return [{ zoom: stop[0], value: stopValue }, stop[1] || findDefaultFromSpec(props.fieldSpec)];
}),
};
} else {
return [{zoom: stop[0], value: stopValue}, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
})
}
}
else {
dataFunc = {
property: "",
type: functionType,
base: value.base,
stops: [
[{ zoom: 6, value: stopValue }, findDefaultFromSpec(props.fieldSpec)],
[{ zoom: 10, value: stopValue }, findDefaultFromSpec(props.fieldSpec)],
],
};
[{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)],
[{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)]
]
}
}
} else {
}
else {
dataFunc = {
property: "",
type: functionType,
base: value.base,
stops: [
[{ zoom: 6, value: stopValue }, props.value || findDefaultFromSpec(props.fieldSpec)],
[{ zoom: 10, value: stopValue }, props.value || findDefaultFromSpec(props.fieldSpec)],
],
};
[{zoom: 6, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)],
[{zoom: 10, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)]
]
}
}
props.onChange(props.fieldName, dataFunc);
};
const makeElevationFunction = () => {
const expression = [
"interpolate",
["linear"],
["elevation"],
0,
"black",
2000,
"white"
];
props.onChange(props.fieldName, expression);
};
const onMarkEditing = () => {
setIsEditing(true);
};
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}
/>
);
this.props.onChange(this.props.fieldName, dataFunc)
}
return (
<div className={propClass} data-wd-key={"spec-field-container:" + props.fieldName}>
onMarkEditing = () => {
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}
</div>
);
};
export default FieldFunction;
}
}
+15 -15
View File
@@ -1,7 +1,8 @@
import React from 'react'
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import Block from "./Block";
import InputString from "./InputString";
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputString from './InputString'
type FieldIdProps = {
value: string
@@ -10,19 +11,18 @@ type FieldIdProps = {
error?: {message: string}
};
const FieldId: React.FC<FieldIdProps> = (props) => {
return (
<Block label="ID" fieldSpec={latest.layer.id}
data-wd-key={props.wdKey}
error={props.error}
export default class FieldId extends React.Component<FieldIdProps> {
render() {
return <Block label="ID" fieldSpec={latest.layer.id}
data-wd-key={this.props.wdKey}
error={this.props.error}
>
<InputString
value={props.value}
onInput={props.onChange}
data-wd-key={props.wdKey + ".input"}
value={this.props.value}
onInput={this.props.onChange}
data-wd-key={this.props.wdKey + ".input"}
/>
</Block>
);
};
export default FieldId;
}
}
+7 -6
View File
@@ -1,11 +1,12 @@
import InputJson, {type InputJsonProps} from "./InputJson";
import React from 'react'
import InputJson, {InputJsonProps} from './InputJson'
type FieldJsonProps = InputJsonProps & {};
const FieldJson: React.FC<FieldJsonProps> = (props) => {
return <InputJson {...props} />;
};
export default FieldJson;
export default class FieldJson extends React.Component<FieldJsonProps> {
render() {
return <InputJson {...this.props} />
}
}
+14 -14
View File
@@ -1,9 +1,9 @@
import React from "react";
import React from 'react'
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import Block from "./Block";
import InputNumber from "./InputNumber";
import { type WithTranslation, withTranslation } from "react-i18next";
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputNumber from './InputNumber'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldMaxZoomInternalProps = {
value?: number
@@ -11,25 +11,25 @@ type FieldMaxZoomInternalProps = {
error?: {message: string}
} & WithTranslation;
const FieldMaxZoomInternal: React.FC<FieldMaxZoomInternalProps> = (props) => {
const t = props.t;
return (
<Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom}
error={props.error}
class FieldMaxZoomInternal extends React.Component<FieldMaxZoomInternalProps> {
render() {
const t = this.props.t;
return <Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom}
error={this.props.error}
data-wd-key="max-zoom"
>
<InputNumber
allowRange={true}
value={props.value}
onChange={props.onChange}
value={this.props.value}
onChange={this.props.onChange}
min={latest.layer.maxzoom.minimum}
max={latest.layer.maxzoom.maximum}
default={latest.layer.maxzoom.maximum}
data-wd-key="max-zoom.input"
/>
</Block>
);
};
}
}
const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal);
export default FieldMaxZoom;
+14 -14
View File
@@ -1,9 +1,9 @@
import React from "react";
import React from 'react'
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import Block from "./Block";
import InputNumber from "./InputNumber";
import { type WithTranslation, withTranslation } from "react-i18next";
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputNumber from './InputNumber'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldMinZoomInternalProps = {
value?: number
@@ -11,25 +11,25 @@ type FieldMinZoomInternalProps = {
error?: {message: string}
} & WithTranslation;
const FieldMinZoomInternal: React.FC<FieldMinZoomInternalProps> = (props) => {
const t = props.t;
return (
<Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom}
error={props.error}
class FieldMinZoomInternal extends React.Component<FieldMinZoomInternalProps> {
render() {
const t = this.props.t;
return <Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom}
error={this.props.error}
data-wd-key="min-zoom"
>
<InputNumber
allowRange={true}
value={props.value}
onChange={props.onChange}
value={this.props.value}
onChange={this.props.onChange}
min={latest.layer.minzoom.minimum}
max={latest.layer.minzoom.maximum}
default={latest.layer.minzoom.minimum}
data-wd-key='min-zoom.input'
/>
</Block>
);
};
}
}
const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
export default FieldMinZoom;
+9 -10
View File
@@ -1,5 +1,6 @@
import InputMultiInput, {type InputMultiInputProps} from "./InputMultiInput";
import Fieldset from "./Fieldset";
import React from 'react'
import InputMultiInput, {InputMultiInputProps} from './InputMultiInput'
import Fieldset from './Fieldset'
type FieldMultiInputProps = InputMultiInputProps & {
@@ -7,12 +8,10 @@ type FieldMultiInputProps = InputMultiInputProps & {
};
const FieldMultiInput: React.FC<FieldMultiInputProps> = (props) => {
return (
<Fieldset label={props.label}>
<InputMultiInput {...props} />
export default class FieldMultiInput extends React.Component<FieldMultiInputProps> {
render() {
return <Fieldset label={this.props.label}>
<InputMultiInput {...this.props} />
</Fieldset>
);
};
export default FieldMultiInput;
}
}
+9 -10
View File
@@ -1,5 +1,6 @@
import InputNumber, {type InputNumberProps} from "./InputNumber";
import Block from "./Block";
import React from 'react'
import InputNumber, {InputNumberProps} from './InputNumber'
import Block from './Block'
type FieldNumberProps = InputNumberProps & {
@@ -10,12 +11,10 @@ type FieldNumberProps = InputNumberProps & {
};
const FieldNumber: React.FC<FieldNumberProps> = (props) => {
return (
<Block label={props.label} fieldSpec={props.fieldSpec}>
<InputNumber {...props} />
export default class FieldNumber extends React.Component<FieldNumberProps> {
render() {
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputNumber {...this.props} />
</Block>
);
};
export default FieldNumber;
}
}
+9 -10
View File
@@ -1,5 +1,6 @@
import Block from "./Block";
import InputSelect, {type InputSelectProps} from "./InputSelect";
import React from 'react'
import Block from './Block'
import InputSelect, {InputSelectProps} from './InputSelect'
type FieldSelectProps = InputSelectProps & {
@@ -10,12 +11,10 @@ type FieldSelectProps = InputSelectProps & {
};
const FieldSelect: React.FC<FieldSelectProps> = (props) => {
return (
<Block label={props.label} fieldSpec={props.fieldSpec}>
<InputSelect {...props} />
export default class FieldSelect extends React.Component<FieldSelectProps> {
render() {
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputSelect {...this.props}/>
</Block>
);
};
export default FieldSelect;
}
}
+21 -23
View File
@@ -1,9 +1,9 @@
import React from "react";
import React from 'react'
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import Block from "./Block";
import InputAutocomplete from "./InputAutocomplete";
import { type WithTranslation, withTranslation } from "react-i18next";
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputAutocomplete from './InputAutocomplete'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldSourceInternalProps = {
value?: string
@@ -13,30 +13,28 @@ type FieldSourceInternalProps = {
error?: {message: string}
} & WithTranslation;
const FieldSourceInternal: React.FC<FieldSourceInternalProps> = ({
onChange = () => {},
sourceIds = [],
wdKey,
value,
error,
t
}) => {
return (
<Block
class FieldSourceInternal extends React.Component<FieldSourceInternalProps> {
static defaultProps = {
onChange: () => {},
sourceIds: [],
}
render() {
const t = this.props.t;
return <Block
label={t("Source")}
fieldSpec={latest.layer.source}
error={error}
data-wd-key={wdKey}
error={this.props.error}
data-wd-key={this.props.wdKey}
>
<InputAutocomplete
value={value}
onChange={onChange}
options={sourceIds?.map((src) => [src, src])}
value={this.props.value}
onChange={this.props.onChange}
options={this.props.sourceIds?.map(src => [src, src])}
/>
</Block>
);
};
}
}
const FieldSource = withTranslation()(FieldSourceInternal);
export default FieldSource;
+24 -21
View File
@@ -1,39 +1,42 @@
import React from "react";
import React from 'react'
import {latest} from "@maplibre/maplibre-gl-style-spec";
import Block from "./Block";
import InputAutocomplete from "./InputAutocomplete";
import { type WithTranslation, withTranslation } from "react-i18next";
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputAutocomplete from './InputAutocomplete'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldSourceLayerInternalProps = {
value?: string
onChange?(...args: unknown[]): unknown
sourceLayerIds?: unknown[]
isFixed?: boolean
error?: {message: string}
} & WithTranslation;
const FieldSourceLayerInternal: React.FC<FieldSourceLayerInternalProps> = ({
onChange = () => {},
sourceLayerIds = [],
value,
error,
t
}) => {
return (
<Block
class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalProps> {
static defaultProps = {
onChange: () => {},
sourceLayerIds: [],
isFixed: false
}
render() {
const t = this.props.t;
return <Block
label={t("Source Layer")}
fieldSpec={latest.layer["source-layer"]}
fieldSpec={latest.layer['source-layer']}
data-wd-key="layer-source-layer"
error={error}
error={this.props.error}
>
<InputAutocomplete
value={value}
onChange={onChange}
options={sourceLayerIds?.map((l) => [l, l])}
keepMenuWithinWindowBounds={!!this.props.isFixed}
value={this.props.value}
onChange={this.props.onChange}
options={this.props.sourceLayerIds?.map(l => [l, l])}
/>
</Block>
);
};
}
}
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
export default FieldSourceLayer;
-49
View File
@@ -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;
+9 -10
View File
@@ -1,5 +1,6 @@
import Block from "./Block";
import InputString, {type InputStringProps} from "./InputString";
import React from 'react'
import Block from './Block'
import InputString, {InputStringProps} from './InputString'
type FieldStringProps = InputStringProps & {
name?: string
@@ -9,12 +10,10 @@ type FieldStringProps = InputStringProps & {
}
};
const FieldString: React.FC<FieldStringProps> = (props) => {
return (
<Block label={props.label} fieldSpec={props.fieldSpec}>
<InputString {...props} />
export default class FieldString extends React.Component<FieldStringProps> {
render() {
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputString {...this.props} />
</Block>
);
};
export default FieldString;
}
}
+42 -32
View File
@@ -1,10 +1,10 @@
import React from "react";
import {v8} from "@maplibre/maplibre-gl-style-spec";
import Block from "./Block";
import InputSelect from "./InputSelect";
import InputString from "./InputString";
import { type WithTranslation, withTranslation } from "react-i18next";
import { startCase } from "lodash";
import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputSelect from './InputSelect'
import InputString from './InputString'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldTypeInternalProps = {
value: string
@@ -14,34 +14,44 @@ type FieldTypeInternalProps = {
disabled?: boolean
} & WithTranslation;
const FieldTypeInternal: React.FC<FieldTypeInternalProps> = ({
t,
value,
wdKey,
onChange,
error,
disabled = false
}) => {
const layerstypes: [string, string][] = Object.keys(v8.layer.type.values || {}).map(v => [v, startCase(v.replace(/-/g, " "))]);
return (
<Block label={t("Type")} fieldSpec={v8.layer.type}
data-wd-key={wdKey}
error={error}
class FieldTypeInternal extends React.Component<FieldTypeInternalProps> {
static defaultProps = {
disabled: false,
}
render() {
const t = this.props.t;
return <Block label={t("Type")} fieldSpec={latest.layer.type}
data-wd-key={this.props.wdKey}
error={this.props.error}
>
{disabled && (
<InputString value={value} disabled={true} />
)}
{!disabled && (
<InputSelect
options={layerstypes}
onChange={onChange}
value={value}
data-wd-key={wdKey + ".select"}
{this.props.disabled &&
<InputString
value={this.props.value}
disabled={true}
/>
)}
}
{!this.props.disabled &&
<InputSelect
options={[
['background', 'Background'],
['fill', 'Fill'],
['line', 'Line'],
['symbol', 'Symbol'],
['raster', 'Raster'],
['circle', 'Circle'],
['fill-extrusion', 'Fill Extrusion'],
['hillshade', 'Hillshade'],
['heatmap', 'Heatmap'],
]}
onChange={this.props.onChange}
value={this.props.value}
data-wd-key={this.props.wdKey + ".select"}
/>
}
</Block>
);
};
}
}
const FieldType = withTranslation()(FieldTypeInternal);
export default FieldType;
+12 -11
View File
@@ -1,5 +1,6 @@
import InputUrl, {type FieldUrlProps as InputUrlProps} from "./InputUrl";
import Block from "./Block";
import React from 'react'
import InputUrl, {FieldUrlProps as InputUrlProps} from './InputUrl'
import Block from './Block'
type FieldUrlProps = InputUrlProps & {
@@ -10,12 +11,12 @@ type FieldUrlProps = InputUrlProps & {
};
const FieldUrl: React.FC<FieldUrlProps> = (props) => {
return (
<Block label={props.label} fieldSpec={props.fieldSpec}>
<InputUrl {...props} />
</Block>
);
};
export default FieldUrl;
export default class FieldUrl extends React.Component<FieldUrlProps> {
render () {
return (
<Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputUrl {...this.props} />
</Block>
);
}
}
+49 -37
View File
@@ -1,53 +1,65 @@
import React, { type PropsWithChildren, type ReactElement } from "react";
import classnames from "classnames";
import FieldDocLabel from "./FieldDocLabel";
import Doc from "./Doc";
import generateUniqueId from "../libs/document-uid";
import React, { PropsWithChildren, ReactElement } from 'react'
import FieldDocLabel from './FieldDocLabel'
import Doc from './Doc'
import generateUniqueId from '../libs/document-uid';
export type FieldsetProps = PropsWithChildren & {
type FieldsetProps = PropsWithChildren & {
label?: string,
fieldSpec?: { doc?: string },
action?: ReactElement,
error?: {message: string}
};
type FieldsetState = {
showDoc: boolean
};
const Fieldset: React.FC<FieldsetProps> = (props) => {
const [showDoc, setShowDoc] = React.useState(false);
const labelId = React.useRef(generateUniqueId("fieldset_label_"));
export default class Fieldset extends React.Component<FieldsetProps, FieldsetState> {
_labelId: string;
const onToggleDoc = (val: boolean) => {
setShowDoc(val);
};
constructor (props: FieldsetProps) {
super(props);
this._labelId = generateUniqueId(`fieldset_label_`);
this.state = {
showDoc: false,
}
}
return (
<div className="maputnik-input-block" role="group" aria-labelledby={labelId.current}>
{props.fieldSpec && (
onToggleDoc = (val: boolean) => {
this.setState({
showDoc: val
});
}
render () {
return <div className="maputnik-input-block" role="group" aria-labelledby={this._labelId}>
{this.props.fieldSpec &&
<div className="maputnik-input-block-label">
<FieldDocLabel
label={props.label}
onToggleDoc={onToggleDoc}
fieldSpec={props.fieldSpec}
label={this.props.label}
onToggleDoc={this.onToggleDoc}
fieldSpec={this.props.fieldSpec}
/>
</div>
)}
{!props.fieldSpec && (
<div className={classnames({
"maputnik-input-block-label": true,
"maputnik-input-block--error": props.error
})}>
{props.label}
}
{!this.props.fieldSpec &&
<div className="maputnik-input-block-label">
{this.props.label}
</div>
)}
<div className="maputnik-input-block-action">{props.action}</div>
<div className="maputnik-input-block-content">{props.children}</div>
{props.fieldSpec && (
<div className="maputnik-doc-inline" style={{ display: showDoc ? "" : "none" }}>
<Doc fieldSpec={props.fieldSpec} />
}
<div className="maputnik-input-block-action">
{this.props.action}
</div>
<div className="maputnik-input-block-content">
{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>
);
};
export default Fieldset;
}
}
+55 -51
View File
@@ -1,25 +1,23 @@
import React from "react";
import { TbMathFunction } from "react-icons/tb";
import { PiListPlusBold } from "react-icons/pi";
import {isEqual} from "lodash";
import {type ExpressionSpecification, type LegacyFilterSpecification} from "maplibre-gl";
import {migrate, convertFilter} from "@maplibre/maplibre-gl-style-spec";
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import React from 'react'
import {mdiTableRowPlusAfter} from '@mdi/js';
import {isEqual} from 'lodash';
import {ExpressionSpecification, LegacyFilterSpecification, StyleSpecification} from 'maplibre-gl'
import {latest, migrate, convertFilter} from '@maplibre/maplibre-gl-style-spec'
import {mdiFunctionVariant} from '@mdi/js';
import {combiningFilterOps} from "../libs/filterops";
import InputSelect from "./InputSelect";
import Block from "./Block";
import SingleFilterEditor from "./SingleFilterEditor";
import FilterEditorBlock from "./FilterEditorBlock";
import InputButton from "./InputButton";
import Doc from "./Doc";
import ExpressionProperty from "./_ExpressionProperty";
import { type WithTranslation, withTranslation } from "react-i18next";
import type { MappedLayerErrors, StyleSpecificationWithId } from "../libs/definitions";
import {combiningFilterOps} from '../libs/filterops'
import InputSelect from './InputSelect'
import Block from './Block'
import SingleFilterEditor from './SingleFilterEditor'
import FilterEditorBlock from './FilterEditorBlock'
import InputButton from './InputButton'
import Doc from './Doc'
import ExpressionProperty from './_ExpressionProperty';
import { WithTranslation, withTranslation } from 'react-i18next';
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification {
const filter = props.filter || ["all"];
const filter = props.filter || ['all'];
if (!Array.isArray(filter)) {
return filter;
@@ -29,7 +27,7 @@ function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecific
let filters = filter.slice(1);
if(combiningFilterOps.indexOf(combiningOp) < 0) {
combiningOp = "all";
combiningOp = 'all';
filters = [filter.slice(0)];
}
@@ -41,7 +39,7 @@ function migrateFilter(filter: LegacyFilterSpecification | ExpressionSpecificati
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 {
"id": "tmp",
"version": 8,
@@ -50,7 +48,7 @@ function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpe
"sources": {
"tmp": {
"type": "geojson",
"data": ""
"data": ''
}
},
"sprite": "",
@@ -82,22 +80,22 @@ function checkIfSimpleFilter (filter: LegacyFilterSpecification | ExpressionSpec
}
function hasCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
return combiningFilterOps.indexOf(filter[0]) >= 0;
return combiningFilterOps.indexOf(filter[0]) >= 0
}
function hasNestedCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
if(hasCombiningFilter(filter)) {
return filter.slice(1).map(f => hasCombiningFilter(f as any)).filter(f => f == true).length > 0;
return filter.slice(1).map(f => hasCombiningFilter(f as any)).filter(f => f == true).length > 0
}
return false;
return false
}
type FilterEditorInternalProps = {
/** Properties of the vector layer and the available fields */
properties?: {[key:string]: any}
filter?: any[]
errors?: MappedLayerErrors
onChange(value: LegacyFilterSpecification | ExpressionSpecification): void
errors?: {[key:string]: any}
onChange(value: LegacyFilterSpecification | ExpressionSpecification): unknown
} & WithTranslation;
type FilterEditorState = {
@@ -109,7 +107,7 @@ type FilterEditorState = {
class FilterEditorInternal extends React.Component<FilterEditorInternalProps, FilterEditorState> {
static defaultProps = {
filter: ["all"],
};
}
constructor (props: FilterEditorInternalProps) {
super(props);
@@ -121,42 +119,42 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
// Convert filter to combining filter
onFilterPartChanged(filterIdx: number, newPart: any[]) {
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
newFilter[filterIdx] = newPart;
this.props.onChange(newFilter);
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
newFilter[filterIdx] = newPart
this.props.onChange(newFilter)
}
deleteFilterItem(filterIdx: number) {
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
newFilter.splice(filterIdx + 1, 1);
this.props.onChange(newFilter);
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
newFilter.splice(filterIdx + 1, 1)
this.props.onChange(newFilter)
}
addFilterItem = () => {
const newFilterItem = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification;
(newFilterItem as any[]).push(["==", "name", ""]);
this.props.onChange(newFilterItem);
};
const newFilterItem = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
(newFilterItem as any[]).push(['==', 'name', ''])
this.props.onChange(newFilterItem)
}
onToggleDoc = (val: boolean) => {
this.setState({
showDoc: val
});
};
}
makeFilter = () => {
this.setState({
displaySimpleFilter: true,
});
};
})
}
makeExpression = () => {
const filter = combiningFilter(this.props);
this.props.onChange(migrateFilter(filter));
this.setState({
displaySimpleFilter: false,
});
};
})
}
static getDerivedStateFromProps(props: Readonly<FilterEditorInternalProps>, state: FilterEditorState) {
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
@@ -171,7 +169,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
else if (displaySimpleFilter && state.displaySimpleFilter === false) {
return {
valueIsSimpleFilter: true,
};
}
}
else {
return {
@@ -199,10 +197,12 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
onClick={this.makeExpression}
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")}
</InputButton>
</div>;
</div>
}
else if (displaySimpleFilter) {
const filter = combiningFilter(this.props);
@@ -216,7 +216,9 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
title={t("Convert to expression")}
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>
</div>
);
@@ -238,7 +240,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
}
</div>
);
});
})
return (
@@ -269,14 +271,15 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
className="maputnik-add-filter"
onClick={this.addFilterItem}
>
<PiListPlusBold style={{ verticalAlign: "text-bottom" }} />
{t("Add filter")}
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> {t("Add filter")}
</InputButton>
</div>
<div
key="doc"
className="maputnik-doc-inline"
style={{display: this.state.showDoc ? "" : "none"}}
style={{display: this.state.showDoc ? '' : 'none'}}
>
<Doc fieldSpec={fieldSpec} />
</div>
@@ -294,6 +297,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
this.props.onChange(defaultFilter);
}}
fieldName="filter"
fieldSpec={fieldSpec}
value={filter}
errors={errors}
onChange={this.props.onChange}
@@ -301,7 +305,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
{this.state.valueIsSimpleFilter &&
<div className="maputnik-expr-infobox">
{t("You've entered an old style filter.")}
{" "}
{' '}
<button
onClick={this.makeFilter}
className="maputnik-expr-infobox__button"
+9 -9
View File
@@ -1,7 +1,7 @@
import React, { type PropsWithChildren } from "react";
import InputButton from "./InputButton";
import {MdDelete} from "react-icons/md";
import { type WithTranslation, withTranslation } from "react-i18next";
import React, { PropsWithChildren } from 'react'
import InputButton from './InputButton'
import {MdDelete} from 'react-icons/md'
import { WithTranslation, withTranslation } from 'react-i18next';
type FilterEditorBlockInternalProps = PropsWithChildren & {
onDelete(...args: unknown[]): unknown
@@ -11,19 +11,19 @@ class FilterEditorBlockInternal extends React.Component<FilterEditorBlockInterna
render() {
const t = this.props.t;
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">
<InputButton
className="maputnik-icon-button"
className="maputnik-delete-filter"
onClick={this.props.onDelete}
title={t("Delete filter block")}
>
<MdDelete />
</InputButton>
</div>
</div>;
<div className="maputnik-filter-editor-block-content">
{this.props.children}
</div>
</div>
}
}
+13
View File
@@ -0,0 +1,13 @@
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>
)
}
}
+13
View File
@@ -0,0 +1,13 @@
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>
)
}
}
+13
View File
@@ -0,0 +1,13 @@
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>
)
}
}
+24 -25
View File
@@ -1,33 +1,32 @@
import React from "react";
import React from 'react'
import type {CSSProperties} from "react";
import { BsBack, BsDiamondFill, BsSunFill } from "react-icons/bs";
import { MdBubbleChart, MdCircle, MdLocationPin, MdPhoto, MdPriorityHigh } from "react-icons/md";
import { IoAnalyticsOutline } from "react-icons/io5";
import { IoMdCube } from "react-icons/io";
import { FaMountain } from "react-icons/fa";
import IconLine from './IconLine'
import IconFill from './IconFill'
import IconSymbol from './IconSymbol'
import IconBackground from './IconBackground'
import IconCircle from './IconCircle'
import IconMissing from './IconMissing'
type IconLayerProps = {
type: string
style?: CSSProperties
style?: object
className?: string
};
const IconLayer: React.FC<IconLayerProps> = (props) => {
const iconProps = { style: props.style };
switch(props.type) {
case "fill-extrusion": return <IoMdCube {...iconProps} />;
case "raster": return <MdPhoto {...iconProps} />;
case "hillshade": return <BsSunFill {...iconProps} />;
case "color-relief": return <FaMountain {...iconProps} />;
case "heatmap": return <MdBubbleChart {...iconProps} />;
case "fill": return <BsDiamondFill {...iconProps} />;
case "background": return <BsBack {...iconProps} />;
case "line": return <IoAnalyticsOutline {...iconProps} />;
case "symbol": return <MdLocationPin {...iconProps} />;
case "circle": return <MdCircle {...iconProps} />;
default: return <MdPriorityHigh {...iconProps} />;
export default class IconLayer extends React.Component<IconLayerProps> {
render() {
const iconProps = { style: this.props.style }
switch(this.props.type) {
case 'fill-extrusion': return <IconBackground {...iconProps} />
case 'raster': return <IconFill {...iconProps} />
case 'hillshade': return <IconFill {...iconProps} />
case 'heatmap': return <IconFill {...iconProps} />
case 'fill': return <IconFill {...iconProps} />
case 'background': return <IconBackground {...iconProps} />
case 'line': return <IconLine {...iconProps} />
case 'symbol': return <IconSymbol {...iconProps} />
case 'circle': return <IconCircle {...iconProps} />
default: return <IconMissing {...iconProps} />
}
}
};
export default IconLayer;
}
+13
View File
@@ -0,0 +1,13 @@
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>
)
}
}
+11
View File
@@ -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} />
)
}
}
+15
View File
@@ -0,0 +1,15 @@
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>
)
}
}
+21 -21
View File
@@ -1,29 +1,29 @@
import React from "react";
import InputString from "./InputString";
import InputNumber from "./InputNumber";
import React from 'react'
import InputString from './InputString'
import InputNumber from './InputNumber'
export type InputArrayProps = {
export type FieldArrayProps = {
value: (string | number | undefined)[]
type?: string
length?: number
default?: (string | number | undefined)[]
onChange?(value: (string | number | undefined)[] | undefined): unknown
"aria-label"?: string
'aria-label'?: string
label?: string
};
type InputArrayState = {
type FieldArrayState = {
value: (string | number | undefined)[]
initialPropsValue: unknown[]
};
}
export default class InputArray extends React.Component<InputArrayProps, InputArrayState> {
export default class FieldArray extends React.Component<FieldArrayProps, FieldArrayState> {
static defaultProps = {
value: [],
default: [],
};
}
constructor (props: InputArrayProps) {
constructor (props: FieldArrayProps) {
super(props);
this.state = {
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 initialPropsValue = state.initialPropsValue.slice(0);
@@ -44,7 +44,7 @@ export default class InputArray extends React.Component<InputArrayProps, InputAr
value[i] = state.value[i];
initialPropsValue[i] = state.value[i];
}
});
})
return {
value,
@@ -54,7 +54,7 @@ export default class InputArray extends React.Component<InputArrayProps, InputAr
isComplete(value: unknown[]) {
return Array(this.props.length).fill(null).every((_, i) => {
const val = value[i];
const val = value[i]
return !(val === undefined || val === "");
});
}
@@ -82,20 +82,20 @@ export default class InputArray extends React.Component<InputArrayProps, InputAr
const containsValues = (
value.length > 0 &&
!value.every(val => {
return (val === "" || val === undefined);
return (val === "" || val === undefined)
})
);
const inputs = Array(this.props.length).fill(null).map((_, i) => {
if(this.props.type === "number") {
if(this.props.type === 'number') {
return <InputNumber
key={i}
default={containsValues || !this.props.default ? undefined : this.props.default[i] as number}
value={value[i] as number}
required={containsValues ? true : false}
onChange={(v) => this.changeValue(i, v)}
aria-label={this.props["aria-label"] || this.props.label}
/>;
aria-label={this.props['aria-label'] || this.props.label}
/>
} else {
return <InputString
key={i}
@@ -103,15 +103,15 @@ export default class InputArray extends React.Component<InputArrayProps, InputAr
value={value[i] as string}
required={containsValues ? true : false}
onChange={this.changeValue.bind(this, i)}
aria-label={this.props["aria-label"] || this.props.label}
/>;
aria-label={this.props['aria-label'] || this.props.label}
/>
}
});
})
return (
<div className="maputnik-array">
{inputs}
</div>
);
)
}
}
-18
View File
@@ -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");
});
});
+81 -97
View File
@@ -1,116 +1,100 @@
import React from "react";
import classnames from "classnames";
import {useCombobox} from "downshift";
import React from 'react'
import classnames from 'classnames'
import Autocomplete from 'react-autocomplete'
const MAX_HEIGHT = 140;
export type InputAutocompleteProps = {
value?: string
options?: any[]
onChange?(value: string | undefined): unknown
"aria-label"?: string
options: any[]
onChange(value: string | undefined): unknown
keepMenuWithinWindowBounds?: boolean
'aria-label'?: string
};
export default function InputAutocomplete({
value,
options = [],
onChange = () => {},
"aria-label": ariaLabel,
}: InputAutocompleteProps) {
const [input, setInput] = React.useState(value || "");
const menuRef = React.useRef<HTMLDivElement>(null);
const [maxHeight, setMaxHeight] = React.useState(MAX_HEIGHT);
export default class InputAutocomplete extends React.Component<InputAutocompleteProps> {
state = {
maxHeight: MAX_HEIGHT
}
const filteredItems = React.useMemo(() => {
const lv = input.toLowerCase();
return options.filter((item) => item[0].toLowerCase().includes(lv));
}, [options, input]);
autocompleteMenuEl: HTMLDivElement | null = null;
const calcMaxHeight = React.useCallback(() => {
if (menuRef.current) {
const space = window.innerHeight - menuRef.current.getBoundingClientRect().top;
setMaxHeight(Math.min(space, MAX_HEIGHT));
}
}, []);
static defaultProps = {
onChange: () => {},
options: [],
}
const {
isOpen,
getMenuProps,
getInputProps,
getItemProps,
highlightedIndex,
openMenu,
} = useCombobox({
items: filteredItems,
inputValue: input,
itemToString: (item) => (item ? item[0] : ""),
stateReducer: (_state, action) => {
if (action.type === useCombobox.stateChangeTypes.InputClick) {
return {...action.changes, isOpen: true};
calcMaxHeight() {
if(this.props.keepMenuWithinWindowBounds) {
const maxHeight = window.innerHeight - this.autocompleteMenuEl!.getBoundingClientRect().top;
const limitedMaxHeight = Math.min(maxHeight, MAX_HEIGHT);
if(limitedMaxHeight != this.state.maxHeight) {
this.setState({
maxHeight: limitedMaxHeight
})
}
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(() => {
window.addEventListener("resize", calcMaxHeight);
return () => window.removeEventListener("resize", calcMaxHeight);
}, [calcMaxHeight]);
componentDidMount() {
this.calcMaxHeight();
}
React.useEffect(() => {
setInput(value || "");
}, [value]);
componentDidUpdate() {
this.calcMaxHeight();
}
return (
<div className="maputnik-autocomplete">
<input
{...getInputProps({
"aria-label": ariaLabel,
onChange(v: string) {
this.props.onChange(v === "" ? undefined : v);
}
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",
spellCheck: false,
onFocus: () => openMenu(),
})}
spellCheck: false
}}
value={this.props.value}
items={this.props.options}
getItemValue={(item) => item[0]}
onSelect={v => this.onChange(v)}
onChange={(_e, v) => this.onChange(v)}
shouldItemRender={(item, value="") => {
if (typeof(value) === "string") {
return item[0].toLowerCase().indexOf(value.toLowerCase()) > -1
}
return false
}}
renderItem={(item, isHighlighted) => (
<div
key={item[0]}
className={classnames({
"maputnik-autocomplete-menu-item": true,
"maputnik-autocomplete-menu-item-selected": isHighlighted,
})}
>
{item[1]}
</div>
)}
/>
<div
{...getMenuProps({}, {suppressRefError: true})}
ref={menuRef}
style={{position: "fixed", overflow: "auto", maxHeight, zIndex: 998}}
className="maputnik-autocomplete-menu"
>
{isOpen &&
filteredItems.map((item, index) => (
<div
key={item[0]}
{...getItemProps({
item,
index,
className: classnames("maputnik-autocomplete-menu-item", {
"maputnik-autocomplete-menu-item-selected": highlightedIndex === index,
}),
})}
>
{item[1]}
</div>
))}
</div>
</div>
);
}
}
+3 -3
View File
@@ -1,5 +1,5 @@
import React from "react";
import classnames from "classnames";
import React from 'react'
import classnames from 'classnames'
type InputButtonProps = {
"data-wd-key"?: string
@@ -28,6 +28,6 @@ export default class InputButton extends React.Component<InputButtonProps> {
style={this.props.style}
>
{this.props.children}
</button>;
</button>
}
}
+5 -5
View File
@@ -1,4 +1,4 @@
import React from "react";
import React from 'react'
export type InputCheckboxProps = {
value?: boolean
@@ -9,11 +9,11 @@ export type InputCheckboxProps = {
export default class InputCheckbox extends React.Component<InputCheckboxProps> {
static defaultProps = {
value: false,
};
}
onChange = () => {
this.props.onChange(!this.props.value);
};
}
render() {
return <div className="maputnik-checkbox-wrapper">
@@ -27,11 +27,11 @@ export default class InputCheckbox extends React.Component<InputCheckboxProps> {
/>
<div className="maputnik-checkbox-box">
<svg style={{
display: this.props.value ? "inline" : "none"
display: this.props.value ? 'inline' : 'none'
}} className="maputnik-checkbox-icon" viewBox='0 0 32 32'>
<path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' />
</svg>
</div>
</div>;
</div>
}
}
+29 -29
View File
@@ -1,12 +1,12 @@
import React from "react";
import Color from "color";
import ChromePicker from "react-color/lib/components/chrome/Chrome";
import {type ColorResult} from "react-color";
import lodash from "lodash";
import React from 'react'
import Color from 'color'
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
import {ColorResult} from 'react-color';
import lodash from 'lodash';
function formatColor(color: ColorResult): string {
const rgb = color.rgb;
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`;
const rgb = color.rgb
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`
}
export type InputColorProps = {
@@ -16,14 +16,14 @@ export type InputColorProps = {
doc?: string
style?: object
default?: string
"aria-label"?: string
'aria-label'?: string
};
/*** Number fields with support for min, max and units and documentation*/
export default class InputColor extends React.Component<InputColorProps> {
state = {
pickerOpened: false
};
}
colorInput: HTMLInputElement | null = null;
constructor (props: InputColorProps) {
@@ -39,29 +39,29 @@ export default class InputColor extends React.Component<InputColorProps> {
//but I am too stupid to get it to work together with fixed position
//and scrollbars so I have to fallback to JavaScript
calcPickerOffset = () => {
const elem = this.colorInput;
const elem = this.colorInput
if(elem) {
const pos = elem.getBoundingClientRect();
const pos = elem.getBoundingClientRect()
return {
top: pos.top,
left: pos.left + 196,
};
}
} else {
return {
top: 160,
left: 555,
};
}
}
};
}
togglePicker = () => {
this.setState({ pickerOpened: !this.state.pickerOpened });
};
this.setState({ pickerOpened: !this.state.pickerOpened })
}
get color() {
// Catch invalid color.
try {
return Color(this.props.value).rgb();
return Color(this.props.value).rgb()
}
catch(err) {
console.warn("Error parsing color: ", err);
@@ -74,7 +74,7 @@ export default class InputColor extends React.Component<InputColorProps> {
}
render() {
const offset = this.calcPickerOffset();
const offset = this.calcPickerOffset()
const currentColor = this.color.object();
const currentChromeColor = {
r: currentColor.r,
@@ -82,12 +82,12 @@ export default class InputColor extends React.Component<InputColorProps> {
b: currentColor.b,
// Rename alpha -> a for ChromePicker
a: currentColor.alpha!
};
}
const picker = <div
className="maputnik-color-picker-offset"
style={{
position: "fixed",
position: 'fixed',
zIndex: 1,
left: offset.left,
top: offset.top,
@@ -101,14 +101,14 @@ export default class InputColor extends React.Component<InputColorProps> {
onClick={this.togglePicker}
style={{
zIndex: -1,
position: "fixed",
top: "0px",
right: "0px",
bottom: "0px",
left: "0px",
position: 'fixed',
top: '0px',
right: '0px',
bottom: '0px',
left: '0px',
}}
/>
</div>;
</div>
const swatchStyle = {
backgroundColor: this.props.value
@@ -118,11 +118,11 @@ export default class InputColor extends React.Component<InputColorProps> {
{this.state.pickerOpened && picker}
<div className="maputnik-color-swatch" style={swatchStyle}></div>
<input
aria-label={this.props["aria-label"]}
aria-label={this.props['aria-label']}
spellCheck="false"
autoComplete="off"
className="maputnik-color"
ref={(input) => {this.colorInput = input;}}
ref={(input) => this.colorInput = input}
onClick={this.togglePicker}
style={this.props.style}
name={this.props.name}
@@ -130,6 +130,6 @@ export default class InputColor extends React.Component<InputColorProps> {
value={this.props.value ? this.props.value : ""}
onChange={(e) => this.onChange(e.target.value)}
/>
</div>;
</div>
}
}
+46 -56
View File
@@ -1,67 +1,64 @@
import React from "react";
import capitalize from "lodash.capitalize";
import {MdDelete} from "react-icons/md";
import { type WithTranslation, withTranslation } from "react-i18next";
import React from 'react'
import capitalize from 'lodash.capitalize'
import {MdDelete} from 'react-icons/md'
import { WithTranslation, withTranslation } from 'react-i18next';
import InputString from "./InputString";
import InputNumber from "./InputNumber";
import InputButton from "./InputButton";
import FieldDocLabel from "./FieldDocLabel";
import InputEnum from "./InputEnum";
import InputUrl from "./InputUrl";
import InputColor from "./InputColor";
import InputString from './InputString'
import InputNumber from './InputNumber'
import InputButton from './InputButton'
import FieldDocLabel from './FieldDocLabel'
import InputEnum from './InputEnum'
import InputUrl from './InputUrl'
export type InputDynamicArrayProps = {
export type FieldDynamicArrayProps = {
value?: (string | number | undefined)[]
type?: "url" | "number" | "enum" | "string" | "color"
type?: 'url' | 'number' | 'enum' | 'string'
default?: (string | number | undefined)[]
onChange?(values: (string | number | undefined)[] | undefined): unknown
style?: object
fieldSpec?: {
values?: any
}
"aria-label"?: string
'aria-label'?: string
label: string
};
}
type 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) {
const values = this.values.slice(0);
values[idx] = newValue;
if (this.props.onChange) this.props.onChange(values);
const values = this.values.slice(0)
values[idx] = newValue
if (this.props.onChange) this.props.onChange(values)
}
get values() {
return this.props.value || this.props.default || [];
return this.props.value || this.props.default || []
}
addValue = () => {
const values = this.values.slice(0);
if (this.props.type === "number") {
values.push(0);
const values = this.values.slice(0)
if (this.props.type === 'number') {
values.push(0)
}
else if (this.props.type === "url") {
else if (this.props.type === 'url') {
values.push("");
}
else if (this.props.type === "enum") {
else if (this.props.type === 'enum') {
const {fieldSpec} = this.props;
const defaultValue = Object.keys(fieldSpec!.values)[0];
values.push(defaultValue);
} else if (this.props.type === "color") {
values.push("#000000");
} else {
values.push("");
values.push("")
}
if (this.props.onChange) this.props.onChange(values);
};
if (this.props.onChange) this.props.onChange(values)
}
deleteValue(valueIdx: number) {
const values = this.values.slice(0);
values.splice(valueIdx, 1);
const values = this.values.slice(0)
values.splice(valueIdx, 1)
if (this.props.onChange) this.props.onChange(values.length > 0 ? values : undefined);
}
@@ -75,42 +72,35 @@ class InputDynamicArrayInternal extends React.Component<InputDynamicArrayInterna
{...i18nProps}
/>;
let input;
if(this.props.type === "url") {
if(this.props.type === 'url') {
input = <InputUrl
value={v as string}
onChange={this.changeValue.bind(this, i)}
aria-label={this.props["aria-label"] || this.props.label}
/>;
aria-label={this.props['aria-label'] || this.props.label}
/>
}
else if (this.props.type === "number") {
else if (this.props.type === 'number') {
input = <InputNumber
value={v as number}
onChange={this.changeValue.bind(this, i)}
aria-label={this.props["aria-label"] || this.props.label}
/>;
aria-label={this.props['aria-label'] || this.props.label}
/>
}
else if (this.props.type === "enum") {
else if (this.props.type === 'enum') {
const options = Object.keys(this.props.fieldSpec?.values).map(v => [v, capitalize(v)]);
input = <InputEnum
options={options}
value={v as string}
onChange={this.changeValue.bind(this, i)}
aria-label={this.props["aria-label"] || this.props.label}
/>;
}
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}
/>;
aria-label={this.props['aria-label'] || this.props.label}
/>
}
else {
input = <InputString
value={v as string}
onChange={this.changeValue.bind(this, i)}
aria-label={this.props["aria-label"] || this.props.label}
/>;
aria-label={this.props['aria-label'] || this.props.label}
/>
}
return <div
@@ -124,8 +114,8 @@ class InputDynamicArrayInternal extends React.Component<InputDynamicArrayInterna
<div className="maputnik-array-block-content">
{input}
</div>
</div>;
});
</div>
})
return (
<div className="maputnik-array">
@@ -141,8 +131,8 @@ class InputDynamicArrayInternal extends React.Component<InputDynamicArrayInterna
}
}
const InputDynamicArray = withTranslation()(InputDynamicArrayInternal);
export default InputDynamicArray;
const FieldDynamicArray = withTranslation()(FieldDynamicArrayInternal);
export default FieldDynamicArray;
type DeleteValueInputButtonProps = {
onClick?(...args: unknown[]): unknown
@@ -159,6 +149,6 @@ class DeleteValueInputButton extends React.Component<DeleteValueInputButtonProps
<FieldDocLabel
label={<MdDelete />}
/>
</InputButton>;
</InputButton>
}
}
+11 -11
View File
@@ -1,14 +1,14 @@
import React from "react";
import InputSelect from "./InputSelect";
import InputMultiInput from "./InputMultiInput";
import React from 'react'
import InputSelect from './InputSelect'
import InputMultiInput from './InputMultiInput'
function optionsLabelLength(options: any[]) {
let sum = 0;
options.forEach(([_, label]) => {
sum += label.length;
});
return sum;
sum += label.length
})
return sum
}
@@ -20,7 +20,7 @@ export type InputEnumProps = {
name?: string
onChange(...args: unknown[]): unknown
options: any[]
"aria-label"?: string
'aria-label'?: string
label?: string
};
@@ -35,15 +35,15 @@ export default class InputEnum extends React.Component<InputEnumProps> {
options={options}
value={(value || this.props.default)!}
onChange={onChange}
aria-label={this.props["aria-label"] || label}
/>;
aria-label={this.props['aria-label'] || label}
/>
} else {
return <InputSelect
options={options}
value={(value || this.props.default)!}
onChange={onChange}
aria-label={this.props["aria-label"] || label}
/>;
aria-label={this.props['aria-label'] || label}
/>
}
}
}
+12 -12
View File
@@ -1,20 +1,20 @@
import React from "react";
import InputAutocomplete from "./InputAutocomplete";
import React from 'react'
import InputAutocomplete from './InputAutocomplete'
export type InputFontProps = {
export type FieldFontProps = {
name: string
value?: string[]
default?: string[]
fonts?: unknown[]
style?: object
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 = {
fonts: []
};
}
get values() {
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) {
const changedValues = this.values.slice(0);
changedValues[idx] = newValue;
const changedValues = this.values.slice(0)
changedValues[idx] = newValue
const filteredValues = changedValues
.filter(v => v !== undefined)
.filter(v => v !== "");
.filter(v => v !== "")
this.props.onChange(filteredValues);
}
@@ -44,13 +44,13 @@ export default class InputFont extends React.Component<InputFontProps> {
key={i}
>
<InputAutocomplete
aria-label={this.props["aria-label"] || this.props.name}
aria-label={this.props['aria-label'] || this.props.name}
value={value}
options={this.props.fonts?.map(f => [f, f])}
onChange={this.changeFont.bind(this, i)}
/>
</li>;
});
</li>
})
return (
<ul className="maputnik-font">
+103 -58
View File
@@ -1,98 +1,126 @@
import React from "react";
import classnames from "classnames";
import { type WithTranslation, withTranslation } from "react-i18next";
import React from 'react'
import classnames from 'classnames';
import CodeMirror, { ModeSpec } from 'codemirror';
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import { type EditorView } from "@codemirror/view";
import stringifyPretty from "json-stringify-pretty-compact";
import 'codemirror/mode/javascript/javascript'
import 'codemirror/addon/lint/lint'
import 'codemirror/addon/edit/matchbrackets'
import 'codemirror/lib/codemirror.css'
import 'codemirror/addon/lint/lint.css'
import stringifyPretty from 'json-stringify-pretty-compact'
import '../libs/codemirror-mgl';
import {createEditor} from "../libs/codemirror-editor-factory";
import type { StylePropertySpecification } from "maplibre-gl";
import type { TransactionSpec } from "@codemirror/state";
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
onChange(object: object): void
onFocus?(...args: unknown[]): unknown
onBlur?(...args: unknown[]): unknown
lintType: "layer" | "style" | "expression" | "json"
spec?: StylePropertySpecification | undefined
/**
* When setting this and using search and replace, the editor will scroll to the selected text
* Use this only when the editor is the only element in the page.
*/
withScroll?: boolean
onJSONValid?(...args: unknown[]): unknown
onJSONInvalid?(...args: unknown[]): unknown
mode?: ModeSpec<any>
lint?: boolean | object
};
type InputJsonInternalProps = InputJsonProps & WithTranslation;
type InputJsonState = {
isEditing: boolean
showMessage: boolean
prevValue: string
};
class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJsonState> {
static defaultProps = {
lineNumbers: true,
lineWrapping: false,
gutters: ["CodeMirror-lint-markers"],
getValue: (data: any) => {
return stringifyPretty(data, {indent: 2, maxLength: 40});
},
onFocus: () => {},
onBlur: () => {},
withScroll: false
};
_view: EditorView | undefined;
onJSONInvalid: () => {},
onJSONValid: () => {},
}
_keyEvent: string;
_doc: CodeMirror.Editor | undefined;
_el: HTMLDivElement | null = null;
_cancelNextChange: boolean = false;
constructor(props: InputJsonInternalProps) {
super(props);
this._keyEvent = "keyboard";
this.state = {
isEditing: false,
prevValue: this.getPrettyJson(this.props.value),
showMessage: false,
prevValue: this.props.getValue!(this.props.layer),
};
}
getPrettyJson(data: any) {
return stringifyPretty(data, {indent: 2, maxLength: 40});
componentDidMount () {
this._doc = CodeMirror(this._el!, {
value: this.props.getValue!(this.props.layer),
mode: this.props.mode || {
name: "mgl",
},
lineWrapping: this.props.lineWrapping,
tabSize: 2,
theme: 'maputnik',
viewportMargin: Infinity,
lineNumbers: this.props.lineNumbers,
lint: this.props.lint || {
context: "layer"
},
matchBrackets: true,
gutters: this.props.gutters,
scrollbarStyle: "null",
});
this._doc.on('change', this.onChange);
this._doc.on('focus', this.onFocus);
this._doc.on('blur', this.onBlur);
}
componentDidMount () {
this._view = createEditor({
parent: this._el!,
value: this.getPrettyJson(this.props.value),
lintType: this.props.lintType || "layer",
onChange: (value:string) => this.onChange(value),
onFocus: () => this.onFocus(),
onBlur: () => this.onBlur(),
spec: this.props.spec
});
onPointerDown = () => {
this._keyEvent = "pointer";
}
onFocus = () => {
if (this.props.onFocus) this.props.onFocus();
this.setState({
isEditing: true,
showMessage: (this._keyEvent === "keyboard"),
});
};
}
onBlur = () => {
this._keyEvent = "keyboard";
if (this.props.onBlur) this.props.onBlur();
this.setState({
isEditing: false,
showMessage: false,
});
};
}
componentWillUnMount () {
this._doc!.off('change', this.onChange);
this._doc!.off('focus', this.onFocus);
this._doc!.off('blur', this.onBlur);
}
componentDidUpdate(prevProps: InputJsonProps) {
if (!this.state.isEditing && prevProps.value !== this.props.value) {
if (!this.state.isEditing && prevProps.layer !== this.props.layer) {
this._cancelNextChange = true;
const transactionSpec: TransactionSpec = {
changes: {
from: 0,
to: this._view!.state.doc.length,
insert: this.getPrettyJson(this.props.value)
}
};
if (this.props.withScroll) {
transactionSpec.selection = this._view!.state.selection;
transactionSpec.scrollIntoView = true;
}
this._view!.dispatch(transactionSpec);
this._doc!.setValue(
this.props.getValue!(this.props.layer),
)
}
}
@@ -100,11 +128,11 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
if (this._cancelNextChange) {
this._cancelNextChange = false;
this.setState({
prevValue: this._view!.state.doc.toString(),
});
prevValue: this._doc!.getValue(),
})
return;
}
const newCode = this._view!.state.doc.toString();
const newCode = this._doc!.getValue();
if (this.state.prevValue !== newCode) {
let parsedLayer, err;
@@ -112,26 +140,43 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
parsedLayer = JSON.parse(newCode);
} catch(_err) {
err = _err;
console.warn(_err);
console.warn(_err)
}
if (!err) {
if (this.props.onChange) this.props.onChange(parsedLayer);
if (err && this.props.onJSONInvalid) {
this.props.onJSONInvalid();
}
else {
if (this.props.onChange) this.props.onChange(parsedLayer)
if (this.props.onJSONValid) this.props.onJSONValid();
}
}
this.setState({
prevValue: newCode,
});
};
}
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
className={classnames("codemirror-container", this.props.className)}
ref={(el) => {this._el = el;}}
ref={(el) => this._el = el}
style={style}
/>
</div>;
</div>
}
}
+11 -11
View File
@@ -1,26 +1,26 @@
import React from "react";
import classnames from "classnames";
import React from 'react'
import classnames from 'classnames'
export type InputMultiInputProps = {
name?: string
value: string
options: any[]
onChange(...args: unknown[]): unknown
"aria-label"?: string
'aria-label'?: string
};
export default class InputMultiInput extends React.Component<InputMultiInputProps> {
render() {
let options = this.props.options;
let options = this.props.options
if(options.length > 0 && !Array.isArray(options[0])) {
options = options.map(v => [v, v]);
options = options.map(v => [v, v])
}
const selectedValue = this.props.value || options[0][0];
const selectedValue = this.props.value || options[0][0]
const radios = options.map(([val, label])=> {
return <label
key={val}
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"
name={this.props.name}
@@ -29,11 +29,11 @@ export default class InputMultiInput extends React.Component<InputMultiInputProp
checked={val === selectedValue}
/>
{label}
</label>;
});
</label>
})
return <fieldset className="maputnik-multibutton" aria-label={this.props["aria-label"]}>
return <fieldset className="maputnik-multibutton" aria-label={this.props['aria-label']}>
{radios}
</fieldset>;
</fieldset>
}
}
+20 -20
View File
@@ -1,5 +1,5 @@
import React, { type BaseSyntheticEvent } from "react";
import generateUniqueId from "../libs/document-uid";
import React, { BaseSyntheticEvent } from 'react'
import generateUniqueId from '../libs/document-uid';
export type InputNumberProps = {
value?: number
@@ -23,22 +23,22 @@ type InputNumberState = {
* This is the value that is currently being edited. It can be an invalid value.
*/
dirtyValue?: number | string | undefined
};
}
export default class InputNumber extends React.Component<InputNumberProps, InputNumberState> {
static defaultProps = {
rangeStep: 1
};
}
_keyboardEvent: boolean = false;
constructor(props: InputNumberProps) {
super(props);
super(props)
this.state = {
uuid: +generateUniqueId(),
editing: false,
value: props.value,
dirtyValue: props.value,
};
}
}
static getDerivedStateFromProps(props: Readonly<InputNumberProps>, state: InputNumberState) {
@@ -57,7 +57,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
const hasChanged = this.props.value !== value;
if(this.isValid(value) && hasChanged) {
if (this.props.onChange) this.props.onChange(value);
if (this.props.onChange) this.props.onChange(value)
this.setState({
value: value,
});
@@ -70,7 +70,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
this.setState({
dirtyValue: newValue === "" ? undefined : newValue,
});
})
}
isValid(v: number | string | undefined) {
@@ -80,18 +80,18 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
const value = +v;
if(isNaN(value)) {
return false;
return false
}
if(!isNaN(this.props.min!) && value < this.props.min!) {
return false;
return false
}
if(!isNaN(this.props.max!) && value > this.props.max!) {
return false;
return false
}
return true;
return true
}
resetValue = () => {
@@ -104,14 +104,14 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
// If set value is invalid fall back to the last valid value from props or at last resort the default value
if (!this.isValid(this.state.value)) {
if(this.isValid(this.props.value)) {
this.changeValue(this.props.value);
this.changeValue(this.props.value)
this.setState({dirtyValue: this.props.value});
} else {
this.changeValue(undefined);
this.setState({dirtyValue: undefined});
}
}
};
}
onChangeRange = (e: BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) => {
let value = parseFloat(e.target.value);
@@ -132,7 +132,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
value = this.state.value! - step;
}
else {
value = this.state.value! + step;
value = this.state.value! + step
}
dirtyValue = value;
}
@@ -153,7 +153,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
this.setState({value, dirtyValue});
if (this.props.onChange) this.props.onChange(value);
};
}
render() {
if(
@@ -217,18 +217,18 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
}}
onBlur={_e => {
this.setState({editing: false});
this.resetValue();
this.resetValue()
}}
data-wd-key={this.props["data-wd-key"] + "-text"}
/>
</div>;
</div>
}
else {
const value = this.state.editing ? this.state.dirtyValue : this.state.value;
return <input
aria-label={this.props["aria-label"]}
aria-label={this.props['aria-label']}
spellCheck="false"
className="maputnik-number"
placeholder={this.props.default?.toString()}
@@ -240,7 +240,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
onBlur={this.resetValue}
required={this.props.required}
data-wd-key={this.props["data-wd-key"]}
/>;
/>
}
}
}
+4 -4
View File
@@ -1,4 +1,4 @@
import React from "react";
import React from 'react'
export type InputSelectProps = {
value: string
@@ -7,7 +7,7 @@ export type InputSelectProps = {
style?: object
onChange(value: string | [string, any]): unknown
title?: string
"aria-label"?: string
'aria-label'?: string
};
export default class InputSelect extends React.Component<InputSelectProps> {
@@ -24,9 +24,9 @@ export default class InputSelect extends React.Component<InputSelectProps> {
title={this.props.title}
value={this.props.value}
onChange={e => this.props.onChange(e.target.value)}
aria-label={this.props["aria-label"]}
aria-label={this.props['aria-label']}
>
{ options.map(([val, label]) => <option key={val} value={val}>{label}</option>) }
</select>;
</select>
}
}
+77 -103
View File
@@ -1,26 +1,24 @@
import React, { type ReactElement } from "react";
import React, { ReactElement } from 'react'
import InputColor, { type InputColorProps } from "./InputColor";
import InputNumber, { type InputNumberProps } from "./InputNumber";
import InputCheckbox, { type InputCheckboxProps } from "./InputCheckbox";
import InputString, { type InputStringProps } from "./InputString";
import InputArray, { type InputArrayProps } from "./InputArray";
import InputDynamicArray, { type InputDynamicArrayProps } from "./InputDynamicArray";
import InputFont, { type InputFontProps } from "./InputFont";
import InputAutocomplete, { type InputAutocompleteProps } from "./InputAutocomplete";
import InputEnum, { type InputEnumProps } from "./InputEnum";
import capitalize from "lodash.capitalize";
import InputColor, { InputColorProps } from './InputColor'
import InputNumber, { InputNumberProps } from './InputNumber'
import InputCheckbox, { InputCheckboxProps } from './InputCheckbox'
import InputString, { InputStringProps } from './InputString'
import InputArray, { FieldArrayProps } from './InputArray'
import InputDynamicArray, { FieldDynamicArrayProps } from './InputDynamicArray'
import InputFont, { FieldFontProps } from './InputFont'
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
import InputEnum, { InputEnumProps } from './InputEnum'
import capitalize from 'lodash.capitalize'
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 InputSpecProps = {
export type SpecFieldProps = {
onChange?(fieldName: string | undefined, value: number | undefined | (string | number | undefined)[]): unknown
fieldName?: string
fieldSpec?: {
default?: unknown
type?: FieldSpecType
type?: 'number' | 'enum' | 'resolvedImage' | 'formatted' | 'string' | 'color' | 'boolean' | 'array'
minimum?: number
maximum?: number
values?: unknown[]
@@ -30,7 +28,8 @@ export type InputSpecProps = {
value?: string | number | unknown[] | boolean
/** Override the style of the field */
style?: object
"aria-label"?: string
'aria-label'?: string
error?: unknown[]
label?: string
action?: ReactElement
};
@@ -38,10 +37,11 @@ export type InputSpecProps = {
/** Display any field from the Maplibre GL style spec and
* choose the correct field component based on the @{fieldSpec}
* to display @{value}. */
export default class InputSpec extends React.Component<InputSpecProps> {
export default class SpecField extends React.Component<SpecFieldProps> {
childNodes() {
const commonProps = {
error: this.props.error,
fieldSpec: this.props.fieldSpec,
label: this.props.label,
action: this.props.action,
@@ -51,96 +51,70 @@ export default class InputSpec extends React.Component<InputSpecProps> {
name: this.props.fieldName,
"data-wd-key": "spec-field-input:" + this.props.fieldName,
onChange: (newValue: number | undefined | (string | number | undefined)[]) => this.props.onChange!(this.props.fieldName, newValue),
"aria-label": this.props["aria-label"],
};
'aria-label': this.props['aria-label'],
}
switch(this.props.fieldSpec?.type) {
case "number": return (
<InputNumber
{...commonProps as InputNumberProps}
min={this.props.fieldSpec.minimum}
max={this.props.fieldSpec.maximum}
/>
);
case "enum": {
const options = Object.keys(this.props.fieldSpec.values || []).map(v => [v, capitalize(v)]);
case 'number': return (
<InputNumber
{...commonProps as InputNumberProps}
min={this.props.fieldSpec.minimum}
max={this.props.fieldSpec.maximum}
/>
)
case 'enum': {
const options = Object.keys(this.props.fieldSpec.values || []).map(v => [v, capitalize(v)])
return <InputEnum
{...commonProps as Omit<InputEnumProps, "options">}
options={options}
/>;
return <InputEnum
{...commonProps as Omit<InputEnumProps, "options">}
options={options}
/>
}
case 'resolvedImage':
case 'formatted':
case 'string':
if (iconProperties.indexOf(this.props.fieldName!) >= 0) {
const options = this.props.fieldSpec.values || [];
return <InputAutocomplete
{...commonProps as Omit<InputAutocompleteProps, "options">}
options={options.map(f => [f, f])}
/>
} else {
return <InputString
{...commonProps as InputStringProps}
/>
}
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])}
/>;
case 'color': return (
<InputColor
{...commonProps as InputColorProps}
/>
)
case 'boolean': return (
<InputCheckbox
{...commonProps as InputCheckboxProps}
/>
)
case 'array':
if(this.props.fieldName === 'text-font') {
return <InputFont
{...commonProps as FieldFontProps}
fonts={this.props.fieldSpec.values}
/>
} else {
if (this.props.fieldSpec.length) {
return <InputArray
{...commonProps as FieldArrayProps}
type={this.props.fieldSpec.value}
length={this.props.fieldSpec.length}
/>
} else {
return <InputString
{...commonProps as InputStringProps}
/>;
return <InputDynamicArray
{...commonProps as FieldDynamicArrayProps}
fieldSpec={this.props.fieldSpec}
type={this.props.fieldSpec.value as FieldDynamicArrayProps['type']}
/>
}
case "color": return (
<InputColor
{...commonProps as InputColorProps}
/>
);
case "boolean": return (
<InputCheckbox
{...commonProps as InputCheckboxProps}
/>
);
case "array":
if(this.props.fieldName === "text-font") {
return <InputFont
{...commonProps as InputFontProps}
fonts={this.props.fieldSpec.values}
/>;
} else {
if (this.props.fieldSpec.length) {
return <InputArray
{...commonProps as InputArrayProps}
type={this.props.fieldSpec.value}
length={this.props.fieldSpec.length}
/>;
} else {
return <InputDynamicArray
{...commonProps as InputDynamicArrayProps}
fieldSpec={this.props.fieldSpec}
type={this.props.fieldSpec.value as InputDynamicArrayProps["type"]}
/>;
}
}
case "numberArray": return (
<InputDynamicArray
{...commonProps as InputDynamicArrayProps}
fieldSpec={this.props.fieldSpec}
type="number"
value={(Array.isArray(this.props.value) ? this.props.value : [this.props.value]) as (string | number | undefined)[]}
/>
);
case "colorArray": return (
<InputDynamicArray
{...commonProps as InputDynamicArrayProps}
fieldSpec={this.props.fieldSpec}
type="color"
value={(Array.isArray(this.props.value) ? this.props.value : [this.props.value]) as (string | number | undefined)[]}
/>
);
case "padding": return (
<InputArray
{...commonProps as InputArrayProps}
type="number"
value={(Array.isArray(this.props.value) ? this.props.value : [this.props.value]) as (string | number | undefined)[]}
length={4}
/>
);
default:
console.warn(`No proper field input for ${this.props.fieldName} type: ${this.props.fieldSpec?.type}`);
return null;
}
default: return null
}
}
+11 -11
View File
@@ -1,4 +1,4 @@
import React from "react";
import React from 'react'
export type InputStringProps = {
"data-wd-key"?: string
@@ -11,26 +11,26 @@ export type InputStringProps = {
required?: boolean
disabled?: boolean
spellCheck?: boolean
"aria-label"?: string
'aria-label'?: string
title?: string
};
type InputStringState = {
editing: boolean
value?: string
};
}
export default class InputString extends React.Component<InputStringProps, InputStringState> {
static defaultProps = {
onInput: () => {},
};
}
constructor(props: InputStringProps) {
super(props);
super(props)
this.state = {
editing: false,
value: props.value || ""
};
value: props.value || ''
}
}
static getDerivedStateFromProps(props: Readonly<InputStringProps>, state: InputStringState) {
@@ -47,17 +47,17 @@ export default class InputString extends React.Component<InputStringProps, Input
let classes;
if(this.props.multi) {
tag = "textarea";
tag = "textarea"
classes = [
"maputnik-string",
"maputnik-string--multi"
];
]
}
else {
tag = "input";
tag = "input"
classes = [
"maputnik-string"
];
]
}
if(this.props.disabled) {
+61 -37
View File
@@ -1,33 +1,57 @@
import React, { type JSX } from "react";
import InputString from "./InputString";
import SmallError from "./SmallError";
import { Trans, type WithTranslation, withTranslation } from "react-i18next";
import { type TFunction } from "i18next";
import { ErrorType, validate } from "../libs/urlopen";
import React from 'react'
import InputString from './InputString'
import SmallError from './SmallError'
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import { TFunction } from 'i18next';
function errorTypeToJsx(errorType: ErrorType | undefined, t: TFunction): JSX.Element | undefined {
switch (errorType) {
case ErrorType.EmptyHttpsProtocol:
return (
function validate(url: string, t: TFunction): JSX.Element | undefined {
if (url === "") {
return;
}
let error;
const getProtocol = (url: string) => {
try {
const urlObj = new URL(url);
return urlObj.protocol;
}
catch (_err) {
return undefined;
}
};
const protocol = getProtocol(url);
const isSsl = window.location.protocol === "https:";
if (!protocol) {
if (isSsl) {
error = (
<SmallError>
<Trans t={t}>Must provide protocol: <code>https://</code></Trans>
</SmallError>
);
case ErrorType.EmptyHttpOrHttpsProtocol:
return (
} else {
error = (
<SmallError>
<Trans t={t}>Must provide protocol: <code>http://</code> or <code>https://</code></Trans>
</SmallError>
);
case ErrorType.CorsError:
return (
<SmallError>
<Trans t={t}>CORS policy won&apos;t allow fetching resources served over http from https, use a <code>https://</code> domain</Trans>
</SmallError>
);
default:
return undefined;
}
}
else if (
protocol &&
protocol === "http:" &&
window.location.protocol === "https:"
) {
error = (
<SmallError>
<Trans t={t}>
CORS policy won&apos;t allow fetching resources served over http from https, use a <code>https://</code> domain
</Trans>
</SmallError>
);
}
return error;
}
export type FieldUrlProps = {
@@ -39,42 +63,42 @@ export type FieldUrlProps = {
onInput?(...args: unknown[]): unknown
multi?: boolean
required?: boolean
"aria-label"?: string
'aria-label'?: string
type?: string
className?: string
};
type InputUrlInternalProps = FieldUrlProps & WithTranslation;
type FieldUrlInternalProps = FieldUrlProps & WithTranslation;
type InputUrlState = {
error?: ErrorType
};
type FieldUrlState = {
error?: React.ReactNode
}
class InputUrlInternal extends React.Component<InputUrlInternalProps, InputUrlState> {
class FieldUrlInternal extends React.Component<FieldUrlInternalProps, FieldUrlState> {
static defaultProps = {
onInput: () => {},
};
}
constructor (props: InputUrlInternalProps) {
constructor (props: FieldUrlInternalProps) {
super(props);
this.state = {
error: validate(props.value),
error: validate(props.value, props.t),
};
}
onInput = (url: string) => {
this.setState({
error: validate(url),
error: validate(url, this.props.t),
});
if (this.props.onInput) this.props.onInput(url);
};
}
onChange = (url: string) => {
this.setState({
error: validate(url),
error: validate(url, this.props.t),
});
this.props.onChange(url);
};
}
render () {
return (
@@ -83,13 +107,13 @@ class InputUrlInternal extends React.Component<InputUrlInternalProps, InputUrlSt
{...this.props}
onInput={this.onInput}
onChange={this.onChange}
aria-label={this.props["aria-label"]}
aria-label={this.props['aria-label']}
/>
{errorTypeToJsx(this.state.error, this.props.t)}
{this.state.error}
</div>
);
}
}
const InputUrl = withTranslation()(InputUrlInternal);
export default InputUrl;
const FieldUrl = withTranslation()(FieldUrlInternal);
export default FieldUrl;
+230 -288
View File
@@ -1,199 +1,155 @@
import React, { type JSX } from "react";
import { Wrapper, Button, Menu, MenuItem } from "react-aria-menubutton";
import { Accordion } from "react-accessible-accordion";
import { MdMoreVert } from "react-icons/md";
import { IconContext } from "react-icons";
import { type BackgroundLayerSpecification, type LayerSpecification, type SourceSpecification } from "maplibre-gl";
import { v8 } from "@maplibre/maplibre-gl-style-spec";
import React, {type JSX} from 'react'
import PropTypes from 'prop-types'
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
import {Accordion} from 'react-accessible-accordion';
import {MdMoreVert} from 'react-icons/md'
import {BackgroundLayerSpecification, LayerSpecification, SourceSpecification} from 'maplibre-gl';
import FieldJson from "./FieldJson";
import FilterEditor from "./FilterEditor";
import PropertyGroup from "./PropertyGroup";
import LayerEditorGroup from "./LayerEditorGroup";
import FieldType from "./FieldType";
import FieldId from "./FieldId";
import FieldMinZoom from "./FieldMinZoom";
import FieldMaxZoom from "./FieldMaxZoom";
import FieldComment from "./FieldComment";
import FieldSource from "./FieldSource";
import FieldSourceLayer from "./FieldSourceLayer";
import { changeType, changeProperty } from "../libs/layer";
import { formatLayerId } from "../libs/format";
import { type WithTranslation, withTranslation } from "react-i18next";
import { type TFunction } from "i18next";
import { NON_SOURCE_LAYERS } from "../libs/non-source-layers";
import { type MappedError, type MappedLayerErrors, type OnMoveLayerCallback } from "../libs/definitions";
import FieldJson from './FieldJson'
import FilterEditor from './FilterEditor'
import PropertyGroup from './PropertyGroup'
import LayerEditorGroup from './LayerEditorGroup'
import FieldType from './FieldType'
import FieldId from './FieldId'
import FieldMinZoom from './FieldMinZoom'
import FieldMaxZoom from './FieldMaxZoom'
import FieldComment from './FieldComment'
import FieldSource from './FieldSource'
import FieldSourceLayer from './FieldSourceLayer'
import { changeType, changeProperty } from '../libs/layer'
import layout from '../config/layout.json'
import {formatLayerId} from '../libs/format';
import { WithTranslation, withTranslation } from 'react-i18next';
import { TFunction } from 'i18next';
type MaputnikLayoutGroup = {
id: string;
title: string;
type: string;
fields: string[];
};
function getLayoutForSymbolType(t: TFunction): MaputnikLayoutGroup[] {
const groups: MaputnikLayoutGroup[] = [];
groups.push({
title: t("General layout properties"),
id: "General_layout_properties",
type: "properties",
fields: Object.keys(v8["layout_symbol"]).filter(f => f.startsWith("symbol-"))
});
groups.push({
title: t("Text layout properties"),
id: "Text_layout_properties",
type: "properties",
fields: Object.keys(v8["layout_symbol"]).filter(f => f.startsWith("text-"))
});
groups.push({
title: t("Icon layout properties"),
id: "Icon_layout_properties",
type: "properties",
fields: Object.keys(v8["layout_symbol"]).filter(f => f.startsWith("icon-"))
});
groups.push({
title: t("Text paint properties"),
id: "Text_paint_properties",
type: "properties",
fields: Object.keys(v8["paint_symbol"]).filter(f => f.startsWith("text-"))
});
groups.push({
title: t("Icon paint properties"),
id: "Icon_paint_properties",
type: "properties",
fields: Object.keys(v8["paint_symbol"]).filter(f => f.startsWith("icon-"))
});
return groups;
function getLayoutForType(type: LayerSpecification["type"], t: TFunction) {
return layout[type] ? {
...layout[type],
groups: layout[type].groups.map(group => {
return {
...group,
id: group.title.replace(/ /g, "_"),
title: t(group.title)
};
}),
} : layout.invalid;
}
function getLayoutForType(type: LayerSpecification["type"], t: TFunction): MaputnikLayoutGroup[] {
if (Object.keys(v8.layer.type.values).indexOf(type) < 0) {
return [];
}
if (type === "symbol") {
return getLayoutForSymbolType(t);
}
const groups: MaputnikLayoutGroup[] = [];
if (Object.keys(v8["paint_" + type]).length > 0) {
groups.push({
title: t("Paint properties"),
id: "Paint_properties",
type: "properties",
fields: Object.keys(v8["paint_" + type]),
});
}
if (Object.keys(v8["layout_" + type]).length > 0) {
groups.push({
title: t("Layout properties"),
id: "Layout_properties",
type: "properties",
fields: Object.keys(v8["layout_" + type])
});
}
return groups;
}
function layoutGroups(layerType: LayerSpecification["type"], t: TFunction): { id: string, title: string, type: string, fields?: string[] }[] {
function layoutGroups(layerType: LayerSpecification["type"], t: TFunction): {id: string, title: string, type: string, fields?: string[]}[] {
const layerGroup = {
id: "layer",
title: t("Layer"),
type: "layer"
};
id: 'layer',
title: t('Layer'),
type: 'layer'
}
const filterGroup = {
id: "filter",
title: t("Filter"),
type: "filter"
};
id: 'filter',
title: t('Filter'),
type: 'filter'
}
const editorGroup = {
id: "jsoneditor",
title: t("JSON Editor"),
type: "jsoneditor"
};
id: 'jsoneditor',
title: t('JSON Editor'),
type: 'jsoneditor'
}
return [layerGroup, filterGroup]
.concat(getLayoutForType(layerType, t))
.concat([editorGroup]);
.concat(getLayoutForType(layerType, t).groups)
.concat([editorGroup])
}
type LayerEditorInternalProps = {
layer: LayerSpecification
sources: { [key: string]: SourceSpecification & { layers: string[] } }
vectorLayers: { [key: string]: any }
spec: any
onLayerChanged(index: number, layer: LayerSpecification): void
sources: {[key: string]: SourceSpecification}
vectorLayers: {[key: string]: any}
spec: object
onLayerChanged(...args: unknown[]): unknown
onLayerIdChange(...args: unknown[]): unknown
onMoveLayer: OnMoveLayerCallback
onMoveLayer(...args: unknown[]): unknown
onLayerDestroy(...args: unknown[]): unknown
onLayerCopy(...args: unknown[]): unknown
onLayerVisibilityToggle(...args: unknown[]): unknown
isFirstLayer?: boolean
isLastLayer?: boolean
layerIndex: number
errors?: MappedError[]
errors?: any[]
} & WithTranslation;
type LayerEditorState = {
editorGroups: { [keys: string]: boolean }
editorGroups: {[keys:string]: boolean}
};
/** Layer editor supporting multiple types of layers. */
class LayerEditorInternal extends React.Component<LayerEditorInternalProps, LayerEditorState> {
static defaultProps = {
onLayerChanged: () => { },
onLayerIdChange: () => { },
onLayerDestroyed: () => { },
};
onLayerChanged: () => {},
onLayerIdChange: () => {},
onLayerDestroyed: () => {},
}
static childContextTypes = {
reactIconBase: PropTypes.object
}
constructor(props: LayerEditorInternalProps) {
super(props);
super(props)
const editorGroups: { [keys: string]: boolean } = {};
for (const group of layoutGroups(this.props.layer.type, props.t)) {
editorGroups[group.title] = true;
}
//TODO: Clean this up and refactor into function
const editorGroups: {[keys:string]: boolean} = {}
layoutGroups(this.props.layer.type, props.t).forEach(group => {
editorGroups[group.title] = true
})
this.state = { editorGroups };
this.state = { editorGroups }
}
static getDerivedStateFromProps(props: Readonly<LayerEditorInternalProps>, state: LayerEditorState) {
const additionalGroups = { ...state.editorGroups };
const additionalGroups = { ...state.editorGroups }
for (const group of getLayoutForType(props.layer.type, props.t)) {
if (!(group.title in additionalGroups)) {
additionalGroups[group.title] = true;
getLayoutForType(props.layer.type, props.t).groups.forEach(group => {
if(!(group.title in additionalGroups)) {
additionalGroups[group.title] = true
}
}
})
return {
editorGroups: additionalGroups
};
}
getChildContext () {
return {
reactIconBase: {
size: 14,
color: '#8e8e8e',
}
}
}
changeProperty(group: keyof LayerSpecification | null, property: string, newValue: any) {
this.props.onLayerChanged(
this.props.layerIndex,
changeProperty(this.props.layer, group, property, newValue)
);
)
}
onGroupToggle(groupTitle: string, active: boolean) {
const changedActiveGroups = {
...this.state.editorGroups,
[groupTitle]: active,
};
}
this.setState({
editorGroups: changedActiveGroups
});
})
}
renderGroupType(type: string, fields?: string[]): JSX.Element {
let comment = "";
if (this.props.layer.metadata) {
comment = (this.props.layer.metadata as any)["maputnik:comment"];
let comment = ""
if(this.props.layer.metadata) {
comment = (this.props.layer.metadata as any)['maputnik:comment']
}
const { errors, layerIndex } = this.props;
const {errors, layerIndex} = this.props;
const errorData: MappedLayerErrors = {};
const errorData: {[key in LayerSpecification as string]: {message: string}} = {};
errors!.forEach(error => {
if (
error.parsed &&
@@ -204,109 +160,108 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
message: error.parsed.data.message
};
}
});
})
let sourceLayerIds;
const layer = this.props.layer as Exclude<LayerSpecification, BackgroundLayerSpecification>;
if (Object.prototype.hasOwnProperty.call(this.props.sources, layer.source)) {
sourceLayerIds = this.props.sources[layer.source].layers;
if(Object.prototype.hasOwnProperty.call(this.props.sources, layer.source)) {
sourceLayerIds = (this.props.sources[layer.source] as any).layers;
}
switch (type) {
case "layer": return <div>
<FieldId
value={this.props.layer.id}
wdKey="layer-editor.layer-id"
error={errorData.id}
onChange={newId => this.props.onLayerIdChange(this.props.layerIndex, this.props.layer.id, newId)}
switch(type) {
case 'layer': return <div>
<FieldId
value={this.props.layer.id}
wdKey="layer-editor.layer-id"
error={errorData.id}
onChange={newId => this.props.onLayerIdChange(this.props.layerIndex, this.props.layer.id, newId)}
/>
<FieldType
disabled={true}
error={errorData.type}
value={this.props.layer.type}
onChange={newType => this.props.onLayerChanged(
this.props.layerIndex,
changeType(this.props.layer, newType)
)}
/>
{this.props.layer.type !== 'background' && <FieldSource
error={errorData.source}
sourceIds={Object.keys(this.props.sources!)}
value={this.props.layer.source}
onChange={v => this.changeProperty(null, 'source', v)}
/>
}
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.props.layer.type) < 0 &&
<FieldSourceLayer
error={errorData['source-layer']}
sourceLayerIds={sourceLayerIds}
value={(this.props.layer as any)['source-layer']}
onChange={v => this.changeProperty(null, 'source-layer', v)}
/>
<FieldType
disabled={true}
error={errorData.type}
value={this.props.layer.type}
onChange={newType => this.props.onLayerChanged(
this.props.layerIndex,
changeType(this.props.layer, newType)
)}
/>
{this.props.layer.type !== "background" && <FieldSource
error={errorData.source}
sourceIds={Object.keys(this.props.sources!)}
value={this.props.layer.source}
onChange={v => this.changeProperty(null, "source", v)}
/>
}
{!NON_SOURCE_LAYERS.includes(this.props.layer.type) &&
<FieldSourceLayer
error={errorData["source-layer"]}
sourceLayerIds={sourceLayerIds}
value={(this.props.layer as any)["source-layer"]}
onChange={v => this.changeProperty(null, "source-layer", v)}
/>
}
<FieldMinZoom
error={errorData.minzoom}
value={this.props.layer.minzoom}
onChange={v => this.changeProperty(null, "minzoom", v)}
/>
<FieldMaxZoom
error={errorData.maxzoom}
value={this.props.layer.maxzoom}
onChange={v => this.changeProperty(null, "maxzoom", v)}
/>
<FieldComment
error={errorData.comment}
value={comment}
onChange={v => this.changeProperty("metadata", "maputnik:comment", v == "" ? undefined : v)}
/>
</div>;
case "filter": return <div>
<div className="maputnik-filter-editor-wrapper">
<FilterEditor
errors={errorData}
filter={(this.props.layer as any).filter}
properties={this.props.vectorLayers[(this.props.layer as any)["source-layer"]]}
onChange={f => this.changeProperty(null, "filter", f)}
/>
</div>
</div>;
case "properties":
return <PropertyGroup
}
<FieldMinZoom
error={errorData.minzoom}
value={this.props.layer.minzoom}
onChange={v => this.changeProperty(null, 'minzoom', v)}
/>
<FieldMaxZoom
error={errorData.maxzoom}
value={this.props.layer.maxzoom}
onChange={v => this.changeProperty(null, 'maxzoom', v)}
/>
<FieldComment
error={errorData.comment}
value={comment}
onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)}
/>
</div>
case 'filter': return <div>
<div className="maputnik-filter-editor-wrapper">
<FilterEditor
errors={errorData}
layer={this.props.layer}
groupFields={fields!}
spec={this.props.spec}
onChange={this.changeProperty.bind(this)}
/>;
case "jsoneditor":
return <FieldJson
lintType="layer"
value={this.props.layer}
onChange={(layer: LayerSpecification) => {
this.props.onLayerChanged(
this.props.layerIndex,
layer
);
}}
/>;
default: return <></>;
filter={(this.props.layer as any).filter}
properties={this.props.vectorLayers[(this.props.layer as any)['source-layer']]}
onChange={f => this.changeProperty(null, 'filter', f)}
/>
</div>
</div>
case 'properties':
return <PropertyGroup
errors={errorData}
layer={this.props.layer}
groupFields={fields!}
spec={this.props.spec}
onChange={this.changeProperty.bind(this)}
/>
case 'jsoneditor':
return <FieldJson
layer={this.props.layer}
onChange={(layer) => {
this.props.onLayerChanged(
this.props.layerIndex,
layer
);
}}
/>
default: return <></>
}
}
moveLayer(offset: number) {
this.props.onMoveLayer({
oldIndex: this.props.layerIndex,
newIndex: this.props.layerIndex + offset
});
newIndex: this.props.layerIndex+offset
})
}
render() {
const t = this.props.t;
const groupIds: string[] = [];
const layerType = this.props.layer.type;
const layerType = this.props.layer.type
const groups = layoutGroups(layerType, t).filter(group => {
return !(layerType === "background" && group.type === "source");
return !(layerType === 'background' && group.type === 'source')
}).map(group => {
const groupId = group.id;
groupIds.push(groupId);
@@ -319,103 +274,90 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
onActiveToggle={this.onGroupToggle.bind(this, group.title)}
>
{this.renderGroupType(group.type, group.fields)}
</LayerEditorGroup>;
});
</LayerEditorGroup>
})
const layout = this.props.layer.layout || {};
const layout = this.props.layer.layout || {}
const items: {
[key: string]: {
text: string,
handler: () => void,
disabled?: boolean,
wdKey?: string
}
} = {
const items: {[key: string]: {text: string, handler: () => void, disabled?: boolean}} = {
delete: {
text: t("Delete"),
handler: () => this.props.onLayerDestroy(this.props.layerIndex),
wdKey: "menu-delete-layer"
handler: () => this.props.onLayerDestroy(this.props.layerIndex)
},
duplicate: {
text: t("Duplicate"),
handler: () => this.props.onLayerCopy(this.props.layerIndex),
wdKey: "menu-duplicate-layer"
handler: () => this.props.onLayerCopy(this.props.layerIndex)
},
hide: {
text: (layout.visibility === "none") ? t("Show") : t("Hide"),
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex),
wdKey: "menu-hide-layer"
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex)
},
moveLayerUp: {
text: t("Move layer up"),
// Not actually used...
disabled: this.props.isFirstLayer,
handler: () => this.moveLayer(-1),
wdKey: "menu-move-layer-up"
handler: () => this.moveLayer(-1)
},
moveLayerDown: {
text: t("Move layer down"),
// Not actually used...
disabled: this.props.isLastLayer,
handler: () => this.moveLayer(+1),
wdKey: "menu-move-layer-down"
handler: () => this.moveLayer(+1)
}
};
}
function handleSelection(id: string, event: React.SyntheticEvent) {
event.stopPropagation();
items[id].handler();
}
return <IconContext.Provider value={{ size: "14px", color: "#8e8e8e" }}>
<section className="maputnik-layer-editor"
role="main"
aria-label={t("Layer editor")}
data-wd-key="layer-editor"
>
<header data-wd-key="layer-editor.header">
<div className="layer-header">
<h2 className="layer-header__title">
{t("Layer")}: {formatLayerId(this.props.layer.id)}
</h2>
<div className="layer-header__info">
<Wrapper
className='more-menu'
onSelection={(id, event) => handleSelection(id as string, event)}
closeOnSelection={false}
>
<Button
id="skip-target-layer-editor"
data-wd-key="skip-target-layer-editor"
className='more-menu__button'
title={"Layer options"}>
<MdMoreVert className="more-menu__button__svg" />
</Button>
<Menu>
<ul className="more-menu__menu">
{Object.keys(items).map((id) => {
const item = items[id];
return <li key={id}>
<MenuItem value={id} className='more-menu__menu__item' data-wd-key={item.wdKey}>
{item.text}
</MenuItem>
</li>;
})}
</ul>
</Menu>
</Wrapper>
</div>
return <section className="maputnik-layer-editor"
role="main"
aria-label={t("Layer editor")}
>
<header>
<div className="layer-header">
<h2 className="layer-header__title">
{t("Layer: {{layerId}}", { layerId: formatLayerId(this.props.layer.id) })}
</h2>
<div className="layer-header__info">
<Wrapper
className='more-menu'
onSelection={handleSelection}
closeOnSelection={false}
>
<Button
id="skip-target-layer-editor"
data-wd-key="skip-target-layer-editor"
className='more-menu__button'
title={"Layer options"}>
<MdMoreVert className="more-menu__button__svg" />
</Button>
<Menu>
<ul className="more-menu__menu">
{Object.keys(items).map((id) => {
const item = items[id];
return <li key={id}>
<MenuItem value={id} className='more-menu__menu__item'>
{item.text}
</MenuItem>
</li>
})}
</ul>
</Menu>
</Wrapper>
</div>
</div>
</header>
<Accordion
allowMultipleExpanded={true}
allowZeroExpanded={true}
preExpanded={groupIds}
>
{groups}
</Accordion>
</section>
</IconContext.Provider>;
</header>
<Accordion
allowMultipleExpanded={true}
allowZeroExpanded={true}
preExpanded={groupIds}
>
{groups}
</Accordion>
</section>
}
}
+19 -7
View File
@@ -1,11 +1,15 @@
import React from "react";
import {MdArrowDropDown, MdArrowDropUp} from "react-icons/md";
import React from 'react'
import Icon from '@mdi/react'
import {
mdiMenuDown,
mdiMenuUp
} from '@mdi/js';
import {
AccordionItem,
AccordionItemHeading,
AccordionItemButton,
AccordionItemPanel,
} from "react-accessible-accordion";
} from 'react-accessible-accordion';
type LayerEditorGroupProps = {
@@ -26,14 +30,22 @@ export default class LayerEditorGroup extends React.Component<LayerEditorGroupPr
onClick={_e => this.props.onActiveToggle(!this.props.isActive)}
>
<AccordionItemButton className="maputnik-layer-editor-group__button">
<span style={{flexGrow: 1, alignContent: "center"}}>{this.props.title}</span>
<MdArrowDropUp size={"2em"} className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--up"></MdArrowDropUp>
<MdArrowDropDown size={"2em"} className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--down"></MdArrowDropDown>
<span style={{flexGrow: 1}}>{this.props.title}</span>
<Icon
path={mdiMenuUp}
size={1}
className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--up"
/>
<Icon
path={mdiMenuDown}
size={1}
className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--down"
/>
</AccordionItemButton>
</AccordionItemHeading>
<AccordionItemPanel>
{this.props.children}
</AccordionItemPanel>
</AccordionItem>;
</AccordionItem>
}
}
+76 -103
View File
@@ -1,39 +1,27 @@
import React, {type JSX} from "react";
import classnames from "classnames";
import lodash from "lodash";
import {
DndContext,
PointerSensor,
useSensor,
useSensors,
closestCenter,
type DragEndEvent,
} from "@dnd-kit/core";
import {
SortableContext,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import React, {type JSX} from 'react'
import classnames from 'classnames'
import lodash from 'lodash';
import LayerListGroup from "./LayerListGroup";
import LayerListItem from "./LayerListItem";
import ModalAdd from "./modals/ModalAdd";
import LayerListGroup from './LayerListGroup'
import LayerListItem from './LayerListItem'
import ModalAdd from './ModalAdd'
import type {LayerSpecification, SourceSpecification} from "maplibre-gl";
import generateUniqueId from "../libs/document-uid";
import { findClosestCommonPrefix, layerPrefix } from "../libs/layer";
import { type WithTranslation, withTranslation } from "react-i18next";
import { type MappedError, type OnMoveLayerCallback } from "../libs/definitions";
import {SortEndHandler, SortableContainer} from 'react-sortable-hoc';
import type {LayerSpecification} from 'maplibre-gl';
import generateUniqueId from '../libs/document-uid';
import { findClosestCommonPrefix, layerPrefix } from '../libs/layer';
import { WithTranslation, withTranslation } from 'react-i18next';
type LayerListContainerProps = {
layers: LayerSpecification[]
selectedLayerIndex: number
onLayersChange(layers: LayerSpecification[]): unknown
onLayerSelect(index: number): void;
onLayerSelect(...args: unknown[]): unknown
onLayerDestroy?(...args: unknown[]): unknown
onLayerCopy(...args: unknown[]): unknown
onLayerVisibilityToggle(...args: unknown[]): unknown
sources: Record<string, SourceSpecification & {layers: string[]}>;
errors: MappedError[]
sources: object
errors: any[]
};
type LayerListContainerInternalProps = LayerListContainerProps & WithTranslation;
@@ -48,9 +36,9 @@ type LayerListContainerState = {
class LayerListContainerInternal extends React.Component<LayerListContainerInternalProps, LayerListContainerState> {
static defaultProps = {
onLayerSelect: () => {},
};
}
selectedItemRef: React.RefObject<any>;
scrollContainerRef: React.RefObject<HTMLElement | null>;
scrollContainerRef: React.RefObject<HTMLElement>;
constructor(props: LayerListContainerInternalProps) {
super(props);
@@ -65,7 +53,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
isOpen: {
add: false,
}
};
}
}
toggleModal(modalName: string) {
@@ -78,74 +66,74 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
...this.state.isOpen,
[modalName]: !this.state.isOpen[modalName]
}
});
})
}
toggleLayers = () => {
let idx = 0;
let idx = 0
const newGroups: {[key:string]: boolean} = {};
const newGroups: {[key:string]: boolean} = {}
this.groupedLayers().forEach(layers => {
const groupPrefix = layerPrefix(layers[0].id);
const lookupKey = [groupPrefix, idx].join("-");
const groupPrefix = layerPrefix(layers[0].id)
const lookupKey = [groupPrefix, idx].join('-')
if (layers.length > 1) {
newGroups[lookupKey] = this.state.areAllGroupsExpanded;
newGroups[lookupKey] = this.state.areAllGroupsExpanded
}
layers.forEach((_layer) => {
idx += 1;
});
idx += 1
})
});
this.setState({
collapsedGroups: newGroups,
areAllGroupsExpanded: !this.state.areAllGroupsExpanded
});
};
})
}
groupedLayers(): (LayerSpecification & {key: string})[][] {
const groups = [];
const groups = []
const layerIdCount = new Map();
for (let i = 0; i < this.props.layers.length; i++) {
const origLayer = this.props.layers[i];
const previousLayer = this.props.layers[i-1];
const previousLayer = this.props.layers[i-1]
layerIdCount.set(origLayer.id,
layerIdCount.has(origLayer.id) ? layerIdCount.get(origLayer.id) + 1 : 0
);
const layer = {
...origLayer,
key: `layers-list-${origLayer.id}-${layerIdCount.get(origLayer.id)}`,
};
}
if(previousLayer && layerPrefix(previousLayer.id) == layerPrefix(layer.id)) {
const lastGroup = groups[groups.length - 1];
lastGroup.push(layer);
const lastGroup = groups[groups.length - 1]
lastGroup.push(layer)
} else {
groups.push([layer]);
groups.push([layer])
}
}
return groups;
return groups
}
toggleLayerGroup(groupPrefix: string, idx: number) {
const lookupKey = [groupPrefix, idx].join("-");
const newGroups = { ...this.state.collapsedGroups };
const lookupKey = [groupPrefix, idx].join('-')
const newGroups = { ...this.state.collapsedGroups }
if(lookupKey in this.state.collapsedGroups) {
newGroups[lookupKey] = !this.state.collapsedGroups[lookupKey];
newGroups[lookupKey] = !this.state.collapsedGroups[lookupKey]
} else {
newGroups[lookupKey] = false;
newGroups[lookupKey] = false
}
this.setState({
collapsedGroups: newGroups
});
})
}
isCollapsed(groupPrefix: string, idx: number) {
const collapsed = this.state.collapsedGroups[[groupPrefix, idx].join("-")];
return collapsed === undefined ? true : collapsed;
const collapsed = this.state.collapsedGroups[[groupPrefix, idx].join('-')]
return collapsed === undefined ? true : collapsed
}
shouldComponentUpdate (nextProps: LayerListContainerProps, nextState: LayerListContainerState) {
@@ -177,7 +165,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
const out = {
...props
} as LayerListContainerProps & { layers?: any };
delete out["layers"];
delete out['layers'];
return out;
}
@@ -200,7 +188,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
const options = {
root: this.scrollContainerRef.current,
threshold: 1.0
};
}
const observer = new IntersectionObserver(entries => {
observer.unobserve(target);
if (entries.length > 0 && entries[0].intersectionRatio < 1) {
@@ -215,25 +203,25 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
render() {
const listItems: JSX.Element[] = [];
let idx = 0;
const listItems: JSX.Element[] = []
let idx = 0
const layersByGroup = this.groupedLayers();
layersByGroup.forEach(layers => {
const groupPrefix = layerPrefix(layers[0].id);
const groupPrefix = layerPrefix(layers[0].id)
if(layers.length > 1) {
const grp = <LayerListGroup
data-wd-key={[groupPrefix, idx].join("-")}
data-wd-key={[groupPrefix, idx].join('-')}
aria-controls={layers.map(l => l.key).join(" ")}
key={`group-${groupPrefix}-${idx}`}
title={groupPrefix}
isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex}
onActiveToggle={this.toggleLayerGroup.bind(this, groupPrefix, idx)}
/>;
listItems.push(grp);
/>
listItems.push(grp)
}
layers.forEach((layer, idxInGroup) => {
const groupIdx = findClosestCommonPrefix(this.props.layers, idx);
const groupIdx = findClosestCommonPrefix(this.props.layers, idx)
const layerError = this.props.errors.find(error => {
return (
@@ -250,10 +238,11 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
const listItem = <LayerListItem
className={classnames({
"maputnik-layer-list-item-collapsed": layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex,
"maputnik-layer-list-item-group-last": idxInGroup == layers.length - 1 && layers.length > 1,
"maputnik-layer-list-item--error": !!layerError
'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex,
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1,
'maputnik-layer-list-item--error': !!layerError
})}
index={idx}
key={layer.key}
id={layer.key}
layerId={layer.id}
@@ -266,17 +255,16 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
onLayerCopy={this.props.onLayerCopy.bind(this)}
onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(this)}
{...additionalProps}
/>;
listItems.push(listItem);
idx += 1;
});
});
/>
listItems.push(listItem)
idx += 1
})
})
const t = this.props.t;
return <section
className="maputnik-layer-list"
data-wd-key="layer-list"
role="complementary"
aria-label={t("Layers list")}
ref={this.scrollContainerRef}
@@ -286,10 +274,10 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
layers={this.props.layers}
sources={this.props.sources}
isOpen={this.state.isOpen.add}
onOpenToggle={this.toggleModal.bind(this, "add")}
onOpenToggle={this.toggleModal.bind(this, 'add')}
onLayersChange={this.props.onLayersChange}
/>
<header className="maputnik-layer-list-header" data-wd-key="layer-list.header">
<header className="maputnik-layer-list-header">
<span className="maputnik-layer-list-header-title">{t("Layers")}</span>
<span className="maputnik-space" />
<div className="maputnik-default-property">
@@ -310,7 +298,7 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
<div className="maputnik-default-property">
<div className="maputnik-multibutton">
<button
onClick={this.toggleModal.bind(this, "add")}
onClick={this.toggleModal.bind(this, 'add')}
data-wd-key="layer-list:add-layer"
className="maputnik-button maputnik-button-selected">
{t("Add Layer")}
@@ -326,40 +314,25 @@ class LayerListContainerInternal extends React.Component<LayerListContainerInter
{listItems}
</ul>
</div>
</section>;
</section>
}
}
const LayerListContainer = withTranslation()(LayerListContainerInternal);
const LayerListContainerSortable = SortableContainer((props: LayerListContainerProps) => <LayerListContainer {...props} />)
type LayerListProps = LayerListContainerProps & {
onMoveLayer: OnMoveLayerCallback
onMoveLayer: SortEndHandler
};
const LayerList: React.FC<LayerListProps> = (props) => {
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }));
const handleDragEnd = (event: DragEndEvent) => {
const {active, over} = event;
if (!over) return;
const oldIndex = props.layers.findIndex(layer => layer.id === active.id);
const newIndex = props.layers.findIndex(layer => layer.id === over.id);
if (oldIndex !== -1 && newIndex !== -1 && oldIndex !== newIndex) {
props.onMoveLayer({oldIndex, newIndex});
}
};
const layerIds = props.layers.map(layer => layer.id);
return (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={layerIds} strategy={verticalListSortingStrategy}>
<LayerListContainer {...props} />
</SortableContext>
</DndContext>
);
};
export default LayerList;
export default class LayerList extends React.Component<LayerListProps> {
render() {
return <LayerListContainerSortable
{...this.props}
helperClass='sortableHelper'
onSortEnd={this.props.onMoveLayer.bind(this)}
useDragHandle={true}
shouldCancelStart={() => false}
/>
}
}

Some files were not shown because too many files have changed in this diff Show More