feat(grid): color quick condition bar keywords, fields and values

This commit is contained in:
Allen authored and GitHub committed 2026-08-26 13:52:41 +08:00
1 parent 4c28492f2c
commit b37a1f8538
3 files changed
+256 -2

No files matched your search

@@ -2,6 +2,7 @@
import { computed, nextTick, onMounted, onUnmounted, ref, useId, watch, type CSSProperties } from "vue";
import { ChevronDown, X } from "@lucide/vue";
import { completeDataGridConditionQuote, useDataGridConditionEditor, type DataGridConditionColumnOption, type DataGridConditionSuggestion, type DataGridConditionSuggestionProvider } from "@/composables/useDataGridConditionEditor";
import { tokenizeDataGridCondition, type DataGridConditionTokenType } from "@/lib/dataGrid/dataGridConditionHighlight";
import { getDataGridConditionSuggestionPosition, getDataGridConditionSuggestionPreferredWidth } from "@/lib/dataGrid/dataGridConditionSuggestionPosition";
import type { DataGridConditionHistoryKind, DataGridConditionHistoryScope } from "@/lib/dataGrid/dataGridConditionHistory";
@@ -73,6 +74,25 @@ const editor = useDataGridConditionEditor({
const activeEditor = computed(() => overlayRef.value ?? inputRef.value);
const hasValue = computed(() => modelValue.value.trim().length > 0);
// Syntax highlight tokens rendered in a layer below the (transparent-text)
// textarea, so keywords / fields / values are colored without losing caret
// and selection behavior.
const highlightTokens = computed(() => tokenizeDataGridCondition(modelValue.value));
const highlightScrollLeft = ref(0);
const highlightScrollTop = ref(0);
const collapsedHighlightStyle = computed<CSSProperties>(() => ({ transform: `translateX(${-highlightScrollLeft.value}px)` }));
const expandedHighlightStyle = computed<CSSProperties>(() => ({ transform: `translate(${-highlightScrollLeft.value}px, ${-highlightScrollTop.value}px)` }));
function highlightTokenClass(type: DataGridConditionTokenType): string | undefined {
if (type === "plain") return undefined;
return `data-grid-condition-token--${type}`;
}
function onEditorScroll(event: Event) {
const target = event.currentTarget as HTMLTextAreaElement;
highlightScrollLeft.value = target.scrollLeft;
highlightScrollTop.value = target.scrollTop;
}
const emptyHistoryText = computed(() => (modelValue.value.trim() ? props.historyNoMatchesText : props.historyEmptyText));
const activeSuggestionId = computed(() => (editor.highlightedIndex.value >= 0 ? `${suggestionListId}-${editor.highlightedIndex.value}` : undefined));
const suggestionPreferredWidth = computed(() => getDataGridConditionSuggestionPreferredWidth(editor.suggestions.value));
@@ -501,6 +521,9 @@ defineExpose({ focus, dismiss: editor.dismiss, rememberHistory: editor.rememberH
{{ props.kind === "where" ? "WHERE" : "ORDER BY" }}
</span>
<div class="relative h-6 min-w-0 flex-1 overflow-hidden">
<div v-if="!composing" aria-hidden="true" class="data-grid-condition-highlight pointer-events-none absolute left-0 top-0" :style="collapsedHighlightStyle">
<span v-for="(token, tokenIndex) in highlightTokens" :key="tokenIndex" :class="highlightTokenClass(token.type)">{{ token.text }}</span>
</div>
<textarea
ref="inputRef"
v-model="modelValue"
@@ -518,7 +541,7 @@ defineExpose({ focus, dismiss: editor.dismiss, rememberHistory: editor.rememberH
:aria-controls="suggestionListId"
:aria-activedescendant="activeSuggestionId"
class="data-grid-topbar-condition-input absolute inset-x-0 top-0 h-6 min-w-0 resize-none bg-transparent outline-none"
:class="[props.kind === 'where' ? 'data-grid-topbar-condition-input--where' : 'data-grid-topbar-condition-input--order', { 'data-grid-topbar-condition-input--compact': props.compact }]"
:class="[props.kind === 'where' ? 'data-grid-topbar-condition-input--where' : 'data-grid-topbar-condition-input--order', { 'data-grid-topbar-condition-input--compact': props.compact, 'data-grid-condition-input--transparent-text': !composing }]"
style="height: 24px"
@focus="onFocus"
@blur="scheduleCollapse"
@@ -528,6 +551,7 @@ defineExpose({ focus, dismiss: editor.dismiss, rememberHistory: editor.rememberH
@compositionstart="onCompositionStart"
@compositionend="onCompositionEnd"
@input="onInput"
@scroll="onEditorScroll"
@contextmenu.stop
@keydown="onKeydown"
/>
@@ -542,6 +566,9 @@ defineExpose({ focus, dismiss: editor.dismiss, rememberHistory: editor.rememberH
<Teleport to="body">
<div v-if="expanded" class="data-grid-topbar-condition-pane--expanded fixed z-[80] flex min-w-0 items-start gap-1" :style="overlayStyle">
<div v-if="!composing" aria-hidden="true" class="data-grid-condition-highlight data-grid-condition-highlight--expanded pointer-events-none absolute" :style="expandedHighlightStyle">
<span v-for="(token, tokenIndex) in highlightTokens" :key="tokenIndex" :class="highlightTokenClass(token.type)">{{ token.text }}</span>
</div>
<textarea
ref="overlayRef"
v-model="modelValue"
@@ -554,7 +581,7 @@ defineExpose({ focus, dismiss: editor.dismiss, rememberHistory: editor.rememberH
:aria-controls="suggestionListId"
:aria-activedescendant="activeSuggestionId"
class="data-grid-topbar-condition-input data-grid-topbar-condition-input--expanded absolute resize-none outline-none"
:class="[props.kind === 'where' ? 'data-grid-topbar-condition-input--where' : 'data-grid-topbar-condition-input--order', { 'data-grid-topbar-condition-input--compact': props.compact }]"
:class="[props.kind === 'where' ? 'data-grid-topbar-condition-input--where' : 'data-grid-topbar-condition-input--order', { 'data-grid-topbar-condition-input--compact': props.compact, 'data-grid-condition-input--transparent-text': !composing }]"
@blur="scheduleCollapse"
@focus="onFocus"
@click="onSelectionChange"
@@ -563,6 +590,7 @@ defineExpose({ focus, dismiss: editor.dismiss, rememberHistory: editor.rememberH
@compositionstart="onCompositionStart"
@compositionend="onCompositionEnd"
@input="onInput"
@scroll="onEditorScroll"
@contextmenu.stop
@keydown="onKeydown"
/>
@@ -804,4 +832,67 @@ defineExpose({ focus, dismiss: editor.dismiss, rememberHistory: editor.rememberH
:global(.dark .data-grid-topbar-condition-input--order.data-grid-topbar-condition-input--compact::placeholder) {
color: rgb(253 186 116 / 70%);
}
/* Syntax highlight layer: sits below the transparent-text textarea and colors
condition keywords / fields / values while the real caret and selection keep
working in the textarea above it. */
.data-grid-condition-highlight {
box-sizing: border-box;
width: max-content;
min-width: 100%;
height: 24px;
padding: 0 0.125rem;
font-family: var(--data-grid-condition-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
font-size: 0.875rem;
font-variant-ligatures: none;
font-feature-settings:
"liga" 0,
"calt" 0;
line-height: 1.5rem;
white-space: pre;
}
.data-grid-condition-highlight--expanded {
top: var(--data-grid-condition-input-top);
bottom: 0.125rem;
left: 0.5rem;
width: calc(100% - 1rem + var(--data-grid-expanded-scrollbar-offset));
min-width: 0;
height: auto;
margin-right: calc(-1 * var(--data-grid-expanded-scrollbar-offset));
padding: 0 calc(var(--data-grid-condition-suffix-width) + 0.5rem) 0.0625rem 0.125rem;
text-indent: var(--data-grid-condition-prefix-indent);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.data-grid-condition-input--transparent-text {
color: transparent !important;
caret-color: var(--foreground);
}
.data-grid-condition-token--keyword {
color: rgb(37 99 235);
font-weight: 600;
}
.data-grid-condition-token--field {
color: rgb(225 29 72);
}
.data-grid-condition-token--value {
color: rgb(13 148 136);
}
:global(.dark .data-grid-condition-token--keyword) {
color: rgb(96 165 250);
}
:global(.dark .data-grid-condition-token--field) {
color: rgb(251 113 133);
}
:global(.dark .data-grid-condition-token--value) {
color: rgb(45 212 191);
}
</style>