src/EditableTree.js
/**
 * A node that's currently being edited. Supports editing as a scalar value, or editing visually as
 * a subtree.
 */
A node that's currently being edited. Supports editing as a scalar value, or editing visually as
a subtree.
A node that ' s currently being edited . Supports editing as a scalar value or editing visually as a subtree .
function EditorNode({
  initialKey, initialValue, isRoot,
  doneEditingCallback, addSiblingCallback,
}) {
  let [editedKey, setEditedKey] = useState(initialKey || '');
  let [editedScalarValue, setEditedScalarValue] = useState(initialValue || '');
  let [visualChildren, setVisualChildren] = useState(null);
  let keyEditorRef = useRef();
  let finishEditing = ({ cancel, reason } = {}) => {
    if (cancel) {
      doneEditingCallback(initialKey, initialValue, reason);
    }
    if (visualChildren) {
      let newValue = Object.fromEntries(visualChildren
        .filter(({ k }) => !!k)
        .map(({ k, v }) => [k, v]));
      doneEditingCallback(editedKey, newValue, reason);
    } else {
      doneEditingCallback(editedKey, editedScalarValue, reason);
    }
  };
  let resizeKeyEditor = (node, val) =>
    node && (node.style.width = `${(val || KEY_PLACEHOLDER).length * 8}px`);
  let handleKeyDown = ev => {
    ev.key === 'Enter' && finishEditing({ reason: 'enter' });
    ev.key === 'Escape' && finishEditing({ cancel: true, reason: 'cancel' });
  };
  useEffect(() => {
    !initialKey && keyEditorRef.current && keyEditorRef.current.focus();
  }, [keyEditorRef]);
  let blurTimeout = useRef();
  useEffect(() => {
    return () => clearTimeout(blurTimeout.current);
  }, []);
  let addChild = () => setVisualChildren([...(visualChildren || []), {}]);
  return <div className={cn('node', { 'is-root': isRoot })}
    onFocus={() => clearTimeout(blurTimeout.current)}
    onBlur={() => {
      clearTimeout(blurTimeout.current);
      blurTimeout.current = setTimeout(() => finishEditing({ reason: 'blur' }));
    }}>
    <div className="node-row">
      <div className={cn('node-kv-edit-box', { 'is-tree-edit-mode': !!visualChildren })}>
        <input className="key-editor"
          disabled={isRoot}
          placeholder={KEY_PLACEHOLDER}
          ref={node => {
            node && resizeKeyEditor(node, editedKey);
            keyEditorRef.current = node;
          }}
          onKeyDown={handleKeyDown}
          value={editedKey}
          onChange={ev => {
            setEditedKey(ev.target.value);
            resizeKeyEditor(ev.target, ev.target.value);
          }} />
        {!visualChildren && <TypedInput
          className="value-editor"
          initialValue={initialValue}
          autoFocus={!!initialKey}
          onKeyDown={handleKeyDown}
          onChange={value => setEditedScalarValue(value)} />}
        {(visualChildren || !editedScalarValue) &&
          <button className="icon-button add-button" onClick={addChild}>
            <i className="material-icons">add</i>
          </button>}
        <div style={{ width: 0, height: 0 }}
          tabIndex={0}
          onFocus={() => {
            finishEditing({ reason: 'add-sibling' });
            addSiblingCallback();
          }} />
      </div>
    </div>
    {visualChildren && <div className="node-children">
      {visualChildren.map((obj, idx) =>
        <EditorNode key={idx}
          doneEditingCallback={(k, v, reason) => {
            obj.k = k;
            obj.v = v;
            if (reason === 'enter') {
              finishEditing({ reason });
            }
          }}
          addSiblingCallback={addChild} />
      )}
    </div>}
  </div>;
}
function EditorNode ( { initialKey , initialValue , isRoot , doneEditingCallback , addSiblingCallback , } ) { let [ editedKey , setEditedKey ] = useState ( initialKey || '' ) ; let [ editedScalarValue , setEditedScalarValue ] = useState ( initialValue || '' ) ; let [ visualChildren , setVisualChildren ] = useState ( null ) ; let keyEditorRef = useRef ( ) ; let finishEditing = ( { cancel , reason } = { } ) => { if ( cancel ) { doneEditingCallback ( initialKey , initialValue , reason ) ; } if ( visualChildren ) { let newValue = Object . fromEntries ( visualChildren . filter ( ( { k } ) => ! ! k ) . map ( ( { k , v } ) => [ k , v ] ) ) ; doneEditingCallback ( editedKey , newValue , reason ) ; } else { doneEditingCallback ( editedKey , editedScalarValue , reason ) ; } } ; let resizeKeyEditor = ( node , val ) => node && ( node . style . width = ` ${ ( val || KEY_PLACEHOLDER ) . length * 8 } ` ) ; let handleKeyDown = ev => { ev . key === 'Enter' && finishEditing ( { reason : 'enter' } ) ; ev . key === 'Escape' && finishEditing ( { cancel : true , reason : 'cancel' } ) ; } ; useEffect ( ( ) => { ! initialKey && keyEditorRef . current && keyEditorRef . current . focus ( ) ; } , [ keyEditorRef ] ) ; let blurTimeout = useRef ( ) ; useEffect ( ( ) => { return ( ) => clearTimeout ( blurTimeout . current ) ; } , [ ] ) ; let addChild = ( ) => setVisualChildren ( [ ... ( visualChildren || [ ] ) , { } ] ) ; return < div className = { cn ( 'node' , { 'is-root' : isRoot } ) } onFocus = { ( ) => clearTimeout ( blurTimeout . current ) } onBlur = { ( ) => { clearTimeout ( blurTimeout . current ) ; blurTimeout . current = setTimeout ( ( ) => finishEditing ( { reason : 'blur' } ) ) ; } } > < div className = "node-row" > < div className = { cn ( 'node-kv-edit-box' , { 'is-tree-edit-mode' : ! ! visualChildren } ) } > < input className = "key-editor" disabled = { isRoot } placeholder = { KEY_PLACEHOLDER } ref = { node => { node && resizeKeyEditor ( node , editedKey ) ; keyEditorRef . current = node ; } } onKeyDown = { handleKeyDown } value = { editedKey } onChange = { ev => { setEditedKey ( ev . target . value ) ; resizeKeyEditor ( ev . target , ev . target . value ) ; } } / > { ! visualChildren && < TypedInput className = "value-editor" initialValue = { initialValue } autoFocus = { ! ! initialKey } onKeyDown = { handleKeyDown } onChange = { value => setEditedScalarValue ( value ) } / > } { ( visualChildren || ! editedScalarValue ) && < button className = "icon-button add-button" onClick = { addChild } > < i className = "material-icons" > add < / i > < / button > } < div style = { { width : 0 , height : 0 } } tabIndex = { 0 } onFocus = { ( ) => { finishEditing ( { reason : 'add-sibling' } ) ; addSiblingCallback ( ) ; } } / > < / div > < / div > { visualChildren && < div className = "node-children" > { visualChildren . map ( ( obj , idx ) => < EditorNode key = { idx } doneEditingCallback = { ( k , v , reason ) => { obj . k = k ; obj . v = v ; if ( reason === 'enter' ) { finishEditing ( { reason } ) ; } } } addSiblingCallback = { addChild } / > ) } < / div > } < / div > ; }
A node that's currently being edited.
A node that ' s currently being edited .
// gather data