mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-30 16:21:39 +00:00
feat(grid): add clipboard shortcuts
This commit is contained in:
@@ -1337,6 +1337,10 @@ function focusScrollerWithoutScrolling() {
|
||||
el.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
function focusGridWithoutScrolling() {
|
||||
gridRef.value?.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
function restoreScrollAcrossFrames(restoreScroll: () => void) {
|
||||
if (cancelScrollRestoreFrame) cancelAnimationFrame(cancelScrollRestoreFrame);
|
||||
restoreScroll();
|
||||
@@ -1427,6 +1431,35 @@ function commitEdit() {
|
||||
editingCell.value = null;
|
||||
}
|
||||
|
||||
function applyCellValue(rowId: number, col: number, value: string | null) {
|
||||
const item = getRowItem(rowId);
|
||||
if (!item || item.isDeleted) return;
|
||||
|
||||
if (item.isNew && item.newIndex !== undefined) {
|
||||
const oldVal = newRows.value[item.newIndex]?.[col];
|
||||
newRows.value[item.newIndex][col] = value === null ? null : coerceCellValue(value, oldVal);
|
||||
newRows.value = [...newRows.value];
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.sourceIndex === undefined) return;
|
||||
|
||||
const oldVal = props.result.rows[item.sourceIndex]?.[col];
|
||||
const newVal = value === null ? null : coerceCellValue(value, oldVal);
|
||||
if (newVal !== oldVal) {
|
||||
if (!dirtyRows.value.has(item.sourceIndex)) dirtyRows.value.set(item.sourceIndex, new Map());
|
||||
dirtyRows.value.get(item.sourceIndex)!.set(col, newVal);
|
||||
if (useTransaction.value && !transactionActive.value) {
|
||||
enterTransaction();
|
||||
}
|
||||
} else {
|
||||
const rowChanges = dirtyRows.value.get(item.sourceIndex);
|
||||
rowChanges?.delete(col);
|
||||
if (rowChanges?.size === 0) dirtyRows.value.delete(item.sourceIndex);
|
||||
}
|
||||
dirtyRows.value = new Map(dirtyRows.value);
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
const restoreScroll = preserveScrollPosition();
|
||||
isCancelling = true;
|
||||
@@ -1602,6 +1635,7 @@ function beginCellSelection(rowIndex: number, colIndex: number, event: MouseEven
|
||||
if (event.button !== 0) return;
|
||||
if (editingCell.value) return;
|
||||
event.preventDefault();
|
||||
focusGridWithoutScrolling();
|
||||
selectSingleCell(rowIndex, colIndex);
|
||||
isSelectingCells.value = true;
|
||||
if (showTranspose.value) transposeRowIndex.value = rowIndex;
|
||||
@@ -1632,6 +1666,80 @@ function copySelectionTsv() {
|
||||
copyText(formatSelectionAsTsv(selectedCells.value));
|
||||
}
|
||||
|
||||
function eventTargetAllowsNativeClipboard(event: KeyboardEvent): boolean {
|
||||
const target = event.target as HTMLElement | null;
|
||||
return !!target?.closest("input, textarea, [contenteditable='true'], [role='textbox']");
|
||||
}
|
||||
|
||||
function clipboardShortcut(event: KeyboardEvent, key: string): boolean {
|
||||
return (event.metaKey || event.ctrlKey) && !event.altKey && event.key.toLowerCase() === key;
|
||||
}
|
||||
|
||||
function selectedRangeStart(): CellPosition | null {
|
||||
const range = selectedRange.value;
|
||||
if (!range) return null;
|
||||
return { rowIndex: range.startRow, colIndex: range.startCol };
|
||||
}
|
||||
|
||||
function parseClipboardTable(text: string): string[][] {
|
||||
const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n$/, "");
|
||||
if (!normalized) return [[""]];
|
||||
return normalized.split("\n").map((row) => row.split("\t"));
|
||||
}
|
||||
|
||||
async function pasteClipboardIntoSelection() {
|
||||
if (!props.editable) return;
|
||||
const start = selectedRangeStart();
|
||||
if (!start) return;
|
||||
|
||||
const text = await navigator.clipboard.readText();
|
||||
const rows = parseClipboardTable(text);
|
||||
rows.forEach((row, rowOffset) => {
|
||||
const item = displayItems.value[start.rowIndex + rowOffset];
|
||||
if (!item) return;
|
||||
row.forEach((value, colOffset) => {
|
||||
const col = start.colIndex + colOffset;
|
||||
if (col >= props.result.columns.length) return;
|
||||
applyCellValue(item.id, col, value);
|
||||
});
|
||||
});
|
||||
toast(t("grid.pasted"));
|
||||
}
|
||||
|
||||
function cutSelection() {
|
||||
if (!props.editable || !selectedRange.value) return;
|
||||
copySelectionTsv();
|
||||
const range = selectedRange.value;
|
||||
for (let rowIndex = range.startRow; rowIndex <= range.endRow; rowIndex++) {
|
||||
const item = displayItems.value[rowIndex];
|
||||
if (!item) continue;
|
||||
for (let col = range.startCol; col <= range.endCol; col++) {
|
||||
applyCellValue(item.id, col, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function onGridKeydown(event: KeyboardEvent) {
|
||||
if (eventTargetAllowsNativeClipboard(event)) return;
|
||||
if (clipboardShortcut(event, "c")) {
|
||||
if (!hasCellSelection.value) return;
|
||||
event.preventDefault();
|
||||
copySelectionTsv();
|
||||
return;
|
||||
}
|
||||
if (clipboardShortcut(event, "x")) {
|
||||
if (!props.editable || !hasCellSelection.value) return;
|
||||
event.preventDefault();
|
||||
cutSelection();
|
||||
return;
|
||||
}
|
||||
if (clipboardShortcut(event, "v")) {
|
||||
if (!props.editable || !hasCellSelection.value) return;
|
||||
event.preventDefault();
|
||||
await pasteClipboardIntoSelection();
|
||||
}
|
||||
}
|
||||
|
||||
function copySelectionCsv() {
|
||||
if (!hasCellSelection.value) return;
|
||||
copyText(formatSelectionAsCsv(selectedCells.value));
|
||||
@@ -2004,7 +2112,13 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="gridRef" class="h-full flex flex-col overflow-hidden" :style="columnVars">
|
||||
<div
|
||||
ref="gridRef"
|
||||
class="h-full flex flex-col overflow-hidden outline-none"
|
||||
:style="columnVars"
|
||||
tabindex="0"
|
||||
@keydown="onGridKeydown"
|
||||
>
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger as-child>
|
||||
<div v-if="hasData || canUseWhereSearch" class="flex-1 flex flex-col overflow-hidden">
|
||||
|
||||
@@ -234,6 +234,7 @@ export default {
|
||||
sortDescending: "Sort Descending",
|
||||
clearSort: "Clear Sort",
|
||||
copied: "Copied!",
|
||||
pasted: "Pasted!",
|
||||
search: "Search...",
|
||||
searchOrWhere: "Search, or enter a WHERE clause...",
|
||||
applyWhere: "Apply WHERE",
|
||||
|
||||
@@ -233,6 +233,7 @@ export default {
|
||||
sortDescending: "降序排序",
|
||||
clearSort: "清除排序",
|
||||
copied: "已复制!",
|
||||
pasted: "已粘贴!",
|
||||
search: "搜索...",
|
||||
searchOrWhere: "搜索,或输入 WHERE 条件...",
|
||||
applyWhere: "应用 WHERE",
|
||||
|
||||
@@ -68,9 +68,7 @@ function sqlValue(value: GridCellValue): string {
|
||||
}
|
||||
|
||||
export function formatSelectionAsTsv(selection: SelectionData): string {
|
||||
const header = selection.columns.join("\t");
|
||||
const body = selection.rows.map((row) => row.map(displayValue).join("\t")).join("\n");
|
||||
return [header, body].filter(Boolean).join("\n");
|
||||
return selection.rows.map((row) => row.map(displayValue).join("\t")).join("\n");
|
||||
}
|
||||
|
||||
export function formatSelectionAsCsv(selection: SelectionData): string {
|
||||
|
||||
Reference in New Issue
Block a user