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/README.md
CHANGED
|
@@ -1,136 +1,49 @@
|
|
|
1
1
|
# hocmai-feedback-widget
|
|
2
2
|
|
|
3
|
-
Widget
|
|
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
|
-
#
|
|
26
|
-
yarn add hocmai-feedback-widget
|
|
9
|
+
# peer deps: react, react-dom
|
|
27
10
|
```
|
|
28
11
|
|
|
29
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 là 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
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
import { captureScreenshot, captureScreenshotWithOptions } from 'hocmai-feedback-widget';
|
|
35
|
+
// Dùng onSubmit của Provider:
|
|
36
|
+
openFeedback(questionId, { questionIdChild });
|
|
74
37
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
72
|
+
## API
|
|
154
73
|
|
|
155
|
-
|
|
156
|
-
> `{ handleContent, contentQuestion, error, placeholder, uploadUrl }` — xem type `CkEditorProps`).
|
|
74
|
+
**`FeedbackProvider`**
|
|
157
75
|
|
|
158
|
-
|
|
76
|
+
| Prop | Kiểu | Mô 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
|
-
|
|
161
|
-
-
|
|
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
|
-
|
|
86
|
+
**`FeedbackData`**
|
|
166
87
|
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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; là 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
|
-
##
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|