hocmai-feedback-widget 0.2.1 → 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 +78 -113
- 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 +13 -1
- package/dist/context/FeedbackContext.d.ts +14 -2
- package/dist/hocmai-feedback-widget.mjs +7550 -0
- package/dist/hocmai-feedback-widget.mjs.map +1 -0
- package/dist/index.d.ts +4 -2
- package/package.json +39 -29
- package/src/App.tsx +147 -58
- package/src/capture/device-detection.ts +52 -0
- package/src/capture/fonts.ts +34 -0
- package/src/capture/index.ts +361 -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 +105 -93
- package/src/context/FeedbackContext.tsx +49 -9
- package/src/index.ts +9 -3
- package/src/main.tsx +7 -9
- 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,109 @@
|
|
|
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**. ESM, tương thích React ≥ 16.8.
|
|
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
|
+
## Cài đặt
|
|
12
6
|
|
|
13
|
-
## Installation
|
|
14
|
-
|
|
15
|
-
### Via NPM/Yarn
|
|
16
7
|
```bash
|
|
17
8
|
npm install hocmai-feedback-widget
|
|
18
|
-
#
|
|
19
|
-
yarn add hocmai-feedback-widget
|
|
9
|
+
# peer deps: react, react-dom
|
|
20
10
|
```
|
|
21
11
|
|
|
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
|
-
```
|
|
32
|
-
|
|
33
|
-
## Usage
|
|
12
|
+
## Sử dụng
|
|
34
13
|
|
|
35
|
-
### 1.
|
|
36
|
-
Wrap your root component (or the section of the app that needs feedback) with `FeedbackProvider`.
|
|
14
|
+
### 1. Bọc app bằng `FeedbackProvider`
|
|
37
15
|
|
|
38
16
|
```tsx
|
|
39
17
|
import { FeedbackProvider, type FeedbackData } from 'hocmai-feedback-widget';
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
console.log('Feedback submitted:', data);
|
|
47
|
-
// data.listImage contains URLs of uploaded images
|
|
48
|
-
// data.deviceInfo contains browser/OS info
|
|
49
|
-
// data.errorType contains the selected category
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<FeedbackProvider onSubmit={handleSubmit} themeColor="#3b82f6">
|
|
54
|
-
<YourAppContent />
|
|
55
|
-
</FeedbackProvider>
|
|
56
|
-
);
|
|
57
|
-
}
|
|
18
|
+
|
|
19
|
+
<FeedbackProvider onSubmit={async (data: FeedbackData) => {
|
|
20
|
+
await api.post('/questionReport/studentReport', data); // data.listImage là URL ảnh đã upload
|
|
21
|
+
}}>
|
|
22
|
+
<App />
|
|
23
|
+
</FeedbackProvider>
|
|
58
24
|
```
|
|
59
25
|
|
|
60
|
-
|
|
61
|
-
|
|
26
|
+
`onSubmit` **không bắt buộc** ở Provider — có thể truyền theo từng lần ở `openFeedback` (xem dưới).
|
|
27
|
+
|
|
28
|
+
### 2. Mở form từ component con bằng `useFeedback`
|
|
62
29
|
|
|
63
30
|
```tsx
|
|
64
31
|
import { useFeedback } from 'hocmai-feedback-widget';
|
|
65
32
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
Report Error
|
|
76
|
-
</button>
|
|
77
|
-
</div>
|
|
78
|
-
);
|
|
79
|
-
};
|
|
33
|
+
const { openFeedback } = useFeedback();
|
|
34
|
+
|
|
35
|
+
// Dùng onSubmit của Provider:
|
|
36
|
+
openFeedback(questionId, { questionIdChild });
|
|
37
|
+
|
|
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
|
+
});
|
|
80
42
|
```
|
|
81
43
|
|
|
82
|
-
### 3.
|
|
83
|
-
|
|
44
|
+
### 3. (Tùy chọn) Rich-text editor cho ô mô tả — `isCkEditor`
|
|
45
|
+
|
|
46
|
+
Bật `isCkEditor` **và** truyền `descriptionEditor` (editor đóng gói sẵn ở entry phụ):
|
|
84
47
|
|
|
85
48
|
```tsx
|
|
86
|
-
import {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
49
|
+
import { FeedbackProvider } from 'hocmai-feedback-widget';
|
|
50
|
+
import CkEditor from 'hocmai-feedback-widget/ckeditor';
|
|
51
|
+
|
|
52
|
+
<FeedbackProvider onSubmit={handleSubmit} isCkEditor descriptionEditor={CkEditor}>
|
|
53
|
+
<App />
|
|
54
|
+
</FeedbackProvider>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Phải cài thêm các peer deps (chỉ khi dùng CKEditor):
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
yarn add @ckeditor/ckeditor5-react hocmai-ckeditor-custom ckeditor5-classic-with-mathtype
|
|
96
61
|
```
|
|
97
62
|
|
|
98
|
-
|
|
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
|
+
```
|
|
99
71
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
|
105
|
-
|
|
72
|
+
## API
|
|
73
|
+
|
|
74
|
+
**`FeedbackProvider`**
|
|
75
|
+
|
|
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`). |
|
|
82
|
+
|
|
83
|
+
**`useFeedback()`** → `{ openFeedback, closeFeedback, isOpen, activeQuestionId }`
|
|
84
|
+
- `openFeedback(questionId, contextData?, onSubmit?)` — mở form; `onSubmit?` ghi đè handler của Provider cho lần này.
|
|
85
|
+
|
|
86
|
+
**`FeedbackData`**
|
|
106
87
|
|
|
107
|
-
### FeedbackData Interface
|
|
108
88
|
```ts
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
os: string;
|
|
118
|
-
browser: string;
|
|
119
|
-
screen: string;
|
|
120
|
-
userAgent: string;
|
|
121
|
-
};
|
|
122
|
-
contextData?: any;
|
|
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;
|
|
123
97
|
}
|
|
124
98
|
```
|
|
125
99
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
-
|
|
129
|
-
- `
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|------|------|---------|-------------|
|
|
134
|
-
| `questionId` | `string \| number` | Required | Main ID of the item being reported. |
|
|
135
|
-
| `onSubmit` | `function` | Required | Async callback receiving `FeedbackData`. |
|
|
136
|
-
| `contextData` | `object` | - | Additional data passed to `FeedbackData`. |
|
|
137
|
-
| `themeColor` | `string` | `#3b82f6` | Theme color. |
|
|
138
|
-
| `isOpen` | `boolean` | - | Control visibility manually. |
|
|
139
|
-
| `onClose` | `function` | - | Callback when closed. |
|
|
140
|
-
| `isEnableCaptureFull` | `boolean` | `true` | Enable full screen capture button. |
|
|
141
|
-
| `isEnableCaptureElement` | `boolean` | `true` | Enable crop/element capture button. |
|
|
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).
|
|
142
107
|
|
|
143
108
|
## License
|
|
144
|
-
MIT
|
|
109
|
+
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;
|
|
@@ -12,7 +13,8 @@ export interface FeedbackData {
|
|
|
12
13
|
export interface FeedbackProps {
|
|
13
14
|
questionId: string | number;
|
|
14
15
|
contextData?: Record<string, any>;
|
|
15
|
-
|
|
16
|
+
/** Submit handler. Optional — may be provided per call via openFeedback or at the Provider. */
|
|
17
|
+
onSubmit?: (data: FeedbackData) => Promise<void> | void;
|
|
16
18
|
themeColor?: string;
|
|
17
19
|
triggerComponent?: React.ReactNode;
|
|
18
20
|
isOpen?: boolean;
|
|
@@ -20,6 +22,16 @@ export interface FeedbackProps {
|
|
|
20
22
|
onClose?: () => void;
|
|
21
23
|
isEnableCaptureFull?: boolean;
|
|
22
24
|
isEnableCaptureElement?: boolean;
|
|
25
|
+
/** Endpoint that accepts { base64 } and returns { file_url }. */
|
|
26
|
+
uploadUrl?: string;
|
|
27
|
+
/** Use the rich-text editor for the description instead of a plain textarea. */
|
|
28
|
+
isCkEditor?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Rich-text editor component for the description (used when isCkEditor is true).
|
|
31
|
+
* Pass the ready-made editor from `hocmai-feedback-widget/ckeditor`, or your own.
|
|
32
|
+
* Kept as a prop so the core bundle has no CKEditor dependency.
|
|
33
|
+
*/
|
|
34
|
+
descriptionEditor?: React.ComponentType<CkEditorProps>;
|
|
23
35
|
}
|
|
24
36
|
declare const FeedbackWidget: React.FC<FeedbackProps>;
|
|
25
37
|
export default FeedbackWidget;
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { default as React, ReactNode } from 'react';
|
|
2
2
|
import { FeedbackData } from '../components/FeedbackWidget';
|
|
3
|
+
import { CkEditorProps } from '../components/CkEditor';
|
|
4
|
+
export type FeedbackSubmitHandler = (data: FeedbackData) => Promise<void> | void;
|
|
3
5
|
interface FeedbackContextType {
|
|
4
|
-
|
|
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;
|
|
5
12
|
closeFeedback: () => void;
|
|
6
13
|
isOpen: boolean;
|
|
7
14
|
activeQuestionId: string | number | null;
|
|
@@ -9,8 +16,13 @@ interface FeedbackContextType {
|
|
|
9
16
|
export declare const useFeedback: () => FeedbackContextType;
|
|
10
17
|
interface FeedbackProviderProps {
|
|
11
18
|
children: ReactNode;
|
|
12
|
-
|
|
19
|
+
/** Default submit handler. Optional — you can instead pass one per call via openFeedback. */
|
|
20
|
+
onSubmit?: FeedbackSubmitHandler;
|
|
13
21
|
themeColor?: string;
|
|
22
|
+
/** Use the rich-text editor for the description instead of a plain textarea. */
|
|
23
|
+
isCkEditor?: boolean;
|
|
24
|
+
/** Editor component for the description (from `hocmai-feedback-widget/ckeditor` or your own). */
|
|
25
|
+
descriptionEditor?: React.ComponentType<CkEditorProps>;
|
|
14
26
|
}
|
|
15
27
|
export declare const FeedbackProvider: React.FC<FeedbackProviderProps>;
|
|
16
28
|
export {};
|