mirror of
https://github.com/maputnik/editor.git
synced 2026-08-31 17:37:25 +00:00
Merge branch 'main' into nginx
This commit is contained in:
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"plugins": [
|
||||
"static-fs",
|
||||
"react-hot-loader/babel",
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/transform-runtime"
|
||||
],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": [
|
||||
["istanbul", {
|
||||
"exclude": ["node_modules/**", "test/**"]
|
||||
}]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2020": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
"plugin:react-hooks/recommended",
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"dist"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"settings": {
|
||||
"react": { "version": "16.4" }
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"react-refresh"],
|
||||
"rules": {
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ "allowConstantExport": true }
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{ "argsIgnorePattern": "^_" }
|
||||
],
|
||||
"no-unused-vars": "off",
|
||||
"react/prop-types": ["off"],
|
||||
// Disable no-undef. It's covered by @typescript-eslint
|
||||
"no-undef": "off",
|
||||
"indent": ["error", 2]
|
||||
},
|
||||
"globals": {
|
||||
"global": "readonly"
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ assignees: ''
|
||||
|
||||
<!-- Thanks for your feedback! Please complete the following information: -->
|
||||
|
||||
**Maputnik version**:<!-- e.g v1.7.0, master -->
|
||||
**Maputnik version**:<!-- e.g v1.7.0, main -->
|
||||
**Browser**:
|
||||
**OS**:<!-- (Windows, macOS, Linux) -->
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 2
|
||||
versioning-strategy: increase
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 2
|
||||
versioning-strategy: increase
|
||||
+41
-101
@@ -2,44 +2,24 @@ name: ci
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
|
||||
# post a comment linking to codesandbox with the current branch
|
||||
# meta-demo-comment:
|
||||
# name: meta/demo-comment
|
||||
# runs-on: ubuntu-latest
|
||||
|
||||
# if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# steps:
|
||||
# - uses: unsplash/comment-on-pr@v1.2.0
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# with:
|
||||
# msg: "Demo: <https://codesandbox.io/embed/github/${{ github.repository }}/tree/${{ github.head_ref }}?view=preview>"
|
||||
|
||||
build-docker:
|
||||
name: build/docker
|
||||
runs-on: ${{ matrix.os }}
|
||||
name: build docker
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:master .
|
||||
- uses: actions/checkout@v4
|
||||
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:main .
|
||||
|
||||
# build the editor
|
||||
build-node:
|
||||
name: "build/node@${{ matrix.node-version }} (${{ matrix.os }})"
|
||||
name: "build on ${{ matrix.os }}"
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||
@@ -48,13 +28,12 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
node-version: [12.x, 14.x, 16.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version-file: '.nvmrc'
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.npm
|
||||
@@ -63,28 +42,21 @@ jobs:
|
||||
${{ runner.os }}-node-
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm run lint
|
||||
- run: npm run lint-css
|
||||
|
||||
|
||||
|
||||
build-artifacts:
|
||||
name: "build/artifacts (${{ matrix.os }})"
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
GOPATH: ${{ github.workspace }}
|
||||
GO111MODULE: off
|
||||
|
||||
name: "build artifacts"
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
node-version: [16.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version-file: '.nvmrc'
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.npm
|
||||
@@ -98,13 +70,7 @@ jobs:
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: editor
|
||||
path: build/build
|
||||
- run: npm run profiling-build
|
||||
- name: artifacts/editor-profiling
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: editor-profiling
|
||||
path: build/profiling
|
||||
path: dist
|
||||
- name: artifacts/storybook
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
@@ -113,16 +79,16 @@ jobs:
|
||||
|
||||
# Build and upload desktop CLI artifacts
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ^1.14.x
|
||||
go-version: ^1.19.x
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: maputnik/desktop
|
||||
ref: v1.0.7
|
||||
ref: master
|
||||
path: ./src/github.com/maputnik/desktop/
|
||||
|
||||
- name: Make
|
||||
@@ -145,53 +111,27 @@ jobs:
|
||||
with:
|
||||
name: maputnik-windows
|
||||
path: ./src/github.com/maputnik/desktop/bin/windows/
|
||||
|
||||
# build and test the editor
|
||||
test_selenium_standalone:
|
||||
name: "test/standalone-${{ matrix.browser }} (${{ matrix.os }})"
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||
|
||||
|
||||
e2e-tests:
|
||||
name: "E2E tests using ${{ matrix.browser }}"
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
node-version: [16]
|
||||
browser: [chrome, firefox]
|
||||
|
||||
container:
|
||||
image: node:${{ matrix.node-version }}
|
||||
options: --network-alias testhost
|
||||
|
||||
services:
|
||||
selenium:
|
||||
# geckodriver-0.31 seems to have problems as of 2022 May 1
|
||||
image: selenium/standalone-${{ matrix.browser == 'firefox' && 'firefox:99.0-geckodriver-0.30-20220427' || matrix.browser }}
|
||||
ports:
|
||||
- 4444:4444
|
||||
options: --shm-size=2gb
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
- run: npm ci
|
||||
- run: BROWSER=${{ matrix.browser }} TEST_NETWORK=testhost DOCKER_HOST=selenium npm run test
|
||||
- if: ${{ matrix.browser == 'chrome' }}
|
||||
run: ./node_modules/.bin/istanbul report --include build/coverage/coverage.json --dir build/coverage html lcov
|
||||
- if: ${{ matrix.browser == 'chrome' }}
|
||||
name: artifacts/coverage
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: coverage
|
||||
path: build/coverage
|
||||
- name: artifacts/screenshots
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: screenshots-${{ matrix.browser }}
|
||||
path: build/screenshots
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- run: npm ci
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v6
|
||||
with:
|
||||
build: npm run build
|
||||
start: npm run start
|
||||
browser: ${{ matrix.browser }}
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ${{ github.workspace }}/.nyc_output/out.json
|
||||
verbose: true
|
||||
|
||||
@@ -2,13 +2,12 @@ name: deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
branches: [ main ]
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
# publish docker to github registry
|
||||
# publish docker to GitHub registry
|
||||
deploy-docker:
|
||||
name: deploy/docker
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -21,8 +20,7 @@ jobs:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login docker.pkg.github.com -u orangemug --password-stdin
|
||||
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:master .
|
||||
- run: docker push docker.pkg.github.com/maputnik/editor/editor:master
|
||||
|
||||
- run: docker build -t docker.pkg.github.com/maputnik/editor/editor:main .
|
||||
- run: docker push docker.pkg.github.com/maputnik/editor/editor:main
|
||||
|
||||
@@ -14,6 +14,7 @@ lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
@@ -33,3 +34,5 @@ public
|
||||
/errorShots
|
||||
/old
|
||||
/build
|
||||
/cypress/screenshots
|
||||
/dist/
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"all": true,
|
||||
"extends": "@istanbuljs/nyc-config-typescript",
|
||||
"check-coverage": false,
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": [
|
||||
"cypress/**/*.*",
|
||||
"**/*.d.ts",
|
||||
"**/*.cy.tsx",
|
||||
"**/*.cy.ts",
|
||||
"./coverage/**",
|
||||
"./cypress/**",
|
||||
"./dist/**",
|
||||
"node_modules"
|
||||
],
|
||||
"report-dir": "coverage",
|
||||
"reporter": ["json", "lcov", "json-summary"]
|
||||
}
|
||||
+8
-23
@@ -1,24 +1,9 @@
|
||||
const rules = require('../config/webpack.rules');
|
||||
|
||||
module.exports = {
|
||||
stories: ['../stories/**/*.stories.js'],
|
||||
addons: [
|
||||
'@storybook/addon-actions',
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-a11y/register',
|
||||
'@storybook/addon-storysource',
|
||||
],
|
||||
webpackFinal: async config => {
|
||||
// do mutation to the config
|
||||
console.log("config.module", config.module);
|
||||
|
||||
return {
|
||||
...config,
|
||||
module: {
|
||||
rules: [
|
||||
...rules,
|
||||
]
|
||||
}
|
||||
};
|
||||
},
|
||||
const config = {
|
||||
stories: ['../stories/**/*.stories.jsx'],
|
||||
addons: ['@storybook/addon-actions', '@storybook/addon-links', '@storybook/addon-a11y/register', '@storybook/addon-storysource'],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {}
|
||||
}
|
||||
};
|
||||
export default config;
|
||||
+4
-7
@@ -1,4 +1,4 @@
|
||||
FROM node:10 as builder
|
||||
FROM node:18 as builder
|
||||
WORKDIR /maputnik
|
||||
|
||||
# Only copy package.json to prevent npm install from running on every build
|
||||
@@ -6,14 +6,11 @@ COPY package.json package-lock.json ./
|
||||
RUN npm install
|
||||
|
||||
# Build maputnik
|
||||
# TODO: we should also do a npm run test here (needs more dependencies)
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# Create a clean nginx-alpine slim image with just the build results
|
||||
FROM nginx:alpine-slim
|
||||
|
||||
# Create a clean nginx-alpine image with just the build results
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY --from=builder /maputnik/build/build /usr/share/nginx/html/
|
||||
COPY --from=builder /maputnik/dist /usr/share/nginx/html/
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
[github-action-ci]: https://github.com/maputnik/editor/actions?query=workflow%3Aci
|
||||
[license]: https://tldrlegal.com/license/mit-license
|
||||
|
||||
A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/mapbox-gl-style-spec/)
|
||||
A free and open visual editor for the [MapLibre GL styles](https://maplibre.org/maplibre-style-spec/)
|
||||
targeted at developers and map designers.
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ The documentation can be found in the [Wiki](https://github.com/maputnik/editor/
|
||||
|
||||
## Develop
|
||||
|
||||
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react) and [Mapbox GL JS](https://www.mapbox.com/mapbox-gl-js/api/).
|
||||
Maputnik is written in ES6 and is using [React](https://github.com/facebook/react) and [MapLibre GL JS](https://maplibre.org/projects/maplibre-gl-js/).
|
||||
|
||||
We ensure building and developing Maputnik works with the [current active LTS Node.js version and above](https://github.com/nodejs/Release#release-schedule).
|
||||
|
||||
@@ -49,18 +49,14 @@ npm install
|
||||
npm run start
|
||||
```
|
||||
|
||||
If you want Maputnik to be accessible externally use the [`--host` option](https://webpack.js.org/configuration/dev-server/#devserverhost):
|
||||
If you want Maputnik to be accessible externally use the [`--host` option](https://vitejs.dev/config/server-options.html#server-host):
|
||||
|
||||
```bash
|
||||
# start externally accessible dev server
|
||||
npm run start -- --host 0.0.0.0
|
||||
```
|
||||
|
||||
The build process will watch for changes to the filesystem, rebuild and autoreload the editor. However note this from the [webpack-dev-server docs](https://webpack.js.org/configuration/dev-server/):
|
||||
|
||||
> webpack uses the file system to get notified of file changes. In some cases this does not work. For example, when using Network File System (NFS). Vagrant also has a lot of problems with this. ([snippet source](https://webpack.js.org/configuration/dev-server/#devserverwatchoptions-))
|
||||
|
||||
To enable polling add `export WEBPACK_DEV_SERVER_POLLING=1` to your environment.
|
||||
The build process will watch for changes to the filesystem, rebuild and autoreload the editor.
|
||||
|
||||
```
|
||||
npm run build
|
||||
@@ -76,9 +72,9 @@ npm run lint-styles
|
||||
|
||||
|
||||
## Tests
|
||||
For testing we use [webdriverio](https://webdriver.io) and [selenium-standalone](https://github.com/webdriverio/selenium-standalone).
|
||||
For E2E testing we use [Cypress](https://www.cypress.io/)
|
||||
|
||||
[selenium-standalone](https://github.com/webdriverio/selenium-standalone) starts a server that will launch browsers on your local machine. You need to have Java installed on your machine as well as *chrome* or *firefox*.
|
||||
[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*:
|
||||
|
||||
@@ -87,10 +83,16 @@ npm run test
|
||||
```
|
||||
or *firefox*:
|
||||
```
|
||||
BROWSER=firefox npm run test
|
||||
npm run test -- --browser firefox
|
||||
```
|
||||
|
||||
After some time you should see a browser launch which will be automated by the test runner.
|
||||
See the following docs for more info: (Launching Browsers)[https://docs.cypress.io/guides/guides/launching-browsers]
|
||||
|
||||
You can also see the tests as they run or select which suites to run by executing:
|
||||
|
||||
```
|
||||
npm run cy:open
|
||||
```
|
||||
|
||||
|
||||
## Related Projects
|
||||
@@ -155,6 +157,5 @@ Sina Martinelli, Nicholas Doiron, Neil Cawse, Urs42, Benedikt Groß, Manuel Roth
|
||||
|
||||
Maputnik is [licensed under MIT](LICENSE) and is Copyright (c) Lukas Martinelli and contributors.
|
||||
|
||||
**Disclaimer** This project is not affiliated with Mapbox or Mapbox Studio. It is an independent style editor for the
|
||||
open source technology in the Mapbox GL ecosystem.
|
||||
**Disclaimer** This is an independent style editor.
|
||||
As contributor please take extra care of not violating any Mapbox trademarks. Do not get inspired by Mapbox Studio and make your own decisions for a good style editor.
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
var webpack = require("webpack");
|
||||
var WebpackDevServer = require("webpack-dev-server");
|
||||
var webpackConfig = require("./webpack.config");
|
||||
var testConfig = require("../test/config/specs");
|
||||
var artifacts = require("../test/artifacts");
|
||||
|
||||
|
||||
var server;
|
||||
var SCREENSHOT_PATH = artifacts.pathSync("screenshots");
|
||||
|
||||
exports.config = {
|
||||
runner: 'local',
|
||||
path: '/wd/hub',
|
||||
specs: [
|
||||
'./test/functional/index.js'
|
||||
],
|
||||
maxInstances: 10,
|
||||
capabilities: [
|
||||
{
|
||||
maxInstances: 5,
|
||||
browserName: (process.env.BROWSER || 'chrome'),
|
||||
}
|
||||
],
|
||||
// geckodriver-0.31 seems to have problems as of 2022 May 1
|
||||
services: process.env.DOCKER_HOST ? [] : [ ['selenium-standalone', { drivers: { firefox: '0.30.0', chrome: 'latest' } } ] ],
|
||||
logLevel: 'info',
|
||||
bail: 0,
|
||||
screenshotPath: SCREENSHOT_PATH,
|
||||
hostname: process.env.DOCKER_HOST || "0.0.0.0",
|
||||
framework: 'mocha',
|
||||
reporters: ['spec'],
|
||||
mochaOpts: {
|
||||
ui: 'bdd',
|
||||
// Because we don't know how long the initial build will take...
|
||||
timeout: 4*60*1000,
|
||||
},
|
||||
onPrepare: async function (config, capabilities) {
|
||||
webpackConfig.devServer.host = testConfig.testNetwork;
|
||||
webpackConfig.devServer.port = testConfig.port;
|
||||
const compiler = webpack(webpackConfig);
|
||||
server = new WebpackDevServer(webpackConfig.devServer, compiler);
|
||||
await server.start();
|
||||
},
|
||||
onComplete: async function (exitCode, config, capabilities) {
|
||||
await server.stop();
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
"use strict";
|
||||
var path = require('path');
|
||||
var rules = require('./webpack.rules');
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
var HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin');
|
||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
|
||||
const HOST = process.env.HOST || "127.0.0.1";
|
||||
const PORT = process.env.PORT || "8888";
|
||||
|
||||
module.exports = {
|
||||
target: 'web',
|
||||
mode: 'development',
|
||||
entry: [
|
||||
`webpack-dev-server/client?http://${HOST}:${PORT}`,
|
||||
`webpack/hot/only-dev-server`,
|
||||
`./src/index.jsx` // Your appʼs entry point
|
||||
],
|
||||
devtool: process.env.WEBPACK_DEVTOOL || 'cheap-module-source-map',
|
||||
output: {
|
||||
path: path.join(__dirname, '..', 'public'),
|
||||
filename: 'bundle.js'
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx']
|
||||
},
|
||||
module: {
|
||||
noParse: [
|
||||
/mapbox-gl\/dist\/mapbox-gl.js/
|
||||
],
|
||||
rules: rules
|
||||
},
|
||||
node: {
|
||||
fs: "empty",
|
||||
net: 'empty',
|
||||
tls: 'empty'
|
||||
},
|
||||
devServer: {
|
||||
// enable HMR
|
||||
hot: true,
|
||||
// serve index.html in place of 404 responses to allow HTML5 history
|
||||
historyApiFallback: true,
|
||||
port: PORT,
|
||||
host: HOST,
|
||||
watchFiles: {
|
||||
options: {
|
||||
// Disabled polling by default as it causes lots of CPU usage and hence drains laptop batteries. To enable polling add WEBPACK_DEV_SERVER_POLLING to your environment
|
||||
// See <https://webpack.js.org/configuration/watch/#watchoptions-poll> for details
|
||||
usePolling: (!!process.env.WEBPACK_DEV_SERVER_POLLING ? true : false),
|
||||
watch: false
|
||||
}
|
||||
}
|
||||
},
|
||||
optimization: {
|
||||
noEmitOnErrors: true,
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Maputnik',
|
||||
template: './src/template.html'
|
||||
}),
|
||||
new HtmlWebpackInlineSVGPlugin({
|
||||
runPreEmit: true,
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: './src/manifest.json',
|
||||
to: 'manifest.json'
|
||||
}
|
||||
]
|
||||
})
|
||||
]
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
var webpack = require('webpack');
|
||||
var path = require('path');
|
||||
var rules = require('./webpack.rules');
|
||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
var HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin');
|
||||
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
|
||||
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
var artifacts = require("../test/artifacts");
|
||||
|
||||
var OUTPATH = artifacts.pathSync("/build");
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
app: './src/index.jsx',
|
||||
},
|
||||
output: {
|
||||
path: OUTPATH,
|
||||
filename: '[name].[contenthash].js',
|
||||
chunkFilename: '[contenthash].js'
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.jsx']
|
||||
},
|
||||
module: {
|
||||
noParse: [
|
||||
/mapbox-gl\/dist\/mapbox-gl.js/
|
||||
],
|
||||
rules: rules
|
||||
},
|
||||
node: {
|
||||
fs: "empty",
|
||||
net: 'empty',
|
||||
tls: 'empty'
|
||||
},
|
||||
plugins: [
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
new WebpackCleanupPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
NODE_ENV: '"production"'
|
||||
}
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './src/template.html',
|
||||
title: 'Maputnik'
|
||||
}),
|
||||
new HtmlWebpackInlineSVGPlugin({
|
||||
runPreEmit: true,
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: './src/manifest.json',
|
||||
to: 'manifest.json'
|
||||
}
|
||||
]
|
||||
}),
|
||||
new BundleAnalyzerPlugin({
|
||||
analyzerMode: 'static',
|
||||
defaultSizes: 'gzip',
|
||||
openAnalyzer: false,
|
||||
generateStatsFile: true,
|
||||
reportFilename: 'bundle-stats.html',
|
||||
statsFilename: 'bundle-stats.json',
|
||||
})
|
||||
]
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
const webpackProdConfig = require('./webpack.production.config');
|
||||
const artifacts = require("../test/artifacts");
|
||||
|
||||
const OUTPATH = artifacts.pathSync("/profiling");
|
||||
|
||||
module.exports = {
|
||||
...webpackProdConfig,
|
||||
output: {
|
||||
...webpackProdConfig.output,
|
||||
path: OUTPATH,
|
||||
},
|
||||
resolve: {
|
||||
...webpackProdConfig.resolve,
|
||||
alias: {
|
||||
...webpackProdConfig.resolve.alias,
|
||||
'react-dom$': 'react-dom/profiling',
|
||||
'scheduler/tracing': 'scheduler/tracing-profiling',
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
exclude: [
|
||||
path.resolve(__dirname, '../node_modules')
|
||||
],
|
||||
use: 'babel-loader'
|
||||
},
|
||||
{
|
||||
test: /\.(eot|ttf|woff|woff2)$/,
|
||||
use: 'file-loader?name=fonts/[name].[ext]'
|
||||
},
|
||||
{
|
||||
test: /\.ico$/,
|
||||
use: 'file-loader?name=[name].[ext]'
|
||||
},
|
||||
{
|
||||
test: /\.(gif|jpg|png)$/,
|
||||
use: 'file-loader?name=img/[name].[ext]'
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
use: [
|
||||
'svg-inline-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.scss$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
"css-loader",
|
||||
"sass-loader"
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.css$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
'css-loader'
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineConfig } from "cypress";
|
||||
import { createRequire } from "module";
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
export default defineConfig({
|
||||
env: {
|
||||
codeCoverage: {
|
||||
exclude: "cypress/**/*.*",
|
||||
},
|
||||
},
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
// implement node event listeners here
|
||||
require("@cypress/code-coverage/task")(on, config);
|
||||
return config;
|
||||
},
|
||||
baseUrl: "http://localhost:8888",
|
||||
retries: {
|
||||
runMode: 2,
|
||||
openMode: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import MaputnikDriver from "./driver";
|
||||
|
||||
describe("accessibility", () => {
|
||||
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
|
||||
describe("skip links", () => {
|
||||
beforeEach(() => {
|
||||
when.setStyle("layer");
|
||||
});
|
||||
|
||||
it("skip link to layer list", () => {
|
||||
const selector = "root:skip:layer-list";
|
||||
should.exist(selector);
|
||||
when.tab();
|
||||
should.beFocused(selector);
|
||||
when.click(selector);
|
||||
should.beFocused("skip-target-layer-list");
|
||||
});
|
||||
|
||||
// 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";
|
||||
should.exist(selector);
|
||||
when.tab().tab();
|
||||
should.beFocused(selector);
|
||||
when.click(selector);
|
||||
should.beFocused("skip-target-layer-editor");
|
||||
});
|
||||
|
||||
it("skip link to map view", () => {
|
||||
const selector = "root:skip:map-view";
|
||||
should.exist(selector);
|
||||
when.tab().tab().tab();
|
||||
should.beFocused(selector);
|
||||
when.click(selector);
|
||||
should.canvasBeFocused();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,210 @@
|
||||
import { CypressHelper } from "@shellygo/cypress-test-utils";
|
||||
import { v1 as uuid } from "uuid";
|
||||
export default class MaputnikDriver {
|
||||
private helper = new CypressHelper({ defaultDataAttribute: "data-wd-key" });
|
||||
public beforeAndAfter = () => {
|
||||
beforeEach(() => {
|
||||
this.given.setupInterception();
|
||||
this.when.setStyle("both");
|
||||
});
|
||||
};
|
||||
|
||||
public given = {
|
||||
setupInterception: () => {
|
||||
cy.intercept("GET", "http://localhost:8888/example-style.json", {
|
||||
fixture: "example-style.json",
|
||||
}).as("example-style.json");
|
||||
cy.intercept("GET", "http://localhost:8888/example-layer-style.json", {
|
||||
fixture: "example-layer-style.json",
|
||||
});
|
||||
cy.intercept("GET", "http://localhost:8888/geojson-style.json", {
|
||||
fixture: "geojson-style.json",
|
||||
});
|
||||
cy.intercept("GET", "http://localhost:8888/raster-style.json", {
|
||||
fixture: "raster-style.json",
|
||||
});
|
||||
cy.intercept("GET", "http://localhost:8888/geojson-raster-style.json", {
|
||||
fixture: "geojson-raster-style.json",
|
||||
});
|
||||
cy.intercept({ method: "GET", url: "*example.local/*" }, []);
|
||||
cy.intercept({ method: "GET", url: "*example.com/*" }, []);
|
||||
},
|
||||
};
|
||||
|
||||
public when = {
|
||||
within: (selector: string, fn: () => void) => {
|
||||
this.helper.when.within(fn, selector);
|
||||
},
|
||||
tab: () => cy.get("body").tab(),
|
||||
waitForExampleFileRequset: () => {
|
||||
this.helper.when.waitForResponse("example-style.json");
|
||||
},
|
||||
chooseExampleFile: () => {
|
||||
cy.get("input[type='file']").selectFile(
|
||||
"cypress/fixtures/example-style.json",
|
||||
{ force: true }
|
||||
);
|
||||
},
|
||||
setStyle: (
|
||||
styleProperties: "geojson" | "raster" | "both" | "layer" | "",
|
||||
zoom?: number
|
||||
) => {
|
||||
let url = "?debug";
|
||||
switch (styleProperties) {
|
||||
case "geojson":
|
||||
url += "&style=http://localhost:8888/geojson-style.json";
|
||||
break;
|
||||
case "raster":
|
||||
url += "&style=http://localhost:8888/raster-style.json";
|
||||
break;
|
||||
case "both":
|
||||
url += "&style=http://localhost:8888/geojson-raster-style.json";
|
||||
break;
|
||||
case "layer":
|
||||
url += "&style=http://localhost:8888/example-layer-style.json";
|
||||
break;
|
||||
}
|
||||
if (zoom) {
|
||||
url += "#" + zoom + "/41.3805/2.1635";
|
||||
}
|
||||
cy.visit("http://localhost:8888/" + url);
|
||||
if (styleProperties) {
|
||||
cy.on("window:confirm", () => true);
|
||||
}
|
||||
cy.get(".maputnik-toolbar-link").should("be.visible");
|
||||
},
|
||||
fillLayersModal: (opts: {type: string, layer?: string, id?: string}) => {
|
||||
var type = opts.type;
|
||||
var layer = opts.layer;
|
||||
var id;
|
||||
if (opts.id) {
|
||||
id = opts.id;
|
||||
} else {
|
||||
id = `${type}:${uuid()}`;
|
||||
}
|
||||
|
||||
cy.get(
|
||||
this.get.dataAttribute("add-layer.layer-type", "select")
|
||||
).select(type);
|
||||
cy.get(this.get.dataAttribute("add-layer.layer-id", "input")).type(id);
|
||||
if (layer) {
|
||||
cy.get(
|
||||
this.get.dataAttribute("add-layer.layer-source-block", "input")
|
||||
).type(layer);
|
||||
}
|
||||
this.when.click("add-layer");
|
||||
|
||||
return id;
|
||||
},
|
||||
|
||||
typeKeys: (keys: string) => {
|
||||
cy.get("body").type(keys);
|
||||
},
|
||||
|
||||
click: (selector: string) => {
|
||||
this.helper.when.click(selector);
|
||||
},
|
||||
|
||||
clickZoomin: () => {
|
||||
cy.get(".maplibregl-ctrl-zoom-in").click();
|
||||
},
|
||||
|
||||
selectWithin: (selector: string, value: string) => {
|
||||
this.when.within(selector, () => {
|
||||
cy.get("select").select(value);
|
||||
});
|
||||
},
|
||||
|
||||
select: (selector: string, value: string) => {
|
||||
this.helper.get.element(selector).select(value);
|
||||
},
|
||||
|
||||
focus: (selector: string) => {
|
||||
this.helper.when.focus(selector);
|
||||
},
|
||||
|
||||
setValue: (selector: string, text: string) => {
|
||||
cy.get(selector).clear().type(text, { parseSpecialCharSequences: false });
|
||||
},
|
||||
|
||||
closeModal: (key: string) => {
|
||||
this.helper.when.waitUntil(() => this.helper.get.element(key));
|
||||
this.when.click(key + ".close-modal");
|
||||
},
|
||||
|
||||
openLayersModal: () => {
|
||||
this.helper.when.click("layer-list:add-layer");
|
||||
|
||||
cy.get(this.get.dataAttribute("modal:add-layer")).should("exist");
|
||||
cy.get(this.get.dataAttribute("modal:add-layer")).should("be.visible");
|
||||
},
|
||||
};
|
||||
|
||||
public get = {
|
||||
isMac: () => {
|
||||
return Cypress.platform === "darwin";
|
||||
},
|
||||
styleFromWindow: (win: Window) => {
|
||||
const styleId = win.localStorage.getItem("maputnik:latest_style");
|
||||
const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`);
|
||||
const obj = JSON.parse(styleItem || "");
|
||||
return obj;
|
||||
},
|
||||
exampleFileUrl: () => {
|
||||
return "http://localhost:8888/example-style.json";
|
||||
},
|
||||
dataAttribute: (key: string, selector?: string): string => {
|
||||
return `*[data-wd-key='${key}'] ${selector || ""}`;
|
||||
},
|
||||
};
|
||||
|
||||
public should = {
|
||||
canvasBeFocused: () => {
|
||||
this.when.within("maplibre:map", () => {
|
||||
cy.get("canvas").should("be.focused");
|
||||
});
|
||||
},
|
||||
notExist: (selector: string) => {
|
||||
cy.get(selector).should("not.exist");
|
||||
},
|
||||
beFocused: (selector: string) => {
|
||||
this.helper.get.element(selector).should("have.focus");
|
||||
},
|
||||
|
||||
notBeFocused: (selector: string) => {
|
||||
this.helper.get.element(selector).should("not.have.focus");
|
||||
},
|
||||
|
||||
beVisible: (selector: string) => {
|
||||
this.helper.get.element(selector).should("be.visible");
|
||||
},
|
||||
|
||||
notBeVisible: (selector: string) => {
|
||||
this.helper.get.element(selector).should("not.be.visible");
|
||||
},
|
||||
|
||||
equalStyleStore: (getter: (obj: any) => any, styleObj: any) => {
|
||||
cy.window().then((win: any) => {
|
||||
const obj = this.get.styleFromWindow(win);
|
||||
assert.deepEqual(getter(obj), styleObj);
|
||||
});
|
||||
},
|
||||
|
||||
styleStoreEqualToExampleFileData: () => {
|
||||
cy.window().then((win: any) => {
|
||||
const obj = this.get.styleFromWindow(win);
|
||||
cy.fixture("example-style.json").should("deep.equal", obj);
|
||||
});
|
||||
},
|
||||
|
||||
exist: (selector: string) => {
|
||||
this.helper.get.element(selector).should("exist");
|
||||
},
|
||||
beSelected: (selector: string, value: string) => {
|
||||
this.helper.get.element(selector).find(`option[value="${value}"]`).should("be.selected");
|
||||
},
|
||||
containText: (selector: string, text: string) => {
|
||||
this.helper.get.element(selector).should("contain.text", text);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import MaputnikDriver from "./driver";
|
||||
|
||||
describe("history", () => {
|
||||
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
|
||||
let undoKeyCombo: string;
|
||||
let redoKeyCombo: string;
|
||||
|
||||
before(() => {
|
||||
const isMac = get.isMac();
|
||||
undoKeyCombo = isMac ? "{meta}z" : "{ctrl}z";
|
||||
redoKeyCombo = isMac ? "{meta}{shift}z" : "{ctrl}y";
|
||||
});
|
||||
|
||||
it("undo/redo", () => {
|
||||
when.setStyle("geojson");
|
||||
when.openLayersModal();
|
||||
|
||||
should.equalStyleStore((a: any) => a.layers, []);
|
||||
|
||||
when.fillLayersModal({
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
when.openLayersModal();
|
||||
when.fillLayersModal({
|
||||
id: "step 2",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
},
|
||||
{
|
||||
id: "step 2",
|
||||
type: "background",
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
when.typeKeys(undoKeyCombo);
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
when.typeKeys(undoKeyCombo);
|
||||
should.equalStyleStore((a: any) => a.layers, []);
|
||||
|
||||
when.typeKeys(redoKeyCombo);
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
when.typeKeys(redoKeyCombo);
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "step 1",
|
||||
type: "background",
|
||||
},
|
||||
{
|
||||
id: "step 2",
|
||||
type: "background",
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import MaputnikDriver from "./driver";
|
||||
|
||||
describe("keyboard", () => {
|
||||
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
describe("shortcuts", () => {
|
||||
beforeEach(() => {
|
||||
given.setupInterception();
|
||||
when.setStyle("");
|
||||
});
|
||||
|
||||
it("ESC should unfocus", () => {
|
||||
const targetSelector = "maputnik-select";
|
||||
when.focus(targetSelector);
|
||||
should.beFocused(targetSelector);
|
||||
|
||||
when.typeKeys("{esc}");
|
||||
expect(should.notBeFocused(targetSelector));
|
||||
});
|
||||
|
||||
it("'?' should show shortcuts modal", () => {
|
||||
when.typeKeys("?");
|
||||
should.beVisible("modal:shortcuts");
|
||||
});
|
||||
|
||||
it("'o' should show open modal", () => {
|
||||
when.typeKeys("o");
|
||||
should.beVisible("modal:open");
|
||||
});
|
||||
|
||||
it("'e' should show export modal", () => {
|
||||
when.typeKeys("e");
|
||||
should.beVisible("modal:export");
|
||||
});
|
||||
|
||||
it("'d' should show sources modal", () => {
|
||||
when.typeKeys("d");
|
||||
should.beVisible("modal:sources");
|
||||
});
|
||||
|
||||
it("'s' should show settings modal", () => {
|
||||
when.typeKeys("s");
|
||||
should.beVisible("modal:settings");
|
||||
});
|
||||
|
||||
it("'i' should change map to inspect mode", () => {
|
||||
when.typeKeys("i");
|
||||
should.beSelected("nav:inspect", "inspect");
|
||||
});
|
||||
|
||||
it("'m' should focus map", () => {
|
||||
when.typeKeys("m");
|
||||
should.canvasBeFocused();
|
||||
});
|
||||
|
||||
it("'!' should show debug modal", () => {
|
||||
when.typeKeys("!");
|
||||
should.beVisible("modal:debug");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,491 @@
|
||||
import { v1 as uuid } from "uuid";
|
||||
import MaputnikDriver from "./driver";
|
||||
|
||||
describe("layers", () => {
|
||||
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
beforeEach(() => {
|
||||
when.setStyle("both");
|
||||
when.openLayersModal();
|
||||
});
|
||||
|
||||
describe("ops", () => {
|
||||
it("delete", () => {
|
||||
var id = when.fillLayersModal({
|
||||
type: "background",
|
||||
});
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
when.click("layer-list-item:" + id + ":delete");
|
||||
|
||||
should.equalStyleStore((a: any) => a.layers, []);
|
||||
});
|
||||
|
||||
it("duplicate", () => {
|
||||
var styleObj;
|
||||
var id = when.fillLayersModal({
|
||||
type: "background",
|
||||
});
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
when.click("layer-list-item:" + id + ":copy");
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: id + "-copy",
|
||||
type: "background",
|
||||
},
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it("hide", () => {
|
||||
var styleObj;
|
||||
var id = when.fillLayersModal({
|
||||
type: "background",
|
||||
});
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
when.click("layer-list-item:" + id + ":toggle-visibility");
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
layout: {
|
||||
visibility: "none",
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
when.click("layer-list-item:" + id + ":toggle-visibility");
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
layout: {
|
||||
visibility: "visible",
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("background", () => {
|
||||
it("add", () => {
|
||||
var id = when.fillLayersModal({
|
||||
type: "background",
|
||||
});
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: id,
|
||||
type: "background",
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
describe("modify", () => {
|
||||
function createBackground() {
|
||||
// Setup
|
||||
var id = uuid();
|
||||
|
||||
when.selectWithin("add-layer.layer-type", "background");
|
||||
when.setValue(
|
||||
get.dataAttribute("add-layer.layer-id", "input"),
|
||||
"background:" + id
|
||||
);
|
||||
|
||||
when.click("add-layer");
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "background:" + id,
|
||||
type: "background",
|
||||
},
|
||||
]
|
||||
);
|
||||
return id;
|
||||
}
|
||||
|
||||
// ====> THESE SHOULD BE FROM THE SPEC
|
||||
describe("layer", () => {
|
||||
it("expand/collapse");
|
||||
it("id", () => {
|
||||
var bgId = createBackground();
|
||||
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
|
||||
var id = uuid();
|
||||
when.setValue(
|
||||
get.dataAttribute("layer-editor.layer-id", "input"),
|
||||
"foobar:" + id
|
||||
);
|
||||
when.click("min-zoom");
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "foobar:" + id,
|
||||
type: "background",
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it("min-zoom", () => {
|
||||
var bgId = createBackground();
|
||||
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.setValue(
|
||||
get.dataAttribute("min-zoom", 'input[type="text"]'),
|
||||
"1"
|
||||
);
|
||||
|
||||
when.click("layer-editor.layer-id");
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
minzoom: 1,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// AND RESET!
|
||||
// driver.setValue(driver.get.dataAttribute("min-zoom", "input"), "")
|
||||
// driver.click(driver.get.dataAttribute("max-zoom", "input"));
|
||||
|
||||
// driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||
// {
|
||||
// "id": 'background:'+bgId,
|
||||
// "type": 'background'
|
||||
// }
|
||||
// ]);
|
||||
});
|
||||
|
||||
it("max-zoom", () => {
|
||||
var bgId = createBackground();
|
||||
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.setValue(
|
||||
get.dataAttribute("max-zoom", 'input[type="text"]'),
|
||||
"1"
|
||||
);
|
||||
|
||||
when.click("layer-editor.layer-id");
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
maxzoom: 1,
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it("comments", () => {
|
||||
var bgId = createBackground();
|
||||
var id = uuid();
|
||||
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
when.setValue(get.dataAttribute("layer-comment", "textarea"), id);
|
||||
|
||||
when.click("layer-editor.layer-id");
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
metadata: {
|
||||
"maputnik:comment": id,
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Unset it again.
|
||||
// TODO: This fails
|
||||
// driver.setValue(driver.getDataAttribute("layer-comment", "textarea"), "");
|
||||
// driver.click(driver.getDataAttribute("min-zoom", "input"));
|
||||
|
||||
// driver.isStyleStoreEqual((a: any) => a.layers, [
|
||||
// {
|
||||
// "id": 'background:'+bgId,
|
||||
// "type": 'background'
|
||||
// }
|
||||
// ]);
|
||||
});
|
||||
|
||||
it("color", () => {
|
||||
var bgId = createBackground();
|
||||
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
|
||||
when.click("spec-field:background-color");
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: "background:" + bgId,
|
||||
type: "background",
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("filter", () => {
|
||||
it("expand/collapse");
|
||||
it("compound filter");
|
||||
});
|
||||
|
||||
describe("paint", () => {
|
||||
it("expand/collapse");
|
||||
it("color");
|
||||
it("pattern");
|
||||
it("opacity");
|
||||
});
|
||||
// <=====
|
||||
|
||||
describe("json-editor", () => {
|
||||
it("expand/collapse");
|
||||
it("modify");
|
||||
|
||||
// TODO
|
||||
it.skip("parse error", () => {
|
||||
var bgId = createBackground();
|
||||
|
||||
when.click("layer-list-item:background:" + bgId);
|
||||
|
||||
var errorSelector = ".CodeMirror-lint-marker-error";
|
||||
should.notExist(errorSelector);
|
||||
|
||||
when.click(".CodeMirror");
|
||||
when.typeKeys(
|
||||
"\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {"
|
||||
);
|
||||
should.exist(errorSelector);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("fill", () => {
|
||||
it("add", () => {
|
||||
var id = when.fillLayersModal({
|
||||
type: "fill",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: id,
|
||||
type: "fill",
|
||||
source: "example",
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: Change source
|
||||
it("change source");
|
||||
});
|
||||
|
||||
describe("line", () => {
|
||||
it("add", () => {
|
||||
var id = when.fillLayersModal({
|
||||
type: "line",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: id,
|
||||
type: "line",
|
||||
source: "example",
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it("groups", () => {
|
||||
// TODO
|
||||
// Click each of the layer groups.
|
||||
});
|
||||
});
|
||||
|
||||
describe("symbol", () => {
|
||||
it("add", () => {
|
||||
var id = when.fillLayersModal({
|
||||
type: "symbol",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: id,
|
||||
type: "symbol",
|
||||
source: "example",
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("raster", () => {
|
||||
it("add", () => {
|
||||
var id = when.fillLayersModal({
|
||||
type: "raster",
|
||||
layer: "raster",
|
||||
});
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: id,
|
||||
type: "raster",
|
||||
source: "raster",
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("circle", () => {
|
||||
it("add", () => {
|
||||
var id = when.fillLayersModal({
|
||||
type: "circle",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: id,
|
||||
type: "circle",
|
||||
source: "example",
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fill extrusion", () => {
|
||||
it("add", () => {
|
||||
var id = when.fillLayersModal({
|
||||
type: "fill-extrusion",
|
||||
layer: "example",
|
||||
});
|
||||
|
||||
should.equalStyleStore(
|
||||
(a: any) => a.layers,
|
||||
[
|
||||
{
|
||||
id: id,
|
||||
type: "fill-extrusion",
|
||||
source: "example",
|
||||
},
|
||||
]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("groups", () => {
|
||||
it("simple", () => {
|
||||
when.setStyle("geojson");
|
||||
|
||||
when.openLayersModal();
|
||||
when.fillLayersModal({
|
||||
id: "foo",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
when.openLayersModal();
|
||||
when.fillLayersModal({
|
||||
id: "foo_bar",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
when.openLayersModal();
|
||||
when.fillLayersModal({
|
||||
id: "foo_bar_baz",
|
||||
type: "background",
|
||||
});
|
||||
|
||||
should.beVisible("layer-list-item:foo");
|
||||
|
||||
should.notBeVisible("layer-list-item:foo_bar");
|
||||
should.notBeVisible("layer-list-item:foo_bar_baz");
|
||||
|
||||
when.click("layer-list-group:foo-0");
|
||||
|
||||
should.beVisible("layer-list-item:foo");
|
||||
should.beVisible("layer-list-item:foo_bar");
|
||||
should.beVisible("layer-list-item:foo_bar_baz");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import MaputnikDriver from "./driver";
|
||||
|
||||
describe("map", () => {
|
||||
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
describe("zoom level", () => {
|
||||
it("via url", () => {
|
||||
var zoomLevel = 12.37;
|
||||
when.setStyle("geojson", zoomLevel);
|
||||
should.beVisible("maplibre:ctrl-zoom");
|
||||
should.containText("maplibre:ctrl-zoom", "Zoom: " + zoomLevel);
|
||||
});
|
||||
|
||||
it("via map controls", () => {
|
||||
var zoomLevel = 12.37;
|
||||
when.setStyle("geojson", zoomLevel);
|
||||
|
||||
should.beVisible("maplibre:ctrl-zoom");
|
||||
when.clickZoomin();
|
||||
should.containText("maplibre:ctrl-zoom", "Zoom: "+(zoomLevel + 1));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
import MaputnikDriver from "./driver";
|
||||
|
||||
describe("modals", () => {
|
||||
let { beforeAndAfter, given, when, get, should } = new MaputnikDriver();
|
||||
beforeAndAfter();
|
||||
beforeEach(() => {
|
||||
when.setStyle("");
|
||||
});
|
||||
describe("open", () => {
|
||||
beforeEach(() => {
|
||||
when.click("nav:open");
|
||||
});
|
||||
|
||||
it("close", () => {
|
||||
when.closeModal("modal:open");
|
||||
should.notExist("modal:open");
|
||||
});
|
||||
|
||||
it.skip("upload", () => {
|
||||
// HM: I was not able to make the following choose file actually to select a file and close the modal...
|
||||
when.chooseExampleFile();
|
||||
|
||||
should.styleStoreEqualToExampleFileData();
|
||||
});
|
||||
|
||||
it("load from url", () => {
|
||||
var styleFileUrl = get.exampleFileUrl();
|
||||
|
||||
when.setValue(get.dataAttribute("modal:open.url.input"), styleFileUrl);
|
||||
when.click("modal:open.url.button");
|
||||
when.waitForExampleFileRequset();
|
||||
|
||||
should.styleStoreEqualToExampleFileData();
|
||||
});
|
||||
});
|
||||
|
||||
describe("shortcuts", () => {
|
||||
it("open/close", () => {
|
||||
when.setStyle("");
|
||||
when.typeKeys("?");
|
||||
when.closeModal("modal:shortcuts");
|
||||
should.notExist("modal:shortcuts");
|
||||
});
|
||||
});
|
||||
|
||||
describe("export", () => {
|
||||
beforeEach(() => {
|
||||
when.click("nav:export");
|
||||
});
|
||||
|
||||
it("close", () => {
|
||||
when.closeModal("modal:export");
|
||||
should.notExist("modal:export");
|
||||
});
|
||||
|
||||
// TODO: Work out how to download a file and check the contents
|
||||
it("download");
|
||||
});
|
||||
|
||||
describe("sources", () => {
|
||||
it("active sources");
|
||||
it("public source");
|
||||
it("add new source");
|
||||
});
|
||||
|
||||
describe("inspect", () => {
|
||||
it("toggle", () => {
|
||||
when.setStyle("geojson");
|
||||
|
||||
when.selectWithin("nav:inspect", "inspect");
|
||||
});
|
||||
});
|
||||
|
||||
describe("style settings", () => {
|
||||
beforeEach(() => {
|
||||
when.click("nav:settings");
|
||||
});
|
||||
|
||||
it("name", () => {
|
||||
when.click("field-doc-button-Name");
|
||||
|
||||
should.containText("spec-field-doc", "name for the style");
|
||||
});
|
||||
|
||||
it("show name specifications", () => {
|
||||
when.setValue(get.dataAttribute("modal:settings.name"), "foobar");
|
||||
when.click("modal:settings.owner");
|
||||
|
||||
should.equalStyleStore((obj) => obj.name, "foobar");
|
||||
});
|
||||
|
||||
it("owner", () => {
|
||||
when.setValue(get.dataAttribute("modal:settings.owner"), "foobar");
|
||||
when.click("modal:settings.name");
|
||||
|
||||
should.equalStyleStore((obj) => obj.owner, "foobar");
|
||||
});
|
||||
it("sprite url", () => {
|
||||
when.setValue(
|
||||
get.dataAttribute("modal:settings.sprite"),
|
||||
"http://example.com"
|
||||
);
|
||||
when.click("modal:settings.name");
|
||||
|
||||
should.equalStyleStore((obj) => obj.sprite, "http://example.com");
|
||||
});
|
||||
it("glyphs url", () => {
|
||||
var glyphsUrl = "http://example.com/{fontstack}/{range}.pbf";
|
||||
when.setValue(get.dataAttribute("modal:settings.glyphs"), glyphsUrl);
|
||||
when.click("modal:settings.name");
|
||||
|
||||
should.equalStyleStore((obj) => obj.glyphs, glyphsUrl);
|
||||
});
|
||||
|
||||
it("maptiler access token", () => {
|
||||
var apiKey = "testing123";
|
||||
when.setValue(
|
||||
get.dataAttribute(
|
||||
"modal:settings.maputnik:openmaptiles_access_token"
|
||||
),
|
||||
apiKey
|
||||
);
|
||||
when.click("modal:settings.name");
|
||||
|
||||
should.equalStyleStore(
|
||||
(obj) => obj.metadata["maputnik:openmaptiles_access_token"],
|
||||
apiKey
|
||||
);
|
||||
});
|
||||
|
||||
it("thunderforest access token", () => {
|
||||
var apiKey = "testing123";
|
||||
when.setValue(
|
||||
get.dataAttribute(
|
||||
"modal:settings.maputnik:thunderforest_access_token"
|
||||
),
|
||||
apiKey
|
||||
);
|
||||
when.click("modal:settings.name");
|
||||
|
||||
should.equalStyleStore(
|
||||
(obj) => obj.metadata["maputnik:thunderforest_access_token"],
|
||||
apiKey
|
||||
);
|
||||
});
|
||||
|
||||
it("style renderer", () => {
|
||||
cy.on("uncaught:exception", () => false); // this is due to the fact that this is an invalid style for openlayers
|
||||
when.select("modal:settings.maputnik:renderer", "ol");
|
||||
should.beSelected("modal:settings.maputnik:renderer", "ol");
|
||||
|
||||
when.click("modal:settings.name");
|
||||
|
||||
should.equalStyleStore((obj) => obj.metadata["maputnik:renderer"], "ol");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sources", () => {
|
||||
it("toggle");
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"metadata": {
|
||||
"maputnik:renderer": "mbgljs"
|
||||
"maputnik:renderer": "mlgljs"
|
||||
},
|
||||
"sources": {},
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"metadata": {
|
||||
"maputnik:renderer": "mbgljs"
|
||||
"maputnik:renderer": "mlgljs"
|
||||
},
|
||||
"sources": {},
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "test-style",
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"metadata": {
|
||||
"maputnik:renderer": "mlgljs"
|
||||
},
|
||||
"sources": {
|
||||
"example": {
|
||||
"type": "vector",
|
||||
"data": {
|
||||
"type": "FeatureCollection",
|
||||
"features":[{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"name": "Dinagat Islands"
|
||||
},
|
||||
"geometry":{
|
||||
"type": "Point",
|
||||
"coordinates": [125.6, 10.1]
|
||||
}
|
||||
}]
|
||||
}
|
||||
},
|
||||
"raster": {
|
||||
"tileSize": 256,
|
||||
"tiles": ["http://localhost/example/{x}/{y}/{z}"],
|
||||
"type": "raster"
|
||||
}
|
||||
},
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": []
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"id": "test-style",
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"metadata": {
|
||||
"maputnik:renderer": "mlgljs"
|
||||
},
|
||||
"sources": {
|
||||
"example": {
|
||||
"type": "vector",
|
||||
"data": {
|
||||
"type": "FeatureCollection",
|
||||
"features":[{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"name": "Dinagat Islands"
|
||||
},
|
||||
"geometry":{
|
||||
"type": "Point",
|
||||
"coordinates": [125.6, 10.1]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
},
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": []
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"id": "test-style",
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"metadata": {
|
||||
"maputnik:renderer": "mlgljs"
|
||||
},
|
||||
"sources": {
|
||||
"raster": {
|
||||
"tileSize": 256,
|
||||
"tiles": ["http://localhost/example/{x}/{y}/{z}"],
|
||||
"type": "raster"
|
||||
}
|
||||
},
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": []
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************
|
||||
// This example commands.ts shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
//
|
||||
// declare global {
|
||||
// namespace Cypress {
|
||||
// interface Chainable {
|
||||
// login(email: string, password: string): Chainable<void>
|
||||
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,22 @@
|
||||
// ***********************************************************
|
||||
// This example support/e2e.ts is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import "@cypress/code-coverage/support";
|
||||
import "cypress-plugin-tab";
|
||||
import "./commands";
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
@@ -2,9 +2,10 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
<title>Maputnik</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="manifest" href="src/manifest.json">
|
||||
<link rel="icon" href="src/favicon.ico" type="image/x-icon" />
|
||||
<style>
|
||||
html {
|
||||
background-color: rgb(28, 31, 36);
|
||||
@@ -23,7 +24,7 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading__logo svg {
|
||||
.loading__logo img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
@@ -38,7 +39,6 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- TODO: Import dynamically -->
|
||||
<!-- From <https://github.com/hail2u/color-blindness-emulation> -->
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
@@ -123,10 +123,10 @@
|
||||
<div id="app"></div>
|
||||
<div class="loading">
|
||||
<div class="loading__logo">
|
||||
<!-- replaced by 'html-webpack-inline-svg-plugin' -->
|
||||
<img inline src="node_modules/maputnik-design/logos/logo-loading.svg" />
|
||||
</div>
|
||||
<div class="loading__text">Loading…</div>
|
||||
</div>
|
||||
<script type="module" src="/src/index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+14862
-18649
File diff suppressed because it is too large
Load Diff
+62
-87
@@ -1,22 +1,18 @@
|
||||
{
|
||||
"name": "maputnik",
|
||||
"version": "1.7.0",
|
||||
"description": "A MapboxGL visual style editor",
|
||||
"version": "2.0.0-pre.2",
|
||||
"description": "A MapLibre GL visual style editor",
|
||||
"type": "module",
|
||||
"main": "''",
|
||||
"scripts": {
|
||||
"stats": "webpack --config config/webpack.production.config.js --progress=profile --json > stats.json",
|
||||
"build": "webpack --config config/webpack.production.config.js --progress=profile --color",
|
||||
"profiling-build": "webpack --config config/webpack.profiling.config.js --progress=profile --color",
|
||||
"test": "cross-env NODE_ENV=test wdio config/wdio.conf.js",
|
||||
"test-watch": "cross-env NODE_ENV=test wdio config/wdio.conf.js --watch",
|
||||
"start": "webpack-dev-server --progress=profile --color --config config/webpack.config.js",
|
||||
"start-prod": "webpack-dev-server --progress=profile --color --config config/webpack.production.config.js",
|
||||
"start-sandbox": "webpack-dev-server --disable-host-check --host 0.0.0.0 --progress=profile --color --config config/webpack.production.config.js",
|
||||
"lint-js": "eslint --ext js --ext jsx src test",
|
||||
"start": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint ./src --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"test": "cypress run",
|
||||
"cy:open": "cypress open",
|
||||
"lint-css": "stylelint \"src/styles/*.scss\"",
|
||||
"lint": "npm run lint-js && npm run lint-css",
|
||||
"storybook": "start-storybook -h 0.0.0.0 -p 6006",
|
||||
"build-storybook": "build-storybook -o build/storybook"
|
||||
"storybook": "storybook dev -h 0.0.0.0 -p 6006",
|
||||
"build-storybook": "storybook build -o build/storybook"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -26,11 +22,14 @@
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/maputnik/editor#readme",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.17.9",
|
||||
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
|
||||
"@mapbox/mapbox-gl-style-spec": "^13.23.1",
|
||||
"@maplibre/maplibre-gl-style-spec": "^17.0.1",
|
||||
"@mdi/js": "^6.6.96",
|
||||
"@mdi/react": "^1.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.16.0",
|
||||
"@typescript-eslint/parser": "^6.16.0",
|
||||
"array-move": "^4.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "^5.65.2",
|
||||
"color": "^4.2.3",
|
||||
@@ -46,8 +45,8 @@
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"mapbox-gl": "^1.13.2",
|
||||
"mapbox-gl-inspect": "^1.3.1",
|
||||
"maplibre-gl": "^2.4.0",
|
||||
"maputnik-design": "github:maputnik/design#172b06c",
|
||||
"ol": "^6.14.1",
|
||||
"ol-mapbox-style": "^7.1.1",
|
||||
@@ -88,87 +87,63 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"eslintConfig": {
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:react/recommended"
|
||||
],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 6,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"impliedStrict": true,
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.9",
|
||||
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
||||
"@babel/plugin-transform-runtime": "^7.17.0",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/preset-flow": "^7.16.7",
|
||||
"@babel/preset-react": "^7.16.7",
|
||||
"@mdi/js": "^6.6.96",
|
||||
"@storybook/addon-a11y": "^6.4.20",
|
||||
"@storybook/addon-actions": "^6.4.20",
|
||||
"@storybook/addon-links": "^6.4.20",
|
||||
"@storybook/addon-storysource": "^6.4.20",
|
||||
"@storybook/addons": "^6.4.20",
|
||||
"@storybook/react": "^6.4.20",
|
||||
"@storybook/theming": "^6.4.20",
|
||||
"@wdio/cli": "^7.19.3",
|
||||
"@wdio/local-runner": "^7.19.3",
|
||||
"@wdio/mocha-framework": "^7.19.3",
|
||||
"@wdio/selenium-standalone-service": "^7.19.1",
|
||||
"@wdio/spec-reporter": "^7.19.1",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^8.2.4",
|
||||
"babel-plugin-istanbul": "^6.1.1",
|
||||
"babel-plugin-static-fs": "^3.0.0",
|
||||
"copy-webpack-plugin": "^6.4.1",
|
||||
"@cypress/code-coverage": "^3.12.15",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||
"@rollup/plugin-replace": "^5.0.5",
|
||||
"@shellygo/cypress-test-utils": "^2.0.9",
|
||||
"@storybook/addon-a11y": "^7.6.5",
|
||||
"@storybook/addon-actions": "^7.6.5",
|
||||
"@storybook/addon-links": "^7.6.5",
|
||||
"@storybook/addon-storysource": "^7.6.5",
|
||||
"@storybook/addons": "^7.6.5",
|
||||
"@storybook/builder-vite": "^7.6.5",
|
||||
"@storybook/react": "^7.6.5",
|
||||
"@storybook/react-vite": "^7.6.5",
|
||||
"@storybook/theming": "^7.6.5",
|
||||
"@types/codemirror": "^5.60.15",
|
||||
"@types/color": "^3.0.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/geojson": "^7946.0.13",
|
||||
"@types/json-to-ast": "^2.1.4",
|
||||
"@types/lodash.capitalize": "^4.2.9",
|
||||
"@types/lodash.clamp": "^4.0.9",
|
||||
"@types/lodash.clonedeep": "^4.5.9",
|
||||
"@types/lodash.get": "^4.4.9",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/lodash.throttle": "^4.1.9",
|
||||
"@types/react": "^16.14.52",
|
||||
"@types/react-aria-menubutton": "^6.2.13",
|
||||
"@types/react-aria-modal": "^4.0.9",
|
||||
"@types/react-autocomplete": "^1.8.9",
|
||||
"@types/react-collapse": "^5.0.4",
|
||||
"@types/react-color": "^3.0.10",
|
||||
"@types/react-dom": "^16.9.24",
|
||||
"@types/react-file-reader-input": "^2.0.4",
|
||||
"@types/react-icon-base": "^2.1.6",
|
||||
"@types/string-hash": "^1.1.3",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^5.2.7",
|
||||
"eslint": "^8.12.0",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"cypress": "^13.6.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"express": "^4.17.3",
|
||||
"html-webpack-inline-svg-plugin": "^2.3.0",
|
||||
"html-webpack-plugin": "^4.5.2",
|
||||
"istanbul": "^0.4.5",
|
||||
"istanbul-lib-coverage": "^3.2.0",
|
||||
"mkdirp": "^1.0.4",
|
||||
"mocha": "^9.2.2",
|
||||
"postcss": "^8.4.12",
|
||||
"react-hot-loader": "^4.13.0",
|
||||
"sass-loader": "^10.2.1",
|
||||
"style-loader": "^2.0.0",
|
||||
"storybook": "^7.6.5",
|
||||
"stylelint": "^14.6.1",
|
||||
"stylelint-config-recommended-scss": "^6.0.0",
|
||||
"stylelint-scss": "^4.2.0",
|
||||
"svg-inline-loader": "^0.8.2",
|
||||
"transform-loader": "^0.2.4",
|
||||
"typescript": "^4.6.3",
|
||||
"typescript": "^5.3.3",
|
||||
"uuid": "^8.3.2",
|
||||
"webdriverio": "^7.19.3",
|
||||
"webpack": "^4.46.0",
|
||||
"webpack-bundle-analyzer": "^4.5.0",
|
||||
"webpack-cleanup-plugin": "^0.5.1",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "^4.8.1"
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-istanbul": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"container": {
|
||||
"startScript": "start-sandbox"
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
// @ts-ignore - this can be easily replaced with arrow functions
|
||||
import autoBind from 'react-autobind';
|
||||
import React from 'react'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import clamp from 'lodash.clamp'
|
||||
import buffer from 'buffer'
|
||||
import get from 'lodash.get'
|
||||
import {unset} from 'lodash'
|
||||
import {arrayMoveMutable} from 'array-move'
|
||||
import url from 'url'
|
||||
import hash from "string-hash";
|
||||
import {Map, LayerSpecification, StyleSpecification, ValidationError, SourceSpecification} from 'maplibre-gl'
|
||||
import {latest, validate} from '@maplibre/maplibre-gl-style-spec'
|
||||
|
||||
import MapMapboxGl from './MapMapboxGl'
|
||||
import MapMaplibreGl from './MapMaplibreGl'
|
||||
import MapOpenLayers from './MapOpenLayers'
|
||||
import LayerList from './LayerList'
|
||||
import LayerEditor from './LayerEditor'
|
||||
import AppToolbar from './AppToolbar'
|
||||
import AppToolbar, { MapState } from './AppToolbar'
|
||||
import AppLayout from './AppLayout'
|
||||
import MessagePanel from './AppMessagePanel'
|
||||
|
||||
@@ -24,8 +27,7 @@ import ModalShortcuts from './ModalShortcuts'
|
||||
import ModalSurvey from './ModalSurvey'
|
||||
import ModalDebug from './ModalDebug'
|
||||
|
||||
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
|
||||
import {latest, validate} from '@mapbox/mapbox-gl-style-spec'
|
||||
import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata'
|
||||
import style from '../libs/style'
|
||||
import { initialStyleUrl, loadStyleUrl, removeStyleQuerystring } from '../libs/urlopen'
|
||||
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
||||
@@ -36,25 +38,13 @@ import LayerWatcher from '../libs/layerwatcher'
|
||||
import tokens from '../config/tokens.json'
|
||||
import isEqual from 'lodash.isequal'
|
||||
import Debug from '../libs/debug'
|
||||
import queryUtil from '../libs/query-util'
|
||||
import {formatLayerId} from '../util/format';
|
||||
import { SortEnd } from 'react-sortable-hoc';
|
||||
import { MapOptions } from 'maplibre-gl';
|
||||
|
||||
import MapboxGl from 'mapbox-gl'
|
||||
// Buffer must be defined globally for @maplibre/maplibre-gl-style-spec validate() function to succeed.
|
||||
window.Buffer = buffer.Buffer;
|
||||
|
||||
|
||||
// Similar functionality as <https://github.com/mapbox/mapbox-gl-js/blob/7e30aadf5177486c2cfa14fe1790c60e217b5e56/src/util/mapbox.js>
|
||||
function normalizeSourceURL (url, apiToken="") {
|
||||
const matches = url.match(/^mapbox:\/\/(.*)/);
|
||||
if (matches) {
|
||||
// mapbox://mapbox.mapbox-streets-v7
|
||||
return `https://api.mapbox.com/v4/${matches[1]}.json?secure&access_token=${apiToken}`
|
||||
}
|
||||
else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
function setFetchAccessToken(url, mapStyle) {
|
||||
function setFetchAccessToken(url: string, mapStyle: StyleSpecification) {
|
||||
const matchesTilehosting = url.match(/\.tilehosting\.com/);
|
||||
const matchesMaptiler = url.match(/\.maptiler\.com/);
|
||||
const matchesThunderforest = url.match(/\.thunderforest\.com/);
|
||||
@@ -75,7 +65,7 @@ function setFetchAccessToken(url, mapStyle) {
|
||||
}
|
||||
}
|
||||
|
||||
function updateRootSpec(spec, fieldName, newValues) {
|
||||
function updateRootSpec(spec: any, fieldName: string, newValues: any) {
|
||||
return {
|
||||
...spec,
|
||||
$root: {
|
||||
@@ -88,15 +78,75 @@ function updateRootSpec(spec, fieldName, newValues) {
|
||||
}
|
||||
}
|
||||
|
||||
export default class App extends React.Component {
|
||||
constructor(props) {
|
||||
type OnStyleChangedOpts = {
|
||||
save?: boolean
|
||||
addRevision?: boolean
|
||||
initialLoad?: boolean
|
||||
}
|
||||
|
||||
type MappedErrors = {
|
||||
message: string
|
||||
parsed?: {
|
||||
type: string
|
||||
data: {
|
||||
index: number
|
||||
key: string
|
||||
message: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type AppState = {
|
||||
errors: MappedErrors[],
|
||||
infos: string[],
|
||||
mapStyle: StyleSpecification & {id: string},
|
||||
dirtyMapStyle?: StyleSpecification,
|
||||
selectedLayerIndex: number,
|
||||
selectedLayerOriginalId?: string,
|
||||
sources: {[key: string]: SourceSpecification},
|
||||
vectorLayers: {},
|
||||
spec: any,
|
||||
mapView: {
|
||||
zoom: number,
|
||||
center: {
|
||||
lng: number,
|
||||
lat: number,
|
||||
},
|
||||
},
|
||||
maplibreGlDebugOptions: Partial<MapOptions> & {
|
||||
showTileBoundaries: boolean,
|
||||
showCollisionBoxes: boolean,
|
||||
showOverdrawInspector: boolean,
|
||||
},
|
||||
openlayersDebugOptions: {
|
||||
debugToolbox: boolean,
|
||||
},
|
||||
mapState: MapState
|
||||
isOpen: {
|
||||
settings: boolean
|
||||
sources: boolean
|
||||
open: boolean
|
||||
shortcuts: boolean
|
||||
export: boolean
|
||||
survey: boolean
|
||||
debug: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export default class App extends React.Component<any, AppState> {
|
||||
revisionStore: RevisionStore;
|
||||
styleStore: StyleStore | ApiStyleStore;
|
||||
layerWatcher: LayerWatcher;
|
||||
shortcutEl: ModalShortcuts | null = null;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
autoBind(this);
|
||||
|
||||
this.revisionStore = new RevisionStore()
|
||||
const params = new URLSearchParams(window.location.search.substring(1))
|
||||
let port = params.get("localport")
|
||||
if (port == null && (window.location.port != 80 && window.location.port != 443)) {
|
||||
if (port == null && (window.location.port !== "80" && window.location.port !== "443")) {
|
||||
port = window.location.port
|
||||
}
|
||||
this.styleStore = new ApiStyleStore({
|
||||
@@ -148,7 +198,7 @@ export default class App extends React.Component {
|
||||
{
|
||||
key: "m",
|
||||
handler: () => {
|
||||
document.querySelector(".mapboxgl-canvas").focus();
|
||||
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -161,7 +211,7 @@ export default class App extends React.Component {
|
||||
|
||||
document.body.addEventListener("keyup", (e) => {
|
||||
if(e.key === "Escape") {
|
||||
e.target.blur();
|
||||
(e.target as HTMLElement).blur();
|
||||
document.body.focus();
|
||||
}
|
||||
else if(this.state.isOpen.shortcuts || document.activeElement === document.body) {
|
||||
@@ -171,7 +221,7 @@ export default class App extends React.Component {
|
||||
|
||||
if(shortcut) {
|
||||
this.setModal("shortcuts", false);
|
||||
shortcut.handler(e);
|
||||
shortcut.handler();
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -204,8 +254,6 @@ export default class App extends React.Component {
|
||||
Debug.set("maputnik", "styleStore", this.styleStore);
|
||||
}
|
||||
|
||||
const queryObj = url.parse(window.location.href, true).query;
|
||||
|
||||
this.state = {
|
||||
errors: [],
|
||||
infos: [],
|
||||
@@ -232,7 +280,7 @@ export default class App extends React.Component {
|
||||
survey: false,
|
||||
debug: false,
|
||||
},
|
||||
mapboxGlDebugOptions: {
|
||||
maplibreGlDebugOptions: {
|
||||
showTileBoundaries: false,
|
||||
showCollisionBoxes: false,
|
||||
showOverdrawInspector: false,
|
||||
@@ -247,25 +295,25 @@ export default class App extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
handleKeyPress = (e) => {
|
||||
handleKeyPress = (e: KeyboardEvent) => {
|
||||
if(navigator.platform.toUpperCase().indexOf('MAC') >= 0) {
|
||||
if(e.metaKey && e.shiftKey && e.keyCode === 90) {
|
||||
e.preventDefault();
|
||||
this.onRedo(e);
|
||||
this.onRedo();
|
||||
}
|
||||
else if(e.metaKey && e.keyCode === 90) {
|
||||
e.preventDefault();
|
||||
this.onUndo(e);
|
||||
this.onUndo();
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(e.ctrlKey && e.keyCode === 90) {
|
||||
e.preventDefault();
|
||||
this.onUndo(e);
|
||||
this.onUndo();
|
||||
}
|
||||
else if(e.ctrlKey && e.keyCode === 89) {
|
||||
e.preventDefault();
|
||||
this.onRedo(e);
|
||||
this.onRedo();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,31 +326,31 @@ export default class App extends React.Component {
|
||||
window.removeEventListener("keydown", this.handleKeyPress);
|
||||
}
|
||||
|
||||
saveStyle(snapshotStyle) {
|
||||
saveStyle(snapshotStyle: StyleSpecification & {id: string}) {
|
||||
this.styleStore.save(snapshotStyle)
|
||||
}
|
||||
|
||||
updateFonts(urlTemplate) {
|
||||
const metadata = this.state.mapStyle.metadata || {}
|
||||
updateFonts(urlTemplate: string) {
|
||||
const metadata: {[key: string]: string} = this.state.mapStyle.metadata || {} as any
|
||||
const accessToken = metadata['maputnik:openmaptiles_access_token'] || tokens.openmaptiles
|
||||
|
||||
let glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
|
||||
const glyphUrl = (typeof urlTemplate === 'string')? urlTemplate.replace('{key}', accessToken): urlTemplate;
|
||||
downloadGlyphsMetadata(glyphUrl, fonts => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, 'glyphs', fonts)})
|
||||
})
|
||||
}
|
||||
|
||||
updateIcons(baseUrl) {
|
||||
updateIcons(baseUrl: string) {
|
||||
downloadSpriteMetadata(baseUrl, icons => {
|
||||
this.setState({ spec: updateRootSpec(this.state.spec, 'sprite', icons)})
|
||||
})
|
||||
}
|
||||
|
||||
onChangeMetadataProperty = (property, value) => {
|
||||
onChangeMetadataProperty = (property: string, value: any) => {
|
||||
// If we're changing renderer reset the map state.
|
||||
if (
|
||||
property === 'maputnik:renderer' &&
|
||||
value !== get(this.state.mapStyle, ['metadata', 'maputnik:renderer'], 'mbgljs')
|
||||
value !== get(this.state.mapStyle, ['metadata', 'maputnik:renderer'], 'mlgljs')
|
||||
) {
|
||||
this.setState({
|
||||
mapState: 'map'
|
||||
@@ -312,14 +360,14 @@ export default class App extends React.Component {
|
||||
const changedStyle = {
|
||||
...this.state.mapStyle,
|
||||
metadata: {
|
||||
...this.state.mapStyle.metadata,
|
||||
...(this.state.mapStyle as any).metadata,
|
||||
[property]: value
|
||||
}
|
||||
}
|
||||
this.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
onStyleChanged = (newStyle, opts={}) => {
|
||||
onStyleChanged = (newStyle: StyleSpecification & {id: string}, opts: OnStyleChangedOpts={}) => {
|
||||
opts = {
|
||||
save: true,
|
||||
addRevision: true,
|
||||
@@ -331,16 +379,16 @@ export default class App extends React.Component {
|
||||
this.getInitialStateFromUrl(newStyle);
|
||||
}
|
||||
|
||||
const errors = validate(newStyle, latest) || [];
|
||||
// This "any" can be removed in latest version of maplibre where maplibre re-exported types from style-spec
|
||||
const errors = validate(newStyle as any, latest) || [];
|
||||
|
||||
// The validate function doesn't give us errors for duplicate error with
|
||||
// empty string for layer.id, manually deal with that here.
|
||||
const layerErrors = [];
|
||||
const layerErrors: (Error | ValidationError)[] = [];
|
||||
if (newStyle && newStyle.layers) {
|
||||
const foundLayers = new Map();
|
||||
const foundLayers = new global.Map();
|
||||
newStyle.layers.forEach((layer, index) => {
|
||||
if (layer.id === "" && foundLayers.has(layer.id)) {
|
||||
const message = `Duplicate layer: ${formatLayerId(layer.id)}`;
|
||||
const error = new Error(
|
||||
`layers[${index}]: duplicate layer id [empty_string], previously used`
|
||||
);
|
||||
@@ -354,7 +402,7 @@ export default class App extends React.Component {
|
||||
// Special case: Duplicate layer id
|
||||
const dupMatch = error.message.match(/layers\[(\d+)\]: (duplicate layer id "?(.*)"?, previously used)/);
|
||||
if (dupMatch) {
|
||||
const [matchStr, index, message] = dupMatch;
|
||||
const [, index, message] = dupMatch;
|
||||
return {
|
||||
message: error.message,
|
||||
parsed: {
|
||||
@@ -371,7 +419,7 @@ export default class App extends React.Component {
|
||||
// Special case: Invalid source
|
||||
const invalidSourceMatch = error.message.match(/layers\[(\d+)\]: (source "(?:.*)" not found)/);
|
||||
if (invalidSourceMatch) {
|
||||
const [matchStr, index, message] = invalidSourceMatch;
|
||||
const [, index, message] = invalidSourceMatch;
|
||||
return {
|
||||
message: error.message,
|
||||
parsed: {
|
||||
@@ -387,7 +435,7 @@ export default class App extends React.Component {
|
||||
|
||||
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
|
||||
if (layerMatch) {
|
||||
const [matchStr, index, group, property, message] = layerMatch;
|
||||
const [, index, group, property, message] = layerMatch;
|
||||
const key = (group && property) ? [group, property].join(".") : property;
|
||||
return {
|
||||
message: error.message,
|
||||
@@ -408,7 +456,7 @@ export default class App extends React.Component {
|
||||
}
|
||||
});
|
||||
|
||||
let dirtyMapStyle = undefined;
|
||||
let dirtyMapStyle: StyleSpecification | undefined = undefined;
|
||||
if (errors.length > 0) {
|
||||
dirtyMapStyle = cloneDeep(newStyle);
|
||||
|
||||
@@ -418,7 +466,7 @@ export default class App extends React.Component {
|
||||
try {
|
||||
const objPath = message.split(":")[0];
|
||||
// Errors can be deply nested for example 'layers[0].filter[1][1][0]' we only care upto the property 'layers[0].filter'
|
||||
const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^\[]+/)[0];
|
||||
const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^[]+/)![0];
|
||||
unset(dirtyMapStyle, unsetPath);
|
||||
}
|
||||
catch (err) {
|
||||
@@ -429,19 +477,19 @@ export default class App extends React.Component {
|
||||
}
|
||||
|
||||
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
||||
this.updateFonts(newStyle.glyphs)
|
||||
this.updateFonts(newStyle.glyphs as string)
|
||||
}
|
||||
if(newStyle.sprite !== this.state.mapStyle.sprite) {
|
||||
this.updateIcons(newStyle.sprite)
|
||||
this.updateIcons(newStyle.sprite as string)
|
||||
}
|
||||
|
||||
if (opts.addRevision) {
|
||||
this.revisionStore.addRevision(newStyle);
|
||||
}
|
||||
if (opts.save) {
|
||||
this.saveStyle(newStyle);
|
||||
this.saveStyle(newStyle as StyleSpecification & {id: string});
|
||||
}
|
||||
|
||||
|
||||
this.setState({
|
||||
mapStyle: newStyle,
|
||||
dirtyMapStyle: dirtyMapStyle,
|
||||
@@ -472,7 +520,7 @@ export default class App extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
onMoveLayer = (move) => {
|
||||
onMoveLayer = (move: SortEnd) => {
|
||||
let { oldIndex, newIndex } = move;
|
||||
let layers = this.state.mapStyle.layers;
|
||||
oldIndex = clamp(oldIndex, 0, layers.length-1);
|
||||
@@ -490,7 +538,7 @@ export default class App extends React.Component {
|
||||
this.onLayersChange(layers);
|
||||
}
|
||||
|
||||
onLayersChange = (changedLayers) => {
|
||||
onLayersChange = (changedLayers: LayerSpecification[]) => {
|
||||
const changedStyle = {
|
||||
...this.state.mapStyle,
|
||||
layers: changedLayers
|
||||
@@ -498,15 +546,15 @@ export default class App extends React.Component {
|
||||
this.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
onLayerDestroy = (index) => {
|
||||
let layers = this.state.mapStyle.layers;
|
||||
onLayerDestroy = (index: number) => {
|
||||
const layers = this.state.mapStyle.layers;
|
||||
const remainingLayers = layers.slice(0);
|
||||
remainingLayers.splice(index, 1);
|
||||
this.onLayersChange(remainingLayers);
|
||||
}
|
||||
|
||||
onLayerCopy = (index) => {
|
||||
let layers = this.state.mapStyle.layers;
|
||||
onLayerCopy = (index: number) => {
|
||||
const layers = this.state.mapStyle.layers;
|
||||
const changedLayers = layers.slice(0)
|
||||
|
||||
const clonedLayer = cloneDeep(changedLayers[index])
|
||||
@@ -515,8 +563,8 @@ export default class App extends React.Component {
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
onLayerVisibilityToggle = (index) => {
|
||||
let layers = this.state.mapStyle.layers;
|
||||
onLayerVisibilityToggle = (index: number) => {
|
||||
const layers = this.state.mapStyle.layers;
|
||||
const changedLayers = layers.slice(0)
|
||||
|
||||
const layer = { ...changedLayers[index] }
|
||||
@@ -529,7 +577,7 @@ export default class App extends React.Component {
|
||||
}
|
||||
|
||||
|
||||
onLayerIdChange = (index, oldId, newId) => {
|
||||
onLayerIdChange = (index: number, _oldId: string, newId: string) => {
|
||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
||||
changedLayers[index] = {
|
||||
...changedLayers[index],
|
||||
@@ -539,27 +587,27 @@ export default class App extends React.Component {
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
onLayerChanged = (index, layer) => {
|
||||
onLayerChanged = (index: number, layer: LayerSpecification) => {
|
||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
||||
changedLayers[index] = layer
|
||||
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
setMapState = (newState) => {
|
||||
setMapState = (newState: MapState) => {
|
||||
this.setState({
|
||||
mapState: newState
|
||||
}, this.setStateInUrl);
|
||||
}
|
||||
|
||||
setDefaultValues = (styleObj) => {
|
||||
const metadata = styleObj.metadata || {}
|
||||
setDefaultValues = (styleObj: StyleSpecification & {id: string}) => {
|
||||
const metadata: {[key: string]: string} = styleObj.metadata || {} as any
|
||||
if(metadata['maputnik:renderer'] === undefined) {
|
||||
const changedStyle = {
|
||||
...styleObj,
|
||||
metadata: {
|
||||
...styleObj.metadata,
|
||||
'maputnik:renderer': 'mbgljs'
|
||||
...styleObj.metadata as any,
|
||||
'maputnik:renderer': 'mlgljs'
|
||||
}
|
||||
}
|
||||
return changedStyle
|
||||
@@ -568,19 +616,19 @@ export default class App extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
openStyle = (styleObj) => {
|
||||
openStyle = (styleObj: StyleSpecification & {id: string}) => {
|
||||
styleObj = this.setDefaultValues(styleObj)
|
||||
this.onStyleChanged(styleObj)
|
||||
}
|
||||
|
||||
fetchSources() {
|
||||
const sourceList = {};
|
||||
const sourceList: {[key: string]: any} = {};
|
||||
|
||||
for(let [key, val] of Object.entries(this.state.mapStyle.sources)) {
|
||||
for(const [key, val] of Object.entries(this.state.mapStyle.sources)) {
|
||||
if(
|
||||
!this.state.sources.hasOwnProperty(key) &&
|
||||
!Object.prototype.hasOwnProperty.call(this.state.sources, key) &&
|
||||
val.type === "vector" &&
|
||||
val.hasOwnProperty("url")
|
||||
Object.prototype.hasOwnProperty.call(val, "url")
|
||||
) {
|
||||
sourceList[key] = {
|
||||
type: val.type,
|
||||
@@ -588,45 +636,40 @@ export default class App extends React.Component {
|
||||
};
|
||||
|
||||
let url = val.url;
|
||||
try {
|
||||
url = normalizeSourceURL(url, MapboxGl.accessToken);
|
||||
} catch(err) {
|
||||
console.warn("Failed to normalizeSourceURL: ", err);
|
||||
}
|
||||
|
||||
try {
|
||||
url = setFetchAccessToken(url, this.state.mapStyle)
|
||||
url = setFetchAccessToken(url!, this.state.mapStyle)
|
||||
} catch(err) {
|
||||
console.warn("Failed to setFetchAccessToken: ", err);
|
||||
}
|
||||
|
||||
fetch(url, {
|
||||
fetch(url!, {
|
||||
mode: 'cors',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
|
||||
if(!json.hasOwnProperty("vector_layers")) {
|
||||
return;
|
||||
}
|
||||
if(!Object.prototype.hasOwnProperty.call(json, "vector_layers")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new objects before setState
|
||||
const sources = Object.assign({}, {
|
||||
[key]: this.state.sources[key],
|
||||
// Create new objects before setState
|
||||
const sources = Object.assign({}, {
|
||||
[key]: this.state.sources[key],
|
||||
});
|
||||
|
||||
for(const layer of json.vector_layers) {
|
||||
(sources[key] as any).layers.push(layer.id)
|
||||
}
|
||||
|
||||
console.debug("Updating source: "+key);
|
||||
this.setState({
|
||||
sources: sources
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Failed to process sources for '%s'", url, err);
|
||||
});
|
||||
|
||||
for(let layer of json.vector_layers) {
|
||||
sources[key].layers.push(layer.id)
|
||||
}
|
||||
|
||||
console.debug("Updating source: "+key);
|
||||
this.setState({
|
||||
sources: sources
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Failed to process sources for '%s'", url, err);
|
||||
});
|
||||
}
|
||||
else {
|
||||
sourceList[key] = this.state.sources[key] || this.state.mapStyle.sources[key];
|
||||
@@ -642,11 +685,17 @@ export default class App extends React.Component {
|
||||
}
|
||||
|
||||
_getRenderer () {
|
||||
const metadata = this.state.mapStyle.metadata || {};
|
||||
return metadata['maputnik:renderer'] || 'mbgljs';
|
||||
const metadata: {[key:string]: string} = this.state.mapStyle.metadata || {} as any;
|
||||
return metadata['maputnik:renderer'] || 'mlgljs';
|
||||
}
|
||||
|
||||
onMapChange = (mapView) => {
|
||||
onMapChange = (mapView: {
|
||||
zoom: number,
|
||||
center: {
|
||||
lng: number,
|
||||
lat: number,
|
||||
},
|
||||
}) => {
|
||||
this.setState({
|
||||
mapView,
|
||||
});
|
||||
@@ -654,16 +703,15 @@ export default class App extends React.Component {
|
||||
|
||||
mapRenderer() {
|
||||
const {mapStyle, dirtyMapStyle} = this.state;
|
||||
const metadata = this.state.mapStyle.metadata || {};
|
||||
|
||||
const mapProps = {
|
||||
mapStyle: (dirtyMapStyle || mapStyle),
|
||||
replaceAccessTokens: (mapStyle) => {
|
||||
replaceAccessTokens: (mapStyle: StyleSpecification) => {
|
||||
return style.replaceAccessTokens(mapStyle, {
|
||||
allowFallback: true
|
||||
});
|
||||
},
|
||||
onDataChange: (e) => {
|
||||
onDataChange: (e: {map: Map}) => {
|
||||
this.layerWatcher.analyzeMap(e.map)
|
||||
this.fetchSources();
|
||||
},
|
||||
@@ -682,9 +730,9 @@ export default class App extends React.Component {
|
||||
onLayerSelect={this.onLayerSelect}
|
||||
/>
|
||||
} else {
|
||||
mapElement = <MapMapboxGl {...mapProps}
|
||||
mapElement = <MapMaplibreGl {...mapProps}
|
||||
onChange={this.onMapChange}
|
||||
options={this.state.mapboxGlDebugOptions}
|
||||
options={this.state.maplibreGlDebugOptions}
|
||||
inspectModeEnabled={this.state.mapState === "inspect"}
|
||||
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
|
||||
onLayerSelect={this.onLayerSelect} />
|
||||
@@ -694,12 +742,12 @@ export default class App extends React.Component {
|
||||
if(this.state.mapState.match(/^filter-/)) {
|
||||
filterName = this.state.mapState.replace(/^filter-/, "");
|
||||
}
|
||||
const elementStyle = {};
|
||||
const elementStyle: {filter?: string} = {};
|
||||
if (filterName) {
|
||||
elementStyle.filter = `url('#${filterName}')`;
|
||||
};
|
||||
}
|
||||
|
||||
return <div style={elementStyle} className="maputnik-map__container">
|
||||
return <div style={elementStyle} className="maputnik-map__container" data-wd-key="maplibre:container">
|
||||
{mapElement}
|
||||
</div>
|
||||
}
|
||||
@@ -712,8 +760,8 @@ export default class App extends React.Component {
|
||||
url.searchParams.set("layer", `${hashVal}~${selectedLayerIndex}`);
|
||||
|
||||
const openModals = Object.entries(isOpen)
|
||||
.map(([key, val]) => (val === true ? key : null))
|
||||
.filter(val => val !== null);
|
||||
.map(([key, val]) => (val === true ? key : null))
|
||||
.filter(val => val !== null);
|
||||
|
||||
if (openModals.length > 0) {
|
||||
url.searchParams.set("modal", openModals.join(","));
|
||||
@@ -732,12 +780,12 @@ export default class App extends React.Component {
|
||||
history.replaceState({selectedLayerIndex}, "Maputnik", url.href);
|
||||
}
|
||||
|
||||
getInitialStateFromUrl = (mapStyle) => {
|
||||
getInitialStateFromUrl = (mapStyle: StyleSpecification) => {
|
||||
const url = new URL(location.href);
|
||||
const modalParam = url.searchParams.get("modal");
|
||||
if (modalParam && modalParam !== "") {
|
||||
const modals = modalParam.split(",");
|
||||
const modalObj = {};
|
||||
const modalObj: {[key: string]: boolean} = {};
|
||||
modals.forEach(modalName => {
|
||||
modalObj[modalName] = true;
|
||||
});
|
||||
@@ -752,7 +800,7 @@ export default class App extends React.Component {
|
||||
|
||||
const view = url.searchParams.get("view");
|
||||
if (view && view !== "") {
|
||||
this.setMapState(view);
|
||||
this.setMapState(view as MapState);
|
||||
}
|
||||
|
||||
const path = url.searchParams.get("layer");
|
||||
@@ -784,14 +832,14 @@ export default class App extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
onLayerSelect = (index) => {
|
||||
onLayerSelect = (index: number) => {
|
||||
this.setState({
|
||||
selectedLayerIndex: index,
|
||||
selectedLayerOriginalId: this.state.mapStyle.layers[index].id,
|
||||
}, this.setStateInUrl);
|
||||
}
|
||||
|
||||
setModal(modalName, value) {
|
||||
setModal(modalName: keyof AppState["isOpen"], value: boolean) {
|
||||
if(modalName === 'survey' && value === false) {
|
||||
localStorage.setItem('survey', '');
|
||||
}
|
||||
@@ -804,11 +852,11 @@ export default class App extends React.Component {
|
||||
}, this.setStateInUrl)
|
||||
}
|
||||
|
||||
toggleModal(modalName) {
|
||||
toggleModal(modalName: keyof AppState["isOpen"]) {
|
||||
this.setModal(modalName, !this.state.isOpen[modalName]);
|
||||
}
|
||||
|
||||
onChangeOpenlayersDebug = (key, value) => {
|
||||
onChangeOpenlayersDebug = (key: keyof AppState["openlayersDebugOptions"], value: boolean) => {
|
||||
this.setState({
|
||||
openlayersDebugOptions: {
|
||||
...this.state.openlayersDebugOptions,
|
||||
@@ -817,10 +865,10 @@ export default class App extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
onChangeMaboxGlDebug = (key, value) => {
|
||||
onChangeMaplibreGlDebug = (key: keyof AppState["maplibreGlDebugOptions"], value: any) => {
|
||||
this.setState({
|
||||
mapboxGlDebugOptions: {
|
||||
...this.state.mapboxGlDebugOptions,
|
||||
maplibreGlDebugOptions: {
|
||||
...this.state.maplibreGlDebugOptions,
|
||||
[key]: value,
|
||||
}
|
||||
});
|
||||
@@ -828,8 +876,7 @@ export default class App extends React.Component {
|
||||
|
||||
render() {
|
||||
const layers = this.state.mapStyle.layers || []
|
||||
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
|
||||
const metadata = this.state.mapStyle.metadata || {}
|
||||
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : undefined
|
||||
|
||||
const toolbar = <AppToolbar
|
||||
renderer={this._getRenderer()}
|
||||
@@ -872,7 +919,7 @@ export default class App extends React.Component {
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
|
||||
onLayerIdChange={this.onLayerIdChange}
|
||||
errors={this.state.errors}
|
||||
/> : null
|
||||
/> : undefined
|
||||
|
||||
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
|
||||
currentLayer={selectedLayer}
|
||||
@@ -881,15 +928,15 @@ export default class App extends React.Component {
|
||||
mapStyle={this.state.mapStyle}
|
||||
errors={this.state.errors}
|
||||
infos={this.state.infos}
|
||||
/> : null
|
||||
/> : undefined
|
||||
|
||||
|
||||
const modals = <div>
|
||||
<ModalDebug
|
||||
renderer={this._getRenderer()}
|
||||
mapboxGlDebugOptions={this.state.mapboxGlDebugOptions}
|
||||
maplibreGlDebugOptions={this.state.maplibreGlDebugOptions}
|
||||
openlayersDebugOptions={this.state.openlayersDebugOptions}
|
||||
onChangeMaboxGlDebug={this.onChangeMaboxGlDebug}
|
||||
onChangeMaplibreGlDebug={this.onChangeMaplibreGlDebug}
|
||||
onChangeOpenlayersDebug={this.onChangeOpenlayersDebug}
|
||||
isOpen={this.state.isOpen.debug}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'debug')}
|
||||
@@ -906,7 +953,6 @@ export default class App extends React.Component {
|
||||
onChangeMetadataProperty={this.onChangeMetadataProperty}
|
||||
isOpen={this.state.isOpen.settings}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'settings')}
|
||||
openlayersDebugOptions={this.state.openlayersDebugOptions}
|
||||
/>
|
||||
<ModalExport
|
||||
mapStyle={this.state.mapStyle}
|
||||
@@ -2,16 +2,16 @@ import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ScrollContainer from './ScrollContainer'
|
||||
|
||||
class AppLayout extends React.Component {
|
||||
static propTypes = {
|
||||
toolbar: PropTypes.element.isRequired,
|
||||
layerList: PropTypes.element.isRequired,
|
||||
layerEditor: PropTypes.element,
|
||||
map: PropTypes.element.isRequired,
|
||||
bottom: PropTypes.element,
|
||||
modals: PropTypes.node,
|
||||
}
|
||||
type AppLayoutProps = {
|
||||
toolbar: React.ReactElement
|
||||
layerList: React.ReactElement
|
||||
layerEditor?: React.ReactElement
|
||||
map: React.ReactElement
|
||||
bottom?: React.ReactElement
|
||||
modals?: React.ReactNode
|
||||
};
|
||||
|
||||
class AppLayout extends React.Component<AppLayoutProps> {
|
||||
static childContextTypes = {
|
||||
reactIconBase: PropTypes.object
|
||||
}
|
||||
@@ -35,8 +35,8 @@ class AppLayout extends React.Component {
|
||||
</div>
|
||||
{this.props.map}
|
||||
{this.props.bottom && <div className="maputnik-layout-bottom">
|
||||
{this.props.bottom}
|
||||
</div>
|
||||
{this.props.bottom}
|
||||
</div>
|
||||
}
|
||||
{this.props.modals}
|
||||
</div>
|
||||
@@ -1,29 +1,28 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {formatLayerId} from '../util/format';
|
||||
import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
|
||||
|
||||
export default class AppMessagePanel extends React.Component {
|
||||
static propTypes = {
|
||||
errors: PropTypes.array,
|
||||
infos: PropTypes.array,
|
||||
mapStyle: PropTypes.object,
|
||||
onLayerSelect: PropTypes.func,
|
||||
currentLayer: PropTypes.object,
|
||||
selectedLayerIndex: PropTypes.number,
|
||||
}
|
||||
type AppMessagePanelProps = {
|
||||
errors?: unknown[]
|
||||
infos?: unknown[]
|
||||
mapStyle?: StyleSpecification
|
||||
onLayerSelect?(...args: unknown[]): unknown
|
||||
currentLayer?: LayerSpecification
|
||||
selectedLayerIndex?: number
|
||||
};
|
||||
|
||||
export default class AppMessagePanel extends React.Component<AppMessagePanelProps> {
|
||||
static defaultProps = {
|
||||
onLayerSelect: () => {},
|
||||
}
|
||||
|
||||
render() {
|
||||
const {selectedLayerIndex} = this.props;
|
||||
const errors = this.props.errors.map((error, idx) => {
|
||||
const errors = this.props.errors?.map((error: any, idx) => {
|
||||
let content;
|
||||
if (error.parsed && error.parsed.type === "layer") {
|
||||
const {parsed} = error;
|
||||
const {mapStyle, currentLayer} = this.props;
|
||||
const layerId = mapStyle.layers[parsed.data.index].id;
|
||||
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
|
||||
content = (
|
||||
<>
|
||||
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
|
||||
@@ -32,7 +31,7 @@ export default class AppMessagePanel extends React.Component {
|
||||
—
|
||||
<button
|
||||
className="maputnik-message-panel__switch-button"
|
||||
onClick={() => this.props.onLayerSelect(parsed.data.index)}
|
||||
onClick={() => this.props.onLayerSelect!(parsed.data.index)}
|
||||
>
|
||||
switch to layer
|
||||
</button>
|
||||
@@ -49,7 +48,7 @@ export default class AppMessagePanel extends React.Component {
|
||||
</p>
|
||||
})
|
||||
|
||||
const infos = this.props.infos.map((m, i) => {
|
||||
const infos = this.props.infos?.map((m, i) => {
|
||||
return <p key={"info-"+i}>{m}</p>
|
||||
})
|
||||
|
||||
@@ -1,38 +1,35 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import {detect} from 'detect-browser';
|
||||
|
||||
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdAssignmentTurnedIn} from 'react-icons/md'
|
||||
|
||||
|
||||
import logoImage from 'maputnik-design/logos/logo-color.svg'
|
||||
import pkgJson from '../../package.json'
|
||||
|
||||
|
||||
// This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of.
|
||||
const browser = detect();
|
||||
const colorAccessibilityFiltersEnabled = ['chrome', 'firefox'].indexOf(browser.name) > -1;
|
||||
const colorAccessibilityFiltersEnabled = ['chrome', 'firefox'].indexOf(browser!.name) > -1;
|
||||
|
||||
|
||||
class IconText extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
}
|
||||
type IconTextProps = {
|
||||
children?: React.ReactNode
|
||||
};
|
||||
|
||||
|
||||
class IconText extends React.Component<IconTextProps> {
|
||||
render() {
|
||||
return <span className="maputnik-icon-text">{this.props.children}</span>
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarLink extends React.Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
href: PropTypes.string,
|
||||
onToggleModal: PropTypes.func,
|
||||
}
|
||||
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)}
|
||||
@@ -45,14 +42,14 @@ class ToolbarLink extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarLinkHighlighted extends React.Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
href: PropTypes.string,
|
||||
onToggleModal: PropTypes.func
|
||||
}
|
||||
type ToolbarLinkHighlightedProps = {
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
href?: string
|
||||
onToggleModal?(...args: unknown[]): unknown
|
||||
};
|
||||
|
||||
class ToolbarLinkHighlighted extends React.Component<ToolbarLinkHighlightedProps> {
|
||||
render() {
|
||||
return <a
|
||||
className={classnames('maputnik-toolbar-link', "maputnik-toolbar-link--highlighted", this.props.className)}
|
||||
@@ -67,12 +64,12 @@ class ToolbarLinkHighlighted extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarSelect extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
wdKey: PropTypes.string
|
||||
}
|
||||
type ToolbarSelectProps = {
|
||||
children?: React.ReactNode
|
||||
wdKey?: string
|
||||
};
|
||||
|
||||
class ToolbarSelect extends React.Component<ToolbarSelectProps> {
|
||||
render() {
|
||||
return <div
|
||||
className='maputnik-toolbar-select'
|
||||
@@ -83,13 +80,13 @@ class ToolbarSelect extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarAction extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
onClick: PropTypes.func,
|
||||
wdKey: PropTypes.string
|
||||
}
|
||||
type ToolbarActionProps = {
|
||||
children?: React.ReactNode
|
||||
onClick?(...args: unknown[]): unknown
|
||||
wdKey?: string
|
||||
};
|
||||
|
||||
class ToolbarAction extends React.Component<ToolbarActionProps> {
|
||||
render() {
|
||||
return <button
|
||||
className='maputnik-toolbar-action'
|
||||
@@ -101,22 +98,24 @@ class ToolbarAction extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default class AppToolbar extends React.Component {
|
||||
static propTypes = {
|
||||
mapStyle: PropTypes.object.isRequired,
|
||||
inspectModeEnabled: PropTypes.bool.isRequired,
|
||||
onStyleChanged: PropTypes.func.isRequired,
|
||||
// A new style has been uploaded
|
||||
onStyleOpen: PropTypes.func.isRequired,
|
||||
// A dict of source id's and the available source layers
|
||||
sources: PropTypes.object.isRequired,
|
||||
children: PropTypes.node,
|
||||
onToggleModal: PropTypes.func,
|
||||
onSetMapState: PropTypes.func,
|
||||
mapState: PropTypes.string,
|
||||
renderer: PropTypes.string,
|
||||
}
|
||||
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
|
||||
|
||||
type AppToolbarProps = {
|
||||
mapStyle: object
|
||||
inspectModeEnabled: boolean
|
||||
onStyleChanged(...args: unknown[]): unknown
|
||||
// A new style has been uploaded
|
||||
onStyleOpen(...args: unknown[]): unknown
|
||||
// A dict of source id's and the available source layers
|
||||
sources: object
|
||||
children?: React.ReactNode
|
||||
onToggleModal(...args: unknown[]): unknown
|
||||
onSetMapState(mapState: MapState): unknown
|
||||
mapState?: MapState
|
||||
renderer?: string
|
||||
};
|
||||
|
||||
export default class AppToolbar extends React.Component<AppToolbarProps> {
|
||||
state = {
|
||||
isOpen: {
|
||||
settings: false,
|
||||
@@ -127,16 +126,16 @@ export default class AppToolbar extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleSelection(val) {
|
||||
handleSelection(val: MapState) {
|
||||
this.props.onSetMapState(val);
|
||||
}
|
||||
|
||||
onSkip = (target) => {
|
||||
onSkip = (target: string) => {
|
||||
if (target === "map") {
|
||||
document.querySelector(".mapboxgl-canvas").focus();
|
||||
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
|
||||
}
|
||||
else {
|
||||
const el = document.querySelector("#skip-target-"+target);
|
||||
const el = document.querySelector("#skip-target-"+target) as HTMLButtonElement;
|
||||
el.focus();
|
||||
}
|
||||
}
|
||||
@@ -152,7 +151,7 @@ export default class AppToolbar extends React.Component {
|
||||
id: "inspect",
|
||||
group: "general",
|
||||
title: "Inspect",
|
||||
disabled: this.props.renderer !== 'mbgljs',
|
||||
disabled: this.props.renderer === 'ol',
|
||||
},
|
||||
{
|
||||
id: "filter-deuteranopia",
|
||||
@@ -193,21 +192,21 @@ export default class AppToolbar extends React.Component {
|
||||
<button
|
||||
data-wd-key="root:skip:layer-list"
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={e => this.onSkip("layer-list")}
|
||||
onClick={_e => this.onSkip("layer-list")}
|
||||
>
|
||||
Layers list
|
||||
</button>
|
||||
<button
|
||||
data-wd-key="root:skip:layer-editor"
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={e => this.onSkip("layer-editor")}
|
||||
onClick={_e => this.onSkip("layer-editor")}
|
||||
>
|
||||
Layer editor
|
||||
</button>
|
||||
<button
|
||||
data-wd-key="root:skip:map-view"
|
||||
className="maputnik-toolbar-skip"
|
||||
onClick={e => this.onSkip("map")}
|
||||
onClick={_e => this.onSkip("map")}
|
||||
>
|
||||
Map view
|
||||
</button>
|
||||
@@ -217,7 +216,7 @@ export default class AppToolbar extends React.Component {
|
||||
rel="noreferrer noopener"
|
||||
href="https://github.com/maputnik/editor"
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{__html: logoImage}} />
|
||||
<img src="node_modules/maputnik-design/logos/logo-color.svg" />
|
||||
<h1>
|
||||
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
|
||||
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
|
||||
@@ -247,12 +246,13 @@ export default class AppToolbar extends React.Component {
|
||||
<label>View
|
||||
<select
|
||||
className="maputnik-select"
|
||||
onChange={(e) => this.handleSelection(e.target.value)}
|
||||
value={currentView.id}
|
||||
data-wd-key="maputnik-select"
|
||||
onChange={(e) => this.handleSelection(e.target.value as MapState)}
|
||||
value={currentView?.id}
|
||||
>
|
||||
{views.filter(v => v.group === "general").map((item) => {
|
||||
return (
|
||||
<option key={item.id} value={item.id} disabled={item.disabled}>
|
||||
<option key={item.id} value={item.id} disabled={item.disabled} data-wd-key={item.id}>
|
||||
{item.title}
|
||||
</option>
|
||||
);
|
||||
@@ -1,40 +1,43 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {SyntheticEvent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import FieldDocLabel from './FieldDocLabel'
|
||||
import Doc from './Doc'
|
||||
|
||||
|
||||
/** Wrap a component with a label */
|
||||
export default class Block extends React.Component {
|
||||
static propTypes = {
|
||||
"data-wd-key": PropTypes.string,
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.element,
|
||||
]),
|
||||
action: PropTypes.element,
|
||||
children: PropTypes.node.isRequired,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
fieldSpec: PropTypes.object,
|
||||
wideMode: PropTypes.bool,
|
||||
error: PropTypes.array,
|
||||
}
|
||||
type BlockProps = {
|
||||
"data-wd-key"?: string
|
||||
label?: string
|
||||
action?: React.ReactElement
|
||||
style?: object
|
||||
onChange?(...args: unknown[]): unknown
|
||||
fieldSpec?: object
|
||||
wideMode?: boolean
|
||||
error?: {message: string}
|
||||
};
|
||||
|
||||
constructor (props) {
|
||||
type BlockState = {
|
||||
showDoc: boolean
|
||||
};
|
||||
|
||||
/** Wrap a component with a label */
|
||||
export default class Block extends React.Component<BlockProps, BlockState> {
|
||||
_blockEl: HTMLDivElement | null = null;
|
||||
|
||||
constructor (props: BlockProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showDoc: false,
|
||||
}
|
||||
}
|
||||
|
||||
onChange(e) {
|
||||
onChange(e: React.BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) {
|
||||
const value = e.target.value
|
||||
return this.props.onChange(value === "" ? undefined : value)
|
||||
if (this.props.onChange) {
|
||||
return this.props.onChange(value === "" ? undefined : value)
|
||||
}
|
||||
}
|
||||
|
||||
onToggleDoc = (val) => {
|
||||
onToggleDoc = (val: boolean) => {
|
||||
this.setState({
|
||||
showDoc: val
|
||||
});
|
||||
@@ -46,20 +49,17 @@ export default class Block extends React.Component {
|
||||
* causing the picker to reopen. This causes a scenario where the picker can
|
||||
* never be closed once open.
|
||||
*/
|
||||
onLabelClick = (event) => {
|
||||
onLabelClick = (event: SyntheticEvent<any, any>) => {
|
||||
const el = event.nativeEvent.target;
|
||||
const nativeEvent = event.nativeEvent;
|
||||
const contains = this._blockEl.contains(el);
|
||||
const contains = this._blockEl?.contains(el);
|
||||
|
||||
if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
event.preventDefault();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
render() {
|
||||
const errors = [].concat(this.props.error || []);
|
||||
|
||||
return <label style={this.props.style}
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
className={classnames({
|
||||
@@ -1,21 +1,21 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Collapse as ReactCollapse from 'react-collapse'
|
||||
import accessibility from '../../libs/accessibility'
|
||||
import { Collapse as ReactCollapse } from 'react-collapse'
|
||||
import {reducedMotionEnabled} from '../libs/accessibility'
|
||||
|
||||
|
||||
export default class Collapse extends React.Component {
|
||||
static propTypes = {
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
children: PropTypes.element.isRequired
|
||||
}
|
||||
type CollapseProps = {
|
||||
isActive: boolean
|
||||
children: React.ReactElement
|
||||
};
|
||||
|
||||
|
||||
export default class Collapse extends React.Component<CollapseProps> {
|
||||
static defaultProps = {
|
||||
isActive: true
|
||||
}
|
||||
|
||||
render() {
|
||||
if (accessibility.reducedMotionEnabled()) {
|
||||
if (reducedMotionEnabled()) {
|
||||
return (
|
||||
<div style={{display: this.props.isActive ? "block" : "none"}}>
|
||||
{this.props.children}
|
||||
@@ -1,13 +1,12 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {MdArrowDropDown, MdArrowDropUp} from 'react-icons/md'
|
||||
|
||||
export default class Collapser extends React.Component {
|
||||
static propTypes = {
|
||||
isCollapsed: PropTypes.bool.isRequired,
|
||||
style: PropTypes.object,
|
||||
}
|
||||
type CollapserProps = {
|
||||
isCollapsed: boolean
|
||||
style?: object
|
||||
};
|
||||
|
||||
export default class Collapser extends React.Component<CollapserProps> {
|
||||
render() {
|
||||
const iconStyle = {
|
||||
width: 20,
|
||||
@@ -1,36 +1,45 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default class Doc extends React.Component {
|
||||
static propTypes = {
|
||||
fieldSpec: PropTypes.object.isRequired,
|
||||
const headers = {
|
||||
js: "JS",
|
||||
android: "Android",
|
||||
ios: "iOS",
|
||||
macos: "macOS",
|
||||
};
|
||||
|
||||
type DocProps = {
|
||||
fieldSpec: {
|
||||
doc?: string
|
||||
values?: {
|
||||
[key: string]: {
|
||||
doc?: string
|
||||
}
|
||||
}
|
||||
'sdk-support'?: {
|
||||
[key: string]: typeof headers
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default class Doc extends React.Component<DocProps> {
|
||||
render () {
|
||||
const {fieldSpec} = this.props;
|
||||
|
||||
const {doc, values} = fieldSpec;
|
||||
const sdkSupport = fieldSpec['sdk-support'];
|
||||
|
||||
const headers = {
|
||||
js: "JS",
|
||||
android: "Android",
|
||||
ios: "iOS",
|
||||
macos: "macOS",
|
||||
};
|
||||
|
||||
const renderValues = (
|
||||
!!values &&
|
||||
// HACK: Currently we merge additional values into the stylespec, so this is required
|
||||
// See <https://github.com/maputnik/editor/blob/master/src/components/fields/PropertyGroup.jsx#L16>
|
||||
// HACK: Currently we merge additional values into the style spec, so this is required
|
||||
// See <https://github.com/maputnik/editor/blob/main/src/components/PropertyGroup.jsx#L16>
|
||||
!Array.isArray(values)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{doc &&
|
||||
{doc &&
|
||||
<div className="SpecDoc">
|
||||
<div className="SpecDoc__doc">{doc}</div>
|
||||
<div className="SpecDoc__doc" data-wd-key='spec-field-doc'>{doc}</div>
|
||||
{renderValues &&
|
||||
<ul className="SpecDoc__values">
|
||||
{Object.entries(values).map(([key, value]) => {
|
||||
@@ -61,10 +70,9 @@ export default class Doc extends React.Component {
|
||||
return (
|
||||
<tr key={key}>
|
||||
<td>{key}</td>
|
||||
{Object.keys(headers).map(k => {
|
||||
const value = supportObj[k];
|
||||
if (supportObj.hasOwnProperty(k)) {
|
||||
return <td key={k}>{supportObj[k]}</td>;
|
||||
{Object.keys(headers).map((k) => {
|
||||
if (Object.prototype.hasOwnProperty.call(supportObj, k)) {
|
||||
return <td key={k}>{supportObj[k as keyof typeof headers]}</td>;
|
||||
}
|
||||
else {
|
||||
return <td key={k}>no</td>;
|
||||
@@ -80,4 +88,4 @@ export default class Doc extends React.Component {
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Block from './Block'
|
||||
import InputArray from './InputArray'
|
||||
import Fieldset from './Fieldset'
|
||||
|
||||
export default class FieldArray extends React.Component {
|
||||
static propTypes = {
|
||||
...InputArray.propTypes,
|
||||
name: PropTypes.string,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
|
||||
return <Fieldset label={props.label}>
|
||||
<InputArray {...props} />
|
||||
</Fieldset>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
import InputArray, { FieldArrayProps as InputArrayProps } from './InputArray'
|
||||
import Fieldset from './Fieldset'
|
||||
|
||||
type FieldArrayProps = InputArrayProps & {
|
||||
name?: string
|
||||
fieldSpec?: {
|
||||
doc: string
|
||||
}
|
||||
};
|
||||
|
||||
export default class FieldArray extends React.Component<FieldArrayProps> {
|
||||
render() {
|
||||
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputArray {...this.props} />
|
||||
</Fieldset>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Block from './Block'
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
|
||||
|
||||
export default class FieldAutocomplete extends React.Component {
|
||||
static propTypes = {
|
||||
...InputAutocomplete.propTypes,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
|
||||
return <Block label={props.label}>
|
||||
<InputAutocomplete {...props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete'
|
||||
|
||||
|
||||
type FieldAutocompleteProps = InputAutocompleteProps & {
|
||||
label?: string;
|
||||
};
|
||||
|
||||
|
||||
export default class FieldAutocomplete extends React.Component<FieldAutocompleteProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label}>
|
||||
<InputAutocomplete {...this.props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Block from './Block'
|
||||
import InputCheckbox from './InputCheckbox'
|
||||
|
||||
|
||||
export default class FieldCheckbox extends React.Component {
|
||||
static propTypes = {
|
||||
...InputCheckbox.propTypes,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
|
||||
return <Block label={this.props.label}>
|
||||
<InputCheckbox {...props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import InputCheckbox, {InputCheckboxProps} from './InputCheckbox'
|
||||
|
||||
|
||||
type FieldCheckboxProps = InputCheckboxProps & {
|
||||
label?: string;
|
||||
};
|
||||
|
||||
|
||||
export default class FieldCheckbox extends React.Component<FieldCheckboxProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label}>
|
||||
<InputCheckbox {...this.props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Block from './Block'
|
||||
import InputColor from './InputColor'
|
||||
|
||||
|
||||
export default class FieldColor extends React.Component {
|
||||
static propTypes = {
|
||||
...InputColor.propTypes,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
|
||||
return <Block label={props.label}>
|
||||
<InputColor {...props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import InputColor, {InputColorProps} from './InputColor'
|
||||
|
||||
|
||||
type FieldColorProps = InputColorProps & {
|
||||
label?: string
|
||||
fieldSpec?: {
|
||||
doc: string
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export default class FieldColor extends React.Component<FieldColorProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputColor {...this.props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Block from './Block'
|
||||
import InputString from './InputString'
|
||||
|
||||
export default class FieldComment extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
type FieldCommentProps = {
|
||||
value?: string
|
||||
onChange(value: string | undefined): unknown
|
||||
error: {message: string}
|
||||
};
|
||||
|
||||
export default class FieldComment extends React.Component<FieldCommentProps> {
|
||||
render() {
|
||||
const fieldSpec = {
|
||||
doc: "Comments for the current layer. This is non-standard and not in the spec."
|
||||
@@ -19,6 +19,7 @@ export default class FieldComment extends React.Component {
|
||||
label={"Comments"}
|
||||
fieldSpec={fieldSpec}
|
||||
data-wd-key="layer-comment"
|
||||
error={this.props.error}
|
||||
>
|
||||
<InputString
|
||||
multi={true}
|
||||
@@ -1,26 +1,27 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
|
||||
|
||||
export default class FieldDocLabel extends React.Component {
|
||||
static propTypes = {
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.string
|
||||
]).isRequired,
|
||||
fieldSpec: PropTypes.object,
|
||||
onToggleDoc: PropTypes.func,
|
||||
type FieldDocLabelProps = {
|
||||
label: object | string | undefined
|
||||
fieldSpec?: {
|
||||
doc?: string
|
||||
}
|
||||
onToggleDoc?(...args: unknown[]): unknown
|
||||
};
|
||||
|
||||
constructor (props) {
|
||||
type FieldDocLabelState = {
|
||||
open: boolean
|
||||
};
|
||||
|
||||
export default class FieldDocLabel extends React.Component<FieldDocLabelProps, FieldDocLabelState> {
|
||||
constructor (props: FieldDocLabelProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
open: false,
|
||||
}
|
||||
}
|
||||
|
||||
onToggleDoc = (open) => {
|
||||
onToggleDoc = (open: boolean) => {
|
||||
this.setState({
|
||||
open,
|
||||
}, () => {
|
||||
@@ -43,6 +44,7 @@ export default class FieldDocLabel extends React.Component {
|
||||
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}
|
||||
>
|
||||
{this.state.open ? <MdHighlightOff /> : <MdInfoOutline />}
|
||||
</button>
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Block from './Block'
|
||||
import InputDynamicArray from './InputDynamicArray'
|
||||
import Fieldset from './Fieldset'
|
||||
|
||||
export default class FieldDynamicArray extends React.Component {
|
||||
static propTypes = {
|
||||
...InputDynamicArray.propTypes,
|
||||
name: PropTypes.string,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
|
||||
return <Fieldset label={props.label}>
|
||||
<InputDynamicArray {...props} />
|
||||
</Fieldset>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import InputDynamicArray, {FieldDynamicArrayProps as InputDynamicArrayProps} from './InputDynamicArray'
|
||||
import Fieldset from './Fieldset'
|
||||
|
||||
type FieldDynamicArrayProps = InputDynamicArrayProps & {
|
||||
name?: string
|
||||
};
|
||||
|
||||
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
|
||||
render() {
|
||||
return <Fieldset label={this.props.label}>
|
||||
<InputDynamicArray {...this.props} />
|
||||
</Fieldset>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputEnum from './InputEnum'
|
||||
import Block from './Block';
|
||||
import Fieldset from './Fieldset';
|
||||
|
||||
|
||||
export default class FieldEnum extends React.Component {
|
||||
static propTypes = {
|
||||
...InputEnum.propTypes,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
|
||||
return <Fieldset label={props.label}>
|
||||
<InputEnum {...props} />
|
||||
</Fieldset>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import InputEnum, {InputEnumProps} from './InputEnum'
|
||||
import Fieldset from './Fieldset';
|
||||
|
||||
|
||||
type FieldEnumProps = InputEnumProps & {
|
||||
label?: string;
|
||||
fieldSpec?: {
|
||||
doc: string
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export default class FieldEnum extends React.Component<FieldEnumProps> {
|
||||
render() {
|
||||
return <Fieldset label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputEnum {...this.props} />
|
||||
</Fieldset>
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import SpecProperty from './_SpecProperty'
|
||||
import DataProperty from './_DataProperty'
|
||||
import DataProperty, { Stop } from './_DataProperty'
|
||||
import ZoomProperty from './_ZoomProperty'
|
||||
import ExpressionProperty from './_ExpressionProperty'
|
||||
import {function as styleFunction} from '@mapbox/mapbox-gl-style-spec';
|
||||
import {function as styleFunction} from '@maplibre/maplibre-gl-style-spec';
|
||||
import {findDefaultFromSpec} from '../util/spec-helper';
|
||||
|
||||
|
||||
function isLiteralExpression (value) {
|
||||
function isLiteralExpression(value: any) {
|
||||
return (Array.isArray(value) && value.length === 2 && value[0] === "literal");
|
||||
}
|
||||
|
||||
function isGetExpression (value) {
|
||||
function isGetExpression(value: any) {
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
value.length === 2 &&
|
||||
@@ -21,14 +20,14 @@ function isGetExpression (value) {
|
||||
);
|
||||
}
|
||||
|
||||
function isZoomField(value) {
|
||||
function isZoomField(value: any) {
|
||||
return (
|
||||
typeof(value) === 'object' &&
|
||||
value.stops &&
|
||||
typeof(value.property) === 'undefined' &&
|
||||
Array.isArray(value.stops) &&
|
||||
value.stops.length > 1 &&
|
||||
value.stops.every(stop => {
|
||||
value.stops.every((stop: Stop) => {
|
||||
return (
|
||||
Array.isArray(stop) &&
|
||||
stop.length === 2
|
||||
@@ -37,22 +36,22 @@ function isZoomField(value) {
|
||||
);
|
||||
}
|
||||
|
||||
function isIdentityProperty (value) {
|
||||
function isIdentityProperty(value: any) {
|
||||
return (
|
||||
typeof(value) === 'object' &&
|
||||
value.type === "identity" &&
|
||||
value.hasOwnProperty("property")
|
||||
Object.prototype.hasOwnProperty.call(value, "property")
|
||||
);
|
||||
}
|
||||
|
||||
function isDataStopProperty (value) {
|
||||
function isDataStopProperty(value: any) {
|
||||
return (
|
||||
typeof(value) === 'object' &&
|
||||
value.stops &&
|
||||
typeof(value.property) !== 'undefined' &&
|
||||
value.stops.length > 1 &&
|
||||
Array.isArray(value.stops) &&
|
||||
value.stops.every(stop => {
|
||||
value.stops.every((stop: Stop) => {
|
||||
return (
|
||||
Array.isArray(stop) &&
|
||||
stop.length === 2 &&
|
||||
@@ -62,26 +61,26 @@ function isDataStopProperty (value) {
|
||||
);
|
||||
}
|
||||
|
||||
function isDataField(value) {
|
||||
function isDataField(value: any) {
|
||||
return (
|
||||
isIdentityProperty(value) ||
|
||||
isDataStopProperty(value)
|
||||
);
|
||||
}
|
||||
|
||||
function isPrimative (value) {
|
||||
function isPrimative(value: any): value is string | boolean | number {
|
||||
const valid = ["string", "boolean", "number"];
|
||||
return valid.includes(typeof(value));
|
||||
}
|
||||
|
||||
function isArrayOfPrimatives (values) {
|
||||
function isArrayOfPrimatives(values: any): values is Array<string | boolean | number> {
|
||||
if (Array.isArray(values)) {
|
||||
return values.every(isPrimative);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getDataType (value, fieldSpec={}) {
|
||||
function getDataType(value: any, fieldSpec={} as any) {
|
||||
if (value === undefined) {
|
||||
return "value";
|
||||
}
|
||||
@@ -103,35 +102,33 @@ function getDataType (value, fieldSpec={}) {
|
||||
}
|
||||
|
||||
|
||||
type FieldFunctionProps = {
|
||||
onChange(fieldName: string, value: any): unknown
|
||||
fieldName: string
|
||||
fieldType: string
|
||||
fieldSpec: any
|
||||
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
|
||||
*/
|
||||
export default class FieldFunction extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
fieldName: PropTypes.string.isRequired,
|
||||
fieldType: PropTypes.string.isRequired,
|
||||
fieldSpec: PropTypes.object.isRequired,
|
||||
errors: PropTypes.object,
|
||||
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.object,
|
||||
PropTypes.string,
|
||||
PropTypes.number,
|
||||
PropTypes.bool,
|
||||
PropTypes.array
|
||||
]),
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super();
|
||||
export default class FieldFunction extends React.Component<FieldFunctionProps, FieldFunctionState> {
|
||||
constructor (props: FieldFunctionProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
dataType: getDataType(props.value, props.fieldSpec),
|
||||
isEditing: false,
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
static getDerivedStateFromProps(props: FieldFunctionProps, state: FieldFunctionState) {
|
||||
// Because otherwise when editing values we end up accidentally changing field type.
|
||||
if (state.isEditing) {
|
||||
return {};
|
||||
@@ -144,7 +141,7 @@ export default class FieldFunction extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
getFieldFunctionType(fieldSpec) {
|
||||
getFieldFunctionType(fieldSpec: any) {
|
||||
if (fieldSpec.expression.interpolated) {
|
||||
return "exponential"
|
||||
}
|
||||
@@ -183,7 +180,7 @@ export default class FieldFunction extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
deleteStop = (stopIdx) => {
|
||||
deleteStop = (stopIdx: number) => {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
stops.splice(stopIdx, 1)
|
||||
|
||||
@@ -207,7 +204,7 @@ export default class FieldFunction extends React.Component {
|
||||
if (value.stops) {
|
||||
zoomFunc = {
|
||||
base: value.base,
|
||||
stops: value.stops.map(stop => {
|
||||
stops: value.stops.map((stop: Stop) => {
|
||||
return [stop[0].zoom, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
|
||||
})
|
||||
}
|
||||
@@ -292,7 +289,7 @@ export default class FieldFunction extends React.Component {
|
||||
property: "",
|
||||
type: functionType,
|
||||
base: value.base,
|
||||
stops: value.stops.map(stop => {
|
||||
stops: value.stops.map((stop: Stop) => {
|
||||
return [{zoom: stop[0], value: stopValue}, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
|
||||
})
|
||||
}
|
||||
@@ -366,7 +363,7 @@ export default class FieldFunction extends React.Component {
|
||||
onDeleteStop={this.deleteStop}
|
||||
onAddStop={this.addStop}
|
||||
onChangeToDataFunction={this.makeDataFunction}
|
||||
onExpressionClick={this.makeExpression}
|
||||
onExpressionClick={this.makeExpression}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -383,7 +380,7 @@ export default class FieldFunction extends React.Component {
|
||||
onDeleteStop={this.deleteStop}
|
||||
onAddStop={this.addStop}
|
||||
onChangeToZoomFunction={this.makeZoomFunction}
|
||||
onExpressionClick={this.makeExpression}
|
||||
onExpressionClick={this.makeExpression}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -397,8 +394,8 @@ export default class FieldFunction extends React.Component {
|
||||
fieldSpec={this.props.fieldSpec}
|
||||
value={this.props.value}
|
||||
onZoomClick={this.makeZoomFunction}
|
||||
onDataClick={this.makeDataFunction}
|
||||
onExpressionClick={this.makeExpression}
|
||||
onDataClick={this.makeDataFunction}
|
||||
onExpressionClick={this.makeExpression}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +1,17 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import Block from './Block'
|
||||
import InputString from './InputString'
|
||||
|
||||
export default class FieldId extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
wdKey: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
error: PropTypes.object,
|
||||
}
|
||||
type FieldIdProps = {
|
||||
value: string
|
||||
wdKey: string
|
||||
onChange(value: string | undefined): unknown
|
||||
error?: {message: string}
|
||||
};
|
||||
|
||||
export default class FieldId extends React.Component<FieldIdProps> {
|
||||
render() {
|
||||
return <Block label={"ID"} fieldSpec={latest.layer.id}
|
||||
data-wd-key={this.props.wdKey}
|
||||
@@ -1,16 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputJson from './InputJson'
|
||||
|
||||
|
||||
export default class FieldJson extends React.Component {
|
||||
static propTypes = {
|
||||
...InputJson.propTypes,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
return <InputJson {...props} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
import InputJson, {InputJsonProps} from './InputJson'
|
||||
|
||||
|
||||
type FieldJsonProps = InputJsonProps & {};
|
||||
|
||||
|
||||
export default class FieldJson extends React.Component<FieldJsonProps> {
|
||||
render() {
|
||||
return <InputJson {...this.props} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import Block from './Block'
|
||||
import InputNumber from './InputNumber'
|
||||
|
||||
export default class FieldMaxZoom extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.number,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
error: PropTypes.object,
|
||||
}
|
||||
type FieldMaxZoomProps = {
|
||||
value?: number
|
||||
onChange(value: number | undefined): unknown
|
||||
error?: {message: string}
|
||||
};
|
||||
|
||||
export default class FieldMaxZoom extends React.Component<FieldMaxZoomProps> {
|
||||
render() {
|
||||
return <Block label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
|
||||
error={this.props.error}
|
||||
@@ -1,17 +1,16 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import Block from './Block'
|
||||
import InputNumber from './InputNumber'
|
||||
|
||||
export default class FieldMinZoom extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.number,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
error: PropTypes.object,
|
||||
}
|
||||
type FieldMinZoomProps = {
|
||||
value?: number
|
||||
onChange(...args: unknown[]): unknown
|
||||
error?: {message: string}
|
||||
};
|
||||
|
||||
export default class FieldMinZoom extends React.Component<FieldMinZoomProps> {
|
||||
render() {
|
||||
return <Block label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
|
||||
error={this.props.error}
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Block from './Block'
|
||||
import InputMultiInput from './InputMultiInput'
|
||||
import Fieldset from './Fieldset'
|
||||
|
||||
|
||||
export default class FieldMultiInput extends React.Component {
|
||||
static propTypes = {
|
||||
...InputMultiInput.propTypes,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
|
||||
return <Fieldset label={props.label}>
|
||||
<InputMultiInput {...props} />
|
||||
</Fieldset>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import InputMultiInput, {InputMultiInputProps} from './InputMultiInput'
|
||||
import Fieldset from './Fieldset'
|
||||
|
||||
|
||||
type FieldMultiInputProps = InputMultiInputProps & {
|
||||
label?: string
|
||||
};
|
||||
|
||||
|
||||
export default class FieldMultiInput extends React.Component<FieldMultiInputProps> {
|
||||
render() {
|
||||
return <Fieldset label={this.props.label}>
|
||||
<InputMultiInput {...this.props} />
|
||||
</Fieldset>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputNumber from './InputNumber'
|
||||
import Block from './Block'
|
||||
|
||||
|
||||
export default class FieldNumber extends React.Component {
|
||||
static propTypes = {
|
||||
...InputNumber.propTypes,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
return <Block label={props.label}>
|
||||
<InputNumber {...props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import InputNumber, {InputNumberProps} from './InputNumber'
|
||||
import Block from './Block'
|
||||
|
||||
|
||||
type FieldNumberProps = InputNumberProps & {
|
||||
label?: string
|
||||
fieldSpec?: {
|
||||
doc: string
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export default class FieldNumber extends React.Component<FieldNumberProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputNumber {...this.props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Block from './Block'
|
||||
import InputSelect from './InputSelect'
|
||||
|
||||
|
||||
export default class FieldSelect extends React.Component {
|
||||
static propTypes = {
|
||||
...InputSelect.propTypes,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
|
||||
return <Block label={props.label}>
|
||||
<InputSelect {...props}/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import InputSelect, {InputSelectProps} from './InputSelect'
|
||||
|
||||
|
||||
type FieldSelectProps = InputSelectProps & {
|
||||
label?: string
|
||||
fieldSpec?: {
|
||||
doc: string
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export default class FieldSelect extends React.Component<FieldSelectProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputSelect {...this.props}/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import Block from './Block'
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
|
||||
export default class FieldSource extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
wdKey: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
sourceIds: PropTypes.array,
|
||||
error: PropTypes.object,
|
||||
}
|
||||
type FieldSourceProps = {
|
||||
value?: string
|
||||
wdKey?: string
|
||||
onChange?(value: string| undefined): unknown
|
||||
sourceIds?: unknown[]
|
||||
error?: {message: string}
|
||||
};
|
||||
|
||||
export default class FieldSource extends React.Component<FieldSourceProps> {
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceIds: [],
|
||||
@@ -29,7 +28,7 @@ export default class FieldSource extends React.Component {
|
||||
<InputAutocomplete
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
options={this.props.sourceIds.map(src => [src, src])}
|
||||
options={this.props.sourceIds?.map(src => [src, src])}
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||
import Block from './Block'
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
|
||||
export default class FieldSourceLayer extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
sourceLayerIds: PropTypes.array,
|
||||
isFixed: PropTypes.bool,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceLayerIds: [],
|
||||
isFixed: false
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Block label={"Source Layer"} fieldSpec={latest.layer['source-layer']}
|
||||
data-wd-key="layer-source-layer"
|
||||
>
|
||||
<InputAutocomplete
|
||||
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
options={this.props.sourceLayerIds.map(l => [l, l])}
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import Block from './Block'
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
|
||||
type FieldSourceLayerProps = {
|
||||
value?: string
|
||||
onChange?(...args: unknown[]): unknown
|
||||
sourceLayerIds?: unknown[]
|
||||
isFixed?: boolean
|
||||
error?: {message: string}
|
||||
};
|
||||
|
||||
export default class FieldSourceLayer extends React.Component<FieldSourceLayerProps> {
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
sourceLayerIds: [],
|
||||
isFixed: false
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Block
|
||||
label={"Source Layer"}
|
||||
fieldSpec={latest.layer['source-layer']}
|
||||
data-wd-key="layer-source-layer"
|
||||
error={this.props.error}
|
||||
>
|
||||
<InputAutocomplete
|
||||
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
options={this.props.sourceLayerIds?.map(l => [l, l])}
|
||||
/>
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Block from './Block'
|
||||
import InputString from './InputString'
|
||||
|
||||
export default class FieldString extends React.Component {
|
||||
static propTypes = {
|
||||
...InputString.propTypes,
|
||||
name: PropTypes.string,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
|
||||
return <Block label={props.label}>
|
||||
<InputString {...props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
import Block from './Block'
|
||||
import InputString, {InputStringProps} from './InputString'
|
||||
|
||||
type FieldStringProps = InputStringProps & {
|
||||
name?: string
|
||||
label?: string
|
||||
fieldSpec?: {
|
||||
doc: string
|
||||
}
|
||||
};
|
||||
|
||||
export default class FieldString extends React.Component<FieldStringProps> {
|
||||
render() {
|
||||
return <Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputString {...this.props} />
|
||||
</Block>
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||
import {latest} from '@maplibre/maplibre-gl-style-spec'
|
||||
import Block from './Block'
|
||||
import InputSelect from './InputSelect'
|
||||
import InputString from './InputString'
|
||||
|
||||
export default class FieldType extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
wdKey: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
error: PropTypes.object,
|
||||
disabled: PropTypes.bool,
|
||||
}
|
||||
type FieldTypeProps = {
|
||||
value: string
|
||||
wdKey?: string
|
||||
onChange(value: string): unknown
|
||||
error?: {message: string}
|
||||
disabled?: boolean
|
||||
};
|
||||
|
||||
export default class FieldType extends React.Component<FieldTypeProps> {
|
||||
static defaultProps = {
|
||||
disabled: false,
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import React, {Fragment} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputUrl from './InputUrl'
|
||||
import Block from './Block'
|
||||
|
||||
|
||||
export default class FieldUrl extends React.Component {
|
||||
static propTypes = {
|
||||
...InputUrl.propTypes,
|
||||
}
|
||||
|
||||
render () {
|
||||
const {props} = this;
|
||||
|
||||
return (
|
||||
<Block label={this.props.label}>
|
||||
<InputUrl {...props} />
|
||||
</Block>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
import InputUrl, {FieldUrlProps as InputUrlProps} from './InputUrl'
|
||||
import Block from './Block'
|
||||
|
||||
|
||||
type FieldUrlProps = InputUrlProps & {
|
||||
label: string;
|
||||
fieldSpec?: {
|
||||
doc: string
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export default class FieldUrl extends React.Component<FieldUrlProps> {
|
||||
render () {
|
||||
return (
|
||||
<Block label={this.props.label} fieldSpec={this.props.fieldSpec}>
|
||||
<InputUrl {...this.props} />
|
||||
</Block>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,36 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { ReactElement } from 'react'
|
||||
import FieldDocLabel from './FieldDocLabel'
|
||||
import Doc from './Doc'
|
||||
import generateUniqueId from '../libs/document-uid';
|
||||
|
||||
type FieldsetProps = {
|
||||
label?: string,
|
||||
fieldSpec?: { doc?: string },
|
||||
action?: ReactElement,
|
||||
};
|
||||
|
||||
let IDX = 0;
|
||||
type FieldsetState = {
|
||||
showDoc: boolean
|
||||
};
|
||||
|
||||
export default class Fieldset extends React.Component {
|
||||
constructor (props) {
|
||||
export default class Fieldset extends React.Component<FieldsetProps, FieldsetState> {
|
||||
_labelId: string;
|
||||
|
||||
constructor (props: FieldsetProps) {
|
||||
super(props);
|
||||
this._labelId = `fieldset_label_${(IDX++)}`;
|
||||
this._labelId = generateUniqueId(`fieldset_label_`);
|
||||
this.state = {
|
||||
showDoc: false,
|
||||
}
|
||||
}
|
||||
|
||||
onToggleDoc = (val) => {
|
||||
onToggleDoc = (val: boolean) => {
|
||||
this.setState({
|
||||
showDoc: val
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
const {props} = this;
|
||||
|
||||
return <div className="maputnik-input-block" role="group" aria-labelledby={this._labelId}>
|
||||
{this.props.fieldSpec &&
|
||||
<div className="maputnik-input-block-label">
|
||||
@@ -36,14 +43,14 @@ export default class Fieldset extends React.Component {
|
||||
}
|
||||
{!this.props.fieldSpec &&
|
||||
<div className="maputnik-input-block-label">
|
||||
{props.label}
|
||||
{this.props.label}
|
||||
</div>
|
||||
}
|
||||
<div className="maputnik-input-block-action">
|
||||
{this.props.action}
|
||||
</div>
|
||||
<div className="maputnik-input-block-content">
|
||||
{props.children}
|
||||
{this.props.children}
|
||||
</div>
|
||||
{this.props.fieldSpec &&
|
||||
<div
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { combiningFilterOps } from '../libs/filterops.js'
|
||||
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 {latest, validate, migrate, convertFilter} from '@mapbox/mapbox-gl-style-spec'
|
||||
import {combiningFilterOps} from '../libs/filterops'
|
||||
import InputSelect from './InputSelect'
|
||||
import Block from './Block'
|
||||
import SingleFilterEditor from './SingleFilterEditor'
|
||||
@@ -12,11 +13,10 @@ import FilterEditorBlock from './FilterEditorBlock'
|
||||
import InputButton from './InputButton'
|
||||
import Doc from './Doc'
|
||||
import ExpressionProperty from './_ExpressionProperty';
|
||||
import {mdiFunctionVariant} from '@mdi/js';
|
||||
|
||||
|
||||
function combiningFilter (props) {
|
||||
let filter = props.filter || ['all'];
|
||||
function combiningFilter(props: FilterEditorProps): LegacyFilterSpecification | ExpressionSpecification {
|
||||
const filter = props.filter || ['all'];
|
||||
|
||||
if (!Array.isArray(filter)) {
|
||||
return filter;
|
||||
@@ -30,19 +30,20 @@ function combiningFilter (props) {
|
||||
filters = [filter.slice(0)];
|
||||
}
|
||||
|
||||
return [combiningOp, ...filters];
|
||||
return [combiningOp, ...filters] as LegacyFilterSpecification | ExpressionSpecification;
|
||||
}
|
||||
|
||||
function migrateFilter (filter) {
|
||||
return migrate(createStyleFromFilter(filter)).layers[0].filter;
|
||||
function migrateFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
|
||||
// This "any" can be removed in latest version of maplibre where maplibre re-exported types from style-spec
|
||||
return (migrate(createStyleFromFilter(filter) as any).layers[0] as any).filter;
|
||||
}
|
||||
|
||||
function createStyleFromFilter (filter) {
|
||||
function createStyleFromFilter(filter: LegacyFilterSpecification | ExpressionSpecification): StyleSpecification & {id: string} {
|
||||
return {
|
||||
"id": "tmp",
|
||||
"version": 8,
|
||||
"name": "Empty Style",
|
||||
"metadata": {"maputnik:renderer": "mbgljs"},
|
||||
"metadata": {"maputnik:renderer": "mlgljs"},
|
||||
"sources": {
|
||||
"tmp": {
|
||||
"type": "geojson",
|
||||
@@ -69,7 +70,7 @@ const FILTER_OPS = [
|
||||
];
|
||||
|
||||
// If we convert a filter that is an expression to an expression it'll remain the same in value
|
||||
function checkIfSimpleFilter (filter) {
|
||||
function checkIfSimpleFilter (filter: LegacyFilterSpecification | ExpressionSpecification) {
|
||||
if (filter.length === 1 && FILTER_OPS.includes(filter[0])) {
|
||||
return true;
|
||||
}
|
||||
@@ -77,33 +78,38 @@ function checkIfSimpleFilter (filter) {
|
||||
return !isEqual(expression, filter);
|
||||
}
|
||||
|
||||
function hasCombiningFilter(filter) {
|
||||
function hasCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
|
||||
return combiningFilterOps.indexOf(filter[0]) >= 0
|
||||
}
|
||||
|
||||
function hasNestedCombiningFilter(filter) {
|
||||
function hasNestedCombiningFilter(filter: LegacyFilterSpecification | ExpressionSpecification) {
|
||||
if(hasCombiningFilter(filter)) {
|
||||
const combinedFilters = filter.slice(1)
|
||||
return filter.slice(1).map(f => hasCombiningFilter(f)).filter(f => f == true).length > 0
|
||||
return filter.slice(1).map(f => hasCombiningFilter(f as any)).filter(f => f == true).length > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export default class FilterEditor extends React.Component {
|
||||
static propTypes = {
|
||||
/** Properties of the vector layer and the available fields */
|
||||
properties: PropTypes.object,
|
||||
filter: PropTypes.array,
|
||||
errors: PropTypes.object,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
type FilterEditorProps = {
|
||||
/** Properties of the vector layer and the available fields */
|
||||
properties?: {[key:string]: any}
|
||||
filter?: any[]
|
||||
errors?: {[key:string]: any}
|
||||
onChange(value: LegacyFilterSpecification | ExpressionSpecification): unknown
|
||||
};
|
||||
|
||||
type FilterEditorState = {
|
||||
showDoc: boolean
|
||||
displaySimpleFilter: boolean
|
||||
valueIsSimpleFilter?: boolean
|
||||
};
|
||||
|
||||
export default class FilterEditor extends React.Component<FilterEditorProps, FilterEditorState> {
|
||||
static defaultProps = {
|
||||
filter: ["all"],
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super();
|
||||
constructor (props: FilterEditorProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showDoc: false,
|
||||
displaySimpleFilter: checkIfSimpleFilter(combiningFilter(props)),
|
||||
@@ -111,25 +117,25 @@ export default class FilterEditor extends React.Component {
|
||||
}
|
||||
|
||||
// Convert filter to combining filter
|
||||
onFilterPartChanged(filterIdx, newPart) {
|
||||
const newFilter = combiningFilter(this.props).slice(0)
|
||||
onFilterPartChanged(filterIdx: number, newPart: any[]) {
|
||||
const newFilter = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
|
||||
newFilter[filterIdx] = newPart
|
||||
this.props.onChange(newFilter)
|
||||
}
|
||||
|
||||
deleteFilterItem(filterIdx) {
|
||||
const newFilter = combiningFilter(this.props).slice(0)
|
||||
deleteFilterItem(filterIdx: number) {
|
||||
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)
|
||||
newFilterItem.push(['==', 'name', ''])
|
||||
const newFilterItem = combiningFilter(this.props).slice(0) as LegacyFilterSpecification | ExpressionSpecification
|
||||
(newFilterItem as any[]).push(['==', 'name', ''])
|
||||
this.props.onChange(newFilterItem)
|
||||
}
|
||||
|
||||
onToggleDoc = (val) => {
|
||||
onToggleDoc = (val: boolean) => {
|
||||
this.setState({
|
||||
showDoc: val
|
||||
});
|
||||
@@ -142,15 +148,14 @@ export default class FilterEditor extends React.Component {
|
||||
}
|
||||
|
||||
makeExpression = () => {
|
||||
let filter = combiningFilter(this.props);
|
||||
const filter = combiningFilter(this.props);
|
||||
this.props.onChange(migrateFilter(filter));
|
||||
this.setState({
|
||||
displaySimpleFilter: false,
|
||||
})
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps (props, currentState) {
|
||||
const {filter} = props;
|
||||
static getDerivedStateFromProps(props: FilterEditorProps, currentState: FilterEditorState) {
|
||||
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
|
||||
|
||||
// Upgrade but never downgrade
|
||||
@@ -178,7 +183,7 @@ export default class FilterEditor extends React.Component {
|
||||
const fieldSpec={
|
||||
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
|
||||
};
|
||||
const defaultFilter = ["all"];
|
||||
const defaultFilter = ["all"] as LegacyFilterSpecification | ExpressionSpecification;
|
||||
|
||||
const isNestedCombiningFilter = displaySimpleFilter && hasNestedCombiningFilter(combiningFilter(this.props));
|
||||
|
||||
@@ -200,8 +205,8 @@ export default class FilterEditor extends React.Component {
|
||||
}
|
||||
else if (displaySimpleFilter) {
|
||||
const filter = combiningFilter(this.props);
|
||||
let combiningOp = filter[0];
|
||||
let filters = filter.slice(1)
|
||||
const combiningOp = filter[0];
|
||||
const filters = filter.slice(1) as (LegacyFilterSpecification | ExpressionSpecification)[];
|
||||
|
||||
const actions = (
|
||||
<div>
|
||||
@@ -218,7 +223,7 @@ export default class FilterEditor extends React.Component {
|
||||
);
|
||||
|
||||
const editorBlocks = filters.map((f, idx) => {
|
||||
const error = errors[`filter[${idx+1}]`];
|
||||
const error = errors![`filter[${idx+1}]`];
|
||||
|
||||
return (
|
||||
<div key={`block-${idx}`}>
|
||||
@@ -247,7 +252,7 @@ export default class FilterEditor extends React.Component {
|
||||
>
|
||||
<InputSelect
|
||||
value={combiningOp}
|
||||
onChange={this.onFilterPartChanged.bind(this, 0)}
|
||||
onChange={(v: [string, any]) => this.onFilterPartChanged(0, v)}
|
||||
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
|
||||
/>
|
||||
</Block>
|
||||
@@ -277,7 +282,7 @@ export default class FilterEditor extends React.Component {
|
||||
);
|
||||
}
|
||||
else {
|
||||
let {filter} = this.props;
|
||||
const {filter} = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -1,14 +1,12 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputButton from './InputButton'
|
||||
import {MdDelete} from 'react-icons/md'
|
||||
|
||||
export default class FilterEditorBlock extends React.Component {
|
||||
static propTypes = {
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
children: PropTypes.element.isRequired,
|
||||
}
|
||||
type FilterEditorBlockProps = {
|
||||
onDelete(...args: unknown[]): unknown
|
||||
};
|
||||
|
||||
export default class FilterEditorBlock extends React.Component<FilterEditorBlockProps> {
|
||||
render() {
|
||||
return <div className="maputnik-filter-editor-block">
|
||||
<div className="maputnik-filter-editor-block-action">
|
||||
@@ -1,33 +0,0 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import IconLine from './IconLine.jsx'
|
||||
import IconFill from './IconFill.jsx'
|
||||
import IconSymbol from './IconSymbol.jsx'
|
||||
import IconBackground from './IconBackground.jsx'
|
||||
import IconCircle from './IconCircle.jsx'
|
||||
import IconMissing from './IconMissing.jsx'
|
||||
|
||||
export default class IconLayer extends React.Component {
|
||||
static propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
style: PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
const iconProps = { style: this.props.style }
|
||||
switch(this.props.type) {
|
||||
case 'fill-extrusion': return <IconBackground {...iconProps} />
|
||||
case 'raster': return <IconFill {...iconProps} />
|
||||
case 'hillshade': return <IconFill {...iconProps} />
|
||||
case 'heatmap': return <IconFill {...iconProps} />
|
||||
case 'fill': return <IconFill {...iconProps} />
|
||||
case 'background': return <IconBackground {...iconProps} />
|
||||
case 'line': return <IconLine {...iconProps} />
|
||||
case 'symbol': return <IconSymbol {...iconProps} />
|
||||
case 'circle': return <IconCircle {...iconProps} />
|
||||
default: return <IconMissing {...iconProps} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react'
|
||||
|
||||
import IconLine from './IconLine'
|
||||
import IconFill from './IconFill'
|
||||
import IconSymbol from './IconSymbol'
|
||||
import IconBackground from './IconBackground'
|
||||
import IconCircle from './IconCircle'
|
||||
import IconMissing from './IconMissing'
|
||||
|
||||
type IconLayerProps = {
|
||||
type: string
|
||||
style?: object
|
||||
className?: string
|
||||
};
|
||||
|
||||
export default class IconLayer extends React.Component<IconLayerProps> {
|
||||
render() {
|
||||
const iconProps = { style: this.props.style }
|
||||
switch(this.props.type) {
|
||||
case 'fill-extrusion': return <IconBackground {...iconProps} />
|
||||
case 'raster': return <IconFill {...iconProps} />
|
||||
case 'hillshade': return <IconFill {...iconProps} />
|
||||
case 'heatmap': return <IconFill {...iconProps} />
|
||||
case 'fill': return <IconFill {...iconProps} />
|
||||
case 'background': return <IconBackground {...iconProps} />
|
||||
case 'line': return <IconLine {...iconProps} />
|
||||
case 'symbol': return <IconSymbol {...iconProps} />
|
||||
case 'circle': return <IconCircle {...iconProps} />
|
||||
default: return <IconMissing {...iconProps} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputString from './InputString'
|
||||
import InputNumber from './InputNumber'
|
||||
|
||||
export default class FieldArray extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.array,
|
||||
type: PropTypes.string,
|
||||
length: PropTypes.number,
|
||||
default: PropTypes.array,
|
||||
onChange: PropTypes.func,
|
||||
'aria-label': PropTypes.string,
|
||||
}
|
||||
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
|
||||
label?: string
|
||||
};
|
||||
|
||||
type FieldArrayState = {
|
||||
value: (string | number | undefined)[]
|
||||
initialPropsValue: unknown[]
|
||||
}
|
||||
|
||||
export default class FieldArray extends React.Component<FieldArrayProps, FieldArrayState> {
|
||||
static defaultProps = {
|
||||
value: [],
|
||||
default: [],
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
constructor (props: FieldArrayProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
value: this.props.value.slice(0),
|
||||
@@ -27,8 +32,8 @@ export default class FieldArray extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
const value = [];
|
||||
static getDerivedStateFromProps(props: FieldArrayProps, state: FieldArrayState) {
|
||||
const value: any[] = [];
|
||||
const initialPropsValue = state.initialPropsValue.slice(0);
|
||||
|
||||
Array(props.length).fill(null).map((_, i) => {
|
||||
@@ -47,24 +52,24 @@ export default class FieldArray extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
isComplete (value) {
|
||||
isComplete(value: unknown[]) {
|
||||
return Array(this.props.length).fill(null).every((_, i) => {
|
||||
const val = value[i]
|
||||
return !(val === undefined || val === "");
|
||||
});
|
||||
}
|
||||
|
||||
changeValue(idx, newValue) {
|
||||
changeValue(idx: number, newValue: string | number | undefined) {
|
||||
const value = this.state.value.slice(0);
|
||||
value[idx] = newValue;
|
||||
|
||||
this.setState({
|
||||
value,
|
||||
}, () => {
|
||||
if (this.isComplete(value)) {
|
||||
if (this.isComplete(value) && this.props.onChange) {
|
||||
this.props.onChange(value);
|
||||
}
|
||||
else {
|
||||
else if (this.props.onChange){
|
||||
// Unset until complete
|
||||
this.props.onChange(undefined);
|
||||
}
|
||||
@@ -85,17 +90,17 @@ export default class FieldArray extends React.Component {
|
||||
if(this.props.type === 'number') {
|
||||
return <InputNumber
|
||||
key={i}
|
||||
default={containsValues ? undefined : this.props.default[i]}
|
||||
value={value[i]}
|
||||
default={containsValues || !this.props.default ? undefined : this.props.default[i] as number}
|
||||
value={value[i] as number}
|
||||
required={containsValues ? true : false}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
onChange={(v) => this.changeValue(i, v)}
|
||||
aria-label={this.props['aria-label'] || this.props.label}
|
||||
/>
|
||||
} else {
|
||||
return <InputString
|
||||
key={i}
|
||||
default={containsValues ? undefined : this.props.default[i]}
|
||||
value={value[i]}
|
||||
default={containsValues || !this.props.default ? undefined : this.props.default[i] as string}
|
||||
value={value[i] as string}
|
||||
required={containsValues ? true : false}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
aria-label={this.props['aria-label'] || this.props.label}
|
||||
@@ -1,24 +1,25 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import Autocomplete from 'react-autocomplete'
|
||||
|
||||
|
||||
const MAX_HEIGHT = 140;
|
||||
|
||||
export default class InputAutocomplete extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
options: PropTypes.array,
|
||||
onChange: PropTypes.func,
|
||||
keepMenuWithinWindowBounds: PropTypes.bool,
|
||||
'aria-label': PropTypes.string,
|
||||
}
|
||||
export type InputAutocompleteProps = {
|
||||
value?: string
|
||||
options: any[]
|
||||
onChange(value: string | undefined): unknown
|
||||
keepMenuWithinWindowBounds?: boolean
|
||||
'aria-label'?: string
|
||||
};
|
||||
|
||||
export default class InputAutocomplete extends React.Component<InputAutocompleteProps> {
|
||||
state = {
|
||||
maxHeight: MAX_HEIGHT
|
||||
}
|
||||
|
||||
autocompleteMenuEl: HTMLDivElement | null = null;
|
||||
|
||||
static defaultProps = {
|
||||
onChange: () => {},
|
||||
options: [],
|
||||
@@ -26,7 +27,7 @@ export default class InputAutocomplete extends React.Component {
|
||||
|
||||
calcMaxHeight() {
|
||||
if(this.props.keepMenuWithinWindowBounds) {
|
||||
const maxHeight = window.innerHeight - this.autocompleteMenuEl.getBoundingClientRect().top;
|
||||
const maxHeight = window.innerHeight - this.autocompleteMenuEl!.getBoundingClientRect().top;
|
||||
const limitedMaxHeight = Math.min(maxHeight, MAX_HEIGHT);
|
||||
|
||||
if(limitedMaxHeight != this.state.maxHeight) {
|
||||
@@ -45,7 +46,7 @@ export default class InputAutocomplete extends React.Component {
|
||||
this.calcMaxHeight();
|
||||
}
|
||||
|
||||
onChange (v) {
|
||||
onChange(v: string) {
|
||||
this.props.onChange(v === "" ? undefined : v);
|
||||
}
|
||||
|
||||
@@ -64,7 +65,7 @@ export default class InputAutocomplete extends React.Component {
|
||||
}}
|
||||
wrapperProps={{
|
||||
className: "maputnik-autocomplete",
|
||||
style: null
|
||||
style: {}
|
||||
}}
|
||||
inputProps={{
|
||||
'aria-label': this.props['aria-label'],
|
||||
@@ -75,11 +76,12 @@ export default class InputAutocomplete extends React.Component {
|
||||
items={this.props.options}
|
||||
getItemValue={(item) => item[0]}
|
||||
onSelect={v => this.onChange(v)}
|
||||
onChange={(e, 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
|
||||
@@ -89,7 +91,7 @@ export default class InputAutocomplete extends React.Component {
|
||||
"maputnik-autocomplete-menu-item-selected": isHighlighted,
|
||||
})}
|
||||
>
|
||||
{item[1]}
|
||||
{item[1]}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
@@ -1,21 +1,20 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
export default class InputButton extends React.Component {
|
||||
static propTypes = {
|
||||
"data-wd-key": PropTypes.string,
|
||||
"aria-label": PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
disabled: PropTypes.bool,
|
||||
type: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
}
|
||||
type InputButtonProps = {
|
||||
"data-wd-key"?: string
|
||||
"aria-label"?: string
|
||||
onClick?(...args: unknown[]): unknown
|
||||
style?: object
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
disabled?: boolean
|
||||
type?: typeof HTMLButtonElement.prototype.type
|
||||
id?: string
|
||||
title?: string
|
||||
};
|
||||
|
||||
export default class InputButton extends React.Component<InputButtonProps> {
|
||||
render() {
|
||||
return <button
|
||||
id={this.props.id}
|
||||
@@ -31,5 +30,4 @@ export default class InputButton extends React.Component {
|
||||
{this.props.children}
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default class InputCheckbox extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.bool,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
}
|
||||
export type InputCheckboxProps = {
|
||||
value?: boolean
|
||||
style?: object
|
||||
onChange(...args: unknown[]): unknown
|
||||
};
|
||||
|
||||
export default class InputCheckbox extends React.Component<InputCheckboxProps> {
|
||||
static defaultProps = {
|
||||
value: false,
|
||||
}
|
||||
@@ -28,12 +27,11 @@ export default class InputCheckbox extends React.Component {
|
||||
/>
|
||||
<div className="maputnik-checkbox-box">
|
||||
<svg style={{
|
||||
display: this.props.value ? 'inline' : 'none'
|
||||
}} className="maputnik-checkbox-icon" viewBox='0 0 32 32'>
|
||||
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>
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,36 +1,37 @@
|
||||
import React from 'react'
|
||||
import Color from 'color'
|
||||
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
|
||||
import PropTypes from 'prop-types'
|
||||
import {ColorResult} from 'react-color';
|
||||
import lodash from 'lodash';
|
||||
|
||||
function formatColor(color) {
|
||||
function formatColor(color: ColorResult): string {
|
||||
const rgb = color.rgb
|
||||
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`
|
||||
}
|
||||
|
||||
/*** Number fields with support for min, max and units and documentation*/
|
||||
export default class InputColor extends React.Component {
|
||||
static propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
name: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
doc: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
default: PropTypes.string,
|
||||
'aria-label': PropTypes.string,
|
||||
}
|
||||
export type InputColorProps = {
|
||||
onChange(...args: unknown[]): unknown
|
||||
name?: string
|
||||
value?: string
|
||||
doc?: string
|
||||
style?: object
|
||||
default?: 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 () {
|
||||
super();
|
||||
constructor (props: InputColorProps) {
|
||||
super(props);
|
||||
this.onChangeNoCheck = lodash.throttle(this.onChangeNoCheck, 1000/30);
|
||||
}
|
||||
|
||||
onChangeNoCheck (v) {
|
||||
onChangeNoCheck(v: string) {
|
||||
this.props.onChange(v);
|
||||
}
|
||||
|
||||
@@ -68,31 +69,31 @@ export default class InputColor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
onChange (v) {
|
||||
onChange (v: string) {
|
||||
this.props.onChange(v === "" ? undefined : v);
|
||||
}
|
||||
|
||||
render() {
|
||||
const offset = this.calcPickerOffset()
|
||||
var currentColor = this.color.object()
|
||||
currentColor = {
|
||||
const currentColor = this.color.object();
|
||||
const currentChromeColor = {
|
||||
r: currentColor.r,
|
||||
g: currentColor.g,
|
||||
b: currentColor.b,
|
||||
// Rename alpha -> a for ChromePicker
|
||||
a: currentColor.alpha
|
||||
a: currentColor.alpha!
|
||||
}
|
||||
|
||||
const picker = <div
|
||||
className="maputnik-color-picker-offset"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
zIndex: 1,
|
||||
position: 'fixed',
|
||||
zIndex: 1,
|
||||
left: offset.left,
|
||||
top: offset.top,
|
||||
}}>
|
||||
<ChromePicker
|
||||
color={currentColor}
|
||||
color={currentChromeColor}
|
||||
onChange={c => this.onChangeNoCheck(formatColor(c))}
|
||||
/>
|
||||
<div
|
||||
@@ -109,7 +110,7 @@ export default class InputColor extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
|
||||
var swatchStyle = {
|
||||
const swatchStyle = {
|
||||
backgroundColor: this.props.value
|
||||
};
|
||||
|
||||
@@ -1,30 +1,34 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import capitalize from 'lodash.capitalize'
|
||||
import {MdDelete} from 'react-icons/md'
|
||||
|
||||
import InputString from './InputString'
|
||||
import InputNumber from './InputNumber'
|
||||
import InputButton from './InputButton'
|
||||
import {MdDelete} from 'react-icons/md'
|
||||
import FieldDocLabel from './FieldDocLabel'
|
||||
import InputEnum from './InputEnum'
|
||||
import capitalize from 'lodash.capitalize'
|
||||
import InputUrl from './InputUrl'
|
||||
|
||||
|
||||
export default class FieldDynamicArray extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.array,
|
||||
type: PropTypes.string,
|
||||
default: PropTypes.array,
|
||||
onChange: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
fieldSpec: PropTypes.object,
|
||||
'aria-label': PropTypes.string,
|
||||
export type FieldDynamicArrayProps = {
|
||||
value?: (string | number | undefined)[]
|
||||
type?: 'url' | 'number' | 'enum' | 'string'
|
||||
default?: (string | number | undefined)[]
|
||||
onChange?(values: (string | number | undefined)[] | undefined): unknown
|
||||
style?: object
|
||||
fieldSpec?: {
|
||||
values?: any
|
||||
}
|
||||
'aria-label'?: string
|
||||
label: string
|
||||
};
|
||||
|
||||
changeValue(idx, newValue) {
|
||||
|
||||
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
|
||||
changeValue(idx: number, newValue: string | number | undefined) {
|
||||
const values = this.values.slice(0)
|
||||
values[idx] = newValue
|
||||
this.props.onChange(values)
|
||||
if (this.props.onChange) this.props.onChange(values)
|
||||
}
|
||||
|
||||
get values() {
|
||||
@@ -41,20 +45,20 @@ export default class FieldDynamicArray extends React.Component {
|
||||
}
|
||||
else if (this.props.type === 'enum') {
|
||||
const {fieldSpec} = this.props;
|
||||
const defaultValue = Object.keys(fieldSpec.values)[0];
|
||||
const defaultValue = Object.keys(fieldSpec!.values)[0];
|
||||
values.push(defaultValue);
|
||||
} else {
|
||||
values.push("")
|
||||
}
|
||||
|
||||
this.props.onChange(values)
|
||||
if (this.props.onChange) this.props.onChange(values)
|
||||
}
|
||||
|
||||
deleteValue(valueIdx) {
|
||||
deleteValue(valueIdx: number) {
|
||||
const values = this.values.slice(0)
|
||||
values.splice(valueIdx, 1)
|
||||
|
||||
this.props.onChange(values.length > 0 ? values : undefined);
|
||||
if (this.props.onChange) this.props.onChange(values.length > 0 ? values : undefined);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -63,30 +67,30 @@ export default class FieldDynamicArray extends React.Component {
|
||||
let input;
|
||||
if(this.props.type === 'url') {
|
||||
input = <InputUrl
|
||||
value={v}
|
||||
value={v as string}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
aria-label={this.props['aria-label'] || this.props.label}
|
||||
/>
|
||||
}
|
||||
else if (this.props.type === 'number') {
|
||||
input = <InputNumber
|
||||
value={v}
|
||||
value={v as number}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
aria-label={this.props['aria-label'] || this.props.label}
|
||||
/>
|
||||
}
|
||||
else if (this.props.type === 'enum') {
|
||||
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)]);
|
||||
const options = Object.keys(this.props.fieldSpec?.values).map(v => [v, capitalize(v)]);
|
||||
input = <InputEnum
|
||||
options={options}
|
||||
value={v}
|
||||
value={v as string}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
aria-label={this.props['aria-label'] || this.props.label}
|
||||
/>
|
||||
}
|
||||
else {
|
||||
input = <InputString
|
||||
value={v}
|
||||
value={v as string}
|
||||
onChange={this.changeValue.bind(this, i)}
|
||||
aria-label={this.props['aria-label'] || this.props.label}
|
||||
/>
|
||||
@@ -96,7 +100,7 @@ export default class FieldDynamicArray extends React.Component {
|
||||
style={this.props.style}
|
||||
key={i}
|
||||
className="maputnik-array-block"
|
||||
>
|
||||
>
|
||||
<div className="maputnik-array-block-action">
|
||||
{deleteValueBtn}
|
||||
</div>
|
||||
@@ -120,11 +124,11 @@ export default class FieldDynamicArray extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteValueInputButton extends React.Component {
|
||||
static propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
}
|
||||
type DeleteValueInputButtonProps = {
|
||||
onClick?(...args: unknown[]): unknown
|
||||
};
|
||||
|
||||
class DeleteValueInputButton extends React.Component<DeleteValueInputButtonProps> {
|
||||
render() {
|
||||
return <InputButton
|
||||
className="maputnik-delete-stop"
|
||||
@@ -133,7 +137,6 @@ class DeleteValueInputButton extends React.Component {
|
||||
>
|
||||
<FieldDocLabel
|
||||
label={<MdDelete />}
|
||||
doc={"Remove array item."}
|
||||
/>
|
||||
</InputButton>
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputSelect from './InputSelect'
|
||||
import InputMultiInput from './InputMultiInput'
|
||||
|
||||
|
||||
function optionsLabelLength(options) {
|
||||
function optionsLabelLength(options: any[]) {
|
||||
let sum = 0;
|
||||
options.forEach(([_, label]) => {
|
||||
sum += label.length
|
||||
@@ -13,18 +12,20 @@ function optionsLabelLength(options) {
|
||||
}
|
||||
|
||||
|
||||
export default class InputEnum extends React.Component {
|
||||
static propTypes = {
|
||||
"data-wd-key": PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
default: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
options: PropTypes.array,
|
||||
'aria-label': PropTypes.string,
|
||||
}
|
||||
export type InputEnumProps = {
|
||||
"data-wd-key"?: string
|
||||
value?: string
|
||||
style?: object
|
||||
default?: string
|
||||
name?: string
|
||||
onChange(...args: unknown[]): unknown
|
||||
options: any[]
|
||||
'aria-label'?: string
|
||||
label?: string
|
||||
};
|
||||
|
||||
|
||||
export default class InputEnum extends React.Component<InputEnumProps> {
|
||||
render() {
|
||||
const {options, value, onChange, name, label} = this.props;
|
||||
|
||||
@@ -32,14 +33,14 @@ export default class InputEnum extends React.Component {
|
||||
return <InputMultiInput
|
||||
name={name}
|
||||
options={options}
|
||||
value={value || this.props.default}
|
||||
value={(value || this.props.default)!}
|
||||
onChange={onChange}
|
||||
aria-label={this.props['aria-label'] || label}
|
||||
/>
|
||||
} else {
|
||||
return <InputSelect
|
||||
options={options}
|
||||
value={value || this.props.default}
|
||||
value={(value || this.props.default)!}
|
||||
onChange={onChange}
|
||||
aria-label={this.props['aria-label'] || label}
|
||||
/>
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import InputAutocomplete from './InputAutocomplete'
|
||||
|
||||
export default class FieldFont extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.array,
|
||||
default: PropTypes.array,
|
||||
fonts: PropTypes.array,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
'aria-label': PropTypes.string,
|
||||
}
|
||||
export type FieldFontProps = {
|
||||
name: string
|
||||
value?: string[]
|
||||
default?: string[]
|
||||
fonts?: unknown[]
|
||||
style?: object
|
||||
onChange(...args: unknown[]): unknown
|
||||
'aria-label'?: string
|
||||
};
|
||||
|
||||
export default class FieldFont extends React.Component<FieldFontProps> {
|
||||
static defaultProps = {
|
||||
fonts: []
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export default class FieldFont extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
changeFont(idx, newValue) {
|
||||
changeFont(idx: number, newValue: string) {
|
||||
const changedValues = this.values.slice(0)
|
||||
changedValues[idx] = newValue
|
||||
const filteredValues = changedValues
|
||||
@@ -46,7 +46,7 @@ export default class FieldFont extends React.Component {
|
||||
<InputAutocomplete
|
||||
aria-label={this.props['aria-label'] || this.props.name}
|
||||
value={value}
|
||||
options={this.props.fonts.map(f => [f, f])}
|
||||
options={this.props.fonts?.map(f => [f, f])}
|
||||
onChange={this.changeFont.bind(this, i)}
|
||||
/>
|
||||
</li>
|
||||
@@ -1,47 +1,45 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames';
|
||||
|
||||
import Block from './Block'
|
||||
import FieldString from './FieldString'
|
||||
import CodeMirror from 'codemirror';
|
||||
import CodeMirror, { ModeSpec } from 'codemirror';
|
||||
|
||||
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 jsonlint from 'jsonlint'
|
||||
import stringifyPretty from 'json-stringify-pretty-compact'
|
||||
import '../util/codemirror-mgl';
|
||||
|
||||
|
||||
export default class InputJson extends React.Component {
|
||||
static propTypes = {
|
||||
layer: PropTypes.any.isRequired,
|
||||
maxHeight: PropTypes.number,
|
||||
onChange: PropTypes.func,
|
||||
lineNumbers: PropTypes.bool,
|
||||
lineWrapping: PropTypes.bool,
|
||||
getValue: PropTypes.func,
|
||||
gutters: PropTypes.array,
|
||||
className: PropTypes.string,
|
||||
onFocus: PropTypes.func,
|
||||
onBlur: PropTypes.func,
|
||||
onJSONValid: PropTypes.func,
|
||||
onJSONInvalid: PropTypes.func,
|
||||
mode: PropTypes.object,
|
||||
lint: PropTypes.oneOfType([
|
||||
PropTypes.bool,
|
||||
PropTypes.object,
|
||||
]),
|
||||
}
|
||||
export type InputJsonProps = {
|
||||
layer: any
|
||||
maxHeight?: number
|
||||
onChange?(...args: unknown[]): unknown
|
||||
lineNumbers?: boolean
|
||||
lineWrapping?: boolean
|
||||
getValue?(data: any): string
|
||||
gutters?: string[]
|
||||
className?: string
|
||||
onFocus?(...args: unknown[]): unknown
|
||||
onBlur?(...args: unknown[]): unknown
|
||||
onJSONValid?(...args: unknown[]): unknown
|
||||
onJSONInvalid?(...args: unknown[]): unknown
|
||||
mode?: ModeSpec<any>
|
||||
lint?: boolean | object
|
||||
};
|
||||
|
||||
type InputJsonState = {
|
||||
isEditing: boolean
|
||||
showMessage: boolean
|
||||
prevValue: string
|
||||
};
|
||||
|
||||
export default class InputJson extends React.Component<InputJsonProps, InputJsonState> {
|
||||
static defaultProps = {
|
||||
lineNumbers: true,
|
||||
lineWrapping: false,
|
||||
gutters: ["CodeMirror-lint-markers"],
|
||||
getValue: (data) => {
|
||||
getValue: (data: any) => {
|
||||
return stringifyPretty(data, {indent: 2, maxLength: 40});
|
||||
},
|
||||
onFocus: () => {},
|
||||
@@ -49,20 +47,24 @@ export default class InputJson extends React.Component {
|
||||
onJSONInvalid: () => {},
|
||||
onJSONValid: () => {},
|
||||
}
|
||||
_keyEvent: string;
|
||||
_doc: CodeMirror.Editor | undefined;
|
||||
_el: HTMLDivElement | null = null;
|
||||
_cancelNextChange: boolean = false;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: InputJsonProps) {
|
||||
super(props);
|
||||
this._keyEvent = "keyboard";
|
||||
this.state = {
|
||||
isEditing: false,
|
||||
showMessage: false,
|
||||
prevValue: this.props.getValue(this.props.layer),
|
||||
prevValue: this.props.getValue!(this.props.layer),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this._doc = CodeMirror(this._el, {
|
||||
value: this.props.getValue(this.props.layer),
|
||||
this._doc = CodeMirror(this._el!, {
|
||||
value: this.props.getValue!(this.props.layer),
|
||||
mode: this.props.mode || {
|
||||
name: "mgl",
|
||||
},
|
||||
@@ -84,12 +86,12 @@ export default class InputJson extends React.Component {
|
||||
this._doc.on('blur', this.onBlur);
|
||||
}
|
||||
|
||||
onPointerDown = (cm, e) => {
|
||||
onPointerDown = () => {
|
||||
this._keyEvent = "pointer";
|
||||
}
|
||||
|
||||
onFocus = (cm, e) => {
|
||||
this.props.onFocus();
|
||||
onFocus = () => {
|
||||
if (this.props.onFocus) this.props.onFocus();
|
||||
this.setState({
|
||||
isEditing: true,
|
||||
showMessage: (this._keyEvent === "keyboard"),
|
||||
@@ -98,7 +100,7 @@ export default class InputJson extends React.Component {
|
||||
|
||||
onBlur = () => {
|
||||
this._keyEvent = "keyboard";
|
||||
this.props.onBlur();
|
||||
if (this.props.onBlur) this.props.onBlur();
|
||||
this.setState({
|
||||
isEditing: false,
|
||||
showMessage: false,
|
||||
@@ -106,29 +108,29 @@ export default class InputJson extends React.Component {
|
||||
}
|
||||
|
||||
componentWillUnMount () {
|
||||
this._doc.off('change', this.onChange);
|
||||
this._doc.off('focus', this.onFocus);
|
||||
this._doc.off('blur', this.onBlur);
|
||||
this._doc!.off('change', this.onChange);
|
||||
this._doc!.off('focus', this.onFocus);
|
||||
this._doc!.off('blur', this.onBlur);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidUpdate(prevProps: InputJsonProps) {
|
||||
if (!this.state.isEditing && prevProps.layer !== this.props.layer) {
|
||||
this._cancelNextChange = true;
|
||||
this._doc.setValue(
|
||||
this.props.getValue(this.props.layer),
|
||||
this._doc!.setValue(
|
||||
this.props.getValue!(this.props.layer),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onChange = (e) => {
|
||||
onChange = (_e: unknown) => {
|
||||
if (this._cancelNextChange) {
|
||||
this._cancelNextChange = false;
|
||||
this.setState({
|
||||
prevValue: this._doc.getValue(),
|
||||
prevValue: this._doc!.getValue(),
|
||||
})
|
||||
return;
|
||||
}
|
||||
const newCode = this._doc.getValue();
|
||||
const newCode = this._doc!.getValue();
|
||||
|
||||
if (this.state.prevValue !== newCode) {
|
||||
let parsedLayer, err;
|
||||
@@ -139,12 +141,12 @@ export default class InputJson extends React.Component {
|
||||
console.warn(_err)
|
||||
}
|
||||
|
||||
if (err) {
|
||||
if (err && this.props.onJSONInvalid) {
|
||||
this.props.onJSONInvalid();
|
||||
}
|
||||
else {
|
||||
this.props.onChange(parsedLayer)
|
||||
this.props.onJSONValid();
|
||||
if (this.props.onChange) this.props.onChange(parsedLayer)
|
||||
if (this.props.onJSONValid) this.props.onJSONValid();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +157,7 @@ export default class InputJson extends React.Component {
|
||||
|
||||
render() {
|
||||
const {showMessage} = this.state;
|
||||
const style = {};
|
||||
const style = {} as {maxHeight?: number};
|
||||
if (this.props.maxHeight) {
|
||||
style.maxHeight = this.props.maxHeight;
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import InputButton from './InputButton'
|
||||
|
||||
export default class InputMultiInput extends React.Component {
|
||||
static propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
options: PropTypes.array.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
export type InputMultiInputProps = {
|
||||
name?: string
|
||||
value: string
|
||||
options: any[]
|
||||
onChange(...args: unknown[]): unknown
|
||||
'aria-label'?: string
|
||||
};
|
||||
|
||||
export default class InputMultiInput extends React.Component<InputMultiInputProps> {
|
||||
render() {
|
||||
let options = this.props.options
|
||||
if(options.length > 0 && !Array.isArray(options[0])) {
|
||||
@@ -25,7 +24,7 @@ export default class InputMultiInput extends React.Component {
|
||||
>
|
||||
<input type="radio"
|
||||
name={this.props.name}
|
||||
onChange={e => this.props.onChange(val)}
|
||||
onChange={_e => this.props.onChange(val)}
|
||||
value={val}
|
||||
checked={val === selectedValue}
|
||||
/>
|
||||
@@ -1,37 +1,44 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, { BaseSyntheticEvent } from 'react'
|
||||
import generateUniqueId from '../libs/document-uid';
|
||||
|
||||
let IDX = 0;
|
||||
export type InputNumberProps = {
|
||||
value?: number
|
||||
default?: number
|
||||
min?: number
|
||||
max?: number
|
||||
onChange?(value: number | undefined): unknown
|
||||
allowRange?: boolean
|
||||
rangeStep?: number
|
||||
wdKey?: string
|
||||
required?: boolean
|
||||
"aria-label"?: string
|
||||
};
|
||||
|
||||
export default class InputNumber extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.number,
|
||||
default: PropTypes.number,
|
||||
min: PropTypes.number,
|
||||
max: PropTypes.number,
|
||||
onChange: PropTypes.func,
|
||||
allowRange: PropTypes.bool,
|
||||
rangeStep: PropTypes.number,
|
||||
wdKey: PropTypes.string,
|
||||
required: PropTypes.bool,
|
||||
"aria-label": PropTypes.string,
|
||||
}
|
||||
type InputNumberState = {
|
||||
uuid: number
|
||||
editing: boolean
|
||||
editingRange?: boolean
|
||||
value?: number
|
||||
dirtyValue?: number
|
||||
}
|
||||
|
||||
export default class InputNumber extends React.Component<InputNumberProps, InputNumberState> {
|
||||
static defaultProps = {
|
||||
rangeStep: 1
|
||||
}
|
||||
_keyboardEvent: boolean = false;
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: InputNumberProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
uuid: IDX++,
|
||||
uuid: +generateUniqueId(),
|
||||
editing: false,
|
||||
value: props.value,
|
||||
dirtyValue: props.value,
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
static getDerivedStateFromProps(props: InputNumberProps, state: InputNumberState) {
|
||||
if (!state.editing && props.value !== state.value) {
|
||||
return {
|
||||
value: props.value,
|
||||
@@ -41,16 +48,15 @@ export default class InputNumber extends React.Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
changeValue(newValue) {
|
||||
changeValue(newValue: number | string | undefined) {
|
||||
const value = (newValue === "" || newValue === undefined) ?
|
||||
undefined :
|
||||
parseFloat(newValue);
|
||||
undefined : +newValue;
|
||||
|
||||
const hasChanged = this.props.value !== value;
|
||||
if(this.isValid(value) && hasChanged) {
|
||||
this.props.onChange(value)
|
||||
if (this.props.onChange) this.props.onChange(value)
|
||||
this.setState({
|
||||
value: newValue,
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
else if (!this.isValid(value) && hasChanged) {
|
||||
@@ -60,25 +66,25 @@ export default class InputNumber extends React.Component {
|
||||
}
|
||||
|
||||
this.setState({
|
||||
dirtyValue: newValue === "" ? undefined : newValue,
|
||||
dirtyValue: newValue === "" ? undefined : value,
|
||||
})
|
||||
}
|
||||
|
||||
isValid(v) {
|
||||
isValid(v: number | string | undefined) {
|
||||
if (v === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const value = parseFloat(v)
|
||||
const value = +v;
|
||||
if(isNaN(value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if(!isNaN(this.props.min) && value < this.props.min) {
|
||||
if(!isNaN(this.props.min!) && value < this.props.min!) {
|
||||
return false
|
||||
}
|
||||
|
||||
if(!isNaN(this.props.max) && value > this.props.max) {
|
||||
if(!isNaN(this.props.max!) && value > this.props.max!) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -88,7 +94,7 @@ export default class InputNumber extends React.Component {
|
||||
resetValue = () => {
|
||||
this.setState({editing: false});
|
||||
// Reset explicitly to default value if value has been cleared
|
||||
if(this.state.value === "") {
|
||||
if(!this.state.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -104,8 +110,8 @@ export default class InputNumber extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
onChangeRange = (e) => {
|
||||
let value = parseFloat(e.target.value, 10);
|
||||
onChangeRange = (e: BaseSyntheticEvent<Event, HTMLInputElement, HTMLInputElement>) => {
|
||||
let value = parseFloat(e.target.value);
|
||||
const step = this.props.rangeStep;
|
||||
let dirtyValue = value;
|
||||
|
||||
@@ -119,11 +125,11 @@ export default class InputNumber extends React.Component {
|
||||
// for example we might go from 13 to 13.23, however because we know
|
||||
// that came from a keyboard event we always want to increase by a
|
||||
// single step value.
|
||||
if (value < this.state.dirtyValue) {
|
||||
value = this.state.value - step;
|
||||
if (value < this.state.dirtyValue!) {
|
||||
value = this.state.value! - step;
|
||||
}
|
||||
else {
|
||||
value = this.state.value + step
|
||||
value = this.state.value! + step
|
||||
}
|
||||
dirtyValue = value;
|
||||
}
|
||||
@@ -133,22 +139,23 @@ export default class InputNumber extends React.Component {
|
||||
}
|
||||
else {
|
||||
value = value + (step - snap);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._keyboardEvent = false;
|
||||
|
||||
// Clamp between min/max
|
||||
value = Math.max(this.props.min, Math.min(this.props.max, value));
|
||||
value = Math.max(this.props.min!, Math.min(this.props.max!, value));
|
||||
|
||||
this.setState({value, dirtyValue});
|
||||
this.props.onChange(value);
|
||||
if (this.props.onChange) this.props.onChange(value);
|
||||
}
|
||||
|
||||
render() {
|
||||
if(
|
||||
this.props.hasOwnProperty("min") && this.props.hasOwnProperty("max") &&
|
||||
Object.prototype.hasOwnProperty.call(this.props, "min") &&
|
||||
Object.prototype.hasOwnProperty.call(this.props, "max") &&
|
||||
this.props.min !== undefined && this.props.max !== undefined &&
|
||||
this.props.allowRange
|
||||
) {
|
||||
@@ -196,15 +203,15 @@ export default class InputNumber extends React.Component {
|
||||
type="text"
|
||||
spellCheck="false"
|
||||
className="maputnik-number"
|
||||
placeholder={this.props.default}
|
||||
placeholder={this.props.default?.toString()}
|
||||
value={inputValue === undefined ? "" : inputValue}
|
||||
onFocus={e => {
|
||||
onFocus={_e => {
|
||||
this.setState({editing: true});
|
||||
}}
|
||||
onChange={e => {
|
||||
this.changeValue(e.target.value);
|
||||
}}
|
||||
onBlur={e => {
|
||||
onBlur={_e => {
|
||||
this.setState({editing: false});
|
||||
this.resetValue()
|
||||
}}
|
||||
@@ -218,7 +225,7 @@ export default class InputNumber extends React.Component {
|
||||
aria-label={this.props['aria-label']}
|
||||
spellCheck="false"
|
||||
className="maputnik-number"
|
||||
placeholder={this.props.default}
|
||||
placeholder={this.props.default?.toString()}
|
||||
value={value === undefined ? "" : value}
|
||||
onChange={e => this.changeValue(e.target.value)}
|
||||
onFocus={() => {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user