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