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 CHANGED
@@ -1,136 +1,49 @@
1
1
  # hocmai-feedback-widget
2
2
 
3
- Widget **báo lỗi câu hỏi kèm chụp ảnh màn hình**, tối ưu cho trang **nhiều công thức MathJax (CHTML)**.
4
-
5
- > Phiên bản `0.2.2`. API tương thích ngược với `0.2.1` — nâng cấp chỉ cần đổi version, không phải sửa code sử dụng.
6
-
7
- ## Vì sao có bản này
8
-
9
- Bộ chụp ở các bản trước tự viết một "layered canvas renderer": quét toàn bộ DOM bằng
10
- `getBoundingClientRect()` (treo khi có hàng nghìn node `<mjx-*>`) và chỉ render được math dạng **SVG**.
11
- Nhưng app render công thức bằng **MathJax v3 CHTML** (HTML + web-font riêng) nên ảnh bị **treo** và
12
- **trắng / mất công thức**.
13
-
14
- Bản này thay bộ chụp bằng [**snapdom**](https://github.com/zumerlab/snapdom) + các kỹ thuật:
15
-
16
- - **Prune off-screen**: chỉ clone phần tử trong vùng nhìn thấy → nhanh (~2s) dù trang cao hàng chục nghìn px.
17
- - **Nhúng font MathJax CHTML thành data-URI** trước khi chụp → công thức render đủ **ngay lần đầu** (hết lỗi `____`).
18
- - **Hâm nóng lúc mở form** (nền, trong khi người dùng gõ mô tả) → lúc bấm chụp chỉ còn 1 lượt nhanh.
19
- - **Fallback** `modern-screenshot` + **timeout** cứng (mặc định 20s) → không bao giờ treo vô hạn.
3
+ Widget báo lỗi câu hỏi kèm chụp ảnh màn hình, tối ưu cho trang nhiều công thức **MathJax CHTML**. ESM, tương thích React ≥ 16.8.
20
4
 
21
5
  ## Cài đặt
22
6
 
23
7
  ```bash
24
8
  npm install hocmai-feedback-widget
25
- # hoặc
26
- yarn add hocmai-feedback-widget
9
+ # peer deps: react, react-dom
27
10
  ```
28
11
 
29
- Peer deps bắt buộc: `react`, `react-dom` (>=16.8).
30
-
31
- Peer deps **tùy chọn** (chỉ khi dùng `isCkEditor`, xem mục [CKEditor](#trình-soạn-thảo-ckeditor-isckeditor)):
32
- `@ckeditor/ckeditor5-react@^41.2.0`, `hocmai-ckeditor-custom`, `ckeditor5-classic-with-mathtype@1.0.0`.
33
-
34
- > Cài qua tarball/local khi phát triển: `npm pack` rồi `yarn add ./hocmai-feedback-widget-0.2.2.tgz`.
35
-
36
- ## Dùng nhanh
12
+ ## Sử dụng
37
13
 
38
14
  ### 1. Bọc app bằng `FeedbackProvider`
15
+
39
16
  ```tsx
40
17
  import { FeedbackProvider, type FeedbackData } from 'hocmai-feedback-widget';
41
18
 
42
- function App({ Component, pageProps }) {
43
- const handleSubmit = async (data: FeedbackData) => {
44
- // data.listImage: mảng URL ảnh đã upload
45
- // data.deviceInfo, data.errorType, data.errorDescription, data.idQuestion...
46
- await api.post('/questionReport/studentReport', data);
47
- };
48
-
49
- return (
50
- <FeedbackProvider onSubmit={handleSubmit} themeColor="#3b82f6">
51
- <Component {...pageProps} />
52
- </FeedbackProvider>
53
- );
54
- }
19
+ <FeedbackProvider onSubmit={async (data: FeedbackData) => {
20
+ await api.post('/questionReport/studentReport', data); // data.listImage URL ảnh đã upload
21
+ }}>
22
+ <App />
23
+ </FeedbackProvider>
55
24
  ```
56
25
 
26
+ `onSubmit` **không bắt buộc** ở Provider — có thể truyền theo từng lần ở `openFeedback` (xem dưới).
27
+
57
28
  ### 2. Mở form từ component con bằng `useFeedback`
29
+
58
30
  ```tsx
59
31
  import { useFeedback } from 'hocmai-feedback-widget';
60
32
 
61
- const QuestionItem = ({ question }) => {
62
- const { openFeedback } = useFeedback();
63
- return (
64
- <button onClick={() => openFeedback(question.id, { questionIdChild: question.childId })}>
65
- Báo lỗi
66
- </button>
67
- );
68
- };
69
- ```
33
+ const { openFeedback } = useFeedback();
70
34
 
71
- ### 3. Chụp màn hình trực tiếp (tùy chọn)
72
- ```ts
73
- import { captureScreenshot, captureScreenshotWithOptions } from 'hocmai-feedback-widget';
35
+ // Dùng onSubmit của Provider:
36
+ openFeedback(questionId, { questionIdChild });
74
37
 
75
- const png = await captureScreenshot(); // toàn viewport
76
- const region = await captureScreenshot({ x, y, width, height }); // 1 vùng (toạ độ viewport)
77
- const tuned = await captureScreenshotWithOptions(undefined, { scale: 2, timeoutMs: 30000 });
38
+ // Hoặc truyền handler riêng cho lần gọi này (ghi đè Provider):
39
+ openFeedback(questionId, { questionIdChild }, async (data) => {
40
+ await api.post('/questionReport/studentReport', data);
41
+ });
78
42
  ```
79
43
 
80
- ## API
81
-
82
- ### `FeedbackProvider`
83
- | Prop | Kiểu | Mặc định | Mô tả |
84
- |------|------|----------|-------|
85
- | `onSubmit` | `(data: FeedbackData) => Promise<void>` | (bắt buộc) | Gọi khi gửi báo lỗi. |
86
- | `themeColor` | `string` | `#3b82f6` | Màu chủ đạo nút/viền. |
87
- | `isCkEditor` | `boolean` | `false` | Dùng CKEditor (rich-text) cho ô mô tả thay vì textarea. |
88
- | `children` | `ReactNode` | — | Nội dung app. |
44
+ ### 3. (Tùy chọn) Rich-text editor cho ô mô tả — `isCkEditor`
89
45
 
90
- ### `useFeedback()`
91
- - `openFeedback(questionId, contextData?)` — mở form. `contextData.questionIdChild` sẽ tự điền ô "ID câu hỏi con".
92
- - `closeFeedback()` — đóng form.
93
- - `isOpen`, `activeQuestionId` — trạng thái hiện tại.
94
-
95
- ### `FeedbackData`
96
- ```ts
97
- interface FeedbackData {
98
- idQuestion: string | number;
99
- idChildQuestion?: string | number;
100
- errorType: string; // incorrect_content | display_error | missing_data | technical_issue | other
101
- errorDescription: string; // text; là HTML nếu bật isCkEditor
102
- listImage: string[]; // URL ảnh đã upload (tối đa 3)
103
- deviceInfo: { os; osVersion; browser; browserVersion; deviceType };
104
- contextData?: any;
105
- }
106
- ```
107
-
108
- ### `FeedbackWidget` (dùng độc lập, không qua Provider)
109
- | Prop | Kiểu | Mặc định | Mô tả |
110
- |------|------|----------|-------|
111
- | `questionId` | `string \| number` | (bắt buộc) | ID câu hỏi. |
112
- | `onSubmit` | `function` | (bắt buộc) | Nhận `FeedbackData`. |
113
- | `contextData` | `object` | — | Dữ liệu kèm theo (vd `questionIdChild`). |
114
- | `themeColor` | `string` | `#3b82f6` | Màu chủ đạo. |
115
- | `isOpen` / `onClose` / `onOpen` | — | — | Điều khiển hiển thị thủ công. |
116
- | `isEnableCaptureFull` | `boolean` | `true` | Bật nút "Toàn màn hình". |
117
- | `isEnableCaptureElement` | `boolean` | `true` | Bật nút "Chọn vùng". |
118
- | `isCkEditor` | `boolean` | `false` | Dùng CKEditor cho ô mô tả. |
119
- | `uploadUrl` | `string` | endpoint LCMS mặc định | Nơi upload ảnh `{ base64 }` → trả `{ file_url }`. |
120
-
121
- ### Tiện ích chụp
122
- - `captureScreenshot(cropRect?, onProgress?) => Promise<string | null>` — trả PNG data-URL (hoặc `null` nếu thất bại).
123
- - `captureScreenshotWithOptions(cropRect, options)` — `options`: `{ scale?, backgroundColor?, timeoutMs?, excludeSelectors?, onProgress? }`.
124
- - `prewarmCapture()` — hâm nóng font/cache (tự gọi khi form mở).
125
- - Types: `Rect`, `CaptureProgress`, `CaptureOptions`.
126
-
127
- ## Trình soạn thảo CKEditor (`isCkEditor`)
128
-
129
- Mặc định ô mô tả là textarea. Muốn dùng **CKEditor (kèm MathType)**, bật `isCkEditor` **và truyền component
130
- editor** qua prop `descriptionEditor`. Editor nằm ở **entry phụ** `hocmai-feedback-widget/ckeditor`.
131
-
132
- > Vì sao tách rời? Để **core widget không phụ thuộc CKEditor** — app không dùng CKEditor vẫn build bình
133
- > thường, không cần cài deps CKEditor. Chỉ ai dùng `descriptionEditor` mới cần cài.
46
+ Bật `isCkEditor` **và** truyền `descriptionEditor` (editor đóng gói sẵn ở entry phụ):
134
47
 
135
48
  ```tsx
136
49
  import { FeedbackProvider } from 'hocmai-feedback-widget';
@@ -141,39 +54,56 @@ import CkEditor from 'hocmai-feedback-widget/ckeditor';
141
54
  </FeedbackProvider>
142
55
  ```
143
56
 
144
- **Khi dùng `descriptionEditor`/`hocmai-feedback-widget/ckeditor`, phải cài thêm:**
57
+ Phải cài thêm các peer deps (chỉ khi dùng CKEditor):
58
+
145
59
  ```bash
146
60
  yarn add @ckeditor/ckeditor5-react hocmai-ckeditor-custom ckeditor5-classic-with-mathtype
147
61
  ```
148
62
 
149
- > ⚠️ **Xung đột phiên bản:** `hocmai-ckeditor-custom` build kiểu cũ → phải dùng `@ckeditor/ckeditor5-react`
150
- > đời cũ (v5/v6), **KHÔNG** dùng latest (v11 cần `ckeditor5>=46`). Nên **copy đúng version** của 3 package
151
- > này từ project gốc đang chạy CKEditor. Build này đã chứa sẵn core nên **không cần** `@ckeditor/ckeditor5-core` riêng.
63
+ ### 4. (Tùy chọn) Chụp màn hình trực tiếp
64
+
65
+ ```ts
66
+ import { captureScreenshot } from 'hocmai-feedback-widget';
67
+
68
+ const png = await captureScreenshot(); // toàn viewport
69
+ const region = await captureScreenshot({ x, y, width, height }); // 1 vùng (toạ độ viewport)
70
+ ```
152
71
 
153
- > ⚠️ Khi bật CKEditor, `errorDescription` là **HTML** (không phải text thuần) — backend cần xử lý phù hợp.
72
+ ## API
154
73
 
155
- > Bạn cũng có thể truyền editor của riêng mình cho `descriptionEditor` (component nhận props
156
- > `{ handleContent, contentQuestion, error, placeholder, uploadUrl }` — xem type `CkEditorProps`).
74
+ **`FeedbackProvider`**
157
75
 
158
- ## chế chụp & giới hạn
76
+ | Prop | Kiểu | tả |
77
+ |------|------|-------|
78
+ | `onSubmit` | `(data: FeedbackData) => Promise<void> \| void` | Tùy chọn. Handler mặc định khi gửi. |
79
+ | `isCkEditor` | `boolean` | Dùng rich-text editor cho ô mô tả (cần `descriptionEditor`). |
80
+ | `descriptionEditor` | `ComponentType` | Editor truyền vào, vd `import CkEditor from 'hocmai-feedback-widget/ckeditor'`. |
81
+ | `themeColor` | `string` | Màu chủ đạo (mặc định `#3b82f6`). |
159
82
 
160
- - Mặc định chụp **vùng viewport** (hoặc vùng crop), không phải toàn trang nhanh kể cả trang dài.
161
- - UI của chính widget tự bị loại khỏi ảnh (qua thuộc tính `data-hsa-report-ui`).
162
- - `scale` mặc định theo thiết bị, bị giới hạn bởi kích thước canvas tối đa của trình duyệt (16384px/cạnh).
163
- - An toàn SSR (không chạm `window`/`document` lúc import).
83
+ **`useFeedback()`** `{ openFeedback, closeFeedback, isOpen, activeQuestionId }`
84
+ - `openFeedback(questionId, contextData?, onSubmit?)` mở form; `onSubmit?` ghi đè handler của Provider cho lần này.
164
85
 
165
- ## Phát triển
86
+ **`FeedbackData`**
166
87
 
167
- ```bash
168
- npm install
169
- npm run dev # dev harness: 60 câu nhiều công thức MathJax CHTML
170
- npm run typecheck # kiểm tra kiểu
171
- npm run test # Playwright: chụp nhanh + không trắng + đúng vùng + cuộn
172
- npm run build # build dist (esm .mjs + umd .cjs + .d.ts)
88
+ ```ts
89
+ {
90
+ idQuestion: string | number;
91
+ idChildQuestion?: string | number;
92
+ errorType: string; // incorrect_content | display_error | missing_data | technical_issue | other
93
+ errorDescription: string; // text; HTML nếu dùng CKEditor
94
+ listImage: string[]; // URL ảnh đã upload (tối đa 3)
95
+ deviceInfo: { os; osVersion; browser; browserVersion; deviceType };
96
+ contextData?: any;
97
+ }
173
98
  ```
174
99
 
175
- ## Migrate từ `hocmai-feedback-widget@0.2.1`
176
- Chỉ cần nâng version — API `FeedbackProvider`/`useFeedback`/`FeedbackData` giữ nguyên. Không phải sửa code sử dụng. (Bản này thêm tùy chọn `isCkEditor` và `uploadUrl`.)
100
+ ## Lưu ý
101
+
102
+ - **onSubmit** ưu tiên: handler theo lần gọi → handler ở Provider → không có thì chỉ cảnh báo (không crash).
103
+ - **CKEditor**: `hocmai-ckeditor-custom` là build kiểu cũ → dùng `@ckeditor/ckeditor5-react` đời cũ (v5/v6), **không** dùng latest. Nên copy đúng version từ project gốc. Build này đã chứa core, **không cần** cài `@ckeditor/ckeditor5-core`.
104
+ - Khi dùng CKEditor, `errorDescription` trả về là **HTML** — backend cần xử lý phù hợp.
105
+ - Core **không** phụ thuộc CKEditor: app không dùng `isCkEditor` vẫn build bình thường, không cần cài deps CKEditor.
106
+ - An toàn **SSR** (không chạm `window`/`document` lúc import).
177
107
 
178
108
  ## License
179
- MIT
109
+ MIT
@@ -13,7 +13,8 @@ export interface FeedbackData {
13
13
  export interface FeedbackProps {
14
14
  questionId: string | number;
15
15
  contextData?: Record<string, any>;
16
- onSubmit: (data: FeedbackData) => Promise<void>;
16
+ /** Submit handler. Optional — may be provided per call via openFeedback or at the Provider. */
17
+ onSubmit?: (data: FeedbackData) => Promise<void> | void;
17
18
  themeColor?: string;
18
19
  triggerComponent?: React.ReactNode;
19
20
  isOpen?: boolean;
@@ -1,8 +1,14 @@
1
1
  import { default as React, ReactNode } from 'react';
2
2
  import { FeedbackData } from '../components/FeedbackWidget';
3
3
  import { CkEditorProps } from '../components/CkEditor';
4
+ export type FeedbackSubmitHandler = (data: FeedbackData) => Promise<void> | void;
4
5
  interface FeedbackContextType {
5
- openFeedback: (questionId: string | number, contextData?: any) => void;
6
+ /**
7
+ * Open the report form.
8
+ * @param onSubmit optional per-call handler; overrides the Provider's onSubmit
9
+ * for this submission. Lets you handle submit from anywhere.
10
+ */
11
+ openFeedback: (questionId: string | number, contextData?: any, onSubmit?: FeedbackSubmitHandler) => void;
6
12
  closeFeedback: () => void;
7
13
  isOpen: boolean;
8
14
  activeQuestionId: string | number | null;
@@ -10,7 +16,8 @@ interface FeedbackContextType {
10
16
  export declare const useFeedback: () => FeedbackContextType;
11
17
  interface FeedbackProviderProps {
12
18
  children: ReactNode;
13
- onSubmit: (data: FeedbackData) => Promise<void>;
19
+ /** Default submit handler. Optional — you can instead pass one per call via openFeedback. */
20
+ onSubmit?: FeedbackSubmitHandler;
14
21
  themeColor?: string;
15
22
  /** Use the rich-text editor for the description instead of a plain textarea. */
16
23
  isCkEditor?: boolean;