@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.mjs
CHANGED
|
@@ -65,7 +65,10 @@ var createS3Uploader = (config) => {
|
|
|
65
65
|
path,
|
|
66
66
|
fileNameTransform,
|
|
67
67
|
appendUUID,
|
|
68
|
-
preserveExtension = true
|
|
68
|
+
preserveExtension = true,
|
|
69
|
+
onProgress,
|
|
70
|
+
uploadTimeoutMs = 12e4,
|
|
71
|
+
maxRetries = 2
|
|
69
72
|
} = config;
|
|
70
73
|
if (!apiEndpoint || apiEndpoint.trim() === "") {
|
|
71
74
|
throw new Error(
|
|
@@ -104,6 +107,15 @@ var createS3Uploader = (config) => {
|
|
|
104
107
|
}
|
|
105
108
|
return `${env}/${path}/${filename}`;
|
|
106
109
|
};
|
|
110
|
+
const debugLog = (loc, msg, data) => {
|
|
111
|
+
const p = fetch("http://127.0.0.1:7686/ingest/1f8ee1c5-0cf0-4ae7-91ed-5ea7ed17130a", {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "b73262" },
|
|
114
|
+
body: JSON.stringify({ sessionId: "b73262", location: loc, message: msg, data, timestamp: Date.now() })
|
|
115
|
+
});
|
|
116
|
+
if (p && typeof p.catch === "function") p.catch(() => {
|
|
117
|
+
});
|
|
118
|
+
};
|
|
107
119
|
return async (file) => {
|
|
108
120
|
try {
|
|
109
121
|
if (!apiEndpoint || apiEndpoint.trim() === "") {
|
|
@@ -112,11 +124,26 @@ var createS3Uploader = (config) => {
|
|
|
112
124
|
);
|
|
113
125
|
}
|
|
114
126
|
const fileName = generateHierarchicalFileName(file);
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
);
|
|
127
|
+
const contentType = file.type || "application/octet-stream";
|
|
128
|
+
const presignedUrlFull = `${apiEndpoint}?key=${encodeURIComponent(fileName)}&contentType=${encodeURIComponent(contentType)}`;
|
|
129
|
+
const tPresigned = Date.now();
|
|
130
|
+
debugLog("s3:step1:presignedReq", "fetching presigned URL", {
|
|
131
|
+
fileName,
|
|
132
|
+
contentType,
|
|
133
|
+
apiEndpoint
|
|
134
|
+
});
|
|
135
|
+
const response = await fetch(presignedUrlFull);
|
|
136
|
+
debugLog("s3:step2:presignedRes", "presigned response", {
|
|
137
|
+
ok: response.ok,
|
|
138
|
+
status: response.status,
|
|
139
|
+
elapsedMs: Date.now() - tPresigned
|
|
140
|
+
});
|
|
118
141
|
if (!response.ok) {
|
|
119
142
|
const errorText = await response.text() || "";
|
|
143
|
+
debugLog("s3:step2b:presignedErr", "presigned failed", {
|
|
144
|
+
status: response.status,
|
|
145
|
+
errorText: errorText.slice(0, 200)
|
|
146
|
+
});
|
|
120
147
|
throw new Error(
|
|
121
148
|
`Failed to get presigned URL: ${response.statusText}, ${errorText}`
|
|
122
149
|
);
|
|
@@ -125,17 +152,64 @@ var createS3Uploader = (config) => {
|
|
|
125
152
|
const { presignedUrl, publicUrl } = responseData;
|
|
126
153
|
const validatedPresignedUrl = validateS3Url(presignedUrl, "presignedUrl");
|
|
127
154
|
const validatedPublicUrl = validateS3Url(publicUrl, "publicUrl");
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
155
|
+
const tPut = Date.now();
|
|
156
|
+
debugLog("s3:step3:putReq", "S3 PUT request", { publicUrlLen: validatedPublicUrl?.length });
|
|
157
|
+
let lastError;
|
|
158
|
+
const attempts = maxRetries + 1;
|
|
159
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
160
|
+
try {
|
|
161
|
+
if (onProgress && typeof XMLHttpRequest !== "undefined") {
|
|
162
|
+
await new Promise((resolve, reject) => {
|
|
163
|
+
const xhr = new XMLHttpRequest();
|
|
164
|
+
xhr.timeout = uploadTimeoutMs;
|
|
165
|
+
xhr.upload.onprogress = (e) => {
|
|
166
|
+
if (e.lengthComputable) {
|
|
167
|
+
onProgress(Math.min(100, Math.round(e.loaded / e.total * 100)));
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
xhr.onload = () => {
|
|
171
|
+
if (xhr.status >= 200 && xhr.status < 300) resolve();
|
|
172
|
+
else reject(new Error(`Failed to upload file: ${xhr.statusText}`));
|
|
173
|
+
};
|
|
174
|
+
xhr.onerror = () => reject(new Error("Upload failed"));
|
|
175
|
+
xhr.ontimeout = () => reject(new Error("Upload timeout"));
|
|
176
|
+
xhr.open("PUT", validatedPresignedUrl);
|
|
177
|
+
xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
|
|
178
|
+
xhr.send(file);
|
|
179
|
+
});
|
|
180
|
+
} else {
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
const timeoutId = setTimeout(() => controller.abort(), uploadTimeoutMs);
|
|
183
|
+
const uploadResponse = await fetch(validatedPresignedUrl, {
|
|
184
|
+
method: "PUT",
|
|
185
|
+
headers: {
|
|
186
|
+
"Content-Type": file.type || "application/octet-stream"
|
|
187
|
+
},
|
|
188
|
+
body: file,
|
|
189
|
+
signal: controller.signal
|
|
190
|
+
});
|
|
191
|
+
clearTimeout(timeoutId);
|
|
192
|
+
debugLog("s3:step4:putRes", "S3 PUT response", {
|
|
193
|
+
ok: uploadResponse.ok,
|
|
194
|
+
status: uploadResponse.status,
|
|
195
|
+
putElapsedMs: Date.now() - tPut
|
|
196
|
+
});
|
|
197
|
+
if (!uploadResponse.ok) {
|
|
198
|
+
throw new Error(`Failed to upload file: ${uploadResponse.statusText}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
debugLog("s3:step5:return", "returning publicUrl", { urlPrefix: validatedPublicUrl.slice(0, 80) });
|
|
202
|
+
return validatedPublicUrl;
|
|
203
|
+
} catch (err) {
|
|
204
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
205
|
+
if (attempt < attempts - 1) {
|
|
206
|
+
debugLog("s3:putRetry", "PUT failed, retrying", { attempt: attempt + 1, attempts });
|
|
207
|
+
} else {
|
|
208
|
+
throw lastError;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
137
211
|
}
|
|
138
|
-
|
|
212
|
+
throw lastError ?? new Error("Upload failed");
|
|
139
213
|
} catch (error) {
|
|
140
214
|
console.error("S3 upload failed:", error);
|
|
141
215
|
throw error;
|
|
@@ -2417,15 +2491,14 @@ var LumirEditorError = class _LumirEditorError extends Error {
|
|
|
2417
2491
|
}
|
|
2418
2492
|
/**
|
|
2419
2493
|
* 잘못된 파일 형식 에러 생성
|
|
2494
|
+
* @param allowVideoUpload true이면 "image and video" 메시지 사용
|
|
2420
2495
|
*/
|
|
2421
|
-
static invalidFileType(fileName) {
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
}
|
|
2428
|
-
);
|
|
2496
|
+
static invalidFileType(fileName, allowVideoUpload) {
|
|
2497
|
+
const message = allowVideoUpload === true ? `Invalid file type: ${fileName}. Only image and video files are allowed.` : `Invalid file type: ${fileName}. Only image files are allowed.`;
|
|
2498
|
+
return new _LumirEditorError(message, {
|
|
2499
|
+
code: "INVALID_FILE_TYPE",
|
|
2500
|
+
context: { fileName }
|
|
2501
|
+
});
|
|
2429
2502
|
}
|
|
2430
2503
|
/**
|
|
2431
2504
|
* S3 설정 에러 생성
|
|
@@ -2448,10 +2521,38 @@ var LumirEditorError = class _LumirEditorError extends Error {
|
|
|
2448
2521
|
|
|
2449
2522
|
// src/constants/limits.ts
|
|
2450
2523
|
var MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
2524
|
+
var MAX_VIDEO_FILE_SIZE = 100 * 1024 * 1024;
|
|
2451
2525
|
var BLOCKED_EXTENSIONS = [".svg", ".svgz"];
|
|
2526
|
+
var ALLOWED_VIDEO_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
2527
|
+
"video/mp4",
|
|
2528
|
+
"video/webm",
|
|
2529
|
+
"video/ogg",
|
|
2530
|
+
"video/quicktime"
|
|
2531
|
+
// .mov
|
|
2532
|
+
]);
|
|
2533
|
+
var ALLOWED_VIDEO_EXTENSIONS = [
|
|
2534
|
+
".mp4",
|
|
2535
|
+
".webm",
|
|
2536
|
+
".ogg",
|
|
2537
|
+
".mov"
|
|
2538
|
+
];
|
|
2452
2539
|
|
|
2453
2540
|
// src/components/LumirEditor.tsx
|
|
2454
|
-
import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2541
|
+
import { Fragment as Fragment4, jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2542
|
+
var DEBUG_LOG = (loc, msg, data) => {
|
|
2543
|
+
fetch("http://127.0.0.1:7686/ingest/1f8ee1c5-0cf0-4ae7-91ed-5ea7ed17130a", {
|
|
2544
|
+
method: "POST",
|
|
2545
|
+
headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "b73262" },
|
|
2546
|
+
body: JSON.stringify({
|
|
2547
|
+
sessionId: "b73262",
|
|
2548
|
+
location: loc,
|
|
2549
|
+
message: msg,
|
|
2550
|
+
data,
|
|
2551
|
+
timestamp: Date.now()
|
|
2552
|
+
})
|
|
2553
|
+
}).catch(() => {
|
|
2554
|
+
});
|
|
2555
|
+
};
|
|
2455
2556
|
var ContentUtils = class {
|
|
2456
2557
|
/**
|
|
2457
2558
|
* JSON 문자열의 유효성을 검증합니다
|
|
@@ -2580,6 +2681,25 @@ var isImageFile = (file) => {
|
|
|
2580
2681
|
}
|
|
2581
2682
|
return file.type?.startsWith("image/") || !file.type && /\.(png|jpe?g|gif|webp|bmp)$/i.test(fileName);
|
|
2582
2683
|
};
|
|
2684
|
+
var isVideoFile = (file) => {
|
|
2685
|
+
const sizeOk = file.size > 0 && file.size <= MAX_VIDEO_FILE_SIZE;
|
|
2686
|
+
const fileName = file.name?.toLowerCase() || "";
|
|
2687
|
+
const mimeMatch = ALLOWED_VIDEO_MIME_TYPES.has(file.type);
|
|
2688
|
+
const videoPrefix = typeof file.type === "string" && file.type.startsWith("video/");
|
|
2689
|
+
const extMatch = !file.type && ALLOWED_VIDEO_EXTENSIONS.some((ext) => fileName.endsWith(ext));
|
|
2690
|
+
const result = sizeOk && (mimeMatch || videoPrefix || extMatch);
|
|
2691
|
+
DEBUG_LOG("isVideoFile:check", "result", {
|
|
2692
|
+
fileName: file.name,
|
|
2693
|
+
fileType: file.type,
|
|
2694
|
+
fileSize: file.size,
|
|
2695
|
+
sizeOk,
|
|
2696
|
+
mimeMatch,
|
|
2697
|
+
videoPrefix,
|
|
2698
|
+
extMatch,
|
|
2699
|
+
result
|
|
2700
|
+
});
|
|
2701
|
+
return result;
|
|
2702
|
+
};
|
|
2583
2703
|
var isHtmlFile = (file) => {
|
|
2584
2704
|
return file.size > 0 && (file.type === "text/html" || file.name?.toLowerCase().endsWith(".html") || file.name?.toLowerCase().endsWith(".htm"));
|
|
2585
2705
|
};
|
|
@@ -2593,15 +2713,17 @@ var escapeHtml = (str) => {
|
|
|
2593
2713
|
};
|
|
2594
2714
|
return str.replace(/[&<>"']/g, (char) => htmlEscapes[char]);
|
|
2595
2715
|
};
|
|
2596
|
-
var
|
|
2716
|
+
var extractMediaUrls = (blocks) => {
|
|
2597
2717
|
const urls = /* @__PURE__ */ new Set();
|
|
2598
2718
|
const traverse = (blockList) => {
|
|
2599
2719
|
for (const block of blockList) {
|
|
2600
2720
|
if (block.type === "image" && block.props?.url) {
|
|
2601
2721
|
const url = block.props.url;
|
|
2602
|
-
if (typeof url === "string" && url.trim())
|
|
2603
|
-
|
|
2604
|
-
|
|
2722
|
+
if (typeof url === "string" && url.trim()) urls.add(url);
|
|
2723
|
+
}
|
|
2724
|
+
if (block.type === "video" && block.props?.url) {
|
|
2725
|
+
const url = block.props.url;
|
|
2726
|
+
if (typeof url === "string" && url.trim()) urls.add(url);
|
|
2605
2727
|
}
|
|
2606
2728
|
if (block.children && Array.isArray(block.children)) {
|
|
2607
2729
|
traverse(block.children);
|
|
@@ -2611,12 +2733,10 @@ var extractImageUrls = (blocks) => {
|
|
|
2611
2733
|
traverse(blocks);
|
|
2612
2734
|
return urls;
|
|
2613
2735
|
};
|
|
2614
|
-
var
|
|
2736
|
+
var findDeletedMediaUrls = (previousUrls, currentUrls) => {
|
|
2615
2737
|
const deleted = [];
|
|
2616
2738
|
previousUrls.forEach((url) => {
|
|
2617
|
-
if (!currentUrls.has(url))
|
|
2618
|
-
deleted.push(url);
|
|
2619
|
-
}
|
|
2739
|
+
if (!currentUrls.has(url)) deleted.push(url);
|
|
2620
2740
|
});
|
|
2621
2741
|
return deleted;
|
|
2622
2742
|
};
|
|
@@ -2726,7 +2846,11 @@ function LumirEditor({
|
|
|
2726
2846
|
onImageDelete
|
|
2727
2847
|
}) {
|
|
2728
2848
|
const [isUploading, setIsUploading] = useState7(false);
|
|
2849
|
+
const [uploadProgress, setUploadProgress] = useState7(null);
|
|
2729
2850
|
const [errorMessage, setErrorMessage] = useState7(null);
|
|
2851
|
+
const floatingMenuFileInputRef = useRef8(null);
|
|
2852
|
+
const floatingMenuBlockRef = useRef8(null);
|
|
2853
|
+
const floatingMenuUploadStartTimeRef = useRef8(0);
|
|
2730
2854
|
const handleError = useCallback13(
|
|
2731
2855
|
(error) => {
|
|
2732
2856
|
onError?.(error);
|
|
@@ -2757,6 +2881,13 @@ function LumirEditor({
|
|
|
2757
2881
|
allowFileUpload
|
|
2758
2882
|
);
|
|
2759
2883
|
}, [disableExtensions, allowVideoUpload, allowAudioUpload, allowFileUpload]);
|
|
2884
|
+
useEffect7(() => {
|
|
2885
|
+
DEBUG_LOG("LumirEditor:init:disabledExtensions", "snapshot", {
|
|
2886
|
+
allowVideoUpload,
|
|
2887
|
+
hasVideoInDisabled: disabledExtensions.includes("video"),
|
|
2888
|
+
disabledList: disabledExtensions.slice(0, 15)
|
|
2889
|
+
});
|
|
2890
|
+
}, [allowVideoUpload, disabledExtensions]);
|
|
2760
2891
|
const fileNameTransformRef = useRef8(s3Upload?.fileNameTransform);
|
|
2761
2892
|
useEffect7(() => {
|
|
2762
2893
|
fileNameTransformRef.current = s3Upload?.fileNameTransform;
|
|
@@ -2769,6 +2900,12 @@ function LumirEditor({
|
|
|
2769
2900
|
path: s3Upload.path,
|
|
2770
2901
|
appendUUID: s3Upload.appendUUID,
|
|
2771
2902
|
preserveExtension: s3Upload.preserveExtension,
|
|
2903
|
+
uploadTimeoutMs: s3Upload.uploadTimeoutMs,
|
|
2904
|
+
maxRetries: s3Upload.maxRetries,
|
|
2905
|
+
onProgress: (percent) => {
|
|
2906
|
+
setUploadProgress(percent);
|
|
2907
|
+
s3Upload.onProgress?.(percent);
|
|
2908
|
+
},
|
|
2772
2909
|
// 최신 콜백을 항상 사용하도록 ref를 통해 접근
|
|
2773
2910
|
fileNameTransform: (originalName, file) => {
|
|
2774
2911
|
return fileNameTransformRef.current ? fileNameTransformRef.current(originalName, file) : originalName;
|
|
@@ -2779,7 +2916,10 @@ function LumirEditor({
|
|
|
2779
2916
|
s3Upload?.env,
|
|
2780
2917
|
s3Upload?.path,
|
|
2781
2918
|
s3Upload?.appendUUID,
|
|
2782
|
-
s3Upload?.preserveExtension
|
|
2919
|
+
s3Upload?.preserveExtension,
|
|
2920
|
+
s3Upload?.uploadTimeoutMs,
|
|
2921
|
+
s3Upload?.maxRetries,
|
|
2922
|
+
s3Upload?.onProgress
|
|
2783
2923
|
]);
|
|
2784
2924
|
const editor = useCreateBlockNote(
|
|
2785
2925
|
{
|
|
@@ -2797,18 +2937,44 @@ function LumirEditor({
|
|
|
2797
2937
|
tabBehavior,
|
|
2798
2938
|
trailingBlock,
|
|
2799
2939
|
uploadFile: async (file) => {
|
|
2800
|
-
|
|
2801
|
-
|
|
2940
|
+
const allowedImage = isImageFile(file);
|
|
2941
|
+
const allowedVideo = allowVideoUpload && isVideoFile(file);
|
|
2942
|
+
DEBUG_LOG("uploadFile:step1:entry", "editor uploadFile callback invoked", {
|
|
2943
|
+
fileName: file.name,
|
|
2944
|
+
fileType: file.type,
|
|
2945
|
+
fileSize: file.size,
|
|
2946
|
+
allowVideoUpload,
|
|
2947
|
+
allowedImage,
|
|
2948
|
+
allowedVideo
|
|
2949
|
+
});
|
|
2950
|
+
if (!allowedImage && !allowedVideo) {
|
|
2951
|
+
const error = LumirEditorError.invalidFileType(
|
|
2952
|
+
file.name,
|
|
2953
|
+
allowVideoUpload
|
|
2954
|
+
);
|
|
2802
2955
|
handleError(error);
|
|
2803
2956
|
throw error;
|
|
2804
2957
|
}
|
|
2805
2958
|
try {
|
|
2806
|
-
|
|
2959
|
+
setUploadProgress(0);
|
|
2960
|
+
let fileUrl;
|
|
2961
|
+
const branch = uploadFile ? "custom" : memoizedS3Upload?.apiEndpoint ? "s3" : "none";
|
|
2962
|
+
DEBUG_LOG("uploadFile:step2:branch", "upload path", {
|
|
2963
|
+
branch,
|
|
2964
|
+
hasCustomUploadFile: !!uploadFile,
|
|
2965
|
+
hasS3ApiEndpoint: !!memoizedS3Upload?.apiEndpoint
|
|
2966
|
+
});
|
|
2807
2967
|
if (uploadFile) {
|
|
2808
|
-
|
|
2968
|
+
const t0 = Date.now();
|
|
2969
|
+
DEBUG_LOG("uploadFile:step3a:custom", "calling custom uploadFile", { fileName: file.name });
|
|
2970
|
+
fileUrl = await uploadFile(file);
|
|
2971
|
+
DEBUG_LOG("uploadFile:step3a:done", "custom uploadFile returned", { urlLen: fileUrl?.length, elapsedMs: Date.now() - t0 });
|
|
2809
2972
|
} else if (memoizedS3Upload?.apiEndpoint) {
|
|
2973
|
+
const t0 = Date.now();
|
|
2974
|
+
DEBUG_LOG("uploadFile:step3b:s3", "calling S3 uploader", { fileName: file.name });
|
|
2810
2975
|
const s3Uploader = createS3Uploader(memoizedS3Upload);
|
|
2811
|
-
|
|
2976
|
+
fileUrl = await s3Uploader(file);
|
|
2977
|
+
DEBUG_LOG("uploadFile:step3b:done", "S3 uploader returned", { urlLen: fileUrl?.length, elapsedMs: Date.now() - t0 });
|
|
2812
2978
|
} else {
|
|
2813
2979
|
const error = LumirEditorError.s3ConfigError(
|
|
2814
2980
|
"No upload method available. Please provide uploadFile or s3Upload configuration."
|
|
@@ -2816,8 +2982,16 @@ function LumirEditor({
|
|
|
2816
2982
|
handleError(error);
|
|
2817
2983
|
throw error;
|
|
2818
2984
|
}
|
|
2819
|
-
|
|
2985
|
+
DEBUG_LOG("uploadFile:step4:success", "returning URL", {
|
|
2986
|
+
fileName: file.name,
|
|
2987
|
+
urlPrefix: fileUrl.slice(0, 80)
|
|
2988
|
+
});
|
|
2989
|
+
return fileUrl;
|
|
2820
2990
|
} catch (error) {
|
|
2991
|
+
DEBUG_LOG("uploadFile:step5:catch", "uploadFile threw", {
|
|
2992
|
+
fileName: file.name,
|
|
2993
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
2994
|
+
});
|
|
2821
2995
|
if (error instanceof LumirEditorError) {
|
|
2822
2996
|
throw error;
|
|
2823
2997
|
}
|
|
@@ -2827,6 +3001,8 @@ function LumirEditor({
|
|
|
2827
3001
|
);
|
|
2828
3002
|
handleError(lumirError);
|
|
2829
3003
|
throw lumirError;
|
|
3004
|
+
} finally {
|
|
3005
|
+
setUploadProgress(null);
|
|
2830
3006
|
}
|
|
2831
3007
|
},
|
|
2832
3008
|
pasteHandler: (ctx) => {
|
|
@@ -2855,7 +3031,15 @@ function LumirEditor({
|
|
|
2855
3031
|
}
|
|
2856
3032
|
const fileList = event?.clipboardData?.files ?? null;
|
|
2857
3033
|
const files = fileList ? Array.from(fileList) : [];
|
|
2858
|
-
const acceptedFiles = files.filter(
|
|
3034
|
+
const acceptedFiles = files.filter(
|
|
3035
|
+
(f) => isImageFile(f) || allowVideoUpload && isVideoFile(f)
|
|
3036
|
+
);
|
|
3037
|
+
DEBUG_LOG("paste:step1:files", "paste clipboard files", {
|
|
3038
|
+
filesCount: files.length,
|
|
3039
|
+
acceptedCount: acceptedFiles.length,
|
|
3040
|
+
fileNames: files.map((f) => f.name),
|
|
3041
|
+
acceptedNames: acceptedFiles.map((f) => f.name)
|
|
3042
|
+
});
|
|
2859
3043
|
if (files.length > 0 && acceptedFiles.length === 0) {
|
|
2860
3044
|
event.preventDefault();
|
|
2861
3045
|
return true;
|
|
@@ -2869,13 +3053,26 @@ function LumirEditor({
|
|
|
2869
3053
|
try {
|
|
2870
3054
|
for (const file of acceptedFiles) {
|
|
2871
3055
|
try {
|
|
3056
|
+
DEBUG_LOG("paste:step2:upload", "calling uploadFile for paste", {
|
|
3057
|
+
fileName: file.name,
|
|
3058
|
+
fileType: file.type
|
|
3059
|
+
});
|
|
2872
3060
|
const url = await editor2.uploadFile(file);
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
3061
|
+
if (isImageFile(file)) {
|
|
3062
|
+
editor2.pasteHTML(
|
|
3063
|
+
`<img src="${escapeHtml(url)}" alt="image" />`
|
|
3064
|
+
);
|
|
3065
|
+
} else if (isVideoFile(file)) {
|
|
3066
|
+
const currentBlock = editor2.getTextCursorPosition().block;
|
|
3067
|
+
editor2.insertBlocks(
|
|
3068
|
+
[{ type: "video", props: { url } }],
|
|
3069
|
+
currentBlock,
|
|
3070
|
+
"after"
|
|
3071
|
+
);
|
|
3072
|
+
}
|
|
2876
3073
|
} catch (err) {
|
|
2877
3074
|
console.warn(
|
|
2878
|
-
"
|
|
3075
|
+
"Upload failed, skipped:",
|
|
2879
3076
|
file.name || "",
|
|
2880
3077
|
err
|
|
2881
3078
|
);
|
|
@@ -2898,6 +3095,7 @@ function LumirEditor({
|
|
|
2898
3095
|
trailingBlock,
|
|
2899
3096
|
uploadFile,
|
|
2900
3097
|
memoizedS3Upload,
|
|
3098
|
+
allowVideoUpload,
|
|
2901
3099
|
linkPreview?.apiEndpoint,
|
|
2902
3100
|
placeholder
|
|
2903
3101
|
]
|
|
@@ -2918,25 +3116,25 @@ function LumirEditor({
|
|
|
2918
3116
|
};
|
|
2919
3117
|
return editor.onEditorContentChange(handleContentChange);
|
|
2920
3118
|
}, [editor, onContentChange]);
|
|
2921
|
-
const
|
|
3119
|
+
const previousMediaUrlsRef = useRef8(/* @__PURE__ */ new Set());
|
|
2922
3120
|
useEffect7(() => {
|
|
2923
3121
|
if (!editor) return;
|
|
2924
3122
|
const initialBlocks = editor.topLevelBlocks;
|
|
2925
|
-
|
|
3123
|
+
previousMediaUrlsRef.current = extractMediaUrls(initialBlocks);
|
|
2926
3124
|
}, [editor]);
|
|
2927
3125
|
useEffect7(() => {
|
|
2928
3126
|
if (!editor || !onImageDelete) return;
|
|
2929
|
-
const
|
|
3127
|
+
const handleMediaDeleteCheck = () => {
|
|
2930
3128
|
const currentBlocks = editor.topLevelBlocks;
|
|
2931
|
-
const currentUrls =
|
|
2932
|
-
const previousUrls =
|
|
2933
|
-
const deletedUrls =
|
|
3129
|
+
const currentUrls = extractMediaUrls(currentBlocks);
|
|
3130
|
+
const previousUrls = previousMediaUrlsRef.current;
|
|
3131
|
+
const deletedUrls = findDeletedMediaUrls(previousUrls, currentUrls);
|
|
2934
3132
|
deletedUrls.forEach((url) => {
|
|
2935
3133
|
onImageDelete(url);
|
|
2936
3134
|
});
|
|
2937
|
-
|
|
3135
|
+
previousMediaUrlsRef.current = currentUrls;
|
|
2938
3136
|
};
|
|
2939
|
-
return editor.onEditorContentChange(
|
|
3137
|
+
return editor.onEditorContentChange(handleMediaDeleteCheck);
|
|
2940
3138
|
}, [editor, onImageDelete]);
|
|
2941
3139
|
useEffect7(() => {
|
|
2942
3140
|
const el = editor?.domElement;
|
|
@@ -2958,11 +3156,31 @@ function LumirEditor({
|
|
|
2958
3156
|
const items = Array.from(e.dataTransfer.items ?? []);
|
|
2959
3157
|
const files = items.filter((it) => it.kind === "file").map((it) => it.getAsFile()).filter((f) => !!f);
|
|
2960
3158
|
const imageFiles = files.filter(isImageFile);
|
|
3159
|
+
const videoFiles = allowVideoUpload ? files.filter(isVideoFile) : [];
|
|
2961
3160
|
const htmlFiles = files.filter(isHtmlFile);
|
|
2962
|
-
|
|
3161
|
+
DEBUG_LOG("drop:step1:files", "drop received", {
|
|
3162
|
+
filesCount: files.length,
|
|
3163
|
+
imageCount: imageFiles.length,
|
|
3164
|
+
videoCount: videoFiles.length,
|
|
3165
|
+
htmlCount: htmlFiles.length,
|
|
3166
|
+
allowVideoUpload,
|
|
3167
|
+
firstFile: files[0] ? {
|
|
3168
|
+
name: files[0].name,
|
|
3169
|
+
type: files[0].type,
|
|
3170
|
+
size: files[0].size,
|
|
3171
|
+
isImage: isImageFile(files[0]),
|
|
3172
|
+
isVideo: isVideoFile(files[0])
|
|
3173
|
+
} : null
|
|
3174
|
+
});
|
|
3175
|
+
if (imageFiles.length === 0 && htmlFiles.length === 0 && videoFiles.length === 0)
|
|
3176
|
+
return;
|
|
2963
3177
|
(async () => {
|
|
2964
3178
|
setIsUploading(true);
|
|
2965
3179
|
try {
|
|
3180
|
+
DEBUG_LOG("drop:step2:async", "drop async started", {
|
|
3181
|
+
imageCount: imageFiles.length,
|
|
3182
|
+
videoCount: videoFiles.length
|
|
3183
|
+
});
|
|
2966
3184
|
for (const file of imageFiles) {
|
|
2967
3185
|
try {
|
|
2968
3186
|
if (editor?.uploadFile) {
|
|
@@ -2981,6 +3199,34 @@ function LumirEditor({
|
|
|
2981
3199
|
);
|
|
2982
3200
|
}
|
|
2983
3201
|
}
|
|
3202
|
+
DEBUG_LOG("drop:step3:videoLoop", "video loop start", {
|
|
3203
|
+
videoCount: videoFiles.length,
|
|
3204
|
+
names: videoFiles.map((f) => f.name)
|
|
3205
|
+
});
|
|
3206
|
+
for (const file of videoFiles) {
|
|
3207
|
+
try {
|
|
3208
|
+
if (editor?.uploadFile) {
|
|
3209
|
+
DEBUG_LOG("drop:step4:videoUpload", "calling uploadFile for video", {
|
|
3210
|
+
fileName: file.name
|
|
3211
|
+
});
|
|
3212
|
+
const url = await editor.uploadFile(file);
|
|
3213
|
+
if (url && typeof url === "string") {
|
|
3214
|
+
const currentBlock = editor.getTextCursorPosition().block;
|
|
3215
|
+
editor.insertBlocks(
|
|
3216
|
+
[{ type: "video", props: { url } }],
|
|
3217
|
+
currentBlock,
|
|
3218
|
+
"after"
|
|
3219
|
+
);
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
} catch (err) {
|
|
3223
|
+
console.warn(
|
|
3224
|
+
"Video upload failed, skipped:",
|
|
3225
|
+
file.name || "",
|
|
3226
|
+
err
|
|
3227
|
+
);
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
2984
3230
|
for (const file of htmlFiles) {
|
|
2985
3231
|
try {
|
|
2986
3232
|
const htmlContent = await file.text();
|
|
@@ -3020,7 +3266,7 @@ function LumirEditor({
|
|
|
3020
3266
|
});
|
|
3021
3267
|
el.removeEventListener("drop", handleDrop, { capture: true });
|
|
3022
3268
|
};
|
|
3023
|
-
}, [editor]);
|
|
3269
|
+
}, [editor, allowVideoUpload]);
|
|
3024
3270
|
const computedSideMenu = useMemo(() => {
|
|
3025
3271
|
return sideMenuAddButton ? sideMenu : false;
|
|
3026
3272
|
}, [sideMenuAddButton, sideMenu]);
|
|
@@ -3033,42 +3279,110 @@ function LumirEditor({
|
|
|
3033
3279
|
className: cn("lumirEditor", className),
|
|
3034
3280
|
style: { position: "relative", display: "flex", flexDirection: "column" },
|
|
3035
3281
|
children: [
|
|
3036
|
-
floatingMenu && editor && /* @__PURE__ */
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3282
|
+
floatingMenu && editor && /* @__PURE__ */ jsxs10(Fragment4, { children: [
|
|
3283
|
+
/* @__PURE__ */ jsx16(
|
|
3284
|
+
"input",
|
|
3285
|
+
{
|
|
3286
|
+
ref: floatingMenuFileInputRef,
|
|
3287
|
+
type: "file",
|
|
3288
|
+
accept: allowVideoUpload ? "image/*,video/mp4,video/webm,video/ogg,video/quicktime,.mov" : "image/*",
|
|
3289
|
+
style: {
|
|
3290
|
+
position: "absolute",
|
|
3291
|
+
left: "-9999px",
|
|
3292
|
+
opacity: 0,
|
|
3293
|
+
pointerEvents: "none"
|
|
3294
|
+
},
|
|
3295
|
+
onChange: async (e) => {
|
|
3296
|
+
const inputEl = e.target;
|
|
3297
|
+
const file = inputEl.files?.[0];
|
|
3298
|
+
DEBUG_LOG("FloatingMenu:step3:onchange", "file input onchange fired", {
|
|
3299
|
+
hasFile: !!file,
|
|
3300
|
+
fileName: file?.name,
|
|
3301
|
+
fileType: file?.type,
|
|
3302
|
+
fileSize: file?.size,
|
|
3303
|
+
hasUploadFile: !!editor?.uploadFile
|
|
3304
|
+
});
|
|
3305
|
+
const blockToInsertAfter = floatingMenuBlockRef.current;
|
|
3306
|
+
if (file && editor.uploadFile && blockToInsertAfter) {
|
|
3307
|
+
const allowedImage = isImageFile(file);
|
|
3308
|
+
const allowedVideo = allowVideoUpload && isVideoFile(file);
|
|
3309
|
+
DEBUG_LOG("FloatingMenu:step4:fileCheck", "allowed check", {
|
|
3310
|
+
fileName: file.name,
|
|
3311
|
+
allowedImage,
|
|
3312
|
+
allowedVideo
|
|
3313
|
+
});
|
|
3314
|
+
if (allowedImage || allowedVideo) {
|
|
3315
|
+
try {
|
|
3316
|
+
setIsUploading(true);
|
|
3317
|
+
floatingMenuUploadStartTimeRef.current = Date.now();
|
|
3318
|
+
DEBUG_LOG("FloatingMenu:step5:uploadStart", "calling editor.uploadFile", {
|
|
3319
|
+
fileName: file.name
|
|
3320
|
+
});
|
|
3321
|
+
const url = await editor.uploadFile(file);
|
|
3322
|
+
const blockType = allowedVideo ? "video" : "image";
|
|
3323
|
+
const elapsedMs = Date.now() - floatingMenuUploadStartTimeRef.current;
|
|
3324
|
+
DEBUG_LOG("FloatingMenu:step6:uploadDone", "upload returned, inserting block", {
|
|
3325
|
+
blockType,
|
|
3326
|
+
blockId: blockToInsertAfter.id,
|
|
3327
|
+
urlLen: url?.length,
|
|
3328
|
+
elapsedMs
|
|
3329
|
+
});
|
|
3330
|
+
editor.insertBlocks(
|
|
3331
|
+
[
|
|
3332
|
+
{
|
|
3333
|
+
type: blockType,
|
|
3334
|
+
props: { url }
|
|
3335
|
+
}
|
|
3336
|
+
],
|
|
3337
|
+
blockToInsertAfter,
|
|
3338
|
+
"after"
|
|
3339
|
+
);
|
|
3340
|
+
} catch (err) {
|
|
3341
|
+
DEBUG_LOG("FloatingMenu:step7:catch", "upload or insert failed", {
|
|
3342
|
+
errMsg: err instanceof Error ? err.message : String(err)
|
|
3343
|
+
});
|
|
3344
|
+
console.error("Upload failed:", err);
|
|
3345
|
+
} finally {
|
|
3346
|
+
setIsUploading(false);
|
|
3347
|
+
}
|
|
3065
3348
|
}
|
|
3066
3349
|
}
|
|
3067
|
-
|
|
3068
|
-
|
|
3350
|
+
inputEl.value = "";
|
|
3351
|
+
}
|
|
3069
3352
|
}
|
|
3070
|
-
|
|
3071
|
-
|
|
3353
|
+
),
|
|
3354
|
+
/* @__PURE__ */ jsx16(
|
|
3355
|
+
FloatingMenu,
|
|
3356
|
+
{
|
|
3357
|
+
editor,
|
|
3358
|
+
position: floatingMenuPosition,
|
|
3359
|
+
onImageUpload: () => {
|
|
3360
|
+
DEBUG_LOG("FloatingMenu:step1:click", "upload button clicked", {
|
|
3361
|
+
allowVideoUpload
|
|
3362
|
+
});
|
|
3363
|
+
let blockToInsertAfter;
|
|
3364
|
+
try {
|
|
3365
|
+
blockToInsertAfter = editor.getTextCursorPosition().block;
|
|
3366
|
+
} catch (err) {
|
|
3367
|
+
DEBUG_LOG("FloatingMenu:step1b:error", "getTextCursorPosition failed", {
|
|
3368
|
+
err: err instanceof Error ? err.message : String(err)
|
|
3369
|
+
});
|
|
3370
|
+
return;
|
|
3371
|
+
}
|
|
3372
|
+
floatingMenuBlockRef.current = blockToInsertAfter;
|
|
3373
|
+
const input = floatingMenuFileInputRef.current;
|
|
3374
|
+
if (!input) return;
|
|
3375
|
+
input.accept = allowVideoUpload ? "image/*,video/mp4,video/webm,video/ogg,video/quicktime,.mov" : "image/*";
|
|
3376
|
+
input.value = "";
|
|
3377
|
+
DEBUG_LOG("FloatingMenu:step2:inputReady", "persistent input ref, about to click", {
|
|
3378
|
+
accept: input.accept
|
|
3379
|
+
});
|
|
3380
|
+
DEBUG_LOG("FloatingMenu:step2b:click", "input.click() about to be called", {});
|
|
3381
|
+
input.click();
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
)
|
|
3385
|
+
] }),
|
|
3072
3386
|
/* @__PURE__ */ jsxs10(
|
|
3073
3387
|
BlockNoteView,
|
|
3074
3388
|
{
|
|
@@ -3095,8 +3409,10 @@ function LumirEditor({
|
|
|
3095
3409
|
const filtered = items.filter((item) => {
|
|
3096
3410
|
const key = (item?.key || "").toString().toLowerCase();
|
|
3097
3411
|
const title = (item?.title || "").toString().toLowerCase();
|
|
3098
|
-
if (
|
|
3099
|
-
|
|
3412
|
+
if (key === "video" || title.includes("video"))
|
|
3413
|
+
return allowVideoUpload;
|
|
3414
|
+
if (["audio", "file"].includes(key)) return false;
|
|
3415
|
+
if (title.includes("audio") || title.includes("file"))
|
|
3100
3416
|
return false;
|
|
3101
3417
|
return true;
|
|
3102
3418
|
});
|
|
@@ -3197,7 +3513,7 @@ function LumirEditor({
|
|
|
3197
3513
|
)
|
|
3198
3514
|
);
|
|
3199
3515
|
},
|
|
3200
|
-
[editor, linkPreview?.apiEndpoint]
|
|
3516
|
+
[editor, allowVideoUpload, linkPreview?.apiEndpoint]
|
|
3201
3517
|
)
|
|
3202
3518
|
}
|
|
3203
3519
|
),
|
|
@@ -3205,7 +3521,13 @@ function LumirEditor({
|
|
|
3205
3521
|
]
|
|
3206
3522
|
}
|
|
3207
3523
|
),
|
|
3208
|
-
isUploading && /* @__PURE__ */
|
|
3524
|
+
isUploading && /* @__PURE__ */ jsxs10("div", { className: "lumirEditor-upload-overlay", children: [
|
|
3525
|
+
/* @__PURE__ */ jsx16("div", { className: "lumirEditor-spinner" }),
|
|
3526
|
+
uploadProgress !== null && /* @__PURE__ */ jsxs10("span", { className: "lumirEditor-upload-progress", children: [
|
|
3527
|
+
uploadProgress,
|
|
3528
|
+
"%"
|
|
3529
|
+
] })
|
|
3530
|
+
] }),
|
|
3209
3531
|
errorMessage && /* @__PURE__ */ jsxs10("div", { className: "lumirEditor-error-toast", children: [
|
|
3210
3532
|
/* @__PURE__ */ jsx16("span", { className: "lumirEditor-error-icon", children: "\u26A0\uFE0F" }),
|
|
3211
3533
|
/* @__PURE__ */ jsx16("span", { className: "lumirEditor-error-message", children: errorMessage }),
|