"إذا أراد العامل أن يؤدي عمله بشكل جيد، فعليه أولاً أن يشحذ أدواته." - كونفوشيوس، "مختارات كونفوشيوس. لو لينجونج"
الصفحة الأمامية > برمجة > إنشاء محرر ذكي: اكتشاف عناوين URL تلقائيًا وتحويلها إلى ارتباطات تشعبية

إنشاء محرر ذكي: اكتشاف عناوين URL تلقائيًا وتحويلها إلى ارتباطات تشعبية

تم النشر بتاريخ 2024-11-04
تصفح:513

هذه فكرة خطرت ببالي في العمل لتحسين تجربة المستخدم. يتضمن ذلك تنفيذ مربع نص يكتشف تلقائيًا عناوين 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"، حيث يتم تشغيل منطق التحويل فقط بعد أن يتوقف المستخدم عن الكتابة لمدة ثانية واحدة افتراضيًا.

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، وكيفية استخدام التحديد والنطاق لاستعادة موضع المؤشر، وكيفية التعامل مع العقد الخاصة بعنصر لتحقيق هدف المحرر الوظيفة. على الرغم من أن التعبيرات العادية ليست محور هذه المقالة، إلا أن التعبير العادي الكامل يمكن أن يعزز قوة المحرر في تحديد سلاسل معينة (سيظل التعبير العادي المستخدم في هذه المقالة مفتوحًا للتعديل). يمكنك الوصول إلى الكود المصدري عبر Github/AutolilnkEditor للحصول على مزيد من التفاصيل إذا كان ذلك مفيدًا لمشروعك.

بيان الافراج تم إعادة نشر هذه المقالة على: https://dev.to/oninebx/building-a-smart-editor-automatically-detect-urls-and-convert-them-to-hyperlinks-ilg?1 إذا كان هناك أي انتهاك، من فضلك اتصل بـ [email protected]
أحدث البرنامج التعليمي أكثر>

تنصل: جميع الموارد المقدمة هي جزئيًا من الإنترنت. إذا كان هناك أي انتهاك لحقوق الطبع والنشر الخاصة بك أو الحقوق والمصالح الأخرى، فيرجى توضيح الأسباب التفصيلية وتقديم دليل على حقوق الطبع والنشر أو الحقوق والمصالح ثم إرسالها إلى البريد الإلكتروني: [email protected]. سوف نتعامل مع الأمر لك في أقرب وقت ممكن.

Copyright© 2022 湘ICP备2022001581号-3