@douyinfe/semi-foundation 2.91.0 → 2.92.1
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/aiChatInput/foundation.ts +43 -5
- package/lib/cjs/aiChatInput/foundation.d.ts +8 -0
- package/lib/cjs/aiChatInput/foundation.js +41 -5
- package/lib/cjs/upload/foundation.d.ts +21 -1
- package/lib/cjs/upload/foundation.js +129 -15
- package/lib/es/aiChatInput/foundation.d.ts +8 -0
- package/lib/es/aiChatInput/foundation.js +41 -5
- package/lib/es/upload/foundation.d.ts +21 -1
- package/lib/es/upload/foundation.js +129 -15
- package/package.json +4 -4
- package/upload/foundation.ts +136 -14
|
@@ -90,12 +90,50 @@ export default class AIChatInputFoundation extends BaseFoundation<AIChatInputAda
|
|
|
90
90
|
this._adapter.focusEditor();
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Delete uploaded file item.
|
|
95
|
+
* Notes:
|
|
96
|
+
* - AIChatInput uses custom upload list UI, so we need to align remove behavior with Upload:
|
|
97
|
+
* 1) respect uploadProps.beforeRemove (support Promise)
|
|
98
|
+
* 2) call uploadProps.onRemove with (currentFile, nextFileList, currentFileItem)
|
|
99
|
+
* 3) still trigger onUploadChange/uploadProps.onChange for fileList update
|
|
100
|
+
*/
|
|
93
101
|
handleUploadFileDelete = (attachment: Attachment) => {
|
|
94
|
-
const {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
const { uploadProps } = this.getProps();
|
|
103
|
+
const { attachments: attachmentsFromState } = this.getStates();
|
|
104
|
+
const attachments = Array.isArray(attachmentsFromState) ? attachmentsFromState : [];
|
|
105
|
+
|
|
106
|
+
// Keep consistent with Upload: disabled means no-op
|
|
107
|
+
if (uploadProps?.disabled) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const index = attachments.findIndex(item => item.uid === attachment.uid);
|
|
112
|
+
if (index < 0) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const beforeRemove = uploadProps?.beforeRemove;
|
|
117
|
+
Promise.resolve(beforeRemove?.(attachment, attachments) ?? true).then(res => {
|
|
118
|
+
// prevent remove while user return false
|
|
119
|
+
if (res === false) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const newAttachments = attachments.slice();
|
|
124
|
+
newAttachments.splice(index, 1);
|
|
125
|
+
|
|
126
|
+
// Align Upload onRemove signature: (File, nextFileList, currentFileItem)
|
|
127
|
+
// currentFile: use original File instance when available
|
|
128
|
+
uploadProps?.onRemove?.(attachment.fileInstance as any, newAttachments as any, attachment as any);
|
|
129
|
+
|
|
130
|
+
// keep existing behavior: update state + notify upload change
|
|
131
|
+
this.onUploadChange({
|
|
132
|
+
currentFile: attachment,
|
|
133
|
+
fileList: newAttachments
|
|
134
|
+
});
|
|
135
|
+
}).catch(() => {
|
|
136
|
+
// if user pass reject promise, no need to do anything
|
|
99
137
|
});
|
|
100
138
|
}
|
|
101
139
|
|
|
@@ -36,6 +36,14 @@ export default class AIChatInputFoundation extends BaseFoundation<AIChatInputAda
|
|
|
36
36
|
changeTemplateVisible: (value: boolean) => void;
|
|
37
37
|
handlePaste: (files: File[]) => void;
|
|
38
38
|
handleSuggestionSelect: (suggestion: Suggestion) => void;
|
|
39
|
+
/**
|
|
40
|
+
* Delete uploaded file item.
|
|
41
|
+
* Notes:
|
|
42
|
+
* - AIChatInput uses custom upload list UI, so we need to align remove behavior with Upload:
|
|
43
|
+
* 1) respect uploadProps.beforeRemove (support Promise)
|
|
44
|
+
* 2) call uploadProps.onRemove with (currentFile, nextFileList, currentFileItem)
|
|
45
|
+
* 3) still trigger onUploadChange/uploadProps.onChange for fileList update
|
|
46
|
+
*/
|
|
39
47
|
handleUploadFileDelete: (attachment: Attachment) => void;
|
|
40
48
|
handleReferenceDelete: (reference: Reference) => void;
|
|
41
49
|
handleReferenceClick: (reference: Reference) => void;
|
|
@@ -42,14 +42,50 @@ class AIChatInputFoundation extends _foundation.default {
|
|
|
42
42
|
this._adapter.setContent(suggestion);
|
|
43
43
|
this._adapter.focusEditor();
|
|
44
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* Delete uploaded file item.
|
|
47
|
+
* Notes:
|
|
48
|
+
* - AIChatInput uses custom upload list UI, so we need to align remove behavior with Upload:
|
|
49
|
+
* 1) respect uploadProps.beforeRemove (support Promise)
|
|
50
|
+
* 2) call uploadProps.onRemove with (currentFile, nextFileList, currentFileItem)
|
|
51
|
+
* 3) still trigger onUploadChange/uploadProps.onChange for fileList update
|
|
52
|
+
*/
|
|
45
53
|
this.handleUploadFileDelete = attachment => {
|
|
54
|
+
var _a;
|
|
46
55
|
const {
|
|
47
|
-
|
|
56
|
+
uploadProps
|
|
57
|
+
} = this.getProps();
|
|
58
|
+
const {
|
|
59
|
+
attachments: attachmentsFromState
|
|
48
60
|
} = this.getStates();
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
const attachments = Array.isArray(attachmentsFromState) ? attachmentsFromState : [];
|
|
62
|
+
// Keep consistent with Upload: disabled means no-op
|
|
63
|
+
if (uploadProps === null || uploadProps === void 0 ? void 0 : uploadProps.disabled) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const index = attachments.findIndex(item => item.uid === attachment.uid);
|
|
67
|
+
if (index < 0) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const beforeRemove = uploadProps === null || uploadProps === void 0 ? void 0 : uploadProps.beforeRemove;
|
|
71
|
+
Promise.resolve((_a = beforeRemove === null || beforeRemove === void 0 ? void 0 : beforeRemove(attachment, attachments)) !== null && _a !== void 0 ? _a : true).then(res => {
|
|
72
|
+
var _a;
|
|
73
|
+
// prevent remove while user return false
|
|
74
|
+
if (res === false) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const newAttachments = attachments.slice();
|
|
78
|
+
newAttachments.splice(index, 1);
|
|
79
|
+
// Align Upload onRemove signature: (File, nextFileList, currentFileItem)
|
|
80
|
+
// currentFile: use original File instance when available
|
|
81
|
+
(_a = uploadProps === null || uploadProps === void 0 ? void 0 : uploadProps.onRemove) === null || _a === void 0 ? void 0 : _a.call(uploadProps, attachment.fileInstance, newAttachments, attachment);
|
|
82
|
+
// keep existing behavior: update state + notify upload change
|
|
83
|
+
this.onUploadChange({
|
|
84
|
+
currentFile: attachment,
|
|
85
|
+
fileList: newAttachments
|
|
86
|
+
});
|
|
87
|
+
}).catch(() => {
|
|
88
|
+
// if user pass reject promise, no need to do anything
|
|
53
89
|
});
|
|
54
90
|
};
|
|
55
91
|
this.handleReferenceDelete = reference => {
|
|
@@ -81,9 +81,29 @@ export interface UploadAdapter<P = Record<string, any>, S = Record<string, any>>
|
|
|
81
81
|
}
|
|
82
82
|
declare class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<UploadAdapter<P, S>, P, S> {
|
|
83
83
|
destroyState: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Canonical storage for objectURL created by this Upload instance.
|
|
86
|
+
* Do NOT rely on React state localUrls because setState is async and may lose updates
|
|
87
|
+
* when _createURL is called multiple times in a sync loop.
|
|
88
|
+
*/
|
|
89
|
+
_localUrls: Record<string, string>;
|
|
84
90
|
constructor(adapter: UploadAdapter<P, S>);
|
|
85
91
|
init(): void;
|
|
92
|
+
/**
|
|
93
|
+
* Sync internal objectURL map from current fileList.
|
|
94
|
+
* Only track blob: url with an in-memory File instance.
|
|
95
|
+
*/
|
|
96
|
+
_syncLocalUrlsFromFileList(): void;
|
|
86
97
|
destroy(): void;
|
|
98
|
+
/**
|
|
99
|
+
* Release objectURL created for a specific file uid
|
|
100
|
+
*/
|
|
101
|
+
_releaseFileUrl(uid: string): void;
|
|
102
|
+
/**
|
|
103
|
+
* Release all objectURL created by this Upload instance.
|
|
104
|
+
* Only call this when files are truly removed (e.g. clear).
|
|
105
|
+
*/
|
|
106
|
+
_releaseAllFileUrls(): void;
|
|
87
107
|
getError({ action, xhr, message, fileName }: {
|
|
88
108
|
action: string;
|
|
89
109
|
xhr: XMLHttpRequest;
|
|
@@ -156,7 +176,7 @@ declare class UploadFoundation<P = Record<string, any>, S = Record<string, any>>
|
|
|
156
176
|
fileInstance: CustomFile;
|
|
157
177
|
}): void;
|
|
158
178
|
handleClear(): void;
|
|
159
|
-
_createURL(fileInstance: CustomFile): string;
|
|
179
|
+
_createURL(fileInstance: CustomFile, uid: string): string;
|
|
160
180
|
releaseMemory(): void;
|
|
161
181
|
_releaseBlob(url: string): void;
|
|
162
182
|
isImage(file: CustomFile): boolean;
|
|
@@ -51,10 +51,19 @@ class UploadFoundation extends _foundation.default {
|
|
|
51
51
|
constructor(adapter) {
|
|
52
52
|
super(Object.assign({}, adapter));
|
|
53
53
|
this.destroyState = false;
|
|
54
|
+
/**
|
|
55
|
+
* Canonical storage for objectURL created by this Upload instance.
|
|
56
|
+
* Do NOT rely on React state localUrls because setState is async and may lose updates
|
|
57
|
+
* when _createURL is called multiple times in a sync loop.
|
|
58
|
+
*/
|
|
59
|
+
this._localUrls = {};
|
|
54
60
|
}
|
|
55
61
|
init() {
|
|
56
62
|
// make sure state reset, otherwise may cause upload abort in React StrictMode, like https://github.com/DouyinFE/semi-design/pull/843
|
|
57
63
|
this.destroyState = false;
|
|
64
|
+
// In controlled mode, parent may keep and pass back fileList with blob url.
|
|
65
|
+
// Sync them into internal map so later remove/clear can revoke correctly.
|
|
66
|
+
this._syncLocalUrlsFromFileList();
|
|
58
67
|
const {
|
|
59
68
|
disabled,
|
|
60
69
|
addOnPasting
|
|
@@ -63,17 +72,81 @@ class UploadFoundation extends _foundation.default {
|
|
|
63
72
|
this.bindPastingHandler();
|
|
64
73
|
}
|
|
65
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Sync internal objectURL map from current fileList.
|
|
77
|
+
* Only track blob: url with an in-memory File instance.
|
|
78
|
+
*/
|
|
79
|
+
_syncLocalUrlsFromFileList() {
|
|
80
|
+
const {
|
|
81
|
+
fileList
|
|
82
|
+
} = this.getStates();
|
|
83
|
+
const next = {};
|
|
84
|
+
if (!Array.isArray(fileList)) {
|
|
85
|
+
this._localUrls = {};
|
|
86
|
+
this._adapter.updateLocalUrls([]);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
fileList.forEach(item => {
|
|
90
|
+
const uid = item && item.uid;
|
|
91
|
+
const url = item && item.url;
|
|
92
|
+
const fileInstance = item && item.fileInstance;
|
|
93
|
+
const hasFileCtor = typeof File !== 'undefined';
|
|
94
|
+
const isFile = hasFileCtor && fileInstance instanceof File;
|
|
95
|
+
if (uid && typeof url === 'string' && url.startsWith('blob:') && isFile) {
|
|
96
|
+
next[uid] = url;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
this._localUrls = next;
|
|
100
|
+
this._adapter.updateLocalUrls(Object.values(this._localUrls));
|
|
101
|
+
}
|
|
66
102
|
destroy() {
|
|
67
103
|
const {
|
|
68
104
|
disabled,
|
|
69
105
|
addOnPasting
|
|
70
106
|
} = this.getProps();
|
|
71
|
-
|
|
107
|
+
// Do NOT revoke objectURL on unmount.
|
|
108
|
+
// In controlled mode, parent may keep and pass back fileList with blob url;
|
|
109
|
+
// revoking here will cause preview/blob ERR_FILE_NOT_FOUND after remount.
|
|
72
110
|
if (!disabled) {
|
|
73
111
|
this.unbindPastingHandler();
|
|
74
112
|
}
|
|
75
113
|
this.destroyState = true;
|
|
76
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Release objectURL created for a specific file uid
|
|
117
|
+
*/
|
|
118
|
+
_releaseFileUrl(uid) {
|
|
119
|
+
if (!uid) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const url = this._localUrls && this._localUrls[uid];
|
|
123
|
+
if (!url) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this._releaseBlob(url);
|
|
127
|
+
const next = Object.assign({}, this._localUrls || {});
|
|
128
|
+
delete next[uid];
|
|
129
|
+
this._localUrls = next;
|
|
130
|
+
this._adapter.updateLocalUrls(Object.values(this._localUrls));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Release all objectURL created by this Upload instance.
|
|
134
|
+
* Only call this when files are truly removed (e.g. clear).
|
|
135
|
+
*/
|
|
136
|
+
_releaseAllFileUrls() {
|
|
137
|
+
const localUrls = this._localUrls;
|
|
138
|
+
if (!localUrls || typeof localUrls !== 'object') {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
Object.keys(localUrls).forEach(uid => {
|
|
142
|
+
const url = localUrls[uid];
|
|
143
|
+
if (url) {
|
|
144
|
+
this._releaseBlob(url);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
this._localUrls = {};
|
|
148
|
+
this._adapter.updateLocalUrls([]);
|
|
149
|
+
}
|
|
77
150
|
getError(_ref) {
|
|
78
151
|
let {
|
|
79
152
|
action,
|
|
@@ -243,6 +316,11 @@ class UploadFoundation extends _foundation.default {
|
|
|
243
316
|
this._adapter.notifyFileSelect([newFile]);
|
|
244
317
|
const newFileItem = this.buildFileItem(newFile, uploadTrigger);
|
|
245
318
|
const newFileList = [...fileList];
|
|
319
|
+
// replace an item, release its previous objectURL
|
|
320
|
+
const oldItem = newFileList[replaceIdx];
|
|
321
|
+
if (oldItem && oldItem.uid) {
|
|
322
|
+
this._releaseFileUrl(oldItem.uid);
|
|
323
|
+
}
|
|
246
324
|
newFileList.splice(replaceIdx, 1, newFileItem);
|
|
247
325
|
this._adapter.notifyChange({
|
|
248
326
|
currentFile: newFileItem,
|
|
@@ -272,7 +350,7 @@ class UploadFoundation extends _foundation.default {
|
|
|
272
350
|
uid: fileInstance.uid,
|
|
273
351
|
percent: 0,
|
|
274
352
|
fileInstance,
|
|
275
|
-
url: this._createURL(fileInstance)
|
|
353
|
+
url: this._createURL(fileInstance, fileInstance.uid)
|
|
276
354
|
};
|
|
277
355
|
if (_sizeInvalid) {
|
|
278
356
|
_file._sizeInvalid = true;
|
|
@@ -497,6 +575,7 @@ class UploadFoundation extends _foundation.default {
|
|
|
497
575
|
} = buResult;
|
|
498
576
|
let newFileList = this.getState('fileList').slice();
|
|
499
577
|
if (autoRemove) {
|
|
578
|
+
this._releaseFileUrl(file.uid);
|
|
500
579
|
newFileList = newFileList.filter(item => item.uid !== file.uid);
|
|
501
580
|
} else {
|
|
502
581
|
const index = this._getFileIndex(file, newFileList);
|
|
@@ -510,7 +589,9 @@ class UploadFoundation extends _foundation.default {
|
|
|
510
589
|
newFileList[index].fileInstance = fileInstance;
|
|
511
590
|
newFileList[index].size = (0, _utils.getFileSize)(fileInstance.size);
|
|
512
591
|
newFileList[index].name = fileInstance.name;
|
|
513
|
-
|
|
592
|
+
// replace preview url, release old one first
|
|
593
|
+
this._releaseFileUrl(file.uid);
|
|
594
|
+
newFileList[index].url = this._createURL(fileInstance, file.uid);
|
|
514
595
|
}
|
|
515
596
|
newFileList[index].shouldUpload = shouldUpload;
|
|
516
597
|
}
|
|
@@ -723,8 +804,15 @@ class UploadFoundation extends _foundation.default {
|
|
|
723
804
|
status ? newFileList[index].status = status : null;
|
|
724
805
|
validateMessage ? newFileList[index].validateMessage = validateMessage : null;
|
|
725
806
|
name ? newFileList[index].name = name : null;
|
|
726
|
-
|
|
727
|
-
|
|
807
|
+
if (url) {
|
|
808
|
+
// if user replaces url, release local objectURL
|
|
809
|
+
this._releaseFileUrl(newFileList[index].uid);
|
|
810
|
+
newFileList[index].url = url;
|
|
811
|
+
}
|
|
812
|
+
if (autoRemove) {
|
|
813
|
+
this._releaseFileUrl(newFileList[index].uid);
|
|
814
|
+
newFileList.splice(index, 1);
|
|
815
|
+
}
|
|
728
816
|
}
|
|
729
817
|
this._adapter.notifySuccess(body, fileInstance, newFileList);
|
|
730
818
|
this._adapter.notifyChange({
|
|
@@ -757,6 +845,7 @@ class UploadFoundation extends _foundation.default {
|
|
|
757
845
|
return;
|
|
758
846
|
}
|
|
759
847
|
newFileList.splice(index, 1);
|
|
848
|
+
this._releaseFileUrl(file.uid);
|
|
760
849
|
this._adapter.notifyRemove(file.fileInstance, newFileList, file);
|
|
761
850
|
this._adapter.updateFileList(newFileList);
|
|
762
851
|
this._adapter.notifyChange({
|
|
@@ -811,6 +900,7 @@ class UploadFoundation extends _foundation.default {
|
|
|
811
900
|
if (res === false) {
|
|
812
901
|
return;
|
|
813
902
|
}
|
|
903
|
+
this._releaseAllFileUrls();
|
|
814
904
|
this._adapter.updateFileList([]);
|
|
815
905
|
this._adapter.notifyClear();
|
|
816
906
|
this._adapter.notifyChange({
|
|
@@ -820,26 +910,50 @@ class UploadFoundation extends _foundation.default {
|
|
|
820
910
|
// if user pass reject promise, no need to do anything
|
|
821
911
|
});
|
|
822
912
|
}
|
|
823
|
-
_createURL(fileInstance) {
|
|
913
|
+
_createURL(fileInstance, uid) {
|
|
824
914
|
// https://stackoverflow.com/questions/31742072/filereader-vs-window-url-createobjecturl
|
|
825
915
|
const url = URL.createObjectURL(fileInstance);
|
|
826
|
-
const {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
916
|
+
const next = Object.assign({}, this._localUrls || {});
|
|
917
|
+
// if there is an old url for this uid, release it first
|
|
918
|
+
if (uid && next[uid] && next[uid] !== url) {
|
|
919
|
+
this._releaseBlob(next[uid]);
|
|
920
|
+
}
|
|
921
|
+
if (uid) {
|
|
922
|
+
next[uid] = url;
|
|
923
|
+
}
|
|
924
|
+
this._localUrls = next;
|
|
925
|
+
// keep adapter/state in sync as a snapshot (legacy array type)
|
|
926
|
+
this._adapter.updateLocalUrls(Object.values(this._localUrls));
|
|
832
927
|
return url;
|
|
833
928
|
}
|
|
834
929
|
// 释放预览文件所占用的内存
|
|
835
930
|
// Release memory used by preview files
|
|
836
931
|
releaseMemory() {
|
|
932
|
+
// Prefer internal map (canonical). Keep old state-based fallback for safety.
|
|
933
|
+
if (this._localUrls && Object.keys(this._localUrls).length) {
|
|
934
|
+
this._releaseAllFileUrls();
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
837
937
|
const {
|
|
838
938
|
localUrls
|
|
839
939
|
} = this.getStates();
|
|
840
|
-
localUrls
|
|
841
|
-
|
|
842
|
-
}
|
|
940
|
+
if (!localUrls) {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
if (Array.isArray(localUrls)) {
|
|
944
|
+
localUrls.forEach(url => {
|
|
945
|
+
this._releaseBlob(url);
|
|
946
|
+
});
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
if (typeof localUrls === 'object') {
|
|
950
|
+
Object.keys(localUrls).forEach(uid => {
|
|
951
|
+
const url = localUrls[uid];
|
|
952
|
+
if (url) {
|
|
953
|
+
this._releaseBlob(url);
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
}
|
|
843
957
|
}
|
|
844
958
|
_releaseBlob(url) {
|
|
845
959
|
try {
|
|
@@ -36,6 +36,14 @@ export default class AIChatInputFoundation extends BaseFoundation<AIChatInputAda
|
|
|
36
36
|
changeTemplateVisible: (value: boolean) => void;
|
|
37
37
|
handlePaste: (files: File[]) => void;
|
|
38
38
|
handleSuggestionSelect: (suggestion: Suggestion) => void;
|
|
39
|
+
/**
|
|
40
|
+
* Delete uploaded file item.
|
|
41
|
+
* Notes:
|
|
42
|
+
* - AIChatInput uses custom upload list UI, so we need to align remove behavior with Upload:
|
|
43
|
+
* 1) respect uploadProps.beforeRemove (support Promise)
|
|
44
|
+
* 2) call uploadProps.onRemove with (currentFile, nextFileList, currentFileItem)
|
|
45
|
+
* 3) still trigger onUploadChange/uploadProps.onChange for fileList update
|
|
46
|
+
*/
|
|
39
47
|
handleUploadFileDelete: (attachment: Attachment) => void;
|
|
40
48
|
handleReferenceDelete: (reference: Reference) => void;
|
|
41
49
|
handleReferenceClick: (reference: Reference) => void;
|
|
@@ -35,14 +35,50 @@ export default class AIChatInputFoundation extends BaseFoundation {
|
|
|
35
35
|
this._adapter.setContent(suggestion);
|
|
36
36
|
this._adapter.focusEditor();
|
|
37
37
|
};
|
|
38
|
+
/**
|
|
39
|
+
* Delete uploaded file item.
|
|
40
|
+
* Notes:
|
|
41
|
+
* - AIChatInput uses custom upload list UI, so we need to align remove behavior with Upload:
|
|
42
|
+
* 1) respect uploadProps.beforeRemove (support Promise)
|
|
43
|
+
* 2) call uploadProps.onRemove with (currentFile, nextFileList, currentFileItem)
|
|
44
|
+
* 3) still trigger onUploadChange/uploadProps.onChange for fileList update
|
|
45
|
+
*/
|
|
38
46
|
this.handleUploadFileDelete = attachment => {
|
|
47
|
+
var _a;
|
|
39
48
|
const {
|
|
40
|
-
|
|
49
|
+
uploadProps
|
|
50
|
+
} = this.getProps();
|
|
51
|
+
const {
|
|
52
|
+
attachments: attachmentsFromState
|
|
41
53
|
} = this.getStates();
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
const attachments = Array.isArray(attachmentsFromState) ? attachmentsFromState : [];
|
|
55
|
+
// Keep consistent with Upload: disabled means no-op
|
|
56
|
+
if (uploadProps === null || uploadProps === void 0 ? void 0 : uploadProps.disabled) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const index = attachments.findIndex(item => item.uid === attachment.uid);
|
|
60
|
+
if (index < 0) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const beforeRemove = uploadProps === null || uploadProps === void 0 ? void 0 : uploadProps.beforeRemove;
|
|
64
|
+
Promise.resolve((_a = beforeRemove === null || beforeRemove === void 0 ? void 0 : beforeRemove(attachment, attachments)) !== null && _a !== void 0 ? _a : true).then(res => {
|
|
65
|
+
var _a;
|
|
66
|
+
// prevent remove while user return false
|
|
67
|
+
if (res === false) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const newAttachments = attachments.slice();
|
|
71
|
+
newAttachments.splice(index, 1);
|
|
72
|
+
// Align Upload onRemove signature: (File, nextFileList, currentFileItem)
|
|
73
|
+
// currentFile: use original File instance when available
|
|
74
|
+
(_a = uploadProps === null || uploadProps === void 0 ? void 0 : uploadProps.onRemove) === null || _a === void 0 ? void 0 : _a.call(uploadProps, attachment.fileInstance, newAttachments, attachment);
|
|
75
|
+
// keep existing behavior: update state + notify upload change
|
|
76
|
+
this.onUploadChange({
|
|
77
|
+
currentFile: attachment,
|
|
78
|
+
fileList: newAttachments
|
|
79
|
+
});
|
|
80
|
+
}).catch(() => {
|
|
81
|
+
// if user pass reject promise, no need to do anything
|
|
46
82
|
});
|
|
47
83
|
};
|
|
48
84
|
this.handleReferenceDelete = reference => {
|
|
@@ -81,9 +81,29 @@ export interface UploadAdapter<P = Record<string, any>, S = Record<string, any>>
|
|
|
81
81
|
}
|
|
82
82
|
declare class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<UploadAdapter<P, S>, P, S> {
|
|
83
83
|
destroyState: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Canonical storage for objectURL created by this Upload instance.
|
|
86
|
+
* Do NOT rely on React state localUrls because setState is async and may lose updates
|
|
87
|
+
* when _createURL is called multiple times in a sync loop.
|
|
88
|
+
*/
|
|
89
|
+
_localUrls: Record<string, string>;
|
|
84
90
|
constructor(adapter: UploadAdapter<P, S>);
|
|
85
91
|
init(): void;
|
|
92
|
+
/**
|
|
93
|
+
* Sync internal objectURL map from current fileList.
|
|
94
|
+
* Only track blob: url with an in-memory File instance.
|
|
95
|
+
*/
|
|
96
|
+
_syncLocalUrlsFromFileList(): void;
|
|
86
97
|
destroy(): void;
|
|
98
|
+
/**
|
|
99
|
+
* Release objectURL created for a specific file uid
|
|
100
|
+
*/
|
|
101
|
+
_releaseFileUrl(uid: string): void;
|
|
102
|
+
/**
|
|
103
|
+
* Release all objectURL created by this Upload instance.
|
|
104
|
+
* Only call this when files are truly removed (e.g. clear).
|
|
105
|
+
*/
|
|
106
|
+
_releaseAllFileUrls(): void;
|
|
87
107
|
getError({ action, xhr, message, fileName }: {
|
|
88
108
|
action: string;
|
|
89
109
|
xhr: XMLHttpRequest;
|
|
@@ -156,7 +176,7 @@ declare class UploadFoundation<P = Record<string, any>, S = Record<string, any>>
|
|
|
156
176
|
fileInstance: CustomFile;
|
|
157
177
|
}): void;
|
|
158
178
|
handleClear(): void;
|
|
159
|
-
_createURL(fileInstance: CustomFile): string;
|
|
179
|
+
_createURL(fileInstance: CustomFile, uid: string): string;
|
|
160
180
|
releaseMemory(): void;
|
|
161
181
|
_releaseBlob(url: string): void;
|
|
162
182
|
isImage(file: CustomFile): boolean;
|
|
@@ -44,10 +44,19 @@ class UploadFoundation extends BaseFoundation {
|
|
|
44
44
|
constructor(adapter) {
|
|
45
45
|
super(Object.assign({}, adapter));
|
|
46
46
|
this.destroyState = false;
|
|
47
|
+
/**
|
|
48
|
+
* Canonical storage for objectURL created by this Upload instance.
|
|
49
|
+
* Do NOT rely on React state localUrls because setState is async and may lose updates
|
|
50
|
+
* when _createURL is called multiple times in a sync loop.
|
|
51
|
+
*/
|
|
52
|
+
this._localUrls = {};
|
|
47
53
|
}
|
|
48
54
|
init() {
|
|
49
55
|
// make sure state reset, otherwise may cause upload abort in React StrictMode, like https://github.com/DouyinFE/semi-design/pull/843
|
|
50
56
|
this.destroyState = false;
|
|
57
|
+
// In controlled mode, parent may keep and pass back fileList with blob url.
|
|
58
|
+
// Sync them into internal map so later remove/clear can revoke correctly.
|
|
59
|
+
this._syncLocalUrlsFromFileList();
|
|
51
60
|
const {
|
|
52
61
|
disabled,
|
|
53
62
|
addOnPasting
|
|
@@ -56,17 +65,81 @@ class UploadFoundation extends BaseFoundation {
|
|
|
56
65
|
this.bindPastingHandler();
|
|
57
66
|
}
|
|
58
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Sync internal objectURL map from current fileList.
|
|
70
|
+
* Only track blob: url with an in-memory File instance.
|
|
71
|
+
*/
|
|
72
|
+
_syncLocalUrlsFromFileList() {
|
|
73
|
+
const {
|
|
74
|
+
fileList
|
|
75
|
+
} = this.getStates();
|
|
76
|
+
const next = {};
|
|
77
|
+
if (!Array.isArray(fileList)) {
|
|
78
|
+
this._localUrls = {};
|
|
79
|
+
this._adapter.updateLocalUrls([]);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
fileList.forEach(item => {
|
|
83
|
+
const uid = item && item.uid;
|
|
84
|
+
const url = item && item.url;
|
|
85
|
+
const fileInstance = item && item.fileInstance;
|
|
86
|
+
const hasFileCtor = typeof File !== 'undefined';
|
|
87
|
+
const isFile = hasFileCtor && fileInstance instanceof File;
|
|
88
|
+
if (uid && typeof url === 'string' && url.startsWith('blob:') && isFile) {
|
|
89
|
+
next[uid] = url;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
this._localUrls = next;
|
|
93
|
+
this._adapter.updateLocalUrls(Object.values(this._localUrls));
|
|
94
|
+
}
|
|
59
95
|
destroy() {
|
|
60
96
|
const {
|
|
61
97
|
disabled,
|
|
62
98
|
addOnPasting
|
|
63
99
|
} = this.getProps();
|
|
64
|
-
|
|
100
|
+
// Do NOT revoke objectURL on unmount.
|
|
101
|
+
// In controlled mode, parent may keep and pass back fileList with blob url;
|
|
102
|
+
// revoking here will cause preview/blob ERR_FILE_NOT_FOUND after remount.
|
|
65
103
|
if (!disabled) {
|
|
66
104
|
this.unbindPastingHandler();
|
|
67
105
|
}
|
|
68
106
|
this.destroyState = true;
|
|
69
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Release objectURL created for a specific file uid
|
|
110
|
+
*/
|
|
111
|
+
_releaseFileUrl(uid) {
|
|
112
|
+
if (!uid) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const url = this._localUrls && this._localUrls[uid];
|
|
116
|
+
if (!url) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
this._releaseBlob(url);
|
|
120
|
+
const next = Object.assign({}, this._localUrls || {});
|
|
121
|
+
delete next[uid];
|
|
122
|
+
this._localUrls = next;
|
|
123
|
+
this._adapter.updateLocalUrls(Object.values(this._localUrls));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Release all objectURL created by this Upload instance.
|
|
127
|
+
* Only call this when files are truly removed (e.g. clear).
|
|
128
|
+
*/
|
|
129
|
+
_releaseAllFileUrls() {
|
|
130
|
+
const localUrls = this._localUrls;
|
|
131
|
+
if (!localUrls || typeof localUrls !== 'object') {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
Object.keys(localUrls).forEach(uid => {
|
|
135
|
+
const url = localUrls[uid];
|
|
136
|
+
if (url) {
|
|
137
|
+
this._releaseBlob(url);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
this._localUrls = {};
|
|
141
|
+
this._adapter.updateLocalUrls([]);
|
|
142
|
+
}
|
|
70
143
|
getError(_ref) {
|
|
71
144
|
let {
|
|
72
145
|
action,
|
|
@@ -236,6 +309,11 @@ class UploadFoundation extends BaseFoundation {
|
|
|
236
309
|
this._adapter.notifyFileSelect([newFile]);
|
|
237
310
|
const newFileItem = this.buildFileItem(newFile, uploadTrigger);
|
|
238
311
|
const newFileList = [...fileList];
|
|
312
|
+
// replace an item, release its previous objectURL
|
|
313
|
+
const oldItem = newFileList[replaceIdx];
|
|
314
|
+
if (oldItem && oldItem.uid) {
|
|
315
|
+
this._releaseFileUrl(oldItem.uid);
|
|
316
|
+
}
|
|
239
317
|
newFileList.splice(replaceIdx, 1, newFileItem);
|
|
240
318
|
this._adapter.notifyChange({
|
|
241
319
|
currentFile: newFileItem,
|
|
@@ -265,7 +343,7 @@ class UploadFoundation extends BaseFoundation {
|
|
|
265
343
|
uid: fileInstance.uid,
|
|
266
344
|
percent: 0,
|
|
267
345
|
fileInstance,
|
|
268
|
-
url: this._createURL(fileInstance)
|
|
346
|
+
url: this._createURL(fileInstance, fileInstance.uid)
|
|
269
347
|
};
|
|
270
348
|
if (_sizeInvalid) {
|
|
271
349
|
_file._sizeInvalid = true;
|
|
@@ -490,6 +568,7 @@ class UploadFoundation extends BaseFoundation {
|
|
|
490
568
|
} = buResult;
|
|
491
569
|
let newFileList = this.getState('fileList').slice();
|
|
492
570
|
if (autoRemove) {
|
|
571
|
+
this._releaseFileUrl(file.uid);
|
|
493
572
|
newFileList = newFileList.filter(item => item.uid !== file.uid);
|
|
494
573
|
} else {
|
|
495
574
|
const index = this._getFileIndex(file, newFileList);
|
|
@@ -503,7 +582,9 @@ class UploadFoundation extends BaseFoundation {
|
|
|
503
582
|
newFileList[index].fileInstance = fileInstance;
|
|
504
583
|
newFileList[index].size = getFileSize(fileInstance.size);
|
|
505
584
|
newFileList[index].name = fileInstance.name;
|
|
506
|
-
|
|
585
|
+
// replace preview url, release old one first
|
|
586
|
+
this._releaseFileUrl(file.uid);
|
|
587
|
+
newFileList[index].url = this._createURL(fileInstance, file.uid);
|
|
507
588
|
}
|
|
508
589
|
newFileList[index].shouldUpload = shouldUpload;
|
|
509
590
|
}
|
|
@@ -716,8 +797,15 @@ class UploadFoundation extends BaseFoundation {
|
|
|
716
797
|
status ? newFileList[index].status = status : null;
|
|
717
798
|
validateMessage ? newFileList[index].validateMessage = validateMessage : null;
|
|
718
799
|
name ? newFileList[index].name = name : null;
|
|
719
|
-
|
|
720
|
-
|
|
800
|
+
if (url) {
|
|
801
|
+
// if user replaces url, release local objectURL
|
|
802
|
+
this._releaseFileUrl(newFileList[index].uid);
|
|
803
|
+
newFileList[index].url = url;
|
|
804
|
+
}
|
|
805
|
+
if (autoRemove) {
|
|
806
|
+
this._releaseFileUrl(newFileList[index].uid);
|
|
807
|
+
newFileList.splice(index, 1);
|
|
808
|
+
}
|
|
721
809
|
}
|
|
722
810
|
this._adapter.notifySuccess(body, fileInstance, newFileList);
|
|
723
811
|
this._adapter.notifyChange({
|
|
@@ -750,6 +838,7 @@ class UploadFoundation extends BaseFoundation {
|
|
|
750
838
|
return;
|
|
751
839
|
}
|
|
752
840
|
newFileList.splice(index, 1);
|
|
841
|
+
this._releaseFileUrl(file.uid);
|
|
753
842
|
this._adapter.notifyRemove(file.fileInstance, newFileList, file);
|
|
754
843
|
this._adapter.updateFileList(newFileList);
|
|
755
844
|
this._adapter.notifyChange({
|
|
@@ -804,6 +893,7 @@ class UploadFoundation extends BaseFoundation {
|
|
|
804
893
|
if (res === false) {
|
|
805
894
|
return;
|
|
806
895
|
}
|
|
896
|
+
this._releaseAllFileUrls();
|
|
807
897
|
this._adapter.updateFileList([]);
|
|
808
898
|
this._adapter.notifyClear();
|
|
809
899
|
this._adapter.notifyChange({
|
|
@@ -813,26 +903,50 @@ class UploadFoundation extends BaseFoundation {
|
|
|
813
903
|
// if user pass reject promise, no need to do anything
|
|
814
904
|
});
|
|
815
905
|
}
|
|
816
|
-
_createURL(fileInstance) {
|
|
906
|
+
_createURL(fileInstance, uid) {
|
|
817
907
|
// https://stackoverflow.com/questions/31742072/filereader-vs-window-url-createobjecturl
|
|
818
908
|
const url = URL.createObjectURL(fileInstance);
|
|
819
|
-
const {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
909
|
+
const next = Object.assign({}, this._localUrls || {});
|
|
910
|
+
// if there is an old url for this uid, release it first
|
|
911
|
+
if (uid && next[uid] && next[uid] !== url) {
|
|
912
|
+
this._releaseBlob(next[uid]);
|
|
913
|
+
}
|
|
914
|
+
if (uid) {
|
|
915
|
+
next[uid] = url;
|
|
916
|
+
}
|
|
917
|
+
this._localUrls = next;
|
|
918
|
+
// keep adapter/state in sync as a snapshot (legacy array type)
|
|
919
|
+
this._adapter.updateLocalUrls(Object.values(this._localUrls));
|
|
825
920
|
return url;
|
|
826
921
|
}
|
|
827
922
|
// 释放预览文件所占用的内存
|
|
828
923
|
// Release memory used by preview files
|
|
829
924
|
releaseMemory() {
|
|
925
|
+
// Prefer internal map (canonical). Keep old state-based fallback for safety.
|
|
926
|
+
if (this._localUrls && Object.keys(this._localUrls).length) {
|
|
927
|
+
this._releaseAllFileUrls();
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
830
930
|
const {
|
|
831
931
|
localUrls
|
|
832
932
|
} = this.getStates();
|
|
833
|
-
localUrls
|
|
834
|
-
|
|
835
|
-
}
|
|
933
|
+
if (!localUrls) {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
if (Array.isArray(localUrls)) {
|
|
937
|
+
localUrls.forEach(url => {
|
|
938
|
+
this._releaseBlob(url);
|
|
939
|
+
});
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (typeof localUrls === 'object') {
|
|
943
|
+
Object.keys(localUrls).forEach(uid => {
|
|
944
|
+
const url = localUrls[uid];
|
|
945
|
+
if (url) {
|
|
946
|
+
this._releaseBlob(url);
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
}
|
|
836
950
|
}
|
|
837
951
|
_releaseBlob(url) {
|
|
838
952
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@douyinfe/semi-foundation",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.92.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"clean": "rimraf lib",
|
|
@@ -9495,8 +9495,8 @@
|
|
|
9495
9495
|
}
|
|
9496
9496
|
},
|
|
9497
9497
|
"dependencies": {
|
|
9498
|
-
"@douyinfe/semi-animation": "2.
|
|
9499
|
-
"@douyinfe/semi-json-viewer-core": "2.
|
|
9498
|
+
"@douyinfe/semi-animation": "2.92.1",
|
|
9499
|
+
"@douyinfe/semi-json-viewer-core": "2.92.1",
|
|
9500
9500
|
"@mdx-js/mdx": "^3.0.1",
|
|
9501
9501
|
"async-validator": "^3.5.0",
|
|
9502
9502
|
"classnames": "^2.2.6",
|
|
@@ -9517,7 +9517,7 @@
|
|
|
9517
9517
|
"*.scss",
|
|
9518
9518
|
"*.css"
|
|
9519
9519
|
],
|
|
9520
|
-
"gitHead": "
|
|
9520
|
+
"gitHead": "4819138bf2ef9f2681a1ca2a53baf3ec755acd8a",
|
|
9521
9521
|
"devDependencies": {
|
|
9522
9522
|
"@babel/plugin-transform-runtime": "^7.15.8",
|
|
9523
9523
|
"@babel/preset-env": "^7.15.8",
|
package/upload/foundation.ts
CHANGED
|
@@ -81,6 +81,8 @@ export interface UploadAdapter<P = Record<string, any>, S = Record<string, any>>
|
|
|
81
81
|
notifyBeforeRemove: (file: BaseFileItem, fileList: Array<BaseFileItem>) => boolean | Promise<boolean>;
|
|
82
82
|
notifyBeforeClear: (fileList: Array<BaseFileItem>) => boolean | Promise<boolean>;
|
|
83
83
|
notifyChange: ({ currentFile, fileList }: { currentFile: BaseFileItem | null; fileList: Array<BaseFileItem> }) => void;
|
|
84
|
+
// Keep this as Array<string> for backward compatibility (external type stability).
|
|
85
|
+
// Internal logic uses uid->url map to avoid setState async timing issues.
|
|
84
86
|
updateLocalUrls: (urls: Array<string>) => void;
|
|
85
87
|
notifyClear: () => void;
|
|
86
88
|
notifyPreviewClick: (file: any) => void;
|
|
@@ -95,6 +97,12 @@ export interface UploadAdapter<P = Record<string, any>, S = Record<string, any>>
|
|
|
95
97
|
|
|
96
98
|
class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<UploadAdapter<P, S>, P, S> {
|
|
97
99
|
destroyState: boolean = false;
|
|
100
|
+
/**
|
|
101
|
+
* Canonical storage for objectURL created by this Upload instance.
|
|
102
|
+
* Do NOT rely on React state localUrls because setState is async and may lose updates
|
|
103
|
+
* when _createURL is called multiple times in a sync loop.
|
|
104
|
+
*/
|
|
105
|
+
_localUrls: Record<string, string> = {};
|
|
98
106
|
constructor(adapter: UploadAdapter<P, S>) {
|
|
99
107
|
super({ ...adapter });
|
|
100
108
|
}
|
|
@@ -102,21 +110,89 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
|
|
|
102
110
|
init(): void {
|
|
103
111
|
// make sure state reset, otherwise may cause upload abort in React StrictMode, like https://github.com/DouyinFE/semi-design/pull/843
|
|
104
112
|
this.destroyState = false;
|
|
113
|
+
// In controlled mode, parent may keep and pass back fileList with blob url.
|
|
114
|
+
// Sync them into internal map so later remove/clear can revoke correctly.
|
|
115
|
+
this._syncLocalUrlsFromFileList();
|
|
105
116
|
const { disabled, addOnPasting } = this.getProps();
|
|
106
117
|
if (addOnPasting && !disabled) {
|
|
107
118
|
this.bindPastingHandler();
|
|
108
119
|
}
|
|
109
120
|
}
|
|
110
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Sync internal objectURL map from current fileList.
|
|
124
|
+
* Only track blob: url with an in-memory File instance.
|
|
125
|
+
*/
|
|
126
|
+
_syncLocalUrlsFromFileList(): void {
|
|
127
|
+
const { fileList } = this.getStates() as any;
|
|
128
|
+
const next: Record<string, string> = {};
|
|
129
|
+
if (!Array.isArray(fileList)) {
|
|
130
|
+
this._localUrls = {};
|
|
131
|
+
this._adapter.updateLocalUrls([]);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
fileList.forEach((item: any) => {
|
|
135
|
+
const uid = item && item.uid;
|
|
136
|
+
const url = item && item.url;
|
|
137
|
+
const fileInstance = item && item.fileInstance;
|
|
138
|
+
const hasFileCtor = typeof File !== 'undefined';
|
|
139
|
+
const isFile = hasFileCtor && fileInstance instanceof File;
|
|
140
|
+
if (uid && typeof url === 'string' && url.startsWith('blob:') && isFile) {
|
|
141
|
+
next[uid] = url;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
this._localUrls = next;
|
|
145
|
+
this._adapter.updateLocalUrls(Object.values(this._localUrls));
|
|
146
|
+
}
|
|
147
|
+
|
|
111
148
|
destroy() {
|
|
112
149
|
const { disabled, addOnPasting } = this.getProps();
|
|
113
|
-
|
|
150
|
+
// Do NOT revoke objectURL on unmount.
|
|
151
|
+
// In controlled mode, parent may keep and pass back fileList with blob url;
|
|
152
|
+
// revoking here will cause preview/blob ERR_FILE_NOT_FOUND after remount.
|
|
114
153
|
if (!disabled) {
|
|
115
154
|
this.unbindPastingHandler();
|
|
116
155
|
}
|
|
117
156
|
this.destroyState = true;
|
|
118
157
|
}
|
|
119
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Release objectURL created for a specific file uid
|
|
161
|
+
*/
|
|
162
|
+
_releaseFileUrl(uid: string): void {
|
|
163
|
+
if (!uid) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const url = this._localUrls && this._localUrls[uid];
|
|
167
|
+
if (!url) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
this._releaseBlob(url);
|
|
171
|
+
const next = { ...(this._localUrls || {}) };
|
|
172
|
+
delete next[uid];
|
|
173
|
+
this._localUrls = next;
|
|
174
|
+
this._adapter.updateLocalUrls(Object.values(this._localUrls));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Release all objectURL created by this Upload instance.
|
|
179
|
+
* Only call this when files are truly removed (e.g. clear).
|
|
180
|
+
*/
|
|
181
|
+
_releaseAllFileUrls(): void {
|
|
182
|
+
const localUrls = this._localUrls;
|
|
183
|
+
if (!localUrls || typeof localUrls !== 'object') {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
Object.keys(localUrls).forEach(uid => {
|
|
187
|
+
const url = localUrls[uid];
|
|
188
|
+
if (url) {
|
|
189
|
+
this._releaseBlob(url);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
this._localUrls = {};
|
|
193
|
+
this._adapter.updateLocalUrls([]);
|
|
194
|
+
}
|
|
195
|
+
|
|
120
196
|
getError({ action, xhr, message, fileName }: { action: string;xhr: XMLHttpRequest;message?: string;fileName: string }): XhrError {
|
|
121
197
|
const status = xhr ? xhr.status : 0;
|
|
122
198
|
const msg = message || `cannot post ${fileName} to ${action}, xhr status: ${status}'`;
|
|
@@ -272,6 +348,12 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
|
|
|
272
348
|
this._adapter.notifyFileSelect([newFile]);
|
|
273
349
|
const newFileItem = this.buildFileItem(newFile, uploadTrigger);
|
|
274
350
|
const newFileList = [...fileList];
|
|
351
|
+
|
|
352
|
+
// replace an item, release its previous objectURL
|
|
353
|
+
const oldItem = newFileList[replaceIdx];
|
|
354
|
+
if (oldItem && oldItem.uid) {
|
|
355
|
+
this._releaseFileUrl(oldItem.uid);
|
|
356
|
+
}
|
|
275
357
|
newFileList.splice(replaceIdx, 1, newFileItem);
|
|
276
358
|
this._adapter.notifyChange({ currentFile: newFileItem, fileList: newFileList });
|
|
277
359
|
this._adapter.updateFileList(newFileList, () => {
|
|
@@ -297,7 +379,7 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
|
|
|
297
379
|
uid: fileInstance.uid,
|
|
298
380
|
percent: 0,
|
|
299
381
|
fileInstance,
|
|
300
|
-
url: this._createURL(fileInstance),
|
|
382
|
+
url: this._createURL(fileInstance, fileInstance.uid),
|
|
301
383
|
};
|
|
302
384
|
|
|
303
385
|
if (_sizeInvalid) {
|
|
@@ -497,6 +579,7 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
|
|
|
497
579
|
const { shouldUpload, status, autoRemove, validateMessage, fileInstance } = buResult;
|
|
498
580
|
let newFileList: Array<BaseFileItem> = this.getState('fileList').slice();
|
|
499
581
|
if (autoRemove) {
|
|
582
|
+
this._releaseFileUrl(file.uid);
|
|
500
583
|
newFileList = newFileList.filter(item => item.uid !== file.uid);
|
|
501
584
|
} else {
|
|
502
585
|
const index = this._getFileIndex(file, newFileList);
|
|
@@ -510,7 +593,9 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
|
|
|
510
593
|
newFileList[index].fileInstance = fileInstance;
|
|
511
594
|
newFileList[index].size = getFileSize(fileInstance.size);
|
|
512
595
|
newFileList[index].name = fileInstance.name;
|
|
513
|
-
|
|
596
|
+
// replace preview url, release old one first
|
|
597
|
+
this._releaseFileUrl(file.uid);
|
|
598
|
+
newFileList[index].url = this._createURL(fileInstance, file.uid);
|
|
514
599
|
}
|
|
515
600
|
newFileList[index].shouldUpload = shouldUpload;
|
|
516
601
|
}
|
|
@@ -674,8 +759,15 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
|
|
|
674
759
|
status ? (newFileList[index].status = status) : null;
|
|
675
760
|
validateMessage ? (newFileList[index].validateMessage = validateMessage) : null;
|
|
676
761
|
name ? (newFileList[index].name = name) : null;
|
|
677
|
-
|
|
678
|
-
|
|
762
|
+
if (url) {
|
|
763
|
+
// if user replaces url, release local objectURL
|
|
764
|
+
this._releaseFileUrl(newFileList[index].uid);
|
|
765
|
+
newFileList[index].url = url;
|
|
766
|
+
}
|
|
767
|
+
if (autoRemove) {
|
|
768
|
+
this._releaseFileUrl(newFileList[index].uid);
|
|
769
|
+
newFileList.splice(index, 1);
|
|
770
|
+
}
|
|
679
771
|
}
|
|
680
772
|
this._adapter.notifySuccess(body, fileInstance, newFileList);
|
|
681
773
|
this._adapter.notifyChange({ fileList: newFileList, currentFile: newFileList[index] });
|
|
@@ -706,6 +798,8 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
|
|
|
706
798
|
}
|
|
707
799
|
newFileList.splice(index, 1);
|
|
708
800
|
|
|
801
|
+
this._releaseFileUrl(file.uid);
|
|
802
|
+
|
|
709
803
|
this._adapter.notifyRemove(file.fileInstance, newFileList, file);
|
|
710
804
|
this._adapter.updateFileList(newFileList);
|
|
711
805
|
this._adapter.notifyChange({ fileList: newFileList, currentFile: file });
|
|
@@ -742,6 +836,7 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
|
|
|
742
836
|
if (res === false) {
|
|
743
837
|
return;
|
|
744
838
|
}
|
|
839
|
+
this._releaseAllFileUrls();
|
|
745
840
|
this._adapter.updateFileList([]);
|
|
746
841
|
this._adapter.notifyClear();
|
|
747
842
|
this._adapter.notifyChange({ fileList: [] } as any);
|
|
@@ -750,23 +845,50 @@ class UploadFoundation<P = Record<string, any>, S = Record<string, any>> extends
|
|
|
750
845
|
});
|
|
751
846
|
}
|
|
752
847
|
|
|
753
|
-
_createURL(fileInstance: CustomFile): string {
|
|
848
|
+
_createURL(fileInstance: CustomFile, uid: string): string {
|
|
754
849
|
// https://stackoverflow.com/questions/31742072/filereader-vs-window-url-createobjecturl
|
|
755
850
|
const url = URL.createObjectURL(fileInstance);
|
|
756
|
-
const
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
851
|
+
const next = { ...(this._localUrls || {}) };
|
|
852
|
+
// if there is an old url for this uid, release it first
|
|
853
|
+
if (uid && next[uid] && next[uid] !== url) {
|
|
854
|
+
this._releaseBlob(next[uid]);
|
|
855
|
+
}
|
|
856
|
+
if (uid) {
|
|
857
|
+
next[uid] = url;
|
|
858
|
+
}
|
|
859
|
+
this._localUrls = next;
|
|
860
|
+
// keep adapter/state in sync as a snapshot (legacy array type)
|
|
861
|
+
this._adapter.updateLocalUrls(Object.values(this._localUrls));
|
|
760
862
|
return url;
|
|
761
863
|
}
|
|
762
864
|
|
|
763
865
|
// 释放预览文件所占用的内存
|
|
764
866
|
// Release memory used by preview files
|
|
765
867
|
releaseMemory(): void {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
this.
|
|
769
|
-
|
|
868
|
+
// Prefer internal map (canonical). Keep old state-based fallback for safety.
|
|
869
|
+
if (this._localUrls && Object.keys(this._localUrls).length) {
|
|
870
|
+
this._releaseAllFileUrls();
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const { localUrls } = this.getStates() as any;
|
|
875
|
+
if (!localUrls) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (Array.isArray(localUrls)) {
|
|
879
|
+
localUrls.forEach(url => {
|
|
880
|
+
this._releaseBlob(url);
|
|
881
|
+
});
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
if (typeof localUrls === 'object') {
|
|
885
|
+
Object.keys(localUrls).forEach(uid => {
|
|
886
|
+
const url = localUrls[uid];
|
|
887
|
+
if (url) {
|
|
888
|
+
this._releaseBlob(url);
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
}
|
|
770
892
|
}
|
|
771
893
|
|
|
772
894
|
_releaseBlob(url: string): void {
|