Compare commits

..

2 Commits

Author SHA1 Message Date
dependabot[bot] ebece40308 Merge 3e7e1e6ce9 into c58191ff62 2025-02-20 14:04:30 +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
199 changed files with 10759 additions and 15530 deletions
+1
View File
@@ -8,3 +8,4 @@ assignees: ''
---
<!-- Thanks for reaching out! If you are having general Maputnik mapping questions, please asking them at https://gis.stackexchange.com/ using the 'maputnik' tag https://gis.stackexchange.com/questions/tagged/maputnik and read https://gis.stackexchange.com/help/how-to-ask before you do so (please keep in mind that you're asking there in a general GIS forum, not a dedicated support channel) -->
+1
View File
@@ -8,3 +8,4 @@
- [ ] Include before/after visuals or gifs if this PR includes visual changes.
- [ ] Write tests for all new functionality.
- [ ] Add an entry to `CHANGELOG.md` under the `## main` section.
-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
+66 -107
View File
@@ -7,148 +7,107 @@ 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:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
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
with:
node-version-file: '.nvmrc'
- run: npm ci
- run: npm run build
- run: npm run lint
- run: npm run lint-css
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- run: npm ci
- run: npm run build
- run: npm run lint
- run: npm run lint-css
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
with:
node-version-file: '.nvmrc'
- run: npm ci
- run: npm run build
- name: artifacts/maputnik
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: maputnik
path: dist
- 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@v4
with:
name: maputnik
path: dist
# Build and upload desktop CLI artifacts
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ^1.23.x
cache-dependency-path: desktop/go.sum
id: go
# Build and upload desktop CLI artifacts
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ^1.23.x
cache-dependency-path: desktop/go.sum
id: go
- name: Build desktop artifacts
run: npm run build-desktop
- name: Build desktop artifacts
run: npm run build-desktop
- name: Artifacts/linux
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: maputnik-linux
path: ./desktop/bin/linux/
- name: Artifacts/linux
uses: actions/upload-artifact@v4
with:
name: maputnik-linux
path: ./desktop/bin/linux/
- name: Artifacts/darwin
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: maputnik-darwin
path: ./desktop/bin/darwin/
- name: Artifacts/windows
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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
- name: Artifacts/darwin
uses: actions/upload-artifact@v4
with:
name: maputnik-darwin
path: ./desktop/bin/darwin/
- name: Artifacts/windows
uses: actions/upload-artifact@v4
with:
name: maputnik-windows
path: ./desktop/bin/windows/
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 }}
+16 -20
View File
@@ -8,48 +8,44 @@ 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'
- name: Install
run: npm ci
- name: Build
- name: Build
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
# publish docker to GitHub registry
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
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- run: docker build -t ghcr.io/maplibre/maputnik:main .
- run: docker push ghcr.io/maplibre/maputnik:main
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@v4
- run: docker build -t ghcr.io/maplibre/maputnik:main .
- run: docker push ghcr.io/maplibre/maputnik:main
+11 -13
View File
@@ -2,7 +2,7 @@ name: Release
on:
push:
branches: [ main ]
branches: [main]
workflow_dispatch:
jobs:
@@ -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:
@@ -102,3 +99,4 @@ jobs:
allowUpdates: true
draft: false
prerelease: false
-24
View File
@@ -1,24 +0,0 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
ci:
autoupdate_schedule: monthly
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-added-large-files
- id: check-executables-have-shebangs
- id: check-json
exclude: 'tsconfig(\.node)?\.json'
- id: check-shebang-scripts-are-executable
- id: check-symlinks
- id: check-toml
- id: check-yaml
args: [ --allow-multiple-documents ]
- id: destroyed-symlinks
- id: end-of-file-fixer
- id: mixed-line-ending
args: [ --fix=lf ]
- id: trailing-whitespace
-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.
+3 -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
@@ -82,3 +32,4 @@
## 1.7.0
- See release notes at https://maputnik.github.io/blog/2020/04/23/release-v1.7.0
+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/
+1
View File
@@ -20,3 +20,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+3 -9
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!
@@ -42,7 +36,7 @@ Maputnik is written in typescript and is using [React](https://github.com/facebo
We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule).
Check out our [Internationalization guide](./src/locales/README.md) for UI text related changes.
Check out our [Internationalization guide](./src/locales/README.md) for UI text related changes.
### Getting Involved
Join the #maplibre or #maputnik slack channel at OSMUS: get an invite at https://slack.openstreetmap.us/ Read the the below guide in order to get familiar with how we do things around here.
@@ -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*:
+1 -1
View File
@@ -1,6 +1,6 @@
# Build Scripts
This folder holds common build scripts used by some of the Github workflows.
This folder holds common build scripts used by some of the Github workflows.
The scripts are borrowed from [maplibre/maplibre-gl-js](https://github.com/maplibre/maplibre-gl-js/tree/bc70bc559cea5c987fa1b79fd44766cef68bbe28/build).
+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');
});
});
});
+2 -37
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");
+12 -190
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,15 +239,15 @@ describe("modals", () => {
it("include API key when change renderer", () => {
when.click("modal:settings.close-modal");
it("inlcude API key when change renderer", () => {
when.click("modal:settings.close-modal")
when.click("nav:open");
get.elementByAttribute("aria-label", "MapTiler Basic").should("exist").click();
when.wait(1000);
when.click("nav:settings");
get.elementByAttribute('aria-label', "MapTiler Basic").should('exist').click();
when.click("nav:settings");
when.select("modal:settings.maputnik:renderer", "mlgljs");
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
"mlgljs"
@@ -308,7 +259,7 @@ describe("modals", () => {
);
given.intercept("https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=*", "tileRequest", "GET");
when.select("modal:settings.maputnik:renderer", "mlgljs");
then(get.inputValue("modal:settings.maputnik:renderer")).shouldEqual(
"mlgljs"
@@ -318,139 +269,10 @@ describe("modals", () => {
when.waitForResponse("tileRequest").its("request").its("url").should("include", `https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=${tokens.openmaptiles}`);
when.waitForResponse("tileRequest").its("request").its("url").should("include", `https://api.maptiler.com/tiles/v3-openmaptiles/tiles.json?key=${tokens.openmaptiles}`);
});
});
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();
});
});
});
@@ -15,3 +15,4 @@
}
]
}
@@ -1,38 +0,0 @@
{
"id": "test-style",
"version": 8,
"name": "Test Style",
"metadata": {
"maputnik:renderer": "mlgljs"
},
"sources": {
"example": {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features":[{
"type": "Feature",
"properties": {
"name": "Dinagat Islands"
},
"geometry":{
"type": "Point",
"coordinates": [125.6, 10.1]
}
}]
}
}
},
"glyphs": "https://www.glyph-server.com/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": [
{
"id": "label",
"type": "symbol",
"source": "example",
"layout": {
"text-font": ["Font"]
}
}
]
}
@@ -1,19 +0,0 @@
{
"id": "test-style",
"center": [50,50],
"zoom": 5,
"version": 8,
"name": "Test Style",
"sources": {
"rectangles": {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": []
}
}
},
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": []
}
@@ -1,17 +0,0 @@
{
"id": "test-style",
"center": [0,51],
"zoom": 7,
"version": 8,
"name": "Test Style",
"sources": {
"rectangles": {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": []
}
}
},
"layers": []
}
+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": []
}
+1 -1
View File
@@ -31,4 +31,4 @@
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": []
}
}
+1 -1
View File
@@ -26,4 +26,4 @@
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
"layers": []
}
}
+1 -1
View File
@@ -15,4 +15,4 @@
"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
}
}
]
}
+1 -1
View File
@@ -34,4 +34,4 @@
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }
// }
-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__"
}
});
+4662 -7468
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.2",
"cypress-plugin-tab": "^1.0.5",
"eslint": "^9.19.0",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.18",
"i18next-parser": "^9.1.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.83.4",
"stylelint": "^16.14.1",
"stylelint-config-recommended-scss": "^14.1.0",
"stylelint-scss": "^6.11.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.22.0",
"uuid": "^11.0.5",
"vite": "^6.1.0",
"vite-plugin-istanbul": "^6.0.2"
}
}
+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>
}
}
+17 -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,18 @@ 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;
+7 -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,15 @@ 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>
);
)
}
}
}
+5 -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,8 @@ 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 -9
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,11 @@ 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 -9
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,11 @@ 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 -9
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,11 @@ 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 -9
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,11 @@ 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 -9
View File
@@ -1,16 +1,16 @@
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;
+10 -11
View File
@@ -1,8 +1,9 @@
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 & {
type FieldEnumProps = InputEnumProps & {
label?: string;
fieldSpec?: {
doc: string
@@ -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 -230
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,301 @@ 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 -5
View File
@@ -1,11 +1,13 @@
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 class FieldJson extends React.Component<FieldJsonProps> {
render() {
return <InputJson {...this.props} />
}
}
export default FieldJson;
+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 -9
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,11 @@ 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;
}
}
+10 -9
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,12 @@ 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;
+25 -22
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
label={t("Source Layer")}
fieldSpec={latest.layer["source-layer"]}
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']}
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 -10
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,13 @@ type FieldUrlProps = InputUrlProps & {
};
const FieldUrl: React.FC<FieldUrlProps> = (props) => {
return (
<Block label={props.label} fieldSpec={props.fieldSpec}>
<InputUrl {...props} />
</Block>
);
};
export default class FieldUrl extends React.Component<FieldUrlProps> {
render () {
return (
<Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
<InputUrl {...this.props} />
</Block>
);
}
}
export default FieldUrl;
+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;
}
}
+57 -53
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 (
@@ -253,8 +255,8 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
value={combiningOp}
onChange={(v: [string, any]) => this.onFilterPartChanged(0, v)}
options={[
["all", t("every filter matches")],
["none", t("no filter matches")],
["all", t("every filter matches")],
["none", t("no filter matches")],
["any", t("any filter matches")]
]}
/>
@@ -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>
}
}
+15
View File
@@ -0,0 +1,15 @@
import React from 'react'
import IconBase from 'react-icon-base'
export default class IconBackground extends React.Component {
render() {
return (
<IconBase viewBox="0 0 20 20" {...this.props}>
<path d="m 1.821019,10.255581 7.414535,5.020197 c 0.372277,0.25206 0.958697,0.239771 1.30985,-0.02745 L 17.539255,9.926162 C 17.89041,9.658941 17.873288,9.238006 17.501015,8.985946 L 10.08648,3.9657402 C 9.714204,3.7136802 9.127782,3.7259703 8.776627,3.9931918 L 1.782775,9.315365 c -0.3511551,0.267221 -0.3340331,0.688156 0.03824,0.940216 z" />
</IconBase>
)
}
}
+15
View File
@@ -0,0 +1,15 @@
import React from 'react'
import IconBase from 'react-icon-base'
export default class IconCircle extends React.Component {
render() {
return (
<IconBase viewBox="0 0 20 20" {...this.props}>
<path transform="translate(2 2)" d="M7.5,0C11.6422,0,15,3.3578,15,7.5S11.6422,15,7.5,15 S0,11.6422,0,7.5S3.3578,0,7.5,0z M7.5,1.6666c-3.2217,0-5.8333,2.6117-5.8333,5.8334S4.2783,13.3334,7.5,13.3334 s5.8333-2.6117,5.8333-5.8334S10.7217,1.6666,7.5,1.6666z"></path>
</IconBase>
)
}
}
+15
View File
@@ -0,0 +1,15 @@
import React from 'react'
import IconBase from 'react-icon-base'
export default class IconFill extends React.Component {
render() {
return (
<IconBase viewBox="0 0 20 20" {...this.props}>
<path d="M 2.84978,9.763512 9.462149,4.7316391 16.47225,9.478015 9.859886,14.509879 2.84978,9.763512 m -1.028761,0.492069 7.414535,5.020197 c 0.372277,0.25206 0.958697,0.239771 1.30985,-0.02745 L 17.539255,9.926162 C 17.89041,9.658941 17.873288,9.238006 17.501015,8.985946 L 10.08648,3.9657402 C 9.714204,3.7136802 9.127782,3.7259703 8.776627,3.9931918 L 1.782775,9.315365 c -0.3511551,0.267221 -0.3340331,0.688156 0.03824,0.940216 l 0,0 z" />
</IconBase>
)
}
}
+24 -24
View File
@@ -1,33 +1,33 @@
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;
+15
View File
@@ -0,0 +1,15 @@
import React from 'react'
import IconBase from 'react-icon-base'
export default class IconLine extends React.Component {
render() {
return (
<IconBase viewBox="0 0 20 20" {...this.props}>
<path d="M 12.34,1.29 C 12.5114,1.1076 12.7497,1.0029 13,1 13.5523,1 14,1.4477 14,2 14.0047,2.2478 13.907,2.4866 13.73,2.66 9.785626,6.5516986 6.6148407,9.7551593 2.65,13.72 2.4793,13.8963 2.2453,13.9971 2,14 1.4477,14 1,13.5523 1,13 0.9953,12.7522 1.093,12.5134 1.27,12.34 4.9761967,8.7018093 9.0356422,4.5930579 12.34,1.29 Z" transform="translate(2,2)" />
</IconBase>
)
}
}
+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} />
)
}
}
+18
View File
@@ -0,0 +1,18 @@
import React from 'react'
import IconBase from 'react-icon-base'
export default class IconSymbol extends React.Component {
render() {
return (
<IconBase viewBox="0 0 20 20" {...this.props}>
<g transform="matrix(1.2718518,0,0,1.2601269,16.559526,-7.4065264)">
<path d="m -9.7959773,11.060163 c -0.3734787,-0.724437 -0.3580577,-1.2147051 -0.00547,-1.8767873 l 8.6034029,-0.019416 c 0.39670292,0.6865644 0.38365934,1.4750693 -0.011097,1.8864953 l -3.1359613,-0.0033 -0.013695,7.1305 c -0.4055357,0.397083 -1.3146432,0.397083 -1.7769191,-0.02274 l 0.030226,-7.104422 z" />
</g>
</IconBase>
)
}
}
+22 -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,16 @@ 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");
});
});
+83 -97
View File
@@ -1,116 +1,102 @@
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>
);
}
}
+4 -4
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>
}
}
}
+6 -6
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>
}
}
}
+30 -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,7 @@ export default class InputColor extends React.Component<InputColorProps> {
value={this.props.value ? this.props.value : ""}
onChange={(e) => this.onChange(e.target.value)}
/>
</div>;
</div>
}
}
+48 -57
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);
}
@@ -70,47 +67,40 @@ class InputDynamicArrayInternal extends React.Component<InputDynamicArrayInterna
const t = this.props.t;
const i18nProps = { t, i18n: this.props.i18n, tReady: this.props.tReady };
const inputs = this.values.map((v, i) => {
const deleteValueBtn= <DeleteValueInputButton
const deleteValueBtn= <DeleteValueInputButton
onClick={this.deleteValue.bind(this, i)}
{...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,7 @@ class DeleteValueInputButton extends React.Component<DeleteValueInputButtonProps
<FieldDocLabel
label={<MdDelete />}
/>
</InputButton>;
</InputButton>
}
}
+12 -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,16 @@ 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>
}
}

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