This is an automated email from the ASF dual-hosted git repository.

robin0716 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/answer-plugins.git


The following commit(s) were added to refs/heads/main by this push:
     new ba8a24d  feat(editor-stacks): add onChange plugin for content 
monitoring and update imports to single quotes
ba8a24d is described below

commit ba8a24dd56845af88d9ed19d5914eeb26b197d8a
Author: robin <[email protected]>
AuthorDate: Thu Dec 25 16:37:36 2025 +0800

    feat(editor-stacks): add onChange plugin for content monitoring and update 
imports to single quotes
---
 editor-stacks/Component.tsx      | 82 ++++++++++++++++++----------------------
 editor-stacks/onChange-plugin.ts | 68 +++++++++++++++++++++++++++++++++
 editor-stacks/package.json       |  9 +++--
 editor-stacks/pnpm-lock.yaml     |  2 +
 4 files changed, 112 insertions(+), 49 deletions(-)

diff --git a/editor-stacks/Component.tsx b/editor-stacks/Component.tsx
index c0dae77..a870fbe 100644
--- a/editor-stacks/Component.tsx
+++ b/editor-stacks/Component.tsx
@@ -17,15 +17,16 @@
  * under the License.
  */
 
-import { FC, useCallback, useEffect, useRef } from "react";
-import { useTranslation } from "react-i18next";
+import { FC, useCallback, useEffect, useRef } from 'react';
+import { useTranslation } from 'react-i18next';
 
-import { StacksEditor } from "@stackoverflow/stacks-editor";
+import { StacksEditor } from '@stackoverflow/stacks-editor';
 
-import "@stackoverflow/stacks";
-import "@stackoverflow/stacks/dist/css/stacks.css";
+import '@stackoverflow/stacks';
+import '@stackoverflow/stacks/dist/css/stacks.css';
 
-import "@stackoverflow/stacks-editor/dist/styles.css";
+import '@stackoverflow/stacks-editor/dist/styles.css';
+import { createOnChangePlugin } from './onChange-plugin';
 
 export interface EditorProps {
   value: string;
@@ -46,13 +47,13 @@ const Component: FC<EditorProps> = ({
   onChange,
   onFocus,
   onBlur,
-  placeholder = "",
+  placeholder = '',
   autoFocus = false,
   imageUploadHandler,
   uploadConfig,
 }) => {
-  const { t } = useTranslation("plugin", {
-    keyPrefix: "editor_stacks.frontend",
+  const { t } = useTranslation('plugin', {
+    keyPrefix: 'editor_stacks.frontend',
   });
   const containerRef = useRef<HTMLDivElement>(null);
   const editorInstanceRef = useRef<StacksEditor | null>(null);
@@ -61,7 +62,7 @@ const Component: FC<EditorProps> = ({
   const onFocusRef = useRef(onFocus);
   const onBlurRef = useRef(onBlur);
   const autoFocusTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
-    null
+    null,
   );
 
   // Version compatibility temporarily disabled
@@ -76,13 +77,13 @@ const Component: FC<EditorProps> = ({
     if (!containerRef.current) return;
 
     containerRef.current?.classList.remove(
-      "theme-light",
-      "theme-dark",
-      "theme-system"
+      'theme-light',
+      'theme-dark',
+      'theme-system',
     );
     const themeAttr =
-      document.documentElement.getAttribute("data-bs-theme") ||
-      document.body.getAttribute("data-bs-theme");
+      document.documentElement.getAttribute('data-bs-theme') ||
+      document.body.getAttribute('data-bs-theme');
 
     if (themeAttr) {
       containerRef.current?.classList.add(`theme-${themeAttr}`);
@@ -99,11 +100,11 @@ const Component: FC<EditorProps> = ({
     });
     observer.observe(document.documentElement, {
       attributes: true,
-      attributeFilter: ["data-bs-theme", "class"],
+      attributeFilter: ['data-bs-theme', 'class'],
     });
     observer.observe(document.body, {
       attributes: true,
-      attributeFilter: ["data-bs-theme", "class"],
+      attributeFilter: ['data-bs-theme', 'class'],
     });
     return () => observer.disconnect();
   }, [syncTheme]);
@@ -116,8 +117,8 @@ const Component: FC<EditorProps> = ({
     let editorInstance: StacksEditor | null = null;
 
     try {
-      editorInstance = new StacksEditor(containerRef.current, value || "", {
-        placeholderText: placeholder || t("placeholder", ""),
+      editorInstance = new StacksEditor(containerRef.current, value || '', {
+        placeholderText: placeholder || t('placeholder', ''),
         parserFeatures: {
           tables: true,
           html: false,
@@ -129,37 +130,26 @@ const Component: FC<EditorProps> = ({
               acceptedFileTypes: uploadConfig?.allowedExtensions,
             }
           : undefined,
+        editorPlugins: onChange
+          ? [
+              createOnChangePlugin((content) => {
+                onChangeRef.current?.(content);
+              }),
+            ]
+          : [],
       });
 
       editorInstanceRef.current = editorInstance;
       isInitializedRef.current = true;
 
       const editor = editorInstance;
-
-      const originalDispatch = editor.editorView.props.dispatchTransaction;
-      editor.editorView.setProps({
-        dispatchTransaction: (tr) => {
-          if (originalDispatch) {
-            originalDispatch.call(editor.editorView, tr);
-          } else {
-            const newState = editor.editorView.state.apply(tr);
-            editor.editorView.updateState(newState);
-          }
-
-          if (tr.docChanged && onChangeRef.current) {
-            const newContent = editor.content;
-            onChangeRef.current(newContent);
-          }
-        },
-      });
-
       const editorElement = editor.dom as HTMLElement;
       const handleFocus = () => onFocusRef.current?.();
       const handleBlur = () => onBlurRef.current?.();
 
       if (editorElement) {
-        editorElement.addEventListener("focus", handleFocus, true);
-        editorElement.addEventListener("blur", handleBlur, true);
+        editorElement.addEventListener('focus', handleFocus, true);
+        editorElement.addEventListener('blur', handleBlur, true);
       }
 
       if (autoFocus) {
@@ -177,25 +167,27 @@ const Component: FC<EditorProps> = ({
         }
 
         if (editorElement) {
-          editorElement.removeEventListener("focus", handleFocus, true);
-          editorElement.removeEventListener("blur", handleBlur, true);
+          editorElement.removeEventListener('focus', handleFocus, true);
+          editorElement.removeEventListener('blur', handleBlur, true);
         }
 
         if (editorInstance) {
           try {
             editorInstance.destroy();
           } catch (e) {
-            console.error("Error destroying editor:", e);
+            console.error('Error destroying editor:', e);
           }
         }
 
         editorInstanceRef.current = null;
         isInitializedRef.current = false;
 
-        containerRef.current!.innerHTML = "";
+        if (containerRef.current) {
+          containerRef.current.innerHTML = '';
+        }
       };
     } catch (error) {
-      console.error("Failed to initialize Stacks Editor:", error);
+      console.error('Failed to initialize Stacks Editor:', error);
       isInitializedRef.current = false;
     }
   }, []);
@@ -211,7 +203,7 @@ const Component: FC<EditorProps> = ({
         editor.content = value;
       }
     } catch (error) {
-      console.error("Error syncing editor content:", error);
+      console.error('Error syncing editor content:', error);
     }
   }, [value]);
 
diff --git a/editor-stacks/onChange-plugin.ts b/editor-stacks/onChange-plugin.ts
new file mode 100644
index 0000000..5c7d62d
--- /dev/null
+++ b/editor-stacks/onChange-plugin.ts
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Plugin } from "prosemirror-state";
+import type { EditorPlugin } from "@stackoverflow/stacks-editor";
+
+/**
+ * Creates a StacksEditor plugin that listens to content changes.
+ * This is the official recommended approach for monitoring editor updates.
+ * Reference: 
https://discuss.prosemirror.net/t/how-to-get-data-from-the-editor/3263/5
+ *
+ * Works in both RichText and Markdown modes automatically.
+ *
+ * @param onUpdate Callback function that receives the updated editor content
+ * @returns A StacksEditor EditorPlugin
+ */
+export const createOnChangePlugin = (
+  onUpdate: (content: string) => void
+): EditorPlugin => {
+  return () => {
+    let lastContent = "";
+
+    const proseMirrorPlugin: any = new Plugin({
+      view() {
+        return {
+          update(view) {
+            try {
+              const content = view.state.doc.textContent;
+
+              // Only trigger callback if content actually changed
+              if (content !== lastContent) {
+                lastContent = content;
+                onUpdate(content);
+              }
+            } catch (error) {
+              console.error("Error getting editor content:", error);
+            }
+          },
+        };
+      },
+    });
+
+    return {
+      richText: {
+        plugins: [proseMirrorPlugin],
+      },
+      commonmark: {
+        plugins: [proseMirrorPlugin],
+      },
+    };
+  };
+};
diff --git a/editor-stacks/package.json b/editor-stacks/package.json
index f678091..7e93f51 100644
--- a/editor-stacks/package.json
+++ b/editor-stacks/package.json
@@ -25,7 +25,8 @@
     "react": "^18.2.0",
     "react-bootstrap": "^2.10.0",
     "react-dom": "^18.2.0",
-    "react-i18next": "^11.18.3"
+    "react-i18next": "^11.18.3",
+    "prosemirror-state": "*"
   },
   "devDependencies": {
     "@modyfi/vite-plugin-yaml": "^1.1.0",
@@ -35,12 +36,12 @@
     "eslint": "^8.45.0",
     "eslint-plugin-react-hooks": "^4.6.0",
     "eslint-plugin-react-refresh": "^0.4.3",
+    "postcss": "^8.4.47",
+    "postcss-prefix-selector": "^1.16.0",
     "typescript": "^5.0.2",
     "vite": "^4.4.5",
     "vite-plugin-css-injected-by-js": "^3.5.2",
-    "vite-plugin-dts": "^3.9.1",
-    "postcss": "^8.4.47",
-    "postcss-prefix-selector": "^1.16.0"
+    "vite-plugin-dts": "^3.9.1"
   },
   "dependencies": {
     "@stackoverflow/stacks": "^2.8.6",
diff --git a/editor-stacks/pnpm-lock.yaml b/editor-stacks/pnpm-lock.yaml
index 722c885..755680c 100644
--- a/editor-stacks/pnpm-lock.yaml
+++ b/editor-stacks/pnpm-lock.yaml
@@ -12,6 +12,7 @@ specifiers:
   eslint-plugin-react-refresh: ^0.4.3
   postcss: ^8.4.47
   postcss-prefix-selector: ^1.16.0
+  prosemirror-state: '*'
   react: ^18.2.0
   react-bootstrap: ^2.10.0
   react-dom: ^18.2.0
@@ -24,6 +25,7 @@ specifiers:
 dependencies:
   '@stackoverflow/stacks': 2.8.6
   '@stackoverflow/stacks-editor': 0.15.3_aqsvt4jfyhkeiaesbjpxbpahfa
+  prosemirror-state: 1.4.4
   react: 18.3.1
   react-bootstrap: 2.10.10_ofeg3paogq7iqgpcoog7ecshju
   react-dom: [email protected]

Reply via email to