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"> <script lang="ts">
import type { Datum } from "./datum.svelte"; import type { Datum } from "./datum.svelte";
import { import {
@ -5,19 +11,12 @@
editor_state_from_datum, editor_state_from_datum,
type EditorState, type EditorState,
} from "./editor-state.svelte"; } from "./editor-state.svelte";
import { type FieldInfo } from "./field.svelte"; import { type Presentation } from "./presentation.svelte";
const BLUR_DEBOUNCE_MS = 100; const BLUR_DEBOUNCE_MS = 100;
type Props = { type Props = {
/** current_presentation: Presentation;
* 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;
on_blur?(ev: FocusEvent): unknown; on_blur?(ev: FocusEvent): unknown;
@ -36,25 +35,28 @@
*/ */
on_restore_focus?(): unknown; 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; value?: Datum;
}; };
let { let {
assignable_fields = [], current_presentation = $bindable(),
field_info = $bindable(),
on_blur, on_blur,
on_cancel_edit, on_cancel_edit,
on_change, on_change,
on_focus, on_focus,
on_restore_focus, on_restore_focus,
potential_presentations = [],
value = $bindable(), value = $bindable(),
}: Props = $props(); }: Props = $props();
let editor_state = $state<EditorState | undefined>(); 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 date_input_element = $state<HTMLInputElement | undefined>();
let text_input_element = $state<HTMLInputElement | undefined>(); let text_input_element = $state<HTMLInputElement | undefined>();
let blur_timeout = $state<number | undefined>(); let blur_timeout = $state<number | undefined>();
@ -67,13 +69,13 @@
export function focus() { export function focus() {
if ( if (
field_info.field.presentation.t === "Dropdown" || current_presentation.t === "Dropdown" ||
field_info.field.presentation.t === "Numeric" || current_presentation.t === "Numeric" ||
field_info.field.presentation.t === "Text" || current_presentation.t === "Text" ||
field_info.field.presentation.t === "Uuid" current_presentation.t === "Uuid"
) { ) {
text_input_element?.focus(); text_input_element?.focus();
} else if (field_info.field.presentation.t === "Timestamp") { } else if (current_presentation.t === "Timestamp") {
date_input_element?.focus(); date_input_element?.focus();
} }
} }
@ -100,23 +102,10 @@
console.warn("preconditions for handle_input() not met"); console.warn("preconditions for handle_input() not met");
return; return;
} }
value = datum_from_editor_state( value = datum_from_editor_state(editor_state, current_presentation);
editor_state,
field_info.field.presentation,
);
on_change?.(value); 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) { function handle_keydown(ev: KeyboardEvent) {
if (ev.key === "Enter") { if (ev.key === "Enter") {
on_restore_focus?.(); 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 = { const interactive_handlers = {
onblur: handle_blur, onblur: handle_blur,
onfocus: handle_focus, onfocus: handle_focus,
@ -141,35 +133,19 @@
onfocus={handle_focus} onfocus={handle_focus}
> >
{#if editor_state} {#if editor_state}
{#if assignable_fields?.length > 0} {#if potential_presentations?.length > 0}
<div class="datum-editor__type-selector"> <div class="datum-editor__type-selector">
<button <select
{...interactive_handlers} {...interactive_handlers}
bind:this={type_selector_menu_button_element} oninput={(ev) => {
class="datum-editor__type-selector-menu-button" current_presentation =
onclick={handle_type_selector_menu_button_click} potential_presentations[parseInt(ev.currentTarget.value)];
type="button" }}
> >
{field_info.field.presentation.t} {#each potential_presentations as presentation, i}
</button> <option value={`${i}`}>{presentation.t}</option>
<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} {/each}
</div> </select>
</div> </div>
{/if} {/if}
<button <button
@ -194,7 +170,7 @@
<i class="ti ti-cube"></i> <i class="ti ti-cube"></i>
{/if} {/if}
</button> </button>
{#if ["Dropdown", "Numeric", "Text", "Uuid"].includes(field_info.field.presentation.t)} {#if ["Dropdown", "Numeric", "Text", "Uuid"].includes(current_presentation.t)}
<input <input
{...interactive_handlers} {...interactive_handlers}
bind:this={text_input_element} bind:this={text_input_element}
@ -213,7 +189,7 @@
class="datum-editor__text-input" class="datum-editor__text-input"
type="text" type="text"
/> />
{:else if field_info.field.presentation.t === "Timestamp"} {:else if current_presentation.t === "Timestamp"}
<div class="datum-editor__timestamp-inputs"> <div class="datum-editor__timestamp-inputs">
<input <input
{...interactive_handlers} {...interactive_handlers}
@ -248,10 +224,10 @@
</div> </div>
{/if} {/if}
<div class="datum-editor__helpers" tabindex="-1"> <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. --> <!-- TODO: This is an awkward way to implement a keyboard-navigable listbox. -->
<menu class="datum-editor__dropdown-options"> <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 --> <!-- FIXME: validate or escape dropdown_option.color -->
<li <li
class={[ class={[

View file

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

View file

@ -718,7 +718,8 @@
<DatumEditor <DatumEditor
bind:this={datum_editor} bind:this={datum_editor}
bind:value={editor_value} 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_blur={() => try_queue_delta()}
on_cancel_edit={cancel_edit} on_cancel_edit={cancel_edit}
on_change={() => { on_change={() => {