Skip to content
Merged
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
42 changes: 2 additions & 40 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
type CustomEditorBlock,
type CustomPartialBlock,
} from "./editor/customMarkdownConverter";
import { createMarkdownPasteHandler } from "./editor/createMarkdownPasteHandler";
import { customSchema, type CustomEditor } from "./editor/customSchema";
import { setStepsFetcher, type StepJsonApiDocument } from "./editor/stepAutocomplete";
import { setSnippetFetcher, type SnippetJsonApiDocument } from "./editor/snippetAutocomplete";
Expand Down Expand Up @@ -347,46 +348,7 @@ function CustomSlashMenu() {
function App() {
const editor = useCreateBlockNote({
schema: customSchema,
pasteHandler: ({ event, editor, defaultPasteHandler }) => {
const plainText = event.clipboardData?.getData("text/plain") ?? "";

if (!plainText.trim()) {
return defaultPasteHandler();
}

try {
const parsedBlocks = markdownToBlocks(plainText);

if (parsedBlocks.length === 0) {
return defaultPasteHandler();
}

const selection = editor.getSelection();
const selectedIds = selection?.blocks
?.map((block) => block.id)
.filter((id): id is string => Boolean(id)) ?? [];

if (selectedIds.length > 0) {
editor.replaceBlocks(selectedIds, parsedBlocks);
} else {
const cursorBlock = editor.getTextCursorPosition().block;
if (cursorBlock) {
editor.replaceBlocks([cursorBlock.id], parsedBlocks);
} else if (editor.document.length > 0) {
const reference = editor.document[editor.document.length - 1];
editor.insertBlocks(parsedBlocks, reference.id, "after");
} else {
return defaultPasteHandler();
}
}

editor.focus();
return true;
} catch (error) {
console.error("Failed to paste custom markdown", error);
return defaultPasteHandler();
}
},
pasteHandler: createMarkdownPasteHandler(markdownToBlocks),
});
const [markdown, setMarkdown] = useState("");
const [conversionError, setConversionError] = useState<string | null>(null);
Expand Down
49 changes: 49 additions & 0 deletions src/editor/createMarkdownPasteHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { BlockNoteEditor } from "@blocknote/core";
import type { CustomPartialBlock } from "./customMarkdownConverter";

type PasteHandlerContext = {
event: ClipboardEvent;
editor: BlockNoteEditor<any, any, any>;
defaultPasteHandler: (context?: {
prioritizeMarkdownOverHTML?: boolean;
plainTextAsMarkdown?: boolean;
}) => boolean | undefined;
};

export function createMarkdownPasteHandler(
converter: (markdown: string) => CustomPartialBlock[],
) {
return ({ event, editor, defaultPasteHandler }: PasteHandlerContext): boolean | undefined => {
const plainText = event.clipboardData?.getData("text/plain") ?? "";
if (!plainText.trim()) return defaultPasteHandler();

try {
const parsedBlocks = converter(plainText);
if (parsedBlocks.length === 0) return defaultPasteHandler();

const selection = editor.getSelection();
const selectedIds = selection?.blocks
?.map((block: any) => block.id)
.filter((id: unknown): id is string => Boolean(id)) ?? [];

if (selectedIds.length > 0) {
editor.replaceBlocks(selectedIds, parsedBlocks);
} else {
const cursorBlock = editor.getTextCursorPosition().block;
if (cursorBlock) {
editor.replaceBlocks([cursorBlock.id], parsedBlocks);
} else if (editor.document.length > 0) {
const reference = editor.document[editor.document.length - 1];
editor.insertBlocks(parsedBlocks, reference.id, "after");
} else {
return defaultPasteHandler();
}
}

editor.focus();
return true;
} catch {
return defaultPasteHandler();
}
};
}
3 changes: 1 addition & 2 deletions src/editor/customMarkdownConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,9 +578,8 @@ function serializeBlocks(blocks: CustomEditorBlock[], ctx: MarkdownContext): str
export function blocksToMarkdown(blocks: CustomEditorBlock[]): string {
const lines = serializeBlocks(blocks, { listDepth: 0, insideQuote: false });
const cleaned = lines
// Collapse excessive blank lines but preserve one extra for empty paragraphs.
.join("\n")
.replace(/\n{4,}/g, "\n\n\n")
.replace(/\n{3,}/g, "\n\n")
.trimEnd();

return cleaned;
Expand Down
4 changes: 2 additions & 2 deletions src/editor/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
--color-accent-500: #10b981;

/* Selection */
--color-selection: #f4d35e;
--color-selection: rgba(0, 120, 215, 0.3);

/* Semantic tokens - these reference the palette above */
--text-primary: #262626;
Expand Down Expand Up @@ -1162,7 +1162,7 @@ html.dark .bn-step-image-preview__content {
}

.bn-step-field:focus-within .overtype-wrapper .overtype-input::selection {
background-color: rgba(244, 211, 94, 0.4) !important;
background-color: var(--color-selection) !important;
}

/* Hide OverType's built-in link tooltip — we use our own */
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,6 @@ export {
type StepImageUploadHandler,
} from "./editor/stepImageUpload";

export { createMarkdownPasteHandler } from "./editor/createMarkdownPasteHandler";

export const testomatioEditorClassName = "markdown testomatio-editor";
Loading