@team-monolith/cds 1.10.0 → 1.10.2
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/icons/Custom.d.ts +1 -0
- 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
package/dist/icons/Custom.d.ts
CHANGED
|
@@ -94,4 +94,5 @@ export declare const CustomPdfColorIcon: React.ForwardRefExoticComponent<{
|
|
|
94
94
|
export declare const customPdfSvg: string;
|
|
95
95
|
export declare const CustomPdfIcon: React.ForwardRefExoticComponent<{
|
|
96
96
|
className?: string | undefined;
|
|
97
|
+
color?: string | undefined;
|
|
97
98
|
} & React.RefAttributes<any>>;
|
|
@@ -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;
|