Invoker Commands API ช่วยให้ปุ่มต่างๆ ทริกเกอร์การดำเนินการกับองค์ประกอบเป้าหมายได้โดยใช้แอตทริบิวต์ HTML
แนวทางนี้ช่วยลดความจำเป็นในการใช้ Listener เหตุการณ์ด้วยตนเอง และแยก UI ออกจากรายละเอียดการติดตั้งใช้งาน
สำหรับการดำเนินการที่กำหนดเองและเจาะจงแอปพลิเคชัน คุณสามารถกำหนดชื่อคำสั่งของคุณเองได้ คำสั่งที่กำหนดเองต้องขึ้นต้นด้วยขีดกลาง 2 ขีด (--) เพื่อหลีกเลี่ยงการทับซ้อนกับคำสั่งในตัวของเบราว์เซอร์ในอนาคต
ขั้นตอนการติดตั้งใช้งาน
- กำหนดองค์ประกอบเป้าหมาย: ระบุองค์ประกอบที่จะตอบสนองต่อการดำเนินการ หากไม่มี
idที่ไม่ซ้ำกัน ให้เพิ่ม - กำหนดค่าปุ่มเรียกใช้: ใช้แอตทริบิวต์
commandforเพื่อชี้ไปยังidของเป้าหมาย และใช้แอตทริบิวต์commandเพื่อระบุชื่อคำสั่งที่กำหนดเอง (ขึ้นต้นด้วย--) - จัดการเหตุการณ์คำสั่ง: แนบ Listener เหตุการณ์
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 }ลงในเครื่องจัดการเหตุการณ์และฟังในระดับบนสุด - Shadow Root: หากเป้าหมายอาจอยู่ใน Shadow Root ให้ใช้
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
Polyfill นี้รองรับการกระทำที่กำหนดเองอย่างเต็มรูปแบบ (เริ่มต้นด้วย --) และส่งเหตุการณ์ command เหมือนกับ API แบบเนทีฟทุกประการ
การนำเข้าแบบไดนามิก (การเพิ่มประสิทธิภาพ)
เพื่อประสิทธิภาพที่ดีที่สุด คุณควรโหลด Polyfill เฉพาะในกรณีที่เบราว์เซอร์ไม่รองรับ API ในตัว ซึ่งจะช่วยประหยัดแบนด์วิดท์และลดเวลาในการเรียกใช้สคริปต์สำหรับผู้ใช้ในเบราว์เซอร์ที่ทันสมัย
หมายเหตุ: Polyfill นี้ไม่ได้จัดการสถานะ ARIA (เช่น aria-pressed หรือ aria-expanded) สำหรับคำสั่งที่กำหนดเอง คุณต้องซิงค์สถานะเหล่านี้ด้วยตนเองใน Listener เหตุการณ์เพื่อให้มั่นใจว่าเว็บไซต์ของคุณเข้าถึงได้
// 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);
}
});