استخدِم Invoker Commands API لتبديل مستوى ظهور العنصرَين <dialog> و[popover] مباشرةً من أزرار HTML، ما يلغي الحاجة إلى متتبِّعات أحداث JavaScript المخصّصة.
من خلال تطبيق السمتَين commandfor (رقم تعريف الهدف) وcommand (الإجراء) على <button>، يتعامل المتصفّح تلقائيًا مع تغييرات حالة الفتح/الإغلاق وإدارة التركيز وعمليات ربط تسهيل الاستخدام (مثل aria-expanded). ننصح باتّباع هذا النهج التعريفي لأنّه يزيل الرموز البرمجية النموذجية الهشة، ويضمن عمل التفاعلات فورًا بعد تحليل HTML، ويضمن تجربة مستخدم قوية يمكن الوصول إليها بشكلٍ أصلي.
تنفيذ النوافذ المنبثقة التعريفية
يمكن فتح النوافذ المنبثقة وإغلاقها باستخدام زر واحد.
<!-- MANDATORY: The commandfor attribute links the invoker to the ID of the target element so the browser knows what to control. -->
<!-- MANDATORY: The command attribute specifies the action to perform. Use 'toggle-popover' to handle both open and close states automatically. -->
<button commandfor="my-popover" command="toggle-popover">
Toggle Popover
</button>
<!-- MANDATORY: The target element must have the popover attribute to be controlled as a popover. -->
<div id="my-popover" popover>
<p>Popover content goes here.</p>
</div>
إذا أردت التحكّم في الفتح والإغلاق باستخدام زرَّين منفصلَين، يمكنك استخدام الأمرَين show-popover وhide-popover.
<!-- MANDATORY: Use 'show-popover' to explicitly open the popover. It will not close the popover if clicked again. -->
<button commandfor="my-explicit-popover" command="show-popover">
Show Popover
</button>
<div id="my-explicit-popover" popover="manual">
<p>This popover is explicitly opened and closed by separate buttons.</p>
<!-- MANDATORY: Use 'hide-popover' to explicitly close the targeted popover. -->
<button commandfor="my-explicit-popover" command="hide-popover">
Hide Popover
</button>
</div>
تنفيذ مربّعات الحوار المشروطة التعريفية
على عكس النوافذ المنبثقة، تستخدم مربّعات الحوار المشروطة عادةً أزرارًا منفصلة للفتح والإغلاق. استخدِم الأمر show-modal تحديدًا عندما تحتاج إلى فتح مربّع حوار كنموذج.
<!-- MANDATORY: Use command="show-modal" to trigger the dialog as a modal, trapping focus and preventing interaction with the rest of the page. -->
<!-- MANDATORY: The commandfor attribute connects this button to the dialog ID. -->
<button commandfor="confirm-dialog" command="show-modal">
Open Confirmation
</button>
<dialog id="confirm-dialog">
<p>Are you sure you want to proceed?</p>
<!-- MANDATORY: Use command="close" to dismiss the dialog safely. -->
<button commandfor="confirm-dialog" command="close">
Cancel
</button>
</dialog>
استراتيجيات احتياطية
بما أنّ "أوامر المُشغِّل" (Invoker Commands) و"النوافذ المنبثقة" (Popovers) غير متوافقة مع جميع المتصفّحات بعد، يجب استخدام polyfills كبدائل للمتصفّحات القديمة.
أوامر Invoker في Polyfill
MANDATORY: Feature detect support by checking for the commandForElement property on the HTMLButtonElement prototype. لا تتحقّق من عنصر النافذة أو المستند. يجب استيراد الرمز البرمجي polyfill ديناميكيًا فقط في حال عدم توفّر الميزة الأصلية. لا تحمِّل الـ polyfill بدون شروط.
مطلوب: الاستماع إلى حدث "command" مباشرةً على العنصر المستهدَف لأنّ حدث "command" الأصلي لا يتصاعد.
الخيار 1: استخدام حزمة
ثبِّت polyfill من خلال npm (npm install invokers-polyfill). هذا النهج مخصّص للمشاريع التي تستخدم حزمة (مثل Vite أو Webpack) أو خرائط الاستيراد. بالنسبة إلى جميع عمليات الإعداد الأخرى، استخدِم خيار شبكة توصيل المحتوى (CDN) أدناه.
// MANDATORY: Feature detect 'commandForElement' on HTMLButtonElement.prototype.
// Conditionally load the invokers-polyfill only in browsers lacking native support.
if (!('commandForElement' in HTMLButtonElement.prototype)) {
import('invokers-polyfill');
}
الخيار 2: استخدام شبكة توصيل المحتوى (CDN)
بالنسبة إلى المشاريع التي لا تتضمّن حزمة، يمكنك استيراد الرمز البرمجي polyfill ديناميكيًا من شبكة توصيل المحتوى (CDN) مباشرةً داخل <script type="module">.
<script type="module">
// MANDATORY: Feature detect 'commandForElement' on HTMLButtonElement.prototype.
// Conditionally load the invokers-polyfill from a CDN only in browsers lacking native support.
if (!('commandForElement' in HTMLButtonElement.prototype)) {
import('https://esm.run/invokers-polyfill');
}
</script>
قيود Invokers Polyfill
ملاحظة إلزامية: لا يتعامل هذا الـ polyfill مع حالات ARIA (مثل aria-expanded) لزر الأمر بالطريقة التي تتعامل بها المتصفحات الأصلية. ننصحك بشدة بالتعامل مع هذه الحالات بنفسك لضمان إمكانية الوصول إلى موقعك بالكامل.
إذا لم تكن واجهة Invoker Commands API متاحة، لن يتم تنشيط الحدث command. للحصول على دعم كامل في جميع المتصفّحات الحديثة، ننصحك باستخدام invokers-polyfill من https://github.com/keithamus/invokers-polyfill عبر npm install أو شبكة توصيل المحتوى (CDN).
يتيح هذا الرمز البرمجي استخدام الإجراءات المخصّصة بالكامل (بدءًا من --) ويرسل الحدث command تمامًا مثل واجهة برمجة التطبيقات الأصلية.
الاستيراد الديناميكي (تحسين الأداء)
للحصول على أفضل أداء، يجب تحميل polyfill فقط إذا كان المتصفّح لا يتيح استخدام واجهة برمجة التطبيقات بشكلٍ تلقائي. يؤدي ذلك إلى توفير معدل نقل البيانات وتقليل وقت تنفيذ النصوص البرمجية للمستخدمين على المتصفحات الحديثة.
ملاحظة: لا يدير هذا الرمز polyfill حالات ARIA (مثل aria-pressed أو aria-expanded) للأوامر المخصّصة. يجب مزامنة هذه الحالات يدويًا في متتبِّع الأحداث لضمان إمكانية الوصول إلى موقعك الإلكتروني.
// 1. Conditionally load the polyfill
const hasNativeSupport = 'commandForElement' in HTMLButtonElement.prototype;
if (!hasNativeSupport) {
// Wrap in an async IIFE to avoid top-level await issues in older browsers
(async () => {
try {
await import('https://esm.run/invokers-polyfill');
} catch (err) {
console.error('Error loading fallback:', err);
}
})();
}
// 2. Manually manage ARIA states in your listener
document.getElementById('action-target').addEventListener('command', (event) => {
const command = event.command;
const target = event.target;
const source = event.source; // The button that triggered the command
if (command === '--spin') {
const isSpun = target.classList.toggle('is-spun');
// Polyfill tip: Manually update ARIA to match the new state
source?.setAttribute('aria-pressed', isSpun);
}
});
الإجراء الاحتياطي اليدوي (النمط التقليدي)
إذا كنت تفضّل عدم استخدام polyfill، يمكنك استخدام مزيج من تفويض الأحداث لإرسال الأحداث وسجلّ الأوامر لمعالجة الإجراءات. هذا نمط معماري شائع في تطوير JavaScript التقليدي، ولا يزال يتسم بالكفاءة العالية وقابلية التوسّع.
// 1. **Optional:** Define a registry of requested actions for cleaner logic
const commandRegistry = {
'--spin': (target) => target.classList.toggle('is-spun'),
'--grow': (target) => target.classList.toggle('is-grown'),
'--reset': (target) => target.classList.remove('is-spun', 'is-grown'),
};
// 2. If CommandEvent doesn't exist, we assume no native support and provide the fallback
if (!globalThis.CommandEvent) {
globalThis.CommandEvent = class CommandEvent extends Event {
constructor(type, { source, command, ...options } = {}) {
super(type, options);
this.source = source;
this.command = command;
}
}
}
// 3. The fallback: Dispatch events manually if native support is missing
document.addEventListener('click', (event) => {
const button = event.composedPath().find((el) => el.matches?.("button[commandfor]"));
if (!button) return;
const target = document.getElementById(button.getAttribute('commandfor'));
const command = button.getAttribute('command');
if (target && command) {
target.dispatchEvent(new CommandEvent('command', {
command,
source: button,
}));
}
});
// 4. **Mandatory:** Register the unified listener directly on the target element
document.getElementById('action-target').addEventListener('command', (event) => {
const command = event.command;
const target = event.target;
const action = commandRegistry[command];
if (action) {
action(target);
}
});
الخيارات الاحتياطية وتوافق المتصفّحات مع Popover
تعتمد واجهة Popover API في الغالب على التحسين التدريجي، ولكن سلوكياتها المحدّدة، مثل الترقية إلى الطبقة العليا والإغلاق البسيط واستدعاء popovertarget، ليس لها مكافئ في CSS فقط. تحتاج المتصفّحات القديمة إلى polyfill أو إلى إجراء احتياطي يدوي إذا كنت تفضّل عدم شحنها.
Polyfill: لإتاحة السمة popover في المتصفّحات القديمة، يمكنك تحميل @oddbird/popover-polyfill بشكل مشروط. إجراء إلزامي: يجب رصد الميزة من خلال التحقّق من السمة popover في HTMLElement.prototype، وتحميل polyfill فقط في حال عدم توفّر الدعم الأصلي، ويجب عدم تحميله بدون شروط.
باستخدام حزمة أو خريطة استيراد:
// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
import("@oddbird/popover-polyfill");
}
بدون حزمة، يمكنك الاستيراد من شبكة توصيل محتوى (CDN) داخل <script type="module">:
<script type="module">
if (!("popover" in HTMLElement.prototype)) {
import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
}
</script>
تحذير بشأن التنسيق: لا يمكن لـ polyfill تحديد الفئة الزائفة :popover-open الحقيقية، لذا يتم تطبيق فئة .\:popover-open بدلاً من ذلك. إجراء إلزامي: ادمج السمتَين مع :is() أو :where()، وإلا ستتجاهل المتصفحات التي لا تتضمّن :popover-open القاعدة بأكملها:
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
بدلاً من ذلك، يمكنك استخدام position: fixed واحتساب الإحداثيات يدويًا من خلال getBoundingClientRect() أو الاعتماد على تحديد الموضع التلقائي باستخدام inset: auto إذا كان ذلك مقبولاً لحالة الاستخدام، وذلك كحل بديل قديم بدون polyfill.