Skip to content

Commit 77106b7

Browse files
authored
REFACTOR: Extract inline list editor into ui-components (#1321)
1 parent 2e349d6 commit 77106b7

6 files changed

Lines changed: 415 additions & 191 deletions

File tree

packages/frontend/src/model/object_list_editor.css

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 20 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,14 @@
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";
112
import 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";
145
import type { Ob, QualifiedName } from "catlog-wasm";
156
import { ObIdInput } from "../components";
167
import { removeProxyAndCopy } from "../util/remove_proxy_and_copy";
178
import { LiveModelContext } from "./context";
189
import { buildObList, extractObList } from "./ob_operations";
1910
import type { ObInputProps } from "./object_input";
2011

21-
import "./object_list_editor.css";
22-
2312
type 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
}

packages/ui-components/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export * from "./form";
1414
export * from "./history_navigator";
1515
export * from "./icon_button";
1616
export * from "./inline_input";
17+
export * from "./inline_list_editor";
1718
export * from "./input_options";
1819
export * from "./katex_display";
1920
export * from "./model_file_icon";
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
.inlineList {
2+
display: flex;
3+
flex-direction: row;
4+
align-items: center;
5+
list-style: none;
6+
padding: 0;
7+
8+
li {
9+
display: flex;
10+
flex-direction: row;
11+
}
12+
}
13+
14+
.defaultDelimiter,
15+
.defaultSeparator {
16+
color: var(--color-gray-800);
17+
}
18+
19+
.defaultDelimiter {
20+
transform: scale(1, 1.5);
21+
}
22+
23+
.emptyListInput {
24+
background: transparent;
25+
border: none;
26+
outline: none;
27+
width: 0.5ex;
28+
margin: 0;
29+
padding: 0;
30+
}

0 commit comments

Comments
 (0)