import { toColor } from '@o/color' import { gloss } from 'gloss' import React, { ChangeEvent, Component, Fragment, PureComponent } from 'react' import { SketchPicker } from 'react-color' import { Input, InputProps } from '../forms/Input' import { colors } from '../helpers/colors' import { Link } from '../Link' import { SimpleText } from '../text/SimpleText' import { View } from '../View/View' import { DataInspectorSetValue } from './DataInspectorControlled' /** * Copyright 2018-present Facebook. * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * @format */ const NullValue = gloss('span', { color: 'rgb(128, 128, 128)', }) const UndefinedValue = gloss('span', { color: 'rgb(128, 128, 128)', }) const StringValue = gloss('span', { color: 'purple', }) const ColorValue = gloss('span', { color: 'rgb(50, 50, 200)', }) const SymbolValue = gloss('span', { color: 'rgb(196, 26, 22)', }) const NumberValue = gloss('span', { color: 'rgb(50, 150, 50)', }) const ColorBox = gloss(View, { boxShadow: 'inset 0 0 1px rgba(0, 0, 0, 1)', display: 'inline-block', height: 12, width: 12, marginRight: 5, verticalAlign: 'middle', }) const FunctionKeyword = gloss('span', { color: 'rgb(170, 13, 145)', fontStyle: 'italic', }) const FunctionName = gloss('span', { fontStyle: 'italic', }) const ColorPickerDescription = gloss({ display: 'inline', position: 'relative', }) type DataDescriptionProps = { path?: string[] type: string value: any setValue?: DataInspectorSetValue } type DescriptionCommitOptions = { value: any keep: boolean clear: boolean } class NumberTextEditor extends PureComponent<{ commit: (opts: DescriptionCommitOptions) => void type: string value: any origValue: any }> { onNumberTextInputChange = (e: ChangeEvent) => { const val = this.props.type === 'number' ? parseFloat(e.target.value) : e.target.value this.props.commit({ clear: false, keep: true, value: val, }) } onNumberTextInputKeyDown = (e: any) => { if (e.key === 'Enter') { const val = this.props.type === 'number' ? parseFloat(this.props.value) : this.props.value this.props.commit({ clear: true, keep: true, value: val }) } else if (e.key === 'Escape') { this.props.commit({ clear: true, keep: false, value: this.props.origValue, }) } } onNumberTextRef = (ref?: HTMLElement) => { if (ref) { ref.focus() } } onNumberTextBlur = () => { this.props.commit({ clear: true, keep: true, value: this.props.value }) } render() { const extraProps: Partial = {} if (this.props.type === 'number') { // render as a HTML number input extraProps.type = 'number' // step="any" allows any sort of float to be input, otherwise we're limited // to decimal extraProps.step = 'any' } return ( ) } } type DataDescriptionState = { editing: boolean origValue: any value: any } export class DataDescription extends PureComponent { constructor(props: DataDescriptionProps, context: Object) { super(props, context) this.state = { editing: false, origValue: '', value: '', } } commit = (opts: DescriptionCommitOptions) => { const { path, setValue } = this.props if (opts.keep && setValue && path) { const val = opts.value this.setState({ value: val }) setValue(path, val) } if (opts.clear) { this.setState({ editing: false, origValue: '', value: '', }) } } _renderEditing() { const { type } = this.props const { origValue, value } = this.state if (type === 'string' || type === 'text' || type === 'number' || type === 'enum') { return ( ) } if (type === 'color') { return } return null } _hasEditUI() { const { type } = this.props return ( type === 'string' || type === 'text' || type === 'number' || type === 'enum' || type === 'color' ) } onEditStart = () => { this.setState({ editing: this._hasEditUI(), origValue: this.props.value, value: this.props.value, }) } render(): any { if (this.state.editing) { return this._renderEditing() } else { return ( ) } } } class ColorEditor extends Component<{ value: any commit: (opts: DescriptionCommitOptions) => void }> { onBlur = () => { this.props.commit({ clear: true, keep: false, value: this.props.value }) } onChange = ({ hex, rgb: { a, b, g, r }, }: { hex: string rgb: { a: number; b: number; g: number; r: number } }) => { const prev = this.props.value let val if (typeof prev === 'string') { if (a === 1) { // hex is fine and has an implicit 100% alpha val = hex } else { // turn into a css rgba value val = `rgba(${r}, ${g}, ${b}, ${a})` } } else if (typeof prev === 'number') { // compute RRGGBBAA value val = (Math.round(a * 255) & 0xff) << 24 val |= (r & 0xff) << 16 val |= (g & 0xff) << 8 val |= b & 0xff const prevClear = ((prev >> 24) & 0xff) === 0 const onlyAlphaChanged = (prev & 0x00ffffff) === (val & 0x00ffffff) if (!onlyAlphaChanged && prevClear) { val = 0xff000000 | (val & 0x00ffffff) } } else { return } this.props.commit({ clear: false, keep: true, value: val }) } render() { let colorInfo try { colorInfo = toColor(this.props.value).toRgb() } catch (err) { console.warn('cant parse color', this.props.value, err) return } return ( {/* } onClose={this.onBlur}> */} ) } } class DataDescriptionPreview extends Component<{ type: string value: any editable: boolean commit: (opts: DescriptionCommitOptions) => void onEdit?: () => void }> { onClick = () => { const { onEdit } = this.props if (this.props.editable && onEdit) { onEdit() } } render() { const { type, value } = this.props const description = ( ) // booleans are always editable so don't require the onEditStart handler if (type === 'boolean') { return description } return ( {description} ) } } class DataDescriptionContainer extends Component<{ type: string value: any editable: boolean commit: (opts: DescriptionCommitOptions) => void }> { onChangeCheckbox = (e: ChangeEvent) => { this.props.commit({ clear: true, keep: true, value: e.target.checked }) } render(): any { const { type, editable, value: val } = this.props switch (type) { case 'number': return {Number(val)} case 'color': { const colorInfo = toColor(val).toRgb() if (colorInfo) { const { a, b, g, r } = colorInfo return [ , rgba({r.toFixed(2)}, {g.toFixed(2)}, {b.toFixed(2)}, {a === 1 ? '1' : a.toFixed(2)}) , ] } else { return Malformed color } } case 'text': case 'string': if (val.startsWith('http://') || val.startsWith('https://')) { return {val} } else { return "{String(val || '')}" } case 'enum': return {String(val)} case 'boolean': return editable ? ( ) : ( {String(val)} ) case 'undefined': return undefined case 'date': if (Object.prototype.toString.call(val) === '[object Date]') { return {val.toString()} } else { return {val} } case 'null': return null case 'array': case 'object': // no description necessary as we'll typically wrap it in [] or {} which already denotes the // type return null case 'function': return ( function  {val.name}() ) case 'symbol': return Symbol() default: return Unknown type "{type}" } } }