11import { HtmlNode , ElementPath } from './types'
22import * as Collapsible from '@radix-ui/react-collapsible'
3- import { Fragment , useState } from 'react'
3+ import { useState } from 'react'
44import { useHtmlEditor } from './Provider'
55import { isVoidElement } from '../../lib/elements'
66import { addChildAtPath , isSamePath , replaceAt } from './util'
77import { hasChildrenSlot } from '../../lib/codegen/util'
88import { Combobox } from '../primitives'
99import { HTML_TAGS } from './data'
1010import { DEFAULT_ATTRIBUTES , DEFAULT_STYLES } from './default-styles'
11+ import { Plus } from 'react-feather'
12+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
1113
1214interface EditorProps {
1315 value : HtmlNode
@@ -19,13 +21,17 @@ interface TreeNodeProps extends EditorProps {
1921}
2022
2123export function TreeNode ( { value, path, onSelect, onChange } : TreeNodeProps ) {
22- const { selected } = useHtmlEditor ( )
24+ const { selected, isEditing , setEditing } = useHtmlEditor ( )
2325 const [ open , setOpen ] = useState ( true )
24- const [ editing , setEditing ] = useState ( false )
2526 const isSelected = isSamePath ( path , selected )
27+ const isEditingNode = isSelected && isEditing
2628
27- if ( editing && ! isSelected ) {
28- setEditing ( false )
29+ function handleSelect ( ) {
30+ // If we are selecting a different node than the currently selected node, move out of editing mode
31+ if ( ! isSelected ) {
32+ setEditing ( false )
33+ }
34+ onSelect ( path )
2935 }
3036
3137 if ( value . type === 'text' ) {
@@ -42,9 +48,11 @@ export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
4248 fontSize : 0 ,
4349 width : '100%' ,
4450 } }
45- onClick = { ( ) => onSelect ( path ) }
51+ onClick = { ( ) => {
52+ handleSelect ( )
53+ } }
4654 >
47- { editing ? (
55+ { isEditingNode ? (
4856 < textarea
4957 sx = { {
5058 display : 'block' ,
@@ -97,15 +105,15 @@ export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
97105 textAlign : 'start' ,
98106 fontSize : 0 ,
99107 } }
100- onClick = { ( ) => onSelect ( path ) }
108+ onClick = { ( ) => handleSelect ( ) }
101109 >
102110 { value . name } : "{ value . value } "
103111 </ button >
104112 </ div >
105113 )
106114 }
107115
108- const tagEditor = editing ? (
116+ const tagEditor = isEditingNode ? (
109117 < Combobox
110118 key = { selected ?. join ( '-' ) }
111119 onFilterItems = { ( filterValue ) => {
@@ -166,7 +174,9 @@ export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
166174 textAlign : 'start' ,
167175 display : 'inline-flex' ,
168176 } }
169- onClick = { ( ) => onSelect ( path ) }
177+ onClick = { ( ) => {
178+ handleSelect ( )
179+ } }
170180 >
171181 <{ tagEditor }
172182 { ! open || isSelfClosing ( value ) ? ' /' : null } >
@@ -177,6 +187,11 @@ export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
177187 return tagButton
178188 }
179189
190+ function handleAddChild ( i : number , type : string ) {
191+ const child = type === 'tag' ? DEFAULT_TAG : DEFAULT_TEXT
192+ onChange ( addChildAtPath ( value , [ i ] , child ) )
193+ }
194+
180195 return (
181196 < Collapsible . Root open = { open } onOpenChange = { setOpen } >
182197 < Collapsible . Trigger
@@ -203,17 +218,18 @@ export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
203218 />
204219 < span sx = { { lineHeight : 1 , fontFamily : 'monospace' } } > { tagButton } </ span >
205220 < Collapsible . Content >
206- < div sx = { { ml : 4 } } >
207- { value . children ?. length ? (
208- value . children ?. map ( ( child , i ) => {
209- return (
210- < Fragment key = { i } >
211- < AddChildButton
212- onClick = { ( ) => {
213- onChange ( addChildAtPath ( value , [ i ] , DEFAULT_CHILD_NODE ) )
214- onSelect ( [ ...path , i ] )
215- } }
216- />
221+ < div sx = { { ml : 4 , py : '0.0625rem' } } >
222+ { value . children ?. map ( ( child , i ) => {
223+ return (
224+ < div key = { i } >
225+ < AddChildButton
226+ onClick = { ( childType ) => {
227+ handleAddChild ( i , childType )
228+ onSelect ( [ ...path , i ] )
229+ setEditing ( true )
230+ } }
231+ />
232+ < div sx = { { py : '0.0625rem' } } >
217233 < TreeNode
218234 value = { child }
219235 onSelect = { onSelect }
@@ -225,29 +241,18 @@ export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
225241 } )
226242 } }
227243 />
228- < AddChildButton
229- onClick = { ( ) => {
230- onChange (
231- addChildAtPath (
232- value ,
233- [ value . children ?. length ?? 0 ] ,
234- DEFAULT_CHILD_NODE
235- )
236- )
237- onSelect ( null )
238- } }
239- />
240- </ Fragment >
241- )
242- } )
243- ) : (
244- < AddChildButton
245- onClick = { ( ) => {
246- onChange ( addChildAtPath ( value , [ 0 ] , DEFAULT_CHILD_NODE ) )
247- onSelect ( [ 0 ] )
248- } }
249- />
250- ) }
244+ </ div >
245+ </ div >
246+ )
247+ } ) }
248+ < AddChildButton
249+ onClick = { ( childType ) => {
250+ const index = value . children ?. length ?? 0
251+ handleAddChild ( index , childType )
252+ onSelect ( [ ...path , index ] )
253+ setEditing ( true )
254+ } }
255+ />
251256 </ div >
252257 < div sx = { { display : 'flex' , alignItems : 'center' } } >
253258 < div
@@ -299,39 +304,83 @@ const isSelfClosing = (node: HtmlNode) => {
299304 return ! hasChildrenSlot ( node . value )
300305}
301306
302- const DEFAULT_CHILD_NODE : HtmlNode = {
307+ const DEFAULT_TAG : HtmlNode = {
303308 type : 'element' ,
304309 tagName : 'div' ,
305- children : [
306- {
307- type : 'text' ,
308- value : '' ,
309- } ,
310- ] ,
311310}
312311
313- function AddChildButton ( { onClick } : { onClick ( ) : void } ) {
312+ const DEFAULT_TEXT : HtmlNode = {
313+ type : 'text' ,
314+ value : '' ,
315+ }
316+
317+ function AddChildButton ( { onClick } : { onClick ( type : string ) : void } ) {
318+ const [ hovered , setHovered ] = useState ( false )
319+ const [ open , setOpen ] = useState ( false )
314320 return (
315- < button
316- onClick = { onClick }
317- sx = { {
318- cursor : 'pointer' ,
319- display : 'block' ,
320- background : 'none' ,
321- border : 'none' ,
322- textAlign : 'left' ,
323- fontSize : 0 ,
324- pt : 2 ,
325- whiteSpace : 'nowrap' ,
326- color : 'muted' ,
327- zIndex : '99' ,
328- transition : 'color .2s ease-in-out' ,
329- ':hover' : {
330- color : 'text' ,
331- } ,
332- } }
333- >
334- + Add child
335- </ button >
321+ < div sx = { { position : 'relative' } } >
322+ < DropdownMenu . Root open = { open } onOpenChange = { setOpen } >
323+ < DropdownMenu . Trigger
324+ sx = { {
325+ '--height' : '0.5rem' ,
326+ display : 'flex' ,
327+ alignItems : 'center' ,
328+ position : 'absolute' ,
329+ height : 'var(--height)' ,
330+ top : 'calc(var(--height) / -2 )' ,
331+ width : '100%' ,
332+ cursor : 'pointer' ,
333+ ':hover' : {
334+ color : 'muted' ,
335+ } ,
336+
337+ background : 'transparent' ,
338+ border : 'none' ,
339+
340+ '::before' : {
341+ content : '""' ,
342+ backgroundColor : hovered || open ? 'muted' : 'transparent' ,
343+ display : 'block' ,
344+ height : '2px' ,
345+ width : '100%' ,
346+ } ,
347+ } }
348+ onMouseEnter = { ( ) => setHovered ( true ) }
349+ onMouseLeave = { ( ) => setHovered ( false ) }
350+ >
351+ < Plus size = { 16 } />
352+ </ DropdownMenu . Trigger >
353+ < DropdownMenu . Content
354+ sx = { {
355+ minWidth : '12rem' ,
356+ border : '1px solid' ,
357+ borderColor : 'border' ,
358+ borderRadius : '0.25rem' ,
359+ backgroundColor : 'background' ,
360+ py : 2 ,
361+ } }
362+ >
363+ { [ 'tag' , 'text' ] . map ( ( childType ) => {
364+ return (
365+ < DropdownMenu . Item
366+ key = { childType }
367+ onClick = { ( ) => {
368+ onClick ( childType )
369+ } }
370+ sx = { {
371+ cursor : 'pointer' ,
372+ px : 3 ,
373+ ':hover' : {
374+ backgroundColor : 'backgroundOffset' ,
375+ } ,
376+ } }
377+ >
378+ Add { childType }
379+ </ DropdownMenu . Item >
380+ )
381+ } ) }
382+ </ DropdownMenu . Content >
383+ </ DropdownMenu . Root >
384+ </ div >
336385 )
337386}
0 commit comments