1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. 
    
  10. import * as React from 'react';
    
  11. import {useContext, useMemo, useRef, useState} from 'react';
    
  12. import {unstable_batchedUpdates as batchedUpdates} from 'react-dom';
    
  13. import {copy} from 'clipboard-js';
    
  14. import {
    
  15.   BridgeContext,
    
  16.   StoreContext,
    
  17. } from 'react-devtools-shared/src/devtools/views/context';
    
  18. import Button from '../../Button';
    
  19. import ButtonIcon from '../../ButtonIcon';
    
  20. import {serializeDataForCopy} from '../../utils';
    
  21. import AutoSizeInput from './AutoSizeInput';
    
  22. import styles from './StyleEditor.css';
    
  23. import {sanitizeForParse} from '../../../utils';
    
  24. 
    
  25. import type {Style} from './types';
    
  26. 
    
  27. type Props = {
    
  28.   id: number,
    
  29.   style: Style,
    
  30. };
    
  31. 
    
  32. type ChangeAttributeFn = (oldName: string, newName: string, value: any) => void;
    
  33. type ChangeValueFn = (name: string, value: any) => void;
    
  34. 
    
  35. export default function StyleEditor({id, style}: Props): React.Node {
    
  36.   const bridge = useContext(BridgeContext);
    
  37.   const store = useContext(StoreContext);
    
  38. 
    
  39.   const changeAttribute = (oldName: string, newName: string, value: any) => {
    
  40.     const rendererID = store.getRendererIDForElement(id);
    
  41.     if (rendererID !== null) {
    
  42.       bridge.send('NativeStyleEditor_renameAttribute', {
    
  43.         id,
    
  44.         rendererID,
    
  45.         oldName,
    
  46.         newName,
    
  47.         value,
    
  48.       });
    
  49.     }
    
  50.   };
    
  51. 
    
  52.   const changeValue = (name: string, value: any) => {
    
  53.     const rendererID = store.getRendererIDForElement(id);
    
  54.     if (rendererID !== null) {
    
  55.       bridge.send('NativeStyleEditor_setValue', {
    
  56.         id,
    
  57.         rendererID,
    
  58.         name,
    
  59.         value,
    
  60.       });
    
  61.     }
    
  62.   };
    
  63. 
    
  64.   const keys = useMemo(() => Array.from(Object.keys(style)), [style]);
    
  65. 
    
  66.   const handleCopy = () => copy(serializeDataForCopy(style));
    
  67. 
    
  68.   return (
    
  69.     <div className={styles.StyleEditor}>
    
  70.       <div className={styles.HeaderRow}>
    
  71.         <div className={styles.Header}>
    
  72.           <div className={styles.Brackets}>{'style {'}</div>
    
  73.         </div>
    
  74.         <Button onClick={handleCopy} title="Copy to clipboard">
    
  75.           <ButtonIcon type="copy" />
    
  76.         </Button>
    
  77.       </div>
    
  78.       {keys.length > 0 &&
    
  79.         keys.map(attribute => (
    
  80.           <Row
    
  81.             key={attribute}
    
  82.             attribute={attribute}
    
  83.             changeAttribute={changeAttribute}
    
  84.             changeValue={changeValue}
    
  85.             validAttributes={store.nativeStyleEditorValidAttributes}
    
  86.             value={style[attribute]}
    
  87.           />
    
  88.         ))}
    
  89.       <NewRow
    
  90.         changeAttribute={changeAttribute}
    
  91.         changeValue={changeValue}
    
  92.         validAttributes={store.nativeStyleEditorValidAttributes}
    
  93.       />
    
  94.       <div className={styles.Brackets}>{'}'}</div>
    
  95.     </div>
    
  96.   );
    
  97. }
    
  98. 
    
  99. type NewRowProps = {
    
  100.   changeAttribute: ChangeAttributeFn,
    
  101.   changeValue: ChangeValueFn,
    
  102.   validAttributes: $ReadOnlyArray<string> | null,
    
  103. };
    
  104. 
    
  105. function NewRow({changeAttribute, changeValue, validAttributes}: NewRowProps) {
    
  106.   const [key, setKey] = useState<number>(0);
    
  107.   const reset = () => setKey(key + 1);
    
  108. 
    
  109.   const newAttributeRef = useRef<string>('');
    
  110. 
    
  111.   const changeAttributeWrapper = (
    
  112.     oldAttribute: string,
    
  113.     newAttribute: string,
    
  114.     value: any,
    
  115.   ) => {
    
  116.     // Ignore attribute changes until a value has been specified
    
  117.     newAttributeRef.current = newAttribute;
    
  118.   };
    
  119. 
    
  120.   const changeValueWrapper = (attribute: string, value: any) => {
    
  121.     // Blur events should reset/cancel if there's no value or no attribute
    
  122.     if (newAttributeRef.current !== '') {
    
  123.       if (value !== '') {
    
  124.         changeValue(newAttributeRef.current, value);
    
  125.       }
    
  126.       reset();
    
  127.     }
    
  128.   };
    
  129. 
    
  130.   return (
    
  131.     <Row
    
  132.       key={key}
    
  133.       attribute={''}
    
  134.       attributePlaceholder="attribute"
    
  135.       changeAttribute={changeAttributeWrapper}
    
  136.       changeValue={changeValueWrapper}
    
  137.       validAttributes={validAttributes}
    
  138.       value={''}
    
  139.       valuePlaceholder="value"
    
  140.     />
    
  141.   );
    
  142. }
    
  143. 
    
  144. type RowProps = {
    
  145.   attribute: string,
    
  146.   attributePlaceholder?: string,
    
  147.   changeAttribute: ChangeAttributeFn,
    
  148.   changeValue: ChangeValueFn,
    
  149.   validAttributes: $ReadOnlyArray<string> | null,
    
  150.   value: any,
    
  151.   valuePlaceholder?: string,
    
  152. };
    
  153. 
    
  154. function Row({
    
  155.   attribute,
    
  156.   attributePlaceholder,
    
  157.   changeAttribute,
    
  158.   changeValue,
    
  159.   validAttributes,
    
  160.   value,
    
  161.   valuePlaceholder,
    
  162. }: RowProps) {
    
  163.   // TODO (RN style editor) Use @reach/combobox to auto-complete attributes.
    
  164.   // The list of valid attributes would need to be injected by RN backend,
    
  165.   // which would need to require them from ReactNativeViewViewConfig "validAttributes.style" keys.
    
  166.   // This would need to degrade gracefully for react-native-web,
    
  167.   // although we could let it also inject a custom set of allowed attributes.
    
  168. 
    
  169.   const [localAttribute, setLocalAttribute] = useState(attribute);
    
  170.   const [localValue, setLocalValue] = useState(JSON.stringify(value));
    
  171.   const [isAttributeValid, setIsAttributeValid] = useState(true);
    
  172.   const [isValueValid, setIsValueValid] = useState(true);
    
  173. 
    
  174.   // $FlowFixMe[missing-local-annot]
    
  175.   const validateAndSetLocalAttribute = newAttribute => {
    
  176.     const isValid =
    
  177.       newAttribute === '' ||
    
  178.       validAttributes === null ||
    
  179.       validAttributes.indexOf(newAttribute) >= 0;
    
  180. 
    
  181.     batchedUpdates(() => {
    
  182.       setLocalAttribute(newAttribute);
    
  183.       setIsAttributeValid(isValid);
    
  184.     });
    
  185.   };
    
  186. 
    
  187.   // $FlowFixMe[missing-local-annot]
    
  188.   const validateAndSetLocalValue = newValue => {
    
  189.     let isValid = false;
    
  190.     try {
    
  191.       JSON.parse(sanitizeForParse(newValue));
    
  192.       isValid = true;
    
  193.     } catch (error) {}
    
  194. 
    
  195.     batchedUpdates(() => {
    
  196.       setLocalValue(newValue);
    
  197.       setIsValueValid(isValid);
    
  198.     });
    
  199.   };
    
  200. 
    
  201.   const resetAttribute = () => {
    
  202.     setLocalAttribute(attribute);
    
  203.   };
    
  204. 
    
  205.   const resetValue = () => {
    
  206.     setLocalValue(value);
    
  207.   };
    
  208. 
    
  209.   const submitValueChange = () => {
    
  210.     if (isAttributeValid && isValueValid) {
    
  211.       const parsedLocalValue = JSON.parse(sanitizeForParse(localValue));
    
  212.       if (value !== parsedLocalValue) {
    
  213.         changeValue(attribute, parsedLocalValue);
    
  214.       }
    
  215.     }
    
  216.   };
    
  217. 
    
  218.   const submitAttributeChange = () => {
    
  219.     if (isAttributeValid && isValueValid) {
    
  220.       if (attribute !== localAttribute) {
    
  221.         changeAttribute(attribute, localAttribute, value);
    
  222.       }
    
  223.     }
    
  224.   };
    
  225. 
    
  226.   return (
    
  227.     <div className={styles.Row}>
    
  228.       <Field
    
  229.         className={isAttributeValid ? styles.Attribute : styles.Invalid}
    
  230.         onChange={validateAndSetLocalAttribute}
    
  231.         onReset={resetAttribute}
    
  232.         onSubmit={submitAttributeChange}
    
  233.         placeholder={attributePlaceholder}
    
  234.         value={localAttribute}
    
  235.       />
    
  236.       :&nbsp;
    
  237.       <Field
    
  238.         className={isValueValid ? styles.Value : styles.Invalid}
    
  239.         onChange={validateAndSetLocalValue}
    
  240.         onReset={resetValue}
    
  241.         onSubmit={submitValueChange}
    
  242.         placeholder={valuePlaceholder}
    
  243.         value={localValue}
    
  244.       />
    
  245.       ;
    
  246.     </div>
    
  247.   );
    
  248. }
    
  249. 
    
  250. type FieldProps = {
    
  251.   className: string,
    
  252.   onChange: (value: any) => void,
    
  253.   onReset: () => void,
    
  254.   onSubmit: () => void,
    
  255.   placeholder?: string,
    
  256.   value: any,
    
  257. };
    
  258. 
    
  259. function Field({
    
  260.   className,
    
  261.   onChange,
    
  262.   onReset,
    
  263.   onSubmit,
    
  264.   placeholder,
    
  265.   value,
    
  266. }: FieldProps) {
    
  267.   // $FlowFixMe[missing-local-annot]
    
  268.   const onKeyDown = event => {
    
  269.     switch (event.key) {
    
  270.       case 'Enter':
    
  271.         onSubmit();
    
  272.         break;
    
  273.       case 'Escape':
    
  274.         onReset();
    
  275.         break;
    
  276.       case 'ArrowDown':
    
  277.       case 'ArrowLeft':
    
  278.       case 'ArrowRight':
    
  279.       case 'ArrowUp':
    
  280.         event.stopPropagation();
    
  281.         break;
    
  282.       default:
    
  283.         break;
    
  284.     }
    
  285.   };
    
  286. 
    
  287.   return (
    
  288.     <AutoSizeInput
    
  289.       className={`${className} ${styles.Input}`}
    
  290.       onBlur={onSubmit}
    
  291.       onChange={(event: $FlowFixMe) => onChange(event.target.value)}
    
  292.       onKeyDown={onKeyDown}
    
  293.       placeholder={placeholder}
    
  294.       value={value}
    
  295.     />
    
  296.   );
    
  297. }