@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/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, placeholder) {
74
+ static validateContent(content, emptyBlockCount = 3) {
76
75
  if (typeof content === "string") {
77
76
  if (content.trim() === "") {
78
- return this.createEmptyBlocks(emptyBlockCount, placeholder);
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, placeholder);
83
+ return this.createEmptyBlocks(emptyBlockCount);
85
84
  }
86
85
  if (!content || content.length === 0) {
87
- return this.createEmptyBlocks(emptyBlockCount, placeholder);
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, placeholder) {
98
- return Array.from({ length: emptyBlockCount }, (_, index) => {
99
- const block = this.createDefaultBlock();
100
- if (index === 0 && placeholder) {
101
- block.content = [{ type: "text", text: placeholder, styles: {} }];
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
- includeDefaultStyles = true,
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
- initialContent,
194
- initialEmptyBlocks,
195
- placeholder
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
- }, [initialContent, initialEmptyBlocks, placeholder]);
206
+ }, [disableExtensions, allowVideoUpload, allowAudioUpload, allowFileUpload]);
198
207
  const editor = useCreateBlockNote(
199
208
  {
200
209
  initialContent: validatedContent,
201
- tables: EditorConfig.getDefaultTableConfig(tables),
202
- heading: EditorConfig.getDefaultHeadingConfig(heading),
210
+ tables: tableConfig,
211
+ heading: headingConfig,
203
212
  animations,
204
213
  defaultStyles,
205
- // 확장 비활성: 비디오/오디오만 제어(파일 확장은 내부 드롭 로직 의존 → 비활성화하지 않음)
206
- disableExtensions: useMemo(() => {
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 accepted = files.filter(
236
- (f) => f.size > 0 && (f.type?.startsWith("image/") || !f.type && /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(f.name || ""))
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 (accepted.length === 0) return defaultPasteHandler() ?? false;
245
+ if (acceptedFiles.length === 0) {
246
+ return defaultPasteHandler() ?? false;
247
+ }
243
248
  event.preventDefault();
244
249
  (async () => {
245
- const doUpload = uploadFile ?? (storeImagesAsBase64 ? fileToBase64 : createObjectUrlUploader);
246
- for (const file of accepted) {
250
+ for (const file of acceptedFiles) {
247
251
  try {
248
- const url = await doUpload(file);
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
- uploadFile,
265
- pasteHandler,
266
- storeImagesAsBase64,
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
- disableExtensions?.join(","),
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 (!editor) return;
286
- editor.isEditable = editable;
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 topLevelBlocks = editor.topLevelBlocks;
300
- const currentContent = JSON.stringify(topLevelBlocks);
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 accepted = files.filter(
340
- (f) => f.size > 0 && (f.type?.startsWith("image/") || !f.type && /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(f.name || ""))
341
- );
342
- if (accepted.length === 0) return;
312
+ const acceptedFiles = files.filter(isImageFile);
313
+ if (acceptedFiles.length === 0) return;
343
314
  (async () => {
344
- const doUpload = uploadFile ?? (storeImagesAsBase64 ? fileToBase64 : createObjectUrlUploader);
345
- for (const f of accepted) {
315
+ for (const file of acceptedFiles) {
346
316
  try {
347
- const url = await doUpload(f);
348
- editor?.pasteHTML(`<img src="${url}" alt="image" />`);
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:", f.name || "", err);
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
- editor,
366
- uploadFile,
367
- storeImagesAsBase64,
368
- allowVideoUpload,
369
- allowAudioUpload
370
- ]);
371
- const computedSideMenu = sideMenuAddButton ? sideMenu : false;
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: async (query) => {
400
- const items = getDefaultReactSlashMenuItems(editor);
401
- const filtered = items.filter((it) => {
402
- const k = (it?.key || "").toString();
403
- if (["video", "audio", "file"].includes(k)) return false;
404
- return true;
405
- });
406
- if (!query) return filtered;
407
- const q = query.toLowerCase();
408
- return filtered.filter(
409
- (it) => (it.title || "").toLowerCase().includes(q) || (it.aliases || []).some(
410
- (a) => a.toLowerCase().includes(q)
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,