fix: fix value editor scrolling and add selection highlight
- Fix value editor not scrollable when content exceeds container (overflow-hidden -> overflow-auto) - Extract vscodeSelectionLayer from QueryEditor into shared module and apply to value editor for consistent selection highlighting
This commit is contained in:
1 parent
84e2093b82
commit
68c8a5cc7b
4 files changed
+93
-89
No files matched your search
@@ -44,6 +44,7 @@ import {
|
|||||||
} from "@/lib/sqlSemanticDiagnostics";
|
} from "@/lib/sqlSemanticDiagnostics";
|
||||||
import type { SqlCompletionColumn } from "@/lib/sqlCompletion";
|
import type { SqlCompletionColumn } from "@/lib/sqlCompletion";
|
||||||
import type { SqlReferenceAnalysis, SqlTableReference, SqlTextSpan } from "@/types/database";
|
import type { SqlReferenceAnalysis, SqlTableReference, SqlTextSpan } from "@/types/database";
|
||||||
|
import { vscodeSelectionLayer } from "@/lib/codemirrorVscodeSelectionLayer";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: string;
|
modelValue: string;
|
||||||
@@ -934,8 +935,6 @@ onMounted(async () => {
|
|||||||
dropCursor,
|
dropCursor,
|
||||||
crosshairCursor,
|
crosshairCursor,
|
||||||
ViewPlugin,
|
ViewPlugin,
|
||||||
layer,
|
|
||||||
RectangleMarker,
|
|
||||||
},
|
},
|
||||||
{ EditorState, Compartment, Prec, StateEffect, StateField },
|
{ EditorState, Compartment, Prec, StateEffect, StateField },
|
||||||
{ sql, MSSQL, MySQL, PostgreSQL, SQLDialect },
|
{ sql, MSSQL, MySQL, PostgreSQL, SQLDialect },
|
||||||
@@ -1069,91 +1068,6 @@ onMounted(async () => {
|
|||||||
{ decorations: (v) => v.decorations },
|
{ decorations: (v) => v.decorations },
|
||||||
);
|
);
|
||||||
|
|
||||||
function getLineElement(view: import("@codemirror/view").EditorView, pos: number): HTMLElement | null {
|
|
||||||
try {
|
|
||||||
const domAt = view.domAtPos(pos);
|
|
||||||
let node: Node | null = domAt.node;
|
|
||||||
while (node) {
|
|
||||||
if (node instanceof HTMLElement && node.classList.contains("cm-line")) return node;
|
|
||||||
node = node.parentElement;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// domAtPos may throw for positions outside the viewport
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const vscodeSelectionLayer = layer({
|
|
||||||
above: false,
|
|
||||||
class: "cm-vscodeSelectionLayer",
|
|
||||||
markers(view) {
|
|
||||||
const markers: InstanceType<typeof RectangleMarker>[] = [];
|
|
||||||
const contentRect = view.contentDOM.getBoundingClientRect();
|
|
||||||
const baseRect = view.scrollDOM.getBoundingClientRect();
|
|
||||||
const base = {
|
|
||||||
left: baseRect.left - view.scrollDOM.scrollLeft * view.scaleX,
|
|
||||||
top: baseRect.top - view.scrollDOM.scrollTop * view.scaleY,
|
|
||||||
};
|
|
||||||
const lineElt = view.contentDOM.querySelector(".cm-line");
|
|
||||||
const lineStyle = lineElt && window.getComputedStyle(lineElt);
|
|
||||||
const contentLeft =
|
|
||||||
contentRect.left +
|
|
||||||
(lineStyle ? parseInt(lineStyle.paddingLeft) + Math.min(0, parseInt(lineStyle.textIndent)) : 0);
|
|
||||||
// single character width for empty-line fallback
|
|
||||||
const sampleCoords = view.coordsAtPos(view.viewport.from);
|
|
||||||
const charWidth = sampleCoords ? sampleCoords.right - sampleCoords.left : 8;
|
|
||||||
for (const r of view.state.selection.ranges) {
|
|
||||||
if (r.empty) continue;
|
|
||||||
const fromLine = view.lineBlockAt(r.from);
|
|
||||||
const toLine = view.lineBlockAt(r.to);
|
|
||||||
for (let pos = fromLine.from; pos <= toLine.from; ) {
|
|
||||||
const line = view.lineBlockAt(pos);
|
|
||||||
const lineEl = getLineElement(view, line.from);
|
|
||||||
if (!lineEl) break;
|
|
||||||
const lineRect = lineEl.getBoundingClientRect();
|
|
||||||
let left = contentLeft - base.left;
|
|
||||||
let right: number;
|
|
||||||
const isFirst = line.from === fromLine.from;
|
|
||||||
const isLast = line.from === toLine.from;
|
|
||||||
if (isFirst) {
|
|
||||||
const c = view.coordsAtPos(r.from);
|
|
||||||
if (c) left = c.left - base.left;
|
|
||||||
}
|
|
||||||
if (isLast) {
|
|
||||||
const c = view.coordsAtPos(r.to);
|
|
||||||
right = c ? c.left - base.left : contentLeft - base.left;
|
|
||||||
} else {
|
|
||||||
// not the last line: measure text content width from DOM
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNodeContents(lineEl);
|
|
||||||
const textRect = range.getBoundingClientRect();
|
|
||||||
const textRight = textRect.right - base.left;
|
|
||||||
right = textRight > left ? textRight : left + charWidth;
|
|
||||||
}
|
|
||||||
const w = right! - left;
|
|
||||||
if (w > 0) {
|
|
||||||
markers.push(
|
|
||||||
new RectangleMarker(
|
|
||||||
"cm-vscodeSelection",
|
|
||||||
left,
|
|
||||||
lineRect.top - base.top,
|
|
||||||
w,
|
|
||||||
lineRect.bottom - lineRect.top,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (isLast) break;
|
|
||||||
pos = line.to + 1;
|
|
||||||
if (pos > view.state.doc.length) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return markers;
|
|
||||||
},
|
|
||||||
update(update, _dom) {
|
|
||||||
return update.docChanged || update.selectionSet || update.viewportChanged;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const state = EditorState.create({
|
const state = EditorState.create({
|
||||||
doc: props.modelValue,
|
doc: props.modelValue,
|
||||||
extensions: [
|
extensions: [
|
||||||
@@ -1171,7 +1085,7 @@ onMounted(async () => {
|
|||||||
history(),
|
history(),
|
||||||
foldGutter(),
|
foldGutter(),
|
||||||
drawSelection(),
|
drawSelection(),
|
||||||
vscodeSelectionLayer,
|
vscodeSelectionLayer(),
|
||||||
dropCursor(),
|
dropCursor(),
|
||||||
EditorState.allowMultipleSelections.of(true),
|
EditorState.allowMultipleSelections.of(true),
|
||||||
indentOnInput(),
|
indentOnInput(),
|
||||||
|
|||||||
@@ -5299,7 +5299,7 @@ defineExpose({
|
|||||||
v-else
|
v-else
|
||||||
ref="valueEditorContainer"
|
ref="valueEditorContainer"
|
||||||
data-cell-detail-editor-root
|
data-cell-detail-editor-root
|
||||||
class="min-h-0 flex-1 w-full rounded border overflow-hidden"
|
class="min-h-0 flex-1 w-full rounded border overflow-auto"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-1 mt-2 shrink-0">
|
<div class="flex gap-1 mt-2 shrink-0">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
highlightSpecialChars,
|
highlightSpecialChars,
|
||||||
highlightActiveLine,
|
highlightActiveLine,
|
||||||
} from "@codemirror/view";
|
} from "@codemirror/view";
|
||||||
|
import { vscodeSelectionLayer } from "@/lib/codemirrorVscodeSelectionLayer";
|
||||||
import { json } from "@codemirror/lang-json";
|
import { json } from "@codemirror/lang-json";
|
||||||
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
||||||
import { bracketMatching } from "@codemirror/language";
|
import { bracketMatching } from "@codemirror/language";
|
||||||
@@ -81,6 +82,7 @@ export function useCellDetailEditor(options: UseCellDetailEditorOptions): UseCel
|
|||||||
highlightSpecialChars(),
|
highlightSpecialChars(),
|
||||||
history(),
|
history(),
|
||||||
drawSelection(),
|
drawSelection(),
|
||||||
|
vscodeSelectionLayer(),
|
||||||
dropCursor(),
|
dropCursor(),
|
||||||
highlightActiveLine(),
|
highlightActiveLine(),
|
||||||
EditorView.theme({
|
EditorView.theme({
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { layer, RectangleMarker, type EditorView } from "@codemirror/view";
|
||||||
|
|
||||||
|
function getLineElement(view: EditorView, pos: number): HTMLElement | null {
|
||||||
|
try {
|
||||||
|
const domAt = view.domAtPos(pos);
|
||||||
|
let node: Node | null = domAt.node;
|
||||||
|
while (node) {
|
||||||
|
if (node instanceof HTMLElement && node.classList.contains("cm-line")) return node;
|
||||||
|
node = node.parentElement;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// domAtPos may throw for positions outside the viewport
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function vscodeSelectionLayer() {
|
||||||
|
return layer({
|
||||||
|
above: false,
|
||||||
|
class: "cm-vscodeSelectionLayer",
|
||||||
|
markers(view) {
|
||||||
|
const markers: InstanceType<typeof RectangleMarker>[] = [];
|
||||||
|
const contentRect = view.contentDOM.getBoundingClientRect();
|
||||||
|
const baseRect = view.scrollDOM.getBoundingClientRect();
|
||||||
|
const base = {
|
||||||
|
left: baseRect.left - view.scrollDOM.scrollLeft * view.scaleX,
|
||||||
|
top: baseRect.top - view.scrollDOM.scrollTop * view.scaleY,
|
||||||
|
};
|
||||||
|
const lineElt = view.contentDOM.querySelector(".cm-line");
|
||||||
|
const lineStyle = lineElt && window.getComputedStyle(lineElt);
|
||||||
|
const contentLeft =
|
||||||
|
contentRect.left +
|
||||||
|
(lineStyle ? parseInt(lineStyle.paddingLeft) + Math.min(0, parseInt(lineStyle.textIndent)) : 0);
|
||||||
|
// single character width for empty-line fallback
|
||||||
|
const sampleCoords = view.coordsAtPos(view.viewport.from);
|
||||||
|
const charWidth = sampleCoords ? sampleCoords.right - sampleCoords.left : 8;
|
||||||
|
for (const r of view.state.selection.ranges) {
|
||||||
|
if (r.empty) continue;
|
||||||
|
const fromLine = view.lineBlockAt(r.from);
|
||||||
|
const toLine = view.lineBlockAt(r.to);
|
||||||
|
for (let pos = fromLine.from; pos <= toLine.from; ) {
|
||||||
|
const line = view.lineBlockAt(pos);
|
||||||
|
const lineEl = getLineElement(view, line.from);
|
||||||
|
if (!lineEl) break;
|
||||||
|
const lineRect = lineEl.getBoundingClientRect();
|
||||||
|
let left = contentLeft - base.left;
|
||||||
|
let right: number;
|
||||||
|
const isFirst = line.from === fromLine.from;
|
||||||
|
const isLast = line.from === toLine.from;
|
||||||
|
if (isFirst) {
|
||||||
|
const c = view.coordsAtPos(r.from);
|
||||||
|
if (c) left = c.left - base.left;
|
||||||
|
}
|
||||||
|
if (isLast) {
|
||||||
|
const c = view.coordsAtPos(r.to);
|
||||||
|
right = c ? c.left - base.left : contentLeft - base.left;
|
||||||
|
} else {
|
||||||
|
// not the last line: measure text content width from DOM
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(lineEl);
|
||||||
|
const textRect = range.getBoundingClientRect();
|
||||||
|
const textRight = textRect.right - base.left;
|
||||||
|
right = textRight > left ? textRight : left + charWidth;
|
||||||
|
}
|
||||||
|
const w = right! - left;
|
||||||
|
if (w > 0) {
|
||||||
|
markers.push(
|
||||||
|
new RectangleMarker(
|
||||||
|
"cm-vscodeSelection",
|
||||||
|
left,
|
||||||
|
lineRect.top - base.top,
|
||||||
|
w,
|
||||||
|
lineRect.bottom - lineRect.top,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isLast) break;
|
||||||
|
pos = line.to + 1;
|
||||||
|
if (pos > view.state.doc.length) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return markers;
|
||||||
|
},
|
||||||
|
update(update, _dom) {
|
||||||
|
return update.docChanged || update.selectionSet || update.viewportChanged;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in new issue
Block a user