Skip to content

Commit

Permalink
init onkeystroke
Browse files Browse the repository at this point in the history
  • Loading branch information
huntabyte committed Dec 22, 2024
1 parent b61eaf2 commit 6b343a9
Show file tree
Hide file tree
Showing 6 changed files with 501 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/runed/src/lib/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ export * from "./useGeolocation/index.js";
export * from "./Context/index.js";
export * from "./IsInViewport/index.js";
export * from "./useActiveElement/index.js";
export * from "./onKeyStroke/index.js";
1 change: 1 addition & 0 deletions packages/runed/src/lib/utilities/onKeyStroke/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./onKeyStroke.svelte.js";
211 changes: 211 additions & 0 deletions packages/runed/src/lib/utilities/onKeyStroke/onKeyStroke.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { defaultDocument } from "$lib/internal/configurable-globals.js";
import type { MaybeGetter } from "$lib/internal/types.js";
import { addEventListener } from "$lib/internal/utils/event.js";
import { noop } from "$lib/internal/utils/function.js";
import { extract } from "../extract/extract.svelte.js";
import { watch } from "../watch/watch.svelte.js";

export type KeyPredicate = (event: KeyboardEvent) => boolean;
export type KeyFilter = true | string | string[] | KeyPredicate;
export type KeyStrokeEventName = "keydown" | "keypress" | "keyup";

export type OnKeyStrokeOptions = {
/**
* The event name to listen to for key strokes.
*
* @default 'keydown'
*/
eventName?: KeyStrokeEventName;
/**
* The target element to listen for key strokes on.
*
* @default document
*/
target?: MaybeGetter<EventTarget | null | undefined>;

/**
* Whether the key stroke handler is passive or not.
*
* @default false
*/
passive?: boolean;

/**
* Whether to ignore repeated events when the key is held down.
*
* @default false
*/
ignoreRepeat?: boolean;

/**
* Whether the key stroke handler is enabled by default or not.
* If set to false, the handler will not be active until enabled by
* calling the returned `start` function.
*
* @default true
*/
immediate?: boolean;
};

export type OnKeyStrokeReturn = {
/** Start listening for key strokes */
start: () => void;
/** Stop listening for keystrokes */
stop: () => void;
/** Whether the keystroke listeners are enabled or not. */
readonly enabled: boolean;
};

/**
* Listen for key strokes.
*
* @see {@link https://runed.dev/docs/utilities/on-key-stroke}
*/
export function onKeyStroke(
key: KeyFilter,
handler: (event: KeyboardEvent) => void,
options?: OnKeyStrokeOptions
): OnKeyStrokeReturn;
export function onKeyStroke(
handler: (event: KeyboardEvent) => void,
options?: OnKeyStrokeOptions
): OnKeyStrokeReturn;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function onKeyStroke(...args: any[]) {
const { key, handler, options } = parseKeyStrokeArgs(...args);

const {
eventName = "keydown",
passive = false,
immediate = true,
ignoreRepeat = false,
} = options;

const target = $derived(extract(options.target) ?? defaultDocument);
let enabled = $state(immediate);

const predicate = createKeyPredicate(key);
const handleKeyStroke = (e: KeyboardEvent) => {
if (e.repeat && ignoreRepeat) return;

if (predicate(e)) {
handler(e);
}
};

let removeListener = noop;

function start() {
enabled = true;
removeListener();
if (!target) return;
// @ts-expect-error - We know the event names are valid
removeListener = addEventListener(target, eventName, handleKeyStroke, passive);
}

function stop() {
enabled = false;
removeListener();
}

watch(
() => enabled,
(isEnabled) => {
if (isEnabled) {
start();
} else {
stop();
}
}
);

$effect(() => {
return () => {
stop();
};
});

return {
start,
stop,
get enabled() {
return enabled;
},
};
}

/**
* Listen to the `'keydown'` event of the given key(s).
*
* @see {@link https://runed.dev/docs/utilities/on-key-stroke}
*/
export function onKeyDown(
key: KeyFilter,
handler: (event: KeyboardEvent) => void,
options: Omit<OnKeyStrokeOptions, "eventName"> = {}
) {
return onKeyStroke(key, handler, { ...options, eventName: "keydown" });
}

/**
* Listen to the `'keypress'` event of the given key(s).
*
* @see {@link https://runed.dev/docs/utilities/on-key-stroke}
*/
export function onKeyPress(
key: KeyFilter,
handler: (event: KeyboardEvent) => void,
options: Omit<OnKeyStrokeOptions, "eventName"> = {}
) {
return onKeyStroke(key, handler, { ...options, eventName: "keypress" });
}

/**
* Listen to the `'keyup'` event of the given key(s).
* @see {@link https://runed.dev/docs/utilities/on-key-stroke}
*/
export function onKeyUp(
key: KeyFilter,
handler: (event: KeyboardEvent) => void,
options: Omit<OnKeyStrokeOptions, "eventName"> = {}
) {
return onKeyStroke(key, handler, { ...options, eventName: "keyup" });
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function parseKeyStrokeArgs(...args: any[]) {
let key: KeyFilter;
let handler: (event: KeyboardEvent) => void;
let options: OnKeyStrokeOptions = {};

if (args.length === 3) {
key = args[0];
handler = args[1];
options = args[2];
} else if (args.length === 2) {
if (typeof args[1] === "object") {
key = true;
handler = args[0];
options = args[1];
} else {
key = args[0];
handler = args[1];
}
} else {
key = true;
handler = args[0];
}

return { key, handler, options };
}

function createKeyPredicate(keyFilter: KeyFilter): KeyPredicate {
if (typeof keyFilter === "function") {
return keyFilter;
} else if (typeof keyFilter === "string") {
return (event: KeyboardEvent) => event.key === keyFilter;
} else if (Array.isArray(keyFilter)) {
return (event: KeyboardEvent) => keyFilter.includes(event.key);
}
return () => true;
}
Loading

0 comments on commit 6b343a9

Please sign in to comment.