Files
mnote/wolai-frontend/src/lib/file-tree/clipboard.ts
T

128 lines
3.9 KiB
TypeScript
Raw Normal View History

2026-01-07 18:38:56 +08:00
"use client";
import type { FileTreeRow } from "./types";
import { parseFileTreeRowId } from "./types";
export type FileTreeClipboardAction = "copy" | "cut";
2026-01-07 18:38:56 +08:00
export type FileTreeClipboardPayloadV1 = {
type: "mnote-file-tree";
version: 1;
action: FileTreeClipboardAction;
rowIds: string[];
};
const PREFIX = "mnote-file-tree-clipboard:v1:";
let memoryClipboardText: string | null = null;
function encodeBase64(text: string): string {
const bytes = new TextEncoder().encode(text);
let binary = "";
bytes.forEach((b) => {
binary += String.fromCharCode(b);
});
return btoa(binary);
}
function decodeBase64(base64: string): string {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i += 1) {
bytes[i] = binary.charCodeAt(i);
}
return new TextDecoder().decode(bytes);
}
export function encodeFileTreeClipboardPayload(payload: FileTreeClipboardPayloadV1): string {
return `${PREFIX}${encodeBase64(JSON.stringify(payload))}`;
}
export function decodeFileTreeClipboardPayload(text: string): FileTreeClipboardPayloadV1 | null {
if (!text || !text.startsWith(PREFIX)) return null;
const base64 = text.slice(PREFIX.length);
try {
const raw = decodeBase64(base64);
const parsed = JSON.parse(raw) as Partial<FileTreeClipboardPayloadV1>;
if (parsed?.type !== "mnote-file-tree" || parsed.version !== 1) return null;
if (parsed.action !== "copy" && parsed.action !== "cut") return null;
2026-01-07 18:38:56 +08:00
if (!Array.isArray(parsed.rowIds) || parsed.rowIds.some((id) => typeof id !== "string")) return null;
return parsed as FileTreeClipboardPayloadV1;
} catch {
return null;
}
}
export async function writeFileTreeClipboardPayload(payload: FileTreeClipboardPayloadV1): Promise<void> {
const text = encodeFileTreeClipboardPayload(payload);
memoryClipboardText = text;
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text);
} catch {
// ignore, fallback to memory
}
}
}
export async function readFileTreeClipboardPayload(): Promise<FileTreeClipboardPayloadV1 | null> {
let text: string | null = memoryClipboardText;
if (typeof navigator !== "undefined" && navigator.clipboard?.readText) {
try {
text = await navigator.clipboard.readText();
} catch {
// ignore, fallback to memory
}
}
if (!text) return null;
return decodeFileTreeClipboardPayload(text);
}
export async function clearFileTreeClipboardPayload(): Promise<void> {
memoryClipboardText = null;
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText("");
} catch {
// ignore, memory fallback 已清空
}
}
}
2026-01-07 18:38:56 +08:00
export function isTextInputTarget(target: EventTarget | null): boolean {
const el = target as HTMLElement | null;
if (!el) return false;
if (el.isContentEditable) return true;
const contentEditable = el.getAttribute?.("contenteditable");
if (contentEditable && contentEditable.toLowerCase() !== "false") {
return true;
}
const tag = el.tagName?.toLowerCase();
return tag === "input" || tag === "textarea" || el.getAttribute?.("role") === "textbox";
}
export function inferPasteTargetDocId({
focusedRowId,
rowById,
activeDocId,
}: {
focusedRowId: string | null;
rowById: Map<string, FileTreeRow>;
activeDocId: string | null;
}): string | null {
if (focusedRowId) {
const parsed = parseFileTreeRowId(focusedRowId);
if (parsed?.kind === "doc") return parsed.docId;
if (parsed?.kind === "index") return parsed.docId;
2026-01-11 12:35:53 +08:00
if (parsed?.kind === "asset-folder") {
const row = rowById.get(focusedRowId);
return row?.kind === "asset-folder" ? row.docId : null;
}
2026-01-07 18:38:56 +08:00
if (parsed?.kind === "asset") {
const row = rowById.get(focusedRowId);
return row?.kind === "asset" ? row.docId : null;
}
}
return activeDocId || null;
}