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.targetازevent.composedPath()[0]استفاده کنید. - دسترسپذیری: فرمانهای سفارشی معناشناسی ذاتی ندارند و باید هر وضعیتی را بهطور صریح اعمال کنید.
ملاحظات دسترسپذیری
- اگر دکمه محتوای بیشتری را نمایش میدهد، از مشخصه
aria-expandedاستفاده کنید که بهطور پیشفرض رویaria-expanded="false"تنظیم شده است و وقتی محتوای همراه نمایش داده میشود، رویaria-expanded="true"تنظیم میشود - اگر دکمه خودش تعاملی را تسهیل میکند (مثلاً دکمههای مبدل که میتوانند پخش و مکث کنند، دکمههای میزان صدا که میتوانند بیصدا و باصدا کنند)، از مشخصه
aria-pressedاستفاده کنید، وقتی غیرفعال است رویaria-pressed="false"تنظیم کنید، و وقتی فعال است رویaria-pressed="true"تنظیم کنید - اگر دکمه عنصری را با نقش
menu،listbox،tree،grid، یاdialogباز میکند، کنترل باید مشخصهaria-haspopupداشته باشد و مقدار آن روی نقش عنصری که باز میکند تنظیم شده باشد - اگر دکمه هنگام فعالسازی غیرفعال میشود، بهجای استفاده از
disabled، ازaria-disabled="true"استفاده کنید تا فوکوس ازبین نرود
راهبردهای جایگزین
اگر Invoker Commands API پشتیبانی نشود، رویداد command اجرا نخواهد شد. برای پشتیبانی کامل در همه مرورگرهای مدرن، توصیه میشود از invokers-polyfill از https://github.com/keithamus/invokers-polyfill ازطریق npm install یا CDN استفاده کنید.
این شبیهساز مرورگر بهطور کامل از کنشهای سفارشی (با شروع از --) پشتیبانی میکند و رویداد command را دقیقاً مانند API بومی ارسال میکند.
وارد کردن پویا (بهینهسازی عملکرد)
برای بهترین عملکرد، فقط درصورتی باید شبیهساز مرورگر را بار کنید که مرورگر بهطور بومی از میانای برنامهسازی کاربردی پشتیبانی نکند. این کار باعث صرفهجویی در پهنای باند و کاهش زمان اجرای اسکریپت برای کاربران در مرورگرهای جدید میشود.
توجه: این شبیهساز مرورگر وضعیتهای 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);
}
});
جایگزین دستی (الگوی سنتی)
اگر ترجیح میدهید از شبیهساز مرورگر استفاده نکنید، میتوانید از ترکیبی از واگذاری رویداد برای ارسال رویدادها و ثبت فرمان برای مدیریت کنشها استفاده کنید. این یک الگوی معماری رایج در توسعه سنتی جاوا اسکریپت است که همچنان بسیار کارآمد و مقیاسپذیر است.
// 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);
}
});