@tipp/ui-quill-editor 4.0.11 → 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 +62 -20
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +62 -20
- package/dist/editor.js.map +1 -1
- package/package.json +3 -3
- package/src/editor.tsx +67 -20
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, {
|
|
@@ -135,21 +153,22 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
135
153
|
const isControlledTitle = controlledTitle !== void 0;
|
|
136
154
|
const isControlledContent = controlledContent !== void 0;
|
|
137
155
|
const isControlledAttachedFiles = controlledAttachedFiles !== void 0;
|
|
138
|
-
const [internalAttachedFiles, setInternalAttachedFiles] = (0, import_react.useState)(
|
|
139
|
-
defaultAttachedFiles || []
|
|
140
|
-
);
|
|
156
|
+
const [internalAttachedFiles, setInternalAttachedFiles] = (0, import_react.useState)(defaultAttachedFiles || []);
|
|
141
157
|
const [fileDeleteLoading, setFileDeleteLoading] = (0, import_react.useState)(/* @__PURE__ */ new Set());
|
|
142
158
|
const [internalTitle, setInternalTitle] = (0, import_react.useState)(defaultTitle || "");
|
|
143
159
|
const [internalContent, setInternalContent] = (0, import_react.useState)(defaultValue || "");
|
|
144
160
|
const title = isControlledTitle ? controlledTitle : internalTitle;
|
|
145
161
|
const content = isControlledContent ? controlledContent : internalContent;
|
|
146
162
|
const attachedFiles = isControlledAttachedFiles ? controlledAttachedFiles : internalAttachedFiles;
|
|
147
|
-
const handleOnChangeContent = (0, import_react.useCallback)(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
+
);
|
|
153
172
|
const handleButtonClick = (0, import_react.useCallback)(() => {
|
|
154
173
|
let input = document.createElement("input");
|
|
155
174
|
input.type = "file";
|
|
@@ -172,7 +191,12 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
172
191
|
input = null;
|
|
173
192
|
});
|
|
174
193
|
input.click();
|
|
175
|
-
}, [
|
|
194
|
+
}, [
|
|
195
|
+
uploadFile,
|
|
196
|
+
attachedFiles,
|
|
197
|
+
isControlledAttachedFiles,
|
|
198
|
+
onChangeAttachedFiles
|
|
199
|
+
]);
|
|
176
200
|
const handleDeleteFile = (0, import_react.useCallback)(
|
|
177
201
|
(fileUrl) => __async(null, null, function* () {
|
|
178
202
|
try {
|
|
@@ -192,7 +216,12 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
192
216
|
});
|
|
193
217
|
}
|
|
194
218
|
}),
|
|
195
|
-
[
|
|
219
|
+
[
|
|
220
|
+
deleteFile,
|
|
221
|
+
attachedFiles,
|
|
222
|
+
isControlledAttachedFiles,
|
|
223
|
+
onChangeAttachedFiles
|
|
224
|
+
]
|
|
196
225
|
);
|
|
197
226
|
const renderAttachedFiles = (0, import_react.useCallback)(() => {
|
|
198
227
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_ui.Box, { width: "100%", children: attachedFiles.map((file) => {
|
|
@@ -228,13 +257,16 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
228
257
|
] });
|
|
229
258
|
}) });
|
|
230
259
|
}, [attachedFiles, fileDeleteLoading, handleDeleteFile]);
|
|
231
|
-
const handleOnChangeTitle = (0, import_react.useCallback)(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
+
);
|
|
238
270
|
const clearEditorState = (0, import_react.useCallback)(() => {
|
|
239
271
|
const emptyTitle = "";
|
|
240
272
|
const emptyContent = "";
|
|
@@ -251,7 +283,14 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
251
283
|
onChangeTitle == null ? void 0 : onChangeTitle(emptyTitle);
|
|
252
284
|
onChangeContent == null ? void 0 : onChangeContent(emptyContent);
|
|
253
285
|
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(emptyFiles);
|
|
254
|
-
}, [
|
|
286
|
+
}, [
|
|
287
|
+
isControlledTitle,
|
|
288
|
+
isControlledContent,
|
|
289
|
+
isControlledAttachedFiles,
|
|
290
|
+
onChangeTitle,
|
|
291
|
+
onChangeContent,
|
|
292
|
+
onChangeAttachedFiles
|
|
293
|
+
]);
|
|
255
294
|
const handleSaveClick = (0, import_react.useCallback)(() => {
|
|
256
295
|
onClickSave == null ? void 0 : onClickSave({
|
|
257
296
|
title,
|
|
@@ -309,7 +348,10 @@ var Editor = (0, import_react.forwardRef)(
|
|
|
309
348
|
onChange: handleOnChangeContent,
|
|
310
349
|
ref: editorRef,
|
|
311
350
|
theme: "snow",
|
|
312
|
-
value: content
|
|
351
|
+
value: content,
|
|
352
|
+
modules: {
|
|
353
|
+
toolbar: toolbarOptions
|
|
354
|
+
}
|
|
313
355
|
}, quillProps)
|
|
314
356
|
),
|
|
315
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 title?: string;\n onChangeTitle?: (value: string) => void;\n content?: string;\n onChangeContent?: (value: string) => void;\n attachedFiles?: Attachment[];\n onChangeAttachedFiles?: (files: Attachment[]) => void;\n}\n\nexport const Editor = forwardRef<ReactQuill, TippEditorProps>(\n (props, ref): React.ReactNode => {\n const {\n defaultAttachedFiles,\n defaultTitle,\n defaultValue,\n onClickSave,\n uploadFile,\n deleteFile,\n isLoading,\n SecondaryButton,\n clearEditor,\n minHeight,\n maxHeight,\n height,\n hideHeader,\n hideFileAttachment,\n hideFooter,\n title: controlledTitle,\n onChangeTitle,\n content: controlledContent,\n onChangeContent,\n attachedFiles: controlledAttachedFiles,\n onChangeAttachedFiles,\n ...quillProps\n } = props;\n const defaultRef = useRef<ReactQuill>(null);\n const editorRef = ref || defaultRef;\n // Controlled vs Uncontrolled 모드 구분\n const isControlledTitle = controlledTitle !== undefined;\n const isControlledContent = controlledContent !== undefined;\n const isControlledAttachedFiles = controlledAttachedFiles !== undefined;\n\n const [internalAttachedFiles, setInternalAttachedFiles] = useState<Attachment[]>(\n defaultAttachedFiles || []\n );\n const [fileDeleteLoading, setFileDeleteLoading] = useState(new Set());\n\n const [internalTitle, setInternalTitle] = useState(defaultTitle || '');\n const [internalContent, setInternalContent] = useState(defaultValue || '');\n\n // 실제 사용할 값들 (controlled일 때는 props 값, uncontrolled일 때는 internal state 값)\n const title = isControlledTitle ? controlledTitle : internalTitle;\n const content = isControlledContent ? controlledContent : internalContent;\n const attachedFiles = isControlledAttachedFiles ? controlledAttachedFiles : internalAttachedFiles;\n\n const handleOnChangeContent = useCallback((value: string) => {\n if (!isControlledContent) {\n setInternalContent(value);\n }\n onChangeContent?.(value);\n }, [isControlledContent, onChangeContent]);\n\n const handleButtonClick = useCallback(() => {\n let input: HTMLInputElement | null = document.createElement('input');\n input.type = 'file';\n input.onchange = async (event) => {\n const file = (event.target as HTMLInputElement).files?.[0];\n if (!file) {\n // console.log('DEBUG: no file');\n toast.error('파일을 선택해주세요.');\n return;\n }\n\n const fileName = file.name;\n const attachment = await uploadFile?.(file, `hr-notes/${fileName}`);\n if (attachment) {\n const newFiles = [...attachedFiles, attachment];\n if (!isControlledAttachedFiles) {\n setInternalAttachedFiles(newFiles);\n }\n onChangeAttachedFiles?.(newFiles);\n }\n input = null;\n };\n input.click();\n }, [uploadFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]);\n\n const handleDeleteFile = useCallback(\n async (fileUrl: string) => {\n try {\n setFileDeleteLoading((p) => p.add(fileUrl));\n await deleteFile?.(fileUrl);\n const newFiles = attachedFiles.filter((item) => item.url !== fileUrl);\n if (!isControlledAttachedFiles) {\n setInternalAttachedFiles(newFiles);\n }\n onChangeAttachedFiles?.(newFiles);\n } catch (err) {\n toast.error('파일 삭제에 실패했습니다.');\n } finally {\n setFileDeleteLoading((p) => {\n p.delete(fileUrl);\n return p;\n });\n }\n },\n [deleteFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]\n );\n\n const renderAttachedFiles = useCallback(() => {\n return (\n <Box width=\"100%\">\n {attachedFiles.map((file) => {\n return (\n <>\n <Separator size=\"4\" />\n <Flex\n align=\"center\"\n justify=\"between\"\n key={`${file.url}_${file.fileName}`}\n p=\"4\"\n width=\"100%\"\n >\n <Link href={file.url} size=\"2\">\n <Flex align=\"center\" gap=\"3\">\n <FileIcon />\n {file.fileName}\n </Flex>\n </Link>\n <Button\n loading={fileDeleteLoading.has(file.url)}\n onClick={() => {\n void handleDeleteFile(file.url);\n }}\n variant=\"ghost\"\n >\n 첨부 파일 삭제\n </Button>\n </Flex>\n </>\n );\n })}\n </Box>\n );\n }, [attachedFiles, fileDeleteLoading, handleDeleteFile]);\n\n const handleOnChangeTitle = useCallback<\n React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>\n >((e) => {\n const newTitle = e.target.value;\n if (!isControlledTitle) {\n setInternalTitle(newTitle);\n }\n onChangeTitle?.(newTitle);\n }, [isControlledTitle, onChangeTitle]);\n\n const clearEditorState = useCallback(() => {\n const emptyTitle = '';\n const emptyContent = '';\n const emptyFiles: Attachment[] = [];\n\n if (!isControlledTitle) {\n setInternalTitle(emptyTitle);\n }\n if (!isControlledContent) {\n setInternalContent(emptyContent);\n }\n if (!isControlledAttachedFiles) {\n setInternalAttachedFiles(emptyFiles);\n }\n\n // controlled 모드일 때도 부모에게 알림\n onChangeTitle?.(emptyTitle);\n onChangeContent?.(emptyContent);\n onChangeAttachedFiles?.(emptyFiles);\n }, [isControlledTitle, isControlledContent, isControlledAttachedFiles, onChangeTitle, onChangeContent, onChangeAttachedFiles]);\n\n const handleSaveClick = useCallback(() => {\n onClickSave?.({\n title,\n content,\n files: attachedFiles,\n });\n }, [onClickSave, title, content, attachedFiles]);\n\n if (props.clearEditor) {\n props.clearEditor.current = clearEditorState;\n }\n\n const cssVariables = {\n '--max-height': maxHeight,\n '--min-height': minHeight,\n '--height': height || '100%',\n } as React.CSSProperties;\n\n return (\n <div\n className=\"tipp-ql-wrapper\"\n style={{\n ...cssVariables,\n }}\n >\n <Grid height=\"100%\" rows={`${hideHeader ? '' : 'auto 1px'} 1fr`}>\n {/* 제목 입력창 */}\n {hideHeader ? null : (\n <>\n <Grid\n align=\"center\"\n columns=\"auto auto 1fr\"\n gap=\"2\"\n height=\"42px\"\n pl=\"2\"\n pr=\"3\"\n width=\"100%\"\n >\n <Box pl=\"3\" pr=\"3\">\n <Typo>제목</Typo>\n </Box>\n <Separator orientation=\"vertical\" style={{ height: '100%' }} />\n <TextField.Root\n className=\"editor-title-text-field\"\n onChange={handleOnChangeTitle}\n placeholder=\"제목을 입력해주세요\"\n value={title}\n />\n </Grid>\n <Separator orientation=\"horizontal\" size=\"4\" />\n </>\n )}\n\n <ReactQuill\n className=\"tipp-ql-editor write-mode\"\n onChange={handleOnChangeContent}\n ref={editorRef}\n theme=\"snow\"\n value={content}\n {...quillProps}\n />\n {renderAttachedFiles()}\n </Grid>\n\n {hideFooter ? null : (\n <>\n <Separator size=\"4\" />\n <Flex\n align=\"center\"\n justify=\"between\"\n p=\"2\"\n pl=\"4\"\n pr=\"4\"\n width=\"100%\"\n >\n {hideFileAttachment ? (\n <div />\n ) : (\n <Button\n color=\"gray\"\n onClick={handleButtonClick}\n variant=\"transparent\"\n >\n <Link2Icon height={20} width={20} />\n </Button>\n )}\n\n <Flex gap=\"2\">\n {clearEditor ? (\n <Button\n color=\"gray\"\n onClick={clearEditorState}\n variant=\"outline\"\n >\n 초기화\n </Button>\n ) : null}\n {SecondaryButton ? SecondaryButton : null}\n <Button disabled={isLoading} onClick={handleSaveClick}>\n 저장\n </Button>\n </Flex>\n </Flex>\n </>\n )}\n </div>\n );\n }\n);\n\nEditor.displayName = 'TIPP-Quill-Editor';\n"],"mappings":"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","names":["_a","ReactQuill"]}
|
|
1
|
+
{"version":3,"sources":["../src/editor.tsx"],"sourcesContent":["import React, { forwardRef, useCallback, useRef, useState } from 'react';\nimport {\n Box,\n Button,\n Flex,\n Grid,\n Link,\n Separator,\n TextField,\n Typo,\n Link2Icon,\n toast,\n FileIcon,\n} from '@tipp/ui';\nimport ReactQuill from 'react-quill-new';\nimport type { Attachment } from './type';\n\nexport interface TippEditorProps extends ReactQuill.ReactQuillProps {\n defaultTitle?: string;\n defaultValue?: string;\n defaultAttachedFiles?: Attachment[];\n /** 저장하기 버튼 클릭 시 실행 */\n onClickSave?: (values: {\n title: string;\n content: string;\n files: Attachment[];\n }) => void;\n /** 파일 업로드 버튼 클릭 시 실행 */\n uploadFile?: (\n file: File,\n destination: string\n ) => Promise<Attachment | undefined>;\n deleteFile?: (fileUrl: string) => Promise<void>;\n /** 외부에서 Editor를 빈 상태로 초기화 시켜야 할 때 사용 */\n clearEditor?: React.MutableRefObject<(() => void) | undefined>;\n /** 초기화 버튼말고 다른 버튼 추가시 */\n SecondaryButton?: React.ReactNode;\n /** true인 경우 저장하기 버튼이 비활성 화 됨. 연타 방지 */\n isLoading?: boolean;\n minHeight?: string;\n maxHeight?: string;\n height?: string;\n /** 제목 입력창 숨김 */\n hideHeader?: boolean;\n /** 첨부 파일 버튼 숨김 */\n hideFileAttachment?: boolean;\n /** 저장 버튼 footer 숨김 */\n hideFooter?: boolean;\n title?: string;\n onChangeTitle?: (value: string) => void;\n content?: string;\n onChangeContent?: (value: string) => void;\n attachedFiles?: Attachment[];\n onChangeAttachedFiles?: (files: Attachment[]) => void;\n}\n\nconst toolbarOptions = [\n ['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.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, {
|
|
@@ -73,21 +91,22 @@ var Editor = forwardRef(
|
|
|
73
91
|
const isControlledTitle = controlledTitle !== void 0;
|
|
74
92
|
const isControlledContent = controlledContent !== void 0;
|
|
75
93
|
const isControlledAttachedFiles = controlledAttachedFiles !== void 0;
|
|
76
|
-
const [internalAttachedFiles, setInternalAttachedFiles] = useState(
|
|
77
|
-
defaultAttachedFiles || []
|
|
78
|
-
);
|
|
94
|
+
const [internalAttachedFiles, setInternalAttachedFiles] = useState(defaultAttachedFiles || []);
|
|
79
95
|
const [fileDeleteLoading, setFileDeleteLoading] = useState(/* @__PURE__ */ new Set());
|
|
80
96
|
const [internalTitle, setInternalTitle] = useState(defaultTitle || "");
|
|
81
97
|
const [internalContent, setInternalContent] = useState(defaultValue || "");
|
|
82
98
|
const title = isControlledTitle ? controlledTitle : internalTitle;
|
|
83
99
|
const content = isControlledContent ? controlledContent : internalContent;
|
|
84
100
|
const attachedFiles = isControlledAttachedFiles ? controlledAttachedFiles : internalAttachedFiles;
|
|
85
|
-
const handleOnChangeContent = useCallback(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
);
|
|
91
110
|
const handleButtonClick = useCallback(() => {
|
|
92
111
|
let input = document.createElement("input");
|
|
93
112
|
input.type = "file";
|
|
@@ -110,7 +129,12 @@ var Editor = forwardRef(
|
|
|
110
129
|
input = null;
|
|
111
130
|
});
|
|
112
131
|
input.click();
|
|
113
|
-
}, [
|
|
132
|
+
}, [
|
|
133
|
+
uploadFile,
|
|
134
|
+
attachedFiles,
|
|
135
|
+
isControlledAttachedFiles,
|
|
136
|
+
onChangeAttachedFiles
|
|
137
|
+
]);
|
|
114
138
|
const handleDeleteFile = useCallback(
|
|
115
139
|
(fileUrl) => __async(null, null, function* () {
|
|
116
140
|
try {
|
|
@@ -130,7 +154,12 @@ var Editor = forwardRef(
|
|
|
130
154
|
});
|
|
131
155
|
}
|
|
132
156
|
}),
|
|
133
|
-
[
|
|
157
|
+
[
|
|
158
|
+
deleteFile,
|
|
159
|
+
attachedFiles,
|
|
160
|
+
isControlledAttachedFiles,
|
|
161
|
+
onChangeAttachedFiles
|
|
162
|
+
]
|
|
134
163
|
);
|
|
135
164
|
const renderAttachedFiles = useCallback(() => {
|
|
136
165
|
return /* @__PURE__ */ jsx(Box, { width: "100%", children: attachedFiles.map((file) => {
|
|
@@ -166,13 +195,16 @@ var Editor = forwardRef(
|
|
|
166
195
|
] });
|
|
167
196
|
}) });
|
|
168
197
|
}, [attachedFiles, fileDeleteLoading, handleDeleteFile]);
|
|
169
|
-
const handleOnChangeTitle = useCallback(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
+
);
|
|
176
208
|
const clearEditorState = useCallback(() => {
|
|
177
209
|
const emptyTitle = "";
|
|
178
210
|
const emptyContent = "";
|
|
@@ -189,7 +221,14 @@ var Editor = forwardRef(
|
|
|
189
221
|
onChangeTitle == null ? void 0 : onChangeTitle(emptyTitle);
|
|
190
222
|
onChangeContent == null ? void 0 : onChangeContent(emptyContent);
|
|
191
223
|
onChangeAttachedFiles == null ? void 0 : onChangeAttachedFiles(emptyFiles);
|
|
192
|
-
}, [
|
|
224
|
+
}, [
|
|
225
|
+
isControlledTitle,
|
|
226
|
+
isControlledContent,
|
|
227
|
+
isControlledAttachedFiles,
|
|
228
|
+
onChangeTitle,
|
|
229
|
+
onChangeContent,
|
|
230
|
+
onChangeAttachedFiles
|
|
231
|
+
]);
|
|
193
232
|
const handleSaveClick = useCallback(() => {
|
|
194
233
|
onClickSave == null ? void 0 : onClickSave({
|
|
195
234
|
title,
|
|
@@ -247,7 +286,10 @@ var Editor = forwardRef(
|
|
|
247
286
|
onChange: handleOnChangeContent,
|
|
248
287
|
ref: editorRef,
|
|
249
288
|
theme: "snow",
|
|
250
|
-
value: content
|
|
289
|
+
value: content,
|
|
290
|
+
modules: {
|
|
291
|
+
toolbar: toolbarOptions
|
|
292
|
+
}
|
|
251
293
|
}, quillProps)
|
|
252
294
|
),
|
|
253
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 title?: string;\n onChangeTitle?: (value: string) => void;\n content?: string;\n onChangeContent?: (value: string) => void;\n attachedFiles?: Attachment[];\n onChangeAttachedFiles?: (files: Attachment[]) => void;\n}\n\nexport const Editor = forwardRef<ReactQuill, TippEditorProps>(\n (props, ref): React.ReactNode => {\n const {\n defaultAttachedFiles,\n defaultTitle,\n defaultValue,\n onClickSave,\n uploadFile,\n deleteFile,\n isLoading,\n SecondaryButton,\n clearEditor,\n minHeight,\n maxHeight,\n height,\n hideHeader,\n hideFileAttachment,\n hideFooter,\n title: controlledTitle,\n onChangeTitle,\n content: controlledContent,\n onChangeContent,\n attachedFiles: controlledAttachedFiles,\n onChangeAttachedFiles,\n ...quillProps\n } = props;\n const defaultRef = useRef<ReactQuill>(null);\n const editorRef = ref || defaultRef;\n // Controlled vs Uncontrolled 모드 구분\n const isControlledTitle = controlledTitle !== undefined;\n const isControlledContent = controlledContent !== undefined;\n const isControlledAttachedFiles = controlledAttachedFiles !== undefined;\n\n const [internalAttachedFiles, setInternalAttachedFiles] = useState<Attachment[]>(\n defaultAttachedFiles || []\n );\n const [fileDeleteLoading, setFileDeleteLoading] = useState(new Set());\n\n const [internalTitle, setInternalTitle] = useState(defaultTitle || '');\n const [internalContent, setInternalContent] = useState(defaultValue || '');\n\n // 실제 사용할 값들 (controlled일 때는 props 값, uncontrolled일 때는 internal state 값)\n const title = isControlledTitle ? controlledTitle : internalTitle;\n const content = isControlledContent ? controlledContent : internalContent;\n const attachedFiles = isControlledAttachedFiles ? controlledAttachedFiles : internalAttachedFiles;\n\n const handleOnChangeContent = useCallback((value: string) => {\n if (!isControlledContent) {\n setInternalContent(value);\n }\n onChangeContent?.(value);\n }, [isControlledContent, onChangeContent]);\n\n const handleButtonClick = useCallback(() => {\n let input: HTMLInputElement | null = document.createElement('input');\n input.type = 'file';\n input.onchange = async (event) => {\n const file = (event.target as HTMLInputElement).files?.[0];\n if (!file) {\n // console.log('DEBUG: no file');\n toast.error('파일을 선택해주세요.');\n return;\n }\n\n const fileName = file.name;\n const attachment = await uploadFile?.(file, `hr-notes/${fileName}`);\n if (attachment) {\n const newFiles = [...attachedFiles, attachment];\n if (!isControlledAttachedFiles) {\n setInternalAttachedFiles(newFiles);\n }\n onChangeAttachedFiles?.(newFiles);\n }\n input = null;\n };\n input.click();\n }, [uploadFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]);\n\n const handleDeleteFile = useCallback(\n async (fileUrl: string) => {\n try {\n setFileDeleteLoading((p) => p.add(fileUrl));\n await deleteFile?.(fileUrl);\n const newFiles = attachedFiles.filter((item) => item.url !== fileUrl);\n if (!isControlledAttachedFiles) {\n setInternalAttachedFiles(newFiles);\n }\n onChangeAttachedFiles?.(newFiles);\n } catch (err) {\n toast.error('파일 삭제에 실패했습니다.');\n } finally {\n setFileDeleteLoading((p) => {\n p.delete(fileUrl);\n return p;\n });\n }\n },\n [deleteFile, attachedFiles, isControlledAttachedFiles, onChangeAttachedFiles]\n );\n\n const renderAttachedFiles = useCallback(() => {\n return (\n <Box width=\"100%\">\n {attachedFiles.map((file) => {\n return (\n <>\n <Separator size=\"4\" />\n <Flex\n align=\"center\"\n justify=\"between\"\n key={`${file.url}_${file.fileName}`}\n p=\"4\"\n width=\"100%\"\n >\n <Link href={file.url} size=\"2\">\n <Flex align=\"center\" gap=\"3\">\n <FileIcon />\n {file.fileName}\n </Flex>\n </Link>\n <Button\n loading={fileDeleteLoading.has(file.url)}\n onClick={() => {\n void handleDeleteFile(file.url);\n }}\n variant=\"ghost\"\n >\n 첨부 파일 삭제\n </Button>\n </Flex>\n </>\n );\n })}\n </Box>\n );\n }, [attachedFiles, fileDeleteLoading, handleDeleteFile]);\n\n const handleOnChangeTitle = useCallback<\n React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>\n >((e) => {\n const newTitle = e.target.value;\n if (!isControlledTitle) {\n setInternalTitle(newTitle);\n }\n onChangeTitle?.(newTitle);\n }, [isControlledTitle, onChangeTitle]);\n\n const clearEditorState = useCallback(() => {\n const emptyTitle = '';\n const emptyContent = '';\n const emptyFiles: Attachment[] = [];\n\n if (!isControlledTitle) {\n setInternalTitle(emptyTitle);\n }\n if (!isControlledContent) {\n setInternalContent(emptyContent);\n }\n if (!isControlledAttachedFiles) {\n setInternalAttachedFiles(emptyFiles);\n }\n\n // controlled 모드일 때도 부모에게 알림\n onChangeTitle?.(emptyTitle);\n onChangeContent?.(emptyContent);\n onChangeAttachedFiles?.(emptyFiles);\n }, [isControlledTitle, isControlledContent, isControlledAttachedFiles, onChangeTitle, onChangeContent, onChangeAttachedFiles]);\n\n const handleSaveClick = useCallback(() => {\n onClickSave?.({\n title,\n content,\n files: attachedFiles,\n });\n }, [onClickSave, title, content, attachedFiles]);\n\n if (props.clearEditor) {\n props.clearEditor.current = clearEditorState;\n }\n\n const cssVariables = {\n '--max-height': maxHeight,\n '--min-height': minHeight,\n '--height': height || '100%',\n } as React.CSSProperties;\n\n return (\n <div\n className=\"tipp-ql-wrapper\"\n style={{\n ...cssVariables,\n }}\n >\n <Grid height=\"100%\" rows={`${hideHeader ? '' : 'auto 1px'} 1fr`}>\n {/* 제목 입력창 */}\n {hideHeader ? null : (\n <>\n <Grid\n align=\"center\"\n columns=\"auto auto 1fr\"\n gap=\"2\"\n height=\"42px\"\n pl=\"2\"\n pr=\"3\"\n width=\"100%\"\n >\n <Box pl=\"3\" pr=\"3\">\n <Typo>제목</Typo>\n </Box>\n <Separator orientation=\"vertical\" style={{ height: '100%' }} />\n <TextField.Root\n className=\"editor-title-text-field\"\n onChange={handleOnChangeTitle}\n placeholder=\"제목을 입력해주세요\"\n value={title}\n />\n </Grid>\n <Separator orientation=\"horizontal\" size=\"4\" />\n </>\n )}\n\n <ReactQuill\n className=\"tipp-ql-editor write-mode\"\n onChange={handleOnChangeContent}\n ref={editorRef}\n theme=\"snow\"\n value={content}\n {...quillProps}\n />\n {renderAttachedFiles()}\n </Grid>\n\n {hideFooter ? null : (\n <>\n <Separator size=\"4\" />\n <Flex\n align=\"center\"\n justify=\"between\"\n p=\"2\"\n pl=\"4\"\n pr=\"4\"\n width=\"100%\"\n >\n {hideFileAttachment ? (\n <div />\n ) : (\n <Button\n color=\"gray\"\n onClick={handleButtonClick}\n variant=\"transparent\"\n >\n <Link2Icon height={20} width={20} />\n </Button>\n )}\n\n <Flex gap=\"2\">\n {clearEditor ? (\n <Button\n color=\"gray\"\n onClick={clearEditorState}\n variant=\"outline\"\n >\n 초기화\n </Button>\n ) : null}\n {SecondaryButton ? SecondaryButton : null}\n <Button disabled={isLoading} onClick={handleSaveClick}>\n 저장\n </Button>\n </Flex>\n </Flex>\n </>\n )}\n </div>\n );\n }\n);\n\nEditor.displayName = 'TIPP-Quill-Editor';\n"],"mappings":"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","names":["_a"]}
|
|
1
|
+
{"version":3,"sources":["../src/editor.tsx"],"sourcesContent":["import React, { forwardRef, useCallback, useRef, useState } from 'react';\nimport {\n Box,\n Button,\n Flex,\n Grid,\n Link,\n Separator,\n TextField,\n Typo,\n Link2Icon,\n toast,\n FileIcon,\n} from '@tipp/ui';\nimport ReactQuill from 'react-quill-new';\nimport type { Attachment } from './type';\n\nexport interface TippEditorProps extends ReactQuill.ReactQuillProps {\n defaultTitle?: string;\n defaultValue?: string;\n defaultAttachedFiles?: Attachment[];\n /** 저장하기 버튼 클릭 시 실행 */\n onClickSave?: (values: {\n title: string;\n content: string;\n files: Attachment[];\n }) => void;\n /** 파일 업로드 버튼 클릭 시 실행 */\n uploadFile?: (\n file: File,\n destination: string\n ) => Promise<Attachment | undefined>;\n deleteFile?: (fileUrl: string) => Promise<void>;\n /** 외부에서 Editor를 빈 상태로 초기화 시켜야 할 때 사용 */\n clearEditor?: React.MutableRefObject<(() => void) | undefined>;\n /** 초기화 버튼말고 다른 버튼 추가시 */\n SecondaryButton?: React.ReactNode;\n /** true인 경우 저장하기 버튼이 비활성 화 됨. 연타 방지 */\n isLoading?: boolean;\n minHeight?: string;\n maxHeight?: string;\n height?: string;\n /** 제목 입력창 숨김 */\n hideHeader?: boolean;\n /** 첨부 파일 버튼 숨김 */\n hideFileAttachment?: boolean;\n /** 저장 버튼 footer 숨김 */\n hideFooter?: boolean;\n title?: string;\n onChangeTitle?: (value: string) => void;\n content?: string;\n onChangeContent?: (value: string) => void;\n attachedFiles?: Attachment[];\n onChangeAttachedFiles?: (files: Attachment[]) => void;\n}\n\nconst toolbarOptions = [\n ['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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tipp/ui-quill-editor",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.12",
|
|
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/eslint-config": "1.1.2",
|
|
62
|
+
"@tipp/typescript-config": "1.0.1"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@tipp/ui": "2.2.7"
|
package/src/editor.tsx
CHANGED
|
@@ -54,6 +54,25 @@ export interface TippEditorProps extends ReactQuill.ReactQuillProps {
|
|
|
54
54
|
onChangeAttachedFiles?: (files: Attachment[]) => void;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
const toolbarOptions = [
|
|
58
|
+
['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
|
+
|
|
57
76
|
export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
58
77
|
(props, ref): React.ReactNode => {
|
|
59
78
|
const {
|
|
@@ -87,9 +106,9 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
87
106
|
const isControlledContent = controlledContent !== undefined;
|
|
88
107
|
const isControlledAttachedFiles = controlledAttachedFiles !== undefined;
|
|
89
108
|
|
|
90
|
-
const [internalAttachedFiles, setInternalAttachedFiles] = useState<
|
|
91
|
-
|
|
92
|
-
);
|
|
109
|
+
const [internalAttachedFiles, setInternalAttachedFiles] = useState<
|
|
110
|
+
Attachment[]
|
|
111
|
+
>(defaultAttachedFiles || []);
|
|
93
112
|
const [fileDeleteLoading, setFileDeleteLoading] = useState(new Set());
|
|
94
113
|
|
|
95
114
|
const [internalTitle, setInternalTitle] = useState(defaultTitle || '');
|
|
@@ -98,14 +117,19 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
98
117
|
// 실제 사용할 값들 (controlled일 때는 props 값, uncontrolled일 때는 internal state 값)
|
|
99
118
|
const title = isControlledTitle ? controlledTitle : internalTitle;
|
|
100
119
|
const content = isControlledContent ? controlledContent : internalContent;
|
|
101
|
-
const attachedFiles = isControlledAttachedFiles
|
|
120
|
+
const attachedFiles = isControlledAttachedFiles
|
|
121
|
+
? controlledAttachedFiles
|
|
122
|
+
: internalAttachedFiles;
|
|
102
123
|
|
|
103
|
-
const handleOnChangeContent = useCallback(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
124
|
+
const handleOnChangeContent = useCallback(
|
|
125
|
+
(value: string) => {
|
|
126
|
+
if (!isControlledContent) {
|
|
127
|
+
setInternalContent(value);
|
|
128
|
+
}
|
|
129
|
+
onChangeContent?.(value);
|
|
130
|
+
},
|
|
131
|
+
[isControlledContent, onChangeContent]
|
|
132
|
+
);
|
|
109
133
|
|
|
110
134
|
const handleButtonClick = useCallback(() => {
|
|
111
135
|
let input: HTMLInputElement | null = document.createElement('input');
|
|
@@ -130,7 +154,12 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
130
154
|
input = null;
|
|
131
155
|
};
|
|
132
156
|
input.click();
|
|
133
|
-
}, [
|
|
157
|
+
}, [
|
|
158
|
+
uploadFile,
|
|
159
|
+
attachedFiles,
|
|
160
|
+
isControlledAttachedFiles,
|
|
161
|
+
onChangeAttachedFiles,
|
|
162
|
+
]);
|
|
134
163
|
|
|
135
164
|
const handleDeleteFile = useCallback(
|
|
136
165
|
async (fileUrl: string) => {
|
|
@@ -151,7 +180,12 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
151
180
|
});
|
|
152
181
|
}
|
|
153
182
|
},
|
|
154
|
-
[
|
|
183
|
+
[
|
|
184
|
+
deleteFile,
|
|
185
|
+
attachedFiles,
|
|
186
|
+
isControlledAttachedFiles,
|
|
187
|
+
onChangeAttachedFiles,
|
|
188
|
+
]
|
|
155
189
|
);
|
|
156
190
|
|
|
157
191
|
const renderAttachedFiles = useCallback(() => {
|
|
@@ -193,13 +227,16 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
193
227
|
|
|
194
228
|
const handleOnChangeTitle = useCallback<
|
|
195
229
|
React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>
|
|
196
|
-
>(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
230
|
+
>(
|
|
231
|
+
(e) => {
|
|
232
|
+
const newTitle = e.target.value;
|
|
233
|
+
if (!isControlledTitle) {
|
|
234
|
+
setInternalTitle(newTitle);
|
|
235
|
+
}
|
|
236
|
+
onChangeTitle?.(newTitle);
|
|
237
|
+
},
|
|
238
|
+
[isControlledTitle, onChangeTitle]
|
|
239
|
+
);
|
|
203
240
|
|
|
204
241
|
const clearEditorState = useCallback(() => {
|
|
205
242
|
const emptyTitle = '';
|
|
@@ -220,7 +257,14 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
220
257
|
onChangeTitle?.(emptyTitle);
|
|
221
258
|
onChangeContent?.(emptyContent);
|
|
222
259
|
onChangeAttachedFiles?.(emptyFiles);
|
|
223
|
-
}, [
|
|
260
|
+
}, [
|
|
261
|
+
isControlledTitle,
|
|
262
|
+
isControlledContent,
|
|
263
|
+
isControlledAttachedFiles,
|
|
264
|
+
onChangeTitle,
|
|
265
|
+
onChangeContent,
|
|
266
|
+
onChangeAttachedFiles,
|
|
267
|
+
]);
|
|
224
268
|
|
|
225
269
|
const handleSaveClick = useCallback(() => {
|
|
226
270
|
onClickSave?.({
|
|
@@ -281,6 +325,9 @@ export const Editor = forwardRef<ReactQuill, TippEditorProps>(
|
|
|
281
325
|
ref={editorRef}
|
|
282
326
|
theme="snow"
|
|
283
327
|
value={content}
|
|
328
|
+
modules={{
|
|
329
|
+
toolbar: toolbarOptions,
|
|
330
|
+
}}
|
|
284
331
|
{...quillProps}
|
|
285
332
|
/>
|
|
286
333
|
{renderAttachedFiles()}
|