hocmai-feedback-widget 0.2.2 → 0.2.3
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.
- package/README.md +60 -130
- package/dist/components/FeedbackWidget.d.ts +2 -1
- package/dist/context/FeedbackContext.d.ts +9 -2
- package/dist/hocmai-feedback-widget.mjs +956 -950
- package/dist/hocmai-feedback-widget.mjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/package.json +10 -4
- package/src/App.tsx +5 -1
- package/src/capture/index.ts +15 -16
- package/src/components/FeedbackWidget.tsx +12 -6
- package/src/context/FeedbackContext.tsx +38 -5
- package/src/index.ts +1 -0
- package/src/main.tsx +5 -6
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ export { default as FeedbackWidget } from './components/FeedbackWidget';
|
|
|
2
2
|
export type { FeedbackData, FeedbackProps } from './components/FeedbackWidget';
|
|
3
3
|
export type { CkEditorProps } from './components/CkEditor';
|
|
4
4
|
export { FeedbackProvider, useFeedback } from './context/FeedbackContext';
|
|
5
|
+
export type { FeedbackSubmitHandler } from './context/FeedbackContext';
|
|
5
6
|
export { captureScreenshot, captureScreenshotWithOptions, prewarmCapture, } from './capture';
|
|
6
7
|
export type { Rect, CaptureProgress, CaptureOptions } from './capture/types';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hocmai-feedback-widget",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Feedback/report widget with MathJax-aware screenshot capture (snapdom + embedded fonts).",
|
|
7
7
|
"module": "./dist/hocmai-feedback-widget.mjs",
|
|
@@ -41,9 +41,15 @@
|
|
|
41
41
|
"ckeditor5-classic-with-mathtype": "*"
|
|
42
42
|
},
|
|
43
43
|
"peerDependenciesMeta": {
|
|
44
|
-
"@ckeditor/ckeditor5-react": {
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
"@ckeditor/ckeditor5-react": {
|
|
45
|
+
"optional": true
|
|
46
|
+
},
|
|
47
|
+
"hocmai-ckeditor-custom": {
|
|
48
|
+
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"ckeditor5-classic-with-mathtype": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
47
53
|
},
|
|
48
54
|
"devDependencies": {
|
|
49
55
|
"@playwright/test": "^1.48.0",
|
package/src/App.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { FeedbackProvider, useFeedback, captureScreenshot, type FeedbackData } from './index';
|
|
3
|
+
import CkEditor from './components/CkEditor';
|
|
3
4
|
|
|
4
5
|
declare global {
|
|
5
6
|
interface Window {
|
|
@@ -123,7 +124,10 @@ const App: React.FC = () => {
|
|
|
123
124
|
}, []);
|
|
124
125
|
|
|
125
126
|
return (
|
|
126
|
-
<FeedbackProvider
|
|
127
|
+
<FeedbackProvider
|
|
128
|
+
isCkEditor={true}
|
|
129
|
+
descriptionEditor={CkEditor}
|
|
130
|
+
onSubmit={handleSubmit}>
|
|
127
131
|
{/* Mirror host app: normal-flow root + fixed overlay watermark + fixed opaque header */}
|
|
128
132
|
<div style={{ width: '100vw', display: 'flex', flexDirection: 'column' }}>
|
|
129
133
|
<Watermark />
|
package/src/capture/index.ts
CHANGED
|
@@ -185,23 +185,17 @@ function warmOnce(): Promise<void> {
|
|
|
185
185
|
await ensureReadyForCapture();
|
|
186
186
|
await inlineMathJaxFonts();
|
|
187
187
|
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
|
|
188
|
+
// snapdom needs one throwaway toCanvas before it renders CHTML glyphs
|
|
189
|
+
// (even with fonts inlined). It only needs to exercise the CHTML/font path
|
|
190
|
+
// ONCE — not the whole page — so warm with a single small formula element.
|
|
191
|
+
// This is far cheaper than a full-body pass (which on a tall page hits the
|
|
192
|
+
// canvas limit and downscales), keeping form-open light.
|
|
193
|
+
const chtml = Array.from(document.querySelectorAll('mjx-container')).find(
|
|
193
194
|
(c) => !c.querySelector('svg')
|
|
194
|
-
);
|
|
195
|
-
if (!
|
|
196
|
-
const vp = viewportSize();
|
|
197
|
-
const keep: ViewportRect = { top: 0, bottom: vp.h, left: 0, right: vp.w };
|
|
195
|
+
) as HTMLElement | undefined;
|
|
196
|
+
if (!chtml) return; // SVG math or no math → no priming needed
|
|
198
197
|
try {
|
|
199
|
-
await snapdom.toCanvas(
|
|
200
|
-
embedFonts: true,
|
|
201
|
-
dpr: 1,
|
|
202
|
-
scale: 1,
|
|
203
|
-
filter: makePruneFilter(keep, [`[${REPORT_UI_ATTR}]`]),
|
|
204
|
-
});
|
|
198
|
+
await snapdom.toCanvas(chtml, { embedFonts: true, dpr: 1, scale: 1 });
|
|
205
199
|
} catch {
|
|
206
200
|
/* warm errors are non-fatal */
|
|
207
201
|
}
|
|
@@ -309,11 +303,16 @@ export async function captureScreenshotWithOptions(
|
|
|
309
303
|
const caps = getCaptureCapabilities();
|
|
310
304
|
const root = document.body;
|
|
311
305
|
const desired = options.scale ?? caps.recommendedScale;
|
|
306
|
+
// Keep ~5% headroom below the hard canvas limit: snapdom measures the element
|
|
307
|
+
// slightly larger than scrollHeight, and hitting the limit triggers a slow
|
|
308
|
+
// internal downscale. Staying under it avoids that and keeps the crop mapping
|
|
309
|
+
// linear.
|
|
310
|
+
const safeMaxCanvas = Math.floor(caps.maxCanvasSize * 0.95);
|
|
312
311
|
const scale = clampScale(
|
|
313
312
|
desired,
|
|
314
313
|
root.scrollWidth || region.w,
|
|
315
314
|
root.scrollHeight || region.h,
|
|
316
|
-
|
|
315
|
+
safeMaxCanvas
|
|
317
316
|
);
|
|
318
317
|
|
|
319
318
|
// Font inlining (idempotent; normally already done from open-time prewarm so
|
|
@@ -4,7 +4,8 @@ import { captureScreenshot, prewarmCapture, type Rect } from '../capture';
|
|
|
4
4
|
import { MessageSquarePlus, Crop, Monitor, X, Hash, AlignLeft, Loader2, Image as ImageIcon } from 'lucide-react';
|
|
5
5
|
import CropOverlay from './CropOverlay';
|
|
6
6
|
// Type-only import — erased at build time, so the core bundle has zero CKEditor
|
|
7
|
-
// coupling and apps build without the CKEditor deps.
|
|
7
|
+
// coupling and apps build without the CKEditor deps. The editor itself is shipped
|
|
8
|
+
// at the subpath `hocmai-feedback-widget/ckeditor` and injected via descriptionEditor.
|
|
8
9
|
import type { CkEditorProps } from './CkEditor';
|
|
9
10
|
|
|
10
11
|
export interface FeedbackData {
|
|
@@ -20,7 +21,8 @@ export interface FeedbackData {
|
|
|
20
21
|
export interface FeedbackProps {
|
|
21
22
|
questionId: string | number;
|
|
22
23
|
contextData?: Record<string, any>;
|
|
23
|
-
|
|
24
|
+
/** Submit handler. Optional — may be provided per call via openFeedback or at the Provider. */
|
|
25
|
+
onSubmit?: (data: FeedbackData) => Promise<void> | void;
|
|
24
26
|
themeColor?: string;
|
|
25
27
|
triggerComponent?: React.ReactNode;
|
|
26
28
|
isOpen?: boolean;
|
|
@@ -252,7 +254,11 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
|
|
|
252
254
|
contextData
|
|
253
255
|
};
|
|
254
256
|
|
|
255
|
-
|
|
257
|
+
if (onSubmit) {
|
|
258
|
+
await onSubmit(feedbackData);
|
|
259
|
+
} else {
|
|
260
|
+
console.warn('[feedback-widget] Không có onSubmit — bỏ qua gửi. Dữ liệu:', feedbackData);
|
|
261
|
+
}
|
|
256
262
|
handleCloseTrigger();
|
|
257
263
|
} catch (error) {
|
|
258
264
|
console.error('Submit feedback failed', error);
|
|
@@ -575,15 +581,15 @@ const FeedbackWidget: React.FC<FeedbackProps> = ({
|
|
|
575
581
|
</span>
|
|
576
582
|
</div>
|
|
577
583
|
<div style={{ position: 'relative' }}>
|
|
578
|
-
<div style={{ position: 'absolute', left: '10px', top: '8px', pointerEvents: 'none' }}>
|
|
584
|
+
{isCkEditor ? null : <div style={{ position: 'absolute', left: '10px', top: '8px', pointerEvents: 'none' }}>
|
|
579
585
|
<AlignLeft size={12} style={{
|
|
580
586
|
color: focusedField === 'desc' ? themeColor : (errors.description ? '#ef4444' : '#9ca3af'),
|
|
581
587
|
transition: 'color 0.2s'
|
|
582
588
|
}} />
|
|
583
|
-
</div>
|
|
589
|
+
</div>}
|
|
584
590
|
{isCkEditor && DescriptionEditor ? (
|
|
585
591
|
<DescriptionEditor
|
|
586
|
-
placeholder="Mô tả chi tiết..."
|
|
592
|
+
placeholder="Mô tả chi tiết lỗi..."
|
|
587
593
|
contentQuestion={description}
|
|
588
594
|
error={!!errors.description}
|
|
589
595
|
uploadUrl={uploadUrl}
|
|
@@ -2,8 +2,19 @@ import React, { createContext, useContext, useState, type ReactNode } from 'reac
|
|
|
2
2
|
import FeedbackWidget, { type FeedbackData } from '../components/FeedbackWidget';
|
|
3
3
|
import type { CkEditorProps } from '../components/CkEditor';
|
|
4
4
|
|
|
5
|
+
export type FeedbackSubmitHandler = (data: FeedbackData) => Promise<void> | void;
|
|
6
|
+
|
|
5
7
|
interface FeedbackContextType {
|
|
6
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Open the report form.
|
|
10
|
+
* @param onSubmit optional per-call handler; overrides the Provider's onSubmit
|
|
11
|
+
* for this submission. Lets you handle submit from anywhere.
|
|
12
|
+
*/
|
|
13
|
+
openFeedback: (
|
|
14
|
+
questionId: string | number,
|
|
15
|
+
contextData?: any,
|
|
16
|
+
onSubmit?: FeedbackSubmitHandler
|
|
17
|
+
) => void;
|
|
7
18
|
closeFeedback: () => void;
|
|
8
19
|
isOpen: boolean;
|
|
9
20
|
activeQuestionId: string | number | null;
|
|
@@ -21,7 +32,8 @@ export const useFeedback = () => {
|
|
|
21
32
|
|
|
22
33
|
interface FeedbackProviderProps {
|
|
23
34
|
children: ReactNode;
|
|
24
|
-
|
|
35
|
+
/** Default submit handler. Optional — you can instead pass one per call via openFeedback. */
|
|
36
|
+
onSubmit?: FeedbackSubmitHandler;
|
|
25
37
|
themeColor?: string;
|
|
26
38
|
/** Use the rich-text editor for the description instead of a plain textarea. */
|
|
27
39
|
isCkEditor?: boolean;
|
|
@@ -39,10 +51,18 @@ export const FeedbackProvider: React.FC<FeedbackProviderProps> = ({
|
|
|
39
51
|
const [isOpen, setIsOpen] = useState(false);
|
|
40
52
|
const [activeQuestionId, setActiveQuestionId] = useState<string | number | null>(null);
|
|
41
53
|
const [activeContextData, setActiveContextData] = useState<any>(undefined);
|
|
54
|
+
// Per-call submit handler set by openFeedback (stored via updater so React
|
|
55
|
+
// doesn't treat the function as a state initializer).
|
|
56
|
+
const [activeOnSubmit, setActiveOnSubmit] = useState<FeedbackSubmitHandler | undefined>(undefined);
|
|
42
57
|
|
|
43
|
-
const openFeedback = (
|
|
58
|
+
const openFeedback = (
|
|
59
|
+
questionId: string | number,
|
|
60
|
+
contextData?: any,
|
|
61
|
+
perCallOnSubmit?: FeedbackSubmitHandler
|
|
62
|
+
) => {
|
|
44
63
|
setActiveQuestionId(questionId);
|
|
45
64
|
setActiveContextData(contextData);
|
|
65
|
+
setActiveOnSubmit(() => perCallOnSubmit);
|
|
46
66
|
setIsOpen(true);
|
|
47
67
|
};
|
|
48
68
|
|
|
@@ -50,6 +70,19 @@ export const FeedbackProvider: React.FC<FeedbackProviderProps> = ({
|
|
|
50
70
|
setIsOpen(false);
|
|
51
71
|
};
|
|
52
72
|
|
|
73
|
+
// Resolve handler: per-call → Provider → warn (never crash).
|
|
74
|
+
const effectiveOnSubmit: FeedbackSubmitHandler = async (data) => {
|
|
75
|
+
const handler = activeOnSubmit ?? onSubmit;
|
|
76
|
+
if (!handler) {
|
|
77
|
+
console.warn(
|
|
78
|
+
'[feedback-widget] Bỏ qua gửi báo lỗi: chưa cung cấp onSubmit (ở FeedbackProvider hoặc openFeedback). Dữ liệu:',
|
|
79
|
+
data
|
|
80
|
+
);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
await handler(data);
|
|
84
|
+
};
|
|
85
|
+
|
|
53
86
|
return (
|
|
54
87
|
<FeedbackContext.Provider value={{ openFeedback, closeFeedback, isOpen, activeQuestionId }}>
|
|
55
88
|
{children}
|
|
@@ -59,7 +92,7 @@ export const FeedbackProvider: React.FC<FeedbackProviderProps> = ({
|
|
|
59
92
|
<FeedbackWidget
|
|
60
93
|
questionId={activeQuestionId}
|
|
61
94
|
contextData={activeContextData}
|
|
62
|
-
onSubmit={
|
|
95
|
+
onSubmit={effectiveOnSubmit}
|
|
63
96
|
themeColor={themeColor}
|
|
64
97
|
isCkEditor={isCkEditor}
|
|
65
98
|
descriptionEditor={descriptionEditor}
|
|
@@ -70,4 +103,4 @@ export const FeedbackProvider: React.FC<FeedbackProviderProps> = ({
|
|
|
70
103
|
)}
|
|
71
104
|
</FeedbackContext.Provider>
|
|
72
105
|
);
|
|
73
|
-
};
|
|
106
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { default as FeedbackWidget } from './components/FeedbackWidget';
|
|
|
2
2
|
export type { FeedbackData, FeedbackProps } from './components/FeedbackWidget';
|
|
3
3
|
export type { CkEditorProps } from './components/CkEditor';
|
|
4
4
|
export { FeedbackProvider, useFeedback } from './context/FeedbackContext';
|
|
5
|
+
export type { FeedbackSubmitHandler } from './context/FeedbackContext';
|
|
5
6
|
|
|
6
7
|
// Screenshot utilities (MathJax-CHTML aware, snapdom-based)
|
|
7
8
|
export {
|
package/src/main.tsx
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
1
|
import ReactDOM from 'react-dom/client';
|
|
3
2
|
import App from './App';
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
);
|
|
4
|
+
// NOTE: No <React.StrictMode> on purpose. In dev, StrictMode double-mounts
|
|
5
|
+
// components, and @ckeditor/ckeditor5-react creates a second editor instance
|
|
6
|
+
// during the remount (its async destroy hasn't finished) — showing TWO editors.
|
|
7
|
+
// The host app runs without StrictMode, so this matches production behaviour.
|
|
8
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
|