@lobehub/editor 4.24.1 → 4.25.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.
Files changed (148) hide show
  1. package/es/codemirror/style.js +42 -0
  2. package/es/editor-kernel/kernel.d.ts +9 -0
  3. package/es/editor-kernel/kernel.js +74 -11
  4. package/es/editor-kernel/react/useDecorators.js +2 -4
  5. package/es/headless/collapsible-plugin.js +74 -0
  6. package/es/headless/index.js +2 -0
  7. package/es/headless.d.ts +30 -0
  8. package/es/headless.js +2618 -139
  9. package/es/index.d.ts +26 -3
  10. package/es/index.js +17 -2
  11. package/es/locale/index.d.ts +14 -0
  12. package/es/locale/index.js +14 -0
  13. package/es/plugins/block/command/index.js +24 -0
  14. package/es/plugins/block/plugin/index.js +7 -5
  15. package/es/plugins/block/react/ReactBlockPlugin.js +43 -1
  16. package/es/plugins/block/react/core/menu-position.js +20 -0
  17. package/es/plugins/block/react/drag/drag-session.js +3 -2
  18. package/es/plugins/block/react/drag/drag-utils.js +13 -1
  19. package/es/plugins/codemirror-block/index.d.ts +2 -1
  20. package/es/plugins/codemirror-block/plugin/index.js +2 -0
  21. package/es/plugins/codemirror-block/react/CodemirrorNode.js +95 -5
  22. package/es/plugins/codemirror-block/react/useCodemirrorEditLock.js +33 -0
  23. package/es/plugins/codemirror-block/service/index.d.ts +40 -0
  24. package/es/plugins/codemirror-block/service/index.js +59 -0
  25. package/es/plugins/collapsible/command/index.d.ts +17 -0
  26. package/es/plugins/collapsible/command/index.js +72 -0
  27. package/es/plugins/collapsible/index.d.ts +7 -0
  28. package/es/plugins/collapsible/node/CollapsibleNode.d.ts +30 -0
  29. package/es/plugins/collapsible/node/CollapsibleNode.js +138 -0
  30. package/es/plugins/collapsible/plugin/index.d.ts +11 -0
  31. package/es/plugins/collapsible/plugin/index.js +535 -0
  32. package/es/plugins/collapsible/react/ReactCollapsiblePlugin.d.ts +6 -0
  33. package/es/plugins/collapsible/react/ReactCollapsiblePlugin.js +19 -0
  34. package/es/plugins/collapsible/react/index.d.ts +3 -0
  35. package/es/plugins/collapsible/react/style.js +67 -0
  36. package/es/plugins/collapsible/react/type.d.ts +6 -0
  37. package/es/plugins/common/plugin/index.js +14 -4
  38. package/es/plugins/common/plugin/progressive-select-all.js +81 -0
  39. package/es/plugins/common/plugin/register.js +73 -4
  40. package/es/plugins/common/react/ReactPlainText.js +27 -9
  41. package/es/plugins/link/command/index.js +26 -3
  42. package/es/plugins/link/conversion/index.js +237 -0
  43. package/es/plugins/link/index.d.ts +5 -3
  44. package/es/plugins/link/node/LinkBlockCardNode.d.ts +16 -0
  45. package/es/plugins/link/node/LinkBlockCardNode.js +66 -0
  46. package/es/plugins/link/node/LinkCardNode.d.ts +42 -0
  47. package/es/plugins/link/node/LinkCardNode.js +161 -0
  48. package/es/plugins/link/node/LinkIframeNode.d.ts +33 -0
  49. package/es/plugins/link/node/LinkIframeNode.js +131 -0
  50. package/es/plugins/link/node/LinkNode.d.ts +48 -2
  51. package/es/plugins/link/node/LinkNode.js +14 -6
  52. package/es/plugins/link/node/SchemaNode.d.ts +36 -0
  53. package/es/plugins/link/node/SchemaNode.js +149 -0
  54. package/es/plugins/link/normalization/index.d.ts +7 -0
  55. package/es/plugins/link/normalization/index.js +38 -0
  56. package/es/plugins/link/plugin/index.d.ts +26 -1
  57. package/es/plugins/link/plugin/index.js +124 -4
  58. package/es/plugins/link/react/ReactLinkPlugin.js +165 -7
  59. package/es/plugins/link/react/components/LinkCard.js +235 -0
  60. package/es/plugins/link/react/components/LinkEdit.js +93 -14
  61. package/es/plugins/link/react/components/LinkIframe.js +128 -0
  62. package/es/plugins/link/react/components/LinkToolbar.js +440 -111
  63. package/es/plugins/link/react/components/SchemaLink.js +49 -0
  64. package/es/plugins/link/react/index.d.ts +2 -2
  65. package/es/plugins/link/react/renderer-registry.d.ts +81 -0
  66. package/es/plugins/link/react/renderer-registry.js +80 -0
  67. package/es/plugins/link/react/style.js +63 -1
  68. package/es/plugins/link/react/type.d.ts +24 -1
  69. package/es/plugins/link/service/i-link-service.d.ts +156 -1
  70. package/es/plugins/link/service/i-link-service.js +258 -5
  71. package/es/plugins/list/plugin/index.js +9 -2
  72. package/es/plugins/litexml/command/diffCommand.js +94 -14
  73. package/es/plugins/litexml/command/index.js +187 -6
  74. package/es/plugins/litexml/diff-validation.js +237 -0
  75. package/es/plugins/litexml/node/DiffContentNode.js +67 -0
  76. package/es/plugins/litexml/node/DiffNode.d.ts +1 -1
  77. package/es/plugins/litexml/node/DiffNode.js +5 -2
  78. package/es/plugins/litexml/node/TableCellDiffNode.js +77 -0
  79. package/es/plugins/litexml/node/TableRowDiffNode.js +73 -0
  80. package/es/plugins/litexml/plugin/index.d.ts +2 -0
  81. package/es/plugins/litexml/plugin/index.js +62 -9
  82. package/es/plugins/litexml/react/DiffNodeToolbar/index.js +10 -12
  83. package/es/plugins/litexml/react/TableRowDiffToolbar.js +123 -0
  84. package/es/plugins/litexml/react/hooks/useHasDiffNode.js +5 -2
  85. package/es/plugins/litexml/react/index.js +6 -2
  86. package/es/plugins/litexml/react/style.js +18 -5
  87. package/es/plugins/litexml/table-cell-diff.js +153 -0
  88. package/es/plugins/litexml/table-row-diff.js +82 -0
  89. package/es/plugins/markdown/data-source/markdown/parse.js +25 -1
  90. package/es/plugins/markdown/plugin/index.js +5 -3
  91. package/es/plugins/slash/react/ReactSlashPlugin.js +22 -43
  92. package/es/plugins/slash/react/components/DefaultSlashMenu.js +76 -16
  93. package/es/plugins/slash/react/menu-utils.js +84 -0
  94. package/es/plugins/slash/service/i-slash-service.d.ts +3 -0
  95. package/es/plugins/table/plugin/index.js +4 -1
  96. package/es/plugins/table/plugin/single-cell-paste.js +79 -0
  97. package/es/plugins/table/react/TableController/hooks.js +7 -4
  98. package/es/plugins/table/react/hooks.js +2 -2
  99. package/es/plugins/table/react/index.js +2 -0
  100. package/es/plugins/table/react/style.js +15 -5
  101. package/es/plugins/table/react/useScrollFocusedTableCellIntoView.js +46 -0
  102. package/es/plugins/toc/index.d.ts +10 -0
  103. package/es/plugins/toc/plugin/index.d.ts +8 -0
  104. package/es/plugins/toc/plugin/index.js +56 -0
  105. package/es/plugins/toc/react/ReactTocPlugin.d.ts +6 -0
  106. package/es/plugins/toc/react/ReactTocPlugin.js +36 -0
  107. package/es/plugins/toc/react/getNearestScrollContainer.d.ts +6 -0
  108. package/es/plugins/toc/react/getNearestScrollContainer.js +18 -0
  109. package/es/plugins/toc/react/hooks/useActiveHeading.js +59 -0
  110. package/es/plugins/toc/react/hooks/useTocItems.js +50 -0
  111. package/es/plugins/toc/react/index.d.ts +5 -0
  112. package/es/plugins/toc/react/type.d.ts +30 -0
  113. package/es/plugins/toc/react/useToc.d.ts +5 -0
  114. package/es/plugins/toc/react/useToc.js +41 -0
  115. package/es/plugins/toc/service/i-toc-service.d.ts +39 -0
  116. package/es/plugins/toc/service/i-toc-service.js +125 -0
  117. package/es/plugins/toc/service/index.d.ts +3 -0
  118. package/es/plugins/toc/types.d.ts +23 -0
  119. package/es/plugins/toc/utils/index.d.ts +6 -0
  120. package/es/plugins/toc/utils/index.js +30 -0
  121. package/es/plugins/toolbar/react/index.js +26 -12
  122. package/es/plugins/toolbar/react/style.js +3 -2
  123. package/es/plugins/virtual-block/plugin/register.js +34 -12
  124. package/es/plugins/yjs/index.d.ts +6 -0
  125. package/es/plugins/yjs/plugin/index.d.ts +7 -0
  126. package/es/plugins/yjs/plugin/index.js +191 -0
  127. package/es/plugins/yjs/plugin/types.d.ts +16 -0
  128. package/es/plugins/yjs/plugin/utils/awareness.js +9 -0
  129. package/es/plugins/yjs/plugin/utils/editor-state.js +47 -0
  130. package/es/plugins/yjs/plugin/utils/history.js +31 -0
  131. package/es/plugins/yjs/plugin/utils/node-properties.js +39 -0
  132. package/es/plugins/yjs/plugin/utils/sync.js +24 -0
  133. package/es/plugins/yjs/react/codemirrorEditLockProvider.js +59 -0
  134. package/es/plugins/yjs/react/index.d.ts +23 -0
  135. package/es/plugins/yjs/react/index.js +200 -0
  136. package/es/plugins/yjs/service/index.d.ts +47 -0
  137. package/es/plugins/yjs/service/index.js +84 -0
  138. package/es/plugins/yjs/websocket-provider.d.ts +78 -0
  139. package/es/plugins/yjs/websocket-provider.js +263 -0
  140. package/es/react/hooks/useEditor.d.ts +24 -2
  141. package/es/react/hooks/useEditor.js +13 -3
  142. package/es/react.d.ts +2 -2
  143. package/es/renderer/LexicalRenderer.js +9 -2
  144. package/es/types/kernel.d.ts +16 -0
  145. package/es/utils/updatePosition.js +5 -2
  146. package/package.json +3 -1
  147. package/patches/@lexical__yjs@0.42.0.patch +42 -0
  148. package/scripts/postinstall-lexical-patch.cjs +62 -36
@@ -7,6 +7,7 @@ import { useTranslation } from "../../../editor-kernel/react/useTranslation.js";
7
7
  import { SELECT_AFTER_CODEMIRROR_COMMAND, SELECT_BEFORE_CODEMIRROR_COMMAND } from "../command/index.js";
8
8
  import { useLexicalNodeSelection } from "../../../editor-kernel/react/useLexicalNodeSelection.js";
9
9
  import MermaidPreview from "./MermaidPreview.js";
10
+ import { useCodemirrorEditLock } from "./useCodemirrorEditLock.js";
10
11
  import { ActionIcon, Block } from "@lobehub/ui";
11
12
  import { CodeXml, Eye } from "lucide-react";
12
13
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -21,6 +22,8 @@ const ReactCodemirrorNode = ({ node, className, editor }) => {
21
22
  const keydownRef = useRef("");
22
23
  const instanceRef = useRef(null);
23
24
  const isEmptyRef = useRef(false);
25
+ const hasLocalEditLockRef = useRef(false);
26
+ const releaseLockTimerRef = useRef(null);
24
27
  const t = useTranslation();
25
28
  const [isSelected, setSelected, clearSelection, isNodeSelected] = useLexicalNodeSelection(node.getKey());
26
29
  const [selectedLang, setSelectedLang] = useState(node.lang || "javascript");
@@ -30,6 +33,48 @@ const ReactCodemirrorNode = ({ node, className, editor }) => {
30
33
  const [expand, setExpand] = useState(true);
31
34
  const [preview, setPreview] = useState(false);
32
35
  const [code, setCode] = useState(node.code);
36
+ const { acquireLock, isLockedByRemote, lockOwnerName, releaseLock } = useCodemirrorEditLock(node.getKey(), "CodeMirror block");
37
+ const editLockRef = useRef({
38
+ acquireLock,
39
+ isLockedByRemote,
40
+ releaseLock
41
+ });
42
+ useEffect(() => {
43
+ editLockRef.current = {
44
+ acquireLock,
45
+ isLockedByRemote,
46
+ releaseLock
47
+ };
48
+ }, [
49
+ acquireLock,
50
+ isLockedByRemote,
51
+ releaseLock
52
+ ]);
53
+ const clearReleaseLockTimer = useCallback(() => {
54
+ if (!releaseLockTimerRef.current) return;
55
+ clearTimeout(releaseLockTimerRef.current);
56
+ releaseLockTimerRef.current = null;
57
+ }, []);
58
+ const acquireEditLock = useCallback(() => {
59
+ clearReleaseLockTimer();
60
+ const acquired = editLockRef.current.acquireLock();
61
+ if (acquired) hasLocalEditLockRef.current = true;
62
+ return acquired;
63
+ }, [clearReleaseLockTimer]);
64
+ const releaseEditLock = useCallback(() => {
65
+ clearReleaseLockTimer();
66
+ if (!hasLocalEditLockRef.current) return;
67
+ editLockRef.current.releaseLock();
68
+ hasLocalEditLockRef.current = false;
69
+ }, [clearReleaseLockTimer]);
70
+ const scheduleReleaseEditLock = useCallback(() => {
71
+ clearReleaseLockTimer();
72
+ releaseLockTimerRef.current = setTimeout(() => {
73
+ releaseLockTimerRef.current = null;
74
+ if (document.visibilityState === "hidden" || instanceRef.current?.view.hasFocus) return;
75
+ releaseEditLock();
76
+ }, 100);
77
+ }, [clearReleaseLockTimer, releaseEditLock]);
33
78
  const handleCopy = useCallback(async () => {
34
79
  if (instanceRef.current) {
35
80
  const code = instanceRef.current.getValue();
@@ -83,14 +128,20 @@ const ReactCodemirrorNode = ({ node, className, editor }) => {
83
128
  return;
84
129
  }
85
130
  if (isSelected && instanceRef.current && isNodeSelected) {
131
+ if (!acquireEditLock()) {
132
+ clearSelection();
133
+ return;
134
+ }
86
135
  if (instanceRef.current?.view.hasFocus) return;
87
136
  instanceRef.current.focus();
88
137
  if (keydownRef.current === "end") instanceRef.current.setSelectionToEnd();
89
138
  }
90
139
  }, [
91
- isSelected,
140
+ acquireEditLock,
141
+ clearSelection,
142
+ editor,
92
143
  isNodeSelected,
93
- editor
144
+ isSelected
94
145
  ]);
95
146
  useEffect(() => {
96
147
  if (instanceRef.current) return;
@@ -102,6 +153,7 @@ const ReactCodemirrorNode = ({ node, className, editor }) => {
102
153
  indentWithTabs: useTabs,
103
154
  lineNumbers: showLineNumbers,
104
155
  mode: node.lang,
156
+ readOnly: editLockRef.current.isLockedByRemote,
105
157
  tabSize,
106
158
  theme: "default",
107
159
  value: node.code
@@ -162,6 +214,11 @@ const ReactCodemirrorNode = ({ node, className, editor }) => {
162
214
  });
163
215
  }));
164
216
  instance.on("focus", () => {
217
+ if (!acquireEditLock()) {
218
+ instanceRef.current?.blur();
219
+ clearSelection();
220
+ return;
221
+ }
165
222
  if (editor.getEditorState().read(() => {
166
223
  const sel = $getSelection();
167
224
  if (!sel) return false;
@@ -169,11 +226,13 @@ const ReactCodemirrorNode = ({ node, className, editor }) => {
169
226
  return false;
170
227
  })) setSelected(true);
171
228
  });
229
+ instance.on("blur", scheduleReleaseEditLock);
172
230
  instanceRef.current = instance;
173
- if (isSelected) instanceRef.current.focus();
231
+ if (isSelected && acquireEditLock()) instanceRef.current.focus();
174
232
  });
175
233
  }
176
234
  return () => {
235
+ releaseEditLock();
177
236
  if (instanceRef.current) {
178
237
  instanceRef.current.destroy();
179
238
  instanceRef.current = null;
@@ -193,13 +252,44 @@ const ReactCodemirrorNode = ({ node, className, editor }) => {
193
252
  node,
194
253
  setSelected
195
254
  ]);
255
+ useEffect(() => {
256
+ const handleVisibilityChange = () => {
257
+ if (document.visibilityState === "visible" && !instanceRef.current?.view.hasFocus) releaseEditLock();
258
+ };
259
+ document.addEventListener("visibilitychange", handleVisibilityChange);
260
+ return () => document.removeEventListener("visibilitychange", handleVisibilityChange);
261
+ }, [releaseEditLock]);
262
+ useEffect(() => {
263
+ instanceRef.current?.setOption("readOnly", isLockedByRemote);
264
+ if (isLockedByRemote) {
265
+ releaseEditLock();
266
+ instanceRef.current?.blur();
267
+ clearSelection();
268
+ }
269
+ }, [
270
+ clearSelection,
271
+ isLockedByRemote,
272
+ releaseEditLock
273
+ ]);
274
+ const handleBlockMouseDown = useCallback((event) => {
275
+ event.stopPropagation();
276
+ if (!isLockedByRemote) return;
277
+ event.preventDefault();
278
+ instanceRef.current?.blur();
279
+ clearSelection();
280
+ }, [clearSelection, isLockedByRemote]);
196
281
  return /* @__PURE__ */ jsxs(Block, {
197
- className: cx(styles, isSelected && !isNodeSelected && "selected", className),
198
- onMouseDown: (e) => e.stopPropagation(),
282
+ className: cx(styles, isSelected && !isNodeSelected && "selected", isLockedByRemote && "collab-locked", className),
283
+ onMouseDown: handleBlockMouseDown,
199
284
  onMouseUp: (e) => e.stopPropagation(),
200
285
  onSelect: (e) => e.stopPropagation(),
201
286
  variant: "filled",
202
287
  children: [
288
+ isLockedByRemote && /* @__PURE__ */ jsxs("div", {
289
+ className: "cm-collab-lock",
290
+ contentEditable: false,
291
+ children: [lockOwnerName || "Someone", " editing"]
292
+ }),
203
293
  /* @__PURE__ */ jsx(Toolbar, {
204
294
  expand,
205
295
  extra: selectedLang === "mermaid" ? /* @__PURE__ */ jsx(ActionIcon, {
@@ -0,0 +1,33 @@
1
+ import { useLexicalComposerContext } from "../../../editor-kernel/react/react-context.js";
2
+ import { ICodemirrorEditLockService } from "../service/index.js";
3
+ import { useCallback, useEffect, useState } from "react";
4
+ //#region src/plugins/codemirror-block/react/useCodemirrorEditLock.ts
5
+ function useCodemirrorEditLock(key, label) {
6
+ const [editor] = useLexicalComposerContext();
7
+ const service = editor.requireService(ICodemirrorEditLockService);
8
+ const [, setVersion] = useState(0);
9
+ useEffect(() => {
10
+ return service?.subscribe(() => {
11
+ setVersion((value) => value + 1);
12
+ });
13
+ }, [service]);
14
+ const remoteLockOwner = service?.getRemoteLockOwner(key) || null;
15
+ const acquireLock = useCallback(() => {
16
+ return service?.acquireLock(key, label) ?? true;
17
+ }, [
18
+ key,
19
+ label,
20
+ service
21
+ ]);
22
+ const releaseLock = useCallback(() => {
23
+ service?.releaseLock(key);
24
+ }, [key, service]);
25
+ return {
26
+ acquireLock,
27
+ isLockedByRemote: !!remoteLockOwner,
28
+ lockOwnerName: remoteLockOwner?.name,
29
+ releaseLock
30
+ };
31
+ }
32
+ //#endregion
33
+ export { useCodemirrorEditLock };
@@ -0,0 +1,40 @@
1
+ import { IServiceID } from "../../../types/kernel.js";
2
+ import "../../../types/index.js";
3
+ //#region src/plugins/codemirror-block/service/index.d.ts
4
+ interface CodemirrorEditLock {
5
+ key: string;
6
+ label?: string;
7
+ updatedAt: number;
8
+ }
9
+ interface CodemirrorEditLockOwner {
10
+ id: number | string;
11
+ lock: CodemirrorEditLock;
12
+ name?: string;
13
+ }
14
+ interface CodemirrorEditLockProvider {
15
+ acquireLock: (key: string, label: string) => boolean;
16
+ getRemoteLockOwner: (key: string) => CodemirrorEditLockOwner | null;
17
+ releaseLock: (key: string) => void;
18
+ subscribe: (listener: () => void) => () => void;
19
+ }
20
+ interface ICodemirrorEditLockService {
21
+ acquireLock: (key: string, label: string) => boolean;
22
+ getRemoteLockOwner: (key: string) => CodemirrorEditLockOwner | null;
23
+ registerProvider: (provider: CodemirrorEditLockProvider) => () => void;
24
+ releaseLock: (key: string) => void;
25
+ subscribe: (listener: () => void) => () => void;
26
+ }
27
+ declare const ICodemirrorEditLockService: IServiceID<ICodemirrorEditLockService>;
28
+ declare class CodemirrorEditLockService implements ICodemirrorEditLockService {
29
+ private providerUnsubscribers;
30
+ private providers;
31
+ private subscribers;
32
+ acquireLock(key: string, label: string): boolean;
33
+ getRemoteLockOwner(key: string): CodemirrorEditLockOwner | null;
34
+ registerProvider(provider: CodemirrorEditLockProvider): () => void;
35
+ releaseLock(key: string): void;
36
+ subscribe(listener: () => void): () => void;
37
+ private notify;
38
+ }
39
+ //#endregion
40
+ export { CodemirrorEditLock, CodemirrorEditLockOwner, CodemirrorEditLockProvider, CodemirrorEditLockService, ICodemirrorEditLockService };
@@ -0,0 +1,59 @@
1
+ import { genServiceId, init_utils } from "../../../editor-kernel/utils.js";
2
+ //#region src/plugins/codemirror-block/service/index.ts
3
+ init_utils();
4
+ const ICodemirrorEditLockService = genServiceId("CodemirrorEditLockService");
5
+ var CodemirrorEditLockService = class {
6
+ constructor() {
7
+ this.providerUnsubscribers = /* @__PURE__ */ new Map();
8
+ this.providers = /* @__PURE__ */ new Set();
9
+ this.subscribers = /* @__PURE__ */ new Set();
10
+ }
11
+ acquireLock(key, label) {
12
+ if (this.getRemoteLockOwner(key)) return false;
13
+ const acquiredProviders = [];
14
+ for (const provider of this.providers) {
15
+ if (!provider.acquireLock(key, label)) {
16
+ acquiredProviders.forEach((acquiredProvider) => acquiredProvider.releaseLock(key));
17
+ this.notify();
18
+ return false;
19
+ }
20
+ acquiredProviders.push(provider);
21
+ }
22
+ this.notify();
23
+ return true;
24
+ }
25
+ getRemoteLockOwner(key) {
26
+ for (const provider of this.providers) {
27
+ const owner = provider.getRemoteLockOwner(key);
28
+ if (owner) return owner;
29
+ }
30
+ return null;
31
+ }
32
+ registerProvider(provider) {
33
+ this.providers.add(provider);
34
+ this.providerUnsubscribers.set(provider, provider.subscribe(() => this.notify()));
35
+ this.notify();
36
+ return () => {
37
+ this.providerUnsubscribers.get(provider)?.();
38
+ this.providerUnsubscribers.delete(provider);
39
+ this.providers.delete(provider);
40
+ this.notify();
41
+ };
42
+ }
43
+ releaseLock(key) {
44
+ this.providers.forEach((provider) => provider.releaseLock(key));
45
+ this.notify();
46
+ }
47
+ subscribe(listener) {
48
+ this.subscribers.add(listener);
49
+ listener();
50
+ return () => {
51
+ this.subscribers.delete(listener);
52
+ };
53
+ }
54
+ notify() {
55
+ this.subscribers.forEach((listener) => listener());
56
+ }
57
+ };
58
+ //#endregion
59
+ export { CodemirrorEditLockService, ICodemirrorEditLockService };
@@ -0,0 +1,17 @@
1
+ import "../node/CollapsibleNode.js";
2
+ import { LexicalEditor, LexicalNode, NodeKey } from "lexical";
3
+ //#region src/plugins/collapsible/command/index.d.ts
4
+ interface InsertCollapsiblePayload {
5
+ children?: LexicalNode[];
6
+ collapsed?: boolean;
7
+ title?: string;
8
+ }
9
+ interface UpdateCollapsiblePayload {
10
+ collapsed?: boolean;
11
+ nodeKey: NodeKey;
12
+ title?: string;
13
+ }
14
+ declare const INSERT_COLLAPSIBLE_COMMAND: import("lexical").LexicalCommand<InsertCollapsiblePayload | undefined>;
15
+ declare const UPDATE_COLLAPSIBLE_COMMAND: import("lexical").LexicalCommand<UpdateCollapsiblePayload>;
16
+ //#endregion
17
+ export { INSERT_COLLAPSIBLE_COMMAND, InsertCollapsiblePayload, UPDATE_COLLAPSIBLE_COMMAND, UpdateCollapsiblePayload };
@@ -0,0 +1,72 @@
1
+ import { $createCollapsibleNode, $isCollapsibleNode } from "../node/CollapsibleNode.js";
2
+ import { $createParagraphNode, $createTextNode, $getNodeByKey, $getSelection, $insertNodes, $isRangeSelection, COMMAND_PRIORITY_EDITOR, createCommand } from "lexical";
3
+ //#region src/plugins/collapsible/command/index.ts
4
+ const INSERT_COLLAPSIBLE_COMMAND = createCommand("INSERT_COLLAPSIBLE_COMMAND");
5
+ const UPDATE_COLLAPSIBLE_COMMAND = createCommand("UPDATE_COLLAPSIBLE_COMMAND");
6
+ function registerCollapsibleCommand(editor, canCollapse = () => true) {
7
+ const unregisterInsert = editor.registerCommand(INSERT_COLLAPSIBLE_COMMAND, (payload = {}) => {
8
+ editor.update(() => {
9
+ const payloadChildren = payload.children;
10
+ const hasPayloadChildren = Boolean(payloadChildren?.length);
11
+ const title = payload.title ?? (hasPayloadChildren ? "Details" : "");
12
+ const collapsibleNode = $createCollapsibleNode(title, Boolean(payload.collapsed));
13
+ let children;
14
+ if (payloadChildren?.length) children = ensureTitleChild(payloadChildren, title);
15
+ else {
16
+ const titleChild = $createParagraphNode();
17
+ if (title) titleChild.append($createTextNode(title));
18
+ children = [titleChild, $createParagraphNode()];
19
+ }
20
+ collapsibleNode.append(...children);
21
+ const parentCollapsible = $getSelectedCollapsibleAncestor();
22
+ if (parentCollapsible) parentCollapsible.insertAfter(collapsibleNode);
23
+ else $insertNodes([collapsibleNode]);
24
+ collapsibleNode.selectStart();
25
+ });
26
+ return true;
27
+ }, COMMAND_PRIORITY_EDITOR);
28
+ const unregisterUpdate = editor.registerCommand(UPDATE_COLLAPSIBLE_COMMAND, ({ collapsed, nodeKey, title }) => {
29
+ editor.update(() => {
30
+ const node = $getNodeByKey(nodeKey);
31
+ if (!$isCollapsibleNode(node)) return;
32
+ if (title !== void 0) {
33
+ node.setTitle(title);
34
+ updateTitleChild(node, title);
35
+ }
36
+ if (collapsed !== void 0 && (!collapsed || canCollapse(node))) node.setCollapsed(collapsed);
37
+ });
38
+ return true;
39
+ }, COMMAND_PRIORITY_EDITOR);
40
+ return () => {
41
+ unregisterInsert();
42
+ unregisterUpdate();
43
+ };
44
+ }
45
+ function $getSelectedCollapsibleAncestor() {
46
+ const selection = $getSelection();
47
+ if (!$isRangeSelection(selection)) return null;
48
+ let current = selection.anchor.getNode();
49
+ while (current) {
50
+ if ($isCollapsibleNode(current)) return current;
51
+ current = current.getParent();
52
+ }
53
+ return null;
54
+ }
55
+ function $createTitleParagraph(title) {
56
+ const paragraph = $createParagraphNode();
57
+ paragraph.append($createTextNode(title));
58
+ return paragraph;
59
+ }
60
+ function ensureTitleChild(children, title) {
61
+ if (!title) return children;
62
+ if (children[0]?.getTextContent().trim() === title.trim()) return children;
63
+ return [$createTitleParagraph(title), ...children];
64
+ }
65
+ function updateTitleChild(node, title) {
66
+ const titleParagraph = $createTitleParagraph(title);
67
+ const firstChild = node.getFirstChild();
68
+ if (firstChild) firstChild.replace(titleParagraph);
69
+ else node.append(titleParagraph);
70
+ }
71
+ //#endregion
72
+ export { INSERT_COLLAPSIBLE_COMMAND, UPDATE_COLLAPSIBLE_COMMAND, registerCollapsibleCommand };
@@ -0,0 +1,7 @@
1
+ import { $createCollapsibleNode, $isCollapsibleNode, CollapsibleNode, SerializedCollapsibleNode } from "./node/CollapsibleNode.js";
2
+ import { INSERT_COLLAPSIBLE_COMMAND, UPDATE_COLLAPSIBLE_COMMAND } from "./command/index.js";
3
+ import { CollapsiblePlugin, CollapsiblePluginOptions } from "./plugin/index.js";
4
+ import { ReactCollapsiblePluginProps } from "./react/type.js";
5
+ import ReactCollapsiblePlugin from "./react/ReactCollapsiblePlugin.js";
6
+ import "./react/index.js";
7
+ export { $createCollapsibleNode, $isCollapsibleNode, CollapsibleNode, CollapsiblePlugin, CollapsiblePluginOptions, INSERT_COLLAPSIBLE_COMMAND, ReactCollapsiblePlugin, type ReactCollapsiblePluginProps, SerializedCollapsibleNode, UPDATE_COLLAPSIBLE_COMMAND };
@@ -0,0 +1,30 @@
1
+ import { DOMConversionMap, DOMExportOutput, EditorConfig, ElementDOMSlot, ElementNode, LexicalEditor, LexicalNode, LexicalUpdateJSON, NodeKey, SerializedElementNode, Spread } from "lexical";
2
+ //#region src/plugins/collapsible/node/CollapsibleNode.d.ts
3
+ type SerializedCollapsibleNode = Spread<{
4
+ collapsed: boolean;
5
+ title: string;
6
+ }, SerializedElementNode>;
7
+ declare class CollapsibleNode extends ElementNode {
8
+ __collapsed: boolean;
9
+ __title: string;
10
+ static getType(): string;
11
+ static clone(node: CollapsibleNode): CollapsibleNode;
12
+ static importDOM(): DOMConversionMap | null;
13
+ static importJSON(serializedNode: SerializedCollapsibleNode): CollapsibleNode;
14
+ constructor(title?: string, collapsed?: boolean, key?: NodeKey);
15
+ createDOM(config: EditorConfig, editor: LexicalEditor): HTMLElement;
16
+ updateDOM(prevNode: this, dom: HTMLElement): boolean;
17
+ exportDOM(): DOMExportOutput;
18
+ exportJSON(): SerializedCollapsibleNode;
19
+ getDOMSlot(element: HTMLElement): ElementDOMSlot<HTMLElement>;
20
+ getTitle(): string;
21
+ isCollapsed(): boolean;
22
+ isShadowRoot(): boolean;
23
+ setCollapsed(collapsed: boolean): this;
24
+ setTitle(title: string): this;
25
+ updateFromJSON(serializedNode: LexicalUpdateJSON<SerializedCollapsibleNode>): this;
26
+ }
27
+ declare function $createCollapsibleNode(title?: string, collapsed?: boolean): CollapsibleNode;
28
+ declare function $isCollapsibleNode(node: LexicalNode | null | undefined): node is CollapsibleNode;
29
+ //#endregion
30
+ export { $createCollapsibleNode, $isCollapsibleNode, CollapsibleNode, SerializedCollapsibleNode };
@@ -0,0 +1,138 @@
1
+ import { $applyNodeReplacement, $createParagraphNode, $createTextNode, ElementNode } from "lexical";
2
+ import { addClassNamesToElement } from "@lexical/utils";
3
+ //#region src/plugins/collapsible/node/CollapsibleNode.ts
4
+ const CONTENT_SELECTOR = "[data-collapsible-content=\"true\"]";
5
+ const TOGGLE_SELECTOR = "[data-collapsible-toggle=\"true\"]";
6
+ var CollapsibleNode = class CollapsibleNode extends ElementNode {
7
+ static getType() {
8
+ return "collapsible";
9
+ }
10
+ static clone(node) {
11
+ return new CollapsibleNode(node.__title, node.__collapsed, node.__key);
12
+ }
13
+ static importDOM() {
14
+ return { details: () => ({
15
+ conversion: $convertDetailsElement,
16
+ priority: 1
17
+ }) };
18
+ }
19
+ static importJSON(serializedNode) {
20
+ const node = $createCollapsibleNode(serializedNode.title || "Details").updateFromJSON(serializedNode);
21
+ if (shouldPrependTitle(serializedNode)) {
22
+ const titleParagraph = $createTitleParagraph(serializedNode.title || "Details");
23
+ const firstChild = node.getFirstChild();
24
+ if (firstChild) firstChild.insertBefore(titleParagraph);
25
+ else node.append(titleParagraph);
26
+ }
27
+ return node;
28
+ }
29
+ constructor(title = "Details", collapsed = false, key) {
30
+ super(key);
31
+ this.__title = title;
32
+ this.__collapsed = collapsed;
33
+ }
34
+ createDOM(config, editor) {
35
+ const element = document.createElement("section");
36
+ addClassNamesToElement(element, config.theme.collapsible);
37
+ element.dataset.collapsible = "true";
38
+ element.dataset.collapsibleCollapsed = String(this.__collapsed);
39
+ const toggle = document.createElement("button");
40
+ toggle.type = "button";
41
+ toggle.contentEditable = "false";
42
+ toggle.dataset.collapsibleToggle = "true";
43
+ toggle.setAttribute("aria-expanded", String(!this.__collapsed));
44
+ toggle.setAttribute("aria-label", this.__collapsed ? "Expand collapsible block" : "Collapse collapsible block");
45
+ toggle.addEventListener("click", () => {
46
+ editor.update(() => {
47
+ const latest = this.getLatest();
48
+ if ($isCollapsibleNode(latest)) latest.setCollapsed(!latest.isCollapsed());
49
+ });
50
+ });
51
+ const content = document.createElement("div");
52
+ content.dataset.collapsibleContent = "true";
53
+ element.append(toggle, content);
54
+ return element;
55
+ }
56
+ updateDOM(prevNode, dom) {
57
+ const toggle = dom.querySelector(TOGGLE_SELECTOR);
58
+ dom.dataset.collapsibleCollapsed = String(this.__collapsed);
59
+ if (toggle && prevNode.__collapsed !== this.__collapsed) {
60
+ toggle.setAttribute("aria-expanded", String(!this.__collapsed));
61
+ toggle.setAttribute("aria-label", this.__collapsed ? "Expand collapsible block" : "Collapse collapsible block");
62
+ }
63
+ return false;
64
+ }
65
+ exportDOM() {
66
+ const element = document.createElement("details");
67
+ if (!this.__collapsed) element.setAttribute("open", "");
68
+ const summary = document.createElement("summary");
69
+ summary.textContent = this.getTitle();
70
+ element.append(summary);
71
+ return { element };
72
+ }
73
+ exportJSON() {
74
+ return {
75
+ ...super.exportJSON(),
76
+ collapsed: this.isCollapsed(),
77
+ title: this.getTitle()
78
+ };
79
+ }
80
+ getDOMSlot(element) {
81
+ const content = element.querySelector(CONTENT_SELECTOR);
82
+ if (content instanceof HTMLElement) return super.getDOMSlot(element).withElement(content);
83
+ return super.getDOMSlot(element);
84
+ }
85
+ getTitle() {
86
+ const latest = this.getLatest();
87
+ const firstChild = latest.getFirstChild();
88
+ return firstChild ? firstChild.getTextContent().trim() : latest.__title;
89
+ }
90
+ isCollapsed() {
91
+ return this.getLatest().__collapsed;
92
+ }
93
+ isShadowRoot() {
94
+ return true;
95
+ }
96
+ setCollapsed(collapsed) {
97
+ const writable = this.getWritable();
98
+ writable.__collapsed = collapsed;
99
+ return writable;
100
+ }
101
+ setTitle(title) {
102
+ const writable = this.getWritable();
103
+ writable.__title = title;
104
+ return writable;
105
+ }
106
+ updateFromJSON(serializedNode) {
107
+ return super.updateFromJSON(serializedNode).setTitle(serializedNode.title || "Details").setCollapsed(Boolean(serializedNode.collapsed));
108
+ }
109
+ };
110
+ function $createCollapsibleNode(title = "Details", collapsed = false) {
111
+ return $applyNodeReplacement(new CollapsibleNode(title, collapsed));
112
+ }
113
+ function $isCollapsibleNode(node) {
114
+ return node instanceof CollapsibleNode;
115
+ }
116
+ function $convertDetailsElement(domNode) {
117
+ if (!(domNode instanceof HTMLDetailsElement)) return { node: null };
118
+ return { node: $createCollapsibleNode(domNode.querySelector("summary")?.textContent?.trim() || "Details", !domNode.open) };
119
+ }
120
+ function $createTitleParagraph(title) {
121
+ const paragraph = $createParagraphNode();
122
+ paragraph.append($createTextNode(title));
123
+ return paragraph;
124
+ }
125
+ function getSerializedText(node) {
126
+ if (!node || typeof node !== "object") return "";
127
+ if ("text" in node && typeof node.text === "string") return node.text;
128
+ if ("children" in node && Array.isArray(node.children)) return node.children.map((child) => getSerializedText(child)).join("");
129
+ return "";
130
+ }
131
+ function shouldPrependTitle(serializedNode) {
132
+ const title = serializedNode.title?.trim();
133
+ if (!title) return false;
134
+ const firstChild = (Array.isArray(serializedNode.children) ? serializedNode.children : [])[0];
135
+ return getSerializedText(firstChild).trim() !== title;
136
+ }
137
+ //#endregion
138
+ export { $createCollapsibleNode, $isCollapsibleNode, CollapsibleNode };
@@ -0,0 +1,11 @@
1
+ import { IEditorPluginConstructor } from "../../../types/kernel.js";
2
+ import "../../../types/index.js";
3
+ //#region src/plugins/collapsible/plugin/index.d.ts
4
+ interface CollapsiblePluginOptions {
5
+ theme?: {
6
+ collapsible?: string;
7
+ };
8
+ }
9
+ declare const CollapsiblePlugin: IEditorPluginConstructor<CollapsiblePluginOptions>;
10
+ //#endregion
11
+ export { CollapsiblePlugin, CollapsiblePluginOptions };