@herca/rn-kit 0.1.51 → 0.1.52

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 (62) hide show
  1. package/lib/module/Icon/Icon.js +15 -1
  2. package/lib/module/Icon/Icon.js.map +1 -1
  3. package/lib/module/Icon/icons/AlignCenter.js +25 -0
  4. package/lib/module/Icon/icons/AlignCenter.js.map +1 -0
  5. package/lib/module/Icon/icons/AlignLeft.js +25 -0
  6. package/lib/module/Icon/icons/AlignLeft.js.map +1 -0
  7. package/lib/module/Icon/icons/AlignRight.js +25 -0
  8. package/lib/module/Icon/icons/AlignRight.js.map +1 -0
  9. package/lib/module/Icon/icons/Bold.js +3 -3
  10. package/lib/module/Icon/icons/Bold.js.map +1 -1
  11. package/lib/module/Icon/icons/Italic.js +3 -3
  12. package/lib/module/Icon/icons/Italic.js.map +1 -1
  13. package/lib/module/Icon/icons/Link.js +25 -0
  14. package/lib/module/Icon/icons/Link.js.map +1 -0
  15. package/lib/module/Icon/icons/ListOrdered.js +34 -0
  16. package/lib/module/Icon/icons/ListOrdered.js.map +1 -0
  17. package/lib/module/Icon/icons/ListUnOrdered.js +25 -0
  18. package/lib/module/Icon/icons/ListUnOrdered.js.map +1 -0
  19. package/lib/module/Icon/icons/StrikeThrough.js +25 -0
  20. package/lib/module/Icon/icons/StrikeThrough.js.map +1 -0
  21. package/lib/module/Icon/icons/UnderLine.js +3 -3
  22. package/lib/module/Icon/icons/UnderLine.js.map +1 -1
  23. package/lib/module/TextEditor/README.md +263 -0
  24. package/lib/module/TextEditor/TextEditor.js +538 -63
  25. package/lib/module/TextEditor/TextEditor.js.map +1 -1
  26. package/lib/typescript/src/Icon/Icon.d.ts.map +1 -1
  27. package/lib/typescript/src/Icon/icons/AlignCenter.d.ts +9 -0
  28. package/lib/typescript/src/Icon/icons/AlignCenter.d.ts.map +1 -0
  29. package/lib/typescript/src/Icon/icons/AlignLeft.d.ts +9 -0
  30. package/lib/typescript/src/Icon/icons/AlignLeft.d.ts.map +1 -0
  31. package/lib/typescript/src/Icon/icons/AlignRight.d.ts +9 -0
  32. package/lib/typescript/src/Icon/icons/AlignRight.d.ts.map +1 -0
  33. package/lib/typescript/src/Icon/icons/Bold.d.ts.map +1 -1
  34. package/lib/typescript/src/Icon/icons/Italic.d.ts.map +1 -1
  35. package/lib/typescript/src/Icon/icons/Link.d.ts +9 -0
  36. package/lib/typescript/src/Icon/icons/Link.d.ts.map +1 -0
  37. package/lib/typescript/src/Icon/icons/ListOrdered.d.ts +9 -0
  38. package/lib/typescript/src/Icon/icons/ListOrdered.d.ts.map +1 -0
  39. package/lib/typescript/src/Icon/icons/ListUnOrdered.d.ts +9 -0
  40. package/lib/typescript/src/Icon/icons/ListUnOrdered.d.ts.map +1 -0
  41. package/lib/typescript/src/Icon/icons/StrikeThrough.d.ts +9 -0
  42. package/lib/typescript/src/Icon/icons/StrikeThrough.d.ts.map +1 -0
  43. package/lib/typescript/src/Icon/icons/UnderLine.d.ts.map +1 -1
  44. package/lib/typescript/src/Icon/type.d.ts +1 -1
  45. package/lib/typescript/src/Icon/type.d.ts.map +1 -1
  46. package/lib/typescript/src/TextEditor/TextEditor.d.ts +20 -1
  47. package/lib/typescript/src/TextEditor/TextEditor.d.ts.map +1 -1
  48. package/package.json +1 -1
  49. package/src/Icon/Icon.tsx +14 -0
  50. package/src/Icon/icons/AlignCenter.tsx +22 -0
  51. package/src/Icon/icons/AlignLeft.tsx +28 -0
  52. package/src/Icon/icons/AlignRight.tsx +22 -0
  53. package/src/Icon/icons/Bold.tsx +8 -2
  54. package/src/Icon/icons/Italic.tsx +8 -2
  55. package/src/Icon/icons/Link.tsx +22 -0
  56. package/src/Icon/icons/ListOrdered.tsx +38 -0
  57. package/src/Icon/icons/ListUnOrdered.tsx +32 -0
  58. package/src/Icon/icons/StrikeThrough.tsx +26 -0
  59. package/src/Icon/icons/UnderLine.tsx +8 -2
  60. package/src/Icon/type.tsx +7 -0
  61. package/src/TextEditor/README.md +263 -0
  62. package/src/TextEditor/TextEditor.tsx +685 -133
@@ -1,26 +1,80 @@
1
1
  "use strict";
2
2
 
3
- import { useRef, useState, useEffect } from 'react';
4
- import { View, StyleSheet, Platform, KeyboardAvoidingView, TouchableOpacity, Keyboard, Dimensions, SafeAreaView } from 'react-native';
3
+ import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
4
+ import { Alert, Dimensions, Keyboard, KeyboardAvoidingView, Platform, SafeAreaView, ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
5
5
  import { WebView } from 'react-native-webview';
6
+ import BottomSheet from "../BottomSheet/BottomSheet.js";
7
+ import Button from "../Button/Button.js";
6
8
  import Color from "../Color/Color.js";
9
+ import Icon from "../Icon/index.js";
10
+ import Input from "../Input/Input.js";
7
11
  import LabelForm from "../LabelForm/LabelForm.js";
8
12
  import Typography from "../Typography/Typography.js";
9
- import Icon from "../Icon/index.js";
10
13
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
- export default function TextEditor({
14
+ const TextEditor = /*#__PURE__*/forwardRef(({
12
15
  label,
13
16
  hasError,
14
17
  hint,
15
18
  onChange,
16
- height = 300
17
- }) {
19
+ height = 300,
20
+ initialValue = '',
21
+ placeholder = 'Tulis sesuatu di sini...',
22
+ maxLength,
23
+ onFocus,
24
+ onBlur,
25
+ inputLinkTextPlacholder = 'Link URL',
26
+ inputLinkUrlPlacholder = 'https://www.example.com',
27
+ inputLabelLinkText = 'Teks Link',
28
+ inputLabelLinkUrl = 'Link URL',
29
+ saveLinkButtonText = 'Simpan',
30
+ cancelLinkButtonText = 'Batal'
31
+ }, ref) => {
18
32
  const [keyboardHeight, setKeyboardHeight] = useState(0);
19
33
  const [showToolbar, setShowToolbar] = useState(false);
34
+ const [activeFormats, setActiveFormats] = useState(new Set());
35
+ const [characterCount, setCharacterCount] = useState(0);
36
+ const [showLinkModal, setShowLinkModal] = useState(false);
37
+ const [linkText, setLinkText] = useState('');
38
+ const [linkUrl, setLinkUrl] = useState('');
39
+ const [isEditingLink, setIsEditingLink] = useState(false);
20
40
  const webviewRef = useRef(null);
41
+ const inputUrlRef = useRef(null);
21
42
  const handleOnChange = data => {
43
+ // Remove HTML tags untuk hitung karakter
44
+ const textOnly = data.replace(/<[^>]*>/g, '');
45
+ setCharacterCount(textOnly.length);
46
+ if (maxLength && textOnly.length > maxLength) {
47
+ return;
48
+ }
22
49
  onChange?.(data);
23
50
  };
51
+
52
+ // Expose methods via ref
53
+ useImperativeHandle(ref, () => ({
54
+ getContent: () => {
55
+ webviewRef.current?.postMessage('getContent');
56
+ },
57
+ setContent: html => {
58
+ const escapedHtml = html.replace(/`/g, '\\`').replace(/\$/g, '\\$');
59
+ webviewRef.current?.injectJavaScript(`
60
+ const editor = document.getElementById('editor');
61
+ editor.innerHTML = \`${escapedHtml}\`;
62
+ if (editor.textContent.trim()) {
63
+ editor.classList.remove('placeholder');
64
+ }
65
+ true;
66
+ `);
67
+ },
68
+ clearContent: () => {
69
+ webviewRef.current?.injectJavaScript(`
70
+ const editor = document.getElementById('editor');
71
+ editor.innerHTML = '';
72
+ editor.classList.add('placeholder');
73
+ window.ReactNativeWebView.postMessage('');
74
+ true;
75
+ `);
76
+ }
77
+ }));
24
78
  const htmlEditor = `
25
79
  <html>
26
80
  <head>
@@ -39,52 +93,225 @@ export default function TextEditor({
39
93
  overflow-x: hidden;
40
94
  }
41
95
  #editor {
42
- min-height: ${height};
96
+ height: 100%;
43
97
  width: 100%;
44
98
  max-width: 100%;
45
- padding: 12px;
46
- font-size: 12px;
47
- border: 1px solid ${Color.gray[200]};
99
+ font-size: 14px;
100
+ line-height: 1.5;
48
101
  border-radius: 6px;
49
102
  outline: none;
50
103
  transition: border 0.2s;
51
104
  overflow-wrap: break-word;
52
- font-weight: 500;
105
+ font-weight: 400;
53
106
  word-wrap: break-word;
54
107
  word-break: break-word;
108
+ color: #333;
109
+ }
110
+ #editor.placeholder:empty:before {
111
+ content: '${placeholder}';
112
+ color: ${Color.gray[400]};
113
+ font-style: italic;
114
+ }
115
+ #editor:focus {
116
+ outline: none;
117
+ }
118
+ /* Style untuk list */
119
+ #editor ul, #editor ol {
120
+ margin: 8px 0;
121
+ padding-left: 24px;
122
+ }
123
+ #editor li {
124
+ margin: 4px 0;
55
125
  }
56
- #editor.focused {
57
- border-color: ${Color.gray[200]}
126
+ /* Style untuk heading */
127
+ #editor h1 { font-size: 24px; font-weight: bold; margin: 12px 0; }
128
+ #editor h2 { font-size: 20px; font-weight: bold; margin: 10px 0; }
129
+ #editor h3 { font-size: 16px; font-weight: bold; margin: 8px 0; }
130
+ /* Style untuk link */
131
+ #editor a {
132
+ color: #3b82f6;
133
+ text-decoration: underline;
134
+ cursor: pointer;
135
+ }
136
+ #editor a:hover {
137
+ color: #2563eb;
58
138
  }
59
139
  </style>
60
140
  </head>
61
141
  <body>
62
- <div id="editor" contenteditable="true">Tulis sesuatu di sini...</div>
142
+ <div id="editor" contenteditable="true" class="placeholder">${initialValue}</div>
63
143
  <script>
64
144
  const editor = document.getElementById("editor");
65
-
145
+
146
+ // Handle placeholder
147
+ if (editor.textContent.trim()) {
148
+ editor.classList.remove('placeholder');
149
+ }
150
+
151
+ editor.addEventListener("input", () => {
152
+ if (editor.textContent.trim()) {
153
+ editor.classList.remove('placeholder');
154
+ } else {
155
+ editor.classList.add('placeholder');
156
+ }
157
+ window.ReactNativeWebView.postMessage(editor.innerHTML);
158
+ });
159
+
66
160
  editor.addEventListener("focus", () => {
67
161
  editor.classList.add("focused");
162
+ window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'focus' }));
68
163
  });
69
164
 
70
165
  editor.addEventListener("blur", () => {
71
166
  editor.classList.remove("focused");
167
+ window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'blur' }));
72
168
  });
169
+
170
+ // Update active formats
171
+ editor.addEventListener("mouseup", updateFormats);
172
+ editor.addEventListener("keyup", updateFormats);
173
+ editor.addEventListener("click", updateFormats);
174
+
175
+ function updateFormats() {
176
+ const formats = [];
177
+ if (document.queryCommandState('bold')) formats.push('bold');
178
+ if (document.queryCommandState('italic')) formats.push('italic');
179
+ if (document.queryCommandState('underline')) formats.push('underline');
180
+ if (document.queryCommandState('strikeThrough')) formats.push('strikeThrough');
181
+
182
+ window.ReactNativeWebView.postMessage(JSON.stringify({
183
+ type: 'formats',
184
+ formats: formats
185
+ }));
186
+ }
187
+
188
+ function getSelectedLinkInfo() {
189
+ const selection = window.getSelection();
190
+ if (!selection || selection.rangeCount === 0) {
191
+ return { text: '', url: '' };
192
+ }
193
+
194
+ const range = selection.getRangeAt(0);
195
+ let linkElement = null;
196
+
197
+ // Check if selection contains or is within a link
198
+ if (range.startContainer.nodeType === Node.ELEMENT_NODE) {
199
+ linkElement = range.startContainer.querySelector('a');
200
+ }
201
+
202
+ // Check parent nodes for link
203
+ let node = range.startContainer;
204
+ while (node && node !== editor) {
205
+ if (node.nodeName === 'A') {
206
+ linkElement = node;
207
+ break;
208
+ }
209
+ node = node.parentNode;
210
+ }
211
+
212
+ // Check if any node in selection is a link
213
+ if (!linkElement && range.commonAncestorContainer) {
214
+ const container = range.commonAncestorContainer;
215
+ if (container.nodeType === Node.ELEMENT_NODE) {
216
+ linkElement = container.querySelector('a');
217
+ }
218
+ }
219
+
220
+ if (linkElement) {
221
+ return {
222
+ text: linkElement.textContent || '',
223
+ url: linkElement.href || '',
224
+ isExisting: true
225
+ };
226
+ }
227
+
228
+ return {
229
+ text: selection.toString(),
230
+ url: '',
231
+ isExisting: false
232
+ };
233
+ }
234
+
73
235
  const handleMessage = (command) => {
74
236
  if (command === 'getContent') {
75
237
  window.ReactNativeWebView.postMessage(editor.innerHTML);
238
+ } else if (command === 'getSelectedText') {
239
+ const linkInfo = getSelectedLinkInfo();
240
+ window.ReactNativeWebView.postMessage(JSON.stringify({
241
+ type: 'selectedText',
242
+ text: linkInfo.text,
243
+ url: linkInfo.url,
244
+ isExisting: linkInfo.isExisting
245
+ }));
246
+ } else if (command.startsWith('insertLink:')) {
247
+ const linkData = command.replace('insertLink:', '');
248
+ const { text, url, isExisting } = JSON.parse(linkData);
249
+ editor.focus();
250
+ const selection = window.getSelection();
251
+
252
+ if (selection && selection.rangeCount > 0) {
253
+ const range = selection.getRangeAt(0);
254
+
255
+ // If editing existing link, find and update it
256
+ if (isExisting) {
257
+ let linkElement = null;
258
+ let node = range.startContainer;
259
+
260
+ while (node && node !== editor) {
261
+ if (node.nodeName === 'A') {
262
+ linkElement = node;
263
+ break;
264
+ }
265
+ node = node.parentNode;
266
+ }
267
+
268
+ if (!linkElement && range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE) {
269
+ linkElement = range.commonAncestorContainer.querySelector('a');
270
+ }
271
+
272
+ if (linkElement) {
273
+ linkElement.href = url;
274
+ linkElement.textContent = text;
275
+ // Move cursor after link
276
+ range.setStartAfter(linkElement);
277
+ range.collapse(true);
278
+ selection.removeAllRanges();
279
+ selection.addRange(range);
280
+ window.ReactNativeWebView.postMessage(editor.innerHTML);
281
+ return;
282
+ }
283
+ }
284
+
285
+ // Create new link
286
+ const link = document.createElement('a');
287
+ link.href = url;
288
+ link.textContent = text;
289
+ link.target = '_blank';
290
+ range.deleteContents();
291
+ range.insertNode(link);
292
+ // Add space after link
293
+ const space = document.createTextNode(' ');
294
+ link.parentNode.insertBefore(space, link.nextSibling);
295
+ // Move cursor after space
296
+ range.setStartAfter(space);
297
+ range.collapse(true);
298
+ selection.removeAllRanges();
299
+ selection.addRange(range);
300
+ }
301
+ window.ReactNativeWebView.postMessage(editor.innerHTML);
76
302
  } else {
77
303
  editor.focus();
78
- document.execCommand(command, false, '');
304
+ document.execCommand(command, false, null);
305
+ updateFormats();
79
306
  }
80
307
  };
81
308
 
82
- // ini untuk Android
309
+ // untuk Android
83
310
  document.addEventListener("message", (event) => {
84
311
  handleMessage(event.data);
85
312
  });
86
313
 
87
- // ini untuk iOS
314
+ // untuk iOS
88
315
  window.addEventListener("message", (event) => {
89
316
  handleMessage(event.data);
90
317
  });
@@ -96,18 +323,147 @@ export default function TextEditor({
96
323
  if (webviewRef.current) {
97
324
  if (Platform.OS === 'ios') {
98
325
  webviewRef.current.injectJavaScript(`
99
- document.execCommand('${command}', false, '');
100
- true; //di ios harus biar berfunsi boldnya
101
- `);
326
+ document.execCommand('${command}', false, null);
327
+ const formats = [];
328
+ if (document.queryCommandState('bold')) formats.push('bold');
329
+ if (document.queryCommandState('italic')) formats.push('italic');
330
+ if (document.queryCommandState('underline')) formats.push('underline');
331
+ if (document.queryCommandState('strikeThrough')) formats.push('strikeThrough');
332
+ window.ReactNativeWebView.postMessage(JSON.stringify({
333
+ type: 'formats',
334
+ formats: formats
335
+ }));
336
+ true;
337
+ `);
102
338
  } else {
103
339
  webviewRef.current.postMessage(command);
104
340
  }
105
341
  }
106
342
  };
343
+ const openLinkModal = () => {
344
+ // Get selected text first
345
+ if (webviewRef.current) {
346
+ webviewRef.current.postMessage('getSelectedText');
347
+ }
348
+ setShowLinkModal(true);
349
+ setTimeout(() => {
350
+ inputUrlRef.current?.focus();
351
+ }, 300);
352
+ };
353
+ console.log(showLinkModal);
354
+ const insertLink = () => {
355
+ if (!linkUrl.trim()) {
356
+ Alert.alert('Error', 'URL tidak boleh kosong');
357
+ return;
358
+ }
359
+ let finalUrl = linkUrl.trim();
360
+ // Add https:// if no protocol
361
+ if (!finalUrl.match(/^https?:\/\//i)) {
362
+ finalUrl = 'https://' + finalUrl;
363
+ }
364
+ const linkData = JSON.stringify({
365
+ text: linkText.trim() || finalUrl,
366
+ url: finalUrl,
367
+ isExisting: isEditingLink
368
+ });
369
+ if (webviewRef.current) {
370
+ if (Platform.OS === 'ios') {
371
+ webviewRef.current.injectJavaScript(`
372
+ const linkData = ${linkData};
373
+ const editor = document.getElementById('editor');
374
+ editor.focus();
375
+ const selection = window.getSelection();
376
+
377
+ if (selection && selection.rangeCount > 0) {
378
+ const range = selection.getRangeAt(0);
379
+
380
+ // If editing existing link
381
+ if (linkData.isExisting) {
382
+ let linkElement = null;
383
+ let node = range.startContainer;
384
+
385
+ while (node && node !== editor) {
386
+ if (node.nodeName === 'A') {
387
+ linkElement = node;
388
+ break;
389
+ }
390
+ node = node.parentNode;
391
+ }
392
+
393
+ if (!linkElement && range.commonAncestorContainer.nodeType === 1) {
394
+ linkElement = range.commonAncestorContainer.querySelector('a');
395
+ }
396
+
397
+ if (linkElement) {
398
+ linkElement.href = linkData.url;
399
+ linkElement.textContent = linkData.text;
400
+ range.setStartAfter(linkElement);
401
+ range.collapse(true);
402
+ selection.removeAllRanges();
403
+ selection.addRange(range);
404
+ window.ReactNativeWebView.postMessage(editor.innerHTML);
405
+ true;
406
+ return;
407
+ }
408
+ }
409
+
410
+ // Create new link
411
+ const link = document.createElement('a');
412
+ link.href = linkData.url;
413
+ link.textContent = linkData.text;
414
+ link.target = '_blank';
415
+ range.deleteContents();
416
+ range.insertNode(link);
417
+ const space = document.createTextNode(' ');
418
+ link.parentNode.insertBefore(space, link.nextSibling);
419
+ range.setStartAfter(space);
420
+ range.collapse(true);
421
+ selection.removeAllRanges();
422
+ selection.addRange(range);
423
+ }
424
+ window.ReactNativeWebView.postMessage(editor.innerHTML);
425
+ true;
426
+ `);
427
+ } else {
428
+ webviewRef.current.postMessage('insertLink:' + linkData);
429
+ }
430
+ }
431
+ setShowLinkModal(false);
432
+ setLinkText('');
433
+ setLinkUrl('');
434
+ setIsEditingLink(false);
435
+ };
436
+ const closeLinkModal = () => {
437
+ setShowLinkModal(false);
438
+ setLinkText('');
439
+ setLinkUrl('');
440
+ setIsEditingLink(false);
441
+ };
442
+ const handleWebViewMessage = event => {
443
+ const data = event.nativeEvent.data;
444
+ try {
445
+ const parsed = JSON.parse(data);
446
+ if (parsed.type === 'formats') {
447
+ setActiveFormats(new Set(parsed.formats));
448
+ } else if (parsed.type === 'focus') {
449
+ onFocus?.();
450
+ } else if (parsed.type === 'blur') {
451
+ onBlur?.();
452
+ } else if (parsed.type === 'selectedText') {
453
+ setLinkText(parsed.text);
454
+ // Clean URL (remove protocol prefix for editing)
455
+ const cleanUrl = parsed.url.replace(/^https?:\/\//, '');
456
+ setLinkUrl(cleanUrl);
457
+ setIsEditingLink(parsed.isExisting || false);
458
+ }
459
+ } catch {
460
+ // Jika bukan JSON, berarti itu HTML content
461
+ handleOnChange(data);
462
+ }
463
+ };
107
464
  useEffect(() => {
108
465
  const showSub = Keyboard.addListener('keyboardDidShow', e => {
109
466
  setKeyboardHeight(e.endCoordinates.height - 35);
110
- console.log(e.endCoordinates.height);
111
467
  setShowToolbar(true);
112
468
  });
113
469
  const hideSub = Keyboard.addListener('keyboardDidHide', () => {
@@ -119,14 +475,59 @@ export default function TextEditor({
119
475
  hideSub.remove();
120
476
  };
121
477
  }, []);
478
+ const gap = Platform.OS === 'ios' ? 200 : 185;
479
+ const isFormatActive = format => activeFormats.has(format);
480
+ const toolbarButtons = [{
481
+ command: 'bold',
482
+ icon: 'Bold',
483
+ label: 'Bold'
484
+ }, {
485
+ command: 'italic',
486
+ icon: 'Italic',
487
+ label: 'Italic'
488
+ }, {
489
+ command: 'underline',
490
+ icon: 'UnderLine',
491
+ label: 'Underline'
492
+ }, {
493
+ command: 'strikeThrough',
494
+ icon: 'strike-through',
495
+ label: 'Strike'
496
+ }, {
497
+ command: 'link',
498
+ icon: 'Link',
499
+ label: 'Link',
500
+ isSpecial: true
501
+ }, {
502
+ command: 'insertUnorderedList',
503
+ icon: 'list-un-ordered',
504
+ label: 'Bullet'
505
+ }, {
506
+ command: 'insertOrderedList',
507
+ icon: 'list-ordered',
508
+ label: 'Number'
509
+ }, {
510
+ command: 'justifyLeft',
511
+ icon: 'align-left',
512
+ label: 'Left'
513
+ }, {
514
+ command: 'justifyCenter',
515
+ icon: 'align-center',
516
+ label: 'Center'
517
+ }, {
518
+ command: 'justifyRight',
519
+ icon: 'align-right',
520
+ label: 'Right'
521
+ }];
122
522
  return /*#__PURE__*/_jsxs(View, {
123
523
  style: styles.container,
124
524
  children: [label ? /*#__PURE__*/_jsx(LabelForm, {
125
525
  title: label
126
526
  }) : null, /*#__PURE__*/_jsx(KeyboardAvoidingView, {
127
- style: (styles.editorWrapper, [{
128
- minHeight: height
129
- }]),
527
+ style: [styles.editorWrapper, {
528
+ minHeight: height,
529
+ height: height
530
+ }],
130
531
  behavior: Platform.OS === 'ios' ? 'padding' : undefined,
131
532
  children: /*#__PURE__*/_jsx(WebView, {
132
533
  ref: webviewRef,
@@ -134,47 +535,90 @@ export default function TextEditor({
134
535
  source: {
135
536
  html: htmlEditor
136
537
  },
137
- onMessage: event => {
138
- handleOnChange(event.nativeEvent.data);
139
- },
538
+ onMessage: handleWebViewMessage,
140
539
  style: styles.webview,
141
540
  javaScriptEnabled: true,
142
541
  domStorageEnabled: true,
143
542
  mixedContentMode: "always",
144
543
  allowUniversalAccessFromFileURLs: true,
145
- allowFileAccess: true
544
+ allowFileAccess: true,
545
+ scrollEnabled: true
146
546
  })
147
- }), hint ? /*#__PURE__*/_jsx(Typography, {
148
- color: hasError ? Color.danger[500] : Color.gray[700],
149
- variant: "t3",
150
- weight: "medium",
151
- children: hint
152
- }) : null, showToolbar ? /*#__PURE__*/_jsxs(SafeAreaView, {
547
+ }), hint || maxLength ? /*#__PURE__*/_jsxs(View, {
548
+ style: styles.hintContainer,
549
+ children: [hint ? /*#__PURE__*/_jsx(Typography, {
550
+ color: hasError ? Color.danger[500] : Color.gray[700],
551
+ variant: "t3",
552
+ weight: "medium",
553
+ children: hint
554
+ }) : null, maxLength && /*#__PURE__*/_jsxs(Typography, {
555
+ color: characterCount > maxLength ? Color.danger[500] : Color.gray[600],
556
+ variant: "t3",
557
+ weight: "medium",
558
+ children: [characterCount, "/", maxLength]
559
+ })]
560
+ }) : null, showToolbar ? /*#__PURE__*/_jsx(SafeAreaView, {
153
561
  style: [styles.toolbar, {
154
- top: Dimensions.get('screen').height - keyboardHeight - 200
562
+ top: Dimensions.get('screen').height - keyboardHeight - gap
155
563
  }],
156
- children: [/*#__PURE__*/_jsx(TouchableOpacity, {
157
- onPress: () => formatText('bold'),
158
- style: styles.toolButton,
159
- children: /*#__PURE__*/_jsx(Icon, {
160
- name: "Bold"
161
- })
162
- }), /*#__PURE__*/_jsx(TouchableOpacity, {
163
- onPress: () => formatText('italic'),
164
- style: styles.toolButton,
165
- children: /*#__PURE__*/_jsx(Icon, {
166
- name: "Italic"
167
- })
168
- }), /*#__PURE__*/_jsx(TouchableOpacity, {
169
- onPress: () => formatText('underline'),
170
- style: styles.toolButton,
171
- children: /*#__PURE__*/_jsx(Icon, {
172
- name: "UnderLine"
173
- })
564
+ children: /*#__PURE__*/_jsx(ScrollView, {
565
+ horizontal: true,
566
+ showsHorizontalScrollIndicator: false,
567
+ contentContainerStyle: styles.toolbarContent,
568
+ children: toolbarButtons.map(button => /*#__PURE__*/_jsx(TouchableOpacity, {
569
+ onPress: () => button.isSpecial ? openLinkModal() : formatText(button.command),
570
+ style: [styles.toolButton, isFormatActive(button.command) && styles.toolButtonActive],
571
+ children: /*#__PURE__*/_jsx(Icon, {
572
+ name: button.icon,
573
+ size: 20,
574
+ color: isFormatActive(button.command) ? '#fff' : Color.gray[900]
575
+ })
576
+ }, button.command))
577
+ })
578
+ }) : null, /*#__PURE__*/_jsxs(BottomSheet, {
579
+ isOpen: showLinkModal,
580
+ onClose: () => setShowLinkModal(false),
581
+ children: [/*#__PURE__*/_jsxs(View, {
582
+ style: styles.modalBody,
583
+ children: [/*#__PURE__*/_jsx(Input, {
584
+ label: inputLabelLinkText,
585
+ value: linkText,
586
+ onChangeText: setLinkText,
587
+ placeholder: inputLinkTextPlacholder,
588
+ autoCapitalize: "none"
589
+ }), /*#__PURE__*/_jsx(Input, {
590
+ ref: inputUrlRef,
591
+ keyboardType: "url",
592
+ label: inputLabelLinkUrl,
593
+ value: linkUrl,
594
+ onChangeText: setLinkUrl,
595
+ placeholder: inputLinkUrlPlacholder,
596
+ autoCapitalize: "none"
597
+ })]
598
+ }), /*#__PURE__*/_jsxs(View, {
599
+ style: styles.modalActions,
600
+ children: [/*#__PURE__*/_jsx(View, {
601
+ style: styles.flex1,
602
+ children: /*#__PURE__*/_jsx(Button, {
603
+ title: cancelLinkButtonText,
604
+ color: "primary",
605
+ onPress: closeLinkModal,
606
+ variant: "outline"
607
+ })
608
+ }), /*#__PURE__*/_jsx(View, {
609
+ style: styles.flex1,
610
+ children: /*#__PURE__*/_jsx(Button, {
611
+ title: saveLinkButtonText,
612
+ color: "primary",
613
+ onPress: insertLink
614
+ })
615
+ })]
174
616
  })]
175
- }) : null]
617
+ })]
176
618
  });
177
- }
619
+ });
620
+ TextEditor.displayName = 'TextEditor';
621
+ export default TextEditor;
178
622
  const styles = StyleSheet.create({
179
623
  container: {
180
624
  backgroundColor: '#fff',
@@ -182,28 +626,59 @@ const styles = StyleSheet.create({
182
626
  position: 'relative'
183
627
  },
184
628
  editorWrapper: {
185
- flex: 1
629
+ flex: 1,
630
+ borderWidth: 1,
631
+ borderColor: Color.gray[200],
632
+ borderRadius: 8,
633
+ overflow: 'hidden',
634
+ paddingVertical: 10,
635
+ paddingHorizontal: 12
636
+ },
637
+ hintContainer: {
638
+ flexDirection: 'row',
639
+ justifyContent: 'space-between',
640
+ alignItems: 'center',
641
+ paddingHorizontal: 2
186
642
  },
187
643
  toolbar: {
188
644
  flexDirection: 'row',
189
645
  borderTopWidth: 1,
190
646
  borderColor: Color.gray[200],
647
+ backgroundColor: '#fff',
191
648
  paddingVertical: 8,
192
- paddingHorizontal: 10,
193
649
  position: 'absolute',
194
- left: 0,
195
- width: '100%',
650
+ left: -24,
651
+ width: Dimensions.get('screen').width,
196
652
  alignItems: 'center'
197
653
  },
654
+ toolbarContent: {
655
+ paddingHorizontal: 8,
656
+ gap: 4
657
+ },
198
658
  toolButton: {
199
659
  padding: 8,
200
660
  marginHorizontal: 4,
201
- borderRadius: 6
661
+ borderRadius: 6,
662
+ minWidth: 36,
663
+ alignItems: 'center',
664
+ justifyContent: 'center'
202
665
  },
203
- toolText: {
204
- fontWeight: 'bold'
666
+ toolButtonActive: {
667
+ backgroundColor: Color.primary?.[1000]
205
668
  },
206
669
  webview: {
670
+ flex: 1,
671
+ backgroundColor: 'transparent'
672
+ },
673
+ modalBody: {
674
+ gap: 16
675
+ },
676
+ modalActions: {
677
+ flexDirection: 'row',
678
+ gap: 12,
679
+ marginTop: 8
680
+ },
681
+ flex1: {
207
682
  flex: 1
208
683
  }
209
684
  });