返回 presentation-ai
custom-item-break-plugin.tsx
根目录 / src / components / notebook / presentation / editor / plugins / custom-item-break-plugin.tsx
1 import {
2 KEYS,
3 nanoid,
4 NodeApi,
5 PathApi,
6 type NodeEntry,
7 type TElement,
8 } from "platejs";
9 import { createTPlatePlugin, type PlateEditor } from "platejs/react";
10
11 import { BULLET_ITEM, COLUMN_GROUP, PARENT_CHILD_RELATIONSHIP } from "../lib";
12
13 const CUSTOM_ITEM_TYPES = new Set<string>(
14 Object.entries(PARENT_CHILD_RELATIONSHIP).flatMap(
15 ([parentType, { child }]) =>
16 parentType === COLUMN_GROUP ? [] : Array.isArray(child) ? child : [child],
17 ),
18 );
19
20 function isElementNode(node: unknown): node is TElement {
21 return (
22 typeof node === "object" &&
23 node !== null &&
24 "type" in node &&
25 "children" in node
26 );
27 }
28
29 function isCustomItemElement(node: unknown): node is TElement {
30 return isElementNode(node) && CUSTOM_ITEM_TYPES.has(node.type);
31 }
32
33 function isParentForItem(parent: TElement, itemType: string): boolean {
34 const relationship =
35 PARENT_CHILD_RELATIONSHIP[
36 parent.type as keyof typeof PARENT_CHILD_RELATIONSHIP
37 ];
38
39 if (!relationship || parent.type === COLUMN_GROUP) return false;
40
41 return Array.isArray(relationship.child)
42 ? relationship.child.includes(itemType as never)
43 : relationship.child === itemType;
44 }
45
46 function getActiveCustomItemEntry(
47 editor: PlateEditor,
48 ): NodeEntry<TElement> | null {
49 const blockEntry = editor.api.block();
50 if (!blockEntry) return null;
51
52 const [block, blockPath] = blockEntry;
53
54 if (isCustomItemElement(block)) {
55 return [block, blockPath];
56 }
57
58 const itemEntry = editor.api.above({
59 at: blockPath,
60 match: (node) => isCustomItemElement(node),
61 }) as NodeEntry<TElement> | undefined;
62
63 return itemEntry ?? null;
64 }
65
66 function createBlankItemFrom(item: TElement): TElement {
67 const itemRecord = item as TElement & Record<string, unknown>;
68 const alignment =
69 typeof itemRecord.alignment === "string"
70 ? { alignment: itemRecord.alignment }
71 : {};
72
73 if (item.type === BULLET_ITEM) {
74 return {
75 ...alignment,
76 forceFullWidth: true,
77 id: nanoid(),
78 type: item.type,
79 children: [{ text: "" }],
80 };
81 }
82
83 return {
84 ...alignment,
85 id: nanoid(),
86 type: item.type,
87 children: [
88 {
89 id: nanoid(),
90 type: KEYS.p,
91 children: [{ text: "" }],
92 },
93 ],
94 };
95 }
96
97 function insertCustomItemOrExit(editor: PlateEditor, reverse = false): void {
98 const itemEntry = getActiveCustomItemEntry(editor);
99
100 if (!itemEntry) {
101 editor.tf.insertExitBreak({ reverse });
102 return;
103 }
104
105 const [item, itemPath] = itemEntry;
106 const parentPath = PathApi.parent(itemPath);
107 const parent = NodeApi.get(editor, parentPath);
108
109 if (!isElementNode(parent) || !isParentForItem(parent, item.type)) {
110 editor.tf.insertExitBreak({ reverse });
111 return;
112 }
113
114 const insertionPath = reverse ? itemPath : PathApi.next(itemPath);
115 editor.tf.insertNodes(createBlankItemFrom(item), {
116 at: insertionPath,
117 select: true,
118 });
119 }
120
121 export const CustomItemBreakPlugin = createTPlatePlugin({
122 key: "presentation-custom-item-break",
123 shortcuts: {
124 insert: {
125 keys: "mod+shift+enter",
126 handler: ({ editor }) => {
127 insertCustomItemOrExit(editor);
128 },
129 },
130 },
131 handlers: {
132 onKeyDown: ({ editor, event }) => {
133 if (
134 event.defaultPrevented ||
135 event.key !== "Enter" ||
136 event.altKey ||
137 !event.shiftKey ||
138 (!event.ctrlKey && !event.metaKey)
139 ) {
140 return;
141 }
142
143 const itemEntry = getActiveCustomItemEntry(editor);
144 if (!itemEntry) return;
145
146 const [item, itemPath] = itemEntry;
147 const parentPath = PathApi.parent(itemPath);
148 const parent = NodeApi.get(editor, parentPath);
149
150 if (!isElementNode(parent) || !isParentForItem(parent, item.type)) {
151 return;
152 }
153
154 event.preventDefault();
155 event.stopPropagation();
156
157 insertCustomItemOrExit(editor);
158
159 return true;
160 },
161 },
162 });
163
163 lines Plain Text