@team-monolith/cds 1.8.7 → 1.9.0

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.
Files changed (30) hide show
  1. package/dist/patterns/LexicalEditor/LexicalEditor.js +2 -0
  2. package/dist/patterns/LexicalEditor/Plugins.js +2 -1
  3. package/dist/patterns/LexicalEditor/nodes/ProblemInputNode/InputComponent.js +4 -1
  4. package/dist/patterns/LexicalEditor/nodes/ProblemInputNode/SettingForm/FormSolution.d.ts +2 -2
  5. package/dist/patterns/LexicalEditor/nodes/ProblemInputNode/SettingForm/FormSolution.js +3 -21
  6. package/dist/patterns/LexicalEditor/nodes/ProblemInputNode/SettingForm/SettingForm.js +5 -5
  7. package/dist/patterns/LexicalEditor/nodes/ProblemInputNode/SettingForm/TextTypeDropdown.d.ts +7 -0
  8. package/dist/patterns/LexicalEditor/nodes/ProblemInputNode/SettingForm/TextTypeDropdown.js +26 -0
  9. package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/ProblemSelectNode.d.ts +39 -0
  10. package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/ProblemSelectNode.js +72 -0
  11. package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SelectBox.d.ts +12 -0
  12. package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SelectBox.js +66 -0
  13. package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SelectComponent.d.ts +8 -0
  14. package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SelectComponent.js +73 -0
  15. package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SettingForm/FormSelection.d.ts +11 -0
  16. package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SettingForm/FormSelection.js +64 -0
  17. package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SettingForm/SettingForm.d.ts +15 -0
  18. package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SettingForm/SettingForm.js +104 -0
  19. package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SettingForm/index.d.ts +1 -0
  20. package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/SettingForm/index.js +1 -0
  21. package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/index.d.ts +2 -0
  22. package/dist/patterns/LexicalEditor/nodes/ProblemSelectNode/index.js +2 -0
  23. package/dist/patterns/LexicalEditor/plugins/ComponentAdderPlugin/ComponentAdderPlugin.js +2 -2
  24. package/dist/patterns/LexicalEditor/plugins/ComponentPickerMenuPlugin/ComponentPickerMenuPlugin.d.ts +1 -2
  25. package/dist/patterns/LexicalEditor/plugins/ComponentPickerMenuPlugin/ComponentPickerMenuPlugin.js +15 -4
  26. package/dist/patterns/LexicalEditor/plugins/ProblemSelectPlugin/index.d.ts +5 -0
  27. package/dist/patterns/LexicalEditor/plugins/ProblemSelectPlugin/index.js +20 -0
  28. package/dist/patterns/LexicalEditor/theme.d.ts +1 -0
  29. package/dist/patterns/LexicalEditor/theme.js +7 -0
  30. package/package.json +1 -1
@@ -11,6 +11,7 @@ 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 { ProblemSelectNode } from "./nodes/ProblemSelectNode";
14
15
  function validateValue(value) {
15
16
  var _a, _b;
16
17
  if (value && typeof value !== "object") {
@@ -35,6 +36,7 @@ export function LexicalEditor(props) {
35
36
  onError: (error) => console.error(error),
36
37
  nodes: [
37
38
  ProblemInputNode,
39
+ ProblemSelectNode,
38
40
  HeadingNode,
39
41
  ListNode,
40
42
  ListItemNode,
@@ -28,6 +28,7 @@ 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
33
  const { className, onChange } = props;
33
34
  const isEditable = useLexicalEditable();
@@ -42,7 +43,7 @@ export default function Plugins(props) {
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;
@@ -32,7 +32,10 @@ export function InputComponent(props) {
32
32
  // 학생 view
33
33
  // TODO: "글자 수대로" 옵션시에 글자 수대로 입력칸을 표시해야 합니다.
34
34
  if (!isEditable) {
35
- return (_jsx(Input, { size: "small", placeholder: placeholder || "여기에 입력하세요.", value: answerInput, onChange: (e) => setAnswerInput(e.target.value), inputProps: {
35
+ return (_jsx(Input, { size: "small", placeholder: placeholder || "여기에 입력하세요.", value: answerInput, onChange: (e) => setAnswerInput(e.target.value),
36
+ // 한글 입력시에 onChange마다 update가 일어나는 것을 방지하기 위해 입력 완료후 onBlur시에 update하는 전략을 사용합니다.
37
+ // 이를 위해 answerInput을 state로 관리합니다.
38
+ inputProps: {
36
39
  onBlur: (_e) => {
37
40
  editor.update(() => {
38
41
  const node = $getNodeByKey(nodeKey);
@@ -1,9 +1,9 @@
1
1
  import { Control } from "react-hook-form";
2
2
  import { ProblemInputPayload } from "../ProblemInputNode";
3
- export interface FormAnswerProps {
3
+ export interface FormSolutionProps {
4
4
  index: number;
5
5
  control: Control<ProblemInputPayload, any>;
6
6
  rules?: any;
7
7
  onDelete?: () => void;
8
8
  }
9
- export declare function FormSolution(props: FormAnswerProps): import("@emotion/react/jsx-runtime").JSX.Element;
9
+ export declare function FormSolution(props: FormSolutionProps): import("@emotion/react/jsx-runtime").JSX.Element;
@@ -2,39 +2,21 @@ import { jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
2
2
  /** @jsxImportSource @emotion/react */
3
3
  import { css, useTheme } from "@emotion/react";
4
4
  import { Controller } from "react-hook-form";
5
- import Dropdown from "../../../../Dropdown";
6
- import DropdownItem from "../../../../Dropdown/DropdownItem";
7
5
  import Input from "../../../../../components/Input";
8
6
  import { AlertFillIcon, DeleteBinLineIcon, ErrorWarningFillIcon, } from "../../../../../icons";
9
7
  import SquareButton from "../../../../../components/SquareButton";
10
8
  import Tooltip from "../../../../../components/Tooltip";
9
+ import TextTypeDropdown from "./TextTypeDropdown";
11
10
  export function FormSolution(props) {
12
11
  const { index, control, rules, onDelete } = props;
13
12
  const theme = useTheme();
14
- const TextTypeDropdown = (disabled) => (_jsx(Controller, { name: `solutions.${index}.textType`, control: control, render: ({ field: { value, onChange } }) => (_jsx(Dropdown, Object.assign({ label: value === "normal" ? "일반 텍스트" : "코드 텍스트", size: "xsmall", color: "textNeutral", closeOnItemClick: true, disabled: disabled, buttonCss: css `
15
- ${disabled && `color: ${theme.color.foreground.neutralAlt};`}
16
- > span {
17
- font-weight: 700;
18
- }
19
- `, menuProps: {
20
- anchorOrigin: {
21
- vertical: "bottom",
22
- horizontal: "center",
23
- },
24
- transformOrigin: {
25
- vertical: "top",
26
- horizontal: "center",
27
- },
28
- } }, { children: _jsx(DropdownItem, { index: 0, label: value === "normal" ? "코드 텍스트" : "일반 텍스트", onClick: () => {
29
- onChange(value === "normal" ? "code" : "normal");
30
- } }) }))) }));
31
13
  return (_jsx(Controller, { name: `solutions.${index}.value`, control: control, rules: rules, render: ({ field: { value, onChange }, fieldState: { invalid, error }, }) => {
32
- const disabled = (error === null || error === void 0 ? void 0 : error.type) === "multiAnswerDisabled";
14
+ const disabled = (error === null || error === void 0 ? void 0 : error.type) === "enabled";
33
15
  return (_jsx(Input, { size: "small", color: invalid ? "activeDanger" : "default", onChange: onChange, disabled: disabled, value: value, hintIcon: !disabled && invalid ? _jsx(ErrorWarningFillIcon, {}) : undefined, hintText: !disabled ? error === null || error === void 0 ? void 0 : error.message : undefined, placeholder: "\uC548\uB155\uD558\uC138\uC694", fullWidth: true, css: css `
34
16
  > div {
35
17
  padding: 4px 12px;
36
18
  }
37
- `, startIcon: TextTypeDropdown(disabled), endIcon: _jsxs("div", Object.assign({ css: css `
19
+ `, startIcon: _jsx(TextTypeDropdown, { index: index, control: control, disabled: disabled }), endIcon: _jsxs("div", Object.assign({ css: css `
38
20
  display: flex;
39
21
  gap: 4px;
40
22
  ` }, { children: [onDelete && (_jsx(SquareButton, { color: "white", size: "xsmall", icon: _jsx(DeleteBinLineIcon, {}), onClick: onDelete })), disabled && (_jsx(Tooltip, Object.assign({ text: _jsxs("span", { children: ["\uC785\uB825 \uCE78 \uC124\uC815\uC774 '\uAE00\uC790 \uC218\uB300\uB85C'\uC778 \uACBD\uC6B0", _jsx("br", {}), "\uC815\uB2F5\uC744 \uD558\uB098\uB9CC \uB4F1\uB85D\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4."] }) }, { children: _jsx(SquareButton, { color: "danger", size: "xsmall", icon: _jsx(AlertFillIcon, { color: theme.color.foreground.neutralAlt }), disabled: true }) })))] })) }));
@@ -4,7 +4,7 @@ import { css, useTheme } from "@emotion/react";
4
4
  import styled from "@emotion/styled";
5
5
  import shadows from "../../../../../foundation/shadows";
6
6
  import { AddFillIcon, AlarmWarningFillIcon, InputMethodLineIcon, QuestionFillIcon, } from "../../../../../icons";
7
- import { useFieldArray, useForm, } from "react-hook-form";
7
+ import { useFieldArray, useForm } from "react-hook-form";
8
8
  import { $isProblemInputNode, } from "../ProblemInputNode";
9
9
  import { FormSolution } from "./FormSolution";
10
10
  import Button from "../../../../../components/Button";
@@ -42,15 +42,15 @@ export default function SettingForm(props) {
42
42
  });
43
43
  onClose();
44
44
  };
45
- const multiAnswerDisabled = watch("showCharacterCount");
45
+ const multipleSolutionsDisabled = watch("showCharacterCount");
46
46
  return (_jsxs(Form, Object.assign({ onSubmit: handleSubmit(onSettingSubmit) }, { children: [_jsxs(Title, { children: [_jsx(InputMethodLineIcon, { css: css `
47
47
  width: 12px;
48
48
  height: 12px;
49
49
  ` }), "\uC8FC\uAD00\uC2DD \uC785\uB825 \uCE78"] }), _jsxs(Content, { children: [_jsxs(Left, { children: [_jsxs(FormArea, { children: [_jsx(Label, { children: "\uC815\uB2F5" }), fields.map((field, index) => (_jsx(FormSolution, { index: index, control: control, rules: {
50
50
  validate: {
51
51
  // required 옵션보다 먼저 검증되어야 하는데 priority 옵션이 없어서 validate에서 통합해서 검증합니다.
52
- multiAnswerDisabled: () => index === 0 ||
53
- !multiAnswerDisabled ||
52
+ enabled: () => index === 0 ||
53
+ !multipleSolutionsDisabled ||
54
54
  "복수 정답이 불가능합니다.",
55
55
  required: (value) => value !== "" || "정답을 입력해주세요.",
56
56
  },
@@ -58,7 +58,7 @@ export default function SettingForm(props) {
58
58
  ? () => {
59
59
  remove(index);
60
60
  }
61
- : undefined }, field.uid)))] }), _jsx(Button, { color: "grey", size: "small", startIcon: _jsx(AddFillIcon, {}), label: "\uBCF5\uC218 \uC815\uB2F5 \uCD94\uAC00", disabled: multiAnswerDisabled, onClick: () => {
61
+ : undefined }, field.uid)))] }), _jsx(Button, { color: "grey", size: "small", startIcon: _jsx(AddFillIcon, {}), label: "\uBCF5\uC218 \uC815\uB2F5 \uCD94\uAC00", disabled: multipleSolutionsDisabled, onClick: () => {
62
62
  append({
63
63
  textType: "normal",
64
64
  value: "",
@@ -0,0 +1,7 @@
1
+ import { Control } from "react-hook-form";
2
+ import { ProblemInputPayload } from "../ProblemInputNode";
3
+ export default function TextTypeDropdown(props: {
4
+ index: number;
5
+ control: Control<ProblemInputPayload, any>;
6
+ disabled: boolean;
7
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Controller } from "react-hook-form";
3
+ import Dropdown from "../../../../Dropdown";
4
+ import { css, useTheme } from "@emotion/react";
5
+ import DropdownItem from "../../../../Dropdown/DropdownItem";
6
+ export default function TextTypeDropdown(props) {
7
+ const theme = useTheme();
8
+ const { index, control, disabled } = props;
9
+ return (_jsx(Controller, { name: `solutions.${index}.textType`, control: control, render: ({ field: { value, onChange } }) => (_jsx(Dropdown, Object.assign({ label: value === "normal" ? "일반 텍스트" : "코드 텍스트", size: "xsmall", color: "textNeutral", closeOnItemClick: true, disabled: disabled, buttonCss: css `
10
+ ${disabled && `color: ${theme.color.foreground.neutralAlt};`}
11
+ > span {
12
+ font-weight: 700;
13
+ }
14
+ `, menuProps: {
15
+ anchorOrigin: {
16
+ vertical: "bottom",
17
+ horizontal: "center",
18
+ },
19
+ transformOrigin: {
20
+ vertical: "top",
21
+ horizontal: "center",
22
+ },
23
+ } }, { children: _jsx(DropdownItem, { index: 0, label: value === "normal" ? "코드 텍스트" : "일반 텍스트", onClick: () => {
24
+ onChange(value === "normal" ? "code" : "normal");
25
+ } }) }))) }));
26
+ }
@@ -0,0 +1,39 @@
1
+ import { DecoratorNode, EditorConfig, LexicalNode, NodeKey, SerializedLexicalNode, Spread } from "lexical";
2
+ import { ReactNode } from "react";
3
+ export interface Selection {
4
+ isAnswer: boolean;
5
+ show: {
6
+ image?: any;
7
+ text: string;
8
+ };
9
+ value: string;
10
+ }
11
+ export interface ProblemSelectPayload {
12
+ selections: Selection[];
13
+ selected: string[];
14
+ key?: NodeKey;
15
+ }
16
+ export type SerializedProblemSelectNode = Spread<ProblemSelectPayload, SerializedLexicalNode>;
17
+ /**
18
+ * selections는 Selection타입의 배열로서 객관식 정보를 담고 있습니다. (교사용)
19
+ * selected는 학생이 선택한 답의 value를 담고 있습니다.(학생용)
20
+ */
21
+ export declare class ProblemSelectNode extends DecoratorNode<ReactNode> {
22
+ __selections: Selection[];
23
+ __selected: string[];
24
+ isInline(): boolean;
25
+ static getType(): string;
26
+ getSelections(): Selection[];
27
+ getSelected(): string[];
28
+ setSolutions(selections: Selection[]): void;
29
+ setSelected(selected: string[]): void;
30
+ static clone(node: ProblemSelectNode): ProblemSelectNode;
31
+ constructor(selections: Selection[], selected: string[], key?: NodeKey);
32
+ createDOM(config: EditorConfig): HTMLElement;
33
+ updateDOM(): boolean;
34
+ static importJSON(serializedNode: SerializedProblemSelectNode): ProblemSelectNode;
35
+ exportJSON(): SerializedProblemSelectNode;
36
+ decorate(): ReactNode;
37
+ }
38
+ export declare function $createProblemSelectNode({ selections, selected, key, }: ProblemSelectPayload): ProblemSelectNode;
39
+ export declare function $isProblemSelectNode(node: LexicalNode | null | undefined): node is ProblemSelectNode;
@@ -0,0 +1,72 @@
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
+ setSolutions(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.__key);
32
+ }
33
+ constructor(selections, selected, key) {
34
+ super(key);
35
+ this.__selections = selections;
36
+ this.__selected = selected;
37
+ }
38
+ createDOM(config) {
39
+ // Define the DOM element here
40
+ const root = document.createElement("div");
41
+ addClassNamesToElement(root, config.theme.problemSelect);
42
+ return root;
43
+ }
44
+ updateDOM() {
45
+ return false;
46
+ }
47
+ static importJSON(serializedNode) {
48
+ const node = $createProblemSelectNode({
49
+ key: serializedNode.key,
50
+ selections: serializedNode.selections,
51
+ selected: serializedNode.selected,
52
+ });
53
+ return node;
54
+ }
55
+ exportJSON() {
56
+ return {
57
+ version: 1,
58
+ type: "problem-select",
59
+ selections: this.__selections,
60
+ selected: this.__selected,
61
+ };
62
+ }
63
+ decorate() {
64
+ return (_jsx(SelectComponent, { selections: this.__selections, selected: this.__selected, nodeKey: this.getKey() }));
65
+ }
66
+ }
67
+ export function $createProblemSelectNode({ selections, selected, key, }) {
68
+ return $applyNodeReplacement(new ProblemSelectNode(selections, selected, key));
69
+ }
70
+ export function $isProblemSelectNode(node) {
71
+ return node instanceof ProblemSelectNode;
72
+ }
@@ -0,0 +1,12 @@
1
+ /** @jsxImportSource @emotion/react */
2
+ import { ImagePayload } from "../ImageNode";
3
+ export interface SelectBoxProps {
4
+ index: number;
5
+ isSelected?: boolean;
6
+ isAnswer?: boolean;
7
+ image?: ImagePayload;
8
+ text: string;
9
+ onClick: () => void;
10
+ fullWidth?: boolean;
11
+ }
12
+ export default function SelectBox(props: SelectBoxProps): import("@emotion/react/jsx-runtime").JSX.Element;
@@ -0,0 +1,66 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
2
+ import styled from "@emotion/styled";
3
+ import { css, useTheme } from "@emotion/react";
4
+ import { CheckFillIcon, CheckboxCircleFillIcon } from "../../../../icons";
5
+ export default function SelectBox(props) {
6
+ const { index, isSelected, isAnswer, image, text, onClick, fullWidth } = props;
7
+ const theme = useTheme();
8
+ return (_jsxs(Container, Object.assign({ isSelected: isSelected, fullWidth: fullWidth, onClick: onClick }, { children: [_jsx(Index, Object.assign({ isSelected: isSelected }, { children: isSelected ? (_jsx(CheckFillIcon, { css: css `
9
+ width: 12px;
10
+ height: 12px;
11
+ ` })) : (index) })), _jsx(Content, { children: text }), isAnswer && (_jsx(CheckboxCircleFillIcon, { color: theme.color.foreground.success, css: css `
12
+ width: 16px;
13
+ height: 16px;
14
+ ` }))] })));
15
+ }
16
+ const Container = styled.div(({ theme, isSelected, fullWidth }) => css `
17
+ cursor: pointer;
18
+ display: flex;
19
+ box-sizing: border-box;
20
+ width: ${fullWidth ? "100%" : "400px"};
21
+ padding: 8px;
22
+ gap: 8px;
23
+ border-radius: 8px;
24
+ background: ${isSelected
25
+ ? theme.color.container.primaryContainer
26
+ : theme.color.background.neutralAlt};
27
+ border: ${isSelected
28
+ ? `1px solid ${theme.color.foreground.primary}`
29
+ : "1px solid transparent"};
30
+ color: ${isSelected
31
+ ? theme.color.container.primaryOnContainer
32
+ : theme.color.foreground.neutralBase};
33
+ `);
34
+ const Index = styled.div(({ theme, isSelected }) => css `
35
+ display: flex;
36
+ box-sizing: border-box;
37
+ width: 20px;
38
+ height: 20px;
39
+ padding: 4px;
40
+ justify-content: center;
41
+ align-items: center;
42
+ border-radius: 4px;
43
+ border: ${isSelected
44
+ ? "none"
45
+ : `1px solid ${theme.color.background.neutralAltActive}`};
46
+ background: ${isSelected
47
+ ? theme.color.background.primary
48
+ : theme.color.background.neutralBase};
49
+ color: ${isSelected
50
+ ? theme.color.foreground.neutralAlt
51
+ : theme.color.foreground.neutralBaseDisabled};
52
+ font-family: ${theme.fontFamily.ui};
53
+ font-size: 14px;
54
+ font-weight: 800;
55
+ line-height: 16px;
56
+ `);
57
+ const Content = styled.div(({ theme }) => css `
58
+ display: flex;
59
+ flex-direction: column;
60
+ gap: 12px;
61
+ flex: 1;
62
+ font-family: ${theme.fontFamily.ui};
63
+ font-size: 14px;
64
+ font-weight: 400;
65
+ line-height: 20px;
66
+ `);
@@ -0,0 +1,8 @@
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
+ nodeKey: NodeKey;
8
+ }): import("@emotion/react/jsx-runtime").JSX.Element;
@@ -0,0 +1,73 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
2
+ /** @jsxImportSource @emotion/react */
3
+ import { $getNodeByKey } from "lexical";
4
+ import { $isProblemSelectNode, } from "./ProblemSelectNode";
5
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
6
+ import { useState } from "react";
7
+ import useLexicalEditable from "@lexical/react/useLexicalEditable";
8
+ import { useFieldArray, useForm } from "react-hook-form";
9
+ import SelectBox from "./SelectBox";
10
+ import { css } from "@emotion/react";
11
+ import SquareButton from "../../../../components/SquareButton";
12
+ import { Settings3FillIcon } from "../../../../icons";
13
+ import SettingForm from "./SettingForm";
14
+ export function SelectComponent(props) {
15
+ const { selections, selected, nodeKey } = props;
16
+ const [editor] = useLexicalComposerContext();
17
+ const [settingOpen, setSettingOpen] = useState(false);
18
+ const isEditable = useLexicalEditable();
19
+ const { control, handleSubmit } = useForm({
20
+ mode: "all",
21
+ defaultValues: {
22
+ selections,
23
+ selected,
24
+ },
25
+ });
26
+ const { fields, append, remove, update } = useFieldArray({
27
+ control,
28
+ name: "selections",
29
+ keyName: "uid",
30
+ });
31
+ // 학생 view
32
+ if (!isEditable) {
33
+ return (_jsx(_Fragment, { children: selections.map((selection, index) => {
34
+ return (_jsx(SelectBox, { index: index + 1, isSelected: selected.includes(selection.value), text: selection.show.text, onClick: () => {
35
+ const isSelected = selected.includes(selection.value);
36
+ if (isSelected) {
37
+ editor.update(() => {
38
+ const node = $getNodeByKey(nodeKey);
39
+ if (!$isProblemSelectNode(node)) {
40
+ return;
41
+ }
42
+ const newSelected = [...selected];
43
+ const index = newSelected.indexOf(selection.value);
44
+ newSelected.splice(index, 1);
45
+ node.setSelected(newSelected);
46
+ });
47
+ }
48
+ else {
49
+ editor.update(() => {
50
+ const node = $getNodeByKey(nodeKey);
51
+ if (!$isProblemSelectNode(node)) {
52
+ return;
53
+ }
54
+ const newSelected = [...selected];
55
+ newSelected.push(selection.value);
56
+ node.setSelected(newSelected);
57
+ });
58
+ }
59
+ }, fullWidth: true }, index));
60
+ }) }));
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: fields.map((field, index) => (_jsx(SelectBox, { index: index + 1, isAnswer: field.isAnswer, text: field.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, { control: control, handleSubmit: handleSubmit, fields: fields, append: append, remove: remove, update: update, nodeKey: nodeKey, onClose: () => setSettingOpen(false) }))] }));
73
+ }
@@ -0,0 +1,11 @@
1
+ import { ProblemSelectPayload } from "../ProblemSelectNode";
2
+ import { Control, FieldArrayWithId, UseFieldArrayUpdate } from "react-hook-form";
3
+ export interface FormSelectionProps {
4
+ index: number;
5
+ control: Control<ProblemSelectPayload, any>;
6
+ field: FieldArrayWithId<ProblemSelectPayload, "selections", "uid">;
7
+ update: UseFieldArrayUpdate<ProblemSelectPayload, "selections">;
8
+ rules?: any;
9
+ onDelete?: () => void;
10
+ }
11
+ export declare function FormSelection(props: FormSelectionProps): import("@emotion/react/jsx-runtime").JSX.Element;
@@ -0,0 +1,64 @@
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 { Controller, } from "react-hook-form";
5
+ import { css } from "@emotion/react";
6
+ import Input from "../../../../../components/Input";
7
+ import { DeleteBinLineIcon, ErrorWarningFillIcon, ImageAddFillIcon, } from "../../../../../icons";
8
+ import SquareButton from "../../../../../components/SquareButton";
9
+ import Switch from "../../../../../components/Switch";
10
+ export function FormSelection(props) {
11
+ const { index, control, field, update, rules, onDelete } = props;
12
+ return (_jsxs(Container, { children: [_jsx(Index, { children: index + 1 }), _jsx(Controller, { name: `selections.${index}.show.text`, control: control, rules: rules, render: ({ field: { value, onChange }, fieldState: { invalid, error }, }) => (_jsx(Input, { size: "small", color: invalid ? "activeDanger" : "default", value: value, onChange: onChange, inputProps: {
13
+ onBlur: (_e) => {
14
+ // onBlur시에 선택지 미리보기에 반영합니다.
15
+ update(index, Object.assign(Object.assign({}, field), { show: Object.assign(Object.assign({}, field.show), { text: value }) }));
16
+ },
17
+ }, placeholder: `${index + 1}번 선택지`, hintIcon: invalid ? _jsx(ErrorWarningFillIcon, {}) : undefined, hintText: error === null || error === void 0 ? void 0 : error.message, multiline: true, fullWidth: true, css: css `
18
+ flex: 1;
19
+ ` })) }), _jsx(SquareButton, { color: "icon", size: "xsmall", icon: _jsx(ImageAddFillIcon, {}), onClick: () => {
20
+ // TODO: 이미지 추가
21
+ } }), _jsx(Controller, { name: `selections.${index}.isAnswer`, control: control, render: ({ field: { value, onChange } }) => (_jsxs(Answer, Object.assign({ onClick: () => {
22
+ onChange(!value);
23
+ // 선택지 미리보기에 정답여부를 반영합니다.
24
+ update(index, Object.assign(Object.assign({}, field), { isAnswer: !value }));
25
+ } }, { children: ["\uC815\uB2F5", _jsx(Switch, { checked: value, size: "small" })] }))) }), onDelete && (_jsx(SquareButton, { color: "white", size: "xsmall", icon: _jsx(DeleteBinLineIcon, {}), onClick: onDelete }))] }));
26
+ }
27
+ const Container = styled.div(({ theme }) => css `
28
+ display: flex;
29
+ padding: 4px 12px;
30
+ gap: 8px;
31
+ align-items: center;
32
+ border-radius: 8px;
33
+ background: ${theme.color.background.neutralAlt};
34
+ `);
35
+ const Index = styled.div(({ theme }) => css `
36
+ display: flex;
37
+ box-sizing: border-box;
38
+ width: 20px;
39
+ height: 20px;
40
+ padding: 4px;
41
+ justify-content: center;
42
+ align-items: center;
43
+ border-radius: 4px;
44
+ border: 1px solid ${theme.color.background.neutralAltActive};
45
+ background: ${theme.color.background.neutralBase};
46
+ color: ${theme.color.foreground.neutralBaseDisabled};
47
+ font-family: ${theme.fontFamily.ui};
48
+ font-size: 14px;
49
+ font-weight: 800;
50
+ line-height: 16px;
51
+ `);
52
+ const Answer = styled.div(({ theme }) => css `
53
+ display: flex;
54
+ padding-right: 4px;
55
+ gap: 8px;
56
+ color: ${theme.color.foreground.neutralBase};
57
+ cursor: pointer;
58
+ /* Default/Label/14px-Md */
59
+ font-family: ${theme.fontFamily.ui};
60
+ font-size: 14px;
61
+ font-style: normal;
62
+ font-weight: 500;
63
+ line-height: 16px; /* 114.286% */
64
+ `);
@@ -0,0 +1,15 @@
1
+ /** @jsxImportSource @emotion/react */
2
+ import { Control, FieldArrayWithId, UseFieldArrayAppend, UseFieldArrayRemove, UseFieldArrayUpdate, UseFormHandleSubmit } from "react-hook-form";
3
+ import { ProblemSelectPayload } from "../ProblemSelectNode";
4
+ import { NodeKey } from "lexical";
5
+ export interface SettingFormProps {
6
+ control: Control<ProblemSelectPayload, any>;
7
+ handleSubmit: UseFormHandleSubmit<ProblemSelectPayload, undefined>;
8
+ fields: FieldArrayWithId<ProblemSelectPayload, "selections", "uid">[];
9
+ append: UseFieldArrayAppend<ProblemSelectPayload, "selections">;
10
+ remove: UseFieldArrayRemove;
11
+ update: UseFieldArrayUpdate<ProblemSelectPayload, "selections">;
12
+ nodeKey: NodeKey;
13
+ onClose: () => void;
14
+ }
15
+ export default function SettingForm(props: SettingFormProps): import("@emotion/react/jsx-runtime").JSX.Element;
@@ -0,0 +1,104 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
2
+ import { $isProblemSelectNode, } from "../ProblemSelectNode";
3
+ import { $getNodeByKey } from "lexical";
4
+ import { css } from "@emotion/react";
5
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
6
+ import styled from "@emotion/styled";
7
+ import shadows from "../../../../../foundation/shadows";
8
+ import { AddFillIcon, ListRadioIcon } from "../../../../../icons";
9
+ import Button from "../../../../../components/Button";
10
+ import { FormSelection } from "./FormSelection";
11
+ export default function SettingForm(props) {
12
+ const { control, handleSubmit, fields, append, remove, update, nodeKey, onClose, } = props;
13
+ const [editor] = useLexicalComposerContext();
14
+ const onSettingSubmit = (data) => {
15
+ editor.update(() => {
16
+ const node = $getNodeByKey(nodeKey);
17
+ if (!$isProblemSelectNode(node)) {
18
+ return;
19
+ }
20
+ node.setSolutions(data.selections);
21
+ });
22
+ onClose();
23
+ };
24
+ function validateDuplicatedSelection(fields, index) {
25
+ if (index === 0)
26
+ return true;
27
+ const duplicatedIndex = fields
28
+ .slice(0, index)
29
+ .findIndex((prevField) => prevField.show.text === fields[index].show.text);
30
+ if (duplicatedIndex < 0)
31
+ return true;
32
+ return `${duplicatedIndex + 1}번 선택지와 같은 내용입니다.`;
33
+ }
34
+ return (_jsxs(Form, Object.assign({ onSubmit: handleSubmit(onSettingSubmit) }, { children: [_jsxs(Title, { children: [_jsx(ListRadioIcon, { css: css `
35
+ width: 12px;
36
+ height: 12px;
37
+ ` }), "\uAC1D\uAD00\uC2DD \uC785\uB825 \uCE78"] }), _jsxs(Content, { children: [_jsxs(FormArea, { children: [_jsx(Label, { children: "\uB2F5\uC548" }), fields.map((field, index) => (_jsx(FormSelection, { index: index, control: control, field: field, update: update, rules: {
38
+ required: "필수 입력 항목입니다.",
39
+ validate: () => validateDuplicatedSelection(fields, index),
40
+ }, onDelete: index !== 0
41
+ ? () => {
42
+ remove(index);
43
+ }
44
+ : undefined }, field.uid)))] }), _jsx(Button, { color: "grey", size: "small", startIcon: _jsx(AddFillIcon, {}), label: "\uC120\uD0DD\uC9C0 \uCD94\uAC00", onClick: () => {
45
+ append({
46
+ isAnswer: false,
47
+ show: {
48
+ text: "",
49
+ },
50
+ value: (fields.length + 1).toString(),
51
+ });
52
+ } })] }), _jsxs(Buttons, { children: [_jsx(Button, { color: "grey", size: "xsmall", label: "\uB2EB\uAE30", onClick: onClose }), _jsx(Button, { color: "primary", size: "xsmall", label: "\uC774\uB300\uB85C \uB123\uAE30", type: "submit" })] })] })));
53
+ }
54
+ const Form = styled.form(({ theme }) => css `
55
+ display: flex;
56
+ width: 620px;
57
+ flex-direction: column;
58
+ border-radius: 6px;
59
+ border: 1px solid ${theme.color.background.neutralAltActive};
60
+ background: ${theme.color.background.neutralBase};
61
+ box-shadow: ${shadows.shadow08};
62
+ `);
63
+ const Title = styled.div(({ theme }) => css `
64
+ display: flex;
65
+ padding: 8px 12px;
66
+ gap: 4px;
67
+ align-items: center;
68
+ color: ${theme.color.foreground.neutralBase};
69
+ /* Default/Label/12px-Md */
70
+ font-family: ${theme.fontFamily.ui};
71
+ font-size: 12px;
72
+ font-style: normal;
73
+ font-weight: 500;
74
+ line-height: 16px; /* 133.333% */
75
+ `);
76
+ const Content = styled.div(({ theme }) => css `
77
+ display: flex;
78
+ flex-direction: column;
79
+ gap: 12px;
80
+ padding: 12px;
81
+ border-top: 1px solid ${theme.color.background.neutralAltActive};
82
+ border-bottom: 1px solid ${theme.color.background.neutralAltActive};
83
+ `);
84
+ const FormArea = styled.div `
85
+ display: flex;
86
+ flex-direction: column;
87
+ gap: 8px;
88
+ `;
89
+ const Label = styled.div(({ theme }) => css `
90
+ color: ${theme.color.foreground.neutralBaseDisabled};
91
+ /* Default/Label/12px-Md */
92
+ font-family: ${theme.fontFamily.ui};
93
+ font-size: 12px;
94
+ font-style: normal;
95
+ font-weight: 500;
96
+ line-height: 16px; /* 133.333% */
97
+ `);
98
+ const Buttons = styled.div `
99
+ display: flex;
100
+ padding: 12px;
101
+ justify-content: flex-end;
102
+ align-items: center;
103
+ gap: 8px;
104
+ `;
@@ -0,0 +1 @@
1
+ export { default } from "./SettingForm";
@@ -0,0 +1 @@
1
+ export { default } from "./SettingForm";
@@ -0,0 +1,2 @@
1
+ export * from "./ProblemSelectNode";
2
+ export * from "./SelectComponent";
@@ -0,0 +1,2 @@
1
+ export * from "./ProblemSelectNode";
2
+ export * from "./SelectComponent";
@@ -22,7 +22,7 @@ import ReactDOM, { createPortal } from "react-dom";
22
22
  import { LexicalNodeMenuPlugin } from "@lexical/react/LexicalNodeMenuPlugin";
23
23
  import { useDraggableBlockMenu } from "./useDraggableBlockMenu";
24
24
  import { css as cssToClassName } from "@emotion/css";
25
- import { ComponentPickerMenuList, getBaseOptions, ComponentDrawerOption, } from "../ComponentPickerMenuPlugin";
25
+ import { ComponentPickerOption, ComponentPickerMenuList, getBaseOptions, ComponentDrawerOption, } from "../ComponentPickerMenuPlugin";
26
26
  import { useFloatingMenu } from "./useFloatingMenu";
27
27
  import ComponentAdder from "./ComponentAdder";
28
28
  import styled from "@emotion/styled";
@@ -137,7 +137,7 @@ export function ComponentAdderPlugin(props) {
137
137
  return true;
138
138
  }
139
139
  const regex = new RegExp(query, "i");
140
- return (option.keywords &&
140
+ return (option instanceof ComponentPickerOption &&
141
141
  (regex.test(option.title) ||
142
142
  option.keywords.some((keyword) => regex.test(keyword))));
143
143
  });
@@ -13,8 +13,7 @@ import { Theme } from "@emotion/react";
13
13
  export declare class ComponentDrawerOption extends MenuOption {
14
14
  title: string;
15
15
  component: ReactElement;
16
- keywords?: Array<string>;
17
- constructor(title: string, component: ReactElement, keywords?: Array<string>);
16
+ constructor(title: string, component: ReactElement);
18
17
  }
19
18
  export declare class ComponentPickerOption extends MenuOption {
20
19
  title: string;
@@ -25,15 +25,15 @@ import { TextIcon, H1Icon, H2Icon, H3Icon, ListUnorderedIcon, ListOrderedIcon, D
25
25
  import { ZINDEX } from "../../../../utils/zIndex";
26
26
  import { css, useTheme } from "@emotion/react";
27
27
  import { INSERT_PROBLEM_INPUT_COMMAND } from "../ProblemInputPlugin";
28
+ import { INSERT_PROBLEM_SELECT_COMMAND } from "../ProblemSelectPlugin";
28
29
  // import useModal from "../../hooks/useModal";
29
30
  // import catTypingGif from "../../images/cat-typing.gif";
30
31
  // import { INSERT_IMAGE_COMMAND, InsertImageDialog } from "../ImagesPlugin";
31
32
  export class ComponentDrawerOption extends MenuOption {
32
- constructor(title, component, keywords) {
33
+ constructor(title, component) {
33
34
  super(title);
34
35
  this.title = title;
35
36
  this.component = component;
36
- this.keywords = keywords;
37
37
  }
38
38
  }
39
39
  export class ComponentPickerOption extends MenuOption {
@@ -87,7 +87,18 @@ function getQuizContextOptions(editor, theme) {
87
87
  icon: _jsx(ListRadioIcon, { color: theme.color.foreground.primary }),
88
88
  keywords: ["problem select", "객관식 입력"],
89
89
  onSelect: () => {
90
- // TODO: 객관식 입력 칸 추가
90
+ editor.dispatchCommand(INSERT_PROBLEM_SELECT_COMMAND, {
91
+ selections: [
92
+ {
93
+ isAnswer: false,
94
+ show: {
95
+ text: "",
96
+ },
97
+ value: "1",
98
+ },
99
+ ],
100
+ selected: [],
101
+ });
91
102
  },
92
103
  }),
93
104
  new ComponentDrawerOption("메뉴구분선", (_jsx("div", { css: css `
@@ -205,7 +216,7 @@ export function ComponentPickerMenuPlugin() {
205
216
  const regex = new RegExp(queryString, "i");
206
217
  return [
207
218
  ...getDynamicOptions(editor, queryString),
208
- ...baseOptions.filter((option) => option.keywords &&
219
+ ...baseOptions.filter((option) => option instanceof ComponentPickerOption &&
209
220
  (regex.test(option.title) ||
210
221
  option.keywords.some((keyword) => regex.test(keyword)))),
211
222
  ];
@@ -0,0 +1,5 @@
1
+ /// <reference types="react" />
2
+ import { LexicalCommand } from "lexical";
3
+ import { ProblemSelectPayload } from "../../nodes/ProblemSelectNode";
4
+ export declare const INSERT_PROBLEM_SELECT_COMMAND: LexicalCommand<ProblemSelectPayload>;
5
+ export default function ProblemInputPlugin(): JSX.Element | null;
@@ -0,0 +1,20 @@
1
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
2
+ import { $insertNodeToNearestRoot } from "@lexical/utils";
3
+ import { COMMAND_PRIORITY_EDITOR, createCommand, } from "lexical";
4
+ import { useEffect } from "react";
5
+ import { $createProblemSelectNode, ProblemSelectNode, } from "../../nodes/ProblemSelectNode";
6
+ export const INSERT_PROBLEM_SELECT_COMMAND = createCommand("INSERT_PROBLEM_SELECT_COMMAND");
7
+ export default function ProblemInputPlugin() {
8
+ const [editor] = useLexicalComposerContext();
9
+ useEffect(() => {
10
+ if (!editor.hasNodes([ProblemSelectNode])) {
11
+ throw new Error("ProblemSelectNode: ProblemSelectNode not registered on editor");
12
+ }
13
+ editor.registerCommand(INSERT_PROBLEM_SELECT_COMMAND, (payload) => {
14
+ const problemInputNode = $createProblemSelectNode(payload);
15
+ $insertNodeToNearestRoot(problemInputNode);
16
+ return true;
17
+ }, COMMAND_PRIORITY_EDITOR);
18
+ }, [editor]);
19
+ return null;
20
+ }
@@ -29,4 +29,5 @@ export declare function getTheme(theme: Theme): {
29
29
  strikethrough: string;
30
30
  };
31
31
  problemInput: string;
32
+ problemSelect: string;
32
33
  };
@@ -252,6 +252,13 @@ export function getTheme(theme) {
252
252
  flex: 1;
253
253
  gap: 4px;
254
254
  margin-bottom: 8px;
255
+ `,
256
+ problemSelect: css `
257
+ display: flex;
258
+ flex-direction: column;
259
+ flex: 1;
260
+ gap: 4px;
261
+ margin-bottom: 8px;
255
262
  `,
256
263
  };
257
264
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-monolith/cds",
3
- "version": "1.8.7",
3
+ "version": "1.9.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "sideEffects": false,