hocmai-feedback-widget 0.2.1 → 0.2.2
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 +132 -97
- package/dist/capture/device-detection.d.ts +13 -0
- package/dist/capture/fonts.d.ts +4 -0
- package/dist/capture/index.d.ts +14 -0
- package/dist/capture/mathjax-fonts.d.ts +5 -0
- package/dist/capture/types.d.ts +25 -0
- package/dist/ckeditor.mjs +113 -0
- package/dist/ckeditor.mjs.map +1 -0
- package/dist/components/CkEditor.d.ts +14 -0
- package/dist/components/FeedbackWidget.d.ts +11 -0
- package/dist/context/FeedbackContext.d.ts +5 -0
- package/dist/hocmai-feedback-widget.mjs +7544 -0
- package/dist/hocmai-feedback-widget.mjs.map +1 -0
- package/dist/index.d.ts +3 -2
- package/package.json +33 -29
- package/src/App.tsx +143 -58
- package/src/capture/device-detection.ts +52 -0
- package/src/capture/fonts.ts +34 -0
- package/src/capture/index.ts +362 -0
- package/src/capture/mathjax-fonts.ts +143 -0
- package/src/capture/types.ts +34 -0
- package/src/ckeditor.d.ts +14 -0
- package/src/components/CkEditor.tsx +130 -0
- package/src/components/CropOverlay.tsx +29 -47
- package/src/components/FeedbackWidget.tsx +96 -90
- package/src/context/FeedbackContext.tsx +11 -4
- package/src/index.ts +8 -3
- package/src/main.tsx +7 -8
- package/src/utils/device.ts +14 -14
- package/dist/support-feedback-lib.es.js +0 -3946
- package/dist/support-feedback-lib.umd.js +0 -84
- package/dist/test-mobile-screenshot.html +0 -211
- package/dist/test-simple-capture.html +0 -107
- package/dist/utils/screenshot/background-renderer.d.ts +0 -3
- package/dist/utils/screenshot/canvas-renderer.d.ts +0 -9
- package/dist/utils/screenshot/cleanup.d.ts +0 -3
- package/dist/utils/screenshot/device-detection.d.ts +0 -16
- package/dist/utils/screenshot/element-collector.d.ts +0 -10
- package/dist/utils/screenshot/image-renderer.d.ts +0 -21
- package/dist/utils/screenshot/performance.d.ts +0 -3
- package/dist/utils/screenshot/svg-renderer.d.ts +0 -24
- package/dist/utils/screenshot/text-renderer.d.ts +0 -23
- package/dist/utils/screenshot/types.d.ts +0 -35
- package/dist/utils/screenshot.d.ts +0 -9
- package/dist/vite.svg +0 -1
- package/src/App.css +0 -42
- package/src/assets/react.svg +0 -1
- package/src/index.css +0 -68
- package/src/utils/screenshot/background-renderer.ts +0 -54
- package/src/utils/screenshot/canvas-renderer.ts +0 -343
- package/src/utils/screenshot/cleanup.ts +0 -32
- package/src/utils/screenshot/device-detection.ts +0 -90
- package/src/utils/screenshot/element-collector.ts +0 -134
- package/src/utils/screenshot/image-renderer.ts +0 -148
- package/src/utils/screenshot/performance.ts +0 -57
- package/src/utils/screenshot/svg-renderer.ts +0 -149
- package/src/utils/screenshot/text-renderer.ts +0 -212
- package/src/utils/screenshot/types.ts +0 -40
- package/src/utils/screenshot.ts +0 -83
package/README.md
CHANGED
|
@@ -1,144 +1,179 @@
|
|
|
1
|
-
#
|
|
1
|
+
# hocmai-feedback-widget
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
- **Context-Aware**: Support for multiple questions/items on a single page.
|
|
7
|
-
- **Screenshot Capture**: Automatic viewport capture with cropping/highlighting.
|
|
8
|
-
- **Auto-Upload**: Screenshots are automatically uploaded to S3 and returned as URLs.
|
|
9
|
-
- **Image Protection**: Prevents right-click/download on sensitive screenshots.
|
|
10
|
-
- **Responsive**: Mobile-friendly "bottom sheet" style on small screens.
|
|
11
|
-
- **Customizable**: Theme colors, context data, and error types.
|
|
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.
|
|
12
6
|
|
|
13
|
-
##
|
|
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.
|
|
20
|
+
|
|
21
|
+
## Cài đặt
|
|
14
22
|
|
|
15
|
-
### Via NPM/Yarn
|
|
16
23
|
```bash
|
|
17
24
|
npm install hocmai-feedback-widget
|
|
18
|
-
#
|
|
25
|
+
# hoặc
|
|
19
26
|
yarn add hocmai-feedback-widget
|
|
20
27
|
```
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
1. Build and pack the project:
|
|
24
|
-
```bash
|
|
25
|
-
npm run build
|
|
26
|
-
npm pack
|
|
27
|
-
```
|
|
28
|
-
2. Install the generated `.tgz` file in your target project:
|
|
29
|
-
```bash
|
|
30
|
-
npm install ./path/to/hocmai-feedback-widget-0.1.0.tgz
|
|
31
|
-
```
|
|
29
|
+
Peer deps bắt buộc: `react`, `react-dom` (>=16.8).
|
|
32
30
|
|
|
33
|
-
|
|
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`.
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
Wrap your root component (or the section of the app that needs feedback) with `FeedbackProvider`.
|
|
34
|
+
> Cài qua tarball/local khi phát triển: `npm pack` rồi `yarn add ./hocmai-feedback-widget-0.2.2.tgz`.
|
|
37
35
|
|
|
36
|
+
## Dùng nhanh
|
|
37
|
+
|
|
38
|
+
### 1. Bọc app bằng `FeedbackProvider`
|
|
38
39
|
```tsx
|
|
39
40
|
import { FeedbackProvider, type FeedbackData } from 'hocmai-feedback-widget';
|
|
40
|
-
// If styles are not automatically injected by your bundler, you might need:
|
|
41
|
-
// import 'hocmai-feedback-widget/dist/style.css';
|
|
42
41
|
|
|
43
|
-
function App() {
|
|
42
|
+
function App({ Component, pageProps }) {
|
|
44
43
|
const handleSubmit = async (data: FeedbackData) => {
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// data.deviceInfo contains browser/OS info
|
|
49
|
-
// data.errorType contains the selected category
|
|
44
|
+
// data.listImage: mảng URL ảnh đã upload
|
|
45
|
+
// data.deviceInfo, data.errorType, data.errorDescription, data.idQuestion...
|
|
46
|
+
await api.post('/questionReport/studentReport', data);
|
|
50
47
|
};
|
|
51
48
|
|
|
52
49
|
return (
|
|
53
50
|
<FeedbackProvider onSubmit={handleSubmit} themeColor="#3b82f6">
|
|
54
|
-
<
|
|
51
|
+
<Component {...pageProps} />
|
|
55
52
|
</FeedbackProvider>
|
|
56
53
|
);
|
|
57
54
|
}
|
|
58
55
|
```
|
|
59
56
|
|
|
60
|
-
### 2.
|
|
61
|
-
Use the `useFeedback` hook anywhere in your child components to open the widget for a specific item.
|
|
62
|
-
|
|
57
|
+
### 2. Mở form từ component con bằng `useFeedback`
|
|
63
58
|
```tsx
|
|
64
59
|
import { useFeedback } from 'hocmai-feedback-widget';
|
|
65
60
|
|
|
66
|
-
const QuestionItem = ({
|
|
61
|
+
const QuestionItem = ({ question }) => {
|
|
67
62
|
const { openFeedback } = useFeedback();
|
|
68
|
-
|
|
69
63
|
return (
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
onClick={() => openFeedback(id, { questionIdChild: 'optional-child-id', ...otherContext })}
|
|
74
|
-
>
|
|
75
|
-
Report Error
|
|
76
|
-
</button>
|
|
77
|
-
</div>
|
|
64
|
+
<button onClick={() => openFeedback(question.id, { questionIdChild: question.childId })}>
|
|
65
|
+
Báo lỗi
|
|
66
|
+
</button>
|
|
78
67
|
);
|
|
79
68
|
};
|
|
80
69
|
```
|
|
81
70
|
|
|
82
|
-
### 3.
|
|
83
|
-
|
|
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';
|
|
84
74
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
<FeedbackWidget
|
|
89
|
-
questionId="123"
|
|
90
|
-
isOpen={isOpen}
|
|
91
|
-
onClose={() => setIsOpen(false)}
|
|
92
|
-
onSubmit={handleSubmit}
|
|
93
|
-
isEnableCaptureFull={true}
|
|
94
|
-
isEnableCaptureElement={true}
|
|
95
|
-
/>
|
|
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 });
|
|
96
78
|
```
|
|
97
79
|
|
|
98
|
-
##
|
|
80
|
+
## API
|
|
99
81
|
|
|
100
|
-
### FeedbackProvider
|
|
101
|
-
| Prop |
|
|
102
|
-
|
|
103
|
-
| `onSubmit` | `(data: FeedbackData) => Promise<void>` |
|
|
104
|
-
| `themeColor` | `string` |
|
|
105
|
-
| `
|
|
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. |
|
|
106
89
|
|
|
107
|
-
###
|
|
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`
|
|
108
96
|
```ts
|
|
109
97
|
interface FeedbackData {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
os: string;
|
|
118
|
-
browser: string;
|
|
119
|
-
screen: string;
|
|
120
|
-
userAgent: string;
|
|
121
|
-
};
|
|
122
|
-
contextData?: any;
|
|
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;
|
|
123
105
|
}
|
|
124
106
|
```
|
|
125
107
|
|
|
126
|
-
###
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
|
133
|
-
|
|
134
|
-
| `
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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.
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import { FeedbackProvider } from 'hocmai-feedback-widget';
|
|
137
|
+
import CkEditor from 'hocmai-feedback-widget/ckeditor';
|
|
138
|
+
|
|
139
|
+
<FeedbackProvider onSubmit={handleSubmit} isCkEditor descriptionEditor={CkEditor}>
|
|
140
|
+
<App />
|
|
141
|
+
</FeedbackProvider>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Khi dùng `descriptionEditor`/`hocmai-feedback-widget/ckeditor`, phải cài thêm:**
|
|
145
|
+
```bash
|
|
146
|
+
yarn add @ckeditor/ckeditor5-react hocmai-ckeditor-custom ckeditor5-classic-with-mathtype
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
> ⚠️ **Xung đột phiên bản:** `hocmai-ckeditor-custom` là 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.
|
|
152
|
+
|
|
153
|
+
> ⚠️ Khi bật CKEditor, `errorDescription` là **HTML** (không phải text thuần) — backend cần xử lý phù hợp.
|
|
154
|
+
|
|
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`).
|
|
157
|
+
|
|
158
|
+
## Cơ chế chụp & giới hạn
|
|
159
|
+
|
|
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).
|
|
164
|
+
|
|
165
|
+
## Phát triển
|
|
166
|
+
|
|
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)
|
|
173
|
+
```
|
|
174
|
+
|
|
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`.)
|
|
142
177
|
|
|
143
178
|
## License
|
|
144
179
|
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface CaptureCapabilities {
|
|
2
|
+
/** Largest canvas dimension (px) the device can reliably allocate. */
|
|
3
|
+
maxCanvasSize: number;
|
|
4
|
+
/** Recommended pixel multiplier before clamping to maxCanvasSize. */
|
|
5
|
+
recommendedScale: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function isIOSSafari(): boolean;
|
|
8
|
+
export declare function getCaptureCapabilities(): CaptureCapabilities;
|
|
9
|
+
/**
|
|
10
|
+
* Clamp a desired scale so neither dimension of the captured element exceeds
|
|
11
|
+
* the device's max canvas size. Keeps capture from silently failing on big pages.
|
|
12
|
+
*/
|
|
13
|
+
export declare function clampScale(desiredScale: number, contentWidth: number, contentHeight: number, maxCanvasSize: number): number;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Wait until MathJax has finished its initial typeset so formulas have laid out. */
|
|
2
|
+
export declare function waitForMathJax(): Promise<void>;
|
|
3
|
+
/** Resolve once the page is ready for a faithful snapshot. */
|
|
4
|
+
export declare function ensureReadyForCapture(): Promise<void>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Rect, CaptureOptions, CaptureProgress } from './types';
|
|
2
|
+
export type { Rect, CaptureProgress, CaptureOptions } from './types';
|
|
3
|
+
/** Prepare fonts for capture (call when the report form opens). */
|
|
4
|
+
export declare function prewarmCapture(): Promise<void>;
|
|
5
|
+
/**
|
|
6
|
+
* Capture a screenshot of the current viewport, or of `cropRect` (viewport coords).
|
|
7
|
+
*
|
|
8
|
+
* Signature is drop-in compatible with hocmai-feedback-widget's captureScreenshot.
|
|
9
|
+
* Strategy: snapdom (fonts embedded → MathJax CHTML renders correctly, single DOM
|
|
10
|
+
* clone → no O(N) layout thrash) with a modern-screenshot fallback. Always bounded
|
|
11
|
+
* by a timeout so it can never hang the UI.
|
|
12
|
+
*/
|
|
13
|
+
export declare function captureScreenshot(cropRect?: Rect, onProgress?: (progress: CaptureProgress) => void): Promise<string | null>;
|
|
14
|
+
export declare function captureScreenshotWithOptions(cropRect: Rect | undefined, options: CaptureOptions): Promise<string | null>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface Rect {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}
|
|
7
|
+
export type CapturePhase = 'preparing' | 'rendering' | 'cropping' | 'exporting' | 'done';
|
|
8
|
+
export interface CaptureProgress {
|
|
9
|
+
phase: CapturePhase;
|
|
10
|
+
progress: number;
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
export interface CaptureOptions {
|
|
14
|
+
/** Output pixel multiplier. Defaults to a device-aware value, capped by max canvas size. */
|
|
15
|
+
scale?: number;
|
|
16
|
+
/** Background fill for the exported PNG. Default '#ffffff'. */
|
|
17
|
+
backgroundColor?: string;
|
|
18
|
+
/** Hard timeout so a capture can never hang the UI. Default 20000ms. */
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
/** CSS selectors to exclude from the capture (the widget's own UI is always excluded). */
|
|
21
|
+
excludeSelectors?: string[];
|
|
22
|
+
onProgress?: (progress: CaptureProgress) => void;
|
|
23
|
+
}
|
|
24
|
+
/** Attribute used to tag the widget's own DOM so it never appears in screenshots. */
|
|
25
|
+
export declare const REPORT_UI_ATTR = "data-hsa-report-ui";
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { jsxs as m, jsx as b } from "react/jsx-runtime";
|
|
2
|
+
import { useRef as f, useState as g } from "react";
|
|
3
|
+
import { CKEditor as R } from "@ckeditor/ckeditor5-react";
|
|
4
|
+
import y from "hocmai-ckeditor-custom";
|
|
5
|
+
import "ckeditor5-classic-with-mathtype/build/translations/vi.js";
|
|
6
|
+
const w = "https://lcms.icanwork.vn/api/upload-to-s3/upload-image-error-to-s3", x = `
|
|
7
|
+
.hsa-ck-wrap .ck-editor { width: 100%; height: 100%; border: none; border-radius: 10px; outline: none; padding: 0; margin: 0; background-color: transparent; }
|
|
8
|
+
.hsa-ck-wrap .ck-toolbar { background-color: transparent !important; border: 1px solid #b2b2b2 !important; border-bottom: none; }
|
|
9
|
+
.hsa-ck-wrap .ck-content { padding: 8px 10px; margin: 0; border: 1px solid #b2b2b2; border-radius: 0 0 6px 6px; outline: none; resize: none; min-height: 120px; }
|
|
10
|
+
.hsa-ck-wrap .ck.ck-editor__editable:not(.ck-editor__nested-editable).ck-focused { box-shadow: none !important; }
|
|
11
|
+
.hsa-ck-wrap .ck.ck-button:not(.ck-disabled):hover, .hsa-ck-wrap a.ck.ck-button:not(.ck-disabled):hover { border-radius: 4px !important; }
|
|
12
|
+
.hsa-ck-wrap.ckstyle-err .ck-toolbar, .hsa-ck-wrap.ckstyle-err .ck-content { border-color: #ef4444 !important; }
|
|
13
|
+
`, T = [
|
|
14
|
+
"MathType",
|
|
15
|
+
"ChemType",
|
|
16
|
+
"heading",
|
|
17
|
+
"|",
|
|
18
|
+
"bold",
|
|
19
|
+
"underline",
|
|
20
|
+
"italic",
|
|
21
|
+
"link",
|
|
22
|
+
"subscript",
|
|
23
|
+
"superscript",
|
|
24
|
+
"fontColor",
|
|
25
|
+
"fontFamily",
|
|
26
|
+
"fontSize",
|
|
27
|
+
"bulletedList",
|
|
28
|
+
"numberedList",
|
|
29
|
+
"|",
|
|
30
|
+
"outdent",
|
|
31
|
+
"indent",
|
|
32
|
+
"alignment",
|
|
33
|
+
"|",
|
|
34
|
+
"imageUpload",
|
|
35
|
+
"blockQuote",
|
|
36
|
+
"insertTable",
|
|
37
|
+
"undo",
|
|
38
|
+
"redo",
|
|
39
|
+
"removeFormat"
|
|
40
|
+
];
|
|
41
|
+
function A(s) {
|
|
42
|
+
return (i) => ({
|
|
43
|
+
upload: () => new Promise((l, r) => {
|
|
44
|
+
i.file.then((a) => {
|
|
45
|
+
const t = new FileReader();
|
|
46
|
+
t.onload = async () => {
|
|
47
|
+
try {
|
|
48
|
+
const d = await (await fetch(s, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: { "Content-Type": "application/json", Accept: "*/*" },
|
|
51
|
+
body: JSON.stringify({ base64: t.result })
|
|
52
|
+
})).json();
|
|
53
|
+
l({ default: d.file_url });
|
|
54
|
+
} catch (o) {
|
|
55
|
+
r(o);
|
|
56
|
+
}
|
|
57
|
+
}, t.onerror = () => r(t.error), t.readAsDataURL(a);
|
|
58
|
+
}).catch(r);
|
|
59
|
+
})
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const S = ({
|
|
63
|
+
handleContent: s,
|
|
64
|
+
placeholder: i,
|
|
65
|
+
contentQuestion: l,
|
|
66
|
+
error: r,
|
|
67
|
+
showOnly: a,
|
|
68
|
+
className: t,
|
|
69
|
+
disableItem: o,
|
|
70
|
+
uploadUrl: d = w
|
|
71
|
+
}) => {
|
|
72
|
+
const k = f(null), [c, u] = g(l ?? ""), h = T.filter(
|
|
73
|
+
(e) => o ? !o.includes(e) : !0
|
|
74
|
+
);
|
|
75
|
+
return /* @__PURE__ */ m(
|
|
76
|
+
"div",
|
|
77
|
+
{
|
|
78
|
+
ref: k,
|
|
79
|
+
className: `hsa-ck-wrap ${r ? "ckstyle-err" : ""} ${a ? "ckstyle" : ""} ${t ?? ""}`,
|
|
80
|
+
style: { color: "#000" },
|
|
81
|
+
children: [
|
|
82
|
+
/* @__PURE__ */ b("style", { children: x }),
|
|
83
|
+
/* @__PURE__ */ b(
|
|
84
|
+
R,
|
|
85
|
+
{
|
|
86
|
+
editor: y,
|
|
87
|
+
config: {
|
|
88
|
+
toolbar: { items: h },
|
|
89
|
+
placeholder: i,
|
|
90
|
+
licenseKey: "MEtLSVRKRXpqd2lSMnNuL3dsYnZHUWozTUpYSjU4NlhjRWtVQ3kzRFR6TlhQVTVBMDJxMWo1V0o3R29LLU1qQXlOREEwTVRNPQ"
|
|
91
|
+
},
|
|
92
|
+
data: c == null ? void 0 : c.replaceAll("<mfenced>", "<mfenced separators>"),
|
|
93
|
+
onReady: (e) => {
|
|
94
|
+
e.setData(
|
|
95
|
+
e.getData().replaceAll("<p> </p>", "").replaceAll("<br> ", "<br/>")
|
|
96
|
+
), a ? e.enableReadOnlyMode("hsa-report-widget") : e.disableReadOnlyMode("hsa-report-widget");
|
|
97
|
+
const n = e.plugins.get("FileRepository");
|
|
98
|
+
n && (n.createUploadAdapter = A(d));
|
|
99
|
+
},
|
|
100
|
+
onChange: (e, n) => {
|
|
101
|
+
const p = n.getData();
|
|
102
|
+
u(p), s(p.replaceAll("<p> </p>", "<br/>"));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
export {
|
|
111
|
+
S as default
|
|
112
|
+
};
|
|
113
|
+
//# sourceMappingURL=ckeditor.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ckeditor.mjs","sources":["../src/components/CkEditor.tsx"],"sourcesContent":["import { useRef, useState } from 'react';\nimport { CKEditor } from '@ckeditor/ckeditor5-react';\nimport ClassicEditor from 'hocmai-ckeditor-custom';\nimport 'ckeditor5-classic-with-mathtype/build/translations/vi.js';\n\nexport interface CkEditorProps {\n /** Called with the editor HTML whenever the content changes. */\n handleContent: (data: string) => void;\n placeholder?: string;\n contentQuestion?: string;\n error?: boolean;\n showOnly?: boolean;\n className?: string;\n disableItem?: string[];\n /** Endpoint that accepts { base64 } and returns { file_url } for image uploads. */\n uploadUrl?: string;\n}\n\nconst DEFAULT_UPLOAD_URL =\n 'https://lcms.icanwork.vn/api/upload-to-s3/upload-image-error-to-s3';\n\n// Style overrides for the CKEditor UI, injected via a <style> tag (self-contained;\n// no Next.js-only CSS-module import).\nconst CKEDITOR_OVERRIDE_CSS = `\n.hsa-ck-wrap .ck-editor { width: 100%; height: 100%; border: none; border-radius: 10px; outline: none; padding: 0; margin: 0; background-color: transparent; }\n.hsa-ck-wrap .ck-toolbar { background-color: transparent !important; border: 1px solid #b2b2b2 !important; border-bottom: none; }\n.hsa-ck-wrap .ck-content { padding: 8px 10px; margin: 0; border: 1px solid #b2b2b2; border-radius: 0 0 6px 6px; outline: none; resize: none; min-height: 120px; }\n.hsa-ck-wrap .ck.ck-editor__editable:not(.ck-editor__nested-editable).ck-focused { box-shadow: none !important; }\n.hsa-ck-wrap .ck.ck-button:not(.ck-disabled):hover, .hsa-ck-wrap a.ck.ck-button:not(.ck-disabled):hover { border-radius: 4px !important; }\n.hsa-ck-wrap.ckstyle-err .ck-toolbar, .hsa-ck-wrap.ckstyle-err .ck-content { border-color: #ef4444 !important; }\n`;\n\nconst TOOLBAR_ITEMS = [\n 'MathType', 'ChemType', 'heading', '|',\n 'bold', 'underline', 'italic', 'link', 'subscript', 'superscript',\n 'fontColor', 'fontFamily', 'fontSize', 'bulletedList', 'numberedList', '|',\n 'outdent', 'indent', 'alignment', '|',\n 'imageUpload', 'blockQuote', 'insertTable', 'undo', 'redo', 'removeFormat',\n];\n\n/** Upload an image as base64 and return its URL (fetch — no axios dependency). */\nfunction makeUploadAdapter(uploadUrl: string) {\n return (loader: { file: Promise<File> }) => ({\n upload: () =>\n new Promise((resolve, reject) => {\n loader.file\n .then((img) => {\n const reader = new FileReader();\n reader.onload = async () => {\n try {\n const res = await fetch(uploadUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', Accept: '*/*' },\n body: JSON.stringify({ base64: reader.result as string }),\n });\n const data = await res.json();\n resolve({ default: data.file_url });\n } catch (err) {\n reject(err);\n }\n };\n reader.onerror = () => reject(reader.error);\n reader.readAsDataURL(img);\n })\n .catch(reject);\n }),\n });\n}\n\nconst CkEditor = ({\n handleContent,\n placeholder,\n contentQuestion,\n error,\n showOnly,\n className,\n disableItem,\n uploadUrl = DEFAULT_UPLOAD_URL,\n}: CkEditorProps) => {\n const refCkeditor = useRef<HTMLDivElement>(null);\n const [content, setContent] = useState(contentQuestion ?? '');\n\n const items = TOOLBAR_ITEMS.filter((item) =>\n disableItem ? !disableItem.includes(item) : true\n );\n\n return (\n <div\n ref={refCkeditor}\n className={`hsa-ck-wrap ${error ? 'ckstyle-err' : ''} ${showOnly ? 'ckstyle' : ''} ${className ?? ''}`}\n style={{ color: '#000' }}\n >\n <style>{CKEDITOR_OVERRIDE_CSS}</style>\n <CKEditor\n editor={ClassicEditor}\n config={{\n toolbar: { items },\n placeholder,\n licenseKey:\n 'MEtLSVRKRXpqd2lSMnNuL3dsYnZHUWozTUpYSjU4NlhjRWtVQ3kzRFR6TlhQVTVBMDJxMWo1V0o3R29LLU1qQXlOREEwTVRNPQ',\n }}\n data={content?.replaceAll('<mfenced>', '<mfenced separators>')}\n onReady={(editor: any) => {\n editor.setData(\n editor\n .getData()\n .replaceAll('<p> </p>', '')\n .replaceAll('<br> ', '<br/>')\n );\n if (showOnly) {\n editor.enableReadOnlyMode('hsa-report-widget');\n } else {\n editor.disableReadOnlyMode('hsa-report-widget');\n }\n const fileRepository = editor.plugins.get('FileRepository');\n if (fileRepository) {\n fileRepository.createUploadAdapter = makeUploadAdapter(uploadUrl);\n }\n }}\n onChange={(_event: unknown, editor: any) => {\n const data = editor.getData();\n setContent(data);\n handleContent(data.replaceAll('<p> </p>', '<br/>'));\n }}\n />\n </div>\n );\n};\n\nexport default CkEditor;"],"names":["DEFAULT_UPLOAD_URL","CKEDITOR_OVERRIDE_CSS","TOOLBAR_ITEMS","makeUploadAdapter","uploadUrl","loader","resolve","reject","img","reader","data","err","CkEditor","handleContent","placeholder","contentQuestion","error","showOnly","className","disableItem","refCkeditor","useRef","content","setContent","useState","items","item","jsxs","jsx","CKEditor","ClassicEditor","editor","fileRepository","_event"],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface CkEditorProps {
|
|
2
|
+
/** Called with the editor HTML whenever the content changes. */
|
|
3
|
+
handleContent: (data: string) => void;
|
|
4
|
+
placeholder?: string;
|
|
5
|
+
contentQuestion?: string;
|
|
6
|
+
error?: boolean;
|
|
7
|
+
showOnly?: boolean;
|
|
8
|
+
className?: string;
|
|
9
|
+
disableItem?: string[];
|
|
10
|
+
/** Endpoint that accepts { base64 } and returns { file_url } for image uploads. */
|
|
11
|
+
uploadUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
declare const CkEditor: ({ handleContent, placeholder, contentQuestion, error, showOnly, className, disableItem, uploadUrl, }: CkEditorProps) => import("react").JSX.Element;
|
|
14
|
+
export default CkEditor;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
2
|
import { getDeviceInfo } from '../utils/device';
|
|
3
|
+
import { CkEditorProps } from './CkEditor';
|
|
3
4
|
export interface FeedbackData {
|
|
4
5
|
idQuestion: string | number;
|
|
5
6
|
idChildQuestion?: string | number;
|
|
@@ -20,6 +21,16 @@ export interface FeedbackProps {
|
|
|
20
21
|
onClose?: () => void;
|
|
21
22
|
isEnableCaptureFull?: boolean;
|
|
22
23
|
isEnableCaptureElement?: boolean;
|
|
24
|
+
/** Endpoint that accepts { base64 } and returns { file_url }. */
|
|
25
|
+
uploadUrl?: string;
|
|
26
|
+
/** Use the rich-text editor for the description instead of a plain textarea. */
|
|
27
|
+
isCkEditor?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Rich-text editor component for the description (used when isCkEditor is true).
|
|
30
|
+
* Pass the ready-made editor from `hocmai-feedback-widget/ckeditor`, or your own.
|
|
31
|
+
* Kept as a prop so the core bundle has no CKEditor dependency.
|
|
32
|
+
*/
|
|
33
|
+
descriptionEditor?: React.ComponentType<CkEditorProps>;
|
|
23
34
|
}
|
|
24
35
|
declare const FeedbackWidget: React.FC<FeedbackProps>;
|
|
25
36
|
export default FeedbackWidget;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { default as React, ReactNode } from 'react';
|
|
2
2
|
import { FeedbackData } from '../components/FeedbackWidget';
|
|
3
|
+
import { CkEditorProps } from '../components/CkEditor';
|
|
3
4
|
interface FeedbackContextType {
|
|
4
5
|
openFeedback: (questionId: string | number, contextData?: any) => void;
|
|
5
6
|
closeFeedback: () => void;
|
|
@@ -11,6 +12,10 @@ interface FeedbackProviderProps {
|
|
|
11
12
|
children: ReactNode;
|
|
12
13
|
onSubmit: (data: FeedbackData) => Promise<void>;
|
|
13
14
|
themeColor?: string;
|
|
15
|
+
/** Use the rich-text editor for the description instead of a plain textarea. */
|
|
16
|
+
isCkEditor?: boolean;
|
|
17
|
+
/** Editor component for the description (from `hocmai-feedback-widget/ckeditor` or your own). */
|
|
18
|
+
descriptionEditor?: React.ComponentType<CkEditorProps>;
|
|
14
19
|
}
|
|
15
20
|
export declare const FeedbackProvider: React.FC<FeedbackProviderProps>;
|
|
16
21
|
export {};
|