@tipp/ui-quill-editor 4.0.9 → 4.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/editor.cjs +100 -34
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.d.cts +10 -0
- package/dist/editor.d.ts +10 -0
- package/dist/editor.js +100 -34
- package/dist/editor.js.map +1 -1
- package/package.json +3 -3
- package/src/editor.tsx +112 -41
package/dist/editor.cjs
CHANGED
|
@@ -99,7 +99,14 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
99
99
|
maxHeight,
|
|
100
100
|
height,
|
|
101
101
|
hideHeader,
|
|
102
|
-
hideFileAttachment
|
|
102
|
+
hideFileAttachment,
|
|
103
|
+
hideFooter,
|
|
104
|
+
title: controlledTitle,
|
|
105
|
+
onChangeTitle,
|
|
106
|
+
content: controlledContent,
|
|
107
|
+
onChangeContent,
|
|
108
|
+
attachedFiles: controlledAttachedFiles,
|
|
109
|
+
onChangeAttachedFiles
|
|
103
110
|
} = _a, quillProps = __objRest(_a, [
|
|
104
111
|
"defaultAttachedFiles",
|
|
105
112
|
"defaultTitle",
|
|
@@ -114,19 +121,35 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
114
121
|
"maxHeight",
|
|
115
122
|
"height",
|
|
116
123
|
"hideHeader",
|
|
117
|
-
"hideFileAttachment"
|
|
124
|
+
"hideFileAttachment",
|
|
125
|
+
"hideFooter",
|
|
126
|
+
"title",
|
|
127
|
+
"onChangeTitle",
|
|
128
|
+
"content",
|
|
129
|
+
"onChangeContent",
|
|
130
|
+
"attachedFiles",
|
|
131
|
+
"onChangeAttachedFiles"
|
|
118
132
|
]);
|
|
119
133
|
const defaultRef = (0, import_react.useRef)(null);
|
|
120
134
|
const editorRef = ref || defaultRef;
|
|
121
|
-
const
|
|
135
|
+
const isControlledTitle = controlledTitle !== void 0;
|
|
136
|
+
const isControlledContent = controlledContent !== void 0;
|
|
137
|
+
const isControlledAttachedFiles = controlledAttachedFiles !== void 0;
|
|
138
|
+
const [internalAttachedFiles, setInternalAttachedFiles] = (0, import_react.useState)(
|
|
122
139
|
defaultAttachedFiles || []
|
|
123
140
|
);
|
|
124
141
|
const [fileDeleteLoading, setFileDeleteLoading] = (0, import_react.useState)(/* @__PURE__ */ new Set());
|
|
125
|
-
const [
|
|
126
|
-
const [
|
|
142
|
+
const [internalTitle, setInternalTitle] = (0, import_react.useState)(defaultTitle || "");
|
|
143
|
+
const [internalContent, setInternalContent] = (0, import_react.useState)(defaultValue || "");
|
|
144
|
+
const title = isControlledTitle ? controlledTitle : internalTitle;
|
|
145
|
+
const content = isControlledContent ? controlledContent : internalContent;
|
|
146
|
+
const attachedFiles = isControlledAttachedFiles ? controlledAttachedFiles : internalAttachedFiles;
|
|
127
147
|
const handleOnChangeContent = (0, import_react.useCallback)((value) => {
|
|
128
|
-
|
|
129
|
-
|
|
148
|
+
if (!isControlledContent) {
|
|
149
|
+
setInternalContent(value);
|
|
150
|
+
}
|
|
151
|
+
onChangeContent == null ? void 0 : onChangeContent(value);
|
|
152
|
+
}, [isControlledContent, onChangeContent]);
|
|
130
153
|
const handleButtonClick = (0, import_react.useCallback)(() => {
|
|
131
154
|
let input = document.createElement("input");
|
|
132
155
|
input.type = "file";
|
|
@@ -140,20 +163,26 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
140
163
|
const fileName = file.name;
|
|
141
164
|
const attachment = yield uploadFile == null ? void 0 : uploadFile(file, `hr-notes/${fileName}`);
|
|
142
165
|
if (attachment) {
|
|
143
|
-
|
|
166
|
+
const newFiles = [...attachedFiles, attachment];
|
|
167
|
+
if (!isControlledAttachedFiles) {
|
|
168
|
+
setInternalAttachedFiles(newFiles);
|
|
169
|
+
}
|
|
170
|
+
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(newFiles);
|
|
144
171
|
}
|
|
145
172
|
input = null;
|
|
146
173
|
});
|
|
147
174
|
input.click();
|
|
148
|
-
}, [uploadFile]);
|
|
175
|
+
}, [uploadFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]);
|
|
149
176
|
const handleDeleteFile = (0, import_react.useCallback)(
|
|
150
177
|
(fileUrl) => __async(null, null, function* () {
|
|
151
178
|
try {
|
|
152
179
|
setFileDeleteLoading((p) => p.add(fileUrl));
|
|
153
180
|
yield deleteFile == null ? void 0 : deleteFile(fileUrl);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
181
|
+
const newFiles = attachedFiles.filter((item) => item.url !== fileUrl);
|
|
182
|
+
if (!isControlledAttachedFiles) {
|
|
183
|
+
setInternalAttachedFiles(newFiles);
|
|
184
|
+
}
|
|
185
|
+
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(newFiles);
|
|
157
186
|
} catch (err) {
|
|
158
187
|
import_ui.toast.error("\uD30C\uC77C \uC0AD\uC81C\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
|
|
159
188
|
} finally {
|
|
@@ -163,7 +192,7 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
163
192
|
});
|
|
164
193
|
}
|
|
165
194
|
}),
|
|
166
|
-
[deleteFile]
|
|
195
|
+
[deleteFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]
|
|
167
196
|
);
|
|
168
197
|
const renderAttachedFiles = (0, import_react.useCallback)(() => {
|
|
169
198
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Box, { width: "100%", children: attachedFiles.map((file) => {
|
|
@@ -200,13 +229,29 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
200
229
|
}) });
|
|
201
230
|
}, [attachedFiles, fileDeleteLoading, handleDeleteFile]);
|
|
202
231
|
const handleOnChangeTitle = (0, import_react.useCallback)((e) => {
|
|
203
|
-
|
|
204
|
-
|
|
232
|
+
const newTitle = e.target.value;
|
|
233
|
+
if (!isControlledTitle) {
|
|
234
|
+
setInternalTitle(newTitle);
|
|
235
|
+
}
|
|
236
|
+
onChangeTitle == null ? void 0 : onChangeTitle(newTitle);
|
|
237
|
+
}, [isControlledTitle, onChangeTitle]);
|
|
205
238
|
const clearEditorState = (0, import_react.useCallback)(() => {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
239
|
+
const emptyTitle = "";
|
|
240
|
+
const emptyContent = "";
|
|
241
|
+
const emptyFiles = [];
|
|
242
|
+
if (!isControlledTitle) {
|
|
243
|
+
setInternalTitle(emptyTitle);
|
|
244
|
+
}
|
|
245
|
+
if (!isControlledContent) {
|
|
246
|
+
setInternalContent(emptyContent);
|
|
247
|
+
}
|
|
248
|
+
if (!isControlledAttachedFiles) {
|
|
249
|
+
setInternalAttachedFiles(emptyFiles);
|
|
250
|
+
}
|
|
251
|
+
onChangeTitle == null ? void 0 : onChangeTitle(emptyTitle);
|
|
252
|
+
onChangeContent == null ? void 0 : onChangeContent(emptyContent);
|
|
253
|
+
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(emptyFiles);
|
|
254
|
+
}, [isControlledTitle, isControlledContent, isControlledAttachedFiles, onChangeTitle, onChangeContent, onChangeAttachedFiles]);
|
|
210
255
|
const handleSaveClick = (0, import_react.useCallback)(() => {
|
|
211
256
|
onClickSave == null ? void 0 : onClickSave({
|
|
212
257
|
title,
|
|
@@ -267,24 +312,45 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
267
312
|
value: content
|
|
268
313
|
}, quillProps)
|
|
269
314
|
),
|
|
270
|
-
renderAttachedFiles()
|
|
271
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Separator, { size: "4" })
|
|
315
|
+
renderAttachedFiles()
|
|
272
316
|
] }),
|
|
273
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
274
|
-
|
|
275
|
-
|
|
317
|
+
hideFooter ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
318
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Separator, { size: "4" }),
|
|
319
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
320
|
+
import_ui.Flex,
|
|
276
321
|
{
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
322
|
+
align: "center",
|
|
323
|
+
justify: "between",
|
|
324
|
+
p: "2",
|
|
325
|
+
pl: "4",
|
|
326
|
+
pr: "4",
|
|
327
|
+
width: "100%",
|
|
328
|
+
children: [
|
|
329
|
+
hideFileAttachment ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
330
|
+
import_ui.Button,
|
|
331
|
+
{
|
|
332
|
+
color: "gray",
|
|
333
|
+
onClick: handleButtonClick,
|
|
334
|
+
variant: "transparent",
|
|
335
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Link2Icon, { height: 20, width: 20 })
|
|
336
|
+
}
|
|
337
|
+
),
|
|
338
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_ui.Flex, { gap: "2", children: [
|
|
339
|
+
clearEditor ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
340
|
+
import_ui.Button,
|
|
341
|
+
{
|
|
342
|
+
color: "gray",
|
|
343
|
+
onClick: clearEditorState,
|
|
344
|
+
variant: "outline",
|
|
345
|
+
children: "\uCD08\uAE30\uD654"
|
|
346
|
+
}
|
|
347
|
+
) : null,
|
|
348
|
+
SecondaryButton ? SecondaryButton : null,
|
|
349
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Button, { disabled: isLoading, onClick: handleSaveClick, children: "\uC800\uC7A5" })
|
|
350
|
+
] })
|
|
351
|
+
]
|
|
281
352
|
}
|
|
282
|
-
)
|
|
283
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_ui.Flex, { gap: "2", children: [
|
|
284
|
-
clearEditor ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Button, { color: "gray", onClick: clearEditorState, variant: "outline", children: "\uCD08\uAE30\uD654" }) : null,
|
|
285
|
-
SecondaryButton ? SecondaryButton : null,
|
|
286
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Button, { disabled: isLoading, onClick: handleSaveClick, children: "\uC800\uC7A5" })
|
|
287
|
-
] })
|
|
353
|
+
)
|
|
288
354
|
] })
|
|
289
355
|
]
|
|
290
356
|
}
|
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 hideHeader?: boolean;\n hideFileAttachment?: 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 ...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 <Separator size=\"4\" />\n </Grid>\n <Flex align=\"center\" justify=\"between\" p=\"2\" pl=\"4\" pr=\"4\" width=\"100%\">\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 color=\"gray\" onClick={clearEditorState} variant=\"outline\">\n 초기화\n </Button>\n ) : null}\n {SecondaryButton ? SecondaryButton : null}\n <Button disabled={isLoading} onClick={handleSaveClick}>\n 저장\n </Button>\n </Flex>\n </Flex>\n </div>\n );\n }\n);\n\nEditor.displayName = 'TIPP-Quill-Editor';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAiE;AACjE,gBAYO;AACP,6BAAuB;AA+GT;AA/EP,IAAM,aAAS;AAAA,EACpB,CAAC,OAAO,QAAyB;AAC/B,UAgBI,YAfF;AAAA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IA9DN,IAgEQ,IADC,uBACD,IADC;AAAA,MAdH;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA;AAGF,UAAM,iBAAa,qBAAmB,IAAI;AAC1C,UAAM,YAAY,OAAO;AACzB,UAAM,CAAC,eAAe,gBAAgB,QAAI;AAAA,MACxC,wBAAwB,CAAC;AAAA,IAC3B;AACA,UAAM,CAAC,mBAAmB,oBAAoB,QAAI,uBAAS,oBAAI,IAAI,CAAC;AAEpE,UAAM,CAAC,OAAO,QAAQ,QAAI,uBAAS,gBAAgB,EAAE;AACrD,UAAM,CAAC,SAAS,UAAU,QAAI,uBAAS,gBAAgB,EAAE;AAEzD,UAAM,4BAAwB,0BAAY,CAAC,UAAkB;AAC3D,iBAAW,KAAK;AAAA,IAClB,GAAG,CAAC,CAAC;AAEL,UAAM,wBAAoB,0BAAY,MAAM;AAC1C,UAAI,QAAiC,SAAS,cAAc,OAAO;AACnE,YAAM,OAAO;AACb,YAAM,WAAW,CAAO,UAAU;AAlFxC,YAAAA;AAmFQ,cAAM,QAAQA,MAAA,MAAM,OAA4B,UAAlC,gBAAAA,IAA0C;AACxD,YAAI,CAAC,MAAM;AAET,0BAAM,MAAM,0DAAa;AACzB;AAAA,QACF;AAEA,cAAM,WAAW,KAAK;AACtB,cAAM,aAAa,MAAM,yCAAa,MAAM,YAAY,QAAQ;AAChE,YAAI,YAAY;AACd,2BAAiB,CAAC,SAAS,CAAC,GAAG,MAAM,UAAU,CAAC;AAAA,QAClD;AACA,gBAAQ;AAAA,MACV;AACA,YAAM,MAAM;AAAA,IACd,GAAG,CAAC,UAAU,CAAC;AAEf,UAAM,uBAAmB;AAAA,MACvB,CAAO,YAAoB;AACzB,YAAI;AACF,+BAAqB,CAAC,MAAM,EAAE,IAAI,OAAO,CAAC;AAC1C,gBAAM,yCAAa;AACnB;AAAA,YAAiB,CAAC,iBAChB,aAAa,OAAO,CAAC,SAAS,KAAK,QAAQ,OAAO;AAAA,UACpD;AAAA,QACF,SAAS,KAAK;AACZ,0BAAM,MAAM,uEAAgB;AAAA,QAC9B,UAAE;AACA,+BAAqB,CAAC,MAAM;AAC1B,cAAE,OAAO,OAAO;AAChB,mBAAO;AAAA,UACT,CAAC;AAAA,QACH;AAAA,MACF;AAAA,MACA,CAAC,UAAU;AAAA,IACb;AAEA,UAAM,0BAAsB,0BAAY,MAAM;AAC5C,aACE,4CAAC,iBAAI,OAAM,QACR,wBAAc,IAAI,CAAC,SAAS;AAC3B,eACE,4EACE;AAAA,sDAAC,uBAAU,MAAK,KAAI;AAAA,UACpB;AAAA,YAAC;AAAA;AAAA,cACC,OAAM;AAAA,cACN,SAAQ;AAAA,cAER,GAAE;AAAA,cACF,OAAM;AAAA,cAEN;AAAA,4DAAC,kBAAK,MAAM,KAAK,KAAK,MAAK,KACzB,uDAAC,kBAAK,OAAM,UAAS,KAAI,KACvB;AAAA,8DAAC,sBAAS;AAAA,kBACT,KAAK;AAAA,mBACR,GACF;AAAA,gBACA;AAAA,kBAAC;AAAA;AAAA,oBACC,SAAS,kBAAkB,IAAI,KAAK,GAAG;AAAA,oBACvC,SAAS,MAAM;AACb,2BAAK,iBAAiB,KAAK,GAAG;AAAA,oBAChC;AAAA,oBACA,SAAQ;AAAA,oBACT;AAAA;AAAA,gBAED;AAAA;AAAA;AAAA,YAlBK,GAAG,KAAK,GAAG,IAAI,KAAK,QAAQ;AAAA,UAmBnC;AAAA,WACF;AAAA,MAEJ,CAAC,GACH;AAAA,IAEJ,GAAG,CAAC,eAAe,mBAAmB,gBAAgB,CAAC;AAEvD,UAAM,0BAAsB,0BAE1B,CAAC,MAAM;AACP,eAAS,EAAE,OAAO,KAAK;AAAA,IACzB,GAAG,CAAC,CAAC;AAEL,UAAM,uBAAmB,0BAAY,MAAM;AACzC,iBAAW,EAAE;AACb,uBAAiB,CAAC,CAAC;AACnB,eAAS,EAAE;AAAA,IACb,GAAG,CAAC,CAAC;AAEL,UAAM,sBAAkB,0BAAY,MAAM;AACxC,iDAAc;AAAA,QACZ;AAAA,QACA;AAAA,QACA,OAAO;AAAA,MACT;AAAA,IACF,GAAG,CAAC,aAAa,OAAO,SAAS,aAAa,CAAC;AAE/C,QAAI,MAAM,aAAa;AACrB,YAAM,YAAY,UAAU;AAAA,IAC9B;AAEA,UAAM,eAAe;AAAA,MACnB,gBAAgB;AAAA,MAChB,gBAAgB;AAAA,MAChB,YAAY,UAAU;AAAA,IACxB;AAEA,WACE;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,OAAO,mBACF;AAAA,QAGL;AAAA,uDAAC,kBAAK,QAAO,QAAO,MAAM,GAAG,aAAa,KAAK,UAAU,QAEtD;AAAA,yBAAa,OACZ,4EACE;AAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,OAAM;AAAA,kBACN,SAAQ;AAAA,kBACR,KAAI;AAAA,kBACJ,QAAO;AAAA,kBACP,IAAG;AAAA,kBACH,IAAG;AAAA,kBACH,OAAM;AAAA,kBAEN;AAAA,gEAAC,iBAAI,IAAG,KAAI,IAAG,KACb,sDAAC,kBAAK,0BAAE,GACV;AAAA,oBACA,4CAAC,uBAAU,aAAY,YAAW,OAAO,EAAE,QAAQ,OAAO,GAAG;AAAA,oBAC7D;AAAA,sBAAC,oBAAU;AAAA,sBAAV;AAAA,wBACC,WAAU;AAAA,wBACV,UAAU;AAAA,wBACV,aAAY;AAAA,wBACZ,OAAO;AAAA;AAAA,oBACT;AAAA;AAAA;AAAA,cACF;AAAA,cACA,4CAAC,uBAAU,aAAY,cAAa,MAAK,KAAI;AAAA,eAC/C;AAAA,YAGF;AAAA,cAAC,uBAAAC;AAAA,cAAA;AAAA,gBACC,WAAU;AAAA,gBACV,UAAU;AAAA,gBACV,KAAK;AAAA,gBACL,OAAM;AAAA,gBACN,OAAO;AAAA,iBACH;AAAA,YACN;AAAA,YACC,oBAAoB;AAAA,YACrB,4CAAC,uBAAU,MAAK,KAAI;AAAA,aACtB;AAAA,UACA,6CAAC,kBAAK,OAAM,UAAS,SAAQ,WAAU,GAAE,KAAI,IAAG,KAAI,IAAG,KAAI,OAAM,QAC9D;AAAA,iCACC,4CAAC,SAAI,IAEL;AAAA,cAAC;AAAA;AAAA,gBACC,OAAM;AAAA,gBACN,SAAS;AAAA,gBACT,SAAQ;AAAA,gBAER,sDAAC,uBAAU,QAAQ,IAAI,OAAO,IAAI;AAAA;AAAA,YACpC;AAAA,YAGF,6CAAC,kBAAK,KAAI,KACP;AAAA,4BACC,4CAAC,oBAAO,OAAM,QAAO,SAAS,kBAAkB,SAAQ,WAAU,gCAElE,IACE;AAAA,cACH,kBAAkB,kBAAkB;AAAA,cACrC,4CAAC,oBAAO,UAAU,WAAW,SAAS,iBAAiB,0BAEvD;AAAA,eACF;AAAA,aACF;AAAA;AAAA;AAAA,IACF;AAAA,EAEJ;AACF;AAEA,OAAO,cAAc;","names":["_a","ReactQuill"]}
|
|
1
|
+
{"version":3,"sources":["../src/editor.tsx"],"sourcesContent":["import React, { forwardRef, useCallback, useRef, useState } from 'react';\nimport {\n Box,\n Button,\n Flex,\n Grid,\n Link,\n Separator,\n TextField,\n Typo,\n Link2Icon,\n toast,\n FileIcon,\n} from '@tipp/ui';\nimport ReactQuill from 'react-quill-new';\nimport type { Attachment } from './type';\n\nexport interface TippEditorProps extends ReactQuill.ReactQuillProps {\n defaultTitle?: string;\n defaultValue?: string;\n defaultAttachedFiles?: Attachment[];\n /** 저장하기 버튼 클릭 시 실행 */\n onClickSave?: (values: {\n title: string;\n content: string;\n files: Attachment[];\n }) => void;\n /** 파일 업로드 버튼 클릭 시 실행 */\n uploadFile?: (\n file: File,\n destination: string\n ) => Promise<Attachment | undefined>;\n deleteFile?: (fileUrl: string) => Promise<void>;\n /** 외부에서 Editor를 빈 상태로 초기화 시켜야 할 때 사용 */\n clearEditor?: React.MutableRefObject<(() => void) | undefined>;\n /** 초기화 버튼말고 다른 버튼 추가시 */\n SecondaryButton?: React.ReactNode;\n /** true인 경우 저장하기 버튼이 비활성 화 됨. 연타 방지 */\n isLoading?: boolean;\n minHeight?: string;\n maxHeight?: string;\n height?: string;\n /** 제목 입력창 숨김 */\n hideHeader?: boolean;\n /** 첨부 파일 버튼 숨김 */\n hideFileAttachment?: boolean;\n /** 저장 버튼 footer 숨김 */\n hideFooter?: boolean;\n title?: string;\n onChangeTitle?: (value: string) => void;\n content?: string;\n onChangeContent?: (value: string) => void;\n attachedFiles?: Attachment[];\n onChangeAttachedFiles?: (files: Attachment[]) => void;\n}\n\nexport const Editor = forwardRef<ReactQuill, TippEditorProps>(\n (props, ref): React.ReactNode => {\n const {\n defaultAttachedFiles,\n defaultTitle,\n defaultValue,\n onClickSave,\n uploadFile,\n deleteFile,\n isLoading,\n SecondaryButton,\n clearEditor,\n minHeight,\n maxHeight,\n height,\n hideHeader,\n hideFileAttachment,\n hideFooter,\n title: controlledTitle,\n onChangeTitle,\n content: controlledContent,\n onChangeContent,\n attachedFiles: controlledAttachedFiles,\n onChangeAttachedFiles,\n ...quillProps\n } = props;\n const defaultRef = useRef<ReactQuill>(null);\n const editorRef = ref || defaultRef;\n // Controlled vs Uncontrolled 모드 구분\n const isControlledTitle = controlledTitle !== undefined;\n const isControlledContent = controlledContent !== undefined;\n const isControlledAttachedFiles = controlledAttachedFiles !== undefined;\n\n const [internalAttachedFiles, setInternalAttachedFiles] = useState<Attachment[]>(\n defaultAttachedFiles || []\n );\n const [fileDeleteLoading, setFileDeleteLoading] = useState(new Set());\n\n const [internalTitle, setInternalTitle] = useState(defaultTitle || '');\n const [internalContent, setInternalContent] = useState(defaultValue || '');\n\n // 실제 사용할 값들 (controlled일 때는 props 값, uncontrolled일 때는 internal state 값)\n const title = isControlledTitle ? controlledTitle : internalTitle;\n const content = isControlledContent ? controlledContent : internalContent;\n const attachedFiles = isControlledAttachedFiles ? controlledAttachedFiles : internalAttachedFiles;\n\n const handleOnChangeContent = useCallback((value: string) => {\n if (!isControlledContent) {\n setInternalContent(value);\n }\n onChangeContent?.(value);\n }, [isControlledContent, onChangeContent]);\n\n const handleButtonClick = useCallback(() => {\n let input: HTMLInputElement | null = document.createElement('input');\n input.type = 'file';\n input.onchange = async (event) => {\n const file = (event.target as HTMLInputElement).files?.[0];\n if (!file) {\n // console.log('DEBUG: no file');\n toast.error('파일을 선택해주세요.');\n return;\n }\n\n const fileName = file.name;\n const attachment = await uploadFile?.(file, `hr-notes/${fileName}`);\n if (attachment) {\n const newFiles = [...attachedFiles, attachment];\n if (!isControlledAttachedFiles) {\n setInternalAttachedFiles(newFiles);\n }\n onChangeAttachedFiles?.(newFiles);\n }\n input = null;\n };\n input.click();\n }, [uploadFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]);\n\n const handleDeleteFile = useCallback(\n async (fileUrl: string) => {\n try {\n setFileDeleteLoading((p) => p.add(fileUrl));\n await deleteFile?.(fileUrl);\n const newFiles = attachedFiles.filter((item) => item.url !== fileUrl);\n if (!isControlledAttachedFiles) {\n setInternalAttachedFiles(newFiles);\n }\n onChangeAttachedFiles?.(newFiles);\n } catch (err) {\n toast.error('파일 삭제에 실패했습니다.');\n } finally {\n setFileDeleteLoading((p) => {\n p.delete(fileUrl);\n return p;\n });\n }\n },\n [deleteFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]\n );\n\n const renderAttachedFiles = useCallback(() => {\n return (\n <Box width=\"100%\">\n {attachedFiles.map((file) => {\n return (\n <>\n <Separator size=\"4\" />\n <Flex\n align=\"center\"\n justify=\"between\"\n key={`${file.url}_${file.fileName}`}\n p=\"4\"\n width=\"100%\"\n >\n <Link href={file.url} size=\"2\">\n <Flex align=\"center\" gap=\"3\">\n <FileIcon />\n {file.fileName}\n </Flex>\n </Link>\n <Button\n loading={fileDeleteLoading.has(file.url)}\n onClick={() => {\n void handleDeleteFile(file.url);\n }}\n variant=\"ghost\"\n >\n 첨부 파일 삭제\n </Button>\n </Flex>\n </>\n );\n })}\n </Box>\n );\n }, [attachedFiles, fileDeleteLoading, handleDeleteFile]);\n\n const handleOnChangeTitle = useCallback<\n React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>\n >((e) => {\n const newTitle = e.target.value;\n if (!isControlledTitle) {\n setInternalTitle(newTitle);\n }\n onChangeTitle?.(newTitle);\n }, [isControlledTitle, onChangeTitle]);\n\n const clearEditorState = useCallback(() => {\n const emptyTitle = '';\n const emptyContent = '';\n const emptyFiles: Attachment[] = [];\n\n if (!isControlledTitle) {\n setInternalTitle(emptyTitle);\n }\n if (!isControlledContent) {\n setInternalContent(emptyContent);\n }\n if (!isControlledAttachedFiles) {\n setInternalAttachedFiles(emptyFiles);\n }\n\n // controlled 모드일 때도 부모에게 알림\n onChangeTitle?.(emptyTitle);\n onChangeContent?.(emptyContent);\n onChangeAttachedFiles?.(emptyFiles);\n }, [isControlledTitle, isControlledContent, isControlledAttachedFiles, onChangeTitle, onChangeContent, onChangeAttachedFiles]);\n\n const handleSaveClick = useCallback(() => {\n onClickSave?.({\n title,\n content,\n files: attachedFiles,\n });\n }, [onClickSave, title, content, attachedFiles]);\n\n if (props.clearEditor) {\n props.clearEditor.current = clearEditorState;\n }\n\n const cssVariables = {\n '--max-height': maxHeight,\n '--min-height': minHeight,\n '--height': height || '100%',\n } as React.CSSProperties;\n\n return (\n <div\n className=\"tipp-ql-wrapper\"\n style={{\n ...cssVariables,\n }}\n >\n <Grid height=\"100%\" rows={`${hideHeader ? '' : 'auto 1px'} 1fr`}>\n {/* 제목 입력창 */}\n {hideHeader ? null : (\n <>\n <Grid\n align=\"center\"\n columns=\"auto auto 1fr\"\n gap=\"2\"\n height=\"42px\"\n pl=\"2\"\n pr=\"3\"\n width=\"100%\"\n >\n <Box pl=\"3\" pr=\"3\">\n <Typo>제목</Typo>\n </Box>\n <Separator orientation=\"vertical\" style={{ height: '100%' }} />\n <TextField.Root\n className=\"editor-title-text-field\"\n onChange={handleOnChangeTitle}\n placeholder=\"제목을 입력해주세요\"\n value={title}\n />\n </Grid>\n <Separator orientation=\"horizontal\" size=\"4\" />\n </>\n )}\n\n <ReactQuill\n className=\"tipp-ql-editor write-mode\"\n onChange={handleOnChangeContent}\n ref={editorRef}\n theme=\"snow\"\n value={content}\n {...quillProps}\n />\n {renderAttachedFiles()}\n </Grid>\n\n {hideFooter ? null : (\n <>\n <Separator size=\"4\" />\n <Flex\n align=\"center\"\n justify=\"between\"\n p=\"2\"\n pl=\"4\"\n pr=\"4\"\n width=\"100%\"\n >\n {hideFileAttachment ? (\n <div />\n ) : (\n <Button\n color=\"gray\"\n onClick={handleButtonClick}\n variant=\"transparent\"\n >\n <Link2Icon height={20} width={20} />\n </Button>\n )}\n\n <Flex gap=\"2\">\n {clearEditor ? (\n <Button\n color=\"gray\"\n onClick={clearEditorState}\n variant=\"outline\"\n >\n 초기화\n </Button>\n ) : null}\n {SecondaryButton ? SecondaryButton : null}\n <Button disabled={isLoading} onClick={handleSaveClick}>\n 저장\n </Button>\n </Flex>\n </Flex>\n </>\n )}\n </div>\n );\n }\n);\n\nEditor.displayName = 'TIPP-Quill-Editor';\n"],"mappings":"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","names":["_a","ReactQuill"]}
|
package/dist/editor.d.cts
CHANGED
|
@@ -24,8 +24,18 @@ interface TippEditorProps extends ReactQuill.ReactQuillProps {
|
|
|
24
24
|
minHeight?: string;
|
|
25
25
|
maxHeight?: string;
|
|
26
26
|
height?: string;
|
|
27
|
+
/** 제목 입력창 숨김 */
|
|
27
28
|
hideHeader?: boolean;
|
|
29
|
+
/** 첨부 파일 버튼 숨김 */
|
|
28
30
|
hideFileAttachment?: boolean;
|
|
31
|
+
/** 저장 버튼 footer 숨김 */
|
|
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;
|
|
29
39
|
}
|
|
30
40
|
declare const Editor: React.ForwardRefExoticComponent<TippEditorProps & React.RefAttributes<ReactQuill>>;
|
|
31
41
|
|
package/dist/editor.d.ts
CHANGED
|
@@ -24,8 +24,18 @@ interface TippEditorProps extends ReactQuill.ReactQuillProps {
|
|
|
24
24
|
minHeight?: string;
|
|
25
25
|
maxHeight?: string;
|
|
26
26
|
height?: string;
|
|
27
|
+
/** 제목 입력창 숨김 */
|
|
27
28
|
hideHeader?: boolean;
|
|
29
|
+
/** 첨부 파일 버튼 숨김 */
|
|
28
30
|
hideFileAttachment?: boolean;
|
|
31
|
+
/** 저장 버튼 footer 숨김 */
|
|
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;
|
|
29
39
|
}
|
|
30
40
|
declare const Editor: React.ForwardRefExoticComponent<TippEditorProps & React.RefAttributes<ReactQuill>>;
|
|
31
41
|
|
package/dist/editor.js
CHANGED
|
@@ -37,7 +37,14 @@ var Editor = forwardRef(
|
|
|
37
37
|
maxHeight,
|
|
38
38
|
height,
|
|
39
39
|
hideHeader,
|
|
40
|
-
hideFileAttachment
|
|
40
|
+
hideFileAttachment,
|
|
41
|
+
hideFooter,
|
|
42
|
+
title: controlledTitle,
|
|
43
|
+
onChangeTitle,
|
|
44
|
+
content: controlledContent,
|
|
45
|
+
onChangeContent,
|
|
46
|
+
attachedFiles: controlledAttachedFiles,
|
|
47
|
+
onChangeAttachedFiles
|
|
41
48
|
} = _a, quillProps = __objRest(_a, [
|
|
42
49
|
"defaultAttachedFiles",
|
|
43
50
|
"defaultTitle",
|
|
@@ -52,19 +59,35 @@ var Editor = forwardRef(
|
|
|
52
59
|
"maxHeight",
|
|
53
60
|
"height",
|
|
54
61
|
"hideHeader",
|
|
55
|
-
"hideFileAttachment"
|
|
62
|
+
"hideFileAttachment",
|
|
63
|
+
"hideFooter",
|
|
64
|
+
"title",
|
|
65
|
+
"onChangeTitle",
|
|
66
|
+
"content",
|
|
67
|
+
"onChangeContent",
|
|
68
|
+
"attachedFiles",
|
|
69
|
+
"onChangeAttachedFiles"
|
|
56
70
|
]);
|
|
57
71
|
const defaultRef = useRef(null);
|
|
58
72
|
const editorRef = ref || defaultRef;
|
|
59
|
-
const
|
|
73
|
+
const isControlledTitle = controlledTitle !== void 0;
|
|
74
|
+
const isControlledContent = controlledContent !== void 0;
|
|
75
|
+
const isControlledAttachedFiles = controlledAttachedFiles !== void 0;
|
|
76
|
+
const [internalAttachedFiles, setInternalAttachedFiles] = useState(
|
|
60
77
|
defaultAttachedFiles || []
|
|
61
78
|
);
|
|
62
79
|
const [fileDeleteLoading, setFileDeleteLoading] = useState(/* @__PURE__ */ new Set());
|
|
63
|
-
const [
|
|
64
|
-
const [
|
|
80
|
+
const [internalTitle, setInternalTitle] = useState(defaultTitle || "");
|
|
81
|
+
const [internalContent, setInternalContent] = useState(defaultValue || "");
|
|
82
|
+
const title = isControlledTitle ? controlledTitle : internalTitle;
|
|
83
|
+
const content = isControlledContent ? controlledContent : internalContent;
|
|
84
|
+
const attachedFiles = isControlledAttachedFiles ? controlledAttachedFiles : internalAttachedFiles;
|
|
65
85
|
const handleOnChangeContent = useCallback((value) => {
|
|
66
|
-
|
|
67
|
-
|
|
86
|
+
if (!isControlledContent) {
|
|
87
|
+
setInternalContent(value);
|
|
88
|
+
}
|
|
89
|
+
onChangeContent == null ? void 0 : onChangeContent(value);
|
|
90
|
+
}, [isControlledContent, onChangeContent]);
|
|
68
91
|
const handleButtonClick = useCallback(() => {
|
|
69
92
|
let input = document.createElement("input");
|
|
70
93
|
input.type = "file";
|
|
@@ -78,20 +101,26 @@ var Editor = forwardRef(
|
|
|
78
101
|
const fileName = file.name;
|
|
79
102
|
const attachment = yield uploadFile == null ? void 0 : uploadFile(file, `hr-notes/${fileName}`);
|
|
80
103
|
if (attachment) {
|
|
81
|
-
|
|
104
|
+
const newFiles = [...attachedFiles, attachment];
|
|
105
|
+
if (!isControlledAttachedFiles) {
|
|
106
|
+
setInternalAttachedFiles(newFiles);
|
|
107
|
+
}
|
|
108
|
+
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(newFiles);
|
|
82
109
|
}
|
|
83
110
|
input = null;
|
|
84
111
|
});
|
|
85
112
|
input.click();
|
|
86
|
-
}, [uploadFile]);
|
|
113
|
+
}, [uploadFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]);
|
|
87
114
|
const handleDeleteFile = useCallback(
|
|
88
115
|
(fileUrl) => __async(null, null, function* () {
|
|
89
116
|
try {
|
|
90
117
|
setFileDeleteLoading((p) => p.add(fileUrl));
|
|
91
118
|
yield deleteFile == null ? void 0 : deleteFile(fileUrl);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
119
|
+
const newFiles = attachedFiles.filter((item) => item.url !== fileUrl);
|
|
120
|
+
if (!isControlledAttachedFiles) {
|
|
121
|
+
setInternalAttachedFiles(newFiles);
|
|
122
|
+
}
|
|
123
|
+
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(newFiles);
|
|
95
124
|
} catch (err) {
|
|
96
125
|
toast.error("\uD30C\uC77C \uC0AD\uC81C\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.");
|
|
97
126
|
} finally {
|
|
@@ -101,7 +130,7 @@ var Editor = forwardRef(
|
|
|
101
130
|
});
|
|
102
131
|
}
|
|
103
132
|
}),
|
|
104
|
-
[deleteFile]
|
|
133
|
+
[deleteFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]
|
|
105
134
|
);
|
|
106
135
|
const renderAttachedFiles = useCallback(() => {
|
|
107
136
|
return /* @__PURE__ */ jsx(Box, { width: "100%", children: attachedFiles.map((file) => {
|
|
@@ -138,13 +167,29 @@ var Editor = forwardRef(
|
|
|
138
167
|
}) });
|
|
139
168
|
}, [attachedFiles, fileDeleteLoading, handleDeleteFile]);
|
|
140
169
|
const handleOnChangeTitle = useCallback((e) => {
|
|
141
|
-
|
|
142
|
-
|
|
170
|
+
const newTitle = e.target.value;
|
|
171
|
+
if (!isControlledTitle) {
|
|
172
|
+
setInternalTitle(newTitle);
|
|
173
|
+
}
|
|
174
|
+
onChangeTitle == null ? void 0 : onChangeTitle(newTitle);
|
|
175
|
+
}, [isControlledTitle, onChangeTitle]);
|
|
143
176
|
const clearEditorState = useCallback(() => {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
177
|
+
const emptyTitle = "";
|
|
178
|
+
const emptyContent = "";
|
|
179
|
+
const emptyFiles = [];
|
|
180
|
+
if (!isControlledTitle) {
|
|
181
|
+
setInternalTitle(emptyTitle);
|
|
182
|
+
}
|
|
183
|
+
if (!isControlledContent) {
|
|
184
|
+
setInternalContent(emptyContent);
|
|
185
|
+
}
|
|
186
|
+
if (!isControlledAttachedFiles) {
|
|
187
|
+
setInternalAttachedFiles(emptyFiles);
|
|
188
|
+
}
|
|
189
|
+
onChangeTitle == null ? void 0 : onChangeTitle(emptyTitle);
|
|
190
|
+
onChangeContent == null ? void 0 : onChangeContent(emptyContent);
|
|
191
|
+
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(emptyFiles);
|
|
192
|
+
}, [isControlledTitle, isControlledContent, isControlledAttachedFiles, onChangeTitle, onChangeContent, onChangeAttachedFiles]);
|
|
148
193
|
const handleSaveClick = useCallback(() => {
|
|
149
194
|
onClickSave == null ? void 0 : onClickSave({
|
|
150
195
|
title,
|
|
@@ -205,24 +250,45 @@ var Editor = forwardRef(
|
|
|
205
250
|
value: content
|
|
206
251
|
}, quillProps)
|
|
207
252
|
),
|
|
208
|
-
renderAttachedFiles()
|
|
209
|
-
/* @__PURE__ */ jsx(Separator, { size: "4" })
|
|
253
|
+
renderAttachedFiles()
|
|
210
254
|
] }),
|
|
211
|
-
/* @__PURE__ */ jsxs(
|
|
212
|
-
|
|
213
|
-
|
|
255
|
+
hideFooter ? null : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
256
|
+
/* @__PURE__ */ jsx(Separator, { size: "4" }),
|
|
257
|
+
/* @__PURE__ */ jsxs(
|
|
258
|
+
Flex,
|
|
214
259
|
{
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
260
|
+
align: "center",
|
|
261
|
+
justify: "between",
|
|
262
|
+
p: "2",
|
|
263
|
+
pl: "4",
|
|
264
|
+
pr: "4",
|
|
265
|
+
width: "100%",
|
|
266
|
+
children: [
|
|
267
|
+
hideFileAttachment ? /* @__PURE__ */ jsx("div", {}) : /* @__PURE__ */ jsx(
|
|
268
|
+
Button,
|
|
269
|
+
{
|
|
270
|
+
color: "gray",
|
|
271
|
+
onClick: handleButtonClick,
|
|
272
|
+
variant: "transparent",
|
|
273
|
+
children: /* @__PURE__ */ jsx(Link2Icon, { height: 20, width: 20 })
|
|
274
|
+
}
|
|
275
|
+
),
|
|
276
|
+
/* @__PURE__ */ jsxs(Flex, { gap: "2", children: [
|
|
277
|
+
clearEditor ? /* @__PURE__ */ jsx(
|
|
278
|
+
Button,
|
|
279
|
+
{
|
|
280
|
+
color: "gray",
|
|
281
|
+
onClick: clearEditorState,
|
|
282
|
+
variant: "outline",
|
|
283
|
+
children: "\uCD08\uAE30\uD654"
|
|
284
|
+
}
|
|
285
|
+
) : null,
|
|
286
|
+
SecondaryButton ? SecondaryButton : null,
|
|
287
|
+
/* @__PURE__ */ jsx(Button, { disabled: isLoading, onClick: handleSaveClick, children: "\uC800\uC7A5" })
|
|
288
|
+
] })
|
|
289
|
+
]
|
|
219
290
|
}
|
|
220
|
-
)
|
|
221
|
-
/* @__PURE__ */ jsxs(Flex, { gap: "2", children: [
|
|
222
|
-
clearEditor ? /* @__PURE__ */ jsx(Button, { color: "gray", onClick: clearEditorState, variant: "outline", children: "\uCD08\uAE30\uD654" }) : null,
|
|
223
|
-
SecondaryButton ? SecondaryButton : null,
|
|
224
|
-
/* @__PURE__ */ jsx(Button, { disabled: isLoading, onClick: handleSaveClick, children: "\uC800\uC7A5" })
|
|
225
|
-
] })
|
|
291
|
+
)
|
|
226
292
|
] })
|
|
227
293
|
]
|
|
228
294
|
}
|
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 hideHeader?: boolean;\n hideFileAttachment?: 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 ...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 <Separator size=\"4\" />\n </Grid>\n <Flex align=\"center\" justify=\"between\" p=\"2\" pl=\"4\" pr=\"4\" width=\"100%\">\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 color=\"gray\" onClick={clearEditorState} variant=\"outline\">\n 초기화\n </Button>\n ) : null}\n {SecondaryButton ? SecondaryButton : null}\n <Button disabled={isLoading} onClick={handleSaveClick}>\n 저장\n </Button>\n </Flex>\n </Flex>\n </div>\n );\n }\n);\n\nEditor.displayName = 'TIPP-Quill-Editor';\n"],"mappings":"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","names":["_a"]}
|
|
1
|
+
{"version":3,"sources":["../src/editor.tsx"],"sourcesContent":["import React, { forwardRef, useCallback, useRef, useState } from 'react';\nimport {\n Box,\n Button,\n Flex,\n Grid,\n Link,\n Separator,\n TextField,\n Typo,\n Link2Icon,\n toast,\n FileIcon,\n} from '@tipp/ui';\nimport ReactQuill from 'react-quill-new';\nimport type { Attachment } from './type';\n\nexport interface TippEditorProps extends ReactQuill.ReactQuillProps {\n defaultTitle?: string;\n defaultValue?: string;\n defaultAttachedFiles?: Attachment[];\n /** 저장하기 버튼 클릭 시 실행 */\n onClickSave?: (values: {\n title: string;\n content: string;\n files: Attachment[];\n }) => void;\n /** 파일 업로드 버튼 클릭 시 실행 */\n uploadFile?: (\n file: File,\n destination: string\n ) => Promise<Attachment | undefined>;\n deleteFile?: (fileUrl: string) => Promise<void>;\n /** 외부에서 Editor를 빈 상태로 초기화 시켜야 할 때 사용 */\n clearEditor?: React.MutableRefObject<(() => void) | undefined>;\n /** 초기화 버튼말고 다른 버튼 추가시 */\n SecondaryButton?: React.ReactNode;\n /** true인 경우 저장하기 버튼이 비활성 화 됨. 연타 방지 */\n isLoading?: boolean;\n minHeight?: string;\n maxHeight?: string;\n height?: string;\n /** 제목 입력창 숨김 */\n hideHeader?: boolean;\n /** 첨부 파일 버튼 숨김 */\n hideFileAttachment?: boolean;\n /** 저장 버튼 footer 숨김 */\n hideFooter?: boolean;\n title?: string;\n onChangeTitle?: (value: string) => void;\n content?: string;\n onChangeContent?: (value: string) => void;\n attachedFiles?: Attachment[];\n onChangeAttachedFiles?: (files: Attachment[]) => void;\n}\n\nexport const Editor = forwardRef<ReactQuill, TippEditorProps>(\n (props, ref): React.ReactNode => {\n const {\n defaultAttachedFiles,\n defaultTitle,\n defaultValue,\n onClickSave,\n uploadFile,\n deleteFile,\n isLoading,\n SecondaryButton,\n clearEditor,\n minHeight,\n maxHeight,\n height,\n hideHeader,\n hideFileAttachment,\n hideFooter,\n title: controlledTitle,\n onChangeTitle,\n content: controlledContent,\n onChangeContent,\n attachedFiles: controlledAttachedFiles,\n onChangeAttachedFiles,\n ...quillProps\n } = props;\n const defaultRef = useRef<ReactQuill>(null);\n const editorRef = ref || defaultRef;\n // Controlled vs Uncontrolled 모드 구분\n const isControlledTitle = controlledTitle !== undefined;\n const isControlledContent = controlledContent !== undefined;\n const isControlledAttachedFiles = controlledAttachedFiles !== undefined;\n\n const [internalAttachedFiles, setInternalAttachedFiles] = useState<Attachment[]>(\n defaultAttachedFiles || []\n );\n const [fileDeleteLoading, setFileDeleteLoading] = useState(new Set());\n\n const [internalTitle, setInternalTitle] = useState(defaultTitle || '');\n const [internalContent, setInternalContent] = useState(defaultValue || '');\n\n // 실제 사용할 값들 (controlled일 때는 props 값, uncontrolled일 때는 internal state 값)\n const title = isControlledTitle ? controlledTitle : internalTitle;\n const content = isControlledContent ? controlledContent : internalContent;\n const attachedFiles = isControlledAttachedFiles ? controlledAttachedFiles : internalAttachedFiles;\n\n const handleOnChangeContent = useCallback((value: string) => {\n if (!isControlledContent) {\n setInternalContent(value);\n }\n onChangeContent?.(value);\n }, [isControlledContent, onChangeContent]);\n\n const handleButtonClick = useCallback(() => {\n let input: HTMLInputElement | null = document.createElement('input');\n input.type = 'file';\n input.onchange = async (event) => {\n const file = (event.target as HTMLInputElement).files?.[0];\n if (!file) {\n // console.log('DEBUG: no file');\n toast.error('파일을 선택해주세요.');\n return;\n }\n\n const fileName = file.name;\n const attachment = await uploadFile?.(file, `hr-notes/${fileName}`);\n if (attachment) {\n const newFiles = [...attachedFiles, attachment];\n if (!isControlledAttachedFiles) {\n setInternalAttachedFiles(newFiles);\n }\n onChangeAttachedFiles?.(newFiles);\n }\n input = null;\n };\n input.click();\n }, [uploadFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]);\n\n const handleDeleteFile = useCallback(\n async (fileUrl: string) => {\n try {\n setFileDeleteLoading((p) => p.add(fileUrl));\n await deleteFile?.(fileUrl);\n const newFiles = attachedFiles.filter((item) => item.url !== fileUrl);\n if (!isControlledAttachedFiles) {\n setInternalAttachedFiles(newFiles);\n }\n onChangeAttachedFiles?.(newFiles);\n } catch (err) {\n toast.error('파일 삭제에 실패했습니다.');\n } finally {\n setFileDeleteLoading((p) => {\n p.delete(fileUrl);\n return p;\n });\n }\n },\n [deleteFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]\n );\n\n const renderAttachedFiles = useCallback(() => {\n return (\n <Box width=\"100%\">\n {attachedFiles.map((file) => {\n return (\n <>\n <Separator size=\"4\" />\n <Flex\n align=\"center\"\n justify=\"between\"\n key={`${file.url}_${file.fileName}`}\n p=\"4\"\n width=\"100%\"\n >\n <Link href={file.url} size=\"2\">\n <Flex align=\"center\" gap=\"3\">\n <FileIcon />\n {file.fileName}\n </Flex>\n </Link>\n <Button\n loading={fileDeleteLoading.has(file.url)}\n onClick={() => {\n void handleDeleteFile(file.url);\n }}\n variant=\"ghost\"\n >\n 첨부 파일 삭제\n </Button>\n </Flex>\n </>\n );\n })}\n </Box>\n );\n }, [attachedFiles, fileDeleteLoading, handleDeleteFile]);\n\n const handleOnChangeTitle = useCallback<\n React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>\n >((e) => {\n const newTitle = e.target.value;\n if (!isControlledTitle) {\n setInternalTitle(newTitle);\n }\n onChangeTitle?.(newTitle);\n }, [isControlledTitle, onChangeTitle]);\n\n const clearEditorState = useCallback(() => {\n const emptyTitle = '';\n const emptyContent = '';\n const emptyFiles: Attachment[] = [];\n\n if (!isControlledTitle) {\n setInternalTitle(emptyTitle);\n }\n if (!isControlledContent) {\n setInternalContent(emptyContent);\n }\n if (!isControlledAttachedFiles) {\n setInternalAttachedFiles(emptyFiles);\n }\n\n // controlled 모드일 때도 부모에게 알림\n onChangeTitle?.(emptyTitle);\n onChangeContent?.(emptyContent);\n onChangeAttachedFiles?.(emptyFiles);\n }, [isControlledTitle, isControlledContent, isControlledAttachedFiles, onChangeTitle, onChangeContent, onChangeAttachedFiles]);\n\n const handleSaveClick = useCallback(() => {\n onClickSave?.({\n title,\n content,\n files: attachedFiles,\n });\n }, [onClickSave, title, content, attachedFiles]);\n\n if (props.clearEditor) {\n props.clearEditor.current = clearEditorState;\n }\n\n const cssVariables = {\n '--max-height': maxHeight,\n '--min-height': minHeight,\n '--height': height || '100%',\n } as React.CSSProperties;\n\n return (\n <div\n className=\"tipp-ql-wrapper\"\n style={{\n ...cssVariables,\n }}\n >\n <Grid height=\"100%\" rows={`${hideHeader ? '' : 'auto 1px'} 1fr`}>\n {/* 제목 입력창 */}\n {hideHeader ? null : (\n <>\n <Grid\n align=\"center\"\n columns=\"auto auto 1fr\"\n gap=\"2\"\n height=\"42px\"\n pl=\"2\"\n pr=\"3\"\n width=\"100%\"\n >\n <Box pl=\"3\" pr=\"3\">\n <Typo>제목</Typo>\n </Box>\n <Separator orientation=\"vertical\" style={{ height: '100%' }} />\n <TextField.Root\n className=\"editor-title-text-field\"\n onChange={handleOnChangeTitle}\n placeholder=\"제목을 입력해주세요\"\n value={title}\n />\n </Grid>\n <Separator orientation=\"horizontal\" size=\"4\" />\n </>\n )}\n\n <ReactQuill\n className=\"tipp-ql-editor write-mode\"\n onChange={handleOnChangeContent}\n ref={editorRef}\n theme=\"snow\"\n value={content}\n {...quillProps}\n />\n {renderAttachedFiles()}\n </Grid>\n\n {hideFooter ? null : (\n <>\n <Separator size=\"4\" />\n <Flex\n align=\"center\"\n justify=\"between\"\n p=\"2\"\n pl=\"4\"\n pr=\"4\"\n width=\"100%\"\n >\n {hideFileAttachment ? (\n <div />\n ) : (\n <Button\n color=\"gray\"\n onClick={handleButtonClick}\n variant=\"transparent\"\n >\n <Link2Icon height={20} width={20} />\n </Button>\n )}\n\n <Flex gap=\"2\">\n {clearEditor ? (\n <Button\n color=\"gray\"\n onClick={clearEditorState}\n variant=\"outline\"\n >\n 초기화\n </Button>\n ) : null}\n {SecondaryButton ? SecondaryButton : null}\n <Button disabled={isLoading} onClick={handleSaveClick}>\n 저장\n </Button>\n </Flex>\n </Flex>\n </>\n )}\n </div>\n );\n }\n);\n\nEditor.displayName = 'TIPP-Quill-Editor';\n"],"mappings":"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","names":["_a"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tipp/ui-quill-editor",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.11",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "tipp 디자인 시스템이 적용된 quillEditor 패키지, quillEditor의 사이즈가 커서 별도 패키지로 분리했습니다.",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"postcss-nesting": "12.0.2",
|
|
59
59
|
"tsup": "^8.0.2",
|
|
60
60
|
"typescript": "^5.3.3",
|
|
61
|
-
"@tipp/
|
|
62
|
-
"@tipp/
|
|
61
|
+
"@tipp/typescript-config": "1.0.1",
|
|
62
|
+
"@tipp/eslint-config": "1.1.2"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@tipp/ui": "2.2.7"
|
package/src/editor.tsx
CHANGED
|
@@ -40,8 +40,18 @@ export interface TippEditorProps extends ReactQuill.ReactQuillProps {
|
|
|
40
40
|
minHeight?: string;
|
|
41
41
|
maxHeight?: string;
|
|
42
42
|
height?: string;
|
|
43
|
+
/** 제목 입력창 숨김 */
|
|
43
44
|
hideHeader?: boolean;
|
|
45
|
+
/** 첨부 파일 버튼 숨김 */
|
|
44
46
|
hideFileAttachment?: boolean;
|
|
47
|
+
/** 저장 버튼 footer 숨김 */
|
|
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;
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
@@ -61,21 +71,41 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
61
71
|
height,
|
|
62
72
|
hideHeader,
|
|
63
73
|
hideFileAttachment,
|
|
74
|
+
hideFooter,
|
|
75
|
+
title: controlledTitle,
|
|
76
|
+
onChangeTitle,
|
|
77
|
+
content: controlledContent,
|
|
78
|
+
onChangeContent,
|
|
79
|
+
attachedFiles: controlledAttachedFiles,
|
|
80
|
+
onChangeAttachedFiles,
|
|
64
81
|
...quillProps
|
|
65
82
|
} = props;
|
|
66
83
|
const defaultRef = useRef<ReactQuill>(null);
|
|
67
84
|
const editorRef = ref || defaultRef;
|
|
68
|
-
|
|
85
|
+
// Controlled vs Uncontrolled 모드 구분
|
|
86
|
+
const isControlledTitle = controlledTitle !== undefined;
|
|
87
|
+
const isControlledContent = controlledContent !== undefined;
|
|
88
|
+
const isControlledAttachedFiles = controlledAttachedFiles !== undefined;
|
|
89
|
+
|
|
90
|
+
const [internalAttachedFiles, setInternalAttachedFiles] = useState<Attachment[]>(
|
|
69
91
|
defaultAttachedFiles || []
|
|
70
92
|
);
|
|
71
93
|
const [fileDeleteLoading, setFileDeleteLoading] = useState(new Set());
|
|
72
94
|
|
|
73
|
-
const [
|
|
74
|
-
const [
|
|
95
|
+
const [internalTitle, setInternalTitle] = useState(defaultTitle || '');
|
|
96
|
+
const [internalContent, setInternalContent] = useState(defaultValue || '');
|
|
97
|
+
|
|
98
|
+
// 실제 사용할 값들 (controlled일 때는 props 값, uncontrolled일 때는 internal state 값)
|
|
99
|
+
const title = isControlledTitle ? controlledTitle : internalTitle;
|
|
100
|
+
const content = isControlledContent ? controlledContent : internalContent;
|
|
101
|
+
const attachedFiles = isControlledAttachedFiles ? controlledAttachedFiles : internalAttachedFiles;
|
|
75
102
|
|
|
76
103
|
const handleOnChangeContent = useCallback((value: string) => {
|
|
77
|
-
|
|
78
|
-
|
|
104
|
+
if (!isControlledContent) {
|
|
105
|
+
setInternalContent(value);
|
|
106
|
+
}
|
|
107
|
+
onChangeContent?.(value);
|
|
108
|
+
}, [isControlledContent, onChangeContent]);
|
|
79
109
|
|
|
80
110
|
const handleButtonClick = useCallback(() => {
|
|
81
111
|
let input: HTMLInputElement | null = document.createElement('input');
|
|
@@ -91,21 +121,27 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
91
121
|
const fileName = file.name;
|
|
92
122
|
const attachment = await uploadFile?.(file, `hr-notes/${fileName}`);
|
|
93
123
|
if (attachment) {
|
|
94
|
-
|
|
124
|
+
const newFiles = [...attachedFiles, attachment];
|
|
125
|
+
if (!isControlledAttachedFiles) {
|
|
126
|
+
setInternalAttachedFiles(newFiles);
|
|
127
|
+
}
|
|
128
|
+
onChangeAttachedFiles?.(newFiles);
|
|
95
129
|
}
|
|
96
130
|
input = null;
|
|
97
131
|
};
|
|
98
132
|
input.click();
|
|
99
|
-
}, [uploadFile]);
|
|
133
|
+
}, [uploadFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]);
|
|
100
134
|
|
|
101
135
|
const handleDeleteFile = useCallback(
|
|
102
136
|
async (fileUrl: string) => {
|
|
103
137
|
try {
|
|
104
138
|
setFileDeleteLoading((p) => p.add(fileUrl));
|
|
105
139
|
await deleteFile?.(fileUrl);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
140
|
+
const newFiles = attachedFiles.filter((item) => item.url !== fileUrl);
|
|
141
|
+
if (!isControlledAttachedFiles) {
|
|
142
|
+
setInternalAttachedFiles(newFiles);
|
|
143
|
+
}
|
|
144
|
+
onChangeAttachedFiles?.(newFiles);
|
|
109
145
|
} catch (err) {
|
|
110
146
|
toast.error('파일 삭제에 실패했습니다.');
|
|
111
147
|
} finally {
|
|
@@ -115,7 +151,7 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
115
151
|
});
|
|
116
152
|
}
|
|
117
153
|
},
|
|
118
|
-
[deleteFile]
|
|
154
|
+
[deleteFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]
|
|
119
155
|
);
|
|
120
156
|
|
|
121
157
|
const renderAttachedFiles = useCallback(() => {
|
|
@@ -158,14 +194,33 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
158
194
|
const handleOnChangeTitle = useCallback<
|
|
159
195
|
React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>
|
|
160
196
|
>((e) => {
|
|
161
|
-
|
|
162
|
-
|
|
197
|
+
const newTitle = e.target.value;
|
|
198
|
+
if (!isControlledTitle) {
|
|
199
|
+
setInternalTitle(newTitle);
|
|
200
|
+
}
|
|
201
|
+
onChangeTitle?.(newTitle);
|
|
202
|
+
}, [isControlledTitle, onChangeTitle]);
|
|
163
203
|
|
|
164
204
|
const clearEditorState = useCallback(() => {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
205
|
+
const emptyTitle = '';
|
|
206
|
+
const emptyContent = '';
|
|
207
|
+
const emptyFiles: Attachment[] = [];
|
|
208
|
+
|
|
209
|
+
if (!isControlledTitle) {
|
|
210
|
+
setInternalTitle(emptyTitle);
|
|
211
|
+
}
|
|
212
|
+
if (!isControlledContent) {
|
|
213
|
+
setInternalContent(emptyContent);
|
|
214
|
+
}
|
|
215
|
+
if (!isControlledAttachedFiles) {
|
|
216
|
+
setInternalAttachedFiles(emptyFiles);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// controlled 모드일 때도 부모에게 알림
|
|
220
|
+
onChangeTitle?.(emptyTitle);
|
|
221
|
+
onChangeContent?.(emptyContent);
|
|
222
|
+
onChangeAttachedFiles?.(emptyFiles);
|
|
223
|
+
}, [isControlledTitle, isControlledContent, isControlledAttachedFiles, onChangeTitle, onChangeContent, onChangeAttachedFiles]);
|
|
169
224
|
|
|
170
225
|
const handleSaveClick = useCallback(() => {
|
|
171
226
|
onClickSave?.({
|
|
@@ -229,33 +284,49 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
229
284
|
{...quillProps}
|
|
230
285
|
/>
|
|
231
286
|
{renderAttachedFiles()}
|
|
232
|
-
<Separator size="4" />
|
|
233
287
|
</Grid>
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
288
|
+
|
|
289
|
+
{hideFooter ? null : (
|
|
290
|
+
<>
|
|
291
|
+
<Separator size="4" />
|
|
292
|
+
<Flex
|
|
293
|
+
align="center"
|
|
294
|
+
justify="between"
|
|
295
|
+
p="2"
|
|
296
|
+
pl="4"
|
|
297
|
+
pr="4"
|
|
298
|
+
width="100%"
|
|
242
299
|
>
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
300
|
+
{hideFileAttachment ? (
|
|
301
|
+
<div />
|
|
302
|
+
) : (
|
|
303
|
+
<Button
|
|
304
|
+
color="gray"
|
|
305
|
+
onClick={handleButtonClick}
|
|
306
|
+
variant="transparent"
|
|
307
|
+
>
|
|
308
|
+
<Link2Icon height={20} width={20} />
|
|
309
|
+
</Button>
|
|
310
|
+
)}
|
|
246
311
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
312
|
+
<Flex gap="2">
|
|
313
|
+
{clearEditor ? (
|
|
314
|
+
<Button
|
|
315
|
+
color="gray"
|
|
316
|
+
onClick={clearEditorState}
|
|
317
|
+
variant="outline"
|
|
318
|
+
>
|
|
319
|
+
초기화
|
|
320
|
+
</Button>
|
|
321
|
+
) : null}
|
|
322
|
+
{SecondaryButton ? SecondaryButton : null}
|
|
323
|
+
<Button disabled={isLoading} onClick={handleSaveClick}>
|
|
324
|
+
저장
|
|
325
|
+
</Button>
|
|
326
|
+
</Flex>
|
|
327
|
+
</Flex>
|
|
328
|
+
</>
|
|
329
|
+
)}
|
|
259
330
|
</div>
|
|
260
331
|
);
|
|
261
332
|
}
|