pixel-react 1.4.1 → 1.4.3
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/.yarn/install-state.gz +0 -0
- package/lib/components/MiniModal/types.d.ts +1 -1
- package/lib/components/Select/types.d.ts +1 -0
- package/lib/index.d.ts +5 -2
- package/lib/index.esm.js +123 -61
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +123 -60
- package/lib/index.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/utils/downloadFile/saveFileFromBlob.d.ts +1 -0
- package/package.json +1 -1
- package/src/assets/icons/add_label_icon.svg +3 -0
- package/src/assets/icons/label_icon.svg +8 -0
- package/src/assets/icons/sample_template_first.svg +29 -0
- package/src/assets/icons/sample_template_second.svg +47 -0
- package/src/assets/styles/_fonts.scss +32 -2
- package/src/components/AllProjectsDropdown/AllProjectsDropdown.scss +2 -3
- package/src/components/AllProjectsDropdown/AllProjectsDropdown.tsx +1 -0
- package/src/components/AppHeader/AppHeader.scss +0 -1
- package/src/components/Icon/Icon.tsx +1 -0
- package/src/components/Icon/iconList.ts +10 -2
- package/src/components/MiniModal/MiniModal.stories.tsx +7 -5
- package/src/components/MiniModal/MiniModal.tsx +29 -23
- package/src/components/MiniModal/types.ts +1 -1
- package/src/components/Modal/Modal.tsx +2 -2
- package/src/components/Select/Select.tsx +2 -0
- package/src/components/Select/types.ts +5 -0
- package/src/components/StateDropdown/StateDropdown.tsx +18 -6
- package/src/components/Typography/Typography.scss +32 -2
- package/src/index.ts +2 -0
- package/src/utils/downloadFile/saveFileFromBlob.stories.tsx +62 -0
- package/src/utils/downloadFile/saveFileFromBlob.ts +40 -0
- /package/src/assets/icons/{fireflink_standard_template.svg → standard_template.svg} +0 -0
@@ -159,7 +159,9 @@ import DrawerMaximizeIcon from '../../assets/icons/drawer_maximize.svg?react';
|
|
159
159
|
import SaveAsStepIcon from '../../assets/icons/save_as_step.svg?react';
|
160
160
|
import SendToStepsIcon from '../../assets/icons/send_step.svg?react';
|
161
161
|
import NoLicenseFound from '../../assets/icons/no_license_found.svg?react';
|
162
|
-
import
|
162
|
+
import StandardTemplate from '../../assets/icons/standard_template.svg?react';
|
163
|
+
import SampleTemplateFirst from '../../assets/icons/sample_template_first.svg?react';
|
164
|
+
import SampleTemplateSecond from '../../assets/icons/sample_template_second.svg?react';
|
163
165
|
import DataProvider from '../../assets/icons/data_provider.svg?react';
|
164
166
|
import LinkExpired from '../../assets/icons/link_expired.svg?react';
|
165
167
|
import LinkReset from '../../assets/icons/reset-link.svg?react';
|
@@ -176,6 +178,8 @@ import dashboardSalesforceIcon from '../../assets/icons/salesforce_icon.svg?reac
|
|
176
178
|
import dashboardMsDynamicIcon from '../../assets/icons/ms_dynamic_icon.svg?react';
|
177
179
|
import MinimizeScript from '../../assets/icons/minimize_script.svg?react';
|
178
180
|
import MaximizeTree from '../../assets/icons/maximize_tree.svg?react';
|
181
|
+
import LabelIcon from '../../assets/icons/label_icon.svg?react';
|
182
|
+
import AddLabelIcon from '../../assets/icons/add_label_icon.svg?react';
|
179
183
|
|
180
184
|
Components['delete_info'] = DeleteInfoIcon;
|
181
185
|
Components['success'] = ToastSuccessIcon;
|
@@ -338,7 +342,9 @@ Components['flaky_status_icon'] = FlakyStatusIcon;
|
|
338
342
|
Components['drawer_maximize'] = DrawerMaximizeIcon;
|
339
343
|
Components['save_as_step'] = SaveAsStepIcon;
|
340
344
|
Components['send_to_steps'] = SendToStepsIcon;
|
341
|
-
Components['
|
345
|
+
Components['standard_template'] = StandardTemplate;
|
346
|
+
Components['sample_template_first'] = SampleTemplateFirst;
|
347
|
+
Components['sample_template_second'] = SampleTemplateSecond;
|
342
348
|
Components['no_license_found'] = NoLicenseFound;
|
343
349
|
Components['data_provider'] = DataProvider;
|
344
350
|
Components['link_expired'] = LinkExpired;
|
@@ -349,5 +355,7 @@ Components['user_warning'] = UserWarning;
|
|
349
355
|
Components['user_with_system'] = UserWithSystem;
|
350
356
|
Components['minimize_script'] = MinimizeScript;
|
351
357
|
Components['maximize_tree'] = MaximizeTree;
|
358
|
+
Components['label_icon'] = LabelIcon;
|
359
|
+
Components['add_label_icon'] = AddLabelIcon;
|
352
360
|
|
353
361
|
export default Components;
|
@@ -5,6 +5,7 @@ import Button from '../Button/Button';
|
|
5
5
|
import './MiniModal.scss';
|
6
6
|
import Typography from '../Typography';
|
7
7
|
import Icon from '../Icon';
|
8
|
+
import React from 'react';
|
8
9
|
|
9
10
|
const meta: Meta<typeof MiniModal> = {
|
10
11
|
title: 'Components/MiniModal',
|
@@ -41,14 +42,15 @@ const BasicModalComponent = () => {
|
|
41
42
|
<div className="ff-mini-modal-buttons-flex ff-mini-modal-gap-10">
|
42
43
|
<Button
|
43
44
|
onClick={() => openModal(1)}
|
45
|
+
id='112233'
|
44
46
|
ref={btnRef1}
|
45
|
-
label="
|
47
|
+
label="122"
|
46
48
|
variant={currentModal === 1 ? 'primary' : 'secondary'}
|
47
49
|
/>
|
48
50
|
|
49
51
|
{currentModal === 1 && (
|
50
52
|
<MiniModal
|
51
|
-
anchorRef=
|
53
|
+
anchorRef="112233"
|
52
54
|
modalProperties={{ width: 300, height: 180 }}
|
53
55
|
headerProps={
|
54
56
|
<>
|
@@ -254,14 +256,14 @@ export const CustomModalWithArrow = () => {
|
|
254
256
|
<div className="ff-mini-modal-buttons-flex ff-mini-modal-gap-10">
|
255
257
|
<Button
|
256
258
|
onClick={() => openModal(1)}
|
257
|
-
|
258
|
-
|
259
|
+
label="12"
|
260
|
+
id="1a2b"
|
259
261
|
variant={currentModal === 1 ? 'primary' : 'secondary'}
|
260
262
|
/>
|
261
263
|
|
262
264
|
{currentModal === 1 && (
|
263
265
|
<MiniModal
|
264
|
-
anchorRef=
|
266
|
+
anchorRef='1a2b'
|
265
267
|
modalProperties={{ width: 300, height: 250 }}
|
266
268
|
headerProps={
|
267
269
|
<>
|
@@ -40,6 +40,7 @@ const MiniModal = forwardRef<HTMLDivElement, MiniEditModalProps>(
|
|
40
40
|
});
|
41
41
|
const [isVisible, setIsVisible] = useState(false);
|
42
42
|
const modalRef = useRef<HTMLDivElement>(null);
|
43
|
+
|
43
44
|
// Function to calculate available space
|
44
45
|
const getAvailableSpace = (rect: Rect): AvailableSpace => {
|
45
46
|
const { top, left, bottom, right } = rect;
|
@@ -53,7 +54,18 @@ const MiniModal = forwardRef<HTMLDivElement, MiniEditModalProps>(
|
|
53
54
|
spaceBottom: viewportHeight - bottom,
|
54
55
|
};
|
55
56
|
};
|
56
|
-
|
57
|
+
|
58
|
+
// Helper function to get the anchor element
|
59
|
+
const getAnchorElement = () => {
|
60
|
+
if (typeof anchorRef === 'string') {
|
61
|
+
return document.getElementById(anchorRef);
|
62
|
+
}
|
63
|
+
return anchorRef?.current || null;
|
64
|
+
};
|
65
|
+
|
66
|
+
const anchorElement = getAnchorElement();
|
67
|
+
const anchorRect = anchorElement?.getBoundingClientRect();
|
68
|
+
|
57
69
|
if (!isWrapped && anchorRect) {
|
58
70
|
const availableSpace = getAvailableSpace(anchorRect);
|
59
71
|
switch (modalPosition) {
|
@@ -82,7 +94,7 @@ const MiniModal = forwardRef<HTMLDivElement, MiniEditModalProps>(
|
|
82
94
|
}
|
83
95
|
}
|
84
96
|
|
85
|
-
//
|
97
|
+
// Specify for the wrapper div left position
|
86
98
|
const calculateAnchorRefLeft = (anchorRefLeftNum?: number) => {
|
87
99
|
if (anchorRefLeftNum) {
|
88
100
|
return anchorRefLeftNum;
|
@@ -90,7 +102,7 @@ const MiniModal = forwardRef<HTMLDivElement, MiniEditModalProps>(
|
|
90
102
|
return 0;
|
91
103
|
};
|
92
104
|
|
93
|
-
//
|
105
|
+
// Specify for the arrow position in left or top
|
94
106
|
const getArrowClassName = () => {
|
95
107
|
if (leftTopArrow && modalPosition === 'right') {
|
96
108
|
return 'left-top-arrow';
|
@@ -99,13 +111,13 @@ const MiniModal = forwardRef<HTMLDivElement, MiniEditModalProps>(
|
|
99
111
|
};
|
100
112
|
const calculatedAnchorRefLeft = calculateAnchorRefLeft(anchorRefLeftNum);
|
101
113
|
|
102
|
-
//
|
114
|
+
// Specify specific wrapper modal position
|
103
115
|
const firstAnchor =
|
104
116
|
firstAnchorRef?.current &&
|
105
117
|
firstAnchorRef?.current?.getBoundingClientRect().left -
|
106
118
|
anchorLeftDistanceForWrapper;
|
107
119
|
|
108
|
-
//
|
120
|
+
// Specifying the modal top
|
109
121
|
const calculateModalTop = () => {
|
110
122
|
const safeHeight = modalProperties?.height ?? 0;
|
111
123
|
if (modalPosition === 'bottom') {
|
@@ -115,24 +127,18 @@ const MiniModal = forwardRef<HTMLDivElement, MiniEditModalProps>(
|
|
115
127
|
} else if (modalPosition === 'right') {
|
116
128
|
return leftTopArrow
|
117
129
|
? modalPositionState.top - (extraRightSpace?.leftTopArrow ?? 30)
|
118
|
-
: modalPositionState?.top -
|
119
|
-
safeHeight / (extraRightSpace?.middleLeftArrow ?? 3.5);
|
130
|
+
: modalPositionState?.top - safeHeight / (extraRightSpace?.middleLeftArrow ?? 3.5);
|
120
131
|
} else if (modalPosition === 'top') {
|
121
|
-
return (
|
122
|
-
modalPositionState.top -
|
123
|
-
(safeHeight + (extraTopSpace?.normalModal ?? 10))
|
124
|
-
);
|
132
|
+
return modalPositionState.top - (safeHeight + (extraTopSpace?.normalModal ?? 10));
|
125
133
|
}
|
126
134
|
return modalPositionState.top - safeHeight / 1.5;
|
127
135
|
};
|
128
136
|
const calculatedModalTop = calculateModalTop();
|
129
137
|
|
130
|
-
//
|
138
|
+
// Specifying the modal left
|
131
139
|
const calculateModalLeft = () => {
|
132
140
|
if (modalPosition === 'right') {
|
133
|
-
return (
|
134
|
-
modalPositionState.left + (extraLeftSpace?.rightAlignModal ?? 40)
|
135
|
-
);
|
141
|
+
return modalPositionState.left + (extraLeftSpace?.rightAlignModal ?? 40);
|
136
142
|
} else if (firstAnchorRef) {
|
137
143
|
return firstAnchor;
|
138
144
|
} else if (modalPosition === 'left') {
|
@@ -143,16 +149,16 @@ const MiniModal = forwardRef<HTMLDivElement, MiniEditModalProps>(
|
|
143
149
|
};
|
144
150
|
const calculatedModalLeft = calculateModalLeft();
|
145
151
|
|
146
|
-
//
|
152
|
+
// Handle escape and enter functionality
|
147
153
|
const handleEsc = useEscapeKey('Escape');
|
148
154
|
const handleEnter = useEscapeKey('Enter');
|
149
155
|
handleEsc(cancelButtonProps?.onClick);
|
150
156
|
handleEnter(proceedButtonProps?.onClick);
|
151
157
|
useClickOutside(modalRef, cancelButtonProps.onClick);
|
152
158
|
|
153
|
-
//
|
159
|
+
// Calculate the modal position
|
154
160
|
const calculatePosition = useCallback(() => {
|
155
|
-
const anchorRect =
|
161
|
+
const anchorRect = anchorElement?.getBoundingClientRect();
|
156
162
|
if (anchorRect) {
|
157
163
|
const { bottom, left } = anchorRect;
|
158
164
|
const { scrollX, scrollY } = window;
|
@@ -164,7 +170,7 @@ const MiniModal = forwardRef<HTMLDivElement, MiniEditModalProps>(
|
|
164
170
|
left: leftPosition,
|
165
171
|
});
|
166
172
|
}
|
167
|
-
}, [
|
173
|
+
}, [anchorElement]);
|
168
174
|
|
169
175
|
useEffect(() => {
|
170
176
|
const timeoutId = setTimeout(() => {
|
@@ -213,10 +219,10 @@ const MiniModal = forwardRef<HTMLDivElement, MiniEditModalProps>(
|
|
213
219
|
modalPosition === 'right'
|
214
220
|
? 'left'
|
215
221
|
: modalPosition === 'top'
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
222
|
+
? 'bottom'
|
223
|
+
: modalPosition === 'left'
|
224
|
+
? 'right'
|
225
|
+
: 'top'
|
220
226
|
} ${getArrowClassName()}`}
|
221
227
|
/>
|
222
228
|
)}
|
@@ -9,7 +9,7 @@ export interface MiniEditModalProps {
|
|
9
9
|
/**
|
10
10
|
* A reference to the button element that triggers the modal.
|
11
11
|
*/
|
12
|
-
anchorRef: RefObject<HTMLButtonElement
|
12
|
+
anchorRef: RefObject<HTMLButtonElement> | string;
|
13
13
|
/**
|
14
14
|
* Optional properties for configuring the modal header.
|
15
15
|
*/
|
@@ -58,9 +58,9 @@ const Modal: React.FC<ModalProps> = ({
|
|
58
58
|
<div
|
59
59
|
style={{
|
60
60
|
boxShadow: boxShadow,
|
61
|
-
borderRadius:
|
61
|
+
borderRadius: '12px' ,
|
62
62
|
}}
|
63
|
-
className="ff-modal-container"
|
63
|
+
className="ff-modal-container"
|
64
64
|
>
|
65
65
|
<div
|
66
66
|
className={classNames(
|
@@ -29,6 +29,7 @@ const Select: FC<SelectProps> = ({
|
|
29
29
|
height = 32,
|
30
30
|
width = '100%',
|
31
31
|
onBlur = () => {},
|
32
|
+
disableInput = false
|
32
33
|
}) => {
|
33
34
|
const selectWidth = typeof width === 'number' ? `${width}px` : width;
|
34
35
|
|
@@ -176,6 +177,7 @@ const Select: FC<SelectProps> = ({
|
|
176
177
|
style={{ zIndex: optionZIndex, color: selectedOptionColor }}
|
177
178
|
disabled={disabled}
|
178
179
|
onChange={handleChange}
|
180
|
+
readOnly={disableInput}
|
179
181
|
/>
|
180
182
|
{optionsRequired && (
|
181
183
|
<div
|
@@ -129,7 +129,9 @@ const StateDropdown = ({
|
|
129
129
|
optionsList={options}
|
130
130
|
selectedOption={selectedOption}
|
131
131
|
showLabel={false}
|
132
|
-
showBorder={
|
132
|
+
showBorder={false}
|
133
|
+
disableInput={true}
|
134
|
+
height={24}
|
133
135
|
/>
|
134
136
|
) : (
|
135
137
|
<Select
|
@@ -139,7 +141,9 @@ const StateDropdown = ({
|
|
139
141
|
optionsList={options}
|
140
142
|
selectedOption={selectedOption}
|
141
143
|
showLabel={false}
|
142
|
-
showBorder={
|
144
|
+
showBorder={false}
|
145
|
+
disableInput={true}
|
146
|
+
height={24}
|
143
147
|
/>
|
144
148
|
);
|
145
149
|
} else if (
|
@@ -154,7 +158,9 @@ const StateDropdown = ({
|
|
154
158
|
optionsList={options}
|
155
159
|
selectedOption={selectedOption}
|
156
160
|
showLabel={false}
|
157
|
-
showBorder={
|
161
|
+
showBorder={false}
|
162
|
+
disableInput={true}
|
163
|
+
height={24}
|
158
164
|
/>
|
159
165
|
);
|
160
166
|
} else if (currentState === 'APPROVED') {
|
@@ -166,7 +172,9 @@ const StateDropdown = ({
|
|
166
172
|
optionsList={options}
|
167
173
|
selectedOption={selectedOption}
|
168
174
|
showLabel={false}
|
169
|
-
showBorder={
|
175
|
+
showBorder={false}
|
176
|
+
disableInput={true}
|
177
|
+
height={24}
|
170
178
|
/>
|
171
179
|
);
|
172
180
|
} else if (currentState === 'REJECTED' && userHasOnlyViewAccess) {
|
@@ -178,7 +186,9 @@ const StateDropdown = ({
|
|
178
186
|
optionsList={options}
|
179
187
|
selectedOption={selectedOption}
|
180
188
|
showLabel={false}
|
181
|
-
showBorder={
|
189
|
+
showBorder={false}
|
190
|
+
disableInput={true}
|
191
|
+
height={24}
|
182
192
|
/>
|
183
193
|
);
|
184
194
|
} else if (currentState === 'NEW' && userHasOnlyViewAccess) {
|
@@ -190,7 +200,9 @@ const StateDropdown = ({
|
|
190
200
|
optionsList={options}
|
191
201
|
selectedOption={selectedOption}
|
192
202
|
showLabel={false}
|
193
|
-
showBorder={
|
203
|
+
showBorder={false}
|
204
|
+
disableInput={true}
|
205
|
+
height={24}
|
194
206
|
/>
|
195
207
|
);
|
196
208
|
} else {
|
@@ -1,10 +1,40 @@
|
|
1
|
-
@import url('https://fonts.cdnfonts.com/css/poppins');
|
2
|
-
|
3
1
|
@mixin fontPoppins($size: 16px) {
|
4
2
|
font-family: 'Poppins';
|
5
3
|
font-size: $size;
|
6
4
|
}
|
7
5
|
|
6
|
+
@font-face {
|
7
|
+
font-family: 'Poppins';
|
8
|
+
font-weight: 400;
|
9
|
+
src:
|
10
|
+
local('Poppins-Regular'),
|
11
|
+
url(../../fonts/Poppins/Poppins-Regular.ttf) format('truetype');
|
12
|
+
}
|
13
|
+
|
14
|
+
@font-face {
|
15
|
+
font-family: 'Poppins';
|
16
|
+
font-weight: 500;
|
17
|
+
src:
|
18
|
+
local('Poppins-Medium'),
|
19
|
+
url(../../fonts/Poppins/Poppins-Medium.ttf) format('truetype');
|
20
|
+
}
|
21
|
+
|
22
|
+
@font-face {
|
23
|
+
font-family: 'Poppins';
|
24
|
+
font-weight: 600;
|
25
|
+
src:
|
26
|
+
local('Poppins-SemiBold'),
|
27
|
+
url(../../fonts/Poppins/Poppins-SemiBold.ttf) format('truetype');
|
28
|
+
}
|
29
|
+
|
30
|
+
@font-face {
|
31
|
+
font-family: 'Poppins';
|
32
|
+
font-weight: 700;
|
33
|
+
src:
|
34
|
+
local('Poppins-Bold'),
|
35
|
+
url(../../fonts/Poppins/Poppins-Bold.ttf) format('truetype');
|
36
|
+
}
|
37
|
+
|
8
38
|
.ff-text {
|
9
39
|
@include fontPoppins();
|
10
40
|
|
package/src/index.ts
CHANGED
@@ -80,6 +80,7 @@ import MultiRadialChart from './components/Charts/MultiRadialChart';
|
|
80
80
|
import Editor from './components/Editor/Editor';
|
81
81
|
import { getSequentialPayload } from './utils/getSequentialPayload/getSequentialPayload';
|
82
82
|
import ConnectingBranch from './components/ConnectingBranch/ConnectingBranch';
|
83
|
+
import { saveFileFromBlob } from './utils/downloadFile/saveFileFromBlob'
|
83
84
|
|
84
85
|
export {
|
85
86
|
Button,
|
@@ -163,4 +164,5 @@ export {
|
|
163
164
|
getEncryptedData,
|
164
165
|
truncateText,
|
165
166
|
getSequentialPayload,
|
167
|
+
saveFileFromBlob,
|
166
168
|
};
|
@@ -0,0 +1,62 @@
|
|
1
|
+
import { saveFileFromBlob } from './saveFileFromBlob';
|
2
|
+
import { Toastify, toast } from '../../components/Toastify/Toastify';
|
3
|
+
|
4
|
+
export default {
|
5
|
+
title: 'Utils/saveFileFromBlob',
|
6
|
+
component: saveFileFromBlob,
|
7
|
+
};
|
8
|
+
|
9
|
+
export const Default = () => {
|
10
|
+
const testCases = [
|
11
|
+
{
|
12
|
+
blob: new Blob(['Hello, world!'], { type: 'text/plain' }),
|
13
|
+
filename: 'hello.txt',
|
14
|
+
},
|
15
|
+
{
|
16
|
+
blob: new Blob(['{ "key": "value" }'], { type: 'application/json' }),
|
17
|
+
filename: 'data.json',
|
18
|
+
},
|
19
|
+
{
|
20
|
+
blob: null,
|
21
|
+
filename: 'invalid.txt',
|
22
|
+
expectedError: 'Invalid Blob object',
|
23
|
+
},
|
24
|
+
];
|
25
|
+
|
26
|
+
const handleDownload = (blob: Blob | null, filename: string) => {
|
27
|
+
try {
|
28
|
+
if (blob instanceof Blob) {
|
29
|
+
saveFileFromBlob(blob, filename);
|
30
|
+
toast.success(`File "${filename}" downloaded successfully!`);
|
31
|
+
} else {
|
32
|
+
throw new Error('Invalid Blob object');
|
33
|
+
}
|
34
|
+
} catch (error: any) {
|
35
|
+
toast.error(error.message);
|
36
|
+
}
|
37
|
+
};
|
38
|
+
|
39
|
+
return (
|
40
|
+
<>
|
41
|
+
<div>
|
42
|
+
<h1>saveFileFromBlob - Test Cases</h1>
|
43
|
+
{testCases.map(({ blob, filename, expectedError }, index) => (
|
44
|
+
<div key={index} style={{ marginBottom: '1rem' }}>
|
45
|
+
<button
|
46
|
+
onClick={() => handleDownload(blob as Blob, filename)}
|
47
|
+
style={{ marginRight: '1rem' }}
|
48
|
+
>
|
49
|
+
Download {filename}
|
50
|
+
</button>
|
51
|
+
{expectedError && (
|
52
|
+
<span style={{ color: 'red' }}>
|
53
|
+
Expected Error: {expectedError}
|
54
|
+
</span>
|
55
|
+
)}
|
56
|
+
</div>
|
57
|
+
))}
|
58
|
+
</div>
|
59
|
+
<Toastify />
|
60
|
+
</>
|
61
|
+
);
|
62
|
+
};
|
@@ -0,0 +1,40 @@
|
|
1
|
+
// saveFileFromBlob.ts
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Save a file from a Blob object.
|
5
|
+
* This utility handles downloading files in browsers, including IE support.
|
6
|
+
*
|
7
|
+
* @param blob - The Blob object representing the file.
|
8
|
+
* @param filename - The name of the file to save.
|
9
|
+
*/
|
10
|
+
interface NavigatorWithMsSaveBlob extends Navigator {
|
11
|
+
msSaveOrOpenBlob?: (_blob: Blob, _filename: string) => void;
|
12
|
+
}
|
13
|
+
|
14
|
+
export const saveFileFromBlob = (blob: Blob, filename: string): void => {
|
15
|
+
if (!blob || !(blob instanceof Blob)) {
|
16
|
+
console.error('Invalid Blob object');
|
17
|
+
throw new Error('Invalid Blob object');
|
18
|
+
}
|
19
|
+
|
20
|
+
const navigatorWithMsSaveBlob = window.navigator as NavigatorWithMsSaveBlob;
|
21
|
+
|
22
|
+
if (navigatorWithMsSaveBlob.msSaveOrOpenBlob) {
|
23
|
+
const saveBlob = navigatorWithMsSaveBlob.msSaveOrOpenBlob;
|
24
|
+
if (saveBlob) {
|
25
|
+
saveBlob(blob, filename);
|
26
|
+
}
|
27
|
+
} else {
|
28
|
+
// For modern browsers
|
29
|
+
const anchorElement = document.createElement('a');
|
30
|
+
document.body.appendChild(anchorElement);
|
31
|
+
const objectURL = window.URL.createObjectURL(blob);
|
32
|
+
anchorElement.href = objectURL;
|
33
|
+
anchorElement.download = filename;
|
34
|
+
anchorElement.click();
|
35
|
+
setTimeout(() => {
|
36
|
+
window.URL.revokeObjectURL(objectURL);
|
37
|
+
document.body.removeChild(anchorElement);
|
38
|
+
}, 0);
|
39
|
+
}
|
40
|
+
};
|
File without changes
|