اذهب إلى المحتوى

سنتناول في هذا المقال التحديد داخل المستند، وكذلك التحديد داخل حقول الاستمارات، مثل <input>.

تستطيع جافاسكربت الوصول إلى تحديد موجود، أو تحديد عقد من DOM وإزالة التحديد عنها سواء كليّا أو جزئيّا، بالإضافة إلى حذف المحتوى المحدّد من المستند، أو وضعه داخل وسم، إلى غير ذلك.

ستجد بعض الوصفات لمهامّ شائعة في نهاية المقال، في قسم "الملخّص". قد تلبّي تلك الوصفات احتياجاتك الحاليّة، لكنّك ستستفيد أكثر بكثير إذا قرأت النصّ كلّه إذ ستتعرف على الكائنين الأساسيين Range وSelection، ولن تحتاج بفهمها أي مقال آخر لاستعمالهما فيما تريد.

المدى

يُعدّ المدى هو المفهوم اﻷساسيّ في موضوع التحديد، وهو عبارة عن زوج من "النقط الحدّية": بداية المدى ونهاية المدى.

يُنشأ كائن Range دون وسائط:

let range = new Range();

يمكننا بعدها ضبط حدود التحديد باستخدام range.setStart(node, offset)‎ وrange.setEnd(node, offset)‎.

يمكن للوسيط الأوّل node أن يكون إمّا عقدة نصيّة أو عقدة عنصريّة، ويكون معنى الوسيط الثاني معتمدا على ذلك:

  • إذا كان node عقدة نصيّة، فلابدّ أن يكون offset هو الموضع داخل النصّ.
  • إذا كان node عقدة عنصريّة، فلابدّ أن يكون offset هو رقم الابن.

على سبيل المثال، لننشئ مدى في هذا المقطع

<p id="p">Example: <i>italic</i> and <b>bold</b></p>

هذه هي بنية DOM الخاصّة به:

dom1.png

لننشئ مدى من أجل ‎"Example: <i>italic</i>"‎. كما يمكن أن نلاحظ، تتشكّل هذه الجملة من الابنين الأوّل والثاني لـ <p>:

range-example-p-0-1.png

  • لنقطة البداية العقدة الأب <p>، والانحراف offset هو 0.
  • لنقطة النهاية العقدة الأب <p> أيضا، لكن لها الانحراف 2 (تضبط المدى إلى غاية offset، لكن دونه).

لو أجرينا الشيفرة أدناه، يمكن أن نرى بأنّ النصّ يُحدّد.

<p id="p">Example: <i>italic</i> and <b>bold</b></p>

<script>
  let range = new Range();

  range.setStart(p, 0);
  range.setEnd(p, 2);

  // للمدى محتوياته على شكل نص، دون الوسوم‏ toString تعيد
  console.log(range); // Example: italic

  // لنطبّق هذا المدى على تحديد المستند (سيُوضَّح هذا لاحقا)‏
  document.getSelection().addRange(range);
</script>

هذه منصّة اختبار أكثر مرونة حيث يمكنك تجربة المزيد من التحديدات المختلفة:

<p id="p">Example: <i>italic</i> and <b>bold</b></p>

From <input id="start" type="number" value=1>  To <input id="end" type="number" value=4>
<button id="button">Click to select</button>
<script>
  button.onclick = () => {
    let range = new Range();

    range.setStart(p, start.value);
    range.setEnd(p, end.value);

    // طبّق التحديد، سيُوضَّح لاحقا‏
    document.getSelection().removeAllRanges();
    document.getSelection().addRange(range);
  };
</script>

على سبيل المثال، يعطي التحديد من 1 إلى 4 المدى التالي:

range-example-p-1-3.png

لا يجب علينا استخدام العقدة نفسها في setStart وsetEnd فقد يمتدّ المدى عبر العديد من العقد غير المترابطة. ما يهمّ فقط هو أن تكون النهاية موجودة بعد البداية.

تحديد أجزاء من العقد النصية

range-example-p-2-b-3.png

ذلك ممكن أيضا، نحتاج فقط إلى ضبط البداية والنهاية على شكل انحراف نسبيّ في العقد النصّيّة.

نحتاج إلى أن ننشئ مدى بحيث:

  • يبدأ من الموضع 2 في الابن اﻷوّل لـ <p> (آخذًا جميع الحروف ما عدا الحرفين اﻷوّلين لـ "Example: ").
  • ينتهي عند الموضع 3 في الابن اﻷوّل لـ <b> (آخذا الحروف الثلاثة اﻷولى لكلمة "bold"، لا أكثر):
<p id="p">Example: <i>italic</i> and <b>bold</b></p>

<script>
  let range = new Range();

  range.setStart(p.firstChild, 2);
  range.setEnd(p.querySelector('b').firstChild, 3);

  console.log(range); // ample: italic and bol

  // استخدم هذا المدى للتحديد (سيُوضَّح هذا لاحقا)‏
  window.getSelection().addRange(range);
</script>

لكائن المدى الخاصّيّات التالية:

range-example-p-2-b-3-range.png

  • startContainer وstartOffset -- العقدة والانحراف عن البداية.
    • في المثال أعلاه: العقدة النصّيّة اﻷولى داخل <p> و2.
  • endContainer وendOffset -- العقدة والانحراف عن النهاية.
    • في المثال أعلاه: العقدة النصّيّة اﻷولى داخل <b> و3.
  • collapsed -- قيمة بوليانية، تكون true عندما يبدأ المدى وينتهي في النقطة نفسها (وبذلك لا يوجد محتوى داخل المدى)،
    • في المثال أعلاه: false
  • commonAncestorContainer -- أقرب سلف مشترك بين جميع العقد داخل المدى،
    • في المثال أعلاه: <p>

توابع المدى

هناك العديد من التوابع الملائمة للتحكّم في اﻷمداء سنعرضها كلها، ونبدأ بتلك التي تضبط بداية المدى:

  • setStart(node, offset)‎ - يضع البداية عند موضع الانحراف offset في العقدة node
  • setStartBefore(node)‎ - يضع البداية قبل node مباشرة
  • setStartAfter(node)‎ - يضع البداية بعد node مباشرة

ضبط نهاية المدى (توابع مماثلة):

  • setEnd(node, offset)‎ - يضع النهاية عند موضع الانحراف offset في العقدة node
  • setEndBefore(node)‎ - يضع النهاية قبل node مباشرة
  • setEndAfter(node)‎ - يضع النهاية بعد node مباشرة

كما سبق بيانه، يمكن أن تكون node عقدة نصّيّة أو عنصريّة، بالنسبة للعقد العنصريّة تتخطّى offset ذلك العدد من الحروف، بينما للعقد العنصريّة تتخطّى ذلك العدد من العقد اﻷبناء.

توابع أخرى:

  • selectNode(node)‎ - يضبط المدى بحيث تُحدَّد كامل العقدة node
  • selectNodeContents(node)‎ - يضبط المدى بحيث تُحدَّد جميع محتويات العقدة node
  • collapse(toStart)‎ إذا كانت toStart=true - يضع end=start، وإلّا يضع start=end، فينطوي بذلك المدى.
  • cloneRange()‎ - ينشئ مدى جديدا بنفس البداية/النهاية

للتحكّم في المحتوى الموجود ضمن المدى:

  • deleteContents()‎ - يزيل محتوى المدى من المستند
  • extractContents()‎ - يزيل محتوى المدى من المستند ويعيده على شكل قطعة مستند DocumentFragment
  • cloneContents()‎ - يستنسخ محتوى المستند ويعيده على شكل قطعة مستند ‎DocumentFragment
  • insertNode(node)‎ - يدرج العقدة node في المستند عند بداية المدى
  • surroundContents(node)‎ - يحيط العقدة بمحتوى المدى. لكي يعمل هذا، يجب أن يحتوي المدى على وسوم فتح وإغلاق لجميع العناصر التي في داخله: لا وجود ﻷمداء جزئيّة مثل ‎<i>abc.

بهذه التوابع يمكننا فعل أيّ شيء بالعقد المُحدّدة.

هذه منصّة الاختبار لرؤيتها كيف تعمل:

Click buttons to run methods on the selection, "resetExample" to reset it.

<p id="p">Example: <i>italic</i> and <b>bold</b></p>

<p id="result"></p>
<script>
  let range = new Range();

  // جميع التوابع المعروضة ممثّلة هنا
  let methods = {
    deleteContents() {
      range.deleteContents()
    },
    extractContents() {
      let content = range.extractContents();
      result.innerHTML = "";
      result.append("extracted: ", content);
    },
    cloneContents() {
      let content = range.cloneContents();
      result.innerHTML = "";
      result.append("cloned: ", content);
    },
    insertNode() {
      let newNode = document.createElement('u');
      newNode.innerHTML = "NEW NODE";
      range.insertNode(newNode);
    },
    surroundContents() {
      let newNode = document.createElement('u');
      try {
        range.surroundContents(newNode);
      } catch(e) { console.log(e) }
    },
    resetExample() {
      p.innerHTML = `Example: <i>italic</i> and <b>bold</b>`;
      result.innerHTML = "";

      range.setStart(p.firstChild, 2);
      range.setEnd(p.querySelector('b').firstChild, 3);

      window.getSelection().removeAllRanges();  
      window.getSelection().addRange(range);  
    }
  };

  for(let method in methods) {
    document.write(`<div><button onclick="methods.${method}()">${method}</button></div>`);
  }

  methods.resetExample();
</script>

توجد أيضا توابع للمقارنة بين اﻷمداء. لكن يندر استخدامها. إن احتجت لها، يُرجى الرجوع إلى المواصفة أو دليل MDN.

التحديد

Range هو كائن عامّ لإدارة أمداء التحديد. يمكننا إنشاء مثل هذه الكائنات، وتمريرها إلى هنا وهناك، لكنّها لا تُحدِّد ظاهريّا أيّ شيء بمفردها.

يُمثَّل تحديد المستند بواسطة الكائن Selection، الذي يمكن الحصول عليه من window.getSelection()‎ أو document.getSelection()‎. قد يتضمّن التحديد صفرا فأكثر من اﻷمداء. على الأقلّ، كما تنصّ على ذلك مواصفة الواجهة البرمجيّة للتحديد. لكن عمليّا، فايرفوكس فقط هو الذي يمكّن من تحديد أمداء متعدّدة في المستند باستخدام المفاتيح Ctrl+click‏ (Cmd+click بالنسبة لـ Mac).

هذه لقطة شاشة فيها تحديد بثلاثة أمداء، تمّ عملها في فايرفوكس:

selection-firefox.png

تدعم المتصفّحات اﻷخرى تحديد مدى واحدا على اﻷكثر. كما سنرى، تلمّح بعض توابع Selection أنّه من الممكن أن يكون هناك عدّة أمداء، لكن ثانية، في جميع المتصفّحات ما عدا فايرفوكس، هناك واحد فقط على اﻷكثر.

خاصيات التحديد

بشكل مشابه للمدى، يملك التحديد بداية، تُسمّى "المرساة (anchor)"، ونهاية تُسمّى "التركيز (focus)".

خاصّيّات التحديد الرئيسيّة هي:

  • anchorNode -- العقدة التي يبدأ فيها التحديد
  • anchorOffset -- الانحراف الذي يبدأ منه التحديد في anchorNode
  • focusNode -- العقدة التي ينتهي فيها التحديد
  • focusOffset -- الانحراف الذي ينتهي عنده التحديد في focusNode
  • isCollapsed --‏ true إذا لم يكن التحديد يحدّد أيّ شيء (مدى فارغ)، أو لم يكن موجودا أصلا
  • rangeCount -- عدد اﻷمداء في التحديد، 1 على اﻷكثر في جميع المتصفّحات باستثناء فايرفوكس

ملاحظة: قد تكون نهاية التحديد في المستند قبل البداية

هناك عدة طرق لتحديد المحتوى، حسب وكيل المستخدم (user agent): الفأرة والمفاتيح المختصرة والضغط على الهاتف، إلى غير ذلك. يمكّن بعضها، مثل الفأرة، من عمل نفس التحديد في الاتجاهين: "من اليمين إلى اليسار" و"من اليسار إلى اليمين".

إذا كانت البداية (المرساة) تأتي في المستند قبل النهاية (التركيز)، فيقال أنّ اتجاه هذا التحديد "إلى اﻷمام".

إذا بدأ المستخدم مثلا التحديد بالفأرة وذهب من "Example" إلى "italic":

selection-direction-forward.png

على خلاف ذلك، إذا ذهب من نهاية "italic" إلى "Example"، فإنّ التحديد متّجه "إلى الخلف"، ويكون التركيز فيه قبل المرساة:

selection-direction-backward.png

هذا مختلف عن كائنات Range التي تتجه دائما إلى اﻷمام: لا يمكن أن تكون بداية المدى قبل نهايته.


أحداث التحديد

هناك أحداث تمكّننا من متابعة التحديد:

  • elem.onselectstart -- عندما يبدأ التحديد في elem، كأن يبدأ المستخدم مثلا بتحريك الفأرة والزر مضغوط.
    • يؤدي منع الفعل الافتراضيّ إلى عدم ابتداء التحديد.
  • document.onselectionchange -- كلّما تغيّر التحديد.
    • يُرجى التنبّه: يمكن أن يُسند هذا المعالج إلى document فقط.

مثال لتتبع التحديد

إليك مثالا صغيرا يظهر حدود التحديد ديناميكيّا حال تغيّرها:

<p id="p">Select me: <i>italic</i> and <b>bold</b></p>

From <input id="from" disabled> – To <input id="to" disabled>
<script>
  document.onselectionchange = function() {
    let {anchorNode, anchorOffset, focusNode, focusOffset} = document.getSelection();

    from.value = `${anchorNode && anchorNode.data}:${anchorOffset}`;
    to.value = `${focusNode && focusNode.data}:${focusOffset}`;
  };
</script>

مثال للحصول على التحديد

للحصول على كامل التحديد:

  • على شكل نص: استدعي فقط document.getSelection().toString()‎.
  • على شكل عقد DOM: احصل على الأمداء المنشأة واستدعي توابع cloneContents()‎ الخاصّة بها (المدى الأوّل فقط إذا لم نكن ندعم التحديد المتعدّد لفايرفوكس).

هذا مثال للحصول على التحديد سواء على شكل نصّ أو عقد DOM:

<p id="p">Select me: <i>italic</i> and <b>bold</b></p>

Cloned: <span id="cloned"></span>
<br>
As text: <span id="astext"></span>

<script>
  document.onselectionchange = function() {
    let selection = document.getSelection();

    cloned.innerHTML = astext.innerHTML = "";

    // من اﻷمداء (ندعم التحديد المتعدّد هنا)‏ DOM استنسخ عقد
    for (let i = 0; i < selection.rangeCount; i++) {
      cloned.append(selection.getRangeAt(i).cloneContents());
    }

    // على شكل نصّ
    astext.innerHTML += selection;
  };
</script>

توابع التحديد

توابع التحديد لإضافة/إزالة اﻷمداء:

  • getRangeAt(i)‎ -- يحصل على المدى رقم i، ابتداءًا من 0. في جميع المتصفّحات ما عدا فايرفوكس، لا يُستعمل سوى 0.
  • addRange(range)‎ -- يضيف المدى range إلى التحديد. تتجاهل جميع المتصفّحات ما عدا فايرفوكس هذا الاستدعاء إذا كان للتحديد مدى مرفقًا به
  • removeRange(range)‎ -- يزيل المدى range من التحديد.
  • removeAllRanges()‎ -- يزيل جميع الأمداء.
  • empty()‎ -- اسم بديل لـ removeAllRanges.

زيادة على ذلك، هناك توابع ملائمة للتحكّم في المدى الخاصّ بالتحديد مباشرة، دون Range:

  • collapse(node, offset)‎ - استبدل المدى المُحدَّد بواحد جديد يبدأ وينتهي عند العقدة المعطاة، node، وفي الموضع offset.
  • setPosition(node, offset)‎ -- اسم بديل لـ collapse.
  • collapseToStart()‎ -- يطوي (يستبدل بمدى فارغ) إلى بداية التحديد،
  • collapseToEnd()‎ -- يطوي إلى نهاية التحديد،
  • extend(node, offset)‎ -- ينقل تركيز التحديد إلى العقدة المعطاة node، والموضع offset،
  • setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset)‎ -- يستبدل مدى التحديد بالبداية المعطاة anchorNode/anchorOffset والنهاية focusNode/focusOffset. يُحدّد جميع المحتوى الذي بينها.
  • selectAllChildren(node)‎ -- يحدّد جميع أبناء العقدة node.
  • deleteFromDocument()‎ -- يزيل المحتوى المُحدَّد من المستند.
  • containsNode(node, allowPartialContainment = false)‎ -- يفحص ما إذا كان التحديد يحتوي على العقدة node (جزئيّا إذا كان الوسيط الثاني true).

وبذلك، يمكننا في كثير من المهامّ استدعاء توابع Selection، ولا حاجة للوصول إلى كائن Range المنشأ.

على سبيل المثال، تحديد كامل محتويات الفقرة <p>:

<p id="p">Select me: <i>italic</i> and <b>bold</b></p>

<script>
  // إلى آخر ابن <p> يحدّد من الابن رقم 0 لـ
  document.getSelection().setBaseAndExtent(p, 0, p, p.childNodes.length);
</script>

نفس الشيء باستخدام اﻷمداء:

<p id="p">Select me: <i>italic</i> and <b>bold</b></p>

<script>
  let range = new Range();
  range.selectNodeContents(p); // or selectNode(p) to select the <p> tag too

  document.getSelection().removeAllRanges(); // clear existing selection if any
  document.getSelection().addRange(range);
</script>


ملاحظة: للقيام بالتحديد، أزل التحديد الموجود أوّلا

إذا كان التحديد موجودًا، أفرغه أوّلا باستخدام removeAllRanges()‎. ثمّ أضف اﻷمداء. وإلّا، ستتجاهل جميع المتصفّحات ما عدا فايرفوكس الأمداء الجديدة.

يُستثنى من ذلك بعض توابع التحديد، التي تستبدل التحديد الموجود، مثل setBaseAndExtent.


التحديد في عناصر التحكم بالاستمارات

توفّر عناصر الاستمارات، مثل input وtextareaواجهات برمجيّة خاصّة بالتحديد، دون الكائنات Selection أو Range. بما أنّ قيمة المُدخل هي نصّ صرف، وليس HTML، فلا حاجة هناك لهذه الكائنات، كلّ شيء أبسط بكثير.

الخاصّيّات:

  • input.selectionStart -- موضع بداية التحديد (قابل للكتابة)
  • input.selectionEnd -- موضع نهاية التحديد (قابل للكتابة)
  • input.selectionDirection -- اتجاه التحديد، واحد من: "forward" (اﻷمام) أو "backward" (الخلف) أو "none" (إذا كان التحديد بواسطة النقر المزدوج بالفأرة مثلا)

اﻷحداث:

  • input.onselect -- يقع عندما يُحدّد شيء ما

التوابع:

  • input.select()‎ -- يحدّد كلّ شيء في عنصر التحكّم بالنصّ (قد يكون textarea بدل input)
  • input.setSelectionRange(start, end, [direction])‎ -- يغيّر التحديد ليمتدّ من الموضع start إلى end، في الاتجاه المُعطى (اختياريّ)
  • input.setRangeText(replacement, [start], [end], [selectionMode])‎ -- يستبدل المدى من النصّ بنصّ جديد

تضبط الوسائطُ الاختياريّة start وend، إذا أُعطيت، بداية المدى ونهايته، وإلّا فيُعتمد تحديد المستخدم.

يوضّح الوسيط اﻷخير، selectionMode، كيفيّة ضبط التحديد بعد استبدال النصّ. القيم الممكنة هي:

  • "select" -- يُحدَّد النصّ المُدرج الجديد.
  • "start" -- ينطوي مدى التحديد قبل النصّ المُدرج مباشرة (يكون المؤشّر قبله مباشرة).
  • "end" -- ينطوي مدى التحديد بعد النصّ المُدرج مباشرة (يكون المؤشّر بعده مباشرة).
  • "preserve" -- يحاول المحافظة على التحديد. هذه هي القيمة الافتراضيّة.

لنرى الآن هذه التوابع تعمل.

مثال: تتبع التحديد

على سبيل المثال، تستخدم هذه الشيفرة حدث onselect لتتبّع التحديد:

<textarea id="area" style="width:80%;height:60px">
Selecting in this text updates values below.
</textarea>
<br>
From <input id="from" disabled> – To <input id="to" disabled>

<script>
  area.onselect = function() {
    from.value = area.selectionStart;
    to.value = area.selectionEnd;
  };
</script>

يُرجى التنبّه:

  • يقع onselect عندما يُحدَّد شيء ما، لكن ليس عندما يُزال التحديد.
  • يجب ألّا يقع document.onselectionchange من أجل التحديدات التي بداخل عناصر التحكّم في الاستمارة، وفقا للمواصفة، إذ ليست له علاقة بـتحديد وأمداء المستند document. تقوم بعض المتصفّحات بتوليده، لكن ينبغي ألّا نعتمد عليه.

مثال: تحريك المؤشر

يمكننا تغيير selectionStart وselectionEnd، التي تضبط التحديد.

من الحالات الشاذّة المهمّة هي عندما تكون selectionStart وselectionEnd متساويتان، ويكون ذلك هو موضع المؤشّر بالضبط. أو، بعبارة أخرى، عندما لا يُحدَّد أيّ شيء، فإن التحديد ينطوي عند موضع المؤشّر. فبإعطاء selectionStart وselectionEnd نفس القيمة، فإنّنا نعمل على تحريك المؤشّر.

على سبيل المثال:

<textarea id="area" style="width:80%;height:60px">
Focus on me, the cursor will be at position 10.
</textarea>

<script>
  area.onfocus = () => {
    // بتأخير منعدم للتنفيذ بعد انتهاء فعل “التركيز” من المتصفّح setTimeout
    setTimeout(() => {
      // يمكننا ضبط أيّ تحديد
      // إذا كانت البداية=النهاية، يكون المؤشّر في ذلك الموضع بالذات
      area.selectionStart = area.selectionEnd = 10;
    });
  };
</script>

مثال: تعديل التحديد

لتعديل محتوى التحديد، يمكننا استخدام التابع input.setRangeText()‎. بالطبع، يمكننا قراءة selectionStart/End و، مع معرفتنا بالتحديد، تغيير السلسلة النصّيّة الجزئيّة الموافقة لـ value، لكنّ setRangeText أقوى و أكثر ملائمة في الكثير من اﻷحيان.

هو تابع معقّد بعض الشيء. يقوم في صورته البسيطة ذات وسيط وحيد باستبدال المدى المُحدَّد من طرف المستخدم ويزيل التحديد.

على سبيل المثال، سُيحاط تحديد المستخدم هنا بـ *...*:

<input id="input" style="width:200px" value="Select here and click the button">
<button id="button">Wrap selection in stars *...*</button>

<script>
button.onclick = () => {
  if (input.selectionStart == input.selectionEnd) {
    return; // nothing is selected
  }

  let selected = input.value.slice(input.selectionStart, input.selectionEnd);
  input.setRangeText(`*${selected}*`);
};
</script>

بالمزيد من الوسائط، يمكننا ضبط بداية المدى start ونهايته end.

في هذا المثال نجد أنّ "THIS" في نصّ المُدخل، نستبدله ونبقي البديل مُحدّدا:

<input id="input" style="width:200px" value="Replace THIS in text">
<button id="button">Replace THIS</button>

<script>
button.onclick = () => {
  let pos = input.value.indexOf("THIS");
  if (pos >= 0) {
    input.setRangeText("*THIS*", pos, pos + 4, "select");
    input.focus(); // focus to make selection visible
  }
};
</script>

مثال: الإدراج عند المؤشر

إذا لم يكن هناك شيء مُحدّد، أو استخدمنا start وend متساويتان في setRangeText، فسيُدرج النصّ الجديد فقط، ولن يزال أيّ شيء، ويمكننا أيضا إدراج شيء ما "عند المؤشّر" باستخدام setRangeText.

هذا زرّ يعمل على إدراج "HELLO" عند موضع المؤشّر ويضع المؤشّر بعده مباشرة. إذا لم يكن التحديد فارغا، فإنّه يُستبدل (يمكننا اكتشاف ذلك بالمقارنة selectionStart!=selectionEnd وعمل شيء آخر بدل ذلك):

<input id="input" style="width:200px" value="Text Text Text Text Text">
<button id="button">Insert "HELLO" at cursor</button>

<script>
  button.onclick = () => {
    input.setRangeText("HELLO", input.selectionStart, input.selectionEnd, "end");
    input.focus();
  };    
</script>

جعل الشيء غير قابل للتحديد

لجعل شيء ما غير قابل للتحديد، هناك ثلاث طرق:

الطريقة الأولى

باستخدام الخاصّيّة user-select: none في CSS

    <style>
    #elem {
      user-select: none;
    }
    </style>
    <div>Selectable <div id="elem">Unselectable</div> Selectable</div>

لا يسمح هذا للتحديد بالبدء عند elem لكن قد يبدأ المستخدم التحديد من مكان آخر ويدخل فيه elem.

وبذلك يصير elem جزءًا من document.getSelection()‎، ويكون التحديد قد وقع فعلًا، إلّا أنّ مُحتواه يُتجاهل عادة عند النسخ-اللصق.

الطريقة الثانية

بمنع الفعل الافتراضيّ عند اﻷحداث onselectstart أو mousedown:

    <div>Selectable <div id="elem">Unselectable</div> Selectable</div>

    <script>
      elem.onselectstart = () => false;
    </script>

يمنع هذا بدأ التحديد من elem، لكن الزائر قد يبدأ من عنصر آخر، ثمّ يمدّه إلى elem.

هذا ملائم عندما يكون هناك معالج حدث آخر لنفس الفعل الذي يُحدث التحديد (mousedown مثلا). فيمكننا إذًا تعطيل التحديد لتجنّب التعارض، مع السماح لمحتويات elem أن تُنسخ.

الطريقة الثالثة

يمكننا أيضا مسح التحديد بعد حصوله باستخدام document.getSelection().empty()‎. يندر استخدام هذا، إذ يتسبّب في ومضات غير محبّذة عند ظهور-اختفاء التحديد.

الملخص

تناولنا واجهتين برمجيّتين للتحديدات:

  1. للصفحة: الكائنان Selection وRange.
  2. للمدخلات النصية input وtextarea: توابع وخاصّيّات إضافيّة.

الواجهة البرمجيّة الثانية بسيطة للغاية، إذ تعمل مع النصّ.

قد تكون أكثر الوصفات استخداما هي:

  1. الحصول على التحديد:
    let selection = document.getSelection();

    let cloned = /* العنصر الذي تُستنسخ فيه العقد المُحدَّدة*/;

    // selection.getRangeAt(0) على Range ثمّ طبّق توابع
    // أو، كما هنا، على جميع اﻷمداء لدعم التحديد المتعدّد
    for (let i = 0; i < selection.rangeCount; i++) {
      cloned.append(selection.getRangeAt(i).cloneContents());
    }
  1. ضبط التحديد
    let selection = document.getSelection();

    // :مباشرة
    selection.setBaseAndExtent(...from...to...);

    // أو يمكننا إنشاء مدى و:‏
    selection.removeAllRanges();
    selection.addRange(range);

وفي النهاية، بالنسبة للمؤشّر. يكون موضع المؤشّر في العناصر القابلة للتحرير، مثل <textarea>، دائما عند بداية التحديد أو نهايته. يمكننا استخدام ذلك للحصول على موضع المؤشّر أو لتحريك المؤشّر بضبط elem.selectionStart وelem.selectionEnd.

المراجع

ترجمة -وبتصرف- للمقال Selection and Range من سلسلة Browser: Document, Events, Interfaces لصاحبها Ilya Kantor


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...