Skip to content

Commit ba8a24d

Browse files
committed
feat(editor-stacks): add onChange plugin for content monitoring and update imports to single quotes
1 parent 17ee92a commit ba8a24d

File tree

4 files changed

+112
-49
lines changed

4 files changed

+112
-49
lines changed

editor-stacks/Component.tsx

Lines changed: 37 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,16 @@
1717
* under the License.
1818
*/
1919

20-
import { FC, useCallback, useEffect, useRef } from "react";
21-
import { useTranslation } from "react-i18next";
20+
import { FC, useCallback, useEffect, useRef } from 'react';
21+
import { useTranslation } from 'react-i18next';
2222

23-
import { StacksEditor } from "@stackoverflow/stacks-editor";
23+
import { StacksEditor } from '@stackoverflow/stacks-editor';
2424

25-
import "@stackoverflow/stacks";
26-
import "@stackoverflow/stacks/dist/css/stacks.css";
25+
import '@stackoverflow/stacks';
26+
import '@stackoverflow/stacks/dist/css/stacks.css';
2727

28-
import "@stackoverflow/stacks-editor/dist/styles.css";
28+
import '@stackoverflow/stacks-editor/dist/styles.css';
29+
import { createOnChangePlugin } from './onChange-plugin';
2930

3031
export interface EditorProps {
3132
value: string;
@@ -46,13 +47,13 @@ const Component: FC<EditorProps> = ({
4647
onChange,
4748
onFocus,
4849
onBlur,
49-
placeholder = "",
50+
placeholder = '',
5051
autoFocus = false,
5152
imageUploadHandler,
5253
uploadConfig,
5354
}) => {
54-
const { t } = useTranslation("plugin", {
55-
keyPrefix: "editor_stacks.frontend",
55+
const { t } = useTranslation('plugin', {
56+
keyPrefix: 'editor_stacks.frontend',
5657
});
5758
const containerRef = useRef<HTMLDivElement>(null);
5859
const editorInstanceRef = useRef<StacksEditor | null>(null);
@@ -61,7 +62,7 @@ const Component: FC<EditorProps> = ({
6162
const onFocusRef = useRef(onFocus);
6263
const onBlurRef = useRef(onBlur);
6364
const autoFocusTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
64-
null
65+
null,
6566
);
6667

6768
// Version compatibility temporarily disabled
@@ -76,13 +77,13 @@ const Component: FC<EditorProps> = ({
7677
if (!containerRef.current) return;
7778

7879
containerRef.current?.classList.remove(
79-
"theme-light",
80-
"theme-dark",
81-
"theme-system"
80+
'theme-light',
81+
'theme-dark',
82+
'theme-system',
8283
);
8384
const themeAttr =
84-
document.documentElement.getAttribute("data-bs-theme") ||
85-
document.body.getAttribute("data-bs-theme");
85+
document.documentElement.getAttribute('data-bs-theme') ||
86+
document.body.getAttribute('data-bs-theme');
8687

8788
if (themeAttr) {
8889
containerRef.current?.classList.add(`theme-${themeAttr}`);
@@ -99,11 +100,11 @@ const Component: FC<EditorProps> = ({
99100
});
100101
observer.observe(document.documentElement, {
101102
attributes: true,
102-
attributeFilter: ["data-bs-theme", "class"],
103+
attributeFilter: ['data-bs-theme', 'class'],
103104
});
104105
observer.observe(document.body, {
105106
attributes: true,
106-
attributeFilter: ["data-bs-theme", "class"],
107+
attributeFilter: ['data-bs-theme', 'class'],
107108
});
108109
return () => observer.disconnect();
109110
}, [syncTheme]);
@@ -116,8 +117,8 @@ const Component: FC<EditorProps> = ({
116117
let editorInstance: StacksEditor | null = null;
117118

118119
try {
119-
editorInstance = new StacksEditor(containerRef.current, value || "", {
120-
placeholderText: placeholder || t("placeholder", ""),
120+
editorInstance = new StacksEditor(containerRef.current, value || '', {
121+
placeholderText: placeholder || t('placeholder', ''),
121122
parserFeatures: {
122123
tables: true,
123124
html: false,
@@ -129,37 +130,26 @@ const Component: FC<EditorProps> = ({
129130
acceptedFileTypes: uploadConfig?.allowedExtensions,
130131
}
131132
: undefined,
133+
editorPlugins: onChange
134+
? [
135+
createOnChangePlugin((content) => {
136+
onChangeRef.current?.(content);
137+
}),
138+
]
139+
: [],
132140
});
133141

134142
editorInstanceRef.current = editorInstance;
135143
isInitializedRef.current = true;
136144

137145
const editor = editorInstance;
138-
139-
const originalDispatch = editor.editorView.props.dispatchTransaction;
140-
editor.editorView.setProps({
141-
dispatchTransaction: (tr) => {
142-
if (originalDispatch) {
143-
originalDispatch.call(editor.editorView, tr);
144-
} else {
145-
const newState = editor.editorView.state.apply(tr);
146-
editor.editorView.updateState(newState);
147-
}
148-
149-
if (tr.docChanged && onChangeRef.current) {
150-
const newContent = editor.content;
151-
onChangeRef.current(newContent);
152-
}
153-
},
154-
});
155-
156146
const editorElement = editor.dom as HTMLElement;
157147
const handleFocus = () => onFocusRef.current?.();
158148
const handleBlur = () => onBlurRef.current?.();
159149

160150
if (editorElement) {
161-
editorElement.addEventListener("focus", handleFocus, true);
162-
editorElement.addEventListener("blur", handleBlur, true);
151+
editorElement.addEventListener('focus', handleFocus, true);
152+
editorElement.addEventListener('blur', handleBlur, true);
163153
}
164154

165155
if (autoFocus) {
@@ -177,25 +167,27 @@ const Component: FC<EditorProps> = ({
177167
}
178168

179169
if (editorElement) {
180-
editorElement.removeEventListener("focus", handleFocus, true);
181-
editorElement.removeEventListener("blur", handleBlur, true);
170+
editorElement.removeEventListener('focus', handleFocus, true);
171+
editorElement.removeEventListener('blur', handleBlur, true);
182172
}
183173

184174
if (editorInstance) {
185175
try {
186176
editorInstance.destroy();
187177
} catch (e) {
188-
console.error("Error destroying editor:", e);
178+
console.error('Error destroying editor:', e);
189179
}
190180
}
191181

192182
editorInstanceRef.current = null;
193183
isInitializedRef.current = false;
194184

195-
containerRef.current!.innerHTML = "";
185+
if (containerRef.current) {
186+
containerRef.current.innerHTML = '';
187+
}
196188
};
197189
} catch (error) {
198-
console.error("Failed to initialize Stacks Editor:", error);
190+
console.error('Failed to initialize Stacks Editor:', error);
199191
isInitializedRef.current = false;
200192
}
201193
}, []);
@@ -211,7 +203,7 @@ const Component: FC<EditorProps> = ({
211203
editor.content = value;
212204
}
213205
} catch (error) {
214-
console.error("Error syncing editor content:", error);
206+
console.error('Error syncing editor content:', error);
215207
}
216208
}, [value]);
217209

editor-stacks/onChange-plugin.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { Plugin } from "prosemirror-state";
21+
import type { EditorPlugin } from "@stackoverflow/stacks-editor";
22+
23+
/**
24+
* Creates a StacksEditor plugin that listens to content changes.
25+
* This is the official recommended approach for monitoring editor updates.
26+
* Reference: https://discuss.prosemirror.net/t/how-to-get-data-from-the-editor/3263/5
27+
*
28+
* Works in both RichText and Markdown modes automatically.
29+
*
30+
* @param onUpdate Callback function that receives the updated editor content
31+
* @returns A StacksEditor EditorPlugin
32+
*/
33+
export const createOnChangePlugin = (
34+
onUpdate: (content: string) => void
35+
): EditorPlugin => {
36+
return () => {
37+
let lastContent = "";
38+
39+
const proseMirrorPlugin: any = new Plugin({
40+
view() {
41+
return {
42+
update(view) {
43+
try {
44+
const content = view.state.doc.textContent;
45+
46+
// Only trigger callback if content actually changed
47+
if (content !== lastContent) {
48+
lastContent = content;
49+
onUpdate(content);
50+
}
51+
} catch (error) {
52+
console.error("Error getting editor content:", error);
53+
}
54+
},
55+
};
56+
},
57+
});
58+
59+
return {
60+
richText: {
61+
plugins: [proseMirrorPlugin],
62+
},
63+
commonmark: {
64+
plugins: [proseMirrorPlugin],
65+
},
66+
};
67+
};
68+
};

editor-stacks/package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
"react": "^18.2.0",
2626
"react-bootstrap": "^2.10.0",
2727
"react-dom": "^18.2.0",
28-
"react-i18next": "^11.18.3"
28+
"react-i18next": "^11.18.3",
29+
"prosemirror-state": "*"
2930
},
3031
"devDependencies": {
3132
"@modyfi/vite-plugin-yaml": "^1.1.0",
@@ -35,12 +36,12 @@
3536
"eslint": "^8.45.0",
3637
"eslint-plugin-react-hooks": "^4.6.0",
3738
"eslint-plugin-react-refresh": "^0.4.3",
39+
"postcss": "^8.4.47",
40+
"postcss-prefix-selector": "^1.16.0",
3841
"typescript": "^5.0.2",
3942
"vite": "^4.4.5",
4043
"vite-plugin-css-injected-by-js": "^3.5.2",
41-
"vite-plugin-dts": "^3.9.1",
42-
"postcss": "^8.4.47",
43-
"postcss-prefix-selector": "^1.16.0"
44+
"vite-plugin-dts": "^3.9.1"
4445
},
4546
"dependencies": {
4647
"@stackoverflow/stacks": "^2.8.6",

editor-stacks/pnpm-lock.yaml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)