Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 21 additions & 19 deletions packages/plugin-toolbar/src/formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,8 @@ function toggleLinePrefix(editor: EditorAPI, prefix: string): boolean {
const line = doc.slice(lineStart, lineEnd);

const newLine = line.startsWith(prefix) ? line.slice(prefix.length) : prefix + line;
const newDoc = doc.slice(0, lineStart) + newLine + doc.slice(lineEnd);
editor.setDocument(newDoc);
editor.setSelection(lineStart + newLine.length);
const selection = { anchor: lineStart + newLine.length };
editor.replaceRange(lineStart, lineEnd, newLine, selection);
return true;
}

Expand Down Expand Up @@ -135,12 +134,10 @@ export function insertCodeBlock(editor: EditorAPI): boolean {
"```\n" + (selected || "") + "\n```" +
(needsTrailingNewline ? "\n" : "");

const newDoc = doc.slice(0, from) + block + doc.slice(to);
editor.setDocument(newDoc);

// Place cursor on the language line (after ```)
const langPos = from + (needsLeadingNewline ? 1 : 0) + 3;
editor.setSelection(langPos);
const selection = { anchor: langPos };
editor.replaceRange(from, to, block, selection);
return true;
}

Expand All @@ -153,12 +150,14 @@ export function insertImage(editor: EditorAPI): boolean {

const alt = selected || "alt text";
const md = `![${alt}](url)`;
const newDoc = doc.slice(0, from) + md + doc.slice(to);
editor.setDocument(newDoc);

// Select the "url" part
const urlStart = from + alt.length + 4;
editor.setSelection(urlStart, urlStart + 3);
const selection = {
anchor: urlStart,
head: urlStart + 3,
};
editor.replaceRange(from, to, md, selection);
return true;
}

Expand All @@ -172,10 +171,12 @@ export function applyTextColor(editor: EditorAPI, color: string): boolean {
if (from === to) return false;

const wrapped = `<span style="color:${color}">${selected}</span>`;
const newDoc = doc.slice(0, from) + wrapped + doc.slice(to);
editor.setDocument(newDoc);
const innerStart = from + `<span style="color:${color}">`.length;
editor.setSelection(innerStart, innerStart + selected.length);
const selection = {
anchor: innerStart,
head: innerStart + selected.length,
};
editor.replaceRange(from, to, wrapped, selection);
return true;
}

Expand All @@ -189,10 +190,12 @@ export function applyHighlight(editor: EditorAPI, color: string): boolean {
if (from === to) return false;

const wrapped = `<mark style="background:${color}">${selected}</mark>`;
const newDoc = doc.slice(0, from) + wrapped + doc.slice(to);
editor.setDocument(newDoc);
const innerStart = from + `<mark style="background:${color}">`.length;
editor.setSelection(innerStart, innerStart + selected.length);
const selection = {
anchor: innerStart,
head: innerStart + selected.length,
};
editor.replaceRange(from, to, wrapped, selection);
return true;
}

Expand All @@ -203,8 +206,7 @@ export function insertHorizontalRule(editor: EditorAPI): boolean {
const needsLeadingNewline = anchor > 0 && doc[anchor - 1] !== "\n";
const hr = (needsLeadingNewline ? "\n" : "") + "---\n";

const newDoc = doc.slice(0, anchor) + hr + doc.slice(anchor);
editor.setDocument(newDoc);
editor.setSelection(anchor + hr.length);
const selection = { anchor: anchor + hr.length };
editor.replaceRange(anchor, anchor, hr, selection);
return true;
}
36 changes: 20 additions & 16 deletions packages/plugin-toolbar/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,23 @@ export function toggleWrap(editor: EditorAPI, marker: string): boolean {

if (before === marker && after === marker) {
// Already wrapped — remove markers
const newDoc =
doc.slice(0, from - marker.length) +
selected +
doc.slice(to + marker.length);
editor.setDocument(newDoc);
editor.setSelection(from - marker.length, to - marker.length);
const replaceFrom = from - marker.length;
const replaceTo = to + marker.length;
const selection = {
anchor: replaceFrom,
head: replaceFrom + selected.length,
};
editor.replaceRange(replaceFrom, replaceTo, selected, selection);
return true;
}

// Wrap selection with markers
const newDoc =
doc.slice(0, from) + marker + selected + marker + doc.slice(to);
editor.setDocument(newDoc);
editor.setSelection(from + marker.length, to + marker.length);
const newContent = marker + selected + marker;
const selection = {
anchor: from + marker.length,
head: from + marker.length + selected.length,
};
editor.replaceRange(from, to, newContent, selection);
return true;
}

Expand Down Expand Up @@ -68,12 +71,14 @@ export function insertLink(editor: EditorAPI): boolean {

const linkText = selected || "link text";
const md = `[${linkText}](url)`;
const newDoc = doc.slice(0, from) + md + doc.slice(to);
editor.setDocument(newDoc);

// Select the "url" part for easy replacement
const urlStart = from + linkText.length + 3;
editor.setSelection(urlStart, urlStart + 3);
const selection = {
anchor: urlStart,
head: urlStart + 3,
};
editor.replaceRange(from, to, md, selection);
return true;
}

Expand All @@ -100,9 +105,8 @@ export function toggleHeading(editor: EditorAPI, level: number): boolean {
newLine = prefix + line;
}

const newDoc = doc.slice(0, lineStart) + newLine + doc.slice(end);
editor.setDocument(newDoc);
editor.setSelection(lineStart + newLine.length);
const selection = { anchor: lineStart + newLine.length };
editor.replaceRange(lineStart, end, newLine, selection);
return true;
}

Expand Down
63 changes: 63 additions & 0 deletions packages/plugin-toolbar/test/plugin-toolbar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,3 +606,66 @@ describe("toggleUnorderedList — atomic undo", () => {
editor.destroy();
});
});

describe("toggleBold — atomic undo", () => {
it("bold toggle → single undo fully restores document", () => {
const container = document.createElement("div");
const original = "hello world";
const editor = createEditor({ container, initialValue: original, plugins: [createHistoryPlugin()] });

editor.setSelection(6, 11);
toggleBold(editor);
expect(editor.getDocument()).toBe("hello **world**");

expect(editor.undo()).toBe(true);
expect(editor.getDocument()).toBe(original);
expect(editor.undo()).toBe(false);
editor.destroy();
});

it("bold toggle → undo → redo recovers bold state in one step", () => {
const container = document.createElement("div");
const editor = createEditor({ container, initialValue: "hello world", plugins: [createHistoryPlugin()] });

editor.setSelection(6, 11);
toggleBold(editor);
editor.undo();
expect(editor.redo()).toBe(true);
expect(editor.getDocument()).toBe("hello **world**");
editor.destroy();
});
});

describe("insertLink — atomic undo", () => {
it("insert link → single undo fully restores document", () => {
const container = document.createElement("div");
const original = "click here";
const editor = createEditor({ container, initialValue: original, plugins: [createHistoryPlugin()] });

editor.setSelection(6, 10);
insertLink(editor);
expect(editor.getDocument()).toBe("click [here](url)");

expect(editor.undo()).toBe(true);
expect(editor.getDocument()).toBe(original);
expect(editor.undo()).toBe(false);
editor.destroy();
});
});

describe("toggleHeading — atomic undo", () => {
it("heading toggle → single undo fully restores document", () => {
const container = document.createElement("div");
const original = "Title";
const editor = createEditor({ container, initialValue: original, plugins: [createHistoryPlugin()] });

editor.setSelection(2);
toggleHeading(editor, 2);
expect(editor.getDocument()).toBe("## Title");

expect(editor.undo()).toBe(true);
expect(editor.getDocument()).toBe(original);
expect(editor.undo()).toBe(false);
editor.destroy();
});
});