src/EditableTree.js
/**
 * Renders a sub-tree, allowing new children to be added to the tree, and allowing
 * child items to be edited.
 */
Renders a sub-tree, allowing new children to be added to the tree, and allowing
child items to be edited.
Renders a sub - tree allowing new children to be added to the tree and allowing child items to be edited .
function SubtreeNode({ k, value, isRoot, updateCallback }, ref) {
  let [addingChild, setAddingChild] = useState(false);
  let [collapsed, setCollapsed] = useState(false);
  let childRefs = useRef({});
  let deepSetCollapsed = collapsed => {
    setCollapsed(collapsed);
    for (let [, v] of Object.entries(childRefs.current)) {
      v && v.setCollapsed && v.setCollapsed(collapsed);
    }
  };
  useImperativeHandle(ref, () => ({
    setCollapsed: deepSetCollapsed
  }));
  useEffect(() => {
    if (!!value && value._collapsed) {
      setCollapsed(true);
      delete value._collapsed;
      updateCallback(k, value);
    }
  }, []);
  return <div ref={ref} className={cn('node',
    {
      'is-collapsed': collapsed && !isRoot,
      'is-root': isRoot
    })}>
    <div className="node-row">
      {!isRoot && <button className="icon-button collapse-button"
        onClick={ev => ((ev.shiftKey || ev.ctrlKey)
          ? deepSetCollapsed
          : setCollapsed)(!collapsed)}>
        <i className="material-icons">arrow_drop_down</i>
      </button>}
      <div className="node-kv-box">
        <div className="node-key">{k}</div>
        <button
          className="icon-button add-button"
          onClick={() => { setAddingChild(true); setCollapsed(false); }}>
          <i className="material-icons">add</i>
        </button>
        <button
          className="icon-button delete-button"
          onClick={() => updateCallback(k, null)}>
          <i className="material-icons">close</i>
        </button>
      </div>
    </div>
    <div className="node-children">
      {Object.entries(value || {})
        .filter(([k, v]) => !k.startsWith('_') && v !== null)
        .sort(([k1], [k2]) => k1.localeCompare(k2))
        .map(([ck, v]) =>
          <Node
            key={ck}
            k={ck}
            value={v}
            ref={node => childRefs.current[ck] = node}
            updateCallback={(newChildKey, newChildValue) => {
              let newValue = { ...value };
              delete newValue[ck];
              if (newChildValue !== null) {
                newValue[newChildKey] = newChildValue;
              } else {
                delete newValue[newChildKey];
              }
              updateCallback(k, newValue); 
            }}
            addSiblingCallback={() => setTimeout(() => setAddingChild(true))}
          />
        )}
      {addingChild && <EditorNode
        doneEditingCallback={(newChildKey, newChildValue) => {
          if (!newChildKey) {
            setAddingChild(false);
            return;
          }
          updateCallback(k, { ...value, [newChildKey]: newChildValue });
          setAddingChild(false);
        }}
        addSiblingCallback={() => setTimeout(() => setAddingChild(true))}
      />}
    </div>
  </div>;
}
function SubtreeNode ( { k , value , isRoot , updateCallback } , ref ) { let [ addingChild , setAddingChild ] = useState ( false ) ; let [ collapsed , setCollapsed ] = useState ( false ) ; let childRefs = useRef ( { } ) ; let deepSetCollapsed = collapsed => { setCollapsed ( collapsed ) ; for ( let [ , v ] of Object . entries ( childRefs . current ) ) { v && v . setCollapsed && v . setCollapsed ( collapsed ) ; } } ; useImperativeHandle ( ref , ( ) => ( { setCollapsed : deepSetCollapsed } ) ) ; useEffect ( ( ) => { if ( ! ! value && value . _collapsed ) { setCollapsed ( true ) ; delete value . _collapsed ; updateCallback ( k , value ) ; } } , [ ] ) ; return < div ref = { ref } className = { cn ( 'node' , { 'is-collapsed' : collapsed && ! isRoot , 'is-root' : isRoot } ) } > < div className = "node-row" > { ! isRoot && < button className = "icon-button collapse-button" onClick = { ev => ( ( ev . shiftKey || ev . ctrlKey ) ? deepSetCollapsed : setCollapsed ) ( ! collapsed ) } > < i className = "material-icons" > arrow_drop_down < / i > < / button > } < div className = "node-kv-box" > < div className = "node-key" > { k } < / div > < button className = "icon-button add-button" onClick = { ( ) => { setAddingChild ( true ) ; setCollapsed ( false ) ; } } > < i className = "material-icons" > add < / i > < / button > < button className = "icon-button delete-button" onClick = { ( ) => updateCallback ( k , null ) } > < i className = "material-icons" > close < / i > < / button > < / div > < / div > < div className = "node-children" > { Object . entries ( value || { } ) . filter ( ( [ k , v ] ) => ! k . startsWith ( '_' ) && v !== null ) . sort ( ( [ k1 ] , [ k2 ] ) => k1 . localeCompare ( k2 ) ) . map ( ( [ ck , v ] ) => < Node key = { ck } k = { ck } value = { v } ref = { node => childRefs . current [ ck ] = node } updateCallback = { ( newChildKey , newChildValue ) => { let newValue = { ... value } ; delete newValue [ ck ] ; if ( newChildValue !== null ) { newValue [ newChildKey ] = newChildValue ; } else { delete newValue [ newChildKey ] ; } updateCallback ( k , newValue ) ; } } addSiblingCallback = { ( ) => setTimeout ( ( ) => setAddingChild ( true ) ) } / > ) } { addingChild && < EditorNode doneEditingCallback = { ( newChildKey , newChildValue ) => { if ( ! newChildKey ) { setAddingChild ( false ) ; return ; } updateCallback ( k , { ... value , [ newChildKey ] : newChildValue } ) ; setAddingChild ( false ) ; } } addSiblingCallback = { ( ) => setTimeout ( ( ) => setAddingChild ( true ) ) } / > } < / div > < / div > ; }
Renders a sub-tree, allowing new children to be added to the tree, and allowing
 child items to be edited.
Renders a sub - tree allowing new children to be added to the tree and allowing child items to be edited .
// propagate update // propagate update