”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > 构建智能编辑器:自动检测 URL 并将其转换为超链接

构建智能编辑器:自动检测 URL 并将其转换为超链接

发布于2024-11-04
浏览:853

这是我在工作中为了改善用户体验而想到的一个想法。它涉及实现一个文本框,自动检测 URL 并将其转换为用户输入的超链接(源代码 Github/AutolinkEditor)。这个很酷的功能实施起来有些棘手,必须解决以下问题。

  • 准确检测文本中的URL
  • 将URL字符串转换为超链接后保持光标位置
  • 当用户编辑超链接文本时相应更新目标URL
  • 保留文本中的换行符
  • 支持粘贴富文本,同时保留文本和换行符,文本样式与文本框格式匹配。

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

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

转换是由“onkeyup”和“onpaste”事件驱动的。为了减少转换频率,通过“setTimeout”实现了延迟机制,默认只有用户停止输入1秒后才会触发转换逻辑。

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);
    }

使用正则表达式识别并提取 URL

我并不打算花时间去构建完美的正则表达式来匹配 URL,所以我通过搜索引擎找到了一个可用的正则表达式。如果有人有更好的,请随时告诉我!

...
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  = `${escapeHtml(text)}`;
}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  = `${escapeHtml(url)}`;
    textContent = afterUrl;
  }
  result  = escapeHtml(textContent); // Append any remaining text
}

转换后恢复光标位置

使用 document.createRange 和 window.getSelection 函数,计算节点文本内的光标位置。由于将URL转换为超链接只是添加标签,而不修改文本内容,因此可以根据之前记录的位置来恢复光标。有关更多详细信息,请阅读 HTML 修改后无法恢复选择,即使是相同的 HTML。

编辑超链接时更新或删除
有时我们会创建文本和目标 URL 相同的超链接(此处称为“简单超链接”)。例如,以下 HTML 显示了这种超链接。

http://www.example.com
对于此类链接,当超链接文本修改时,目标 URL 也应自动更新以保持同步。为了使逻辑更加稳健,当超链接文本不再是有效的 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;
  ...
}

为了实现此功能,我将“简单超链接”存储在一个对象中,并在 onpaste、onkeyup 和 onfocus 事件期间实时更新它们,以确保上述逻辑仅处理简单超链接。

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;
  ...
}

处理换行符和样式

处理粘贴的富文本时,编辑器将自动使用编辑器的文本样式设置文本样式。为了保持格式,富文本中的
标签和所有超链接将被保留。处理输入文本更为复杂。当用户按 Enter 键添加新行时,div 元素将添加到编辑器中,编辑器将其替换为
以保持格式设置。

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 "
"; } return ""; }

结论

本文介绍了一些实现简单编辑器的实用技巧,比如常见的 onkeyup 和 onpaste 事件,如何使用 Selection 和 Range 来恢复光标位置,以及如何处理元素的节点来实现编辑器的功能。功能。虽然正则表达式不是本文的重点,但完整的正则表达式可以增强编辑器识别特定字符串的稳健性(本文中使用的正则表达式将保持开放以供修改)。如果对您的项目有帮助,您可以通过 Github/AutolilnkEditor 访问源代码以获取更多详细信息。

版本声明 本文转载于:https://dev.to/oninebx/building-a-smart-editor-automatically-detect-urls-and-convert-them-to-hyperlinks-ilg?1如有侵犯,请联系[email protected]删除
最新教程 更多>
  • 插入数据时如何修复“常规错误:2006 MySQL 服务器已消失”?
    插入数据时如何修复“常规错误:2006 MySQL 服务器已消失”?
    插入记录时如何解决“一般错误:2006 MySQL 服务器已消失”介绍:将数据插入 MySQL 数据库有时会导致错误“一般错误:2006 MySQL 服务器已消失”。当与服务器的连接丢失时会出现此错误,通常是由于 MySQL 配置中的两个变量之一所致。解决方案:解决此错误的关键是调整wait_tim...
    编程 发布于2024-12-26
  • Bootstrap 4 Beta 中的列偏移发生了什么?
    Bootstrap 4 Beta 中的列偏移发生了什么?
    Bootstrap 4 Beta:列偏移的删除和恢复Bootstrap 4 在其 Beta 1 版本中引入了重大更改柱子偏移了。然而,随着 Beta 2 的后续发布,这些变化已经逆转。从 offset-md-* 到 ml-auto在 Bootstrap 4 Beta 1 中, offset-md-*...
    编程 发布于2024-12-26
  • HTML 格式标签
    HTML 格式标签
    HTML 格式化元素 **HTML Formatting is a process of formatting text for better look and feel. HTML provides us ability to format text without us...
    编程 发布于2024-12-26
  • 如何使用 MySQL 查找今天生日的用户?
    如何使用 MySQL 查找今天生日的用户?
    如何使用 MySQL 识别今天生日的用户使用 MySQL 确定今天是否是用户的生日涉及查找生日匹配的所有行今天的日期。这可以通过一个简单的 MySQL 查询来实现,该查询将存储为 UNIX 时间戳的生日与今天的日期进行比较。以下 SQL 查询将获取今天有生日的所有用户: FROM USERS ...
    编程 发布于2024-12-26
  • 在 Go 中使用 WebSocket 进行实时通信
    在 Go 中使用 WebSocket 进行实时通信
    构建需要实时更新的应用程序(例如聊天应用程序、实时通知或协作工具)需要比传统 HTTP 更快、更具交互性的通信方法。这就是 WebSockets 发挥作用的地方!今天,我们将探讨如何在 Go 中使用 WebSocket,以便您可以向应用程序添加实时功能。 在这篇文章中,我们将介绍: WebSocke...
    编程 发布于2024-12-26
  • 除了“if”语句之外:还有什么地方可以在不进行强制转换的情况下使用具有显式“bool”转换的类型?
    除了“if”语句之外:还有什么地方可以在不进行强制转换的情况下使用具有显式“bool”转换的类型?
    无需强制转换即可上下文转换为 bool您的类定义了对 bool 的显式转换,使您能够在条件语句中直接使用其实例“t”。然而,这种显式转换提出了一个问题:“t”在哪里可以在不进行强制转换的情况下用作 bool?上下文转换场景C 标准指定了四种值可以根据上下文转换为的主要场景bool:语句:if、whi...
    编程 发布于2024-12-26
  • 如何修复 macOS 上 Django 中的“配置不正确:加载 MySQLdb 模块时出错”?
    如何修复 macOS 上 Django 中的“配置不正确:加载 MySQLdb 模块时出错”?
    MySQL配置不正确:相对路径的问题在Django中运行python manage.py runserver时,可能会遇到以下错误:ImproperlyConfigured: Error loading MySQLdb module: dlopen(/Library/Python/2.7/site-...
    编程 发布于2024-12-26
  • 尽管代码有效,为什么 POST 请求无法捕获 PHP 中的输入?
    尽管代码有效,为什么 POST 请求无法捕获 PHP 中的输入?
    解决 PHP 中的 POST 请求故障在提供的代码片段中:action=''而不是:action="<?php echo $_SERVER['PHP_SELF'];?>";?>"检查 $_POST数组:表单提交后使用 var_dump 检查 $_POST 数...
    编程 发布于2024-12-26
  • 如何在 PHP 中组合两个关联数组,同时保留唯一 ID 并处理重复名称?
    如何在 PHP 中组合两个关联数组,同时保留唯一 ID 并处理重复名称?
    在 PHP 中组合关联数组在 PHP 中,将两个关联数组组合成一个数组是一项常见任务。考虑以下请求:问题描述:提供的代码定义了两个关联数组,$array1和$array2。目标是创建一个新数组 $array3,它合并两个数组中的所有键值对。 此外,提供的数组具有唯一的 ID,而名称可能重合。要求是构...
    编程 发布于2024-12-26
  • 大批
    大批
    方法是可以在对象上调用的 fns 数组是对象,因此它们在 JS 中也有方法。 slice(begin):将数组的一部分提取到新数组中,而不改变原始数组。 let arr = ['a','b','c','d','e']; // Usecase: Extract till index p...
    编程 发布于2024-12-26
  • 如何在 PHP 中转换所有类型的智能引号?
    如何在 PHP 中转换所有类型的智能引号?
    在 PHP 中转换所有类型的智能引号智能引号是用于代替常规直引号(' 和 ")的印刷标记。它们提供了更精致和然而,软件应用程序通常会在不同类型的智能引号之间进行转换,从而导致不一致。智能引号中的挑战转换转换智能引号的困难在于用于表示它们的各种编码和字符,不同的操作系统和软件程序采用...
    编程 发布于2024-12-26
  • 循环 JavaScript 数组有哪些不同的方法?
    循环 JavaScript 数组有哪些不同的方法?
    使用 JavaScript 循环遍历数组遍历数组的元素是 JavaScript 中的一项常见任务。有多种方法可供选择,每种方法都有自己的优点和局限性。让我们探讨一下这些选项:数组1。 for-of 循​​环 (ES2015 )此循环使用迭代器迭代数组的值:const arr = ["a&q...
    编程 发布于2024-12-26
  • 如何在 Python 中有效地暂停 Selenium WebDriver 执行?
    如何在 Python 中有效地暂停 Selenium WebDriver 执行?
    Selenium WebDriver 中的等待和条件语句问题: 如何在 Python 中暂停 Selenium WebDriver 执行几毫秒?答案:虽然time.sleep() 函数可用于暂停执行指定的秒数,在 Selenium WebDriver 自动化中一般不建议使用。使用 Selenium ...
    编程 发布于2024-12-26
  • C++ 赋值运算符应该是虚拟的吗?
    C++ 赋值运算符应该是虚拟的吗?
    C 中的虚拟赋值运算符及其必要性 虽然赋值运算符可以在 C 中定义为虚拟,但这不是强制要求。然而,这种虚拟声明引发了关于虚拟性的必要性以及其他运算符是否也可以虚拟的问题。虚拟赋值运算符的案例赋值运算符本质上并不虚拟。然而,当将继承类的对象分配给基类变量时,它就变得必要了。这种动态绑定保证了调用基于对...
    编程 发布于2024-12-26
  • JavaScript 中的 Let 与 Var:范围和用法有什么区别?
    JavaScript 中的 Let 与 Var:范围和用法有什么区别?
    JavaScript 中的 Let 与 Var:揭秘范围和临时死区在 ECMAScript 6 中引入,let 语句引发了开发人员的困惑,特别是它与已建立的 var 关键字有何不同。本文深入研究了这两个变量声明之间的细微差别,重点介绍了它们的作用域规则和最佳用例。范围根本区别在于它们的作用域行为。用...
    编程 发布于2024-12-26

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3