@lumir-company/editor 0.2.0 → 0.2.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/README.md +379 -925
- package/dist/index.d.mts +8 -20
- package/dist/index.d.ts +8 -20
- package/dist/index.js +104 -134
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +105 -135
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +37 -13
- package/package.json +6 -5
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/components/LumirEditor.tsx","../src/utils/cn.ts"],"sourcesContent":["\"use client\";\n\n// 컴포넌트 및 유틸리티 export\nexport {\n default as LumirEditor,\n ContentUtils,\n EditorConfig,\n} from \"./components/LumirEditor\";\nexport { cn } from \"./utils/cn\";\n\n// 타입 export (별도 파일에서 관리)\nexport type {\n LumirEditorProps,\n EditorType,\n DefaultPartialBlock,\n DefaultBlockSchema,\n DefaultInlineContentSchema,\n DefaultStyleSchema,\n PartialBlock,\n BlockNoteEditor,\n} from \"./types\";\n","\"use client\";\n\nimport { useEffect, useMemo } from \"react\";\nimport {\n useCreateBlockNote,\n SideMenu as BlockSideMenu,\n SideMenuController,\n DragHandleButton,\n SuggestionMenuController,\n getDefaultReactSlashMenuItems,\n} from \"@blocknote/react\";\nimport { BlockNoteView } from \"@blocknote/mantine\";\nimport { cn } from \"../utils/cn\";\n\nimport type {\n DefaultPartialBlock,\n LumirEditorProps,\n DefaultBlockSchema,\n DefaultInlineContentSchema,\n DefaultStyleSchema,\n} from \"../types\";\n\n// ==========================================\n// 유틸리티 클래스들 (안전한 리팩토링)\n// ==========================================\n\n/**\n * 콘텐츠 관리 유틸리티\n * 기본 블록 생성 및 콘텐츠 검증 로직을 담당\n */\nexport class ContentUtils {\n /**\n * JSON 문자열의 유효성을 검증합니다\n * @param jsonString 검증할 JSON 문자열\n * @returns 유효한 JSON 문자열인지 여부\n */\n static isValidJSONString(jsonString: string): boolean {\n try {\n const parsed = JSON.parse(jsonString);\n return Array.isArray(parsed);\n } catch {\n return false;\n }\n }\n\n /**\n * JSON 문자열을 DefaultPartialBlock 배열로 파싱합니다\n * @param jsonString JSON 문자열\n * @returns 파싱된 블록 배열 또는 null (파싱 실패 시)\n */\n static parseJSONContent(jsonString: string): DefaultPartialBlock[] | null {\n try {\n const parsed = JSON.parse(jsonString);\n if (Array.isArray(parsed)) {\n return parsed as DefaultPartialBlock[];\n }\n return null;\n } catch {\n return null;\n }\n }\n /**\n * 기본 paragraph 블록 생성\n * @returns 기본 설정이 적용된 DefaultPartialBlock\n */\n static createDefaultBlock(): DefaultPartialBlock {\n return {\n type: \"paragraph\",\n props: {\n textColor: \"default\",\n backgroundColor: \"default\",\n textAlignment: \"left\",\n },\n content: [{ type: \"text\", text: \"\", styles: {} }],\n children: [],\n };\n }\n\n /**\n * 콘텐츠 유효성 검증 및 기본값 설정\n * @param content 사용자 제공 콘텐츠 (객체 배열 또는 JSON 문자열)\n * @param emptyBlockCount 빈 블록 개수 (기본값: 3)\n * @param placeholder 첫 번째 블록의 placeholder 텍스트\n * @returns 검증된 콘텐츠 배열\n */\n static validateContent(\n content?: DefaultPartialBlock[] | string,\n emptyBlockCount: number = 3,\n placeholder?: string\n ): DefaultPartialBlock[] {\n // 1. 문자열인 경우 JSON 파싱 시도\n if (typeof content === \"string\") {\n if (content.trim() === \"\") {\n return this.createEmptyBlocks(emptyBlockCount, placeholder);\n }\n\n const parsedContent = this.parseJSONContent(content);\n if (parsedContent && parsedContent.length > 0) {\n return parsedContent;\n }\n\n // 파싱 실패 시 빈 블록 생성\n return this.createEmptyBlocks(emptyBlockCount, placeholder);\n }\n\n // 2. 배열인 경우 기존 로직\n if (!content || content.length === 0) {\n return this.createEmptyBlocks(emptyBlockCount, placeholder);\n }\n\n return content;\n }\n\n /**\n * 빈 블록들을 생성합니다\n * @param emptyBlockCount 생성할 블록 개수\n * @param placeholder 첫 번째 블록의 placeholder 텍스트\n * @returns 생성된 빈 블록 배열\n */\n private static createEmptyBlocks(\n emptyBlockCount: number,\n placeholder?: string\n ): DefaultPartialBlock[] {\n return Array.from({ length: emptyBlockCount }, (_, index) => {\n const block = this.createDefaultBlock();\n // 첫 번째 블록에 placeholder 텍스트 적용\n if (index === 0 && placeholder) {\n block.content = [{ type: \"text\", text: placeholder, styles: {} }];\n }\n return block;\n });\n }\n}\n\n/**\n * 에디터 설정 관리 유틸리티\n * 각종 설정의 기본값과 검증 로직을 담당\n */\nexport class EditorConfig {\n /**\n * 테이블 설정 기본값 적용\n * @param userTables 사용자 테이블 설정\n * @returns 기본값이 적용된 테이블 설정\n */\n static getDefaultTableConfig(userTables?: LumirEditorProps[\"tables\"]) {\n return {\n splitCells: userTables?.splitCells ?? true,\n cellBackgroundColor: userTables?.cellBackgroundColor ?? true,\n cellTextColor: userTables?.cellTextColor ?? true,\n headers: userTables?.headers ?? true,\n };\n }\n\n /**\n * 헤딩 설정 기본값 적용\n * @param userHeading 사용자 헤딩 설정\n * @returns 기본값이 적용된 헤딩 설정\n */\n static getDefaultHeadingConfig(userHeading?: LumirEditorProps[\"heading\"]) {\n return userHeading?.levels && userHeading.levels.length > 0\n ? userHeading\n : { levels: [1, 2, 3, 4, 5, 6] as (1 | 2 | 3 | 4 | 5 | 6)[] };\n }\n\n /**\n * 비활성화할 확장 기능 목록 생성\n * @param userExtensions 사용자 정의 비활성 확장\n * @param allowVideo 비디오 업로드 허용 여부\n * @param allowAudio 오디오 업로드 허용 여부\n * @returns 비활성화할 확장 기능 목록\n */\n static getDisabledExtensions(\n userExtensions?: string[],\n allowVideo = false,\n allowAudio = false\n ): string[] {\n const set = new Set<string>(userExtensions ?? []);\n if (!allowVideo) set.add(\"video\");\n if (!allowAudio) set.add(\"audio\");\n return Array.from(set);\n }\n}\n\nconst createObjectUrlUploader = async (file: File): Promise<string> => {\n return URL.createObjectURL(file);\n};\n\nconst fileToBase64 = async (file: File): Promise<string> =>\n await new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(String(reader.result));\n reader.onerror = () => reject(new Error(\"FileReader failed\"));\n reader.readAsDataURL(file);\n });\n\nexport default function LumirEditor({\n // editor options\n initialContent,\n initialEmptyBlocks = 3,\n placeholder,\n uploadFile,\n pasteHandler,\n tables,\n heading,\n animations = true,\n defaultStyles = true,\n disableExtensions,\n domAttributes,\n tabBehavior = \"prefer-navigate-ui\",\n trailingBlock = true,\n resolveFileUrl,\n storeImagesAsBase64 = true,\n allowVideoUpload = false,\n allowAudioUpload = false,\n allowFileUpload = false,\n // view options\n editable = true,\n theme = \"light\",\n formattingToolbar = true,\n linkToolbar = true,\n sideMenu = true,\n slashMenu = true,\n emojiPicker = true,\n filePanel = true,\n tableHandles = true,\n comments = true,\n onSelectionChange,\n className = \"\",\n includeDefaultStyles = true,\n sideMenuAddButton = true,\n // callbacks / refs\n onContentChange,\n editorRef,\n}: LumirEditorProps) {\n const validatedContent = useMemo<DefaultPartialBlock[]>(() => {\n return ContentUtils.validateContent(\n initialContent,\n initialEmptyBlocks,\n placeholder\n );\n }, [initialContent, initialEmptyBlocks, placeholder]);\n\n const editor = useCreateBlockNote<\n DefaultBlockSchema,\n DefaultInlineContentSchema,\n DefaultStyleSchema\n >(\n {\n initialContent: validatedContent as DefaultPartialBlock[],\n tables: EditorConfig.getDefaultTableConfig(tables),\n heading: EditorConfig.getDefaultHeadingConfig(heading),\n animations,\n defaultStyles,\n // 확장 비활성: 비디오/오디오만 제어(파일 확장은 내부 드롭 로직 의존 → 비활성화하지 않음)\n disableExtensions: useMemo(() => {\n return EditorConfig.getDisabledExtensions(\n disableExtensions,\n allowVideoUpload,\n allowAudioUpload\n );\n }, [disableExtensions, allowVideoUpload, allowAudioUpload]),\n domAttributes,\n tabBehavior,\n trailingBlock,\n resolveFileUrl,\n uploadFile: async (file) => {\n const custom = uploadFile;\n const fallback = storeImagesAsBase64\n ? fileToBase64\n : createObjectUrlUploader;\n try {\n if (custom) return await custom(file);\n return await fallback(file);\n } catch (_) {\n // Fallback to ObjectURL when FileReader or custom upload fails\n try {\n return await createObjectUrlUploader(file);\n } catch {\n throw new Error(\"Failed to process file for upload\");\n }\n }\n },\n pasteHandler: (ctx) => {\n const { event, editor, defaultPasteHandler } = ctx as any;\n const fileList =\n (event?.clipboardData?.files as FileList | null) ?? null;\n const files: File[] = fileList ? Array.from(fileList) : [];\n const accepted: File[] = files.filter(\n (f: File) =>\n f.size > 0 &&\n (f.type?.startsWith(\"image/\") ||\n (!f.type &&\n /\\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(f.name || \"\")))\n );\n // 파일 항목이 있으나 허용되지 않으면 기본 처리도 막고 무시\n if (files.length > 0 && accepted.length === 0) {\n event.preventDefault();\n return true;\n }\n if (accepted.length === 0) return defaultPasteHandler() ?? false;\n event.preventDefault();\n (async () => {\n const doUpload =\n uploadFile ??\n (storeImagesAsBase64 ? fileToBase64 : createObjectUrlUploader);\n for (const file of accepted) {\n try {\n const url = await doUpload(file);\n editor.pasteHTML(`<img src=\"${url}\" alt=\"image\" />`);\n } catch (err) {\n // 업로드 실패 파일은 삽입하지 않음 (삭제/스킵)\n // console.warn로만 기록하여 UI 오류를 막음\n console.warn(\n \"Image upload failed, skipped:\",\n file.name || \"\",\n err\n );\n continue;\n }\n }\n })();\n return true;\n },\n },\n [\n uploadFile,\n pasteHandler,\n storeImagesAsBase64,\n allowVideoUpload,\n allowAudioUpload,\n allowFileUpload,\n tables?.splitCells,\n tables?.cellBackgroundColor,\n tables?.cellTextColor,\n tables?.headers,\n heading?.levels?.join(\",\"),\n animations,\n defaultStyles,\n disableExtensions?.join(\",\"),\n domAttributes ? JSON.stringify(domAttributes) : undefined,\n tabBehavior,\n trailingBlock,\n resolveFileUrl,\n ]\n );\n\n useEffect(() => {\n if (!editor) return;\n editor.isEditable = editable;\n const el = editor.domElement as HTMLElement | undefined;\n if (!editable) {\n if (el) {\n el.style.userSelect = \"text\";\n (\n el.style as CSSStyleDeclaration & { webkitUserSelect?: string }\n ).webkitUserSelect = \"text\";\n }\n }\n }, [editor, editable]);\n\n useEffect(() => {\n if (!editor || !onContentChange) return;\n let lastContent = \"\";\n const handleContentChange = () => {\n const topLevelBlocks =\n editor.topLevelBlocks as unknown as DefaultPartialBlock[];\n const currentContent = JSON.stringify(topLevelBlocks);\n if (lastContent === currentContent) return;\n lastContent = currentContent;\n onContentChange(topLevelBlocks);\n };\n editor.onEditorContentChange(handleContentChange);\n return () => {};\n }, [editor, onContentChange]);\n\n // 외부에서 imperative API 접근을 위한 ref 연결\n useEffect(() => {\n if (!editorRef) return;\n editorRef.current = editor ?? null;\n return () => {\n if (editorRef) editorRef.current = null;\n };\n }, [editor, editorRef]);\n\n useEffect(() => {\n const el = editor?.domElement as HTMLElement | undefined;\n if (!el) return;\n const handleDragOver = (e: DragEvent) => {\n if (e.defaultPrevented) return;\n const hasFiles = (\n e.dataTransfer?.types as unknown as string[] | undefined\n )?.includes?.(\"Files\");\n if (hasFiles) {\n e.preventDefault();\n e.stopPropagation();\n // @ts-ignore\n if (typeof (e as any).stopImmediatePropagation === \"function\") {\n // @ts-ignore\n (e as any).stopImmediatePropagation();\n }\n }\n };\n\n const handleDrop = (e: DragEvent) => {\n if (!e.dataTransfer) return;\n const hasFiles = (\n (e.dataTransfer.types as unknown as string[] | undefined) ?? []\n ).includes(\"Files\");\n if (!hasFiles) return;\n\n // 기본 드롭 동작을 항상 차단해, 허용되지 않는 파일이 렌더링되지 않도록 함\n e.preventDefault();\n e.stopPropagation();\n // @ts-ignore\n (e as any).stopImmediatePropagation?.();\n\n // DataTransferItem 우선 (디렉토리/가짜 항목 배제)\n const items = Array.from(e.dataTransfer.items ?? []);\n const files = items\n .filter((it) => it.kind === \"file\")\n .map((it) => it.getAsFile())\n .filter((f): f is File => !!f);\n\n const accepted = files.filter(\n (f) =>\n f.size > 0 &&\n (f.type?.startsWith(\"image/\") ||\n (!f.type && /\\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(f.name || \"\")))\n );\n if (accepted.length === 0) return; // 차단만 하고 아무것도 삽입하지 않음\n\n (async () => {\n const doUpload =\n uploadFile ??\n (storeImagesAsBase64 ? fileToBase64 : createObjectUrlUploader);\n for (const f of accepted) {\n try {\n const url = await doUpload(f);\n editor?.pasteHTML(`<img src=\"${url}\" alt=\"image\" />`);\n } catch (err) {\n // 실패 파일은 삽입하지 않음\n console.warn(\"Image upload failed, skipped:\", f.name || \"\", err);\n continue;\n }\n }\n })();\n };\n el.addEventListener(\"dragover\", handleDragOver, { capture: true });\n el.addEventListener(\"drop\", handleDrop, { capture: true });\n return () => {\n el.removeEventListener(\"dragover\", handleDragOver, {\n capture: true,\n } as any);\n el.removeEventListener(\"drop\", handleDrop, { capture: true } as any);\n };\n }, [\n editor,\n uploadFile,\n storeImagesAsBase64,\n allowVideoUpload,\n allowAudioUpload,\n ]);\n\n // Add block 버튼을 끄면 기본 SideMenu를 비활성화하고 커스텀 SideMenu만 렌더(드래그 핸들 유지)\n const computedSideMenu = sideMenuAddButton ? sideMenu : false;\n\n // 공식 가이드 방식: 커스텀 SideMenu 컴포넌트 전달 (버튼 미제공 → 플러스 버튼 없음)\n // 공식 가이드 패턴: props를 전달받아 DragHandleButton만 유지\n const DragHandleOnlySideMenu = (props: any) => {\n return (\n <BlockSideMenu {...props}>\n <DragHandleButton {...props} />\n </BlockSideMenu>\n );\n };\n\n return (\n <BlockNoteView\n className={cn(\n includeDefaultStyles &&\n 'lumirEditor w-full h-full min-w-[300px] overflow-auto rounded-md border border-gray-300 focus-within:ring-2 focus-within:ring-black [&_.bn-editor]:px-[12px] [&_[data-content-type=\"paragraph\"]]:text-[14px] bg-white',\n className\n )}\n editor={editor}\n editable={editable}\n theme={theme as \"light\" | \"dark\" | undefined}\n formattingToolbar={formattingToolbar}\n linkToolbar={linkToolbar}\n sideMenu={computedSideMenu}\n slashMenu={false}\n emojiPicker={emojiPicker}\n filePanel={filePanel}\n tableHandles={tableHandles}\n comments={comments}\n onSelectionChange={onSelectionChange}\n >\n <SuggestionMenuController\n triggerCharacter=\"/\"\n getItems={async (query: string) => {\n const items = getDefaultReactSlashMenuItems(editor);\n const filtered = items.filter((it: any) => {\n const k = (it?.key || \"\").toString();\n if ([\"video\", \"audio\", \"file\"].includes(k)) return false;\n return true;\n });\n if (!query) return filtered;\n const q = query.toLowerCase();\n return filtered.filter(\n (it: any) =>\n (it.title || \"\").toLowerCase().includes(q) ||\n (it.aliases || []).some((a: string) =>\n a.toLowerCase().includes(q)\n )\n );\n }}\n />\n {!sideMenuAddButton && (\n <SideMenuController sideMenu={DragHandleOnlySideMenu} />\n )}\n </BlockNoteView>\n );\n}\n","// clsx와 tailwind-merge를 사용한 className 유틸리티\r\n// 사용자가 직접 설치하도록 권장하거나, 간단한 버전 제공\r\n\r\nexport function cn(...inputs: (string | undefined | null | false)[]) {\r\n return inputs.filter(Boolean).join(' ');\r\n}\r\n"],"mappings":"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","names":["import_react","editor","BlockSideMenu"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/components/LumirEditor.tsx","../src/utils/cn.ts"],"sourcesContent":["'use client';\r\n\r\n// 컴포넌트 및 유틸리티 export\r\nexport {\r\n default as LumirEditor,\r\n ContentUtils,\r\n EditorConfig,\r\n} from './components/LumirEditor';\r\nexport { cn } from './utils/cn';\r\n\r\n// 타입 export (별도 파일에서 관리)\r\nexport type {\r\n LumirEditorProps,\r\n EditorType,\r\n DefaultPartialBlock,\r\n DefaultBlockSchema,\r\n DefaultInlineContentSchema,\r\n DefaultStyleSchema,\r\n PartialBlock,\r\n BlockNoteEditor,\r\n} from './types';\r\n","'use client';\n\nimport { useEffect, useMemo, useCallback } from 'react';\nimport {\n useCreateBlockNote,\n SideMenu as BlockSideMenu,\n SideMenuController,\n DragHandleButton,\n SuggestionMenuController,\n getDefaultReactSlashMenuItems,\n} from '@blocknote/react';\nimport { BlockNoteView } from '@blocknote/mantine';\nimport { cn } from '../utils/cn';\n\nimport type {\n DefaultPartialBlock,\n LumirEditorProps,\n DefaultBlockSchema,\n DefaultInlineContentSchema,\n DefaultStyleSchema,\n} from '../types';\n\n// ==========================================\n// 유틸리티 클래스들\n// ==========================================\n\n/**\n * 콘텐츠 관리 유틸리티\n * 기본 블록 생성 및 콘텐츠 검증 로직을 담당\n */\nexport class ContentUtils {\n /**\n * JSON 문자열의 유효성을 검증합니다\n * @param jsonString 검증할 JSON 문자열\n * @returns 유효한 JSON 문자열인지 여부\n */\n static isValidJSONString(jsonString: string): boolean {\n try {\n const parsed = JSON.parse(jsonString);\n return Array.isArray(parsed);\n } catch {\n return false;\n }\n }\n\n /**\n * JSON 문자열을 DefaultPartialBlock 배열로 파싱합니다\n * @param jsonString JSON 문자열\n * @returns 파싱된 블록 배열 또는 null (파싱 실패 시)\n */\n static parseJSONContent(jsonString: string): DefaultPartialBlock[] | null {\n try {\n const parsed = JSON.parse(jsonString);\n if (Array.isArray(parsed)) {\n return parsed as DefaultPartialBlock[];\n }\n return null;\n } catch {\n return null;\n }\n }\n\n /**\n * 기본 paragraph 블록 생성\n * @returns 기본 설정이 적용된 DefaultPartialBlock\n */\n static createDefaultBlock(): DefaultPartialBlock {\n return {\n type: 'paragraph',\n props: {\n textColor: 'default',\n backgroundColor: 'default',\n textAlignment: 'left',\n },\n content: [{ type: 'text', text: '', styles: {} }],\n children: [],\n };\n }\n\n /**\n * 콘텐츠 유효성 검증 및 기본값 설정\n * @param content 사용자 제공 콘텐츠 (객체 배열 또는 JSON 문자열)\n * @param emptyBlockCount 빈 블록 개수 (기본값: 3)\n * @returns 검증된 콘텐츠 배열\n */\n static validateContent(\n content?: DefaultPartialBlock[] | string,\n emptyBlockCount: number = 3,\n ): DefaultPartialBlock[] {\n // 1. 문자열인 경우 JSON 파싱 시도\n if (typeof content === 'string') {\n if (content.trim() === '') {\n return this.createEmptyBlocks(emptyBlockCount);\n }\n\n const parsedContent = this.parseJSONContent(content);\n if (parsedContent && parsedContent.length > 0) {\n return parsedContent;\n }\n\n // 파싱 실패 시 빈 블록 생성\n return this.createEmptyBlocks(emptyBlockCount);\n }\n\n // 2. 배열인 경우 기존 로직\n if (!content || content.length === 0) {\n return this.createEmptyBlocks(emptyBlockCount);\n }\n\n return content;\n }\n\n /**\n * 빈 블록들을 생성합니다\n * @param emptyBlockCount 생성할 블록 개수\n * @returns 생성된 빈 블록 배열\n */\n private static createEmptyBlocks(\n emptyBlockCount: number,\n ): DefaultPartialBlock[] {\n return Array.from({ length: emptyBlockCount }, () =>\n this.createDefaultBlock(),\n );\n }\n}\n\n/**\n * 에디터 설정 관리 유틸리티\n * 각종 설정의 기본값과 검증 로직을 담당\n */\nexport class EditorConfig {\n /**\n * 테이블 설정 기본값 적용\n * @param userTables 사용자 테이블 설정\n * @returns 기본값이 적용된 테이블 설정\n */\n static getDefaultTableConfig(userTables?: LumirEditorProps['tables']) {\n return {\n splitCells: userTables?.splitCells ?? true,\n cellBackgroundColor: userTables?.cellBackgroundColor ?? true,\n cellTextColor: userTables?.cellTextColor ?? true,\n headers: userTables?.headers ?? true,\n };\n }\n\n /**\n * 헤딩 설정 기본값 적용\n * @param userHeading 사용자 헤딩 설정\n * @returns 기본값이 적용된 헤딩 설정\n */\n static getDefaultHeadingConfig(userHeading?: LumirEditorProps['heading']) {\n return userHeading?.levels && userHeading.levels.length > 0\n ? userHeading\n : { levels: [1, 2, 3, 4, 5, 6] as (1 | 2 | 3 | 4 | 5 | 6)[] };\n }\n\n /**\n * 비활성화할 확장 기능 목록 생성\n * @param userExtensions 사용자 정의 비활성 확장\n * @param allowVideo 비디오 업로드 허용 여부\n * @param allowAudio 오디오 업로드 허용 여부\n * @param allowFile 일반 파일 업로드 허용 여부\n * @returns 비활성화할 확장 기능 목록\n */\n static getDisabledExtensions(\n userExtensions?: string[],\n allowVideo = false,\n allowAudio = false,\n allowFile = false,\n ): string[] {\n const set = new Set<string>(userExtensions ?? []);\n if (!allowVideo) set.add('video');\n if (!allowAudio) set.add('audio');\n if (!allowFile) set.add('file');\n return Array.from(set);\n }\n}\n\nconst createObjectUrlUploader = async (file: File): Promise<string> => {\n return URL.createObjectURL(file);\n};\n\n// 파일 타입 검증 함수\nconst isImageFile = (file: File): boolean => {\n return (\n file.size > 0 &&\n (file.type?.startsWith('image/') ||\n (!file.type && /\\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(file.name || '')))\n );\n};\n\n// 이미지 파일을 Base64로 변환하는 함수\nconst fileToBase64 = async (file: File): Promise<string> =>\n await new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(String(reader.result));\n reader.onerror = () => reject(new Error('FileReader failed'));\n reader.readAsDataURL(file);\n });\n\nexport default function LumirEditor({\n // editor options\n initialContent,\n initialEmptyBlocks = 3,\n uploadFile,\n tables,\n heading,\n animations = true,\n defaultStyles = true,\n disableExtensions,\n tabBehavior = 'prefer-navigate-ui',\n trailingBlock = true,\n resolveFileUrl,\n storeImagesAsBase64 = true,\n allowVideoUpload = false,\n allowAudioUpload = false,\n allowFileUpload = false,\n // view options\n editable = true,\n theme = 'light',\n formattingToolbar = true,\n linkToolbar = true,\n sideMenu = true,\n slashMenu = true,\n emojiPicker = true,\n filePanel = true,\n tableHandles = true,\n onSelectionChange,\n className = '',\n sideMenuAddButton = false,\n // callbacks / refs\n onContentChange,\n}: LumirEditorProps) {\n const validatedContent = useMemo<DefaultPartialBlock[]>(() => {\n return ContentUtils.validateContent(initialContent, initialEmptyBlocks);\n }, [initialContent, initialEmptyBlocks]);\n\n // 테이블 설정 메모이제이션\n const tableConfig = useMemo(() => {\n return EditorConfig.getDefaultTableConfig(tables);\n }, [\n tables?.splitCells,\n tables?.cellBackgroundColor,\n tables?.cellTextColor,\n tables?.headers,\n ]);\n\n // 헤딩 설정 메모이제이션\n const headingConfig = useMemo(() => {\n return EditorConfig.getDefaultHeadingConfig(heading);\n }, [heading?.levels?.join(',') ?? '']);\n\n // 비활성화 확장 메모이제이션\n const disabledExtensions = useMemo(() => {\n return EditorConfig.getDisabledExtensions(\n disableExtensions,\n allowVideoUpload,\n allowAudioUpload,\n allowFileUpload,\n );\n }, [disableExtensions, allowVideoUpload, allowAudioUpload, allowFileUpload]);\n\n const editor = useCreateBlockNote<\n DefaultBlockSchema,\n DefaultInlineContentSchema,\n DefaultStyleSchema\n >(\n {\n initialContent: validatedContent as DefaultPartialBlock[],\n tables: tableConfig,\n heading: headingConfig,\n animations,\n defaultStyles,\n // 확장 비활성: 비디오/오디오/파일 제어\n disableExtensions: disabledExtensions,\n tabBehavior,\n trailingBlock,\n resolveFileUrl,\n uploadFile: async (file) => {\n // 이미지 파일만 허용 (이미지 전용 에디터)\n if (!isImageFile(file)) {\n throw new Error('Only image files are allowed');\n }\n\n const custom = uploadFile;\n const fallback = storeImagesAsBase64\n ? fileToBase64\n : createObjectUrlUploader;\n try {\n if (custom) return await custom(file);\n return await fallback(file);\n } catch (_) {\n // Fallback to ObjectURL when FileReader or custom upload fails\n try {\n return await createObjectUrlUploader(file);\n } catch {\n throw new Error('Failed to process file for upload');\n }\n }\n },\n pasteHandler: (ctx) => {\n const { event, editor, defaultPasteHandler } = ctx as any;\n const fileList =\n (event?.clipboardData?.files as FileList | null) ?? null;\n const files: File[] = fileList ? Array.from(fileList) : [];\n const acceptedFiles: File[] = files.filter(isImageFile);\n\n // 파일이 있지만 이미지가 없으면 기본 처리 막고 무시\n if (files.length > 0 && acceptedFiles.length === 0) {\n event.preventDefault();\n return true;\n }\n\n // 이미지가 없으면 기본 처리\n if (acceptedFiles.length === 0) {\n return defaultPasteHandler() ?? false;\n }\n\n event.preventDefault();\n (async () => {\n for (const file of acceptedFiles) {\n try {\n // 에디터의 uploadFile 함수 사용 (통일된 로직)\n const url = await editor.uploadFile(file);\n editor.pasteHTML(`<img src=\"${url}\" alt=\"image\" />`);\n } catch (err) {\n console.warn(\n 'Image upload failed, skipped:',\n file.name || '',\n err,\n );\n }\n }\n })();\n return true;\n },\n },\n [\n validatedContent,\n tableConfig,\n headingConfig,\n animations,\n defaultStyles,\n disabledExtensions,\n tabBehavior,\n trailingBlock,\n resolveFileUrl,\n uploadFile,\n storeImagesAsBase64,\n ],\n );\n\n // 편집 가능 여부 설정\n useEffect(() => {\n if (editor) {\n editor.isEditable = editable;\n }\n }, [editor, editable]);\n\n // 콘텐츠 변경 감지\n useEffect(() => {\n if (!editor || !onContentChange) return;\n\n const handleContentChange = () => {\n // BlockNote의 올바른 API 사용\n const blocks = editor.topLevelBlocks as DefaultPartialBlock[];\n onContentChange(blocks);\n };\n\n return editor.onEditorContentChange(handleContentChange);\n }, [editor, onContentChange]);\n\n // 드래그앤드롭 이미지 처리\n useEffect(() => {\n const el = editor?.domElement as HTMLElement | undefined;\n if (!el) return;\n\n const handleDragOver = (e: DragEvent) => {\n if (e.defaultPrevented) return;\n const hasFiles = (\n e.dataTransfer?.types as unknown as string[] | undefined\n )?.includes?.('Files');\n if (hasFiles) {\n e.preventDefault();\n e.stopPropagation();\n }\n };\n\n const handleDrop = (e: DragEvent) => {\n if (!e.dataTransfer) return;\n const hasFiles = (\n (e.dataTransfer.types as unknown as string[] | undefined) ?? []\n ).includes('Files');\n if (!hasFiles) return;\n\n e.preventDefault();\n e.stopPropagation();\n\n const items = Array.from(e.dataTransfer.items ?? []);\n const files = items\n .filter((it) => it.kind === 'file')\n .map((it) => it.getAsFile())\n .filter((f): f is File => !!f);\n\n // 이미지 파일만 허용\n const acceptedFiles = files.filter(isImageFile);\n\n if (acceptedFiles.length === 0) return;\n\n (async () => {\n for (const file of acceptedFiles) {\n try {\n // 에디터의 uploadFile 함수 사용 (일관된 로직)\n if (editor?.uploadFile) {\n const url = await editor.uploadFile(file);\n if (url) {\n editor.pasteHTML(`<img src=\"${url}\" alt=\"image\" />`);\n }\n }\n } catch (err) {\n console.warn('Image upload failed, skipped:', file.name || '', err);\n }\n }\n })();\n };\n\n el.addEventListener('dragover', handleDragOver, { capture: true });\n el.addEventListener('drop', handleDrop, { capture: true });\n\n return () => {\n el.removeEventListener('dragover', handleDragOver, {\n capture: true,\n } as any);\n el.removeEventListener('drop', handleDrop, { capture: true } as any);\n };\n }, [editor]);\n\n // SideMenu 설정 (Add 버튼 제어)\n const computedSideMenu = useMemo(() => {\n return sideMenuAddButton ? sideMenu : false;\n }, [sideMenuAddButton, sideMenu]);\n\n // Add 버튼 없는 사이드 메뉴 (드래그 핸들만) - 메모이제이션\n const DragHandleOnlySideMenu = useMemo(() => {\n return (props: any) => (\n <BlockSideMenu {...props}>\n <DragHandleButton {...props} />\n </BlockSideMenu>\n );\n }, []);\n\n return (\n <div className={cn('lumirEditor', className)}>\n <BlockNoteView\n editor={editor}\n editable={editable}\n theme={theme}\n formattingToolbar={formattingToolbar}\n linkToolbar={linkToolbar}\n sideMenu={computedSideMenu}\n slashMenu={false}\n emojiPicker={emojiPicker}\n filePanel={filePanel}\n tableHandles={tableHandles}\n onSelectionChange={onSelectionChange}>\n {slashMenu && (\n <SuggestionMenuController\n triggerCharacter='/'\n getItems={useCallback(\n async (query: string) => {\n const items = getDefaultReactSlashMenuItems(editor);\n // 비디오, 오디오, 파일 관련 항목 제거\n const filtered = items.filter((item: any) => {\n const key = (item?.key || '').toString().toLowerCase();\n const title = (item?.title || '').toString().toLowerCase();\n // 비디오, 오디오, 파일 관련 항목 제거\n if (['video', 'audio', 'file'].includes(key)) return false;\n if (\n title.includes('video') ||\n title.includes('audio') ||\n title.includes('file')\n )\n return false;\n return true;\n });\n\n if (!query) return filtered;\n const q = query.toLowerCase();\n return filtered.filter(\n (item: any) =>\n item.title?.toLowerCase().includes(q) ||\n (item.aliases || []).some((a: string) =>\n a.toLowerCase().includes(q),\n ),\n );\n },\n [editor],\n )}\n />\n )}\n {!sideMenuAddButton && (\n <SideMenuController sideMenu={DragHandleOnlySideMenu} />\n )}\n </BlockNoteView>\n </div>\n );\n}\n","// clsx와 tailwind-merge를 사용한 className 유틸리티\r\n// 사용자가 직접 설치하도록 권장하거나, 간단한 버전 제공\r\n\r\nexport function cn(...inputs: (string | undefined | null | false)[]) {\r\n return inputs.filter(Boolean).join(' ');\r\n}\r\n"],"mappings":"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","names":["import_react","editor","BlockSideMenu"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/components/LumirEditor.tsx
|
|
4
|
-
import { useEffect, useMemo } from "react";
|
|
4
|
+
import { useEffect, useMemo, useCallback } from "react";
|
|
5
5
|
import {
|
|
6
6
|
useCreateBlockNote,
|
|
7
7
|
SideMenu as BlockSideMenu,
|
|
@@ -69,39 +69,34 @@ var ContentUtils = class {
|
|
|
69
69
|
* 콘텐츠 유효성 검증 및 기본값 설정
|
|
70
70
|
* @param content 사용자 제공 콘텐츠 (객체 배열 또는 JSON 문자열)
|
|
71
71
|
* @param emptyBlockCount 빈 블록 개수 (기본값: 3)
|
|
72
|
-
* @param placeholder 첫 번째 블록의 placeholder 텍스트
|
|
73
72
|
* @returns 검증된 콘텐츠 배열
|
|
74
73
|
*/
|
|
75
|
-
static validateContent(content, emptyBlockCount = 3
|
|
74
|
+
static validateContent(content, emptyBlockCount = 3) {
|
|
76
75
|
if (typeof content === "string") {
|
|
77
76
|
if (content.trim() === "") {
|
|
78
|
-
return this.createEmptyBlocks(emptyBlockCount
|
|
77
|
+
return this.createEmptyBlocks(emptyBlockCount);
|
|
79
78
|
}
|
|
80
79
|
const parsedContent = this.parseJSONContent(content);
|
|
81
80
|
if (parsedContent && parsedContent.length > 0) {
|
|
82
81
|
return parsedContent;
|
|
83
82
|
}
|
|
84
|
-
return this.createEmptyBlocks(emptyBlockCount
|
|
83
|
+
return this.createEmptyBlocks(emptyBlockCount);
|
|
85
84
|
}
|
|
86
85
|
if (!content || content.length === 0) {
|
|
87
|
-
return this.createEmptyBlocks(emptyBlockCount
|
|
86
|
+
return this.createEmptyBlocks(emptyBlockCount);
|
|
88
87
|
}
|
|
89
88
|
return content;
|
|
90
89
|
}
|
|
91
90
|
/**
|
|
92
91
|
* 빈 블록들을 생성합니다
|
|
93
92
|
* @param emptyBlockCount 생성할 블록 개수
|
|
94
|
-
* @param placeholder 첫 번째 블록의 placeholder 텍스트
|
|
95
93
|
* @returns 생성된 빈 블록 배열
|
|
96
94
|
*/
|
|
97
|
-
static createEmptyBlocks(emptyBlockCount
|
|
98
|
-
return Array.from(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
return block;
|
|
104
|
-
});
|
|
95
|
+
static createEmptyBlocks(emptyBlockCount) {
|
|
96
|
+
return Array.from(
|
|
97
|
+
{ length: emptyBlockCount },
|
|
98
|
+
() => this.createDefaultBlock()
|
|
99
|
+
);
|
|
105
100
|
}
|
|
106
101
|
};
|
|
107
102
|
var EditorConfig = class {
|
|
@@ -131,18 +126,23 @@ var EditorConfig = class {
|
|
|
131
126
|
* @param userExtensions 사용자 정의 비활성 확장
|
|
132
127
|
* @param allowVideo 비디오 업로드 허용 여부
|
|
133
128
|
* @param allowAudio 오디오 업로드 허용 여부
|
|
129
|
+
* @param allowFile 일반 파일 업로드 허용 여부
|
|
134
130
|
* @returns 비활성화할 확장 기능 목록
|
|
135
131
|
*/
|
|
136
|
-
static getDisabledExtensions(userExtensions, allowVideo = false, allowAudio = false) {
|
|
132
|
+
static getDisabledExtensions(userExtensions, allowVideo = false, allowAudio = false, allowFile = false) {
|
|
137
133
|
const set = new Set(userExtensions ?? []);
|
|
138
134
|
if (!allowVideo) set.add("video");
|
|
139
135
|
if (!allowAudio) set.add("audio");
|
|
136
|
+
if (!allowFile) set.add("file");
|
|
140
137
|
return Array.from(set);
|
|
141
138
|
}
|
|
142
139
|
};
|
|
143
140
|
var createObjectUrlUploader = async (file) => {
|
|
144
141
|
return URL.createObjectURL(file);
|
|
145
142
|
};
|
|
143
|
+
var isImageFile = (file) => {
|
|
144
|
+
return file.size > 0 && (file.type?.startsWith("image/") || !file.type && /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(file.name || ""));
|
|
145
|
+
};
|
|
146
146
|
var fileToBase64 = async (file) => await new Promise((resolve, reject) => {
|
|
147
147
|
const reader = new FileReader();
|
|
148
148
|
reader.onload = () => resolve(String(reader.result));
|
|
@@ -153,15 +153,12 @@ function LumirEditor({
|
|
|
153
153
|
// editor options
|
|
154
154
|
initialContent,
|
|
155
155
|
initialEmptyBlocks = 3,
|
|
156
|
-
placeholder,
|
|
157
156
|
uploadFile,
|
|
158
|
-
pasteHandler,
|
|
159
157
|
tables,
|
|
160
158
|
heading,
|
|
161
159
|
animations = true,
|
|
162
160
|
defaultStyles = true,
|
|
163
161
|
disableExtensions,
|
|
164
|
-
domAttributes,
|
|
165
162
|
tabBehavior = "prefer-navigate-ui",
|
|
166
163
|
trailingBlock = true,
|
|
167
164
|
resolveFileUrl,
|
|
@@ -179,42 +176,50 @@ function LumirEditor({
|
|
|
179
176
|
emojiPicker = true,
|
|
180
177
|
filePanel = true,
|
|
181
178
|
tableHandles = true,
|
|
182
|
-
comments = true,
|
|
183
179
|
onSelectionChange,
|
|
184
180
|
className = "",
|
|
185
|
-
|
|
186
|
-
sideMenuAddButton = true,
|
|
181
|
+
sideMenuAddButton = false,
|
|
187
182
|
// callbacks / refs
|
|
188
|
-
onContentChange
|
|
189
|
-
editorRef
|
|
183
|
+
onContentChange
|
|
190
184
|
}) {
|
|
191
185
|
const validatedContent = useMemo(() => {
|
|
192
|
-
return ContentUtils.validateContent(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
186
|
+
return ContentUtils.validateContent(initialContent, initialEmptyBlocks);
|
|
187
|
+
}, [initialContent, initialEmptyBlocks]);
|
|
188
|
+
const tableConfig = useMemo(() => {
|
|
189
|
+
return EditorConfig.getDefaultTableConfig(tables);
|
|
190
|
+
}, [
|
|
191
|
+
tables?.splitCells,
|
|
192
|
+
tables?.cellBackgroundColor,
|
|
193
|
+
tables?.cellTextColor,
|
|
194
|
+
tables?.headers
|
|
195
|
+
]);
|
|
196
|
+
const headingConfig = useMemo(() => {
|
|
197
|
+
return EditorConfig.getDefaultHeadingConfig(heading);
|
|
198
|
+
}, [heading?.levels?.join(",") ?? ""]);
|
|
199
|
+
const disabledExtensions = useMemo(() => {
|
|
200
|
+
return EditorConfig.getDisabledExtensions(
|
|
201
|
+
disableExtensions,
|
|
202
|
+
allowVideoUpload,
|
|
203
|
+
allowAudioUpload,
|
|
204
|
+
allowFileUpload
|
|
196
205
|
);
|
|
197
|
-
}, [
|
|
206
|
+
}, [disableExtensions, allowVideoUpload, allowAudioUpload, allowFileUpload]);
|
|
198
207
|
const editor = useCreateBlockNote(
|
|
199
208
|
{
|
|
200
209
|
initialContent: validatedContent,
|
|
201
|
-
tables:
|
|
202
|
-
heading:
|
|
210
|
+
tables: tableConfig,
|
|
211
|
+
heading: headingConfig,
|
|
203
212
|
animations,
|
|
204
213
|
defaultStyles,
|
|
205
|
-
// 확장 비활성:
|
|
206
|
-
disableExtensions:
|
|
207
|
-
return EditorConfig.getDisabledExtensions(
|
|
208
|
-
disableExtensions,
|
|
209
|
-
allowVideoUpload,
|
|
210
|
-
allowAudioUpload
|
|
211
|
-
);
|
|
212
|
-
}, [disableExtensions, allowVideoUpload, allowAudioUpload]),
|
|
213
|
-
domAttributes,
|
|
214
|
+
// 확장 비활성: 비디오/오디오/파일 제어
|
|
215
|
+
disableExtensions: disabledExtensions,
|
|
214
216
|
tabBehavior,
|
|
215
217
|
trailingBlock,
|
|
216
218
|
resolveFileUrl,
|
|
217
219
|
uploadFile: async (file) => {
|
|
220
|
+
if (!isImageFile(file)) {
|
|
221
|
+
throw new Error("Only image files are allowed");
|
|
222
|
+
}
|
|
218
223
|
const custom = uploadFile;
|
|
219
224
|
const fallback = storeImagesAsBase64 ? fileToBase64 : createObjectUrlUploader;
|
|
220
225
|
try {
|
|
@@ -232,20 +237,19 @@ function LumirEditor({
|
|
|
232
237
|
const { event, editor: editor2, defaultPasteHandler } = ctx;
|
|
233
238
|
const fileList = event?.clipboardData?.files ?? null;
|
|
234
239
|
const files = fileList ? Array.from(fileList) : [];
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
);
|
|
238
|
-
if (files.length > 0 && accepted.length === 0) {
|
|
240
|
+
const acceptedFiles = files.filter(isImageFile);
|
|
241
|
+
if (files.length > 0 && acceptedFiles.length === 0) {
|
|
239
242
|
event.preventDefault();
|
|
240
243
|
return true;
|
|
241
244
|
}
|
|
242
|
-
if (
|
|
245
|
+
if (acceptedFiles.length === 0) {
|
|
246
|
+
return defaultPasteHandler() ?? false;
|
|
247
|
+
}
|
|
243
248
|
event.preventDefault();
|
|
244
249
|
(async () => {
|
|
245
|
-
|
|
246
|
-
for (const file of accepted) {
|
|
250
|
+
for (const file of acceptedFiles) {
|
|
247
251
|
try {
|
|
248
|
-
const url = await
|
|
252
|
+
const url = await editor2.uploadFile(file);
|
|
249
253
|
editor2.pasteHTML(`<img src="${url}" alt="image" />`);
|
|
250
254
|
} catch (err) {
|
|
251
255
|
console.warn(
|
|
@@ -253,7 +257,6 @@ function LumirEditor({
|
|
|
253
257
|
file.name || "",
|
|
254
258
|
err
|
|
255
259
|
);
|
|
256
|
-
continue;
|
|
257
260
|
}
|
|
258
261
|
}
|
|
259
262
|
})();
|
|
@@ -261,58 +264,32 @@ function LumirEditor({
|
|
|
261
264
|
}
|
|
262
265
|
},
|
|
263
266
|
[
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
allowVideoUpload,
|
|
268
|
-
allowAudioUpload,
|
|
269
|
-
allowFileUpload,
|
|
270
|
-
tables?.splitCells,
|
|
271
|
-
tables?.cellBackgroundColor,
|
|
272
|
-
tables?.cellTextColor,
|
|
273
|
-
tables?.headers,
|
|
274
|
-
heading?.levels?.join(","),
|
|
267
|
+
validatedContent,
|
|
268
|
+
tableConfig,
|
|
269
|
+
headingConfig,
|
|
275
270
|
animations,
|
|
276
271
|
defaultStyles,
|
|
277
|
-
|
|
278
|
-
domAttributes ? JSON.stringify(domAttributes) : void 0,
|
|
272
|
+
disabledExtensions,
|
|
279
273
|
tabBehavior,
|
|
280
274
|
trailingBlock,
|
|
281
|
-
resolveFileUrl
|
|
275
|
+
resolveFileUrl,
|
|
276
|
+
uploadFile,
|
|
277
|
+
storeImagesAsBase64
|
|
282
278
|
]
|
|
283
279
|
);
|
|
284
280
|
useEffect(() => {
|
|
285
|
-
if (
|
|
286
|
-
|
|
287
|
-
const el = editor.domElement;
|
|
288
|
-
if (!editable) {
|
|
289
|
-
if (el) {
|
|
290
|
-
el.style.userSelect = "text";
|
|
291
|
-
el.style.webkitUserSelect = "text";
|
|
292
|
-
}
|
|
281
|
+
if (editor) {
|
|
282
|
+
editor.isEditable = editable;
|
|
293
283
|
}
|
|
294
284
|
}, [editor, editable]);
|
|
295
285
|
useEffect(() => {
|
|
296
286
|
if (!editor || !onContentChange) return;
|
|
297
|
-
let lastContent = "";
|
|
298
287
|
const handleContentChange = () => {
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
if (lastContent === currentContent) return;
|
|
302
|
-
lastContent = currentContent;
|
|
303
|
-
onContentChange(topLevelBlocks);
|
|
304
|
-
};
|
|
305
|
-
editor.onEditorContentChange(handleContentChange);
|
|
306
|
-
return () => {
|
|
288
|
+
const blocks = editor.topLevelBlocks;
|
|
289
|
+
onContentChange(blocks);
|
|
307
290
|
};
|
|
291
|
+
return editor.onEditorContentChange(handleContentChange);
|
|
308
292
|
}, [editor, onContentChange]);
|
|
309
|
-
useEffect(() => {
|
|
310
|
-
if (!editorRef) return;
|
|
311
|
-
editorRef.current = editor ?? null;
|
|
312
|
-
return () => {
|
|
313
|
-
if (editorRef) editorRef.current = null;
|
|
314
|
-
};
|
|
315
|
-
}, [editor, editorRef]);
|
|
316
293
|
useEffect(() => {
|
|
317
294
|
const el = editor?.domElement;
|
|
318
295
|
if (!el) return;
|
|
@@ -322,9 +299,6 @@ function LumirEditor({
|
|
|
322
299
|
if (hasFiles) {
|
|
323
300
|
e.preventDefault();
|
|
324
301
|
e.stopPropagation();
|
|
325
|
-
if (typeof e.stopImmediatePropagation === "function") {
|
|
326
|
-
e.stopImmediatePropagation();
|
|
327
|
-
}
|
|
328
302
|
}
|
|
329
303
|
};
|
|
330
304
|
const handleDrop = (e) => {
|
|
@@ -333,22 +307,21 @@ function LumirEditor({
|
|
|
333
307
|
if (!hasFiles) return;
|
|
334
308
|
e.preventDefault();
|
|
335
309
|
e.stopPropagation();
|
|
336
|
-
e.stopImmediatePropagation?.();
|
|
337
310
|
const items = Array.from(e.dataTransfer.items ?? []);
|
|
338
311
|
const files = items.filter((it) => it.kind === "file").map((it) => it.getAsFile()).filter((f) => !!f);
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
);
|
|
342
|
-
if (accepted.length === 0) return;
|
|
312
|
+
const acceptedFiles = files.filter(isImageFile);
|
|
313
|
+
if (acceptedFiles.length === 0) return;
|
|
343
314
|
(async () => {
|
|
344
|
-
|
|
345
|
-
for (const f of accepted) {
|
|
315
|
+
for (const file of acceptedFiles) {
|
|
346
316
|
try {
|
|
347
|
-
|
|
348
|
-
|
|
317
|
+
if (editor?.uploadFile) {
|
|
318
|
+
const url = await editor.uploadFile(file);
|
|
319
|
+
if (url) {
|
|
320
|
+
editor.pasteHTML(`<img src="${url}" alt="image" />`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
349
323
|
} catch (err) {
|
|
350
|
-
console.warn("Image upload failed, skipped:",
|
|
351
|
-
continue;
|
|
324
|
+
console.warn("Image upload failed, skipped:", file.name || "", err);
|
|
352
325
|
}
|
|
353
326
|
}
|
|
354
327
|
})();
|
|
@@ -361,24 +334,16 @@ function LumirEditor({
|
|
|
361
334
|
});
|
|
362
335
|
el.removeEventListener("drop", handleDrop, { capture: true });
|
|
363
336
|
};
|
|
364
|
-
}, [
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
]);
|
|
371
|
-
|
|
372
|
-
const DragHandleOnlySideMenu = (props) => {
|
|
373
|
-
return /* @__PURE__ */ jsx(BlockSideMenu, { ...props, children: /* @__PURE__ */ jsx(DragHandleButton, { ...props }) });
|
|
374
|
-
};
|
|
375
|
-
return /* @__PURE__ */ jsxs(
|
|
337
|
+
}, [editor]);
|
|
338
|
+
const computedSideMenu = useMemo(() => {
|
|
339
|
+
return sideMenuAddButton ? sideMenu : false;
|
|
340
|
+
}, [sideMenuAddButton, sideMenu]);
|
|
341
|
+
const DragHandleOnlySideMenu = useMemo(() => {
|
|
342
|
+
return (props) => /* @__PURE__ */ jsx(BlockSideMenu, { ...props, children: /* @__PURE__ */ jsx(DragHandleButton, { ...props }) });
|
|
343
|
+
}, []);
|
|
344
|
+
return /* @__PURE__ */ jsx("div", { className: cn("lumirEditor", className), children: /* @__PURE__ */ jsxs(
|
|
376
345
|
BlockNoteView,
|
|
377
346
|
{
|
|
378
|
-
className: cn(
|
|
379
|
-
includeDefaultStyles && 'lumirEditor w-full h-full min-w-[300px] overflow-auto rounded-md border border-gray-300 focus-within:ring-2 focus-within:ring-black [&_.bn-editor]:px-[12px] [&_[data-content-type="paragraph"]]:text-[14px] bg-white',
|
|
380
|
-
className
|
|
381
|
-
),
|
|
382
347
|
editor,
|
|
383
348
|
editable,
|
|
384
349
|
theme,
|
|
@@ -389,34 +354,39 @@ function LumirEditor({
|
|
|
389
354
|
emojiPicker,
|
|
390
355
|
filePanel,
|
|
391
356
|
tableHandles,
|
|
392
|
-
comments,
|
|
393
357
|
onSelectionChange,
|
|
394
358
|
children: [
|
|
395
|
-
/* @__PURE__ */ jsx(
|
|
359
|
+
slashMenu && /* @__PURE__ */ jsx(
|
|
396
360
|
SuggestionMenuController,
|
|
397
361
|
{
|
|
398
362
|
triggerCharacter: "/",
|
|
399
|
-
getItems:
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
)
|
|
412
|
-
|
|
413
|
-
|
|
363
|
+
getItems: useCallback(
|
|
364
|
+
async (query) => {
|
|
365
|
+
const items = getDefaultReactSlashMenuItems(editor);
|
|
366
|
+
const filtered = items.filter((item) => {
|
|
367
|
+
const key = (item?.key || "").toString().toLowerCase();
|
|
368
|
+
const title = (item?.title || "").toString().toLowerCase();
|
|
369
|
+
if (["video", "audio", "file"].includes(key)) return false;
|
|
370
|
+
if (title.includes("video") || title.includes("audio") || title.includes("file"))
|
|
371
|
+
return false;
|
|
372
|
+
return true;
|
|
373
|
+
});
|
|
374
|
+
if (!query) return filtered;
|
|
375
|
+
const q = query.toLowerCase();
|
|
376
|
+
return filtered.filter(
|
|
377
|
+
(item) => item.title?.toLowerCase().includes(q) || (item.aliases || []).some(
|
|
378
|
+
(a) => a.toLowerCase().includes(q)
|
|
379
|
+
)
|
|
380
|
+
);
|
|
381
|
+
},
|
|
382
|
+
[editor]
|
|
383
|
+
)
|
|
414
384
|
}
|
|
415
385
|
),
|
|
416
386
|
!sideMenuAddButton && /* @__PURE__ */ jsx(SideMenuController, { sideMenu: DragHandleOnlySideMenu })
|
|
417
387
|
]
|
|
418
388
|
}
|
|
419
|
-
);
|
|
389
|
+
) });
|
|
420
390
|
}
|
|
421
391
|
export {
|
|
422
392
|
ContentUtils,
|