@gateweb/react-utils 1.11.0 → 1.13.0

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.
@@ -474,11 +474,33 @@ declare const fakeApi: <T>(returnValue: T, result?: boolean, time?: number) => P
474
474
  message: string;
475
475
  }>;
476
476
 
477
+ declare const MimeTypeMap: {
478
+ readonly jpeg: "image/jpeg";
479
+ readonly jpg: "image/jpeg";
480
+ readonly gif: "image/gif";
481
+ readonly png: "image/png";
482
+ readonly pdf: "application/pdf";
483
+ readonly zip: "application/zip";
484
+ readonly csv: "text/csv";
485
+ readonly ppt: "application/vnd.ms-powerpoint";
486
+ readonly pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation";
487
+ readonly xls: "application/vnd.ms-excel";
488
+ readonly xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
489
+ readonly doc: "application/msword";
490
+ readonly docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
491
+ readonly txt: "text/plain";
492
+ };
493
+ declare const OtherMimeType = "application/octet-stream";
494
+ type MimeTypeExtension = keyof typeof MimeTypeMap;
495
+ type MimeTypeValue = (typeof MimeTypeMap)[MimeTypeExtension] | typeof OtherMimeType;
496
+
477
497
  /**
478
- * 檢查檔案是否為合法的 MIME 類型
498
+ * 檢查檔案是否為合法的檔案類型
499
+ *
500
+ * `accepts` 可同時接受副檔名以及 MIME 類型
479
501
  *
480
502
  * @param file 檔案
481
- * @param accepts 允許的 MIME 類型
503
+ * @param accepts 允許的類型
482
504
  *
483
505
  * @example
484
506
  *
@@ -486,6 +508,7 @@ declare const fakeApi: <T>(returnValue: T, result?: boolean, time?: number) => P
486
508
  * validateFileType({ type: 'image/png' }, ['image/png', 'image/jpeg']) // true
487
509
  * validateFileType({ type: 'image/png' }, ['image/jpeg']) // false
488
510
  * validateFileType({ type: 'image/png' }, ['image/*']) // true
511
+ * validateFileType({ name: '圖片.png', type: 'image/png' }, ['.png']) // true
489
512
  * ```
490
513
  */
491
514
  declare const validateFileType: (file: File, accepts: string[]) => boolean;
@@ -507,8 +530,44 @@ declare const validateFileType: (file: File, accepts: string[]) => boolean;
507
530
  * getMimeType('txt') // 'text/plain'
508
531
  * getMimeType('zip') // 'application/zip'
509
532
  * getMimeType('mp4') // 'application/octet-stream'
533
+ * getMimeType('xls') // 'application/vnd.ms-excel'
534
+ * getMimeType('xlsx') // 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
535
+ * getMimeType('doc') // 'application/msword'
536
+ * getMimeType('docx') // 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
537
+ *
538
+ * @deprecated use `parseFileInfoFromFilename` instead
539
+ */
540
+ declare const getMimeType: (fileName: string) => "image/jpeg" | "image/gif" | "image/png" | "application/pdf" | "application/zip" | "text/csv" | "application/vnd.ms-powerpoint" | "application/vnd.openxmlformats-officedocument.presentationml.presentation" | "application/vnd.ms-excel" | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" | "application/msword" | "application/vnd.openxmlformats-officedocument.wordprocessingml.document" | "text/plain";
541
+ /**
542
+ * 用來解析後端在 response header content-disposition 的內容
543
+ *
544
+ * 一般來說格式會有以下兩種
545
+ *
546
+ * - Content-Disposition: attachment; filename="file name.jpg"
547
+ * - Content-Disposition: attachment; filename*=UTF-8''file%20name2.jpg
548
+ *
549
+ * 如果格式正確就會取得檔案名稱(包含副檔名),優先取 filename* 內的內容
550
+ *
551
+ * @param disposition Content-Disposition
552
+ *
553
+ * @example
554
+ *
555
+ * parseFilenameFromDisposition('attachment; filename="file name1.jpg') // file name.jpg
556
+ * parseFilenameFromDisposition('attachment; filename*=UTF-8''file%20name2.jpg') // file name2.jpg
557
+ * parseFilenameFromDisposition('attachment; filename="file name.jpg; filename*=UTF-8''file%20name2.jpg') // file name2.jpg
558
+ */
559
+ declare const parseFilenameFromDisposition: (disposition: string) => string | undefined;
560
+ /**
561
+ * 解析 `filename` 回傳檔名、副檔名、MIME type
562
+ *
563
+ * @param filename 檔案名稱
564
+ *
565
+ * @example
566
+ *
567
+ * parseFileInfoFromFilename('image.jpg') // ['image', 'jpg', 'image/jpeg']
568
+ * parseFileInfoFromFilename('image') // ['image', '', 'application/octet-stream']
510
569
  */
511
- declare const getMimeType: (fileName: string) => "image/jpeg" | "image/png" | "application/pdf" | "application/zip" | "text/csv" | "text/plain" | "application/octet-stream";
570
+ declare const parseFileInfoFromFilename: (filename: string) => [string, string, MimeTypeValue];
512
571
 
513
572
  declare function invariant(condition: any, message?: string | (() => string)): asserts condition;
514
573
 
@@ -1145,4 +1204,4 @@ declare const getLocalStorage: <T>(key: string, deCode?: boolean) => T | undefin
1145
1204
  */
1146
1205
  declare const setLocalStorage: (key: string, value: Record<string, any>, enCode?: boolean) => void;
1147
1206
 
1148
- export { ByteSize, type PartialBy, QueryProvider, type RequiredBy, type TCountdownActions, adToRocEra, camelCase2PascalCase, camelCase2SnakeCase, camelString2PascalString, camelString2SnakeString, convertBytes, createDataContext, createEnumLikeObject, debounce, deepMerge, downloadFile, extractEnumLikeObject, fakeApi, formatAmount, formatBytes, formatStarMask, generatePeriodArray, getCurrentPeriod, getLocalStorage, getMimeType, invariant, isChinese, isDateString, isDateTimeString, isEmail, isEnglish, isEqual, isNonZeroStart, isNumber, isNumberAtLeastN, isNumberN, isNumberNM, isServer, isTWMobile, isTWPhone, isTimeString, isValidPassword, maskString, mergeRefs, objectToSearchParams, omit, omitByValue, pascalCase2CamelCase, pascalCase2SnakeCase, pascalString2CamelString, pascalString2SnakeString, pick, pickByValue, rocEraToAd, searchParamsToObject, setLocalStorage, snakeCase2CamelCase, snakeCase2PascalCase, snakeString2CamelString, snakeString2PascalString, throttle, useCountdown, useQueryContext, useValue, validTaxId, validateDateString, validateFileType, wait };
1207
+ export { ByteSize, type MimeTypeExtension, MimeTypeMap, type MimeTypeValue, OtherMimeType, type PartialBy, QueryProvider, type RequiredBy, type TCountdownActions, adToRocEra, camelCase2PascalCase, camelCase2SnakeCase, camelString2PascalString, camelString2SnakeString, convertBytes, createDataContext, createEnumLikeObject, debounce, deepMerge, downloadFile, extractEnumLikeObject, fakeApi, formatAmount, formatBytes, formatStarMask, generatePeriodArray, getCurrentPeriod, getLocalStorage, getMimeType, invariant, isChinese, isDateString, isDateTimeString, isEmail, isEnglish, isEqual, isNonZeroStart, isNumber, isNumberAtLeastN, isNumberN, isNumberNM, isServer, isTWMobile, isTWPhone, isTimeString, isValidPassword, maskString, mergeRefs, objectToSearchParams, omit, omitByValue, parseFileInfoFromFilename, parseFilenameFromDisposition, pascalCase2CamelCase, pascalCase2SnakeCase, pascalString2CamelString, pascalString2SnakeString, pick, pickByValue, rocEraToAd, searchParamsToObject, setLocalStorage, snakeCase2CamelCase, snakeCase2PascalCase, snakeString2CamelString, snakeString2PascalString, throttle, useCountdown, useQueryContext, useValue, validTaxId, validateDateString, validateFileType, wait };
package/dist/cjs/index.js CHANGED
@@ -238,11 +238,31 @@ function createEnumLikeObject(obj, name, scene) {
238
238
  }, time);
239
239
  });
240
240
 
241
+ const MimeTypeMap = {
242
+ jpeg: 'image/jpeg',
243
+ jpg: 'image/jpeg',
244
+ gif: 'image/gif',
245
+ png: 'image/png',
246
+ pdf: 'application/pdf',
247
+ zip: 'application/zip',
248
+ csv: 'text/csv',
249
+ ppt: 'application/vnd.ms-powerpoint',
250
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
251
+ xls: 'application/vnd.ms-excel',
252
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
253
+ doc: 'application/msword',
254
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
255
+ txt: 'text/plain'
256
+ };
257
+ const OtherMimeType = 'application/octet-stream';
258
+
241
259
  /**
242
- * 檢查檔案是否為合法的 MIME 類型
260
+ * 檢查檔案是否為合法的檔案類型
261
+ *
262
+ * `accepts` 可同時接受副檔名以及 MIME 類型
243
263
  *
244
264
  * @param file 檔案
245
- * @param accepts 允許的 MIME 類型
265
+ * @param accepts 允許的類型
246
266
  *
247
267
  * @example
248
268
  *
@@ -250,21 +270,24 @@ function createEnumLikeObject(obj, name, scene) {
250
270
  * validateFileType({ type: 'image/png' }, ['image/png', 'image/jpeg']) // true
251
271
  * validateFileType({ type: 'image/png' }, ['image/jpeg']) // false
252
272
  * validateFileType({ type: 'image/png' }, ['image/*']) // true
273
+ * validateFileType({ name: '圖片.png', type: 'image/png' }, ['.png']) // true
253
274
  * ```
254
275
  */ const validateFileType = (file, accepts)=>{
255
276
  if (accepts.length === 0) return true;
256
277
  // 獲取文件的MIME類型
257
278
  const fileMimeType = file.type;
279
+ // 提取副檔名(含 .,且轉小寫)
280
+ const fileExt = file.name.includes('.') ? `.${file.name.split('.').pop().toLowerCase()}` : '';
258
281
  return accepts.some((accept)=>{
282
+ if (accept.startsWith('.')) {
283
+ // 以副檔名檢查,忽略大小寫
284
+ return fileExt === accept.toLowerCase();
285
+ }
259
286
  if (accept === fileMimeType) {
260
287
  return true;
261
288
  }
262
289
  if (accept.endsWith('/*')) {
263
- const acceptedCategory = accept.split('/')[0];
264
- const fileCategory = fileMimeType.split('/')[0];
265
- if (acceptedCategory === fileCategory) {
266
- return true;
267
- }
290
+ return accept.split('/')[0] === fileMimeType.split('/')[0];
268
291
  }
269
292
  return false;
270
293
  });
@@ -287,24 +310,74 @@ function createEnumLikeObject(obj, name, scene) {
287
310
  * getMimeType('txt') // 'text/plain'
288
311
  * getMimeType('zip') // 'application/zip'
289
312
  * getMimeType('mp4') // 'application/octet-stream'
313
+ * getMimeType('xls') // 'application/vnd.ms-excel'
314
+ * getMimeType('xlsx') // 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
315
+ * getMimeType('doc') // 'application/msword'
316
+ * getMimeType('docx') // 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
317
+ *
318
+ * @deprecated use `parseFileInfoFromFilename` instead
290
319
  */ const getMimeType = (fileName)=>{
291
- switch((fileName.split('.').pop() || '').toLocaleLowerCase()){
292
- case 'jpeg':
293
- case 'jpg':
294
- return 'image/jpeg';
295
- case 'png':
296
- return 'image/png';
297
- case 'pdf':
298
- return 'application/pdf';
299
- case 'zip':
300
- return 'application/zip';
301
- case 'csv':
302
- return 'text/csv';
303
- case 'txt':
304
- return 'text/plain';
305
- default:
306
- return 'application/octet-stream';
320
+ const ext = (fileName.split('.').pop() || '').toLowerCase();
321
+ return MimeTypeMap[ext] ?? OtherMimeType;
322
+ };
323
+ /**
324
+ * 用來解析後端在 response header content-disposition 的內容
325
+ *
326
+ * 一般來說格式會有以下兩種
327
+ *
328
+ * - Content-Disposition: attachment; filename="file name.jpg"
329
+ * - Content-Disposition: attachment; filename*=UTF-8''file%20name2.jpg
330
+ *
331
+ * 如果格式正確就會取得檔案名稱(包含副檔名),優先取 filename* 內的內容
332
+ *
333
+ * @param disposition Content-Disposition
334
+ *
335
+ * @example
336
+ *
337
+ * parseFilenameFromDisposition('attachment; filename="file name1.jpg') // file name.jpg
338
+ * parseFilenameFromDisposition('attachment; filename*=UTF-8''file%20name2.jpg') // file name2.jpg
339
+ * parseFilenameFromDisposition('attachment; filename="file name.jpg; filename*=UTF-8''file%20name2.jpg') // file name2.jpg
340
+ */ const parseFilenameFromDisposition = (disposition)=>{
341
+ // 1. 先找 filename*
342
+ const filenameStarMatch = disposition.match(/filename\*\s*=\s*(?:UTF-8'')?([^;]+)/i);
343
+ if (filenameStarMatch && filenameStarMatch[1]) {
344
+ // 依 RFC 5987 格式(UTF-8''URL-ENCODED),要先 decode
345
+ try {
346
+ return decodeURIComponent(filenameStarMatch[1].replace(/(^['"]|['"]$)/g, ''));
347
+ } catch {
348
+ // fallback,如果 decode 失敗,直接傳回原值
349
+ return filenameStarMatch[1];
350
+ }
307
351
  }
352
+ // 2. 沒有 filename*,再找 filename
353
+ const filenameMatch = disposition.match(/filename\s*=\s*("?)([^";]+)\1/i);
354
+ if (filenameMatch && filenameMatch[2]) {
355
+ return filenameMatch[2];
356
+ }
357
+ // 3. 都沒有則 undefined
358
+ return undefined;
359
+ };
360
+ /**
361
+ * 解析 `filename` 回傳檔名、副檔名、MIME type
362
+ *
363
+ * @param filename 檔案名稱
364
+ *
365
+ * @example
366
+ *
367
+ * parseFileInfoFromFilename('image.jpg') // ['image', 'jpg', 'image/jpeg']
368
+ * parseFileInfoFromFilename('image') // ['image', '', 'application/octet-stream']
369
+ */ const parseFileInfoFromFilename = (filename)=>{
370
+ const lastDot = filename.lastIndexOf('.');
371
+ if (lastDot === -1) return [
372
+ filename,
373
+ '',
374
+ OtherMimeType
375
+ ]; // 沒有副檔名
376
+ return [
377
+ filename.slice(0, lastDot),
378
+ filename.slice(lastDot + 1),
379
+ MimeTypeMap[filename.slice(lastDot + 1)] ?? OtherMimeType
380
+ ];
308
381
  };
309
382
 
310
383
  // const isProduction: boolean = process.env.NODE_ENV === 'production';
@@ -1226,6 +1299,8 @@ exports.downloadFile = downloadClient.downloadFile;
1226
1299
  exports.getLocalStorage = webStorageClient.getLocalStorage;
1227
1300
  exports.setLocalStorage = webStorageClient.setLocalStorage;
1228
1301
  exports.ByteSize = ByteSize;
1302
+ exports.MimeTypeMap = MimeTypeMap;
1303
+ exports.OtherMimeType = OtherMimeType;
1229
1304
  exports.adToRocEra = adToRocEra;
1230
1305
  exports.camelCase2PascalCase = camelCase2PascalCase;
1231
1306
  exports.camelCase2SnakeCase = camelCase2SnakeCase;
@@ -1266,6 +1341,8 @@ exports.mergeRefs = mergeRefs;
1266
1341
  exports.objectToSearchParams = objectToSearchParams;
1267
1342
  exports.omit = omit;
1268
1343
  exports.omitByValue = omitByValue;
1344
+ exports.parseFileInfoFromFilename = parseFileInfoFromFilename;
1345
+ exports.parseFilenameFromDisposition = parseFilenameFromDisposition;
1269
1346
  exports.pascalCase2CamelCase = pascalCase2CamelCase;
1270
1347
  exports.pascalCase2SnakeCase = pascalCase2SnakeCase;
1271
1348
  exports.pascalString2CamelString = pascalString2CamelString;
@@ -474,11 +474,33 @@ declare const fakeApi: <T>(returnValue: T, result?: boolean, time?: number) => P
474
474
  message: string;
475
475
  }>;
476
476
 
477
+ declare const MimeTypeMap: {
478
+ readonly jpeg: "image/jpeg";
479
+ readonly jpg: "image/jpeg";
480
+ readonly gif: "image/gif";
481
+ readonly png: "image/png";
482
+ readonly pdf: "application/pdf";
483
+ readonly zip: "application/zip";
484
+ readonly csv: "text/csv";
485
+ readonly ppt: "application/vnd.ms-powerpoint";
486
+ readonly pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation";
487
+ readonly xls: "application/vnd.ms-excel";
488
+ readonly xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
489
+ readonly doc: "application/msword";
490
+ readonly docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
491
+ readonly txt: "text/plain";
492
+ };
493
+ declare const OtherMimeType = "application/octet-stream";
494
+ type MimeTypeExtension = keyof typeof MimeTypeMap;
495
+ type MimeTypeValue = (typeof MimeTypeMap)[MimeTypeExtension] | typeof OtherMimeType;
496
+
477
497
  /**
478
- * 檢查檔案是否為合法的 MIME 類型
498
+ * 檢查檔案是否為合法的檔案類型
499
+ *
500
+ * `accepts` 可同時接受副檔名以及 MIME 類型
479
501
  *
480
502
  * @param file 檔案
481
- * @param accepts 允許的 MIME 類型
503
+ * @param accepts 允許的類型
482
504
  *
483
505
  * @example
484
506
  *
@@ -486,6 +508,7 @@ declare const fakeApi: <T>(returnValue: T, result?: boolean, time?: number) => P
486
508
  * validateFileType({ type: 'image/png' }, ['image/png', 'image/jpeg']) // true
487
509
  * validateFileType({ type: 'image/png' }, ['image/jpeg']) // false
488
510
  * validateFileType({ type: 'image/png' }, ['image/*']) // true
511
+ * validateFileType({ name: '圖片.png', type: 'image/png' }, ['.png']) // true
489
512
  * ```
490
513
  */
491
514
  declare const validateFileType: (file: File, accepts: string[]) => boolean;
@@ -507,8 +530,44 @@ declare const validateFileType: (file: File, accepts: string[]) => boolean;
507
530
  * getMimeType('txt') // 'text/plain'
508
531
  * getMimeType('zip') // 'application/zip'
509
532
  * getMimeType('mp4') // 'application/octet-stream'
533
+ * getMimeType('xls') // 'application/vnd.ms-excel'
534
+ * getMimeType('xlsx') // 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
535
+ * getMimeType('doc') // 'application/msword'
536
+ * getMimeType('docx') // 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
537
+ *
538
+ * @deprecated use `parseFileInfoFromFilename` instead
539
+ */
540
+ declare const getMimeType: (fileName: string) => "image/jpeg" | "image/gif" | "image/png" | "application/pdf" | "application/zip" | "text/csv" | "application/vnd.ms-powerpoint" | "application/vnd.openxmlformats-officedocument.presentationml.presentation" | "application/vnd.ms-excel" | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" | "application/msword" | "application/vnd.openxmlformats-officedocument.wordprocessingml.document" | "text/plain";
541
+ /**
542
+ * 用來解析後端在 response header content-disposition 的內容
543
+ *
544
+ * 一般來說格式會有以下兩種
545
+ *
546
+ * - Content-Disposition: attachment; filename="file name.jpg"
547
+ * - Content-Disposition: attachment; filename*=UTF-8''file%20name2.jpg
548
+ *
549
+ * 如果格式正確就會取得檔案名稱(包含副檔名),優先取 filename* 內的內容
550
+ *
551
+ * @param disposition Content-Disposition
552
+ *
553
+ * @example
554
+ *
555
+ * parseFilenameFromDisposition('attachment; filename="file name1.jpg') // file name.jpg
556
+ * parseFilenameFromDisposition('attachment; filename*=UTF-8''file%20name2.jpg') // file name2.jpg
557
+ * parseFilenameFromDisposition('attachment; filename="file name.jpg; filename*=UTF-8''file%20name2.jpg') // file name2.jpg
558
+ */
559
+ declare const parseFilenameFromDisposition: (disposition: string) => string | undefined;
560
+ /**
561
+ * 解析 `filename` 回傳檔名、副檔名、MIME type
562
+ *
563
+ * @param filename 檔案名稱
564
+ *
565
+ * @example
566
+ *
567
+ * parseFileInfoFromFilename('image.jpg') // ['image', 'jpg', 'image/jpeg']
568
+ * parseFileInfoFromFilename('image') // ['image', '', 'application/octet-stream']
510
569
  */
511
- declare const getMimeType: (fileName: string) => "image/jpeg" | "image/png" | "application/pdf" | "application/zip" | "text/csv" | "text/plain" | "application/octet-stream";
570
+ declare const parseFileInfoFromFilename: (filename: string) => [string, string, MimeTypeValue];
512
571
 
513
572
  declare function invariant(condition: any, message?: string | (() => string)): asserts condition;
514
573
 
@@ -1145,4 +1204,4 @@ declare const getLocalStorage: <T>(key: string, deCode?: boolean) => T | undefin
1145
1204
  */
1146
1205
  declare const setLocalStorage: (key: string, value: Record<string, any>, enCode?: boolean) => void;
1147
1206
 
1148
- export { ByteSize, type PartialBy, QueryProvider, type RequiredBy, type TCountdownActions, adToRocEra, camelCase2PascalCase, camelCase2SnakeCase, camelString2PascalString, camelString2SnakeString, convertBytes, createDataContext, createEnumLikeObject, debounce, deepMerge, downloadFile, extractEnumLikeObject, fakeApi, formatAmount, formatBytes, formatStarMask, generatePeriodArray, getCurrentPeriod, getLocalStorage, getMimeType, invariant, isChinese, isDateString, isDateTimeString, isEmail, isEnglish, isEqual, isNonZeroStart, isNumber, isNumberAtLeastN, isNumberN, isNumberNM, isServer, isTWMobile, isTWPhone, isTimeString, isValidPassword, maskString, mergeRefs, objectToSearchParams, omit, omitByValue, pascalCase2CamelCase, pascalCase2SnakeCase, pascalString2CamelString, pascalString2SnakeString, pick, pickByValue, rocEraToAd, searchParamsToObject, setLocalStorage, snakeCase2CamelCase, snakeCase2PascalCase, snakeString2CamelString, snakeString2PascalString, throttle, useCountdown, useQueryContext, useValue, validTaxId, validateDateString, validateFileType, wait };
1207
+ export { ByteSize, type MimeTypeExtension, MimeTypeMap, type MimeTypeValue, OtherMimeType, type PartialBy, QueryProvider, type RequiredBy, type TCountdownActions, adToRocEra, camelCase2PascalCase, camelCase2SnakeCase, camelString2PascalString, camelString2SnakeString, convertBytes, createDataContext, createEnumLikeObject, debounce, deepMerge, downloadFile, extractEnumLikeObject, fakeApi, formatAmount, formatBytes, formatStarMask, generatePeriodArray, getCurrentPeriod, getLocalStorage, getMimeType, invariant, isChinese, isDateString, isDateTimeString, isEmail, isEnglish, isEqual, isNonZeroStart, isNumber, isNumberAtLeastN, isNumberN, isNumberNM, isServer, isTWMobile, isTWPhone, isTimeString, isValidPassword, maskString, mergeRefs, objectToSearchParams, omit, omitByValue, parseFileInfoFromFilename, parseFilenameFromDisposition, pascalCase2CamelCase, pascalCase2SnakeCase, pascalString2CamelString, pascalString2SnakeString, pick, pickByValue, rocEraToAd, searchParamsToObject, setLocalStorage, snakeCase2CamelCase, snakeCase2PascalCase, snakeString2CamelString, snakeString2PascalString, throttle, useCountdown, useQueryContext, useValue, validTaxId, validateDateString, validateFileType, wait };
package/dist/es/index.mjs CHANGED
@@ -231,11 +231,31 @@ function createEnumLikeObject(obj, name, scene) {
231
231
  }, time);
232
232
  });
233
233
 
234
+ const MimeTypeMap = {
235
+ jpeg: 'image/jpeg',
236
+ jpg: 'image/jpeg',
237
+ gif: 'image/gif',
238
+ png: 'image/png',
239
+ pdf: 'application/pdf',
240
+ zip: 'application/zip',
241
+ csv: 'text/csv',
242
+ ppt: 'application/vnd.ms-powerpoint',
243
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
244
+ xls: 'application/vnd.ms-excel',
245
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
246
+ doc: 'application/msword',
247
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
248
+ txt: 'text/plain'
249
+ };
250
+ const OtherMimeType = 'application/octet-stream';
251
+
234
252
  /**
235
- * 檢查檔案是否為合法的 MIME 類型
253
+ * 檢查檔案是否為合法的檔案類型
254
+ *
255
+ * `accepts` 可同時接受副檔名以及 MIME 類型
236
256
  *
237
257
  * @param file 檔案
238
- * @param accepts 允許的 MIME 類型
258
+ * @param accepts 允許的類型
239
259
  *
240
260
  * @example
241
261
  *
@@ -243,21 +263,24 @@ function createEnumLikeObject(obj, name, scene) {
243
263
  * validateFileType({ type: 'image/png' }, ['image/png', 'image/jpeg']) // true
244
264
  * validateFileType({ type: 'image/png' }, ['image/jpeg']) // false
245
265
  * validateFileType({ type: 'image/png' }, ['image/*']) // true
266
+ * validateFileType({ name: '圖片.png', type: 'image/png' }, ['.png']) // true
246
267
  * ```
247
268
  */ const validateFileType = (file, accepts)=>{
248
269
  if (accepts.length === 0) return true;
249
270
  // 獲取文件的MIME類型
250
271
  const fileMimeType = file.type;
272
+ // 提取副檔名(含 .,且轉小寫)
273
+ const fileExt = file.name.includes('.') ? `.${file.name.split('.').pop().toLowerCase()}` : '';
251
274
  return accepts.some((accept)=>{
275
+ if (accept.startsWith('.')) {
276
+ // 以副檔名檢查,忽略大小寫
277
+ return fileExt === accept.toLowerCase();
278
+ }
252
279
  if (accept === fileMimeType) {
253
280
  return true;
254
281
  }
255
282
  if (accept.endsWith('/*')) {
256
- const acceptedCategory = accept.split('/')[0];
257
- const fileCategory = fileMimeType.split('/')[0];
258
- if (acceptedCategory === fileCategory) {
259
- return true;
260
- }
283
+ return accept.split('/')[0] === fileMimeType.split('/')[0];
261
284
  }
262
285
  return false;
263
286
  });
@@ -280,24 +303,74 @@ function createEnumLikeObject(obj, name, scene) {
280
303
  * getMimeType('txt') // 'text/plain'
281
304
  * getMimeType('zip') // 'application/zip'
282
305
  * getMimeType('mp4') // 'application/octet-stream'
306
+ * getMimeType('xls') // 'application/vnd.ms-excel'
307
+ * getMimeType('xlsx') // 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
308
+ * getMimeType('doc') // 'application/msword'
309
+ * getMimeType('docx') // 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
310
+ *
311
+ * @deprecated use `parseFileInfoFromFilename` instead
283
312
  */ const getMimeType = (fileName)=>{
284
- switch((fileName.split('.').pop() || '').toLocaleLowerCase()){
285
- case 'jpeg':
286
- case 'jpg':
287
- return 'image/jpeg';
288
- case 'png':
289
- return 'image/png';
290
- case 'pdf':
291
- return 'application/pdf';
292
- case 'zip':
293
- return 'application/zip';
294
- case 'csv':
295
- return 'text/csv';
296
- case 'txt':
297
- return 'text/plain';
298
- default:
299
- return 'application/octet-stream';
313
+ const ext = (fileName.split('.').pop() || '').toLowerCase();
314
+ return MimeTypeMap[ext] ?? OtherMimeType;
315
+ };
316
+ /**
317
+ * 用來解析後端在 response header content-disposition 的內容
318
+ *
319
+ * 一般來說格式會有以下兩種
320
+ *
321
+ * - Content-Disposition: attachment; filename="file name.jpg"
322
+ * - Content-Disposition: attachment; filename*=UTF-8''file%20name2.jpg
323
+ *
324
+ * 如果格式正確就會取得檔案名稱(包含副檔名),優先取 filename* 內的內容
325
+ *
326
+ * @param disposition Content-Disposition
327
+ *
328
+ * @example
329
+ *
330
+ * parseFilenameFromDisposition('attachment; filename="file name1.jpg') // file name.jpg
331
+ * parseFilenameFromDisposition('attachment; filename*=UTF-8''file%20name2.jpg') // file name2.jpg
332
+ * parseFilenameFromDisposition('attachment; filename="file name.jpg; filename*=UTF-8''file%20name2.jpg') // file name2.jpg
333
+ */ const parseFilenameFromDisposition = (disposition)=>{
334
+ // 1. 先找 filename*
335
+ const filenameStarMatch = disposition.match(/filename\*\s*=\s*(?:UTF-8'')?([^;]+)/i);
336
+ if (filenameStarMatch && filenameStarMatch[1]) {
337
+ // 依 RFC 5987 格式(UTF-8''URL-ENCODED),要先 decode
338
+ try {
339
+ return decodeURIComponent(filenameStarMatch[1].replace(/(^['"]|['"]$)/g, ''));
340
+ } catch {
341
+ // fallback,如果 decode 失敗,直接傳回原值
342
+ return filenameStarMatch[1];
343
+ }
300
344
  }
345
+ // 2. 沒有 filename*,再找 filename
346
+ const filenameMatch = disposition.match(/filename\s*=\s*("?)([^";]+)\1/i);
347
+ if (filenameMatch && filenameMatch[2]) {
348
+ return filenameMatch[2];
349
+ }
350
+ // 3. 都沒有則 undefined
351
+ return undefined;
352
+ };
353
+ /**
354
+ * 解析 `filename` 回傳檔名、副檔名、MIME type
355
+ *
356
+ * @param filename 檔案名稱
357
+ *
358
+ * @example
359
+ *
360
+ * parseFileInfoFromFilename('image.jpg') // ['image', 'jpg', 'image/jpeg']
361
+ * parseFileInfoFromFilename('image') // ['image', '', 'application/octet-stream']
362
+ */ const parseFileInfoFromFilename = (filename)=>{
363
+ const lastDot = filename.lastIndexOf('.');
364
+ if (lastDot === -1) return [
365
+ filename,
366
+ '',
367
+ OtherMimeType
368
+ ]; // 沒有副檔名
369
+ return [
370
+ filename.slice(0, lastDot),
371
+ filename.slice(lastDot + 1),
372
+ MimeTypeMap[filename.slice(lastDot + 1)] ?? OtherMimeType
373
+ ];
301
374
  };
302
375
 
303
376
  // const isProduction: boolean = process.env.NODE_ENV === 'production';
@@ -1212,4 +1285,4 @@ function mergeRefs(refs) {
1212
1285
  return dayjs(endMonth, 'YYYYMM').subtract(1911, 'year').format('YYYYMM').substring(1);
1213
1286
  };
1214
1287
 
1215
- export { ByteSize, adToRocEra, camelCase2PascalCase, camelCase2SnakeCase, camelString2PascalString, camelString2SnakeString, convertBytes, createDataContext, createEnumLikeObject, debounce, deepMerge, extractEnumLikeObject, fakeApi, formatAmount, formatBytes, formatStarMask, generatePeriodArray, getCurrentPeriod, getMimeType, invariant, isChinese, isDateString, isDateTimeString, isEmail, isEnglish, isEqual, isNonZeroStart, isNumber, isNumberAtLeastN, isNumberN, isNumberNM, isServer, isTWMobile, isTWPhone, isTimeString, isValidPassword, maskString, mergeRefs, objectToSearchParams, omit, omitByValue, pascalCase2CamelCase, pascalCase2SnakeCase, pascalString2CamelString, pascalString2SnakeString, pick, pickByValue, rocEraToAd, searchParamsToObject, snakeCase2CamelCase, snakeCase2PascalCase, snakeString2CamelString, snakeString2PascalString, throttle, useValue, validTaxId, validateDateString, validateFileType, wait };
1288
+ export { ByteSize, MimeTypeMap, OtherMimeType, adToRocEra, camelCase2PascalCase, camelCase2SnakeCase, camelString2PascalString, camelString2SnakeString, convertBytes, createDataContext, createEnumLikeObject, debounce, deepMerge, extractEnumLikeObject, fakeApi, formatAmount, formatBytes, formatStarMask, generatePeriodArray, getCurrentPeriod, getMimeType, invariant, isChinese, isDateString, isDateTimeString, isEmail, isEnglish, isEqual, isNonZeroStart, isNumber, isNumberAtLeastN, isNumberN, isNumberNM, isServer, isTWMobile, isTWPhone, isTimeString, isValidPassword, maskString, mergeRefs, objectToSearchParams, omit, omitByValue, parseFileInfoFromFilename, parseFilenameFromDisposition, pascalCase2CamelCase, pascalCase2SnakeCase, pascalString2CamelString, pascalString2SnakeString, pick, pickByValue, rocEraToAd, searchParamsToObject, snakeCase2CamelCase, snakeCase2PascalCase, snakeString2CamelString, snakeString2PascalString, throttle, useValue, validTaxId, validateDateString, validateFileType, wait };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gateweb/react-utils",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "description": "React Utils for GateWeb",
5
5
  "homepage": "https://github.com/GatewebSolutions/react-utils",
6
6
  "files": [