Add strikethrough shortcut, fenced code toggle, and CodeMirror-aware undo/redo
- Bind Ctrl+Shift+S to strikethrough - Smart code toggle: fenced block for multi-line selections, inline otherwise - Link editing: selecting an existing link now selects its URL for editing - Route undo/redo shortcuts through IPC so CodeMirror history is used when the editor has focus instead of Electron's native menu-role undo/redo - Add Indent/Outdent toolbar buttons
This commit is contained in:
parent
c09f4adc30
commit
45990bbd62
9 changed files with 185 additions and 12 deletions
|
|
@ -58,6 +58,7 @@ function createWindow() {
|
|||
preload: path.join(__dirname, "preload.js"),
|
||||
},
|
||||
});
|
||||
forwardEditShortcuts(mainWindow);
|
||||
mainWindow.loadFile(path.join(__dirname, "index.html"));
|
||||
mainWindow.webContents.on("did-finish-load", () => {
|
||||
console.log("[folio] renderer loaded");
|
||||
|
|
@ -114,6 +115,25 @@ function notifyChange() {
|
|||
getWindow()?.webContents.send("folio:chapters-changed");
|
||||
}
|
||||
|
||||
// Route undo/redo shortcuts to the renderer so CodeMirror's own history is
|
||||
// used when the editor has focus, instead of Electron's native (menu-role)
|
||||
// undo/redo which does not understand the editor's history.
|
||||
function forwardEditShortcuts(win: BrowserWindow) {
|
||||
win.webContents.on("before-input-event", (event, input) => {
|
||||
if (input.type !== "keyDown" || !input.key) return;
|
||||
const mod = input.control || input.meta;
|
||||
if (!mod) return;
|
||||
const key = input.key.toLowerCase();
|
||||
if (key === "z" && !input.shift) {
|
||||
event.preventDefault();
|
||||
win.webContents.send("folio:undo-requested");
|
||||
} else if ((key === "z" && input.shift) || (key === "y" && !input.shift)) {
|
||||
event.preventDefault();
|
||||
win.webContents.send("folio:redo-requested");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function openBookAt(p: string): Promise<BookResult | ErrResult> {
|
||||
try {
|
||||
const meta = loadBook(p);
|
||||
|
|
|
|||
|
|
@ -66,8 +66,16 @@ export function buildMenu(
|
|||
const editMenu: MenuItem = {
|
||||
label: "Edit",
|
||||
submenu: [
|
||||
{ role: "undo" },
|
||||
{ role: "redo" },
|
||||
{
|
||||
label: "Undo",
|
||||
accelerator: "CmdOrCtrl+Z",
|
||||
click: () => getWindow()?.webContents.send("folio:undo-requested"),
|
||||
},
|
||||
{
|
||||
label: "Redo",
|
||||
accelerator: isMac ? "Cmd+Shift+Z" : "Ctrl+Shift+Z",
|
||||
click: () => getWindow()?.webContents.send("folio:redo-requested"),
|
||||
},
|
||||
{ type: "separator" },
|
||||
{ role: "cut" },
|
||||
{ role: "copy" },
|
||||
|
|
|
|||
|
|
@ -47,4 +47,8 @@ contextBridge.exposeInMainWorld("folio", {
|
|||
ipcRenderer.on("folio:export-markdown-requested", () => cb()),
|
||||
onExportZipRequested: (cb: () => void) =>
|
||||
ipcRenderer.on("folio:export-zip-requested", () => cb()),
|
||||
onUndoRequested: (cb: () => void) =>
|
||||
ipcRenderer.on("folio:undo-requested", () => cb()),
|
||||
onRedoRequested: (cb: () => void) =>
|
||||
ipcRenderer.on("folio:redo-requested", () => cb()),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { BookMeta } from "../main/project.js";
|
|||
import { alertMessage, confirmMessage, promptInput } from "./dialog.js";
|
||||
import { createMdEditor, editorText, setEditorText } from "./md-editor.js";
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
import { redo, undo } from "@codemirror/commands";
|
||||
import { redo, undo, indentLess, indentMore } from "@codemirror/commands";
|
||||
import {
|
||||
clearFormatting,
|
||||
headingLevelAt,
|
||||
|
|
@ -11,7 +11,7 @@ import {
|
|||
toggleBlockquote,
|
||||
toggleBold,
|
||||
toggleBulletList,
|
||||
toggleInlineCode,
|
||||
toggleCode,
|
||||
toggleItalic,
|
||||
toggleLink,
|
||||
toggleOrderedList,
|
||||
|
|
@ -45,6 +45,8 @@ export type FolioAPI = {
|
|||
onRevealRequested: (cb: (p: string) => void) => void;
|
||||
onExportMarkdownRequested: (cb: () => void) => void;
|
||||
onExportZipRequested: (cb: () => void) => void;
|
||||
onUndoRequested: (cb: () => void) => void;
|
||||
onRedoRequested: (cb: () => void) => void;
|
||||
};
|
||||
|
||||
const $ = (id: string) => document.getElementById(id) as HTMLElement;
|
||||
|
|
@ -102,11 +104,13 @@ export function initBook(api: FolioAPI) {
|
|||
bold: toggleBold,
|
||||
italic: toggleItalic,
|
||||
strike: toggleStrikethrough,
|
||||
code: toggleInlineCode,
|
||||
code: toggleCode,
|
||||
link: toggleLink,
|
||||
bullet: toggleBulletList,
|
||||
number: toggleOrderedList,
|
||||
quote: toggleBlockquote,
|
||||
indent: indentMore,
|
||||
outdent: indentLess,
|
||||
hr: insertHorizontalRule,
|
||||
clear: clearFormatting,
|
||||
};
|
||||
|
|
@ -537,6 +541,14 @@ export function initBook(api: FolioAPI) {
|
|||
api.onRevealRequested((p) => void api.revealInFolder(p));
|
||||
api.onExportMarkdownRequested(() => void exportMarkdown());
|
||||
api.onExportZipRequested(() => void exportZip());
|
||||
api.onUndoRequested(() => {
|
||||
if (editorView.hasFocus) undo(editorView);
|
||||
else document.execCommand("undo");
|
||||
});
|
||||
api.onRedoRequested(() => {
|
||||
if (editorView.hasFocus) redo(editorView);
|
||||
else document.execCommand("redo");
|
||||
});
|
||||
|
||||
setChapterButtons(false);
|
||||
void renderRecents();
|
||||
|
|
|
|||
|
|
@ -68,13 +68,15 @@
|
|||
<button class="toolbar-btn b" data-cmd="bold" title="Bold (Ctrl+B)">B</button>
|
||||
<button class="toolbar-btn i" data-cmd="italic" title="Italic (Ctrl+I)">I</button>
|
||||
<button class="toolbar-btn s" data-cmd="strike" title="Strikethrough">S</button>
|
||||
<button class="toolbar-btn code" data-cmd="code" title="Inline code">code</button>
|
||||
<button class="toolbar-btn code" data-cmd="code" title="Inline code / code block">code</button>
|
||||
<button class="toolbar-btn" data-cmd="link" title="Link (Ctrl+K)">↗</button>
|
||||
</div>
|
||||
<div class="toolbar-group">
|
||||
<button class="toolbar-btn" data-cmd="bullet" title="Bulleted list">•</button>
|
||||
<button class="toolbar-btn" data-cmd="number" title="Numbered list">1.</button>
|
||||
<button class="toolbar-btn" data-cmd="quote" title="Blockquote">❝</button>
|
||||
<button class="toolbar-btn" data-cmd="indent" title="Indent (Tab)">→</button>
|
||||
<button class="toolbar-btn" data-cmd="outdent" title="Outdent (Shift+Tab)">←</button>
|
||||
<button class="toolbar-btn" data-cmd="hr" title="Horizontal rule">―</button>
|
||||
<button class="toolbar-btn" data-cmd="clear" title="Clear formatting">⌫</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
import { markdown } from "@codemirror/lang-markdown";
|
||||
import { highlightSelectionMatches, search, searchKeymap } from "@codemirror/search";
|
||||
import { tags } from "@lezer/highlight";
|
||||
import { toggleBold, toggleItalic, toggleLink } from "./md-format.js";
|
||||
import { toggleBold, toggleItalic, toggleLink, toggleStrikethrough } from "./md-format.js";
|
||||
|
||||
export interface MdEditorCallbacks {
|
||||
onChange: (text: string) => void;
|
||||
|
|
@ -27,6 +27,7 @@ export interface MdEditorCallbacks {
|
|||
const formatBindings: KeyBinding[] = [
|
||||
{ key: "Mod-b", run: toggleBold, preventDefault: true },
|
||||
{ key: "Mod-i", run: toggleItalic, preventDefault: true },
|
||||
{ key: "Mod-Shift-s", run: toggleStrikethrough, preventDefault: true },
|
||||
{ key: "Mod-k", run: toggleLink, preventDefault: true },
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -98,6 +98,44 @@ export const toggleItalic: FormatCommand = toggleInline("*");
|
|||
export const toggleStrikethrough: FormatCommand = toggleInline("~~");
|
||||
export const toggleInlineCode: FormatCommand = toggleInline("`");
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Code: inline for single-line/empty selection, fenced for blocks */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const fenceLineRe = /^```[a-zA-Z0-9_-]*[ \t]*$/;
|
||||
|
||||
export const toggleCode: FormatCommand = (target) => {
|
||||
const { state, dispatch } = target;
|
||||
const { from, to } = state.selection.main;
|
||||
const first = state.doc.lineAt(from);
|
||||
const last = state.doc.lineAt(to);
|
||||
|
||||
if (first.number === last.number) {
|
||||
return toggleInlineCode(target);
|
||||
}
|
||||
|
||||
let start = first.from;
|
||||
let end = last.to;
|
||||
let region = slice(state, start, end);
|
||||
let lines = region.split("\n");
|
||||
|
||||
// Selection starts at an opening fence: treat it as a fenced-block toggle.
|
||||
if (lines.length > 1 && fenceLineRe.test(lines[0])) {
|
||||
if (!fenceLineRe.test(lines[lines.length - 1]) && last.number < state.doc.lines) {
|
||||
const next = state.doc.line(last.number + 1);
|
||||
if (fenceLineRe.test(next.text)) end = next.to;
|
||||
}
|
||||
region = slice(state, start, end);
|
||||
lines = region.split("\n");
|
||||
const body = lines.slice(1, lines.length > 1 && fenceLineRe.test(lines[lines.length - 1]) ? -1 : lines.length);
|
||||
dispatch({ changes: { from: start, to: end, insert: body.join("\n") } });
|
||||
return true;
|
||||
}
|
||||
|
||||
dispatch({ changes: { from: start, to: end, insert: "```\n" + region + "\n```" } });
|
||||
return true;
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Links */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
|
@ -106,6 +144,19 @@ export const toggleLink: FormatCommand = (target) => {
|
|||
const { state, dispatch } = target;
|
||||
const { from, to } = state.selection.main;
|
||||
|
||||
const text = slice(state, from, to);
|
||||
|
||||
// Selection is already a link → select its URL so the user can edit it.
|
||||
const link = /^\[([^\]\n]*)\]\(([^)\n]*)\)$/.exec(text);
|
||||
if (link) {
|
||||
const urlFrom = from + 1 + link[1].length + 2;
|
||||
dispatch({
|
||||
selection: EditorSelection.range(urlFrom, urlFrom + link[2].length),
|
||||
scrollIntoView: true,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (from === to) {
|
||||
const pos = from;
|
||||
dispatch({
|
||||
|
|
@ -115,10 +166,9 @@ export const toggleLink: FormatCommand = (target) => {
|
|||
return true;
|
||||
}
|
||||
|
||||
const label = slice(state, from, to);
|
||||
const urlStart = from + label.length + 3;
|
||||
const urlStart = from + text.length + 3;
|
||||
dispatch({
|
||||
changes: { from, to, insert: `[${label}](url)` },
|
||||
changes: { from, to, insert: `[${text}](url)` },
|
||||
selection: EditorSelection.range(urlStart, urlStart + 3),
|
||||
});
|
||||
return true;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue