1- import { Apps } from "@mui/icons-material" ;
2- import { Button , MenuItem , Select , Stack , Typography } from "@mui/material" ;
1+ import { AddPhotoAlternate , Apps } from "@mui/icons-material" ;
2+ import { Button , CircularProgress , MenuItem , Select , Stack , Tab , Tabs , TextField , Typography } from "@mui/material" ;
3+ import { Grid } from "@mui/system" ;
4+ import { Suspense } from "@suspensive/react" ;
35import MDEditor , { GroupOptions , ICommand , commands } from "@uiw/react-md-editor" ;
46import type { MDXComponents } from "mdx/types" ;
57import * as React from "react" ;
8+ import * as R from "remeda" ;
69// import * as CryptoJS from "crypto-js";
710
811import Hooks from "../hooks" ;
@@ -65,6 +68,135 @@ const TextEditorStyle: React.CSSProperties = {
6568// );
6669// }
6770
71+ const insertText = ( newText : string , getState : ( ) => false | commands . TextState , textApi : commands . TextAreaTextApi ) => {
72+ const state = getState ( ) ;
73+ if ( ! state ) return undefined ;
74+
75+ if ( state . selectedText ) {
76+ newText += `\n${ state . selectedText } ` ;
77+ if ( state . selection . start - 1 !== - 1 && state . text [ state . selection . start - 1 ] !== "\n" ) newText = `\n${ newText } ` ;
78+ } else {
79+ if ( state . selection . start - 1 !== - 1 && state . text [ state . selection . start - 1 ] !== "\n" ) newText = `\n${ newText } ` ;
80+ if ( state . selection . end !== state . text . length && state . text [ state . selection . end ] !== "\n" ) newText += "\n" ;
81+ }
82+
83+ textApi . replaceSelection ( newText ) ;
84+ } ;
85+
86+ type ImageSelectorWidgetStateType = {
87+ tab : number ;
88+ selectedImageUrl ?: string ;
89+ } ;
90+
91+ type PublicFileType = {
92+ id : string ;
93+ file : string ;
94+ mimetype : string ;
95+ } ;
96+
97+ const ImageSelector : GroupOptions [ "children" ] = Suspense . with (
98+ { fallback : < CircularProgress /> } ,
99+ ( { close, getState, textApi } ) => {
100+ const urlInputRef = React . useRef < HTMLInputElement > ( null ) ;
101+ const backendAdminAPIClient = Hooks . BackendAdminAPI . useBackendAdminClient ( ) ;
102+ const { data } = Hooks . BackendAdminAPI . useListQuery < PublicFileType > ( backendAdminAPIClient , "file" , "publicfile" ) ;
103+ const [ widgetState , setWidgetState ] = React . useState < ImageSelectorWidgetStateType > ( { tab : 0 } ) ;
104+ const setTab = ( _ : React . SyntheticEvent , tab : number ) => setWidgetState ( ( ps ) => ( { ...ps , tab } ) ) ;
105+ const setImageUrl = ( selectedImageUrl ?: string ) => setWidgetState ( ( ps ) => ( { ...ps , selectedImageUrl } ) ) ;
106+
107+ const insertImage = ( inputStr : string ) => {
108+ console . log ( textApi , getState ) ;
109+ if ( ! textApi || ! getState ) return undefined ;
110+ insertText ( inputStr , getState , textApi ) ;
111+ setImageUrl ( ) ;
112+ close ( ) ;
113+ } ;
114+ const getSelectedUrl = ( ) : string | undefined => {
115+ if (
116+ widgetState . tab === 0 &&
117+ R . isString ( widgetState . selectedImageUrl ) &&
118+ widgetState . selectedImageUrl . trim ( ) !== ""
119+ ) {
120+ return widgetState . selectedImageUrl . trim ( ) ;
121+ } else if (
122+ widgetState . tab === 1 &&
123+ urlInputRef . current &&
124+ urlInputRef . current . checkValidity ( ) &&
125+ urlInputRef . current . value . trim ( ) !== ""
126+ ) {
127+ return urlInputRef . current . value . trim ( ) ;
128+ }
129+
130+ if ( widgetState . tab === 0 ) alert ( "사진을 선택해주세요." ) ;
131+ if ( widgetState . tab === 1 ) urlInputRef . current ?. reportValidity ( ) ;
132+
133+ return undefined ;
134+ } ;
135+ const onHTMLInsertBtnClick = ( ) => {
136+ const url = getSelectedUrl ( ) ;
137+ if ( R . isString ( url ) ) insertImage ( `<img src="${ url } " alt="이미지 설명" />` ) ;
138+ } ;
139+ const onMarkdownInsertBtnClick = ( ) => {
140+ const url = getSelectedUrl ( ) ;
141+ if ( R . isString ( url ) ) insertImage ( `` ) ;
142+ } ;
143+
144+ return (
145+ < Stack spacing = { 1 } sx = { { p : 1 , flexGrow : 1 , minWidth : 200 , maxHeight : "50rem" } } >
146+ < Tabs value = { widgetState . tab } onChange = { setTab } scrollButtons = { false } >
147+ < Tab wrapped label = "업로드 된 사진 중 선택" />
148+ < Tab wrapped label = "사진 URL 직접 입력" />
149+ </ Tabs >
150+ { widgetState . tab === 0 && (
151+ < >
152+ < Typography variant = "subtitle1" color = "text.secondary" >
153+ 업로드 된 사진 중 선택
154+ </ Typography >
155+ < Grid >
156+ { data
157+ . filter ( ( item ) => item . mimetype . startsWith ( "image/" ) )
158+ . map ( ( item ) => ( { ...item , file : item . file . split ( "?" ) [ 0 ] } ) ) // Remove query parameters if any
159+ . map ( ( item ) => {
160+ const selected = widgetState . selectedImageUrl === item . file ;
161+ return (
162+ < Button
163+ variant = "outlined"
164+ size = "small"
165+ onClick = { ( ) => setImageUrl ( item . file ) }
166+ sx = { {
167+ border : `1px solid ${ selected ? "primary.main" : "grey.400" } ` ,
168+ backgroundColor : selected ? "primary.main" : "transparent" ,
169+ } }
170+ >
171+ < img src = { item . file } alt = "이미지 미리보기" style = { { maxWidth : 100 , maxHeight : 100 } } />
172+ </ Button >
173+ ) ;
174+ } ) }
175+ </ Grid >
176+ </ >
177+ ) }
178+ { widgetState . tab === 1 && (
179+ < >
180+ < Typography variant = "subtitle1" color = "text.secondary" >
181+ 사진 URL 직접 입력
182+ </ Typography >
183+ < TextField label = "사진 URL" size = "small" type = "url" fullWidth required inputRef = { urlInputRef } />
184+ </ >
185+ ) }
186+ < Button size = "small" variant = "contained" onClick = { onHTMLInsertBtnClick } >
187+ HTML로 삽입
188+ </ Button >
189+ < Button size = "small" variant = "contained" onClick = { onMarkdownInsertBtnClick } >
190+ 마크다운으로 삽입
191+ </ Button >
192+ < Button size = "small" variant = "outlined" sx = { { flexGrow : 1 } } onClick = { close } >
193+ 닫기
194+ </ Button >
195+ </ Stack >
196+ ) ;
197+ }
198+ ) ;
199+
68200const getCustomComponentSelector : ( registeredComponentList : CustomComponentInfoType [ ] ) => GroupOptions [ "children" ] =
69201 ( registeredComponentList ) =>
70202 ( { close, getState, textApi } ) => {
@@ -73,24 +205,10 @@ const getCustomComponentSelector: (registeredComponentList: CustomComponentInfoT
73205 const onInsertBtnClick = ( ) => {
74206 if ( ! textApi || ! getState || ! registeredComponentList ?. length || ! componentSelectorRef . current ) return undefined ;
75207
76- const state = getState ( ) ;
77- if ( ! state ) return undefined ;
78-
79208 const selectedComponentData = registeredComponentList . find ( ( { k } ) => k === componentSelectorRef ?. current ?. value ) ;
80209 if ( ! selectedComponentData ) return undefined ;
81210
82- let newText = `<${ selectedComponentData . k } />` ;
83- if ( state . selectedText ) {
84- newText += `\n${ state . selectedText } ` ;
85- if ( state . selection . start - 1 !== - 1 && state . text [ state . selection . start - 1 ] !== "\n" )
86- newText = `\n${ newText } ` ;
87- } else {
88- if ( state . selection . start - 1 !== - 1 && state . text [ state . selection . start - 1 ] !== "\n" )
89- newText = `\n${ newText } ` ;
90- if ( state . selection . end !== state . text . length && state . text [ state . selection . end ] !== "\n" ) newText += "\n" ;
91- }
92-
93- textApi . replaceSelection ( newText ) ;
211+ insertText ( `<${ selectedComponentData . k } />` , getState , textApi ) ;
94212 close ( ) ;
95213 } ;
96214
@@ -158,7 +276,6 @@ export const MDXEditor: React.FC<MDXEditorProps> = ({ disabled, defaultValue, on
158276 commands . quote ,
159277 commands . codeBlock ,
160278 commands . hr ,
161- commands . image ,
162279 commands . divider ,
163280 commands . unorderedListCommand ,
164281 commands . orderedListCommand ,
@@ -170,6 +287,13 @@ export const MDXEditor: React.FC<MDXEditorProps> = ({ disabled, defaultValue, on
170287 children : getCustomComponentSelector ( registeredComponentList ) ,
171288 buttonProps : { "aria-label" : "Insert custom component" } ,
172289 } ) ,
290+ commands . group ( [ ] , {
291+ name : "image selector" ,
292+ groupName : "image selector" ,
293+ icon : < AddPhotoAlternate style = { { fontSize : 12 } } /> ,
294+ children : ( props ) => < ImageSelector { ...props } /> ,
295+ buttonProps : { "aria-label" : "Insert image" } ,
296+ } ) ,
173297 ] }
174298 extraCommands = { extraCommands }
175299 style = { TextEditorStyle }
0 commit comments