move error calc, add preview syntax

This commit is contained in:
faiztyanirh 2026-03-10 13:04:27 +07:00
parent 05ee4617d5
commit 45c8d6969a
2 changed files with 228 additions and 325 deletions

View File

@ -18,9 +18,6 @@
let cursorIndex = $state(0);
let showLiteralPopover = $state(false);
let literalPopoverType = $state(('string'));
$inspect(tokens)
// let expression = $state('');
// let cursorPosition = $state(0);
function uid() {
return Math.random().toString(36).slice(2, 9);
@ -106,92 +103,6 @@ $inspect(tokens)
props.calFormState.form[key] = [];
props.calFormState.validateField?.(key, [], false);
}
// function unselectAll(key) {
// props.calFormState.form[key] = [];
// props.calFormState.validateField?.(key, [], false);
// }
// function getErrorStatus(formulaCode = '') {
// const selected = props.calFormState.form.FormulaInput;
// if (!Array.isArray(selected)) return [];
// return selected.map((item) => ({
// value: item.value,
// done: new RegExp(`\\b${item.value}\\b`, 'i').test(formulaCode)
// }));
// }
// function addToExpression(text) {
// const before = expression.slice(0, cursorPosition);
// const after = expression.slice(cursorPosition);
// expression = before + text + after;
// cursorPosition += text.length;
// }
// function addOperator(op) {
// addToExpression(op);
// props.calFormState.form.FormulaCode = expression;
// props.calFormState.validateField?.('FormulaCode', expression, false);
// }
// function addValue(val) {
// addToExpression(val);
// props.calFormState.form.FormulaCode = expression;
// props.calFormState.validateField?.('FormulaCode', expression, false);
// }
// function handleInput(e) {
// expression = e.target.value;
// cursorPosition = e.target.selectionStart;
// formState.form.FormulaCode = expression;
// formState.validateField?.('FormulaCode', expression, false);
// }
// function handleClick(e) {
// cursorPosition = e.target.selectionStart;
// }
// function handleContainerClick(e) {
// const rect = e.currentTarget.getBoundingClientRect();
// const text = expression;
// const charWidth = 8.5;
// const padding = 12;
// const clickX = e.clientX - rect.left - padding;
// let newPos = Math.floor(clickX / charWidth);
// newPos = Math.max(0, Math.min(newPos, text.length));
// cursorPosition = newPos;
// }
// function moveCursorLeft() {
// if (cursorPosition > 0) {
// cursorPosition -= 1;
// }
// }
// function moveCursorRight() {
// if (cursorPosition < expression.length) {
// cursorPosition += 1;
// }
// }
// function deleteChar() {
// if (cursorPosition > 0) {
// const before = expression.slice(0, cursorPosition - 1);
// const after = expression.slice(cursorPosition);
// expression = before + after;
// cursorPosition -= 1;
// props.calFormState.form.FormulaCode = expression;
// props.calFormState.validateField?.('FormulaCode', expression, false);
// }
// }
// function clearExpression() {
// expression = '';
// cursorPosition = 0;
// props.calFormState.form.FormulaCode = expression;
// props.calFormState.validateField?.('FormulaCode', expression, false);
// }
</script>
<div class="flex flex-col gap-4 w-full">

View File

@ -126,6 +126,20 @@
{#if required}
<span class="text-destructive text-xl leading-none h-3.5">*</span>
{/if}
{#if key === 'FormulaCode' && formState.form.FormulaInput?.length}
{@const inputStatus = onGetErrorStatus?.()}
<div class="flex items-center gap-2 text-sm text-destructive">
<span>Must included :</span>
<div class="flex gap-1 flex-wrap">
{#each inputStatus as item (item.value)}
<Badge class="px-1 text-[10px]" variant={item.done ? 'default' : 'destructive'}>
{item.value}
</Badge>
{/each}
</div>
</div>
{/if}
</div>
<div class="relative flex flex-col items-center w-full">
@ -487,52 +501,49 @@
<MoveLeftIcon class="w-4 h-4" />
</Button>
<div
class="relative flex-1 min-h-[2rem] rounded-md border bg-background px-3 py-2 font-mono text-sm cursor-text focus-within:ring-1 focus-within:ring-ring"
role="textbox"
tabindex="0"
onkeydown={(e) => {
if (e.key === 'Backspace') { e.preventDefault(); onDeleteChar(); }
if (e.key === 'ArrowLeft') onMoveCursorLeft();
if (e.key === 'ArrowRight') onMoveCursorRight();
}}
>
{#if tokens.length === 0}
<span class="text-muted-foreground text-xs italic">Click buttons below to build formula...</span>
{:else}
<!-- Split tokens into lines -->
{@const lines = (() => {
const result = [[]];
tokens.forEach((tok, idx) => {
if (tok.type === 'newline') result.push([]);
else result[result.length - 1].push({ tok, idx });
});
return result;
})()}
{#each lines as line, lineIdx}
<div class="flex flex-wrap items-center gap-1 min-h-[28px] {lineIdx > 0 ? 'mt-1 pt-1 border-t border-dashed border-border' : ''}">
{#each line as { tok, idx }}
{#if cursorIndex === idx}
<span class="animate-cursor inline-block h-5 w-0.5 bg-primary rounded"></span>
{/if}
<button
type="button"
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold border transition-colors "
onclick={() => onSetCursor(idx + 1)}
title="Click to move cursor here"
>
{tok.value}
</button>
class="relative flex-1 min-h-[2rem] rounded-md border bg-background px-3 py-2 font-mono text-sm cursor-text focus-within:ring-1 focus-within:ring-ring"
role="textbox"
tabindex="0"
onkeydown={(e) => {
if (e.key === 'Backspace') { e.preventDefault(); onDeleteChar(); }
if (e.key === 'ArrowLeft') onMoveCursorLeft();
if (e.key === 'ArrowRight') onMoveCursorRight();
}}
>
{#if tokens.length === 0}
<span class="text-muted-foreground text-xs italic">Select test then click buttons below to build formula</span>
{:else}
{@const lines = (() => {
const result = [[]];
tokens.forEach((tok, idx) => {
if (tok.type === 'newline') result.push([]);
else result[result.length - 1].push({ tok, idx });
});
return result;
})()}
{#each lines as line, lineIdx}
<div class="flex flex-wrap items-center gap-1 min-h-[28px] {lineIdx > 0 ? 'mt-1 pt-1 border-t border-dashed border-border' : ''}">
{#each line as { tok, idx }}
{#if cursorIndex === idx}
<span class="animate-cursor inline-block h-5 w-0.5 bg-primary rounded"></span>
{/if}
<button
type="button"
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold border transition-colors"
onclick={() => onSetCursor(idx + 1)}
>
{tok.value}
</button>
{/each}
{#if line.length > 0 && cursorIndex === line[line.length - 1].idx + 1}
<span class="animate-cursor inline-block h-5 w-0.5 bg-primary rounded"></span>
{:else if line.length === 0 && lineIdx === lines.length - 1}
<span class="animate-cursor inline-block h-5 w-0.5 bg-primary rounded"></span>
{/if}
</div>
{/each}
{#if line.length > 0 && cursorIndex === line[line.length - 1].idx + 1}
<span class="animate-cursor inline-block h-5 w-0.5 bg-primary rounded"></span>
{/if}
</div>
{/each}
{#if cursorIndex === tokens.length && (tokens.length === 0 || tokens[tokens.length - 1].type === 'newline')}
<span class="animate-cursor inline-block h-5 w-0.5 bg-primary rounded mt-1"></span>
{/if}
{/if}
</div>
{/if}
</div>
<Button type="button" variant="outline" size="icon" onclick={onMoveCursorRight}>
<MoveRightIcon class="w-4 h-4" />
</Button>
@ -545,187 +556,184 @@
<Button type="button" variant="outline" size="icon" onclick={onAddNewline} title="New line">
<CornerDownLeftIcon class="w-4 h-4" />
</Button>
</div>
</div>
{#if formState.form.FormulaInput?.length > 0}
<div class="flex flex-col gap-4">
<!-- Selected Tests -->
<div class="flex flex-col gap-2">
<span class="text-sm font-medium">Selected Tests</span>
<div class="flex flex-wrap gap-2">
{#each formState.form.FormulaInput as item (item)}
<Button
type="button"
variant="outline"
class="h-auto w-auto p-2"
onclick={() => onAddValue?.(item.value)}
>
{item.value}
</Button>
{/each}
</div>
</div>
<!-- Custom Literal Values -->
<div class="flex flex-col gap-2">
<span class="text-sm font-medium">Custom Values</span>
<div class="flex flex-wrap gap-2">
<!-- String literal popover -->
<Popover.Root bind:open={stringPopoverOpen}>
<Popover.Trigger>
{#snippet child({ props: triggerProps })}
<Button
{...triggerProps}
type="button"
variant="outline"
class="h-auto w-auto p-2"
>
"abc"
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-64" side="bottom" align="start">
<div>
<div class="flex flex-col gap-3">
<p class="text-sm font-semibold">Enter Text Value</p>
<Input
type="text"
placeholder='e.g. F, POS, NEG'
bind:value={stringLiteralInput}
onkeydown={(e) => {
if (e.key === 'Enter' && stringLiteralInput.trim()) {
onAddLiteral(`"${stringLiteralInput.trim()}"`);
stringLiteralInput = '';
}
}}
/>
<div class="flex justify-end gap-2">
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<Label class="uppercase tracking-wide">Selected Tests</Label>
<div class="flex flex-wrap gap-2">
{#each formState.form.FormulaInput as item (item)}
<Button
type="button"
size="sm"
disabled={!stringLiteralInput.trim()}
onclick={() => {
onAddLiteral(`"${stringLiteralInput.trim()}"`);
stringLiteralInput = '';
}}
type="button"
variant="outline"
class="h-auto w-auto p-2"
onclick={() => onAddValue?.(item.value)}
>
Add
{item.value}
</Button>
</div>
{/each}
</div>
</div>
<!-- Custom Literal Values -->
<div class="flex flex-col gap-2">
<Label class="uppercase tracking-wide">Custom Values</Label>
<div class="flex flex-wrap gap-2">
<Popover.Root bind:open={stringPopoverOpen}>
<Popover.Trigger>
{#snippet child({ props: triggerProps })}
<Button
{...triggerProps}
type="button"
variant="outline"
class="h-auto w-auto p-2"
>
"abc"
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-64" side="bottom" align="start">
<div>
<div class="flex flex-col gap-3">
<p class="text-sm font-semibold">Enter Text Value</p>
<Input
type="text"
placeholder='e.g. F, POS, NEG'
bind:value={stringLiteralInput}
onkeydown={(e) => {
if (e.key === 'Enter' && stringLiteralInput.trim()) {
onAddLiteral(`"${stringLiteralInput.trim()}"`);
stringLiteralInput = '';
}
}}
/>
<div class="flex justify-end gap-2">
<Button
type="button"
size="sm"
disabled={!stringLiteralInput.trim()}
onclick={() => {
onAddLiteral(`"${stringLiteralInput.trim()}"`);
stringLiteralInput = '';
}}
>
Add
</Button>
</div>
</div>
</div>
</Popover.Content>
</Popover.Root>
<Popover.Root bind:open={numberPopoverOpen}>
<Popover.Trigger>
{#snippet child({ props: triggerProps })}
<Button
{...triggerProps}
type="button"
variant="outline"
class="h-auto w-auto p-2"
>
123
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-64" side="bottom" align="start">
<div>
<div class="flex flex-col gap-3">
<p class="text-sm font-semibold">Enter Number Value</p>
<Input
type="number"
placeholder='e.g. 142'
bind:value={numberLiteralInput}
onkeydown={(e) => {
if (e.key === 'Enter' && numberLiteralInput != null && !isNaN(numberLiteralInput)) {
onAddLiteral(String(numberLiteralInput));
numberLiteralInput = null;
}
}}
/>
<div class="flex justify-end gap-2">
<Button
type="button"
size="sm"
disabled={numberLiteralInput == null || isNaN(numberLiteralInput)}
onclick={() => {
onAddLiteral(String(numberLiteralInput));
numberLiteralInput = null;
numberPopoverOpen = false;
}}
>
Add
</Button>
</div>
</div>
</div>
</Popover.Content>
</Popover.Root>
</div>
</div>
<div class="flex flex-col gap-2">
<Label class="uppercase tracking-wide">Logical Operators</Label>
<div class="flex flex-wrap gap-2">
{#each logicalop as op}
<Button
type="button"
variant="outline"
class="h-auto w-auto p-2"
onclick={() => onAddOperator?.(op)}
>
{op}
</Button>
{/each}
</div>
</div>
<!-- Comparison Operators -->
<div class="flex flex-col gap-2">
<Label class="uppercase tracking-wide">Comparison Operators</Label>
<div class="flex flex-wrap gap-2">
{#each comparisonop as op}
<Button
type="button"
variant="outline"
size="icon"
onclick={() => onAddOperator?.(op)}
>
{op}
</Button>
{/each}
</div>
</div>
<!-- Math Operators -->
<div class="flex flex-col gap-2">
<Label class="uppercase tracking-wide">Math Operators</Label>
<div class="flex flex-wrap gap-2">
{#each operators as op}
<Button
type="button"
variant="outline"
size="icon"
onclick={() => onAddOperator?.(op)}
>
{op}
</Button>
{/each}
</div>
</div>
{#if tokens.length > 0}
<div class="flex flex-col gap-2">
<Label class="uppercase tracking-wide">Preview</Label>
<div class="border-2 border-dashed border-muted-foreground/30 rounded-lg">
<pre class="font-mono text-sm bg-muted/50 p-2 rounded">{expressionString}</pre>
</div>
</div>
</Popover.Content>
</Popover.Root>
<!-- Number literal popover -->
<Popover.Root bind:open={numberPopoverOpen}>
<Popover.Trigger>
{#snippet child({ props: triggerProps })}
<Button
{...triggerProps}
type="button"
variant="outline"
class="h-auto w-auto p-2"
>
123
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-64" side="bottom" align="start">
<div>
<div class="flex flex-col gap-3">
<p class="text-sm font-semibold">Enter Number Value</p>
<Input
type="number"
placeholder='e.g. 142'
bind:value={numberLiteralInput}
onkeydown={(e) => {
if (e.key === 'Enter' && numberLiteralInput != null && !isNaN(numberLiteralInput)) {
onAddLiteral(String(numberLiteralInput));
numberLiteralInput = null;
}
}}
/>
<div class="flex justify-end gap-2">
<Button
type="button"
size="sm"
disabled={numberLiteralInput == null || isNaN(numberLiteralInput)}
onclick={() => {
onAddLiteral(String(numberLiteralInput));
numberLiteralInput = null;
numberPopoverOpen = false;
}}
>
Add
</Button>
</div>
</div>
</div>
</Popover.Content>
</Popover.Root>
</div>
</div>
<!-- Logical Operators (Keywords) -->
<div class="flex flex-col gap-2">
<span class="text-sm font-medium">Logical Operators</span>
<div class="flex flex-wrap gap-2">
{#each logicalop as op}
<Button
type="button"
variant="outline"
class="h-auto w-auto p-2"
onclick={() => onAddOperator?.(op)}
>
{op}
</Button>
{/each}
</div>
</div>
<!-- Comparison Operators -->
<div class="flex flex-col gap-2">
<span class="text-sm font-medium">Comparison Operators</span>
<div class="flex flex-wrap gap-2">
{#each comparisonop as op}
<Button
type="button"
variant="outline"
size="icon"
onclick={() => onAddOperator?.(op)}
>
{op}
</Button>
{/each}
</div>
</div>
<!-- Math Operators -->
<div class="flex flex-col gap-2">
<span class="text-sm font-medium">Math Operators</span>
<div class="flex flex-wrap gap-2">
{#each operators as op}
<Button
type="button"
variant="outline"
size="icon"
onclick={() => onAddOperator?.(op)}
>
{op}
</Button>
{/each}
</div>
</div>
<!-- Preview -->
{#if tokens.length > 0}
<div class="section">
<label class="section-label">Preview</label>
<pre class="expression-preview">{expressionString}</pre>
</div>
{/if}
</div>
{/if}
{/if}
</div>
{/if}
</div>
{:else if type === 'members'}
{@const filteredOptions = getFilteredOptions(key)}
@ -814,22 +822,6 @@
{errorMessage}
</div>
{/if}
{#if key === 'FormulaCode' && formState.form.FormulaInput?.length}
{@const inputStatus = onGetErrorStatus?.()}
<div class="flex items-center gap-2 text-sm text-destructive">
<span>Must included :</span>
<div class="flex gap-1 flex-wrap">
{#each inputStatus as item (item.value)}
<Badge variant={item.done ? 'default' : 'destructive'}>
{item.value}
</Badge>
{/each}
</div>
</div>
{/if}
</div>
</div>
</div>