Lädt...


🔧 Building a Smart Editor: Automatically Detect URLs and Convert Them to Hyperlinks


Nachrichtenbereich: 🔧 Programmierung
🔗 Quelle: dev.to

This is an idea I came up with at work to improve the user experience. It involves implementing a text box that automatically detects URLs and converts them into hyperlinks as the user types(Source code Github/AutolinkEditor). This cool feature is somewhat tricky to implement, and the following issues must be addressed.

  • Accurately detect URLs within the text
  • Maintain the cursor position after converting the URL string into a hyperlink
  • Update the target URL accordingly when users edit the hyperlink text
  • Preserve line breaks in the text
  • Support pasting rich text while retaining both text and line breaks, with the text style matching the format of the text box.

Image description

...
 if(target && target.contentEditable){
  ...
  target.contentEditable = true;
  target.focus();
 }
...

The conversion is driven by “onkeyup” and “onpaste” events. To reduce the frequency of conversions, a delay mechanism is implemented with “setTimeout”, where the conversion logic is triggered only after the user stops typing for 1 second by default.

idle(func, delay = 1000) {
      ...
      const idleHandler = function(...args) {
        if(this[timer]){
          clearTimeout(this[timer]);
          this[timer] = null;
        }
        this[timer] = setTimeout(() => {
          func(...args);
          this[timer] = null;
        }, delay);

      };
      return idleHandler.bind(this);
    }

Identify and extract URLs with regular expression

I didn’t intend to spend time crafting the perfect regex for matching URLs, so I found a usable one via a search engine. If anyone has a better one, feel free to let me know!

...
const URLRegex = /^(https?:\/\/(([a-zA-Z0-9]+-?)+[a-zA-Z0-9]+\.)+(([a-zA-Z0-9]+-?)+[a-zA-Z0-9]+))(:\d+)?(\/.*)?(\?.*)?(#.*)?$/;
const URLInTextRegex = /(https?:\/\/(([a-zA-Z0-9]+-?)+[a-zA-Z0-9]+\.)+(([a-zA-Z0-9]+-?)+[a-zA-Z0-9]+))(:\d+)?(\/.*)?(\?.*)?(#.*)?/;
...

if(URLRegex.test(text)){
  result += `<a href="${escapeHtml(text)}">${escapeHtml(text)}</a>`;
}else {
  // text contains url
  let textContent = text;
  let match;
  while ((match = URLInTextRegex.exec(textContent)) !== null) {
    const url = match[0];
    const beforeUrl = textContent.slice(0, match.index);
    const afterUrl = textContent.slice(match.index + url.length);

    result += escapeHtml(beforeUrl);
    result += `<a href="${escapeHtml(url)}">${escapeHtml(url)}</a>`;
    textContent = afterUrl;
  }
  result += escapeHtml(textContent); // Append any remaining text
}

Restoring the cursor position after conversion

With document.createRange and window.getSelectionfunctions, calculate the cursor position within the node’s text. Since converting URLs into hyperlinks only adds tags without modifying the text content, the cursor can be restored based on the previously recorded position. For more details, please read Can’t restore selection after HTML modify, even if it’s the same HTML.

Update or remove when editing hyperlink
Sometimes we create hyperlinks where the text and the target URL are the same(called ‘simple hyperlinks’ here). For example, the following HTML shows this kind of hyperlink.

http://www.example.com
For such links, when the hyperlink text is modified, the target URL should also be automatically updated to keep them in sync. To make the logic more robust, the link will be converted back to plain text when the hyperlink text is no longer a valid URL.

handleAnchor: anchor => {
  ...
    const text = anchor.textContent;
    if(URLRegex.test(text)){
      return nodeHandler.makePlainAnchor(anchor);
    }else {
      return anchor.textContent;
    }
  ...
}
...
makePlainAnchor: target => {
  ...
  const result = document.createElement("a");
  result.href = target.href;
  result.textContent = target.textContent;
  return result;
  ...
}

To implement this feature, I store the ‘simple hyperlinks’ in an object and update them in real-time during the onpaste, onkeyup, and onfocus events to ensure that the above logic only handles simple hyperlinks.

target.onpaste = initializer.idle(e => {
  ...
  inclusion = contentConvertor.indexAnchors(target);
}, 0);

const handleKeyup = initializer.idle(e => {
  ...
  inclusion = contentConvertor.indexAnchors(target);
  ...
}, 1000);

target.onkeyup = handleKeyup;
target.onfocus = e => {
  inclusion = contentConvertor.indexAnchors(target);
}

...

indexAnchors(target) {
  const inclusion = {};
  ...
  const anchorTags = target.querySelectorAll('a');
  if(anchorTags) {
    const idPrefix = target.id === "" ? target.dataset.id : target.id;

    anchorTags.forEach((anchor, index) => {
      const anchorId = anchor.dataset.id ?? `${idPrefix}-anchor-${index}`;
      if(anchor.href.replace(/\/+$/, '').toLowerCase() === anchor.textContent.toLowerCase()) {
        if(!anchor.dataset.id){
          anchor.setAttribute('data-id', anchorId);
        }
        inclusion[[anchorId]] = anchor.href;
      }
    });
  }
  return Object.keys(inclusion).length === 0 ? null : inclusion;
  ...
}

Handle line breaks and styles

When handling pasted rich text, the editor will automatically style the text with the editor’s text styles. To maintain formatting,
tags in the rich text and all hyperlinks will be preserved. Handling input text is more complex. When the user presses Enter to add a new line, a div element is added to the editor, which the editor replaces with a
to maintain the formatting.

node.childNodes.forEach(child => {
  if (child.nodeType === 1) { 
    if(child.tagName === 'A') { // anchar element
      const key = child.id === "" ? child.dataset.id : child.id;

      if(inclusion && inclusion[key]){
        const disposedAnchor = handleAnchor(child);
        if(disposedAnchor){
          if(disposedAnchor instanceof HTMLAnchorElement) {
            disposedAnchor.href = disposedAnchor.textContent;
          }
          result += disposedAnchor.outerHTML ?? disposedAnchor;
        }
      }else {
        result += makePlainAnchor(child)?.outerHTML ?? "";
      }
    }else { 
      result += compensateBR(child) + this.extractTextAndAnchor(child, inclusion, nodeHandler);
    }
  } 
});

...
const ElementsOfBR = new Set([
  'block',
  'block flex',
  'block flow',
  'block flow-root',
  'block grid',
  'list-item',
]);
compensateBR: target => {
  if(target && 
    (target instanceof HTMLBRElement || ElementsOfBR.has(window.getComputedStyle(target).display))){
      return "<br />";
  }
  return "";
}

Conclusions

This article describes some practical techniques used to implement a simple editor, such as common events like onkeyup and onpaste, how to use Selection and Range to restore the cursor position, and how to handle the nodes of an element to achieve the editor's functionality. While regular expressions are not the focus of this article, a complete regex can enhance the editor's robustness in identifying specific strings (the regex used in this article will remain open for modification). You can access the source code via Github/AutolilnkEditor to get more details if it is helpful for your project.

...

🔧 Building a Smart Editor: Automatically Detect URLs and Convert Them to Hyperlinks


📈 95.14 Punkte
🔧 Programmierung

🔧 Web Images: Resize and Convert Perfectly (and Automatically)


📈 24.59 Punkte
🔧 Programmierung

🔧 How to Convert URLs to PDF: A Step-by-Step Guide


📈 24.31 Punkte
🔧 Programmierung

🔧 Convert URLs to EPUB: Easily Create eBooks


📈 24.31 Punkte
🔧 Programmierung

📰 How to Disable Brave Browser Automatically Adding Affiliate Codes in URLs


📈 24.14 Punkte
📰 IT Security Nachrichten

📰 Firefox Can Now Automatically Remove Tracking From URLs


📈 24.14 Punkte
📰 IT Security Nachrichten

📰 X.com Automatically Changing Link Text but Not URLs


📈 24.14 Punkte
📰 IT Security Nachrichten

🪟 Microsoft Edge mobile will soon be able to detect typos in your URLs


📈 23.81 Punkte
🪟 Windows Tipps

🔧 Hyperlinks: Quick and easy.


📈 23.02 Punkte
🔧 Programmierung

📰 Homoglyphs - Get Similar Letters, Convert To ASCII, Detect Possible Languages And UTF-8 Group


📈 22.96 Punkte
📰 IT Security Nachrichten

🎥 Automatically detect and support against anti debug with IDAGhidra to streamline debugging process


📈 22.79 Punkte
🎥 IT Security Video

📰 How to automatically convert Google Drive uploads to Docs format


📈 21.97 Punkte
📰 IT Nachrichten

🔧 Review: Video Tap - automatically convert YouTube videos to blog posts


📈 21.97 Punkte
🔧 Programmierung

🔧 Is there are any mods/plugins to convert old "/particle" to new automatically?


📈 21.97 Punkte
🔧 Programmierung

📰 Bundesgerichtshof präzisiert Haftung für Hyperlinks


📈 21.71 Punkte
📰 IT Nachrichten

📰 Das Web vor Gericht: Hyperlinks nur noch zu klar legalen Inhalten?


📈 21.71 Punkte
📰 IT Security Nachrichten

📰 Praxis-Tipp: So verwendet man Hyperlinks in Office-Textdokumenten


📈 21.71 Punkte
📰 IT Security Nachrichten

📰 EuGH-Urteil stellt klar: Hyperlinks nur noch zu klar legalen Inhalten


📈 21.71 Punkte
📰 IT Security Nachrichten

📰 EuGH zur Haftung für Hyperlinks: Bei Gewinnerzielungsabsicht: Kenntnis vermutet


📈 21.71 Punkte
📰 IT Nachrichten

🪟 Neu: Problem mit Verknüpfungen und Hyperlinks in PowerPoint


📈 21.71 Punkte
🪟 Windows Tipps

📰 Bundesgerichtshof präzisiert Haftung für Hyperlinks


📈 21.71 Punkte
📰 IT Nachrichten

📰 Das Web vor Gericht: Hyperlinks nur noch zu klar legalen Inhalten?


📈 21.71 Punkte
📰 IT Security Nachrichten

📰 Praxis-Tipp: So verwendet man Hyperlinks in Office-Textdokumenten


📈 21.71 Punkte
📰 IT Security Nachrichten

matomo