Skip to content

Commit 327d167

Browse files
authored
fix terminal paste code (#2596)
lots of work to handle DataTransferItems with a better heuristic: * first process images * otherwise find the *first* text/plain (or text) item * otherwise find a text/html item (and extract the textContent using the DOM) * otherwise find a generic paste item * otherwise fail the paste this should fix the html => to the terminal issue.
1 parent 99a2576 commit 327d167

File tree

1 file changed

+173
-48
lines changed

1 file changed

+173
-48
lines changed

frontend/app/view/term/termutil.ts

Lines changed: 173 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { TabRpcClient } from "@/app/store/wshrpcutil";
77
import base64 from "base64-js";
88
import { colord } from "colord";
99

10+
export type GenClipboardItem = { text?: string; image?: Blob };
11+
1012
function applyTransparencyToColor(hexColor: string, transparency: number): string {
1113
const alpha = 1 - transparency; // transparency is already 0-1
1214
return colord(hexColor).alpha(alpha).toHex();
@@ -97,58 +99,187 @@ export async function createTempFileFromBlob(blob: Blob): Promise<string> {
9799
}
98100

99101
/**
100-
* Extracts text or image data from a clipboard item.
101-
* Prioritizes images over text - if an image is found, only the image is returned.
102+
* Extracts text or image data from a ClipboardItem using prioritized extraction modes.
103+
*
104+
* Mode 1 (Images): If image types are present, returns the first image
105+
* Mode 2 (Plain Text): If text/plain, text/plain;*, or "text" is found
106+
* Mode 3 (HTML): If text/html is found, extracts text content via DOM
107+
* Mode 4 (Generic): If empty string or null type exists
102108
*
103-
* @param item - Either a DataTransferItem or ClipboardItem
104-
* @returns Object with either text or image, or null if neither could be extracted
109+
* @param item - ClipboardItem to extract data from
110+
* @returns Object with either text or image, or null if no supported content found
105111
*/
106-
export async function extractClipboardData(
107-
item: DataTransferItem | ClipboardItem
108-
): Promise<{ text?: string; image?: Blob } | null> {
109-
// Check if it's a DataTransferItem (has 'kind' property)
110-
if ("kind" in item) {
111-
const dataTransferItem = item as DataTransferItem;
112-
113-
// Check for image first
114-
if (dataTransferItem.type.startsWith("image/")) {
115-
const blob = dataTransferItem.getAsFile();
116-
if (blob) {
117-
return { image: blob };
118-
}
112+
export async function extractClipboardData(item: ClipboardItem): Promise<GenClipboardItem | null> {
113+
// Mode #1: Check for image first
114+
const imageTypes = item.types.filter((type) => type.startsWith("image/"));
115+
if (imageTypes.length > 0) {
116+
const blob = await item.getType(imageTypes[0]);
117+
return { image: blob };
118+
}
119+
120+
// Mode #2: Try text/plain, text/plain;*, or "text"
121+
const plainTextType = item.types.find((t) => t === "text" || t === "text/plain" || t.startsWith("text/plain;"));
122+
if (plainTextType) {
123+
const blob = await item.getType(plainTextType);
124+
const text = await blob.text();
125+
return text ? { text } : null;
126+
}
127+
128+
// Mode #3: Try text/html - extract text via DOM
129+
const htmlType = item.types.find((t) => t === "text/html" || t.startsWith("text/html;"));
130+
if (htmlType) {
131+
const blob = await item.getType(htmlType);
132+
const html = await blob.text();
133+
if (!html) {
134+
return null;
119135
}
136+
const tempDiv = document.createElement("div");
137+
tempDiv.innerHTML = html;
138+
const text = tempDiv.textContent || "";
139+
return text ? { text } : null;
140+
}
120141

121-
// If not an image, try text
122-
if (dataTransferItem.kind === "string") {
123-
return new Promise((resolve) => {
124-
dataTransferItem.getAsString((text) => {
125-
resolve(text ? { text } : null);
126-
});
127-
});
142+
// Mode #4: Try empty string or null type
143+
const genericType = item.types.find((t) => t === "");
144+
if (genericType != null) {
145+
const blob = await item.getType(genericType);
146+
const text = await blob.text();
147+
return text ? { text } : null;
148+
}
149+
150+
return null;
151+
}
152+
153+
/**
154+
* Finds the first DataTransferItem matching the specified kind and type predicate.
155+
*
156+
* @param items - The DataTransferItemList to search
157+
* @param kind - The kind to match ("file" or "string")
158+
* @param typePredicate - Function that returns true if the type matches
159+
* @returns The first matching DataTransferItem, or null if none found
160+
*/
161+
function findFirstDataTransferItem(
162+
items: DataTransferItemList,
163+
kind: string,
164+
typePredicate: (type: string) => boolean
165+
): DataTransferItem | null {
166+
for (let i = 0; i < items.length; i++) {
167+
const item = items[i];
168+
if (item.kind === kind && typePredicate(item.type)) {
169+
return item;
128170
}
171+
}
172+
return null;
173+
}
129174

130-
return null;
175+
/**
176+
* Finds all DataTransferItems matching the specified kind and type predicate.
177+
*
178+
* @param items - The DataTransferItemList to search
179+
* @param kind - The kind to match ("file" or "string")
180+
* @param typePredicate - Function that returns true if the type matches
181+
* @returns Array of matching DataTransferItems
182+
*/
183+
function findAllDataTransferItems(
184+
items: DataTransferItemList,
185+
kind: string,
186+
typePredicate: (type: string) => boolean
187+
): DataTransferItem[] {
188+
const results: DataTransferItem[] = [];
189+
for (let i = 0; i < items.length; i++) {
190+
const item = items[i];
191+
if (item.kind === kind && typePredicate(item.type)) {
192+
results.push(item);
193+
}
131194
}
195+
return results;
196+
}
132197

133-
// It's a ClipboardItem
134-
const clipboardItem = item as ClipboardItem;
198+
/**
199+
* Extracts clipboard data from a DataTransferItemList using prioritized extraction modes.
200+
*
201+
* The function uses a hierarchical approach to determine what data to extract:
202+
*
203+
* Mode 1 (Image Files): If any image file items are present, extracts only image files
204+
* - Returns array of {image: Blob} for each image/* MIME type
205+
* - Ignores all non-image items when image files are present
206+
* - Non-image files (e.g., PDFs) allow fallthrough to text modes
207+
*
208+
* Mode 2 (Plain Text): If text/plain is found (and no image files)
209+
* - Returns single-item array with first text/plain content as {text: string}
210+
* - Matches: "text", "text/plain", or types starting with "text/plain"
211+
*
212+
* Mode 3 (HTML): If text/html is found (and no image files or plain text)
213+
* - Extracts text content from first HTML item using DOM parsing
214+
* - Returns single-item array as {text: string}
215+
*
216+
* Mode 4 (Generic String): If string item with empty/null type exists
217+
* - Returns first string item with no type identifier
218+
* - Returns single-item array as {text: string}
219+
*
220+
* @param items - The DataTransferItemList to process
221+
* @returns Array of GenClipboardItem objects, or empty array if no supported content found
222+
*/
223+
export async function extractDataTransferItems(items: DataTransferItemList): Promise<GenClipboardItem[]> {
224+
// Mode #1: If image files are present, only extract image files
225+
const imageFiles = findAllDataTransferItems(items, "file", (type) => type.startsWith("image/"));
226+
if (imageFiles.length > 0) {
227+
const results: GenClipboardItem[] = [];
228+
for (const item of imageFiles) {
229+
const blob = item.getAsFile();
230+
if (blob) {
231+
results.push({ image: blob });
232+
}
233+
}
234+
return results;
235+
}
135236

136-
// Check for image first
137-
const imageTypes = clipboardItem.types.filter((type) => type.startsWith("image/"));
138-
if (imageTypes.length > 0) {
139-
const blob = await clipboardItem.getType(imageTypes[0]);
140-
return { image: blob };
237+
// Mode #2: If text/plain is present, only extract the first text/plain
238+
const plainTextItem = findFirstDataTransferItem(
239+
items,
240+
"string",
241+
(type) => type === "text" || type === "text/plain" || type.startsWith("text/plain;")
242+
);
243+
if (plainTextItem) {
244+
return new Promise((resolve) => {
245+
plainTextItem.getAsString((text) => {
246+
resolve(text ? [{ text }] : []);
247+
});
248+
});
141249
}
142250

143-
// If not an image, try text
144-
const textType = clipboardItem.types.find((t) => ["text/plain", "text/html", "text/rtf"].includes(t));
145-
if (textType) {
146-
const blob = await clipboardItem.getType(textType);
147-
const text = await blob.text();
148-
return text ? { text } : null;
251+
// Mode #3: If text/html is present, extract text from first HTML
252+
const htmlItem = findFirstDataTransferItem(
253+
items,
254+
"string",
255+
(type) => type === "text/html" || type.startsWith("text/html;")
256+
);
257+
if (htmlItem) {
258+
return new Promise((resolve) => {
259+
htmlItem.getAsString((html) => {
260+
if (!html) {
261+
resolve([]);
262+
return;
263+
}
264+
const tempDiv = document.createElement("div");
265+
tempDiv.innerHTML = html;
266+
const text = tempDiv.textContent || "";
267+
resolve(text ? [{ text }] : []);
268+
});
269+
});
149270
}
150271

151-
return null;
272+
// Mode #4: If there's a string item with empty/null type, extract first one
273+
const genericStringItem = findFirstDataTransferItem(items, "string", (type) => type === "" || type == null);
274+
if (genericStringItem) {
275+
return new Promise((resolve) => {
276+
genericStringItem.getAsString((text) => {
277+
resolve(text ? [{ text }] : []);
278+
});
279+
});
280+
}
281+
282+
return [];
152283
}
153284

154285
/**
@@ -158,19 +289,13 @@ export async function extractClipboardData(
158289
* @param e - The ClipboardEvent (optional)
159290
* @returns Array of objects containing text and/or image data
160291
*/
161-
export async function extractAllClipboardData(e?: ClipboardEvent): Promise<Array<{ text?: string; image?: Blob }>> {
162-
const results: Array<{ text?: string; image?: Blob }> = [];
292+
export async function extractAllClipboardData(e?: ClipboardEvent): Promise<Array<GenClipboardItem>> {
293+
const results: Array<GenClipboardItem> = [];
163294

164295
try {
165296
// First try using ClipboardEvent.clipboardData.items
166297
if (e?.clipboardData?.items) {
167-
for (let i = 0; i < e.clipboardData.items.length; i++) {
168-
const data = await extractClipboardData(e.clipboardData.items[i]);
169-
if (data) {
170-
results.push(data);
171-
}
172-
}
173-
return results;
298+
return await extractDataTransferItems(e.clipboardData.items);
174299
}
175300

176301
// Fallback: Try Clipboard API

0 commit comments

Comments
 (0)