testomatio-editor-blocks 0.4.46 → 0.4.47
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/stepField.js +132 -12
- package/package/styles.css +9 -4
- package/package.json +1 -1
- package/src/editor/blocks/stepField.tsx +137 -8
- package/src/editor/styles.css +9 -4
|
@@ -18,6 +18,17 @@ 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
|
+
function getActiveFormats(formatting, selStart, selEnd) {
|
|
22
|
+
const active = new Set();
|
|
23
|
+
if (selStart === selEnd)
|
|
24
|
+
return active;
|
|
25
|
+
for (const f of formatting) {
|
|
26
|
+
if (selStart < f.end && selEnd > f.start) {
|
|
27
|
+
active.add(f.type);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return active;
|
|
31
|
+
}
|
|
21
32
|
function stripInlineMarkdown(markdown) {
|
|
22
33
|
const links = [];
|
|
23
34
|
const formatting = [];
|
|
@@ -248,8 +259,6 @@ function getCaretRectInPreview(preview, offset, textareaValue) {
|
|
|
248
259
|
return null;
|
|
249
260
|
}
|
|
250
261
|
function applyFormattingHighlights(preview, formatting, textareaValue) {
|
|
251
|
-
if (formatting.length === 0)
|
|
252
|
-
return;
|
|
253
262
|
// Remove previous formatting highlights
|
|
254
263
|
const existingBold = preview.querySelectorAll("strong.step-preview-bold");
|
|
255
264
|
for (let i = 0; i < existingBold.length; i++) {
|
|
@@ -287,6 +296,8 @@ function applyFormattingHighlights(preview, formatting, textareaValue) {
|
|
|
287
296
|
// After unwrapping formatting elements, merge adjacent/empty text nodes
|
|
288
297
|
// so the tree walker sees clean text nodes matching the original structure.
|
|
289
298
|
preview.normalize();
|
|
299
|
+
if (formatting.length === 0)
|
|
300
|
+
return;
|
|
290
301
|
// OverType splits textarea lines into <div> elements, discarding the \n
|
|
291
302
|
// characters. Convert textarea-space positions (with \n) to preview-space
|
|
292
303
|
// positions (without \n) so we can find the correct text nodes.
|
|
@@ -500,10 +511,14 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
500
511
|
const linkSelectionRef = useRef(null);
|
|
501
512
|
const linksRef = useRef([]);
|
|
502
513
|
const formattingRef = useRef([]);
|
|
514
|
+
const formattingUndoRef = useRef([]);
|
|
515
|
+
const formattingRedoRef = useRef([]);
|
|
503
516
|
const caretRef = useRef(null);
|
|
504
517
|
const prevTextRef = useRef("");
|
|
505
518
|
const isSyncingRef = useRef(false);
|
|
506
519
|
const [cursorLink, setCursorLink] = useState(null);
|
|
520
|
+
const [activeFormats, setActiveFormats] = useState(new Set());
|
|
521
|
+
const [linkActive, setLinkActive] = useState(false);
|
|
507
522
|
const Components = useComponentsContext();
|
|
508
523
|
const resolvedPlaceholder = placeholder !== null && placeholder !== void 0 ? placeholder : "";
|
|
509
524
|
useEffect(() => {
|
|
@@ -523,6 +538,8 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
523
538
|
}
|
|
524
539
|
linksRef.current = adjustLinksForEdit(linksRef.current, editPos, delta);
|
|
525
540
|
formattingRef.current = adjustFormattingForEdit(formattingRef.current, editPos, delta);
|
|
541
|
+
formattingUndoRef.current = [];
|
|
542
|
+
formattingRedoRef.current = [];
|
|
526
543
|
prevTextRef.current = nextValue;
|
|
527
544
|
const markdown = buildFullMarkdown(nextValue, linksRef.current, formattingRef.current);
|
|
528
545
|
setPlainTextValue((prev) => {
|
|
@@ -831,17 +848,33 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
831
848
|
};
|
|
832
849
|
}, [enableImageUpload, insertImageMarkdown, onImageFile, textareaNode, uploadImage]);
|
|
833
850
|
const handleToolbarAction = useCallback((action) => {
|
|
834
|
-
var _a, _b, _c;
|
|
851
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
835
852
|
const instance = editorInstanceRef.current;
|
|
836
853
|
if (!textareaNode || !instance) {
|
|
837
854
|
return;
|
|
838
855
|
}
|
|
839
856
|
textareaNode.focus();
|
|
840
857
|
const fmtType = action === "toggleBold" ? "bold" : action === "toggleCode" ? "code" : "italic";
|
|
841
|
-
const
|
|
842
|
-
const
|
|
858
|
+
const rawStart = (_a = textareaNode.selectionStart) !== null && _a !== void 0 ? _a : 0;
|
|
859
|
+
const rawEnd = (_b = textareaNode.selectionEnd) !== null && _b !== void 0 ? _b : 0;
|
|
860
|
+
// Trim leading/trailing whitespace from the selection so that
|
|
861
|
+
// formatting markers wrap only the meaningful content.
|
|
862
|
+
const selectedText = textareaNode.value.slice(rawStart, rawEnd);
|
|
863
|
+
const leadingWs = (_d = (_c = selectedText.match(/^(\s*)/)) === null || _c === void 0 ? void 0 : _c[1].length) !== null && _d !== void 0 ? _d : 0;
|
|
864
|
+
const trailingWs = (_f = (_e = selectedText.match(/(\s*)$/)) === null || _e === void 0 ? void 0 : _e[1].length) !== null && _f !== void 0 ? _f : 0;
|
|
865
|
+
const start = rawStart + leadingWs;
|
|
866
|
+
const end = rawEnd - trailingWs;
|
|
867
|
+
// If selection is all whitespace, nothing to format
|
|
868
|
+
if (start >= end)
|
|
869
|
+
return;
|
|
843
870
|
// Check if selection is already formatted
|
|
844
871
|
const existingIdx = formattingRef.current.findIndex((f) => f.type === fmtType && f.start <= start && f.end >= end);
|
|
872
|
+
// Save current state for undo before modifying
|
|
873
|
+
formattingUndoRef.current = [
|
|
874
|
+
...formattingUndoRef.current,
|
|
875
|
+
{ formatting: [...formattingRef.current], links: [...linksRef.current] },
|
|
876
|
+
];
|
|
877
|
+
formattingRedoRef.current = [];
|
|
845
878
|
if (existingIdx !== -1) {
|
|
846
879
|
// Remove formatting
|
|
847
880
|
formattingRef.current = formattingRef.current.filter((_, i) => i !== existingIdx);
|
|
@@ -852,17 +885,56 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
852
885
|
}
|
|
853
886
|
else {
|
|
854
887
|
// No selection — nothing to format
|
|
888
|
+
formattingUndoRef.current = formattingUndoRef.current.slice(0, -1);
|
|
855
889
|
return;
|
|
856
890
|
}
|
|
857
891
|
const currentValue = instance.getValue();
|
|
858
892
|
prevTextRef.current = currentValue;
|
|
859
893
|
const markdown = buildFullMarkdown(currentValue, linksRef.current, formattingRef.current);
|
|
860
|
-
(
|
|
894
|
+
(_g = onChangeRef.current) === null || _g === void 0 ? void 0 : _g.call(onChangeRef, markdown);
|
|
861
895
|
setPlainTextValue(markdownToPlainText(markdown));
|
|
862
896
|
// Re-apply highlights
|
|
863
897
|
applyFormattingHighlights(instance.preview, formattingRef.current, textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.value);
|
|
864
898
|
applyLinkHighlights(instance.preview, linksRef.current);
|
|
865
899
|
}, [textareaNode]);
|
|
900
|
+
const updateActiveFormats = useCallback(() => {
|
|
901
|
+
var _a, _b;
|
|
902
|
+
if (!textareaNode)
|
|
903
|
+
return;
|
|
904
|
+
const selStart = (_a = textareaNode.selectionStart) !== null && _a !== void 0 ? _a : 0;
|
|
905
|
+
const selEnd = (_b = textareaNode.selectionEnd) !== null && _b !== void 0 ? _b : 0;
|
|
906
|
+
const next = getActiveFormats(formattingRef.current, selStart, selEnd);
|
|
907
|
+
setActiveFormats((prev) => {
|
|
908
|
+
if (prev.size === next.size && [...prev].every((t) => next.has(t)))
|
|
909
|
+
return prev;
|
|
910
|
+
return next;
|
|
911
|
+
});
|
|
912
|
+
const hasLink = selStart !== selEnd && linksRef.current.some((l) => selStart < l.end && selEnd > l.start);
|
|
913
|
+
setLinkActive(hasLink);
|
|
914
|
+
}, [textareaNode]);
|
|
915
|
+
useEffect(() => {
|
|
916
|
+
if (!textareaNode)
|
|
917
|
+
return;
|
|
918
|
+
const onSelectionChange = () => {
|
|
919
|
+
if (document.activeElement === textareaNode) {
|
|
920
|
+
updateActiveFormats();
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
const onBlur = () => {
|
|
924
|
+
setActiveFormats(new Set());
|
|
925
|
+
setLinkActive(false);
|
|
926
|
+
};
|
|
927
|
+
document.addEventListener("selectionchange", onSelectionChange);
|
|
928
|
+
textareaNode.addEventListener("keyup", updateActiveFormats);
|
|
929
|
+
textareaNode.addEventListener("mouseup", updateActiveFormats);
|
|
930
|
+
textareaNode.addEventListener("blur", onBlur);
|
|
931
|
+
return () => {
|
|
932
|
+
document.removeEventListener("selectionchange", onSelectionChange);
|
|
933
|
+
textareaNode.removeEventListener("keyup", updateActiveFormats);
|
|
934
|
+
textareaNode.removeEventListener("mouseup", updateActiveFormats);
|
|
935
|
+
textareaNode.removeEventListener("blur", onBlur);
|
|
936
|
+
};
|
|
937
|
+
}, [textareaNode, updateActiveFormats]);
|
|
866
938
|
const linkPopoverRef = useRef(null);
|
|
867
939
|
// Close link popover on outside click
|
|
868
940
|
useEffect(() => {
|
|
@@ -1044,7 +1116,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
1044
1116
|
const keydownHandlerRef = useRef(null);
|
|
1045
1117
|
useEffect(() => {
|
|
1046
1118
|
keydownHandlerRef.current = (event) => {
|
|
1047
|
-
var _a, _b;
|
|
1119
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1048
1120
|
if (readOnly) {
|
|
1049
1121
|
const openKeys = enableAutocomplete && (event.metaKey || event.ctrlKey) && AUTOCOMPLETE_TRIGGER_KEYS.has(event.code);
|
|
1050
1122
|
if (!READ_ONLY_ALLOWED_KEYS.has(event.key) && !openKeys) {
|
|
@@ -1073,6 +1145,54 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
1073
1145
|
handleToolbarAction("toggleCode");
|
|
1074
1146
|
return;
|
|
1075
1147
|
}
|
|
1148
|
+
if (event.key === "z" || event.key === "Z") {
|
|
1149
|
+
const undoStack = formattingUndoRef.current;
|
|
1150
|
+
if (undoStack.length > 0) {
|
|
1151
|
+
event.preventDefault();
|
|
1152
|
+
event.stopImmediatePropagation();
|
|
1153
|
+
formattingRedoRef.current = [
|
|
1154
|
+
...formattingRedoRef.current,
|
|
1155
|
+
{ formatting: [...formattingRef.current], links: [...linksRef.current] },
|
|
1156
|
+
];
|
|
1157
|
+
const prev = undoStack[undoStack.length - 1];
|
|
1158
|
+
formattingUndoRef.current = undoStack.slice(0, -1);
|
|
1159
|
+
formattingRef.current = prev.formatting;
|
|
1160
|
+
linksRef.current = prev.links;
|
|
1161
|
+
const instance = editorInstanceRef.current;
|
|
1162
|
+
if (instance) {
|
|
1163
|
+
const markdown = buildFullMarkdown(instance.getValue(), linksRef.current, formattingRef.current);
|
|
1164
|
+
(_b = onChangeRef.current) === null || _b === void 0 ? void 0 : _b.call(onChangeRef, markdown);
|
|
1165
|
+
setPlainTextValue(markdownToPlainText(markdown));
|
|
1166
|
+
applyFormattingHighlights(instance.preview, formattingRef.current, (_c = instance.textarea) === null || _c === void 0 ? void 0 : _c.value);
|
|
1167
|
+
applyLinkHighlights(instance.preview, linksRef.current);
|
|
1168
|
+
}
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
if (modKey && event.shiftKey && (event.key === "z" || event.key === "Z")) {
|
|
1174
|
+
const redoStack = formattingRedoRef.current;
|
|
1175
|
+
if (redoStack.length > 0) {
|
|
1176
|
+
event.preventDefault();
|
|
1177
|
+
event.stopImmediatePropagation();
|
|
1178
|
+
formattingUndoRef.current = [
|
|
1179
|
+
...formattingUndoRef.current,
|
|
1180
|
+
{ formatting: [...formattingRef.current], links: [...linksRef.current] },
|
|
1181
|
+
];
|
|
1182
|
+
const next = redoStack[redoStack.length - 1];
|
|
1183
|
+
formattingRedoRef.current = redoStack.slice(0, -1);
|
|
1184
|
+
formattingRef.current = next.formatting;
|
|
1185
|
+
linksRef.current = next.links;
|
|
1186
|
+
const instance = editorInstanceRef.current;
|
|
1187
|
+
if (instance) {
|
|
1188
|
+
const markdown = buildFullMarkdown(instance.getValue(), linksRef.current, formattingRef.current);
|
|
1189
|
+
(_d = onChangeRef.current) === null || _d === void 0 ? void 0 : _d.call(onChangeRef, markdown);
|
|
1190
|
+
setPlainTextValue(markdownToPlainText(markdown));
|
|
1191
|
+
applyFormattingHighlights(instance.preview, formattingRef.current, (_e = instance.textarea) === null || _e === void 0 ? void 0 : _e.value);
|
|
1192
|
+
applyLinkHighlights(instance.preview, linksRef.current);
|
|
1193
|
+
}
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1076
1196
|
}
|
|
1077
1197
|
if (enableAutocomplete && shouldShowAutocomplete) {
|
|
1078
1198
|
if (event.key === "ArrowDown") {
|
|
@@ -1087,7 +1207,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
1087
1207
|
}
|
|
1088
1208
|
if (event.key === "Enter" || event.key === "Tab") {
|
|
1089
1209
|
event.preventDefault();
|
|
1090
|
-
const suggestion = (
|
|
1210
|
+
const suggestion = (_f = filteredSuggestions[activeSuggestionIndex]) !== null && _f !== void 0 ? _f : filteredSuggestions[0];
|
|
1091
1211
|
if (suggestion) {
|
|
1092
1212
|
applySuggestion(suggestion);
|
|
1093
1213
|
}
|
|
@@ -1162,20 +1282,20 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
1162
1282
|
}, tabIndex: -1, children: "Edit link" }), _jsx("a", { className: "bn-step-link-tooltip__btn", href: cursorLink.url, target: "_blank", rel: "noopener noreferrer", onMouseDown: (event) => event.stopPropagation(), tabIndex: -1, children: _jsxs("svg", { width: "12", height: "12", viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": "true", children: [_jsx("path", { d: "M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z" }), _jsx("path", { d: "M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z" })] }) }), _jsx("button", { type: "button", className: "bn-step-link-tooltip__btn bn-step-link-tooltip__btn--danger", onMouseDown: (event) => {
|
|
1163
1283
|
event.preventDefault();
|
|
1164
1284
|
handleRemoveLink();
|
|
1165
|
-
}, tabIndex: -1, children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { d: "M7 3h2a1 1 0 0 0-2 0ZM6 3a2 2 0 1 1 4 0h4a.5.5 0 0 1 0 1h-.564l-1.205 8.838A2.5 2.5 0 0 1 9.754 15H6.246a2.5 2.5 0 0 1-2.477-2.162L2.564 4H2a.5.5 0 0 1 0-1h4Zm1 3.5a.5.5 0 0 0-1 0v5a.5.5 0 0 0 1 0v-5ZM9.5 6a.5.5 0 0 0-.5.5v5a.5.5 0 0 0 1 0v-5a.5.5 0 0 0-.5-.5Z" }) }) })] })), showToolbar && (_jsxs("div", { className: "bn-step-toolbar", "aria-label": `${label} controls`, children: [showFormattingButtons && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "bn-step-toolbar__button"
|
|
1285
|
+
}, tabIndex: -1, children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { d: "M7 3h2a1 1 0 0 0-2 0ZM6 3a2 2 0 1 1 4 0h4a.5.5 0 0 1 0 1h-.564l-1.205 8.838A2.5 2.5 0 0 1 9.754 15H6.246a2.5 2.5 0 0 1-2.477-2.162L2.564 4H2a.5.5 0 0 1 0-1h4Zm1 3.5a.5.5 0 0 0-1 0v5a.5.5 0 0 0 1 0v-5ZM9.5 6a.5.5 0 0 0-.5.5v5a.5.5 0 0 0 1 0v-5a.5.5 0 0 0-.5-.5Z" }) }) })] })), showToolbar && (_jsxs("div", { className: "bn-step-toolbar", "aria-label": `${label} controls`, children: [showFormattingButtons && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: `bn-step-toolbar__button${activeFormats.has("bold") ? " bn-step-toolbar__button--active" : ""}`, "data-tooltip": "Bold", onMouseDown: (event) => {
|
|
1166
1286
|
event.preventDefault();
|
|
1167
1287
|
handleToolbarAction("toggleBold");
|
|
1168
|
-
}, "aria-label": "Bold", tabIndex: -1, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M4 2.66675H8.33333C8.92064 2.66677 9.49502 2.83918 9.98525 3.1626C10.4755 3.48602 10.86 3.94622 11.0911 4.48613C11.3223 5.02604 11.3898 5.62192 11.2855 6.19988C11.1811 6.77783 10.9094 7.31244 10.504 7.73741C11.0752 8.06825 11.5213 8.57823 11.7733 9.18833C12.0252 9.79844 12.0689 10.4746 11.8976 11.1121C11.7263 11.7495 11.3495 12.3127 10.8256 12.7143C10.3018 13.1159 9.66008 13.3335 9 13.3334H4V12.0001H4.66667V4.00008H4V2.66675ZM6 7.33341H8.33333C8.77536 7.33341 9.19928 7.15782 9.51184 6.84526C9.8244 6.5327 10 6.10878 10 5.66675C10 5.22472 9.8244 4.8008 9.51184 4.48824C9.19928 4.17568 8.77536 4.00008 8.33333 4.00008H6V7.33341ZM6 8.66675V12.0001H9C9.44203 12.0001 9.86595 11.8245 10.1785 11.5119C10.4911 11.1994 10.6667 10.7754 10.6667 10.3334C10.6667 9.89139 10.4911 9.46746 10.1785 9.1549C9.86595 8.84234 9.44203 8.66675 9 8.66675H6Z", fill: "currentColor" }) }) }), _jsx("button", { type: "button", className: "bn-step-toolbar__button"
|
|
1288
|
+
}, "aria-label": "Bold", tabIndex: -1, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M4 2.66675H8.33333C8.92064 2.66677 9.49502 2.83918 9.98525 3.1626C10.4755 3.48602 10.86 3.94622 11.0911 4.48613C11.3223 5.02604 11.3898 5.62192 11.2855 6.19988C11.1811 6.77783 10.9094 7.31244 10.504 7.73741C11.0752 8.06825 11.5213 8.57823 11.7733 9.18833C12.0252 9.79844 12.0689 10.4746 11.8976 11.1121C11.7263 11.7495 11.3495 12.3127 10.8256 12.7143C10.3018 13.1159 9.66008 13.3335 9 13.3334H4V12.0001H4.66667V4.00008H4V2.66675ZM6 7.33341H8.33333C8.77536 7.33341 9.19928 7.15782 9.51184 6.84526C9.8244 6.5327 10 6.10878 10 5.66675C10 5.22472 9.8244 4.8008 9.51184 4.48824C9.19928 4.17568 8.77536 4.00008 8.33333 4.00008H6V7.33341ZM6 8.66675V12.0001H9C9.44203 12.0001 9.86595 11.8245 10.1785 11.5119C10.4911 11.1994 10.6667 10.7754 10.6667 10.3334C10.6667 9.89139 10.4911 9.46746 10.1785 9.1549C9.86595 8.84234 9.44203 8.66675 9 8.66675H6Z", fill: "currentColor" }) }) }), _jsx("button", { type: "button", className: `bn-step-toolbar__button${activeFormats.has("italic") ? " bn-step-toolbar__button--active" : ""}`, "data-tooltip": "Italic", onMouseDown: (event) => {
|
|
1169
1289
|
event.preventDefault();
|
|
1170
1290
|
handleToolbarAction("toggleItalic");
|
|
1171
|
-
}, "aria-label": "Italic", tabIndex: -1, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M8.66699 13.3334H4.66699V12.0001H5.95166L8.69566 4.00008H7.33366V2.66675H11.3337V4.00008H10.049L7.30499 12.0001H8.66699V13.3334Z", fill: "currentColor" }) }) }), _jsx("button", { type: "button", className: "bn-step-toolbar__button"
|
|
1291
|
+
}, "aria-label": "Italic", tabIndex: -1, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M8.66699 13.3334H4.66699V12.0001H5.95166L8.69566 4.00008H7.33366V2.66675H11.3337V4.00008H10.049L7.30499 12.0001H8.66699V13.3334Z", fill: "currentColor" }) }) }), _jsx("button", { type: "button", className: `bn-step-toolbar__button${activeFormats.has("code") ? " bn-step-toolbar__button--active" : ""}`, "data-tooltip": "Code", onMouseDown: (event) => {
|
|
1172
1292
|
event.preventDefault();
|
|
1173
1293
|
handleToolbarAction("toggleCode");
|
|
1174
1294
|
}, "aria-label": "Code", tabIndex: -1, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M10.333 12.6667L14 8.00008L10.333 3.33341L9.15833 4.28341L12.1583 8.00008L9.15833 11.7167L10.333 12.6667ZM5.66699 12.6667L6.84166 11.7167L3.84166 8.00008L6.84166 4.28341L5.66699 3.33341L2 8.00008L5.66699 12.6667Z", fill: "currentColor" }) }) })] })), enableImageUpload && uploadImage && showImageButton && (_jsx("button", { type: "button", className: "bn-step-toolbar__button", "data-tooltip": "Insert image", onMouseDown: (event) => {
|
|
1175
1295
|
var _a;
|
|
1176
1296
|
event.preventDefault();
|
|
1177
1297
|
(_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click();
|
|
1178
|
-
}, "aria-label": "Insert image", tabIndex: -1, disabled: isUploading, children: _jsx(ImageUploadIcon, {}) })), showFormattingButtons && Components && (_jsxs(Components.Generic.Popover.Root, { opened: showLinkPopover, position: "top", children: [_jsx(Components.Generic.Popover.Trigger, { children: _jsx("button", { type: "button", className: "bn-step-toolbar__button"
|
|
1298
|
+
}, "aria-label": "Insert image", tabIndex: -1, disabled: isUploading, children: _jsx(ImageUploadIcon, {}) })), showFormattingButtons && Components && (_jsxs(Components.Generic.Popover.Root, { opened: showLinkPopover, position: "top", children: [_jsx(Components.Generic.Popover.Trigger, { children: _jsx("button", { type: "button", className: `bn-step-toolbar__button${linkActive ? " bn-step-toolbar__button--active" : ""}`, "data-tooltip": "Insert link", onMouseDown: (event) => {
|
|
1179
1299
|
event.preventDefault();
|
|
1180
1300
|
if (showLinkPopover) {
|
|
1181
1301
|
setShowLinkPopover(false);
|
package/package/styles.css
CHANGED
|
@@ -942,6 +942,11 @@ html.dark .bn-step-image-preview__content {
|
|
|
942
942
|
background: var(--step-bg-button-hover);
|
|
943
943
|
}
|
|
944
944
|
|
|
945
|
+
.bn-step-toolbar__button--active {
|
|
946
|
+
background: var(--step-bg-button-hover);
|
|
947
|
+
color: var(--step-input-border-focus);
|
|
948
|
+
}
|
|
949
|
+
|
|
945
950
|
[data-tooltip] {
|
|
946
951
|
position: relative;
|
|
947
952
|
}
|
|
@@ -1074,8 +1079,7 @@ html.dark .bn-step-image-preview__content {
|
|
|
1074
1079
|
}
|
|
1075
1080
|
|
|
1076
1081
|
.bn-step-editor .overtype-wrapper .overtype-preview strong.step-preview-bold {
|
|
1077
|
-
-
|
|
1078
|
-
font-weight: inherit !important;
|
|
1082
|
+
font-weight: 600 !important;
|
|
1079
1083
|
color: inherit !important;
|
|
1080
1084
|
}
|
|
1081
1085
|
|
|
@@ -1085,9 +1089,10 @@ html.dark .bn-step-image-preview__content {
|
|
|
1085
1089
|
}
|
|
1086
1090
|
|
|
1087
1091
|
.bn-step-editor .overtype-wrapper .overtype-preview code.step-preview-code {
|
|
1088
|
-
background-color:
|
|
1092
|
+
background-color: transparent !important;
|
|
1089
1093
|
border-radius: 3px !important;
|
|
1090
|
-
|
|
1094
|
+
font-family: "Fira Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;
|
|
1095
|
+
color: rgb(146, 64, 14) !important;
|
|
1091
1096
|
}
|
|
1092
1097
|
|
|
1093
1098
|
.bn-step-custom-caret {
|
package/package.json
CHANGED
|
@@ -81,6 +81,22 @@ type ExtractedImage = {
|
|
|
81
81
|
|
|
82
82
|
type LinkMeta = { start: number; end: number; url: string };
|
|
83
83
|
type FormattingMeta = { start: number; end: number; type: "bold" | "italic" | "code" };
|
|
84
|
+
type FormatType = "bold" | "italic" | "code";
|
|
85
|
+
|
|
86
|
+
function getActiveFormats(
|
|
87
|
+
formatting: FormattingMeta[],
|
|
88
|
+
selStart: number,
|
|
89
|
+
selEnd: number,
|
|
90
|
+
): Set<FormatType> {
|
|
91
|
+
const active = new Set<FormatType>();
|
|
92
|
+
if (selStart === selEnd) return active;
|
|
93
|
+
for (const f of formatting) {
|
|
94
|
+
if (selStart < f.end && selEnd > f.start) {
|
|
95
|
+
active.add(f.type);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return active;
|
|
99
|
+
}
|
|
84
100
|
|
|
85
101
|
|
|
86
102
|
function stripInlineMarkdown(markdown: string): {
|
|
@@ -345,8 +361,6 @@ function getCaretRectInPreview(preview: HTMLElement, offset: number, textareaVal
|
|
|
345
361
|
}
|
|
346
362
|
|
|
347
363
|
function applyFormattingHighlights(preview: HTMLElement, formatting: FormattingMeta[], textareaValue?: string) {
|
|
348
|
-
if (formatting.length === 0) return;
|
|
349
|
-
|
|
350
364
|
// Remove previous formatting highlights
|
|
351
365
|
const existingBold = preview.querySelectorAll("strong.step-preview-bold");
|
|
352
366
|
for (let i = 0; i < existingBold.length; i++) {
|
|
@@ -386,6 +400,8 @@ function applyFormattingHighlights(preview: HTMLElement, formatting: FormattingM
|
|
|
386
400
|
// so the tree walker sees clean text nodes matching the original structure.
|
|
387
401
|
preview.normalize();
|
|
388
402
|
|
|
403
|
+
if (formatting.length === 0) return;
|
|
404
|
+
|
|
389
405
|
// OverType splits textarea lines into <div> elements, discarding the \n
|
|
390
406
|
// characters. Convert textarea-space positions (with \n) to preview-space
|
|
391
407
|
// positions (without \n) so we can find the correct text nodes.
|
|
@@ -632,10 +648,14 @@ export function StepField({
|
|
|
632
648
|
const linkSelectionRef = useRef<{ start: number; end: number; text: string } | null>(null);
|
|
633
649
|
const linksRef = useRef<LinkMeta[]>([]);
|
|
634
650
|
const formattingRef = useRef<FormattingMeta[]>([]);
|
|
651
|
+
const formattingUndoRef = useRef<Array<{ formatting: FormattingMeta[]; links: LinkMeta[] }>>([]);
|
|
652
|
+
const formattingRedoRef = useRef<Array<{ formatting: FormattingMeta[]; links: LinkMeta[] }>>([]);
|
|
635
653
|
const caretRef = useRef<HTMLDivElement | null>(null);
|
|
636
654
|
const prevTextRef = useRef("");
|
|
637
655
|
const isSyncingRef = useRef(false);
|
|
638
656
|
const [cursorLink, setCursorLink] = useState<LinkMeta | null>(null);
|
|
657
|
+
const [activeFormats, setActiveFormats] = useState<Set<FormatType>>(new Set());
|
|
658
|
+
const [linkActive, setLinkActive] = useState(false);
|
|
639
659
|
const Components = useComponentsContext();
|
|
640
660
|
const resolvedPlaceholder = placeholder ?? "";
|
|
641
661
|
|
|
@@ -658,6 +678,8 @@ export function StepField({
|
|
|
658
678
|
|
|
659
679
|
linksRef.current = adjustLinksForEdit(linksRef.current, editPos, delta);
|
|
660
680
|
formattingRef.current = adjustFormattingForEdit(formattingRef.current, editPos, delta);
|
|
681
|
+
formattingUndoRef.current = [];
|
|
682
|
+
formattingRedoRef.current = [];
|
|
661
683
|
prevTextRef.current = nextValue;
|
|
662
684
|
|
|
663
685
|
const markdown = buildFullMarkdown(nextValue, linksRef.current, formattingRef.current);
|
|
@@ -1024,14 +1046,32 @@ export function StepField({
|
|
|
1024
1046
|
textareaNode.focus();
|
|
1025
1047
|
|
|
1026
1048
|
const fmtType: "bold" | "italic" | "code" = action === "toggleBold" ? "bold" : action === "toggleCode" ? "code" : "italic";
|
|
1027
|
-
const
|
|
1028
|
-
const
|
|
1049
|
+
const rawStart = textareaNode.selectionStart ?? 0;
|
|
1050
|
+
const rawEnd = textareaNode.selectionEnd ?? 0;
|
|
1051
|
+
|
|
1052
|
+
// Trim leading/trailing whitespace from the selection so that
|
|
1053
|
+
// formatting markers wrap only the meaningful content.
|
|
1054
|
+
const selectedText = textareaNode.value.slice(rawStart, rawEnd);
|
|
1055
|
+
const leadingWs = selectedText.match(/^(\s*)/)?.[1].length ?? 0;
|
|
1056
|
+
const trailingWs = selectedText.match(/(\s*)$/)?.[1].length ?? 0;
|
|
1057
|
+
const start = rawStart + leadingWs;
|
|
1058
|
+
const end = rawEnd - trailingWs;
|
|
1059
|
+
|
|
1060
|
+
// If selection is all whitespace, nothing to format
|
|
1061
|
+
if (start >= end) return;
|
|
1029
1062
|
|
|
1030
1063
|
// Check if selection is already formatted
|
|
1031
1064
|
const existingIdx = formattingRef.current.findIndex(
|
|
1032
1065
|
(f) => f.type === fmtType && f.start <= start && f.end >= end,
|
|
1033
1066
|
);
|
|
1034
1067
|
|
|
1068
|
+
// Save current state for undo before modifying
|
|
1069
|
+
formattingUndoRef.current = [
|
|
1070
|
+
...formattingUndoRef.current,
|
|
1071
|
+
{ formatting: [...formattingRef.current], links: [...linksRef.current] },
|
|
1072
|
+
];
|
|
1073
|
+
formattingRedoRef.current = [];
|
|
1074
|
+
|
|
1035
1075
|
if (existingIdx !== -1) {
|
|
1036
1076
|
// Remove formatting
|
|
1037
1077
|
formattingRef.current = formattingRef.current.filter((_, i) => i !== existingIdx);
|
|
@@ -1040,6 +1080,7 @@ export function StepField({
|
|
|
1040
1080
|
formattingRef.current = [...formattingRef.current, { start, end, type: fmtType }];
|
|
1041
1081
|
} else {
|
|
1042
1082
|
// No selection — nothing to format
|
|
1083
|
+
formattingUndoRef.current = formattingUndoRef.current.slice(0, -1);
|
|
1043
1084
|
return;
|
|
1044
1085
|
}
|
|
1045
1086
|
|
|
@@ -1057,6 +1098,46 @@ export function StepField({
|
|
|
1057
1098
|
[textareaNode],
|
|
1058
1099
|
);
|
|
1059
1100
|
|
|
1101
|
+
const updateActiveFormats = useCallback(() => {
|
|
1102
|
+
if (!textareaNode) return;
|
|
1103
|
+
const selStart = textareaNode.selectionStart ?? 0;
|
|
1104
|
+
const selEnd = textareaNode.selectionEnd ?? 0;
|
|
1105
|
+
const next = getActiveFormats(formattingRef.current, selStart, selEnd);
|
|
1106
|
+
setActiveFormats((prev) => {
|
|
1107
|
+
if (prev.size === next.size && [...prev].every((t) => next.has(t))) return prev;
|
|
1108
|
+
return next;
|
|
1109
|
+
});
|
|
1110
|
+
const hasLink = selStart !== selEnd && linksRef.current.some((l) => selStart < l.end && selEnd > l.start);
|
|
1111
|
+
setLinkActive(hasLink);
|
|
1112
|
+
}, [textareaNode]);
|
|
1113
|
+
|
|
1114
|
+
useEffect(() => {
|
|
1115
|
+
if (!textareaNode) return;
|
|
1116
|
+
|
|
1117
|
+
const onSelectionChange = () => {
|
|
1118
|
+
if (document.activeElement === textareaNode) {
|
|
1119
|
+
updateActiveFormats();
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
const onBlur = () => {
|
|
1124
|
+
setActiveFormats(new Set());
|
|
1125
|
+
setLinkActive(false);
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
document.addEventListener("selectionchange", onSelectionChange);
|
|
1129
|
+
textareaNode.addEventListener("keyup", updateActiveFormats);
|
|
1130
|
+
textareaNode.addEventListener("mouseup", updateActiveFormats);
|
|
1131
|
+
textareaNode.addEventListener("blur", onBlur);
|
|
1132
|
+
|
|
1133
|
+
return () => {
|
|
1134
|
+
document.removeEventListener("selectionchange", onSelectionChange);
|
|
1135
|
+
textareaNode.removeEventListener("keyup", updateActiveFormats);
|
|
1136
|
+
textareaNode.removeEventListener("mouseup", updateActiveFormats);
|
|
1137
|
+
textareaNode.removeEventListener("blur", onBlur);
|
|
1138
|
+
};
|
|
1139
|
+
}, [textareaNode, updateActiveFormats]);
|
|
1140
|
+
|
|
1060
1141
|
const linkPopoverRef = useRef<HTMLDivElement>(null);
|
|
1061
1142
|
|
|
1062
1143
|
// Close link popover on outside click
|
|
@@ -1310,6 +1391,54 @@ export function StepField({
|
|
|
1310
1391
|
handleToolbarAction("toggleCode");
|
|
1311
1392
|
return;
|
|
1312
1393
|
}
|
|
1394
|
+
if (event.key === "z" || event.key === "Z") {
|
|
1395
|
+
const undoStack = formattingUndoRef.current;
|
|
1396
|
+
if (undoStack.length > 0) {
|
|
1397
|
+
event.preventDefault();
|
|
1398
|
+
event.stopImmediatePropagation();
|
|
1399
|
+
formattingRedoRef.current = [
|
|
1400
|
+
...formattingRedoRef.current,
|
|
1401
|
+
{ formatting: [...formattingRef.current], links: [...linksRef.current] },
|
|
1402
|
+
];
|
|
1403
|
+
const prev = undoStack[undoStack.length - 1];
|
|
1404
|
+
formattingUndoRef.current = undoStack.slice(0, -1);
|
|
1405
|
+
formattingRef.current = prev.formatting;
|
|
1406
|
+
linksRef.current = prev.links;
|
|
1407
|
+
const instance = editorInstanceRef.current;
|
|
1408
|
+
if (instance) {
|
|
1409
|
+
const markdown = buildFullMarkdown(instance.getValue(), linksRef.current, formattingRef.current);
|
|
1410
|
+
onChangeRef.current?.(markdown);
|
|
1411
|
+
setPlainTextValue(markdownToPlainText(markdown));
|
|
1412
|
+
applyFormattingHighlights(instance.preview, formattingRef.current, instance.textarea?.value);
|
|
1413
|
+
applyLinkHighlights(instance.preview, linksRef.current);
|
|
1414
|
+
}
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
if (modKey && event.shiftKey && (event.key === "z" || event.key === "Z")) {
|
|
1420
|
+
const redoStack = formattingRedoRef.current;
|
|
1421
|
+
if (redoStack.length > 0) {
|
|
1422
|
+
event.preventDefault();
|
|
1423
|
+
event.stopImmediatePropagation();
|
|
1424
|
+
formattingUndoRef.current = [
|
|
1425
|
+
...formattingUndoRef.current,
|
|
1426
|
+
{ formatting: [...formattingRef.current], links: [...linksRef.current] },
|
|
1427
|
+
];
|
|
1428
|
+
const next = redoStack[redoStack.length - 1];
|
|
1429
|
+
formattingRedoRef.current = redoStack.slice(0, -1);
|
|
1430
|
+
formattingRef.current = next.formatting;
|
|
1431
|
+
linksRef.current = next.links;
|
|
1432
|
+
const instance = editorInstanceRef.current;
|
|
1433
|
+
if (instance) {
|
|
1434
|
+
const markdown = buildFullMarkdown(instance.getValue(), linksRef.current, formattingRef.current);
|
|
1435
|
+
onChangeRef.current?.(markdown);
|
|
1436
|
+
setPlainTextValue(markdownToPlainText(markdown));
|
|
1437
|
+
applyFormattingHighlights(instance.preview, formattingRef.current, instance.textarea?.value);
|
|
1438
|
+
applyLinkHighlights(instance.preview, linksRef.current);
|
|
1439
|
+
}
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1313
1442
|
}
|
|
1314
1443
|
|
|
1315
1444
|
if (enableAutocomplete && shouldShowAutocomplete) {
|
|
@@ -1489,7 +1618,7 @@ export function StepField({
|
|
|
1489
1618
|
<>
|
|
1490
1619
|
<button
|
|
1491
1620
|
type="button"
|
|
1492
|
-
className="bn-step-toolbar__button"
|
|
1621
|
+
className={`bn-step-toolbar__button${activeFormats.has("bold") ? " bn-step-toolbar__button--active" : ""}`}
|
|
1493
1622
|
data-tooltip="Bold"
|
|
1494
1623
|
onMouseDown={(event) => {
|
|
1495
1624
|
event.preventDefault();
|
|
@@ -1504,7 +1633,7 @@ export function StepField({
|
|
|
1504
1633
|
</button>
|
|
1505
1634
|
<button
|
|
1506
1635
|
type="button"
|
|
1507
|
-
className="bn-step-toolbar__button"
|
|
1636
|
+
className={`bn-step-toolbar__button${activeFormats.has("italic") ? " bn-step-toolbar__button--active" : ""}`}
|
|
1508
1637
|
data-tooltip="Italic"
|
|
1509
1638
|
onMouseDown={(event) => {
|
|
1510
1639
|
event.preventDefault();
|
|
@@ -1519,7 +1648,7 @@ export function StepField({
|
|
|
1519
1648
|
</button>
|
|
1520
1649
|
<button
|
|
1521
1650
|
type="button"
|
|
1522
|
-
className="bn-step-toolbar__button"
|
|
1651
|
+
className={`bn-step-toolbar__button${activeFormats.has("code") ? " bn-step-toolbar__button--active" : ""}`}
|
|
1523
1652
|
data-tooltip="Code"
|
|
1524
1653
|
onMouseDown={(event) => {
|
|
1525
1654
|
event.preventDefault();
|
|
@@ -1558,7 +1687,7 @@ export function StepField({
|
|
|
1558
1687
|
<Components.Generic.Popover.Trigger>
|
|
1559
1688
|
<button
|
|
1560
1689
|
type="button"
|
|
1561
|
-
className="bn-step-toolbar__button"
|
|
1690
|
+
className={`bn-step-toolbar__button${linkActive ? " bn-step-toolbar__button--active" : ""}`}
|
|
1562
1691
|
data-tooltip="Insert link"
|
|
1563
1692
|
onMouseDown={(event) => {
|
|
1564
1693
|
event.preventDefault();
|
package/src/editor/styles.css
CHANGED
|
@@ -942,6 +942,11 @@ html.dark .bn-step-image-preview__content {
|
|
|
942
942
|
background: var(--step-bg-button-hover);
|
|
943
943
|
}
|
|
944
944
|
|
|
945
|
+
.bn-step-toolbar__button--active {
|
|
946
|
+
background: var(--step-bg-button-hover);
|
|
947
|
+
color: var(--step-input-border-focus);
|
|
948
|
+
}
|
|
949
|
+
|
|
945
950
|
[data-tooltip] {
|
|
946
951
|
position: relative;
|
|
947
952
|
}
|
|
@@ -1074,8 +1079,7 @@ html.dark .bn-step-image-preview__content {
|
|
|
1074
1079
|
}
|
|
1075
1080
|
|
|
1076
1081
|
.bn-step-editor .overtype-wrapper .overtype-preview strong.step-preview-bold {
|
|
1077
|
-
-
|
|
1078
|
-
font-weight: inherit !important;
|
|
1082
|
+
font-weight: 600 !important;
|
|
1079
1083
|
color: inherit !important;
|
|
1080
1084
|
}
|
|
1081
1085
|
|
|
@@ -1085,9 +1089,10 @@ html.dark .bn-step-image-preview__content {
|
|
|
1085
1089
|
}
|
|
1086
1090
|
|
|
1087
1091
|
.bn-step-editor .overtype-wrapper .overtype-preview code.step-preview-code {
|
|
1088
|
-
background-color:
|
|
1092
|
+
background-color: transparent !important;
|
|
1089
1093
|
border-radius: 3px !important;
|
|
1090
|
-
|
|
1094
|
+
font-family: "Fira Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important;
|
|
1095
|
+
color: rgb(146, 64, 14) !important;
|
|
1091
1096
|
}
|
|
1092
1097
|
|
|
1093
1098
|
.bn-step-custom-caret {
|