/** | |
* Listen for keyboard event and trigger `shortcut` {@link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent | CustomEvent } | |
* @example Typical usage | |
* | |
* ```svelte | |
* <script lang="ts"> | |
* import { shortcut, type ShortcutEventDetail } from '@svelte-put/shortcut'; | |
* | |
* let commandPalette = false; | |
* | |
* function onOpenCommandPalette() { | |
* commandPalette = true; | |
* } | |
* function onCloseCommandPalette() { | |
* commandPalette = false; | |
* } | |
* | |
* function doSomethingElse(details: ShortcutEventDetail) { | |
* console.log('Action was placed on:', details.node); | |
* console.log('Trigger:', details.trigger); | |
* } | |
* | |
* function onShortcut(event: CustomEvent<ShortcutEventDetail>) { | |
* if (event.detail.trigger.id === 'do-something-else') { | |
* console.log('Same as doSomethingElse()'); | |
* // be careful here doSomethingElse would have been called too | |
* } | |
* } | |
* </script> | |
* | |
* <svelte:window | |
* use:shortcut={{ | |
* trigger: [ | |
* { | |
* key: 'k', | |
* | |
* // trigger if either ctrl or meta is pressed | |
* modifier: ['ctrl', 'meta'], | |
* | |
* callback: onOpenCommandPalette, | |
* preventDefault: true, | |
* }, | |
* { | |
* key: 'Escape', | |
* | |
* // preferably avoid arrow functions here for better performance | |
* // with arrow functions the action has to be updated more frequently | |
* callback: onCloseCommandPalette, | |
* | |
* enabled: commandPalette, | |
* preventDefault: true, | |
* }, | |
* { | |
* key: 'k', | |
* | |
* // trigger if both ctrl & shift are pressed | |
* modifier: [['ctrl', 'shift']], | |
* id: 'do-something-else', | |
* callback: doSomethingElse, | |
* }, | |
* ], | |
* }} | |
* on:shortcut={onShortcut} | |
* /> | |
* ``` | |
* | |
* | |
* | |
* As with any svelte action, `shortcut` should be use with element and not component. | |
* | |
* ```html | |
* <-- correct usage--> | |
* <div use:intersect /> | |
* | |
* <-- incorrect usage--> | |
* <Component use:intersect/> | |
* ``` | |
* | |
* You can either: | |
* | |
* - pass multiple callbacks to their associated triggers, or | |
* | |
* - pass one single handler to the `on:shortcut` event, in which case you should | |
* provide an ID to each trigger to be able to distinguish what trigger was activated | |
* in the event handler. | |
* | |
* Either way, only use `callback` or `on:shortcut` and not both to | |
* avoid handler duplication. | |
* @param {HTMLElement} node - HTMLElement to add event listener to | |
* @param {import('./public').ShortcutParameter} param - svelte action parameters | |
* @returns {import('./public').ShortcutActionReturn} | |
*/ | |
export function shortcut(node, param) { | |
let { enabled = true, trigger, type = 'keydown' } = param; | |
/** | |
* @param {KeyboardEvent} event | |
*/ | |
function handler(event) { | |
const normalizedTriggers = Array.isArray(trigger) ? trigger : [trigger]; | |
/** @type {Record<import('./public').ShortcutModifier, boolean>} */ | |
const modifiedMap = { | |
alt: event.altKey, | |
ctrl: event.ctrlKey, | |
shift: event.shiftKey, | |
meta: event.metaKey, | |
}; | |
for (const trigger of normalizedTriggers) { | |
const mergedTrigger = { | |
modifier: [], | |
preventDefault: false, | |
enabled: true, | |
...trigger, | |
}; | |
const { modifier, key, callback, preventDefault, enabled: triggerEnabled } = mergedTrigger; | |
if (triggerEnabled) { | |
if (modifier.length) { | |
const modifierDefs = (Array.isArray(modifier) ? modifier : [modifier]).map((def) => | |
typeof def === 'string' ? [def] : def, | |
); | |
const modified = modifierDefs.some((def) => | |
def.every((modifier) => modifiedMap[modifier]), | |
); | |
if (!modified) continue; | |
} | |
if (event.key === key) { | |
if (preventDefault) event.preventDefault(); | |
/** @type {import('./public').ShortcutEventDetail} */ | |
const detail = { | |
node, | |
trigger: mergedTrigger, | |
originalEvent: event, | |
}; | |
node.dispatchEvent(new CustomEvent('shortcut', { detail })); | |
callback?.(detail); | |
} | |
} | |
} | |
} | |
if (enabled) node.addEventListener(type, handler); | |
return { | |
update: (update) => { | |
const { enabled: newEnabled = true, type: newType = 'keydown' } = update; | |
if (enabled && (!newEnabled || type !== newType)) { | |
node.removeEventListener(type, handler); | |
} else if (!enabled && newEnabled) { | |
node.addEventListener(newType, handler); | |
} | |
enabled = newEnabled; | |
type = newType; | |
trigger = update.trigger; | |
}, | |
destroy: () => { | |
node.removeEventListener(type, handler); | |
}, | |
}; | |
} | |