Merge pull request #706 from orangemug/fix/input-label-a11y

Improved label accessibility
This commit is contained in:
Orange Mug 2020-06-30 10:50:37 +01:00 committed by GitHub
commit 8b766777ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
128 changed files with 4124 additions and 1693 deletions

View file

@ -16,7 +16,6 @@ module.exports = {
...config, ...config,
module: { module: {
rules: [ rules: [
...config.module.rules,
...rules, ...rules,
] ]
} }

9
package-lock.json generated
View file

@ -11766,8 +11766,8 @@
"integrity": "sha512-v674D0WtpxCXlA6E+sBlG1QJWdUkz/s9qAD91bJSXBGuBL5lL4tJXpoJEftecphCh2SVQCjWMS2vhylc3AIQTg==" "integrity": "sha512-v674D0WtpxCXlA6E+sBlG1QJWdUkz/s9qAD91bJSXBGuBL5lL4tJXpoJEftecphCh2SVQCjWMS2vhylc3AIQTg=="
}, },
"maputnik-design": { "maputnik-design": {
"version": "github:maputnik/design#f7a2b4d63918adadc242ec05c563d3ba4eb4df68", "version": "github:maputnik/design#172b06cb062a82f62bb7239474533a110915ef3e",
"from": "github:maputnik/design#f7a2b4d" "from": "github:maputnik/design#172b06c"
}, },
"markdown-escapes": { "markdown-escapes": {
"version": "1.0.4", "version": "1.0.4",
@ -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",

View file

@ -46,7 +46,7 @@
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
"mapbox-gl": "^1.11.0", "mapbox-gl": "^1.11.0",
"mapbox-gl-inspect": "^1.3.1", "mapbox-gl-inspect": "^1.3.1",
"maputnik-design": "github:maputnik/design#f7a2b4d", "maputnik-design": "github:maputnik/design#172b06c",
"ol": "^6.3.1", "ol": "^6.3.1",
"ol-mapbox-style": "^6.0.1", "ol-mapbox-style": "^6.0.1",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
@ -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": {

View file

@ -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) {

View file

@ -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,
}, },
]; ];
@ -178,7 +184,7 @@ export default class AppToolbar extends React.Component {
return view.id === this.props.mapState; return view.id === this.props.mapState;
}); });
return <div className='maputnik-toolbar'> return <nav className='maputnik-toolbar'>
<div className="maputnik-toolbar__inner"> <div className="maputnik-toolbar__inner">
<div <div
className="maputnik-toolbar-logo-container" className="maputnik-toolbar-logo-container"
@ -205,16 +211,18 @@ export default class AppToolbar extends React.Component {
> >
Map view Map view
</button> </button>
<div <a
className="maputnik-toolbar-logo" className="maputnik-toolbar-logo"
tabIndex="-1" target="blank"
rel="noreferrer noopener"
href="https://github.com/maputnik/editor"
> >
<span dangerouslySetInnerHTML={{__html: logoImage}} /> <span dangerouslySetInnerHTML={{__html: logoImage}} />
<h1> <h1>
<span className="maputnik-toolbar-name">{pkgJson.name}</span> <span className="maputnik-toolbar-name">{pkgJson.name}</span>
<span className="maputnik-toolbar-version">v{pkgJson.version}</span> <span className="maputnik-toolbar-version">v{pkgJson.version}</span>
</h1> </h1>
</div> </a>
</div> </div>
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar"> <div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}> <ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
@ -242,13 +250,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>
@ -263,6 +280,6 @@ export default class AppToolbar extends React.Component {
</ToolbarLinkHighlighted> </ToolbarLinkHighlighted>
</div> </div>
</div> </div>
</div> </nav>
} }
} }

View file

@ -12,7 +12,7 @@ export default class Block extends React.Component {
label: PropTypes.oneOfType([ label: PropTypes.oneOfType([
PropTypes.string, PropTypes.string,
PropTypes.element, PropTypes.element,
]).isRequired, ]),
action: PropTypes.element, action: PropTypes.element,
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
style: PropTypes.object, style: PropTypes.object,
@ -40,55 +40,64 @@ export default class Block extends React.Component {
}); });
} }
/**
* Some fields for example <InputColor/> bind click events inside the element
* to close the picker. This in turn propagates to the <label/> element
* causing the picker to reopen. This causes a scenario where the picker can
* never be closed once open.
*/
onLabelClick = (event) => {
const el = event.nativeEvent.target;
const nativeEvent = event.nativeEvent;
const contains = this._blockEl.contains(el);
if (event.nativeEvent.target.nodeName !== "INPUT" && !contains) {
event.stopPropagation();
}
event.preventDefault();
}
render() { render() {
const errors = [].concat(this.props.error || []); const errors = [].concat(this.props.error || []);
return <div style={this.props.style} return <label style={this.props.style}
data-wd-key={this.props["data-wd-key"]} data-wd-key={this.props["data-wd-key"]}
className={classnames({ className={classnames({
"maputnik-input-block": true, "maputnik-input-block": true,
"maputnik-input-block--wide": this.props.wideMode, "maputnik-input-block--wide": this.props.wideMode,
"maputnik-action-block": this.props.action "maputnik-action-block": this.props.action
})} })}
> onClick={this.onLabelClick}
>
{this.props.fieldSpec && {this.props.fieldSpec &&
<div className="maputnik-input-block-label"> <div className="maputnik-input-block-label">
<FieldDocLabel <FieldDocLabel
label={this.props.label} label={this.props.label}
onToggleDoc={this.onToggleDoc} onToggleDoc={this.onToggleDoc}
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec}
/> />
</div> </div>
} }
{!this.props.fieldSpec && {!this.props.fieldSpec &&
<label className="maputnik-input-block-label"> <div className="maputnik-input-block-label">
{this.props.label} {this.props.label}
</label> </div>
} }
{this.props.action &&
<div className="maputnik-input-block-action"> <div className="maputnik-input-block-action">
{this.props.action} {this.props.action}
</div> </div>
} <div className="maputnik-input-block-content" ref={el => this._blockEl = el}>
<div className="maputnik-input-block-content">
{this.props.children} {this.props.children}
</div> </div>
{errors.length > 0 && {this.props.fieldSpec &&
<div className="maputnik-inline-error"> <div
{[].concat(this.props.error).map((error, idx) => { className="maputnik-doc-inline"
return <div key={idx}>{error.message}</div> style={{display: this.state.showDoc ? '' : 'none'}}
})} >
<Doc fieldSpec={this.props.fieldSpec} />
</div> </div>
} }
{this.props.fieldSpec && </label>
<div
className="maputnik-doc-inline"
style={{display: this.state.showDoc ? '' : 'none'}}
>
<Doc fieldSpec={this.props.fieldSpec} />
</div>
}
</div>
} }
} }

View file

@ -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>
} }
} }

View file

@ -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>
} }
} }

View file

@ -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>
} }
} }

View file

@ -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>
} }
} }

View file

@ -0,0 +1,31 @@
import React from 'react'
import PropTypes from 'prop-types'
import Block from './Block'
import InputString from './InputString'
export default class FieldComment 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"
>
<InputString
multi={true}
value={this.props.value}
onChange={this.props.onChange}
default="Comment..."
/>
</Block>
}
}

View file

@ -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>
} }
} }

View file

@ -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}
/>
}
} }
} }

View file

@ -200,12 +200,35 @@ export default class FieldFunction extends React.Component {
} }
makeZoomFunction = () => { makeZoomFunction = () => {
const zoomFunc = { const {value} = this.props;
stops: [
[6, this.props.value || findDefaultFromSpec(this.props.fieldSpec)], let zoomFunc;
[10, this.props.value || findDefaultFromSpec(this.props.fieldSpec)] if (typeof(value) === "object") {
] if (value.stops) {
zoomFunc = {
stops: value.stops.map(stop => {
return [stop[0].zoom, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
})
}
}
else {
zoomFunc = {
stops: [
[6, findDefaultFromSpec(this.props.fieldSpec)],
[10, findDefaultFromSpec(this.props.fieldSpec)]
]
}
}
} }
else {
zoomFunc = {
stops: [
[6, value || findDefaultFromSpec(this.props.fieldSpec)],
[10, value || findDefaultFromSpec(this.props.fieldSpec)]
]
}
}
this.props.onChange(this.props.fieldName, zoomFunc) this.props.onChange(this.props.fieldName, zoomFunc)
} }
@ -258,14 +281,41 @@ export default class FieldFunction extends React.Component {
makeDataFunction = () => { makeDataFunction = () => {
const functionType = this.getFieldFunctionType(this.props.fieldSpec); const functionType = this.getFieldFunctionType(this.props.fieldSpec);
const stopValue = functionType === 'categorical' ? '' : 0; const stopValue = functionType === 'categorical' ? '' : 0;
const dataFunc = { const {value} = this.props;
property: "", let dataFunc;
type: functionType,
stops: [ if (typeof(value) === "object") {
[{zoom: 6, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)], if (value.stops) {
[{zoom: 10, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)] dataFunc = {
] property: "",
type: functionType,
stops: value.stops.map(stop => {
return [{zoom: stop[0], value: stopValue}, stop[1] || findDefaultFromSpec(this.props.fieldSpec)];
})
}
}
else {
dataFunc = {
property: "",
type: functionType,
stops: [
[{zoom: 6, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)],
[{zoom: 10, value: stopValue}, findDefaultFromSpec(this.props.fieldSpec)]
]
}
}
} }
else {
dataFunc = {
property: "",
type: functionType,
stops: [
[{zoom: 6, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)],
[{zoom: 10, value: stopValue}, this.props.value || findDefaultFromSpec(this.props.fieldSpec)]
]
}
}
this.props.onChange(this.props.fieldName, dataFunc) this.props.onChange(this.props.fieldName, dataFunc)
} }
@ -310,11 +360,13 @@ export default class FieldFunction extends React.Component {
value={this.props.value} value={this.props.value}
onDeleteStop={this.deleteStop} onDeleteStop={this.deleteStop}
onAddStop={this.addStop} onAddStop={this.addStop}
onChangeToDataFunction={this.makeDataFunction}
onExpressionClick={this.makeExpression} onExpressionClick={this.makeExpression}
/> />
) )
} }
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}
@ -325,6 +377,7 @@ export default class FieldFunction extends React.Component {
value={this.props.value} value={this.props.value}
onDeleteStop={this.deleteStop} onDeleteStop={this.deleteStop}
onAddStop={this.addStop} onAddStop={this.addStop}
onChangeToZoomFunction={this.makeZoomFunction}
onExpressionClick={this.makeExpression} onExpressionClick={this.makeExpression}
/> />
) )

View file

@ -0,0 +1,27 @@
import React from 'react'
import PropTypes from 'prop-types'
import {latest} from '@mapbox/mapbox-gl-style-spec'
import Block from './Block'
import InputString from './InputString'
export default class FieldId 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}
>
<InputString
value={this.props.value}
onChange={this.props.onChange}
/>
</Block>
}
}

View 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} />
}
}

View file

@ -0,0 +1,30 @@
import React from 'react'
import PropTypes from 'prop-types'
import {latest} from '@mapbox/mapbox-gl-style-spec'
import Block from './Block'
import InputNumber from './InputNumber'
export default class FieldMaxZoom 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"
>
<InputNumber
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>
}
}

View file

@ -0,0 +1,30 @@
import React from 'react'
import PropTypes from 'prop-types'
import {latest} from '@mapbox/mapbox-gl-style-spec'
import Block from './Block'
import InputNumber from './InputNumber'
export default class FieldMinZoom 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"
>
<InputNumber
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>
}
}

View file

@ -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>
} }
} }

View file

@ -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}
/>
}
} }
} }

View file

@ -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>
} }
} }

View file

@ -0,0 +1,36 @@
import React from 'react'
import PropTypes from 'prop-types'
import {latest} from '@mapbox/mapbox-gl-style-spec'
import Block from './Block'
import InputAutocomplete from './InputAutocomplete'
export default class FieldSource 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}
>
<InputAutocomplete
value={this.props.value}
onChange={this.props.onChange}
options={this.props.sourceIds.map(src => [src, src])}
/>
</Block>
}
}

View file

@ -0,0 +1,34 @@
import React from 'react'
import PropTypes from 'prop-types'
import {latest} from '@mapbox/mapbox-gl-style-spec'
import Block from './Block'
import InputAutocomplete from './InputAutocomplete'
export default class FieldSourceLayer 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"
>
<InputAutocomplete
keepMenuWithinWindowBounds={!!this.props.isFixed}
value={this.props.value}
onChange={this.props.onChange}
options={this.props.sourceLayerIds.map(l => [l, l])}
/>
</Block>
}
}

View file

@ -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,
});
} }
} }

View file

@ -0,0 +1,52 @@
import React from 'react'
import PropTypes from 'prop-types'
import {latest} from '@mapbox/mapbox-gl-style-spec'
import Block from './Block'
import InputSelect from './InputSelect'
import InputString from './InputString'
export default class FieldType 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 &&
<InputString
value={this.props.value}
disabled={true}
/>
}
{!this.props.disabled &&
<InputSelect
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>
}
}

View file

@ -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&apos;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>
); );
} }
} }

View file

@ -0,0 +1,58 @@
import React from 'react'
import PropTypes from 'prop-types'
import FieldDocLabel from './FieldDocLabel'
import Doc from './Doc'
let IDX = 0;
export default class Fieldset extends React.Component {
constructor (props) {
super(props);
this._labelId = `fieldset_label_${(IDX++)}`;
this.state = {
showDoc: false,
}
}
onToggleDoc = (val) => {
this.setState({
showDoc: val
});
}
render () {
const {props} = this;
return <div className="maputnik-input-block" role="group" aria-labelledby={this._labelId}>
{this.props.fieldSpec &&
<div className="maputnik-input-block-label">
<FieldDocLabel
label={this.props.label}
onToggleDoc={this.onToggleDoc}
fieldSpec={this.props.fieldSpec}
/>
</div>
}
{!this.props.fieldSpec &&
<div className="maputnik-input-block-label">
{props.label}
</div>
}
<div className="maputnik-input-block-action">
{this.props.action}
</div>
<div className="maputnik-input-block-content">
{props.children}
</div>
{this.props.fieldSpec &&
<div
className="maputnik-doc-inline"
style={{display: this.state.showDoc ? '' : 'none'}}
>
<Doc fieldSpec={this.props.fieldSpec} />
</div>
}
</div>
}
}

View file

@ -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"

View file

@ -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}

View 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'] || this.props.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'] || this.props.label}
/>
}
})
return (
<div className="maputnik-array">
{inputs}
</div>
)
}
}

View 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
menuStyle={{
position: "fixed",
overflow: "auto",
maxHeight: this.state.maxHeight,
zIndex: '998'
}}
wrapperProps={{
className: "maputnik-autocomplete",
style: null
}}
inputProps={{
'aria-label': this.props['aria-label'],
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>
}
}

View file

@ -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

View file

@ -0,0 +1,39 @@
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,
}
onChange = () => {
this.props.onChange(!this.props.value);
}
render() {
return <div className="maputnik-checkbox-wrapper">
<input
className="maputnik-checkbox"
type="checkbox"
style={this.props.style}
onChange={this.onChange}
onClick={this.onChange}
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>
</div>
}
}

View 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>
}
}

View file

@ -0,0 +1,141 @@
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)}
aria-label={this.props['aria-label'] || this.props.label}
/>
}
else if (this.props.type === 'number') {
input = <InputNumber
value={v}
onChange={this.changeValue.bind(this, i)}
aria-label={this.props['aria-label'] || this.props.label}
/>
}
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)}
aria-label={this.props['aria-label'] || this.props.label}
/>
}
else {
input = <InputString
value={v}
onChange={this.changeValue.bind(this, i)}
aria-label={this.props['aria-label'] || this.props.label}
/>
}
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>
}
}

View 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, label} = 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'] || label}
/>
} else {
return <InputSelect
options={options}
value={value || this.props.default}
onChange={onChange}
aria-label={this.props['aria-label'] || label}
/>
}
}
}

View 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'] || this.props.name}
value={value}
options={this.props.fonts.map(f => [f, f])}
onChange={this.changeFont.bind(this, i)}
/>
</li>
})
return (
<ul className="maputnik-font">
{inputs}
</ul>
);
}
}

View file

@ -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>
} }
} }

View 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>
}
}

View file

@ -0,0 +1,234 @@
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}
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}
/>
}
}
}

View 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>
}
}

View 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>
);
}
}

View 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
View 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&apos;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>
);
}
}

View file

@ -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
/> );
}}
/>
} }
} }
@ -288,7 +290,7 @@ export default class LayerEditor extends React.Component {
items[id].handler(); items[id].handler();
} }
return <div className="maputnik-layer-editor" return <section className="maputnik-layer-editor"
role="main" role="main"
aria-label="Layer editor" aria-label="Layer editor"
> >
@ -330,6 +332,6 @@ export default class LayerEditor extends React.Component {
> >
{groups} {groups}
</Accordion> </Accordion>
</div> </section>
} }
} }

View file

@ -248,6 +248,7 @@ class LayerListContainer extends React.Component {
})} })}
index={idx} index={idx}
key={layer.key} key={layer.key}
id={layer.key}
layerId={layer.id} layerId={layer.id}
layerIndex={idx} layerIndex={idx}
layerType={layer.type} layerType={layer.type}
@ -264,7 +265,7 @@ class LayerListContainer extends React.Component {
}) })
}) })
return <div return <section
className="maputnik-layer-list" className="maputnik-layer-list"
role="complementary" role="complementary"
aria-label="Layers list" aria-label="Layers list"
@ -302,13 +303,15 @@ class LayerListContainer extends React.Component {
</div> </div>
</div> </div>
</header> </header>
<ul className="maputnik-layer-list-container" <div
role="navigation" role="navigation"
aria-label="Layers list" aria-label="Layers list"
> >
{listItems} <ul className="maputnik-layer-list-container">
</ul> {listItems}
</div> </ul>
</div>
</section>
} }
} }

View file

@ -100,6 +100,7 @@ class LayerListItem extends React.Component {
const visibilityAction = this.props.visibility === 'visible' ? 'show' : 'hide'; const visibilityAction = this.props.visibility === 'visible' ? 'show' : 'hide';
return <li return <li
id={this.props.id}
key={this.props.layerId} key={this.props.layerId}
onClick={e => this.props.onLayerSelect(this.props.layerIndex)} onClick={e => this.props.onLayerSelect(this.props.layerIndex)}
data-wd-key={"layer-list-item:"+this.props.layerId} data-wd-key={"layer-list-item:"+this.props.layerId}

View file

@ -247,3 +247,4 @@ export default class MapMapboxGl extends React.Component {
} }
} }
} }

View file

@ -32,17 +32,12 @@ export default class Modal extends React.Component {
}); });
} }
getApplicationNode() {
return document.getElementById('app');
}
render() { render() {
if(this.props.isOpen) { if(this.props.isOpen) {
return <AriaModal return <AriaModal
titleText={this.props.title} titleText={this.props.title}
underlayClickExits={this.props.underlayClickExits} underlayClickExits={this.props.underlayClickExits}
underlayProps={this.props.underlayProps} underlayProps={this.props.underlayProps}
getApplicationNode={this.getApplicationNode}
data-wd-key={this.props["data-wd-key"]} data-wd-key={this.props["data-wd-key"]}
verticallyCenter={true} verticallyCenter={true}
onExit={this.onClose} onExit={this.onClose}

View file

@ -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>
} }

View file

@ -29,8 +29,8 @@ export default class ModalDebug extends React.Component {
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={'Debug'} title={'Debug'}
> >
<div className="maputnik-modal-section maputnik-modal-shortcuts"> <section className="maputnik-modal-section maputnik-modal-shortcuts">
<h4>Options</h4> <h1>Options</h1>
{this.props.renderer === 'mbgljs' && {this.props.renderer === 'mbgljs' &&
<ul> <ul>
{Object.entries(this.props.mapboxGlDebugOptions).map(([key, val]) => { {Object.entries(this.props.mapboxGlDebugOptions).map(([key, val]) => {
@ -53,9 +53,9 @@ export default class ModalDebug extends React.Component {
})} })}
</ul> </ul>
} }
</div> </section>
<div className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h4>Links</h4> <h1>Links</h1>
<p> <p>
<a <a
target="_blank" target="_blank"
@ -65,7 +65,7 @@ export default class ModalDebug extends React.Component {
Open in OSM Open in OSM
</a> &mdash; Opens the current view on openstreetmap.org </a> &mdash; Opens the current view on openstreetmap.org
</p> </p>
</div> </section>
</Modal> </Modal>
} }
} }

View file

@ -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'
@ -68,50 +67,41 @@ export default class ModalExport extends React.Component {
className="maputnik-export-modal" className="maputnik-export-modal"
> >
<div className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h4>Download Style</h4> <h1>Download Style</h1>
<p> <p>
Download a JSON style to your computer. Download a JSON style to your computer.
</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> </section>
</Modal> </Modal>
} }

View file

@ -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>
} }

View file

@ -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,23 +22,23 @@ 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)}
> >
<header className="maputnik-public-style-header"> <div className="maputnik-public-style-header">
<h4>{this.props.title}</h4> <div>{this.props.title}</div>
<span className="maputnik-space" /> <span className="maputnik-space" />
<MdAddCircleOutline /> <MdAddCircleOutline />
</header> </div>
<div <div
className="maputnik-public-style-thumbnail" className="maputnik-public-style-thumbnail"
style={{ style={{
backgroundImage: `url(${this.props.thumbnailUrl})` backgroundImage: `url(${this.props.thumbnailUrl})`
}} }}
></div> ></div>
</Button> </InputButton>
</div> </div>
} }
} }
@ -198,20 +198,21 @@ export default class ModalOpen extends React.Component {
> >
{errorElement} {errorElement}
<section className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h2>Upload Style</h2> <h1>Upload Style</h1>
<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" aria-label="Style file">
<Button className="maputnik-upload-button"><MdFileUpload /> Upload</Button> <InputButton className="maputnik-upload-button"><MdFileUpload /> Upload</InputButton>
</FileReaderInput> </FileReaderInput>
</section> </section>
<section className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h2>Load from URL</h2>
<p>
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>
<form onSubmit={this.onSubmitUrl}> <form onSubmit={this.onSubmitUrl}>
<FieldUrl <h1>Load from URL</h1>
<p>
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>
<InputUrl
aria-label="Style URL"
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,18 +222,18 @@ 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>
<section className="maputnik-modal-section maputnik-modal-section--shrink"> <section className="maputnik-modal-section maputnik-modal-section--shrink">
<h2>Gallery Styles</h2> <h1>Gallery Styles</h1>
<p> <p>
Open one of the publicly available styles to start from. Open one of the publicly available styles to start from.
</p> </p>

View file

@ -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>
} }

View file

@ -105,7 +105,7 @@ export default class ModalShortcuts extends React.Component {
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={'Shortcuts'} title={'Shortcuts'}
> >
<div className="maputnik-modal-section maputnik-modal-shortcuts"> <section className="maputnik-modal-section maputnik-modal-shortcuts">
<p> <p>
Press <code>ESC</code> to lose focus of any active elements, then press one of: Press <code>ESC</code> to lose focus of any active elements, then press one of:
</p> </p>
@ -125,7 +125,7 @@ export default class ModalShortcuts extends React.Component {
</li> </li>
})} })}
</ul> </ul>
</div> </section>
</Modal> </Modal>
} }
} }

View file

@ -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,14 @@ 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
aria-label={`Remove '${this.props.sourceId}' source`}
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 +208,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>
} }
} }
@ -291,28 +292,28 @@ export default class ModalSources extends React.Component {
onOpenToggle={this.props.onOpenToggle} onOpenToggle={this.props.onOpenToggle}
title={'Sources'} title={'Sources'}
> >
<div className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h4>Active Sources</h4> <h1>Active Sources</h1>
{activeSources} {activeSources}
</div> </section>
<div className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h4>Choose Public Source</h4> <h1>Choose Public Source</h1>
<p> <p>
Add one of the publicly available sources to your style. Add one of the publicly available sources to your style.
</p> </p>
<div className="maputnik-public-sources" style={{maxwidth: 500}}> <div className="maputnik-public-sources" style={{maxwidth: 500}}>
{tilesetOptions} {tilesetOptions}
</div> </div>
</div> </section>
<div className="maputnik-modal-section"> <section className="maputnik-modal-section">
<h4>Add New Source</h4> <h1>Add New Source</h1>
<p>Add a new source to your style. You can only choose the source type and id at creation time!</p> <p>Add a new source to your style. You can only choose the source type and id at creation time!</p>
<AddSource <AddSource
onAdd={(sourceId, source) => this.props.onStyleChanged(addSource(mapStyle, sourceId, source))} onAdd={(sourceId, source) => this.props.onStyleChanged(addSource(mapStyle, sourceId, source))}
/> />
</div> </section>
</Modal> </Modal>
} }
} }

View file

@ -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} />

View file

@ -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 dont track you, so we dont know how you use Maputnik. Help us make Maputnik better for you by completing a 7minute survey carried out by our contributing designer.</p> <p className="maputnik-modal-survey__description">We dont track you, so we dont know how you use Maputnik. Help us make Maputnik better for you by completing a 7minute 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>

View file

@ -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,16 @@ 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
aria-label="key"
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
aria-label="function"
value={filterOp} value={filterOp}
onChange={newFilterOp => this.onFilterPartChanged(newFilterOp, propertyName, filterArgs)} onChange={newFilterOp => this.onFilterPartChanged(newFilterOp, propertyName, filterArgs)}
options={otherFilterOps} options={otherFilterOps}
@ -79,7 +81,8 @@ 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
aria-label="value"
value={filterArgs.join(',')} value={filterArgs.join(',')}
onChange={ v=> this.onFilterPartChanged(filterOp, propertyName, v.split(','))} onChange={ v=> this.onFilterPartChanged(filterOp, propertyName, v.split(','))}
/> />

View file

@ -1,132 +1,51 @@
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: ({fieldSpec}) => (Object.keys(fieldSpec.values).length <= 3 ? Fieldset : Block),
number: () => Block,
boolean: () => Block,
array: () => Fieldset,
resolvedImage: () => Block,
number: () => Block,
string: () => Block,
formatted: () => 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,
name: this.props.fieldName, const typeBlockFn = typeMap[fieldType];
onChange: newValue => this.props.onChange(this.props.fieldName, newValue)
let TypeBlock;
if (typeBlockFn) {
TypeBlock = typeBlockFn(props);
}
else {
console.warn("No such type for '%s'", fieldType);
TypeBlock = Block;
} }
function childNodes() { return <TypeBlock
switch(this.props.fieldSpec.type) { label={props.label}
case 'number': return ( action={props.action}
<FieldNumber fieldSpec={this.props.fieldSpec}
{...commonProps} >
min={this.props.fieldSpec.minimum} <InputSpec {...props} />
max={this.props.fieldSpec.maximum} </TypeBlock>
/>
)
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>
);
} }
} }

View file

@ -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'
@ -91,7 +91,7 @@ export default class DataProperty extends React.Component {
getDataFunctionTypes(fieldSpec) { getDataFunctionTypes(fieldSpec) {
if (fieldSpec.expression.interpolated) { if (fieldSpec.expression.interpolated) {
return ["categorical", "interval", "exponential", "identity"] return ["interpolate", "categorical", "interval", "exponential", "identity"]
} }
else { else {
return ["categorical", "interval", "identity"] return ["categorical", "interval", "identity"]
@ -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);
@ -161,6 +167,18 @@ export default class DataProperty extends React.Component {
this.onChange(this.props.fieldName, changedValue) this.onChange(this.props.fieldName, changedValue)
} }
changeDataType(propVal) {
if (propVal === "interpolate") {
this.props.onChangeToZoomFunction();
}
else {
this.onChange(this.props.fieldName, {
...this.props.value,
type: propVal,
});
}
}
changeDataProperty(propName, propVal) { changeDataProperty(propName, propVal) {
if (propVal) { if (propVal) {
this.props.value[propName] = propVal this.props.value[propName] = propVal
@ -188,6 +206,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 +214,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 +243,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 +275,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 +287,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.changeDataType(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>
} }
} }

View file

@ -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>
} }
} }

View file

@ -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",

View file

@ -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>
} }
} }

View file

@ -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>
} }
} }
} }

View file

@ -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>
} }
} }

View file

@ -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'
@ -124,6 +126,12 @@ export default class ZoomProperty extends React.Component {
this.props.onChange(this.props.fieldName, changedValue) this.props.onChange(this.props.fieldName, changedValue)
} }
changeDataType = (type) => {
if (type !== "interpolate") {
this.props.onChangeToDataFunction(type);
}
}
render() { render() {
const {fieldName, fieldType, errors} = this.props; const {fieldName, fieldType, errors} = this.props;
@ -143,52 +151,94 @@ 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 aria-label="Zoom"
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}
/> />
</div> </td>
<div className="maputnik-zoom-spec-property-stop-value"> <td>
<SpecField <InputSpec
fieldName={this.props.fieldName} aria-label="Output value"
fieldSpec={this.props.fieldSpec} fieldName={this.props.fieldName}
value={value} fieldSpec={this.props.fieldSpec}
onChange={(_, newValue) => this.changeZoomStop(idx, zoomLevel, newValue)} value={value}
/> onChange={(_, newValue) => this.changeZoomStop(idx, zoomLevel, newValue)}
</div> />
</div> </td>
</Block> <td>
{deleteStopBtn}
</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.changeDataType(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['property-type'] === 'data-driven') {
return ["interpolate", "categorical", "interval", "exponential", "identity"];
}
else {
return ["interpolate"];
}
}
} }

View file

@ -63,9 +63,13 @@ h4 {
} }
input:focus, input:focus,
textarea:focus,
*[role="button"]:focus,
button:focus,
.maputnik-toolbar-link:focus,
select:focus { select:focus {
color: $color-white !important; color: $color-white;
outline: #8e8e8e auto 1px !important; outline: #8e8e8e auto 1px;
} }
label:hover { label:hover {

View file

@ -30,14 +30,12 @@
.maputnik-doc-button { .maputnik-doc-button {
opacity: 1; opacity: 1;
pointer-events: all; pointer-events: all;
cursor: pointer;
} }
} }
// DOC LABEL // DOC LABEL
.maputnik-doc { .maputnik-doc {
&-target {
cursor: help;
}
&-wrapper { &-wrapper {
display: inline-block; display: inline-block;
@ -69,7 +67,7 @@
background: $color-black; background: $color-black;
color: white; color: white;
border: none; border: none;
padding: 0; padding: 4px;
svg { svg {
pointer-events: none; pointer-events: none;
@ -89,6 +87,7 @@
font-size: 12px; font-size: 12px;
margin-top: $margin-3; margin-top: $margin-3;
line-height: 1.5; line-height: 1.5;
flex: 1 0;
} }
.maputnik-doc-target:hover .maputnik-doc-popup { .maputnik-doc-target:hover .maputnik-doc-popup {
@ -163,18 +162,27 @@
// INPUT BLOCK // INPUT BLOCK
.maputnik-input-block { .maputnik-input-block {
margin: $margin-3; margin: $margin-3;
display: flex;
flex-wrap: wrap;
&-label { &-label {
color: $color-lowgray; color: $color-lowgray;
display: inline-block;
user-select: none; user-select: none;
width: 50%; width: 32%;
vertical-align: top; vertical-align: top;
font-size: 12px; font-size: 12px;
} }
&-action {
color: $color-lowgray;
user-select: none;
width: 18%;
vertical-align: top;
font-size: 12px;
text-align: right;
}
&-content { &-content {
display: inline-block;
width: 50%; width: 50%;
} }
} }
@ -183,6 +191,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 {
@ -270,6 +279,7 @@
.maputnik-input-block-content { .maputnik-input-block-content {
display: block; display: block;
width: auto; width: auto;
flex: 1;
} }
.maputnik-input-block-label { .maputnik-input-block-label {

View file

@ -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);
} }
} }
@ -124,9 +124,13 @@
// CHECKBOX // CHECKBOX
.maputnik-checkbox { .maputnik-checkbox {
position: absolute; position: absolute;
z-index: -1; z-index: 1;
opacity: 0; width: 100%;
width: 50%; height: 100%;
margin: 0;
left: 0;
top: 0;
outline: none;
&-wrapper { &-wrapper {
@extend .maputnik-input; @extend .maputnik-input;
@ -141,6 +145,7 @@
} }
&-box { &-box {
z-index: 2;
display: inline-block; display: inline-block;
text-align: center; text-align: center;
height: 24px; height: 24px;
@ -152,12 +157,26 @@
border-width: 2px; border-width: 2px;
border-color: $color-gray; border-color: $color-gray;
transition: background-color 0.1s ease-out; transition: background-color 0.1s ease-out;
position: absolute;
top: 0;
left: 0;
// So they fall through to the <input/> beneath
pointer-events: none;
@media screen and (prefers-reduced-motion: reduce) { @media screen and (prefers-reduced-motion: reduce) {
transition-duration: 0ms; transition-duration: 0ms;
} }
} }
&:focus {
z-index: 8;
}
&:focus + &-box {
z-index: 9;
}
&-icon { &-icon {
width: 50%; width: 50%;
height: 50%; height: 50%;
@ -203,7 +222,6 @@
.maputnik-input-block-content { .maputnik-input-block-content {
position: relative; position: relative;
overflow: hidden;
} }
.SpecDoc__sdk-support { .SpecDoc__sdk-support {

View file

@ -143,6 +143,8 @@
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
// HACK: Remove important
outline: none !important;
} }
&-group-header { &-group-header {

View file

@ -8,12 +8,23 @@
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 {
padding-top: $margin-3; padding-top: $margin-3;
padding-bottom: $margin-3; padding-bottom: $margin-3;
h1 {
font-size: $font-size-4;
}
h2 {
font-size: $font-size-5;
}
/* Bug fix: <http://stackoverflow.com/questions/28636832/firefox-overflow-y-not-working-with-nested-flexbox> */ /* Bug fix: <http://stackoverflow.com/questions/28636832/firefox-overflow-y-not-working-with-nested-flexbox> */
min-height: 0; min-height: 0;
@ -50,7 +61,7 @@
} }
.maputnik-modal-scroller { .maputnik-modal-scroller {
max-height: calc(100vh - 35px); flex: 1;
overflow-y: auto; overflow-y: auto;
} }
@ -116,6 +127,15 @@
.maputnik-export-modal { .maputnik-export-modal {
width: 400px; width: 400px;
max-width: 100%; max-width: 100%;
.maputnik-input-block-label {
width: 50%;
align-self: center;
}
.maputnik-input-block-action {
width: 0;
}
} }
.maputnik-add-layer { .maputnik-add-layer {
@ -195,8 +215,8 @@
width: 30%; width: 30%;
} }
.maputnik-input-block-content { .maputnik-input-block-action {
width: 70%; width: 20%;
} }
} }
@ -207,8 +227,8 @@
width: 30%; width: 30%;
} }
.maputnik-input-block-content { .maputnik-input-block-action {
width: 70%; width: 20%;
} }
} }

View file

@ -73,8 +73,8 @@
width: 30%; width: 30%;
} }
.maputnik-input-block:not(:first-child) .maputnik-input-block-label { .maputnik-input-block-action {
visibility: hidden; display: none;
} }
.maputnik-input-block-content { .maputnik-input-block-content {

View file

@ -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;
}
}

View file

@ -41,6 +41,7 @@
<!-- TODO: Import dynamically --> <!-- TODO: Import dynamically -->
<!-- From <https://github.com/hail2u/color-blindness-emulation> --> <!-- From <https://github.com/hail2u/color-blindness-emulation> -->
<svg <svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
version="1.1"> version="1.1">
<defs> <defs>

View file

@ -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}

View file

@ -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}

View file

@ -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}
/> />

View file

@ -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}
/> />

View file

@ -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}

View file

@ -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}

View file

@ -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>
);
};

View 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>
};

View file

@ -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}

View file

@ -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}

View file

@ -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}

View file

@ -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}
/> />

View file

@ -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>
);
};

View file

@ -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}

View 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>
};

View 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>
);
};

Some files were not shown because too many files have changed in this diff Show more