@team-monolith/cds 1.48.0 → 1.49.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.
- package/dist/patterns/LexicalEditor/LexicalEditor.js +37 -1
- package/dist/patterns/LexicalEditor/Plugins.js +2 -1
- package/dist/patterns/LexicalEditor/nodes/LayoutContainerNode.d.ts +30 -0
- package/dist/patterns/LexicalEditor/nodes/LayoutContainerNode.js +89 -0
- package/dist/patterns/LexicalEditor/nodes/LayoutItemNode.d.ts +22 -0
- package/dist/patterns/LexicalEditor/nodes/LayoutItemNode.js +45 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentPickerMenuPlugin/ComponentPickerMenuPlugin.js +12 -2
- package/dist/patterns/LexicalEditor/plugins/LayoutPlugin/index.d.ts +14 -0
- package/dist/patterns/LexicalEditor/plugins/LayoutPlugin/index.js +125 -0
- package/dist/patterns/LexicalEditor/plugins/ProblemSelectPlugin/index.d.ts +1 -1
- package/dist/patterns/LexicalEditor/plugins/ProblemSelectPlugin/index.js +1 -1
- package/package.json +2 -1
|
@@ -13,6 +13,9 @@ import Plugins from "./Plugins";
|
|
|
13
13
|
import { ColoredQuoteNode, ProblemInputNode } from "./nodes";
|
|
14
14
|
import { LexicalCustomConfigContext } from "./LexicalCustomConfigContext";
|
|
15
15
|
import { ProblemSelectNode } from "./nodes/ProblemSelectNode";
|
|
16
|
+
import { LayoutContainerNode } from "./nodes/LayoutContainerNode";
|
|
17
|
+
import { LayoutItemNode } from "./nodes/LayoutItemNode";
|
|
18
|
+
import _ from "lodash";
|
|
16
19
|
function validateValue(value) {
|
|
17
20
|
var _a, _b;
|
|
18
21
|
if (value && typeof value !== "object") {
|
|
@@ -29,6 +32,25 @@ function validateValue(value) {
|
|
|
29
32
|
}
|
|
30
33
|
return true;
|
|
31
34
|
}
|
|
35
|
+
/** undefined value를 제외한 객체 일치 여부를 판단합니다. */
|
|
36
|
+
function isCleanEqualObjects(oldObj, newObj) {
|
|
37
|
+
const cleanedOldObj = getCleanObject(oldObj);
|
|
38
|
+
const cleanedNewObj = getCleanObject(newObj);
|
|
39
|
+
return _.isEqual(cleanedOldObj, cleanedNewObj);
|
|
40
|
+
}
|
|
41
|
+
/** 객체 내에 undefined value를 갖는 key를 재귀적으로 모두 제거합니다. */
|
|
42
|
+
function getCleanObject(obj) {
|
|
43
|
+
const newObj = _.cloneDeep(obj);
|
|
44
|
+
for (const key in newObj) {
|
|
45
|
+
if (newObj[key] === undefined) {
|
|
46
|
+
delete newObj[key];
|
|
47
|
+
}
|
|
48
|
+
else if (_.isObject(newObj[key])) {
|
|
49
|
+
newObj[key] = getCleanObject(newObj[key]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return newObj;
|
|
53
|
+
}
|
|
32
54
|
export function LexicalEditor(props) {
|
|
33
55
|
const { className, contentEditableClassName, value, onChange, editable = true, showQuizSolution = false, freezeProblemNode = false, isQuizEnabled = false, children, } = props;
|
|
34
56
|
const theme = useTheme();
|
|
@@ -52,6 +74,8 @@ export function LexicalEditor(props) {
|
|
|
52
74
|
ImageNode,
|
|
53
75
|
HorizontalRuleNode,
|
|
54
76
|
ColoredQuoteNode,
|
|
77
|
+
LayoutContainerNode,
|
|
78
|
+
LayoutItemNode,
|
|
55
79
|
{
|
|
56
80
|
replace: QuoteNode,
|
|
57
81
|
with: (_node) => {
|
|
@@ -63,5 +87,17 @@ export function LexicalEditor(props) {
|
|
|
63
87
|
editorState: validateValue(value) ? JSON.stringify(value) : undefined,
|
|
64
88
|
editable: editable,
|
|
65
89
|
};
|
|
66
|
-
|
|
90
|
+
/** onChangeHandler 도입 배경
|
|
91
|
+
* LayoutPlugIn이 알기 힘든 이슈로 blocks value중 indent key 등을 undefined로 만듭니다.
|
|
92
|
+
* 이로 인해, 렉시컬 RHF 폼에서 유저 상호작용 없이도 dirtyFields가 무의미하게 변화하게 되어, UX에 문제가 생깁니다.
|
|
93
|
+
* 이를 해결하기 위해, 유의미한 변경이 일어났을 때만 onChange를 호출합니다.
|
|
94
|
+
*/
|
|
95
|
+
const onChangeHandler = onChange
|
|
96
|
+
? (blocks) => {
|
|
97
|
+
if (!isCleanEqualObjects(value, blocks)) {
|
|
98
|
+
onChange(blocks);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
: undefined;
|
|
102
|
+
return (_jsx(LexicalCustomConfigContext.Provider, Object.assign({ value: { freezeProblemNode, showQuizSolution } }, { children: _jsxs(LexicalComposer, Object.assign({ initialConfig: initialConfig }, { children: [_jsx(Plugins, { className: className, contentEditableClassName: contentEditableClassName, onChange: onChangeHandler, isQuizEnabled: isQuizEnabled }), _jsx(_Fragment, { children: children })] })) })));
|
|
67
103
|
}
|
|
@@ -29,6 +29,7 @@ import ListMaxIndentLevelPlugin from "./plugins/ListMaxIndentLevelPlugin";
|
|
|
29
29
|
import styled from "@emotion/styled";
|
|
30
30
|
import ProblemInputPlugin from "./plugins/ProblemInputPlugin";
|
|
31
31
|
import ProblemSelectPlugin from "./plugins/ProblemSelectPlugin";
|
|
32
|
+
import { LayoutPlugin } from "./plugins/LayoutPlugin";
|
|
32
33
|
export default function Plugins(props) {
|
|
33
34
|
const { className, contentEditableClassName, onChange, isQuizEnabled } = props;
|
|
34
35
|
const isEditable = useLexicalEditable();
|
|
@@ -43,7 +44,7 @@ export default function Plugins(props) {
|
|
|
43
44
|
onChange === null || onChange === void 0 ? void 0 : onChange(editorState.toJSON());
|
|
44
45
|
},
|
|
45
46
|
// ignore 하지 않으면 Form에서 수정하지 않았는데 Dirty로 처리됨.
|
|
46
|
-
ignoreSelectionChange: true }), _jsx(AutoFocusPlugin, {}), isEditable && (_jsxs(_Fragment, { children: [_jsx(TabIndentationPlugin, {}), _jsx(ComponentPickerMenuPlugin, { isQuizEnabled: isQuizEnabled }), _jsx(MarkdownShortcutPlugin, { transformers: CODLE_TRANSFORMERS }), _jsx(HistoryPlugin, {})] })), floatingAnchorElem && isEditable && (_jsxs(_Fragment, { children: [_jsx(ComponentAdderPlugin, { anchorElem: floatingAnchorElem, isQuizEnabled: isQuizEnabled }), _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, {})] }));
|
|
47
|
+
ignoreSelectionChange: true }), _jsx(AutoFocusPlugin, {}), isEditable && (_jsxs(_Fragment, { children: [_jsx(TabIndentationPlugin, {}), _jsx(ComponentPickerMenuPlugin, { isQuizEnabled: isQuizEnabled }), _jsx(MarkdownShortcutPlugin, { transformers: CODLE_TRANSFORMERS }), _jsx(HistoryPlugin, {})] })), floatingAnchorElem && isEditable && (_jsxs(_Fragment, { children: [_jsx(ComponentAdderPlugin, { anchorElem: floatingAnchorElem, isQuizEnabled: isQuizEnabled }), _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, {}), _jsx(LayoutPlugin, {})] }));
|
|
47
48
|
}
|
|
48
49
|
const ScrollArea = styled.div `
|
|
49
50
|
min-height: 150px;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import type { DOMConversionMap, DOMExportOutput, EditorConfig, LexicalNode, NodeKey, SerializedElementNode, Spread } from "lexical";
|
|
9
|
+
import { ElementNode } from "lexical";
|
|
10
|
+
export type SerializedLayoutContainerNode = Spread<{
|
|
11
|
+
templateColumns: string;
|
|
12
|
+
}, SerializedElementNode>;
|
|
13
|
+
export declare class LayoutContainerNode extends ElementNode {
|
|
14
|
+
__templateColumns: string;
|
|
15
|
+
constructor(templateColumns: string, key?: NodeKey);
|
|
16
|
+
static getType(): string;
|
|
17
|
+
static clone(node: LayoutContainerNode): LayoutContainerNode;
|
|
18
|
+
createDOM(config: EditorConfig): HTMLElement;
|
|
19
|
+
exportDOM(): DOMExportOutput;
|
|
20
|
+
updateDOM(prevNode: LayoutContainerNode, dom: HTMLElement): boolean;
|
|
21
|
+
static importDOM(): DOMConversionMap | null;
|
|
22
|
+
static importJSON(json: SerializedLayoutContainerNode): LayoutContainerNode;
|
|
23
|
+
isShadowRoot(): boolean;
|
|
24
|
+
canBeEmpty(): boolean;
|
|
25
|
+
exportJSON(): SerializedLayoutContainerNode;
|
|
26
|
+
getTemplateColumns(): string;
|
|
27
|
+
setTemplateColumns(templateColumns: string): void;
|
|
28
|
+
}
|
|
29
|
+
export declare function $createLayoutContainerNode(templateColumns: string): LayoutContainerNode;
|
|
30
|
+
export declare function $isLayoutContainerNode(node: LexicalNode | null | undefined): node is LayoutContainerNode;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import { addClassNamesToElement } from "@lexical/utils";
|
|
9
|
+
import { ElementNode } from "lexical";
|
|
10
|
+
function $convertLayoutContainerElement(domNode) {
|
|
11
|
+
const styleAttributes = window.getComputedStyle(domNode);
|
|
12
|
+
const templateColumns = styleAttributes.getPropertyValue("grid-template-columns");
|
|
13
|
+
if (templateColumns) {
|
|
14
|
+
const node = $createLayoutContainerNode(templateColumns);
|
|
15
|
+
return { node };
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
export class LayoutContainerNode extends ElementNode {
|
|
20
|
+
constructor(templateColumns, key) {
|
|
21
|
+
super(key);
|
|
22
|
+
this.__templateColumns = templateColumns;
|
|
23
|
+
}
|
|
24
|
+
static getType() {
|
|
25
|
+
return "layout-container";
|
|
26
|
+
}
|
|
27
|
+
static clone(node) {
|
|
28
|
+
return new LayoutContainerNode(node.__templateColumns, node.__key);
|
|
29
|
+
}
|
|
30
|
+
createDOM(config) {
|
|
31
|
+
const dom = document.createElement("div");
|
|
32
|
+
dom.style.display = "grid";
|
|
33
|
+
dom.style.gap = "24px";
|
|
34
|
+
dom.style.gridTemplateColumns = this.__templateColumns;
|
|
35
|
+
if (typeof config.theme.layoutContainer === "string") {
|
|
36
|
+
addClassNamesToElement(dom, config.theme.layoutContainer);
|
|
37
|
+
}
|
|
38
|
+
return dom;
|
|
39
|
+
}
|
|
40
|
+
exportDOM() {
|
|
41
|
+
const element = document.createElement("div");
|
|
42
|
+
element.style.gridTemplateColumns = this.__templateColumns;
|
|
43
|
+
element.setAttribute("data-lexical-layout-container", "true");
|
|
44
|
+
return { element };
|
|
45
|
+
}
|
|
46
|
+
updateDOM(prevNode, dom) {
|
|
47
|
+
if (prevNode.__templateColumns !== this.__templateColumns) {
|
|
48
|
+
dom.style.gridTemplateColumns = this.__templateColumns;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
static importDOM() {
|
|
53
|
+
return {
|
|
54
|
+
div: (domNode) => {
|
|
55
|
+
if (!domNode.hasAttribute("data-lexical-layout-container")) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
conversion: $convertLayoutContainerElement,
|
|
60
|
+
priority: 2,
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
static importJSON(json) {
|
|
66
|
+
return $createLayoutContainerNode(json.templateColumns);
|
|
67
|
+
}
|
|
68
|
+
isShadowRoot() {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
canBeEmpty() {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
exportJSON() {
|
|
75
|
+
return Object.assign(Object.assign({}, super.exportJSON()), { templateColumns: this.__templateColumns, type: "layout-container", version: 1 });
|
|
76
|
+
}
|
|
77
|
+
getTemplateColumns() {
|
|
78
|
+
return this.getLatest().__templateColumns;
|
|
79
|
+
}
|
|
80
|
+
setTemplateColumns(templateColumns) {
|
|
81
|
+
this.getWritable().__templateColumns = templateColumns;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function $createLayoutContainerNode(templateColumns) {
|
|
85
|
+
return new LayoutContainerNode(templateColumns);
|
|
86
|
+
}
|
|
87
|
+
export function $isLayoutContainerNode(node) {
|
|
88
|
+
return node instanceof LayoutContainerNode;
|
|
89
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import type { DOMConversionMap, EditorConfig, LexicalNode, SerializedElementNode } from "lexical";
|
|
9
|
+
import { ElementNode } from "lexical";
|
|
10
|
+
export type SerializedLayoutItemNode = SerializedElementNode;
|
|
11
|
+
export declare class LayoutItemNode extends ElementNode {
|
|
12
|
+
static getType(): string;
|
|
13
|
+
static clone(node: LayoutItemNode): LayoutItemNode;
|
|
14
|
+
createDOM(config: EditorConfig): HTMLElement;
|
|
15
|
+
updateDOM(): boolean;
|
|
16
|
+
static importDOM(): DOMConversionMap | null;
|
|
17
|
+
static importJSON(): LayoutItemNode;
|
|
18
|
+
isShadowRoot(): boolean;
|
|
19
|
+
exportJSON(): SerializedLayoutItemNode;
|
|
20
|
+
}
|
|
21
|
+
export declare function $createLayoutItemNode(): LayoutItemNode;
|
|
22
|
+
export declare function $isLayoutItemNode(node: LexicalNode | null | undefined): node is LayoutItemNode;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import { addClassNamesToElement } from "@lexical/utils";
|
|
9
|
+
import { ElementNode } from "lexical";
|
|
10
|
+
export class LayoutItemNode extends ElementNode {
|
|
11
|
+
static getType() {
|
|
12
|
+
return "layout-item";
|
|
13
|
+
}
|
|
14
|
+
static clone(node) {
|
|
15
|
+
return new LayoutItemNode(node.__key);
|
|
16
|
+
}
|
|
17
|
+
createDOM(config) {
|
|
18
|
+
const dom = document.createElement("div");
|
|
19
|
+
if (typeof config.theme.layoutItem === "string") {
|
|
20
|
+
addClassNamesToElement(dom, config.theme.layoutItem);
|
|
21
|
+
}
|
|
22
|
+
return dom;
|
|
23
|
+
}
|
|
24
|
+
updateDOM() {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
static importDOM() {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
static importJSON() {
|
|
31
|
+
return $createLayoutItemNode();
|
|
32
|
+
}
|
|
33
|
+
isShadowRoot() {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
exportJSON() {
|
|
37
|
+
return Object.assign(Object.assign({}, super.exportJSON()), { type: "layout-item", version: 1 });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function $createLayoutItemNode() {
|
|
41
|
+
return new LayoutItemNode();
|
|
42
|
+
}
|
|
43
|
+
export function $isLayoutItemNode(node) {
|
|
44
|
+
return node instanceof LayoutItemNode;
|
|
45
|
+
}
|
package/dist/patterns/LexicalEditor/plugins/ComponentPickerMenuPlugin/ComponentPickerMenuPlugin.js
CHANGED
|
@@ -21,11 +21,12 @@ import * as ReactDOM from "react-dom";
|
|
|
21
21
|
import { css as cssToClassName } from "@emotion/css";
|
|
22
22
|
import { ComponentPickerMenuList } from "./ComponentPickerMenuList";
|
|
23
23
|
import { InsertImageDialog } from "../ImagesPlugin/InsertImageDialog";
|
|
24
|
-
import { TextIcon, H1Icon, H2Icon, H3Icon, ListUnorderedIcon, ListOrderedIcon, DoubleQuotesLIcon, CodeViewIcon, SeparatorIcon, ImageLineIcon, InputMethodLineIcon, ListRadioIcon, } from "../../../../icons";
|
|
24
|
+
import { TextIcon, H1Icon, H2Icon, H3Icon, ListUnorderedIcon, ListOrderedIcon, DoubleQuotesLIcon, CodeViewIcon, SeparatorIcon, ImageLineIcon, InputMethodLineIcon, ListRadioIcon, LayoutColumnLineIcon, } from "../../../../icons";
|
|
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
28
|
import { INSERT_PROBLEM_SELECT_COMMAND } from "../ProblemSelectPlugin";
|
|
29
|
+
import { INSERT_LAYOUT_COMMAND } from "../LayoutPlugin";
|
|
29
30
|
// import useModal from "../../hooks/useModal";
|
|
30
31
|
// import catTypingGif from "../../images/cat-typing.gif";
|
|
31
32
|
// import { INSERT_IMAGE_COMMAND, InsertImageDialog } from "../ImagesPlugin";
|
|
@@ -196,8 +197,17 @@ export function getBaseOptions(props) {
|
|
|
196
197
|
onSelect: () => setImageOpen(true),
|
|
197
198
|
}),
|
|
198
199
|
];
|
|
200
|
+
// 로컬스토리지 devMode가 true이면 칼럼 컨텍스트 메뉴를 추가합니다.
|
|
201
|
+
const isDevMode = localStorage.getItem("devMode") === "true";
|
|
202
|
+
if (isDevMode) {
|
|
203
|
+
baseOptions.push(new ComponentPickerOption("칼럼", {
|
|
204
|
+
icon: _jsx(LayoutColumnLineIcon, {}),
|
|
205
|
+
keywords: ["columns", "layout", "grid"],
|
|
206
|
+
onSelect: () => editor.dispatchCommand(INSERT_LAYOUT_COMMAND, "1fr 1fr"),
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
199
209
|
// isQuizEnabled이거나 로컬스토리지 devMode가 true이면 퀴즈 컨텍스트 메뉴를 추가합니다.
|
|
200
|
-
if (isQuizEnabled ||
|
|
210
|
+
if (isQuizEnabled || isDevMode) {
|
|
201
211
|
return [...getQuizContextOptions(editor, theme), ...baseOptions];
|
|
202
212
|
}
|
|
203
213
|
return baseOptions;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import type { LexicalCommand, NodeKey } from "lexical";
|
|
9
|
+
export declare const INSERT_LAYOUT_COMMAND: LexicalCommand<string>;
|
|
10
|
+
export declare const UPDATE_LAYOUT_COMMAND: LexicalCommand<{
|
|
11
|
+
template: string;
|
|
12
|
+
nodeKey: NodeKey;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function LayoutPlugin(): null;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
9
|
+
import { $findMatchingParent, $insertNodeToNearestRoot, mergeRegister, } from "@lexical/utils";
|
|
10
|
+
import { $createParagraphNode, $getNodeByKey, $getSelection, $isRangeSelection, COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_LOW, createCommand, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_LEFT_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, } from "lexical";
|
|
11
|
+
import { useEffect } from "react";
|
|
12
|
+
import { $createLayoutContainerNode, $isLayoutContainerNode, LayoutContainerNode, } from "../../nodes/LayoutContainerNode";
|
|
13
|
+
import { $createLayoutItemNode, $isLayoutItemNode, LayoutItemNode, } from "../../nodes/LayoutItemNode";
|
|
14
|
+
export const INSERT_LAYOUT_COMMAND = createCommand();
|
|
15
|
+
export const UPDATE_LAYOUT_COMMAND = createCommand();
|
|
16
|
+
export function LayoutPlugin() {
|
|
17
|
+
const [editor] = useLexicalComposerContext();
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!editor.hasNodes([LayoutContainerNode, LayoutItemNode])) {
|
|
20
|
+
throw new Error("LayoutPlugin: LayoutContainerNode, or LayoutItemNode not registered on editor");
|
|
21
|
+
}
|
|
22
|
+
const $onEscape = (before) => {
|
|
23
|
+
var _a, _b;
|
|
24
|
+
const selection = $getSelection();
|
|
25
|
+
if ($isRangeSelection(selection) &&
|
|
26
|
+
selection.isCollapsed() &&
|
|
27
|
+
selection.anchor.offset === 0) {
|
|
28
|
+
const container = $findMatchingParent(selection.anchor.getNode(), $isLayoutContainerNode);
|
|
29
|
+
if ($isLayoutContainerNode(container)) {
|
|
30
|
+
const parent = container.getParent();
|
|
31
|
+
const child = parent &&
|
|
32
|
+
(before
|
|
33
|
+
? parent.getFirstChild()
|
|
34
|
+
: parent === null || parent === void 0 ? void 0 : parent.getLastChild());
|
|
35
|
+
const descendant = before
|
|
36
|
+
? (_a = container.getFirstDescendant()) === null || _a === void 0 ? void 0 : _a.getKey()
|
|
37
|
+
: (_b = container.getLastDescendant()) === null || _b === void 0 ? void 0 : _b.getKey();
|
|
38
|
+
if (parent !== null &&
|
|
39
|
+
child === container &&
|
|
40
|
+
selection.anchor.key === descendant) {
|
|
41
|
+
if (before) {
|
|
42
|
+
container.insertBefore($createParagraphNode());
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
container.insertAfter($createParagraphNode());
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
};
|
|
52
|
+
return mergeRegister(
|
|
53
|
+
// When layout is the last child pressing down/right arrow will insert paragraph
|
|
54
|
+
// below it to allow adding more content. It's similar what $insertBlockNode
|
|
55
|
+
// (mainly for decorators), except it'll always be possible to continue adding
|
|
56
|
+
// new content even if trailing paragraph is accidentally deleted
|
|
57
|
+
editor.registerCommand(KEY_ARROW_DOWN_COMMAND, () => $onEscape(false), COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_ARROW_RIGHT_COMMAND, () => $onEscape(false), COMMAND_PRIORITY_LOW),
|
|
58
|
+
// When layout is the first child pressing up/left arrow will insert paragraph
|
|
59
|
+
// above it to allow adding more content. It's similar what $insertBlockNode
|
|
60
|
+
// (mainly for decorators), except it'll always be possible to continue adding
|
|
61
|
+
// new content even if leading paragraph is accidentally deleted
|
|
62
|
+
editor.registerCommand(KEY_ARROW_UP_COMMAND, () => $onEscape(true), COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_ARROW_LEFT_COMMAND, () => $onEscape(true), COMMAND_PRIORITY_LOW), editor.registerCommand(INSERT_LAYOUT_COMMAND, (template) => {
|
|
63
|
+
editor.update(() => {
|
|
64
|
+
const container = $createLayoutContainerNode(template);
|
|
65
|
+
const itemsCount = getItemsCountFromTemplate(template);
|
|
66
|
+
for (let i = 0; i < itemsCount; i++) {
|
|
67
|
+
container.append($createLayoutItemNode().append($createParagraphNode()));
|
|
68
|
+
}
|
|
69
|
+
$insertNodeToNearestRoot(container);
|
|
70
|
+
container.selectStart();
|
|
71
|
+
});
|
|
72
|
+
return true;
|
|
73
|
+
}, COMMAND_PRIORITY_EDITOR), editor.registerCommand(UPDATE_LAYOUT_COMMAND, ({ template, nodeKey }) => {
|
|
74
|
+
editor.update(() => {
|
|
75
|
+
const container = $getNodeByKey(nodeKey);
|
|
76
|
+
if (!$isLayoutContainerNode(container)) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const itemsCount = getItemsCountFromTemplate(template);
|
|
80
|
+
const prevItemsCount = getItemsCountFromTemplate(container.getTemplateColumns());
|
|
81
|
+
// Add or remove extra columns if new template does not match existing one
|
|
82
|
+
if (itemsCount > prevItemsCount) {
|
|
83
|
+
for (let i = prevItemsCount; i < itemsCount; i++) {
|
|
84
|
+
container.append($createLayoutItemNode().append($createParagraphNode()));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else if (itemsCount < prevItemsCount) {
|
|
88
|
+
for (let i = prevItemsCount - 1; i >= itemsCount; i--) {
|
|
89
|
+
const layoutItem = container.getChildAtIndex(i);
|
|
90
|
+
if ($isLayoutItemNode(layoutItem)) {
|
|
91
|
+
layoutItem.remove();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
container.setTemplateColumns(template);
|
|
96
|
+
});
|
|
97
|
+
return true;
|
|
98
|
+
}, COMMAND_PRIORITY_EDITOR),
|
|
99
|
+
// Structure enforcing transformers for each node type. In case nesting structure is not
|
|
100
|
+
// "Container > Item" it'll unwrap nodes and convert it back
|
|
101
|
+
// to regular content.
|
|
102
|
+
editor.registerNodeTransform(LayoutItemNode, (node) => {
|
|
103
|
+
const parent = node.getParent();
|
|
104
|
+
if (!$isLayoutContainerNode(parent)) {
|
|
105
|
+
const children = node.getChildren();
|
|
106
|
+
for (const child of children) {
|
|
107
|
+
node.insertBefore(child);
|
|
108
|
+
}
|
|
109
|
+
node.remove();
|
|
110
|
+
}
|
|
111
|
+
}), editor.registerNodeTransform(LayoutContainerNode, (node) => {
|
|
112
|
+
const children = node.getChildren();
|
|
113
|
+
if (!children.every($isLayoutItemNode)) {
|
|
114
|
+
for (const child of children) {
|
|
115
|
+
node.insertBefore(child);
|
|
116
|
+
}
|
|
117
|
+
node.remove();
|
|
118
|
+
}
|
|
119
|
+
}));
|
|
120
|
+
}, [editor]);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
function getItemsCountFromTemplate(template) {
|
|
124
|
+
return template.trim().split(/\s+/).length;
|
|
125
|
+
}
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
import { LexicalCommand } from "lexical";
|
|
3
3
|
import { ProblemSelectPayload } from "../../nodes/ProblemSelectNode";
|
|
4
4
|
export declare const INSERT_PROBLEM_SELECT_COMMAND: LexicalCommand<ProblemSelectPayload>;
|
|
5
|
-
export default function
|
|
5
|
+
export default function ProblemSelectPlugin(): JSX.Element | null;
|
|
@@ -4,7 +4,7 @@ import { COMMAND_PRIORITY_EDITOR, createCommand, } from "lexical";
|
|
|
4
4
|
import { useEffect } from "react";
|
|
5
5
|
import { $createProblemSelectNode, ProblemSelectNode, } from "../../nodes/ProblemSelectNode";
|
|
6
6
|
export const INSERT_PROBLEM_SELECT_COMMAND = createCommand("INSERT_PROBLEM_SELECT_COMMAND");
|
|
7
|
-
export default function
|
|
7
|
+
export default function ProblemSelectPlugin() {
|
|
8
8
|
const [editor] = useLexicalComposerContext();
|
|
9
9
|
useEffect(() => {
|
|
10
10
|
if (!editor.hasNodes([ProblemSelectNode])) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-monolith/cds",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.49.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"@types/react-dom": "^18.2.13",
|
|
16
16
|
"hex-to-css-filter": "^5.4.0",
|
|
17
17
|
"lexical": "^0.12.4",
|
|
18
|
+
"lodash": "^4.17.21",
|
|
18
19
|
"react": "^18.2.0",
|
|
19
20
|
"react-dom": "^18.2.0",
|
|
20
21
|
"react-hook-form": "^7.48.2",
|