mirror of
https://github.com/faiztyanirh/clqms-shadcn-v1.git
synced 2026-04-22 09:35:34 +07:00
move error calc, add preview syntax
This commit is contained in:
parent
05ee4617d5
commit
45c8d6969a
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user