/*** Copyright (c) Meta Platforms, Inc. and affiliates.** This source code is licensed under the MIT license found in the* LICENSE file in the root directory of this source tree.** @flow*/import * as React from 'react';
import {useContext, useMemo, useRef, useState} from 'react';
import {unstable_batchedUpdates as batchedUpdates} from 'react-dom';
import {copy} from 'clipboard-js';
import {
BridgeContext,
StoreContext,
} from 'react-devtools-shared/src/devtools/views/context';
import Button from '../../Button';
import ButtonIcon from '../../ButtonIcon';
import {serializeDataForCopy} from '../../utils';
import AutoSizeInput from './AutoSizeInput';
import styles from './StyleEditor.css';
import {sanitizeForParse} from '../../../utils';
import type {Style} from './types';
type Props = {
id: number,
style: Style,
};type ChangeAttributeFn = (oldName: string, newName: string, value: any) => void;
type ChangeValueFn = (name: string, value: any) => void;
export default function StyleEditor({id, style}: Props): React.Node {
const bridge = useContext(BridgeContext);
const store = useContext(StoreContext);
const changeAttribute = (oldName: string, newName: string, value: any) => {
const rendererID = store.getRendererIDForElement(id);
if (rendererID !== null) {
bridge.send('NativeStyleEditor_renameAttribute', {
id,
rendererID,
oldName,
newName,
value,
});}};const changeValue = (name: string, value: any) => {
const rendererID = store.getRendererIDForElement(id);
if (rendererID !== null) {
bridge.send('NativeStyleEditor_setValue', {
id,
rendererID,
name,
value,
});}};const keys = useMemo(() => Array.from(Object.keys(style)), [style]);
const handleCopy = () => copy(serializeDataForCopy(style));
return (
<div className={styles.StyleEditor}>
<div className={styles.HeaderRow}>
<div className={styles.Header}>
<div className={styles.Brackets}>{'style {'}</div>
</div>
<Button onClick={handleCopy} title="Copy to clipboard">
<ButtonIcon type="copy" />
</Button>
</div>
{keys.length > 0 &&
keys.map(attribute => (
<Row
key={attribute}
attribute={attribute}
changeAttribute={changeAttribute}
changeValue={changeValue}
validAttributes={store.nativeStyleEditorValidAttributes}
value={style[attribute]}
/>))}
<NewRow
changeAttribute={changeAttribute}
changeValue={changeValue}
validAttributes={store.nativeStyleEditorValidAttributes}
/><div className={styles.Brackets}>{'}'}</div>
</div>
);
}type NewRowProps = {
changeAttribute: ChangeAttributeFn,
changeValue: ChangeValueFn,
validAttributes: $ReadOnlyArray<string> | null,
};function NewRow({changeAttribute, changeValue, validAttributes}: NewRowProps) {
const [key, setKey] = useState<number>(0);
const reset = () => setKey(key + 1);
const newAttributeRef = useRef<string>('');
const changeAttributeWrapper = (
oldAttribute: string,
newAttribute: string,
value: any,
) => {
// Ignore attribute changes until a value has been specified
newAttributeRef.current = newAttribute;};const changeValueWrapper = (attribute: string, value: any) => {
// Blur events should reset/cancel if there's no value or no attribute
if (newAttributeRef.current !== '') {
if (value !== '') {
changeValue(newAttributeRef.current, value);
}reset();
}};return (
<Row
key={key}
attribute={''}
attributePlaceholder="attribute"
changeAttribute={changeAttributeWrapper}
changeValue={changeValueWrapper}
validAttributes={validAttributes}
value={''}
valuePlaceholder="value"
/>);
}type RowProps = {
attribute: string,
attributePlaceholder?: string,
changeAttribute: ChangeAttributeFn,
changeValue: ChangeValueFn,
validAttributes: $ReadOnlyArray<string> | null,
value: any,
valuePlaceholder?: string,
};function Row({
attribute,attributePlaceholder,changeAttribute,changeValue,validAttributes,value,valuePlaceholder,}: RowProps) {// TODO (RN style editor) Use @reach/combobox to auto-complete attributes.
// The list of valid attributes would need to be injected by RN backend,
// which would need to require them from ReactNativeViewViewConfig "validAttributes.style" keys.
// This would need to degrade gracefully for react-native-web,
// although we could let it also inject a custom set of allowed attributes.
const [localAttribute, setLocalAttribute] = useState(attribute);
const [localValue, setLocalValue] = useState(JSON.stringify(value));
const [isAttributeValid, setIsAttributeValid] = useState(true);
const [isValueValid, setIsValueValid] = useState(true);
// $FlowFixMe[missing-local-annot]
const validateAndSetLocalAttribute = newAttribute => {
const isValid =
newAttribute === '' ||
validAttributes === null ||
validAttributes.indexOf(newAttribute) >= 0;
batchedUpdates(() => {
setLocalAttribute(newAttribute);
setIsAttributeValid(isValid);
});};// $FlowFixMe[missing-local-annot]
const validateAndSetLocalValue = newValue => {
let isValid = false;
try {
JSON.parse(sanitizeForParse(newValue));
isValid = true;
} catch (error) {}
batchedUpdates(() => {
setLocalValue(newValue);
setIsValueValid(isValid);
});};const resetAttribute = () => {
setLocalAttribute(attribute);
};const resetValue = () => {
setLocalValue(value);
};const submitValueChange = () => {
if (isAttributeValid && isValueValid) {
const parsedLocalValue = JSON.parse(sanitizeForParse(localValue));
if (value !== parsedLocalValue) {
changeValue(attribute, parsedLocalValue);
}}};const submitAttributeChange = () => {
if (isAttributeValid && isValueValid) {
if (attribute !== localAttribute) {
changeAttribute(attribute, localAttribute, value);
}}};return (
<div className={styles.Row}>
<Field
className={isAttributeValid ? styles.Attribute : styles.Invalid}
onChange={validateAndSetLocalAttribute}
onReset={resetAttribute}
onSubmit={submitAttributeChange}
placeholder={attributePlaceholder}
value={localAttribute}
/>:
<Field
className={isValueValid ? styles.Value : styles.Invalid}
onChange={validateAndSetLocalValue}
onReset={resetValue}
onSubmit={submitValueChange}
placeholder={valuePlaceholder}
value={localValue}
/>;</div>
);}type FieldProps = {className: string,onChange: (value: any) => void,onReset: () => void,onSubmit: () => void,placeholder?: string,
value: any,};function Field({className,onChange,onReset,onSubmit,placeholder,value,}: FieldProps) {// $FlowFixMe[missing-local-annot]
const onKeyDown = event => {
switch (event.key) {
case 'Enter':
onSubmit();
break;
case 'Escape':
onReset();
break;
case 'ArrowDown':
case 'ArrowLeft':
case 'ArrowRight':
case 'ArrowUp':
event.stopPropagation();
break;
default:
break;
}};return (
<AutoSizeInput
className={`${className} ${styles.Input}`}
onBlur={onSubmit}onChange={(event: $FlowFixMe) => onChange(event.target.value)}onKeyDown={onKeyDown}placeholder={placeholder}value={value}/>);}