diff --git a/src/file-selector.spec.ts b/src/file-selector.spec.ts index d64aee8..2493ece 100644 --- a/src/file-selector.spec.ts +++ b/src/file-selector.spec.ts @@ -1,7 +1,6 @@ import {FileWithPath} from './file'; import {fromEvent} from './file-selector'; - it('returns a Promise', async () => { const evt = new Event('test'); expect(fromEvent(evt)).toBeInstanceOf(Promise); @@ -34,7 +33,7 @@ it('should return the evt {target} {files} if the passed event is an input evt', expect(file.type).toBe(mockFile.type); expect(file.size).toBe(mockFile.size); expect(file.lastModified).toBe(mockFile.lastModified); - expect(file.path).toBe(name); + expect(file.path).toBe(`./${name}`); }); it('should return an empty array if the evt {target} has no {files} prop', async () => { @@ -59,7 +58,7 @@ it('should return files if the arg is a list of FileSystemFileHandle', async () expect(file.type).toBe(mockFile.type); expect(file.size).toBe(mockFile.size); expect(file.lastModified).toBe(mockFile.lastModified); - expect(file.path).toBe(name); + expect(file.path).toBe(`./${name}`); }); it('should return an empty array if the passed event is not a DragEvent', async () => { @@ -85,7 +84,7 @@ it('should return {files} from DataTransfer if {items} is not defined (e.g. IE11 expect(file.type).toBe(mockFile.type); expect(file.size).toBe(mockFile.size); expect(file.lastModified).toBe(mockFile.lastModified); - expect(file.path).toBe(name); + expect(file.path).toBe(`./${name}`); }); it('should return files from DataTransfer {items} if the passed event is a DragEvent', async () => { @@ -106,7 +105,32 @@ it('should return files from DataTransfer {items} if the passed event is a DragE expect(file.type).toBe(mockFile.type); expect(file.size).toBe(mockFile.size); expect(file.lastModified).toBe(mockFile.lastModified); - expect(file.path).toBe(name); + expect(file.path).toBe(`./${name}`); +}); + +it('should use the {fullPath} for {path} if {webkitGetAsEntry} is supported and the items are FileSystemFileEntry', async () => { + const name = 'test.json'; + const fullPath = '/testfolder/test.json' + const mockFile = createFile(name, {ping: true}, { + type: 'application/json' + }); + + const file = fileSystemFileEntryFromFile(mockFile); + file.fullPath = fullPath + const item = dataTransferItemFromEntry(file, mockFile); + const evt = dragEvtFromFilesAndItems([], [item]); + + const files = await fromEvent(evt); + expect(files).toHaveLength(1); + expect(files.every(file => file instanceof File)).toBe(true); + + const [f] = files as FileWithPath[]; + + expect(f.name).toBe(mockFile.name); + expect(f.type).toBe(mockFile.type); + expect(f.size).toBe(mockFile.size); + expect(f.lastModified).toBe(mockFile.lastModified); + expect(f.path).toBe(fullPath); }); it('skips DataTransfer {items} that are of kind "string"', async () => { @@ -127,7 +151,7 @@ it('skips DataTransfer {items} that are of kind "string"', async () => { expect(file.type).toBe(mockFile.type); expect(file.size).toBe(mockFile.size); expect(file.lastModified).toBe(mockFile.lastModified); - expect(file.path).toBe(name); + expect(file.path).toBe(`./${name}`); }); it('can read a tree of directories recursively and return a flat list of FileWithPath objects', async () => { @@ -277,7 +301,7 @@ it('should use getAsFileSystemHandle when available', async () => { expect(file.type).toBe(f.type); expect(file.size).toBe(f.size); expect(file.lastModified).toBe(f.lastModified); - expect(file.path).toBe(name); + expect(file.path).toBe(`./${name}`); }); function dragEvtFromItems(items: DataTransferItem | DataTransferItem[], type: string = 'drop'): DragEvent { @@ -477,6 +501,7 @@ interface DirEntry extends Entry { interface Entry { isDirectory: boolean; isFile: boolean; + fullPath?: string; } interface DirReader { diff --git a/src/file-selector.ts b/src/file-selector.ts index f495d66..4aaee19 100644 --- a/src/file-selector.ts +++ b/src/file-selector.ts @@ -110,7 +110,7 @@ function toFilePromises(item: DataTransferItem) { return fromDirEntry(entry) as any; } - return fromDataTransferItem(item); + return fromDataTransferItem(item, entry); } function flatten(items: any[]): T[] { @@ -120,7 +120,7 @@ function flatten(items: any[]): T[] { ], []); } -function fromDataTransferItem(item: DataTransferItem) { +function fromDataTransferItem(item: DataTransferItem, entry?: FileSystemEntry | null) { if (typeof (item as any).getAsFileSystemHandle === 'function') { return (item as any).getAsFileSystemHandle() .then(async (h: any) => { @@ -133,7 +133,7 @@ function fromDataTransferItem(item: DataTransferItem) { if (!file) { return Promise.reject(`${item} is not a File`); } - const fwp = toFileWithPath(file); + const fwp = toFileWithPath(file, entry?.fullPath ?? undefined); return Promise.resolve(fwp); } diff --git a/src/file.spec.ts b/src/file.spec.ts index 72c8f91..8f2f61d 100644 --- a/src/file.spec.ts +++ b/src/file.spec.ts @@ -52,7 +52,7 @@ describe('toFile()', () => { const name = 'test.json'; const file = new File([], name); const fileWithPath = toFileWithPath(file); - expect(fileWithPath.path).toBe(name); + expect(fileWithPath.path).toBe(`./${name}`); }); it('uses the File {webkitRelativePath} as {path} if it exists', () => { @@ -66,6 +66,44 @@ describe('toFile()', () => { expect(fileWithPath.path).toBe(path); }); + it('sets the {relativePath} if provided without overwriting {path}', () => { + const fullPath = '/Users/test/Desktop/test/test.json'; + const path = '/test/test.json'; + const file = new File([], 'test.json'); + + // @ts-expect-error + file.path = fullPath; + const fileWithPath = toFileWithPath(file, path); + expect(fileWithPath.path).toBe(fullPath); + expect(fileWithPath.relativePath).toBe(path); + }); + + test('{relativePath} is enumerable', () => { + const path = '/test/test.json'; + const file = new File([], 'test.json'); + const fileWithPath = toFileWithPath(file, path); + + expect(Object.keys(fileWithPath)).toContain('relativePath'); + + const keys: string[] = []; + for (const key in fileWithPath) { + keys.push(key); + } + + expect(keys).toContain('relativePath'); + }); + + it('uses the File {webkitRelativePath} as {relativePath} if it exists', () => { + const name = 'test.json'; + const path = 'test/test.json'; + const file = new File([], name); + Object.defineProperty(file, 'webkitRelativePath', { + value: path + }); + const fileWithPath = toFileWithPath(file); + expect(fileWithPath.relativePath).toBe(path); + }); + it('sets the {type} from extension', () => { const types = Array.from(COMMON_MIME_TYPES.values()); const files = Array.from(COMMON_MIME_TYPES.keys()) diff --git a/src/file.ts b/src/file.ts index d022d2b..d493b7c 100644 --- a/src/file.ts +++ b/src/file.ts @@ -1203,21 +1203,17 @@ export const COMMON_MIME_TYPES = new Map([ export function toFileWithPath(file: FileWithPath, path?: string, h?: FileSystemHandle): FileWithPath { const f = withMimeType(file); + const {webkitRelativePath} = file; + const p = typeof path === 'string' + ? path + // If is set, + // the File will have a {webkitRelativePath} property + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory + : typeof webkitRelativePath === 'string' && webkitRelativePath.length > 0 + ? webkitRelativePath + : `./${file.name}`; if (typeof f.path !== 'string') { // on electron, path is already set to the absolute path - const {webkitRelativePath} = file; - Object.defineProperty(f, 'path', { - value: typeof path === 'string' - ? path - // If is set, - // the File will have a {webkitRelativePath} property - // https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory - : typeof webkitRelativePath === 'string' && webkitRelativePath.length > 0 - ? webkitRelativePath - : file.name, - writable: false, - configurable: false, - enumerable: true - }); + setObjProp(f, 'path', p); } if (h !== undefined) { Object.defineProperty(f, 'handle', { @@ -1227,12 +1223,15 @@ export function toFileWithPath(file: FileWithPath, path?: string, h?: FileSystem enumerable: true }); } + // Always populate a relative path so that even electron apps have access to a relativePath value + setObjProp(f, 'relativePath', p); return f; } export interface FileWithPath extends File { readonly path?: string; readonly handle?: FileSystemFileHandle; + readonly relativePath?: string; } function withMimeType(file: FileWithPath) { @@ -1255,3 +1254,12 @@ function withMimeType(file: FileWithPath) { return file; } + +function setObjProp(f: FileWithPath, key: string, value: string) { + Object.defineProperty(f, key, { + value, + writable: false, + configurable: false, + enumerable: true + }) +}