@lobehub/editor 1.21.3 → 1.22.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.
@@ -1,3 +1,4 @@
1
+ import { HistoryState } from '@lexical/history';
1
2
  import EventEmitter from 'eventemitter3';
2
3
  import { CommandListener, CommandListenerPriority, CommandPayloadType, DecoratorNode, LexicalCommand, LexicalEditor, LexicalNodeConfig } from 'lexical';
3
4
  import { HotkeyId } from "../types/hotkey";
@@ -17,8 +18,10 @@ export declare class Kernel extends EventEmitter implements IEditorKernel {
17
18
  private localeMap;
18
19
  private hotReloadMode;
19
20
  private logger;
21
+ private historyState;
20
22
  private editor?;
21
23
  constructor();
24
+ getHistoryState(): HistoryState;
22
25
  private detectDevelopmentMode;
23
26
  /**
24
27
  * Globally enable or disable hot reload mode for all kernel instances
@@ -19,6 +19,7 @@ function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.g
19
19
  function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
20
20
  function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); }
21
21
  function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
22
+ import { createEmptyHistoryState } from '@lexical/history';
22
23
  import EventEmitter from 'eventemitter3';
23
24
  import { $getSelection, $isRangeSelection, COMMAND_PRIORITY_CRITICAL, KEY_DOWN_COMMAND, createEditor } from 'lexical';
24
25
  import { get, merge, template, templateSettings } from 'lodash-es';
@@ -48,6 +49,7 @@ export var Kernel = /*#__PURE__*/function (_EventEmitter) {
48
49
  _defineProperty(_assertThisInitialized(_this), "localeMap", defaultLocale);
49
50
  _defineProperty(_assertThisInitialized(_this), "hotReloadMode", false);
50
51
  _defineProperty(_assertThisInitialized(_this), "logger", createDebugLogger('kernel'));
52
+ _defineProperty(_assertThisInitialized(_this), "historyState", createEmptyHistoryState());
51
53
  _defineProperty(_assertThisInitialized(_this), "editor", void 0);
52
54
  _defineProperty(_assertThisInitialized(_this), "_commands", new Map());
53
55
  _defineProperty(_assertThisInitialized(_this), "_commandsClean", new Map());
@@ -58,6 +60,11 @@ export var Kernel = /*#__PURE__*/function (_EventEmitter) {
58
60
  return _this;
59
61
  }
60
62
  _createClass(Kernel, [{
63
+ key: "getHistoryState",
64
+ value: function getHistoryState() {
65
+ return this.historyState;
66
+ }
67
+ }, {
61
68
  key: "detectDevelopmentMode",
62
69
  value: function detectDevelopmentMode() {
63
70
  var _process$env;
@@ -22,7 +22,7 @@ function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key i
22
22
  function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); }
23
23
  function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
24
24
  import { registerDragonSupport } from '@lexical/dragon';
25
- import { createEmptyHistoryState, registerHistory } from '@lexical/history';
25
+ import { registerHistory } from '@lexical/history';
26
26
  import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode, HeadingNode, QuoteNode, registerRichText } from '@lexical/rich-text';
27
27
  import { $createLineBreakNode, $createParagraphNode, $getSelection, $isRangeSelection, $isTextNode, COMMAND_PRIORITY_HIGH, INSERT_LINE_BREAK_COMMAND, INSERT_PARAGRAPH_COMMAND } from 'lexical';
28
28
  import { KernelPlugin } from "../../../editor-kernel/plugin";
@@ -322,7 +322,7 @@ export var CommonPlugin = (_class = /*#__PURE__*/function (_KernelPlugin) {
322
322
  key: "onInit",
323
323
  value: function onInit(editor) {
324
324
  var _this$config2;
325
- this.registerClears(registerRichText(editor), registerDragonSupport(editor), registerHistory(editor, createEmptyHistoryState(), 300), registerHeaderBackspace(editor), registerRichKeydown(editor, this.kernel, {
325
+ this.registerClears(registerRichText(editor), registerDragonSupport(editor), registerHistory(editor, this.kernel.getHistoryState(), 300), registerHeaderBackspace(editor), registerRichKeydown(editor, this.kernel, {
326
326
  enableHotkey: (_this$config2 = this.config) === null || _this$config2 === void 0 ? void 0 : _this$config2.enableHotkey
327
327
  }), registerCommands(editor), registerBreakLineClick(editor), registerCursorNode(editor), registerLastElement(editor),
328
328
  // Convert soft line breaks (Shift+Enter) to hard line breaks (paragraph breaks)
@@ -47,7 +47,8 @@ var ReactPlainText = /*#__PURE__*/memo(function (_ref) {
47
47
  onPressEnter = _ref.onPressEnter,
48
48
  onCompositionStart = _ref.onCompositionStart,
49
49
  onCompositionEnd = _ref.onCompositionEnd,
50
- onContextMenu = _ref.onContextMenu;
50
+ onContextMenu = _ref.onContextMenu,
51
+ onTextChange = _ref.onTextChange;
51
52
  var isChat = variant === 'chat';
52
53
  var _theme$fontSize = theme.fontSize,
53
54
  fontSize = _theme$fontSize === void 0 ? isChat ? 14 : 16 : _theme$fontSize,
@@ -103,10 +104,29 @@ var ReactPlainText = /*#__PURE__*/memo(function (_ref) {
103
104
  editor.setDocument(type, content);
104
105
  setIsInitialized(true);
105
106
  }
106
- return (_editor$getLexicalEdi = editor.getLexicalEditor()) === null || _editor$getLexicalEdi === void 0 ? void 0 : _editor$getLexicalEdi.registerUpdateListener(function () {
107
+
108
+ // Track previous content for onTextChange comparison
109
+ var previousContent;
110
+ return (_editor$getLexicalEdi = editor.getLexicalEditor()) === null || _editor$getLexicalEdi === void 0 ? void 0 : _editor$getLexicalEdi.registerUpdateListener(function (_ref2) {
111
+ var dirtyElements = _ref2.dirtyElements,
112
+ dirtyLeaves = _ref2.dirtyLeaves;
113
+ // Always trigger onChange for any update
107
114
  onChange === null || onChange === void 0 || onChange(editor);
115
+
116
+ // Only trigger onTextChange when content actually changes
117
+ if (onTextChange && (dirtyElements.size > 0 || dirtyLeaves.size > 0)) {
118
+ var currentContent = JSON.stringify(editor.getDocument(type));
119
+ if (previousContent === undefined) {
120
+ // First update after initialization
121
+ previousContent = currentContent;
122
+ } else if (currentContent !== previousContent) {
123
+ // Content has actually changed
124
+ previousContent = currentContent;
125
+ onTextChange(editor);
126
+ }
127
+ }
108
128
  });
109
- }, [editor, type, content, onChange, isInitialized]);
129
+ }, [editor, type, content, onChange, onTextChange, isInitialized]);
110
130
  useEffect(function () {
111
131
  if (editor && onPressEnter) {
112
132
  return editor.registerHighCommand(KEY_DOWN_COMMAND, function (event) {
@@ -50,6 +50,11 @@ export interface ReactPlainTextProps {
50
50
  editor: IEditor;
51
51
  event: KeyboardEvent;
52
52
  }) => boolean | void;
53
+ /**
54
+ * Callback triggered only when text content changes
55
+ * Unlike onChange, this won't trigger on cursor movement or selection changes
56
+ */
57
+ onTextChange?: (editor: IEditor) => void;
53
58
  style?: CSSProperties;
54
59
  theme?: CommonPluginOptions['theme'] & {
55
60
  fontSize?: number;
@@ -1,6 +1,8 @@
1
+ import { HistoryState, HistoryStateEntry } from '@lexical/history';
1
2
  import { LexicalEditor } from 'lexical';
2
3
  import { MarkdownShortCutService } from '../service/shortcut';
3
4
  export declare const INSERT_MARKDOWN_COMMAND: import("lexical").LexicalCommand<{
5
+ historyState: HistoryStateEntry | null;
4
6
  markdown: string;
5
7
  }>;
6
- export declare function registerMarkdownCommand(editor: LexicalEditor, service: MarkdownShortCutService): () => void;
8
+ export declare function registerMarkdownCommand(editor: LexicalEditor, service: MarkdownShortCutService, history: HistoryState): () => void;
@@ -1,25 +1,43 @@
1
- import { $getSelection, COMMAND_PRIORITY_HIGH, createCommand } from 'lexical';
1
+ import { $getSelection, CAN_UNDO_COMMAND, COMMAND_PRIORITY_HIGH, HISTORIC_TAG, createCommand } from 'lexical';
2
2
  import { createDebugLogger } from "../../../utils/debug";
3
3
  import { parseMarkdownToLexical } from "../data-source/markdown/parse";
4
4
  import { $generateNodesFromSerializedNodes, $insertGeneratedNodes } from "../utils";
5
5
  var logger = createDebugLogger('plugin', 'markdown');
6
6
  export var INSERT_MARKDOWN_COMMAND = createCommand('INSERT_MARKDOWN_COMMAND');
7
- export function registerMarkdownCommand(editor, service) {
7
+ function undoToEntry(editor, historyState, entry) {
8
+ var undoStack = historyState.undoStack;
9
+ var current = historyState.current;
10
+ if (current) {
11
+ undoStack.push(current);
12
+ editor.dispatchCommand(CAN_UNDO_COMMAND, false);
13
+ }
14
+ historyState.current = entry || null;
15
+ if (entry) {
16
+ editor.setEditorState(entry.editorState, {
17
+ tag: HISTORIC_TAG
18
+ });
19
+ }
20
+ return editor.getEditorState();
21
+ }
22
+ export function registerMarkdownCommand(editor, service, history) {
8
23
  return editor.registerCommand(INSERT_MARKDOWN_COMMAND, function (payload) {
9
24
  var markdown = payload.markdown;
10
25
  logger.debug('INSERT_MARKDOWN_COMMAND payload:', payload);
11
- editor.update(function () {
12
- try {
13
- // Use the markdown data source to parse the content
14
- var root = parseMarkdownToLexical(markdown, service.markdownReaders);
15
- var selection = $getSelection();
16
- var nodes = $generateNodesFromSerializedNodes(root.children);
17
- logger.debug('INSERT_MARKDOWN_COMMAND nodes:', nodes);
18
- $insertGeneratedNodes(editor, nodes, selection);
19
- return true;
20
- } catch (error) {
21
- logger.error('Failed to handle markdown paste:', error);
22
- }
26
+ undoToEntry(editor, history, payload.historyState);
27
+ queueMicrotask(function () {
28
+ editor.update(function () {
29
+ try {
30
+ // Use the markdown data source to parse the content
31
+ var root = parseMarkdownToLexical(markdown, service.markdownReaders);
32
+ var selection = $getSelection();
33
+ var nodes = $generateNodesFromSerializedNodes(root.children);
34
+ logger.debug('INSERT_MARKDOWN_COMMAND nodes:', nodes);
35
+ $insertGeneratedNodes(editor, nodes, selection);
36
+ return true;
37
+ } catch (error) {
38
+ logger.error('Failed to handle markdown paste:', error);
39
+ }
40
+ });
23
41
  });
24
42
  return false;
25
43
  }, COMMAND_PRIORITY_HIGH // Priority
@@ -144,9 +144,11 @@ export var MarkdownPlugin = (_class = /*#__PURE__*/function (_KernelPlugin) {
144
144
  if (hasMarkdownContent) {
145
145
  // Markdown detected - show confirmation dialog
146
146
  _this2.logger.debug('markdown patterns detected:', _this2.getMarkdownPatterns(text));
147
+ var historyState = _this2.kernel.getHistoryState().current;
147
148
  setTimeout(function () {
148
149
  _this2.kernel.emit('markdownParse', {
149
150
  cacheState: editor.getEditorState(),
151
+ historyState: historyState,
150
152
  markdown: text
151
153
  });
152
154
  }, 10);
@@ -156,7 +158,7 @@ export var MarkdownPlugin = (_class = /*#__PURE__*/function (_KernelPlugin) {
156
158
  }
157
159
  return false;
158
160
  }, COMMAND_PRIORITY_CRITICAL));
159
- this.register(registerMarkdownCommand(editor, this.service));
161
+ this.register(registerMarkdownCommand(editor, this.service, this.kernel.getHistoryState()));
160
162
  }
161
163
 
162
164
  /**
@@ -8,7 +8,6 @@ function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" !=
8
8
  function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
9
9
  import { Button } from '@lobehub/ui';
10
10
  import { Space, notification } from 'antd';
11
- import { UNDO_COMMAND } from 'lexical';
12
11
  import { useLayoutEffect } from 'react';
13
12
  import { useLexicalComposerContext } from "../../../editor-kernel/react";
14
13
  import { useTranslation } from "../../../editor-kernel/react/useTranslation";
@@ -28,7 +27,8 @@ var ReactMarkdownPlugin = function ReactMarkdownPlugin() {
28
27
  useLayoutEffect(function () {
29
28
  editor.registerPlugin(MarkdownPlugin);
30
29
  var handleEvent = function handleEvent(_ref) {
31
- var markdown = _ref.markdown;
30
+ var markdown = _ref.markdown,
31
+ historyState = _ref.historyState;
32
32
  var key = "open".concat(Date.now());
33
33
  var actions = /*#__PURE__*/_jsxs(Space, {
34
34
  children: [/*#__PURE__*/_jsx(Button, {
@@ -39,8 +39,8 @@ var ReactMarkdownPlugin = function ReactMarkdownPlugin() {
39
39
  children: t('markdown.cancel')
40
40
  }), /*#__PURE__*/_jsx(Button, {
41
41
  onClick: function onClick() {
42
- editor.dispatchCommand(UNDO_COMMAND, undefined);
43
42
  editor.dispatchCommand(INSERT_MARKDOWN_COMMAND, {
43
+ historyState: historyState,
44
44
  markdown: markdown
45
45
  });
46
46
  api.destroy();
@@ -53,7 +53,8 @@ var Editor = /*#__PURE__*/memo(function (_ref) {
53
53
  markdownOption = _ref$markdownOption === void 0 ? true : _ref$markdownOption,
54
54
  onCompositionStart = _ref.onCompositionStart,
55
55
  onCompositionEnd = _ref.onCompositionEnd,
56
- onContextMenu = _ref.onContextMenu;
56
+ onContextMenu = _ref.onContextMenu,
57
+ onTextChange = _ref.onTextChange;
57
58
  var _useEditorContent = useEditorContent(),
58
59
  config = _useEditorContent.config;
59
60
  var enableSlash = Boolean((slashOption === null || slashOption === void 0 ? void 0 : slashOption.items) && slashOption.items.length > 0);
@@ -107,6 +108,7 @@ var Editor = /*#__PURE__*/memo(function (_ref) {
107
108
  onFocus: onFocus,
108
109
  onKeyDown: onKeyDown,
109
110
  onPressEnter: onPressEnter,
111
+ onTextChange: onTextChange,
110
112
  style: style,
111
113
  variant: variant,
112
114
  children: /*#__PURE__*/_jsx(ReactEditorContent, {
@@ -29,6 +29,11 @@ export interface EditorProps extends Partial<ReactEditorContentProps>, Omit<Reac
29
29
  };
30
30
  mentionOption?: MentionOption;
31
31
  onInit?: (editor: IEditor) => void;
32
+ /**
33
+ * Callback triggered only when text content changes
34
+ * Unlike onChange, this won't trigger on cursor movement or selection changes
35
+ */
36
+ onTextChange?: (editor: IEditor) => void;
32
37
  plugins?: EditorPlugin[];
33
38
  slashOption?: Partial<ReactSlashOptionProps>;
34
39
  style?: CSSProperties;
@@ -1,8 +1,9 @@
1
+ import type { HistoryState, HistoryStateEntry } from '@lexical/history';
1
2
  import type { CommandListener, CommandListenerPriority, CommandPayloadType, DecoratorNode, EditorState, LexicalCommand, LexicalEditor, LexicalNodeConfig } from 'lexical';
2
3
  import type DataSource from "../editor-kernel/data-source";
3
- import { HotkeyId } from "./hotkey";
4
- import { HotkeyOptions, HotkeysEvent } from "../utils/hotkey/registerHotkey";
5
- import { ILocaleKeys } from './locale';
4
+ import type { HotkeyId } from "./hotkey";
5
+ import type { HotkeyOptions, HotkeysEvent } from "../utils/hotkey/registerHotkey";
6
+ import type { ILocaleKeys } from './locale';
6
7
  export type Commands = Map<LexicalCommand<unknown>, Array<Set<CommandListener<unknown>>>>;
7
8
  export type CommandsClean = Map<LexicalCommand<unknown>, () => void>;
8
9
  /**
@@ -28,6 +29,7 @@ export interface IKernelEventMap {
28
29
  */
29
30
  markdownParse: (params: {
30
31
  cacheState: EditorState;
32
+ historyState: HistoryStateEntry | null;
31
33
  markdown: string;
32
34
  }) => void;
33
35
  }
@@ -188,6 +190,10 @@ export interface IEditorKernel extends IEditor {
188
190
  * @param name
189
191
  */
190
192
  getDecorator(name: string): ((_node: DecoratorNode<any>, _editor: LexicalEditor) => any) | undefined;
193
+ /**
194
+ * Get editor history state
195
+ */
196
+ getHistoryState(): HistoryState;
191
197
  /**
192
198
  * Get all registered decorator names
193
199
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/editor",
3
- "version": "1.21.3",
3
+ "version": "1.22.1",
4
4
  "description": "A powerful and extensible rich text editor built on Meta's Lexical framework, providing a modern editing experience with React integration.",
5
5
  "keywords": [
6
6
  "lobehub",
@@ -27,38 +27,38 @@
27
27
  "react.js"
28
28
  ],
29
29
  "dependencies": {
30
- "@floating-ui/dom": "^1.7.3",
31
- "@floating-ui/react": "^0.27.15",
32
- "@lexical/code": "^0.34.0",
33
- "@lexical/code-shiki": "^0.34.0",
34
- "@lexical/dragon": "^0.34.0",
35
- "@lexical/history": "^0.34.0",
36
- "@lexical/link": "^0.34.0",
37
- "@lexical/list": "^0.34.0",
38
- "@lexical/rich-text": "^0.34.0",
39
- "@lexical/selection": "^0.34.0",
40
- "@lexical/table": "^0.34.0",
41
- "@lexical/utils": "^0.34.0",
42
- "@shikijs/core": "^3.9.2",
43
- "@shikijs/engine-javascript": "^3.9.2",
44
- "ahooks": "^3.9.3",
30
+ "@floating-ui/dom": "^1.7.4",
31
+ "@floating-ui/react": "^0.27.16",
32
+ "@lexical/code": "^0.38.2",
33
+ "@lexical/code-shiki": "^0.38.2",
34
+ "@lexical/dragon": "^0.38.2",
35
+ "@lexical/history": "^0.38.2",
36
+ "@lexical/link": "^0.38.2",
37
+ "@lexical/list": "^0.38.2",
38
+ "@lexical/rich-text": "^0.38.2",
39
+ "@lexical/selection": "^0.38.2",
40
+ "@lexical/table": "^0.38.2",
41
+ "@lexical/utils": "^0.38.2",
42
+ "@shikijs/core": "^3.15.0",
43
+ "@shikijs/engine-javascript": "^3.15.0",
44
+ "ahooks": "^3.9.6",
45
45
  "antd-style": "^3.7.1",
46
- "debug": "^4.4.1",
46
+ "debug": "^4.4.3",
47
47
  "eventemitter3": "^5.0.1",
48
- "framer-motion": "^12.23.12",
48
+ "framer-motion": "^12.23.24",
49
49
  "fuse.js": "^7.1.0",
50
- "katex": "^0.16.22",
51
- "lexical": "^0.34.0",
50
+ "katex": "^0.16.25",
51
+ "lexical": "^0.38.2",
52
52
  "lodash-es": "^4.17.21",
53
- "lucide-react": "^0.536.0",
53
+ "lucide-react": "^0.553.0",
54
54
  "polished": "^4.3.1",
55
55
  "re-resizable": "^6.11.2",
56
56
  "react-error-boundary": "^6.0.0",
57
57
  "react-layout-kit": "^2.0.0",
58
58
  "react-merge-refs": "^3.0.2",
59
- "remark-cjk-friendly": "^1.2.1",
59
+ "remark-cjk-friendly": "^1.2.3",
60
60
  "remark-supersub": "^1.0.0",
61
- "shiki": "^3.9.2",
61
+ "shiki": "^3.15.0",
62
62
  "ts-key-enum": "^3.0.13",
63
63
  "use-merge-value": "^1.2.0"
64
64
  },