testomatio-editor-blocks 0.4.49 → 0.4.51
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.
- package/package/editor/blocks/markdown.js +1 -1
- package/package/editor/blocks/step.d.ts +1 -1
- package/package/editor/blocks/step.js +8 -18
- package/package/editor/blocks/stepField.d.ts +11 -0
- package/package/editor/blocks/stepField.js +228 -85
- package/package/editor/createMarkdownPasteHandler.js +18 -0
- package/package/editor/customMarkdownConverter.js +5 -7
- package/package/styles.css +16 -3
- package/package.json +1 -1
- package/src/editor/blocks/markdown.ts +1 -1
- package/src/editor/blocks/step.tsx +26 -42
- package/src/editor/blocks/stepField.tsx +267 -81
- package/src/editor/blocks/stepFieldFormatting.test.ts +44 -0
- package/src/editor/createMarkdownPasteHandler.ts +16 -0
- package/src/editor/customMarkdownConverter.test.ts +3 -3
- package/src/editor/customMarkdownConverter.ts +5 -7
- package/src/editor/styles.css +16 -3
|
@@ -18,6 +18,7 @@ const markdownParser = OverType.MarkdownParser;
|
|
|
18
18
|
function ImageUploadIcon() {
|
|
19
19
|
return (_jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "M12.667 2C13.0335 2.00008 13.3474 2.13057 13.6084 2.3916C13.8694 2.65264 13.9999 2.96648 14 3.33301V12.667C13.9999 13.0335 13.8694 13.3474 13.6084 13.6084C13.3474 13.8694 13.0335 13.9999 12.667 14H3.33301C2.96648 13.9999 2.65264 13.8694 2.3916 13.6084C2.13057 13.3474 2.00008 13.0335 2 12.667V3.33301C2.00008 2.96648 2.13057 2.65264 2.3916 2.3916C2.65264 2.13057 2.96648 2.00008 3.33301 2H12.667ZM3.33301 12.667H12.667V3.33301H3.33301V12.667ZM12 11.333H4L6 8.66699L7.5 10.667L9.5 8L12 11.333ZM5.66699 4.66699C5.94455 4.66707 6.18066 4.76375 6.375 4.95801C6.56944 5.15245 6.66699 5.38921 6.66699 5.66699C6.66692 5.94463 6.56937 6.18063 6.375 6.375C6.18063 6.56937 5.94463 6.66692 5.66699 6.66699C5.38921 6.66699 5.15245 6.56944 4.95801 6.375C4.76375 6.18066 4.66707 5.94455 4.66699 5.66699C4.66699 5.38921 4.76356 5.15245 4.95801 4.95801C5.15245 4.76356 5.38921 4.66699 5.66699 4.66699Z", fill: "currentColor" }) }));
|
|
20
20
|
}
|
|
21
|
+
const UNDO_STACK_LIMIT = 100;
|
|
21
22
|
function getActiveFormats(formatting, selStart, selEnd) {
|
|
22
23
|
const active = new Set();
|
|
23
24
|
if (selStart === selEnd)
|
|
@@ -170,7 +171,7 @@ function stripInlineMarkdown(markdown) {
|
|
|
170
171
|
}
|
|
171
172
|
return { plainText, links, formatting };
|
|
172
173
|
}
|
|
173
|
-
function buildFullMarkdown(plainText, links, formatting) {
|
|
174
|
+
export function buildFullMarkdown(plainText, links, formatting) {
|
|
174
175
|
if (links.length === 0 && formatting.length === 0)
|
|
175
176
|
return plainText;
|
|
176
177
|
const markers = [];
|
|
@@ -184,7 +185,7 @@ function buildFullMarkdown(plainText, links, formatting) {
|
|
|
184
185
|
closeMarker = isMultiline ? "\n```" : "`";
|
|
185
186
|
}
|
|
186
187
|
else {
|
|
187
|
-
openMarker = fmt.type === "bold" ? "**" : "
|
|
188
|
+
openMarker = fmt.type === "bold" ? "**" : "_";
|
|
188
189
|
closeMarker = openMarker;
|
|
189
190
|
}
|
|
190
191
|
// Opening: outer markers (bold) before inner (italic) → bold order=0, italic order=1
|
|
@@ -211,17 +212,24 @@ function buildFullMarkdown(plainText, links, formatting) {
|
|
|
211
212
|
function adjustFormattingForEdit(formatting, editPos, delta) {
|
|
212
213
|
return formatting
|
|
213
214
|
.map((fmt) => {
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
215
|
+
if (delta >= 0) {
|
|
216
|
+
if (editPos <= fmt.start) {
|
|
217
|
+
return { ...fmt, start: fmt.start + delta, end: fmt.end + delta };
|
|
218
|
+
}
|
|
219
|
+
if (editPos >= fmt.end) {
|
|
220
|
+
return fmt;
|
|
221
|
+
}
|
|
222
|
+
return { ...fmt, end: fmt.end + delta };
|
|
219
223
|
}
|
|
220
|
-
|
|
224
|
+
const delEnd = editPos + Math.abs(delta);
|
|
225
|
+
const newStart = fmt.start < editPos ? fmt.start : fmt.start >= delEnd ? fmt.start + delta : editPos;
|
|
226
|
+
const newEnd = fmt.end <= editPos ? fmt.end : fmt.end >= delEnd ? fmt.end + delta : editPos;
|
|
227
|
+
return { ...fmt, start: newStart, end: newEnd };
|
|
221
228
|
})
|
|
222
229
|
.filter((fmt) => fmt.end > fmt.start);
|
|
223
230
|
}
|
|
224
231
|
function getCaretRectInPreview(preview, offset, textareaValue) {
|
|
232
|
+
var _a;
|
|
225
233
|
// Convert textarea-space offset to preview-space (strip newlines)
|
|
226
234
|
let nlCount = 0;
|
|
227
235
|
if (textareaValue) {
|
|
@@ -242,7 +250,16 @@ function getCaretRectInPreview(preview, offset, textareaValue) {
|
|
|
242
250
|
const range = document.createRange();
|
|
243
251
|
range.setStart(textNode, localOffset);
|
|
244
252
|
range.collapse(true);
|
|
245
|
-
|
|
253
|
+
let rect = range.getBoundingClientRect();
|
|
254
|
+
// Collapsed ranges at position 0 can return an empty rect in some browsers
|
|
255
|
+
if (rect.height === 0 && rect.top === 0 && rect.left === 0) {
|
|
256
|
+
const span = document.createElement("span");
|
|
257
|
+
span.textContent = "\u200B";
|
|
258
|
+
range.insertNode(span);
|
|
259
|
+
rect = span.getBoundingClientRect();
|
|
260
|
+
(_a = span.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(span);
|
|
261
|
+
preview.normalize();
|
|
262
|
+
}
|
|
246
263
|
const previewRect = preview.getBoundingClientRect();
|
|
247
264
|
return {
|
|
248
265
|
top: rect.top - previewRect.top + preview.scrollTop,
|
|
@@ -403,13 +420,19 @@ function applyFormattingHighlights(preview, formatting, textareaValue) {
|
|
|
403
420
|
function adjustLinksForEdit(links, editPos, delta) {
|
|
404
421
|
return links
|
|
405
422
|
.map((link) => {
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
423
|
+
if (delta >= 0) {
|
|
424
|
+
if (editPos <= link.start) {
|
|
425
|
+
return { ...link, start: link.start + delta, end: link.end + delta };
|
|
426
|
+
}
|
|
427
|
+
if (editPos >= link.end) {
|
|
428
|
+
return link;
|
|
429
|
+
}
|
|
430
|
+
return { ...link, end: link.end + delta };
|
|
411
431
|
}
|
|
412
|
-
|
|
432
|
+
const delEnd = editPos + Math.abs(delta);
|
|
433
|
+
const newStart = link.start < editPos ? link.start : link.start >= delEnd ? link.start + delta : editPos;
|
|
434
|
+
const newEnd = link.end <= editPos ? link.end : link.end >= delEnd ? link.end + delta : editPos;
|
|
435
|
+
return { ...link, start: newStart, end: newEnd };
|
|
413
436
|
})
|
|
414
437
|
.filter((link) => link.end > link.start);
|
|
415
438
|
}
|
|
@@ -511,8 +534,8 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
511
534
|
const linkSelectionRef = useRef(null);
|
|
512
535
|
const linksRef = useRef([]);
|
|
513
536
|
const formattingRef = useRef([]);
|
|
514
|
-
const
|
|
515
|
-
const
|
|
537
|
+
const undoStackRef = useRef([]);
|
|
538
|
+
const redoStackRef = useRef([]);
|
|
516
539
|
const caretRef = useRef(null);
|
|
517
540
|
const prevTextRef = useRef("");
|
|
518
541
|
const isSyncingRef = useRef(false);
|
|
@@ -524,10 +547,26 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
524
547
|
useEffect(() => {
|
|
525
548
|
onChangeRef.current = onChange;
|
|
526
549
|
}, [onChange]);
|
|
550
|
+
const pushUndoSnapshot = useCallback((text, formatting, links, cursorStart, cursorEnd) => {
|
|
551
|
+
const lastSnapshot = undoStackRef.current[undoStackRef.current.length - 1];
|
|
552
|
+
if (lastSnapshot &&
|
|
553
|
+
lastSnapshot.text === text &&
|
|
554
|
+
JSON.stringify(lastSnapshot.formatting) === JSON.stringify(formatting) &&
|
|
555
|
+
JSON.stringify(lastSnapshot.links) === JSON.stringify(links)) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
undoStackRef.current = [
|
|
559
|
+
...undoStackRef.current.slice(-(UNDO_STACK_LIMIT - 1)),
|
|
560
|
+
{ text, formatting: [...formatting], links: [...links], cursorStart, cursorEnd },
|
|
561
|
+
];
|
|
562
|
+
redoStackRef.current = [];
|
|
563
|
+
}, []);
|
|
527
564
|
const handleEditorChange = useCallback((nextValue) => {
|
|
528
565
|
var _a;
|
|
529
566
|
if (isSyncingRef.current)
|
|
530
567
|
return;
|
|
568
|
+
if (nextValue === prevTextRef.current)
|
|
569
|
+
return;
|
|
531
570
|
const prevText = prevTextRef.current;
|
|
532
571
|
const delta = nextValue.length - prevText.length;
|
|
533
572
|
// Find where the edit happened by comparing old and new text
|
|
@@ -536,10 +575,13 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
536
575
|
while (editPos < minLen && prevText[editPos] === nextValue[editPos]) {
|
|
537
576
|
editPos++;
|
|
538
577
|
}
|
|
578
|
+
// Capture pre-edit state for undo BEFORE mutating
|
|
579
|
+
const prevFormatting = [...formattingRef.current];
|
|
580
|
+
const prevLinks = [...linksRef.current];
|
|
539
581
|
linksRef.current = adjustLinksForEdit(linksRef.current, editPos, delta);
|
|
540
582
|
formattingRef.current = adjustFormattingForEdit(formattingRef.current, editPos, delta);
|
|
541
|
-
|
|
542
|
-
|
|
583
|
+
// Push pre-edit state to undo stack
|
|
584
|
+
pushUndoSnapshot(prevText, prevFormatting, prevLinks, editPos, editPos);
|
|
543
585
|
prevTextRef.current = nextValue;
|
|
544
586
|
const markdown = buildFullMarkdown(nextValue, linksRef.current, formattingRef.current);
|
|
545
587
|
setPlainTextValue((prev) => {
|
|
@@ -547,9 +589,8 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
547
589
|
return prev === normalized ? prev : normalized;
|
|
548
590
|
});
|
|
549
591
|
(_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, markdown);
|
|
550
|
-
}, []);
|
|
592
|
+
}, [pushUndoSnapshot]);
|
|
551
593
|
useEffect(() => {
|
|
552
|
-
var _a;
|
|
553
594
|
const container = editorContainerRef.current;
|
|
554
595
|
if (!container) {
|
|
555
596
|
return;
|
|
@@ -558,6 +599,11 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
558
599
|
linksRef.current = links;
|
|
559
600
|
formattingRef.current = formatting;
|
|
560
601
|
prevTextRef.current = plainText;
|
|
602
|
+
// Push initial state as the baseline undo snapshot
|
|
603
|
+
undoStackRef.current = [
|
|
604
|
+
{ text: plainText, formatting: [...formatting], links: [...links], cursorStart: 0, cursorEnd: 0 },
|
|
605
|
+
];
|
|
606
|
+
redoStackRef.current = [];
|
|
561
607
|
const [instance] = OverType.init(container, {
|
|
562
608
|
value: plainText,
|
|
563
609
|
placeholder: resolvedPlaceholder,
|
|
@@ -575,9 +621,25 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
575
621
|
applyFormattingHighlights(this.preview, formattingRef.current, (_a = this.textarea) === null || _a === void 0 ? void 0 : _a.value);
|
|
576
622
|
applyLinkHighlights(this.preview, linksRef.current);
|
|
577
623
|
};
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
|
|
624
|
+
// Force a full update through the monkey-patched pipeline
|
|
625
|
+
instance.updatePreview();
|
|
626
|
+
// Safety net: re-apply formatting if the preview gets reset externally
|
|
627
|
+
// (e.g. by the original updatePreview being called outside our monkey-patch)
|
|
628
|
+
let isApplyingFormatting = false;
|
|
629
|
+
const formattingObserver = new MutationObserver(() => {
|
|
630
|
+
var _a;
|
|
631
|
+
if (isApplyingFormatting)
|
|
632
|
+
return;
|
|
633
|
+
const hasFormatting = formattingRef.current.length > 0;
|
|
634
|
+
const hasAnyFormattingElement = instance.preview.querySelector("strong.step-preview-bold, em.step-preview-italic, code.step-preview-code") !== null;
|
|
635
|
+
if (hasFormatting && !hasAnyFormattingElement) {
|
|
636
|
+
isApplyingFormatting = true;
|
|
637
|
+
applyFormattingHighlights(instance.preview, formattingRef.current, (_a = instance.textarea) === null || _a === void 0 ? void 0 : _a.value);
|
|
638
|
+
applyLinkHighlights(instance.preview, linksRef.current);
|
|
639
|
+
isApplyingFormatting = false;
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
formattingObserver.observe(instance.preview, { childList: true, subtree: true });
|
|
581
643
|
// Create custom caret element inside the wrapper
|
|
582
644
|
const caretEl = document.createElement("div");
|
|
583
645
|
caretEl.className = "bn-step-custom-caret";
|
|
@@ -586,6 +648,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
586
648
|
editorInstanceRef.current = instance;
|
|
587
649
|
setTextareaNode(instance.textarea);
|
|
588
650
|
return () => {
|
|
651
|
+
formattingObserver.disconnect();
|
|
589
652
|
caretRef.current = null;
|
|
590
653
|
instance.destroy();
|
|
591
654
|
editorInstanceRef.current = null;
|
|
@@ -709,6 +772,21 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
709
772
|
delete textareaNode.dataset.stepField;
|
|
710
773
|
}
|
|
711
774
|
}, [fieldName, textareaNode]);
|
|
775
|
+
// Block native undo/redo at the beforeinput level so the browser never
|
|
776
|
+
// applies its own history on the textarea — our custom stack handles it.
|
|
777
|
+
useEffect(() => {
|
|
778
|
+
if (!textareaNode)
|
|
779
|
+
return;
|
|
780
|
+
const blockNativeUndoRedo = (e) => {
|
|
781
|
+
if (e.inputType === "historyUndo" || e.inputType === "historyRedo") {
|
|
782
|
+
e.preventDefault();
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
textareaNode.addEventListener("beforeinput", blockNativeUndoRedo);
|
|
786
|
+
return () => {
|
|
787
|
+
textareaNode.removeEventListener("beforeinput", blockNativeUndoRedo);
|
|
788
|
+
};
|
|
789
|
+
}, [textareaNode]);
|
|
712
790
|
useEffect(() => {
|
|
713
791
|
if (!textareaNode) {
|
|
714
792
|
return;
|
|
@@ -735,6 +813,16 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
735
813
|
const handleBlur = () => {
|
|
736
814
|
setIsFocused(false);
|
|
737
815
|
setShowAllSuggestions(false);
|
|
816
|
+
// Re-apply formatting highlights after blur because OverType may
|
|
817
|
+
// re-render the preview (via debounced selectionchange) and strip them.
|
|
818
|
+
const instance = editorInstanceRef.current;
|
|
819
|
+
if (instance) {
|
|
820
|
+
requestAnimationFrame(() => {
|
|
821
|
+
var _a;
|
|
822
|
+
applyFormattingHighlights(instance.preview, formattingRef.current, (_a = instance.textarea) === null || _a === void 0 ? void 0 : _a.value);
|
|
823
|
+
applyLinkHighlights(instance.preview, linksRef.current);
|
|
824
|
+
});
|
|
825
|
+
}
|
|
738
826
|
};
|
|
739
827
|
textareaNode.addEventListener("focus", handleFocus);
|
|
740
828
|
textareaNode.addEventListener("blur", handleBlur);
|
|
@@ -870,24 +958,22 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
870
958
|
// Check if selection is already formatted
|
|
871
959
|
const existingIdx = formattingRef.current.findIndex((f) => f.type === fmtType && f.start <= start && f.end >= end);
|
|
872
960
|
// Save current state for undo before modifying
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
{ formatting: [...formattingRef.current], links: [...linksRef.current] },
|
|
876
|
-
];
|
|
877
|
-
formattingRedoRef.current = [];
|
|
961
|
+
const currentText = instance.getValue();
|
|
962
|
+
pushUndoSnapshot(currentText, formattingRef.current, linksRef.current, start, end);
|
|
878
963
|
if (existingIdx !== -1) {
|
|
879
964
|
// Remove formatting
|
|
880
965
|
formattingRef.current = formattingRef.current.filter((_, i) => i !== existingIdx);
|
|
881
966
|
}
|
|
882
967
|
else if (start !== end) {
|
|
883
|
-
// Remove overlapping formatting
|
|
884
|
-
|
|
968
|
+
// Remove overlapping formatting:
|
|
969
|
+
// - Code: remove ALL overlapping formatting (code replaces bold/italic)
|
|
970
|
+
// - Bold/Italic: remove only overlapping formatting of the SAME type
|
|
971
|
+
formattingRef.current = formattingRef.current.filter((f) => f.start >= end || f.end <= start || (fmtType !== "code" && f.type !== fmtType));
|
|
885
972
|
// Add formatting for selection
|
|
886
973
|
formattingRef.current = [...formattingRef.current, { start, end, type: fmtType }];
|
|
887
974
|
}
|
|
888
975
|
else {
|
|
889
976
|
// No selection — nothing to format
|
|
890
|
-
formattingUndoRef.current = formattingUndoRef.current.slice(0, -1);
|
|
891
977
|
return;
|
|
892
978
|
}
|
|
893
979
|
const currentValue = instance.getValue();
|
|
@@ -898,7 +984,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
898
984
|
// Re-apply highlights
|
|
899
985
|
applyFormattingHighlights(instance.preview, formattingRef.current, textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.value);
|
|
900
986
|
applyLinkHighlights(instance.preview, linksRef.current);
|
|
901
|
-
}, [textareaNode]);
|
|
987
|
+
}, [textareaNode, pushUndoSnapshot]);
|
|
902
988
|
const updateActiveFormats = useCallback(() => {
|
|
903
989
|
var _a, _b;
|
|
904
990
|
if (!textareaNode)
|
|
@@ -973,6 +1059,8 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
973
1059
|
return;
|
|
974
1060
|
}
|
|
975
1061
|
const currentValue = instance.getValue();
|
|
1062
|
+
// Push undo snapshot before link edit
|
|
1063
|
+
pushUndoSnapshot(currentValue, formattingRef.current, linksRef.current, sel.start, sel.end);
|
|
976
1064
|
const linkText = text || sel.text || url;
|
|
977
1065
|
// Replace selected text with link display text (no markdown syntax in textarea)
|
|
978
1066
|
const before = currentValue.slice(0, sel.start);
|
|
@@ -995,20 +1083,26 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
995
1083
|
linkSelectionRef.current = null;
|
|
996
1084
|
setCursorLink(null);
|
|
997
1085
|
requestAnimationFrame(() => textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.focus());
|
|
998
|
-
}, [textareaNode]);
|
|
1086
|
+
}, [textareaNode, pushUndoSnapshot]);
|
|
999
1087
|
const handleRemoveLink = useCallback(() => {
|
|
1000
|
-
var _a, _b;
|
|
1088
|
+
var _a, _b, _c, _d;
|
|
1089
|
+
const instance = editorInstanceRef.current;
|
|
1090
|
+
// Push undo snapshot before link removal
|
|
1091
|
+
if (instance) {
|
|
1092
|
+
const currentText = instance.getValue();
|
|
1093
|
+
const cursorPos = (_b = (_a = instance.textarea) === null || _a === void 0 ? void 0 : _a.selectionStart) !== null && _b !== void 0 ? _b : 0;
|
|
1094
|
+
pushUndoSnapshot(currentText, formattingRef.current, linksRef.current, cursorPos, cursorPos);
|
|
1095
|
+
}
|
|
1001
1096
|
linksRef.current = linksRef.current.filter((l) => l !== cursorLink);
|
|
1002
1097
|
setCursorLink(null);
|
|
1003
|
-
const instance = editorInstanceRef.current;
|
|
1004
1098
|
if (instance) {
|
|
1005
1099
|
const markdown = buildFullMarkdown(instance.getValue(), linksRef.current, formattingRef.current);
|
|
1006
|
-
(
|
|
1100
|
+
(_c = onChangeRef.current) === null || _c === void 0 ? void 0 : _c.call(onChangeRef, markdown);
|
|
1007
1101
|
// Re-apply highlights since links changed
|
|
1008
|
-
applyFormattingHighlights(instance.preview, formattingRef.current, (
|
|
1102
|
+
applyFormattingHighlights(instance.preview, formattingRef.current, (_d = instance.textarea) === null || _d === void 0 ? void 0 : _d.value);
|
|
1009
1103
|
applyLinkHighlights(instance.preview, linksRef.current);
|
|
1010
1104
|
}
|
|
1011
|
-
}, [cursorLink]);
|
|
1105
|
+
}, [cursorLink, pushUndoSnapshot]);
|
|
1012
1106
|
const suggestionPool = useMemo(() => {
|
|
1013
1107
|
if (!suggestionFilter) {
|
|
1014
1108
|
return suggestions;
|
|
@@ -1062,6 +1156,11 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
1062
1156
|
}, [value]);
|
|
1063
1157
|
const handleRemoveImage = useCallback((image) => {
|
|
1064
1158
|
var _a;
|
|
1159
|
+
// Push undo snapshot before image removal
|
|
1160
|
+
if (editorInstanceRef.current) {
|
|
1161
|
+
const currentText = editorInstanceRef.current.getValue();
|
|
1162
|
+
pushUndoSnapshot(currentText, formattingRef.current, linksRef.current, image.start, image.end);
|
|
1163
|
+
}
|
|
1065
1164
|
const before = value.slice(0, image.start);
|
|
1066
1165
|
const after = value.slice(image.end);
|
|
1067
1166
|
const nextValue = `${before}${after}`.replace(/\n{3,}/g, "\n\n");
|
|
@@ -1071,7 +1170,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
1071
1170
|
(_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, nextValue);
|
|
1072
1171
|
setPlainTextValue(markdownToPlainText(nextValue));
|
|
1073
1172
|
setPreviewImageUrl((prev) => (prev === image.url ? null : prev));
|
|
1074
|
-
}, [value]);
|
|
1173
|
+
}, [value, pushUndoSnapshot]);
|
|
1075
1174
|
const handleImageClick = useCallback((url) => {
|
|
1076
1175
|
setPreviewImageUrl(url);
|
|
1077
1176
|
}, []);
|
|
@@ -1096,14 +1195,18 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
1096
1195
|
return true;
|
|
1097
1196
|
}, [textareaNode]);
|
|
1098
1197
|
const applySuggestion = useCallback((suggestion) => {
|
|
1099
|
-
var _a;
|
|
1198
|
+
var _a, _b;
|
|
1100
1199
|
const escaped = escapeMarkdownText(suggestion.title);
|
|
1101
1200
|
const instance = editorInstanceRef.current;
|
|
1102
1201
|
if (instance) {
|
|
1202
|
+
// Push undo snapshot before applying suggestion
|
|
1203
|
+
const currentText = instance.getValue();
|
|
1204
|
+
const cursorPos = (_a = textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.selectionStart) !== null && _a !== void 0 ? _a : 0;
|
|
1205
|
+
pushUndoSnapshot(currentText, formattingRef.current, linksRef.current, cursorPos, cursorPos);
|
|
1103
1206
|
instance.setValue(escaped);
|
|
1104
1207
|
}
|
|
1105
1208
|
setPlainTextValue(suggestion.title);
|
|
1106
|
-
(
|
|
1209
|
+
(_b = onChangeRef.current) === null || _b === void 0 ? void 0 : _b.call(onChangeRef, escaped);
|
|
1107
1210
|
onSuggestionSelect === null || onSuggestionSelect === void 0 ? void 0 : onSuggestionSelect(suggestion);
|
|
1108
1211
|
setActiveSuggestionIndex(0);
|
|
1109
1212
|
setShowAllSuggestions(false);
|
|
@@ -1114,11 +1217,11 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
1114
1217
|
textareaNode.selectionEnd = escaped.length;
|
|
1115
1218
|
}
|
|
1116
1219
|
});
|
|
1117
|
-
}, [onSuggestionSelect, textareaNode]);
|
|
1220
|
+
}, [onSuggestionSelect, textareaNode, pushUndoSnapshot]);
|
|
1118
1221
|
const keydownHandlerRef = useRef(null);
|
|
1119
1222
|
useEffect(() => {
|
|
1120
1223
|
keydownHandlerRef.current = (event) => {
|
|
1121
|
-
var _a, _b, _c, _d, _e, _f;
|
|
1224
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
1122
1225
|
if (readOnly) {
|
|
1123
1226
|
const openKeys = enableAutocomplete && (event.metaKey || event.ctrlKey) && AUTOCOMPLETE_TRIGGER_KEYS.has(event.code);
|
|
1124
1227
|
if (!READ_ONLY_ALLOWED_KEYS.has(event.key) && !openKeys) {
|
|
@@ -1147,54 +1250,94 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
1147
1250
|
handleToolbarAction("toggleCode");
|
|
1148
1251
|
return;
|
|
1149
1252
|
}
|
|
1253
|
+
if (event.key === "a" || event.key === "A") {
|
|
1254
|
+
event.stopPropagation();
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1150
1257
|
if (event.key === "z" || event.key === "Z") {
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
];
|
|
1159
|
-
const prev = undoStack[undoStack.length - 1];
|
|
1160
|
-
formattingUndoRef.current = undoStack.slice(0, -1);
|
|
1161
|
-
formattingRef.current = prev.formatting;
|
|
1162
|
-
linksRef.current = prev.links;
|
|
1163
|
-
const instance = editorInstanceRef.current;
|
|
1164
|
-
if (instance) {
|
|
1165
|
-
const markdown = buildFullMarkdown(instance.getValue(), linksRef.current, formattingRef.current);
|
|
1166
|
-
(_b = onChangeRef.current) === null || _b === void 0 ? void 0 : _b.call(onChangeRef, markdown);
|
|
1167
|
-
setPlainTextValue(markdownToPlainText(markdown));
|
|
1168
|
-
applyFormattingHighlights(instance.preview, formattingRef.current, (_c = instance.textarea) === null || _c === void 0 ? void 0 : _c.value);
|
|
1169
|
-
applyLinkHighlights(instance.preview, linksRef.current);
|
|
1170
|
-
}
|
|
1258
|
+
event.preventDefault();
|
|
1259
|
+
event.stopImmediatePropagation();
|
|
1260
|
+
const stack = undoStackRef.current;
|
|
1261
|
+
if (stack.length === 0)
|
|
1262
|
+
return;
|
|
1263
|
+
const instance = editorInstanceRef.current;
|
|
1264
|
+
if (!instance)
|
|
1171
1265
|
return;
|
|
1266
|
+
// Push current state to redo stack
|
|
1267
|
+
const currentText = instance.getValue();
|
|
1268
|
+
redoStackRef.current = [
|
|
1269
|
+
...redoStackRef.current,
|
|
1270
|
+
{
|
|
1271
|
+
text: currentText,
|
|
1272
|
+
formatting: [...formattingRef.current],
|
|
1273
|
+
links: [...linksRef.current],
|
|
1274
|
+
cursorStart: (_b = textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.selectionStart) !== null && _b !== void 0 ? _b : 0,
|
|
1275
|
+
cursorEnd: (_c = textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.selectionEnd) !== null && _c !== void 0 ? _c : 0,
|
|
1276
|
+
},
|
|
1277
|
+
];
|
|
1278
|
+
// Pop from undo stack
|
|
1279
|
+
const prev = stack[stack.length - 1];
|
|
1280
|
+
undoStackRef.current = stack.slice(0, -1);
|
|
1281
|
+
// Restore state
|
|
1282
|
+
formattingRef.current = prev.formatting;
|
|
1283
|
+
linksRef.current = prev.links;
|
|
1284
|
+
prevTextRef.current = prev.text;
|
|
1285
|
+
isSyncingRef.current = true;
|
|
1286
|
+
instance.setValue(prev.text);
|
|
1287
|
+
isSyncingRef.current = false;
|
|
1288
|
+
if (textareaNode) {
|
|
1289
|
+
textareaNode.selectionStart = prev.cursorStart;
|
|
1290
|
+
textareaNode.selectionEnd = prev.cursorEnd;
|
|
1172
1291
|
}
|
|
1292
|
+
const markdown = buildFullMarkdown(prev.text, prev.links, prev.formatting);
|
|
1293
|
+
(_d = onChangeRef.current) === null || _d === void 0 ? void 0 : _d.call(onChangeRef, markdown);
|
|
1294
|
+
setPlainTextValue(markdownToPlainText(markdown));
|
|
1295
|
+
applyFormattingHighlights(instance.preview, formattingRef.current, (_e = instance.textarea) === null || _e === void 0 ? void 0 : _e.value);
|
|
1296
|
+
applyLinkHighlights(instance.preview, linksRef.current);
|
|
1297
|
+
return;
|
|
1173
1298
|
}
|
|
1174
1299
|
}
|
|
1175
1300
|
if (modKey && event.shiftKey && (event.key === "z" || event.key === "Z")) {
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
];
|
|
1184
|
-
const next = redoStack[redoStack.length - 1];
|
|
1185
|
-
formattingRedoRef.current = redoStack.slice(0, -1);
|
|
1186
|
-
formattingRef.current = next.formatting;
|
|
1187
|
-
linksRef.current = next.links;
|
|
1188
|
-
const instance = editorInstanceRef.current;
|
|
1189
|
-
if (instance) {
|
|
1190
|
-
const markdown = buildFullMarkdown(instance.getValue(), linksRef.current, formattingRef.current);
|
|
1191
|
-
(_d = onChangeRef.current) === null || _d === void 0 ? void 0 : _d.call(onChangeRef, markdown);
|
|
1192
|
-
setPlainTextValue(markdownToPlainText(markdown));
|
|
1193
|
-
applyFormattingHighlights(instance.preview, formattingRef.current, (_e = instance.textarea) === null || _e === void 0 ? void 0 : _e.value);
|
|
1194
|
-
applyLinkHighlights(instance.preview, linksRef.current);
|
|
1195
|
-
}
|
|
1301
|
+
event.preventDefault();
|
|
1302
|
+
event.stopImmediatePropagation();
|
|
1303
|
+
const stack = redoStackRef.current;
|
|
1304
|
+
if (stack.length === 0)
|
|
1305
|
+
return;
|
|
1306
|
+
const instance = editorInstanceRef.current;
|
|
1307
|
+
if (!instance)
|
|
1196
1308
|
return;
|
|
1309
|
+
// Push current state to undo stack
|
|
1310
|
+
const currentText = instance.getValue();
|
|
1311
|
+
undoStackRef.current = [
|
|
1312
|
+
...undoStackRef.current,
|
|
1313
|
+
{
|
|
1314
|
+
text: currentText,
|
|
1315
|
+
formatting: [...formattingRef.current],
|
|
1316
|
+
links: [...linksRef.current],
|
|
1317
|
+
cursorStart: (_f = textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.selectionStart) !== null && _f !== void 0 ? _f : 0,
|
|
1318
|
+
cursorEnd: (_g = textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.selectionEnd) !== null && _g !== void 0 ? _g : 0,
|
|
1319
|
+
},
|
|
1320
|
+
];
|
|
1321
|
+
// Pop from redo stack
|
|
1322
|
+
const next = stack[stack.length - 1];
|
|
1323
|
+
redoStackRef.current = stack.slice(0, -1);
|
|
1324
|
+
// Restore state
|
|
1325
|
+
formattingRef.current = next.formatting;
|
|
1326
|
+
linksRef.current = next.links;
|
|
1327
|
+
prevTextRef.current = next.text;
|
|
1328
|
+
isSyncingRef.current = true;
|
|
1329
|
+
instance.setValue(next.text);
|
|
1330
|
+
isSyncingRef.current = false;
|
|
1331
|
+
if (textareaNode) {
|
|
1332
|
+
textareaNode.selectionStart = next.cursorStart;
|
|
1333
|
+
textareaNode.selectionEnd = next.cursorEnd;
|
|
1197
1334
|
}
|
|
1335
|
+
const markdown = buildFullMarkdown(next.text, next.links, next.formatting);
|
|
1336
|
+
(_h = onChangeRef.current) === null || _h === void 0 ? void 0 : _h.call(onChangeRef, markdown);
|
|
1337
|
+
setPlainTextValue(markdownToPlainText(markdown));
|
|
1338
|
+
applyFormattingHighlights(instance.preview, next.formatting, (_j = instance.textarea) === null || _j === void 0 ? void 0 : _j.value);
|
|
1339
|
+
applyLinkHighlights(instance.preview, next.links);
|
|
1340
|
+
return;
|
|
1198
1341
|
}
|
|
1199
1342
|
if (enableAutocomplete && shouldShowAutocomplete) {
|
|
1200
1343
|
if (event.key === "ArrowDown") {
|
|
@@ -1209,7 +1352,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
1209
1352
|
}
|
|
1210
1353
|
if (event.key === "Enter" || event.key === "Tab") {
|
|
1211
1354
|
event.preventDefault();
|
|
1212
|
-
const suggestion = (
|
|
1355
|
+
const suggestion = (_k = filteredSuggestions[activeSuggestionIndex]) !== null && _k !== void 0 ? _k : filteredSuggestions[0];
|
|
1213
1356
|
if (suggestion) {
|
|
1214
1357
|
applySuggestion(suggestion);
|
|
1215
1358
|
}
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
const BLOCK_MARKDOWN_PREFIX = /^(\s*)(#{1,6}\s|[-*+]\s|\d+[.)]\s|>\s|```|~~~|\||!\[)/;
|
|
2
|
+
function isInlineOnlyPaste(plainText, parsedBlocks) {
|
|
3
|
+
if (parsedBlocks.length !== 1)
|
|
4
|
+
return false;
|
|
5
|
+
const [block] = parsedBlocks;
|
|
6
|
+
if (block.type !== "paragraph")
|
|
7
|
+
return false;
|
|
8
|
+
if (block.children && block.children.length > 0)
|
|
9
|
+
return false;
|
|
10
|
+
if (/\r?\n/.test(plainText))
|
|
11
|
+
return false;
|
|
12
|
+
if (BLOCK_MARKDOWN_PREFIX.test(plainText))
|
|
13
|
+
return false;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
1
16
|
export function createMarkdownPasteHandler(converter) {
|
|
2
17
|
return ({ event, editor, defaultPasteHandler }) => {
|
|
3
18
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
@@ -21,6 +36,9 @@ export function createMarkdownPasteHandler(converter) {
|
|
|
21
36
|
const parsedBlocks = converter(plainText);
|
|
22
37
|
if (parsedBlocks.length === 0)
|
|
23
38
|
return defaultPasteHandler();
|
|
39
|
+
if (isInlineOnlyPaste(plainText, parsedBlocks)) {
|
|
40
|
+
return defaultPasteHandler({ plainTextAsMarkdown: false });
|
|
41
|
+
}
|
|
24
42
|
const selection = editor.getSelection();
|
|
25
43
|
const selectedIds = (_h = (_g = selection === null || selection === void 0 ? void 0 : selection.blocks) === null || _g === void 0 ? void 0 : _g.map((block) => block.id).filter((id) => Boolean(id))) !== null && _h !== void 0 ? _h : [];
|
|
26
44
|
if (selectedIds.length > 0) {
|
|
@@ -100,7 +100,7 @@ function applyTextStyles(text, styles) {
|
|
|
100
100
|
wrappers.push({ prefix: "**", suffix: "**" });
|
|
101
101
|
}
|
|
102
102
|
if (styles.italic) {
|
|
103
|
-
wrappers.push({ prefix: "
|
|
103
|
+
wrappers.push({ prefix: "_", suffix: "_" });
|
|
104
104
|
}
|
|
105
105
|
if (styles.strike) {
|
|
106
106
|
wrappers.push({ prefix: "~~", suffix: "~~" });
|
|
@@ -511,10 +511,7 @@ function serializeBlocks(blocks, ctx) {
|
|
|
511
511
|
}
|
|
512
512
|
export function blocksToMarkdown(blocks) {
|
|
513
513
|
const lines = serializeBlocks(blocks, { listDepth: 0, insideQuote: false });
|
|
514
|
-
const cleaned = lines
|
|
515
|
-
.join("\n")
|
|
516
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
517
|
-
.trimEnd();
|
|
514
|
+
const cleaned = lines.join("\n").trimEnd();
|
|
518
515
|
return cleaned;
|
|
519
516
|
}
|
|
520
517
|
function parseInlineMarkdown(text) {
|
|
@@ -592,8 +589,9 @@ function parseInlineMarkdown(text) {
|
|
|
592
589
|
continue;
|
|
593
590
|
}
|
|
594
591
|
}
|
|
595
|
-
if (cleaned
|
|
596
|
-
const
|
|
592
|
+
if (cleaned[i] === "*" || cleaned[i] === "_") {
|
|
593
|
+
const marker = cleaned[i];
|
|
594
|
+
const end = cleaned.indexOf(marker, i + 1);
|
|
597
595
|
if (end !== -1) {
|
|
598
596
|
pushPlain();
|
|
599
597
|
const inner = cleaned.slice(i + 1, end);
|
package/package/styles.css
CHANGED
|
@@ -947,6 +947,10 @@ html.dark .bn-step-image-preview__content {
|
|
|
947
947
|
color: var(--step-input-border-focus);
|
|
948
948
|
}
|
|
949
949
|
|
|
950
|
+
.bn-container {
|
|
951
|
+
overflow-x: clip;
|
|
952
|
+
}
|
|
953
|
+
|
|
950
954
|
[data-tooltip] {
|
|
951
955
|
position: relative;
|
|
952
956
|
}
|
|
@@ -1079,7 +1083,8 @@ html.dark .bn-step-image-preview__content {
|
|
|
1079
1083
|
}
|
|
1080
1084
|
|
|
1081
1085
|
.bn-step-editor .overtype-wrapper .overtype-preview strong.step-preview-bold {
|
|
1082
|
-
|
|
1086
|
+
-webkit-text-stroke: 0.5px currentColor;
|
|
1087
|
+
font-weight: inherit !important;
|
|
1083
1088
|
color: inherit !important;
|
|
1084
1089
|
}
|
|
1085
1090
|
|
|
@@ -1090,8 +1095,8 @@ html.dark .bn-step-image-preview__content {
|
|
|
1090
1095
|
|
|
1091
1096
|
.bn-step-editor .overtype-wrapper .overtype-preview code.step-preview-code {
|
|
1092
1097
|
background-color: transparent !important;
|
|
1093
|
-
|
|
1094
|
-
font-
|
|
1098
|
+
font-family: inherit !important;
|
|
1099
|
+
font-size: inherit !important;
|
|
1095
1100
|
color: rgb(146, 64, 14) !important;
|
|
1096
1101
|
}
|
|
1097
1102
|
|
|
@@ -1356,3 +1361,11 @@ html.dark .bn-step-image-preview__content {
|
|
|
1356
1361
|
.bn-testcase--draft {
|
|
1357
1362
|
--status-color: var(--status-default);
|
|
1358
1363
|
}
|
|
1364
|
+
|
|
1365
|
+
/* Prevent unnecessary horizontal scrollbar on BlockNote tables.
|
|
1366
|
+
ProseMirror sets overflow-x: auto on .tableWrapper, but the
|
|
1367
|
+
table-widgets-container extends slightly past the wrapper edge,
|
|
1368
|
+
triggering a scrollbar even when table content fits. */
|
|
1369
|
+
.bn-editor [data-content-type="table"] .tableWrapper {
|
|
1370
|
+
overflow-x: hidden;
|
|
1371
|
+
}
|
package/package.json
CHANGED
|
@@ -140,7 +140,7 @@ function fallbackHtmlToMarkdown(html: string): string {
|
|
|
140
140
|
.replace(/<br\s*\/?>/gi, "\n")
|
|
141
141
|
.replace(/<\/?(div|p)>/gi, "\n")
|
|
142
142
|
.replace(/<strong>(.*?)<\/strong>/gis, (_m, content) => `**${content}**`)
|
|
143
|
-
.replace(/<(em|i)>(.*?)<\/(em|i)>/gis, (_m, _tag, content) =>
|
|
143
|
+
.replace(/<(em|i)>(.*?)<\/(em|i)>/gis, (_m, _tag, content) => `_${content}_`)
|
|
144
144
|
.replace(/<span[^>]*>/gi, "")
|
|
145
145
|
.replace(/<\/span>/gi, "")
|
|
146
146
|
.replace(/<u>(.*?)<\/u>/gis, (_m, content) => `<u>${content}</u>`);
|