@team-monolith/cds 1.119.0 → 1.119.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.
@@ -17,7 +17,7 @@ import { ListItemNode, ListNode } from "@lexical/list";
17
17
  import { CodeHighlightNode, CodeNode } from "@lexical/code";
18
18
  import { LinkNode } from "@lexical/link";
19
19
  import { FileNode } from "./FileNode";
20
- export declare const nodes: (typeof ColoredQuoteNode | typeof QuoteNode | typeof ImageNode | typeof VideoNode | typeof FileNode | typeof ProblemInputNode | typeof ProblemSelectNode | typeof SelfEvaluationNode | typeof SheetInputNode | typeof SheetSelectNode | typeof LayoutContainerNode | typeof ToggleContainerNode | typeof ToggleContentNode | typeof ToggleTitleNode | typeof HeadingNode | typeof ListNode | typeof ListItemNode | typeof CodeNode | typeof CodeHighlightNode | typeof TableNode | typeof TableCellNode | typeof TableRowNode | typeof LinkNode | typeof HorizontalRuleNode | {
20
+ export declare const nodes: (typeof ColoredQuoteNode | typeof QuoteNode | typeof ImageNode | typeof VideoNode | typeof FileNode | typeof ProblemInputNode | typeof ProblemSelectNode | typeof SelfEvaluationNode | typeof SheetInputNode | typeof SheetSelectNode | typeof LayoutContainerNode | typeof ToggleContentNode | typeof ToggleTitleNode | typeof ToggleContainerNode | typeof HeadingNode | typeof ListNode | typeof ListItemNode | typeof CodeNode | typeof CodeHighlightNode | typeof TableNode | typeof TableCellNode | typeof TableRowNode | typeof LinkNode | typeof HorizontalRuleNode | {
21
21
  replace: typeof QuoteNode;
22
22
  with: () => ColoredQuoteNode;
23
23
  })[];
@@ -11,6 +11,7 @@
11
11
  import { addClassNamesToElement } from "@lexical/utils";
12
12
  import { $getSiblingCaret, $isElementNode, $rewindSiblingCaret, ElementNode, isHTMLElement, } from "lexical";
13
13
  import { setDomHiddenUntilFound } from "../../utils/toggleUtils";
14
+ import { DATA_LEXICAL_TOGGLE_ICON_BUTTON } from "./ToggleTitleNode";
14
15
  export function $convertToggleContainerElement(domNode) {
15
16
  // open 속성이 없으면 기본값 false
16
17
  const isOpen = "open" in domNode ? domNode.open : false;
@@ -55,7 +56,9 @@ export class ToggleContainerNode extends ElementNode {
55
56
  // IS_CHROME 분기 제거
56
57
  createDOM(config) {
57
58
  const dom = document.createElement("div");
58
- dom.setAttribute("open", "");
59
+ if (this.__open) {
60
+ dom.setAttribute("open", "");
61
+ }
59
62
  addClassNamesToElement(dom, config.theme.toggle.container);
60
63
  return dom;
61
64
  }
@@ -81,7 +84,7 @@ export class ToggleContainerNode extends ElementNode {
81
84
  if (!isHTMLElement(titleDom)) {
82
85
  throw new Error("Expected titleDom to be an HTMLElement");
83
86
  }
84
- const iconButton = titleDom.firstElementChild;
87
+ const iconButton = titleDom.querySelector(`[${DATA_LEXICAL_TOGGLE_ICON_BUTTON}]`);
85
88
  if (iconButton) {
86
89
  iconButton.setAttribute("aria-expanded", currentOpen.toString());
87
90
  }
@@ -6,6 +6,7 @@
6
6
  *
7
7
  */
8
8
  import { DOMConversionOutput, EditorConfig, ElementNode, LexicalEditor, LexicalNode, RangeSelection } from "lexical";
9
+ export declare const DATA_LEXICAL_TOGGLE_ICON_BUTTON = "data-lexical-toggle-icon-button";
9
10
  export declare function $convertToggleTitleElement(_domNode: HTMLElement): DOMConversionOutput | null;
10
11
  /** @noInheritDoc */
11
12
  export declare class ToggleTitleNode extends ElementNode {
@@ -10,6 +10,7 @@ import { addClassNamesToElement } from "@lexical/utils";
10
10
  import { $createParagraphNode, $isElementNode, buildImportMap, ElementNode, } from "lexical";
11
11
  import { $isToggleContainerNode } from "./ToggleContainerNode";
12
12
  import { $isToggleContentNode } from "./ToggleContentNode";
13
+ export const DATA_LEXICAL_TOGGLE_ICON_BUTTON = "data-lexical-toggle-icon-button";
13
14
  export function $convertToggleTitleElement(_domNode) {
14
15
  const node = $createToggleTitleNode();
15
16
  return {
@@ -44,6 +45,7 @@ export class ToggleTitleNode extends ElementNode {
44
45
  addClassNamesToElement(iconBtn, config.theme.toggle.iconBtn);
45
46
  iconBtn.setAttribute("aria-label", "Toggle content");
46
47
  iconBtn.setAttribute("contenteditable", "false");
48
+ iconBtn.setAttribute(DATA_LEXICAL_TOGGLE_ICON_BUTTON, "true");
47
49
  editor.getEditorState().read(() => {
48
50
  const containerNode = this.getParent();
49
51
  if ($isToggleContainerNode(containerNode)) {
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  /**
3
3
  * Context Menu (:: 버튼)
4
4
  */
5
- import { $createParagraphNode, ParagraphNode, } from "lexical";
5
+ import { $createParagraphNode, $createTextNode, $isElementNode, ParagraphNode, } from "lexical";
6
6
  import { ComponentPickerOption, getBaseOptions, } from "../ComponentPickerMenuPlugin";
7
7
  import { $createHeadingNode, $createQuoteNode, HeadingNode, } from "@lexical/rich-text";
8
8
  import { $setBlocksType } from "@lexical/selection";
@@ -12,7 +12,8 @@ import { H1Icon, H2Icon, H3Icon, CloseFillIcon, ListUnorderedIcon, ListOrderedIc
12
12
  import { $isColoredQuoteNode, } from "../../nodes/ColoredQuoteNode";
13
13
  import { useTheme } from "@emotion/react";
14
14
  import { css } from "@emotion/css";
15
- import { $isFileNode, $isImageNode, $isVideoNode, $isProblemInputNode, } from "../../nodes";
15
+ import { $unwrapNode } from "@lexical/utils";
16
+ import { $isFileNode, $isImageNode, $isVideoNode, $isProblemInputNode, $isToggleContainerNode, $isToggleTitleNode, $isToggleContentNode, } from "../../nodes";
16
17
  import { $isProblemSelectNode, } from "../../nodes/ProblemSelectNode";
17
18
  import { $isSheetInputNode } from "../../nodes/SheetInputNode";
18
19
  import { $isSheetSelectNode, } from "../../nodes/SheetSelectNode";
@@ -163,6 +164,60 @@ function getListContextMenuOptions(editor, theme, node) {
163
164
  }),
164
165
  ];
165
166
  }
167
+ function convertToggleTitleToHeading(container, tag) {
168
+ const titleNode = container.getFirstChild();
169
+ if (!$isToggleTitleNode(titleNode)) {
170
+ return;
171
+ }
172
+ const contentNode = titleNode.getNextSibling();
173
+ if (!$isToggleContentNode(contentNode)) {
174
+ return;
175
+ }
176
+ const headingNode = $createHeadingNode(tag);
177
+ const nodesForHeading = titleNode
178
+ .getChildren()
179
+ .flatMap((child) => $isElementNode(child) ? child.getChildren() : [child]);
180
+ if (nodesForHeading.length > 0) {
181
+ headingNode.append(...nodesForHeading);
182
+ }
183
+ if (headingNode.getChildrenSize() === 0) {
184
+ headingNode.append($createTextNode(""));
185
+ }
186
+ titleNode.replace(headingNode);
187
+ $unwrapNode(contentNode);
188
+ $unwrapNode(container);
189
+ headingNode.selectStart();
190
+ }
191
+ function getToggleContextMenuOptions(editor, container) {
192
+ return [
193
+ ...[1, 2, 3].map((n) => {
194
+ const titleMap = {
195
+ 1: i18n.t("큰 제목", { context: "렉시컬 드롭다운 메뉴" }),
196
+ 2: i18n.t("중간 제목", { context: "렉시컬 드롭다운 메뉴" }),
197
+ 3: i18n.t("작은 제목", { context: "렉시컬 드롭다운 메뉴" }),
198
+ };
199
+ const iconMap = {
200
+ 1: _jsx(H1Icon, {}),
201
+ 2: _jsx(H2Icon, {}),
202
+ 3: _jsx(H3Icon, {}),
203
+ };
204
+ return new ComponentPickerOption(titleMap[n], {
205
+ icon: iconMap[n],
206
+ keywords: ["heading", "header", `h${n}`, titleMap[n]],
207
+ onSelect: () => editor.update(() => {
208
+ convertToggleTitleToHeading(container, `h${n}`);
209
+ }),
210
+ });
211
+ }),
212
+ new ComponentPickerOption(i18n.t("블록 삭제"), {
213
+ icon: _jsx(CloseFillIcon, {}),
214
+ keywords: [],
215
+ onSelect: () => {
216
+ container.remove();
217
+ },
218
+ }),
219
+ ];
220
+ }
166
221
  function getProblemInputContextMenuOptions(node) {
167
222
  return [
168
223
  new ComponentPickerOption(i18n.t("블록 삭제"), {
@@ -244,8 +299,8 @@ function getColoredQuoteContextMenuOptions(editor, theme, node) {
244
299
  return [
245
300
  new ComponentPickerOption(i18n.t("회색"), {
246
301
  icon: (_jsx(InputMethodLineIcon, { color: theme.color.container.marbleOnContainer })),
247
- iconContainerClassName: css `
248
- background: ${theme.color.container.marbleContainer};
302
+ iconContainerClassName: css `
303
+ background: ${theme.color.container.marbleContainer};
249
304
  `,
250
305
  keywords: ["grey", "gray"],
251
306
  onSelect: () => {
@@ -256,8 +311,8 @@ function getColoredQuoteContextMenuOptions(editor, theme, node) {
256
311
  }),
257
312
  new ComponentPickerOption(i18n.t("빨간색"), {
258
313
  icon: (_jsx(InputMethodLineIcon, { color: theme.color.container.redOnContainer })),
259
- iconContainerClassName: css `
260
- background: ${theme.color.container.redContainer};
314
+ iconContainerClassName: css `
315
+ background: ${theme.color.container.redContainer};
261
316
  `,
262
317
  keywords: ["red"],
263
318
  onSelect: () => {
@@ -268,8 +323,8 @@ function getColoredQuoteContextMenuOptions(editor, theme, node) {
268
323
  }),
269
324
  new ComponentPickerOption(i18n.t("노란색"), {
270
325
  icon: (_jsx(InputMethodLineIcon, { color: theme.color.container.yellowOnContainer })),
271
- iconContainerClassName: css `
272
- background: ${theme.color.container.yellowContainer};
326
+ iconContainerClassName: css `
327
+ background: ${theme.color.container.yellowContainer};
273
328
  `,
274
329
  keywords: ["yellow"],
275
330
  onSelect: () => {
@@ -280,8 +335,8 @@ function getColoredQuoteContextMenuOptions(editor, theme, node) {
280
335
  }),
281
336
  new ComponentPickerOption(i18n.t("파란색"), {
282
337
  icon: (_jsx(InputMethodLineIcon, { color: theme.color.container.blueOnContainer })),
283
- iconContainerClassName: css `
284
- background: ${theme.color.container.blueContainer};
338
+ iconContainerClassName: css `
339
+ background: ${theme.color.container.blueContainer};
285
340
  `,
286
341
  keywords: ["blue"],
287
342
  onSelect: () => {
@@ -292,8 +347,8 @@ function getColoredQuoteContextMenuOptions(editor, theme, node) {
292
347
  }),
293
348
  new ComponentPickerOption(i18n.t("초록색"), {
294
349
  icon: (_jsx(InputMethodLineIcon, { color: theme.color.container.greenOnContainer })),
295
- iconContainerClassName: css `
296
- background: ${theme.color.container.greenContainer};
350
+ iconContainerClassName: css `
351
+ background: ${theme.color.container.greenContainer};
297
352
  `,
298
353
  keywords: ["green"],
299
354
  onSelect: () => {
@@ -360,6 +415,16 @@ export function useContextMenuOptions(props) {
360
415
  else if (node instanceof ListNode || node instanceof ListItemNode) {
361
416
  return getListContextMenuOptions(editor, theme, node);
362
417
  }
418
+ else if ($isToggleContainerNode(node)) {
419
+ return getToggleContextMenuOptions(editor, node);
420
+ }
421
+ else if ($isToggleTitleNode(node) || $isToggleContentNode(node)) {
422
+ const parent = node.getParent();
423
+ if (parent && $isToggleContainerNode(parent)) {
424
+ return getToggleContextMenuOptions(editor, parent);
425
+ }
426
+ return [];
427
+ }
363
428
  else if ($isColoredQuoteNode(node)) {
364
429
  return getColoredQuoteContextMenuOptions(editor, theme, node);
365
430
  }
@@ -15,7 +15,7 @@ import { LexicalTypeaheadMenuPlugin, MenuOption, useBasicTypeaheadTriggerMatch,
15
15
  import { $createHeadingNode, $createQuoteNode } from "@lexical/rich-text";
16
16
  import { $setBlocksType } from "@lexical/selection";
17
17
  import { INSERT_TABLE_COMMAND } from "@lexical/table";
18
- import { $createParagraphNode, $getSelection, $isRangeSelection, } from "lexical";
18
+ import { $createParagraphNode, $findMatchingParent, $getSelection, $isRangeSelection, } from "lexical";
19
19
  import { useCallback, useMemo, useState } from "react";
20
20
  import * as ReactDOM from "react-dom";
21
21
  import { css as cssToClassName } from "@emotion/css";
@@ -39,6 +39,7 @@ import { i18n } from "../../../../i18n/i18n";
39
39
  import { useTranslation } from "react-i18next";
40
40
  import { getTexts } from "../../../../texts";
41
41
  import { INSERT_TOGGLE_COMMAND } from "../TogglePlugin";
42
+ import { $isToggleTitleNode } from "../../nodes";
42
43
  // import useModal from "../../hooks/useModal";
43
44
  // import catTypingGif from "../../images/cat-typing.gif";
44
45
  // import { INSERT_IMAGE_COMMAND, InsertImageDialog } from "../ImagesPlugin";
@@ -60,6 +61,16 @@ export class ComponentPickerOption extends MenuOption {
60
61
  this.onSelect = options.onSelect.bind(this);
61
62
  }
62
63
  }
64
+ function isSelectionInsideToggleTitle(editor) {
65
+ return editor.getEditorState().read(() => {
66
+ const selection = $getSelection();
67
+ if (!$isRangeSelection(selection)) {
68
+ return false;
69
+ }
70
+ const titleNode = $findMatchingParent(selection.anchor.getNode(), (node) => $isToggleTitleNode(node));
71
+ return Boolean(titleNode);
72
+ });
73
+ }
63
74
  function getDynamicOptions(editor, queryString) {
64
75
  const options = [];
65
76
  // Lexical 원본 코드를 가져왔기 때문에 코드 변경을 최소화하기위함
@@ -198,6 +209,44 @@ function getQuizContextOptions(editor, theme) {
198
209
  ` }))),
199
210
  ];
200
211
  }
212
+ function getToggleTitleOptions(props) {
213
+ const { editor } = props;
214
+ return [
215
+ new ComponentPickerOption(i18n.t("본문", { context: "렉시컬 드롭다운 메뉴" }), {
216
+ icon: _jsx(TextIcon, {}),
217
+ // eslint-disable-next-line i18next/no-literal-string
218
+ keywords: ["normal", "paragraph", "p", "text", "본문", "단락", "내용"],
219
+ onSelect: () => editor.update(() => {
220
+ const selection = $getSelection();
221
+ if ($isRangeSelection(selection)) {
222
+ $setBlocksType(selection, () => $createParagraphNode());
223
+ }
224
+ }),
225
+ }),
226
+ ...[1, 2, 3].map((n) => {
227
+ const titleMap = {
228
+ 1: i18n.t("큰 제목", { context: "렉시컬 드롭다운 메뉴" }),
229
+ 2: i18n.t("중간 제목", { context: "렉시컬 드롭다운 메뉴" }),
230
+ 3: i18n.t("작은 제목", { context: "렉시컬 드롭다운 메뉴" }),
231
+ };
232
+ const iconMap = {
233
+ 1: _jsx(H1Icon, {}),
234
+ 2: _jsx(H2Icon, {}),
235
+ 3: _jsx(H3Icon, {}),
236
+ };
237
+ return new ComponentPickerOption(titleMap[n], {
238
+ icon: iconMap[n],
239
+ keywords: ["heading", "header", `h${n}`, titleMap[n]],
240
+ onSelect: () => editor.update(() => {
241
+ const selection = $getSelection();
242
+ if ($isRangeSelection(selection)) {
243
+ $setBlocksType(selection, () => $createHeadingNode(`h${n}`));
244
+ }
245
+ }),
246
+ });
247
+ }),
248
+ ];
249
+ }
201
250
  export function getBaseOptions(props) {
202
251
  const { editor, theme, setImageOpen, setVideoOpen, setFileOpen, isSheetEnabled, isQuizEnabled, showFileUpload, } = props;
203
252
  const baseOptions = [
@@ -329,23 +378,30 @@ export function ComponentPickerMenuPlugin(props) {
329
378
  const [editor] = useLexicalComposerContext();
330
379
  const theme = useTheme();
331
380
  const [queryString, setQueryString] = useState(null);
381
+ const [isInsideToggleTitle, setIsInsideToggleTitle] = useState(() => isSelectionInsideToggleTitle(editor));
332
382
  const [imageOpen, setImageOpen] = useState(false);
333
383
  const [videoOpen, setVideoOpen] = useState(false);
334
384
  const [fileOpen, setFileOpen] = useState(false);
335
385
  const checkForTriggerMatch = useBasicTypeaheadTriggerMatch("/", {
336
386
  minLength: 0,
337
387
  });
388
+ const handleQueryChange = useCallback((nextQuery) => {
389
+ setQueryString(nextQuery);
390
+ setIsInsideToggleTitle(() => isSelectionInsideToggleTitle(editor));
391
+ }, [editor]);
338
392
  const options = useMemo(() => {
339
- const baseOptions = getBaseOptions({
340
- editor,
341
- theme,
342
- setImageOpen,
343
- setVideoOpen,
344
- isSheetEnabled,
345
- isQuizEnabled,
346
- setFileOpen,
347
- showFileUpload,
348
- });
393
+ const baseOptions = isInsideToggleTitle
394
+ ? getToggleTitleOptions({ editor })
395
+ : getBaseOptions({
396
+ editor,
397
+ theme,
398
+ setImageOpen,
399
+ setVideoOpen,
400
+ isSheetEnabled,
401
+ isQuizEnabled,
402
+ setFileOpen,
403
+ showFileUpload,
404
+ });
349
405
  if (!queryString) {
350
406
  return baseOptions;
351
407
  }
@@ -357,7 +413,7 @@ export function ComponentPickerMenuPlugin(props) {
357
413
  option.keywords.some((keyword) => regex.test(keyword)))),
358
414
  ];
359
415
  // eslint-disable-next-line react-hooks/exhaustive-deps
360
- }, [editor, queryString]);
416
+ }, [editor, queryString, isInsideToggleTitle]);
361
417
  const onSelectOption = useCallback((selectedOption, nodeToRemove, closeMenu, matchingString) => {
362
418
  if (selectedOption instanceof ComponentDrawerOption) {
363
419
  return;
@@ -378,7 +434,7 @@ export function ComponentPickerMenuPlugin(props) {
378
434
  editor.dispatchCommand(INSERT_VIDEO_COMMAND, props);
379
435
  }, onClose: () => setVideoOpen(false), shouldReset: true }), _jsx(UploadFileDialog, { open: fileOpen, onClose: () => setFileOpen(false), onChange: (props) => {
380
436
  editor.dispatchCommand(UPLOAD_FILE_COMMAND, props);
381
- } }), _jsx(LexicalTypeaheadMenuPlugin, { onQueryChange: setQueryString, onSelectOption: onSelectOption, triggerFn: checkForTriggerMatch, options: options, anchorClassName: cssToClassName `
437
+ } }), _jsx(LexicalTypeaheadMenuPlugin, { onQueryChange: handleQueryChange, onSelectOption: onSelectOption, triggerFn: checkForTriggerMatch, options: options, anchorClassName: cssToClassName `
382
438
  z-index: ${ZINDEX.DIALOG + 1};
383
439
  `, menuRenderFn: (anchorElementRef, { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }) => anchorElementRef.current && options.length
384
440
  ? ReactDOM.createPortal(_jsx(ComponentPickerMenuList, { options: options, selectedIndex: selectedIndex, selectOptionAndCleanUp: selectOptionAndCleanUp, setHighlightedIndex: setHighlightedIndex }), anchorElementRef.current)
@@ -296,9 +296,10 @@ export function getTheme(theme, editable) {
296
296
  `,
297
297
  toggle: {
298
298
  container: css `
299
- display: flex;
300
- flex-direction: column;
301
- gap: 4px;
299
+ margin: 16px 0;
300
+ &[open] > summary {
301
+ margin-bottom: 4px;
302
+ }
302
303
  `,
303
304
  title: css `
304
305
  display: flex;
@@ -308,6 +309,7 @@ export function getTheme(theme, editable) {
308
309
  & > :last-child {
309
310
  flex: 1;
310
311
  min-width: 0;
312
+ margin: 0;
311
313
  }
312
314
  `,
313
315
  iconBtn: css `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-monolith/cds",
3
- "version": "1.119.0",
3
+ "version": "1.119.2",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "sideEffects": false,