diff --git a/package-lock.json b/package-lock.json
index e70bddac..334dbc5a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3168,6 +3168,11 @@
}
}
},
+ "code-error-fragment": {
+ "version": "0.0.230",
+ "resolved": "https://registry.npmjs.org/code-error-fragment/-/code-error-fragment-0.0.230.tgz",
+ "integrity": "sha512-cadkfKp6932H8UkhzE/gcUqhRMNf8jHzkAN7+5Myabswaghu4xABTgPHDCjW+dBAJxj/SpkTYokpzDqY4pCzQw=="
+ },
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
@@ -5783,8 +5788,7 @@
"grapheme-splitter": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
- "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
- "dev": true
+ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ=="
},
"grid-index": {
"version": "1.1.0",
@@ -7141,6 +7145,15 @@
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
"dev": true
},
+ "json-to-ast": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/json-to-ast/-/json-to-ast-2.1.0.tgz",
+ "integrity": "sha512-W9Lq347r8tA1DfMvAGn9QNcgYm4Wm7Yc+k8e6vezpMnRT+NHbtlxgNBXRVjXe9YM6eTn6+p/MKOlV/aABJcSnQ==",
+ "requires": {
+ "code-error-fragment": "0.0.230",
+ "grapheme-splitter": "^1.0.4"
+ }
+ },
"json3": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz",
diff --git a/package.json b/package.json
index de5583d2..ebfb4f0b 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
"color": "^3.1.2",
"detect-browser": "^4.8.0",
"file-saver": "^2.0.2",
+ "json-to-ast": "^2.1.0",
"jsonlint": "github:josdejong/jsonlint#85a19d7",
"lodash": "^4.17.15",
"lodash.capitalize": "^4.2.1",
diff --git a/src/components/App.jsx b/src/components/App.jsx
index f147a672..839129d4 100644
--- a/src/components/App.jsx
+++ b/src/components/App.jsx
@@ -3,6 +3,7 @@ import React from 'react'
import cloneDeep from 'lodash.clonedeep'
import clamp from 'lodash.clamp'
import get from 'lodash.get'
+import {unset} from 'lodash'
import {arrayMove} from 'react-sortable-hoc'
import url from 'url'
@@ -97,7 +98,7 @@ export default class App extends React.Component {
port = window.location.port
}
this.styleStore = new ApiStyleStore({
- onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, false),
+ onLocalStyleChange: mapStyle => this.onStyleChanged(mapStyle, {save: false}),
port: port,
host: params.get("localhost")
})
@@ -316,39 +317,87 @@ export default class App extends React.Component {
this.onStyleChanged(changedStyle)
}
- onStyleChanged = (newStyle, save=true) => {
+ onStyleChanged = (newStyle, opts={}) => {
+ opts = {
+ save: true,
+ addRevision: true,
+ ...opts,
+ };
- const errors = validate(newStyle, latest)
- if(errors.length === 0) {
-
- if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
- this.updateFonts(newStyle.glyphs)
+ const errors = validate(newStyle, latest) || [];
+ const mappedErrors = errors.map(error => {
+ const layerMatch = error.message.match(/layers\[(\d+)\]\.(?:(\S+)\.)?(\S+): (.*)/);
+ if (layerMatch) {
+ const [matchStr, index, group, property, message] = layerMatch;
+ const key = (group && property) ? [group, property].join(".") : property;
+ return {
+ message: error.message,
+ parsed: {
+ type: "layer",
+ data: {
+ index,
+ key,
+ message
+ }
+ }
+ }
}
- if(newStyle.sprite !== this.state.mapStyle.sprite) {
- this.updateIcons(newStyle.sprite)
+ else {
+ return {
+ message: error.message,
+ };
}
+ })
- this.revisionStore.addRevision(newStyle)
- if(save) this.saveStyle(newStyle)
- this.setState({
- mapStyle: newStyle,
- errors: [],
- })
- } else {
- this.setState({
- errors: errors.map(err => err.message)
- })
+ let dirtyMapStyle = undefined;
+ if (errors.length > 0) {
+ dirtyMapStyle = cloneDeep(newStyle);
+
+ errors.forEach(error => {
+ const {message} = error;
+ if (message) {
+ try {
+ const objPath = message.split(":")[0];
+ // Errors can be deply nested for example 'layers[0].filter[1][1][0]' we only care upto the property 'layers[0].filter'
+ const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^\[]+/)[0];
+ unset(dirtyMapStyle, unsetPath);
+ }
+ catch (err) {
+ console.warn(err);
+ }
+ }
+ });
}
+ if(newStyle.glyphs !== this.state.mapStyle.glyphs) {
+ this.updateFonts(newStyle.glyphs)
+ }
+ if(newStyle.sprite !== this.state.mapStyle.sprite) {
+ this.updateIcons(newStyle.sprite)
+ }
+
+ if (opts.addRevision) {
+ this.revisionStore.addRevision(newStyle);
+ }
+ if (opts.save) {
+ this.saveStyle(newStyle);
+ }
+
+ this.setState({
+ mapStyle: newStyle,
+ dirtyMapStyle: dirtyMapStyle,
+ errors: mappedErrors,
+ })
+
this.fetchSources();
}
onUndo = () => {
const activeStyle = this.revisionStore.undo()
+
const messages = undoMessages(this.state.mapStyle, activeStyle)
- this.saveStyle(activeStyle)
+ this.onStyleChanged(activeStyle, {addRevision: false});
this.setState({
- mapStyle: activeStyle,
infos: messages,
})
}
@@ -356,9 +405,8 @@ export default class App extends React.Component {
onRedo = () => {
const activeStyle = this.revisionStore.redo()
const messages = redoMessages(this.state.mapStyle, activeStyle)
- this.saveStyle(activeStyle)
+ this.onStyleChanged(activeStyle, {addRevision: false});
this.setState({
- mapStyle: activeStyle,
infos: messages,
})
}
@@ -546,10 +594,11 @@ export default class App extends React.Component {
}
mapRenderer() {
+ const {mapStyle, dirtyMapStyle} = this.state;
const metadata = this.state.mapStyle.metadata || {};
const mapProps = {
- mapStyle: this.state.mapStyle,
+ mapStyle: (dirtyMapStyle || mapStyle),
replaceAccessTokens: (mapStyle) => {
return style.replaceAccessTokens(mapStyle, {
allowFallback: true
@@ -663,6 +712,7 @@ export default class App extends React.Component {
selectedLayerIndex={this.state.selectedLayerIndex}
layers={layers}
sources={this.state.sources}
+ errors={this.state.errors}
/>
const layerEditor = selectedLayer ? : null
const bottomPanel = (this.state.errors.length + this.state.infos.length) > 0 ? : null
diff --git a/src/components/MessagePanel.jsx b/src/components/MessagePanel.jsx
index 53c873e0..f114a8c4 100644
--- a/src/components/MessagePanel.jsx
+++ b/src/components/MessagePanel.jsx
@@ -5,11 +5,45 @@ class MessagePanel extends React.Component {
static propTypes = {
errors: PropTypes.array,
infos: PropTypes.array,
+ mapStyle: PropTypes.object,
+ onLayerSelect: PropTypes.func,
+ currentLayer: PropTypes.object,
+ }
+
+ static defaultProps = {
+ onLayerSelect: () => {},
}
render() {
- const errors = this.props.errors.map((m, i) => {
- return
{m}
+ const errors = this.props.errors.map((error, idx) => {
+ let content;
+ if (error.parsed && error.parsed.type === "layer") {
+ const {parsed} = error;
+ const {mapStyle, currentLayer} = this.props;
+ const layerId = mapStyle.layers[parsed.data.index].id;
+ content = (
+ <>
+ Layer '{layerId}' : {parsed.data.message}
+ {currentLayer.id !== layerId &&
+ <>
+ —
+ this.props.onLayerSelect(layerId)}
+ >
+ switch to layer
+
+ >
+ }
+ >
+ );
+ }
+ else {
+ content = error.message;
+ }
+ return
+ {content}
+
})
const infos = this.props.infos.map((m, i) => {
diff --git a/src/components/fields/DocLabel.jsx b/src/components/fields/DocLabel.jsx
index 83fd6248..5375fbb8 100644
--- a/src/components/fields/DocLabel.jsx
+++ b/src/components/fields/DocLabel.jsx
@@ -49,8 +49,15 @@ export default class DocLabel extends React.Component {
}
+ else if (label) {
+ return
+
+ {label}
+
+
+ }
else {
- return
+
}
}
}
diff --git a/src/components/fields/FunctionSpecField.jsx b/src/components/fields/FunctionSpecField.jsx
index e2542c5a..cc8fb16c 100644
--- a/src/components/fields/FunctionSpecField.jsx
+++ b/src/components/fields/FunctionSpecField.jsx
@@ -4,14 +4,78 @@ import PropTypes from 'prop-types'
import SpecProperty from './_SpecProperty'
import DataProperty from './_DataProperty'
import ZoomProperty from './_ZoomProperty'
+import ExpressionProperty from './_ExpressionProperty'
+import {function as styleFunction} from '@mapbox/mapbox-gl-style-spec';
+function isLiteralExpression (value) {
+ return (Array.isArray(value) && value.length === 2 && value[0] === "literal");
+}
+
function isZoomField(value) {
- return typeof value === 'object' && value.stops && typeof value.property === 'undefined'
+ return (
+ typeof(value) === 'object' &&
+ value.stops &&
+ typeof(value.property) === 'undefined' &&
+ Array.isArray(value.stops) &&
+ value.stops.length > 1 &&
+ value.stops.every(stop => {
+ return (
+ Array.isArray(stop) &&
+ stop.length === 2
+ );
+ })
+ );
}
function isDataField(value) {
- return typeof value === 'object' && value.stops && typeof value.property !== 'undefined'
+ return (
+ typeof(value) === 'object' &&
+ value.stops &&
+ typeof(value.property) !== 'undefined' &&
+ value.stops.length > 1 &&
+ Array.isArray(value.stops) &&
+ value.stops.every(stop => {
+ return (
+ Array.isArray(stop) &&
+ stop.length === 2 &&
+ typeof(stop[0]) === 'object'
+ );
+ })
+ );
+}
+
+function isPrimative (value) {
+ const valid = ["string", "boolean", "number"];
+ return valid.includes(typeof(value));
+}
+
+function isArrayOfPrimatives (values) {
+ if (Array.isArray(values)) {
+ return values.every(isPrimative);
+ }
+ return false;
+}
+
+function getDataType (value, fieldSpec={}) {
+ if (value === undefined) {
+ return "value";
+ }
+ else if (isPrimative(value)) {
+ return "value";
+ }
+ else if (fieldSpec.type === "array" && isArrayOfPrimatives(value)) {
+ return "value";
+ }
+ else if (isZoomField(value)) {
+ return "zoom_function";
+ }
+ else if (isDataField(value)) {
+ return "data_function";
+ }
+ else {
+ return "expression";
+ }
}
/**
@@ -40,7 +104,9 @@ export default class FunctionSpecProperty extends React.Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
fieldName: PropTypes.string.isRequired,
+ fieldType: PropTypes.string.isRequired,
fieldSpec: PropTypes.object.isRequired,
+ errors: PropTypes.object,
value: PropTypes.oneOfType([
PropTypes.object,
@@ -51,6 +117,27 @@ export default class FunctionSpecProperty extends React.Component {
]),
}
+ constructor (props) {
+ super();
+ this.state = {
+ dataType: getDataType(props.value, props.fieldSpec),
+ isEditing: false,
+ }
+ }
+
+ static getDerivedStateFromProps(props, state) {
+ // Because otherwise when editing values we end up accidentally changing field type.
+ if (state.isEditing) {
+ return {};
+ }
+ else {
+ return {
+ isEditing: false,
+ dataType: getDataType(props.value, props.fieldSpec)
+ };
+ }
+ }
+
getFieldFunctionType(fieldSpec) {
if (fieldSpec.expression.interpolated) {
return "exponential"
@@ -82,6 +169,14 @@ export default class FunctionSpecProperty extends React.Component {
this.props.onChange(this.props.fieldName, changedValue)
}
+ deleteExpression = () => {
+ const {fieldSpec, fieldName} = this.props;
+ this.props.onChange(fieldName, fieldSpec.default);
+ this.setState({
+ dataType: "value",
+ });
+ }
+
deleteStop = (stopIdx) => {
const stops = this.props.value.stops.slice(0)
stops.splice(stopIdx, 1)
@@ -108,6 +203,39 @@ export default class FunctionSpecProperty extends React.Component {
this.props.onChange(this.props.fieldName, zoomFunc)
}
+ undoExpression = () => {
+ const {value, fieldName} = this.props;
+
+ if (isLiteralExpression(value)) {
+ this.props.onChange(fieldName, value[1]);
+ this.setState({
+ dataType: "value",
+ });
+ }
+ }
+
+ canUndo = () => {
+ const {value, fieldSpec} = this.props;
+ return (
+ isLiteralExpression(value) ||
+ isPrimative(value) ||
+ (Array.isArray(value) && fieldSpec.type === "array")
+ );
+ }
+
+ makeExpression = () => {
+ const {value, fieldSpec} = this.props;
+ let expression;
+
+ if (typeof(value) === "object" && 'stops' in value) {
+ expression = styleFunction.convertFunction(value, fieldSpec);
+ }
+ else {
+ expression = ["literal", value || this.props.fieldSpec.default];
+ }
+ this.props.onChange(this.props.fieldName, expression);
+ }
+
makeDataFunction = () => {
const functionType = this.getFieldFunctionType(this.props.fieldSpec);
const stopValue = functionType === 'categorical' ? '' : 0;
@@ -122,43 +250,78 @@ export default class FunctionSpecProperty extends React.Component {
this.props.onChange(this.props.fieldName, dataFunc)
}
+ onMarkEditing = () => {
+ this.setState({isEditing: true});
+ }
+
+ onUnmarkEditing = () => {
+ this.setState({isEditing: false});
+ }
+
render() {
+ const {dataType} = this.state;
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
let specField;
- if (isZoomField(this.props.value)) {
+ if (dataType === "expression") {
+ specField = (
+
+ );
+ }
+ else if (dataType === "zoom_function") {
specField = (
)
}
- else if (isDataField(this.props.value)) {
+ else if (dataType === "data_function") {
specField = (
)
}
else {
specField = (
)
}
diff --git a/src/components/fields/PropertyGroup.jsx b/src/components/fields/PropertyGroup.jsx
index 35577244..ae1f4df1 100644
--- a/src/components/fields/PropertyGroup.jsx
+++ b/src/components/fields/PropertyGroup.jsx
@@ -40,6 +40,7 @@ export default class PropertyGroup extends React.Component {
groupFields: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
spec: PropTypes.object.isRequired,
+ errors: PropTypes.object,
}
onPropertyChange = (property, newValue) => {
@@ -48,18 +49,22 @@ export default class PropertyGroup extends React.Component {
}
render() {
+ const {errors} = this.props;
const fields = this.props.groupFields.map(fieldName => {
const fieldSpec = getFieldSpec(this.props.spec, this.props.layer.type, fieldName)
const paint = this.props.layer.paint || {}
const layout = this.props.layer.layout || {}
const fieldValue = fieldName in paint ? paint[fieldName] : layout[fieldName]
+ const fieldType = fieldName in paint ? 'paint' : 'layout';
return
})
diff --git a/src/components/fields/_DataProperty.jsx b/src/components/fields/_DataProperty.jsx
index b4b5431d..80961b79 100644
--- a/src/components/fields/_DataProperty.jsx
+++ b/src/components/fields/_DataProperty.jsx
@@ -39,7 +39,9 @@ export default class DataProperty extends React.Component {
onChange: PropTypes.func,
onDeleteStop: PropTypes.func,
onAddStop: PropTypes.func,
+ onExpressionClick: PropTypes.func,
fieldName: PropTypes.string,
+ fieldType: PropTypes.string,
fieldSpec: PropTypes.object,
value: PropTypes.oneOfType([
PropTypes.object,
@@ -48,6 +50,7 @@ export default class DataProperty extends React.Component {
PropTypes.bool,
PropTypes.array
]),
+ errors: PropTypes.object,
}
state = {
@@ -144,6 +147,8 @@ export default class DataProperty extends React.Component {
}
render() {
+ const {fieldName, fieldType, errors} = this.props;
+
if (typeof this.props.value.type === "undefined") {
this.props.value.type = this.getFieldFunctionType(this.props.fieldSpec)
}
@@ -181,7 +186,22 @@ export default class DataProperty extends React.Component {
}
- return
+ const errorKeyStart = `${fieldType}.${fieldName}.stops[${idx}]`;
+ const foundErrors = Object.entries(errors).filter(([key, error]) => {
+ return key.startsWith(errorKeyStart);
+ });
+
+ const message = foundErrors.map(([key, error]) => {
+ return error.message;
+ }).join("");
+ const error = message ? {message} : undefined;
+
+ return
{zoomInput}
{dataInput}
@@ -198,58 +218,64 @@ export default class DataProperty extends React.Component {
})
return
-
-
-
-
-
- this.changeDataProperty("property", propVal)}
- />
-
-
-
-
-
- this.changeDataProperty("type", propVal)}
- title={"Select a type of data scale (default is 'categorical')."}
- options={this.getDataFunctionTypes(this.props.fieldSpec)}
- />
-
-
-
-
-
- this.changeDataProperty("default", propVal)}
- />
-
-
- {dataFields}
-
+
- Add stop
-
-
-
+
+
+
+ this.changeDataProperty("property", propVal)}
+ />
+
+
+
+
+
+ this.changeDataProperty("type", propVal)}
+ title={"Select a type of data scale (default is 'categorical')."}
+ options={this.getDataFunctionTypes(this.props.fieldSpec)}
+ />
+
+
+
+
+
+ this.changeDataProperty("default", propVal)}
+ />
+
+
+
+
+ {dataFields}
+
+ Add stop
+
+
+ Convert to expression
+
}
}
diff --git a/src/components/fields/_ExpressionProperty.jsx b/src/components/fields/_ExpressionProperty.jsx
new file mode 100644
index 00000000..99005991
--- /dev/null
+++ b/src/components/fields/_ExpressionProperty.jsx
@@ -0,0 +1,135 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+
+import InputBlock from '../inputs/InputBlock'
+import Button from '../Button'
+import {MdDelete, MdUndo} from 'react-icons/md'
+import StringInput from '../inputs/StringInput'
+
+import labelFromFieldName from './_labelFromFieldName'
+import stringifyPretty from 'json-stringify-pretty-compact'
+import JSONEditor from '../layers/JSONEditor'
+
+
+export default class ExpressionProperty extends React.Component {
+ static propTypes = {
+ onDelete: PropTypes.func,
+ fieldName: PropTypes.string,
+ fieldType: PropTypes.string,
+ fieldSpec: PropTypes.object,
+ value: PropTypes.any,
+ errors: PropTypes.object,
+ onChange: PropTypes.func,
+ onUndo: PropTypes.func,
+ canUndo: PropTypes.func,
+ onFocus: PropTypes.func,
+ onBlur: PropTypes.func,
+ }
+
+ static defaultProps = {
+ errors: {},
+ onFocus: () => {},
+ onBlur: () => {},
+ }
+
+ constructor (props) {
+ super();
+ this.state = {
+ jsonError: false,
+ };
+ }
+
+ onJSONInvalid = (err) => {
+ this.setState({
+ jsonError: true,
+ })
+ }
+
+ onJSONValid = () => {
+ this.setState({
+ jsonError: false,
+ })
+ }
+
+ render() {
+ const {errors, fieldName, fieldType, value, canUndo} = this.props;
+ const {jsonError} = this.state;
+ const undoDisabled = canUndo ? !canUndo() : true;
+
+ const deleteStopBtn = (
+ <>
+ {this.props.onUndo &&
+
+
+
+ }
+
+
+
+ >
+ );
+
+ const fieldKey = fieldType === undefined ? fieldName : `${fieldType}.${fieldName}`;
+
+ const fieldError = errors[fieldKey];
+ const errorKeyStart = `${fieldKey}[`;
+ const foundErrors = [];
+
+ function getValue (data) {
+ return stringifyPretty(data, {indent: 2, maxLength: 38})
+ }
+
+ if (jsonError) {
+ foundErrors.push({message: "Invalid JSON"});
+ }
+ else {
+ Object.entries(errors)
+ .filter(([key, error]) => {
+ return key.startsWith(errorKeyStart);
+ })
+ .forEach(([key, error]) => {
+ return foundErrors.push(error);
+ })
+
+ if (fieldError) {
+ foundErrors.push(fieldError);
+ }
+ }
+
+ return
+
+
+ }
+}
diff --git a/src/components/fields/_FunctionButtons.jsx b/src/components/fields/_FunctionButtons.jsx
index 220e77e8..e96b1351 100644
--- a/src/components/fields/_FunctionButtons.jsx
+++ b/src/components/fields/_FunctionButtons.jsx
@@ -3,18 +3,50 @@ import PropTypes from 'prop-types'
import Button from '../Button'
import {MdFunctions, MdInsertChart} from 'react-icons/md'
+import {mdiFunctionVariant} from '@mdi/js';
+/**
+ * So here we can't just check is `Array.isArray(value)` because certain
+ * properties accept arrays as values, for example `text-font`. So we must try
+ * and create an expression.
+ */
+function isExpression(value, fieldSpec={}) {
+ if (!Array.isArray(value)) {
+ return false;
+ }
+ try {
+ expression.createExpression(value, fieldSpec);
+ return true;
+ }
+ catch (err) {
+ return false;
+ }
+}
+
export default class FunctionButtons extends React.Component {
static propTypes = {
fieldSpec: PropTypes.object,
onZoomClick: PropTypes.func,
onDataClick: PropTypes.func,
+ onExpressionClick: PropTypes.func,
}
render() {
- let makeZoomButton, makeDataButton
+ let makeZoomButton, makeDataButton, expressionButton;
+
if (this.props.fieldSpec.expression.parameters.includes('zoom')) {
+ expressionButton = (
+
+
+
+
+
+ );
+
makeZoomButton =
}
- return {makeDataButton}{makeZoomButton}
+ return
+ {expressionButton}
+ {makeDataButton}
+ {makeZoomButton}
+
}
else {
- return null
+ return {expressionButton}
}
}
}
diff --git a/src/components/fields/_SpecProperty.jsx b/src/components/fields/_SpecProperty.jsx
index febe80a6..7799977e 100644
--- a/src/components/fields/_SpecProperty.jsx
+++ b/src/components/fields/_SpecProperty.jsx
@@ -13,18 +13,32 @@ export default class SpecProperty extends React.Component {
onZoomClick: PropTypes.func.isRequired,
onDataClick: PropTypes.func.isRequired,
fieldName: PropTypes.string,
- fieldSpec: PropTypes.object
+ fieldType: PropTypes.string,
+ fieldSpec: PropTypes.object,
+ value: PropTypes.any,
+ errors: PropTypes.object,
+ onExpressionClick: PropTypes.func,
+ }
+
+ static defaultProps = {
+ errors: {},
}
render() {
+ const {errors, fieldName, fieldType} = this.props;
+
const functionBtn =
+ const error = errors[fieldType+"."+fieldName];
+
return {
const zoomLevel = stop[0]
const key = this.state.refs[idx];
const value = stop[1]
const deleteStopBtn=
+ const errorKeyStart = `${fieldType}.${fieldName}.stops[${idx}]`;
+ const foundErrors = Object.entries(errors).filter(([key, error]) => {
+ return key.startsWith(errorKeyStart);
+ });
+
+ const message = foundErrors.map(([key, error]) => {
+ return error.message;
+ }).join("");
+ const error = message ? {message} : undefined;
+
return
Add stop
+
+ Convert to expression
+
}
}
diff --git a/src/components/fields/_labelFromFieldName.js b/src/components/fields/_labelFromFieldName.js
index fea405f6..666d833c 100644
--- a/src/components/fields/_labelFromFieldName.js
+++ b/src/components/fields/_labelFromFieldName.js
@@ -1,6 +1,13 @@
import capitalize from 'lodash.capitalize'
export default function labelFromFieldName(fieldName) {
- let label = fieldName.split('-').slice(1).join(' ')
- return capitalize(label)
+ let label;
+ const parts = fieldName.split('-');
+ if (parts.length > 1) {
+ label = fieldName.split('-').slice(1).join(' ');
+ }
+ else {
+ label = fieldName;
+ }
+ return capitalize(label);
}
diff --git a/src/components/filter/FilterEditor.jsx b/src/components/filter/FilterEditor.jsx
index e49948ef..e11125b2 100644
--- a/src/components/filter/FilterEditor.jsx
+++ b/src/components/filter/FilterEditor.jsx
@@ -2,13 +2,84 @@ import React from 'react'
import PropTypes from 'prop-types'
import { combiningFilterOps } from '../../libs/filterops.js'
-import {latest} from '@mapbox/mapbox-gl-style-spec'
+import {latest, validate, migrate} from '@mapbox/mapbox-gl-style-spec'
import DocLabel from '../fields/DocLabel'
import SelectInput from '../inputs/SelectInput'
+import InputBlock from '../inputs/InputBlock'
import SingleFilterEditor from './SingleFilterEditor'
import FilterEditorBlock from './FilterEditorBlock'
import Button from '../Button'
import SpecDoc from '../inputs/SpecDoc'
+import ExpressionProperty from '../fields/_ExpressionProperty';
+import {mdiFunctionVariant} from '@mdi/js';
+
+
+function combiningFilter (props) {
+ let filter = props.filter || ['all'];
+
+ if (!Array.isArray(filter)) {
+ return filter;
+ }
+
+ let combiningOp = filter[0];
+ let filters = filter.slice(1);
+
+ if(combiningFilterOps.indexOf(combiningOp) < 0) {
+ combiningOp = 'all';
+ filters = [filter.slice(0)];
+ }
+
+ return [combiningOp, ...filters];
+}
+
+function migrateFilter (filter) {
+ return migrate(createStyleFromFilter(filter)).layers[0].filter;
+}
+
+function createStyleFromFilter (filter) {
+ return {
+ "id": "tmp",
+ "version": 8,
+ "name": "Empty Style",
+ "metadata": {"maputnik:renderer": "mbgljs"},
+ "sources": {
+ "tmp": {
+ "type": "geojson",
+ "data": {}
+ }
+ },
+ "sprite": "",
+ "glyphs": "https://orangemug.github.io/font-glyphs/glyphs/{fontstack}/{range}.pbf",
+ "layers": [
+ {
+ id: "tmp",
+ type: "fill",
+ source: "tmp",
+ filter: filter,
+ },
+ ],
+ };
+}
+
+/**
+ * This is doing way more work than we need it to, however validating a whole
+ * style if the only thing that's exported from mapbox-gl-style-spec at the
+ * moment. Not really an issue though as it take ~0.1ms to calculate.
+ */
+function checkIfSimpleFilter (filter) {
+ if (!filter || !combiningFilterOps.includes(filter[0])) {
+ return false;
+ }
+
+ // Because "none" isn't supported by the next expression syntax we can test
+ // with ["none", ...] because it'll return false if it's a new style
+ // expression.
+ const moddedFilter = ["none", ...filter.slice(1)];
+ const tmpStyle = createStyleFromFilter(moddedFilter)
+
+ const errors = validate(tmpStyle);
+ return (errors.length < 1);
+}
function hasCombiningFilter(filter) {
return combiningFilterOps.indexOf(filter[0]) >= 0
@@ -27,46 +98,37 @@ export default class CombiningFilterEditor extends React.Component {
/** Properties of the vector layer and the available fields */
properties: PropTypes.object,
filter: PropTypes.array,
+ errors: PropTypes.object,
onChange: PropTypes.func.isRequired,
}
- constructor () {
+ static defaultProps = {
+ filter: ["all"],
+ }
+
+ constructor (props) {
super();
this.state = {
showDoc: false,
+ displaySimpleFilter: checkIfSimpleFilter(combiningFilter(props)),
};
}
// Convert filter to combining filter
- combiningFilter() {
- let filter = this.props.filter || ['all']
-
- let combiningOp = filter[0]
- let filters = filter.slice(1)
-
- if(combiningFilterOps.indexOf(combiningOp) < 0) {
- combiningOp = 'all'
- filters = [filter.slice(0)]
- }
-
- return [combiningOp, ...filters]
- }
-
onFilterPartChanged(filterIdx, newPart) {
- const newFilter = this.combiningFilter().slice(0)
+ const newFilter = combiningFilter(this.props).slice(0)
newFilter[filterIdx] = newPart
this.props.onChange(newFilter)
}
deleteFilterItem(filterIdx) {
- const newFilter = this.combiningFilter().slice(0)
- console.log('Delete', filterIdx, newFilter)
+ const newFilter = combiningFilter(this.props).slice(0)
newFilter.splice(filterIdx + 1, 1)
this.props.onChange(newFilter)
}
addFilterItem = () => {
- const newFilterItem = this.combiningFilter().slice(0)
+ const newFilterItem = combiningFilter(this.props).slice(0)
newFilterItem.push(['==', 'name', ''])
this.props.onChange(newFilterItem)
}
@@ -77,60 +139,171 @@ export default class CombiningFilterEditor extends React.Component {
});
}
- render() {
- const filter = this.combiningFilter()
- let combiningOp = filter[0]
- let filters = filter.slice(1)
+ makeFilter = () => {
+ this.setState({
+ displaySimpleFilter: true,
+ })
+ }
+ makeExpression = () => {
+ let filter = combiningFilter(this.props);
+ this.props.onChange(migrateFilter(filter));
+ this.setState({
+ displaySimpleFilter: false,
+ })
+ }
+
+ static getDerivedStateFromProps (props, currentState) {
+ const {filter} = props;
+ const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
+
+ // Upgrade but never downgrade
+ if (!displaySimpleFilter && currentState.displaySimpleFilter === true) {
+ return {
+ displaySimpleFilter: false,
+ valueIsSimpleFilter: false,
+ };
+ }
+ else if (displaySimpleFilter && currentState.displaySimpleFilter === false) {
+ return {
+ valueIsSimpleFilter: true,
+ }
+ }
+ else {
+ return {
+ valueIsSimpleFilter: false,
+ };
+ }
+ }
+
+ render() {
+ const {errors} = this.props;
+ const {displaySimpleFilter} = this.state;
const fieldSpec={
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
};
+ const defaultFilter = ["all"];
- const editorBlocks = filters.map((f, idx) => {
- return
-
-
- })
+ const isNestedCombiningFilter = displaySimpleFilter && hasNestedCombiningFilter(combiningFilter(this.props));
- //TODO: Implement support for nested filter
- if(hasNestedCombiningFilter(filter)) {
+ if (isNestedCombiningFilter) {
return
- Nested filters are not supported.
-
- }
-
- return
-
-
-
-
- {editorBlocks}
-
+
+ Nested filters are not supported.
+
- Add filter
+ onClick={this.makeExpression}
+ >
+
+
+
+ Upgrade to expression
-
-
-
-
+ }
+ else if (displaySimpleFilter) {
+ const filter = combiningFilter(this.props);
+ let combiningOp = filter[0];
+ let filters = filter.slice(1)
+
+ const actions = (
+
+ );
+
+ const editorBlocks = filters.map((f, idx) => {
+ const error = errors[`filter[${idx+1}]`];
+
+ return (
+ <>
+
+
+
+ {error &&
+ {error.message}
+ }
+ >
+ );
+ })
+
+
+ return (
+ <>
+
+
+
+ {editorBlocks}
+
+
+ Add filter
+
+
+
+
+
+ >
+ );
+ }
+ else {
+ let {filter} = this.props;
+
+ return (
+ <>
+ {
+ this.setState({displaySimpleFilter: true});
+ this.props.onChange(defaultFilter);
+ }}
+ fieldName="filter"
+ fieldSpec={fieldSpec}
+ value={filter}
+ errors={errors}
+ onChange={this.props.onChange}
+ />
+ {this.state.valueIsSimpleFilter &&
+
+ You've entered a old style filter,{' '}
+
+ switch to filter editor
+
+
+ }
+ >
+ );
+ }
}
}
diff --git a/src/components/icons/LayerIcon.jsx b/src/components/icons/LayerIcon.jsx
index 7cba8cef..fb5d3277 100644
--- a/src/components/icons/LayerIcon.jsx
+++ b/src/components/icons/LayerIcon.jsx
@@ -6,6 +6,7 @@ import FillIcon from './FillIcon.jsx'
import SymbolIcon from './SymbolIcon.jsx'
import BackgroundIcon from './BackgroundIcon.jsx'
import CircleIcon from './CircleIcon.jsx'
+import MissingIcon from './MissingIcon.jsx'
class LayerIcon extends React.Component {
static propTypes = {
@@ -25,6 +26,7 @@ class LayerIcon extends React.Component {
case 'line': return
case 'symbol': return
case 'circle': return
+ default: return
}
}
}
diff --git a/src/components/icons/MissingIcon.jsx b/src/components/icons/MissingIcon.jsx
new file mode 100644
index 00000000..71f76ddd
--- /dev/null
+++ b/src/components/icons/MissingIcon.jsx
@@ -0,0 +1,11 @@
+import React from 'react'
+import {MdPriorityHigh} from 'react-icons/md'
+
+
+export default class MissingIcon extends React.Component {
+ render() {
+ return (
+
+ )
+ }
+}
diff --git a/src/components/inputs/InputBlock.jsx b/src/components/inputs/InputBlock.jsx
index f35d3b1a..5e1976ed 100644
--- a/src/components/inputs/InputBlock.jsx
+++ b/src/components/inputs/InputBlock.jsx
@@ -18,6 +18,8 @@ class InputBlock extends React.Component {
style: PropTypes.object,
onChange: PropTypes.func,
fieldSpec: PropTypes.object,
+ wideMode: PropTypes.bool,
+ error: PropTypes.array,
}
constructor (props) {
@@ -39,10 +41,13 @@ class InputBlock extends React.Component {
}
render() {
+ const errors = [].concat(this.props.error || []);
+
return
@@ -68,6 +73,13 @@ class InputBlock extends React.Component {
{this.props.children}
+ {errors.length > 0 &&
+
+ {[].concat(this.props.error).map((error, idx) => {
+ return
{error.message}
+ })}
+
+ }
{this.props.fieldSpec &&
also the API has changed, see comment in file
-import '../../vendor/codemirror/addon/lint/json-lint'
+import stringifyPretty from 'json-stringify-pretty-compact'
+import '../util/codemirror-mgl';
class JSONEditor extends React.Component {
static propTypes = {
- layer: PropTypes.object.isRequired,
+ layer: PropTypes.any.isRequired,
maxHeight: PropTypes.number,
onChange: PropTypes.func,
+ lineNumbers: PropTypes.bool,
+ lineWrapping: PropTypes.bool,
+ getValue: PropTypes.func,
+ gutters: PropTypes.array,
+ className: PropTypes.string,
+ onFocus: PropTypes.func,
+ onBlur: PropTypes.func,
+ onJSONValid: PropTypes.func,
+ onJSONInvalid: PropTypes.func,
+ mode: PropTypes.object,
+ lint: PropTypes.oneOfType([
+ PropTypes.bool,
+ PropTypes.object,
+ ]),
+ }
+
+ static defaultProps = {
+ lineNumbers: true,
+ lineWrapping: false,
+ gutters: ["CodeMirror-lint-markers"],
+ getValue: (data) => {
+ return stringifyPretty(data, {indent: 2, maxLength: 40});
+ },
+ onFocus: () => {},
+ onBlur: () => {},
+ onJSONInvalid: () => {},
+ onJSONValid: () => {},
}
constructor(props) {
super(props)
this.state = {
isEditing: false,
- prevValue: this.getValue(),
+ prevValue: this.props.getValue(this.props.layer),
};
}
- getValue () {
- return JSON.stringify(this.props.layer, null, 2);
- }
-
componentDidMount () {
this._doc = CodeMirror(this._el, {
- value: this.getValue(),
- mode: {
- name: "javascript",
- json: true
+ value: this.props.getValue(this.props.layer),
+ mode: this.props.mode || {
+ name: "mgl",
},
+ lineWrapping: this.props.lineWrapping,
tabSize: 2,
theme: 'maputnik',
viewportMargin: Infinity,
- lineNumbers: true,
- lint: true,
+ lineNumbers: this.props.lineNumbers,
+ lint: this.props.lint || {
+ context: "layer"
+ },
matchBrackets: true,
- gutters: ["CodeMirror-lint-markers"],
+ gutters: this.props.gutters,
scrollbarStyle: "null",
});
@@ -58,12 +83,14 @@ class JSONEditor extends React.Component {
}
onFocus = () => {
+ this.props.onFocus();
this.setState({
isEditing: true
});
}
onBlur = () => {
+ this.props.onBlur();
this.setState({
isEditing: false
});
@@ -79,7 +106,7 @@ class JSONEditor extends React.Component {
if (!this.state.isEditing && prevProps.layer !== this.props.layer) {
this._cancelNextChange = true;
this._doc.setValue(
- this.getValue(),
+ this.props.getValue(this.props.layer),
)
}
}
@@ -87,16 +114,28 @@ class JSONEditor extends React.Component {
onChange = (e) => {
if (this._cancelNextChange) {
this._cancelNextChange = false;
+ this.setState({
+ prevValue: this._doc.getValue(),
+ })
return;
}
const newCode = this._doc.getValue();
if (this.state.prevValue !== newCode) {
+ let parsedLayer, err;
try {
- const parsedLayer = JSON.parse(newCode)
+ parsedLayer = JSON.parse(newCode);
+ } catch(_err) {
+ err = _err;
+ console.warn(_err)
+ }
+
+ if (err) {
+ this.props.onJSONInvalid();
+ }
+ else {
this.props.onChange(parsedLayer)
- } catch(err) {
- console.warn(err)
+ this.props.onJSONValid();
}
}
@@ -112,7 +151,7 @@ class JSONEditor extends React.Component {
}
return
this._el = el}
style={style}
/>
diff --git a/src/components/layers/LayerEditor.jsx b/src/components/layers/LayerEditor.jsx
index ed161163..79b4c161 100644
--- a/src/components/layers/LayerEditor.jsx
+++ b/src/components/layers/LayerEditor.jsx
@@ -20,6 +20,10 @@ import { changeType, changeProperty } from '../../libs/layer'
import layout from '../../config/layout.json'
+function getLayoutForType (type) {
+ return layout[type] ? layout[type] : layout.invalid;
+}
+
function layoutGroups(layerType) {
const layerGroup = {
title: 'Layer',
@@ -33,7 +37,9 @@ function layoutGroups(layerType) {
title: 'JSON Editor',
type: 'jsoneditor'
}
- return [layerGroup, filterGroup].concat(layout[layerType].groups).concat([editorGroup])
+ return [layerGroup, filterGroup]
+ .concat(getLayoutForType(layerType).groups)
+ .concat([editorGroup])
}
/** Layer editor supporting multiple types of layers. */
@@ -52,6 +58,7 @@ export default class LayerEditor extends React.Component {
isFirstLayer: PropTypes.bool,
isLastLayer: PropTypes.bool,
layerIndex: PropTypes.number,
+ errors: PropTypes.array,
}
static defaultProps = {
@@ -79,7 +86,7 @@ export default class LayerEditor extends React.Component {
static getDerivedStateFromProps(props, state) {
const additionalGroups = { ...state.editorGroups }
- layout[props.layer.type].groups.forEach(group => {
+ getLayoutForType(props.layer.type).groups.forEach(group => {
if(!(group.title in additionalGroups)) {
additionalGroups[group.title] = true
}
@@ -118,6 +125,20 @@ export default class LayerEditor extends React.Component {
if(this.props.layer.metadata) {
comment = this.props.layer.metadata['maputnik:comment']
}
+ const {errors, layerIndex} = this.props;
+
+ const errorData = {};
+ errors.forEach(error => {
+ if (
+ error.parsed &&
+ error.parsed.type === "layer" &&
+ error.parsed.data.index == layerIndex
+ ) {
+ errorData[error.parsed.data.key] = {
+ message: error.parsed.data.message
+ };
+ }
+ })
let sourceLayerIds;
if(this.props.sources.hasOwnProperty(this.props.layer.source)) {
@@ -129,13 +150,17 @@ export default class LayerEditor extends React.Component {
this.props.onLayerIdChange(this.props.layer.id, newId)}
/>
this.props.onLayerChanged(changeType(this.props.layer, newType))}
/>
{this.props.layer.type !== 'background' && this.changeProperty(null, 'source', v)}
@@ -143,20 +168,24 @@ export default class LayerEditor extends React.Component {
}
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.props.layer.type) < 0 &&
this.changeProperty(null, 'source-layer', v)}
/>
}
this.changeProperty(null, 'minzoom', v)}
/>
this.changeProperty(null, 'maxzoom', v)}
/>
this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)}
/>
@@ -164,6 +193,7 @@ export default class LayerEditor extends React.Component {
case 'filter': return
this.changeProperty(null, 'filter', f)}
@@ -171,6 +201,7 @@ export default class LayerEditor extends React.Component {
case 'properties': return {
const groupIdx = findClosestCommonPrefix(this.props.layers, idx)
+ const layerError = this.props.errors.find(error => {
+ return (
+ error.parsed &&
+ error.parsed.type === "layer" &&
+ error.parsed.data.index == idx
+ );
+ });
+
const listItem = 1 && this.isCollapsed(groupPrefix, groupIdx) && idx !== this.props.selectedLayerIndex,
- 'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1
+ 'maputnik-layer-list-item-group-last': idxInGroup == layers.length - 1 && layers.length > 1,
+ 'maputnik-layer-list-item--error': !!layerError
})}
index={idx}
key={layer.id}
diff --git a/src/components/layers/LayerTypeBlock.jsx b/src/components/layers/LayerTypeBlock.jsx
index 6e29c25c..1c685986 100644
--- a/src/components/layers/LayerTypeBlock.jsx
+++ b/src/components/layers/LayerTypeBlock.jsx
@@ -4,33 +4,49 @@ import PropTypes from 'prop-types'
import {latest} from '@mapbox/mapbox-gl-style-spec'
import InputBlock from '../inputs/InputBlock'
import SelectInput from '../inputs/SelectInput'
+import StringInput from '../inputs/StringInput'
class LayerTypeBlock extends React.Component {
static propTypes = {
value: PropTypes.string.isRequired,
wdKey: PropTypes.string,
onChange: PropTypes.func.isRequired,
+ error: PropTypes.object,
+ disabled: PropTypes.bool,
+ }
+
+ static defaultProps = {
+ disabled: false,
}
render() {
return
-
+ {this.props.disabled &&
+
+ }
+ {!this.props.disabled &&
+
+ }
}
}
diff --git a/src/components/layers/MaxZoomBlock.jsx b/src/components/layers/MaxZoomBlock.jsx
index d05ac76f..ee75f42f 100644
--- a/src/components/layers/MaxZoomBlock.jsx
+++ b/src/components/layers/MaxZoomBlock.jsx
@@ -9,10 +9,12 @@ class MaxZoomBlock extends React.Component {
static propTypes = {
value: PropTypes.number,
onChange: PropTypes.func.isRequired,
+ error: PropTypes.object,
}
render() {
return
0) {
+ // JSON invalid so don't go any further
+ return found;
+ }
+
+ const ast = jsonToAst(text);
+ const input = JSON.parse(text);
+
+ function getArrayPositionalFromAst (node, path) {
+ if (!node) {
+ return undefined;
+ }
+ else if (path.length < 1) {
+ return node;
+ }
+ else if (!node.children) {
+ return undefined;
+ }
+ else {
+ const key = path[0];
+ let newNode;
+ if (key.match(/^[0-9]+$/)) {
+ newNode = node.children[path[0]];
+ }
+ else {
+ newNode = node.children.find(childNode => {
+ return (
+ childNode.key &&
+ childNode.key.type === "Identifier" &&
+ childNode.key.value === key
+ );
+ });
+ if (newNode) {
+ newNode = newNode.value;
+ }
+ }
+ return getArrayPositionalFromAst(newNode, path.slice(1))
+ }
+ }
+
+ let out;
+ if (context === "layer") {
+ // Just an empty style so we can validate a layer.
+ const errors = validate({
+ "version": 8,
+ "name": "Empty Style",
+ "metadata": {},
+ "sources": {},
+ "sprite": "",
+ "glyphs": "https://example.com/glyphs/{fontstack}/{range}.pbf",
+ "layers": [
+ input
+ ]
+ });
+
+ if (errors) {
+ out = {
+ result: "error",
+ value: errors
+ .filter(err => {
+ // Remove missing 'layer source' errors, because we don't include them
+ if (err.message.match(/^layers\[0\]: source ".*" not found$/)) {
+ return false;
+ }
+ else {
+ return true;
+ }
+ })
+ .map(err => {
+ // Remove the 'layers[0].' as we're validating the layer only here
+ const errMessageParts = err.message.replace(/^layers\[0\]./, "").split(":");
+ return {
+ key: errMessageParts[0],
+ message: errMessageParts[1],
+ };
+ })
+ }
+ }
+ }
+ else if (context === "expression") {
+ out = expression.createExpression(input, opts.spec);
+ }
+ else {
+ throw new Error(`Invalid context ${context}`);
+ }
+
+ if (out.result === "error") {
+ const errors = out.value;
+ errors.forEach(error => {
+ const {key, message} = error;
+
+ if (!key) {
+ const lastLineHandle = doc.getLineHandle(doc.lastLine());
+ const err = {
+ from: CodeMirror.Pos(doc.firstLine(), 0),
+ to: CodeMirror.Pos(doc.lastLine(), lastLineHandle.text.length),
+ message: message,
+ }
+ found.push(err);
+ }
+ else if (key) {
+ const path = key.replace(/^\[|\]$/g, "").split(/\.|[\[\]]+/).filter(Boolean)
+ const parsedError = getArrayPositionalFromAst(ast, path);
+ if (!parsedError) {
+ console.warn("Something went wrong parsing error:", error);
+ return;
+ }
+
+ const {loc} = parsedError;
+ const {start, end} = loc;
+
+ found.push({
+ from: CodeMirror.Pos(start.line - 1, start.column),
+ to: CodeMirror.Pos(end.line - 1, end.column),
+ message: message,
+ });
+ }
+ })
+ }
+
+ return found;
+});
diff --git a/src/config/layout.json b/src/config/layout.json
index 7d23808a..7b3321d0 100644
--- a/src/config/layout.json
+++ b/src/config/layout.json
@@ -233,5 +233,8 @@
]
}
]
+ },
+ "invalid": {
+ "groups": []
}
}
diff --git a/src/styles/_components.scss b/src/styles/_components.scss
index 8a6ce103..eedefb50 100644
--- a/src/styles/_components.scss
+++ b/src/styles/_components.scss
@@ -146,7 +146,7 @@
.maputnik-icon-button {
background-color: transparent;
- &:hover {
+ &:hover:not(:disabled) {
background-color: transparent;
label,
@@ -182,18 +182,26 @@
.maputnik-action-block {
.maputnik-input-block-label {
display: inline-block;
- width: 35%;
+ width: 32%;
}
.maputnik-input-block-action {
vertical-align: top;
display: inline-block;
- width: 15%;
+ width: 18%;
}
.maputnik-input-block-action > div {
text-align: right;
}
+
+}
+
+.maputnik-data-spec-block,
+.maputnik-zoom-spec-property {
+ .maputnik-inline-error {
+ margin-left: 32%;
+ }
}
// SPACE HELPER
@@ -208,6 +216,12 @@
&-error {
color: $color-red;
}
+
+ &__switch-button {
+ all: unset;
+ text-decoration: underline;
+ cursor: pointer;
+ }
}
.maputnik-dialog {
@@ -238,3 +252,51 @@
}
}
}
+
+.maputnik-inline-error {
+ color: #a4a4a4;
+ padding: 0.4em 0.4em;
+ font-size: 0.9em;
+ border: solid 1px $color-red;
+ border-radius: 2px;
+ margin: $margin-2 0px;
+}
+
+.maputnik-expression-editor {
+ border: solid 1px $color-gray;
+}
+
+.maputnik-input-block--wide {
+ .maputnik-input-block-content {
+ display: block;
+ width: auto;
+ }
+
+ .maputnik-input-block-label {
+ width: 82%;
+ }
+
+ .maputnik-input-block-action {
+ text-align: right;
+ }
+}
+
+.maputnik-expr-infobox {
+ font-size: $font-size-6;
+ background: $color-midgray;
+ padding: $margin-2;
+ border-radius: 2px;
+ border-top-right-radius: 0px;
+ border-top-left-radius: 0px;
+ color: $color-white;
+}
+
+.maputnik-expr-infobox__button {
+ background: none;
+ border: none;
+ padding: 0;
+ text-decoration: underline;
+ color: currentColor;
+ cursor: pointer;
+}
+
diff --git a/src/styles/_filtereditor.scss b/src/styles/_filtereditor.scss
index 7c3d1542..d836be9e 100644
--- a/src/styles/_filtereditor.scss
+++ b/src/styles/_filtereditor.scss
@@ -1,5 +1,10 @@
.maputnik-filter-editor-wrapper {
padding: $margin-3;
+ overflow: hidden;
+
+ .maputnik-input-block {
+ margin: 0;
+ }
}
.maputnik-filter-editor {
diff --git a/src/styles/_input.scss b/src/styles/_input.scss
index 03aa4ea3..b9a48ec7 100644
--- a/src/styles/_input.scss
+++ b/src/styles/_input.scss
@@ -25,6 +25,11 @@
resize: vertical;
height: 78px;
}
+
+ &--disabled {
+ background: transparent;
+ border: none;
+ }
}
.maputnik-number-container {
diff --git a/src/styles/_layer.scss b/src/styles/_layer.scss
index e933da4f..1648bd44 100644
--- a/src/styles/_layer.scss
+++ b/src/styles/_layer.scss
@@ -99,6 +99,11 @@
}
}
+
+ .maputnik-layer-list-item--error {
+ color: $color-red;
+ }
+
&-item-selected {
color: $color-white;
}
diff --git a/src/styles/_layout.scss b/src/styles/_layout.scss
index f3db7eb1..b18dfa20 100644
--- a/src/styles/_layout.scss
+++ b/src/styles/_layout.scss
@@ -38,7 +38,6 @@
&-bottom {
position: fixed;
- height: 50px;
bottom: 0;
right: 0;
z-index: 1;
diff --git a/src/styles/_zoomproperty.scss b/src/styles/_zoomproperty.scss
index 2b9ce14b..38e26c55 100644
--- a/src/styles/_zoomproperty.scss
+++ b/src/styles/_zoomproperty.scss
@@ -2,9 +2,8 @@
.maputnik-make-zoom-function {
background-color: transparent;
display: inline-block;
- padding-bottom: 0;
- padding-top: 0;
vertical-align: middle;
+ padding: 0 $margin-2 0 0;
@extend .maputnik-icon-button;
}
@@ -63,9 +62,8 @@
.maputnik-make-data-function {
background-color: transparent;
display: inline-block;
- padding-bottom: 0;
- padding-top: 0;
vertical-align: middle;
+ padding: 0 $margin-2 0 0;
@extend .maputnik-icon-button;
}
@@ -98,10 +96,6 @@
.maputnik-data-spec-property-input {
width: 75%;
display: inline-block;
-
- .maputnik-string {
- margin-bottom: 3%;
- }
}
}
}
diff --git a/src/styles/index.scss b/src/styles/index.scss
index 759b73cf..11cca10a 100644
--- a/src/styles/index.scss
+++ b/src/styles/index.scss
@@ -34,3 +34,4 @@
width: 14px;
height: 14px;
}
+
diff --git a/src/vendor/codemirror/addon/lint/json-lint.js b/src/vendor/codemirror/addon/lint/json-lint.js
deleted file mode 100644
index 5c80a840..00000000
--- a/src/vendor/codemirror/addon/lint/json-lint.js
+++ /dev/null
@@ -1,31 +0,0 @@
-// CodeMirror, copyright (c) by Marijn Haverbeke and others
-// Distributed under an MIT license: http://codemirror.net/LICENSE
-
-// Depends on fork of jsonlint from
-// becuase of
-var jsonlint = require("jsonlint");
-var CodeMirror = require("codemirror");
-
-CodeMirror.registerHelper("lint", "json", function(text) {
- var found = [];
-
- // NOTE: This was modified from the original to remove the global, also the
- // old jsonlint API was 'jsonlint.parseError' its now
- // 'jsonlint.parser.parseError'
- jsonlint.parser.parseError = function(str, hash) {
- var loc = hash.loc;
- found.push({
- from: CodeMirror.Pos(loc.first_line - 1, loc.first_column),
- to: CodeMirror.Pos(loc.last_line - 1, loc.last_column),
- message: str
- });
- };
-
- try {
- jsonlint.parse(text);
- }
- catch(e) {
- // Do nothing we catch the error above
- }
- return found;
-});