mirror of
https://github.com/a-nyx/maputnik-with-pmtiles.git
synced 2024-12-26 05:30:36 +01:00
Fixed more input accessibility issues, also
- Added searchParams based router for easier testing - Added more stories to the storybook
This commit is contained in:
parent
d6f31ec82e
commit
2cc179acc1
127 changed files with 3858 additions and 1832 deletions
|
@ -16,7 +16,6 @@ module.exports = {
|
||||||
...config,
|
...config,
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
...config.module.rules,
|
|
||||||
...rules,
|
...rules,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
5
package-lock.json
generated
5
package-lock.json
generated
|
@ -17124,6 +17124,11 @@
|
||||||
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
|
"integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"string-hash": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz",
|
||||||
|
"integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs="
|
||||||
|
},
|
||||||
"string-width": {
|
"string-width": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
|
||||||
|
|
|
@ -66,6 +66,7 @@
|
||||||
"react-sortable-hoc": "^1.11.0",
|
"react-sortable-hoc": "^1.11.0",
|
||||||
"reconnecting-websocket": "^4.4.0",
|
"reconnecting-websocket": "^4.4.0",
|
||||||
"slugify": "^1.3.6",
|
"slugify": "^1.3.6",
|
||||||
|
"string-hash": "^1.1.3",
|
||||||
"url": "^0.11.0"
|
"url": "^0.11.0"
|
||||||
},
|
},
|
||||||
"jshintConfig": {
|
"jshintConfig": {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import get from 'lodash.get'
|
||||||
import {unset} from 'lodash'
|
import {unset} from 'lodash'
|
||||||
import arrayMove from 'array-move'
|
import arrayMove from 'array-move'
|
||||||
import url from 'url'
|
import url from 'url'
|
||||||
|
import hash from "string-hash";
|
||||||
|
|
||||||
import MapMapboxGl from './MapMapboxGl'
|
import MapMapboxGl from './MapMapboxGl'
|
||||||
import MapOpenLayers from './MapOpenLayers'
|
import MapOpenLayers from './MapOpenLayers'
|
||||||
|
@ -189,7 +190,7 @@ export default class App extends React.Component {
|
||||||
console.log('Falling back to local storage for storing styles')
|
console.log('Falling back to local storage for storing styles')
|
||||||
this.styleStore = new StyleStore()
|
this.styleStore = new StyleStore()
|
||||||
}
|
}
|
||||||
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle))
|
this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle, {initialLoad: true}))
|
||||||
|
|
||||||
if(Debug.enabled()) {
|
if(Debug.enabled()) {
|
||||||
Debug.set("maputnik", "styleStore", this.styleStore);
|
Debug.set("maputnik", "styleStore", this.styleStore);
|
||||||
|
@ -322,9 +323,14 @@ export default class App extends React.Component {
|
||||||
opts = {
|
opts = {
|
||||||
save: true,
|
save: true,
|
||||||
addRevision: true,
|
addRevision: true,
|
||||||
|
initialLoad: false,
|
||||||
...opts,
|
...opts,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (opts.initialLoad) {
|
||||||
|
this.getInitialStateFromUrl(newStyle);
|
||||||
|
}
|
||||||
|
|
||||||
const errors = validate(newStyle, latest) || [];
|
const errors = validate(newStyle, latest) || [];
|
||||||
|
|
||||||
// The validate function doesn't give us errors for duplicate error with
|
// The validate function doesn't give us errors for duplicate error with
|
||||||
|
@ -442,6 +448,7 @@ export default class App extends React.Component {
|
||||||
errors: mappedErrors,
|
errors: mappedErrors,
|
||||||
}, () => {
|
}, () => {
|
||||||
this.fetchSources();
|
this.fetchSources();
|
||||||
|
this.setStateInUrl();
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -542,7 +549,7 @@ export default class App extends React.Component {
|
||||||
setMapState = (newState) => {
|
setMapState = (newState) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
mapState: newState
|
mapState: newState
|
||||||
})
|
}, this.setStateInUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
setDefaultValues = (styleObj) => {
|
setDefaultValues = (styleObj) => {
|
||||||
|
@ -697,8 +704,85 @@ export default class App extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setStateInUrl = () => {
|
||||||
|
const {mapState, mapStyle, isOpen} = this.state;
|
||||||
|
const {selectedLayerIndex} = this.state;
|
||||||
|
const url = new URL(location.href);
|
||||||
|
const hashVal = hash(JSON.stringify(mapStyle));
|
||||||
|
url.searchParams.set("layer", `${hashVal}~${selectedLayerIndex}`);
|
||||||
|
|
||||||
|
const openModals = Object.entries(isOpen)
|
||||||
|
.map(([key, val]) => (val === true ? key : null))
|
||||||
|
.filter(val => val !== null);
|
||||||
|
|
||||||
|
if (openModals.length > 0) {
|
||||||
|
url.searchParams.set("modal", openModals.join(","));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
url.searchParams.delete("modal");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapState === "map") {
|
||||||
|
url.searchParams.delete("view");
|
||||||
|
}
|
||||||
|
else if (mapState === "inspect") {
|
||||||
|
url.searchParams.set("view", "inspect");
|
||||||
|
}
|
||||||
|
|
||||||
|
history.replaceState({selectedLayerIndex}, "Maputnik", url.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
getInitialStateFromUrl = (mapStyle) => {
|
||||||
|
const url = new URL(location.href);
|
||||||
|
const modalParam = url.searchParams.get("modal");
|
||||||
|
if (modalParam && modalParam !== "") {
|
||||||
|
const modals = modalParam.split(",");
|
||||||
|
const modalObj = {};
|
||||||
|
modals.forEach(modalName => {
|
||||||
|
modalObj[modalName] = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
isOpen: {
|
||||||
|
...this.state.isOpen,
|
||||||
|
...modalObj,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = url.searchParams.get("view");
|
||||||
|
if (view && view !== "") {
|
||||||
|
this.setMapState(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = url.searchParams.get("layer");
|
||||||
|
if (path) {
|
||||||
|
try {
|
||||||
|
const parts = path.split("~");
|
||||||
|
const [hashVal, selectedLayerIndex] = [
|
||||||
|
parts[0],
|
||||||
|
parseInt(parts[1], 10),
|
||||||
|
];
|
||||||
|
|
||||||
|
let invalid = false;
|
||||||
|
if (hashVal !== "-") {
|
||||||
|
const currentHashVal = hash(JSON.stringify(mapStyle));
|
||||||
|
if (currentHashVal !== parseInt(hashVal, 10)) {
|
||||||
|
invalid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!invalid) {
|
||||||
|
this.setState({selectedLayerIndex});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.warn(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onLayerSelect = (index) => {
|
onLayerSelect = (index) => {
|
||||||
this.setState({ selectedLayerIndex: index })
|
this.setState({ selectedLayerIndex: index }, this.setStateInUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
setModal(modalName, value) {
|
setModal(modalName, value) {
|
||||||
|
@ -711,7 +795,7 @@ export default class App extends React.Component {
|
||||||
...this.state.isOpen,
|
...this.state.isOpen,
|
||||||
[modalName]: value
|
[modalName]: value
|
||||||
}
|
}
|
||||||
})
|
}, this.setStateInUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleModal(modalName) {
|
toggleModal(modalName) {
|
||||||
|
|
|
@ -145,31 +145,37 @@ export default class AppToolbar extends React.Component {
|
||||||
const views = [
|
const views = [
|
||||||
{
|
{
|
||||||
id: "map",
|
id: "map",
|
||||||
|
group: "general",
|
||||||
title: "Map",
|
title: "Map",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "inspect",
|
id: "inspect",
|
||||||
|
group: "general",
|
||||||
title: "Inspect",
|
title: "Inspect",
|
||||||
disabled: this.props.renderer !== 'mbgljs',
|
disabled: this.props.renderer !== 'mbgljs',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "filter-deuteranopia",
|
id: "filter-deuteranopia",
|
||||||
title: "Deuteranopia color filter",
|
group: "color-accessibility",
|
||||||
|
title: "Deuteranopia filter",
|
||||||
disabled: !colorAccessibilityFiltersEnabled,
|
disabled: !colorAccessibilityFiltersEnabled,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "filter-protanopia",
|
id: "filter-protanopia",
|
||||||
title: "Protanopia color filter",
|
group: "color-accessibility",
|
||||||
|
title: "Protanopia filter",
|
||||||
disabled: !colorAccessibilityFiltersEnabled,
|
disabled: !colorAccessibilityFiltersEnabled,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "filter-tritanopia",
|
id: "filter-tritanopia",
|
||||||
title: "Tritanopia color filter",
|
group: "color-accessibility",
|
||||||
|
title: "Tritanopia filter",
|
||||||
disabled: !colorAccessibilityFiltersEnabled,
|
disabled: !colorAccessibilityFiltersEnabled,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "filter-achromatopsia",
|
id: "filter-achromatopsia",
|
||||||
title: "Achromatopsia color filter",
|
group: "color-accessibility",
|
||||||
|
title: "Achromatopsia filter",
|
||||||
disabled: !colorAccessibilityFiltersEnabled,
|
disabled: !colorAccessibilityFiltersEnabled,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -242,13 +248,22 @@ export default class AppToolbar extends React.Component {
|
||||||
onChange={(e) => this.handleSelection(e.target.value)}
|
onChange={(e) => this.handleSelection(e.target.value)}
|
||||||
value={currentView.id}
|
value={currentView.id}
|
||||||
>
|
>
|
||||||
{views.map((item) => {
|
{views.filter(v => v.group === "general").map((item) => {
|
||||||
return (
|
return (
|
||||||
<option key={item.id} value={item.id} disabled={item.disabled}>
|
<option key={item.id} value={item.id} disabled={item.disabled}>
|
||||||
{item.title}
|
{item.title}
|
||||||
</option>
|
</option>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
<optgroup label="Color accessibility">
|
||||||
|
{views.filter(v => v.group === "color-accessibility").map((item) => {
|
||||||
|
return (
|
||||||
|
<option key={item.id} value={item.id} disabled={item.disabled}>
|
||||||
|
{item.title}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</ToolbarSelect>
|
</ToolbarSelect>
|
||||||
|
|
|
@ -50,44 +50,15 @@ export default class Block extends React.Component {
|
||||||
"maputnik-input-block--wide": this.props.wideMode,
|
"maputnik-input-block--wide": this.props.wideMode,
|
||||||
"maputnik-action-block": this.props.action
|
"maputnik-action-block": this.props.action
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{this.props.fieldSpec &&
|
<label>
|
||||||
<div className="maputnik-input-block-label">
|
<div className="maputnik-input-block-label">
|
||||||
<FieldDocLabel
|
{this.props.label}
|
||||||
label={this.props.label}
|
|
||||||
onToggleDoc={this.onToggleDoc}
|
|
||||||
fieldSpec={this.props.fieldSpec}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{!this.props.fieldSpec &&
|
|
||||||
<label className="maputnik-input-block-label">
|
|
||||||
{this.props.label}
|
|
||||||
</label>
|
|
||||||
}
|
|
||||||
{this.props.action &&
|
|
||||||
<div className="maputnik-input-block-action">
|
|
||||||
{this.props.action}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div className="maputnik-input-block-content">
|
|
||||||
{this.props.children}
|
|
||||||
</div>
|
|
||||||
{errors.length > 0 &&
|
|
||||||
<div className="maputnik-inline-error">
|
|
||||||
{[].concat(this.props.error).map((error, idx) => {
|
|
||||||
return <div key={idx}>{error.message}</div>
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
<div className="maputnik-input-block-content">
|
||||||
{this.props.fieldSpec &&
|
{this.props.children}
|
||||||
<div
|
</div>
|
||||||
className="maputnik-doc-inline"
|
</label>
|
||||||
style={{display: this.state.showDoc ? '' : 'none'}}
|
|
||||||
>
|
|
||||||
<Doc fieldSpec={this.props.fieldSpec} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,108 +1,21 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import FieldString from './FieldString'
|
import Block from './Block'
|
||||||
import FieldNumber from './FieldNumber'
|
import InputArray from './InputArray'
|
||||||
|
import Fieldset from './Fieldset'
|
||||||
|
|
||||||
export default class FieldArray extends React.Component {
|
export default class FieldArray extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.array,
|
...InputArray.propTypes,
|
||||||
type: PropTypes.string,
|
name: PropTypes.string,
|
||||||
length: PropTypes.number,
|
|
||||||
default: PropTypes.array,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
render() {
|
||||||
const {value} = this.state;
|
const {props} = this;
|
||||||
|
|
||||||
const containsValues = (
|
return <Fieldset label={props.label}>
|
||||||
value.length > 0 &&
|
<InputArray {...props} />
|
||||||
!value.every(val => {
|
</Fieldset>
|
||||||
return (val === "" || val === undefined)
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const inputs = Array(this.props.length).fill(null).map((_, i) => {
|
|
||||||
if(this.props.type === 'number') {
|
|
||||||
return <FieldNumber
|
|
||||||
key={i}
|
|
||||||
default={containsValues ? undefined : this.props.default[i]}
|
|
||||||
value={value[i]}
|
|
||||||
required={containsValues ? true : false}
|
|
||||||
onChange={this.changeValue.bind(this, i)}
|
|
||||||
/>
|
|
||||||
} else {
|
|
||||||
return <FieldString
|
|
||||||
key={i}
|
|
||||||
default={containsValues ? undefined : this.props.default[i]}
|
|
||||||
value={value[i]}
|
|
||||||
required={containsValues ? true : false}
|
|
||||||
onChange={this.changeValue.bind(this, i)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return <div className="maputnik-array">
|
|
||||||
{inputs}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,97 +1,20 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import classnames from 'classnames'
|
import Block from './Block'
|
||||||
import Autocomplete from 'react-autocomplete'
|
import InputAutocomplete from './InputAutocomplete'
|
||||||
|
|
||||||
|
|
||||||
const MAX_HEIGHT = 140;
|
|
||||||
|
|
||||||
export default class FieldAutocomplete extends React.Component {
|
export default class FieldAutocomplete extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.string,
|
...InputAutocomplete.propTypes,
|
||||||
options: PropTypes.array,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
keepMenuWithinWindowBounds: PropTypes.bool
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
render() {
|
||||||
return <div
|
const {props} = this;
|
||||||
ref={(el) => {
|
|
||||||
this.autocompleteMenuEl = el;
|
return <Block label={props.label}>
|
||||||
}}
|
<InputAutocomplete {...props} />
|
||||||
>
|
</Block>
|
||||||
<Autocomplete
|
|
||||||
menuStyle={{
|
|
||||||
position: "fixed",
|
|
||||||
overflow: "auto",
|
|
||||||
maxHeight: this.state.maxHeight,
|
|
||||||
zIndex: '998'
|
|
||||||
}}
|
|
||||||
wrapperProps={{
|
|
||||||
className: "maputnik-autocomplete",
|
|
||||||
style: null
|
|
||||||
}}
|
|
||||||
inputProps={{
|
|
||||||
className: "maputnik-string",
|
|
||||||
spellCheck: false
|
|
||||||
}}
|
|
||||||
value={this.props.value}
|
|
||||||
items={this.props.options}
|
|
||||||
getItemValue={(item) => 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) => (
|
|
||||||
<div
|
|
||||||
key={item[0]}
|
|
||||||
className={classnames({
|
|
||||||
"maputnik-autocomplete-menu-item": true,
|
|
||||||
"maputnik-autocomplete-menu-item-selected": isHighlighted,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{item[1]}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,34 +1,20 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputCheckbox from './InputCheckbox'
|
||||||
|
|
||||||
|
|
||||||
export default class FieldCheckbox extends React.Component {
|
export default class FieldCheckbox extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.bool,
|
...InputCheckbox.propTypes,
|
||||||
style: PropTypes.object,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
value: false,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <label className="maputnik-checkbox-wrapper">
|
const {props} = this;
|
||||||
<input
|
|
||||||
className="maputnik-checkbox"
|
return <Block label={this.props.label}>
|
||||||
type="checkbox"
|
<InputCheckbox {...props} />
|
||||||
style={this.props.style}
|
</Block>
|
||||||
onChange={e => this.props.onChange(!this.props.value)}
|
|
||||||
checked={this.props.value}
|
|
||||||
/>
|
|
||||||
<div className="maputnik-checkbox-box">
|
|
||||||
<svg style={{
|
|
||||||
display: this.props.value ? 'inline' : 'none'
|
|
||||||
}} className="maputnik-checkbox-icon" viewBox='0 0 32 32'>
|
|
||||||
<path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,132 +1,20 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Color from 'color'
|
|
||||||
import ChromePicker from 'react-color/lib/components/chrome/Chrome'
|
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import lodash from 'lodash';
|
import Block from './Block'
|
||||||
|
import InputColor from './InputColor'
|
||||||
|
|
||||||
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 FieldColor extends React.Component {
|
export default class FieldColor extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onChange: PropTypes.func.isRequired,
|
...InputColor.propTypes,
|
||||||
name: PropTypes.string,
|
|
||||||
value: PropTypes.string,
|
|
||||||
doc: PropTypes.string,
|
|
||||||
style: PropTypes.object,
|
|
||||||
default: 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() {
|
render() {
|
||||||
const offset = this.calcPickerOffset()
|
const {props} = this;
|
||||||
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 = <div
|
return <Block label={props.label}>
|
||||||
className="maputnik-color-picker-offset"
|
<InputColor {...props} />
|
||||||
style={{
|
</Block>
|
||||||
position: 'fixed',
|
|
||||||
zIndex: 1,
|
|
||||||
left: offset.left,
|
|
||||||
top: offset.top,
|
|
||||||
}}>
|
|
||||||
<ChromePicker
|
|
||||||
color={currentColor}
|
|
||||||
onChange={c => this.onChangeNoCheck(formatColor(c))}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="maputnik-color-picker-offset"
|
|
||||||
onClick={this.togglePicker}
|
|
||||||
style={{
|
|
||||||
zIndex: -1,
|
|
||||||
position: 'fixed',
|
|
||||||
top: '0px',
|
|
||||||
right: '0px',
|
|
||||||
bottom: '0px',
|
|
||||||
left: '0px',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
var swatchStyle = {
|
|
||||||
backgroundColor: this.props.value
|
|
||||||
};
|
|
||||||
|
|
||||||
return <div className="maputnik-color-wrapper">
|
|
||||||
{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}
|
|
||||||
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)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,9 @@ import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import FieldString from './FieldString'
|
import InputString from './InputString'
|
||||||
|
|
||||||
export default class BlockComment extends React.Component {
|
export default class FieldComment extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
@ -20,7 +20,7 @@ export default class BlockComment extends React.Component {
|
||||||
fieldSpec={fieldSpec}
|
fieldSpec={fieldSpec}
|
||||||
data-wd-key="layer-comment"
|
data-wd-key="layer-comment"
|
||||||
>
|
>
|
||||||
<FieldString
|
<InputString
|
||||||
multi={true}
|
multi={true}
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
|
@ -29,4 +29,3 @@ export default class BlockComment extends React.Component {
|
||||||
</Block>
|
</Block>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,136 +1,21 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import FieldString from './FieldString'
|
import Block from './Block'
|
||||||
import FieldNumber from './FieldNumber'
|
import InputDynamicArray from './InputDynamicArray'
|
||||||
import Button from './Button'
|
import Fieldset from './Fieldset'
|
||||||
import {MdDelete} from 'react-icons/md'
|
|
||||||
import FieldDocLabel from './FieldDocLabel'
|
|
||||||
import FieldEnum from './FieldEnum'
|
|
||||||
import capitalize from 'lodash.capitalize'
|
|
||||||
import FieldUrl from './FieldUrl'
|
|
||||||
|
|
||||||
|
|
||||||
export default class FieldDynamicArray extends React.Component {
|
export default class FieldDynamicArray extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.array,
|
...InputDynamicArray.propTypes,
|
||||||
type: PropTypes.string,
|
name: PropTypes.string,
|
||||||
default: PropTypes.array,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
style: PropTypes.object,
|
|
||||||
fieldSpec: PropTypes.object,
|
|
||||||
}
|
|
||||||
|
|
||||||
changeValue(idx, newValue) {
|
|
||||||
console.log(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() {
|
render() {
|
||||||
const inputs = this.values.map((v, i) => {
|
const {props} = this;
|
||||||
const deleteValueBtn= <DeleteValueButton onClick={this.deleteValue.bind(this, i)} />
|
|
||||||
let input;
|
|
||||||
if(this.props.type === 'url') {
|
|
||||||
input = <FieldUrl
|
|
||||||
value={v}
|
|
||||||
onChange={this.changeValue.bind(this, i)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
else if (this.props.type === 'number') {
|
|
||||||
input = <FieldNumber
|
|
||||||
value={v}
|
|
||||||
onChange={this.changeValue.bind(this, i)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
else if (this.props.type === 'enum') {
|
|
||||||
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)]);
|
|
||||||
|
|
||||||
input = <FieldEnum
|
return <Fieldset label={props.label}>
|
||||||
options={options}
|
<InputDynamicArray {...props} />
|
||||||
value={v}
|
</Fieldset>
|
||||||
onChange={this.changeValue.bind(this, i)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
input = <FieldString
|
|
||||||
value={v}
|
|
||||||
onChange={this.changeValue.bind(this, i)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div
|
|
||||||
style={this.props.style}
|
|
||||||
key={i}
|
|
||||||
className="maputnik-array-block"
|
|
||||||
>
|
|
||||||
<div className="maputnik-array-block-action">
|
|
||||||
{deleteValueBtn}
|
|
||||||
</div>
|
|
||||||
<div className="maputnik-array-block-content">
|
|
||||||
{input}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
|
|
||||||
return <div className="maputnik-array">
|
|
||||||
{inputs}
|
|
||||||
<Button
|
|
||||||
className="maputnik-array-add-value"
|
|
||||||
onClick={this.addValue}
|
|
||||||
>
|
|
||||||
Add value
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DeleteValueButton extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return <Button
|
|
||||||
className="maputnik-delete-stop"
|
|
||||||
onClick={this.props.onClick}
|
|
||||||
title="Remove array item"
|
|
||||||
>
|
|
||||||
<FieldDocLabel
|
|
||||||
label={<MdDelete />}
|
|
||||||
doc={"Remove array item."}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,45 +1,20 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import FieldSelect from './FieldSelect'
|
import InputEnum from './InputEnum'
|
||||||
import FieldMultiInput from './FieldMultiInput'
|
import Block from './Block';
|
||||||
|
import Fieldset from './Fieldset';
|
||||||
|
|
||||||
function optionsLabelLength(options) {
|
|
||||||
let sum = 0;
|
|
||||||
options.forEach(([_, label]) => {
|
|
||||||
sum += label.length
|
|
||||||
})
|
|
||||||
return sum
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default class FieldEnum extends React.Component {
|
export default class FieldEnum extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
"data-wd-key": PropTypes.string,
|
...InputEnum.propTypes,
|
||||||
value: PropTypes.string,
|
|
||||||
style: PropTypes.object,
|
|
||||||
default: PropTypes.string,
|
|
||||||
name: PropTypes.string,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
options: PropTypes.array,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {options, value, onChange, name} = this.props;
|
const {props} = this;
|
||||||
|
|
||||||
if(options.length <= 3 && optionsLabelLength(options) <= 20) {
|
return <Fieldset label={props.label}>
|
||||||
return <FieldMultiInput
|
<InputEnum {...props} />
|
||||||
name={name}
|
</Fieldset>
|
||||||
options={options}
|
|
||||||
value={value || this.props.default}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
} else {
|
|
||||||
return <FieldSelect
|
|
||||||
options={options}
|
|
||||||
value={value || this.props.default}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -315,6 +315,7 @@ export default class FieldFunction extends React.Component {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else if (dataType === "data_function") {
|
else if (dataType === "data_function") {
|
||||||
|
// TODO: Rename to FieldFunction **this file** shouldn't be called that
|
||||||
specField = (
|
specField = (
|
||||||
<DataProperty
|
<DataProperty
|
||||||
errors={this.props.errors}
|
errors={this.props.errors}
|
||||||
|
|
|
@ -3,9 +3,9 @@ import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import FieldString from './FieldString'
|
import InputString from './InputString'
|
||||||
|
|
||||||
export default class BlockId extends React.Component {
|
export default class FieldId extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
wdKey: PropTypes.string.isRequired,
|
wdKey: PropTypes.string.isRequired,
|
||||||
|
@ -18,11 +18,10 @@ export default class BlockId extends React.Component {
|
||||||
data-wd-key={this.props.wdKey}
|
data-wd-key={this.props.wdKey}
|
||||||
error={this.props.error}
|
error={this.props.error}
|
||||||
>
|
>
|
||||||
<FieldString
|
<InputString
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
/>
|
/>
|
||||||
</Block>
|
</Block>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
16
src/components/FieldJson.jsx
Normal file
16
src/components/FieldJson.jsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import InputJson from './InputJson'
|
||||||
|
|
||||||
|
|
||||||
|
export default class FieldJson extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
...InputJson.propTypes,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {props} = this;
|
||||||
|
return <InputJson {...props} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,9 +3,9 @@ import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import FieldNumber from './FieldNumber'
|
import InputNumber from './InputNumber'
|
||||||
|
|
||||||
export default class BlockMaxZoom extends React.Component {
|
export default class FieldMaxZoom extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.number,
|
value: PropTypes.number,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
@ -17,7 +17,7 @@ export default class BlockMaxZoom extends React.Component {
|
||||||
error={this.props.error}
|
error={this.props.error}
|
||||||
data-wd-key="max-zoom"
|
data-wd-key="max-zoom"
|
||||||
>
|
>
|
||||||
<FieldNumber
|
<InputNumber
|
||||||
allowRange={true}
|
allowRange={true}
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
|
@ -28,4 +28,3 @@ export default class BlockMaxZoom extends React.Component {
|
||||||
</Block>
|
</Block>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,9 @@ import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import FieldNumber from './FieldNumber'
|
import InputNumber from './InputNumber'
|
||||||
|
|
||||||
export default class BlockMinZoom extends React.Component {
|
export default class FieldMinZoom extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.number,
|
value: PropTypes.number,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
|
@ -17,7 +17,7 @@ export default class BlockMinZoom extends React.Component {
|
||||||
error={this.props.error}
|
error={this.props.error}
|
||||||
data-wd-key="min-zoom"
|
data-wd-key="min-zoom"
|
||||||
>
|
>
|
||||||
<FieldNumber
|
<InputNumber
|
||||||
allowRange={true}
|
allowRange={true}
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
|
@ -28,4 +28,3 @@ export default class BlockMinZoom extends React.Component {
|
||||||
</Block>
|
</Block>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,41 +1,21 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import classnames from 'classnames'
|
import Block from './Block'
|
||||||
import Button from './Button'
|
import InputMultiInput from './InputMultiInput'
|
||||||
|
import Fieldset from './Fieldset'
|
||||||
|
|
||||||
|
|
||||||
export default class FieldMultiInput extends React.Component {
|
export default class FieldMultiInput extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
...InputMultiInput.propTypes,
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
options: PropTypes.array.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let options = this.props.options
|
const {props} = this;
|
||||||
if(options.length > 0 && !Array.isArray(options[0])) {
|
|
||||||
options = options.map(v => [v, v])
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedValue = this.props.value || options[0][0]
|
return <Fieldset label={props.label}>
|
||||||
const radios = options.map(([val, label])=> {
|
<InputMultiInput {...props} />
|
||||||
return <label
|
</Fieldset>
|
||||||
key={val}
|
|
||||||
className={classnames("maputnik-radio-as-button", {"maputnik-button-selected": val === selectedValue})}
|
|
||||||
>
|
|
||||||
<input type="radio"
|
|
||||||
name={this.props.name}
|
|
||||||
onChange={e => this.props.onChange(val)}
|
|
||||||
value={val}
|
|
||||||
checked={val === selectedValue}
|
|
||||||
/>
|
|
||||||
{label}
|
|
||||||
</label>
|
|
||||||
})
|
|
||||||
|
|
||||||
return <fieldset className="maputnik-multibutton">
|
|
||||||
{radios}
|
|
||||||
</fieldset>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,232 +1,19 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
import InputNumber from './InputNumber'
|
||||||
|
import Block from './Block'
|
||||||
|
|
||||||
let IDX = 0;
|
|
||||||
|
|
||||||
export default class FieldNumber extends React.Component {
|
export default class FieldNumber extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.number,
|
...InputNumber.propTypes,
|
||||||
default: PropTypes.number,
|
|
||||||
min: PropTypes.number,
|
|
||||||
max: PropTypes.number,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
allowRange: PropTypes.bool,
|
|
||||||
rangeStep: PropTypes.number,
|
|
||||||
wdKey: PropTypes.string,
|
|
||||||
required: PropTypes.bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
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 <input/> 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() {
|
render() {
|
||||||
if(
|
const {props} = this;
|
||||||
this.props.hasOwnProperty("min") && this.props.hasOwnProperty("max") &&
|
return <Block label={props.label}>
|
||||||
this.props.min !== undefined && this.props.max !== undefined &&
|
<InputNumber {...props} />
|
||||||
this.props.allowRange
|
</Block>
|
||||||
) {
|
|
||||||
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 <div className="maputnik-number-container">
|
|
||||||
<input
|
|
||||||
className="maputnik-number-range"
|
|
||||||
key="range"
|
|
||||||
type="range"
|
|
||||||
max={this.props.max}
|
|
||||||
min={this.props.min}
|
|
||||||
step="any"
|
|
||||||
spellCheck="false"
|
|
||||||
value={value === undefined ? defaultValue : value}
|
|
||||||
aria-hidden="true"
|
|
||||||
onChange={this.onChangeRange}
|
|
||||||
onKeyDown={() => {
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
key="text"
|
|
||||||
type="text"
|
|
||||||
spellCheck="false"
|
|
||||||
className="maputnik-number"
|
|
||||||
placeholder={this.props.default}
|
|
||||||
value={inputValue === undefined ? "" : inputValue}
|
|
||||||
onFocus={e => {
|
|
||||||
this.setState({editing: true});
|
|
||||||
}}
|
|
||||||
onChange={e => {
|
|
||||||
this.changeValue(e.target.value);
|
|
||||||
}}
|
|
||||||
onBlur={e => {
|
|
||||||
this.setState({editing: false});
|
|
||||||
this.resetValue()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const value = this.state.editing ? this.state.dirtyValue : this.state.value;
|
|
||||||
|
|
||||||
return <input
|
|
||||||
spellCheck="false"
|
|
||||||
className="maputnik-number"
|
|
||||||
placeholder={this.props.default}
|
|
||||||
value={value === undefined ? "" : value}
|
|
||||||
onChange={e => this.changeValue(e.target.value)}
|
|
||||||
onFocus={() => {
|
|
||||||
this.setState({editing: true});
|
|
||||||
}}
|
|
||||||
onBlur={this.resetValue}
|
|
||||||
required={this.props.required}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,33 +1,20 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputSelect from './InputSelect'
|
||||||
|
|
||||||
|
|
||||||
export default class FieldSelect extends React.Component {
|
export default class FieldSelect extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.string.isRequired,
|
...InputSelect.propTypes,
|
||||||
"data-wd-key": PropTypes.string,
|
|
||||||
options: PropTypes.array.isRequired,
|
|
||||||
style: PropTypes.object,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
title: PropTypes.string,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let options = this.props.options
|
const {props} = this;
|
||||||
if(options.length > 0 && !Array.isArray(options[0])) {
|
|
||||||
options = options.map(v => [v, v])
|
|
||||||
}
|
|
||||||
|
|
||||||
return <select
|
return <Block label={props.label}>
|
||||||
className="maputnik-select"
|
<InputSelect {...props}/>
|
||||||
data-wd-key={this.props["data-wd-key"]}
|
</Block>
|
||||||
style={this.props.style}
|
|
||||||
title={this.props.title}
|
|
||||||
value={this.props.value}
|
|
||||||
onChange={e => this.props.onChange(e.target.value)}
|
|
||||||
>
|
|
||||||
{ options.map(([val, label]) => <option key={val} value={val}>{label}</option>) }
|
|
||||||
</select>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,9 @@ import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import FieldAutocomplete from './FieldAutocomplete'
|
import InputAutocomplete from './InputAutocomplete'
|
||||||
|
|
||||||
export default class BlockSource extends React.Component {
|
export default class FieldSource extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
wdKey: PropTypes.string,
|
wdKey: PropTypes.string,
|
||||||
|
@ -26,7 +26,7 @@ export default class BlockSource extends React.Component {
|
||||||
error={this.props.error}
|
error={this.props.error}
|
||||||
data-wd-key={this.props.wdKey}
|
data-wd-key={this.props.wdKey}
|
||||||
>
|
>
|
||||||
<FieldAutocomplete
|
<InputAutocomplete
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
options={this.props.sourceIds.map(src => [src, src])}
|
options={this.props.sourceIds.map(src => [src, src])}
|
||||||
|
@ -34,4 +34,3 @@ export default class BlockSource extends React.Component {
|
||||||
</Block>
|
</Block>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,9 @@ import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import FieldAutocomplete from './FieldAutocomplete'
|
import InputAutocomplete from './InputAutocomplete'
|
||||||
|
|
||||||
export default class BlockSourceLayer extends React.Component {
|
export default class FieldSourceLayer extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
|
@ -23,7 +23,7 @@ export default class BlockSourceLayer extends React.Component {
|
||||||
return <Block label={"Source Layer"} fieldSpec={latest.layer['source-layer']}
|
return <Block label={"Source Layer"} fieldSpec={latest.layer['source-layer']}
|
||||||
data-wd-key="layer-source-layer"
|
data-wd-key="layer-source-layer"
|
||||||
>
|
>
|
||||||
<FieldAutocomplete
|
<InputAutocomplete
|
||||||
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
onChange={this.props.onChange}
|
onChange={this.props.onChange}
|
||||||
|
@ -32,4 +32,3 @@ export default class BlockSourceLayer extends React.Component {
|
||||||
</Block>
|
</Block>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,92 +1,20 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputString from './InputString'
|
||||||
|
|
||||||
export default class FieldString extends React.Component {
|
export default class FieldString extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
"data-wd-key": PropTypes.string,
|
...InputString.propTypes,
|
||||||
value: PropTypes.string,
|
name: 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,
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
render() {
|
||||||
let tag;
|
const {props} = this;
|
||||||
let classes;
|
|
||||||
|
|
||||||
if(!!this.props.multi) {
|
return <Block label={props.label}>
|
||||||
tag = "textarea"
|
<InputString {...props} />
|
||||||
classes = [
|
</Block>
|
||||||
"maputnik-string",
|
|
||||||
"maputnik-string--multi"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
tag = "input"
|
|
||||||
classes = [
|
|
||||||
"maputnik-string"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!!this.props.disabled) {
|
|
||||||
classes.push("maputnik-string--disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
return React.createElement(tag, {
|
|
||||||
"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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,10 @@ import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import FieldSelect from './FieldSelect'
|
import InputSelect from './InputSelect'
|
||||||
import FieldString from './FieldString'
|
import InputString from './InputString'
|
||||||
|
|
||||||
export default class BlockType extends React.Component {
|
export default class FieldType extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
wdKey: PropTypes.string,
|
wdKey: PropTypes.string,
|
||||||
|
@ -25,13 +25,13 @@ export default class BlockType extends React.Component {
|
||||||
error={this.props.error}
|
error={this.props.error}
|
||||||
>
|
>
|
||||||
{this.props.disabled &&
|
{this.props.disabled &&
|
||||||
<FieldString
|
<InputString
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{!this.props.disabled &&
|
{!this.props.disabled &&
|
||||||
<FieldSelect
|
<InputSelect
|
||||||
options={[
|
options={[
|
||||||
['background', 'Background'],
|
['background', 'Background'],
|
||||||
['fill', 'Fill'],
|
['fill', 'Fill'],
|
||||||
|
@ -50,4 +50,3 @@ export default class BlockType extends React.Component {
|
||||||
</Block>
|
</Block>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,100 +1,21 @@
|
||||||
import React, {Fragment} from 'react'
|
import React, {Fragment} from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import FieldString from './FieldString'
|
import InputUrl from './InputUrl'
|
||||||
import SmallError from './SmallError'
|
import Block from './Block'
|
||||||
|
|
||||||
|
|
||||||
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 = (
|
|
||||||
<SmallError>
|
|
||||||
Must provide protocol {
|
|
||||||
isSsl
|
|
||||||
? <code>https://</code>
|
|
||||||
: <><code>http://</code> or <code>https://</code></>
|
|
||||||
}
|
|
||||||
</SmallError>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else if (
|
|
||||||
protocol &&
|
|
||||||
protocol === "http:" &&
|
|
||||||
window.location.protocol === "https:"
|
|
||||||
) {
|
|
||||||
error = (
|
|
||||||
<SmallError>
|
|
||||||
CORS policy won't allow fetching resources served over http from https, use a <code>https://</code> domain
|
|
||||||
</SmallError>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class FieldUrl extends React.Component {
|
export default class FieldUrl extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
"data-wd-key": PropTypes.string,
|
...InputUrl.propTypes,
|
||||||
value: PropTypes.string,
|
|
||||||
style: PropTypes.object,
|
|
||||||
default: PropTypes.string,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
onInput: PropTypes.func,
|
|
||||||
multi: PropTypes.bool,
|
|
||||||
required: PropTypes.bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
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 () {
|
render () {
|
||||||
|
const {props} = this;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Block label={this.props.label}>
|
||||||
<FieldString
|
<InputUrl {...props} />
|
||||||
{...this.props}
|
</Block>
|
||||||
onInput={this.onInput}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>
|
|
||||||
{this.state.error}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
23
src/components/Fieldset.jsx
Normal file
23
src/components/Fieldset.jsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
|
||||||
|
let IDX = 0;
|
||||||
|
|
||||||
|
export default class Fieldset extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props);
|
||||||
|
this._labelId = `fieldset_label_${(IDX++)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {props} = this;
|
||||||
|
|
||||||
|
return <div className="maputnik-input-block" role="group" aria-labelledby={this._labelId}>
|
||||||
|
<div className="maputnik-input-block-label" id={this._labelId}>{props.label}</div>
|
||||||
|
<div className="maputnik-input-block-content">
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,11 +4,11 @@ import { combiningFilterOps } from '../libs/filterops.js'
|
||||||
import {mdiTableRowPlusAfter} from '@mdi/js';
|
import {mdiTableRowPlusAfter} from '@mdi/js';
|
||||||
|
|
||||||
import {latest, validate, migrate} from '@mapbox/mapbox-gl-style-spec'
|
import {latest, validate, migrate} from '@mapbox/mapbox-gl-style-spec'
|
||||||
import FieldSelect from './FieldSelect'
|
import InputSelect from './InputSelect'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import SingleFilterEditor from './SingleFilterEditor'
|
import SingleFilterEditor from './SingleFilterEditor'
|
||||||
import FilterEditorBlock from './FilterEditorBlock'
|
import FilterEditorBlock from './FilterEditorBlock'
|
||||||
import Button from './Button'
|
import InputButton from './InputButton'
|
||||||
import Doc from './Doc'
|
import Doc from './Doc'
|
||||||
import ExpressionProperty from './_ExpressionProperty';
|
import ExpressionProperty from './_ExpressionProperty';
|
||||||
import {mdiFunctionVariant} from '@mdi/js';
|
import {mdiFunctionVariant} from '@mdi/js';
|
||||||
|
@ -191,7 +191,7 @@ export default class FilterEditor extends React.Component {
|
||||||
<p>
|
<p>
|
||||||
Nested filters are not supported.
|
Nested filters are not supported.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<InputButton
|
||||||
onClick={this.makeExpression}
|
onClick={this.makeExpression}
|
||||||
title="Convert to expression"
|
title="Convert to expression"
|
||||||
>
|
>
|
||||||
|
@ -199,7 +199,7 @@ export default class FilterEditor extends React.Component {
|
||||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||||
</svg>
|
</svg>
|
||||||
Upgrade to expression
|
Upgrade to expression
|
||||||
</Button>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else if (displaySimpleFilter) {
|
else if (displaySimpleFilter) {
|
||||||
|
@ -209,7 +209,7 @@ export default class FilterEditor extends React.Component {
|
||||||
|
|
||||||
const actions = (
|
const actions = (
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<InputButton
|
||||||
onClick={this.makeExpression}
|
onClick={this.makeExpression}
|
||||||
title="Convert to expression"
|
title="Convert to expression"
|
||||||
className="maputnik-make-zoom-function"
|
className="maputnik-make-zoom-function"
|
||||||
|
@ -217,7 +217,7 @@ export default class FilterEditor extends React.Component {
|
||||||
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -249,7 +249,7 @@ export default class FilterEditor extends React.Component {
|
||||||
label={"Filter"}
|
label={"Filter"}
|
||||||
action={actions}
|
action={actions}
|
||||||
>
|
>
|
||||||
<FieldSelect
|
<InputSelect
|
||||||
value={combiningOp}
|
value={combiningOp}
|
||||||
onChange={this.onFilterPartChanged.bind(this, 0)}
|
onChange={this.onFilterPartChanged.bind(this, 0)}
|
||||||
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
|
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
|
||||||
|
@ -260,7 +260,7 @@ export default class FilterEditor extends React.Component {
|
||||||
key="buttons"
|
key="buttons"
|
||||||
className="maputnik-filter-editor-add-wrapper"
|
className="maputnik-filter-editor-add-wrapper"
|
||||||
>
|
>
|
||||||
<Button
|
<InputButton
|
||||||
data-wd-key="layer-filter-button"
|
data-wd-key="layer-filter-button"
|
||||||
className="maputnik-add-filter"
|
className="maputnik-add-filter"
|
||||||
onClick={this.addFilterItem}
|
onClick={this.addFilterItem}
|
||||||
|
@ -268,7 +268,7 @@ export default class FilterEditor extends React.Component {
|
||||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||||
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
||||||
</svg> Add filter
|
</svg> Add filter
|
||||||
</Button>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
key="doc"
|
key="doc"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import Button from './Button'
|
import InputButton from './InputButton'
|
||||||
import {MdDelete} from 'react-icons/md'
|
import {MdDelete} from 'react-icons/md'
|
||||||
|
|
||||||
export default class FilterEditorBlock extends React.Component {
|
export default class FilterEditorBlock extends React.Component {
|
||||||
|
@ -12,13 +12,13 @@ export default class FilterEditorBlock extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return <div className="maputnik-filter-editor-block">
|
return <div className="maputnik-filter-editor-block">
|
||||||
<div className="maputnik-filter-editor-block-action">
|
<div className="maputnik-filter-editor-block-action">
|
||||||
<Button
|
<InputButton
|
||||||
className="maputnik-delete-filter"
|
className="maputnik-delete-filter"
|
||||||
onClick={this.props.onDelete}
|
onClick={this.props.onDelete}
|
||||||
title="Delete filter block"
|
title="Delete filter block"
|
||||||
>
|
>
|
||||||
<MdDelete />
|
<MdDelete />
|
||||||
</Button>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="maputnik-filter-editor-block-content">
|
<div className="maputnik-filter-editor-block-content">
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
|
|
113
src/components/InputArray.jsx
Normal file
113
src/components/InputArray.jsx
Normal file
|
@ -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 <InputNumber
|
||||||
|
key={i}
|
||||||
|
default={containsValues ? undefined : this.props.default[i]}
|
||||||
|
value={value[i]}
|
||||||
|
required={containsValues ? true : false}
|
||||||
|
onChange={this.changeValue.bind(this, i)}
|
||||||
|
aria-label={this.props['aria-label']}
|
||||||
|
/>
|
||||||
|
} else {
|
||||||
|
return <InputString
|
||||||
|
key={i}
|
||||||
|
default={containsValues ? undefined : this.props.default[i]}
|
||||||
|
value={value[i]}
|
||||||
|
required={containsValues ? true : false}
|
||||||
|
onChange={this.changeValue.bind(this, i)}
|
||||||
|
aria-label={this.props['aria-label']}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="maputnik-array">
|
||||||
|
{inputs}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
100
src/components/InputAutocomplete.jsx
Normal file
100
src/components/InputAutocomplete.jsx
Normal file
|
@ -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 <div
|
||||||
|
ref={(el) => {
|
||||||
|
this.autocompleteMenuEl = el;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Autocomplete
|
||||||
|
aria-label={this.props['aria-label']}
|
||||||
|
menuStyle={{
|
||||||
|
position: "fixed",
|
||||||
|
overflow: "auto",
|
||||||
|
maxHeight: this.state.maxHeight,
|
||||||
|
zIndex: '998'
|
||||||
|
}}
|
||||||
|
wrapperProps={{
|
||||||
|
className: "maputnik-autocomplete",
|
||||||
|
style: null
|
||||||
|
}}
|
||||||
|
inputProps={{
|
||||||
|
className: "maputnik-string",
|
||||||
|
spellCheck: false
|
||||||
|
}}
|
||||||
|
value={this.props.value}
|
||||||
|
items={this.props.options}
|
||||||
|
getItemValue={(item) => 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) => (
|
||||||
|
<div
|
||||||
|
key={item[0]}
|
||||||
|
className={classnames({
|
||||||
|
"maputnik-autocomplete-menu-item": true,
|
||||||
|
"maputnik-autocomplete-menu-item-selected": isHighlighted,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{item[1]}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
|
|
||||||
class Button extends React.Component {
|
export default class InputButton extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
"data-wd-key": PropTypes.string,
|
"data-wd-key": PropTypes.string,
|
||||||
"aria-label": PropTypes.string,
|
"aria-label": PropTypes.string,
|
||||||
|
@ -33,4 +33,3 @@ class Button extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Button
|
|
34
src/components/InputCheckbox.jsx
Normal file
34
src/components/InputCheckbox.jsx
Normal file
|
@ -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 <label className="maputnik-checkbox-wrapper">
|
||||||
|
<input
|
||||||
|
className="maputnik-checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
style={this.props.style}
|
||||||
|
onChange={e => this.props.onChange(!this.props.value)}
|
||||||
|
checked={this.props.value}
|
||||||
|
/>
|
||||||
|
<div className="maputnik-checkbox-box">
|
||||||
|
<svg style={{
|
||||||
|
display: this.props.value ? 'inline' : 'none'
|
||||||
|
}} className="maputnik-checkbox-icon" viewBox='0 0 32 32'>
|
||||||
|
<path d='M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z' />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
135
src/components/InputColor.jsx
Normal file
135
src/components/InputColor.jsx
Normal file
|
@ -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 = <div
|
||||||
|
className="maputnik-color-picker-offset"
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
zIndex: 1,
|
||||||
|
left: offset.left,
|
||||||
|
top: offset.top,
|
||||||
|
}}>
|
||||||
|
<ChromePicker
|
||||||
|
color={currentColor}
|
||||||
|
onChange={c => this.onChangeNoCheck(formatColor(c))}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="maputnik-color-picker-offset"
|
||||||
|
onClick={this.togglePicker}
|
||||||
|
style={{
|
||||||
|
zIndex: -1,
|
||||||
|
position: 'fixed',
|
||||||
|
top: '0px',
|
||||||
|
right: '0px',
|
||||||
|
bottom: '0px',
|
||||||
|
left: '0px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
var swatchStyle = {
|
||||||
|
backgroundColor: this.props.value
|
||||||
|
};
|
||||||
|
|
||||||
|
return <div className="maputnik-color-wrapper">
|
||||||
|
{this.state.pickerOpened && picker}
|
||||||
|
<div className="maputnik-color-swatch" style={swatchStyle}></div>
|
||||||
|
<input
|
||||||
|
aria-label={this.props['aria-label']}
|
||||||
|
spellCheck="false"
|
||||||
|
autoComplete="off"
|
||||||
|
className="maputnik-color"
|
||||||
|
ref={(input) => 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)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
138
src/components/InputDynamicArray.jsx
Normal file
138
src/components/InputDynamicArray.jsx
Normal file
|
@ -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= <DeleteValueInputButton onClick={this.deleteValue.bind(this, i)} />
|
||||||
|
let input;
|
||||||
|
if(this.props.type === 'url') {
|
||||||
|
input = <InputUrl
|
||||||
|
value={v}
|
||||||
|
onChange={this.changeValue.bind(this, i)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
else if (this.props.type === 'number') {
|
||||||
|
input = <InputNumber
|
||||||
|
value={v}
|
||||||
|
onChange={this.changeValue.bind(this, i)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
else if (this.props.type === 'enum') {
|
||||||
|
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)]);
|
||||||
|
|
||||||
|
input = <InputEnum
|
||||||
|
options={options}
|
||||||
|
value={v}
|
||||||
|
onChange={this.changeValue.bind(this, i)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
input = <InputString
|
||||||
|
value={v}
|
||||||
|
onChange={this.changeValue.bind(this, i)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div
|
||||||
|
style={this.props.style}
|
||||||
|
key={i}
|
||||||
|
className="maputnik-array-block"
|
||||||
|
>
|
||||||
|
<div className="maputnik-array-block-action">
|
||||||
|
{deleteValueBtn}
|
||||||
|
</div>
|
||||||
|
<div className="maputnik-array-block-content">
|
||||||
|
{input}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="maputnik-array">
|
||||||
|
{inputs}
|
||||||
|
<InputButton
|
||||||
|
className="maputnik-array-add-value"
|
||||||
|
onClick={this.addValue}
|
||||||
|
>
|
||||||
|
Add value
|
||||||
|
</InputButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeleteValueInputButton extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <InputButton
|
||||||
|
className="maputnik-delete-stop"
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
title="Remove array item"
|
||||||
|
>
|
||||||
|
<FieldDocLabel
|
||||||
|
label={<MdDelete />}
|
||||||
|
doc={"Remove array item."}
|
||||||
|
/>
|
||||||
|
</InputButton>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
49
src/components/InputEnum.jsx
Normal file
49
src/components/InputEnum.jsx
Normal file
|
@ -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 <InputMultiInput
|
||||||
|
name={name}
|
||||||
|
options={options}
|
||||||
|
value={value || this.props.default}
|
||||||
|
onChange={onChange}
|
||||||
|
aria-label={this.props['aria-label']}
|
||||||
|
/>
|
||||||
|
} else {
|
||||||
|
return <InputSelect
|
||||||
|
options={options}
|
||||||
|
value={value || this.props.default}
|
||||||
|
onChange={onChange}
|
||||||
|
aria-label={this.props['aria-label']}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
61
src/components/InputFont.jsx
Normal file
61
src/components/InputFont.jsx
Normal file
|
@ -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 <li
|
||||||
|
key={i}
|
||||||
|
>
|
||||||
|
<InputAutocomplete
|
||||||
|
aria-label={this.props['aria-label']}
|
||||||
|
value={value}
|
||||||
|
options={this.props.fonts.map(f => [f, f])}
|
||||||
|
onChange={this.changeFont.bind(this, i)}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="maputnik-font">
|
||||||
|
{inputs}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,7 @@ import stringifyPretty from 'json-stringify-pretty-compact'
|
||||||
import '../util/codemirror-mgl';
|
import '../util/codemirror-mgl';
|
||||||
|
|
||||||
|
|
||||||
export default class FieldJsonEditor extends React.Component {
|
export default class InputJson extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
layer: PropTypes.any.isRequired,
|
layer: PropTypes.any.isRequired,
|
||||||
maxHeight: PropTypes.number,
|
maxHeight: PropTypes.number,
|
||||||
|
@ -172,4 +172,3 @@ export default class FieldJsonEditor extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
42
src/components/InputMultiInput.jsx
Normal file
42
src/components/InputMultiInput.jsx
Normal file
|
@ -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 <label
|
||||||
|
key={val}
|
||||||
|
className={classnames("maputnik-radio-as-button", {"maputnik-button-selected": val === selectedValue})}
|
||||||
|
>
|
||||||
|
<input type="radio"
|
||||||
|
name={this.props.name}
|
||||||
|
onChange={e => this.props.onChange(val)}
|
||||||
|
value={val}
|
||||||
|
checked={val === selectedValue}
|
||||||
|
/>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
})
|
||||||
|
|
||||||
|
return <fieldset className="maputnik-multibutton" aria-label={this.props['aria-label']}>
|
||||||
|
{radios}
|
||||||
|
</fieldset>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
235
src/components/InputNumber.jsx
Normal file
235
src/components/InputNumber.jsx
Normal file
|
@ -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 <input/> 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 <div className="maputnik-number-container">
|
||||||
|
<input
|
||||||
|
className="maputnik-number-range"
|
||||||
|
key="range"
|
||||||
|
type="range"
|
||||||
|
max={this.props.max}
|
||||||
|
min={this.props.min}
|
||||||
|
step="any"
|
||||||
|
spellCheck="false"
|
||||||
|
value={value === undefined ? defaultValue : value}
|
||||||
|
aria-hidden="true"
|
||||||
|
onChange={this.onChangeRange}
|
||||||
|
onKeyDown={() => {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
key="text"
|
||||||
|
type="text"
|
||||||
|
spellCheck="false"
|
||||||
|
className="maputnik-number"
|
||||||
|
placeholder={this.props.default}
|
||||||
|
value={inputValue === undefined ? "" : inputValue}
|
||||||
|
onFocus={e => {
|
||||||
|
this.setState({editing: true});
|
||||||
|
}}
|
||||||
|
onChange={e => {
|
||||||
|
this.changeValue(e.target.value);
|
||||||
|
}}
|
||||||
|
onBlur={e => {
|
||||||
|
this.setState({editing: false});
|
||||||
|
this.resetValue()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const value = this.state.editing ? this.state.dirtyValue : this.state.value;
|
||||||
|
|
||||||
|
return <input
|
||||||
|
aria-label={this.props['aria-label']}
|
||||||
|
spellCheck="false"
|
||||||
|
className="maputnik-number"
|
||||||
|
placeholder={this.props.default}
|
||||||
|
value={value === undefined ? "" : value}
|
||||||
|
onChange={e => this.changeValue(e.target.value)}
|
||||||
|
onFocus={() => {
|
||||||
|
this.setState({editing: true});
|
||||||
|
}}
|
||||||
|
onBlur={this.resetValue}
|
||||||
|
required={this.props.required}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
36
src/components/InputSelect.jsx
Normal file
36
src/components/InputSelect.jsx
Normal file
|
@ -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 <select
|
||||||
|
className="maputnik-select"
|
||||||
|
data-wd-key={this.props["data-wd-key"]}
|
||||||
|
style={this.props.style}
|
||||||
|
title={this.props.title}
|
||||||
|
value={this.props.value}
|
||||||
|
onChange={e => this.props.onChange(e.target.value)}
|
||||||
|
aria-label={this.props['aria-label']}
|
||||||
|
>
|
||||||
|
{ options.map(([val, label]) => <option key={val} value={val}>{label}</option>) }
|
||||||
|
</select>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
139
src/components/InputSpec.jsx
Normal file
139
src/components/InputSpec.jsx
Normal file
|
@ -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 (
|
||||||
|
<InputNumber
|
||||||
|
{...commonProps}
|
||||||
|
min={this.props.fieldSpec.minimum}
|
||||||
|
max={this.props.fieldSpec.maximum}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case 'enum':
|
||||||
|
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)])
|
||||||
|
|
||||||
|
return <InputEnum
|
||||||
|
{...commonProps}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
case 'resolvedImage':
|
||||||
|
case 'formatted':
|
||||||
|
case 'string':
|
||||||
|
if (iconProperties.indexOf(this.props.fieldName) >= 0) {
|
||||||
|
const options = this.props.fieldSpec.values || [];
|
||||||
|
return <InputAutocomplete
|
||||||
|
{...commonProps}
|
||||||
|
options={options.map(f => [f, f])}
|
||||||
|
/>
|
||||||
|
} else {
|
||||||
|
return <InputString
|
||||||
|
{...commonProps}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
case 'color': return (
|
||||||
|
<InputColor
|
||||||
|
{...commonProps}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case 'boolean': return (
|
||||||
|
<InputCheckbox
|
||||||
|
{...commonProps}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case 'array':
|
||||||
|
if(this.props.fieldName === 'text-font') {
|
||||||
|
return <InputFont
|
||||||
|
{...commonProps}
|
||||||
|
fonts={this.props.fieldSpec.values}
|
||||||
|
/>
|
||||||
|
} else {
|
||||||
|
if (this.props.fieldSpec.length) {
|
||||||
|
return <InputArray
|
||||||
|
{...commonProps}
|
||||||
|
type={this.props.fieldSpec.value}
|
||||||
|
length={this.props.fieldSpec.length}
|
||||||
|
/>
|
||||||
|
} else {
|
||||||
|
return <InputDynamicArray
|
||||||
|
{...commonProps}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
type={this.props.fieldSpec.value}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default: return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-wd-key={"spec-field:"+this.props.fieldName}>
|
||||||
|
{childNodes.call(this)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
95
src/components/InputString.jsx
Normal file
95
src/components/InputString.jsx
Normal file
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
103
src/components/InputUrl.jsx
Normal file
103
src/components/InputUrl.jsx
Normal file
|
@ -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 = (
|
||||||
|
<SmallError>
|
||||||
|
Must provide protocol {
|
||||||
|
isSsl
|
||||||
|
? <code>https://</code>
|
||||||
|
: <><code>http://</code> or <code>https://</code></>
|
||||||
|
}
|
||||||
|
</SmallError>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (
|
||||||
|
protocol &&
|
||||||
|
protocol === "http:" &&
|
||||||
|
window.location.protocol === "https:"
|
||||||
|
) {
|
||||||
|
error = (
|
||||||
|
<SmallError>
|
||||||
|
CORS policy won't allow fetching resources served over http from https, use a <code>https://</code> domain
|
||||||
|
</SmallError>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<InputString
|
||||||
|
{...this.props}
|
||||||
|
onInput={this.onInput}
|
||||||
|
onChange={this.onChange}
|
||||||
|
aria-label={this.props['aria-label']}
|
||||||
|
/>
|
||||||
|
{this.state.error}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,17 +2,17 @@ import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
|
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton'
|
||||||
|
|
||||||
import FieldJsonEditor from './FieldJsonEditor'
|
import FieldJson from './FieldJson'
|
||||||
import FilterEditor from './FilterEditor'
|
import FilterEditor from './FilterEditor'
|
||||||
import PropertyGroup from './PropertyGroup'
|
import PropertyGroup from './PropertyGroup'
|
||||||
import LayerEditorGroup from './LayerEditorGroup'
|
import LayerEditorGroup from './LayerEditorGroup'
|
||||||
import BlockType from './BlockType'
|
import FieldType from './FieldType'
|
||||||
import BlockId from './BlockId'
|
import FieldId from './FieldId'
|
||||||
import BlockMinZoom from './BlockMinZoom'
|
import FieldMinZoom from './FieldMinZoom'
|
||||||
import BlockMaxZoom from './BlockMaxZoom'
|
import FieldMaxZoom from './FieldMaxZoom'
|
||||||
import BlockComment from './BlockComment'
|
import FieldComment from './FieldComment'
|
||||||
import BlockSource from './BlockSource'
|
import FieldSource from './FieldSource'
|
||||||
import BlockSourceLayer from './BlockSourceLayer'
|
import FieldSourceLayer from './FieldSourceLayer'
|
||||||
import {Accordion} from 'react-accessible-accordion';
|
import {Accordion} from 'react-accessible-accordion';
|
||||||
|
|
||||||
import {MdMoreVert} from 'react-icons/md'
|
import {MdMoreVert} from 'react-icons/md'
|
||||||
|
@ -152,13 +152,13 @@ export default class LayerEditor extends React.Component {
|
||||||
|
|
||||||
switch(type) {
|
switch(type) {
|
||||||
case 'layer': return <div>
|
case 'layer': return <div>
|
||||||
<BlockId
|
<FieldId
|
||||||
value={this.props.layer.id}
|
value={this.props.layer.id}
|
||||||
wdKey="layer-editor.layer-id"
|
wdKey="layer-editor.layer-id"
|
||||||
error={errorData.id}
|
error={errorData.id}
|
||||||
onChange={newId => this.props.onLayerIdChange(this.props.layerIndex, this.props.layer.id, newId)}
|
onChange={newId => this.props.onLayerIdChange(this.props.layerIndex, this.props.layer.id, newId)}
|
||||||
/>
|
/>
|
||||||
<BlockType
|
<FieldType
|
||||||
disabled={true}
|
disabled={true}
|
||||||
error={errorData.type}
|
error={errorData.type}
|
||||||
value={this.props.layer.type}
|
value={this.props.layer.type}
|
||||||
|
@ -167,7 +167,7 @@ export default class LayerEditor extends React.Component {
|
||||||
changeType(this.props.layer, newType)
|
changeType(this.props.layer, newType)
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{this.props.layer.type !== 'background' && <BlockSource
|
{this.props.layer.type !== 'background' && <FieldSource
|
||||||
error={errorData.source}
|
error={errorData.source}
|
||||||
sourceIds={Object.keys(this.props.sources)}
|
sourceIds={Object.keys(this.props.sources)}
|
||||||
value={this.props.layer.source}
|
value={this.props.layer.source}
|
||||||
|
@ -175,24 +175,24 @@ export default class LayerEditor extends React.Component {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.props.layer.type) < 0 &&
|
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.props.layer.type) < 0 &&
|
||||||
<BlockSourceLayer
|
<FieldSourceLayer
|
||||||
error={errorData['source-layer']}
|
error={errorData['source-layer']}
|
||||||
sourceLayerIds={sourceLayerIds}
|
sourceLayerIds={sourceLayerIds}
|
||||||
value={this.props.layer['source-layer']}
|
value={this.props.layer['source-layer']}
|
||||||
onChange={v => this.changeProperty(null, 'source-layer', v)}
|
onChange={v => this.changeProperty(null, 'source-layer', v)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<BlockMinZoom
|
<FieldMinZoom
|
||||||
error={errorData.minzoom}
|
error={errorData.minzoom}
|
||||||
value={this.props.layer.minzoom}
|
value={this.props.layer.minzoom}
|
||||||
onChange={v => this.changeProperty(null, 'minzoom', v)}
|
onChange={v => this.changeProperty(null, 'minzoom', v)}
|
||||||
/>
|
/>
|
||||||
<BlockMaxZoom
|
<FieldMaxZoom
|
||||||
error={errorData.maxzoom}
|
error={errorData.maxzoom}
|
||||||
value={this.props.layer.maxzoom}
|
value={this.props.layer.maxzoom}
|
||||||
onChange={v => this.changeProperty(null, 'maxzoom', v)}
|
onChange={v => this.changeProperty(null, 'maxzoom', v)}
|
||||||
/>
|
/>
|
||||||
<BlockComment
|
<FieldComment
|
||||||
error={errorData.comment}
|
error={errorData.comment}
|
||||||
value={comment}
|
value={comment}
|
||||||
onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)}
|
onChange={v => this.changeProperty('metadata', 'maputnik:comment', v == "" ? undefined : v)}
|
||||||
|
@ -208,22 +208,24 @@ export default class LayerEditor extends React.Component {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
case 'properties': return <PropertyGroup
|
case 'properties':
|
||||||
errors={errorData}
|
return <PropertyGroup
|
||||||
layer={this.props.layer}
|
errors={errorData}
|
||||||
groupFields={fields}
|
layer={this.props.layer}
|
||||||
spec={this.props.spec}
|
groupFields={fields}
|
||||||
onChange={this.changeProperty.bind(this)}
|
spec={this.props.spec}
|
||||||
/>
|
onChange={this.changeProperty.bind(this)}
|
||||||
case 'jsoneditor': return <FieldJsonEditor
|
/>
|
||||||
layer={this.props.layer}
|
case 'jsoneditor':
|
||||||
onChange={(layer) => {
|
return <FieldJson
|
||||||
this.props.onLayerChanged(
|
layer={this.props.layer}
|
||||||
this.props.layerIndex,
|
onChange={(layer) => {
|
||||||
layer
|
this.props.onLayerChanged(
|
||||||
);
|
this.props.layerIndex,
|
||||||
}}
|
layer
|
||||||
/>
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -237,3 +237,4 @@ export default class MapMapboxGl extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import Button from './Button'
|
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||||
|
import InputButton from './InputButton'
|
||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
|
|
||||||
import BlockType from './BlockType'
|
import FieldType from './FieldType'
|
||||||
import BlockId from './BlockId'
|
import FieldId from './FieldId'
|
||||||
import BlockSource from './BlockSource'
|
import FieldSource from './FieldSource'
|
||||||
import BlockSourceLayer from './BlockSourceLayer'
|
import FieldSourceLayer from './FieldSourceLayer'
|
||||||
|
|
||||||
export default class ModalAdd extends React.Component {
|
export default class ModalAdd extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -129,20 +130,22 @@ export default class ModalAdd extends React.Component {
|
||||||
className="maputnik-add-modal"
|
className="maputnik-add-modal"
|
||||||
>
|
>
|
||||||
<div className="maputnik-add-layer">
|
<div className="maputnik-add-layer">
|
||||||
<BlockId
|
<FieldId
|
||||||
|
label="ID"
|
||||||
|
fieldSpec={latest.layer.id}
|
||||||
value={this.state.id}
|
value={this.state.id}
|
||||||
wdKey="add-layer.layer-id"
|
wdKey="add-layer.layer-id"
|
||||||
onChange={v => {
|
onChange={v => {
|
||||||
this.setState({ id: v })
|
this.setState({ id: v })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<BlockType
|
<FieldType
|
||||||
value={this.state.type}
|
value={this.state.type}
|
||||||
wdKey="add-layer.layer-type"
|
wdKey="add-layer.layer-type"
|
||||||
onChange={v => this.setState({ type: v })}
|
onChange={v => this.setState({ type: v })}
|
||||||
/>
|
/>
|
||||||
{this.state.type !== 'background' &&
|
{this.state.type !== 'background' &&
|
||||||
<BlockSource
|
<FieldSource
|
||||||
sourceIds={sources}
|
sourceIds={sources}
|
||||||
wdKey="add-layer.layer-source-block"
|
wdKey="add-layer.layer-source-block"
|
||||||
value={this.state.source}
|
value={this.state.source}
|
||||||
|
@ -150,20 +153,20 @@ export default class ModalAdd extends React.Component {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
|
{['background', 'raster', 'hillshade', 'heatmap'].indexOf(this.state.type) < 0 &&
|
||||||
<BlockSourceLayer
|
<FieldSourceLayer
|
||||||
isFixed={true}
|
isFixed={true}
|
||||||
sourceLayerIds={layers}
|
sourceLayerIds={layers}
|
||||||
value={this.state['source-layer']}
|
value={this.state['source-layer']}
|
||||||
onChange={v => this.setState({ 'source-layer': v })}
|
onChange={v => this.setState({ 'source-layer': v })}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
<Button
|
<InputButton
|
||||||
className="maputnik-add-layer-button"
|
className="maputnik-add-layer-button"
|
||||||
onClick={this.addLayer}
|
onClick={this.addLayer}
|
||||||
data-wd-key="add-layer"
|
data-wd-key="add-layer"
|
||||||
>
|
>
|
||||||
Add Layer
|
Add Layer
|
||||||
</Button>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,9 @@ import Slugify from 'slugify'
|
||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
|
|
||||||
import {format} from '@mapbox/mapbox-gl-style-spec'
|
import {format} from '@mapbox/mapbox-gl-style-spec'
|
||||||
import Block from './Block'
|
|
||||||
import FieldString from './FieldString'
|
import FieldString from './FieldString'
|
||||||
import FieldCheckbox from './FieldCheckbox'
|
import FieldCheckbox from './FieldCheckbox'
|
||||||
import Button from './Button'
|
import InputButton from './InputButton'
|
||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
import {MdFileDownload} from 'react-icons/md'
|
import {MdFileDownload} from 'react-icons/md'
|
||||||
import style from '../libs/style'
|
import style from '../libs/style'
|
||||||
|
@ -75,42 +74,33 @@ export default class ModalExport extends React.Component {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Block
|
<FieldString
|
||||||
label={fieldSpecAdditional.maputnik.mapbox_access_token.label}
|
label={fieldSpecAdditional.maputnik.mapbox_access_token.label}
|
||||||
fieldSpec={fieldSpecAdditional.maputnik.mapbox_access_token}
|
fieldSpec={fieldSpecAdditional.maputnik.mapbox_access_token}
|
||||||
>
|
value={(this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token']}
|
||||||
<FieldString
|
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
|
||||||
value={(this.props.mapStyle.metadata || {})['maputnik:mapbox_access_token']}
|
/>
|
||||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
|
<FieldString
|
||||||
/>
|
|
||||||
</Block>
|
|
||||||
<Block
|
|
||||||
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
|
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
|
||||||
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
|
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
|
||||||
>
|
value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']}
|
||||||
<FieldString
|
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
|
||||||
value={(this.props.mapStyle.metadata || {})['maputnik:openmaptiles_access_token']}
|
/>
|
||||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
|
<FieldString
|
||||||
/>
|
|
||||||
</Block>
|
|
||||||
<Block
|
|
||||||
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
|
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
|
||||||
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
|
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
|
||||||
>
|
value={(this.props.mapStyle.metadata || {})['maputnik:thunderforest_access_token']}
|
||||||
<FieldString
|
onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
|
||||||
value={(this.props.mapStyle.metadata || {})['maputnik:thunderforest_access_token']}
|
/>
|
||||||
onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
|
|
||||||
/>
|
|
||||||
</Block>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<InputButton
|
||||||
onClick={this.downloadStyle.bind(this)}
|
onClick={this.downloadStyle.bind(this)}
|
||||||
title="Download style"
|
title="Download style"
|
||||||
>
|
>
|
||||||
<MdFileDownload />
|
<MdFileDownload />
|
||||||
Download
|
Download
|
||||||
</Button>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import Button from './Button'
|
import InputButton from './InputButton'
|
||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,9 +34,9 @@ export default class ModalLoading extends React.Component {
|
||||||
{this.props.message}
|
{this.props.message}
|
||||||
</p>
|
</p>
|
||||||
<p className="maputnik-dialog__buttons">
|
<p className="maputnik-dialog__buttons">
|
||||||
<Button onClick={(e) => this.props.onCancel(e)}>
|
<InputButton onClick={(e) => this.props.onCancel(e)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</InputButton>
|
||||||
</p>
|
</p>
|
||||||
</Modal>
|
</Modal>
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,9 @@ import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import ModalLoading from './ModalLoading'
|
import ModalLoading from './ModalLoading'
|
||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
import Button from './Button'
|
import InputButton from './InputButton'
|
||||||
import FileReaderInput from 'react-file-reader-input'
|
import FileReaderInput from 'react-file-reader-input'
|
||||||
import FieldUrl from './FieldUrl'
|
import InputUrl from './InputUrl'
|
||||||
|
|
||||||
import {MdFileUpload} from 'react-icons/md'
|
import {MdFileUpload} from 'react-icons/md'
|
||||||
import {MdAddCircleOutline} from 'react-icons/md'
|
import {MdAddCircleOutline} from 'react-icons/md'
|
||||||
|
@ -22,7 +22,7 @@ class PublicStyle extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div className="maputnik-public-style">
|
return <div className="maputnik-public-style">
|
||||||
<Button
|
<InputButton
|
||||||
className="maputnik-public-style-button"
|
className="maputnik-public-style-button"
|
||||||
aria-label={this.props.title}
|
aria-label={this.props.title}
|
||||||
onClick={() => this.props.onSelect(this.props.url)}
|
onClick={() => this.props.onSelect(this.props.url)}
|
||||||
|
@ -38,7 +38,7 @@ class PublicStyle extends React.Component {
|
||||||
backgroundImage: `url(${this.props.thumbnailUrl})`
|
backgroundImage: `url(${this.props.thumbnailUrl})`
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
</Button>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -201,7 +201,7 @@ export default class ModalOpen extends React.Component {
|
||||||
<h2>Upload Style</h2>
|
<h2>Upload Style</h2>
|
||||||
<p>Upload a JSON style from your computer.</p>
|
<p>Upload a JSON style from your computer.</p>
|
||||||
<FileReaderInput onChange={this.onUpload} tabIndex="-1">
|
<FileReaderInput onChange={this.onUpload} tabIndex="-1">
|
||||||
<Button className="maputnik-upload-button"><MdFileUpload /> Upload</Button>
|
<InputButton className="maputnik-upload-button"><MdFileUpload /> Upload</InputButton>
|
||||||
</FileReaderInput>
|
</FileReaderInput>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@ -211,7 +211,7 @@ export default class ModalOpen extends React.Component {
|
||||||
Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>.
|
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>
|
</p>
|
||||||
<form onSubmit={this.onSubmitUrl}>
|
<form onSubmit={this.onSubmitUrl}>
|
||||||
<FieldUrl
|
<InputUrl
|
||||||
data-wd-key="modal:open.url.input"
|
data-wd-key="modal:open.url.input"
|
||||||
type="text"
|
type="text"
|
||||||
className="maputnik-input"
|
className="maputnik-input"
|
||||||
|
@ -221,12 +221,12 @@ export default class ModalOpen extends React.Component {
|
||||||
onChange={this.onChangeUrl}
|
onChange={this.onChangeUrl}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<InputButton
|
||||||
data-wd-key="modal:open.url.button"
|
data-wd-key="modal:open.url.button"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="maputnik-big-button"
|
className="maputnik-big-button"
|
||||||
disabled={this.state.styleUrl.length < 1}
|
disabled={this.state.styleUrl.length < 1}
|
||||||
>Load from URL</Button>
|
>Load from URL</InputButton>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -87,169 +87,158 @@ export default class ModalSettings extends React.Component {
|
||||||
title={'Style Settings'}
|
title={'Style Settings'}
|
||||||
>
|
>
|
||||||
<div className="modal:settings">
|
<div className="modal:settings">
|
||||||
<Block label={"Name"} fieldSpec={latest.$root.name}>
|
|
||||||
<FieldString {...inputProps}
|
<FieldString {...inputProps}
|
||||||
|
label={"Name"}
|
||||||
|
fieldSpec={latest.$root.name}
|
||||||
data-wd-key="modal:settings.name"
|
data-wd-key="modal:settings.name"
|
||||||
value={this.props.mapStyle.name}
|
value={this.props.mapStyle.name}
|
||||||
onChange={this.changeStyleProperty.bind(this, "name")}
|
onChange={this.changeStyleProperty.bind(this, "name")}
|
||||||
/>
|
/>
|
||||||
</Block>
|
|
||||||
<Block label={"Owner"} fieldSpec={{doc: "Owner ID of the style. Used by Mapbox or future style APIs."}}>
|
|
||||||
<FieldString {...inputProps}
|
<FieldString {...inputProps}
|
||||||
|
label={"Owner"}
|
||||||
|
fieldSpec={{doc: "Owner ID of the style. Used by Mapbox or future style APIs."}}
|
||||||
data-wd-key="modal:settings.owner"
|
data-wd-key="modal:settings.owner"
|
||||||
value={this.props.mapStyle.owner}
|
value={this.props.mapStyle.owner}
|
||||||
onChange={this.changeStyleProperty.bind(this, "owner")}
|
onChange={this.changeStyleProperty.bind(this, "owner")}
|
||||||
/>
|
/>
|
||||||
</Block>
|
|
||||||
<Block label={"Sprite URL"} fieldSpec={latest.$root.sprite}>
|
|
||||||
<FieldUrl {...inputProps}
|
<FieldUrl {...inputProps}
|
||||||
|
fieldSpec={latest.$root.sprite}
|
||||||
|
label="Sprite URL"
|
||||||
data-wd-key="modal:settings.sprite"
|
data-wd-key="modal:settings.sprite"
|
||||||
value={this.props.mapStyle.sprite}
|
value={this.props.mapStyle.sprite}
|
||||||
onChange={this.changeStyleProperty.bind(this, "sprite")}
|
onChange={this.changeStyleProperty.bind(this, "sprite")}
|
||||||
/>
|
/>
|
||||||
</Block>
|
|
||||||
|
|
||||||
<Block label={"Glyphs URL"} fieldSpec={latest.$root.glyphs}>
|
|
||||||
<FieldUrl {...inputProps}
|
<FieldUrl {...inputProps}
|
||||||
|
label="Glyphs URL"
|
||||||
|
fieldSpec={latest.$root.glyphs}
|
||||||
data-wd-key="modal:settings.glyphs"
|
data-wd-key="modal:settings.glyphs"
|
||||||
value={this.props.mapStyle.glyphs}
|
value={this.props.mapStyle.glyphs}
|
||||||
onChange={this.changeStyleProperty.bind(this, "glyphs")}
|
onChange={this.changeStyleProperty.bind(this, "glyphs")}
|
||||||
/>
|
/>
|
||||||
</Block>
|
|
||||||
|
|
||||||
<Block
|
|
||||||
label={fieldSpecAdditional.maputnik.mapbox_access_token.label}
|
|
||||||
fieldSpec={fieldSpecAdditional.maputnik.mapbox_access_token}
|
|
||||||
>
|
|
||||||
<FieldString {...inputProps}
|
<FieldString {...inputProps}
|
||||||
|
label={fieldSpecAdditional.maputnik.mapbox_access_token.label}
|
||||||
|
fieldSpec={fieldSpecAdditional.maputnik.mapbox_access_token}
|
||||||
data-wd-key="modal:settings.maputnik:mapbox_access_token"
|
data-wd-key="modal:settings.maputnik:mapbox_access_token"
|
||||||
value={metadata['maputnik:mapbox_access_token']}
|
value={metadata['maputnik:mapbox_access_token']}
|
||||||
onChange={onChangeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
|
onChange={onChangeMetadataProperty.bind(this, "maputnik:mapbox_access_token")}
|
||||||
/>
|
/>
|
||||||
</Block>
|
|
||||||
|
|
||||||
<Block
|
|
||||||
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
|
|
||||||
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
|
|
||||||
>
|
|
||||||
<FieldString {...inputProps}
|
<FieldString {...inputProps}
|
||||||
|
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
|
||||||
|
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
|
||||||
data-wd-key="modal:settings.maputnik:openmaptiles_access_token"
|
data-wd-key="modal:settings.maputnik:openmaptiles_access_token"
|
||||||
value={metadata['maputnik:openmaptiles_access_token']}
|
value={metadata['maputnik:openmaptiles_access_token']}
|
||||||
onChange={onChangeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
|
onChange={onChangeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
|
||||||
/>
|
/>
|
||||||
</Block>
|
|
||||||
|
|
||||||
<Block
|
|
||||||
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
|
|
||||||
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
|
|
||||||
>
|
|
||||||
<FieldString {...inputProps}
|
<FieldString {...inputProps}
|
||||||
|
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
|
||||||
|
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
|
||||||
data-wd-key="modal:settings.maputnik:thunderforest_access_token"
|
data-wd-key="modal:settings.maputnik:thunderforest_access_token"
|
||||||
value={metadata['maputnik:thunderforest_access_token']}
|
value={metadata['maputnik:thunderforest_access_token']}
|
||||||
onChange={onChangeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
|
onChange={onChangeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
|
||||||
/>
|
/>
|
||||||
</Block>
|
|
||||||
|
|
||||||
<Block label={"Center"} fieldSpec={latest.$root.center}>
|
|
||||||
<FieldArray
|
<FieldArray
|
||||||
|
label={"Center"}
|
||||||
|
fieldSpec={latest.$root.center}
|
||||||
length={2}
|
length={2}
|
||||||
type="number"
|
type="number"
|
||||||
value={mapStyle.center}
|
value={mapStyle.center}
|
||||||
default={latest.$root.center.default || [0, 0]}
|
default={latest.$root.center.default || [0, 0]}
|
||||||
onChange={this.changeStyleProperty.bind(this, "center")}
|
onChange={this.changeStyleProperty.bind(this, "center")}
|
||||||
/>
|
/>
|
||||||
</Block>
|
|
||||||
|
|
||||||
<Block label={"Zoom"} fieldSpec={latest.$root.zoom}>
|
|
||||||
<FieldNumber
|
<FieldNumber
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
|
label={"Zoom"}
|
||||||
|
fieldSpec={latest.$root.zoom}
|
||||||
value={mapStyle.zoom}
|
value={mapStyle.zoom}
|
||||||
default={latest.$root.zoom.default || 0}
|
default={latest.$root.zoom.default || 0}
|
||||||
onChange={this.changeStyleProperty.bind(this, "zoom")}
|
onChange={this.changeStyleProperty.bind(this, "zoom")}
|
||||||
/>
|
/>
|
||||||
</Block>
|
|
||||||
|
|
||||||
<Block label={"Bearing"} fieldSpec={latest.$root.bearing}>
|
|
||||||
<FieldNumber
|
<FieldNumber
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
|
label={"Bearing"}
|
||||||
|
fieldSpec={latest.$root.bearing}
|
||||||
value={mapStyle.bearing}
|
value={mapStyle.bearing}
|
||||||
default={latest.$root.bearing.default}
|
default={latest.$root.bearing.default}
|
||||||
onChange={this.changeStyleProperty.bind(this, "bearing")}
|
onChange={this.changeStyleProperty.bind(this, "bearing")}
|
||||||
/>
|
/>
|
||||||
</Block>
|
|
||||||
|
|
||||||
<Block label={"Pitch"} fieldSpec={latest.$root.pitch}>
|
|
||||||
<FieldNumber
|
<FieldNumber
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
|
label={"Pitch"}
|
||||||
|
fieldSpec={latest.$root.pitch}
|
||||||
value={mapStyle.pitch}
|
value={mapStyle.pitch}
|
||||||
default={latest.$root.pitch.default}
|
default={latest.$root.pitch.default}
|
||||||
onChange={this.changeStyleProperty.bind(this, "pitch")}
|
onChange={this.changeStyleProperty.bind(this, "pitch")}
|
||||||
/>
|
/>
|
||||||
</Block>
|
|
||||||
|
|
||||||
<Block label={"Light anchor"} fieldSpec={latest.light.anchor}>
|
|
||||||
<FieldEnum
|
<FieldEnum
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
|
label={"Light anchor"}
|
||||||
|
fieldSpec={latest.light.anchor}
|
||||||
name="light-anchor"
|
name="light-anchor"
|
||||||
value={light.anchor}
|
value={light.anchor}
|
||||||
options={Object.keys(latest.light.anchor.values)}
|
options={Object.keys(latest.light.anchor.values)}
|
||||||
default={latest.light.anchor.default}
|
default={latest.light.anchor.default}
|
||||||
onChange={this.changeLightProperty.bind(this, "anchor")}
|
onChange={this.changeLightProperty.bind(this, "anchor")}
|
||||||
/>
|
/>
|
||||||
</Block>
|
|
||||||
|
|
||||||
<Block label={"Light color"} fieldSpec={latest.light.color}>
|
|
||||||
<FieldColor
|
<FieldColor
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
|
label={"Light color"}
|
||||||
|
fieldSpec={latest.light.color}
|
||||||
value={light.color}
|
value={light.color}
|
||||||
default={latest.light.color.default}
|
default={latest.light.color.default}
|
||||||
onChange={this.changeLightProperty.bind(this, "color")}
|
onChange={this.changeLightProperty.bind(this, "color")}
|
||||||
/>
|
/>
|
||||||
</Block>
|
|
||||||
|
|
||||||
<Block label={"Light intensity"} fieldSpec={latest.light.intensity}>
|
|
||||||
<FieldNumber
|
<FieldNumber
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
|
label={"Light intensity"}
|
||||||
|
fieldSpec={latest.light.intensity}
|
||||||
value={light.intensity}
|
value={light.intensity}
|
||||||
default={latest.light.intensity.default}
|
default={latest.light.intensity.default}
|
||||||
onChange={this.changeLightProperty.bind(this, "intensity")}
|
onChange={this.changeLightProperty.bind(this, "intensity")}
|
||||||
/>
|
/>
|
||||||
</Block>
|
|
||||||
|
|
||||||
<Block label={"Light position"} fieldSpec={latest.light.position}>
|
|
||||||
<FieldArray
|
<FieldArray
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
|
label={"Light position"}
|
||||||
|
fieldSpec={latest.light.position}
|
||||||
type="number"
|
type="number"
|
||||||
length={latest.light.position.length}
|
length={latest.light.position.length}
|
||||||
value={light.position}
|
value={light.position}
|
||||||
default={latest.light.position.default}
|
default={latest.light.position.default}
|
||||||
onChange={this.changeLightProperty.bind(this, "position")}
|
onChange={this.changeLightProperty.bind(this, "position")}
|
||||||
/>
|
/>
|
||||||
</Block>
|
|
||||||
|
|
||||||
<Block label={"Transition delay"} fieldSpec={latest.transition.delay}>
|
|
||||||
<FieldNumber
|
<FieldNumber
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
|
label={"Transition delay"}
|
||||||
|
fieldSpec={latest.transition.delay}
|
||||||
value={transition.delay}
|
value={transition.delay}
|
||||||
default={latest.transition.delay.default}
|
default={latest.transition.delay.default}
|
||||||
onChange={this.changeTransitionProperty.bind(this, "delay")}
|
onChange={this.changeTransitionProperty.bind(this, "delay")}
|
||||||
/>
|
/>
|
||||||
</Block>
|
|
||||||
|
|
||||||
<Block label={"Transition duration"} fieldSpec={latest.transition.duration}>
|
|
||||||
<FieldNumber
|
<FieldNumber
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
|
label={"Transition duration"}
|
||||||
|
fieldSpec={latest.transition.duration}
|
||||||
value={transition.duration}
|
value={transition.duration}
|
||||||
default={latest.transition.duration.default}
|
default={latest.transition.duration.default}
|
||||||
onChange={this.changeTransitionProperty.bind(this, "duration")}
|
onChange={this.changeTransitionProperty.bind(this, "duration")}
|
||||||
/>
|
/>
|
||||||
</Block>
|
|
||||||
|
|
||||||
<Block
|
|
||||||
label={fieldSpecAdditional.maputnik.style_renderer.label}
|
|
||||||
fieldSpec={fieldSpecAdditional.maputnik.style_renderer}
|
|
||||||
>
|
|
||||||
<FieldSelect {...inputProps}
|
<FieldSelect {...inputProps}
|
||||||
|
label={fieldSpecAdditional.maputnik.style_renderer.label}
|
||||||
|
fieldSpec={fieldSpecAdditional.maputnik.style_renderer}
|
||||||
data-wd-key="modal:settings.maputnik:renderer"
|
data-wd-key="modal:settings.maputnik:renderer"
|
||||||
options={[
|
options={[
|
||||||
['mbgljs', 'MapboxGL JS'],
|
['mbgljs', 'MapboxGL JS'],
|
||||||
|
@ -258,10 +247,6 @@ export default class ModalSettings extends React.Component {
|
||||||
value={metadata['maputnik:renderer'] || 'mbgljs'}
|
value={metadata['maputnik:renderer'] || 'mbgljs'}
|
||||||
onChange={onChangeMetadataProperty.bind(this, 'maputnik:renderer')}
|
onChange={onChangeMetadataProperty.bind(this, 'maputnik:renderer')}
|
||||||
/>
|
/>
|
||||||
</Block>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
import Button from './Button'
|
import InputButton from './InputButton'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import FieldString from './FieldString'
|
import FieldString from './FieldString'
|
||||||
import FieldSelect from './FieldSelect'
|
import FieldSelect from './FieldSelect'
|
||||||
|
@ -24,7 +24,7 @@ class PublicSource extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div className="maputnik-public-source">
|
return <div className="maputnik-public-source">
|
||||||
<Button
|
<InputButton
|
||||||
className="maputnik-public-source-select"
|
className="maputnik-public-source-select"
|
||||||
onClick={() => this.props.onSelect(this.props.id)}
|
onClick={() => this.props.onSelect(this.props.id)}
|
||||||
>
|
>
|
||||||
|
@ -34,7 +34,7 @@ class PublicSource extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
<span className="maputnik-space" />
|
<span className="maputnik-space" />
|
||||||
<MdAddCircleOutline />
|
<MdAddCircleOutline />
|
||||||
</Button>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,13 +83,13 @@ class ActiveModalSourcesTypeEditor extends React.Component {
|
||||||
<div className="maputnik-active-source-type-editor-header">
|
<div className="maputnik-active-source-type-editor-header">
|
||||||
<span className="maputnik-active-source-type-editor-header-id">#{this.props.sourceId}</span>
|
<span className="maputnik-active-source-type-editor-header-id">#{this.props.sourceId}</span>
|
||||||
<span className="maputnik-space" />
|
<span className="maputnik-space" />
|
||||||
<Button
|
<InputButton
|
||||||
className="maputnik-active-source-type-editor-header-delete"
|
className="maputnik-active-source-type-editor-header-delete"
|
||||||
onClick={()=> this.props.onDelete(this.props.sourceId)}
|
onClick={()=> this.props.onDelete(this.props.sourceId)}
|
||||||
style={{backgroundColor: 'transparent'}}
|
style={{backgroundColor: 'transparent'}}
|
||||||
>
|
>
|
||||||
<MdDelete />
|
<MdDelete />
|
||||||
</Button>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="maputnik-active-source-type-editor-content">
|
<div className="maputnik-active-source-type-editor-content">
|
||||||
<ModalSourcesTypeEditor
|
<ModalSourcesTypeEditor
|
||||||
|
@ -207,41 +207,41 @@ class AddSource extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div className="maputnik-add-source">
|
return <div className="maputnik-add-source">
|
||||||
<Block label={"Source ID"} fieldSpec={{doc: "Unique ID that identifies the source and is used in the layer to reference the source."}}>
|
<FieldString
|
||||||
<FieldString
|
label={"Source ID"}
|
||||||
value={this.state.sourceId}
|
fieldSpec={{doc: "Unique ID that identifies the source and is used in the layer to reference the source."}}
|
||||||
onChange={v => this.setState({ sourceId: v})}
|
value={this.state.sourceId}
|
||||||
/>
|
onChange={v => this.setState({ sourceId: v})}
|
||||||
</Block>
|
/>
|
||||||
<Block label={"Source Type"} fieldSpec={sourceTypeFieldSpec}>
|
<FieldSelect
|
||||||
<FieldSelect
|
label={"Source Type"}
|
||||||
options={[
|
fieldSpec={sourceTypeFieldSpec}
|
||||||
['geojson_json', 'GeoJSON (JSON)'],
|
options={[
|
||||||
['geojson_url', 'GeoJSON (URL)'],
|
['geojson_json', 'GeoJSON (JSON)'],
|
||||||
['tilejson_vector', 'Vector (TileJSON URL)'],
|
['geojson_url', 'GeoJSON (URL)'],
|
||||||
['tilexyz_vector', 'Vector (XYZ URLs)'],
|
['tilejson_vector', 'Vector (TileJSON URL)'],
|
||||||
['tilejson_raster', 'Raster (TileJSON URL)'],
|
['tilexyz_vector', 'Vector (XYZ URLs)'],
|
||||||
['tilexyz_raster', 'Raster (XYZ URL)'],
|
['tilejson_raster', 'Raster (TileJSON URL)'],
|
||||||
['tilejson_raster-dem', 'Raster DEM (TileJSON URL)'],
|
['tilexyz_raster', 'Raster (XYZ URL)'],
|
||||||
['tilexyz_raster-dem', 'Raster DEM (XYZ URLs)'],
|
['tilejson_raster-dem', 'Raster DEM (TileJSON URL)'],
|
||||||
['image', 'Image'],
|
['tilexyz_raster-dem', 'Raster DEM (XYZ URLs)'],
|
||||||
['video', 'Video'],
|
['image', 'Image'],
|
||||||
]}
|
['video', 'Video'],
|
||||||
onChange={mode => this.setState({mode: mode, source: this.defaultSource(mode)})}
|
]}
|
||||||
value={this.state.mode}
|
onChange={mode => this.setState({mode: mode, source: this.defaultSource(mode)})}
|
||||||
/>
|
value={this.state.mode}
|
||||||
</Block>
|
/>
|
||||||
<ModalSourcesTypeEditor
|
<ModalSourcesTypeEditor
|
||||||
onChange={this.onChangeSource}
|
onChange={this.onChangeSource}
|
||||||
mode={this.state.mode}
|
mode={this.state.mode}
|
||||||
source={this.state.source}
|
source={this.state.source}
|
||||||
/>
|
/>
|
||||||
<Button
|
<InputButton
|
||||||
className="maputnik-add-source-button"
|
className="maputnik-add-source-button"
|
||||||
onClick={this.onAdd}
|
onClick={this.onAdd}
|
||||||
>
|
>
|
||||||
Add Source
|
Add Source
|
||||||
</Button>
|
</InputButton>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,12 @@ import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import FieldString from './FieldString'
|
|
||||||
import FieldUrl from './FieldUrl'
|
import FieldUrl from './FieldUrl'
|
||||||
import FieldNumber from './FieldNumber'
|
import FieldNumber from './FieldNumber'
|
||||||
import FieldSelect from './FieldSelect'
|
import FieldSelect from './FieldSelect'
|
||||||
import FieldDynamicArray from './FieldDynamicArray'
|
import FieldDynamicArray from './FieldDynamicArray'
|
||||||
import FieldArray from './FieldArray'
|
import FieldArray from './FieldArray'
|
||||||
import FieldJsonEditor from './FieldJsonEditor'
|
import FieldJson from './FieldJson'
|
||||||
|
|
||||||
|
|
||||||
class TileJSONSourceEditor extends React.Component {
|
class TileJSONSourceEditor extends React.Component {
|
||||||
|
@ -20,15 +19,15 @@ class TileJSONSourceEditor extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div>
|
return <div>
|
||||||
<Block label={"TileJSON URL"} fieldSpec={latest.source_vector.url}>
|
<FieldUrl
|
||||||
<FieldUrl
|
label={"TileJSON URL"}
|
||||||
value={this.props.source.url}
|
fieldSpec={latest.source_vector.url}
|
||||||
onChange={url => this.props.onChange({
|
value={this.props.source.url}
|
||||||
...this.props.source,
|
onChange={url => this.props.onChange({
|
||||||
url: url
|
...this.props.source,
|
||||||
})}
|
url: url
|
||||||
/>
|
})}
|
||||||
</Block>
|
/>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -50,36 +49,36 @@ class TileURLSourceEditor extends React.Component {
|
||||||
|
|
||||||
renderTileUrls() {
|
renderTileUrls() {
|
||||||
const tiles = this.props.source.tiles || [];
|
const tiles = this.props.source.tiles || [];
|
||||||
return <Block label={"Tile URL"} fieldSpec={latest.source_vector.tiles}>
|
return <FieldDynamicArray
|
||||||
<FieldDynamicArray
|
label={"Tile URL"}
|
||||||
type="url"
|
fieldSpec={latest.source_vector.tiles}
|
||||||
value={tiles}
|
type="url"
|
||||||
onChange={this.changeTileUrls.bind(this)}
|
value={tiles}
|
||||||
/>
|
onChange={this.changeTileUrls.bind(this)}
|
||||||
</Block>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div>
|
return <div>
|
||||||
{this.renderTileUrls()}
|
{this.renderTileUrls()}
|
||||||
<Block label={"Min Zoom"} fieldSpec={latest.source_vector.minzoom}>
|
<FieldNumber
|
||||||
<FieldNumber
|
label={"Min Zoom"}
|
||||||
value={this.props.source.minzoom || 0}
|
fieldSpec={latest.source_vector.minzoom}
|
||||||
onChange={minzoom => this.props.onChange({
|
value={this.props.source.minzoom || 0}
|
||||||
...this.props.source,
|
onChange={minzoom => this.props.onChange({
|
||||||
minzoom: minzoom
|
...this.props.source,
|
||||||
})}
|
minzoom: minzoom
|
||||||
/>
|
})}
|
||||||
</Block>
|
/>
|
||||||
<Block label={"Max Zoom"} fieldSpec={latest.source_vector.maxzoom}>
|
<FieldNumber
|
||||||
<FieldNumber
|
label={"Max Zoom"}
|
||||||
value={this.props.source.maxzoom || 22}
|
fieldSpec={latest.source_vector.maxzoom}
|
||||||
onChange={maxzoom => this.props.onChange({
|
value={this.props.source.maxzoom || 22}
|
||||||
...this.props.source,
|
onChange={maxzoom => this.props.onChange({
|
||||||
maxzoom: maxzoom
|
...this.props.source,
|
||||||
})}
|
maxzoom: maxzoom
|
||||||
/>
|
})}
|
||||||
</Block>
|
/>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -104,26 +103,26 @@ class ImageSourceEditor extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<Block label={"Image URL"} fieldSpec={latest.source_image.url}>
|
<FieldUrl
|
||||||
<FieldUrl
|
label={"Image URL"}
|
||||||
value={this.props.source.url}
|
fieldSpec={latest.source_image.url}
|
||||||
onChange={url => this.props.onChange({
|
value={this.props.source.url}
|
||||||
...this.props.source,
|
onChange={url => this.props.onChange({
|
||||||
url,
|
...this.props.source,
|
||||||
})}
|
url,
|
||||||
/>
|
})}
|
||||||
</Block>
|
/>
|
||||||
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
|
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
|
||||||
return (
|
return (
|
||||||
<Block label={`Coord ${label}`} key={label}>
|
<FieldArray
|
||||||
<FieldArray
|
label={`Coord ${label}`}
|
||||||
length={2}
|
key={label}
|
||||||
type="number"
|
length={2}
|
||||||
value={this.props.source.coordinates[idx]}
|
type="number"
|
||||||
default={[0, 0]}
|
value={this.props.source.coordinates[idx]}
|
||||||
onChange={(val) => changeCoord(idx, val)}
|
default={[0, 0]}
|
||||||
/>
|
onChange={(val) => changeCoord(idx, val)}
|
||||||
</Block>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
@ -155,25 +154,25 @@ class VideoSourceEditor extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<Block label={"Video URL"} fieldSpec={latest.source_video.urls}>
|
<FieldDynamicArray
|
||||||
<FieldDynamicArray
|
label={"Video URL"}
|
||||||
type="string"
|
fieldSpec={latest.source_video.urls}
|
||||||
value={this.props.source.urls}
|
type="string"
|
||||||
default={""}
|
value={this.props.source.urls}
|
||||||
onChange={changeUrls}
|
default={""}
|
||||||
/>
|
onChange={changeUrls}
|
||||||
</Block>
|
/>
|
||||||
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
|
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
|
||||||
return (
|
return (
|
||||||
<Block label={`Coord ${label}`} key={label}>
|
<FieldArray
|
||||||
<FieldArray
|
label={`Coord ${label}`}
|
||||||
length={2}
|
key={label}
|
||||||
type="number"
|
length={2}
|
||||||
value={this.props.source.coordinates[idx]}
|
type="number"
|
||||||
default={[0, 0]}
|
value={this.props.source.coordinates[idx]}
|
||||||
onChange={val => changeCoord(idx, val)}
|
default={[0, 0]}
|
||||||
/>
|
onChange={val => changeCoord(idx, val)}
|
||||||
</Block>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
@ -187,15 +186,15 @@ class GeoJSONSourceUrlEditor extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <Block label={"GeoJSON URL"} fieldSpec={latest.source_geojson.data}>
|
return <FieldUrl
|
||||||
<FieldUrl
|
label={"GeoJSON URL"}
|
||||||
value={this.props.source.data}
|
fieldSpec={latest.source_geojson.data}
|
||||||
onChange={data => this.props.onChange({
|
value={this.props.source.data}
|
||||||
...this.props.source,
|
onChange={data => this.props.onChange({
|
||||||
data: data
|
...this.props.source,
|
||||||
})}
|
data: data
|
||||||
/>
|
})}
|
||||||
</Block>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,7 +206,7 @@ class GeoJSONSourceFieldJsonEditor extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <Block label={"GeoJSON"} fieldSpec={latest.source_geojson.data}>
|
return <Block label={"GeoJSON"} fieldSpec={latest.source_geojson.data}>
|
||||||
<FieldJsonEditor
|
<FieldJson
|
||||||
layer={this.props.source.data}
|
layer={this.props.source.data}
|
||||||
maxHeight={200}
|
maxHeight={200}
|
||||||
mode={{
|
mode={{
|
||||||
|
@ -247,16 +246,16 @@ export default class ModalSourcesTypeEditor extends React.Component {
|
||||||
case 'tilexyz_raster': return <TileURLSourceEditor {...commonProps} />
|
case 'tilexyz_raster': return <TileURLSourceEditor {...commonProps} />
|
||||||
case 'tilejson_raster-dem': return <TileJSONSourceEditor {...commonProps} />
|
case 'tilejson_raster-dem': return <TileJSONSourceEditor {...commonProps} />
|
||||||
case 'tilexyz_raster-dem': return <TileURLSourceEditor {...commonProps}>
|
case 'tilexyz_raster-dem': return <TileURLSourceEditor {...commonProps}>
|
||||||
<Block label={"Encoding"} fieldSpec={latest.source_raster_dem.encoding}>
|
<FieldSelect
|
||||||
<FieldSelect
|
label={"Encoding"}
|
||||||
options={Object.keys(latest.source_raster_dem.encoding.values)}
|
fieldSpec={latest.source_raster_dem.encoding}
|
||||||
onChange={encoding => this.props.onChange({
|
options={Object.keys(latest.source_raster_dem.encoding.values)}
|
||||||
...this.props.source,
|
onChange={encoding => this.props.onChange({
|
||||||
encoding: encoding
|
...this.props.source,
|
||||||
})}
|
encoding: encoding
|
||||||
value={this.props.source.encoding || latest.source_raster_dem.encoding.default}
|
})}
|
||||||
/>
|
value={this.props.source.encoding || latest.source_raster_dem.encoding.default}
|
||||||
</Block>
|
/>
|
||||||
</TileURLSourceEditor>
|
</TileURLSourceEditor>
|
||||||
case 'image': return <ImageSourceEditor {...commonProps} />
|
case 'image': return <ImageSourceEditor {...commonProps} />
|
||||||
case 'video': return <VideoSourceEditor {...commonProps} />
|
case 'video': return <VideoSourceEditor {...commonProps} />
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import Button from './Button'
|
import InputButton from './InputButton'
|
||||||
import Modal from './Modal'
|
import Modal from './Modal'
|
||||||
|
|
||||||
import logoImage from 'maputnik-design/logos/logo-color.svg'
|
import logoImage from 'maputnik-design/logos/logo-color.svg'
|
||||||
|
@ -26,10 +26,10 @@ export default class ModalSurvey extends React.Component {
|
||||||
title="Maputnik Survey"
|
title="Maputnik Survey"
|
||||||
>
|
>
|
||||||
<div className="maputnik-modal-survey">
|
<div className="maputnik-modal-survey">
|
||||||
<div className="maputnik-modal-survey__logo" dangerouslySetInnerHTML={{__html: logoImage}} />
|
<img src={logoImage} className="maputnik-modal-survey__logo" />
|
||||||
<h1>You + Maputnik = Maputnik better for you</h1>
|
<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>
|
<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>
|
<InputButton onClick={this.onClick} className="maputnik-big-button maputnik-white-button maputnik-wide-button">Take the Maputnik Survey</InputButton>
|
||||||
<p className="maputnik-modal-survey__footnote">It takes 7 minutes, tops! Every question is optional.</p>
|
<p className="maputnik-modal-survey__footnote">It takes 7 minutes, tops! Every question is optional.</p>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -2,9 +2,9 @@ import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import { otherFilterOps } from '../libs/filterops.js'
|
import { otherFilterOps } from '../libs/filterops.js'
|
||||||
import FieldString from './FieldString'
|
import InputString from './InputString'
|
||||||
import FieldAutocomplete from './FieldAutocomplete'
|
import InputAutocomplete from './InputAutocomplete'
|
||||||
import FieldSelect from './FieldSelect'
|
import InputSelect from './InputSelect'
|
||||||
|
|
||||||
function tryParseInt(v) {
|
function tryParseInt(v) {
|
||||||
if (v === '') return v
|
if (v === '') return v
|
||||||
|
@ -64,14 +64,14 @@ export default class SingleFilterEditor extends React.Component {
|
||||||
|
|
||||||
return <div className="maputnik-filter-editor-single">
|
return <div className="maputnik-filter-editor-single">
|
||||||
<div className="maputnik-filter-editor-property">
|
<div className="maputnik-filter-editor-property">
|
||||||
<FieldAutocomplete
|
<InputAutocomplete
|
||||||
value={propertyName}
|
value={propertyName}
|
||||||
options={Object.keys(this.props.properties).map(propName => [propName, propName])}
|
options={Object.keys(this.props.properties).map(propName => [propName, propName])}
|
||||||
onChange={newPropertyName => this.onFilterPartChanged(filterOp, newPropertyName, filterArgs)}
|
onChange={newPropertyName => this.onFilterPartChanged(filterOp, newPropertyName, filterArgs)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="maputnik-filter-editor-operator">
|
<div className="maputnik-filter-editor-operator">
|
||||||
<FieldSelect
|
<InputSelect
|
||||||
value={filterOp}
|
value={filterOp}
|
||||||
onChange={newFilterOp => this.onFilterPartChanged(newFilterOp, propertyName, filterArgs)}
|
onChange={newFilterOp => this.onFilterPartChanged(newFilterOp, propertyName, filterArgs)}
|
||||||
options={otherFilterOps}
|
options={otherFilterOps}
|
||||||
|
@ -79,7 +79,7 @@ export default class SingleFilterEditor extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
{filterArgs.length > 0 &&
|
{filterArgs.length > 0 &&
|
||||||
<div className="maputnik-filter-editor-args">
|
<div className="maputnik-filter-editor-args">
|
||||||
<FieldString
|
<InputString
|
||||||
value={filterArgs.join(',')}
|
value={filterArgs.join(',')}
|
||||||
onChange={ v=> this.onFilterPartChanged(filterOp, propertyName, v.split(','))}
|
onChange={ v=> this.onFilterPartChanged(filterOp, propertyName, v.split(','))}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,132 +1,41 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
import Block from './Block'
|
||||||
|
import InputSpec from './InputSpec'
|
||||||
|
import Fieldset from './Fieldset'
|
||||||
|
|
||||||
import FieldColor from './FieldColor'
|
|
||||||
import FieldNumber from './FieldNumber'
|
|
||||||
import FieldCheckbox from './FieldCheckbox'
|
|
||||||
import FieldString from './FieldString'
|
|
||||||
import FieldSelect from './FieldSelect'
|
|
||||||
import FieldMultiInput from './FieldMultiInput'
|
|
||||||
import FieldArray from './FieldArray'
|
|
||||||
import FieldDynamicArray from './FieldDynamicArray'
|
|
||||||
import FieldFont from './FieldFont'
|
|
||||||
import FieldSymbol from './FieldSymbol'
|
|
||||||
import FieldEnum from './FieldEnum'
|
|
||||||
import capitalize from 'lodash.capitalize'
|
|
||||||
|
|
||||||
const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image']
|
const typeMap = {
|
||||||
|
color: Block,
|
||||||
|
enum: Fieldset,
|
||||||
|
number: Block,
|
||||||
|
boolean: Block,
|
||||||
|
array: Fieldset,
|
||||||
|
resolvedImage: Block,
|
||||||
|
number: Block,
|
||||||
|
string: Block
|
||||||
|
};
|
||||||
|
|
||||||
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 {
|
export default class SpecField extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onChange: PropTypes.func.isRequired,
|
...InputSpec.propTypes,
|
||||||
fieldName: PropTypes.string.isRequired,
|
name: PropTypes.string,
|
||||||
fieldSpec: PropTypes.object.isRequired,
|
|
||||||
value: PropTypes.oneOfType([
|
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.number,
|
|
||||||
PropTypes.array,
|
|
||||||
PropTypes.bool
|
|
||||||
]),
|
|
||||||
/** Override the style of the field */
|
|
||||||
style: PropTypes.object,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const commonProps = {
|
const {props} = this;
|
||||||
style: this.props.style,
|
|
||||||
value: this.props.value,
|
const fieldType = props.fieldSpec.type;
|
||||||
default: this.props.fieldSpec.default,
|
let TypeBlock = typeMap[fieldType];
|
||||||
name: this.props.fieldName,
|
|
||||||
onChange: newValue => this.props.onChange(this.props.fieldName, newValue)
|
if (!TypeBlock) {
|
||||||
|
console.warn("No such type for '%s'", fieldType);
|
||||||
|
TypeBlock = Block;
|
||||||
}
|
}
|
||||||
|
|
||||||
function childNodes() {
|
return <TypeBlock label={props.label}>
|
||||||
switch(this.props.fieldSpec.type) {
|
<InputSpec {...props} />
|
||||||
case 'number': return (
|
</TypeBlock>
|
||||||
<FieldNumber
|
|
||||||
{...commonProps}
|
|
||||||
min={this.props.fieldSpec.minimum}
|
|
||||||
max={this.props.fieldSpec.maximum}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
case 'enum':
|
|
||||||
const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)])
|
|
||||||
|
|
||||||
return <FieldEnum
|
|
||||||
{...commonProps}
|
|
||||||
options={options}
|
|
||||||
/>
|
|
||||||
case 'resolvedImage':
|
|
||||||
case 'formatted':
|
|
||||||
case 'string':
|
|
||||||
if(iconProperties.indexOf(this.props.fieldName) >= 0) {
|
|
||||||
return <FieldSymbol
|
|
||||||
{...commonProps}
|
|
||||||
icons={this.props.fieldSpec.values}
|
|
||||||
/>
|
|
||||||
} else {
|
|
||||||
return <FieldString
|
|
||||||
{...commonProps}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
case 'color': return (
|
|
||||||
<FieldColor
|
|
||||||
{...commonProps}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
case 'boolean': return (
|
|
||||||
<FieldCheckbox
|
|
||||||
{...commonProps}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
case 'array':
|
|
||||||
if(this.props.fieldName === 'text-font') {
|
|
||||||
return <FieldFont
|
|
||||||
{...commonProps}
|
|
||||||
fonts={this.props.fieldSpec.values}
|
|
||||||
/>
|
|
||||||
} else {
|
|
||||||
if (this.props.fieldSpec.length) {
|
|
||||||
return <FieldArray
|
|
||||||
{...commonProps}
|
|
||||||
type={this.props.fieldSpec.value}
|
|
||||||
length={this.props.fieldSpec.length}
|
|
||||||
/>
|
|
||||||
} else {
|
|
||||||
return <FieldDynamicArray
|
|
||||||
{...commonProps}
|
|
||||||
fieldSpec={this.props.fieldSpec}
|
|
||||||
type={this.props.fieldSpec.value}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default: return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div data-wd-key={"spec-field:"+this.props.fieldName}>
|
|
||||||
{childNodes.call(this)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,12 @@ import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
|
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
|
||||||
|
|
||||||
import Button from './Button'
|
import InputButton from './InputButton'
|
||||||
import SpecField from './SpecField'
|
import InputSpec from './InputSpec'
|
||||||
import FieldNumber from './FieldNumber'
|
import InputNumber from './InputNumber'
|
||||||
import FieldString from './FieldString'
|
import InputString from './InputString'
|
||||||
import FieldSelect from './FieldSelect'
|
import InputSelect from './InputSelect'
|
||||||
import Doc from './Doc'
|
import FieldDocLabel from './FieldDocLabel'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import docUid from '../libs/document-uid'
|
import docUid from '../libs/document-uid'
|
||||||
import sortNumerically from '../libs/sort-numerically'
|
import sortNumerically from '../libs/sort-numerically'
|
||||||
|
@ -149,8 +149,14 @@ export default class DataProperty extends React.Component {
|
||||||
|
|
||||||
changeStop(changeIdx, stopData, value) {
|
changeStop(changeIdx, stopData, value) {
|
||||||
const stops = this.props.value.stops.slice(0)
|
const stops = this.props.value.stops.slice(0)
|
||||||
const changedStop = stopData.zoom === undefined ? stopData.value : stopData
|
// const changedStop = stopData.zoom === undefined ? stopData.value : stopData
|
||||||
stops[changeIdx] = [changedStop, value]
|
stops[changeIdx] = [
|
||||||
|
{
|
||||||
|
...stopData,
|
||||||
|
zoom: (stopData.zoom === undefined) ? 0 : stopData.zoom,
|
||||||
|
},
|
||||||
|
value
|
||||||
|
];
|
||||||
|
|
||||||
const orderedStops = this.orderStopsByZoom(stops);
|
const orderedStops = this.orderStopsByZoom(stops);
|
||||||
|
|
||||||
|
@ -188,6 +194,7 @@ export default class DataProperty extends React.Component {
|
||||||
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
|
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop.bind(this, idx)} />
|
||||||
|
|
||||||
const dataProps = {
|
const dataProps = {
|
||||||
|
'aria-label': "Input value",
|
||||||
label: "Data value",
|
label: "Data value",
|
||||||
value: dataLevel,
|
value: dataLevel,
|
||||||
onChange: newData => this.changeStop(idx, { zoom: zoomLevel, value: newData }, value)
|
onChange: newData => this.changeStop(idx, { zoom: zoomLevel, value: newData }, value)
|
||||||
|
@ -195,16 +202,17 @@ export default class DataProperty extends React.Component {
|
||||||
|
|
||||||
let dataInput;
|
let dataInput;
|
||||||
if(this.props.value.type === "categorical") {
|
if(this.props.value.type === "categorical") {
|
||||||
dataInput = <FieldString {...dataProps} />
|
dataInput = <InputString {...dataProps} />
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
dataInput = <FieldNumber {...dataProps} />
|
dataInput = <InputNumber {...dataProps} />
|
||||||
}
|
}
|
||||||
|
|
||||||
let zoomInput = null;
|
let zoomInput = null;
|
||||||
if(zoomLevel !== undefined) {
|
if(zoomLevel !== undefined) {
|
||||||
zoomInput = <div className="maputnik-data-spec-property-stop-edit">
|
zoomInput = <div>
|
||||||
<FieldNumber
|
<InputNumber
|
||||||
|
aria-label="Zoom"
|
||||||
value={zoomLevel}
|
value={zoomLevel}
|
||||||
onChange={newZoom => this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)}
|
onChange={newZoom => this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)}
|
||||||
min={0}
|
min={0}
|
||||||
|
@ -223,6 +231,27 @@ export default class DataProperty extends React.Component {
|
||||||
}).join("");
|
}).join("");
|
||||||
const error = message ? {message} : undefined;
|
const error = message ? {message} : undefined;
|
||||||
|
|
||||||
|
return <tr key={key}>
|
||||||
|
<td>
|
||||||
|
{zoomInput}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{dataInput}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<InputSpec
|
||||||
|
aria-label="Output value"
|
||||||
|
fieldName={this.props.fieldName}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
value={value}
|
||||||
|
onChange={(_, newValue) => this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{deleteStopBtn}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
return <Block
|
return <Block
|
||||||
error={error}
|
error={error}
|
||||||
key={key}
|
key={key}
|
||||||
|
@ -234,7 +263,7 @@ export default class DataProperty extends React.Component {
|
||||||
{dataInput}
|
{dataInput}
|
||||||
</div>
|
</div>
|
||||||
<div className="maputnik-data-spec-property-stop-value">
|
<div className="maputnik-data-spec-property-stop-value">
|
||||||
<SpecField
|
<InputSpec
|
||||||
fieldName={this.props.fieldName}
|
fieldName={this.props.fieldName}
|
||||||
fieldSpec={this.props.fieldSpec}
|
fieldSpec={this.props.fieldSpec}
|
||||||
value={value}
|
value={value}
|
||||||
|
@ -246,74 +275,83 @@ export default class DataProperty extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="maputnik-data-spec-block">
|
return <div className="maputnik-data-spec-block">
|
||||||
<div className="maputnik-data-spec-property">
|
<fieldset className="maputnik-data-spec-property">
|
||||||
<Block
|
<legend>{labelFromFieldName(this.props.fieldName)}</legend>
|
||||||
fieldSpec={this.props.fieldSpec}
|
<div className="maputnik-data-fieldset-inner">
|
||||||
label={labelFromFieldName(this.props.fieldName)}
|
<Block
|
||||||
>
|
label={"Function"}
|
||||||
<div className="maputnik-data-spec-property-group">
|
>
|
||||||
<Doc
|
|
||||||
label="Type"
|
|
||||||
/>
|
|
||||||
<div className="maputnik-data-spec-property-input">
|
<div className="maputnik-data-spec-property-input">
|
||||||
<FieldSelect
|
<InputSelect
|
||||||
value={this.props.value.type}
|
value={this.props.value.type}
|
||||||
onChange={propVal => this.changeDataProperty("type", propVal)}
|
onChange={propVal => this.changeDataProperty("type", propVal)}
|
||||||
title={"Select a type of data scale (default is 'categorical')."}
|
title={"Select a type of data scale (default is 'categorical')."}
|
||||||
options={this.getDataFunctionTypes(this.props.fieldSpec)}
|
options={this.getDataFunctionTypes(this.props.fieldSpec)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Block>
|
||||||
<div className="maputnik-data-spec-property-group">
|
<Block
|
||||||
<Doc
|
label={"Property"}
|
||||||
label="Property"
|
>
|
||||||
/>
|
|
||||||
<div className="maputnik-data-spec-property-input">
|
<div className="maputnik-data-spec-property-input">
|
||||||
<FieldString
|
<InputString
|
||||||
value={this.props.value.property}
|
value={this.props.value.property}
|
||||||
title={"Input a data property to base styles off of."}
|
title={"Input a data property to base styles off of."}
|
||||||
onChange={propVal => this.changeDataProperty("property", propVal)}
|
onChange={propVal => this.changeDataProperty("property", propVal)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Block>
|
||||||
{dataFields &&
|
{dataFields &&
|
||||||
<div className="maputnik-data-spec-property-group">
|
<Block
|
||||||
<Doc
|
label={"Default"}
|
||||||
label="Default"
|
>
|
||||||
|
<InputSpec
|
||||||
|
fieldName={this.props.fieldName}
|
||||||
|
fieldSpec={this.props.fieldSpec}
|
||||||
|
value={this.props.value.default}
|
||||||
|
onChange={(_, propVal) => this.changeDataProperty("default", propVal)}
|
||||||
/>
|
/>
|
||||||
<div className="maputnik-data-spec-property-input">
|
</Block>
|
||||||
<SpecField
|
}
|
||||||
fieldName={this.props.fieldName}
|
{dataFields &&
|
||||||
fieldSpec={this.props.fieldSpec}
|
<div className="maputnik-function-stop">
|
||||||
value={this.props.value.default}
|
<table className="maputnik-function-stop-table">
|
||||||
onChange={(_, propVal) => this.changeDataProperty("default", propVal)}
|
<caption>Stops</caption>
|
||||||
/>
|
<thead>
|
||||||
</div>
|
<tr>
|
||||||
|
<th>Zoom</th>
|
||||||
|
<th>Input value</th>
|
||||||
|
<th rowSpan="2">Output value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{dataFields}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</Block>
|
<div className="maputnik-toolbox">
|
||||||
</div>
|
{dataFields &&
|
||||||
{dataFields &&
|
<InputButton
|
||||||
<>
|
className="maputnik-add-stop"
|
||||||
{dataFields}
|
onClick={this.props.onAddStop.bind(this)}
|
||||||
<Button
|
>
|
||||||
className="maputnik-add-stop"
|
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||||
onClick={this.props.onAddStop.bind(this)}
|
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
||||||
>
|
</svg> Add stop
|
||||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
</InputButton>
|
||||||
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
}
|
||||||
</svg> Add stop
|
<InputButton
|
||||||
</Button>
|
className="maputnik-add-stop"
|
||||||
</>
|
onClick={this.props.onExpressionClick.bind(this)}
|
||||||
}
|
>
|
||||||
<Button
|
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||||
className="maputnik-add-stop"
|
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||||
onClick={this.props.onExpressionClick.bind(this)}
|
</svg> Convert to expression
|
||||||
>
|
</InputButton>
|
||||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
</div>
|
||||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
</div>
|
||||||
</svg> Convert to expression
|
</fieldset>
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import Button from './Button'
|
import InputButton from './InputButton'
|
||||||
import {MdDelete} from 'react-icons/md'
|
import {MdDelete} from 'react-icons/md'
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,12 +11,12 @@ export default class DeleteStopButton extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <Button
|
return <InputButton
|
||||||
className="maputnik-delete-stop"
|
className="maputnik-delete-stop"
|
||||||
onClick={this.props.onClick}
|
onClick={this.props.onClick}
|
||||||
title={"Remove zoom level from stop"}
|
title={"Remove zoom level from stop"}
|
||||||
>
|
>
|
||||||
<MdDelete />
|
<MdDelete />
|
||||||
</Button>
|
</InputButton>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,13 @@ import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
import Button from './Button'
|
import InputButton from './InputButton'
|
||||||
import {MdDelete, MdUndo} from 'react-icons/md'
|
import {MdDelete, MdUndo} from 'react-icons/md'
|
||||||
import FieldString from './FieldString'
|
import FieldString from './FieldString'
|
||||||
|
|
||||||
import labelFromFieldName from './_labelFromFieldName'
|
import labelFromFieldName from './_labelFromFieldName'
|
||||||
import stringifyPretty from 'json-stringify-pretty-compact'
|
import stringifyPretty from 'json-stringify-pretty-compact'
|
||||||
import FieldJsonEditor from './FieldJsonEditor'
|
import FieldJson from './FieldJson'
|
||||||
|
|
||||||
|
|
||||||
export default class ExpressionProperty extends React.Component {
|
export default class ExpressionProperty extends React.Component {
|
||||||
|
@ -59,7 +59,7 @@ export default class ExpressionProperty extends React.Component {
|
||||||
const deleteStopBtn = (
|
const deleteStopBtn = (
|
||||||
<>
|
<>
|
||||||
{this.props.onUndo &&
|
{this.props.onUndo &&
|
||||||
<Button
|
<InputButton
|
||||||
key="undo_action"
|
key="undo_action"
|
||||||
onClick={this.props.onUndo}
|
onClick={this.props.onUndo}
|
||||||
disabled={undoDisabled}
|
disabled={undoDisabled}
|
||||||
|
@ -67,16 +67,16 @@ export default class ExpressionProperty extends React.Component {
|
||||||
title="Revert from expression"
|
title="Revert from expression"
|
||||||
>
|
>
|
||||||
<MdUndo />
|
<MdUndo />
|
||||||
</Button>
|
</InputButton>
|
||||||
}
|
}
|
||||||
<Button
|
<InputButton
|
||||||
key="delete_action"
|
key="delete_action"
|
||||||
onClick={this.props.onDelete}
|
onClick={this.props.onDelete}
|
||||||
className="maputnik-delete-stop"
|
className="maputnik-delete-stop"
|
||||||
title="Delete expression"
|
title="Delete expression"
|
||||||
>
|
>
|
||||||
<MdDelete />
|
<MdDelete />
|
||||||
</Button>
|
</InputButton>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -114,7 +114,7 @@ export default class ExpressionProperty extends React.Component {
|
||||||
action={deleteStopBtn}
|
action={deleteStopBtn}
|
||||||
wideMode={true}
|
wideMode={true}
|
||||||
>
|
>
|
||||||
<FieldJsonEditor
|
<FieldJson
|
||||||
mode={{name: "mgl"}}
|
mode={{name: "mgl"}}
|
||||||
lint={{
|
lint={{
|
||||||
context: "expression",
|
context: "expression",
|
||||||
|
|
32
src/components/_FieldComment.jsx
Normal file
32
src/components/_FieldComment.jsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import Block from './Block'
|
||||||
|
import FieldString from './FieldString'
|
||||||
|
|
||||||
|
export default class BlockComment extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: PropTypes.string,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const fieldSpec = {
|
||||||
|
doc: "Comments for the current layer. This is non-standard and not in the spec."
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Block
|
||||||
|
label={"Comments"}
|
||||||
|
fieldSpec={fieldSpec}
|
||||||
|
data-wd-key="layer-comment"
|
||||||
|
>
|
||||||
|
<FieldString
|
||||||
|
multi={true}
|
||||||
|
value={this.props.value}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
default="Comment..."
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import Block from './Block'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import FieldAutocomplete from './FieldAutocomplete'
|
import FieldAutocomplete from './FieldAutocomplete'
|
||||||
|
|
||||||
|
@ -39,17 +40,22 @@ export default class FieldFont extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const inputs = this.values.map((value, i) => {
|
const inputs = this.values.map((value, i) => {
|
||||||
return <FieldAutocomplete
|
return <li
|
||||||
key={i}
|
key={i}
|
||||||
value={value}
|
>
|
||||||
options={this.props.fonts.map(f => [f, f])}
|
<FieldAutocomplete
|
||||||
onChange={this.changeFont.bind(this, i)}
|
value={value}
|
||||||
/>
|
options={this.props.fonts.map(f => [f, f])}
|
||||||
|
onChange={this.changeFont.bind(this, i)}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
})
|
})
|
||||||
|
|
||||||
return <div className="maputnik-font">
|
return <Block label={this.props.label}>
|
||||||
{inputs}
|
<ul className="maputnik-font">
|
||||||
</div>
|
{inputs}
|
||||||
|
</ul>
|
||||||
|
</Block>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
28
src/components/_FieldId.jsx
Normal file
28
src/components/_FieldId.jsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||||
|
import Block from './Block'
|
||||||
|
import FieldString from './FieldString'
|
||||||
|
|
||||||
|
export default class BlockId extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
wdKey: PropTypes.string.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <Block label={"ID"} fieldSpec={latest.layer.id}
|
||||||
|
data-wd-key={this.props.wdKey}
|
||||||
|
error={this.props.error}
|
||||||
|
>
|
||||||
|
<FieldString
|
||||||
|
value={this.props.value}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
31
src/components/_FieldMaxZoom.jsx
Normal file
31
src/components/_FieldMaxZoom.jsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||||
|
import Block from './Block'
|
||||||
|
import FieldNumber from './FieldNumber'
|
||||||
|
|
||||||
|
export default class BlockMaxZoom extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: PropTypes.number,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <Block label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
|
||||||
|
error={this.props.error}
|
||||||
|
data-wd-key="max-zoom"
|
||||||
|
>
|
||||||
|
<FieldNumber
|
||||||
|
allowRange={true}
|
||||||
|
value={this.props.value}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
min={latest.layer.maxzoom.minimum}
|
||||||
|
max={latest.layer.maxzoom.maximum}
|
||||||
|
default={latest.layer.maxzoom.maximum}
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
31
src/components/_FieldMinZoom.jsx
Normal file
31
src/components/_FieldMinZoom.jsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||||
|
import Block from './Block'
|
||||||
|
import FieldNumber from './FieldNumber'
|
||||||
|
|
||||||
|
export default class BlockMinZoom extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: PropTypes.number,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <Block label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
|
||||||
|
error={this.props.error}
|
||||||
|
data-wd-key="min-zoom"
|
||||||
|
>
|
||||||
|
<FieldNumber
|
||||||
|
allowRange={true}
|
||||||
|
value={this.props.value}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
min={latest.layer.minzoom.minimum}
|
||||||
|
max={latest.layer.minzoom.maximum}
|
||||||
|
default={latest.layer.minzoom.minimum}
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
37
src/components/_FieldSource.jsx
Normal file
37
src/components/_FieldSource.jsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||||
|
import Block from './Block'
|
||||||
|
import FieldAutocomplete from './FieldAutocomplete'
|
||||||
|
|
||||||
|
export default class BlockSource extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: PropTypes.string,
|
||||||
|
wdKey: PropTypes.string,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
sourceIds: PropTypes.array,
|
||||||
|
error: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
onChange: () => {},
|
||||||
|
sourceIds: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <Block
|
||||||
|
label={"Source"}
|
||||||
|
fieldSpec={latest.layer.source}
|
||||||
|
error={this.props.error}
|
||||||
|
data-wd-key={this.props.wdKey}
|
||||||
|
>
|
||||||
|
<FieldAutocomplete
|
||||||
|
value={this.props.value}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
options={this.props.sourceIds.map(src => [src, src])}
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
35
src/components/_FieldSourceLayer.jsx
Normal file
35
src/components/_FieldSourceLayer.jsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||||
|
import Block from './Block'
|
||||||
|
import FieldAutocomplete from './FieldAutocomplete'
|
||||||
|
|
||||||
|
export default class BlockSourceLayer extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
value: PropTypes.string,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
sourceLayerIds: PropTypes.array,
|
||||||
|
isFixed: PropTypes.bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
onChange: () => {},
|
||||||
|
sourceLayerIds: [],
|
||||||
|
isFixed: false
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <Block label={"Source Layer"} fieldSpec={latest.layer['source-layer']}
|
||||||
|
data-wd-key="layer-source-layer"
|
||||||
|
>
|
||||||
|
<FieldAutocomplete
|
||||||
|
keepMenuWithinWindowBounds={!!this.props.isFixed}
|
||||||
|
value={this.props.value}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
options={this.props.sourceLayerIds.map(l => [l, l])}
|
||||||
|
/>
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
53
src/components/_FieldType.jsx
Normal file
53
src/components/_FieldType.jsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||||
|
import Block from './Block'
|
||||||
|
import FieldSelect from './FieldSelect'
|
||||||
|
import FieldString from './FieldString'
|
||||||
|
|
||||||
|
export default class BlockType 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 <Block label={"Type"} fieldSpec={latest.layer.type}
|
||||||
|
data-wd-key={this.props.wdKey}
|
||||||
|
error={this.props.error}
|
||||||
|
>
|
||||||
|
{this.props.disabled &&
|
||||||
|
<FieldString
|
||||||
|
value={this.props.value}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{!this.props.disabled &&
|
||||||
|
<FieldSelect
|
||||||
|
options={[
|
||||||
|
['background', 'Background'],
|
||||||
|
['fill', 'Fill'],
|
||||||
|
['line', 'Line'],
|
||||||
|
['symbol', 'Symbol'],
|
||||||
|
['raster', 'Raster'],
|
||||||
|
['circle', 'Circle'],
|
||||||
|
['fill-extrusion', 'Fill Extrusion'],
|
||||||
|
['hillshade', 'Hillshade'],
|
||||||
|
['heatmap', 'Heatmap'],
|
||||||
|
]}
|
||||||
|
onChange={this.props.onChange}
|
||||||
|
value={this.props.value}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</Block>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
import Button from './Button'
|
import InputButton from './InputButton'
|
||||||
import {MdFunctions, MdInsertChart} from 'react-icons/md'
|
import {MdFunctions, MdInsertChart} from 'react-icons/md'
|
||||||
import {mdiFunctionVariant} from '@mdi/js';
|
import {mdiFunctionVariant} from '@mdi/js';
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ function isExpression(value, fieldSpec={}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class FunctionButtons extends React.Component {
|
export default class FunctionInputButtons extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
fieldSpec: PropTypes.object,
|
fieldSpec: PropTypes.object,
|
||||||
onZoomClick: PropTypes.func,
|
onZoomClick: PropTypes.func,
|
||||||
|
@ -33,11 +33,11 @@ export default class FunctionButtons extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let makeZoomButton, makeDataButton, expressionButton;
|
let makeZoomInputButton, makeDataInputButton, expressionInputButton;
|
||||||
|
|
||||||
if (this.props.fieldSpec.expression.parameters.includes('zoom')) {
|
if (this.props.fieldSpec.expression.parameters.includes('zoom')) {
|
||||||
expressionButton = (
|
expressionInputButton = (
|
||||||
<Button
|
<InputButton
|
||||||
className="maputnik-make-zoom-function"
|
className="maputnik-make-zoom-function"
|
||||||
onClick={this.props.onExpressionClick}
|
onClick={this.props.onExpressionClick}
|
||||||
title="Convert to expression"
|
title="Convert to expression"
|
||||||
|
@ -45,34 +45,34 @@ export default class FunctionButtons extends React.Component {
|
||||||
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
|
||||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</InputButton>
|
||||||
);
|
);
|
||||||
|
|
||||||
makeZoomButton = <Button
|
makeZoomInputButton = <InputButton
|
||||||
className="maputnik-make-zoom-function"
|
className="maputnik-make-zoom-function"
|
||||||
onClick={this.props.onZoomClick}
|
onClick={this.props.onZoomClick}
|
||||||
title="Convert property into a zoom function"
|
title="Convert property into a zoom function"
|
||||||
>
|
>
|
||||||
<MdFunctions />
|
<MdFunctions />
|
||||||
</Button>
|
</InputButton>
|
||||||
|
|
||||||
if (this.props.fieldSpec['property-type'] === 'data-driven') {
|
if (this.props.fieldSpec['property-type'] === 'data-driven') {
|
||||||
makeDataButton = <Button
|
makeDataInputButton = <InputButton
|
||||||
className="maputnik-make-data-function"
|
className="maputnik-make-data-function"
|
||||||
onClick={this.props.onDataClick}
|
onClick={this.props.onDataClick}
|
||||||
title="Convert property to data function"
|
title="Convert property to data function"
|
||||||
>
|
>
|
||||||
<MdInsertChart />
|
<MdInsertChart />
|
||||||
</Button>
|
</InputButton>
|
||||||
}
|
}
|
||||||
return <div>
|
return <div>
|
||||||
{expressionButton}
|
{expressionInputButton}
|
||||||
{makeDataButton}
|
{makeDataInputButton}
|
||||||
{makeZoomButton}
|
{makeZoomInputButton}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return <div>{expressionButton}</div>
|
return <div>{expressionInputButton}</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,13 +37,12 @@ export default class SpecProperty extends React.Component {
|
||||||
|
|
||||||
const error = errors[fieldType+"."+fieldName];
|
const error = errors[fieldType+"."+fieldName];
|
||||||
|
|
||||||
return <Block
|
return <SpecField
|
||||||
|
{...this.props}
|
||||||
error={error}
|
error={error}
|
||||||
fieldSpec={this.props.fieldSpec}
|
fieldSpec={this.props.fieldSpec}
|
||||||
label={labelFromFieldName(this.props.fieldName)}
|
label={labelFromFieldName(this.props.fieldName)}
|
||||||
action={functionBtn}
|
action={functionBtn}
|
||||||
>
|
/>
|
||||||
<SpecField {...this.props} />
|
|
||||||
</Block>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,11 @@ import React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
|
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
|
||||||
|
|
||||||
import Button from './Button'
|
import InputButton from './InputButton'
|
||||||
import SpecField from './SpecField'
|
import InputSpec from './InputSpec'
|
||||||
import FieldNumber from './FieldNumber'
|
import InputNumber from './InputNumber'
|
||||||
|
import InputSelect from './InputSelect'
|
||||||
|
import FieldDocLabel from './FieldDocLabel'
|
||||||
import Block from './Block'
|
import Block from './Block'
|
||||||
|
|
||||||
import DeleteStopButton from './_DeleteStopButton'
|
import DeleteStopButton from './_DeleteStopButton'
|
||||||
|
@ -143,52 +145,92 @@ export default class ZoomProperty extends React.Component {
|
||||||
}).join("");
|
}).join("");
|
||||||
const error = message ? {message} : undefined;
|
const error = message ? {message} : undefined;
|
||||||
|
|
||||||
return <Block
|
return <tr
|
||||||
error={error}
|
|
||||||
key={key}
|
key={key}
|
||||||
fieldSpec={this.props.fieldSpec}
|
|
||||||
label={labelFromFieldName(this.props.fieldName)}
|
|
||||||
action={deleteStopBtn}
|
|
||||||
>
|
>
|
||||||
<div>
|
<td>
|
||||||
<div className="maputnik-zoom-spec-property-stop-edit">
|
<InputNumber
|
||||||
<FieldNumber
|
value={zoomLevel}
|
||||||
value={zoomLevel}
|
onChange={changedStop => this.changeZoomStop(idx, changedStop, value)}
|
||||||
onChange={changedStop => this.changeZoomStop(idx, changedStop, value)}
|
min={0}
|
||||||
min={0}
|
max={22}
|
||||||
max={22}
|
/>
|
||||||
/>
|
</td>
|
||||||
</div>
|
<td>
|
||||||
<div className="maputnik-zoom-spec-property-stop-value">
|
<InputSpec
|
||||||
<SpecField
|
fieldName={this.props.fieldName}
|
||||||
fieldName={this.props.fieldName}
|
fieldSpec={this.props.fieldSpec}
|
||||||
fieldSpec={this.props.fieldSpec}
|
value={value}
|
||||||
value={value}
|
onChange={(_, newValue) => this.changeZoomStop(idx, zoomLevel, newValue)}
|
||||||
onChange={(_, newValue) => this.changeZoomStop(idx, zoomLevel, newValue)}
|
/>
|
||||||
/>
|
</td>
|
||||||
</div>
|
<td>
|
||||||
</div>
|
{deleteStopBtn}
|
||||||
</Block>
|
</td>
|
||||||
|
</tr>
|
||||||
});
|
});
|
||||||
|
|
||||||
return <div className="maputnik-zoom-spec-property">
|
// return <div className="maputnik-zoom-spec-property">
|
||||||
{zoomFields}
|
return <div className="maputnik-data-spec-block">
|
||||||
<Button
|
<fieldset className="maputnik-data-spec-property">
|
||||||
className="maputnik-add-stop"
|
<legend>{labelFromFieldName(this.props.fieldName)}</legend>
|
||||||
onClick={this.props.onAddStop.bind(this)}
|
<div className="maputnik-data-fieldset-inner">
|
||||||
>
|
<Block
|
||||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
label={"Function"}
|
||||||
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
>
|
||||||
</svg> Add stop
|
<div className="maputnik-data-spec-property-input">
|
||||||
</Button>
|
<InputSelect
|
||||||
<Button
|
value={"interpolate"}
|
||||||
className="maputnik-add-stop"
|
onChange={propVal => this.changeDataProperty("type", propVal)}
|
||||||
onClick={this.props.onExpressionClick.bind(this)}
|
title={"Select a type of data scale (default is 'categorical')."}
|
||||||
>
|
options={this.getDataFunctionTypes(this.props.fieldSpec)}
|
||||||
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
/>
|
||||||
<path fill="currentColor" d={mdiFunctionVariant} />
|
</div>
|
||||||
</svg> Convert to expression
|
</Block>
|
||||||
</Button>
|
<div className="maputnik-function-stop">
|
||||||
|
<table className="maputnik-function-stop-table maputnik-function-stop-table--zoom">
|
||||||
|
<caption>Stops</caption>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Zoom</th>
|
||||||
|
<th rowSpan="2">Output value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{zoomFields}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className="maputnik-toolbox">
|
||||||
|
<InputButton
|
||||||
|
className="maputnik-add-stop"
|
||||||
|
onClick={this.props.onAddStop.bind(this)}
|
||||||
|
>
|
||||||
|
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d={mdiTableRowPlusAfter} />
|
||||||
|
</svg> Add stop
|
||||||
|
</InputButton>
|
||||||
|
<InputButton
|
||||||
|
className="maputnik-add-stop"
|
||||||
|
onClick={this.props.onExpressionClick.bind(this)}
|
||||||
|
>
|
||||||
|
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
|
||||||
|
<path fill="currentColor" d={mdiFunctionVariant} />
|
||||||
|
</svg> Convert to expression
|
||||||
|
</InputButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDataFunctionTypes(fieldSpec) {
|
||||||
|
if (fieldSpec.expression.interpolated) {
|
||||||
|
return ["categorical", "interval", "exponential", "identity", "interpolate"]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return ["categorical", "interval", "identity", "interpolate"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -183,6 +183,7 @@
|
||||||
.maputnik-input-block-label {
|
.maputnik-input-block-label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 32%;
|
width: 32%;
|
||||||
|
margin-bottom: $margin-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-input-block-action {
|
.maputnik-input-block-action {
|
||||||
|
|
|
@ -77,13 +77,13 @@
|
||||||
.maputnik-array-block-action {
|
.maputnik-array-block-action {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 14%;
|
width: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-array-block-content {
|
.maputnik-array-block-content {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 86%;
|
width: calc(100% - 2em);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,9 @@
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
position: relative;
|
position: relative;
|
||||||
font-family: $font-family;
|
font-family: $font-family;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-modal-section {
|
.maputnik-modal-section {
|
||||||
|
@ -50,7 +53,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-modal-scroller {
|
.maputnik-modal-scroller {
|
||||||
max-height: calc(100vh - 35px);
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -73,10 +73,6 @@
|
||||||
width: 30%;
|
width: 30%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.maputnik-input-block:not(:first-child) .maputnik-input-block-label {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.maputnik-input-block-content {
|
.maputnik-input-block-content {
|
||||||
width: 70%;
|
width: 70%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,3 +35,135 @@
|
||||||
height: 14px;
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.maputnik-data-spec-property {
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-data-fieldset-inner {
|
||||||
|
background: $color-black;
|
||||||
|
border: solid 1px $color-midgray;
|
||||||
|
border-radius: 2px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
// HACK: Overide
|
||||||
|
.maputnik-input-block {
|
||||||
|
margin: $margin-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-add-stop {
|
||||||
|
display: inline-block;
|
||||||
|
float: none;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-toolbox {
|
||||||
|
margin: $margin-3;
|
||||||
|
margin-top: $margin-3;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-data-spec-property {
|
||||||
|
legend {
|
||||||
|
font-size: $font-size-6;
|
||||||
|
color: $color-lowgray;
|
||||||
|
margin-bottom: $margin-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-data-spec-property-group {
|
||||||
|
margin-bottom: $margin-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-data-spec-block {
|
||||||
|
margin: $margin-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-function-stop {
|
||||||
|
padding-left: $margin-2;
|
||||||
|
padding-right: $margin-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maputnik-function-stop-table {
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: $margin-2;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
thead th {
|
||||||
|
padding: $margin-1 $margin-2;
|
||||||
|
padding-left: 0;
|
||||||
|
color: $color-lowgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
td, th {
|
||||||
|
font-size: $font-size-6;
|
||||||
|
color: $color-white;
|
||||||
|
|
||||||
|
// HACK
|
||||||
|
> * {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
vertical-align: text-top;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:first-child)
|
||||||
|
{
|
||||||
|
padding-top: $margin-1;
|
||||||
|
padding-left: $margin-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
width: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
width: 6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(4) {
|
||||||
|
// HACK
|
||||||
|
width: 1.8em;
|
||||||
|
|
||||||
|
.maputnik-delete-stop {
|
||||||
|
padding: 0;
|
||||||
|
width: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--zoom {
|
||||||
|
td, th {
|
||||||
|
&:nth-child(2) {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
// HACK
|
||||||
|
width: 1.8em;
|
||||||
|
|
||||||
|
.maputnik-delete-stop {
|
||||||
|
padding: 0;
|
||||||
|
width: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
caption {
|
||||||
|
color: $color-lowgray;
|
||||||
|
text-align: left;
|
||||||
|
border-top: solid 1px $color-black;
|
||||||
|
font-size: $font-size-6;
|
||||||
|
height: 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ export const NumberType = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<FieldArray
|
<FieldArray
|
||||||
|
label="Foobar"
|
||||||
type="number"
|
type="number"
|
||||||
value={value}
|
value={value}
|
||||||
length={3}
|
length={3}
|
||||||
|
@ -32,6 +33,7 @@ export const StringType = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<FieldArray
|
<FieldArray
|
||||||
|
label="Foobar"
|
||||||
type="string"
|
type="string"
|
||||||
value={value}
|
value={value}
|
||||||
length={3}
|
length={3}
|
||||||
|
|
|
@ -18,6 +18,7 @@ export const Basic = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<FieldAutocomplete
|
<FieldAutocomplete
|
||||||
|
label="Foobar"
|
||||||
options={options}
|
options={options}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
|
|
|
@ -17,6 +17,7 @@ export const BasicUnchecked = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<FieldCheckbox
|
<FieldCheckbox
|
||||||
|
label="Foobar"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
/>
|
/>
|
||||||
|
@ -30,6 +31,7 @@ export const BasicChecked = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<FieldCheckbox
|
<FieldCheckbox
|
||||||
|
label="Foobar"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -17,7 +17,7 @@ export const Basic = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<FieldColor
|
<FieldColor
|
||||||
name="color"
|
label="Foobar"
|
||||||
value={color}
|
value={color}
|
||||||
onChange={setColor}
|
onChange={setColor}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import {useActionState} from './helper';
|
|
||||||
import FieldComment from '../src/components/FieldComment';
|
|
||||||
import {Wrapper} from './ui';
|
|
||||||
import {withA11y} from '@storybook/addon-a11y';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'FieldComment',
|
|
||||||
component: FieldComment,
|
|
||||||
decorators: [withA11y],
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const Basic = () => {
|
|
||||||
const [value, setValue] = useActionState("onChange", "Hello\nworld");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<FieldComment
|
|
||||||
value={value}
|
|
||||||
onChange={setValue}
|
|
||||||
/>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ export const NumberType = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<FieldDynamicArray
|
<FieldDynamicArray
|
||||||
|
label="Foobar"
|
||||||
type="number"
|
type="number"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
|
@ -31,6 +32,7 @@ export const UrlType = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<FieldDynamicArray
|
<FieldDynamicArray
|
||||||
|
label="Foobar"
|
||||||
type="url"
|
type="url"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
|
@ -45,6 +47,7 @@ export const EnumType = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<FieldDynamicArray
|
<FieldDynamicArray
|
||||||
|
label="Foobar"
|
||||||
fieldSpec={{values: {"foo": null, "bar": null, "baz": null}}}
|
fieldSpec={{values: {"foo": null, "bar": null, "baz": null}}}
|
||||||
type="enum"
|
type="enum"
|
||||||
value={value}
|
value={value}
|
||||||
|
|
|
@ -18,6 +18,7 @@ export const BasicFew = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<FieldEnum
|
<FieldEnum
|
||||||
|
label="Foobar"
|
||||||
options={options}
|
options={options}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
|
@ -33,6 +34,7 @@ export const BasicFewWithDefault = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<FieldEnum
|
<FieldEnum
|
||||||
|
label="Foobar"
|
||||||
options={options}
|
options={options}
|
||||||
default={"Baz"}
|
default={"Baz"}
|
||||||
value={value}
|
value={value}
|
||||||
|
@ -49,6 +51,7 @@ export const BasicMany = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<FieldEnum
|
<FieldEnum
|
||||||
|
label="Foobar"
|
||||||
options={options}
|
options={options}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
|
@ -64,7 +67,8 @@ export const BasicManyWithDefault = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<FieldEnum
|
<FieldEnum
|
||||||
options={options}A
|
label="Foobar"
|
||||||
|
options={options}
|
||||||
default={"h"}
|
default={"h"}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import {useActionState} from './helper';
|
|
||||||
import FieldFont from '../src/components/FieldFont';
|
|
||||||
import {Wrapper} from './ui';
|
|
||||||
import {withA11y} from '@storybook/addon-a11y';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'FieldFont',
|
|
||||||
component: FieldFont,
|
|
||||||
decorators: [withA11y],
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const Basic = () => {
|
|
||||||
const fonts = ["Comic Sans", "Helvectica", "Gotham"];
|
|
||||||
const [value, setValue] = useActionState("onChange", ["Comic Sans"]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<FieldFont
|
|
||||||
fonts={fonts}
|
|
||||||
value={value}
|
|
||||||
onChange={setValue}
|
|
||||||
/>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
43
stories/FieldFunction.stories.js
Normal file
43
stories/FieldFunction.stories.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import React from 'react';
|
||||||
|
import FieldFunction from '../src/components/FieldFunction';
|
||||||
|
import {action} from '@storybook/addon-actions';
|
||||||
|
import {Wrapper} from './ui';
|
||||||
|
import {withA11y} from '@storybook/addon-a11y';
|
||||||
|
import {latest} from '@mapbox/mapbox-gl-style-spec'
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'FieldFunction',
|
||||||
|
component: FieldFunction,
|
||||||
|
decorators: [withA11y],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Basic = () => {
|
||||||
|
const value = {
|
||||||
|
"property": "rank",
|
||||||
|
"type": "categorical",
|
||||||
|
"default": "#222",
|
||||||
|
"stops": [
|
||||||
|
[
|
||||||
|
{"zoom": 6, "value": ""},
|
||||||
|
["#777"]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{"zoom": 10, "value": ""},
|
||||||
|
["#444"]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
return <div style={{width: "360px"}}>
|
||||||
|
<FieldFunction
|
||||||
|
onChange={() => {}}
|
||||||
|
value={value}
|
||||||
|
errors={[]}
|
||||||
|
fieldName={"Color"}
|
||||||
|
fieldType={"color"}
|
||||||
|
fieldSpec={latest['paint_fill']['fill-color']}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import {useActionState} from './helper';
|
|
||||||
import FieldId from '../src/components/FieldId';
|
|
||||||
import {Wrapper} from './ui';
|
|
||||||
import {withA11y} from '@storybook/addon-a11y';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'FieldId',
|
|
||||||
component: FieldId,
|
|
||||||
decorators: [withA11y],
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const Basic = () => {
|
|
||||||
const [value, setValue] = useActionState("onChange", "water");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<FieldId
|
|
||||||
value={value}
|
|
||||||
onChange={setValue}
|
|
||||||
/>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import {useActionState} from './helper';
|
|
||||||
import FieldMaxZoom from '../src/components/FieldMaxZoom';
|
|
||||||
import {Wrapper} from './ui';
|
|
||||||
import {withA11y} from '@storybook/addon-a11y';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'FieldMaxZoom',
|
|
||||||
component: FieldMaxZoom,
|
|
||||||
decorators: [withA11y],
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const Basic = () => {
|
|
||||||
const [value, setValue] = useActionState("onChange", 12);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<FieldMaxZoom
|
|
||||||
value={value}
|
|
||||||
onChange={setValue}
|
|
||||||
/>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import {useActionState} from './helper';
|
|
||||||
import FieldMinZoom from '../src/components/FieldMinZoom';
|
|
||||||
import {Wrapper} from './ui';
|
|
||||||
import {withA11y} from '@storybook/addon-a11y';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'FieldMinZoom',
|
|
||||||
component: FieldMinZoom,
|
|
||||||
decorators: [withA11y],
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const Basic = () => {
|
|
||||||
const [value, setValue] = useActionState("onChange", 2);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<FieldMinZoom
|
|
||||||
value={value}
|
|
||||||
onChange={setValue}
|
|
||||||
/>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ export const Basic = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<FieldMultiInput
|
<FieldMultiInput
|
||||||
|
label="Foobar"
|
||||||
options={options}
|
options={options}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
|
|
|
@ -16,7 +16,7 @@ export const Basic = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<FieldNumber
|
<FieldNumber
|
||||||
name="number"
|
label="number"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
/>
|
/>
|
||||||
|
@ -30,7 +30,7 @@ export const Range = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<FieldNumber
|
<FieldNumber
|
||||||
name="number"
|
label="number"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
min={1}
|
min={1}
|
||||||
|
|
|
@ -18,6 +18,7 @@ export const Basic = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<FieldSelect
|
<FieldSelect
|
||||||
|
label="Foobar"
|
||||||
options={options}
|
options={options}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import {useActionState} from './helper';
|
|
||||||
import FieldSource from '../src/components/FieldSource';
|
|
||||||
import {Wrapper} from './ui';
|
|
||||||
import {withA11y} from '@storybook/addon-a11y';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'FieldSource',
|
|
||||||
component: FieldSource,
|
|
||||||
decorators: [withA11y],
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const Basic = () => {
|
|
||||||
const [value, setValue] = useActionState("onChange", "openmaptiles");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<FieldSource
|
|
||||||
value={value}
|
|
||||||
onChange={setValue}
|
|
||||||
/>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import {useActionState} from './helper';
|
|
||||||
import FieldSourceLayer from '../src/components/FieldSourceLayer';
|
|
||||||
import {Wrapper} from './ui';
|
|
||||||
import {withA11y} from '@storybook/addon-a11y';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'FieldSourceLayer',
|
|
||||||
component: FieldSourceLayer,
|
|
||||||
decorators: [withA11y],
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const Basic = () => {
|
|
||||||
const [value, setValue] = useActionState("onChange", "water");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<FieldSourceLayer
|
|
||||||
value={value}
|
|
||||||
onChange={setValue}
|
|
||||||
/>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -17,49 +17,7 @@ export const Basic = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<FieldString
|
<FieldString
|
||||||
value={value}
|
label="Foobar"
|
||||||
onChange={setValue}
|
|
||||||
/>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WithDefault = () => {
|
|
||||||
const [value, setValue] = useActionState("onChange", null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<FieldString
|
|
||||||
value={value}
|
|
||||||
default={"Edit me..."}
|
|
||||||
onChange={setValue}
|
|
||||||
/>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Multiline = () => {
|
|
||||||
const [value, setValue] = useActionState("onChange", "Hello\nworld");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<FieldString
|
|
||||||
multi={true}
|
|
||||||
value={value}
|
|
||||||
onChange={setValue}
|
|
||||||
/>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MultilineWithDefault = () => {
|
|
||||||
const [value, setValue] = useActionState("onChange", null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<FieldString
|
|
||||||
multi={true}
|
|
||||||
default={"Edit\nme.."}
|
|
||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import {useActionState} from './helper';
|
|
||||||
import FieldSymbol from '../src/components/FieldSymbol';
|
|
||||||
import {Wrapper} from './ui';
|
|
||||||
import {withA11y} from '@storybook/addon-a11y';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'FieldSymbol',
|
|
||||||
component: FieldSymbol,
|
|
||||||
decorators: [withA11y],
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const Basic = () => {
|
|
||||||
const icons = ["Bicycle", "Ski", "Ramp"];
|
|
||||||
const [value, setValue] = useActionState("onChange", "Ski");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<FieldSymbol
|
|
||||||
icons={icons}
|
|
||||||
value={value}
|
|
||||||
onChange={setValue}
|
|
||||||
/>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import {useActionState} from './helper';
|
|
||||||
import FieldType from '../src/components/FieldType';
|
|
||||||
import {Wrapper} from './ui';
|
|
||||||
import {withA11y} from '@storybook/addon-a11y';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'FieldType',
|
|
||||||
component: FieldType,
|
|
||||||
decorators: [withA11y],
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const Basic = () => {
|
|
||||||
const [value, setValue] = useActionState("onChange", "background");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<FieldType
|
|
||||||
value={value}
|
|
||||||
onChange={setValue}
|
|
||||||
/>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ export const Valid = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<FieldUrl
|
<FieldUrl
|
||||||
|
label="Foobar"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
onInput={setValue}
|
onInput={setValue}
|
||||||
|
@ -31,6 +32,7 @@ export const Invalid = () => {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<FieldUrl
|
<FieldUrl
|
||||||
|
label="Foobar"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
onInput={setValue}
|
onInput={setValue}
|
||||||
|
|
56
stories/IconLayer.stories.js
Normal file
56
stories/IconLayer.stories.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import React from 'react';
|
||||||
|
import IconLayer from '../src/components/IconLayer';
|
||||||
|
import {action} from '@storybook/addon-actions';
|
||||||
|
import {Wrapper} from './ui';
|
||||||
|
import {withA11y} from '@storybook/addon-a11y';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'IconLayer',
|
||||||
|
component: IconLayer,
|
||||||
|
decorators: [withA11y],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IconList = () => {
|
||||||
|
const types = [
|
||||||
|
'fill-extrusion',
|
||||||
|
'raster',
|
||||||
|
'hillshade',
|
||||||
|
'heatmap',
|
||||||
|
'fill',
|
||||||
|
'background',
|
||||||
|
'line',
|
||||||
|
'symbol',
|
||||||
|
'circle',
|
||||||
|
'INVALID',
|
||||||
|
]
|
||||||
|
|
||||||
|
return <Wrapper>
|
||||||
|
<table style={{textAlign: "left"}}>
|
||||||
|
<thead style={{borderBottom: "solid 1px white"}}>
|
||||||
|
<tr>
|
||||||
|
<td>ID</td>
|
||||||
|
<td>Preview</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{types.map(type => (
|
||||||
|
<tr>
|
||||||
|
<td style={{paddingRight: "1em"}}>
|
||||||
|
<code>{type}</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<IconLayer type={type} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Wrapper>
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
44
stories/InputArray.stories.js
Normal file
44
stories/InputArray.stories.js
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {useActionState} from './helper';
|
||||||
|
import InputArray from '../src/components/InputArray';
|
||||||
|
import {Wrapper} from './ui';
|
||||||
|
import {withA11y} from '@storybook/addon-a11y';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'InputArray',
|
||||||
|
component: InputArray,
|
||||||
|
decorators: [withA11y],
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const NumberType = () => {
|
||||||
|
const [value, setValue] = useActionState("onChange", [1,2,3]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<InputArray
|
||||||
|
type="number"
|
||||||
|
value={value}
|
||||||
|
length={3}
|
||||||
|
onChange={setValue}
|
||||||
|
/>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StringType = () => {
|
||||||
|
const [value, setValue] = useActionState("onChange", ["a", "b", "c"]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<InputArray
|
||||||
|
type="string"
|
||||||
|
value={value}
|
||||||
|
length={3}
|
||||||
|
onChange={setValue}
|
||||||
|
/>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
29
stories/InputAutocomplete.stories.js
Normal file
29
stories/InputAutocomplete.stories.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {useActionState} from './helper';
|
||||||
|
import InputAutocomplete from '../src/components/InputAutocomplete';
|
||||||
|
import {InputContainer} from './ui';
|
||||||
|
import {withA11y} from '@storybook/addon-a11y';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'InputAutocomplete',
|
||||||
|
component: InputAutocomplete,
|
||||||
|
decorators: [withA11y],
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const Basic = () => {
|
||||||
|
const options = [["FOO", "foo"], ["BAR", "bar"], ["BAZ", "baz"]];
|
||||||
|
const [value, setValue] = useActionState("onChange", "bar");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputContainer>
|
||||||
|
<InputAutocomplete
|
||||||
|
label="Foobar"
|
||||||
|
options={options}
|
||||||
|
value={value}
|
||||||
|
onChange={setValue}
|
||||||
|
/>
|
||||||
|
</InputContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue