phonograph/svelte/src/expression-selector.svelte

181 lines
4.6 KiB
Svelte
Raw Normal View History

2025-08-24 23:24:01 -07:00
<script lang="ts">
import plus_circle_icon from "../assets/heroicons/20/solid/plus-circle.svg?raw";
import { type PgExpressionAny, expression_icon } from "./expression.svelte";
type Props = {
on_change?(new_value: PgExpressionAny): void;
value?: PgExpressionAny;
};
let { on_change, value = $bindable() }: Props = $props();
let menu_button_element = $state<HTMLButtonElement | undefined>();
let popover_element = $state<HTMLDivElement | undefined>();
2025-10-01 22:36:19 -07:00
// Hacky workaround because as of September 2025 implicit anchor association
// is still pretty broken, at least in Firefox.
let anchor_name = $state(`--anchor-${Math.floor(Math.random() * 1000000)}`);
2025-08-24 23:24:01 -07:00
const expressions: ReadonlyArray<{
section_label: string;
expressions: ReadonlyArray<PgExpressionAny>;
}> = [
{
section_label: "Comparisons",
expressions: [
{
t: "Comparison",
c: {
t: "Infix",
c: {
lhs: { t: "Identifier", c: { parts_raw: [] } },
operator: "Eq",
rhs: { t: "Literal", c: { t: "Text", c: "" } },
},
},
},
{
t: "Comparison",
c: {
t: "Infix",
c: {
lhs: { t: "Identifier", c: { parts_raw: [] } },
operator: "Neq",
rhs: { t: "Literal", c: { t: "Text", c: "" } },
},
},
},
{
t: "Comparison",
c: {
t: "Infix",
c: {
lhs: { t: "Identifier", c: { parts_raw: [] } },
operator: "Lt",
rhs: { t: "Literal", c: { t: "Text", c: "" } },
},
},
},
{
t: "Comparison",
c: {
t: "Infix",
c: {
lhs: { t: "Identifier", c: { parts_raw: [] } },
operator: "Gt",
rhs: { t: "Literal", c: { t: "Text", c: "" } },
},
},
},
{
t: "Comparison",
c: {
t: "IsNull",
c: {
lhs: { t: "Identifier", c: { parts_raw: [] } },
},
},
},
{
t: "Comparison",
c: {
t: "IsNotNull",
c: {
lhs: { t: "Identifier", c: { parts_raw: [] } },
},
},
},
],
},
{
section_label: "Conjunctions",
expressions: [
{
t: "Comparison",
c: { t: "Infix", c: { operator: "And" } },
},
{
t: "Comparison",
c: { t: "Infix", c: { operator: "Or" } },
},
],
},
{
section_label: "Values",
expressions: [
{
t: "Identifier",
c: { parts_raw: [] },
},
{
t: "Literal",
c: { t: "Text", c: "" },
},
],
},
{
section_label: "Transformations",
expressions: [
{
t: "ToJson",
c: { entries: [] },
},
],
},
];
let iconography_current = $derived(value && expression_icon(value));
function handle_menu_button_click() {
popover_element?.togglePopover();
}
function handle_expression_button_click(expr: PgExpressionAny) {
value = expr;
popover_element?.hidePopover();
menu_button_element?.focus();
on_change?.(value);
}
</script>
<div class="expression-selector__container">
<button
aria-label={`Select expression type (current: ${iconography_current?.label ?? "None"})`}
bind:this={menu_button_element}
class="expression-selector__expression-button"
onclick={handle_menu_button_click}
2025-10-01 22:36:19 -07:00
style:anchor-name={anchor_name}
2025-08-24 23:24:01 -07:00
title={iconography_current?.label}
type="button"
>
{#if value}
{@html iconography_current?.html}
{:else}
{@html plus_circle_icon}
{/if}
</button>
<div
bind:this={popover_element}
class="expression-selector__popover"
popover="auto"
2025-10-01 22:36:19 -07:00
style:position-anchor={anchor_name}
2025-08-24 23:24:01 -07:00
>
{#each expressions as section}
<ul class="expression-selector__section">
{#each section.expressions as expr}
{@const iconography = expression_icon(expr)}
<li class="expression-selector__li">
<button
class="expression-selector__expression-button"
onclick={() => handle_expression_button_click(expr)}
title={iconography.label}
type="button"
>
{@html iconography.html}
</button>
</li>
{/each}
</ul>
{/each}
</div>
</div>