1
1
import { HtmlNode , ElementPath } from './types'
2
2
import * as Collapsible from '@radix-ui/react-collapsible'
3
- import { Fragment , useState } from 'react'
3
+ import { useState } from 'react'
4
4
import { useHtmlEditor } from './Provider'
5
5
import { isVoidElement } from '../../lib/elements'
6
6
import { addChildAtPath , isSamePath , replaceAt } from './util'
7
7
import { hasChildrenSlot } from '../../lib/codegen/util'
8
8
import { Combobox } from '../primitives'
9
9
import { HTML_TAGS } from './data'
10
10
import { DEFAULT_ATTRIBUTES , DEFAULT_STYLES } from './default-styles'
11
+ import { Plus } from 'react-feather'
12
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
11
13
12
14
interface EditorProps {
13
15
value : HtmlNode
@@ -19,13 +21,17 @@ interface TreeNodeProps extends EditorProps {
19
21
}
20
22
21
23
export function TreeNode ( { value, path, onSelect, onChange } : TreeNodeProps ) {
22
- const { selected } = useHtmlEditor ( )
24
+ const { selected, isEditing , setEditing } = useHtmlEditor ( )
23
25
const [ open , setOpen ] = useState ( true )
24
- const [ editing , setEditing ] = useState ( false )
25
26
const isSelected = isSamePath ( path , selected )
27
+ const isEditingNode = isSelected && isEditing
26
28
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 )
29
35
}
30
36
31
37
if ( value . type === 'text' ) {
@@ -42,9 +48,11 @@ export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
42
48
fontSize : 0 ,
43
49
width : '100%' ,
44
50
} }
45
- onClick = { ( ) => onSelect ( path ) }
51
+ onClick = { ( ) => {
52
+ handleSelect ( )
53
+ } }
46
54
>
47
- { editing ? (
55
+ { isEditingNode ? (
48
56
< textarea
49
57
sx = { {
50
58
display : 'block' ,
@@ -97,15 +105,15 @@ export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
97
105
textAlign : 'start' ,
98
106
fontSize : 0 ,
99
107
} }
100
- onClick = { ( ) => onSelect ( path ) }
108
+ onClick = { ( ) => handleSelect ( ) }
101
109
>
102
110
{ value . name } : "{ value . value } "
103
111
</ button >
104
112
</ div >
105
113
)
106
114
}
107
115
108
- const tagEditor = editing ? (
116
+ const tagEditor = isEditingNode ? (
109
117
< Combobox
110
118
key = { selected ?. join ( '-' ) }
111
119
onFilterItems = { ( filterValue ) => {
@@ -166,7 +174,9 @@ export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
166
174
textAlign : 'start' ,
167
175
display : 'inline-flex' ,
168
176
} }
169
- onClick = { ( ) => onSelect ( path ) }
177
+ onClick = { ( ) => {
178
+ handleSelect ( )
179
+ } }
170
180
>
171
181
<{ tagEditor }
172
182
{ ! open || isSelfClosing ( value ) ? ' /' : null } >
@@ -177,6 +187,11 @@ export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
177
187
return tagButton
178
188
}
179
189
190
+ function handleAddChild ( i : number , type : string ) {
191
+ const child = type === 'tag' ? DEFAULT_TAG : DEFAULT_TEXT
192
+ onChange ( addChildAtPath ( value , [ i ] , child ) )
193
+ }
194
+
180
195
return (
181
196
< Collapsible . Root open = { open } onOpenChange = { setOpen } >
182
197
< Collapsible . Trigger
@@ -203,17 +218,18 @@ export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
203
218
/>
204
219
< span sx = { { lineHeight : 1 , fontFamily : 'monospace' } } > { tagButton } </ span >
205
220
< 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' } } >
217
233
< TreeNode
218
234
value = { child }
219
235
onSelect = { onSelect }
@@ -225,29 +241,18 @@ export function TreeNode({ value, path, onSelect, onChange }: TreeNodeProps) {
225
241
} )
226
242
} }
227
243
/>
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
+ />
251
256
</ div >
252
257
< div sx = { { display : 'flex' , alignItems : 'center' } } >
253
258
< div
@@ -299,39 +304,83 @@ const isSelfClosing = (node: HtmlNode) => {
299
304
return ! hasChildrenSlot ( node . value )
300
305
}
301
306
302
- const DEFAULT_CHILD_NODE : HtmlNode = {
307
+ const DEFAULT_TAG : HtmlNode = {
303
308
type : 'element' ,
304
309
tagName : 'div' ,
305
- children : [
306
- {
307
- type : 'text' ,
308
- value : '' ,
309
- } ,
310
- ] ,
311
310
}
312
311
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 )
314
320
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 >
336
385
)
337
386
}
0 commit comments