Search code examples
javascriptdomdom-eventscontenteditablemutation-observers

MutationObserver create infinite loop when I replace a string with <span> tag


I would like to replace all occurence with a html contenteditable div. I need to replace the occurence with a tag to customize this with a css. But when I try to replace with a span, MutationObserver generate a infinite loop...

document.designMode = "on";

var text = document.getElementById('text');

let observer = new MutationObserver(mutations =>
  mutations.forEach(mutation => {
    console.log(text.textContent);
    // let a = "a";
    // a.style.color = "red";
    text.textContent = text.textContent.replace('/', '<span>/</span>');
    observer.disconnect(); 
    const range = document.createRange();
    const textNode = text.firstChild;
    range.setStart(textNode, text.textContent.length);
    range.setEnd(textNode, text.textContent.length);
    const sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);
    // observer.disconnect(); 
  })
);

observer.observe(text, {
  childList: true,
  characterData: true,
  subtree: true,
  
});

<p id="text" contenteditable="true">I'm a text will be change</p>

Solution

  • The infinite loop is caused by replacing typed / characters with <span>/</span> without turning off the observer first - each replacement triggers the observer again.

    The solution for this problem is simple: turn the observer off before replacement and turn it on again afterwards:

    "use strict";
    document.designMode = "on";
    
    var text = document.getElementById('text');
    
    let observer = new MutationObserver(mutations =>
      mutations.forEach(mutation => {
        console.log(text.textContent);
        observer.disconnect(); // turn observer off;
        text.textContent = text.textContent.replace('/', '<span>/</span>');
    
        const range = document.createRange();
        const textNode = text.firstChild;
        range.setStart(textNode, text.textContent.length);
        range.setEnd(textNode, text.textContent.length);
        const sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
        observe(); // turn back on 
      })
    );
    
    const observe = ()=> {
    
      observer.observe(text, {
        childList: true,
        characterData: true,
        subtree: true,
       
      });
    };
    observe();
    span { background-color: yellow;}
    <p id="text" contenteditable="true">I'm a text will be change</p>

    But this only hightlights a potential new problem: the code replaces textContent with HTML which, because it's inside a text node, is displayed in source form, and because <span>/</span> is inside a text node, CSS to highlight SPAN elements in yellow doesn't work (it's not a SPAN element).

    Assuming the span element should be rendered as HTML, the design problem now becomes how to replace newly typed forward slashes with a SPAN element in the DOM, not in a text node. The mutation.target property of Mutation Records may be a good place to start by splitting it into three nodes: a textnode for text before the /, an element node for <span>/</span> and a final textnode for text following the / if any, followed by repositioning the cursor after the span node if required.