@team-monolith/cds 1.3.3 → 1.4.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/README.md +2 -2
- package/dist/CodleDesignSystemProvider.d.ts +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/patterns/LexicalEditor/LexicalEditor.d.ts +14 -0
- package/dist/patterns/LexicalEditor/LexicalEditor.js +39 -0
- package/dist/patterns/LexicalEditor/Plugins.d.ts +10 -0
- package/dist/patterns/LexicalEditor/Plugins.js +81 -0
- package/dist/patterns/LexicalEditor/hr.svg +3 -0
- package/dist/patterns/LexicalEditor/index.d.ts +2 -0
- package/dist/patterns/LexicalEditor/index.js +2 -0
- package/dist/patterns/LexicalEditor/nodes/ImageComponent.d.ts +18 -0
- package/dist/patterns/LexicalEditor/nodes/ImageComponent.js +143 -0
- package/dist/patterns/LexicalEditor/nodes/ImageNode.d.ts +56 -0
- package/dist/patterns/LexicalEditor/nodes/ImageNode.js +117 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentAdderPlugin/ComponentAdder.d.ts +10 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentAdderPlugin/ComponentAdder.js +68 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentAdderPlugin/ComponentAdderPlugin.d.ts +11 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentAdderPlugin/ComponentAdderPlugin.js +214 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentAdderPlugin/getContextMenuOptions.d.ts +6 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentAdderPlugin/getContextMenuOptions.js +90 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentAdderPlugin/index.d.ts +1 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentAdderPlugin/index.js +1 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentAdderPlugin/menu.svg +8 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentAdderPlugin/plus.svg +3 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentAdderPlugin/useDraggableBlockMenu.d.ts +16 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentAdderPlugin/useDraggableBlockMenu.js +234 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentAdderPlugin/useFloatingMenu.d.ts +20 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentAdderPlugin/useFloatingMenu.js +158 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentPickerMenuPlugin/ComponentPickerMenuItem.d.ts +10 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentPickerMenuPlugin/ComponentPickerMenuItem.js +40 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentPickerMenuPlugin/ComponentPickerMenuList.d.ts +9 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentPickerMenuPlugin/ComponentPickerMenuList.js +46 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentPickerMenuPlugin/ComponentPickerMenuPlugin.d.ts +26 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentPickerMenuPlugin/ComponentPickerMenuPlugin.js +174 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentPickerMenuPlugin/index.d.ts +3 -0
- package/dist/patterns/LexicalEditor/plugins/ComponentPickerMenuPlugin/index.js +3 -0
- package/dist/patterns/LexicalEditor/plugins/FloatingLinkEditorPlugin/FloatingLinkEditor.d.ts +11 -0
- package/dist/patterns/LexicalEditor/plugins/FloatingLinkEditorPlugin/FloatingLinkEditor.js +201 -0
- package/dist/patterns/LexicalEditor/plugins/FloatingLinkEditorPlugin/index.d.ts +6 -0
- package/dist/patterns/LexicalEditor/plugins/FloatingLinkEditorPlugin/index.js +62 -0
- package/dist/patterns/LexicalEditor/plugins/FloatingTextFormatToolbarPlugin/FloatingTextFormatPopup.d.ts +13 -0
- package/dist/patterns/LexicalEditor/plugins/FloatingTextFormatToolbarPlugin/FloatingTextFormatPopup.js +60 -0
- package/dist/patterns/LexicalEditor/plugins/FloatingTextFormatToolbarPlugin/index.d.ts +4 -0
- package/dist/patterns/LexicalEditor/plugins/FloatingTextFormatToolbarPlugin/index.js +190 -0
- package/dist/patterns/LexicalEditor/plugins/ImagesPlugin/InsertImageDialog.d.ts +7 -0
- package/dist/patterns/LexicalEditor/plugins/ImagesPlugin/InsertImageDialog.js +39 -0
- package/dist/patterns/LexicalEditor/plugins/ImagesPlugin/InsertImageUploadedDialogBody.d.ts +5 -0
- package/dist/patterns/LexicalEditor/plugins/ImagesPlugin/InsertImageUploadedDialogBody.js +49 -0
- package/dist/patterns/LexicalEditor/plugins/ImagesPlugin/InsertImageUriDialogBody.d.ts +5 -0
- package/dist/patterns/LexicalEditor/plugins/ImagesPlugin/InsertImageUriDialogBody.js +24 -0
- package/dist/patterns/LexicalEditor/plugins/ImagesPlugin/index.d.ts +14 -0
- package/dist/patterns/LexicalEditor/plugins/ImagesPlugin/index.js +151 -0
- package/dist/patterns/LexicalEditor/plugins/ListMaxIndentLevelPlugin/index.d.ts +12 -0
- package/dist/patterns/LexicalEditor/plugins/ListMaxIndentLevelPlugin/index.js +49 -0
- package/dist/patterns/LexicalEditor/plugins/MarkdownTransformers/index.d.ts +12 -0
- package/dist/patterns/LexicalEditor/plugins/MarkdownTransformers/index.js +184 -0
- package/dist/patterns/LexicalEditor/theme.d.ts +24 -0
- package/dist/patterns/LexicalEditor/theme.js +178 -0
- package/dist/patterns/LexicalEditor/utils/getDOMRangeRect.d.ts +8 -0
- package/dist/patterns/LexicalEditor/utils/getDOMRangeRect.js +22 -0
- package/dist/patterns/LexicalEditor/utils/getSelectedNode.d.ts +2 -0
- package/dist/patterns/LexicalEditor/utils/getSelectedNode.js +24 -0
- package/dist/patterns/LexicalEditor/utils/guard.d.ts +8 -0
- package/dist/patterns/LexicalEditor/utils/guard.js +10 -0
- package/dist/patterns/LexicalEditor/utils/point.d.ts +21 -0
- package/dist/patterns/LexicalEditor/utils/point.js +41 -0
- package/dist/patterns/LexicalEditor/utils/rect.d.ts +45 -0
- package/dist/patterns/LexicalEditor/utils/rect.js +99 -0
- package/dist/patterns/LexicalEditor/utils/setFloatingElemPosition.d.ts +1 -0
- package/dist/patterns/LexicalEditor/utils/setFloatingElemPosition.js +36 -0
- package/dist/patterns/LexicalEditor/utils/setFloatingElemPositionForLinkEditor.d.ts +1 -0
- package/dist/patterns/LexicalEditor/utils/setFloatingElemPositionForLinkEditor.js +32 -0
- package/dist/patterns/LexicalEditor/utils/url.d.ts +9 -0
- package/dist/patterns/LexicalEditor/utils/url.js +34 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
|
|
33
33
|
코들 스튜디오 시스템에서의 JL4 과업이 완료되지 않아, 두가지 버전을 동시에 관리해야합니다. 브랜치 이름은 다음과 같습니다.
|
|
34
34
|
|
|
35
|
+
- **1.0** : React 18 (최신 브랜치)
|
|
35
36
|
- **main** : React 17
|
|
36
|
-
- **1.0** : React 18
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
1.0 브랜치(`v1.x`) 는 [jce-codle-react](https://github.com/team-monolith-product/jce-codle-react)에서 사용 중이며, main 브랜치(`v0.x`)는 [jce-codle-jlext](https://github.com/team-monolith-product/jce-codle-jlext)에서 사용중입니다.
|
|
39
39
|
|
|
40
40
|
## 개발 및 배포 절차
|
|
41
41
|
|
|
@@ -83,6 +83,9 @@ interface CodleDesignSystemContext {
|
|
|
83
83
|
};
|
|
84
84
|
};
|
|
85
85
|
};
|
|
86
|
+
lexical?: {
|
|
87
|
+
uploadByFile: (file: File) => Promise<string>;
|
|
88
|
+
};
|
|
86
89
|
}
|
|
87
90
|
export declare const CodleDesignSystemContext: import("react").Context<CodleDesignSystemContext>;
|
|
88
91
|
export default function (props: {
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { SerializedEditorState, SerializedLexicalNode } from "lexical";
|
|
3
|
+
export interface LexicalEditorProps {
|
|
4
|
+
className?: string;
|
|
5
|
+
value: any;
|
|
6
|
+
onChange: (blocks: SerializedEditorState<SerializedLexicalNode>) => void;
|
|
7
|
+
/** editable. 수정 모드 / 읽기 모드 여부
|
|
8
|
+
* initialConfig에 전달되므로 마운트 된 이후 수정해도 반영되지 않음
|
|
9
|
+
*/
|
|
10
|
+
editable?: boolean;
|
|
11
|
+
/** 외부에서 플러그인을 주입하는 경우 활용함 */
|
|
12
|
+
children?: JSX.Element | string | (JSX.Element | string)[];
|
|
13
|
+
}
|
|
14
|
+
export declare function LexicalEditor(props: LexicalEditorProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { LexicalComposer, } from "@lexical/react/LexicalComposer";
|
|
3
|
+
import { HeadingNode, QuoteNode } from "@lexical/rich-text";
|
|
4
|
+
import { TableCellNode, TableNode, TableRowNode } from "@lexical/table";
|
|
5
|
+
import { ListItemNode, ListNode } from "@lexical/list";
|
|
6
|
+
import { CodeHighlightNode, CodeNode } from "@lexical/code";
|
|
7
|
+
import { AutoLinkNode, LinkNode } from "@lexical/link";
|
|
8
|
+
import { ImageNode } from "./nodes/ImageNode";
|
|
9
|
+
import { HorizontalRuleNode } from "@lexical/react/LexicalHorizontalRuleNode";
|
|
10
|
+
import { getTheme } from "./theme";
|
|
11
|
+
import { useTheme } from "@emotion/react";
|
|
12
|
+
import Plugins from "./Plugins";
|
|
13
|
+
export function LexicalEditor(props) {
|
|
14
|
+
const { value, onChange, editable = true, children } = props;
|
|
15
|
+
const theme = useTheme();
|
|
16
|
+
const initialConfig = {
|
|
17
|
+
namespace: "CodleLexicalEditor",
|
|
18
|
+
onError: (error) => console.error(error),
|
|
19
|
+
nodes: [
|
|
20
|
+
HeadingNode,
|
|
21
|
+
ListNode,
|
|
22
|
+
ListItemNode,
|
|
23
|
+
QuoteNode,
|
|
24
|
+
CodeNode,
|
|
25
|
+
CodeHighlightNode,
|
|
26
|
+
TableNode,
|
|
27
|
+
TableCellNode,
|
|
28
|
+
TableRowNode,
|
|
29
|
+
AutoLinkNode,
|
|
30
|
+
LinkNode,
|
|
31
|
+
ImageNode,
|
|
32
|
+
HorizontalRuleNode,
|
|
33
|
+
],
|
|
34
|
+
theme: getTheme(theme),
|
|
35
|
+
editorState: JSON.stringify(value),
|
|
36
|
+
editable: editable,
|
|
37
|
+
};
|
|
38
|
+
return (_jsxs(LexicalComposer, Object.assign({ initialConfig: initialConfig }, { children: [_jsx(Plugins, { onChange: onChange }), _jsx(_Fragment, { children: children })] })));
|
|
39
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** @jsxImportSource @emotion/react */
|
|
2
|
+
/**
|
|
3
|
+
* isEditable 등으로 인해 ComposerContext 내부에서 플러그인을 따로 정의할 컴포넌트가 필요하여 작성함.
|
|
4
|
+
*/
|
|
5
|
+
import { ReactElement } from "react";
|
|
6
|
+
import { SerializedEditorState, SerializedLexicalNode } from "lexical";
|
|
7
|
+
export interface PluginsProps {
|
|
8
|
+
onChange: (blocks: SerializedEditorState<SerializedLexicalNode>) => void;
|
|
9
|
+
}
|
|
10
|
+
export default function Plugins(props: PluginsProps): ReactElement;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "@emotion/react/jsx-runtime";
|
|
2
|
+
/** @jsxImportSource @emotion/react */
|
|
3
|
+
/**
|
|
4
|
+
* isEditable 등으로 인해 ComposerContext 내부에서 플러그인을 따로 정의할 컴포넌트가 필요하여 작성함.
|
|
5
|
+
*/
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
|
|
8
|
+
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
|
|
9
|
+
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
|
|
10
|
+
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
|
|
11
|
+
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
|
|
12
|
+
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
|
|
13
|
+
import { CODLE_TRANSFORMERS } from "./plugins/MarkdownTransformers";
|
|
14
|
+
import { ListPlugin } from "@lexical/react/LexicalListPlugin";
|
|
15
|
+
import { TabIndentationPlugin } from "@lexical/react/LexicalTabIndentationPlugin";
|
|
16
|
+
import { HorizontalRulePlugin } from "@lexical/react/LexicalHorizontalRulePlugin";
|
|
17
|
+
import { ComponentAdderPlugin } from "./plugins/ComponentAdderPlugin/ComponentAdderPlugin";
|
|
18
|
+
import { ComponentPickerMenuPlugin } from "./plugins/ComponentPickerMenuPlugin";
|
|
19
|
+
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
|
|
20
|
+
import FloatingTextFormatToolbarPlugin from "./plugins/FloatingTextFormatToolbarPlugin";
|
|
21
|
+
import ImagesPlugin from "./plugins/ImagesPlugin";
|
|
22
|
+
import { TablePlugin } from "@lexical/react/LexicalTablePlugin";
|
|
23
|
+
import hr from "./hr.svg";
|
|
24
|
+
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
|
|
25
|
+
import LexicalClickableLinkPlugin from "@lexical/react/LexicalClickableLinkPlugin";
|
|
26
|
+
import FloatingLinkEditorPlugin from "./plugins/FloatingLinkEditorPlugin";
|
|
27
|
+
import useLexicalEditable from "@lexical/react/useLexicalEditable";
|
|
28
|
+
import ListMaxIndentLevelPlugin from "./plugins/ListMaxIndentLevelPlugin";
|
|
29
|
+
import styled from "@emotion/styled";
|
|
30
|
+
export default function Plugins(props) {
|
|
31
|
+
const { onChange } = props;
|
|
32
|
+
const isEditable = useLexicalEditable();
|
|
33
|
+
const [floatingAnchorElem, setFloatingAnchorElem] = useState(null);
|
|
34
|
+
const [isLinkEditMode, setIsLinkEditMode] = useState(false);
|
|
35
|
+
const onRef = (_floatingAnchorElem) => {
|
|
36
|
+
if (_floatingAnchorElem !== null) {
|
|
37
|
+
setFloatingAnchorElem(_floatingAnchorElem);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
return (_jsxs(_Fragment, { children: [_jsx(RichTextPlugin, { contentEditable: _jsx(ScrollArea, { children: _jsx(FloatingAnchor, Object.assign({ ref: onRef }, { children: _jsx(StyledContentEditable, {}) })) }), placeholder: _jsx("div", { children: "\uB0B4\uC6A9\uC744 \uC785\uB825 \uD574 \uC8FC\uC138\uC694." }), ErrorBoundary: LexicalErrorBoundary }), _jsx(OnChangePlugin, { onChange: (editorState) => {
|
|
41
|
+
onChange(editorState.toJSON());
|
|
42
|
+
} }), _jsx(HistoryPlugin, {}), _jsx(AutoFocusPlugin, {}), floatingAnchorElem && (_jsxs(_Fragment, { children: [_jsx(ComponentAdderPlugin, { anchorElem: floatingAnchorElem }), _jsx(FloatingTextFormatToolbarPlugin, { anchorElem: floatingAnchorElem }), _jsx(FloatingLinkEditorPlugin, { anchorElem: floatingAnchorElem, isLinkEditMode: isLinkEditMode, setIsLinkEditMode: setIsLinkEditMode })] })), !isEditable && _jsx(LexicalClickableLinkPlugin, {}), _jsx(MarkdownShortcutPlugin, { transformers: CODLE_TRANSFORMERS }), _jsx(ComponentPickerMenuPlugin, {}), _jsx(ListPlugin, {}), _jsx(TabIndentationPlugin, {}), _jsx(HorizontalRulePlugin, {}), _jsx(ImagesPlugin, {}), _jsx(TablePlugin, {}), _jsx(LinkPlugin, {}), _jsx(ListMaxIndentLevelPlugin, { maxDepth: 5 })] }));
|
|
43
|
+
}
|
|
44
|
+
const ScrollArea = styled.div `
|
|
45
|
+
min-height: 150px;
|
|
46
|
+
border: 0;
|
|
47
|
+
display: flex;
|
|
48
|
+
position: relative;
|
|
49
|
+
outline: 0;
|
|
50
|
+
z-index: 0;
|
|
51
|
+
overflow: auto;
|
|
52
|
+
resize: vertical;
|
|
53
|
+
`;
|
|
54
|
+
const FloatingAnchor = styled.div `
|
|
55
|
+
flex: auto;
|
|
56
|
+
position: relative;
|
|
57
|
+
resize: vertical;
|
|
58
|
+
z-index: -1;
|
|
59
|
+
`;
|
|
60
|
+
const StyledContentEditable = styled(ContentEditable) `
|
|
61
|
+
border: 0;
|
|
62
|
+
font-size: 15px;
|
|
63
|
+
display: block;
|
|
64
|
+
position: relative;
|
|
65
|
+
outline: 0;
|
|
66
|
+
padding: 8px 28px 40px 64px;
|
|
67
|
+
min-height: 150px;
|
|
68
|
+
|
|
69
|
+
hr {
|
|
70
|
+
// HorizontalRuleNode는 Lexical theme에 등록되어 있지 않습니다.
|
|
71
|
+
// 따라서 hr 태그를 직접 조작합니다.
|
|
72
|
+
|
|
73
|
+
// FloatingMenu가 올바른 위치에 붙으려면 충분한 높이를 가져야합니다.
|
|
74
|
+
// HR 태그에서 border를 이용해서 중심에 선을 그리기 어렵습니다.
|
|
75
|
+
// hr svg는 1px 높이의 svg입니다.
|
|
76
|
+
// center 및 repeat-x를 지정하여 좌우로 긴 선을 그립니다.
|
|
77
|
+
height: 26px;
|
|
78
|
+
border: 0;
|
|
79
|
+
background: url(${hr}) center repeat-x;
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
/// <reference types="react" />
|
|
9
|
+
import type { LexicalCommand, NodeKey } from "lexical";
|
|
10
|
+
export declare const RIGHT_CLICK_IMAGE_COMMAND: LexicalCommand<MouseEvent>;
|
|
11
|
+
export default function ImageComponent({ src, altText, nodeKey, width, height, maxWidth, }: {
|
|
12
|
+
altText: string;
|
|
13
|
+
height: "inherit" | number;
|
|
14
|
+
maxWidth: number;
|
|
15
|
+
nodeKey: NodeKey;
|
|
16
|
+
src: string;
|
|
17
|
+
width: "inherit" | number;
|
|
18
|
+
}): JSX.Element;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
3
|
+
import { useLexicalNodeSelection } from "@lexical/react/useLexicalNodeSelection";
|
|
4
|
+
import { mergeRegister } from "@lexical/utils";
|
|
5
|
+
import { $getNodeByKey, $getSelection, $isNodeSelection, $isRangeSelection, $setSelection, CLICK_COMMAND, COMMAND_PRIORITY_LOW, createCommand, DRAGSTART_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, KEY_ENTER_COMMAND, KEY_ESCAPE_COMMAND, SELECTION_CHANGE_COMMAND, } from "lexical";
|
|
6
|
+
import { Suspense, useCallback, useEffect, useRef, useState } from "react";
|
|
7
|
+
import { $isImageNode } from "./ImageNode";
|
|
8
|
+
const imageCache = new Set();
|
|
9
|
+
export const RIGHT_CLICK_IMAGE_COMMAND = createCommand("RIGHT_CLICK_IMAGE_COMMAND");
|
|
10
|
+
function useSuspenseImage(src) {
|
|
11
|
+
if (!imageCache.has(src)) {
|
|
12
|
+
throw new Promise((resolve) => {
|
|
13
|
+
const img = new Image();
|
|
14
|
+
img.src = src;
|
|
15
|
+
img.onload = () => {
|
|
16
|
+
imageCache.add(src);
|
|
17
|
+
resolve(null);
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function LazyImage({ altText, className, imageRef, src, width, height, maxWidth, }) {
|
|
23
|
+
useSuspenseImage(src);
|
|
24
|
+
return (_jsx("img", { className: className || undefined, src: src, alt: altText, ref: imageRef, style: {
|
|
25
|
+
height,
|
|
26
|
+
maxWidth,
|
|
27
|
+
width,
|
|
28
|
+
}, draggable: "false" }));
|
|
29
|
+
}
|
|
30
|
+
export default function ImageComponent({ src, altText, nodeKey, width, height, maxWidth, }) {
|
|
31
|
+
const imageRef = useRef(null);
|
|
32
|
+
const buttonRef = useRef(null);
|
|
33
|
+
const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey);
|
|
34
|
+
const [editor] = useLexicalComposerContext();
|
|
35
|
+
const [selection, setSelection] = useState(null);
|
|
36
|
+
const activeEditorRef = useRef(null);
|
|
37
|
+
const onDelete = useCallback((payload) => {
|
|
38
|
+
if (isSelected && $isNodeSelection($getSelection())) {
|
|
39
|
+
const event = payload;
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
const node = $getNodeByKey(nodeKey);
|
|
42
|
+
if ($isImageNode(node)) {
|
|
43
|
+
node.remove();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}, [isSelected, nodeKey]);
|
|
48
|
+
const onEnter = useCallback((event) => {
|
|
49
|
+
const latestSelection = $getSelection();
|
|
50
|
+
const buttonElem = buttonRef.current;
|
|
51
|
+
if (isSelected &&
|
|
52
|
+
$isNodeSelection(latestSelection) &&
|
|
53
|
+
latestSelection.getNodes().length === 1) {
|
|
54
|
+
if (buttonElem !== null && buttonElem !== document.activeElement) {
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
buttonElem.focus();
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}, [isSelected]);
|
|
62
|
+
const onEscape = useCallback((event) => {
|
|
63
|
+
if (buttonRef.current === event.target) {
|
|
64
|
+
$setSelection(null);
|
|
65
|
+
editor.update(() => {
|
|
66
|
+
setSelected(true);
|
|
67
|
+
const parentRootElement = editor.getRootElement();
|
|
68
|
+
if (parentRootElement !== null) {
|
|
69
|
+
parentRootElement.focus();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}, [editor, setSelected]);
|
|
76
|
+
const onClick = useCallback((payload) => {
|
|
77
|
+
const event = payload;
|
|
78
|
+
if (event.target === imageRef.current) {
|
|
79
|
+
if (event.shiftKey) {
|
|
80
|
+
setSelected(!isSelected);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
clearSelection();
|
|
84
|
+
setSelected(true);
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}, [isSelected, setSelected, clearSelection]);
|
|
90
|
+
const onRightClick = useCallback((event) => {
|
|
91
|
+
editor.getEditorState().read(() => {
|
|
92
|
+
const latestSelection = $getSelection();
|
|
93
|
+
const domElement = event.target;
|
|
94
|
+
if (domElement.tagName === "IMG" &&
|
|
95
|
+
$isRangeSelection(latestSelection) &&
|
|
96
|
+
latestSelection.getNodes().length === 1) {
|
|
97
|
+
editor.dispatchCommand(RIGHT_CLICK_IMAGE_COMMAND, event);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}, [editor]);
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
let isMounted = true;
|
|
103
|
+
const rootElement = editor.getRootElement();
|
|
104
|
+
const unregister = mergeRegister(editor.registerUpdateListener(({ editorState }) => {
|
|
105
|
+
if (isMounted) {
|
|
106
|
+
setSelection(editorState.read(() => $getSelection()));
|
|
107
|
+
}
|
|
108
|
+
}), editor.registerCommand(SELECTION_CHANGE_COMMAND, (_, activeEditor) => {
|
|
109
|
+
activeEditorRef.current = activeEditor;
|
|
110
|
+
return false;
|
|
111
|
+
}, COMMAND_PRIORITY_LOW), editor.registerCommand(CLICK_COMMAND, onClick, COMMAND_PRIORITY_LOW), editor.registerCommand(RIGHT_CLICK_IMAGE_COMMAND, onClick, COMMAND_PRIORITY_LOW), editor.registerCommand(DRAGSTART_COMMAND, (event) => {
|
|
112
|
+
if (event.target === imageRef.current) {
|
|
113
|
+
// TODO This is just a temporary workaround for FF to behave like other browsers.
|
|
114
|
+
// Ideally, this handles drag & drop too (and all browsers).
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_DELETE_COMMAND, onDelete, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_BACKSPACE_COMMAND, onDelete, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_ENTER_COMMAND, onEnter, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_ESCAPE_COMMAND, onEscape, COMMAND_PRIORITY_LOW));
|
|
120
|
+
rootElement === null || rootElement === void 0 ? void 0 : rootElement.addEventListener("contextmenu", onRightClick);
|
|
121
|
+
return () => {
|
|
122
|
+
isMounted = false;
|
|
123
|
+
unregister();
|
|
124
|
+
rootElement === null || rootElement === void 0 ? void 0 : rootElement.removeEventListener("contextmenu", onRightClick);
|
|
125
|
+
};
|
|
126
|
+
}, [
|
|
127
|
+
clearSelection,
|
|
128
|
+
editor,
|
|
129
|
+
isSelected,
|
|
130
|
+
nodeKey,
|
|
131
|
+
onDelete,
|
|
132
|
+
onEnter,
|
|
133
|
+
onEscape,
|
|
134
|
+
onClick,
|
|
135
|
+
onRightClick,
|
|
136
|
+
setSelected,
|
|
137
|
+
]);
|
|
138
|
+
const draggable = isSelected && $isNodeSelection(selection);
|
|
139
|
+
const isFocused = isSelected;
|
|
140
|
+
return (_jsx(Suspense, Object.assign({ fallback: null }, { children: _jsx("div", Object.assign({ draggable: draggable }, { children: _jsx(LazyImage, { className: isFocused
|
|
141
|
+
? `focused ${$isNodeSelection(selection) ? "draggable" : ""}`
|
|
142
|
+
: null, src: src, altText: altText, imageRef: imageRef, width: width, height: height, maxWidth: maxWidth }) })) })));
|
|
143
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
/// <reference types="react" />
|
|
9
|
+
import type { DOMConversionMap, DOMExportOutput, EditorConfig, LexicalEditor, LexicalNode, NodeKey, SerializedEditor, SerializedLexicalNode, Spread } from "lexical";
|
|
10
|
+
import { DecoratorNode } from "lexical";
|
|
11
|
+
export interface ImagePayload {
|
|
12
|
+
altText: string;
|
|
13
|
+
caption?: LexicalEditor;
|
|
14
|
+
height?: number;
|
|
15
|
+
key?: NodeKey;
|
|
16
|
+
maxWidth?: number;
|
|
17
|
+
showCaption?: boolean;
|
|
18
|
+
src: string;
|
|
19
|
+
width?: number;
|
|
20
|
+
captionsEnabled?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export type SerializedImageNode = Spread<{
|
|
23
|
+
altText: string;
|
|
24
|
+
caption: SerializedEditor;
|
|
25
|
+
height?: number;
|
|
26
|
+
maxWidth: number;
|
|
27
|
+
showCaption: boolean;
|
|
28
|
+
src: string;
|
|
29
|
+
width?: number;
|
|
30
|
+
}, SerializedLexicalNode>;
|
|
31
|
+
export declare class ImageNode extends DecoratorNode<JSX.Element> {
|
|
32
|
+
__src: string;
|
|
33
|
+
__altText: string;
|
|
34
|
+
__width: "inherit" | number;
|
|
35
|
+
__height: "inherit" | number;
|
|
36
|
+
__maxWidth: number;
|
|
37
|
+
__showCaption: boolean;
|
|
38
|
+
__caption: LexicalEditor;
|
|
39
|
+
__captionsEnabled: boolean;
|
|
40
|
+
static getType(): string;
|
|
41
|
+
static clone(node: ImageNode): ImageNode;
|
|
42
|
+
static importJSON(serializedNode: SerializedImageNode): ImageNode;
|
|
43
|
+
exportDOM(): DOMExportOutput;
|
|
44
|
+
static importDOM(): DOMConversionMap | null;
|
|
45
|
+
constructor(src: string, altText: string, maxWidth: number, width?: "inherit" | number, height?: "inherit" | number, showCaption?: boolean, caption?: LexicalEditor, captionsEnabled?: boolean, key?: NodeKey);
|
|
46
|
+
exportJSON(): SerializedImageNode;
|
|
47
|
+
setWidthAndHeight(width: "inherit" | number, height: "inherit" | number): void;
|
|
48
|
+
setShowCaption(showCaption: boolean): void;
|
|
49
|
+
createDOM(config: EditorConfig): HTMLElement;
|
|
50
|
+
updateDOM(): false;
|
|
51
|
+
getSrc(): string;
|
|
52
|
+
getAltText(): string;
|
|
53
|
+
decorate(): JSX.Element;
|
|
54
|
+
}
|
|
55
|
+
export declare function $createImageNode({ altText, height, maxWidth, captionsEnabled, src, width, showCaption, caption, key, }: ImagePayload): ImageNode;
|
|
56
|
+
export declare function $isImageNode(node: LexicalNode | null | undefined): node is ImageNode;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { $applyNodeReplacement, createEditor, DecoratorNode } from "lexical";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Suspense } from "react";
|
|
5
|
+
const ImageComponent = React.lazy(
|
|
6
|
+
// @ts-ignore
|
|
7
|
+
() => import("./ImageComponent"));
|
|
8
|
+
function convertImageElement(domNode) {
|
|
9
|
+
if (domNode instanceof HTMLImageElement) {
|
|
10
|
+
const { alt: altText, src, width, height } = domNode;
|
|
11
|
+
const node = $createImageNode({ altText, height, src, width });
|
|
12
|
+
return { node };
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
export class ImageNode extends DecoratorNode {
|
|
17
|
+
static getType() {
|
|
18
|
+
return "image";
|
|
19
|
+
}
|
|
20
|
+
static clone(node) {
|
|
21
|
+
return new ImageNode(node.__src, node.__altText, node.__maxWidth, node.__width, node.__height, node.__showCaption, node.__caption, node.__captionsEnabled, node.__key);
|
|
22
|
+
}
|
|
23
|
+
static importJSON(serializedNode) {
|
|
24
|
+
const { altText, height, width, maxWidth, caption, src, showCaption } = serializedNode;
|
|
25
|
+
const node = $createImageNode({
|
|
26
|
+
altText,
|
|
27
|
+
height,
|
|
28
|
+
maxWidth,
|
|
29
|
+
showCaption,
|
|
30
|
+
src,
|
|
31
|
+
width,
|
|
32
|
+
});
|
|
33
|
+
const nestedEditor = node.__caption;
|
|
34
|
+
const editorState = nestedEditor.parseEditorState(caption.editorState);
|
|
35
|
+
if (!editorState.isEmpty()) {
|
|
36
|
+
nestedEditor.setEditorState(editorState);
|
|
37
|
+
}
|
|
38
|
+
return node;
|
|
39
|
+
}
|
|
40
|
+
exportDOM() {
|
|
41
|
+
const element = document.createElement("img");
|
|
42
|
+
element.setAttribute("src", this.__src);
|
|
43
|
+
element.setAttribute("alt", this.__altText);
|
|
44
|
+
element.setAttribute("width", this.__width.toString());
|
|
45
|
+
element.setAttribute("height", this.__height.toString());
|
|
46
|
+
return { element };
|
|
47
|
+
}
|
|
48
|
+
static importDOM() {
|
|
49
|
+
return {
|
|
50
|
+
img: (node) => ({
|
|
51
|
+
conversion: convertImageElement,
|
|
52
|
+
priority: 0,
|
|
53
|
+
}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
constructor(src, altText, maxWidth, width, height, showCaption, caption, captionsEnabled, key) {
|
|
57
|
+
super(key);
|
|
58
|
+
this.__src = src;
|
|
59
|
+
this.__altText = altText;
|
|
60
|
+
this.__maxWidth = maxWidth;
|
|
61
|
+
this.__width = width || "inherit";
|
|
62
|
+
this.__height = height || "inherit";
|
|
63
|
+
this.__showCaption = showCaption || false;
|
|
64
|
+
this.__caption = caption || createEditor();
|
|
65
|
+
this.__captionsEnabled = captionsEnabled || captionsEnabled === undefined;
|
|
66
|
+
}
|
|
67
|
+
exportJSON() {
|
|
68
|
+
return {
|
|
69
|
+
altText: this.getAltText(),
|
|
70
|
+
caption: this.__caption.toJSON(),
|
|
71
|
+
height: this.__height === "inherit" ? 0 : this.__height,
|
|
72
|
+
maxWidth: this.__maxWidth,
|
|
73
|
+
showCaption: this.__showCaption,
|
|
74
|
+
src: this.getSrc(),
|
|
75
|
+
type: "image",
|
|
76
|
+
version: 1,
|
|
77
|
+
width: this.__width === "inherit" ? 0 : this.__width,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
setWidthAndHeight(width, height) {
|
|
81
|
+
const writable = this.getWritable();
|
|
82
|
+
writable.__width = width;
|
|
83
|
+
writable.__height = height;
|
|
84
|
+
}
|
|
85
|
+
setShowCaption(showCaption) {
|
|
86
|
+
const writable = this.getWritable();
|
|
87
|
+
writable.__showCaption = showCaption;
|
|
88
|
+
}
|
|
89
|
+
// View
|
|
90
|
+
createDOM(config) {
|
|
91
|
+
const span = document.createElement("span");
|
|
92
|
+
const theme = config.theme;
|
|
93
|
+
const className = theme.image;
|
|
94
|
+
if (className !== undefined) {
|
|
95
|
+
span.className = className;
|
|
96
|
+
}
|
|
97
|
+
return span;
|
|
98
|
+
}
|
|
99
|
+
updateDOM() {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
getSrc() {
|
|
103
|
+
return this.__src;
|
|
104
|
+
}
|
|
105
|
+
getAltText() {
|
|
106
|
+
return this.__altText;
|
|
107
|
+
}
|
|
108
|
+
decorate() {
|
|
109
|
+
return (_jsx(Suspense, Object.assign({ fallback: null }, { children: _jsx(ImageComponent, { src: this.__src, altText: this.__altText, width: this.__width, height: this.__height, maxWidth: this.__maxWidth, nodeKey: this.getKey() }) })));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export function $createImageNode({ altText, height, maxWidth = 500, captionsEnabled, src, width, showCaption, caption, key, }) {
|
|
113
|
+
return $applyNodeReplacement(new ImageNode(src, altText, maxWidth, width, height, showCaption, caption, captionsEnabled, key));
|
|
114
|
+
}
|
|
115
|
+
export function $isImageNode(node) {
|
|
116
|
+
return node instanceof ImageNode;
|
|
117
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface ComponentAdderProps {
|
|
3
|
+
className?: string;
|
|
4
|
+
onPlusClick?: () => void;
|
|
5
|
+
onMenuClick?: () => void;
|
|
6
|
+
onMenuDragStart?: (event: React.DragEvent<HTMLDivElement>) => void;
|
|
7
|
+
onMenuDragEnd?: (event: React.DragEvent<HTMLDivElement>) => void;
|
|
8
|
+
}
|
|
9
|
+
declare const _default: React.ForwardRefExoticComponent<ComponentAdderProps & React.RefAttributes<HTMLDivElement>>;
|
|
10
|
+
export default _default;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import plus from "./plus.svg";
|
|
5
|
+
import menu from "./menu.svg";
|
|
6
|
+
import styled from "@emotion/styled";
|
|
7
|
+
function ComponentAdder(props, ref) {
|
|
8
|
+
const { className, onPlusClick, onMenuClick, onMenuDragStart, onMenuDragEnd } = props;
|
|
9
|
+
const [editor] = useLexicalComposerContext();
|
|
10
|
+
return (_jsxs(Container, Object.assign({ ref: ref, className: className }, { children: [_jsx(PlusContainer, Object.assign({ onClick: onPlusClick }, { children: editor._editable && _jsx(PlusIcon, {}) })), _jsx(MenuContainer, Object.assign({ draggable: true, onClick: onMenuClick, onDragStart: onMenuDragStart, onDragEnd: onMenuDragEnd }, { children: editor._editable && _jsx(MenuIcon, {}) }))] })));
|
|
11
|
+
}
|
|
12
|
+
export default forwardRef(ComponentAdder);
|
|
13
|
+
const Container = styled.div `
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: flex-start;
|
|
16
|
+
gap: 3px;
|
|
17
|
+
|
|
18
|
+
position: absolute;
|
|
19
|
+
left: 0;
|
|
20
|
+
top: 0;
|
|
21
|
+
will-change: transform;
|
|
22
|
+
`;
|
|
23
|
+
const PlusContainer = styled.div `
|
|
24
|
+
display: flex;
|
|
25
|
+
width: 26px;
|
|
26
|
+
height: 26px;
|
|
27
|
+
justify-content: center;
|
|
28
|
+
align-items: center;
|
|
29
|
+
border-radius: 5px;
|
|
30
|
+
background: ${({ theme }) => theme.color.foreground.neutralAlt};
|
|
31
|
+
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
|
|
34
|
+
&:hover {
|
|
35
|
+
background-color: #eff2f5;
|
|
36
|
+
}
|
|
37
|
+
`;
|
|
38
|
+
const PlusIcon = styled.div `
|
|
39
|
+
width: 24px;
|
|
40
|
+
height: 24px;
|
|
41
|
+
// svg는 hover 이벤트를 path마다 받아서 불편함
|
|
42
|
+
// Lexical에서 동일한 접근법을 사용하고 있음
|
|
43
|
+
background-image: url(${plus});
|
|
44
|
+
`;
|
|
45
|
+
const MenuContainer = styled.div `
|
|
46
|
+
display: flex;
|
|
47
|
+
width: 26px;
|
|
48
|
+
height: 26px;
|
|
49
|
+
justify-content: center;
|
|
50
|
+
align-items: center;
|
|
51
|
+
border-radius: 5px;
|
|
52
|
+
background: ${({ theme }) => theme.color.foreground.neutralAlt};
|
|
53
|
+
|
|
54
|
+
cursor: grab;
|
|
55
|
+
|
|
56
|
+
&:active {
|
|
57
|
+
cursor: grabbing;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
&:hover {
|
|
61
|
+
background-color: #eff2f5;
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
const MenuIcon = styled.div `
|
|
65
|
+
width: 24px;
|
|
66
|
+
height: 24px;
|
|
67
|
+
background-image: url(${menu});
|
|
68
|
+
`;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* + 버튼을 눌러 컴포넌트를 추가하는 플러그인
|
|
3
|
+
* ComponentPickerPlugin과 동일한 메뉴가 노출됩니다.
|
|
4
|
+
*/
|
|
5
|
+
/** @jsxImportSource @emotion/react */
|
|
6
|
+
/// <reference types="react" />
|
|
7
|
+
export interface ComponentAdderPluginProps {
|
|
8
|
+
anchorElem: HTMLElement;
|
|
9
|
+
}
|
|
10
|
+
export declare const COMPONENT_ADDER_MENU_CLASSNAME = "component-adder-menu";
|
|
11
|
+
export declare function ComponentAdderPlugin(props: ComponentAdderPluginProps): JSX.Element | null;
|