{"id":"l-js-4-2","courseId":"javascript-fundamentals","lang":"ar","kind":"lesson","summary":"تستجيب للنقرات وضغطات المفاتيح باستخدام addEventListener، وتقرأ كائن الحدث، وتفهم انتشار الأحداث للأعلى (bubbling)، وتتعامل مع قائمة كاملة بمستمع واحد مفوَّض يظل يعمل مع إضافة العناصر وإزالتها.","html":"<p>يستطيع Pocket أن يرسم قائمته. والآن يريد الناس أن يفعلوا بها أشياء: حذف مصروف، أو تعليم أحدها بأنه رُوجع، أو التصفية حسب الفئة. كل ذلك يبدأ بأن يفعل المستخدم شيئًا، كالنقر أو الكتابة، فيتفاعل كودك. والمتصفّح يُبلغ عن كل ما يفعله المستخدمون على شكل <strong>أحداث</strong> (events).</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">الاستماع إلى حدث</h2>\n<p>تطلب من عنصر أن يُعلمك بحدث ما باستخدام <code dir=\"ltr\">addEventListener</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">app.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> clearButton</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">querySelector</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"#clear\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">clearButton.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addEventListener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"click\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">event</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Clear was clicked\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, event.type);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>اقرأها هكذا: “حين تقع نقرة على هذا الزر، استدعِ هذه الدالة”. دالتك لا تُنفَّذ الآن. بل يخزّنها المتصفّح ويستدعيها لاحقًا، مرة لكل نقرة، ويمرّر إليها <strong>كائن حدث</strong> (event object) فيه تفاصيل ما جرى. هذه فكرة الـ callback من درس الدوال، والمتصفّح هنا هو الكود الذي يستدعيك راجعًا.</p>\n<p>أنواع الأحداث التي ستستخدمها أكثر من غيرها: <code dir=\"ltr\">click</code>، و<code dir=\"ltr\">input</code> (تغيّرت قيمة حقل نصي)، و<code dir=\"ltr\">change</code> (اعتُمدت قيمة، مثلًا في قائمة اختيار)، و<code dir=\"ltr\">submit</code> (أُرسل نموذج)، و<code dir=\"ltr\">keydown</code> (ضُغط مفتاح؛ و<code dir=\"ltr\">event.key</code> تقول أيّ مفتاح، مثل <code dir=\"ltr\">&quot;Enter&quot;</code> أو <code dir=\"ltr\">&quot;Escape&quot;</code>).</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>استدعاء معالج الحدث بدلًا من تمريره</span></p>\n<p><code dir=\"ltr\">button.addEventListener(&quot;click&quot;, clearAll());</code> تنفّذ <code dir=\"ltr\">clearAll</code> مرة واحدة، الآن، أثناء تحميل الصفحة، وتسجّل ما أرجعته، وهو عادةً <code dir=\"ltr\">undefined</code>. ثم لا تفعل النقرات شيئًا. مرّر الدالة، <code dir=\"ltr\">clearAll</code>، أو غلّف الاستدعاء الذي يحتاج إلى وسائط بدالة سهمية: <code dir=\"ltr\">() =&gt; removeExpense(id)</code>.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">البيانات أولًا، ثم العرض</h2>\n<p>أهم عادة مع الأحداث: <strong>لا تعامل الصفحة على أنها المكان الذي تعيش فيه بياناتك</strong>. احتفظ بالبيانات في متغيّر، وغيّر المتغيّر في معالج الحدث، واستدعِ دالة العرض من الدرس السابق لتعيد الرسم:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">app.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> expenses </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  { id: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"exp-1\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, label: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Coffee\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amount: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">450</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  { id: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"exp-2\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, label: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Train\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, amount: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1220</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">];</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">clearButton.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addEventListener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"click\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  expenses </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [];</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  renderExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(expenses);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>معالج الحدث لا يمسّ عناصر <code dir=\"ltr\">&lt;li&gt;</code> مباشرة أبدًا. إنه يغيّر البيانات، ودالة العرض تجعل الصفحة مطابقة لها. ومهما أضفت من أزرار، يبقى هناك مكان واحد يرسم القائمة، فلا يمكن أن يختلف المجموع والقائمة أبدًا.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">الانتشار للأعلى: الأحداث تصعد في الشجرة</h2>\n<p>حين تنقر على زر داخل عنصر قائمة داخل قائمة، فأنت تنقر عليها كلها: الزر، و<code dir=\"ltr\">&lt;li&gt;</code>، و<code dir=\"ltr\">&lt;ul&gt;</code>، و<code dir=\"ltr\">&lt;body&gt;</code>. ويمثّل المتصفّح ذلك بأن يسلّم الحدث أولًا إلى العنصر الذي نقرت عليه، وهو <strong>الهدف</strong> (target)، ثم إلى كل سلف بدوره، صعودًا حتى <code dir=\"ltr\">document</code>. وهذا يُسمّى <strong>الانتشار للأعلى</strong> (bubbling).</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 640 250\" role=\"img\" aria-labelledby=\"l-js-4-2-t1\"><defs><marker id=\"arrow-l-js-4-2-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-js-4-2-t1\">النقرة على زر الحذف تبدأ عند الزر، وهو هدف الحدث، ثم تنتشر صعودًا إلى li، ثم إلى ul حيث يتعامل معها مستمع مفوَّض، ثم تواصل إلى body وdocument.</title>\n  <rect class=\"d-box\" x=\"20\" y=\"16\" width=\"600\" height=\"220\" rx=\"14\"/>\n  <text class=\"d-code\" x=\"40\" y=\"42\">body</text>\n  <rect class=\"d-box-accent\" x=\"50\" y=\"56\" width=\"540\" height=\"164\" rx=\"12\"/>\n  <text class=\"d-code\" x=\"70\" y=\"82\">ul#expense-list   (المستمع هنا)</text>\n  <rect class=\"d-box\" x=\"80\" y=\"98\" width=\"480\" height=\"104\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"100\" y=\"124\">li data-id=\"l-js-4-2-exp-2\"</text>\n  <text class=\"d-label\" x=\"110\" y=\"168\">Train  $12.20</text>\n  <rect class=\"d-box-primary\" x=\"380\" y=\"140\" width=\"150\" height=\"46\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"455\" y=\"168\" text-anchor=\"middle\">button.delete</text>\n  <path class=\"d-arrow\" d=\"M455 140 L455 116 L330 116\" marker-end=\"url(#arrow-l-js-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M300 100 L300 78\" marker-end=\"url(#arrow-l-js-4-2-1)\"/>\n  <text class=\"d-label-muted\" x=\"455\" y=\"230\" text-anchor=\"middle\">الهدف: حيث بدأت النقرة</text>\n</svg><figcaption>النقرة تنتشر من العنصر المنقور صعودًا عبر أسلافه</figcaption></figure>\n<p>داخل معالج الحدث، <code dir=\"ltr\">event.target</code> هو العنصر الذي بدأ فيه الحدث، و<code dir=\"ltr\">event.currentTarget</code> هو العنصر الذي يعمل مستمعه الآن. ولذلك يسمع المستمع الموجود على <code dir=\"ltr\">&lt;ul&gt;</code> النقرات على كل زر بداخلها، ويستطيع مع ذلك أن يعرف أيّ زر كان.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">تفويض الأحداث: مستمع واحد للقائمة كلها</h2>\n<p>لنفترض أن لكل مصروف زر حذف. يمكنك أن تُلحق مستمعًا بكل زر، لكن دالة العرض ترمي هذه الأزرار وتصنع أزرارًا جديدة في كل مرة تتغيّر فيها القائمة، والأزرار الجديدة بلا مستمعين. بدلًا من ذلك، ألحِق مستمعًا <strong>واحدًا</strong> بالقائمة، وهي لا تُستبدل أبدًا، واستنتج ما الذي نُقر عليه:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">app.js</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> list</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">querySelector</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"#expense-list\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">list.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addEventListener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"click\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">event</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> button</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> event.target.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">closest</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"button.delete\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">button) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// the click was somewhere else in the list</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> button.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">closest</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"li\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).dataset.id;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  expenses </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> expenses.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">filter</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">e</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> e.id </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!==</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> id);</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  renderExpenses</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(expenses);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p><code dir=\"ltr\">closest(selector)</code> تبدأ من عنصر وتصعد عبر أسلافه حتى تجد واحدًا يطابق، أو تُرجع <code dir=\"ltr\">null</code>. وهذا يغطّي النقرات على أيقونة داخل الزر، والحارس <code dir=\"ltr\">if (!button)</code> يتجاهل النقرات على باقي العنصر. والمعرّف يأتي من <code dir=\"ltr\">data-id</code> الذي ضبطته أثناء العرض، وتغيير البيانات يتم بـ <code dir=\"ltr\">filter</code>، تمامًا كما في القسم 3.</p>\n<p>يُسمّى هذا النمط <strong>تفويض الأحداث</strong> (event delegation). إنه يعني مستمعين أقل، ويظل يعمل مع العناصر المضافة بعد إعداد المستمع، لأن نقراتها تنتشر إلى القائمة نفسها.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>استخدم أزرارًا حقيقية للأفعال</span></p>\n<p>عنصر <code dir=\"ltr\">&lt;div&gt;</code> قابل للنقر يبدو بالشكل نفسه، لكن لا يمكن الوصول إليه بمفتاح Tab، ولا تعلن عنه قارئات الشاشة (screen readers) على أنه زر. أما <code dir=\"ltr\">&lt;button type=&quot;button&quot;&gt;</code> فيحصل على دعم لوحة المفاتيح وإمكانية الوصول مجانًا، والضغط على Enter أو Space فوقه يُطلق <code dir=\"ltr\">click</code>.</p>\n</aside>\n<p>لإيقاف انتشار النقرة إلى أعلى يمكنك استدعاء <code dir=\"ltr\">event.stopPropagation()</code>، لكنك نادرًا ما ستحتاج إليها، واستخدامها كثيرًا يُعطّل التفويض في مكان آخر. أما <code dir=\"ltr\">event.preventDefault()</code> فمختلفة وأكثر شيوعًا بكثير: إنها تلغي الفعل الافتراضي للمتصفّح، مثل اتباع رابط أو إرسال نموذج. وهذه الثانية هي نجمة الدرس التالي.</p>\n","toc":[{"id":"sec-1","text":"الاستماع إلى حدث","level":2},{"id":"sec-2","text":"البيانات أولًا، ثم العرض","level":2},{"id":"sec-3","text":"الانتشار للأعلى: الأحداث تصعد في الشجرة","level":2},{"id":"sec-4","text":"تفويض الأحداث: مستمع واحد للقائمة كلها","level":2}],"takeaways":["<code dir=\"ltr\">addEventListener(type, handler)</code> تسجّل دالة يستدعيها المتصفّح لاحقًا، مع كائن حدث يصف ما جرى.","مرّر معالج الحدث (handler) نفسه، دون أقواس؛ فكتابة <code dir=\"ltr\">handler()</code> تستدعيه فورًا وتسجّل القيمة التي أرجعها بدلًا منه.","تنتشر الأحداث للأعلى من العنصر الذي نُقر عليه عبر أسلافه، لذلك يسمع المستمع الموجود على عنصر أب النقرات على أبنائه.","مع تفويض الأحداث، يستخدم مستمع واحد على الحاوية <code dir=\"ltr\">event.target.closest(...)</code> ليعرف أيّ عنصر نُقر عليه، بما في ذلك العناصر المضافة لاحقًا.","احتفظ بالبيانات في متغيّرات، وغيّر البيانات داخل معالجات الأحداث، ثم استدعِ دالة العرض؛ ولا تعامل الصفحة أبدًا على أنها مصدر الحقيقة."],"further":[{"title":"Introduction to events (MDN)","url":"https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/Events"},{"title":"EventTarget.addEventListener() (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener"},{"title":"Event bubbling (MDN)","url":"https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/Event_bubbling"}],"quiz":[{"q":"<p>ما الخطأ في <code dir=\"ltr\">button.addEventListener(&quot;click&quot;, deleteExpense());</code>؟</p>","options":[{"text":"يجب أن تُكتب أسماء الأحداث بأحرف كبيرة، مثل “CLICK”.","why":"أنواع الأحداث نصوص بأحرف صغيرة مثل “click” و&quot;keydown&quot;."},{"text":"تُنفَّذ <code dir=\"ltr\">deleteExpense</code> مرة واحدة فورًا، وتُسجَّل القيمة التي أرجعتها (<code dir=\"ltr\">undefined</code> على الأرجح) بدلًا من الدالة.","why":"صحيح. مرّر الدالة نفسها، <code dir=\"ltr\">deleteExpense</code>، أو غلّفها، <code dir=\"ltr\">() =&gt; deleteExpense(id)</code>، كي يستدعيها المتصفّح مع كل نقرة."},{"text":"لا يمكن أن يكون للأزرار مستمعو نقرات؛ هذا للروابط فقط.","why":"يمكن لأي عنصر أن يكون له مستمع نقرات، والأزرار هي العنصر الصحيح للأفعال."}],"answer":1},{"q":"<p>عنصر قائمة يحتوي على <code dir=\"ltr\">&lt;span&gt;Coffee&lt;/span&gt; &lt;button class=&quot;delete&quot;&gt;×&lt;/button&gt;</code>. والمستمع موجود على <code dir=\"ltr\">&lt;ul&gt;</code>. ينقر المستخدم على الزر. ما قيمتا <code dir=\"ltr\">event.target</code> و<code dir=\"ltr\">event.currentTarget</code>؟</p>","options":[{"text":"كلتاهما <code dir=\"ltr\">&lt;ul&gt;</code>.","why":"<code dir=\"ltr\">currentTarget</code> هي <code dir=\"ltr\">&lt;ul&gt;</code>، لكن <code dir=\"ltr\">target</code> هي العنصر الذي نُقر عليه فعلًا."},{"text":"<code dir=\"ltr\">target</code> هي <code dir=\"ltr\">&lt;ul&gt;</code> و<code dir=\"ltr\">currentTarget</code> هي الزر.","why":"العكس هو الصحيح. <code dir=\"ltr\">target</code> حيث بدأ الحدث؛ و<code dir=\"ltr\">currentTarget</code> حيث يوجد المستمع."},{"text":"<code dir=\"ltr\">target</code> هي الزر و<code dir=\"ltr\">currentTarget</code> هي <code dir=\"ltr\">&lt;ul&gt;</code>.","why":"صحيح. بدأت النقرة على الزر وانتشرت للأعلى حتى <code dir=\"ltr\">&lt;ul&gt;</code>، التي يعمل مستمعها الآن."}],"answer":2},{"q":"<p>لماذا يظل المستمع المفوَّض يعمل مع المصاريف المضافة بعد تحميل الصفحة؟</p>","options":[{"text":"ينسخ المتصفّح المستمعين تلقائيًا إلى كل ابن جديد.","why":"المستمعون لا يُنسخون أبدًا. كل واحد يبقى بالضبط حيث ألحقته."},{"text":"المستمع على الحاوية، وهي لا تتغيّر أبدًا، والنقرات على أي ابن تنتشر للأعلى حتى تصل إليها.","why":"صحيح. العناصر الجديدة داخل الحاوية نفسها، لذلك تصل نقراتها إلى المستمع نفسه."},{"text":"المستمعون المفوَّضون يُنفَّذون مجددًا كلما تغيّر الـ DOM.","why":"المستمعون لا يُنفَّذون إلا حين يقع حدثهم. ولا شيء يُعاد تنفيذه عند تغيّر الـ DOM."}],"answer":1},{"q":"<p>داخل معالج نقرات مفوَّض، لماذا نستخدم <code dir=\"ltr\">event.target.closest(&quot;.delete&quot;)</code> بدلًا من فحص <code dir=\"ltr\">event.target.classList.contains(&quot;delete&quot;)</code>؟</p>","options":[{"text":"<code dir=\"ltr\">closest</code> أسرع من <code dir=\"ltr\">classList</code>.","why":"السرعة ليست المشكلة؛ فكلتاهما فورية. المشكلة في العنصر الذي تقع عليه النقرة."},{"text":"<code dir=\"ltr\">classList</code> لا تعمل مع العناصر الموجودة داخل قائمة.","why":"<code dir=\"ltr\">classList</code> تعمل مع كل عنصر. المشكلة أن الهدف قد يكون ابنًا للزر."},{"text":"قد تقع النقرة على عنصر داخل الزر، مثل أيقونة، و<code dir=\"ltr\">closest</code> تصعد لتجد الزر رغم ذلك.","why":"صحيح. <code dir=\"ltr\">closest</code> تفحص العنصر نفسه ثم كل سلف له، وتُرجع أول تطابق أو <code dir=\"ltr\">null</code>."}],"answer":2}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<h1>Pocket</h1>\n<button type=\"button\" id=\"add-sample\">Add a sample expense</button>\n<ul id=\"expense-list\"></ul>\n<p id=\"total\"></p>\n","css":"li { display: flex; gap: 8px; align-items: center; }\n","js":"let expenses = [\n  { id: \"exp-1\", label: \"Coffee\", amount: 450 },\n  { id: \"exp-2\", label: \"Train\", amount: 1220 },\n  { id: \"exp-3\", label: \"Lunch\", amount: 1350 },\n];\nlet nextNumber = 4;\n\nconst list = document.querySelector(\"#expense-list\");\nconst totalEl = document.querySelector(\"#total\");\nconst formatMoney = (cents) => `$${(cents / 100).toFixed(2)}`;\n\nfunction renderExpenses() {\n  list.replaceChildren();\n  for (const expense of expenses) {\n    const li = document.createElement(\"li\");\n    li.dataset.id = expense.id;\n    const label = document.createElement(\"span\");\n    label.textContent = `${expense.label}: ${formatMoney(expense.amount)}`;\n    const button = document.createElement(\"button\");\n    button.type = \"button\";\n    button.className = \"delete\";\n    button.textContent = \"Delete\";\n    li.append(label, button);\n    list.append(li);\n  }\n  const total = expenses.reduce((sum, e) => sum + e.amount, 0);\n  totalEl.textContent = `Total: ${formatMoney(total)}`;\n}\n\ndocument.querySelector(\"#add-sample\").addEventListener(\"click\", () => {\n  expenses = [...expenses, { id: `exp-${nextNumber}`, label: `Sample ${nextNumber}`, amount: 100 }];\n  nextNumber++;\n  renderExpenses();\n});\n\nrenderExpenses();\n\n// BUG: these listeners are lost every time renderExpenses rebuilds the list.\n// Replace this block with ONE delegated listener on the list.\nfor (const button of document.querySelectorAll(\".delete\")) {\n  button.addEventListener(\"click\", () => {\n    const id = button.closest(\"li\").dataset.id;\n    expenses = expenses.filter((e) => e.id !== id);\n    renderExpenses();\n  });\n}\n"},"solution":{"html":"<h1>Pocket</h1>\n<button type=\"button\" id=\"add-sample\">Add a sample expense</button>\n<ul id=\"expense-list\"></ul>\n<p id=\"total\"></p>\n","css":"li { display: flex; gap: 8px; align-items: center; }\n","js":"let expenses = [\n  { id: \"exp-1\", label: \"Coffee\", amount: 450 },\n  { id: \"exp-2\", label: \"Train\", amount: 1220 },\n  { id: \"exp-3\", label: \"Lunch\", amount: 1350 },\n];\nlet nextNumber = 4;\n\nconst list = document.querySelector(\"#expense-list\");\nconst totalEl = document.querySelector(\"#total\");\nconst formatMoney = (cents) => `$${(cents / 100).toFixed(2)}`;\n\nfunction renderExpenses() {\n  list.replaceChildren();\n  for (const expense of expenses) {\n    const li = document.createElement(\"li\");\n    li.dataset.id = expense.id;\n    const label = document.createElement(\"span\");\n    label.textContent = `${expense.label}: ${formatMoney(expense.amount)}`;\n    const button = document.createElement(\"button\");\n    button.type = \"button\";\n    button.className = \"delete\";\n    button.textContent = \"Delete\";\n    li.append(label, button);\n    list.append(li);\n  }\n  const total = expenses.reduce((sum, e) => sum + e.amount, 0);\n  totalEl.textContent = `Total: ${formatMoney(total)}`;\n}\n\ndocument.querySelector(\"#add-sample\").addEventListener(\"click\", () => {\n  expenses = [...expenses, { id: `exp-${nextNumber}`, label: `Sample ${nextNumber}`, amount: 100 }];\n  nextNumber++;\n  renderExpenses();\n});\n\nrenderExpenses();\n\nlist.addEventListener(\"click\", (event) => {\n  const button = event.target.closest(\".delete\");\n  if (!button) {\n    return;\n  }\n  const id = button.closest(\"li\").dataset.id;\n  expenses = expenses.filter((e) => e.id !== id);\n  renderExpenses();\n});\n"},"checks":[{"id":"first-delete","test":"await click($('li[data-id=\"exp-2\"] .delete'));\nreturn $$('#expense-list li').length === 2 && !$('li[data-id=\"exp-2\"]') && text('#total') === 'Total: $18.00';\n"},{"id":"second-delete","test":"await click($('li[data-id=\"exp-1\"] .delete'));\nreturn $$('#expense-list li').length === 1 && !$('li[data-id=\"exp-1\"]') && text('#total') === 'Total: $13.50';\n"},{"id":"new-items","test":"await click($('#add-sample'));\nconst added = $('li[data-id=\"exp-4\"] .delete');\nif (!added) return false;\nawait click(added);\nreturn !$('li[data-id=\"exp-4\"]') && $$('#expense-list li').length === 1;\n"},{"id":"ignore-other-clicks","test":"await click($('#expense-list li span'));\nreturn $$('#expense-list li').length === 1;\n"},{"id":"data-updated","test":"return expenses.length === 1 && expenses[0].id === 'exp-3';\n"}],"title":"احذف بمستمع مفوَّض واحد","prompt":"<p>لكل مصروف زر حذف (Delete). يُلحق كود البداية مستمعًا بكل زر، لكن <code dir=\"ltr\">renderExpenses</code> تستبدل الأزرار بعد أول حذف، فلا يفعل الحذف الثاني شيئًا، ولا يمكن أبدًا حذف المصاريف الجديدة.</p>\n<p>استبدل الحلقة في الأسفل بمستمع نقرات <strong>واحد</strong> على <code dir=\"ltr\">#expense-list</code>. حين تقع نقرة على زر <code dir=\"ltr\">.delete</code> (أو داخله)، أزِل ذلك المصروف من <code dir=\"ltr\">expenses</code> مستخدمًا <code dir=\"ltr\">data-id</code> الخاص بالعنصر، ثم استدعِ <code dir=\"ltr\">renderExpenses()</code>. ويجب ألّا تفعل النقرات في أي مكان آخر من القائمة شيئًا.</p>\n","hints":["ألحِقه بالقائمة، فهي لا تُستبدل أبدًا: <code dir=\"ltr\">list.addEventListener(&quot;click&quot;, (event) =&gt; { … });</code>.","<code dir=\"ltr\">event.target.closest(&quot;.delete&quot;)</code> تعطيك الزر، أو <code dir=\"ltr\">null</code> إن لم تكن النقرة على زر. اخرج مبكرًا حين تكون <code dir=\"ltr\">null</code>.","المعرّف موجود على <code dir=\"ltr\">&lt;li&gt;</code>: <code dir=\"ltr\">button.closest(&quot;li&quot;).dataset.id</code>. ثم <code dir=\"ltr\">expenses = expenses.filter((e) =&gt; e.id !== id);</code> و<code dir=\"ltr\">renderExpenses();</code>."],"explanation":"<p>عنصر القائمة يُنشئه HTML مرة واحدة ولا يُستبدل أبدًا، لذلك ينجو المستمع الموجود عليه من كل عملية عرض. والنقرات على أي زر حذف تنتشر إليه للأعلى، بما في ذلك أزرار المصاريف التي لم تكن موجودة حين أُضيف المستمع. وتجد <code dir=\"ltr\">closest(&quot;.delete&quot;)</code> الزر حتى لو وقعت النقرة على شيء بداخله، وتُرجع <code dir=\"ltr\">null</code> للنقرات على التسمية، فيتجاهلها الـ <code dir=\"ltr\">return</code> المبكر. ومعالج الحدث لا يغيّر إلا البيانات ثم يعيد العرض، فتبقى القائمة والمجموع متّفقين دائمًا مع <code dir=\"ltr\">expenses</code>.</p>\n","checkLabels":{"first-delete":"حذف مصروف يُزيله ويحدّث المجموع","second-delete":"الحذف الثاني ما زال يعمل بعد إعادة عرض القائمة","new-items":"يمكن حذف المصاريف المضافة لاحقًا أيضًا","ignore-other-clicks":"النقر على تسمية العنصر لا يحذف شيئًا","data-updated":"تُحدَّث مصفوفة المصاريف نفسها، لا الصفحة فقط"}},"words":578,"readMinutes":3}