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]