feat(editor): drag result grid columns into the SQL editor
This commit is contained in:
1 parent
9e4fe8ce56
commit
ab4e747bf9
17 files changed
+911
-25
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createTableReferencePayload, parseTableReferencePayload, tableReferenceInsertText } from "@/lib/editor/queryEditorTableDrop";
|
||||
import { createColumnReferencePayload, createTableReferencePayload, parseTableReferencePayload, tableReferenceInsertText } from "@/lib/editor/queryEditorTableDrop";
|
||||
|
||||
describe("query editor table reference drop", () => {
|
||||
it("inserts a quoted database name for database references", () => {
|
||||
@@ -38,4 +38,108 @@ describe("query editor table reference drop", () => {
|
||||
|
||||
expect(parseTableReferencePayload(JSON.stringify(payload))).toEqual(payload);
|
||||
});
|
||||
|
||||
it("creates a column-only payload without a source table", () => {
|
||||
const payload = createColumnReferencePayload({
|
||||
connectionId: "conn-1",
|
||||
database: "app-db",
|
||||
columnNames: ["id", "order no"],
|
||||
databaseType: "mysql",
|
||||
});
|
||||
|
||||
expect(payload).toEqual({
|
||||
kind: "dbx-table-reference",
|
||||
connectionId: "conn-1",
|
||||
database: "app-db",
|
||||
columnNames: ["id", "order no"],
|
||||
referenceType: "column",
|
||||
databaseType: "mysql",
|
||||
} satisfies Partial<typeof payload>);
|
||||
expect(payload && "tableName" in payload).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects column payloads without connection, database, or names", () => {
|
||||
expect(createColumnReferencePayload({ database: "db", columnNames: ["id"] })).toBeNull();
|
||||
expect(createColumnReferencePayload({ connectionId: "conn-1", columnNames: ["id"] })).toBeNull();
|
||||
expect(createColumnReferencePayload({ connectionId: "conn-1", database: "db", columnNames: [] })).toBeNull();
|
||||
expect(createColumnReferencePayload({ connectionId: "conn-1", database: "db", columnNames: ["", " "] })).toBeNull();
|
||||
});
|
||||
|
||||
it("trims surrounding whitespace from column names instead of inserting it verbatim", () => {
|
||||
const payload = createColumnReferencePayload({
|
||||
connectionId: "conn-1",
|
||||
database: "db",
|
||||
columnNames: [" id ", "name"],
|
||||
})!;
|
||||
expect(payload.columnNames).toEqual(["id", "name"]);
|
||||
});
|
||||
|
||||
it("round-trips multi-column payloads and keeps legacy single-column payloads parseable", () => {
|
||||
const multi = createColumnReferencePayload({
|
||||
connectionId: "conn-1",
|
||||
database: "app-db",
|
||||
schema: "public",
|
||||
columnNames: ["id", "name"],
|
||||
databaseType: "postgres",
|
||||
})!;
|
||||
expect(parseTableReferencePayload(JSON.stringify(multi))).toEqual(multi);
|
||||
|
||||
const legacy = createTableReferencePayload({
|
||||
connectionId: "conn-1",
|
||||
database: "app-db",
|
||||
tableName: "users",
|
||||
columnName: "email",
|
||||
databaseType: "postgres",
|
||||
})!;
|
||||
expect(parseTableReferencePayload(JSON.stringify(legacy))).toEqual(legacy);
|
||||
});
|
||||
|
||||
it("inserts smart-quoted names per selected column joined by comma + newline", () => {
|
||||
// 普通名称裸输出,保留字/含特殊字符才加方言引号
|
||||
const mysql = createColumnReferencePayload({
|
||||
connectionId: "conn-1",
|
||||
database: "app-db",
|
||||
columnNames: ["id", "order no", "order", "created_at"],
|
||||
databaseType: "mysql",
|
||||
})!;
|
||||
expect(tableReferenceInsertText(mysql)).toBe("id,\n`order no`,\n`order`,\ncreated_at");
|
||||
|
||||
const postgres = createColumnReferencePayload({
|
||||
connectionId: "conn-1",
|
||||
database: "app-db",
|
||||
columnNames: ["id", "OrderNo", "select"],
|
||||
databaseType: "postgres",
|
||||
})!;
|
||||
expect(tableReferenceInsertText(postgres)).toBe('id,\n"OrderNo",\n"select"');
|
||||
});
|
||||
|
||||
it("keeps smart quoting for dialects without a dedicated keyword table", () => {
|
||||
// 未知方言用通用保守判定:普通名裸输出,特殊字符走全量引号回退
|
||||
const oracle = createColumnReferencePayload({
|
||||
connectionId: "conn-1",
|
||||
database: "app-db",
|
||||
columnNames: ["id", "order no"],
|
||||
databaseType: "oracle",
|
||||
})!;
|
||||
expect(tableReferenceInsertText(oracle)).toBe('id,\n"order no"');
|
||||
});
|
||||
|
||||
it("bare-quotes sqlserver columns when safe and brackets reserved words", () => {
|
||||
const payload = createColumnReferencePayload({
|
||||
connectionId: "conn-1",
|
||||
database: "app-db",
|
||||
columnNames: ["id", "order", "order no"],
|
||||
databaseType: "sqlserver",
|
||||
})!;
|
||||
expect(tableReferenceInsertText(payload)).toBe("id,\n[order],\n[order no]");
|
||||
});
|
||||
|
||||
it("falls back to the editor database type when the payload omits one", () => {
|
||||
const payload = createColumnReferencePayload({
|
||||
connectionId: "conn-1",
|
||||
database: "app-db",
|
||||
columnNames: ["order no"],
|
||||
})!;
|
||||
expect(tableReferenceInsertText(payload, "mysql")).toBe("`order no`");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { beginTableReferenceDragFeedback, isOverSqlEditorTarget, isPointOverElementRoot } from "@/lib/editor/tableReferenceDragFeedback";
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
document.body.className = "";
|
||||
document.body.style.cursor = "";
|
||||
});
|
||||
|
||||
describe("isOverSqlEditorTarget", () => {
|
||||
it("detects pointers inside the query editor root only", () => {
|
||||
const editor = document.createElement("div");
|
||||
editor.setAttribute("data-query-editor-root", "");
|
||||
const inner = document.createElement("textarea");
|
||||
editor.appendChild(inner);
|
||||
document.body.appendChild(editor);
|
||||
|
||||
document.elementFromPoint = () => inner;
|
||||
expect(isOverSqlEditorTarget(10, 10)).toBe(true);
|
||||
document.elementFromPoint = () => document.body;
|
||||
expect(isOverSqlEditorTarget(10, 10)).toBe(false);
|
||||
});
|
||||
|
||||
it("falls back to geometric bounds when elementFromPoint hits an overlay", () => {
|
||||
const editor = document.createElement("div");
|
||||
editor.setAttribute("data-query-editor-root", "");
|
||||
Object.defineProperty(editor, "getBoundingClientRect", {
|
||||
value: () => ({ left: 0, top: 0, right: 400, bottom: 300 }),
|
||||
});
|
||||
document.body.appendChild(editor);
|
||||
|
||||
// 命中被透明覆盖层拦截时,回退为编辑器包围盒判定。
|
||||
document.elementFromPoint = () => null;
|
||||
expect(isOverSqlEditorTarget(10, 10)).toBe(true);
|
||||
// 覆盖层命中但不落在任何编辑器包围盒内仍为 false。
|
||||
const farEditor = document.createElement("div");
|
||||
farEditor.setAttribute("data-query-editor-root", "");
|
||||
Object.defineProperty(farEditor, "getBoundingClientRect", {
|
||||
value: () => ({ left: 500, top: 500, right: 900, bottom: 800 }),
|
||||
});
|
||||
document.body.appendChild(farEditor);
|
||||
document.elementFromPoint = () => null;
|
||||
expect(isOverSqlEditorTarget(450, 450)).toBe(false);
|
||||
expect(isOverSqlEditorTarget(600, 600)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isPointOverElementRoot", () => {
|
||||
it("requires elementFromPoint to hit inside the given root", () => {
|
||||
const editor = document.createElement("div");
|
||||
const inner = document.createElement("textarea");
|
||||
editor.appendChild(inner);
|
||||
document.body.appendChild(editor);
|
||||
|
||||
document.elementFromPoint = () => inner;
|
||||
expect(isPointOverElementRoot(10, 10, editor)).toBe(true);
|
||||
document.elementFromPoint = () => document.body;
|
||||
expect(isPointOverElementRoot(10, 10, editor)).toBe(false);
|
||||
expect(isPointOverElementRoot(10, 10, null)).toBe(false);
|
||||
});
|
||||
|
||||
it("falls back to the root bounding box when elementFromPoint hits an overlay", () => {
|
||||
const editor = document.createElement("div");
|
||||
Object.defineProperty(editor, "getBoundingClientRect", {
|
||||
value: () => ({ left: 0, top: 0, right: 400, bottom: 300 }),
|
||||
});
|
||||
document.body.appendChild(editor);
|
||||
|
||||
// 覆盖层拦截时回退为传入根节点的包围盒判定。
|
||||
const overlay = document.createElement("div");
|
||||
document.elementFromPoint = () => overlay;
|
||||
expect(isPointOverElementRoot(10, 10, editor)).toBe(true);
|
||||
expect(isPointOverElementRoot(450, 450, editor)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("beginTableReferenceDragFeedback", () => {
|
||||
it("shows a following chip and restores body state on end", () => {
|
||||
const feedback = beginTableReferenceDragFeedback("id, name 等 3 列");
|
||||
const chip = document.querySelector<HTMLElement>("[data-table-reference-drag-chip]");
|
||||
expect(chip?.textContent).toBe("id, name 等 3 列");
|
||||
expect(document.body.style.cursor).toBe("copy");
|
||||
expect(chip?.style.visibility).toBe("hidden");
|
||||
|
||||
feedback.update(40, 60);
|
||||
expect(chip?.style.visibility).toBe("visible");
|
||||
expect(Number.parseFloat(chip!.style.left)).toBeGreaterThanOrEqual(8);
|
||||
expect(Number.parseFloat(chip!.style.top)).toBeGreaterThanOrEqual(8);
|
||||
|
||||
feedback.end();
|
||||
expect(document.querySelector("[data-table-reference-drag-chip]")).toBeNull();
|
||||
expect(document.body.style.cursor).toBe("");
|
||||
});
|
||||
|
||||
it("clamps the chip inside the viewport", () => {
|
||||
const feedback = beginTableReferenceDragFeedback("col");
|
||||
const chip = document.querySelector<HTMLElement>("[data-table-reference-drag-chip]")!;
|
||||
feedback.update(window.innerWidth - 2, window.innerHeight - 2);
|
||||
const left = Number.parseFloat(chip.style.left);
|
||||
const top = Number.parseFloat(chip.style.top);
|
||||
expect(left + chip.getBoundingClientRect().width).toBeLessThanOrEqual(window.innerWidth);
|
||||
expect(top + chip.getBoundingClientRect().height).toBeLessThanOrEqual(window.innerHeight);
|
||||
feedback.end();
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,41 @@
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
import { qualifiedTableName, quoteTableIdentifier } from "@/lib/table/tableSelectSql";
|
||||
import { requiresMysqlIdentifierQuote, requiresPostgresIdentifierQuote } from "@/lib/sql/sqlIdentifier.ts";
|
||||
|
||||
/** 智能引号使用反引号的方言族。 */
|
||||
const SMART_QUOTE_BACKTICK_TYPES = new Set<DatabaseType>(["mysql", "clickhouse", "hive", "kyuubi", "impala", "spark", "databricks", "databend", "tdengine", "access", "doris", "starrocks", "goldendb"]);
|
||||
/** 智能引号使用双引号的方言族(SQL Server 方括号除外)。 */
|
||||
const SMART_QUOTE_DOUBLE_TYPES = new Set<DatabaseType>(["postgres", "gaussdb", "opengauss"]);
|
||||
/** SQL Server 族智能引号使用方括号。 */
|
||||
const SMART_QUOTE_BRACKET_TYPES = new Set<DatabaseType>(["sqlserver"]);
|
||||
|
||||
/**
|
||||
* 列引用插入的按需引号:普通名称(合法标识符字符、非保留字)裸输出,
|
||||
* 保留字或含特殊字符时按方言加引号。表名/库名仍走全量引号。
|
||||
*
|
||||
* MySQL/PG/SQL Server 族用各自的保留字判定;其余方言无专属保留字表,
|
||||
* 用「严格标识符正则 + PG/MySQL 保留字并集」做通用保守判定。
|
||||
*/
|
||||
function quoteColumnReferenceName(databaseType: DatabaseType | undefined, name: string): string {
|
||||
if (databaseType && SMART_QUOTE_BACKTICK_TYPES.has(databaseType)) {
|
||||
return requiresMysqlIdentifierQuote(name) ? `\`${name.replace(/`/g, "``")}\`` : name;
|
||||
}
|
||||
if (databaseType && SMART_QUOTE_DOUBLE_TYPES.has(databaseType)) {
|
||||
// PG 族裸标识符会折叠为小写,混合大小写必须加引号保留原样。
|
||||
return requiresPostgresIdentifierQuote(name) ? `"${name.replace(/"/g, '""')}"` : name;
|
||||
}
|
||||
if (databaseType && SMART_QUOTE_BRACKET_TYPES.has(databaseType)) {
|
||||
return requiresPostgresIdentifierQuote(name) ? `[${name.replace(/\]/g, "]]")}]` : name;
|
||||
}
|
||||
// 其余方言:引号格式仍由 quoteTableIdentifier 按方言决定,是否加引号
|
||||
// 用通用保守判定(严格标识符正则 + PG/MySQL 保留字并集)。
|
||||
return requiresMysqlIdentifierQuote(name) ? quoteTableIdentifier(databaseType, name) : name;
|
||||
}
|
||||
|
||||
export const DBX_TABLE_REFERENCE_MIME = "application/x-dbx-table-reference";
|
||||
export const DBX_TABLE_REFERENCE_DROP_EVENT = "dbx-table-reference-drop";
|
||||
export const DBX_TABLE_REFERENCE_HOVER_EVENT = "dbx-table-reference-hover";
|
||||
export const DBX_TABLE_REFERENCE_DRAG_END_EVENT = "dbx-table-reference-drag-end";
|
||||
|
||||
export interface QueryEditorTableReferencePayload {
|
||||
kind: "dbx-table-reference";
|
||||
@@ -11,6 +44,8 @@ export interface QueryEditorTableReferencePayload {
|
||||
schema?: string;
|
||||
tableName?: string;
|
||||
columnName?: string;
|
||||
/** 多个结果列一起拖入时按选择顺序排列;单列沿用 columnName。 */
|
||||
columnNames?: string[];
|
||||
referenceType?: "database" | "table" | "column";
|
||||
databaseType?: DatabaseType;
|
||||
driverProfile?: string;
|
||||
@@ -22,6 +57,23 @@ export interface QueryEditorTableReferenceDropDetail {
|
||||
clientY: number;
|
||||
}
|
||||
|
||||
export interface QueryEditorTableReferenceHoverDetail {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
}
|
||||
|
||||
function normalizeColumnNames(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.map((name) => (typeof name === "string" ? name.trim() : "")).filter((name) => name.length > 0);
|
||||
}
|
||||
|
||||
/** schema/databaseType/driverProfile 为可选元数据,payload 各构造路径共用同一拷贝规则。 */
|
||||
function applyOptionalReferenceMeta(payload: QueryEditorTableReferencePayload, source: Partial<QueryEditorTableReferencePayload>) {
|
||||
if (typeof source.schema === "string" && source.schema) payload.schema = source.schema;
|
||||
if (source.databaseType) payload.databaseType = source.databaseType;
|
||||
if (typeof source.driverProfile === "string" && source.driverProfile) payload.driverProfile = source.driverProfile;
|
||||
}
|
||||
|
||||
let activeTableReferencePayload: QueryEditorTableReferencePayload | null = null;
|
||||
|
||||
export function createTableReferencePayload(options: {
|
||||
@@ -51,9 +103,21 @@ export function createTableReferencePayload(options: {
|
||||
payload.columnName = options.columnName;
|
||||
payload.referenceType = "column";
|
||||
}
|
||||
if (options.schema) payload.schema = options.schema;
|
||||
if (options.databaseType) payload.databaseType = options.databaseType;
|
||||
if (options.driverProfile) payload.driverProfile = options.driverProfile;
|
||||
applyOptionalReferenceMeta(payload, options);
|
||||
return payload;
|
||||
}
|
||||
|
||||
export function createColumnReferencePayload(options: { connectionId?: string; database?: string; schema?: string; columnNames?: readonly (string | undefined | null)[]; databaseType?: DatabaseType }): QueryEditorTableReferencePayload | null {
|
||||
const columnNames = normalizeColumnNames(options.columnNames);
|
||||
if (!options.connectionId || options.database == null || columnNames.length === 0) return null;
|
||||
const payload: QueryEditorTableReferencePayload = {
|
||||
kind: "dbx-table-reference",
|
||||
connectionId: options.connectionId,
|
||||
database: options.database,
|
||||
referenceType: "column",
|
||||
columnNames,
|
||||
};
|
||||
applyOptionalReferenceMeta(payload, options as Partial<QueryEditorTableReferencePayload>);
|
||||
return payload;
|
||||
}
|
||||
|
||||
@@ -79,7 +143,20 @@ export function parseTableReferencePayload(value: string | undefined | null): Qu
|
||||
if (typeof parsed.driverProfile === "string" && parsed.driverProfile) payload.driverProfile = parsed.driverProfile;
|
||||
return payload;
|
||||
}
|
||||
if (typeof parsed.tableName !== "string" || !parsed.tableName) return null;
|
||||
if (typeof parsed.tableName !== "string" || !parsed.tableName) {
|
||||
// 无来源表的纯列引用(如从查询结果列头拖入),tableName 非必需。
|
||||
const columnNames = normalizeColumnNames(parsed.columnNames);
|
||||
if (columnNames.length === 0) return null;
|
||||
const payload: QueryEditorTableReferencePayload = {
|
||||
kind: "dbx-table-reference",
|
||||
connectionId: parsed.connectionId,
|
||||
database: parsed.database,
|
||||
referenceType: "column",
|
||||
columnNames,
|
||||
};
|
||||
applyOptionalReferenceMeta(payload, parsed);
|
||||
return payload;
|
||||
}
|
||||
const columnName = typeof parsed.columnName === "string" && parsed.columnName ? parsed.columnName : undefined;
|
||||
const referenceType = parsed.referenceType === "column" || columnName ? "column" : "table";
|
||||
if (referenceType === "column" && !columnName) return null;
|
||||
@@ -93,9 +170,7 @@ export function parseTableReferencePayload(value: string | undefined | null): Qu
|
||||
payload.columnName = columnName;
|
||||
payload.referenceType = "column";
|
||||
}
|
||||
if (typeof parsed.schema === "string" && parsed.schema) payload.schema = parsed.schema;
|
||||
if (parsed.databaseType) payload.databaseType = parsed.databaseType;
|
||||
if (typeof parsed.driverProfile === "string" && parsed.driverProfile) payload.driverProfile = parsed.driverProfile;
|
||||
applyOptionalReferenceMeta(payload, parsed);
|
||||
return payload;
|
||||
} catch {
|
||||
return null;
|
||||
@@ -128,13 +203,22 @@ export function createTableReferenceDropEvent(detail: QueryEditorTableReferenceD
|
||||
return new CustomEvent<QueryEditorTableReferenceDropDetail>(DBX_TABLE_REFERENCE_DROP_EVENT, { detail });
|
||||
}
|
||||
|
||||
export function createTableReferenceHoverEvent(detail: QueryEditorTableReferenceHoverDetail) {
|
||||
return new CustomEvent<QueryEditorTableReferenceHoverDetail>(DBX_TABLE_REFERENCE_HOVER_EVENT, { detail });
|
||||
}
|
||||
|
||||
export function createTableReferenceDragEndEvent(): Event {
|
||||
return new Event(DBX_TABLE_REFERENCE_DRAG_END_EVENT);
|
||||
}
|
||||
|
||||
export function tableReferenceInsertText(payload: QueryEditorTableReferencePayload, fallbackDatabaseType?: DatabaseType): string {
|
||||
const databaseType = payload.databaseType ?? fallbackDatabaseType;
|
||||
if (payload.referenceType === "database") {
|
||||
return quoteTableIdentifier(databaseType, payload.database);
|
||||
}
|
||||
if (payload.referenceType === "column" && payload.columnName) {
|
||||
return quoteTableIdentifier(databaseType, payload.columnName);
|
||||
const columnNames = payload.columnNames?.length ? payload.columnNames : payload.columnName ? [payload.columnName] : [];
|
||||
if (payload.referenceType === "column" && columnNames.length > 0) {
|
||||
return columnNames.map((name) => quoteColumnReferenceName(databaseType, name)).join(",\n");
|
||||
}
|
||||
const tableName = payload.tableName || payload.database;
|
||||
return qualifiedTableName({
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 表引用拖拽(侧边栏表/列、查询结果列头)共用的指针模拟拖拽反馈:
|
||||
* 跟随鼠标的 chip 浮层 + body copy 光标。落点探测与插入见 queryEditorTableDrop.ts。
|
||||
*/
|
||||
|
||||
export const QUERY_EDITOR_DROP_TARGET_SELECTOR = "[data-query-editor-root]";
|
||||
|
||||
/** 拖拽期间加在 body 上,禁用文本选择(样式见 globals.css)。 */
|
||||
export const TABLE_REFERENCE_DRAGGING_CLASS = "dbx-table-reference-dragging";
|
||||
|
||||
const CHIP_OFFSET_X = 14;
|
||||
const CHIP_OFFSET_Y = 18;
|
||||
const CHIP_VIEWPORT_MARGIN = 8;
|
||||
|
||||
/**
|
||||
* elementFromPoint 命中 root 内部才算命中;elementFromPoint 可能被透明覆盖层
|
||||
* 拦截(面板层等),此时回退为 root 包围盒的几何包含判定。
|
||||
*/
|
||||
export function isPointOverElementRoot(clientX: number, clientY: number, root: Element | null | undefined, doc: Document = document): boolean {
|
||||
if (!root) return false;
|
||||
const target = doc.elementFromPoint(clientX, clientY);
|
||||
if (target instanceof Element && root.contains(target)) return true;
|
||||
const rect = root.getBoundingClientRect();
|
||||
return clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom;
|
||||
}
|
||||
|
||||
export function isOverSqlEditorTarget(clientX: number, clientY: number, doc: Document = document): boolean {
|
||||
for (const root of doc.querySelectorAll(QUERY_EDITOR_DROP_TARGET_SELECTOR)) {
|
||||
if (isPointOverElementRoot(clientX, clientY, root, doc)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 多列摘要由 i18n 插值完成:调用方以 { names: 前两个列名, count: 总数 } 调 t("grid.columnDragChipMany", ...)。 */
|
||||
|
||||
export interface TableReferenceDragFeedback {
|
||||
update(clientX: number, clientY: number): void;
|
||||
end(): void;
|
||||
}
|
||||
|
||||
export function beginTableReferenceDragFeedback(label: string, doc: Document = document): TableReferenceDragFeedback {
|
||||
const chip = doc.createElement("div");
|
||||
chip.dataset.tableReferenceDragChip = "";
|
||||
chip.setAttribute("aria-hidden", "true");
|
||||
chip.className = "pointer-events-none fixed z-[9999] max-w-72 truncate rounded-md border border-border bg-popover px-2 py-1 font-mono text-xs leading-4 text-popover-foreground shadow-lg";
|
||||
chip.textContent = label;
|
||||
chip.style.visibility = "hidden";
|
||||
doc.body.appendChild(chip);
|
||||
doc.body.classList.add(TABLE_REFERENCE_DRAGGING_CLASS);
|
||||
doc.body.style.cursor = "copy";
|
||||
|
||||
const viewport = doc.defaultView ?? window;
|
||||
const update = (clientX: number, clientY: number) => {
|
||||
if (chip.style.visibility === "hidden") chip.style.visibility = "visible";
|
||||
const rect = chip.getBoundingClientRect();
|
||||
const x = Math.min(clientX + CHIP_OFFSET_X, viewport.innerWidth - rect.width - CHIP_VIEWPORT_MARGIN);
|
||||
const y = Math.min(clientY + CHIP_OFFSET_Y, viewport.innerHeight - rect.height - CHIP_VIEWPORT_MARGIN);
|
||||
chip.style.left = `${Math.max(CHIP_VIEWPORT_MARGIN, x)}px`;
|
||||
chip.style.top = `${Math.max(CHIP_VIEWPORT_MARGIN, y)}px`;
|
||||
};
|
||||
|
||||
return {
|
||||
update,
|
||||
end() {
|
||||
chip.remove();
|
||||
doc.body.classList.remove(TABLE_REFERENCE_DRAGGING_CLASS);
|
||||
if (doc.body.style.cursor === "copy") doc.body.style.cursor = "";
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user