From bb72a58e54bb2d40a49925e7b93d8307e04419cf Mon Sep 17 00:00:00 2001 From: zipg Date: Wed, 1 Jul 2026 17:47:35 +0800 Subject: [PATCH] feat(grid): expandable WHERE/ORDER BY condition inputs for long expressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: 优化 WHERE 和 ORDER BY 长条件输入体验 * Fix: 对齐展开条件输入首行控件 --------- Co-authored-by: staff --- apps/desktop/src/components/grid/DataGrid.vue | 1644 ++++++++++++----- 1 file changed, 1192 insertions(+), 452 deletions(-) diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 3e89cb24f..1624d5826 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -483,18 +483,40 @@ const suggestionLeft = ref(0); const whereSuggestions = ref([]); const whereSuggestionIndex = ref(-1); const whereHistoryMenuOpen = ref(false); -const whereFilterInputRef = ref(); +const whereFilterInputRef = ref(); +const whereFilterOverlayRef = ref(); +const whereConditionPaneRef = ref(); +const whereConditionControlRef = ref(); const whereMeasureRef = ref(); const whereSuggestionLeft = ref(0); const whereSuggestionPosition = ref({ left: 0, top: 0 }); +const whereConditionRendered = ref(false); +const whereConditionExpanded = ref(false); +const whereConditionHeight = ref(28); +const whereConditionRect = ref({ left: 0, top: 0, width: 0, height: 28 }); +const whereConditionScrollTop = ref(0); +const whereConditionImmediateHeight = ref(false); +let whereConditionCollapseTimer: ReturnType | undefined; +let suppressNextWhereWatchResize = false; const orderBySuggestions = ref([]); const orderBySuggestionIndex = ref(-1); const orderByHistoryMenuOpen = ref(false); -const orderByInputRef = ref(); +const orderByInputRef = ref(); +const orderByOverlayRef = ref(); +const orderByConditionPaneRef = ref(); +const orderByConditionControlRef = ref(); const orderByMeasureRef = ref(); const orderBySuggestionLeft = ref(0); const orderBySuggestionPosition = ref({ left: 0, top: 0 }); +const orderByConditionRendered = ref(false); +const orderByConditionExpanded = ref(false); +const orderByConditionHeight = ref(28); +const orderByConditionRect = ref({ left: 0, top: 0, width: 0, height: 28 }); +const orderByConditionScrollTop = ref(0); +const orderByConditionImmediateHeight = ref(false); +let orderByConditionCollapseTimer: ReturnType | undefined; +let suppressNextOrderByWatchResize = false; const orderByInput = ref(props.initialOrderByInput ?? ""); const hasOrderByInput = computed(() => orderByInput.value.trim().length > 0); @@ -534,6 +556,437 @@ const orderBySuggestionStyle = computed(() => ({ const showOrderBySuggestionDropdown = computed(() => orderBySuggestions.value.length > 0 || orderByHistoryMenuOpen.value); const orderByHistoryEmptyText = computed(() => (orderByInput.value.trim() ? t("grid.conditionHistoryNoMatches") : t("grid.conditionHistoryEmpty"))); +type ConditionInputKind = "where" | "orderBy"; +const CONDITION_EXPANDED_MAX_HEIGHT = 260; +let openingConditionHistoryKind: ConditionInputKind | null = null; + +function conditionInputRef(kind: ConditionInputKind) { + return kind === "where" ? whereFilterInputRef.value : orderByInputRef.value; +} + +function conditionOverlayRef(kind: ConditionInputKind) { + return kind === "where" ? whereFilterOverlayRef.value : orderByOverlayRef.value; +} + +function conditionPaneRef(kind: ConditionInputKind) { + return kind === "where" ? whereConditionPaneRef.value : orderByConditionPaneRef.value; +} + +function conditionControlRef(kind: ConditionInputKind) { + return kind === "where" ? whereConditionControlRef.value : orderByConditionControlRef.value; +} + +function conditionEditorRef(kind: ConditionInputKind) { + return conditionOverlayRef(kind) ?? conditionInputRef(kind); +} + +function conditionInputTextWidth(input: HTMLTextAreaElement): number { + const probe = document.createElement("span"); + const style = window.getComputedStyle(input); + probe.textContent = input.value || input.placeholder || ""; + probe.style.cssText = ` + position: fixed; + left: -9999px; + top: -9999px; + visibility: hidden; + white-space: pre; + font: ${style.font}; + font-size: ${style.fontSize}; + font-family: ${style.fontFamily}; + font-weight: ${style.fontWeight}; + letter-spacing: ${style.letterSpacing}; + `; + document.body.appendChild(probe); + const width = probe.getBoundingClientRect().width; + probe.remove(); + return width; +} + +function conditionInputShouldExpand(input: HTMLTextAreaElement): boolean { + return input.value.length > 0 && conditionInputTextWidth(input) > input.clientWidth + 1; +} + +function conditionInputContentHeight(input: HTMLTextAreaElement): number { + const probe = document.createElement("div"); + const style = window.getComputedStyle(input); + probe.textContent = input.value || input.placeholder || ""; + probe.style.cssText = ` + position: fixed; + left: -9999px; + top: -9999px; + visibility: hidden; + box-sizing: border-box; + width: ${input.clientWidth}px; + min-height: 0; + padding: ${style.paddingTop} ${style.paddingRight} ${style.paddingBottom} ${style.paddingLeft}; + border: 0; + white-space: pre-wrap; + overflow-wrap: anywhere; + font: ${style.font}; + font-size: ${style.fontSize}; + font-family: ${style.fontFamily}; + font-weight: ${style.fontWeight}; + line-height: ${style.lineHeight}; + letter-spacing: ${style.letterSpacing}; + text-indent: ${style.textIndent}; + `; + document.body.appendChild(probe); + const height = probe.scrollHeight; + probe.remove(); + return height; +} + +function conditionExpandedHeight(input: HTMLTextAreaElement): number { + const style = window.getComputedStyle(input); + const lineHeight = Number.parseFloat(style.lineHeight) || 24; + const inputRect = input.getBoundingClientRect(); + const pane = input.parentElement; + const paneRect = pane?.getBoundingClientRect(); + const verticalInset = paneRect ? Math.max(0, inputRect.top - paneRect.top) + Math.max(0, paneRect.bottom - inputRect.bottom) : 0; + const minimumHeight = Math.max(56, lineHeight * 2.5); + const availableHeight = Math.max(minimumHeight, window.innerHeight - input.getBoundingClientRect().top - 12); + const contentHeight = conditionInputContentHeight(input); + return Math.min(Math.max(minimumHeight, contentHeight + verticalInset), Math.min(CONDITION_EXPANDED_MAX_HEIGHT, availableHeight)); +} + +function conditionCaretTop(input: HTMLTextAreaElement, caretIndex: number): number { + const probe = document.createElement("div"); + const style = window.getComputedStyle(input); + const textBeforeCaret = input.value.slice(0, caretIndex); + probe.textContent = textBeforeCaret || " "; + probe.style.cssText = ` + position: fixed; + left: -9999px; + top: -9999px; + visibility: hidden; + box-sizing: border-box; + width: ${input.clientWidth}px; + min-height: 0; + padding: ${style.paddingTop} ${style.paddingRight} ${style.paddingBottom} ${style.paddingLeft}; + border: 0; + white-space: pre-wrap; + overflow-wrap: anywhere; + font: ${style.font}; + font-size: ${style.fontSize}; + font-family: ${style.fontFamily}; + font-weight: ${style.fontWeight}; + line-height: ${style.lineHeight}; + letter-spacing: ${style.letterSpacing}; + text-indent: ${style.textIndent}; + `; + document.body.appendChild(probe); + const lineHeight = Number.parseFloat(style.lineHeight) || 24; + const top = Math.max(0, probe.scrollHeight - lineHeight); + probe.remove(); + return top; +} + +function setConditionScrollTop(kind: ConditionInputKind, scrollTop: number) { + if (kind === "where") { + whereConditionScrollTop.value = scrollTop; + } else { + orderByConditionScrollTop.value = scrollTop; + } +} + +function scrollConditionCaretIntoView(kind: ConditionInputKind, input: HTMLTextAreaElement, caretIndex: number) { + const caretTop = conditionCaretTop(input, caretIndex); + const lineHeight = Number.parseFloat(window.getComputedStyle(input).lineHeight) || 24; + const topPadding = lineHeight * 0.5; + const maxScrollTop = Math.max(0, input.scrollHeight - input.clientHeight); + input.scrollTop = Math.min(Math.max(0, caretTop - topPadding), maxScrollTop); + setConditionScrollTop(kind, input.scrollTop); +} + +function conditionCollapsedHeight(kind: ConditionInputKind) { + const pane = conditionPaneRef(kind) ?? conditionControlRef(kind); + return pane ? pane.getBoundingClientRect().height : 28; +} + +function clearConditionCollapseTimer(kind: ConditionInputKind) { + if (kind === "where") { + if (whereConditionCollapseTimer) clearTimeout(whereConditionCollapseTimer); + whereConditionCollapseTimer = undefined; + } else { + if (orderByConditionCollapseTimer) clearTimeout(orderByConditionCollapseTimer); + orderByConditionCollapseTimer = undefined; + } +} + +function conditionPaneRendered(kind: ConditionInputKind) { + return kind === "where" ? whereConditionRendered.value : orderByConditionRendered.value; +} + +function setConditionRendered(kind: ConditionInputKind, rendered: boolean) { + if (kind === "where") { + whereConditionRendered.value = rendered; + } else { + orderByConditionRendered.value = rendered; + } +} + +function conditionPaneExpanded(kind: ConditionInputKind) { + return kind === "where" ? whereConditionExpanded.value : orderByConditionExpanded.value; +} + +function setConditionExpandedState(kind: ConditionInputKind, expanded: boolean) { + if (kind === "where") { + whereConditionExpanded.value = expanded; + } else { + orderByConditionExpanded.value = expanded; + } +} + +function queueConditionPaneRemoval(kind: ConditionInputKind) { + clearConditionCollapseTimer(kind); + const timer = setTimeout(() => { + if (!conditionPaneExpanded(kind)) setConditionRendered(kind, false); + }, 160); + if (kind === "where") { + whereConditionCollapseTimer = timer; + } else { + orderByConditionCollapseTimer = timer; + } +} + +function setConditionExpanded(kind: ConditionInputKind, expanded: boolean) { + if (expanded) { + const wasRendered = conditionPaneRendered(kind); + clearConditionCollapseTimer(kind); + setConditionRendered(kind, true); + if (wasRendered) { + setConditionExpandedState(kind, true); + } else { + setConditionExpandedState(kind, false); + requestAnimationFrame(() => requestAnimationFrame(() => setConditionExpandedState(kind, true))); + } + return; + } + setConditionExpandedState(kind, false); + queueConditionPaneRemoval(kind); +} + +function setConditionHeight(kind: ConditionInputKind, height: number) { + if (kind === "where") { + whereConditionHeight.value = height; + } else { + orderByConditionHeight.value = height; + } +} + +function setConditionRect(kind: ConditionInputKind, rect: DOMRect) { + const nextRect = { + left: rect.left, + top: rect.top, + width: rect.width, + height: rect.height, + }; + if (kind === "where") { + whereConditionRect.value = nextRect; + } else { + orderByConditionRect.value = nextRect; + } +} + +function conditionInputInsets(kind: ConditionInputKind) { + const pane = conditionPaneRef(kind); + const input = conditionInputRef(kind); + const control = conditionControlRef(kind); + if (!pane || !input) { + return { controlsTop: 3, inputTop: 3, prefix: 0, suffix: 0 }; + } + const paneRect = pane.getBoundingClientRect(); + const inputRect = input.getBoundingClientRect(); + const controlRect = control?.getBoundingClientRect(); + const paneStyle = window.getComputedStyle(pane); + const paddingLeft = Number.parseFloat(paneStyle.paddingLeft) || 0; + const paddingRight = Number.parseFloat(paneStyle.paddingRight) || 0; + return { + controlsTop: Math.max(0, (controlRect?.top ?? inputRect.top) - paneRect.top), + inputTop: Math.max(0, inputRect.top - paneRect.top), + prefix: Math.max(0, inputRect.left - paneRect.left - paddingLeft), + suffix: Math.max(28, paneRect.right - inputRect.right - paddingRight), + }; +} + +function conditionPaneStyle(kind: ConditionInputKind): CSSProperties { + const rendered = conditionPaneRendered(kind); + const expanded = conditionPaneExpanded(kind); + const height = kind === "where" ? whereConditionHeight.value : orderByConditionHeight.value; + const immediateHeight = kind === "where" ? whereConditionImmediateHeight.value : orderByConditionImmediateHeight.value; + if (!rendered) return {}; + const rect = kind === "where" ? whereConditionRect.value : orderByConditionRect.value; + const collapsedHeight = rect.height || 28; + const insets = conditionInputInsets(kind); + return { + "--data-grid-condition-controls-top": `${insets.controlsTop}px`, + "--data-grid-condition-input-top": `${insets.inputTop}px`, + "--data-grid-condition-prefix-indent": `${insets.prefix}px`, + "--data-grid-condition-suffix-width": `${insets.suffix}px`, + height: `${expanded ? height : collapsedHeight}px`, + left: `${rect.left}px`, + pointerEvents: expanded ? "auto" : "none", + top: `${rect.top}px`, + transition: immediateHeight ? "box-shadow 150ms ease" : undefined, + width: `${rect.width}px`, + }; +} + +function conditionTopbarOverlayStyle(kind: ConditionInputKind): CSSProperties { + const scrollTop = kind === "where" ? whereConditionScrollTop.value : orderByConditionScrollTop.value; + return { + transform: `translateY(${-scrollTop}px)`, + }; +} + +function focusConditionOverlay(kind: ConditionInputKind, source: HTMLTextAreaElement) { + window.setTimeout(() => { + const overlay = conditionOverlayRef(kind); + if (!overlay) return; + const start = source.selectionStart ?? source.value.length; + const end = source.selectionEnd ?? start; + overlay.focus(); + overlay.setSelectionRange(start, end); + scrollConditionCaretIntoView(kind, overlay, start); + requestAnimationFrame(() => scrollConditionCaretIntoView(kind, overlay, start)); + updateConditionSuggestionPosition(kind); + }, 0); +} + +function focusConditionInput(kind: ConditionInputKind, source: HTMLTextAreaElement) { + const input = conditionInputRef(kind); + if (!input) return; + const start = source.selectionStart ?? source.value.length; + const end = source.selectionEnd ?? start; + nextTick(() => { + input.focus(); + input.setSelectionRange(start, end); + updateConditionSuggestionPosition(kind); + }); +} + +function onConditionOverlayScroll(kind: ConditionInputKind, event: Event) { + const input = event.target instanceof HTMLTextAreaElement ? event.target : null; + if (!input) return; + setConditionScrollTop(kind, input.scrollTop); +} + +function resetConditionOverlayScrollIfContentFits(kind: ConditionInputKind) { + requestAnimationFrame(() => { + const overlay = conditionOverlayRef(kind); + const pane = overlay?.parentElement; + if (!overlay || !pane || overlay.scrollHeight > overlay.clientHeight + 1 || pane.getBoundingClientRect().height >= CONDITION_EXPANDED_MAX_HEIGHT - 1) return; + overlay.scrollTop = 0; + setConditionScrollTop(kind, 0); + }); +} + +function onConditionOverlayClick(kind: ConditionInputKind) { + if (kind === "where") { + if (whereHistoryMenuOpen.value) { + dismissWhereSuggestions(); + return; + } + updateWhereSuggestionPosition(); + } else { + if (orderByHistoryMenuOpen.value) { + dismissOrderBySuggestions(); + return; + } + updateOrderBySuggestionPosition(); + } +} + +function resizeConditionInput(kind: ConditionInputKind, options: { focusOverlay?: boolean; expandFromFocus?: boolean; immediateHeight?: boolean } = {}) { + nextTick(() => { + const input = conditionInputRef(kind); + if (!input) return; + const editor = conditionEditorRef(kind) ?? input; + const focused = document.activeElement === input || document.activeElement === conditionOverlayRef(kind); + const expanded = focused && conditionInputShouldExpand(input) && (options.expandFromFocus || document.activeElement === conditionOverlayRef(kind)); + const shouldRestoreInputFocus = !expanded && document.activeElement === conditionOverlayRef(kind); + if (shouldRestoreInputFocus && editor instanceof HTMLTextAreaElement) focusConditionInput(kind, editor); + if (expanded) setConditionRect(kind, (conditionPaneRef(kind) ?? conditionControlRef(kind) ?? input).getBoundingClientRect()); + setConditionExpanded(kind, expanded); + requestAnimationFrame(() => { + if (!expanded) { + if (kind === "where") whereConditionImmediateHeight.value = false; + else orderByConditionImmediateHeight.value = false; + setConditionHeight(kind, conditionCollapsedHeight(kind)); + return; + } + if (options.immediateHeight) { + if (kind === "where") whereConditionImmediateHeight.value = true; + else orderByConditionImmediateHeight.value = true; + } + setConditionHeight(kind, conditionExpandedHeight(editor)); + if (options.immediateHeight) { + requestAnimationFrame(() => { + if (kind === "where") whereConditionImmediateHeight.value = false; + else orderByConditionImmediateHeight.value = false; + }); + } + updateConditionSuggestionPosition(kind); + if (options.focusOverlay && document.activeElement === input) focusConditionOverlay(kind, input); + else resetConditionOverlayScrollIfContentFits(kind); + }); + }); +} + +function onConditionInputClick(kind: ConditionInputKind) { + updateConditionSuggestionPosition(kind); + resizeConditionInput(kind, { expandFromFocus: true, focusOverlay: true }); +} + +function collapseConditionInput(kind: ConditionInputKind) { + setConditionExpanded(kind, false); + setConditionHeight(kind, conditionCollapsedHeight(kind)); + setConditionScrollTop(kind, 0); +} + +function collapseConditionInputAfterBlur(kind: ConditionInputKind) { + window.setTimeout(() => { + const input = conditionInputRef(kind); + const overlay = conditionOverlayRef(kind); + if (document.activeElement === input || document.activeElement === overlay) return; + collapseConditionInput(kind); + }, 0); +} + +function resizeFocusedConditionInputs() { + resizeConditionInput("where"); + resizeConditionInput("orderBy"); +} + +function normalizeConditionInputValue(value: string) { + return value.replace(/\s*\r?\n\s*/g, " "); +} + +function updateConditionSuggestionPosition(kind: ConditionInputKind) { + if (kind === "where") { + updateWhereSuggestionPosition(); + } else { + updateOrderBySuggestionPosition(); + } +} + +function setConditionHistorySuggestionPosition(kind: ConditionInputKind, target: EventTarget | null) { + if (!(target instanceof HTMLElement)) { + updateConditionSuggestionPosition(kind); + return; + } + const rect = target.getBoundingClientRect(); + const position = { + left: Math.max(0, Math.min(rect.left, window.innerWidth - 180)), + top: rect.bottom + 2, + }; + if (kind === "where") { + whereSuggestionPosition.value = position; + } else { + orderBySuggestionPosition.value = position; + } +} + type LocalFilterMode = "local" | "server"; type LocalFilterOption = { key: string; @@ -1285,6 +1738,12 @@ function ensureStructuredFilterRule() { } } +function openStructuredFilterFromExpandedWhere() { + collapseConditionInput("where"); + ensureStructuredFilterRule(); + filterBuilderOpen.value = true; +} + function addStructuredFilterRule() { ensureStructuredFilterRule(); structuredFilterRules.value = [...structuredFilterRules.value, defaultStructuredFilterRule()]; @@ -1522,7 +1981,7 @@ function onSearchKeydown(e: KeyboardEvent) { // --- WHERE filter input suggestions --- function updateWhereSuggestionPosition() { nextTick(() => { - const input = whereFilterInputRef.value; + const input = conditionEditorRef("where"); const measure = whereMeasureRef.value; if (!input || !measure) return; const cursorPos = input.selectionStart ?? 0; @@ -1545,7 +2004,7 @@ function acceptWhereSuggestion() { whereSuggestions.value = []; whereSuggestionIndex.value = -1; whereHistoryMenuOpen.value = false; - whereFilterInputRef.value?.focus(); + conditionEditorRef("where")?.focus(); return; } const lastWordMatch = whereFilterInput.value.match(/([^\s,()><=!&|]+)$/); @@ -1557,7 +2016,7 @@ function acceptWhereSuggestion() { whereSuggestions.value = []; whereSuggestionIndex.value = -1; whereHistoryMenuOpen.value = false; - whereFilterInputRef.value?.focus(); + conditionEditorRef("where")?.focus(); } function dismissWhereSuggestions() { @@ -1579,13 +2038,21 @@ function showWhereHistorySuggestions() { const history = loadDataGridConditionHistory("where", conditionHistoryScope.value, whereFilterInput.value); whereSuggestions.value = history.map((value) => ({ value, kind: "history" })); whereSuggestionIndex.value = -1; + if (openingConditionHistoryKind === "where") return; updateWhereSuggestionPosition(); } -function openWhereHistoryMenu() { +function openWhereHistoryMenu(event?: MouseEvent) { + openingConditionHistoryKind = "where"; + conditionEditorRef("where")?.focus(); whereHistoryMenuOpen.value = true; - showWhereHistorySuggestions(); - whereFilterInputRef.value?.focus(); + const history = loadDataGridConditionHistory("where", conditionHistoryScope.value, whereFilterInput.value); + whereSuggestions.value = history.map((value) => ({ value, kind: "history" })); + whereSuggestionIndex.value = -1; + setConditionHistorySuggestionPosition("where", event?.currentTarget ?? null); + requestAnimationFrame(() => { + if (openingConditionHistoryKind === "where") openingConditionHistoryKind = null; + }); } function deleteWhereHistorySuggestion(value: string) { @@ -1599,9 +2066,17 @@ function deleteWhereHistorySuggestion(value: string) { } function onWhereFilterInput(event: Event) { - const input = event.target instanceof HTMLInputElement ? event.target : null; + const input = event.target instanceof HTMLTextAreaElement ? event.target : null; if (!input) return; - const nextValue = input.value; + suppressNextWhereWatchResize = true; + const nextValue = normalizeConditionInputValue(input.value); + if (nextValue !== input.value) { + whereFilterInput.value = nextValue; + previousWhereFilterInputValue = nextValue; + nextTick(() => input.setSelectionRange(nextValue.length, nextValue.length)); + resizeConditionInput("where", { expandFromFocus: true, focusOverlay: true, immediateHeight: true }); + return; + } if ( insertedSqlSingleQuoteAtCaret({ previousValue: previousWhereFilterInputValue, @@ -1614,6 +2089,7 @@ function onWhereFilterInput(event: Event) { whereFilterInput.value = pairedValue; previousWhereFilterInputValue = pairedValue; nextTick(() => input.setSelectionRange(caret, caret)); + resizeConditionInput("where", { expandFromFocus: true, focusOverlay: true, immediateHeight: true }); return; } const nextCaret = caretPositionInsideInsertedSqlSingleQuotes({ @@ -1622,6 +2098,7 @@ function onWhereFilterInput(event: Event) { selectionStart: input.selectionStart, }); previousWhereFilterInputValue = nextValue; + resizeConditionInput("where", { expandFromFocus: true, focusOverlay: true, immediateHeight: true }); if (nextCaret == null) return; nextTick(() => input.setSelectionRange(nextCaret, nextCaret)); } @@ -1630,6 +2107,11 @@ watch(whereFilterInput, (val) => { emit("update:whereInput", currentWhereInput() ?? ""); persistStructuredFilterState(); previousWhereFilterInputValue = val; + if (suppressNextWhereWatchResize) { + suppressNextWhereWatchResize = false; + } else { + resizeConditionInput("where"); + } whereSuggestions.value = []; whereHistoryMenuOpen.value = false; if (!props.tableMeta?.columns?.length) return; @@ -1653,7 +2135,7 @@ watch(whereFilterInput, (val) => { function onWhereFilterKeydown(e: KeyboardEvent) { if (e.key in PAIRS && !e.ctrlKey && !e.metaKey) { - const input = e.target as HTMLInputElement; + const input = e.target as HTMLTextAreaElement; const start = input.selectionStart ?? 0; const end = input.selectionEnd ?? 0; const close = PAIRS[e.key]; @@ -1715,7 +2197,7 @@ function onWhereFilterKeydown(e: KeyboardEvent) { // --- ORDER BY input suggestions --- function updateOrderBySuggestionPosition() { nextTick(() => { - const input = orderByInputRef.value; + const input = conditionEditorRef("orderBy"); const measure = orderByMeasureRef.value; if (!input || !measure) return; const cursorPos = input.selectionStart ?? 0; @@ -1738,7 +2220,7 @@ function acceptOrderBySuggestion() { orderBySuggestions.value = []; orderBySuggestionIndex.value = -1; orderByHistoryMenuOpen.value = false; - orderByInputRef.value?.focus(); + conditionEditorRef("orderBy")?.focus(); return; } const lastWordMatch = orderByInput.value.match(/([^\s,()]+)$/); @@ -1750,7 +2232,7 @@ function acceptOrderBySuggestion() { orderBySuggestions.value = []; orderBySuggestionIndex.value = -1; orderByHistoryMenuOpen.value = false; - orderByInputRef.value?.focus(); + conditionEditorRef("orderBy")?.focus(); } function dismissOrderBySuggestions() { @@ -1772,13 +2254,21 @@ function showOrderByHistorySuggestions() { const history = loadDataGridConditionHistory("orderBy", conditionHistoryScope.value, orderByInput.value); orderBySuggestions.value = history.map((value) => ({ value, kind: "history" })); orderBySuggestionIndex.value = -1; + if (openingConditionHistoryKind === "orderBy") return; updateOrderBySuggestionPosition(); } -function openOrderByHistoryMenu() { +function openOrderByHistoryMenu(event?: MouseEvent) { + openingConditionHistoryKind = "orderBy"; + conditionEditorRef("orderBy")?.focus(); orderByHistoryMenuOpen.value = true; - showOrderByHistorySuggestions(); - orderByInputRef.value?.focus(); + const history = loadDataGridConditionHistory("orderBy", conditionHistoryScope.value, orderByInput.value); + orderBySuggestions.value = history.map((value) => ({ value, kind: "history" })); + orderBySuggestionIndex.value = -1; + setConditionHistorySuggestionPosition("orderBy", event?.currentTarget ?? null); + requestAnimationFrame(() => { + if (openingConditionHistoryKind === "orderBy") openingConditionHistoryKind = null; + }); } function deleteOrderByHistorySuggestion(value: string) { @@ -1793,6 +2283,11 @@ function deleteOrderByHistorySuggestion(value: string) { watch(orderByInput, (val) => { emit("update:orderByInput", val); + if (suppressNextOrderByWatchResize) { + suppressNextOrderByWatchResize = false; + } else { + resizeConditionInput("orderBy"); + } orderBySuggestions.value = []; orderByHistoryMenuOpen.value = false; if (!props.tableMeta?.columns?.length) return; @@ -1814,6 +2309,18 @@ watch(orderByInput, (val) => { } }); +function onOrderByInput(event: Event) { + const input = event.target instanceof HTMLTextAreaElement ? event.target : null; + if (!input) return; + suppressNextOrderByWatchResize = true; + const nextValue = normalizeConditionInputValue(input.value); + if (nextValue !== input.value) { + orderByInput.value = nextValue; + nextTick(() => input.setSelectionRange(nextValue.length, nextValue.length)); + } + resizeConditionInput("orderBy", { expandFromFocus: true, focusOverlay: true, immediateHeight: true }); +} + function onOrderByKeydown(e: KeyboardEvent) { if (orderBySuggestions.value.length > 0) { if (e.key === "Tab") { @@ -5105,12 +5612,17 @@ onActivated(resumeCanvasGridWork); onMounted(() => { if (typeof window === "undefined") return; window.addEventListener("resize", scheduleCanvasPixelRatioRefresh); + window.addEventListener("resize", resizeFocusedConditionInputs); window.visualViewport?.addEventListener("resize", scheduleCanvasPixelRatioRefresh); + window.visualViewport?.addEventListener("resize", resizeFocusedConditionInputs); window.addEventListener("dbx:ui-scale-applied", scheduleCanvasPixelRatioRefresh); + window.addEventListener("dbx:ui-scale-applied", resizeFocusedConditionInputs); }); onDeactivated(pauseCanvasGridWork); onUnmounted(() => { pauseCanvasGridWork(); + clearConditionCollapseTimer("where"); + clearConditionCollapseTimer("orderBy"); gridHorizontalScrollbarResizeObserver?.disconnect(); dataGridTopbarResizeObserver?.disconnect(); disconnectCellEditResizeObserver(); @@ -5122,8 +5634,11 @@ onUnmounted(() => { } if (typeof window === "undefined") return; window.removeEventListener("resize", scheduleCanvasPixelRatioRefresh); + window.removeEventListener("resize", resizeFocusedConditionInputs); window.visualViewport?.removeEventListener("resize", scheduleCanvasPixelRatioRefresh); + window.visualViewport?.removeEventListener("resize", resizeFocusedConditionInputs); window.removeEventListener("dbx:ui-scale-applied", scheduleCanvasPixelRatioRefresh); + window.removeEventListener("dbx:ui-scale-applied", resizeFocusedConditionInputs); }); function setRowStatusFilter(value: string) { @@ -6934,6 +7449,7 @@ function onSearchSplitResizeMove(event: MouseEvent) { containerWidth, desiredWidth: searchSplitStartWidth + event.clientX - searchSplitStartX, }); + resizeFocusedConditionInputs(); } function onSearchSplitResizeEnd() { @@ -6946,6 +7462,21 @@ function onSearchSplitResizeEnd() { function resetSearchSplitWidth() { const containerWidth = searchSplitContainerWidth(); searchSplitWhereWidth.value = containerWidth > 0 ? clampSearchSplitWidth({ containerWidth }) : null; + resizeFocusedConditionInputs(); +} + +function onDataGridTopbarFixedActionWheel(event: WheelEvent) { + if (Math.abs(event.deltaX) <= Math.abs(event.deltaY)) return; + event.preventDefault(); + event.stopPropagation(); +} + +function onConditionInputWheel(event: WheelEvent) { + const input = event.currentTarget instanceof HTMLTextAreaElement ? event.currentTarget : null; + if (!input || Math.abs(event.deltaX) <= Math.abs(event.deltaY)) return; + input.scrollLeft += event.deltaX; + event.preventDefault(); + event.stopPropagation(); } function onDdlResizeStart(event: MouseEvent) { @@ -7368,442 +7899,568 @@ const gridContextMenuItems = computed(() => {
-
-
-
- -
- -