@team-monolith/cds 1.10.0 → 1.10.1
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/patterns/LexicalEditor/LexicalCustomConfigContext.d.ts +6 -0
- package/dist/patterns/LexicalEditor/LexicalCustomConfigContext.js +5 -0
- package/dist/patterns/LexicalEditor/LexicalEditor.d.ts +4 -0
- package/dist/patterns/LexicalEditor/LexicalEditor.js +5 -2
- package/dist/patterns/LexicalEditor/Plugins.d.ts +1 -0
- package/dist/patterns/LexicalEditor/Plugins.js +4 -3
- package/dist/patterns/LexicalEditor/nodes/ProblemInputNode/InputComponent.js +16 -9
- package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/ProblemSelectNode.d.ts +45 -0
- package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/ProblemSelectNode.js +74 -0
- package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SelectBox.d.ts +11 -0
- package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SelectBox.js +73 -0
- package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SelectComponent.d.ts +9 -0
- package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SelectComponent.js +85 -0
- package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SettingForm/FormSelection.d.ts +14 -0
- package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SettingForm/FormSelection.js +91 -0
- package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SettingForm/InsertImageDialog.d.ts +10 -0
- package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SettingForm/InsertImageDialog.js +109 -0
- package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SettingForm/InsertImageUploadedDialogBody.d.ts +9 -0
- package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SettingForm/InsertImageUploadedDialogBody.js +50 -0
- package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SettingForm/InsertImageUriDialogBody.d.ts +8 -0
- package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SettingForm/InsertImageUriDialogBody.js +17 -0
- package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SettingForm/SettingForm.d.ts +8 -0
- package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SettingForm/SettingForm.js +146 -0
- package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SettingForm/index.d.ts +1 -0
- package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SettingForm/index.js +1 -0
- package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/index.d.ts +2 -0
- package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/index.js +2 -0
- package/dist/patterns/LexicalEditor/plugins/ActionsPlugin/index.d.ts +9 -0
- package/dist/patterns/LexicalEditor/plugins/ActionsPlugin/index.js +24 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentAdderPlugin/useContextMenuOptions.js +15 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentPickerMenuPlugin/ComponentPickerMenuPlugin.js +13 -1
- package/dist/patterns/LexicalEditor/plugins/ImagesPlugin/InsertImageDialog.js +7 -6
- package/dist/patterns/LexicalEditor/plugins/ImagesPlugin/InsertImageUploadedDialogBody.d.ts +13 -4
- package/dist/patterns/LexicalEditor/plugins/ImagesPlugin/InsertImageUploadedDialogBody.js +5 -6
- package/dist/patterns/LexicalEditor/plugins/ImagesPlugin/InsertImageUriDialogBody.d.ts +9 -4
- package/dist/patterns/LexicalEditor/plugins/ImagesPlugin/InsertImageUriDialogBody.js +4 -7
- package/dist/patterns/LexicalEditor/plugins/ProblemSelectPlugin/index.d.ts +5 -0
- package/dist/patterns/LexicalEditor/plugins/ProblemSelectPlugin/index.js +20 -0
- package/dist/patterns/LexicalEditor/theme.d.ts +1 -0
- package/dist/patterns/LexicalEditor/theme.js +7 -0
- package/package.json +1 -1
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
import { SerializedEditorState, SerializedLexicalNode } from "lexical";
|
|
3
3
|
export interface LexicalEditorProps {
|
|
4
4
|
className?: string;
|
|
5
|
+
contentEditableClassName?: string;
|
|
5
6
|
value?: any;
|
|
6
7
|
onChange?: (blocks: SerializedEditorState<SerializedLexicalNode>) => void;
|
|
7
8
|
/** editable. 수정 모드 / 읽기 모드 여부
|
|
8
9
|
* initialConfig에 전달되므로 마운트 된 이후 수정해도 반영되지 않음
|
|
9
10
|
*/
|
|
10
11
|
editable?: boolean;
|
|
12
|
+
/** freeze. 문제 블록을 수정하지 못하도록 함
|
|
13
|
+
*/
|
|
14
|
+
freezeProblemNode?: boolean;
|
|
11
15
|
/** 외부에서 플러그인을 주입하는 경우 활용함 */
|
|
12
16
|
children?: JSX.Element | string | (JSX.Element | string)[];
|
|
13
17
|
}
|
|
@@ -11,6 +11,8 @@ import { getTheme } from "./theme";
|
|
|
11
11
|
import { useTheme } from "@emotion/react";
|
|
12
12
|
import Plugins from "./Plugins";
|
|
13
13
|
import { ColoredQuoteNode, ProblemInputNode } from "./nodes";
|
|
14
|
+
import { LexicalCustomConfigContext } from "./LexicalCustomConfigContext";
|
|
15
|
+
import { ProblemSelectNode } from "./nodes/ProblemSelectNode";
|
|
14
16
|
function validateValue(value) {
|
|
15
17
|
var _a, _b;
|
|
16
18
|
if (value && typeof value !== "object") {
|
|
@@ -28,13 +30,14 @@ function validateValue(value) {
|
|
|
28
30
|
return true;
|
|
29
31
|
}
|
|
30
32
|
export function LexicalEditor(props) {
|
|
31
|
-
const { className, value, onChange, editable = true, children } = props;
|
|
33
|
+
const { className, contentEditableClassName, value, onChange, editable = true, freezeProblemNode, children, } = props;
|
|
32
34
|
const theme = useTheme();
|
|
33
35
|
const initialConfig = {
|
|
34
36
|
namespace: "CodleLexicalEditor",
|
|
35
37
|
onError: (error) => console.error(error),
|
|
36
38
|
nodes: [
|
|
37
39
|
ProblemInputNode,
|
|
40
|
+
ProblemSelectNode,
|
|
38
41
|
HeadingNode,
|
|
39
42
|
ListNode,
|
|
40
43
|
ListItemNode,
|
|
@@ -60,5 +63,5 @@ export function LexicalEditor(props) {
|
|
|
60
63
|
editorState: validateValue(value) ? JSON.stringify(value) : undefined,
|
|
61
64
|
editable: editable,
|
|
62
65
|
};
|
|
63
|
-
return (_jsxs(LexicalComposer, Object.assign({ initialConfig: initialConfig }, { children: [_jsx(Plugins, { className: className, onChange: onChange }), _jsx(_Fragment, { children: children })] })));
|
|
66
|
+
return (_jsx(LexicalCustomConfigContext.Provider, Object.assign({ value: { freezeProblemNode: freezeProblemNode !== null && freezeProblemNode !== void 0 ? freezeProblemNode : false } }, { children: _jsxs(LexicalComposer, Object.assign({ initialConfig: initialConfig }, { children: [_jsx(Plugins, { className: className, contentEditableClassName: contentEditableClassName, onChange: onChange }), _jsx(_Fragment, { children: children })] })) })));
|
|
64
67
|
}
|
|
@@ -6,6 +6,7 @@ import { ReactElement } from "react";
|
|
|
6
6
|
import { SerializedEditorState, SerializedLexicalNode } from "lexical";
|
|
7
7
|
export interface PluginsProps {
|
|
8
8
|
className?: string;
|
|
9
|
+
contentEditableClassName?: string;
|
|
9
10
|
onChange?: (blocks: SerializedEditorState<SerializedLexicalNode>) => void;
|
|
10
11
|
}
|
|
11
12
|
export default function Plugins(props: PluginsProps): ReactElement;
|
|
@@ -28,8 +28,9 @@ import useLexicalEditable from "@lexical/react/useLexicalEditable";
|
|
|
28
28
|
import ListMaxIndentLevelPlugin from "./plugins/ListMaxIndentLevelPlugin";
|
|
29
29
|
import styled from "@emotion/styled";
|
|
30
30
|
import ProblemInputPlugin from "./plugins/ProblemInputPlugin";
|
|
31
|
+
import ProblemSelectPlugin from "./plugins/ProblemSelectPlugin";
|
|
31
32
|
export default function Plugins(props) {
|
|
32
|
-
const { className, onChange } = props;
|
|
33
|
+
const { className, contentEditableClassName, onChange } = props;
|
|
33
34
|
const isEditable = useLexicalEditable();
|
|
34
35
|
const [floatingAnchorElem, setFloatingAnchorElem] = useState(null);
|
|
35
36
|
const [isLinkEditMode, setIsLinkEditMode] = useState(false);
|
|
@@ -38,11 +39,11 @@ export default function Plugins(props) {
|
|
|
38
39
|
setFloatingAnchorElem(_floatingAnchorElem);
|
|
39
40
|
}
|
|
40
41
|
};
|
|
41
|
-
return (_jsxs(_Fragment, { children: [_jsx(RichTextPlugin, { contentEditable: _jsx(ScrollArea, Object.assign({ className: className }, { children: _jsx(FloatingAnchor, Object.assign({ ref: onRef }, { children: _jsx(StyledContentEditable, { isEditable: isEditable }) })) })), placeholder: null, ErrorBoundary: LexicalErrorBoundary }), _jsx(OnChangePlugin, { onChange: (editorState) => {
|
|
42
|
+
return (_jsxs(_Fragment, { children: [_jsx(RichTextPlugin, { contentEditable: _jsx(ScrollArea, Object.assign({ className: className }, { children: _jsx(FloatingAnchor, Object.assign({ ref: onRef }, { children: _jsx(StyledContentEditable, { className: contentEditableClassName, isEditable: isEditable }) })) })), placeholder: null, ErrorBoundary: LexicalErrorBoundary }), _jsx(OnChangePlugin, { onChange: (editorState) => {
|
|
42
43
|
onChange === null || onChange === void 0 ? void 0 : onChange(editorState.toJSON());
|
|
43
44
|
},
|
|
44
45
|
// ignore 하지 않으면 Form에서 수정하지 않았는데 Dirty로 처리됨.
|
|
45
|
-
ignoreSelectionChange: true }), _jsx(AutoFocusPlugin, {}), isEditable && (_jsxs(_Fragment, { children: [_jsx(TabIndentationPlugin, {}), _jsx(ComponentPickerMenuPlugin, {}), _jsx(MarkdownShortcutPlugin, { transformers: CODLE_TRANSFORMERS }), _jsx(HistoryPlugin, {})] })), floatingAnchorElem && isEditable && (_jsxs(_Fragment, { children: [_jsx(ComponentAdderPlugin, { anchorElem: floatingAnchorElem }), _jsx(FloatingTextFormatToolbarPlugin, { anchorElem: floatingAnchorElem }), _jsx(FloatingLinkEditorPlugin, { anchorElem: floatingAnchorElem, isLinkEditMode: isLinkEditMode, setIsLinkEditMode: setIsLinkEditMode })] })), !isEditable && _jsx(LexicalClickableLinkPlugin, {}), _jsx(ListPlugin, {}), _jsx(HorizontalRulePlugin, {}), _jsx(ImagesPlugin, {}), _jsx(TablePlugin, {}), _jsx(LinkPlugin, {}), _jsx(ListMaxIndentLevelPlugin, { maxDepth: 5 }), _jsx(ProblemInputPlugin, {})] }));
|
|
46
|
+
ignoreSelectionChange: true }), _jsx(AutoFocusPlugin, {}), isEditable && (_jsxs(_Fragment, { children: [_jsx(TabIndentationPlugin, {}), _jsx(ComponentPickerMenuPlugin, {}), _jsx(MarkdownShortcutPlugin, { transformers: CODLE_TRANSFORMERS }), _jsx(HistoryPlugin, {})] })), floatingAnchorElem && isEditable && (_jsxs(_Fragment, { children: [_jsx(ComponentAdderPlugin, { anchorElem: floatingAnchorElem }), _jsx(FloatingTextFormatToolbarPlugin, { anchorElem: floatingAnchorElem }), _jsx(FloatingLinkEditorPlugin, { anchorElem: floatingAnchorElem, isLinkEditMode: isLinkEditMode, setIsLinkEditMode: setIsLinkEditMode })] })), !isEditable && _jsx(LexicalClickableLinkPlugin, {}), _jsx(ListPlugin, {}), _jsx(HorizontalRulePlugin, {}), _jsx(ImagesPlugin, {}), _jsx(TablePlugin, {}), _jsx(LinkPlugin, {}), _jsx(ListMaxIndentLevelPlugin, { maxDepth: 5 }), _jsx(ProblemInputPlugin, {}), _jsx(ProblemSelectPlugin, {})] }));
|
|
46
47
|
}
|
|
47
48
|
const ScrollArea = styled.div `
|
|
48
49
|
min-height: 150px;
|
|
@@ -12,7 +12,7 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
12
12
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@emotion/react/jsx-runtime";
|
|
13
13
|
/** @jsxImportSource @emotion/react */
|
|
14
14
|
import { css } from "@emotion/react";
|
|
15
|
-
import { useState } from "react";
|
|
15
|
+
import { useContext, useState } from "react";
|
|
16
16
|
import Input from "../../../../components/Input";
|
|
17
17
|
import SquareButton from "../../../../components/SquareButton";
|
|
18
18
|
import { Settings3FillIcon } from "../../../../icons";
|
|
@@ -22,6 +22,7 @@ import useLexicalEditable from "@lexical/react/useLexicalEditable";
|
|
|
22
22
|
import { $isProblemInputNode } from "./ProblemInputNode";
|
|
23
23
|
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
24
24
|
import { $getNodeByKey } from "lexical";
|
|
25
|
+
import { LexicalCustomConfigContext } from "../../LexicalCustomConfigContext";
|
|
25
26
|
export function InputComponent(props) {
|
|
26
27
|
const { answer } = props, settingFormProps = __rest(props, ["answer"]);
|
|
27
28
|
const { placeholder, nodeKey } = settingFormProps;
|
|
@@ -29,23 +30,29 @@ export function InputComponent(props) {
|
|
|
29
30
|
const [settingOpen, setSettingOpen] = useState(false);
|
|
30
31
|
const isEditable = useLexicalEditable();
|
|
31
32
|
const [answerInput, setAnswerInput] = useState(answer);
|
|
33
|
+
const lexicalCustomConfig = useContext(LexicalCustomConfigContext);
|
|
32
34
|
// 학생 view
|
|
33
35
|
// TODO: "글자 수대로" 옵션시에 글자 수대로 입력칸을 표시해야 합니다.
|
|
34
36
|
if (!isEditable) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
if (lexicalCustomConfig.freezeProblemNode) {
|
|
38
|
+
return (_jsx(Input, { size: "small", placeholder: placeholder || "여기에 입력하세요.", value: answerInput, color: "default", fullWidth: true, inputProps: { readOnly: true } }));
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
return (_jsx(Input, { size: "small", placeholder: placeholder || "여기에 입력하세요.", value: answerInput, onChange: (e) => {
|
|
42
|
+
// SoT를 EditorState로 설정하는 경우 한글 입력시 문제가 생김.
|
|
43
|
+
// ex) "나비" 입력시 -> "ㄴ나납ㅂ비"
|
|
44
|
+
// SoT를 내부 State로 관리하고 EditorState를 따로 설정.
|
|
45
|
+
// 문제를 더 파악하고 추후 개선 필요.
|
|
46
|
+
setAnswerInput(e.target.value);
|
|
40
47
|
editor.update(() => {
|
|
41
48
|
const node = $getNodeByKey(nodeKey);
|
|
42
49
|
if (!$isProblemInputNode(node)) {
|
|
43
50
|
return;
|
|
44
51
|
}
|
|
45
|
-
node.setAnswer(
|
|
52
|
+
node.setAnswer(e.target.value);
|
|
46
53
|
});
|
|
47
|
-
},
|
|
48
|
-
|
|
54
|
+
}, color: "default", fullWidth: true }));
|
|
55
|
+
}
|
|
49
56
|
}
|
|
50
57
|
// 교사 edit view
|
|
51
58
|
return (_jsxs(_Fragment, { children: [_jsxs("div", Object.assign({ css: css `
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { DecoratorNode, EditorConfig, LexicalNode, NodeKey, SerializedLexicalNode, Spread } from "lexical";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
export interface ImageProps {
|
|
4
|
+
src: string;
|
|
5
|
+
altText: string;
|
|
6
|
+
}
|
|
7
|
+
export interface Selection {
|
|
8
|
+
isAnswer: boolean;
|
|
9
|
+
show: {
|
|
10
|
+
image?: ImageProps | null;
|
|
11
|
+
text: string;
|
|
12
|
+
};
|
|
13
|
+
value: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ProblemSelectPayload {
|
|
16
|
+
selections: Selection[];
|
|
17
|
+
selected: string[];
|
|
18
|
+
hasMultipleAnswers?: boolean;
|
|
19
|
+
key?: NodeKey;
|
|
20
|
+
}
|
|
21
|
+
export type SerializedProblemSelectNode = Spread<ProblemSelectPayload, SerializedLexicalNode>;
|
|
22
|
+
/**
|
|
23
|
+
* selections는 Selection타입의 배열로서 객관식 정보를 담고 있습니다. (교사용)
|
|
24
|
+
* selected는 학생이 선택한 답의 value를 담고 있습니다.(학생용)
|
|
25
|
+
*/
|
|
26
|
+
export declare class ProblemSelectNode extends DecoratorNode<ReactNode> {
|
|
27
|
+
__selections: Selection[];
|
|
28
|
+
__selected: string[];
|
|
29
|
+
__hasMultipleAnswers: boolean | undefined;
|
|
30
|
+
isInline(): boolean;
|
|
31
|
+
static getType(): string;
|
|
32
|
+
getSelections(): Selection[];
|
|
33
|
+
getSelected(): string[];
|
|
34
|
+
setSelections(selections: Selection[]): void;
|
|
35
|
+
setSelected(selected: string[]): void;
|
|
36
|
+
static clone(node: ProblemSelectNode): ProblemSelectNode;
|
|
37
|
+
constructor(selections: Selection[], selected: string[], hasMultipleAnswers?: boolean, key?: NodeKey);
|
|
38
|
+
createDOM(config: EditorConfig): HTMLElement;
|
|
39
|
+
updateDOM(): boolean;
|
|
40
|
+
static importJSON(serializedNode: SerializedProblemSelectNode): ProblemSelectNode;
|
|
41
|
+
exportJSON(): SerializedProblemSelectNode;
|
|
42
|
+
decorate(): ReactNode;
|
|
43
|
+
}
|
|
44
|
+
export declare function $createProblemSelectNode({ selections, selected, hasMultipleAnswers, key, }: ProblemSelectPayload): ProblemSelectNode;
|
|
45
|
+
export declare function $isProblemSelectNode(node: LexicalNode | null | undefined): node is ProblemSelectNode;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { $applyNodeReplacement, DecoratorNode, } from "lexical";
|
|
3
|
+
import { addClassNamesToElement } from "@lexical/utils";
|
|
4
|
+
import { SelectComponent } from "./SelectComponent";
|
|
5
|
+
/**
|
|
6
|
+
* selections는 Selection타입의 배열로서 객관식 정보를 담고 있습니다. (교사용)
|
|
7
|
+
* selected는 학생이 선택한 답의 value를 담고 있습니다.(학생용)
|
|
8
|
+
*/
|
|
9
|
+
export class ProblemSelectNode extends DecoratorNode {
|
|
10
|
+
isInline() {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
static getType() {
|
|
14
|
+
return "problem-select";
|
|
15
|
+
}
|
|
16
|
+
getSelections() {
|
|
17
|
+
return this.__selections;
|
|
18
|
+
}
|
|
19
|
+
getSelected() {
|
|
20
|
+
return this.__selected;
|
|
21
|
+
}
|
|
22
|
+
setSelections(selections) {
|
|
23
|
+
const self = this.getWritable();
|
|
24
|
+
self.__selections = selections;
|
|
25
|
+
}
|
|
26
|
+
setSelected(selected) {
|
|
27
|
+
const self = this.getWritable();
|
|
28
|
+
self.__selected = selected;
|
|
29
|
+
}
|
|
30
|
+
static clone(node) {
|
|
31
|
+
return new ProblemSelectNode(node.__selections, node.__selected, node.__hasMultipleAnswers, node.__key);
|
|
32
|
+
}
|
|
33
|
+
constructor(selections, selected, hasMultipleAnswers, key) {
|
|
34
|
+
super(key);
|
|
35
|
+
this.__selections = selections;
|
|
36
|
+
this.__selected = selected;
|
|
37
|
+
this.__hasMultipleAnswers = hasMultipleAnswers;
|
|
38
|
+
}
|
|
39
|
+
createDOM(config) {
|
|
40
|
+
// Define the DOM element here
|
|
41
|
+
const root = document.createElement("div");
|
|
42
|
+
addClassNamesToElement(root, config.theme.problemSelect);
|
|
43
|
+
return root;
|
|
44
|
+
}
|
|
45
|
+
updateDOM() {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
static importJSON(serializedNode) {
|
|
49
|
+
const node = $createProblemSelectNode({
|
|
50
|
+
key: serializedNode.key,
|
|
51
|
+
selections: serializedNode.selections,
|
|
52
|
+
selected: serializedNode.selected,
|
|
53
|
+
hasMultipleAnswers: serializedNode.hasMultipleAnswers,
|
|
54
|
+
});
|
|
55
|
+
return node;
|
|
56
|
+
}
|
|
57
|
+
exportJSON() {
|
|
58
|
+
return {
|
|
59
|
+
version: 1,
|
|
60
|
+
type: "problem-select",
|
|
61
|
+
selections: this.__selections,
|
|
62
|
+
selected: this.__selected,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
decorate() {
|
|
66
|
+
return (_jsx(SelectComponent, { selections: this.__selections, selected: this.__selected, hasMultipleAnswers: this.__hasMultipleAnswers, nodeKey: this.getKey() }));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function $createProblemSelectNode({ selections, selected, hasMultipleAnswers, key, }) {
|
|
70
|
+
return $applyNodeReplacement(new ProblemSelectNode(selections, selected, hasMultipleAnswers, key));
|
|
71
|
+
}
|
|
72
|
+
export function $isProblemSelectNode(node) {
|
|
73
|
+
return node instanceof ProblemSelectNode;
|
|
74
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ImageProps } from "./ProblemSelectNode";
|
|
2
|
+
export interface SelectBoxProps {
|
|
3
|
+
index: number;
|
|
4
|
+
isSelected?: boolean;
|
|
5
|
+
isAnswer?: boolean;
|
|
6
|
+
image?: ImageProps | null;
|
|
7
|
+
text: string;
|
|
8
|
+
onClick?: () => void;
|
|
9
|
+
fullWidth?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export default function SelectBox(props: SelectBoxProps): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource @emotion/react */
|
|
3
|
+
import styled from "@emotion/styled";
|
|
4
|
+
import { css, useTheme } from "@emotion/react";
|
|
5
|
+
import { CheckFillIcon, CheckboxCircleFillIcon } from "../../../../icons";
|
|
6
|
+
export default function SelectBox(props) {
|
|
7
|
+
const { index, isSelected, isAnswer, image, text, onClick, fullWidth } = props;
|
|
8
|
+
const theme = useTheme();
|
|
9
|
+
return (_jsxs(Container, Object.assign({ isSelected: isSelected, fullWidth: fullWidth, onClick: onClick }, { children: [_jsx(Index, Object.assign({ isSelected: isSelected }, { children: isSelected ? (_jsx(CheckFillIcon, { css: css `
|
|
10
|
+
width: 12px;
|
|
11
|
+
height: 12px;
|
|
12
|
+
` })) : (index) })), _jsxs(Content, { children: [image && (_jsx("img", { src: image.src, alt: image.altText, css: css `
|
|
13
|
+
height: auto;
|
|
14
|
+
// 이미지로 인해 좌우로 스크롤이 생기는 것을 방지
|
|
15
|
+
max-width: 100%;
|
|
16
|
+
width: fit-content;
|
|
17
|
+
border-radius: 6px;
|
|
18
|
+
` })), text] }), isAnswer && (_jsx(CheckboxCircleFillIcon, { color: theme.color.foreground.success, css: css `
|
|
19
|
+
width: 16px;
|
|
20
|
+
height: 16px;
|
|
21
|
+
` }))] })));
|
|
22
|
+
}
|
|
23
|
+
const Container = styled.div(({ theme, isSelected, fullWidth }) => css `
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
display: flex;
|
|
26
|
+
box-sizing: border-box;
|
|
27
|
+
width: ${fullWidth ? "100%" : "400px"};
|
|
28
|
+
padding: 8px;
|
|
29
|
+
gap: 8px;
|
|
30
|
+
border-radius: 8px;
|
|
31
|
+
background: ${isSelected
|
|
32
|
+
? theme.color.container.primaryContainer
|
|
33
|
+
: theme.color.background.neutralAlt};
|
|
34
|
+
border: ${isSelected
|
|
35
|
+
? `1px solid ${theme.color.foreground.primary}`
|
|
36
|
+
: "1px solid transparent"};
|
|
37
|
+
color: ${isSelected
|
|
38
|
+
? theme.color.container.primaryOnContainer
|
|
39
|
+
: theme.color.foreground.neutralBase};
|
|
40
|
+
`);
|
|
41
|
+
const Index = styled.div(({ theme, isSelected }) => css `
|
|
42
|
+
display: flex;
|
|
43
|
+
box-sizing: border-box;
|
|
44
|
+
width: 20px;
|
|
45
|
+
height: 20px;
|
|
46
|
+
padding: 4px;
|
|
47
|
+
justify-content: center;
|
|
48
|
+
align-items: center;
|
|
49
|
+
border-radius: 4px;
|
|
50
|
+
border: ${isSelected
|
|
51
|
+
? "none"
|
|
52
|
+
: `1px solid ${theme.color.background.neutralAltActive}`};
|
|
53
|
+
background: ${isSelected
|
|
54
|
+
? theme.color.background.primary
|
|
55
|
+
: theme.color.background.neutralBase};
|
|
56
|
+
color: ${isSelected
|
|
57
|
+
? theme.color.foreground.neutralAlt
|
|
58
|
+
: theme.color.foreground.neutralBaseDisabled};
|
|
59
|
+
font-family: ${theme.fontFamily.ui};
|
|
60
|
+
font-size: 14px;
|
|
61
|
+
font-weight: 800;
|
|
62
|
+
line-height: 16px;
|
|
63
|
+
`);
|
|
64
|
+
const Content = styled.div(({ theme }) => css `
|
|
65
|
+
display: flex;
|
|
66
|
+
flex-direction: column;
|
|
67
|
+
gap: 12px;
|
|
68
|
+
flex: 1;
|
|
69
|
+
font-family: ${theme.fontFamily.ui};
|
|
70
|
+
font-size: 14px;
|
|
71
|
+
font-weight: 400;
|
|
72
|
+
line-height: 20px;
|
|
73
|
+
`);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** @jsxImportSource @emotion/react */
|
|
2
|
+
import { NodeKey } from "lexical";
|
|
3
|
+
import { Selection } from "./ProblemSelectNode";
|
|
4
|
+
export declare function SelectComponent(props: {
|
|
5
|
+
selections: Selection[];
|
|
6
|
+
selected: string[];
|
|
7
|
+
hasMultipleAnswers?: boolean;
|
|
8
|
+
nodeKey: NodeKey;
|
|
9
|
+
}): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
12
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@emotion/react/jsx-runtime";
|
|
13
|
+
/** @jsxImportSource @emotion/react */
|
|
14
|
+
import { $getNodeByKey } from "lexical";
|
|
15
|
+
import { $isProblemSelectNode } from "./ProblemSelectNode";
|
|
16
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
17
|
+
import { useContext, useState } from "react";
|
|
18
|
+
import useLexicalEditable from "@lexical/react/useLexicalEditable";
|
|
19
|
+
import SelectBox from "./SelectBox";
|
|
20
|
+
import { css } from "@emotion/react";
|
|
21
|
+
import SquareButton from "../../../../components/SquareButton";
|
|
22
|
+
import { AlarmWarningFillIcon, Settings3FillIcon } from "../../../../icons";
|
|
23
|
+
import SettingForm from "./SettingForm";
|
|
24
|
+
import styled from "@emotion/styled";
|
|
25
|
+
import { LexicalCustomConfigContext } from "../../LexicalCustomConfigContext";
|
|
26
|
+
export function SelectComponent(props) {
|
|
27
|
+
const { selected, hasMultipleAnswers } = props, settingFormProps = __rest(props, ["selected", "hasMultipleAnswers"]);
|
|
28
|
+
const { selections, nodeKey } = settingFormProps;
|
|
29
|
+
const [editor] = useLexicalComposerContext();
|
|
30
|
+
const [settingOpen, setSettingOpen] = useState(false);
|
|
31
|
+
const isEditable = useLexicalEditable();
|
|
32
|
+
const lexicalCustomConfig = useContext(LexicalCustomConfigContext);
|
|
33
|
+
// 학생 view
|
|
34
|
+
if (!isEditable) {
|
|
35
|
+
return (_jsxs(_Fragment, { children: [hasMultipleAnswers && (_jsxs(Alert, { children: [_jsx(AlarmWarningFillIcon, { css: css `
|
|
36
|
+
width: 14px;
|
|
37
|
+
height: 14px;
|
|
38
|
+
` }), "\uC9C8\uBB38\uC5D0 \uD574\uB2F9\uD558\uB294 \uB2F5\uC744 \uBAA8\uB450 \uACE0\uB974\uB294 \uBB38\uC81C\uC785\uB2C8\uB2E4."] })), selections.map((selection, index) => (_jsx(SelectBox, { index: index + 1, isSelected: selected.includes(selection.value), image: selection.show.image, text: selection.show.text, onClick: () => {
|
|
39
|
+
if (lexicalCustomConfig.freezeProblemNode)
|
|
40
|
+
return;
|
|
41
|
+
const isSelected = selected.includes(selection.value);
|
|
42
|
+
if (isSelected) {
|
|
43
|
+
editor.update(() => {
|
|
44
|
+
const node = $getNodeByKey(nodeKey);
|
|
45
|
+
if (!$isProblemSelectNode(node)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
node.setSelected(selected.filter((v) => v !== selection.value));
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
editor.update(() => {
|
|
53
|
+
const node = $getNodeByKey(nodeKey);
|
|
54
|
+
if (!$isProblemSelectNode(node)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
node.setSelected([...selected, selection.value]);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}, fullWidth: true }, index)))] }));
|
|
61
|
+
}
|
|
62
|
+
// 교사 edit view
|
|
63
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", Object.assign({ css: css `
|
|
64
|
+
display: flex;
|
|
65
|
+
gap: 4px;
|
|
66
|
+
` }, { children: [_jsx("div", Object.assign({ css: css `
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-direction: column;
|
|
69
|
+
gap: 4px;
|
|
70
|
+
` }, { children: selections.map((selection, index) => (_jsx(SelectBox, { index: index + 1, isAnswer: selection.isAnswer, image: selection.show.image, text: selection.show.text || `${index + 1}번 선택지`, onClick: () => setSettingOpen(true) }, index))) })), _jsx(SquareButton, { size: "small", color: "icon", icon: _jsx(Settings3FillIcon, {}), onClick: () => {
|
|
71
|
+
setSettingOpen(true);
|
|
72
|
+
} })] })), settingOpen && (_jsx(SettingForm, Object.assign({}, settingFormProps, { onClose: () => setSettingOpen(false) })))] }));
|
|
73
|
+
}
|
|
74
|
+
const Alert = styled.div(({ theme }) => css `
|
|
75
|
+
display: flex;
|
|
76
|
+
gap: 4px;
|
|
77
|
+
margin-bottom: 12px;
|
|
78
|
+
color: ${theme.color.foreground.neutralBaseDisabled};
|
|
79
|
+
/* Default/Label/12px-Md */
|
|
80
|
+
font-family: ${theme.fontFamily.ui};
|
|
81
|
+
font-size: 12px;
|
|
82
|
+
font-style: normal;
|
|
83
|
+
font-weight: 500;
|
|
84
|
+
line-height: 16px; /* 133.333% */
|
|
85
|
+
`);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Selection } from "../ProblemSelectNode";
|
|
2
|
+
import { Control, UseFormWatch } from "react-hook-form";
|
|
3
|
+
export interface FormSelectionProps {
|
|
4
|
+
index: number;
|
|
5
|
+
control: Control<{
|
|
6
|
+
selections: Selection[];
|
|
7
|
+
}, any>;
|
|
8
|
+
watch: UseFormWatch<{
|
|
9
|
+
selections: Selection[];
|
|
10
|
+
}>;
|
|
11
|
+
rules?: any;
|
|
12
|
+
onDelete?: () => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function FormSelection(props: FormSelectionProps): import("@emotion/react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource @emotion/react */
|
|
3
|
+
import styled from "@emotion/styled";
|
|
4
|
+
import { Controller } from "react-hook-form";
|
|
5
|
+
import { css } from "@emotion/react";
|
|
6
|
+
import Input from "../../../../../components/Input";
|
|
7
|
+
import { DeleteBinLineIcon, ErrorWarningFillIcon, ImageAddFillIcon, ImageEditFillIcon, } from "../../../../../icons";
|
|
8
|
+
import SquareButton from "../../../../../components/SquareButton";
|
|
9
|
+
import Switch from "../../../../../components/Switch";
|
|
10
|
+
import { useState } from "react";
|
|
11
|
+
import { InsertImageDialog } from "./InsertImageDialog";
|
|
12
|
+
export function FormSelection(props) {
|
|
13
|
+
const { index, control, rules, watch, onDelete } = props;
|
|
14
|
+
const [imageOpen, setImageOpen] = useState(false);
|
|
15
|
+
const [inputFocused, setInputFocused] = useState(false);
|
|
16
|
+
return (_jsxs(Container, { children: [_jsx(Index, { children: index + 1 }), _jsxs("div", Object.assign({ css: css `
|
|
17
|
+
display: flex;
|
|
18
|
+
flex: 1;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
gap: 4px;
|
|
21
|
+
` }, { children: [_jsx(Controller, { name: `selections.${index}.show.image`, control: control, render: ({ field: { value, onChange } }) => (_jsxs(_Fragment, { children: [_jsx(InsertImageDialog, { title: value ? "이미지 바꾸기" : "이미지 삽입하기", open: imageOpen, onClose: () => setImageOpen(false), updateImg: (props) => {
|
|
22
|
+
onChange(props);
|
|
23
|
+
}, deleteButton: value !== null }), value && value.src && (_jsx("img", { src: value.src, alt: value.altText, css: css `
|
|
24
|
+
height: auto;
|
|
25
|
+
// 이미지로 인해 좌우로 스크롤이 생기는 것을 방지
|
|
26
|
+
max-width: min(400px, 100%);
|
|
27
|
+
width: fit-content;
|
|
28
|
+
border-radius: 6px;
|
|
29
|
+
`, draggable: "false" }))] })) }), _jsx(Controller, { name: `selections.${index}.show.text`, control: control, rules: rules, render: ({ field: { value, onChange }, fieldState: { invalid, error }, }) => (_jsx(Input, { size: "small", color: invalid
|
|
30
|
+
? "activeDanger"
|
|
31
|
+
: inputFocused
|
|
32
|
+
? "activePrimary"
|
|
33
|
+
: "default", value: value, onChange: onChange, inputProps: {
|
|
34
|
+
onFocus: (_e) => {
|
|
35
|
+
setInputFocused(true);
|
|
36
|
+
},
|
|
37
|
+
onBlur: (_e) => {
|
|
38
|
+
setInputFocused(false);
|
|
39
|
+
},
|
|
40
|
+
}, placeholder: `${index + 1}번 선택지`, hintIcon: invalid ? _jsx(ErrorWarningFillIcon, {}) : undefined, hintText: error === null || error === void 0 ? void 0 : error.message, multiline: true, fullWidth: true, css: css `
|
|
41
|
+
flex: 1;
|
|
42
|
+
` })) })] })), _jsxs("div", Object.assign({ css: css `
|
|
43
|
+
display: flex;
|
|
44
|
+
height: 36px;
|
|
45
|
+
gap: 8px;
|
|
46
|
+
align-items: center;
|
|
47
|
+
` }, { children: [_jsx(SquareButton, { color: "icon", size: "xsmall", icon: watch(`selections.${index}.show.image`) ? (_jsx(ImageEditFillIcon, {})) : (_jsx(ImageAddFillIcon, {})), onClick: () => {
|
|
48
|
+
setImageOpen(true);
|
|
49
|
+
} }), _jsx(Controller, { name: `selections.${index}.isAnswer`, control: control, render: ({ field: { value, onChange } }) => (_jsxs(Answer, Object.assign({ onClick: () => {
|
|
50
|
+
onChange(!value);
|
|
51
|
+
} }, { children: ["\uC815\uB2F5", _jsx(Switch, { checked: value, size: "small" })] }))) }), onDelete && (_jsx(SquareButton, { color: "white", size: "xsmall", icon: _jsx(DeleteBinLineIcon, {}), onClick: onDelete }))] }))] }));
|
|
52
|
+
}
|
|
53
|
+
const Container = styled.div(({ theme }) => css `
|
|
54
|
+
display: flex;
|
|
55
|
+
padding: 4px 12px;
|
|
56
|
+
gap: 8px;
|
|
57
|
+
border-radius: 8px;
|
|
58
|
+
background: ${theme.color.background.neutralAlt};
|
|
59
|
+
`);
|
|
60
|
+
const Index = styled.div(({ theme }) => css `
|
|
61
|
+
display: flex;
|
|
62
|
+
box-sizing: border-box;
|
|
63
|
+
width: 20px;
|
|
64
|
+
height: 20px;
|
|
65
|
+
padding: 4px;
|
|
66
|
+
margin-top: 8px;
|
|
67
|
+
justify-content: center;
|
|
68
|
+
align-items: center;
|
|
69
|
+
border-radius: 4px;
|
|
70
|
+
border: 1px solid ${theme.color.background.neutralAltActive};
|
|
71
|
+
background: ${theme.color.background.neutralBase};
|
|
72
|
+
color: ${theme.color.foreground.neutralBaseDisabled};
|
|
73
|
+
font-family: ${theme.fontFamily.ui};
|
|
74
|
+
font-size: 14px;
|
|
75
|
+
font-weight: 800;
|
|
76
|
+
line-height: 16px;
|
|
77
|
+
`);
|
|
78
|
+
const Answer = styled.div(({ theme }) => css `
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
padding-right: 4px;
|
|
82
|
+
gap: 8px;
|
|
83
|
+
color: ${theme.color.foreground.neutralBase};
|
|
84
|
+
cursor: pointer;
|
|
85
|
+
/* Default/Label/14px-Md */
|
|
86
|
+
font-family: ${theme.fontFamily.ui};
|
|
87
|
+
font-size: 14px;
|
|
88
|
+
font-style: normal;
|
|
89
|
+
font-weight: 500;
|
|
90
|
+
line-height: 16px; /* 114.286% */
|
|
91
|
+
`);
|
package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SettingForm/InsertImageDialog.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ImageProps } from "../ProblemSelectNode";
|
|
2
|
+
export interface InsertImageDialogProps {
|
|
3
|
+
title: string;
|
|
4
|
+
open: boolean;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
updateImg: (props: ImageProps | null) => void;
|
|
7
|
+
/** 전달하면 "이미지 삭제하기" 버튼이 생깁니다. */
|
|
8
|
+
deleteButton?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function InsertImageDialog(props: InsertImageDialogProps): import("@emotion/react/jsx-runtime").JSX.Element;
|