Better support for expressions

- Expression editing state
 - CodeMirror JSON editor
 - Improved styling
This commit is contained in:
orangemug 2020-02-01 17:07:52 +00:00
parent 725b752e35
commit c5c3e93aff
5 changed files with 78 additions and 29 deletions

View file

@ -22,7 +22,7 @@ function isDataField(value) {
* properties accept arrays as values, for example `text-font`. So we must try
* and create an expression.
*/
function isExpression(value, fieldSpec={}) {
function checkIsExpression (value, fieldSpec={}) {
if (!Array.isArray(value)) {
return false;
}
@ -72,6 +72,13 @@ export default class FunctionSpecProperty extends React.Component {
]),
}
constructor (props) {
super();
this.state = {
isExpression: checkIsExpression(props.value, props.fieldSpec),
}
}
getFieldFunctionType(fieldSpec) {
if (fieldSpec.expression.interpolated) {
return "exponential"
@ -103,6 +110,13 @@ export default class FunctionSpecProperty extends React.Component {
this.props.onChange(this.props.fieldName, changedValue)
}
deleteExpression = (newValue) => {
this.props.onChange(this.props.fieldName, newValue);
this.setState({
isExpression: false,
});
}
deleteStop = (stopIdx) => {
const stops = this.props.value.stops.slice(0)
stops.splice(stopIdx, 1)
@ -132,6 +146,10 @@ export default class FunctionSpecProperty extends React.Component {
makeExpression = () => {
const expression = ["literal", this.props.value || this.props.fieldSpec.default];
this.props.onChange(this.props.fieldName, expression);
this.setState({
isExpression: true,
});
}
makeDataFunction = () => {
@ -152,11 +170,12 @@ export default class FunctionSpecProperty extends React.Component {
const propClass = this.props.fieldSpec.default === this.props.value ? "maputnik-default-property" : "maputnik-modified-property"
let specField;
if (isExpression(this.props.value, this.props.fieldSpec)) {
if (this.state.isExpression) {
specField = (
<ExpressionProperty
error={this.props.error}
onChange={this.props.onChange.bind(this, this.props.fieldName)}
onDelete={this.deleteExpression}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={this.props.value}

View file

@ -8,6 +8,7 @@ import StringInput from '../inputs/StringInput'
import labelFromFieldName from './_labelFromFieldName'
import stringifyPretty from 'json-stringify-pretty-compact'
import JSONEditor from '../layers/JSONEditor'
function isLiteralExpression (value) {
@ -16,7 +17,7 @@ function isLiteralExpression (value) {
export default class ExpressionProperty extends React.Component {
static propTypes = {
onDeleteStop: PropTypes.func,
onDelete: PropTypes.func,
fieldName: PropTypes.string,
fieldSpec: PropTypes.object
}
@ -28,21 +29,14 @@ export default class ExpressionProperty extends React.Component {
};
}
onChange = (value) => {
try {
const jsonVal = JSON.parse(value);
if (isLiteralExpression(jsonVal)) {
this.setState({
lastValue: jsonVal
});
}
this.props.onChange(jsonVal);
}
catch (err) {
// TODO: Handle JSON parse error
onChange = (jsonVal) => {
if (isLiteralExpression(jsonVal)) {
this.setState({
lastValue: jsonVal
});
}
this.props.onChange(jsonVal);
}
onDelete = () => {
@ -50,13 +44,13 @@ export default class ExpressionProperty extends React.Component {
const {value, fieldName, fieldSpec} = this.props;
if (isLiteralExpression(value)) {
this.props.onChange(value[1]);
this.props.onDelete(value[1]);
}
else if (isLiteralExpression(lastValue)) {
this.props.onChange(lastValue[1]);
this.props.onDelete(lastValue[1]);
}
else {
this.props.onChange(fieldSpec.default);
this.props.onDelete(fieldSpec.default);
}
}
@ -75,12 +69,16 @@ export default class ExpressionProperty extends React.Component {
doc={this.props.fieldSpec.doc}
label={labelFromFieldName(this.props.fieldName)}
action={deleteStopBtn}
wideMode={true}
>
<StringInput
multi={true}
value={stringifyPretty(this.props.value, {indent: 2})}
spellCheck={false}
onInput={this.onChange}
<JSONEditor
className="maputnik-expression-editor"
layer={this.props.value}
lineNumbers={false}
maxHeight={200}
lineWrapping={true}
getValue={(data) => stringifyPretty(data, {indent: 2, maxLength: 50})}
onChange={this.onChange}
/>
</InputBlock>
}

View file

@ -28,6 +28,7 @@ class InputBlock extends React.Component {
data-wd-key={this.props["data-wd-key"]}
className={classnames({
"maputnik-input-block": true,
"maputnik-input-block--wide": this.props.wideMode,
"maputnik-action-block": this.props.action
})}
>

View file

@ -1,5 +1,6 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames';
import InputBlock from '../inputs/InputBlock'
import StringInput from '../inputs/StringInput'
@ -22,6 +23,15 @@ class JSONEditor extends React.Component {
layer: PropTypes.object.isRequired,
maxHeight: PropTypes.number,
onChange: PropTypes.func,
lineNumbers: PropTypes.bool,
}
static defaultProps = {
lineNumbers: true,
gutters: ["CodeMirror-lint-markers"],
getValue: (data) => {
return JSON.stringify(data, null, 2)
}
}
constructor(props) {
@ -33,7 +43,7 @@ class JSONEditor extends React.Component {
}
getValue () {
return JSON.stringify(this.props.layer, null, 2);
return this.props.getValue(this.props.layer);
}
componentDidMount () {
@ -43,13 +53,14 @@ class JSONEditor extends React.Component {
name: "javascript",
json: true
},
lineWrapping: this.props.lineWrapping,
tabSize: 2,
theme: 'maputnik',
viewportMargin: Infinity,
lineNumbers: true,
lineNumbers: this.props.lineNumbers,
lint: true,
matchBrackets: true,
gutters: ["CodeMirror-lint-markers"],
gutters: this.props.gutters,
scrollbarStyle: "null",
});
@ -113,7 +124,7 @@ class JSONEditor extends React.Component {
}
return <div
className="codemirror-container"
className={classnames("codemirror-container", this.props.className)}
ref={(el) => this._el = el}
style={style}
/>

View file

@ -35,6 +35,7 @@
height: 14px;
}
// TODO: Move these into correct *.scss files
.maputnik-inline-error {
color: #a4a4a4;
padding: 0.4em 0.4em;
@ -43,3 +44,22 @@
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;
}
}