mirror of
https://github.com/robonen/tools.git
synced 2026-03-20 02:44:45 +00:00
refactor: change separate tools by category
This commit is contained in:
1
core/platform/README.md
Normal file
1
core/platform/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# @robonen/platform
|
||||
16
core/platform/build.config.ts
Normal file
16
core/platform/build.config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineBuildConfig } from 'unbuild';
|
||||
|
||||
export default defineBuildConfig({
|
||||
entries: [
|
||||
'src/browsers',
|
||||
'src/multi',
|
||||
],
|
||||
clean: true,
|
||||
declaration: true,
|
||||
rollup: {
|
||||
emitCJS: true,
|
||||
esbuild: {
|
||||
// minify: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
7
core/platform/jsr.json
Normal file
7
core/platform/jsr.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://jsr.io/schema/config-file.v1.json",
|
||||
"name": "@robonen/platform",
|
||||
"license": "Apache-2.0",
|
||||
"version": "0.0.2",
|
||||
"exports": "./src/index.ts"
|
||||
}
|
||||
50
core/platform/package.json
Normal file
50
core/platform/package.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "@robonen/platform",
|
||||
"version": "0.0.3",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Platform dependent utilities for javascript development",
|
||||
"keywords": [
|
||||
"javascript",
|
||||
"typescript",
|
||||
"browser",
|
||||
"platform",
|
||||
"node",
|
||||
"bun",
|
||||
"deno"
|
||||
],
|
||||
"author": "Robonen Andrew <robonenandrew@gmail.com>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/robonen/tools.git",
|
||||
"directory": "packages/platform"
|
||||
},
|
||||
"packageManager": "pnpm@10.11.0",
|
||||
"engines": {
|
||||
"node": ">=22.15.1"
|
||||
},
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
"./browsers": {
|
||||
"types": "./dist/browsers.d.ts",
|
||||
"import": "./dist/browsers.mjs",
|
||||
"require": "./dist/browsers.cjs"
|
||||
},
|
||||
"./multi": {
|
||||
"types": "./dist/multi.d.ts",
|
||||
"import": "./dist/multi.mjs",
|
||||
"require": "./dist/multi.cjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"dev": "vitest dev",
|
||||
"build": "unbuild"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@robonen/tsconfig": "workspace:*",
|
||||
"unbuild": "catalog:"
|
||||
}
|
||||
}
|
||||
69
core/platform/src/browsers/focusGuard/index.test.ts
Normal file
69
core/platform/src/browsers/focusGuard/index.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { focusGuard, createGuardAttrs } from '.';
|
||||
|
||||
describe('focusGuard', () => {
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('initialize with the correct default namespace', () => {
|
||||
const guard = focusGuard();
|
||||
|
||||
expect(guard.selector).toBe('data-focus-guard');
|
||||
});
|
||||
|
||||
it('create focus guards in the DOM', () => {
|
||||
const guard = focusGuard();
|
||||
guard.createGuard();
|
||||
|
||||
const guards = document.querySelectorAll(`[${guard.selector}]`);
|
||||
expect(guards.length).toBe(2);
|
||||
|
||||
guards.forEach((element) => {
|
||||
expect(element.tagName).toBe('SPAN');
|
||||
expect(element.getAttribute('tabindex')).toBe('0');
|
||||
});
|
||||
});
|
||||
|
||||
it('remove focus guards from the DOM correctly', () => {
|
||||
const guard = focusGuard();
|
||||
guard.createGuard();
|
||||
guard.removeGuard();
|
||||
|
||||
const guards = document.querySelectorAll(`[${guard.selector}]`);
|
||||
|
||||
expect(guards.length).toBe(0);
|
||||
});
|
||||
|
||||
it('reuse the same guards when calling createGuard multiple times', () => {
|
||||
const guard = focusGuard();
|
||||
guard.createGuard();
|
||||
guard.createGuard();
|
||||
|
||||
guard.removeGuard();
|
||||
const guards = document.querySelectorAll(`[${guard.selector}]`);
|
||||
|
||||
expect(guards.length).toBe(0);
|
||||
});
|
||||
|
||||
it('allow custom namespaces', () => {
|
||||
const namespace = 'custom-guard';
|
||||
const guard = focusGuard(namespace);
|
||||
guard.createGuard();
|
||||
|
||||
expect(guard.selector).toBe(`data-${namespace}`);
|
||||
|
||||
const guards = document.querySelectorAll(`[${guard.selector}]`);
|
||||
expect(guards.length).toBe(2);
|
||||
});
|
||||
|
||||
it('createGuardAttrs should create a valid guard element', () => {
|
||||
const namespace = 'custom-guard';
|
||||
const element = createGuardAttrs(namespace);
|
||||
|
||||
expect(element.tagName).toBe('SPAN');
|
||||
expect(element.getAttribute(namespace)).toBe('');
|
||||
expect(element.getAttribute('tabindex')).toBe('0');
|
||||
expect(element.getAttribute('style')).toBe('outline: none; opacity: 0; pointer-events: none; position: fixed;');
|
||||
});
|
||||
});
|
||||
50
core/platform/src/browsers/focusGuard/index.ts
Normal file
50
core/platform/src/browsers/focusGuard/index.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @name focusGuard
|
||||
* @category Browsers
|
||||
* @description Adds a pair of focus guards at the boundaries of the DOM tree to ensure consistent focus behavior
|
||||
*
|
||||
* @param {string} namespace - The namespace to use for the guard attributes
|
||||
* @returns {Object} - An object containing the selector, createGuard, and removeGuard functions
|
||||
*
|
||||
* @example
|
||||
* const guard = focusGuard();
|
||||
* guard.createGuard();
|
||||
* guard.removeGuard();
|
||||
*
|
||||
* @example
|
||||
* const guard = focusGuard('focus-guard');
|
||||
* guard.createGuard();
|
||||
* guard.removeGuard();
|
||||
*
|
||||
* @since 0.0.3
|
||||
*/
|
||||
export function focusGuard(namespace: string = 'focus-guard') {
|
||||
const guardAttr = `data-${namespace}`;
|
||||
|
||||
const createGuard = () => {
|
||||
const edges = document.querySelectorAll(`[${guardAttr}]`);
|
||||
|
||||
document.body.insertAdjacentElement('afterbegin', edges[0] ?? createGuardAttrs(guardAttr));
|
||||
document.body.insertAdjacentElement('beforeend', edges[1] ?? createGuardAttrs(guardAttr));
|
||||
};
|
||||
|
||||
const removeGuard = () => {
|
||||
document.querySelectorAll(`[${guardAttr}]`).forEach((element) => element.remove());
|
||||
};
|
||||
|
||||
return {
|
||||
selector: guardAttr,
|
||||
createGuard,
|
||||
removeGuard,
|
||||
};
|
||||
}
|
||||
|
||||
export function createGuardAttrs(namespace: string) {
|
||||
const element = document.createElement('span');
|
||||
|
||||
element.setAttribute(namespace, '');
|
||||
element.setAttribute('tabindex', '0');
|
||||
element.setAttribute('style', 'outline: none; opacity: 0; pointer-events: none; position: fixed;');
|
||||
|
||||
return element;
|
||||
}
|
||||
1
core/platform/src/browsers/index.ts
Normal file
1
core/platform/src/browsers/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './focusGuard';
|
||||
47
core/platform/src/multi/debounce/index.ts
Normal file
47
core/platform/src/multi/debounce/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
export interface DebounceOptions {
|
||||
/**
|
||||
* Call the function on the leading edge of the timeout, instead of waiting for the trailing edge
|
||||
*/
|
||||
readonly immediate?: boolean;
|
||||
|
||||
/**
|
||||
* Call the function on the trailing edge with the last used arguments.
|
||||
* Result of call is from previous call
|
||||
*/
|
||||
readonly trailing?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_DEBOUNCE_OPTIONS: DebounceOptions = {
|
||||
trailing: true,
|
||||
}
|
||||
|
||||
export function debounce<FnArguments extends unknown[], FnReturn>(
|
||||
fn: (...args: FnArguments) => PromiseLike<FnReturn> | FnReturn,
|
||||
timeout: number = 20,
|
||||
options: DebounceOptions = {},
|
||||
) {
|
||||
options = {
|
||||
...DEFAULT_DEBOUNCE_OPTIONS,
|
||||
...options,
|
||||
};
|
||||
|
||||
if (!Number.isFinite(timeout) || timeout <= 0)
|
||||
throw new TypeError('Debounce timeout must be a positive number');
|
||||
|
||||
// Last result for leading edge
|
||||
let leadingValue: PromiseLike<FnReturn> | FnReturn;
|
||||
|
||||
// Debounce timeout id
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
// Promises to be resolved when debounce is finished
|
||||
let resolveList: Array<(value: unknown) => void> = [];
|
||||
|
||||
// State of currently resolving promise
|
||||
let currentResolve: Promise<FnReturn>;
|
||||
|
||||
// Trailing call information
|
||||
let trailingArgs: unknown[];
|
||||
|
||||
|
||||
}
|
||||
28
core/platform/src/multi/global/index.ts
Normal file
28
core/platform/src/multi/global/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// TODO: tests
|
||||
|
||||
/**
|
||||
* @name _global
|
||||
* @category Multi
|
||||
* @description Global object that works in any environment
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
export const _global =
|
||||
typeof globalThis !== 'undefined'
|
||||
? globalThis
|
||||
: typeof window !== 'undefined'
|
||||
? window
|
||||
: typeof global !== 'undefined'
|
||||
? global
|
||||
: typeof self !== 'undefined'
|
||||
? self
|
||||
: undefined;
|
||||
|
||||
/**
|
||||
* @name isClient
|
||||
* @category Multi
|
||||
* @description Check if the current environment is the client
|
||||
*
|
||||
* @since 0.0.1
|
||||
*/
|
||||
export const isClient = typeof window !== 'undefined' && typeof document !== 'undefined';
|
||||
2
core/platform/src/multi/index.ts
Normal file
2
core/platform/src/multi/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './global';
|
||||
// export * from './debounce';
|
||||
6
core/platform/tsconfig.json
Normal file
6
core/platform/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "@robonen/tsconfig/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM"]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user