diff --git a/package-lock.json b/package-lock.json index aa242d6e..21ee9a7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,6 +69,7 @@ "@storybook/theming": "^7.6.5", "@types/color": "^3.0.6", "@types/cors": "^2.8.17", + "@types/lodash.capitalize": "^4.2.9", "@types/lodash.isequal": "^4.5.8", "@types/lodash.throttle": "^4.1.9", "@types/react": "^16.14.52", @@ -4732,6 +4733,15 @@ "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", "dev": true }, + "node_modules/@types/lodash.capitalize": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/@types/lodash.capitalize/-/lodash.capitalize-4.2.9.tgz", + "integrity": "sha512-SV1dav/WbuI816SVig4trFz8ID/m5maVzC8I/E/DejPvmlXvIhw7Y0GWxJ03UhU6qaZOj6qQnR1xxC0mP7ZXoQ==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/lodash.isequal": { "version": "4.5.8", "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.8.tgz", diff --git a/package.json b/package.json index 9c4be94b..8bf44847 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,7 @@ "@storybook/theming": "^7.6.5", "@types/color": "^3.0.6", "@types/cors": "^2.8.17", + "@types/lodash.capitalize": "^4.2.9", "@types/lodash.isequal": "^4.5.8", "@types/lodash.throttle": "^4.1.9", "@types/react": "^16.14.52", diff --git a/src/components/Fieldset.tsx b/src/components/Fieldset.tsx index 3b314e37..ab095064 100644 --- a/src/components/Fieldset.tsx +++ b/src/components/Fieldset.tsx @@ -1,11 +1,11 @@ -import React from 'react' +import React, { ReactElement } from 'react' import FieldDocLabel from './FieldDocLabel' import Doc from './Doc' type FieldsetProps = { label: string, fieldSpec?: { doc?: string }, - action?: string, + action?: ReactElement, }; type FieldsetState = { diff --git a/src/components/InputArray.tsx b/src/components/InputArray.tsx index a0963ca1..ae4aa2da 100644 --- a/src/components/InputArray.tsx +++ b/src/components/InputArray.tsx @@ -2,7 +2,7 @@ import React from 'react' import InputString from './InputString' import InputNumber from './InputNumber' -type FieldArrayProps = { +export type FieldArrayProps = { value: string[] type?: string length?: number diff --git a/src/components/InputAutocomplete.tsx b/src/components/InputAutocomplete.tsx index e574aca3..fabaa16f 100644 --- a/src/components/InputAutocomplete.tsx +++ b/src/components/InputAutocomplete.tsx @@ -5,7 +5,7 @@ import Autocomplete from 'react-autocomplete' const MAX_HEIGHT = 140; -type InputAutocompleteProps = { +export type InputAutocompleteProps = { value?: string options: any[] onChange(...args: unknown[]): unknown diff --git a/src/components/InputCheckbox.tsx b/src/components/InputCheckbox.tsx index 55c9c758..a799e5a3 100644 --- a/src/components/InputCheckbox.tsx +++ b/src/components/InputCheckbox.tsx @@ -1,6 +1,6 @@ import React from 'react' -type InputCheckboxProps = { +export type InputCheckboxProps = { value?: boolean style?: object onChange(...args: unknown[]): unknown diff --git a/src/components/InputColor.tsx b/src/components/InputColor.tsx index 7c919edd..ad1c6e76 100644 --- a/src/components/InputColor.tsx +++ b/src/components/InputColor.tsx @@ -9,7 +9,7 @@ function formatColor(color: ColorResult): string { return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${rgb.a})` } -type InputColorProps = { +export type InputColorProps = { onChange(...args: unknown[]): unknown name?: string value?: string diff --git a/src/components/InputDynamicArray.jsx b/src/components/InputDynamicArray.tsx similarity index 70% rename from src/components/InputDynamicArray.jsx rename to src/components/InputDynamicArray.tsx index bf353707..aef615f7 100644 --- a/src/components/InputDynamicArray.jsx +++ b/src/components/InputDynamicArray.tsx @@ -1,30 +1,34 @@ import React from 'react' -import PropTypes from 'prop-types' +import capitalize from 'lodash.capitalize' +import {MdDelete} from 'react-icons/md' + 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, +export type FieldDynamicArrayProps = { + value?: (string | number)[] + type?: 'url' | 'number' | 'enum' + default?: (string | number)[] + onChange?(...args: unknown[]): unknown + style?: object + fieldSpec?: { + values?: any } + 'aria-label'?: string + label: string +}; - changeValue(idx, newValue) { + +export default class FieldDynamicArray extends React.Component { + changeValue(idx: number, newValue: string | number) { const values = this.values.slice(0) values[idx] = newValue - this.props.onChange(values) + if (this.props.onChange) this.props.onChange(values) } get values() { @@ -41,20 +45,20 @@ export default class FieldDynamicArray extends React.Component { } else if (this.props.type === 'enum') { const {fieldSpec} = this.props; - const defaultValue = Object.keys(fieldSpec.values)[0]; + const defaultValue = Object.keys(fieldSpec!.values)[0]; values.push(defaultValue); } else { values.push("") } - this.props.onChange(values) + if (this.props.onChange) this.props.onChange(values) } - deleteValue(valueIdx) { + deleteValue(valueIdx: number) { const values = this.values.slice(0) values.splice(valueIdx, 1) - this.props.onChange(values.length > 0 ? values : undefined); + if (this.props.onChange) this.props.onChange(values.length > 0 ? values : undefined); } render() { @@ -63,30 +67,30 @@ export default class FieldDynamicArray extends React.Component { 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 options = Object.keys(this.props.fieldSpec?.values).map(v => [v, capitalize(v)]); input = } else { input = @@ -120,11 +124,11 @@ export default class FieldDynamicArray extends React.Component { } } -class DeleteValueInputButton extends React.Component { - static propTypes = { - onClick: PropTypes.func, - } +type DeleteValueInputButtonProps = { + onClick?(...args: unknown[]): unknown +}; +class DeleteValueInputButton extends React.Component { render() { return } - doc={"Remove array item."} + fieldSpec={{doc:" Remove array item."}} /> } diff --git a/src/components/InputEnum.tsx b/src/components/InputEnum.tsx index 93b1dece..a8f40d3f 100644 --- a/src/components/InputEnum.tsx +++ b/src/components/InputEnum.tsx @@ -17,7 +17,7 @@ export type InputEnumProps = { value?: string style?: object default?: string - name: string + name?: string onChange(...args: unknown[]): unknown options: any[] 'aria-label'?: string diff --git a/src/components/InputFont.tsx b/src/components/InputFont.tsx index b015a8ed..7bdaf9fa 100644 --- a/src/components/InputFont.tsx +++ b/src/components/InputFont.tsx @@ -1,7 +1,7 @@ import React from 'react' import InputAutocomplete from './InputAutocomplete' -type FieldFontProps = { +export type FieldFontProps = { name: string value?: string[] default?: string[] diff --git a/src/components/InputMultiInput.tsx b/src/components/InputMultiInput.tsx index 632b8c3b..f58bbcc6 100644 --- a/src/components/InputMultiInput.tsx +++ b/src/components/InputMultiInput.tsx @@ -2,7 +2,7 @@ import React from 'react' import classnames from 'classnames' type InputMultiInputProps = { - name: string + name?: string value: string options: any[] onChange(...args: unknown[]): unknown diff --git a/src/components/InputNumber.tsx b/src/components/InputNumber.tsx index c7b044df..eb7d6e18 100644 --- a/src/components/InputNumber.tsx +++ b/src/components/InputNumber.tsx @@ -2,7 +2,7 @@ import React, { BaseSyntheticEvent } from 'react' let IDX = 0; -type InputNumberProps = { +export type InputNumberProps = { value?: number default?: number min?: number diff --git a/src/components/InputSpec.jsx b/src/components/InputSpec.jsx deleted file mode 100644 index 021a9787..00000000 --- a/src/components/InputSpec.jsx +++ /dev/null @@ -1,139 +0,0 @@ -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 Maplibre 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/InputSpec.tsx b/src/components/InputSpec.tsx new file mode 100644 index 00000000..f79bb4da --- /dev/null +++ b/src/components/InputSpec.tsx @@ -0,0 +1,126 @@ +import React, { ReactElement } from 'react' + +import InputColor, { InputColorProps } from './InputColor' +import InputNumber, { InputNumberProps } from './InputNumber' +import InputCheckbox, { InputCheckboxProps } from './InputCheckbox' +import InputString, { InputStringProps } from './InputString' +import InputArray, { FieldArrayProps } from './InputArray' +import InputDynamicArray, { FieldDynamicArrayProps } from './InputDynamicArray' +import InputFont, { FieldFontProps } from './InputFont' +import InputAutocomplete, { InputAutocompleteProps } from './InputAutocomplete' +import InputEnum, { InputEnumProps } from './InputEnum' +import capitalize from 'lodash.capitalize' + +const iconProperties = ['background-pattern', 'fill-pattern', 'line-pattern', 'fill-extrusion-pattern', 'icon-image'] + +export type SpecFieldProps = { + onChange(...args: unknown[]): unknown + fieldName: string + fieldSpec: { + default?: unknown + type: 'number' | 'enum' | 'resolvedImage' | 'formatted' | 'string' | 'color' | 'boolean' | 'array' + minimum?: number + maximum?: number + values?: unknown[] + length?: number + value?: string + } + value?: string | number | unknown[] | boolean + /** Override the style of the field */ + style?: object + 'aria-label'?: string + error: unknown[] + label: string + action: ReactElement +}; + +/** Display any field from the Maplibre GL style spec and + * choose the correct field component based on the @{fieldSpec} + * to display @{value}. */ +export default class SpecField extends React.Component { + + childNodes() { + 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: string) => this.props.onChange(this.props.fieldName, newValue), + 'aria-label': this.props['aria-label'], + } + switch(this.props.fieldSpec.type) { + case 'number': return ( + + ) + case 'enum': + const options = Object.keys(this.props.fieldSpec.values || []).map(v => [v, capitalize(v)]) + + return } + options={options} + /> + case 'resolvedImage': + case 'formatted': + case 'string': + if (iconProperties.indexOf(this.props.fieldName) >= 0) { + const options = this.props.fieldSpec.values || []; + return } + options={options.map(f => [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 + } + } + + render() { + return ( +
+ {this.childNodes()} +
+ ); + } +} diff --git a/src/components/InputString.tsx b/src/components/InputString.tsx index b00ecc26..ee1d0bc6 100644 --- a/src/components/InputString.tsx +++ b/src/components/InputString.tsx @@ -6,7 +6,7 @@ export type InputStringProps = { style?: object default?: string onChange?(...args: unknown[]): unknown - onInput(...args: unknown[]): unknown + onInput?(...args: unknown[]): unknown multi?: boolean required?: boolean disabled?: boolean @@ -77,7 +77,7 @@ export default class InputString extends React.Component { - this.props.onInput(this.state.value); + if (this.props.onInput) this.props.onInput(this.state.value); }); }, onBlur: () => { diff --git a/src/components/SpecField.jsx b/src/components/SpecField.tsx similarity index 71% rename from src/components/SpecField.jsx rename to src/components/SpecField.tsx index cb55031e..c399cb83 100644 --- a/src/components/SpecField.jsx +++ b/src/components/SpecField.tsx @@ -1,13 +1,12 @@ import React from 'react' -import PropTypes from 'prop-types' import Block from './Block' -import InputSpec from './InputSpec' +import InputSpec, { SpecFieldProps as InputFieldSpecProps } from './InputSpec' import Fieldset from './Fieldset' const typeMap = { color: () => Block, - enum: ({fieldSpec}) => (Object.keys(fieldSpec.values).length <= 3 ? Fieldset : Block), + enum: ({fieldSpec}: any) => (Object.keys(fieldSpec.values).length <= 3 ? Fieldset : Block), boolean: () => Block, array: () => Fieldset, resolvedImage: () => Block, @@ -16,12 +15,11 @@ const typeMap = { formatted: () => Block, }; -export default class SpecField extends React.Component { - static propTypes = { - ...InputSpec.propTypes, - name: PropTypes.string, - } +type SpecFieldProps = InputFieldSpecProps & { + name?: string +}; +export default class SpecField extends React.Component { render() { const {props} = this;