mirror of
https://github.com/maputnik/editor.git
synced 2026-01-03 20:10:02 +00:00
Compare commits
254 Commits
v1.6.0
...
v1.7.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d51ea9b25 | ||
|
|
fcfc7ab874 | ||
|
|
a0bc4744a2 | ||
|
|
e6e4c928f3 | ||
|
|
00388e03b8 | ||
|
|
f70d078ec6 | ||
|
|
1d8131fb85 | ||
|
|
8c82db9162 | ||
|
|
f23f60807a | ||
|
|
8f581956e8 | ||
|
|
87fb0f6a5c | ||
|
|
1c953bc296 | ||
|
|
ce976991d4 | ||
|
|
be7642976b | ||
|
|
a5b226d9f3 | ||
|
|
1b3d8b5b79 | ||
|
|
97a61afc24 | ||
|
|
d1f6bc95db | ||
|
|
10b03c4e00 | ||
|
|
449d8e7665 | ||
|
|
4b8800e8ac | ||
|
|
874c6460f6 | ||
|
|
55cb86f721 | ||
|
|
a30017fd2c | ||
|
|
3b5ba6c59e | ||
|
|
a693f6db4e | ||
|
|
5be7e0c7ec | ||
|
|
7c6b3c0d80 | ||
|
|
e5e03be382 | ||
|
|
0d35106cc8 | ||
|
|
5710edcff7 | ||
|
|
2cc7c63bb1 | ||
|
|
ba9d21c045 | ||
|
|
4ef6ecb7eb | ||
|
|
52e8b21b3d | ||
|
|
c6ba4f66e2 | ||
|
|
5a47a96f09 | ||
|
|
ae878f6000 | ||
|
|
aebfe62a8e | ||
|
|
6be3543616 | ||
|
|
0f6708d9d4 | ||
|
|
0705522a24 | ||
|
|
35098111ac | ||
|
|
39333953d7 | ||
|
|
adea3d0f13 | ||
|
|
d1cb2690fc | ||
|
|
3ffdcc9639 | ||
|
|
793b5d15ad | ||
|
|
cff32696cc | ||
|
|
029eff9317 | ||
|
|
b7d08dfaa6 | ||
|
|
94089836bf | ||
|
|
ff8a8fb749 | ||
|
|
1300951a29 | ||
|
|
3cb1ed9403 | ||
|
|
a5ac1cc93d | ||
|
|
29a0ef0d1c | ||
|
|
26907f7014 | ||
|
|
3ac06c7cb1 | ||
|
|
f268f09ca2 | ||
|
|
f4c18fd91b | ||
|
|
0567b098ec | ||
|
|
dc6006fd6d | ||
|
|
109261ba00 | ||
|
|
b539644b2b | ||
|
|
be36eec93d | ||
|
|
fe5066a2a4 | ||
|
|
642e5c0b29 | ||
|
|
97bdc93a39 | ||
|
|
c770b440c2 | ||
|
|
7559985a2e | ||
|
|
532bbecb47 | ||
|
|
8ed67e98ce | ||
|
|
5792c632f9 | ||
|
|
3e2927e6a4 | ||
|
|
f09cc25a3b | ||
|
|
c5c3e93aff | ||
|
|
cc371d6a70 | ||
|
|
1b17e8fa0a | ||
|
|
bc4706de83 | ||
|
|
0f22eb83d3 | ||
|
|
a8cbe19f09 | ||
|
|
c714e23d79 | ||
|
|
5b3d579f87 | ||
|
|
725b752e35 | ||
|
|
223721a65d | ||
|
|
9b4d924dff | ||
|
|
b31537e063 | ||
|
|
63ed8c1de3 | ||
|
|
7aa0298f7c | ||
|
|
62f3cbe8fb | ||
|
|
30facc885f | ||
|
|
17aa88e3b6 | ||
|
|
5b9af07ebc | ||
|
|
0009c74948 | ||
|
|
8911f83ef3 | ||
|
|
2fafafe0dc | ||
|
|
27e6675d26 | ||
|
|
4269e4573c | ||
|
|
096e2b6aec | ||
|
|
33e04b3527 | ||
|
|
79fa2b3508 | ||
|
|
d5ef412300 | ||
|
|
0726a494be | ||
|
|
926969b921 | ||
|
|
59e070f463 | ||
|
|
2ccd1d227e | ||
|
|
655877f67e | ||
|
|
6c240d53e4 | ||
|
|
f89f8ed4ea | ||
|
|
6123b464de | ||
|
|
49dba02e8f | ||
|
|
fb49a3abe5 | ||
|
|
c88f9ab5dc | ||
|
|
d886b14d09 | ||
|
|
bd1204a7a5 | ||
|
|
9cadda0236 | ||
|
|
90ea6323c1 | ||
|
|
51f2cfac16 | ||
|
|
4dbb423ac2 | ||
|
|
a3ee1cc27e | ||
|
|
fea0798349 | ||
|
|
bd8abffa28 | ||
|
|
a5f3a43cde | ||
|
|
6c5dc7e06b | ||
|
|
b1c8a12e88 | ||
|
|
401c6971f4 | ||
|
|
7e5a5ce077 | ||
|
|
6b245c9894 | ||
|
|
b963fe9619 | ||
|
|
673887d93b | ||
|
|
06898429fd | ||
|
|
0196ba4eb4 | ||
|
|
ef81534a17 | ||
|
|
a958ec943b | ||
|
|
4e3b395b3d | ||
|
|
5e7fd4f93c | ||
|
|
25cad5bb25 | ||
|
|
f9c230414e | ||
|
|
866f8d034a | ||
|
|
be6aa559fb | ||
|
|
a560176d83 | ||
|
|
4644e78fd2 | ||
|
|
237cc16b97 | ||
|
|
dffa54afb0 | ||
|
|
225e5c48e4 | ||
|
|
2e017d252a | ||
|
|
e728e5f7e4 | ||
|
|
f0371b41b1 | ||
|
|
a51442921a | ||
|
|
f39fb34f36 | ||
|
|
566201fb45 | ||
|
|
88841b56e7 | ||
|
|
5aa0b4e7d9 | ||
|
|
f19fc4a8a1 | ||
|
|
cd162309a8 | ||
|
|
aead867e27 | ||
|
|
663f295623 | ||
|
|
c588164190 | ||
|
|
d61d0a5795 | ||
|
|
dddd604f7b | ||
|
|
ea3b9a20c5 | ||
|
|
7415b8af08 | ||
|
|
d06e053d34 | ||
|
|
7075a8b05e | ||
|
|
4cbcf14588 | ||
|
|
ca202d7701 | ||
|
|
8dfc16e7ee | ||
|
|
fbf5cec670 | ||
|
|
14d4383f8a | ||
|
|
58bdd39f9e | ||
|
|
ab9ab7acc7 | ||
|
|
be39fd2ec8 | ||
|
|
3c0185da27 | ||
|
|
b37b7276fb | ||
|
|
c45cf2f0c8 | ||
|
|
1f03fdbb50 | ||
|
|
f3b8c5362a | ||
|
|
c9a5dd01be | ||
|
|
0fa4d40e92 | ||
|
|
8a6e64c8c2 | ||
|
|
72b6dd1ae9 | ||
|
|
ee525631fa | ||
|
|
ee9e055af3 | ||
|
|
b214c6ac7e | ||
|
|
eb75020861 | ||
|
|
a44e757e31 | ||
|
|
9ac908948d | ||
|
|
19e82e5890 | ||
|
|
bf84fd24ee | ||
|
|
affeb7c751 | ||
|
|
9743361e0d | ||
|
|
ab16120af2 | ||
|
|
37e5ba0fff | ||
|
|
0aa0dad7fb | ||
|
|
2910efde6e | ||
|
|
eac7656786 | ||
|
|
be3175beae | ||
|
|
26de95a263 | ||
|
|
d0a47bd122 | ||
|
|
8c760bb810 | ||
|
|
c27deefdef | ||
|
|
392a845460 | ||
|
|
e7622c2080 | ||
|
|
3a558412ba | ||
|
|
95e205943a | ||
|
|
eb8686325c | ||
|
|
1f77e156e6 | ||
|
|
92ee50a4a4 | ||
|
|
ef23f01e67 | ||
|
|
22b6a4a2bf | ||
|
|
201ecac156 | ||
|
|
563a78ed42 | ||
|
|
47acc2640b | ||
|
|
f088788246 | ||
|
|
e219dcd332 | ||
|
|
b8829d9a5c | ||
|
|
2c83c976c6 | ||
|
|
d63782ddf2 | ||
|
|
3eabcbec72 | ||
|
|
00ab303e44 | ||
|
|
38bf12701e | ||
|
|
e4ec1d155a | ||
|
|
361f083687 | ||
|
|
c1a59200e2 | ||
|
|
6e0432ff5e | ||
|
|
1c83de08c1 | ||
|
|
0af828543b | ||
|
|
369cc23a30 | ||
|
|
db56ad8b2e | ||
|
|
7fa17d81ac | ||
|
|
019c6a0086 | ||
|
|
c1bee74b57 | ||
|
|
b794279304 | ||
|
|
935dfa1704 | ||
|
|
bda7a0e659 | ||
|
|
8d1cc340b8 | ||
|
|
338c6b59a8 | ||
|
|
021f8ab400 | ||
|
|
f305db9e3e | ||
|
|
e916b25594 | ||
|
|
1fec89b69e | ||
|
|
911549aca3 | ||
|
|
41329ec2f8 | ||
|
|
15cdfbc980 | ||
|
|
5053058c32 | ||
|
|
8a8cfad303 | ||
|
|
b1d097a40f | ||
|
|
3a0fc6eeac | ||
|
|
b456b59c44 | ||
|
|
e18d304313 | ||
|
|
fe0df2a4ef | ||
|
|
a51fdb8435 | ||
|
|
cdd5d27908 |
1
.babelrc
1
.babelrc
@@ -4,6 +4,7 @@
|
|||||||
"@babel/preset-react"
|
"@babel/preset-react"
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
"static-fs",
|
||||||
"react-hot-loader/babel",
|
"react-hot-loader/babel",
|
||||||
"@babel/plugin-proposal-class-properties"
|
"@babel/plugin-proposal-class-properties"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ templates:
|
|||||||
|
|
||||||
- run: mkdir -p /tmp/artifacts/logs
|
- run: mkdir -p /tmp/artifacts/logs
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm run lint
|
- run: npm run lint-js
|
||||||
- run: npm run lint-styles
|
- run: npm run lint-css
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: /tmp/artifacts
|
path: /tmp/artifacts
|
||||||
destination: /artifacts
|
destination: /artifacts
|
||||||
@@ -41,19 +41,15 @@ templates:
|
|||||||
|
|
||||||
- run: mkdir -p /tmp/artifacts/logs
|
- run: mkdir -p /tmp/artifacts/logs
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npm run lint
|
- run: npm run profiling-build
|
||||||
- run: npm run lint-styles
|
- run: npm run lint-js
|
||||||
|
- run: npm run lint-css
|
||||||
- run: DOCKER_HOST=localhost npm test
|
- run: DOCKER_HOST=localhost npm test
|
||||||
- run: ./node_modules/.bin/istanbul report --include /tmp/artifacts/coverage/coverage.json --dir /tmp/artifacts/coverage html lcov
|
- run: ./node_modules/.bin/istanbul report --include /tmp/artifacts/coverage/coverage.json --dir /tmp/artifacts/coverage html lcov
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: /tmp/artifacts
|
path: /tmp/artifacts
|
||||||
destination: /artifacts
|
destination: /artifacts
|
||||||
jobs:
|
jobs:
|
||||||
build-linux-node-v8:
|
|
||||||
docker:
|
|
||||||
- image: node:8
|
|
||||||
working_directory: ~/repo-linux-node-v8
|
|
||||||
steps: *build-steps
|
|
||||||
build-linux-node-v10:
|
build-linux-node-v10:
|
||||||
docker:
|
docker:
|
||||||
- image: node:10
|
- image: node:10
|
||||||
@@ -65,13 +61,10 @@ jobs:
|
|||||||
- image: node:12
|
- image: node:12
|
||||||
working_directory: ~/repo-linux-node-v12
|
working_directory: ~/repo-linux-node-v12
|
||||||
steps: *build-steps
|
steps: *build-steps
|
||||||
build-osx-node-v8:
|
build-linux-node-v13:
|
||||||
macos:
|
docker:
|
||||||
xcode: "9.0"
|
- image: node:13
|
||||||
dependencies:
|
working_directory: ~/repo-linux-node-v13
|
||||||
override:
|
|
||||||
- brew install node@8
|
|
||||||
working_directory: ~/repo-osx-node-v8
|
|
||||||
steps: *build-steps
|
steps: *build-steps
|
||||||
build-osx-node-v10:
|
build-osx-node-v10:
|
||||||
macos:
|
macos:
|
||||||
@@ -89,14 +82,22 @@ jobs:
|
|||||||
- brew install node@12
|
- brew install node@12
|
||||||
working_directory: ~/repo-osx-node-v12
|
working_directory: ~/repo-osx-node-v12
|
||||||
steps: *build-steps
|
steps: *build-steps
|
||||||
|
build-osx-node-v13:
|
||||||
|
macos:
|
||||||
|
xcode: "9.0"
|
||||||
|
dependencies:
|
||||||
|
override:
|
||||||
|
- brew install node@13
|
||||||
|
working_directory: ~/repo-osx-node-v13
|
||||||
|
steps: *build-steps
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
version: 2
|
||||||
build:
|
build:
|
||||||
jobs:
|
jobs:
|
||||||
- build-linux-node-v8
|
|
||||||
- build-linux-node-v10
|
- build-linux-node-v10
|
||||||
- build-linux-node-v12
|
- build-linux-node-v12
|
||||||
- build-osx-node-v8
|
- build-linux-node-v13
|
||||||
- build-osx-node-v10
|
- build-osx-node-v10
|
||||||
- build-osx-node-v12
|
- build-osx-node-v12
|
||||||
|
- build-osx-node-v13
|
||||||
|
|||||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
custom: "https://maputnik.github.io/donate"
|
||||||
@@ -18,7 +18,6 @@ A free and open visual editor for the [Mapbox GL styles](https://www.mapbox.com/
|
|||||||
targeted at developers and map designers.
|
targeted at developers and map designers.
|
||||||
|
|
||||||
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
|
- :link: Design your maps online at **<https://maputnik.github.io/editor/>** (all in local storage)
|
||||||
- :link: Try out the v1.6.0-beta release at: https://maputnik.github.io/releases/v1.6.0-beta/
|
|
||||||
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
|
- :link: Use the [Maputnik CLI](https://github.com/maputnik/editor/wiki/Maputnik-CLI) for local style development
|
||||||
|
|
||||||
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independence is an OSS map designer.
|
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independence is an OSS map designer.
|
||||||
|
|||||||
10
appveyor.yml
10
appveyor.yml
@@ -1,9 +1,9 @@
|
|||||||
image: Visual Studio 2015
|
image: Visual Studio 2019
|
||||||
environment:
|
environment:
|
||||||
matrix:
|
matrix:
|
||||||
- nodejs_version: "8"
|
|
||||||
- nodejs_version: "10"
|
- nodejs_version: "10"
|
||||||
- nodejs_version: "12"
|
- nodejs_version: "12"
|
||||||
|
- nodejs_version: "13"
|
||||||
platform:
|
platform:
|
||||||
- x86
|
- x86
|
||||||
- x64
|
- x64
|
||||||
@@ -17,10 +17,10 @@ install:
|
|||||||
Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) $env:platform
|
Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) $env:platform
|
||||||
}
|
}
|
||||||
- md public
|
- md public
|
||||||
- npm --vs2015 install --global windows-build-tools
|
- npm install --global windows-build-tools
|
||||||
- npm install
|
- npm install
|
||||||
build_script:
|
build_script:
|
||||||
- npm run build
|
- npm run build
|
||||||
test_script:
|
test_script:
|
||||||
- npm run lint
|
- npm run lint-js
|
||||||
- npm run lint-styles
|
- npm run lint-css
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ exports.config = {
|
|||||||
//
|
//
|
||||||
screenshotPath: SCREENSHOT_PATH,
|
screenshotPath: SCREENSHOT_PATH,
|
||||||
// Note: This is here because @orangemug currently runs Maputnik inside a docker container.
|
// Note: This is here because @orangemug currently runs Maputnik inside a docker container.
|
||||||
host: process.env.DOCKER_HOST || "0.0.0.0",
|
hostname: process.env.DOCKER_HOST || "0.0.0.0",
|
||||||
// Set a base URL in order to shorten url command calls. If your `url` parameter starts
|
// Set a base URL in order to shorten url command calls. If your `url` parameter starts
|
||||||
// with `/`, the base url gets prepended, not including the path portion of your baseUrl.
|
// with `/`, the base url gets prepended, not including the path portion of your baseUrl.
|
||||||
// If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url
|
// If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url
|
||||||
@@ -118,7 +118,6 @@ exports.config = {
|
|||||||
// Services take over a specific job you don't want to take care of. They enhance
|
// Services take over a specific job you don't want to take care of. They enhance
|
||||||
// your test setup with almost no effort. Unlike plugins, they don't add new
|
// your test setup with almost no effort. Unlike plugins, they don't add new
|
||||||
// commands. Instead, they hook themselves up into the test process.
|
// commands. Instead, they hook themselves up into the test process.
|
||||||
services: ['selenium-standalone'],
|
|
||||||
//
|
//
|
||||||
// Framework you want to run your specs with.
|
// Framework you want to run your specs with.
|
||||||
// The following are supported: Mocha, Jasmine, and Cucumber
|
// The following are supported: Mocha, Jasmine, and Cucumber
|
||||||
@@ -147,12 +146,16 @@ exports.config = {
|
|||||||
onPrepare: function (config, capabilities) {
|
onPrepare: function (config, capabilities) {
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
var compiler = webpack(webpackConfig);
|
var compiler = webpack(webpackConfig);
|
||||||
|
const serverHost = isDocker() ? "0.0.0.0" : "localhost";
|
||||||
|
|
||||||
server = new WebpackDevServer(compiler, {
|
server = new WebpackDevServer(compiler, {
|
||||||
|
host: serverHost,
|
||||||
stats: {
|
stats: {
|
||||||
colors: true
|
colors: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
server.listen(testConfig.port, (isDocker() ? "0.0.0.0" : "localhost"), function(err) {
|
|
||||||
|
server.listen(testConfig.port, serverHost, function(err) {
|
||||||
if(err) {
|
if(err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ var webpack = require('webpack');
|
|||||||
var path = require('path');
|
var path = require('path');
|
||||||
var rules = require('./webpack.rules');
|
var rules = require('./webpack.rules');
|
||||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
var HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin');
|
||||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
|
|
||||||
const HOST = process.env.HOST || "127.0.0.1";
|
const HOST = process.env.HOST || "127.0.0.1";
|
||||||
@@ -61,6 +62,9 @@ module.exports = {
|
|||||||
title: 'Maputnik',
|
title: 'Maputnik',
|
||||||
template: './src/template.html'
|
template: './src/template.html'
|
||||||
}),
|
}),
|
||||||
|
new HtmlWebpackInlineSVGPlugin({
|
||||||
|
runPreEmit: true,
|
||||||
|
}),
|
||||||
new CopyWebpackPlugin([
|
new CopyWebpackPlugin([
|
||||||
{
|
{
|
||||||
from: './src/manifest.json',
|
from: './src/manifest.json',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ var webpack = require('webpack');
|
|||||||
var path = require('path');
|
var path = require('path');
|
||||||
var rules = require('./webpack.rules');
|
var rules = require('./webpack.rules');
|
||||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
var HtmlWebpackInlineSVGPlugin = require('html-webpack-inline-svg-plugin');
|
||||||
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
|
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
|
||||||
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
@@ -44,6 +45,9 @@ module.exports = {
|
|||||||
template: './src/template.html',
|
template: './src/template.html',
|
||||||
title: 'Maputnik'
|
title: 'Maputnik'
|
||||||
}),
|
}),
|
||||||
|
new HtmlWebpackInlineSVGPlugin({
|
||||||
|
runPreEmit: true,
|
||||||
|
}),
|
||||||
new CopyWebpackPlugin([
|
new CopyWebpackPlugin([
|
||||||
{
|
{
|
||||||
from: './src/manifest.json',
|
from: './src/manifest.json',
|
||||||
|
|||||||
20
config/webpack.profiling.config.js
Normal file
20
config/webpack.profiling.config.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -17,9 +17,15 @@ module.exports = [
|
|||||||
use: 'file-loader?name=[name].[ext]'
|
use: 'file-loader?name=[name].[ext]'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(svg|gif|jpg|png)$/,
|
test: /\.(gif|jpg|png)$/,
|
||||||
use: 'file-loader?name=img/[name].[ext]'
|
use: 'file-loader?name=img/[name].[ext]'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.svg$/,
|
||||||
|
use: [
|
||||||
|
'svg-inline-loader'
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /[\/\\](node_modules|global|src)[\/\\].*\.scss$/,
|
test: /[\/\\](node_modules|global|src)[\/\\].*\.scss$/,
|
||||||
use: [
|
use: [
|
||||||
|
|||||||
18821
package-lock.json
generated
18821
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
138
package.json
138
package.json
@@ -1,16 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "maputnik",
|
"name": "maputnik",
|
||||||
"version": "1.6.0",
|
"version": "1.7.0-beta",
|
||||||
"description": "A MapboxGL visual style editor",
|
"description": "A MapboxGL visual style editor",
|
||||||
"main": "''",
|
"main": "''",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"stats": "webpack --config config/webpack.production.config.js --profile --json > stats.json",
|
"stats": "webpack --config config/webpack.production.config.js --profile --json > stats.json",
|
||||||
"build": "webpack --config config/webpack.production.config.js --progress --profile --colors",
|
"build": "webpack --config config/webpack.production.config.js --progress --profile --colors",
|
||||||
|
"profiling-build": "webpack --config config/webpack.profiling.config.js --progress --profile --colors",
|
||||||
"test": "cross-env NODE_ENV=test wdio config/wdio.conf.js",
|
"test": "cross-env NODE_ENV=test wdio config/wdio.conf.js",
|
||||||
"test-watch": "cross-env NODE_ENV=test wdio config/wdio.conf.js --watch",
|
"test-watch": "cross-env NODE_ENV=test wdio config/wdio.conf.js --watch",
|
||||||
"start": "webpack-dev-server --progress --profile --colors --config config/webpack.config.js",
|
"start": "webpack-dev-server --progress --profile --colors --config config/webpack.config.js",
|
||||||
"lint": "eslint --ext js --ext jsx src test",
|
"lint-js": "eslint --ext js --ext jsx src test",
|
||||||
"lint-styles": "stylelint \"src/styles/*.scss\""
|
"lint-css": "stylelint \"src/styles/*.scss\"",
|
||||||
|
"lint": "npm run lint-js && npm run lint-css"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -20,43 +22,46 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://github.com/maputnik/editor#readme",
|
"homepage": "https://github.com/maputnik/editor#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.1.2",
|
"@babel/runtime": "^7.8.4",
|
||||||
"@mapbox/mapbox-gl-rtl-text": "^0.2.2",
|
"@mapbox/mapbox-gl-rtl-text": "^0.2.3",
|
||||||
"@mapbox/mapbox-gl-style-spec": "^13.7.2",
|
"@mapbox/mapbox-gl-style-spec": "^13.12.0",
|
||||||
|
"@mdi/react": "^1.3.0",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"codemirror": "^5.40.2",
|
"codemirror": "^5.52.0",
|
||||||
"color": "^3.0.0",
|
"color": "^3.1.2",
|
||||||
"detect-browser": "^4.5.0",
|
"detect-browser": "^5.0.0",
|
||||||
"file-saver": "^1.3.8",
|
"file-saver": "^2.0.2",
|
||||||
|
"json-to-ast": "^2.1.0",
|
||||||
"jsonlint": "github:josdejong/jsonlint#85a19d7",
|
"jsonlint": "github:josdejong/jsonlint#85a19d7",
|
||||||
|
"lodash": "^4.17.15",
|
||||||
"lodash.capitalize": "^4.2.1",
|
"lodash.capitalize": "^4.2.1",
|
||||||
"lodash.clamp": "^4.0.3",
|
"lodash.clamp": "^4.0.3",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"mapbox-gl": "^1.2.0",
|
"mapbox-gl": "^1.9.0",
|
||||||
"mapbox-gl-inspect": "^1.3.1",
|
"mapbox-gl-inspect": "^1.3.1",
|
||||||
"maputnik-design": "github:maputnik/design",
|
"maputnik-design": "github:maputnik/design#f7a2b4d",
|
||||||
"ol": "^6.0.0-beta.8",
|
"ol": "^6.2.1",
|
||||||
"ol-mapbox-style": "^5.0.0-beta.2",
|
"ol-mapbox-style": "^6.0.1",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.7.2",
|
||||||
"react": "^16.5.2",
|
"react": "^16.12.0",
|
||||||
"react-aria-menubutton": "^6.0.1",
|
"react-accessible-accordion": "^3.0.1",
|
||||||
"react-aria-modal": "^3.0.0",
|
"react-aria-menubutton": "^6.3.0",
|
||||||
|
"react-aria-modal": "^4.0.0",
|
||||||
"react-autobind": "^1.0.6",
|
"react-autobind": "^1.0.6",
|
||||||
"react-autocomplete": "^1.8.1",
|
"react-autocomplete": "^1.8.1",
|
||||||
"react-codemirror2": "^5.1.0",
|
"react-collapse": "^5.0.1",
|
||||||
"react-collapse": "^4.0.3",
|
"react-color": "^2.18.0",
|
||||||
"react-color": "^2.14.1",
|
"react-dom": "^16.12.0",
|
||||||
"react-dom": "^16.5.2",
|
|
||||||
"react-file-reader-input": "^2.0.0",
|
"react-file-reader-input": "^2.0.0",
|
||||||
"react-icon-base": "^2.1.2",
|
"react-icon-base": "^2.1.2",
|
||||||
"react-icons": "^3.1.0",
|
"react-icons": "^3.9.0",
|
||||||
"react-motion": "^0.5.2",
|
"react-motion": "^0.5.2",
|
||||||
"react-sortable-hoc": "^0.8.3",
|
"react-sortable-hoc": "^1.11.0",
|
||||||
"reconnecting-websocket": "^3.2.2",
|
"reconnecting-websocket": "^4.4.0",
|
||||||
"slugify": "^1.3.1",
|
"slugify": "^1.3.6",
|
||||||
"url": "^0.11.0"
|
"url": "^0.11.0"
|
||||||
},
|
},
|
||||||
"jshintConfig": {
|
"jshintConfig": {
|
||||||
@@ -100,51 +105,54 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.1.2",
|
"@babel/core": "^7.8.4",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.1.0",
|
"@babel/plugin-proposal-class-properties": "^7.5.5",
|
||||||
"@babel/plugin-transform-runtime": "^7.1.0",
|
"@babel/plugin-transform-runtime": "^7.6.2",
|
||||||
"@babel/preset-env": "^7.1.0",
|
"@babel/preset-env": "^7.6.3",
|
||||||
"@babel/preset-flow": "^7.0.0",
|
"@babel/preset-flow": "^7.0.0",
|
||||||
"@babel/preset-react": "^7.0.0",
|
"@babel/preset-react": "^7.6.3",
|
||||||
"@wdio/cli": "^5.10.4",
|
"@mdi/js": "^5.0.45",
|
||||||
"@wdio/local-runner": "^5.10.4",
|
"@wdio/cli": "^6.0.5",
|
||||||
"@wdio/mocha-framework": "^5.10.1",
|
"@wdio/local-runner": "^6.0.5",
|
||||||
"@wdio/selenium-standalone-service": "^5.9.3",
|
"@wdio/mocha-framework": "^6.0.4",
|
||||||
"@wdio/spec-reporter": "^5.9.3",
|
"@wdio/selenium-standalone-service": "^6.0.4",
|
||||||
"@wdio/sync": "^5.10.1",
|
"@wdio/spec-reporter": "^6.0.4",
|
||||||
"babel-eslint": "^10.0.1",
|
"@wdio/sync": "^6.0.1",
|
||||||
"babel-loader": "8.0.4",
|
"babel-eslint": "^10.0.3",
|
||||||
"babel-plugin-istanbul": "^5.0.1",
|
"babel-loader": "^8.1.0",
|
||||||
"copy-webpack-plugin": "^4.5.2",
|
"babel-plugin-istanbul": "^6.0.0",
|
||||||
"cors": "^2.8.4",
|
"babel-plugin-static-fs": "^3.0.0",
|
||||||
"cross-env": "^5.2.0",
|
"copy-webpack-plugin": "^5.1.1",
|
||||||
"css-loader": "^1.0.0",
|
"cors": "^2.8.5",
|
||||||
"eslint": "^5.6.1",
|
"cross-env": "^7.0.0",
|
||||||
"eslint-plugin-react": "^7.11.1",
|
"css-loader": "^3.4.2",
|
||||||
|
"eslint": "^6.8.0",
|
||||||
|
"eslint-plugin-react": "^7.18.3",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"file-loader": "^2.0.0",
|
"file-loader": "^6.0.0",
|
||||||
|
"html-webpack-inline-svg-plugin": "^1.3.0",
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
"is-docker": "^2.0.0",
|
"is-docker": "^2.0.0",
|
||||||
"istanbul": "^0.4.5",
|
"istanbul": "^0.4.5",
|
||||||
"istanbul-lib-coverage": "^2.0.1",
|
"istanbul-lib-coverage": "^3.0.0",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^1.0.3",
|
||||||
"mocha": "^6.1.4",
|
"mocha": "^7.0.1",
|
||||||
"node-sass": "^4.12.0",
|
"node-sass": "^4.13.1",
|
||||||
"raw-loader": "^0.5.1",
|
"react-hot-loader": "^4.12.19",
|
||||||
"react-hot-loader": "^4.3.11",
|
"sass-loader": "^8.0.2",
|
||||||
"sass-loader": "^7.1.0",
|
"selenium-standalone": "^6.17.0",
|
||||||
"selenium-standalone": "^6.16.0",
|
"style-loader": "^1.1.3",
|
||||||
"style-loader": "^0.23.0",
|
"stylelint": "^13.2.0",
|
||||||
"stylelint": "^10.0.0",
|
"stylelint-config-recommended-scss": "^4.2.0",
|
||||||
"stylelint-config-recommended-scss": "^3.2.0",
|
"stylelint-scss": "^3.14.2",
|
||||||
"stylelint-scss": "^3.5.4",
|
"svg-inline-loader": "^0.8.2",
|
||||||
"transform-loader": "^0.2.4",
|
"transform-loader": "^0.2.4",
|
||||||
"uuid": "^3.3.2",
|
"uuid": "^7.0.2",
|
||||||
"webdriverio": "^5.10.4",
|
"webdriverio": "^6.0.5",
|
||||||
"webpack": "^4.20.2",
|
"webpack": "^4.41.6",
|
||||||
"webpack-bundle-analyzer": "^3.0.2",
|
"webpack-bundle-analyzer": "^3.6.0",
|
||||||
"webpack-cleanup-plugin": "^0.5.1",
|
"webpack-cleanup-plugin": "^0.5.1",
|
||||||
"webpack-cli": "^3.1.2",
|
"webpack-cli": "^3.3.11",
|
||||||
"webpack-dev-server": "^3.1.9"
|
"webpack-dev-server": "^3.10.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React from 'react'
|
|||||||
import cloneDeep from 'lodash.clonedeep'
|
import cloneDeep from 'lodash.clonedeep'
|
||||||
import clamp from 'lodash.clamp'
|
import clamp from 'lodash.clamp'
|
||||||
import get from 'lodash.get'
|
import get from 'lodash.get'
|
||||||
|
import {unset} from 'lodash'
|
||||||
import {arrayMove} from 'react-sortable-hoc'
|
import {arrayMove} from 'react-sortable-hoc'
|
||||||
import url from 'url'
|
import url from 'url'
|
||||||
|
|
||||||
@@ -97,7 +98,7 @@ export default class App extends React.Component {
|
|||||||
port = window.location.port
|
port = window.location.port
|
||||||
}
|
}
|
||||||
this.styleStore = new ApiStyleStore({
|
this.styleStore = new ApiStyleStore({
|
||||||
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false),
|
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, {save: false}),
|
||||||
port: port,
|
port: port,
|
||||||
host: params.get("localhost")
|
host: params.get("localhost")
|
||||||
})
|
})
|
||||||
@@ -212,13 +213,21 @@ export default class App extends React.Component {
|
|||||||
vectorLayers: {},
|
vectorLayers: {},
|
||||||
mapState: "map",
|
mapState: "map",
|
||||||
spec: latest,
|
spec: latest,
|
||||||
|
mapView: {
|
||||||
|
zoom: 0,
|
||||||
|
center: {
|
||||||
|
lng: 0,
|
||||||
|
lat: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
isOpen: {
|
isOpen: {
|
||||||
settings: false,
|
settings: false,
|
||||||
sources: false,
|
sources: false,
|
||||||
open: false,
|
open: false,
|
||||||
shortcuts: false,
|
shortcuts: false,
|
||||||
export: false,
|
export: false,
|
||||||
survey: localStorage.hasOwnProperty('survey') ? false : true,
|
// TODO: Disabled for now, this should be opened on the Nth visit to the editor
|
||||||
|
survey: false,
|
||||||
debug: false,
|
debug: false,
|
||||||
},
|
},
|
||||||
mapboxGlDebugOptions: {
|
mapboxGlDebugOptions: {
|
||||||
@@ -308,39 +317,87 @@ export default class App extends React.Component {
|
|||||||
this.onStyleChanged(changedStyle)
|
this.onStyleChanged(changedStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
onStyleChanged = (newStyle, save=true) => {
|
onStyleChanged = (newStyle, opts={}) => {
|
||||||
|
opts = {
|
||||||
|
save: true,
|
||||||
|
addRevision: true,
|
||||||
|
...opts,
|
||||||
|
};
|
||||||
|
|
||||||
const errors = validate(newStyle, latest)
|
const errors = validate(newStyle, latest) || [];
|
||||||
if(errors.length === 0) {
|
const mappedErrors = errors.map(error => {
|
||||||
|
const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
|
||||||
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
if (layerMatch) {
|
||||||
this.updateFonts(newStyle.glyphs)
|
const [matchStr, index, group, property, message] = layerMatch;
|
||||||
|
const key = (group && property) ? [group, property].join(".") : property;
|
||||||
|
return {
|
||||||
|
message: error.message,
|
||||||
|
parsed: {
|
||||||
|
type: "layer",
|
||||||
|
data: {
|
||||||
|
index,
|
||||||
|
key,
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(newStyle.sprite !== this.state.mapStyle.sprite) {
|
else {
|
||||||
this.updateIcons(newStyle.sprite)
|
return {
|
||||||
|
message: error.message,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
this.revisionStore.addRevision(newStyle)
|
let dirtyMapStyle = undefined;
|
||||||
if(save) this.saveStyle(newStyle)
|
if (errors.length > 0) {
|
||||||
this.setState({
|
dirtyMapStyle = cloneDeep(newStyle);
|
||||||
mapStyle: newStyle,
|
|
||||||
errors: [],
|
errors.forEach(error => {
|
||||||
})
|
const {message} = error;
|
||||||
} else {
|
if (message) {
|
||||||
this.setState({
|
try {
|
||||||
errors: errors.map(err => err.message)
|
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];
|
||||||
|
unset(dirtyMapStyle, unsetPath);
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.warn(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
|
||||||
|
this.updateFonts(newStyle.glyphs)
|
||||||
|
}
|
||||||
|
if(newStyle.sprite !== this.state.mapStyle.sprite) {
|
||||||
|
this.updateIcons(newStyle.sprite)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.addRevision) {
|
||||||
|
this.revisionStore.addRevision(newStyle);
|
||||||
|
}
|
||||||
|
if (opts.save) {
|
||||||
|
this.saveStyle(newStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
mapStyle: newStyle,
|
||||||
|
dirtyMapStyle: dirtyMapStyle,
|
||||||
|
errors: mappedErrors,
|
||||||
|
})
|
||||||
|
|
||||||
this.fetchSources();
|
this.fetchSources();
|
||||||
}
|
}
|
||||||
|
|
||||||
onUndo = () => {
|
onUndo = () => {
|
||||||
const activeStyle = this.revisionStore.undo()
|
const activeStyle = this.revisionStore.undo()
|
||||||
|
|
||||||
const messages = undoMessages(this.state.mapStyle, activeStyle)
|
const messages = undoMessages(this.state.mapStyle, activeStyle)
|
||||||
this.saveStyle(activeStyle)
|
this.onStyleChanged(activeStyle, {addRevision: false});
|
||||||
this.setState({
|
this.setState({
|
||||||
mapStyle: activeStyle,
|
|
||||||
infos: messages,
|
infos: messages,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -348,9 +405,8 @@ export default class App extends React.Component {
|
|||||||
onRedo = () => {
|
onRedo = () => {
|
||||||
const activeStyle = this.revisionStore.redo()
|
const activeStyle = this.revisionStore.redo()
|
||||||
const messages = redoMessages(this.state.mapStyle, activeStyle)
|
const messages = redoMessages(this.state.mapStyle, activeStyle)
|
||||||
this.saveStyle(activeStyle)
|
this.onStyleChanged(activeStyle, {addRevision: false});
|
||||||
this.setState({
|
this.setState({
|
||||||
mapStyle: activeStyle,
|
|
||||||
infos: messages,
|
infos: messages,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -531,11 +587,23 @@ export default class App extends React.Component {
|
|||||||
return metadata['maputnik:renderer'] || 'mbgljs';
|
return metadata['maputnik:renderer'] || 'mbgljs';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMapChange = (mapView) => {
|
||||||
|
this.setState({
|
||||||
|
mapView,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
mapRenderer() {
|
mapRenderer() {
|
||||||
|
const {mapStyle, dirtyMapStyle} = this.state;
|
||||||
const metadata = this.state.mapStyle.metadata || {};
|
const metadata = this.state.mapStyle.metadata || {};
|
||||||
|
|
||||||
const mapProps = {
|
const mapProps = {
|
||||||
mapStyle: style.replaceAccessTokens(this.state.mapStyle, {allowFallback: true}),
|
mapStyle: (dirtyMapStyle || mapStyle),
|
||||||
|
replaceAccessTokens: (mapStyle) => {
|
||||||
|
return style.replaceAccessTokens(mapStyle, {
|
||||||
|
allowFallback: true
|
||||||
|
});
|
||||||
|
},
|
||||||
onDataChange: (e) => {
|
onDataChange: (e) => {
|
||||||
this.layerWatcher.analyzeMap(e.map)
|
this.layerWatcher.analyzeMap(e.map)
|
||||||
this.fetchSources();
|
this.fetchSources();
|
||||||
@@ -550,11 +618,13 @@ export default class App extends React.Component {
|
|||||||
if(renderer === 'ol') {
|
if(renderer === 'ol') {
|
||||||
mapElement = <OpenLayersMap
|
mapElement = <OpenLayersMap
|
||||||
{...mapProps}
|
{...mapProps}
|
||||||
|
onChange={this.onMapChange}
|
||||||
debugToolbox={this.state.openlayersDebugOptions.debugToolbox}
|
debugToolbox={this.state.openlayersDebugOptions.debugToolbox}
|
||||||
onLayerSelect={this.onLayerSelect}
|
onLayerSelect={this.onLayerSelect}
|
||||||
/>
|
/>
|
||||||
} else {
|
} else {
|
||||||
mapElement = <MapboxGlMap {...mapProps}
|
mapElement = <MapboxGlMap {...mapProps}
|
||||||
|
onChange={this.onMapChange}
|
||||||
options={this.state.mapboxGlDebugOptions}
|
options={this.state.mapboxGlDebugOptions}
|
||||||
inspectModeEnabled={this.state.mapState === "inspect"}
|
inspectModeEnabled={this.state.mapState === "inspect"}
|
||||||
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
|
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
|
||||||
@@ -642,9 +712,11 @@ export default class App extends React.Component {
|
|||||||
selectedLayerIndex={this.state.selectedLayerIndex}
|
selectedLayerIndex={this.state.selectedLayerIndex}
|
||||||
layers={layers}
|
layers={layers}
|
||||||
sources={this.state.sources}
|
sources={this.state.sources}
|
||||||
|
errors={this.state.errors}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
const layerEditor = selectedLayer ? <LayerEditor
|
const layerEditor = selectedLayer ? <LayerEditor
|
||||||
|
key={selectedLayer.id}
|
||||||
layer={selectedLayer}
|
layer={selectedLayer}
|
||||||
layerIndex={this.state.selectedLayerIndex}
|
layerIndex={this.state.selectedLayerIndex}
|
||||||
isFirstLayer={this.state.selectedLayerIndex < 1}
|
isFirstLayer={this.state.selectedLayerIndex < 1}
|
||||||
@@ -658,9 +730,13 @@ export default class App extends React.Component {
|
|||||||
onLayerCopy={this.onLayerCopy}
|
onLayerCopy={this.onLayerCopy}
|
||||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
|
onLayerVisibilityToggle={this.onLayerVisibilityToggle}
|
||||||
onLayerIdChange={this.onLayerIdChange}
|
onLayerIdChange={this.onLayerIdChange}
|
||||||
|
errors={this.state.errors}
|
||||||
/> : null
|
/> : null
|
||||||
|
|
||||||
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
|
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? <MessagePanel
|
||||||
|
currentLayer={selectedLayer}
|
||||||
|
onLayerSelect={this.onLayerSelect}
|
||||||
|
mapStyle={this.state.mapStyle}
|
||||||
errors={this.state.errors}
|
errors={this.state.errors}
|
||||||
infos={this.state.infos}
|
infos={this.state.infos}
|
||||||
/> : null
|
/> : null
|
||||||
@@ -675,6 +751,7 @@ export default class App extends React.Component {
|
|||||||
onChangeOpenlayersDebug={this.onChangeOpenlayersDebug}
|
onChangeOpenlayersDebug={this.onChangeOpenlayersDebug}
|
||||||
isOpen={this.state.isOpen.debug}
|
isOpen={this.state.isOpen.debug}
|
||||||
onOpenToggle={this.toggleModal.bind(this, 'debug')}
|
onOpenToggle={this.toggleModal.bind(this, 'debug')}
|
||||||
|
mapView={this.state.mapView}
|
||||||
/>
|
/>
|
||||||
<ShortcutsModal
|
<ShortcutsModal
|
||||||
ref={(el) => this.shortcutEl = el}
|
ref={(el) => this.shortcutEl = el}
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ class Button extends React.Component {
|
|||||||
aria-label={this.props["aria-label"]}
|
aria-label={this.props["aria-label"]}
|
||||||
className={classnames("maputnik-button", this.props.className)}
|
className={classnames("maputnik-button", this.props.className)}
|
||||||
data-wd-key={this.props["data-wd-key"]}
|
data-wd-key={this.props["data-wd-key"]}
|
||||||
style={this.props.style}>
|
style={this.props.style}
|
||||||
|
>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,45 @@ class MessagePanel extends React.Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
errors: PropTypes.array,
|
errors: PropTypes.array,
|
||||||
infos: PropTypes.array,
|
infos: PropTypes.array,
|
||||||
|
mapStyle: PropTypes.object,
|
||||||
|
onLayerSelect: PropTypes.func,
|
||||||
|
currentLayer: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
onLayerSelect: () => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const errors = this.props.errors.map((m, i) => {
|
const errors = this.props.errors.map((error, idx) => {
|
||||||
return <p key={"error-"+i} className="maputnik-message-panel-error">{m}</p>
|
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;
|
||||||
|
content = (
|
||||||
|
<>
|
||||||
|
Layer <span>'{layerId}'</span>: {parsed.data.message}
|
||||||
|
{currentLayer.id !== layerId &&
|
||||||
|
<>
|
||||||
|
—
|
||||||
|
<button
|
||||||
|
className="maputnik-message-panel__switch-button"
|
||||||
|
onClick={() => this.props.onLayerSelect(layerId)}
|
||||||
|
>
|
||||||
|
switch to layer
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
content = error.message;
|
||||||
|
}
|
||||||
|
return <p key={"error-"+idx} className="maputnik-message-panel-error">
|
||||||
|
{content}
|
||||||
|
</p>
|
||||||
})
|
})
|
||||||
|
|
||||||
const infos = this.props.infos.map((m, i) => {
|
const infos = this.props.infos.map((m, i) => {
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ export default class Toolbar extends React.Component {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
className="maputnik-toolbar-logo"
|
className="maputnik-toolbar-logo"
|
||||||
>
|
>
|
||||||
<img src={logoImage} alt="Maputnik" />
|
<span dangerouslySetInnerHTML={{__html: logoImage}} />
|
||||||
<h1>
|
<h1>
|
||||||
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
|
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
|
||||||
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
|
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react'
|
|||||||
import Color from 'color'
|
import Color from 'color'
|
||||||
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
|
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
import lodash from 'lodash';
|
||||||
|
|
||||||
function formatColor(color) {
|
function formatColor(color) {
|
||||||
const rgb = color.rgb
|
const rgb = color.rgb
|
||||||
@@ -12,7 +13,7 @@ function formatColor(color) {
|
|||||||
class ColorField extends React.Component {
|
class ColorField extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
doc: PropTypes.string,
|
doc: PropTypes.string,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
@@ -23,6 +24,15 @@ class ColorField extends React.Component {
|
|||||||
pickerOpened: false
|
pickerOpened: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
super();
|
||||||
|
this.onChangeNoCheck = lodash.throttle(this.onChangeNoCheck, 1000/30);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeNoCheck (v) {
|
||||||
|
this.props.onChange(v);
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: I much rather would do this with absolute positioning
|
//TODO: I much rather would do this with absolute positioning
|
||||||
//but I am too stupid to get it to work together with fixed position
|
//but I am too stupid to get it to work together with fixed position
|
||||||
//and scrollbars so I have to fallback to JavaScript
|
//and scrollbars so I have to fallback to JavaScript
|
||||||
@@ -82,7 +92,7 @@ class ColorField extends React.Component {
|
|||||||
}}>
|
}}>
|
||||||
<ChromePicker
|
<ChromePicker
|
||||||
color={currentColor}
|
color={currentColor}
|
||||||
onChange={c => this.props.onChange(formatColor(c))}
|
onChange={c => this.onChangeNoCheck(formatColor(c))}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="maputnik-color-picker-offset"
|
className="maputnik-color-picker-offset"
|
||||||
|
|||||||
@@ -1,23 +1,63 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import {MdInfoOutline, MdHighlightOff} from 'react-icons/md'
|
||||||
|
|
||||||
export default class DocLabel extends React.Component {
|
export default class DocLabel extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
label: PropTypes.oneOfType([
|
label: PropTypes.oneOfType([
|
||||||
PropTypes.object,
|
PropTypes.object,
|
||||||
PropTypes.string
|
PropTypes.string
|
||||||
]).isRequired,
|
]).isRequired,
|
||||||
doc: PropTypes.string.isRequired,
|
fieldSpec: PropTypes.object.isRequired,
|
||||||
|
onToggleDoc: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
open: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggleDoc = (open) => {
|
||||||
|
this.setState({
|
||||||
|
open,
|
||||||
|
}, () => {
|
||||||
|
if (this.props.onToggleDoc) {
|
||||||
|
this.props.onToggleDoc(this.state.open);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <label className="maputnik-doc-wrapper">
|
const {label, fieldSpec} = this.props;
|
||||||
<div className="maputnik-doc-target">
|
const {doc} = fieldSpec || {};
|
||||||
<span>{this.props.label}</span>
|
|
||||||
<div className="maputnik-doc-popup">
|
if (doc) {
|
||||||
{this.props.doc}
|
return <label className="maputnik-doc-wrapper">
|
||||||
|
<div className="maputnik-doc-target">
|
||||||
|
{label}
|
||||||
|
{'\xa0'}
|
||||||
|
<button
|
||||||
|
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)}
|
||||||
|
>
|
||||||
|
{this.state.open ? <MdHighlightOff /> : <MdInfoOutline />}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</label>
|
||||||
</label>
|
}
|
||||||
|
else if (label) {
|
||||||
|
return <label className="maputnik-doc-wrapper">
|
||||||
|
<div className="maputnik-doc-target">
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
<div />
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,97 @@ import PropTypes from 'prop-types'
|
|||||||
import SpecProperty from './_SpecProperty'
|
import SpecProperty from './_SpecProperty'
|
||||||
import DataProperty from './_DataProperty'
|
import DataProperty from './_DataProperty'
|
||||||
import ZoomProperty from './_ZoomProperty'
|
import ZoomProperty from './_ZoomProperty'
|
||||||
|
import ExpressionProperty from './_ExpressionProperty'
|
||||||
|
import {function as styleFunction} from '@mapbox/mapbox-gl-style-spec';
|
||||||
|
|
||||||
|
|
||||||
|
function isLiteralExpression (value) {
|
||||||
|
return (Array.isArray(value) && value.length === 2 && value[0] === "literal");
|
||||||
|
}
|
||||||
|
|
||||||
function isZoomField(value) {
|
function isZoomField(value) {
|
||||||
return typeof value === 'object' && value.stops && typeof value.property === 'undefined'
|
return (
|
||||||
|
typeof(value) === 'object' &&
|
||||||
|
value.stops &&
|
||||||
|
typeof(value.property) === 'undefined' &&
|
||||||
|
Array.isArray(value.stops) &&
|
||||||
|
value.stops.length > 1 &&
|
||||||
|
value.stops.every(stop => {
|
||||||
|
return (
|
||||||
|
Array.isArray(stop) &&
|
||||||
|
stop.length === 2
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDataField(value) {
|
function isDataField(value) {
|
||||||
return typeof value === 'object' && value.stops && typeof value.property !== 'undefined'
|
return (
|
||||||
|
typeof(value) === 'object' &&
|
||||||
|
value.stops &&
|
||||||
|
typeof(value.property) !== 'undefined' &&
|
||||||
|
value.stops.length > 1 &&
|
||||||
|
Array.isArray(value.stops) &&
|
||||||
|
value.stops.every(stop => {
|
||||||
|
return (
|
||||||
|
Array.isArray(stop) &&
|
||||||
|
stop.length === 2 &&
|
||||||
|
typeof(stop[0]) === 'object'
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPrimative (value) {
|
||||||
|
const valid = ["string", "boolean", "number"];
|
||||||
|
return valid.includes(typeof(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isArrayOfPrimatives (values) {
|
||||||
|
if (Array.isArray(values)) {
|
||||||
|
return values.every(isPrimative);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDataType (value, fieldSpec={}) {
|
||||||
|
if (value === undefined) {
|
||||||
|
return "value";
|
||||||
|
}
|
||||||
|
else if (isPrimative(value)) {
|
||||||
|
return "value";
|
||||||
|
}
|
||||||
|
else if (fieldSpec.type === "array" && isArrayOfPrimatives(value)) {
|
||||||
|
return "value";
|
||||||
|
}
|
||||||
|
else if (isZoomField(value)) {
|
||||||
|
return "zoom_function";
|
||||||
|
}
|
||||||
|
else if (isDataField(value)) {
|
||||||
|
return "data_function";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "expression";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If we don't have a default value just make one up
|
||||||
|
*/
|
||||||
|
function findDefaultFromSpec (spec) {
|
||||||
|
if (spec.hasOwnProperty('default')) {
|
||||||
|
return spec.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaults = {
|
||||||
|
'color': '#000000',
|
||||||
|
'string': '',
|
||||||
|
'boolean': false,
|
||||||
|
'number': 0,
|
||||||
|
'array': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaults[spec.type] || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Supports displaying spec field for zoom function objects
|
/** Supports displaying spec field for zoom function objects
|
||||||
@@ -21,7 +104,9 @@ export default class FunctionSpecProperty extends React.Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
fieldName: PropTypes.string.isRequired,
|
fieldName: PropTypes.string.isRequired,
|
||||||
|
fieldType: PropTypes.string.isRequired,
|
||||||
fieldSpec: PropTypes.object.isRequired,
|
fieldSpec: PropTypes.object.isRequired,
|
||||||
|
errors: PropTypes.object,
|
||||||
|
|
||||||
value: PropTypes.oneOfType([
|
value: PropTypes.oneOfType([
|
||||||
PropTypes.object,
|
PropTypes.object,
|
||||||
@@ -32,6 +117,27 @@ export default class FunctionSpecProperty extends React.Component {
|
|||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super();
|
||||||
|
this.state = {
|
||||||
|
dataType: getDataType(props.value, props.fieldSpec),
|
||||||
|
isEditing: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromProps(props, state) {
|
||||||
|
// Because otherwise when editing values we end up accidentally changing field type.
|
||||||
|
if (state.isEditing) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return {
|
||||||
|
isEditing: false,
|
||||||
|
dataType: getDataType(props.value, props.fieldSpec)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getFieldFunctionType(fieldSpec) {
|
getFieldFunctionType(fieldSpec) {
|
||||||
if (fieldSpec.expression.interpolated) {
|
if (fieldSpec.expression.interpolated) {
|
||||||
return "exponential"
|
return "exponential"
|
||||||
@@ -63,6 +169,14 @@ export default class FunctionSpecProperty extends React.Component {
|
|||||||
this.props.onChange(this.props.fieldName, changedValue)
|
this.props.onChange(this.props.fieldName, changedValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteExpression = () => {
|
||||||
|
const {fieldSpec, fieldName} = this.props;
|
||||||
|
this.props.onChange(fieldName, fieldSpec.default);
|
||||||
|
this.setState({
|
||||||
|
dataType: "value",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
deleteStop = (stopIdx) => {
|
deleteStop = (stopIdx) => {
|
||||||
const stops = this.props.value.stops.slice(0)
|
const stops = this.props.value.stops.slice(0)
|
||||||
stops.splice(stopIdx, 1)
|
stops.splice(stopIdx, 1)
|
||||||
@@ -82,13 +196,46 @@ export default class FunctionSpecProperty extends React.Component {
|
|||||||
makeZoomFunction = () => {
|
makeZoomFunction = () => {
|
||||||
const zoomFunc = {
|
const zoomFunc = {
|
||||||
stops: [
|
stops: [
|
||||||
[6, this.props.value],
|
[6, this.props.value || findDefaultFromSpec(this.props.fieldSpec)],
|
||||||
[10, this.props.value]
|
[10, this.props.value || findDefaultFromSpec(this.props.fieldSpec)]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
this.props.onChange(this.props.fieldName, zoomFunc)
|
this.props.onChange(this.props.fieldName, zoomFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
undoExpression = () => {
|
||||||
|
const {value, fieldName} = this.props;
|
||||||
|
|
||||||
|
if (isLiteralExpression(value)) {
|
||||||
|
this.props.onChange(fieldName, value[1]);
|
||||||
|
this.setState({
|
||||||
|
dataType: "value",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canUndo = () => {
|
||||||
|
const {value, fieldSpec} = this.props;
|
||||||
|
return (
|
||||||
|
isLiteralExpression(value) ||
|
||||||
|
isPrimative(value) ||
|
||||||
|
(Array.isArray(value) && fieldSpec.type === "array")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
makeExpression = () => {
|
||||||
|
const {value, fieldSpec} = this.props;
|
||||||
|
let expression;
|
||||||
|
|
||||||
|
if (typeof(value) === "object" && 'stops' in value) {
|
||||||
|
expression = styleFunction.convertFunction(value, fieldSpec);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
expression = ["literal", value || this.props.fieldSpec.default];
|
||||||
|
}
|
||||||
|
this.props.onChange(this.props.fieldName, expression);
|
||||||
|
}
|
||||||
|
|
||||||
makeDataFunction = () => {
|
makeDataFunction = () => {
|
||||||
const functionType = this.getFieldFunctionType(this.props.fieldSpec);
|
const functionType = this.getFieldFunctionType(this.props.fieldSpec);
|
||||||
const stopValue = functionType === 'categorical' ? '' : 0;
|
const stopValue = functionType === 'categorical' ? '' : 0;
|
||||||
@@ -96,50 +243,85 @@ export default class FunctionSpecProperty extends React.Component {
|
|||||||
property: "",
|
property: "",
|
||||||
type: functionType,
|
type: functionType,
|
||||||
stops: [
|
stops: [
|
||||||
[{zoom: 6, value: stopValue}, this.props.value || stopValue],
|
[{zoom: 6, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)],
|
||||||
[{zoom: 10, value: stopValue}, this.props.value || stopValue]
|
[{zoom: 10, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
this.props.onChange(this.props.fieldName, dataFunc)
|
this.props.onChange(this.props.fieldName, dataFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMarkEditing = () => {
|
||||||
|
this.setState({isEditing: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmarkEditing = () => {
|
||||||
|
this.setState({isEditing: false});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {dataType} = this.state;
|
||||||
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
|
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
|
||||||
let specField;
|
let specField;
|
||||||
|
|
||||||
if (isZoomField(this.props.value)) {
|
if (dataType === "expression") {
|
||||||
|
specField = (
|
||||||
|
<ExpressionProperty
|
||||||
|
errors={this.props.errors}
|
||||||
|
onChange={this.props.onChange.bind(this, this.props.fieldName)}
|
||||||
|
canUndo={this.canUndo}
|
||||||
|
onUndo={this.undoExpression}
|
||||||
|
onDelete={this.deleteExpression}
|
||||||
|
fieldType={this.props.fieldType}
|
||||||
|
fieldName={this.props.fieldName}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
value={this.props.value}
|
||||||
|
onFocus={this.onMarkEditing}
|
||||||
|
onBlur={this.onUnmarkEditing}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (dataType === "zoom_function") {
|
||||||
specField = (
|
specField = (
|
||||||
<ZoomProperty
|
<ZoomProperty
|
||||||
|
errors={this.props.errors}
|
||||||
onChange={this.props.onChange.bind(this)}
|
onChange={this.props.onChange.bind(this)}
|
||||||
|
fieldType={this.props.fieldType}
|
||||||
fieldName={this.props.fieldName}
|
fieldName={this.props.fieldName}
|
||||||
fieldSpec={this.props.fieldSpec}
|
fieldSpec={this.props.fieldSpec}
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onDeleteStop={this.deleteStop}
|
onDeleteStop={this.deleteStop}
|
||||||
onAddStop={this.addStop}
|
onAddStop={this.addStop}
|
||||||
|
onExpressionClick={this.makeExpression}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else if (isDataField(this.props.value)) {
|
else if (dataType === "data_function") {
|
||||||
specField = (
|
specField = (
|
||||||
<DataProperty
|
<DataProperty
|
||||||
|
errors={this.props.errors}
|
||||||
onChange={this.props.onChange.bind(this)}
|
onChange={this.props.onChange.bind(this)}
|
||||||
|
fieldType={this.props.fieldType}
|
||||||
fieldName={this.props.fieldName}
|
fieldName={this.props.fieldName}
|
||||||
fieldSpec={this.props.fieldSpec}
|
fieldSpec={this.props.fieldSpec}
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onDeleteStop={this.deleteStop}
|
onDeleteStop={this.deleteStop}
|
||||||
onAddStop={this.addStop}
|
onAddStop={this.addStop}
|
||||||
|
onExpressionClick={this.makeExpression}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
specField = (
|
specField = (
|
||||||
<SpecProperty
|
<SpecProperty
|
||||||
|
errors={this.props.errors}
|
||||||
onChange={this.props.onChange.bind(this)}
|
onChange={this.props.onChange.bind(this)}
|
||||||
|
fieldType={this.props.fieldType}
|
||||||
fieldName={this.props.fieldName}
|
fieldName={this.props.fieldName}
|
||||||
fieldSpec={this.props.fieldSpec}
|
fieldSpec={this.props.fieldSpec}
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onZoomClick={this.makeZoomFunction}
|
onZoomClick={this.makeZoomFunction}
|
||||||
onDataClick={this.makeDataFunction}
|
onDataClick={this.makeDataFunction}
|
||||||
|
onExpressionClick={this.makeExpression}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export default class PropertyGroup extends React.Component {
|
|||||||
groupFields: PropTypes.array.isRequired,
|
groupFields: PropTypes.array.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
spec: PropTypes.object.isRequired,
|
spec: PropTypes.object.isRequired,
|
||||||
|
errors: PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
onPropertyChange = (property, newValue) => {
|
onPropertyChange = (property, newValue) => {
|
||||||
@@ -48,18 +49,22 @@ export default class PropertyGroup extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {errors} = this.props;
|
||||||
const fields = this.props.groupFields.map(fieldName => {
|
const fields = this.props.groupFields.map(fieldName => {
|
||||||
const fieldSpec = getFieldSpec(this.props.spec, this.props.layer.type, fieldName)
|
const fieldSpec = getFieldSpec(this.props.spec, this.props.layer.type, fieldName)
|
||||||
|
|
||||||
const paint = this.props.layer.paint || {}
|
const paint = this.props.layer.paint || {}
|
||||||
const layout = this.props.layer.layout || {}
|
const layout = this.props.layer.layout || {}
|
||||||
const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName]
|
const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName]
|
||||||
|
const fieldType = fieldName in paint ? 'paint' : 'layout';
|
||||||
|
|
||||||
return <FunctionSpecField
|
return <FunctionSpecField
|
||||||
|
errors={errors}
|
||||||
onChange={this.onPropertyChange}
|
onChange={this.onPropertyChange}
|
||||||
key={fieldName}
|
key={fieldName}
|
||||||
fieldName={fieldName}
|
fieldName={fieldName}
|
||||||
value={fieldValue === undefined ? fieldSpec.default : fieldValue}
|
value={fieldValue}
|
||||||
|
fieldType={fieldType}
|
||||||
fieldSpec={fieldSpec}
|
fieldSpec={fieldSpec}
|
||||||
/>
|
/>
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import ArrayInput from '../inputs/ArrayInput'
|
|||||||
import DynamicArrayInput from '../inputs/DynamicArrayInput'
|
import DynamicArrayInput from '../inputs/DynamicArrayInput'
|
||||||
import FontInput from '../inputs/FontInput'
|
import FontInput from '../inputs/FontInput'
|
||||||
import IconInput from '../inputs/IconInput'
|
import IconInput from '../inputs/IconInput'
|
||||||
|
import EnumInput from '../inputs/EnumInput'
|
||||||
import capitalize from 'lodash.capitalize'
|
import capitalize from 'lodash.capitalize'
|
||||||
|
|
||||||
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
|
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
|
||||||
@@ -70,17 +71,11 @@ export default class SpecField extends React.Component {
|
|||||||
case 'enum':
|
case 'enum':
|
||||||
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)])
|
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)])
|
||||||
|
|
||||||
if(options.length <= 3 && optionsLabelLength(options) <= 20) {
|
return <EnumInput
|
||||||
return <MultiButtonInput
|
{...commonProps}
|
||||||
{...commonProps}
|
options={options}
|
||||||
options={options}
|
/>
|
||||||
/>
|
case 'resolvedImage':
|
||||||
} else {
|
|
||||||
return <SelectInput
|
|
||||||
{...commonProps}
|
|
||||||
options={options}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
case 'formatted':
|
case 'formatted':
|
||||||
case 'string':
|
case 'string':
|
||||||
if(iconProperties.indexOf(this.props.fieldName) >= 0) {
|
if(iconProperties.indexOf(this.props.fieldName) >= 0) {
|
||||||
@@ -119,6 +114,7 @@ export default class SpecField extends React.Component {
|
|||||||
} else {
|
} else {
|
||||||
return <DynamicArrayInput
|
return <DynamicArrayInput
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
type={this.props.fieldSpec.value}
|
type={this.props.fieldSpec.value}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,17 +8,40 @@ import StringInput from '../inputs/StringInput'
|
|||||||
import SelectInput from '../inputs/SelectInput'
|
import SelectInput from '../inputs/SelectInput'
|
||||||
import DocLabel from './DocLabel'
|
import DocLabel from './DocLabel'
|
||||||
import InputBlock from '../inputs/InputBlock'
|
import InputBlock from '../inputs/InputBlock'
|
||||||
|
import docUid from '../../libs/document-uid'
|
||||||
|
import sortNumerically from '../../libs/sort-numerically'
|
||||||
|
|
||||||
import labelFromFieldName from './_labelFromFieldName'
|
import labelFromFieldName from './_labelFromFieldName'
|
||||||
import DeleteStopButton from './_DeleteStopButton'
|
import DeleteStopButton from './_DeleteStopButton'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function setStopRefs(props, state) {
|
||||||
|
// This is initialsed below only if required to improved performance.
|
||||||
|
let newRefs;
|
||||||
|
|
||||||
|
if(props.value && props.value.stops) {
|
||||||
|
props.value.stops.forEach((val, idx) => {
|
||||||
|
if(!state.refs.hasOwnProperty(idx)) {
|
||||||
|
if(!newRefs) {
|
||||||
|
newRefs = {...state};
|
||||||
|
}
|
||||||
|
newRefs[idx] = docUid("stop-");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return newRefs;
|
||||||
|
}
|
||||||
|
|
||||||
export default class DataProperty extends React.Component {
|
export default class DataProperty extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
onDeleteStop: PropTypes.func,
|
onDeleteStop: PropTypes.func,
|
||||||
onAddStop: PropTypes.func,
|
onAddStop: PropTypes.func,
|
||||||
|
onExpressionClick: PropTypes.func,
|
||||||
fieldName: PropTypes.string,
|
fieldName: PropTypes.string,
|
||||||
|
fieldType: PropTypes.string,
|
||||||
fieldSpec: PropTypes.object,
|
fieldSpec: PropTypes.object,
|
||||||
value: PropTypes.oneOfType([
|
value: PropTypes.oneOfType([
|
||||||
PropTypes.object,
|
PropTypes.object,
|
||||||
@@ -27,6 +50,31 @@ export default class DataProperty extends React.Component {
|
|||||||
PropTypes.bool,
|
PropTypes.bool,
|
||||||
PropTypes.array
|
PropTypes.array
|
||||||
]),
|
]),
|
||||||
|
errors: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
state = {
|
||||||
|
refs: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const newRefs = setStopRefs(this.props, this.state);
|
||||||
|
|
||||||
|
if(newRefs) {
|
||||||
|
this.setState({
|
||||||
|
refs: newRefs
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromProps(props, state) {
|
||||||
|
const newRefs = setStopRefs(props, state);
|
||||||
|
if(newRefs) {
|
||||||
|
return {
|
||||||
|
refs: newRefs
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFieldFunctionType(fieldSpec) {
|
getFieldFunctionType(fieldSpec) {
|
||||||
@@ -48,14 +96,42 @@ export default class DataProperty extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Order the stops altering the refs to reflect their new position.
|
||||||
|
orderStopsByZoom(stops) {
|
||||||
|
const mappedWithRef = stops
|
||||||
|
.map((stop, idx) => {
|
||||||
|
return {
|
||||||
|
ref: this.state.refs[idx],
|
||||||
|
data: stop
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Sort by zoom
|
||||||
|
.sort((a, b) => sortNumerically(a.data[0].zoom, b.data[0].zoom));
|
||||||
|
|
||||||
|
// Fetch the new position of the stops
|
||||||
|
const newRefs = {};
|
||||||
|
mappedWithRef
|
||||||
|
.forEach((stop, idx) =>{
|
||||||
|
newRefs[idx] = stop.ref;
|
||||||
|
})
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
refs: newRefs
|
||||||
|
});
|
||||||
|
|
||||||
|
return mappedWithRef.map((item) => item.data);
|
||||||
|
}
|
||||||
|
|
||||||
changeStop(changeIdx, stopData, value) {
|
changeStop(changeIdx, stopData, value) {
|
||||||
const stops = this.props.value.stops.slice(0)
|
const stops = this.props.value.stops.slice(0)
|
||||||
const changedStop = stopData.zoom === undefined ? stopData.value : stopData
|
const changedStop = stopData.zoom === undefined ? stopData.value : stopData
|
||||||
stops[changeIdx] = [changedStop, value]
|
stops[changeIdx] = [changedStop, value]
|
||||||
|
|
||||||
|
const orderedStops = this.orderStopsByZoom(stops);
|
||||||
|
|
||||||
const changedValue = {
|
const changedValue = {
|
||||||
...this.props.value,
|
...this.props.value,
|
||||||
stops: stops,
|
stops: orderedStops,
|
||||||
}
|
}
|
||||||
this.props.onChange(this.props.fieldName, changedValue)
|
this.props.onChange(this.props.fieldName, changedValue)
|
||||||
}
|
}
|
||||||
@@ -71,12 +147,15 @@ export default class DataProperty extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {fieldName, fieldType, errors} = this.props;
|
||||||
|
|
||||||
if (typeof this.props.value.type === "undefined") {
|
if (typeof this.props.value.type === "undefined") {
|
||||||
this.props.value.type = this.getFieldFunctionType(this.props.fieldSpec)
|
this.props.value.type = this.getFieldFunctionType(this.props.fieldSpec)
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataFields = this.props.value.stops.map((stop, idx) => {
|
const dataFields = this.props.value.stops.map((stop, idx) => {
|
||||||
const zoomLevel = typeof stop[0] === 'object' ? stop[0].zoom : undefined;
|
const zoomLevel = typeof stop[0] === 'object' ? stop[0].zoom : undefined;
|
||||||
|
const key = this.state.refs[idx];
|
||||||
const dataLevel = typeof stop[0] === 'object' ? stop[0].value : stop[0];
|
const dataLevel = typeof stop[0] === 'object' ? stop[0].value : stop[0];
|
||||||
const value = stop[1]
|
const value = stop[1]
|
||||||
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
|
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
|
||||||
@@ -107,7 +186,22 @@ export default class DataProperty extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
return <InputBlock key={idx} action={deleteStopBtn} label="">
|
const errorKeyStart = `${fieldType}.${fieldName}.stops[${idx}]`;
|
||||||
|
const foundErrors = Object.entries(errors).filter(([key, error]) => {
|
||||||
|
return key.startsWith(errorKeyStart);
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = foundErrors.map(([key, error]) => {
|
||||||
|
return error.message;
|
||||||
|
}).join("");
|
||||||
|
const error = message ? {message} : undefined;
|
||||||
|
|
||||||
|
return <InputBlock
|
||||||
|
error={error}
|
||||||
|
key={key}
|
||||||
|
action={deleteStopBtn}
|
||||||
|
label=""
|
||||||
|
>
|
||||||
{zoomInput}
|
{zoomInput}
|
||||||
<div className="maputnik-data-spec-property-stop-data">
|
<div className="maputnik-data-spec-property-stop-data">
|
||||||
{dataInput}
|
{dataInput}
|
||||||
@@ -124,52 +218,51 @@ export default class DataProperty extends React.Component {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return <div className="maputnik-data-spec-block">
|
return <div className="maputnik-data-spec-block">
|
||||||
<div className="maputnik-data-spec-property">
|
<div className="maputnik-data-spec-property">
|
||||||
<InputBlock
|
<InputBlock
|
||||||
doc={this.props.fieldSpec.doc}
|
fieldSpec={this.props.fieldSpec}
|
||||||
label={labelFromFieldName(this.props.fieldName)}
|
label={labelFromFieldName(this.props.fieldName)}
|
||||||
>
|
>
|
||||||
<div className="maputnik-data-spec-property-group">
|
<div className="maputnik-data-spec-property-group">
|
||||||
<DocLabel
|
<DocLabel
|
||||||
label="Property"
|
label="Property"
|
||||||
doc={"Input a data property to base styles off of."}
|
|
||||||
/>
|
|
||||||
<div className="maputnik-data-spec-property-input">
|
|
||||||
<StringInput
|
|
||||||
value={this.props.value.property}
|
|
||||||
onChange={propVal => this.changeDataProperty("property", propVal)}
|
|
||||||
/>
|
/>
|
||||||
|
<div className="maputnik-data-spec-property-input">
|
||||||
|
<StringInput
|
||||||
|
value={this.props.value.property}
|
||||||
|
title={"Input a data property to base styles off of."}
|
||||||
|
onChange={propVal => this.changeDataProperty("property", propVal)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="maputnik-data-spec-property-group">
|
||||||
<div className="maputnik-data-spec-property-group">
|
<DocLabel
|
||||||
<DocLabel
|
label="Type"
|
||||||
label="Type"
|
|
||||||
doc={"Select a type of data scale (default is 'categorical')."}
|
|
||||||
/>
|
|
||||||
<div className="maputnik-data-spec-property-input">
|
|
||||||
<SelectInput
|
|
||||||
value={this.props.value.type}
|
|
||||||
onChange={propVal => this.changeDataProperty("type", propVal)}
|
|
||||||
options={this.getDataFunctionTypes(this.props.fieldSpec)}
|
|
||||||
/>
|
/>
|
||||||
|
<div className="maputnik-data-spec-property-input">
|
||||||
|
<SelectInput
|
||||||
|
value={this.props.value.type}
|
||||||
|
onChange={propVal => this.changeDataProperty("type", propVal)}
|
||||||
|
title={"Select a type of data scale (default is 'categorical')."}
|
||||||
|
options={this.getDataFunctionTypes(this.props.fieldSpec)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="maputnik-data-spec-property-group">
|
||||||
<div className="maputnik-data-spec-property-group">
|
<DocLabel
|
||||||
<DocLabel
|
label="Default"
|
||||||
label="Default"
|
|
||||||
doc={"Input a default value for data if not covered by the scales."}
|
|
||||||
/>
|
|
||||||
<div className="maputnik-data-spec-property-input">
|
|
||||||
<SpecField
|
|
||||||
fieldName={this.props.fieldName}
|
|
||||||
fieldSpec={this.props.fieldSpec}
|
|
||||||
value={this.props.value.default}
|
|
||||||
onChange={(_, propVal) => this.changeDataProperty("default", propVal)}
|
|
||||||
/>
|
/>
|
||||||
|
<div className="maputnik-data-spec-property-input">
|
||||||
|
<SpecField
|
||||||
|
fieldName={this.props.fieldName}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
value={this.props.value.default}
|
||||||
|
onChange={(_, propVal) => this.changeDataProperty("default", propVal)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</InputBlock>
|
||||||
</InputBlock>
|
</div>
|
||||||
</div>
|
|
||||||
{dataFields}
|
{dataFields}
|
||||||
<Button
|
<Button
|
||||||
className="maputnik-add-stop"
|
className="maputnik-add-stop"
|
||||||
@@ -177,6 +270,12 @@ export default class DataProperty extends React.Component {
|
|||||||
>
|
>
|
||||||
Add stop
|
Add stop
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="maputnik-add-stop"
|
||||||
|
onClick={this.props.onExpressionClick.bind(this)}
|
||||||
|
>
|
||||||
|
Convert to expression
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import DocLabel from './DocLabel'
|
|
||||||
import Button from '../Button'
|
import Button from '../Button'
|
||||||
import {MdDelete} from 'react-icons/md'
|
import {MdDelete} from 'react-icons/md'
|
||||||
|
|
||||||
@@ -15,11 +14,9 @@ export default class DeleteStopButton extends React.Component {
|
|||||||
return <Button
|
return <Button
|
||||||
className="maputnik-delete-stop"
|
className="maputnik-delete-stop"
|
||||||
onClick={this.props.onClick}
|
onClick={this.props.onClick}
|
||||||
|
title={"Remove zoom level stop."}
|
||||||
>
|
>
|
||||||
<DocLabel
|
<MdDelete />
|
||||||
label={<MdDelete />}
|
|
||||||
doc={"Remove zoom level stop."}
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
135
src/components/fields/_ExpressionProperty.jsx
Normal file
135
src/components/fields/_ExpressionProperty.jsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import InputBlock from '../inputs/InputBlock'
|
||||||
|
import Button from '../Button'
|
||||||
|
import {MdDelete, MdUndo} from 'react-icons/md'
|
||||||
|
import StringInput from '../inputs/StringInput'
|
||||||
|
|
||||||
|
import labelFromFieldName from './_labelFromFieldName'
|
||||||
|
import stringifyPretty from 'json-stringify-pretty-compact'
|
||||||
|
import JSONEditor from '../layers/JSONEditor'
|
||||||
|
|
||||||
|
|
||||||
|
export default class ExpressionProperty extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
onDelete: PropTypes.func,
|
||||||
|
fieldName: PropTypes.string,
|
||||||
|
fieldType: PropTypes.string,
|
||||||
|
fieldSpec: PropTypes.object,
|
||||||
|
value: PropTypes.any,
|
||||||
|
errors: PropTypes.object,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
onUndo: PropTypes.func,
|
||||||
|
canUndo: PropTypes.func,
|
||||||
|
onFocus: PropTypes.func,
|
||||||
|
onBlur: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
errors: {},
|
||||||
|
onFocus: () => {},
|
||||||
|
onBlur: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super();
|
||||||
|
this.state = {
|
||||||
|
jsonError: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onJSONInvalid = (err) => {
|
||||||
|
this.setState({
|
||||||
|
jsonError: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onJSONValid = () => {
|
||||||
|
this.setState({
|
||||||
|
jsonError: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {errors, fieldName, fieldType, value, canUndo} = this.props;
|
||||||
|
const {jsonError} = this.state;
|
||||||
|
const undoDisabled = canUndo ? !canUndo() : true;
|
||||||
|
|
||||||
|
const deleteStopBtn = (
|
||||||
|
<>
|
||||||
|
{this.props.onUndo &&
|
||||||
|
<Button
|
||||||
|
key="undo_action"
|
||||||
|
onClick={this.props.onUndo}
|
||||||
|
disabled={undoDisabled}
|
||||||
|
className="maputnik-delete-stop"
|
||||||
|
>
|
||||||
|
<MdUndo />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
<Button
|
||||||
|
key="delete_action"
|
||||||
|
onClick={this.props.onDelete}
|
||||||
|
className="maputnik-delete-stop"
|
||||||
|
>
|
||||||
|
<MdDelete />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const fieldKey = fieldType === undefined ? fieldName : `${fieldType}.${fieldName}`;
|
||||||
|
|
||||||
|
const fieldError = errors[fieldKey];
|
||||||
|
const errorKeyStart = `${fieldKey}[`;
|
||||||
|
const foundErrors = [];
|
||||||
|
|
||||||
|
function getValue (data) {
|
||||||
|
return stringifyPretty(data, {indent: 2, maxLength: 38})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonError) {
|
||||||
|
foundErrors.push({message: "Invalid JSON"});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Object.entries(errors)
|
||||||
|
.filter(([key, error]) => {
|
||||||
|
return key.startsWith(errorKeyStart);
|
||||||
|
})
|
||||||
|
.forEach(([key, error]) => {
|
||||||
|
return foundErrors.push(error);
|
||||||
|
})
|
||||||
|
|
||||||
|
if (fieldError) {
|
||||||
|
foundErrors.push(fieldError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <InputBlock
|
||||||
|
error={foundErrors}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
label={labelFromFieldName(this.props.fieldName)}
|
||||||
|
action={deleteStopBtn}
|
||||||
|
wideMode={true}
|
||||||
|
>
|
||||||
|
<JSONEditor
|
||||||
|
mode={{name: "mgl"}}
|
||||||
|
lint={{
|
||||||
|
context: "expression",
|
||||||
|
spec: this.props.fieldSpec,
|
||||||
|
}}
|
||||||
|
className="maputnik-expression-editor"
|
||||||
|
onFocus={this.props.onFocus}
|
||||||
|
onBlur={this.props.onBlur}
|
||||||
|
onJSONInvalid={this.onJSONInvalid}
|
||||||
|
onJSONValid={this.onJSONValid}
|
||||||
|
layer={value}
|
||||||
|
lineNumbers={false}
|
||||||
|
maxHeight={200}
|
||||||
|
lineWrapping={true}
|
||||||
|
getValue={getValue}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,48 +1,77 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import DocLabel from './DocLabel'
|
|
||||||
import Button from '../Button'
|
import Button from '../Button'
|
||||||
import {MdFunctions, MdInsertChart} from 'react-icons/md'
|
import {MdFunctions, MdInsertChart} from 'react-icons/md'
|
||||||
|
import {mdiFunctionVariant} from '@mdi/js';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* So here we can't just check is `Array.isArray(value)` because certain
|
||||||
|
* properties accept arrays as values, for example `text-font`. So we must try
|
||||||
|
* and create an expression.
|
||||||
|
*/
|
||||||
|
function isExpression(value, fieldSpec={}) {
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
expression.createExpression(value, fieldSpec);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default class FunctionButtons extends React.Component {
|
export default class FunctionButtons extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
fieldSpec: PropTypes.object,
|
fieldSpec: PropTypes.object,
|
||||||
onZoomClick: PropTypes.func,
|
onZoomClick: PropTypes.func,
|
||||||
onDataClick: PropTypes.func,
|
onDataClick: PropTypes.func,
|
||||||
|
onExpressionClick: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let makeZoomButton, makeDataButton
|
let makeZoomButton, makeDataButton, expressionButton;
|
||||||
|
|
||||||
if (this.props.fieldSpec.expression.parameters.includes('zoom')) {
|
if (this.props.fieldSpec.expression.parameters.includes('zoom')) {
|
||||||
|
expressionButton = (
|
||||||
|
<Button
|
||||||
|
className="maputnik-make-zoom-function"
|
||||||
|
onClick={this.props.onExpressionClick}
|
||||||
|
>
|
||||||
|
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
makeZoomButton = <Button
|
makeZoomButton = <Button
|
||||||
className="maputnik-make-zoom-function"
|
className="maputnik-make-zoom-function"
|
||||||
onClick={this.props.onZoomClick}
|
onClick={this.props.onZoomClick}
|
||||||
|
title={"Turn property into a zoom function to enable a map feature to change with map's zoom level."}
|
||||||
>
|
>
|
||||||
<DocLabel
|
<MdFunctions />
|
||||||
label={<MdFunctions />}
|
|
||||||
cursorTargetStyle={{ cursor: 'pointer' }}
|
|
||||||
doc={"Turn property into a zoom function to enable a map feature to change with map's zoom level."}
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
if (this.props.fieldSpec['property-type'] === 'data-driven') {
|
if (this.props.fieldSpec['property-type'] === 'data-driven') {
|
||||||
makeDataButton = <Button
|
makeDataButton = <Button
|
||||||
className="maputnik-make-data-function"
|
className="maputnik-make-data-function"
|
||||||
onClick={this.props.onDataClick}
|
onClick={this.props.onDataClick}
|
||||||
|
title={"Turn property into a data function to enable a map feature to change according to data properties and the map's zoom level."}
|
||||||
>
|
>
|
||||||
<DocLabel
|
<MdInsertChart />
|
||||||
label={<MdInsertChart />}
|
|
||||||
cursorTargetStyle={{ cursor: 'pointer' }}
|
|
||||||
doc={"Turn property into a data function to enable a map feature to change according to data properties and the map's zoom level."}
|
|
||||||
/>
|
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
return <div>{makeDataButton}{makeZoomButton}</div>
|
return <div>
|
||||||
|
{expressionButton}
|
||||||
|
{makeDataButton}
|
||||||
|
{makeZoomButton}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return null
|
return <div>{expressionButton}</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,18 +13,33 @@ export default class SpecProperty extends React.Component {
|
|||||||
onZoomClick: PropTypes.func.isRequired,
|
onZoomClick: PropTypes.func.isRequired,
|
||||||
onDataClick: PropTypes.func.isRequired,
|
onDataClick: PropTypes.func.isRequired,
|
||||||
fieldName: PropTypes.string,
|
fieldName: PropTypes.string,
|
||||||
fieldSpec: PropTypes.object
|
fieldType: PropTypes.string,
|
||||||
|
fieldSpec: PropTypes.object,
|
||||||
|
value: PropTypes.any,
|
||||||
|
errors: PropTypes.object,
|
||||||
|
onExpressionClick: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
errors: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {errors, fieldName, fieldType} = this.props;
|
||||||
|
|
||||||
const functionBtn = <FunctionButtons
|
const functionBtn = <FunctionButtons
|
||||||
fieldSpec={this.props.fieldSpec}
|
fieldSpec={this.props.fieldSpec}
|
||||||
onZoomClick={this.props.onZoomClick}
|
onZoomClick={this.props.onZoomClick}
|
||||||
onDataClick={this.props.onDataClick}
|
onDataClick={this.props.onDataClick}
|
||||||
|
value={this.props.value}
|
||||||
|
onExpressionClick={this.props.onExpressionClick}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
const error = errors[fieldType+"."+fieldName];
|
||||||
|
|
||||||
return <InputBlock
|
return <InputBlock
|
||||||
doc={this.props.fieldSpec.doc}
|
error={error}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
label={labelFromFieldName(this.props.fieldName)}
|
label={labelFromFieldName(this.props.fieldName)}
|
||||||
action={functionBtn}
|
action={functionBtn}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -42,8 +42,11 @@ export default class ZoomProperty extends React.Component {
|
|||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
onDeleteStop: PropTypes.func,
|
onDeleteStop: PropTypes.func,
|
||||||
onAddStop: PropTypes.func,
|
onAddStop: PropTypes.func,
|
||||||
|
onExpressionClick: PropTypes.func,
|
||||||
|
fieldType: PropTypes.string,
|
||||||
fieldName: PropTypes.string,
|
fieldName: PropTypes.string,
|
||||||
fieldSpec: PropTypes.object,
|
fieldSpec: PropTypes.object,
|
||||||
|
errors: PropTypes.object,
|
||||||
value: PropTypes.oneOfType([
|
value: PropTypes.oneOfType([
|
||||||
PropTypes.object,
|
PropTypes.object,
|
||||||
PropTypes.string,
|
PropTypes.string,
|
||||||
@@ -53,6 +56,10 @@ export default class ZoomProperty extends React.Component {
|
|||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
errors: {},
|
||||||
|
}
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
refs: {}
|
refs: {}
|
||||||
}
|
}
|
||||||
@@ -117,15 +124,28 @@ export default class ZoomProperty extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {fieldName, fieldType, errors} = this.props;
|
||||||
|
|
||||||
const zoomFields = this.props.value.stops.map((stop, idx) => {
|
const zoomFields = this.props.value.stops.map((stop, idx) => {
|
||||||
const zoomLevel = stop[0]
|
const zoomLevel = stop[0]
|
||||||
const key = this.state.refs[idx];
|
const key = this.state.refs[idx];
|
||||||
const value = stop[1]
|
const value = stop[1]
|
||||||
const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
|
const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
|
||||||
|
|
||||||
|
const errorKeyStart = `${fieldType}.${fieldName}.stops[${idx}]`;
|
||||||
|
const foundErrors = Object.entries(errors).filter(([key, error]) => {
|
||||||
|
return key.startsWith(errorKeyStart);
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = foundErrors.map(([key, error]) => {
|
||||||
|
return error.message;
|
||||||
|
}).join("");
|
||||||
|
const error = message ? {message} : undefined;
|
||||||
|
|
||||||
return <InputBlock
|
return <InputBlock
|
||||||
|
error={error}
|
||||||
key={key}
|
key={key}
|
||||||
doc={this.props.fieldSpec.doc}
|
fieldSpec={this.props.fieldSpec}
|
||||||
label={labelFromFieldName(this.props.fieldName)}
|
label={labelFromFieldName(this.props.fieldName)}
|
||||||
action={deleteStopBtn}
|
action={deleteStopBtn}
|
||||||
>
|
>
|
||||||
@@ -158,6 +178,12 @@ export default class ZoomProperty extends React.Component {
|
|||||||
>
|
>
|
||||||
Add stop
|
Add stop
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="maputnik-add-stop"
|
||||||
|
onClick={this.props.onExpressionClick.bind(this)}
|
||||||
|
>
|
||||||
|
Convert to expression
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import capitalize from 'lodash.capitalize'
|
import capitalize from 'lodash.capitalize'
|
||||||
|
|
||||||
export default function labelFromFieldName(fieldName) {
|
export default function labelFromFieldName(fieldName) {
|
||||||
let label = fieldName.split('-').slice(1).join(' ')
|
let label;
|
||||||
return capitalize(label)
|
const parts = fieldName.split('-');
|
||||||
|
if (parts.length > 1) {
|
||||||
|
label = fieldName.split('-').slice(1).join(' ');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
label = fieldName;
|
||||||
|
}
|
||||||
|
return capitalize(label);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,84 @@ import React from 'react'
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { combiningFilterOps } from '../../libs/filterops.js'
|
import { combiningFilterOps } from '../../libs/filterops.js'
|
||||||
|
|
||||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
import {latest, validate, migrate} from '@mapbox/mapbox-gl-style-spec'
|
||||||
import DocLabel from '../fields/DocLabel'
|
import DocLabel from '../fields/DocLabel'
|
||||||
import SelectInput from '../inputs/SelectInput'
|
import SelectInput from '../inputs/SelectInput'
|
||||||
|
import InputBlock from '../inputs/InputBlock'
|
||||||
import SingleFilterEditor from './SingleFilterEditor'
|
import SingleFilterEditor from './SingleFilterEditor'
|
||||||
import FilterEditorBlock from './FilterEditorBlock'
|
import FilterEditorBlock from './FilterEditorBlock'
|
||||||
import Button from '../Button'
|
import Button from '../Button'
|
||||||
|
import SpecDoc from '../inputs/SpecDoc'
|
||||||
|
import ExpressionProperty from '../fields/_ExpressionProperty';
|
||||||
|
import {mdiFunctionVariant} from '@mdi/js';
|
||||||
|
|
||||||
|
|
||||||
|
function combiningFilter (props) {
|
||||||
|
let filter = props.filter || ['all'];
|
||||||
|
|
||||||
|
if (!Array.isArray(filter)) {
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
let combiningOp = filter[0];
|
||||||
|
let filters = filter.slice(1);
|
||||||
|
|
||||||
|
if(combiningFilterOps.indexOf(combiningOp) < 0) {
|
||||||
|
combiningOp = 'all';
|
||||||
|
filters = [filter.slice(0)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [combiningOp, ...filters];
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrateFilter (filter) {
|
||||||
|
return migrate(createStyleFromFilter(filter)).layers[0].filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStyleFromFilter (filter) {
|
||||||
|
return {
|
||||||
|
"id": "tmp",
|
||||||
|
"version": 8,
|
||||||
|
"name": "Empty Style",
|
||||||
|
"metadata": {"maputnik:renderer": "mbgljs"},
|
||||||
|
"sources": {
|
||||||
|
"tmp": {
|
||||||
|
"type": "geojson",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sprite": "",
|
||||||
|
"glyphs": "https://orangemug.github.io/font-glyphs/glyphs/{fontstack}/{range}.pbf",
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
id: "tmp",
|
||||||
|
type: "fill",
|
||||||
|
source: "tmp",
|
||||||
|
filter: filter,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is doing way more work than we need it to, however validating a whole
|
||||||
|
* style if the only thing that's exported from mapbox-gl-style-spec at the
|
||||||
|
* moment. Not really an issue though as it take ~0.1ms to calculate.
|
||||||
|
*/
|
||||||
|
function checkIfSimpleFilter (filter) {
|
||||||
|
if (!filter || !combiningFilterOps.includes(filter[0])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Because "none" isn't supported by the next expression syntax we can test
|
||||||
|
// with ["none", ...] because it'll return false if it's a new style
|
||||||
|
// expression.
|
||||||
|
const moddedFilter = ["none", ...filter.slice(1)];
|
||||||
|
const tmpStyle = createStyleFromFilter(moddedFilter)
|
||||||
|
|
||||||
|
const errors = validate(tmpStyle);
|
||||||
|
return (errors.length < 1);
|
||||||
|
}
|
||||||
|
|
||||||
function hasCombiningFilter(filter) {
|
function hasCombiningFilter(filter) {
|
||||||
return combiningFilterOps.indexOf(filter[0]) >= 0
|
return combiningFilterOps.indexOf(filter[0]) >= 0
|
||||||
@@ -26,86 +98,212 @@ export default class CombiningFilterEditor extends React.Component {
|
|||||||
/** Properties of the vector layer and the available fields */
|
/** Properties of the vector layer and the available fields */
|
||||||
properties: PropTypes.object,
|
properties: PropTypes.object,
|
||||||
filter: PropTypes.array,
|
filter: PropTypes.array,
|
||||||
|
errors: PropTypes.object,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert filter to combining filter
|
static defaultProps = {
|
||||||
combiningFilter() {
|
filter: ["all"],
|
||||||
let filter = this.props.filter || ['all']
|
|
||||||
|
|
||||||
let combiningOp = filter[0]
|
|
||||||
let filters = filter.slice(1)
|
|
||||||
|
|
||||||
if(combiningFilterOps.indexOf(combiningOp) < 0) {
|
|
||||||
combiningOp = 'all'
|
|
||||||
filters = [filter.slice(0)]
|
|
||||||
}
|
|
||||||
|
|
||||||
return [combiningOp, ...filters]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super();
|
||||||
|
this.state = {
|
||||||
|
showDoc: false,
|
||||||
|
displaySimpleFilter: checkIfSimpleFilter(combiningFilter(props)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert filter to combining filter
|
||||||
onFilterPartChanged(filterIdx, newPart) {
|
onFilterPartChanged(filterIdx, newPart) {
|
||||||
const newFilter = this.combiningFilter().slice(0)
|
const newFilter = combiningFilter(this.props).slice(0)
|
||||||
newFilter[filterIdx] = newPart
|
newFilter[filterIdx] = newPart
|
||||||
this.props.onChange(newFilter)
|
this.props.onChange(newFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteFilterItem(filterIdx) {
|
deleteFilterItem(filterIdx) {
|
||||||
const newFilter = this.combiningFilter().slice(0)
|
const newFilter = combiningFilter(this.props).slice(0)
|
||||||
console.log('Delete', filterIdx, newFilter)
|
|
||||||
newFilter.splice(filterIdx + 1, 1)
|
newFilter.splice(filterIdx + 1, 1)
|
||||||
this.props.onChange(newFilter)
|
this.props.onChange(newFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
addFilterItem = () => {
|
addFilterItem = () => {
|
||||||
const newFilterItem = this.combiningFilter().slice(0)
|
const newFilterItem = combiningFilter(this.props).slice(0)
|
||||||
newFilterItem.push(['==', 'name', ''])
|
newFilterItem.push(['==', 'name', ''])
|
||||||
this.props.onChange(newFilterItem)
|
this.props.onChange(newFilterItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
onToggleDoc = (val) => {
|
||||||
const filter = this.combiningFilter()
|
this.setState({
|
||||||
let combiningOp = filter[0]
|
showDoc: val
|
||||||
let filters = filter.slice(1)
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const editorBlocks = filters.map((f, idx) => {
|
makeFilter = () => {
|
||||||
return <FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
|
this.setState({
|
||||||
<SingleFilterEditor
|
displaySimpleFilter: true,
|
||||||
properties={this.props.properties}
|
|
||||||
filter={f}
|
|
||||||
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
|
|
||||||
/>
|
|
||||||
</FilterEditorBlock>
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: Implement support for nested filter
|
makeExpression = () => {
|
||||||
if(hasNestedCombiningFilter(filter)) {
|
let filter = combiningFilter(this.props);
|
||||||
return <div className="maputnik-filter-editor-unsupported">
|
this.props.onChange(migrateFilter(filter));
|
||||||
Nested filters are not supported.
|
this.setState({
|
||||||
</div>
|
displaySimpleFilter: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromProps (props, currentState) {
|
||||||
|
const {filter} = props;
|
||||||
|
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
|
||||||
|
|
||||||
|
// Upgrade but never downgrade
|
||||||
|
if (!displaySimpleFilter && currentState.displaySimpleFilter === true) {
|
||||||
|
return {
|
||||||
|
displaySimpleFilter: false,
|
||||||
|
valueIsSimpleFilter: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
else if (displaySimpleFilter && currentState.displaySimpleFilter === false) {
|
||||||
|
return {
|
||||||
|
valueIsSimpleFilter: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return {
|
||||||
|
valueIsSimpleFilter: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return <div className="maputnik-filter-editor">
|
render() {
|
||||||
<div className="maputnik-filter-editor-compound-select" data-wd-key="layer-filter">
|
const {errors} = this.props;
|
||||||
<DocLabel
|
const {displaySimpleFilter} = this.state;
|
||||||
label={"Compound Filter"}
|
const fieldSpec={
|
||||||
doc={latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."}
|
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
|
||||||
/>
|
};
|
||||||
<SelectInput
|
const defaultFilter = ["all"];
|
||||||
value={combiningOp}
|
|
||||||
onChange={this.onFilterPartChanged.bind(this, 0)}
|
const isNestedCombiningFilter = displaySimpleFilter && hasNestedCombiningFilter(combiningFilter(this.props));
|
||||||
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
|
|
||||||
/>
|
if (isNestedCombiningFilter) {
|
||||||
</div>
|
return <div className="maputnik-filter-editor-unsupported">
|
||||||
{editorBlocks}
|
<p>
|
||||||
<div className="maputnik-filter-editor-add-wrapper">
|
Nested filters are not supported.
|
||||||
|
</p>
|
||||||
<Button
|
<Button
|
||||||
data-wd-key="layer-filter-button"
|
onClick={this.makeExpression}
|
||||||
className="maputnik-add-filter"
|
>
|
||||||
onClick={this.addFilterItem}>
|
<svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||||
Add filter
|
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||||
|
</svg>
|
||||||
|
Upgrade to expression
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
|
else if (displaySimpleFilter) {
|
||||||
|
const filter = combiningFilter(this.props);
|
||||||
|
let combiningOp = filter[0];
|
||||||
|
let filters = filter.slice(1)
|
||||||
|
|
||||||
|
const actions = (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
onClick={this.makeExpression}
|
||||||
|
className="maputnik-make-zoom-function"
|
||||||
|
>
|
||||||
|
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const editorBlocks = filters.map((f, idx) => {
|
||||||
|
const error = errors[`filter[${idx+1}]`];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FilterEditorBlock key={idx} onDelete={this.deleteFilterItem.bind(this, idx)}>
|
||||||
|
<SingleFilterEditor
|
||||||
|
properties={this.props.properties}
|
||||||
|
filter={f}
|
||||||
|
onChange={this.onFilterPartChanged.bind(this, idx + 1)}
|
||||||
|
/>
|
||||||
|
</FilterEditorBlock>
|
||||||
|
{error &&
|
||||||
|
<div className="maputnik-inline-error">{error.message}</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<InputBlock
|
||||||
|
key="top"
|
||||||
|
fieldSpec={fieldSpec}
|
||||||
|
label={"Filter"}
|
||||||
|
action={actions}
|
||||||
|
>
|
||||||
|
<SelectInput
|
||||||
|
value={combiningOp}
|
||||||
|
onChange={this.onFilterPartChanged.bind(this, 0)}
|
||||||
|
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
{editorBlocks}
|
||||||
|
<div
|
||||||
|
key="buttons"
|
||||||
|
className="maputnik-filter-editor-add-wrapper"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
data-wd-key="layer-filter-button"
|
||||||
|
className="maputnik-add-filter"
|
||||||
|
onClick={this.addFilterItem}>
|
||||||
|
Add filter
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="doc"
|
||||||
|
className="maputnik-doc-inline"
|
||||||
|
style={{display: this.state.showDoc ? '' : 'none'}}
|
||||||
|
>
|
||||||
|
<SpecDoc fieldSpec={fieldSpec} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let {filter} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ExpressionProperty
|
||||||
|
onDelete={() => {
|
||||||
|
this.setState({displaySimpleFilter: true});
|
||||||
|
this.props.onChange(defaultFilter);
|
||||||
|
}}
|
||||||
|
fieldName="filter"
|
||||||
|
fieldSpec={fieldSpec}
|
||||||
|
value={filter}
|
||||||
|
errors={errors}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
/>
|
||||||
|
{this.state.valueIsSimpleFilter &&
|
||||||
|
<div className="maputnik-expr-infobox">
|
||||||
|
You've entered a old style filter,{' '}
|
||||||
|
<button
|
||||||
|
onClick={this.makeFilter}
|
||||||
|
className="maputnik-expr-infobox__button"
|
||||||
|
>
|
||||||
|
switch to filter editor
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import FillIcon from './FillIcon.jsx'
|
|||||||
import SymbolIcon from './SymbolIcon.jsx'
|
import SymbolIcon from './SymbolIcon.jsx'
|
||||||
import BackgroundIcon from './BackgroundIcon.jsx'
|
import BackgroundIcon from './BackgroundIcon.jsx'
|
||||||
import CircleIcon from './CircleIcon.jsx'
|
import CircleIcon from './CircleIcon.jsx'
|
||||||
|
import MissingIcon from './MissingIcon.jsx'
|
||||||
|
|
||||||
class LayerIcon extends React.Component {
|
class LayerIcon extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@@ -25,6 +26,7 @@ class LayerIcon extends React.Component {
|
|||||||
case 'line': return <LineIcon {...iconProps} />
|
case 'line': return <LineIcon {...iconProps} />
|
||||||
case 'symbol': return <SymbolIcon {...iconProps} />
|
case 'symbol': return <SymbolIcon {...iconProps} />
|
||||||
case 'circle': return <CircleIcon {...iconProps} />
|
case 'circle': return <CircleIcon {...iconProps} />
|
||||||
|
default: return <MissingIcon {...iconProps} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/components/icons/MissingIcon.jsx
Normal file
11
src/components/icons/MissingIcon.jsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {MdPriorityHigh} from 'react-icons/md'
|
||||||
|
|
||||||
|
|
||||||
|
export default class MissingIcon extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<MdPriorityHigh {...this.props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,29 +12,89 @@ class ArrayInput extends React.Component {
|
|||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
changeValue(idx, newValue) {
|
static defaultProps = {
|
||||||
console.log(idx, newValue)
|
value: [],
|
||||||
const values = this.values.slice(0)
|
default: [],
|
||||||
values[idx] = newValue
|
|
||||||
this.props.onChange(values)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get values() {
|
constructor (props) {
|
||||||
return this.props.value || this.props.default || []
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
value: this.props.value.slice(0),
|
||||||
|
// This is so we can compare changes in getDerivedStateFromProps
|
||||||
|
initialPropsValue: this.props.value.slice(0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromProps(props, state) {
|
||||||
|
const value = [];
|
||||||
|
const initialPropsValue = state.initialPropsValue.slice(0);
|
||||||
|
|
||||||
|
Array(props.length).fill(null).map((_, i) => {
|
||||||
|
if (props.value[i] === state.initialPropsValue[i]) {
|
||||||
|
value[i] = state.value[i];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
value[i] = state.value[i];
|
||||||
|
initialPropsValue[i] = state.value[i];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
initialPropsValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
isComplete (value) {
|
||||||
|
return Array(this.props.length).fill(null).every((_, i) => {
|
||||||
|
const val = value[i]
|
||||||
|
return !(val === undefined || val === "");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
changeValue(idx, newValue) {
|
||||||
|
const value = this.state.value.slice(0);
|
||||||
|
value[idx] = newValue;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
value,
|
||||||
|
}, () => {
|
||||||
|
if (this.isComplete(value)) {
|
||||||
|
this.props.onChange(value);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Unset until complete
|
||||||
|
this.props.onChange(undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const inputs = this.values.map((v, i) => {
|
const {value} = this.state;
|
||||||
|
|
||||||
|
const containsValues = (
|
||||||
|
value.length > 0 &&
|
||||||
|
!value.every(val => {
|
||||||
|
return (val === "" || val === undefined)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const inputs = Array(this.props.length).fill(null).map((_, i) => {
|
||||||
if(this.props.type === 'number') {
|
if(this.props.type === 'number') {
|
||||||
return <NumberInput
|
return <NumberInput
|
||||||
key={i}
|
key={i}
|
||||||
value={v}
|
default={containsValues ? undefined : this.props.default[i]}
|
||||||
|
value={value[i]}
|
||||||
|
required={containsValues ? true : false}
|
||||||
onChange={this.changeValue.bind(this, i)}
|
onChange={this.changeValue.bind(this, i)}
|
||||||
/>
|
/>
|
||||||
} else {
|
} else {
|
||||||
return <StringInput
|
return <StringInput
|
||||||
key={i}
|
key={i}
|
||||||
value={v}
|
default={containsValues ? undefined : this.props.default[i]}
|
||||||
|
value={value[i]}
|
||||||
|
required={containsValues ? true : false}
|
||||||
onChange={this.changeValue.bind(this, i)}
|
onChange={this.changeValue.bind(this, i)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,15 @@ import PropTypes from 'prop-types'
|
|||||||
|
|
||||||
class CheckboxInput extends React.Component {
|
class CheckboxInput extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.bool.isRequired,
|
value: PropTypes.bool,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
value: false,
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <label className="maputnik-checkbox-wrapper">
|
return <label className="maputnik-checkbox-wrapper">
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import NumberInput from './NumberInput'
|
|||||||
import Button from '../Button'
|
import Button from '../Button'
|
||||||
import {MdDelete} from 'react-icons/md'
|
import {MdDelete} from 'react-icons/md'
|
||||||
import DocLabel from '../fields/DocLabel'
|
import DocLabel from '../fields/DocLabel'
|
||||||
|
import EnumInput from '../inputs/SelectInput'
|
||||||
|
import capitalize from 'lodash.capitalize'
|
||||||
|
|
||||||
|
|
||||||
class DynamicArrayInput extends React.Component {
|
class DynamicArrayInput extends React.Component {
|
||||||
@@ -14,6 +16,7 @@ class DynamicArrayInput extends React.Component {
|
|||||||
default: PropTypes.array,
|
default: PropTypes.array,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
|
fieldSpec: PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
changeValue(idx, newValue) {
|
changeValue(idx, newValue) {
|
||||||
@@ -31,6 +34,11 @@ class DynamicArrayInput extends React.Component {
|
|||||||
const values = this.values.slice(0)
|
const values = this.values.slice(0)
|
||||||
if (this.props.type === 'number') {
|
if (this.props.type === 'number') {
|
||||||
values.push(0)
|
values.push(0)
|
||||||
|
}
|
||||||
|
else if (this.props.type === 'enum') {
|
||||||
|
const {fieldSpec} = this.props;
|
||||||
|
const defaultValue = Object.keys(fieldSpec.values)[0];
|
||||||
|
values.push(defaultValue);
|
||||||
} else {
|
} else {
|
||||||
values.push("")
|
values.push("")
|
||||||
}
|
}
|
||||||
@@ -48,15 +56,28 @@ class DynamicArrayInput extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
const inputs = this.values.map((v, i) => {
|
const inputs = this.values.map((v, i) => {
|
||||||
const deleteValueBtn= <DeleteValueButton onClick={this.deleteValue.bind(this, i)} />
|
const deleteValueBtn= <DeleteValueButton onClick={this.deleteValue.bind(this, i)} />
|
||||||
const input = this.props.type === 'number'
|
let input;
|
||||||
? <NumberInput
|
if (this.props.type === 'number') {
|
||||||
|
input = <NumberInput
|
||||||
value={v}
|
value={v}
|
||||||
onChange={this.changeValue.bind(this, i)}
|
onChange={this.changeValue.bind(this, i)}
|
||||||
/>
|
/>
|
||||||
: <StringInput
|
}
|
||||||
|
else if (this.props.type === 'enum') {
|
||||||
|
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)]);
|
||||||
|
|
||||||
|
input = <EnumInput
|
||||||
|
options={options}
|
||||||
value={v}
|
value={v}
|
||||||
onChange={this.changeValue.bind(this, i)}
|
onChange={this.changeValue.bind(this, i)}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
input = <StringInput
|
||||||
|
value={v}
|
||||||
|
onChange={this.changeValue.bind(this, i)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
return <div
|
return <div
|
||||||
style={this.props.style}
|
style={this.props.style}
|
||||||
|
|||||||
45
src/components/inputs/EnumInput.jsx
Normal file
45
src/components/inputs/EnumInput.jsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import SelectInput from '../inputs/SelectInput'
|
||||||
|
import MultiButtonInput from '../inputs/MultiButtonInput'
|
||||||
|
|
||||||
|
|
||||||
|
function optionsLabelLength(options) {
|
||||||
|
let sum = 0;
|
||||||
|
options.forEach(([_, label]) => {
|
||||||
|
sum += label.length
|
||||||
|
})
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class EnumInput extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
"data-wd-key": PropTypes.string,
|
||||||
|
value: PropTypes.string,
|
||||||
|
style: PropTypes.object,
|
||||||
|
default: PropTypes.string,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
options: PropTypes.array,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {options, value, onChange} = this.props;
|
||||||
|
|
||||||
|
if(options.length <= 3 && optionsLabelLength(options) <= 20) {
|
||||||
|
return <MultiButtonInput
|
||||||
|
options={options}
|
||||||
|
value={value || this.props.default}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
} else {
|
||||||
|
return <SelectInput
|
||||||
|
options={options}
|
||||||
|
value={value || this.props.default}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EnumInput
|
||||||
@@ -2,6 +2,8 @@ import React from 'react'
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import DocLabel from '../fields/DocLabel'
|
import DocLabel from '../fields/DocLabel'
|
||||||
|
import SpecDoc from './SpecDoc'
|
||||||
|
|
||||||
|
|
||||||
/** Wrap a component with a label */
|
/** Wrap a component with a label */
|
||||||
class InputBlock extends React.Component {
|
class InputBlock extends React.Component {
|
||||||
@@ -11,11 +13,20 @@ class InputBlock extends React.Component {
|
|||||||
PropTypes.string,
|
PropTypes.string,
|
||||||
PropTypes.element,
|
PropTypes.element,
|
||||||
]).isRequired,
|
]).isRequired,
|
||||||
doc: PropTypes.string,
|
|
||||||
action: PropTypes.element,
|
action: PropTypes.element,
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
|
fieldSpec: PropTypes.object,
|
||||||
|
wideMode: PropTypes.bool,
|
||||||
|
error: PropTypes.array,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
showDoc: false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange(e) {
|
onChange(e) {
|
||||||
@@ -23,23 +34,33 @@ class InputBlock extends React.Component {
|
|||||||
return this.props.onChange(value === "" ? undefined : value)
|
return this.props.onChange(value === "" ? undefined : value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onToggleDoc = (val) => {
|
||||||
|
this.setState({
|
||||||
|
showDoc: val
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const errors = [].concat(this.props.error || []);
|
||||||
|
|
||||||
return <div style={this.props.style}
|
return <div style={this.props.style}
|
||||||
data-wd-key={this.props["data-wd-key"]}
|
data-wd-key={this.props["data-wd-key"]}
|
||||||
className={classnames({
|
className={classnames({
|
||||||
"maputnik-input-block": true,
|
"maputnik-input-block": true,
|
||||||
|
"maputnik-input-block--wide": this.props.wideMode,
|
||||||
"maputnik-action-block": this.props.action
|
"maputnik-action-block": this.props.action
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{this.props.doc &&
|
{this.props.fieldSpec &&
|
||||||
<div className="maputnik-input-block-label">
|
<div className="maputnik-input-block-label">
|
||||||
<DocLabel
|
<DocLabel
|
||||||
label={this.props.label}
|
label={this.props.label}
|
||||||
doc={this.props.doc}
|
onToggleDoc={this.onToggleDoc}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{!this.props.doc &&
|
{!this.props.fieldSpec &&
|
||||||
<label className="maputnik-input-block-label">
|
<label className="maputnik-input-block-label">
|
||||||
{this.props.label}
|
{this.props.label}
|
||||||
</label>
|
</label>
|
||||||
@@ -52,6 +73,21 @@ class InputBlock extends React.Component {
|
|||||||
<div className="maputnik-input-block-content">
|
<div className="maputnik-input-block-content">
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
|
{errors.length > 0 &&
|
||||||
|
<div className="maputnik-inline-error">
|
||||||
|
{[].concat(this.props.error).map((error, idx) => {
|
||||||
|
return <div key={idx}>{error.message}</div>
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{this.props.fieldSpec &&
|
||||||
|
<div
|
||||||
|
className="maputnik-doc-inline"
|
||||||
|
style={{display: this.state.showDoc ? '' : 'none'}}
|
||||||
|
>
|
||||||
|
<SpecDoc fieldSpec={this.props.fieldSpec} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
let IDX = 0;
|
||||||
|
|
||||||
class NumberInput extends React.Component {
|
class NumberInput extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.number,
|
value: PropTypes.number,
|
||||||
@@ -8,36 +10,59 @@ class NumberInput extends React.Component {
|
|||||||
min: PropTypes.number,
|
min: PropTypes.number,
|
||||||
max: PropTypes.number,
|
max: PropTypes.number,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
|
allowRange: PropTypes.bool,
|
||||||
|
rangeStep: PropTypes.number,
|
||||||
|
wdKey: PropTypes.string,
|
||||||
|
required: PropTypes.bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
rangeStep: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
|
uuid: IDX++,
|
||||||
editing: false,
|
editing: false,
|
||||||
value: props.value,
|
value: props.value,
|
||||||
|
dirtyValue: props.value,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static getDerivedStateFromProps(props, state) {
|
static getDerivedStateFromProps(props, state) {
|
||||||
if (!state.editing) {
|
if (!state.editing) {
|
||||||
return {
|
return {
|
||||||
value: props.value
|
value: props.value,
|
||||||
|
dirtyValue: props.value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
changeValue(newValue) {
|
changeValue(newValue) {
|
||||||
this.setState({editing: true});
|
const value = (newValue === "" || newValue === undefined) ?
|
||||||
const value = parseFloat(newValue)
|
undefined :
|
||||||
|
parseFloat(newValue);
|
||||||
|
|
||||||
const hasChanged = this.state.value !== value
|
const hasChanged = this.props.value !== value;
|
||||||
if(this.isValid(value) && hasChanged) {
|
if(this.isValid(value) && hasChanged) {
|
||||||
this.props.onChange(value)
|
this.props.onChange(value)
|
||||||
|
this.setState({
|
||||||
|
dirtyValue: newValue,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.setState({ value: newValue })
|
|
||||||
|
this.setState({
|
||||||
|
value: newValue,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
isValid(v) {
|
isValid(v) {
|
||||||
|
if (v === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const value = parseFloat(v)
|
const value = parseFloat(v)
|
||||||
if(isNaN(value)) {
|
if(isNaN(value)) {
|
||||||
return false
|
return false
|
||||||
@@ -58,7 +83,7 @@ class NumberInput extends React.Component {
|
|||||||
this.setState({editing: false});
|
this.setState({editing: false});
|
||||||
// Reset explicitly to default value if value has been cleared
|
// Reset explicitly to default value if value has been cleared
|
||||||
if(this.state.value === "") {
|
if(this.state.value === "") {
|
||||||
return this.changeValue(this.props.default)
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If set value is invalid fall back to the last valid value from props or at last resort the default value
|
// If set value is invalid fall back to the last valid value from props or at last resort the default value
|
||||||
@@ -66,20 +91,117 @@ class NumberInput extends React.Component {
|
|||||||
if(this.isValid(this.props.value)) {
|
if(this.isValid(this.props.value)) {
|
||||||
this.changeValue(this.props.value)
|
this.changeValue(this.props.value)
|
||||||
} else {
|
} else {
|
||||||
this.changeValue(this.props.default)
|
this.changeValue(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onChangeRange = (e) => {
|
||||||
|
let value = parseFloat(e.target.value, 10);
|
||||||
|
const step = this.props.rangeStep;
|
||||||
|
let dirtyValue = value;
|
||||||
|
|
||||||
|
if(step) {
|
||||||
|
// Can't do this with the <input/> range step attribute else we won't be able to set a high precision value via the text input.
|
||||||
|
const snap = value % step;
|
||||||
|
|
||||||
|
// Round up/down to step
|
||||||
|
if (this._keyboardEvent) {
|
||||||
|
// If it's keyboard event we might get a low positive/negative value,
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
value = this.state.value + step
|
||||||
|
}
|
||||||
|
dirtyValue = value;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (snap < step/2) {
|
||||||
|
value = value - snap;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
value = value + (step - snap);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._keyboardEvent = false;
|
||||||
|
|
||||||
|
// Clamp between min/max
|
||||||
|
value = Math.max(this.props.min, Math.min(this.props.max, value));
|
||||||
|
|
||||||
|
this.setState({value, dirtyValue});
|
||||||
|
this.props.onChange(value);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <input
|
if(
|
||||||
spellCheck="false"
|
this.props.hasOwnProperty("min") && this.props.hasOwnProperty("max") &&
|
||||||
className="maputnik-number"
|
this.props.min !== undefined && this.props.max !== undefined &&
|
||||||
placeholder={this.props.default}
|
this.props.allowRange
|
||||||
value={this.state.value}
|
) {
|
||||||
onChange={e => this.changeValue(e.target.value)}
|
const dirtyValue = this.state.dirtyValue === undefined ? this.props.default : this.state.dirtyValue
|
||||||
onBlur={this.resetValue}
|
const value = this.state.value === undefined ? "" : this.state.value;
|
||||||
/>
|
|
||||||
|
return <div className="maputnik-number-container">
|
||||||
|
<input
|
||||||
|
className="maputnik-number-range"
|
||||||
|
key="range"
|
||||||
|
type="range"
|
||||||
|
max={this.props.max}
|
||||||
|
min={this.props.min}
|
||||||
|
step="any"
|
||||||
|
spellCheck="false"
|
||||||
|
value={dirtyValue}
|
||||||
|
aria-hidden="true"
|
||||||
|
onChange={this.onChangeRange}
|
||||||
|
onKeyDown={() => {
|
||||||
|
this._keyboardEvent = true;
|
||||||
|
}}
|
||||||
|
onPointerDown={() => {
|
||||||
|
this.setState({editing: true});
|
||||||
|
}}
|
||||||
|
onPointerUp={() => {
|
||||||
|
// Safari doesn't get onBlur event
|
||||||
|
this.setState({editing: false});
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
this.setState({editing: false});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
key="text"
|
||||||
|
type="text"
|
||||||
|
spellCheck="false"
|
||||||
|
className="maputnik-number"
|
||||||
|
placeholder={this.props.default}
|
||||||
|
value={value}
|
||||||
|
onChange={e => {
|
||||||
|
if (!this.state.editing) {
|
||||||
|
this.changeValue(e.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={this.resetValue}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const value = this.state.value === undefined ? "" : this.state.value;
|
||||||
|
|
||||||
|
return <input
|
||||||
|
spellCheck="false"
|
||||||
|
className="maputnik-number"
|
||||||
|
placeholder={this.props.default}
|
||||||
|
value={value}
|
||||||
|
onChange={e => this.changeValue(e.target.value)}
|
||||||
|
onBlur={this.resetValue}
|
||||||
|
required={this.props.required}
|
||||||
|
/>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class SelectInput extends React.Component {
|
|||||||
options: PropTypes.array.isRequired,
|
options: PropTypes.array.isRequired,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
title: PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ class SelectInput extends React.Component {
|
|||||||
className="maputnik-select"
|
className="maputnik-select"
|
||||||
data-wd-key={this.props["data-wd-key"]}
|
data-wd-key={this.props["data-wd-key"]}
|
||||||
style={this.props.style}
|
style={this.props.style}
|
||||||
|
title={this.props.title}
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onChange={e => this.props.onChange(e.target.value)}
|
onChange={e => this.props.onChange(e.target.value)}
|
||||||
>
|
>
|
||||||
|
|||||||
83
src/components/inputs/SpecDoc.js
Normal file
83
src/components/inputs/SpecDoc.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
export default class SpecDoc extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
fieldSpec: PropTypes.object.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
!Array.isArray(values)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{doc &&
|
||||||
|
<div className="SpecDoc">
|
||||||
|
<div className="SpecDoc__doc">{doc}</div>
|
||||||
|
{renderValues &&
|
||||||
|
<ul className="SpecDoc__values">
|
||||||
|
{Object.entries(values).map(([key, value]) => {
|
||||||
|
return (
|
||||||
|
<li key={key}>
|
||||||
|
<code>{JSON.stringify(key)}</code>
|
||||||
|
<div>{value.doc}</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{sdkSupport &&
|
||||||
|
<div className="SpecDoc__sdk-support">
|
||||||
|
<table className="SpecDoc__sdk-support__table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
{Object.values(headers).map(header => {
|
||||||
|
return <th key={header}>{header}</th>;
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Object.entries(sdkSupport).map(([key, supportObj]) => {
|
||||||
|
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>;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return <td key={k}>no</td>;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,15 @@ class StringInput extends React.Component {
|
|||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
default: PropTypes.string,
|
default: PropTypes.string,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
|
onInput: PropTypes.func,
|
||||||
multi: PropTypes.bool,
|
multi: PropTypes.bool,
|
||||||
|
required: PropTypes.bool,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
spellCheck: PropTypes.bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
onInput: () => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -45,25 +53,33 @@ class StringInput extends React.Component {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!!this.props.disabled) {
|
||||||
|
classes.push("maputnik-string--disabled");
|
||||||
|
}
|
||||||
|
|
||||||
return React.createElement(tag, {
|
return React.createElement(tag, {
|
||||||
"data-wd-key": this.props["data-wd-key"],
|
"data-wd-key": this.props["data-wd-key"],
|
||||||
spellCheck: !(tag === "input"),
|
spellCheck: this.props.hasOwnProperty("spellCheck") ? this.props.spellCheck : !(tag === "input"),
|
||||||
|
disabled: this.props.disabled,
|
||||||
className: classes.join(" "),
|
className: classes.join(" "),
|
||||||
style: this.props.style,
|
style: this.props.style,
|
||||||
value: this.state.value,
|
value: this.state.value === undefined ? "" : this.state.value,
|
||||||
placeholder: this.props.default,
|
placeholder: this.props.default,
|
||||||
onChange: e => {
|
onChange: e => {
|
||||||
this.setState({
|
this.setState({
|
||||||
editing: true,
|
editing: true,
|
||||||
value: e.target.value
|
value: e.target.value
|
||||||
})
|
}, () => {
|
||||||
|
this.props.onInput(this.state.value);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onBlur: () => {
|
onBlur: () => {
|
||||||
if(this.state.value!==this.props.value) {
|
if(this.state.value!==this.props.value) {
|
||||||
this.setState({editing: false});
|
this.setState({editing: false});
|
||||||
this.props.onChange(this.state.value);
|
this.props.onChange(this.state.value);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
required: this.props.required,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
77
src/components/inputs/UrlInput.jsx
Normal file
77
src/components/inputs/UrlInput.jsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import StringInput from './StringInput'
|
||||||
|
import SmallError from '../util/SmallError'
|
||||||
|
|
||||||
|
|
||||||
|
function validate (url) {
|
||||||
|
let error;
|
||||||
|
const getProtocol = (url) => {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
return urlObj.protocol;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const protocol = getProtocol(url);
|
||||||
|
if (
|
||||||
|
protocol &&
|
||||||
|
protocol === "http:" &&
|
||||||
|
window.location.protocol === "https:"
|
||||||
|
) {
|
||||||
|
error = (
|
||||||
|
<SmallError>
|
||||||
|
CORS policy won't allow fetching resources served over http from https, use a <code>https://</code> domain
|
||||||
|
</SmallError>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UrlInput extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
"data-wd-key": PropTypes.string,
|
||||||
|
value: PropTypes.string,
|
||||||
|
style: PropTypes.object,
|
||||||
|
default: PropTypes.string,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
onInput: PropTypes.func,
|
||||||
|
multi: PropTypes.bool,
|
||||||
|
required: PropTypes.bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
onInput: () => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
error: validate(props.value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onInput = (url) => {
|
||||||
|
this.setState({
|
||||||
|
error: validate(url)
|
||||||
|
});
|
||||||
|
this.props.onInput(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<StringInput
|
||||||
|
{...this.props}
|
||||||
|
onInput={this.onInput}
|
||||||
|
/>
|
||||||
|
{this.state.error}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UrlInput
|
||||||
@@ -11,9 +11,13 @@ class MetadataBlock extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const fieldSpec = {
|
||||||
|
doc: "Comments for the current layer. This is non-standard and not in the spec."
|
||||||
|
};
|
||||||
|
|
||||||
return <InputBlock
|
return <InputBlock
|
||||||
label={"Comments"}
|
label={"Comments"}
|
||||||
doc={"Comments for the current layer. This is non-standard and not in the spec."}
|
fieldSpec={fieldSpec}
|
||||||
data-wd-key="layer-comment"
|
data-wd-key="layer-comment"
|
||||||
>
|
>
|
||||||
<StringInput
|
<StringInput
|
||||||
|
|||||||
@@ -1,79 +1,159 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import {Controlled as CodeMirror} from 'react-codemirror2'
|
|
||||||
import InputBlock from '../inputs/InputBlock'
|
import InputBlock from '../inputs/InputBlock'
|
||||||
import StringInput from '../inputs/StringInput'
|
import StringInput from '../inputs/StringInput'
|
||||||
|
import CodeMirror from 'codemirror';
|
||||||
|
|
||||||
import 'codemirror/mode/javascript/javascript'
|
import 'codemirror/mode/javascript/javascript'
|
||||||
import 'codemirror/addon/lint/lint'
|
import 'codemirror/addon/lint/lint'
|
||||||
|
import 'codemirror/addon/edit/matchbrackets'
|
||||||
import 'codemirror/lib/codemirror.css'
|
import 'codemirror/lib/codemirror.css'
|
||||||
import 'codemirror/addon/lint/lint.css'
|
import 'codemirror/addon/lint/lint.css'
|
||||||
import '../../codemirror-maputnik.css'
|
|
||||||
import jsonlint from 'jsonlint'
|
import jsonlint from 'jsonlint'
|
||||||
|
import stringifyPretty from 'json-stringify-pretty-compact'
|
||||||
// This is mainly because of this issue <https://github.com/zaach/jsonlint/issues/57> also the API has changed, see comment in file
|
import '../util/codemirror-mgl';
|
||||||
import '../../vendor/codemirror/addon/lint/json-lint'
|
|
||||||
|
|
||||||
|
|
||||||
class JSONEditor extends React.Component {
|
class JSONEditor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
layer: PropTypes.object.isRequired,
|
layer: PropTypes.any.isRequired,
|
||||||
|
maxHeight: PropTypes.number,
|
||||||
onChange: PropTypes.func,
|
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,
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
lineNumbers: true,
|
||||||
|
lineWrapping: false,
|
||||||
|
gutters: ["CodeMirror-lint-markers"],
|
||||||
|
getValue: (data) => {
|
||||||
|
return stringifyPretty(data, {indent: 2, maxLength: 40});
|
||||||
|
},
|
||||||
|
onFocus: () => {},
|
||||||
|
onBlur: () => {},
|
||||||
|
onJSONInvalid: () => {},
|
||||||
|
onJSONValid: () => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
code: JSON.stringify(props.layer, null, 2)
|
isEditing: false,
|
||||||
}
|
prevValue: this.props.getValue(this.props.layer),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidMount () {
|
||||||
if (prevProps.layer !== this.props.layer) {
|
this._doc = CodeMirror(this._el, {
|
||||||
this.setState({
|
value: this.props.getValue(this.props.layer),
|
||||||
code: JSON.stringify(this.props.layer, null, 2)
|
mode: this.props.mode || {
|
||||||
})
|
name: "mgl",
|
||||||
}
|
},
|
||||||
}
|
lineWrapping: this.props.lineWrapping,
|
||||||
|
|
||||||
onCodeUpdate(newCode) {
|
|
||||||
try {
|
|
||||||
const parsedLayer = JSON.parse(newCode)
|
|
||||||
this.props.onChange(parsedLayer)
|
|
||||||
} catch(err) {
|
|
||||||
console.warn(err)
|
|
||||||
} finally {
|
|
||||||
this.setState({
|
|
||||||
code: newCode
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetValue() {
|
|
||||||
console.log('reset')
|
|
||||||
this.setState({
|
|
||||||
code: JSON.stringify(this.props.layer, null, 2)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const codeMirrorOptions = {
|
|
||||||
mode: {name: "javascript", json: true},
|
|
||||||
tabSize: 2,
|
tabSize: 2,
|
||||||
theme: 'maputnik',
|
theme: 'maputnik',
|
||||||
viewportMargin: Infinity,
|
viewportMargin: Infinity,
|
||||||
lineNumbers: true,
|
lineNumbers: this.props.lineNumbers,
|
||||||
lint: true,
|
lint: this.props.lint || {
|
||||||
gutters: ["CodeMirror-lint-markers"],
|
context: "layer"
|
||||||
|
},
|
||||||
|
matchBrackets: true,
|
||||||
|
gutters: this.props.gutters,
|
||||||
scrollbarStyle: "null",
|
scrollbarStyle: "null",
|
||||||
|
});
|
||||||
|
|
||||||
|
this._doc.on('change', this.onChange);
|
||||||
|
this._doc.on('focus', this.onFocus);
|
||||||
|
this._doc.on('blur', this.onBlur);
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocus = () => {
|
||||||
|
this.props.onFocus();
|
||||||
|
this.setState({
|
||||||
|
isEditing: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlur = () => {
|
||||||
|
this.props.onBlur();
|
||||||
|
this.setState({
|
||||||
|
isEditing: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnMount () {
|
||||||
|
this._doc.off('change', this.onChange);
|
||||||
|
this._doc.off('focus', this.onFocus);
|
||||||
|
this._doc.off('blur', this.onBlur);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (!this.state.isEditing && prevProps.layer !== this.props.layer) {
|
||||||
|
this._cancelNextChange = true;
|
||||||
|
this._doc.setValue(
|
||||||
|
this.props.getValue(this.props.layer),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange = (e) => {
|
||||||
|
if (this._cancelNextChange) {
|
||||||
|
this._cancelNextChange = false;
|
||||||
|
this.setState({
|
||||||
|
prevValue: this._doc.getValue(),
|
||||||
|
})
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newCode = this._doc.getValue();
|
||||||
|
|
||||||
|
if (this.state.prevValue !== newCode) {
|
||||||
|
let parsedLayer, err;
|
||||||
|
try {
|
||||||
|
parsedLayer = JSON.parse(newCode);
|
||||||
|
} catch(_err) {
|
||||||
|
err = _err;
|
||||||
|
console.warn(_err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
this.props.onJSONInvalid();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.props.onChange(parsedLayer)
|
||||||
|
this.props.onJSONValid();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <CodeMirror
|
this.setState({
|
||||||
value={this.state.code}
|
prevValue: newCode,
|
||||||
onBeforeChange={(editor, data, value) => this.onCodeUpdate(value)}
|
});
|
||||||
onFocusChange={focused => focused ? true : this.resetValue()}
|
}
|
||||||
options={codeMirrorOptions}
|
|
||||||
|
render() {
|
||||||
|
const style = {};
|
||||||
|
if (this.props.maxHeight) {
|
||||||
|
style.maxHeight = this.props.maxHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div
|
||||||
|
className={classnames("codemirror-container", this.props.className)}
|
||||||
|
ref={(el) => this._el = el}
|
||||||
|
style={style}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import MaxZoomBlock from './MaxZoomBlock'
|
|||||||
import CommentBlock from './CommentBlock'
|
import CommentBlock from './CommentBlock'
|
||||||
import LayerSourceBlock from './LayerSourceBlock'
|
import LayerSourceBlock from './LayerSourceBlock'
|
||||||
import LayerSourceLayerBlock from './LayerSourceLayerBlock'
|
import LayerSourceLayerBlock from './LayerSourceLayerBlock'
|
||||||
|
import {Accordion} from 'react-accessible-accordion';
|
||||||
|
|
||||||
import {MdMoreVert} from 'react-icons/md'
|
import {MdMoreVert} from 'react-icons/md'
|
||||||
|
|
||||||
@@ -20,6 +21,10 @@ import { changeType, changeProperty } from '../../libs/layer'
|
|||||||
import layout from '../../config/layout.json'
|
import layout from '../../config/layout.json'
|
||||||
|
|
||||||
|
|
||||||
|
function getLayoutForType (type) {
|
||||||
|
return layout[type] ? layout[type] : layout.invalid;
|
||||||
|
}
|
||||||
|
|
||||||
function layoutGroups(layerType) {
|
function layoutGroups(layerType) {
|
||||||
const layerGroup = {
|
const layerGroup = {
|
||||||
title: 'Layer',
|
title: 'Layer',
|
||||||
@@ -33,7 +38,9 @@ function layoutGroups(layerType) {
|
|||||||
title: 'JSON Editor',
|
title: 'JSON Editor',
|
||||||
type: 'jsoneditor'
|
type: 'jsoneditor'
|
||||||
}
|
}
|
||||||
return [layerGroup, filterGroup].concat(layout[layerType].groups).concat([editorGroup])
|
return [layerGroup, filterGroup]
|
||||||
|
.concat(getLayoutForType(layerType).groups)
|
||||||
|
.concat([editorGroup])
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Layer editor supporting multiple types of layers. */
|
/** Layer editor supporting multiple types of layers. */
|
||||||
@@ -52,6 +59,7 @@ export default class LayerEditor extends React.Component {
|
|||||||
isFirstLayer: PropTypes.bool,
|
isFirstLayer: PropTypes.bool,
|
||||||
isLastLayer: PropTypes.bool,
|
isLastLayer: PropTypes.bool,
|
||||||
layerIndex: PropTypes.number,
|
layerIndex: PropTypes.number,
|
||||||
|
errors: PropTypes.array,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@@ -79,7 +87,7 @@ export default class LayerEditor extends React.Component {
|
|||||||
static getDerivedStateFromProps(props, state) {
|
static getDerivedStateFromProps(props, state) {
|
||||||
const additionalGroups = { ...state.editorGroups }
|
const additionalGroups = { ...state.editorGroups }
|
||||||
|
|
||||||
layout[props.layer.type].groups.forEach(group => {
|
getLayoutForType(props.layer.type).groups.forEach(group => {
|
||||||
if(!(group.title in additionalGroups)) {
|
if(!(group.title in additionalGroups)) {
|
||||||
additionalGroups[group.title] = true
|
additionalGroups[group.title] = true
|
||||||
}
|
}
|
||||||
@@ -118,6 +126,20 @@ export default class LayerEditor extends React.Component {
|
|||||||
if(this.props.layer.metadata) {
|
if(this.props.layer.metadata) {
|
||||||
comment = this.props.layer.metadata['maputnik:comment']
|
comment = this.props.layer.metadata['maputnik:comment']
|
||||||
}
|
}
|
||||||
|
const {errors, layerIndex} = this.props;
|
||||||
|
|
||||||
|
const errorData = {};
|
||||||
|
errors.forEach(error => {
|
||||||
|
if (
|
||||||
|
error.parsed &&
|
||||||
|
error.parsed.type === "layer" &&
|
||||||
|
error.parsed.data.index == layerIndex
|
||||||
|
) {
|
||||||
|
errorData[error.parsed.data.key] = {
|
||||||
|
message: error.parsed.data.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
let sourceLayerIds;
|
let sourceLayerIds;
|
||||||
if(this.props.sources.hasOwnProperty(this.props.layer.source)) {
|
if(this.props.sources.hasOwnProperty(this.props.layer.source)) {
|
||||||
@@ -129,34 +151,42 @@ export default class LayerEditor extends React.Component {
|
|||||||
<LayerIdBlock
|
<LayerIdBlock
|
||||||
value={this.props.layer.id}
|
value={this.props.layer.id}
|
||||||
wdKey="layer-editor.layer-id"
|
wdKey="layer-editor.layer-id"
|
||||||
|
error={errorData.id}
|
||||||
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
|
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
|
||||||
/>
|
/>
|
||||||
<LayerTypeBlock
|
<LayerTypeBlock
|
||||||
|
disabled={true}
|
||||||
|
error={errorData.type}
|
||||||
value={this.props.layer.type}
|
value={this.props.layer.type}
|
||||||
onChange={newType => this.props.onLayerChanged(changeType(this.props.layer, newType))}
|
onChange={newType => this.props.onLayerChanged(changeType(this.props.layer, newType))}
|
||||||
/>
|
/>
|
||||||
{this.props.layer.type !== 'background' && <LayerSourceBlock
|
{this.props.layer.type !== 'background' && <LayerSourceBlock
|
||||||
|
error={errorData.sources}
|
||||||
sourceIds={Object.keys(this.props.sources)}
|
sourceIds={Object.keys(this.props.sources)}
|
||||||
value={this.props.layer.source}
|
value={this.props.layer.source}
|
||||||
onChange={v => this.changeProperty(null, 'source', v)}
|
onChange={v => this.changeProperty(null, 'source', v)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
|
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.props.layer.type) < 0 &&
|
||||||
<LayerSourceLayerBlock
|
<LayerSourceLayerBlock
|
||||||
|
error={errorData['source-layer']}
|
||||||
sourceLayerIds={sourceLayerIds}
|
sourceLayerIds={sourceLayerIds}
|
||||||
value={this.props.layer['source-layer']}
|
value={this.props.layer['source-layer']}
|
||||||
onChange={v => this.changeProperty(null, 'source-layer', v)}
|
onChange={v => this.changeProperty(null, 'source-layer', v)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<MinZoomBlock
|
<MinZoomBlock
|
||||||
|
error={errorData.minzoom}
|
||||||
value={this.props.layer.minzoom}
|
value={this.props.layer.minzoom}
|
||||||
onChange={v => this.changeProperty(null, 'minzoom', v)}
|
onChange={v => this.changeProperty(null, 'minzoom', v)}
|
||||||
/>
|
/>
|
||||||
<MaxZoomBlock
|
<MaxZoomBlock
|
||||||
|
error={errorData.maxzoom}
|
||||||
value={this.props.layer.maxzoom}
|
value={this.props.layer.maxzoom}
|
||||||
onChange={v => this.changeProperty(null, 'maxzoom', v)}
|
onChange={v => this.changeProperty(null, 'maxzoom', v)}
|
||||||
/>
|
/>
|
||||||
<CommentBlock
|
<CommentBlock
|
||||||
|
error={errorData.comment}
|
||||||
value={comment}
|
value={comment}
|
||||||
onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)}
|
onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)}
|
||||||
/>
|
/>
|
||||||
@@ -164,6 +194,7 @@ export default class LayerEditor extends React.Component {
|
|||||||
case 'filter': return <div>
|
case 'filter': return <div>
|
||||||
<div className="maputnik-filter-editor-wrapper">
|
<div className="maputnik-filter-editor-wrapper">
|
||||||
<FilterEditor
|
<FilterEditor
|
||||||
|
errors={errorData}
|
||||||
filter={this.props.layer.filter}
|
filter={this.props.layer.filter}
|
||||||
properties={this.props.vectorLayers[this.props.layer['source-layer']]}
|
properties={this.props.vectorLayers[this.props.layer['source-layer']]}
|
||||||
onChange={f => this.changeProperty(null, 'filter', f)}
|
onChange={f => this.changeProperty(null, 'filter', f)}
|
||||||
@@ -171,6 +202,7 @@ export default class LayerEditor extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
case 'properties': return <PropertyGroup
|
case 'properties': return <PropertyGroup
|
||||||
|
errors={errorData}
|
||||||
layer={this.props.layer}
|
layer={this.props.layer}
|
||||||
groupFields={fields}
|
groupFields={fields}
|
||||||
spec={this.props.spec}
|
spec={this.props.spec}
|
||||||
@@ -191,12 +223,16 @@ export default class LayerEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const groupIds = [];
|
||||||
const layerType = this.props.layer.type
|
const layerType = this.props.layer.type
|
||||||
const groups = layoutGroups(layerType).filter(group => {
|
const groups = layoutGroups(layerType).filter(group => {
|
||||||
return !(layerType === 'background' && group.type === 'source')
|
return !(layerType === 'background' && group.type === 'source')
|
||||||
}).map(group => {
|
}).map(group => {
|
||||||
|
const groupId = group.title.replace(/ /g, "_");
|
||||||
|
groupIds.push(groupId);
|
||||||
return <LayerEditorGroup
|
return <LayerEditorGroup
|
||||||
data-wd-key={group.title}
|
data-wd-key={group.title}
|
||||||
|
id={groupId}
|
||||||
key={group.title}
|
key={group.title}
|
||||||
title={group.title}
|
title={group.title}
|
||||||
isActive={this.state.editorGroups[group.title]}
|
isActive={this.state.editorGroups[group.title]}
|
||||||
@@ -273,7 +309,13 @@ export default class LayerEditor extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
{groups}
|
<Accordion
|
||||||
|
allowMultipleExpanded={true}
|
||||||
|
allowZeroExpanded={true}
|
||||||
|
preExpanded={groupIds}
|
||||||
|
>
|
||||||
|
{groups}
|
||||||
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import Collapser from './Collapser'
|
import Icon from '@mdi/react'
|
||||||
import Collapse from './Collapse'
|
import {
|
||||||
|
mdiMenuDown,
|
||||||
|
mdiMenuUp
|
||||||
|
} from '@mdi/js';
|
||||||
|
import {
|
||||||
|
AccordionItem,
|
||||||
|
AccordionItemHeading,
|
||||||
|
AccordionItemButton,
|
||||||
|
AccordionItemPanel,
|
||||||
|
} from 'react-accessible-accordion';
|
||||||
|
|
||||||
|
|
||||||
export default class LayerEditorGroup extends React.Component {
|
export default class LayerEditorGroup extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
"id": PropTypes.string,
|
||||||
"data-wd-key": PropTypes.string,
|
"data-wd-key": PropTypes.string,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
isActive: PropTypes.bool.isRequired,
|
isActive: PropTypes.bool.isRequired,
|
||||||
@@ -14,20 +24,28 @@ export default class LayerEditorGroup extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div>
|
return <AccordionItem uuid={this.props.id}>
|
||||||
<div className="maputnik-layer-editor-group"
|
<AccordionItemHeading className="maputnik-layer-editor-group"
|
||||||
data-wd-key={"layer-editor-group:"+this.props["data-wd-key"]}
|
data-wd-key={"layer-editor-group:"+this.props["data-wd-key"]}
|
||||||
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
|
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
|
||||||
>
|
>
|
||||||
<span>{this.props.title}</span>
|
<AccordionItemButton className="maputnik-layer-editor-group__button">
|
||||||
<span style={{flexGrow: 1}} />
|
<span style={{flexGrow: 1}}>{this.props.title}</span>
|
||||||
<Collapser isCollapsed={this.props.isActive} />
|
<Icon
|
||||||
</div>
|
path={mdiMenuUp}
|
||||||
<Collapse isActive={this.props.isActive}>
|
size={1}
|
||||||
<div className="react-collapse-container">
|
className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--up"
|
||||||
{this.props.children}
|
/>
|
||||||
</div>
|
<Icon
|
||||||
</Collapse>
|
path={mdiMenuDown}
|
||||||
</div>
|
size={1}
|
||||||
|
className="maputnik-layer-editor-group__button__icon maputnik-layer-editor-group__button__icon--down"
|
||||||
|
/>
|
||||||
|
</AccordionItemButton>
|
||||||
|
</AccordionItemHeading>
|
||||||
|
<AccordionItemPanel>
|
||||||
|
{this.props.children}
|
||||||
|
</AccordionItemPanel>
|
||||||
|
</AccordionItem>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class LayerIdBlock extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <InputBlock label={"ID"} doc={latest.layer.id.doc}
|
return <InputBlock label={"ID"} fieldSpec={latest.layer.id}
|
||||||
data-wd-key={this.props.wdKey}
|
data-wd-key={this.props.wdKey}
|
||||||
>
|
>
|
||||||
<StringInput
|
<StringInput
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
import lodash from 'lodash';
|
||||||
|
|
||||||
import LayerListGroup from './LayerListGroup'
|
import LayerListGroup from './LayerListGroup'
|
||||||
import LayerListItem from './LayerListItem'
|
import LayerListItem from './LayerListItem'
|
||||||
@@ -116,6 +117,50 @@ class LayerListContainer extends React.Component {
|
|||||||
return collapsed === undefined ? true : collapsed
|
return collapsed === undefined ? true : collapsed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate (nextProps, nextState) {
|
||||||
|
// Always update on state change
|
||||||
|
if (this.state !== nextState) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This component tree only requires id and visibility from the layers
|
||||||
|
// objects
|
||||||
|
function getRequiredProps (layer) {
|
||||||
|
const out = {
|
||||||
|
id: layer.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (layer.layout) {
|
||||||
|
out.layout = {
|
||||||
|
visibility: layer.layout.visibility
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
const layersEqual = lodash.isEqual(
|
||||||
|
nextProps.layers.map(getRequiredProps),
|
||||||
|
this.props.layers.map(getRequiredProps),
|
||||||
|
);
|
||||||
|
|
||||||
|
function withoutLayers (props) {
|
||||||
|
const out = {
|
||||||
|
...props
|
||||||
|
};
|
||||||
|
delete out['layers'];
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare the props without layers because we've already compared them
|
||||||
|
// efficiently above.
|
||||||
|
const propsEqual = lodash.isEqual(
|
||||||
|
withoutLayers(this.props),
|
||||||
|
withoutLayers(nextProps)
|
||||||
|
);
|
||||||
|
|
||||||
|
const propsChanged = !(layersEqual && propsEqual);
|
||||||
|
return propsChanged;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
||||||
const listItems = []
|
const listItems = []
|
||||||
@@ -136,10 +181,19 @@ class LayerListContainer extends React.Component {
|
|||||||
layers.forEach((layer, idxInGroup) => {
|
layers.forEach((layer, idxInGroup) => {
|
||||||
const groupIdx = findClosestCommonPrefix(this.props.layers, idx)
|
const groupIdx = findClosestCommonPrefix(this.props.layers, idx)
|
||||||
|
|
||||||
|
const layerError = this.props.errors.find(error => {
|
||||||
|
return (
|
||||||
|
error.parsed &&
|
||||||
|
error.parsed.type === "layer" &&
|
||||||
|
error.parsed.data.index == idx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const listItem = <LayerListItem
|
const listItem = <LayerListItem
|
||||||
className={classnames({
|
className={classnames({
|
||||||
'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex,
|
'maputnik-layer-list-item-collapsed': layers.length > 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex,
|
||||||
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1
|
'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1,
|
||||||
|
'maputnik-layer-list-item--error': !!layerError
|
||||||
})}
|
})}
|
||||||
index={idx}
|
index={idx}
|
||||||
key={layer.id}
|
key={layer.id}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class LayerSourceBlock extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <InputBlock label={"Source"} doc={latest.layer.source.doc}
|
return <InputBlock label={"Source"} fieldSpec={latest.layer.source}
|
||||||
data-wd-key={this.props.wdKey}
|
data-wd-key={this.props.wdKey}
|
||||||
>
|
>
|
||||||
<AutocompleteInput
|
<AutocompleteInput
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class LayerSourceLayer extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <InputBlock label={"Source Layer"} doc={latest.layer['source-layer'].doc}
|
return <InputBlock label={"Source Layer"} fieldSpec={latest.layer['source-layer']}
|
||||||
data-wd-key="layer-source-layer"
|
data-wd-key="layer-source-layer"
|
||||||
>
|
>
|
||||||
<AutocompleteInput
|
<AutocompleteInput
|
||||||
|
|||||||
@@ -4,33 +4,49 @@ import PropTypes from 'prop-types'
|
|||||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||||
import InputBlock from '../inputs/InputBlock'
|
import InputBlock from '../inputs/InputBlock'
|
||||||
import SelectInput from '../inputs/SelectInput'
|
import SelectInput from '../inputs/SelectInput'
|
||||||
|
import StringInput from '../inputs/StringInput'
|
||||||
|
|
||||||
class LayerTypeBlock extends React.Component {
|
class LayerTypeBlock extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
wdKey: PropTypes.string,
|
wdKey: PropTypes.string,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
disabled: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <InputBlock label={"Type"} doc={latest.layer.type.doc}
|
return <InputBlock label={"Type"} fieldSpec={latest.layer.type}
|
||||||
data-wd-key={this.props.wdKey}
|
data-wd-key={this.props.wdKey}
|
||||||
|
error={this.props.error}
|
||||||
>
|
>
|
||||||
<SelectInput
|
{this.props.disabled &&
|
||||||
options={[
|
<StringInput
|
||||||
['background', 'Background'],
|
value={this.props.value}
|
||||||
['fill', 'Fill'],
|
disabled={true}
|
||||||
['line', 'Line'],
|
/>
|
||||||
['symbol', 'Symbol'],
|
}
|
||||||
['raster', 'Raster'],
|
{!this.props.disabled &&
|
||||||
['circle', 'Circle'],
|
<SelectInput
|
||||||
['fill-extrusion', 'Fill Extrusion'],
|
options={[
|
||||||
['hillshade', 'Hillshade'],
|
['background', 'Background'],
|
||||||
['heatmap', 'Heatmap'],
|
['fill', 'Fill'],
|
||||||
]}
|
['line', 'Line'],
|
||||||
onChange={this.props.onChange}
|
['symbol', 'Symbol'],
|
||||||
value={this.props.value}
|
['raster', 'Raster'],
|
||||||
/>
|
['circle', 'Circle'],
|
||||||
|
['fill-extrusion', 'Fill Extrusion'],
|
||||||
|
['hillshade', 'Hillshade'],
|
||||||
|
['heatmap', 'Heatmap'],
|
||||||
|
]}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
value={this.props.value}
|
||||||
|
/>
|
||||||
|
}
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,16 @@ class MaxZoomBlock extends React.Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.number,
|
value: PropTypes.number,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <InputBlock label={"Max Zoom"} doc={latest.layer.maxzoom.doc}
|
return <InputBlock label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
|
||||||
|
error={this.props.error}
|
||||||
data-wd-key="max-zoom"
|
data-wd-key="max-zoom"
|
||||||
>
|
>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
|
allowRange={true}
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
min={latest.layer.maxzoom.minimum}
|
min={latest.layer.maxzoom.minimum}
|
||||||
|
|||||||
@@ -9,13 +9,16 @@ class MinZoomBlock extends React.Component {
|
|||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.number,
|
value: PropTypes.number,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <InputBlock label={"Min Zoom"} doc={latest.layer.minzoom.doc}
|
return <InputBlock label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
|
||||||
|
error={this.props.error}
|
||||||
data-wd-key="min-zoom"
|
data-wd-key="min-zoom"
|
||||||
>
|
>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
|
allowRange={true}
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
min={latest.layer.minzoom.minimum}
|
min={latest.layer.minzoom.minimum}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ function renderProperties(feature) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFeature(feature) {
|
function renderFeature(feature, idx) {
|
||||||
return <div key={`${feature.sourceLayer}-${feature.id}`}>
|
return <div key={`${feature.sourceLayer}-${idx}`}>
|
||||||
<div className="maputnik-popup-layer-id">{feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div>
|
<div className="maputnik-popup-layer-id">{feature.layer['source-layer']}{feature.inspectModeCounter && <span> × {feature.inspectModeCounter}</span>}</div>
|
||||||
<InputBlock key={"property-type"} label={"$type"}>
|
<InputBlock key={"property-type"} label={"$type"}>
|
||||||
<StringInput value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
|
<StringInput value={feature.geometry.type} style={{backgroundColor: 'transparent'}} />
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ const IS_SUPPORTED = MapboxGl.supported();
|
|||||||
|
|
||||||
function renderPopup(popup, mountNode) {
|
function renderPopup(popup, mountNode) {
|
||||||
ReactDOM.render(popup, mountNode);
|
ReactDOM.render(popup, mountNode);
|
||||||
var content = mountNode.innerHTML;
|
return mountNode;
|
||||||
return content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
|
function buildInspectStyle(originalMapStyle, coloredLayers, highlightedLayer) {
|
||||||
@@ -61,12 +60,15 @@ export default class MapboxGlMap extends React.Component {
|
|||||||
inspectModeEnabled: PropTypes.bool.isRequired,
|
inspectModeEnabled: PropTypes.bool.isRequired,
|
||||||
highlightedLayer: PropTypes.object,
|
highlightedLayer: PropTypes.object,
|
||||||
options: PropTypes.object,
|
options: PropTypes.object,
|
||||||
|
replaceAccessTokens: PropTypes.func.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onMapLoaded: () => {},
|
onMapLoaded: () => {},
|
||||||
onDataChange: () => {},
|
onDataChange: () => {},
|
||||||
onLayerSelect: () => {},
|
onLayerSelect: () => {},
|
||||||
|
onChange: () => {},
|
||||||
mapboxAccessToken: tokens.mapbox,
|
mapboxAccessToken: tokens.mapbox,
|
||||||
options: {},
|
options: {},
|
||||||
}
|
}
|
||||||
@@ -87,21 +89,12 @@ export default class MapboxGlMap extends React.Component {
|
|||||||
const metadata = props.mapStyle.metadata || {}
|
const metadata = props.mapStyle.metadata || {}
|
||||||
MapboxGl.accessToken = metadata['maputnik:mapbox_access_token'] || tokens.mapbox
|
MapboxGl.accessToken = metadata['maputnik:mapbox_access_token'] || tokens.mapbox
|
||||||
|
|
||||||
if(!props.inspectModeEnabled) {
|
//Mapbox GL now does diffing natively so we don't need to calculate
|
||||||
//Mapbox GL now does diffing natively so we don't need to calculate
|
//the necessary operations ourselves!
|
||||||
//the necessary operations ourselves!
|
this.state.map.setStyle(
|
||||||
this.state.map.setStyle(props.mapStyle, { diff: true})
|
this.props.replaceAccessTokens(props.mapStyle),
|
||||||
}
|
{diff: true}
|
||||||
}
|
)
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
|
||||||
let should = false;
|
|
||||||
try {
|
|
||||||
should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState);
|
|
||||||
} catch(e) {
|
|
||||||
// no biggie, carry on
|
|
||||||
}
|
|
||||||
return should;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
@@ -112,6 +105,9 @@ export default class MapboxGlMap extends React.Component {
|
|||||||
this.updateMapFromProps(this.props);
|
this.updateMapFromProps(this.props);
|
||||||
|
|
||||||
if(this.props.inspectModeEnabled !== prevProps.inspectModeEnabled) {
|
if(this.props.inspectModeEnabled !== prevProps.inspectModeEnabled) {
|
||||||
|
// HACK: Fix for <https://github.com/maputnik/editor/issues/576>, while we wait for a proper fix.
|
||||||
|
// eslint-disable-next-line
|
||||||
|
this.state.inspect._popupBlocked = false;
|
||||||
this.state.inspect.toggleInspector()
|
this.state.inspect.toggleInspector()
|
||||||
}
|
}
|
||||||
if(this.props.inspectModeEnabled) {
|
if(this.props.inspectModeEnabled) {
|
||||||
@@ -133,16 +129,24 @@ export default class MapboxGlMap extends React.Component {
|
|||||||
container: this.container,
|
container: this.container,
|
||||||
style: this.props.mapStyle,
|
style: this.props.mapStyle,
|
||||||
hash: true,
|
hash: true,
|
||||||
|
maxZoom: 24
|
||||||
}
|
}
|
||||||
|
|
||||||
const map = new MapboxGl.Map(mapOpts);
|
const map = new MapboxGl.Map(mapOpts);
|
||||||
|
|
||||||
|
const mapViewChange = () => {
|
||||||
|
const center = map.getCenter();
|
||||||
|
const zoom = map.getZoom();
|
||||||
|
this.props.onChange({center, zoom});
|
||||||
|
}
|
||||||
|
mapViewChange();
|
||||||
|
|
||||||
map.showTileBoundaries = mapOpts.showTileBoundaries;
|
map.showTileBoundaries = mapOpts.showTileBoundaries;
|
||||||
map.showCollisionBoxes = mapOpts.showCollisionBoxes;
|
map.showCollisionBoxes = mapOpts.showCollisionBoxes;
|
||||||
map.showOverdrawInspector = mapOpts.showOverdrawInspector;
|
map.showOverdrawInspector = mapOpts.showOverdrawInspector;
|
||||||
|
|
||||||
const zoom = new ZoomControl;
|
const zoomControl = new ZoomControl;
|
||||||
map.addControl(zoom, 'top-right');
|
map.addControl(zoomControl, 'top-right');
|
||||||
|
|
||||||
const nav = new MapboxGl.NavigationControl({visualizePitch:true});
|
const nav = new MapboxGl.NavigationControl({visualizePitch:true});
|
||||||
map.addControl(nav, 'top-right');
|
map.addControl(nav, 'top-right');
|
||||||
@@ -190,11 +194,18 @@ export default class MapboxGlMap extends React.Component {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
map.on("error", e => {
|
||||||
|
console.log("ERROR", e);
|
||||||
|
})
|
||||||
|
|
||||||
map.on("zoom", e => {
|
map.on("zoom", e => {
|
||||||
this.setState({
|
this.setState({
|
||||||
zoom: map.getZoom()
|
zoom: map.getZoom()
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
|
map.on("dragend", mapViewChange);
|
||||||
|
map.on("zoomend", mapViewChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export default class OpenLayersMap extends React.Component {
|
|||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
onLayerSelect: PropTypes.func.isRequired,
|
onLayerSelect: PropTypes.func.isRequired,
|
||||||
debugToolbox: PropTypes.bool.isRequired,
|
debugToolbox: PropTypes.bool.isRequired,
|
||||||
|
replaceAccessTokens: PropTypes.func.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@@ -61,7 +63,9 @@ export default class OpenLayersMap extends React.Component {
|
|||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (this.props.mapStyle !== prevProps.mapStyle) {
|
if (this.props.mapStyle !== prevProps.mapStyle) {
|
||||||
this.updateStyle(this.props.mapStyle);
|
this.updateStyle(
|
||||||
|
this.props.replaceAccessTokens(this.props.mapStyle)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +97,22 @@ export default class OpenLayersMap extends React.Component {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const onMoveEnd = () => {
|
||||||
|
const zoom = map.getView().getZoom();
|
||||||
|
const center = toLonLat(map.getView().getCenter());
|
||||||
|
|
||||||
|
this.props.onChange({
|
||||||
|
zoom,
|
||||||
|
center: {
|
||||||
|
lng: center[0],
|
||||||
|
lat: center[1],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMoveEnd();
|
||||||
|
map.on('moveend', onMoveEnd);
|
||||||
|
|
||||||
map.on('postrender', (evt) => {
|
map.on('postrender', (evt) => {
|
||||||
const center = toLonLat(map.getView().getCenter());
|
const center = toLonLat(map.getView().getCenter());
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -108,7 +128,9 @@ export default class OpenLayersMap extends React.Component {
|
|||||||
|
|
||||||
|
|
||||||
this.map = map;
|
this.map = map;
|
||||||
this.updateStyle(this.props.mapStyle);
|
this.updateStyle(
|
||||||
|
this.props.replaceAccessTokens(this.props.mapStyle)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
closeOverlay = (e) => {
|
closeOverlay = (e) => {
|
||||||
@@ -136,7 +158,7 @@ export default class OpenLayersMap extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="maputnik-ol-zoom">
|
<div className="maputnik-ol-zoom">
|
||||||
Zoom level: {this.state.zoom}
|
Zoom: {this.state.zoom}
|
||||||
</div>
|
</div>
|
||||||
{this.props.debugToolbox &&
|
{this.props.debugToolbox &&
|
||||||
<div className="maputnik-ol-debug">
|
<div className="maputnik-ol-debug">
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ class AddModal extends React.Component {
|
|||||||
onOpenToggle={this.props.onOpenToggle}
|
onOpenToggle={this.props.onOpenToggle}
|
||||||
title={'Add Layer'}
|
title={'Add Layer'}
|
||||||
data-wd-key="modal:add-layer"
|
data-wd-key="modal:add-layer"
|
||||||
|
className="maputnik-add-modal"
|
||||||
>
|
>
|
||||||
<div className="maputnik-add-layer">
|
<div className="maputnik-add-layer">
|
||||||
<LayerIdBlock
|
<LayerIdBlock
|
||||||
|
|||||||
@@ -13,9 +13,16 @@ class DebugModal extends React.Component {
|
|||||||
onOpenToggle: PropTypes.func.isRequired,
|
onOpenToggle: PropTypes.func.isRequired,
|
||||||
mapboxGlDebugOptions: PropTypes.object,
|
mapboxGlDebugOptions: PropTypes.object,
|
||||||
openlayersDebugOptions: PropTypes.object,
|
openlayersDebugOptions: PropTypes.object,
|
||||||
|
mapView: PropTypes.object,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {mapView} = this.props;
|
||||||
|
|
||||||
|
const osmZoom = Math.round(mapView.zoom)+1;
|
||||||
|
const osmLon = Number.parseFloat(mapView.center.lng).toFixed(5);
|
||||||
|
const osmLat = Number.parseFloat(mapView.center.lat).toFixed(5);
|
||||||
|
|
||||||
return <Modal
|
return <Modal
|
||||||
data-wd-key="debug-modal"
|
data-wd-key="debug-modal"
|
||||||
isOpen={this.props.isOpen}
|
isOpen={this.props.isOpen}
|
||||||
@@ -23,6 +30,7 @@ class DebugModal extends React.Component {
|
|||||||
title={'Debug'}
|
title={'Debug'}
|
||||||
>
|
>
|
||||||
<div className="maputnik-modal-section maputnik-modal-shortcuts">
|
<div className="maputnik-modal-section maputnik-modal-shortcuts">
|
||||||
|
<h4>Options</h4>
|
||||||
{this.props.renderer === 'mbgljs' &&
|
{this.props.renderer === 'mbgljs' &&
|
||||||
<ul>
|
<ul>
|
||||||
{Object.entries(this.props.mapboxGlDebugOptions).map(([key, val]) => {
|
{Object.entries(this.props.mapboxGlDebugOptions).map(([key, val]) => {
|
||||||
@@ -46,6 +54,18 @@ class DebugModal extends React.Component {
|
|||||||
</ul>
|
</ul>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="maputnik-modal-section">
|
||||||
|
<h4>Links</h4>
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href={`https://www.openstreetmap.org/#map=${osmZoom}/${osmLat}/${osmLon}`}
|
||||||
|
>
|
||||||
|
Open in OSM
|
||||||
|
</a> — Opens the current view on openstreetmap.org
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import Button from '../Button'
|
|||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
import {MdFileDownload} from 'react-icons/md'
|
import {MdFileDownload} from 'react-icons/md'
|
||||||
import style from '../../libs/style'
|
import style from '../../libs/style'
|
||||||
|
import fieldSpecAdditional from '../../libs/field-spec-additional'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -70,6 +71,7 @@ class ExportModal extends React.Component {
|
|||||||
isOpen={this.props.isOpen}
|
isOpen={this.props.isOpen}
|
||||||
onOpenToggle={this.props.onOpenToggle}
|
onOpenToggle={this.props.onOpenToggle}
|
||||||
title={'Export Style'}
|
title={'Export Style'}
|
||||||
|
className="maputnik-export-modal"
|
||||||
>
|
>
|
||||||
|
|
||||||
<div className="maputnik-modal-section">
|
<div className="maputnik-modal-section">
|
||||||
@@ -78,26 +80,35 @@ class ExportModal extends React.Component {
|
|||||||
Download a JSON style to your computer.
|
Download a JSON style to your computer.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<div>
|
||||||
<InputBlock label={"MapTiler Access Token: "}>
|
<InputBlock
|
||||||
|
label={fieldSpecAdditional.maputnik.mapbox_access_token.label}
|
||||||
|
fieldSpec={fieldSpecAdditional.maputnik.mapbox_access_token}
|
||||||
|
>
|
||||||
<StringInput
|
<StringInput
|
||||||
value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']}
|
value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']}
|
||||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
|
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
<InputBlock label={"Mapbox Access Token: "}>
|
<InputBlock
|
||||||
|
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
|
||||||
|
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
|
||||||
|
>
|
||||||
<StringInput
|
<StringInput
|
||||||
value={(this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token']}
|
value={(this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token']}
|
||||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
|
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
<InputBlock label={"Thunderforest Access Token: "}>
|
<InputBlock
|
||||||
|
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
|
||||||
|
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
|
||||||
|
>
|
||||||
<StringInput
|
<StringInput
|
||||||
value={(this.props.mapStyle.metadata || {})['maputnik:thunderforest_access_token']}
|
value={(this.props.mapStyle.metadata || {})['maputnik:thunderforest_access_token']}
|
||||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
|
onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
</p>
|
</div>
|
||||||
|
|
||||||
<Button onClick={this.downloadStyle.bind(this)}>
|
<Button onClick={this.downloadStyle.bind(this)}>
|
||||||
<MdFileDownload />
|
<MdFileDownload />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react'
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import {MdClose} from 'react-icons/md'
|
import {MdClose} from 'react-icons/md'
|
||||||
import AriaModal from 'react-aria-modal'
|
import AriaModal from 'react-aria-modal'
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
class Modal extends React.Component {
|
class Modal extends React.Component {
|
||||||
@@ -13,6 +14,7 @@ class Modal extends React.Component {
|
|||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
underlayClickExits: PropTypes.bool,
|
underlayClickExits: PropTypes.bool,
|
||||||
underlayProps: PropTypes.object,
|
underlayProps: PropTypes.object,
|
||||||
|
className: PropTypes.string,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@@ -45,7 +47,7 @@ class Modal extends React.Component {
|
|||||||
verticallyCenter={true}
|
verticallyCenter={true}
|
||||||
onExit={this.onClose}
|
onExit={this.onClose}
|
||||||
>
|
>
|
||||||
<div className="maputnik-modal"
|
<div className={classnames("maputnik-modal", this.props.className)}
|
||||||
data-wd-key={this.props["data-wd-key"]}
|
data-wd-key={this.props["data-wd-key"]}
|
||||||
>
|
>
|
||||||
<header className="maputnik-modal-header">
|
<header className="maputnik-modal-header">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import LoadingModal from './LoadingModal'
|
|||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
import Button from '../Button'
|
import Button from '../Button'
|
||||||
import FileReaderInput from 'react-file-reader-input'
|
import FileReaderInput from 'react-file-reader-input'
|
||||||
|
import UrlInput from '../inputs/UrlInput'
|
||||||
|
|
||||||
import {MdFileUpload} from 'react-icons/md'
|
import {MdFileUpload} from 'react-icons/md'
|
||||||
import {MdAddCircleOutline} from 'react-icons/md'
|
import {MdAddCircleOutline} from 'react-icons/md'
|
||||||
@@ -122,9 +123,8 @@ class OpenModal extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpenUrl = () => {
|
onOpenUrl = (url) => {
|
||||||
const url = this.styleUrlElement.value;
|
this.onStyleSelect(this.state.styleUrl);
|
||||||
this.onStyleSelect(url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpload = (_, files) => {
|
onUpload = (_, files) => {
|
||||||
@@ -160,9 +160,9 @@ class OpenModal extends React.Component {
|
|||||||
this.props.onOpenToggle();
|
this.props.onOpenToggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeUrl = () => {
|
onChangeUrl = (url) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
styleUrl: this.styleUrlElement.value
|
styleUrl: url,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,14 +209,13 @@ class OpenModal extends React.Component {
|
|||||||
<p>
|
<p>
|
||||||
Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>.
|
Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>.
|
||||||
</p>
|
</p>
|
||||||
<input
|
<UrlInput
|
||||||
data-wd-key="open-modal.url.input"
|
data-wd-key="open-modal.url.input"
|
||||||
type="text"
|
type="text"
|
||||||
ref={(input) => this.styleUrlElement = input}
|
|
||||||
className="maputnik-input"
|
className="maputnik-input"
|
||||||
placeholder="Enter URL..."
|
default="Enter URL..."
|
||||||
value={this.state.styleUrl}
|
value={this.state.styleUrl}
|
||||||
onChange={this.onChangeUrl}
|
onInput={this.onChangeUrl}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -3,9 +3,15 @@ import PropTypes from 'prop-types'
|
|||||||
|
|
||||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||||
import InputBlock from '../inputs/InputBlock'
|
import InputBlock from '../inputs/InputBlock'
|
||||||
|
import ArrayInput from '../inputs/ArrayInput'
|
||||||
|
import NumberInput from '../inputs/NumberInput'
|
||||||
import StringInput from '../inputs/StringInput'
|
import StringInput from '../inputs/StringInput'
|
||||||
|
import UrlInput from '../inputs/UrlInput'
|
||||||
import SelectInput from '../inputs/SelectInput'
|
import SelectInput from '../inputs/SelectInput'
|
||||||
|
import EnumInput from '../inputs/EnumInput'
|
||||||
|
import ColorField from '../fields/ColorField'
|
||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
|
import fieldSpecAdditional from '../../libs/field-spec-additional'
|
||||||
|
|
||||||
class SettingsModal extends React.Component {
|
class SettingsModal extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@@ -16,56 +22,105 @@ class SettingsModal extends React.Component {
|
|||||||
onOpenToggle: PropTypes.func.isRequired,
|
onOpenToggle: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changeTransitionProperty(property, value) {
|
||||||
|
const transition = {
|
||||||
|
...this.props.mapStyle.transition,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === undefined) {
|
||||||
|
delete transition[property];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
transition[property] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onStyleChanged({
|
||||||
|
...this.props.mapStyle,
|
||||||
|
transition,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
changeLightProperty(property, value) {
|
||||||
|
const light = {
|
||||||
|
...this.props.mapStyle.light,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === undefined) {
|
||||||
|
delete light[property];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
light[property] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onStyleChanged({
|
||||||
|
...this.props.mapStyle,
|
||||||
|
light,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
changeStyleProperty(property, value) {
|
changeStyleProperty(property, value) {
|
||||||
const changedStyle = {
|
const changedStyle = {
|
||||||
...this.props.mapStyle,
|
...this.props.mapStyle,
|
||||||
[property]: value
|
};
|
||||||
|
|
||||||
|
if (value === undefined) {
|
||||||
|
delete changedStyle[property];
|
||||||
}
|
}
|
||||||
this.props.onStyleChanged(changedStyle)
|
else {
|
||||||
|
changedStyle[property] = value;
|
||||||
|
}
|
||||||
|
this.props.onStyleChanged(changedStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const metadata = this.props.mapStyle.metadata || {}
|
const metadata = this.props.mapStyle.metadata || {}
|
||||||
const {onChangeMetadataProperty} = this.props;
|
const {onChangeMetadataProperty, mapStyle} = this.props;
|
||||||
const inputProps = { }
|
const inputProps = { }
|
||||||
|
|
||||||
|
const light = this.props.mapStyle.light || {};
|
||||||
|
const transition = this.props.mapStyle.transition || {};
|
||||||
|
|
||||||
return <Modal
|
return <Modal
|
||||||
data-wd-key="modal-settings"
|
data-wd-key="modal-settings"
|
||||||
isOpen={this.props.isOpen}
|
isOpen={this.props.isOpen}
|
||||||
onOpenToggle={this.props.onOpenToggle}
|
onOpenToggle={this.props.onOpenToggle}
|
||||||
title={'Style Settings'}
|
title={'Style Settings'}
|
||||||
>
|
>
|
||||||
<div style={{minWidth: 350}}>
|
<div className="modal-settings">
|
||||||
<InputBlock label={"Name"} doc={latest.$root.name.doc}>
|
<InputBlock label={"Name"} fieldSpec={latest.$root.name}>
|
||||||
<StringInput {...inputProps}
|
<StringInput {...inputProps}
|
||||||
data-wd-key="modal-settings.name"
|
data-wd-key="modal-settings.name"
|
||||||
value={this.props.mapStyle.name}
|
value={this.props.mapStyle.name}
|
||||||
onChange={this.changeStyleProperty.bind(this, "name")}
|
onChange={this.changeStyleProperty.bind(this, "name")}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
<InputBlock label={"Owner"} doc={"Owner ID of the style. Used by Mapbox or future style APIs."}>
|
<InputBlock label={"Owner"} fieldSpec={{doc: "Owner ID of the style. Used by Mapbox or future style APIs."}}>
|
||||||
<StringInput {...inputProps}
|
<StringInput {...inputProps}
|
||||||
data-wd-key="modal-settings.owner"
|
data-wd-key="modal-settings.owner"
|
||||||
value={this.props.mapStyle.owner}
|
value={this.props.mapStyle.owner}
|
||||||
onChange={this.changeStyleProperty.bind(this, "owner")}
|
onChange={this.changeStyleProperty.bind(this, "owner")}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
<InputBlock label={"Sprite URL"} doc={latest.$root.sprite.doc}>
|
<InputBlock label={"Sprite URL"} fieldSpec={latest.$root.sprite}>
|
||||||
<StringInput {...inputProps}
|
<UrlInput {...inputProps}
|
||||||
data-wd-key="modal-settings.sprite"
|
data-wd-key="modal-settings.sprite"
|
||||||
value={this.props.mapStyle.sprite}
|
value={this.props.mapStyle.sprite}
|
||||||
onChange={this.changeStyleProperty.bind(this, "sprite")}
|
onChange={this.changeStyleProperty.bind(this, "sprite")}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
|
|
||||||
<InputBlock label={"Glyphs URL"} doc={latest.$root.glyphs.doc}>
|
<InputBlock label={"Glyphs URL"} fieldSpec={latest.$root.glyphs}>
|
||||||
<StringInput {...inputProps}
|
<UrlInput {...inputProps}
|
||||||
data-wd-key="modal-settings.glyphs"
|
data-wd-key="modal-settings.glyphs"
|
||||||
value={this.props.mapStyle.glyphs}
|
value={this.props.mapStyle.glyphs}
|
||||||
onChange={this.changeStyleProperty.bind(this, "glyphs")}
|
onChange={this.changeStyleProperty.bind(this, "glyphs")}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
|
|
||||||
<InputBlock label={"Mapbox Access Token"} doc={"Public access token for Mapbox services."}>
|
<InputBlock
|
||||||
|
label={fieldSpecAdditional.maputnik.mapbox_access_token.label}
|
||||||
|
fieldSpec={fieldSpecAdditional.maputnik.mapbox_access_token}
|
||||||
|
>
|
||||||
<StringInput {...inputProps}
|
<StringInput {...inputProps}
|
||||||
data-wd-key="modal-settings.maputnik:mapbox_access_token"
|
data-wd-key="modal-settings.maputnik:mapbox_access_token"
|
||||||
value={metadata['maputnik:mapbox_access_token']}
|
value={metadata['maputnik:mapbox_access_token']}
|
||||||
@@ -73,7 +128,10 @@ class SettingsModal extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
|
|
||||||
<InputBlock label={"MapTiler Access Token"} doc={"Public access token for MapTiler Cloud."}>
|
<InputBlock
|
||||||
|
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
|
||||||
|
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
|
||||||
|
>
|
||||||
<StringInput {...inputProps}
|
<StringInput {...inputProps}
|
||||||
data-wd-key="modal-settings.maputnik:openmaptiles_access_token"
|
data-wd-key="modal-settings.maputnik:openmaptiles_access_token"
|
||||||
value={metadata['maputnik:openmaptiles_access_token']}
|
value={metadata['maputnik:openmaptiles_access_token']}
|
||||||
@@ -81,7 +139,10 @@ class SettingsModal extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
|
|
||||||
<InputBlock label={"Thunderforest Access Token"} doc={"Public access token for Thunderforest services."}>
|
<InputBlock
|
||||||
|
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
|
||||||
|
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
|
||||||
|
>
|
||||||
<StringInput {...inputProps}
|
<StringInput {...inputProps}
|
||||||
data-wd-key="modal-settings.maputnik:thunderforest_access_token"
|
data-wd-key="modal-settings.maputnik:thunderforest_access_token"
|
||||||
value={metadata['maputnik:thunderforest_access_token']}
|
value={metadata['maputnik:thunderforest_access_token']}
|
||||||
@@ -89,7 +150,104 @@ class SettingsModal extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
|
|
||||||
<InputBlock label={"Style Renderer"} doc={"Choose the default Maputnik renderer for this style."}>
|
<InputBlock label={"Center"} fieldSpec={latest.$root.center}>
|
||||||
|
<ArrayInput
|
||||||
|
length={2}
|
||||||
|
type="number"
|
||||||
|
value={mapStyle.center}
|
||||||
|
default={latest.$root.center.default || [0, 0]}
|
||||||
|
onChange={this.changeStyleProperty.bind(this, "center")}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
|
||||||
|
<InputBlock label={"Zoom"} fieldSpec={latest.$root.zoom}>
|
||||||
|
<NumberInput
|
||||||
|
{...inputProps}
|
||||||
|
value={mapStyle.zoom}
|
||||||
|
default={latest.$root.zoom.default || 0}
|
||||||
|
onChange={this.changeStyleProperty.bind(this, "zoom")}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
|
||||||
|
<InputBlock label={"Bearing"} fieldSpec={latest.$root.bearing}>
|
||||||
|
<NumberInput
|
||||||
|
{...inputProps}
|
||||||
|
value={mapStyle.bearing}
|
||||||
|
default={latest.$root.bearing.default}
|
||||||
|
onChange={this.changeStyleProperty.bind(this, "bearing")}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
|
||||||
|
<InputBlock label={"Pitch"} fieldSpec={latest.$root.pitch}>
|
||||||
|
<NumberInput
|
||||||
|
{...inputProps}
|
||||||
|
value={mapStyle.pitch}
|
||||||
|
default={latest.$root.pitch.default}
|
||||||
|
onChange={this.changeStyleProperty.bind(this, "pitch")}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
|
||||||
|
<InputBlock label={"Light anchor"} fieldSpec={latest.light.anchor}>
|
||||||
|
<EnumInput
|
||||||
|
{...inputProps}
|
||||||
|
value={light.anchor}
|
||||||
|
options={Object.keys(latest.light.anchor.values)}
|
||||||
|
default={latest.light.anchor.default}
|
||||||
|
onChange={this.changeLightProperty.bind(this, "anchor")}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
|
||||||
|
<InputBlock label={"Light color"} fieldSpec={latest.light.color}>
|
||||||
|
<ColorField
|
||||||
|
{...inputProps}
|
||||||
|
value={light.color}
|
||||||
|
default={latest.light.color.default}
|
||||||
|
onChange={this.changeLightProperty.bind(this, "color")}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
|
||||||
|
<InputBlock label={"Light intensity"} fieldSpec={latest.light.intensity}>
|
||||||
|
<NumberInput
|
||||||
|
{...inputProps}
|
||||||
|
value={light.intensity}
|
||||||
|
default={latest.light.intensity.default}
|
||||||
|
onChange={this.changeLightProperty.bind(this, "intensity")}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
|
||||||
|
<InputBlock label={"Light position"} fieldSpec={latest.light.position}>
|
||||||
|
<ArrayInput
|
||||||
|
{...inputProps}
|
||||||
|
type="number"
|
||||||
|
length={latest.light.position.length}
|
||||||
|
value={light.position}
|
||||||
|
default={latest.light.position.default}
|
||||||
|
onChange={this.changeLightProperty.bind(this, "position")}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
|
||||||
|
<InputBlock label={"Transition delay"} fieldSpec={latest.transition.delay}>
|
||||||
|
<NumberInput
|
||||||
|
{...inputProps}
|
||||||
|
value={transition.delay}
|
||||||
|
default={latest.transition.delay.default}
|
||||||
|
onChange={this.changeTransitionProperty.bind(this, "delay")}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
|
||||||
|
<InputBlock label={"Transition duration"} fieldSpec={latest.transition.duration}>
|
||||||
|
<NumberInput
|
||||||
|
{...inputProps}
|
||||||
|
value={transition.duration}
|
||||||
|
default={latest.transition.duration.default}
|
||||||
|
onChange={this.changeTransitionProperty.bind(this, "duration")}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
|
||||||
|
<InputBlock
|
||||||
|
label={fieldSpecAdditional.maputnik.style_renderer.label}
|
||||||
|
fieldSpec={fieldSpecAdditional.maputnik.style_renderer}
|
||||||
|
>
|
||||||
<SelectInput {...inputProps}
|
<SelectInput {...inputProps}
|
||||||
data-wd-key="modal-settings.maputnik:renderer"
|
data-wd-key="modal-settings.maputnik:renderer"
|
||||||
options={[
|
options={[
|
||||||
@@ -101,6 +259,8 @@ class SettingsModal extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,20 @@ function editorMode(source) {
|
|||||||
if(source.tiles) return 'tilexyz_vector'
|
if(source.tiles) return 'tilexyz_vector'
|
||||||
return 'tilejson_vector'
|
return 'tilejson_vector'
|
||||||
}
|
}
|
||||||
if(source.type === 'geojson') return 'geojson'
|
if(source.type === 'geojson') {
|
||||||
|
if (typeof(source.data) === "string") {
|
||||||
|
return 'geojson_url';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 'geojson_json';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(source.type === 'image') {
|
||||||
|
return 'image';
|
||||||
|
}
|
||||||
|
if(source.type === 'video') {
|
||||||
|
return 'video';
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,76 +118,128 @@ class AddSource extends React.Component {
|
|||||||
|
|
||||||
defaultSource(mode) {
|
defaultSource(mode) {
|
||||||
const source = (this.state || {}).source || {}
|
const source = (this.state || {}).source || {}
|
||||||
|
const {protocol} = window.location;
|
||||||
|
|
||||||
switch(mode) {
|
switch(mode) {
|
||||||
case 'geojson': return {
|
case 'geojson_url': return {
|
||||||
type: 'geojson',
|
type: 'geojson',
|
||||||
data: source.data || 'http://localhost:3000/geojson.json'
|
data: `${protocol}//localhost:3000/geojson.json`
|
||||||
|
}
|
||||||
|
case 'geojson_json': return {
|
||||||
|
type: 'geojson',
|
||||||
|
data: {}
|
||||||
}
|
}
|
||||||
case 'tilejson_vector': return {
|
case 'tilejson_vector': return {
|
||||||
type: 'vector',
|
type: 'vector',
|
||||||
url: source.url || 'http://localhost:3000/tilejson.json'
|
url: source.url || `${protocol}//localhost:3000/tilejson.json`
|
||||||
}
|
}
|
||||||
case 'tilexyz_vector': return {
|
case 'tilexyz_vector': return {
|
||||||
type: 'vector',
|
type: 'vector',
|
||||||
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
|
tiles: source.tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.pbf`],
|
||||||
minZoom: source.minzoom || 0,
|
minZoom: source.minzoom || 0,
|
||||||
maxZoom: source.maxzoom || 14
|
maxZoom: source.maxzoom || 14
|
||||||
}
|
}
|
||||||
case 'tilejson_raster': return {
|
case 'tilejson_raster': return {
|
||||||
type: 'raster',
|
type: 'raster',
|
||||||
url: source.url || 'http://localhost:3000/tilejson.json'
|
url: source.url || `${protocol}//localhost:3000/tilejson.json`
|
||||||
}
|
}
|
||||||
case 'tilexyz_raster': return {
|
case 'tilexyz_raster': return {
|
||||||
type: 'raster',
|
type: 'raster',
|
||||||
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
|
tiles: source.tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.pbf`],
|
||||||
minzoom: source.minzoom || 0,
|
minzoom: source.minzoom || 0,
|
||||||
maxzoom: source.maxzoom || 14
|
maxzoom: source.maxzoom || 14
|
||||||
}
|
}
|
||||||
case 'tilejson_raster-dem': return {
|
case 'tilejson_raster-dem': return {
|
||||||
type: 'raster-dem',
|
type: 'raster-dem',
|
||||||
url: source.url || 'http://localhost:3000/tilejson.json'
|
url: source.url || `${protocol}//localhost:3000/tilejson.json`
|
||||||
}
|
}
|
||||||
case 'tilexyz_raster-dem': return {
|
case 'tilexyz_raster-dem': return {
|
||||||
type: 'raster-dem',
|
type: 'raster-dem',
|
||||||
tiles: source.tiles || ['http://localhost:3000/{x}/{y}/{z}.pbf'],
|
tiles: source.tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.pbf`],
|
||||||
minzoom: source.minzoom || 0,
|
minzoom: source.minzoom || 0,
|
||||||
maxzoom: source.maxzoom || 14
|
maxzoom: source.maxzoom || 14
|
||||||
}
|
}
|
||||||
|
case 'image': return {
|
||||||
|
type: 'image',
|
||||||
|
url: `${protocol}//localhost:3000/image.png`,
|
||||||
|
coordinates: [
|
||||||
|
[0,0],
|
||||||
|
[0,0],
|
||||||
|
[0,0],
|
||||||
|
[0,0],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
case 'video': return {
|
||||||
|
type: 'video',
|
||||||
|
urls: [
|
||||||
|
`${protocol}//localhost:3000/movie.mp4`
|
||||||
|
],
|
||||||
|
coordinates: [
|
||||||
|
[0,0],
|
||||||
|
[0,0],
|
||||||
|
[0,0],
|
||||||
|
[0,0],
|
||||||
|
],
|
||||||
|
}
|
||||||
default: return {}
|
default: return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onAdd = () => {
|
||||||
|
const {source, sourceId} = this.state;
|
||||||
|
this.props.onAdd(sourceId, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChangeSource = (source) => {
|
||||||
|
this.setState({source});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
// Kind of a hack because the type changes, however maputnik has 1..n
|
||||||
|
// options per type, for example
|
||||||
|
//
|
||||||
|
// - 'geojson' - 'GeoJSON (URL)' and 'GeoJSON (JSON)'
|
||||||
|
// - 'raster' - 'Raster (TileJSON URL)' and 'Raster (XYZ URL)'
|
||||||
|
//
|
||||||
|
// So we just ignore the values entirely as they are self explanatory
|
||||||
|
const sourceTypeFieldSpec = {
|
||||||
|
doc: latest.source_vector.type.doc
|
||||||
|
};
|
||||||
|
|
||||||
return <div className="maputnik-add-source">
|
return <div className="maputnik-add-source">
|
||||||
<InputBlock label={"Source ID"} doc={"Unique ID that identifies the source and is used in the layer to reference the source."}>
|
<InputBlock label={"Source ID"} fieldSpec={{doc: "Unique ID that identifies the source and is used in the layer to reference the source."}}>
|
||||||
<StringInput
|
<StringInput
|
||||||
value={this.state.sourceId}
|
value={this.state.sourceId}
|
||||||
onChange={v => this.setState({ sourceId: v})}
|
onChange={v => this.setState({ sourceId: v})}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
<InputBlock label={"Source Type"} doc={latest.source_vector.type.doc}>
|
<InputBlock label={"Source Type"} fieldSpec={sourceTypeFieldSpec}>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
options={[
|
options={[
|
||||||
['geojson', 'GeoJSON'],
|
['geojson_json', 'GeoJSON (JSON)'],
|
||||||
|
['geojson_url', 'GeoJSON (URL)'],
|
||||||
['tilejson_vector', 'Vector (TileJSON URL)'],
|
['tilejson_vector', 'Vector (TileJSON URL)'],
|
||||||
['tilexyz_vector', 'Vector (XYZ URLs)'],
|
['tilexyz_vector', 'Vector (XYZ URLs)'],
|
||||||
['tilejson_raster', 'Raster (TileJSON URL)'],
|
['tilejson_raster', 'Raster (TileJSON URL)'],
|
||||||
['tilexyz_raster', 'Raster (XYZ URL)'],
|
['tilexyz_raster', 'Raster (XYZ URL)'],
|
||||||
['tilejson_raster-dem', 'Raster DEM (TileJSON URL)'],
|
['tilejson_raster-dem', 'Raster DEM (TileJSON URL)'],
|
||||||
['tilexyz_raster-dem', 'Raster DEM (XYZ URLs)'],
|
['tilexyz_raster-dem', 'Raster DEM (XYZ URLs)'],
|
||||||
|
['image', 'Image'],
|
||||||
|
['video', 'Video'],
|
||||||
]}
|
]}
|
||||||
onChange={mode => this.setState({mode: mode, source: this.defaultSource(mode)})}
|
onChange={mode => this.setState({mode: mode, source: this.defaultSource(mode)})}
|
||||||
value={this.state.mode}
|
value={this.state.mode}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
<SourceTypeEditor
|
<SourceTypeEditor
|
||||||
onChange={src => this.setState({ source: src })}
|
onChange={this.onChangeSource}
|
||||||
mode={this.state.mode}
|
mode={this.state.mode}
|
||||||
source={this.state.source}
|
source={this.state.source}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className="maputnik-add-source-button"
|
className="maputnik-add-source-button"
|
||||||
onClick={() => this.props.onAdd(this.state.sourceId, this.state.source)}>
|
onClick={this.onAdd}
|
||||||
|
>
|
||||||
Add Source
|
Add Source
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class SurveyModal extends React.Component {
|
|||||||
title="Maputnik Survey"
|
title="Maputnik Survey"
|
||||||
>
|
>
|
||||||
<div className="maputnik-modal-survey">
|
<div className="maputnik-modal-survey">
|
||||||
<img className="maputnik-modal-survey__logo" src={logoImage} alt="" width="128" />
|
<div className="maputnik-modal-survey__logo" dangerouslySetInnerHTML={{__html: logoImage}} />
|
||||||
<h1>You + Maputnik = Maputnik better for you</h1>
|
<h1>You + Maputnik = Maputnik better for you</h1>
|
||||||
<p className="maputnik-modal-survey__description">We don’t track you, so we don’t know how you use Maputnik. Help us make Maputnik better for you by completing a 7–minute survey carried out by our contributing designer.</p>
|
<p className="maputnik-modal-survey__description">We don’t track you, so we don’t know how you use Maputnik. Help us make Maputnik better for you by completing a 7–minute survey carried out by our contributing designer.</p>
|
||||||
<Button onClick={this.onClick} className="maputnik-big-button maputnik-white-button maputnik-wide-button">Take the Maputnik Survey</Button>
|
<Button onClick={this.onClick} className="maputnik-big-button maputnik-white-button maputnik-wide-button">Take the Maputnik Survey</Button>
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ import PropTypes from 'prop-types'
|
|||||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||||
import InputBlock from '../inputs/InputBlock'
|
import InputBlock from '../inputs/InputBlock'
|
||||||
import StringInput from '../inputs/StringInput'
|
import StringInput from '../inputs/StringInput'
|
||||||
|
import UrlInput from '../inputs/UrlInput'
|
||||||
import NumberInput from '../inputs/NumberInput'
|
import NumberInput from '../inputs/NumberInput'
|
||||||
import SelectInput from '../inputs/SelectInput'
|
import SelectInput from '../inputs/SelectInput'
|
||||||
|
import DynamicArrayInput from '../inputs/DynamicArrayInput'
|
||||||
|
import ArrayInput from '../inputs/ArrayInput'
|
||||||
|
import JSONEditor from '../layers/JSONEditor'
|
||||||
|
|
||||||
|
|
||||||
class TileJSONSourceEditor extends React.Component {
|
class TileJSONSourceEditor extends React.Component {
|
||||||
@@ -16,8 +20,8 @@ class TileJSONSourceEditor extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div>
|
return <div>
|
||||||
<InputBlock label={"TileJSON URL"} doc={latest.source_vector.url.doc}>
|
<InputBlock label={"TileJSON URL"} fieldSpec={latest.source_vector.url}>
|
||||||
<StringInput
|
<UrlInput
|
||||||
value={this.props.source.url}
|
value={this.props.source.url}
|
||||||
onChange={url => this.props.onChange({
|
onChange={url => this.props.onChange({
|
||||||
...this.props.source,
|
...this.props.source,
|
||||||
@@ -50,8 +54,8 @@ class TileURLSourceEditor extends React.Component {
|
|||||||
const prefix = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th']
|
const prefix = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th']
|
||||||
const tiles = this.props.source.tiles || []
|
const tiles = this.props.source.tiles || []
|
||||||
return tiles.map((tileUrl, tileIndex) => {
|
return tiles.map((tileUrl, tileIndex) => {
|
||||||
return <InputBlock key={tileIndex} label={prefix[tileIndex] + " Tile URL"} doc={latest.source_vector.tiles.doc}>
|
return <InputBlock key={tileIndex} label={prefix[tileIndex] + " Tile URL"} fieldSpec={latest.source_vector.tiles}>
|
||||||
<StringInput
|
<UrlInput
|
||||||
value={tileUrl}
|
value={tileUrl}
|
||||||
onChange={this.changeTileUrl.bind(this, tileIndex)}
|
onChange={this.changeTileUrl.bind(this, tileIndex)}
|
||||||
/>
|
/>
|
||||||
@@ -62,7 +66,7 @@ class TileURLSourceEditor extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
return <div>
|
return <div>
|
||||||
{this.renderTileUrls()}
|
{this.renderTileUrls()}
|
||||||
<InputBlock label={"Min Zoom"} doc={latest.source_vector.minzoom.doc}>
|
<InputBlock label={"Min Zoom"} fieldSpec={latest.source_vector.minzoom}>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={this.props.source.minzoom || 0}
|
value={this.props.source.minzoom || 0}
|
||||||
onChange={minzoom => this.props.onChange({
|
onChange={minzoom => this.props.onChange({
|
||||||
@@ -71,7 +75,7 @@ class TileURLSourceEditor extends React.Component {
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
<InputBlock label={"Max Zoom"} doc={latest.source_vector.maxzoom.doc}>
|
<InputBlock label={"Max Zoom"} fieldSpec={latest.source_vector.maxzoom}>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={this.props.source.maxzoom || 22}
|
value={this.props.source.maxzoom || 22}
|
||||||
onChange={maxzoom => this.props.onChange({
|
onChange={maxzoom => this.props.onChange({
|
||||||
@@ -86,15 +90,109 @@ class TileURLSourceEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeoJSONSourceEditor extends React.Component {
|
class ImageSourceEditor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
source: PropTypes.object.isRequired,
|
source: PropTypes.object.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <InputBlock label={"GeoJSON Data"} doc={latest.source_geojson.data.doc}>
|
const changeCoord = (idx, val) => {
|
||||||
<StringInput
|
const coordinates = this.props.source.coordinates.slice(0);
|
||||||
|
coordinates[idx] = val;
|
||||||
|
|
||||||
|
this.props.onChange({
|
||||||
|
...this.props.source,
|
||||||
|
coordinates,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<InputBlock label={"Image URL"} doc={latest.source_image.url.doc}>
|
||||||
|
<UrlInput
|
||||||
|
value={this.props.source.url}
|
||||||
|
onChange={url => this.props.onChange({
|
||||||
|
...this.props.source,
|
||||||
|
url,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
|
||||||
|
return (
|
||||||
|
<InputBlock label={`Coord ${label}`} key={label}>
|
||||||
|
<ArrayInput
|
||||||
|
length={2}
|
||||||
|
type="number"
|
||||||
|
value={this.props.source.coordinates[idx]}
|
||||||
|
default={[0, 0]}
|
||||||
|
onChange={(val) => changeCoord(idx, val)}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VideoSourceEditor extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
source: PropTypes.object.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const changeCoord = (idx, val) => {
|
||||||
|
const coordinates = this.props.source.coordinates.slice(0);
|
||||||
|
coordinates[idx] = val;
|
||||||
|
|
||||||
|
this.props.onChange({
|
||||||
|
...this.props.source,
|
||||||
|
coordinates,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeUrls = (urls) => {
|
||||||
|
this.props.onChange({
|
||||||
|
...this.props.source,
|
||||||
|
urls,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
<InputBlock label={"Video URL"} doc={latest.source_video.urls.doc}>
|
||||||
|
<DynamicArrayInput
|
||||||
|
type="string"
|
||||||
|
value={this.props.source.urls}
|
||||||
|
default={""}
|
||||||
|
onChange={changeUrls}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
|
||||||
|
return (
|
||||||
|
<InputBlock label={`Coord ${label}`} key={label}>
|
||||||
|
<ArrayInput
|
||||||
|
length={2}
|
||||||
|
type="number"
|
||||||
|
value={this.props.source.coordinates[idx]}
|
||||||
|
default={[0, 0]}
|
||||||
|
onChange={val => changeCoord(idx, val)}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GeoJSONSourceUrlEditor extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
source: PropTypes.object.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <InputBlock label={"GeoJSON URL"} fieldSpec={latest.source_geojson.data}>
|
||||||
|
<UrlInput
|
||||||
value={this.props.source.data}
|
value={this.props.source.data}
|
||||||
onChange={data => this.props.onChange({
|
onChange={data => this.props.onChange({
|
||||||
...this.props.source,
|
...this.props.source,
|
||||||
@@ -105,6 +203,28 @@ class GeoJSONSourceEditor extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class GeoJSONSourceJSONEditor extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
source: PropTypes.object.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <InputBlock label={"GeoJSON"} fieldSpec={latest.source_geojson.data}>
|
||||||
|
<JSONEditor
|
||||||
|
layer={this.props.source.data}
|
||||||
|
maxHeight={200}
|
||||||
|
onChange={data => {
|
||||||
|
this.props.onChange({
|
||||||
|
...this.props.source,
|
||||||
|
data,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</InputBlock>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class SourceTypeEditor extends React.Component {
|
class SourceTypeEditor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
mode: PropTypes.string.isRequired,
|
mode: PropTypes.string.isRequired,
|
||||||
@@ -118,14 +238,15 @@ class SourceTypeEditor extends React.Component {
|
|||||||
onChange: this.props.onChange,
|
onChange: this.props.onChange,
|
||||||
}
|
}
|
||||||
switch(this.props.mode) {
|
switch(this.props.mode) {
|
||||||
case 'geojson': return <GeoJSONSourceEditor {...commonProps} />
|
case 'geojson_url': return <GeoJSONSourceUrlEditor {...commonProps} />
|
||||||
|
case 'geojson_json': return <GeoJSONSourceJSONEditor {...commonProps} />
|
||||||
case 'tilejson_vector': return <TileJSONSourceEditor {...commonProps} />
|
case 'tilejson_vector': return <TileJSONSourceEditor {...commonProps} />
|
||||||
case 'tilexyz_vector': return <TileURLSourceEditor {...commonProps} />
|
case 'tilexyz_vector': return <TileURLSourceEditor {...commonProps} />
|
||||||
case 'tilejson_raster': return <TileJSONSourceEditor {...commonProps} />
|
case 'tilejson_raster': return <TileJSONSourceEditor {...commonProps} />
|
||||||
case 'tilexyz_raster': return <TileURLSourceEditor {...commonProps} />
|
case 'tilexyz_raster': return <TileURLSourceEditor {...commonProps} />
|
||||||
case 'tilejson_raster-dem': return <TileJSONSourceEditor {...commonProps} />
|
case 'tilejson_raster-dem': return <TileJSONSourceEditor {...commonProps} />
|
||||||
case 'tilexyz_raster-dem': return <TileURLSourceEditor {...commonProps}>
|
case 'tilexyz_raster-dem': return <TileURLSourceEditor {...commonProps}>
|
||||||
<InputBlock label={"Encoding"} doc={latest.source_raster_dem.encoding.doc}>
|
<InputBlock label={"Encoding"} fieldSpec={latest.source_raster_dem.encoding}>
|
||||||
<SelectInput
|
<SelectInput
|
||||||
options={Object.keys(latest.source_raster_dem.encoding.values)}
|
options={Object.keys(latest.source_raster_dem.encoding.values)}
|
||||||
onChange={encoding => this.props.onChange({
|
onChange={encoding => this.props.onChange({
|
||||||
@@ -136,6 +257,8 @@ class SourceTypeEditor extends React.Component {
|
|||||||
/>
|
/>
|
||||||
</InputBlock>
|
</InputBlock>
|
||||||
</TileURLSourceEditor>
|
</TileURLSourceEditor>
|
||||||
|
case 'image': return <ImageSourceEditor {...commonProps} />
|
||||||
|
case 'video': return <VideoSourceEditor {...commonProps} />
|
||||||
default: return null
|
default: return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/components/util/SmallError.jsx
Normal file
20
src/components/util/SmallError.jsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import './SmallError.scss';
|
||||||
|
|
||||||
|
|
||||||
|
class SmallError extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div className="SmallError">
|
||||||
|
Error: {this.props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SmallError
|
||||||
7
src/components/util/SmallError.scss
Normal file
7
src/components/util/SmallError.scss
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
@import '../../styles/vars';
|
||||||
|
|
||||||
|
.SmallError {
|
||||||
|
color: #E57373;
|
||||||
|
font-size: $font-size-6;
|
||||||
|
margin-top: $margin-2
|
||||||
|
}
|
||||||
154
src/components/util/codemirror-mgl.js
Normal file
154
src/components/util/codemirror-mgl.js
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import jsonlint from 'jsonlint';
|
||||||
|
import CodeMirror from 'codemirror';
|
||||||
|
import jsonToAst from 'json-to-ast';
|
||||||
|
import {expression, validate, latest} from '@mapbox/mapbox-gl-style-spec';
|
||||||
|
|
||||||
|
|
||||||
|
CodeMirror.defineMode("mgl", function(config, parserConfig) {
|
||||||
|
// Just using the javascript mode with json enabled. Our logic is in the linter below.
|
||||||
|
return CodeMirror.modes.javascript(
|
||||||
|
{...config, json: true},
|
||||||
|
parserConfig
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
CodeMirror.registerHelper("lint", "mgl", function(text, opts, doc) {
|
||||||
|
const found = [];
|
||||||
|
const {parser} = jsonlint;
|
||||||
|
const {context} = opts;
|
||||||
|
|
||||||
|
parser.parseError = function(str, hash) {
|
||||||
|
const loc = hash.loc;
|
||||||
|
found.push({
|
||||||
|
from: CodeMirror.Pos(loc.first_line - 1, loc.first_column),
|
||||||
|
to: CodeMirror.Pos(loc.last_line - 1, loc.last_column),
|
||||||
|
message: str
|
||||||
|
});
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
parser.parse(text);
|
||||||
|
}
|
||||||
|
catch (e) {}
|
||||||
|
|
||||||
|
if (found.length > 0) {
|
||||||
|
// JSON invalid so don't go any further
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ast = jsonToAst(text);
|
||||||
|
const input = JSON.parse(text);
|
||||||
|
|
||||||
|
function getArrayPositionalFromAst (node, path) {
|
||||||
|
if (!node) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
else if (path.length < 1) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
else if (!node.children) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const key = path[0];
|
||||||
|
let newNode;
|
||||||
|
if (key.match(/^[0-9]+$/)) {
|
||||||
|
newNode = node.children[path[0]];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
newNode = node.children.find(childNode => {
|
||||||
|
return (
|
||||||
|
childNode.key &&
|
||||||
|
childNode.key.type === "Identifier" &&
|
||||||
|
childNode.key.value === key
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (newNode) {
|
||||||
|
newNode = newNode.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getArrayPositionalFromAst(newNode, path.slice(1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let out;
|
||||||
|
if (context === "layer") {
|
||||||
|
// Just an empty style so we can validate a layer.
|
||||||
|
const errors = validate({
|
||||||
|
"version": 8,
|
||||||
|
"name": "Empty Style",
|
||||||
|
"metadata": {},
|
||||||
|
"sources": {},
|
||||||
|
"sprite": "",
|
||||||
|
"glyphs": "https://example.com/glyphs/{fontstack}/{range}.pbf",
|
||||||
|
"layers": [
|
||||||
|
input
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errors) {
|
||||||
|
out = {
|
||||||
|
result: "error",
|
||||||
|
value: errors
|
||||||
|
.filter(err => {
|
||||||
|
// Remove missing 'layer source' errors, because we don't include them
|
||||||
|
if (err.message.match(/^layers\[0\]: source ".*" not found$/)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(err => {
|
||||||
|
// Remove the 'layers[0].' as we're validating the layer only here
|
||||||
|
const errMessageParts = err.message.replace(/^layers\[0\]./, "").split(":");
|
||||||
|
return {
|
||||||
|
key: errMessageParts[0],
|
||||||
|
message: errMessageParts[1],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (context === "expression") {
|
||||||
|
out = expression.createExpression(input, opts.spec);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error(`Invalid context ${context}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out.result === "error") {
|
||||||
|
const errors = out.value;
|
||||||
|
errors.forEach(error => {
|
||||||
|
const {key, message} = error;
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
const lastLineHandle = doc.getLineHandle(doc.lastLine());
|
||||||
|
const err = {
|
||||||
|
from: CodeMirror.Pos(doc.firstLine(), 0),
|
||||||
|
to: CodeMirror.Pos(doc.lastLine(), lastLineHandle.text.length),
|
||||||
|
message: message,
|
||||||
|
}
|
||||||
|
found.push(err);
|
||||||
|
}
|
||||||
|
else if (key) {
|
||||||
|
const path = key.replace(/^\[|\]$/g, "").split(/\.|[\[\]]+/).filter(Boolean)
|
||||||
|
const parsedError = getArrayPositionalFromAst(ast, path);
|
||||||
|
if (!parsedError) {
|
||||||
|
console.warn("Something went wrong parsing error:", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {loc} = parsedError;
|
||||||
|
const {start, end} = loc;
|
||||||
|
|
||||||
|
found.push({
|
||||||
|
from: CodeMirror.Pos(start.line - 1, start.column),
|
||||||
|
to: CodeMirror.Pos(end.line - 1, end.column),
|
||||||
|
message: message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return found;
|
||||||
|
});
|
||||||
@@ -128,11 +128,14 @@
|
|||||||
"text-justify",
|
"text-justify",
|
||||||
"text-anchor",
|
"text-anchor",
|
||||||
"text-max-angle",
|
"text-max-angle",
|
||||||
|
"text-writing-mode",
|
||||||
"text-rotate",
|
"text-rotate",
|
||||||
"text-keep-upright",
|
"text-keep-upright",
|
||||||
"text-transform",
|
"text-transform",
|
||||||
"text-offset",
|
"text-offset",
|
||||||
"text-optional"
|
"text-optional",
|
||||||
|
"text-variable-anchor",
|
||||||
|
"text-radial-offset"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -230,5 +233,8 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"invalid": {
|
||||||
|
"groups": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +1,62 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"id": "osm-liberty",
|
||||||
|
"title": "OSM Liberty",
|
||||||
|
"url": "https://maputnik.github.io/osm-liberty/style.json",
|
||||||
|
"thumbnail": "https://maputnik.github.io/thumbnails/osm-liberty.png"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "klokantech-basic",
|
"id": "klokantech-basic",
|
||||||
"title": "Klokantech Basic",
|
"title": "Klokantech Basic",
|
||||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/klokantech-basic-gl-style@v1.8/style.json",
|
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/klokantech-basic-gl-style@v1.9/style.json",
|
||||||
"thumbnail": "https://maputnik.github.io/thumbnails/klokantech-basic.png"
|
"thumbnail": "https://maputnik.github.io/thumbnails/klokantech-basic.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "dark-matter",
|
"id": "dark-matter",
|
||||||
"title": "Dark Matter",
|
"title": "Dark Matter",
|
||||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/dark-matter-gl-style@v1.7/style.json",
|
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/dark-matter-gl-style@v1.8/style.json",
|
||||||
"thumbnail": "https://maputnik.github.io/thumbnails/dark-matter.png"
|
"thumbnail": "https://maputnik.github.io/thumbnails/dark-matter.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "positron",
|
"id": "positron",
|
||||||
"title": "Positron",
|
"title": "Positron",
|
||||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/positron-gl-style@v1.7/style.json",
|
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/positron-gl-style@v1.8/style.json",
|
||||||
"thumbnail": "https://maputnik.github.io/thumbnails/positron.png"
|
"thumbnail": "https://maputnik.github.io/thumbnails/positron.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "osm-bright",
|
"id": "osm-bright",
|
||||||
"title": "OSM Bright",
|
"title": "OSM Bright",
|
||||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/osm-bright-gl-style@v1.8/style.json",
|
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/osm-bright-gl-style@v1.9/style.json",
|
||||||
"thumbnail": "https://maputnik.github.io/thumbnails/osm-bright.png"
|
"thumbnail": "https://maputnik.github.io/thumbnails/osm-bright.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "toner-gl-style",
|
"id": "toner-gl-style",
|
||||||
"title": "Toner",
|
"title": "Toner",
|
||||||
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/toner-gl-style@c289223/style.json",
|
"url": "https://cdn.jsdelivr.net/gh/openmaptiles/toner-gl-style@dcb6e64/style.json",
|
||||||
"thumbnail": "https://maputnik.github.io/thumbnails/toner.png"
|
"thumbnail": "https://maputnik.github.io/thumbnails/toner.png"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "osm-liberty",
|
|
||||||
"title": "OSM Liberty",
|
|
||||||
"url": "https://maputnik.github.io/osm-liberty/style.json",
|
|
||||||
"thumbnail": "https://maputnik.github.io/osm-liberty/thumbnail.png"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "os-zoomstack-outdoor",
|
"id": "os-zoomstack-outdoor",
|
||||||
"title": "Zoomstack Outdoor",
|
"title": "Zoomstack Outdoor",
|
||||||
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/styles/open-zoomstack-outdoor/style.json",
|
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-outdoor/style.json",
|
||||||
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-outdoor.png"
|
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-outdoor.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "os-zoomstack-road",
|
"id": "os-zoomstack-road",
|
||||||
"title": "Zoomstack Road",
|
"title": "Zoomstack Road",
|
||||||
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/styles/open-zoomstack-road/style.json",
|
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-road/style.json",
|
||||||
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-road.png"
|
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-road.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "os-zoomstack-light",
|
"id": "os-zoomstack-light",
|
||||||
"title": "Zoomstack Light",
|
"title": "Zoomstack Light",
|
||||||
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/styles/open-zoomstack-light/style.json",
|
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-light/style.json",
|
||||||
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-light.png"
|
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-light.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "os-zoomstack-night",
|
"id": "os-zoomstack-night",
|
||||||
"title": "Zoomstack Night",
|
"title": "Zoomstack Night",
|
||||||
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/styles/open-zoomstack-night/style.json",
|
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/styles/open-zoomstack-night/style.json",
|
||||||
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-night.png"
|
"thumbnail": "https://maputnik.github.io/thumbnails/os-zoomstack-night.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
},
|
},
|
||||||
"open_zoomstack": {
|
"open_zoomstack": {
|
||||||
"type": "vector",
|
"type": "vector",
|
||||||
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/data/vector/open-zoomstack/config.json",
|
"url": "https://s3-eu-west-1.amazonaws.com/tiles.os.uk/v2/data/vector/open-zoomstack/config.json",
|
||||||
"title": "OS Open Zoomstack"
|
"title": "OS Open Zoomstack"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,3 +12,6 @@ ReactDOM.render(
|
|||||||
</IconContext.Provider>,
|
</IconContext.Provider>,
|
||||||
document.querySelector("#app")
|
document.querySelector("#app")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Hide the loader.
|
||||||
|
document.querySelector(".loading").style.display = "none";
|
||||||
|
|||||||
22
src/libs/field-spec-additional.js
Normal file
22
src/libs/field-spec-additional.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const spec = {
|
||||||
|
maputnik: {
|
||||||
|
mapbox_access_token: {
|
||||||
|
label: "Mapbox Access Token",
|
||||||
|
doc: "Public access token for Mapbox services."
|
||||||
|
},
|
||||||
|
maptiler_access_token: {
|
||||||
|
label: "MapTiler Access Token",
|
||||||
|
doc: "Public access token for MapTiler Cloud."
|
||||||
|
},
|
||||||
|
thunderforest_access_token: {
|
||||||
|
label: "Thunderforest Access Token",
|
||||||
|
doc: "Public access token for Thunderforest services."
|
||||||
|
},
|
||||||
|
style_renderer: {
|
||||||
|
label: "Style Renderer",
|
||||||
|
doc: "Choose the default Maputnik renderer for this style.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default spec;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import MapboxGl from 'mapbox-gl'
|
import MapboxGl from 'mapbox-gl'
|
||||||
|
import {readFileSync} from 'fs'
|
||||||
|
|
||||||
// Load mapbox-gl-rtl-text using object urls without needing http://localhost for AJAX.
|
const data = readFileSync(__dirname+"/../../node_modules/@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.js", "utf8");
|
||||||
const data = require("raw-loader?mimetype=text/javascript!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.js");
|
|
||||||
|
|
||||||
const blob = new window.Blob([data], {
|
const blob = new window.Blob([data], {
|
||||||
type: "text/javascript"
|
type: "text/javascript"
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ export default class ZoomControl {
|
|||||||
this._map = map;
|
this._map = map;
|
||||||
this._container = document.createElement('div');
|
this._container = document.createElement('div');
|
||||||
this._container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group mapboxgl-ctrl-zoom';
|
this._container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group mapboxgl-ctrl-zoom';
|
||||||
|
this._container.innerHTML = `
|
||||||
|
Zoom: <span></span>
|
||||||
|
`;
|
||||||
|
this._textEl = this._container.querySelector("span");
|
||||||
|
|
||||||
this.addEventListeners();
|
this.addEventListeners();
|
||||||
|
|
||||||
@@ -10,7 +14,7 @@ export default class ZoomControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateZoomLevel() {
|
updateZoomLevel() {
|
||||||
this._container.innerHTML = `Zoom level: ${this._map.getZoom().toFixed(2)}`;
|
this._textEl.innerHTML = this._map.getZoom().toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
addEventListeners (){
|
addEventListeners (){
|
||||||
|
|||||||
@@ -32,7 +32,6 @@
|
|||||||
|
|
||||||
.mapboxgl-popup-content {
|
.mapboxgl-popup-content {
|
||||||
background-color: rgb(28, 31, 36);
|
background-color: rgb(28, 31, 36);
|
||||||
border-radius: 0px;
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.298039) 0px 0px 5px 0px;
|
box-shadow: rgba(0, 0, 0, 0.298039) 0px 0px 5px 0px;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
@@ -57,20 +56,29 @@
|
|||||||
border-color: rgb(28, 31, 36);
|
border-color: rgb(28, 31, 36);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapboxgl-ctrl-group > button:hover {
|
.mapboxgl-ctrl button:not(:disabled):hover {
|
||||||
background-color: rgb(86, 83, 83);
|
background-color: rgb(86, 83, 83);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-in {
|
.mapboxgl-ctrl-zoom-in {
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl-compass {
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-in .mapboxgl-ctrl-icon {
|
||||||
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%270%200%2020%2020%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%0A%20%20%3Cpath%20style%3D%27fill%3A%23a4a4a4%3B%27%20d%3D%27M%2010%206%20C%209.446%206%209%206.4459904%209%207%20L%209%209%20L%207%209%20C%206.446%209%206%209.446%206%2010%20C%206%2010.554%206.446%2011%207%2011%20L%209%2011%20L%209%2013%20C%209%2013.55401%209.446%2014%2010%2014%20C%2010.554%2014%2011%2013.55401%2011%2013%20L%2011%2011%20L%2013%2011%20C%2013.554%2011%2014%2010.554%2014%2010%20C%2014%209.446%2013.554%209%2013%209%20L%2011%209%20L%2011%207%20C%2011%206.4459904%2010.554%206%2010%206%20z%27%20%2F%3E%0A%3C%2Fsvg%3E%0A")
|
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%270%200%2020%2020%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%0A%20%20%3Cpath%20style%3D%27fill%3A%23a4a4a4%3B%27%20d%3D%27M%2010%206%20C%209.446%206%209%206.4459904%209%207%20L%209%209%20L%207%209%20C%206.446%209%206%209.446%206%2010%20C%206%2010.554%206.446%2011%207%2011%20L%209%2011%20L%209%2013%20C%209%2013.55401%209.446%2014%2010%2014%20C%2010.554%2014%2011%2013.55401%2011%2013%20L%2011%2011%20L%2013%2011%20C%2013.554%2011%2014%2010.554%2014%2010%20C%2014%209.446%2013.554%209%2013%209%20L%2011%209%20L%2011%207%20C%2011%206.4459904%2010.554%206%2010%206%20z%27%20%2F%3E%0A%3C%2Fsvg%3E%0A")
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapboxgl-ctrl-icon.mapboxgl-ctrl-zoom-out {
|
.mapboxgl-ctrl button.mapboxgl-ctrl-zoom-out .mapboxgl-ctrl-icon {
|
||||||
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%270%200%2020%2020%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%0A%20%20%3Cpath%20style%3D%27fill%3A%23a4a4a4%3B%27%20d%3D%27m%207%2C9%20c%20-0.554%2C0%20-1%2C0.446%20-1%2C1%200%2C0.554%200.446%2C1%201%2C1%20l%206%2C0%20c%200.554%2C0%201%2C-0.446%201%2C-1%200%2C-0.554%20-0.446%2C-1%20-1%2C-1%20z%27%20%2F%3E%0A%3C%2Fsvg%3E%0A")
|
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20viewBox%3D%270%200%2020%2020%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%0A%20%20%3Cpath%20style%3D%27fill%3A%23a4a4a4%3B%27%20d%3D%27m%207%2C9%20c%20-0.554%2C0%20-1%2C0.446%20-1%2C1%200%2C0.554%200.446%2C1%201%2C1%20l%206%2C0%20c%200.554%2C0%201%2C-0.446%201%2C-1%200%2C-0.554%20-0.446%2C-1%20-1%2C-1%20z%27%20%2F%3E%0A%3C%2Fsvg%3E%0A")
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapboxgl-ctrl-icon.mapboxgl-ctrl-compass > .mapboxgl-ctrl-compass-arrow {
|
.mapboxgl-ctrl button.mapboxgl-ctrl-compass .mapboxgl-ctrl-icon {
|
||||||
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2020%2020%27%3E%0A%09%3Cpolygon%20fill%3D%27%23a4a4a4%27%20points%3D%276%2C9%2010%2C1%2014%2C9%27%2F%3E%0A%09%3Cpolygon%20fill%3D%27%23f0f0f0%27%20points%3D%276%2C11%2010%2C19%2014%2C11%20%27%2F%3E%0A%3C%2Fsvg%3E")
|
background-image: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2020%2020%27%3E%0A%09%3Cpolygon%20fill%3D%27%23a4a4a4%27%20points%3D%276%2C9%2010%2C1%2014%2C9%27%2F%3E%0A%09%3Cpolygon%20fill%3D%27%23545454%27%20points%3D%276%2C11%2010%2C19%2014%2C11%20%27%2F%3E%0A%3C%2Fsvg%3E");
|
||||||
|
background-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mapboxgl-ctrl-inspect {
|
.mapboxgl-ctrl-inspect {
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
|
.CodeMirror-lint-tooltip {
|
||||||
|
z-index: 2000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codemirror-container {
|
||||||
|
max-width: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.cm-s-maputnik.CodeMirror {
|
.cm-s-maputnik.CodeMirror {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-s-maputnik.CodeMirror, .cm-s-maputnik .CodeMirror-gutters {
|
.cm-s-maputnik.CodeMirror, .cm-s-maputnik .CodeMirror-gutters {
|
||||||
@@ -8,16 +19,12 @@
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-s-maputnik.CodeMirror {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-maputnik .CodeMirror-gutters {
|
.cm-s-maputnik .CodeMirror-gutters {
|
||||||
background: #212328;
|
background: #212328;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-s-maputnik .CodeMirror-cursor {
|
.cm-s-maputnik .CodeMirror-cursor {
|
||||||
border-left: solid thin #8e8e8e !important;
|
border-left: solid thin #f0f0f0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-s-maputnik.CodeMirror-focused div.CodeMirror-selected {
|
.cm-s-maputnik.CodeMirror-focused div.CodeMirror-selected {
|
||||||
@@ -47,5 +54,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cm-s-maputnik .CodeMirror-matchingbracket {
|
.cm-s-maputnik .CodeMirror-matchingbracket {
|
||||||
text-decoration: underline; color: white !important;
|
background: hsla(223, 12%, 35%, 1);
|
||||||
|
color: $color-white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-s-maputnik .CodeMirror-nonmatchingbracket {
|
||||||
|
background-color: #bb0000;
|
||||||
|
color: white !important;
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,13 @@
|
|||||||
// MAP
|
// MAP
|
||||||
.maputnik-map__container {
|
.maputnik-map__container {
|
||||||
|
background: white;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
top: $toolbar-height + $toolbar-offset;
|
top: $toolbar-height + $toolbar-offset;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
height: calc(100% - #{$toolbar-height + $toolbar-offset});
|
height: calc(100% - #{$toolbar-height + $toolbar-offset});
|
||||||
width: calc(
|
width: $layout-map-width;
|
||||||
100%
|
|
||||||
- 200px /* layer list */
|
|
||||||
- 350px /* layer editor */
|
|
||||||
);
|
|
||||||
|
|
||||||
&--error {
|
&--error {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -28,6 +25,14 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maputnik-input-block:hover,
|
||||||
|
.maputnik-filter-editor-compound-select:hover {
|
||||||
|
.maputnik-doc-button {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DOC LABEL
|
// DOC LABEL
|
||||||
.maputnik-doc {
|
.maputnik-doc {
|
||||||
&-target {
|
&-target {
|
||||||
@@ -57,6 +62,33 @@
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-button {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background: $color-black;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--open {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-doc-inline {
|
||||||
|
color: $color-lowgray;
|
||||||
|
background-color: $color-gray;
|
||||||
|
padding: $margin-2;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: $margin-3;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-doc-target:hover .maputnik-doc-popup {
|
.maputnik-doc-target:hover .maputnik-doc-popup {
|
||||||
@@ -114,7 +146,7 @@
|
|||||||
.maputnik-icon-button {
|
.maputnik-icon-button {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
||||||
&:hover {
|
&:hover:not(:disabled) {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
||||||
label,
|
label,
|
||||||
@@ -138,6 +170,7 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-content {
|
&-content {
|
||||||
@@ -149,18 +182,26 @@
|
|||||||
.maputnik-action-block {
|
.maputnik-action-block {
|
||||||
.maputnik-input-block-label {
|
.maputnik-input-block-label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 35%;
|
width: 32%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-input-block-action {
|
.maputnik-input-block-action {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 15%;
|
width: 18%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-input-block-action > div {
|
.maputnik-input-block-action > div {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-data-spec-block,
|
||||||
|
.maputnik-zoom-spec-property {
|
||||||
|
.maputnik-inline-error {
|
||||||
|
margin-left: 32%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SPACE HELPER
|
// SPACE HELPER
|
||||||
@@ -175,6 +216,12 @@
|
|||||||
&-error {
|
&-error {
|
||||||
color: $color-red;
|
color: $color-red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__switch-button {
|
||||||
|
all: unset;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-dialog {
|
.maputnik-dialog {
|
||||||
@@ -205,3 +252,51 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maputnik-inline-error {
|
||||||
|
color: #a4a4a4;
|
||||||
|
padding: 0.4em 0.4em;
|
||||||
|
font-size: 0.9em;
|
||||||
|
border: solid 1px $color-red;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: $margin-2 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-expression-editor {
|
||||||
|
border: solid 1px $color-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-input-block--wide {
|
||||||
|
.maputnik-input-block-content {
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-input-block-label {
|
||||||
|
width: 82%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-input-block-action {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-expr-infobox {
|
||||||
|
font-size: $font-size-6;
|
||||||
|
background: $color-midgray;
|
||||||
|
padding: $margin-2;
|
||||||
|
border-radius: 2px;
|
||||||
|
border-top-right-radius: 0px;
|
||||||
|
border-top-left-radius: 0px;
|
||||||
|
color: $color-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-expr-infobox__button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
text-decoration: underline;
|
||||||
|
color: currentColor;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
.maputnik-filter-editor-wrapper {
|
.maputnik-filter-editor-wrapper {
|
||||||
padding: $margin-3;
|
padding: $margin-3;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.maputnik-input-block {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-filter-editor {
|
.maputnik-filter-editor {
|
||||||
|
|||||||
@@ -11,6 +11,11 @@
|
|||||||
border: none;
|
border: none;
|
||||||
background-color: $color-gray;
|
background-color: $color-gray;
|
||||||
color: lighten($color-lowgray, 12);
|
color: lighten($color-lowgray, 12);
|
||||||
|
|
||||||
|
&:invalid {
|
||||||
|
border: solid 1px #B71C1C;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-string {
|
.maputnik-string {
|
||||||
@@ -20,6 +25,21 @@
|
|||||||
resize: vertical;
|
resize: vertical;
|
||||||
height: 78px;
|
height: 78px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--disabled {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-number-container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-number-range {
|
||||||
|
width: calc(100% - 4.5em);
|
||||||
|
margin-right: 0.5em;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-number {
|
.maputnik-number {
|
||||||
@@ -173,3 +193,35 @@
|
|||||||
margin-bottom: $margin-3;
|
margin-bottom: $margin-3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maputnik-input-block-content {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SpecDoc__sdk-support {
|
||||||
|
position: relative;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SpecDoc__sdk-support__table {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: $margin-3;
|
||||||
|
|
||||||
|
td, th {
|
||||||
|
border: solid 1px $color-midgray;
|
||||||
|
padding: 4px 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.SpecDoc__values li {
|
||||||
|
margin-top: $margin-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SpecDoc__values code {
|
||||||
|
background: $color-midgray;
|
||||||
|
padding: 0.1em 0.3em;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -99,6 +99,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.maputnik-layer-list-item--error {
|
||||||
|
color: $color-red;
|
||||||
|
}
|
||||||
|
|
||||||
&-item-selected {
|
&-item-selected {
|
||||||
color: $color-white;
|
color: $color-white;
|
||||||
}
|
}
|
||||||
@@ -158,10 +163,39 @@
|
|||||||
color: $color-white;
|
color: $color-white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
padding: $margin-2;
|
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
border-top: solid 1px #36383e;
|
border-top: solid 1px #36383e;
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
padding: $margin-2;
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon--up {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon--down {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button[aria-expanded="true"] {
|
||||||
|
.maputnik-layer-editor-group__button__icon--up {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-layer-editor-group__button__icon--down {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@include flex-row;
|
@include flex-row;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@@ -32,17 +32,16 @@
|
|||||||
top: $toolbar-height + $toolbar-offset;
|
top: $toolbar-height + $toolbar-offset;
|
||||||
left: 200px;
|
left: 200px;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 350px;
|
width: 370px;
|
||||||
background-color: $color-black;
|
background-color: $color-black;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-bottom {
|
&-bottom {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
height: 50px;
|
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 550px;
|
right: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 100%;
|
width: $layout-map-width;
|
||||||
background-color: $color-black;
|
background-color: $color-black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,11 @@
|
|||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ol-attribution a {
|
||||||
|
color: rgba(0, 0, 0, 0.75);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.ol-control {
|
.ol-control {
|
||||||
button {
|
button {
|
||||||
background-color: rgb(28, 31, 36);
|
background-color: rgb(28, 31, 36);
|
||||||
|
|||||||
@@ -108,6 +108,16 @@
|
|||||||
background-color: $color-midgray;
|
background-color: $color-midgray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maputnik-add-modal {
|
||||||
|
width: 400px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-export-modal {
|
||||||
|
width: 400px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.maputnik-add-layer {
|
.maputnik-add-layer {
|
||||||
@extend .clearfix;
|
@extend .clearfix;
|
||||||
}
|
}
|
||||||
@@ -180,10 +190,26 @@
|
|||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
padding: $margin-2;
|
padding: $margin-2;
|
||||||
|
|
||||||
|
.maputnik-input-block-label {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-input-block-content {
|
||||||
|
width: 70%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-add-source {
|
.maputnik-add-source {
|
||||||
@extend .clearfix;
|
@extend .clearfix;
|
||||||
|
|
||||||
|
.maputnik-input-block-label {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-input-block-content {
|
||||||
|
width: 70%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-add-source-button {
|
.maputnik-add-source-button {
|
||||||
@@ -254,6 +280,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
height: 128px;
|
height: 128px;
|
||||||
|
width: 128px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-modal-survey__description {
|
.maputnik-modal-survey__description {
|
||||||
@@ -264,3 +291,7 @@
|
|||||||
color: $color-green;
|
color: $color-green;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-settings {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
.react-codemirror2 {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
@@ -7,3 +7,7 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ReactCollapse--collapse {
|
||||||
|
transition: height 180ms;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
div:not(.maputnik-toolbar__actions) {
|
div:not(.maputnik-toolbar__actions) {
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
background-color: #26282e;
|
background-color: #26282e;
|
||||||
width: 5px;
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
line-height: 26px;
|
line-height: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
svg {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
padding-right: $margin-2;
|
padding-right: $margin-2;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
|||||||
27
src/styles/_vars.scss
Normal file
27
src/styles/_vars.scss
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
$color-black: #191b20;
|
||||||
|
$color-gray: #222429;
|
||||||
|
$color-midgray: #303237;
|
||||||
|
$color-lowgray: #a4a4a4;
|
||||||
|
$color-white: #f0f0f0;
|
||||||
|
$color-red: #cf4a4a;
|
||||||
|
$color-green: #53b972;
|
||||||
|
$margin-1: 3px;
|
||||||
|
$margin-2: 5px;
|
||||||
|
$margin-3: 10px;
|
||||||
|
$margin-4: 30px;
|
||||||
|
$margin-5: 40px;
|
||||||
|
$font-size-1: 24px;
|
||||||
|
$font-size-2: 20px;
|
||||||
|
$font-size-3: 18px;
|
||||||
|
$font-size-4: 16px;
|
||||||
|
$font-size-5: 14px;
|
||||||
|
$font-size-6: 12px;
|
||||||
|
$font-family: Roboto, sans-serif;
|
||||||
|
|
||||||
|
$toolbar-height: 40px;
|
||||||
|
$toolbar-offset: 0;
|
||||||
|
|
||||||
|
$layout-list-width: 200px;
|
||||||
|
$layout-editor-width: 370px;
|
||||||
|
$layout-map-width: calc(100% - #{$layout-list-width + $layout-editor-width});
|
||||||
|
|
||||||
@@ -2,9 +2,8 @@
|
|||||||
.maputnik-make-zoom-function {
|
.maputnik-make-zoom-function {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding-bottom: 0;
|
|
||||||
padding-top: 0;
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
padding: 0 $margin-2 0 0;
|
||||||
|
|
||||||
@extend .maputnik-icon-button;
|
@extend .maputnik-icon-button;
|
||||||
}
|
}
|
||||||
@@ -45,17 +44,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-delete-stop {
|
.maputnik-delete-stop {
|
||||||
|
display: inline-block;
|
||||||
|
padding-bottom: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
@extend .maputnik-icon-button;
|
@extend .maputnik-icon-button;
|
||||||
|
|
||||||
vertical-align: top;
|
|
||||||
|
|
||||||
.maputnik-doc-wrapper {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.maputnik-doc-target {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-add-stop {
|
.maputnik-add-stop {
|
||||||
@@ -68,9 +62,8 @@
|
|||||||
.maputnik-make-data-function {
|
.maputnik-make-data-function {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding-bottom: 0;
|
|
||||||
padding-top: 0;
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
padding: 0 $margin-2 0 0;
|
||||||
|
|
||||||
@extend .maputnik-icon-button;
|
@extend .maputnik-icon-button;
|
||||||
}
|
}
|
||||||
@@ -103,10 +96,6 @@
|
|||||||
.maputnik-data-spec-property-input {
|
.maputnik-data-spec-property-input {
|
||||||
width: 75%;
|
width: 75%;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
.maputnik-string {
|
|
||||||
margin-bottom: 3%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,4 @@
|
|||||||
$color-black: #191b20;
|
@import 'vars';
|
||||||
$color-gray: #222429;
|
|
||||||
$color-midgray: #303237;
|
|
||||||
$color-lowgray: #a4a4a4;
|
|
||||||
$color-white: #f0f0f0;
|
|
||||||
$color-red: #cf4a4a;
|
|
||||||
$color-green: #53b972;
|
|
||||||
$margin-1: 3px;
|
|
||||||
$margin-2: 5px;
|
|
||||||
$margin-3: 10px;
|
|
||||||
$margin-4: 30px;
|
|
||||||
$margin-5: 40px;
|
|
||||||
$font-size-1: 24px;
|
|
||||||
$font-size-2: 20px;
|
|
||||||
$font-size-3: 18px;
|
|
||||||
$font-size-4: 16px;
|
|
||||||
$font-size-5: 14px;
|
|
||||||
$font-size-6: 12px;
|
|
||||||
$font-family: Roboto, sans-serif;
|
|
||||||
|
|
||||||
$toolbar-height: 40px;
|
|
||||||
$toolbar-offset: 0;
|
|
||||||
|
|
||||||
@import 'mixins';
|
@import 'mixins';
|
||||||
@import 'reset';
|
@import 'reset';
|
||||||
@import 'base';
|
@import 'base';
|
||||||
@@ -37,8 +15,8 @@ $toolbar-offset: 0;
|
|||||||
@import 'zoomproperty';
|
@import 'zoomproperty';
|
||||||
@import 'popup';
|
@import 'popup';
|
||||||
@import 'map';
|
@import 'map';
|
||||||
|
@import 'codemirror';
|
||||||
@import 'react-collapse';
|
@import 'react-collapse';
|
||||||
@import 'react-codemirror';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hacks for webdriverio isVisibleWithinViewport
|
* Hacks for webdriverio isVisibleWithinViewport
|
||||||
@@ -56,3 +34,4 @@ $toolbar-offset: 0;
|
|||||||
width: 14px;
|
width: 14px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,63 +9,32 @@
|
|||||||
html {
|
html {
|
||||||
background-color: rgb(28, 31, 36);
|
background-color: rgb(28, 31, 36);
|
||||||
}
|
}
|
||||||
#loader,
|
|
||||||
#loader:before,
|
.loading {
|
||||||
#loader:after {
|
text-align: center;
|
||||||
border-radius: 50%;
|
|
||||||
width: 2.5em;
|
|
||||||
height: 2.5em;
|
|
||||||
-webkit-animation-fill-mode: both;
|
|
||||||
animation-fill-mode: both;
|
|
||||||
-webkit-animation: pulseload 1.8s infinite ease-in-out;
|
|
||||||
animation: pulseload 1.8s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
#loader {
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 10px;
|
|
||||||
margin: 80px auto;
|
|
||||||
position: relative;
|
|
||||||
text-indent: -9999em;
|
|
||||||
-webkit-transform: translateZ(0);
|
|
||||||
-ms-transform: translateZ(0);
|
|
||||||
transform: translateZ(0);
|
|
||||||
-webkit-animation-delay: -0.16s;
|
|
||||||
animation-delay: -0.16s;
|
|
||||||
}
|
|
||||||
#loader:before,
|
|
||||||
#loader:after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
#loader:before {
|
|
||||||
left: -3.5em;
|
.loading__logo svg {
|
||||||
-webkit-animation-delay: -0.32s;
|
width: 200px;
|
||||||
animation-delay: -0.32s;
|
height: 200px;
|
||||||
}
|
}
|
||||||
#loader:after {
|
|
||||||
left: 3.5em;
|
.loading__text {
|
||||||
}
|
font-family: sans-serif;
|
||||||
@-webkit-keyframes pulseload {
|
color: white;
|
||||||
0%,
|
font-size: 1.2em;
|
||||||
80%,
|
padding-bottom: 2em;
|
||||||
100% {
|
|
||||||
box-shadow: 0 2.5em 0 -1.3em;
|
|
||||||
}
|
|
||||||
40% {
|
|
||||||
box-shadow: 0 2.5em 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes pulseload {
|
|
||||||
0%,
|
|
||||||
80%,
|
|
||||||
100% {
|
|
||||||
box-shadow: 0 2.5em 0 -1.3em;
|
|
||||||
}
|
|
||||||
40% {
|
|
||||||
box-shadow: 0 2.5em 0 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -150,8 +119,13 @@
|
|||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<div id="app">
|
<div id="app"></div>
|
||||||
<div id="loader">Loading...</div>
|
<div class="loading">
|
||||||
|
<div class="loading__logo">
|
||||||
|
<!-- replaced by 'html-webpack-inline-svg-plugin' -->
|
||||||
|
<img inline src="node_modules/maputnik-design/logos/logo-loading.svg" />
|
||||||
|
</div>
|
||||||
|
<div class="loading__text">Loading…</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
31
src/vendor/codemirror/addon/lint/json-lint.js
vendored
31
src/vendor/codemirror/addon/lint/json-lint.js
vendored
@@ -1,31 +0,0 @@
|
|||||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
|
||||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
|
||||||
|
|
||||||
// Depends on fork of jsonlint from <https://github.com/josdejong/jsonlint>
|
|
||||||
// becuase of <https://github.com/zaach/jsonlint/issues/57>
|
|
||||||
var jsonlint = require("jsonlint");
|
|
||||||
var CodeMirror = require("codemirror");
|
|
||||||
|
|
||||||
CodeMirror.registerHelper("lint", "json", function(text) {
|
|
||||||
var found = [];
|
|
||||||
|
|
||||||
// NOTE: This was modified from the original to remove the global, also the
|
|
||||||
// old jsonlint API was 'jsonlint.parseError' its now
|
|
||||||
// 'jsonlint.parser.parseError'
|
|
||||||
jsonlint.parser.parseError = function(str, hash) {
|
|
||||||
var loc = hash.loc;
|
|
||||||
found.push({
|
|
||||||
from: CodeMirror.Pos(loc.first_line - 1, loc.first_column),
|
|
||||||
to: CodeMirror.Pos(loc.last_line - 1, loc.last_column),
|
|
||||||
message: str
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
jsonlint.parse(text);
|
|
||||||
}
|
|
||||||
catch(e) {
|
|
||||||
// Do nothing we catch the error above
|
|
||||||
}
|
|
||||||
return found;
|
|
||||||
});
|
|
||||||
@@ -200,7 +200,7 @@ describe("layers", function() {
|
|||||||
|
|
||||||
const elem = $(wd.$("layer-list-item:background:"+bgId));
|
const elem = $(wd.$("layer-list-item:background:"+bgId));
|
||||||
elem.click();
|
elem.click();
|
||||||
browser.setValueSafe(wd.$("min-zoom", "input"), 1)
|
browser.setValueSafe(wd.$("min-zoom", 'input[type="text"]'), 1)
|
||||||
const elem2 = $(wd.$("layer-editor.layer-id", "input"));
|
const elem2 = $(wd.$("layer-editor.layer-id", "input"));
|
||||||
elem2.click();
|
elem2.click();
|
||||||
|
|
||||||
@@ -232,7 +232,7 @@ describe("layers", function() {
|
|||||||
|
|
||||||
const elem = $(wd.$("layer-list-item:background:"+bgId));
|
const elem = $(wd.$("layer-list-item:background:"+bgId));
|
||||||
elem.click();
|
elem.click();
|
||||||
browser.setValueSafe(wd.$("max-zoom", "input"), 1)
|
browser.setValueSafe(wd.$("max-zoom", 'input[type="text"]'), 1)
|
||||||
const elem2 = $(wd.$("layer-editor.layer-id", "input"));
|
const elem2 = $(wd.$("layer-editor.layer-id", "input"));
|
||||||
elem2.click();
|
elem2.click();
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ var helper = require("../helper");
|
|||||||
|
|
||||||
|
|
||||||
function closeModal(wdKey) {
|
function closeModal(wdKey) {
|
||||||
|
const selector = wd.$(wdKey);
|
||||||
|
|
||||||
browser.waitUntil(function() {
|
browser.waitUntil(function() {
|
||||||
const elem = $(wdKey);
|
const elem = $(selector);
|
||||||
return elem.isDisplayedInViewport();
|
return elem.isDisplayedInViewport();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -15,8 +17,9 @@ function closeModal(wdKey) {
|
|||||||
closeBtnSelector.click();
|
closeBtnSelector.click();
|
||||||
|
|
||||||
browser.waitUntil(function() {
|
browser.waitUntil(function() {
|
||||||
const elem = $(wdKey);
|
return browser.execute((selector) => {
|
||||||
return !elem.isDisplayed();
|
return !document.querySelector(selector);
|
||||||
|
}, selector);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user