skip to Main Content

I am developing a feature in my app that searches through the text and abbreviates words/phrases while the user is inputting the text.

let dictionary = {
  "M": {
    "Mobile telephone number": "M/TEL"
  },
  "T": {
    "Telephone": "TEL"
  }
};

function abbreviateText() {
  const input = document.getElementById("input").value;
  let output = input;

  for (const groupKey in dictionary) {
    const group = dictionary[groupKey];

    for (const key in group) {
      const regex = new RegExp(`\b${key}\b`, "gi");
      const replacement = `<span class="tooltip" data-tooltip="${key}">${group[key]}</span>`;
      output = output.replace(regex, replacement);
    }
  }

  document.getElementById("output").innerHTML = output;
}
<textarea id="input" placeholder="Enter message" rows="7" oninput="abbreviateText()"></textarea>
<p id="output"></p>

When inputting mobile telephone number, it should output M/TEL instead of TEL number">M/TEL.

The source output should be <span class="tooltip" data-tooltip="Mobile telephone number">M/TEL"</span>. Note that there is not a span tag encompassing TEL.

I have other instances of this occurring so the solution needs to be general.

2

Answers


  1. If you are sure that your initial input does not contain HTML you can use 2 step replacement.

    1. Run the first loop to replace values with some placeholders.
    2. Run the second loop to replace placeholders.

    Also you need to make sure you have "longer" replacements checking first.

    let dictionary = {
      "M": {
        "Mobile telephone number": "M/TEL"
      },
      "T": {
        "Telephone": "TEL"
      }
    };
    
    const replacements = Object
      .values(dictionary)
      .flatMap(obj => Object.entries(obj))
      .sort((a, b) => a[0] < b[0])
    
    
    function abbreviateText() {
      const input = document.getElementById("input").value;
      let output = input;
      
      const found = []
    
      for (const [key, value] of replacements) {
        const regex = new RegExp(`\b${key}\b`, "gi");
        
        if (regex.test(output)) {
          output = output.replace(regex, `__replacement:${found.length}`);
          found.push([found.length, `<span class="tooltip" data-tooltip="${key}">${value}</span>`])
        }
      }
      
      for (const [index, replacement] of found) {
        output = output.replace(new RegExp(`__replacement:${index}`, 'gi'), replacement)
      }
    
      document.getElementById("output").innerHTML = output;
    }
    <textarea id="input" placeholder="Enter message" rows="7" oninput="abbreviateText()"></textarea>
    <p id="output"></p>
    Login or Signup to reply.
  2. You can tokenize the whole output and then process the tokens one by one:

    // Note the outer parentheses! These form a capturing group,
    // whose match's content will be pushed back to the splitted array at odd indices.
    const tokenizer = new RegExp(`(${words.join('|')})`, 'i');
    
    input.addEventListener('input', function() {
      const value = this.value;
      const tokens = value.split(tokenizer);
      
      let joined = '';
      
      for (const [index, token] of tokens.entries()) {
        const isRegisteredWord = index % 2 === 1;
        
        if (isRegisteredWord) {
          const { abbreviated, tooltip } = wordToAbbreviation[token.toLowerCase()];
          joined += `<abbr title="${tooltip}">${abbreviated}</abbr>`;
        } else {
          joined += token;
        }
      }
      
      output.innerHTML = joined;
    });
    

    To avoid unnecessary iteration, changing the data structure to something like this is essential:

    const dictionary = {
      M: {
        'mobile telephone number': {
          abbreviated: 'M/TEL',
          tooltip: 'Mobile telephone number'
        }
      },
      T: {
        telephone: {
          abbreviated: 'TEL',
          tooltip: 'Telephone'
        }
      }
    };
    
    // Flattening is required, however.
    const wordToAbbreviation = Object.assign(
      Object.create(null),
      ...Object.values(dictionary)
    );
    

    …or you can afford removing the grouping altogether so that the second step is unnecessary:

    const dictionary = {
      'mobile telephone number': {
        abbreviated: 'M/TEL',
        tooltip: 'Mobile telephone number'
      }
      telephone: {
        abbreviated: 'TEL',
        tooltip: 'Telephone'
      }
    };
    

    Try it:

    const dictionary = {
      M: {
        'mobile telephone number': {
          abbreviated: 'M/TEL',
          tooltip: 'Mobile telephone number'
        }
      },
      T: {
        telephone: {
          abbreviated: 'TEL',
          tooltip: 'Telephone'
        }
      }
    };
    
    const wordToAbbreviation = Object.assign(
      Object.create(null),
      ...Object.values(dictionary)
    );
    
    const input = document.getElementById('input');
    const output = document.getElementById('output');
    
    const words = Object.keys(wordToAbbreviation);
    const tokenizer = new RegExp(`(${words.join('|')})`, 'i');
    
    input.addEventListener('input', function() {
      const value = this.value;
      const tokens = value.split(tokenizer);
      
      let joined = '';
      
      for (const [index, token] of tokens.entries()) {
        const isRegisteredWord = index % 2 === 1;
        
        if (isRegisteredWord) {
          const { abbreviated, tooltip } = wordToAbbreviation[token.toLowerCase()];
          joined += `<abbr title="${tooltip}">${abbreviated}</abbr>`;
        } else {
          joined += token;
        }
      }
      
      output.innerHTML = joined;
    });
    
    input.dispatchEvent(new InputEvent('input'));
    <textarea id="input" placeholder="Enter message">mobile TelePhone nuMbeR</textarea>
    <p id="output"></p>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search