phx-react 1.3.1632 → 1.3.1639

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 (35) hide show
  1. package/dist/cjs/components/TableReport/TableReport.js +1 -1
  2. package/dist/cjs/components/TableReport/TableReport.js.map +1 -1
  3. package/dist/cjs/index.d.ts +1 -2
  4. package/dist/cjs/index.js +3 -5
  5. package/dist/cjs/index.js.map +1 -1
  6. package/dist/esm/components/TableReport/TableReport.js +1 -1
  7. package/dist/esm/components/TableReport/TableReport.js.map +1 -1
  8. package/dist/esm/index.d.ts +1 -2
  9. package/dist/esm/index.js +1 -2
  10. package/dist/esm/index.js.map +1 -1
  11. package/package.json +5 -3
  12. package/dist/cjs/components/TextEditor/TextEditor.d.ts +0 -16
  13. package/dist/cjs/components/TextEditor/TextEditor.js +0 -866
  14. package/dist/cjs/components/TextEditor/TextEditor.js.map +0 -1
  15. package/dist/cjs/components/TextEditor/custom/CustomImage.d.ts +0 -3
  16. package/dist/cjs/components/TextEditor/custom/CustomImage.js +0 -86
  17. package/dist/cjs/components/TextEditor/custom/CustomImage.js.map +0 -1
  18. package/dist/cjs/components/TextEditor/custom/SelectionHighlight.d.ts +0 -2
  19. package/dist/cjs/components/TextEditor/custom/SelectionHighlight.js +0 -59
  20. package/dist/cjs/components/TextEditor/custom/SelectionHighlight.js.map +0 -1
  21. package/dist/cjs/components/TextEditor/editor.constant.d.ts +0 -28
  22. package/dist/cjs/components/TextEditor/editor.constant.js +0 -131
  23. package/dist/cjs/components/TextEditor/editor.constant.js.map +0 -1
  24. package/dist/esm/components/TextEditor/TextEditor.d.ts +0 -16
  25. package/dist/esm/components/TextEditor/TextEditor.js +0 -862
  26. package/dist/esm/components/TextEditor/TextEditor.js.map +0 -1
  27. package/dist/esm/components/TextEditor/custom/CustomImage.d.ts +0 -3
  28. package/dist/esm/components/TextEditor/custom/CustomImage.js +0 -84
  29. package/dist/esm/components/TextEditor/custom/CustomImage.js.map +0 -1
  30. package/dist/esm/components/TextEditor/custom/SelectionHighlight.d.ts +0 -2
  31. package/dist/esm/components/TextEditor/custom/SelectionHighlight.js +0 -56
  32. package/dist/esm/components/TextEditor/custom/SelectionHighlight.js.map +0 -1
  33. package/dist/esm/components/TextEditor/editor.constant.d.ts +0 -28
  34. package/dist/esm/components/TextEditor/editor.constant.js +0 -128
  35. package/dist/esm/components/TextEditor/editor.constant.js.map +0 -1
@@ -1,866 +0,0 @@
1
- "use strict";
2
- 'use client';
3
- Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.default = TextEditor;
5
- const tslib_1 = require("tslib");
6
- const react_1 = tslib_1.__importDefault(require("react"));
7
- const react_2 = require("react");
8
- const react_3 = require("@tiptap/react");
9
- const starter_kit_1 = tslib_1.__importDefault(require("@tiptap/starter-kit"));
10
- const extension_underline_1 = tslib_1.__importDefault(require("@tiptap/extension-underline"));
11
- // import ImageExt from '@tiptap/extension-image'
12
- const extension_heading_1 = tslib_1.__importDefault(require("@tiptap/extension-heading"));
13
- const extension_link_1 = tslib_1.__importDefault(require("@tiptap/extension-link"));
14
- const extension_text_align_1 = tslib_1.__importDefault(require("@tiptap/extension-text-align"));
15
- const extension_text_style_1 = require("@tiptap/extension-text-style");
16
- const extension_color_1 = tslib_1.__importDefault(require("@tiptap/extension-color"));
17
- const react_colorful_1 = require("react-colorful");
18
- const editor_constant_1 = require("./editor.constant");
19
- require("katex/dist/katex.min.css");
20
- const react_hook_form_1 = require("react-hook-form");
21
- const Modal_1 = require("../Modal/Modal");
22
- const Input_1 = require("../Input/Input");
23
- const EditorStyle_1 = tslib_1.__importDefault(require("./styles/EditorStyle"));
24
- const emoji_picker_react_1 = tslib_1.__importStar(require("emoji-picker-react"));
25
- const extension_placeholder_1 = tslib_1.__importDefault(require("@tiptap/extension-placeholder"));
26
- const FormUpload_1 = require("../UploadFile/FormUpload");
27
- const CustomImage_1 = tslib_1.__importDefault(require("./custom/CustomImage"));
28
- const SelectionHighlight_1 = require("./custom/SelectionHighlight");
29
- function TextEditor({ initialData, onChange, apiCdnUpload, height, label, placeholder, disabled = false, }) {
30
- var _a;
31
- const [editor, setEditor] = (0, react_2.useState)(null);
32
- const [, forceUpdate] = (0, react_2.useState)(0);
33
- const [showColorPicker, setShowColorPicker] = (0, react_2.useState)(false);
34
- const [fontColor, setFontColor] = (0, react_2.useState)('#333333');
35
- const pickerRef = (0, react_2.useRef)(null);
36
- const toolbarRef = (0, react_2.useRef)(null);
37
- const emojiPickerRef = (0, react_2.useRef)(null);
38
- const [fontSizeInput, setFontSizeInput] = (0, react_2.useState)('');
39
- const [isEditingFontSize, setIsEditingFontSize] = (0, react_2.useState)(false);
40
- const [pickerPos, setPickerPos] = (0, react_2.useState)({
41
- left: 0,
42
- top: 0,
43
- });
44
- const [emojiPickerPos, setEmojiPickerPos] = (0, react_2.useState)({
45
- left: 0,
46
- top: 0,
47
- });
48
- const [showLinkInput, setShowLinkInput] = (0, react_2.useState)(false);
49
- const [disabledItems, setDisabledItems] = (0, react_2.useState)(new Set());
50
- const [showEmojiPicker, setShowEmojiPicker] = (0, react_2.useState)(false);
51
- const [openPdfModal, setOpenPdfModal] = (0, react_2.useState)(false);
52
- const [pdfFile, setPdfFile] = (0, react_2.useState)(null);
53
- const { formState: { errors }, register, handleSubmit, reset, } = (0, react_hook_form_1.useForm)({
54
- defaultValues: {
55
- link: '',
56
- },
57
- });
58
- const [loadingConvertPdf, setLoadingConvertPdf] = (0, react_2.useState)(false);
59
- // eslint-disable-next-line no-useless-escape
60
- const regexUrl = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
61
- const normalizeColor = (val) => {
62
- try {
63
- if (val.startsWith('#')) {
64
- return val;
65
- }
66
- if (val.startsWith('rgba')) {
67
- const [r, g, b, a] = val
68
- .replace(/rgba\(/, '')
69
- .replace(/\)/, '')
70
- .split(',')
71
- .map((v) => v.trim());
72
- const rn = parseInt(r, 10).toString(16).padStart(2, '0');
73
- const gn = parseInt(g, 10).toString(16).padStart(2, '0');
74
- const bn = parseInt(b, 10).toString(16).padStart(2, '0');
75
- const an = Math.round(parseFloat(a) * 255)
76
- .toString(16)
77
- .padStart(2, '0');
78
- return `#${rn}${gn}${bn}${an}`;
79
- }
80
- if (val.startsWith('rgb')) {
81
- const [r, g, b] = val
82
- .replace(/rgb\(/, '')
83
- .replace(/\)/, '')
84
- .split(',')
85
- .map((v) => v.trim());
86
- const rn = parseInt(r, 10).toString(16).padStart(2, '0');
87
- const gn = parseInt(g, 10).toString(16).padStart(2, '0');
88
- const bn = parseInt(b, 10).toString(16).padStart(2, '0');
89
- return `#${rn}${gn}${bn}`;
90
- }
91
- return val;
92
- }
93
- catch (_a) {
94
- return val;
95
- }
96
- };
97
- const getCurrentSelectionFontSize = (0, react_2.useCallback)(() => {
98
- var _a;
99
- try {
100
- const attrSize = (_a = editor === null || editor === void 0 ? void 0 : editor.getAttributes('textStyle')) === null || _a === void 0 ? void 0 : _a.fontSize;
101
- if (attrSize)
102
- return attrSize;
103
- const sel = window.getSelection();
104
- const node = sel === null || sel === void 0 ? void 0 : sel.anchorNode;
105
- let el = null;
106
- if (node) {
107
- el = node.nodeType === 1 ? node : node.parentElement;
108
- }
109
- if (el) {
110
- const size = window.getComputedStyle(el).fontSize;
111
- if (size)
112
- return size;
113
- }
114
- return '';
115
- }
116
- catch (_b) {
117
- return '';
118
- }
119
- }, [editor]);
120
- const isHtmlEmpty = (html) => {
121
- const hasImage = /<img\b[^>]*>/i.test(html);
122
- const text = html
123
- .replace(/<[^>]*>/g, '')
124
- .replace(/&nbsp;/g, '')
125
- .trim();
126
- return text === '' && !hasImage;
127
- };
128
- const getCurrentSelectionColor = () => {
129
- var _a;
130
- try {
131
- const attrColor = (_a = editor === null || editor === void 0 ? void 0 : editor.getAttributes('textStyle')) === null || _a === void 0 ? void 0 : _a.color;
132
- if (attrColor) {
133
- return normalizeColor(attrColor);
134
- }
135
- const sel = window.getSelection();
136
- const node = sel === null || sel === void 0 ? void 0 : sel.anchorNode;
137
- let el = null;
138
- if (node) {
139
- el = node.nodeType === 1 ? node : node.parentElement;
140
- }
141
- if (el) {
142
- const color = window.getComputedStyle(el).color;
143
- if (color)
144
- return normalizeColor(color);
145
- }
146
- return null;
147
- }
148
- catch (_b) {
149
- return null;
150
- }
151
- };
152
- const uploadImageFunction = async (formData) => {
153
- const response = await fetch(apiCdnUpload, {
154
- method: 'POST',
155
- body: formData,
156
- });
157
- if (response.ok) {
158
- return response.json();
159
- }
160
- };
161
- const handleConvertPdfToImage = async (file) => {
162
- setLoadingConvertPdf(true);
163
- try {
164
- const PDFJS = window.pdfjsLib;
165
- const fileURL = URL.createObjectURL(file);
166
- const pdfDoc = await PDFJS.getDocument({ url: fileURL }).promise;
167
- const { numPages } = pdfDoc;
168
- const uploadTasks = [];
169
- for (let i = 1; i <= numPages; i++) {
170
- uploadTasks.push(async () => {
171
- const page = await pdfDoc.getPage(i);
172
- const viewport = page.getViewport({ scale: 2 });
173
- const canvas = document.createElement('canvas');
174
- const context = canvas.getContext('2d');
175
- canvas.width = viewport.width;
176
- canvas.height = viewport.height;
177
- await page.render({ canvasContext: context, viewport }).promise;
178
- const blob = await new Promise((resolve) => canvas.toBlob((b) => resolve(b), 'image/webp'));
179
- const fileName = `${file.name.replace(/\.[^/.]+$/, '')}-page-${i}.webp`;
180
- const imageFile = new File([blob], fileName, { type: 'image/webp' });
181
- const link = await uploadImage(imageFile);
182
- return { index: i, link };
183
- });
184
- }
185
- const results = [];
186
- const BATCH_SIZE = 5;
187
- const totalUploadTasks = uploadTasks.length;
188
- for (let i = 0; i < totalUploadTasks; i += BATCH_SIZE) {
189
- const batch = uploadTasks.slice(i, i + BATCH_SIZE);
190
- const batchResults = await Promise.all(batch.map((fn) => fn()));
191
- results.push(...batchResults);
192
- }
193
- const sorted = results.filter((r) => r.link).sort((a, b) => a.index - b.index);
194
- const content = sorted.map(({ link }) => ({
195
- type: 'image',
196
- attrs: { src: link },
197
- }));
198
- editor === null || editor === void 0 ? void 0 : editor.chain().focus().insertContent(content).run();
199
- }
200
- finally {
201
- setLoadingConvertPdf(false);
202
- setOpenPdfModal(false);
203
- setPdfFile(null);
204
- }
205
- };
206
- const normalizeHtml = (html) => {
207
- return html.replace(/<p><\/p>/g, editor_constant_1.emptyParagraph);
208
- };
209
- (0, react_2.useEffect)(() => {
210
- const e = new react_3.Editor({
211
- content: initialData,
212
- extensions: [
213
- extension_placeholder_1.default.configure({
214
- placeholder: placeholder,
215
- }),
216
- extension_text_style_1.TextStyle,
217
- extension_text_style_1.FontSize,
218
- extension_color_1.default,
219
- starter_kit_1.default.configure({
220
- heading: false,
221
- }),
222
- extension_underline_1.default,
223
- CustomImage_1.default,
224
- extension_heading_1.default.configure({ levels: [1, 2, 3, 4] }),
225
- extension_link_1.default.configure({ openOnClick: false }),
226
- extension_text_align_1.default.configure({
227
- types: ['heading', 'paragraph', 'image'],
228
- }),
229
- SelectionHighlight_1.SelectionHighlight,
230
- ],
231
- editorProps: {
232
- attributes: {
233
- class: `prose editor-height px-3 py-1.5 focus:outline-none border border-t-0 border-gray-300 rounded-b-md bg-white overflow-y-auto`,
234
- },
235
- },
236
- });
237
- e.on('transaction', () => {
238
- forceUpdate((n) => n + 1);
239
- });
240
- e.on('update', ({ editor }) => {
241
- const content = normalizeHtml(editor.getHTML());
242
- const isEmpty = isHtmlEmpty(content);
243
- onChange === null || onChange === void 0 ? void 0 : onChange(!isEmpty ? content : '');
244
- });
245
- setEditor(e);
246
- return () => {
247
- e.destroy();
248
- };
249
- }, []);
250
- (0, react_2.useEffect)(() => {
251
- if (!editor)
252
- return;
253
- const dom = editor.view.dom;
254
- dom.classList.toggle('bg-gray-200', disabled);
255
- dom.classList.toggle('bg-white', !disabled);
256
- dom.classList.toggle('cursor-not-allowed', disabled);
257
- editor.setEditable(!disabled);
258
- }, [editor, disabled]);
259
- (0, react_2.useEffect)(() => {
260
- if (!editor)
261
- return;
262
- const updateDisabledItems = () => {
263
- const disabled = new Set();
264
- if (editor.isActive('blockquote')) {
265
- disabled.add('Văn bản');
266
- }
267
- if (editor.isActive('heading')) {
268
- disabled.add('Trích dẫn');
269
- }
270
- setDisabledItems(disabled);
271
- };
272
- updateDisabledItems();
273
- editor.on('selectionUpdate', updateDisabledItems);
274
- editor.on('transaction', updateDisabledItems);
275
- return () => {
276
- editor.off('selectionUpdate', updateDisabledItems);
277
- editor.off('transaction', updateDisabledItems);
278
- };
279
- }, [editor]);
280
- (0, react_2.useEffect)(() => {
281
- if (!editor)
282
- return;
283
- const handlePaste = async (event) => {
284
- var _a;
285
- const items = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.items;
286
- if (!items)
287
- return;
288
- event.preventDefault();
289
- for (let i = 0; i < items.length; i++) {
290
- const item = items[i];
291
- if (item.type.indexOf('image') !== -1) {
292
- const file = item.getAsFile();
293
- if (!file)
294
- continue;
295
- const tempId = `img-${Date.now()}`;
296
- const tempUrl = URL.createObjectURL(file);
297
- try {
298
- editor === null || editor === void 0 ? void 0 : editor.chain().focus().insertContent({
299
- type: 'image',
300
- attrs: {
301
- src: tempUrl,
302
- 'data-loading': 'true',
303
- 'data-temp-id': tempId,
304
- },
305
- }).run();
306
- uploadImage(file)
307
- .then((res) => {
308
- if (!res)
309
- throw new Error('Upload failed');
310
- editor === null || editor === void 0 ? void 0 : editor.chain().command(({ tr }) => {
311
- let targetPos = null;
312
- tr.doc.descendants((node, pos) => {
313
- if (node.type.name === 'image' && node.attrs['data-temp-id'] === tempId) {
314
- targetPos = pos;
315
- return false;
316
- }
317
- return true;
318
- });
319
- if (targetPos !== null) {
320
- const node = tr.doc.nodeAt(targetPos);
321
- if (node) {
322
- tr.setNodeMarkup(targetPos, undefined, {
323
- ...node.attrs,
324
- src: res,
325
- 'data-loading': null,
326
- 'data-temp-id': null,
327
- });
328
- return true;
329
- }
330
- }
331
- return false;
332
- }).run();
333
- })
334
- .catch((error) => {
335
- console.error('Failed to upload pasted image:', error);
336
- editor === null || editor === void 0 ? void 0 : editor.chain().command(({ tr }) => {
337
- let targetPos = null;
338
- tr.doc.descendants((node, pos) => {
339
- if (node.type.name === 'image' && node.attrs['data-temp-id'] === tempId) {
340
- targetPos = pos;
341
- return false;
342
- }
343
- return true;
344
- });
345
- if (targetPos !== null) {
346
- tr.delete(targetPos, targetPos + 1);
347
- }
348
- return true;
349
- }).run();
350
- })
351
- .finally(() => {
352
- URL.revokeObjectURL(tempUrl);
353
- });
354
- }
355
- catch (error) {
356
- console.error('Error handling pasted image:', error);
357
- URL.revokeObjectURL(tempUrl);
358
- }
359
- break;
360
- }
361
- }
362
- };
363
- const dom = editor.view.dom;
364
- dom.addEventListener('paste', handlePaste);
365
- return () => {
366
- dom.removeEventListener('paste', handlePaste);
367
- };
368
- }, [editor, getCurrentSelectionFontSize]);
369
- (0, react_2.useEffect)(() => {
370
- if (!showColorPicker)
371
- return;
372
- const onDocMouseDown = (e) => {
373
- const node = pickerRef.current;
374
- const target = e.target;
375
- const isColorButton = target.closest('[data-color-button]');
376
- const isColorPicker = node === null || node === void 0 ? void 0 : node.contains(target);
377
- if (node && !isColorPicker && !isColorButton) {
378
- setShowColorPicker(false);
379
- // @ts-ignore
380
- editor === null || editor === void 0 ? void 0 : editor.commands.clearSelectionHighlight();
381
- }
382
- };
383
- document.addEventListener('mousedown', onDocMouseDown);
384
- return () => {
385
- document.removeEventListener('mousedown', onDocMouseDown);
386
- };
387
- }, [showColorPicker]);
388
- (0, react_2.useEffect)(() => {
389
- if (!showEmojiPicker)
390
- return;
391
- const onDocMouseDown = (e) => {
392
- const node = emojiPickerRef.current;
393
- const target = e.target;
394
- const isEmojiPicker = node === null || node === void 0 ? void 0 : node.contains(target);
395
- if (node && !isEmojiPicker) {
396
- setShowEmojiPicker(false);
397
- }
398
- };
399
- document.addEventListener('mousedown', onDocMouseDown);
400
- return () => {
401
- document.removeEventListener('mousedown', onDocMouseDown);
402
- };
403
- }, [showEmojiPicker]);
404
- (0, react_2.useEffect)(() => {
405
- if (!editor || !showColorPicker)
406
- return;
407
- const updateFromSelection = () => {
408
- const current = getCurrentSelectionColor();
409
- if (current)
410
- setFontColor(current);
411
- };
412
- updateFromSelection();
413
- editor.on('selectionUpdate', updateFromSelection);
414
- return () => {
415
- editor.off('selectionUpdate', updateFromSelection);
416
- };
417
- }, [editor, showColorPicker]);
418
- (0, react_2.useEffect)(() => {
419
- if (!editor)
420
- return;
421
- const onSelection = () => {
422
- if (!isEditingFontSize) {
423
- const cur = getCurrentSelectionFontSize();
424
- const m = cur.match(/^(\d+(?:\.\d+)?)/);
425
- setFontSizeInput(m ? m[1] : '');
426
- }
427
- forceUpdate((n) => n + 1);
428
- };
429
- editor.on('selectionUpdate', onSelection);
430
- return () => {
431
- editor.off('selectionUpdate', onSelection);
432
- };
433
- }, [editor, isEditingFontSize, getCurrentSelectionFontSize]);
434
- (0, react_2.useEffect)(() => {
435
- if (!window.pdfjsLib) {
436
- const script = document.createElement('script');
437
- script.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js';
438
- document.body.appendChild(script);
439
- }
440
- }, []);
441
- (0, react_2.useEffect)(() => {
442
- if (!editor)
443
- return;
444
- const cur = getCurrentSelectionFontSize();
445
- const m = cur.match(/^(\d+(?:\.\d+)?)/);
446
- setFontSizeInput(m ? m[1] : '');
447
- }, [editor]);
448
- if (!editor)
449
- return react_1.default.createElement("p", null, "Loading editor...");
450
- const addLink = () => {
451
- setShowLinkInput(true);
452
- };
453
- const handleInsertLink = handleSubmit((data) => {
454
- const { link } = data;
455
- editor === null || editor === void 0 ? void 0 : editor.chain().focus().extendMarkRange('link').setLink({ href: link }).run();
456
- setShowLinkInput(false);
457
- reset();
458
- });
459
- const onMouseDownToolbar = (e, item) => {
460
- var _a;
461
- e.preventDefault();
462
- const container = toolbarRef.current;
463
- const btnRect = e.currentTarget.getBoundingClientRect();
464
- const contRect = container === null || container === void 0 ? void 0 : container.getBoundingClientRect();
465
- if (item.name === 'Màu chữ') {
466
- // @ts-ignore
467
- editor === null || editor === void 0 ? void 0 : editor.commands.setSelectionHighlight();
468
- if (contRect) {
469
- setPickerPos({
470
- left: btnRect.left - contRect.left - 90,
471
- top: btnRect.bottom - contRect.top + 8,
472
- });
473
- }
474
- const current = getCurrentSelectionColor();
475
- if (current)
476
- setFontColor(current);
477
- setShowColorPicker(!showColorPicker);
478
- forceUpdate((x) => x + 1);
479
- }
480
- else if (item.name === 'Biểu tượng cảm xúc') {
481
- if (contRect) {
482
- setEmojiPickerPos({
483
- left: btnRect.left - contRect.left - 170,
484
- top: btnRect.bottom - contRect.top + 8,
485
- });
486
- }
487
- setShowEmojiPicker(!showEmojiPicker);
488
- e.stopPropagation();
489
- }
490
- else {
491
- (_a = item.action) === null || _a === void 0 ? void 0 : _a.call(item);
492
- forceUpdate((x) => x + 1);
493
- }
494
- };
495
- const fontSizeOptions = [
496
- { label: '12', value: '12px' },
497
- { label: '14', value: '14px' },
498
- { label: '16', value: '16px' },
499
- { label: '18', value: '18px' },
500
- { label: '20', value: '20px' },
501
- { label: '24', value: '24px' },
502
- { label: '28', value: '28px' },
503
- { label: '32', value: '32px' },
504
- { label: '36', value: '36px' },
505
- { label: '40', value: '40px' },
506
- { label: '44', value: '44px' },
507
- { label: '48', value: '48px' },
508
- { label: '52', value: '52px' },
509
- { label: '56', value: '56px' },
510
- { label: '60', value: '60px' },
511
- { label: '64', value: '64px' },
512
- { label: '72', value: '72px' },
513
- { label: '80', value: '80px' },
514
- { label: '96', value: '96px' },
515
- ];
516
- const paragraphOptions = [
517
- { label: 'Văn bản', value: 0 },
518
- { label: 'Tiêu đề 1', value: 1 },
519
- { label: 'Tiêu đề 2', value: 2 },
520
- { label: 'Tiêu đề 3', value: 3 },
521
- { label: 'Tiêu đề 4', value: 4 },
522
- ];
523
- const toolbarItems = [
524
- {
525
- name: 'Văn bản',
526
- isDisabled: () => disabledItems.has('Văn bản'),
527
- component: () => (react_1.default.createElement("select", { className: `w-24 text-xs leading-5 focus:ring-0 bg-transparent border-none rounded-md py-1 px-2 duration-200 ${disabledItems.has('Văn bản') ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:bg-gray-200'}`, disabled: disabledItems.has('Văn bản'), onChange: (e) => {
528
- const level = Number(e.target.value);
529
- if (level === 0) {
530
- editor.chain().focus().setParagraph().run();
531
- }
532
- else {
533
- //@ts-expect-error: nothing
534
- editor.chain().focus().setHeading({ level }).run();
535
- }
536
- }, value: (() => {
537
- if (editor.isActive('heading', { level: 1 }))
538
- return 1;
539
- if (editor.isActive('heading', { level: 2 }))
540
- return 2;
541
- if (editor.isActive('heading', { level: 3 }))
542
- return 3;
543
- if (editor.isActive('heading', { level: 4 }))
544
- return 4;
545
- return 0;
546
- })() }, paragraphOptions.map((option) => (react_1.default.createElement("option", { key: option.value, value: option.value }, option.label))))),
547
- borderRight: true,
548
- },
549
- {
550
- name: 'Giảm cỡ chữ',
551
- action: () => {
552
- const currentSize = parseInt(fontSizeInput) || 12;
553
- const newSize = Math.max(1, currentSize - 1);
554
- setFontSizeInput(String(newSize));
555
- editor.chain().focus().setFontSize(`${newSize}px`).run();
556
- forceUpdate((x) => x + 1);
557
- },
558
- icon: editor_constant_1.toolbar_svg.minus,
559
- },
560
- {
561
- name: 'Cỡ chữ',
562
- component: () => (react_1.default.createElement("div", { className: 'relative flex items-center' },
563
- react_1.default.createElement("input", { type: 'number', className: 'w-10 p-0 text-xs leading-5 text-center bg-transparent bg-white border-gray-500 rounded-md focus:ring-0 focus:border-gray-500', value: fontSizeInput || isEditingFontSize ? fontSizeInput : 12, onFocus: () => {
564
- setIsEditingFontSize(true);
565
- const cur = getCurrentSelectionFontSize();
566
- const m = cur.match(/^(\d+(?:\.\d+)?)/);
567
- setFontSizeInput(m ? m[1] : '');
568
- }, onChange: (e) => {
569
- setFontSizeInput(e.target.value);
570
- }, onBlur: (e) => {
571
- requestAnimationFrame(() => setIsEditingFontSize(false));
572
- const raw = e.target.value.trim();
573
- if (!raw) {
574
- setFontSizeInput('');
575
- editor.chain().focus().setMark('textStyle', { fontSize: '12px' }).run();
576
- }
577
- else {
578
- let num = Number(raw);
579
- if (!Number.isNaN(num) && num > 0) {
580
- num = Math.min(500, num);
581
- editor.chain().focus().setFontSize(`${num}px`).run();
582
- setFontSizeInput(num.toString());
583
- }
584
- else {
585
- setFontSizeInput('12');
586
- editor.chain().focus().setFontSize('12px').run();
587
- }
588
- }
589
- }, onKeyDown: (e) => {
590
- if (e.key === 'Enter') {
591
- // eslint-disable-next-line @typescript-eslint/no-extra-semi
592
- ;
593
- e.target.blur();
594
- }
595
- } }),
596
- isEditingFontSize && (react_1.default.createElement("div", { className: 'absolute left-0 top-[110%] z-50 w-16 max-h-60 overflow-auto rounded-md border bg-white shadow' }, fontSizeOptions.map((option) => (react_1.default.createElement("button", { key: option.value, type: 'button', className: 'w-full px-2 py-1 text-left text-[12px] hover:bg-gray-100', onMouseDown: (e) => {
597
- e.preventDefault();
598
- const numMatch = option.value.match(/^(\d+(?:\.\d+)?)/);
599
- setFontSizeInput(numMatch ? numMatch[1] : '');
600
- editor.chain().focus().setFontSize(option.value).run();
601
- setIsEditingFontSize(false);
602
- } }, option.label))))))),
603
- },
604
- {
605
- name: 'Tăng cỡ chữ',
606
- icon: editor_constant_1.toolbar_svg.plus,
607
- borderRight: true,
608
- action: () => {
609
- const currentSize = parseInt(fontSizeInput) || 12;
610
- const newSize = Math.min(500, currentSize + 1);
611
- setFontSizeInput(String(newSize));
612
- editor.chain().focus().setFontSize(`${newSize}px`).run();
613
- forceUpdate((x) => x + 1);
614
- },
615
- },
616
- {
617
- name: 'In đậm',
618
- icon: editor_constant_1.toolbar_svg.bold,
619
- isActive: () => editor.isActive('bold'),
620
- action: () => editor.chain().focus().toggleBold().run(),
621
- },
622
- {
623
- name: 'In nghiêng',
624
- icon: editor_constant_1.toolbar_svg.italic,
625
- isActive: () => editor.isActive('italic'),
626
- action: () => editor.chain().focus().toggleItalic().run(),
627
- },
628
- {
629
- name: 'Gạch chân',
630
- icon: editor_constant_1.toolbar_svg.underline,
631
- isActive: () => editor.isActive('underline'),
632
- action: () => editor.chain().focus().toggleUnderline().run(),
633
- },
634
- {
635
- name: 'Màu chữ',
636
- icon: editor_constant_1.toolbar_svg.textColor,
637
- action: () => setShowColorPicker((s) => !s),
638
- isActive: () => showColorPicker,
639
- borderRight: true,
640
- },
641
- {
642
- name: 'Biểu tượng cảm xúc',
643
- icon: editor_constant_1.toolbar_svg.emoji,
644
- action: () => setShowEmojiPicker(!showEmojiPicker),
645
- borderRight: true,
646
- },
647
- {
648
- name: 'Căn trái',
649
- icon: editor_constant_1.toolbar_svg.alignLeft,
650
- action: () => editor.chain().focus().setTextAlign('left').run(),
651
- isActive: () => {
652
- const isLeft = editor.isActive({ textAlign: 'left' });
653
- const isCenter = editor.isActive({ textAlign: 'center' });
654
- const isRight = editor.isActive({ textAlign: 'right' });
655
- const isJustify = editor.isActive({ textAlign: 'justify' });
656
- const noAlign = !isLeft && !isCenter && !isRight && !isJustify;
657
- return isLeft || noAlign;
658
- },
659
- },
660
- {
661
- name: 'Căn giữa',
662
- icon: editor_constant_1.toolbar_svg.alignMiddle,
663
- action: () => editor.chain().focus().setTextAlign('center').run(),
664
- isActive: () => editor.isActive({ textAlign: 'center' }),
665
- },
666
- {
667
- name: 'Căn phải',
668
- icon: editor_constant_1.toolbar_svg.alignRight,
669
- action: () => editor.chain().focus().setTextAlign('right').run(),
670
- isActive: () => editor.isActive({ textAlign: 'right' }),
671
- },
672
- {
673
- name: 'Căn đều',
674
- icon: editor_constant_1.toolbar_svg.alignJustify,
675
- action: () => editor.chain().focus().setTextAlign('justify').run(),
676
- isActive: () => editor.isActive({ textAlign: 'justify' }),
677
- borderRight: true,
678
- },
679
- {
680
- name: 'Trích dẫn',
681
- icon: editor_constant_1.toolbar_svg.quotes,
682
- isActive: () => editor.isActive('blockquote'),
683
- isDisabled: () => disabledItems.has('Trích dẫn'),
684
- action: () => editor.chain().focus().toggleBlockquote().run(),
685
- },
686
- {
687
- name: 'Danh sách không thứ tự',
688
- icon: editor_constant_1.toolbar_svg.unorderedList,
689
- isActive: () => editor.isActive('bulletList'),
690
- action: () => editor.chain().focus().toggleBulletList().run(),
691
- },
692
- {
693
- name: 'Danh sách có thứ tự',
694
- icon: editor_constant_1.toolbar_svg.orderList,
695
- isActive: () => editor.isActive('orderedList'),
696
- action: () => editor.chain().focus().toggleOrderedList().run(),
697
- borderRight: true,
698
- },
699
- {
700
- name: 'PDF',
701
- icon: editor_constant_1.toolbar_svg.pdf,
702
- action: () => setOpenPdfModal(true),
703
- },
704
- {
705
- name: 'Hình ảnh',
706
- icon: editor_constant_1.toolbar_svg.image,
707
- action: async () => {
708
- const input = document.createElement('input');
709
- input.type = 'file';
710
- input.accept = 'image/*';
711
- input.onchange = async () => {
712
- var _a;
713
- const file = (_a = input.files) === null || _a === void 0 ? void 0 : _a[0];
714
- if (file) {
715
- const tempId = `img-${Date.now()}`;
716
- editor === null || editor === void 0 ? void 0 : editor.chain().focus().insertContent({
717
- type: 'image',
718
- attrs: {
719
- src: URL.createObjectURL(file),
720
- 'data-loading': 'true',
721
- 'data-temp-id': tempId,
722
- style: 'display: block; margin: 0 auto; width: auto; height: auto;',
723
- },
724
- }).setTextSelection(editor.state.selection.to + 1).run();
725
- const res = await uploadImage(file);
726
- if (res) {
727
- editor === null || editor === void 0 ? void 0 : editor.chain().command(({ tr }) => {
728
- let targetPos = null;
729
- tr.doc.descendants((node, pos) => {
730
- if (node.type.name === 'image' && node.attrs['data-temp-id'] === tempId) {
731
- targetPos = pos;
732
- return false;
733
- }
734
- return true;
735
- });
736
- if (targetPos !== null) {
737
- const node = tr.doc.nodeAt(targetPos);
738
- if (node) {
739
- tr.setNodeMarkup(targetPos, undefined, {
740
- ...node.attrs,
741
- src: res,
742
- 'data-loading': null,
743
- 'data-temp-id': null,
744
- style: 'display: block; margin: 0 auto; width: auto; height: auto;',
745
- });
746
- }
747
- }
748
- return true;
749
- }).run();
750
- }
751
- else {
752
- editor === null || editor === void 0 ? void 0 : editor.chain().command(() => {
753
- var _a;
754
- const node = document.querySelector(`img[data-temp-id="${tempId}"]`);
755
- if (node) {
756
- (_a = node.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(node);
757
- }
758
- return true;
759
- }).run();
760
- }
761
- }
762
- };
763
- input.click();
764
- },
765
- },
766
- {
767
- name: 'Đường dẫn',
768
- icon: editor_constant_1.toolbar_svg.link,
769
- isActive: () => editor.isActive('link'),
770
- action: addLink,
771
- },
772
- ];
773
- const uploadImage = async (file) => {
774
- try {
775
- const formData = new FormData();
776
- formData.append('file', file);
777
- formData.append('projectId', 'test');
778
- formData.append('moduleId', 'test');
779
- const res = await uploadImageFunction(formData);
780
- if (res) {
781
- return res.link;
782
- }
783
- else {
784
- return null;
785
- }
786
- }
787
- catch (error) {
788
- console.error(error);
789
- return null;
790
- }
791
- };
792
- return (react_1.default.createElement("div", { className: 'phx-editor' },
793
- react_1.default.createElement(EditorStyle_1.default, { editorHeight: height }),
794
- label && react_1.default.createElement("label", { className: 'block mb-1 text-xs font-normal text-gray-700' }, label),
795
- react_1.default.createElement("div", { className: 'relative z-30' },
796
- react_1.default.createElement("div", { ref: toolbarRef, className: `flex flex-wrap gap-1 p-2 bg-gray-100 border border-gray-300 rounded-t-md
797
- ${disabled ? 'pointer-events-none' : ''}
798
- ` },
799
- toolbarItems.map((item) => {
800
- var _a, _b, _c, _d;
801
- (_a = item.isActive) === null || _a === void 0 ? void 0 : _a.call(item);
802
- return item.component ? (react_1.default.createElement("div", { className: 'flex items-center gap-x-2' },
803
- item.component(),
804
- item.borderRight && react_1.default.createElement("div", { className: 'w-[1px] h-5 bg-gray-300' }))) : (react_1.default.createElement("div", { className: 'flex items-center gap-x-2' },
805
- react_1.default.createElement("button", { key: item.name, type: 'button', "data-color-button": 'true', onMouseDown: (e) => onMouseDownToolbar(e, item), className: `p-1 rounded-md duration-200 ${((_b = item.isActive) === null || _b === void 0 ? void 0 : _b.call(item)) ? 'bg-gray-200' : ''} ${((_c = item.isDisabled) === null || _c === void 0 ? void 0 : _c.call(item)) ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-200 cursor-pointer'}`, disabled: (_d = item.isDisabled) === null || _d === void 0 ? void 0 : _d.call(item) }, (item === null || item === void 0 ? void 0 : item.icon) && react_1.default.createElement("span", { dangerouslySetInnerHTML: { __html: item.icon } })),
806
- item.borderRight && react_1.default.createElement("div", { className: 'w-[1px] h-5 bg-gray-300' })));
807
- }),
808
- showEmojiPicker && (react_1.default.createElement("div", { ref: emojiPickerRef, className: 'fixed z-50 p-5 bg-white border shadow sm:absolute rounded-xl', style: {
809
- left: '50%',
810
- top: emojiPickerPos.top,
811
- transform: 'translateX(-50%)',
812
- ...(window.innerWidth >= 640 && {
813
- left: emojiPickerPos.left,
814
- transform: 'none',
815
- }),
816
- } },
817
- react_1.default.createElement(emoji_picker_react_1.default, { emojiStyle: emoji_picker_react_1.EmojiStyle.APPLE, searchPlaceHolder: 'T\u00ECm ki\u1EBFm', onEmojiClick: (emojiData) => editor === null || editor === void 0 ? void 0 : editor.chain().focus().insertContent(emojiData.emoji).run(), previewConfig: {
818
- showPreview: false,
819
- }, skinTonesDisabled: true, height: 350, width: 300 }))),
820
- showColorPicker && (react_1.default.createElement("div", { ref: pickerRef, className: 'fixed z-50 p-5 bg-white border shadow sm:absolute rounded-xl', style: {
821
- left: '50%',
822
- top: pickerPos.top,
823
- transform: 'translateX(-50%)',
824
- ...(window.innerWidth >= 640 && {
825
- left: pickerPos.left,
826
- transform: 'none',
827
- }),
828
- } },
829
- react_1.default.createElement(react_colorful_1.HexAlphaColorPicker, { color: fontColor, onChange: (color) => {
830
- const norm = normalizeColor(color);
831
- setFontColor(norm);
832
- editor === null || editor === void 0 ? void 0 : editor.chain().focus().setColor(norm).setMark('textStyle', { color: `${norm} !important` }).run();
833
- } }),
834
- react_1.default.createElement("div", { className: 'flex items-center justify-center gap-2 mt-2' },
835
- react_1.default.createElement("div", { className: 'flex items-center gap-x-2 border border-gray-300 py-1.5 px-3 rounded-xl' },
836
- react_1.default.createElement("div", { className: 'w-6 h-6 border rounded ', style: { backgroundColor: fontColor.replace(/ !important$/, '') }, title: fontColor }),
837
- react_1.default.createElement("input", { type: 'text', className: 'p-0 border-none !ring-0 w-[144px]', value: fontColor.replace(/ !important$/, ''), onChange: (e) => {
838
- const raw = e.target.value.startsWith('#')
839
- ? e.target.value
840
- : `#${e.target.value.replace(/^#/, '')}`;
841
- const norm = normalizeColor(raw);
842
- setFontColor(norm);
843
- if (/^#([0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(norm)) {
844
- editor === null || editor === void 0 ? void 0 : editor.chain().focus().setColor(norm).run();
845
- }
846
- } }))))))),
847
- react_1.default.createElement(react_3.EditorContent, { editor: editor }),
848
- react_1.default.createElement(Modal_1.PHXModal, { onHide: () => {
849
- setShowLinkInput(false);
850
- reset();
851
- }, show: showLinkInput, title: 'Ch\u00E8n \u0111\u01B0\u1EDDng li\u00EAn k\u1EBFt', onPrimaryClick: handleInsertLink, closeButton: true },
852
- react_1.default.createElement(Input_1.PHXInput, { error: !!errors.link, errorType: 'custom-message', errorMessageCustom: (_a = errors.link) === null || _a === void 0 ? void 0 : _a.message, register: {
853
- ...register('link', {
854
- pattern: { value: regexUrl, message: 'Đường dẫn không hợp lệ' },
855
- required: 'Vui lòng nhập đường dẫn',
856
- }),
857
- }, label: 'Nh\u1EADp \u0111\u01B0\u1EDDng d\u1EABn', placeholder: 'https://example.com' })),
858
- react_1.default.createElement(Modal_1.PHXModal, { disableSubmit: !pdfFile, show: openPdfModal, onHide: () => setOpenPdfModal(false), title: 'T\u1EA3i l\u00EAn t\u00E0i li\u1EC7u PDF', onPrimaryClick: () => {
859
- if (pdfFile)
860
- handleConvertPdfToImage(pdfFile);
861
- }, primaryLoading: loadingConvertPdf },
862
- react_1.default.createElement(FormUpload_1.FormUpload, { fileName: pdfFile === null || pdfFile === void 0 ? void 0 : pdfFile.name, fileType: 'pdf', helpText: 'PDF, t\u1ED1i \u0111a 20MB', isHorizontalLayout: true, handleFileChange: (e) => {
863
- setPdfFile(e.target.files[0]);
864
- } }))));
865
- }
866
- //# sourceMappingURL=TextEditor.js.map