@lumir-company/editor 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1299 -966
- package/dist/api/link-preview.d.mts +40 -0
- package/dist/api/link-preview.d.ts +40 -0
- package/dist/api/link-preview.js +190 -0
- package/dist/api/link-preview.js.map +1 -0
- package/dist/api/link-preview.mjs +163 -0
- package/dist/api/link-preview.mjs.map +1 -0
- package/dist/index.d.mts +18 -4
- package/dist/index.d.ts +18 -4
- package/dist/index.js +411 -89
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +412 -90
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +977 -964
- package/package.json +10 -4
package/dist/index.js
CHANGED
|
@@ -91,7 +91,10 @@ var createS3Uploader = (config) => {
|
|
|
91
91
|
path,
|
|
92
92
|
fileNameTransform,
|
|
93
93
|
appendUUID,
|
|
94
|
-
preserveExtension = true
|
|
94
|
+
preserveExtension = true,
|
|
95
|
+
onProgress,
|
|
96
|
+
uploadTimeoutMs = 12e4,
|
|
97
|
+
maxRetries = 2
|
|
95
98
|
} = config;
|
|
96
99
|
if (!apiEndpoint || apiEndpoint.trim() === "") {
|
|
97
100
|
throw new Error(
|
|
@@ -130,6 +133,15 @@ var createS3Uploader = (config) => {
|
|
|
130
133
|
}
|
|
131
134
|
return `${env}/${path}/${filename}`;
|
|
132
135
|
};
|
|
136
|
+
const debugLog = (loc, msg, data) => {
|
|
137
|
+
const p = fetch("http://127.0.0.1:7686/ingest/1f8ee1c5-0cf0-4ae7-91ed-5ea7ed17130a", {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "b73262" },
|
|
140
|
+
body: JSON.stringify({ sessionId: "b73262", location: loc, message: msg, data, timestamp: Date.now() })
|
|
141
|
+
});
|
|
142
|
+
if (p && typeof p.catch === "function") p.catch(() => {
|
|
143
|
+
});
|
|
144
|
+
};
|
|
133
145
|
return async (file) => {
|
|
134
146
|
try {
|
|
135
147
|
if (!apiEndpoint || apiEndpoint.trim() === "") {
|
|
@@ -138,11 +150,26 @@ var createS3Uploader = (config) => {
|
|
|
138
150
|
);
|
|
139
151
|
}
|
|
140
152
|
const fileName = generateHierarchicalFileName(file);
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
);
|
|
153
|
+
const contentType = file.type || "application/octet-stream";
|
|
154
|
+
const presignedUrlFull = `${apiEndpoint}?key=${encodeURIComponent(fileName)}&contentType=${encodeURIComponent(contentType)}`;
|
|
155
|
+
const tPresigned = Date.now();
|
|
156
|
+
debugLog("s3:step1:presignedReq", "fetching presigned URL", {
|
|
157
|
+
fileName,
|
|
158
|
+
contentType,
|
|
159
|
+
apiEndpoint
|
|
160
|
+
});
|
|
161
|
+
const response = await fetch(presignedUrlFull);
|
|
162
|
+
debugLog("s3:step2:presignedRes", "presigned response", {
|
|
163
|
+
ok: response.ok,
|
|
164
|
+
status: response.status,
|
|
165
|
+
elapsedMs: Date.now() - tPresigned
|
|
166
|
+
});
|
|
144
167
|
if (!response.ok) {
|
|
145
168
|
const errorText = await response.text() || "";
|
|
169
|
+
debugLog("s3:step2b:presignedErr", "presigned failed", {
|
|
170
|
+
status: response.status,
|
|
171
|
+
errorText: errorText.slice(0, 200)
|
|
172
|
+
});
|
|
146
173
|
throw new Error(
|
|
147
174
|
`Failed to get presigned URL: ${response.statusText}, ${errorText}`
|
|
148
175
|
);
|
|
@@ -151,17 +178,64 @@ var createS3Uploader = (config) => {
|
|
|
151
178
|
const { presignedUrl, publicUrl } = responseData;
|
|
152
179
|
const validatedPresignedUrl = validateS3Url(presignedUrl, "presignedUrl");
|
|
153
180
|
const validatedPublicUrl = validateS3Url(publicUrl, "publicUrl");
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
181
|
+
const tPut = Date.now();
|
|
182
|
+
debugLog("s3:step3:putReq", "S3 PUT request", { publicUrlLen: validatedPublicUrl?.length });
|
|
183
|
+
let lastError;
|
|
184
|
+
const attempts = maxRetries + 1;
|
|
185
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
186
|
+
try {
|
|
187
|
+
if (onProgress && typeof XMLHttpRequest !== "undefined") {
|
|
188
|
+
await new Promise((resolve, reject) => {
|
|
189
|
+
const xhr = new XMLHttpRequest();
|
|
190
|
+
xhr.timeout = uploadTimeoutMs;
|
|
191
|
+
xhr.upload.onprogress = (e) => {
|
|
192
|
+
if (e.lengthComputable) {
|
|
193
|
+
onProgress(Math.min(100, Math.round(e.loaded / e.total * 100)));
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
xhr.onload = () => {
|
|
197
|
+
if (xhr.status >= 200 && xhr.status < 300) resolve();
|
|
198
|
+
else reject(new Error(`Failed to upload file: ${xhr.statusText}`));
|
|
199
|
+
};
|
|
200
|
+
xhr.onerror = () => reject(new Error("Upload failed"));
|
|
201
|
+
xhr.ontimeout = () => reject(new Error("Upload timeout"));
|
|
202
|
+
xhr.open("PUT", validatedPresignedUrl);
|
|
203
|
+
xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
|
|
204
|
+
xhr.send(file);
|
|
205
|
+
});
|
|
206
|
+
} else {
|
|
207
|
+
const controller = new AbortController();
|
|
208
|
+
const timeoutId = setTimeout(() => controller.abort(), uploadTimeoutMs);
|
|
209
|
+
const uploadResponse = await fetch(validatedPresignedUrl, {
|
|
210
|
+
method: "PUT",
|
|
211
|
+
headers: {
|
|
212
|
+
"Content-Type": file.type || "application/octet-stream"
|
|
213
|
+
},
|
|
214
|
+
body: file,
|
|
215
|
+
signal: controller.signal
|
|
216
|
+
});
|
|
217
|
+
clearTimeout(timeoutId);
|
|
218
|
+
debugLog("s3:step4:putRes", "S3 PUT response", {
|
|
219
|
+
ok: uploadResponse.ok,
|
|
220
|
+
status: uploadResponse.status,
|
|
221
|
+
putElapsedMs: Date.now() - tPut
|
|
222
|
+
});
|
|
223
|
+
if (!uploadResponse.ok) {
|
|
224
|
+
throw new Error(`Failed to upload file: ${uploadResponse.statusText}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
debugLog("s3:step5:return", "returning publicUrl", { urlPrefix: validatedPublicUrl.slice(0, 80) });
|
|
228
|
+
return validatedPublicUrl;
|
|
229
|
+
} catch (err) {
|
|
230
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
231
|
+
if (attempt < attempts - 1) {
|
|
232
|
+
debugLog("s3:putRetry", "PUT failed, retrying", { attempt: attempt + 1, attempts });
|
|
233
|
+
} else {
|
|
234
|
+
throw lastError;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
163
237
|
}
|
|
164
|
-
|
|
238
|
+
throw lastError ?? new Error("Upload failed");
|
|
165
239
|
} catch (error) {
|
|
166
240
|
console.error("S3 upload failed:", error);
|
|
167
241
|
throw error;
|
|
@@ -2438,15 +2512,14 @@ var LumirEditorError = class _LumirEditorError extends Error {
|
|
|
2438
2512
|
}
|
|
2439
2513
|
/**
|
|
2440
2514
|
* 잘못된 파일 형식 에러 생성
|
|
2515
|
+
* @param allowVideoUpload true이면 "image and video" 메시지 사용
|
|
2441
2516
|
*/
|
|
2442
|
-
static invalidFileType(fileName) {
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
}
|
|
2449
|
-
);
|
|
2517
|
+
static invalidFileType(fileName, allowVideoUpload) {
|
|
2518
|
+
const message = allowVideoUpload === true ? `Invalid file type: ${fileName}. Only image and video files are allowed.` : `Invalid file type: ${fileName}. Only image files are allowed.`;
|
|
2519
|
+
return new _LumirEditorError(message, {
|
|
2520
|
+
code: "INVALID_FILE_TYPE",
|
|
2521
|
+
context: { fileName }
|
|
2522
|
+
});
|
|
2450
2523
|
}
|
|
2451
2524
|
/**
|
|
2452
2525
|
* S3 설정 에러 생성
|
|
@@ -2469,10 +2542,38 @@ var LumirEditorError = class _LumirEditorError extends Error {
|
|
|
2469
2542
|
|
|
2470
2543
|
// src/constants/limits.ts
|
|
2471
2544
|
var MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
2545
|
+
var MAX_VIDEO_FILE_SIZE = 100 * 1024 * 1024;
|
|
2472
2546
|
var BLOCKED_EXTENSIONS = [".svg", ".svgz"];
|
|
2547
|
+
var ALLOWED_VIDEO_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
2548
|
+
"video/mp4",
|
|
2549
|
+
"video/webm",
|
|
2550
|
+
"video/ogg",
|
|
2551
|
+
"video/quicktime"
|
|
2552
|
+
// .mov
|
|
2553
|
+
]);
|
|
2554
|
+
var ALLOWED_VIDEO_EXTENSIONS = [
|
|
2555
|
+
".mp4",
|
|
2556
|
+
".webm",
|
|
2557
|
+
".ogg",
|
|
2558
|
+
".mov"
|
|
2559
|
+
];
|
|
2473
2560
|
|
|
2474
2561
|
// src/components/LumirEditor.tsx
|
|
2475
2562
|
var import_jsx_runtime16 = require("react/jsx-runtime");
|
|
2563
|
+
var DEBUG_LOG = (loc, msg, data) => {
|
|
2564
|
+
fetch("http://127.0.0.1:7686/ingest/1f8ee1c5-0cf0-4ae7-91ed-5ea7ed17130a", {
|
|
2565
|
+
method: "POST",
|
|
2566
|
+
headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "b73262" },
|
|
2567
|
+
body: JSON.stringify({
|
|
2568
|
+
sessionId: "b73262",
|
|
2569
|
+
location: loc,
|
|
2570
|
+
message: msg,
|
|
2571
|
+
data,
|
|
2572
|
+
timestamp: Date.now()
|
|
2573
|
+
})
|
|
2574
|
+
}).catch(() => {
|
|
2575
|
+
});
|
|
2576
|
+
};
|
|
2476
2577
|
var ContentUtils = class {
|
|
2477
2578
|
/**
|
|
2478
2579
|
* JSON 문자열의 유효성을 검증합니다
|
|
@@ -2601,6 +2702,25 @@ var isImageFile = (file) => {
|
|
|
2601
2702
|
}
|
|
2602
2703
|
return file.type?.startsWith("image/") || !file.type && /\.(png|jpe?g|gif|webp|bmp)$/i.test(fileName);
|
|
2603
2704
|
};
|
|
2705
|
+
var isVideoFile = (file) => {
|
|
2706
|
+
const sizeOk = file.size > 0 && file.size <= MAX_VIDEO_FILE_SIZE;
|
|
2707
|
+
const fileName = file.name?.toLowerCase() || "";
|
|
2708
|
+
const mimeMatch = ALLOWED_VIDEO_MIME_TYPES.has(file.type);
|
|
2709
|
+
const videoPrefix = typeof file.type === "string" && file.type.startsWith("video/");
|
|
2710
|
+
const extMatch = !file.type && ALLOWED_VIDEO_EXTENSIONS.some((ext) => fileName.endsWith(ext));
|
|
2711
|
+
const result = sizeOk && (mimeMatch || videoPrefix || extMatch);
|
|
2712
|
+
DEBUG_LOG("isVideoFile:check", "result", {
|
|
2713
|
+
fileName: file.name,
|
|
2714
|
+
fileType: file.type,
|
|
2715
|
+
fileSize: file.size,
|
|
2716
|
+
sizeOk,
|
|
2717
|
+
mimeMatch,
|
|
2718
|
+
videoPrefix,
|
|
2719
|
+
extMatch,
|
|
2720
|
+
result
|
|
2721
|
+
});
|
|
2722
|
+
return result;
|
|
2723
|
+
};
|
|
2604
2724
|
var isHtmlFile = (file) => {
|
|
2605
2725
|
return file.size > 0 && (file.type === "text/html" || file.name?.toLowerCase().endsWith(".html") || file.name?.toLowerCase().endsWith(".htm"));
|
|
2606
2726
|
};
|
|
@@ -2614,15 +2734,17 @@ var escapeHtml = (str) => {
|
|
|
2614
2734
|
};
|
|
2615
2735
|
return str.replace(/[&<>"']/g, (char) => htmlEscapes[char]);
|
|
2616
2736
|
};
|
|
2617
|
-
var
|
|
2737
|
+
var extractMediaUrls = (blocks) => {
|
|
2618
2738
|
const urls = /* @__PURE__ */ new Set();
|
|
2619
2739
|
const traverse = (blockList) => {
|
|
2620
2740
|
for (const block of blockList) {
|
|
2621
2741
|
if (block.type === "image" && block.props?.url) {
|
|
2622
2742
|
const url = block.props.url;
|
|
2623
|
-
if (typeof url === "string" && url.trim())
|
|
2624
|
-
|
|
2625
|
-
|
|
2743
|
+
if (typeof url === "string" && url.trim()) urls.add(url);
|
|
2744
|
+
}
|
|
2745
|
+
if (block.type === "video" && block.props?.url) {
|
|
2746
|
+
const url = block.props.url;
|
|
2747
|
+
if (typeof url === "string" && url.trim()) urls.add(url);
|
|
2626
2748
|
}
|
|
2627
2749
|
if (block.children && Array.isArray(block.children)) {
|
|
2628
2750
|
traverse(block.children);
|
|
@@ -2632,12 +2754,10 @@ var extractImageUrls = (blocks) => {
|
|
|
2632
2754
|
traverse(blocks);
|
|
2633
2755
|
return urls;
|
|
2634
2756
|
};
|
|
2635
|
-
var
|
|
2757
|
+
var findDeletedMediaUrls = (previousUrls, currentUrls) => {
|
|
2636
2758
|
const deleted = [];
|
|
2637
2759
|
previousUrls.forEach((url) => {
|
|
2638
|
-
if (!currentUrls.has(url))
|
|
2639
|
-
deleted.push(url);
|
|
2640
|
-
}
|
|
2760
|
+
if (!currentUrls.has(url)) deleted.push(url);
|
|
2641
2761
|
});
|
|
2642
2762
|
return deleted;
|
|
2643
2763
|
};
|
|
@@ -2747,7 +2867,11 @@ function LumirEditor({
|
|
|
2747
2867
|
onImageDelete
|
|
2748
2868
|
}) {
|
|
2749
2869
|
const [isUploading, setIsUploading] = (0, import_react16.useState)(false);
|
|
2870
|
+
const [uploadProgress, setUploadProgress] = (0, import_react16.useState)(null);
|
|
2750
2871
|
const [errorMessage, setErrorMessage] = (0, import_react16.useState)(null);
|
|
2872
|
+
const floatingMenuFileInputRef = (0, import_react16.useRef)(null);
|
|
2873
|
+
const floatingMenuBlockRef = (0, import_react16.useRef)(null);
|
|
2874
|
+
const floatingMenuUploadStartTimeRef = (0, import_react16.useRef)(0);
|
|
2751
2875
|
const handleError = (0, import_react16.useCallback)(
|
|
2752
2876
|
(error) => {
|
|
2753
2877
|
onError?.(error);
|
|
@@ -2778,6 +2902,13 @@ function LumirEditor({
|
|
|
2778
2902
|
allowFileUpload
|
|
2779
2903
|
);
|
|
2780
2904
|
}, [disableExtensions, allowVideoUpload, allowAudioUpload, allowFileUpload]);
|
|
2905
|
+
(0, import_react16.useEffect)(() => {
|
|
2906
|
+
DEBUG_LOG("LumirEditor:init:disabledExtensions", "snapshot", {
|
|
2907
|
+
allowVideoUpload,
|
|
2908
|
+
hasVideoInDisabled: disabledExtensions.includes("video"),
|
|
2909
|
+
disabledList: disabledExtensions.slice(0, 15)
|
|
2910
|
+
});
|
|
2911
|
+
}, [allowVideoUpload, disabledExtensions]);
|
|
2781
2912
|
const fileNameTransformRef = (0, import_react16.useRef)(s3Upload?.fileNameTransform);
|
|
2782
2913
|
(0, import_react16.useEffect)(() => {
|
|
2783
2914
|
fileNameTransformRef.current = s3Upload?.fileNameTransform;
|
|
@@ -2790,6 +2921,12 @@ function LumirEditor({
|
|
|
2790
2921
|
path: s3Upload.path,
|
|
2791
2922
|
appendUUID: s3Upload.appendUUID,
|
|
2792
2923
|
preserveExtension: s3Upload.preserveExtension,
|
|
2924
|
+
uploadTimeoutMs: s3Upload.uploadTimeoutMs,
|
|
2925
|
+
maxRetries: s3Upload.maxRetries,
|
|
2926
|
+
onProgress: (percent) => {
|
|
2927
|
+
setUploadProgress(percent);
|
|
2928
|
+
s3Upload.onProgress?.(percent);
|
|
2929
|
+
},
|
|
2793
2930
|
// 최신 콜백을 항상 사용하도록 ref를 통해 접근
|
|
2794
2931
|
fileNameTransform: (originalName, file) => {
|
|
2795
2932
|
return fileNameTransformRef.current ? fileNameTransformRef.current(originalName, file) : originalName;
|
|
@@ -2800,7 +2937,10 @@ function LumirEditor({
|
|
|
2800
2937
|
s3Upload?.env,
|
|
2801
2938
|
s3Upload?.path,
|
|
2802
2939
|
s3Upload?.appendUUID,
|
|
2803
|
-
s3Upload?.preserveExtension
|
|
2940
|
+
s3Upload?.preserveExtension,
|
|
2941
|
+
s3Upload?.uploadTimeoutMs,
|
|
2942
|
+
s3Upload?.maxRetries,
|
|
2943
|
+
s3Upload?.onProgress
|
|
2804
2944
|
]);
|
|
2805
2945
|
const editor = (0, import_react17.useCreateBlockNote)(
|
|
2806
2946
|
{
|
|
@@ -2818,18 +2958,44 @@ function LumirEditor({
|
|
|
2818
2958
|
tabBehavior,
|
|
2819
2959
|
trailingBlock,
|
|
2820
2960
|
uploadFile: async (file) => {
|
|
2821
|
-
|
|
2822
|
-
|
|
2961
|
+
const allowedImage = isImageFile(file);
|
|
2962
|
+
const allowedVideo = allowVideoUpload && isVideoFile(file);
|
|
2963
|
+
DEBUG_LOG("uploadFile:step1:entry", "editor uploadFile callback invoked", {
|
|
2964
|
+
fileName: file.name,
|
|
2965
|
+
fileType: file.type,
|
|
2966
|
+
fileSize: file.size,
|
|
2967
|
+
allowVideoUpload,
|
|
2968
|
+
allowedImage,
|
|
2969
|
+
allowedVideo
|
|
2970
|
+
});
|
|
2971
|
+
if (!allowedImage && !allowedVideo) {
|
|
2972
|
+
const error = LumirEditorError.invalidFileType(
|
|
2973
|
+
file.name,
|
|
2974
|
+
allowVideoUpload
|
|
2975
|
+
);
|
|
2823
2976
|
handleError(error);
|
|
2824
2977
|
throw error;
|
|
2825
2978
|
}
|
|
2826
2979
|
try {
|
|
2827
|
-
|
|
2980
|
+
setUploadProgress(0);
|
|
2981
|
+
let fileUrl;
|
|
2982
|
+
const branch = uploadFile ? "custom" : memoizedS3Upload?.apiEndpoint ? "s3" : "none";
|
|
2983
|
+
DEBUG_LOG("uploadFile:step2:branch", "upload path", {
|
|
2984
|
+
branch,
|
|
2985
|
+
hasCustomUploadFile: !!uploadFile,
|
|
2986
|
+
hasS3ApiEndpoint: !!memoizedS3Upload?.apiEndpoint
|
|
2987
|
+
});
|
|
2828
2988
|
if (uploadFile) {
|
|
2829
|
-
|
|
2989
|
+
const t0 = Date.now();
|
|
2990
|
+
DEBUG_LOG("uploadFile:step3a:custom", "calling custom uploadFile", { fileName: file.name });
|
|
2991
|
+
fileUrl = await uploadFile(file);
|
|
2992
|
+
DEBUG_LOG("uploadFile:step3a:done", "custom uploadFile returned", { urlLen: fileUrl?.length, elapsedMs: Date.now() - t0 });
|
|
2830
2993
|
} else if (memoizedS3Upload?.apiEndpoint) {
|
|
2994
|
+
const t0 = Date.now();
|
|
2995
|
+
DEBUG_LOG("uploadFile:step3b:s3", "calling S3 uploader", { fileName: file.name });
|
|
2831
2996
|
const s3Uploader = createS3Uploader(memoizedS3Upload);
|
|
2832
|
-
|
|
2997
|
+
fileUrl = await s3Uploader(file);
|
|
2998
|
+
DEBUG_LOG("uploadFile:step3b:done", "S3 uploader returned", { urlLen: fileUrl?.length, elapsedMs: Date.now() - t0 });
|
|
2833
2999
|
} else {
|
|
2834
3000
|
const error = LumirEditorError.s3ConfigError(
|
|
2835
3001
|
"No upload method available. Please provide uploadFile or s3Upload configuration."
|
|
@@ -2837,8 +3003,16 @@ function LumirEditor({
|
|
|
2837
3003
|
handleError(error);
|
|
2838
3004
|
throw error;
|
|
2839
3005
|
}
|
|
2840
|
-
|
|
3006
|
+
DEBUG_LOG("uploadFile:step4:success", "returning URL", {
|
|
3007
|
+
fileName: file.name,
|
|
3008
|
+
urlPrefix: fileUrl.slice(0, 80)
|
|
3009
|
+
});
|
|
3010
|
+
return fileUrl;
|
|
2841
3011
|
} catch (error) {
|
|
3012
|
+
DEBUG_LOG("uploadFile:step5:catch", "uploadFile threw", {
|
|
3013
|
+
fileName: file.name,
|
|
3014
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
3015
|
+
});
|
|
2842
3016
|
if (error instanceof LumirEditorError) {
|
|
2843
3017
|
throw error;
|
|
2844
3018
|
}
|
|
@@ -2848,6 +3022,8 @@ function LumirEditor({
|
|
|
2848
3022
|
);
|
|
2849
3023
|
handleError(lumirError);
|
|
2850
3024
|
throw lumirError;
|
|
3025
|
+
} finally {
|
|
3026
|
+
setUploadProgress(null);
|
|
2851
3027
|
}
|
|
2852
3028
|
},
|
|
2853
3029
|
pasteHandler: (ctx) => {
|
|
@@ -2876,7 +3052,15 @@ function LumirEditor({
|
|
|
2876
3052
|
}
|
|
2877
3053
|
const fileList = event?.clipboardData?.files ?? null;
|
|
2878
3054
|
const files = fileList ? Array.from(fileList) : [];
|
|
2879
|
-
const acceptedFiles = files.filter(
|
|
3055
|
+
const acceptedFiles = files.filter(
|
|
3056
|
+
(f) => isImageFile(f) || allowVideoUpload && isVideoFile(f)
|
|
3057
|
+
);
|
|
3058
|
+
DEBUG_LOG("paste:step1:files", "paste clipboard files", {
|
|
3059
|
+
filesCount: files.length,
|
|
3060
|
+
acceptedCount: acceptedFiles.length,
|
|
3061
|
+
fileNames: files.map((f) => f.name),
|
|
3062
|
+
acceptedNames: acceptedFiles.map((f) => f.name)
|
|
3063
|
+
});
|
|
2880
3064
|
if (files.length > 0 && acceptedFiles.length === 0) {
|
|
2881
3065
|
event.preventDefault();
|
|
2882
3066
|
return true;
|
|
@@ -2890,13 +3074,26 @@ function LumirEditor({
|
|
|
2890
3074
|
try {
|
|
2891
3075
|
for (const file of acceptedFiles) {
|
|
2892
3076
|
try {
|
|
3077
|
+
DEBUG_LOG("paste:step2:upload", "calling uploadFile for paste", {
|
|
3078
|
+
fileName: file.name,
|
|
3079
|
+
fileType: file.type
|
|
3080
|
+
});
|
|
2893
3081
|
const url = await editor2.uploadFile(file);
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
3082
|
+
if (isImageFile(file)) {
|
|
3083
|
+
editor2.pasteHTML(
|
|
3084
|
+
`<img src="${escapeHtml(url)}" alt="image" />`
|
|
3085
|
+
);
|
|
3086
|
+
} else if (isVideoFile(file)) {
|
|
3087
|
+
const currentBlock = editor2.getTextCursorPosition().block;
|
|
3088
|
+
editor2.insertBlocks(
|
|
3089
|
+
[{ type: "video", props: { url } }],
|
|
3090
|
+
currentBlock,
|
|
3091
|
+
"after"
|
|
3092
|
+
);
|
|
3093
|
+
}
|
|
2897
3094
|
} catch (err) {
|
|
2898
3095
|
console.warn(
|
|
2899
|
-
"
|
|
3096
|
+
"Upload failed, skipped:",
|
|
2900
3097
|
file.name || "",
|
|
2901
3098
|
err
|
|
2902
3099
|
);
|
|
@@ -2919,6 +3116,7 @@ function LumirEditor({
|
|
|
2919
3116
|
trailingBlock,
|
|
2920
3117
|
uploadFile,
|
|
2921
3118
|
memoizedS3Upload,
|
|
3119
|
+
allowVideoUpload,
|
|
2922
3120
|
linkPreview?.apiEndpoint,
|
|
2923
3121
|
placeholder
|
|
2924
3122
|
]
|
|
@@ -2939,25 +3137,25 @@ function LumirEditor({
|
|
|
2939
3137
|
};
|
|
2940
3138
|
return editor.onEditorContentChange(handleContentChange);
|
|
2941
3139
|
}, [editor, onContentChange]);
|
|
2942
|
-
const
|
|
3140
|
+
const previousMediaUrlsRef = (0, import_react16.useRef)(/* @__PURE__ */ new Set());
|
|
2943
3141
|
(0, import_react16.useEffect)(() => {
|
|
2944
3142
|
if (!editor) return;
|
|
2945
3143
|
const initialBlocks = editor.topLevelBlocks;
|
|
2946
|
-
|
|
3144
|
+
previousMediaUrlsRef.current = extractMediaUrls(initialBlocks);
|
|
2947
3145
|
}, [editor]);
|
|
2948
3146
|
(0, import_react16.useEffect)(() => {
|
|
2949
3147
|
if (!editor || !onImageDelete) return;
|
|
2950
|
-
const
|
|
3148
|
+
const handleMediaDeleteCheck = () => {
|
|
2951
3149
|
const currentBlocks = editor.topLevelBlocks;
|
|
2952
|
-
const currentUrls =
|
|
2953
|
-
const previousUrls =
|
|
2954
|
-
const deletedUrls =
|
|
3150
|
+
const currentUrls = extractMediaUrls(currentBlocks);
|
|
3151
|
+
const previousUrls = previousMediaUrlsRef.current;
|
|
3152
|
+
const deletedUrls = findDeletedMediaUrls(previousUrls, currentUrls);
|
|
2955
3153
|
deletedUrls.forEach((url) => {
|
|
2956
3154
|
onImageDelete(url);
|
|
2957
3155
|
});
|
|
2958
|
-
|
|
3156
|
+
previousMediaUrlsRef.current = currentUrls;
|
|
2959
3157
|
};
|
|
2960
|
-
return editor.onEditorContentChange(
|
|
3158
|
+
return editor.onEditorContentChange(handleMediaDeleteCheck);
|
|
2961
3159
|
}, [editor, onImageDelete]);
|
|
2962
3160
|
(0, import_react16.useEffect)(() => {
|
|
2963
3161
|
const el = editor?.domElement;
|
|
@@ -2979,11 +3177,31 @@ function LumirEditor({
|
|
|
2979
3177
|
const items = Array.from(e.dataTransfer.items ?? []);
|
|
2980
3178
|
const files = items.filter((it) => it.kind === "file").map((it) => it.getAsFile()).filter((f) => !!f);
|
|
2981
3179
|
const imageFiles = files.filter(isImageFile);
|
|
3180
|
+
const videoFiles = allowVideoUpload ? files.filter(isVideoFile) : [];
|
|
2982
3181
|
const htmlFiles = files.filter(isHtmlFile);
|
|
2983
|
-
|
|
3182
|
+
DEBUG_LOG("drop:step1:files", "drop received", {
|
|
3183
|
+
filesCount: files.length,
|
|
3184
|
+
imageCount: imageFiles.length,
|
|
3185
|
+
videoCount: videoFiles.length,
|
|
3186
|
+
htmlCount: htmlFiles.length,
|
|
3187
|
+
allowVideoUpload,
|
|
3188
|
+
firstFile: files[0] ? {
|
|
3189
|
+
name: files[0].name,
|
|
3190
|
+
type: files[0].type,
|
|
3191
|
+
size: files[0].size,
|
|
3192
|
+
isImage: isImageFile(files[0]),
|
|
3193
|
+
isVideo: isVideoFile(files[0])
|
|
3194
|
+
} : null
|
|
3195
|
+
});
|
|
3196
|
+
if (imageFiles.length === 0 && htmlFiles.length === 0 && videoFiles.length === 0)
|
|
3197
|
+
return;
|
|
2984
3198
|
(async () => {
|
|
2985
3199
|
setIsUploading(true);
|
|
2986
3200
|
try {
|
|
3201
|
+
DEBUG_LOG("drop:step2:async", "drop async started", {
|
|
3202
|
+
imageCount: imageFiles.length,
|
|
3203
|
+
videoCount: videoFiles.length
|
|
3204
|
+
});
|
|
2987
3205
|
for (const file of imageFiles) {
|
|
2988
3206
|
try {
|
|
2989
3207
|
if (editor?.uploadFile) {
|
|
@@ -3002,6 +3220,34 @@ function LumirEditor({
|
|
|
3002
3220
|
);
|
|
3003
3221
|
}
|
|
3004
3222
|
}
|
|
3223
|
+
DEBUG_LOG("drop:step3:videoLoop", "video loop start", {
|
|
3224
|
+
videoCount: videoFiles.length,
|
|
3225
|
+
names: videoFiles.map((f) => f.name)
|
|
3226
|
+
});
|
|
3227
|
+
for (const file of videoFiles) {
|
|
3228
|
+
try {
|
|
3229
|
+
if (editor?.uploadFile) {
|
|
3230
|
+
DEBUG_LOG("drop:step4:videoUpload", "calling uploadFile for video", {
|
|
3231
|
+
fileName: file.name
|
|
3232
|
+
});
|
|
3233
|
+
const url = await editor.uploadFile(file);
|
|
3234
|
+
if (url && typeof url === "string") {
|
|
3235
|
+
const currentBlock = editor.getTextCursorPosition().block;
|
|
3236
|
+
editor.insertBlocks(
|
|
3237
|
+
[{ type: "video", props: { url } }],
|
|
3238
|
+
currentBlock,
|
|
3239
|
+
"after"
|
|
3240
|
+
);
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
} catch (err) {
|
|
3244
|
+
console.warn(
|
|
3245
|
+
"Video upload failed, skipped:",
|
|
3246
|
+
file.name || "",
|
|
3247
|
+
err
|
|
3248
|
+
);
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3005
3251
|
for (const file of htmlFiles) {
|
|
3006
3252
|
try {
|
|
3007
3253
|
const htmlContent = await file.text();
|
|
@@ -3041,7 +3287,7 @@ function LumirEditor({
|
|
|
3041
3287
|
});
|
|
3042
3288
|
el.removeEventListener("drop", handleDrop, { capture: true });
|
|
3043
3289
|
};
|
|
3044
|
-
}, [editor]);
|
|
3290
|
+
}, [editor, allowVideoUpload]);
|
|
3045
3291
|
const computedSideMenu = (0, import_react16.useMemo)(() => {
|
|
3046
3292
|
return sideMenuAddButton ? sideMenu : false;
|
|
3047
3293
|
}, [sideMenuAddButton, sideMenu]);
|
|
@@ -3054,42 +3300,110 @@ function LumirEditor({
|
|
|
3054
3300
|
className: cn("lumirEditor", className),
|
|
3055
3301
|
style: { position: "relative", display: "flex", flexDirection: "column" },
|
|
3056
3302
|
children: [
|
|
3057
|
-
floatingMenu && editor && /* @__PURE__ */ (0, import_jsx_runtime16.
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3303
|
+
floatingMenu && editor && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
|
|
3304
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3305
|
+
"input",
|
|
3306
|
+
{
|
|
3307
|
+
ref: floatingMenuFileInputRef,
|
|
3308
|
+
type: "file",
|
|
3309
|
+
accept: allowVideoUpload ? "image/*,video/mp4,video/webm,video/ogg,video/quicktime,.mov" : "image/*",
|
|
3310
|
+
style: {
|
|
3311
|
+
position: "absolute",
|
|
3312
|
+
left: "-9999px",
|
|
3313
|
+
opacity: 0,
|
|
3314
|
+
pointerEvents: "none"
|
|
3315
|
+
},
|
|
3316
|
+
onChange: async (e) => {
|
|
3317
|
+
const inputEl = e.target;
|
|
3318
|
+
const file = inputEl.files?.[0];
|
|
3319
|
+
DEBUG_LOG("FloatingMenu:step3:onchange", "file input onchange fired", {
|
|
3320
|
+
hasFile: !!file,
|
|
3321
|
+
fileName: file?.name,
|
|
3322
|
+
fileType: file?.type,
|
|
3323
|
+
fileSize: file?.size,
|
|
3324
|
+
hasUploadFile: !!editor?.uploadFile
|
|
3325
|
+
});
|
|
3326
|
+
const blockToInsertAfter = floatingMenuBlockRef.current;
|
|
3327
|
+
if (file && editor.uploadFile && blockToInsertAfter) {
|
|
3328
|
+
const allowedImage = isImageFile(file);
|
|
3329
|
+
const allowedVideo = allowVideoUpload && isVideoFile(file);
|
|
3330
|
+
DEBUG_LOG("FloatingMenu:step4:fileCheck", "allowed check", {
|
|
3331
|
+
fileName: file.name,
|
|
3332
|
+
allowedImage,
|
|
3333
|
+
allowedVideo
|
|
3334
|
+
});
|
|
3335
|
+
if (allowedImage || allowedVideo) {
|
|
3336
|
+
try {
|
|
3337
|
+
setIsUploading(true);
|
|
3338
|
+
floatingMenuUploadStartTimeRef.current = Date.now();
|
|
3339
|
+
DEBUG_LOG("FloatingMenu:step5:uploadStart", "calling editor.uploadFile", {
|
|
3340
|
+
fileName: file.name
|
|
3341
|
+
});
|
|
3342
|
+
const url = await editor.uploadFile(file);
|
|
3343
|
+
const blockType = allowedVideo ? "video" : "image";
|
|
3344
|
+
const elapsedMs = Date.now() - floatingMenuUploadStartTimeRef.current;
|
|
3345
|
+
DEBUG_LOG("FloatingMenu:step6:uploadDone", "upload returned, inserting block", {
|
|
3346
|
+
blockType,
|
|
3347
|
+
blockId: blockToInsertAfter.id,
|
|
3348
|
+
urlLen: url?.length,
|
|
3349
|
+
elapsedMs
|
|
3350
|
+
});
|
|
3351
|
+
editor.insertBlocks(
|
|
3352
|
+
[
|
|
3353
|
+
{
|
|
3354
|
+
type: blockType,
|
|
3355
|
+
props: { url }
|
|
3356
|
+
}
|
|
3357
|
+
],
|
|
3358
|
+
blockToInsertAfter,
|
|
3359
|
+
"after"
|
|
3360
|
+
);
|
|
3361
|
+
} catch (err) {
|
|
3362
|
+
DEBUG_LOG("FloatingMenu:step7:catch", "upload or insert failed", {
|
|
3363
|
+
errMsg: err instanceof Error ? err.message : String(err)
|
|
3364
|
+
});
|
|
3365
|
+
console.error("Upload failed:", err);
|
|
3366
|
+
} finally {
|
|
3367
|
+
setIsUploading(false);
|
|
3368
|
+
}
|
|
3086
3369
|
}
|
|
3087
3370
|
}
|
|
3088
|
-
|
|
3089
|
-
|
|
3371
|
+
inputEl.value = "";
|
|
3372
|
+
}
|
|
3090
3373
|
}
|
|
3091
|
-
|
|
3092
|
-
|
|
3374
|
+
),
|
|
3375
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
|
|
3376
|
+
FloatingMenu,
|
|
3377
|
+
{
|
|
3378
|
+
editor,
|
|
3379
|
+
position: floatingMenuPosition,
|
|
3380
|
+
onImageUpload: () => {
|
|
3381
|
+
DEBUG_LOG("FloatingMenu:step1:click", "upload button clicked", {
|
|
3382
|
+
allowVideoUpload
|
|
3383
|
+
});
|
|
3384
|
+
let blockToInsertAfter;
|
|
3385
|
+
try {
|
|
3386
|
+
blockToInsertAfter = editor.getTextCursorPosition().block;
|
|
3387
|
+
} catch (err) {
|
|
3388
|
+
DEBUG_LOG("FloatingMenu:step1b:error", "getTextCursorPosition failed", {
|
|
3389
|
+
err: err instanceof Error ? err.message : String(err)
|
|
3390
|
+
});
|
|
3391
|
+
return;
|
|
3392
|
+
}
|
|
3393
|
+
floatingMenuBlockRef.current = blockToInsertAfter;
|
|
3394
|
+
const input = floatingMenuFileInputRef.current;
|
|
3395
|
+
if (!input) return;
|
|
3396
|
+
input.accept = allowVideoUpload ? "image/*,video/mp4,video/webm,video/ogg,video/quicktime,.mov" : "image/*";
|
|
3397
|
+
input.value = "";
|
|
3398
|
+
DEBUG_LOG("FloatingMenu:step2:inputReady", "persistent input ref, about to click", {
|
|
3399
|
+
accept: input.accept
|
|
3400
|
+
});
|
|
3401
|
+
DEBUG_LOG("FloatingMenu:step2b:click", "input.click() about to be called", {});
|
|
3402
|
+
input.click();
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
)
|
|
3406
|
+
] }),
|
|
3093
3407
|
/* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
|
|
3094
3408
|
import_mantine.BlockNoteView,
|
|
3095
3409
|
{
|
|
@@ -3116,8 +3430,10 @@ function LumirEditor({
|
|
|
3116
3430
|
const filtered = items.filter((item) => {
|
|
3117
3431
|
const key = (item?.key || "").toString().toLowerCase();
|
|
3118
3432
|
const title = (item?.title || "").toString().toLowerCase();
|
|
3119
|
-
if (
|
|
3120
|
-
|
|
3433
|
+
if (key === "video" || title.includes("video"))
|
|
3434
|
+
return allowVideoUpload;
|
|
3435
|
+
if (["audio", "file"].includes(key)) return false;
|
|
3436
|
+
if (title.includes("audio") || title.includes("file"))
|
|
3121
3437
|
return false;
|
|
3122
3438
|
return true;
|
|
3123
3439
|
});
|
|
@@ -3218,7 +3534,7 @@ function LumirEditor({
|
|
|
3218
3534
|
)
|
|
3219
3535
|
);
|
|
3220
3536
|
},
|
|
3221
|
-
[editor, linkPreview?.apiEndpoint]
|
|
3537
|
+
[editor, allowVideoUpload, linkPreview?.apiEndpoint]
|
|
3222
3538
|
)
|
|
3223
3539
|
}
|
|
3224
3540
|
),
|
|
@@ -3226,7 +3542,13 @@ function LumirEditor({
|
|
|
3226
3542
|
]
|
|
3227
3543
|
}
|
|
3228
3544
|
),
|
|
3229
|
-
isUploading && /* @__PURE__ */ (0, import_jsx_runtime16.
|
|
3545
|
+
isUploading && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "lumirEditor-upload-overlay", children: [
|
|
3546
|
+
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "lumirEditor-spinner" }),
|
|
3547
|
+
uploadProgress !== null && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "lumirEditor-upload-progress", children: [
|
|
3548
|
+
uploadProgress,
|
|
3549
|
+
"%"
|
|
3550
|
+
] })
|
|
3551
|
+
] }),
|
|
3230
3552
|
errorMessage && /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "lumirEditor-error-toast", children: [
|
|
3231
3553
|
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "lumirEditor-error-icon", children: "\u26A0\uFE0F" }),
|
|
3232
3554
|
/* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "lumirEditor-error-message", children: errorMessage }),
|