hocmai-feedback-widget 0.2.2 → 0.2.4

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HOCMAI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,136 +1,77 @@
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)**.
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
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.
5
+ ![version](https://img.shields.io/badge/version-0.2.3-blue)
6
+ ![react](https://img.shields.io/badge/react-%E2%89%A516.8-61dafb)
7
+ ![module](https://img.shields.io/badge/module-ESM-f7df1e)
8
+ ![license](https://img.shields.io/badge/license-MIT-green)
6
9
 
7
- ## sao bản này
10
+ Bộ chụp dùng [snapdom](https://github.com/zumerlab/snapdom) + nhúng font MathJax → chụp nhanh, đủ công thức ngay lần đầu, an toàn SSR.
8
11
 
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**.
12
+ ## Mục lục
13
13
 
14
- Bản này thay bộ chụp bằng [**snapdom**](https://github.com/zumerlab/snapdom) + các kỹ thuật:
14
+ - [Tính năng](#tính-năng)
15
+ - [Cài đặt](#cài-đặt)
16
+ - [Sử dụng](#sử-dụng)
17
+ - [API](#api)
18
+ - [Lưu ý](#lưu-ý)
19
+ - [License](#license)
15
20
 
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 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.
21
+ ## Tính năng
22
+
23
+ - 📸 Chụp **toàn màn hình** hoặc **chọn vùng**, render đúng công thức MathJax CHTML.
24
+ - 📝 Mô tả lỗi bằng **textarea** hoặc **CKEditor** (tùy chọn).
25
+ - 🖼️ Tự upload ảnh và trả về URL (tối đa 3 ảnh).
26
+ - 🔌 API gọn: `FeedbackProvider` + hook `useFeedback`.
27
+ - 🧩 Core **không** phụ thuộc CKEditor — chỉ cài khi cần.
20
28
 
21
29
  ## Cài đặt
22
30
 
23
31
  ```bash
24
32
  npm install hocmai-feedback-widget
25
- # hoặc
26
- yarn add hocmai-feedback-widget
27
33
  ```
28
34
 
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`.
35
+ **Peer dependencies:** `react`, `react-dom` (>= 16.8).
33
36
 
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
37
+ ## Sử dụng
37
38
 
38
39
  ### 1. Bọc app bằng `FeedbackProvider`
40
+
39
41
  ```tsx
40
42
  import { FeedbackProvider, type FeedbackData } from 'hocmai-feedback-widget';
41
43
 
42
- function App({ Component, pageProps }) {
43
- const handleSubmit = async (data: FeedbackData) => {
44
+ <FeedbackProvider
45
+ onSubmit={async (data: FeedbackData) => {
44
46
  // data.listImage: mảng URL ảnh đã upload
45
- // data.deviceInfo, data.errorType, data.errorDescription, data.idQuestion...
46
47
  await api.post('/questionReport/studentReport', data);
47
- };
48
-
49
- return (
50
- <FeedbackProvider onSubmit={handleSubmit} themeColor="#3b82f6">
51
- <Component {...pageProps} />
52
- </FeedbackProvider>
53
- );
54
- }
48
+ }}
49
+ >
50
+ <App />
51
+ </FeedbackProvider>;
55
52
  ```
56
53
 
54
+ > `onSubmit` **không bắt buộc** ở Provider — có thể truyền theo từng lần ở `openFeedback` (xem mục 2).
55
+
57
56
  ### 2. Mở form từ component con bằng `useFeedback`
57
+
58
58
  ```tsx
59
59
  import { useFeedback } from 'hocmai-feedback-widget';
60
60
 
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
- ```
61
+ const { openFeedback } = useFeedback();
70
62
 
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';
63
+ // a) Dùng onSubmit của Provider:
64
+ openFeedback(questionId, { questionIdChild });
74
65
 
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 });
66
+ // b) Hoặc truyền handler riêng cho lần gọi này (ghi đè Provider):
67
+ openFeedback(questionId, { questionIdChild }, async (data) => {
68
+ await api.post('/questionReport/studentReport', data);
69
+ });
78
70
  ```
79
71
 
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. |
89
-
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
- ```
72
+ ### 3. Rich-text editor cho ô mô tả _(tùy chọn)_
107
73
 
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.
74
+ Bật `isCkEditor` **và** truyền `descriptionEditor` (editor đóng gói sẵn entry phụ):
134
75
 
135
76
  ```tsx
136
77
  import { FeedbackProvider } from 'hocmai-feedback-widget';
@@ -138,42 +79,82 @@ import CkEditor from 'hocmai-feedback-widget/ckeditor';
138
79
 
139
80
  <FeedbackProvider onSubmit={handleSubmit} isCkEditor descriptionEditor={CkEditor}>
140
81
  <App />
141
- </FeedbackProvider>
82
+ </FeedbackProvider>;
142
83
  ```
143
84
 
144
- **Khi dùng `descriptionEditor`/`hocmai-feedback-widget/ckeditor`, phải cài thêm:**
85
+ Cài thêm peer deps (chỉ khi dùng CKEditor):
86
+
145
87
  ```bash
146
- yarn add @ckeditor/ckeditor5-react hocmai-ckeditor-custom ckeditor5-classic-with-mathtype
88
+ yarn add @ckeditor/ckeditor5-react@5.0.0 hocmai-ckeditor-custom ckeditor5-classic-with-mathtype
147
89
  ```
148
90
 
149
- > ⚠️ **Xung đột phiên bản:** `hocmai-ckeditor-custom` build kiểu 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.
91
+ **Lưu ý**: `@ckeditor/ckeditor5-react` chỉ hỗ trợ version 5, `hocmai-ckeditor-custom` thể dùng version 0.0.2 hoặc 0.0.7 trở lên.
152
92
 
153
- > ⚠️ Khi bật CKEditor, `errorDescription` **HTML** (không phải text thuần) — backend cần xử lý phù hợp.
93
+ ### 4. Chụp màn hình trực tiếp _(tùy chọn)_
154
94
 
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`).
95
+ ```ts
96
+ import { captureScreenshot } from 'hocmai-feedback-widget';
157
97
 
158
- ## chế chụp & giới hạn
98
+ const png = await captureScreenshot(); // toàn viewport
99
+ const region = await captureScreenshot({ x, y, width, height }); // 1 vùng (toạ độ viewport)
100
+ ```
159
101
 
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).
102
+ ## API
164
103
 
165
- ## Phát triển
104
+ ### `FeedbackProvider`
166
105
 
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)
106
+ | Prop | Kiểu | Bắt buộc | Mặc định | Mô tả |
107
+ | --- | --- | :---: | :---: | --- |
108
+ | `children` | `ReactNode` | | | Nội dung app được bọc. |
109
+ | `onSubmit` | `(data: FeedbackData) => Promise<void> \| void` | — | — | Handler mặc định khi gửi báo lỗi. Có thể bỏ, truyền theo từng lần ở `openFeedback`. |
110
+ | `themeColor` | `string` | | `#3b82f6` | Màu chủ đạo của nút/viền. |
111
+ | `isCkEditor` | `boolean` | | `false` | Dùng rich-text editor cho ô mô tả (cần kèm `descriptionEditor`). |
112
+ | `descriptionEditor` | `ComponentType<CkEditorProps>` | — | — | Component editor cho ô mô tả, vd `import CkEditor from 'hocmai-feedback-widget/ckeditor'`. |
113
+ | `isEnableCaptureFull` | `boolean` | — | `true` | Hiện nút "Toàn màn hình". |
114
+ | `isEnableCaptureElement` | `boolean` | — | `true` | Hiện nút "Chọn vùng". |
115
+ | `uploadUrl` | `string` | — | endpoint LCMS | Nơi upload ảnh: nhận `{ base64 }`, trả `{ file_url }`. |
116
+
117
+ ### `useFeedback()`
118
+
119
+ Trả về:
120
+
121
+ | Thành phần | Kiểu | Mô tả |
122
+ | --- | --- | --- |
123
+ | `openFeedback` | `(questionId, contextData?, onSubmit?) => void` | Mở form báo lỗi cho một câu hỏi. |
124
+ | `closeFeedback` | `() => void` | Đóng form. |
125
+ | `isOpen` | `boolean` | Form đang mở hay không. |
126
+ | `activeQuestionId` | `string \| number \| null` | ID câu hỏi đang mở form (`null` nếu đóng). |
127
+
128
+ Tham số của `openFeedback`:
129
+
130
+ | Tham số | Kiểu | Bắt buộc | Mô tả |
131
+ | --- | --- | :---: | --- |
132
+ | `questionId` | `string \| number` | ✓ | ID câu hỏi; tự điền vào ô "ID câu hỏi". |
133
+ | `contextData` | `object` | — | Dữ liệu kèm theo (trả lại trong `FeedbackData.contextData`). `contextData.questionIdChild` tự điền ô "ID câu hỏi con". |
134
+ | `onSubmit` | `(data: FeedbackData) => Promise<void> \| void` | — | Handler riêng cho lần gọi này, **ghi đè** `onSubmit` của Provider. |
135
+
136
+ ### `FeedbackData`
137
+
138
+ ```ts
139
+ interface FeedbackData {
140
+ idQuestion: string | number;
141
+ idChildQuestion?: string | number;
142
+ errorType: string; // incorrect_content | display_error | missing_data | technical_issue | other
143
+ errorDescription: string; // text; là HTML nếu dùng CKEditor
144
+ listImage: string[]; // URL ảnh đã upload (tối đa 3)
145
+ deviceInfo: { os; osVersion; browser; browserVersion; deviceType };
146
+ contextData?: any;
147
+ }
173
148
  ```
174
149
 
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`.)
150
+ ## Lưu ý
151
+
152
+ - **Thứ tự `onSubmit`:** handler theo lần gọi → handler ở Provider → nếu không có thì chỉ cảnh báo (không crash).
153
+ - **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 nên **không cần** cài `@ckeditor/ckeditor5-core`.
154
+ - Khi dùng CKEditor, `errorDescription` trả về là **HTML** — backend cần xử lý phù hợp.
155
+ - Core **không** phụ thuộc CKEditor: app không dùng `isCkEditor` vẫn build bình thường.
156
+ - An toàn **SSR** (không chạm `window`/`document` lúc import).
177
157
 
178
158
  ## License
179
- MIT
159
+
160
+ [MIT](./LICENSE)
@@ -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,12 +16,19 @@ 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;
17
24
  /** Editor component for the description (from `hocmai-feedback-widget/ckeditor` or your own). */
18
25
  descriptionEditor?: React.ComponentType<CkEditorProps>;
26
+ /** Show the "full screen" capture button. Default true. */
27
+ isEnableCaptureFull?: boolean;
28
+ /** Show the "select region" capture button. Default true. */
29
+ isEnableCaptureElement?: boolean;
30
+ /** Endpoint that accepts { base64 } and returns { file_url } for image uploads. */
31
+ uploadUrl?: string;
19
32
  }
20
33
  export declare const FeedbackProvider: React.FC<FeedbackProviderProps>;
21
34
  export {};