From b32659f1244f54e36f02dcf8708292813604cdf4 Mon Sep 17 00:00:00 2001 From: ai-engineer-devansh-singh Date: Sat, 18 Oct 2025 14:32:57 +0100 Subject: [PATCH 1/2] hotkey fix for the editor --- markdoc/editor/hotkeys/hotkeys.markdoc.ts | 216 ++++++++++++---------- 1 file changed, 118 insertions(+), 98 deletions(-) diff --git a/markdoc/editor/hotkeys/hotkeys.markdoc.ts b/markdoc/editor/hotkeys/hotkeys.markdoc.ts index b9fb74c4..05b58f38 100644 --- a/markdoc/editor/hotkeys/hotkeys.markdoc.ts +++ b/markdoc/editor/hotkeys/hotkeys.markdoc.ts @@ -54,107 +54,127 @@ export const useMarkdownHotkeys = ( const handleHotkey = useCallback( (hotkey: Hotkey) => (e: KeyboardEvent) => { e.preventDefault(); - if (textareaRef.current) { - const textarea = textareaRef.current; - const startPos = textarea.selectionStart; - const endPos = textarea.selectionEnd; - const currentValue = textarea.value; - const { markup, type } = hotkey; - let newText; - - switch (type) { - case "pre": - newText = `${markup}${currentValue.slice(startPos, endPos)}`; - break; - - case "wrap": - // check for codeBlock, url then default wrap - if (hotkey.key === "c" && hotkey.useShift) { - newText = `${markup}\n\n${markup}`; - } else if (hotkey.key === "u") { - newText = `${markup[0]}${currentValue.slice(startPos, endPos)}${ - markup[1] - }`; - } else { - newText = `${markup}${currentValue.slice( - startPos, - endPos, - )}${markup}`; - } - break; - - case "blockQuote": - const lines = currentValue.slice(startPos, endPos).split("\n"); - const quotedLines = lines.map((line) => `${markup} ${line}`); - newText = quotedLines.join("\n"); - break; - - case "linkOrImage": - const selectedText = currentValue.slice(startPos, endPos); - if (!selectedText) return; // Do nothing if no text is selected - - const url = prompt("Enter the URL:"); - if (!url) return; - - const tag = markup - .replace("text", selectedText) - .replace("url", url); - textarea.value = `${currentValue.slice( - 0, + e.stopPropagation(); + + const textarea = textareaRef.current; + if (!textarea) return; + + const startPos = textarea.selectionStart; + const endPos = textarea.selectionEnd; + const currentValue = textarea.value; + const { markup, type } = hotkey; + let newText; + + switch (type) { + case "pre": + newText = `${markup}${currentValue.slice(startPos, endPos)}`; + break; + + case "wrap": + // check for codeBlock, url then default wrap + if (hotkey.key === "c" && hotkey.useShift) { + newText = `${markup}\n\n${markup}`; + } else if (hotkey.key === "u") { + newText = `${markup[0]}${currentValue.slice(startPos, endPos)}${ + markup[1] + }`; + } else { + newText = `${markup}${currentValue.slice( startPos, - )}${tag}${currentValue.slice(endPos)}`; - const cursorPos = startPos + tag.length; - textarea.setSelectionRange(cursorPos, cursorPos); - return; - - case "select": - let start = startPos - 1; - - // Move left while the cursor is on whitespace - while (start >= 0 && /\s/.test(currentValue[start])) { - start--; - } - - // Move left while the cursor is on non-whitespace - while (start >= 0 && /\S/.test(currentValue[start])) { - start--; - } - - start++; // Move to the beginning of the word - - // Trim right whitespace - let trimmedEnd = endPos; - while (/\s/.test(currentValue[trimmedEnd - 1])) { - trimmedEnd--; - } - textarea.setSelectionRange(start, trimmedEnd); - return; - - default: - setSelectCount(0); - return; - } - - textarea.value = `${currentValue.slice( - 0, - startPos, - )}${newText}${currentValue.slice(endPos)}`; - const cursorPos = - type === "wrap" && hotkey.key === "c" && hotkey.useShift - ? startPos + markup.length + 1 - : startPos + newText.length; - textarea.setSelectionRange(cursorPos, cursorPos); + endPos, + )}${markup}`; + } + break; + + case "blockQuote": + const lines = currentValue.slice(startPos, endPos).split("\n"); + const quotedLines = lines.map((line) => `${markup} ${line}`); + newText = quotedLines.join("\n"); + break; + + case "linkOrImage": + const selectedText = currentValue.slice(startPos, endPos); + if (!selectedText) return; // Do nothing if no text is selected + + const url = prompt("Enter the URL:"); + if (!url) return; + + const tag = markup + .replace("text", selectedText) + .replace("url", url); + textarea.value = `${currentValue.slice( + 0, + startPos, + )}${tag}${currentValue.slice(endPos)}`; + const cursorPos = startPos + tag.length; + textarea.setSelectionRange(cursorPos, cursorPos); + return; + + case "select": + let start = startPos - 1; + + // Move left while the cursor is on whitespace + while (start >= 0 && /\s/.test(currentValue[start])) { + start--; + } + + // Move left while the cursor is on non-whitespace + while (start >= 0 && /\S/.test(currentValue[start])) { + start--; + } + + start++; // Move to the beginning of the word + + // Trim right whitespace + let trimmedEnd = endPos; + while (/\s/.test(currentValue[trimmedEnd - 1])) { + trimmedEnd--; + } + textarea.setSelectionRange(start, trimmedEnd); + return; + + default: + return; } + + textarea.value = `${currentValue.slice( + 0, + startPos, + )}${newText}${currentValue.slice(endPos)}`; + const cursorPos = + type === "wrap" && hotkey.key === "c" && hotkey.useShift + ? startPos + markup.length + 1 + : startPos + newText.length; + textarea.setSelectionRange(cursorPos, cursorPos); }, - [], + [textareaRef], ); - // Map each hotkey to its corresponding callback - Object.values(hotkeys).forEach((hotkey) => { - useHotkeys( - `${hotkey.key}${hotkey.useShift ? "+meta+shift" : "+meta"}`, - handleHotkey(hotkey), - { enableOnFormTags: true }, - ); - }); + // Use useEffect to bind event listeners directly to the textarea + useEffect(() => { + const textarea = textareaRef.current; + if (!textarea) return; + + const keydownHandler = (e: KeyboardEvent) => { + // Check if it's a meta/ctrl key combination + if (!e.metaKey && !e.ctrlKey) return; + + // Find matching hotkey + const matchingHotkey = Object.values(hotkeys).find((hotkey) => { + const isCorrectKey = e.key === hotkey.key; + const hasCorrectShift = hotkey.useShift ? e.shiftKey : !e.shiftKey; + return isCorrectKey && hasCorrectShift; + }); + + if (matchingHotkey) { + handleHotkey(matchingHotkey)(e); + } + }; + + textarea.addEventListener('keydown', keydownHandler); + + return () => { + textarea.removeEventListener('keydown', keydownHandler); + }; + }, [textareaRef, handleHotkey]); }; From 539bbb3b1c76836d59fa5666ce8688cccaae728a Mon Sep 17 00:00:00 2001 From: ai-engineer-devansh-singh Date: Sat, 18 Oct 2025 15:21:32 +0100 Subject: [PATCH 2/2] Fix hotkeys not working after preview mode toggle --- markdoc/editor/hotkeys/hotkeys.markdoc.ts | 37 +++++++++++++++++------ 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/markdoc/editor/hotkeys/hotkeys.markdoc.ts b/markdoc/editor/hotkeys/hotkeys.markdoc.ts index 05b58f38..01ba715a 100644 --- a/markdoc/editor/hotkeys/hotkeys.markdoc.ts +++ b/markdoc/editor/hotkeys/hotkeys.markdoc.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from "react"; +import { useCallback, useEffect, useRef } from "react"; import { useHotkeys } from "react-hotkeys-hook"; export interface Hotkey { @@ -50,6 +50,9 @@ export const hotkeys: Record = { export const useMarkdownHotkeys = ( textareaRef: React.RefObject, ) => { + const currentTextareaRef = useRef(null); + const handlerRef = useRef<(e: KeyboardEvent) => void>(); + // Create a single callback for all hotkeys const handleHotkey = useCallback( (hotkey: Hotkey) => (e: KeyboardEvent) => { @@ -150,12 +153,8 @@ export const useMarkdownHotkeys = ( [textareaRef], ); - // Use useEffect to bind event listeners directly to the textarea useEffect(() => { - const textarea = textareaRef.current; - if (!textarea) return; - - const keydownHandler = (e: KeyboardEvent) => { + handlerRef.current = (e: KeyboardEvent) => { // Check if it's a meta/ctrl key combination if (!e.metaKey && !e.ctrlKey) return; @@ -170,11 +169,31 @@ export const useMarkdownHotkeys = ( handleHotkey(matchingHotkey)(e); } }; + }, [handleHotkey]); - textarea.addEventListener('keydown', keydownHandler); + useEffect(() => { + const textarea = textareaRef.current; + + if (textarea === currentTextareaRef.current) return; + + // Clean up previous event listener + if (currentTextareaRef.current && handlerRef.current) { + currentTextareaRef.current.removeEventListener('keydown', handlerRef.current); + } + + // Set up new event listener if textarea exists + if (textarea && handlerRef.current) { + textarea.addEventListener('keydown', handlerRef.current); + currentTextareaRef.current = textarea; + } else { + currentTextareaRef.current = null; + } return () => { - textarea.removeEventListener('keydown', keydownHandler); + if (currentTextareaRef.current && handlerRef.current) { + currentTextareaRef.current.removeEventListener('keydown', handlerRef.current); + currentTextareaRef.current = null; + } }; - }, [textareaRef, handleHotkey]); + }); };