hocmai-feedback-widget 0.2.0 → 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.
Files changed (57) hide show
  1. package/README.md +132 -97
  2. package/dist/capture/device-detection.d.ts +13 -0
  3. package/dist/capture/fonts.d.ts +4 -0
  4. package/dist/capture/index.d.ts +14 -0
  5. package/dist/capture/mathjax-fonts.d.ts +5 -0
  6. package/dist/capture/types.d.ts +25 -0
  7. package/dist/ckeditor.mjs +113 -0
  8. package/dist/ckeditor.mjs.map +1 -0
  9. package/dist/components/CkEditor.d.ts +14 -0
  10. package/dist/components/FeedbackWidget.d.ts +11 -0
  11. package/dist/context/FeedbackContext.d.ts +5 -0
  12. package/dist/hocmai-feedback-widget.mjs +7544 -0
  13. package/dist/hocmai-feedback-widget.mjs.map +1 -0
  14. package/dist/index.d.ts +3 -0
  15. package/package.json +32 -28
  16. package/src/App.tsx +143 -58
  17. package/src/capture/device-detection.ts +52 -0
  18. package/src/capture/fonts.ts +34 -0
  19. package/src/capture/index.ts +362 -0
  20. package/src/capture/mathjax-fonts.ts +143 -0
  21. package/src/capture/types.ts +34 -0
  22. package/src/ckeditor.d.ts +14 -0
  23. package/src/components/CkEditor.tsx +130 -0
  24. package/src/components/CropOverlay.tsx +29 -47
  25. package/src/components/FeedbackWidget.tsx +96 -90
  26. package/src/context/FeedbackContext.tsx +11 -4
  27. package/src/index.ts +9 -0
  28. package/src/main.tsx +7 -8
  29. package/src/utils/device.ts +14 -14
  30. package/dist/support-feedback-lib.es.js +0 -3867
  31. package/dist/support-feedback-lib.umd.js +0 -84
  32. package/dist/utils/screenshot/background-renderer.d.ts +0 -3
  33. package/dist/utils/screenshot/canvas-renderer.d.ts +0 -9
  34. package/dist/utils/screenshot/cleanup.d.ts +0 -3
  35. package/dist/utils/screenshot/device-detection.d.ts +0 -2
  36. package/dist/utils/screenshot/element-collector.d.ts +0 -10
  37. package/dist/utils/screenshot/image-renderer.d.ts +0 -21
  38. package/dist/utils/screenshot/performance.d.ts +0 -3
  39. package/dist/utils/screenshot/svg-renderer.d.ts +0 -24
  40. package/dist/utils/screenshot/text-renderer.d.ts +0 -23
  41. package/dist/utils/screenshot/types.d.ts +0 -35
  42. package/dist/utils/screenshot.d.ts +0 -9
  43. package/dist/vite.svg +0 -1
  44. package/src/App.css +0 -42
  45. package/src/assets/react.svg +0 -1
  46. package/src/index.css +0 -68
  47. package/src/utils/screenshot/background-renderer.ts +0 -54
  48. package/src/utils/screenshot/canvas-renderer.ts +0 -217
  49. package/src/utils/screenshot/cleanup.ts +0 -32
  50. package/src/utils/screenshot/device-detection.ts +0 -38
  51. package/src/utils/screenshot/element-collector.ts +0 -83
  52. package/src/utils/screenshot/image-renderer.ts +0 -148
  53. package/src/utils/screenshot/performance.ts +0 -57
  54. package/src/utils/screenshot/svg-renderer.ts +0 -126
  55. package/src/utils/screenshot/text-renderer.ts +0 -212
  56. package/src/utils/screenshot/types.ts +0 -40
  57. package/src/utils/screenshot.ts +0 -68
package/README.md CHANGED
@@ -1,144 +1,179 @@
1
- # Support Feedback Widget
1
+ # hocmai-feedback-widget
2
2
 
3
- A React library for collecting user feedback with screenshot capabilities, context support, and interactive region cropping.
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
- ## Features
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
- ## Installation
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
- # or
25
+ # hoặc
19
26
  yarn add hocmai-feedback-widget
20
27
  ```
21
28
 
22
- ### Local Installation
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
- ## Usage
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
- ### 1. Wrap your application
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
- // Send data to your API
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
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
- <YourAppContent />
51
+ <Component {...pageProps} />
55
52
  </FeedbackProvider>
56
53
  );
57
54
  }
58
55
  ```
59
56
 
60
- ### 2. Trigger Feedback Form
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 = ({ id, content }) => {
61
+ const QuestionItem = ({ question }) => {
67
62
  const { openFeedback } = useFeedback();
68
-
69
63
  return (
70
- <div className="card">
71
- <p>{content}</p>
72
- <button
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. Standalone Component
83
- If you need a single widget instance and manage state manually (legacy mode):
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
- ```tsx
86
- import { FeedbackWidget } from 'hocmai-feedback-widget';
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
- ## Props & Configuration
80
+ ## API
99
81
 
100
- ### FeedbackProvider
101
- | Prop | Type | Required | Description |
102
- |------|------|----------|-------------|
103
- | `onSubmit` | `(data: FeedbackData) => Promise<void>` | Yes | Callback when feedback is sent. |
104
- | `themeColor` | `string` | No | Primary color for buttons/highlights. Default: `#3b82f6`. |
105
- | `children` | `ReactNode` | Yes | Your application content. |
82
+ ### `FeedbackProvider`
83
+ | Prop | Kiểu | Mặc định | 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
- ### FeedbackData Interface
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
- idQuestion: string | number;
111
- idChildQuestion?: string | number;
112
- errorDescription: string;
113
- errorType: string;
114
- listImage: string[]; // Array of image URLs
115
- deviceInfo: {
116
- device: string;
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
- ### useFeedback Hook
127
- Returns an object with:
128
- - `openFeedback(questionId: string | number, contextData?: any)`: Opens the form. `contextData` can include `questionIdChild` which will pre-fill the child question ID field.
129
- - `closeFeedback()`: Closes the form.
130
-
131
- ### FeedbackWidget Props
132
- | Prop | Type | Default | Description |
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. |
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 ô 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,5 @@
1
+ /**
2
+ * Fetch MathJax fonts and inject them as data-URI @font-face. Idempotent and
3
+ * cached. No-op if there are no MathJax CHTML font faces (e.g. SVG output).
4
+ */
5
+ export declare function inlineMathJaxFonts(): Promise<void>;
@@ -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>&nbsp;</p>", "").replaceAll("<br>&nbsp;", "<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>&nbsp;</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>&nbsp;</p>', '')\n .replaceAll('<br>&nbsp;', '<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>&nbsp;</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 {};