-
+
{this.props.children}
diff --git a/src/components/InputArray.jsx b/src/components/InputArray.jsx
new file mode 100644
index 0000000..dc24362
--- /dev/null
+++ b/src/components/InputArray.jsx
@@ -0,0 +1,113 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import InputString from './InputString'
+import InputNumber from './InputNumber'
+
+export default class FieldArray extends React.Component {
+ static propTypes = {
+ value: PropTypes.array,
+ type: PropTypes.string,
+ length: PropTypes.number,
+ default: PropTypes.array,
+ onChange: PropTypes.func,
+ 'aria-label': PropTypes.string,
+ }
+
+ static defaultProps = {
+ value: [],
+ default: [],
+ }
+
+ constructor (props) {
+ super(props);
+ this.state = {
+ value: this.props.value.slice(0),
+ // This is so we can compare changes in getDerivedStateFromProps
+ initialPropsValue: this.props.value.slice(0),
+ };
+ }
+
+ static getDerivedStateFromProps(props, state) {
+ const value = [];
+ const initialPropsValue = state.initialPropsValue.slice(0);
+
+ Array(props.length).fill(null).map((_, i) => {
+ if (props.value[i] === state.initialPropsValue[i]) {
+ value[i] = state.value[i];
+ }
+ else {
+ value[i] = state.value[i];
+ initialPropsValue[i] = state.value[i];
+ }
+ })
+
+ return {
+ value,
+ initialPropsValue,
+ };
+ }
+
+ isComplete (value) {
+ return Array(this.props.length).fill(null).every((_, i) => {
+ const val = value[i]
+ return !(val === undefined || val === "");
+ });
+ }
+
+ changeValue(idx, newValue) {
+ const value = this.state.value.slice(0);
+ value[idx] = newValue;
+
+ this.setState({
+ value,
+ }, () => {
+ if (this.isComplete(value)) {
+ this.props.onChange(value);
+ }
+ else {
+ // Unset until complete
+ this.props.onChange(undefined);
+ }
+ });
+ }
+
+ render() {
+ const {value} = this.state;
+
+ const containsValues = (
+ value.length > 0 &&
+ !value.every(val => {
+ return (val === "" || val === undefined)
+ })
+ );
+
+ const inputs = Array(this.props.length).fill(null).map((_, i) => {
+ if(this.props.type === 'number') {
+ return
+ } else {
+ return
+ }
+ })
+
+ return (
+
+ {inputs}
+
+ )
+ }
+}
+
diff --git a/src/components/InputAutocomplete.jsx b/src/components/InputAutocomplete.jsx
new file mode 100644
index 0000000..8d2027b
--- /dev/null
+++ b/src/components/InputAutocomplete.jsx
@@ -0,0 +1,100 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import Autocomplete from 'react-autocomplete'
+
+
+const MAX_HEIGHT = 140;
+
+export default class InputAutocomplete extends React.Component {
+ static propTypes = {
+ value: PropTypes.string,
+ options: PropTypes.array,
+ onChange: PropTypes.func,
+ keepMenuWithinWindowBounds: PropTypes.bool,
+ 'aria-label': PropTypes.string,
+ }
+
+ state = {
+ maxHeight: MAX_HEIGHT
+ }
+
+ static defaultProps = {
+ onChange: () => {},
+ options: [],
+ }
+
+ calcMaxHeight() {
+ if(this.props.keepMenuWithinWindowBounds) {
+ const maxHeight = window.innerHeight - this.autocompleteMenuEl.getBoundingClientRect().top;
+ const limitedMaxHeight = Math.min(maxHeight, MAX_HEIGHT);
+
+ if(limitedMaxHeight != this.state.maxHeight) {
+ this.setState({
+ maxHeight: limitedMaxHeight
+ })
+ }
+ }
+ }
+
+ componentDidMount() {
+ this.calcMaxHeight();
+ }
+
+ componentDidUpdate() {
+ this.calcMaxHeight();
+ }
+
+ onChange (v) {
+ this.props.onChange(v === "" ? undefined : v);
+ }
+
+ render() {
+ return
{
+ this.autocompleteMenuEl = el;
+ }}
+ >
+
item[0]}
+ onSelect={v => this.onChange(v)}
+ onChange={(e, v) => this.onChange(v)}
+ shouldItemRender={(item, value="") => {
+ if (typeof(value) === "string") {
+ return item[0].toLowerCase().indexOf(value.toLowerCase()) > -1
+ }
+ }}
+ renderItem={(item, isHighlighted) => (
+
+ {item[1]}
+
+ )}
+ />
+
+ }
+}
+
+
diff --git a/src/components/Button.jsx b/src/components/InputButton.jsx
similarity index 93%
rename from src/components/Button.jsx
rename to src/components/InputButton.jsx
index cbddd5a..e9c4b23 100644
--- a/src/components/Button.jsx
+++ b/src/components/InputButton.jsx
@@ -2,7 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
-class Button extends React.Component {
+export default class InputButton extends React.Component {
static propTypes = {
"data-wd-key": PropTypes.string,
"aria-label": PropTypes.string,
@@ -33,4 +33,3 @@ class Button extends React.Component {
}
}
-export default Button
diff --git a/src/components/InputCheckbox.jsx b/src/components/InputCheckbox.jsx
new file mode 100644
index 0000000..e01d99c
--- /dev/null
+++ b/src/components/InputCheckbox.jsx
@@ -0,0 +1,34 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+
+export default class InputCheckbox extends React.Component {
+ static propTypes = {
+ value: PropTypes.bool,
+ style: PropTypes.object,
+ onChange: PropTypes.func,
+ }
+
+ static defaultProps = {
+ value: false,
+ }
+
+ render() {
+ return
+ }
+}
+
diff --git a/src/components/InputColor.jsx b/src/components/InputColor.jsx
new file mode 100644
index 0000000..02fb096
--- /dev/null
+++ b/src/components/InputColor.jsx
@@ -0,0 +1,135 @@
+import React from 'react'
+import Color from 'color'
+import ChromePicker from 'react-color/lib/components/chrome/Chrome'
+import PropTypes from 'prop-types'
+import lodash from 'lodash';
+
+function formatColor(color) {
+ const rgb = color.rgb
+ return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})`
+}
+
+/*** Number fields with support for min, max and units and documentation*/
+export default class InputColor extends React.Component {
+ static propTypes = {
+ onChange: PropTypes.func.isRequired,
+ name: PropTypes.string,
+ value: PropTypes.string,
+ doc: PropTypes.string,
+ style: PropTypes.object,
+ default: PropTypes.string,
+ 'aria-label': PropTypes.string,
+ }
+
+ state = {
+ pickerOpened: false
+ }
+
+ constructor () {
+ super();
+ this.onChangeNoCheck = lodash.throttle(this.onChangeNoCheck, 1000/30);
+ }
+
+ onChangeNoCheck (v) {
+ this.props.onChange(v);
+ }
+
+ //TODO: I much rather would do this with absolute positioning
+ //but I am too stupid to get it to work together with fixed position
+ //and scrollbars so I have to fallback to JavaScript
+ calcPickerOffset = () => {
+ const elem = this.colorInput
+ if(elem) {
+ const pos = elem.getBoundingClientRect()
+ return {
+ top: pos.top,
+ left: pos.left + 196,
+ }
+ } else {
+ return {
+ top: 160,
+ left: 555,
+ }
+ }
+ }
+
+ togglePicker = () => {
+ this.setState({ pickerOpened: !this.state.pickerOpened })
+ }
+
+ get color() {
+ // Catch invalid color.
+ try {
+ return Color(this.props.value).rgb()
+ }
+ catch(err) {
+ console.warn("Error parsing color: ", err);
+ return Color("rgb(255,255,255)");
+ }
+ }
+
+ onChange (v) {
+ this.props.onChange(v === "" ? undefined : v);
+ }
+
+ render() {
+ const offset = this.calcPickerOffset()
+ var currentColor = this.color.object()
+ currentColor = {
+ r: currentColor.r,
+ g: currentColor.g,
+ b: currentColor.b,
+ // Rename alpha -> a for ChromePicker
+ a: currentColor.alpha
+ }
+
+ const picker =
+
this.onChangeNoCheck(formatColor(c))}
+ />
+
+
+
+ var swatchStyle = {
+ backgroundColor: this.props.value
+ };
+
+ return
+ {this.state.pickerOpened && picker}
+
+
this.colorInput = input}
+ onClick={this.togglePicker}
+ style={this.props.style}
+ name={this.props.name}
+ placeholder={this.props.default}
+ value={this.props.value ? this.props.value : ""}
+ onChange={(e) => this.onChange(e.target.value)}
+ />
+
+ }
+}
+
diff --git a/src/components/InputDynamicArray.jsx b/src/components/InputDynamicArray.jsx
new file mode 100644
index 0000000..05d9a26
--- /dev/null
+++ b/src/components/InputDynamicArray.jsx
@@ -0,0 +1,138 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import InputString from './InputString'
+import InputNumber from './InputNumber'
+import InputButton from './InputButton'
+import {MdDelete} from 'react-icons/md'
+import FieldDocLabel from './FieldDocLabel'
+import InputEnum from './InputEnum'
+import capitalize from 'lodash.capitalize'
+import InputUrl from './InputUrl'
+
+
+export default class FieldDynamicArray extends React.Component {
+ static propTypes = {
+ value: PropTypes.array,
+ type: PropTypes.string,
+ default: PropTypes.array,
+ onChange: PropTypes.func,
+ style: PropTypes.object,
+ fieldSpec: PropTypes.object,
+ 'aria-label': PropTypes.string,
+ }
+
+ changeValue(idx, newValue) {
+ const values = this.values.slice(0)
+ values[idx] = newValue
+ this.props.onChange(values)
+ }
+
+ get values() {
+ return this.props.value || this.props.default || []
+ }
+
+ addValue = () => {
+ const values = this.values.slice(0)
+ if (this.props.type === 'number') {
+ values.push(0)
+ }
+ else if (this.props.type === 'url') {
+ values.push("");
+ }
+ else if (this.props.type === 'enum') {
+ const {fieldSpec} = this.props;
+ const defaultValue = Object.keys(fieldSpec.values)[0];
+ values.push(defaultValue);
+ } else {
+ values.push("")
+ }
+
+ this.props.onChange(values)
+ }
+
+ deleteValue(valueIdx) {
+ const values = this.values.slice(0)
+ values.splice(valueIdx, 1)
+
+ this.props.onChange(values)
+ }
+
+ render() {
+ const inputs = this.values.map((v, i) => {
+ const deleteValueBtn=
+ let input;
+ if(this.props.type === 'url') {
+ input =
+ }
+ else if (this.props.type === 'number') {
+ input =
+ }
+ else if (this.props.type === 'enum') {
+ const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)]);
+
+ input =
+ }
+ else {
+ input =
+ }
+
+ return
+
+ {deleteValueBtn}
+
+
+ {input}
+
+
+ })
+
+ return (
+
+ {inputs}
+
+ Add value
+
+
+ );
+ }
+}
+
+class DeleteValueInputButton extends React.Component {
+ static propTypes = {
+ onClick: PropTypes.func,
+ }
+
+ render() {
+ return
+ }
+ doc={"Remove array item."}
+ />
+
+ }
+}
+
diff --git a/src/components/InputEnum.jsx b/src/components/InputEnum.jsx
new file mode 100644
index 0000000..f066949
--- /dev/null
+++ b/src/components/InputEnum.jsx
@@ -0,0 +1,49 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import InputSelect from './InputSelect'
+import InputMultiInput from './InputMultiInput'
+
+
+function optionsLabelLength(options) {
+ let sum = 0;
+ options.forEach(([_, label]) => {
+ sum += label.length
+ })
+ return sum
+}
+
+
+export default class InputEnum extends React.Component {
+ static propTypes = {
+ "data-wd-key": PropTypes.string,
+ value: PropTypes.string,
+ style: PropTypes.object,
+ default: PropTypes.string,
+ name: PropTypes.string,
+ onChange: PropTypes.func,
+ options: PropTypes.array,
+ 'aria-label': PropTypes.string,
+ }
+
+ render() {
+ const {options, value, onChange, name} = this.props;
+
+ if(options.length <= 3 && optionsLabelLength(options) <= 20) {
+ return
+ } else {
+ return
+ }
+ }
+}
+
diff --git a/src/components/InputFont.jsx b/src/components/InputFont.jsx
new file mode 100644
index 0000000..9fc897b
--- /dev/null
+++ b/src/components/InputFont.jsx
@@ -0,0 +1,61 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import InputAutocomplete from './InputAutocomplete'
+
+export default class FieldFont extends React.Component {
+ static propTypes = {
+ value: PropTypes.array,
+ default: PropTypes.array,
+ fonts: PropTypes.array,
+ style: PropTypes.object,
+ onChange: PropTypes.func.isRequired,
+ 'aria-label': PropTypes.string,
+ }
+
+ static defaultProps = {
+ fonts: []
+ }
+
+ get values() {
+ const out = this.props.value || this.props.default || [];
+
+ // Always put a "" in the last field to you can keep adding entries
+ if (out[out.length-1] !== ""){
+ return out.concat("");
+ }
+ else {
+ return out;
+ }
+ }
+
+ changeFont(idx, newValue) {
+ const changedValues = this.values.slice(0)
+ changedValues[idx] = newValue
+ const filteredValues = changedValues
+ .filter(v => v !== undefined)
+ .filter(v => v !== "")
+
+ this.props.onChange(filteredValues);
+ }
+
+ render() {
+ const inputs = this.values.map((value, i) => {
+ return
+ [f, f])}
+ onChange={this.changeFont.bind(this, i)}
+ />
+
+ })
+
+ return (
+
+ );
+ }
+}
diff --git a/src/components/FieldJsonEditor.jsx b/src/components/InputJson.jsx
similarity index 98%
rename from src/components/FieldJsonEditor.jsx
rename to src/components/InputJson.jsx
index a32774c..54bac98 100644
--- a/src/components/FieldJsonEditor.jsx
+++ b/src/components/InputJson.jsx
@@ -16,7 +16,7 @@ import stringifyPretty from 'json-stringify-pretty-compact'
import '../util/codemirror-mgl';
-export default class FieldJsonEditor extends React.Component {
+export default class InputJson extends React.Component {
static propTypes = {
layer: PropTypes.any.isRequired,
maxHeight: PropTypes.number,
@@ -172,4 +172,3 @@ export default class FieldJsonEditor extends React.Component {
}
}
-
diff --git a/src/components/InputMultiInput.jsx b/src/components/InputMultiInput.jsx
new file mode 100644
index 0000000..a9d2aad
--- /dev/null
+++ b/src/components/InputMultiInput.jsx
@@ -0,0 +1,42 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+import InputButton from './InputButton'
+
+export default class InputMultiInput extends React.Component {
+ static propTypes = {
+ name: PropTypes.string.isRequired,
+ value: PropTypes.string.isRequired,
+ options: PropTypes.array.isRequired,
+ onChange: PropTypes.func.isRequired,
+ }
+
+ render() {
+ let options = this.props.options
+ if(options.length > 0 && !Array.isArray(options[0])) {
+ options = options.map(v => [v, v])
+ }
+
+ const selectedValue = this.props.value || options[0][0]
+ const radios = options.map(([val, label])=> {
+ return
+ })
+
+ return
+ }
+}
+
+
diff --git a/src/components/InputNumber.jsx b/src/components/InputNumber.jsx
new file mode 100644
index 0000000..d660daf
--- /dev/null
+++ b/src/components/InputNumber.jsx
@@ -0,0 +1,235 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+
+let IDX = 0;
+
+export default class InputNumber extends React.Component {
+ static propTypes = {
+ value: PropTypes.number,
+ default: PropTypes.number,
+ min: PropTypes.number,
+ max: PropTypes.number,
+ onChange: PropTypes.func,
+ allowRange: PropTypes.bool,
+ rangeStep: PropTypes.number,
+ wdKey: PropTypes.string,
+ required: PropTypes.bool,
+ "aria-label": PropTypes.string,
+ }
+
+ static defaultProps = {
+ rangeStep: 1
+ }
+
+ constructor(props) {
+ super(props)
+ this.state = {
+ uuid: IDX++,
+ editing: false,
+ value: props.value,
+ dirtyValue: props.value,
+ }
+ }
+
+ static getDerivedStateFromProps(props, state) {
+ if (!state.editing && props.value !== state.value) {
+ return {
+ value: props.value,
+ dirtyValue: props.value,
+ };
+ }
+ return null;
+ }
+
+ changeValue(newValue) {
+ const value = (newValue === "" || newValue === undefined) ?
+ undefined :
+ parseFloat(newValue);
+
+ const hasChanged = this.props.value !== value;
+ if(this.isValid(value) && hasChanged) {
+ this.props.onChange(value)
+ this.setState({
+ value: newValue,
+ });
+ }
+ else if (!this.isValid(value) && hasChanged) {
+ this.setState({
+ value: undefined,
+ });
+ }
+
+ this.setState({
+ dirtyValue: newValue === "" ? undefined : newValue,
+ })
+ }
+
+ isValid(v) {
+ if (v === undefined) {
+ return true;
+ }
+
+ const value = parseFloat(v)
+ if(isNaN(value)) {
+ return false
+ }
+
+ if(!isNaN(this.props.min) && value < this.props.min) {
+ return false
+ }
+
+ if(!isNaN(this.props.max) && value > this.props.max) {
+ return false
+ }
+
+ return true
+ }
+
+ resetValue = () => {
+ this.setState({editing: false});
+ // Reset explicitly to default value if value has been cleared
+ if(this.state.value === "") {
+ return;
+ }
+
+ // If set value is invalid fall back to the last valid value from props or at last resort the default value
+ if (!this.isValid(this.state.value)) {
+ if(this.isValid(this.props.value)) {
+ this.changeValue(this.props.value)
+ this.setState({dirtyValue: this.props.value});
+ } else {
+ this.changeValue(undefined);
+ this.setState({dirtyValue: undefined});
+ }
+ }
+ }
+
+ onChangeRange = (e) => {
+ let value = parseFloat(e.target.value, 10);
+ const step = this.props.rangeStep;
+ let dirtyValue = value;
+
+ if(step) {
+ // Can't do this with the
range step attribute else we won't be able to set a high precision value via the text input.
+ const snap = value % step;
+
+ // Round up/down to step
+ if (this._keyboardEvent) {
+ // If it's keyboard event we might get a low positive/negative value,
+ // for example we might go from 13 to 13.23, however because we know
+ // that came from a keyboard event we always want to increase by a
+ // single step value.
+ if (value < this.state.dirtyValue) {
+ value = this.state.value - step;
+ }
+ else {
+ value = this.state.value + step
+ }
+ dirtyValue = value;
+ }
+ else {
+ if (snap < step/2) {
+ value = value - snap;
+ }
+ else {
+ value = value + (step - snap);
+ };
+ }
+ }
+
+ this._keyboardEvent = false;
+
+ // Clamp between min/max
+ value = Math.max(this.props.min, Math.min(this.props.max, value));
+
+ this.setState({value, dirtyValue});
+ this.props.onChange(value);
+ }
+
+ render() {
+ if(
+ this.props.hasOwnProperty("min") && this.props.hasOwnProperty("max") &&
+ this.props.min !== undefined && this.props.max !== undefined &&
+ this.props.allowRange
+ ) {
+ const value = this.state.editing ? this.state.dirtyValue : this.state.value;
+ const defaultValue = this.props.default === undefined ? "" : this.props.default;
+ let inputValue;
+ if (this.state.editingRange) {
+ inputValue = this.state.value;
+ }
+ else {
+ inputValue = value;
+ }
+
+ return
+ {
+ this._keyboardEvent = true;
+ }}
+ onPointerDown={() => {
+ this.setState({editing: true, editingRange: true});
+ }}
+ onPointerUp={() => {
+ // Safari doesn't get onBlur event
+ this.setState({editing: false, editingRange: false});
+ }}
+ onBlur={() => {
+ this.setState({
+ editing: false,
+ editingRange: false,
+ dirtyValue: this.state.value,
+ });
+ }}
+ />
+ {
+ this.setState({editing: true});
+ }}
+ onChange={e => {
+ this.changeValue(e.target.value);
+ }}
+ onBlur={e => {
+ this.setState({editing: false});
+ this.resetValue()
+ }}
+ />
+
+ }
+ else {
+ const value = this.state.editing ? this.state.dirtyValue : this.state.value;
+
+ return
this.changeValue(e.target.value)}
+ onFocus={() => {
+ this.setState({editing: true});
+ }}
+ onBlur={this.resetValue}
+ required={this.props.required}
+ />
+ }
+ }
+}
+
+
diff --git a/src/components/InputSelect.jsx b/src/components/InputSelect.jsx
new file mode 100644
index 0000000..a138736
--- /dev/null
+++ b/src/components/InputSelect.jsx
@@ -0,0 +1,36 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+
+export default class InputSelect 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,
+ title: PropTypes.string,
+ 'aria-label': PropTypes.string,
+ }
+
+
+ render() {
+ let options = this.props.options
+ if(options.length > 0 && !Array.isArray(options[0])) {
+ options = options.map(v => [v, v])
+ }
+
+ return
+ }
+}
+
+
diff --git a/src/components/InputSpec.jsx b/src/components/InputSpec.jsx
new file mode 100644
index 0000000..1e470b2
--- /dev/null
+++ b/src/components/InputSpec.jsx
@@ -0,0 +1,139 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+
+import InputColor from './InputColor'
+import InputNumber from './InputNumber'
+import InputCheckbox from './InputCheckbox'
+import InputString from './InputString'
+import InputSelect from './InputSelect'
+import InputMultiInput from './InputMultiInput'
+import InputArray from './InputArray'
+import InputDynamicArray from './InputDynamicArray'
+import InputFont from './InputFont'
+import InputAutocomplete from './InputAutocomplete'
+import InputEnum from './InputEnum'
+import capitalize from 'lodash.capitalize'
+
+const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
+
+function labelFromFieldName(fieldName) {
+ let label = fieldName.split('-').slice(1).join(' ')
+ if(label.length > 0) {
+ label = label.charAt(0).toUpperCase() + label.slice(1);
+ }
+ return label
+}
+
+function optionsLabelLength(options) {
+ let sum = 0;
+ options.forEach(([_, label]) => {
+ sum += label.length
+ })
+ return sum
+}
+
+/** Display any field from the Mapbox GL style spec and
+ * choose the correct field component based on the @{fieldSpec}
+ * to display @{value}. */
+export default class SpecField extends React.Component {
+ static propTypes = {
+ onChange: PropTypes.func.isRequired,
+ fieldName: PropTypes.string.isRequired,
+ fieldSpec: PropTypes.object.isRequired,
+ value: PropTypes.oneOfType([
+ PropTypes.string,
+ PropTypes.number,
+ PropTypes.array,
+ PropTypes.bool
+ ]),
+ /** Override the style of the field */
+ style: PropTypes.object,
+ 'aria-label': PropTypes.string,
+ }
+
+ render() {
+ const commonProps = {
+ error: this.props.error,
+ fieldSpec: this.props.fieldSpec,
+ label: this.props.label,
+ action: this.props.action,
+ style: this.props.style,
+ value: this.props.value,
+ default: this.props.fieldSpec.default,
+ name: this.props.fieldName,
+ onChange: newValue => this.props.onChange(this.props.fieldName, newValue),
+ 'aria-label': this.props['aria-label'],
+ }
+
+ function childNodes() {
+ switch(this.props.fieldSpec.type) {
+ case 'number': return (
+
+ )
+ case 'enum':
+ const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)])
+
+ return
+ case 'resolvedImage':
+ case 'formatted':
+ case 'string':
+ if (iconProperties.indexOf(this.props.fieldName) >= 0) {
+ const options = this.props.fieldSpec.values || [];
+ return
[f, f])}
+ />
+ } else {
+ return
+ }
+ case 'color': return (
+
+ )
+ case 'boolean': return (
+
+ )
+ case 'array':
+ if(this.props.fieldName === 'text-font') {
+ return
+ } else {
+ if (this.props.fieldSpec.length) {
+ return
+ } else {
+ return
+ }
+ }
+ default: return null
+ }
+ }
+
+ return (
+
+ {childNodes.call(this)}
+
+ );
+ }
+}
diff --git a/src/components/InputString.jsx b/src/components/InputString.jsx
new file mode 100644
index 0000000..3efd93d
--- /dev/null
+++ b/src/components/InputString.jsx
@@ -0,0 +1,95 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+
+export default class InputString extends React.Component {
+ static propTypes = {
+ "data-wd-key": PropTypes.string,
+ value: PropTypes.string,
+ style: PropTypes.object,
+ default: PropTypes.string,
+ onChange: PropTypes.func,
+ onInput: PropTypes.func,
+ multi: PropTypes.bool,
+ required: PropTypes.bool,
+ disabled: PropTypes.bool,
+ spellCheck: PropTypes.bool,
+ 'aria-label': PropTypes.string,
+ }
+
+ static defaultProps = {
+ onInput: () => {},
+ }
+
+ constructor(props) {
+ super(props)
+ this.state = {
+ editing: false,
+ value: props.value || ''
+ }
+ }
+
+ static getDerivedStateFromProps(props, state) {
+ if (!state.editing) {
+ return {
+ value: props.value
+ };
+ }
+ return {};
+ }
+
+ render() {
+ let tag;
+ let classes;
+
+ if(!!this.props.multi) {
+ tag = "textarea"
+ classes = [
+ "maputnik-string",
+ "maputnik-string--multi"
+ ]
+ }
+ else {
+ tag = "input"
+ classes = [
+ "maputnik-string"
+ ]
+ }
+
+ if(!!this.props.disabled) {
+ classes.push("maputnik-string--disabled");
+ }
+
+ return React.createElement(tag, {
+ "aria-label": this.props["aria-label"],
+ "data-wd-key": this.props["data-wd-key"],
+ spellCheck: this.props.hasOwnProperty("spellCheck") ? this.props.spellCheck : !(tag === "input"),
+ disabled: this.props.disabled,
+ className: classes.join(" "),
+ style: this.props.style,
+ value: this.state.value === undefined ? "" : this.state.value,
+ placeholder: this.props.default,
+ onChange: e => {
+ this.setState({
+ editing: true,
+ value: e.target.value
+ }, () => {
+ this.props.onInput(this.state.value);
+ });
+ },
+ onBlur: () => {
+ if(this.state.value!==this.props.value) {
+ this.setState({editing: false});
+ this.props.onChange(this.state.value);
+ }
+ },
+ onKeyDown: (e) => {
+ if (e.keyCode === 13) {
+ this.props.onChange(this.state.value);
+ }
+ },
+ required: this.props.required,
+ });
+ }
+}
+
+
diff --git a/src/components/InputUrl.jsx b/src/components/InputUrl.jsx
new file mode 100644
index 0000000..dd62caa
--- /dev/null
+++ b/src/components/InputUrl.jsx
@@ -0,0 +1,103 @@
+import React, {Fragment} from 'react'
+import PropTypes from 'prop-types'
+import InputString from './InputString'
+import SmallError from './SmallError'
+
+
+function validate (url) {
+ if (url === "") {
+ return;
+ }
+
+ let error;
+ const getProtocol = (url) => {
+ try {
+ const urlObj = new URL(url);
+ return urlObj.protocol;
+ }
+ catch (err) {
+ return undefined;
+ }
+ };
+ const protocol = getProtocol(url);
+ const isSsl = window.location.protocol === "https:";
+
+ if (!protocol) {
+ error = (
+
+ Must provide protocol {
+ isSsl
+ ? https://
+ : <>http://
or https://
>
+ }
+
+ );
+ }
+ else if (
+ protocol &&
+ protocol === "http:" &&
+ window.location.protocol === "https:"
+ ) {
+ error = (
+
+ CORS policy won't allow fetching resources served over http from https, use a https://
domain
+
+ );
+ }
+
+ return error;
+}
+
+export default class FieldUrl extends React.Component {
+ static propTypes = {
+ "data-wd-key": PropTypes.string,
+ value: PropTypes.string,
+ style: PropTypes.object,
+ default: PropTypes.string,
+ onChange: PropTypes.func,
+ onInput: PropTypes.func,
+ multi: PropTypes.bool,
+ required: PropTypes.bool,
+ 'aria-label': PropTypes.string,
+ }
+
+ static defaultProps = {
+ onInput: () => {},
+ }
+
+ constructor (props) {
+ super(props);
+ this.state = {
+ error: validate(props.value)
+ };
+ }
+
+ onInput = (url) => {
+ this.setState({
+ error: validate(url)
+ });
+ this.props.onInput(url);
+ }
+
+ onChange = (url) => {
+ this.setState({
+ error: validate(url)
+ });
+ this.props.onChange(url);
+ }
+
+ render () {
+ return (
+
+
+ {this.state.error}
+
+ );
+ }
+}
+
diff --git a/src/components/LayerEditor.jsx b/src/components/LayerEditor.jsx
index 5868fd5..1479ffc 100644
--- a/src/components/LayerEditor.jsx
+++ b/src/components/LayerEditor.jsx
@@ -2,17 +2,17 @@ import React from 'react'
import PropTypes from 'prop-types'
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
-import FieldJsonEditor from './FieldJsonEditor'
+import FieldJson from './FieldJson'
import FilterEditor from './FilterEditor'
import PropertyGroup from './PropertyGroup'
import LayerEditorGroup from './LayerEditorGroup'
-import BlockType from './BlockType'
-import BlockId from './BlockId'
-import BlockMinZoom from './BlockMinZoom'
-import BlockMaxZoom from './BlockMaxZoom'
-import BlockComment from './BlockComment'
-import BlockSource from './BlockSource'
-import BlockSourceLayer from './BlockSourceLayer'
+import FieldType from './FieldType'
+import FieldId from './FieldId'
+import FieldMinZoom from './FieldMinZoom'
+import FieldMaxZoom from './FieldMaxZoom'
+import FieldComment from './FieldComment'
+import FieldSource from './FieldSource'
+import FieldSourceLayer from './FieldSourceLayer'
import {Accordion} from 'react-accessible-accordion';
import {MdMoreVert} from 'react-icons/md'
@@ -152,13 +152,13 @@ export default class LayerEditor extends React.Component {
switch(type) {
case 'layer': return
- this.props.onLayerIdChange(this.props.layerIndex, this.props.layer.id, newId)}
/>
-
- {this.props.layer.type !== 'background' &&
}
{['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)}
@@ -208,22 +208,24 @@ export default class LayerEditor extends React.Component {
/>
- case 'properties': return