mirror of
https://github.com/a-nyx/maputnik-with-pmtiles.git
synced 2024-12-28 18:11:21 +01:00
Merge remote-tracking branch 'upstream/master' into feature/add-thunderforest-source
Conflicts: src/components/App.jsx
This commit is contained in:
commit
fd59f42819
90 changed files with 11345 additions and 6012 deletions
11
.babelrc
11
.babelrc
|
@ -1,4 +1,13 @@
|
|||
{
|
||||
"presets": ["env", "react"],
|
||||
"plugins": ["transform-object-rest-spread", "transform-class-properties"]
|
||||
"plugins": ["transform-object-rest-spread", "transform-class-properties"],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": [
|
||||
["istanbul", {
|
||||
exclude: ["node_modules/**", "test/**"]
|
||||
}]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
103
.circleci/config.yml
Normal file
103
.circleci/config.yml
Normal file
|
@ -0,0 +1,103 @@
|
|||
version: 2
|
||||
templates:
|
||||
# Test the build **only** no webdriver
|
||||
build-steps: &build-steps
|
||||
- checkout
|
||||
- run:
|
||||
name: "Create artifacts directory"
|
||||
command: mkdir /tmp/artifacts
|
||||
- restore_cache:
|
||||
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
|
||||
|
||||
- run: npm install
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
|
||||
|
||||
- run: mkdir -p /tmp/artifacts/logs
|
||||
- run: npm run build
|
||||
- run: npm run lint
|
||||
- run: npm run lint-styles
|
||||
- store_artifacts:
|
||||
path: /tmp/artifacts
|
||||
destination: /artifacts
|
||||
# Test in webdriver
|
||||
wdio-steps: &wdio-steps
|
||||
- checkout
|
||||
- run:
|
||||
name: "Create artifacts directory"
|
||||
command: mkdir /tmp/artifacts
|
||||
- restore_cache:
|
||||
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
|
||||
|
||||
- run: npm install
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: v1-dependencies-{{ arch }}-{{ checksum "package.json" }}
|
||||
|
||||
- run: mkdir -p /tmp/artifacts/logs
|
||||
- run: npm run build
|
||||
- run: npm run lint
|
||||
- run: npm run lint-styles
|
||||
- run: DOCKER_HOST=localhost npm test
|
||||
- run: ./node_modules/.bin/istanbul report --include /tmp/artifacts/coverage/coverage.json --dir /tmp/artifacts/coverage html lcov
|
||||
- store_artifacts:
|
||||
path: /tmp/artifacts
|
||||
destination: /artifacts
|
||||
jobs:
|
||||
build-linux-node-v6:
|
||||
docker:
|
||||
- image: node:6
|
||||
working_directory: ~/repo-linux-node-v6
|
||||
steps: *build-steps
|
||||
build-linux-node-v8:
|
||||
docker:
|
||||
- image: node:8
|
||||
- image: selenium/standalone-chrome:3.8.1
|
||||
working_directory: ~/repo-linux-node-v8
|
||||
steps: *wdio-steps
|
||||
build-linux-node-v10:
|
||||
docker:
|
||||
- image: node:10
|
||||
working_directory: ~/repo-linux-node-v10
|
||||
steps: *build-steps
|
||||
build-osx-node-v6:
|
||||
macos:
|
||||
xcode: "9.0"
|
||||
dependencies:
|
||||
override:
|
||||
- brew install node@6
|
||||
working_directory: ~/repo-osx-node-v6
|
||||
steps: *build-steps
|
||||
build-osx-node-v8:
|
||||
macos:
|
||||
xcode: "9.0"
|
||||
dependencies:
|
||||
override:
|
||||
- brew install node@8
|
||||
working_directory: ~/repo-osx-node-v8
|
||||
steps: *build-steps
|
||||
build-osx-node-v10:
|
||||
macos:
|
||||
xcode: "9.0"
|
||||
dependencies:
|
||||
override:
|
||||
- brew install node@10
|
||||
working_directory: ~/repo-osx-node-v10
|
||||
steps: *build-steps
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build:
|
||||
jobs:
|
||||
- build-linux-node-v6
|
||||
- build-linux-node-v8
|
||||
- build-linux-node-v10
|
||||
- build-osx-node-v6
|
||||
- build-osx-node-v8
|
||||
- build-osx-node-v10
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -30,3 +30,6 @@ node_modules
|
|||
|
||||
# Ignore build files
|
||||
public
|
||||
/errorShots
|
||||
/old
|
||||
/build
|
||||
|
|
19
.travis.yml
19
.travis.yml
|
@ -1,30 +1,12 @@
|
|||
language: node_js
|
||||
addons:
|
||||
firefox: latest
|
||||
matrix:
|
||||
include:
|
||||
- os: linux
|
||||
node_js: "6"
|
||||
- os: linux
|
||||
env: CXX=g++-4.8
|
||||
node_js: "7"
|
||||
- os: linux
|
||||
node_js: "8"
|
||||
- os: linux
|
||||
env: CXX=g++-4.8
|
||||
node_js: "9"
|
||||
- os: osx
|
||||
node_js: "6"
|
||||
- os: osx
|
||||
node_js: "7"
|
||||
- os: osx
|
||||
node_js: "8"
|
||||
- os: osx
|
||||
node_js: "9"
|
||||
before_install:
|
||||
- export CHROME_BIN=chromium-browser
|
||||
- export DISPLAY=:99.0
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sh -e /etc/init.d/xvfb start; fi
|
||||
install:
|
||||
- npm install
|
||||
script:
|
||||
|
@ -32,7 +14,6 @@ script:
|
|||
- node --stack_size=100000 $(which npm) run build
|
||||
- npm run lint
|
||||
- npm run lint-styles
|
||||
- npm run test
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
|
|
32
README.md
32
README.md
|
@ -22,6 +22,11 @@ targeted at developers and map designers.
|
|||
|
||||
Mapbox has built one of the best and most amazing OSS ecosystems. A key component to ensure its longevity and independance is an OSS map designer.
|
||||
|
||||
|
||||
## Donations
|
||||
If you or your organisation has seen value from Maputnik, please consider donating at <https://maputnik.github.io/donate>
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation can be found in the [Wiki](https://github.com/maputnik/editor/wiki). You are welcome to collaborate!
|
||||
|
@ -68,6 +73,33 @@ npm run lint
|
|||
npm run lint-styles
|
||||
```
|
||||
|
||||
|
||||
## Tests
|
||||
For testing we use [webdriverio](http://webdriver.io) and [selenium-standalone](https://github.com/vvo/selenium-standalone)
|
||||
|
||||
[selenium-standalone](https://github.com/vvo/selenium-standalone) starts a server that will launch browsers on your local machine. We use chrome so you **must** have chrome installed on your machine.
|
||||
|
||||
Now open and terminal and run the following. This will install the drivers on your local machine
|
||||
|
||||
```
|
||||
./node_modules/.bin/selenium-standalone install
|
||||
```
|
||||
|
||||
Now start the standalone server
|
||||
|
||||
```
|
||||
./node_modules/.bin/selenium-standalone start
|
||||
```
|
||||
|
||||
Then open another terminal and run
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
After some time you should see a browser launch which will be automated by the test runner.
|
||||
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [maputnik-dev-server](https://github.com/nycplanning/labs-maputnik-dev-server) - An express.js server that allows for quickly loading the style from any mapboxGL map into mapuntnik.
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
environment:
|
||||
matrix:
|
||||
- nodejs_version: "6"
|
||||
- nodejs_version: "7"
|
||||
- nodejs_version: "8"
|
||||
- nodejs_version: "9"
|
||||
platform:
|
||||
|
@ -16,4 +15,3 @@ build_script:
|
|||
- npm run build
|
||||
test_script:
|
||||
- npm run lint
|
||||
- npm test
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
machine:
|
||||
node:
|
||||
version: 6
|
||||
test:
|
||||
post:
|
||||
- npm run build
|
|
@ -1,48 +1,62 @@
|
|||
var webpack = require("webpack");
|
||||
var WebpackDevServer = require("webpack-dev-server");
|
||||
var webpackConfig = require("./webpack.production.config");
|
||||
var webpackConfig = require("./webpack.config");
|
||||
var testConfig = require("../test/config/specs");
|
||||
var artifacts = require("../test/artifacts");
|
||||
var isDocker = require("is-docker");
|
||||
|
||||
|
||||
var server;
|
||||
var SCREENSHOT_PATH = artifacts.pathSync("screenshots");
|
||||
|
||||
exports.config = {
|
||||
specs: [
|
||||
'./test/specs/**/*.js'
|
||||
'./test/functional/index.js'
|
||||
],
|
||||
exclude: [
|
||||
],
|
||||
maxInstances: 10,
|
||||
capabilities: [{
|
||||
maxInstances: 5,
|
||||
browserName: 'firefox'
|
||||
browserName: 'chrome'
|
||||
}],
|
||||
sync: true,
|
||||
logLevel: 'verbose',
|
||||
coloredLogs: true,
|
||||
bail: 0,
|
||||
screenshotPath: './errorShots/',
|
||||
screenshotPath: SCREENSHOT_PATH,
|
||||
// Note: This is here because @orangemug currently runs Maputnik inside a docker container.
|
||||
host: process.env.DOCKER_HOST || "0.0.0.0",
|
||||
baseUrl: 'http://localhost',
|
||||
waitforTimeout: 10000,
|
||||
connectionRetryTimeout: 90000,
|
||||
connectionRetryCount: 3,
|
||||
services: ['phantomjs'],
|
||||
framework: 'mocha',
|
||||
reporters: ['spec'],
|
||||
phantomjsOpts: {
|
||||
webdriverLogfile: 'phantomjs.log'
|
||||
},
|
||||
mochaOpts: {
|
||||
ui: 'bdd',
|
||||
// Because we don't know how long the initial build will take...
|
||||
timeout: 4*60*1000
|
||||
},
|
||||
onPrepare: function (config, capabilities) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var compiler = webpack(webpackConfig);
|
||||
server = new WebpackDevServer(compiler, {});
|
||||
server.listen(testConfig.port);
|
||||
server = new WebpackDevServer(compiler, {
|
||||
stats: {
|
||||
colors: true
|
||||
}
|
||||
});
|
||||
server.listen(testConfig.port, (isDocker() ? "0.0.0.0" : "localhost"), function(err) {
|
||||
if(err) {
|
||||
reject(err);
|
||||
}
|
||||
else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
onComplete: function(exitCode) {
|
||||
server.close();
|
||||
server.close()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
var webpack = require('webpack');
|
||||
var path = require('path');
|
||||
var loaders = require('./webpack.loaders');
|
||||
|
@ -7,15 +6,10 @@ var HtmlWebpackPlugin = require('html-webpack-plugin');
|
|||
var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
|
||||
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
var artifacts = require("../test/artifacts");
|
||||
var UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
||||
|
||||
var OUTPATH;
|
||||
if(process.env.CIRCLE_ARTIFACTS) {
|
||||
OUTPATH = path.join(process.env.CIRCLE_ARTIFACTS, "build");
|
||||
}
|
||||
else {
|
||||
OUTPATH = path.join(__dirname, '..', 'public');
|
||||
}
|
||||
var OUTPATH = artifacts.pathSync("/build");
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
|
|
14063
package-lock.json
generated
14063
package-lock.json
generated
File diff suppressed because it is too large
Load diff
99
package.json
99
package.json
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"name": "maputnik",
|
||||
"version": "1.1.0",
|
||||
"version": "1.4.0",
|
||||
"description": "A MapboxGL visual style editor",
|
||||
"main": "''",
|
||||
"scripts": {
|
||||
"stats": "webpack --config config/webpack.production.config.js --profile --json > stats.json",
|
||||
"build": "webpack --config config/webpack.production.config.js --progress --profile --colors",
|
||||
"test": "wdio config/wdio.conf.js",
|
||||
"test-watch": "wdio config/wdio.conf.js --watch",
|
||||
"test": "cross-env NODE_ENV=test wdio config/wdio.conf.js",
|
||||
"test-watch": "cross-env NODE_ENV=test wdio config/wdio.conf.js --watch",
|
||||
"start": "webpack-dev-server --progress --profile --colors --config config/webpack.config.js",
|
||||
"lint": "eslint --ext js --ext jsx {src,test}",
|
||||
"lint-styles": "stylelint 'src/styles/*.scss'",
|
||||
|
@ -21,33 +21,36 @@
|
|||
"license": "MIT",
|
||||
"homepage": "https://github.com/maputnik/editor#readme",
|
||||
"dependencies": {
|
||||
"@mapbox/mapbox-gl-rtl-text": "^0.1.1",
|
||||
"@mapbox/mapbox-gl-style-spec": "^11.1.1",
|
||||
"@mapbox/mapbox-gl-rtl-text": "^0.1.2",
|
||||
"@mapbox/mapbox-gl-style-spec": "^12.0.0",
|
||||
"classnames": "^2.2.5",
|
||||
"codemirror": "^5.32.0",
|
||||
"color": "^2.0.0",
|
||||
"file-saver": "^1.3.3",
|
||||
"codemirror": "^5.37.0",
|
||||
"color": "^3.0.0",
|
||||
"file-saver": "^1.3.8",
|
||||
"github-api": "^3.0.0",
|
||||
"jsonlint": "github:josdejong/jsonlint#85a19d7",
|
||||
"lodash.capitalize": "^4.2.1",
|
||||
"lodash.clamp": "^4.0.3",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"mapbox-gl": "^0.44.1",
|
||||
"mapbox-gl-inspect": "^1.3.0",
|
||||
"mapbox-gl": "^0.45.0",
|
||||
"mapbox-gl-inspect": "^1.3.1",
|
||||
"maputnik-design": "github:maputnik/design",
|
||||
"mousetrap": "^1.6.1",
|
||||
"ol-mapbox-style": "^2.10.1",
|
||||
"ol": "^4.6.4",
|
||||
"ol": "^4.6.5",
|
||||
"prop-types": "^15.6.0",
|
||||
"react": "16.0.0",
|
||||
"react": "^16.3.2",
|
||||
"react-addons-pure-render-mixin": "^15.6.2",
|
||||
"react-aria-menubutton": "^5.1.1",
|
||||
"react-aria-modal": "^2.12.1",
|
||||
"react-autocomplete": "^1.7.2",
|
||||
"react-codemirror2": "^3.0.7",
|
||||
"react-codemirror2": "^4.2.1",
|
||||
"react-collapse": "^4.0.3",
|
||||
"react-color": "^2.13.8",
|
||||
"react-color": "^2.14.1",
|
||||
"react-copy-to-clipboard": "^5.0.1",
|
||||
"react-dom": "16.0.0",
|
||||
"react-dom": "^16.3.2",
|
||||
"react-file-reader-input": "^1.1.4",
|
||||
"react-height": "^3.0.0",
|
||||
"react-icon-base": "^2.1.1",
|
||||
|
@ -55,14 +58,25 @@
|
|||
"react-motion": "^0.5.2",
|
||||
"react-sortable-hoc": "^0.6.8",
|
||||
"reconnecting-websocket": "^3.2.2",
|
||||
"request": "^2.83.0",
|
||||
"request": "^2.85.0",
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"jshintConfig": {
|
||||
"esversion": 6
|
||||
},
|
||||
"stylelint": {
|
||||
"extends": "stylelint-config-standard"
|
||||
"extends": "stylelint-config-recommended-scss",
|
||||
"rules": {
|
||||
"no-descending-specificity": null,
|
||||
"media-feature-name-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignoreMediaFeatureNames": [
|
||||
"prefers-reduced-motion"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"eslintConfig": {
|
||||
"plugins": [
|
||||
|
@ -88,9 +102,10 @@
|
|||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-eslint": "^8.0.2",
|
||||
"babel-loader": "7.1.1",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-eslint": "^8.2.3",
|
||||
"babel-loader": "7.1.4",
|
||||
"babel-plugin-istanbul": "^4.1.6",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-flow-strip-types": "^6.22.0",
|
||||
|
@ -100,36 +115,42 @@
|
|||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-flow": "^6.23.0",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-register": "^6.26.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"base64-loader": "^1.0.0",
|
||||
"copy-webpack-plugin": "^4.2.0",
|
||||
"css-loader": "^0.28.7",
|
||||
"eslint": "^4.10.0",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
"cors": "^2.8.4",
|
||||
"cross-env": "^5.1.4",
|
||||
"css-loader": "^0.28.11",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-plugin-react": "^7.4.0",
|
||||
"express": "^4.16.3",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"file-loader": "^1.1.5",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"is-docker": "^1.1.0",
|
||||
"istanbul": "^0.4.5",
|
||||
"istanbul-lib-coverage": "^1.2.0",
|
||||
"json-loader": "^0.5.7",
|
||||
"karma": "^1.7.1",
|
||||
"karma-chrome-launcher": "^2.2.0",
|
||||
"karma-firefox-launcher": "^1.0.1",
|
||||
"karma-mocha": "^1.3.0",
|
||||
"karma-webpack": "^2.0.5",
|
||||
"mocha": "^4.0.1",
|
||||
"mocha-loader": "^1.1.1",
|
||||
"node-sass": "^4.6.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"mocha": "^5.1.1",
|
||||
"node-sass": "^4.9.0",
|
||||
"nsp": "^3.1.0",
|
||||
"react-hot-loader": "^3.1.1",
|
||||
"sass-loader": "^6.0.6",
|
||||
"style-loader": "^0.19.0",
|
||||
"stylelint": "^7.13.0",
|
||||
"stylelint-config-standard": "^15.0.1",
|
||||
"sass-loader": "^7.0.1",
|
||||
"selenium-standalone": "^6.14.0",
|
||||
"style-loader": "^0.20.3",
|
||||
"stylelint": "^9.2.0",
|
||||
"stylelint-config-recommended-scss": "^3.2.0",
|
||||
"stylelint-scss": "^3.0.0",
|
||||
"transform-loader": "^0.2.4",
|
||||
"uglifyjs-webpack-plugin": "^1.1.8",
|
||||
"wdio-mocha-framework": "^0.5.11",
|
||||
"uglifyjs-webpack-plugin": "^1.2.4",
|
||||
"uuid": "^3.1.0",
|
||||
"wdio-mocha-framework": "^0.5.13",
|
||||
"wdio-phantomjs-service": "^0.2.2",
|
||||
"wdio-selenium-standalone-service": "0.0.10",
|
||||
"wdio-spec-reporter": "^0.1.2",
|
||||
"webdriverio": "^4.8.0",
|
||||
"webdriverio": "^4.12.0",
|
||||
"webpack": "^3.8.1",
|
||||
"webpack-bundle-analyzer": "^2.9.0",
|
||||
"webpack-cleanup-plugin": "^0.5.1",
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import React from 'react'
|
||||
import Mousetrap from 'mousetrap'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import clamp from 'lodash.clamp'
|
||||
import {arrayMove} from 'react-sortable-hoc'
|
||||
import url from 'url'
|
||||
|
||||
import MapboxGlMap from './map/MapboxGlMap'
|
||||
import OpenLayers3Map from './map/OpenLayers3Map'
|
||||
|
@ -9,8 +13,15 @@ import Toolbar from './Toolbar'
|
|||
import AppLayout from './AppLayout'
|
||||
import MessagePanel from './MessagePanel'
|
||||
|
||||
import SettingsModal from './modals/SettingsModal'
|
||||
import ExportModal from './modals/ExportModal'
|
||||
import SourcesModal from './modals/SourcesModal'
|
||||
import OpenModal from './modals/OpenModal'
|
||||
import ShortcutsModal from './modals/ShortcutsModal'
|
||||
import SurveyModal from './modals/SurveyModal'
|
||||
|
||||
import { downloadGlyphsMetadata, downloadSpriteMetadata } from '../libs/metadata'
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import style from '../libs/style.js'
|
||||
import { initialStyleUrl, loadStyleUrl } from '../libs/urlopen'
|
||||
import { undoMessages, redoMessages } from '../libs/diffmessage'
|
||||
|
@ -20,9 +31,11 @@ import { RevisionStore } from '../libs/revisions'
|
|||
import LayerWatcher from '../libs/layerwatcher'
|
||||
import tokens from '../config/tokens.json'
|
||||
import isEqual from 'lodash.isequal'
|
||||
import Debug from '../libs/debug'
|
||||
import queryUtil from '../libs/query-util'
|
||||
|
||||
import MapboxGl from 'mapbox-gl'
|
||||
import mapboxUtil from 'mapbox-gl/src/util/mapbox'
|
||||
import { normalizeSourceURL } from 'mapbox-gl/src/util/mapbox'
|
||||
|
||||
|
||||
function updateRootSpec(spec, fieldName, newValues) {
|
||||
|
@ -46,6 +59,79 @@ export default class App extends React.Component {
|
|||
onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false)
|
||||
})
|
||||
|
||||
|
||||
const keyCodes = {
|
||||
"esc": 27,
|
||||
"?": 191,
|
||||
"o": 79,
|
||||
"e": 69,
|
||||
"s": 83,
|
||||
"d": 68,
|
||||
"i": 73,
|
||||
"m": 77,
|
||||
}
|
||||
|
||||
const shortcuts = [
|
||||
{
|
||||
keyCode: keyCodes["?"],
|
||||
handler: () => {
|
||||
this.toggleModal("shortcuts");
|
||||
}
|
||||
},
|
||||
{
|
||||
keyCode: keyCodes["o"],
|
||||
handler: () => {
|
||||
this.toggleModal("open");
|
||||
}
|
||||
},
|
||||
{
|
||||
keyCode: keyCodes["e"],
|
||||
handler: () => {
|
||||
this.toggleModal("export");
|
||||
}
|
||||
},
|
||||
{
|
||||
keyCode: keyCodes["d"],
|
||||
handler: () => {
|
||||
this.toggleModal("sources");
|
||||
}
|
||||
},
|
||||
{
|
||||
keyCode: keyCodes["s"],
|
||||
handler: () => {
|
||||
this.toggleModal("settings");
|
||||
}
|
||||
},
|
||||
{
|
||||
keyCode: keyCodes["i"],
|
||||
handler: () => {
|
||||
this.changeInspectMode();
|
||||
}
|
||||
},
|
||||
{
|
||||
keyCode: keyCodes["m"],
|
||||
handler: () => {
|
||||
document.querySelector(".mapboxgl-canvas").focus();
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
document.body.addEventListener("keyup", (e) => {
|
||||
if(e.keyCode === keyCodes["esc"]) {
|
||||
e.target.blur();
|
||||
document.body.focus();
|
||||
}
|
||||
else if(document.activeElement === document.body) {
|
||||
const shortcut = shortcuts.find((shortcut) => {
|
||||
return (shortcut.keyCode === e.keyCode)
|
||||
})
|
||||
|
||||
if(shortcut) {
|
||||
shortcut.handler(e);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const styleUrl = initialStyleUrl()
|
||||
if(styleUrl) {
|
||||
this.styleStore = new StyleStore()
|
||||
|
@ -57,9 +143,21 @@ export default class App extends React.Component {
|
|||
this.styleStore = new StyleStore()
|
||||
}
|
||||
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
|
||||
|
||||
if(Debug.enabled()) {
|
||||
Debug.set("maputnik", "styleStore", this.styleStore);
|
||||
Debug.set("maputnik", "revisionStore", this.revisionStore);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if(Debug.enabled()) {
|
||||
Debug.set("maputnik", "revisionStore", this.revisionStore);
|
||||
Debug.set("maputnik", "styleStore", this.styleStore);
|
||||
}
|
||||
|
||||
const queryObj = url.parse(window.location.href, true).query;
|
||||
|
||||
this.state = {
|
||||
errors: [],
|
||||
infos: [],
|
||||
|
@ -69,6 +167,18 @@ export default class App extends React.Component {
|
|||
vectorLayers: {},
|
||||
inspectModeEnabled: false,
|
||||
spec: styleSpec.latest,
|
||||
isOpen: {
|
||||
settings: false,
|
||||
sources: false,
|
||||
open: false,
|
||||
shortcuts: false,
|
||||
export: false,
|
||||
survey: localStorage.hasOwnProperty('survey') ? false : true
|
||||
},
|
||||
mapOptions: {
|
||||
showTileBoundaries: queryUtil.asBool(queryObj, "show-tile-boundaries")
|
||||
},
|
||||
mapFilter: queryObj["color-blindness-emulation"],
|
||||
}
|
||||
|
||||
this.layerWatcher = new LayerWatcher({
|
||||
|
@ -86,11 +196,6 @@ export default class App extends React.Component {
|
|||
Mousetrap.unbind(['mod+y', 'mod+shift+z'], this.onRedo.bind(this));
|
||||
}
|
||||
|
||||
onReset() {
|
||||
this.styleStore.purge()
|
||||
loadDefaultStyle(mapStyle => this.onStyleOpen(mapStyle))
|
||||
}
|
||||
|
||||
saveStyle(snapshotStyle) {
|
||||
this.styleStore.save(snapshotStyle)
|
||||
}
|
||||
|
@ -158,6 +263,24 @@ export default class App extends React.Component {
|
|||
})
|
||||
}
|
||||
|
||||
onMoveLayer(move) {
|
||||
let { oldIndex, newIndex } = move;
|
||||
let layers = this.state.mapStyle.layers;
|
||||
oldIndex = clamp(oldIndex, 0, layers.length-1);
|
||||
newIndex = clamp(newIndex, 0, layers.length-1);
|
||||
if(oldIndex === newIndex) return;
|
||||
|
||||
if (oldIndex === this.state.selectedLayerIndex) {
|
||||
this.setState({
|
||||
selectedLayerIndex: newIndex
|
||||
});
|
||||
}
|
||||
|
||||
layers = layers.slice(0);
|
||||
layers = arrayMove(layers, oldIndex, newIndex);
|
||||
this.onLayersChange(layers);
|
||||
}
|
||||
|
||||
onLayersChange(changedLayers) {
|
||||
const changedStyle = {
|
||||
...this.state.mapStyle,
|
||||
|
@ -166,6 +289,40 @@ export default class App extends React.Component {
|
|||
this.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
onLayerDestroy(layerId) {
|
||||
let layers = this.state.mapStyle.layers;
|
||||
const remainingLayers = layers.slice(0);
|
||||
const idx = style.indexOfLayer(remainingLayers, layerId)
|
||||
remainingLayers.splice(idx, 1);
|
||||
this.onLayersChange(remainingLayers);
|
||||
}
|
||||
|
||||
onLayerCopy(layerId) {
|
||||
let layers = this.state.mapStyle.layers;
|
||||
const changedLayers = layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, layerId)
|
||||
|
||||
const clonedLayer = cloneDeep(changedLayers[idx])
|
||||
clonedLayer.id = clonedLayer.id + "-copy"
|
||||
changedLayers.splice(idx, 0, clonedLayer)
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
onLayerVisibilityToggle(layerId) {
|
||||
let layers = this.state.mapStyle.layers;
|
||||
const changedLayers = layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, layerId)
|
||||
|
||||
const layer = { ...changedLayers[idx] }
|
||||
const changedLayout = 'layout' in layer ? {...layer.layout} : {}
|
||||
changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none'
|
||||
|
||||
layer.layout = changedLayout
|
||||
changedLayers[idx] = layer
|
||||
this.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
|
||||
onLayerIdChange(oldId, newId) {
|
||||
const changedLayers = this.state.mapStyle.layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, oldId)
|
||||
|
@ -208,7 +365,7 @@ export default class App extends React.Component {
|
|||
if(!this.state.sources.hasOwnProperty(key) && val.type === "vector" && val.hasOwnProperty("url")) {
|
||||
let url = val.url;
|
||||
try {
|
||||
url = mapboxUtil.normalizeSourceURL(url, MapboxGl.accessToken);
|
||||
url = normalizeSourceURL(url, MapboxGl.accessToken);
|
||||
} catch(err) {
|
||||
console.warn("Failed to normalizeSourceURL: ", err);
|
||||
}
|
||||
|
@ -250,7 +407,8 @@ export default class App extends React.Component {
|
|||
|
||||
mapRenderer() {
|
||||
const mapProps = {
|
||||
mapStyle: style.replaceAccessTokens(this.state.mapStyle, {allowFallback: true}),
|
||||
mapStyle: style.replaceAccessToken(this.state.mapStyle, {allowFallback: true}),
|
||||
options: this.state.mapOptions,
|
||||
onDataChange: (e) => {
|
||||
this.layerWatcher.analyzeMap(e.map)
|
||||
this.fetchSources();
|
||||
|
@ -260,15 +418,26 @@ export default class App extends React.Component {
|
|||
const metadata = this.state.mapStyle.metadata || {}
|
||||
const renderer = metadata['maputnik:renderer'] || 'mbgljs'
|
||||
|
||||
let mapElement;
|
||||
|
||||
// Check if OL3 code has been loaded?
|
||||
if(renderer === 'ol3') {
|
||||
return <OpenLayers3Map {...mapProps} />
|
||||
mapElement = <OpenLayers3Map {...mapProps} />
|
||||
} else {
|
||||
return <MapboxGlMap {...mapProps}
|
||||
mapElement = <MapboxGlMap {...mapProps}
|
||||
inspectModeEnabled={this.state.inspectModeEnabled}
|
||||
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
|
||||
onLayerSelect={this.onLayerSelect.bind(this)} />
|
||||
}
|
||||
|
||||
const elementStyle = {};
|
||||
if(this.state.mapFilter) {
|
||||
elementStyle.filter = `url('#${this.state.mapFilter}')`;
|
||||
}
|
||||
|
||||
return <div style={elementStyle}>
|
||||
{mapElement}
|
||||
</div>
|
||||
}
|
||||
|
||||
onLayerSelect(layerId) {
|
||||
|
@ -276,6 +445,19 @@ export default class App extends React.Component {
|
|||
this.setState({ selectedLayerIndex: idx })
|
||||
}
|
||||
|
||||
toggleModal(modalName) {
|
||||
this.setState({
|
||||
isOpen: {
|
||||
...this.state.isOpen,
|
||||
[modalName]: !this.state.isOpen[modalName]
|
||||
}
|
||||
})
|
||||
|
||||
if(modalName === 'survey') {
|
||||
localStorage.setItem('survey', '');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const layers = this.state.mapStyle.layers || []
|
||||
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : null
|
||||
|
@ -288,9 +470,14 @@ export default class App extends React.Component {
|
|||
onStyleChanged={this.onStyleChanged.bind(this)}
|
||||
onStyleOpen={this.onStyleChanged.bind(this)}
|
||||
onInspectModeToggle={this.changeInspectMode.bind(this)}
|
||||
onToggleModal={this.toggleModal.bind(this)}
|
||||
/>
|
||||
|
||||
const layerList = <LayerList
|
||||
onMoveLayer={this.onMoveLayer.bind(this)}
|
||||
onLayerDestroy={this.onLayerDestroy.bind(this)}
|
||||
onLayerCopy={this.onLayerCopy.bind(this)}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
|
||||
onLayersChange={this.onLayersChange.bind(this)}
|
||||
onLayerSelect={this.onLayerSelect.bind(this)}
|
||||
selectedLayerIndex={this.state.selectedLayerIndex}
|
||||
|
@ -300,10 +487,17 @@ export default class App extends React.Component {
|
|||
|
||||
const layerEditor = selectedLayer ? <LayerEditor
|
||||
layer={selectedLayer}
|
||||
layerIndex={this.state.selectedLayerIndex}
|
||||
isFirstLayer={this.state.selectedLayerIndex < 1}
|
||||
isLastLayer={this.state.selectedLayerIndex === this.state.mapStyle.layers.length-1}
|
||||
sources={this.state.sources}
|
||||
vectorLayers={this.state.vectorLayers}
|
||||
spec={this.state.spec}
|
||||
onMoveLayer={this.onMoveLayer.bind(this)}
|
||||
onLayerChanged={this.onLayerChanged.bind(this)}
|
||||
onLayerDestroy={this.onLayerDestroy.bind(this)}
|
||||
onLayerCopy={this.onLayerCopy.bind(this)}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
|
||||
onLayerIdChange={this.onLayerIdChange.bind(this)}
|
||||
/> : null
|
||||
|
||||
|
@ -312,12 +506,48 @@ export default class App extends React.Component {
|
|||
infos={this.state.infos}
|
||||
/> : null
|
||||
|
||||
|
||||
const modals = <div>
|
||||
<ShortcutsModal
|
||||
isOpen={this.state.isOpen.shortcuts}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
|
||||
/>
|
||||
<SettingsModal
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged.bind(this)}
|
||||
isOpen={this.state.isOpen.settings}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'settings')}
|
||||
/>
|
||||
<ExportModal
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged.bind(this)}
|
||||
isOpen={this.state.isOpen.export}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'export')}
|
||||
/>
|
||||
<OpenModal
|
||||
isOpen={this.state.isOpen.open}
|
||||
onStyleOpen={this.onStyleChanged.bind(this)}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'open')}
|
||||
/>
|
||||
<SourcesModal
|
||||
mapStyle={this.state.mapStyle}
|
||||
onStyleChanged={this.onStyleChanged.bind(this)}
|
||||
isOpen={this.state.isOpen.sources}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'sources')}
|
||||
/>
|
||||
<SurveyModal
|
||||
isOpen={this.state.isOpen.survey}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'survey')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
return <AppLayout
|
||||
toolbar={toolbar}
|
||||
layerList={layerList}
|
||||
layerEditor={layerEditor}
|
||||
map={this.mapRenderer()}
|
||||
bottom={bottomPanel}
|
||||
modals={modals}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ class AppLayout extends React.Component {
|
|||
layerEditor: PropTypes.element,
|
||||
map: PropTypes.element.isRequired,
|
||||
bottom: PropTypes.element,
|
||||
modals: PropTypes.node,
|
||||
}
|
||||
|
||||
static childContextTypes = {
|
||||
|
@ -39,6 +40,7 @@ class AppLayout extends React.Component {
|
|||
{this.props.bottom}
|
||||
</div>
|
||||
}
|
||||
{this.props.modals}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ import classnames from 'classnames'
|
|||
|
||||
class Button extends React.Component {
|
||||
static propTypes = {
|
||||
"data-wd-key": PropTypes.string,
|
||||
"aria-label": PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
style: PropTypes.object,
|
||||
className: PropTypes.string,
|
||||
|
@ -11,12 +13,14 @@ class Button extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
return <a
|
||||
return <button
|
||||
onClick={this.props.onClick}
|
||||
aria-label={this.props["aria-label"]}
|
||||
className={classnames("maputnik-button", this.props.className)}
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
style={this.props.style}>
|
||||
{this.props.children}
|
||||
</a>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,12 +16,9 @@ import MdInsertEmoticon from 'react-icons/lib/md/insert-emoticon'
|
|||
import MdFontDownload from 'react-icons/lib/md/font-download'
|
||||
import HelpIcon from 'react-icons/lib/md/help-outline'
|
||||
import InspectionIcon from 'react-icons/lib/md/find-in-page'
|
||||
import SurveyIcon from 'react-icons/lib/md/assignment-turned-in'
|
||||
|
||||
import logoImage from 'maputnik-design/logos/logo-color.svg'
|
||||
import SettingsModal from './modals/SettingsModal'
|
||||
import ExportModal from './modals/ExportModal'
|
||||
import SourcesModal from './modals/SourcesModal'
|
||||
import OpenModal from './modals/OpenModal'
|
||||
import pkgJson from '../../package.json'
|
||||
|
||||
import style from '../libs/style'
|
||||
|
@ -41,6 +38,7 @@ class ToolbarLink extends React.Component {
|
|||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
href: PropTypes.string,
|
||||
onToggleModal: PropTypes.func,
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -55,19 +53,43 @@ class ToolbarLink extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
class ToolbarAction extends React.Component {
|
||||
class ToolbarLinkHighlighted extends React.Component {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
onClick: PropTypes.func
|
||||
href: PropTypes.string,
|
||||
onToggleModal: PropTypes.func
|
||||
}
|
||||
|
||||
render() {
|
||||
return <a
|
||||
className={classnames('maputnik-toolbar-link', "maputnik-toolbar-link--highlighted", this.props.className)}
|
||||
href={this.props.href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<span className="maputnik-toolbar-link-wrapper">
|
||||
{this.props.children}
|
||||
</span>
|
||||
</a>
|
||||
}
|
||||
}
|
||||
|
||||
class ToolbarAction extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
onClick: PropTypes.func,
|
||||
wdKey: PropTypes.string
|
||||
}
|
||||
|
||||
render() {
|
||||
return <button
|
||||
className='maputnik-toolbar-action'
|
||||
data-wd-key={this.props.wdKey}
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
{this.props.children}
|
||||
</a>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,7 +103,8 @@ export default class Toolbar extends React.Component {
|
|||
// A dict of source id's and the available source layers
|
||||
sources: PropTypes.object.isRequired,
|
||||
onInspectModeToggle: PropTypes.func.isRequired,
|
||||
children: PropTypes.node
|
||||
children: PropTypes.node,
|
||||
onToggleModal: PropTypes.func,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
@ -97,68 +120,45 @@ export default class Toolbar extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
toggleModal(modalName) {
|
||||
this.setState({
|
||||
isOpen: {
|
||||
...this.state.isOpen,
|
||||
[modalName]: !this.state.isOpen[modalName]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className='maputnik-toolbar'>
|
||||
<SettingsModal
|
||||
mapStyle={this.props.mapStyle}
|
||||
onStyleChanged={this.props.onStyleChanged}
|
||||
isOpen={this.state.isOpen.settings}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'settings')}
|
||||
/>
|
||||
<ExportModal
|
||||
mapStyle={this.props.mapStyle}
|
||||
onStyleChanged={this.props.onStyleChanged}
|
||||
isOpen={this.state.isOpen.export}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'export')}
|
||||
/>
|
||||
<OpenModal
|
||||
isOpen={this.state.isOpen.open}
|
||||
onStyleOpen={this.props.onStyleOpen}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'open')}
|
||||
/>
|
||||
<SourcesModal
|
||||
mapStyle={this.props.mapStyle}
|
||||
onStyleChanged={this.props.onStyleChanged}
|
||||
isOpen={this.state.isOpen.sources}
|
||||
onOpenToggle={this.toggleModal.bind(this, 'sources')}
|
||||
/>
|
||||
<div className="maputnik-toolbar__inner">
|
||||
<ToolbarLink
|
||||
href={"https://github.com/maputnik/editor"}
|
||||
<div
|
||||
className="maputnik-toolbar-logo-container"
|
||||
>
|
||||
<a className="maputnik-toolbar-skip" href="#skip-menu">
|
||||
Skip navigation
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/maputnik/editor"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="maputnik-toolbar-logo"
|
||||
>
|
||||
<img src={logoImage} alt="Maputnik" />
|
||||
<h1>Maputnik
|
||||
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
|
||||
</h1>
|
||||
</ToolbarLink>
|
||||
</a>
|
||||
</div>
|
||||
<div className="maputnik-toolbar__actions">
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'open')}>
|
||||
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
|
||||
<OpenIcon />
|
||||
<IconText>Open</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'export')}>
|
||||
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
|
||||
<MdFileDownload />
|
||||
<IconText>Export</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'sources')}>
|
||||
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
|
||||
<SourcesIcon />
|
||||
<IconText>Sources</IconText>
|
||||
<IconText>Data Sources</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction onClick={this.toggleModal.bind(this, 'settings')}>
|
||||
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
|
||||
<SettingsIcon />
|
||||
<IconText>Style Settings</IconText>
|
||||
</ToolbarAction>
|
||||
<ToolbarAction onClick={this.props.onInspectModeToggle}>
|
||||
<ToolbarAction wdKey="nav:inspect" onClick={this.props.onInspectModeToggle}>
|
||||
<InspectionIcon />
|
||||
<IconText>
|
||||
{ this.props.inspectModeEnabled && <span>Map Mode</span> }
|
||||
|
@ -169,6 +169,10 @@ export default class Toolbar extends React.Component {
|
|||
<HelpIcon />
|
||||
<IconText>Help</IconText>
|
||||
</ToolbarLink>
|
||||
<ToolbarLinkHighlighted href={"https://gregorywolanski.typeform.com/to/cPgaSY"}>
|
||||
<SurveyIcon />
|
||||
<IconText>Take the Maputnik Survey</IconText>
|
||||
</ToolbarLinkHighlighted>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -105,6 +105,7 @@ class ColorField extends React.Component {
|
|||
{this.state.pickerOpened && picker}
|
||||
<div className="maputnik-color-swatch" style={swatchStyle}></div>
|
||||
<input
|
||||
spellCheck="false"
|
||||
className="maputnik-color"
|
||||
ref={(input) => this.colorInput = input}
|
||||
onClick={this.togglePicker.bind(this)}
|
||||
|
|
|
@ -131,8 +131,7 @@ export default class FunctionSpecProperty extends React.Component {
|
|||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <div className={propClass}>
|
||||
return <div className={propClass} data-wd-key={"spec-field:"+this.props.fieldName}>
|
||||
{specField}
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -58,6 +58,8 @@ export default class SpecField extends React.Component {
|
|||
name: this.props.fieldName,
|
||||
onChange: newValue => this.props.onChange(this.props.fieldName, newValue)
|
||||
}
|
||||
|
||||
function childNodes() {
|
||||
switch(this.props.fieldSpec.type) {
|
||||
case 'number': return (
|
||||
<NumberInput
|
||||
|
@ -124,4 +126,11 @@ export default class SpecField extends React.Component {
|
|||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-wd-key={"spec-field:"+this.props.fieldName}>
|
||||
{childNodes.call(this)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,8 @@ export default class DataProperty extends React.Component {
|
|||
|
||||
changeStop(changeIdx, stopData, value) {
|
||||
const stops = this.props.value.stops.slice(0)
|
||||
stops[changeIdx] = [stopData, value]
|
||||
const changedStop = stopData.zoom === undefined ? stopData.value : stopData
|
||||
stops[changeIdx] = [changedStop, value]
|
||||
const changedValue = {
|
||||
...this.props.value,
|
||||
stops: stops,
|
||||
|
@ -75,8 +76,8 @@ export default class DataProperty extends React.Component {
|
|||
}
|
||||
|
||||
const dataFields = this.props.value.stops.map((stop, idx) => {
|
||||
const zoomLevel = stop[0].zoom
|
||||
const dataLevel = stop[0].value
|
||||
const zoomLevel = typeof stop[0] === 'object' ? stop[0].zoom : undefined;
|
||||
const dataLevel = typeof stop[0] === 'object' ? stop[0].value : stop[0];
|
||||
const value = stop[1]
|
||||
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
|
||||
|
||||
|
@ -94,8 +95,9 @@ export default class DataProperty extends React.Component {
|
|||
dataInput = <NumberInput {...dataProps} />
|
||||
}
|
||||
|
||||
return <InputBlock key={idx} action={deleteStopBtn} label="">
|
||||
<div className="maputnik-data-spec-property-stop-edit">
|
||||
let zoomInput = null;
|
||||
if(zoomLevel !== undefined) {
|
||||
zoomInput = <div className="maputnik-data-spec-property-stop-edit">
|
||||
<NumberInput
|
||||
value={zoomLevel}
|
||||
onChange={newZoom => this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)}
|
||||
|
@ -103,6 +105,10 @@ export default class DataProperty extends React.Component {
|
|||
max={22}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
return <InputBlock key={idx} action={deleteStopBtn} label="">
|
||||
{zoomInput}
|
||||
<div className="maputnik-data-spec-property-stop-data">
|
||||
{dataInput}
|
||||
</div>
|
||||
|
|
|
@ -37,7 +37,7 @@ export default class ZoomProperty extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
refs: this.setStopRefs(this.props)
|
||||
})
|
||||
|
@ -66,7 +66,7 @@ export default class ZoomProperty extends React.Component {
|
|||
return newRefs;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
const newRefs = this.setStopRefs(nextProps);
|
||||
if(newRefs) {
|
||||
this.setState({
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
|||
import PropTypes from 'prop-types'
|
||||
import { combiningFilterOps } from '../../libs/filterops.js'
|
||||
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import DocLabel from '../fields/DocLabel'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import SingleFilterEditor from './SingleFilterEditor'
|
||||
|
@ -89,7 +89,7 @@ export default class CombiningFilterEditor extends React.Component {
|
|||
}
|
||||
|
||||
return <div className="maputnik-filter-editor">
|
||||
<div className="maputnik-filter-editor-compound-select">
|
||||
<div className="maputnik-filter-editor-compound-select" data-wd-key="layer-filter">
|
||||
<DocLabel
|
||||
label={"Compound Filter"}
|
||||
doc={styleSpec.latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."}
|
||||
|
@ -103,6 +103,7 @@ export default class CombiningFilterEditor extends React.Component {
|
|||
{editorBlocks}
|
||||
<div className="maputnik-filter-editor-add-wrapper">
|
||||
<Button
|
||||
data-wd-key="layer-filter-button"
|
||||
className="maputnik-add-filter"
|
||||
onClick={this.addFilterItem.bind(this)}>
|
||||
Add filter
|
||||
|
|
|
@ -25,7 +25,6 @@ class LayerIcon extends React.Component {
|
|||
case 'line': return <LineIcon {...iconProps} />
|
||||
case 'symbol': return <SymbolIcon {...iconProps} />
|
||||
case 'circle': return <CircleIcon {...iconProps} />
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ class AutocompleteInput extends React.Component {
|
|||
>
|
||||
<Autocomplete
|
||||
menuStyle={{
|
||||
position: "absolute",
|
||||
position: "fixed",
|
||||
overflow: "auto",
|
||||
maxHeight: this.state.maxHeight
|
||||
}}
|
||||
|
@ -63,7 +63,8 @@ class AutocompleteInput extends React.Component {
|
|||
style: null
|
||||
}}
|
||||
inputProps={{
|
||||
className: "maputnik-string"
|
||||
className: "maputnik-string",
|
||||
spellCheck: false
|
||||
}}
|
||||
value={this.props.value}
|
||||
items={this.props.options}
|
||||
|
|
|
@ -6,6 +6,7 @@ import DocLabel from '../fields/DocLabel'
|
|||
/** Wrap a component with a label */
|
||||
class InputBlock extends React.Component {
|
||||
static propTypes = {
|
||||
"data-wd-key": PropTypes.string,
|
||||
label: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.element,
|
||||
|
@ -24,6 +25,7 @@ class InputBlock extends React.Component {
|
|||
|
||||
render() {
|
||||
return <div style={this.props.style}
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
className={classnames({
|
||||
"maputnik-input-block": true,
|
||||
"maputnik-action-block": this.props.action
|
||||
|
|
|
@ -17,7 +17,7 @@ class NumberInput extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
this.setState({ value: nextProps.value })
|
||||
}
|
||||
|
||||
|
@ -67,6 +67,7 @@ class NumberInput extends React.Component {
|
|||
|
||||
render() {
|
||||
return <input
|
||||
spellCheck="false"
|
||||
className="maputnik-number"
|
||||
placeholder={this.props.default}
|
||||
value={this.state.value}
|
||||
|
|
|
@ -4,6 +4,7 @@ import PropTypes from 'prop-types'
|
|||
class SelectInput extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
"data-wd-key": PropTypes.string,
|
||||
options: PropTypes.array.isRequired,
|
||||
style: PropTypes.object,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
|
@ -18,6 +19,7 @@ class SelectInput extends React.Component {
|
|||
|
||||
return <select
|
||||
className="maputnik-select"
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
style={this.props.style}
|
||||
value={this.props.value}
|
||||
onChange={e => this.props.onChange(e.target.value)}
|
||||
|
|
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
|
|||
|
||||
class StringInput extends React.Component {
|
||||
static propTypes = {
|
||||
"data-wd-key": PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
default: PropTypes.string,
|
||||
|
@ -17,7 +18,7 @@ class StringInput extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
this.setState({ value: nextProps.value || '' })
|
||||
}
|
||||
|
||||
|
@ -40,11 +41,17 @@ class StringInput extends React.Component {
|
|||
}
|
||||
|
||||
return React.createElement(tag, {
|
||||
"data-wd-key": this.props["data-wd-key"],
|
||||
spellCheck: !(tag === "input"),
|
||||
className: classes.join(" "),
|
||||
style: this.props.style,
|
||||
value: this.state.value,
|
||||
placeholder: this.props.default,
|
||||
onChange: e => this.setState({ value: e.target.value }),
|
||||
onChange: e => {
|
||||
this.setState({
|
||||
value: e.target.value
|
||||
})
|
||||
},
|
||||
onBlur: () => {
|
||||
if(this.state.value!==this.props.value) this.props.onChange(this.state.value)
|
||||
}
|
||||
|
|
30
src/components/layers/Collapse.jsx
Normal file
30
src/components/layers/Collapse.jsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Collapse from 'react-collapse'
|
||||
import accessibility from '../../libs/accessibility'
|
||||
|
||||
|
||||
export default class CollapseAlt extends React.Component {
|
||||
static propTypes = {
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
children: PropTypes.element.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
if (accessibility.reducedMotionEnabled()) {
|
||||
return (
|
||||
<div style={{display: this.props.isActive ? "block" : "none"}}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<Collapse isOpened={this.props.isActive}>
|
||||
{this.props.children}
|
||||
</Collapse>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,11 @@ class MetadataBlock extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Comments"} doc={"Comments for the current layer. This is non-standard and not in the spec."}>
|
||||
return <InputBlock
|
||||
label={"Comments"}
|
||||
doc={"Comments for the current layer. This is non-standard and not in the spec."}
|
||||
data-wd-key="layer-comment"
|
||||
>
|
||||
<StringInput
|
||||
multi={true}
|
||||
value={this.props.value}
|
||||
|
|
|
@ -4,7 +4,6 @@ import PropTypes from 'prop-types'
|
|||
import {Controlled as CodeMirror} from 'react-codemirror2'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
|
||||
import 'codemirror/mode/javascript/javascript'
|
||||
import 'codemirror/addon/lint/lint'
|
||||
|
@ -30,7 +29,7 @@ class JSONEditor extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
this.setState({
|
||||
code: JSON.stringify(nextProps.layer, null, 2)
|
||||
})
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
|
||||
|
||||
import JSONEditor from './JSONEditor'
|
||||
import FilterEditor from '../filter/FilterEditor'
|
||||
|
@ -13,17 +14,14 @@ import CommentBlock from './CommentBlock'
|
|||
import LayerSourceBlock from './LayerSourceBlock'
|
||||
import LayerSourceLayerBlock from './LayerSourceLayerBlock'
|
||||
|
||||
import MoreVertIcon from 'react-icons/lib/md/more-vert'
|
||||
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import MultiButtonInput from '../inputs/MultiButtonInput'
|
||||
|
||||
import { changeType, changeProperty } from '../../libs/layer'
|
||||
import layout from '../../config/layout.json'
|
||||
|
||||
class UnsupportedLayer extends React.Component {
|
||||
render() {
|
||||
return <div></div>
|
||||
}
|
||||
}
|
||||
|
||||
function layoutGroups(layerType) {
|
||||
const layerGroup = {
|
||||
|
@ -50,6 +48,13 @@ export default class LayerEditor extends React.Component {
|
|||
spec: PropTypes.object.isRequired,
|
||||
onLayerChanged: PropTypes.func,
|
||||
onLayerIdChange: PropTypes.func,
|
||||
onMoveLayer: PropTypes.func,
|
||||
onLayerDestroy: PropTypes.func,
|
||||
onLayerCopy: PropTypes.func,
|
||||
onLayerVisibilityToggle: PropTypes.func,
|
||||
isFirstLayer: PropTypes.bool,
|
||||
isLastLayer: PropTypes.bool,
|
||||
layerIndex: PropTypes.number,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -74,7 +79,7 @@ export default class LayerEditor extends React.Component {
|
|||
this.state = { editorGroups }
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
const additionalGroups = { ...this.state.editorGroups }
|
||||
|
||||
layout[nextProps.layer.type].groups.forEach(group => {
|
||||
|
@ -126,6 +131,7 @@ export default class LayerEditor extends React.Component {
|
|||
case 'layer': return <div>
|
||||
<LayerIdBlock
|
||||
value={this.props.layer.id}
|
||||
wdKey="layer-editor.layer-id"
|
||||
onChange={newId => this.props.onLayerIdChange(this.props.layer.id, newId)}
|
||||
/>
|
||||
<LayerTypeBlock
|
||||
|
@ -177,16 +183,23 @@ export default class LayerEditor extends React.Component {
|
|||
layer={this.props.layer}
|
||||
onChange={this.props.onLayerChanged}
|
||||
/>
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
moveLayer(offset) {
|
||||
this.props.onMoveLayer({
|
||||
oldIndex: this.props.layerIndex,
|
||||
newIndex: this.props.layerIndex+offset
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const layerType = this.props.layer.type
|
||||
const groups = layoutGroups(layerType).filter(group => {
|
||||
return !(layerType === 'background' && group.type === 'source')
|
||||
}).map(group => {
|
||||
return <LayerEditorGroup
|
||||
data-wd-key={group.title}
|
||||
key={group.title}
|
||||
title={group.title}
|
||||
isActive={this.state.editorGroups[group.title]}
|
||||
|
@ -196,8 +209,73 @@ export default class LayerEditor extends React.Component {
|
|||
</LayerEditorGroup>
|
||||
})
|
||||
|
||||
const layout = this.props.layer.layout || {}
|
||||
|
||||
const items = {
|
||||
delete: {
|
||||
text: "Delete",
|
||||
handler: () => this.props.onLayerDestroy(this.props.layer.id)
|
||||
},
|
||||
duplicate: {
|
||||
text: "Duplicate",
|
||||
handler: () => this.props.onLayerCopy(this.props.layer.id)
|
||||
},
|
||||
hide: {
|
||||
text: (layout.visibility === "none") ? "Show" : "Hide",
|
||||
handler: () => this.props.onLayerVisibilityToggle(this.props.layer.id)
|
||||
},
|
||||
moveLayerUp: {
|
||||
text: "Move layer up",
|
||||
// Not actually used...
|
||||
disabled: this.props.isFirstLayer,
|
||||
handler: () => this.moveLayer(-1)
|
||||
},
|
||||
moveLayerDown: {
|
||||
text: "Move layer down",
|
||||
// Not actually used...
|
||||
disabled: this.props.isLastLayer,
|
||||
handler: () => this.moveLayer(+1)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelection(id, event) {
|
||||
event.stopPropagation;
|
||||
items[id].handler();
|
||||
}
|
||||
|
||||
return <div className="maputnik-layer-editor"
|
||||
>
|
||||
<header>
|
||||
<div className="layer-header">
|
||||
<h2 className="layer-header__title">
|
||||
Layer: {this.props.layer.id}
|
||||
</h2>
|
||||
<div className="layer-header__info">
|
||||
<Wrapper
|
||||
className='more-menu'
|
||||
onSelection={handleSelection}
|
||||
closeOnSelection={false}
|
||||
>
|
||||
<Button className='more-menu__button'>
|
||||
<MoreVertIcon className="more-menu__button__svg" />
|
||||
</Button>
|
||||
<Menu>
|
||||
<ul className="more-menu__menu">
|
||||
{Object.keys(items).map((id, idx) => {
|
||||
const item = items[id];
|
||||
return <li key={id}>
|
||||
<MenuItem value={id} className='more-menu__menu__item'>
|
||||
{item.text}
|
||||
</MenuItem>
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
</Menu>
|
||||
</Wrapper>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</header>
|
||||
{groups}
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Collapse from 'react-collapse'
|
||||
import Collapser from './Collapser'
|
||||
import Collapse from './Collapse'
|
||||
|
||||
|
||||
export default class LayerEditorGroup extends React.Component {
|
||||
static propTypes = {
|
||||
"data-wd-key": PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
children: PropTypes.element.isRequired,
|
||||
|
@ -14,14 +16,17 @@ export default class LayerEditorGroup extends React.Component {
|
|||
render() {
|
||||
return <div>
|
||||
<div className="maputnik-layer-editor-group"
|
||||
data-wd-key={"layer-editor-group:"+this.props["data-wd-key"]}
|
||||
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
|
||||
>
|
||||
<span>{this.props.title}</span>
|
||||
<span style={{flexGrow: 1}} />
|
||||
<Collapser isCollapsed={this.props.isActive} />
|
||||
</div>
|
||||
<Collapse isOpened={this.props.isActive}>
|
||||
<Collapse isActive={this.props.isActive}>
|
||||
<div className="react-collapse-container">
|
||||
{this.props.children}
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
|
||||
class LayerIdBlock extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
wdKey: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"ID"} doc={styleSpec.latest.layer.id.doc}>
|
||||
return <InputBlock label={"ID"} doc={styleSpec.latest.layer.id.doc}
|
||||
data-wd-key={this.props.wdKey}
|
||||
>
|
||||
<StringInput
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
|
||||
import Button from '../Button'
|
||||
import LayerListGroup from './LayerListGroup'
|
||||
|
@ -10,7 +9,7 @@ import AddIcon from 'react-icons/lib/md/add-circle-outline'
|
|||
import AddModal from '../modals/AddModal'
|
||||
|
||||
import style from '../../libs/style.js'
|
||||
import {SortableContainer, SortableHandle, arrayMove} from 'react-sortable-hoc';
|
||||
import {SortableContainer, SortableHandle} from 'react-sortable-hoc';
|
||||
|
||||
const layerListPropTypes = {
|
||||
layers: PropTypes.array.isRequired,
|
||||
|
@ -57,36 +56,6 @@ class LayerListContainer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onLayerDestroy(layerId) {
|
||||
const remainingLayers = this.props.layers.slice(0)
|
||||
const idx = style.indexOfLayer(remainingLayers, layerId)
|
||||
remainingLayers.splice(idx, 1);
|
||||
this.props.onLayersChange(remainingLayers)
|
||||
}
|
||||
|
||||
onLayerCopy(layerId) {
|
||||
const changedLayers = this.props.layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, layerId)
|
||||
|
||||
const clonedLayer = cloneDeep(changedLayers[idx])
|
||||
clonedLayer.id = clonedLayer.id + "-copy"
|
||||
changedLayers.splice(idx, 0, clonedLayer)
|
||||
this.props.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
onLayerVisibilityToggle(layerId) {
|
||||
const changedLayers = this.props.layers.slice(0)
|
||||
const idx = style.indexOfLayer(changedLayers, layerId)
|
||||
|
||||
const layer = { ...changedLayers[idx] }
|
||||
const changedLayout = 'layout' in layer ? {...layer.layout} : {}
|
||||
changedLayout.visibility = changedLayout.visibility === 'none' ? 'visible' : 'none'
|
||||
|
||||
layer.layout = changedLayout
|
||||
changedLayers[idx] = layer
|
||||
this.props.onLayersChange(changedLayers)
|
||||
}
|
||||
|
||||
toggleModal(modalName) {
|
||||
this.setState({
|
||||
isOpen: {
|
||||
|
@ -162,6 +131,7 @@ class LayerListContainer extends React.Component {
|
|||
const groupPrefix = layerPrefix(layers[0].id)
|
||||
if(layers.length > 1) {
|
||||
const grp = <LayerListGroup
|
||||
data-wd-key={[groupPrefix, idx].join('-')}
|
||||
key={[groupPrefix, idx].join('-')}
|
||||
title={groupPrefix}
|
||||
isActive={!this.isCollapsed(groupPrefix, idx) || idx === this.props.selectedLayerIndex}
|
||||
|
@ -185,9 +155,9 @@ class LayerListContainer extends React.Component {
|
|||
visibility={(layer.layout || {}).visibility}
|
||||
isSelected={idx === this.props.selectedLayerIndex}
|
||||
onLayerSelect={this.props.onLayerSelect}
|
||||
onLayerDestroy={this.onLayerDestroy.bind(this)}
|
||||
onLayerCopy={this.onLayerCopy.bind(this)}
|
||||
onLayerVisibilityToggle={this.onLayerVisibilityToggle.bind(this)}
|
||||
onLayerDestroy={this.props.onLayerDestroy.bind(this)}
|
||||
onLayerCopy={this.props.onLayerCopy.bind(this)}
|
||||
onLayerVisibilityToggle={this.props.onLayerVisibilityToggle.bind(this)}
|
||||
/>
|
||||
listItems.push(listItem)
|
||||
idx += 1
|
||||
|
@ -207,20 +177,22 @@ class LayerListContainer extends React.Component {
|
|||
<span className="maputnik-space" />
|
||||
<div className="maputnik-default-property">
|
||||
<div className="maputnik-multibutton">
|
||||
<a
|
||||
<button
|
||||
id="skip-menu"
|
||||
onClick={this.toggleLayers.bind(this)}
|
||||
className="maputnik-button">
|
||||
{this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"}
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="maputnik-default-property">
|
||||
<div className="maputnik-multibutton">
|
||||
<a
|
||||
<button
|
||||
onClick={this.toggleModal.bind(this, 'add')}
|
||||
data-wd-key="layer-list:add-layer"
|
||||
className="maputnik-button maputnik-button-selected">
|
||||
Add Layer
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
@ -234,18 +206,10 @@ class LayerListContainer extends React.Component {
|
|||
export default class LayerList extends React.Component {
|
||||
static propTypes = {...layerListPropTypes}
|
||||
|
||||
onSortEnd(move) {
|
||||
const { oldIndex, newIndex } = move
|
||||
if(oldIndex === newIndex) return
|
||||
let layers = this.props.layers.slice(0)
|
||||
layers = arrayMove(layers, oldIndex, newIndex)
|
||||
this.props.onLayersChange(layers)
|
||||
}
|
||||
|
||||
render() {
|
||||
return <LayerListContainer
|
||||
{...this.props}
|
||||
onSortEnd={this.onSortEnd.bind(this)}
|
||||
onSortEnd={this.props.onMoveLayer.bind(this)}
|
||||
useDragHandle={true}
|
||||
/>
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import Collapser from './Collapser'
|
|||
export default class LayerListGroup extends React.Component {
|
||||
static propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
"data-wd-key": PropTypes.string,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
onActiveToggle: PropTypes.func.isRequired
|
||||
}
|
||||
|
@ -12,6 +13,7 @@ export default class LayerListGroup extends React.Component {
|
|||
render() {
|
||||
return <li className="maputnik-layer-list-group">
|
||||
<div className="maputnik-layer-list-group-header"
|
||||
data-wd-key={"layer-list-group:"+this.props["data-wd-key"]}
|
||||
onClick={e => this.props.onActiveToggle(!this.props.isActive)}
|
||||
>
|
||||
<span className="maputnik-layer-list-group-title">{this.props.title}</span>
|
||||
|
|
|
@ -33,25 +33,28 @@ class IconAction extends React.Component {
|
|||
static propTypes = {
|
||||
action: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
wdKey: PropTypes.string
|
||||
}
|
||||
|
||||
renderIcon() {
|
||||
switch(this.props.action) {
|
||||
case 'copy': return <CopyIcon />
|
||||
case 'duplicate': return <CopyIcon />
|
||||
case 'show': return <VisibilityIcon />
|
||||
case 'hide': return <VisibilityOffIcon />
|
||||
case 'delete': return <DeleteIcon />
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <a
|
||||
return <button
|
||||
tabIndex="-1"
|
||||
title={this.props.action}
|
||||
className="maputnik-layer-list-icon-action"
|
||||
data-wd-key={this.props.wdKey}
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
{this.renderIcon()}
|
||||
</a>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,6 +95,7 @@ class LayerListItem extends React.Component {
|
|||
return <li
|
||||
key={this.props.layerId}
|
||||
onClick={e => this.props.onLayerSelect(this.props.layerId)}
|
||||
data-wd-key={"layer-list-item:"+this.props.layerId}
|
||||
className={classnames({
|
||||
"maputnik-layer-list-item": true,
|
||||
"maputnik-layer-list-item-selected": this.props.isSelected,
|
||||
|
@ -101,14 +105,17 @@ class LayerListItem extends React.Component {
|
|||
<span className="maputnik-layer-list-item-id">{this.props.layerId}</span>
|
||||
<span style={{flexGrow: 1}} />
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":delete"}
|
||||
action={'delete'}
|
||||
onClick={e => this.props.onLayerDestroy(this.props.layerId)}
|
||||
/>
|
||||
<IconAction
|
||||
action={'copy'}
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":copy"}
|
||||
action={'duplicate'}
|
||||
onClick={e => this.props.onLayerCopy(this.props.layerId)}
|
||||
/>
|
||||
<IconAction
|
||||
wdKey={"layer-list-item:"+this.props.layerId+":toggle-visibility"}
|
||||
action={this.props.visibility === 'visible' ? 'hide' : 'show'}
|
||||
onClick={e => this.props.onLayerVisibilityToggle(this.props.layerId)}
|
||||
/>
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import AutocompleteInput from '../inputs/AutocompleteInput'
|
||||
|
||||
class LayerSourceBlock extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
wdKey: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
sourceIds: PropTypes.array,
|
||||
}
|
||||
|
@ -20,7 +20,9 @@ class LayerSourceBlock extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Source"} doc={styleSpec.latest.layer.source.doc}>
|
||||
return <InputBlock label={"Source"} doc={styleSpec.latest.layer.source.doc}
|
||||
data-wd-key={this.props.wdKey}
|
||||
>
|
||||
<AutocompleteInput
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import AutocompleteInput from '../inputs/AutocompleteInput'
|
||||
|
||||
class LayerSourceLayer extends React.Component {
|
||||
|
@ -22,7 +21,9 @@ class LayerSourceLayer extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Source Layer"} doc={styleSpec.latest.layer['source-layer'].doc}>
|
||||
return <InputBlock label={"Source Layer"} doc={styleSpec.latest.layer['source-layer'].doc}
|
||||
data-wd-key="layer-source-layer"
|
||||
>
|
||||
<AutocompleteInput
|
||||
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
||||
value={this.props.value}
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
|
||||
class LayerTypeBlock extends React.Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
wdKey: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Type"} doc={styleSpec.latest.layer.type.doc}>
|
||||
return <InputBlock label={"Type"} doc={styleSpec.latest.layer.type.doc}
|
||||
data-wd-key={this.props.wdKey}
|
||||
>
|
||||
<SelectInput
|
||||
options={[
|
||||
['background', 'Background'],
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
|
||||
|
@ -12,7 +12,9 @@ class MaxZoomBlock extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Max Zoom"} doc={styleSpec.latest.layer.maxzoom.doc}>
|
||||
return <InputBlock label={"Max Zoom"} doc={styleSpec.latest.layer.maxzoom.doc}
|
||||
data-wd-key="max-zoom"
|
||||
>
|
||||
<NumberInput
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
|
||||
|
@ -12,7 +12,9 @@ class MinZoomBlock extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"Min Zoom"} doc={styleSpec.latest.layer.minzoom.doc}>
|
||||
return <InputBlock label={"Min Zoom"} doc={styleSpec.latest.layer.minzoom.doc}
|
||||
data-wd-key="min-zoom"
|
||||
>
|
||||
<NumberInput
|
||||
value={this.props.value}
|
||||
onChange={this.props.onChange}
|
||||
|
|
|
@ -58,6 +58,7 @@ export default class MapboxGlMap extends React.Component {
|
|||
mapStyle: PropTypes.object.isRequired,
|
||||
inspectModeEnabled: PropTypes.bool.isRequired,
|
||||
highlightedLayer: PropTypes.object,
|
||||
options: PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -65,6 +66,7 @@ export default class MapboxGlMap extends React.Component {
|
|||
onDataChange: () => {},
|
||||
onLayerSelect: () => {},
|
||||
mapboxAccessToken: tokens.mapbox,
|
||||
options: {},
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
@ -79,7 +81,7 @@ export default class MapboxGlMap extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if(!this.state.map) return
|
||||
const metadata = nextProps.mapStyle.metadata || {}
|
||||
MapboxGl.accessToken = metadata['maputnik:mapbox_access_token'] || tokens.mapbox
|
||||
|
@ -92,20 +94,29 @@ export default class MapboxGlMap extends React.Component {
|
|||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const map = this.state.map;
|
||||
|
||||
if(this.props.inspectModeEnabled !== prevProps.inspectModeEnabled) {
|
||||
this.state.inspect.toggleInspector()
|
||||
}
|
||||
if(this.props.inspectModeEnabled) {
|
||||
this.state.inspect.render()
|
||||
}
|
||||
|
||||
map.showTileBoundaries = this.props.options.showTileBoundaries;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const map = new MapboxGl.Map({
|
||||
const mapOpts = {
|
||||
...this.props.options,
|
||||
container: this.container,
|
||||
style: this.props.mapStyle,
|
||||
hash: true,
|
||||
})
|
||||
}
|
||||
|
||||
const map = new MapboxGl.Map(mapOpts);
|
||||
|
||||
map.showTileBoundaries = mapOpts.showTileBoundaries;
|
||||
|
||||
const zoom = new ZoomControl;
|
||||
map.addControl(zoom, 'top-right');
|
||||
|
@ -121,6 +132,7 @@ export default class MapboxGlMap extends React.Component {
|
|||
showMapPopupOnHover: false,
|
||||
showInspectMapPopupOnHover: true,
|
||||
showInspectButton: false,
|
||||
blockHoverPopupOnClick: true,
|
||||
assignLayerColor: (layerId, alpha) => {
|
||||
return Color(colors.brightColor(layerId, alpha)).desaturate(0.5).string()
|
||||
},
|
||||
|
|
|
@ -29,7 +29,7 @@ class OpenLayers3Map extends React.Component {
|
|||
const styleFunc = olms.apply(this.map, newMapStyle)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
require.ensure(["ol", "ol-mapbox-style"], () => {
|
||||
if(!this.map) return
|
||||
this.updateStyle(nextProps.mapStyle)
|
||||
|
|
|
@ -4,7 +4,6 @@ import PropTypes from 'prop-types'
|
|||
import Button from '../Button'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import Modal from './Modal'
|
||||
|
||||
import LayerTypeBlock from '../layers/LayerTypeBlock'
|
||||
|
@ -56,7 +55,7 @@ class AddModal extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps, nextState) {
|
||||
UNSAFE_componentWillUpdate(nextProps, nextState) {
|
||||
// Check if source is valid for new type
|
||||
const oldType = this.state.type;
|
||||
const newType = nextState.type;
|
||||
|
@ -119,19 +118,25 @@ class AddModal extends React.Component {
|
|||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Add Layer'}
|
||||
data-wd-key="modal:add-layer"
|
||||
>
|
||||
<div className="maputnik-add-layer">
|
||||
<LayerIdBlock
|
||||
value={this.state.id}
|
||||
onChange={v => this.setState({ id: v })}
|
||||
wdKey="add-layer.layer-id"
|
||||
onChange={v => {
|
||||
this.setState({ id: v })
|
||||
}}
|
||||
/>
|
||||
<LayerTypeBlock
|
||||
value={this.state.type}
|
||||
wdKey="add-layer.layer-type"
|
||||
onChange={v => this.setState({ type: v })}
|
||||
/>
|
||||
{this.state.type !== 'background' &&
|
||||
<LayerSourceBlock
|
||||
sourceIds={sources}
|
||||
wdKey="add-layer.layer-source-block"
|
||||
value={this.state.source}
|
||||
onChange={v => this.setState({ source: v })}
|
||||
/>
|
||||
|
@ -144,7 +149,11 @@ class AddModal extends React.Component {
|
|||
onChange={v => this.setState({ 'source-layer': v })}
|
||||
/>
|
||||
}
|
||||
<Button className="maputnik-add-layer-button" onClick={this.addLayer.bind(this)}>
|
||||
<Button
|
||||
className="maputnik-add-layer-button"
|
||||
onClick={this.addLayer.bind(this)}
|
||||
data-wd-key="add-layer"
|
||||
>
|
||||
Add Layer
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -2,10 +2,9 @@ import React from 'react'
|
|||
import PropTypes from 'prop-types'
|
||||
import { saveAs } from 'file-saver'
|
||||
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
import CheckboxInput from '../inputs/CheckboxInput'
|
||||
import Button from '../Button'
|
||||
import Modal from './Modal'
|
||||
|
@ -32,7 +31,7 @@ class Gist extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
preview: !!(nextProps.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']
|
||||
|
@ -236,12 +235,27 @@ class ExportModal extends React.Component {
|
|||
}
|
||||
|
||||
downloadStyle() {
|
||||
const blob = new Blob([styleSpec.format(stripAccessTokens(this.props.mapStyle))], {type: "application/json;charset=utf-8"});
|
||||
const tokenStyle = styleSpec.format(stripAccessTokens(style.replaceAccessToken(this.props.mapStyle)));
|
||||
|
||||
const blob = new Blob([tokenStyle], {type: "application/json;charset=utf-8"});
|
||||
saveAs(blob, this.props.mapStyle.id + ".json");
|
||||
}
|
||||
|
||||
changeMetadataProperty(property, value) {
|
||||
const changedStyle = {
|
||||
...this.props.mapStyle,
|
||||
metadata: {
|
||||
...this.props.mapStyle.metadata,
|
||||
[property]: value
|
||||
}
|
||||
}
|
||||
this.props.onStyleChanged(changedStyle)
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return <Modal
|
||||
data-wd-key="export-modal"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Export Style'}
|
||||
|
@ -252,13 +266,29 @@ class ExportModal extends React.Component {
|
|||
<p>
|
||||
Download a JSON style to your computer.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<InputBlock label={"OpenMapTiles Access Token: "}>
|
||||
<StringInput
|
||||
value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Mapbox Access Token: "}>
|
||||
<StringInput
|
||||
value={(this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
|
||||
/>
|
||||
</InputBlock>
|
||||
</p>
|
||||
|
||||
<Button onClick={this.downloadStyle.bind(this)}>
|
||||
<MdFileDownload />
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="maputnik-modal-section">
|
||||
<div className="maputnik-modal-section hide">
|
||||
<h4>Save style</h4>
|
||||
<Gist mapStyle={this.props.mapStyle} onStyleChanged={this.props.onStyleChanged}/>
|
||||
</div>
|
||||
|
|
49
src/components/modals/LoadingModal.jsx
Normal file
49
src/components/modals/LoadingModal.jsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Button from '../Button'
|
||||
import Modal from './Modal'
|
||||
|
||||
|
||||
class LoadingModal extends React.Component {
|
||||
static propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
message: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
underlayOnClick(e) {
|
||||
// This stops click events falling through to underlying modals.
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Modal
|
||||
data-wd-key="loading-modal"
|
||||
isOpen={this.props.isOpen}
|
||||
underlayClickExits={false}
|
||||
underlayProps={{
|
||||
onClick: (e) => underlayProps(e)
|
||||
}}
|
||||
closeable={false}
|
||||
title={this.props.title}
|
||||
onOpenToggle={() => this.props.onCancel()}
|
||||
>
|
||||
<p>
|
||||
{this.props.message}
|
||||
</p>
|
||||
<p className="maputnik-dialog__buttons">
|
||||
<Button onClick={(e) => this.props.onCancel(e)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</p>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
export default LoadingModal
|
|
@ -1,33 +1,61 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import CloseIcon from 'react-icons/lib/md/close'
|
||||
import Overlay from './Overlay'
|
||||
import AriaModal from 'react-aria-modal'
|
||||
|
||||
|
||||
class Modal extends React.Component {
|
||||
static propTypes = {
|
||||
"data-wd-key": PropTypes.string,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
underlayClickExits: PropTypes.bool,
|
||||
underlayProps: PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
underlayClickExits: true
|
||||
}
|
||||
|
||||
getApplicationNode() {
|
||||
return document.getElementById('app');
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Overlay isOpen={this.props.isOpen}>
|
||||
<div className="maputnik-modal">
|
||||
if(this.props.isOpen) {
|
||||
return <AriaModal
|
||||
titleText={this.props.title}
|
||||
underlayClickExits={this.props.underlayClickExits}
|
||||
underlayProps={this.props.underlayProps}
|
||||
getApplicationNode={this.getApplicationNode}
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
verticallyCenter={true}
|
||||
onExit={() => this.props.onOpenToggle(false)}
|
||||
>
|
||||
<div className="maputnik-modal"
|
||||
data-wd-key={this.props["data-wd-key"]}
|
||||
>
|
||||
<header className="maputnik-modal-header">
|
||||
<h1 className="maputnik-modal-header-title">{this.props.title}</h1>
|
||||
<span className="maputnik-modal-header-space"></span>
|
||||
<a className="maputnik-modal-header-toggle"
|
||||
<button className="maputnik-modal-header-toggle"
|
||||
onClick={() => this.props.onOpenToggle(false)}
|
||||
data-wd-key={this.props["data-wd-key"]+".close-modal"}
|
||||
>
|
||||
<CloseIcon />
|
||||
</a>
|
||||
</button>
|
||||
</header>
|
||||
<div className="maputnik-modal-scroller">
|
||||
<div className="maputnik-modal-content">{this.props.children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Overlay>
|
||||
</AriaModal>
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import LoadingModal from './LoadingModal'
|
||||
import Modal from './Modal'
|
||||
import Button from '../Button'
|
||||
import FileReaderInput from 'react-file-reader-input'
|
||||
|
@ -23,6 +24,7 @@ class PublicStyle extends React.Component {
|
|||
return <div className="maputnik-public-style">
|
||||
<Button
|
||||
className="maputnik-public-style-button"
|
||||
aria-label={this.props.title}
|
||||
onClick={() => this.props.onSelect(this.props.url)}
|
||||
>
|
||||
<header className="maputnik-public-style-header">
|
||||
|
@ -30,11 +32,12 @@ class PublicStyle extends React.Component {
|
|||
<span className="maputnik-space" />
|
||||
<AddIcon />
|
||||
</header>
|
||||
<img
|
||||
<div
|
||||
className="maputnik-public-style-thumbnail"
|
||||
src={this.props.thumbnailUrl}
|
||||
alt={this.props.title}
|
||||
/>
|
||||
style={{
|
||||
backgroundImage: `url(${this.props.thumbnailUrl})`
|
||||
}}
|
||||
></div>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
@ -58,13 +61,33 @@ class OpenModal extends React.Component {
|
|||
})
|
||||
}
|
||||
|
||||
onCancelActiveRequest(e) {
|
||||
// Else the click propagates to the underlying modal
|
||||
if(e) e.stopPropagation();
|
||||
|
||||
if(this.state.activeRequest) {
|
||||
this.state.activeRequest.abort();
|
||||
this.setState({
|
||||
activeRequest: null,
|
||||
activeRequestUrl: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onStyleSelect(styleUrl) {
|
||||
this.clearError();
|
||||
|
||||
request({
|
||||
const reqOpts = {
|
||||
url: styleUrl,
|
||||
withCredentials: false,
|
||||
}, (error, response, body) => {
|
||||
}
|
||||
|
||||
const activeRequest = request(reqOpts, (error, response, body) => {
|
||||
this.setState({
|
||||
activeRequest: null,
|
||||
activeRequestUrl: null
|
||||
});
|
||||
|
||||
if (!error && response.statusCode == 200) {
|
||||
const mapStyle = style.ensureStyleValidity(JSON.parse(body))
|
||||
console.log('Loaded style ', mapStyle.id)
|
||||
|
@ -74,6 +97,11 @@ class OpenModal extends React.Component {
|
|||
console.warn('Could not open the style URL', styleUrl)
|
||||
}
|
||||
})
|
||||
|
||||
this.setState({
|
||||
activeRequest: activeRequest,
|
||||
activeRequestUrl: reqOpts.url
|
||||
})
|
||||
}
|
||||
|
||||
onOpenUrl() {
|
||||
|
@ -133,6 +161,7 @@ class OpenModal extends React.Component {
|
|||
}
|
||||
|
||||
return <Modal
|
||||
data-wd-key="open-modal"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={() => this.onOpenToggle()}
|
||||
title={'Open Style'}
|
||||
|
@ -141,7 +170,7 @@ class OpenModal extends React.Component {
|
|||
<section className="maputnik-modal-section">
|
||||
<h2>Upload Style</h2>
|
||||
<p>Upload a JSON style from your computer.</p>
|
||||
<FileReaderInput onChange={this.onUpload.bind(this)}>
|
||||
<FileReaderInput onChange={this.onUpload.bind(this)} tabIndex="-1">
|
||||
<Button className="maputnik-upload-button"><FileUploadIcon /> Upload</Button>
|
||||
</FileReaderInput>
|
||||
</section>
|
||||
|
@ -151,9 +180,9 @@ class OpenModal extends React.Component {
|
|||
<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>.
|
||||
</p>
|
||||
<input type="text" ref={(input) => this.styleUrlElement = input} className="maputnik-input" placeholder="Enter URL..."/>
|
||||
<input data-wd-key="open-modal.url.input" type="text" ref={(input) => this.styleUrlElement = input} className="maputnik-input" placeholder="Enter URL..."/>
|
||||
<div>
|
||||
<Button className="maputnik-big-button" onClick={this.onOpenUrl.bind(this)}>Open URL</Button>
|
||||
<Button data-wd-key="open-modal.url.button" className="maputnik-big-button" onClick={this.onOpenUrl.bind(this)}>Open URL</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
@ -166,6 +195,13 @@ class OpenModal extends React.Component {
|
|||
{styleOptions}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<LoadingModal
|
||||
isOpen={!!this.state.activeRequest}
|
||||
title={'Loading style'}
|
||||
onCancel={(e) => this.onCancelActiveRequest(e)}
|
||||
message={"Loading: "+this.state.activeRequestUrl}
|
||||
/>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
|
||||
class Overlay extends React.Component {
|
||||
static propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
children: PropTypes.element.isRequired
|
||||
}
|
||||
|
||||
render() {
|
||||
let overlayStyle = {}
|
||||
if(!this.props.isOpen) {
|
||||
overlayStyle['display'] = 'none';
|
||||
}
|
||||
|
||||
return <div className={"maputnik-overlay"} style={overlayStyle}>
|
||||
<div className={"maputnik-overlay-viewport"} />
|
||||
{this.props.children}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
export default Overlay
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
|
@ -42,6 +42,7 @@ class SettingsModal extends React.Component {
|
|||
const metadata = this.props.mapStyle.metadata || {}
|
||||
const inputProps = { }
|
||||
return <Modal
|
||||
data-wd-key="modal-settings"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Style Settings'}
|
||||
|
@ -49,18 +50,21 @@ class SettingsModal extends React.Component {
|
|||
<div style={{minWidth: 350}}>
|
||||
<InputBlock label={"Name"} doc={styleSpec.latest.$root.name.doc}>
|
||||
<StringInput {...inputProps}
|
||||
data-wd-key="modal-settings.name"
|
||||
value={this.props.mapStyle.name}
|
||||
onChange={this.changeStyleProperty.bind(this, "name")}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Owner"} doc={"Owner ID of the style. Used by Mapbox or future style APIs."}>
|
||||
<StringInput {...inputProps}
|
||||
data-wd-key="modal-settings.owner"
|
||||
value={this.props.mapStyle.owner}
|
||||
onChange={this.changeStyleProperty.bind(this, "owner")}
|
||||
/>
|
||||
</InputBlock>
|
||||
<InputBlock label={"Sprite URL"} doc={styleSpec.latest.$root.sprite.doc}>
|
||||
<StringInput {...inputProps}
|
||||
data-wd-key="modal-settings.sprite"
|
||||
value={this.props.mapStyle.sprite}
|
||||
onChange={this.changeStyleProperty.bind(this, "sprite")}
|
||||
/>
|
||||
|
@ -68,6 +72,7 @@ class SettingsModal extends React.Component {
|
|||
|
||||
<InputBlock label={"Glyphs URL"} doc={styleSpec.latest.$root.glyphs.doc}>
|
||||
<StringInput {...inputProps}
|
||||
data-wd-key="modal-settings.glyphs"
|
||||
value={this.props.mapStyle.glyphs}
|
||||
onChange={this.changeStyleProperty.bind(this, "glyphs")}
|
||||
/>
|
||||
|
@ -75,6 +80,7 @@ class SettingsModal extends React.Component {
|
|||
|
||||
<InputBlock label={"Mapbox Access Token"} doc={"Public access token for Mapbox services."}>
|
||||
<StringInput {...inputProps}
|
||||
data-wd-key="modal-settings.maputnik:mapbox_access_token"
|
||||
value={metadata['maputnik:mapbox_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
|
||||
/>
|
||||
|
@ -82,6 +88,7 @@ class SettingsModal extends React.Component {
|
|||
|
||||
<InputBlock label={"OpenMapTiles Access Token"} doc={"Public access token for the OpenMapTiles CDN."}>
|
||||
<StringInput {...inputProps}
|
||||
data-wd-key="modal-settings.maputnik:openmaptiles_access_token"
|
||||
value={metadata['maputnik:openmaptiles_access_token']}
|
||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
|
||||
/>
|
||||
|
@ -89,6 +96,7 @@ class SettingsModal extends React.Component {
|
|||
|
||||
<InputBlock label={"Style Renderer"} doc={"Choose the default Maputnik renderer for this style."}>
|
||||
<SelectInput {...inputProps}
|
||||
data-wd-key="modal-settings.maputnik:renderer"
|
||||
options={[
|
||||
['mbgljs', 'MapboxGL JS'],
|
||||
['ol3', 'Open Layers 3'],
|
||||
|
|
73
src/components/modals/ShortcutsModal.jsx
Normal file
73
src/components/modals/ShortcutsModal.jsx
Normal file
|
@ -0,0 +1,73 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Button from '../Button'
|
||||
import Modal from './Modal'
|
||||
|
||||
|
||||
class ShortcutsModal extends React.Component {
|
||||
static propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const help = [
|
||||
{
|
||||
key: "?",
|
||||
text: "Shortcuts menu"
|
||||
},
|
||||
{
|
||||
key: "o",
|
||||
text: "Open modal"
|
||||
},
|
||||
{
|
||||
key: "e",
|
||||
text: "Export modal"
|
||||
},
|
||||
{
|
||||
key: "d",
|
||||
text: "Data Sources modal"
|
||||
},
|
||||
{
|
||||
key: "s",
|
||||
text: "Style Settings modal"
|
||||
},
|
||||
{
|
||||
key: "i",
|
||||
text: "Toggle inspect"
|
||||
},
|
||||
{
|
||||
key: "m",
|
||||
text: "Focus map"
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
return <Modal
|
||||
data-wd-key="shortcuts-modal"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title={'Shortcuts'}
|
||||
>
|
||||
<div className="maputnik-modal-section maputnik-modal-shortcuts">
|
||||
<p>
|
||||
Press <code>ESC</code> to lose focus of any active elements, then press one of:
|
||||
</p>
|
||||
<ul>
|
||||
{help.map((item) => {
|
||||
return <li key={item.key}>
|
||||
<code>{item.key}</code> {item.text}
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
export default ShortcutsModal
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import Modal from './Modal'
|
||||
import Button from '../Button'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
|
|
41
src/components/modals/SurveyModal.jsx
Normal file
41
src/components/modals/SurveyModal.jsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Button from '../Button'
|
||||
import Modal from './Modal'
|
||||
|
||||
import logoImage from 'maputnik-design/logos/logo-color.svg'
|
||||
|
||||
class SurveyModal extends React.Component {
|
||||
static propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onOpenToggle: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) { super(props); }
|
||||
|
||||
onClick = () => {
|
||||
window.open('https://gregorywolanski.typeform.com/to/cPgaSY', '_blank');
|
||||
|
||||
this.props.onOpenToggle();
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Modal
|
||||
data-wd-key="modal-survey"
|
||||
isOpen={this.props.isOpen}
|
||||
onOpenToggle={this.props.onOpenToggle}
|
||||
title="Maputnik Survey"
|
||||
>
|
||||
<div className="maputnik-modal-survey">
|
||||
<img className="maputnik-modal-survey__logo" src={logoImage} alt="" width="128" />
|
||||
<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>
|
||||
<Button onClick={this.onClick} className="maputnik-big-button maputnik-white-button maputnik-wide-button">Take the Maputnik Survey</Button>
|
||||
<p className="maputnik-modal-survey__footnote">It takes 7 minutes, tops! Every question is optional.</p>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
}
|
||||
|
||||
export default SurveyModal
|
|
@ -1,18 +1,22 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import InputBlock from '../inputs/InputBlock'
|
||||
import StringInput from '../inputs/StringInput'
|
||||
import NumberInput from '../inputs/NumberInput'
|
||||
import SelectInput from '../inputs/SelectInput'
|
||||
|
||||
|
||||
class TileJSONSourceEditor extends React.Component {
|
||||
static propTypes = {
|
||||
source: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
}
|
||||
|
||||
render() {
|
||||
return <InputBlock label={"TileJSON URL"} doc={styleSpec.latest.source_vector.url.doc}>
|
||||
return <div>
|
||||
<InputBlock label={"TileJSON URL"} doc={styleSpec.latest.source_vector.url.doc}>
|
||||
<StringInput
|
||||
value={this.props.source.url}
|
||||
onChange={url => this.props.onChange({
|
||||
|
@ -21,6 +25,8 @@ class TileJSONSourceEditor extends React.Component {
|
|||
})}
|
||||
/>
|
||||
</InputBlock>
|
||||
{this.props.children}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,6 +34,7 @@ class TileURLSourceEditor extends React.Component {
|
|||
static propTypes = {
|
||||
source: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
children: PropTypes.node,
|
||||
}
|
||||
|
||||
changeTileUrl(idx, value) {
|
||||
|
@ -73,6 +80,7 @@ class TileURLSourceEditor extends React.Component {
|
|||
})}
|
||||
/>
|
||||
</InputBlock>
|
||||
{this.props.children}
|
||||
</div>
|
||||
|
||||
}
|
||||
|
@ -116,7 +124,18 @@ class SourceTypeEditor extends React.Component {
|
|||
case 'tilejson_raster': return <TileJSONSourceEditor {...commonProps} />
|
||||
case 'tilexyz_raster': return <TileURLSourceEditor {...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={styleSpec.latest.source_raster_dem.encoding.doc}>
|
||||
<SelectInput
|
||||
options={Object.keys(styleSpec.latest.source_raster_dem.encoding.values)}
|
||||
onChange={encoding => this.props.onChange({
|
||||
...this.props.source,
|
||||
encoding: encoding
|
||||
})}
|
||||
value={this.props.source.encoding || styleSpec.latest.source_raster_dem.encoding.default}
|
||||
/>
|
||||
</InputBlock>
|
||||
</TileURLSourceEditor>
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,8 +26,8 @@
|
|||
{
|
||||
"id": "osm-liberty",
|
||||
"title": "OSM Liberty",
|
||||
"url": "https://rawgit.com/lukasmartinelli/osm-liberty/gh-pages/style.json",
|
||||
"thumbnail": "https://cdn.rawgit.com/lukasmartinelli/osm-liberty/gh-pages/thumbnail.png"
|
||||
"url": "https://rawgit.com/maputnik/osm-liberty/gh-pages/style.json",
|
||||
"thumbnail": "https://cdn.rawgit.com/maputnik/osm-liberty/gh-pages/thumbnail.png"
|
||||
},
|
||||
{
|
||||
"id": "empty-style",
|
||||
|
|
12
src/libs/accessibility.js
Normal file
12
src/libs/accessibility.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import lodash from 'lodash'
|
||||
|
||||
|
||||
// Throttle for 3 seconds so when a user enables it they don't have to refresh the page.
|
||||
const reducedMotionEnabled = lodash.throttle(() => {
|
||||
return window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||
}, 3000);
|
||||
|
||||
|
||||
export default {
|
||||
reducedMotionEnabled
|
||||
}
|
44
src/libs/debug.js
Normal file
44
src/libs/debug.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
import querystring from 'querystring'
|
||||
|
||||
|
||||
const debugStore = {};
|
||||
|
||||
function enabled() {
|
||||
const qs = querystring.parse(window.location.search.slice(1));
|
||||
if(qs.hasOwnProperty("debug")) {
|
||||
return !!qs.debug.match(/^(|1|true)$/);
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function genErr() {
|
||||
return new Error("Debug not enabled, enable by appending '?debug' to your query string");
|
||||
}
|
||||
|
||||
function set(namespace, key, value) {
|
||||
if(!enabled()) {
|
||||
throw genErr();
|
||||
}
|
||||
debugStore[namespace] = debugStore[namespace] || {};
|
||||
debugStore[namespace][key] = value;
|
||||
}
|
||||
|
||||
function get(namespace, key) {
|
||||
if(!enabled()) {
|
||||
throw genErr();
|
||||
}
|
||||
if(debugStore.hasOwnProperty(namespace)) {
|
||||
return debugStore[namespace][key];
|
||||
}
|
||||
}
|
||||
|
||||
const mod = {
|
||||
enabled,
|
||||
get,
|
||||
set
|
||||
}
|
||||
|
||||
window.debug = mod;
|
||||
export default mod;
|
|
@ -1,4 +1,4 @@
|
|||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
|
||||
export function diffMessages(beforeStyle, afterStyle) {
|
||||
const changes = styleSpec.diff(beforeStyle, afterStyle)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
export const combiningFilterOps = ['all', 'any', 'none']
|
||||
export const setFilterOps = ['in', '!in']
|
||||
export const otherFilterOps = Object
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
import * as styleSpec from '@mapbox/mapbox-gl-style-spec/style-spec'
|
||||
|
||||
export function changeType(layer, newType) {
|
||||
const changedPaintProps = { ...layer.paint }
|
||||
|
|
17
src/libs/query-util.js
Normal file
17
src/libs/query-util.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
function asBool(queryObj, key) {
|
||||
if(queryObj.hasOwnProperty(key)) {
|
||||
if(queryObj[key].match(/^false|0$/)) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
asBool
|
||||
}
|
|
@ -18,6 +18,11 @@ html {
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
// The UI is 100% height so prevent bounce scroll on OSX
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
|
@ -76,3 +81,7 @@ label:hover {
|
|||
a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,11 @@
|
|||
right: 0;
|
||||
bottom: 0;
|
||||
height: calc(100% - #{$toolbar-height + $toolbar-offset});
|
||||
width: 75%;
|
||||
width: calc(
|
||||
100%
|
||||
- 200px /* layer list */
|
||||
- 350px /* layer editor */
|
||||
);
|
||||
}
|
||||
|
||||
// DOC LABEL
|
||||
|
@ -53,8 +57,10 @@
|
|||
font-size: $font-size-6;
|
||||
padding: $margin-2;
|
||||
user-select: none;
|
||||
border-width: 0;
|
||||
border-radius: 2px;
|
||||
box-sizing: border-box;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($color-midgray, 12);
|
||||
|
@ -69,6 +75,20 @@
|
|||
font-size: $font-size-5;
|
||||
}
|
||||
|
||||
.maputnik-wide-button {
|
||||
padding: $margin-2 $margin-3;
|
||||
}
|
||||
|
||||
.maputnik-green-button {
|
||||
background-color: $color-green;
|
||||
color: $color-black;
|
||||
}
|
||||
|
||||
.maputnik-white-button {
|
||||
background-color: $color-white;
|
||||
color: $color-black;
|
||||
}
|
||||
|
||||
.maputnik-icon-button {
|
||||
background-color: transparent;
|
||||
|
||||
|
|
|
@ -3,24 +3,34 @@
|
|||
}
|
||||
|
||||
.maputnik-filter-editor {
|
||||
@extend .clearfix;
|
||||
color: $color-lowgray;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-property {
|
||||
display: inline-block;
|
||||
width: '22%';
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-operator {
|
||||
display: inline-block;
|
||||
width: 19%;
|
||||
margin-left: 2%;
|
||||
display: inline-block;
|
||||
width: 17%;
|
||||
|
||||
.maputnik-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-args {
|
||||
display: inline-block;
|
||||
width: 54%;
|
||||
margin-left: 2%;
|
||||
|
||||
.maputnik-string,
|
||||
.maputnik-number {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-compound-select {
|
||||
|
@ -40,10 +50,6 @@
|
|||
color: $color-midgray;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor {
|
||||
@extend .clearfix;
|
||||
}
|
||||
|
||||
.maputnik-add-filter {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
|
@ -57,9 +63,6 @@
|
|||
.maputnik-filter-editor-block-action {
|
||||
margin-top: $margin-2;
|
||||
margin-bottom: $margin-2;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-block-action {
|
||||
display: inline-block;
|
||||
width: 6%;
|
||||
margin-right: 1.5%;
|
||||
|
@ -70,27 +73,3 @@
|
|||
width: 92.5%;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-property {
|
||||
display: inline-block;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-operator {
|
||||
display: inline-block;
|
||||
width: 17%;
|
||||
|
||||
.maputnik-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-filter-editor-args {
|
||||
display: inline-block;
|
||||
width: 54%;
|
||||
|
||||
.maputnik-string,
|
||||
.maputnik-number {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
|
||||
.maputnik-color-swatch {
|
||||
height: 26px;
|
||||
width: 3px;
|
||||
width: 14px;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
@ -91,7 +91,6 @@
|
|||
|
||||
.maputnik-button-selected {
|
||||
background-color: lighten($color-midgray, 12);
|
||||
outline: 1px $color-white;
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
@ -126,6 +125,10 @@
|
|||
border-width: 2px;
|
||||
border-color: $color-gray;
|
||||
transition: background-color 0.1s ease-out;
|
||||
|
||||
@media screen and (prefers-reduced-motion: reduce) {
|
||||
transition-duration: 0ms;
|
||||
}
|
||||
}
|
||||
|
||||
&-icon {
|
||||
|
|
|
@ -43,17 +43,32 @@
|
|||
-webkit-transition: opacity 600ms, visibility 600ms;
|
||||
transition: opacity 600ms, visibility 600ms;
|
||||
|
||||
@media screen and (prefers-reduced-motion: reduce) {
|
||||
transition-duration: 0;
|
||||
}
|
||||
|
||||
@include flex-row;
|
||||
}
|
||||
|
||||
&-icon-action svg {
|
||||
&-icon-action {
|
||||
display: none;
|
||||
|
||||
svg {
|
||||
fill: $color-black;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-layer-list-item:hover,
|
||||
.maputnik-layer-list-item-selected {
|
||||
background-color: lighten($color-black, 2);
|
||||
|
||||
.maputnik-layer-list-icon-action {
|
||||
display: block;
|
||||
background: initial;
|
||||
border: none;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.maputnik-layer-list-icon-action svg {
|
||||
fill: darken($color-lowgray, 0.5);
|
||||
|
||||
|
@ -122,6 +137,7 @@
|
|||
user-select: none;
|
||||
padding: $margin-2;
|
||||
line-height: 20px;
|
||||
border-top: solid 1px #36383e;
|
||||
|
||||
@include flex-row;
|
||||
|
||||
|
@ -164,3 +180,41 @@
|
|||
color: $color-lowgray;
|
||||
}
|
||||
}
|
||||
|
||||
.more-menu {
|
||||
position: relative;
|
||||
|
||||
&__menu {
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
background: $color-black;
|
||||
border: solid 1px $color-midgray;
|
||||
right: 0;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
&__button__svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
&__menu__item {
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.layer-header {
|
||||
display: flex;
|
||||
padding: 6px;
|
||||
background: $color-black;
|
||||
|
||||
&__title {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
&__info {
|
||||
min-width: 28px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//SCROLLING
|
||||
.maputnik-scroll-container {
|
||||
overflow-x: visible;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.3);
|
||||
z-index: 3;
|
||||
position: relative;
|
||||
font-family: $font-family;
|
||||
}
|
||||
|
||||
.maputnik-modal-section {
|
||||
|
@ -42,7 +43,10 @@
|
|||
}
|
||||
|
||||
.maputnik-modal-header-toggle {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: initial;
|
||||
color: white;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.maputnik-modal-scroller {
|
||||
|
@ -60,31 +64,6 @@
|
|||
@extend .maputnik-space;
|
||||
}
|
||||
|
||||
//OVERLAY
|
||||
.maputnik-overlay-viewport {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
opacity: 0.875;
|
||||
background-color: rgb(28, 31, 36);
|
||||
}
|
||||
|
||||
.maputnik-overlay {
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9;
|
||||
|
||||
@include flex-row;
|
||||
}
|
||||
|
||||
//OPEN MODAL
|
||||
.maputnik-upload-button {
|
||||
@extend .maputnik-big-button;
|
||||
|
@ -109,6 +88,7 @@
|
|||
background-color: $color-gray;
|
||||
padding: $margin-3;
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-midgray;
|
||||
|
@ -123,6 +103,9 @@
|
|||
display: block;
|
||||
margin-top: $margin-2;
|
||||
width: 100%;
|
||||
padding-top: calc(400 / 600 * 100%);
|
||||
background-size: cover;
|
||||
background-color: $color-midgray;
|
||||
}
|
||||
|
||||
.maputnik-add-layer {
|
||||
|
@ -160,6 +143,7 @@
|
|||
font-size: $font-size-5;
|
||||
color: $color-lowgray;
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
|
||||
@include flex-row;
|
||||
}
|
||||
|
@ -243,3 +227,38 @@
|
|||
text-decoration: none;
|
||||
color: #ef5350;
|
||||
}
|
||||
|
||||
.maputnik-modal-shortcuts {
|
||||
code {
|
||||
color: white;
|
||||
background: #3c3c3c;
|
||||
padding: 2px 6px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
margin-right: 4px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-modal-survey {
|
||||
width: 372px;
|
||||
}
|
||||
|
||||
.maputnik-modal-survey__logo {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.maputnik-modal-survey__description {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.maputnik-modal-survey__footnote {
|
||||
color: $color-green;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
|
3
src/styles/_react-codemirror.scss
Normal file
3
src/styles/_react-codemirror.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
.react-codemirror2 {
|
||||
max-width: 100%;
|
||||
}
|
9
src/styles/_react-collapse.scss
Normal file
9
src/styles/_react-collapse.scss
Normal file
|
@ -0,0 +1,9 @@
|
|||
// See <https://github.com/nkbt/react-collapse/commit/4f4fbce7c6c07b082dc62062338c9294c656f9df>
|
||||
.react-collapse-container {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
|
@ -9,15 +9,25 @@
|
|||
background-color: $color-black;
|
||||
}
|
||||
|
||||
.maputnik-toolbar-logo-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.maputnik-toolbar-logo {
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
flex: 0 0 180px;
|
||||
width: 180px;
|
||||
text-align: left;
|
||||
background-color: $color-black;
|
||||
padding: $margin-2;
|
||||
height: $toolbar-height;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
h1 {
|
||||
display: inline;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
img {
|
||||
|
@ -47,14 +57,38 @@
|
|||
}
|
||||
}
|
||||
|
||||
.maputnik-toolbar-link--highlighted {
|
||||
line-height: 1;
|
||||
padding: $margin-2 $margin-3;
|
||||
|
||||
.maputnik-toolbar-link-wrapper {
|
||||
background-color: $color-white;
|
||||
border-radius: 2px;
|
||||
padding: $margin-2;
|
||||
margin-top: $margin-1;
|
||||
color: $color-black;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $color-black;
|
||||
}
|
||||
|
||||
&:hover .maputnik-toolbar-link-wrapper {
|
||||
background-color: lighten($color-midgray, 12);
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.maputnik-toolbar-version {
|
||||
position: absolute;
|
||||
font-size: 10px;
|
||||
bottom: -2px;
|
||||
margin-left: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.maputnik-toolbar-action {
|
||||
background: inherit;
|
||||
border-width: 0;
|
||||
@extend .maputnik-toolbar-link;
|
||||
}
|
||||
|
||||
|
@ -67,10 +101,6 @@
|
|||
margin-left: $margin-1;
|
||||
}
|
||||
|
||||
.maputnik-toolbar-logo {
|
||||
flex: 0 0 170px;
|
||||
}
|
||||
|
||||
.maputnik-toolbar__inner {
|
||||
display: flex;
|
||||
}
|
||||
|
@ -80,3 +110,23 @@
|
|||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.maputnik-toolbar-skip {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
width: 0px;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
display: block;
|
||||
background-color: $color-black;
|
||||
z-index: 999;
|
||||
line-height: 40px;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -64,10 +64,6 @@
|
|||
margin-right: $margin-3;
|
||||
}
|
||||
|
||||
.maputnik-zoom-spec-property .maputnik-input-block:not(:first-child) .maputnik-input-block-label {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
// DATA FUNC
|
||||
.maputnik-make-data-function {
|
||||
background-color: transparent;
|
||||
|
@ -79,16 +75,15 @@
|
|||
@extend .maputnik-icon-button;
|
||||
}
|
||||
|
||||
// DATA PROPERTY
|
||||
.maputnik-data-spec-block {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.maputnik-data-spec-property {
|
||||
.maputnik-input-block-label {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.maputnik-input-block:not(:first-child) .maputnik-input-block-label {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.maputnik-input-block-content {
|
||||
width: 70%;
|
||||
}
|
||||
|
@ -117,6 +112,8 @@
|
|||
}
|
||||
|
||||
.maputnik-data-spec-block {
|
||||
overflow: auto;
|
||||
|
||||
.maputnik-data-spec-property-stop-edit,
|
||||
.maputnik-data-spec-property-stop-data {
|
||||
display: inline-block;
|
||||
|
@ -129,6 +126,10 @@
|
|||
}
|
||||
|
||||
.maputnik-data-spec-property-stop-data {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.maputnik-data-spec-property-stop-edit + .maputnik-data-spec-property-stop-data {
|
||||
width: 78%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ $color-midgray: #36383e;
|
|||
$color-lowgray: #8e8e8e;
|
||||
$color-white: #f0f0f0;
|
||||
$color-red: #cf4a4a;
|
||||
$color-green: #53b972;
|
||||
$margin-1: 3px;
|
||||
$margin-2: 5px;
|
||||
$margin-3: 10px;
|
||||
|
@ -36,3 +37,16 @@ $toolbar-offset: 0;
|
|||
@import 'zoomproperty';
|
||||
@import 'popup';
|
||||
@import 'map';
|
||||
@import 'react-collapse';
|
||||
@import 'react-codemirror';
|
||||
|
||||
/**
|
||||
* Hacks for webdriverio isVisibleWithinViewport
|
||||
*/
|
||||
#app {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.maputnik-layout {
|
||||
height: 100vh;
|
||||
}
|
||||
|
|
|
@ -69,6 +69,87 @@
|
|||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- TODO: Import dynamically -->
|
||||
<!-- From <https://github.com/hail2u/color-blindness-emulation> -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1">
|
||||
<defs>
|
||||
<filter id="protanopia">
|
||||
<feColorMatrix
|
||||
in="SourceGraphic"
|
||||
type="matrix"
|
||||
values="0.567, 0.433, 0, 0, 0
|
||||
0.558, 0.442, 0, 0, 0
|
||||
0, 0.242, 0.758, 0, 0
|
||||
0, 0, 0, 1, 0"/>
|
||||
</filter>
|
||||
<filter id="protanomaly">
|
||||
<feColorMatrix
|
||||
in="SourceGraphic"
|
||||
type="matrix"
|
||||
values="0.817, 0.183, 0, 0, 0
|
||||
0.333, 0.667, 0, 0, 0
|
||||
0, 0.125, 0.875, 0, 0
|
||||
0, 0, 0, 1, 0"/>
|
||||
</filter>
|
||||
<filter id="deuteranopia">
|
||||
<feColorMatrix
|
||||
in="SourceGraphic"
|
||||
type="matrix"
|
||||
values="0.625, 0.375, 0, 0, 0
|
||||
0.7, 0.3, 0, 0, 0
|
||||
0, 0.3, 0.7, 0, 0
|
||||
0, 0, 0, 1, 0"/>
|
||||
</filter>
|
||||
<filter id="deuteranomaly">
|
||||
<feColorMatrix
|
||||
in="SourceGraphic"
|
||||
type="matrix"
|
||||
values="0.8, 0.2, 0, 0, 0
|
||||
0.258, 0.742, 0, 0, 0
|
||||
0, 0.142, 0.858, 0, 0
|
||||
0, 0, 0, 1, 0"/>
|
||||
</filter>
|
||||
<filter id="tritanopia">
|
||||
<feColorMatrix
|
||||
in="SourceGraphic"
|
||||
type="matrix"
|
||||
values="0.95, 0.05, 0, 0, 0
|
||||
0, 0.433, 0.567, 0, 0
|
||||
0, 0.475, 0.525, 0, 0
|
||||
0, 0, 0, 1, 0"/>
|
||||
</filter>
|
||||
<filter id="tritanomaly">
|
||||
<feColorMatrix
|
||||
in="SourceGraphic"
|
||||
type="matrix"
|
||||
values="0.967, 0.033, 0, 0, 0
|
||||
0, 0.733, 0.267, 0, 0
|
||||
0, 0.183, 0.817, 0, 0
|
||||
0, 0, 0, 1, 0"/>
|
||||
</filter>
|
||||
<filter id="achromatopsia">
|
||||
<feColorMatrix
|
||||
in="SourceGraphic"
|
||||
type="matrix"
|
||||
values="0.299, 0.587, 0.114, 0, 0
|
||||
0.299, 0.587, 0.114, 0, 0
|
||||
0.299, 0.587, 0.114, 0, 0
|
||||
0, 0, 0, 1, 0"/>
|
||||
</filter>
|
||||
<filter id="achromatomaly">
|
||||
<feColorMatrix
|
||||
in="SourceGraphic"
|
||||
type="matrix"
|
||||
values="0.618, 0.320, 0.062, 0, 0
|
||||
0.163, 0.775, 0.062, 0, 0
|
||||
0.163, 0.320, 0.516, 0, 0
|
||||
0, 0, 0, 1, 0"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div id="app">
|
||||
<div id="loader">Loading...</div>
|
||||
</div>
|
||||
|
|
39
test/artifacts.js
Normal file
39
test/artifacts.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
var path = require("path");
|
||||
var mkdirp = require("mkdirp");
|
||||
|
||||
|
||||
function genPath(subPath) {
|
||||
subPath = subPath || ".";
|
||||
var buildPath;
|
||||
|
||||
if(process.env.CIRCLECI) {
|
||||
buildPath = path.join("/tmp/artifacts", subPath);
|
||||
}
|
||||
else {
|
||||
buildPath = path.join(__dirname, '..', 'build', subPath);
|
||||
}
|
||||
|
||||
return buildPath;
|
||||
}
|
||||
|
||||
module.exports.path = function(subPath) {
|
||||
var dirPath = genPath(subPath);
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
mkdirp(dirPath, function(err) {
|
||||
if(err) {
|
||||
reject(err);
|
||||
}
|
||||
else {
|
||||
resolve(dirPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.pathSync = function(subPath) {
|
||||
var dirPath = genPath(subPath);
|
||||
mkdirp.sync(dirPath);
|
||||
return dirPath;
|
||||
}
|
||||
|
12
test/example-style.json
Normal file
12
test/example-style.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"id": "test-style",
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"metadata": {
|
||||
"maputnik:renderer": "mbgljs"
|
||||
},
|
||||
"sources": {},
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": []
|
||||
}
|
77
test/functional/helper.js
Normal file
77
test/functional/helper.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
var wd = require("../wd-helper");
|
||||
var uuid = require('uuid/v1');
|
||||
var geoServer = require("../geojson-server");
|
||||
|
||||
|
||||
var geoserver = geoServer.listen(9002);
|
||||
|
||||
module.exports = {
|
||||
getStyleUrl: function(styles) {
|
||||
var port = geoserver.address().port;
|
||||
return "http://localhost:"+port+"/styles/empty/"+styles.join(",");
|
||||
},
|
||||
getGeoServerUrl: function(urlPath) {
|
||||
var port = geoserver.address().port;
|
||||
return "http://localhost:"+port+"/"+urlPath;
|
||||
},
|
||||
getStyleStore: function(browser) {
|
||||
var result = browser.executeAsync(function(done) {
|
||||
window.debug.get("maputnik", "styleStore").latestStyle(done);
|
||||
})
|
||||
return result.value;
|
||||
},
|
||||
getRevisionStore: function(browser) {
|
||||
var result = browser.execute(function(done) {
|
||||
var rs = window.debug.get("maputnik", "revisionStore")
|
||||
|
||||
return {
|
||||
currentIdx: rs.currentIdx,
|
||||
revisions: rs.revisions
|
||||
};
|
||||
})
|
||||
return result.value;
|
||||
},
|
||||
modal: {
|
||||
addLayer: {
|
||||
open: function() {
|
||||
var selector = wd.$('layer-list:add-layer');
|
||||
browser.click(selector);
|
||||
|
||||
// Wait for events
|
||||
browser.flushReactUpdates();
|
||||
|
||||
browser.waitForExist(wd.$('modal:add-layer'));
|
||||
browser.isVisible(wd.$('modal:add-layer'));
|
||||
browser.isVisibleWithinViewport(wd.$('modal:add-layer'));
|
||||
|
||||
// Wait for events
|
||||
browser.flushReactUpdates();
|
||||
},
|
||||
fill: function(opts) {
|
||||
var type = opts.type;
|
||||
var layer = opts.layer;
|
||||
var id;
|
||||
if(opts.id) {
|
||||
id = opts.id
|
||||
}
|
||||
else {
|
||||
id = type+":"+uuid();
|
||||
}
|
||||
|
||||
browser.selectByValue(wd.$("add-layer.layer-type", "select"), type);
|
||||
browser.flushReactUpdates();
|
||||
|
||||
browser.setValueSafe(wd.$("add-layer.layer-id", "input"), id);
|
||||
if(layer) {
|
||||
browser.setValueSafe(wd.$("add-layer.layer-source-block", "input"), layer);
|
||||
}
|
||||
|
||||
browser.flushReactUpdates();
|
||||
browser.click(wd.$("add-layer"));
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
100
test/functional/history/index.js
Normal file
100
test/functional/history/index.js
Normal file
|
@ -0,0 +1,100 @@
|
|||
var assert = require("assert");
|
||||
var config = require("../../config/specs");
|
||||
var helper = require("../helper");
|
||||
var wd = require("../../wd-helper");
|
||||
|
||||
|
||||
describe.skip("history", function() {
|
||||
/**
|
||||
* See <https://github.com/webdriverio/webdriverio/issues/1126>
|
||||
*/
|
||||
it("undo/redo", function() {
|
||||
var styleObj;
|
||||
|
||||
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
|
||||
"geojson:example"
|
||||
]));
|
||||
|
||||
helper.modal.addLayer.open();
|
||||
|
||||
styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, []);
|
||||
|
||||
helper.modal.addLayer.fill({
|
||||
id: "step 1",
|
||||
type: "background"
|
||||
})
|
||||
|
||||
styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": "step 1",
|
||||
"type": 'background'
|
||||
}
|
||||
]);
|
||||
|
||||
helper.modal.addLayer.open();
|
||||
helper.modal.addLayer.fill({
|
||||
id: "step 2",
|
||||
type: "background"
|
||||
})
|
||||
|
||||
styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": "step 1",
|
||||
"type": 'background'
|
||||
},
|
||||
{
|
||||
"id": "step 2",
|
||||
"type": 'background'
|
||||
}
|
||||
]);
|
||||
|
||||
browser
|
||||
.keys(['Control', 'z'])
|
||||
.keys(['Control']);
|
||||
styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": "step 1",
|
||||
"type": 'background'
|
||||
}
|
||||
]);
|
||||
|
||||
browser
|
||||
.keys(['Control', 'z'])
|
||||
.keys(['Control']);
|
||||
styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
]);
|
||||
|
||||
browser
|
||||
.keys(['Control', 'y'])
|
||||
.keys(['Control']);
|
||||
styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": "step 1",
|
||||
"type": 'background'
|
||||
}
|
||||
]);
|
||||
|
||||
browser
|
||||
.keys(['Control', 'y'])
|
||||
.keys(['Control']);
|
||||
styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": "step 1",
|
||||
"type": 'background'
|
||||
},
|
||||
{
|
||||
"id": "step 2",
|
||||
"type": 'background'
|
||||
}
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
})
|
33
test/functional/index.js
Normal file
33
test/functional/index.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
var assert = require('assert');
|
||||
var config = require("../config/specs");
|
||||
var geoServer = require("../geojson-server");
|
||||
var helper = require("./helper");
|
||||
|
||||
require("./util/webdriverio-ext");
|
||||
|
||||
|
||||
describe('maputnik', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
|
||||
"geojson:example",
|
||||
"raster:raster"
|
||||
]));
|
||||
browser.waitForExist(".maputnik-toolbar-link");
|
||||
browser.flushReactUpdates();
|
||||
});
|
||||
|
||||
// -------- setup --------
|
||||
require("./util/coverage");
|
||||
// -----------------------
|
||||
|
||||
// ---- All the tests ----
|
||||
require("./history");
|
||||
require("./layers");
|
||||
require("./map");
|
||||
require("./modals");
|
||||
require("./screenshots");
|
||||
// ------------------------
|
||||
|
||||
});
|
||||
|
485
test/functional/layers/index.js
Normal file
485
test/functional/layers/index.js
Normal file
|
@ -0,0 +1,485 @@
|
|||
var assert = require("assert");
|
||||
var config = require("../../config/specs");
|
||||
var helper = require("../helper");
|
||||
var uuid = require('uuid/v1');
|
||||
var wd = require("../../wd-helper");
|
||||
|
||||
|
||||
describe("layers", function() {
|
||||
beforeEach(function() {
|
||||
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
|
||||
"geojson:example",
|
||||
"raster:raster"
|
||||
]));
|
||||
browser.waitForExist(".maputnik-toolbar-link");
|
||||
browser.flushReactUpdates();
|
||||
|
||||
helper.modal.addLayer.open();
|
||||
});
|
||||
|
||||
describe("ops", function() {
|
||||
it("delete", function() {
|
||||
var styleObj;
|
||||
var id = helper.modal.addLayer.fill({
|
||||
type: "background"
|
||||
})
|
||||
|
||||
styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": id,
|
||||
"type": 'background'
|
||||
},
|
||||
]);
|
||||
|
||||
browser.click(wd.$("layer-list-item:"+id+":delete", ""));
|
||||
|
||||
styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
]);
|
||||
});
|
||||
|
||||
it("duplicate", function() {
|
||||
var styleObj;
|
||||
var id = helper.modal.addLayer.fill({
|
||||
type: "background"
|
||||
})
|
||||
|
||||
styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": id,
|
||||
"type": 'background'
|
||||
},
|
||||
]);
|
||||
|
||||
browser.click(wd.$("layer-list-item:"+id+":copy", ""));
|
||||
|
||||
styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": id+"-copy",
|
||||
"type": "background"
|
||||
},
|
||||
{
|
||||
"id": id,
|
||||
"type": "background"
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("hide", function() {
|
||||
var styleObj;
|
||||
var id = helper.modal.addLayer.fill({
|
||||
type: "background"
|
||||
})
|
||||
|
||||
styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": id,
|
||||
"type": 'background'
|
||||
},
|
||||
]);
|
||||
|
||||
browser.click(wd.$("layer-list-item:"+id+":toggle-visibility", ""));
|
||||
|
||||
styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": id,
|
||||
"type": "background",
|
||||
"layout": {
|
||||
"visibility": "none"
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
browser.click(wd.$("layer-list-item:"+id+":toggle-visibility", ""));
|
||||
|
||||
styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": id,
|
||||
"type": "background",
|
||||
"layout": {
|
||||
"visibility": "visible"
|
||||
}
|
||||
},
|
||||
]);
|
||||
})
|
||||
})
|
||||
|
||||
describe("grouped", function() {
|
||||
it("with underscore")
|
||||
it("no without underscore")
|
||||
it("double underscore only grouped once")
|
||||
})
|
||||
|
||||
describe("tooltips", function() {
|
||||
})
|
||||
|
||||
describe("help", function() {
|
||||
})
|
||||
|
||||
|
||||
describe('background', function () {
|
||||
|
||||
it.skip("add", function() {
|
||||
var id = helper.modal.addLayer.fill({
|
||||
type: "background"
|
||||
})
|
||||
|
||||
browser.waitUntil(function() {
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": id,
|
||||
"type": 'background'
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("modify", function() {
|
||||
function createBackground() {
|
||||
// Setup
|
||||
var id = uuid();
|
||||
|
||||
browser.selectByValue(wd.$("add-layer.layer-type", "select"), "background");
|
||||
browser.flushReactUpdates();
|
||||
browser.setValueSafe(wd.$("add-layer.layer-id", "input"), "background:"+id);
|
||||
|
||||
browser.click(wd.$("add-layer"));
|
||||
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": 'background:'+id,
|
||||
"type": 'background'
|
||||
}
|
||||
]);
|
||||
return id;
|
||||
}
|
||||
|
||||
// ====> THESE SHOULD BE FROM THE SPEC
|
||||
describe("layer", function() {
|
||||
it("expand/collapse");
|
||||
it("id", function() {
|
||||
var bgId = createBackground();
|
||||
|
||||
browser.click(wd.$("layer-list-item:background:"+bgId))
|
||||
|
||||
var id = uuid();
|
||||
browser.setValueSafe(wd.$("layer-editor.layer-id", "input"), "foobar:"+id)
|
||||
browser.click(wd.$("min-zoom"))
|
||||
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": 'foobar:'+id,
|
||||
"type": 'background'
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
// NOTE: This needs to be removed from the code
|
||||
it("type");
|
||||
|
||||
it("min-zoom", function() {
|
||||
var bgId = createBackground();
|
||||
|
||||
browser.click(wd.$("layer-list-item:background:"+bgId))
|
||||
browser.setValueSafe(wd.$("min-zoom", "input"), 1)
|
||||
browser.click(wd.$("layer-editor.layer-id", "input"));
|
||||
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": 'background:'+bgId,
|
||||
"type": 'background',
|
||||
"minzoom": 1
|
||||
}
|
||||
]);
|
||||
|
||||
// AND RESET!
|
||||
// browser.setValueSafe(wd.$("min-zoom", "input"), "")
|
||||
// browser.click(wd.$("max-zoom", "input"));
|
||||
|
||||
// var styleObj = helper.getStyleStore(browser);
|
||||
|
||||
// assert.deepEqual(styleObj.layers, [
|
||||
// {
|
||||
// "id": 'background:'+bgId,
|
||||
// "type": 'background'
|
||||
// }
|
||||
// ]);
|
||||
});
|
||||
|
||||
it("max-zoom", function() {
|
||||
var bgId = createBackground();
|
||||
|
||||
browser.click(wd.$("layer-list-item:background:"+bgId))
|
||||
browser.setValueSafe(wd.$("max-zoom", "input"), 1)
|
||||
browser.click(wd.$("layer-editor.layer-id", "input"));
|
||||
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": 'background:'+bgId,
|
||||
"type": 'background',
|
||||
"maxzoom": 1
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("comments", function() {
|
||||
var bgId = createBackground();
|
||||
var id = uuid();
|
||||
|
||||
browser.click(wd.$("layer-list-item:background:"+bgId));
|
||||
browser.setValueSafe(wd.$("layer-comment", "textarea"), id);
|
||||
browser.click(wd.$("layer-editor.layer-id", "input"));
|
||||
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": 'background:'+bgId,
|
||||
"type": 'background',
|
||||
metadata: {
|
||||
'maputnik:comment': id
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
// Unset it again.
|
||||
// TODO: This fails
|
||||
// browser.setValueSafe(wd.$("layer-comment", "textarea"), "");
|
||||
// browser.click(wd.$("min-zoom", "input"));
|
||||
// browser.flushReactUpdates();
|
||||
|
||||
// var styleObj = helper.getStyleStore(browser);
|
||||
// assert.deepEqual(styleObj.layers, [
|
||||
// {
|
||||
// "id": 'background:'+bgId,
|
||||
// "type": 'background'
|
||||
// }
|
||||
// ]);
|
||||
});
|
||||
|
||||
it("color", null, function() {
|
||||
var bgId = createBackground();
|
||||
var id = uuid();
|
||||
|
||||
browser.click(wd.$("layer-list-item:background:"+bgId));
|
||||
|
||||
browser.click(wd.$("spec-field:background-color", "input"))
|
||||
// browser.debug();
|
||||
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": 'background:'+bgId,
|
||||
"type": 'background'
|
||||
}
|
||||
]);
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
describe("filter", function() {
|
||||
it("expand/collapse");
|
||||
it("compound filter");
|
||||
})
|
||||
|
||||
describe("paint", function() {
|
||||
it("expand/collapse");
|
||||
it("color");
|
||||
it("pattern");
|
||||
it("opacity");
|
||||
})
|
||||
// <=====
|
||||
|
||||
describe("json-editor", function() {
|
||||
it("expand/collapse");
|
||||
it("modify");
|
||||
|
||||
// TODO
|
||||
it.skip("parse error", function() {
|
||||
var bgId = createBackground();
|
||||
var id = uuid();
|
||||
|
||||
browser.click(wd.$("layer-list-item:background:"+bgId));
|
||||
|
||||
var errorSelector = ".CodeMirror-lint-marker-error";
|
||||
assert.equal(browser.isExisting(errorSelector), false);
|
||||
|
||||
browser.click(".CodeMirror")
|
||||
browser.keys("\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013\uE013 {");
|
||||
browser.waitForExist(errorSelector)
|
||||
|
||||
browser.click(wd.$("layer-editor.layer-id"));
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe('fill', function () {
|
||||
it.skip("add", function() {
|
||||
// browser.debug();
|
||||
|
||||
var id = helper.modal.addLayer.fill({
|
||||
type: "fill",
|
||||
layer: "example"
|
||||
});
|
||||
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": id,
|
||||
"type": 'fill',
|
||||
"source": "example"
|
||||
}
|
||||
]);
|
||||
})
|
||||
|
||||
// TODO: Change source
|
||||
it("change source")
|
||||
});
|
||||
|
||||
describe('line', function () {
|
||||
it.skip("add", function() {
|
||||
var id = helper.modal.addLayer.fill({
|
||||
type: "line",
|
||||
layer: "example"
|
||||
});
|
||||
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": id,
|
||||
"type": "line",
|
||||
"source": "example",
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("groups", null, function() {
|
||||
// TODO
|
||||
// Click each of the layer groups.
|
||||
})
|
||||
});
|
||||
|
||||
describe('symbol', function () {
|
||||
it.skip("add", function() {
|
||||
var id = helper.modal.addLayer.fill({
|
||||
type: "symbol",
|
||||
layer: "example"
|
||||
});
|
||||
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": id,
|
||||
"type": "symbol",
|
||||
"source": "example",
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('raster', function () {
|
||||
it.skip("add", function() {
|
||||
var id = helper.modal.addLayer.fill({
|
||||
type: "raster",
|
||||
layer: "raster"
|
||||
});
|
||||
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": id,
|
||||
"type": "raster",
|
||||
"source": "raster",
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('circle', function () {
|
||||
it.skip("add", function() {
|
||||
var id = helper.modal.addLayer.fill({
|
||||
type: "circle",
|
||||
layer: "example"
|
||||
});
|
||||
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": id,
|
||||
"type": "circle",
|
||||
"source": "example",
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('fill extrusion', function () {
|
||||
it.skip("add", function() {
|
||||
var id = helper.modal.addLayer.fill({
|
||||
type: "fill-extrusion",
|
||||
layer: "example"
|
||||
});
|
||||
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleObj.layers, [
|
||||
{
|
||||
"id": id,
|
||||
"type": 'fill-extrusion',
|
||||
"source": "example"
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe.skip("groups", function() {
|
||||
it("simple", function() {
|
||||
browser.url(config.baseUrl+"?debug&style="+getStyleUrl([
|
||||
"geojson:example"
|
||||
]));
|
||||
|
||||
helper.modal.addLayer.open();
|
||||
var aId = helper.modal.addLayer.fill({
|
||||
id: "foo",
|
||||
type: "background"
|
||||
})
|
||||
|
||||
helper.modal.addLayer.open();
|
||||
var bId = helper.modal.addLayer.fill({
|
||||
id: "foo_bar",
|
||||
type: "background"
|
||||
})
|
||||
|
||||
helper.modal.addLayer.open();
|
||||
var bId = helper.modal.addLayer.fill({
|
||||
id: "foo_baz",
|
||||
type: "background"
|
||||
})
|
||||
|
||||
browser.waitForExist(wd.$("layer-list-group:foo-0"));
|
||||
|
||||
assert.equal(browser.isVisibleWithinViewport(wd.$("layer-list-item:foo")), false);
|
||||
assert.equal(browser.isVisibleWithinViewport(wd.$("layer-list-item:foo_bar")), false);
|
||||
assert.equal(browser.isVisibleWithinViewport(wd.$("layer-list-item:foo_baz")), false);
|
||||
|
||||
browser.click(wd.$("layer-list-group:foo-0"));
|
||||
|
||||
assert.equal(browser.isVisibleWithinViewport(wd.$("layer-list-item:foo")), true);
|
||||
assert.equal(browser.isVisibleWithinViewport(wd.$("layer-list-item:foo_bar")), true);
|
||||
assert.equal(browser.isVisibleWithinViewport(wd.$("layer-list-item:foo_baz")), true);
|
||||
})
|
||||
})
|
||||
});
|
||||
|
35
test/functional/map/index.js
Normal file
35
test/functional/map/index.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
var assert = require('assert');
|
||||
var wd = require("../../wd-helper");
|
||||
var config = require("../../config/specs");
|
||||
var helper = require("../helper");
|
||||
|
||||
|
||||
describe("map", function() {
|
||||
describe.skip("zoom level", function() {
|
||||
it("via url", function() {
|
||||
var zoomLevel = "12.37"
|
||||
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
|
||||
"geojson:example"
|
||||
])+"#"+zoomLevel+"/41.3805/2.1635");
|
||||
|
||||
browser.waitUntil(function () {
|
||||
return (
|
||||
browser.isVisible(".mapboxgl-ctrl-zoom")
|
||||
&& browser.getText(".mapboxgl-ctrl-zoom") === "Zoom level: "+(zoomLevel)
|
||||
);
|
||||
}, 10*1000)
|
||||
})
|
||||
it("via map controls", function() {
|
||||
var zoomLevel = 12.37;
|
||||
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
|
||||
"geojson:example"
|
||||
])+"#"+zoomLevel+"/41.3805/2.1635");
|
||||
|
||||
browser.click(".mapboxgl-ctrl-zoom-in")
|
||||
browser.waitUntil(function () {
|
||||
var text = browser.getText(".mapboxgl-ctrl-zoom")
|
||||
return text === "Zoom level: "+(zoomLevel+1);
|
||||
}, 10*1000)
|
||||
})
|
||||
})
|
||||
})
|
188
test/functional/modals/index.js
Normal file
188
test/functional/modals/index.js
Normal file
|
@ -0,0 +1,188 @@
|
|||
var assert = require('assert');
|
||||
var fs = require("fs");
|
||||
var wd = require("../../wd-helper");
|
||||
var config = require("../../config/specs");
|
||||
var helper = require("../helper");
|
||||
|
||||
|
||||
function closeModal(wdKey) {
|
||||
browser.waitUntil(function() {
|
||||
return browser.isVisibleWithinViewport(wd.$(wdKey));
|
||||
});
|
||||
|
||||
var closeBtnSelector = wd.$(wdKey+".close-modal");
|
||||
browser.click(closeBtnSelector);
|
||||
|
||||
browser.waitUntil(function() {
|
||||
return !browser.isVisibleWithinViewport(wd.$(wdKey));
|
||||
});
|
||||
}
|
||||
|
||||
describe("modals", function() {
|
||||
describe("open", function() {
|
||||
var styleFilePath = __dirname+"/../../example-style.json";
|
||||
var styleFileData = JSON.parse(fs.readFileSync(styleFilePath));
|
||||
|
||||
beforeEach(function() {
|
||||
browser.url(config.baseUrl+"?debug");
|
||||
|
||||
browser.waitForExist(".maputnik-toolbar-link");
|
||||
browser.flushReactUpdates();
|
||||
|
||||
browser.click(wd.$("nav:open"))
|
||||
browser.flushReactUpdates();
|
||||
});
|
||||
|
||||
it("close", function() {
|
||||
closeModal("open-modal");
|
||||
});
|
||||
|
||||
it("upload", function() {
|
||||
browser.waitForExist("*[type='file']")
|
||||
browser.chooseFile("*[type='file']", styleFilePath);
|
||||
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleFileData, styleObj);
|
||||
});
|
||||
|
||||
it("load from url", function() {
|
||||
var styleFileUrl = helper.getGeoServerUrl("example-style.json");
|
||||
|
||||
browser.setValueSafe(wd.$("open-modal.url.input"), styleFileUrl);
|
||||
|
||||
var selector = wd.$("open-modal.url.button");
|
||||
browser.click(selector);
|
||||
|
||||
// Allow the network request to happen
|
||||
// NOTE: Its localhost so this should be fast.
|
||||
browser.pause(300);
|
||||
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
assert.deepEqual(styleFileData, styleObj);
|
||||
});
|
||||
|
||||
// TODO: Need to work out how to mock out the end points
|
||||
it("gallery")
|
||||
})
|
||||
|
||||
describe("export", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.url(config.baseUrl+"?debug");
|
||||
|
||||
browser.waitForExist(".maputnik-toolbar-link");
|
||||
browser.flushReactUpdates();
|
||||
|
||||
browser.click(wd.$("nav:export"))
|
||||
browser.flushReactUpdates();
|
||||
});
|
||||
|
||||
it("close", function() {
|
||||
closeModal("export-modal");
|
||||
});
|
||||
|
||||
// TODO: Work out how to download a file and check the contents
|
||||
it("download")
|
||||
|
||||
// TODO: Work out how to mock the end git points
|
||||
it("save to gist")
|
||||
})
|
||||
|
||||
describe("sources", function() {
|
||||
it("active sources")
|
||||
it("public source")
|
||||
it("add new source")
|
||||
})
|
||||
|
||||
describe("inspect", function() {
|
||||
it("toggle", function() {
|
||||
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
|
||||
"geojson:example"
|
||||
]));
|
||||
|
||||
browser.click(wd.$("nav:inspect"));
|
||||
})
|
||||
})
|
||||
|
||||
describe("style settings", function() {
|
||||
beforeEach(function() {
|
||||
browser.url(config.baseUrl+"?debug");
|
||||
|
||||
browser.waitForExist(".maputnik-toolbar-link");
|
||||
browser.flushReactUpdates();
|
||||
|
||||
browser.click(wd.$("nav:settings"))
|
||||
browser.flushReactUpdates();
|
||||
});
|
||||
|
||||
it("name", function() {
|
||||
browser.setValueSafe(wd.$("modal-settings.name"), "foobar")
|
||||
browser.click(wd.$("modal-settings.owner"))
|
||||
browser.flushReactUpdates();
|
||||
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
assert.equal(styleObj.name, "foobar");
|
||||
})
|
||||
it("owner", function() {
|
||||
browser.setValueSafe(wd.$("modal-settings.owner"), "foobar")
|
||||
browser.click(wd.$("modal-settings.name"))
|
||||
browser.flushReactUpdates();
|
||||
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
assert.equal(styleObj.owner, "foobar");
|
||||
})
|
||||
it("sprite url", function() {
|
||||
browser.setValueSafe(wd.$("modal-settings.sprite"), "http://example.com")
|
||||
browser.click(wd.$("modal-settings.name"))
|
||||
browser.flushReactUpdates();
|
||||
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
assert.equal(styleObj.sprite, "http://example.com");
|
||||
})
|
||||
it("glyphs url", function() {
|
||||
var glyphsUrl = "http://example.com/{fontstack}/{range}.pbf"
|
||||
browser.setValueSafe(wd.$("modal-settings.glyphs"), glyphsUrl)
|
||||
browser.click(wd.$("modal-settings.name"))
|
||||
browser.flushReactUpdates();
|
||||
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
assert.equal(styleObj.glyphs, glyphsUrl);
|
||||
})
|
||||
|
||||
it("mapbox access token", function() {
|
||||
var apiKey = "testing123";
|
||||
browser.setValueSafe(wd.$("modal-settings.maputnik:mapbox_access_token"), apiKey);
|
||||
browser.click(wd.$("modal-settings.name"))
|
||||
browser.flushReactUpdates();
|
||||
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
browser.waitUntil(function() {
|
||||
return styleObj.metadata["maputnik:mapbox_access_token"] == apiKey;
|
||||
})
|
||||
})
|
||||
|
||||
it("open map tiles access token", function() {
|
||||
var apiKey = "testing123";
|
||||
browser.setValueSafe(wd.$("modal-settings.maputnik:openmaptiles_access_token"), apiKey);
|
||||
browser.click(wd.$("modal-settings.name"))
|
||||
browser.flushReactUpdates();
|
||||
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
assert.equal(styleObj.metadata["maputnik:openmaptiles_access_token"], apiKey);
|
||||
})
|
||||
|
||||
it("style renderer", function() {
|
||||
var selector = wd.$("modal-settings.maputnik:renderer");
|
||||
browser.selectByValue(selector, "ol3");
|
||||
browser.click(wd.$("modal-settings.name"))
|
||||
browser.flushReactUpdates();
|
||||
|
||||
var styleObj = helper.getStyleStore(browser);
|
||||
assert.equal(styleObj.metadata["maputnik:renderer"], "ol3");
|
||||
})
|
||||
})
|
||||
|
||||
describe("sources", function() {
|
||||
it("toggle")
|
||||
})
|
||||
})
|
93
test/functional/screenshots/index.js
Normal file
93
test/functional/screenshots/index.js
Normal file
|
@ -0,0 +1,93 @@
|
|||
var artifacts = require("../../artifacts");
|
||||
var config = require("../../config/specs");
|
||||
var helper = require("../helper");
|
||||
var wd = require("../../wd-helper");
|
||||
|
||||
|
||||
// These will get used in the marketing material. They are also useful to do a quick manual check of the styling across browsers
|
||||
// NOTE: These duplicate some of the tests, however this is indended becuase it's likely these will change for aesthetic reasons over time
|
||||
describe('screenshots', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
browser.windowHandleSize({
|
||||
width: 1280,
|
||||
height: 800
|
||||
});
|
||||
})
|
||||
|
||||
it("front_page", function() {
|
||||
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
|
||||
"geojson:example"
|
||||
]));
|
||||
browser.waitForExist(".maputnik-toolbar-link");
|
||||
browser.flushReactUpdates();
|
||||
|
||||
browser.takeScreenShot("/front_page.png")
|
||||
})
|
||||
|
||||
it("open", function() {
|
||||
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
|
||||
"geojson:example"
|
||||
]));
|
||||
browser.waitForExist(".maputnik-toolbar-link");
|
||||
browser.flushReactUpdates();
|
||||
|
||||
browser.click(wd.$("nav:open"))
|
||||
browser.flushReactUpdates();
|
||||
|
||||
browser.takeScreenShot("/open.png")
|
||||
})
|
||||
|
||||
it("export", function() {
|
||||
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
|
||||
"geojson:example"
|
||||
]));
|
||||
browser.waitForExist(".maputnik-toolbar-link");
|
||||
browser.flushReactUpdates();
|
||||
|
||||
browser.click(wd.$("nav:export"))
|
||||
browser.flushReactUpdates();
|
||||
|
||||
browser.takeScreenShot("/export.png")
|
||||
})
|
||||
|
||||
it("sources", function() {
|
||||
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
|
||||
"geojson:example"
|
||||
]));
|
||||
browser.waitForExist(".maputnik-toolbar-link");
|
||||
browser.flushReactUpdates();
|
||||
|
||||
browser.click(wd.$("nav:sources"))
|
||||
browser.flushReactUpdates();
|
||||
|
||||
browser.takeScreenShot("/sources.png")
|
||||
})
|
||||
|
||||
it("style settings", function() {
|
||||
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
|
||||
"geojson:example"
|
||||
]));
|
||||
browser.waitForExist(".maputnik-toolbar-link");
|
||||
browser.flushReactUpdates();
|
||||
|
||||
browser.click(wd.$("nav:settings"))
|
||||
browser.flushReactUpdates();
|
||||
|
||||
browser.takeScreenShot("/settings.png")
|
||||
})
|
||||
|
||||
it("inspect", function() {
|
||||
browser.url(config.baseUrl+"?debug&style="+helper.getStyleUrl([
|
||||
"geojson:example"
|
||||
]));
|
||||
browser.waitForExist(".maputnik-toolbar-link");
|
||||
browser.flushReactUpdates();
|
||||
|
||||
browser.click(wd.$("nav:inspect"))
|
||||
browser.flushReactUpdates();
|
||||
|
||||
browser.takeScreenShot("/inspect.png")
|
||||
})
|
||||
})
|
||||
|
24
test/functional/util/coverage.js
Normal file
24
test/functional/util/coverage.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
var artifacts = require("../../artifacts");
|
||||
var fs = require("fs");
|
||||
var istanbulCov = require('istanbul-lib-coverage');
|
||||
|
||||
var COVERAGE_PATH = artifacts.pathSync("/coverage");
|
||||
|
||||
|
||||
var coverage = istanbulCov.createCoverageMap({});
|
||||
|
||||
// Capture the coverage after each test
|
||||
afterEach(function() {
|
||||
// Code coverage
|
||||
var results = browser.execute(function() {
|
||||
return window.__coverage__;
|
||||
});
|
||||
|
||||
coverage.merge(results.value);
|
||||
})
|
||||
|
||||
// Dump the coverage to a file
|
||||
after(function() {
|
||||
var jsonStr = JSON.stringify(coverage, null, 2);
|
||||
fs.writeFileSync(COVERAGE_PATH+"/coverage.json", jsonStr);
|
||||
})
|
58
test/functional/util/webdriverio-ext.js
Normal file
58
test/functional/util/webdriverio-ext.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
var artifacts = require("../../artifacts");
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
|
||||
|
||||
browser.timeoutsAsyncScript(20*1000);
|
||||
browser.timeoutsImplicitWait(20*1000);
|
||||
|
||||
var SCREENSHOTS_PATH = artifacts.pathSync("/screenshots");
|
||||
|
||||
/**
|
||||
* Sometimes chrome driver can result in the wrong text.
|
||||
*
|
||||
* See <https://github.com/webdriverio/webdriverio/issues/1886>
|
||||
*/
|
||||
try {
|
||||
browser.addCommand('setValueSafe', function(selector, text) {
|
||||
for(var i=0; i<10; i++) {
|
||||
browser.waitForVisible(selector);
|
||||
|
||||
var elements = browser.elements(selector);
|
||||
if(elements.length > 1) {
|
||||
throw "Too many elements found";
|
||||
}
|
||||
|
||||
browser.setValue(selector, text);
|
||||
var browserText = browser.getValue(selector);
|
||||
|
||||
if(browserText == text) {
|
||||
return;
|
||||
}
|
||||
else {
|
||||
console.error("Warning: setValue failed, trying again");
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for change events to fire and state updated
|
||||
browser.flushReactUpdates();
|
||||
})
|
||||
|
||||
browser.addCommand('takeScreenShot', function(filepath) {
|
||||
var data = browser.screenshot();
|
||||
fs.writeFileSync(path.join(SCREENSHOTS_PATH, filepath), data.value, 'base64');
|
||||
});
|
||||
|
||||
browser.addCommand('flushReactUpdates', function() {
|
||||
browser.executeAsync(function(done) {
|
||||
// For any events to propogate
|
||||
setImmediate(function() {
|
||||
// For the DOM to be updated.
|
||||
setImmediate(done);
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
} catch(err) {
|
||||
console.error(">>> Ignored error: "+err);
|
||||
}
|
90
test/geojson-server.js
Normal file
90
test/geojson-server.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
const cors = require("cors");
|
||||
const express = require("express");
|
||||
const fs = require("fs");
|
||||
const sourceData = require("./sources");
|
||||
|
||||
|
||||
var app = express();
|
||||
|
||||
app.use(cors());
|
||||
|
||||
|
||||
function buildStyle(opts) {
|
||||
opts = opts || {};
|
||||
opts = Object.assign({
|
||||
sources: {}
|
||||
}, opts);
|
||||
|
||||
return {
|
||||
"id": "test-style",
|
||||
"version": 8,
|
||||
"name": "Test Style",
|
||||
"metadata": {
|
||||
"maputnik:renderer": "mbgljs"
|
||||
},
|
||||
"sources": opts.sources,
|
||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||
"layers": []
|
||||
}
|
||||
}
|
||||
|
||||
function buildGeoJSONSource(data) {
|
||||
return {
|
||||
type: "vector",
|
||||
data: data
|
||||
};
|
||||
}
|
||||
|
||||
function buildResterSource(req, key) {
|
||||
return {
|
||||
"tileSize": 256,
|
||||
"tiles": [
|
||||
req.protocol + '://' + req.get('host') + "/" + key + "/{x}/{y}/{z}"
|
||||
],
|
||||
"type": "raster"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
app.get("/sources/raster/{x}/{y}/{z}", function(req, res) {
|
||||
res.status(404).end();
|
||||
})
|
||||
|
||||
app.get("/styles/empty/:sources", function(req, res) {
|
||||
var reqSources = req.params.sources.split(",");
|
||||
|
||||
var sources = {};
|
||||
reqSources.forEach(function(key) {
|
||||
var parts = key.split(":");
|
||||
var type = parts[0];
|
||||
var key = parts[1];
|
||||
|
||||
if(type === "geojson") {
|
||||
sources[key] = buildGeoJSONSource(sourceData[key]);
|
||||
}
|
||||
else if(type === "raster") {
|
||||
sources[key] = buildResterSource(req, key);
|
||||
}
|
||||
else {
|
||||
console.error("ERR: Invalid type: %s", type);
|
||||
throw "Invalid type"
|
||||
}
|
||||
});
|
||||
|
||||
var json = buildStyle({
|
||||
sources: sources
|
||||
});
|
||||
res.send(json);
|
||||
})
|
||||
|
||||
app.get("/example-style.json", function(req, res) {
|
||||
res.json(
|
||||
JSON.parse(
|
||||
fs.readFileSync(__dirname+"/example-style.json").toString()
|
||||
)
|
||||
);
|
||||
})
|
||||
|
||||
|
||||
module.exports = app;
|
15
test/sources/example.json
Normal file
15
test/sources/example.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"type":"FeatureCollection",
|
||||
"features":[
|
||||
{
|
||||
"type":"Feature",
|
||||
"properties": {
|
||||
"name": "Dinagat Islands"
|
||||
},
|
||||
"geometry":{
|
||||
"type": "Point",
|
||||
"coordinates": [125.6, 10.1]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
3
test/sources/index.js
Normal file
3
test/sources/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
example: require("./example")
|
||||
};
|
|
@ -1,15 +0,0 @@
|
|||
var assert = require('assert');
|
||||
var config = require("../config/specs");
|
||||
|
||||
|
||||
describe('maputnik', function() {
|
||||
|
||||
it('check logo exists', function () {
|
||||
browser.url(config.baseUrl);
|
||||
browser.waitForExist(".maputnik-toolbar-link");
|
||||
|
||||
var src = browser.getAttribute(".maputnik-toolbar-link img", "src");
|
||||
assert.equal(src, config.baseUrl+'/img/logo-color.svg');
|
||||
});
|
||||
|
||||
});
|
6
test/wd-helper.js
Normal file
6
test/wd-helper.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
"$": function(key, selector) {
|
||||
selector = selector || "";
|
||||
return "*[data-wd-key='"+key+"'] "+selector;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue