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

View file

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

View file

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

View file

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

View file

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