Merge branch 'main' into nginx

This commit is contained in:
Harel M
2023-12-26 23:21:47 +02:00
committed by GitHub
260 changed files with 19531 additions and 23823 deletions
-21
View File
@@ -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/**"]
}]
]
}
}
}
+45
View File
@@ -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"
}
}
+1 -1
View File
@@ -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) -->
+14
View File
@@ -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
View File
@@ -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
+7 -9
View File
@@ -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
+3
View File
@@ -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/
+1
View File
@@ -0,0 +1 @@
18.19
+18
View File
@@ -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
View File
@@ -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
View File
@@ -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/
+15 -14
View File
@@ -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.
-47
View File
@@ -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();
}
}
-74
View File
@@ -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'
}
]
})
]
};
-68
View File
@@ -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',
})
]
};
-20
View File
@@ -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',
}
}
};
-44
View File
@@ -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'
]
}
];
+23
View File
@@ -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,
},
},
});
+40
View File
@@ -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();
});
});
});
+210
View File
@@ -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);
}
};
}
+97
View File
@@ -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",
},
]
);
});
});
+61
View File
@@ -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");
});
});
});
+491
View File
@@ -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");
});
});
});
+23
View File
@@ -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));
});
});
});
+161
View File
@@ -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": []
}
+29
View File
@@ -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": []
}
+18
View File
@@ -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": []
}
+37
View File
@@ -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>
// }
// }
// }
+22
View File
@@ -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')
+5 -5
View File
@@ -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&hellip;</div>
</div>
<script type="module" src="/src/index.jsx"></script>
</body>
</html>
+14862 -18649
View File
File diff suppressed because it is too large Load Diff
+62 -87
View File
@@ -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"
}
}
-5
View File
@@ -1,5 +0,0 @@
{
"container": {
"startScript": "start-sandbox"
}
}
+183 -137
View File
@@ -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 {
&nbsp;&mdash;&nbsp;
<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 {
</>
);
}
}
}
-21
View File
@@ -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>
}
}
+19
View File
@@ -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>
}
}
-20
View File
@@ -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>
}
}
+18
View File
@@ -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>
}
}
-20
View File
@@ -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>
}
}
+18
View File
@@ -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>
}
}
-20
View File
@@ -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>
}
}
+21
View File
@@ -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>
-21
View File
@@ -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>
}
}
+16
View File
@@ -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>
}
}
-20
View File
@@ -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>
}
}
+20
View File
@@ -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}
-16
View File
@@ -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} />
}
}
+13
View File
@@ -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}
-21
View File
@@ -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>
}
}
+18
View File
@@ -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>
}
}
-19
View File
@@ -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>
}
}
+20
View File
@@ -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>
}
}
-20
View File
@@ -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>
}
}
+22
View File
@@ -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>
}
-34
View File
@@ -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>
}
}
+37
View File
@@ -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>
}
}
-20
View File
@@ -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>
}
}
+19
View File
@@ -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,
}
-22
View File
@@ -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>
);
}
}
+23
View File
@@ -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">
-33
View File
@@ -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} />
}
}
}
+33
View File
@@ -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