کنش‌های دکمه سفارشی

ویژگی‌های وب: فرمان‌های فراخوان

‫Invoker Commands API به دکمه‌ها اجازه می‌دهد تا کنش‌ها را در عناصر هدف به‌صورت اعلانی بااستفاده از مشخصه‌های HTML راه‌اندازی کنند. این رویکرد نیاز به شنوندگان رویداد دستی را کاهش می‌دهد و واسط کاربر را از جزئیات پیاده‌سازی جدا می‌کند. برای کنش‌های سفارشی مختص برنامه، می‌توانید نام فرمان‌های خودتان را تعریف کنید. فرمان‌های سفارشی باید با خط تیره دوتایی (--) شروع شوند تا با فرمان‌های داخلی مرورگر در آینده تداخل نداشته باشند.

مراحل پیاده‌سازی

  1. عنصر هدف را تعریف کنید: عنصری را که به کنش پاسخ می‌دهد شناسایی کنید. اگر id منحصربه‌فردی ندارد، یکی اضافه کنید.
  2. پیکربندی دکمه فراخوان: از مشخصه commandfor برای اشاره به id هدف و از مشخصه command برای مشخص کردن نام فرمان سفارشی (با پیشوند --) استفاده کنید.
  3. مدیریت رویداد فرمان: یک شنونده رویداد command را مستقیماً به عنصر هدف متصل کنید. شیء رویداد حاوی خصوصیت command و خصوصیت target است (که به عنصر شناسایی‌شده توسط commandfor اشاره دارد).
  4. مدیریت وضعیت‌های 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);
  }
 });