summaryrefslogtreecommitdiffstatshomepage
path: root/frontend/src/lib/Actions.ts
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/lib/Actions.ts')
-rw-r--r--frontend/src/lib/Actions.ts109
1 files changed, 109 insertions, 0 deletions
diff --git a/frontend/src/lib/Actions.ts b/frontend/src/lib/Actions.ts
new file mode 100644
index 0000000..7231c2f
--- /dev/null
+++ b/frontend/src/lib/Actions.ts
@@ -0,0 +1,109 @@
+export function debounce(
+ node: HTMLInputElement,
+ { callback, timeout = 500 }: { callback: () => void; timeout?: number }
+) {
+ let timer: NodeJS.Timeout;
+
+ function trigger(event: KeyboardEvent) {
+ clearTimeout(timer);
+ if (event.key !== 'Enter') {
+ timer = setTimeout(callback, timeout);
+ } else {
+ callback();
+ }
+ }
+
+ node.addEventListener('keyup', trigger);
+
+ return {
+ destroy() {
+ clearTimeout(timer);
+ node.removeEventListener('keyup', trigger);
+ }
+ };
+}
+
+export function clickOutside(
+ node: HTMLElement,
+ { handler, ignore }: { handler: () => void; ignore?: HTMLElement }
+) {
+ const handle = (event: Event) => {
+ const target = event.target as HTMLElement;
+ if (!target || target === ignore) return;
+
+ if (node && !node.contains(target) && !event.defaultPrevented) {
+ handler();
+ }
+ };
+
+ document.addEventListener('click', handle, true);
+
+ return {
+ destroy() {
+ document.removeEventListener('click', handle, true);
+ }
+ };
+}
+
+export const focusableElements = [
+ 'a[href]',
+ 'area[href]',
+ 'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',
+ 'select:not([disabled]):not([aria-hidden])',
+ 'textarea:not([disabled]):not([aria-hidden])',
+ 'button:not([disabled]):not([aria-hidden])',
+ 'iframe',
+ 'object',
+ 'embed',
+ '[contenteditable]',
+ '[tabindex]:not([tabindex^="-"])'
+];
+
+let trapped: HTMLElement[] = [];
+
+export function trapFocus(node: HTMLElement) {
+ function handler(event: KeyboardEvent) {
+ if (event.target === window) return;
+
+ // return if we're not the topmost node to handle
+ if (trapped.at(0) !== node) return;
+
+ const focusable = node.querySelectorAll<HTMLElement>(focusableElements.join());
+
+ const first = focusable[0];
+ const last = focusable[focusable.length - 1];
+
+ if (event.key === 'Tab') {
+ if (!node.contains(document.activeElement)) {
+ first.focus();
+ event.preventDefault();
+ }
+
+ if (event.shiftKey && event.target === first) {
+ last.focus();
+ event.preventDefault();
+ } else if (!event.shiftKey && event.target === last) {
+ first.focus();
+ event.preventDefault();
+ }
+ }
+ }
+
+ if (document.activeElement instanceof HTMLElement) {
+ // if we trap focus, make sure to blur any previously selected external
+ // item such that focus does not remain outside of the node
+ if (!node.contains(document.activeElement)) {
+ document.activeElement.blur();
+ }
+ }
+
+ document.addEventListener('keydown', handler);
+ trapped.unshift(node);
+
+ return {
+ destroy() {
+ document.removeEventListener('keydown', handler);
+ trapped = trapped.filter((i) => i !== node);
+ }
+ };
+}