add Numeric presentation to expression datum editor

This commit is contained in:
Brent Schroeter 2026-01-14 19:37:27 +00:00
parent fa782d2ed6
commit 292ebd470f
4 changed files with 52 additions and 83 deletions

View file

@ -1,3 +1,9 @@
<!--
@component
Provides user interface for specifying arbitrary scalar `Datum` values, for
example within the `<TableViewer />` or `<ExpressionEditor />`.
-->
<script lang="ts">
import type { Datum } from "./datum.svelte";
import {
@ -5,19 +11,12 @@
editor_state_from_datum,
type EditorState,
} from "./editor-state.svelte";
import { type FieldInfo } from "./field.svelte";
import { type Presentation } from "./presentation.svelte";
const BLUR_DEBOUNCE_MS = 100;
type Props = {
/**
* For use cases in which the user may select between multiple datum types,
* such as when embedded in an expression editor, this array represents the
* permissible set of field parameters.
*/
assignable_fields?: ReadonlyArray<FieldInfo>;
field_info: FieldInfo;
current_presentation: Presentation;
on_blur?(ev: FocusEvent): unknown;
@ -36,25 +35,28 @@
*/
on_restore_focus?(): unknown;
/**
* For use cases in which the user may select between multiple datum types,
* such as when embedded in an expression editor, this array represents the
* permissible set of available presentations.
*/
potential_presentations?: Presentation[];
value?: Datum;
};
let {
assignable_fields = [],
field_info = $bindable(),
current_presentation = $bindable(),
on_blur,
on_cancel_edit,
on_change,
on_focus,
on_restore_focus,
potential_presentations = [],
value = $bindable(),
}: Props = $props();
let editor_state = $state<EditorState | undefined>();
let type_selector_menu_button_element = $state<
HTMLButtonElement | undefined
>();
let type_selector_popover_element = $state<HTMLDivElement | undefined>();
let date_input_element = $state<HTMLInputElement | undefined>();
let text_input_element = $state<HTMLInputElement | undefined>();
let blur_timeout = $state<number | undefined>();
@ -67,13 +69,13 @@
export function focus() {
if (
field_info.field.presentation.t === "Dropdown" ||
field_info.field.presentation.t === "Numeric" ||
field_info.field.presentation.t === "Text" ||
field_info.field.presentation.t === "Uuid"
current_presentation.t === "Dropdown" ||
current_presentation.t === "Numeric" ||
current_presentation.t === "Text" ||
current_presentation.t === "Uuid"
) {
text_input_element?.focus();
} else if (field_info.field.presentation.t === "Timestamp") {
} else if (current_presentation.t === "Timestamp") {
date_input_element?.focus();
}
}
@ -100,23 +102,10 @@
console.warn("preconditions for handle_input() not met");
return;
}
value = datum_from_editor_state(
editor_state,
field_info.field.presentation,
);
value = datum_from_editor_state(editor_state, current_presentation);
on_change?.(value);
}
function handle_type_selector_menu_button_click() {
type_selector_popover_element?.togglePopover();
}
function handle_type_selector_field_button_click(value: FieldInfo) {
field_info = value;
type_selector_popover_element?.hidePopover();
type_selector_menu_button_element?.focus();
}
function handle_keydown(ev: KeyboardEvent) {
if (ev.key === "Enter") {
on_restore_focus?.();
@ -127,6 +116,9 @@
}
}
// These handlers must be passed to interactive children to correctly handle
// focus/blur behavior, particularly when the `<DatumEditor />` is a child of
// `<TableViewer />`.
const interactive_handlers = {
onblur: handle_blur,
onfocus: handle_focus,
@ -141,35 +133,19 @@
onfocus={handle_focus}
>
{#if editor_state}
{#if assignable_fields?.length > 0}
{#if potential_presentations?.length > 0}
<div class="datum-editor__type-selector">
<button
<select
{...interactive_handlers}
bind:this={type_selector_menu_button_element}
class="datum-editor__type-selector-menu-button"
onclick={handle_type_selector_menu_button_click}
type="button"
oninput={(ev) => {
current_presentation =
potential_presentations[parseInt(ev.currentTarget.value)];
}}
>
{field_info.field.presentation.t}
</button>
<div
bind:this={type_selector_popover_element}
class="datum-editor__type-selector-popover"
onblur={handle_blur}
onfocus={handle_focus}
popover="auto"
>
{#each assignable_fields as assignable_field_info}
<button
{...interactive_handlers}
onclick={() =>
handle_type_selector_field_button_click(assignable_field_info)}
type="button"
>
{assignable_field_info.field.presentation.t}
</button>
{#each potential_presentations as presentation, i}
<option value={`${i}`}>{presentation.t}</option>
{/each}
</div>
</select>
</div>
{/if}
<button
@ -194,7 +170,7 @@
<i class="ti ti-cube"></i>
{/if}
</button>
{#if ["Dropdown", "Numeric", "Text", "Uuid"].includes(field_info.field.presentation.t)}
{#if ["Dropdown", "Numeric", "Text", "Uuid"].includes(current_presentation.t)}
<input
{...interactive_handlers}
bind:this={text_input_element}
@ -213,7 +189,7 @@
class="datum-editor__text-input"
type="text"
/>
{:else if field_info.field.presentation.t === "Timestamp"}
{:else if current_presentation.t === "Timestamp"}
<div class="datum-editor__timestamp-inputs">
<input
{...interactive_handlers}
@ -248,10 +224,10 @@
</div>
{/if}
<div class="datum-editor__helpers" tabindex="-1">
{#if field_info.field.presentation.t === "Dropdown"}
{#if current_presentation.t === "Dropdown"}
<!-- TODO: This is an awkward way to implement a keyboard-navigable listbox. -->
<menu class="datum-editor__dropdown-options">
{#each field_info.field.presentation.c.options as dropdown_option}
{#each current_presentation.c.options as dropdown_option}
<!-- FIXME: validate or escape dropdown_option.color -->
<li
class={[

View file

@ -14,29 +14,15 @@
import ExpressionSelector from "./expression-selector.svelte";
import { type PgExpressionAny } from "./expression.svelte";
import ExpressionEditor from "./expression-editor.webc.svelte";
import { type FieldInfo } from "./field.svelte";
import { RFC_3339_S, type Presentation } from "./presentation.svelte";
import type { Datum } from "./datum.svelte";
const ASSIGNABLE_PRESENTATIONS: Presentation[] = [
const POTENTIAL_PRESENTATIONS: Presentation[] = [
{ t: "Numeric", c: {} },
{ t: "Text", c: { input_mode: { t: "MultiLine", c: {} } } },
{ t: "Timestamp", c: { format: RFC_3339_S } },
{ t: "Uuid", c: {} },
];
const ASSIGNABLE_FIELDS: FieldInfo[] = ASSIGNABLE_PRESENTATIONS.map(
(presentation) => ({
field: {
id: "",
name: "",
ordinality: -1,
presentation,
table_label: "",
table_width_px: -1,
},
not_null: true,
has_default: false,
}),
);
type Props = {
identifier_hints?: string[];
@ -47,7 +33,7 @@
// Dynamic state to bind to datum editor.
let editor_value = $state<Datum | undefined>();
let editor_field_info = $state<FieldInfo>(ASSIGNABLE_FIELDS[0]);
let editor_presentation = $state<Presentation>(POTENTIAL_PRESENTATIONS[0]);
$effect(() => {
editor_value = value?.t === "Literal" ? value.c : undefined;
@ -93,9 +79,9 @@
</select>
{:else if value.t === "Literal"}
<DatumEditor
bind:field_info={editor_field_info}
bind:current_presentation={editor_presentation}
bind:value={editor_value}
assignable_fields={ASSIGNABLE_FIELDS}
potential_presentations={POTENTIAL_PRESENTATIONS}
on_change={handle_editor_change}
/>
{/if}

View file

@ -1,3 +1,9 @@
<!--
@component
Dropdown menu with grid of buttons for quickly selecting a Postgres expression
type. Used by `<ExpressionEditor />`.
-->
<script lang="ts">
import { type PgExpressionAny, expression_icon } from "./expression.svelte";

View file

@ -718,7 +718,8 @@
<DatumEditor
bind:this={datum_editor}
bind:value={editor_value}
field_info={lazy_data.fields[selections[0].coords.field_idx]}
current_presentation={lazy_data.fields[selections[0].coords.field_idx]
.field.presentation}
on_blur={() => try_queue_delta()}
on_cancel_edit={cancel_edit}
on_change={() => {