@phpsoftbox/react-softbox 0.6.1 → 0.7.0

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.
@@ -0,0 +1,1149 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import Prism from 'prismjs';
4
+ import 'prismjs/components/prism-markup';
5
+ import 'prismjs/components/prism-markup-templating';
6
+ import 'prismjs/components/prism-clike';
7
+ import 'prismjs/components/prism-javascript';
8
+ import 'prismjs/components/prism-typescript';
9
+ import 'prismjs/components/prism-jsx';
10
+ import 'prismjs/components/prism-tsx';
11
+ import 'prismjs/components/prism-json';
12
+ import 'prismjs/components/prism-css';
13
+ import 'prismjs/components/prism-bash';
14
+ import 'prismjs/components/prism-markdown';
15
+ import 'prismjs/components/prism-php';
16
+ import Textarea from '../Input/Textarea/Textarea';
17
+ import Dropdown from '../Menu/Dropdown';
18
+ import styles from './MarkdownEditor.module.css';
19
+ const MarkdownEditorTextareaSlot = () => null;
20
+ const MarkdownEditorPreviewSlot = () => null;
21
+ MarkdownEditorTextareaSlot.displayName = 'MarkdownEditor.Textarea';
22
+ MarkdownEditorPreviewSlot.displayName = 'MarkdownEditor.Preview';
23
+ const escapeHtml = (value) => value
24
+ .replace(/&/g, '&')
25
+ .replace(/</g, '&lt;')
26
+ .replace(/>/g, '&gt;')
27
+ .replace(/"/g, '&quot;')
28
+ .replace(/'/g, '&#39;');
29
+ const resolvePrismLanguage = (language) => {
30
+ const normalized = language.trim().toLowerCase();
31
+ if (!normalized) {
32
+ return 'markup';
33
+ }
34
+ const aliasMap = {
35
+ js: 'javascript',
36
+ mjs: 'javascript',
37
+ cjs: 'javascript',
38
+ ts: 'typescript',
39
+ jsx: 'jsx',
40
+ tsx: 'tsx',
41
+ html: 'markup',
42
+ xml: 'markup',
43
+ svg: 'markup',
44
+ sh: 'bash',
45
+ shell: 'bash',
46
+ zsh: 'bash',
47
+ md: 'markdown',
48
+ php8: 'php',
49
+ php7: 'php',
50
+ phtml: 'php',
51
+ };
52
+ const resolved = aliasMap[normalized] ?? normalized;
53
+ return Prism.languages[resolved] ? resolved : 'markup';
54
+ };
55
+ const renderMarkdown = (value) => {
56
+ if (!value) {
57
+ return '';
58
+ }
59
+ const codeBlocks = [];
60
+ let codeIndex = 0;
61
+ const markdownWithCodeTokens = value.replace(/```([^\n`]*)\n?([\s\S]*?)```/g, (_, rawLanguage, rawCode) => {
62
+ const language = (rawLanguage ?? '').trim().toLowerCase();
63
+ const cleanCode = (rawCode ?? '').replace(/^\n+|\n+$/g, '');
64
+ const prismLanguage = resolvePrismLanguage(language);
65
+ const grammar = Prism.languages[prismLanguage];
66
+ const highlighted = cleanCode
67
+ ? Prism.highlight(cleanCode, grammar, prismLanguage)
68
+ : '';
69
+ const lines = (highlighted || '&nbsp;').split('\n');
70
+ const codeLinesHtml = lines
71
+ .map((line, index) => {
72
+ return `<span class="md-code-line"><span class="md-code-num">${index + 1}</span><span class="md-code-text">${line || '&nbsp;'}</span></span>`;
73
+ })
74
+ .join('');
75
+ const token = `@@CODEBLOCK_${codeIndex}@@`;
76
+ const encodedCode = escapeHtml(encodeURIComponent(cleanCode));
77
+ const languageLabel = escapeHtml(language || prismLanguage || 'text');
78
+ const languageClass = escapeHtml((prismLanguage || 'text').replace(/[^a-z0-9_-]/g, ''));
79
+ const block = `
80
+ <div class="md-code-wrap" data-code-block="true" data-code="${encodedCode}">
81
+ <div class="md-code-head">
82
+ <span class="md-code-lang">${languageLabel}</span>
83
+ <button type="button" class="md-code-copy" data-code-copy="true">Скопировать</button>
84
+ </div>
85
+ <pre class="md-code-pre"><code class="md-code md-lang-${languageClass}">${codeLinesHtml}</code></pre>
86
+ </div>`;
87
+ codeBlocks.push({ token, html: block });
88
+ codeIndex += 1;
89
+ return token;
90
+ });
91
+ let html = escapeHtml(markdownWithCodeTokens);
92
+ html = html.replace(/^######\s(.+)$/gm, '<h6>$1</h6>');
93
+ html = html.replace(/^#####\s(.+)$/gm, '<h5>$1</h5>');
94
+ html = html.replace(/^####\s(.+)$/gm, '<h4>$1</h4>');
95
+ html = html.replace(/^###\s(.+)$/gm, '<h3>$1</h3>');
96
+ html = html.replace(/^##\s(.+)$/gm, '<h2>$1</h2>');
97
+ html = html.replace(/^#\s(.+)$/gm, '<h1>$1</h1>');
98
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
99
+ html = html.replace(/__(.+?)__/g, '<u>$1</u>');
100
+ html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
101
+ html = html.replace(/~~(.+?)~~/g, '<del>$1</del>');
102
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
103
+ html = html.replace(/!\[(.*?)\]\(((?:https?:\/\/|blob:|data:image\/)[^\s)]+)\)/g, '<img src="$2" alt="$1" />');
104
+ html = html.replace(/\[(.+?)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
105
+ const lines = html.split('\n');
106
+ const output = [];
107
+ let inQuote = false;
108
+ let lastWasText = false;
109
+ const listStack = [];
110
+ const closeQuote = () => {
111
+ if (inQuote) {
112
+ output.push('</blockquote>');
113
+ inQuote = false;
114
+ lastWasText = false;
115
+ }
116
+ };
117
+ const closeList = () => {
118
+ const item = listStack.pop();
119
+ if (!item) {
120
+ return;
121
+ }
122
+ if (item.openItem) {
123
+ output.push('</li>');
124
+ }
125
+ output.push(`</${item.type}>`);
126
+ };
127
+ const closeListsToLevel = (level) => {
128
+ while (listStack.length > 0 && listStack[listStack.length - 1].level > level) {
129
+ closeList();
130
+ }
131
+ };
132
+ const closeCurrentItem = () => {
133
+ const current = listStack[listStack.length - 1];
134
+ if (current?.openItem) {
135
+ output.push('</li>');
136
+ current.openItem = false;
137
+ }
138
+ };
139
+ const openList = (type, level) => {
140
+ output.push(`<${type}>`);
141
+ listStack.push({ type, level, openItem: false });
142
+ };
143
+ const openListItem = (content) => {
144
+ const current = listStack[listStack.length - 1];
145
+ if (!current) {
146
+ return;
147
+ }
148
+ output.push(`<li>${content}`);
149
+ current.openItem = true;
150
+ };
151
+ const closeAllLists = () => {
152
+ while (listStack.length > 0) {
153
+ closeList();
154
+ }
155
+ };
156
+ lines.forEach((rawLine) => {
157
+ const normalized = rawLine.replace(/\t/g, ' ');
158
+ const trimmed = normalized.trim();
159
+ const codeTokenMatch = /^@@CODEBLOCK_\d+@@$/.exec(trimmed);
160
+ if (codeTokenMatch) {
161
+ closeQuote();
162
+ closeAllLists();
163
+ output.push(trimmed);
164
+ lastWasText = false;
165
+ return;
166
+ }
167
+ const quoteMatch = /^>\s?(.*)$/.exec(normalized);
168
+ if (quoteMatch) {
169
+ closeAllLists();
170
+ if (!inQuote) {
171
+ output.push('<blockquote>');
172
+ inQuote = true;
173
+ }
174
+ else if (lastWasText) {
175
+ output.push('<br />');
176
+ }
177
+ output.push(quoteMatch[1]);
178
+ lastWasText = true;
179
+ return;
180
+ }
181
+ closeQuote();
182
+ const ulMatch = /^(\s*)[-*]\s+(.+)$/.exec(normalized);
183
+ const olMatch = /^(\s*)\d+\.\s+(.+)$/.exec(normalized);
184
+ const listMatch = ulMatch ?? olMatch;
185
+ if (listMatch) {
186
+ const type = ulMatch ? 'ul' : 'ol';
187
+ const indent = listMatch[1]?.length ?? 0;
188
+ const level = Math.floor(indent / 2);
189
+ const content = listMatch[2];
190
+ if (listStack.length === 0) {
191
+ openList(type, level);
192
+ }
193
+ else {
194
+ let current = listStack[listStack.length - 1];
195
+ if (level > current.level) {
196
+ for (let nextLevel = current.level + 1; nextLevel <= level; nextLevel += 1) {
197
+ openList(type, nextLevel);
198
+ }
199
+ }
200
+ else if (level < current.level) {
201
+ closeCurrentItem();
202
+ closeListsToLevel(level);
203
+ }
204
+ current = listStack[listStack.length - 1];
205
+ if (!current || current.level !== level) {
206
+ openList(type, level);
207
+ current = listStack[listStack.length - 1];
208
+ }
209
+ if (current && current.type !== type) {
210
+ closeCurrentItem();
211
+ closeList();
212
+ openList(type, level);
213
+ }
214
+ }
215
+ closeCurrentItem();
216
+ openListItem(content);
217
+ lastWasText = false;
218
+ return;
219
+ }
220
+ closeAllLists();
221
+ if (!trimmed) {
222
+ if (lastWasText) {
223
+ output.push('<br />');
224
+ }
225
+ lastWasText = false;
226
+ return;
227
+ }
228
+ if (lastWasText) {
229
+ output.push('<br />');
230
+ }
231
+ output.push(normalized);
232
+ lastWasText = true;
233
+ });
234
+ closeQuote();
235
+ closeAllLists();
236
+ html = output.join('');
237
+ codeBlocks.forEach((block) => {
238
+ html = html.replace(block.token, block.html);
239
+ });
240
+ return html;
241
+ };
242
+ function MarkdownEditorRoot({ value, onChange, children, placeholder = 'Введите текст...', label, editorLabel = 'Редактор', previewLabel = 'Предпросмотр', readOnly = false, minHeight = 220, className, editorClassName, previewClassName, }) {
243
+ const html = React.useMemo(() => renderMarkdown(value), [value]);
244
+ const textareaRef = React.useRef(null);
245
+ const fileInputRef = React.useRef(null);
246
+ const objectUrlsRef = React.useRef([]);
247
+ const copyTimerRef = React.useRef(new Map());
248
+ const editHistoryRef = React.useRef({ undo: [], redo: [] });
249
+ const [activePanel, setActivePanel] = React.useState(null);
250
+ const [linkText, setLinkText] = React.useState('');
251
+ const [linkUrl, setLinkUrl] = React.useState('https://');
252
+ const [imageAlt, setImageAlt] = React.useState('');
253
+ const [imageUrl, setImageUrl] = React.useState('https://');
254
+ const [panelError, setPanelError] = React.useState('');
255
+ const [activeFormats, setActiveFormats] = React.useState({
256
+ bold: false,
257
+ italic: false,
258
+ underline: false,
259
+ strike: false,
260
+ code: false,
261
+ codeBlock: false,
262
+ quote: false,
263
+ list: null,
264
+ heading: null,
265
+ });
266
+ React.useEffect(() => () => {
267
+ if (typeof URL === 'undefined') {
268
+ return;
269
+ }
270
+ objectUrlsRef.current.forEach((url) => URL.revokeObjectURL(url));
271
+ objectUrlsRef.current = [];
272
+ }, []);
273
+ React.useEffect(() => () => {
274
+ copyTimerRef.current.forEach((timerId) => window.clearTimeout(timerId));
275
+ copyTimerRef.current.clear();
276
+ }, []);
277
+ const getLineRange = (start, end) => {
278
+ const lineStart = value.lastIndexOf('\n', start - 1) + 1;
279
+ const lineEndIndex = value.indexOf('\n', end);
280
+ const lineEnd = lineEndIndex === -1 ? value.length : lineEndIndex;
281
+ const line = value.slice(lineStart, lineEnd);
282
+ return { lineStart, lineEnd, line };
283
+ };
284
+ const isInsideCodeBlock = (start, end) => {
285
+ const before = value.slice(0, start);
286
+ const after = value.slice(end);
287
+ const beforeCount = (before.match(/```/g) ?? []).length;
288
+ const afterCount = (after.match(/```/g) ?? []).length;
289
+ return beforeCount % 2 === 1 && afterCount > 0;
290
+ };
291
+ const updateActiveFormats = React.useCallback(() => {
292
+ const target = textareaRef.current;
293
+ if (!target) {
294
+ return;
295
+ }
296
+ const start = target.selectionStart ?? 0;
297
+ const end = target.selectionEnd ?? start;
298
+ const { lineStart, line } = getLineRange(start, end);
299
+ const lineOffset = start - lineStart;
300
+ const headingMatch = /^(\s*)(#{1,6})\s+/.exec(line);
301
+ const heading = headingMatch ? headingMatch[2].length : null;
302
+ const quote = /^>\s?/.test(line);
303
+ const list = /^(\s*)\d+\.\s+/.test(line) ? 'ol' : /^(\s*)[-*]\s+/.test(line) ? 'ul' : null;
304
+ const isWrapped = (marker, forbidAdjacent = false) => {
305
+ if (start !== end) {
306
+ if (start < marker.length) {
307
+ return false;
308
+ }
309
+ return value.slice(start - marker.length, start) === marker
310
+ && value.slice(end, end + marker.length) === marker;
311
+ }
312
+ return Boolean(findWrapInLine(line, lineOffset, marker, forbidAdjacent));
313
+ };
314
+ const isItalicWrapped = () => {
315
+ if (start !== end) {
316
+ if (start < 1) {
317
+ return false;
318
+ }
319
+ const before = value.slice(start - 1, start);
320
+ const after = value.slice(end, end + 1);
321
+ if (before !== '*' || after !== '*') {
322
+ return false;
323
+ }
324
+ const beforePair = start >= 2 ? value.slice(start - 2, start) : '';
325
+ const afterPair = value.slice(end, end + 2);
326
+ return beforePair !== '**' && afterPair !== '**';
327
+ }
328
+ const findSingleLeft = (text, pos) => {
329
+ for (let i = pos - 1; i >= 0; i -= 1) {
330
+ if (text[i] !== '*') {
331
+ continue;
332
+ }
333
+ if (text[i - 1] === '*' || text[i + 1] === '*') {
334
+ continue;
335
+ }
336
+ return i;
337
+ }
338
+ return -1;
339
+ };
340
+ const findSingleRight = (text, pos) => {
341
+ for (let i = pos; i < text.length; i += 1) {
342
+ if (text[i] !== '*') {
343
+ continue;
344
+ }
345
+ if (text[i - 1] === '*' || text[i + 1] === '*') {
346
+ continue;
347
+ }
348
+ return i;
349
+ }
350
+ return -1;
351
+ };
352
+ const left = findSingleLeft(line, lineOffset);
353
+ const right = findSingleRight(line, lineOffset);
354
+ return left !== -1 && right !== -1 && left < lineOffset && right >= lineOffset;
355
+ };
356
+ const codeBlock = isInsideCodeBlock(start, end);
357
+ setActiveFormats({
358
+ bold: isWrapped('**', true),
359
+ italic: isItalicWrapped(),
360
+ underline: isWrapped('__', true),
361
+ strike: isWrapped('~~'),
362
+ code: !codeBlock && isWrapped('`', true),
363
+ codeBlock,
364
+ quote,
365
+ list,
366
+ heading,
367
+ });
368
+ }, [value]);
369
+ React.useEffect(() => {
370
+ updateActiveFormats();
371
+ }, [value, updateActiveFormats]);
372
+ const scheduleSelection = (start, end) => {
373
+ if (typeof window === 'undefined') {
374
+ return;
375
+ }
376
+ window.requestAnimationFrame(() => {
377
+ const target = textareaRef.current;
378
+ if (!target) {
379
+ return;
380
+ }
381
+ target.focus();
382
+ target.setSelectionRange(start, end);
383
+ updateActiveFormats();
384
+ });
385
+ };
386
+ const getSelectionRange = () => {
387
+ const target = textareaRef.current;
388
+ if (!target) {
389
+ return { start: value.length, end: value.length, text: '' };
390
+ }
391
+ const start = target.selectionStart ?? 0;
392
+ const end = target.selectionEnd ?? start;
393
+ return {
394
+ start,
395
+ end,
396
+ text: value.slice(start, end),
397
+ };
398
+ };
399
+ const pushHistory = () => {
400
+ const { start, end } = getSelectionRange();
401
+ const history = editHistoryRef.current;
402
+ history.undo.push({
403
+ value,
404
+ selectionStart: start,
405
+ selectionEnd: end,
406
+ });
407
+ if (history.undo.length > 150) {
408
+ history.undo.shift();
409
+ }
410
+ history.redo = [];
411
+ };
412
+ const undoEdit = () => {
413
+ const history = editHistoryRef.current;
414
+ const snapshot = history.undo.pop();
415
+ if (!snapshot) {
416
+ return false;
417
+ }
418
+ const { start, end } = getSelectionRange();
419
+ history.redo.push({
420
+ value,
421
+ selectionStart: start,
422
+ selectionEnd: end,
423
+ });
424
+ onChange(snapshot.value);
425
+ scheduleSelection(snapshot.selectionStart, snapshot.selectionEnd);
426
+ return true;
427
+ };
428
+ const redoEdit = () => {
429
+ const history = editHistoryRef.current;
430
+ const snapshot = history.redo.pop();
431
+ if (!snapshot) {
432
+ return false;
433
+ }
434
+ const { start, end } = getSelectionRange();
435
+ history.undo.push({
436
+ value,
437
+ selectionStart: start,
438
+ selectionEnd: end,
439
+ });
440
+ onChange(snapshot.value);
441
+ scheduleSelection(snapshot.selectionStart, snapshot.selectionEnd);
442
+ return true;
443
+ };
444
+ const findFenceRange = (selectionStart, selectionEnd) => {
445
+ const fenceRegex = /^```[^\n]*$/gm;
446
+ const fences = [];
447
+ let match = fenceRegex.exec(value);
448
+ while (match) {
449
+ fences.push({
450
+ start: match.index,
451
+ end: match.index + match[0].length,
452
+ });
453
+ match = fenceRegex.exec(value);
454
+ }
455
+ for (let i = 0; i + 1 < fences.length; i += 2) {
456
+ const openFence = fences[i];
457
+ const closeFence = fences[i + 1];
458
+ const openLineEndIndex = value.indexOf('\n', openFence.end);
459
+ const openSectionEnd = openLineEndIndex === -1 ? value.length : openLineEndIndex + 1;
460
+ const closeLineEndIndex = value.indexOf('\n', closeFence.end);
461
+ const closeSectionEnd = closeLineEndIndex === -1 ? value.length : closeLineEndIndex + 1;
462
+ const contentStart = openSectionEnd;
463
+ const contentEnd = closeFence.start;
464
+ const isInside = selectionStart >= contentStart
465
+ && selectionEnd <= contentEnd;
466
+ if (isInside) {
467
+ return {
468
+ openFenceStart: openFence.start,
469
+ contentStart,
470
+ contentEnd,
471
+ closeFenceStart: closeFence.start,
472
+ closeFenceEnd: closeSectionEnd,
473
+ };
474
+ }
475
+ }
476
+ return null;
477
+ };
478
+ const applyEdit = (replacement, rangeStart, rangeEnd, selectionStart, selectionEnd) => {
479
+ pushHistory();
480
+ const target = textareaRef.current;
481
+ if (target && typeof target.setRangeText === 'function') {
482
+ if (document.activeElement !== target) {
483
+ try {
484
+ target.focus({ preventScroll: true });
485
+ }
486
+ catch {
487
+ target.focus();
488
+ }
489
+ }
490
+ target.setRangeText(replacement, rangeStart, rangeEnd, 'preserve');
491
+ onChange(target.value);
492
+ scheduleSelection(selectionStart, selectionEnd);
493
+ return;
494
+ }
495
+ const nextValue = `${value.slice(0, rangeStart)}${replacement}${value.slice(rangeEnd)}`;
496
+ onChange(nextValue);
497
+ scheduleSelection(selectionStart, selectionEnd);
498
+ };
499
+ const replaceSelection = (replacement, selectionStart, selectionEnd) => {
500
+ const { start, end } = getSelectionRange();
501
+ applyEdit(replacement, start, end, selectionStart, selectionEnd);
502
+ };
503
+ const isMarkerAt = (text, index, marker, forbidAdjacent) => {
504
+ if (index < 0 || index + marker.length > text.length) {
505
+ return false;
506
+ }
507
+ if (text.slice(index, index + marker.length) !== marker) {
508
+ return false;
509
+ }
510
+ if (!forbidAdjacent) {
511
+ return true;
512
+ }
513
+ const ch = marker[0];
514
+ const before = text[index - 1];
515
+ const after = text[index + marker.length];
516
+ if (before === ch || after === ch) {
517
+ return false;
518
+ }
519
+ return true;
520
+ };
521
+ const findWrapInLine = (line, offset, marker, forbidAdjacent) => {
522
+ const positions = [];
523
+ for (let i = 0; i <= line.length - marker.length; i += 1) {
524
+ if (isMarkerAt(line, i, marker, forbidAdjacent)) {
525
+ positions.push(i);
526
+ i += marker.length - 1;
527
+ }
528
+ }
529
+ if (positions.length < 2) {
530
+ return null;
531
+ }
532
+ const beforeCount = positions.filter((pos) => pos < offset).length;
533
+ if (beforeCount === 0 || beforeCount % 2 === 0) {
534
+ return null;
535
+ }
536
+ const left = positions[beforeCount - 1];
537
+ const right = positions[beforeCount];
538
+ if (right === undefined || right <= left) {
539
+ return null;
540
+ }
541
+ return { left, right };
542
+ };
543
+ const toggleWrap = (marker, placeholder, forbidAdjacent = false) => {
544
+ const { start, end, text } = getSelectionRange();
545
+ if (start !== end) {
546
+ const hasLeft = start >= marker.length && value.slice(start - marker.length, start) === marker;
547
+ const hasRight = value.slice(end, end + marker.length) === marker;
548
+ if (hasLeft && hasRight) {
549
+ const rangeStart = start - marker.length;
550
+ const rangeEnd = end + marker.length;
551
+ applyEdit(text, rangeStart, rangeEnd, rangeStart, rangeStart + text.length);
552
+ return;
553
+ }
554
+ const content = text || placeholder;
555
+ const replacement = `${marker}${content}${marker}`;
556
+ applyEdit(replacement, start, end, start + marker.length, start + marker.length + content.length);
557
+ return;
558
+ }
559
+ const { lineStart, line } = getLineRange(start, end);
560
+ const offset = start - lineStart;
561
+ const pair = findWrapInLine(line, offset, marker, forbidAdjacent);
562
+ if (pair) {
563
+ const rangeStart = lineStart + pair.left;
564
+ const rangeEnd = lineStart + pair.right + marker.length;
565
+ const content = value.slice(rangeStart + marker.length, rangeEnd - marker.length);
566
+ const cursor = Math.max(rangeStart, start - marker.length);
567
+ applyEdit(content, rangeStart, rangeEnd, cursor, cursor);
568
+ return;
569
+ }
570
+ const replacement = `${marker}${placeholder}${marker}`;
571
+ const selectionStart = start + marker.length;
572
+ const selectionEnd = selectionStart + placeholder.length;
573
+ replaceSelection(replacement, selectionStart, selectionEnd);
574
+ };
575
+ const toggleItalic = () => toggleWrap('*', 'Курсив', true);
576
+ const applyHeading = (level) => {
577
+ const prefix = `${'#'.repeat(level)} `;
578
+ const { start, end } = getSelectionRange();
579
+ const { lineStart, lineEnd, line } = getLineRange(start, end);
580
+ const lines = line.split('\n');
581
+ const allPrefixed = lines.every((lineItem) => lineItem.trim() === '' || lineItem.startsWith(prefix));
582
+ const replacement = lines
583
+ .map((lineItem) => {
584
+ if (lineItem.trim() === '') {
585
+ return lineItem;
586
+ }
587
+ const cleaned = lineItem.replace(/^#{1,6}\s+/, '');
588
+ return allPrefixed ? cleaned : `${prefix}${cleaned}`;
589
+ })
590
+ .join('\n');
591
+ applyEdit(replacement, lineStart, lineEnd, lineStart, lineStart + replacement.length);
592
+ };
593
+ const applyList = (type) => {
594
+ const { start, end } = getSelectionRange();
595
+ const { lineStart, lineEnd, line } = getLineRange(start, end);
596
+ const lines = line.split('\n');
597
+ const prefixMatcher = type === 'ol' ? /^\s*\d+\.\s+/ : /^\s*[-*]\s+/;
598
+ const stripOther = type === 'ol' ? /^\s*[-*]\s+/ : /^\s*\d+\.\s+/;
599
+ const allPrefixed = lines.every((lineItem) => lineItem.trim() === '' || prefixMatcher.test(lineItem));
600
+ const replacement = lines
601
+ .map((lineItem, index) => {
602
+ if (lineItem.trim() === '') {
603
+ return lineItem;
604
+ }
605
+ if (allPrefixed) {
606
+ return lineItem.replace(prefixMatcher, '');
607
+ }
608
+ const cleaned = lineItem.replace(prefixMatcher, '').replace(stripOther, '');
609
+ if (type === 'ul') {
610
+ return `- ${cleaned}`;
611
+ }
612
+ return `${index + 1}. ${cleaned}`;
613
+ })
614
+ .join('\n');
615
+ applyEdit(replacement, lineStart, lineEnd, lineStart, lineStart + replacement.length);
616
+ };
617
+ const applyQuote = () => {
618
+ const { start, end } = getSelectionRange();
619
+ const { lineStart, lineEnd, line } = getLineRange(start, end);
620
+ const lines = line.split('\n');
621
+ const allQuoted = lines.every((lineItem) => lineItem.trim() === '' || /^>\s?/.test(lineItem));
622
+ const replacement = lines
623
+ .map((lineItem) => {
624
+ if (lineItem.trim() === '') {
625
+ return lineItem;
626
+ }
627
+ const cleaned = lineItem.replace(/^>\s?/, '');
628
+ return allQuoted ? cleaned : `> ${cleaned}`;
629
+ })
630
+ .join('\n');
631
+ applyEdit(replacement, lineStart, lineEnd, lineStart, lineStart + replacement.length);
632
+ };
633
+ const toggleCodeBlock = () => {
634
+ const { start, end, text } = getSelectionRange();
635
+ const fenceRange = findFenceRange(start, end);
636
+ if (fenceRange) {
637
+ const content = value.slice(fenceRange.contentStart, fenceRange.contentEnd);
638
+ const selectionStart = Math.max(fenceRange.openFenceStart, fenceRange.openFenceStart + (start - fenceRange.contentStart));
639
+ const selectionEnd = Math.max(selectionStart, fenceRange.openFenceStart + (end - fenceRange.contentStart));
640
+ applyEdit(content, fenceRange.openFenceStart, fenceRange.closeFenceEnd, selectionStart, selectionEnd);
641
+ return;
642
+ }
643
+ const content = text || 'код';
644
+ const replacement = `\`\`\`text\n${content}\n\`\`\``;
645
+ const selectionStart = start + 8;
646
+ const selectionEnd = selectionStart + content.length;
647
+ replaceSelection(replacement, selectionStart, selectionEnd);
648
+ };
649
+ const isValidHttpUrl = (next) => /^https?:\/\/\S+/i.test(next.trim());
650
+ const isValidImageUrl = (next) => /^(https?:\/\/|blob:|data:image\/)\S+/i.test(next.trim());
651
+ const insertLink = () => {
652
+ const trimmed = linkUrl.trim();
653
+ if (!trimmed) {
654
+ setPanelError('Введите URL ссылки.');
655
+ return;
656
+ }
657
+ if (!isValidHttpUrl(trimmed)) {
658
+ setPanelError('Некорректный URL. Разрешены только http/https.');
659
+ return;
660
+ }
661
+ const text = linkText.trim() || 'Ссылка';
662
+ const replacement = `[${text}](${trimmed})`;
663
+ const { start } = getSelectionRange();
664
+ const selectionStart = start + 1;
665
+ const selectionEnd = selectionStart + text.length;
666
+ replaceSelection(replacement, selectionStart, selectionEnd);
667
+ setActivePanel(null);
668
+ setPanelError('');
669
+ };
670
+ const insertImageByUrl = () => {
671
+ const trimmed = imageUrl.trim();
672
+ if (!trimmed) {
673
+ setPanelError('Введите URL изображения.');
674
+ return;
675
+ }
676
+ if (!isValidImageUrl(trimmed)) {
677
+ setPanelError('Некорректный URL. Разрешены http/https, blob, data:image.');
678
+ return;
679
+ }
680
+ const text = imageAlt.trim() || 'Изображение';
681
+ const replacement = `![${text}](${trimmed})`;
682
+ const { start } = getSelectionRange();
683
+ const selectionStart = start + 2;
684
+ const selectionEnd = selectionStart + text.length;
685
+ replaceSelection(replacement, selectionStart, selectionEnd);
686
+ setActivePanel(null);
687
+ setPanelError('');
688
+ };
689
+ const handleFileInsert = (file) => {
690
+ if (!file || typeof URL === 'undefined') {
691
+ return;
692
+ }
693
+ const url = URL.createObjectURL(file);
694
+ objectUrlsRef.current.push(url);
695
+ const baseName = file.name.replace(/\.[^.]+$/, '');
696
+ const replacement = `![${baseName || 'Изображение'}](${url})`;
697
+ const { start } = getSelectionRange();
698
+ const selectionStart = start + 2;
699
+ const selectionEnd = selectionStart + (baseName || 'Изображение').length;
700
+ replaceSelection(replacement, selectionStart, selectionEnd);
701
+ };
702
+ const handleOpenLinkPanel = () => {
703
+ const { text } = getSelectionRange();
704
+ setLinkText(text || 'Ссылка');
705
+ setLinkUrl('https://');
706
+ setActivePanel((prev) => (prev === 'link' ? null : 'link'));
707
+ setPanelError('');
708
+ };
709
+ const handleOpenImagePanel = () => {
710
+ const { text } = getSelectionRange();
711
+ setImageAlt(text || 'Изображение');
712
+ setImageUrl('https://');
713
+ setActivePanel((prev) => (prev === 'image-url' ? null : 'image-url'));
714
+ setPanelError('');
715
+ };
716
+ const handleFileButtonClick = () => {
717
+ if (readOnly) {
718
+ return;
719
+ }
720
+ fileInputRef.current?.click();
721
+ };
722
+ const handleFileChange = (event) => {
723
+ const file = event.target.files?.[0] ?? null;
724
+ handleFileInsert(file);
725
+ event.target.value = '';
726
+ };
727
+ const handlePreviewClick = async (event) => {
728
+ const target = event.target;
729
+ if (!target) {
730
+ return;
731
+ }
732
+ const button = target.closest('[data-code-copy="true"]');
733
+ if (!button) {
734
+ return;
735
+ }
736
+ event.preventDefault();
737
+ const wrapper = button.closest('[data-code-block="true"]');
738
+ const encoded = wrapper?.getAttribute('data-code');
739
+ if (!encoded) {
740
+ return;
741
+ }
742
+ let code = '';
743
+ try {
744
+ code = decodeURIComponent(encoded);
745
+ }
746
+ catch {
747
+ return;
748
+ }
749
+ let copied = false;
750
+ try {
751
+ if (navigator.clipboard?.writeText) {
752
+ await navigator.clipboard.writeText(code);
753
+ copied = true;
754
+ }
755
+ }
756
+ catch {
757
+ copied = false;
758
+ }
759
+ if (!copied && typeof document !== 'undefined') {
760
+ const area = document.createElement('textarea');
761
+ area.value = code;
762
+ area.setAttribute('readonly', 'true');
763
+ area.style.position = 'fixed';
764
+ area.style.opacity = '0';
765
+ document.body.appendChild(area);
766
+ area.select();
767
+ try {
768
+ copied = document.execCommand('copy');
769
+ }
770
+ catch {
771
+ copied = false;
772
+ }
773
+ document.body.removeChild(area);
774
+ }
775
+ if (!copied) {
776
+ return;
777
+ }
778
+ const defaultLabel = button.dataset.defaultLabel || button.textContent || 'Скопировать';
779
+ button.dataset.defaultLabel = defaultLabel;
780
+ button.textContent = 'Скопировано';
781
+ const previousTimer = copyTimerRef.current.get(button);
782
+ if (previousTimer) {
783
+ window.clearTimeout(previousTimer);
784
+ }
785
+ const timerId = window.setTimeout(() => {
786
+ button.textContent = defaultLabel;
787
+ copyTimerRef.current.delete(button);
788
+ }, 1400);
789
+ copyTimerRef.current.set(button, timerId);
790
+ };
791
+ const handleTextareaKeyDown = (event) => {
792
+ if (readOnly) {
793
+ return;
794
+ }
795
+ if (event.nativeEvent.isComposing) {
796
+ return;
797
+ }
798
+ const { start, end, text } = getSelectionRange();
799
+ const hasSelection = start !== end;
800
+ const hasModifier = event.ctrlKey || event.metaKey || event.altKey;
801
+ const key = event.key;
802
+ if (!hasModifier && hasSelection) {
803
+ if (key === '"' || key === "'") {
804
+ const marker = key;
805
+ const hasLeft = start > 0 && value[start - 1] === marker;
806
+ const hasRight = value[end] === marker;
807
+ event.preventDefault();
808
+ event.stopPropagation();
809
+ if (hasLeft && hasRight) {
810
+ scheduleSelection(start, end);
811
+ }
812
+ else {
813
+ replaceSelection(`${marker}${text}${marker}`, start + 1, end + 1);
814
+ }
815
+ return;
816
+ }
817
+ if (key === '*') {
818
+ event.preventDefault();
819
+ event.stopPropagation();
820
+ toggleItalic();
821
+ return;
822
+ }
823
+ if (key === '_') {
824
+ event.preventDefault();
825
+ event.stopPropagation();
826
+ toggleWrap('__', 'Подчеркнутый', true);
827
+ return;
828
+ }
829
+ if (key === '~') {
830
+ event.preventDefault();
831
+ event.stopPropagation();
832
+ toggleWrap('~~', 'Зачеркнутый');
833
+ return;
834
+ }
835
+ }
836
+ const isMod = event.ctrlKey || event.metaKey;
837
+ if (!isMod) {
838
+ return;
839
+ }
840
+ const consume = () => {
841
+ event.preventDefault();
842
+ event.stopPropagation();
843
+ };
844
+ const code = event.code;
845
+ if (!event.altKey && code === 'KeyZ') {
846
+ const handled = event.shiftKey ? redoEdit() : undoEdit();
847
+ if (handled) {
848
+ consume();
849
+ return;
850
+ }
851
+ }
852
+ if (!event.altKey && code === 'KeyY') {
853
+ const handled = redoEdit();
854
+ if (handled) {
855
+ consume();
856
+ return;
857
+ }
858
+ }
859
+ if (event.altKey) {
860
+ const digitMatch = /^Digit([1-6])$/.exec(code);
861
+ const numpadMatch = /^Numpad([1-6])$/.exec(code);
862
+ const level = digitMatch ? Number(digitMatch[1]) : numpadMatch ? Number(numpadMatch[1]) : null;
863
+ if (level) {
864
+ consume();
865
+ applyHeading(level);
866
+ return;
867
+ }
868
+ }
869
+ if (event.shiftKey) {
870
+ switch (code) {
871
+ case 'KeyX':
872
+ consume();
873
+ toggleWrap('~~', 'Зачеркнутый');
874
+ return;
875
+ case 'KeyQ':
876
+ consume();
877
+ applyQuote();
878
+ return;
879
+ case 'KeyK':
880
+ consume();
881
+ handleOpenImagePanel();
882
+ return;
883
+ default:
884
+ break;
885
+ }
886
+ if (code === 'Digit7' || code === 'Numpad7') {
887
+ consume();
888
+ applyList('ol');
889
+ return;
890
+ }
891
+ if (code === 'Digit8' || code === 'Numpad8') {
892
+ consume();
893
+ applyList('ul');
894
+ return;
895
+ }
896
+ }
897
+ if (event.altKey && code === 'KeyC') {
898
+ consume();
899
+ toggleCodeBlock();
900
+ return;
901
+ }
902
+ switch (code) {
903
+ case 'KeyB':
904
+ consume();
905
+ toggleWrap('**', 'Жирный', true);
906
+ return;
907
+ case 'KeyI':
908
+ consume();
909
+ toggleItalic();
910
+ return;
911
+ case 'KeyU':
912
+ consume();
913
+ toggleWrap('__', 'Подчеркнутый', true);
914
+ return;
915
+ case 'KeyE':
916
+ consume();
917
+ toggleWrap('`', 'Код', true);
918
+ return;
919
+ case 'KeyK':
920
+ consume();
921
+ handleOpenLinkPanel();
922
+ return;
923
+ default:
924
+ break;
925
+ }
926
+ };
927
+ const headingActive = activeFormats.heading !== null;
928
+ const formatActive = activeFormats.bold
929
+ || activeFormats.italic
930
+ || activeFormats.underline
931
+ || activeFormats.strike
932
+ || activeFormats.code
933
+ || activeFormats.codeBlock
934
+ || activeFormats.quote
935
+ || activeFormats.list !== null;
936
+ const headingItems = [
937
+ {
938
+ label: 'H1 Заголовок',
939
+ icon: 'H1',
940
+ onClick: () => applyHeading(1),
941
+ meta: 'Ctrl/Meta+Alt+1',
942
+ disabled: readOnly,
943
+ active: activeFormats.heading === 1,
944
+ },
945
+ {
946
+ label: 'H2 Заголовок',
947
+ icon: 'H2',
948
+ onClick: () => applyHeading(2),
949
+ meta: 'Ctrl/Meta+Alt+2',
950
+ disabled: readOnly,
951
+ active: activeFormats.heading === 2,
952
+ },
953
+ {
954
+ label: 'H3 Заголовок',
955
+ icon: 'H3',
956
+ onClick: () => applyHeading(3),
957
+ meta: 'Ctrl/Meta+Alt+3',
958
+ disabled: readOnly,
959
+ active: activeFormats.heading === 3,
960
+ },
961
+ {
962
+ label: 'H4 Заголовок',
963
+ icon: 'H4',
964
+ onClick: () => applyHeading(4),
965
+ meta: 'Ctrl/Meta+Alt+4',
966
+ disabled: readOnly,
967
+ active: activeFormats.heading === 4,
968
+ },
969
+ {
970
+ label: 'H5 Заголовок',
971
+ icon: 'H5',
972
+ onClick: () => applyHeading(5),
973
+ meta: 'Ctrl/Meta+Alt+5',
974
+ disabled: readOnly,
975
+ active: activeFormats.heading === 5,
976
+ },
977
+ {
978
+ label: 'H6 Заголовок',
979
+ icon: 'H6',
980
+ onClick: () => applyHeading(6),
981
+ meta: 'Ctrl/Meta+Alt+6',
982
+ disabled: readOnly,
983
+ active: activeFormats.heading === 6,
984
+ },
985
+ ];
986
+ const formatItems = [
987
+ {
988
+ label: 'Жирный',
989
+ icon: 'B',
990
+ onClick: () => toggleWrap('**', 'Жирный', true),
991
+ meta: 'Ctrl/Meta+B',
992
+ disabled: readOnly,
993
+ active: activeFormats.bold,
994
+ },
995
+ {
996
+ label: 'Курсив',
997
+ icon: 'I',
998
+ onClick: () => toggleItalic(),
999
+ meta: 'Ctrl/Meta+I',
1000
+ disabled: readOnly,
1001
+ active: activeFormats.italic,
1002
+ },
1003
+ {
1004
+ label: 'Подчеркнутый',
1005
+ icon: 'U',
1006
+ onClick: () => toggleWrap('__', 'Подчеркнутый', true),
1007
+ meta: 'Ctrl/Meta+U',
1008
+ disabled: readOnly,
1009
+ active: activeFormats.underline,
1010
+ },
1011
+ {
1012
+ label: 'Зачеркнутый',
1013
+ icon: 'S',
1014
+ onClick: () => toggleWrap('~~', 'Зачеркнутый'),
1015
+ meta: 'Ctrl/Meta+Shift+X',
1016
+ disabled: readOnly,
1017
+ active: activeFormats.strike,
1018
+ },
1019
+ { divider: true },
1020
+ {
1021
+ label: 'Inline код',
1022
+ icon: '</>',
1023
+ onClick: () => toggleWrap('`', 'Код', true),
1024
+ meta: 'Ctrl/Meta+E',
1025
+ disabled: readOnly,
1026
+ active: activeFormats.code,
1027
+ },
1028
+ {
1029
+ label: 'Код блок',
1030
+ icon: '{}',
1031
+ onClick: () => toggleCodeBlock(),
1032
+ meta: 'Ctrl/Meta+Alt+C',
1033
+ disabled: readOnly,
1034
+ active: activeFormats.codeBlock,
1035
+ },
1036
+ { divider: true },
1037
+ {
1038
+ label: 'Цитата',
1039
+ icon: '>',
1040
+ onClick: () => applyQuote(),
1041
+ meta: 'Ctrl/Meta+Shift+Q',
1042
+ disabled: readOnly,
1043
+ active: activeFormats.quote,
1044
+ },
1045
+ {
1046
+ label: 'Маркированный список',
1047
+ icon: '*',
1048
+ onClick: () => applyList('ul'),
1049
+ meta: 'Ctrl/Meta+Shift+8',
1050
+ disabled: readOnly,
1051
+ active: activeFormats.list === 'ul',
1052
+ },
1053
+ {
1054
+ label: 'Нумерованный список',
1055
+ icon: '1.',
1056
+ onClick: () => applyList('ol'),
1057
+ meta: 'Ctrl/Meta+Shift+7',
1058
+ disabled: readOnly,
1059
+ active: activeFormats.list === 'ol',
1060
+ },
1061
+ ];
1062
+ const renderEditorPanel = (slotProps) => {
1063
+ return (_jsxs("div", { className: [styles.panel, editorClassName, slotProps?.className].filter(Boolean).join(' '), children: [_jsx("div", { className: styles.panelHeader, children: slotProps?.label ?? editorLabel }), _jsxs("div", { className: styles.toolbar, children: [_jsx("div", { className: styles.toolbarGroup, children: readOnly ? (_jsxs("span", { className: [
1064
+ styles.toolButton,
1065
+ styles.toolButtonDisabled,
1066
+ headingActive ? styles.toolButtonActive : null,
1067
+ ]
1068
+ .filter(Boolean)
1069
+ .join(' '), children: ["\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A", _jsx("span", { className: styles.toolCaret, "aria-hidden": "true" })] })) : (_jsx(Dropdown, { items: headingItems, align: "left", trigger: _jsxs("span", { className: [styles.toolButton, headingActive ? styles.toolButtonActive : null].filter(Boolean).join(' '), children: ["\u0417\u0430\u0433\u043E\u043B\u043E\u0432\u043E\u043A", _jsx("span", { className: styles.toolCaret, "aria-hidden": "true" })] }) })) }), _jsx("div", { className: styles.toolbarGroup, children: readOnly ? (_jsxs("span", { className: [
1070
+ styles.toolButton,
1071
+ styles.toolButtonDisabled,
1072
+ formatActive ? styles.toolButtonActive : null,
1073
+ ]
1074
+ .filter(Boolean)
1075
+ .join(' '), children: ["\u0424\u043E\u0440\u043C\u0430\u0442", _jsx("span", { className: styles.toolCaret, "aria-hidden": "true" })] })) : (_jsx(Dropdown, { items: formatItems, align: "left", trigger: _jsxs("span", { className: [styles.toolButton, formatActive ? styles.toolButtonActive : null].filter(Boolean).join(' '), children: ["\u0424\u043E\u0440\u043C\u0430\u0442", _jsx("span", { className: styles.toolCaret, "aria-hidden": "true" })] }) })) }), _jsxs("div", { className: styles.toolbarGroup, children: [_jsx("button", { type: "button", className: [styles.toolButton, activePanel === 'link' ? styles.toolButtonActive : null].filter(Boolean).join(' '), onClick: handleOpenLinkPanel, disabled: readOnly, title: "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0441\u0441\u044B\u043B\u043A\u0443 (Ctrl/Meta+K)", children: "Link" }), _jsx("button", { type: "button", className: [styles.toolButton, activePanel === 'image-url' ? styles.toolButtonActive : null].filter(Boolean).join(' '), onClick: handleOpenImagePanel, disabled: readOnly, title: "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435 \u043F\u043E URL (Ctrl/Meta+Shift+K)", children: "Image URL" }), _jsx("button", { type: "button", className: styles.toolButton, onClick: handleFileButtonClick, disabled: readOnly, title: "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435", children: "Image File" }), _jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", className: styles.fileInput, onChange: handleFileChange, tabIndex: -1 })] })] }), activePanel === 'link' ? (_jsxs("div", { className: styles.toolPanel, children: [_jsx("input", { type: "text", className: styles.toolInput, value: linkText, onChange: (event) => {
1076
+ setLinkText(event.target.value);
1077
+ setPanelError('');
1078
+ }, placeholder: "\u0422\u0435\u043A\u0441\u0442 \u0441\u0441\u044B\u043B\u043A\u0438", disabled: readOnly }), _jsx("input", { type: "text", className: styles.toolInput, value: linkUrl, onChange: (event) => {
1079
+ setLinkUrl(event.target.value);
1080
+ setPanelError('');
1081
+ }, placeholder: "https://example.com", disabled: readOnly }), _jsx("button", { type: "button", className: styles.toolAction, onClick: insertLink, disabled: readOnly, children: "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044C" }), panelError ? _jsx("div", { className: styles.toolError, children: panelError }) : null] })) : null, activePanel === 'image-url' ? (_jsxs("div", { className: styles.toolPanel, children: [_jsx("input", { type: "text", className: styles.toolInput, value: imageAlt, onChange: (event) => {
1082
+ setImageAlt(event.target.value);
1083
+ setPanelError('');
1084
+ }, placeholder: "Alt \u0442\u0435\u043A\u0441\u0442", disabled: readOnly }), _jsx("input", { type: "text", className: styles.toolInput, value: imageUrl, onChange: (event) => {
1085
+ setImageUrl(event.target.value);
1086
+ setPanelError('');
1087
+ }, placeholder: "https://example.com/image.png", disabled: readOnly }), _jsx("button", { type: "button", className: styles.toolAction, onClick: insertImageByUrl, disabled: readOnly, children: "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044C" }), panelError ? _jsx("div", { className: styles.toolError, children: panelError }) : null] })) : null, _jsx(Textarea, { ref: textareaRef, value: value, placeholder: placeholder, onChange: (event) => onChange(event.target.value), onKeyDown: handleTextareaKeyDown, onKeyUp: updateActiveFormats, onSelect: updateActiveFormats, onClick: updateActiveFormats, className: styles.textarea, readOnly: readOnly })] }));
1088
+ };
1089
+ const renderPreviewPanel = (slotProps) => {
1090
+ return (_jsxs("div", { className: [styles.panel, previewClassName, slotProps?.className].filter(Boolean).join(' '), children: [_jsx("div", { className: styles.panelHeader, children: slotProps?.label ?? previewLabel }), _jsx("div", { className: styles.preview, "data-empty": value === '', onClick: handlePreviewClick, children: value ? (_jsx("div", { dangerouslySetInnerHTML: { __html: html } })) : (_jsx("div", { className: styles.empty, children: "\u041D\u0435\u0442 \u0442\u0435\u043A\u0441\u0442\u0430 \u0434\u043B\u044F \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430." })) })] }));
1091
+ };
1092
+ const renderValue = (input) => {
1093
+ if (Array.isArray(input)) {
1094
+ let changed = false;
1095
+ const next = input.map((item) => {
1096
+ const rendered = renderValue(item);
1097
+ if (rendered !== item) {
1098
+ changed = true;
1099
+ }
1100
+ return rendered;
1101
+ });
1102
+ return changed ? next : input;
1103
+ }
1104
+ if (React.isValidElement(input)) {
1105
+ if (input.type === MarkdownEditorTextareaSlot) {
1106
+ return renderEditorPanel(input.props);
1107
+ }
1108
+ if (input.type === MarkdownEditorPreviewSlot) {
1109
+ return renderPreviewPanel(input.props);
1110
+ }
1111
+ const props = (input.props ?? {});
1112
+ let changed = false;
1113
+ const nextProps = {};
1114
+ Object.keys(props).forEach((key) => {
1115
+ const nextValue = renderValue(props[key]);
1116
+ nextProps[key] = nextValue;
1117
+ if (nextValue !== props[key]) {
1118
+ changed = true;
1119
+ }
1120
+ });
1121
+ if (!changed) {
1122
+ return input;
1123
+ }
1124
+ return React.cloneElement(input, nextProps);
1125
+ }
1126
+ if (input && typeof input === 'object') {
1127
+ const record = input;
1128
+ let changed = false;
1129
+ const nextRecord = {};
1130
+ Object.keys(record).forEach((key) => {
1131
+ const nextValue = renderValue(record[key]);
1132
+ nextRecord[key] = nextValue;
1133
+ if (nextValue !== record[key]) {
1134
+ changed = true;
1135
+ }
1136
+ });
1137
+ return changed ? nextRecord : input;
1138
+ }
1139
+ return input;
1140
+ };
1141
+ return (_jsxs("div", { className: [styles.root, className].filter(Boolean).join(' '), style: {
1142
+ ['--markdown-editor-min-height']: `${minHeight}px`,
1143
+ }, children: [label ? _jsx("div", { className: styles.label, children: label }) : null, renderValue(children)] }));
1144
+ }
1145
+ const MarkdownEditor = MarkdownEditorRoot;
1146
+ MarkdownEditor.Textarea = MarkdownEditorTextareaSlot;
1147
+ MarkdownEditor.Preview = MarkdownEditorPreviewSlot;
1148
+ export default MarkdownEditor;
1149
+ //# sourceMappingURL=MarkdownEditor.js.map