@@ -7,6 +7,8 @@ import { TabRpcClient } from "@/app/store/wshrpcutil";
77import base64 from "base64-js" ;
88import { colord } from "colord" ;
99
10+ export type GenClipboardItem = { text ?: string ; image ?: Blob } ;
11+
1012function 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