Search code examples
tinymceselectiontinymce-react

How to re-select the same text after modifying it in TinyMCE React


I'm attempting to implement the case change feature available in Microsoft Word with Shift + F3 into a TinyMCE React editor. The problem I'm running into is the last part where it should keep the same text selected/highlighted. The below works fine, as long as I haven't highlighted the last character of a node. If I have selected the end of a line, I get an error: Uncaught DOMException: Index or size is negative or greater than the allowed amount

So far I have the following:

const handleCaseChange = (ed: Editor) => {
   ed.on("keydown", (event: KeyboardEvent) => {
      if (event.shiftKey && event.key === "F3") {
         event.preventDefault();
         const selection = ed.selection.getSel();
         const selectedText = selection?.toString();
         const startOffset = selection?.getRangeAt(0).startOffset;
         const endOffset = selection?.getRangeAt(0).endOffset;

         if (selectedText !== undefined && selectedText.length > 0) {
            let transformedText;
            if (selectedText === selectedText.toUpperCase()) {
               transformedText = selectedText.toLowerCase();
            } else if (selectedText === selectedText.toLowerCase()) {
               transformedText = capitalizeEachWord(selectedText);
            } else {
               transformedText = selectedText.toUpperCase();
            }
            ed.selection.setContent(transformedText);
            const range = ed.getDoc().createRange();

            // This is what's currently erroring
            range.setStart(selection.anchorNode, startOffset);

            if (endOffset === selection?.anchorNode?.textContent?.length) {
               range.setEndAfter(selection.anchorNode);
            } else {
               range.setEnd(selection.anchorNode, endOffset);
            }
            selection.removeAllRanges();
            selection.addRange(range);
         }
      }
   }
}
const capitalizeEachWord = (str: string) => str.replace(/\b\w/g, (char: string) => char.toUpperCase());

What else could I try in the range.setStart to get this to work correctly?


Solution

  • I found a simple solution from the TinyMCE docs - selection.getBookmark().

    getBookmark

    Returns a bookmark location for the current selection. This bookmark object can then be used to restore the selection after some content modification to the document.

    I tried the below and it works to change the case and then mark the new content as selected.

    const handleCaseChange = (ed: Editor) => {
       ed.on("keydown", (event: KeyboardEvent) => {
          if (event.shiftKey && event.key === "F3") {
             event.preventDefault();
             const selection = ed.selection.getSel();
             const selectedText = selection?.toString();
    
             if (selectedText !== undefined && selectedText.length > 0) {
                let transformedText;
                if (selectedText === selectedText.toUpperCase()) {
                   transformedText = selectedText.toLowerCase();
                } else if (selectedText === selectedText.toLowerCase()) {
                   transformedText = capitalizeEachWord(selectedText);
                } else {
                   transformedText = selectedText.toUpperCase();
                }
    
                const bookmark = ed.selection.getBookmark();
                ed.selection.setContent(transformedText);
                ed.selection.moveToBookmark(bookmark);
             }
          }
       }
    }
    const capitalizeEachWord = (str: string) => str.replace(/\b\w/g, (char: string) => char.toUpperCase());