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>
|