1
- import { useCallback , useMemo } from "react" ;
1
+ import { useCallback , useMemo , useRef } from "react" ;
2
+ import { nanoid } from "nanoid" ;
3
+ import { mergeRefs } from "@react-aria/utils" ;
2
4
import { useStore } from "@nanostores/react" ;
3
5
import { shallowEqual } from "shallow-equal" ;
4
- import { toast } from "@webstudio-is/design-system" ;
5
- import { collectionComponent } from "@webstudio-is/react-sdk" ;
6
+ import {
7
+ toast ,
8
+ Tree ,
9
+ TreeItemLabel ,
10
+ TreeItemBody ,
11
+ type TreeItemRenderProps ,
12
+ styled ,
13
+ theme ,
14
+ getNodeVars ,
15
+ rawTheme ,
16
+ Tooltip ,
17
+ SmallIconButton ,
18
+ } from "@webstudio-is/design-system" ;
19
+ import { EyeconClosedIcon , EyeconOpenIcon } from "@webstudio-is/icons" ;
20
+ import { collectionComponent , showAttribute } from "@webstudio-is/react-sdk" ;
6
21
import {
7
22
$hoveredInstanceSelector ,
8
23
$instances ,
@@ -11,6 +26,11 @@ import {
11
26
$textEditingInstanceSelector ,
12
27
$registeredComponentMetas ,
13
28
$dragAndDropState ,
29
+ $editingItemSelector ,
30
+ $propValuesByInstanceSelector ,
31
+ getIndexedInstanceId ,
32
+ $props ,
33
+ $propsIndex ,
14
34
} from "~/shared/nano-states" ;
15
35
import type { InstanceSelector } from "~/shared/tree-utils" ;
16
36
import {
@@ -21,7 +41,136 @@ import {
21
41
isInstanceDetachable ,
22
42
getComponentTemplateData ,
23
43
} from "~/shared/instance-utils" ;
24
- import { InstanceTree } from "./tree" ;
44
+ import { useContentEditable } from "~/shared/dom-hooks" ;
45
+ import { getInstanceLabel } from "~/shared/instance-utils" ;
46
+ import { serverSyncStore } from "~/shared/sync" ;
47
+ import type { Instance } from "@webstudio-is/sdk" ;
48
+ import { MetaIcon } from "./meta-icon" ;
49
+
50
+ const TreeItem = ( {
51
+ prefix,
52
+ value,
53
+ isEditing,
54
+ isEditable = false ,
55
+ onChangeValue,
56
+ onChangeEditing,
57
+ } : {
58
+ isEditable : boolean ;
59
+ isEditing : boolean ;
60
+ prefix ?: React . ReactNode ;
61
+ value : string ;
62
+ onChangeValue : ( value : string ) => void ;
63
+ onChangeEditing : ( isEditing : boolean ) => void ;
64
+ } ) => {
65
+ const editableRef = useRef < HTMLDivElement | null > ( null ) ;
66
+ const { ref, handlers } = useContentEditable ( {
67
+ value,
68
+ isEditable,
69
+ isEditing,
70
+ onChangeValue : ( value : string ) => {
71
+ onChangeValue ( value ) ;
72
+ const button = editableRef . current ?. closest (
73
+ "[data-item-button-id]"
74
+ ) as HTMLElement ;
75
+ button ?. focus ( ) ;
76
+ } ,
77
+ onChangeEditing,
78
+ } ) ;
79
+
80
+ return (
81
+ < EditableTreeItemLabel
82
+ ref = { mergeRefs ( editableRef , ref ) }
83
+ { ...handlers }
84
+ isEditing = { isEditing }
85
+ prefix = { prefix }
86
+ >
87
+ { value }
88
+ </ EditableTreeItemLabel >
89
+ ) ;
90
+ } ;
91
+
92
+ const EditableTreeItemLabel = styled ( TreeItemLabel , {
93
+ variants : {
94
+ isEditing : {
95
+ true : {
96
+ background : theme . colors . backgroundControls ,
97
+ padding : theme . spacing [ 3 ] ,
98
+ borderRadius : theme . spacing [ 3 ] ,
99
+ color : theme . colors . hiContrast ,
100
+ outline : "none" ,
101
+ cursor : "auto" ,
102
+ textOverflow : "clip" ,
103
+ userSelect : "text" ,
104
+ } ,
105
+ } ,
106
+ } ,
107
+ } ) ;
108
+
109
+ const ShowToggle = ( {
110
+ show,
111
+ onChange,
112
+ } : {
113
+ show : boolean ;
114
+ onChange : ( show : boolean ) => void ;
115
+ } ) => {
116
+ return (
117
+ < Tooltip
118
+ // If you are changing it, change the other one too
119
+ content = "Removes the instance from the DOM. Breakpoints have no effect on this setting."
120
+ disableHoverableContent
121
+ variant = "wrapped"
122
+ >
123
+ < SmallIconButton
124
+ aria-label = "Show"
125
+ onClick = { ( ) => onChange ( show ? false : true ) }
126
+ icon = { show ? < EyeconOpenIcon /> : < EyeconClosedIcon /> }
127
+ />
128
+ </ Tooltip >
129
+ ) ;
130
+ } ;
131
+
132
+ const updateShowProp = ( instanceId : Instance [ "id" ] , value : boolean ) => {
133
+ serverSyncStore . createTransaction ( [ $props ] , ( props ) => {
134
+ const { propsByInstanceId } = $propsIndex . get ( ) ;
135
+ const instanceProps = propsByInstanceId . get ( instanceId ) ;
136
+ let showProp = instanceProps ?. find ( ( prop ) => prop . name === showAttribute ) ;
137
+
138
+ if ( showProp === undefined ) {
139
+ showProp = {
140
+ id : nanoid ( ) ,
141
+ instanceId,
142
+ name : showAttribute ,
143
+ type : "boolean" ,
144
+ value,
145
+ } ;
146
+ }
147
+ if ( showProp . type === "boolean" ) {
148
+ props . set ( showProp . id , {
149
+ ...showProp ,
150
+ value,
151
+ } ) ;
152
+ }
153
+ } ) ;
154
+ } ;
155
+
156
+ const updateInstanceLabel = ( instanceId : Instance [ "id" ] , value : string ) => {
157
+ serverSyncStore . createTransaction ( [ $instances ] , ( instances ) => {
158
+ const instance = instances . get ( instanceId ) ;
159
+ if ( instance === undefined ) {
160
+ return ;
161
+ }
162
+ instance . label = value ;
163
+ } ) ;
164
+ } ;
165
+
166
+ const canLeaveParent = ( [ instanceId ] : InstanceSelector ) => {
167
+ const instance = $instances . get ( ) . get ( instanceId ) ;
168
+ if ( instance === undefined ) {
169
+ return false ;
170
+ }
171
+ const meta = $registeredComponentMetas . get ( ) . get ( instance . component ) ;
172
+ return meta ?. type !== "rich-text-child" ;
173
+ } ;
25
174
26
175
export const NavigatorTree = ( ) => {
27
176
const selectedInstanceSelector = useStore ( $selectedInstanceSelector ) ;
@@ -30,6 +179,8 @@ export const NavigatorTree = () => {
30
179
const instances = useStore ( $instances ) ;
31
180
const metas = useStore ( $registeredComponentMetas ) ;
32
181
const state = useStore ( $dragAndDropState ) ;
182
+ const editingItemSelector = useStore ( $editingItemSelector ) ;
183
+ const propValues = useStore ( $propValuesByInstanceSelector ) ;
33
184
34
185
const dragPayload = state . dragPayload ;
35
186
@@ -117,17 +268,128 @@ export const NavigatorTree = () => {
117
268
$textEditingInstanceSelector . set ( undefined ) ;
118
269
} , [ ] ) ;
119
270
271
+ const getItemChildren = useCallback (
272
+ ( instanceSelector : InstanceSelector ) => {
273
+ const [ instanceId , parentId ] = instanceSelector ;
274
+ let instance = instances . get ( instanceId ) ;
275
+ const children : Instance [ ] = [ ] ;
276
+
277
+ // put fake collection item instances according to collection data
278
+ if ( instance ?. component === collectionComponent ) {
279
+ const data = propValues
280
+ . get ( JSON . stringify ( instanceSelector ) )
281
+ ?. get ( "data" ) ;
282
+ // create items only when collection has content
283
+ if ( Array . isArray ( data ) && instance . children . length > 0 ) {
284
+ data . forEach ( ( _item , index ) => {
285
+ children . push ( {
286
+ type : "instance" ,
287
+ id : getIndexedInstanceId ( instanceId , index ) ,
288
+ component : "ws:collection-item" ,
289
+ children : [ ] ,
290
+ } ) ;
291
+ } ) ;
292
+ }
293
+ return children ;
294
+ }
295
+
296
+ // put parent children as own when parent is a collection
297
+ if ( instance === undefined ) {
298
+ const parentInstance = instances . get ( parentId ) ;
299
+ if ( parentInstance ?. component === collectionComponent ) {
300
+ instance = parentInstance ;
301
+ }
302
+ }
303
+ if ( instance === undefined ) {
304
+ return children ;
305
+ }
306
+
307
+ for ( const child of instance . children ) {
308
+ if ( child . type !== "id" ) {
309
+ continue ;
310
+ }
311
+ const childInstance = instances . get ( child . value ) ;
312
+ if ( childInstance === undefined ) {
313
+ continue ;
314
+ }
315
+ children . push ( childInstance ) ;
316
+ }
317
+ return children ;
318
+ } ,
319
+ [ instances , propValues ]
320
+ ) ;
321
+
322
+ const renderItem = useCallback (
323
+ ( props : TreeItemRenderProps < Instance > ) => {
324
+ const { itemData, itemSelector } = props ;
325
+ const meta = metas . get ( itemData . component ) ;
326
+ if ( meta === undefined ) {
327
+ return < > </ > ;
328
+ }
329
+ const label = getInstanceLabel ( itemData , meta ) ;
330
+ const isEditing = shallowEqual ( itemSelector , editingItemSelector ) ;
331
+ const instanceProps = propValues . get ( JSON . stringify ( itemSelector ) ) ;
332
+ const show = Boolean ( instanceProps ?. get ( showAttribute ) ?? true ) ;
333
+
334
+ return (
335
+ < TreeItemBody
336
+ { ...props }
337
+ selectionEvent = "focus"
338
+ suffix = {
339
+ < ShowToggle
340
+ show = { show }
341
+ onChange = { ( show ) => {
342
+ updateShowProp ( itemData . id , show ) ;
343
+ } }
344
+ />
345
+ }
346
+ >
347
+ < TreeItem
348
+ isEditable = { true }
349
+ isEditing = { isEditing }
350
+ onChangeValue = { ( val ) => {
351
+ updateInstanceLabel ( props . itemData . id , val ) ;
352
+ } }
353
+ onChangeEditing = { ( isEditing ) => {
354
+ $editingItemSelector . set (
355
+ isEditing === true ? props . itemSelector : undefined
356
+ ) ;
357
+ } }
358
+ prefix = { < MetaIcon icon = { meta . icon } /> }
359
+ value = { label }
360
+ />
361
+ </ TreeItemBody >
362
+ ) ;
363
+ } ,
364
+ [ metas , editingItemSelector , propValues ]
365
+ ) ;
366
+
120
367
if ( rootInstance === undefined ) {
121
368
return ;
122
369
}
123
370
124
371
return (
125
- < InstanceTree
372
+ < Tree
126
373
root = { rootInstance }
127
374
selectedItemSelector = { selectedInstanceSelector }
128
375
highlightedItemSelector = { hoveredInstanceSelector }
129
376
dragItemSelector = { dragItemSelector }
130
377
dropTarget = { state . dropTarget }
378
+ canLeaveParent = { canLeaveParent }
379
+ getItemChildren = { getItemChildren }
380
+ getItemProps = { ( { itemData, itemSelector } ) => {
381
+ const props = propValues . get ( JSON . stringify ( itemSelector ) ) ;
382
+ const opacity = props ?. get ( showAttribute ) === false ? 0.4 : undefined ;
383
+ const color =
384
+ itemData . component === "Slot"
385
+ ? rawTheme . colors . foregroundReusable
386
+ : undefined ;
387
+ return {
388
+ style : getNodeVars ( { color, opacity } ) ,
389
+ } ;
390
+ } }
391
+ renderItem = { renderItem }
392
+ editingItemId = { editingItemSelector ?. [ 0 ] }
131
393
isItemHidden = { isItemHidden }
132
394
findClosestDroppableIndex = { findClosestDroppableIndex }
133
395
onSelect = { handleSelect }
0 commit comments