تتيح واجهة Invoker Commands API للأزرار تفعيل الإجراءات على العناصر المستهدَفة بشكل تعريفي باستخدام سمات HTML.
يقلّل هذا النهج من الحاجة إلى متتبّعات الأحداث اليدوية ويفصل واجهة المستخدم عن تفاصيل التنفيذ.
بالنسبة إلى الإجراءات المخصّصة الخاصة بالتطبيق، يمكنك تحديد أسماء الأوامر الخاصة بك. يجب أن تبدأ الأوامر المخصّصة بشرطتين (--) لتجنُّب التعارض مع الأوامر المضمّنة في المتصفّح في المستقبل.
خطوات التنفيذ
- تحديد العنصر المستهدَف: حدِّد العنصر الذي سيستجيب للإجراء. إذا لم يكن لديه سمة
idفريدة، أضِف واحدة. - ضبط زر الاستدعاء: استخدِم السمة
commandforللإشارة إلىidالهدف، والسمةcommandلتحديد اسم الأمر المخصّص (مسبوقًا بـ--). - معالجة حدث الأمر: يمكنك إرفاق متتبِّع أحداث
commandمباشرةً بالعنصر المستهدَف. يحتوي عنصر الحدث على السمةcommandوالسمةtarget(تشير إلى العنصر الذي تم تحديده من خلالcommandfor). - التعامل مع حالات aria: لا تتضمّن الأوامر المخصّصة دلالات متأصلة، وعليك التعامل مع حالات مثل
aria-pressedأوaria-expanded.
مثال: عناصر التحكّم في الصور المتحركة المخصّصة
<!-- The target element that will respond to custom commands -->
<div id="action-target" class="target">
Action Target
</div>
<!-- Buttons declaratively linked to the target element -->
<!-- Each button sends a unique custom command starting with '--' -->
<button commandfor="action-target" command="--spin">
Spin
</button>
<button commandfor="action-target" command="--grow">
Grow
</button>
<button commandfor="action-target" command="--reset">
Reset All
</button>
<script>
// 1. **Optional:** Define a registry of requested actions for cleaner logic
const commandRegistry = {
'--spin': (target, source) => {
const isSpun = target.classList.toggle('is-spun');
// Set ARIA states, as custom commands have no inherent semantics.
source?.setAttribute('aria-pressed', isSpun);
},
'--grow': (target, source) => {
const isGrown = target.classList.toggle('is-grown');
source?.setAttribute('aria-pressed', isGrown);
},
'--reset': (target) => {
target.classList.remove('is-spun', 'is-grown');
// Reset all associated buttons' ARIA states
document.querySelectorAll(`button[commandfor="${target.id}"]`).forEach(btn => {
btn.setAttribute('aria-pressed', 'false');
});
},
};
// 2. **Mandatory:** Listen for the 'command' event directly on the target element
// (This is necessary because the native 'command' event does not bubble)
document.getElementById('action-target').addEventListener('command', (event) => {
const command = event.command;
const target = event.target;
const source = event.source; // event.source refers to the triggering button
const action = commandRegistry[command];
if (action) {
action(target, source);
}
});
</script>
القيود الرئيسية
- بادئة الأوامر المخصّصة: يجب أن تبدأ جميع أسماء الأوامر المخصّصة بـ
--(مثلاً،command="--my-action"). - الاستهداف: يجب أن تتطابق السمة
commandforمع السمةidلعنصر في شجرة المستند نفسها. - عدم السماح بالفقاعات: لا يسمح الحدث
commandبالفقاعات. إذا كانت هناك عدة أهداف محتملة، أضِف{ capture: true }إلى معالج الأحداث واستمع إلى عنصر رئيسي. - جذور الظل: إذا كان الهدف في جذر ظل، استخدِم
event.composedPath()[0]بدلاً منevent.target. - تسهيل الاستخدام: لا تتضمّن الطلبات المخصّصة دلالات متأصلة، وعليك تطبيق أي حالات بشكل صريح.
اعتبارات تسهيل الاستخدام
- إذا كان الزر يعرض محتوًى إضافيًا، استخدِم السمة
aria-expanded، واضبطها علىaria-expanded="false"تلقائيًا وعلىaria-expanded="true"عند عرض المحتوى المصاحب - إذا كان الزر يسهّل التفاعل نفسه (على سبيل المثال، أزرار الإيقاف/التفعيل التي يمكنها تشغيل المحتوى وإيقافه مؤقتًا، وأزرار التحكّم بمستوى الصوت التي يمكنها كتم الصوت وإعادة الصوت)، استخدِم السمة
aria-pressed، واضبطها علىaria-pressed="false"عندما تكون غير نشطة، وعلىaria-pressed="true"عندما تكون نشطة - إذا كان الزر يفتح عنصرًا له دور
menuأوlistboxأوtreeأوgridأوdialog، يجب أن يتضمّن عنصر التحكّم السمةaria-haspopup، مع ضبط القيمة على دور العنصر الذي يفتحه - إذا تم إيقاف الزر عند التفعيل، استخدِم
aria-disabled="true"بدلاً منdisabledلكي لا يتم فقدان التركيز
استراتيجيات احتياطية
إذا لم تكن واجهة 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);
}
});