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.
Files changed (59) hide show
  1. package/README.md +78 -113
  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 +13 -1
  11. package/dist/context/FeedbackContext.d.ts +14 -2
  12. package/dist/hocmai-feedback-widget.mjs +7550 -0
  13. package/dist/hocmai-feedback-widget.mjs.map +1 -0
  14. package/dist/index.d.ts +4 -2
  15. package/package.json +39 -29
  16. package/src/App.tsx +147 -58
  17. package/src/capture/device-detection.ts +52 -0
  18. package/src/capture/fonts.ts +34 -0
  19. package/src/capture/index.ts +361 -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 +105 -93
  26. package/src/context/FeedbackContext.tsx +49 -9
  27. package/src/index.ts +9 -3
  28. package/src/main.tsx +7 -9
  29. package/src/utils/device.ts +14 -14
  30. package/dist/support-feedback-lib.es.js +0 -3946
  31. package/dist/support-feedback-lib.umd.js +0 -84
  32. package/dist/test-mobile-screenshot.html +0 -211
  33. package/dist/test-simple-capture.html +0 -107
  34. package/dist/utils/screenshot/background-renderer.d.ts +0 -3
  35. package/dist/utils/screenshot/canvas-renderer.d.ts +0 -9
  36. package/dist/utils/screenshot/cleanup.d.ts +0 -3
  37. package/dist/utils/screenshot/device-detection.d.ts +0 -16
  38. package/dist/utils/screenshot/element-collector.d.ts +0 -10
  39. package/dist/utils/screenshot/image-renderer.d.ts +0 -21
  40. package/dist/utils/screenshot/performance.d.ts +0 -3
  41. package/dist/utils/screenshot/svg-renderer.d.ts +0 -24
  42. package/dist/utils/screenshot/text-renderer.d.ts +0 -23
  43. package/dist/utils/screenshot/types.d.ts +0 -35
  44. package/dist/utils/screenshot.d.ts +0 -9
  45. package/dist/vite.svg +0 -1
  46. package/src/App.css +0 -42
  47. package/src/assets/react.svg +0 -1
  48. package/src/index.css +0 -68
  49. package/src/utils/screenshot/background-renderer.ts +0 -54
  50. package/src/utils/screenshot/canvas-renderer.ts +0 -343
  51. package/src/utils/screenshot/cleanup.ts +0 -32
  52. package/src/utils/screenshot/device-detection.ts +0 -90
  53. package/src/utils/screenshot/element-collector.ts +0 -134
  54. package/src/utils/screenshot/image-renderer.ts +0 -148
  55. package/src/utils/screenshot/performance.ts +0 -57
  56. package/src/utils/screenshot/svg-renderer.ts +0 -149
  57. package/src/utils/screenshot/text-renderer.ts +0 -212
  58. package/src/utils/screenshot/types.ts +0 -40
  59. package/src/utils/screenshot.ts +0 -83
package/README.md CHANGED
@@ -1,144 +1,109 @@
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**. ESM, tương thích React ≥ 16.8.
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
+ ## Cài đặt
12
6
 
13
- ## Installation
14
-
15
- ### Via NPM/Yarn
16
7
  ```bash
17
8
  npm install hocmai-feedback-widget
18
- # or
19
- yarn add hocmai-feedback-widget
9
+ # peer deps: react, react-dom
20
10
  ```
21
11
 
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
- ```
32
-
33
- ## Usage
12
+ ## Sử dụng
34
13
 
35
- ### 1. Wrap your application
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
- // If styles are not automatically injected by your bundler, you might need:
41
- // import 'hocmai-feedback-widget/dist/style.css';
42
-
43
- function App() {
44
- 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
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
- ### 2. Trigger Feedback Form
61
- Use the `useFeedback` hook anywhere in your child components to open the widget for a specific item.
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 QuestionItem = ({ id, content }) => {
67
- const { openFeedback } = useFeedback();
68
-
69
- 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>
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. Standalone Component
83
- If you need a single widget instance and manage state manually (legacy mode):
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 { 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
- />
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
- ## Props & Configuration
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
- ### 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. |
72
+ ## API
73
+
74
+ **`FeedbackProvider`**
75
+
76
+ | Prop | Kiểu | 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
- 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;
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
- ### 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. |
100
+ ## Lưu ý
101
+
102
+ - **onSubmit** ưu tiên: handler theo lần gọi handler Provider không 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,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;
@@ -12,7 +13,8 @@ export interface FeedbackData {
12
13
  export interface FeedbackProps {
13
14
  questionId: string | number;
14
15
  contextData?: Record<string, any>;
15
- onSubmit: (data: FeedbackData) => Promise<void>;
16
+ /** Submit handler. Optional — may be provided per call via openFeedback or at the Provider. */
17
+ onSubmit?: (data: FeedbackData) => Promise<void> | void;
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
- openFeedback: (questionId: string | number, contextData?: any) => void;
6
+ /**
7
+ * Open the report form.
8
+ * @param onSubmit optional per-call handler; overrides the Provider's onSubmit
9
+ * for this submission. Lets you handle submit from anywhere.
10
+ */
11
+ openFeedback: (questionId: string | number, contextData?: any, onSubmit?: FeedbackSubmitHandler) => void;
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
- onSubmit: (data: FeedbackData) => Promise<void>;
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 {};