/** * 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 */ import Link from '../Link'; import {DataInspectorSetValue} from './DataInspector'; import {PureComponent} from 'react'; import styled from 'react-emotion'; import {SketchPicker} from 'react-color'; import {Component, Fragment} from 'react'; import Popover from '../Popover'; import {colors} from '../colors'; import Input from '../Input'; import React, {KeyboardEvent} from 'react'; const NullValue = styled('span')({ color: 'rgb(128, 128, 128)', }); const UndefinedValue = styled('span')({ color: 'rgb(128, 128, 128)', }); const StringValue = styled('span')({ color: colors.cherryDark1, }); const ColorValue = styled('span')({ color: colors.blueGrey, }); const SymbolValue = styled('span')({ color: 'rgb(196, 26, 22)', }); const NumberValue = styled('span')({ color: colors.tealDark1, }); const ColorBox = styled('span')((props: {color: string}) => ({ backgroundColor: props.color, boxShadow: 'inset 0 0 1px rgba(0, 0, 0, 1)', display: 'inline-block', height: 12, marginRight: 5, verticalAlign: 'middle', width: 12, })); const FunctionKeyword = styled('span')({ color: 'rgb(170, 13, 145)', fontStyle: 'italic', }); const FunctionName = styled('span')({ fontStyle: 'italic', }); const ColorPickerDescription = styled('div')({ display: 'inline', position: 'relative', }); type DataDescriptionProps = { path?: Array; type: string; value: any; setValue: DataInspectorSetValue | null | undefined; }; type DescriptionCommitOptions = { value: any; keep: boolean; clear: boolean; set: boolean; }; class NumberTextEditor extends PureComponent<{ commit: (opts: DescriptionCommitOptions) => void; type: string; value: any; origValue: any; }> { onNumberTextInputChange = (e: React.ChangeEvent) => { const val = this.props.type === 'number' ? parseFloat(e.target.value) : e.target.value; this.props.commit({ clear: false, keep: true, value: val, set: false, }); }; onNumberTextInputKeyDown = (e: KeyboardEvent) => { 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, set: true}); } else if (e.key === 'Escape') { this.props.commit({ clear: true, keep: false, value: this.props.origValue, set: false, }); } }; onNumberTextRef = (ref: HTMLElement | undefined | null) => { if (ref) { ref.focus(); } }; onNumberTextBlur = () => { this.props.commit({ clear: true, keep: true, value: this.props.value, set: true, }); }; render() { const extraProps: any = {}; 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 default class DataDescription extends PureComponent< DataDescriptionProps, DataDescriptionState > { 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}); if (opts.set) { 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, set: true, }); }; 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, set: true}); }; render() { const colorInfo = parseColor(this.props.value); if (!colorInfo) { return ; } return ( ); } } 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} ); } } function parseColor( val: string | number, ): | { r: number; g: number; b: number; a: number; } | undefined | null { if (typeof val === 'number') { const a = ((val >> 24) & 0xff) / 255; const r = (val >> 16) & 0xff; const g = (val >> 8) & 0xff; const b = val & 0xff; return {a, b, g, r}; } if (typeof val !== 'string') { return; } if (val[0] !== '#') { return; } // remove leading hash val = val.slice(1); // only allow RGB and ARGB hex values if (val.length !== 3 && val.length !== 6 && val.length !== 8) { return; } // split every 2 characters const parts = val.match(/.{1,2}/g); if (!parts) { return; } // get the alpha value let a = 1; // extract alpha if passed AARRGGBB if (val.length === 8) { a = parseInt(parts.shift(), 16) / 255; } const size = val.length; const [r, g, b] = parts.map(num => { if (size === 3) { return parseInt(num + num, 16); } else { return parseInt(num, 16); } }); return {a, b, g, r}; } class DataDescriptionContainer extends Component<{ type: string; value: any; editable: boolean; commit: (opts: DescriptionCommitOptions) => void; }> { onChangeCheckbox = (e: React.ChangeEvent) => { this.props.commit({ clear: true, keep: true, value: e.target.checked, set: true, }); }; render(): any { const {type, editable, value: val} = this.props; switch (type) { case 'number': return {Number(val)}; case 'color': { const colorInfo = parseColor(val); if (typeof val === 'number' && val === 0) { return (not set); } else if (colorInfo) { const {a, b, g, r} = colorInfo; return [ , rgba({r}, {g}, {b}, {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}"; } } }