diff --git a/.storybook/main.js b/.storybook/main.js index 81ab3a1..55c1c31 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -16,7 +16,6 @@ module.exports = { ...config, module: { rules: [ - ...config.module.rules, ...rules, ] } diff --git a/package-lock.json b/package-lock.json index 4c73eae..ed128af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17124,6 +17124,11 @@ "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", "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": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", diff --git a/package.json b/package.json index 047d86e..77c9c20 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "react-sortable-hoc": "^1.11.0", "reconnecting-websocket": "^4.4.0", "slugify": "^1.3.6", + "string-hash": "^1.1.3", "url": "^0.11.0" }, "jshintConfig": { diff --git a/src/components/App.jsx b/src/components/App.jsx index bd91f0f..a88cd56 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -6,6 +6,7 @@ import get from 'lodash.get' import {unset} from 'lodash' import arrayMove from 'array-move' import url from 'url' +import hash from "string-hash"; import MapMapboxGl from './MapMapboxGl' 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') this.styleStore = new StyleStore() } - this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle)) + this.styleStore.latestStyle(mapStyle => this.onStyleChanged(mapStyle, {initialLoad: true})) if(Debug.enabled()) { Debug.set("maputnik", "styleStore", this.styleStore); @@ -322,9 +323,14 @@ export default class App extends React.Component { opts = { save: true, addRevision: true, + initialLoad: false, ...opts, }; + if (opts.initialLoad) { + this.getInitialStateFromUrl(newStyle); + } + const errors = validate(newStyle, latest) || []; // 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, }, () => { this.fetchSources(); + this.setStateInUrl(); }) } @@ -542,7 +549,7 @@ export default class App extends React.Component { setMapState = (newState) => { this.setState({ mapState: newState - }) + }, this.setStateInUrl); } setDefaultValues = (styleObj) => { @@ -697,8 +704,85 @@ export default class App extends React.Component { } + 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) => { - this.setState({ selectedLayerIndex: index }) + this.setState({ selectedLayerIndex: index }, this.setStateInUrl); } setModal(modalName, value) { @@ -711,7 +795,7 @@ export default class App extends React.Component { ...this.state.isOpen, [modalName]: value } - }) + }, this.setStateInUrl) } toggleModal(modalName) { diff --git a/src/components/AppToolbar.jsx b/src/components/AppToolbar.jsx index 758a097..052e1ee 100644 --- a/src/components/AppToolbar.jsx +++ b/src/components/AppToolbar.jsx @@ -145,31 +145,37 @@ export default class AppToolbar extends React.Component { const views = [ { id: "map", + group: "general", title: "Map", }, { id: "inspect", + group: "general", title: "Inspect", disabled: this.props.renderer !== 'mbgljs', }, { id: "filter-deuteranopia", - title: "Deuteranopia color filter", + group: "color-accessibility", + title: "Deuteranopia filter", disabled: !colorAccessibilityFiltersEnabled, }, { id: "filter-protanopia", - title: "Protanopia color filter", + group: "color-accessibility", + title: "Protanopia filter", disabled: !colorAccessibilityFiltersEnabled, }, { id: "filter-tritanopia", - title: "Tritanopia color filter", + group: "color-accessibility", + title: "Tritanopia filter", disabled: !colorAccessibilityFiltersEnabled, }, { id: "filter-achromatopsia", - title: "Achromatopsia color filter", + group: "color-accessibility", + title: "Achromatopsia filter", disabled: !colorAccessibilityFiltersEnabled, }, ]; @@ -242,13 +248,22 @@ export default class AppToolbar extends React.Component { onChange={(e) => this.handleSelection(e.target.value)} value={currentView.id} > - {views.map((item) => { + {views.filter(v => v.group === "general").map((item) => { return ( ); })} + + {views.filter(v => v.group === "color-accessibility").map((item) => { + return ( + + ); + })} + diff --git a/src/components/Block.jsx b/src/components/Block.jsx index c462203..4f798d0 100644 --- a/src/components/Block.jsx +++ b/src/components/Block.jsx @@ -50,44 +50,15 @@ export default class Block extends React.Component { "maputnik-input-block--wide": this.props.wideMode, "maputnik-action-block": this.props.action })} - > - {this.props.fieldSpec && -
- -
- } - {!this.props.fieldSpec && - - } - {this.props.action && -
- {this.props.action} -
- } -
- {this.props.children} -
- {errors.length > 0 && -
- {[].concat(this.props.error).map((error, idx) => { - return
{error.message}
- })} + > +
} } diff --git a/src/components/FieldArray.jsx b/src/components/FieldArray.jsx index 7c78961..00103fc 100644 --- a/src/components/FieldArray.jsx +++ b/src/components/FieldArray.jsx @@ -1,108 +1,21 @@ import React from 'react' import PropTypes from 'prop-types' -import FieldString from './FieldString' -import FieldNumber from './FieldNumber' +import Block from './Block' +import InputArray from './InputArray' +import Fieldset from './Fieldset' export default class FieldArray extends React.Component { static propTypes = { - value: PropTypes.array, - type: 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); - } - }); + ...InputArray.propTypes, + name: PropTypes.string, } render() { - const {value} = this.state; + const {props} = this; - const containsValues = ( - value.length > 0 && - !value.every(val => { - return (val === "" || val === undefined) - }) - ); - - const inputs = Array(this.props.length).fill(null).map((_, i) => { - if(this.props.type === 'number') { - return - } else { - return - } - }) - - return
- {inputs} -
+ return
+ +
} } diff --git a/src/components/FieldAutocomplete.jsx b/src/components/FieldAutocomplete.jsx index 47c4864..da63105 100644 --- a/src/components/FieldAutocomplete.jsx +++ b/src/components/FieldAutocomplete.jsx @@ -1,97 +1,20 @@ import React from 'react' import PropTypes from 'prop-types' -import classnames from 'classnames' -import Autocomplete from 'react-autocomplete' +import Block from './Block' +import InputAutocomplete from './InputAutocomplete' -const MAX_HEIGHT = 140; - export default class FieldAutocomplete extends React.Component { static propTypes = { - value: PropTypes.string, - 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); + ...InputAutocomplete.propTypes, } render() { - return
{ - this.autocompleteMenuEl = el; - }} - > - item[0]} - onSelect={v => this.onChange(v)} - onChange={(e, v) => this.onChange(v)} - shouldItemRender={(item, value="") => { - if (typeof(value) === "string") { - return item[0].toLowerCase().indexOf(value.toLowerCase()) > -1 - } - }} - renderItem={(item, isHighlighted) => ( -
- {item[1]} -
- )} - /> -
+ const {props} = this; + + return + + } } diff --git a/src/components/FieldCheckbox.jsx b/src/components/FieldCheckbox.jsx index 066f3c8..7cdc08b 100644 --- a/src/components/FieldCheckbox.jsx +++ b/src/components/FieldCheckbox.jsx @@ -1,34 +1,20 @@ import React from 'react' import PropTypes from 'prop-types' +import Block from './Block' +import InputCheckbox from './InputCheckbox' + export default class FieldCheckbox extends React.Component { static propTypes = { - value: PropTypes.bool, - style: PropTypes.object, - onChange: PropTypes.func, - } - - static defaultProps = { - value: false, + ...InputCheckbox.propTypes, } render() { - return + const {props} = this; + + return + + } } diff --git a/src/components/FieldColor.jsx b/src/components/FieldColor.jsx index 1f73b4e..2e9067b 100644 --- a/src/components/FieldColor.jsx +++ b/src/components/FieldColor.jsx @@ -1,132 +1,20 @@ 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'; +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 { static propTypes = { - onChange: PropTypes.func.isRequired, - 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); + ...InputColor.propTypes, } 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 {props} = this; - const picker =
- this.onChangeNoCheck(formatColor(c))} - /> -
-
- - var swatchStyle = { - backgroundColor: this.props.value - }; - - return
- {this.state.pickerOpened && picker} -
- this.colorInput = input} - onClick={this.togglePicker} - style={this.props.style} - name={this.props.name} - placeholder={this.props.default} - value={this.props.value ? this.props.value : ""} - onChange={(e) => this.onChange(e.target.value)} - /> -
+ return + + } } diff --git a/src/components/FieldComment.jsx b/src/components/FieldComment.jsx index d64ca2e..9c6125e 100644 --- a/src/components/FieldComment.jsx +++ b/src/components/FieldComment.jsx @@ -2,9 +2,9 @@ import React from 'react' import PropTypes from 'prop-types' import Block from './Block' -import FieldString from './FieldString' +import InputString from './InputString' -export default class BlockComment extends React.Component { +export default class FieldComment extends React.Component { static propTypes = { value: PropTypes.string, onChange: PropTypes.func.isRequired, @@ -20,7 +20,7 @@ export default class BlockComment extends React.Component { fieldSpec={fieldSpec} data-wd-key="layer-comment" > - } } - diff --git a/src/components/FieldDynamicArray.jsx b/src/components/FieldDynamicArray.jsx index c8f3b71..47bbbaf 100644 --- a/src/components/FieldDynamicArray.jsx +++ b/src/components/FieldDynamicArray.jsx @@ -1,136 +1,21 @@ import React from 'react' import PropTypes from 'prop-types' -import FieldString from './FieldString' -import FieldNumber from './FieldNumber' -import Button from './Button' -import {MdDelete} from 'react-icons/md' -import FieldDocLabel from './FieldDocLabel' -import FieldEnum from './FieldEnum' -import capitalize from 'lodash.capitalize' -import FieldUrl from './FieldUrl' - +import Block from './Block' +import InputDynamicArray from './InputDynamicArray' +import Fieldset from './Fieldset' 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, - } - - 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) + ...InputDynamicArray.propTypes, + name: PropTypes.string, } render() { - const inputs = this.values.map((v, i) => { - const deleteValueBtn= - let input; - if(this.props.type === 'url') { - input = - } - else if (this.props.type === 'number') { - input = - } - else if (this.props.type === 'enum') { - const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)]); + const {props} = this; - input = - } - else { - input = - } - - return
-
- {deleteValueBtn} -
-
- {input} -
-
- }) - - return
- {inputs} - -
- } -} - -class DeleteValueButton extends React.Component { - static propTypes = { - onClick: PropTypes.func, - } - - render() { - return + return
+ +
} } diff --git a/src/components/FieldEnum.jsx b/src/components/FieldEnum.jsx index a9e89a0..f4268ec 100644 --- a/src/components/FieldEnum.jsx +++ b/src/components/FieldEnum.jsx @@ -1,45 +1,20 @@ import React from 'react' import PropTypes from 'prop-types' -import FieldSelect from './FieldSelect' -import FieldMultiInput from './FieldMultiInput' - - -function optionsLabelLength(options) { - let sum = 0; - options.forEach(([_, label]) => { - sum += label.length - }) - return sum -} +import InputEnum from './InputEnum' +import Block from './Block'; +import Fieldset from './Fieldset'; export default class FieldEnum 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, + ...InputEnum.propTypes, } render() { - const {options, value, onChange, name} = this.props; + const {props} = this; - if(options.length <= 3 && optionsLabelLength(options) <= 20) { - return - } else { - return - } + return
+ +
} } diff --git a/src/components/FieldFunction.jsx b/src/components/FieldFunction.jsx index 3cb25a2..ac5887b 100644 --- a/src/components/FieldFunction.jsx +++ b/src/components/FieldFunction.jsx @@ -315,6 +315,7 @@ export default class FieldFunction extends React.Component { ) } else if (dataType === "data_function") { + // TODO: Rename to FieldFunction **this file** shouldn't be called that specField = ( - } } - diff --git a/src/components/FieldJson.jsx b/src/components/FieldJson.jsx new file mode 100644 index 0000000..1f6ccea --- /dev/null +++ b/src/components/FieldJson.jsx @@ -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 + } +} + diff --git a/src/components/FieldMaxZoom.jsx b/src/components/FieldMaxZoom.jsx index a032915..2d38ad3 100644 --- a/src/components/FieldMaxZoom.jsx +++ b/src/components/FieldMaxZoom.jsx @@ -3,9 +3,9 @@ import PropTypes from 'prop-types' import {latest} from '@mapbox/mapbox-gl-style-spec' import Block from './Block' -import FieldNumber from './FieldNumber' +import InputNumber from './InputNumber' -export default class BlockMaxZoom extends React.Component { +export default class FieldMaxZoom extends React.Component { static propTypes = { value: PropTypes.number, onChange: PropTypes.func.isRequired, @@ -17,7 +17,7 @@ export default class BlockMaxZoom extends React.Component { error={this.props.error} data-wd-key="max-zoom" > - } } - diff --git a/src/components/FieldMinZoom.jsx b/src/components/FieldMinZoom.jsx index 99e2953..f0252d7 100644 --- a/src/components/FieldMinZoom.jsx +++ b/src/components/FieldMinZoom.jsx @@ -3,9 +3,9 @@ import PropTypes from 'prop-types' import {latest} from '@mapbox/mapbox-gl-style-spec' import Block from './Block' -import FieldNumber from './FieldNumber' +import InputNumber from './InputNumber' -export default class BlockMinZoom extends React.Component { +export default class FieldMinZoom extends React.Component { static propTypes = { value: PropTypes.number, onChange: PropTypes.func.isRequired, @@ -17,7 +17,7 @@ export default class BlockMinZoom extends React.Component { error={this.props.error} data-wd-key="min-zoom" > - } } - diff --git a/src/components/FieldMultiInput.jsx b/src/components/FieldMultiInput.jsx index 94d7ba9..ae5daf6 100644 --- a/src/components/FieldMultiInput.jsx +++ b/src/components/FieldMultiInput.jsx @@ -1,41 +1,21 @@ import React from 'react' import PropTypes from 'prop-types' -import classnames from 'classnames' -import Button from './Button' +import Block from './Block' +import InputMultiInput from './InputMultiInput' +import Fieldset from './Fieldset' + export default class FieldMultiInput extends React.Component { static propTypes = { - name: PropTypes.string.isRequired, - value: PropTypes.string.isRequired, - options: PropTypes.array.isRequired, - onChange: PropTypes.func.isRequired, + ...InputMultiInput.propTypes, } render() { - let options = this.props.options - if(options.length > 0 && !Array.isArray(options[0])) { - options = options.map(v => [v, v]) - } + const {props} = this; - const selectedValue = this.props.value || options[0][0] - const radios = options.map(([val, label])=> { - return - }) - - return
- {radios} -
+ return
+ +
} } diff --git a/src/components/FieldNumber.jsx b/src/components/FieldNumber.jsx index a74bb08..0d42ddf 100644 --- a/src/components/FieldNumber.jsx +++ b/src/components/FieldNumber.jsx @@ -1,232 +1,19 @@ import React from 'react' import PropTypes from 'prop-types' +import InputNumber from './InputNumber' +import Block from './Block' -let IDX = 0; export default class FieldNumber 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, - } - - static defaultProps = { - rangeStep: 1 - } - - constructor(props) { - super(props) - this.state = { - uuid: IDX++, - editing: false, - value: props.value, - dirtyValue: props.value, - } - } - - static getDerivedStateFromProps(props, state) { - if (!state.editing && props.value !== state.value) { - return { - value: props.value, - dirtyValue: props.value, - }; - } - return null; - } - - changeValue(newValue) { - const value = (newValue === "" || newValue === undefined) ? - undefined : - parseFloat(newValue); - - const hasChanged = this.props.value !== value; - if(this.isValid(value) && hasChanged) { - this.props.onChange(value) - this.setState({ - value: newValue, - }); - } - else if (!this.isValid(value) && hasChanged) { - this.setState({ - value: undefined, - }); - } - - this.setState({ - dirtyValue: newValue === "" ? undefined : newValue, - }) - } - - isValid(v) { - if (v === undefined) { - return true; - } - - const value = parseFloat(v) - if(isNaN(value)) { - return false - } - - if(!isNaN(this.props.min) && value < this.props.min) { - return false - } - - if(!isNaN(this.props.max) && value > this.props.max) { - return false - } - - return true - } - - resetValue = () => { - this.setState({editing: false}); - // Reset explicitly to default value if value has been cleared - if(this.state.value === "") { - return; - } - - // If set value is invalid fall back to the last valid value from props or at last resort the default value - if (!this.isValid(this.state.value)) { - if(this.isValid(this.props.value)) { - this.changeValue(this.props.value) - this.setState({dirtyValue: this.props.value}); - } else { - this.changeValue(undefined); - this.setState({dirtyValue: undefined}); - } - } - } - - onChangeRange = (e) => { - let value = parseFloat(e.target.value, 10); - const step = this.props.rangeStep; - let dirtyValue = value; - - if(step) { - // Can't do this with the range step attribute else we won't be able to set a high precision value via the text input. - const snap = value % step; - - // Round up/down to step - if (this._keyboardEvent) { - // If it's keyboard event we might get a low positive/negative value, - // for example we might go from 13 to 13.23, however because we know - // that came from a keyboard event we always want to increase by a - // single step value. - if (value < this.state.dirtyValue) { - value = this.state.value - step; - } - else { - value = this.state.value + step - } - dirtyValue = value; - } - else { - if (snap < step/2) { - value = value - snap; - } - else { - value = value + (step - snap); - }; - } - } - - this._keyboardEvent = false; - - // Clamp between min/max - value = Math.max(this.props.min, Math.min(this.props.max, value)); - - this.setState({value, dirtyValue}); - this.props.onChange(value); + ...InputNumber.propTypes, } render() { - if( - this.props.hasOwnProperty("min") && this.props.hasOwnProperty("max") && - this.props.min !== undefined && this.props.max !== undefined && - this.props.allowRange - ) { - const value = this.state.editing ? this.state.dirtyValue : this.state.value; - const defaultValue = this.props.default === undefined ? "" : this.props.default; - let inputValue; - if (this.state.editingRange) { - inputValue = this.state.value; - } - else { - inputValue = value; - } - - return
- { - this._keyboardEvent = true; - }} - onPointerDown={() => { - this.setState({editing: true, editingRange: true}); - }} - onPointerUp={() => { - // Safari doesn't get onBlur event - this.setState({editing: false, editingRange: false}); - }} - onBlur={() => { - this.setState({ - editing: false, - editingRange: false, - dirtyValue: this.state.value, - }); - }} - /> - { - this.setState({editing: true}); - }} - onChange={e => { - this.changeValue(e.target.value); - }} - onBlur={e => { - this.setState({editing: false}); - this.resetValue() - }} - /> -
- } - else { - const value = this.state.editing ? this.state.dirtyValue : this.state.value; - - return this.changeValue(e.target.value)} - onFocus={() => { - this.setState({editing: true}); - }} - onBlur={this.resetValue} - required={this.props.required} - /> - } + const {props} = this; + return + + } } diff --git a/src/components/FieldSelect.jsx b/src/components/FieldSelect.jsx index fc54eee..1d279c6 100644 --- a/src/components/FieldSelect.jsx +++ b/src/components/FieldSelect.jsx @@ -1,33 +1,20 @@ import React from 'react' import PropTypes from 'prop-types' +import Block from './Block' +import InputSelect from './InputSelect' + export default class FieldSelect 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, + ...InputSelect.propTypes, } - render() { - let options = this.props.options - if(options.length > 0 && !Array.isArray(options[0])) { - options = options.map(v => [v, v]) - } + const {props} = this; - return + return + + } } diff --git a/src/components/FieldSource.jsx b/src/components/FieldSource.jsx index bcee37f..7dcc96d 100644 --- a/src/components/FieldSource.jsx +++ b/src/components/FieldSource.jsx @@ -3,9 +3,9 @@ import PropTypes from 'prop-types' import {latest} from '@mapbox/mapbox-gl-style-spec' import Block from './Block' -import FieldAutocomplete from './FieldAutocomplete' +import InputAutocomplete from './InputAutocomplete' -export default class BlockSource extends React.Component { +export default class FieldSource extends React.Component { static propTypes = { value: PropTypes.string, wdKey: PropTypes.string, @@ -26,7 +26,7 @@ export default class BlockSource extends React.Component { error={this.props.error} data-wd-key={this.props.wdKey} > - [src, src])} @@ -34,4 +34,3 @@ export default class BlockSource extends React.Component { } } - diff --git a/src/components/FieldSourceLayer.jsx b/src/components/FieldSourceLayer.jsx index ff617f4..895d43d 100644 --- a/src/components/FieldSourceLayer.jsx +++ b/src/components/FieldSourceLayer.jsx @@ -3,9 +3,9 @@ import PropTypes from 'prop-types' import {latest} from '@mapbox/mapbox-gl-style-spec' import Block from './Block' -import FieldAutocomplete from './FieldAutocomplete' +import InputAutocomplete from './InputAutocomplete' -export default class BlockSourceLayer extends React.Component { +export default class FieldSourceLayer extends React.Component { static propTypes = { value: PropTypes.string, onChange: PropTypes.func, @@ -23,7 +23,7 @@ export default class BlockSourceLayer extends React.Component { return - } } - diff --git a/src/components/FieldString.jsx b/src/components/FieldString.jsx index d061d81..89d9327 100644 --- a/src/components/FieldString.jsx +++ b/src/components/FieldString.jsx @@ -1,92 +1,20 @@ import React from 'react' import PropTypes from 'prop-types' +import Block from './Block' +import InputString from './InputString' export default class FieldString 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, - } - - 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 {}; + ...InputString.propTypes, + name: PropTypes.string, } render() { - let tag; - let classes; + const {props} = this; - 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, { - "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, - }); + return + + } } diff --git a/src/components/FieldType.jsx b/src/components/FieldType.jsx index cc44523..d95c1c2 100644 --- a/src/components/FieldType.jsx +++ b/src/components/FieldType.jsx @@ -3,10 +3,10 @@ import PropTypes from 'prop-types' import {latest} from '@mapbox/mapbox-gl-style-spec' import Block from './Block' -import FieldSelect from './FieldSelect' -import FieldString from './FieldString' +import InputSelect from './InputSelect' +import InputString from './InputString' -export default class BlockType extends React.Component { +export default class FieldType extends React.Component { static propTypes = { value: PropTypes.string.isRequired, wdKey: PropTypes.string, @@ -25,13 +25,13 @@ export default class BlockType extends React.Component { error={this.props.error} > {this.props.disabled && - } {!this.props.disabled && - } } - diff --git a/src/components/FieldUrl.jsx b/src/components/FieldUrl.jsx index 8132eb8..3c53b19 100644 --- a/src/components/FieldUrl.jsx +++ b/src/components/FieldUrl.jsx @@ -1,100 +1,21 @@ import React, {Fragment} from 'react' import PropTypes from 'prop-types' -import FieldString from './FieldString' -import SmallError from './SmallError' +import InputUrl from './InputUrl' +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 = ( - - Must provide protocol { - isSsl - ? https:// - : <>http:// or https:// - } - - ); - } - else if ( - protocol && - protocol === "http:" && - window.location.protocol === "https:" - ) { - error = ( - - CORS policy won't allow fetching resources served over http from https, use a https:// domain - - ); - } - - return error; -} - export default class FieldUrl extends React.Component { static propTypes = { - "data-wd-key": PropTypes.string, - value: PropTypes.string, - style: PropTypes.object, - default: PropTypes.string, - onChange: PropTypes.func, - onInput: PropTypes.func, - multi: PropTypes.bool, - required: PropTypes.bool, - } - - 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); + ...InputUrl.propTypes, } render () { + const {props} = this; + return ( -
- - {this.state.error} -
+ + + ); } } diff --git a/src/components/Fieldset.jsx b/src/components/Fieldset.jsx new file mode 100644 index 0000000..4b7cada --- /dev/null +++ b/src/components/Fieldset.jsx @@ -0,0 +1,23 @@ +import React from 'react' +import PropTypes from 'prop-types' + + +let IDX = 0; + +export default class Fieldset extends React.Component { + constructor (props) { + super(props); + this._labelId = `fieldset_label_${(IDX++)}`; + } + + render () { + const {props} = this; + + return
+
{props.label}
+
+ {props.children} +
+
+ } +} diff --git a/src/components/FilterEditor.jsx b/src/components/FilterEditor.jsx index 55a04fd..d99d8a9 100644 --- a/src/components/FilterEditor.jsx +++ b/src/components/FilterEditor.jsx @@ -4,11 +4,11 @@ import { combiningFilterOps } from '../libs/filterops.js' import {mdiTableRowPlusAfter} from '@mdi/js'; import {latest, validate, migrate} from '@mapbox/mapbox-gl-style-spec' -import FieldSelect from './FieldSelect' +import InputSelect from './InputSelect' import Block from './Block' import SingleFilterEditor from './SingleFilterEditor' import FilterEditorBlock from './FilterEditorBlock' -import Button from './Button' +import InputButton from './InputButton' import Doc from './Doc' import ExpressionProperty from './_ExpressionProperty'; import {mdiFunctionVariant} from '@mdi/js'; @@ -191,7 +191,7 @@ export default class FilterEditor extends React.Component {

Nested filters are not supported.

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

    - - - - + - - - + - - + value={(this.props.mapStyle.metadata || {})['maputnik:thunderforest_access_token']} + onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")} + />
    - +
    diff --git a/src/components/ModalLoading.jsx b/src/components/ModalLoading.jsx index 6c34485..07303c3 100644 --- a/src/components/ModalLoading.jsx +++ b/src/components/ModalLoading.jsx @@ -1,7 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' -import Button from './Button' +import InputButton from './InputButton' import Modal from './Modal' @@ -34,9 +34,9 @@ export default class ModalLoading extends React.Component { {this.props.message}

    - +

    } diff --git a/src/components/ModalOpen.jsx b/src/components/ModalOpen.jsx index 15d564c..42bde38 100644 --- a/src/components/ModalOpen.jsx +++ b/src/components/ModalOpen.jsx @@ -2,9 +2,9 @@ import React from 'react' import PropTypes from 'prop-types' import ModalLoading from './ModalLoading' import Modal from './Modal' -import Button from './Button' +import InputButton from './InputButton' import FileReaderInput from 'react-file-reader-input' -import FieldUrl from './FieldUrl' +import InputUrl from './InputUrl' import {MdFileUpload} from 'react-icons/md' import {MdAddCircleOutline} from 'react-icons/md' @@ -22,7 +22,7 @@ class PublicStyle extends React.Component { render() { return
    -
    - + } } @@ -201,7 +201,7 @@ export default class ModalOpen extends React.Component {

    Upload Style

    Upload a JSON style from your computer.

    - + Upload @@ -211,7 +211,7 @@ export default class ModalOpen extends React.Component { Load from a URL. Note that the URL must have CORS enabled.

    -
    - + >Load from URL
    diff --git a/src/components/ModalSettings.jsx b/src/components/ModalSettings.jsx index 831d0ca..c3e733a 100644 --- a/src/components/ModalSettings.jsx +++ b/src/components/ModalSettings.jsx @@ -87,169 +87,158 @@ export default class ModalSettings extends React.Component { title={'Style Settings'} >
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    } diff --git a/src/components/ModalSources.jsx b/src/components/ModalSources.jsx index 2d6583c..475aa45 100644 --- a/src/components/ModalSources.jsx +++ b/src/components/ModalSources.jsx @@ -2,7 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import {latest} from '@mapbox/mapbox-gl-style-spec' import Modal from './Modal' -import Button from './Button' +import InputButton from './InputButton' import Block from './Block' import FieldString from './FieldString' import FieldSelect from './FieldSelect' @@ -24,7 +24,7 @@ class PublicSource extends React.Component { render() { return
    -
    - + } } @@ -83,13 +83,13 @@ class ActiveModalSourcesTypeEditor extends React.Component {
    #{this.props.sourceId} - +
    - - this.setState({ sourceId: v})} - /> - - - this.setState({mode: mode, source: this.defaultSource(mode)})} - value={this.state.mode} - /> - + this.setState({ sourceId: v})} + /> + this.setState({mode: mode, source: this.defaultSource(mode)})} + value={this.state.mode} + /> - +
    } } diff --git a/src/components/ModalSourcesTypeEditor.jsx b/src/components/ModalSourcesTypeEditor.jsx index 1ce995e..4a84b24 100644 --- a/src/components/ModalSourcesTypeEditor.jsx +++ b/src/components/ModalSourcesTypeEditor.jsx @@ -2,13 +2,12 @@ import React from 'react' import PropTypes from 'prop-types' import {latest} from '@mapbox/mapbox-gl-style-spec' import Block from './Block' -import FieldString from './FieldString' import FieldUrl from './FieldUrl' import FieldNumber from './FieldNumber' import FieldSelect from './FieldSelect' import FieldDynamicArray from './FieldDynamicArray' import FieldArray from './FieldArray' -import FieldJsonEditor from './FieldJsonEditor' +import FieldJson from './FieldJson' class TileJSONSourceEditor extends React.Component { @@ -20,15 +19,15 @@ class TileJSONSourceEditor extends React.Component { render() { return
    - - this.props.onChange({ - ...this.props.source, - url: url - })} - /> - + this.props.onChange({ + ...this.props.source, + url: url + })} + /> {this.props.children}
    } @@ -50,36 +49,36 @@ class TileURLSourceEditor extends React.Component { renderTileUrls() { const tiles = this.props.source.tiles || []; - return - - + return } render() { return
    {this.renderTileUrls()} - - this.props.onChange({ - ...this.props.source, - minzoom: minzoom - })} - /> - - - this.props.onChange({ - ...this.props.source, - maxzoom: maxzoom - })} - /> - + this.props.onChange({ + ...this.props.source, + minzoom: minzoom + })} + /> + this.props.onChange({ + ...this.props.source, + maxzoom: maxzoom + })} + /> {this.props.children}
    @@ -104,26 +103,26 @@ class ImageSourceEditor extends React.Component { } return
    - - this.props.onChange({ - ...this.props.source, - url, - })} - /> - + this.props.onChange({ + ...this.props.source, + url, + })} + /> {["top left", "top right", "bottom right", "bottom left"].map((label, idx) => { return ( - - changeCoord(idx, val)} - /> - + changeCoord(idx, val)} + /> ); })}
    @@ -155,25 +154,25 @@ class VideoSourceEditor extends React.Component { } return
    - - - + {["top left", "top right", "bottom right", "bottom left"].map((label, idx) => { return ( - - changeCoord(idx, val)} - /> - + changeCoord(idx, val)} + /> ); })}
    @@ -187,15 +186,15 @@ class GeoJSONSourceUrlEditor extends React.Component { } render() { - return - this.props.onChange({ - ...this.props.source, - data: data - })} - /> - + return this.props.onChange({ + ...this.props.source, + data: data + })} + /> } } @@ -207,7 +206,7 @@ class GeoJSONSourceFieldJsonEditor extends React.Component { render() { return - case 'tilejson_raster-dem': return case 'tilexyz_raster-dem': return - - this.props.onChange({ - ...this.props.source, - encoding: encoding - })} - value={this.props.source.encoding || latest.source_raster_dem.encoding.default} - /> - + this.props.onChange({ + ...this.props.source, + encoding: encoding + })} + value={this.props.source.encoding || latest.source_raster_dem.encoding.default} + /> case 'image': return case 'video': return diff --git a/src/components/ModalSurvey.jsx b/src/components/ModalSurvey.jsx index 9087142..13ab939 100644 --- a/src/components/ModalSurvey.jsx +++ b/src/components/ModalSurvey.jsx @@ -1,7 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' -import Button from './Button' +import InputButton from './InputButton' import Modal from './Modal' import logoImage from 'maputnik-design/logos/logo-color.svg' @@ -26,10 +26,10 @@ export default class ModalSurvey extends React.Component { title="Maputnik Survey" >
    -
    +

    You + Maputnik = Maputnik better for you

    We don’t track you, so we don’t know how you use Maputnik. Help us make Maputnik better for you by completing a 7–minute survey carried out by our contributing designer.

    - + Take the Maputnik Survey

    It takes 7 minutes, tops! Every question is optional.

    diff --git a/src/components/SingleFilterEditor.jsx b/src/components/SingleFilterEditor.jsx index 676f78c..2b09431 100644 --- a/src/components/SingleFilterEditor.jsx +++ b/src/components/SingleFilterEditor.jsx @@ -2,9 +2,9 @@ import React from 'react' import PropTypes from 'prop-types' import { otherFilterOps } from '../libs/filterops.js' -import FieldString from './FieldString' -import FieldAutocomplete from './FieldAutocomplete' -import FieldSelect from './FieldSelect' +import InputString from './InputString' +import InputAutocomplete from './InputAutocomplete' +import InputSelect from './InputSelect' function tryParseInt(v) { if (v === '') return v @@ -64,14 +64,14 @@ export default class SingleFilterEditor extends React.Component { return
    - [propName, propName])} onChange={newPropertyName => this.onFilterPartChanged(filterOp, newPropertyName, filterArgs)} />
    - this.onFilterPartChanged(newFilterOp, propertyName, filterArgs)} options={otherFilterOps} @@ -79,7 +79,7 @@ export default class SingleFilterEditor extends React.Component {
    {filterArgs.length > 0 &&
    - this.onFilterPartChanged(filterOp, propertyName, v.split(','))} /> diff --git a/src/components/SpecField.jsx b/src/components/SpecField.jsx index dc6d2df..45be8a2 100644 --- a/src/components/SpecField.jsx +++ b/src/components/SpecField.jsx @@ -1,132 +1,41 @@ import React from 'react' import PropTypes from 'prop-types' +import Block from './Block' +import InputSpec from './InputSpec' +import Fieldset from './Fieldset' -import FieldColor from './FieldColor' -import FieldNumber from './FieldNumber' -import FieldCheckbox from './FieldCheckbox' -import FieldString from './FieldString' -import FieldSelect from './FieldSelect' -import FieldMultiInput from './FieldMultiInput' -import FieldArray from './FieldArray' -import FieldDynamicArray from './FieldDynamicArray' -import FieldFont from './FieldFont' -import FieldSymbol from './FieldSymbol' -import FieldEnum from './FieldEnum' -import capitalize from 'lodash.capitalize' -const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image'] +const typeMap = { + color: Block, + enum: Fieldset, + number: Block, + boolean: Block, + array: Fieldset, + resolvedImage: Block, + number: Block, + string: Block +}; -function labelFromFieldName(fieldName) { - let label = fieldName.split('-').slice(1).join(' ') - if(label.length > 0) { - label = label.charAt(0).toUpperCase() + label.slice(1); - } - return label -} - -function optionsLabelLength(options) { - let sum = 0; - options.forEach(([_, label]) => { - sum += label.length - }) - return sum -} - -/** Display any field from the Mapbox GL style spec and - * choose the correct field component based on the @{fieldSpec} - * to display @{value}. */ export default class SpecField extends React.Component { 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, + ...InputSpec.propTypes, + name: PropTypes.string, } render() { - const commonProps = { - 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) + const {props} = this; + + const fieldType = props.fieldSpec.type; + let TypeBlock = typeMap[fieldType]; + + if (!TypeBlock) { + console.warn("No such type for '%s'", fieldType); + TypeBlock = Block; } - function childNodes() { - switch(this.props.fieldSpec.type) { - case 'number': return ( - - ) - case 'enum': - const options = Object.keys(this.props.fieldSpec.values).map(v => [v, capitalize(v)]) - - return - case 'resolvedImage': - case 'formatted': - case 'string': - if(iconProperties.indexOf(this.props.fieldName) >= 0) { - return - } else { - return - } - case 'color': return ( - - ) - case 'boolean': return ( - - ) - case 'array': - if(this.props.fieldName === 'text-font') { - return - } else { - if (this.props.fieldSpec.length) { - return - } else { - return - } - } - default: return null - } - } - - return ( -
    - {childNodes.call(this)} -
    - ); + return + + } } + diff --git a/src/components/_DataProperty.jsx b/src/components/_DataProperty.jsx index 6c0a4ce..7b12bb9 100644 --- a/src/components/_DataProperty.jsx +++ b/src/components/_DataProperty.jsx @@ -2,12 +2,12 @@ import React from 'react' import PropTypes from 'prop-types' import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js'; -import Button from './Button' -import SpecField from './SpecField' -import FieldNumber from './FieldNumber' -import FieldString from './FieldString' -import FieldSelect from './FieldSelect' -import Doc from './Doc' +import InputButton from './InputButton' +import InputSpec from './InputSpec' +import InputNumber from './InputNumber' +import InputString from './InputString' +import InputSelect from './InputSelect' +import FieldDocLabel from './FieldDocLabel' import Block from './Block' import docUid from '../libs/document-uid' import sortNumerically from '../libs/sort-numerically' @@ -149,8 +149,14 @@ export default class DataProperty extends React.Component { changeStop(changeIdx, stopData, value) { const stops = this.props.value.stops.slice(0) - const changedStop = stopData.zoom === undefined ? stopData.value : stopData - stops[changeIdx] = [changedStop, value] + // const changedStop = stopData.zoom === undefined ? stopData.value : stopData + stops[changeIdx] = [ + { + ...stopData, + zoom: (stopData.zoom === undefined) ? 0 : stopData.zoom, + }, + value + ]; const orderedStops = this.orderStopsByZoom(stops); @@ -188,6 +194,7 @@ export default class DataProperty extends React.Component { const deleteStopBtn = const dataProps = { + 'aria-label': "Input value", label: "Data value", value: dataLevel, onChange: newData => this.changeStop(idx, { zoom: zoomLevel, value: newData }, value) @@ -195,16 +202,17 @@ export default class DataProperty extends React.Component { let dataInput; if(this.props.value.type === "categorical") { - dataInput = + dataInput = } else { - dataInput = + dataInput = } let zoomInput = null; if(zoomLevel !== undefined) { - zoomInput =
    - + this.changeStop(idx, {zoom: newZoom, value: dataLevel}, value)} min={0} @@ -223,6 +231,27 @@ export default class DataProperty extends React.Component { }).join(""); const error = message ? {message} : undefined; + return + + {zoomInput} + + + {dataInput} + + + this.changeStop(idx, {zoom: zoomLevel, value: dataLevel}, newValue)} + /> + + + {deleteStopBtn} + + + return
    - -
    - -
    - +
    + {labelFromFieldName(this.props.fieldName)} +
    +
    - this.changeDataProperty("type", propVal)} title={"Select a type of data scale (default is 'categorical')."} options={this.getDataFunctionTypes(this.props.fieldSpec)} />
    -
    -
    - + +
    - this.changeDataProperty("property", propVal)} />
    -
    + {dataFields && -
    - + this.changeDataProperty("default", propVal)} /> -
    - this.changeDataProperty("default", propVal)} - /> -
    + + } + {dataFields && +
    + + + + + + + + + + + {dataFields} + +
    Stops
    ZoomInput valueOutput value
    } - -
    - {dataFields && - <> - {dataFields} - - - } - +
    + {dataFields && + + + + Add stop + + } + + + + Convert to expression + +
    +
    +
    } } diff --git a/src/components/_DeleteStopButton.jsx b/src/components/_DeleteStopButton.jsx index 840e1c3..d387cb3 100644 --- a/src/components/_DeleteStopButton.jsx +++ b/src/components/_DeleteStopButton.jsx @@ -1,7 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' -import Button from './Button' +import InputButton from './InputButton' import {MdDelete} from 'react-icons/md' @@ -11,12 +11,12 @@ export default class DeleteStopButton extends React.Component { } render() { - return + } } diff --git a/src/components/_ExpressionProperty.jsx b/src/components/_ExpressionProperty.jsx index f7f6803..09bdfda 100644 --- a/src/components/_ExpressionProperty.jsx +++ b/src/components/_ExpressionProperty.jsx @@ -2,13 +2,13 @@ import React from 'react' import PropTypes from 'prop-types' import Block from './Block' -import Button from './Button' +import InputButton from './InputButton' import {MdDelete, MdUndo} from 'react-icons/md' import FieldString from './FieldString' import labelFromFieldName from './_labelFromFieldName' import stringifyPretty from 'json-stringify-pretty-compact' -import FieldJsonEditor from './FieldJsonEditor' +import FieldJson from './FieldJson' export default class ExpressionProperty extends React.Component { @@ -59,7 +59,7 @@ export default class ExpressionProperty extends React.Component { const deleteStopBtn = ( <> {this.props.onUndo && - + } - + ); @@ -114,7 +114,7 @@ export default class ExpressionProperty extends React.Component { action={deleteStopBtn} wideMode={true} > - + + + } +} + diff --git a/src/components/FieldFont.jsx b/src/components/_FieldFont.jsx similarity index 74% rename from src/components/FieldFont.jsx rename to src/components/_FieldFont.jsx index 2aece10..cd6ca15 100644 --- a/src/components/FieldFont.jsx +++ b/src/components/_FieldFont.jsx @@ -1,4 +1,5 @@ import React from 'react' +import Block from './Block' import PropTypes from 'prop-types' import FieldAutocomplete from './FieldAutocomplete' @@ -39,17 +40,22 @@ export default class FieldFont extends React.Component { render() { const inputs = this.values.map((value, i) => { - return [f, f])} - onChange={this.changeFont.bind(this, i)} - /> + > + [f, f])} + onChange={this.changeFont.bind(this, i)} + /> + }) - return
    - {inputs} -
    + return +
      + {inputs} +
    +
    } } diff --git a/src/components/_FieldId.jsx b/src/components/_FieldId.jsx new file mode 100644 index 0000000..ebeedc9 --- /dev/null +++ b/src/components/_FieldId.jsx @@ -0,0 +1,28 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import {latest} from '@mapbox/mapbox-gl-style-spec' +import Block from './Block' +import FieldString from './FieldString' + +export default class BlockId extends React.Component { + static propTypes = { + value: PropTypes.string.isRequired, + wdKey: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + error: PropTypes.object, + } + + render() { + return + + + } +} + diff --git a/src/components/_FieldMaxZoom.jsx b/src/components/_FieldMaxZoom.jsx new file mode 100644 index 0000000..a032915 --- /dev/null +++ b/src/components/_FieldMaxZoom.jsx @@ -0,0 +1,31 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import {latest} from '@mapbox/mapbox-gl-style-spec' +import Block from './Block' +import FieldNumber from './FieldNumber' + +export default class BlockMaxZoom extends React.Component { + static propTypes = { + value: PropTypes.number, + onChange: PropTypes.func.isRequired, + error: PropTypes.object, + } + + render() { + return + + + } +} + diff --git a/src/components/_FieldMinZoom.jsx b/src/components/_FieldMinZoom.jsx new file mode 100644 index 0000000..99e2953 --- /dev/null +++ b/src/components/_FieldMinZoom.jsx @@ -0,0 +1,31 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import {latest} from '@mapbox/mapbox-gl-style-spec' +import Block from './Block' +import FieldNumber from './FieldNumber' + +export default class BlockMinZoom extends React.Component { + static propTypes = { + value: PropTypes.number, + onChange: PropTypes.func.isRequired, + error: PropTypes.object, + } + + render() { + return + + + } +} + diff --git a/src/components/_FieldSource.jsx b/src/components/_FieldSource.jsx new file mode 100644 index 0000000..bcee37f --- /dev/null +++ b/src/components/_FieldSource.jsx @@ -0,0 +1,37 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import {latest} from '@mapbox/mapbox-gl-style-spec' +import Block from './Block' +import FieldAutocomplete from './FieldAutocomplete' + +export default class BlockSource extends React.Component { + static propTypes = { + value: PropTypes.string, + wdKey: PropTypes.string, + onChange: PropTypes.func, + sourceIds: PropTypes.array, + error: PropTypes.object, + } + + static defaultProps = { + onChange: () => {}, + sourceIds: [], + } + + render() { + return + [src, src])} + /> + + } +} + diff --git a/src/components/_FieldSourceLayer.jsx b/src/components/_FieldSourceLayer.jsx new file mode 100644 index 0000000..ff617f4 --- /dev/null +++ b/src/components/_FieldSourceLayer.jsx @@ -0,0 +1,35 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import {latest} from '@mapbox/mapbox-gl-style-spec' +import Block from './Block' +import FieldAutocomplete from './FieldAutocomplete' + +export default class BlockSourceLayer extends React.Component { + static propTypes = { + value: PropTypes.string, + onChange: PropTypes.func, + sourceLayerIds: PropTypes.array, + isFixed: PropTypes.bool, + } + + static defaultProps = { + onChange: () => {}, + sourceLayerIds: [], + isFixed: false + } + + render() { + return + [l, l])} + /> + + } +} + diff --git a/src/components/FieldSymbol.jsx b/src/components/_FieldSymbol.jsx similarity index 100% rename from src/components/FieldSymbol.jsx rename to src/components/_FieldSymbol.jsx diff --git a/src/components/_FieldType.jsx b/src/components/_FieldType.jsx new file mode 100644 index 0000000..cc44523 --- /dev/null +++ b/src/components/_FieldType.jsx @@ -0,0 +1,53 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import {latest} from '@mapbox/mapbox-gl-style-spec' +import Block from './Block' +import FieldSelect from './FieldSelect' +import FieldString from './FieldString' + +export default class BlockType extends React.Component { + static propTypes = { + value: PropTypes.string.isRequired, + wdKey: PropTypes.string, + onChange: PropTypes.func.isRequired, + error: PropTypes.object, + disabled: PropTypes.bool, + } + + static defaultProps = { + disabled: false, + } + + render() { + return + {this.props.disabled && + + } + {!this.props.disabled && + + } + + } +} + diff --git a/src/components/_FunctionButtons.jsx b/src/components/_FunctionButtons.jsx index eeda73c..bacd344 100644 --- a/src/components/_FunctionButtons.jsx +++ b/src/components/_FunctionButtons.jsx @@ -1,7 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' -import Button from './Button' +import InputButton from './InputButton' import {MdFunctions, MdInsertChart} from 'react-icons/md' 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 = { fieldSpec: PropTypes.object, onZoomClick: PropTypes.func, @@ -33,11 +33,11 @@ export default class FunctionButtons extends React.Component { } render() { - let makeZoomButton, makeDataButton, expressionButton; + let makeZoomInputButton, makeDataInputButton, expressionInputButton; if (this.props.fieldSpec.expression.parameters.includes('zoom')) { - expressionButton = ( - + ); - makeZoomButton = + if (this.props.fieldSpec['property-type'] === 'data-driven') { - makeDataButton = + } return
    - {expressionButton} - {makeDataButton} - {makeZoomButton} + {expressionInputButton} + {makeDataInputButton} + {makeZoomInputButton}
    } else { - return
    {expressionButton}
    + return
    {expressionInputButton}
    } } } diff --git a/src/components/_SpecProperty.jsx b/src/components/_SpecProperty.jsx index 43f2448..e4df518 100644 --- a/src/components/_SpecProperty.jsx +++ b/src/components/_SpecProperty.jsx @@ -37,13 +37,12 @@ export default class SpecProperty extends React.Component { const error = errors[fieldType+"."+fieldName]; - return - - + /> } } diff --git a/src/components/_ZoomProperty.jsx b/src/components/_ZoomProperty.jsx index 5a088f5..79ba4d9 100644 --- a/src/components/_ZoomProperty.jsx +++ b/src/components/_ZoomProperty.jsx @@ -2,9 +2,11 @@ import React from 'react' import PropTypes from 'prop-types' import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js'; -import Button from './Button' -import SpecField from './SpecField' -import FieldNumber from './FieldNumber' +import InputButton from './InputButton' +import InputSpec from './InputSpec' +import InputNumber from './InputNumber' +import InputSelect from './InputSelect' +import FieldDocLabel from './FieldDocLabel' import Block from './Block' import DeleteStopButton from './_DeleteStopButton' @@ -143,52 +145,92 @@ export default class ZoomProperty extends React.Component { }).join(""); const error = message ? {message} : undefined; - return -
    -
    - this.changeZoomStop(idx, changedStop, value)} - min={0} - max={22} - /> -
    -
    - this.changeZoomStop(idx, zoomLevel, newValue)} - /> -
    -
    -
    + + this.changeZoomStop(idx, changedStop, value)} + min={0} + max={22} + /> + + + this.changeZoomStop(idx, zoomLevel, newValue)} + /> + + + {deleteStopBtn} + + }); - return
    - {zoomFields} - - + // return
    + return
    +
    + {labelFromFieldName(this.props.fieldName)} +
    + +
    + this.changeDataProperty("type", propVal)} + title={"Select a type of data scale (default is 'categorical')."} + options={this.getDataFunctionTypes(this.props.fieldSpec)} + /> +
    +
    +
    + + + + + + + + + + {zoomFields} + +
    Stops
    ZoomOutput value
    +
    +
    + + + + Add stop + + + + + Convert to expression + +
    +
    +
    } + + getDataFunctionTypes(fieldSpec) { + if (fieldSpec.expression.interpolated) { + return ["categorical", "interval", "exponential", "identity", "interpolate"] + } + else { + return ["categorical", "interval", "identity", "interpolate"] + } + } + } diff --git a/src/styles/_components.scss b/src/styles/_components.scss index eedefb5..d86896b 100644 --- a/src/styles/_components.scss +++ b/src/styles/_components.scss @@ -183,6 +183,7 @@ .maputnik-input-block-label { display: inline-block; width: 32%; + margin-bottom: $margin-3; } .maputnik-input-block-action { diff --git a/src/styles/_input.scss b/src/styles/_input.scss index 9f19d58..16f08bc 100644 --- a/src/styles/_input.scss +++ b/src/styles/_input.scss @@ -77,13 +77,13 @@ .maputnik-array-block-action { vertical-align: top; display: inline-block; - width: 14%; + width: 2em; } .maputnik-array-block-content { vertical-align: top; display: inline-block; - width: 86%; + width: calc(100% - 2em); } } diff --git a/src/styles/_modal.scss b/src/styles/_modal.scss index 5e321f2..b1413fe 100644 --- a/src/styles/_modal.scss +++ b/src/styles/_modal.scss @@ -8,6 +8,9 @@ z-index: 3; position: relative; font-family: $font-family; + display: flex; + flex-direction: column; + max-height: 100vh; } .maputnik-modal-section { @@ -50,7 +53,7 @@ } .maputnik-modal-scroller { - max-height: calc(100vh - 35px); + flex: 1; overflow-y: auto; } diff --git a/src/styles/_zoomproperty.scss b/src/styles/_zoomproperty.scss index 38e26c5..1108691 100644 --- a/src/styles/_zoomproperty.scss +++ b/src/styles/_zoomproperty.scss @@ -73,10 +73,6 @@ width: 30%; } - .maputnik-input-block:not(:first-child) .maputnik-input-block-label { - visibility: hidden; - } - .maputnik-input-block-content { width: 70%; } diff --git a/src/styles/index.scss b/src/styles/index.scss index 11cca10..8acf3a2 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -35,3 +35,135 @@ 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; + } +} + diff --git a/stories/FieldArray.stories.js b/stories/FieldArray.stories.js index 0ee6aab..2b4e9db 100644 --- a/stories/FieldArray.stories.js +++ b/stories/FieldArray.stories.js @@ -17,6 +17,7 @@ export const NumberType = () => { return ( { return ( { return ( { return ( @@ -30,6 +31,7 @@ export const BasicChecked = () => { return ( diff --git a/stories/FieldColor.stories.js b/stories/FieldColor.stories.js index 31a35ac..b88b792 100644 --- a/stories/FieldColor.stories.js +++ b/stories/FieldColor.stories.js @@ -17,7 +17,7 @@ export const Basic = () => { return ( diff --git a/stories/FieldComment.stories.js b/stories/FieldComment.stories.js deleted file mode 100644 index 6c815d9..0000000 --- a/stories/FieldComment.stories.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import {useActionState} from './helper'; -import FieldComment from '../src/components/FieldComment'; -import {Wrapper} from './ui'; -import {withA11y} from '@storybook/addon-a11y'; - -export default { - title: 'FieldComment', - component: FieldComment, - decorators: [withA11y], -}; - - -export const Basic = () => { - const [value, setValue] = useActionState("onChange", "Hello\nworld"); - - return ( - - - - ); -}; - diff --git a/stories/FieldDynamicArray.stories.js b/stories/FieldDynamicArray.stories.js index 85555fd..1fef7d9 100644 --- a/stories/FieldDynamicArray.stories.js +++ b/stories/FieldDynamicArray.stories.js @@ -17,6 +17,7 @@ export const NumberType = () => { return ( { return ( { return ( { return ( { return ( { return ( { return ( { - const fonts = ["Comic Sans", "Helvectica", "Gotham"]; - const [value, setValue] = useActionState("onChange", ["Comic Sans"]); - - return ( - - - - ); -}; - diff --git a/stories/FieldFunction.stories.js b/stories/FieldFunction.stories.js new file mode 100644 index 0000000..9efca27 --- /dev/null +++ b/stories/FieldFunction.stories.js @@ -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
    + {}} + value={value} + errors={[]} + fieldName={"Color"} + fieldType={"color"} + fieldSpec={latest['paint_fill']['fill-color']} + /> +
    +}; + diff --git a/stories/FieldId.stories.js b/stories/FieldId.stories.js deleted file mode 100644 index b2b97ae..0000000 --- a/stories/FieldId.stories.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import {useActionState} from './helper'; -import FieldId from '../src/components/FieldId'; -import {Wrapper} from './ui'; -import {withA11y} from '@storybook/addon-a11y'; - -export default { - title: 'FieldId', - component: FieldId, - decorators: [withA11y], -}; - - -export const Basic = () => { - const [value, setValue] = useActionState("onChange", "water"); - - return ( - - - - ); -}; - diff --git a/stories/FieldMaxZoom.stories.js b/stories/FieldMaxZoom.stories.js deleted file mode 100644 index 729f889..0000000 --- a/stories/FieldMaxZoom.stories.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import {useActionState} from './helper'; -import FieldMaxZoom from '../src/components/FieldMaxZoom'; -import {Wrapper} from './ui'; -import {withA11y} from '@storybook/addon-a11y'; - -export default { - title: 'FieldMaxZoom', - component: FieldMaxZoom, - decorators: [withA11y], -}; - - -export const Basic = () => { - const [value, setValue] = useActionState("onChange", 12); - - return ( - - - - ); -}; - diff --git a/stories/FieldMinZoom.stories.js b/stories/FieldMinZoom.stories.js deleted file mode 100644 index f88eaee..0000000 --- a/stories/FieldMinZoom.stories.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import {useActionState} from './helper'; -import FieldMinZoom from '../src/components/FieldMinZoom'; -import {Wrapper} from './ui'; -import {withA11y} from '@storybook/addon-a11y'; - -export default { - title: 'FieldMinZoom', - component: FieldMinZoom, - decorators: [withA11y], -}; - - -export const Basic = () => { - const [value, setValue] = useActionState("onChange", 2); - - return ( - - - - ); -}; - diff --git a/stories/FieldMultiInput.stories.js b/stories/FieldMultiInput.stories.js index 443bb14..444d38a 100644 --- a/stories/FieldMultiInput.stories.js +++ b/stories/FieldMultiInput.stories.js @@ -18,6 +18,7 @@ export const Basic = () => { return ( { return ( @@ -30,7 +30,7 @@ export const Range = () => { return ( { return ( { - const [value, setValue] = useActionState("onChange", "openmaptiles"); - - return ( - - - - ); -}; - diff --git a/stories/FieldSourceLayer.stories.js b/stories/FieldSourceLayer.stories.js deleted file mode 100644 index 5d8b0ec..0000000 --- a/stories/FieldSourceLayer.stories.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import {useActionState} from './helper'; -import FieldSourceLayer from '../src/components/FieldSourceLayer'; -import {Wrapper} from './ui'; -import {withA11y} from '@storybook/addon-a11y'; - -export default { - title: 'FieldSourceLayer', - component: FieldSourceLayer, - decorators: [withA11y], -}; - - -export const Basic = () => { - const [value, setValue] = useActionState("onChange", "water"); - - return ( - - - - ); -}; - diff --git a/stories/FieldString.stories.js b/stories/FieldString.stories.js index 2889b45..9a65417 100644 --- a/stories/FieldString.stories.js +++ b/stories/FieldString.stories.js @@ -17,49 +17,7 @@ export const Basic = () => { return ( - - ); -}; - -export const WithDefault = () => { - const [value, setValue] = useActionState("onChange", null); - - return ( - - - - ); -}; - -export const Multiline = () => { - const [value, setValue] = useActionState("onChange", "Hello\nworld"); - - return ( - - - - ); -}; - -export const MultilineWithDefault = () => { - const [value, setValue] = useActionState("onChange", null); - - return ( - - diff --git a/stories/FieldSymbol.stories.js b/stories/FieldSymbol.stories.js deleted file mode 100644 index 300eabf..0000000 --- a/stories/FieldSymbol.stories.js +++ /dev/null @@ -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 ( - - - - ); -}; - - diff --git a/stories/FieldType.stories.js b/stories/FieldType.stories.js deleted file mode 100644 index 2312bca..0000000 --- a/stories/FieldType.stories.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import {useActionState} from './helper'; -import FieldType from '../src/components/FieldType'; -import {Wrapper} from './ui'; -import {withA11y} from '@storybook/addon-a11y'; - -export default { - title: 'FieldType', - component: FieldType, - decorators: [withA11y], -}; - - -export const Basic = () => { - const [value, setValue] = useActionState("onChange", "background"); - - return ( - - - - ); -}; - diff --git a/stories/FieldUrl.stories.js b/stories/FieldUrl.stories.js index b30d7bc..091cd81 100644 --- a/stories/FieldUrl.stories.js +++ b/stories/FieldUrl.stories.js @@ -17,6 +17,7 @@ export const Valid = () => { return ( { return ( { + const types = [ + 'fill-extrusion', + 'raster', + 'hillshade', + 'heatmap', + 'fill', + 'background', + 'line', + 'symbol', + 'circle', + 'INVALID', + ] + + return + + + + + + + + + {types.map(type => ( + + + + + ))} + +
    IDPreview
    + {type} + + +
    +
    +}; + + + + + + diff --git a/stories/InputArray.stories.js b/stories/InputArray.stories.js new file mode 100644 index 0000000..ce50607 --- /dev/null +++ b/stories/InputArray.stories.js @@ -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 ( + + + + ); +}; + +export const StringType = () => { + const [value, setValue] = useActionState("onChange", ["a", "b", "c"]); + + return ( + + + + ); +}; + + diff --git a/stories/InputAutocomplete.stories.js b/stories/InputAutocomplete.stories.js new file mode 100644 index 0000000..108e918 --- /dev/null +++ b/stories/InputAutocomplete.stories.js @@ -0,0 +1,29 @@ +import React from 'react'; +import {useActionState} from './helper'; +import InputAutocomplete from '../src/components/InputAutocomplete'; +import {InputContainer} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + +export default { + title: 'InputAutocomplete', + component: InputAutocomplete, + decorators: [withA11y], +}; + + +export const Basic = () => { + const options = [["FOO", "foo"], ["BAR", "bar"], ["BAZ", "baz"]]; + const [value, setValue] = useActionState("onChange", "bar"); + + return ( + + + + ); +}; + diff --git a/stories/InputButton.stories.js b/stories/InputButton.stories.js new file mode 100644 index 0000000..e984568 --- /dev/null +++ b/stories/InputButton.stories.js @@ -0,0 +1,21 @@ +import React from 'react'; +import InputButton from '../src/components/InputButton'; +import {action} from '@storybook/addon-actions'; +import {InputContainer} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + + +export default { + title: 'InputButton', + component: InputButton, + decorators: [withA11y], +}; + +export const Basic = () => ( + + + Hello InputButton + + +); + diff --git a/stories/InputCheckbox.stories.js b/stories/InputCheckbox.stories.js new file mode 100644 index 0000000..0ec0da4 --- /dev/null +++ b/stories/InputCheckbox.stories.js @@ -0,0 +1,41 @@ +import React from 'react'; +import {useActionState} from './helper'; +import InputCheckbox from '../src/components/InputCheckbox'; +import {InputContainer} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + +export default { + title: 'InputCheckbox', + component: InputCheckbox, + decorators: [withA11y], +}; + + +export const BasicUnchecked = () => { + const [value, setValue] = useActionState("onChange", false); + + return ( + + + + ); +}; + +export const BasicChecked = () => { + const [value, setValue] = useActionState("onChange", true); + + return ( + + + + ); +}; + diff --git a/stories/InputColor.stories.js b/stories/InputColor.stories.js new file mode 100644 index 0000000..7d34c57 --- /dev/null +++ b/stories/InputColor.stories.js @@ -0,0 +1,27 @@ +import React from 'react'; +import {useActionState} from './helper'; +import InputColor from '../src/components/InputColor'; +import {InputContainer} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + +export default { + title: 'InputColor', + component: InputColor, + decorators: [withA11y], +}; + + +export const Basic = () => { + const [color, setColor] = useActionState("onChange", "#ff0000"); + + return ( + + + + ); +}; + diff --git a/stories/InputDynamicArray.stories.js b/stories/InputDynamicArray.stories.js new file mode 100644 index 0000000..fde4438 --- /dev/null +++ b/stories/InputDynamicArray.stories.js @@ -0,0 +1,55 @@ +import React from 'react'; +import {useActionState} from './helper'; +import InputDynamicArray from '../src/components/InputDynamicArray'; +import {Wrapper} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + +export default { + title: 'InputDynamicArray', + component: InputDynamicArray, + decorators: [withA11y], +}; + + +export const NumberType = () => { + const [value, setValue] = useActionState("onChange", [1,2,3]); + + return ( + + + + ); +}; + +export const UrlType = () => { + const [value, setValue] = useActionState("onChange", ["http://example.com"]); + + return ( + + + + ); +}; + +export const EnumType = () => { + const [value, setValue] = useActionState("onChange", ["foo"]); + + return ( + + + + ); +}; diff --git a/stories/InputEnum.stories.js b/stories/InputEnum.stories.js new file mode 100644 index 0000000..802a2de --- /dev/null +++ b/stories/InputEnum.stories.js @@ -0,0 +1,77 @@ +import React from 'react'; +import {useActionState} from './helper'; +import InputEnum from '../src/components/InputEnum'; +import {InputContainer} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + +export default { + title: 'InputEnum', + component: InputEnum, + decorators: [withA11y], +}; + + +export const BasicFew = () => { + const options = ["Foo", "Bar", "Baz"]; + const [value, setValue] = useActionState("onChange", "Foo"); + + return ( + + + + ); +}; + +export const BasicFewWithDefault = () => { + const options = ["Foo", "Bar", "Baz"]; + const [value, setValue] = useActionState("onChange", null); + + return ( + + + + ); +}; + +export const BasicMany = () => { + const options = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; + const [value, setValue] = useActionState("onChange", "a"); + + return ( + + + + ); +}; + +export const BasicManyWithDefault = () => { + const options = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; + const [value, setValue] = useActionState("onChange", "a"); + + return ( + + + + ); +}; + + + diff --git a/stories/InputJson.stories.js b/stories/InputJson.stories.js new file mode 100644 index 0000000..200f532 --- /dev/null +++ b/stories/InputJson.stories.js @@ -0,0 +1,27 @@ +import React from 'react'; +import InputJson from '../src/components/InputJson'; +import {action} from '@storybook/addon-actions'; +import {Wrapper} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + + +export default { + title: 'InputJson', + component: InputJson, + decorators: [withA11y], +}; + +export const Basic = () => { + const layer = { + id: "background", + type: "background", + }; + + return + + +}; + diff --git a/stories/InputMultiInput.stories.js b/stories/InputMultiInput.stories.js new file mode 100644 index 0000000..05a8aaf --- /dev/null +++ b/stories/InputMultiInput.stories.js @@ -0,0 +1,29 @@ +import React from 'react'; +import {useActionState} from './helper'; +import InputMultiInput from '../src/components/InputMultiInput'; +import {InputContainer} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + +export default { + title: 'InputMultiInput', + component: InputMultiInput, + decorators: [withA11y], +}; + + +export const Basic = () => { + const options = [["FOO", "foo"], ["BAR", "bar"], ["BAZ", "baz"]]; + const [value, setValue] = useActionState("onChange", "FOO"); + + return ( + + + + ); +}; + + diff --git a/stories/InputNumber.stories.js b/stories/InputNumber.stories.js new file mode 100644 index 0000000..3132275 --- /dev/null +++ b/stories/InputNumber.stories.js @@ -0,0 +1,44 @@ +import React from 'react'; +import {useActionState} from './helper'; +import InputNumber from '../src/components/InputNumber'; +import {InputContainer} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + +export default { + title: 'InputNumber', + component: InputNumber, + decorators: [withA11y], +}; + +export const Basic = () => { + const [value, setValue] = useActionState("onChange", 1); + + return ( + + + + ); +}; + +export const Range = () => { + const [value, setValue] = useActionState("onChange", 1); + + return ( + + + + ); +}; + diff --git a/stories/InputSelect.stories.js b/stories/InputSelect.stories.js new file mode 100644 index 0000000..48a1497 --- /dev/null +++ b/stories/InputSelect.stories.js @@ -0,0 +1,30 @@ +import React from 'react'; +import {useActionState} from './helper'; +import InputSelect from '../src/components/InputSelect'; +import {InputContainer} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + +export default { + title: 'InputSelect', + component: InputSelect, + decorators: [withA11y], +}; + + +export const Basic = () => { + const options = [["FOO", "Foo"], ["BAR", "Bar"], ["BAZ", "Baz"]]; + const [value, setValue] = useActionState("onChange", "FOO"); + + return ( + + + + ); +}; + + + diff --git a/stories/InputString.stories.js b/stories/InputString.stories.js new file mode 100644 index 0000000..0d33528 --- /dev/null +++ b/stories/InputString.stories.js @@ -0,0 +1,69 @@ +import React from 'react'; +import {useActionState} from './helper'; +import InputString from '../src/components/InputString'; +import {InputContainer} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + +export default { + title: 'InputString', + component: InputString, + decorators: [withA11y], +}; + + +export const Basic = () => { + const [value, setValue] = useActionState("onChange", "Hello world"); + + return ( + + + + ); +}; + +export const WithDefault = () => { + const [value, setValue] = useActionState("onChange", null); + + return ( + + + + ); +}; + +export const Multiline = () => { + const [value, setValue] = useActionState("onChange", "Hello\nworld"); + + return ( + + + + ); +}; + +export const MultilineWithDefault = () => { + const [value, setValue] = useActionState("onChange", null); + + return ( + + + + ); +}; + diff --git a/stories/InputUrl.stories.js b/stories/InputUrl.stories.js new file mode 100644 index 0000000..4b2bd53 --- /dev/null +++ b/stories/InputUrl.stories.js @@ -0,0 +1,42 @@ +import React from 'react'; +import {useActionState} from './helper'; +import InputUrl from '../src/components/InputUrl'; +import {InputContainer} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + +export default { + title: 'InputUrl', + component: InputUrl, + decorators: [withA11y], +}; + + +export const Valid = () => { + const [value, setValue] = useActionState("onChange", "http://example.com"); + + return ( + + + + ); +}; + +export const Invalid = () => { + const [value, setValue] = useActionState("onChange", "foo"); + + return ( + + + + ); +}; + + diff --git a/stories/LayerEditor.stories.js b/stories/LayerEditor.stories.js new file mode 100644 index 0000000..0af25a8 --- /dev/null +++ b/stories/LayerEditor.stories.js @@ -0,0 +1,202 @@ +import React from 'react'; +import LayerEditor from '../src/components/LayerEditor'; +import {action} from '@storybook/addon-actions'; +import {withA11y} from '@storybook/addon-a11y'; +import {latest} from '@mapbox/mapbox-gl-style-spec' + + +export default { + title: 'LayerEditor', + component: LayerEditor, + decorators: [withA11y], +}; + +export const Background = () => ( +
    + {}} + onLayerIdChange={() => {}} + onMoveLayer={() => {}} + onLayerDestroy={() => {}} + onLayerCopy={() => {}} + onLayerVisibilityToggle={() => {}} + isFirstLayer={true} + isLastLayer={false} + layerIndex={0} + /> +
    +); + +export const Fill = () => ( +
    + {}} + onLayerIdChange={() => {}} + onMoveLayer={() => {}} + onLayerDestroy={() => {}} + onLayerCopy={() => {}} + onLayerVisibilityToggle={() => {}} + isFirstLayer={true} + isLastLayer={false} + layerIndex={0} + /> +
    +); + +export const Line = () => ( +
    + {}} + onLayerIdChange={() => {}} + onMoveLayer={() => {}} + onLayerDestroy={() => {}} + onLayerCopy={() => {}} + onLayerVisibilityToggle={() => {}} + isFirstLayer={true} + isLastLayer={false} + layerIndex={0} + /> +
    +); + +export const Symbol = () => ( +
    + {}} + onLayerIdChange={() => {}} + onMoveLayer={() => {}} + onLayerDestroy={() => {}} + onLayerCopy={() => {}} + onLayerVisibilityToggle={() => {}} + isFirstLayer={true} + isLastLayer={false} + layerIndex={0} + /> +
    +); + +export const Raster = () => ( +
    + {}} + onLayerIdChange={() => {}} + onMoveLayer={() => {}} + onLayerDestroy={() => {}} + onLayerCopy={() => {}} + onLayerVisibilityToggle={() => {}} + isFirstLayer={true} + isLastLayer={false} + layerIndex={0} + /> +
    +); + +export const Cirlce = () => ( +
    + {}} + onLayerIdChange={() => {}} + onMoveLayer={() => {}} + onLayerDestroy={() => {}} + onLayerCopy={() => {}} + onLayerVisibilityToggle={() => {}} + isFirstLayer={true} + isLastLayer={false} + layerIndex={0} + /> +
    +); + +export const FillExtrusion = () => ( +
    + {}} + onLayerIdChange={() => {}} + onMoveLayer={() => {}} + onLayerDestroy={() => {}} + onLayerCopy={() => {}} + onLayerVisibilityToggle={() => {}} + isFirstLayer={true} + isLastLayer={false} + layerIndex={0} + /> +
    +); + +export const Heatmap = () => ( +
    + {}} + onLayerIdChange={() => {}} + onMoveLayer={() => {}} + onLayerDestroy={() => {}} + onLayerCopy={() => {}} + onLayerVisibilityToggle={() => {}} + isFirstLayer={true} + isLastLayer={false} + layerIndex={0} + /> +
    +); + +export const Hillshade = () => ( +
    + {}} + onLayerIdChange={() => {}} + onMoveLayer={() => {}} + onLayerDestroy={() => {}} + onLayerCopy={() => {}} + onLayerVisibilityToggle={() => {}} + isFirstLayer={true} + isLastLayer={false} + layerIndex={0} + /> +
    +); + diff --git a/stories/LayerListItem.stories.js b/stories/LayerListItem.stories.js new file mode 100644 index 0000000..8870e7c --- /dev/null +++ b/stories/LayerListItem.stories.js @@ -0,0 +1,58 @@ +import React from 'react'; +import LayerList from '../src/components/LayerList'; +import {action} from '@storybook/addon-actions'; +import {Wrapper} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + + +export default { + title: 'LayerList', + component: LayerList, + decorators: [withA11y], +}; + +export const Basic = () => ( + +
    + {}} + onLayerSelect={() => {}} + onLayerDestroy={() => {}} + onLayerCopy={() => {}} + onLayerVisibilityToggle={() => {}} + onMoveLayer={() => {}} + sources={{}} + errors={[]} + /> +
    +
    +); + + diff --git a/stories/MapMapboxGl.stories.js b/stories/MapMapboxGl.stories.js new file mode 100644 index 0000000..a8dc589 --- /dev/null +++ b/stories/MapMapboxGl.stories.js @@ -0,0 +1,109 @@ +import React from 'react'; +import MapMapboxGl from '../src/components/MapMapboxGl'; +import {action} from '@storybook/addon-actions'; +import {Wrapper} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + + +export default { + title: 'MapMapboxGl', + component: MapMapboxGl, + decorators: [withA11y], +}; + +const mapStyle = { + "version": 8, + "sources": { + "test1": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [0, -10] + }, + "properties": {} + } + ] + } + }, + "test2": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [15, 10] + }, + "properties": {} + } + ] + } + }, + "test3": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-15, 10] + }, + "properties": {} + } + ] + } + } + }, + "sprite": "", + "glyphs": "https://orangemug.github.io/font-glyphs/glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "test1", + "type": "circle", + "source": "test1", + "paint": { + "circle-radius": 40, + "circle-color": "red" + } + }, + { + "id": "test2", + "type": "circle", + "source": "test2", + "paint": { + "circle-radius": 40, + "circle-color": "green" + } + }, + { + "id": "test3", + "type": "circle", + "source": "test3", + "paint": { + "circle-radius": 40, + "circle-color": "blue" + } + } + ] +} + +export const Basic = () => { + return
    + s} + /> +
    +}; + + diff --git a/stories/MapOpenLayers.stories.js b/stories/MapOpenLayers.stories.js new file mode 100644 index 0000000..a8458bd --- /dev/null +++ b/stories/MapOpenLayers.stories.js @@ -0,0 +1,112 @@ +import React from 'react'; +import MapOpenLayers from '../src/components/MapOpenLayers'; +import {action} from '@storybook/addon-actions'; +import {Wrapper} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + + +export default { + title: 'MapOpenLayers', + component: MapOpenLayers, + decorators: [withA11y], +}; + +const mapStyle = { + "version": 8, + "sources": { + "test1": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [0, -10] + }, + "properties": {} + } + ] + } + }, + "test2": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [15, 10] + }, + "properties": {} + } + ] + } + }, + "test3": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [-15, 10] + }, + "properties": {} + } + ] + } + } + }, + "sprite": "", + "glyphs": "https://orangemug.github.io/font-glyphs/glyphs/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "test1", + "type": "circle", + "source": "test1", + "paint": { + "circle-radius": 40, + "circle-color": "red" + } + }, + { + "id": "test2", + "type": "circle", + "source": "test2", + "paint": { + "circle-radius": 40, + "circle-color": "green" + } + }, + { + "id": "test3", + "type": "circle", + "source": "test3", + "paint": { + "circle-radius": 40, + "circle-color": "blue" + } + } + ] +} + +export const Basic = () => { + return
    + s} + onChange={() => {}} + debugToolbox={true} + /> +
    +}; + + + diff --git a/stories/Modal.stories.js b/stories/Modal.stories.js new file mode 100644 index 0000000..039e512 --- /dev/null +++ b/stories/Modal.stories.js @@ -0,0 +1,26 @@ +import React from 'react'; +import Modal from '../src/components/Modal'; +import {action} from '@storybook/addon-actions'; +import {Wrapper} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + + +export default { + title: 'Modal', + component: Modal, + decorators: [withA11y], +}; + +export const Basic = () => ( + +
    + + {[...Array(50).keys()].map(() => { + return

    Some text

    + })} +
    +
    +
    +); + + diff --git a/stories/ModalAdd.stories.js b/stories/ModalAdd.stories.js new file mode 100644 index 0000000..be28966 --- /dev/null +++ b/stories/ModalAdd.stories.js @@ -0,0 +1,25 @@ +import React from 'react'; +import ModalAdd from '../src/components/ModalAdd'; +import {action} from '@storybook/addon-actions'; +import {Wrapper} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + + +export default { + title: 'ModalAdd', + component: ModalAdd, + decorators: [withA11y], +}; + +export const Basic = () => ( + +
    + +
    +
    +); + diff --git a/stories/ModalDebug.stories.js b/stories/ModalDebug.stories.js new file mode 100644 index 0000000..34202a3 --- /dev/null +++ b/stories/ModalDebug.stories.js @@ -0,0 +1,28 @@ +import React from 'react'; +import ModalDebug from '../src/components/ModalDebug'; +import {action} from '@storybook/addon-actions'; +import {Wrapper} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + + +export default { + title: 'ModalDebug', + component: ModalDebug, + decorators: [withA11y], +}; + +export const Basic = () => ( + +
    + +
    +
    +); + + + diff --git a/stories/ModalExport.stories.js b/stories/ModalExport.stories.js new file mode 100644 index 0000000..8040397 --- /dev/null +++ b/stories/ModalExport.stories.js @@ -0,0 +1,26 @@ +import React from 'react'; +import ModalExport from '../src/components/ModalExport'; +import {action} from '@storybook/addon-actions'; +import {Wrapper} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + + +export default { + title: 'ModalExport', + component: ModalExport, + decorators: [withA11y], +}; + +export const Basic = () => ( + +
    + +
    +
    +); + + + diff --git a/stories/ModalLoading.stories.js b/stories/ModalLoading.stories.js new file mode 100644 index 0000000..bd93459 --- /dev/null +++ b/stories/ModalLoading.stories.js @@ -0,0 +1,25 @@ +import React from 'react'; +import ModalLoading from '../src/components/ModalLoading'; +import {action} from '@storybook/addon-actions'; +import {Wrapper} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + + +export default { + title: 'ModalLoading', + component: ModalLoading, + decorators: [withA11y], +}; + +export const Basic = () => ( + +
    + +
    +
    +); + diff --git a/stories/ModalOpen.stories.js b/stories/ModalOpen.stories.js new file mode 100644 index 0000000..c97bf45 --- /dev/null +++ b/stories/ModalOpen.stories.js @@ -0,0 +1,31 @@ +import React from 'react'; +import ModalOpen from '../src/components/ModalOpen'; +import {action} from '@storybook/addon-actions'; +import {Wrapper} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + + +export default { + title: 'ModalOpen', + component: ModalOpen, + decorators: [withA11y], +}; + +export const Basic = () => ( + +
    + +
    +
    +); + + + + + + diff --git a/stories/ModalSettings.stories.js b/stories/ModalSettings.stories.js new file mode 100644 index 0000000..49f42db --- /dev/null +++ b/stories/ModalSettings.stories.js @@ -0,0 +1,29 @@ +import React from 'react'; +import ModalSettings from '../src/components/ModalSettings'; +import {action} from '@storybook/addon-actions'; +import {Wrapper} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + + +export default { + title: 'ModalSettings', + component: ModalSettings, + decorators: [withA11y], +}; + +export const Basic = () => ( + +
    + +
    +
    +); + + + + diff --git a/stories/ModalShortcuts.stories.js b/stories/ModalShortcuts.stories.js new file mode 100644 index 0000000..e69de29 diff --git a/stories/ModalSources.stories.js b/stories/ModalSources.stories.js new file mode 100644 index 0000000..03bcc65 --- /dev/null +++ b/stories/ModalSources.stories.js @@ -0,0 +1,32 @@ +import React from 'react'; +import ModalSources from '../src/components/ModalSources'; +import {action} from '@storybook/addon-actions'; +import {Wrapper} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + + +export default { + title: 'ModalSources', + component: ModalSources, + decorators: [withA11y], +}; + +export const Basic = () => ( + +
    + +
    +
    +); + + + + + + + diff --git a/stories/Button.stories.js b/stories/ModalSurvey.stories.js similarity index 55% rename from stories/Button.stories.js rename to stories/ModalSurvey.stories.js index c158d7c..b0bacd7 100644 --- a/stories/Button.stories.js +++ b/stories/ModalSurvey.stories.js @@ -1,21 +1,29 @@ import React from 'react'; -import Button from '../src/components/Button'; +import ModalSurvey from '../src/components/ModalSurvey'; import {action} from '@storybook/addon-actions'; import {Wrapper} from './ui'; import {withA11y} from '@storybook/addon-a11y'; export default { - title: 'Button', - component: Button, + title: 'ModalSurvey', + component: ModalSurvey, decorators: [withA11y], }; export const Basic = () => ( - +
    + +
    ); + + + + + + diff --git a/stories/ScrollContainer.stories.js b/stories/ScrollContainer.stories.js new file mode 100644 index 0000000..f684f07 --- /dev/null +++ b/stories/ScrollContainer.stories.js @@ -0,0 +1,26 @@ +import React from 'react'; +import ScrollContainer from '../src/components/ScrollContainer'; +import {action} from '@storybook/addon-actions'; +import {Wrapper} from './ui'; +import {withA11y} from '@storybook/addon-a11y'; + + +export default { + title: 'ScrollContainer', + component: ScrollContainer, + decorators: [withA11y], +}; + +export const Basic = () => ( + +
    + + {[...Array(50).keys()].map(() => { + return

    Some text

    + })} +
    +
    +
    +); + + diff --git a/stories/ui.js b/stories/ui.js index eb6333c..d04db8a 100644 --- a/stories/ui.js +++ b/stories/ui.js @@ -17,3 +17,11 @@ export function Wrapper ({children}) { ); }; +export function InputContainer ({children}) { + return ( +
    + {children} +
    + ); +}; +