@tipp/ui-quill-editor 4.0.10 → 4.0.12
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 +109 -24
- 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 +109 -24
- package/dist/editor.js.map +1 -1
- package/package.json +1 -1
- package/src/editor.tsx +118 -21
package/dist/editor.cjs
CHANGED
|
@@ -83,6 +83,24 @@ 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
|
+
["bold", "italic", "underline", "strike"],
|
|
88
|
+
// toggled buttons
|
|
89
|
+
["blockquote"],
|
|
90
|
+
["link"],
|
|
91
|
+
[{ header: 1 }, { header: 2 }, { header: 3 }],
|
|
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: [] }], // dropdown with defaults from theme
|
|
100
|
+
// [{ font: [] }],
|
|
101
|
+
// [{ align: [] }],
|
|
102
|
+
// ['clean'], // remove formatting button
|
|
103
|
+
];
|
|
86
104
|
var Editor = (0, import_react.forwardRef)(
|
|
87
105
|
(props, ref) => {
|
|
88
106
|
const _a = props, {
|
|
@@ -100,7 +118,13 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
100
118
|
height,
|
|
101
119
|
hideHeader,
|
|
102
120
|
hideFileAttachment,
|
|
103
|
-
hideFooter
|
|
121
|
+
hideFooter,
|
|
122
|
+
title: controlledTitle,
|
|
123
|
+
onChangeTitle,
|
|
124
|
+
content: controlledContent,
|
|
125
|
+
onChangeContent,
|
|
126
|
+
attachedFiles: controlledAttachedFiles,
|
|
127
|
+
onChangeAttachedFiles
|
|
104
128
|
} = _a, quillProps = __objRest(_a, [
|
|
105
129
|
"defaultAttachedFiles",
|
|
106
130
|
"defaultTitle",
|
|
@@ -116,19 +140,35 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
116
140
|
"height",
|
|
117
141
|
"hideHeader",
|
|
118
142
|
"hideFileAttachment",
|
|
119
|
-
"hideFooter"
|
|
143
|
+
"hideFooter",
|
|
144
|
+
"title",
|
|
145
|
+
"onChangeTitle",
|
|
146
|
+
"content",
|
|
147
|
+
"onChangeContent",
|
|
148
|
+
"attachedFiles",
|
|
149
|
+
"onChangeAttachedFiles"
|
|
120
150
|
]);
|
|
121
151
|
const defaultRef = (0, import_react.useRef)(null);
|
|
122
152
|
const editorRef = ref || defaultRef;
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
153
|
+
const isControlledTitle = controlledTitle !== void 0;
|
|
154
|
+
const isControlledContent = controlledContent !== void 0;
|
|
155
|
+
const isControlledAttachedFiles = controlledAttachedFiles !== void 0;
|
|
156
|
+
const [internalAttachedFiles, setInternalAttachedFiles] = (0, import_react.useState)(defaultAttachedFiles || []);
|
|
126
157
|
const [fileDeleteLoading, setFileDeleteLoading] = (0, import_react.useState)(/* @__PURE__ */ new Set());
|
|
127
|
-
const [
|
|
128
|
-
const [
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
158
|
+
const [internalTitle, setInternalTitle] = (0, import_react.useState)(defaultTitle || "");
|
|
159
|
+
const [internalContent, setInternalContent] = (0, import_react.useState)(defaultValue || "");
|
|
160
|
+
const title = isControlledTitle ? controlledTitle : internalTitle;
|
|
161
|
+
const content = isControlledContent ? controlledContent : internalContent;
|
|
162
|
+
const attachedFiles = isControlledAttachedFiles ? controlledAttachedFiles : internalAttachedFiles;
|
|
163
|
+
const handleOnChangeContent = (0, import_react.useCallback)(
|
|
164
|
+
(value) => {
|
|
165
|
+
if (!isControlledContent) {
|
|
166
|
+
setInternalContent(value);
|
|
167
|
+
}
|
|
168
|
+
onChangeContent == null ? void 0 : onChangeContent(value);
|
|
169
|
+
},
|
|
170
|
+
[isControlledContent, onChangeContent]
|
|
171
|
+
);
|
|
132
172
|
const handleButtonClick = (0, import_react.useCallback)(() => {
|
|
133
173
|
let input = document.createElement("input");
|
|
134
174
|
input.type = "file";
|
|
@@ -142,20 +182,31 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
142
182
|
const fileName = file.name;
|
|
143
183
|
const attachment = yield uploadFile == null ? void 0 : uploadFile(file, `hr-notes/${fileName}`);
|
|
144
184
|
if (attachment) {
|
|
145
|
-
|
|
185
|
+
const newFiles = [...attachedFiles, attachment];
|
|
186
|
+
if (!isControlledAttachedFiles) {
|
|
187
|
+
setInternalAttachedFiles(newFiles);
|
|
188
|
+
}
|
|
189
|
+
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(newFiles);
|
|
146
190
|
}
|
|
147
191
|
input = null;
|
|
148
192
|
});
|
|
149
193
|
input.click();
|
|
150
|
-
}, [
|
|
194
|
+
}, [
|
|
195
|
+
uploadFile,
|
|
196
|
+
attachedFiles,
|
|
197
|
+
isControlledAttachedFiles,
|
|
198
|
+
onChangeAttachedFiles
|
|
199
|
+
]);
|
|
151
200
|
const handleDeleteFile = (0, import_react.useCallback)(
|
|
152
201
|
(fileUrl) => __async(null, null, function* () {
|
|
153
202
|
try {
|
|
154
203
|
setFileDeleteLoading((p) => p.add(fileUrl));
|
|
155
204
|
yield deleteFile == null ? void 0 : deleteFile(fileUrl);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
205
|
+
const newFiles = attachedFiles.filter((item) => item.url !== fileUrl);
|
|
206
|
+
if (!isControlledAttachedFiles) {
|
|
207
|
+
setInternalAttachedFiles(newFiles);
|
|
208
|
+
}
|
|
209
|
+
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(newFiles);
|
|
159
210
|
} catch (err) {
|
|
160
211
|
import_ui.toast.error("\uD30C\uC77C \uC0AD\uC81C\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
|
|
161
212
|
} finally {
|
|
@@ -165,7 +216,12 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
165
216
|
});
|
|
166
217
|
}
|
|
167
218
|
}),
|
|
168
|
-
[
|
|
219
|
+
[
|
|
220
|
+
deleteFile,
|
|
221
|
+
attachedFiles,
|
|
222
|
+
isControlledAttachedFiles,
|
|
223
|
+
onChangeAttachedFiles
|
|
224
|
+
]
|
|
169
225
|
);
|
|
170
226
|
const renderAttachedFiles = (0, import_react.useCallback)(() => {
|
|
171
227
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Box, { width: "100%", children: attachedFiles.map((file) => {
|
|
@@ -201,14 +257,40 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
201
257
|
] });
|
|
202
258
|
}) });
|
|
203
259
|
}, [attachedFiles, fileDeleteLoading, handleDeleteFile]);
|
|
204
|
-
const handleOnChangeTitle = (0, import_react.useCallback)(
|
|
205
|
-
|
|
206
|
-
|
|
260
|
+
const handleOnChangeTitle = (0, import_react.useCallback)(
|
|
261
|
+
(e) => {
|
|
262
|
+
const newTitle = e.target.value;
|
|
263
|
+
if (!isControlledTitle) {
|
|
264
|
+
setInternalTitle(newTitle);
|
|
265
|
+
}
|
|
266
|
+
onChangeTitle == null ? void 0 : onChangeTitle(newTitle);
|
|
267
|
+
},
|
|
268
|
+
[isControlledTitle, onChangeTitle]
|
|
269
|
+
);
|
|
207
270
|
const clearEditorState = (0, import_react.useCallback)(() => {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
271
|
+
const emptyTitle = "";
|
|
272
|
+
const emptyContent = "";
|
|
273
|
+
const emptyFiles = [];
|
|
274
|
+
if (!isControlledTitle) {
|
|
275
|
+
setInternalTitle(emptyTitle);
|
|
276
|
+
}
|
|
277
|
+
if (!isControlledContent) {
|
|
278
|
+
setInternalContent(emptyContent);
|
|
279
|
+
}
|
|
280
|
+
if (!isControlledAttachedFiles) {
|
|
281
|
+
setInternalAttachedFiles(emptyFiles);
|
|
282
|
+
}
|
|
283
|
+
onChangeTitle == null ? void 0 : onChangeTitle(emptyTitle);
|
|
284
|
+
onChangeContent == null ? void 0 : onChangeContent(emptyContent);
|
|
285
|
+
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(emptyFiles);
|
|
286
|
+
}, [
|
|
287
|
+
isControlledTitle,
|
|
288
|
+
isControlledContent,
|
|
289
|
+
isControlledAttachedFiles,
|
|
290
|
+
onChangeTitle,
|
|
291
|
+
onChangeContent,
|
|
292
|
+
onChangeAttachedFiles
|
|
293
|
+
]);
|
|
212
294
|
const handleSaveClick = (0, import_react.useCallback)(() => {
|
|
213
295
|
onClickSave == null ? void 0 : onClickSave({
|
|
214
296
|
title,
|
|
@@ -266,7 +348,10 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
266
348
|
onChange: handleOnChangeContent,
|
|
267
349
|
ref: editorRef,
|
|
268
350
|
theme: "snow",
|
|
269
|
-
value: content
|
|
351
|
+
value: content,
|
|
352
|
+
modules: {
|
|
353
|
+
toolbar: toolbarOptions
|
|
354
|
+
}
|
|
270
355
|
}, quillProps)
|
|
271
356
|
),
|
|
272
357
|
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}\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\nconst toolbarOptions = [\n ['bold', 'italic', 'underline', 'strike'], // toggled buttons\n ['blockquote'],\n ['link'],\n [{ header: 1 }, { header: 2 }, { header: 3 }], // 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.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
|
@@ -21,6 +21,24 @@ 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
|
+
["bold", "italic", "underline", "strike"],
|
|
26
|
+
// toggled buttons
|
|
27
|
+
["blockquote"],
|
|
28
|
+
["link"],
|
|
29
|
+
[{ header: 1 }, { header: 2 }, { header: 3 }],
|
|
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: [] }], // dropdown with defaults from theme
|
|
38
|
+
// [{ font: [] }],
|
|
39
|
+
// [{ align: [] }],
|
|
40
|
+
// ['clean'], // remove formatting button
|
|
41
|
+
];
|
|
24
42
|
var Editor = forwardRef(
|
|
25
43
|
(props, ref) => {
|
|
26
44
|
const _a = props, {
|
|
@@ -38,7 +56,13 @@ var Editor = forwardRef(
|
|
|
38
56
|
height,
|
|
39
57
|
hideHeader,
|
|
40
58
|
hideFileAttachment,
|
|
41
|
-
hideFooter
|
|
59
|
+
hideFooter,
|
|
60
|
+
title: controlledTitle,
|
|
61
|
+
onChangeTitle,
|
|
62
|
+
content: controlledContent,
|
|
63
|
+
onChangeContent,
|
|
64
|
+
attachedFiles: controlledAttachedFiles,
|
|
65
|
+
onChangeAttachedFiles
|
|
42
66
|
} = _a, quillProps = __objRest(_a, [
|
|
43
67
|
"defaultAttachedFiles",
|
|
44
68
|
"defaultTitle",
|
|
@@ -54,19 +78,35 @@ var Editor = forwardRef(
|
|
|
54
78
|
"height",
|
|
55
79
|
"hideHeader",
|
|
56
80
|
"hideFileAttachment",
|
|
57
|
-
"hideFooter"
|
|
81
|
+
"hideFooter",
|
|
82
|
+
"title",
|
|
83
|
+
"onChangeTitle",
|
|
84
|
+
"content",
|
|
85
|
+
"onChangeContent",
|
|
86
|
+
"attachedFiles",
|
|
87
|
+
"onChangeAttachedFiles"
|
|
58
88
|
]);
|
|
59
89
|
const defaultRef = useRef(null);
|
|
60
90
|
const editorRef = ref || defaultRef;
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
91
|
+
const isControlledTitle = controlledTitle !== void 0;
|
|
92
|
+
const isControlledContent = controlledContent !== void 0;
|
|
93
|
+
const isControlledAttachedFiles = controlledAttachedFiles !== void 0;
|
|
94
|
+
const [internalAttachedFiles, setInternalAttachedFiles] = useState(defaultAttachedFiles || []);
|
|
64
95
|
const [fileDeleteLoading, setFileDeleteLoading] = useState(/* @__PURE__ */ new Set());
|
|
65
|
-
const [
|
|
66
|
-
const [
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
96
|
+
const [internalTitle, setInternalTitle] = useState(defaultTitle || "");
|
|
97
|
+
const [internalContent, setInternalContent] = useState(defaultValue || "");
|
|
98
|
+
const title = isControlledTitle ? controlledTitle : internalTitle;
|
|
99
|
+
const content = isControlledContent ? controlledContent : internalContent;
|
|
100
|
+
const attachedFiles = isControlledAttachedFiles ? controlledAttachedFiles : internalAttachedFiles;
|
|
101
|
+
const handleOnChangeContent = useCallback(
|
|
102
|
+
(value) => {
|
|
103
|
+
if (!isControlledContent) {
|
|
104
|
+
setInternalContent(value);
|
|
105
|
+
}
|
|
106
|
+
onChangeContent == null ? void 0 : onChangeContent(value);
|
|
107
|
+
},
|
|
108
|
+
[isControlledContent, onChangeContent]
|
|
109
|
+
);
|
|
70
110
|
const handleButtonClick = useCallback(() => {
|
|
71
111
|
let input = document.createElement("input");
|
|
72
112
|
input.type = "file";
|
|
@@ -80,20 +120,31 @@ var Editor = forwardRef(
|
|
|
80
120
|
const fileName = file.name;
|
|
81
121
|
const attachment = yield uploadFile == null ? void 0 : uploadFile(file, `hr-notes/${fileName}`);
|
|
82
122
|
if (attachment) {
|
|
83
|
-
|
|
123
|
+
const newFiles = [...attachedFiles, attachment];
|
|
124
|
+
if (!isControlledAttachedFiles) {
|
|
125
|
+
setInternalAttachedFiles(newFiles);
|
|
126
|
+
}
|
|
127
|
+
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(newFiles);
|
|
84
128
|
}
|
|
85
129
|
input = null;
|
|
86
130
|
});
|
|
87
131
|
input.click();
|
|
88
|
-
}, [
|
|
132
|
+
}, [
|
|
133
|
+
uploadFile,
|
|
134
|
+
attachedFiles,
|
|
135
|
+
isControlledAttachedFiles,
|
|
136
|
+
onChangeAttachedFiles
|
|
137
|
+
]);
|
|
89
138
|
const handleDeleteFile = useCallback(
|
|
90
139
|
(fileUrl) => __async(null, null, function* () {
|
|
91
140
|
try {
|
|
92
141
|
setFileDeleteLoading((p) => p.add(fileUrl));
|
|
93
142
|
yield deleteFile == null ? void 0 : deleteFile(fileUrl);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
143
|
+
const newFiles = attachedFiles.filter((item) => item.url !== fileUrl);
|
|
144
|
+
if (!isControlledAttachedFiles) {
|
|
145
|
+
setInternalAttachedFiles(newFiles);
|
|
146
|
+
}
|
|
147
|
+
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(newFiles);
|
|
97
148
|
} catch (err) {
|
|
98
149
|
toast.error("\uD30C\uC77C \uC0AD\uC81C\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
|
|
99
150
|
} finally {
|
|
@@ -103,7 +154,12 @@ var Editor = forwardRef(
|
|
|
103
154
|
});
|
|
104
155
|
}
|
|
105
156
|
}),
|
|
106
|
-
[
|
|
157
|
+
[
|
|
158
|
+
deleteFile,
|
|
159
|
+
attachedFiles,
|
|
160
|
+
isControlledAttachedFiles,
|
|
161
|
+
onChangeAttachedFiles
|
|
162
|
+
]
|
|
107
163
|
);
|
|
108
164
|
const renderAttachedFiles = useCallback(() => {
|
|
109
165
|
return /* @__PURE__ */ jsx(Box, { width: "100%", children: attachedFiles.map((file) => {
|
|
@@ -139,14 +195,40 @@ var Editor = forwardRef(
|
|
|
139
195
|
] });
|
|
140
196
|
}) });
|
|
141
197
|
}, [attachedFiles, fileDeleteLoading, handleDeleteFile]);
|
|
142
|
-
const handleOnChangeTitle = useCallback(
|
|
143
|
-
|
|
144
|
-
|
|
198
|
+
const handleOnChangeTitle = useCallback(
|
|
199
|
+
(e) => {
|
|
200
|
+
const newTitle = e.target.value;
|
|
201
|
+
if (!isControlledTitle) {
|
|
202
|
+
setInternalTitle(newTitle);
|
|
203
|
+
}
|
|
204
|
+
onChangeTitle == null ? void 0 : onChangeTitle(newTitle);
|
|
205
|
+
},
|
|
206
|
+
[isControlledTitle, onChangeTitle]
|
|
207
|
+
);
|
|
145
208
|
const clearEditorState = useCallback(() => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
209
|
+
const emptyTitle = "";
|
|
210
|
+
const emptyContent = "";
|
|
211
|
+
const emptyFiles = [];
|
|
212
|
+
if (!isControlledTitle) {
|
|
213
|
+
setInternalTitle(emptyTitle);
|
|
214
|
+
}
|
|
215
|
+
if (!isControlledContent) {
|
|
216
|
+
setInternalContent(emptyContent);
|
|
217
|
+
}
|
|
218
|
+
if (!isControlledAttachedFiles) {
|
|
219
|
+
setInternalAttachedFiles(emptyFiles);
|
|
220
|
+
}
|
|
221
|
+
onChangeTitle == null ? void 0 : onChangeTitle(emptyTitle);
|
|
222
|
+
onChangeContent == null ? void 0 : onChangeContent(emptyContent);
|
|
223
|
+
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(emptyFiles);
|
|
224
|
+
}, [
|
|
225
|
+
isControlledTitle,
|
|
226
|
+
isControlledContent,
|
|
227
|
+
isControlledAttachedFiles,
|
|
228
|
+
onChangeTitle,
|
|
229
|
+
onChangeContent,
|
|
230
|
+
onChangeAttachedFiles
|
|
231
|
+
]);
|
|
150
232
|
const handleSaveClick = useCallback(() => {
|
|
151
233
|
onClickSave == null ? void 0 : onClickSave({
|
|
152
234
|
title,
|
|
@@ -204,7 +286,10 @@ var Editor = forwardRef(
|
|
|
204
286
|
onChange: handleOnChangeContent,
|
|
205
287
|
ref: editorRef,
|
|
206
288
|
theme: "snow",
|
|
207
|
-
value: content
|
|
289
|
+
value: content,
|
|
290
|
+
modules: {
|
|
291
|
+
toolbar: toolbarOptions
|
|
292
|
+
}
|
|
208
293
|
}, quillProps)
|
|
209
294
|
),
|
|
210
295
|
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}\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\nconst toolbarOptions = [\n ['bold', 'italic', 'underline', 'strike'], // toggled buttons\n ['blockquote'],\n ['link'],\n [{ header: 1 }, { header: 2 }, { header: 3 }], // 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
|
@@ -46,8 +46,33 @@ 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
|
|
|
57
|
+
const toolbarOptions = [
|
|
58
|
+
['bold', 'italic', 'underline', 'strike'], // toggled buttons
|
|
59
|
+
['blockquote'],
|
|
60
|
+
['link'],
|
|
61
|
+
[{ header: 1 }, { header: 2 }, { header: 3 }], // 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
|
+
|
|
51
76
|
export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
52
77
|
(props, ref): React.ReactNode => {
|
|
53
78
|
const {
|
|
@@ -66,21 +91,45 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
66
91
|
hideHeader,
|
|
67
92
|
hideFileAttachment,
|
|
68
93
|
hideFooter,
|
|
94
|
+
title: controlledTitle,
|
|
95
|
+
onChangeTitle,
|
|
96
|
+
content: controlledContent,
|
|
97
|
+
onChangeContent,
|
|
98
|
+
attachedFiles: controlledAttachedFiles,
|
|
99
|
+
onChangeAttachedFiles,
|
|
69
100
|
...quillProps
|
|
70
101
|
} = props;
|
|
71
102
|
const defaultRef = useRef<ReactQuill>(null);
|
|
72
103
|
const editorRef = ref || defaultRef;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
104
|
+
// Controlled vs Uncontrolled 모드 구분
|
|
105
|
+
const isControlledTitle = controlledTitle !== undefined;
|
|
106
|
+
const isControlledContent = controlledContent !== undefined;
|
|
107
|
+
const isControlledAttachedFiles = controlledAttachedFiles !== undefined;
|
|
108
|
+
|
|
109
|
+
const [internalAttachedFiles, setInternalAttachedFiles] = useState<
|
|
110
|
+
Attachment[]
|
|
111
|
+
>(defaultAttachedFiles || []);
|
|
76
112
|
const [fileDeleteLoading, setFileDeleteLoading] = useState(new Set());
|
|
77
113
|
|
|
78
|
-
const [
|
|
79
|
-
const [
|
|
114
|
+
const [internalTitle, setInternalTitle] = useState(defaultTitle || '');
|
|
115
|
+
const [internalContent, setInternalContent] = useState(defaultValue || '');
|
|
80
116
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
117
|
+
// 실제 사용할 값들 (controlled일 때는 props 값, uncontrolled일 때는 internal state 값)
|
|
118
|
+
const title = isControlledTitle ? controlledTitle : internalTitle;
|
|
119
|
+
const content = isControlledContent ? controlledContent : internalContent;
|
|
120
|
+
const attachedFiles = isControlledAttachedFiles
|
|
121
|
+
? controlledAttachedFiles
|
|
122
|
+
: internalAttachedFiles;
|
|
123
|
+
|
|
124
|
+
const handleOnChangeContent = useCallback(
|
|
125
|
+
(value: string) => {
|
|
126
|
+
if (!isControlledContent) {
|
|
127
|
+
setInternalContent(value);
|
|
128
|
+
}
|
|
129
|
+
onChangeContent?.(value);
|
|
130
|
+
},
|
|
131
|
+
[isControlledContent, onChangeContent]
|
|
132
|
+
);
|
|
84
133
|
|
|
85
134
|
const handleButtonClick = useCallback(() => {
|
|
86
135
|
let input: HTMLInputElement | null = document.createElement('input');
|
|
@@ -96,21 +145,32 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
96
145
|
const fileName = file.name;
|
|
97
146
|
const attachment = await uploadFile?.(file, `hr-notes/${fileName}`);
|
|
98
147
|
if (attachment) {
|
|
99
|
-
|
|
148
|
+
const newFiles = [...attachedFiles, attachment];
|
|
149
|
+
if (!isControlledAttachedFiles) {
|
|
150
|
+
setInternalAttachedFiles(newFiles);
|
|
151
|
+
}
|
|
152
|
+
onChangeAttachedFiles?.(newFiles);
|
|
100
153
|
}
|
|
101
154
|
input = null;
|
|
102
155
|
};
|
|
103
156
|
input.click();
|
|
104
|
-
}, [
|
|
157
|
+
}, [
|
|
158
|
+
uploadFile,
|
|
159
|
+
attachedFiles,
|
|
160
|
+
isControlledAttachedFiles,
|
|
161
|
+
onChangeAttachedFiles,
|
|
162
|
+
]);
|
|
105
163
|
|
|
106
164
|
const handleDeleteFile = useCallback(
|
|
107
165
|
async (fileUrl: string) => {
|
|
108
166
|
try {
|
|
109
167
|
setFileDeleteLoading((p) => p.add(fileUrl));
|
|
110
168
|
await deleteFile?.(fileUrl);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
169
|
+
const newFiles = attachedFiles.filter((item) => item.url !== fileUrl);
|
|
170
|
+
if (!isControlledAttachedFiles) {
|
|
171
|
+
setInternalAttachedFiles(newFiles);
|
|
172
|
+
}
|
|
173
|
+
onChangeAttachedFiles?.(newFiles);
|
|
114
174
|
} catch (err) {
|
|
115
175
|
toast.error('파일 삭제에 실패했습니다.');
|
|
116
176
|
} finally {
|
|
@@ -120,7 +180,12 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
120
180
|
});
|
|
121
181
|
}
|
|
122
182
|
},
|
|
123
|
-
[
|
|
183
|
+
[
|
|
184
|
+
deleteFile,
|
|
185
|
+
attachedFiles,
|
|
186
|
+
isControlledAttachedFiles,
|
|
187
|
+
onChangeAttachedFiles,
|
|
188
|
+
]
|
|
124
189
|
);
|
|
125
190
|
|
|
126
191
|
const renderAttachedFiles = useCallback(() => {
|
|
@@ -162,15 +227,44 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
162
227
|
|
|
163
228
|
const handleOnChangeTitle = useCallback<
|
|
164
229
|
React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>
|
|
165
|
-
>(
|
|
166
|
-
|
|
167
|
-
|
|
230
|
+
>(
|
|
231
|
+
(e) => {
|
|
232
|
+
const newTitle = e.target.value;
|
|
233
|
+
if (!isControlledTitle) {
|
|
234
|
+
setInternalTitle(newTitle);
|
|
235
|
+
}
|
|
236
|
+
onChangeTitle?.(newTitle);
|
|
237
|
+
},
|
|
238
|
+
[isControlledTitle, onChangeTitle]
|
|
239
|
+
);
|
|
168
240
|
|
|
169
241
|
const clearEditorState = useCallback(() => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
242
|
+
const emptyTitle = '';
|
|
243
|
+
const emptyContent = '';
|
|
244
|
+
const emptyFiles: Attachment[] = [];
|
|
245
|
+
|
|
246
|
+
if (!isControlledTitle) {
|
|
247
|
+
setInternalTitle(emptyTitle);
|
|
248
|
+
}
|
|
249
|
+
if (!isControlledContent) {
|
|
250
|
+
setInternalContent(emptyContent);
|
|
251
|
+
}
|
|
252
|
+
if (!isControlledAttachedFiles) {
|
|
253
|
+
setInternalAttachedFiles(emptyFiles);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// controlled 모드일 때도 부모에게 알림
|
|
257
|
+
onChangeTitle?.(emptyTitle);
|
|
258
|
+
onChangeContent?.(emptyContent);
|
|
259
|
+
onChangeAttachedFiles?.(emptyFiles);
|
|
260
|
+
}, [
|
|
261
|
+
isControlledTitle,
|
|
262
|
+
isControlledContent,
|
|
263
|
+
isControlledAttachedFiles,
|
|
264
|
+
onChangeTitle,
|
|
265
|
+
onChangeContent,
|
|
266
|
+
onChangeAttachedFiles,
|
|
267
|
+
]);
|
|
174
268
|
|
|
175
269
|
const handleSaveClick = useCallback(() => {
|
|
176
270
|
onClickSave?.({
|
|
@@ -231,6 +325,9 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
231
325
|
ref={editorRef}
|
|
232
326
|
theme="snow"
|
|
233
327
|
value={content}
|
|
328
|
+
modules={{
|
|
329
|
+
toolbar: toolbarOptions,
|
|
330
|
+
}}
|
|
234
331
|
{...quillProps}
|
|
235
332
|
/>
|
|
236
333
|
{renderAttachedFiles()}
|