@team-monolith/cds 1.47.1 → 1.48.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,11 +30,11 @@ export const light = {
30
30
  success: COLOR.green07,
31
31
  successActive: COLOR.green08,
32
32
  successDisabled: COLOR.green03,
33
- info: COLOR.teal08,
34
- infoActive: COLOR.teal09,
33
+ info: COLOR.teal06,
34
+ infoActive: COLOR.teal08,
35
35
  infoDisabled: COLOR.teal02,
36
- warning: COLOR.yellow08,
37
- warningActive: COLOR.yellow09,
36
+ warning: COLOR.yellow06,
37
+ warningActive: COLOR.yellow08,
38
38
  warningDisabled: COLOR.yellow02,
39
39
  inverse: COLOR.black,
40
40
  inverseActive: COLOR.grey08,
@@ -141,11 +141,11 @@ export const dark = {
141
141
  success: COLOR.green04,
142
142
  successActive: COLOR.green03,
143
143
  successDisabled: COLOR.green08,
144
- info: COLOR.teal03,
145
- infoActive: COLOR.teal02,
146
- infoDisabled: COLOR.teal09,
147
- warning: COLOR.yellow03,
148
- warningActive: COLOR.yellow02,
144
+ info: COLOR.teal06,
145
+ infoActive: COLOR.teal08,
146
+ infoDisabled: COLOR.teal02,
147
+ warning: COLOR.yellow08,
148
+ warningActive: COLOR.yellow06,
149
149
  warningDisabled: COLOR.yellow09,
150
150
  inverse: COLOR.white,
151
151
  inverseActive: COLOR.grey03,
@@ -13,8 +13,6 @@ 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
16
  function validateValue(value) {
19
17
  var _a, _b;
20
18
  if (value && typeof value !== "object") {
@@ -54,8 +52,6 @@ export function LexicalEditor(props) {
54
52
  ImageNode,
55
53
  HorizontalRuleNode,
56
54
  ColoredQuoteNode,
57
- LayoutContainerNode,
58
- LayoutItemNode,
59
55
  {
60
56
  replace: QuoteNode,
61
57
  with: (_node) => {
@@ -29,7 +29,6 @@ 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";
33
32
  export default function Plugins(props) {
34
33
  const { className, contentEditableClassName, onChange, isQuizEnabled } = props;
35
34
  const isEditable = useLexicalEditable();
@@ -44,7 +43,7 @@ export default function Plugins(props) {
44
43
  onChange === null || onChange === void 0 ? void 0 : onChange(editorState.toJSON());
45
44
  },
46
45
  // ignore 하지 않으면 Form에서 수정하지 않았는데 Dirty로 처리됨.
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, {})] }));
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, {})] }));
48
47
  }
49
48
  const ScrollArea = styled.div `
50
49
  min-height: 150px;
@@ -4,43 +4,48 @@ export interface ImageProps {
4
4
  src: string;
5
5
  altText: string;
6
6
  }
7
- export interface Selection {
8
- /** 해당 선택지의 정답 유무는 채점하기 전의 학생에게는 노출하지 않습니다. */
9
- isAnswer?: boolean;
7
+ export interface SelectionWithoutSolution {
10
8
  show: {
11
9
  image: ImageProps | null;
12
10
  text: string;
13
11
  };
14
12
  value: string;
15
13
  }
16
- export interface ProblemSelectPayload {
17
- selections: Selection[];
14
+ export type Selection = SelectionWithoutSolution & {
15
+ isAnswer: boolean;
16
+ };
17
+ /** 정답이 공개되지 않은 객관식 문제는 다중정답 여부를 전달받기 위해서 서버에서 hasMultipleSolutions를 보내줍니다. */
18
+ export type ProblemSelectPayload = {
18
19
  selected: string[];
19
- hasMultipleAnswers: boolean;
20
20
  key?: NodeKey;
21
- }
21
+ } & ({
22
+ selections: Selection[];
23
+ } | {
24
+ selections: SelectionWithoutSolution[];
25
+ hasMultipleSolutions?: boolean;
26
+ });
22
27
  export type SerializedProblemSelectNode = Spread<ProblemSelectPayload, SerializedLexicalNode>;
23
28
  /**
24
29
  * selections는 Selection타입의 배열로서 객관식 정보를 담고 있습니다. (교사용)
25
30
  * selected는 학생이 선택한 답의 value를 담고 있습니다.(학생용)
26
31
  */
27
32
  export declare class ProblemSelectNode extends DecoratorNode<ReactNode> {
28
- __selections: Selection[];
33
+ __selections: Selection[] | SelectionWithoutSolution[];
29
34
  __selected: string[];
30
- __hasMultipleAnswers: boolean;
35
+ __hasMultipleSolutions: boolean | undefined;
31
36
  isInline(): boolean;
32
37
  static getType(): string;
33
- getSelections(): Selection[];
38
+ getSelections(): Selection[] | SelectionWithoutSolution[];
34
39
  getSelected(): string[];
35
- setSelections(selections: Selection[]): void;
40
+ setSelections(selections: Selection[] | SelectionWithoutSolution[]): void;
36
41
  setSelected(selected: string[]): void;
37
42
  static clone(node: ProblemSelectNode): ProblemSelectNode;
38
- constructor(selections: Selection[], selected: string[], hasMultipleAnswers: boolean, key?: NodeKey);
43
+ constructor(selections: Selection[] | SelectionWithoutSolution[], selected: string[], hasMultipleSolutions?: boolean, key?: NodeKey);
39
44
  createDOM(config: EditorConfig): HTMLElement;
40
45
  updateDOM(): boolean;
41
46
  static importJSON(serializedNode: SerializedProblemSelectNode): ProblemSelectNode;
42
47
  exportJSON(): SerializedProblemSelectNode;
43
48
  decorate(): ReactNode;
44
49
  }
45
- export declare function $createProblemSelectNode({ selections, selected, hasMultipleAnswers, key, }: ProblemSelectPayload): ProblemSelectNode;
50
+ export declare function $createProblemSelectNode(payload: ProblemSelectPayload): ProblemSelectNode;
46
51
  export declare function $isProblemSelectNode(node: LexicalNode | null | undefined): node is ProblemSelectNode;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { $applyNodeReplacement, DecoratorNode, } from "lexical";
2
+ import { DecoratorNode, } from "lexical";
3
3
  import { addClassNamesToElement } from "@lexical/utils";
4
4
  import { SelectComponent } from "./SelectComponent";
5
5
  /**
@@ -28,13 +28,13 @@ export class ProblemSelectNode extends DecoratorNode {
28
28
  self.__selected = selected;
29
29
  }
30
30
  static clone(node) {
31
- return new ProblemSelectNode(node.__selections, node.__selected, node.__hasMultipleAnswers, node.__key);
31
+ return new ProblemSelectNode(node.__selections, node.__selected, node.__hasMultipleSolutions, node.__key);
32
32
  }
33
- constructor(selections, selected, hasMultipleAnswers, key) {
33
+ constructor(selections, selected, hasMultipleSolutions, key) {
34
34
  super(key);
35
35
  this.__selections = selections;
36
36
  this.__selected = selected;
37
- this.__hasMultipleAnswers = hasMultipleAnswers;
37
+ this.__hasMultipleSolutions = hasMultipleSolutions;
38
38
  }
39
39
  createDOM(config) {
40
40
  // Define the DOM element here
@@ -46,29 +46,44 @@ export class ProblemSelectNode extends DecoratorNode {
46
46
  return false;
47
47
  }
48
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;
49
+ if ("hasMultipleSolutions" in serializedNode) {
50
+ return $createProblemSelectNode({
51
+ key: serializedNode.key,
52
+ selections: serializedNode.selections,
53
+ selected: serializedNode.selected,
54
+ hasMultipleSolutions: serializedNode.hasMultipleSolutions,
55
+ });
56
+ }
57
+ else {
58
+ return $createProblemSelectNode({
59
+ key: serializedNode.key,
60
+ selections: serializedNode.selections,
61
+ selected: serializedNode.selected,
62
+ });
63
+ }
56
64
  }
57
65
  exportJSON() {
58
66
  return {
67
+ type: ProblemSelectNode.getType(),
59
68
  version: 1,
60
- type: "problem-select",
69
+ key: this.getKey(),
61
70
  selections: this.__selections,
62
71
  selected: this.__selected,
63
- hasMultipleAnswers: this.__hasMultipleAnswers,
64
72
  };
65
73
  }
66
74
  decorate() {
67
- return (_jsx(SelectComponent, { selections: this.__selections, selected: this.__selected, hasMultipleAnswers: this.__hasMultipleAnswers, nodeKey: this.getKey() }));
75
+ return (_jsx(SelectComponent, { selections: this.__selections, selected: this.__selected, hasMultipleSolutions: this.__hasMultipleSolutions, nodeKey: this.getKey() }));
68
76
  }
69
77
  }
70
- export function $createProblemSelectNode({ selections, selected, hasMultipleAnswers, key, }) {
71
- return $applyNodeReplacement(new ProblemSelectNode(selections, selected, hasMultipleAnswers, key));
78
+ export function $createProblemSelectNode(payload) {
79
+ if ("hasMultipleSolutions" in payload) {
80
+ const { selections, selected, key, hasMultipleSolutions } = payload;
81
+ return new ProblemSelectNode(selections, selected, hasMultipleSolutions, key);
82
+ }
83
+ else {
84
+ const { selections, selected, key } = payload;
85
+ return new ProblemSelectNode(selections, selected, undefined, key);
86
+ }
72
87
  }
73
88
  export function $isProblemSelectNode(node) {
74
89
  return node instanceof ProblemSelectNode;
@@ -1,9 +1,9 @@
1
1
  /** @jsxImportSource @emotion/react */
2
2
  import { NodeKey } from "lexical";
3
- import { Selection } from "./ProblemSelectNode";
3
+ import { Selection, SelectionWithoutSolution } from "./ProblemSelectNode";
4
4
  export declare function SelectComponent(props: {
5
- selections: Selection[];
5
+ selections: Selection[] | SelectionWithoutSolution[];
6
6
  selected: string[];
7
- hasMultipleAnswers: boolean;
7
+ hasMultipleSolutions: boolean | undefined;
8
8
  nodeKey: NodeKey;
9
9
  }): import("@emotion/react/jsx-runtime").JSX.Element;
@@ -1,18 +1,7 @@
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
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@emotion/react/jsx-runtime";
13
2
  /** @jsxImportSource @emotion/react */
14
3
  import { $getNodeByKey } from "lexical";
15
- import { $isProblemSelectNode } from "./ProblemSelectNode";
4
+ import { $isProblemSelectNode, } from "./ProblemSelectNode";
16
5
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
17
6
  import { useContext, useState } from "react";
18
7
  import useLexicalEditable from "@lexical/react/useLexicalEditable";
@@ -24,18 +13,26 @@ import SettingForm from "./SettingForm";
24
13
  import styled from "@emotion/styled";
25
14
  import { LexicalCustomConfigContext } from "../../LexicalCustomConfigContext";
26
15
  export function SelectComponent(props) {
27
- const { selected, hasMultipleAnswers } = props, settingFormProps = __rest(props, ["selected", "hasMultipleAnswers"]);
28
- const { selections, nodeKey } = settingFormProps;
16
+ const { selected, hasMultipleSolutions, selections, nodeKey } = props;
29
17
  const [editor] = useLexicalComposerContext();
30
18
  const [settingOpen, setSettingOpen] = useState(false);
31
19
  const isEditable = useLexicalEditable();
32
20
  const { freezeProblemNode, showQuizSolution } = useContext(LexicalCustomConfigContext);
21
+ const showMultipleSolutions = hasMultipleSolutions ||
22
+ selections.flatMap((s) => {
23
+ if ("isAnswer" in s && s.isAnswer) {
24
+ return [s.isAnswer];
25
+ }
26
+ return [];
27
+ }).length > 1;
33
28
  // view
34
29
  if (!isEditable) {
35
- return (_jsxs(_Fragment, { children: [hasMultipleAnswers && (_jsxs(Alert, { children: [_jsx(AlarmWarningFillIcon, { css: css `
30
+ return (_jsxs(_Fragment, { children: [showMultipleSolutions && (_jsxs(Alert, { children: [_jsx(AlarmWarningFillIcon, { css: css `
36
31
  width: 14px;
37
32
  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(SelectBoxView, { index: index + 1, isAnswer: showQuizSolution ? selection.isAnswer : undefined, isSelected: selected.includes(selection.value), image: selection.show.image, text: selection.show.text, onClick: freezeProblemNode
33
+ ` }), "\uC9C8\uBB38\uC5D0 \uD574\uB2F9\uD558\uB294 \uB2F5\uC744 \uBAA8\uB450 \uACE0\uB974\uB294 \uBB38\uC81C\uC785\uB2C8\uB2E4."] })), selections.map((selection, index) => (_jsx(SelectBoxView, { index: index + 1, isAnswer: showQuizSolution && "isAnswer" in selection
34
+ ? selection.isAnswer
35
+ : undefined, isSelected: selected.includes(selection.value), image: selection.show.image, text: selection.show.text, onClick: freezeProblemNode
39
36
  ? undefined
40
37
  : () => {
41
38
  const isSelected = selected.includes(selection.value);
@@ -54,7 +51,7 @@ export function SelectComponent(props) {
54
51
  if (!$isProblemSelectNode(node)) {
55
52
  return;
56
53
  }
57
- if (hasMultipleAnswers) {
54
+ if (showMultipleSolutions) {
58
55
  node.setSelected([...selected, selection.value]);
59
56
  }
60
57
  else {
@@ -72,9 +69,9 @@ export function SelectComponent(props) {
72
69
  display: flex;
73
70
  flex-direction: column;
74
71
  gap: 4px;
75
- ` }, { children: selections.map((selection, index) => (_jsx(SelectBoxEdit, { index: index + 1, isAnswer: Boolean(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: () => {
72
+ ` }, { children: selections.map((selection, index) => (_jsx(SelectBoxEdit, { index: index + 1, isAnswer: "isAnswer" in selection && 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: () => {
76
73
  setSettingOpen(true);
77
- } })] })), settingOpen && (_jsx(SettingForm, Object.assign({}, settingFormProps, { onClose: () => setSettingOpen(false) })))] }));
74
+ } })] })), settingOpen && (_jsx(SettingForm, { selections: selections, nodeKey: nodeKey, onClose: () => setSettingOpen(false) }))] }));
78
75
  }
79
76
  const Alert = styled.div(({ theme }) => css `
80
77
  display: flex;
@@ -21,12 +21,11 @@ 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, LayoutColumnLineIcon, } from "../../../../icons";
24
+ import { TextIcon, H1Icon, H2Icon, H3Icon, ListUnorderedIcon, ListOrderedIcon, DoubleQuotesLIcon, CodeViewIcon, SeparatorIcon, ImageLineIcon, InputMethodLineIcon, ListRadioIcon, } 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";
30
29
  // import useModal from "../../hooks/useModal";
31
30
  // import catTypingGif from "../../images/cat-typing.gif";
32
31
  // import { INSERT_IMAGE_COMMAND, InsertImageDialog } from "../ImagesPlugin";
@@ -100,7 +99,6 @@ function getQuizContextOptions(editor, theme) {
100
99
  },
101
100
  ],
102
101
  selected: [],
103
- hasMultipleAnswers: false,
104
102
  });
105
103
  },
106
104
  }),
@@ -197,17 +195,8 @@ export function getBaseOptions(props) {
197
195
  onSelect: () => setImageOpen(true),
198
196
  }),
199
197
  ];
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
- }
209
198
  // isQuizEnabled이거나 로컬스토리지 devMode가 true이면 퀴즈 컨텍스트 메뉴를 추가합니다.
210
- if (isQuizEnabled || isDevMode) {
199
+ if (isQuizEnabled || localStorage.getItem("devMode") === "true") {
211
200
  return [...getQuizContextOptions(editor, theme), ...baseOptions];
212
201
  }
213
202
  return baseOptions;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-monolith/cds",
3
- "version": "1.47.1",
3
+ "version": "1.48.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "sideEffects": false,
@@ -1,30 +0,0 @@
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;
@@ -1,89 +0,0 @@
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
- }
@@ -1,22 +0,0 @@
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;
@@ -1,45 +0,0 @@
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
- }
@@ -1,14 +0,0 @@
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;
@@ -1,125 +0,0 @@
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
- }