@tipp/ui-quill-editor 4.0.10 → 4.0.11
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/editor.cjs +62 -19
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.d.cts +6 -0
- package/dist/editor.d.ts +6 -0
- package/dist/editor.js +62 -19
- package/dist/editor.js.map +1 -1
- package/package.json +3 -3
- package/src/editor.tsx +67 -17
package/dist/editor.cjs
CHANGED
|
@@ -100,7 +100,13 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
100
100
|
height,
|
|
101
101
|
hideHeader,
|
|
102
102
|
hideFileAttachment,
|
|
103
|
-
hideFooter
|
|
103
|
+
hideFooter,
|
|
104
|
+
title: controlledTitle,
|
|
105
|
+
onChangeTitle,
|
|
106
|
+
content: controlledContent,
|
|
107
|
+
onChangeContent,
|
|
108
|
+
attachedFiles: controlledAttachedFiles,
|
|
109
|
+
onChangeAttachedFiles
|
|
104
110
|
} = _a, quillProps = __objRest(_a, [
|
|
105
111
|
"defaultAttachedFiles",
|
|
106
112
|
"defaultTitle",
|
|
@@ -116,19 +122,34 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
116
122
|
"height",
|
|
117
123
|
"hideHeader",
|
|
118
124
|
"hideFileAttachment",
|
|
119
|
-
"hideFooter"
|
|
125
|
+
"hideFooter",
|
|
126
|
+
"title",
|
|
127
|
+
"onChangeTitle",
|
|
128
|
+
"content",
|
|
129
|
+
"onChangeContent",
|
|
130
|
+
"attachedFiles",
|
|
131
|
+
"onChangeAttachedFiles"
|
|
120
132
|
]);
|
|
121
133
|
const defaultRef = (0, import_react.useRef)(null);
|
|
122
134
|
const editorRef = ref || defaultRef;
|
|
123
|
-
const
|
|
135
|
+
const isControlledTitle = controlledTitle !== void 0;
|
|
136
|
+
const isControlledContent = controlledContent !== void 0;
|
|
137
|
+
const isControlledAttachedFiles = controlledAttachedFiles !== void 0;
|
|
138
|
+
const [internalAttachedFiles, setInternalAttachedFiles] = (0, import_react.useState)(
|
|
124
139
|
defaultAttachedFiles || []
|
|
125
140
|
);
|
|
126
141
|
const [fileDeleteLoading, setFileDeleteLoading] = (0, import_react.useState)(/* @__PURE__ */ new Set());
|
|
127
|
-
const [
|
|
128
|
-
const [
|
|
142
|
+
const [internalTitle, setInternalTitle] = (0, import_react.useState)(defaultTitle || "");
|
|
143
|
+
const [internalContent, setInternalContent] = (0, import_react.useState)(defaultValue || "");
|
|
144
|
+
const title = isControlledTitle ? controlledTitle : internalTitle;
|
|
145
|
+
const content = isControlledContent ? controlledContent : internalContent;
|
|
146
|
+
const attachedFiles = isControlledAttachedFiles ? controlledAttachedFiles : internalAttachedFiles;
|
|
129
147
|
const handleOnChangeContent = (0, import_react.useCallback)((value) => {
|
|
130
|
-
|
|
131
|
-
|
|
148
|
+
if (!isControlledContent) {
|
|
149
|
+
setInternalContent(value);
|
|
150
|
+
}
|
|
151
|
+
onChangeContent == null ? void 0 : onChangeContent(value);
|
|
152
|
+
}, [isControlledContent, onChangeContent]);
|
|
132
153
|
const handleButtonClick = (0, import_react.useCallback)(() => {
|
|
133
154
|
let input = document.createElement("input");
|
|
134
155
|
input.type = "file";
|
|
@@ -142,20 +163,26 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
142
163
|
const fileName = file.name;
|
|
143
164
|
const attachment = yield uploadFile == null ? void 0 : uploadFile(file, `hr-notes/${fileName}`);
|
|
144
165
|
if (attachment) {
|
|
145
|
-
|
|
166
|
+
const newFiles = [...attachedFiles, attachment];
|
|
167
|
+
if (!isControlledAttachedFiles) {
|
|
168
|
+
setInternalAttachedFiles(newFiles);
|
|
169
|
+
}
|
|
170
|
+
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(newFiles);
|
|
146
171
|
}
|
|
147
172
|
input = null;
|
|
148
173
|
});
|
|
149
174
|
input.click();
|
|
150
|
-
}, [uploadFile]);
|
|
175
|
+
}, [uploadFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]);
|
|
151
176
|
const handleDeleteFile = (0, import_react.useCallback)(
|
|
152
177
|
(fileUrl) => __async(null, null, function* () {
|
|
153
178
|
try {
|
|
154
179
|
setFileDeleteLoading((p) => p.add(fileUrl));
|
|
155
180
|
yield deleteFile == null ? void 0 : deleteFile(fileUrl);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
181
|
+
const newFiles = attachedFiles.filter((item) => item.url !== fileUrl);
|
|
182
|
+
if (!isControlledAttachedFiles) {
|
|
183
|
+
setInternalAttachedFiles(newFiles);
|
|
184
|
+
}
|
|
185
|
+
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(newFiles);
|
|
159
186
|
} catch (err) {
|
|
160
187
|
import_ui.toast.error("\uD30C\uC77C \uC0AD\uC81C\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
|
|
161
188
|
} finally {
|
|
@@ -165,7 +192,7 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
165
192
|
});
|
|
166
193
|
}
|
|
167
194
|
}),
|
|
168
|
-
[deleteFile]
|
|
195
|
+
[deleteFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]
|
|
169
196
|
);
|
|
170
197
|
const renderAttachedFiles = (0, import_react.useCallback)(() => {
|
|
171
198
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Box, { width: "100%", children: attachedFiles.map((file) => {
|
|
@@ -202,13 +229,29 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
202
229
|
}) });
|
|
203
230
|
}, [attachedFiles, fileDeleteLoading, handleDeleteFile]);
|
|
204
231
|
const handleOnChangeTitle = (0, import_react.useCallback)((e) => {
|
|
205
|
-
|
|
206
|
-
|
|
232
|
+
const newTitle = e.target.value;
|
|
233
|
+
if (!isControlledTitle) {
|
|
234
|
+
setInternalTitle(newTitle);
|
|
235
|
+
}
|
|
236
|
+
onChangeTitle == null ? void 0 : onChangeTitle(newTitle);
|
|
237
|
+
}, [isControlledTitle, onChangeTitle]);
|
|
207
238
|
const clearEditorState = (0, import_react.useCallback)(() => {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
239
|
+
const emptyTitle = "";
|
|
240
|
+
const emptyContent = "";
|
|
241
|
+
const emptyFiles = [];
|
|
242
|
+
if (!isControlledTitle) {
|
|
243
|
+
setInternalTitle(emptyTitle);
|
|
244
|
+
}
|
|
245
|
+
if (!isControlledContent) {
|
|
246
|
+
setInternalContent(emptyContent);
|
|
247
|
+
}
|
|
248
|
+
if (!isControlledAttachedFiles) {
|
|
249
|
+
setInternalAttachedFiles(emptyFiles);
|
|
250
|
+
}
|
|
251
|
+
onChangeTitle == null ? void 0 : onChangeTitle(emptyTitle);
|
|
252
|
+
onChangeContent == null ? void 0 : onChangeContent(emptyContent);
|
|
253
|
+
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(emptyFiles);
|
|
254
|
+
}, [isControlledTitle, isControlledContent, isControlledAttachedFiles, onChangeTitle, onChangeContent, onChangeAttachedFiles]);
|
|
212
255
|
const handleSaveClick = (0, import_react.useCallback)(() => {
|
|
213
256
|
onClickSave == null ? void 0 : onClickSave({
|
|
214
257
|
title,
|
package/dist/editor.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/editor.tsx"],"sourcesContent":["import React, { forwardRef, useCallback, useRef, useState } from 'react';\nimport {\n Box,\n Button,\n Flex,\n Grid,\n Link,\n Separator,\n TextField,\n Typo,\n Link2Icon,\n toast,\n FileIcon,\n} from '@tipp/ui';\nimport ReactQuill from 'react-quill-new';\nimport type { Attachment } from './type';\n\nexport interface TippEditorProps extends ReactQuill.ReactQuillProps {\n defaultTitle?: string;\n defaultValue?: string;\n defaultAttachedFiles?: Attachment[];\n /** 저장하기 버튼 클릭 시 실행 */\n onClickSave?: (values: {\n title: string;\n content: string;\n files: Attachment[];\n }) => void;\n /** 파일 업로드 버튼 클릭 시 실행 */\n uploadFile?: (\n file: File,\n destination: string\n ) => Promise<Attachment | undefined>;\n deleteFile?: (fileUrl: string) => Promise<void>;\n /** 외부에서 Editor를 빈 상태로 초기화 시켜야 할 때 사용 */\n clearEditor?: React.MutableRefObject<(() => void) | undefined>;\n /** 초기화 버튼말고 다른 버튼 추가시 */\n SecondaryButton?: React.ReactNode;\n /** true인 경우 저장하기 버튼이 비활성 화 됨. 연타 방지 */\n isLoading?: boolean;\n minHeight?: string;\n maxHeight?: string;\n height?: string;\n /** 제목 입력창 숨김 */\n hideHeader?: boolean;\n /** 첨부 파일 버튼 숨김 */\n hideFileAttachment?: boolean;\n /** 저장 버튼 footer 숨김 */\n hideFooter?: boolean;\n}\n\nexport const Editor = forwardRef<ReactQuill, TippEditorProps>(\n (props, ref): React.ReactNode => {\n const {\n defaultAttachedFiles,\n defaultTitle,\n defaultValue,\n onClickSave,\n uploadFile,\n deleteFile,\n isLoading,\n SecondaryButton,\n clearEditor,\n minHeight,\n maxHeight,\n height,\n hideHeader,\n hideFileAttachment,\n hideFooter,\n ...quillProps\n } = props;\n const defaultRef = useRef<ReactQuill>(null);\n const editorRef = ref || defaultRef;\n const [attachedFiles, setAttachedFiles] = useState<Attachment[]>(\n defaultAttachedFiles || []\n );\n const [fileDeleteLoading, setFileDeleteLoading] = useState(new Set());\n\n const [title, setTitle] = useState(defaultTitle || '');\n const [content, setContent] = useState(defaultValue || '');\n\n const handleOnChangeContent = useCallback((value: string) => {\n setContent(value);\n }, []);\n\n const handleButtonClick = useCallback(() => {\n let input: HTMLInputElement | null = document.createElement('input');\n input.type = 'file';\n input.onchange = async (event) => {\n const file = (event.target as HTMLInputElement).files?.[0];\n if (!file) {\n // console.log('DEBUG: no file');\n toast.error('파일을 선택해주세요.');\n return;\n }\n\n const fileName = file.name;\n const attachment = await uploadFile?.(file, `hr-notes/${fileName}`);\n if (attachment) {\n setAttachedFiles((prev) => [...prev, attachment]);\n }\n input = null;\n };\n input.click();\n }, [uploadFile]);\n\n const handleDeleteFile = useCallback(\n async (fileUrl: string) => {\n try {\n setFileDeleteLoading((p) => p.add(fileUrl));\n await deleteFile?.(fileUrl);\n setAttachedFiles((currentFiles) =>\n currentFiles.filter((item) => item.url !== fileUrl)\n );\n } catch (err) {\n toast.error('파일 삭제에 실패했습니다.');\n } finally {\n setFileDeleteLoading((p) => {\n p.delete(fileUrl);\n return p;\n });\n }\n },\n [deleteFile]\n );\n\n const renderAttachedFiles = useCallback(() => {\n return (\n <Box width=\"100%\">\n {attachedFiles.map((file) => {\n return (\n <>\n <Separator size=\"4\" />\n <Flex\n align=\"center\"\n justify=\"between\"\n key={`${file.url}_${file.fileName}`}\n p=\"4\"\n width=\"100%\"\n >\n <Link href={file.url} size=\"2\">\n <Flex align=\"center\" gap=\"3\">\n <FileIcon />\n {file.fileName}\n </Flex>\n </Link>\n <Button\n loading={fileDeleteLoading.has(file.url)}\n onClick={() => {\n void handleDeleteFile(file.url);\n }}\n variant=\"ghost\"\n >\n 첨부 파일 삭제\n </Button>\n </Flex>\n </>\n );\n })}\n </Box>\n );\n }, [attachedFiles, fileDeleteLoading, handleDeleteFile]);\n\n const handleOnChangeTitle = useCallback<\n React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>\n >((e) => {\n setTitle(e.target.value);\n }, []);\n\n const clearEditorState = useCallback(() => {\n setContent('');\n setAttachedFiles([]);\n setTitle('');\n }, []);\n\n const handleSaveClick = useCallback(() => {\n onClickSave?.({\n title,\n content,\n files: attachedFiles,\n });\n }, [onClickSave, title, content, attachedFiles]);\n\n if (props.clearEditor) {\n props.clearEditor.current = clearEditorState;\n }\n\n const cssVariables = {\n '--max-height': maxHeight,\n '--min-height': minHeight,\n '--height': height || '100%',\n } as React.CSSProperties;\n\n return (\n <div\n className=\"tipp-ql-wrapper\"\n style={{\n ...cssVariables,\n }}\n >\n <Grid height=\"100%\" rows={`${hideHeader ? '' : 'auto 1px'} 1fr`}>\n {/* 제목 입력창 */}\n {hideHeader ? null : (\n <>\n <Grid\n align=\"center\"\n columns=\"auto auto 1fr\"\n gap=\"2\"\n height=\"42px\"\n pl=\"2\"\n pr=\"3\"\n width=\"100%\"\n >\n <Box pl=\"3\" pr=\"3\">\n <Typo>제목</Typo>\n </Box>\n <Separator orientation=\"vertical\" style={{ height: '100%' }} />\n <TextField.Root\n className=\"editor-title-text-field\"\n onChange={handleOnChangeTitle}\n placeholder=\"제목을 입력해주세요\"\n value={title}\n />\n </Grid>\n <Separator orientation=\"horizontal\" size=\"4\" />\n </>\n )}\n\n <ReactQuill\n className=\"tipp-ql-editor write-mode\"\n onChange={handleOnChangeContent}\n ref={editorRef}\n theme=\"snow\"\n value={content}\n {...quillProps}\n />\n {renderAttachedFiles()}\n </Grid>\n\n {hideFooter ? null : (\n <>\n <Separator size=\"4\" />\n <Flex\n align=\"center\"\n justify=\"between\"\n p=\"2\"\n pl=\"4\"\n pr=\"4\"\n width=\"100%\"\n >\n {hideFileAttachment ? (\n <div />\n ) : (\n <Button\n color=\"gray\"\n onClick={handleButtonClick}\n variant=\"transparent\"\n >\n <Link2Icon height={20} width={20} />\n </Button>\n )}\n\n <Flex gap=\"2\">\n {clearEditor ? (\n <Button\n color=\"gray\"\n onClick={clearEditorState}\n variant=\"outline\"\n >\n 초기화\n </Button>\n ) : null}\n {SecondaryButton ? SecondaryButton : null}\n <Button disabled={isLoading} onClick={handleSaveClick}>\n 저장\n </Button>\n </Flex>\n </Flex>\n </>\n )}\n </div>\n );\n }\n);\n\nEditor.displayName = 'TIPP-Quill-Editor';\n"],"mappings":"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","names":["_a","ReactQuill"]}
|
|
1
|
+
{"version":3,"sources":["../src/editor.tsx"],"sourcesContent":["import React, { forwardRef, useCallback, useRef, useState } from 'react';\nimport {\n Box,\n Button,\n Flex,\n Grid,\n Link,\n Separator,\n TextField,\n Typo,\n Link2Icon,\n toast,\n FileIcon,\n} from '@tipp/ui';\nimport ReactQuill from 'react-quill-new';\nimport type { Attachment } from './type';\n\nexport interface TippEditorProps extends ReactQuill.ReactQuillProps {\n defaultTitle?: string;\n defaultValue?: string;\n defaultAttachedFiles?: Attachment[];\n /** 저장하기 버튼 클릭 시 실행 */\n onClickSave?: (values: {\n title: string;\n content: string;\n files: Attachment[];\n }) => void;\n /** 파일 업로드 버튼 클릭 시 실행 */\n uploadFile?: (\n file: File,\n destination: string\n ) => Promise<Attachment | undefined>;\n deleteFile?: (fileUrl: string) => Promise<void>;\n /** 외부에서 Editor를 빈 상태로 초기화 시켜야 할 때 사용 */\n clearEditor?: React.MutableRefObject<(() => void) | undefined>;\n /** 초기화 버튼말고 다른 버튼 추가시 */\n SecondaryButton?: React.ReactNode;\n /** true인 경우 저장하기 버튼이 비활성 화 됨. 연타 방지 */\n isLoading?: boolean;\n minHeight?: string;\n maxHeight?: string;\n height?: string;\n /** 제목 입력창 숨김 */\n hideHeader?: boolean;\n /** 첨부 파일 버튼 숨김 */\n hideFileAttachment?: boolean;\n /** 저장 버튼 footer 숨김 */\n hideFooter?: boolean;\n title?: string;\n onChangeTitle?: (value: string) => void;\n content?: string;\n onChangeContent?: (value: string) => void;\n attachedFiles?: Attachment[];\n onChangeAttachedFiles?: (files: Attachment[]) => void;\n}\n\nexport const Editor = forwardRef<ReactQuill, TippEditorProps>(\n (props, ref): React.ReactNode => {\n const {\n defaultAttachedFiles,\n defaultTitle,\n defaultValue,\n onClickSave,\n uploadFile,\n deleteFile,\n isLoading,\n SecondaryButton,\n clearEditor,\n minHeight,\n maxHeight,\n height,\n hideHeader,\n hideFileAttachment,\n hideFooter,\n title: controlledTitle,\n onChangeTitle,\n content: controlledContent,\n onChangeContent,\n attachedFiles: controlledAttachedFiles,\n onChangeAttachedFiles,\n ...quillProps\n } = props;\n const defaultRef = useRef<ReactQuill>(null);\n const editorRef = ref || defaultRef;\n // Controlled vs Uncontrolled 모드 구분\n const isControlledTitle = controlledTitle !== undefined;\n const isControlledContent = controlledContent !== undefined;\n const isControlledAttachedFiles = controlledAttachedFiles !== undefined;\n\n const [internalAttachedFiles, setInternalAttachedFiles] = useState<Attachment[]>(\n defaultAttachedFiles || []\n );\n const [fileDeleteLoading, setFileDeleteLoading] = useState(new Set());\n\n const [internalTitle, setInternalTitle] = useState(defaultTitle || '');\n const [internalContent, setInternalContent] = useState(defaultValue || '');\n\n // 실제 사용할 값들 (controlled일 때는 props 값, uncontrolled일 때는 internal state 값)\n const title = isControlledTitle ? controlledTitle : internalTitle;\n const content = isControlledContent ? controlledContent : internalContent;\n const attachedFiles = isControlledAttachedFiles ? controlledAttachedFiles : internalAttachedFiles;\n\n const handleOnChangeContent = useCallback((value: string) => {\n if (!isControlledContent) {\n setInternalContent(value);\n }\n onChangeContent?.(value);\n }, [isControlledContent, onChangeContent]);\n\n const handleButtonClick = useCallback(() => {\n let input: HTMLInputElement | null = document.createElement('input');\n input.type = 'file';\n input.onchange = async (event) => {\n const file = (event.target as HTMLInputElement).files?.[0];\n if (!file) {\n // console.log('DEBUG: no file');\n toast.error('파일을 선택해주세요.');\n return;\n }\n\n const fileName = file.name;\n const attachment = await uploadFile?.(file, `hr-notes/${fileName}`);\n if (attachment) {\n const newFiles = [...attachedFiles, attachment];\n if (!isControlledAttachedFiles) {\n setInternalAttachedFiles(newFiles);\n }\n onChangeAttachedFiles?.(newFiles);\n }\n input = null;\n };\n input.click();\n }, [uploadFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]);\n\n const handleDeleteFile = useCallback(\n async (fileUrl: string) => {\n try {\n setFileDeleteLoading((p) => p.add(fileUrl));\n await deleteFile?.(fileUrl);\n const newFiles = attachedFiles.filter((item) => item.url !== fileUrl);\n if (!isControlledAttachedFiles) {\n setInternalAttachedFiles(newFiles);\n }\n onChangeAttachedFiles?.(newFiles);\n } catch (err) {\n toast.error('파일 삭제에 실패했습니다.');\n } finally {\n setFileDeleteLoading((p) => {\n p.delete(fileUrl);\n return p;\n });\n }\n },\n [deleteFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]\n );\n\n const renderAttachedFiles = useCallback(() => {\n return (\n <Box width=\"100%\">\n {attachedFiles.map((file) => {\n return (\n <>\n <Separator size=\"4\" />\n <Flex\n align=\"center\"\n justify=\"between\"\n key={`${file.url}_${file.fileName}`}\n p=\"4\"\n width=\"100%\"\n >\n <Link href={file.url} size=\"2\">\n <Flex align=\"center\" gap=\"3\">\n <FileIcon />\n {file.fileName}\n </Flex>\n </Link>\n <Button\n loading={fileDeleteLoading.has(file.url)}\n onClick={() => {\n void handleDeleteFile(file.url);\n }}\n variant=\"ghost\"\n >\n 첨부 파일 삭제\n </Button>\n </Flex>\n </>\n );\n })}\n </Box>\n );\n }, [attachedFiles, fileDeleteLoading, handleDeleteFile]);\n\n const handleOnChangeTitle = useCallback<\n React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>\n >((e) => {\n const newTitle = e.target.value;\n if (!isControlledTitle) {\n setInternalTitle(newTitle);\n }\n onChangeTitle?.(newTitle);\n }, [isControlledTitle, onChangeTitle]);\n\n const clearEditorState = useCallback(() => {\n const emptyTitle = '';\n const emptyContent = '';\n const emptyFiles: Attachment[] = [];\n\n if (!isControlledTitle) {\n setInternalTitle(emptyTitle);\n }\n if (!isControlledContent) {\n setInternalContent(emptyContent);\n }\n if (!isControlledAttachedFiles) {\n setInternalAttachedFiles(emptyFiles);\n }\n\n // controlled 모드일 때도 부모에게 알림\n onChangeTitle?.(emptyTitle);\n onChangeContent?.(emptyContent);\n onChangeAttachedFiles?.(emptyFiles);\n }, [isControlledTitle, isControlledContent, isControlledAttachedFiles, onChangeTitle, onChangeContent, onChangeAttachedFiles]);\n\n const handleSaveClick = useCallback(() => {\n onClickSave?.({\n title,\n content,\n files: attachedFiles,\n });\n }, [onClickSave, title, content, attachedFiles]);\n\n if (props.clearEditor) {\n props.clearEditor.current = clearEditorState;\n }\n\n const cssVariables = {\n '--max-height': maxHeight,\n '--min-height': minHeight,\n '--height': height || '100%',\n } as React.CSSProperties;\n\n return (\n <div\n className=\"tipp-ql-wrapper\"\n style={{\n ...cssVariables,\n }}\n >\n <Grid height=\"100%\" rows={`${hideHeader ? '' : 'auto 1px'} 1fr`}>\n {/* 제목 입력창 */}\n {hideHeader ? null : (\n <>\n <Grid\n align=\"center\"\n columns=\"auto auto 1fr\"\n gap=\"2\"\n height=\"42px\"\n pl=\"2\"\n pr=\"3\"\n width=\"100%\"\n >\n <Box pl=\"3\" pr=\"3\">\n <Typo>제목</Typo>\n </Box>\n <Separator orientation=\"vertical\" style={{ height: '100%' }} />\n <TextField.Root\n className=\"editor-title-text-field\"\n onChange={handleOnChangeTitle}\n placeholder=\"제목을 입력해주세요\"\n value={title}\n />\n </Grid>\n <Separator orientation=\"horizontal\" size=\"4\" />\n </>\n )}\n\n <ReactQuill\n className=\"tipp-ql-editor write-mode\"\n onChange={handleOnChangeContent}\n ref={editorRef}\n theme=\"snow\"\n value={content}\n {...quillProps}\n />\n {renderAttachedFiles()}\n </Grid>\n\n {hideFooter ? null : (\n <>\n <Separator size=\"4\" />\n <Flex\n align=\"center\"\n justify=\"between\"\n p=\"2\"\n pl=\"4\"\n pr=\"4\"\n width=\"100%\"\n >\n {hideFileAttachment ? (\n <div />\n ) : (\n <Button\n color=\"gray\"\n onClick={handleButtonClick}\n variant=\"transparent\"\n >\n <Link2Icon height={20} width={20} />\n </Button>\n )}\n\n <Flex gap=\"2\">\n {clearEditor ? (\n <Button\n color=\"gray\"\n onClick={clearEditorState}\n variant=\"outline\"\n >\n 초기화\n </Button>\n ) : null}\n {SecondaryButton ? SecondaryButton : null}\n <Button disabled={isLoading} onClick={handleSaveClick}>\n 저장\n </Button>\n </Flex>\n </Flex>\n </>\n )}\n </div>\n );\n }\n);\n\nEditor.displayName = 'TIPP-Quill-Editor';\n"],"mappings":"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","names":["_a","ReactQuill"]}
|
package/dist/editor.d.cts
CHANGED
|
@@ -30,6 +30,12 @@ interface TippEditorProps extends ReactQuill.ReactQuillProps {
|
|
|
30
30
|
hideFileAttachment?: boolean;
|
|
31
31
|
/** 저장 버튼 footer 숨김 */
|
|
32
32
|
hideFooter?: boolean;
|
|
33
|
+
title?: string;
|
|
34
|
+
onChangeTitle?: (value: string) => void;
|
|
35
|
+
content?: string;
|
|
36
|
+
onChangeContent?: (value: string) => void;
|
|
37
|
+
attachedFiles?: Attachment[];
|
|
38
|
+
onChangeAttachedFiles?: (files: Attachment[]) => void;
|
|
33
39
|
}
|
|
34
40
|
declare const Editor: React.ForwardRefExoticComponent<TippEditorProps & React.RefAttributes<ReactQuill>>;
|
|
35
41
|
|
package/dist/editor.d.ts
CHANGED
|
@@ -30,6 +30,12 @@ interface TippEditorProps extends ReactQuill.ReactQuillProps {
|
|
|
30
30
|
hideFileAttachment?: boolean;
|
|
31
31
|
/** 저장 버튼 footer 숨김 */
|
|
32
32
|
hideFooter?: boolean;
|
|
33
|
+
title?: string;
|
|
34
|
+
onChangeTitle?: (value: string) => void;
|
|
35
|
+
content?: string;
|
|
36
|
+
onChangeContent?: (value: string) => void;
|
|
37
|
+
attachedFiles?: Attachment[];
|
|
38
|
+
onChangeAttachedFiles?: (files: Attachment[]) => void;
|
|
33
39
|
}
|
|
34
40
|
declare const Editor: React.ForwardRefExoticComponent<TippEditorProps & React.RefAttributes<ReactQuill>>;
|
|
35
41
|
|
package/dist/editor.js
CHANGED
|
@@ -38,7 +38,13 @@ var Editor = forwardRef(
|
|
|
38
38
|
height,
|
|
39
39
|
hideHeader,
|
|
40
40
|
hideFileAttachment,
|
|
41
|
-
hideFooter
|
|
41
|
+
hideFooter,
|
|
42
|
+
title: controlledTitle,
|
|
43
|
+
onChangeTitle,
|
|
44
|
+
content: controlledContent,
|
|
45
|
+
onChangeContent,
|
|
46
|
+
attachedFiles: controlledAttachedFiles,
|
|
47
|
+
onChangeAttachedFiles
|
|
42
48
|
} = _a, quillProps = __objRest(_a, [
|
|
43
49
|
"defaultAttachedFiles",
|
|
44
50
|
"defaultTitle",
|
|
@@ -54,19 +60,34 @@ var Editor = forwardRef(
|
|
|
54
60
|
"height",
|
|
55
61
|
"hideHeader",
|
|
56
62
|
"hideFileAttachment",
|
|
57
|
-
"hideFooter"
|
|
63
|
+
"hideFooter",
|
|
64
|
+
"title",
|
|
65
|
+
"onChangeTitle",
|
|
66
|
+
"content",
|
|
67
|
+
"onChangeContent",
|
|
68
|
+
"attachedFiles",
|
|
69
|
+
"onChangeAttachedFiles"
|
|
58
70
|
]);
|
|
59
71
|
const defaultRef = useRef(null);
|
|
60
72
|
const editorRef = ref || defaultRef;
|
|
61
|
-
const
|
|
73
|
+
const isControlledTitle = controlledTitle !== void 0;
|
|
74
|
+
const isControlledContent = controlledContent !== void 0;
|
|
75
|
+
const isControlledAttachedFiles = controlledAttachedFiles !== void 0;
|
|
76
|
+
const [internalAttachedFiles, setInternalAttachedFiles] = useState(
|
|
62
77
|
defaultAttachedFiles || []
|
|
63
78
|
);
|
|
64
79
|
const [fileDeleteLoading, setFileDeleteLoading] = useState(/* @__PURE__ */ new Set());
|
|
65
|
-
const [
|
|
66
|
-
const [
|
|
80
|
+
const [internalTitle, setInternalTitle] = useState(defaultTitle || "");
|
|
81
|
+
const [internalContent, setInternalContent] = useState(defaultValue || "");
|
|
82
|
+
const title = isControlledTitle ? controlledTitle : internalTitle;
|
|
83
|
+
const content = isControlledContent ? controlledContent : internalContent;
|
|
84
|
+
const attachedFiles = isControlledAttachedFiles ? controlledAttachedFiles : internalAttachedFiles;
|
|
67
85
|
const handleOnChangeContent = useCallback((value) => {
|
|
68
|
-
|
|
69
|
-
|
|
86
|
+
if (!isControlledContent) {
|
|
87
|
+
setInternalContent(value);
|
|
88
|
+
}
|
|
89
|
+
onChangeContent == null ? void 0 : onChangeContent(value);
|
|
90
|
+
}, [isControlledContent, onChangeContent]);
|
|
70
91
|
const handleButtonClick = useCallback(() => {
|
|
71
92
|
let input = document.createElement("input");
|
|
72
93
|
input.type = "file";
|
|
@@ -80,20 +101,26 @@ var Editor = forwardRef(
|
|
|
80
101
|
const fileName = file.name;
|
|
81
102
|
const attachment = yield uploadFile == null ? void 0 : uploadFile(file, `hr-notes/${fileName}`);
|
|
82
103
|
if (attachment) {
|
|
83
|
-
|
|
104
|
+
const newFiles = [...attachedFiles, attachment];
|
|
105
|
+
if (!isControlledAttachedFiles) {
|
|
106
|
+
setInternalAttachedFiles(newFiles);
|
|
107
|
+
}
|
|
108
|
+
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(newFiles);
|
|
84
109
|
}
|
|
85
110
|
input = null;
|
|
86
111
|
});
|
|
87
112
|
input.click();
|
|
88
|
-
}, [uploadFile]);
|
|
113
|
+
}, [uploadFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]);
|
|
89
114
|
const handleDeleteFile = useCallback(
|
|
90
115
|
(fileUrl) => __async(null, null, function* () {
|
|
91
116
|
try {
|
|
92
117
|
setFileDeleteLoading((p) => p.add(fileUrl));
|
|
93
118
|
yield deleteFile == null ? void 0 : deleteFile(fileUrl);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
119
|
+
const newFiles = attachedFiles.filter((item) => item.url !== fileUrl);
|
|
120
|
+
if (!isControlledAttachedFiles) {
|
|
121
|
+
setInternalAttachedFiles(newFiles);
|
|
122
|
+
}
|
|
123
|
+
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(newFiles);
|
|
97
124
|
} catch (err) {
|
|
98
125
|
toast.error("\uD30C\uC77C \uC0AD\uC81C\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
|
|
99
126
|
} finally {
|
|
@@ -103,7 +130,7 @@ var Editor = forwardRef(
|
|
|
103
130
|
});
|
|
104
131
|
}
|
|
105
132
|
}),
|
|
106
|
-
[deleteFile]
|
|
133
|
+
[deleteFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]
|
|
107
134
|
);
|
|
108
135
|
const renderAttachedFiles = useCallback(() => {
|
|
109
136
|
return /* @__PURE__ */ jsx(Box, { width: "100%", children: attachedFiles.map((file) => {
|
|
@@ -140,13 +167,29 @@ var Editor = forwardRef(
|
|
|
140
167
|
}) });
|
|
141
168
|
}, [attachedFiles, fileDeleteLoading, handleDeleteFile]);
|
|
142
169
|
const handleOnChangeTitle = useCallback((e) => {
|
|
143
|
-
|
|
144
|
-
|
|
170
|
+
const newTitle = e.target.value;
|
|
171
|
+
if (!isControlledTitle) {
|
|
172
|
+
setInternalTitle(newTitle);
|
|
173
|
+
}
|
|
174
|
+
onChangeTitle == null ? void 0 : onChangeTitle(newTitle);
|
|
175
|
+
}, [isControlledTitle, onChangeTitle]);
|
|
145
176
|
const clearEditorState = useCallback(() => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
177
|
+
const emptyTitle = "";
|
|
178
|
+
const emptyContent = "";
|
|
179
|
+
const emptyFiles = [];
|
|
180
|
+
if (!isControlledTitle) {
|
|
181
|
+
setInternalTitle(emptyTitle);
|
|
182
|
+
}
|
|
183
|
+
if (!isControlledContent) {
|
|
184
|
+
setInternalContent(emptyContent);
|
|
185
|
+
}
|
|
186
|
+
if (!isControlledAttachedFiles) {
|
|
187
|
+
setInternalAttachedFiles(emptyFiles);
|
|
188
|
+
}
|
|
189
|
+
onChangeTitle == null ? void 0 : onChangeTitle(emptyTitle);
|
|
190
|
+
onChangeContent == null ? void 0 : onChangeContent(emptyContent);
|
|
191
|
+
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(emptyFiles);
|
|
192
|
+
}, [isControlledTitle, isControlledContent, isControlledAttachedFiles, onChangeTitle, onChangeContent, onChangeAttachedFiles]);
|
|
150
193
|
const handleSaveClick = useCallback(() => {
|
|
151
194
|
onClickSave == null ? void 0 : onClickSave({
|
|
152
195
|
title,
|
package/dist/editor.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/editor.tsx"],"sourcesContent":["import React, { forwardRef, useCallback, useRef, useState } from 'react';\nimport {\n Box,\n Button,\n Flex,\n Grid,\n Link,\n Separator,\n TextField,\n Typo,\n Link2Icon,\n toast,\n FileIcon,\n} from '@tipp/ui';\nimport ReactQuill from 'react-quill-new';\nimport type { Attachment } from './type';\n\nexport interface TippEditorProps extends ReactQuill.ReactQuillProps {\n defaultTitle?: string;\n defaultValue?: string;\n defaultAttachedFiles?: Attachment[];\n /** 저장하기 버튼 클릭 시 실행 */\n onClickSave?: (values: {\n title: string;\n content: string;\n files: Attachment[];\n }) => void;\n /** 파일 업로드 버튼 클릭 시 실행 */\n uploadFile?: (\n file: File,\n destination: string\n ) => Promise<Attachment | undefined>;\n deleteFile?: (fileUrl: string) => Promise<void>;\n /** 외부에서 Editor를 빈 상태로 초기화 시켜야 할 때 사용 */\n clearEditor?: React.MutableRefObject<(() => void) | undefined>;\n /** 초기화 버튼말고 다른 버튼 추가시 */\n SecondaryButton?: React.ReactNode;\n /** true인 경우 저장하기 버튼이 비활성 화 됨. 연타 방지 */\n isLoading?: boolean;\n minHeight?: string;\n maxHeight?: string;\n height?: string;\n /** 제목 입력창 숨김 */\n hideHeader?: boolean;\n /** 첨부 파일 버튼 숨김 */\n hideFileAttachment?: boolean;\n /** 저장 버튼 footer 숨김 */\n hideFooter?: boolean;\n}\n\nexport const Editor = forwardRef<ReactQuill, TippEditorProps>(\n (props, ref): React.ReactNode => {\n const {\n defaultAttachedFiles,\n defaultTitle,\n defaultValue,\n onClickSave,\n uploadFile,\n deleteFile,\n isLoading,\n SecondaryButton,\n clearEditor,\n minHeight,\n maxHeight,\n height,\n hideHeader,\n hideFileAttachment,\n hideFooter,\n ...quillProps\n } = props;\n const defaultRef = useRef<ReactQuill>(null);\n const editorRef = ref || defaultRef;\n const [attachedFiles, setAttachedFiles] = useState<Attachment[]>(\n defaultAttachedFiles || []\n );\n const [fileDeleteLoading, setFileDeleteLoading] = useState(new Set());\n\n const [title, setTitle] = useState(defaultTitle || '');\n const [content, setContent] = useState(defaultValue || '');\n\n const handleOnChangeContent = useCallback((value: string) => {\n setContent(value);\n }, []);\n\n const handleButtonClick = useCallback(() => {\n let input: HTMLInputElement | null = document.createElement('input');\n input.type = 'file';\n input.onchange = async (event) => {\n const file = (event.target as HTMLInputElement).files?.[0];\n if (!file) {\n // console.log('DEBUG: no file');\n toast.error('파일을 선택해주세요.');\n return;\n }\n\n const fileName = file.name;\n const attachment = await uploadFile?.(file, `hr-notes/${fileName}`);\n if (attachment) {\n setAttachedFiles((prev) => [...prev, attachment]);\n }\n input = null;\n };\n input.click();\n }, [uploadFile]);\n\n const handleDeleteFile = useCallback(\n async (fileUrl: string) => {\n try {\n setFileDeleteLoading((p) => p.add(fileUrl));\n await deleteFile?.(fileUrl);\n setAttachedFiles((currentFiles) =>\n currentFiles.filter((item) => item.url !== fileUrl)\n );\n } catch (err) {\n toast.error('파일 삭제에 실패했습니다.');\n } finally {\n setFileDeleteLoading((p) => {\n p.delete(fileUrl);\n return p;\n });\n }\n },\n [deleteFile]\n );\n\n const renderAttachedFiles = useCallback(() => {\n return (\n <Box width=\"100%\">\n {attachedFiles.map((file) => {\n return (\n <>\n <Separator size=\"4\" />\n <Flex\n align=\"center\"\n justify=\"between\"\n key={`${file.url}_${file.fileName}`}\n p=\"4\"\n width=\"100%\"\n >\n <Link href={file.url} size=\"2\">\n <Flex align=\"center\" gap=\"3\">\n <FileIcon />\n {file.fileName}\n </Flex>\n </Link>\n <Button\n loading={fileDeleteLoading.has(file.url)}\n onClick={() => {\n void handleDeleteFile(file.url);\n }}\n variant=\"ghost\"\n >\n 첨부 파일 삭제\n </Button>\n </Flex>\n </>\n );\n })}\n </Box>\n );\n }, [attachedFiles, fileDeleteLoading, handleDeleteFile]);\n\n const handleOnChangeTitle = useCallback<\n React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>\n >((e) => {\n setTitle(e.target.value);\n }, []);\n\n const clearEditorState = useCallback(() => {\n setContent('');\n setAttachedFiles([]);\n setTitle('');\n }, []);\n\n const handleSaveClick = useCallback(() => {\n onClickSave?.({\n title,\n content,\n files: attachedFiles,\n });\n }, [onClickSave, title, content, attachedFiles]);\n\n if (props.clearEditor) {\n props.clearEditor.current = clearEditorState;\n }\n\n const cssVariables = {\n '--max-height': maxHeight,\n '--min-height': minHeight,\n '--height': height || '100%',\n } as React.CSSProperties;\n\n return (\n <div\n className=\"tipp-ql-wrapper\"\n style={{\n ...cssVariables,\n }}\n >\n <Grid height=\"100%\" rows={`${hideHeader ? '' : 'auto 1px'} 1fr`}>\n {/* 제목 입력창 */}\n {hideHeader ? null : (\n <>\n <Grid\n align=\"center\"\n columns=\"auto auto 1fr\"\n gap=\"2\"\n height=\"42px\"\n pl=\"2\"\n pr=\"3\"\n width=\"100%\"\n >\n <Box pl=\"3\" pr=\"3\">\n <Typo>제목</Typo>\n </Box>\n <Separator orientation=\"vertical\" style={{ height: '100%' }} />\n <TextField.Root\n className=\"editor-title-text-field\"\n onChange={handleOnChangeTitle}\n placeholder=\"제목을 입력해주세요\"\n value={title}\n />\n </Grid>\n <Separator orientation=\"horizontal\" size=\"4\" />\n </>\n )}\n\n <ReactQuill\n className=\"tipp-ql-editor write-mode\"\n onChange={handleOnChangeContent}\n ref={editorRef}\n theme=\"snow\"\n value={content}\n {...quillProps}\n />\n {renderAttachedFiles()}\n </Grid>\n\n {hideFooter ? null : (\n <>\n <Separator size=\"4\" />\n <Flex\n align=\"center\"\n justify=\"between\"\n p=\"2\"\n pl=\"4\"\n pr=\"4\"\n width=\"100%\"\n >\n {hideFileAttachment ? (\n <div />\n ) : (\n <Button\n color=\"gray\"\n onClick={handleButtonClick}\n variant=\"transparent\"\n >\n <Link2Icon height={20} width={20} />\n </Button>\n )}\n\n <Flex gap=\"2\">\n {clearEditor ? (\n <Button\n color=\"gray\"\n onClick={clearEditorState}\n variant=\"outline\"\n >\n 초기화\n </Button>\n ) : null}\n {SecondaryButton ? SecondaryButton : null}\n <Button disabled={isLoading} onClick={handleSaveClick}>\n 저장\n </Button>\n </Flex>\n </Flex>\n </>\n )}\n </div>\n );\n }\n);\n\nEditor.displayName = 'TIPP-Quill-Editor';\n"],"mappings":"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","names":["_a"]}
|
|
1
|
+
{"version":3,"sources":["../src/editor.tsx"],"sourcesContent":["import React, { forwardRef, useCallback, useRef, useState } from 'react';\nimport {\n Box,\n Button,\n Flex,\n Grid,\n Link,\n Separator,\n TextField,\n Typo,\n Link2Icon,\n toast,\n FileIcon,\n} from '@tipp/ui';\nimport ReactQuill from 'react-quill-new';\nimport type { Attachment } from './type';\n\nexport interface TippEditorProps extends ReactQuill.ReactQuillProps {\n defaultTitle?: string;\n defaultValue?: string;\n defaultAttachedFiles?: Attachment[];\n /** 저장하기 버튼 클릭 시 실행 */\n onClickSave?: (values: {\n title: string;\n content: string;\n files: Attachment[];\n }) => void;\n /** 파일 업로드 버튼 클릭 시 실행 */\n uploadFile?: (\n file: File,\n destination: string\n ) => Promise<Attachment | undefined>;\n deleteFile?: (fileUrl: string) => Promise<void>;\n /** 외부에서 Editor를 빈 상태로 초기화 시켜야 할 때 사용 */\n clearEditor?: React.MutableRefObject<(() => void) | undefined>;\n /** 초기화 버튼말고 다른 버튼 추가시 */\n SecondaryButton?: React.ReactNode;\n /** true인 경우 저장하기 버튼이 비활성 화 됨. 연타 방지 */\n isLoading?: boolean;\n minHeight?: string;\n maxHeight?: string;\n height?: string;\n /** 제목 입력창 숨김 */\n hideHeader?: boolean;\n /** 첨부 파일 버튼 숨김 */\n hideFileAttachment?: boolean;\n /** 저장 버튼 footer 숨김 */\n hideFooter?: boolean;\n title?: string;\n onChangeTitle?: (value: string) => void;\n content?: string;\n onChangeContent?: (value: string) => void;\n attachedFiles?: Attachment[];\n onChangeAttachedFiles?: (files: Attachment[]) => void;\n}\n\nexport const Editor = forwardRef<ReactQuill, TippEditorProps>(\n (props, ref): React.ReactNode => {\n const {\n defaultAttachedFiles,\n defaultTitle,\n defaultValue,\n onClickSave,\n uploadFile,\n deleteFile,\n isLoading,\n SecondaryButton,\n clearEditor,\n minHeight,\n maxHeight,\n height,\n hideHeader,\n hideFileAttachment,\n hideFooter,\n title: controlledTitle,\n onChangeTitle,\n content: controlledContent,\n onChangeContent,\n attachedFiles: controlledAttachedFiles,\n onChangeAttachedFiles,\n ...quillProps\n } = props;\n const defaultRef = useRef<ReactQuill>(null);\n const editorRef = ref || defaultRef;\n // Controlled vs Uncontrolled 모드 구분\n const isControlledTitle = controlledTitle !== undefined;\n const isControlledContent = controlledContent !== undefined;\n const isControlledAttachedFiles = controlledAttachedFiles !== undefined;\n\n const [internalAttachedFiles, setInternalAttachedFiles] = useState<Attachment[]>(\n defaultAttachedFiles || []\n );\n const [fileDeleteLoading, setFileDeleteLoading] = useState(new Set());\n\n const [internalTitle, setInternalTitle] = useState(defaultTitle || '');\n const [internalContent, setInternalContent] = useState(defaultValue || '');\n\n // 실제 사용할 값들 (controlled일 때는 props 값, uncontrolled일 때는 internal state 값)\n const title = isControlledTitle ? controlledTitle : internalTitle;\n const content = isControlledContent ? controlledContent : internalContent;\n const attachedFiles = isControlledAttachedFiles ? controlledAttachedFiles : internalAttachedFiles;\n\n const handleOnChangeContent = useCallback((value: string) => {\n if (!isControlledContent) {\n setInternalContent(value);\n }\n onChangeContent?.(value);\n }, [isControlledContent, onChangeContent]);\n\n const handleButtonClick = useCallback(() => {\n let input: HTMLInputElement | null = document.createElement('input');\n input.type = 'file';\n input.onchange = async (event) => {\n const file = (event.target as HTMLInputElement).files?.[0];\n if (!file) {\n // console.log('DEBUG: no file');\n toast.error('파일을 선택해주세요.');\n return;\n }\n\n const fileName = file.name;\n const attachment = await uploadFile?.(file, `hr-notes/${fileName}`);\n if (attachment) {\n const newFiles = [...attachedFiles, attachment];\n if (!isControlledAttachedFiles) {\n setInternalAttachedFiles(newFiles);\n }\n onChangeAttachedFiles?.(newFiles);\n }\n input = null;\n };\n input.click();\n }, [uploadFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]);\n\n const handleDeleteFile = useCallback(\n async (fileUrl: string) => {\n try {\n setFileDeleteLoading((p) => p.add(fileUrl));\n await deleteFile?.(fileUrl);\n const newFiles = attachedFiles.filter((item) => item.url !== fileUrl);\n if (!isControlledAttachedFiles) {\n setInternalAttachedFiles(newFiles);\n }\n onChangeAttachedFiles?.(newFiles);\n } catch (err) {\n toast.error('파일 삭제에 실패했습니다.');\n } finally {\n setFileDeleteLoading((p) => {\n p.delete(fileUrl);\n return p;\n });\n }\n },\n [deleteFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]\n );\n\n const renderAttachedFiles = useCallback(() => {\n return (\n <Box width=\"100%\">\n {attachedFiles.map((file) => {\n return (\n <>\n <Separator size=\"4\" />\n <Flex\n align=\"center\"\n justify=\"between\"\n key={`${file.url}_${file.fileName}`}\n p=\"4\"\n width=\"100%\"\n >\n <Link href={file.url} size=\"2\">\n <Flex align=\"center\" gap=\"3\">\n <FileIcon />\n {file.fileName}\n </Flex>\n </Link>\n <Button\n loading={fileDeleteLoading.has(file.url)}\n onClick={() => {\n void handleDeleteFile(file.url);\n }}\n variant=\"ghost\"\n >\n 첨부 파일 삭제\n </Button>\n </Flex>\n </>\n );\n })}\n </Box>\n );\n }, [attachedFiles, fileDeleteLoading, handleDeleteFile]);\n\n const handleOnChangeTitle = useCallback<\n React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>\n >((e) => {\n const newTitle = e.target.value;\n if (!isControlledTitle) {\n setInternalTitle(newTitle);\n }\n onChangeTitle?.(newTitle);\n }, [isControlledTitle, onChangeTitle]);\n\n const clearEditorState = useCallback(() => {\n const emptyTitle = '';\n const emptyContent = '';\n const emptyFiles: Attachment[] = [];\n\n if (!isControlledTitle) {\n setInternalTitle(emptyTitle);\n }\n if (!isControlledContent) {\n setInternalContent(emptyContent);\n }\n if (!isControlledAttachedFiles) {\n setInternalAttachedFiles(emptyFiles);\n }\n\n // controlled 모드일 때도 부모에게 알림\n onChangeTitle?.(emptyTitle);\n onChangeContent?.(emptyContent);\n onChangeAttachedFiles?.(emptyFiles);\n }, [isControlledTitle, isControlledContent, isControlledAttachedFiles, onChangeTitle, onChangeContent, onChangeAttachedFiles]);\n\n const handleSaveClick = useCallback(() => {\n onClickSave?.({\n title,\n content,\n files: attachedFiles,\n });\n }, [onClickSave, title, content, attachedFiles]);\n\n if (props.clearEditor) {\n props.clearEditor.current = clearEditorState;\n }\n\n const cssVariables = {\n '--max-height': maxHeight,\n '--min-height': minHeight,\n '--height': height || '100%',\n } as React.CSSProperties;\n\n return (\n <div\n className=\"tipp-ql-wrapper\"\n style={{\n ...cssVariables,\n }}\n >\n <Grid height=\"100%\" rows={`${hideHeader ? '' : 'auto 1px'} 1fr`}>\n {/* 제목 입력창 */}\n {hideHeader ? null : (\n <>\n <Grid\n align=\"center\"\n columns=\"auto auto 1fr\"\n gap=\"2\"\n height=\"42px\"\n pl=\"2\"\n pr=\"3\"\n width=\"100%\"\n >\n <Box pl=\"3\" pr=\"3\">\n <Typo>제목</Typo>\n </Box>\n <Separator orientation=\"vertical\" style={{ height: '100%' }} />\n <TextField.Root\n className=\"editor-title-text-field\"\n onChange={handleOnChangeTitle}\n placeholder=\"제목을 입력해주세요\"\n value={title}\n />\n </Grid>\n <Separator orientation=\"horizontal\" size=\"4\" />\n </>\n )}\n\n <ReactQuill\n className=\"tipp-ql-editor write-mode\"\n onChange={handleOnChangeContent}\n ref={editorRef}\n theme=\"snow\"\n value={content}\n {...quillProps}\n />\n {renderAttachedFiles()}\n </Grid>\n\n {hideFooter ? null : (\n <>\n <Separator size=\"4\" />\n <Flex\n align=\"center\"\n justify=\"between\"\n p=\"2\"\n pl=\"4\"\n pr=\"4\"\n width=\"100%\"\n >\n {hideFileAttachment ? (\n <div />\n ) : (\n <Button\n color=\"gray\"\n onClick={handleButtonClick}\n variant=\"transparent\"\n >\n <Link2Icon height={20} width={20} />\n </Button>\n )}\n\n <Flex gap=\"2\">\n {clearEditor ? (\n <Button\n color=\"gray\"\n onClick={clearEditorState}\n variant=\"outline\"\n >\n 초기화\n </Button>\n ) : null}\n {SecondaryButton ? SecondaryButton : null}\n <Button disabled={isLoading} onClick={handleSaveClick}>\n 저장\n </Button>\n </Flex>\n </Flex>\n </>\n )}\n </div>\n );\n }\n);\n\nEditor.displayName = 'TIPP-Quill-Editor';\n"],"mappings":"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","names":["_a"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tipp/ui-quill-editor",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.11",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "tipp 디자인 시스템이 적용된 quillEditor 패키지, quillEditor의 사이즈가 커서 별도 패키지로 분리했습니다.",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"postcss-nesting": "12.0.2",
|
|
59
59
|
"tsup": "^8.0.2",
|
|
60
60
|
"typescript": "^5.3.3",
|
|
61
|
-
"@tipp/
|
|
62
|
-
"@tipp/
|
|
61
|
+
"@tipp/typescript-config": "1.0.1",
|
|
62
|
+
"@tipp/eslint-config": "1.1.2"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@tipp/ui": "2.2.7"
|
package/src/editor.tsx
CHANGED
|
@@ -46,6 +46,12 @@ export interface TippEditorProps extends ReactQuill.ReactQuillProps {
|
|
|
46
46
|
hideFileAttachment?: boolean;
|
|
47
47
|
/** 저장 버튼 footer 숨김 */
|
|
48
48
|
hideFooter?: boolean;
|
|
49
|
+
title?: string;
|
|
50
|
+
onChangeTitle?: (value: string) => void;
|
|
51
|
+
content?: string;
|
|
52
|
+
onChangeContent?: (value: string) => void;
|
|
53
|
+
attachedFiles?: Attachment[];
|
|
54
|
+
onChangeAttachedFiles?: (files: Attachment[]) => void;
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
@@ -66,21 +72,40 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
66
72
|
hideHeader,
|
|
67
73
|
hideFileAttachment,
|
|
68
74
|
hideFooter,
|
|
75
|
+
title: controlledTitle,
|
|
76
|
+
onChangeTitle,
|
|
77
|
+
content: controlledContent,
|
|
78
|
+
onChangeContent,
|
|
79
|
+
attachedFiles: controlledAttachedFiles,
|
|
80
|
+
onChangeAttachedFiles,
|
|
69
81
|
...quillProps
|
|
70
82
|
} = props;
|
|
71
83
|
const defaultRef = useRef<ReactQuill>(null);
|
|
72
84
|
const editorRef = ref || defaultRef;
|
|
73
|
-
|
|
85
|
+
// Controlled vs Uncontrolled 모드 구분
|
|
86
|
+
const isControlledTitle = controlledTitle !== undefined;
|
|
87
|
+
const isControlledContent = controlledContent !== undefined;
|
|
88
|
+
const isControlledAttachedFiles = controlledAttachedFiles !== undefined;
|
|
89
|
+
|
|
90
|
+
const [internalAttachedFiles, setInternalAttachedFiles] = useState<Attachment[]>(
|
|
74
91
|
defaultAttachedFiles || []
|
|
75
92
|
);
|
|
76
93
|
const [fileDeleteLoading, setFileDeleteLoading] = useState(new Set());
|
|
77
94
|
|
|
78
|
-
const [
|
|
79
|
-
const [
|
|
95
|
+
const [internalTitle, setInternalTitle] = useState(defaultTitle || '');
|
|
96
|
+
const [internalContent, setInternalContent] = useState(defaultValue || '');
|
|
97
|
+
|
|
98
|
+
// 실제 사용할 값들 (controlled일 때는 props 값, uncontrolled일 때는 internal state 값)
|
|
99
|
+
const title = isControlledTitle ? controlledTitle : internalTitle;
|
|
100
|
+
const content = isControlledContent ? controlledContent : internalContent;
|
|
101
|
+
const attachedFiles = isControlledAttachedFiles ? controlledAttachedFiles : internalAttachedFiles;
|
|
80
102
|
|
|
81
103
|
const handleOnChangeContent = useCallback((value: string) => {
|
|
82
|
-
|
|
83
|
-
|
|
104
|
+
if (!isControlledContent) {
|
|
105
|
+
setInternalContent(value);
|
|
106
|
+
}
|
|
107
|
+
onChangeContent?.(value);
|
|
108
|
+
}, [isControlledContent, onChangeContent]);
|
|
84
109
|
|
|
85
110
|
const handleButtonClick = useCallback(() => {
|
|
86
111
|
let input: HTMLInputElement | null = document.createElement('input');
|
|
@@ -96,21 +121,27 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
96
121
|
const fileName = file.name;
|
|
97
122
|
const attachment = await uploadFile?.(file, `hr-notes/${fileName}`);
|
|
98
123
|
if (attachment) {
|
|
99
|
-
|
|
124
|
+
const newFiles = [...attachedFiles, attachment];
|
|
125
|
+
if (!isControlledAttachedFiles) {
|
|
126
|
+
setInternalAttachedFiles(newFiles);
|
|
127
|
+
}
|
|
128
|
+
onChangeAttachedFiles?.(newFiles);
|
|
100
129
|
}
|
|
101
130
|
input = null;
|
|
102
131
|
};
|
|
103
132
|
input.click();
|
|
104
|
-
}, [uploadFile]);
|
|
133
|
+
}, [uploadFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]);
|
|
105
134
|
|
|
106
135
|
const handleDeleteFile = useCallback(
|
|
107
136
|
async (fileUrl: string) => {
|
|
108
137
|
try {
|
|
109
138
|
setFileDeleteLoading((p) => p.add(fileUrl));
|
|
110
139
|
await deleteFile?.(fileUrl);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
140
|
+
const newFiles = attachedFiles.filter((item) => item.url !== fileUrl);
|
|
141
|
+
if (!isControlledAttachedFiles) {
|
|
142
|
+
setInternalAttachedFiles(newFiles);
|
|
143
|
+
}
|
|
144
|
+
onChangeAttachedFiles?.(newFiles);
|
|
114
145
|
} catch (err) {
|
|
115
146
|
toast.error('파일 삭제에 실패했습니다.');
|
|
116
147
|
} finally {
|
|
@@ -120,7 +151,7 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
120
151
|
});
|
|
121
152
|
}
|
|
122
153
|
},
|
|
123
|
-
[deleteFile]
|
|
154
|
+
[deleteFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]
|
|
124
155
|
);
|
|
125
156
|
|
|
126
157
|
const renderAttachedFiles = useCallback(() => {
|
|
@@ -163,14 +194,33 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
163
194
|
const handleOnChangeTitle = useCallback<
|
|
164
195
|
React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>
|
|
165
196
|
>((e) => {
|
|
166
|
-
|
|
167
|
-
|
|
197
|
+
const newTitle = e.target.value;
|
|
198
|
+
if (!isControlledTitle) {
|
|
199
|
+
setInternalTitle(newTitle);
|
|
200
|
+
}
|
|
201
|
+
onChangeTitle?.(newTitle);
|
|
202
|
+
}, [isControlledTitle, onChangeTitle]);
|
|
168
203
|
|
|
169
204
|
const clearEditorState = useCallback(() => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
205
|
+
const emptyTitle = '';
|
|
206
|
+
const emptyContent = '';
|
|
207
|
+
const emptyFiles: Attachment[] = [];
|
|
208
|
+
|
|
209
|
+
if (!isControlledTitle) {
|
|
210
|
+
setInternalTitle(emptyTitle);
|
|
211
|
+
}
|
|
212
|
+
if (!isControlledContent) {
|
|
213
|
+
setInternalContent(emptyContent);
|
|
214
|
+
}
|
|
215
|
+
if (!isControlledAttachedFiles) {
|
|
216
|
+
setInternalAttachedFiles(emptyFiles);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// controlled 모드일 때도 부모에게 알림
|
|
220
|
+
onChangeTitle?.(emptyTitle);
|
|
221
|
+
onChangeContent?.(emptyContent);
|
|
222
|
+
onChangeAttachedFiles?.(emptyFiles);
|
|
223
|
+
}, [isControlledTitle, isControlledContent, isControlledAttachedFiles, onChangeTitle, onChangeContent, onChangeAttachedFiles]);
|
|
174
224
|
|
|
175
225
|
const handleSaveClick = useCallback(() => {
|
|
176
226
|
onClickSave?.({
|