1- import {
2- batch ,
3- createEffect ,
4- Index ,
5- type JSX ,
6- mergeProps ,
7- Show ,
8- untrack ,
9- useContext ,
10- } from "solid-js" ;
1+ import { createEffect , type JSX , splitProps , useContext } from "solid-js" ;
112import invariant from "tiny-invariant" ;
123
13- import { type FocusHandle , type TextInputOptions , useChildFocus } from "catcolab-ui-components" ;
4+ import { InlineListEditor , type TextInputOptions } from "catcolab-ui-components" ;
145import type { Ob , QualifiedName } from "catlog-wasm" ;
156import { ObIdInput } from "../components" ;
167import { removeProxyAndCopy } from "../util/remove_proxy_and_copy" ;
178import { LiveModelContext } from "./context" ;
189import { buildObList , extractObList } from "./ob_operations" ;
1910import type { ObInputProps } from "./object_input" ;
2011
21- import "./object_list_editor.css" ;
22-
2312type ObListEditorProps = ObInputProps &
2413 TextInputOptions & {
2514 insertKey ?: string ;
@@ -29,35 +18,12 @@ type ObListEditorProps = ObInputProps &
2918 } ;
3019
3120/** Edits a list of objects of given type. */
32- export function ObListEditor ( originalProps : ObListEditorProps ) {
33- const props = mergeProps (
34- {
35- insertKey : "," ,
36- startDelimiter : < div class = "default-delimiter" > { "[" } </ div > ,
37- endDelimiter : < div class = "default-delimiter" > { "]" } </ div > ,
38- separator : ( ) => < div class = "default-separator" > { "," } </ div > ,
39- } ,
40- originalProps ,
41- ) ;
21+ export function ObListEditor ( allProps : ObListEditorProps ) {
22+ const [ props , listProps ] = splitProps ( allProps , [ "ob" , "setOb" , "obType" , "placeholder" ] ) ;
4223
4324 const liveModel = useContext ( LiveModelContext ) ;
4425 invariant ( liveModel , "Live model should be provided as context" ) ;
4526
46- const parentFocus : FocusHandle = {
47- hasFocus : ( ) => props . focus ?. hasFocus ( ) ?? ! ! props . isActive ,
48- setFocused : ( focused ) => {
49- if ( props . focus ) {
50- props . focus . setFocused ( focused ) ;
51- } else if ( focused ) {
52- props . hasFocused ?.( ) ;
53- }
54- } ,
55- } ;
56- const focus = useChildFocus < number > ( parentFocus , { default : 0 } ) ;
57-
58- // Track which indices have non-empty text (including incomplete input).
59- const inputTexts = new Map < number , string > ( ) ;
60-
6127 const modeAppType = ( ) => {
6228 if ( props . obType . tag !== "ModeApp" ) {
6329 throw new Error ( `Object type should be a list modality, received: ${ props . obType } ` ) ;
@@ -68,22 +34,7 @@ export function ObListEditor(originalProps: ObListEditorProps) {
6834 const obList = ( ) : Array < Ob | null > => extractObList ( props . ob ) ;
6935
7036 const setObList = ( objects : Array < Ob | null > ) => {
71- props . setOb ( buildObList ( modeAppType ( ) . content . modality , objects ) ) ;
72- } ;
73-
74- const updateObList = ( f : ( objects : Array < Ob | null > ) => void ) => {
75- const objects = removeProxyAndCopy ( obList ( ) ) ;
76- f ( objects ) ;
77- setObList ( objects ) ;
78- } ;
79-
80- const insertNewOb = ( i : number ) => {
81- batch ( ( ) => {
82- updateObList ( ( objects ) => {
83- objects . splice ( i , 0 , null ) ;
84- } ) ;
85- focus . setActiveChild ( i ) ;
86- } ) ;
37+ props . setOb ( buildObList ( modeAppType ( ) . content . modality , removeProxyAndCopy ( objects ) ) ) ;
8738 } ;
8839
8940 const completions = ( ) : QualifiedName [ ] | undefined =>
@@ -96,114 +47,21 @@ export function ObListEditor(originalProps: ObListEditorProps) {
9647 }
9748 } ) ;
9849
99- // Insert into new object into empty list when focus is gained.
100- createEffect ( ( ) => {
101- if ( parentFocus . hasFocus ( ) && untrack ( obList ) . length === 0 ) {
102- insertNewOb ( 0 ) ;
103- }
104- } ) ;
105-
106- /** Clean up null placeholders that have no user-entered text. */
107- const deactivate = ( ) => {
108- const objects = obList ( ) . filter ( ( ob , i ) => ob !== null || ( inputTexts . get ( i ) ?? "" ) !== "" ) ;
109- if ( objects . length !== obList ( ) . length ) {
110- setObList ( objects ) ;
111- }
112- } ;
113-
114- // Clean up when the component becomes inactive.
115- createEffect ( ( ) => {
116- if ( ! parentFocus . hasFocus ( ) ) {
117- untrack ( ( ) => deactivate ( ) ) ;
118- }
119- } ) ;
120-
12150 return (
122- < ul
123- class = "object-list"
124- onMouseDown = { ( evt ) => {
125- if ( obList ( ) . length === 0 ) {
126- insertNewOb ( 0 ) ;
127- parentFocus . setFocused ( true ) ;
128- evt . preventDefault ( ) ;
129- }
130- } }
131- >
132- { props . startDelimiter }
133- < Index each = { obList ( ) } fallback = { < input class = "empty-list-input" /> } >
134- { ( ob , i ) => (
135- < li >
136- < Show when = { i > 0 && props . separator } > { ( sep ) => sep ( ) ( i ) } </ Show >
137- < ObIdInput
138- ob = { ob ( ) }
139- setOb = { ( ob ) => {
140- updateObList ( ( objects ) => {
141- objects [ i ] = ob ;
142- } ) ;
143- } }
144- onTextChange = { ( text ) => inputTexts . set ( i , text ) }
145- placeholder = { props . placeholder }
146- idToLabel = { ( id ) => liveModel ( ) . elaboratedModel ( ) ?. obGeneratorLabel ( id ) }
147- labelToId = { ( label ) =>
148- liveModel ( ) . elaboratedModel ( ) ?. obGeneratorWithLabel ( label )
149- }
150- completions = { completions ( ) }
151- focus = { focus . childFocus ( i ) }
152- deleteBackward = { ( ) =>
153- batch ( ( ) => {
154- updateObList ( ( objects ) => {
155- objects . splice ( i , 1 ) ;
156- } ) ;
157- if ( i === 0 ) {
158- props . deleteBackward ?.( ) ;
159- } else {
160- focus . setActiveChild ( i - 1 ) ;
161- }
162- } )
163- }
164- deleteForward = { ( ) => {
165- batch ( ( ) => {
166- updateObList ( ( objects ) => {
167- objects . splice ( i , 1 ) ;
168- } ) ;
169- if ( i === 0 ) {
170- props . deleteForward ?.( ) ;
171- }
172- } ) ;
173- } }
174- exitBackward = { ( ) => props . exitBackward ?.( ) }
175- exitForward = { ( ) => props . exitForward ?.( ) }
176- exitLeft = { ( ) => {
177- if ( i === 0 ) {
178- props . exitLeft ?.( ) ;
179- } else {
180- focus . setActiveChild ( i - 1 ) ;
181- }
182- } }
183- exitRight = { ( ) => {
184- if ( i === obList ( ) . length - 1 ) {
185- props . exitRight ?.( ) ;
186- } else {
187- focus . setActiveChild ( i + 1 ) ;
188- }
189- } }
190- interceptKeyDown = { ( evt ) => {
191- if ( evt . key === props . insertKey ) {
192- insertNewOb ( i + 1 ) ;
193- return true ;
194- } else if ( evt . key === "Home" && ! evt . shiftKey ) {
195- // TODO: Should move to beginning of input.
196- focus . setActiveChild ( 0 ) ;
197- } else if ( evt . key === "End" && ! evt . shiftKey ) {
198- focus . setActiveChild ( obList ( ) . length - 1 ) ;
199- }
200- return false ;
201- } }
202- />
203- </ li >
204- ) }
205- </ Index >
206- { props . endDelimiter }
207- </ ul >
51+ < InlineListEditor items = { obList ( ) } setItems = { setObList } { ...listProps } >
52+ { ( ob , setOb , options ) => (
53+ < ObIdInput
54+ ob = { ob ( ) }
55+ setOb = { setOb }
56+ placeholder = { props . placeholder }
57+ idToLabel = { ( id ) => liveModel ( ) . elaboratedModel ( ) ?. obGeneratorLabel ( id ) }
58+ labelToId = { ( label ) =>
59+ liveModel ( ) . elaboratedModel ( ) ?. obGeneratorWithLabel ( label )
60+ }
61+ completions = { completions ( ) }
62+ { ...options }
63+ />
64+ ) }
65+ </ InlineListEditor >
20866 ) ;
20967}
0 commit comments