@shipstatic/drop 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -161
- package/dist/index.cjs +108 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -22
- package/dist/index.d.ts +37 -22
- package/dist/index.js +110 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -72,9 +72,31 @@ interface DropReturn {
|
|
|
72
72
|
statusText: string;
|
|
73
73
|
/** Whether currently processing files (ZIP extraction, etc.) */
|
|
74
74
|
isProcessing: boolean;
|
|
75
|
+
/** Whether user is currently dragging over the dropzone */
|
|
76
|
+
isDragging: boolean;
|
|
75
77
|
/** Last validation error if any */
|
|
76
78
|
validationError: ClientError | null;
|
|
77
|
-
/**
|
|
79
|
+
/** Get props to spread on dropzone element (handles drag & drop) */
|
|
80
|
+
getDropzoneProps: () => {
|
|
81
|
+
onDragOver: (e: React.DragEvent) => void;
|
|
82
|
+
onDragLeave: (e: React.DragEvent) => void;
|
|
83
|
+
onDrop: (e: React.DragEvent) => void;
|
|
84
|
+
onClick: () => void;
|
|
85
|
+
};
|
|
86
|
+
/** Get props to spread on hidden file input element */
|
|
87
|
+
getInputProps: () => {
|
|
88
|
+
ref: React.RefObject<HTMLInputElement | null>;
|
|
89
|
+
type: 'file';
|
|
90
|
+
style: {
|
|
91
|
+
display: string;
|
|
92
|
+
};
|
|
93
|
+
multiple: boolean;
|
|
94
|
+
webkitdirectory: string;
|
|
95
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
96
|
+
};
|
|
97
|
+
/** Programmatically trigger file picker */
|
|
98
|
+
open: () => void;
|
|
99
|
+
/** Manually process files (for advanced usage) */
|
|
78
100
|
processFiles: (files: File[]) => Promise<void>;
|
|
79
101
|
/** Clear all files and reset state */
|
|
80
102
|
clearAll: () => void;
|
|
@@ -88,9 +110,19 @@ interface DropReturn {
|
|
|
88
110
|
}) => void;
|
|
89
111
|
}
|
|
90
112
|
/**
|
|
91
|
-
* Headless drop hook
|
|
92
|
-
*
|
|
93
|
-
*
|
|
113
|
+
* Headless drop hook for file upload workflows
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```tsx
|
|
117
|
+
* const drop = useDrop({ ship });
|
|
118
|
+
*
|
|
119
|
+
* return (
|
|
120
|
+
* <div {...drop.getDropzoneProps()} style={{...}}>
|
|
121
|
+
* <input {...drop.getInputProps()} />
|
|
122
|
+
* {drop.isDragging ? "📂 Drop" : "📁 Click"}
|
|
123
|
+
* </div>
|
|
124
|
+
* );
|
|
125
|
+
* ```
|
|
94
126
|
*/
|
|
95
127
|
declare function useDrop(options: DropOptions): DropReturn;
|
|
96
128
|
|
|
@@ -152,26 +184,9 @@ declare function extractZipToFiles(zipFile: File): Promise<ZipExtractionResult>;
|
|
|
152
184
|
* normalizePath('/absolute/path.txt') → 'absolute/path.txt'
|
|
153
185
|
*/
|
|
154
186
|
declare function normalizePath(path: string): string;
|
|
155
|
-
/**
|
|
156
|
-
* Check if a file path is a junk file that should be filtered out
|
|
157
|
-
* Filters common system files like .DS_Store, Thumbs.db, desktop.ini,
|
|
158
|
-
* and macOS resource fork metadata in __MACOSX directories
|
|
159
|
-
*
|
|
160
|
-
* Case-insensitive matching to handle files from different operating systems
|
|
161
|
-
* (Windows file systems are case-insensitive, so Thumbs.db === THUMBS.DB)
|
|
162
|
-
*
|
|
163
|
-
* @example
|
|
164
|
-
* isJunkFile('.DS_Store') → true
|
|
165
|
-
* isJunkFile('.ds_store') → true (case-insensitive)
|
|
166
|
-
* isJunkFile('THUMBS.DB') → true (case-insensitive)
|
|
167
|
-
* isJunkFile('folder/.DS_Store') → true
|
|
168
|
-
* isJunkFile('__MACOSX/file.txt') → true
|
|
169
|
-
* isJunkFile('mydsstore.txt') → false
|
|
170
|
-
*/
|
|
171
|
-
declare function isJunkFile(path: string): boolean;
|
|
172
187
|
/**
|
|
173
188
|
* Check if a file is a ZIP file based on MIME type or extension
|
|
174
189
|
*/
|
|
175
190
|
declare function isZipFile(file: File): boolean;
|
|
176
191
|
|
|
177
|
-
export { type ClientError, type DropOptions, type DropReturn, FILE_STATUSES, type FileStatus, type ProcessedFile, type ZipExtractionResult, createProcessedFile, extractZipToFiles, formatFileSize, getValidFiles,
|
|
192
|
+
export { type ClientError, type DropOptions, type DropReturn, FILE_STATUSES, type FileStatus, type ProcessedFile, type ZipExtractionResult, createProcessedFile, extractZipToFiles, formatFileSize, getValidFiles, isZipFile, normalizePath, stripCommonPrefix, useDrop };
|
package/dist/index.d.ts
CHANGED
|
@@ -72,9 +72,31 @@ interface DropReturn {
|
|
|
72
72
|
statusText: string;
|
|
73
73
|
/** Whether currently processing files (ZIP extraction, etc.) */
|
|
74
74
|
isProcessing: boolean;
|
|
75
|
+
/** Whether user is currently dragging over the dropzone */
|
|
76
|
+
isDragging: boolean;
|
|
75
77
|
/** Last validation error if any */
|
|
76
78
|
validationError: ClientError | null;
|
|
77
|
-
/**
|
|
79
|
+
/** Get props to spread on dropzone element (handles drag & drop) */
|
|
80
|
+
getDropzoneProps: () => {
|
|
81
|
+
onDragOver: (e: React.DragEvent) => void;
|
|
82
|
+
onDragLeave: (e: React.DragEvent) => void;
|
|
83
|
+
onDrop: (e: React.DragEvent) => void;
|
|
84
|
+
onClick: () => void;
|
|
85
|
+
};
|
|
86
|
+
/** Get props to spread on hidden file input element */
|
|
87
|
+
getInputProps: () => {
|
|
88
|
+
ref: React.RefObject<HTMLInputElement | null>;
|
|
89
|
+
type: 'file';
|
|
90
|
+
style: {
|
|
91
|
+
display: string;
|
|
92
|
+
};
|
|
93
|
+
multiple: boolean;
|
|
94
|
+
webkitdirectory: string;
|
|
95
|
+
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
96
|
+
};
|
|
97
|
+
/** Programmatically trigger file picker */
|
|
98
|
+
open: () => void;
|
|
99
|
+
/** Manually process files (for advanced usage) */
|
|
78
100
|
processFiles: (files: File[]) => Promise<void>;
|
|
79
101
|
/** Clear all files and reset state */
|
|
80
102
|
clearAll: () => void;
|
|
@@ -88,9 +110,19 @@ interface DropReturn {
|
|
|
88
110
|
}) => void;
|
|
89
111
|
}
|
|
90
112
|
/**
|
|
91
|
-
* Headless drop hook
|
|
92
|
-
*
|
|
93
|
-
*
|
|
113
|
+
* Headless drop hook for file upload workflows
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```tsx
|
|
117
|
+
* const drop = useDrop({ ship });
|
|
118
|
+
*
|
|
119
|
+
* return (
|
|
120
|
+
* <div {...drop.getDropzoneProps()} style={{...}}>
|
|
121
|
+
* <input {...drop.getInputProps()} />
|
|
122
|
+
* {drop.isDragging ? "📂 Drop" : "📁 Click"}
|
|
123
|
+
* </div>
|
|
124
|
+
* );
|
|
125
|
+
* ```
|
|
94
126
|
*/
|
|
95
127
|
declare function useDrop(options: DropOptions): DropReturn;
|
|
96
128
|
|
|
@@ -152,26 +184,9 @@ declare function extractZipToFiles(zipFile: File): Promise<ZipExtractionResult>;
|
|
|
152
184
|
* normalizePath('/absolute/path.txt') → 'absolute/path.txt'
|
|
153
185
|
*/
|
|
154
186
|
declare function normalizePath(path: string): string;
|
|
155
|
-
/**
|
|
156
|
-
* Check if a file path is a junk file that should be filtered out
|
|
157
|
-
* Filters common system files like .DS_Store, Thumbs.db, desktop.ini,
|
|
158
|
-
* and macOS resource fork metadata in __MACOSX directories
|
|
159
|
-
*
|
|
160
|
-
* Case-insensitive matching to handle files from different operating systems
|
|
161
|
-
* (Windows file systems are case-insensitive, so Thumbs.db === THUMBS.DB)
|
|
162
|
-
*
|
|
163
|
-
* @example
|
|
164
|
-
* isJunkFile('.DS_Store') → true
|
|
165
|
-
* isJunkFile('.ds_store') → true (case-insensitive)
|
|
166
|
-
* isJunkFile('THUMBS.DB') → true (case-insensitive)
|
|
167
|
-
* isJunkFile('folder/.DS_Store') → true
|
|
168
|
-
* isJunkFile('__MACOSX/file.txt') → true
|
|
169
|
-
* isJunkFile('mydsstore.txt') → false
|
|
170
|
-
*/
|
|
171
|
-
declare function isJunkFile(path: string): boolean;
|
|
172
187
|
/**
|
|
173
188
|
* Check if a file is a ZIP file based on MIME type or extension
|
|
174
189
|
*/
|
|
175
190
|
declare function isZipFile(file: File): boolean;
|
|
176
191
|
|
|
177
|
-
export { type ClientError, type DropOptions, type DropReturn, FILE_STATUSES, type FileStatus, type ProcessedFile, type ZipExtractionResult, createProcessedFile, extractZipToFiles, formatFileSize, getValidFiles,
|
|
192
|
+
export { type ClientError, type DropOptions, type DropReturn, FILE_STATUSES, type FileStatus, type ProcessedFile, type ZipExtractionResult, createProcessedFile, extractZipToFiles, formatFileSize, getValidFiles, isZipFile, normalizePath, stripCommonPrefix, useDrop };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useRef, useCallback } from 'react';
|
|
2
|
-
import { formatFileSize as formatFileSize$1, getValidFiles as getValidFiles$1, validateFiles } from '@shipstatic/ship';
|
|
2
|
+
import { formatFileSize as formatFileSize$1, getValidFiles as getValidFiles$1, filterJunk, validateFiles } from '@shipstatic/ship';
|
|
3
3
|
|
|
4
4
|
var __create = Object.create;
|
|
5
5
|
var __defProp = Object.defineProperty;
|
|
@@ -11735,7 +11735,6 @@ async function extractZipToFiles(zipFile) {
|
|
|
11735
11735
|
errors.push(`Skipped invalid path: ${path}`);
|
|
11736
11736
|
continue;
|
|
11737
11737
|
}
|
|
11738
|
-
if (isJunkFile(sanitizedPath)) continue;
|
|
11739
11738
|
try {
|
|
11740
11739
|
const content = await entry.async("blob");
|
|
11741
11740
|
const mimeType = getMimeType(sanitizedPath);
|
|
@@ -11773,11 +11772,6 @@ function normalizePath(path) {
|
|
|
11773
11772
|
}
|
|
11774
11773
|
return normalized.join("/");
|
|
11775
11774
|
}
|
|
11776
|
-
function isJunkFile(path) {
|
|
11777
|
-
const basename = (path.split("/").pop() || "").toLowerCase();
|
|
11778
|
-
const junkFiles = [".ds_store", "thumbs.db", "desktop.ini", "._.ds_store"];
|
|
11779
|
-
return path.toLowerCase().startsWith("__macosx/") || junkFiles.includes(basename);
|
|
11780
|
-
}
|
|
11781
11775
|
function isZipFile(file) {
|
|
11782
11776
|
return file.type === "application/zip" || file.type === "application/x-zip-compressed" || file.name.toLowerCase().endsWith(".zip");
|
|
11783
11777
|
}
|
|
@@ -11838,6 +11832,38 @@ function stripCommonPrefix(files) {
|
|
|
11838
11832
|
path: f.path.startsWith(prefix) ? f.path.slice(prefix.length) : f.path
|
|
11839
11833
|
}));
|
|
11840
11834
|
}
|
|
11835
|
+
async function traverseFileTree(entry, files, currentPath = "") {
|
|
11836
|
+
if (entry.isFile) {
|
|
11837
|
+
const file = await new Promise((resolve, reject) => {
|
|
11838
|
+
entry.file(resolve, reject);
|
|
11839
|
+
});
|
|
11840
|
+
const relativePath = currentPath ? `${currentPath}/${file.name}` : file.name;
|
|
11841
|
+
Object.defineProperty(file, "webkitRelativePath", {
|
|
11842
|
+
value: relativePath,
|
|
11843
|
+
writable: false
|
|
11844
|
+
});
|
|
11845
|
+
files.push(file);
|
|
11846
|
+
} else if (entry.isDirectory) {
|
|
11847
|
+
const dirReader = entry.createReader();
|
|
11848
|
+
let allEntries = [];
|
|
11849
|
+
const readEntriesBatch = async () => {
|
|
11850
|
+
const batch = await new Promise(
|
|
11851
|
+
(resolve, reject) => {
|
|
11852
|
+
dirReader.readEntries(resolve, reject);
|
|
11853
|
+
}
|
|
11854
|
+
);
|
|
11855
|
+
if (batch.length > 0) {
|
|
11856
|
+
allEntries = allEntries.concat(batch);
|
|
11857
|
+
await readEntriesBatch();
|
|
11858
|
+
}
|
|
11859
|
+
};
|
|
11860
|
+
await readEntriesBatch();
|
|
11861
|
+
for (const childEntry of allEntries) {
|
|
11862
|
+
const entryPath = childEntry.isDirectory ? currentPath ? `${currentPath}/${childEntry.name}` : childEntry.name : currentPath;
|
|
11863
|
+
await traverseFileTree(childEntry, files, entryPath);
|
|
11864
|
+
}
|
|
11865
|
+
}
|
|
11866
|
+
}
|
|
11841
11867
|
function useDrop(options) {
|
|
11842
11868
|
const {
|
|
11843
11869
|
ship,
|
|
@@ -11849,8 +11875,10 @@ function useDrop(options) {
|
|
|
11849
11875
|
const [sourceName, setSourceName] = useState("");
|
|
11850
11876
|
const [statusText, setStatusText] = useState("");
|
|
11851
11877
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
11878
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
11852
11879
|
const [validationError, setValidationError] = useState(null);
|
|
11853
11880
|
const isProcessingRef = useRef(false);
|
|
11881
|
+
const inputRef = useRef(null);
|
|
11854
11882
|
const processFiles = useCallback(async (newFiles) => {
|
|
11855
11883
|
if (isProcessingRef.current) {
|
|
11856
11884
|
console.warn("File processing already in progress. Ignoring duplicate call.");
|
|
@@ -11891,9 +11919,16 @@ function useDrop(options) {
|
|
|
11891
11919
|
} else {
|
|
11892
11920
|
allFiles.push(...newFiles);
|
|
11893
11921
|
}
|
|
11922
|
+
const getFilePath = (f) => {
|
|
11923
|
+
const webkitPath = f.webkitRelativePath;
|
|
11924
|
+
return webkitPath && webkitPath.trim() ? webkitPath : f.name;
|
|
11925
|
+
};
|
|
11926
|
+
const filePaths = allFiles.map(getFilePath);
|
|
11927
|
+
const validPaths = new Set(filterJunk(filePaths));
|
|
11928
|
+
const cleanFiles = allFiles.filter((f) => validPaths.has(getFilePath(f)));
|
|
11894
11929
|
setStatusText("Processing files...");
|
|
11895
11930
|
const processedFiles = await Promise.all(
|
|
11896
|
-
|
|
11931
|
+
cleanFiles.map((file) => createProcessedFile(file))
|
|
11897
11932
|
);
|
|
11898
11933
|
const finalFiles = stripPrefix ? stripCommonPrefix(processedFiles) : processedFiles;
|
|
11899
11934
|
const config = await ship.getConfig();
|
|
@@ -11935,6 +11970,7 @@ function useDrop(options) {
|
|
|
11935
11970
|
setSourceName("");
|
|
11936
11971
|
setStatusText("");
|
|
11937
11972
|
setValidationError(null);
|
|
11973
|
+
setIsDragging(false);
|
|
11938
11974
|
isProcessingRef.current = false;
|
|
11939
11975
|
setIsProcessing(false);
|
|
11940
11976
|
}, []);
|
|
@@ -11946,14 +11982,79 @@ function useDrop(options) {
|
|
|
11946
11982
|
(file) => file.id === fileId ? { ...file, ...state } : file
|
|
11947
11983
|
));
|
|
11948
11984
|
}, []);
|
|
11985
|
+
const handleDragOver = useCallback((e) => {
|
|
11986
|
+
e.preventDefault();
|
|
11987
|
+
setIsDragging(true);
|
|
11988
|
+
}, []);
|
|
11989
|
+
const handleDragLeave = useCallback((e) => {
|
|
11990
|
+
e.preventDefault();
|
|
11991
|
+
setIsDragging(false);
|
|
11992
|
+
}, []);
|
|
11993
|
+
const handleDrop = useCallback(async (e) => {
|
|
11994
|
+
e.preventDefault();
|
|
11995
|
+
setIsDragging(false);
|
|
11996
|
+
const items = Array.from(e.dataTransfer.items);
|
|
11997
|
+
const files2 = [];
|
|
11998
|
+
let hasEntries = false;
|
|
11999
|
+
for (const item of items) {
|
|
12000
|
+
if (item.kind === "file") {
|
|
12001
|
+
const entry = item.webkitGetAsEntry?.();
|
|
12002
|
+
if (entry) {
|
|
12003
|
+
hasEntries = true;
|
|
12004
|
+
await traverseFileTree(
|
|
12005
|
+
entry,
|
|
12006
|
+
files2,
|
|
12007
|
+
entry.isDirectory ? entry.name : ""
|
|
12008
|
+
);
|
|
12009
|
+
}
|
|
12010
|
+
}
|
|
12011
|
+
}
|
|
12012
|
+
if (!hasEntries && e.dataTransfer.files.length > 0) {
|
|
12013
|
+
files2.push(...Array.from(e.dataTransfer.files));
|
|
12014
|
+
}
|
|
12015
|
+
if (files2.length > 0) {
|
|
12016
|
+
await processFiles(files2);
|
|
12017
|
+
}
|
|
12018
|
+
}, [processFiles]);
|
|
12019
|
+
const handleInputChange = useCallback((e) => {
|
|
12020
|
+
const files2 = Array.from(e.target.files || []);
|
|
12021
|
+
if (files2.length > 0) {
|
|
12022
|
+
processFiles(files2);
|
|
12023
|
+
}
|
|
12024
|
+
}, [processFiles]);
|
|
12025
|
+
const open = useCallback(() => {
|
|
12026
|
+
inputRef.current?.click();
|
|
12027
|
+
}, []);
|
|
12028
|
+
const getDropzoneProps = useCallback(() => ({
|
|
12029
|
+
onDragOver: handleDragOver,
|
|
12030
|
+
onDragLeave: handleDragLeave,
|
|
12031
|
+
onDrop: handleDrop,
|
|
12032
|
+
onClick: open
|
|
12033
|
+
}), [handleDragOver, handleDragLeave, handleDrop, open]);
|
|
12034
|
+
const getInputProps = useCallback(() => ({
|
|
12035
|
+
ref: inputRef,
|
|
12036
|
+
type: "file",
|
|
12037
|
+
style: { display: "none" },
|
|
12038
|
+
multiple: true,
|
|
12039
|
+
webkitdirectory: "",
|
|
12040
|
+
onChange: handleInputChange
|
|
12041
|
+
}), [handleInputChange]);
|
|
11949
12042
|
return {
|
|
12043
|
+
// State
|
|
11950
12044
|
files,
|
|
11951
12045
|
sourceName,
|
|
11952
12046
|
statusText,
|
|
11953
12047
|
isProcessing,
|
|
12048
|
+
isDragging,
|
|
11954
12049
|
validationError,
|
|
12050
|
+
// Primary API: Prop getters
|
|
12051
|
+
getDropzoneProps,
|
|
12052
|
+
getInputProps,
|
|
12053
|
+
// Actions
|
|
12054
|
+
open,
|
|
11955
12055
|
processFiles,
|
|
11956
12056
|
clearAll,
|
|
12057
|
+
// Helpers
|
|
11957
12058
|
getValidFiles: getValidFilesCallback,
|
|
11958
12059
|
updateFileStatus
|
|
11959
12060
|
};
|
|
@@ -11982,6 +12083,6 @@ mime-db/index.js:
|
|
|
11982
12083
|
*)
|
|
11983
12084
|
*/
|
|
11984
12085
|
|
|
11985
|
-
export { FILE_STATUSES, createProcessedFile, extractZipToFiles, formatFileSize, getValidFiles,
|
|
12086
|
+
export { FILE_STATUSES, createProcessedFile, extractZipToFiles, formatFileSize, getValidFiles, isZipFile, normalizePath, stripCommonPrefix, useDrop };
|
|
11986
12087
|
//# sourceMappingURL=index.js.map
|
|
11987
12088
|
//# sourceMappingURL=index.js.map
|