網頁功能:
Invoker 指令
Invoker Commands API 可讓按鈕使用 HTML 屬性,以宣告方式觸發目標元素上的動作。這種做法可減少手動事件監聽器的需求,並將 UI 與實作詳細資料分離。如果是應用程式專屬的自訂動作,您可以自行定義指令名稱。自訂指令必須以雙破折號 (--) 開頭,以免與日後的內建瀏覽器指令衝突。
導入步驟
- 定義目標元素:找出要回應動作的元素。如果沒有不重複的
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 }新增至事件處理常式,並監聽祖系。 - 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 事件。如要完整支援所有新式瀏覽器,建議透過 npm install 或 CDN,使用 https://github.com/keithamus/invokers-polyfill 的 invokers-polyfill。
這個 polyfill 完全支援自訂動作 (從 -- 開始),並會像原生 API 一樣調度 command 事件。
動態匯入 (效能最佳化)
為獲得最佳效能,只有在瀏覽器不支援原生 API 時,才應載入 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);
}
});