Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 0 additions & 29 deletions packages/frontend/src/model/object_list_editor.css

This file was deleted.

182 changes: 20 additions & 162 deletions packages/frontend/src/model/object_list_editor.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,14 @@
import {
batch,
createEffect,
Index,
type JSX,
mergeProps,
Show,
untrack,
useContext,
} from "solid-js";
import { createEffect, type JSX, splitProps, useContext } from "solid-js";
import invariant from "tiny-invariant";

import { type FocusHandle, type TextInputOptions, useChildFocus } from "catcolab-ui-components";
import { InlineListEditor, type TextInputOptions } from "catcolab-ui-components";
import type { Ob, QualifiedName } from "catlog-wasm";
import { ObIdInput } from "../components";
import { removeProxyAndCopy } from "../util/remove_proxy_and_copy";
import { LiveModelContext } from "./context";
import { buildObList, extractObList } from "./ob_operations";
import type { ObInputProps } from "./object_input";

import "./object_list_editor.css";

type ObListEditorProps = ObInputProps &
TextInputOptions & {
insertKey?: string;
Expand All @@ -29,35 +18,12 @@ type ObListEditorProps = ObInputProps &
};

/** Edits a list of objects of given type. */
export function ObListEditor(originalProps: ObListEditorProps) {
const props = mergeProps(
{
insertKey: ",",
startDelimiter: <div class="default-delimiter">{"["}</div>,
endDelimiter: <div class="default-delimiter">{"]"}</div>,
separator: () => <div class="default-separator">{","}</div>,
},
originalProps,
);
export function ObListEditor(allProps: ObListEditorProps) {
const [props, listProps] = splitProps(allProps, ["ob", "setOb", "obType", "placeholder"]);

const liveModel = useContext(LiveModelContext);
invariant(liveModel, "Live model should be provided as context");

const parentFocus: FocusHandle = {
hasFocus: () => props.focus?.hasFocus() ?? !!props.isActive,
setFocused: (focused) => {
if (props.focus) {
props.focus.setFocused(focused);
} else if (focused) {
props.hasFocused?.();
}
},
};
const focus = useChildFocus<number>(parentFocus, { default: 0 });

// Track which indices have non-empty text (including incomplete input).
const inputTexts = new Map<number, string>();

const modeAppType = () => {
if (props.obType.tag !== "ModeApp") {
throw new Error(`Object type should be a list modality, received: ${props.obType}`);
Expand All @@ -68,22 +34,7 @@ export function ObListEditor(originalProps: ObListEditorProps) {
const obList = (): Array<Ob | null> => extractObList(props.ob);

const setObList = (objects: Array<Ob | null>) => {
props.setOb(buildObList(modeAppType().content.modality, objects));
};

const updateObList = (f: (objects: Array<Ob | null>) => void) => {
const objects = removeProxyAndCopy(obList());
f(objects);
setObList(objects);
};

const insertNewOb = (i: number) => {
batch(() => {
updateObList((objects) => {
objects.splice(i, 0, null);
});
focus.setActiveChild(i);
});
props.setOb(buildObList(modeAppType().content.modality, removeProxyAndCopy(objects)));
};

const completions = (): QualifiedName[] | undefined =>
Expand All @@ -96,114 +47,21 @@ export function ObListEditor(originalProps: ObListEditorProps) {
}
});

// Insert into new object into empty list when focus is gained.
createEffect(() => {
if (parentFocus.hasFocus() && untrack(obList).length === 0) {
insertNewOb(0);
}
});

/** Clean up null placeholders that have no user-entered text. */
const deactivate = () => {
const objects = obList().filter((ob, i) => ob !== null || (inputTexts.get(i) ?? "") !== "");
if (objects.length !== obList().length) {
setObList(objects);
}
};

// Clean up when the component becomes inactive.
createEffect(() => {
if (!parentFocus.hasFocus()) {
untrack(() => deactivate());
}
});

return (
<ul
class="object-list"
onMouseDown={(evt) => {
if (obList().length === 0) {
insertNewOb(0);
parentFocus.setFocused(true);
evt.preventDefault();
}
}}
>
{props.startDelimiter}
<Index each={obList()} fallback={<input class="empty-list-input" />}>
{(ob, i) => (
<li>
<Show when={i > 0 && props.separator}>{(sep) => sep()(i)}</Show>
<ObIdInput
ob={ob()}
setOb={(ob) => {
updateObList((objects) => {
objects[i] = ob;
});
}}
onTextChange={(text) => inputTexts.set(i, text)}
placeholder={props.placeholder}
idToLabel={(id) => liveModel().elaboratedModel()?.obGeneratorLabel(id)}
labelToId={(label) =>
liveModel().elaboratedModel()?.obGeneratorWithLabel(label)
}
completions={completions()}
focus={focus.childFocus(i)}
deleteBackward={() =>
batch(() => {
updateObList((objects) => {
objects.splice(i, 1);
});
if (i === 0) {
props.deleteBackward?.();
} else {
focus.setActiveChild(i - 1);
}
})
}
deleteForward={() => {
batch(() => {
updateObList((objects) => {
objects.splice(i, 1);
});
if (i === 0) {
props.deleteForward?.();
}
});
}}
exitBackward={() => props.exitBackward?.()}
exitForward={() => props.exitForward?.()}
exitLeft={() => {
if (i === 0) {
props.exitLeft?.();
} else {
focus.setActiveChild(i - 1);
}
}}
exitRight={() => {
if (i === obList().length - 1) {
props.exitRight?.();
} else {
focus.setActiveChild(i + 1);
}
}}
interceptKeyDown={(evt) => {
if (evt.key === props.insertKey) {
insertNewOb(i + 1);
return true;
} else if (evt.key === "Home" && !evt.shiftKey) {
// TODO: Should move to beginning of input.
focus.setActiveChild(0);
} else if (evt.key === "End" && !evt.shiftKey) {
focus.setActiveChild(obList().length - 1);
}
return false;
}}
/>
</li>
)}
</Index>
{props.endDelimiter}
</ul>
<InlineListEditor items={obList()} setItems={setObList} {...listProps}>
{(ob, setOb, options) => (
<ObIdInput
ob={ob()}
setOb={setOb}
placeholder={props.placeholder}
idToLabel={(id) => liveModel().elaboratedModel()?.obGeneratorLabel(id)}
labelToId={(label) =>
liveModel().elaboratedModel()?.obGeneratorWithLabel(label)
}
completions={completions()}
{...options}
/>
)}
</InlineListEditor>
);
}
1 change: 1 addition & 0 deletions packages/ui-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export * from "./form";
export * from "./history_navigator";
export * from "./icon_button";
export * from "./inline_input";
export * from "./inline_list_editor";
export * from "./input_options";
export * from "./katex_display";
export * from "./model_file_icon";
Expand Down
30 changes: 30 additions & 0 deletions packages/ui-components/src/inline_list_editor.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.inlineList {
display: flex;
flex-direction: row;
align-items: center;
list-style: none;
padding: 0;

li {
display: flex;
flex-direction: row;
}
}

.defaultDelimiter,
.defaultSeparator {
color: var(--color-gray-800);
}

.defaultDelimiter {
transform: scale(1, 1.5);
}

.emptyListInput {
background: transparent;
border: none;
outline: none;
width: 0.5ex;
margin: 0;
padding: 0;
}
Loading
Loading