diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index d70c8e6b61..0bf248ee06 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -152,6 +152,7 @@ import type { DataGridSortDirection, DataGridSortMode } from "@/lib/dataGridSort import { getTableMetadataCapabilities } from "@/lib/tableMetadataCapabilities"; import { supportsTableStructureEditing } from "@/lib/databaseCapabilities"; import { forgetDataGridConditionHistory, loadDataGridConditionHistory, rememberDataGridConditionHistory } from "@/lib/dataGridConditionHistory"; +import { getDataGridConditionSuggestionPosition } from "@/lib/dataGridConditionSuggestionPosition"; import { caretPositionInsideInsertedSqlSingleQuotes, insertedSqlSingleQuoteAtCaret } from "@/lib/sqlQuoteCaret"; import { effectiveDatabaseTypeForConnection } from "@/lib/jdbcDialect"; import { isMacOS } from "@/lib/platform"; @@ -494,9 +495,8 @@ 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 whereSuggestionDropdownRef = ref(); +const whereSuggestionPosition = ref({ left: 0, top: 0, width: 180 }); const whereConditionRendered = ref(false); const whereConditionExpanded = ref(false); const whereConditionHeight = ref(28); @@ -513,9 +513,8 @@ 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 orderBySuggestionDropdownRef = ref(); +const orderBySuggestionPosition = ref({ left: 0, top: 0, width: 180 }); const orderByConditionRendered = ref(false); const orderByConditionExpanded = ref(false); const orderByConditionHeight = ref(28); @@ -524,11 +523,14 @@ const orderByConditionScrollTop = ref(0); const orderByConditionImmediateHeight = ref(false); let orderByConditionCollapseTimer: ReturnType | undefined; let suppressNextOrderByWatchResize = false; +const conditionHistoryPreview = ref<{ value: string; left: number; top: number; maxWidth: number; arrowTop: number; side: "left" | "right" } | null>(null); const orderByInput = ref(props.initialOrderByInput ?? ""); const hasOrderByInput = computed(() => orderByInput.value.trim().length > 0); const whereFilterInput = ref(props.initialWhereInput ?? ""); let previousWhereFilterInputValue = whereFilterInput.value; +let suppressWhereHistoryOnNextEmptyInput = false; +let suppressOrderByHistoryOnNextEmptyInput = false; const hasWhereFilterInput = computed(() => whereFilterInput.value.trim().length > 0); const conditionHistoryScope = computed(() => ({ connectionId: props.connectionId, @@ -552,6 +554,7 @@ const whereSearchPaneStyle = computed(() => { const whereSuggestionStyle = computed(() => ({ left: `${whereSuggestionPosition.value.left}px`, top: `${whereSuggestionPosition.value.top}px`, + width: `${whereSuggestionPosition.value.width}px`, })); const showWhereSuggestionDropdown = computed(() => whereSuggestions.value.length > 0 || whereHistoryMenuOpen.value); const whereHistoryEmptyText = computed(() => (whereFilterInput.value.trim() ? t("grid.conditionHistoryNoMatches") : t("grid.conditionHistoryEmpty"))); @@ -559,13 +562,29 @@ const whereHistoryEmptyText = computed(() => (whereFilterInput.value.trim() ? t( const orderBySuggestionStyle = computed(() => ({ left: `${orderBySuggestionPosition.value.left}px`, top: `${orderBySuggestionPosition.value.top}px`, + width: `${orderBySuggestionPosition.value.width}px`, })); const showOrderBySuggestionDropdown = computed(() => orderBySuggestions.value.length > 0 || orderByHistoryMenuOpen.value); const orderByHistoryEmptyText = computed(() => (orderByInput.value.trim() ? t("grid.conditionHistoryNoMatches") : t("grid.conditionHistoryEmpty"))); +const conditionHistoryPreviewStyle = computed(() => { + const preview = conditionHistoryPreview.value; + if (!preview) return {}; + return { + left: `${preview.left}px`, + maxWidth: `${preview.maxWidth}px`, + top: `${preview.top}px`, + }; +}); +const conditionHistoryPreviewArrowStyle = computed(() => { + const preview = conditionHistoryPreview.value; + if (!preview) return {}; + return { + top: `${preview.arrowTop}px`, + }; +}); 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; @@ -941,6 +960,16 @@ function resizeConditionInput(kind: ConditionInputKind, options: { focusOverlay? } function onConditionInputClick(kind: ConditionInputKind) { + if (kind === "where" && showWhereSuggestionDropdown.value) { + dismissWhereSuggestions(); + resizeConditionInput(kind, { expandFromFocus: true, focusOverlay: true }); + return; + } + if (kind === "orderBy" && showOrderBySuggestionDropdown.value) { + dismissOrderBySuggestions(); + resizeConditionInput(kind, { expandFromFocus: true, focusOverlay: true }); + return; + } updateConditionSuggestionPosition(kind); resizeConditionInput(kind, { expandFromFocus: true, focusOverlay: true }); } @@ -977,21 +1006,31 @@ function updateConditionSuggestionPosition(kind: ConditionInputKind) { } } -function setConditionHistorySuggestionPosition(kind: ConditionInputKind, target: EventTarget | null) { - if (!(target instanceof HTMLElement)) { - updateConditionSuggestionPosition(kind); +function showConditionHistoryPreview(value: string, event: MouseEvent) { + const target = event.currentTarget instanceof HTMLElement ? event.currentTarget : null; + if (!target) return; + const textElement = target.querySelector("[data-condition-history-text]"); + if (!textElement || textElement.scrollWidth <= textElement.clientWidth + 1) { + hideConditionHistoryPreview(); 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; - } + const gap = 10; + const minWidth = 280; + const preferredWidth = 560; + const availableRight = window.innerWidth - rect.right - gap - 8; + const availableLeft = rect.left - gap - 8; + const placeRight = availableRight >= minWidth || availableRight >= availableLeft; + const maxWidth = Math.max(minWidth, Math.min(preferredWidth, placeRight ? availableRight : availableLeft)); + const left = placeRight ? rect.right + gap : Math.max(8, rect.left - gap - maxWidth); + const estimatedHeight = Math.min(260, Math.max(56, Math.ceil(value.length / 58) * 18 + 24)); + const top = Math.min(Math.max(8, rect.top), Math.max(8, window.innerHeight - estimatedHeight - 8)); + const arrowTop = Math.min(Math.max(14, rect.top + rect.height / 2 - top), estimatedHeight - 14); + conditionHistoryPreview.value = { value, left, top, maxWidth, arrowTop, side: placeRight ? "left" : "right" }; +} + +function hideConditionHistoryPreview() { + conditionHistoryPreview.value = null; } type LocalFilterMode = "local" | "server"; @@ -1989,16 +2028,9 @@ function onSearchKeydown(e: KeyboardEvent) { function updateWhereSuggestionPosition() { nextTick(() => { const input = conditionEditorRef("where"); - const measure = whereMeasureRef.value; - if (!input || !measure) return; - const cursorPos = input.selectionStart ?? 0; - measure.textContent = whereFilterInput.value.slice(0, cursorPos); - whereSuggestionLeft.value = measure.getBoundingClientRect().width; + if (!input) return; const inputRect = input.getBoundingClientRect(); - whereSuggestionPosition.value = { - left: Math.max(0, Math.min(inputRect.left + whereSuggestionLeft.value, window.innerWidth - 180)), - top: inputRect.bottom + 2, - }; + whereSuggestionPosition.value = getDataGridConditionSuggestionPosition(inputRect, { viewportWidth: window.innerWidth }); }); } @@ -2030,6 +2062,24 @@ function dismissWhereSuggestions() { whereSuggestions.value = []; whereSuggestionIndex.value = -1; whereHistoryMenuOpen.value = false; + hideConditionHistoryPreview(); +} + +function eventTargetInside(event: Event, ...elements: Array): boolean { + const path = typeof event.composedPath === "function" ? event.composedPath() : []; + const target = event.target instanceof Node ? event.target : null; + return elements.some((element) => element && (path.includes(element) || (target ? element.contains(target) : false))); +} + +function onConditionSuggestionDocumentPointerDown(event: PointerEvent) { + const whereExpandedPane = whereFilterOverlayRef.value?.parentElement ?? undefined; + const orderByExpandedPane = orderByOverlayRef.value?.parentElement ?? undefined; + if (!eventTargetInside(event, whereFilterInputRef.value, whereFilterOverlayRef.value, whereConditionPaneRef.value, whereExpandedPane, whereSuggestionDropdownRef.value)) { + dismissWhereSuggestions(); + } + if (!eventTargetInside(event, orderByInputRef.value, orderByOverlayRef.value, orderByConditionPaneRef.value, orderByExpandedPane, orderBySuggestionDropdownRef.value)) { + dismissOrderBySuggestions(); + } } function navigateWhereSuggestion(delta: number) { @@ -2041,32 +2091,29 @@ function navigateWhereSuggestion(delta: number) { whereSuggestionIndex.value = Math.min(Math.max(whereSuggestionIndex.value + delta, 0), whereSuggestions.value.length - 1); } -function showWhereHistorySuggestions() { - const history = loadDataGridConditionHistory("where", conditionHistoryScope.value, whereFilterInput.value); +function openWhereHistoryMenu() { + if (showWhereSuggestionDropdown.value) { + dismissWhereSuggestions(); + return; + } + conditionEditorRef("where")?.focus(); + whereHistoryMenuOpen.value = true; + const history = loadDataGridConditionHistory("where", conditionHistoryScope.value); whereSuggestions.value = history.map((value) => ({ value, kind: "history" })); whereSuggestionIndex.value = -1; - if (openingConditionHistoryKind === "where") return; updateWhereSuggestionPosition(); } -function openWhereHistoryMenu(event?: MouseEvent) { - openingConditionHistoryKind = "where"; - conditionEditorRef("where")?.focus(); - whereHistoryMenuOpen.value = true; - 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 clearWhereFilterInput() { + suppressWhereHistoryOnNextEmptyInput = true; + whereFilterInput.value = ""; + dismissWhereSuggestions(); + void applyWhereFilter(); } function deleteWhereHistorySuggestion(value: string) { const history = forgetDataGridConditionHistory("where", conditionHistoryScope.value, value); - const query = whereFilterInput.value; - const filtered = query.trim() ? loadDataGridConditionHistory("where", conditionHistoryScope.value, query) : history; - whereSuggestions.value = filtered.map((item) => ({ value: item, kind: "history" })); + whereSuggestions.value = history.map((item) => ({ value: item, kind: "history" })); whereSuggestionIndex.value = whereSuggestions.value.length ? Math.min(whereSuggestionIndex.value, whereSuggestions.value.length - 1) : -1; whereHistoryMenuOpen.value = true; updateWhereSuggestionPosition(); @@ -2121,12 +2168,15 @@ watch(whereFilterInput, (val) => { } whereSuggestions.value = []; whereHistoryMenuOpen.value = false; - if (!props.tableMeta?.columns?.length) return; const trimmed = val.trim(); if (trimmed.length === 0) { - showWhereHistorySuggestions(); + if (suppressWhereHistoryOnNextEmptyInput) { + suppressWhereHistoryOnNextEmptyInput = false; + } return; } + suppressWhereHistoryOnNextEmptyInput = false; + if (!props.tableMeta?.columns?.length) return; const lastToken = trimmed.split(/[\s,()><=!&|]+/).pop() || ""; if (lastToken.length > 0) { const tl = lastToken.toLowerCase(); @@ -2205,16 +2255,9 @@ function onWhereFilterKeydown(e: KeyboardEvent) { function updateOrderBySuggestionPosition() { nextTick(() => { const input = conditionEditorRef("orderBy"); - const measure = orderByMeasureRef.value; - if (!input || !measure) return; - const cursorPos = input.selectionStart ?? 0; - measure.textContent = orderByInput.value.slice(0, cursorPos); - orderBySuggestionLeft.value = measure.getBoundingClientRect().width; + if (!input) return; const inputRect = input.getBoundingClientRect(); - orderBySuggestionPosition.value = { - left: Math.max(0, Math.min(inputRect.left + orderBySuggestionLeft.value, window.innerWidth - 180)), - top: inputRect.bottom + 2, - }; + orderBySuggestionPosition.value = getDataGridConditionSuggestionPosition(inputRect, { viewportWidth: window.innerWidth }); }); } @@ -2246,6 +2289,7 @@ function dismissOrderBySuggestions() { orderBySuggestions.value = []; orderBySuggestionIndex.value = -1; orderByHistoryMenuOpen.value = false; + hideConditionHistoryPreview(); } function navigateOrderBySuggestion(delta: number) { @@ -2257,32 +2301,29 @@ function navigateOrderBySuggestion(delta: number) { orderBySuggestionIndex.value = Math.min(Math.max(orderBySuggestionIndex.value + delta, 0), orderBySuggestions.value.length - 1); } -function showOrderByHistorySuggestions() { - const history = loadDataGridConditionHistory("orderBy", conditionHistoryScope.value, orderByInput.value); +function openOrderByHistoryMenu() { + if (showOrderBySuggestionDropdown.value) { + dismissOrderBySuggestions(); + return; + } + conditionEditorRef("orderBy")?.focus(); + orderByHistoryMenuOpen.value = true; + const history = loadDataGridConditionHistory("orderBy", conditionHistoryScope.value); orderBySuggestions.value = history.map((value) => ({ value, kind: "history" })); orderBySuggestionIndex.value = -1; - if (openingConditionHistoryKind === "orderBy") return; updateOrderBySuggestionPosition(); } -function openOrderByHistoryMenu(event?: MouseEvent) { - openingConditionHistoryKind = "orderBy"; - conditionEditorRef("orderBy")?.focus(); - orderByHistoryMenuOpen.value = true; - 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 clearOrderByInput() { + suppressOrderByHistoryOnNextEmptyInput = true; + orderByInput.value = ""; + dismissOrderBySuggestions(); + void applyOrderBySearch(); } function deleteOrderByHistorySuggestion(value: string) { const history = forgetDataGridConditionHistory("orderBy", conditionHistoryScope.value, value); - const query = orderByInput.value; - const filtered = query.trim() ? loadDataGridConditionHistory("orderBy", conditionHistoryScope.value, query) : history; - orderBySuggestions.value = filtered.map((item) => ({ value: item, kind: "history" })); + orderBySuggestions.value = history.map((item) => ({ value: item, kind: "history" })); orderBySuggestionIndex.value = orderBySuggestions.value.length ? Math.min(orderBySuggestionIndex.value, orderBySuggestions.value.length - 1) : -1; orderByHistoryMenuOpen.value = true; updateOrderBySuggestionPosition(); @@ -2297,12 +2338,15 @@ watch(orderByInput, (val) => { } orderBySuggestions.value = []; orderByHistoryMenuOpen.value = false; - if (!props.tableMeta?.columns?.length) return; const trimmed = val.trim(); if (trimmed.length === 0) { - showOrderByHistorySuggestions(); + if (suppressOrderByHistoryOnNextEmptyInput) { + suppressOrderByHistoryOnNextEmptyInput = false; + } return; } + suppressOrderByHistoryOnNextEmptyInput = false; + if (!props.tableMeta?.columns?.length) return; const lastToken = trimmed.split(/[\s,()]+/).pop() || ""; if (lastToken.length > 0 && !["asc", "desc"].includes(lastToken.toLowerCase())) { const tl = lastToken.toLowerCase(); @@ -5670,6 +5714,7 @@ onMounted(() => { window.visualViewport?.addEventListener("resize", resizeFocusedConditionInputs); window.addEventListener("dbx:ui-scale-applied", scheduleCanvasPixelRatioRefresh); window.addEventListener("dbx:ui-scale-applied", resizeFocusedConditionInputs); + document.addEventListener("pointerdown", onConditionSuggestionDocumentPointerDown, true); }); onDeactivated(pauseCanvasGridWork); onUnmounted(() => { @@ -5692,6 +5737,7 @@ onUnmounted(() => { window.visualViewport?.removeEventListener("resize", resizeFocusedConditionInputs); window.removeEventListener("dbx:ui-scale-applied", scheduleCanvasPixelRatioRefresh); window.removeEventListener("dbx:ui-scale-applied", resizeFocusedConditionInputs); + document.removeEventListener("pointerdown", onConditionSuggestionDocumentPointerDown, true); }); function setRowStatusFilter(value: string) { @@ -8222,10 +8268,7 @@ const gridContextMenuItems = computed(() => { placeholder="WHERE" @input="onWhereFilterInput" @keydown="onWhereFilterKeydown" - @focus=" - showWhereHistorySuggestions(); - resizeConditionInput('where'); - " + @focus="resizeConditionInput('where')" @click="onConditionInputClick('where')" @wheel="onConditionInputWheel" @blur=" @@ -8237,14 +8280,7 @@ const gridContextMenuItems = computed(() => { - @@ -8290,37 +8326,33 @@ const gridContextMenuItems = computed(() => { - -