@tipp/ui-quill-editor 4.0.11 → 4.0.13
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 +63 -20
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +63 -20
- package/dist/editor.js.map +1 -1
- package/package.json +1 -1
- package/src/editor.tsx +67 -20
package/dist/editor.cjs
CHANGED
|
@@ -83,6 +83,25 @@ var import_react = require("react");
|
|
|
83
83
|
var import_ui = require("@tipp/ui");
|
|
84
84
|
var import_react_quill_new = __toESM(require("react-quill-new"), 1);
|
|
85
85
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
86
|
+
var toolbarOptions = [
|
|
87
|
+
[{ size: ["small", false, "large", "huge"] }],
|
|
88
|
+
["bold", "italic", "underline", "strike"],
|
|
89
|
+
// toggled buttons
|
|
90
|
+
["blockquote"],
|
|
91
|
+
["link"],
|
|
92
|
+
// custom button values
|
|
93
|
+
[{ list: "ordered" }, { list: "bullet" }, { list: "check" }],
|
|
94
|
+
// [{ script: 'sub' }, { script: 'super' }], // superscript/subscript
|
|
95
|
+
// [{ indent: '-1' }, { indent: '+1' }], // outdent/indent
|
|
96
|
+
// [{ direction: 'rtl' }], // text direction
|
|
97
|
+
// [{ size: ['small', false, 'large', 'huge'] }], // custom dropdown
|
|
98
|
+
// [{ header: [1, 2, 3, 4, 5, 6, false] }],
|
|
99
|
+
[{ color: [] }, { background: [] }],
|
|
100
|
+
// dropdown with defaults from theme
|
|
101
|
+
// [{ font: [] }],
|
|
102
|
+
[{ align: [] }]
|
|
103
|
+
// ['clean'], // remove formatting button
|
|
104
|
+
];
|
|
86
105
|
var Editor = (0, import_react.forwardRef)(
|
|
87
106
|
(props, ref) => {
|
|
88
107
|
const _a = props, {
|
|
@@ -135,21 +154,22 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
135
154
|
const isControlledTitle = controlledTitle !== void 0;
|
|
136
155
|
const isControlledContent = controlledContent !== void 0;
|
|
137
156
|
const isControlledAttachedFiles = controlledAttachedFiles !== void 0;
|
|
138
|
-
const [internalAttachedFiles, setInternalAttachedFiles] = (0, import_react.useState)(
|
|
139
|
-
defaultAttachedFiles || []
|
|
140
|
-
);
|
|
157
|
+
const [internalAttachedFiles, setInternalAttachedFiles] = (0, import_react.useState)(defaultAttachedFiles || []);
|
|
141
158
|
const [fileDeleteLoading, setFileDeleteLoading] = (0, import_react.useState)(/* @__PURE__ */ new Set());
|
|
142
159
|
const [internalTitle, setInternalTitle] = (0, import_react.useState)(defaultTitle || "");
|
|
143
160
|
const [internalContent, setInternalContent] = (0, import_react.useState)(defaultValue || "");
|
|
144
161
|
const title = isControlledTitle ? controlledTitle : internalTitle;
|
|
145
162
|
const content = isControlledContent ? controlledContent : internalContent;
|
|
146
163
|
const attachedFiles = isControlledAttachedFiles ? controlledAttachedFiles : internalAttachedFiles;
|
|
147
|
-
const handleOnChangeContent = (0, import_react.useCallback)(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
164
|
+
const handleOnChangeContent = (0, import_react.useCallback)(
|
|
165
|
+
(value) => {
|
|
166
|
+
if (!isControlledContent) {
|
|
167
|
+
setInternalContent(value);
|
|
168
|
+
}
|
|
169
|
+
onChangeContent == null ? void 0 : onChangeContent(value);
|
|
170
|
+
},
|
|
171
|
+
[isControlledContent, onChangeContent]
|
|
172
|
+
);
|
|
153
173
|
const handleButtonClick = (0, import_react.useCallback)(() => {
|
|
154
174
|
let input = document.createElement("input");
|
|
155
175
|
input.type = "file";
|
|
@@ -172,7 +192,12 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
172
192
|
input = null;
|
|
173
193
|
});
|
|
174
194
|
input.click();
|
|
175
|
-
}, [
|
|
195
|
+
}, [
|
|
196
|
+
uploadFile,
|
|
197
|
+
attachedFiles,
|
|
198
|
+
isControlledAttachedFiles,
|
|
199
|
+
onChangeAttachedFiles
|
|
200
|
+
]);
|
|
176
201
|
const handleDeleteFile = (0, import_react.useCallback)(
|
|
177
202
|
(fileUrl) => __async(null, null, function* () {
|
|
178
203
|
try {
|
|
@@ -192,7 +217,12 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
192
217
|
});
|
|
193
218
|
}
|
|
194
219
|
}),
|
|
195
|
-
[
|
|
220
|
+
[
|
|
221
|
+
deleteFile,
|
|
222
|
+
attachedFiles,
|
|
223
|
+
isControlledAttachedFiles,
|
|
224
|
+
onChangeAttachedFiles
|
|
225
|
+
]
|
|
196
226
|
);
|
|
197
227
|
const renderAttachedFiles = (0, import_react.useCallback)(() => {
|
|
198
228
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Box, { width: "100%", children: attachedFiles.map((file) => {
|
|
@@ -228,13 +258,16 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
228
258
|
] });
|
|
229
259
|
}) });
|
|
230
260
|
}, [attachedFiles, fileDeleteLoading, handleDeleteFile]);
|
|
231
|
-
const handleOnChangeTitle = (0, import_react.useCallback)(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
261
|
+
const handleOnChangeTitle = (0, import_react.useCallback)(
|
|
262
|
+
(e) => {
|
|
263
|
+
const newTitle = e.target.value;
|
|
264
|
+
if (!isControlledTitle) {
|
|
265
|
+
setInternalTitle(newTitle);
|
|
266
|
+
}
|
|
267
|
+
onChangeTitle == null ? void 0 : onChangeTitle(newTitle);
|
|
268
|
+
},
|
|
269
|
+
[isControlledTitle, onChangeTitle]
|
|
270
|
+
);
|
|
238
271
|
const clearEditorState = (0, import_react.useCallback)(() => {
|
|
239
272
|
const emptyTitle = "";
|
|
240
273
|
const emptyContent = "";
|
|
@@ -251,7 +284,14 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
251
284
|
onChangeTitle == null ? void 0 : onChangeTitle(emptyTitle);
|
|
252
285
|
onChangeContent == null ? void 0 : onChangeContent(emptyContent);
|
|
253
286
|
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(emptyFiles);
|
|
254
|
-
}, [
|
|
287
|
+
}, [
|
|
288
|
+
isControlledTitle,
|
|
289
|
+
isControlledContent,
|
|
290
|
+
isControlledAttachedFiles,
|
|
291
|
+
onChangeTitle,
|
|
292
|
+
onChangeContent,
|
|
293
|
+
onChangeAttachedFiles
|
|
294
|
+
]);
|
|
255
295
|
const handleSaveClick = (0, import_react.useCallback)(() => {
|
|
256
296
|
onClickSave == null ? void 0 : onClickSave({
|
|
257
297
|
title,
|
|
@@ -309,7 +349,10 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
309
349
|
onChange: handleOnChangeContent,
|
|
310
350
|
ref: editorRef,
|
|
311
351
|
theme: "snow",
|
|
312
|
-
value: content
|
|
352
|
+
value: content,
|
|
353
|
+
modules: {
|
|
354
|
+
toolbar: toolbarOptions
|
|
355
|
+
}
|
|
313
356
|
}, quillProps)
|
|
314
357
|
),
|
|
315
358
|
renderAttachedFiles()
|
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 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"]}
|
|
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\nconst toolbarOptions = [\n [{ size: ['small', false, 'large', 'huge'] }],\n ['bold', 'italic', 'underline', 'strike'], // toggled buttons\n ['blockquote'],\n ['link'], // custom button values\n [{ list: 'ordered' }, { list: 'bullet' }, { list: 'check' }],\n // [{ script: 'sub' }, { script: 'super' }], // superscript/subscript\n // [{ indent: '-1' }, { indent: '+1' }], // outdent/indent\n // [{ direction: 'rtl' }], // text direction\n\n // [{ size: ['small', false, 'large', 'huge'] }], // custom dropdown\n // [{ header: [1, 2, 3, 4, 5, 6, false] }],\n\n [{ color: [] }, { background: [] }], // dropdown with defaults from theme\n // [{ font: [] }],\n [{ align: [] }],\n // ['clean'], // remove formatting button\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<\n Attachment[]\n >(defaultAttachedFiles || []);\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\n ? controlledAttachedFiles\n : internalAttachedFiles;\n\n const handleOnChangeContent = useCallback(\n (value: string) => {\n if (!isControlledContent) {\n setInternalContent(value);\n }\n onChangeContent?.(value);\n },\n [isControlledContent, onChangeContent]\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 const newFiles = [...attachedFiles, attachment];\n if (!isControlledAttachedFiles) {\n setInternalAttachedFiles(newFiles);\n }\n onChangeAttachedFiles?.(newFiles);\n }\n input = null;\n };\n input.click();\n }, [\n uploadFile,\n attachedFiles,\n isControlledAttachedFiles,\n onChangeAttachedFiles,\n ]);\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 [\n deleteFile,\n attachedFiles,\n isControlledAttachedFiles,\n onChangeAttachedFiles,\n ]\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 >(\n (e) => {\n const newTitle = e.target.value;\n if (!isControlledTitle) {\n setInternalTitle(newTitle);\n }\n onChangeTitle?.(newTitle);\n },\n [isControlledTitle, onChangeTitle]\n );\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 }, [\n isControlledTitle,\n isControlledContent,\n isControlledAttachedFiles,\n onChangeTitle,\n onChangeContent,\n onChangeAttachedFiles,\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 modules={{\n toolbar: toolbarOptions,\n }}\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.js
CHANGED
|
@@ -21,6 +21,25 @@ import {
|
|
|
21
21
|
} from "@tipp/ui";
|
|
22
22
|
import ReactQuill from "react-quill-new";
|
|
23
23
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
24
|
+
var toolbarOptions = [
|
|
25
|
+
[{ size: ["small", false, "large", "huge"] }],
|
|
26
|
+
["bold", "italic", "underline", "strike"],
|
|
27
|
+
// toggled buttons
|
|
28
|
+
["blockquote"],
|
|
29
|
+
["link"],
|
|
30
|
+
// custom button values
|
|
31
|
+
[{ list: "ordered" }, { list: "bullet" }, { list: "check" }],
|
|
32
|
+
// [{ script: 'sub' }, { script: 'super' }], // superscript/subscript
|
|
33
|
+
// [{ indent: '-1' }, { indent: '+1' }], // outdent/indent
|
|
34
|
+
// [{ direction: 'rtl' }], // text direction
|
|
35
|
+
// [{ size: ['small', false, 'large', 'huge'] }], // custom dropdown
|
|
36
|
+
// [{ header: [1, 2, 3, 4, 5, 6, false] }],
|
|
37
|
+
[{ color: [] }, { background: [] }],
|
|
38
|
+
// dropdown with defaults from theme
|
|
39
|
+
// [{ font: [] }],
|
|
40
|
+
[{ align: [] }]
|
|
41
|
+
// ['clean'], // remove formatting button
|
|
42
|
+
];
|
|
24
43
|
var Editor = forwardRef(
|
|
25
44
|
(props, ref) => {
|
|
26
45
|
const _a = props, {
|
|
@@ -73,21 +92,22 @@ var Editor = forwardRef(
|
|
|
73
92
|
const isControlledTitle = controlledTitle !== void 0;
|
|
74
93
|
const isControlledContent = controlledContent !== void 0;
|
|
75
94
|
const isControlledAttachedFiles = controlledAttachedFiles !== void 0;
|
|
76
|
-
const [internalAttachedFiles, setInternalAttachedFiles] = useState(
|
|
77
|
-
defaultAttachedFiles || []
|
|
78
|
-
);
|
|
95
|
+
const [internalAttachedFiles, setInternalAttachedFiles] = useState(defaultAttachedFiles || []);
|
|
79
96
|
const [fileDeleteLoading, setFileDeleteLoading] = useState(/* @__PURE__ */ new Set());
|
|
80
97
|
const [internalTitle, setInternalTitle] = useState(defaultTitle || "");
|
|
81
98
|
const [internalContent, setInternalContent] = useState(defaultValue || "");
|
|
82
99
|
const title = isControlledTitle ? controlledTitle : internalTitle;
|
|
83
100
|
const content = isControlledContent ? controlledContent : internalContent;
|
|
84
101
|
const attachedFiles = isControlledAttachedFiles ? controlledAttachedFiles : internalAttachedFiles;
|
|
85
|
-
const handleOnChangeContent = useCallback(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
const handleOnChangeContent = useCallback(
|
|
103
|
+
(value) => {
|
|
104
|
+
if (!isControlledContent) {
|
|
105
|
+
setInternalContent(value);
|
|
106
|
+
}
|
|
107
|
+
onChangeContent == null ? void 0 : onChangeContent(value);
|
|
108
|
+
},
|
|
109
|
+
[isControlledContent, onChangeContent]
|
|
110
|
+
);
|
|
91
111
|
const handleButtonClick = useCallback(() => {
|
|
92
112
|
let input = document.createElement("input");
|
|
93
113
|
input.type = "file";
|
|
@@ -110,7 +130,12 @@ var Editor = forwardRef(
|
|
|
110
130
|
input = null;
|
|
111
131
|
});
|
|
112
132
|
input.click();
|
|
113
|
-
}, [
|
|
133
|
+
}, [
|
|
134
|
+
uploadFile,
|
|
135
|
+
attachedFiles,
|
|
136
|
+
isControlledAttachedFiles,
|
|
137
|
+
onChangeAttachedFiles
|
|
138
|
+
]);
|
|
114
139
|
const handleDeleteFile = useCallback(
|
|
115
140
|
(fileUrl) => __async(null, null, function* () {
|
|
116
141
|
try {
|
|
@@ -130,7 +155,12 @@ var Editor = forwardRef(
|
|
|
130
155
|
});
|
|
131
156
|
}
|
|
132
157
|
}),
|
|
133
|
-
[
|
|
158
|
+
[
|
|
159
|
+
deleteFile,
|
|
160
|
+
attachedFiles,
|
|
161
|
+
isControlledAttachedFiles,
|
|
162
|
+
onChangeAttachedFiles
|
|
163
|
+
]
|
|
134
164
|
);
|
|
135
165
|
const renderAttachedFiles = useCallback(() => {
|
|
136
166
|
return /* @__PURE__ */ jsx(Box, { width: "100%", children: attachedFiles.map((file) => {
|
|
@@ -166,13 +196,16 @@ var Editor = forwardRef(
|
|
|
166
196
|
] });
|
|
167
197
|
}) });
|
|
168
198
|
}, [attachedFiles, fileDeleteLoading, handleDeleteFile]);
|
|
169
|
-
const handleOnChangeTitle = useCallback(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
199
|
+
const handleOnChangeTitle = useCallback(
|
|
200
|
+
(e) => {
|
|
201
|
+
const newTitle = e.target.value;
|
|
202
|
+
if (!isControlledTitle) {
|
|
203
|
+
setInternalTitle(newTitle);
|
|
204
|
+
}
|
|
205
|
+
onChangeTitle == null ? void 0 : onChangeTitle(newTitle);
|
|
206
|
+
},
|
|
207
|
+
[isControlledTitle, onChangeTitle]
|
|
208
|
+
);
|
|
176
209
|
const clearEditorState = useCallback(() => {
|
|
177
210
|
const emptyTitle = "";
|
|
178
211
|
const emptyContent = "";
|
|
@@ -189,7 +222,14 @@ var Editor = forwardRef(
|
|
|
189
222
|
onChangeTitle == null ? void 0 : onChangeTitle(emptyTitle);
|
|
190
223
|
onChangeContent == null ? void 0 : onChangeContent(emptyContent);
|
|
191
224
|
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(emptyFiles);
|
|
192
|
-
}, [
|
|
225
|
+
}, [
|
|
226
|
+
isControlledTitle,
|
|
227
|
+
isControlledContent,
|
|
228
|
+
isControlledAttachedFiles,
|
|
229
|
+
onChangeTitle,
|
|
230
|
+
onChangeContent,
|
|
231
|
+
onChangeAttachedFiles
|
|
232
|
+
]);
|
|
193
233
|
const handleSaveClick = useCallback(() => {
|
|
194
234
|
onClickSave == null ? void 0 : onClickSave({
|
|
195
235
|
title,
|
|
@@ -247,7 +287,10 @@ var Editor = forwardRef(
|
|
|
247
287
|
onChange: handleOnChangeContent,
|
|
248
288
|
ref: editorRef,
|
|
249
289
|
theme: "snow",
|
|
250
|
-
value: content
|
|
290
|
+
value: content,
|
|
291
|
+
modules: {
|
|
292
|
+
toolbar: toolbarOptions
|
|
293
|
+
}
|
|
251
294
|
}, quillProps)
|
|
252
295
|
),
|
|
253
296
|
renderAttachedFiles()
|
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 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"]}
|
|
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\nconst toolbarOptions = [\n [{ size: ['small', false, 'large', 'huge'] }],\n ['bold', 'italic', 'underline', 'strike'], // toggled buttons\n ['blockquote'],\n ['link'], // custom button values\n [{ list: 'ordered' }, { list: 'bullet' }, { list: 'check' }],\n // [{ script: 'sub' }, { script: 'super' }], // superscript/subscript\n // [{ indent: '-1' }, { indent: '+1' }], // outdent/indent\n // [{ direction: 'rtl' }], // text direction\n\n // [{ size: ['small', false, 'large', 'huge'] }], // custom dropdown\n // [{ header: [1, 2, 3, 4, 5, 6, false] }],\n\n [{ color: [] }, { background: [] }], // dropdown with defaults from theme\n // [{ font: [] }],\n [{ align: [] }],\n // ['clean'], // remove formatting button\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<\n Attachment[]\n >(defaultAttachedFiles || []);\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\n ? controlledAttachedFiles\n : internalAttachedFiles;\n\n const handleOnChangeContent = useCallback(\n (value: string) => {\n if (!isControlledContent) {\n setInternalContent(value);\n }\n onChangeContent?.(value);\n },\n [isControlledContent, onChangeContent]\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 const newFiles = [...attachedFiles, attachment];\n if (!isControlledAttachedFiles) {\n setInternalAttachedFiles(newFiles);\n }\n onChangeAttachedFiles?.(newFiles);\n }\n input = null;\n };\n input.click();\n }, [\n uploadFile,\n attachedFiles,\n isControlledAttachedFiles,\n onChangeAttachedFiles,\n ]);\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 [\n deleteFile,\n attachedFiles,\n isControlledAttachedFiles,\n onChangeAttachedFiles,\n ]\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 >(\n (e) => {\n const newTitle = e.target.value;\n if (!isControlledTitle) {\n setInternalTitle(newTitle);\n }\n onChangeTitle?.(newTitle);\n },\n [isControlledTitle, onChangeTitle]\n );\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 }, [\n isControlledTitle,\n isControlledContent,\n isControlledAttachedFiles,\n onChangeTitle,\n onChangeContent,\n onChangeAttachedFiles,\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 modules={{\n toolbar: toolbarOptions,\n }}\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
package/src/editor.tsx
CHANGED
|
@@ -54,6 +54,25 @@ export interface TippEditorProps extends ReactQuill.ReactQuillProps {
|
|
|
54
54
|
onChangeAttachedFiles?: (files: Attachment[]) => void;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
const toolbarOptions = [
|
|
58
|
+
[{ size: ['small', false, 'large', 'huge'] }],
|
|
59
|
+
['bold', 'italic', 'underline', 'strike'], // toggled buttons
|
|
60
|
+
['blockquote'],
|
|
61
|
+
['link'], // custom button values
|
|
62
|
+
[{ list: 'ordered' }, { list: 'bullet' }, { list: 'check' }],
|
|
63
|
+
// [{ script: 'sub' }, { script: 'super' }], // superscript/subscript
|
|
64
|
+
// [{ indent: '-1' }, { indent: '+1' }], // outdent/indent
|
|
65
|
+
// [{ direction: 'rtl' }], // text direction
|
|
66
|
+
|
|
67
|
+
// [{ size: ['small', false, 'large', 'huge'] }], // custom dropdown
|
|
68
|
+
// [{ header: [1, 2, 3, 4, 5, 6, false] }],
|
|
69
|
+
|
|
70
|
+
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
|
|
71
|
+
// [{ font: [] }],
|
|
72
|
+
[{ align: [] }],
|
|
73
|
+
// ['clean'], // remove formatting button
|
|
74
|
+
];
|
|
75
|
+
|
|
57
76
|
export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
58
77
|
(props, ref): React.ReactNode => {
|
|
59
78
|
const {
|
|
@@ -87,9 +106,9 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
87
106
|
const isControlledContent = controlledContent !== undefined;
|
|
88
107
|
const isControlledAttachedFiles = controlledAttachedFiles !== undefined;
|
|
89
108
|
|
|
90
|
-
const [internalAttachedFiles, setInternalAttachedFiles] = useState<
|
|
91
|
-
|
|
92
|
-
);
|
|
109
|
+
const [internalAttachedFiles, setInternalAttachedFiles] = useState<
|
|
110
|
+
Attachment[]
|
|
111
|
+
>(defaultAttachedFiles || []);
|
|
93
112
|
const [fileDeleteLoading, setFileDeleteLoading] = useState(new Set());
|
|
94
113
|
|
|
95
114
|
const [internalTitle, setInternalTitle] = useState(defaultTitle || '');
|
|
@@ -98,14 +117,19 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
98
117
|
// 실제 사용할 값들 (controlled일 때는 props 값, uncontrolled일 때는 internal state 값)
|
|
99
118
|
const title = isControlledTitle ? controlledTitle : internalTitle;
|
|
100
119
|
const content = isControlledContent ? controlledContent : internalContent;
|
|
101
|
-
const attachedFiles = isControlledAttachedFiles
|
|
120
|
+
const attachedFiles = isControlledAttachedFiles
|
|
121
|
+
? controlledAttachedFiles
|
|
122
|
+
: internalAttachedFiles;
|
|
102
123
|
|
|
103
|
-
const handleOnChangeContent = useCallback(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
124
|
+
const handleOnChangeContent = useCallback(
|
|
125
|
+
(value: string) => {
|
|
126
|
+
if (!isControlledContent) {
|
|
127
|
+
setInternalContent(value);
|
|
128
|
+
}
|
|
129
|
+
onChangeContent?.(value);
|
|
130
|
+
},
|
|
131
|
+
[isControlledContent, onChangeContent]
|
|
132
|
+
);
|
|
109
133
|
|
|
110
134
|
const handleButtonClick = useCallback(() => {
|
|
111
135
|
let input: HTMLInputElement | null = document.createElement('input');
|
|
@@ -130,7 +154,12 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
130
154
|
input = null;
|
|
131
155
|
};
|
|
132
156
|
input.click();
|
|
133
|
-
}, [
|
|
157
|
+
}, [
|
|
158
|
+
uploadFile,
|
|
159
|
+
attachedFiles,
|
|
160
|
+
isControlledAttachedFiles,
|
|
161
|
+
onChangeAttachedFiles,
|
|
162
|
+
]);
|
|
134
163
|
|
|
135
164
|
const handleDeleteFile = useCallback(
|
|
136
165
|
async (fileUrl: string) => {
|
|
@@ -151,7 +180,12 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
151
180
|
});
|
|
152
181
|
}
|
|
153
182
|
},
|
|
154
|
-
[
|
|
183
|
+
[
|
|
184
|
+
deleteFile,
|
|
185
|
+
attachedFiles,
|
|
186
|
+
isControlledAttachedFiles,
|
|
187
|
+
onChangeAttachedFiles,
|
|
188
|
+
]
|
|
155
189
|
);
|
|
156
190
|
|
|
157
191
|
const renderAttachedFiles = useCallback(() => {
|
|
@@ -193,13 +227,16 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
193
227
|
|
|
194
228
|
const handleOnChangeTitle = useCallback<
|
|
195
229
|
React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>
|
|
196
|
-
>(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
230
|
+
>(
|
|
231
|
+
(e) => {
|
|
232
|
+
const newTitle = e.target.value;
|
|
233
|
+
if (!isControlledTitle) {
|
|
234
|
+
setInternalTitle(newTitle);
|
|
235
|
+
}
|
|
236
|
+
onChangeTitle?.(newTitle);
|
|
237
|
+
},
|
|
238
|
+
[isControlledTitle, onChangeTitle]
|
|
239
|
+
);
|
|
203
240
|
|
|
204
241
|
const clearEditorState = useCallback(() => {
|
|
205
242
|
const emptyTitle = '';
|
|
@@ -220,7 +257,14 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
220
257
|
onChangeTitle?.(emptyTitle);
|
|
221
258
|
onChangeContent?.(emptyContent);
|
|
222
259
|
onChangeAttachedFiles?.(emptyFiles);
|
|
223
|
-
}, [
|
|
260
|
+
}, [
|
|
261
|
+
isControlledTitle,
|
|
262
|
+
isControlledContent,
|
|
263
|
+
isControlledAttachedFiles,
|
|
264
|
+
onChangeTitle,
|
|
265
|
+
onChangeContent,
|
|
266
|
+
onChangeAttachedFiles,
|
|
267
|
+
]);
|
|
224
268
|
|
|
225
269
|
const handleSaveClick = useCallback(() => {
|
|
226
270
|
onClickSave?.({
|
|
@@ -281,6 +325,9 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
281
325
|
ref={editorRef}
|
|
282
326
|
theme="snow"
|
|
283
327
|
value={content}
|
|
328
|
+
modules={{
|
|
329
|
+
toolbar: toolbarOptions,
|
|
330
|
+
}}
|
|
284
331
|
{...quillProps}
|
|
285
332
|
/>
|
|
286
333
|
{renderAttachedFiles()}
|