Files
flipper/src/ui/components/data-inspector/DataDescription.tsx
Daniel Büchele 0ec48d4fc9 index
Summary:
This converts the index file to TypeScript which is used as an export for all plugins. Alongside, I had to fix a couple of errors in various files to make sure everything works correctly.

For plugins using flow, we define an interface which uses the type definition for FlipperPlugin and types all UI components as `any`.

Reviewed By: passy

Differential Revision: D16936890

fbshipit-source-id: dacd9a63a82b9f0bbb530b06072186874cba7b6f
2019-08-22 10:02:27 -07:00

547 lines
12 KiB
TypeScript

/**
* 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<string>;
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<HTMLInputElement>) => {
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 (
<Input
key="input"
{...extraProps}
compact={true}
onChange={this.onNumberTextInputChange}
onKeyDown={this.onNumberTextInputKeyDown}
innerRef={this.onNumberTextRef}
onBlur={this.onNumberTextBlur}
value={this.props.value}
/>
);
}
}
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 (
<NumberTextEditor
type={type}
value={value}
origValue={origValue}
commit={this.commit}
/>
);
}
if (type === 'color') {
return <ColorEditor value={value} commit={this.commit} />;
}
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 (
<DataDescriptionPreview
type={this.props.type}
value={this.props.value}
editable={Boolean(this.props.setValue)}
commit={this.commit}
onEdit={this.onEditStart}
/>
);
}
}
}
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 <Fragment />;
}
return (
<ColorPickerDescription>
<DataDescriptionPreview
type="color"
value={this.props.value}
editable={false}
commit={this.props.commit}
/>
<Popover onDismiss={this.onBlur}>
<SketchPicker
color={colorInfo}
presetColors={[
colors.blue,
colors.green,
colors.red,
colors.blueGrey,
colors.slate,
colors.aluminum,
colors.seaFoam,
colors.teal,
colors.lime,
colors.lemon,
colors.orange,
colors.tomato,
colors.cherry,
colors.pink,
colors.grape,
]}
onChange={this.onChange}
/>
</Popover>
</ColorPickerDescription>
);
}
}
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 = (
<DataDescriptionContainer
type={type}
value={value}
editable={this.props.editable}
commit={this.props.commit}
/>
);
// booleans are always editable so don't require the onEditStart handler
if (type === 'boolean') {
return description;
}
return (
<span onClick={this.onClick} role="button" tabIndex={-1}>
{description}
</span>
);
}
}
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<HTMLInputElement>) => {
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 <NumberValue>{Number(val)}</NumberValue>;
case 'color': {
const colorInfo = parseColor(val);
if (typeof val === 'number' && val === 0) {
return <UndefinedValue>(not set)</UndefinedValue>;
} else if (colorInfo) {
const {a, b, g, r} = colorInfo;
return [
<ColorBox key="color-box" color={`rgba(${r}, ${g}, ${b}, ${a})`} />,
<ColorValue key="value">
rgba({r}, {g}, {b}, {a === 1 ? '1' : a.toFixed(2)})
</ColorValue>,
];
} else {
return <span>Malformed color</span>;
}
}
case 'text':
case 'string':
if (val.startsWith('http://') || val.startsWith('https://')) {
return <Link href={val}>{val}</Link>;
} else {
return <StringValue>"{String(val || '')}"</StringValue>;
}
case 'enum':
return <StringValue>{String(val)}</StringValue>;
case 'boolean':
return editable ? (
<input
type="checkbox"
checked={Boolean(val)}
disabled={!editable}
onChange={this.onChangeCheckbox}
/>
) : (
<StringValue>{String(val)}</StringValue>
);
case 'undefined':
return <UndefinedValue>undefined</UndefinedValue>;
case 'date':
if (Object.prototype.toString.call(val) === '[object Date]') {
return <span>{val.toString()}</span>;
} else {
return <span>{val}</span>;
}
case 'null':
return <NullValue>null</NullValue>;
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 (
<span>
<FunctionKeyword>function</FunctionKeyword>
<FunctionName>&nbsp;{val.name}()</FunctionName>
</span>
);
case 'symbol':
return <SymbolValue>Symbol()</SymbolValue>;
default:
return <span>Unknown type "{type}"</span>;
}
}
}